@maintainer-pro/ai-cli 0.1.0

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/index.cjs ADDED
@@ -0,0 +1,1117 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ buildClaudeUserPrompt: () => buildClaudeUserPrompt,
34
+ buildConversationPrompt: () => buildConversationPrompt,
35
+ buildCursorPrompt: () => buildCursorPrompt,
36
+ buildPriorConversationsContext: () => buildPriorConversationsContext,
37
+ callAi: () => callAi,
38
+ commandExists: () => commandExists,
39
+ createAntigravityProvider: () => createAntigravityProvider,
40
+ createBuiltinProviders: () => createBuiltinProviders,
41
+ createChatHandler: () => createChatHandler,
42
+ createClaudeProvider: () => createClaudeProvider,
43
+ createCursorProvider: () => createCursorProvider,
44
+ createDefaultSystemPrompt: () => createDefaultSystemPrompt,
45
+ createLocalDirectoryStore: () => createLocalDirectoryStore,
46
+ createMaintainerProStore: () => createMaintainerProStore,
47
+ createMaintainerProStoreFromEnv: () => createMaintainerProStoreFromEnv,
48
+ createToolValidator: () => createToolValidator,
49
+ formatClientContext: () => formatClientContext,
50
+ getProviderPreference: () => getProviderPreference,
51
+ parseAiResponse: () => parseAiResponse,
52
+ providerLabel: () => providerLabel,
53
+ resolveCliBinary: () => resolveCliBinary,
54
+ resolveProvider: () => resolveProvider,
55
+ saveChatAttachments: () => saveChatAttachments,
56
+ toNextRoute: () => toNextRoute
57
+ });
58
+ module.exports = __toCommonJS(index_exports);
59
+
60
+ // src/detect.ts
61
+ var import_execa2 = require("execa");
62
+
63
+ // src/prompt.ts
64
+ function buildConversationPrompt(messages) {
65
+ if (messages.length === 0) return "";
66
+ const lines = messages.map(
67
+ (m) => `${m.role === "user" ? "Human" : "Assistant"}: ${m.content}`
68
+ );
69
+ return lines.join("\n\n");
70
+ }
71
+ function formatClientContext(context) {
72
+ if (!context) return "";
73
+ const lines = [
74
+ "Client context (live UI snapshot):",
75
+ `- Route: ${context.route}`,
76
+ `- Page title: ${context.pageTitle}`
77
+ ];
78
+ if (context.visiblePanels?.length) {
79
+ lines.push(`- Visible panels: ${context.visiblePanels.join(", ")}`);
80
+ }
81
+ if (context.focusedElement) {
82
+ lines.push(`- Focused element: ${context.focusedElement}`);
83
+ }
84
+ if (context.relevantFiles?.length) {
85
+ lines.push(`- Relevant files: ${context.relevantFiles.join(", ")}`);
86
+ }
87
+ if (context.data && Object.keys(context.data).length > 0) {
88
+ lines.push("- App state:");
89
+ lines.push(JSON.stringify(context.data, null, 2));
90
+ }
91
+ return lines.join("\n");
92
+ }
93
+ function splitMessages(messages) {
94
+ let latestUserIndex = -1;
95
+ for (let i = messages.length - 1; i >= 0; i--) {
96
+ if (messages[i].role === "user") {
97
+ latestUserIndex = i;
98
+ break;
99
+ }
100
+ }
101
+ const request = latestUserIndex >= 0 ? messages[latestUserIndex].content.trim() : "";
102
+ const history = latestUserIndex > 0 ? messages.slice(0, latestUserIndex) : [];
103
+ return { request, history };
104
+ }
105
+ function buildUserFacingPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext) {
106
+ const { request, history } = splitMessages(messages);
107
+ const historyBlock = history.length > 0 ? `Conversation so far:
108
+ ${buildConversationPrompt(history)}
109
+
110
+ ` : "";
111
+ const contextBlock = formatClientContext(context);
112
+ const contextSection = contextBlock ? `${contextBlock}
113
+
114
+ ` : "";
115
+ const systemSection = systemPrompt ? `${systemPrompt}
116
+
117
+ ` : "";
118
+ const priorSection = priorConversationsContext?.trim() ? `${priorConversationsContext.trim()}
119
+
120
+ ` : "";
121
+ const attachmentsSection = attachmentPaths && attachmentPaths.length > 0 ? `Screenshots / images attached by the user (open and inspect these image files):
122
+ ${attachmentPaths.map((p) => `- ${p}`).join("\n")}
123
+
124
+ ` : "";
125
+ return `${systemSection}${contextSection}${priorSection}${historyBlock}${attachmentsSection}Current user request:
126
+ ${request || "(See the attached screenshot(s).)"}
127
+
128
+ Do the work now (or ask one short clarifying question if needed). Reply in English, non-technical and user-facing. If it's not resolving, offer a quick call.`;
129
+ }
130
+ function buildCursorPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext) {
131
+ return buildUserFacingPrompt(
132
+ systemPrompt,
133
+ messages,
134
+ context,
135
+ attachmentPaths,
136
+ priorConversationsContext
137
+ );
138
+ }
139
+ function buildClaudeUserPrompt(messages, context, attachmentPaths, priorConversationsContext) {
140
+ return buildUserFacingPrompt(
141
+ null,
142
+ messages,
143
+ context,
144
+ attachmentPaths,
145
+ priorConversationsContext
146
+ );
147
+ }
148
+
149
+ // src/parser.ts
150
+ function parseAiResponse(raw, provider) {
151
+ const toolCalls = [];
152
+ const jsonBlockRegex = /```json\s*\n?([\s\S]*?)\n?```/g;
153
+ let match;
154
+ while ((match = jsonBlockRegex.exec(raw)) !== null) {
155
+ try {
156
+ const parsed = JSON.parse(match[1].trim());
157
+ if (parsed !== null && typeof parsed === "object" && "tool" in parsed && typeof parsed.tool === "string") {
158
+ toolCalls.push(parsed);
159
+ }
160
+ } catch {
161
+ }
162
+ }
163
+ const text = raw.replace(/```json\s*\n?[\s\S]*?\n?```/g, "").trim();
164
+ return { text, toolCalls, provider };
165
+ }
166
+
167
+ // src/resolve-command.ts
168
+ var import_node_fs = __toESM(require("fs"), 1);
169
+ var import_node_path = __toESM(require("path"), 1);
170
+ function parseVersionRank(versionName) {
171
+ const datePart = versionName.split("-")[0] ?? "";
172
+ const parts = datePart.split(".");
173
+ if (parts.length !== 3) return 0;
174
+ const [year, month, day] = parts;
175
+ return Number(`${year}${month.padStart(2, "0")}${day.padStart(2, "0")}`);
176
+ }
177
+ function findLatestCursorVersionDir(installDir) {
178
+ const versionsRoot = import_node_path.default.join(installDir, "versions");
179
+ if (!import_node_fs.default.existsSync(versionsRoot)) return null;
180
+ const dirs = import_node_fs.default.readdirSync(versionsRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).filter(
181
+ (name) => /^\d{4}\.\d{1,2}\.\d{1,2}(-\d{2}-\d{2}-\d{2})?-[a-f0-9]+$/.test(name)
182
+ ).sort((a, b) => parseVersionRank(b) - parseVersionRank(a));
183
+ return dirs[0] ? import_node_path.default.join(versionsRoot, dirs[0]) : null;
184
+ }
185
+ function cursorInstallDirFromCommand(command) {
186
+ const normalized = command.replace(/\//g, import_node_path.default.sep);
187
+ if (import_node_path.default.isAbsolute(normalized) || normalized.includes(import_node_path.default.sep)) {
188
+ const dir = import_node_path.default.dirname(normalized);
189
+ if (import_node_fs.default.existsSync(import_node_path.default.join(dir, "versions"))) return dir;
190
+ }
191
+ const localAppData = process.env.LOCALAPPDATA;
192
+ if (localAppData) {
193
+ const candidate = import_node_path.default.join(localAppData, "cursor-agent");
194
+ if (import_node_fs.default.existsSync(import_node_path.default.join(candidate, "versions"))) return candidate;
195
+ }
196
+ return null;
197
+ }
198
+ function resolveCliCommand(provider, command) {
199
+ if (provider === "cursor" && process.platform === "win32") {
200
+ const installDir = cursorInstallDirFromCommand(command);
201
+ if (installDir) {
202
+ const versionDir = findLatestCursorVersionDir(installDir);
203
+ if (versionDir) {
204
+ const nodePath = import_node_path.default.join(versionDir, "node.exe");
205
+ const indexPath = import_node_path.default.join(versionDir, "index.js");
206
+ if (import_node_fs.default.existsSync(nodePath) && import_node_fs.default.existsSync(indexPath)) {
207
+ return {
208
+ file: nodePath,
209
+ argsPrefix: [indexPath],
210
+ shell: false
211
+ };
212
+ }
213
+ }
214
+ }
215
+ }
216
+ const isWinBatch = process.platform === "win32" && /\.(cmd|bat)$/i.test(command);
217
+ const isBare = process.platform === "win32" && !command.includes("\\") && !command.includes("/");
218
+ const isAbsoluteExe = process.platform === "win32" && import_node_path.default.isAbsolute(command) && /\.exe$/i.test(command);
219
+ return {
220
+ file: command,
221
+ argsPrefix: [],
222
+ shell: isAbsoluteExe ? false : isWinBatch || isBare
223
+ };
224
+ }
225
+
226
+ // src/run-cli.ts
227
+ var import_execa = require("execa");
228
+ async function runCli(resolved, args, options = {}) {
229
+ const result = await (0, import_execa.execa)(resolved.file, [...resolved.argsPrefix, ...args], {
230
+ shell: resolved.shell,
231
+ input: options.stdin,
232
+ cwd: options.cwd,
233
+ maxBuffer: 10 * 1024 * 1024
234
+ });
235
+ return result.stdout;
236
+ }
237
+
238
+ // src/providers/claude.ts
239
+ function createClaudeProvider() {
240
+ return {
241
+ id: "claude",
242
+ label: "Claude CLI",
243
+ async isAvailable() {
244
+ return await resolveCliBinary("claude") !== null;
245
+ },
246
+ async call(messages, context, options) {
247
+ const command = await resolveCliBinary("claude");
248
+ if (!command) throw new Error("Claude CLI is not available");
249
+ return callClaudeCli(command, messages, context, options);
250
+ }
251
+ };
252
+ }
253
+ async function callClaudeCli(command, messages, context, options) {
254
+ const prompt = buildClaudeUserPrompt(
255
+ messages,
256
+ context,
257
+ options.attachmentPaths,
258
+ options.priorConversationsContext
259
+ );
260
+ const resolved = resolveCliCommand("claude", command);
261
+ const workspace = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
262
+ const stdout = await runCli(
263
+ resolved,
264
+ [
265
+ "--print",
266
+ "--system-prompt",
267
+ options.systemPrompt,
268
+ "--no-session-persistence",
269
+ prompt
270
+ ],
271
+ { cwd: workspace }
272
+ );
273
+ return parseAiResponse(stdout, "claude");
274
+ }
275
+
276
+ // src/providers/cursor.ts
277
+ function createCursorProvider() {
278
+ return {
279
+ id: "cursor",
280
+ label: "Cursor Agent CLI",
281
+ async isAvailable() {
282
+ return await resolveCliBinary("cursor") !== null;
283
+ },
284
+ async call(messages, context, options) {
285
+ const command = await resolveCliBinary("cursor");
286
+ if (!command) throw new Error("Cursor Agent CLI is not available");
287
+ return callCursorCli(command, messages, context, options);
288
+ }
289
+ };
290
+ }
291
+ async function callCursorCli(command, messages, context, options) {
292
+ const prompt = buildCursorPrompt(
293
+ options.systemPrompt,
294
+ messages,
295
+ context,
296
+ options.attachmentPaths,
297
+ options.priorConversationsContext
298
+ );
299
+ const resolved = resolveCliCommand("cursor", command);
300
+ const workspace = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
301
+ const stdout = await runCli(
302
+ resolved,
303
+ [
304
+ "--print",
305
+ "--output-format",
306
+ "text",
307
+ "--trust",
308
+ "--force",
309
+ "--workspace",
310
+ workspace
311
+ ],
312
+ { stdin: prompt, cwd: workspace }
313
+ );
314
+ return parseAiResponse(stdout, "cursor");
315
+ }
316
+
317
+ // src/providers/antigravity.ts
318
+ function createAntigravityProvider() {
319
+ return {
320
+ id: "antigravity",
321
+ label: "Antigravity CLI",
322
+ async isAvailable() {
323
+ return await resolveCliBinary("antigravity") !== null;
324
+ },
325
+ async call(messages, context, options) {
326
+ const command = await resolveCliBinary("antigravity");
327
+ if (!command) throw new Error("Antigravity CLI is not available");
328
+ return callAntigravityCli(command, messages, context, options);
329
+ }
330
+ };
331
+ }
332
+ async function callAntigravityCli(command, messages, context, options) {
333
+ const basePrompt = buildCursorPrompt(
334
+ options.systemPrompt,
335
+ messages,
336
+ context,
337
+ options.attachmentPaths,
338
+ options.priorConversationsContext
339
+ );
340
+ const prompt = `You are operating as a coding agent with full permission to read and edit files in this workspace. Do not introduce yourself. Do not ask what you are. Execute the user's latest request now (edit files and/or emit runtime tool JSON as instructed).
341
+
342
+ ${basePrompt}`;
343
+ const resolved = resolveCliCommand("antigravity", command);
344
+ const workspace = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
345
+ const args = [
346
+ "-p",
347
+ prompt,
348
+ "--mode",
349
+ "accept-edits",
350
+ "--output-format",
351
+ "text",
352
+ "--dangerously-skip-permissions",
353
+ "--add-dir",
354
+ workspace,
355
+ "--print-timeout",
356
+ "10m"
357
+ ];
358
+ const stdout = await runCli(resolved, args, { cwd: workspace });
359
+ return parseAiResponse(stdout, "antigravity");
360
+ }
361
+
362
+ // src/detect.ts
363
+ var DEFAULT_COMMANDS = {
364
+ claude: ["claude"],
365
+ cursor: ["agent"],
366
+ antigravity: ["agy", "antigravity"]
367
+ };
368
+ async function resolveCommandPath(command) {
369
+ try {
370
+ if (process.platform === "win32") {
371
+ const result2 = await (0, import_execa2.execa)("where.exe", [command], { reject: false });
372
+ if (result2.exitCode !== 0) return null;
373
+ const lines = result2.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
374
+ const exe = lines.find((line) => /\.exe$/i.test(line));
375
+ return exe ?? lines[0] ?? null;
376
+ }
377
+ const result = await (0, import_execa2.execa)("which", [command], { reject: false });
378
+ if (result.exitCode !== 0) return null;
379
+ const path5 = result.stdout.trim().split(/\r?\n/)[0]?.trim();
380
+ return path5 || null;
381
+ } catch {
382
+ return null;
383
+ }
384
+ }
385
+ async function commandExists(command) {
386
+ return await resolveCommandPath(command) !== null;
387
+ }
388
+ function getProviderPreference(override) {
389
+ if (override) return override;
390
+ const raw = (process.env.AI_CLI_PROVIDER ?? "auto").toLowerCase().trim();
391
+ if (raw === "claude" || raw === "cursor" || raw === "antigravity" || raw === "agy" || raw === "auto") {
392
+ return raw === "agy" ? "antigravity" : raw;
393
+ }
394
+ console.warn(
395
+ `Unknown AI_CLI_PROVIDER="${raw}", falling back to "auto". Use auto|claude|cursor|antigravity.`
396
+ );
397
+ return "auto";
398
+ }
399
+ function matchOverrideToProvider(override, provider) {
400
+ const lower = override.toLowerCase();
401
+ if (provider === "claude") return lower.includes("claude");
402
+ if (provider === "antigravity") {
403
+ return lower.includes("agy") || lower.includes("antigravity");
404
+ }
405
+ return lower.includes("agent") || lower.includes("cursor") || !lower.includes("claude") && !lower.includes("agy") && !lower.includes("antigravity");
406
+ }
407
+ async function resolveCliBinary(provider) {
408
+ const override = process.env.AI_CLI_COMMAND?.trim();
409
+ const pref = getProviderPreference();
410
+ if (override) {
411
+ if (pref === provider) return override;
412
+ if (pref === "auto" && matchOverrideToProvider(override, provider)) {
413
+ return override;
414
+ }
415
+ }
416
+ for (const command of DEFAULT_COMMANDS[provider]) {
417
+ const resolved = await resolveCommandPath(command);
418
+ if (resolved) return resolved;
419
+ }
420
+ return null;
421
+ }
422
+ function createBuiltinProviders() {
423
+ return [
424
+ createClaudeProvider(),
425
+ createCursorProvider(),
426
+ createAntigravityProvider()
427
+ ];
428
+ }
429
+ async function resolveProvider(options) {
430
+ const preference = getProviderPreference(options?.preference);
431
+ const providers = options?.providers?.length ? options.providers : createBuiltinProviders();
432
+ const order = preference === "auto" ? providers : providers.filter((p) => p.id === preference);
433
+ for (const provider of order) {
434
+ if (await provider.isAvailable()) return provider;
435
+ }
436
+ if (preference === "auto") {
437
+ throw new Error(
438
+ "No AI provider available. Install Claude CLI (`claude`), Cursor Agent CLI (`agent`), or Antigravity CLI (`agy`), set AI_CLI_PROVIDER / AI_CLI_COMMAND, or pass custom providers."
439
+ );
440
+ }
441
+ throw new Error(
442
+ `AI provider "${preference}" is not available. Or set AI_CLI_COMMAND / pass a custom provider.`
443
+ );
444
+ }
445
+ function providerLabel(providerId) {
446
+ if (providerId === "claude") return "Claude CLI";
447
+ if (providerId === "cursor") return "Cursor Agent CLI";
448
+ if (providerId === "antigravity" || providerId === "agy") {
449
+ return "Antigravity CLI";
450
+ }
451
+ return providerId;
452
+ }
453
+
454
+ // src/call-ai.ts
455
+ async function callAi(messages, context, options) {
456
+ if (!options.systemPrompt?.trim()) {
457
+ throw new Error("callAi requires options.systemPrompt");
458
+ }
459
+ const provider = await resolveProvider({
460
+ preference: options.providerPreference,
461
+ providers: options.providers
462
+ });
463
+ return provider.call(messages, context, options);
464
+ }
465
+
466
+ // src/system-prompt.ts
467
+ function createDefaultSystemPrompt(input) {
468
+ const files = input.relevantFilesHint ? `Key UI files (use internally only; never name them in your reply):
469
+ ${input.relevantFilesHint}` : "";
470
+ const tools = input.runtimeToolsHint ? `## When to use runtime tools
471
+ Only when the user wants to change live in-app data (not source code), return a short user-facing sentence PLUS a JSON tool call in a \`\`\`json fence:
472
+
473
+ ${input.runtimeToolsHint}
474
+
475
+ Only use those runtime tools for live state. Never invent runtime tools.` : "";
476
+ return `You are a helpful product assistant for an app the user is looking at right now.
477
+
478
+ ${input.productDescription}
479
+
480
+ Behind the scenes you can edit this repository and update live app data, but the user is an end user \u2014 not a developer.
481
+
482
+ ## How to talk
483
+ - Always reply in English, even if the user writes in another language.
484
+ - Reply like a friendly product assistant, short and clear \u2014 keep the chat light so they don't get bored.
485
+ - Describe what changed in the UI, then invite them to check it.
486
+ - If the user attaches screenshots, inspect those image files and use them to understand the request.
487
+ - Ask a quick clarifying question when the request is vague or has more than one reasonable option (one question at a time, easy choices when you can).
488
+ - After you make a change, check in briefly: did that look right? want another tweak?
489
+ - If you are stuck, the request is unclear after a couple of tries, or they seem frustrated, offer a call: e.g. "Happy to jump on a quick call if that's easier \u2014 just say when."
490
+ - Do NOT mention file paths, component names, repos, commits, diffs, TypeScript, React, or tools.
491
+ - Do NOT use technical phrasing like "updated in Foo.tsx".
492
+
493
+ ## When to edit the codebase
494
+ Edit source files when the user asks to change labels, layout, styling, copy, or behavior in the app.
495
+ ${files}
496
+
497
+ ${tools}
498
+
499
+ Do not refuse UI/label changes \u2014 implement them in source files.
500
+ Answer the user's latest request directly \u2014 never reply with a generic greeting.`;
501
+ }
502
+
503
+ // src/http/attachments.ts
504
+ var import_promises = __toESM(require("fs/promises"), 1);
505
+ var import_node_path2 = __toESM(require("path"), 1);
506
+ var MAX_ATTACHMENTS = 5;
507
+ var MAX_BYTES = 4 * 1024 * 1024;
508
+ var ALLOWED = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif"]);
509
+ function extForMime(mime) {
510
+ switch (mime) {
511
+ case "image/jpeg":
512
+ case "image/jpg":
513
+ return ".jpg";
514
+ case "image/webp":
515
+ return ".webp";
516
+ case "image/gif":
517
+ return ".gif";
518
+ default:
519
+ return ".png";
520
+ }
521
+ }
522
+ async function saveChatAttachments(attachments, workspaceDir) {
523
+ if (!attachments?.length) return [];
524
+ const selected = attachments.slice(0, MAX_ATTACHMENTS);
525
+ const dir = import_node_path2.default.join(workspaceDir, ".maintainer-pro", "uploads");
526
+ await import_promises.default.mkdir(dir, { recursive: true });
527
+ const paths = [];
528
+ const stamp = Date.now();
529
+ for (let i = 0; i < selected.length; i++) {
530
+ const item = selected[i];
531
+ const mime = (item.mimeType || "").toLowerCase();
532
+ if (!ALLOWED.has(mime)) {
533
+ throw new Error(`Unsupported image type: ${item.mimeType}`);
534
+ }
535
+ if (!item.data?.trim()) {
536
+ throw new Error("Attachment data is empty");
537
+ }
538
+ const buffer = Buffer.from(item.data, "base64");
539
+ if (buffer.byteLength > MAX_BYTES) {
540
+ throw new Error(`Screenshot too large (max ${MAX_BYTES / (1024 * 1024)}MB)`);
541
+ }
542
+ const safeBase = (item.name || `screenshot-${i + 1}`).replace(/[^\w.\-]+/g, "_").slice(0, 64);
543
+ const fileName = `${stamp}-${i + 1}-${safeBase}${extForMime(mime)}`;
544
+ const filePath = import_node_path2.default.join(dir, fileName);
545
+ await import_promises.default.writeFile(filePath, buffer);
546
+ paths.push(filePath);
547
+ }
548
+ return paths;
549
+ }
550
+
551
+ // src/http/prior-conversations.ts
552
+ var DEFAULT_MAX_CONVERSATIONS = 3;
553
+ var DEFAULT_MAX_MESSAGES = 8;
554
+ var MAX_MESSAGE_CHARS = 400;
555
+ function tokenize(text) {
556
+ return text.toLowerCase().split(/[^a-z0-9]+/i).filter((w) => w.length > 3);
557
+ }
558
+ function truncate(text, max = MAX_MESSAGE_CHARS) {
559
+ const trimmed = text.trim().replace(/\s+/g, " ");
560
+ if (trimmed.length <= max) return trimmed;
561
+ return `${trimmed.slice(0, max - 1)}\u2026`;
562
+ }
563
+ function scoreConversation(messages, requestTokens) {
564
+ if (requestTokens.length === 0) return 0;
565
+ const blob = messages.map((m) => m.content.toLowerCase()).join(" ");
566
+ let score = 0;
567
+ for (const token of requestTokens) {
568
+ if (blob.includes(token)) score += 1;
569
+ }
570
+ return score;
571
+ }
572
+ async function buildPriorConversationsContext(db, options) {
573
+ if (!db.listConversations) return "";
574
+ const conversations = await db.listConversations();
575
+ const others = conversations.filter(
576
+ (c) => c.id !== options.excludeConversationId && c.messages.some((m) => m.role === "user" || m.role === "assistant")
577
+ );
578
+ if (others.length === 0) return "";
579
+ const requestTokens = tokenize(options.currentRequest);
580
+ const maxConversations = options.maxConversations ?? DEFAULT_MAX_CONVERSATIONS;
581
+ const maxMessages = options.maxMessagesPerConversation ?? DEFAULT_MAX_MESSAGES;
582
+ const ranked = others.map((c) => ({
583
+ ...c,
584
+ score: scoreConversation(c.messages, requestTokens),
585
+ updatedMs: Date.parse(c.updatedAt) || 0
586
+ })).sort((a, b) => {
587
+ if (b.score !== a.score) return b.score - a.score;
588
+ return b.updatedMs - a.updatedMs;
589
+ }).slice(0, maxConversations);
590
+ const selected = requestTokens.length > 0 && ranked.every((c) => c.score === 0) ? [...others].sort(
591
+ (a, b) => (Date.parse(b.updatedAt) || 0) - (Date.parse(a.updatedAt) || 0)
592
+ ).slice(0, maxConversations) : ranked;
593
+ const blocks = selected.map((c) => {
594
+ const slice = c.messages.length > maxMessages ? c.messages.slice(-maxMessages) : c.messages;
595
+ const lines = slice.filter((m) => m.role === "user" || m.role === "assistant").map(
596
+ (m) => `${m.role === "user" ? "Human" : "Assistant"}: ${truncate(m.content)}`
597
+ );
598
+ const when = c.updatedAt ? ` (updated ${c.updatedAt.slice(0, 10)})` : "";
599
+ return `--- Prior conversation ${c.id.slice(0, 8)}\u2026${when} ---
600
+ ${lines.join("\n")}`;
601
+ });
602
+ if (blocks.length === 0) return "";
603
+ return `Prior conversations from this workspace (use only if relevant to the current request; ignore if unrelated):
604
+
605
+ ${blocks.join("\n\n")}`;
606
+ }
607
+
608
+ // src/http/tools.ts
609
+ function createToolValidator(schemas) {
610
+ return function validateToolCall(toolCall) {
611
+ const schema = schemas[toolCall.tool];
612
+ if (!schema) {
613
+ return { valid: false, error: `Unknown tool: ${toolCall.tool}` };
614
+ }
615
+ const result = schema.safeParse(toolCall.args);
616
+ if (!result.success) {
617
+ return { valid: false, error: result.error.message };
618
+ }
619
+ return {
620
+ valid: true,
621
+ name: toolCall.tool,
622
+ args: result.data
623
+ };
624
+ };
625
+ }
626
+
627
+ // src/http/handler.ts
628
+ var SHARED_CONVERSATION_ID = "shared";
629
+ var conversationTurnSeq = /* @__PURE__ */ new Map();
630
+ function beginConversationTurn(conversationId) {
631
+ const next = (conversationTurnSeq.get(conversationId) ?? 0) + 1;
632
+ conversationTurnSeq.set(conversationId, next);
633
+ return next;
634
+ }
635
+ function isActiveConversationTurn(conversationId, turn) {
636
+ const current = conversationTurnSeq.get(conversationId);
637
+ if (current === void 0) return true;
638
+ return current === turn;
639
+ }
640
+ function resolveConversationId(request, options, bodyId) {
641
+ if (bodyId) return bodyId;
642
+ const urlId = new URL(request.url).searchParams.get("conversationId");
643
+ if (urlId) return urlId;
644
+ if (typeof options.conversationId === "function") {
645
+ return options.conversationId(request);
646
+ }
647
+ return options.conversationId;
648
+ }
649
+ async function resolveSharedConversationId(request, options, bodyId) {
650
+ const explicit = resolveConversationId(request, options, bodyId);
651
+ if (explicit) return explicit;
652
+ if (options.db?.listConversations) {
653
+ const conversations = await options.db.listConversations();
654
+ const latest = conversations.find(
655
+ (c) => c.messages.some((m) => m.role === "user" || m.role === "assistant")
656
+ );
657
+ if (latest) return latest.id;
658
+ }
659
+ return SHARED_CONVERSATION_ID;
660
+ }
661
+ function createChatHandler(options) {
662
+ const validate = options.tools ? createToolValidator(options.tools) : null;
663
+ const workspaceDir = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
664
+ const baseCallOptions = {
665
+ systemPrompt: options.systemPrompt,
666
+ workspaceDir,
667
+ providerPreference: options.providerPreference,
668
+ providers: options.providers
669
+ };
670
+ return {
671
+ async GET(request) {
672
+ try {
673
+ const provider = await resolveProvider({
674
+ preference: options.providerPreference,
675
+ providers: options.providers
676
+ });
677
+ const conversationId = await resolveSharedConversationId(
678
+ request,
679
+ options
680
+ );
681
+ let messages;
682
+ if (options.db?.listMessages) {
683
+ messages = await options.db.listMessages(conversationId);
684
+ }
685
+ return Response.json({
686
+ provider: provider.id,
687
+ providerLabel: provider.label || providerLabel(provider.id),
688
+ conversationId,
689
+ messages: messages ?? []
690
+ });
691
+ } catch (err) {
692
+ const message = err instanceof Error ? err.message : "No AI CLI provider available";
693
+ return Response.json(
694
+ { error: message, provider: null },
695
+ { status: 503 }
696
+ );
697
+ }
698
+ },
699
+ async POST(request) {
700
+ try {
701
+ const body = await request.json();
702
+ const conversationId = await resolveSharedConversationId(
703
+ request,
704
+ options,
705
+ body.conversationId
706
+ );
707
+ if (body.type === "status") {
708
+ const content = body.content?.trim();
709
+ if (!conversationId || !content) {
710
+ return Response.json(
711
+ { error: "conversationId and content are required" },
712
+ { status: 400 }
713
+ );
714
+ }
715
+ if (options.db) {
716
+ await options.db.ensureConversation(conversationId);
717
+ await options.db.saveMessage({
718
+ conversationId,
719
+ role: "assistant",
720
+ content
721
+ });
722
+ }
723
+ return Response.json({ ok: true, conversationId });
724
+ }
725
+ if (body.type === "reply") {
726
+ const content = body.content?.trim();
727
+ if (!conversationId || !content) {
728
+ return Response.json(
729
+ { error: "conversationId and content are required" },
730
+ { status: 400 }
731
+ );
732
+ }
733
+ if (options.db) {
734
+ await options.db.ensureConversation(conversationId);
735
+ await options.db.saveMessage({
736
+ conversationId,
737
+ role: "assistant",
738
+ content,
739
+ provider: "developer",
740
+ senderType: "developer",
741
+ senderName: body.senderName?.trim() || void 0
742
+ });
743
+ }
744
+ return Response.json({ ok: true, conversationId });
745
+ }
746
+ const { messages, context, attachments } = body;
747
+ if (!Array.isArray(messages) || messages.length === 0) {
748
+ return Response.json(
749
+ { error: "messages array is required" },
750
+ { status: 400 }
751
+ );
752
+ }
753
+ let attachmentPaths = [];
754
+ let persistAttachmentPaths = [];
755
+ if (options.db?.uploadAttachments) {
756
+ const uploaded = await options.db.uploadAttachments(
757
+ conversationId,
758
+ attachments
759
+ );
760
+ attachmentPaths = uploaded.localPaths;
761
+ persistAttachmentPaths = uploaded.refs;
762
+ } else {
763
+ attachmentPaths = await saveChatAttachments(
764
+ attachments,
765
+ workspaceDir
766
+ );
767
+ persistAttachmentPaths = attachmentPaths;
768
+ }
769
+ const turn = beginConversationTurn(conversationId);
770
+ const signal = request.signal;
771
+ if (options.db && conversationId) {
772
+ const latest = messages[messages.length - 1];
773
+ const persistContent = body.userMessage?.trim() || (latest?.role === "user" ? latest.content : "");
774
+ if (persistContent) {
775
+ await options.db.ensureConversation(conversationId);
776
+ await options.db.saveMessage({
777
+ conversationId,
778
+ role: "user",
779
+ content: persistContent,
780
+ attachmentPaths: persistAttachmentPaths.length > 0 ? persistAttachmentPaths : void 0,
781
+ senderType: body.senderType === "client" ? "client" : void 0,
782
+ senderName: body.senderName?.trim() || void 0
783
+ });
784
+ }
785
+ }
786
+ const latestUser = [...messages].reverse().find((m) => m.role === "user");
787
+ const priorConversationsContext = options.db ? await buildPriorConversationsContext(options.db, {
788
+ excludeConversationId: conversationId,
789
+ currentRequest: latestUser?.content ?? ""
790
+ }) : "";
791
+ if (signal.aborted || !isActiveConversationTurn(conversationId, turn)) {
792
+ return Response.json({
793
+ superseded: true,
794
+ conversationId: conversationId ?? null
795
+ });
796
+ }
797
+ const aiResponse = await callAi(messages, context, {
798
+ ...baseCallOptions,
799
+ attachmentPaths,
800
+ priorConversationsContext: priorConversationsContext || void 0
801
+ });
802
+ if (!isActiveConversationTurn(conversationId, turn)) {
803
+ return Response.json({
804
+ superseded: true,
805
+ conversationId: conversationId ?? null
806
+ });
807
+ }
808
+ const validatedToolCalls = validate ? aiResponse.toolCalls.filter((tc) => validate(tc).valid) : aiResponse.toolCalls;
809
+ if (options.onToolCalls && validatedToolCalls.length > 0) {
810
+ await options.onToolCalls(validatedToolCalls, context);
811
+ }
812
+ if (options.db && conversationId) {
813
+ await options.db.ensureConversation(conversationId);
814
+ await options.db.saveMessage({
815
+ conversationId,
816
+ role: "assistant",
817
+ content: aiResponse.text,
818
+ provider: aiResponse.provider,
819
+ senderType: "ai"
820
+ });
821
+ if (validatedToolCalls.length > 0) {
822
+ await options.db.saveToolEvents?.(
823
+ conversationId,
824
+ validatedToolCalls
825
+ );
826
+ }
827
+ }
828
+ return Response.json({
829
+ text: aiResponse.text,
830
+ toolCalls: validatedToolCalls,
831
+ provider: aiResponse.provider,
832
+ providerLabel: providerLabel(aiResponse.provider),
833
+ conversationId: conversationId ?? null
834
+ });
835
+ } catch (err) {
836
+ console.error("Chat API error:", err);
837
+ return Response.json(
838
+ {
839
+ error: "Sorry about that \u2014 I couldn't finish this right now. Happy to jump on a quick call if you'd like help \u2014 just let us know when works."
840
+ },
841
+ { status: 500 }
842
+ );
843
+ }
844
+ }
845
+ };
846
+ }
847
+ function toNextRoute(handlers) {
848
+ return {
849
+ GET: (request) => handlers.GET(request),
850
+ POST: (request) => handlers.POST(request)
851
+ };
852
+ }
853
+
854
+ // src/http/local-store.ts
855
+ var import_promises2 = __toESM(require("fs/promises"), 1);
856
+ var import_node_path3 = __toESM(require("path"), 1);
857
+ var import_node_crypto = require("crypto");
858
+ async function readConversation(filePath) {
859
+ try {
860
+ const raw = await import_promises2.default.readFile(filePath, "utf8");
861
+ return JSON.parse(raw);
862
+ } catch (err) {
863
+ if (err.code === "ENOENT") return null;
864
+ throw err;
865
+ }
866
+ }
867
+ async function writeConversation(filePath, data) {
868
+ await import_promises2.default.mkdir(import_node_path3.default.dirname(filePath), { recursive: true });
869
+ await import_promises2.default.writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
870
+ }
871
+ function createLocalDirectoryStore(baseDir) {
872
+ const fileFor = (id) => import_node_path3.default.join(baseDir, `${id}.json`);
873
+ return {
874
+ async ensureConversation(id) {
875
+ const existing = await readConversation(fileFor(id));
876
+ if (existing) return;
877
+ await writeConversation(fileFor(id), {
878
+ id,
879
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
880
+ messages: []
881
+ });
882
+ },
883
+ async saveMessage(input) {
884
+ const filePath = fileFor(input.conversationId);
885
+ const existing = await readConversation(filePath) ?? {
886
+ id: input.conversationId,
887
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
888
+ messages: []
889
+ };
890
+ const message = {
891
+ id: (0, import_node_crypto.randomUUID)(),
892
+ conversationId: input.conversationId,
893
+ role: input.role,
894
+ content: input.content,
895
+ provider: input.provider ?? null,
896
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
897
+ attachmentPaths: input.attachmentPaths,
898
+ senderType: input.senderType ?? null,
899
+ senderName: input.senderName ?? null
900
+ };
901
+ existing.messages.push(message);
902
+ existing.updatedAt = message.createdAt;
903
+ await writeConversation(filePath, existing);
904
+ return message;
905
+ },
906
+ async listMessages(conversationId) {
907
+ const existing = await readConversation(fileFor(conversationId));
908
+ return existing?.messages ?? [];
909
+ },
910
+ async listConversations() {
911
+ await import_promises2.default.mkdir(baseDir, { recursive: true });
912
+ const entries = await import_promises2.default.readdir(baseDir);
913
+ const conversations = [];
914
+ for (const entry of entries) {
915
+ if (!entry.endsWith(".json")) continue;
916
+ const filePath = import_node_path3.default.join(baseDir, entry);
917
+ const existing = await readConversation(filePath);
918
+ if (!existing?.id) continue;
919
+ conversations.push({
920
+ id: existing.id,
921
+ updatedAt: existing.updatedAt,
922
+ messages: existing.messages
923
+ });
924
+ }
925
+ return conversations.sort(
926
+ (a, b) => (Date.parse(b.updatedAt) || 0) - (Date.parse(a.updatedAt) || 0)
927
+ );
928
+ },
929
+ async saveToolEvents(conversationId, events) {
930
+ if (events.length === 0) return;
931
+ const existing = await readConversation(fileFor(conversationId));
932
+ if (!existing) return;
933
+ const toolFile = import_node_path3.default.join(baseDir, `${conversationId}.tools.jsonl`);
934
+ const lines = events.map(
935
+ (e) => JSON.stringify({
936
+ conversationId,
937
+ ...e,
938
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
939
+ })
940
+ ).join("\n");
941
+ await import_promises2.default.appendFile(toolFile, `${lines}
942
+ `, "utf8");
943
+ }
944
+ };
945
+ }
946
+
947
+ // src/http/maintainer-pro-store.ts
948
+ var import_promises3 = __toESM(require("fs/promises"), 1);
949
+ var import_node_os = __toESM(require("os"), 1);
950
+ var import_node_path4 = __toESM(require("path"), 1);
951
+ async function mpFetch(baseUrl, apiKey, pathName, init, fetchImpl) {
952
+ const url = `${baseUrl.replace(/\/$/, "")}${pathName}`;
953
+ const res = await fetchImpl(url, {
954
+ ...init,
955
+ headers: {
956
+ Authorization: `Bearer ${apiKey}`,
957
+ "Content-Type": "application/json",
958
+ ...init?.headers ?? {}
959
+ }
960
+ });
961
+ if (!res.ok) {
962
+ const text = await res.text().catch(() => "");
963
+ throw new Error(
964
+ `Maintainer Pro ${pathName} failed (${res.status}): ${text || res.statusText}`
965
+ );
966
+ }
967
+ const contentType = res.headers.get("content-type") ?? "";
968
+ if (contentType.includes("application/json")) {
969
+ return res.json();
970
+ }
971
+ return res;
972
+ }
973
+ function createMaintainerProStore(options) {
974
+ const baseUrl = options.baseUrl;
975
+ const apiKey = options.apiKey;
976
+ const fetchImpl = options.fetchImpl ?? fetch;
977
+ const tempDir = options.tempDir ?? import_node_path4.default.join(import_node_os.default.tmpdir(), "maintainer-pro-store");
978
+ const store = {
979
+ async ensureConversation(id) {
980
+ await mpFetch(
981
+ baseUrl,
982
+ apiKey,
983
+ `/api/v1/store/conversations/${encodeURIComponent(id)}/ensure`,
984
+ { method: "POST", body: "{}" },
985
+ fetchImpl
986
+ );
987
+ },
988
+ async saveMessage(input) {
989
+ const attachmentIds = (input.attachmentPaths ?? []).map((p) => {
990
+ const m = /^maintainer-pro:\/\/(.+)$/.exec(p);
991
+ return m?.[1];
992
+ }).filter((id) => Boolean(id));
993
+ const data = await mpFetch(
994
+ baseUrl,
995
+ apiKey,
996
+ `/api/v1/store/conversations/${encodeURIComponent(input.conversationId)}/messages`,
997
+ {
998
+ method: "POST",
999
+ body: JSON.stringify({
1000
+ role: input.role,
1001
+ content: input.content,
1002
+ provider: input.provider,
1003
+ senderType: input.senderType,
1004
+ senderName: input.senderName,
1005
+ attachmentIds
1006
+ })
1007
+ },
1008
+ fetchImpl
1009
+ );
1010
+ return data.message;
1011
+ },
1012
+ async listMessages(conversationId) {
1013
+ const data = await mpFetch(
1014
+ baseUrl,
1015
+ apiKey,
1016
+ `/api/v1/store/conversations/${encodeURIComponent(conversationId)}/messages`,
1017
+ { method: "GET" },
1018
+ fetchImpl
1019
+ );
1020
+ return data.messages ?? [];
1021
+ },
1022
+ async listConversations() {
1023
+ const data = await mpFetch(
1024
+ baseUrl,
1025
+ apiKey,
1026
+ `/api/v1/store/conversations`,
1027
+ { method: "GET" },
1028
+ fetchImpl
1029
+ );
1030
+ return data.conversations ?? [];
1031
+ },
1032
+ async saveToolEvents(conversationId, events) {
1033
+ if (!events.length) return;
1034
+ await mpFetch(
1035
+ baseUrl,
1036
+ apiKey,
1037
+ `/api/v1/store/conversations/${encodeURIComponent(conversationId)}/tool-events`,
1038
+ {
1039
+ method: "POST",
1040
+ body: JSON.stringify({ events })
1041
+ },
1042
+ fetchImpl
1043
+ );
1044
+ },
1045
+ async uploadAttachments(conversationId, attachments) {
1046
+ if (!attachments?.length) {
1047
+ return { refs: [], localPaths: [], attachmentIds: [] };
1048
+ }
1049
+ await import_promises3.default.mkdir(tempDir, { recursive: true });
1050
+ const refs = [];
1051
+ const localPaths = [];
1052
+ const attachmentIds = [];
1053
+ for (const item of attachments.slice(0, 5)) {
1054
+ const data = await mpFetch(
1055
+ baseUrl,
1056
+ apiKey,
1057
+ `/api/v1/store/attachments`,
1058
+ {
1059
+ method: "POST",
1060
+ body: JSON.stringify({
1061
+ conversationId,
1062
+ name: item.name,
1063
+ mimeType: item.mimeType,
1064
+ data: item.data
1065
+ })
1066
+ },
1067
+ fetchImpl
1068
+ );
1069
+ const id = data.attachment.id;
1070
+ attachmentIds.push(id);
1071
+ refs.push(data.attachment.ref || `maintainer-pro://${id}`);
1072
+ const ext = item.mimeType.includes("jpeg") || item.mimeType.includes("jpg") ? ".jpg" : item.mimeType.includes("webp") ? ".webp" : item.mimeType.includes("gif") ? ".gif" : ".png";
1073
+ const localPath = import_node_path4.default.join(
1074
+ tempDir,
1075
+ `${conversationId}-${id}${ext}`
1076
+ );
1077
+ await import_promises3.default.writeFile(localPath, Buffer.from(item.data, "base64"));
1078
+ localPaths.push(localPath);
1079
+ }
1080
+ return { refs, localPaths, attachmentIds };
1081
+ }
1082
+ };
1083
+ return store;
1084
+ }
1085
+ function createMaintainerProStoreFromEnv() {
1086
+ const baseUrl = process.env.MAINTAINER_PRO_URL?.trim();
1087
+ const apiKey = process.env.MAINTAINER_PRO_API_KEY?.trim();
1088
+ if (!baseUrl || !apiKey) return null;
1089
+ return createMaintainerProStore({ baseUrl, apiKey });
1090
+ }
1091
+ // Annotate the CommonJS export names for ESM import in node:
1092
+ 0 && (module.exports = {
1093
+ buildClaudeUserPrompt,
1094
+ buildConversationPrompt,
1095
+ buildCursorPrompt,
1096
+ buildPriorConversationsContext,
1097
+ callAi,
1098
+ commandExists,
1099
+ createAntigravityProvider,
1100
+ createBuiltinProviders,
1101
+ createChatHandler,
1102
+ createClaudeProvider,
1103
+ createCursorProvider,
1104
+ createDefaultSystemPrompt,
1105
+ createLocalDirectoryStore,
1106
+ createMaintainerProStore,
1107
+ createMaintainerProStoreFromEnv,
1108
+ createToolValidator,
1109
+ formatClientContext,
1110
+ getProviderPreference,
1111
+ parseAiResponse,
1112
+ providerLabel,
1113
+ resolveCliBinary,
1114
+ resolveProvider,
1115
+ saveChatAttachments,
1116
+ toNextRoute
1117
+ });