@cairnvibe/indexer 0.2.1 → 0.2.3
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/ensure-transpile.js +113 -0
- package/dist/init.js +84 -3
- package/dist/inject-widget.js +84 -19
- package/dist/setup.js +30 -3
- package/package.json +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// `cairn setup`'s other departure from `init`'s "never touch an existing
|
|
3
|
+
// file" rule, for the same class of reason as inject-widget.ts: without
|
|
4
|
+
// this, @cairnvibe/sdk and @cairnvibe/core — which ship raw, untranspiled
|
|
5
|
+
// .tsx/.ts as their main entry deliberately, so bundlers apply the
|
|
6
|
+
// *consuming* project's own JSX/TS settings — have no instruction telling
|
|
7
|
+
// Next.js to transform that source at all. Found live, not theoretical:
|
|
8
|
+
// a real project on Next.js 16 + Turbopack failed cold with "Unknown
|
|
9
|
+
// module type" on @cairnvibe/sdk/src/index.tsx the moment `next dev` ran,
|
|
10
|
+
// because nothing in that project's next.config.ts listed it in
|
|
11
|
+
// transpilePackages. Fixing that one project by hand isn't the fix — every
|
|
12
|
+
// consuming project needs this, automatically, which is what this does.
|
|
13
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
14
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.ensureTranspilePackages = ensureTranspilePackages;
|
|
18
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
19
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
20
|
+
const ts_morph_1 = require("ts-morph");
|
|
21
|
+
const REQUIRED_PACKAGES = ["@cairnvibe/sdk", "@cairnvibe/core"];
|
|
22
|
+
const CONFIG_CANDIDATES = ["next.config.ts", "next.config.mjs", "next.config.js", "next.config.cjs"];
|
|
23
|
+
function findConfigFile(absDir) {
|
|
24
|
+
for (const name of CONFIG_CANDIDATES) {
|
|
25
|
+
const p = node_path_1.default.join(absDir, name);
|
|
26
|
+
if (node_fs_1.default.existsSync(p))
|
|
27
|
+
return p;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/** Resolves `export default X` / `module.exports = X` down to the actual
|
|
32
|
+
* object literal, following one level of `const nextConfig = {...}`
|
|
33
|
+
* indirection — covers the two shapes basically every real Next.js
|
|
34
|
+
* config file uses. Anything else (a config wrapped in a plugin function
|
|
35
|
+
* call like `withSentryConfig(nextConfig)`) returns null on purpose —
|
|
36
|
+
* safely falling back to printed instructions beats guessing which
|
|
37
|
+
* argument of an arbitrary function call is the "real" config. */
|
|
38
|
+
function resolveConfigObject(sf, expr) {
|
|
39
|
+
if (!expr)
|
|
40
|
+
return null;
|
|
41
|
+
if (expr.getKind() === ts_morph_1.SyntaxKind.ObjectLiteralExpression)
|
|
42
|
+
return expr;
|
|
43
|
+
if (expr.getKind() === ts_morph_1.SyntaxKind.Identifier) {
|
|
44
|
+
const varDecl = sf.getVariableDeclaration(expr.getText());
|
|
45
|
+
const init = varDecl?.getInitializer();
|
|
46
|
+
if (init?.getKind() === ts_morph_1.SyntaxKind.ObjectLiteralExpression)
|
|
47
|
+
return init;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function findExportedConfigObject(sf) {
|
|
52
|
+
// `export default X;`
|
|
53
|
+
const defaultExport = sf.getExportAssignments()[0];
|
|
54
|
+
if (defaultExport) {
|
|
55
|
+
const resolved = resolveConfigObject(sf, defaultExport.getExpression());
|
|
56
|
+
if (resolved)
|
|
57
|
+
return resolved;
|
|
58
|
+
}
|
|
59
|
+
// `module.exports = X;`
|
|
60
|
+
const moduleExports = sf
|
|
61
|
+
.getDescendantsOfKind(ts_morph_1.SyntaxKind.BinaryExpression)
|
|
62
|
+
.find((b) => b.getOperatorToken().getText() === "=" && b.getLeft().getText() === "module.exports");
|
|
63
|
+
if (moduleExports) {
|
|
64
|
+
const resolved = resolveConfigObject(sf, moduleExports.getRight());
|
|
65
|
+
if (resolved)
|
|
66
|
+
return resolved;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
function ensureTranspilePackages(dir) {
|
|
71
|
+
const absDir = node_path_1.default.resolve(dir);
|
|
72
|
+
const existing = findConfigFile(absDir);
|
|
73
|
+
if (!existing) {
|
|
74
|
+
// No config at all yet — the simplest possible one, ESM since that's
|
|
75
|
+
// what every current Next.js version accepts for a fresh project.
|
|
76
|
+
const newPath = node_path_1.default.join(absDir, "next.config.mjs");
|
|
77
|
+
node_fs_1.default.writeFileSync(newPath, `/** @type {import('next').NextConfig} */\nconst nextConfig = {\n transpilePackages: ${JSON.stringify(REQUIRED_PACKAGES)},\n};\n\nexport default nextConfig;\n`);
|
|
78
|
+
return { ok: true, filePath: newPath, created: true };
|
|
79
|
+
}
|
|
80
|
+
const relTarget = node_path_1.default.relative(absDir, existing) || existing;
|
|
81
|
+
const manualHint = `add manually: transpilePackages: ${JSON.stringify(REQUIRED_PACKAGES)}`;
|
|
82
|
+
try {
|
|
83
|
+
const project = new ts_morph_1.Project({ useInMemoryFileSystem: false, skipAddingFilesFromTsConfig: true });
|
|
84
|
+
const sf = project.addSourceFileAtPath(existing);
|
|
85
|
+
const configObject = findExportedConfigObject(sf);
|
|
86
|
+
if (!configObject) {
|
|
87
|
+
return { ok: false, reason: `couldn't confidently find the config object in ${relTarget} (it may be wrapped in a plugin function) — ${manualHint}` };
|
|
88
|
+
}
|
|
89
|
+
const existingProp = configObject.getProperty("transpilePackages");
|
|
90
|
+
if (existingProp?.getKind() === ts_morph_1.SyntaxKind.PropertyAssignment) {
|
|
91
|
+
const initializer = existingProp.asKindOrThrow(ts_morph_1.SyntaxKind.PropertyAssignment).getInitializer();
|
|
92
|
+
if (initializer?.getKind() !== ts_morph_1.SyntaxKind.ArrayLiteralExpression) {
|
|
93
|
+
return { ok: false, reason: `${relTarget}'s transpilePackages isn't a plain array — ${manualHint}` };
|
|
94
|
+
}
|
|
95
|
+
const arr = initializer.asKindOrThrow(ts_morph_1.SyntaxKind.ArrayLiteralExpression);
|
|
96
|
+
const current = arr.getElements().map((e) => e.getText().replace(/^["']|["']$/g, ""));
|
|
97
|
+
const toAdd = REQUIRED_PACKAGES.filter((p) => !current.includes(p));
|
|
98
|
+
if (toAdd.length === 0) {
|
|
99
|
+
return { ok: false, reason: `${relTarget} already lists these packages — leaving it alone` };
|
|
100
|
+
}
|
|
101
|
+
for (const pkg of toAdd)
|
|
102
|
+
arr.addElement(`"${pkg}"`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
configObject.addPropertyAssignment({ name: "transpilePackages", initializer: JSON.stringify(REQUIRED_PACKAGES) });
|
|
106
|
+
}
|
|
107
|
+
sf.saveSync();
|
|
108
|
+
return { ok: true, filePath: existing };
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
return { ok: false, reason: `couldn't safely modify ${relTarget} (${err.message}) — ${manualHint}` };
|
|
112
|
+
}
|
|
113
|
+
}
|
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
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// `cairn setup`'s one departure from `init`'s "never touch an existing
|
|
3
|
-
// file" rule — but only for this one, narrow, reversible edit (
|
|
4
|
-
// import + one JSX tag
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// file" rule — but only for this one, narrow, reversible edit (a new
|
|
4
|
+
// wrapper component file, plus one import + one JSX tag in the real
|
|
5
|
+
// layout), and only ever via a real AST parse, never blind string
|
|
6
|
+
// splicing. Any structure this doesn't recognize falls back to printing
|
|
7
|
+
// the two-line manual instruction instead of guessing — the same "don't
|
|
8
|
+
// corrupt what you don't understand" discipline `init` uses for whole
|
|
9
|
+
// files, applied here at the node level.
|
|
10
|
+
//
|
|
11
|
+
// Real bug this fixes, found by testing against an actual project, not
|
|
12
|
+
// a synthetic fixture: an earlier version inserted `<Copilot
|
|
13
|
+
// onDo={(action, target) => {...}} />` directly into app/layout.tsx.
|
|
14
|
+
// layout.tsx is a Server Component by default (App Router) — React
|
|
15
|
+
// Server Components cannot accept a plain inline function as a prop on
|
|
16
|
+
// a Client Component, which <Copilot/> is ("Event handlers cannot be
|
|
17
|
+
// passed to Client Component props"). The working example app
|
|
18
|
+
// (examples/demo-app/components/CopilotWithActions.tsx) already solved
|
|
19
|
+
// this the right way: a small "use client" wrapper component that
|
|
20
|
+
// *defines* onDo itself, so no function ever crosses the server/client
|
|
21
|
+
// boundary — layout.tsx only ever references the wrapper by name, with
|
|
22
|
+
// zero function props. This module now generates that same wrapper
|
|
23
|
+
// instead of inlining Copilot directly, for both App Router and Pages
|
|
24
|
+
// Router (Pages Router doesn't strictly need it — no RSC boundary
|
|
25
|
+
// there — but the same shape avoids a special case and matches the one
|
|
26
|
+
// real, working example this project has).
|
|
9
27
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
28
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
29
|
};
|
|
@@ -14,7 +32,35 @@ exports.injectWidget = injectWidget;
|
|
|
14
32
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
15
33
|
const node_path_1 = __importDefault(require("node:path"));
|
|
16
34
|
const ts_morph_1 = require("ts-morph");
|
|
17
|
-
const
|
|
35
|
+
const WRAPPER_COMPONENT_NAME = "CairnCopilot";
|
|
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, so the widget had no way to
|
|
40
|
+
// know voice existed regardless of whether a valid key was configured.
|
|
41
|
+
// These routes only exist when ensure-transpile.ts's sibling in setup.ts
|
|
42
|
+
// asked init.ts to scaffold them (voice: true) — never reference them here
|
|
43
|
+
// unwired to a real backend route.
|
|
44
|
+
const voiceProps = voice ? '\n speakEndpoint="/api/copilot/speak"\n transcribeEndpoint="/api/copilot/transcribe"' : "";
|
|
45
|
+
return `"use client";
|
|
46
|
+
|
|
47
|
+
import { Copilot } from "@cairnvibe/sdk";
|
|
48
|
+
|
|
49
|
+
// A small client wrapper so the layout/app file (a server component, for
|
|
50
|
+
// metadata etc.) never has to pass a function prop across the server/client
|
|
51
|
+
// boundary — see the comment in inject-widget.ts for why that fails.
|
|
52
|
+
export function ${WRAPPER_COMPONENT_NAME}() {
|
|
53
|
+
return (
|
|
54
|
+
<Copilot
|
|
55
|
+
registeredActions={[]}
|
|
56
|
+
onDo={(action, target) => {
|
|
57
|
+
// run it through your own auth
|
|
58
|
+
}}${voiceProps}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
18
64
|
function findLayoutFile(absDir, framework) {
|
|
19
65
|
const candidates = framework === "next-app-router"
|
|
20
66
|
? ["app/layout.tsx", "app/layout.jsx"]
|
|
@@ -26,17 +72,34 @@ function findLayoutFile(absDir, framework) {
|
|
|
26
72
|
}
|
|
27
73
|
return null;
|
|
28
74
|
}
|
|
29
|
-
function
|
|
75
|
+
function toPosixRelativeImport(fromFile, toFileNoExt) {
|
|
76
|
+
let rel = node_path_1.default.relative(node_path_1.default.dirname(fromFile), toFileNoExt).split(node_path_1.default.sep).join("/");
|
|
77
|
+
if (!rel.startsWith("."))
|
|
78
|
+
rel = `./${rel}`;
|
|
79
|
+
return rel;
|
|
80
|
+
}
|
|
81
|
+
function injectWidget(dir, framework, options = {}) {
|
|
30
82
|
const absDir = node_path_1.default.resolve(dir);
|
|
31
83
|
const target = findLayoutFile(absDir, framework);
|
|
32
84
|
if (!target) {
|
|
33
|
-
return { injected: false, reason: "no app/layout.tsx or pages/_app.tsx found — add
|
|
85
|
+
return { injected: false, reason: "no app/layout.tsx or pages/_app.tsx found — add the widget manually" };
|
|
34
86
|
}
|
|
35
87
|
const relTarget = node_path_1.default.relative(absDir, target) || target;
|
|
36
88
|
const original = node_fs_1.default.readFileSync(target, "utf8");
|
|
37
|
-
if (original.includes("@cairnvibe/sdk") || original.includes("<Copilot")) {
|
|
38
|
-
return { injected: false, reason: `${relTarget} already references
|
|
89
|
+
if (original.includes(WRAPPER_COMPONENT_NAME) || original.includes("@cairnvibe/sdk") || original.includes("<Copilot")) {
|
|
90
|
+
return { injected: false, reason: `${relTarget} already references the widget — leaving it alone` };
|
|
91
|
+
}
|
|
92
|
+
// The wrapper always matches the layout file's own extension (.tsx stays
|
|
93
|
+
// .tsx, .jsx stays .jsx — a .tsx file dropped into a plain-JS project has
|
|
94
|
+
// no type checker configured for it and would just confuse tooling).
|
|
95
|
+
const ext = node_path_1.default.extname(target); // ".tsx" or ".jsx"
|
|
96
|
+
const wrapperPath = node_path_1.default.join(absDir, "components", `${WRAPPER_COMPONENT_NAME}${ext}`);
|
|
97
|
+
if (!node_fs_1.default.existsSync(wrapperPath)) {
|
|
98
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(wrapperPath), { recursive: true });
|
|
99
|
+
node_fs_1.default.writeFileSync(wrapperPath, wrapperSource(!!options.voice));
|
|
39
100
|
}
|
|
101
|
+
const importPath = toPosixRelativeImport(target, wrapperPath.slice(0, -ext.length));
|
|
102
|
+
const widgetJsx = `<${WRAPPER_COMPONENT_NAME} />`;
|
|
40
103
|
try {
|
|
41
104
|
const project = new ts_morph_1.Project({
|
|
42
105
|
useInMemoryFileSystem: false,
|
|
@@ -44,9 +107,9 @@ function injectWidget(dir, framework) {
|
|
|
44
107
|
compilerOptions: { jsx: ts_morph_1.ts.JsxEmit.ReactJSX, allowJs: true, esModuleInterop: true, target: ts_morph_1.ts.ScriptTarget.ES2022 },
|
|
45
108
|
});
|
|
46
109
|
const sf = project.addSourceFileAtPath(target);
|
|
47
|
-
const hasImport = sf.getImportDeclarations().some((d) => d.getModuleSpecifierValue() ===
|
|
110
|
+
const hasImport = sf.getImportDeclarations().some((d) => d.getModuleSpecifierValue() === importPath);
|
|
48
111
|
if (!hasImport) {
|
|
49
|
-
sf.addImportDeclaration({ moduleSpecifier:
|
|
112
|
+
sf.addImportDeclaration({ moduleSpecifier: importPath, namedImports: [WRAPPER_COMPONENT_NAME] });
|
|
50
113
|
}
|
|
51
114
|
// `insertText` at a position, not `replaceWithText` on a node — the latter
|
|
52
115
|
// asks ts-morph to structurally reconcile old vs. new trees, which fails
|
|
@@ -62,7 +125,7 @@ function injectWidget(dir, framework) {
|
|
|
62
125
|
if (bodyOpening) {
|
|
63
126
|
const closing = bodyOpening.getParentIfKind(ts_morph_1.SyntaxKind.JsxElement)?.getClosingElement();
|
|
64
127
|
if (closing) {
|
|
65
|
-
sf.insertText(closing.getStart(), `${
|
|
128
|
+
sf.insertText(closing.getStart(), `${widgetJsx}\n `);
|
|
66
129
|
inserted = true;
|
|
67
130
|
}
|
|
68
131
|
}
|
|
@@ -72,7 +135,7 @@ function injectWidget(dir, framework) {
|
|
|
72
135
|
.getDescendantsOfKind(ts_morph_1.SyntaxKind.JsxExpression)
|
|
73
136
|
.find((e) => e.getExpression()?.getText() === "children");
|
|
74
137
|
if (childrenExpr) {
|
|
75
|
-
sf.insertText(childrenExpr.getEnd(), `\n ${
|
|
138
|
+
sf.insertText(childrenExpr.getEnd(), `\n ${widgetJsx}`);
|
|
76
139
|
inserted = true;
|
|
77
140
|
}
|
|
78
141
|
}
|
|
@@ -89,20 +152,22 @@ function injectWidget(dir, framework) {
|
|
|
89
152
|
// first insertion changes the source text underneath it.
|
|
90
153
|
const start = componentTag.getStart();
|
|
91
154
|
const end = componentTag.getEnd();
|
|
92
|
-
sf.insertText(end, `\n ${
|
|
155
|
+
sf.insertText(end, `\n ${widgetJsx}\n </>`);
|
|
93
156
|
sf.insertText(start, `<>\n `);
|
|
94
157
|
inserted = true;
|
|
95
158
|
}
|
|
96
159
|
}
|
|
97
160
|
if (!inserted) {
|
|
98
|
-
return { injected: false, reason: `couldn't find a safe spot in ${relTarget} — add
|
|
161
|
+
return { injected: false, reason: `couldn't find a safe spot in ${relTarget} — add the widget manually` };
|
|
99
162
|
}
|
|
100
163
|
sf.saveSync();
|
|
101
|
-
return { injected: true, filePath: target };
|
|
164
|
+
return { injected: true, filePath: target, wrapperPath };
|
|
102
165
|
}
|
|
103
166
|
catch (err) {
|
|
104
167
|
// Never leave a half-written file — ts-morph only writes on saveSync(),
|
|
105
168
|
// so a thrown error here means the original file on disk is untouched.
|
|
106
|
-
|
|
169
|
+
// The wrapper component file, if it was just created, is still valid
|
|
170
|
+
// and harmless on its own — it's just not referenced from anywhere yet.
|
|
171
|
+
return { injected: false, reason: `couldn't safely modify ${relTarget} (${err.message}) — add the widget manually` };
|
|
107
172
|
}
|
|
108
173
|
}
|
package/dist/setup.js
CHANGED
|
@@ -22,6 +22,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
22
22
|
const node_child_process_1 = require("node:child_process");
|
|
23
23
|
const init_1 = require("./init");
|
|
24
24
|
const inject_widget_1 = require("./inject-widget");
|
|
25
|
+
const ensure_transpile_1 = require("./ensure-transpile");
|
|
25
26
|
const prompt_1 = require("./prompt");
|
|
26
27
|
const l1_scan_1 = require("./l1-scan");
|
|
27
28
|
const l2_reachability_1 = require("./l2-reachability");
|
|
@@ -202,6 +203,18 @@ async function runSetup(dir) {
|
|
|
202
203
|
], 1);
|
|
203
204
|
const deepgramKey = voiceChoice === "deepgram" ? await (0, prompt_1.askOptional)("Paste your DEEPGRAM_API_KEY: ") : null;
|
|
204
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
|
+
}
|
|
205
218
|
// 4. Write a real .env (not just .env.example) with whatever was actually given.
|
|
206
219
|
const envLines = [];
|
|
207
220
|
if (provider === "anthropic")
|
|
@@ -223,14 +236,28 @@ async function runSetup(dir) {
|
|
|
223
236
|
// deliberately doesn't do. Falls back to printing instructions on
|
|
224
237
|
// anything it can't confidently parse.
|
|
225
238
|
const framework = init.framework;
|
|
226
|
-
const inject = (0, inject_widget_1.injectWidget)(dir, framework);
|
|
239
|
+
const inject = (0, inject_widget_1.injectWidget)(dir, framework, { voice: wantsVoice });
|
|
227
240
|
if (inject.injected) {
|
|
228
|
-
|
|
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}`));
|
|
229
243
|
}
|
|
230
244
|
else {
|
|
231
|
-
console.log(`\
|
|
245
|
+
console.log(`\nWidget not auto-wired (${inject.reason}). Add it yourself:`);
|
|
232
246
|
console.log(' import { Copilot } from "@cairnvibe/sdk";');
|
|
233
247
|
console.log(" <Copilot registeredActions={[]} onDo={(action, target) => { /* run it */ }} />");
|
|
248
|
+
console.log(" (in a \"use client\" component — see examples/demo-app/components/CopilotWithActions.tsx for why)");
|
|
249
|
+
}
|
|
250
|
+
// 5b. @cairnvibe/sdk and @cairnvibe/core ship raw TS/TSX as their main
|
|
251
|
+
// entry deliberately — bundlers need transpilePackages to know to
|
|
252
|
+
// transform it. Without this, real projects fail cold at `next dev`
|
|
253
|
+
// with "Unknown module type", not something a demo on a fresh project
|
|
254
|
+
// would ever surface (this repo's own next.config.js already has it).
|
|
255
|
+
const transpile = (0, ensure_transpile_1.ensureTranspilePackages)(dir);
|
|
256
|
+
if (transpile.ok) {
|
|
257
|
+
console.log((0, ui_1.green)(`✓ ${transpile.created ? "created" : "updated"} ${node_path_1.default.relative(absDir, transpile.filePath)} with transpilePackages`));
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
console.log(`\ntranspilePackages not auto-added (${transpile.reason})`);
|
|
234
261
|
}
|
|
235
262
|
// 6. Build the manifest once now, if we actually have a usable key — no
|
|
236
263
|
// point trying (and failing loudly) with nothing to call. On failure,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cairnvibe/indexer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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" },
|