Files
mesh-catalog/modules/umami/tools/index.ts
T
jschoubben e8061191c7 umami: complete, on the sdk — the first fully converted module
umami is now a whole module, not a manifest: its own API client, its
tools, and its provisioner all live in the module and build on
@novox/mesh-sdk.

- client.ts — umami's API client, moved out of the shared sdk into the
  module (ADR 0044); umami's tools and provisioner both import it.
- tools/ — umami_create_site / umami_delete_site on the sdk tool harness
  (registerModuleTools); the tool logic and client are the module's.
- provisioner/ — the adapter making umami a provider of the mesh
  'analytics' interface: a consumer contributes {domain}, receives
  {siteId, snippet, dashboard}. ~20 lines, because the watch/seal/grant
  loop is the sdk harness's.

Type-checks against the real mesh-sdk (tsc --noEmit clean); the manifest
parses against internal/catalogue. Remaining to actually run: build and
publish the module + its mesh-provision-umami-analytics image (the
placeholder digest), which is the pipeline's job.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-03 23:05:20 +02:00

62 lines
2.4 KiB
TypeScript

// umami's tools — moved here from the shared sdk (novox/hq ADR 0044). They import umami's own
// client (../client) and plug into the mesh through the sdk's tool harness. Editing them rebuilds
// umami and nothing else.
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
import { UmamiClient } from "../client.js";
export function getUmamiTools(umami: UmamiClient): ToolDefinition[] {
return [
{
name: "umami_create_site",
description: "Register a website in umami and return its tracking snippet.",
input: {
domain: { type: "string", description: "the site domain, e.g. my-app.example" },
name: { type: "string", description: "display name (defaults to the domain)" },
},
run: async (args) => {
const domain = String(args.domain);
const name = args.name ? String(args.name) : domain;
const token = await umami.getToken();
const existing = await umami.findWebsite(token, domain);
const site = existing ?? (await umami.createWebsite(token, domain, name));
return {
created: !existing,
id: site.id,
name: site.name,
domain: site.domain,
snippet: umami.snippet(site.id),
dashboard: umami.dashboard(site.id),
};
},
},
{
name: "umami_delete_site",
description: "Delete a website from umami (destructive).",
input: {
domain: { type: "string", description: "the site domain to delete" },
confirm: { type: "boolean", description: "must be true to delete" },
},
run: async (args) => {
if (args.confirm !== true) return { deleted: false, reason: "confirm must be true" };
const domain = String(args.domain);
const token = await umami.getToken();
const site = await umami.findWebsite(token, domain);
if (!site) return { deleted: false, reason: `no website with domain ${domain}` };
await umami.deleteWebsite(token, site.id);
return { deleted: true, id: site.id, domain: site.domain };
},
},
];
}
// The module contributes its tools as a function of its resolved environment; the client lives in
// the module, so a token being absent simply yields no tools rather than a failure.
registerModuleTools("umami", (env) => {
try {
return getUmamiTools(UmamiClient.fromEnv(env));
} catch {
return [];
}
});