@cairnvibe/indexer 0.2.2 → 0.2.4
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/init.js +84 -3
- package/dist/inject-widget.js +17 -4
- package/dist/manifest.js +23 -0
- package/dist/setup.js +34 -2
- package/package.json +1 -1
package/dist/init.js
CHANGED
|
@@ -35,7 +35,7 @@ function writeIfAbsent(filePath, content, result) {
|
|
|
35
35
|
node_fs_1.default.writeFileSync(filePath, content);
|
|
36
36
|
result.filesWritten.push(filePath);
|
|
37
37
|
}
|
|
38
|
-
function runInit(dir) {
|
|
38
|
+
function runInit(dir, options = {}) {
|
|
39
39
|
const absDir = node_path_1.default.resolve(dir);
|
|
40
40
|
const pkgPath = node_path_1.default.join(absDir, "package.json");
|
|
41
41
|
let pkg = {};
|
|
@@ -59,11 +59,23 @@ function runInit(dir) {
|
|
|
59
59
|
writeIfAbsent(node_path_1.default.join(absDir, ".env.example"), ENV_TEMPLATE, result);
|
|
60
60
|
if (result.framework === "next-app-router") {
|
|
61
61
|
writeIfAbsent(node_path_1.default.join(absDir, "app", "api", "copilot", "route.ts"), NEXT_APP_ROUTE, result);
|
|
62
|
-
|
|
62
|
+
const widgetProps = ['registeredActions={[]}', "onDo={(action, target) => { /* run it */ }}"];
|
|
63
|
+
if (options.voice) {
|
|
64
|
+
writeIfAbsent(node_path_1.default.join(absDir, "app", "api", "copilot", "speak", "route.ts"), NEXT_APP_SPEAK_ROUTE, result);
|
|
65
|
+
writeIfAbsent(node_path_1.default.join(absDir, "app", "api", "copilot", "transcribe", "route.ts"), NEXT_APP_TRANSCRIBE_ROUTE, result);
|
|
66
|
+
widgetProps.push('speakEndpoint="/api/copilot/speak"', 'transcribeEndpoint="/api/copilot/transcribe"');
|
|
67
|
+
}
|
|
68
|
+
result.nextSteps.push("1. cp .env.example .env and fill in your key(s).", "2. Add the widget to app/layout.tsx:", ' import { Copilot } from "@cairnvibe/sdk";', ` <Copilot ${widgetProps.join(" ")} />`, "3. npx cairn build . (scans this Next.js app's source)", "4. npm run dev, then ask it a question.");
|
|
63
69
|
}
|
|
64
70
|
else if (result.framework === "next-pages-router") {
|
|
65
71
|
writeIfAbsent(node_path_1.default.join(absDir, "pages", "api", "copilot.ts"), NEXT_PAGES_API_ROUTE, result);
|
|
66
|
-
|
|
72
|
+
const widgetProps = ['registeredActions={[]}', "onDo={(action, target) => { /* run it */ }}"];
|
|
73
|
+
if (options.voice) {
|
|
74
|
+
writeIfAbsent(node_path_1.default.join(absDir, "pages", "api", "copilot", "speak.ts"), NEXT_PAGES_SPEAK_ROUTE, result);
|
|
75
|
+
writeIfAbsent(node_path_1.default.join(absDir, "pages", "api", "copilot", "transcribe.ts"), NEXT_PAGES_TRANSCRIBE_ROUTE, result);
|
|
76
|
+
widgetProps.push('speakEndpoint="/api/copilot/speak"', 'transcribeEndpoint="/api/copilot/transcribe"');
|
|
77
|
+
}
|
|
78
|
+
result.nextSteps.push("1. cp .env.example .env and fill in your key(s).", "2. Add the widget to pages/_app.tsx:", ' import { Copilot } from "@cairnvibe/sdk";', ` <Copilot ${widgetProps.join(" ")} />`, "3. npx cairn build . (scans this Next.js app's source)", "4. npm run dev, then ask it a question.");
|
|
67
79
|
}
|
|
68
80
|
else {
|
|
69
81
|
writeIfAbsent(node_path_1.default.join(absDir, "cairn-server.cjs"), STANDALONE_SERVER, result);
|
|
@@ -125,6 +137,75 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|
|
125
137
|
res.status(result.status).json(result.body);
|
|
126
138
|
}
|
|
127
139
|
`;
|
|
140
|
+
// The exact shape examples/demo-app's already-working routes use — copied,
|
|
141
|
+
// not reinvented, since that's the one place in this repo these have
|
|
142
|
+
// actually been proven against real Deepgram calls.
|
|
143
|
+
const NEXT_APP_SPEAK_ROUTE = `import { createSpeakHandler } from "@cairnvibe/sdk/speak-server";
|
|
144
|
+
|
|
145
|
+
const handler = createSpeakHandler({ apiKey: process.env.DEEPGRAM_API_KEY ?? "" });
|
|
146
|
+
|
|
147
|
+
export async function POST(request: Request) {
|
|
148
|
+
const { text } = await request.json().catch(() => ({ text: "" }));
|
|
149
|
+
const result = await handler(text ?? "");
|
|
150
|
+
|
|
151
|
+
if ("error" in result.body) {
|
|
152
|
+
return Response.json(result.body, { status: result.status });
|
|
153
|
+
}
|
|
154
|
+
return new Response(result.body.audio, {
|
|
155
|
+
status: result.status,
|
|
156
|
+
headers: { "content-type": result.body.contentType },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
`;
|
|
160
|
+
const NEXT_APP_TRANSCRIBE_ROUTE = `import { NextResponse } from "next/server";
|
|
161
|
+
import { createTranscribeHandler } from "@cairnvibe/sdk/transcribe-server";
|
|
162
|
+
|
|
163
|
+
const handler = createTranscribeHandler({ apiKey: process.env.DEEPGRAM_API_KEY ?? "" });
|
|
164
|
+
|
|
165
|
+
export async function POST(request: Request) {
|
|
166
|
+
const contentType = request.headers.get("content-type") ?? "audio/webm";
|
|
167
|
+
const buffer = await request.arrayBuffer();
|
|
168
|
+
const result = await handler(buffer, contentType);
|
|
169
|
+
return NextResponse.json(result.body, { status: result.status });
|
|
170
|
+
}
|
|
171
|
+
`;
|
|
172
|
+
const NEXT_PAGES_SPEAK_ROUTE = `import type { NextApiRequest, NextApiResponse } from "next";
|
|
173
|
+
import { createSpeakHandler } from "@cairnvibe/sdk/speak-server";
|
|
174
|
+
|
|
175
|
+
const handler = createSpeakHandler({ apiKey: process.env.DEEPGRAM_API_KEY ?? "" });
|
|
176
|
+
|
|
177
|
+
export default async function speak(req: NextApiRequest, res: NextApiResponse) {
|
|
178
|
+
if (req.method !== "POST") return res.status(405).end();
|
|
179
|
+
const result = await handler(req.body?.text ?? "");
|
|
180
|
+
if ("error" in result.body) {
|
|
181
|
+
return res.status(result.status).json(result.body);
|
|
182
|
+
}
|
|
183
|
+
res.status(result.status).setHeader("content-type", result.body.contentType).send(Buffer.from(result.body.audio));
|
|
184
|
+
}
|
|
185
|
+
`;
|
|
186
|
+
const NEXT_PAGES_TRANSCRIBE_ROUTE = `import type { NextApiRequest, NextApiResponse } from "next";
|
|
187
|
+
import { createTranscribeHandler } from "@cairnvibe/sdk/transcribe-server";
|
|
188
|
+
|
|
189
|
+
const handler = createTranscribeHandler({ apiKey: process.env.DEEPGRAM_API_KEY ?? "" });
|
|
190
|
+
|
|
191
|
+
// Pages Router's default body parser only handles JSON/form bodies — raw
|
|
192
|
+
// audio bytes need this off so the handler gets the untouched buffer.
|
|
193
|
+
export const config = { api: { bodyParser: false } };
|
|
194
|
+
|
|
195
|
+
async function readRawBody(req: NextApiRequest): Promise<Buffer> {
|
|
196
|
+
const chunks: Buffer[] = [];
|
|
197
|
+
for await (const chunk of req) chunks.push(chunk as Buffer);
|
|
198
|
+
return Buffer.concat(chunks);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export default async function transcribe(req: NextApiRequest, res: NextApiResponse) {
|
|
202
|
+
if (req.method !== "POST") return res.status(405).end();
|
|
203
|
+
const contentType = req.headers["content-type"] ?? "audio/webm";
|
|
204
|
+
const buffer = await readRawBody(req);
|
|
205
|
+
const result = await handler(buffer, contentType);
|
|
206
|
+
res.status(result.status).json(result.body);
|
|
207
|
+
}
|
|
208
|
+
`;
|
|
128
209
|
const STANDALONE_SERVER = `// cairn-server.cjs — generated by \`cairn init\`. Any backend framework
|
|
129
210
|
// works here (createCopilotHandler is plain Node) — this is just the
|
|
130
211
|
// simplest one to scaffold. Run: node cairn-server.cjs
|
package/dist/inject-widget.js
CHANGED
|
@@ -33,7 +33,20 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
33
33
|
const node_path_1 = __importDefault(require("node:path"));
|
|
34
34
|
const ts_morph_1 = require("ts-morph");
|
|
35
35
|
const WRAPPER_COMPONENT_NAME = "CairnCopilot";
|
|
36
|
-
function wrapperSource() {
|
|
36
|
+
function wrapperSource(voice) {
|
|
37
|
+
// Real bug this closes: choosing voice during `cairn setup` used to save a
|
|
38
|
+
// DEEPGRAM_API_KEY that nothing ever read — the generated wrapper never
|
|
39
|
+
// passed speakEndpoint/transcribeEndpoint/realtimeUrl, so the widget had
|
|
40
|
+
// no way to know voice existed regardless of whether a valid key was
|
|
41
|
+
// configured. These routes only exist when setup.ts asked init.ts to
|
|
42
|
+
// scaffold them (voice: true) — never reference them here unwired to a
|
|
43
|
+
// real backend. realtimeUrl points at the port `cairn-realtime --with`
|
|
44
|
+
// (setup.ts rewrites the dev script to start it alongside next dev) —
|
|
45
|
+
// without that, a widget with realtimeUrl set just fails to connect,
|
|
46
|
+
// which reads as "voice doesn't work" with no clue why.
|
|
47
|
+
const voiceProps = voice
|
|
48
|
+
? '\n speakEndpoint="/api/copilot/speak"\n transcribeEndpoint="/api/copilot/transcribe"\n realtimeUrl="ws://localhost:3010"'
|
|
49
|
+
: "";
|
|
37
50
|
return `"use client";
|
|
38
51
|
|
|
39
52
|
import { Copilot } from "@cairnvibe/sdk";
|
|
@@ -47,7 +60,7 @@ export function ${WRAPPER_COMPONENT_NAME}() {
|
|
|
47
60
|
registeredActions={[]}
|
|
48
61
|
onDo={(action, target) => {
|
|
49
62
|
// run it through your own auth
|
|
50
|
-
}}
|
|
63
|
+
}}${voiceProps}
|
|
51
64
|
/>
|
|
52
65
|
);
|
|
53
66
|
}
|
|
@@ -70,7 +83,7 @@ function toPosixRelativeImport(fromFile, toFileNoExt) {
|
|
|
70
83
|
rel = `./${rel}`;
|
|
71
84
|
return rel;
|
|
72
85
|
}
|
|
73
|
-
function injectWidget(dir, framework) {
|
|
86
|
+
function injectWidget(dir, framework, options = {}) {
|
|
74
87
|
const absDir = node_path_1.default.resolve(dir);
|
|
75
88
|
const target = findLayoutFile(absDir, framework);
|
|
76
89
|
if (!target) {
|
|
@@ -88,7 +101,7 @@ function injectWidget(dir, framework) {
|
|
|
88
101
|
const wrapperPath = node_path_1.default.join(absDir, "components", `${WRAPPER_COMPONENT_NAME}${ext}`);
|
|
89
102
|
if (!node_fs_1.default.existsSync(wrapperPath)) {
|
|
90
103
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(wrapperPath), { recursive: true });
|
|
91
|
-
node_fs_1.default.writeFileSync(wrapperPath, wrapperSource());
|
|
104
|
+
node_fs_1.default.writeFileSync(wrapperPath, wrapperSource(!!options.voice));
|
|
92
105
|
}
|
|
93
106
|
const importPath = toPosixRelativeImport(target, wrapperPath.slice(0, -ext.length));
|
|
94
107
|
const widgetJsx = `<${WRAPPER_COMPONENT_NAME} />`;
|
package/dist/manifest.js
CHANGED
|
@@ -27,6 +27,28 @@ function assembleManifest(rootDir, facts, l2, l3) {
|
|
|
27
27
|
conflicts: l2.conflicts,
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
+
const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
31
|
+
/**
|
|
32
|
+
* Turns l1-scan's traced `"POST /api/items/${id}"`-shaped string into
|
|
33
|
+
* structured, executable data — this is what lets a `do` action actually
|
|
34
|
+
* run (verb-executor.ts) instead of only ever describing itself. Only
|
|
35
|
+
* real mutating methods count as an action; `"navigate ..."` (a Link) and
|
|
36
|
+
* a bare GET aren't "do" material — see ApiCallSchema's doc comment in
|
|
37
|
+
* @cairnvibe/core for the safety reasoning (bounded to calls a human
|
|
38
|
+
* developer already wrote and shipped, nothing invented at runtime).
|
|
39
|
+
*/
|
|
40
|
+
function parseApiCall(handlerCall) {
|
|
41
|
+
if (!handlerCall)
|
|
42
|
+
return null;
|
|
43
|
+
const spaceIndex = handlerCall.indexOf(" ");
|
|
44
|
+
if (spaceIndex === -1)
|
|
45
|
+
return null;
|
|
46
|
+
const method = handlerCall.slice(0, spaceIndex);
|
|
47
|
+
const url = handlerCall.slice(spaceIndex + 1);
|
|
48
|
+
if (!MUTATING_METHODS.has(method) || !url)
|
|
49
|
+
return null;
|
|
50
|
+
return { method: method, url };
|
|
51
|
+
}
|
|
30
52
|
function toManifestElement(el, elDesc, baseEvidence) {
|
|
31
53
|
const evidence = [baseEvidence];
|
|
32
54
|
if (el.handlerCall)
|
|
@@ -41,6 +63,7 @@ function toManifestElement(el, elDesc, baseEvidence) {
|
|
|
41
63
|
does: elDesc?.does ?? "Unknown — no description generated for this element.",
|
|
42
64
|
confidence: elDesc?.confidence ?? 0,
|
|
43
65
|
evidence,
|
|
66
|
+
apiCall: parseApiCall(el.handlerCall),
|
|
44
67
|
};
|
|
45
68
|
}
|
|
46
69
|
function elementFallbackSelector(el) {
|
package/dist/setup.js
CHANGED
|
@@ -203,6 +203,18 @@ async function runSetup(dir) {
|
|
|
203
203
|
], 1);
|
|
204
204
|
const deepgramKey = voiceChoice === "deepgram" ? await (0, prompt_1.askOptional)("Paste your DEEPGRAM_API_KEY: ") : null;
|
|
205
205
|
(0, prompt_1.closePrompts)();
|
|
206
|
+
// 3b. Scaffold the speak/transcribe backend routes now that we know
|
|
207
|
+
// whether voice was actually chosen — runInit is idempotent (never
|
|
208
|
+
// overwrites), so calling it again here is safe. Real bug this closes:
|
|
209
|
+
// voice used to write only the key, never the routes or the widget props
|
|
210
|
+
// that would ever call them — "on" did nothing beyond saving a string
|
|
211
|
+
// nothing read.
|
|
212
|
+
const wantsVoice = voiceChoice === "deepgram";
|
|
213
|
+
if (wantsVoice) {
|
|
214
|
+
const voiceInit = (0, init_1.runInit)(dir, { voice: true });
|
|
215
|
+
for (const f of voiceInit.filesWritten)
|
|
216
|
+
console.log(` wrote ${node_path_1.default.relative(absDir, f) || f}`);
|
|
217
|
+
}
|
|
206
218
|
// 4. Write a real .env (not just .env.example) with whatever was actually given.
|
|
207
219
|
const envLines = [];
|
|
208
220
|
if (provider === "anthropic")
|
|
@@ -224,9 +236,10 @@ async function runSetup(dir) {
|
|
|
224
236
|
// deliberately doesn't do. Falls back to printing instructions on
|
|
225
237
|
// anything it can't confidently parse.
|
|
226
238
|
const framework = init.framework;
|
|
227
|
-
const inject = (0, inject_widget_1.injectWidget)(dir, framework);
|
|
239
|
+
const inject = (0, inject_widget_1.injectWidget)(dir, framework, { voice: wantsVoice });
|
|
228
240
|
if (inject.injected) {
|
|
229
|
-
|
|
241
|
+
const voiceNote = wantsVoice ? " — wired for voice (speak + transcribe)" : "";
|
|
242
|
+
console.log((0, ui_1.green)(`✓ wired the widget into ${node_path_1.default.relative(absDir, inject.filePath)} (via a new components/CairnCopilot.tsx wrapper)${voiceNote}`));
|
|
230
243
|
}
|
|
231
244
|
else {
|
|
232
245
|
console.log(`\nWidget not auto-wired (${inject.reason}). Add it yourself:`);
|
|
@@ -282,5 +295,24 @@ async function runSetup(dir) {
|
|
|
282
295
|
console.log((0, ui_1.dim)("(--if-configured means a build with no key set yet skips this step instead of failing the whole build —"));
|
|
283
296
|
console.log((0, ui_1.dim)(" set the same key as an environment variable on whatever platform you deploy to.)"));
|
|
284
297
|
}
|
|
298
|
+
// 8. Wire the realtime voice relay into the normal dev workflow — the
|
|
299
|
+
// other real half of "voice was completely unwired." realtimeUrl on the
|
|
300
|
+
// widget (wired above) just fails to connect if nothing's actually
|
|
301
|
+
// listening on that port; found live, and indistinguishable from "voice
|
|
302
|
+
// doesn't work" with zero indication that a whole separate process needs
|
|
303
|
+
// to be running. `cairn-realtime --with "<original dev command>"` runs
|
|
304
|
+
// both from the one command a project's dev workflow already uses,
|
|
305
|
+
// instead of a second terminal nobody remembers to open. Wraps whatever
|
|
306
|
+
// `dev` already does (a custom server, Turbopack, anything) rather than
|
|
307
|
+
// replacing it — the realtime relay runs alongside it, not instead of it.
|
|
308
|
+
if (wantsVoice && pkg?.scripts?.dev && !pkg.scripts.dev.includes("cairn-realtime")) {
|
|
309
|
+
const pkgPath = node_path_1.default.join(absDir, "package.json");
|
|
310
|
+
const fresh = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf8"));
|
|
311
|
+
const originalDev = fresh.scripts.dev;
|
|
312
|
+
fresh.scripts.dev = `cairn-realtime --port 3010 --with ${JSON.stringify(originalDev)}`;
|
|
313
|
+
node_fs_1.default.writeFileSync(pkgPath, JSON.stringify(fresh, null, 2) + "\n");
|
|
314
|
+
console.log((0, ui_1.green)('✓ wired the realtime voice relay into `npm run dev` — it now starts alongside your app automatically.'));
|
|
315
|
+
console.log((0, ui_1.dim)("(a missing/invalid Deepgram key skips voice only, never blocks your app's own dev server from starting.)"));
|
|
316
|
+
}
|
|
285
317
|
console.log(`\n${(0, ui_1.bold)("Done.")} \`npm run dev\` and ask it something.`);
|
|
286
318
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cairnvibe/indexer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
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" },
|