@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.cjs
CHANGED
|
@@ -30,12 +30,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
ACCESS_IGNORE_BEGIN: () => ACCESS_IGNORE_BEGIN,
|
|
34
|
+
ACCESS_IGNORE_END: () => ACCESS_IGNORE_END,
|
|
35
|
+
DEFAULT_AI_IGNORE_PATHS: () => DEFAULT_AI_IGNORE_PATHS,
|
|
36
|
+
PROMPT_SECTION: () => PROMPT_SECTION,
|
|
33
37
|
WORKING_PROVIDER: () => WORKING_PROVIDER,
|
|
34
38
|
buildClaudeUserPrompt: () => buildClaudeUserPrompt,
|
|
35
39
|
buildConversationPrompt: () => buildConversationPrompt,
|
|
36
40
|
buildCursorPrompt: () => buildCursorPrompt,
|
|
37
41
|
buildPriorConversationsContext: () => buildPriorConversationsContext,
|
|
38
42
|
callAi: () => callAi,
|
|
43
|
+
collectParentChain: () => collectParentChain,
|
|
39
44
|
commandExists: () => commandExists,
|
|
40
45
|
createAntigravityProvider: () => createAntigravityProvider,
|
|
41
46
|
createBuiltinProviders: () => createBuiltinProviders,
|
|
@@ -43,19 +48,36 @@ __export(index_exports, {
|
|
|
43
48
|
createClaudeProvider: () => createClaudeProvider,
|
|
44
49
|
createCursorProvider: () => createCursorProvider,
|
|
45
50
|
createDefaultSystemPrompt: () => createDefaultSystemPrompt,
|
|
51
|
+
createInfoLogger: () => createInfoLogger,
|
|
46
52
|
createLocalDirectoryStore: () => createLocalDirectoryStore,
|
|
53
|
+
createLogger: () => createLogger,
|
|
47
54
|
createMaintainerProStore: () => createMaintainerProStore,
|
|
48
55
|
createMaintainerProStoreFromEnv: () => createMaintainerProStoreFromEnv,
|
|
56
|
+
createSyncedChatStore: () => createSyncedChatStore,
|
|
49
57
|
createToolValidator: () => createToolValidator,
|
|
58
|
+
formatAccessPolicyPromptSection: () => formatAccessPolicyPromptSection,
|
|
50
59
|
formatClientContext: () => formatClientContext,
|
|
60
|
+
formatParentChainContext: () => formatParentChainContext,
|
|
51
61
|
getProviderPreference: () => getProviderPreference,
|
|
52
62
|
inspectAndRepairWorkspace: () => inspectAndRepairWorkspace,
|
|
63
|
+
isDevMode: () => isDevMode,
|
|
64
|
+
isIgnoredRelative: () => isIgnoredRelative,
|
|
65
|
+
isInsideWorkspace: () => isInsideWorkspace,
|
|
66
|
+
isPathAllowed: () => isPathAllowed,
|
|
67
|
+
normalizeIgnorePaths: () => normalizeIgnorePaths,
|
|
68
|
+
parentChainForTurn: () => parentChainForTurn,
|
|
53
69
|
parseAiResponse: () => parseAiResponse,
|
|
70
|
+
parseIgnorePathsEnv: () => parseIgnorePathsEnv,
|
|
71
|
+
previewText: () => previewText,
|
|
54
72
|
providerLabel: () => providerLabel,
|
|
73
|
+
renderManagedIgnoreBlock: () => renderManagedIgnoreBlock,
|
|
55
74
|
resolveCliBinary: () => resolveCliBinary,
|
|
75
|
+
resolveIgnorePaths: () => resolveIgnorePaths,
|
|
76
|
+
resolveLogLevel: () => resolveLogLevel,
|
|
56
77
|
resolveProvider: () => resolveProvider,
|
|
57
78
|
saveChatAttachments: () => saveChatAttachments,
|
|
58
|
-
toNextRoute: () => toNextRoute
|
|
79
|
+
toNextRoute: () => toNextRoute,
|
|
80
|
+
upsertManagedIgnoreFile: () => upsertManagedIgnoreFile
|
|
59
81
|
});
|
|
60
82
|
module.exports = __toCommonJS(index_exports);
|
|
61
83
|
|
|
@@ -63,6 +85,43 @@ module.exports = __toCommonJS(index_exports);
|
|
|
63
85
|
var import_execa2 = require("execa");
|
|
64
86
|
|
|
65
87
|
// src/prompt.ts
|
|
88
|
+
var PROMPT_SECTION = {
|
|
89
|
+
system: { begin: "<<<SYSTEM_BEGIN>>>", end: "<<<SYSTEM_END>>>" },
|
|
90
|
+
clientContext: {
|
|
91
|
+
begin: "<<<CLIENT_CONTEXT_BEGIN>>>",
|
|
92
|
+
end: "<<<CLIENT_CONTEXT_END>>>"
|
|
93
|
+
},
|
|
94
|
+
priorConversations: {
|
|
95
|
+
begin: "<<<PRIOR_CONVERSATIONS_BEGIN>>>",
|
|
96
|
+
end: "<<<PRIOR_CONVERSATIONS_END>>>"
|
|
97
|
+
},
|
|
98
|
+
parentChain: {
|
|
99
|
+
begin: "<<<PARENT_CHAIN_BEGIN>>>",
|
|
100
|
+
end: "<<<PARENT_CHAIN_END>>>"
|
|
101
|
+
},
|
|
102
|
+
history: {
|
|
103
|
+
begin: "<<<CONVERSATION_HISTORY_BEGIN>>>",
|
|
104
|
+
end: "<<<CONVERSATION_HISTORY_END>>>"
|
|
105
|
+
},
|
|
106
|
+
attachments: {
|
|
107
|
+
begin: "<<<ATTACHMENTS_BEGIN>>>",
|
|
108
|
+
end: "<<<ATTACHMENTS_END>>>"
|
|
109
|
+
},
|
|
110
|
+
currentRequest: {
|
|
111
|
+
begin: "<<<CURRENT_REQUEST_BEGIN>>>",
|
|
112
|
+
end: "<<<CURRENT_REQUEST_END>>>"
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
function wrapSection(tag, body, title) {
|
|
116
|
+
const trimmed = body.trim();
|
|
117
|
+
if (!trimmed) return "";
|
|
118
|
+
const header = title ? `${tag.begin} ${title}` : tag.begin;
|
|
119
|
+
return `${header}
|
|
120
|
+
${trimmed}
|
|
121
|
+
${tag.end}
|
|
122
|
+
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
66
125
|
function buildConversationPrompt(messages) {
|
|
67
126
|
if (messages.length === 0) return "";
|
|
68
127
|
const lines = messages.map(
|
|
@@ -104,50 +163,67 @@ function splitMessages(messages) {
|
|
|
104
163
|
const history = latestUserIndex > 0 ? messages.slice(0, latestUserIndex) : [];
|
|
105
164
|
return { request, history };
|
|
106
165
|
}
|
|
107
|
-
function buildUserFacingPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext, technical) {
|
|
166
|
+
function buildUserFacingPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext, technical, parentChainContext) {
|
|
108
167
|
const { request, history } = splitMessages(messages);
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const contextSection =
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
168
|
+
const systemSection = wrapSection(
|
|
169
|
+
PROMPT_SECTION.system,
|
|
170
|
+
systemPrompt ?? "",
|
|
171
|
+
"instructions"
|
|
172
|
+
);
|
|
173
|
+
const contextSection = wrapSection(
|
|
174
|
+
PROMPT_SECTION.clientContext,
|
|
175
|
+
formatClientContext(context),
|
|
176
|
+
"live UI snapshot"
|
|
177
|
+
);
|
|
178
|
+
const priorSection = wrapSection(
|
|
179
|
+
PROMPT_SECTION.priorConversations,
|
|
180
|
+
priorConversationsContext ?? "",
|
|
181
|
+
"other chats (secondary)"
|
|
182
|
+
);
|
|
183
|
+
const parentSection = wrapSection(
|
|
184
|
+
PROMPT_SECTION.parentChain,
|
|
185
|
+
parentChainContext ?? "",
|
|
186
|
+
"reply-to parents \u2014 primary thread context"
|
|
187
|
+
);
|
|
188
|
+
const historySection = wrapSection(
|
|
189
|
+
PROMPT_SECTION.history,
|
|
190
|
+
history.length > 0 ? buildConversationPrompt(history) : "",
|
|
191
|
+
"earlier turns in this conversation"
|
|
192
|
+
);
|
|
193
|
+
const attachmentsSection = wrapSection(
|
|
194
|
+
PROMPT_SECTION.attachments,
|
|
195
|
+
attachmentPaths && attachmentPaths.length > 0 ? `Screenshots / images attached by the user (open and inspect these image files):
|
|
196
|
+
${attachmentPaths.map((p) => `- ${p}`).join("\n")}` : "",
|
|
197
|
+
"user attachments"
|
|
198
|
+
);
|
|
199
|
+
const currentSection = wrapSection(
|
|
200
|
+
PROMPT_SECTION.currentRequest,
|
|
201
|
+
request || "(See the attached screenshot(s).)",
|
|
202
|
+
"answer this message"
|
|
203
|
+
);
|
|
127
204
|
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.";
|
|
128
|
-
return `${systemSection}${contextSection}${priorSection}${
|
|
129
|
-
${request || "(See the attached screenshot(s).)"}
|
|
130
|
-
|
|
131
|
-
${closer}`;
|
|
205
|
+
return `${systemSection}${contextSection}${priorSection}${parentSection}${historySection}${attachmentsSection}${currentSection}${closer}`;
|
|
132
206
|
}
|
|
133
|
-
function buildCursorPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext, technical) {
|
|
207
|
+
function buildCursorPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext, technical, parentChainContext) {
|
|
134
208
|
return buildUserFacingPrompt(
|
|
135
209
|
systemPrompt,
|
|
136
210
|
messages,
|
|
137
211
|
context,
|
|
138
212
|
attachmentPaths,
|
|
139
213
|
priorConversationsContext,
|
|
140
|
-
technical
|
|
214
|
+
technical,
|
|
215
|
+
parentChainContext
|
|
141
216
|
);
|
|
142
217
|
}
|
|
143
|
-
function buildClaudeUserPrompt(messages, context, attachmentPaths, priorConversationsContext, technical) {
|
|
218
|
+
function buildClaudeUserPrompt(messages, context, attachmentPaths, priorConversationsContext, technical, parentChainContext) {
|
|
144
219
|
return buildUserFacingPrompt(
|
|
145
220
|
null,
|
|
146
221
|
messages,
|
|
147
222
|
context,
|
|
148
223
|
attachmentPaths,
|
|
149
224
|
priorConversationsContext,
|
|
150
|
-
technical
|
|
225
|
+
technical,
|
|
226
|
+
parentChainContext
|
|
151
227
|
);
|
|
152
228
|
}
|
|
153
229
|
|
|
@@ -261,7 +337,8 @@ async function callClaudeCli(command, messages, context, options) {
|
|
|
261
337
|
context,
|
|
262
338
|
options.attachmentPaths,
|
|
263
339
|
options.priorConversationsContext,
|
|
264
|
-
options.technical
|
|
340
|
+
options.technical,
|
|
341
|
+
options.parentChainContext
|
|
265
342
|
);
|
|
266
343
|
const resolved = resolveCliCommand("claude", command);
|
|
267
344
|
const workspace = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
|
|
@@ -301,7 +378,8 @@ async function callCursorCli(command, messages, context, options) {
|
|
|
301
378
|
context,
|
|
302
379
|
options.attachmentPaths,
|
|
303
380
|
options.priorConversationsContext,
|
|
304
|
-
options.technical
|
|
381
|
+
options.technical,
|
|
382
|
+
options.parentChainContext
|
|
305
383
|
);
|
|
306
384
|
const resolved = resolveCliCommand("cursor", command);
|
|
307
385
|
const workspace = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
|
|
@@ -343,7 +421,8 @@ async function callAntigravityCli(command, messages, context, options) {
|
|
|
343
421
|
context,
|
|
344
422
|
options.attachmentPaths,
|
|
345
423
|
options.priorConversationsContext,
|
|
346
|
-
options.technical
|
|
424
|
+
options.technical,
|
|
425
|
+
options.parentChainContext
|
|
347
426
|
);
|
|
348
427
|
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).
|
|
349
428
|
|
|
@@ -384,8 +463,8 @@ async function resolveCommandPath(command) {
|
|
|
384
463
|
}
|
|
385
464
|
const result = await (0, import_execa2.execa)("which", [command], { reject: false });
|
|
386
465
|
if (result.exitCode !== 0) return null;
|
|
387
|
-
const
|
|
388
|
-
return
|
|
466
|
+
const path6 = result.stdout.trim().split(/\r?\n/)[0]?.trim();
|
|
467
|
+
return path6 || null;
|
|
389
468
|
} catch {
|
|
390
469
|
return null;
|
|
391
470
|
}
|
|
@@ -471,6 +550,51 @@ async function callAi(messages, context, options) {
|
|
|
471
550
|
return provider.call(messages, context, options);
|
|
472
551
|
}
|
|
473
552
|
|
|
553
|
+
// src/parent-chain.ts
|
|
554
|
+
function collectParentChain(rows, startId) {
|
|
555
|
+
if (!startId) return [];
|
|
556
|
+
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
557
|
+
const chain = [];
|
|
558
|
+
const seen = /* @__PURE__ */ new Set();
|
|
559
|
+
let current = byId.get(startId);
|
|
560
|
+
while (current && !seen.has(current.id)) {
|
|
561
|
+
seen.add(current.id);
|
|
562
|
+
const content = String(current.content ?? "").trim();
|
|
563
|
+
if (content && current.provider !== "working") {
|
|
564
|
+
const role = current.role === "assistant" || current.role === "system" ? current.role : "user";
|
|
565
|
+
chain.unshift({
|
|
566
|
+
id: current.id,
|
|
567
|
+
role,
|
|
568
|
+
content,
|
|
569
|
+
...current.senderName?.trim() ? { senderName: current.senderName.trim() } : {}
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
const nextId = current.parentMessageId?.trim();
|
|
573
|
+
current = nextId ? byId.get(nextId) : void 0;
|
|
574
|
+
}
|
|
575
|
+
return chain;
|
|
576
|
+
}
|
|
577
|
+
function parentChainForTurn(rows, currentMessageId, fallbackParentId) {
|
|
578
|
+
if (!currentMessageId && !fallbackParentId) return [];
|
|
579
|
+
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
580
|
+
const current = currentMessageId ? byId.get(currentMessageId) : void 0;
|
|
581
|
+
const replyToId = current?.parentMessageId && String(current.parentMessageId).trim() || fallbackParentId && String(fallbackParentId).trim() || null;
|
|
582
|
+
return collectParentChain(rows, replyToId);
|
|
583
|
+
}
|
|
584
|
+
function formatParentChainContext(chain) {
|
|
585
|
+
if (chain.length === 0) return "";
|
|
586
|
+
const lines = [
|
|
587
|
+
"context: this message has parents hierarchy linked with it",
|
|
588
|
+
"Prefer this chain over unrelated queued or later messages when interpreting the current request.",
|
|
589
|
+
""
|
|
590
|
+
];
|
|
591
|
+
chain.forEach((entry, index) => {
|
|
592
|
+
const who = entry.senderName || (entry.role === "assistant" ? "Assistant" : entry.role === "system" ? "System" : "Human");
|
|
593
|
+
lines.push(`${index + 1}. [${who}] ${entry.content}`);
|
|
594
|
+
});
|
|
595
|
+
return lines.join("\n");
|
|
596
|
+
}
|
|
597
|
+
|
|
474
598
|
// src/system-prompt.ts
|
|
475
599
|
function createDefaultSystemPrompt(input) {
|
|
476
600
|
const files = input.relevantFilesHint ? `Key UI files (use internally only; never name them in your reply):
|
|
@@ -481,6 +605,14 @@ Only when the user wants to change live in-app data (not source code), return a
|
|
|
481
605
|
${input.runtimeToolsHint}
|
|
482
606
|
|
|
483
607
|
Only use those runtime tools for live state. Never invent runtime tools.` : "";
|
|
608
|
+
const access = input.ignorePaths !== void 0 ? `
|
|
609
|
+
|
|
610
|
+
## File access boundaries (mandatory)
|
|
611
|
+
- You may only read or edit files inside the current project workspace directory.
|
|
612
|
+
- Never read, write, move, or delete files outside that workspace.
|
|
613
|
+
- Never touch paths that match this ignore list (relative to the workspace):
|
|
614
|
+
${input.ignorePaths.length ? input.ignorePaths.map((p) => `- ${p}`).join("\n") : "- (none beyond staying inside the workspace)"}
|
|
615
|
+
- 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.` : "";
|
|
484
616
|
return `You are a helpful product assistant for an app the user is looking at right now.
|
|
485
617
|
|
|
486
618
|
${input.productDescription}
|
|
@@ -501,6 +633,7 @@ Behind the scenes you can edit this repository and update live app data, but the
|
|
|
501
633
|
## When to edit the codebase
|
|
502
634
|
Edit source files when the user asks to change labels, layout, styling, copy, or behavior in the app.
|
|
503
635
|
${files}
|
|
636
|
+
${access}
|
|
504
637
|
|
|
505
638
|
${tools}
|
|
506
639
|
|
|
@@ -508,9 +641,191 @@ Do not refuse UI/label changes \u2014 implement them in source files.
|
|
|
508
641
|
Answer the user's latest request directly \u2014 never reply with a generic greeting.`;
|
|
509
642
|
}
|
|
510
643
|
|
|
644
|
+
// src/access-policy.ts
|
|
645
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
646
|
+
var DEFAULT_AI_IGNORE_PATHS = [
|
|
647
|
+
".env",
|
|
648
|
+
".env.*",
|
|
649
|
+
"**/.env",
|
|
650
|
+
"**/.env.*"
|
|
651
|
+
];
|
|
652
|
+
function parseIgnorePathsEnv(raw) {
|
|
653
|
+
if (!raw?.trim()) return [];
|
|
654
|
+
const trimmed = raw.trim();
|
|
655
|
+
try {
|
|
656
|
+
const parsed = JSON.parse(trimmed);
|
|
657
|
+
if (Array.isArray(parsed)) {
|
|
658
|
+
return normalizeIgnorePaths(parsed.map(String));
|
|
659
|
+
}
|
|
660
|
+
} catch {
|
|
661
|
+
}
|
|
662
|
+
return normalizeIgnorePaths(trimmed.split(/[\n,]+/));
|
|
663
|
+
}
|
|
664
|
+
function normalizeIgnorePaths(paths) {
|
|
665
|
+
const out = [];
|
|
666
|
+
const seen = /* @__PURE__ */ new Set();
|
|
667
|
+
for (const raw of paths) {
|
|
668
|
+
const p = raw.trim().replace(/\\/g, "/");
|
|
669
|
+
if (!p || p.startsWith("/") || p.includes("..")) continue;
|
|
670
|
+
if (seen.has(p)) continue;
|
|
671
|
+
seen.add(p);
|
|
672
|
+
out.push(p);
|
|
673
|
+
}
|
|
674
|
+
return out;
|
|
675
|
+
}
|
|
676
|
+
function resolveIgnorePaths(partnerPaths) {
|
|
677
|
+
return normalizeIgnorePaths([
|
|
678
|
+
...DEFAULT_AI_IGNORE_PATHS,
|
|
679
|
+
...partnerPaths
|
|
680
|
+
]);
|
|
681
|
+
}
|
|
682
|
+
function isInsideWorkspace(workspaceDir, targetPath) {
|
|
683
|
+
const root = import_node_path2.default.resolve(workspaceDir);
|
|
684
|
+
const target = import_node_path2.default.resolve(targetPath);
|
|
685
|
+
const rel = import_node_path2.default.relative(root, target);
|
|
686
|
+
return rel === "" || !rel.startsWith("..") && !import_node_path2.default.isAbsolute(rel);
|
|
687
|
+
}
|
|
688
|
+
function globToRegExp(pattern) {
|
|
689
|
+
let p = pattern.replace(/\\/g, "/");
|
|
690
|
+
if (p.endsWith("/")) p = `${p}**`;
|
|
691
|
+
let i = 0;
|
|
692
|
+
let out = "^";
|
|
693
|
+
while (i < p.length) {
|
|
694
|
+
if (p.startsWith("**/", i)) {
|
|
695
|
+
out += "(?:.*/)?";
|
|
696
|
+
i += 3;
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
if (p[i] === "*" && p[i + 1] !== "*") {
|
|
700
|
+
out += "[^/]*";
|
|
701
|
+
i += 1;
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (p.startsWith("**", i)) {
|
|
705
|
+
out += ".*";
|
|
706
|
+
i += 2;
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
if (p[i] === "?") {
|
|
710
|
+
out += "[^/]";
|
|
711
|
+
i += 1;
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
const ch = p[i];
|
|
715
|
+
if (/[.+^${}()|[\]\\]/.test(ch)) out += `\\${ch}`;
|
|
716
|
+
else out += ch;
|
|
717
|
+
i += 1;
|
|
718
|
+
}
|
|
719
|
+
out += "$";
|
|
720
|
+
return new RegExp(out, "i");
|
|
721
|
+
}
|
|
722
|
+
function isIgnoredRelative(relativePath, patterns) {
|
|
723
|
+
const rel = relativePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
724
|
+
if (!rel) return false;
|
|
725
|
+
return patterns.some((pattern) => {
|
|
726
|
+
const re = globToRegExp(pattern);
|
|
727
|
+
if (re.test(rel)) return true;
|
|
728
|
+
if (!pattern.includes("*") && !pattern.endsWith("/")) {
|
|
729
|
+
return rel === pattern || rel.startsWith(`${pattern}/`);
|
|
730
|
+
}
|
|
731
|
+
return false;
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
function isPathAllowed(workspaceDir, targetPath, ignorePaths) {
|
|
735
|
+
if (!isInsideWorkspace(workspaceDir, targetPath)) return false;
|
|
736
|
+
const rel = import_node_path2.default.relative(import_node_path2.default.resolve(workspaceDir), import_node_path2.default.resolve(targetPath));
|
|
737
|
+
return !isIgnoredRelative(rel, ignorePaths);
|
|
738
|
+
}
|
|
739
|
+
function formatAccessPolicyPromptSection(input) {
|
|
740
|
+
const ignores = input.ignorePaths.length ? input.ignorePaths.map((p) => `- ${p}`).join("\n") : "- (none beyond staying inside the workspace)";
|
|
741
|
+
return `## File access boundaries (mandatory)
|
|
742
|
+
- You may only read or edit files inside the current project workspace directory.
|
|
743
|
+
- Never read, write, move, or delete files outside that workspace.
|
|
744
|
+
- Never touch paths that match this ignore list (relative to the workspace):
|
|
745
|
+
${ignores}
|
|
746
|
+
- 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.`;
|
|
747
|
+
}
|
|
748
|
+
var ACCESS_IGNORE_BEGIN = "# maintainer-pro:access-begin";
|
|
749
|
+
var ACCESS_IGNORE_END = "# maintainer-pro:access-end";
|
|
750
|
+
function renderManagedIgnoreBlock(ignorePaths) {
|
|
751
|
+
const lines = [
|
|
752
|
+
ACCESS_IGNORE_BEGIN,
|
|
753
|
+
"# Managed by Maintainer Pro \u2014 do not edit this block by hand.",
|
|
754
|
+
...ignorePaths,
|
|
755
|
+
ACCESS_IGNORE_END
|
|
756
|
+
];
|
|
757
|
+
return `${lines.join("\n")}
|
|
758
|
+
`;
|
|
759
|
+
}
|
|
760
|
+
function upsertManagedIgnoreFile(existing, ignorePaths) {
|
|
761
|
+
const block = renderManagedIgnoreBlock(ignorePaths);
|
|
762
|
+
const begin = existing.indexOf(ACCESS_IGNORE_BEGIN);
|
|
763
|
+
const end = existing.indexOf(ACCESS_IGNORE_END);
|
|
764
|
+
if (begin >= 0 && end > begin) {
|
|
765
|
+
const afterEnd = end + ACCESS_IGNORE_END.length;
|
|
766
|
+
const before = existing.slice(0, begin).replace(/\s+$/, "");
|
|
767
|
+
let after = existing.slice(afterEnd).replace(/^\r?\n/, "");
|
|
768
|
+
const parts = [before, block.trimEnd(), after.trimStart()].filter(
|
|
769
|
+
(s) => s.length > 0
|
|
770
|
+
);
|
|
771
|
+
return `${parts.join("\n\n")}
|
|
772
|
+
`;
|
|
773
|
+
}
|
|
774
|
+
const trimmed = existing.replace(/\s+$/, "");
|
|
775
|
+
return trimmed ? `${trimmed}
|
|
776
|
+
|
|
777
|
+
${block}` : block;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/http/handler.ts
|
|
781
|
+
var import_node_crypto = require("crypto");
|
|
782
|
+
|
|
783
|
+
// src/log.ts
|
|
784
|
+
var import_pino = __toESM(require("pino"), 1);
|
|
785
|
+
function isDevMode() {
|
|
786
|
+
if (process.env.AI_DEV === "1" || process.env.AI_DEV === "true") return true;
|
|
787
|
+
const env = (process.env.NODE_ENV || process.env.AI_ENV || "").trim().toLowerCase();
|
|
788
|
+
return env === "development" || env === "dev" || env === "test";
|
|
789
|
+
}
|
|
790
|
+
function resolveLogLevel() {
|
|
791
|
+
const explicit = process.env.LOG_LEVEL?.trim() || process.env.AI_LOG_LEVEL?.trim();
|
|
792
|
+
if (explicit) return explicit;
|
|
793
|
+
return isDevMode() ? "debug" : "info";
|
|
794
|
+
}
|
|
795
|
+
function createLogger(name) {
|
|
796
|
+
const level = resolveLogLevel();
|
|
797
|
+
const pretty = process.env.LOG_PRETTY !== "0" && typeof process.stdout?.isTTY === "boolean" && process.stdout.isTTY;
|
|
798
|
+
if (pretty) {
|
|
799
|
+
return (0, import_pino.default)({
|
|
800
|
+
name,
|
|
801
|
+
level,
|
|
802
|
+
transport: {
|
|
803
|
+
target: "pino-pretty",
|
|
804
|
+
options: {
|
|
805
|
+
colorize: true,
|
|
806
|
+
translateTime: "HH:MM:ss",
|
|
807
|
+
ignore: "pid,hostname"
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
return (0, import_pino.default)({ name, level });
|
|
813
|
+
}
|
|
814
|
+
function createInfoLogger(name) {
|
|
815
|
+
const log = createLogger(name);
|
|
816
|
+
return (msg) => {
|
|
817
|
+
log.info(msg);
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function previewText(text, max = 160) {
|
|
821
|
+
const t = String(text ?? "").replace(/\s+/g, " ").trim();
|
|
822
|
+
if (!t) return "";
|
|
823
|
+
return t.length <= max ? t : `${t.slice(0, max)}\u2026`;
|
|
824
|
+
}
|
|
825
|
+
|
|
511
826
|
// src/http/attachments.ts
|
|
512
827
|
var import_promises = __toESM(require("fs/promises"), 1);
|
|
513
|
-
var
|
|
828
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
514
829
|
var MAX_ATTACHMENTS = 5;
|
|
515
830
|
var MAX_BYTES = 4 * 1024 * 1024;
|
|
516
831
|
var ALLOWED = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif"]);
|
|
@@ -530,7 +845,7 @@ function extForMime(mime) {
|
|
|
530
845
|
async function saveChatAttachments(attachments, workspaceDir) {
|
|
531
846
|
if (!attachments?.length) return [];
|
|
532
847
|
const selected = attachments.slice(0, MAX_ATTACHMENTS);
|
|
533
|
-
const dir =
|
|
848
|
+
const dir = import_node_path3.default.join(workspaceDir, ".maintainer-pro", "uploads");
|
|
534
849
|
await import_promises.default.mkdir(dir, { recursive: true });
|
|
535
850
|
const paths = [];
|
|
536
851
|
const stamp = Date.now();
|
|
@@ -549,7 +864,10 @@ async function saveChatAttachments(attachments, workspaceDir) {
|
|
|
549
864
|
}
|
|
550
865
|
const safeBase = (item.name || `screenshot-${i + 1}`).replace(/[^\w.\-]+/g, "_").slice(0, 64);
|
|
551
866
|
const fileName = `${stamp}-${i + 1}-${safeBase}${extForMime(mime)}`;
|
|
552
|
-
const filePath =
|
|
867
|
+
const filePath = import_node_path3.default.resolve(dir, fileName);
|
|
868
|
+
if (!isInsideWorkspace(workspaceDir, filePath)) {
|
|
869
|
+
throw new Error("Attachment path escaped the project workspace");
|
|
870
|
+
}
|
|
553
871
|
await import_promises.default.writeFile(filePath, buffer);
|
|
554
872
|
paths.push(filePath);
|
|
555
873
|
}
|
|
@@ -636,7 +954,7 @@ function createToolValidator(schemas) {
|
|
|
636
954
|
|
|
637
955
|
// src/http/handler.ts
|
|
638
956
|
var WORKING_PROVIDER = "working";
|
|
639
|
-
async function setWorkingMessage(db, conversationId) {
|
|
957
|
+
async function setWorkingMessage(db, conversationId, parentMessageId) {
|
|
640
958
|
await db.ensureConversation(conversationId);
|
|
641
959
|
await db.clearWorkingMessages?.(conversationId);
|
|
642
960
|
await db.saveMessage({
|
|
@@ -644,9 +962,26 @@ async function setWorkingMessage(db, conversationId) {
|
|
|
644
962
|
role: "assistant",
|
|
645
963
|
content: "",
|
|
646
964
|
provider: WORKING_PROVIDER,
|
|
647
|
-
senderType: "ai"
|
|
965
|
+
senderType: "ai",
|
|
966
|
+
parentMessageId: parentMessageId ?? void 0
|
|
648
967
|
});
|
|
649
968
|
}
|
|
969
|
+
function savedMessageFields(result) {
|
|
970
|
+
if (!result || typeof result !== "object") return {};
|
|
971
|
+
const row = result;
|
|
972
|
+
const next = row.nextQueued && typeof row.nextQueued === "object" && typeof row.nextQueued.id === "string" ? {
|
|
973
|
+
id: row.nextQueued.id,
|
|
974
|
+
content: String(
|
|
975
|
+
row.nextQueued.content ?? ""
|
|
976
|
+
)
|
|
977
|
+
} : null;
|
|
978
|
+
return {
|
|
979
|
+
id: typeof row.id === "string" ? row.id : void 0,
|
|
980
|
+
queueStatus: typeof row.queueStatus === "string" || row.queueStatus === null ? row.queueStatus : void 0,
|
|
981
|
+
queuePosition: typeof row.queuePosition === "number" || row.queuePosition === null ? row.queuePosition : void 0,
|
|
982
|
+
nextQueued: next
|
|
983
|
+
};
|
|
984
|
+
}
|
|
650
985
|
var SHARED_CONVERSATION_ID = "shared";
|
|
651
986
|
var conversationTurnSeq = /* @__PURE__ */ new Map();
|
|
652
987
|
function beginConversationTurn(conversationId) {
|
|
@@ -681,6 +1016,7 @@ async function resolveSharedConversationId(request, options, bodyId) {
|
|
|
681
1016
|
return SHARED_CONVERSATION_ID;
|
|
682
1017
|
}
|
|
683
1018
|
function createChatHandler(options) {
|
|
1019
|
+
const logger = options.logger ?? createLogger("ai-cli:chat");
|
|
684
1020
|
const validate = options.tools ? createToolValidator(options.tools) : null;
|
|
685
1021
|
const workspaceDir = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
|
|
686
1022
|
const baseCallOptions = {
|
|
@@ -689,6 +1025,7 @@ function createChatHandler(options) {
|
|
|
689
1025
|
providerPreference: options.providerPreference,
|
|
690
1026
|
providers: options.providers
|
|
691
1027
|
};
|
|
1028
|
+
logger.debug({ workspaceDir }, "chat handler ready");
|
|
692
1029
|
return {
|
|
693
1030
|
async GET(request) {
|
|
694
1031
|
try {
|
|
@@ -704,6 +1041,14 @@ function createChatHandler(options) {
|
|
|
704
1041
|
if (options.db?.listMessages) {
|
|
705
1042
|
messages = await options.db.listMessages(conversationId);
|
|
706
1043
|
}
|
|
1044
|
+
logger.debug(
|
|
1045
|
+
{
|
|
1046
|
+
conversationId,
|
|
1047
|
+
provider: provider.id,
|
|
1048
|
+
messageCount: messages?.length ?? 0
|
|
1049
|
+
},
|
|
1050
|
+
"GET /api/chat"
|
|
1051
|
+
);
|
|
707
1052
|
return Response.json({
|
|
708
1053
|
provider: provider.id,
|
|
709
1054
|
providerLabel: provider.label || providerLabel(provider.id),
|
|
@@ -711,6 +1056,7 @@ function createChatHandler(options) {
|
|
|
711
1056
|
messages: messages ?? []
|
|
712
1057
|
});
|
|
713
1058
|
} catch (err) {
|
|
1059
|
+
logger.error({ err }, "GET /api/chat failed");
|
|
714
1060
|
const message = err instanceof Error ? err.message : "No AI CLI provider available";
|
|
715
1061
|
return Response.json(
|
|
716
1062
|
{ error: message, provider: null },
|
|
@@ -720,7 +1066,9 @@ function createChatHandler(options) {
|
|
|
720
1066
|
},
|
|
721
1067
|
async POST(request) {
|
|
722
1068
|
let trackedConversationId;
|
|
1069
|
+
let trackedParentUserMessageId;
|
|
723
1070
|
let trackedTurn = 0;
|
|
1071
|
+
const startedAt = Date.now();
|
|
724
1072
|
try {
|
|
725
1073
|
const body = await request.json();
|
|
726
1074
|
const conversationId = await resolveSharedConversationId(
|
|
@@ -729,6 +1077,19 @@ function createChatHandler(options) {
|
|
|
729
1077
|
body.conversationId
|
|
730
1078
|
);
|
|
731
1079
|
trackedConversationId = conversationId;
|
|
1080
|
+
logger.debug(
|
|
1081
|
+
{
|
|
1082
|
+
conversationId,
|
|
1083
|
+
type: body.type ?? "turn",
|
|
1084
|
+
skipPersistUser: Boolean(body.skipPersistUser),
|
|
1085
|
+
userMessageId: body.userMessageId,
|
|
1086
|
+
messageCount: body.messages?.length ?? 0,
|
|
1087
|
+
attachmentCount: body.attachments?.length ?? 0,
|
|
1088
|
+
queueStatus: body.queueStatus,
|
|
1089
|
+
preview: previewText(body.userMessage || body.content)
|
|
1090
|
+
},
|
|
1091
|
+
"POST /api/chat"
|
|
1092
|
+
);
|
|
732
1093
|
if (body.type === "working-clear") {
|
|
733
1094
|
if (!conversationId) {
|
|
734
1095
|
return Response.json(
|
|
@@ -753,11 +1114,54 @@ function createChatHandler(options) {
|
|
|
753
1114
|
await options.db.ensureConversation(conversationId);
|
|
754
1115
|
await options.db.saveMessage({
|
|
755
1116
|
conversationId,
|
|
1117
|
+
id: body.messageId,
|
|
756
1118
|
role: "assistant",
|
|
757
1119
|
content,
|
|
758
1120
|
provider: "developer",
|
|
759
1121
|
senderType: "developer",
|
|
760
|
-
senderName: body.senderName?.trim() || void 0
|
|
1122
|
+
senderName: body.senderName?.trim() || void 0,
|
|
1123
|
+
parentMessageId: body.parentMessageId
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
return Response.json({ ok: true, conversationId });
|
|
1127
|
+
}
|
|
1128
|
+
if (body.type === "user") {
|
|
1129
|
+
const content = body.content?.trim();
|
|
1130
|
+
if (!conversationId || !content) {
|
|
1131
|
+
return Response.json(
|
|
1132
|
+
{ error: "conversationId and content are required" },
|
|
1133
|
+
{ status: 400 }
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
if (options.db) {
|
|
1137
|
+
await options.db.ensureConversation(conversationId);
|
|
1138
|
+
let persistAttachmentPaths2 = [];
|
|
1139
|
+
if (options.db.uploadAttachments && body.attachments?.length) {
|
|
1140
|
+
const uploaded = await options.db.uploadAttachments(
|
|
1141
|
+
conversationId,
|
|
1142
|
+
body.attachments
|
|
1143
|
+
);
|
|
1144
|
+
persistAttachmentPaths2 = uploaded.refs;
|
|
1145
|
+
}
|
|
1146
|
+
const saved = await options.db.saveMessage({
|
|
1147
|
+
conversationId,
|
|
1148
|
+
id: body.messageId,
|
|
1149
|
+
role: "user",
|
|
1150
|
+
content,
|
|
1151
|
+
attachmentPaths: persistAttachmentPaths2.length > 0 ? persistAttachmentPaths2 : void 0,
|
|
1152
|
+
senderType: body.senderType === "client" ? "client" : void 0,
|
|
1153
|
+
senderName: body.senderName?.trim() || void 0,
|
|
1154
|
+
parentMessageId: body.parentMessageId,
|
|
1155
|
+
intent: "queue",
|
|
1156
|
+
queueStatus: body.queueStatus === "queued" || body.queueStatus === "working" ? body.queueStatus : void 0
|
|
1157
|
+
});
|
|
1158
|
+
const fields = savedMessageFields(saved);
|
|
1159
|
+
return Response.json({
|
|
1160
|
+
ok: true,
|
|
1161
|
+
conversationId,
|
|
1162
|
+
message: saved,
|
|
1163
|
+
queueStatus: fields.queueStatus ?? null,
|
|
1164
|
+
queuePosition: fields.queuePosition ?? null
|
|
761
1165
|
});
|
|
762
1166
|
}
|
|
763
1167
|
return Response.json({ ok: true, conversationId });
|
|
@@ -788,27 +1192,65 @@ function createChatHandler(options) {
|
|
|
788
1192
|
const turn = beginConversationTurn(conversationId ?? SHARED_CONVERSATION_ID);
|
|
789
1193
|
trackedTurn = turn;
|
|
790
1194
|
const signal = request.signal;
|
|
1195
|
+
let parentUserMessageId = typeof body.userMessageId === "string" && body.userMessageId ? body.userMessageId : void 0;
|
|
1196
|
+
trackedParentUserMessageId = parentUserMessageId;
|
|
791
1197
|
if (options.db && conversationId) {
|
|
792
1198
|
const latest = messages[messages.length - 1];
|
|
793
1199
|
const persistContent = body.userMessage?.trim() || (latest?.role === "user" ? latest.content : "");
|
|
794
|
-
if (persistContent) {
|
|
1200
|
+
if (persistContent && !body.skipPersistUser && !parentUserMessageId) {
|
|
795
1201
|
await options.db.ensureConversation(conversationId);
|
|
796
|
-
await options.db.saveMessage({
|
|
1202
|
+
const saved = await options.db.saveMessage({
|
|
797
1203
|
conversationId,
|
|
798
1204
|
role: "user",
|
|
799
1205
|
content: persistContent,
|
|
800
1206
|
attachmentPaths: persistAttachmentPaths.length > 0 ? persistAttachmentPaths : void 0,
|
|
801
1207
|
senderType: body.senderType === "client" ? "client" : void 0,
|
|
802
|
-
senderName: body.senderName?.trim() || void 0
|
|
1208
|
+
senderName: body.senderName?.trim() || void 0,
|
|
1209
|
+
intent: "run"
|
|
803
1210
|
});
|
|
1211
|
+
parentUserMessageId = savedMessageFields(saved).id ?? parentUserMessageId;
|
|
1212
|
+
trackedParentUserMessageId = parentUserMessageId;
|
|
1213
|
+
}
|
|
1214
|
+
if (!body.skipPersistUser) {
|
|
1215
|
+
await setWorkingMessage(
|
|
1216
|
+
options.db,
|
|
1217
|
+
conversationId,
|
|
1218
|
+
parentUserMessageId
|
|
1219
|
+
);
|
|
804
1220
|
}
|
|
805
|
-
await setWorkingMessage(options.db, conversationId);
|
|
806
1221
|
}
|
|
807
1222
|
const latestUser = [...messages].reverse().find((m) => m.role === "user");
|
|
808
1223
|
const priorConversationsContext = options.db ? await buildPriorConversationsContext(options.db, {
|
|
809
1224
|
excludeConversationId: conversationId,
|
|
810
1225
|
currentRequest: latestUser?.content ?? ""
|
|
811
1226
|
}) : "";
|
|
1227
|
+
let parentChainContext = "";
|
|
1228
|
+
if (options.db?.listMessages && conversationId) {
|
|
1229
|
+
const stored = await options.db.listMessages(conversationId);
|
|
1230
|
+
const chain = parentChainForTurn(
|
|
1231
|
+
stored.map((row) => ({
|
|
1232
|
+
id: row.id,
|
|
1233
|
+
role: row.role,
|
|
1234
|
+
content: row.content,
|
|
1235
|
+
parentMessageId: row.parentMessageId,
|
|
1236
|
+
senderName: row.senderName,
|
|
1237
|
+
provider: row.provider
|
|
1238
|
+
})),
|
|
1239
|
+
parentUserMessageId,
|
|
1240
|
+
body.parentMessageId
|
|
1241
|
+
);
|
|
1242
|
+
parentChainContext = formatParentChainContext(chain);
|
|
1243
|
+
if (parentChainContext) {
|
|
1244
|
+
logger.debug(
|
|
1245
|
+
{
|
|
1246
|
+
conversationId,
|
|
1247
|
+
parentUserMessageId,
|
|
1248
|
+
chainLength: chain.length
|
|
1249
|
+
},
|
|
1250
|
+
"parent chain context"
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
812
1254
|
if (!isActiveConversationTurn(
|
|
813
1255
|
conversationId ?? SHARED_CONVERSATION_ID,
|
|
814
1256
|
turn
|
|
@@ -819,23 +1261,58 @@ function createChatHandler(options) {
|
|
|
819
1261
|
});
|
|
820
1262
|
}
|
|
821
1263
|
if (signal.aborted) {
|
|
822
|
-
|
|
823
|
-
|
|
1264
|
+
logger.debug({ conversationId, turn }, "aborted before AI");
|
|
1265
|
+
if (conversationId && options.db) {
|
|
1266
|
+
if (options.db.releaseWorkingTurn) {
|
|
1267
|
+
await options.db.releaseWorkingTurn(
|
|
1268
|
+
conversationId,
|
|
1269
|
+
parentUserMessageId
|
|
1270
|
+
);
|
|
1271
|
+
} else {
|
|
1272
|
+
await options.db.clearWorkingMessages?.(conversationId);
|
|
1273
|
+
}
|
|
824
1274
|
}
|
|
825
1275
|
return Response.json({
|
|
826
1276
|
superseded: true,
|
|
827
1277
|
conversationId: conversationId ?? null
|
|
828
1278
|
});
|
|
829
1279
|
}
|
|
1280
|
+
logger.debug(
|
|
1281
|
+
{
|
|
1282
|
+
conversationId,
|
|
1283
|
+
turn,
|
|
1284
|
+
parentUserMessageId,
|
|
1285
|
+
historyLength: messages.length,
|
|
1286
|
+
hasParentChain: Boolean(parentChainContext),
|
|
1287
|
+
preview: previewText(
|
|
1288
|
+
[...messages].reverse().find((m) => m.role === "user")?.content
|
|
1289
|
+
)
|
|
1290
|
+
},
|
|
1291
|
+
"calling AI provider"
|
|
1292
|
+
);
|
|
1293
|
+
const aiStarted = Date.now();
|
|
830
1294
|
const aiResponse = await callAi(messages, context, {
|
|
831
1295
|
...baseCallOptions,
|
|
832
1296
|
attachmentPaths,
|
|
833
|
-
priorConversationsContext: priorConversationsContext || void 0
|
|
1297
|
+
priorConversationsContext: priorConversationsContext || void 0,
|
|
1298
|
+
parentChainContext: parentChainContext || void 0
|
|
834
1299
|
});
|
|
1300
|
+
logger.debug(
|
|
1301
|
+
{
|
|
1302
|
+
conversationId,
|
|
1303
|
+
turn,
|
|
1304
|
+
provider: aiResponse.provider,
|
|
1305
|
+
toolCalls: aiResponse.toolCalls.length,
|
|
1306
|
+
ms: Date.now() - aiStarted,
|
|
1307
|
+
preview: previewText(aiResponse.text)
|
|
1308
|
+
},
|
|
1309
|
+
"AI provider returned"
|
|
1310
|
+
);
|
|
835
1311
|
if (!isActiveConversationTurn(
|
|
836
1312
|
conversationId ?? SHARED_CONVERSATION_ID,
|
|
837
1313
|
turn
|
|
838
1314
|
)) {
|
|
1315
|
+
logger.debug({ conversationId, turn }, "superseded after AI");
|
|
839
1316
|
return Response.json({
|
|
840
1317
|
superseded: true,
|
|
841
1318
|
conversationId: conversationId ?? null
|
|
@@ -843,38 +1320,108 @@ function createChatHandler(options) {
|
|
|
843
1320
|
}
|
|
844
1321
|
const validatedToolCalls = validate ? aiResponse.toolCalls.filter((tc) => validate(tc).valid) : aiResponse.toolCalls;
|
|
845
1322
|
if (options.onToolCalls && validatedToolCalls.length > 0) {
|
|
1323
|
+
logger.debug(
|
|
1324
|
+
{ conversationId, count: validatedToolCalls.length },
|
|
1325
|
+
"applying tool calls"
|
|
1326
|
+
);
|
|
846
1327
|
await options.onToolCalls(validatedToolCalls, context);
|
|
847
1328
|
}
|
|
1329
|
+
let nextQueued = null;
|
|
1330
|
+
const assistantMessageId = typeof body.assistantMessageId === "string" && body.assistantMessageId ? body.assistantMessageId : (0, import_node_crypto.randomUUID)();
|
|
1331
|
+
let replyId = assistantMessageId;
|
|
848
1332
|
if (options.db && conversationId) {
|
|
849
1333
|
await options.db.ensureConversation(conversationId);
|
|
850
|
-
await options.db.
|
|
851
|
-
await options.db.saveMessage({
|
|
1334
|
+
const savedReply = await options.db.saveMessage({
|
|
852
1335
|
conversationId,
|
|
1336
|
+
id: assistantMessageId,
|
|
853
1337
|
role: "assistant",
|
|
854
1338
|
content: aiResponse.text,
|
|
855
1339
|
provider: aiResponse.provider,
|
|
856
|
-
senderType: "ai"
|
|
1340
|
+
senderType: "ai",
|
|
1341
|
+
parentMessageId: parentUserMessageId
|
|
857
1342
|
});
|
|
1343
|
+
replyId = savedMessageFields(savedReply).id ?? assistantMessageId;
|
|
1344
|
+
if (options.db.releaseWorkingTurn) {
|
|
1345
|
+
let released = void 0;
|
|
1346
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1347
|
+
try {
|
|
1348
|
+
released = await options.db.releaseWorkingTurn(
|
|
1349
|
+
conversationId,
|
|
1350
|
+
parentUserMessageId
|
|
1351
|
+
);
|
|
1352
|
+
break;
|
|
1353
|
+
} catch (err) {
|
|
1354
|
+
logger.warn(
|
|
1355
|
+
{ err, conversationId, parentUserMessageId, attempt },
|
|
1356
|
+
"releaseWorkingTurn failed; retrying"
|
|
1357
|
+
);
|
|
1358
|
+
if (attempt === 2) throw err;
|
|
1359
|
+
await new Promise((r) => setTimeout(r, 250 * (attempt + 1)));
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
nextQueued = released && typeof released === "object" && released.nextQueued && typeof released.nextQueued.id === "string" ? {
|
|
1363
|
+
id: released.nextQueued.id,
|
|
1364
|
+
content: String(released.nextQueued.content ?? "")
|
|
1365
|
+
} : null;
|
|
1366
|
+
logger.debug(
|
|
1367
|
+
{ conversationId, parentUserMessageId, nextQueued: nextQueued?.id },
|
|
1368
|
+
"released working turn"
|
|
1369
|
+
);
|
|
1370
|
+
} else {
|
|
1371
|
+
await options.db.clearWorkingMessages?.(conversationId);
|
|
1372
|
+
nextQueued = savedMessageFields(savedReply).nextQueued ?? null;
|
|
1373
|
+
}
|
|
858
1374
|
if (validatedToolCalls.length > 0) {
|
|
859
1375
|
await options.db.saveToolEvents?.(
|
|
860
1376
|
conversationId,
|
|
861
1377
|
validatedToolCalls
|
|
862
1378
|
);
|
|
863
1379
|
}
|
|
1380
|
+
logger.debug(
|
|
1381
|
+
{
|
|
1382
|
+
conversationId,
|
|
1383
|
+
replyId,
|
|
1384
|
+
nextQueued: nextQueued?.id
|
|
1385
|
+
},
|
|
1386
|
+
"persisted AI reply"
|
|
1387
|
+
);
|
|
864
1388
|
}
|
|
1389
|
+
logger.debug(
|
|
1390
|
+
{ conversationId, turn, ms: Date.now() - startedAt },
|
|
1391
|
+
"POST /api/chat done"
|
|
1392
|
+
);
|
|
865
1393
|
return Response.json({
|
|
866
1394
|
text: aiResponse.text,
|
|
867
1395
|
toolCalls: validatedToolCalls,
|
|
868
1396
|
provider: aiResponse.provider,
|
|
869
1397
|
providerLabel: providerLabel(aiResponse.provider),
|
|
870
|
-
conversationId: conversationId ?? null
|
|
1398
|
+
conversationId: conversationId ?? null,
|
|
1399
|
+
messageId: replyId,
|
|
1400
|
+
nextQueued,
|
|
1401
|
+
parentMessageId: parentUserMessageId ?? null
|
|
871
1402
|
});
|
|
872
1403
|
} catch (err) {
|
|
873
1404
|
console.error("Chat API error:", err);
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1405
|
+
logger.error(
|
|
1406
|
+
{
|
|
1407
|
+
err,
|
|
1408
|
+
conversationId: trackedConversationId,
|
|
1409
|
+
turn: trackedTurn,
|
|
1410
|
+
ms: Date.now() - startedAt
|
|
1411
|
+
},
|
|
1412
|
+
"POST /api/chat failed"
|
|
1413
|
+
);
|
|
1414
|
+
if (trackedConversationId && options.db && isActiveConversationTurn(trackedConversationId, trackedTurn)) {
|
|
1415
|
+
if (options.db.releaseWorkingTurn) {
|
|
1416
|
+
await options.db.releaseWorkingTurn(
|
|
1417
|
+
trackedConversationId,
|
|
1418
|
+
trackedParentUserMessageId
|
|
1419
|
+
).catch(() => void 0);
|
|
1420
|
+
} else {
|
|
1421
|
+
await options.db.clearWorkingMessages?.(trackedConversationId).catch(
|
|
1422
|
+
() => void 0
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
878
1425
|
}
|
|
879
1426
|
return Response.json(
|
|
880
1427
|
{
|
|
@@ -895,8 +1442,13 @@ function toNextRoute(handlers) {
|
|
|
895
1442
|
|
|
896
1443
|
// src/http/local-store.ts
|
|
897
1444
|
var import_promises2 = __toESM(require("fs/promises"), 1);
|
|
898
|
-
var
|
|
899
|
-
var
|
|
1445
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
1446
|
+
var import_node_crypto2 = require("crypto");
|
|
1447
|
+
function userHasAiReply(rows, workingId) {
|
|
1448
|
+
return rows.some(
|
|
1449
|
+
(m) => m.role === "assistant" && m.parentMessageId === workingId && m.provider !== "working" && Boolean(m.content?.trim())
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
900
1452
|
async function readConversation(filePath) {
|
|
901
1453
|
try {
|
|
902
1454
|
const raw = await import_promises2.default.readFile(filePath, "utf8");
|
|
@@ -907,11 +1459,11 @@ async function readConversation(filePath) {
|
|
|
907
1459
|
}
|
|
908
1460
|
}
|
|
909
1461
|
async function writeConversation(filePath, data) {
|
|
910
|
-
await import_promises2.default.mkdir(
|
|
1462
|
+
await import_promises2.default.mkdir(import_node_path4.default.dirname(filePath), { recursive: true });
|
|
911
1463
|
await import_promises2.default.writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
912
1464
|
}
|
|
913
1465
|
function createLocalDirectoryStore(baseDir) {
|
|
914
|
-
const fileFor = (id) =>
|
|
1466
|
+
const fileFor = (id) => import_node_path4.default.join(baseDir, `${id}.json`);
|
|
915
1467
|
return {
|
|
916
1468
|
async ensureConversation(id) {
|
|
917
1469
|
const existing = await readConversation(fileFor(id));
|
|
@@ -929,21 +1481,273 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
929
1481
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
930
1482
|
messages: []
|
|
931
1483
|
};
|
|
1484
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1485
|
+
const isClientUser = input.role === "user" && input.senderType !== "developer" && input.provider !== "developer";
|
|
1486
|
+
const isWorking = input.provider === "working";
|
|
1487
|
+
const isAi = input.role === "assistant" && input.provider !== "working" && input.senderType !== "developer" && input.provider !== "developer";
|
|
1488
|
+
const busy = existing.messages.some((m) => m.queueStatus === "working");
|
|
1489
|
+
const makeWorking = (parentMessageId) => ({
|
|
1490
|
+
id: (0, import_node_crypto2.randomUUID)(),
|
|
1491
|
+
conversationId: input.conversationId,
|
|
1492
|
+
role: "assistant",
|
|
1493
|
+
content: "",
|
|
1494
|
+
provider: "working",
|
|
1495
|
+
createdAt: now,
|
|
1496
|
+
senderType: "ai",
|
|
1497
|
+
parentMessageId
|
|
1498
|
+
});
|
|
1499
|
+
const promoteNext = () => {
|
|
1500
|
+
const waiting = existing.messages.filter((m) => m.queueStatus === "queued").sort(
|
|
1501
|
+
(a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)
|
|
1502
|
+
);
|
|
1503
|
+
const next = waiting[0];
|
|
1504
|
+
if (!next) return null;
|
|
1505
|
+
next.queueStatus = "working";
|
|
1506
|
+
next.queuePosition = null;
|
|
1507
|
+
existing.messages.push(makeWorking(next.id));
|
|
1508
|
+
return next;
|
|
1509
|
+
};
|
|
1510
|
+
if (isClientUser) {
|
|
1511
|
+
if (input.intent === "run") {
|
|
1512
|
+
for (const row of existing.messages) {
|
|
1513
|
+
if (row.queueStatus === "working") {
|
|
1514
|
+
row.queueStatus = null;
|
|
1515
|
+
row.queuePosition = null;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
existing.messages = existing.messages.filter(
|
|
1519
|
+
(m) => m.provider !== "working"
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
const queueStatus = input.intent === "run" ? "working" : busy ? "queued" : "working";
|
|
1523
|
+
const existingById2 = input.id ? existing.messages.find((m) => m.id === input.id) : void 0;
|
|
1524
|
+
if (existingById2) {
|
|
1525
|
+
return { ...existingById2, nextQueued: null };
|
|
1526
|
+
}
|
|
1527
|
+
const message2 = {
|
|
1528
|
+
id: input.id ?? (0, import_node_crypto2.randomUUID)(),
|
|
1529
|
+
conversationId: input.conversationId,
|
|
1530
|
+
role: input.role,
|
|
1531
|
+
content: input.content,
|
|
1532
|
+
provider: input.provider ?? null,
|
|
1533
|
+
createdAt: now,
|
|
1534
|
+
attachmentPaths: input.attachmentPaths,
|
|
1535
|
+
senderType: input.senderType ?? null,
|
|
1536
|
+
senderName: input.senderName ?? null,
|
|
1537
|
+
parentMessageId: input.parentMessageId ?? null,
|
|
1538
|
+
queueStatus,
|
|
1539
|
+
queuePosition: null
|
|
1540
|
+
};
|
|
1541
|
+
existing.messages.push(message2);
|
|
1542
|
+
existing.updatedAt = now;
|
|
1543
|
+
await writeConversation(filePath, existing);
|
|
1544
|
+
return { ...message2, nextQueued: null };
|
|
1545
|
+
}
|
|
1546
|
+
if (isWorking) {
|
|
1547
|
+
existing.messages = existing.messages.filter(
|
|
1548
|
+
(m) => m.provider !== "working"
|
|
1549
|
+
);
|
|
1550
|
+
if (input.parentMessageId) {
|
|
1551
|
+
const parent = existing.messages.find(
|
|
1552
|
+
(m) => m.id === input.parentMessageId
|
|
1553
|
+
);
|
|
1554
|
+
if (parent && parent.queueStatus !== "working") {
|
|
1555
|
+
parent.queueStatus = "working";
|
|
1556
|
+
parent.queuePosition = null;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
const message2 = {
|
|
1560
|
+
id: input.id ?? (0, import_node_crypto2.randomUUID)(),
|
|
1561
|
+
conversationId: input.conversationId,
|
|
1562
|
+
role: "assistant",
|
|
1563
|
+
content: "",
|
|
1564
|
+
provider: "working",
|
|
1565
|
+
createdAt: now,
|
|
1566
|
+
senderType: "ai",
|
|
1567
|
+
parentMessageId: input.parentMessageId ?? null
|
|
1568
|
+
};
|
|
1569
|
+
existing.messages.push(message2);
|
|
1570
|
+
existing.updatedAt = now;
|
|
1571
|
+
await writeConversation(filePath, existing);
|
|
1572
|
+
return { ...message2, nextQueued: null };
|
|
1573
|
+
}
|
|
1574
|
+
const existingById = input.id ? existing.messages.find((m) => m.id === input.id) : void 0;
|
|
1575
|
+
if (existingById) {
|
|
1576
|
+
return { ...existingById, nextQueued: null };
|
|
1577
|
+
}
|
|
932
1578
|
const message = {
|
|
933
|
-
id: (0,
|
|
1579
|
+
id: input.id ?? (0, import_node_crypto2.randomUUID)(),
|
|
934
1580
|
conversationId: input.conversationId,
|
|
935
1581
|
role: input.role,
|
|
936
1582
|
content: input.content,
|
|
937
1583
|
provider: input.provider ?? null,
|
|
938
|
-
createdAt:
|
|
1584
|
+
createdAt: now,
|
|
939
1585
|
attachmentPaths: input.attachmentPaths,
|
|
940
1586
|
senderType: input.senderType ?? null,
|
|
941
|
-
senderName: input.senderName ?? null
|
|
1587
|
+
senderName: input.senderName ?? null,
|
|
1588
|
+
parentMessageId: input.parentMessageId ?? null
|
|
942
1589
|
};
|
|
943
1590
|
existing.messages.push(message);
|
|
944
|
-
|
|
1591
|
+
let nextQueued = null;
|
|
1592
|
+
if (isAi) {
|
|
1593
|
+
existing.messages = existing.messages.filter(
|
|
1594
|
+
(m) => m.provider !== "working"
|
|
1595
|
+
);
|
|
1596
|
+
if (input.parentMessageId) {
|
|
1597
|
+
const parent = existing.messages.find(
|
|
1598
|
+
(m) => m.id === input.parentMessageId
|
|
1599
|
+
);
|
|
1600
|
+
if (parent) {
|
|
1601
|
+
parent.queueStatus = null;
|
|
1602
|
+
parent.queuePosition = null;
|
|
1603
|
+
}
|
|
1604
|
+
} else {
|
|
1605
|
+
for (const row of existing.messages) {
|
|
1606
|
+
if (row.queueStatus === "working") {
|
|
1607
|
+
row.queueStatus = null;
|
|
1608
|
+
row.queuePosition = null;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
nextQueued = promoteNext();
|
|
1613
|
+
}
|
|
1614
|
+
existing.updatedAt = now;
|
|
945
1615
|
await writeConversation(filePath, existing);
|
|
946
|
-
return
|
|
1616
|
+
return {
|
|
1617
|
+
...message,
|
|
1618
|
+
nextQueued: nextQueued ? { id: nextQueued.id, content: nextQueued.content } : null
|
|
1619
|
+
};
|
|
1620
|
+
},
|
|
1621
|
+
async recoverQueue(conversationId) {
|
|
1622
|
+
const filePath = fileFor(conversationId);
|
|
1623
|
+
const existing = await readConversation(filePath);
|
|
1624
|
+
if (!existing) {
|
|
1625
|
+
return {
|
|
1626
|
+
action: "noop",
|
|
1627
|
+
workingId: null,
|
|
1628
|
+
promotedId: null,
|
|
1629
|
+
nextQueued: null
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
const working = existing.messages.find(
|
|
1633
|
+
(m) => m.role === "user" && m.queueStatus === "working"
|
|
1634
|
+
);
|
|
1635
|
+
if (working) {
|
|
1636
|
+
if (userHasAiReply(existing.messages, working.id)) {
|
|
1637
|
+
existing.messages = existing.messages.filter(
|
|
1638
|
+
(m) => m.provider !== "working"
|
|
1639
|
+
);
|
|
1640
|
+
working.queueStatus = null;
|
|
1641
|
+
working.queuePosition = null;
|
|
1642
|
+
const waiting2 = existing.messages.filter((m) => m.queueStatus === "queued").sort(
|
|
1643
|
+
(a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)
|
|
1644
|
+
);
|
|
1645
|
+
const next2 = waiting2[0] ?? null;
|
|
1646
|
+
if (next2) {
|
|
1647
|
+
next2.queueStatus = "working";
|
|
1648
|
+
next2.queuePosition = null;
|
|
1649
|
+
existing.messages.push({
|
|
1650
|
+
id: (0, import_node_crypto2.randomUUID)(),
|
|
1651
|
+
conversationId,
|
|
1652
|
+
role: "assistant",
|
|
1653
|
+
content: "",
|
|
1654
|
+
provider: "working",
|
|
1655
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1656
|
+
senderType: "ai",
|
|
1657
|
+
parentMessageId: next2.id
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1661
|
+
await writeConversation(filePath, existing);
|
|
1662
|
+
return {
|
|
1663
|
+
action: "completed_and_promoted",
|
|
1664
|
+
workingId: working.id,
|
|
1665
|
+
promotedId: next2?.id ?? null,
|
|
1666
|
+
nextQueued: next2 ? { id: next2.id, content: next2.content } : null
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
return {
|
|
1670
|
+
action: "redispatched_working",
|
|
1671
|
+
workingId: working.id,
|
|
1672
|
+
promotedId: null,
|
|
1673
|
+
nextQueued: null
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
const waiting = existing.messages.filter((m) => m.queueStatus === "queued").sort(
|
|
1677
|
+
(a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)
|
|
1678
|
+
);
|
|
1679
|
+
const next = waiting[0] ?? null;
|
|
1680
|
+
if (!next) {
|
|
1681
|
+
return {
|
|
1682
|
+
action: "noop",
|
|
1683
|
+
workingId: null,
|
|
1684
|
+
promotedId: null,
|
|
1685
|
+
nextQueued: null
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
next.queueStatus = "working";
|
|
1689
|
+
next.queuePosition = null;
|
|
1690
|
+
existing.messages.push({
|
|
1691
|
+
id: (0, import_node_crypto2.randomUUID)(),
|
|
1692
|
+
conversationId,
|
|
1693
|
+
role: "assistant",
|
|
1694
|
+
content: "",
|
|
1695
|
+
provider: "working",
|
|
1696
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1697
|
+
senderType: "ai",
|
|
1698
|
+
parentMessageId: next.id
|
|
1699
|
+
});
|
|
1700
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1701
|
+
await writeConversation(filePath, existing);
|
|
1702
|
+
return {
|
|
1703
|
+
action: "promoted_queued",
|
|
1704
|
+
workingId: null,
|
|
1705
|
+
promotedId: next.id,
|
|
1706
|
+
nextQueued: { id: next.id, content: next.content }
|
|
1707
|
+
};
|
|
1708
|
+
},
|
|
1709
|
+
async releaseWorkingTurn(conversationId, parentMessageId) {
|
|
1710
|
+
const filePath = fileFor(conversationId);
|
|
1711
|
+
const existing = await readConversation(filePath);
|
|
1712
|
+
if (!existing) return { nextQueued: null };
|
|
1713
|
+
existing.messages = existing.messages.filter(
|
|
1714
|
+
(m) => m.provider !== "working"
|
|
1715
|
+
);
|
|
1716
|
+
if (parentMessageId) {
|
|
1717
|
+
const parent = existing.messages.find((m) => m.id === parentMessageId);
|
|
1718
|
+
if (parent) {
|
|
1719
|
+
parent.queueStatus = null;
|
|
1720
|
+
parent.queuePosition = null;
|
|
1721
|
+
}
|
|
1722
|
+
} else {
|
|
1723
|
+
for (const row of existing.messages) {
|
|
1724
|
+
if (row.queueStatus === "working") {
|
|
1725
|
+
row.queueStatus = null;
|
|
1726
|
+
row.queuePosition = null;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
const waiting = existing.messages.filter((m) => m.queueStatus === "queued").sort(
|
|
1731
|
+
(a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)
|
|
1732
|
+
);
|
|
1733
|
+
const next = waiting[0] ?? null;
|
|
1734
|
+
if (next) {
|
|
1735
|
+
next.queueStatus = "working";
|
|
1736
|
+
next.queuePosition = null;
|
|
1737
|
+
existing.messages.push({
|
|
1738
|
+
id: (0, import_node_crypto2.randomUUID)(),
|
|
1739
|
+
conversationId,
|
|
1740
|
+
role: "assistant",
|
|
1741
|
+
content: "",
|
|
1742
|
+
provider: "working",
|
|
1743
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1744
|
+
senderType: "ai",
|
|
1745
|
+
parentMessageId: next.id
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1749
|
+
await writeConversation(filePath, existing);
|
|
1750
|
+
return next ? { nextQueued: { id: next.id, content: next.content } } : { nextQueued: null };
|
|
947
1751
|
},
|
|
948
1752
|
async clearWorkingMessages(conversationId) {
|
|
949
1753
|
const filePath = fileFor(conversationId);
|
|
@@ -965,7 +1769,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
965
1769
|
const conversations = [];
|
|
966
1770
|
for (const entry of entries) {
|
|
967
1771
|
if (!entry.endsWith(".json")) continue;
|
|
968
|
-
const filePath =
|
|
1772
|
+
const filePath = import_node_path4.default.join(baseDir, entry);
|
|
969
1773
|
const existing = await readConversation(filePath);
|
|
970
1774
|
if (!existing?.id) continue;
|
|
971
1775
|
conversations.push({
|
|
@@ -982,7 +1786,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
982
1786
|
if (events.length === 0) return;
|
|
983
1787
|
const existing = await readConversation(fileFor(conversationId));
|
|
984
1788
|
if (!existing) return;
|
|
985
|
-
const toolFile =
|
|
1789
|
+
const toolFile = import_node_path4.default.join(baseDir, `${conversationId}.tools.jsonl`);
|
|
986
1790
|
const lines = events.map(
|
|
987
1791
|
(e) => JSON.stringify({
|
|
988
1792
|
conversationId,
|
|
@@ -999,7 +1803,353 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
999
1803
|
// src/http/maintainer-pro-store.ts
|
|
1000
1804
|
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
1001
1805
|
var import_node_os = __toESM(require("os"), 1);
|
|
1002
|
-
var
|
|
1806
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
1807
|
+
|
|
1808
|
+
// src/http/synced-store.ts
|
|
1809
|
+
function userHasAiReply2(rows, workingId) {
|
|
1810
|
+
return rows.some(
|
|
1811
|
+
(row) => row.role === "assistant" && row.parentMessageId === workingId && row.provider !== "working" && row.senderType !== "developer" && Boolean(row.content?.trim())
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
function nowIso() {
|
|
1815
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1816
|
+
}
|
|
1817
|
+
function toWsUrl(baseUrl, apiKey) {
|
|
1818
|
+
const u = new URL(`${baseUrl.replace(/\/$/, "")}/api/v1/ws`);
|
|
1819
|
+
u.protocol = u.protocol === "https:" ? "wss:" : "ws:";
|
|
1820
|
+
u.searchParams.set("apiKey", apiKey);
|
|
1821
|
+
return u.toString();
|
|
1822
|
+
}
|
|
1823
|
+
function asCachedMessage(raw) {
|
|
1824
|
+
const id = typeof raw.id === "string" ? raw.id : "";
|
|
1825
|
+
const role = typeof raw.role === "string" ? raw.role : "";
|
|
1826
|
+
if (!id || !role) return null;
|
|
1827
|
+
return {
|
|
1828
|
+
id,
|
|
1829
|
+
role,
|
|
1830
|
+
content: typeof raw.content === "string" ? raw.content : "",
|
|
1831
|
+
provider: typeof raw.provider === "string" ? raw.provider : null,
|
|
1832
|
+
createdAt: typeof raw.createdAt === "string" ? raw.createdAt : void 0,
|
|
1833
|
+
attachmentPaths: Array.isArray(raw.attachmentPaths) ? raw.attachmentPaths.filter((p) => typeof p === "string") : void 0,
|
|
1834
|
+
senderType: raw.senderType === "client" || raw.senderType === "developer" || raw.senderType === "ai" ? raw.senderType : null,
|
|
1835
|
+
senderName: typeof raw.senderName === "string" ? raw.senderName : null,
|
|
1836
|
+
parentMessageId: typeof raw.parentMessageId === "string" ? raw.parentMessageId : null,
|
|
1837
|
+
queueStatus: raw.queueStatus === "working" || raw.queueStatus === "queued" ? raw.queueStatus : raw.queueStatus === null ? null : void 0,
|
|
1838
|
+
queuePosition: typeof raw.queuePosition === "number" ? raw.queuePosition : null
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
function cloneMessages(messages) {
|
|
1842
|
+
return messages.map((m) => ({ ...m }));
|
|
1843
|
+
}
|
|
1844
|
+
function createSyncedChatStore(options) {
|
|
1845
|
+
const { remote, maintainerProUrl, apiKey, onWorkingTurn } = options;
|
|
1846
|
+
const logger = options.logger ?? createLogger("ai-cli");
|
|
1847
|
+
const info = (msg) => {
|
|
1848
|
+
if (options.log) options.log(msg);
|
|
1849
|
+
else logger.info(msg);
|
|
1850
|
+
};
|
|
1851
|
+
const conversations = /* @__PURE__ */ new Map();
|
|
1852
|
+
const inflightRefresh = /* @__PURE__ */ new Map();
|
|
1853
|
+
let stopped = false;
|
|
1854
|
+
let socket = null;
|
|
1855
|
+
let reconnectAttempt = 0;
|
|
1856
|
+
let reconnectTimer = null;
|
|
1857
|
+
let pingTimer = null;
|
|
1858
|
+
const stats = () => {
|
|
1859
|
+
let messages = 0;
|
|
1860
|
+
for (const conv of conversations.values()) messages += conv.messages.length;
|
|
1861
|
+
return { conversations: conversations.size, messages };
|
|
1862
|
+
};
|
|
1863
|
+
const putConversation = (id, messages, updatedAt = nowIso()) => {
|
|
1864
|
+
conversations.set(id, { id, updatedAt, messages: cloneMessages(messages) });
|
|
1865
|
+
};
|
|
1866
|
+
const refreshConversation = (id) => {
|
|
1867
|
+
const existing = inflightRefresh.get(id);
|
|
1868
|
+
if (existing) return existing;
|
|
1869
|
+
const pending = (async () => {
|
|
1870
|
+
const messages = remote.listMessages ? await remote.listMessages(id) : [];
|
|
1871
|
+
putConversation(id, messages);
|
|
1872
|
+
return cloneMessages(messages);
|
|
1873
|
+
})().finally(() => {
|
|
1874
|
+
inflightRefresh.delete(id);
|
|
1875
|
+
});
|
|
1876
|
+
inflightRefresh.set(id, pending);
|
|
1877
|
+
return pending;
|
|
1878
|
+
};
|
|
1879
|
+
const loadSnapshot = async () => {
|
|
1880
|
+
if (remote.listSnapshot) {
|
|
1881
|
+
try {
|
|
1882
|
+
return await remote.listSnapshot();
|
|
1883
|
+
} catch {
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
if (!remote.listConversations || !remote.listMessages) return [];
|
|
1887
|
+
const summaries = await remote.listConversations();
|
|
1888
|
+
const loaded = [];
|
|
1889
|
+
for (const row of summaries) {
|
|
1890
|
+
loaded.push({
|
|
1891
|
+
id: row.id,
|
|
1892
|
+
updatedAt: row.updatedAt,
|
|
1893
|
+
messages: await remote.listMessages(row.id)
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
return loaded;
|
|
1897
|
+
};
|
|
1898
|
+
const hydrate = async () => {
|
|
1899
|
+
const snapshot = await loadSnapshot();
|
|
1900
|
+
conversations.clear();
|
|
1901
|
+
for (const row of snapshot) {
|
|
1902
|
+
putConversation(row.id, row.messages, row.updatedAt);
|
|
1903
|
+
}
|
|
1904
|
+
const next = stats();
|
|
1905
|
+
info(
|
|
1906
|
+
`cache hydrated (${next.conversations} conversations, ${next.messages} messages)`
|
|
1907
|
+
);
|
|
1908
|
+
for (const conv of conversations.values()) {
|
|
1909
|
+
const working = conv.messages.find(
|
|
1910
|
+
(row) => row.role === "user" && row.queueStatus === "working" && row.senderType !== "developer"
|
|
1911
|
+
);
|
|
1912
|
+
if (!working) continue;
|
|
1913
|
+
if (!userHasAiReply2(conv.messages, working.id)) {
|
|
1914
|
+
logger.debug(
|
|
1915
|
+
{ conversationId: conv.id, messageId: working.id },
|
|
1916
|
+
"resume working turn"
|
|
1917
|
+
);
|
|
1918
|
+
onWorkingTurn?.({ conversationId: conv.id, message: working });
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
return next;
|
|
1922
|
+
};
|
|
1923
|
+
const upsertMessage = (conversationId, message) => {
|
|
1924
|
+
const conv = conversations.get(conversationId) ?? {
|
|
1925
|
+
id: conversationId,
|
|
1926
|
+
updatedAt: nowIso(),
|
|
1927
|
+
messages: []
|
|
1928
|
+
};
|
|
1929
|
+
const index = conv.messages.findIndex((row) => row.id === message.id);
|
|
1930
|
+
const wasWorking = index >= 0 && conv.messages[index]?.queueStatus === "working";
|
|
1931
|
+
if (index >= 0) {
|
|
1932
|
+
conv.messages[index] = { ...conv.messages[index], ...message };
|
|
1933
|
+
} else {
|
|
1934
|
+
conv.messages.push(message);
|
|
1935
|
+
}
|
|
1936
|
+
conv.updatedAt = nowIso();
|
|
1937
|
+
conversations.set(conversationId, conv);
|
|
1938
|
+
const becameWorking = message.role === "user" && message.queueStatus === "working" && message.senderType !== "developer" && !wasWorking;
|
|
1939
|
+
if (becameWorking) {
|
|
1940
|
+
logger.debug(
|
|
1941
|
+
{ conversationId, messageId: message.id },
|
|
1942
|
+
"working turn"
|
|
1943
|
+
);
|
|
1944
|
+
onWorkingTurn?.({ conversationId, message });
|
|
1945
|
+
}
|
|
1946
|
+
};
|
|
1947
|
+
const deleteMessages = (conversationId, ids) => {
|
|
1948
|
+
const conv = conversations.get(conversationId);
|
|
1949
|
+
if (!conv || ids.length === 0) return;
|
|
1950
|
+
const drop = new Set(ids);
|
|
1951
|
+
conv.messages = conv.messages.filter((row) => !drop.has(row.id));
|
|
1952
|
+
conv.updatedAt = nowIso();
|
|
1953
|
+
};
|
|
1954
|
+
const applyEvent = (msg) => {
|
|
1955
|
+
const externalId = typeof msg.externalId === "string" && msg.externalId ? msg.externalId : typeof msg.conversationId === "string" ? msg.conversationId : "";
|
|
1956
|
+
if (!externalId) return;
|
|
1957
|
+
if (msg.type === "message.created") {
|
|
1958
|
+
const raw = msg.message && typeof msg.message === "object" ? msg.message : null;
|
|
1959
|
+
const cached = raw ? asCachedMessage(raw) : null;
|
|
1960
|
+
if (cached) {
|
|
1961
|
+
upsertMessage(externalId, cached);
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
void refreshConversation(externalId).catch(() => void 0);
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
if (msg.type === "message.updated") {
|
|
1968
|
+
const raw = msg.message && typeof msg.message === "object" ? msg.message : null;
|
|
1969
|
+
const cached = raw ? asCachedMessage(raw) : null;
|
|
1970
|
+
if (cached) {
|
|
1971
|
+
upsertMessage(externalId, cached);
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
void refreshConversation(externalId).catch(() => void 0);
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
if (msg.type === "message.deleted") {
|
|
1978
|
+
const ids = Array.isArray(msg.ids) ? msg.ids.filter((id) => typeof id === "string") : [];
|
|
1979
|
+
deleteMessages(externalId, ids);
|
|
1980
|
+
}
|
|
1981
|
+
};
|
|
1982
|
+
const stopSocket = () => {
|
|
1983
|
+
if (pingTimer) {
|
|
1984
|
+
clearInterval(pingTimer);
|
|
1985
|
+
pingTimer = null;
|
|
1986
|
+
}
|
|
1987
|
+
if (reconnectTimer) {
|
|
1988
|
+
clearTimeout(reconnectTimer);
|
|
1989
|
+
reconnectTimer = null;
|
|
1990
|
+
}
|
|
1991
|
+
try {
|
|
1992
|
+
socket?.close();
|
|
1993
|
+
} catch {
|
|
1994
|
+
}
|
|
1995
|
+
socket = null;
|
|
1996
|
+
};
|
|
1997
|
+
const connectRealtime = () => {
|
|
1998
|
+
if (stopped) return;
|
|
1999
|
+
const WebSocketCtor = globalThis.WebSocket;
|
|
2000
|
+
if (!WebSocketCtor) {
|
|
2001
|
+
info("WebSocket unavailable; cache will refresh after local writes only");
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
const ws = new WebSocketCtor(toWsUrl(maintainerProUrl, apiKey));
|
|
2005
|
+
socket = ws;
|
|
2006
|
+
ws.addEventListener("open", () => {
|
|
2007
|
+
reconnectAttempt = 0;
|
|
2008
|
+
logger.debug("store websocket open");
|
|
2009
|
+
if (pingTimer) clearInterval(pingTimer);
|
|
2010
|
+
pingTimer = setInterval(() => {
|
|
2011
|
+
try {
|
|
2012
|
+
if (ws.readyState === 1) ws.send(JSON.stringify({ type: "ping" }));
|
|
2013
|
+
} catch {
|
|
2014
|
+
}
|
|
2015
|
+
}, 2e4);
|
|
2016
|
+
});
|
|
2017
|
+
ws.addEventListener("message", (event) => {
|
|
2018
|
+
let payload;
|
|
2019
|
+
try {
|
|
2020
|
+
payload = JSON.parse(String(event.data));
|
|
2021
|
+
} catch {
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
if (payload.type === "hello") {
|
|
2025
|
+
const meta = payload.meta && typeof payload.meta === "object" ? payload.meta : null;
|
|
2026
|
+
const sandboxId = typeof meta?.sandboxId === "string" ? meta.sandboxId : "";
|
|
2027
|
+
if (sandboxId && ws.readyState === 1) {
|
|
2028
|
+
ws.send(
|
|
2029
|
+
JSON.stringify({ type: "subscribe", channels: [`sandbox:${sandboxId}`] })
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
logger.debug({ sandboxId }, "store websocket hello");
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
if (payload.type === "message.created" || payload.type === "message.updated" || payload.type === "message.deleted") {
|
|
2036
|
+
logger.debug({ type: payload.type }, "store websocket event");
|
|
2037
|
+
applyEvent(payload);
|
|
2038
|
+
}
|
|
2039
|
+
});
|
|
2040
|
+
ws.addEventListener("close", () => {
|
|
2041
|
+
if (socket === ws) socket = null;
|
|
2042
|
+
if (pingTimer) {
|
|
2043
|
+
clearInterval(pingTimer);
|
|
2044
|
+
pingTimer = null;
|
|
2045
|
+
}
|
|
2046
|
+
if (stopped) return;
|
|
2047
|
+
const delay = Math.min(3e4, 1e3 * 2 ** Math.min(reconnectAttempt, 5));
|
|
2048
|
+
reconnectAttempt += 1;
|
|
2049
|
+
logger.debug({ delay, reconnectAttempt }, "store websocket reconnect");
|
|
2050
|
+
reconnectTimer = setTimeout(() => {
|
|
2051
|
+
reconnectTimer = null;
|
|
2052
|
+
void hydrate().catch((err) => {
|
|
2053
|
+
logger.warn(
|
|
2054
|
+
`cache rehydrate failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2055
|
+
);
|
|
2056
|
+
}).finally(() => {
|
|
2057
|
+
connectRealtime();
|
|
2058
|
+
});
|
|
2059
|
+
}, delay);
|
|
2060
|
+
});
|
|
2061
|
+
};
|
|
2062
|
+
const ready = hydrate().catch((err) => {
|
|
2063
|
+
logger.warn(
|
|
2064
|
+
`cache hydrate failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2065
|
+
);
|
|
2066
|
+
return stats();
|
|
2067
|
+
});
|
|
2068
|
+
void ready.then(() => {
|
|
2069
|
+
if (!stopped) connectRealtime();
|
|
2070
|
+
});
|
|
2071
|
+
const store = {
|
|
2072
|
+
ready,
|
|
2073
|
+
stop: () => {
|
|
2074
|
+
stopped = true;
|
|
2075
|
+
stopSocket();
|
|
2076
|
+
},
|
|
2077
|
+
async ensureConversation(id) {
|
|
2078
|
+
await remote.ensureConversation(id);
|
|
2079
|
+
if (!conversations.has(id)) {
|
|
2080
|
+
putConversation(id, []);
|
|
2081
|
+
}
|
|
2082
|
+
},
|
|
2083
|
+
async saveMessage(input) {
|
|
2084
|
+
const saved = await remote.saveMessage(input);
|
|
2085
|
+
await refreshConversation(input.conversationId).catch(() => void 0);
|
|
2086
|
+
return saved;
|
|
2087
|
+
},
|
|
2088
|
+
async listMessages(conversationId) {
|
|
2089
|
+
await ready.catch(() => void 0);
|
|
2090
|
+
const hit = conversations.get(conversationId);
|
|
2091
|
+
if (hit) return cloneMessages(hit.messages);
|
|
2092
|
+
return refreshConversation(conversationId);
|
|
2093
|
+
},
|
|
2094
|
+
async clearWorkingMessages(conversationId) {
|
|
2095
|
+
await remote.clearWorkingMessages?.(conversationId);
|
|
2096
|
+
await refreshConversation(conversationId).catch(() => void 0);
|
|
2097
|
+
},
|
|
2098
|
+
async releaseWorkingTurn(conversationId, parentMessageId) {
|
|
2099
|
+
const result = await remote.releaseWorkingTurn?.(
|
|
2100
|
+
conversationId,
|
|
2101
|
+
parentMessageId
|
|
2102
|
+
);
|
|
2103
|
+
await refreshConversation(conversationId).catch(() => void 0);
|
|
2104
|
+
return result ?? { nextQueued: null };
|
|
2105
|
+
},
|
|
2106
|
+
async recoverQueue(conversationId) {
|
|
2107
|
+
const result = await remote.recoverQueue?.(conversationId);
|
|
2108
|
+
await refreshConversation(conversationId).catch(() => void 0);
|
|
2109
|
+
const conv = conversations.get(conversationId);
|
|
2110
|
+
if (conv) {
|
|
2111
|
+
const working = conv.messages.find(
|
|
2112
|
+
(row) => row.role === "user" && row.queueStatus === "working" && row.senderType !== "developer"
|
|
2113
|
+
);
|
|
2114
|
+
if (working && !userHasAiReply2(conv.messages, working.id)) {
|
|
2115
|
+
logger.info(
|
|
2116
|
+
{ conversationId, messageId: working.id, action: result?.action },
|
|
2117
|
+
"recoverQueue \u2192 resume working turn"
|
|
2118
|
+
);
|
|
2119
|
+
onWorkingTurn?.({ conversationId, message: working });
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
return result ?? {
|
|
2123
|
+
action: "noop",
|
|
2124
|
+
workingId: null,
|
|
2125
|
+
promotedId: null,
|
|
2126
|
+
nextQueued: null
|
|
2127
|
+
};
|
|
2128
|
+
},
|
|
2129
|
+
async listConversations() {
|
|
2130
|
+
await ready.catch(() => void 0);
|
|
2131
|
+
return [...conversations.values()].map((conv) => ({
|
|
2132
|
+
id: conv.id,
|
|
2133
|
+
updatedAt: conv.updatedAt,
|
|
2134
|
+
messages: cloneMessages(conv.messages)
|
|
2135
|
+
})).sort(
|
|
2136
|
+
(a, b) => (Date.parse(b.updatedAt) || 0) - (Date.parse(a.updatedAt) || 0)
|
|
2137
|
+
);
|
|
2138
|
+
},
|
|
2139
|
+
async saveToolEvents(conversationId, events) {
|
|
2140
|
+
await remote.saveToolEvents?.(conversationId, events);
|
|
2141
|
+
},
|
|
2142
|
+
async uploadAttachments(conversationId, attachments) {
|
|
2143
|
+
if (!remote.uploadAttachments) {
|
|
2144
|
+
return { refs: [], localPaths: [] };
|
|
2145
|
+
}
|
|
2146
|
+
return remote.uploadAttachments(conversationId, attachments);
|
|
2147
|
+
}
|
|
2148
|
+
};
|
|
2149
|
+
return store;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
// src/http/maintainer-pro-store.ts
|
|
1003
2153
|
async function mpFetch(baseUrl, apiKey, pathName, init, fetchImpl) {
|
|
1004
2154
|
const url = `${baseUrl.replace(/\/$/, "")}${pathName}`;
|
|
1005
2155
|
const res = await fetchImpl(url, {
|
|
@@ -1026,7 +2176,7 @@ function createMaintainerProStore(options) {
|
|
|
1026
2176
|
const baseUrl = options.baseUrl;
|
|
1027
2177
|
const apiKey = options.apiKey;
|
|
1028
2178
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1029
|
-
const tempDir = options.tempDir ??
|
|
2179
|
+
const tempDir = options.tempDir ?? import_node_path5.default.join(import_node_os.default.tmpdir(), "maintainer-pro-store");
|
|
1030
2180
|
const store = {
|
|
1031
2181
|
async ensureConversation(id) {
|
|
1032
2182
|
await mpFetch(
|
|
@@ -1049,17 +2199,54 @@ function createMaintainerProStore(options) {
|
|
|
1049
2199
|
{
|
|
1050
2200
|
method: "POST",
|
|
1051
2201
|
body: JSON.stringify({
|
|
2202
|
+
id: input.id,
|
|
1052
2203
|
role: input.role,
|
|
1053
2204
|
content: input.content,
|
|
1054
2205
|
provider: input.provider,
|
|
1055
2206
|
senderType: input.senderType,
|
|
1056
2207
|
senderName: input.senderName,
|
|
1057
|
-
|
|
2208
|
+
parentMessageId: input.parentMessageId ?? void 0,
|
|
2209
|
+
attachmentIds,
|
|
2210
|
+
intent: input.intent,
|
|
2211
|
+
queueStatus: input.queueStatus ?? void 0
|
|
2212
|
+
})
|
|
2213
|
+
},
|
|
2214
|
+
fetchImpl
|
|
2215
|
+
);
|
|
2216
|
+
return {
|
|
2217
|
+
...data.message,
|
|
2218
|
+
nextQueued: data.nextQueued ?? null
|
|
2219
|
+
};
|
|
2220
|
+
},
|
|
2221
|
+
async releaseWorkingTurn(conversationId, parentMessageId) {
|
|
2222
|
+
const data = await mpFetch(
|
|
2223
|
+
baseUrl,
|
|
2224
|
+
apiKey,
|
|
2225
|
+
`/api/v1/store/conversations/${encodeURIComponent(conversationId)}/complete-turn`,
|
|
2226
|
+
{
|
|
2227
|
+
method: "POST",
|
|
2228
|
+
body: JSON.stringify({
|
|
2229
|
+
parentMessageId: parentMessageId ?? void 0
|
|
1058
2230
|
})
|
|
1059
2231
|
},
|
|
1060
2232
|
fetchImpl
|
|
1061
2233
|
);
|
|
1062
|
-
return data.
|
|
2234
|
+
return { nextQueued: data.nextQueued ?? null };
|
|
2235
|
+
},
|
|
2236
|
+
async recoverQueue(conversationId) {
|
|
2237
|
+
const data = await mpFetch(
|
|
2238
|
+
baseUrl,
|
|
2239
|
+
apiKey,
|
|
2240
|
+
`/api/v1/store/conversations/${encodeURIComponent(conversationId)}/recover-queue`,
|
|
2241
|
+
{ method: "POST", body: "{}" },
|
|
2242
|
+
fetchImpl
|
|
2243
|
+
);
|
|
2244
|
+
return {
|
|
2245
|
+
action: data.action ?? "noop",
|
|
2246
|
+
workingId: data.workingId ?? null,
|
|
2247
|
+
promotedId: data.promotedId ?? null,
|
|
2248
|
+
nextQueued: data.nextQueued ?? null
|
|
2249
|
+
};
|
|
1063
2250
|
},
|
|
1064
2251
|
async clearWorkingMessages(conversationId) {
|
|
1065
2252
|
await mpFetch(
|
|
@@ -1090,6 +2277,16 @@ function createMaintainerProStore(options) {
|
|
|
1090
2277
|
);
|
|
1091
2278
|
return data.conversations ?? [];
|
|
1092
2279
|
},
|
|
2280
|
+
async listSnapshot() {
|
|
2281
|
+
const data = await mpFetch(
|
|
2282
|
+
baseUrl,
|
|
2283
|
+
apiKey,
|
|
2284
|
+
`/api/v1/store/snapshot`,
|
|
2285
|
+
{ method: "GET" },
|
|
2286
|
+
fetchImpl
|
|
2287
|
+
);
|
|
2288
|
+
return data.conversations ?? [];
|
|
2289
|
+
},
|
|
1093
2290
|
async saveToolEvents(conversationId, events) {
|
|
1094
2291
|
if (!events.length) return;
|
|
1095
2292
|
await mpFetch(
|
|
@@ -1131,7 +2328,7 @@ function createMaintainerProStore(options) {
|
|
|
1131
2328
|
attachmentIds.push(id);
|
|
1132
2329
|
refs.push(data.attachment.ref || `maintainer-pro://${id}`);
|
|
1133
2330
|
const ext = item.mimeType.includes("jpeg") || item.mimeType.includes("jpg") ? ".jpg" : item.mimeType.includes("webp") ? ".webp" : item.mimeType.includes("gif") ? ".gif" : ".png";
|
|
1134
|
-
const localPath =
|
|
2331
|
+
const localPath = import_node_path5.default.join(
|
|
1135
2332
|
tempDir,
|
|
1136
2333
|
`${conversationId}-${id}${ext}`
|
|
1137
2334
|
);
|
|
@@ -1141,13 +2338,34 @@ function createMaintainerProStore(options) {
|
|
|
1141
2338
|
return { refs, localPaths, attachmentIds };
|
|
1142
2339
|
}
|
|
1143
2340
|
};
|
|
1144
|
-
|
|
2341
|
+
if (options.sync === false) {
|
|
2342
|
+
return Object.assign(store, {
|
|
2343
|
+
ready: Promise.resolve({ conversations: 0, messages: 0 }),
|
|
2344
|
+
stop() {
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
return createSyncedChatStore({
|
|
2349
|
+
remote: store,
|
|
2350
|
+
maintainerProUrl: baseUrl,
|
|
2351
|
+
apiKey,
|
|
2352
|
+
logger: options.logger,
|
|
2353
|
+
log: options.log,
|
|
2354
|
+
onWorkingTurn: options.onWorkingTurn
|
|
2355
|
+
});
|
|
1145
2356
|
}
|
|
1146
|
-
function createMaintainerProStoreFromEnv() {
|
|
2357
|
+
function createMaintainerProStoreFromEnv(options) {
|
|
1147
2358
|
const baseUrl = process.env.MAINTAINER_PRO_URL?.trim();
|
|
1148
2359
|
const apiKey = process.env.MAINTAINER_PRO_API_KEY?.trim();
|
|
1149
2360
|
if (!baseUrl || !apiKey) return null;
|
|
1150
|
-
return createMaintainerProStore({
|
|
2361
|
+
return createMaintainerProStore({
|
|
2362
|
+
baseUrl,
|
|
2363
|
+
apiKey,
|
|
2364
|
+
logger: options?.logger,
|
|
2365
|
+
log: options?.log,
|
|
2366
|
+
sync: options?.sync,
|
|
2367
|
+
onWorkingTurn: options?.onWorkingTurn
|
|
2368
|
+
});
|
|
1151
2369
|
}
|
|
1152
2370
|
|
|
1153
2371
|
// src/workspace-inspect.ts
|
|
@@ -1257,12 +2475,17 @@ async function inspectAndRepairWorkspace(input) {
|
|
|
1257
2475
|
}
|
|
1258
2476
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1259
2477
|
0 && (module.exports = {
|
|
2478
|
+
ACCESS_IGNORE_BEGIN,
|
|
2479
|
+
ACCESS_IGNORE_END,
|
|
2480
|
+
DEFAULT_AI_IGNORE_PATHS,
|
|
2481
|
+
PROMPT_SECTION,
|
|
1260
2482
|
WORKING_PROVIDER,
|
|
1261
2483
|
buildClaudeUserPrompt,
|
|
1262
2484
|
buildConversationPrompt,
|
|
1263
2485
|
buildCursorPrompt,
|
|
1264
2486
|
buildPriorConversationsContext,
|
|
1265
2487
|
callAi,
|
|
2488
|
+
collectParentChain,
|
|
1266
2489
|
commandExists,
|
|
1267
2490
|
createAntigravityProvider,
|
|
1268
2491
|
createBuiltinProviders,
|
|
@@ -1270,17 +2493,34 @@ async function inspectAndRepairWorkspace(input) {
|
|
|
1270
2493
|
createClaudeProvider,
|
|
1271
2494
|
createCursorProvider,
|
|
1272
2495
|
createDefaultSystemPrompt,
|
|
2496
|
+
createInfoLogger,
|
|
1273
2497
|
createLocalDirectoryStore,
|
|
2498
|
+
createLogger,
|
|
1274
2499
|
createMaintainerProStore,
|
|
1275
2500
|
createMaintainerProStoreFromEnv,
|
|
2501
|
+
createSyncedChatStore,
|
|
1276
2502
|
createToolValidator,
|
|
2503
|
+
formatAccessPolicyPromptSection,
|
|
1277
2504
|
formatClientContext,
|
|
2505
|
+
formatParentChainContext,
|
|
1278
2506
|
getProviderPreference,
|
|
1279
2507
|
inspectAndRepairWorkspace,
|
|
2508
|
+
isDevMode,
|
|
2509
|
+
isIgnoredRelative,
|
|
2510
|
+
isInsideWorkspace,
|
|
2511
|
+
isPathAllowed,
|
|
2512
|
+
normalizeIgnorePaths,
|
|
2513
|
+
parentChainForTurn,
|
|
1280
2514
|
parseAiResponse,
|
|
2515
|
+
parseIgnorePathsEnv,
|
|
2516
|
+
previewText,
|
|
1281
2517
|
providerLabel,
|
|
2518
|
+
renderManagedIgnoreBlock,
|
|
1282
2519
|
resolveCliBinary,
|
|
2520
|
+
resolveIgnorePaths,
|
|
2521
|
+
resolveLogLevel,
|
|
1283
2522
|
resolveProvider,
|
|
1284
2523
|
saveChatAttachments,
|
|
1285
|
-
toNextRoute
|
|
2524
|
+
toNextRoute,
|
|
2525
|
+
upsertManagedIgnoreFile
|
|
1286
2526
|
});
|