@maintainer-pro/ai-cli 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/dist/index.cjs +1428 -74
- package/dist/index.d.cts +244 -6
- package/dist/index.d.ts +244 -6
- package/dist/index.js +1404 -73
- package/package.json +5 -3
package/dist/index.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,18 +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,
|
|
62
|
+
inspectAndRepairWorkspace: () => inspectAndRepairWorkspace,
|
|
63
|
+
isDevMode: () => isDevMode,
|
|
64
|
+
isIgnoredRelative: () => isIgnoredRelative,
|
|
65
|
+
isInsideWorkspace: () => isInsideWorkspace,
|
|
66
|
+
isPathAllowed: () => isPathAllowed,
|
|
67
|
+
normalizeIgnorePaths: () => normalizeIgnorePaths,
|
|
68
|
+
parentChainForTurn: () => parentChainForTurn,
|
|
52
69
|
parseAiResponse: () => parseAiResponse,
|
|
70
|
+
parseIgnorePathsEnv: () => parseIgnorePathsEnv,
|
|
71
|
+
previewText: () => previewText,
|
|
53
72
|
providerLabel: () => providerLabel,
|
|
73
|
+
renderManagedIgnoreBlock: () => renderManagedIgnoreBlock,
|
|
54
74
|
resolveCliBinary: () => resolveCliBinary,
|
|
75
|
+
resolveIgnorePaths: () => resolveIgnorePaths,
|
|
76
|
+
resolveLogLevel: () => resolveLogLevel,
|
|
55
77
|
resolveProvider: () => resolveProvider,
|
|
56
78
|
saveChatAttachments: () => saveChatAttachments,
|
|
57
|
-
toNextRoute: () => toNextRoute
|
|
79
|
+
toNextRoute: () => toNextRoute,
|
|
80
|
+
upsertManagedIgnoreFile: () => upsertManagedIgnoreFile
|
|
58
81
|
});
|
|
59
82
|
module.exports = __toCommonJS(index_exports);
|
|
60
83
|
|
|
@@ -62,6 +85,43 @@ module.exports = __toCommonJS(index_exports);
|
|
|
62
85
|
var import_execa2 = require("execa");
|
|
63
86
|
|
|
64
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
|
+
}
|
|
65
125
|
function buildConversationPrompt(messages) {
|
|
66
126
|
if (messages.length === 0) return "";
|
|
67
127
|
const lines = messages.map(
|
|
@@ -103,47 +163,67 @@ function splitMessages(messages) {
|
|
|
103
163
|
const history = latestUserIndex > 0 ? messages.slice(0, latestUserIndex) : [];
|
|
104
164
|
return { request, history };
|
|
105
165
|
}
|
|
106
|
-
function buildUserFacingPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext) {
|
|
166
|
+
function buildUserFacingPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext, technical, parentChainContext) {
|
|
107
167
|
const { request, history } = splitMessages(messages);
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const contextSection =
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
+
);
|
|
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.";
|
|
205
|
+
return `${systemSection}${contextSection}${priorSection}${parentSection}${historySection}${attachmentsSection}${currentSection}${closer}`;
|
|
130
206
|
}
|
|
131
|
-
function buildCursorPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext) {
|
|
207
|
+
function buildCursorPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext, technical, parentChainContext) {
|
|
132
208
|
return buildUserFacingPrompt(
|
|
133
209
|
systemPrompt,
|
|
134
210
|
messages,
|
|
135
211
|
context,
|
|
136
212
|
attachmentPaths,
|
|
137
|
-
priorConversationsContext
|
|
213
|
+
priorConversationsContext,
|
|
214
|
+
technical,
|
|
215
|
+
parentChainContext
|
|
138
216
|
);
|
|
139
217
|
}
|
|
140
|
-
function buildClaudeUserPrompt(messages, context, attachmentPaths, priorConversationsContext) {
|
|
218
|
+
function buildClaudeUserPrompt(messages, context, attachmentPaths, priorConversationsContext, technical, parentChainContext) {
|
|
141
219
|
return buildUserFacingPrompt(
|
|
142
220
|
null,
|
|
143
221
|
messages,
|
|
144
222
|
context,
|
|
145
223
|
attachmentPaths,
|
|
146
|
-
priorConversationsContext
|
|
224
|
+
priorConversationsContext,
|
|
225
|
+
technical,
|
|
226
|
+
parentChainContext
|
|
147
227
|
);
|
|
148
228
|
}
|
|
149
229
|
|
|
@@ -256,7 +336,9 @@ async function callClaudeCli(command, messages, context, options) {
|
|
|
256
336
|
messages,
|
|
257
337
|
context,
|
|
258
338
|
options.attachmentPaths,
|
|
259
|
-
options.priorConversationsContext
|
|
339
|
+
options.priorConversationsContext,
|
|
340
|
+
options.technical,
|
|
341
|
+
options.parentChainContext
|
|
260
342
|
);
|
|
261
343
|
const resolved = resolveCliCommand("claude", command);
|
|
262
344
|
const workspace = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
|
|
@@ -295,7 +377,9 @@ async function callCursorCli(command, messages, context, options) {
|
|
|
295
377
|
messages,
|
|
296
378
|
context,
|
|
297
379
|
options.attachmentPaths,
|
|
298
|
-
options.priorConversationsContext
|
|
380
|
+
options.priorConversationsContext,
|
|
381
|
+
options.technical,
|
|
382
|
+
options.parentChainContext
|
|
299
383
|
);
|
|
300
384
|
const resolved = resolveCliCommand("cursor", command);
|
|
301
385
|
const workspace = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
|
|
@@ -336,7 +420,9 @@ async function callAntigravityCli(command, messages, context, options) {
|
|
|
336
420
|
messages,
|
|
337
421
|
context,
|
|
338
422
|
options.attachmentPaths,
|
|
339
|
-
options.priorConversationsContext
|
|
423
|
+
options.priorConversationsContext,
|
|
424
|
+
options.technical,
|
|
425
|
+
options.parentChainContext
|
|
340
426
|
);
|
|
341
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).
|
|
342
428
|
|
|
@@ -377,8 +463,8 @@ async function resolveCommandPath(command) {
|
|
|
377
463
|
}
|
|
378
464
|
const result = await (0, import_execa2.execa)("which", [command], { reject: false });
|
|
379
465
|
if (result.exitCode !== 0) return null;
|
|
380
|
-
const
|
|
381
|
-
return
|
|
466
|
+
const path6 = result.stdout.trim().split(/\r?\n/)[0]?.trim();
|
|
467
|
+
return path6 || null;
|
|
382
468
|
} catch {
|
|
383
469
|
return null;
|
|
384
470
|
}
|
|
@@ -464,6 +550,51 @@ async function callAi(messages, context, options) {
|
|
|
464
550
|
return provider.call(messages, context, options);
|
|
465
551
|
}
|
|
466
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
|
+
|
|
467
598
|
// src/system-prompt.ts
|
|
468
599
|
function createDefaultSystemPrompt(input) {
|
|
469
600
|
const files = input.relevantFilesHint ? `Key UI files (use internally only; never name them in your reply):
|
|
@@ -474,6 +605,14 @@ Only when the user wants to change live in-app data (not source code), return a
|
|
|
474
605
|
${input.runtimeToolsHint}
|
|
475
606
|
|
|
476
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.` : "";
|
|
477
616
|
return `You are a helpful product assistant for an app the user is looking at right now.
|
|
478
617
|
|
|
479
618
|
${input.productDescription}
|
|
@@ -494,6 +633,7 @@ Behind the scenes you can edit this repository and update live app data, but the
|
|
|
494
633
|
## When to edit the codebase
|
|
495
634
|
Edit source files when the user asks to change labels, layout, styling, copy, or behavior in the app.
|
|
496
635
|
${files}
|
|
636
|
+
${access}
|
|
497
637
|
|
|
498
638
|
${tools}
|
|
499
639
|
|
|
@@ -501,9 +641,191 @@ Do not refuse UI/label changes \u2014 implement them in source files.
|
|
|
501
641
|
Answer the user's latest request directly \u2014 never reply with a generic greeting.`;
|
|
502
642
|
}
|
|
503
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
|
+
|
|
504
826
|
// src/http/attachments.ts
|
|
505
827
|
var import_promises = __toESM(require("fs/promises"), 1);
|
|
506
|
-
var
|
|
828
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
507
829
|
var MAX_ATTACHMENTS = 5;
|
|
508
830
|
var MAX_BYTES = 4 * 1024 * 1024;
|
|
509
831
|
var ALLOWED = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif"]);
|
|
@@ -523,7 +845,7 @@ function extForMime(mime) {
|
|
|
523
845
|
async function saveChatAttachments(attachments, workspaceDir) {
|
|
524
846
|
if (!attachments?.length) return [];
|
|
525
847
|
const selected = attachments.slice(0, MAX_ATTACHMENTS);
|
|
526
|
-
const dir =
|
|
848
|
+
const dir = import_node_path3.default.join(workspaceDir, ".maintainer-pro", "uploads");
|
|
527
849
|
await import_promises.default.mkdir(dir, { recursive: true });
|
|
528
850
|
const paths = [];
|
|
529
851
|
const stamp = Date.now();
|
|
@@ -542,7 +864,10 @@ async function saveChatAttachments(attachments, workspaceDir) {
|
|
|
542
864
|
}
|
|
543
865
|
const safeBase = (item.name || `screenshot-${i + 1}`).replace(/[^\w.\-]+/g, "_").slice(0, 64);
|
|
544
866
|
const fileName = `${stamp}-${i + 1}-${safeBase}${extForMime(mime)}`;
|
|
545
|
-
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
|
+
}
|
|
546
871
|
await import_promises.default.writeFile(filePath, buffer);
|
|
547
872
|
paths.push(filePath);
|
|
548
873
|
}
|
|
@@ -629,7 +954,7 @@ function createToolValidator(schemas) {
|
|
|
629
954
|
|
|
630
955
|
// src/http/handler.ts
|
|
631
956
|
var WORKING_PROVIDER = "working";
|
|
632
|
-
async function setWorkingMessage(db, conversationId) {
|
|
957
|
+
async function setWorkingMessage(db, conversationId, parentMessageId) {
|
|
633
958
|
await db.ensureConversation(conversationId);
|
|
634
959
|
await db.clearWorkingMessages?.(conversationId);
|
|
635
960
|
await db.saveMessage({
|
|
@@ -637,9 +962,26 @@ async function setWorkingMessage(db, conversationId) {
|
|
|
637
962
|
role: "assistant",
|
|
638
963
|
content: "",
|
|
639
964
|
provider: WORKING_PROVIDER,
|
|
640
|
-
senderType: "ai"
|
|
965
|
+
senderType: "ai",
|
|
966
|
+
parentMessageId: parentMessageId ?? void 0
|
|
641
967
|
});
|
|
642
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
|
+
}
|
|
643
985
|
var SHARED_CONVERSATION_ID = "shared";
|
|
644
986
|
var conversationTurnSeq = /* @__PURE__ */ new Map();
|
|
645
987
|
function beginConversationTurn(conversationId) {
|
|
@@ -674,6 +1016,7 @@ async function resolveSharedConversationId(request, options, bodyId) {
|
|
|
674
1016
|
return SHARED_CONVERSATION_ID;
|
|
675
1017
|
}
|
|
676
1018
|
function createChatHandler(options) {
|
|
1019
|
+
const logger = options.logger ?? createLogger("ai-cli:chat");
|
|
677
1020
|
const validate = options.tools ? createToolValidator(options.tools) : null;
|
|
678
1021
|
const workspaceDir = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
|
|
679
1022
|
const baseCallOptions = {
|
|
@@ -682,6 +1025,7 @@ function createChatHandler(options) {
|
|
|
682
1025
|
providerPreference: options.providerPreference,
|
|
683
1026
|
providers: options.providers
|
|
684
1027
|
};
|
|
1028
|
+
logger.debug({ workspaceDir }, "chat handler ready");
|
|
685
1029
|
return {
|
|
686
1030
|
async GET(request) {
|
|
687
1031
|
try {
|
|
@@ -697,6 +1041,14 @@ function createChatHandler(options) {
|
|
|
697
1041
|
if (options.db?.listMessages) {
|
|
698
1042
|
messages = await options.db.listMessages(conversationId);
|
|
699
1043
|
}
|
|
1044
|
+
logger.debug(
|
|
1045
|
+
{
|
|
1046
|
+
conversationId,
|
|
1047
|
+
provider: provider.id,
|
|
1048
|
+
messageCount: messages?.length ?? 0
|
|
1049
|
+
},
|
|
1050
|
+
"GET /api/chat"
|
|
1051
|
+
);
|
|
700
1052
|
return Response.json({
|
|
701
1053
|
provider: provider.id,
|
|
702
1054
|
providerLabel: provider.label || providerLabel(provider.id),
|
|
@@ -704,6 +1056,7 @@ function createChatHandler(options) {
|
|
|
704
1056
|
messages: messages ?? []
|
|
705
1057
|
});
|
|
706
1058
|
} catch (err) {
|
|
1059
|
+
logger.error({ err }, "GET /api/chat failed");
|
|
707
1060
|
const message = err instanceof Error ? err.message : "No AI CLI provider available";
|
|
708
1061
|
return Response.json(
|
|
709
1062
|
{ error: message, provider: null },
|
|
@@ -713,7 +1066,9 @@ function createChatHandler(options) {
|
|
|
713
1066
|
},
|
|
714
1067
|
async POST(request) {
|
|
715
1068
|
let trackedConversationId;
|
|
1069
|
+
let trackedParentUserMessageId;
|
|
716
1070
|
let trackedTurn = 0;
|
|
1071
|
+
const startedAt = Date.now();
|
|
717
1072
|
try {
|
|
718
1073
|
const body = await request.json();
|
|
719
1074
|
const conversationId = await resolveSharedConversationId(
|
|
@@ -722,6 +1077,19 @@ function createChatHandler(options) {
|
|
|
722
1077
|
body.conversationId
|
|
723
1078
|
);
|
|
724
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
|
+
);
|
|
725
1093
|
if (body.type === "working-clear") {
|
|
726
1094
|
if (!conversationId) {
|
|
727
1095
|
return Response.json(
|
|
@@ -746,11 +1114,54 @@ function createChatHandler(options) {
|
|
|
746
1114
|
await options.db.ensureConversation(conversationId);
|
|
747
1115
|
await options.db.saveMessage({
|
|
748
1116
|
conversationId,
|
|
1117
|
+
id: body.messageId,
|
|
749
1118
|
role: "assistant",
|
|
750
1119
|
content,
|
|
751
1120
|
provider: "developer",
|
|
752
1121
|
senderType: "developer",
|
|
753
|
-
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
|
|
754
1165
|
});
|
|
755
1166
|
}
|
|
756
1167
|
return Response.json({ ok: true, conversationId });
|
|
@@ -781,27 +1192,65 @@ function createChatHandler(options) {
|
|
|
781
1192
|
const turn = beginConversationTurn(conversationId ?? SHARED_CONVERSATION_ID);
|
|
782
1193
|
trackedTurn = turn;
|
|
783
1194
|
const signal = request.signal;
|
|
1195
|
+
let parentUserMessageId = typeof body.userMessageId === "string" && body.userMessageId ? body.userMessageId : void 0;
|
|
1196
|
+
trackedParentUserMessageId = parentUserMessageId;
|
|
784
1197
|
if (options.db && conversationId) {
|
|
785
1198
|
const latest = messages[messages.length - 1];
|
|
786
1199
|
const persistContent = body.userMessage?.trim() || (latest?.role === "user" ? latest.content : "");
|
|
787
|
-
if (persistContent) {
|
|
1200
|
+
if (persistContent && !body.skipPersistUser && !parentUserMessageId) {
|
|
788
1201
|
await options.db.ensureConversation(conversationId);
|
|
789
|
-
await options.db.saveMessage({
|
|
1202
|
+
const saved = await options.db.saveMessage({
|
|
790
1203
|
conversationId,
|
|
791
1204
|
role: "user",
|
|
792
1205
|
content: persistContent,
|
|
793
1206
|
attachmentPaths: persistAttachmentPaths.length > 0 ? persistAttachmentPaths : void 0,
|
|
794
1207
|
senderType: body.senderType === "client" ? "client" : void 0,
|
|
795
|
-
senderName: body.senderName?.trim() || void 0
|
|
1208
|
+
senderName: body.senderName?.trim() || void 0,
|
|
1209
|
+
intent: "run"
|
|
796
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
|
+
);
|
|
797
1220
|
}
|
|
798
|
-
await setWorkingMessage(options.db, conversationId);
|
|
799
1221
|
}
|
|
800
1222
|
const latestUser = [...messages].reverse().find((m) => m.role === "user");
|
|
801
1223
|
const priorConversationsContext = options.db ? await buildPriorConversationsContext(options.db, {
|
|
802
1224
|
excludeConversationId: conversationId,
|
|
803
1225
|
currentRequest: latestUser?.content ?? ""
|
|
804
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
|
+
}
|
|
805
1254
|
if (!isActiveConversationTurn(
|
|
806
1255
|
conversationId ?? SHARED_CONVERSATION_ID,
|
|
807
1256
|
turn
|
|
@@ -812,23 +1261,58 @@ function createChatHandler(options) {
|
|
|
812
1261
|
});
|
|
813
1262
|
}
|
|
814
1263
|
if (signal.aborted) {
|
|
815
|
-
|
|
816
|
-
|
|
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
|
+
}
|
|
817
1274
|
}
|
|
818
1275
|
return Response.json({
|
|
819
1276
|
superseded: true,
|
|
820
1277
|
conversationId: conversationId ?? null
|
|
821
1278
|
});
|
|
822
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();
|
|
823
1294
|
const aiResponse = await callAi(messages, context, {
|
|
824
1295
|
...baseCallOptions,
|
|
825
1296
|
attachmentPaths,
|
|
826
|
-
priorConversationsContext: priorConversationsContext || void 0
|
|
1297
|
+
priorConversationsContext: priorConversationsContext || void 0,
|
|
1298
|
+
parentChainContext: parentChainContext || void 0
|
|
827
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
|
+
);
|
|
828
1311
|
if (!isActiveConversationTurn(
|
|
829
1312
|
conversationId ?? SHARED_CONVERSATION_ID,
|
|
830
1313
|
turn
|
|
831
1314
|
)) {
|
|
1315
|
+
logger.debug({ conversationId, turn }, "superseded after AI");
|
|
832
1316
|
return Response.json({
|
|
833
1317
|
superseded: true,
|
|
834
1318
|
conversationId: conversationId ?? null
|
|
@@ -836,38 +1320,108 @@ function createChatHandler(options) {
|
|
|
836
1320
|
}
|
|
837
1321
|
const validatedToolCalls = validate ? aiResponse.toolCalls.filter((tc) => validate(tc).valid) : aiResponse.toolCalls;
|
|
838
1322
|
if (options.onToolCalls && validatedToolCalls.length > 0) {
|
|
1323
|
+
logger.debug(
|
|
1324
|
+
{ conversationId, count: validatedToolCalls.length },
|
|
1325
|
+
"applying tool calls"
|
|
1326
|
+
);
|
|
839
1327
|
await options.onToolCalls(validatedToolCalls, context);
|
|
840
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;
|
|
841
1332
|
if (options.db && conversationId) {
|
|
842
1333
|
await options.db.ensureConversation(conversationId);
|
|
843
|
-
await options.db.
|
|
844
|
-
await options.db.saveMessage({
|
|
1334
|
+
const savedReply = await options.db.saveMessage({
|
|
845
1335
|
conversationId,
|
|
1336
|
+
id: assistantMessageId,
|
|
846
1337
|
role: "assistant",
|
|
847
1338
|
content: aiResponse.text,
|
|
848
1339
|
provider: aiResponse.provider,
|
|
849
|
-
senderType: "ai"
|
|
1340
|
+
senderType: "ai",
|
|
1341
|
+
parentMessageId: parentUserMessageId
|
|
850
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
|
+
}
|
|
851
1374
|
if (validatedToolCalls.length > 0) {
|
|
852
1375
|
await options.db.saveToolEvents?.(
|
|
853
1376
|
conversationId,
|
|
854
1377
|
validatedToolCalls
|
|
855
1378
|
);
|
|
856
1379
|
}
|
|
1380
|
+
logger.debug(
|
|
1381
|
+
{
|
|
1382
|
+
conversationId,
|
|
1383
|
+
replyId,
|
|
1384
|
+
nextQueued: nextQueued?.id
|
|
1385
|
+
},
|
|
1386
|
+
"persisted AI reply"
|
|
1387
|
+
);
|
|
857
1388
|
}
|
|
1389
|
+
logger.debug(
|
|
1390
|
+
{ conversationId, turn, ms: Date.now() - startedAt },
|
|
1391
|
+
"POST /api/chat done"
|
|
1392
|
+
);
|
|
858
1393
|
return Response.json({
|
|
859
1394
|
text: aiResponse.text,
|
|
860
1395
|
toolCalls: validatedToolCalls,
|
|
861
1396
|
provider: aiResponse.provider,
|
|
862
1397
|
providerLabel: providerLabel(aiResponse.provider),
|
|
863
|
-
conversationId: conversationId ?? null
|
|
1398
|
+
conversationId: conversationId ?? null,
|
|
1399
|
+
messageId: replyId,
|
|
1400
|
+
nextQueued,
|
|
1401
|
+
parentMessageId: parentUserMessageId ?? null
|
|
864
1402
|
});
|
|
865
1403
|
} catch (err) {
|
|
866
1404
|
console.error("Chat API error:", err);
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
+
}
|
|
871
1425
|
}
|
|
872
1426
|
return Response.json(
|
|
873
1427
|
{
|
|
@@ -888,8 +1442,13 @@ function toNextRoute(handlers) {
|
|
|
888
1442
|
|
|
889
1443
|
// src/http/local-store.ts
|
|
890
1444
|
var import_promises2 = __toESM(require("fs/promises"), 1);
|
|
891
|
-
var
|
|
892
|
-
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
|
+
}
|
|
893
1452
|
async function readConversation(filePath) {
|
|
894
1453
|
try {
|
|
895
1454
|
const raw = await import_promises2.default.readFile(filePath, "utf8");
|
|
@@ -900,11 +1459,11 @@ async function readConversation(filePath) {
|
|
|
900
1459
|
}
|
|
901
1460
|
}
|
|
902
1461
|
async function writeConversation(filePath, data) {
|
|
903
|
-
await import_promises2.default.mkdir(
|
|
1462
|
+
await import_promises2.default.mkdir(import_node_path4.default.dirname(filePath), { recursive: true });
|
|
904
1463
|
await import_promises2.default.writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
905
1464
|
}
|
|
906
1465
|
function createLocalDirectoryStore(baseDir) {
|
|
907
|
-
const fileFor = (id) =>
|
|
1466
|
+
const fileFor = (id) => import_node_path4.default.join(baseDir, `${id}.json`);
|
|
908
1467
|
return {
|
|
909
1468
|
async ensureConversation(id) {
|
|
910
1469
|
const existing = await readConversation(fileFor(id));
|
|
@@ -922,21 +1481,273 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
922
1481
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
923
1482
|
messages: []
|
|
924
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
|
+
}
|
|
925
1578
|
const message = {
|
|
926
|
-
id: (0,
|
|
1579
|
+
id: input.id ?? (0, import_node_crypto2.randomUUID)(),
|
|
927
1580
|
conversationId: input.conversationId,
|
|
928
1581
|
role: input.role,
|
|
929
1582
|
content: input.content,
|
|
930
1583
|
provider: input.provider ?? null,
|
|
931
|
-
createdAt:
|
|
1584
|
+
createdAt: now,
|
|
932
1585
|
attachmentPaths: input.attachmentPaths,
|
|
933
1586
|
senderType: input.senderType ?? null,
|
|
934
|
-
senderName: input.senderName ?? null
|
|
1587
|
+
senderName: input.senderName ?? null,
|
|
1588
|
+
parentMessageId: input.parentMessageId ?? null
|
|
935
1589
|
};
|
|
936
1590
|
existing.messages.push(message);
|
|
937
|
-
|
|
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;
|
|
1615
|
+
await writeConversation(filePath, existing);
|
|
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();
|
|
938
1749
|
await writeConversation(filePath, existing);
|
|
939
|
-
return
|
|
1750
|
+
return next ? { nextQueued: { id: next.id, content: next.content } } : { nextQueued: null };
|
|
940
1751
|
},
|
|
941
1752
|
async clearWorkingMessages(conversationId) {
|
|
942
1753
|
const filePath = fileFor(conversationId);
|
|
@@ -958,7 +1769,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
958
1769
|
const conversations = [];
|
|
959
1770
|
for (const entry of entries) {
|
|
960
1771
|
if (!entry.endsWith(".json")) continue;
|
|
961
|
-
const filePath =
|
|
1772
|
+
const filePath = import_node_path4.default.join(baseDir, entry);
|
|
962
1773
|
const existing = await readConversation(filePath);
|
|
963
1774
|
if (!existing?.id) continue;
|
|
964
1775
|
conversations.push({
|
|
@@ -975,7 +1786,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
975
1786
|
if (events.length === 0) return;
|
|
976
1787
|
const existing = await readConversation(fileFor(conversationId));
|
|
977
1788
|
if (!existing) return;
|
|
978
|
-
const toolFile =
|
|
1789
|
+
const toolFile = import_node_path4.default.join(baseDir, `${conversationId}.tools.jsonl`);
|
|
979
1790
|
const lines = events.map(
|
|
980
1791
|
(e) => JSON.stringify({
|
|
981
1792
|
conversationId,
|
|
@@ -992,7 +1803,353 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
992
1803
|
// src/http/maintainer-pro-store.ts
|
|
993
1804
|
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
994
1805
|
var import_node_os = __toESM(require("os"), 1);
|
|
995
|
-
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
|
|
996
2153
|
async function mpFetch(baseUrl, apiKey, pathName, init, fetchImpl) {
|
|
997
2154
|
const url = `${baseUrl.replace(/\/$/, "")}${pathName}`;
|
|
998
2155
|
const res = await fetchImpl(url, {
|
|
@@ -1019,7 +2176,7 @@ function createMaintainerProStore(options) {
|
|
|
1019
2176
|
const baseUrl = options.baseUrl;
|
|
1020
2177
|
const apiKey = options.apiKey;
|
|
1021
2178
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1022
|
-
const tempDir = options.tempDir ??
|
|
2179
|
+
const tempDir = options.tempDir ?? import_node_path5.default.join(import_node_os.default.tmpdir(), "maintainer-pro-store");
|
|
1023
2180
|
const store = {
|
|
1024
2181
|
async ensureConversation(id) {
|
|
1025
2182
|
await mpFetch(
|
|
@@ -1042,17 +2199,54 @@ function createMaintainerProStore(options) {
|
|
|
1042
2199
|
{
|
|
1043
2200
|
method: "POST",
|
|
1044
2201
|
body: JSON.stringify({
|
|
2202
|
+
id: input.id,
|
|
1045
2203
|
role: input.role,
|
|
1046
2204
|
content: input.content,
|
|
1047
2205
|
provider: input.provider,
|
|
1048
2206
|
senderType: input.senderType,
|
|
1049
2207
|
senderName: input.senderName,
|
|
1050
|
-
|
|
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
|
|
1051
2230
|
})
|
|
1052
2231
|
},
|
|
1053
2232
|
fetchImpl
|
|
1054
2233
|
);
|
|
1055
|
-
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
|
+
};
|
|
1056
2250
|
},
|
|
1057
2251
|
async clearWorkingMessages(conversationId) {
|
|
1058
2252
|
await mpFetch(
|
|
@@ -1083,6 +2277,16 @@ function createMaintainerProStore(options) {
|
|
|
1083
2277
|
);
|
|
1084
2278
|
return data.conversations ?? [];
|
|
1085
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
|
+
},
|
|
1086
2290
|
async saveToolEvents(conversationId, events) {
|
|
1087
2291
|
if (!events.length) return;
|
|
1088
2292
|
await mpFetch(
|
|
@@ -1124,7 +2328,7 @@ function createMaintainerProStore(options) {
|
|
|
1124
2328
|
attachmentIds.push(id);
|
|
1125
2329
|
refs.push(data.attachment.ref || `maintainer-pro://${id}`);
|
|
1126
2330
|
const ext = item.mimeType.includes("jpeg") || item.mimeType.includes("jpg") ? ".jpg" : item.mimeType.includes("webp") ? ".webp" : item.mimeType.includes("gif") ? ".gif" : ".png";
|
|
1127
|
-
const localPath =
|
|
2331
|
+
const localPath = import_node_path5.default.join(
|
|
1128
2332
|
tempDir,
|
|
1129
2333
|
`${conversationId}-${id}${ext}`
|
|
1130
2334
|
);
|
|
@@ -1134,22 +2338,154 @@ function createMaintainerProStore(options) {
|
|
|
1134
2338
|
return { refs, localPaths, attachmentIds };
|
|
1135
2339
|
}
|
|
1136
2340
|
};
|
|
1137
|
-
|
|
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
|
+
});
|
|
1138
2356
|
}
|
|
1139
|
-
function createMaintainerProStoreFromEnv() {
|
|
2357
|
+
function createMaintainerProStoreFromEnv(options) {
|
|
1140
2358
|
const baseUrl = process.env.MAINTAINER_PRO_URL?.trim();
|
|
1141
2359
|
const apiKey = process.env.MAINTAINER_PRO_API_KEY?.trim();
|
|
1142
2360
|
if (!baseUrl || !apiKey) return null;
|
|
1143
|
-
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
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
// src/workspace-inspect.ts
|
|
2372
|
+
var import_zod = require("zod");
|
|
2373
|
+
var inspectJsonSchema = import_zod.z.object({
|
|
2374
|
+
kind: import_zod.z.enum(["next", "vite", "html", "empty", "other"]).optional(),
|
|
2375
|
+
name: import_zod.z.string().max(200).optional(),
|
|
2376
|
+
summary: import_zod.z.string().max(800).optional(),
|
|
2377
|
+
scripts: import_zod.z.object({
|
|
2378
|
+
ui: import_zod.z.string().min(1).max(64).optional(),
|
|
2379
|
+
backend: import_zod.z.string().min(1).max(64).optional(),
|
|
2380
|
+
app: import_zod.z.string().min(1).max(64).optional()
|
|
2381
|
+
}).optional(),
|
|
2382
|
+
ports: import_zod.z.object({
|
|
2383
|
+
ui: import_zod.z.number().int().min(1).max(65535).optional(),
|
|
2384
|
+
backend: import_zod.z.number().int().min(1).max(65535).optional(),
|
|
2385
|
+
app: import_zod.z.number().int().min(1).max(65535).optional()
|
|
2386
|
+
}).optional(),
|
|
2387
|
+
issues: import_zod.z.array(import_zod.z.string().max(500)).max(20).optional(),
|
|
2388
|
+
fixes: import_zod.z.array(import_zod.z.string().max(500)).max(20).optional(),
|
|
2389
|
+
ready: import_zod.z.boolean().optional()
|
|
2390
|
+
});
|
|
2391
|
+
function extractInspectJson(text) {
|
|
2392
|
+
const fence = text.match(/```json\s*([\s\S]*?)```/i);
|
|
2393
|
+
const raw = fence?.[1]?.trim() || text.match(/\{[\s\S]*\}/)?.[0];
|
|
2394
|
+
if (!raw) return null;
|
|
2395
|
+
try {
|
|
2396
|
+
const parsed = inspectJsonSchema.safeParse(JSON.parse(raw));
|
|
2397
|
+
return parsed.success ? parsed.data : null;
|
|
2398
|
+
} catch {
|
|
2399
|
+
return null;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
function setupSystemPrompt(input) {
|
|
2403
|
+
return `You are Maintainer Pro's local setup assistant on the developer's machine.
|
|
2404
|
+
|
|
2405
|
+
Workspace: ${input.workspaceDir}
|
|
2406
|
+
App name: ${input.appName || "the app"}
|
|
2407
|
+
|
|
2408
|
+
Inspect this repository (package.json, README, lockfiles, existing env files, how the UI and API start).
|
|
2409
|
+
You MAY edit files to fix setup problems that would block local run or the chat widget (env keys, widget mount, missing config).
|
|
2410
|
+
Do NOT start long-lived dev servers, tunnels, or install global tools.
|
|
2411
|
+
Do NOT run \`npm run dev\` or similar.
|
|
2412
|
+
|
|
2413
|
+
Script names you report MUST already exist in package.json. Omit chat/ai-server scripts.
|
|
2414
|
+
|
|
2415
|
+
End with a short human summary AND one \`\`\`json fence with:
|
|
2416
|
+
|
|
2417
|
+
{
|
|
2418
|
+
"kind": "next" | "vite" | "html" | "empty" | "other",
|
|
2419
|
+
"name": "package or folder name",
|
|
2420
|
+
"summary": "one sentence about this project",
|
|
2421
|
+
"scripts": { "ui": "dev:client", "backend": "dev:server" },
|
|
2422
|
+
"ports": { "ui": 5173, "backend": 4100 },
|
|
2423
|
+
"issues": ["remaining problems"],
|
|
2424
|
+
"fixes": ["what you changed"],
|
|
2425
|
+
"ready": true
|
|
2426
|
+
}`;
|
|
2427
|
+
}
|
|
2428
|
+
function setupUserMessage(input) {
|
|
2429
|
+
if (input.problem?.trim()) {
|
|
2430
|
+
return `The local bridge failed to keep this project running:
|
|
2431
|
+
|
|
2432
|
+
${input.problem.trim()}
|
|
2433
|
+
|
|
2434
|
+
${input.extraContext ? `${input.extraContext.trim()}
|
|
2435
|
+
|
|
2436
|
+
` : ""}Inspect the repo, fix the cause if you can, and return the JSON report.`;
|
|
2437
|
+
}
|
|
2438
|
+
return `${input.extraContext ? `${input.extraContext.trim()}
|
|
2439
|
+
|
|
2440
|
+
` : ""}Inspect this project, fix any setup gaps you can, and return the JSON report.`;
|
|
2441
|
+
}
|
|
2442
|
+
async function inspectAndRepairWorkspace(input) {
|
|
2443
|
+
const workspaceDir = input.workspaceDir;
|
|
2444
|
+
const provider = await resolveProvider({ preference: "auto" });
|
|
2445
|
+
const response = await callAi(
|
|
2446
|
+
[{ role: "user", content: setupUserMessage(input) }],
|
|
2447
|
+
{
|
|
2448
|
+
route: "/local-setup",
|
|
2449
|
+
pageTitle: input.appName || "Local setup",
|
|
2450
|
+
relevantFiles: ["package.json", "README.md", ".env", ".env.example"],
|
|
2451
|
+
data: {
|
|
2452
|
+
workspaceDir,
|
|
2453
|
+
problem: input.problem || null
|
|
2454
|
+
}
|
|
2455
|
+
},
|
|
2456
|
+
{
|
|
2457
|
+
systemPrompt: setupSystemPrompt(input),
|
|
2458
|
+
workspaceDir,
|
|
2459
|
+
technical: true
|
|
2460
|
+
}
|
|
2461
|
+
);
|
|
2462
|
+
const parsed = extractInspectJson(response.text);
|
|
2463
|
+
return {
|
|
2464
|
+
kind: parsed?.kind ?? "other",
|
|
2465
|
+
name: parsed?.name?.trim() || input.appName || "",
|
|
2466
|
+
summary: parsed?.summary?.trim() || response.text.replace(/```json[\s\S]*```/i, "").trim().slice(0, 400),
|
|
2467
|
+
scripts: parsed?.scripts ?? {},
|
|
2468
|
+
ports: parsed?.ports ?? {},
|
|
2469
|
+
issues: parsed?.issues ?? [],
|
|
2470
|
+
fixes: parsed?.fixes ?? [],
|
|
2471
|
+
ready: parsed?.ready ?? parsed?.issues?.length === 0,
|
|
2472
|
+
provider: response.provider || provider.id,
|
|
2473
|
+
rawText: response.text
|
|
2474
|
+
};
|
|
1144
2475
|
}
|
|
1145
2476
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1146
2477
|
0 && (module.exports = {
|
|
2478
|
+
ACCESS_IGNORE_BEGIN,
|
|
2479
|
+
ACCESS_IGNORE_END,
|
|
2480
|
+
DEFAULT_AI_IGNORE_PATHS,
|
|
2481
|
+
PROMPT_SECTION,
|
|
1147
2482
|
WORKING_PROVIDER,
|
|
1148
2483
|
buildClaudeUserPrompt,
|
|
1149
2484
|
buildConversationPrompt,
|
|
1150
2485
|
buildCursorPrompt,
|
|
1151
2486
|
buildPriorConversationsContext,
|
|
1152
2487
|
callAi,
|
|
2488
|
+
collectParentChain,
|
|
1153
2489
|
commandExists,
|
|
1154
2490
|
createAntigravityProvider,
|
|
1155
2491
|
createBuiltinProviders,
|
|
@@ -1157,16 +2493,34 @@ function createMaintainerProStoreFromEnv() {
|
|
|
1157
2493
|
createClaudeProvider,
|
|
1158
2494
|
createCursorProvider,
|
|
1159
2495
|
createDefaultSystemPrompt,
|
|
2496
|
+
createInfoLogger,
|
|
1160
2497
|
createLocalDirectoryStore,
|
|
2498
|
+
createLogger,
|
|
1161
2499
|
createMaintainerProStore,
|
|
1162
2500
|
createMaintainerProStoreFromEnv,
|
|
2501
|
+
createSyncedChatStore,
|
|
1163
2502
|
createToolValidator,
|
|
2503
|
+
formatAccessPolicyPromptSection,
|
|
1164
2504
|
formatClientContext,
|
|
2505
|
+
formatParentChainContext,
|
|
1165
2506
|
getProviderPreference,
|
|
2507
|
+
inspectAndRepairWorkspace,
|
|
2508
|
+
isDevMode,
|
|
2509
|
+
isIgnoredRelative,
|
|
2510
|
+
isInsideWorkspace,
|
|
2511
|
+
isPathAllowed,
|
|
2512
|
+
normalizeIgnorePaths,
|
|
2513
|
+
parentChainForTurn,
|
|
1166
2514
|
parseAiResponse,
|
|
2515
|
+
parseIgnorePathsEnv,
|
|
2516
|
+
previewText,
|
|
1167
2517
|
providerLabel,
|
|
2518
|
+
renderManagedIgnoreBlock,
|
|
1168
2519
|
resolveCliBinary,
|
|
2520
|
+
resolveIgnorePaths,
|
|
2521
|
+
resolveLogLevel,
|
|
1169
2522
|
resolveProvider,
|
|
1170
2523
|
saveChatAttachments,
|
|
1171
|
-
toNextRoute
|
|
2524
|
+
toNextRoute,
|
|
2525
|
+
upsertManagedIgnoreFile
|
|
1172
2526
|
});
|