@cairnvibe/indexer 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -16,6 +16,7 @@ const llm_1 = require("./llm");
16
16
  const manifest_1 = require("./manifest");
17
17
  const diff_1 = require("./diff");
18
18
  const docs_1 = require("./docs");
19
+ const webmcp_1 = require("./webmcp");
19
20
  const init_1 = require("./init");
20
21
  const setup_1 = require("./setup");
21
22
  /**
@@ -180,6 +181,29 @@ async function main() {
180
181
  console.error(`wrote ${outPath}`);
181
182
  return;
182
183
  }
184
+ if (command === "webmcp") {
185
+ const manifestPath = node_path_1.default.join(node_path_1.default.resolve(dir), "ui-manifest.json");
186
+ if (!node_fs_1.default.existsSync(manifestPath)) {
187
+ console.error(`cairn webmcp: no ${manifestPath} — run \`cairn build ${dir}\` first.`);
188
+ process.exit(1);
189
+ }
190
+ const manifest = core_1.ManifestSchema.parse(JSON.parse(node_fs_1.default.readFileSync(manifestPath, "utf8")));
191
+ const component = (0, webmcp_1.generateWebMcpComponent)(manifest);
192
+ if (!component) {
193
+ console.error("cairn webmcp: no real, traced actions (apiCall-backed elements) found in this manifest — nothing safe to register. Nothing written.");
194
+ return;
195
+ }
196
+ const outPath = node_path_1.default.join(node_path_1.default.resolve(dir), "components", "CairnWebMcpTools.tsx");
197
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(outPath), { recursive: true });
198
+ node_fs_1.default.writeFileSync(outPath, component);
199
+ console.error(`wrote ${outPath}`);
200
+ console.error("");
201
+ console.error("Next step: add it once, near your Copilot widget (e.g. app/layout.tsx):");
202
+ console.error(' import { CairnWebMcpTools } from "./components/CairnWebMcpTools";');
203
+ console.error(" <CairnWebMcpTools />");
204
+ console.error("Re-run `cairn webmcp` after a fresh `cairn build` whenever this app's real actions change.");
205
+ return;
206
+ }
183
207
  console.error("usage:");
184
208
  console.error(" cairn setup [dir] (the one-command path: installs deps, asks for keys — skippable, wires the widget in, builds once, auto-rebuilds on future `npm run build`)");
185
209
  console.error(" cairn init <dir> (scaffolds the API route/server + .env.example, detects your framework — no prompts, no installs)");
@@ -188,6 +212,7 @@ async function main() {
188
212
  console.error(" cairn build <url> [--provider anthropic|groq] [--out <dir>] [--storage-state <file>] (any framework — crawls a running app; --storage-state replays a saved logged-in session for auth-gated apps)");
189
213
  console.error(" cairn diff <old-manifest.json> <new-manifest.json>");
190
214
  console.error(" cairn docs <dir> (reads <dir>/ui-manifest.json, writes <dir>/CAIRN_DOCS.md)");
215
+ console.error(" cairn webmcp <dir> (reads <dir>/ui-manifest.json, writes <dir>/components/CairnWebMcpTools.tsx — registers this app's real, traced actions as WebMCP tools any agent can call, not just Cairn)");
191
216
  process.exit(command ? 1 : 0);
192
217
  }
193
218
  main().catch((err) => {
package/dist/webmcp.js ADDED
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ // Cairn as a WebMCP *producer*, not just a consumer
3
+ // (https://webmachinelearning.github.io/webmcp/). The indexer already
4
+ // traces real, safe, mutating actions statically (l1-scan.ts's
5
+ // findApiCallIn — a real POST/PUT/PATCH/DELETE call an element's own
6
+ // handler already makes, never invented; GET is deliberately excluded, per
7
+ // ApiCallSchema's own doc comment, since a read-only call isn't an
8
+ // "action"). This turns that same traced set into real
9
+ // document.modelContext.registerTool() calls — so running `cairn build`
10
+ // doesn't just make a site usable by Cairn's own runtime, it also makes it
11
+ // agent-ready for any future agent that understands the standard, with zero
12
+ // Cairn lock-in (the generated component has no Cairn import at all).
13
+ //
14
+ // Pure formatting/codegen from an already-built manifest, no LLM call — same
15
+ // shape as docs.ts's generateDocsMarkdown.
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.generateWebMcpComponent = generateWebMcpComponent;
18
+ /** Turns a route into a name-safe prefix — "/" (home) has no segments to
19
+ * join, "/invoices" -> "invoices", "/agents/new" -> "agents-new". */
20
+ function routePrefix(route) {
21
+ const slug = route
22
+ .split("/")
23
+ .filter(Boolean)
24
+ .join("-")
25
+ .replace(/[^a-zA-Z0-9-]/g, "-");
26
+ return slug || "home";
27
+ }
28
+ /**
29
+ * Collects every real apiCall-backed element across the whole manifest into
30
+ * one flat, deduped tool list. Deduped by (id, method, url) together, not
31
+ * id alone — id is only ever guaranteed unique WITHIN a page (l1-scan
32
+ * assigns them per-page), so two different page-scoped elements can share
33
+ * an id by coincidence and must stay two separate tools; a genuinely
34
+ * global element (e.g. a layout nav action) instead appears on every
35
+ * page's own `elements` array with the IDENTICAL apiCall each time
36
+ * (assembleManifest spreads globalElements onto every page) — matching on
37
+ * the full key is what tells "really the same action, seen N times" apart
38
+ * from "two different actions that happen to share an id".
39
+ */
40
+ function collectTools(manifest) {
41
+ const seen = new Map();
42
+ const pages = [...manifest.pages].sort((a, b) => a.route.localeCompare(b.route));
43
+ for (const page of pages) {
44
+ for (const el of page.elements) {
45
+ if (!el.apiCall)
46
+ continue;
47
+ const key = `${el.id}::${el.apiCall.method}::${el.apiCall.url}`;
48
+ if (seen.has(key))
49
+ continue;
50
+ seen.set(key, {
51
+ name: `${routePrefix(page.route)}-${el.id}`,
52
+ description: el.does,
53
+ apiCall: el.apiCall,
54
+ });
55
+ }
56
+ }
57
+ return [...seen.values()];
58
+ }
59
+ function jsStringLiteral(value) {
60
+ return JSON.stringify(value);
61
+ }
62
+ /**
63
+ * Generates a real, self-contained TSX component ("use client") that
64
+ * registers every real apiCall-backed action in the manifest as a WebMCP
65
+ * tool, on mount, at the app root — one component, added once, covers every
66
+ * page (each tool's execute() fires the same same-origin fetch the client-
67
+ * side `do` verb's own apiCall fallback already does — see verb-executor.ts's
68
+ * executeApiCall — real session cookies via credentials: "same-origin", no
69
+ * request body since static tracing only ever captures method+url). Returns
70
+ * null when the manifest has no real apiCall-backed elements — nothing
71
+ * safe to register, so nothing to scaffold.
72
+ */
73
+ function generateWebMcpComponent(manifest) {
74
+ const tools = collectTools(manifest);
75
+ if (tools.length === 0)
76
+ return null;
77
+ const registrations = tools
78
+ .map((tool) => ` register({
79
+ name: ${jsStringLiteral(tool.name)},
80
+ description: ${jsStringLiteral(tool.description)},
81
+ inputSchema: { type: "object", properties: {} },
82
+ execute: async () => {
83
+ const res = await fetch(${jsStringLiteral(tool.apiCall.url)}, { method: ${jsStringLiteral(tool.apiCall.method)}, credentials: "same-origin" });
84
+ return { ok: res.ok, status: res.status };
85
+ },
86
+ });`)
87
+ .join("\n\n");
88
+ return `// Generated by \`cairn webmcp\` from commit ${jsStringLiteral(manifest.commit)} at ${manifest.generatedAt}.
89
+ // Do not edit by hand — regenerate instead (\`npx cairn webmcp <dir>\` after
90
+ // a fresh \`cairn build\`, whenever the app's real actions change).
91
+ //
92
+ // Registers this app's own real, traced actions
93
+ // (https://webmachinelearning.github.io/webmcp/) so ANY agent that
94
+ // understands the standard — not just Cairn — can discover and call them
95
+ // directly. No Cairn import here on purpose: this keeps working even in an
96
+ // app that later removes Cairn entirely, and costs nothing on a browser
97
+ // that doesn't implement WebMCP yet (the overwhelming majority right now —
98
+ // registerTool() is checked for and simply skipped if absent).
99
+ "use client";
100
+
101
+ import { useEffect } from "react";
102
+
103
+ export function CairnWebMcpTools() {
104
+ useEffect(() => {
105
+ const modelContext = (document as unknown as { modelContext?: any }).modelContext;
106
+ if (!modelContext?.registerTool) return;
107
+
108
+ let unregistered = false;
109
+ const handles: { remove?: () => void }[] = [];
110
+ const register = (tool: unknown) => {
111
+ void modelContext
112
+ .registerTool(tool)
113
+ .then((registered: { remove?: () => void }) => {
114
+ if (unregistered) registered?.remove?.();
115
+ else handles.push(registered);
116
+ })
117
+ .catch(() => {
118
+ // A page without a real WebMCP implementation, or a tool name
119
+ // this app already registered some other way — nothing to do.
120
+ });
121
+ };
122
+
123
+ ${registrations}
124
+
125
+ return () => {
126
+ unregistered = true;
127
+ for (const handle of handles) handle?.remove?.();
128
+ };
129
+ }, []);
130
+
131
+ return null;
132
+ }
133
+ `;
134
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cairnvibe/indexer",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Cairn's analyzer and installer (the `cairn` CLI) — scans Next.js source or crawls any running app, and scaffolds the backend either way.",
5
5
  "license": "MIT",
6
6
  "publishConfig": { "access": "public" },