Skip to content

Contributions

An integration’s entry module exports two functions. probe() reports what the host looks like, and contribute() returns what the integration wants applied to the project. Noemata calls both on every reconcile, in dependency order, and merges the results of every installed integration before writing anything.

The context

Both functions receive a context object with these fields:

FieldTypeWhat it holds
platform'darwin' | 'linux' | 'win32'The host operating system.
archstringThe host architecture, as Node reports it.
homestringThe user’s home directory.
workspaceDirstringThe project directory, where noemata.json lives.
gitRootstring | undefinedThe root of the repository containing the project, when there is one.
packageDirstringThe integration package’s own root directory, for reading files shipped in the package.
loggerLoggerA logger with debug, info, warn, and error. Output goes to the reconcile log rather than the terminal.
databaseobject | undefinedThe project’s configured database, as { clickhouse: { hosts } }. An integration observing that database can treat its presence as availability.

contribute() receives the same fields plus:

FieldTypeWhat it holds
settingsobjectThe integration’s entry under integrations.installed, validated against its settings schema.
otlp{ endpoint, protocol }The collector’s OTLP endpoint and protocol (http/protobuf, grpc, or http/json), for pointing the observed software at Noemata.
integrationsIntegrationsThe other installed integrations: ids, and get(id). See Dependencies.
store{ readFile(path) }Reads a file as it will be once every earlier integration’s edits are applied. Returns null when the file does not exist. Use it, rather than the file system, before deciding whether to edit a file.

probe()

probe(ctx): Promise<ProbeResult>

probe() answers whether the software the integration observes is present on this host. It must not prompt, write files, or take longer than a few seconds, since it runs on every reconcile and, for integrations Noemata ships, during the setup wizard’s detection step.

FieldTypeWhat it holds
availablebooleanWhether the observed software was found.
detailstringOne line shown next to the result, such as the version found or why detection failed.
capabilitiesstring[]Names for what the integration can collect on this host, such as native-otel or transcripts. Shown in the detection report.
metaanyFacts contribute() needs, such as a binary path or a config file location. Typed from the return type of probe() in TypeScript.

A package with no probe() is treated as always available.

contribute()

contribute(probe: ProbeResult, ctx): Contribution | Promise<Contribution>

Every field of the returned object is optional. A package that only ships frames and skills needs no entry module at all; those install from the manifest.

collector

Configuration for the project’s OpenTelemetry collector. Noemata generates one collector configuration from every integration’s fragment plus its own base pipelines, which carry the always-on OTLP receiver and the exporter to the database.

collector: {
receivers: { 'docker_stats/docker': { endpoint: 'unix:///var/run/docker.sock' } },
processors: { 'filter/docker': { ... } },
exporters: { ... },
pipelines: {
metrics: { receivers: ['docker_stats/docker'], processors: ['filter/docker'] },
},
basePipelineProcessors: {
logs: ['transform/claude_desktop'],
},
}
  • receivers, processors, exporters define components. Each key is the component’s name in the collector configuration and must be namespaced as <type>/<integration>, for example filelog/claude_code. Two integrations defining the same key is an error at reconcile time.
  • pipelines lists, per signal, the component names to wire into a pipeline owned by this integration. The generated pipeline is named <signal>/<integration> and always ends in the database exporter, so exporters is only needed for an additional destination. A signal must appear in the manifest’s signals for its pipeline to be generated.
  • basePipelineProcessors lists, per signal, processor names to insert into the base pipelines fed by the shared OTLP receiver. Use it when the integration must transform data pushed to the collector by an SDK whose receiver it does not own. Define the processors under processors; list only their names here. They run after the memory limiter and before the shared resource-detection processor.

When probe() reports the software absent, Noemata still writes the component definitions but leaves the integration’s <signal>/<integration> pipelines out of the configuration, so a stopped dependency never prevents the collector from starting.

fileEdits

Edits to files the user owns, such as the observed application’s own configuration. Each edit names a file, describes itself in one line, and computes the file’s new content:

fileEdits: [
{
path: path.join(ctx.home, '.claude', 'settings.json'),
description: 'Point Claude Code telemetry at the Noemata collector',
compute(current) {
const settings = current ? JSON.parse(current) : {};
settings.env = { ...settings.env, OTEL_EXPORTER_OTLP_ENDPOINT: ctx.otlp.endpoint };
return JSON.stringify(settings, null, 2);
},
},
];

compute(current) receives the file’s content as it will be after every earlier integration’s edits, or null when the file does not exist, and returns the desired content. Return null, or the same content, to leave the file alone. Noemata shows the user a diff between current and the result, and regenerates that diff when an earlier edit to the same file is accepted or rejected.

Edits inside the repository apply without a prompt. Edits outside it, such as the example above, are governed by integrations.external_edits and, for packages installed from npm, additionally by trusted on the package’s entry under integrations.packages; see Trusting a package. An edit the project does not permit is skipped, and reconcile prints the edit’s description with the setting that would allow it. Keep the description specific, since that line is what the user sees when deciding.

The SDK exports mergeJSONSettings(current, patch) for the common case of merging keys into a JSON settings file while preserving the user’s formatting and comments.

installSuggestions

What the user should install when probe() finds the software missing, or finds it without a capability the integration relies on. Each suggestion names the subject, explains why it is needed, and lists one or more ways to install it:

installSuggestions: [
{
subject: 'Docker',
description: 'Docker is needed to collect container metrics.',
required: true,
methods: [
{ id: 'brew', label: 'Homebrew', command: 'brew install --cask docker' },
{ id: 'manual', label: 'Docker Desktop', url: 'https://docs.docker.com/desktop/' },
],
},
];

id is one of brew, npm, pnpm, yarn, bun, pipx, uv, pip, docker, tarball, or manual. command is shown for the user to run; Noemata never runs it. required: true marks a subject without which the integration collects nothing, as opposed to one that adds a capability.

notes

Lines printed after reconcile for things the user has to do by hand, such as restarting a terminal so an edited environment takes effect. Keep each note to one sentence that names the action.

What an integration cannot do

  • Run arbitrary commands on the host. Contributions are declarative; the collector, the file system, and the workspace apply them.
  • Write files other than through fileEdits, the frame pack, and the skill pack. A probe() or contribute() that writes to disk directly bypasses the consent and diff flow, and its writes are not tracked or removable.
  • Read another integration’s contributions. The integrations view exposes manifests, settings, and probe results; collector fragments and file edits stay private, except as they become visible through store.readFile.