@maintainer-pro/ai-cli 0.1.3 → 0.1.5
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/README.md +0 -1
- package/dist/index.cjs +1428 -74
- package/dist/index.d.cts +244 -6
- package/dist/index.d.ts +244 -6
- package/dist/index.js +1404 -73
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
import { execa as execa2 } from "execa";
|
|
3
3
|
|
|
4
4
|
// src/prompt.ts
|
|
5
|
+
var PROMPT_SECTION = {
|
|
6
|
+
system: { begin: "<<<SYSTEM_BEGIN>>>", end: "<<<SYSTEM_END>>>" },
|
|
7
|
+
clientContext: {
|
|
8
|
+
begin: "<<<CLIENT_CONTEXT_BEGIN>>>",
|
|
9
|
+
end: "<<<CLIENT_CONTEXT_END>>>"
|
|
10
|
+
},
|
|
11
|
+
priorConversations: {
|
|
12
|
+
begin: "<<<PRIOR_CONVERSATIONS_BEGIN>>>",
|
|
13
|
+
end: "<<<PRIOR_CONVERSATIONS_END>>>"
|
|
14
|
+
},
|
|
15
|
+
parentChain: {
|
|
16
|
+
begin: "<<<PARENT_CHAIN_BEGIN>>>",
|
|
17
|
+
end: "<<<PARENT_CHAIN_END>>>"
|
|
18
|
+
},
|
|
19
|
+
history: {
|
|
20
|
+
begin: "<<<CONVERSATION_HISTORY_BEGIN>>>",
|
|
21
|
+
end: "<<<CONVERSATION_HISTORY_END>>>"
|
|
22
|
+
},
|
|
23
|
+
attachments: {
|
|
24
|
+
begin: "<<<ATTACHMENTS_BEGIN>>>",
|
|
25
|
+
end: "<<<ATTACHMENTS_END>>>"
|
|
26
|
+
},
|
|
27
|
+
currentRequest: {
|
|
28
|
+
begin: "<<<CURRENT_REQUEST_BEGIN>>>",
|
|
29
|
+
end: "<<<CURRENT_REQUEST_END>>>"
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
function wrapSection(tag, body, title) {
|
|
33
|
+
const trimmed = body.trim();
|
|
34
|
+
if (!trimmed) return "";
|
|
35
|
+
const header = title ? `${tag.begin} ${title}` : tag.begin;
|
|
36
|
+
return `${header}
|
|
37
|
+
${trimmed}
|
|
38
|
+
${tag.end}
|
|
39
|
+
|
|
40
|
+
`;
|
|
41
|
+
}
|
|
5
42
|
function buildConversationPrompt(messages) {
|
|
6
43
|
if (messages.length === 0) return "";
|
|
7
44
|
const lines = messages.map(
|
|
@@ -43,47 +80,67 @@ function splitMessages(messages) {
|
|
|
43
80
|
const history = latestUserIndex > 0 ? messages.slice(0, latestUserIndex) : [];
|
|
44
81
|
return { request, history };
|
|
45
82
|
}
|
|
46
|
-
function buildUserFacingPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext) {
|
|
83
|
+
function buildUserFacingPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext, technical, parentChainContext) {
|
|
47
84
|
const { request, history } = splitMessages(messages);
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const contextSection =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
85
|
+
const systemSection = wrapSection(
|
|
86
|
+
PROMPT_SECTION.system,
|
|
87
|
+
systemPrompt ?? "",
|
|
88
|
+
"instructions"
|
|
89
|
+
);
|
|
90
|
+
const contextSection = wrapSection(
|
|
91
|
+
PROMPT_SECTION.clientContext,
|
|
92
|
+
formatClientContext(context),
|
|
93
|
+
"live UI snapshot"
|
|
94
|
+
);
|
|
95
|
+
const priorSection = wrapSection(
|
|
96
|
+
PROMPT_SECTION.priorConversations,
|
|
97
|
+
priorConversationsContext ?? "",
|
|
98
|
+
"other chats (secondary)"
|
|
99
|
+
);
|
|
100
|
+
const parentSection = wrapSection(
|
|
101
|
+
PROMPT_SECTION.parentChain,
|
|
102
|
+
parentChainContext ?? "",
|
|
103
|
+
"reply-to parents \u2014 primary thread context"
|
|
104
|
+
);
|
|
105
|
+
const historySection = wrapSection(
|
|
106
|
+
PROMPT_SECTION.history,
|
|
107
|
+
history.length > 0 ? buildConversationPrompt(history) : "",
|
|
108
|
+
"earlier turns in this conversation"
|
|
109
|
+
);
|
|
110
|
+
const attachmentsSection = wrapSection(
|
|
111
|
+
PROMPT_SECTION.attachments,
|
|
112
|
+
attachmentPaths && attachmentPaths.length > 0 ? `Screenshots / images attached by the user (open and inspect these image files):
|
|
113
|
+
${attachmentPaths.map((p) => `- ${p}`).join("\n")}` : "",
|
|
114
|
+
"user attachments"
|
|
115
|
+
);
|
|
116
|
+
const currentSection = wrapSection(
|
|
117
|
+
PROMPT_SECTION.currentRequest,
|
|
118
|
+
request || "(See the attached screenshot(s).)",
|
|
119
|
+
"answer this message"
|
|
120
|
+
);
|
|
121
|
+
const closer = technical ? "Do the work now. Follow the requested output format exactly." : "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.";
|
|
122
|
+
return `${systemSection}${contextSection}${priorSection}${parentSection}${historySection}${attachmentsSection}${currentSection}${closer}`;
|
|
70
123
|
}
|
|
71
|
-
function buildCursorPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext) {
|
|
124
|
+
function buildCursorPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext, technical, parentChainContext) {
|
|
72
125
|
return buildUserFacingPrompt(
|
|
73
126
|
systemPrompt,
|
|
74
127
|
messages,
|
|
75
128
|
context,
|
|
76
129
|
attachmentPaths,
|
|
77
|
-
priorConversationsContext
|
|
130
|
+
priorConversationsContext,
|
|
131
|
+
technical,
|
|
132
|
+
parentChainContext
|
|
78
133
|
);
|
|
79
134
|
}
|
|
80
|
-
function buildClaudeUserPrompt(messages, context, attachmentPaths, priorConversationsContext) {
|
|
135
|
+
function buildClaudeUserPrompt(messages, context, attachmentPaths, priorConversationsContext, technical, parentChainContext) {
|
|
81
136
|
return buildUserFacingPrompt(
|
|
82
137
|
null,
|
|
83
138
|
messages,
|
|
84
139
|
context,
|
|
85
140
|
attachmentPaths,
|
|
86
|
-
priorConversationsContext
|
|
141
|
+
priorConversationsContext,
|
|
142
|
+
technical,
|
|
143
|
+
parentChainContext
|
|
87
144
|
);
|
|
88
145
|
}
|
|
89
146
|
|
|
@@ -196,7 +253,9 @@ async function callClaudeCli(command, messages, context, options) {
|
|
|
196
253
|
messages,
|
|
197
254
|
context,
|
|
198
255
|
options.attachmentPaths,
|
|
199
|
-
options.priorConversationsContext
|
|
256
|
+
options.priorConversationsContext,
|
|
257
|
+
options.technical,
|
|
258
|
+
options.parentChainContext
|
|
200
259
|
);
|
|
201
260
|
const resolved = resolveCliCommand("claude", command);
|
|
202
261
|
const workspace = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
|
|
@@ -235,7 +294,9 @@ async function callCursorCli(command, messages, context, options) {
|
|
|
235
294
|
messages,
|
|
236
295
|
context,
|
|
237
296
|
options.attachmentPaths,
|
|
238
|
-
options.priorConversationsContext
|
|
297
|
+
options.priorConversationsContext,
|
|
298
|
+
options.technical,
|
|
299
|
+
options.parentChainContext
|
|
239
300
|
);
|
|
240
301
|
const resolved = resolveCliCommand("cursor", command);
|
|
241
302
|
const workspace = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
|
|
@@ -276,7 +337,9 @@ async function callAntigravityCli(command, messages, context, options) {
|
|
|
276
337
|
messages,
|
|
277
338
|
context,
|
|
278
339
|
options.attachmentPaths,
|
|
279
|
-
options.priorConversationsContext
|
|
340
|
+
options.priorConversationsContext,
|
|
341
|
+
options.technical,
|
|
342
|
+
options.parentChainContext
|
|
280
343
|
);
|
|
281
344
|
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).
|
|
282
345
|
|
|
@@ -317,8 +380,8 @@ async function resolveCommandPath(command) {
|
|
|
317
380
|
}
|
|
318
381
|
const result = await execa2("which", [command], { reject: false });
|
|
319
382
|
if (result.exitCode !== 0) return null;
|
|
320
|
-
const
|
|
321
|
-
return
|
|
383
|
+
const path6 = result.stdout.trim().split(/\r?\n/)[0]?.trim();
|
|
384
|
+
return path6 || null;
|
|
322
385
|
} catch {
|
|
323
386
|
return null;
|
|
324
387
|
}
|
|
@@ -404,6 +467,51 @@ async function callAi(messages, context, options) {
|
|
|
404
467
|
return provider.call(messages, context, options);
|
|
405
468
|
}
|
|
406
469
|
|
|
470
|
+
// src/parent-chain.ts
|
|
471
|
+
function collectParentChain(rows, startId) {
|
|
472
|
+
if (!startId) return [];
|
|
473
|
+
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
474
|
+
const chain = [];
|
|
475
|
+
const seen = /* @__PURE__ */ new Set();
|
|
476
|
+
let current = byId.get(startId);
|
|
477
|
+
while (current && !seen.has(current.id)) {
|
|
478
|
+
seen.add(current.id);
|
|
479
|
+
const content = String(current.content ?? "").trim();
|
|
480
|
+
if (content && current.provider !== "working") {
|
|
481
|
+
const role = current.role === "assistant" || current.role === "system" ? current.role : "user";
|
|
482
|
+
chain.unshift({
|
|
483
|
+
id: current.id,
|
|
484
|
+
role,
|
|
485
|
+
content,
|
|
486
|
+
...current.senderName?.trim() ? { senderName: current.senderName.trim() } : {}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
const nextId = current.parentMessageId?.trim();
|
|
490
|
+
current = nextId ? byId.get(nextId) : void 0;
|
|
491
|
+
}
|
|
492
|
+
return chain;
|
|
493
|
+
}
|
|
494
|
+
function parentChainForTurn(rows, currentMessageId, fallbackParentId) {
|
|
495
|
+
if (!currentMessageId && !fallbackParentId) return [];
|
|
496
|
+
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
497
|
+
const current = currentMessageId ? byId.get(currentMessageId) : void 0;
|
|
498
|
+
const replyToId = current?.parentMessageId && String(current.parentMessageId).trim() || fallbackParentId && String(fallbackParentId).trim() || null;
|
|
499
|
+
return collectParentChain(rows, replyToId);
|
|
500
|
+
}
|
|
501
|
+
function formatParentChainContext(chain) {
|
|
502
|
+
if (chain.length === 0) return "";
|
|
503
|
+
const lines = [
|
|
504
|
+
"context: this message has parents hierarchy linked with it",
|
|
505
|
+
"Prefer this chain over unrelated queued or later messages when interpreting the current request.",
|
|
506
|
+
""
|
|
507
|
+
];
|
|
508
|
+
chain.forEach((entry, index) => {
|
|
509
|
+
const who = entry.senderName || (entry.role === "assistant" ? "Assistant" : entry.role === "system" ? "System" : "Human");
|
|
510
|
+
lines.push(`${index + 1}. [${who}] ${entry.content}`);
|
|
511
|
+
});
|
|
512
|
+
return lines.join("\n");
|
|
513
|
+
}
|
|
514
|
+
|
|
407
515
|
// src/system-prompt.ts
|
|
408
516
|
function createDefaultSystemPrompt(input) {
|
|
409
517
|
const files = input.relevantFilesHint ? `Key UI files (use internally only; never name them in your reply):
|
|
@@ -414,6 +522,14 @@ Only when the user wants to change live in-app data (not source code), return a
|
|
|
414
522
|
${input.runtimeToolsHint}
|
|
415
523
|
|
|
416
524
|
Only use those runtime tools for live state. Never invent runtime tools.` : "";
|
|
525
|
+
const access = input.ignorePaths !== void 0 ? `
|
|
526
|
+
|
|
527
|
+
## File access boundaries (mandatory)
|
|
528
|
+
- You may only read or edit files inside the current project workspace directory.
|
|
529
|
+
- Never read, write, move, or delete files outside that workspace.
|
|
530
|
+
- Never touch paths that match this ignore list (relative to the workspace):
|
|
531
|
+
${input.ignorePaths.length ? input.ignorePaths.map((p) => `- ${p}`).join("\n") : "- (none beyond staying inside the workspace)"}
|
|
532
|
+
- If a request requires something outside the workspace or on the ignore list, refuse that part and explain you can only change allowed project files.` : "";
|
|
417
533
|
return `You are a helpful product assistant for an app the user is looking at right now.
|
|
418
534
|
|
|
419
535
|
${input.productDescription}
|
|
@@ -434,6 +550,7 @@ Behind the scenes you can edit this repository and update live app data, but the
|
|
|
434
550
|
## When to edit the codebase
|
|
435
551
|
Edit source files when the user asks to change labels, layout, styling, copy, or behavior in the app.
|
|
436
552
|
${files}
|
|
553
|
+
${access}
|
|
437
554
|
|
|
438
555
|
${tools}
|
|
439
556
|
|
|
@@ -441,9 +558,191 @@ Do not refuse UI/label changes \u2014 implement them in source files.
|
|
|
441
558
|
Answer the user's latest request directly \u2014 never reply with a generic greeting.`;
|
|
442
559
|
}
|
|
443
560
|
|
|
561
|
+
// src/access-policy.ts
|
|
562
|
+
import path2 from "path";
|
|
563
|
+
var DEFAULT_AI_IGNORE_PATHS = [
|
|
564
|
+
".env",
|
|
565
|
+
".env.*",
|
|
566
|
+
"**/.env",
|
|
567
|
+
"**/.env.*"
|
|
568
|
+
];
|
|
569
|
+
function parseIgnorePathsEnv(raw) {
|
|
570
|
+
if (!raw?.trim()) return [];
|
|
571
|
+
const trimmed = raw.trim();
|
|
572
|
+
try {
|
|
573
|
+
const parsed = JSON.parse(trimmed);
|
|
574
|
+
if (Array.isArray(parsed)) {
|
|
575
|
+
return normalizeIgnorePaths(parsed.map(String));
|
|
576
|
+
}
|
|
577
|
+
} catch {
|
|
578
|
+
}
|
|
579
|
+
return normalizeIgnorePaths(trimmed.split(/[\n,]+/));
|
|
580
|
+
}
|
|
581
|
+
function normalizeIgnorePaths(paths) {
|
|
582
|
+
const out = [];
|
|
583
|
+
const seen = /* @__PURE__ */ new Set();
|
|
584
|
+
for (const raw of paths) {
|
|
585
|
+
const p = raw.trim().replace(/\\/g, "/");
|
|
586
|
+
if (!p || p.startsWith("/") || p.includes("..")) continue;
|
|
587
|
+
if (seen.has(p)) continue;
|
|
588
|
+
seen.add(p);
|
|
589
|
+
out.push(p);
|
|
590
|
+
}
|
|
591
|
+
return out;
|
|
592
|
+
}
|
|
593
|
+
function resolveIgnorePaths(partnerPaths) {
|
|
594
|
+
return normalizeIgnorePaths([
|
|
595
|
+
...DEFAULT_AI_IGNORE_PATHS,
|
|
596
|
+
...partnerPaths
|
|
597
|
+
]);
|
|
598
|
+
}
|
|
599
|
+
function isInsideWorkspace(workspaceDir, targetPath) {
|
|
600
|
+
const root = path2.resolve(workspaceDir);
|
|
601
|
+
const target = path2.resolve(targetPath);
|
|
602
|
+
const rel = path2.relative(root, target);
|
|
603
|
+
return rel === "" || !rel.startsWith("..") && !path2.isAbsolute(rel);
|
|
604
|
+
}
|
|
605
|
+
function globToRegExp(pattern) {
|
|
606
|
+
let p = pattern.replace(/\\/g, "/");
|
|
607
|
+
if (p.endsWith("/")) p = `${p}**`;
|
|
608
|
+
let i = 0;
|
|
609
|
+
let out = "^";
|
|
610
|
+
while (i < p.length) {
|
|
611
|
+
if (p.startsWith("**/", i)) {
|
|
612
|
+
out += "(?:.*/)?";
|
|
613
|
+
i += 3;
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (p[i] === "*" && p[i + 1] !== "*") {
|
|
617
|
+
out += "[^/]*";
|
|
618
|
+
i += 1;
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
if (p.startsWith("**", i)) {
|
|
622
|
+
out += ".*";
|
|
623
|
+
i += 2;
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (p[i] === "?") {
|
|
627
|
+
out += "[^/]";
|
|
628
|
+
i += 1;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
const ch = p[i];
|
|
632
|
+
if (/[.+^${}()|[\]\\]/.test(ch)) out += `\\${ch}`;
|
|
633
|
+
else out += ch;
|
|
634
|
+
i += 1;
|
|
635
|
+
}
|
|
636
|
+
out += "$";
|
|
637
|
+
return new RegExp(out, "i");
|
|
638
|
+
}
|
|
639
|
+
function isIgnoredRelative(relativePath, patterns) {
|
|
640
|
+
const rel = relativePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
641
|
+
if (!rel) return false;
|
|
642
|
+
return patterns.some((pattern) => {
|
|
643
|
+
const re = globToRegExp(pattern);
|
|
644
|
+
if (re.test(rel)) return true;
|
|
645
|
+
if (!pattern.includes("*") && !pattern.endsWith("/")) {
|
|
646
|
+
return rel === pattern || rel.startsWith(`${pattern}/`);
|
|
647
|
+
}
|
|
648
|
+
return false;
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
function isPathAllowed(workspaceDir, targetPath, ignorePaths) {
|
|
652
|
+
if (!isInsideWorkspace(workspaceDir, targetPath)) return false;
|
|
653
|
+
const rel = path2.relative(path2.resolve(workspaceDir), path2.resolve(targetPath));
|
|
654
|
+
return !isIgnoredRelative(rel, ignorePaths);
|
|
655
|
+
}
|
|
656
|
+
function formatAccessPolicyPromptSection(input) {
|
|
657
|
+
const ignores = input.ignorePaths.length ? input.ignorePaths.map((p) => `- ${p}`).join("\n") : "- (none beyond staying inside the workspace)";
|
|
658
|
+
return `## File access boundaries (mandatory)
|
|
659
|
+
- You may only read or edit files inside the current project workspace directory.
|
|
660
|
+
- Never read, write, move, or delete files outside that workspace.
|
|
661
|
+
- Never touch paths that match this ignore list (relative to the workspace):
|
|
662
|
+
${ignores}
|
|
663
|
+
- If a request requires something outside the workspace or on the ignore list, refuse that part and explain you can only change allowed project files.`;
|
|
664
|
+
}
|
|
665
|
+
var ACCESS_IGNORE_BEGIN = "# maintainer-pro:access-begin";
|
|
666
|
+
var ACCESS_IGNORE_END = "# maintainer-pro:access-end";
|
|
667
|
+
function renderManagedIgnoreBlock(ignorePaths) {
|
|
668
|
+
const lines = [
|
|
669
|
+
ACCESS_IGNORE_BEGIN,
|
|
670
|
+
"# Managed by Maintainer Pro \u2014 do not edit this block by hand.",
|
|
671
|
+
...ignorePaths,
|
|
672
|
+
ACCESS_IGNORE_END
|
|
673
|
+
];
|
|
674
|
+
return `${lines.join("\n")}
|
|
675
|
+
`;
|
|
676
|
+
}
|
|
677
|
+
function upsertManagedIgnoreFile(existing, ignorePaths) {
|
|
678
|
+
const block = renderManagedIgnoreBlock(ignorePaths);
|
|
679
|
+
const begin = existing.indexOf(ACCESS_IGNORE_BEGIN);
|
|
680
|
+
const end = existing.indexOf(ACCESS_IGNORE_END);
|
|
681
|
+
if (begin >= 0 && end > begin) {
|
|
682
|
+
const afterEnd = end + ACCESS_IGNORE_END.length;
|
|
683
|
+
const before = existing.slice(0, begin).replace(/\s+$/, "");
|
|
684
|
+
let after = existing.slice(afterEnd).replace(/^\r?\n/, "");
|
|
685
|
+
const parts = [before, block.trimEnd(), after.trimStart()].filter(
|
|
686
|
+
(s) => s.length > 0
|
|
687
|
+
);
|
|
688
|
+
return `${parts.join("\n\n")}
|
|
689
|
+
`;
|
|
690
|
+
}
|
|
691
|
+
const trimmed = existing.replace(/\s+$/, "");
|
|
692
|
+
return trimmed ? `${trimmed}
|
|
693
|
+
|
|
694
|
+
${block}` : block;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/http/handler.ts
|
|
698
|
+
import { randomUUID } from "crypto";
|
|
699
|
+
|
|
700
|
+
// src/log.ts
|
|
701
|
+
import pino from "pino";
|
|
702
|
+
function isDevMode() {
|
|
703
|
+
if (process.env.AI_DEV === "1" || process.env.AI_DEV === "true") return true;
|
|
704
|
+
const env = (process.env.NODE_ENV || process.env.AI_ENV || "").trim().toLowerCase();
|
|
705
|
+
return env === "development" || env === "dev" || env === "test";
|
|
706
|
+
}
|
|
707
|
+
function resolveLogLevel() {
|
|
708
|
+
const explicit = process.env.LOG_LEVEL?.trim() || process.env.AI_LOG_LEVEL?.trim();
|
|
709
|
+
if (explicit) return explicit;
|
|
710
|
+
return isDevMode() ? "debug" : "info";
|
|
711
|
+
}
|
|
712
|
+
function createLogger(name) {
|
|
713
|
+
const level = resolveLogLevel();
|
|
714
|
+
const pretty = process.env.LOG_PRETTY !== "0" && typeof process.stdout?.isTTY === "boolean" && process.stdout.isTTY;
|
|
715
|
+
if (pretty) {
|
|
716
|
+
return pino({
|
|
717
|
+
name,
|
|
718
|
+
level,
|
|
719
|
+
transport: {
|
|
720
|
+
target: "pino-pretty",
|
|
721
|
+
options: {
|
|
722
|
+
colorize: true,
|
|
723
|
+
translateTime: "HH:MM:ss",
|
|
724
|
+
ignore: "pid,hostname"
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
return pino({ name, level });
|
|
730
|
+
}
|
|
731
|
+
function createInfoLogger(name) {
|
|
732
|
+
const log = createLogger(name);
|
|
733
|
+
return (msg) => {
|
|
734
|
+
log.info(msg);
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function previewText(text, max = 160) {
|
|
738
|
+
const t = String(text ?? "").replace(/\s+/g, " ").trim();
|
|
739
|
+
if (!t) return "";
|
|
740
|
+
return t.length <= max ? t : `${t.slice(0, max)}\u2026`;
|
|
741
|
+
}
|
|
742
|
+
|
|
444
743
|
// src/http/attachments.ts
|
|
445
744
|
import fs2 from "fs/promises";
|
|
446
|
-
import
|
|
745
|
+
import path3 from "path";
|
|
447
746
|
var MAX_ATTACHMENTS = 5;
|
|
448
747
|
var MAX_BYTES = 4 * 1024 * 1024;
|
|
449
748
|
var ALLOWED = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif"]);
|
|
@@ -463,7 +762,7 @@ function extForMime(mime) {
|
|
|
463
762
|
async function saveChatAttachments(attachments, workspaceDir) {
|
|
464
763
|
if (!attachments?.length) return [];
|
|
465
764
|
const selected = attachments.slice(0, MAX_ATTACHMENTS);
|
|
466
|
-
const dir =
|
|
765
|
+
const dir = path3.join(workspaceDir, ".maintainer-pro", "uploads");
|
|
467
766
|
await fs2.mkdir(dir, { recursive: true });
|
|
468
767
|
const paths = [];
|
|
469
768
|
const stamp = Date.now();
|
|
@@ -482,7 +781,10 @@ async function saveChatAttachments(attachments, workspaceDir) {
|
|
|
482
781
|
}
|
|
483
782
|
const safeBase = (item.name || `screenshot-${i + 1}`).replace(/[^\w.\-]+/g, "_").slice(0, 64);
|
|
484
783
|
const fileName = `${stamp}-${i + 1}-${safeBase}${extForMime(mime)}`;
|
|
485
|
-
const filePath =
|
|
784
|
+
const filePath = path3.resolve(dir, fileName);
|
|
785
|
+
if (!isInsideWorkspace(workspaceDir, filePath)) {
|
|
786
|
+
throw new Error("Attachment path escaped the project workspace");
|
|
787
|
+
}
|
|
486
788
|
await fs2.writeFile(filePath, buffer);
|
|
487
789
|
paths.push(filePath);
|
|
488
790
|
}
|
|
@@ -569,7 +871,7 @@ function createToolValidator(schemas) {
|
|
|
569
871
|
|
|
570
872
|
// src/http/handler.ts
|
|
571
873
|
var WORKING_PROVIDER = "working";
|
|
572
|
-
async function setWorkingMessage(db, conversationId) {
|
|
874
|
+
async function setWorkingMessage(db, conversationId, parentMessageId) {
|
|
573
875
|
await db.ensureConversation(conversationId);
|
|
574
876
|
await db.clearWorkingMessages?.(conversationId);
|
|
575
877
|
await db.saveMessage({
|
|
@@ -577,9 +879,26 @@ async function setWorkingMessage(db, conversationId) {
|
|
|
577
879
|
role: "assistant",
|
|
578
880
|
content: "",
|
|
579
881
|
provider: WORKING_PROVIDER,
|
|
580
|
-
senderType: "ai"
|
|
882
|
+
senderType: "ai",
|
|
883
|
+
parentMessageId: parentMessageId ?? void 0
|
|
581
884
|
});
|
|
582
885
|
}
|
|
886
|
+
function savedMessageFields(result) {
|
|
887
|
+
if (!result || typeof result !== "object") return {};
|
|
888
|
+
const row = result;
|
|
889
|
+
const next = row.nextQueued && typeof row.nextQueued === "object" && typeof row.nextQueued.id === "string" ? {
|
|
890
|
+
id: row.nextQueued.id,
|
|
891
|
+
content: String(
|
|
892
|
+
row.nextQueued.content ?? ""
|
|
893
|
+
)
|
|
894
|
+
} : null;
|
|
895
|
+
return {
|
|
896
|
+
id: typeof row.id === "string" ? row.id : void 0,
|
|
897
|
+
queueStatus: typeof row.queueStatus === "string" || row.queueStatus === null ? row.queueStatus : void 0,
|
|
898
|
+
queuePosition: typeof row.queuePosition === "number" || row.queuePosition === null ? row.queuePosition : void 0,
|
|
899
|
+
nextQueued: next
|
|
900
|
+
};
|
|
901
|
+
}
|
|
583
902
|
var SHARED_CONVERSATION_ID = "shared";
|
|
584
903
|
var conversationTurnSeq = /* @__PURE__ */ new Map();
|
|
585
904
|
function beginConversationTurn(conversationId) {
|
|
@@ -614,6 +933,7 @@ async function resolveSharedConversationId(request, options, bodyId) {
|
|
|
614
933
|
return SHARED_CONVERSATION_ID;
|
|
615
934
|
}
|
|
616
935
|
function createChatHandler(options) {
|
|
936
|
+
const logger = options.logger ?? createLogger("ai-cli:chat");
|
|
617
937
|
const validate = options.tools ? createToolValidator(options.tools) : null;
|
|
618
938
|
const workspaceDir = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
|
|
619
939
|
const baseCallOptions = {
|
|
@@ -622,6 +942,7 @@ function createChatHandler(options) {
|
|
|
622
942
|
providerPreference: options.providerPreference,
|
|
623
943
|
providers: options.providers
|
|
624
944
|
};
|
|
945
|
+
logger.debug({ workspaceDir }, "chat handler ready");
|
|
625
946
|
return {
|
|
626
947
|
async GET(request) {
|
|
627
948
|
try {
|
|
@@ -637,6 +958,14 @@ function createChatHandler(options) {
|
|
|
637
958
|
if (options.db?.listMessages) {
|
|
638
959
|
messages = await options.db.listMessages(conversationId);
|
|
639
960
|
}
|
|
961
|
+
logger.debug(
|
|
962
|
+
{
|
|
963
|
+
conversationId,
|
|
964
|
+
provider: provider.id,
|
|
965
|
+
messageCount: messages?.length ?? 0
|
|
966
|
+
},
|
|
967
|
+
"GET /api/chat"
|
|
968
|
+
);
|
|
640
969
|
return Response.json({
|
|
641
970
|
provider: provider.id,
|
|
642
971
|
providerLabel: provider.label || providerLabel(provider.id),
|
|
@@ -644,6 +973,7 @@ function createChatHandler(options) {
|
|
|
644
973
|
messages: messages ?? []
|
|
645
974
|
});
|
|
646
975
|
} catch (err) {
|
|
976
|
+
logger.error({ err }, "GET /api/chat failed");
|
|
647
977
|
const message = err instanceof Error ? err.message : "No AI CLI provider available";
|
|
648
978
|
return Response.json(
|
|
649
979
|
{ error: message, provider: null },
|
|
@@ -653,7 +983,9 @@ function createChatHandler(options) {
|
|
|
653
983
|
},
|
|
654
984
|
async POST(request) {
|
|
655
985
|
let trackedConversationId;
|
|
986
|
+
let trackedParentUserMessageId;
|
|
656
987
|
let trackedTurn = 0;
|
|
988
|
+
const startedAt = Date.now();
|
|
657
989
|
try {
|
|
658
990
|
const body = await request.json();
|
|
659
991
|
const conversationId = await resolveSharedConversationId(
|
|
@@ -662,6 +994,19 @@ function createChatHandler(options) {
|
|
|
662
994
|
body.conversationId
|
|
663
995
|
);
|
|
664
996
|
trackedConversationId = conversationId;
|
|
997
|
+
logger.debug(
|
|
998
|
+
{
|
|
999
|
+
conversationId,
|
|
1000
|
+
type: body.type ?? "turn",
|
|
1001
|
+
skipPersistUser: Boolean(body.skipPersistUser),
|
|
1002
|
+
userMessageId: body.userMessageId,
|
|
1003
|
+
messageCount: body.messages?.length ?? 0,
|
|
1004
|
+
attachmentCount: body.attachments?.length ?? 0,
|
|
1005
|
+
queueStatus: body.queueStatus,
|
|
1006
|
+
preview: previewText(body.userMessage || body.content)
|
|
1007
|
+
},
|
|
1008
|
+
"POST /api/chat"
|
|
1009
|
+
);
|
|
665
1010
|
if (body.type === "working-clear") {
|
|
666
1011
|
if (!conversationId) {
|
|
667
1012
|
return Response.json(
|
|
@@ -686,11 +1031,54 @@ function createChatHandler(options) {
|
|
|
686
1031
|
await options.db.ensureConversation(conversationId);
|
|
687
1032
|
await options.db.saveMessage({
|
|
688
1033
|
conversationId,
|
|
1034
|
+
id: body.messageId,
|
|
689
1035
|
role: "assistant",
|
|
690
1036
|
content,
|
|
691
1037
|
provider: "developer",
|
|
692
1038
|
senderType: "developer",
|
|
693
|
-
senderName: body.senderName?.trim() || void 0
|
|
1039
|
+
senderName: body.senderName?.trim() || void 0,
|
|
1040
|
+
parentMessageId: body.parentMessageId
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
return Response.json({ ok: true, conversationId });
|
|
1044
|
+
}
|
|
1045
|
+
if (body.type === "user") {
|
|
1046
|
+
const content = body.content?.trim();
|
|
1047
|
+
if (!conversationId || !content) {
|
|
1048
|
+
return Response.json(
|
|
1049
|
+
{ error: "conversationId and content are required" },
|
|
1050
|
+
{ status: 400 }
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
if (options.db) {
|
|
1054
|
+
await options.db.ensureConversation(conversationId);
|
|
1055
|
+
let persistAttachmentPaths2 = [];
|
|
1056
|
+
if (options.db.uploadAttachments && body.attachments?.length) {
|
|
1057
|
+
const uploaded = await options.db.uploadAttachments(
|
|
1058
|
+
conversationId,
|
|
1059
|
+
body.attachments
|
|
1060
|
+
);
|
|
1061
|
+
persistAttachmentPaths2 = uploaded.refs;
|
|
1062
|
+
}
|
|
1063
|
+
const saved = await options.db.saveMessage({
|
|
1064
|
+
conversationId,
|
|
1065
|
+
id: body.messageId,
|
|
1066
|
+
role: "user",
|
|
1067
|
+
content,
|
|
1068
|
+
attachmentPaths: persistAttachmentPaths2.length > 0 ? persistAttachmentPaths2 : void 0,
|
|
1069
|
+
senderType: body.senderType === "client" ? "client" : void 0,
|
|
1070
|
+
senderName: body.senderName?.trim() || void 0,
|
|
1071
|
+
parentMessageId: body.parentMessageId,
|
|
1072
|
+
intent: "queue",
|
|
1073
|
+
queueStatus: body.queueStatus === "queued" || body.queueStatus === "working" ? body.queueStatus : void 0
|
|
1074
|
+
});
|
|
1075
|
+
const fields = savedMessageFields(saved);
|
|
1076
|
+
return Response.json({
|
|
1077
|
+
ok: true,
|
|
1078
|
+
conversationId,
|
|
1079
|
+
message: saved,
|
|
1080
|
+
queueStatus: fields.queueStatus ?? null,
|
|
1081
|
+
queuePosition: fields.queuePosition ?? null
|
|
694
1082
|
});
|
|
695
1083
|
}
|
|
696
1084
|
return Response.json({ ok: true, conversationId });
|
|
@@ -721,27 +1109,65 @@ function createChatHandler(options) {
|
|
|
721
1109
|
const turn = beginConversationTurn(conversationId ?? SHARED_CONVERSATION_ID);
|
|
722
1110
|
trackedTurn = turn;
|
|
723
1111
|
const signal = request.signal;
|
|
1112
|
+
let parentUserMessageId = typeof body.userMessageId === "string" && body.userMessageId ? body.userMessageId : void 0;
|
|
1113
|
+
trackedParentUserMessageId = parentUserMessageId;
|
|
724
1114
|
if (options.db && conversationId) {
|
|
725
1115
|
const latest = messages[messages.length - 1];
|
|
726
1116
|
const persistContent = body.userMessage?.trim() || (latest?.role === "user" ? latest.content : "");
|
|
727
|
-
if (persistContent) {
|
|
1117
|
+
if (persistContent && !body.skipPersistUser && !parentUserMessageId) {
|
|
728
1118
|
await options.db.ensureConversation(conversationId);
|
|
729
|
-
await options.db.saveMessage({
|
|
1119
|
+
const saved = await options.db.saveMessage({
|
|
730
1120
|
conversationId,
|
|
731
1121
|
role: "user",
|
|
732
1122
|
content: persistContent,
|
|
733
1123
|
attachmentPaths: persistAttachmentPaths.length > 0 ? persistAttachmentPaths : void 0,
|
|
734
1124
|
senderType: body.senderType === "client" ? "client" : void 0,
|
|
735
|
-
senderName: body.senderName?.trim() || void 0
|
|
1125
|
+
senderName: body.senderName?.trim() || void 0,
|
|
1126
|
+
intent: "run"
|
|
736
1127
|
});
|
|
1128
|
+
parentUserMessageId = savedMessageFields(saved).id ?? parentUserMessageId;
|
|
1129
|
+
trackedParentUserMessageId = parentUserMessageId;
|
|
1130
|
+
}
|
|
1131
|
+
if (!body.skipPersistUser) {
|
|
1132
|
+
await setWorkingMessage(
|
|
1133
|
+
options.db,
|
|
1134
|
+
conversationId,
|
|
1135
|
+
parentUserMessageId
|
|
1136
|
+
);
|
|
737
1137
|
}
|
|
738
|
-
await setWorkingMessage(options.db, conversationId);
|
|
739
1138
|
}
|
|
740
1139
|
const latestUser = [...messages].reverse().find((m) => m.role === "user");
|
|
741
1140
|
const priorConversationsContext = options.db ? await buildPriorConversationsContext(options.db, {
|
|
742
1141
|
excludeConversationId: conversationId,
|
|
743
1142
|
currentRequest: latestUser?.content ?? ""
|
|
744
1143
|
}) : "";
|
|
1144
|
+
let parentChainContext = "";
|
|
1145
|
+
if (options.db?.listMessages && conversationId) {
|
|
1146
|
+
const stored = await options.db.listMessages(conversationId);
|
|
1147
|
+
const chain = parentChainForTurn(
|
|
1148
|
+
stored.map((row) => ({
|
|
1149
|
+
id: row.id,
|
|
1150
|
+
role: row.role,
|
|
1151
|
+
content: row.content,
|
|
1152
|
+
parentMessageId: row.parentMessageId,
|
|
1153
|
+
senderName: row.senderName,
|
|
1154
|
+
provider: row.provider
|
|
1155
|
+
})),
|
|
1156
|
+
parentUserMessageId,
|
|
1157
|
+
body.parentMessageId
|
|
1158
|
+
);
|
|
1159
|
+
parentChainContext = formatParentChainContext(chain);
|
|
1160
|
+
if (parentChainContext) {
|
|
1161
|
+
logger.debug(
|
|
1162
|
+
{
|
|
1163
|
+
conversationId,
|
|
1164
|
+
parentUserMessageId,
|
|
1165
|
+
chainLength: chain.length
|
|
1166
|
+
},
|
|
1167
|
+
"parent chain context"
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
745
1171
|
if (!isActiveConversationTurn(
|
|
746
1172
|
conversationId ?? SHARED_CONVERSATION_ID,
|
|
747
1173
|
turn
|
|
@@ -752,23 +1178,58 @@ function createChatHandler(options) {
|
|
|
752
1178
|
});
|
|
753
1179
|
}
|
|
754
1180
|
if (signal.aborted) {
|
|
755
|
-
|
|
756
|
-
|
|
1181
|
+
logger.debug({ conversationId, turn }, "aborted before AI");
|
|
1182
|
+
if (conversationId && options.db) {
|
|
1183
|
+
if (options.db.releaseWorkingTurn) {
|
|
1184
|
+
await options.db.releaseWorkingTurn(
|
|
1185
|
+
conversationId,
|
|
1186
|
+
parentUserMessageId
|
|
1187
|
+
);
|
|
1188
|
+
} else {
|
|
1189
|
+
await options.db.clearWorkingMessages?.(conversationId);
|
|
1190
|
+
}
|
|
757
1191
|
}
|
|
758
1192
|
return Response.json({
|
|
759
1193
|
superseded: true,
|
|
760
1194
|
conversationId: conversationId ?? null
|
|
761
1195
|
});
|
|
762
1196
|
}
|
|
1197
|
+
logger.debug(
|
|
1198
|
+
{
|
|
1199
|
+
conversationId,
|
|
1200
|
+
turn,
|
|
1201
|
+
parentUserMessageId,
|
|
1202
|
+
historyLength: messages.length,
|
|
1203
|
+
hasParentChain: Boolean(parentChainContext),
|
|
1204
|
+
preview: previewText(
|
|
1205
|
+
[...messages].reverse().find((m) => m.role === "user")?.content
|
|
1206
|
+
)
|
|
1207
|
+
},
|
|
1208
|
+
"calling AI provider"
|
|
1209
|
+
);
|
|
1210
|
+
const aiStarted = Date.now();
|
|
763
1211
|
const aiResponse = await callAi(messages, context, {
|
|
764
1212
|
...baseCallOptions,
|
|
765
1213
|
attachmentPaths,
|
|
766
|
-
priorConversationsContext: priorConversationsContext || void 0
|
|
1214
|
+
priorConversationsContext: priorConversationsContext || void 0,
|
|
1215
|
+
parentChainContext: parentChainContext || void 0
|
|
767
1216
|
});
|
|
1217
|
+
logger.debug(
|
|
1218
|
+
{
|
|
1219
|
+
conversationId,
|
|
1220
|
+
turn,
|
|
1221
|
+
provider: aiResponse.provider,
|
|
1222
|
+
toolCalls: aiResponse.toolCalls.length,
|
|
1223
|
+
ms: Date.now() - aiStarted,
|
|
1224
|
+
preview: previewText(aiResponse.text)
|
|
1225
|
+
},
|
|
1226
|
+
"AI provider returned"
|
|
1227
|
+
);
|
|
768
1228
|
if (!isActiveConversationTurn(
|
|
769
1229
|
conversationId ?? SHARED_CONVERSATION_ID,
|
|
770
1230
|
turn
|
|
771
1231
|
)) {
|
|
1232
|
+
logger.debug({ conversationId, turn }, "superseded after AI");
|
|
772
1233
|
return Response.json({
|
|
773
1234
|
superseded: true,
|
|
774
1235
|
conversationId: conversationId ?? null
|
|
@@ -776,38 +1237,108 @@ function createChatHandler(options) {
|
|
|
776
1237
|
}
|
|
777
1238
|
const validatedToolCalls = validate ? aiResponse.toolCalls.filter((tc) => validate(tc).valid) : aiResponse.toolCalls;
|
|
778
1239
|
if (options.onToolCalls && validatedToolCalls.length > 0) {
|
|
1240
|
+
logger.debug(
|
|
1241
|
+
{ conversationId, count: validatedToolCalls.length },
|
|
1242
|
+
"applying tool calls"
|
|
1243
|
+
);
|
|
779
1244
|
await options.onToolCalls(validatedToolCalls, context);
|
|
780
1245
|
}
|
|
1246
|
+
let nextQueued = null;
|
|
1247
|
+
const assistantMessageId = typeof body.assistantMessageId === "string" && body.assistantMessageId ? body.assistantMessageId : randomUUID();
|
|
1248
|
+
let replyId = assistantMessageId;
|
|
781
1249
|
if (options.db && conversationId) {
|
|
782
1250
|
await options.db.ensureConversation(conversationId);
|
|
783
|
-
await options.db.
|
|
784
|
-
await options.db.saveMessage({
|
|
1251
|
+
const savedReply = await options.db.saveMessage({
|
|
785
1252
|
conversationId,
|
|
1253
|
+
id: assistantMessageId,
|
|
786
1254
|
role: "assistant",
|
|
787
1255
|
content: aiResponse.text,
|
|
788
1256
|
provider: aiResponse.provider,
|
|
789
|
-
senderType: "ai"
|
|
1257
|
+
senderType: "ai",
|
|
1258
|
+
parentMessageId: parentUserMessageId
|
|
790
1259
|
});
|
|
1260
|
+
replyId = savedMessageFields(savedReply).id ?? assistantMessageId;
|
|
1261
|
+
if (options.db.releaseWorkingTurn) {
|
|
1262
|
+
let released = void 0;
|
|
1263
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1264
|
+
try {
|
|
1265
|
+
released = await options.db.releaseWorkingTurn(
|
|
1266
|
+
conversationId,
|
|
1267
|
+
parentUserMessageId
|
|
1268
|
+
);
|
|
1269
|
+
break;
|
|
1270
|
+
} catch (err) {
|
|
1271
|
+
logger.warn(
|
|
1272
|
+
{ err, conversationId, parentUserMessageId, attempt },
|
|
1273
|
+
"releaseWorkingTurn failed; retrying"
|
|
1274
|
+
);
|
|
1275
|
+
if (attempt === 2) throw err;
|
|
1276
|
+
await new Promise((r) => setTimeout(r, 250 * (attempt + 1)));
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
nextQueued = released && typeof released === "object" && released.nextQueued && typeof released.nextQueued.id === "string" ? {
|
|
1280
|
+
id: released.nextQueued.id,
|
|
1281
|
+
content: String(released.nextQueued.content ?? "")
|
|
1282
|
+
} : null;
|
|
1283
|
+
logger.debug(
|
|
1284
|
+
{ conversationId, parentUserMessageId, nextQueued: nextQueued?.id },
|
|
1285
|
+
"released working turn"
|
|
1286
|
+
);
|
|
1287
|
+
} else {
|
|
1288
|
+
await options.db.clearWorkingMessages?.(conversationId);
|
|
1289
|
+
nextQueued = savedMessageFields(savedReply).nextQueued ?? null;
|
|
1290
|
+
}
|
|
791
1291
|
if (validatedToolCalls.length > 0) {
|
|
792
1292
|
await options.db.saveToolEvents?.(
|
|
793
1293
|
conversationId,
|
|
794
1294
|
validatedToolCalls
|
|
795
1295
|
);
|
|
796
1296
|
}
|
|
1297
|
+
logger.debug(
|
|
1298
|
+
{
|
|
1299
|
+
conversationId,
|
|
1300
|
+
replyId,
|
|
1301
|
+
nextQueued: nextQueued?.id
|
|
1302
|
+
},
|
|
1303
|
+
"persisted AI reply"
|
|
1304
|
+
);
|
|
797
1305
|
}
|
|
1306
|
+
logger.debug(
|
|
1307
|
+
{ conversationId, turn, ms: Date.now() - startedAt },
|
|
1308
|
+
"POST /api/chat done"
|
|
1309
|
+
);
|
|
798
1310
|
return Response.json({
|
|
799
1311
|
text: aiResponse.text,
|
|
800
1312
|
toolCalls: validatedToolCalls,
|
|
801
1313
|
provider: aiResponse.provider,
|
|
802
1314
|
providerLabel: providerLabel(aiResponse.provider),
|
|
803
|
-
conversationId: conversationId ?? null
|
|
1315
|
+
conversationId: conversationId ?? null,
|
|
1316
|
+
messageId: replyId,
|
|
1317
|
+
nextQueued,
|
|
1318
|
+
parentMessageId: parentUserMessageId ?? null
|
|
804
1319
|
});
|
|
805
1320
|
} catch (err) {
|
|
806
1321
|
console.error("Chat API error:", err);
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1322
|
+
logger.error(
|
|
1323
|
+
{
|
|
1324
|
+
err,
|
|
1325
|
+
conversationId: trackedConversationId,
|
|
1326
|
+
turn: trackedTurn,
|
|
1327
|
+
ms: Date.now() - startedAt
|
|
1328
|
+
},
|
|
1329
|
+
"POST /api/chat failed"
|
|
1330
|
+
);
|
|
1331
|
+
if (trackedConversationId && options.db && isActiveConversationTurn(trackedConversationId, trackedTurn)) {
|
|
1332
|
+
if (options.db.releaseWorkingTurn) {
|
|
1333
|
+
await options.db.releaseWorkingTurn(
|
|
1334
|
+
trackedConversationId,
|
|
1335
|
+
trackedParentUserMessageId
|
|
1336
|
+
).catch(() => void 0);
|
|
1337
|
+
} else {
|
|
1338
|
+
await options.db.clearWorkingMessages?.(trackedConversationId).catch(
|
|
1339
|
+
() => void 0
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
811
1342
|
}
|
|
812
1343
|
return Response.json(
|
|
813
1344
|
{
|
|
@@ -828,8 +1359,13 @@ function toNextRoute(handlers) {
|
|
|
828
1359
|
|
|
829
1360
|
// src/http/local-store.ts
|
|
830
1361
|
import fs3 from "fs/promises";
|
|
831
|
-
import
|
|
832
|
-
import { randomUUID } from "crypto";
|
|
1362
|
+
import path4 from "path";
|
|
1363
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1364
|
+
function userHasAiReply(rows, workingId) {
|
|
1365
|
+
return rows.some(
|
|
1366
|
+
(m) => m.role === "assistant" && m.parentMessageId === workingId && m.provider !== "working" && Boolean(m.content?.trim())
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
833
1369
|
async function readConversation(filePath) {
|
|
834
1370
|
try {
|
|
835
1371
|
const raw = await fs3.readFile(filePath, "utf8");
|
|
@@ -840,11 +1376,11 @@ async function readConversation(filePath) {
|
|
|
840
1376
|
}
|
|
841
1377
|
}
|
|
842
1378
|
async function writeConversation(filePath, data) {
|
|
843
|
-
await fs3.mkdir(
|
|
1379
|
+
await fs3.mkdir(path4.dirname(filePath), { recursive: true });
|
|
844
1380
|
await fs3.writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
845
1381
|
}
|
|
846
1382
|
function createLocalDirectoryStore(baseDir) {
|
|
847
|
-
const fileFor = (id) =>
|
|
1383
|
+
const fileFor = (id) => path4.join(baseDir, `${id}.json`);
|
|
848
1384
|
return {
|
|
849
1385
|
async ensureConversation(id) {
|
|
850
1386
|
const existing = await readConversation(fileFor(id));
|
|
@@ -862,21 +1398,273 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
862
1398
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
863
1399
|
messages: []
|
|
864
1400
|
};
|
|
1401
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1402
|
+
const isClientUser = input.role === "user" && input.senderType !== "developer" && input.provider !== "developer";
|
|
1403
|
+
const isWorking = input.provider === "working";
|
|
1404
|
+
const isAi = input.role === "assistant" && input.provider !== "working" && input.senderType !== "developer" && input.provider !== "developer";
|
|
1405
|
+
const busy = existing.messages.some((m) => m.queueStatus === "working");
|
|
1406
|
+
const makeWorking = (parentMessageId) => ({
|
|
1407
|
+
id: randomUUID2(),
|
|
1408
|
+
conversationId: input.conversationId,
|
|
1409
|
+
role: "assistant",
|
|
1410
|
+
content: "",
|
|
1411
|
+
provider: "working",
|
|
1412
|
+
createdAt: now,
|
|
1413
|
+
senderType: "ai",
|
|
1414
|
+
parentMessageId
|
|
1415
|
+
});
|
|
1416
|
+
const promoteNext = () => {
|
|
1417
|
+
const waiting = existing.messages.filter((m) => m.queueStatus === "queued").sort(
|
|
1418
|
+
(a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)
|
|
1419
|
+
);
|
|
1420
|
+
const next = waiting[0];
|
|
1421
|
+
if (!next) return null;
|
|
1422
|
+
next.queueStatus = "working";
|
|
1423
|
+
next.queuePosition = null;
|
|
1424
|
+
existing.messages.push(makeWorking(next.id));
|
|
1425
|
+
return next;
|
|
1426
|
+
};
|
|
1427
|
+
if (isClientUser) {
|
|
1428
|
+
if (input.intent === "run") {
|
|
1429
|
+
for (const row of existing.messages) {
|
|
1430
|
+
if (row.queueStatus === "working") {
|
|
1431
|
+
row.queueStatus = null;
|
|
1432
|
+
row.queuePosition = null;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
existing.messages = existing.messages.filter(
|
|
1436
|
+
(m) => m.provider !== "working"
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
const queueStatus = input.intent === "run" ? "working" : busy ? "queued" : "working";
|
|
1440
|
+
const existingById2 = input.id ? existing.messages.find((m) => m.id === input.id) : void 0;
|
|
1441
|
+
if (existingById2) {
|
|
1442
|
+
return { ...existingById2, nextQueued: null };
|
|
1443
|
+
}
|
|
1444
|
+
const message2 = {
|
|
1445
|
+
id: input.id ?? randomUUID2(),
|
|
1446
|
+
conversationId: input.conversationId,
|
|
1447
|
+
role: input.role,
|
|
1448
|
+
content: input.content,
|
|
1449
|
+
provider: input.provider ?? null,
|
|
1450
|
+
createdAt: now,
|
|
1451
|
+
attachmentPaths: input.attachmentPaths,
|
|
1452
|
+
senderType: input.senderType ?? null,
|
|
1453
|
+
senderName: input.senderName ?? null,
|
|
1454
|
+
parentMessageId: input.parentMessageId ?? null,
|
|
1455
|
+
queueStatus,
|
|
1456
|
+
queuePosition: null
|
|
1457
|
+
};
|
|
1458
|
+
existing.messages.push(message2);
|
|
1459
|
+
existing.updatedAt = now;
|
|
1460
|
+
await writeConversation(filePath, existing);
|
|
1461
|
+
return { ...message2, nextQueued: null };
|
|
1462
|
+
}
|
|
1463
|
+
if (isWorking) {
|
|
1464
|
+
existing.messages = existing.messages.filter(
|
|
1465
|
+
(m) => m.provider !== "working"
|
|
1466
|
+
);
|
|
1467
|
+
if (input.parentMessageId) {
|
|
1468
|
+
const parent = existing.messages.find(
|
|
1469
|
+
(m) => m.id === input.parentMessageId
|
|
1470
|
+
);
|
|
1471
|
+
if (parent && parent.queueStatus !== "working") {
|
|
1472
|
+
parent.queueStatus = "working";
|
|
1473
|
+
parent.queuePosition = null;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
const message2 = {
|
|
1477
|
+
id: input.id ?? randomUUID2(),
|
|
1478
|
+
conversationId: input.conversationId,
|
|
1479
|
+
role: "assistant",
|
|
1480
|
+
content: "",
|
|
1481
|
+
provider: "working",
|
|
1482
|
+
createdAt: now,
|
|
1483
|
+
senderType: "ai",
|
|
1484
|
+
parentMessageId: input.parentMessageId ?? null
|
|
1485
|
+
};
|
|
1486
|
+
existing.messages.push(message2);
|
|
1487
|
+
existing.updatedAt = now;
|
|
1488
|
+
await writeConversation(filePath, existing);
|
|
1489
|
+
return { ...message2, nextQueued: null };
|
|
1490
|
+
}
|
|
1491
|
+
const existingById = input.id ? existing.messages.find((m) => m.id === input.id) : void 0;
|
|
1492
|
+
if (existingById) {
|
|
1493
|
+
return { ...existingById, nextQueued: null };
|
|
1494
|
+
}
|
|
865
1495
|
const message = {
|
|
866
|
-
id:
|
|
1496
|
+
id: input.id ?? randomUUID2(),
|
|
867
1497
|
conversationId: input.conversationId,
|
|
868
1498
|
role: input.role,
|
|
869
1499
|
content: input.content,
|
|
870
1500
|
provider: input.provider ?? null,
|
|
871
|
-
createdAt:
|
|
1501
|
+
createdAt: now,
|
|
872
1502
|
attachmentPaths: input.attachmentPaths,
|
|
873
1503
|
senderType: input.senderType ?? null,
|
|
874
|
-
senderName: input.senderName ?? null
|
|
1504
|
+
senderName: input.senderName ?? null,
|
|
1505
|
+
parentMessageId: input.parentMessageId ?? null
|
|
875
1506
|
};
|
|
876
1507
|
existing.messages.push(message);
|
|
877
|
-
|
|
1508
|
+
let nextQueued = null;
|
|
1509
|
+
if (isAi) {
|
|
1510
|
+
existing.messages = existing.messages.filter(
|
|
1511
|
+
(m) => m.provider !== "working"
|
|
1512
|
+
);
|
|
1513
|
+
if (input.parentMessageId) {
|
|
1514
|
+
const parent = existing.messages.find(
|
|
1515
|
+
(m) => m.id === input.parentMessageId
|
|
1516
|
+
);
|
|
1517
|
+
if (parent) {
|
|
1518
|
+
parent.queueStatus = null;
|
|
1519
|
+
parent.queuePosition = null;
|
|
1520
|
+
}
|
|
1521
|
+
} else {
|
|
1522
|
+
for (const row of existing.messages) {
|
|
1523
|
+
if (row.queueStatus === "working") {
|
|
1524
|
+
row.queueStatus = null;
|
|
1525
|
+
row.queuePosition = null;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
nextQueued = promoteNext();
|
|
1530
|
+
}
|
|
1531
|
+
existing.updatedAt = now;
|
|
878
1532
|
await writeConversation(filePath, existing);
|
|
879
|
-
return
|
|
1533
|
+
return {
|
|
1534
|
+
...message,
|
|
1535
|
+
nextQueued: nextQueued ? { id: nextQueued.id, content: nextQueued.content } : null
|
|
1536
|
+
};
|
|
1537
|
+
},
|
|
1538
|
+
async recoverQueue(conversationId) {
|
|
1539
|
+
const filePath = fileFor(conversationId);
|
|
1540
|
+
const existing = await readConversation(filePath);
|
|
1541
|
+
if (!existing) {
|
|
1542
|
+
return {
|
|
1543
|
+
action: "noop",
|
|
1544
|
+
workingId: null,
|
|
1545
|
+
promotedId: null,
|
|
1546
|
+
nextQueued: null
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
const working = existing.messages.find(
|
|
1550
|
+
(m) => m.role === "user" && m.queueStatus === "working"
|
|
1551
|
+
);
|
|
1552
|
+
if (working) {
|
|
1553
|
+
if (userHasAiReply(existing.messages, working.id)) {
|
|
1554
|
+
existing.messages = existing.messages.filter(
|
|
1555
|
+
(m) => m.provider !== "working"
|
|
1556
|
+
);
|
|
1557
|
+
working.queueStatus = null;
|
|
1558
|
+
working.queuePosition = null;
|
|
1559
|
+
const waiting2 = existing.messages.filter((m) => m.queueStatus === "queued").sort(
|
|
1560
|
+
(a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)
|
|
1561
|
+
);
|
|
1562
|
+
const next2 = waiting2[0] ?? null;
|
|
1563
|
+
if (next2) {
|
|
1564
|
+
next2.queueStatus = "working";
|
|
1565
|
+
next2.queuePosition = null;
|
|
1566
|
+
existing.messages.push({
|
|
1567
|
+
id: randomUUID2(),
|
|
1568
|
+
conversationId,
|
|
1569
|
+
role: "assistant",
|
|
1570
|
+
content: "",
|
|
1571
|
+
provider: "working",
|
|
1572
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1573
|
+
senderType: "ai",
|
|
1574
|
+
parentMessageId: next2.id
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1578
|
+
await writeConversation(filePath, existing);
|
|
1579
|
+
return {
|
|
1580
|
+
action: "completed_and_promoted",
|
|
1581
|
+
workingId: working.id,
|
|
1582
|
+
promotedId: next2?.id ?? null,
|
|
1583
|
+
nextQueued: next2 ? { id: next2.id, content: next2.content } : null
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
return {
|
|
1587
|
+
action: "redispatched_working",
|
|
1588
|
+
workingId: working.id,
|
|
1589
|
+
promotedId: null,
|
|
1590
|
+
nextQueued: null
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
const waiting = existing.messages.filter((m) => m.queueStatus === "queued").sort(
|
|
1594
|
+
(a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)
|
|
1595
|
+
);
|
|
1596
|
+
const next = waiting[0] ?? null;
|
|
1597
|
+
if (!next) {
|
|
1598
|
+
return {
|
|
1599
|
+
action: "noop",
|
|
1600
|
+
workingId: null,
|
|
1601
|
+
promotedId: null,
|
|
1602
|
+
nextQueued: null
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
next.queueStatus = "working";
|
|
1606
|
+
next.queuePosition = null;
|
|
1607
|
+
existing.messages.push({
|
|
1608
|
+
id: randomUUID2(),
|
|
1609
|
+
conversationId,
|
|
1610
|
+
role: "assistant",
|
|
1611
|
+
content: "",
|
|
1612
|
+
provider: "working",
|
|
1613
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1614
|
+
senderType: "ai",
|
|
1615
|
+
parentMessageId: next.id
|
|
1616
|
+
});
|
|
1617
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1618
|
+
await writeConversation(filePath, existing);
|
|
1619
|
+
return {
|
|
1620
|
+
action: "promoted_queued",
|
|
1621
|
+
workingId: null,
|
|
1622
|
+
promotedId: next.id,
|
|
1623
|
+
nextQueued: { id: next.id, content: next.content }
|
|
1624
|
+
};
|
|
1625
|
+
},
|
|
1626
|
+
async releaseWorkingTurn(conversationId, parentMessageId) {
|
|
1627
|
+
const filePath = fileFor(conversationId);
|
|
1628
|
+
const existing = await readConversation(filePath);
|
|
1629
|
+
if (!existing) return { nextQueued: null };
|
|
1630
|
+
existing.messages = existing.messages.filter(
|
|
1631
|
+
(m) => m.provider !== "working"
|
|
1632
|
+
);
|
|
1633
|
+
if (parentMessageId) {
|
|
1634
|
+
const parent = existing.messages.find((m) => m.id === parentMessageId);
|
|
1635
|
+
if (parent) {
|
|
1636
|
+
parent.queueStatus = null;
|
|
1637
|
+
parent.queuePosition = null;
|
|
1638
|
+
}
|
|
1639
|
+
} else {
|
|
1640
|
+
for (const row of existing.messages) {
|
|
1641
|
+
if (row.queueStatus === "working") {
|
|
1642
|
+
row.queueStatus = null;
|
|
1643
|
+
row.queuePosition = null;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
const waiting = existing.messages.filter((m) => m.queueStatus === "queued").sort(
|
|
1648
|
+
(a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)
|
|
1649
|
+
);
|
|
1650
|
+
const next = waiting[0] ?? null;
|
|
1651
|
+
if (next) {
|
|
1652
|
+
next.queueStatus = "working";
|
|
1653
|
+
next.queuePosition = null;
|
|
1654
|
+
existing.messages.push({
|
|
1655
|
+
id: randomUUID2(),
|
|
1656
|
+
conversationId,
|
|
1657
|
+
role: "assistant",
|
|
1658
|
+
content: "",
|
|
1659
|
+
provider: "working",
|
|
1660
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1661
|
+
senderType: "ai",
|
|
1662
|
+
parentMessageId: next.id
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1666
|
+
await writeConversation(filePath, existing);
|
|
1667
|
+
return next ? { nextQueued: { id: next.id, content: next.content } } : { nextQueued: null };
|
|
880
1668
|
},
|
|
881
1669
|
async clearWorkingMessages(conversationId) {
|
|
882
1670
|
const filePath = fileFor(conversationId);
|
|
@@ -898,7 +1686,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
898
1686
|
const conversations = [];
|
|
899
1687
|
for (const entry of entries) {
|
|
900
1688
|
if (!entry.endsWith(".json")) continue;
|
|
901
|
-
const filePath =
|
|
1689
|
+
const filePath = path4.join(baseDir, entry);
|
|
902
1690
|
const existing = await readConversation(filePath);
|
|
903
1691
|
if (!existing?.id) continue;
|
|
904
1692
|
conversations.push({
|
|
@@ -915,7 +1703,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
915
1703
|
if (events.length === 0) return;
|
|
916
1704
|
const existing = await readConversation(fileFor(conversationId));
|
|
917
1705
|
if (!existing) return;
|
|
918
|
-
const toolFile =
|
|
1706
|
+
const toolFile = path4.join(baseDir, `${conversationId}.tools.jsonl`);
|
|
919
1707
|
const lines = events.map(
|
|
920
1708
|
(e) => JSON.stringify({
|
|
921
1709
|
conversationId,
|
|
@@ -932,7 +1720,353 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
932
1720
|
// src/http/maintainer-pro-store.ts
|
|
933
1721
|
import fs4 from "fs/promises";
|
|
934
1722
|
import os from "os";
|
|
935
|
-
import
|
|
1723
|
+
import path5 from "path";
|
|
1724
|
+
|
|
1725
|
+
// src/http/synced-store.ts
|
|
1726
|
+
function userHasAiReply2(rows, workingId) {
|
|
1727
|
+
return rows.some(
|
|
1728
|
+
(row) => row.role === "assistant" && row.parentMessageId === workingId && row.provider !== "working" && row.senderType !== "developer" && Boolean(row.content?.trim())
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
function nowIso() {
|
|
1732
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1733
|
+
}
|
|
1734
|
+
function toWsUrl(baseUrl, apiKey) {
|
|
1735
|
+
const u = new URL(`${baseUrl.replace(/\/$/, "")}/api/v1/ws`);
|
|
1736
|
+
u.protocol = u.protocol === "https:" ? "wss:" : "ws:";
|
|
1737
|
+
u.searchParams.set("apiKey", apiKey);
|
|
1738
|
+
return u.toString();
|
|
1739
|
+
}
|
|
1740
|
+
function asCachedMessage(raw) {
|
|
1741
|
+
const id = typeof raw.id === "string" ? raw.id : "";
|
|
1742
|
+
const role = typeof raw.role === "string" ? raw.role : "";
|
|
1743
|
+
if (!id || !role) return null;
|
|
1744
|
+
return {
|
|
1745
|
+
id,
|
|
1746
|
+
role,
|
|
1747
|
+
content: typeof raw.content === "string" ? raw.content : "",
|
|
1748
|
+
provider: typeof raw.provider === "string" ? raw.provider : null,
|
|
1749
|
+
createdAt: typeof raw.createdAt === "string" ? raw.createdAt : void 0,
|
|
1750
|
+
attachmentPaths: Array.isArray(raw.attachmentPaths) ? raw.attachmentPaths.filter((p) => typeof p === "string") : void 0,
|
|
1751
|
+
senderType: raw.senderType === "client" || raw.senderType === "developer" || raw.senderType === "ai" ? raw.senderType : null,
|
|
1752
|
+
senderName: typeof raw.senderName === "string" ? raw.senderName : null,
|
|
1753
|
+
parentMessageId: typeof raw.parentMessageId === "string" ? raw.parentMessageId : null,
|
|
1754
|
+
queueStatus: raw.queueStatus === "working" || raw.queueStatus === "queued" ? raw.queueStatus : raw.queueStatus === null ? null : void 0,
|
|
1755
|
+
queuePosition: typeof raw.queuePosition === "number" ? raw.queuePosition : null
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
function cloneMessages(messages) {
|
|
1759
|
+
return messages.map((m) => ({ ...m }));
|
|
1760
|
+
}
|
|
1761
|
+
function createSyncedChatStore(options) {
|
|
1762
|
+
const { remote, maintainerProUrl, apiKey, onWorkingTurn } = options;
|
|
1763
|
+
const logger = options.logger ?? createLogger("ai-cli");
|
|
1764
|
+
const info = (msg) => {
|
|
1765
|
+
if (options.log) options.log(msg);
|
|
1766
|
+
else logger.info(msg);
|
|
1767
|
+
};
|
|
1768
|
+
const conversations = /* @__PURE__ */ new Map();
|
|
1769
|
+
const inflightRefresh = /* @__PURE__ */ new Map();
|
|
1770
|
+
let stopped = false;
|
|
1771
|
+
let socket = null;
|
|
1772
|
+
let reconnectAttempt = 0;
|
|
1773
|
+
let reconnectTimer = null;
|
|
1774
|
+
let pingTimer = null;
|
|
1775
|
+
const stats = () => {
|
|
1776
|
+
let messages = 0;
|
|
1777
|
+
for (const conv of conversations.values()) messages += conv.messages.length;
|
|
1778
|
+
return { conversations: conversations.size, messages };
|
|
1779
|
+
};
|
|
1780
|
+
const putConversation = (id, messages, updatedAt = nowIso()) => {
|
|
1781
|
+
conversations.set(id, { id, updatedAt, messages: cloneMessages(messages) });
|
|
1782
|
+
};
|
|
1783
|
+
const refreshConversation = (id) => {
|
|
1784
|
+
const existing = inflightRefresh.get(id);
|
|
1785
|
+
if (existing) return existing;
|
|
1786
|
+
const pending = (async () => {
|
|
1787
|
+
const messages = remote.listMessages ? await remote.listMessages(id) : [];
|
|
1788
|
+
putConversation(id, messages);
|
|
1789
|
+
return cloneMessages(messages);
|
|
1790
|
+
})().finally(() => {
|
|
1791
|
+
inflightRefresh.delete(id);
|
|
1792
|
+
});
|
|
1793
|
+
inflightRefresh.set(id, pending);
|
|
1794
|
+
return pending;
|
|
1795
|
+
};
|
|
1796
|
+
const loadSnapshot = async () => {
|
|
1797
|
+
if (remote.listSnapshot) {
|
|
1798
|
+
try {
|
|
1799
|
+
return await remote.listSnapshot();
|
|
1800
|
+
} catch {
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (!remote.listConversations || !remote.listMessages) return [];
|
|
1804
|
+
const summaries = await remote.listConversations();
|
|
1805
|
+
const loaded = [];
|
|
1806
|
+
for (const row of summaries) {
|
|
1807
|
+
loaded.push({
|
|
1808
|
+
id: row.id,
|
|
1809
|
+
updatedAt: row.updatedAt,
|
|
1810
|
+
messages: await remote.listMessages(row.id)
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
return loaded;
|
|
1814
|
+
};
|
|
1815
|
+
const hydrate = async () => {
|
|
1816
|
+
const snapshot = await loadSnapshot();
|
|
1817
|
+
conversations.clear();
|
|
1818
|
+
for (const row of snapshot) {
|
|
1819
|
+
putConversation(row.id, row.messages, row.updatedAt);
|
|
1820
|
+
}
|
|
1821
|
+
const next = stats();
|
|
1822
|
+
info(
|
|
1823
|
+
`cache hydrated (${next.conversations} conversations, ${next.messages} messages)`
|
|
1824
|
+
);
|
|
1825
|
+
for (const conv of conversations.values()) {
|
|
1826
|
+
const working = conv.messages.find(
|
|
1827
|
+
(row) => row.role === "user" && row.queueStatus === "working" && row.senderType !== "developer"
|
|
1828
|
+
);
|
|
1829
|
+
if (!working) continue;
|
|
1830
|
+
if (!userHasAiReply2(conv.messages, working.id)) {
|
|
1831
|
+
logger.debug(
|
|
1832
|
+
{ conversationId: conv.id, messageId: working.id },
|
|
1833
|
+
"resume working turn"
|
|
1834
|
+
);
|
|
1835
|
+
onWorkingTurn?.({ conversationId: conv.id, message: working });
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return next;
|
|
1839
|
+
};
|
|
1840
|
+
const upsertMessage = (conversationId, message) => {
|
|
1841
|
+
const conv = conversations.get(conversationId) ?? {
|
|
1842
|
+
id: conversationId,
|
|
1843
|
+
updatedAt: nowIso(),
|
|
1844
|
+
messages: []
|
|
1845
|
+
};
|
|
1846
|
+
const index = conv.messages.findIndex((row) => row.id === message.id);
|
|
1847
|
+
const wasWorking = index >= 0 && conv.messages[index]?.queueStatus === "working";
|
|
1848
|
+
if (index >= 0) {
|
|
1849
|
+
conv.messages[index] = { ...conv.messages[index], ...message };
|
|
1850
|
+
} else {
|
|
1851
|
+
conv.messages.push(message);
|
|
1852
|
+
}
|
|
1853
|
+
conv.updatedAt = nowIso();
|
|
1854
|
+
conversations.set(conversationId, conv);
|
|
1855
|
+
const becameWorking = message.role === "user" && message.queueStatus === "working" && message.senderType !== "developer" && !wasWorking;
|
|
1856
|
+
if (becameWorking) {
|
|
1857
|
+
logger.debug(
|
|
1858
|
+
{ conversationId, messageId: message.id },
|
|
1859
|
+
"working turn"
|
|
1860
|
+
);
|
|
1861
|
+
onWorkingTurn?.({ conversationId, message });
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
1864
|
+
const deleteMessages = (conversationId, ids) => {
|
|
1865
|
+
const conv = conversations.get(conversationId);
|
|
1866
|
+
if (!conv || ids.length === 0) return;
|
|
1867
|
+
const drop = new Set(ids);
|
|
1868
|
+
conv.messages = conv.messages.filter((row) => !drop.has(row.id));
|
|
1869
|
+
conv.updatedAt = nowIso();
|
|
1870
|
+
};
|
|
1871
|
+
const applyEvent = (msg) => {
|
|
1872
|
+
const externalId = typeof msg.externalId === "string" && msg.externalId ? msg.externalId : typeof msg.conversationId === "string" ? msg.conversationId : "";
|
|
1873
|
+
if (!externalId) return;
|
|
1874
|
+
if (msg.type === "message.created") {
|
|
1875
|
+
const raw = msg.message && typeof msg.message === "object" ? msg.message : null;
|
|
1876
|
+
const cached = raw ? asCachedMessage(raw) : null;
|
|
1877
|
+
if (cached) {
|
|
1878
|
+
upsertMessage(externalId, cached);
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
void refreshConversation(externalId).catch(() => void 0);
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
if (msg.type === "message.updated") {
|
|
1885
|
+
const raw = msg.message && typeof msg.message === "object" ? msg.message : null;
|
|
1886
|
+
const cached = raw ? asCachedMessage(raw) : null;
|
|
1887
|
+
if (cached) {
|
|
1888
|
+
upsertMessage(externalId, cached);
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
void refreshConversation(externalId).catch(() => void 0);
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
if (msg.type === "message.deleted") {
|
|
1895
|
+
const ids = Array.isArray(msg.ids) ? msg.ids.filter((id) => typeof id === "string") : [];
|
|
1896
|
+
deleteMessages(externalId, ids);
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
const stopSocket = () => {
|
|
1900
|
+
if (pingTimer) {
|
|
1901
|
+
clearInterval(pingTimer);
|
|
1902
|
+
pingTimer = null;
|
|
1903
|
+
}
|
|
1904
|
+
if (reconnectTimer) {
|
|
1905
|
+
clearTimeout(reconnectTimer);
|
|
1906
|
+
reconnectTimer = null;
|
|
1907
|
+
}
|
|
1908
|
+
try {
|
|
1909
|
+
socket?.close();
|
|
1910
|
+
} catch {
|
|
1911
|
+
}
|
|
1912
|
+
socket = null;
|
|
1913
|
+
};
|
|
1914
|
+
const connectRealtime = () => {
|
|
1915
|
+
if (stopped) return;
|
|
1916
|
+
const WebSocketCtor = globalThis.WebSocket;
|
|
1917
|
+
if (!WebSocketCtor) {
|
|
1918
|
+
info("WebSocket unavailable; cache will refresh after local writes only");
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
const ws = new WebSocketCtor(toWsUrl(maintainerProUrl, apiKey));
|
|
1922
|
+
socket = ws;
|
|
1923
|
+
ws.addEventListener("open", () => {
|
|
1924
|
+
reconnectAttempt = 0;
|
|
1925
|
+
logger.debug("store websocket open");
|
|
1926
|
+
if (pingTimer) clearInterval(pingTimer);
|
|
1927
|
+
pingTimer = setInterval(() => {
|
|
1928
|
+
try {
|
|
1929
|
+
if (ws.readyState === 1) ws.send(JSON.stringify({ type: "ping" }));
|
|
1930
|
+
} catch {
|
|
1931
|
+
}
|
|
1932
|
+
}, 2e4);
|
|
1933
|
+
});
|
|
1934
|
+
ws.addEventListener("message", (event) => {
|
|
1935
|
+
let payload;
|
|
1936
|
+
try {
|
|
1937
|
+
payload = JSON.parse(String(event.data));
|
|
1938
|
+
} catch {
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
if (payload.type === "hello") {
|
|
1942
|
+
const meta = payload.meta && typeof payload.meta === "object" ? payload.meta : null;
|
|
1943
|
+
const sandboxId = typeof meta?.sandboxId === "string" ? meta.sandboxId : "";
|
|
1944
|
+
if (sandboxId && ws.readyState === 1) {
|
|
1945
|
+
ws.send(
|
|
1946
|
+
JSON.stringify({ type: "subscribe", channels: [`sandbox:${sandboxId}`] })
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
logger.debug({ sandboxId }, "store websocket hello");
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
if (payload.type === "message.created" || payload.type === "message.updated" || payload.type === "message.deleted") {
|
|
1953
|
+
logger.debug({ type: payload.type }, "store websocket event");
|
|
1954
|
+
applyEvent(payload);
|
|
1955
|
+
}
|
|
1956
|
+
});
|
|
1957
|
+
ws.addEventListener("close", () => {
|
|
1958
|
+
if (socket === ws) socket = null;
|
|
1959
|
+
if (pingTimer) {
|
|
1960
|
+
clearInterval(pingTimer);
|
|
1961
|
+
pingTimer = null;
|
|
1962
|
+
}
|
|
1963
|
+
if (stopped) return;
|
|
1964
|
+
const delay = Math.min(3e4, 1e3 * 2 ** Math.min(reconnectAttempt, 5));
|
|
1965
|
+
reconnectAttempt += 1;
|
|
1966
|
+
logger.debug({ delay, reconnectAttempt }, "store websocket reconnect");
|
|
1967
|
+
reconnectTimer = setTimeout(() => {
|
|
1968
|
+
reconnectTimer = null;
|
|
1969
|
+
void hydrate().catch((err) => {
|
|
1970
|
+
logger.warn(
|
|
1971
|
+
`cache rehydrate failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1972
|
+
);
|
|
1973
|
+
}).finally(() => {
|
|
1974
|
+
connectRealtime();
|
|
1975
|
+
});
|
|
1976
|
+
}, delay);
|
|
1977
|
+
});
|
|
1978
|
+
};
|
|
1979
|
+
const ready = hydrate().catch((err) => {
|
|
1980
|
+
logger.warn(
|
|
1981
|
+
`cache hydrate failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1982
|
+
);
|
|
1983
|
+
return stats();
|
|
1984
|
+
});
|
|
1985
|
+
void ready.then(() => {
|
|
1986
|
+
if (!stopped) connectRealtime();
|
|
1987
|
+
});
|
|
1988
|
+
const store = {
|
|
1989
|
+
ready,
|
|
1990
|
+
stop: () => {
|
|
1991
|
+
stopped = true;
|
|
1992
|
+
stopSocket();
|
|
1993
|
+
},
|
|
1994
|
+
async ensureConversation(id) {
|
|
1995
|
+
await remote.ensureConversation(id);
|
|
1996
|
+
if (!conversations.has(id)) {
|
|
1997
|
+
putConversation(id, []);
|
|
1998
|
+
}
|
|
1999
|
+
},
|
|
2000
|
+
async saveMessage(input) {
|
|
2001
|
+
const saved = await remote.saveMessage(input);
|
|
2002
|
+
await refreshConversation(input.conversationId).catch(() => void 0);
|
|
2003
|
+
return saved;
|
|
2004
|
+
},
|
|
2005
|
+
async listMessages(conversationId) {
|
|
2006
|
+
await ready.catch(() => void 0);
|
|
2007
|
+
const hit = conversations.get(conversationId);
|
|
2008
|
+
if (hit) return cloneMessages(hit.messages);
|
|
2009
|
+
return refreshConversation(conversationId);
|
|
2010
|
+
},
|
|
2011
|
+
async clearWorkingMessages(conversationId) {
|
|
2012
|
+
await remote.clearWorkingMessages?.(conversationId);
|
|
2013
|
+
await refreshConversation(conversationId).catch(() => void 0);
|
|
2014
|
+
},
|
|
2015
|
+
async releaseWorkingTurn(conversationId, parentMessageId) {
|
|
2016
|
+
const result = await remote.releaseWorkingTurn?.(
|
|
2017
|
+
conversationId,
|
|
2018
|
+
parentMessageId
|
|
2019
|
+
);
|
|
2020
|
+
await refreshConversation(conversationId).catch(() => void 0);
|
|
2021
|
+
return result ?? { nextQueued: null };
|
|
2022
|
+
},
|
|
2023
|
+
async recoverQueue(conversationId) {
|
|
2024
|
+
const result = await remote.recoverQueue?.(conversationId);
|
|
2025
|
+
await refreshConversation(conversationId).catch(() => void 0);
|
|
2026
|
+
const conv = conversations.get(conversationId);
|
|
2027
|
+
if (conv) {
|
|
2028
|
+
const working = conv.messages.find(
|
|
2029
|
+
(row) => row.role === "user" && row.queueStatus === "working" && row.senderType !== "developer"
|
|
2030
|
+
);
|
|
2031
|
+
if (working && !userHasAiReply2(conv.messages, working.id)) {
|
|
2032
|
+
logger.info(
|
|
2033
|
+
{ conversationId, messageId: working.id, action: result?.action },
|
|
2034
|
+
"recoverQueue \u2192 resume working turn"
|
|
2035
|
+
);
|
|
2036
|
+
onWorkingTurn?.({ conversationId, message: working });
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
return result ?? {
|
|
2040
|
+
action: "noop",
|
|
2041
|
+
workingId: null,
|
|
2042
|
+
promotedId: null,
|
|
2043
|
+
nextQueued: null
|
|
2044
|
+
};
|
|
2045
|
+
},
|
|
2046
|
+
async listConversations() {
|
|
2047
|
+
await ready.catch(() => void 0);
|
|
2048
|
+
return [...conversations.values()].map((conv) => ({
|
|
2049
|
+
id: conv.id,
|
|
2050
|
+
updatedAt: conv.updatedAt,
|
|
2051
|
+
messages: cloneMessages(conv.messages)
|
|
2052
|
+
})).sort(
|
|
2053
|
+
(a, b) => (Date.parse(b.updatedAt) || 0) - (Date.parse(a.updatedAt) || 0)
|
|
2054
|
+
);
|
|
2055
|
+
},
|
|
2056
|
+
async saveToolEvents(conversationId, events) {
|
|
2057
|
+
await remote.saveToolEvents?.(conversationId, events);
|
|
2058
|
+
},
|
|
2059
|
+
async uploadAttachments(conversationId, attachments) {
|
|
2060
|
+
if (!remote.uploadAttachments) {
|
|
2061
|
+
return { refs: [], localPaths: [] };
|
|
2062
|
+
}
|
|
2063
|
+
return remote.uploadAttachments(conversationId, attachments);
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
return store;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
// src/http/maintainer-pro-store.ts
|
|
936
2070
|
async function mpFetch(baseUrl, apiKey, pathName, init, fetchImpl) {
|
|
937
2071
|
const url = `${baseUrl.replace(/\/$/, "")}${pathName}`;
|
|
938
2072
|
const res = await fetchImpl(url, {
|
|
@@ -959,7 +2093,7 @@ function createMaintainerProStore(options) {
|
|
|
959
2093
|
const baseUrl = options.baseUrl;
|
|
960
2094
|
const apiKey = options.apiKey;
|
|
961
2095
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
962
|
-
const tempDir = options.tempDir ??
|
|
2096
|
+
const tempDir = options.tempDir ?? path5.join(os.tmpdir(), "maintainer-pro-store");
|
|
963
2097
|
const store = {
|
|
964
2098
|
async ensureConversation(id) {
|
|
965
2099
|
await mpFetch(
|
|
@@ -982,17 +2116,54 @@ function createMaintainerProStore(options) {
|
|
|
982
2116
|
{
|
|
983
2117
|
method: "POST",
|
|
984
2118
|
body: JSON.stringify({
|
|
2119
|
+
id: input.id,
|
|
985
2120
|
role: input.role,
|
|
986
2121
|
content: input.content,
|
|
987
2122
|
provider: input.provider,
|
|
988
2123
|
senderType: input.senderType,
|
|
989
2124
|
senderName: input.senderName,
|
|
990
|
-
|
|
2125
|
+
parentMessageId: input.parentMessageId ?? void 0,
|
|
2126
|
+
attachmentIds,
|
|
2127
|
+
intent: input.intent,
|
|
2128
|
+
queueStatus: input.queueStatus ?? void 0
|
|
2129
|
+
})
|
|
2130
|
+
},
|
|
2131
|
+
fetchImpl
|
|
2132
|
+
);
|
|
2133
|
+
return {
|
|
2134
|
+
...data.message,
|
|
2135
|
+
nextQueued: data.nextQueued ?? null
|
|
2136
|
+
};
|
|
2137
|
+
},
|
|
2138
|
+
async releaseWorkingTurn(conversationId, parentMessageId) {
|
|
2139
|
+
const data = await mpFetch(
|
|
2140
|
+
baseUrl,
|
|
2141
|
+
apiKey,
|
|
2142
|
+
`/api/v1/store/conversations/${encodeURIComponent(conversationId)}/complete-turn`,
|
|
2143
|
+
{
|
|
2144
|
+
method: "POST",
|
|
2145
|
+
body: JSON.stringify({
|
|
2146
|
+
parentMessageId: parentMessageId ?? void 0
|
|
991
2147
|
})
|
|
992
2148
|
},
|
|
993
2149
|
fetchImpl
|
|
994
2150
|
);
|
|
995
|
-
return data.
|
|
2151
|
+
return { nextQueued: data.nextQueued ?? null };
|
|
2152
|
+
},
|
|
2153
|
+
async recoverQueue(conversationId) {
|
|
2154
|
+
const data = await mpFetch(
|
|
2155
|
+
baseUrl,
|
|
2156
|
+
apiKey,
|
|
2157
|
+
`/api/v1/store/conversations/${encodeURIComponent(conversationId)}/recover-queue`,
|
|
2158
|
+
{ method: "POST", body: "{}" },
|
|
2159
|
+
fetchImpl
|
|
2160
|
+
);
|
|
2161
|
+
return {
|
|
2162
|
+
action: data.action ?? "noop",
|
|
2163
|
+
workingId: data.workingId ?? null,
|
|
2164
|
+
promotedId: data.promotedId ?? null,
|
|
2165
|
+
nextQueued: data.nextQueued ?? null
|
|
2166
|
+
};
|
|
996
2167
|
},
|
|
997
2168
|
async clearWorkingMessages(conversationId) {
|
|
998
2169
|
await mpFetch(
|
|
@@ -1023,6 +2194,16 @@ function createMaintainerProStore(options) {
|
|
|
1023
2194
|
);
|
|
1024
2195
|
return data.conversations ?? [];
|
|
1025
2196
|
},
|
|
2197
|
+
async listSnapshot() {
|
|
2198
|
+
const data = await mpFetch(
|
|
2199
|
+
baseUrl,
|
|
2200
|
+
apiKey,
|
|
2201
|
+
`/api/v1/store/snapshot`,
|
|
2202
|
+
{ method: "GET" },
|
|
2203
|
+
fetchImpl
|
|
2204
|
+
);
|
|
2205
|
+
return data.conversations ?? [];
|
|
2206
|
+
},
|
|
1026
2207
|
async saveToolEvents(conversationId, events) {
|
|
1027
2208
|
if (!events.length) return;
|
|
1028
2209
|
await mpFetch(
|
|
@@ -1064,7 +2245,7 @@ function createMaintainerProStore(options) {
|
|
|
1064
2245
|
attachmentIds.push(id);
|
|
1065
2246
|
refs.push(data.attachment.ref || `maintainer-pro://${id}`);
|
|
1066
2247
|
const ext = item.mimeType.includes("jpeg") || item.mimeType.includes("jpg") ? ".jpg" : item.mimeType.includes("webp") ? ".webp" : item.mimeType.includes("gif") ? ".gif" : ".png";
|
|
1067
|
-
const localPath =
|
|
2248
|
+
const localPath = path5.join(
|
|
1068
2249
|
tempDir,
|
|
1069
2250
|
`${conversationId}-${id}${ext}`
|
|
1070
2251
|
);
|
|
@@ -1074,21 +2255,153 @@ function createMaintainerProStore(options) {
|
|
|
1074
2255
|
return { refs, localPaths, attachmentIds };
|
|
1075
2256
|
}
|
|
1076
2257
|
};
|
|
1077
|
-
|
|
2258
|
+
if (options.sync === false) {
|
|
2259
|
+
return Object.assign(store, {
|
|
2260
|
+
ready: Promise.resolve({ conversations: 0, messages: 0 }),
|
|
2261
|
+
stop() {
|
|
2262
|
+
}
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
return createSyncedChatStore({
|
|
2266
|
+
remote: store,
|
|
2267
|
+
maintainerProUrl: baseUrl,
|
|
2268
|
+
apiKey,
|
|
2269
|
+
logger: options.logger,
|
|
2270
|
+
log: options.log,
|
|
2271
|
+
onWorkingTurn: options.onWorkingTurn
|
|
2272
|
+
});
|
|
1078
2273
|
}
|
|
1079
|
-
function createMaintainerProStoreFromEnv() {
|
|
2274
|
+
function createMaintainerProStoreFromEnv(options) {
|
|
1080
2275
|
const baseUrl = process.env.MAINTAINER_PRO_URL?.trim();
|
|
1081
2276
|
const apiKey = process.env.MAINTAINER_PRO_API_KEY?.trim();
|
|
1082
2277
|
if (!baseUrl || !apiKey) return null;
|
|
1083
|
-
return createMaintainerProStore({
|
|
2278
|
+
return createMaintainerProStore({
|
|
2279
|
+
baseUrl,
|
|
2280
|
+
apiKey,
|
|
2281
|
+
logger: options?.logger,
|
|
2282
|
+
log: options?.log,
|
|
2283
|
+
sync: options?.sync,
|
|
2284
|
+
onWorkingTurn: options?.onWorkingTurn
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// src/workspace-inspect.ts
|
|
2289
|
+
import { z } from "zod";
|
|
2290
|
+
var inspectJsonSchema = z.object({
|
|
2291
|
+
kind: z.enum(["next", "vite", "html", "empty", "other"]).optional(),
|
|
2292
|
+
name: z.string().max(200).optional(),
|
|
2293
|
+
summary: z.string().max(800).optional(),
|
|
2294
|
+
scripts: z.object({
|
|
2295
|
+
ui: z.string().min(1).max(64).optional(),
|
|
2296
|
+
backend: z.string().min(1).max(64).optional(),
|
|
2297
|
+
app: z.string().min(1).max(64).optional()
|
|
2298
|
+
}).optional(),
|
|
2299
|
+
ports: z.object({
|
|
2300
|
+
ui: z.number().int().min(1).max(65535).optional(),
|
|
2301
|
+
backend: z.number().int().min(1).max(65535).optional(),
|
|
2302
|
+
app: z.number().int().min(1).max(65535).optional()
|
|
2303
|
+
}).optional(),
|
|
2304
|
+
issues: z.array(z.string().max(500)).max(20).optional(),
|
|
2305
|
+
fixes: z.array(z.string().max(500)).max(20).optional(),
|
|
2306
|
+
ready: z.boolean().optional()
|
|
2307
|
+
});
|
|
2308
|
+
function extractInspectJson(text) {
|
|
2309
|
+
const fence = text.match(/```json\s*([\s\S]*?)```/i);
|
|
2310
|
+
const raw = fence?.[1]?.trim() || text.match(/\{[\s\S]*\}/)?.[0];
|
|
2311
|
+
if (!raw) return null;
|
|
2312
|
+
try {
|
|
2313
|
+
const parsed = inspectJsonSchema.safeParse(JSON.parse(raw));
|
|
2314
|
+
return parsed.success ? parsed.data : null;
|
|
2315
|
+
} catch {
|
|
2316
|
+
return null;
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
function setupSystemPrompt(input) {
|
|
2320
|
+
return `You are Maintainer Pro's local setup assistant on the developer's machine.
|
|
2321
|
+
|
|
2322
|
+
Workspace: ${input.workspaceDir}
|
|
2323
|
+
App name: ${input.appName || "the app"}
|
|
2324
|
+
|
|
2325
|
+
Inspect this repository (package.json, README, lockfiles, existing env files, how the UI and API start).
|
|
2326
|
+
You MAY edit files to fix setup problems that would block local run or the chat widget (env keys, widget mount, missing config).
|
|
2327
|
+
Do NOT start long-lived dev servers, tunnels, or install global tools.
|
|
2328
|
+
Do NOT run \`npm run dev\` or similar.
|
|
2329
|
+
|
|
2330
|
+
Script names you report MUST already exist in package.json. Omit chat/ai-server scripts.
|
|
2331
|
+
|
|
2332
|
+
End with a short human summary AND one \`\`\`json fence with:
|
|
2333
|
+
|
|
2334
|
+
{
|
|
2335
|
+
"kind": "next" | "vite" | "html" | "empty" | "other",
|
|
2336
|
+
"name": "package or folder name",
|
|
2337
|
+
"summary": "one sentence about this project",
|
|
2338
|
+
"scripts": { "ui": "dev:client", "backend": "dev:server" },
|
|
2339
|
+
"ports": { "ui": 5173, "backend": 4100 },
|
|
2340
|
+
"issues": ["remaining problems"],
|
|
2341
|
+
"fixes": ["what you changed"],
|
|
2342
|
+
"ready": true
|
|
2343
|
+
}`;
|
|
2344
|
+
}
|
|
2345
|
+
function setupUserMessage(input) {
|
|
2346
|
+
if (input.problem?.trim()) {
|
|
2347
|
+
return `The local bridge failed to keep this project running:
|
|
2348
|
+
|
|
2349
|
+
${input.problem.trim()}
|
|
2350
|
+
|
|
2351
|
+
${input.extraContext ? `${input.extraContext.trim()}
|
|
2352
|
+
|
|
2353
|
+
` : ""}Inspect the repo, fix the cause if you can, and return the JSON report.`;
|
|
2354
|
+
}
|
|
2355
|
+
return `${input.extraContext ? `${input.extraContext.trim()}
|
|
2356
|
+
|
|
2357
|
+
` : ""}Inspect this project, fix any setup gaps you can, and return the JSON report.`;
|
|
2358
|
+
}
|
|
2359
|
+
async function inspectAndRepairWorkspace(input) {
|
|
2360
|
+
const workspaceDir = input.workspaceDir;
|
|
2361
|
+
const provider = await resolveProvider({ preference: "auto" });
|
|
2362
|
+
const response = await callAi(
|
|
2363
|
+
[{ role: "user", content: setupUserMessage(input) }],
|
|
2364
|
+
{
|
|
2365
|
+
route: "/local-setup",
|
|
2366
|
+
pageTitle: input.appName || "Local setup",
|
|
2367
|
+
relevantFiles: ["package.json", "README.md", ".env", ".env.example"],
|
|
2368
|
+
data: {
|
|
2369
|
+
workspaceDir,
|
|
2370
|
+
problem: input.problem || null
|
|
2371
|
+
}
|
|
2372
|
+
},
|
|
2373
|
+
{
|
|
2374
|
+
systemPrompt: setupSystemPrompt(input),
|
|
2375
|
+
workspaceDir,
|
|
2376
|
+
technical: true
|
|
2377
|
+
}
|
|
2378
|
+
);
|
|
2379
|
+
const parsed = extractInspectJson(response.text);
|
|
2380
|
+
return {
|
|
2381
|
+
kind: parsed?.kind ?? "other",
|
|
2382
|
+
name: parsed?.name?.trim() || input.appName || "",
|
|
2383
|
+
summary: parsed?.summary?.trim() || response.text.replace(/```json[\s\S]*```/i, "").trim().slice(0, 400),
|
|
2384
|
+
scripts: parsed?.scripts ?? {},
|
|
2385
|
+
ports: parsed?.ports ?? {},
|
|
2386
|
+
issues: parsed?.issues ?? [],
|
|
2387
|
+
fixes: parsed?.fixes ?? [],
|
|
2388
|
+
ready: parsed?.ready ?? parsed?.issues?.length === 0,
|
|
2389
|
+
provider: response.provider || provider.id,
|
|
2390
|
+
rawText: response.text
|
|
2391
|
+
};
|
|
1084
2392
|
}
|
|
1085
2393
|
export {
|
|
2394
|
+
ACCESS_IGNORE_BEGIN,
|
|
2395
|
+
ACCESS_IGNORE_END,
|
|
2396
|
+
DEFAULT_AI_IGNORE_PATHS,
|
|
2397
|
+
PROMPT_SECTION,
|
|
1086
2398
|
WORKING_PROVIDER,
|
|
1087
2399
|
buildClaudeUserPrompt,
|
|
1088
2400
|
buildConversationPrompt,
|
|
1089
2401
|
buildCursorPrompt,
|
|
1090
2402
|
buildPriorConversationsContext,
|
|
1091
2403
|
callAi,
|
|
2404
|
+
collectParentChain,
|
|
1092
2405
|
commandExists,
|
|
1093
2406
|
createAntigravityProvider,
|
|
1094
2407
|
createBuiltinProviders,
|
|
@@ -1096,16 +2409,34 @@ export {
|
|
|
1096
2409
|
createClaudeProvider,
|
|
1097
2410
|
createCursorProvider,
|
|
1098
2411
|
createDefaultSystemPrompt,
|
|
2412
|
+
createInfoLogger,
|
|
1099
2413
|
createLocalDirectoryStore,
|
|
2414
|
+
createLogger,
|
|
1100
2415
|
createMaintainerProStore,
|
|
1101
2416
|
createMaintainerProStoreFromEnv,
|
|
2417
|
+
createSyncedChatStore,
|
|
1102
2418
|
createToolValidator,
|
|
2419
|
+
formatAccessPolicyPromptSection,
|
|
1103
2420
|
formatClientContext,
|
|
2421
|
+
formatParentChainContext,
|
|
1104
2422
|
getProviderPreference,
|
|
2423
|
+
inspectAndRepairWorkspace,
|
|
2424
|
+
isDevMode,
|
|
2425
|
+
isIgnoredRelative,
|
|
2426
|
+
isInsideWorkspace,
|
|
2427
|
+
isPathAllowed,
|
|
2428
|
+
normalizeIgnorePaths,
|
|
2429
|
+
parentChainForTurn,
|
|
1105
2430
|
parseAiResponse,
|
|
2431
|
+
parseIgnorePathsEnv,
|
|
2432
|
+
previewText,
|
|
1106
2433
|
providerLabel,
|
|
2434
|
+
renderManagedIgnoreBlock,
|
|
1107
2435
|
resolveCliBinary,
|
|
2436
|
+
resolveIgnorePaths,
|
|
2437
|
+
resolveLogLevel,
|
|
1108
2438
|
resolveProvider,
|
|
1109
2439
|
saveChatAttachments,
|
|
1110
|
-
toNextRoute
|
|
2440
|
+
toNextRoute,
|
|
2441
|
+
upsertManagedIgnoreFile
|
|
1111
2442
|
};
|