@maintainer-pro/ai-cli 0.1.4 → 0.1.6
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 +2397 -115
- package/dist/index.d.cts +382 -11
- package/dist/index.d.ts +382 -11
- package/dist/index.js +2350 -121
- package/package.json +5 -3
package/dist/index.cjs
CHANGED
|
@@ -30,12 +30,22 @@ 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
|
+
AI_SERVER_APP_ID: () => AI_SERVER_APP_ID,
|
|
36
|
+
AI_SERVER_DEFAULT_PORT: () => AI_SERVER_DEFAULT_PORT,
|
|
37
|
+
COLLABORATER_DIR: () => COLLABORATER_DIR,
|
|
38
|
+
DEFAULT_AI_IGNORE_PATHS: () => DEFAULT_AI_IGNORE_PATHS,
|
|
39
|
+
HOST_APPS_FILE: () => HOST_APPS_FILE,
|
|
40
|
+
MAINTAINER_PRO_HOME_DIR: () => MAINTAINER_PRO_HOME_DIR,
|
|
41
|
+
PROMPT_SECTION: () => PROMPT_SECTION,
|
|
33
42
|
WORKING_PROVIDER: () => WORKING_PROVIDER,
|
|
34
43
|
buildClaudeUserPrompt: () => buildClaudeUserPrompt,
|
|
35
44
|
buildConversationPrompt: () => buildConversationPrompt,
|
|
36
45
|
buildCursorPrompt: () => buildCursorPrompt,
|
|
37
46
|
buildPriorConversationsContext: () => buildPriorConversationsContext,
|
|
38
47
|
callAi: () => callAi,
|
|
48
|
+
collectParentChain: () => collectParentChain,
|
|
39
49
|
commandExists: () => commandExists,
|
|
40
50
|
createAntigravityProvider: () => createAntigravityProvider,
|
|
41
51
|
createBuiltinProviders: () => createBuiltinProviders,
|
|
@@ -43,19 +53,62 @@ __export(index_exports, {
|
|
|
43
53
|
createClaudeProvider: () => createClaudeProvider,
|
|
44
54
|
createCursorProvider: () => createCursorProvider,
|
|
45
55
|
createDefaultSystemPrompt: () => createDefaultSystemPrompt,
|
|
56
|
+
createInfoLogger: () => createInfoLogger,
|
|
46
57
|
createLocalDirectoryStore: () => createLocalDirectoryStore,
|
|
58
|
+
createLogger: () => createLogger,
|
|
47
59
|
createMaintainerProStore: () => createMaintainerProStore,
|
|
48
60
|
createMaintainerProStoreFromEnv: () => createMaintainerProStoreFromEnv,
|
|
61
|
+
createSyncedChatStore: () => createSyncedChatStore,
|
|
49
62
|
createToolValidator: () => createToolValidator,
|
|
63
|
+
defaultAiServerApp: () => defaultAiServerApp,
|
|
64
|
+
detectHostAppsFromFiles: () => detectHostAppsFromFiles,
|
|
65
|
+
ensureAiServerApp: () => ensureAiServerApp,
|
|
66
|
+
ensureProjectDataDir: () => ensureProjectDataDir,
|
|
67
|
+
ensureSingleHost: () => ensureSingleHost,
|
|
68
|
+
formatAccessPolicyPromptSection: () => formatAccessPolicyPromptSection,
|
|
50
69
|
formatClientContext: () => formatClientContext,
|
|
70
|
+
formatParentChainContext: () => formatParentChainContext,
|
|
51
71
|
getProviderPreference: () => getProviderPreference,
|
|
72
|
+
hostAppsCachePath: () => hostAppsCachePath2,
|
|
73
|
+
hostAppsFingerprint: () => hostAppsFingerprint,
|
|
52
74
|
inspectAndRepairWorkspace: () => inspectAndRepairWorkspace,
|
|
75
|
+
inspectConfigOnly: () => inspectConfigOnly,
|
|
76
|
+
isDevMode: () => isDevMode,
|
|
77
|
+
isIgnoredRelative: () => isIgnoredRelative,
|
|
78
|
+
isInsideWorkspace: () => isInsideWorkspace,
|
|
79
|
+
isPathAllowed: () => isPathAllowed,
|
|
80
|
+
maintainerProHome: () => maintainerProHome,
|
|
81
|
+
mergeDesiredHostApps: () => mergeDesiredHostApps,
|
|
82
|
+
normalizeEnvMaps: () => normalizeEnvMaps,
|
|
83
|
+
normalizeHostApp: () => normalizeHostApp,
|
|
84
|
+
normalizeHostApps: () => normalizeHostApps,
|
|
85
|
+
normalizeIgnorePaths: () => normalizeIgnorePaths,
|
|
86
|
+
parentChainForTurn: () => parentChainForTurn,
|
|
53
87
|
parseAiResponse: () => parseAiResponse,
|
|
88
|
+
parseIgnorePathsEnv: () => parseIgnorePathsEnv,
|
|
89
|
+
parsePort: () => parsePort,
|
|
90
|
+
previewText: () => previewText,
|
|
91
|
+
projectHostAppsPath: () => hostAppsCachePath,
|
|
92
|
+
projectIdForFolder: () => projectIdForFolder,
|
|
93
|
+
projectUploadsDir: () => projectUploadsDir,
|
|
94
|
+
proposeHostAppsFromConfig: () => proposeHostAppsFromConfig,
|
|
54
95
|
providerLabel: () => providerLabel,
|
|
96
|
+
readCollaboraterApps: () => readCollaboraterApps,
|
|
97
|
+
readHostAppsCache: () => readHostAppsCache,
|
|
98
|
+
readProjectEnvLayers: () => readProjectEnvLayers,
|
|
99
|
+
renderManagedIgnoreBlock: () => renderManagedIgnoreBlock,
|
|
55
100
|
resolveCliBinary: () => resolveCliBinary,
|
|
101
|
+
resolveHostApps: () => resolveHostApps,
|
|
102
|
+
resolveIgnorePaths: () => resolveIgnorePaths,
|
|
103
|
+
resolveLogLevel: () => resolveLogLevel,
|
|
104
|
+
resolveProjectDataDir: () => resolveProjectDataDir,
|
|
56
105
|
resolveProvider: () => resolveProvider,
|
|
106
|
+
sanitizeProjectId: () => sanitizeProjectId,
|
|
57
107
|
saveChatAttachments: () => saveChatAttachments,
|
|
58
|
-
toNextRoute: () => toNextRoute
|
|
108
|
+
toNextRoute: () => toNextRoute,
|
|
109
|
+
upsertManagedIgnoreFile: () => upsertManagedIgnoreFile,
|
|
110
|
+
writeCollaboraterApps: () => writeCollaboraterApps,
|
|
111
|
+
writeHostAppsCache: () => writeHostAppsCache
|
|
59
112
|
});
|
|
60
113
|
module.exports = __toCommonJS(index_exports);
|
|
61
114
|
|
|
@@ -63,6 +116,43 @@ module.exports = __toCommonJS(index_exports);
|
|
|
63
116
|
var import_execa2 = require("execa");
|
|
64
117
|
|
|
65
118
|
// src/prompt.ts
|
|
119
|
+
var PROMPT_SECTION = {
|
|
120
|
+
system: { begin: "<<<SYSTEM_BEGIN>>>", end: "<<<SYSTEM_END>>>" },
|
|
121
|
+
clientContext: {
|
|
122
|
+
begin: "<<<CLIENT_CONTEXT_BEGIN>>>",
|
|
123
|
+
end: "<<<CLIENT_CONTEXT_END>>>"
|
|
124
|
+
},
|
|
125
|
+
priorConversations: {
|
|
126
|
+
begin: "<<<PRIOR_CONVERSATIONS_BEGIN>>>",
|
|
127
|
+
end: "<<<PRIOR_CONVERSATIONS_END>>>"
|
|
128
|
+
},
|
|
129
|
+
parentChain: {
|
|
130
|
+
begin: "<<<PARENT_CHAIN_BEGIN>>>",
|
|
131
|
+
end: "<<<PARENT_CHAIN_END>>>"
|
|
132
|
+
},
|
|
133
|
+
history: {
|
|
134
|
+
begin: "<<<CONVERSATION_HISTORY_BEGIN>>>",
|
|
135
|
+
end: "<<<CONVERSATION_HISTORY_END>>>"
|
|
136
|
+
},
|
|
137
|
+
attachments: {
|
|
138
|
+
begin: "<<<ATTACHMENTS_BEGIN>>>",
|
|
139
|
+
end: "<<<ATTACHMENTS_END>>>"
|
|
140
|
+
},
|
|
141
|
+
currentRequest: {
|
|
142
|
+
begin: "<<<CURRENT_REQUEST_BEGIN>>>",
|
|
143
|
+
end: "<<<CURRENT_REQUEST_END>>>"
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
function wrapSection(tag, body, title) {
|
|
147
|
+
const trimmed = body.trim();
|
|
148
|
+
if (!trimmed) return "";
|
|
149
|
+
const header = title ? `${tag.begin} ${title}` : tag.begin;
|
|
150
|
+
return `${header}
|
|
151
|
+
${trimmed}
|
|
152
|
+
${tag.end}
|
|
153
|
+
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
66
156
|
function buildConversationPrompt(messages) {
|
|
67
157
|
if (messages.length === 0) return "";
|
|
68
158
|
const lines = messages.map(
|
|
@@ -104,50 +194,67 @@ function splitMessages(messages) {
|
|
|
104
194
|
const history = latestUserIndex > 0 ? messages.slice(0, latestUserIndex) : [];
|
|
105
195
|
return { request, history };
|
|
106
196
|
}
|
|
107
|
-
function buildUserFacingPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext, technical) {
|
|
197
|
+
function buildUserFacingPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext, technical, parentChainContext) {
|
|
108
198
|
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
|
-
|
|
199
|
+
const systemSection = wrapSection(
|
|
200
|
+
PROMPT_SECTION.system,
|
|
201
|
+
systemPrompt ?? "",
|
|
202
|
+
"instructions"
|
|
203
|
+
);
|
|
204
|
+
const contextSection = wrapSection(
|
|
205
|
+
PROMPT_SECTION.clientContext,
|
|
206
|
+
formatClientContext(context),
|
|
207
|
+
"live UI snapshot"
|
|
208
|
+
);
|
|
209
|
+
const priorSection = wrapSection(
|
|
210
|
+
PROMPT_SECTION.priorConversations,
|
|
211
|
+
priorConversationsContext ?? "",
|
|
212
|
+
"other chats (secondary)"
|
|
213
|
+
);
|
|
214
|
+
const parentSection = wrapSection(
|
|
215
|
+
PROMPT_SECTION.parentChain,
|
|
216
|
+
parentChainContext ?? "",
|
|
217
|
+
"reply-to parents \u2014 primary thread context"
|
|
218
|
+
);
|
|
219
|
+
const historySection = wrapSection(
|
|
220
|
+
PROMPT_SECTION.history,
|
|
221
|
+
history.length > 0 ? buildConversationPrompt(history) : "",
|
|
222
|
+
"earlier turns in this conversation"
|
|
223
|
+
);
|
|
224
|
+
const attachmentsSection = wrapSection(
|
|
225
|
+
PROMPT_SECTION.attachments,
|
|
226
|
+
attachmentPaths && attachmentPaths.length > 0 ? `Screenshots / images attached by the user (open and inspect these image files; they are stored under ~/.maintainer-pro for this project, not in the host app, and you are allowed to read them):
|
|
227
|
+
${attachmentPaths.map((p) => `- ${p}`).join("\n")}` : "",
|
|
228
|
+
"user attachments"
|
|
229
|
+
);
|
|
230
|
+
const currentSection = wrapSection(
|
|
231
|
+
PROMPT_SECTION.currentRequest,
|
|
232
|
+
request || "(See the attached screenshot(s).)",
|
|
233
|
+
"answer this message"
|
|
234
|
+
);
|
|
127
235
|
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}`;
|
|
236
|
+
return `${systemSection}${contextSection}${priorSection}${parentSection}${historySection}${attachmentsSection}${currentSection}${closer}`;
|
|
132
237
|
}
|
|
133
|
-
function buildCursorPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext, technical) {
|
|
238
|
+
function buildCursorPrompt(systemPrompt, messages, context, attachmentPaths, priorConversationsContext, technical, parentChainContext) {
|
|
134
239
|
return buildUserFacingPrompt(
|
|
135
240
|
systemPrompt,
|
|
136
241
|
messages,
|
|
137
242
|
context,
|
|
138
243
|
attachmentPaths,
|
|
139
244
|
priorConversationsContext,
|
|
140
|
-
technical
|
|
245
|
+
technical,
|
|
246
|
+
parentChainContext
|
|
141
247
|
);
|
|
142
248
|
}
|
|
143
|
-
function buildClaudeUserPrompt(messages, context, attachmentPaths, priorConversationsContext, technical) {
|
|
249
|
+
function buildClaudeUserPrompt(messages, context, attachmentPaths, priorConversationsContext, technical, parentChainContext) {
|
|
144
250
|
return buildUserFacingPrompt(
|
|
145
251
|
null,
|
|
146
252
|
messages,
|
|
147
253
|
context,
|
|
148
254
|
attachmentPaths,
|
|
149
255
|
priorConversationsContext,
|
|
150
|
-
technical
|
|
256
|
+
technical,
|
|
257
|
+
parentChainContext
|
|
151
258
|
);
|
|
152
259
|
}
|
|
153
260
|
|
|
@@ -261,7 +368,8 @@ async function callClaudeCli(command, messages, context, options) {
|
|
|
261
368
|
context,
|
|
262
369
|
options.attachmentPaths,
|
|
263
370
|
options.priorConversationsContext,
|
|
264
|
-
options.technical
|
|
371
|
+
options.technical,
|
|
372
|
+
options.parentChainContext
|
|
265
373
|
);
|
|
266
374
|
const resolved = resolveCliCommand("claude", command);
|
|
267
375
|
const workspace = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
|
|
@@ -301,7 +409,8 @@ async function callCursorCli(command, messages, context, options) {
|
|
|
301
409
|
context,
|
|
302
410
|
options.attachmentPaths,
|
|
303
411
|
options.priorConversationsContext,
|
|
304
|
-
options.technical
|
|
412
|
+
options.technical,
|
|
413
|
+
options.parentChainContext
|
|
305
414
|
);
|
|
306
415
|
const resolved = resolveCliCommand("cursor", command);
|
|
307
416
|
const workspace = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
|
|
@@ -343,7 +452,8 @@ async function callAntigravityCli(command, messages, context, options) {
|
|
|
343
452
|
context,
|
|
344
453
|
options.attachmentPaths,
|
|
345
454
|
options.priorConversationsContext,
|
|
346
|
-
options.technical
|
|
455
|
+
options.technical,
|
|
456
|
+
options.parentChainContext
|
|
347
457
|
);
|
|
348
458
|
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
459
|
|
|
@@ -384,8 +494,8 @@ async function resolveCommandPath(command) {
|
|
|
384
494
|
}
|
|
385
495
|
const result = await (0, import_execa2.execa)("which", [command], { reject: false });
|
|
386
496
|
if (result.exitCode !== 0) return null;
|
|
387
|
-
const
|
|
388
|
-
return
|
|
497
|
+
const path9 = result.stdout.trim().split(/\r?\n/)[0]?.trim();
|
|
498
|
+
return path9 || null;
|
|
389
499
|
} catch {
|
|
390
500
|
return null;
|
|
391
501
|
}
|
|
@@ -471,6 +581,51 @@ async function callAi(messages, context, options) {
|
|
|
471
581
|
return provider.call(messages, context, options);
|
|
472
582
|
}
|
|
473
583
|
|
|
584
|
+
// src/parent-chain.ts
|
|
585
|
+
function collectParentChain(rows, startId) {
|
|
586
|
+
if (!startId) return [];
|
|
587
|
+
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
588
|
+
const chain = [];
|
|
589
|
+
const seen = /* @__PURE__ */ new Set();
|
|
590
|
+
let current = byId.get(startId);
|
|
591
|
+
while (current && !seen.has(current.id)) {
|
|
592
|
+
seen.add(current.id);
|
|
593
|
+
const content = String(current.content ?? "").trim();
|
|
594
|
+
if (content && current.provider !== "working") {
|
|
595
|
+
const role = current.role === "assistant" || current.role === "system" ? current.role : "user";
|
|
596
|
+
chain.unshift({
|
|
597
|
+
id: current.id,
|
|
598
|
+
role,
|
|
599
|
+
content,
|
|
600
|
+
...current.senderName?.trim() ? { senderName: current.senderName.trim() } : {}
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
const nextId = current.parentMessageId?.trim();
|
|
604
|
+
current = nextId ? byId.get(nextId) : void 0;
|
|
605
|
+
}
|
|
606
|
+
return chain;
|
|
607
|
+
}
|
|
608
|
+
function parentChainForTurn(rows, currentMessageId, fallbackParentId) {
|
|
609
|
+
if (!currentMessageId && !fallbackParentId) return [];
|
|
610
|
+
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
611
|
+
const current = currentMessageId ? byId.get(currentMessageId) : void 0;
|
|
612
|
+
const replyToId = current?.parentMessageId && String(current.parentMessageId).trim() || fallbackParentId && String(fallbackParentId).trim() || null;
|
|
613
|
+
return collectParentChain(rows, replyToId);
|
|
614
|
+
}
|
|
615
|
+
function formatParentChainContext(chain) {
|
|
616
|
+
if (chain.length === 0) return "";
|
|
617
|
+
const lines = [
|
|
618
|
+
"context: this message has parents hierarchy linked with it",
|
|
619
|
+
"Prefer this chain over unrelated queued or later messages when interpreting the current request.",
|
|
620
|
+
""
|
|
621
|
+
];
|
|
622
|
+
chain.forEach((entry, index) => {
|
|
623
|
+
const who = entry.senderName || (entry.role === "assistant" ? "Assistant" : entry.role === "system" ? "System" : "Human");
|
|
624
|
+
lines.push(`${index + 1}. [${who}] ${entry.content}`);
|
|
625
|
+
});
|
|
626
|
+
return lines.join("\n");
|
|
627
|
+
}
|
|
628
|
+
|
|
474
629
|
// src/system-prompt.ts
|
|
475
630
|
function createDefaultSystemPrompt(input) {
|
|
476
631
|
const files = input.relevantFilesHint ? `Key UI files (use internally only; never name them in your reply):
|
|
@@ -481,6 +636,14 @@ Only when the user wants to change live in-app data (not source code), return a
|
|
|
481
636
|
${input.runtimeToolsHint}
|
|
482
637
|
|
|
483
638
|
Only use those runtime tools for live state. Never invent runtime tools.` : "";
|
|
639
|
+
const access = input.ignorePaths !== void 0 ? `
|
|
640
|
+
|
|
641
|
+
## File access boundaries (mandatory)
|
|
642
|
+
- You may only read or edit files inside the current project workspace directory.
|
|
643
|
+
- Never read, write, move, or delete files outside that workspace.
|
|
644
|
+
- Never touch paths that match this ignore list (relative to the workspace):
|
|
645
|
+
${(input.ignorePaths ?? []).filter((p) => !p.startsWith("!")).length ? (input.ignorePaths ?? []).filter((p) => !p.startsWith("!")).map((p) => `- ${p}`).join("\n") : "- (none beyond staying inside the workspace)"}
|
|
646
|
+
- 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
647
|
return `You are a helpful product assistant for an app the user is looking at right now.
|
|
485
648
|
|
|
486
649
|
${input.productDescription}
|
|
@@ -501,6 +664,7 @@ Behind the scenes you can edit this repository and update live app data, but the
|
|
|
501
664
|
## When to edit the codebase
|
|
502
665
|
Edit source files when the user asks to change labels, layout, styling, copy, or behavior in the app.
|
|
503
666
|
${files}
|
|
667
|
+
${access}
|
|
504
668
|
|
|
505
669
|
${tools}
|
|
506
670
|
|
|
@@ -508,14 +672,257 @@ Do not refuse UI/label changes \u2014 implement them in source files.
|
|
|
508
672
|
Answer the user's latest request directly \u2014 never reply with a generic greeting.`;
|
|
509
673
|
}
|
|
510
674
|
|
|
675
|
+
// src/access-policy.ts
|
|
676
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
677
|
+
var DEFAULT_AI_IGNORE_PATHS = [
|
|
678
|
+
".env",
|
|
679
|
+
".env.*",
|
|
680
|
+
"**/.env",
|
|
681
|
+
"**/.env.*"
|
|
682
|
+
];
|
|
683
|
+
function parseIgnorePathsEnv(raw) {
|
|
684
|
+
if (!raw?.trim()) return [];
|
|
685
|
+
const trimmed = raw.trim();
|
|
686
|
+
try {
|
|
687
|
+
const parsed = JSON.parse(trimmed);
|
|
688
|
+
if (Array.isArray(parsed)) {
|
|
689
|
+
return normalizeIgnorePaths(parsed.map(String));
|
|
690
|
+
}
|
|
691
|
+
} catch {
|
|
692
|
+
}
|
|
693
|
+
return normalizeIgnorePaths(trimmed.split(/[\n,]+/));
|
|
694
|
+
}
|
|
695
|
+
function normalizeIgnorePaths(paths) {
|
|
696
|
+
const out = [];
|
|
697
|
+
const seen = /* @__PURE__ */ new Set();
|
|
698
|
+
for (const raw of paths) {
|
|
699
|
+
const p = raw.trim().replace(/\\/g, "/");
|
|
700
|
+
if (!p || p.startsWith("/") || p.includes("..")) continue;
|
|
701
|
+
if (seen.has(p)) continue;
|
|
702
|
+
seen.add(p);
|
|
703
|
+
out.push(p);
|
|
704
|
+
}
|
|
705
|
+
return out;
|
|
706
|
+
}
|
|
707
|
+
function resolveIgnorePaths(partnerPaths) {
|
|
708
|
+
return normalizeIgnorePaths([
|
|
709
|
+
...DEFAULT_AI_IGNORE_PATHS,
|
|
710
|
+
...partnerPaths
|
|
711
|
+
]);
|
|
712
|
+
}
|
|
713
|
+
function isInsideWorkspace(workspaceDir, targetPath) {
|
|
714
|
+
const root = import_node_path2.default.resolve(workspaceDir);
|
|
715
|
+
const target = import_node_path2.default.resolve(targetPath);
|
|
716
|
+
const rel = import_node_path2.default.relative(root, target);
|
|
717
|
+
return rel === "" || !rel.startsWith("..") && !import_node_path2.default.isAbsolute(rel);
|
|
718
|
+
}
|
|
719
|
+
function globToRegExp(pattern) {
|
|
720
|
+
let p = pattern.replace(/\\/g, "/");
|
|
721
|
+
if (p.endsWith("/")) p = `${p}**`;
|
|
722
|
+
let i = 0;
|
|
723
|
+
let out = "^";
|
|
724
|
+
while (i < p.length) {
|
|
725
|
+
if (p.startsWith("**/", i)) {
|
|
726
|
+
out += "(?:.*/)?";
|
|
727
|
+
i += 3;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (p[i] === "*" && p[i + 1] !== "*") {
|
|
731
|
+
out += "[^/]*";
|
|
732
|
+
i += 1;
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
if (p.startsWith("**", i)) {
|
|
736
|
+
out += ".*";
|
|
737
|
+
i += 2;
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
if (p[i] === "?") {
|
|
741
|
+
out += "[^/]";
|
|
742
|
+
i += 1;
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
const ch = p[i];
|
|
746
|
+
if (/[.+^${}()|[\]\\]/.test(ch)) out += `\\${ch}`;
|
|
747
|
+
else out += ch;
|
|
748
|
+
i += 1;
|
|
749
|
+
}
|
|
750
|
+
out += "$";
|
|
751
|
+
return new RegExp(out, "i");
|
|
752
|
+
}
|
|
753
|
+
function isIgnoredRelative(relativePath, patterns) {
|
|
754
|
+
const rel = relativePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
755
|
+
if (!rel) return false;
|
|
756
|
+
return patterns.some((pattern) => {
|
|
757
|
+
const re = globToRegExp(pattern);
|
|
758
|
+
if (re.test(rel)) return true;
|
|
759
|
+
if (!pattern.includes("*") && !pattern.endsWith("/")) {
|
|
760
|
+
return rel === pattern || rel.startsWith(`${pattern}/`);
|
|
761
|
+
}
|
|
762
|
+
return false;
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
function isPathAllowed(workspaceDir, targetPath, ignorePaths) {
|
|
766
|
+
if (!isInsideWorkspace(workspaceDir, targetPath)) return false;
|
|
767
|
+
const rel = import_node_path2.default.relative(import_node_path2.default.resolve(workspaceDir), import_node_path2.default.resolve(targetPath));
|
|
768
|
+
return !isIgnoredRelative(rel, ignorePaths);
|
|
769
|
+
}
|
|
770
|
+
function formatAccessPolicyPromptSection(input) {
|
|
771
|
+
const ignores = input.ignorePaths.filter((p) => !p.startsWith("!")).length ? input.ignorePaths.filter((p) => !p.startsWith("!")).map((p) => `- ${p}`).join("\n") : "- (none beyond staying inside the workspace)";
|
|
772
|
+
return `## File access boundaries (mandatory)
|
|
773
|
+
- You may only read or edit files inside the current project workspace directory.
|
|
774
|
+
- Never write, move, or delete files outside that workspace.
|
|
775
|
+
- You may read screenshot files listed in the attachments section (those live in the Maintainer Pro data folder, not in the project).
|
|
776
|
+
- Never touch paths that match this ignore list (relative to the workspace):
|
|
777
|
+
${ignores}
|
|
778
|
+
- 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.`;
|
|
779
|
+
}
|
|
780
|
+
var ACCESS_IGNORE_BEGIN = "# maintainer-pro:access-begin";
|
|
781
|
+
var ACCESS_IGNORE_END = "# maintainer-pro:access-end";
|
|
782
|
+
function renderManagedIgnoreBlock(ignorePaths) {
|
|
783
|
+
const lines = [
|
|
784
|
+
ACCESS_IGNORE_BEGIN,
|
|
785
|
+
"# Managed by Maintainer Pro \u2014 do not edit this block by hand.",
|
|
786
|
+
...ignorePaths,
|
|
787
|
+
ACCESS_IGNORE_END
|
|
788
|
+
];
|
|
789
|
+
return `${lines.join("\n")}
|
|
790
|
+
`;
|
|
791
|
+
}
|
|
792
|
+
function upsertManagedIgnoreFile(existing, ignorePaths) {
|
|
793
|
+
const block = renderManagedIgnoreBlock(ignorePaths);
|
|
794
|
+
const begin = existing.indexOf(ACCESS_IGNORE_BEGIN);
|
|
795
|
+
const end = existing.indexOf(ACCESS_IGNORE_END);
|
|
796
|
+
if (begin >= 0 && end > begin) {
|
|
797
|
+
const afterEnd = end + ACCESS_IGNORE_END.length;
|
|
798
|
+
const before = existing.slice(0, begin).replace(/\s+$/, "");
|
|
799
|
+
let after = existing.slice(afterEnd).replace(/^\r?\n/, "");
|
|
800
|
+
const parts = [before, block.trimEnd(), after.trimStart()].filter(
|
|
801
|
+
(s) => s.length > 0
|
|
802
|
+
);
|
|
803
|
+
return `${parts.join("\n\n")}
|
|
804
|
+
`;
|
|
805
|
+
}
|
|
806
|
+
const trimmed = existing.replace(/\s+$/, "");
|
|
807
|
+
return trimmed ? `${trimmed}
|
|
808
|
+
|
|
809
|
+
${block}` : block;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// src/http/handler.ts
|
|
813
|
+
var import_node_crypto2 = require("crypto");
|
|
814
|
+
|
|
815
|
+
// src/log.ts
|
|
816
|
+
var import_pino = __toESM(require("pino"), 1);
|
|
817
|
+
function isDevMode() {
|
|
818
|
+
if (process.env.AI_DEV === "1" || process.env.AI_DEV === "true") return true;
|
|
819
|
+
const env = (process.env.NODE_ENV || process.env.AI_ENV || "").trim().toLowerCase();
|
|
820
|
+
return env === "development" || env === "dev" || env === "test";
|
|
821
|
+
}
|
|
822
|
+
function resolveLogLevel() {
|
|
823
|
+
const explicit = process.env.LOG_LEVEL?.trim() || process.env.AI_LOG_LEVEL?.trim();
|
|
824
|
+
if (explicit) return explicit;
|
|
825
|
+
return isDevMode() ? "debug" : "info";
|
|
826
|
+
}
|
|
827
|
+
function createLogger(name) {
|
|
828
|
+
const level = resolveLogLevel();
|
|
829
|
+
const pretty = process.env.LOG_PRETTY !== "0" && typeof process.stdout?.isTTY === "boolean" && process.stdout.isTTY;
|
|
830
|
+
if (pretty) {
|
|
831
|
+
return (0, import_pino.default)({
|
|
832
|
+
name,
|
|
833
|
+
level,
|
|
834
|
+
transport: {
|
|
835
|
+
target: "pino-pretty",
|
|
836
|
+
options: {
|
|
837
|
+
colorize: true,
|
|
838
|
+
translateTime: "HH:MM:ss",
|
|
839
|
+
ignore: "pid,hostname"
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
return (0, import_pino.default)({ name, level });
|
|
845
|
+
}
|
|
846
|
+
function createInfoLogger(name) {
|
|
847
|
+
const log = createLogger(name);
|
|
848
|
+
return (msg) => {
|
|
849
|
+
log.info(msg);
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
function previewText(text, max = 160) {
|
|
853
|
+
const t = String(text ?? "").replace(/\s+/g, " ").trim();
|
|
854
|
+
if (!t) return "";
|
|
855
|
+
return t.length <= max ? t : `${t.slice(0, max)}\u2026`;
|
|
856
|
+
}
|
|
857
|
+
|
|
511
858
|
// src/http/attachments.ts
|
|
512
859
|
var import_promises = __toESM(require("fs/promises"), 1);
|
|
513
|
-
var
|
|
860
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
861
|
+
|
|
862
|
+
// src/project-data.ts
|
|
863
|
+
var import_node_crypto = require("crypto");
|
|
864
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
865
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
866
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
867
|
+
var MAINTAINER_PRO_HOME_DIR = ".maintainer-pro";
|
|
868
|
+
var HOST_APPS_FILE = "apps.json";
|
|
869
|
+
function maintainerProHome() {
|
|
870
|
+
const override = process.env.MAINTAINER_PRO_HOME?.trim();
|
|
871
|
+
if (override) return import_node_path3.default.resolve(override);
|
|
872
|
+
return import_node_path3.default.join(import_node_os.default.homedir(), MAINTAINER_PRO_HOME_DIR);
|
|
873
|
+
}
|
|
874
|
+
function projectIdForFolder(folder) {
|
|
875
|
+
const resolved = import_node_path3.default.resolve(folder || "");
|
|
876
|
+
const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
877
|
+
return (0, import_node_crypto.createHash)("sha256").update(key).digest("hex").slice(0, 16);
|
|
878
|
+
}
|
|
879
|
+
function sanitizeProjectId(id) {
|
|
880
|
+
return String(id || "").trim().replace(/[^\w.-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 80);
|
|
881
|
+
}
|
|
882
|
+
function resolveProjectDataDir(input) {
|
|
883
|
+
const override = String(input.dataDir || "").trim();
|
|
884
|
+
if (override) return import_node_path3.default.resolve(override);
|
|
885
|
+
const sandbox = sanitizeProjectId(String(input.sandboxId || ""));
|
|
886
|
+
const id = sandbox || projectIdForFolder(input.workspaceDir);
|
|
887
|
+
return import_node_path3.default.join(maintainerProHome(), "projects", id);
|
|
888
|
+
}
|
|
889
|
+
function projectUploadsDir(input) {
|
|
890
|
+
return import_node_path3.default.join(resolveProjectDataDir(input), "uploads");
|
|
891
|
+
}
|
|
892
|
+
function hostAppsCachePath(input) {
|
|
893
|
+
return import_node_path3.default.join(resolveProjectDataDir(input), HOST_APPS_FILE);
|
|
894
|
+
}
|
|
895
|
+
function ensureProjectDataDir(input) {
|
|
896
|
+
const dir = resolveProjectDataDir(input);
|
|
897
|
+
import_node_fs2.default.mkdirSync(dir, { recursive: true });
|
|
898
|
+
import_node_fs2.default.mkdirSync(import_node_path3.default.join(dir, "uploads"), { recursive: true });
|
|
899
|
+
try {
|
|
900
|
+
import_node_fs2.default.writeFileSync(
|
|
901
|
+
import_node_path3.default.join(dir, "workspace.json"),
|
|
902
|
+
`${JSON.stringify(
|
|
903
|
+
{
|
|
904
|
+
workspaceDir: import_node_path3.default.resolve(input.workspaceDir || ""),
|
|
905
|
+
sandboxId: input.sandboxId ? String(input.sandboxId) : null,
|
|
906
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
907
|
+
},
|
|
908
|
+
null,
|
|
909
|
+
2
|
|
910
|
+
)}
|
|
911
|
+
`,
|
|
912
|
+
"utf8"
|
|
913
|
+
);
|
|
914
|
+
} catch {
|
|
915
|
+
}
|
|
916
|
+
return dir;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// src/http/attachments.ts
|
|
514
920
|
var MAX_ATTACHMENTS = 5;
|
|
515
921
|
var MAX_BYTES = 4 * 1024 * 1024;
|
|
516
922
|
var ALLOWED = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif"]);
|
|
517
923
|
function extForMime(mime) {
|
|
518
|
-
|
|
924
|
+
const normalized = mime.split(";")[0]?.trim().toLowerCase() || "";
|
|
925
|
+
switch (normalized) {
|
|
519
926
|
case "image/jpeg":
|
|
520
927
|
case "image/jpg":
|
|
521
928
|
return ".jpg";
|
|
@@ -527,11 +934,31 @@ function extForMime(mime) {
|
|
|
527
934
|
return ".png";
|
|
528
935
|
}
|
|
529
936
|
}
|
|
530
|
-
|
|
937
|
+
function attachmentIdFromRef(ref) {
|
|
938
|
+
const trimmed = String(ref || "").trim();
|
|
939
|
+
if (!trimmed) return null;
|
|
940
|
+
const m = /^maintainer-pro:\/\/(.+)$/i.exec(trimmed);
|
|
941
|
+
if (m?.[1]) return m[1];
|
|
942
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
943
|
+
trimmed
|
|
944
|
+
)) {
|
|
945
|
+
return trimmed;
|
|
946
|
+
}
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
function uploadsRoot(input) {
|
|
950
|
+
ensureProjectDataDir(input);
|
|
951
|
+
return projectUploadsDir(input);
|
|
952
|
+
}
|
|
953
|
+
async function saveChatAttachments(attachments, workspaceDir, extra) {
|
|
531
954
|
if (!attachments?.length) return [];
|
|
532
955
|
const selected = attachments.slice(0, MAX_ATTACHMENTS);
|
|
533
|
-
const
|
|
534
|
-
|
|
956
|
+
const loc = {
|
|
957
|
+
workspaceDir,
|
|
958
|
+
dataDir: extra?.dataDir,
|
|
959
|
+
sandboxId: extra?.sandboxId
|
|
960
|
+
};
|
|
961
|
+
const dir = uploadsRoot(loc);
|
|
535
962
|
const paths = [];
|
|
536
963
|
const stamp = Date.now();
|
|
537
964
|
for (let i = 0; i < selected.length; i++) {
|
|
@@ -549,7 +976,10 @@ async function saveChatAttachments(attachments, workspaceDir) {
|
|
|
549
976
|
}
|
|
550
977
|
const safeBase = (item.name || `screenshot-${i + 1}`).replace(/[^\w.\-]+/g, "_").slice(0, 64);
|
|
551
978
|
const fileName = `${stamp}-${i + 1}-${safeBase}${extForMime(mime)}`;
|
|
552
|
-
const filePath =
|
|
979
|
+
const filePath = import_node_path4.default.resolve(dir, fileName);
|
|
980
|
+
if (!isInsideWorkspace(dir, filePath)) {
|
|
981
|
+
throw new Error("Attachment path escaped the project data folder");
|
|
982
|
+
}
|
|
553
983
|
await import_promises.default.writeFile(filePath, buffer);
|
|
554
984
|
paths.push(filePath);
|
|
555
985
|
}
|
|
@@ -636,7 +1066,7 @@ function createToolValidator(schemas) {
|
|
|
636
1066
|
|
|
637
1067
|
// src/http/handler.ts
|
|
638
1068
|
var WORKING_PROVIDER = "working";
|
|
639
|
-
async function setWorkingMessage(db, conversationId) {
|
|
1069
|
+
async function setWorkingMessage(db, conversationId, parentMessageId) {
|
|
640
1070
|
await db.ensureConversation(conversationId);
|
|
641
1071
|
await db.clearWorkingMessages?.(conversationId);
|
|
642
1072
|
await db.saveMessage({
|
|
@@ -644,9 +1074,26 @@ async function setWorkingMessage(db, conversationId) {
|
|
|
644
1074
|
role: "assistant",
|
|
645
1075
|
content: "",
|
|
646
1076
|
provider: WORKING_PROVIDER,
|
|
647
|
-
senderType: "ai"
|
|
1077
|
+
senderType: "ai",
|
|
1078
|
+
parentMessageId: parentMessageId ?? void 0
|
|
648
1079
|
});
|
|
649
1080
|
}
|
|
1081
|
+
function savedMessageFields(result) {
|
|
1082
|
+
if (!result || typeof result !== "object") return {};
|
|
1083
|
+
const row = result;
|
|
1084
|
+
const next = row.nextQueued && typeof row.nextQueued === "object" && typeof row.nextQueued.id === "string" ? {
|
|
1085
|
+
id: row.nextQueued.id,
|
|
1086
|
+
content: String(
|
|
1087
|
+
row.nextQueued.content ?? ""
|
|
1088
|
+
)
|
|
1089
|
+
} : null;
|
|
1090
|
+
return {
|
|
1091
|
+
id: typeof row.id === "string" ? row.id : void 0,
|
|
1092
|
+
queueStatus: typeof row.queueStatus === "string" || row.queueStatus === null ? row.queueStatus : void 0,
|
|
1093
|
+
queuePosition: typeof row.queuePosition === "number" || row.queuePosition === null ? row.queuePosition : void 0,
|
|
1094
|
+
nextQueued: next
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
650
1097
|
var SHARED_CONVERSATION_ID = "shared";
|
|
651
1098
|
var conversationTurnSeq = /* @__PURE__ */ new Map();
|
|
652
1099
|
function beginConversationTurn(conversationId) {
|
|
@@ -681,14 +1128,18 @@ async function resolveSharedConversationId(request, options, bodyId) {
|
|
|
681
1128
|
return SHARED_CONVERSATION_ID;
|
|
682
1129
|
}
|
|
683
1130
|
function createChatHandler(options) {
|
|
1131
|
+
const logger = options.logger ?? createLogger("ai-cli:chat");
|
|
684
1132
|
const validate = options.tools ? createToolValidator(options.tools) : null;
|
|
685
1133
|
const workspaceDir = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
|
|
1134
|
+
const dataDir = options.dataDir;
|
|
1135
|
+
const sandboxId = options.sandboxId;
|
|
686
1136
|
const baseCallOptions = {
|
|
687
1137
|
systemPrompt: options.systemPrompt,
|
|
688
1138
|
workspaceDir,
|
|
689
1139
|
providerPreference: options.providerPreference,
|
|
690
1140
|
providers: options.providers
|
|
691
1141
|
};
|
|
1142
|
+
logger.debug({ workspaceDir }, "chat handler ready");
|
|
692
1143
|
return {
|
|
693
1144
|
async GET(request) {
|
|
694
1145
|
try {
|
|
@@ -704,6 +1155,14 @@ function createChatHandler(options) {
|
|
|
704
1155
|
if (options.db?.listMessages) {
|
|
705
1156
|
messages = await options.db.listMessages(conversationId);
|
|
706
1157
|
}
|
|
1158
|
+
logger.debug(
|
|
1159
|
+
{
|
|
1160
|
+
conversationId,
|
|
1161
|
+
provider: provider.id,
|
|
1162
|
+
messageCount: messages?.length ?? 0
|
|
1163
|
+
},
|
|
1164
|
+
"GET /api/chat"
|
|
1165
|
+
);
|
|
707
1166
|
return Response.json({
|
|
708
1167
|
provider: provider.id,
|
|
709
1168
|
providerLabel: provider.label || providerLabel(provider.id),
|
|
@@ -711,6 +1170,7 @@ function createChatHandler(options) {
|
|
|
711
1170
|
messages: messages ?? []
|
|
712
1171
|
});
|
|
713
1172
|
} catch (err) {
|
|
1173
|
+
logger.error({ err }, "GET /api/chat failed");
|
|
714
1174
|
const message = err instanceof Error ? err.message : "No AI CLI provider available";
|
|
715
1175
|
return Response.json(
|
|
716
1176
|
{ error: message, provider: null },
|
|
@@ -720,7 +1180,9 @@ function createChatHandler(options) {
|
|
|
720
1180
|
},
|
|
721
1181
|
async POST(request) {
|
|
722
1182
|
let trackedConversationId;
|
|
1183
|
+
let trackedParentUserMessageId;
|
|
723
1184
|
let trackedTurn = 0;
|
|
1185
|
+
const startedAt = Date.now();
|
|
724
1186
|
try {
|
|
725
1187
|
const body = await request.json();
|
|
726
1188
|
const conversationId = await resolveSharedConversationId(
|
|
@@ -729,6 +1191,19 @@ function createChatHandler(options) {
|
|
|
729
1191
|
body.conversationId
|
|
730
1192
|
);
|
|
731
1193
|
trackedConversationId = conversationId;
|
|
1194
|
+
logger.debug(
|
|
1195
|
+
{
|
|
1196
|
+
conversationId,
|
|
1197
|
+
type: body.type ?? "turn",
|
|
1198
|
+
skipPersistUser: Boolean(body.skipPersistUser),
|
|
1199
|
+
userMessageId: body.userMessageId,
|
|
1200
|
+
messageCount: body.messages?.length ?? 0,
|
|
1201
|
+
attachmentCount: body.attachments?.length ?? 0,
|
|
1202
|
+
queueStatus: body.queueStatus,
|
|
1203
|
+
preview: previewText(body.userMessage || body.content)
|
|
1204
|
+
},
|
|
1205
|
+
"POST /api/chat"
|
|
1206
|
+
);
|
|
732
1207
|
if (body.type === "working-clear") {
|
|
733
1208
|
if (!conversationId) {
|
|
734
1209
|
return Response.json(
|
|
@@ -753,11 +1228,54 @@ function createChatHandler(options) {
|
|
|
753
1228
|
await options.db.ensureConversation(conversationId);
|
|
754
1229
|
await options.db.saveMessage({
|
|
755
1230
|
conversationId,
|
|
1231
|
+
id: body.messageId,
|
|
756
1232
|
role: "assistant",
|
|
757
1233
|
content,
|
|
758
1234
|
provider: "developer",
|
|
759
1235
|
senderType: "developer",
|
|
760
|
-
senderName: body.senderName?.trim() || void 0
|
|
1236
|
+
senderName: body.senderName?.trim() || void 0,
|
|
1237
|
+
parentMessageId: body.parentMessageId
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
return Response.json({ ok: true, conversationId });
|
|
1241
|
+
}
|
|
1242
|
+
if (body.type === "user") {
|
|
1243
|
+
const content = body.content?.trim();
|
|
1244
|
+
if (!conversationId || !content) {
|
|
1245
|
+
return Response.json(
|
|
1246
|
+
{ error: "conversationId and content are required" },
|
|
1247
|
+
{ status: 400 }
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
if (options.db) {
|
|
1251
|
+
await options.db.ensureConversation(conversationId);
|
|
1252
|
+
let persistAttachmentPaths2 = [];
|
|
1253
|
+
if (options.db.uploadAttachments && body.attachments?.length) {
|
|
1254
|
+
const uploaded = await options.db.uploadAttachments(
|
|
1255
|
+
conversationId,
|
|
1256
|
+
body.attachments
|
|
1257
|
+
);
|
|
1258
|
+
persistAttachmentPaths2 = uploaded.refs;
|
|
1259
|
+
}
|
|
1260
|
+
const saved = await options.db.saveMessage({
|
|
1261
|
+
conversationId,
|
|
1262
|
+
id: body.messageId,
|
|
1263
|
+
role: "user",
|
|
1264
|
+
content,
|
|
1265
|
+
attachmentPaths: persistAttachmentPaths2.length > 0 ? persistAttachmentPaths2 : void 0,
|
|
1266
|
+
senderType: body.senderType === "client" ? "client" : void 0,
|
|
1267
|
+
senderName: body.senderName?.trim() || void 0,
|
|
1268
|
+
parentMessageId: body.parentMessageId,
|
|
1269
|
+
intent: "queue",
|
|
1270
|
+
queueStatus: body.queueStatus === "queued" || body.queueStatus === "working" ? body.queueStatus : void 0
|
|
1271
|
+
});
|
|
1272
|
+
const fields = savedMessageFields(saved);
|
|
1273
|
+
return Response.json({
|
|
1274
|
+
ok: true,
|
|
1275
|
+
conversationId,
|
|
1276
|
+
message: saved,
|
|
1277
|
+
queueStatus: fields.queueStatus ?? null,
|
|
1278
|
+
queuePosition: fields.queuePosition ?? null
|
|
761
1279
|
});
|
|
762
1280
|
}
|
|
763
1281
|
return Response.json({ ok: true, conversationId });
|
|
@@ -771,44 +1289,99 @@ function createChatHandler(options) {
|
|
|
771
1289
|
}
|
|
772
1290
|
let attachmentPaths = [];
|
|
773
1291
|
let persistAttachmentPaths = [];
|
|
774
|
-
if (options.db?.uploadAttachments) {
|
|
1292
|
+
if (options.db?.uploadAttachments && attachments?.length) {
|
|
775
1293
|
const uploaded = await options.db.uploadAttachments(
|
|
776
1294
|
conversationId,
|
|
777
1295
|
attachments
|
|
778
1296
|
);
|
|
779
1297
|
attachmentPaths = uploaded.localPaths;
|
|
780
1298
|
persistAttachmentPaths = uploaded.refs;
|
|
781
|
-
} else {
|
|
1299
|
+
} else if (attachments?.length) {
|
|
782
1300
|
attachmentPaths = await saveChatAttachments(
|
|
783
1301
|
attachments,
|
|
784
|
-
workspaceDir
|
|
1302
|
+
workspaceDir,
|
|
1303
|
+
{ dataDir, sandboxId }
|
|
785
1304
|
);
|
|
786
1305
|
persistAttachmentPaths = attachmentPaths;
|
|
787
1306
|
}
|
|
1307
|
+
const storedUserId = typeof body.userMessageId === "string" && body.userMessageId ? body.userMessageId : void 0;
|
|
1308
|
+
if (!attachmentPaths.length && conversationId && options.db?.materializeMessageAttachments) {
|
|
1309
|
+
attachmentPaths = await options.db.materializeMessageAttachments(
|
|
1310
|
+
conversationId,
|
|
1311
|
+
storedUserId,
|
|
1312
|
+
workspaceDir
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
logger.debug(
|
|
1316
|
+
{
|
|
1317
|
+
conversationId,
|
|
1318
|
+
userMessageId: storedUserId,
|
|
1319
|
+
attachmentCount: attachmentPaths.length
|
|
1320
|
+
},
|
|
1321
|
+
"cli attachments"
|
|
1322
|
+
);
|
|
788
1323
|
const turn = beginConversationTurn(conversationId ?? SHARED_CONVERSATION_ID);
|
|
789
1324
|
trackedTurn = turn;
|
|
790
1325
|
const signal = request.signal;
|
|
1326
|
+
let parentUserMessageId = typeof body.userMessageId === "string" && body.userMessageId ? body.userMessageId : void 0;
|
|
1327
|
+
trackedParentUserMessageId = parentUserMessageId;
|
|
791
1328
|
if (options.db && conversationId) {
|
|
792
1329
|
const latest = messages[messages.length - 1];
|
|
793
1330
|
const persistContent = body.userMessage?.trim() || (latest?.role === "user" ? latest.content : "");
|
|
794
|
-
if (persistContent) {
|
|
1331
|
+
if (persistContent && !body.skipPersistUser && !parentUserMessageId) {
|
|
795
1332
|
await options.db.ensureConversation(conversationId);
|
|
796
|
-
await options.db.saveMessage({
|
|
1333
|
+
const saved = await options.db.saveMessage({
|
|
797
1334
|
conversationId,
|
|
798
1335
|
role: "user",
|
|
799
1336
|
content: persistContent,
|
|
800
1337
|
attachmentPaths: persistAttachmentPaths.length > 0 ? persistAttachmentPaths : void 0,
|
|
801
1338
|
senderType: body.senderType === "client" ? "client" : void 0,
|
|
802
|
-
senderName: body.senderName?.trim() || void 0
|
|
1339
|
+
senderName: body.senderName?.trim() || void 0,
|
|
1340
|
+
intent: "run"
|
|
803
1341
|
});
|
|
1342
|
+
parentUserMessageId = savedMessageFields(saved).id ?? parentUserMessageId;
|
|
1343
|
+
trackedParentUserMessageId = parentUserMessageId;
|
|
1344
|
+
}
|
|
1345
|
+
if (!body.skipPersistUser) {
|
|
1346
|
+
await setWorkingMessage(
|
|
1347
|
+
options.db,
|
|
1348
|
+
conversationId,
|
|
1349
|
+
parentUserMessageId
|
|
1350
|
+
);
|
|
804
1351
|
}
|
|
805
|
-
await setWorkingMessage(options.db, conversationId);
|
|
806
1352
|
}
|
|
807
1353
|
const latestUser = [...messages].reverse().find((m) => m.role === "user");
|
|
808
1354
|
const priorConversationsContext = options.db ? await buildPriorConversationsContext(options.db, {
|
|
809
1355
|
excludeConversationId: conversationId,
|
|
810
1356
|
currentRequest: latestUser?.content ?? ""
|
|
811
1357
|
}) : "";
|
|
1358
|
+
let parentChainContext = "";
|
|
1359
|
+
if (options.db?.listMessages && conversationId) {
|
|
1360
|
+
const stored = await options.db.listMessages(conversationId);
|
|
1361
|
+
const chain = parentChainForTurn(
|
|
1362
|
+
stored.map((row) => ({
|
|
1363
|
+
id: row.id,
|
|
1364
|
+
role: row.role,
|
|
1365
|
+
content: row.content,
|
|
1366
|
+
parentMessageId: row.parentMessageId,
|
|
1367
|
+
senderName: row.senderName,
|
|
1368
|
+
provider: row.provider
|
|
1369
|
+
})),
|
|
1370
|
+
parentUserMessageId,
|
|
1371
|
+
body.parentMessageId
|
|
1372
|
+
);
|
|
1373
|
+
parentChainContext = formatParentChainContext(chain);
|
|
1374
|
+
if (parentChainContext) {
|
|
1375
|
+
logger.debug(
|
|
1376
|
+
{
|
|
1377
|
+
conversationId,
|
|
1378
|
+
parentUserMessageId,
|
|
1379
|
+
chainLength: chain.length
|
|
1380
|
+
},
|
|
1381
|
+
"parent chain context"
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
812
1385
|
if (!isActiveConversationTurn(
|
|
813
1386
|
conversationId ?? SHARED_CONVERSATION_ID,
|
|
814
1387
|
turn
|
|
@@ -819,23 +1392,59 @@ function createChatHandler(options) {
|
|
|
819
1392
|
});
|
|
820
1393
|
}
|
|
821
1394
|
if (signal.aborted) {
|
|
822
|
-
|
|
823
|
-
|
|
1395
|
+
logger.debug({ conversationId, turn }, "aborted before AI");
|
|
1396
|
+
if (conversationId && options.db) {
|
|
1397
|
+
if (options.db.releaseWorkingTurn) {
|
|
1398
|
+
await options.db.releaseWorkingTurn(
|
|
1399
|
+
conversationId,
|
|
1400
|
+
parentUserMessageId
|
|
1401
|
+
);
|
|
1402
|
+
} else {
|
|
1403
|
+
await options.db.clearWorkingMessages?.(conversationId);
|
|
1404
|
+
}
|
|
824
1405
|
}
|
|
825
1406
|
return Response.json({
|
|
826
1407
|
superseded: true,
|
|
827
1408
|
conversationId: conversationId ?? null
|
|
828
1409
|
});
|
|
829
1410
|
}
|
|
1411
|
+
logger.debug(
|
|
1412
|
+
{
|
|
1413
|
+
conversationId,
|
|
1414
|
+
turn,
|
|
1415
|
+
parentUserMessageId,
|
|
1416
|
+
historyLength: messages.length,
|
|
1417
|
+
attachmentCount: attachmentPaths.length,
|
|
1418
|
+
hasParentChain: Boolean(parentChainContext),
|
|
1419
|
+
preview: previewText(
|
|
1420
|
+
[...messages].reverse().find((m) => m.role === "user")?.content
|
|
1421
|
+
)
|
|
1422
|
+
},
|
|
1423
|
+
"calling AI provider"
|
|
1424
|
+
);
|
|
1425
|
+
const aiStarted = Date.now();
|
|
830
1426
|
const aiResponse = await callAi(messages, context, {
|
|
831
1427
|
...baseCallOptions,
|
|
832
1428
|
attachmentPaths,
|
|
833
|
-
priorConversationsContext: priorConversationsContext || void 0
|
|
1429
|
+
priorConversationsContext: priorConversationsContext || void 0,
|
|
1430
|
+
parentChainContext: parentChainContext || void 0
|
|
834
1431
|
});
|
|
1432
|
+
logger.debug(
|
|
1433
|
+
{
|
|
1434
|
+
conversationId,
|
|
1435
|
+
turn,
|
|
1436
|
+
provider: aiResponse.provider,
|
|
1437
|
+
toolCalls: aiResponse.toolCalls.length,
|
|
1438
|
+
ms: Date.now() - aiStarted,
|
|
1439
|
+
preview: previewText(aiResponse.text)
|
|
1440
|
+
},
|
|
1441
|
+
"AI provider returned"
|
|
1442
|
+
);
|
|
835
1443
|
if (!isActiveConversationTurn(
|
|
836
1444
|
conversationId ?? SHARED_CONVERSATION_ID,
|
|
837
1445
|
turn
|
|
838
1446
|
)) {
|
|
1447
|
+
logger.debug({ conversationId, turn }, "superseded after AI");
|
|
839
1448
|
return Response.json({
|
|
840
1449
|
superseded: true,
|
|
841
1450
|
conversationId: conversationId ?? null
|
|
@@ -843,38 +1452,108 @@ function createChatHandler(options) {
|
|
|
843
1452
|
}
|
|
844
1453
|
const validatedToolCalls = validate ? aiResponse.toolCalls.filter((tc) => validate(tc).valid) : aiResponse.toolCalls;
|
|
845
1454
|
if (options.onToolCalls && validatedToolCalls.length > 0) {
|
|
1455
|
+
logger.debug(
|
|
1456
|
+
{ conversationId, count: validatedToolCalls.length },
|
|
1457
|
+
"applying tool calls"
|
|
1458
|
+
);
|
|
846
1459
|
await options.onToolCalls(validatedToolCalls, context);
|
|
847
1460
|
}
|
|
1461
|
+
let nextQueued = null;
|
|
1462
|
+
const assistantMessageId = typeof body.assistantMessageId === "string" && body.assistantMessageId ? body.assistantMessageId : (0, import_node_crypto2.randomUUID)();
|
|
1463
|
+
let replyId = assistantMessageId;
|
|
848
1464
|
if (options.db && conversationId) {
|
|
849
1465
|
await options.db.ensureConversation(conversationId);
|
|
850
|
-
await options.db.
|
|
851
|
-
await options.db.saveMessage({
|
|
1466
|
+
const savedReply = await options.db.saveMessage({
|
|
852
1467
|
conversationId,
|
|
1468
|
+
id: assistantMessageId,
|
|
853
1469
|
role: "assistant",
|
|
854
1470
|
content: aiResponse.text,
|
|
855
1471
|
provider: aiResponse.provider,
|
|
856
|
-
senderType: "ai"
|
|
1472
|
+
senderType: "ai",
|
|
1473
|
+
parentMessageId: parentUserMessageId
|
|
857
1474
|
});
|
|
1475
|
+
replyId = savedMessageFields(savedReply).id ?? assistantMessageId;
|
|
1476
|
+
if (options.db.releaseWorkingTurn) {
|
|
1477
|
+
let released = void 0;
|
|
1478
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1479
|
+
try {
|
|
1480
|
+
released = await options.db.releaseWorkingTurn(
|
|
1481
|
+
conversationId,
|
|
1482
|
+
parentUserMessageId
|
|
1483
|
+
);
|
|
1484
|
+
break;
|
|
1485
|
+
} catch (err) {
|
|
1486
|
+
logger.warn(
|
|
1487
|
+
{ err, conversationId, parentUserMessageId, attempt },
|
|
1488
|
+
"releaseWorkingTurn failed; retrying"
|
|
1489
|
+
);
|
|
1490
|
+
if (attempt === 2) throw err;
|
|
1491
|
+
await new Promise((r) => setTimeout(r, 250 * (attempt + 1)));
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
nextQueued = released && typeof released === "object" && released.nextQueued && typeof released.nextQueued.id === "string" ? {
|
|
1495
|
+
id: released.nextQueued.id,
|
|
1496
|
+
content: String(released.nextQueued.content ?? "")
|
|
1497
|
+
} : null;
|
|
1498
|
+
logger.debug(
|
|
1499
|
+
{ conversationId, parentUserMessageId, nextQueued: nextQueued?.id },
|
|
1500
|
+
"released working turn"
|
|
1501
|
+
);
|
|
1502
|
+
} else {
|
|
1503
|
+
await options.db.clearWorkingMessages?.(conversationId);
|
|
1504
|
+
nextQueued = savedMessageFields(savedReply).nextQueued ?? null;
|
|
1505
|
+
}
|
|
858
1506
|
if (validatedToolCalls.length > 0) {
|
|
859
1507
|
await options.db.saveToolEvents?.(
|
|
860
1508
|
conversationId,
|
|
861
1509
|
validatedToolCalls
|
|
862
1510
|
);
|
|
863
1511
|
}
|
|
1512
|
+
logger.debug(
|
|
1513
|
+
{
|
|
1514
|
+
conversationId,
|
|
1515
|
+
replyId,
|
|
1516
|
+
nextQueued: nextQueued?.id
|
|
1517
|
+
},
|
|
1518
|
+
"persisted AI reply"
|
|
1519
|
+
);
|
|
864
1520
|
}
|
|
1521
|
+
logger.debug(
|
|
1522
|
+
{ conversationId, turn, ms: Date.now() - startedAt },
|
|
1523
|
+
"POST /api/chat done"
|
|
1524
|
+
);
|
|
865
1525
|
return Response.json({
|
|
866
1526
|
text: aiResponse.text,
|
|
867
1527
|
toolCalls: validatedToolCalls,
|
|
868
1528
|
provider: aiResponse.provider,
|
|
869
1529
|
providerLabel: providerLabel(aiResponse.provider),
|
|
870
|
-
conversationId: conversationId ?? null
|
|
1530
|
+
conversationId: conversationId ?? null,
|
|
1531
|
+
messageId: replyId,
|
|
1532
|
+
nextQueued,
|
|
1533
|
+
parentMessageId: parentUserMessageId ?? null
|
|
871
1534
|
});
|
|
872
1535
|
} catch (err) {
|
|
873
1536
|
console.error("Chat API error:", err);
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1537
|
+
logger.error(
|
|
1538
|
+
{
|
|
1539
|
+
err,
|
|
1540
|
+
conversationId: trackedConversationId,
|
|
1541
|
+
turn: trackedTurn,
|
|
1542
|
+
ms: Date.now() - startedAt
|
|
1543
|
+
},
|
|
1544
|
+
"POST /api/chat failed"
|
|
1545
|
+
);
|
|
1546
|
+
if (trackedConversationId && options.db && isActiveConversationTurn(trackedConversationId, trackedTurn)) {
|
|
1547
|
+
if (options.db.releaseWorkingTurn) {
|
|
1548
|
+
await options.db.releaseWorkingTurn(
|
|
1549
|
+
trackedConversationId,
|
|
1550
|
+
trackedParentUserMessageId
|
|
1551
|
+
).catch(() => void 0);
|
|
1552
|
+
} else {
|
|
1553
|
+
await options.db.clearWorkingMessages?.(trackedConversationId).catch(
|
|
1554
|
+
() => void 0
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
878
1557
|
}
|
|
879
1558
|
return Response.json(
|
|
880
1559
|
{
|
|
@@ -895,8 +1574,13 @@ function toNextRoute(handlers) {
|
|
|
895
1574
|
|
|
896
1575
|
// src/http/local-store.ts
|
|
897
1576
|
var import_promises2 = __toESM(require("fs/promises"), 1);
|
|
898
|
-
var
|
|
899
|
-
var
|
|
1577
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
1578
|
+
var import_node_crypto3 = require("crypto");
|
|
1579
|
+
function userHasAiReply(rows, workingId) {
|
|
1580
|
+
return rows.some(
|
|
1581
|
+
(m) => m.role === "assistant" && m.parentMessageId === workingId && m.provider !== "working" && Boolean(m.content?.trim())
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
900
1584
|
async function readConversation(filePath) {
|
|
901
1585
|
try {
|
|
902
1586
|
const raw = await import_promises2.default.readFile(filePath, "utf8");
|
|
@@ -907,11 +1591,11 @@ async function readConversation(filePath) {
|
|
|
907
1591
|
}
|
|
908
1592
|
}
|
|
909
1593
|
async function writeConversation(filePath, data) {
|
|
910
|
-
await import_promises2.default.mkdir(
|
|
1594
|
+
await import_promises2.default.mkdir(import_node_path5.default.dirname(filePath), { recursive: true });
|
|
911
1595
|
await import_promises2.default.writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
912
1596
|
}
|
|
913
1597
|
function createLocalDirectoryStore(baseDir) {
|
|
914
|
-
const fileFor = (id) =>
|
|
1598
|
+
const fileFor = (id) => import_node_path5.default.join(baseDir, `${id}.json`);
|
|
915
1599
|
return {
|
|
916
1600
|
async ensureConversation(id) {
|
|
917
1601
|
const existing = await readConversation(fileFor(id));
|
|
@@ -929,21 +1613,273 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
929
1613
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
930
1614
|
messages: []
|
|
931
1615
|
};
|
|
1616
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1617
|
+
const isClientUser = input.role === "user" && input.senderType !== "developer" && input.provider !== "developer";
|
|
1618
|
+
const isWorking = input.provider === "working";
|
|
1619
|
+
const isAi = input.role === "assistant" && input.provider !== "working" && input.senderType !== "developer" && input.provider !== "developer";
|
|
1620
|
+
const busy = existing.messages.some((m) => m.queueStatus === "working");
|
|
1621
|
+
const makeWorking = (parentMessageId) => ({
|
|
1622
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
1623
|
+
conversationId: input.conversationId,
|
|
1624
|
+
role: "assistant",
|
|
1625
|
+
content: "",
|
|
1626
|
+
provider: "working",
|
|
1627
|
+
createdAt: now,
|
|
1628
|
+
senderType: "ai",
|
|
1629
|
+
parentMessageId
|
|
1630
|
+
});
|
|
1631
|
+
const promoteNext = () => {
|
|
1632
|
+
const waiting = existing.messages.filter((m) => m.queueStatus === "queued").sort(
|
|
1633
|
+
(a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)
|
|
1634
|
+
);
|
|
1635
|
+
const next = waiting[0];
|
|
1636
|
+
if (!next) return null;
|
|
1637
|
+
next.queueStatus = "working";
|
|
1638
|
+
next.queuePosition = null;
|
|
1639
|
+
existing.messages.push(makeWorking(next.id));
|
|
1640
|
+
return next;
|
|
1641
|
+
};
|
|
1642
|
+
if (isClientUser) {
|
|
1643
|
+
if (input.intent === "run") {
|
|
1644
|
+
for (const row of existing.messages) {
|
|
1645
|
+
if (row.queueStatus === "working") {
|
|
1646
|
+
row.queueStatus = null;
|
|
1647
|
+
row.queuePosition = null;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
existing.messages = existing.messages.filter(
|
|
1651
|
+
(m) => m.provider !== "working"
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
const queueStatus = input.intent === "run" ? "working" : busy ? "queued" : "working";
|
|
1655
|
+
const existingById2 = input.id ? existing.messages.find((m) => m.id === input.id) : void 0;
|
|
1656
|
+
if (existingById2) {
|
|
1657
|
+
return { ...existingById2, nextQueued: null };
|
|
1658
|
+
}
|
|
1659
|
+
const message2 = {
|
|
1660
|
+
id: input.id ?? (0, import_node_crypto3.randomUUID)(),
|
|
1661
|
+
conversationId: input.conversationId,
|
|
1662
|
+
role: input.role,
|
|
1663
|
+
content: input.content,
|
|
1664
|
+
provider: input.provider ?? null,
|
|
1665
|
+
createdAt: now,
|
|
1666
|
+
attachmentPaths: input.attachmentPaths,
|
|
1667
|
+
senderType: input.senderType ?? null,
|
|
1668
|
+
senderName: input.senderName ?? null,
|
|
1669
|
+
parentMessageId: input.parentMessageId ?? null,
|
|
1670
|
+
queueStatus,
|
|
1671
|
+
queuePosition: null
|
|
1672
|
+
};
|
|
1673
|
+
existing.messages.push(message2);
|
|
1674
|
+
existing.updatedAt = now;
|
|
1675
|
+
await writeConversation(filePath, existing);
|
|
1676
|
+
return { ...message2, nextQueued: null };
|
|
1677
|
+
}
|
|
1678
|
+
if (isWorking) {
|
|
1679
|
+
existing.messages = existing.messages.filter(
|
|
1680
|
+
(m) => m.provider !== "working"
|
|
1681
|
+
);
|
|
1682
|
+
if (input.parentMessageId) {
|
|
1683
|
+
const parent = existing.messages.find(
|
|
1684
|
+
(m) => m.id === input.parentMessageId
|
|
1685
|
+
);
|
|
1686
|
+
if (parent && parent.queueStatus !== "working") {
|
|
1687
|
+
parent.queueStatus = "working";
|
|
1688
|
+
parent.queuePosition = null;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
const message2 = {
|
|
1692
|
+
id: input.id ?? (0, import_node_crypto3.randomUUID)(),
|
|
1693
|
+
conversationId: input.conversationId,
|
|
1694
|
+
role: "assistant",
|
|
1695
|
+
content: "",
|
|
1696
|
+
provider: "working",
|
|
1697
|
+
createdAt: now,
|
|
1698
|
+
senderType: "ai",
|
|
1699
|
+
parentMessageId: input.parentMessageId ?? null
|
|
1700
|
+
};
|
|
1701
|
+
existing.messages.push(message2);
|
|
1702
|
+
existing.updatedAt = now;
|
|
1703
|
+
await writeConversation(filePath, existing);
|
|
1704
|
+
return { ...message2, nextQueued: null };
|
|
1705
|
+
}
|
|
1706
|
+
const existingById = input.id ? existing.messages.find((m) => m.id === input.id) : void 0;
|
|
1707
|
+
if (existingById) {
|
|
1708
|
+
return { ...existingById, nextQueued: null };
|
|
1709
|
+
}
|
|
932
1710
|
const message = {
|
|
933
|
-
id: (0,
|
|
1711
|
+
id: input.id ?? (0, import_node_crypto3.randomUUID)(),
|
|
934
1712
|
conversationId: input.conversationId,
|
|
935
1713
|
role: input.role,
|
|
936
1714
|
content: input.content,
|
|
937
1715
|
provider: input.provider ?? null,
|
|
938
|
-
createdAt:
|
|
1716
|
+
createdAt: now,
|
|
939
1717
|
attachmentPaths: input.attachmentPaths,
|
|
940
1718
|
senderType: input.senderType ?? null,
|
|
941
|
-
senderName: input.senderName ?? null
|
|
1719
|
+
senderName: input.senderName ?? null,
|
|
1720
|
+
parentMessageId: input.parentMessageId ?? null
|
|
942
1721
|
};
|
|
943
1722
|
existing.messages.push(message);
|
|
944
|
-
|
|
1723
|
+
let nextQueued = null;
|
|
1724
|
+
if (isAi) {
|
|
1725
|
+
existing.messages = existing.messages.filter(
|
|
1726
|
+
(m) => m.provider !== "working"
|
|
1727
|
+
);
|
|
1728
|
+
if (input.parentMessageId) {
|
|
1729
|
+
const parent = existing.messages.find(
|
|
1730
|
+
(m) => m.id === input.parentMessageId
|
|
1731
|
+
);
|
|
1732
|
+
if (parent) {
|
|
1733
|
+
parent.queueStatus = null;
|
|
1734
|
+
parent.queuePosition = null;
|
|
1735
|
+
}
|
|
1736
|
+
} else {
|
|
1737
|
+
for (const row of existing.messages) {
|
|
1738
|
+
if (row.queueStatus === "working") {
|
|
1739
|
+
row.queueStatus = null;
|
|
1740
|
+
row.queuePosition = null;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
nextQueued = promoteNext();
|
|
1745
|
+
}
|
|
1746
|
+
existing.updatedAt = now;
|
|
1747
|
+
await writeConversation(filePath, existing);
|
|
1748
|
+
return {
|
|
1749
|
+
...message,
|
|
1750
|
+
nextQueued: nextQueued ? { id: nextQueued.id, content: nextQueued.content } : null
|
|
1751
|
+
};
|
|
1752
|
+
},
|
|
1753
|
+
async recoverQueue(conversationId) {
|
|
1754
|
+
const filePath = fileFor(conversationId);
|
|
1755
|
+
const existing = await readConversation(filePath);
|
|
1756
|
+
if (!existing) {
|
|
1757
|
+
return {
|
|
1758
|
+
action: "noop",
|
|
1759
|
+
workingId: null,
|
|
1760
|
+
promotedId: null,
|
|
1761
|
+
nextQueued: null
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
const working = existing.messages.find(
|
|
1765
|
+
(m) => m.role === "user" && m.queueStatus === "working"
|
|
1766
|
+
);
|
|
1767
|
+
if (working) {
|
|
1768
|
+
if (userHasAiReply(existing.messages, working.id)) {
|
|
1769
|
+
existing.messages = existing.messages.filter(
|
|
1770
|
+
(m) => m.provider !== "working"
|
|
1771
|
+
);
|
|
1772
|
+
working.queueStatus = null;
|
|
1773
|
+
working.queuePosition = null;
|
|
1774
|
+
const waiting2 = existing.messages.filter((m) => m.queueStatus === "queued").sort(
|
|
1775
|
+
(a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)
|
|
1776
|
+
);
|
|
1777
|
+
const next2 = waiting2[0] ?? null;
|
|
1778
|
+
if (next2) {
|
|
1779
|
+
next2.queueStatus = "working";
|
|
1780
|
+
next2.queuePosition = null;
|
|
1781
|
+
existing.messages.push({
|
|
1782
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
1783
|
+
conversationId,
|
|
1784
|
+
role: "assistant",
|
|
1785
|
+
content: "",
|
|
1786
|
+
provider: "working",
|
|
1787
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1788
|
+
senderType: "ai",
|
|
1789
|
+
parentMessageId: next2.id
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1793
|
+
await writeConversation(filePath, existing);
|
|
1794
|
+
return {
|
|
1795
|
+
action: "completed_and_promoted",
|
|
1796
|
+
workingId: working.id,
|
|
1797
|
+
promotedId: next2?.id ?? null,
|
|
1798
|
+
nextQueued: next2 ? { id: next2.id, content: next2.content } : null
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
return {
|
|
1802
|
+
action: "redispatched_working",
|
|
1803
|
+
workingId: working.id,
|
|
1804
|
+
promotedId: null,
|
|
1805
|
+
nextQueued: null
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
const waiting = existing.messages.filter((m) => m.queueStatus === "queued").sort(
|
|
1809
|
+
(a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)
|
|
1810
|
+
);
|
|
1811
|
+
const next = waiting[0] ?? null;
|
|
1812
|
+
if (!next) {
|
|
1813
|
+
return {
|
|
1814
|
+
action: "noop",
|
|
1815
|
+
workingId: null,
|
|
1816
|
+
promotedId: null,
|
|
1817
|
+
nextQueued: null
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
next.queueStatus = "working";
|
|
1821
|
+
next.queuePosition = null;
|
|
1822
|
+
existing.messages.push({
|
|
1823
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
1824
|
+
conversationId,
|
|
1825
|
+
role: "assistant",
|
|
1826
|
+
content: "",
|
|
1827
|
+
provider: "working",
|
|
1828
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1829
|
+
senderType: "ai",
|
|
1830
|
+
parentMessageId: next.id
|
|
1831
|
+
});
|
|
1832
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1833
|
+
await writeConversation(filePath, existing);
|
|
1834
|
+
return {
|
|
1835
|
+
action: "promoted_queued",
|
|
1836
|
+
workingId: null,
|
|
1837
|
+
promotedId: next.id,
|
|
1838
|
+
nextQueued: { id: next.id, content: next.content }
|
|
1839
|
+
};
|
|
1840
|
+
},
|
|
1841
|
+
async releaseWorkingTurn(conversationId, parentMessageId) {
|
|
1842
|
+
const filePath = fileFor(conversationId);
|
|
1843
|
+
const existing = await readConversation(filePath);
|
|
1844
|
+
if (!existing) return { nextQueued: null };
|
|
1845
|
+
existing.messages = existing.messages.filter(
|
|
1846
|
+
(m) => m.provider !== "working"
|
|
1847
|
+
);
|
|
1848
|
+
if (parentMessageId) {
|
|
1849
|
+
const parent = existing.messages.find((m) => m.id === parentMessageId);
|
|
1850
|
+
if (parent) {
|
|
1851
|
+
parent.queueStatus = null;
|
|
1852
|
+
parent.queuePosition = null;
|
|
1853
|
+
}
|
|
1854
|
+
} else {
|
|
1855
|
+
for (const row of existing.messages) {
|
|
1856
|
+
if (row.queueStatus === "working") {
|
|
1857
|
+
row.queueStatus = null;
|
|
1858
|
+
row.queuePosition = null;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
const waiting = existing.messages.filter((m) => m.queueStatus === "queued").sort(
|
|
1863
|
+
(a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)
|
|
1864
|
+
);
|
|
1865
|
+
const next = waiting[0] ?? null;
|
|
1866
|
+
if (next) {
|
|
1867
|
+
next.queueStatus = "working";
|
|
1868
|
+
next.queuePosition = null;
|
|
1869
|
+
existing.messages.push({
|
|
1870
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
1871
|
+
conversationId,
|
|
1872
|
+
role: "assistant",
|
|
1873
|
+
content: "",
|
|
1874
|
+
provider: "working",
|
|
1875
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1876
|
+
senderType: "ai",
|
|
1877
|
+
parentMessageId: next.id
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
945
1881
|
await writeConversation(filePath, existing);
|
|
946
|
-
return
|
|
1882
|
+
return next ? { nextQueued: { id: next.id, content: next.content } } : { nextQueued: null };
|
|
947
1883
|
},
|
|
948
1884
|
async clearWorkingMessages(conversationId) {
|
|
949
1885
|
const filePath = fileFor(conversationId);
|
|
@@ -965,7 +1901,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
965
1901
|
const conversations = [];
|
|
966
1902
|
for (const entry of entries) {
|
|
967
1903
|
if (!entry.endsWith(".json")) continue;
|
|
968
|
-
const filePath =
|
|
1904
|
+
const filePath = import_node_path5.default.join(baseDir, entry);
|
|
969
1905
|
const existing = await readConversation(filePath);
|
|
970
1906
|
if (!existing?.id) continue;
|
|
971
1907
|
conversations.push({
|
|
@@ -982,7 +1918,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
982
1918
|
if (events.length === 0) return;
|
|
983
1919
|
const existing = await readConversation(fileFor(conversationId));
|
|
984
1920
|
if (!existing) return;
|
|
985
|
-
const toolFile =
|
|
1921
|
+
const toolFile = import_node_path5.default.join(baseDir, `${conversationId}.tools.jsonl`);
|
|
986
1922
|
const lines = events.map(
|
|
987
1923
|
(e) => JSON.stringify({
|
|
988
1924
|
conversationId,
|
|
@@ -998,35 +1934,389 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
998
1934
|
|
|
999
1935
|
// src/http/maintainer-pro-store.ts
|
|
1000
1936
|
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
1001
|
-
var
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1937
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
1938
|
+
|
|
1939
|
+
// src/http/synced-store.ts
|
|
1940
|
+
function userHasAiReply2(rows, workingId) {
|
|
1941
|
+
return rows.some(
|
|
1942
|
+
(row) => row.role === "assistant" && row.parentMessageId === workingId && row.provider !== "working" && row.senderType !== "developer" && Boolean(row.content?.trim())
|
|
1943
|
+
);
|
|
1944
|
+
}
|
|
1945
|
+
function nowIso() {
|
|
1946
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1947
|
+
}
|
|
1948
|
+
function toWsUrl(baseUrl, apiKey) {
|
|
1949
|
+
const u = new URL(`${baseUrl.replace(/\/$/, "")}/api/v1/ws`);
|
|
1950
|
+
u.protocol = u.protocol === "https:" ? "wss:" : "ws:";
|
|
1951
|
+
u.searchParams.set("apiKey", apiKey);
|
|
1952
|
+
return u.toString();
|
|
1953
|
+
}
|
|
1954
|
+
function asCachedMessage(raw) {
|
|
1955
|
+
const id = typeof raw.id === "string" ? raw.id : "";
|
|
1956
|
+
const role = typeof raw.role === "string" ? raw.role : "";
|
|
1957
|
+
if (!id || !role) return null;
|
|
1958
|
+
return {
|
|
1959
|
+
id,
|
|
1960
|
+
role,
|
|
1961
|
+
content: typeof raw.content === "string" ? raw.content : "",
|
|
1962
|
+
provider: typeof raw.provider === "string" ? raw.provider : null,
|
|
1963
|
+
createdAt: typeof raw.createdAt === "string" ? raw.createdAt : void 0,
|
|
1964
|
+
attachmentPaths: Array.isArray(raw.attachmentPaths) ? raw.attachmentPaths.filter((p) => typeof p === "string") : Array.isArray(raw.attachmentIds) ? raw.attachmentIds.filter((id2) => typeof id2 === "string").map((id2) => `maintainer-pro://${id2}`) : void 0,
|
|
1965
|
+
senderType: raw.senderType === "client" || raw.senderType === "developer" || raw.senderType === "ai" ? raw.senderType : null,
|
|
1966
|
+
senderName: typeof raw.senderName === "string" ? raw.senderName : null,
|
|
1967
|
+
parentMessageId: typeof raw.parentMessageId === "string" ? raw.parentMessageId : null,
|
|
1968
|
+
queueStatus: raw.queueStatus === "working" || raw.queueStatus === "queued" ? raw.queueStatus : raw.queueStatus === null ? null : void 0,
|
|
1969
|
+
queuePosition: typeof raw.queuePosition === "number" ? raw.queuePosition : null
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
function cloneMessages(messages) {
|
|
1973
|
+
return messages.map((m) => ({ ...m }));
|
|
1974
|
+
}
|
|
1975
|
+
function createSyncedChatStore(options) {
|
|
1976
|
+
const { remote, maintainerProUrl, apiKey, onWorkingTurn } = options;
|
|
1977
|
+
const logger = options.logger ?? createLogger("ai-cli");
|
|
1978
|
+
const info = (msg) => {
|
|
1979
|
+
if (options.log) options.log(msg);
|
|
1980
|
+
else logger.info(msg);
|
|
1981
|
+
};
|
|
1982
|
+
const conversations = /* @__PURE__ */ new Map();
|
|
1983
|
+
const inflightRefresh = /* @__PURE__ */ new Map();
|
|
1984
|
+
let stopped = false;
|
|
1985
|
+
let socket = null;
|
|
1986
|
+
let reconnectAttempt = 0;
|
|
1987
|
+
let reconnectTimer = null;
|
|
1988
|
+
let pingTimer = null;
|
|
1989
|
+
const stats = () => {
|
|
1990
|
+
let messages = 0;
|
|
1991
|
+
for (const conv of conversations.values()) messages += conv.messages.length;
|
|
1992
|
+
return { conversations: conversations.size, messages };
|
|
1993
|
+
};
|
|
1994
|
+
const putConversation = (id, messages, updatedAt = nowIso()) => {
|
|
1995
|
+
conversations.set(id, { id, updatedAt, messages: cloneMessages(messages) });
|
|
1996
|
+
};
|
|
1997
|
+
const refreshConversation = (id) => {
|
|
1998
|
+
const existing = inflightRefresh.get(id);
|
|
1999
|
+
if (existing) return existing;
|
|
2000
|
+
const pending = (async () => {
|
|
2001
|
+
const messages = remote.listMessages ? await remote.listMessages(id) : [];
|
|
2002
|
+
putConversation(id, messages);
|
|
2003
|
+
return cloneMessages(messages);
|
|
2004
|
+
})().finally(() => {
|
|
2005
|
+
inflightRefresh.delete(id);
|
|
2006
|
+
});
|
|
2007
|
+
inflightRefresh.set(id, pending);
|
|
2008
|
+
return pending;
|
|
2009
|
+
};
|
|
2010
|
+
const loadSnapshot = async () => {
|
|
2011
|
+
if (remote.listSnapshot) {
|
|
2012
|
+
try {
|
|
2013
|
+
return await remote.listSnapshot();
|
|
2014
|
+
} catch {
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
if (!remote.listConversations || !remote.listMessages) return [];
|
|
2018
|
+
const summaries = await remote.listConversations();
|
|
2019
|
+
const loaded = [];
|
|
2020
|
+
for (const row of summaries) {
|
|
2021
|
+
loaded.push({
|
|
2022
|
+
id: row.id,
|
|
2023
|
+
updatedAt: row.updatedAt,
|
|
2024
|
+
messages: await remote.listMessages(row.id)
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
return loaded;
|
|
2028
|
+
};
|
|
2029
|
+
const hydrate = async () => {
|
|
2030
|
+
const snapshot = await loadSnapshot();
|
|
2031
|
+
conversations.clear();
|
|
2032
|
+
for (const row of snapshot) {
|
|
2033
|
+
putConversation(row.id, row.messages, row.updatedAt);
|
|
2034
|
+
}
|
|
2035
|
+
const next = stats();
|
|
2036
|
+
info(
|
|
2037
|
+
`cache hydrated (${next.conversations} conversations, ${next.messages} messages)`
|
|
2038
|
+
);
|
|
2039
|
+
for (const conv of conversations.values()) {
|
|
2040
|
+
const working = conv.messages.find(
|
|
2041
|
+
(row) => row.role === "user" && row.queueStatus === "working" && row.senderType !== "developer"
|
|
2042
|
+
);
|
|
2043
|
+
if (!working) continue;
|
|
2044
|
+
if (!userHasAiReply2(conv.messages, working.id)) {
|
|
2045
|
+
logger.debug(
|
|
2046
|
+
{ conversationId: conv.id, messageId: working.id },
|
|
2047
|
+
"resume working turn"
|
|
2048
|
+
);
|
|
2049
|
+
onWorkingTurn?.({ conversationId: conv.id, message: working });
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
return next;
|
|
2053
|
+
};
|
|
2054
|
+
const upsertMessage = (conversationId, message) => {
|
|
2055
|
+
const conv = conversations.get(conversationId) ?? {
|
|
2056
|
+
id: conversationId,
|
|
2057
|
+
updatedAt: nowIso(),
|
|
2058
|
+
messages: []
|
|
2059
|
+
};
|
|
2060
|
+
const index = conv.messages.findIndex((row) => row.id === message.id);
|
|
2061
|
+
const wasWorking = index >= 0 && conv.messages[index]?.queueStatus === "working";
|
|
2062
|
+
if (index >= 0) {
|
|
2063
|
+
conv.messages[index] = { ...conv.messages[index], ...message };
|
|
2064
|
+
} else {
|
|
2065
|
+
conv.messages.push(message);
|
|
2066
|
+
}
|
|
2067
|
+
conv.updatedAt = nowIso();
|
|
2068
|
+
conversations.set(conversationId, conv);
|
|
2069
|
+
const becameWorking = message.role === "user" && message.queueStatus === "working" && message.senderType !== "developer" && !wasWorking;
|
|
2070
|
+
if (becameWorking) {
|
|
2071
|
+
logger.debug(
|
|
2072
|
+
{ conversationId, messageId: message.id },
|
|
2073
|
+
"working turn"
|
|
2074
|
+
);
|
|
2075
|
+
onWorkingTurn?.({ conversationId, message });
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
const deleteMessages = (conversationId, ids) => {
|
|
2079
|
+
const conv = conversations.get(conversationId);
|
|
2080
|
+
if (!conv || ids.length === 0) return;
|
|
2081
|
+
const drop = new Set(ids);
|
|
2082
|
+
conv.messages = conv.messages.filter((row) => !drop.has(row.id));
|
|
2083
|
+
conv.updatedAt = nowIso();
|
|
2084
|
+
};
|
|
2085
|
+
const applyEvent = (msg) => {
|
|
2086
|
+
const externalId = typeof msg.externalId === "string" && msg.externalId ? msg.externalId : typeof msg.conversationId === "string" ? msg.conversationId : "";
|
|
2087
|
+
if (!externalId) return;
|
|
2088
|
+
if (msg.type === "message.created") {
|
|
2089
|
+
const raw = msg.message && typeof msg.message === "object" ? msg.message : null;
|
|
2090
|
+
const cached = raw ? asCachedMessage(raw) : null;
|
|
2091
|
+
if (cached) {
|
|
2092
|
+
upsertMessage(externalId, cached);
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
void refreshConversation(externalId).catch(() => void 0);
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
if (msg.type === "message.updated") {
|
|
2099
|
+
const raw = msg.message && typeof msg.message === "object" ? msg.message : null;
|
|
2100
|
+
const cached = raw ? asCachedMessage(raw) : null;
|
|
2101
|
+
if (cached) {
|
|
2102
|
+
upsertMessage(externalId, cached);
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
void refreshConversation(externalId).catch(() => void 0);
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
if (msg.type === "message.deleted") {
|
|
2109
|
+
const ids = Array.isArray(msg.ids) ? msg.ids.filter((id) => typeof id === "string") : [];
|
|
2110
|
+
deleteMessages(externalId, ids);
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
const stopSocket = () => {
|
|
2114
|
+
if (pingTimer) {
|
|
2115
|
+
clearInterval(pingTimer);
|
|
2116
|
+
pingTimer = null;
|
|
2117
|
+
}
|
|
2118
|
+
if (reconnectTimer) {
|
|
2119
|
+
clearTimeout(reconnectTimer);
|
|
2120
|
+
reconnectTimer = null;
|
|
2121
|
+
}
|
|
2122
|
+
try {
|
|
2123
|
+
socket?.close();
|
|
2124
|
+
} catch {
|
|
2125
|
+
}
|
|
2126
|
+
socket = null;
|
|
2127
|
+
};
|
|
2128
|
+
const connectRealtime = () => {
|
|
2129
|
+
if (stopped) return;
|
|
2130
|
+
const WebSocketCtor = globalThis.WebSocket;
|
|
2131
|
+
if (!WebSocketCtor) {
|
|
2132
|
+
info("WebSocket unavailable; cache will refresh after local writes only");
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
const ws = new WebSocketCtor(toWsUrl(maintainerProUrl, apiKey));
|
|
2136
|
+
socket = ws;
|
|
2137
|
+
ws.addEventListener("open", () => {
|
|
2138
|
+
reconnectAttempt = 0;
|
|
2139
|
+
logger.debug("store websocket open");
|
|
2140
|
+
if (pingTimer) clearInterval(pingTimer);
|
|
2141
|
+
pingTimer = setInterval(() => {
|
|
2142
|
+
try {
|
|
2143
|
+
if (ws.readyState === 1) ws.send(JSON.stringify({ type: "ping" }));
|
|
2144
|
+
} catch {
|
|
2145
|
+
}
|
|
2146
|
+
}, 2e4);
|
|
2147
|
+
});
|
|
2148
|
+
ws.addEventListener("message", (event) => {
|
|
2149
|
+
let payload;
|
|
2150
|
+
try {
|
|
2151
|
+
payload = JSON.parse(String(event.data));
|
|
2152
|
+
} catch {
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
if (payload.type === "hello") {
|
|
2156
|
+
const meta = payload.meta && typeof payload.meta === "object" ? payload.meta : null;
|
|
2157
|
+
const sandboxId = typeof meta?.sandboxId === "string" ? meta.sandboxId : "";
|
|
2158
|
+
if (sandboxId && ws.readyState === 1) {
|
|
2159
|
+
ws.send(
|
|
2160
|
+
JSON.stringify({ type: "subscribe", channels: [`sandbox:${sandboxId}`] })
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
logger.debug({ sandboxId }, "store websocket hello");
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
if (payload.type === "message.created" || payload.type === "message.updated" || payload.type === "message.deleted") {
|
|
2167
|
+
logger.debug({ type: payload.type }, "store websocket event");
|
|
2168
|
+
applyEvent(payload);
|
|
2169
|
+
}
|
|
2170
|
+
});
|
|
2171
|
+
ws.addEventListener("close", () => {
|
|
2172
|
+
if (socket === ws) socket = null;
|
|
2173
|
+
if (pingTimer) {
|
|
2174
|
+
clearInterval(pingTimer);
|
|
2175
|
+
pingTimer = null;
|
|
2176
|
+
}
|
|
2177
|
+
if (stopped) return;
|
|
2178
|
+
const delay = Math.min(3e4, 1e3 * 2 ** Math.min(reconnectAttempt, 5));
|
|
2179
|
+
reconnectAttempt += 1;
|
|
2180
|
+
logger.debug({ delay, reconnectAttempt }, "store websocket reconnect");
|
|
2181
|
+
reconnectTimer = setTimeout(() => {
|
|
2182
|
+
reconnectTimer = null;
|
|
2183
|
+
void hydrate().catch((err) => {
|
|
2184
|
+
logger.warn(
|
|
2185
|
+
`cache rehydrate failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2186
|
+
);
|
|
2187
|
+
}).finally(() => {
|
|
2188
|
+
connectRealtime();
|
|
2189
|
+
});
|
|
2190
|
+
}, delay);
|
|
2191
|
+
});
|
|
2192
|
+
};
|
|
2193
|
+
const ready = hydrate().catch((err) => {
|
|
2194
|
+
logger.warn(
|
|
2195
|
+
`cache hydrate failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2196
|
+
);
|
|
2197
|
+
return stats();
|
|
2198
|
+
});
|
|
2199
|
+
void ready.then(() => {
|
|
2200
|
+
if (!stopped) connectRealtime();
|
|
2201
|
+
});
|
|
2202
|
+
const store = {
|
|
2203
|
+
ready,
|
|
2204
|
+
stop: () => {
|
|
2205
|
+
stopped = true;
|
|
2206
|
+
stopSocket();
|
|
2207
|
+
},
|
|
2208
|
+
async ensureConversation(id) {
|
|
2209
|
+
await remote.ensureConversation(id);
|
|
2210
|
+
if (!conversations.has(id)) {
|
|
2211
|
+
putConversation(id, []);
|
|
2212
|
+
}
|
|
2213
|
+
},
|
|
2214
|
+
async saveMessage(input) {
|
|
2215
|
+
const saved = await remote.saveMessage(input);
|
|
2216
|
+
await refreshConversation(input.conversationId).catch(() => void 0);
|
|
2217
|
+
return saved;
|
|
2218
|
+
},
|
|
2219
|
+
async listMessages(conversationId) {
|
|
2220
|
+
await ready.catch(() => void 0);
|
|
2221
|
+
const hit = conversations.get(conversationId);
|
|
2222
|
+
if (hit) return cloneMessages(hit.messages);
|
|
2223
|
+
return refreshConversation(conversationId);
|
|
2224
|
+
},
|
|
2225
|
+
async clearWorkingMessages(conversationId) {
|
|
2226
|
+
await remote.clearWorkingMessages?.(conversationId);
|
|
2227
|
+
await refreshConversation(conversationId).catch(() => void 0);
|
|
2228
|
+
},
|
|
2229
|
+
async releaseWorkingTurn(conversationId, parentMessageId) {
|
|
2230
|
+
const result = await remote.releaseWorkingTurn?.(
|
|
2231
|
+
conversationId,
|
|
2232
|
+
parentMessageId
|
|
2233
|
+
);
|
|
2234
|
+
await refreshConversation(conversationId).catch(() => void 0);
|
|
2235
|
+
return result ?? { nextQueued: null };
|
|
2236
|
+
},
|
|
2237
|
+
async recoverQueue(conversationId) {
|
|
2238
|
+
const result = await remote.recoverQueue?.(conversationId);
|
|
2239
|
+
await refreshConversation(conversationId).catch(() => void 0);
|
|
2240
|
+
const conv = conversations.get(conversationId);
|
|
2241
|
+
if (conv) {
|
|
2242
|
+
const working = conv.messages.find(
|
|
2243
|
+
(row) => row.role === "user" && row.queueStatus === "working" && row.senderType !== "developer"
|
|
2244
|
+
);
|
|
2245
|
+
if (working && !userHasAiReply2(conv.messages, working.id)) {
|
|
2246
|
+
logger.info(
|
|
2247
|
+
{ conversationId, messageId: working.id, action: result?.action },
|
|
2248
|
+
"recoverQueue \u2192 resume working turn"
|
|
2249
|
+
);
|
|
2250
|
+
onWorkingTurn?.({ conversationId, message: working });
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
return result ?? {
|
|
2254
|
+
action: "noop",
|
|
2255
|
+
workingId: null,
|
|
2256
|
+
promotedId: null,
|
|
2257
|
+
nextQueued: null
|
|
2258
|
+
};
|
|
2259
|
+
},
|
|
2260
|
+
async listConversations() {
|
|
2261
|
+
await ready.catch(() => void 0);
|
|
2262
|
+
return [...conversations.values()].map((conv) => ({
|
|
2263
|
+
id: conv.id,
|
|
2264
|
+
updatedAt: conv.updatedAt,
|
|
2265
|
+
messages: cloneMessages(conv.messages)
|
|
2266
|
+
})).sort(
|
|
2267
|
+
(a, b) => (Date.parse(b.updatedAt) || 0) - (Date.parse(a.updatedAt) || 0)
|
|
2268
|
+
);
|
|
2269
|
+
},
|
|
2270
|
+
async saveToolEvents(conversationId, events) {
|
|
2271
|
+
await remote.saveToolEvents?.(conversationId, events);
|
|
2272
|
+
},
|
|
2273
|
+
async uploadAttachments(conversationId, attachments) {
|
|
2274
|
+
if (!remote.uploadAttachments) {
|
|
2275
|
+
return { refs: [], localPaths: [] };
|
|
2276
|
+
}
|
|
2277
|
+
return remote.uploadAttachments(conversationId, attachments);
|
|
2278
|
+
},
|
|
2279
|
+
async materializeMessageAttachments(conversationId, messageId, workspaceDir) {
|
|
2280
|
+
if (!remote.materializeMessageAttachments) return [];
|
|
2281
|
+
return remote.materializeMessageAttachments(
|
|
2282
|
+
conversationId,
|
|
2283
|
+
messageId,
|
|
2284
|
+
workspaceDir
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
};
|
|
2288
|
+
return store;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
// src/http/maintainer-pro-store.ts
|
|
2292
|
+
async function mpFetch(baseUrl, apiKey, pathName, init, fetchImpl) {
|
|
2293
|
+
const url = `${baseUrl.replace(/\/$/, "")}${pathName}`;
|
|
2294
|
+
const res = await fetchImpl(url, {
|
|
2295
|
+
...init,
|
|
2296
|
+
headers: {
|
|
2297
|
+
Authorization: `Bearer ${apiKey}`,
|
|
2298
|
+
"Content-Type": "application/json",
|
|
2299
|
+
...init?.headers ?? {}
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
if (!res.ok) {
|
|
2303
|
+
const text = await res.text().catch(() => "");
|
|
2304
|
+
throw new Error(
|
|
2305
|
+
`Maintainer Pro ${pathName} failed (${res.status}): ${text || res.statusText}`
|
|
2306
|
+
);
|
|
2307
|
+
}
|
|
2308
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
2309
|
+
if (contentType.includes("application/json")) {
|
|
2310
|
+
return res.json();
|
|
2311
|
+
}
|
|
2312
|
+
return res;
|
|
1024
2313
|
}
|
|
1025
2314
|
function createMaintainerProStore(options) {
|
|
1026
2315
|
const baseUrl = options.baseUrl;
|
|
1027
2316
|
const apiKey = options.apiKey;
|
|
1028
2317
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1029
|
-
const
|
|
2318
|
+
const logger = options.logger;
|
|
2319
|
+
const tempDir = options.tempDir ?? projectUploadsDir({ workspaceDir: process.cwd() });
|
|
1030
2320
|
const store = {
|
|
1031
2321
|
async ensureConversation(id) {
|
|
1032
2322
|
await mpFetch(
|
|
@@ -1049,17 +2339,54 @@ function createMaintainerProStore(options) {
|
|
|
1049
2339
|
{
|
|
1050
2340
|
method: "POST",
|
|
1051
2341
|
body: JSON.stringify({
|
|
2342
|
+
id: input.id,
|
|
1052
2343
|
role: input.role,
|
|
1053
2344
|
content: input.content,
|
|
1054
2345
|
provider: input.provider,
|
|
1055
2346
|
senderType: input.senderType,
|
|
1056
2347
|
senderName: input.senderName,
|
|
1057
|
-
|
|
2348
|
+
parentMessageId: input.parentMessageId ?? void 0,
|
|
2349
|
+
attachmentIds,
|
|
2350
|
+
intent: input.intent,
|
|
2351
|
+
queueStatus: input.queueStatus ?? void 0
|
|
1058
2352
|
})
|
|
1059
2353
|
},
|
|
1060
2354
|
fetchImpl
|
|
1061
2355
|
);
|
|
1062
|
-
return
|
|
2356
|
+
return {
|
|
2357
|
+
...data.message,
|
|
2358
|
+
nextQueued: data.nextQueued ?? null
|
|
2359
|
+
};
|
|
2360
|
+
},
|
|
2361
|
+
async releaseWorkingTurn(conversationId, parentMessageId) {
|
|
2362
|
+
const data = await mpFetch(
|
|
2363
|
+
baseUrl,
|
|
2364
|
+
apiKey,
|
|
2365
|
+
`/api/v1/store/conversations/${encodeURIComponent(conversationId)}/complete-turn`,
|
|
2366
|
+
{
|
|
2367
|
+
method: "POST",
|
|
2368
|
+
body: JSON.stringify({
|
|
2369
|
+
parentMessageId: parentMessageId ?? void 0
|
|
2370
|
+
})
|
|
2371
|
+
},
|
|
2372
|
+
fetchImpl
|
|
2373
|
+
);
|
|
2374
|
+
return { nextQueued: data.nextQueued ?? null };
|
|
2375
|
+
},
|
|
2376
|
+
async recoverQueue(conversationId) {
|
|
2377
|
+
const data = await mpFetch(
|
|
2378
|
+
baseUrl,
|
|
2379
|
+
apiKey,
|
|
2380
|
+
`/api/v1/store/conversations/${encodeURIComponent(conversationId)}/recover-queue`,
|
|
2381
|
+
{ method: "POST", body: "{}" },
|
|
2382
|
+
fetchImpl
|
|
2383
|
+
);
|
|
2384
|
+
return {
|
|
2385
|
+
action: data.action ?? "noop",
|
|
2386
|
+
workingId: data.workingId ?? null,
|
|
2387
|
+
promotedId: data.promotedId ?? null,
|
|
2388
|
+
nextQueued: data.nextQueued ?? null
|
|
2389
|
+
};
|
|
1063
2390
|
},
|
|
1064
2391
|
async clearWorkingMessages(conversationId) {
|
|
1065
2392
|
await mpFetch(
|
|
@@ -1090,6 +2417,16 @@ function createMaintainerProStore(options) {
|
|
|
1090
2417
|
);
|
|
1091
2418
|
return data.conversations ?? [];
|
|
1092
2419
|
},
|
|
2420
|
+
async listSnapshot() {
|
|
2421
|
+
const data = await mpFetch(
|
|
2422
|
+
baseUrl,
|
|
2423
|
+
apiKey,
|
|
2424
|
+
`/api/v1/store/snapshot`,
|
|
2425
|
+
{ method: "GET" },
|
|
2426
|
+
fetchImpl
|
|
2427
|
+
);
|
|
2428
|
+
return data.conversations ?? [];
|
|
2429
|
+
},
|
|
1093
2430
|
async saveToolEvents(conversationId, events) {
|
|
1094
2431
|
if (!events.length) return;
|
|
1095
2432
|
await mpFetch(
|
|
@@ -1130,28 +2467,131 @@ function createMaintainerProStore(options) {
|
|
|
1130
2467
|
const id = data.attachment.id;
|
|
1131
2468
|
attachmentIds.push(id);
|
|
1132
2469
|
refs.push(data.attachment.ref || `maintainer-pro://${id}`);
|
|
1133
|
-
const
|
|
1134
|
-
const localPath = import_node_path4.default.join(
|
|
2470
|
+
const localPath = import_node_path6.default.join(
|
|
1135
2471
|
tempDir,
|
|
1136
|
-
`${conversationId}-${id}${
|
|
2472
|
+
`${conversationId}-${id}${extForMime(item.mimeType)}`
|
|
1137
2473
|
);
|
|
2474
|
+
await import_promises3.default.mkdir(import_node_path6.default.dirname(localPath), { recursive: true });
|
|
1138
2475
|
await import_promises3.default.writeFile(localPath, Buffer.from(item.data, "base64"));
|
|
1139
2476
|
localPaths.push(localPath);
|
|
1140
2477
|
}
|
|
1141
2478
|
return { refs, localPaths, attachmentIds };
|
|
2479
|
+
},
|
|
2480
|
+
async materializeMessageAttachments(conversationId, messageId, workspaceDir) {
|
|
2481
|
+
if (!conversationId || !workspaceDir) return [];
|
|
2482
|
+
const data = await mpFetch(
|
|
2483
|
+
baseUrl,
|
|
2484
|
+
apiKey,
|
|
2485
|
+
`/api/v1/store/conversations/${encodeURIComponent(conversationId)}/messages`,
|
|
2486
|
+
{ method: "GET" },
|
|
2487
|
+
fetchImpl
|
|
2488
|
+
);
|
|
2489
|
+
const messages = data.messages ?? [];
|
|
2490
|
+
const byId = new Map(messages.map((row) => [row.id, row]));
|
|
2491
|
+
const refs = [];
|
|
2492
|
+
const fromMessageIds = [];
|
|
2493
|
+
const seenMsg = /* @__PURE__ */ new Set();
|
|
2494
|
+
const pushRow = (row) => {
|
|
2495
|
+
if (!row || seenMsg.has(row.id)) return;
|
|
2496
|
+
seenMsg.add(row.id);
|
|
2497
|
+
const before = refs.length;
|
|
2498
|
+
refs.push(...row.attachmentPaths ?? []);
|
|
2499
|
+
refs.push(
|
|
2500
|
+
...(row.attachmentIds ?? []).map((id) => `maintainer-pro://${id}`)
|
|
2501
|
+
);
|
|
2502
|
+
if (refs.length > before) fromMessageIds.push(row.id);
|
|
2503
|
+
};
|
|
2504
|
+
let current = messageId ? byId.get(messageId) : void 0;
|
|
2505
|
+
while (current && !seenMsg.has(current.id)) {
|
|
2506
|
+
pushRow(current);
|
|
2507
|
+
const parentId = current.parentMessageId?.trim();
|
|
2508
|
+
current = parentId ? byId.get(parentId) : void 0;
|
|
2509
|
+
}
|
|
2510
|
+
for (const row of messages) {
|
|
2511
|
+
if (row.role && row.role !== "user") continue;
|
|
2512
|
+
pushRow(row);
|
|
2513
|
+
}
|
|
2514
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2515
|
+
const ids = [];
|
|
2516
|
+
for (const ref of refs) {
|
|
2517
|
+
const id = attachmentIdFromRef(ref);
|
|
2518
|
+
if (!id || seen.has(id)) continue;
|
|
2519
|
+
seen.add(id);
|
|
2520
|
+
ids.push(id);
|
|
2521
|
+
}
|
|
2522
|
+
const localPaths = [];
|
|
2523
|
+
for (const id of ids.slice(0, 5)) {
|
|
2524
|
+
const url = `${baseUrl.replace(/\/$/, "")}/api/v1/store/attachments/${encodeURIComponent(id)}`;
|
|
2525
|
+
const res = await fetchImpl(url, {
|
|
2526
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
2527
|
+
});
|
|
2528
|
+
if (!res.ok) {
|
|
2529
|
+
continue;
|
|
2530
|
+
}
|
|
2531
|
+
const mime = res.headers.get("content-type") || "image/png";
|
|
2532
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
2533
|
+
if (!buffer.byteLength) continue;
|
|
2534
|
+
const localPath = import_node_path6.default.join(
|
|
2535
|
+
tempDir,
|
|
2536
|
+
`${id}${extForMime(mime)}`
|
|
2537
|
+
);
|
|
2538
|
+
await import_promises3.default.mkdir(tempDir, { recursive: true });
|
|
2539
|
+
await import_promises3.default.writeFile(localPath, buffer);
|
|
2540
|
+
localPaths.push(localPath);
|
|
2541
|
+
}
|
|
2542
|
+
logger?.debug(
|
|
2543
|
+
{
|
|
2544
|
+
conversationId,
|
|
2545
|
+
messageId,
|
|
2546
|
+
fromMessageIds,
|
|
2547
|
+
stored: ids.length,
|
|
2548
|
+
written: localPaths.length
|
|
2549
|
+
},
|
|
2550
|
+
"materialize attachments"
|
|
2551
|
+
);
|
|
2552
|
+
return localPaths;
|
|
1142
2553
|
}
|
|
1143
2554
|
};
|
|
1144
|
-
|
|
2555
|
+
if (options.sync === false) {
|
|
2556
|
+
return Object.assign(store, {
|
|
2557
|
+
ready: Promise.resolve({ conversations: 0, messages: 0 }),
|
|
2558
|
+
stop() {
|
|
2559
|
+
}
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2562
|
+
return createSyncedChatStore({
|
|
2563
|
+
remote: store,
|
|
2564
|
+
maintainerProUrl: baseUrl,
|
|
2565
|
+
apiKey,
|
|
2566
|
+
logger: options.logger,
|
|
2567
|
+
log: options.log,
|
|
2568
|
+
onWorkingTurn: options.onWorkingTurn
|
|
2569
|
+
});
|
|
1145
2570
|
}
|
|
1146
|
-
function createMaintainerProStoreFromEnv() {
|
|
2571
|
+
function createMaintainerProStoreFromEnv(options) {
|
|
1147
2572
|
const baseUrl = process.env.MAINTAINER_PRO_URL?.trim();
|
|
1148
2573
|
const apiKey = process.env.MAINTAINER_PRO_API_KEY?.trim();
|
|
1149
2574
|
if (!baseUrl || !apiKey) return null;
|
|
1150
|
-
return createMaintainerProStore({
|
|
2575
|
+
return createMaintainerProStore({
|
|
2576
|
+
baseUrl,
|
|
2577
|
+
apiKey,
|
|
2578
|
+
tempDir: projectUploadsDir({
|
|
2579
|
+
workspaceDir: process.env.AI_CLI_WORKSPACE || process.cwd(),
|
|
2580
|
+
sandboxId: process.env.MAINTAINER_PRO_SANDBOX_ID,
|
|
2581
|
+
dataDir: process.env.MAINTAINER_PRO_DATA_DIR
|
|
2582
|
+
}),
|
|
2583
|
+
logger: options?.logger,
|
|
2584
|
+
log: options?.log,
|
|
2585
|
+
sync: options?.sync,
|
|
2586
|
+
onWorkingTurn: options?.onWorkingTurn
|
|
2587
|
+
});
|
|
1151
2588
|
}
|
|
1152
2589
|
|
|
1153
2590
|
// src/workspace-inspect.ts
|
|
2591
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
2592
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
1154
2593
|
var import_zod = require("zod");
|
|
2594
|
+
var CONFIG_INSPECT_TIMEOUT_MS = 18e3;
|
|
1155
2595
|
var inspectJsonSchema = import_zod.z.object({
|
|
1156
2596
|
kind: import_zod.z.enum(["next", "vite", "html", "empty", "other"]).optional(),
|
|
1157
2597
|
name: import_zod.z.string().max(200).optional(),
|
|
@@ -1221,6 +2661,154 @@ ${input.extraContext ? `${input.extraContext.trim()}
|
|
|
1221
2661
|
|
|
1222
2662
|
` : ""}Inspect this project, fix any setup gaps you can, and return the JSON report.`;
|
|
1223
2663
|
}
|
|
2664
|
+
function readSnippet(file, max = 4e3) {
|
|
2665
|
+
try {
|
|
2666
|
+
const text = import_node_fs3.default.readFileSync(file, "utf8");
|
|
2667
|
+
return text.length > max ? `${text.slice(0, max)}
|
|
2668
|
+
\u2026` : text;
|
|
2669
|
+
} catch {
|
|
2670
|
+
return null;
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
function envKeysOnly(file) {
|
|
2674
|
+
try {
|
|
2675
|
+
const keys = import_node_fs3.default.readFileSync(file, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && line.includes("=")).map((line) => line.slice(0, line.indexOf("=")).trim()).filter(Boolean);
|
|
2676
|
+
return keys.length ? keys.join("\n") : null;
|
|
2677
|
+
} catch {
|
|
2678
|
+
return null;
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
function gatherConfigSnippets(workspaceDir) {
|
|
2682
|
+
const folder = import_node_path7.default.resolve(workspaceDir);
|
|
2683
|
+
const parts = [];
|
|
2684
|
+
const pkg = readSnippet(import_node_path7.default.join(folder, "package.json"));
|
|
2685
|
+
if (pkg) parts.push(`### package.json
|
|
2686
|
+
\`\`\`json
|
|
2687
|
+
${pkg}
|
|
2688
|
+
\`\`\``);
|
|
2689
|
+
for (const lock of [
|
|
2690
|
+
"package-lock.json",
|
|
2691
|
+
"pnpm-lock.yaml",
|
|
2692
|
+
"yarn.lock",
|
|
2693
|
+
"bun.lockb"
|
|
2694
|
+
]) {
|
|
2695
|
+
if (import_node_fs3.default.existsSync(import_node_path7.default.join(folder, lock))) {
|
|
2696
|
+
parts.push(`### lockfile
|
|
2697
|
+
${lock}`);
|
|
2698
|
+
break;
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
for (const rel of [
|
|
2702
|
+
"vite.config.ts",
|
|
2703
|
+
"vite.config.js",
|
|
2704
|
+
"vite.config.mjs",
|
|
2705
|
+
"next.config.js",
|
|
2706
|
+
"next.config.mjs",
|
|
2707
|
+
"next.config.ts"
|
|
2708
|
+
]) {
|
|
2709
|
+
const body = readSnippet(import_node_path7.default.join(folder, rel), 2500);
|
|
2710
|
+
if (body) parts.push(`### ${rel}
|
|
2711
|
+
\`\`\`
|
|
2712
|
+
${body}
|
|
2713
|
+
\`\`\``);
|
|
2714
|
+
}
|
|
2715
|
+
for (const rel of [".env.example", ".env", ".env.local", ".env.development"]) {
|
|
2716
|
+
const keys = envKeysOnly(import_node_path7.default.join(folder, rel));
|
|
2717
|
+
if (keys) parts.push(`### ${rel} keys (values omitted)
|
|
2718
|
+
\`\`\`
|
|
2719
|
+
${keys}
|
|
2720
|
+
\`\`\``);
|
|
2721
|
+
}
|
|
2722
|
+
return parts.join("\n\n") || "(no config files found)";
|
|
2723
|
+
}
|
|
2724
|
+
function configOnlySystemPrompt(input) {
|
|
2725
|
+
return `You are Maintainer Pro's read-only setup advisor.
|
|
2726
|
+
|
|
2727
|
+
Workspace: ${input.workspaceDir}
|
|
2728
|
+
App name: ${input.appName || "the app"}
|
|
2729
|
+
|
|
2730
|
+
You are given configuration file excerpts only.
|
|
2731
|
+
DO NOT edit, create, or delete any files.
|
|
2732
|
+
DO NOT run shell commands, install packages, or start servers.
|
|
2733
|
+
Only infer how this project runs locally from the excerpts.
|
|
2734
|
+
|
|
2735
|
+
Script names you report MUST appear in package.json scripts. Omit chat/ai-server scripts.
|
|
2736
|
+
|
|
2737
|
+
Reply with a short human summary AND one \`\`\`json fence with:
|
|
2738
|
+
|
|
2739
|
+
{
|
|
2740
|
+
"kind": "next" | "vite" | "html" | "empty" | "other",
|
|
2741
|
+
"name": "package or folder name",
|
|
2742
|
+
"summary": "one sentence about this project",
|
|
2743
|
+
"scripts": { "ui": "dev:client", "backend": "dev:server" },
|
|
2744
|
+
"ports": { "ui": 5173, "backend": 4100 },
|
|
2745
|
+
"issues": ["uncertainties"],
|
|
2746
|
+
"fixes": [],
|
|
2747
|
+
"ready": true
|
|
2748
|
+
}`;
|
|
2749
|
+
}
|
|
2750
|
+
function configOnlyUserMessage(input, snippets) {
|
|
2751
|
+
return `${input.extraContext ? `${input.extraContext.trim()}
|
|
2752
|
+
|
|
2753
|
+
` : ""}Analyze these config excerpts and suggest local apps/ports/scripts. Do not change anything on disk.
|
|
2754
|
+
|
|
2755
|
+
${snippets}`;
|
|
2756
|
+
}
|
|
2757
|
+
function toInspectResult(input, text, providerId) {
|
|
2758
|
+
const parsed = extractInspectJson(text);
|
|
2759
|
+
return {
|
|
2760
|
+
kind: parsed?.kind ?? "other",
|
|
2761
|
+
name: parsed?.name?.trim() || input.appName || "",
|
|
2762
|
+
summary: parsed?.summary?.trim() || text.replace(/```json[\s\S]*```/i, "").trim().slice(0, 400),
|
|
2763
|
+
scripts: parsed?.scripts ?? {},
|
|
2764
|
+
ports: parsed?.ports ?? {},
|
|
2765
|
+
issues: parsed?.issues ?? [],
|
|
2766
|
+
fixes: parsed?.fixes ?? [],
|
|
2767
|
+
ready: parsed?.ready ?? parsed?.issues?.length === 0,
|
|
2768
|
+
provider: providerId,
|
|
2769
|
+
rawText: text
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
async function inspectConfigOnly(input, opts = {}) {
|
|
2773
|
+
const workspaceDir = import_node_path7.default.resolve(input.workspaceDir);
|
|
2774
|
+
const timeoutMs = opts.timeoutMs ?? CONFIG_INSPECT_TIMEOUT_MS;
|
|
2775
|
+
const provider = await resolveProvider({ preference: "auto" });
|
|
2776
|
+
const snippets = gatherConfigSnippets(workspaceDir);
|
|
2777
|
+
const call = callAi(
|
|
2778
|
+
[{ role: "user", content: configOnlyUserMessage(input, snippets) }],
|
|
2779
|
+
{
|
|
2780
|
+
route: "/local-setup-propose",
|
|
2781
|
+
pageTitle: input.appName || "Setup suggestions",
|
|
2782
|
+
relevantFiles: ["package.json", ".env.example"],
|
|
2783
|
+
data: {
|
|
2784
|
+
workspaceDir,
|
|
2785
|
+
readOnly: true
|
|
2786
|
+
}
|
|
2787
|
+
},
|
|
2788
|
+
{
|
|
2789
|
+
systemPrompt: configOnlySystemPrompt(input),
|
|
2790
|
+
workspaceDir,
|
|
2791
|
+
technical: true
|
|
2792
|
+
}
|
|
2793
|
+
);
|
|
2794
|
+
const timed = await Promise.race([
|
|
2795
|
+
call.then((response) => ({ ok: true, response })),
|
|
2796
|
+
new Promise(
|
|
2797
|
+
(resolve) => setTimeout(
|
|
2798
|
+
() => resolve({ ok: false, error: `Config inspect timed out after ${timeoutMs}ms` }),
|
|
2799
|
+
timeoutMs
|
|
2800
|
+
)
|
|
2801
|
+
)
|
|
2802
|
+
]);
|
|
2803
|
+
if (!timed.ok) {
|
|
2804
|
+
throw new Error(timed.error);
|
|
2805
|
+
}
|
|
2806
|
+
return toInspectResult(
|
|
2807
|
+
input,
|
|
2808
|
+
timed.response.text,
|
|
2809
|
+
timed.response.provider || provider.id
|
|
2810
|
+
);
|
|
2811
|
+
}
|
|
1224
2812
|
async function inspectAndRepairWorkspace(input) {
|
|
1225
2813
|
const workspaceDir = input.workspaceDir;
|
|
1226
2814
|
const provider = await resolveProvider({ preference: "auto" });
|
|
@@ -1241,28 +2829,679 @@ async function inspectAndRepairWorkspace(input) {
|
|
|
1241
2829
|
technical: true
|
|
1242
2830
|
}
|
|
1243
2831
|
);
|
|
1244
|
-
|
|
2832
|
+
return toInspectResult(
|
|
2833
|
+
input,
|
|
2834
|
+
response.text,
|
|
2835
|
+
response.provider || provider.id
|
|
2836
|
+
);
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
// src/host-apps.ts
|
|
2840
|
+
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
2841
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
2842
|
+
var import_node_crypto4 = require("crypto");
|
|
2843
|
+
var COLLABORATER_DIR = ".collaborater";
|
|
2844
|
+
var AI_SERVER_APP_ID = "ai-server";
|
|
2845
|
+
var AI_SERVER_DEFAULT_PORT = 3100;
|
|
2846
|
+
var ENV_FILES = [".env", ".env.local", ".env.development", ".env.example"];
|
|
2847
|
+
function isPort(value) {
|
|
2848
|
+
return Number.isInteger(value) && Number(value) >= 1024 && Number(value) <= 65535;
|
|
2849
|
+
}
|
|
2850
|
+
function parsePort(value, fallback = 0) {
|
|
2851
|
+
if (typeof value === "number" && isPort(value)) return value;
|
|
2852
|
+
const text = String(value ?? "").trim();
|
|
2853
|
+
if (!text) return fallback;
|
|
2854
|
+
if (/^\d{2,5}$/.test(text)) {
|
|
2855
|
+
const n = Number(text);
|
|
2856
|
+
return isPort(n) ? n : fallback;
|
|
2857
|
+
}
|
|
2858
|
+
try {
|
|
2859
|
+
const port = Number(new URL(text).port);
|
|
2860
|
+
return isPort(port) ? port : fallback;
|
|
2861
|
+
} catch {
|
|
2862
|
+
const match = text.match(/--port(?:\s+|=)(\d{2,5})/i) || text.match(/\bPORT[=:]\s*(\d{2,5})/i) || text.match(/:(\d{2,5})\b/);
|
|
2863
|
+
const n = match ? Number(match[1]) : 0;
|
|
2864
|
+
return isPort(n) ? n : fallback;
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
function dataInput(folder, extra) {
|
|
1245
2868
|
return {
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
2869
|
+
workspaceDir: folder,
|
|
2870
|
+
sandboxId: extra?.sandboxId,
|
|
2871
|
+
dataDir: extra?.dataDir
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
function hostAppsCachePath2(folder, extra) {
|
|
2875
|
+
return hostAppsCachePath(dataInput(folder, extra));
|
|
2876
|
+
}
|
|
2877
|
+
function defaultAiServerApp(port = AI_SERVER_DEFAULT_PORT) {
|
|
2878
|
+
return {
|
|
2879
|
+
id: AI_SERVER_APP_ID,
|
|
2880
|
+
name: "AI server",
|
|
2881
|
+
role: "ai-server",
|
|
2882
|
+
port: parsePort(port, AI_SERVER_DEFAULT_PORT),
|
|
2883
|
+
startCommand: null,
|
|
2884
|
+
source: "default",
|
|
2885
|
+
locked: true
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
function normalizeHostApp(raw) {
|
|
2889
|
+
if (!raw || typeof raw !== "object") return null;
|
|
2890
|
+
const row = raw;
|
|
2891
|
+
const port = parsePort(row.port);
|
|
2892
|
+
if (!port) return null;
|
|
2893
|
+
const role = String(row.role || "custom");
|
|
2894
|
+
const allowed = ["ai-server", "ui", "backend", "app", "custom"];
|
|
2895
|
+
const id = String(row.id || "").trim() || (role === "ai-server" ? AI_SERVER_APP_ID : "");
|
|
2896
|
+
if (!id) return null;
|
|
2897
|
+
return {
|
|
2898
|
+
id: id.slice(0, 64),
|
|
2899
|
+
name: String(row.name || id).trim().slice(0, 80) || id,
|
|
2900
|
+
role: allowed.includes(role) ? role : "custom",
|
|
2901
|
+
port,
|
|
2902
|
+
startCommand: typeof row.startCommand === "string" && row.startCommand.trim() ? row.startCommand.trim().slice(0, 200) : null,
|
|
2903
|
+
source: ["default", "env", "package", "ai", "manual"].includes(
|
|
2904
|
+
row.source
|
|
2905
|
+
) ? row.source : "manual",
|
|
2906
|
+
locked: row.locked === true || id === AI_SERVER_APP_ID || role === "ai-server",
|
|
2907
|
+
host: role !== "ai-server" && id !== AI_SERVER_APP_ID && row.host === true,
|
|
2908
|
+
envMaps: normalizeEnvMaps(row.envMaps)
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
function normalizeEnvMaps(raw) {
|
|
2912
|
+
if (!Array.isArray(raw)) return [];
|
|
2913
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2914
|
+
const out = [];
|
|
2915
|
+
for (const item of raw) {
|
|
2916
|
+
if (!item || typeof item !== "object") continue;
|
|
2917
|
+
const row = item;
|
|
2918
|
+
const key = String(row.key || "").trim();
|
|
2919
|
+
const sourceAppId = String(row.sourceAppId || "").trim().slice(0, 64);
|
|
2920
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || key.length > 80) continue;
|
|
2921
|
+
if (!sourceAppId || seen.has(key)) continue;
|
|
2922
|
+
seen.add(key);
|
|
2923
|
+
out.push({ key, sourceAppId });
|
|
2924
|
+
}
|
|
2925
|
+
return out.slice(0, 30);
|
|
2926
|
+
}
|
|
2927
|
+
function ensureSingleHost(apps) {
|
|
2928
|
+
const next = apps.map((app) => ({
|
|
2929
|
+
...app,
|
|
2930
|
+
host: app.role !== "ai-server" && app.id !== AI_SERVER_APP_ID && app.host === true
|
|
2931
|
+
}));
|
|
2932
|
+
const marked = next.filter((app) => app.host);
|
|
2933
|
+
if (marked.length === 1) return next;
|
|
2934
|
+
if (marked.length > 1) {
|
|
2935
|
+
let kept = false;
|
|
2936
|
+
return next.map((app) => {
|
|
2937
|
+
if (!app.host) return app;
|
|
2938
|
+
if (kept) return { ...app, host: false };
|
|
2939
|
+
kept = true;
|
|
2940
|
+
return app;
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
const fallback = next.find((app) => app.role === "ui" || app.role === "app") || next.find((app) => app.role !== "ai-server");
|
|
2944
|
+
return next.map(
|
|
2945
|
+
(app) => fallback && app.id === fallback.id ? { ...app, host: true } : app
|
|
2946
|
+
);
|
|
2947
|
+
}
|
|
2948
|
+
function normalizeHostApps(raw) {
|
|
2949
|
+
if (!Array.isArray(raw)) return [];
|
|
2950
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2951
|
+
const apps = [];
|
|
2952
|
+
for (const item of raw) {
|
|
2953
|
+
const app = normalizeHostApp(item);
|
|
2954
|
+
if (!app || seen.has(app.id)) continue;
|
|
2955
|
+
seen.add(app.id);
|
|
2956
|
+
apps.push(app);
|
|
2957
|
+
}
|
|
2958
|
+
return apps;
|
|
2959
|
+
}
|
|
2960
|
+
function ensureAiServerApp(apps, preferredPort = AI_SERVER_DEFAULT_PORT) {
|
|
2961
|
+
const next = normalizeHostApps(apps);
|
|
2962
|
+
const existing = next.find((app) => app.id === AI_SERVER_APP_ID || app.role === "ai-server");
|
|
2963
|
+
if (existing) {
|
|
2964
|
+
existing.id = AI_SERVER_APP_ID;
|
|
2965
|
+
existing.role = "ai-server";
|
|
2966
|
+
existing.locked = true;
|
|
2967
|
+
existing.host = false;
|
|
2968
|
+
existing.name = existing.name || "AI server";
|
|
2969
|
+
return ensureSingleHost([existing, ...next.filter((app) => app !== existing)]);
|
|
2970
|
+
}
|
|
2971
|
+
return ensureSingleHost([defaultAiServerApp(preferredPort), ...next]);
|
|
2972
|
+
}
|
|
2973
|
+
function readText(file) {
|
|
2974
|
+
try {
|
|
2975
|
+
return import_node_fs4.default.readFileSync(file, "utf8");
|
|
2976
|
+
} catch {
|
|
2977
|
+
return null;
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
function readEnvFileMap(file) {
|
|
2981
|
+
const text = readText(file);
|
|
2982
|
+
if (!text) return {};
|
|
2983
|
+
const map = {};
|
|
2984
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
2985
|
+
const line = raw.trim();
|
|
2986
|
+
if (!line || line.startsWith("#")) continue;
|
|
2987
|
+
const eq = line.indexOf("=");
|
|
2988
|
+
if (eq < 1) continue;
|
|
2989
|
+
const key = line.slice(0, eq).trim();
|
|
2990
|
+
let value = line.slice(eq + 1).trim();
|
|
2991
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
2992
|
+
value = value.slice(1, -1);
|
|
2993
|
+
}
|
|
2994
|
+
if (key) map[key] = value;
|
|
2995
|
+
}
|
|
2996
|
+
return map;
|
|
2997
|
+
}
|
|
2998
|
+
function readProjectEnvLayers(folder) {
|
|
2999
|
+
const resolved = import_node_path8.default.resolve(folder);
|
|
3000
|
+
const layers = [];
|
|
3001
|
+
const merged = {};
|
|
3002
|
+
for (const name of ENV_FILES) {
|
|
3003
|
+
const file = import_node_path8.default.join(resolved, name);
|
|
3004
|
+
const values = readEnvFileMap(file);
|
|
3005
|
+
if (!Object.keys(values).length) continue;
|
|
3006
|
+
layers.push({ file: name, values });
|
|
3007
|
+
if (name === ".env.example") continue;
|
|
3008
|
+
Object.assign(merged, values);
|
|
3009
|
+
}
|
|
3010
|
+
return { merged, layers };
|
|
3011
|
+
}
|
|
3012
|
+
function readPackageJson(folder) {
|
|
3013
|
+
try {
|
|
3014
|
+
return JSON.parse(import_node_fs4.default.readFileSync(import_node_path8.default.join(folder, "package.json"), "utf8"));
|
|
3015
|
+
} catch {
|
|
3016
|
+
return null;
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
function isChatScript(name, command) {
|
|
3020
|
+
return /\b(ai-server|ai-cli|dev:chat|start:chat)\b/i.test(`${name} ${command}`);
|
|
3021
|
+
}
|
|
3022
|
+
function isUiCommand(command) {
|
|
3023
|
+
return /\b(vite|next|nuxt|astro|remix|react-scripts|webpack-dev-server|parcel)\b/i.test(
|
|
3024
|
+
command
|
|
3025
|
+
);
|
|
3026
|
+
}
|
|
3027
|
+
function configPort(folder, files) {
|
|
3028
|
+
for (const rel of files) {
|
|
3029
|
+
const text = readText(import_node_path8.default.join(folder, rel));
|
|
3030
|
+
if (!text) continue;
|
|
3031
|
+
const match = text.match(/\bport\s*[:=]\s*(\d{2,5})/i) || text.match(/--port(?:\s+|=)(\d{2,5})/i);
|
|
3032
|
+
const port = parsePort(match?.[1]);
|
|
3033
|
+
if (port) return port;
|
|
3034
|
+
}
|
|
3035
|
+
return 0;
|
|
3036
|
+
}
|
|
3037
|
+
function hostAppsFingerprint(folder) {
|
|
3038
|
+
const resolved = import_node_path8.default.resolve(folder);
|
|
3039
|
+
const parts = [];
|
|
3040
|
+
for (const rel of [
|
|
3041
|
+
"package.json",
|
|
3042
|
+
...ENV_FILES,
|
|
3043
|
+
"vite.config.ts",
|
|
3044
|
+
"vite.config.js",
|
|
3045
|
+
"vite.config.mjs",
|
|
3046
|
+
"next.config.js",
|
|
3047
|
+
"next.config.mjs",
|
|
3048
|
+
"next.config.ts"
|
|
3049
|
+
]) {
|
|
3050
|
+
const file = import_node_path8.default.join(resolved, rel);
|
|
3051
|
+
if (!import_node_fs4.default.existsSync(file)) continue;
|
|
3052
|
+
try {
|
|
3053
|
+
const st = import_node_fs4.default.statSync(file);
|
|
3054
|
+
parts.push(`${rel}:${st.size}:${Math.floor(st.mtimeMs)}`);
|
|
3055
|
+
} catch {
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
return (0, import_node_crypto4.createHash)("sha256").update(parts.join("|") || resolved).digest("hex").slice(0, 32);
|
|
3059
|
+
}
|
|
3060
|
+
function pickScript(scripts, names) {
|
|
3061
|
+
return names.find((name) => scripts[name] && !isChatScript(name, scripts[name])) || null;
|
|
3062
|
+
}
|
|
3063
|
+
function detectHostAppsFromFiles(folder, opts = {}) {
|
|
3064
|
+
const resolved = import_node_path8.default.resolve(folder);
|
|
3065
|
+
const reasons = [];
|
|
3066
|
+
const { merged, layers } = readProjectEnvLayers(resolved);
|
|
3067
|
+
const pkg = readPackageJson(resolved);
|
|
3068
|
+
const scripts = pkg?.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
|
|
3069
|
+
const apps = [];
|
|
3070
|
+
const envAi = parsePort(merged.AI_SERVER_PORT) || parsePort(merged.AI_SERVER_URL);
|
|
3071
|
+
const aiPort = envAi || parsePort(opts.preferredAiPort, AI_SERVER_DEFAULT_PORT);
|
|
3072
|
+
apps.push({
|
|
3073
|
+
...defaultAiServerApp(aiPort),
|
|
3074
|
+
source: envAi ? "env" : "default"
|
|
3075
|
+
});
|
|
3076
|
+
const envPorts = {
|
|
3077
|
+
PORT: layers.filter((layer) => layer.file !== ".env.example").map((layer) => ({ file: layer.file, port: parsePort(layer.values.PORT) })).filter((row) => row.port),
|
|
3078
|
+
APP_URL: parsePort(merged.APP_URL || merged.NEXT_PUBLIC_APP_URL || merged.PUBLIC_URL),
|
|
3079
|
+
API_PORT: parsePort(merged.API_PORT || merged.API_URL || merged.VITE_API_URL)
|
|
3080
|
+
};
|
|
3081
|
+
const portValues = [...new Set(envPorts.PORT.map((row) => row.port))];
|
|
3082
|
+
if (portValues.length > 1) {
|
|
3083
|
+
reasons.push(
|
|
3084
|
+
`PORT differs across env files: ${envPorts.PORT.map((row) => `${row.file}=${row.port}`).join(", ")}`
|
|
3085
|
+
);
|
|
3086
|
+
}
|
|
3087
|
+
if (envPorts.PORT[0] && envPorts.APP_URL && envPorts.PORT[0].port !== envPorts.APP_URL) {
|
|
3088
|
+
reasons.push(
|
|
3089
|
+
`PORT=${envPorts.PORT[0].port} does not match APP_URL port ${envPorts.APP_URL}`
|
|
3090
|
+
);
|
|
3091
|
+
}
|
|
3092
|
+
const uiScript = pickScript(scripts, [
|
|
3093
|
+
"dev:client",
|
|
3094
|
+
"dev:ui",
|
|
3095
|
+
"dev:web",
|
|
3096
|
+
"dev:frontend",
|
|
3097
|
+
"client",
|
|
3098
|
+
"start:client"
|
|
3099
|
+
]) || (scripts.dev && !isChatScript("dev", scripts.dev) ? "dev" : null);
|
|
3100
|
+
const apiScript = pickScript(scripts, [
|
|
3101
|
+
"dev:server",
|
|
3102
|
+
"dev:backend",
|
|
3103
|
+
"dev:api",
|
|
3104
|
+
"server",
|
|
3105
|
+
"start:server"
|
|
3106
|
+
]);
|
|
3107
|
+
const vitePort = configPort(resolved, ["vite.config.ts", "vite.config.js", "vite.config.mjs"]) || 5173;
|
|
3108
|
+
const nextPort = configPort(resolved, ["next.config.js", "next.config.mjs", "next.config.ts"]) || 3e3;
|
|
3109
|
+
const portFromEnv = parsePort(merged.PORT);
|
|
3110
|
+
const hostFromEnv = parsePort(merged.HOST_PORT) || parsePort(merged.HOST_URL) || parsePort(merged.CORS_ORIGIN);
|
|
3111
|
+
const uiFromEnv = hostFromEnv || envPorts.APP_URL || (portFromEnv && portFromEnv !== aiPort ? portFromEnv : 0);
|
|
3112
|
+
if (uiScript) {
|
|
3113
|
+
const command = scripts[uiScript] || "";
|
|
3114
|
+
const scriptPort = parsePort(command);
|
|
3115
|
+
const fallback = isUiCommand(command) && /vite/i.test(command) ? vitePort : nextPort;
|
|
3116
|
+
const port = uiFromEnv || scriptPort || fallback;
|
|
3117
|
+
apps.push({
|
|
3118
|
+
id: "ui",
|
|
3119
|
+
name: opts.appName || pkg?.name || "App",
|
|
3120
|
+
role: isUiCommand(command) ? "ui" : "app",
|
|
3121
|
+
port,
|
|
3122
|
+
startCommand: `npm run ${uiScript}`,
|
|
3123
|
+
source: uiFromEnv ? "env" : scriptPort ? "package" : "package",
|
|
3124
|
+
host: true
|
|
3125
|
+
});
|
|
3126
|
+
} else if (uiFromEnv) {
|
|
3127
|
+
apps.push({
|
|
3128
|
+
id: "ui",
|
|
3129
|
+
name: opts.appName || pkg?.name || "App",
|
|
3130
|
+
role: "ui",
|
|
3131
|
+
port: uiFromEnv,
|
|
3132
|
+
startCommand: scripts.dev ? "npm run dev" : null,
|
|
3133
|
+
source: "env",
|
|
3134
|
+
host: true
|
|
3135
|
+
});
|
|
3136
|
+
}
|
|
3137
|
+
if (apiScript && apiScript !== uiScript) {
|
|
3138
|
+
const command = scripts[apiScript] || "";
|
|
3139
|
+
const port = envPorts.API_PORT || parsePort(command) || configPort(resolved, []) || 4100;
|
|
3140
|
+
apps.push({
|
|
3141
|
+
id: "backend",
|
|
3142
|
+
name: "Backend",
|
|
3143
|
+
role: "backend",
|
|
3144
|
+
port,
|
|
3145
|
+
startCommand: `npm run ${apiScript}`,
|
|
3146
|
+
source: envPorts.API_PORT ? "env" : "package"
|
|
3147
|
+
});
|
|
3148
|
+
}
|
|
3149
|
+
const hostApps = apps.filter((app) => app.role !== "ai-server");
|
|
3150
|
+
if (pkg && Object.keys(scripts).length && hostApps.length === 0) {
|
|
3151
|
+
reasons.push("Found package.json scripts but no host app port in env or config");
|
|
3152
|
+
}
|
|
3153
|
+
return {
|
|
3154
|
+
apps: ensureAiServerApp(apps, aiPort),
|
|
3155
|
+
reasons,
|
|
3156
|
+
confused: reasons.length > 0
|
|
3157
|
+
};
|
|
3158
|
+
}
|
|
3159
|
+
function readHostAppsCache(folder, extra) {
|
|
3160
|
+
try {
|
|
3161
|
+
const raw = JSON.parse(
|
|
3162
|
+
import_node_fs4.default.readFileSync(hostAppsCachePath2(folder, extra), "utf8")
|
|
3163
|
+
);
|
|
3164
|
+
const apps = ensureAiServerApp(normalizeHostApps(raw.apps));
|
|
3165
|
+
if (!apps.length) return null;
|
|
3166
|
+
return {
|
|
3167
|
+
apps,
|
|
3168
|
+
fingerprint: typeof raw.fingerprint === "string" ? raw.fingerprint : void 0,
|
|
3169
|
+
updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : void 0
|
|
3170
|
+
};
|
|
3171
|
+
} catch {
|
|
3172
|
+
return null;
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
var readCollaboraterApps = readHostAppsCache;
|
|
3176
|
+
function writeHostAppsCache(folder, apps, extra = {}, loc) {
|
|
3177
|
+
const resolved = import_node_path8.default.resolve(folder);
|
|
3178
|
+
ensureProjectDataDir(dataInput(resolved, loc));
|
|
3179
|
+
const payload = {
|
|
3180
|
+
version: 1,
|
|
3181
|
+
fingerprint: hostAppsFingerprint(resolved),
|
|
3182
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3183
|
+
apps: ensureAiServerApp(apps),
|
|
3184
|
+
...extra
|
|
3185
|
+
};
|
|
3186
|
+
import_node_fs4.default.writeFileSync(
|
|
3187
|
+
hostAppsCachePath2(resolved, loc),
|
|
3188
|
+
`${JSON.stringify(payload, null, 2)}
|
|
3189
|
+
`,
|
|
3190
|
+
"utf8"
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3193
|
+
var writeCollaboraterApps = writeHostAppsCache;
|
|
3194
|
+
function mergeDesiredHostApps(desired, detected) {
|
|
3195
|
+
const byId = new Map(detected.map((app) => [app.id, app]));
|
|
3196
|
+
const byRole = new Map(detected.map((app) => [app.role, app]));
|
|
3197
|
+
const merged = desired.map((app) => {
|
|
3198
|
+
const local = byId.get(app.id) || (app.role !== "custom" ? byRole.get(app.role) : void 0);
|
|
3199
|
+
return {
|
|
3200
|
+
...local,
|
|
3201
|
+
...app,
|
|
3202
|
+
startCommand: app.startCommand || local?.startCommand || null,
|
|
3203
|
+
locked: app.locked || app.id === AI_SERVER_APP_ID || app.role === "ai-server",
|
|
3204
|
+
host: app.role === "ai-server" || app.id === AI_SERVER_APP_ID ? false : app.host === true || app.host !== false && local?.host === true,
|
|
3205
|
+
envMaps: app.envMaps && app.envMaps.length ? app.envMaps : local?.envMaps || []
|
|
3206
|
+
};
|
|
3207
|
+
});
|
|
3208
|
+
return ensureAiServerApp(merged);
|
|
3209
|
+
}
|
|
3210
|
+
function appsFromInspectPorts(ports, scripts, name) {
|
|
3211
|
+
const apps = [];
|
|
3212
|
+
if (ports.ui || scripts.ui) {
|
|
3213
|
+
apps.push({
|
|
3214
|
+
id: "ui",
|
|
3215
|
+
name,
|
|
3216
|
+
role: "ui",
|
|
3217
|
+
port: parsePort(ports.ui, 5173),
|
|
3218
|
+
startCommand: scripts.ui ? `npm run ${scripts.ui}` : null,
|
|
3219
|
+
source: "ai"
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
3222
|
+
if (ports.backend || scripts.backend) {
|
|
3223
|
+
apps.push({
|
|
3224
|
+
id: "backend",
|
|
3225
|
+
name: "Backend",
|
|
3226
|
+
role: "backend",
|
|
3227
|
+
port: parsePort(ports.backend, 4100),
|
|
3228
|
+
startCommand: scripts.backend ? `npm run ${scripts.backend}` : null,
|
|
3229
|
+
source: "ai"
|
|
3230
|
+
});
|
|
3231
|
+
}
|
|
3232
|
+
if (!apps.length && (ports.app || scripts.app)) {
|
|
3233
|
+
apps.push({
|
|
3234
|
+
id: "app",
|
|
3235
|
+
name,
|
|
3236
|
+
role: "app",
|
|
3237
|
+
port: parsePort(ports.app, 3e3),
|
|
3238
|
+
startCommand: scripts.app ? `npm run ${scripts.app}` : null,
|
|
3239
|
+
source: "ai"
|
|
3240
|
+
});
|
|
3241
|
+
}
|
|
3242
|
+
return apps;
|
|
3243
|
+
}
|
|
3244
|
+
function collectScriptAlternatives(folder, primary, opts = {}) {
|
|
3245
|
+
const pkg = readPackageJson(folder);
|
|
3246
|
+
const scripts = pkg?.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
|
|
3247
|
+
const usedCommands = new Set(
|
|
3248
|
+
primary.map((app) => app.startCommand?.trim()).filter((cmd) => Boolean(cmd))
|
|
3249
|
+
);
|
|
3250
|
+
const alternatives = [];
|
|
3251
|
+
const hostApp = primary.find((app) => app.role === "ui" || app.role === "app") || primary.find((app) => app.role !== "ai-server");
|
|
3252
|
+
const backendApp = primary.find((app) => app.role === "backend");
|
|
3253
|
+
for (const [name, command] of Object.entries(scripts)) {
|
|
3254
|
+
if (isChatScript(name, command)) continue;
|
|
3255
|
+
if (!/^(dev|start|serve)/i.test(name) && !isUiCommand(command)) continue;
|
|
3256
|
+
const startCommand = `npm run ${name}`;
|
|
3257
|
+
if (usedCommands.has(startCommand)) continue;
|
|
3258
|
+
const scriptPort = parsePort(command);
|
|
3259
|
+
const looksBackend = /\b(server|api|backend)\b/i.test(`${name} ${command}`) && !isUiCommand(command);
|
|
3260
|
+
const target = looksBackend ? backendApp || hostApp : hostApp;
|
|
3261
|
+
if (!target) continue;
|
|
3262
|
+
const fallbackPort = scriptPort || (looksBackend ? 4100 : isUiCommand(command) && /vite/i.test(command) ? 5173 : 3e3);
|
|
3263
|
+
alternatives.push({
|
|
3264
|
+
appId: target.id,
|
|
3265
|
+
port: fallbackPort,
|
|
3266
|
+
startCommand,
|
|
3267
|
+
label: `${name} \u2192 ${fallbackPort}`,
|
|
3268
|
+
source: "package"
|
|
3269
|
+
});
|
|
3270
|
+
if (alternatives.length >= 8) break;
|
|
3271
|
+
}
|
|
3272
|
+
const { layers } = readProjectEnvLayers(folder);
|
|
3273
|
+
if (hostApp) {
|
|
3274
|
+
for (const layer of layers) {
|
|
3275
|
+
if (layer.file === ".env.example") continue;
|
|
3276
|
+
const port = parsePort(layer.values.PORT);
|
|
3277
|
+
if (!port || port === hostApp.port) continue;
|
|
3278
|
+
if (alternatives.some(
|
|
3279
|
+
(alt) => alt.appId === hostApp.id && alt.port === port && !alt.startCommand
|
|
3280
|
+
)) {
|
|
3281
|
+
continue;
|
|
3282
|
+
}
|
|
3283
|
+
alternatives.push({
|
|
3284
|
+
appId: hostApp.id,
|
|
3285
|
+
port,
|
|
3286
|
+
startCommand: hostApp.startCommand || null,
|
|
3287
|
+
label: `${layer.file} PORT=${port}`,
|
|
3288
|
+
source: "env"
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
void opts;
|
|
3293
|
+
return alternatives;
|
|
3294
|
+
}
|
|
3295
|
+
function proposalConfidence(apps, reasons, usedAi) {
|
|
3296
|
+
const hostApps = apps.filter((app) => app.role !== "ai-server");
|
|
3297
|
+
if (!hostApps.length) return "low";
|
|
3298
|
+
if (reasons.length === 0 && hostApps.every((app) => app.startCommand)) {
|
|
3299
|
+
return usedAi ? "medium" : "high";
|
|
3300
|
+
}
|
|
3301
|
+
if (reasons.some((reason) => /differ|does not match|no host app/i.test(reason))) {
|
|
3302
|
+
return "low";
|
|
3303
|
+
}
|
|
3304
|
+
return "medium";
|
|
3305
|
+
}
|
|
3306
|
+
function projectSummaryFromDetect(folder, apps, opts = {}) {
|
|
3307
|
+
const pkg = readPackageJson(folder);
|
|
3308
|
+
const name = opts.appName || pkg?.name || import_node_path8.default.basename(import_node_path8.default.resolve(folder));
|
|
3309
|
+
const hostApps = apps.filter((app) => app.role !== "ai-server");
|
|
3310
|
+
if (!hostApps.length) {
|
|
3311
|
+
return `${name}: no UI/backend ports detected from config yet.`;
|
|
3312
|
+
}
|
|
3313
|
+
return `${name}: ${hostApps.map((app) => `${app.name} on ${app.port}${app.startCommand ? ` (${app.startCommand})` : ""}`).join(", ")}.`;
|
|
3314
|
+
}
|
|
3315
|
+
async function proposeHostAppsFromConfig(input) {
|
|
3316
|
+
const folder = import_node_path8.default.resolve(input.workspaceDir);
|
|
3317
|
+
const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
|
|
3318
|
+
const fingerprint = hostAppsFingerprint(folder);
|
|
3319
|
+
const allowAi = input.allowAi !== false;
|
|
3320
|
+
const detected = detectHostAppsFromFiles(folder, {
|
|
3321
|
+
preferredAiPort: preferredAi,
|
|
3322
|
+
appName: input.appName
|
|
3323
|
+
});
|
|
3324
|
+
let apps = ensureAiServerApp(detected.apps, preferredAi);
|
|
3325
|
+
let reasons = [...detected.reasons];
|
|
3326
|
+
let usedAi = false;
|
|
3327
|
+
let projectSummary = projectSummaryFromDetect(folder, apps, {
|
|
3328
|
+
appName: input.appName
|
|
3329
|
+
});
|
|
3330
|
+
const hostCount = apps.filter((app) => app.role !== "ai-server").length;
|
|
3331
|
+
const needsAi = allowAi && (detected.confused || hostCount === 0 || reasons.some((reason) => /differ|does not match/i.test(reason)));
|
|
3332
|
+
if (needsAi) {
|
|
3333
|
+
try {
|
|
3334
|
+
const inspected = await inspectConfigOnly({
|
|
3335
|
+
workspaceDir: folder,
|
|
3336
|
+
appName: input.appName,
|
|
3337
|
+
extraContext: [
|
|
3338
|
+
"File-based detection was uncertain or incomplete.",
|
|
3339
|
+
...reasons
|
|
3340
|
+
].join("\n")
|
|
3341
|
+
});
|
|
3342
|
+
usedAi = true;
|
|
3343
|
+
const fromAi = appsFromInspectPorts(
|
|
3344
|
+
inspected.ports,
|
|
3345
|
+
inspected.scripts,
|
|
3346
|
+
inspected.name || input.appName || "App"
|
|
3347
|
+
);
|
|
3348
|
+
if (fromAi.length) {
|
|
3349
|
+
apps = ensureAiServerApp(
|
|
3350
|
+
[defaultAiServerApp(preferredAi), ...fromAi],
|
|
3351
|
+
preferredAi
|
|
3352
|
+
);
|
|
3353
|
+
}
|
|
3354
|
+
if (inspected.summary) projectSummary = inspected.summary;
|
|
3355
|
+
if (inspected.issues?.length) {
|
|
3356
|
+
reasons = [...reasons, ...inspected.issues];
|
|
3357
|
+
}
|
|
3358
|
+
} catch (err) {
|
|
3359
|
+
reasons.push(
|
|
3360
|
+
`AI suggest skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
3361
|
+
);
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
const alternatives = collectScriptAlternatives(folder, apps, {
|
|
3365
|
+
appName: input.appName
|
|
3366
|
+
});
|
|
3367
|
+
const confidence = proposalConfidence(apps, reasons, usedAi);
|
|
3368
|
+
const needsReview = confidence !== "high" || detected.confused || apps.filter((app) => app.role !== "ai-server").length === 0;
|
|
3369
|
+
return {
|
|
3370
|
+
apps,
|
|
3371
|
+
alternatives,
|
|
3372
|
+
reasons,
|
|
3373
|
+
confidence,
|
|
3374
|
+
projectSummary,
|
|
3375
|
+
usedAi,
|
|
3376
|
+
needsReview,
|
|
3377
|
+
fingerprint
|
|
3378
|
+
};
|
|
3379
|
+
}
|
|
3380
|
+
async function resolveHostApps(input) {
|
|
3381
|
+
const folder = import_node_path8.default.resolve(input.workspaceDir);
|
|
3382
|
+
const loc = { sandboxId: input.sandboxId, dataDir: input.dataDir };
|
|
3383
|
+
const fingerprint = hostAppsFingerprint(folder);
|
|
3384
|
+
const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
|
|
3385
|
+
const detected = detectHostAppsFromFiles(folder, {
|
|
3386
|
+
preferredAiPort: preferredAi,
|
|
3387
|
+
appName: input.appName
|
|
3388
|
+
});
|
|
3389
|
+
const desired = normalizeHostApps(input.desired);
|
|
3390
|
+
if (desired.length) {
|
|
3391
|
+
const apps2 = mergeDesiredHostApps(desired, detected.apps);
|
|
3392
|
+
try {
|
|
3393
|
+
writeHostAppsCache(folder, apps2, { source: "desired" }, loc);
|
|
3394
|
+
} catch {
|
|
3395
|
+
}
|
|
3396
|
+
return {
|
|
3397
|
+
apps: apps2,
|
|
3398
|
+
source: "desired",
|
|
3399
|
+
cached: false,
|
|
3400
|
+
usedAi: false,
|
|
3401
|
+
confused: false,
|
|
3402
|
+
reasons: [],
|
|
3403
|
+
fingerprint
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
if (!input.force) {
|
|
3407
|
+
const cached = readHostAppsCache(folder, loc);
|
|
3408
|
+
if (cached && cached.fingerprint === fingerprint) {
|
|
3409
|
+
return {
|
|
3410
|
+
apps: ensureAiServerApp(cached.apps, preferredAi),
|
|
3411
|
+
source: "cache",
|
|
3412
|
+
cached: true,
|
|
3413
|
+
usedAi: false,
|
|
3414
|
+
confused: false,
|
|
3415
|
+
reasons: [],
|
|
3416
|
+
fingerprint
|
|
3417
|
+
};
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
const needsAi = Boolean(input.allowAi) && (Boolean(input.force && detected.confused) || detected.confused && detected.apps.filter((app) => app.role !== "ai-server").length === 0 || detected.reasons.some((reason) => /differ|does not match/i.test(reason)));
|
|
3421
|
+
if (needsAi) {
|
|
3422
|
+
try {
|
|
3423
|
+
const inspected = await inspectConfigOnly({
|
|
3424
|
+
workspaceDir: folder,
|
|
3425
|
+
appName: input.appName,
|
|
3426
|
+
extraContext: `Port detection was uncertain:
|
|
3427
|
+
${detected.reasons.join("\n")}
|
|
3428
|
+
Return the real local ports and npm script names.`
|
|
3429
|
+
});
|
|
3430
|
+
const fromAi = appsFromInspectPorts(
|
|
3431
|
+
inspected.ports,
|
|
3432
|
+
inspected.scripts,
|
|
3433
|
+
inspected.name || input.appName || "App"
|
|
3434
|
+
);
|
|
3435
|
+
const apps2 = ensureAiServerApp(
|
|
3436
|
+
[defaultAiServerApp(preferredAi), ...fromAi],
|
|
3437
|
+
preferredAi
|
|
3438
|
+
);
|
|
3439
|
+
writeHostAppsCache(
|
|
3440
|
+
folder,
|
|
3441
|
+
apps2,
|
|
3442
|
+
{
|
|
3443
|
+
source: "ai",
|
|
3444
|
+
reasons: detected.reasons,
|
|
3445
|
+
summary: inspected.summary
|
|
3446
|
+
},
|
|
3447
|
+
loc
|
|
3448
|
+
);
|
|
3449
|
+
return {
|
|
3450
|
+
apps: apps2,
|
|
3451
|
+
source: "ai",
|
|
3452
|
+
cached: false,
|
|
3453
|
+
usedAi: true,
|
|
3454
|
+
confused: detected.confused,
|
|
3455
|
+
reasons: detected.reasons,
|
|
3456
|
+
fingerprint
|
|
3457
|
+
};
|
|
3458
|
+
} catch (err) {
|
|
3459
|
+
detected.reasons.push(
|
|
3460
|
+
`AI inspect skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
3461
|
+
);
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
const apps = ensureAiServerApp(detected.apps, preferredAi);
|
|
3465
|
+
try {
|
|
3466
|
+
writeHostAppsCache(
|
|
3467
|
+
folder,
|
|
3468
|
+
apps,
|
|
3469
|
+
{
|
|
3470
|
+
source: detected.confused ? "env" : "env",
|
|
3471
|
+
reasons: detected.reasons
|
|
3472
|
+
},
|
|
3473
|
+
loc
|
|
3474
|
+
);
|
|
3475
|
+
} catch {
|
|
3476
|
+
}
|
|
3477
|
+
return {
|
|
3478
|
+
apps,
|
|
3479
|
+
source: apps.some((app) => app.source === "env") ? "env" : "default",
|
|
3480
|
+
cached: false,
|
|
3481
|
+
usedAi: false,
|
|
3482
|
+
confused: detected.confused,
|
|
3483
|
+
reasons: detected.reasons,
|
|
3484
|
+
fingerprint
|
|
1256
3485
|
};
|
|
1257
3486
|
}
|
|
1258
3487
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1259
3488
|
0 && (module.exports = {
|
|
3489
|
+
ACCESS_IGNORE_BEGIN,
|
|
3490
|
+
ACCESS_IGNORE_END,
|
|
3491
|
+
AI_SERVER_APP_ID,
|
|
3492
|
+
AI_SERVER_DEFAULT_PORT,
|
|
3493
|
+
COLLABORATER_DIR,
|
|
3494
|
+
DEFAULT_AI_IGNORE_PATHS,
|
|
3495
|
+
HOST_APPS_FILE,
|
|
3496
|
+
MAINTAINER_PRO_HOME_DIR,
|
|
3497
|
+
PROMPT_SECTION,
|
|
1260
3498
|
WORKING_PROVIDER,
|
|
1261
3499
|
buildClaudeUserPrompt,
|
|
1262
3500
|
buildConversationPrompt,
|
|
1263
3501
|
buildCursorPrompt,
|
|
1264
3502
|
buildPriorConversationsContext,
|
|
1265
3503
|
callAi,
|
|
3504
|
+
collectParentChain,
|
|
1266
3505
|
commandExists,
|
|
1267
3506
|
createAntigravityProvider,
|
|
1268
3507
|
createBuiltinProviders,
|
|
@@ -1270,17 +3509,60 @@ async function inspectAndRepairWorkspace(input) {
|
|
|
1270
3509
|
createClaudeProvider,
|
|
1271
3510
|
createCursorProvider,
|
|
1272
3511
|
createDefaultSystemPrompt,
|
|
3512
|
+
createInfoLogger,
|
|
1273
3513
|
createLocalDirectoryStore,
|
|
3514
|
+
createLogger,
|
|
1274
3515
|
createMaintainerProStore,
|
|
1275
3516
|
createMaintainerProStoreFromEnv,
|
|
3517
|
+
createSyncedChatStore,
|
|
1276
3518
|
createToolValidator,
|
|
3519
|
+
defaultAiServerApp,
|
|
3520
|
+
detectHostAppsFromFiles,
|
|
3521
|
+
ensureAiServerApp,
|
|
3522
|
+
ensureProjectDataDir,
|
|
3523
|
+
ensureSingleHost,
|
|
3524
|
+
formatAccessPolicyPromptSection,
|
|
1277
3525
|
formatClientContext,
|
|
3526
|
+
formatParentChainContext,
|
|
1278
3527
|
getProviderPreference,
|
|
3528
|
+
hostAppsCachePath,
|
|
3529
|
+
hostAppsFingerprint,
|
|
1279
3530
|
inspectAndRepairWorkspace,
|
|
3531
|
+
inspectConfigOnly,
|
|
3532
|
+
isDevMode,
|
|
3533
|
+
isIgnoredRelative,
|
|
3534
|
+
isInsideWorkspace,
|
|
3535
|
+
isPathAllowed,
|
|
3536
|
+
maintainerProHome,
|
|
3537
|
+
mergeDesiredHostApps,
|
|
3538
|
+
normalizeEnvMaps,
|
|
3539
|
+
normalizeHostApp,
|
|
3540
|
+
normalizeHostApps,
|
|
3541
|
+
normalizeIgnorePaths,
|
|
3542
|
+
parentChainForTurn,
|
|
1280
3543
|
parseAiResponse,
|
|
3544
|
+
parseIgnorePathsEnv,
|
|
3545
|
+
parsePort,
|
|
3546
|
+
previewText,
|
|
3547
|
+
projectHostAppsPath,
|
|
3548
|
+
projectIdForFolder,
|
|
3549
|
+
projectUploadsDir,
|
|
3550
|
+
proposeHostAppsFromConfig,
|
|
1281
3551
|
providerLabel,
|
|
3552
|
+
readCollaboraterApps,
|
|
3553
|
+
readHostAppsCache,
|
|
3554
|
+
readProjectEnvLayers,
|
|
3555
|
+
renderManagedIgnoreBlock,
|
|
1282
3556
|
resolveCliBinary,
|
|
3557
|
+
resolveHostApps,
|
|
3558
|
+
resolveIgnorePaths,
|
|
3559
|
+
resolveLogLevel,
|
|
3560
|
+
resolveProjectDataDir,
|
|
1283
3561
|
resolveProvider,
|
|
3562
|
+
sanitizeProjectId,
|
|
1284
3563
|
saveChatAttachments,
|
|
1285
|
-
toNextRoute
|
|
3564
|
+
toNextRoute,
|
|
3565
|
+
upsertManagedIgnoreFile,
|
|
3566
|
+
writeCollaboraterApps,
|
|
3567
|
+
writeHostAppsCache
|
|
1286
3568
|
});
|