@maintainer-pro/ai-cli 0.1.4 → 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/dist/index.cjs +1314 -74
- package/dist/index.d.cts +207 -6
- package/dist/index.d.ts +207 -6
- package/dist/index.js +1291 -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,50 +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, technical) {
|
|
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
|
-
|
|
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
|
+
);
|
|
66
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.";
|
|
67
|
-
return `${systemSection}${contextSection}${priorSection}${
|
|
68
|
-
${request || "(See the attached screenshot(s).)"}
|
|
69
|
-
|
|
70
|
-
${closer}`;
|
|
122
|
+
return `${systemSection}${contextSection}${priorSection}${parentSection}${historySection}${attachmentsSection}${currentSection}${closer}`;
|
|
71
123
|
}
|
|
72
|
-
function buildCursorPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext, technical) {
|
|
124
|
+
function buildCursorPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext, technical, parentChainContext) {
|
|
73
125
|
return buildUserFacingPrompt(
|
|
74
126
|
systemPrompt,
|
|
75
127
|
messages,
|
|
76
128
|
context,
|
|
77
129
|
attachmentPaths,
|
|
78
130
|
priorConversationsContext,
|
|
79
|
-
technical
|
|
131
|
+
technical,
|
|
132
|
+
parentChainContext
|
|
80
133
|
);
|
|
81
134
|
}
|
|
82
|
-
function buildClaudeUserPrompt(messages, context, attachmentPaths, priorConversationsContext, technical) {
|
|
135
|
+
function buildClaudeUserPrompt(messages, context, attachmentPaths, priorConversationsContext, technical, parentChainContext) {
|
|
83
136
|
return buildUserFacingPrompt(
|
|
84
137
|
null,
|
|
85
138
|
messages,
|
|
86
139
|
context,
|
|
87
140
|
attachmentPaths,
|
|
88
141
|
priorConversationsContext,
|
|
89
|
-
technical
|
|
142
|
+
technical,
|
|
143
|
+
parentChainContext
|
|
90
144
|
);
|
|
91
145
|
}
|
|
92
146
|
|
|
@@ -200,7 +254,8 @@ async function callClaudeCli(command, messages, context, options) {
|
|
|
200
254
|
context,
|
|
201
255
|
options.attachmentPaths,
|
|
202
256
|
options.priorConversationsContext,
|
|
203
|
-
options.technical
|
|
257
|
+
options.technical,
|
|
258
|
+
options.parentChainContext
|
|
204
259
|
);
|
|
205
260
|
const resolved = resolveCliCommand("claude", command);
|
|
206
261
|
const workspace = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
|
|
@@ -240,7 +295,8 @@ async function callCursorCli(command, messages, context, options) {
|
|
|
240
295
|
context,
|
|
241
296
|
options.attachmentPaths,
|
|
242
297
|
options.priorConversationsContext,
|
|
243
|
-
options.technical
|
|
298
|
+
options.technical,
|
|
299
|
+
options.parentChainContext
|
|
244
300
|
);
|
|
245
301
|
const resolved = resolveCliCommand("cursor", command);
|
|
246
302
|
const workspace = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
|
|
@@ -282,7 +338,8 @@ async function callAntigravityCli(command, messages, context, options) {
|
|
|
282
338
|
context,
|
|
283
339
|
options.attachmentPaths,
|
|
284
340
|
options.priorConversationsContext,
|
|
285
|
-
options.technical
|
|
341
|
+
options.technical,
|
|
342
|
+
options.parentChainContext
|
|
286
343
|
);
|
|
287
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).
|
|
288
345
|
|
|
@@ -323,8 +380,8 @@ async function resolveCommandPath(command) {
|
|
|
323
380
|
}
|
|
324
381
|
const result = await execa2("which", [command], { reject: false });
|
|
325
382
|
if (result.exitCode !== 0) return null;
|
|
326
|
-
const
|
|
327
|
-
return
|
|
383
|
+
const path6 = result.stdout.trim().split(/\r?\n/)[0]?.trim();
|
|
384
|
+
return path6 || null;
|
|
328
385
|
} catch {
|
|
329
386
|
return null;
|
|
330
387
|
}
|
|
@@ -410,6 +467,51 @@ async function callAi(messages, context, options) {
|
|
|
410
467
|
return provider.call(messages, context, options);
|
|
411
468
|
}
|
|
412
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
|
+
|
|
413
515
|
// src/system-prompt.ts
|
|
414
516
|
function createDefaultSystemPrompt(input) {
|
|
415
517
|
const files = input.relevantFilesHint ? `Key UI files (use internally only; never name them in your reply):
|
|
@@ -420,6 +522,14 @@ Only when the user wants to change live in-app data (not source code), return a
|
|
|
420
522
|
${input.runtimeToolsHint}
|
|
421
523
|
|
|
422
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.` : "";
|
|
423
533
|
return `You are a helpful product assistant for an app the user is looking at right now.
|
|
424
534
|
|
|
425
535
|
${input.productDescription}
|
|
@@ -440,6 +550,7 @@ Behind the scenes you can edit this repository and update live app data, but the
|
|
|
440
550
|
## When to edit the codebase
|
|
441
551
|
Edit source files when the user asks to change labels, layout, styling, copy, or behavior in the app.
|
|
442
552
|
${files}
|
|
553
|
+
${access}
|
|
443
554
|
|
|
444
555
|
${tools}
|
|
445
556
|
|
|
@@ -447,9 +558,191 @@ Do not refuse UI/label changes \u2014 implement them in source files.
|
|
|
447
558
|
Answer the user's latest request directly \u2014 never reply with a generic greeting.`;
|
|
448
559
|
}
|
|
449
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
|
+
|
|
450
743
|
// src/http/attachments.ts
|
|
451
744
|
import fs2 from "fs/promises";
|
|
452
|
-
import
|
|
745
|
+
import path3 from "path";
|
|
453
746
|
var MAX_ATTACHMENTS = 5;
|
|
454
747
|
var MAX_BYTES = 4 * 1024 * 1024;
|
|
455
748
|
var ALLOWED = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif"]);
|
|
@@ -469,7 +762,7 @@ function extForMime(mime) {
|
|
|
469
762
|
async function saveChatAttachments(attachments, workspaceDir) {
|
|
470
763
|
if (!attachments?.length) return [];
|
|
471
764
|
const selected = attachments.slice(0, MAX_ATTACHMENTS);
|
|
472
|
-
const dir =
|
|
765
|
+
const dir = path3.join(workspaceDir, ".maintainer-pro", "uploads");
|
|
473
766
|
await fs2.mkdir(dir, { recursive: true });
|
|
474
767
|
const paths = [];
|
|
475
768
|
const stamp = Date.now();
|
|
@@ -488,7 +781,10 @@ async function saveChatAttachments(attachments, workspaceDir) {
|
|
|
488
781
|
}
|
|
489
782
|
const safeBase = (item.name || `screenshot-${i + 1}`).replace(/[^\w.\-]+/g, "_").slice(0, 64);
|
|
490
783
|
const fileName = `${stamp}-${i + 1}-${safeBase}${extForMime(mime)}`;
|
|
491
|
-
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
|
+
}
|
|
492
788
|
await fs2.writeFile(filePath, buffer);
|
|
493
789
|
paths.push(filePath);
|
|
494
790
|
}
|
|
@@ -575,7 +871,7 @@ function createToolValidator(schemas) {
|
|
|
575
871
|
|
|
576
872
|
// src/http/handler.ts
|
|
577
873
|
var WORKING_PROVIDER = "working";
|
|
578
|
-
async function setWorkingMessage(db, conversationId) {
|
|
874
|
+
async function setWorkingMessage(db, conversationId, parentMessageId) {
|
|
579
875
|
await db.ensureConversation(conversationId);
|
|
580
876
|
await db.clearWorkingMessages?.(conversationId);
|
|
581
877
|
await db.saveMessage({
|
|
@@ -583,9 +879,26 @@ async function setWorkingMessage(db, conversationId) {
|
|
|
583
879
|
role: "assistant",
|
|
584
880
|
content: "",
|
|
585
881
|
provider: WORKING_PROVIDER,
|
|
586
|
-
senderType: "ai"
|
|
882
|
+
senderType: "ai",
|
|
883
|
+
parentMessageId: parentMessageId ?? void 0
|
|
587
884
|
});
|
|
588
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
|
+
}
|
|
589
902
|
var SHARED_CONVERSATION_ID = "shared";
|
|
590
903
|
var conversationTurnSeq = /* @__PURE__ */ new Map();
|
|
591
904
|
function beginConversationTurn(conversationId) {
|
|
@@ -620,6 +933,7 @@ async function resolveSharedConversationId(request, options, bodyId) {
|
|
|
620
933
|
return SHARED_CONVERSATION_ID;
|
|
621
934
|
}
|
|
622
935
|
function createChatHandler(options) {
|
|
936
|
+
const logger = options.logger ?? createLogger("ai-cli:chat");
|
|
623
937
|
const validate = options.tools ? createToolValidator(options.tools) : null;
|
|
624
938
|
const workspaceDir = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
|
|
625
939
|
const baseCallOptions = {
|
|
@@ -628,6 +942,7 @@ function createChatHandler(options) {
|
|
|
628
942
|
providerPreference: options.providerPreference,
|
|
629
943
|
providers: options.providers
|
|
630
944
|
};
|
|
945
|
+
logger.debug({ workspaceDir }, "chat handler ready");
|
|
631
946
|
return {
|
|
632
947
|
async GET(request) {
|
|
633
948
|
try {
|
|
@@ -643,6 +958,14 @@ function createChatHandler(options) {
|
|
|
643
958
|
if (options.db?.listMessages) {
|
|
644
959
|
messages = await options.db.listMessages(conversationId);
|
|
645
960
|
}
|
|
961
|
+
logger.debug(
|
|
962
|
+
{
|
|
963
|
+
conversationId,
|
|
964
|
+
provider: provider.id,
|
|
965
|
+
messageCount: messages?.length ?? 0
|
|
966
|
+
},
|
|
967
|
+
"GET /api/chat"
|
|
968
|
+
);
|
|
646
969
|
return Response.json({
|
|
647
970
|
provider: provider.id,
|
|
648
971
|
providerLabel: provider.label || providerLabel(provider.id),
|
|
@@ -650,6 +973,7 @@ function createChatHandler(options) {
|
|
|
650
973
|
messages: messages ?? []
|
|
651
974
|
});
|
|
652
975
|
} catch (err) {
|
|
976
|
+
logger.error({ err }, "GET /api/chat failed");
|
|
653
977
|
const message = err instanceof Error ? err.message : "No AI CLI provider available";
|
|
654
978
|
return Response.json(
|
|
655
979
|
{ error: message, provider: null },
|
|
@@ -659,7 +983,9 @@ function createChatHandler(options) {
|
|
|
659
983
|
},
|
|
660
984
|
async POST(request) {
|
|
661
985
|
let trackedConversationId;
|
|
986
|
+
let trackedParentUserMessageId;
|
|
662
987
|
let trackedTurn = 0;
|
|
988
|
+
const startedAt = Date.now();
|
|
663
989
|
try {
|
|
664
990
|
const body = await request.json();
|
|
665
991
|
const conversationId = await resolveSharedConversationId(
|
|
@@ -668,6 +994,19 @@ function createChatHandler(options) {
|
|
|
668
994
|
body.conversationId
|
|
669
995
|
);
|
|
670
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
|
+
);
|
|
671
1010
|
if (body.type === "working-clear") {
|
|
672
1011
|
if (!conversationId) {
|
|
673
1012
|
return Response.json(
|
|
@@ -692,11 +1031,54 @@ function createChatHandler(options) {
|
|
|
692
1031
|
await options.db.ensureConversation(conversationId);
|
|
693
1032
|
await options.db.saveMessage({
|
|
694
1033
|
conversationId,
|
|
1034
|
+
id: body.messageId,
|
|
695
1035
|
role: "assistant",
|
|
696
1036
|
content,
|
|
697
1037
|
provider: "developer",
|
|
698
1038
|
senderType: "developer",
|
|
699
|
-
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
|
|
700
1082
|
});
|
|
701
1083
|
}
|
|
702
1084
|
return Response.json({ ok: true, conversationId });
|
|
@@ -727,27 +1109,65 @@ function createChatHandler(options) {
|
|
|
727
1109
|
const turn = beginConversationTurn(conversationId ?? SHARED_CONVERSATION_ID);
|
|
728
1110
|
trackedTurn = turn;
|
|
729
1111
|
const signal = request.signal;
|
|
1112
|
+
let parentUserMessageId = typeof body.userMessageId === "string" && body.userMessageId ? body.userMessageId : void 0;
|
|
1113
|
+
trackedParentUserMessageId = parentUserMessageId;
|
|
730
1114
|
if (options.db && conversationId) {
|
|
731
1115
|
const latest = messages[messages.length - 1];
|
|
732
1116
|
const persistContent = body.userMessage?.trim() || (latest?.role === "user" ? latest.content : "");
|
|
733
|
-
if (persistContent) {
|
|
1117
|
+
if (persistContent && !body.skipPersistUser && !parentUserMessageId) {
|
|
734
1118
|
await options.db.ensureConversation(conversationId);
|
|
735
|
-
await options.db.saveMessage({
|
|
1119
|
+
const saved = await options.db.saveMessage({
|
|
736
1120
|
conversationId,
|
|
737
1121
|
role: "user",
|
|
738
1122
|
content: persistContent,
|
|
739
1123
|
attachmentPaths: persistAttachmentPaths.length > 0 ? persistAttachmentPaths : void 0,
|
|
740
1124
|
senderType: body.senderType === "client" ? "client" : void 0,
|
|
741
|
-
senderName: body.senderName?.trim() || void 0
|
|
1125
|
+
senderName: body.senderName?.trim() || void 0,
|
|
1126
|
+
intent: "run"
|
|
742
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
|
+
);
|
|
743
1137
|
}
|
|
744
|
-
await setWorkingMessage(options.db, conversationId);
|
|
745
1138
|
}
|
|
746
1139
|
const latestUser = [...messages].reverse().find((m) => m.role === "user");
|
|
747
1140
|
const priorConversationsContext = options.db ? await buildPriorConversationsContext(options.db, {
|
|
748
1141
|
excludeConversationId: conversationId,
|
|
749
1142
|
currentRequest: latestUser?.content ?? ""
|
|
750
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
|
+
}
|
|
751
1171
|
if (!isActiveConversationTurn(
|
|
752
1172
|
conversationId ?? SHARED_CONVERSATION_ID,
|
|
753
1173
|
turn
|
|
@@ -758,23 +1178,58 @@ function createChatHandler(options) {
|
|
|
758
1178
|
});
|
|
759
1179
|
}
|
|
760
1180
|
if (signal.aborted) {
|
|
761
|
-
|
|
762
|
-
|
|
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
|
+
}
|
|
763
1191
|
}
|
|
764
1192
|
return Response.json({
|
|
765
1193
|
superseded: true,
|
|
766
1194
|
conversationId: conversationId ?? null
|
|
767
1195
|
});
|
|
768
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();
|
|
769
1211
|
const aiResponse = await callAi(messages, context, {
|
|
770
1212
|
...baseCallOptions,
|
|
771
1213
|
attachmentPaths,
|
|
772
|
-
priorConversationsContext: priorConversationsContext || void 0
|
|
1214
|
+
priorConversationsContext: priorConversationsContext || void 0,
|
|
1215
|
+
parentChainContext: parentChainContext || void 0
|
|
773
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
|
+
);
|
|
774
1228
|
if (!isActiveConversationTurn(
|
|
775
1229
|
conversationId ?? SHARED_CONVERSATION_ID,
|
|
776
1230
|
turn
|
|
777
1231
|
)) {
|
|
1232
|
+
logger.debug({ conversationId, turn }, "superseded after AI");
|
|
778
1233
|
return Response.json({
|
|
779
1234
|
superseded: true,
|
|
780
1235
|
conversationId: conversationId ?? null
|
|
@@ -782,38 +1237,108 @@ function createChatHandler(options) {
|
|
|
782
1237
|
}
|
|
783
1238
|
const validatedToolCalls = validate ? aiResponse.toolCalls.filter((tc) => validate(tc).valid) : aiResponse.toolCalls;
|
|
784
1239
|
if (options.onToolCalls && validatedToolCalls.length > 0) {
|
|
1240
|
+
logger.debug(
|
|
1241
|
+
{ conversationId, count: validatedToolCalls.length },
|
|
1242
|
+
"applying tool calls"
|
|
1243
|
+
);
|
|
785
1244
|
await options.onToolCalls(validatedToolCalls, context);
|
|
786
1245
|
}
|
|
1246
|
+
let nextQueued = null;
|
|
1247
|
+
const assistantMessageId = typeof body.assistantMessageId === "string" && body.assistantMessageId ? body.assistantMessageId : randomUUID();
|
|
1248
|
+
let replyId = assistantMessageId;
|
|
787
1249
|
if (options.db && conversationId) {
|
|
788
1250
|
await options.db.ensureConversation(conversationId);
|
|
789
|
-
await options.db.
|
|
790
|
-
await options.db.saveMessage({
|
|
1251
|
+
const savedReply = await options.db.saveMessage({
|
|
791
1252
|
conversationId,
|
|
1253
|
+
id: assistantMessageId,
|
|
792
1254
|
role: "assistant",
|
|
793
1255
|
content: aiResponse.text,
|
|
794
1256
|
provider: aiResponse.provider,
|
|
795
|
-
senderType: "ai"
|
|
1257
|
+
senderType: "ai",
|
|
1258
|
+
parentMessageId: parentUserMessageId
|
|
796
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
|
+
}
|
|
797
1291
|
if (validatedToolCalls.length > 0) {
|
|
798
1292
|
await options.db.saveToolEvents?.(
|
|
799
1293
|
conversationId,
|
|
800
1294
|
validatedToolCalls
|
|
801
1295
|
);
|
|
802
1296
|
}
|
|
1297
|
+
logger.debug(
|
|
1298
|
+
{
|
|
1299
|
+
conversationId,
|
|
1300
|
+
replyId,
|
|
1301
|
+
nextQueued: nextQueued?.id
|
|
1302
|
+
},
|
|
1303
|
+
"persisted AI reply"
|
|
1304
|
+
);
|
|
803
1305
|
}
|
|
1306
|
+
logger.debug(
|
|
1307
|
+
{ conversationId, turn, ms: Date.now() - startedAt },
|
|
1308
|
+
"POST /api/chat done"
|
|
1309
|
+
);
|
|
804
1310
|
return Response.json({
|
|
805
1311
|
text: aiResponse.text,
|
|
806
1312
|
toolCalls: validatedToolCalls,
|
|
807
1313
|
provider: aiResponse.provider,
|
|
808
1314
|
providerLabel: providerLabel(aiResponse.provider),
|
|
809
|
-
conversationId: conversationId ?? null
|
|
1315
|
+
conversationId: conversationId ?? null,
|
|
1316
|
+
messageId: replyId,
|
|
1317
|
+
nextQueued,
|
|
1318
|
+
parentMessageId: parentUserMessageId ?? null
|
|
810
1319
|
});
|
|
811
1320
|
} catch (err) {
|
|
812
1321
|
console.error("Chat API error:", err);
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
+
}
|
|
817
1342
|
}
|
|
818
1343
|
return Response.json(
|
|
819
1344
|
{
|
|
@@ -834,8 +1359,13 @@ function toNextRoute(handlers) {
|
|
|
834
1359
|
|
|
835
1360
|
// src/http/local-store.ts
|
|
836
1361
|
import fs3 from "fs/promises";
|
|
837
|
-
import
|
|
838
|
-
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
|
+
}
|
|
839
1369
|
async function readConversation(filePath) {
|
|
840
1370
|
try {
|
|
841
1371
|
const raw = await fs3.readFile(filePath, "utf8");
|
|
@@ -846,11 +1376,11 @@ async function readConversation(filePath) {
|
|
|
846
1376
|
}
|
|
847
1377
|
}
|
|
848
1378
|
async function writeConversation(filePath, data) {
|
|
849
|
-
await fs3.mkdir(
|
|
1379
|
+
await fs3.mkdir(path4.dirname(filePath), { recursive: true });
|
|
850
1380
|
await fs3.writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
851
1381
|
}
|
|
852
1382
|
function createLocalDirectoryStore(baseDir) {
|
|
853
|
-
const fileFor = (id) =>
|
|
1383
|
+
const fileFor = (id) => path4.join(baseDir, `${id}.json`);
|
|
854
1384
|
return {
|
|
855
1385
|
async ensureConversation(id) {
|
|
856
1386
|
const existing = await readConversation(fileFor(id));
|
|
@@ -868,21 +1398,273 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
868
1398
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
869
1399
|
messages: []
|
|
870
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
|
+
}
|
|
871
1495
|
const message = {
|
|
872
|
-
id:
|
|
1496
|
+
id: input.id ?? randomUUID2(),
|
|
873
1497
|
conversationId: input.conversationId,
|
|
874
1498
|
role: input.role,
|
|
875
1499
|
content: input.content,
|
|
876
1500
|
provider: input.provider ?? null,
|
|
877
|
-
createdAt:
|
|
1501
|
+
createdAt: now,
|
|
878
1502
|
attachmentPaths: input.attachmentPaths,
|
|
879
1503
|
senderType: input.senderType ?? null,
|
|
880
|
-
senderName: input.senderName ?? null
|
|
1504
|
+
senderName: input.senderName ?? null,
|
|
1505
|
+
parentMessageId: input.parentMessageId ?? null
|
|
881
1506
|
};
|
|
882
1507
|
existing.messages.push(message);
|
|
883
|
-
|
|
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;
|
|
884
1532
|
await writeConversation(filePath, existing);
|
|
885
|
-
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 };
|
|
886
1668
|
},
|
|
887
1669
|
async clearWorkingMessages(conversationId) {
|
|
888
1670
|
const filePath = fileFor(conversationId);
|
|
@@ -904,7 +1686,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
904
1686
|
const conversations = [];
|
|
905
1687
|
for (const entry of entries) {
|
|
906
1688
|
if (!entry.endsWith(".json")) continue;
|
|
907
|
-
const filePath =
|
|
1689
|
+
const filePath = path4.join(baseDir, entry);
|
|
908
1690
|
const existing = await readConversation(filePath);
|
|
909
1691
|
if (!existing?.id) continue;
|
|
910
1692
|
conversations.push({
|
|
@@ -921,7 +1703,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
921
1703
|
if (events.length === 0) return;
|
|
922
1704
|
const existing = await readConversation(fileFor(conversationId));
|
|
923
1705
|
if (!existing) return;
|
|
924
|
-
const toolFile =
|
|
1706
|
+
const toolFile = path4.join(baseDir, `${conversationId}.tools.jsonl`);
|
|
925
1707
|
const lines = events.map(
|
|
926
1708
|
(e) => JSON.stringify({
|
|
927
1709
|
conversationId,
|
|
@@ -938,7 +1720,353 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
938
1720
|
// src/http/maintainer-pro-store.ts
|
|
939
1721
|
import fs4 from "fs/promises";
|
|
940
1722
|
import os from "os";
|
|
941
|
-
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
|
|
942
2070
|
async function mpFetch(baseUrl, apiKey, pathName, init, fetchImpl) {
|
|
943
2071
|
const url = `${baseUrl.replace(/\/$/, "")}${pathName}`;
|
|
944
2072
|
const res = await fetchImpl(url, {
|
|
@@ -965,7 +2093,7 @@ function createMaintainerProStore(options) {
|
|
|
965
2093
|
const baseUrl = options.baseUrl;
|
|
966
2094
|
const apiKey = options.apiKey;
|
|
967
2095
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
968
|
-
const tempDir = options.tempDir ??
|
|
2096
|
+
const tempDir = options.tempDir ?? path5.join(os.tmpdir(), "maintainer-pro-store");
|
|
969
2097
|
const store = {
|
|
970
2098
|
async ensureConversation(id) {
|
|
971
2099
|
await mpFetch(
|
|
@@ -988,17 +2116,54 @@ function createMaintainerProStore(options) {
|
|
|
988
2116
|
{
|
|
989
2117
|
method: "POST",
|
|
990
2118
|
body: JSON.stringify({
|
|
2119
|
+
id: input.id,
|
|
991
2120
|
role: input.role,
|
|
992
2121
|
content: input.content,
|
|
993
2122
|
provider: input.provider,
|
|
994
2123
|
senderType: input.senderType,
|
|
995
2124
|
senderName: input.senderName,
|
|
996
|
-
|
|
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
|
|
997
2147
|
})
|
|
998
2148
|
},
|
|
999
2149
|
fetchImpl
|
|
1000
2150
|
);
|
|
1001
|
-
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
|
+
};
|
|
1002
2167
|
},
|
|
1003
2168
|
async clearWorkingMessages(conversationId) {
|
|
1004
2169
|
await mpFetch(
|
|
@@ -1029,6 +2194,16 @@ function createMaintainerProStore(options) {
|
|
|
1029
2194
|
);
|
|
1030
2195
|
return data.conversations ?? [];
|
|
1031
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
|
+
},
|
|
1032
2207
|
async saveToolEvents(conversationId, events) {
|
|
1033
2208
|
if (!events.length) return;
|
|
1034
2209
|
await mpFetch(
|
|
@@ -1070,7 +2245,7 @@ function createMaintainerProStore(options) {
|
|
|
1070
2245
|
attachmentIds.push(id);
|
|
1071
2246
|
refs.push(data.attachment.ref || `maintainer-pro://${id}`);
|
|
1072
2247
|
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 =
|
|
2248
|
+
const localPath = path5.join(
|
|
1074
2249
|
tempDir,
|
|
1075
2250
|
`${conversationId}-${id}${ext}`
|
|
1076
2251
|
);
|
|
@@ -1080,13 +2255,34 @@ function createMaintainerProStore(options) {
|
|
|
1080
2255
|
return { refs, localPaths, attachmentIds };
|
|
1081
2256
|
}
|
|
1082
2257
|
};
|
|
1083
|
-
|
|
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
|
+
});
|
|
1084
2273
|
}
|
|
1085
|
-
function createMaintainerProStoreFromEnv() {
|
|
2274
|
+
function createMaintainerProStoreFromEnv(options) {
|
|
1086
2275
|
const baseUrl = process.env.MAINTAINER_PRO_URL?.trim();
|
|
1087
2276
|
const apiKey = process.env.MAINTAINER_PRO_API_KEY?.trim();
|
|
1088
2277
|
if (!baseUrl || !apiKey) return null;
|
|
1089
|
-
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
|
+
});
|
|
1090
2286
|
}
|
|
1091
2287
|
|
|
1092
2288
|
// src/workspace-inspect.ts
|
|
@@ -1195,12 +2391,17 @@ async function inspectAndRepairWorkspace(input) {
|
|
|
1195
2391
|
};
|
|
1196
2392
|
}
|
|
1197
2393
|
export {
|
|
2394
|
+
ACCESS_IGNORE_BEGIN,
|
|
2395
|
+
ACCESS_IGNORE_END,
|
|
2396
|
+
DEFAULT_AI_IGNORE_PATHS,
|
|
2397
|
+
PROMPT_SECTION,
|
|
1198
2398
|
WORKING_PROVIDER,
|
|
1199
2399
|
buildClaudeUserPrompt,
|
|
1200
2400
|
buildConversationPrompt,
|
|
1201
2401
|
buildCursorPrompt,
|
|
1202
2402
|
buildPriorConversationsContext,
|
|
1203
2403
|
callAi,
|
|
2404
|
+
collectParentChain,
|
|
1204
2405
|
commandExists,
|
|
1205
2406
|
createAntigravityProvider,
|
|
1206
2407
|
createBuiltinProviders,
|
|
@@ -1208,17 +2409,34 @@ export {
|
|
|
1208
2409
|
createClaudeProvider,
|
|
1209
2410
|
createCursorProvider,
|
|
1210
2411
|
createDefaultSystemPrompt,
|
|
2412
|
+
createInfoLogger,
|
|
1211
2413
|
createLocalDirectoryStore,
|
|
2414
|
+
createLogger,
|
|
1212
2415
|
createMaintainerProStore,
|
|
1213
2416
|
createMaintainerProStoreFromEnv,
|
|
2417
|
+
createSyncedChatStore,
|
|
1214
2418
|
createToolValidator,
|
|
2419
|
+
formatAccessPolicyPromptSection,
|
|
1215
2420
|
formatClientContext,
|
|
2421
|
+
formatParentChainContext,
|
|
1216
2422
|
getProviderPreference,
|
|
1217
2423
|
inspectAndRepairWorkspace,
|
|
2424
|
+
isDevMode,
|
|
2425
|
+
isIgnoredRelative,
|
|
2426
|
+
isInsideWorkspace,
|
|
2427
|
+
isPathAllowed,
|
|
2428
|
+
normalizeIgnorePaths,
|
|
2429
|
+
parentChainForTurn,
|
|
1218
2430
|
parseAiResponse,
|
|
2431
|
+
parseIgnorePathsEnv,
|
|
2432
|
+
previewText,
|
|
1219
2433
|
providerLabel,
|
|
2434
|
+
renderManagedIgnoreBlock,
|
|
1220
2435
|
resolveCliBinary,
|
|
2436
|
+
resolveIgnorePaths,
|
|
2437
|
+
resolveLogLevel,
|
|
1221
2438
|
resolveProvider,
|
|
1222
2439
|
saveChatAttachments,
|
|
1223
|
-
toNextRoute
|
|
2440
|
+
toNextRoute,
|
|
2441
|
+
upsertManagedIgnoreFile
|
|
1224
2442
|
};
|