@maintainer-pro/ai-cli 0.1.5 → 0.1.7
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 +1094 -51
- package/dist/index.d.cts +176 -6
- package/dist/index.d.ts +176 -6
- package/dist/index.js +1063 -51
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -32,7 +32,12 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
ACCESS_IGNORE_BEGIN: () => ACCESS_IGNORE_BEGIN,
|
|
34
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,
|
|
35
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,
|
|
36
41
|
PROMPT_SECTION: () => PROMPT_SECTION,
|
|
37
42
|
WORKING_PROVIDER: () => WORKING_PROVIDER,
|
|
38
43
|
buildClaudeUserPrompt: () => buildClaudeUserPrompt,
|
|
@@ -55,29 +60,55 @@ __export(index_exports, {
|
|
|
55
60
|
createMaintainerProStoreFromEnv: () => createMaintainerProStoreFromEnv,
|
|
56
61
|
createSyncedChatStore: () => createSyncedChatStore,
|
|
57
62
|
createToolValidator: () => createToolValidator,
|
|
63
|
+
defaultAiServerApp: () => defaultAiServerApp,
|
|
64
|
+
detectHostAppsFromFiles: () => detectHostAppsFromFiles,
|
|
65
|
+
ensureAiServerApp: () => ensureAiServerApp,
|
|
66
|
+
ensureProjectDataDir: () => ensureProjectDataDir,
|
|
67
|
+
ensureSingleHost: () => ensureSingleHost,
|
|
58
68
|
formatAccessPolicyPromptSection: () => formatAccessPolicyPromptSection,
|
|
59
69
|
formatClientContext: () => formatClientContext,
|
|
60
70
|
formatParentChainContext: () => formatParentChainContext,
|
|
61
71
|
getProviderPreference: () => getProviderPreference,
|
|
72
|
+
hostAppsCachePath: () => hostAppsCachePath2,
|
|
73
|
+
hostAppsFingerprint: () => hostAppsFingerprint,
|
|
62
74
|
inspectAndRepairWorkspace: () => inspectAndRepairWorkspace,
|
|
75
|
+
inspectConfigOnly: () => inspectConfigOnly,
|
|
63
76
|
isDevMode: () => isDevMode,
|
|
64
77
|
isIgnoredRelative: () => isIgnoredRelative,
|
|
65
78
|
isInsideWorkspace: () => isInsideWorkspace,
|
|
66
79
|
isPathAllowed: () => isPathAllowed,
|
|
80
|
+
maintainerProHome: () => maintainerProHome,
|
|
81
|
+
mergeDesiredHostApps: () => mergeDesiredHostApps,
|
|
82
|
+
normalizeEnvMaps: () => normalizeEnvMaps,
|
|
83
|
+
normalizeHostApp: () => normalizeHostApp,
|
|
84
|
+
normalizeHostApps: () => normalizeHostApps,
|
|
67
85
|
normalizeIgnorePaths: () => normalizeIgnorePaths,
|
|
68
86
|
parentChainForTurn: () => parentChainForTurn,
|
|
69
87
|
parseAiResponse: () => parseAiResponse,
|
|
70
88
|
parseIgnorePathsEnv: () => parseIgnorePathsEnv,
|
|
89
|
+
parsePort: () => parsePort,
|
|
71
90
|
previewText: () => previewText,
|
|
91
|
+
projectHostAppsPath: () => hostAppsCachePath,
|
|
92
|
+
projectIdForFolder: () => projectIdForFolder,
|
|
93
|
+
projectUploadsDir: () => projectUploadsDir,
|
|
94
|
+
proposeHostAppsFromConfig: () => proposeHostAppsFromConfig,
|
|
72
95
|
providerLabel: () => providerLabel,
|
|
96
|
+
readCollaboraterApps: () => readCollaboraterApps,
|
|
97
|
+
readHostAppsCache: () => readHostAppsCache,
|
|
98
|
+
readProjectEnvLayers: () => readProjectEnvLayers,
|
|
73
99
|
renderManagedIgnoreBlock: () => renderManagedIgnoreBlock,
|
|
74
100
|
resolveCliBinary: () => resolveCliBinary,
|
|
101
|
+
resolveHostApps: () => resolveHostApps,
|
|
75
102
|
resolveIgnorePaths: () => resolveIgnorePaths,
|
|
76
103
|
resolveLogLevel: () => resolveLogLevel,
|
|
104
|
+
resolveProjectDataDir: () => resolveProjectDataDir,
|
|
77
105
|
resolveProvider: () => resolveProvider,
|
|
106
|
+
sanitizeProjectId: () => sanitizeProjectId,
|
|
78
107
|
saveChatAttachments: () => saveChatAttachments,
|
|
79
108
|
toNextRoute: () => toNextRoute,
|
|
80
|
-
upsertManagedIgnoreFile: () => upsertManagedIgnoreFile
|
|
109
|
+
upsertManagedIgnoreFile: () => upsertManagedIgnoreFile,
|
|
110
|
+
writeCollaboraterApps: () => writeCollaboraterApps,
|
|
111
|
+
writeHostAppsCache: () => writeHostAppsCache
|
|
81
112
|
});
|
|
82
113
|
module.exports = __toCommonJS(index_exports);
|
|
83
114
|
|
|
@@ -192,7 +223,7 @@ function buildUserFacingPrompt(systemPrompt, messages, context, attachmentPaths,
|
|
|
192
223
|
);
|
|
193
224
|
const attachmentsSection = wrapSection(
|
|
194
225
|
PROMPT_SECTION.attachments,
|
|
195
|
-
attachmentPaths && attachmentPaths.length > 0 ? `Screenshots / images attached by the user (open and inspect these image files):
|
|
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):
|
|
196
227
|
${attachmentPaths.map((p) => `- ${p}`).join("\n")}` : "",
|
|
197
228
|
"user attachments"
|
|
198
229
|
);
|
|
@@ -346,6 +377,7 @@ async function callClaudeCli(command, messages, context, options) {
|
|
|
346
377
|
resolved,
|
|
347
378
|
[
|
|
348
379
|
"--print",
|
|
380
|
+
"--dangerously-skip-permissions",
|
|
349
381
|
"--system-prompt",
|
|
350
382
|
options.systemPrompt,
|
|
351
383
|
"--no-session-persistence",
|
|
@@ -463,8 +495,8 @@ async function resolveCommandPath(command) {
|
|
|
463
495
|
}
|
|
464
496
|
const result = await (0, import_execa2.execa)("which", [command], { reject: false });
|
|
465
497
|
if (result.exitCode !== 0) return null;
|
|
466
|
-
const
|
|
467
|
-
return
|
|
498
|
+
const path9 = result.stdout.trim().split(/\r?\n/)[0]?.trim();
|
|
499
|
+
return path9 || null;
|
|
468
500
|
} catch {
|
|
469
501
|
return null;
|
|
470
502
|
}
|
|
@@ -611,7 +643,7 @@ Only use those runtime tools for live state. Never invent runtime tools.` : "";
|
|
|
611
643
|
- You may only read or edit files inside the current project workspace directory.
|
|
612
644
|
- Never read, write, move, or delete files outside that workspace.
|
|
613
645
|
- Never touch paths that match this ignore list (relative to the workspace):
|
|
614
|
-
${input.ignorePaths.length ? input.ignorePaths.map((p) => `- ${p}`).join("\n") : "- (none beyond staying inside the workspace)"}
|
|
646
|
+
${(input.ignorePaths ?? []).filter((p) => !p.startsWith("!")).length ? (input.ignorePaths ?? []).filter((p) => !p.startsWith("!")).map((p) => `- ${p}`).join("\n") : "- (none beyond staying inside the workspace)"}
|
|
615
647
|
- 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.` : "";
|
|
616
648
|
return `You are a helpful product assistant for an app the user is looking at right now.
|
|
617
649
|
|
|
@@ -737,10 +769,11 @@ function isPathAllowed(workspaceDir, targetPath, ignorePaths) {
|
|
|
737
769
|
return !isIgnoredRelative(rel, ignorePaths);
|
|
738
770
|
}
|
|
739
771
|
function formatAccessPolicyPromptSection(input) {
|
|
740
|
-
const ignores = input.ignorePaths.length ? input.ignorePaths.map((p) => `- ${p}`).join("\n") : "- (none beyond staying inside the workspace)";
|
|
772
|
+
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)";
|
|
741
773
|
return `## File access boundaries (mandatory)
|
|
742
774
|
- You may only read or edit files inside the current project workspace directory.
|
|
743
|
-
- Never
|
|
775
|
+
- Never write, move, or delete files outside that workspace.
|
|
776
|
+
- You may read screenshot files listed in the attachments section (those live in the Maintainer Pro data folder, not in the project).
|
|
744
777
|
- Never touch paths that match this ignore list (relative to the workspace):
|
|
745
778
|
${ignores}
|
|
746
779
|
- 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.`;
|
|
@@ -778,7 +811,7 @@ ${block}` : block;
|
|
|
778
811
|
}
|
|
779
812
|
|
|
780
813
|
// src/http/handler.ts
|
|
781
|
-
var
|
|
814
|
+
var import_node_crypto2 = require("crypto");
|
|
782
815
|
|
|
783
816
|
// src/log.ts
|
|
784
817
|
var import_pino = __toESM(require("pino"), 1);
|
|
@@ -825,12 +858,72 @@ function previewText(text, max = 160) {
|
|
|
825
858
|
|
|
826
859
|
// src/http/attachments.ts
|
|
827
860
|
var import_promises = __toESM(require("fs/promises"), 1);
|
|
861
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
862
|
+
|
|
863
|
+
// src/project-data.ts
|
|
864
|
+
var import_node_crypto = require("crypto");
|
|
865
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
866
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
828
867
|
var import_node_path3 = __toESM(require("path"), 1);
|
|
868
|
+
var MAINTAINER_PRO_HOME_DIR = ".maintainer-pro";
|
|
869
|
+
var HOST_APPS_FILE = "apps.json";
|
|
870
|
+
function maintainerProHome() {
|
|
871
|
+
const override = process.env.MAINTAINER_PRO_HOME?.trim();
|
|
872
|
+
if (override) return import_node_path3.default.resolve(override);
|
|
873
|
+
return import_node_path3.default.join(import_node_os.default.homedir(), MAINTAINER_PRO_HOME_DIR);
|
|
874
|
+
}
|
|
875
|
+
function projectIdForFolder(folder) {
|
|
876
|
+
const resolved = import_node_path3.default.resolve(folder || "");
|
|
877
|
+
const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
878
|
+
return (0, import_node_crypto.createHash)("sha256").update(key).digest("hex").slice(0, 16);
|
|
879
|
+
}
|
|
880
|
+
function sanitizeProjectId(id) {
|
|
881
|
+
return String(id || "").trim().replace(/[^\w.-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 80);
|
|
882
|
+
}
|
|
883
|
+
function resolveProjectDataDir(input) {
|
|
884
|
+
const override = String(input.dataDir || "").trim();
|
|
885
|
+
if (override) return import_node_path3.default.resolve(override);
|
|
886
|
+
const sandbox = sanitizeProjectId(String(input.sandboxId || ""));
|
|
887
|
+
const id = sandbox || projectIdForFolder(input.workspaceDir);
|
|
888
|
+
return import_node_path3.default.join(maintainerProHome(), "projects", id);
|
|
889
|
+
}
|
|
890
|
+
function projectUploadsDir(input) {
|
|
891
|
+
return import_node_path3.default.join(resolveProjectDataDir(input), "uploads");
|
|
892
|
+
}
|
|
893
|
+
function hostAppsCachePath(input) {
|
|
894
|
+
return import_node_path3.default.join(resolveProjectDataDir(input), HOST_APPS_FILE);
|
|
895
|
+
}
|
|
896
|
+
function ensureProjectDataDir(input) {
|
|
897
|
+
const dir = resolveProjectDataDir(input);
|
|
898
|
+
import_node_fs2.default.mkdirSync(dir, { recursive: true });
|
|
899
|
+
import_node_fs2.default.mkdirSync(import_node_path3.default.join(dir, "uploads"), { recursive: true });
|
|
900
|
+
try {
|
|
901
|
+
import_node_fs2.default.writeFileSync(
|
|
902
|
+
import_node_path3.default.join(dir, "workspace.json"),
|
|
903
|
+
`${JSON.stringify(
|
|
904
|
+
{
|
|
905
|
+
workspaceDir: import_node_path3.default.resolve(input.workspaceDir || ""),
|
|
906
|
+
sandboxId: input.sandboxId ? String(input.sandboxId) : null,
|
|
907
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
908
|
+
},
|
|
909
|
+
null,
|
|
910
|
+
2
|
|
911
|
+
)}
|
|
912
|
+
`,
|
|
913
|
+
"utf8"
|
|
914
|
+
);
|
|
915
|
+
} catch {
|
|
916
|
+
}
|
|
917
|
+
return dir;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// src/http/attachments.ts
|
|
829
921
|
var MAX_ATTACHMENTS = 5;
|
|
830
922
|
var MAX_BYTES = 4 * 1024 * 1024;
|
|
831
923
|
var ALLOWED = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif"]);
|
|
832
924
|
function extForMime(mime) {
|
|
833
|
-
|
|
925
|
+
const normalized = mime.split(";")[0]?.trim().toLowerCase() || "";
|
|
926
|
+
switch (normalized) {
|
|
834
927
|
case "image/jpeg":
|
|
835
928
|
case "image/jpg":
|
|
836
929
|
return ".jpg";
|
|
@@ -842,11 +935,31 @@ function extForMime(mime) {
|
|
|
842
935
|
return ".png";
|
|
843
936
|
}
|
|
844
937
|
}
|
|
845
|
-
|
|
938
|
+
function attachmentIdFromRef(ref) {
|
|
939
|
+
const trimmed = String(ref || "").trim();
|
|
940
|
+
if (!trimmed) return null;
|
|
941
|
+
const m = /^maintainer-pro:\/\/(.+)$/i.exec(trimmed);
|
|
942
|
+
if (m?.[1]) return m[1];
|
|
943
|
+
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(
|
|
944
|
+
trimmed
|
|
945
|
+
)) {
|
|
946
|
+
return trimmed;
|
|
947
|
+
}
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
function uploadsRoot(input) {
|
|
951
|
+
ensureProjectDataDir(input);
|
|
952
|
+
return projectUploadsDir(input);
|
|
953
|
+
}
|
|
954
|
+
async function saveChatAttachments(attachments, workspaceDir, extra) {
|
|
846
955
|
if (!attachments?.length) return [];
|
|
847
956
|
const selected = attachments.slice(0, MAX_ATTACHMENTS);
|
|
848
|
-
const
|
|
849
|
-
|
|
957
|
+
const loc = {
|
|
958
|
+
workspaceDir,
|
|
959
|
+
dataDir: extra?.dataDir,
|
|
960
|
+
sandboxId: extra?.sandboxId
|
|
961
|
+
};
|
|
962
|
+
const dir = uploadsRoot(loc);
|
|
850
963
|
const paths = [];
|
|
851
964
|
const stamp = Date.now();
|
|
852
965
|
for (let i = 0; i < selected.length; i++) {
|
|
@@ -864,9 +977,9 @@ async function saveChatAttachments(attachments, workspaceDir) {
|
|
|
864
977
|
}
|
|
865
978
|
const safeBase = (item.name || `screenshot-${i + 1}`).replace(/[^\w.\-]+/g, "_").slice(0, 64);
|
|
866
979
|
const fileName = `${stamp}-${i + 1}-${safeBase}${extForMime(mime)}`;
|
|
867
|
-
const filePath =
|
|
868
|
-
if (!isInsideWorkspace(
|
|
869
|
-
throw new Error("Attachment path escaped the project
|
|
980
|
+
const filePath = import_node_path4.default.resolve(dir, fileName);
|
|
981
|
+
if (!isInsideWorkspace(dir, filePath)) {
|
|
982
|
+
throw new Error("Attachment path escaped the project data folder");
|
|
870
983
|
}
|
|
871
984
|
await import_promises.default.writeFile(filePath, buffer);
|
|
872
985
|
paths.push(filePath);
|
|
@@ -1019,6 +1132,8 @@ function createChatHandler(options) {
|
|
|
1019
1132
|
const logger = options.logger ?? createLogger("ai-cli:chat");
|
|
1020
1133
|
const validate = options.tools ? createToolValidator(options.tools) : null;
|
|
1021
1134
|
const workspaceDir = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
|
|
1135
|
+
const dataDir = options.dataDir;
|
|
1136
|
+
const sandboxId = options.sandboxId;
|
|
1022
1137
|
const baseCallOptions = {
|
|
1023
1138
|
systemPrompt: options.systemPrompt,
|
|
1024
1139
|
workspaceDir,
|
|
@@ -1175,20 +1290,37 @@ function createChatHandler(options) {
|
|
|
1175
1290
|
}
|
|
1176
1291
|
let attachmentPaths = [];
|
|
1177
1292
|
let persistAttachmentPaths = [];
|
|
1178
|
-
if (options.db?.uploadAttachments) {
|
|
1293
|
+
if (options.db?.uploadAttachments && attachments?.length) {
|
|
1179
1294
|
const uploaded = await options.db.uploadAttachments(
|
|
1180
1295
|
conversationId,
|
|
1181
1296
|
attachments
|
|
1182
1297
|
);
|
|
1183
1298
|
attachmentPaths = uploaded.localPaths;
|
|
1184
1299
|
persistAttachmentPaths = uploaded.refs;
|
|
1185
|
-
} else {
|
|
1300
|
+
} else if (attachments?.length) {
|
|
1186
1301
|
attachmentPaths = await saveChatAttachments(
|
|
1187
1302
|
attachments,
|
|
1188
|
-
workspaceDir
|
|
1303
|
+
workspaceDir,
|
|
1304
|
+
{ dataDir, sandboxId }
|
|
1189
1305
|
);
|
|
1190
1306
|
persistAttachmentPaths = attachmentPaths;
|
|
1191
1307
|
}
|
|
1308
|
+
const storedUserId = typeof body.userMessageId === "string" && body.userMessageId ? body.userMessageId : void 0;
|
|
1309
|
+
if (!attachmentPaths.length && conversationId && options.db?.materializeMessageAttachments) {
|
|
1310
|
+
attachmentPaths = await options.db.materializeMessageAttachments(
|
|
1311
|
+
conversationId,
|
|
1312
|
+
storedUserId,
|
|
1313
|
+
workspaceDir
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
logger.debug(
|
|
1317
|
+
{
|
|
1318
|
+
conversationId,
|
|
1319
|
+
userMessageId: storedUserId,
|
|
1320
|
+
attachmentCount: attachmentPaths.length
|
|
1321
|
+
},
|
|
1322
|
+
"cli attachments"
|
|
1323
|
+
);
|
|
1192
1324
|
const turn = beginConversationTurn(conversationId ?? SHARED_CONVERSATION_ID);
|
|
1193
1325
|
trackedTurn = turn;
|
|
1194
1326
|
const signal = request.signal;
|
|
@@ -1283,6 +1415,7 @@ function createChatHandler(options) {
|
|
|
1283
1415
|
turn,
|
|
1284
1416
|
parentUserMessageId,
|
|
1285
1417
|
historyLength: messages.length,
|
|
1418
|
+
attachmentCount: attachmentPaths.length,
|
|
1286
1419
|
hasParentChain: Boolean(parentChainContext),
|
|
1287
1420
|
preview: previewText(
|
|
1288
1421
|
[...messages].reverse().find((m) => m.role === "user")?.content
|
|
@@ -1327,7 +1460,7 @@ function createChatHandler(options) {
|
|
|
1327
1460
|
await options.onToolCalls(validatedToolCalls, context);
|
|
1328
1461
|
}
|
|
1329
1462
|
let nextQueued = null;
|
|
1330
|
-
const assistantMessageId = typeof body.assistantMessageId === "string" && body.assistantMessageId ? body.assistantMessageId : (0,
|
|
1463
|
+
const assistantMessageId = typeof body.assistantMessageId === "string" && body.assistantMessageId ? body.assistantMessageId : (0, import_node_crypto2.randomUUID)();
|
|
1331
1464
|
let replyId = assistantMessageId;
|
|
1332
1465
|
if (options.db && conversationId) {
|
|
1333
1466
|
await options.db.ensureConversation(conversationId);
|
|
@@ -1442,8 +1575,8 @@ function toNextRoute(handlers) {
|
|
|
1442
1575
|
|
|
1443
1576
|
// src/http/local-store.ts
|
|
1444
1577
|
var import_promises2 = __toESM(require("fs/promises"), 1);
|
|
1445
|
-
var
|
|
1446
|
-
var
|
|
1578
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
1579
|
+
var import_node_crypto3 = require("crypto");
|
|
1447
1580
|
function userHasAiReply(rows, workingId) {
|
|
1448
1581
|
return rows.some(
|
|
1449
1582
|
(m) => m.role === "assistant" && m.parentMessageId === workingId && m.provider !== "working" && Boolean(m.content?.trim())
|
|
@@ -1459,11 +1592,11 @@ async function readConversation(filePath) {
|
|
|
1459
1592
|
}
|
|
1460
1593
|
}
|
|
1461
1594
|
async function writeConversation(filePath, data) {
|
|
1462
|
-
await import_promises2.default.mkdir(
|
|
1595
|
+
await import_promises2.default.mkdir(import_node_path5.default.dirname(filePath), { recursive: true });
|
|
1463
1596
|
await import_promises2.default.writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
1464
1597
|
}
|
|
1465
1598
|
function createLocalDirectoryStore(baseDir) {
|
|
1466
|
-
const fileFor = (id) =>
|
|
1599
|
+
const fileFor = (id) => import_node_path5.default.join(baseDir, `${id}.json`);
|
|
1467
1600
|
return {
|
|
1468
1601
|
async ensureConversation(id) {
|
|
1469
1602
|
const existing = await readConversation(fileFor(id));
|
|
@@ -1487,7 +1620,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1487
1620
|
const isAi = input.role === "assistant" && input.provider !== "working" && input.senderType !== "developer" && input.provider !== "developer";
|
|
1488
1621
|
const busy = existing.messages.some((m) => m.queueStatus === "working");
|
|
1489
1622
|
const makeWorking = (parentMessageId) => ({
|
|
1490
|
-
id: (0,
|
|
1623
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
1491
1624
|
conversationId: input.conversationId,
|
|
1492
1625
|
role: "assistant",
|
|
1493
1626
|
content: "",
|
|
@@ -1525,7 +1658,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1525
1658
|
return { ...existingById2, nextQueued: null };
|
|
1526
1659
|
}
|
|
1527
1660
|
const message2 = {
|
|
1528
|
-
id: input.id ?? (0,
|
|
1661
|
+
id: input.id ?? (0, import_node_crypto3.randomUUID)(),
|
|
1529
1662
|
conversationId: input.conversationId,
|
|
1530
1663
|
role: input.role,
|
|
1531
1664
|
content: input.content,
|
|
@@ -1557,7 +1690,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1557
1690
|
}
|
|
1558
1691
|
}
|
|
1559
1692
|
const message2 = {
|
|
1560
|
-
id: input.id ?? (0,
|
|
1693
|
+
id: input.id ?? (0, import_node_crypto3.randomUUID)(),
|
|
1561
1694
|
conversationId: input.conversationId,
|
|
1562
1695
|
role: "assistant",
|
|
1563
1696
|
content: "",
|
|
@@ -1576,7 +1709,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1576
1709
|
return { ...existingById, nextQueued: null };
|
|
1577
1710
|
}
|
|
1578
1711
|
const message = {
|
|
1579
|
-
id: input.id ?? (0,
|
|
1712
|
+
id: input.id ?? (0, import_node_crypto3.randomUUID)(),
|
|
1580
1713
|
conversationId: input.conversationId,
|
|
1581
1714
|
role: input.role,
|
|
1582
1715
|
content: input.content,
|
|
@@ -1647,7 +1780,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1647
1780
|
next2.queueStatus = "working";
|
|
1648
1781
|
next2.queuePosition = null;
|
|
1649
1782
|
existing.messages.push({
|
|
1650
|
-
id: (0,
|
|
1783
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
1651
1784
|
conversationId,
|
|
1652
1785
|
role: "assistant",
|
|
1653
1786
|
content: "",
|
|
@@ -1688,7 +1821,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1688
1821
|
next.queueStatus = "working";
|
|
1689
1822
|
next.queuePosition = null;
|
|
1690
1823
|
existing.messages.push({
|
|
1691
|
-
id: (0,
|
|
1824
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
1692
1825
|
conversationId,
|
|
1693
1826
|
role: "assistant",
|
|
1694
1827
|
content: "",
|
|
@@ -1735,7 +1868,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1735
1868
|
next.queueStatus = "working";
|
|
1736
1869
|
next.queuePosition = null;
|
|
1737
1870
|
existing.messages.push({
|
|
1738
|
-
id: (0,
|
|
1871
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
1739
1872
|
conversationId,
|
|
1740
1873
|
role: "assistant",
|
|
1741
1874
|
content: "",
|
|
@@ -1769,7 +1902,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1769
1902
|
const conversations = [];
|
|
1770
1903
|
for (const entry of entries) {
|
|
1771
1904
|
if (!entry.endsWith(".json")) continue;
|
|
1772
|
-
const filePath =
|
|
1905
|
+
const filePath = import_node_path5.default.join(baseDir, entry);
|
|
1773
1906
|
const existing = await readConversation(filePath);
|
|
1774
1907
|
if (!existing?.id) continue;
|
|
1775
1908
|
conversations.push({
|
|
@@ -1786,7 +1919,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1786
1919
|
if (events.length === 0) return;
|
|
1787
1920
|
const existing = await readConversation(fileFor(conversationId));
|
|
1788
1921
|
if (!existing) return;
|
|
1789
|
-
const toolFile =
|
|
1922
|
+
const toolFile = import_node_path5.default.join(baseDir, `${conversationId}.tools.jsonl`);
|
|
1790
1923
|
const lines = events.map(
|
|
1791
1924
|
(e) => JSON.stringify({
|
|
1792
1925
|
conversationId,
|
|
@@ -1802,8 +1935,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1802
1935
|
|
|
1803
1936
|
// src/http/maintainer-pro-store.ts
|
|
1804
1937
|
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
1805
|
-
var
|
|
1806
|
-
var import_node_path5 = __toESM(require("path"), 1);
|
|
1938
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
1807
1939
|
|
|
1808
1940
|
// src/http/synced-store.ts
|
|
1809
1941
|
function userHasAiReply2(rows, workingId) {
|
|
@@ -1830,7 +1962,7 @@ function asCachedMessage(raw) {
|
|
|
1830
1962
|
content: typeof raw.content === "string" ? raw.content : "",
|
|
1831
1963
|
provider: typeof raw.provider === "string" ? raw.provider : null,
|
|
1832
1964
|
createdAt: typeof raw.createdAt === "string" ? raw.createdAt : void 0,
|
|
1833
|
-
attachmentPaths: Array.isArray(raw.attachmentPaths) ? raw.attachmentPaths.filter((p) => typeof p === "string") : void 0,
|
|
1965
|
+
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,
|
|
1834
1966
|
senderType: raw.senderType === "client" || raw.senderType === "developer" || raw.senderType === "ai" ? raw.senderType : null,
|
|
1835
1967
|
senderName: typeof raw.senderName === "string" ? raw.senderName : null,
|
|
1836
1968
|
parentMessageId: typeof raw.parentMessageId === "string" ? raw.parentMessageId : null,
|
|
@@ -2144,6 +2276,14 @@ function createSyncedChatStore(options) {
|
|
|
2144
2276
|
return { refs: [], localPaths: [] };
|
|
2145
2277
|
}
|
|
2146
2278
|
return remote.uploadAttachments(conversationId, attachments);
|
|
2279
|
+
},
|
|
2280
|
+
async materializeMessageAttachments(conversationId, messageId, workspaceDir) {
|
|
2281
|
+
if (!remote.materializeMessageAttachments) return [];
|
|
2282
|
+
return remote.materializeMessageAttachments(
|
|
2283
|
+
conversationId,
|
|
2284
|
+
messageId,
|
|
2285
|
+
workspaceDir
|
|
2286
|
+
);
|
|
2147
2287
|
}
|
|
2148
2288
|
};
|
|
2149
2289
|
return store;
|
|
@@ -2176,7 +2316,8 @@ function createMaintainerProStore(options) {
|
|
|
2176
2316
|
const baseUrl = options.baseUrl;
|
|
2177
2317
|
const apiKey = options.apiKey;
|
|
2178
2318
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2179
|
-
const
|
|
2319
|
+
const logger = options.logger;
|
|
2320
|
+
const tempDir = options.tempDir ?? projectUploadsDir({ workspaceDir: process.cwd() });
|
|
2180
2321
|
const store = {
|
|
2181
2322
|
async ensureConversation(id) {
|
|
2182
2323
|
await mpFetch(
|
|
@@ -2327,15 +2468,89 @@ function createMaintainerProStore(options) {
|
|
|
2327
2468
|
const id = data.attachment.id;
|
|
2328
2469
|
attachmentIds.push(id);
|
|
2329
2470
|
refs.push(data.attachment.ref || `maintainer-pro://${id}`);
|
|
2330
|
-
const
|
|
2331
|
-
const localPath = import_node_path5.default.join(
|
|
2471
|
+
const localPath = import_node_path6.default.join(
|
|
2332
2472
|
tempDir,
|
|
2333
|
-
`${conversationId}-${id}${
|
|
2473
|
+
`${conversationId}-${id}${extForMime(item.mimeType)}`
|
|
2334
2474
|
);
|
|
2475
|
+
await import_promises3.default.mkdir(import_node_path6.default.dirname(localPath), { recursive: true });
|
|
2335
2476
|
await import_promises3.default.writeFile(localPath, Buffer.from(item.data, "base64"));
|
|
2336
2477
|
localPaths.push(localPath);
|
|
2337
2478
|
}
|
|
2338
2479
|
return { refs, localPaths, attachmentIds };
|
|
2480
|
+
},
|
|
2481
|
+
async materializeMessageAttachments(conversationId, messageId, workspaceDir) {
|
|
2482
|
+
if (!conversationId || !workspaceDir) return [];
|
|
2483
|
+
const data = await mpFetch(
|
|
2484
|
+
baseUrl,
|
|
2485
|
+
apiKey,
|
|
2486
|
+
`/api/v1/store/conversations/${encodeURIComponent(conversationId)}/messages`,
|
|
2487
|
+
{ method: "GET" },
|
|
2488
|
+
fetchImpl
|
|
2489
|
+
);
|
|
2490
|
+
const messages = data.messages ?? [];
|
|
2491
|
+
const byId = new Map(messages.map((row) => [row.id, row]));
|
|
2492
|
+
const refs = [];
|
|
2493
|
+
const fromMessageIds = [];
|
|
2494
|
+
const seenMsg = /* @__PURE__ */ new Set();
|
|
2495
|
+
const pushRow = (row) => {
|
|
2496
|
+
if (!row || seenMsg.has(row.id)) return;
|
|
2497
|
+
seenMsg.add(row.id);
|
|
2498
|
+
const before = refs.length;
|
|
2499
|
+
refs.push(...row.attachmentPaths ?? []);
|
|
2500
|
+
refs.push(
|
|
2501
|
+
...(row.attachmentIds ?? []).map((id) => `maintainer-pro://${id}`)
|
|
2502
|
+
);
|
|
2503
|
+
if (refs.length > before) fromMessageIds.push(row.id);
|
|
2504
|
+
};
|
|
2505
|
+
let current = messageId ? byId.get(messageId) : void 0;
|
|
2506
|
+
while (current && !seenMsg.has(current.id)) {
|
|
2507
|
+
pushRow(current);
|
|
2508
|
+
const parentId = current.parentMessageId?.trim();
|
|
2509
|
+
current = parentId ? byId.get(parentId) : void 0;
|
|
2510
|
+
}
|
|
2511
|
+
for (const row of messages) {
|
|
2512
|
+
if (row.role && row.role !== "user") continue;
|
|
2513
|
+
pushRow(row);
|
|
2514
|
+
}
|
|
2515
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2516
|
+
const ids = [];
|
|
2517
|
+
for (const ref of refs) {
|
|
2518
|
+
const id = attachmentIdFromRef(ref);
|
|
2519
|
+
if (!id || seen.has(id)) continue;
|
|
2520
|
+
seen.add(id);
|
|
2521
|
+
ids.push(id);
|
|
2522
|
+
}
|
|
2523
|
+
const localPaths = [];
|
|
2524
|
+
for (const id of ids.slice(0, 5)) {
|
|
2525
|
+
const url = `${baseUrl.replace(/\/$/, "")}/api/v1/store/attachments/${encodeURIComponent(id)}`;
|
|
2526
|
+
const res = await fetchImpl(url, {
|
|
2527
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
2528
|
+
});
|
|
2529
|
+
if (!res.ok) {
|
|
2530
|
+
continue;
|
|
2531
|
+
}
|
|
2532
|
+
const mime = res.headers.get("content-type") || "image/png";
|
|
2533
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
2534
|
+
if (!buffer.byteLength) continue;
|
|
2535
|
+
const localPath = import_node_path6.default.join(
|
|
2536
|
+
tempDir,
|
|
2537
|
+
`${id}${extForMime(mime)}`
|
|
2538
|
+
);
|
|
2539
|
+
await import_promises3.default.mkdir(tempDir, { recursive: true });
|
|
2540
|
+
await import_promises3.default.writeFile(localPath, buffer);
|
|
2541
|
+
localPaths.push(localPath);
|
|
2542
|
+
}
|
|
2543
|
+
logger?.debug(
|
|
2544
|
+
{
|
|
2545
|
+
conversationId,
|
|
2546
|
+
messageId,
|
|
2547
|
+
fromMessageIds,
|
|
2548
|
+
stored: ids.length,
|
|
2549
|
+
written: localPaths.length
|
|
2550
|
+
},
|
|
2551
|
+
"materialize attachments"
|
|
2552
|
+
);
|
|
2553
|
+
return localPaths;
|
|
2339
2554
|
}
|
|
2340
2555
|
};
|
|
2341
2556
|
if (options.sync === false) {
|
|
@@ -2361,6 +2576,11 @@ function createMaintainerProStoreFromEnv(options) {
|
|
|
2361
2576
|
return createMaintainerProStore({
|
|
2362
2577
|
baseUrl,
|
|
2363
2578
|
apiKey,
|
|
2579
|
+
tempDir: projectUploadsDir({
|
|
2580
|
+
workspaceDir: process.env.AI_CLI_WORKSPACE || process.cwd(),
|
|
2581
|
+
sandboxId: process.env.MAINTAINER_PRO_SANDBOX_ID,
|
|
2582
|
+
dataDir: process.env.MAINTAINER_PRO_DATA_DIR
|
|
2583
|
+
}),
|
|
2364
2584
|
logger: options?.logger,
|
|
2365
2585
|
log: options?.log,
|
|
2366
2586
|
sync: options?.sync,
|
|
@@ -2369,7 +2589,10 @@ function createMaintainerProStoreFromEnv(options) {
|
|
|
2369
2589
|
}
|
|
2370
2590
|
|
|
2371
2591
|
// src/workspace-inspect.ts
|
|
2592
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
2593
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
2372
2594
|
var import_zod = require("zod");
|
|
2595
|
+
var CONFIG_INSPECT_TIMEOUT_MS = 18e3;
|
|
2373
2596
|
var inspectJsonSchema = import_zod.z.object({
|
|
2374
2597
|
kind: import_zod.z.enum(["next", "vite", "html", "empty", "other"]).optional(),
|
|
2375
2598
|
name: import_zod.z.string().max(200).optional(),
|
|
@@ -2439,6 +2662,154 @@ ${input.extraContext ? `${input.extraContext.trim()}
|
|
|
2439
2662
|
|
|
2440
2663
|
` : ""}Inspect this project, fix any setup gaps you can, and return the JSON report.`;
|
|
2441
2664
|
}
|
|
2665
|
+
function readSnippet(file, max = 4e3) {
|
|
2666
|
+
try {
|
|
2667
|
+
const text = import_node_fs3.default.readFileSync(file, "utf8");
|
|
2668
|
+
return text.length > max ? `${text.slice(0, max)}
|
|
2669
|
+
\u2026` : text;
|
|
2670
|
+
} catch {
|
|
2671
|
+
return null;
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
function envKeysOnly(file) {
|
|
2675
|
+
try {
|
|
2676
|
+
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);
|
|
2677
|
+
return keys.length ? keys.join("\n") : null;
|
|
2678
|
+
} catch {
|
|
2679
|
+
return null;
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
function gatherConfigSnippets(workspaceDir) {
|
|
2683
|
+
const folder = import_node_path7.default.resolve(workspaceDir);
|
|
2684
|
+
const parts = [];
|
|
2685
|
+
const pkg = readSnippet(import_node_path7.default.join(folder, "package.json"));
|
|
2686
|
+
if (pkg) parts.push(`### package.json
|
|
2687
|
+
\`\`\`json
|
|
2688
|
+
${pkg}
|
|
2689
|
+
\`\`\``);
|
|
2690
|
+
for (const lock of [
|
|
2691
|
+
"package-lock.json",
|
|
2692
|
+
"pnpm-lock.yaml",
|
|
2693
|
+
"yarn.lock",
|
|
2694
|
+
"bun.lockb"
|
|
2695
|
+
]) {
|
|
2696
|
+
if (import_node_fs3.default.existsSync(import_node_path7.default.join(folder, lock))) {
|
|
2697
|
+
parts.push(`### lockfile
|
|
2698
|
+
${lock}`);
|
|
2699
|
+
break;
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
for (const rel of [
|
|
2703
|
+
"vite.config.ts",
|
|
2704
|
+
"vite.config.js",
|
|
2705
|
+
"vite.config.mjs",
|
|
2706
|
+
"next.config.js",
|
|
2707
|
+
"next.config.mjs",
|
|
2708
|
+
"next.config.ts"
|
|
2709
|
+
]) {
|
|
2710
|
+
const body = readSnippet(import_node_path7.default.join(folder, rel), 2500);
|
|
2711
|
+
if (body) parts.push(`### ${rel}
|
|
2712
|
+
\`\`\`
|
|
2713
|
+
${body}
|
|
2714
|
+
\`\`\``);
|
|
2715
|
+
}
|
|
2716
|
+
for (const rel of [".env.example", ".env", ".env.local", ".env.development"]) {
|
|
2717
|
+
const keys = envKeysOnly(import_node_path7.default.join(folder, rel));
|
|
2718
|
+
if (keys) parts.push(`### ${rel} keys (values omitted)
|
|
2719
|
+
\`\`\`
|
|
2720
|
+
${keys}
|
|
2721
|
+
\`\`\``);
|
|
2722
|
+
}
|
|
2723
|
+
return parts.join("\n\n") || "(no config files found)";
|
|
2724
|
+
}
|
|
2725
|
+
function configOnlySystemPrompt(input) {
|
|
2726
|
+
return `You are Maintainer Pro's read-only setup advisor.
|
|
2727
|
+
|
|
2728
|
+
Workspace: ${input.workspaceDir}
|
|
2729
|
+
App name: ${input.appName || "the app"}
|
|
2730
|
+
|
|
2731
|
+
You are given configuration file excerpts only.
|
|
2732
|
+
DO NOT edit, create, or delete any files.
|
|
2733
|
+
DO NOT run shell commands, install packages, or start servers.
|
|
2734
|
+
Only infer how this project runs locally from the excerpts.
|
|
2735
|
+
|
|
2736
|
+
Script names you report MUST appear in package.json scripts. Omit chat/ai-server scripts.
|
|
2737
|
+
|
|
2738
|
+
Reply with a short human summary AND one \`\`\`json fence with:
|
|
2739
|
+
|
|
2740
|
+
{
|
|
2741
|
+
"kind": "next" | "vite" | "html" | "empty" | "other",
|
|
2742
|
+
"name": "package or folder name",
|
|
2743
|
+
"summary": "one sentence about this project",
|
|
2744
|
+
"scripts": { "ui": "dev:client", "backend": "dev:server" },
|
|
2745
|
+
"ports": { "ui": 5173, "backend": 4100 },
|
|
2746
|
+
"issues": ["uncertainties"],
|
|
2747
|
+
"fixes": [],
|
|
2748
|
+
"ready": true
|
|
2749
|
+
}`;
|
|
2750
|
+
}
|
|
2751
|
+
function configOnlyUserMessage(input, snippets) {
|
|
2752
|
+
return `${input.extraContext ? `${input.extraContext.trim()}
|
|
2753
|
+
|
|
2754
|
+
` : ""}Analyze these config excerpts and suggest local apps/ports/scripts. Do not change anything on disk.
|
|
2755
|
+
|
|
2756
|
+
${snippets}`;
|
|
2757
|
+
}
|
|
2758
|
+
function toInspectResult(input, text, providerId) {
|
|
2759
|
+
const parsed = extractInspectJson(text);
|
|
2760
|
+
return {
|
|
2761
|
+
kind: parsed?.kind ?? "other",
|
|
2762
|
+
name: parsed?.name?.trim() || input.appName || "",
|
|
2763
|
+
summary: parsed?.summary?.trim() || text.replace(/```json[\s\S]*```/i, "").trim().slice(0, 400),
|
|
2764
|
+
scripts: parsed?.scripts ?? {},
|
|
2765
|
+
ports: parsed?.ports ?? {},
|
|
2766
|
+
issues: parsed?.issues ?? [],
|
|
2767
|
+
fixes: parsed?.fixes ?? [],
|
|
2768
|
+
ready: parsed?.ready ?? parsed?.issues?.length === 0,
|
|
2769
|
+
provider: providerId,
|
|
2770
|
+
rawText: text
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
async function inspectConfigOnly(input, opts = {}) {
|
|
2774
|
+
const workspaceDir = import_node_path7.default.resolve(input.workspaceDir);
|
|
2775
|
+
const timeoutMs = opts.timeoutMs ?? CONFIG_INSPECT_TIMEOUT_MS;
|
|
2776
|
+
const provider = await resolveProvider({ preference: "auto" });
|
|
2777
|
+
const snippets = gatherConfigSnippets(workspaceDir);
|
|
2778
|
+
const call = callAi(
|
|
2779
|
+
[{ role: "user", content: configOnlyUserMessage(input, snippets) }],
|
|
2780
|
+
{
|
|
2781
|
+
route: "/local-setup-propose",
|
|
2782
|
+
pageTitle: input.appName || "Setup suggestions",
|
|
2783
|
+
relevantFiles: ["package.json", ".env.example"],
|
|
2784
|
+
data: {
|
|
2785
|
+
workspaceDir,
|
|
2786
|
+
readOnly: true
|
|
2787
|
+
}
|
|
2788
|
+
},
|
|
2789
|
+
{
|
|
2790
|
+
systemPrompt: configOnlySystemPrompt(input),
|
|
2791
|
+
workspaceDir,
|
|
2792
|
+
technical: true
|
|
2793
|
+
}
|
|
2794
|
+
);
|
|
2795
|
+
const timed = await Promise.race([
|
|
2796
|
+
call.then((response) => ({ ok: true, response })),
|
|
2797
|
+
new Promise(
|
|
2798
|
+
(resolve) => setTimeout(
|
|
2799
|
+
() => resolve({ ok: false, error: `Config inspect timed out after ${timeoutMs}ms` }),
|
|
2800
|
+
timeoutMs
|
|
2801
|
+
)
|
|
2802
|
+
)
|
|
2803
|
+
]);
|
|
2804
|
+
if (!timed.ok) {
|
|
2805
|
+
throw new Error(timed.error);
|
|
2806
|
+
}
|
|
2807
|
+
return toInspectResult(
|
|
2808
|
+
input,
|
|
2809
|
+
timed.response.text,
|
|
2810
|
+
timed.response.provider || provider.id
|
|
2811
|
+
);
|
|
2812
|
+
}
|
|
2442
2813
|
async function inspectAndRepairWorkspace(input) {
|
|
2443
2814
|
const workspaceDir = input.workspaceDir;
|
|
2444
2815
|
const provider = await resolveProvider({ preference: "auto" });
|
|
@@ -2459,25 +2830,671 @@ async function inspectAndRepairWorkspace(input) {
|
|
|
2459
2830
|
technical: true
|
|
2460
2831
|
}
|
|
2461
2832
|
);
|
|
2462
|
-
|
|
2833
|
+
return toInspectResult(
|
|
2834
|
+
input,
|
|
2835
|
+
response.text,
|
|
2836
|
+
response.provider || provider.id
|
|
2837
|
+
);
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
// src/host-apps.ts
|
|
2841
|
+
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
2842
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
2843
|
+
var import_node_crypto4 = require("crypto");
|
|
2844
|
+
var COLLABORATER_DIR = ".collaborater";
|
|
2845
|
+
var AI_SERVER_APP_ID = "ai-server";
|
|
2846
|
+
var AI_SERVER_DEFAULT_PORT = 3100;
|
|
2847
|
+
var ENV_FILES = [".env", ".env.local", ".env.development", ".env.example"];
|
|
2848
|
+
function isPort(value) {
|
|
2849
|
+
return Number.isInteger(value) && Number(value) >= 1024 && Number(value) <= 65535;
|
|
2850
|
+
}
|
|
2851
|
+
function parsePort(value, fallback = 0) {
|
|
2852
|
+
if (typeof value === "number" && isPort(value)) return value;
|
|
2853
|
+
const text = String(value ?? "").trim();
|
|
2854
|
+
if (!text) return fallback;
|
|
2855
|
+
if (/^\d{2,5}$/.test(text)) {
|
|
2856
|
+
const n = Number(text);
|
|
2857
|
+
return isPort(n) ? n : fallback;
|
|
2858
|
+
}
|
|
2859
|
+
try {
|
|
2860
|
+
const port = Number(new URL(text).port);
|
|
2861
|
+
return isPort(port) ? port : fallback;
|
|
2862
|
+
} catch {
|
|
2863
|
+
const match = text.match(/--port(?:\s+|=)(\d{2,5})/i) || text.match(/\bPORT[=:]\s*(\d{2,5})/i) || text.match(/:(\d{2,5})\b/);
|
|
2864
|
+
const n = match ? Number(match[1]) : 0;
|
|
2865
|
+
return isPort(n) ? n : fallback;
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
function dataInput(folder, extra) {
|
|
2463
2869
|
return {
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2870
|
+
workspaceDir: folder,
|
|
2871
|
+
sandboxId: extra?.sandboxId,
|
|
2872
|
+
dataDir: extra?.dataDir
|
|
2873
|
+
};
|
|
2874
|
+
}
|
|
2875
|
+
function hostAppsCachePath2(folder, extra) {
|
|
2876
|
+
return hostAppsCachePath(dataInput(folder, extra));
|
|
2877
|
+
}
|
|
2878
|
+
function defaultAiServerApp(port = AI_SERVER_DEFAULT_PORT) {
|
|
2879
|
+
return {
|
|
2880
|
+
id: AI_SERVER_APP_ID,
|
|
2881
|
+
name: "AI server",
|
|
2882
|
+
role: "ai-server",
|
|
2883
|
+
port: parsePort(port, AI_SERVER_DEFAULT_PORT),
|
|
2884
|
+
startCommand: null,
|
|
2885
|
+
source: "default",
|
|
2886
|
+
locked: true
|
|
2887
|
+
};
|
|
2888
|
+
}
|
|
2889
|
+
function normalizeHostApp(raw) {
|
|
2890
|
+
if (!raw || typeof raw !== "object") return null;
|
|
2891
|
+
const row = raw;
|
|
2892
|
+
const port = parsePort(row.port);
|
|
2893
|
+
if (!port) return null;
|
|
2894
|
+
const role = String(row.role || "custom");
|
|
2895
|
+
const allowed = ["ai-server", "ui", "backend", "app", "custom"];
|
|
2896
|
+
const id = String(row.id || "").trim() || (role === "ai-server" ? AI_SERVER_APP_ID : "");
|
|
2897
|
+
if (!id) return null;
|
|
2898
|
+
return {
|
|
2899
|
+
id: id.slice(0, 64),
|
|
2900
|
+
name: String(row.name || id).trim().slice(0, 80) || id,
|
|
2901
|
+
role: allowed.includes(role) ? role : "custom",
|
|
2902
|
+
port,
|
|
2903
|
+
startCommand: typeof row.startCommand === "string" && row.startCommand.trim() ? row.startCommand.trim().slice(0, 200) : null,
|
|
2904
|
+
source: ["default", "env", "package", "ai", "manual"].includes(
|
|
2905
|
+
row.source
|
|
2906
|
+
) ? row.source : "manual",
|
|
2907
|
+
locked: row.locked === true || id === AI_SERVER_APP_ID || role === "ai-server",
|
|
2908
|
+
host: role !== "ai-server" && id !== AI_SERVER_APP_ID && row.host === true,
|
|
2909
|
+
envMaps: normalizeEnvMaps(row.envMaps)
|
|
2910
|
+
};
|
|
2911
|
+
}
|
|
2912
|
+
function normalizeEnvMaps(raw) {
|
|
2913
|
+
if (!Array.isArray(raw)) return [];
|
|
2914
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2915
|
+
const out = [];
|
|
2916
|
+
for (const item of raw) {
|
|
2917
|
+
if (!item || typeof item !== "object") continue;
|
|
2918
|
+
const row = item;
|
|
2919
|
+
const key = String(row.key || "").trim();
|
|
2920
|
+
const sourceAppId = String(row.sourceAppId || "").trim().slice(0, 64);
|
|
2921
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || key.length > 80) continue;
|
|
2922
|
+
if (!sourceAppId || seen.has(key)) continue;
|
|
2923
|
+
seen.add(key);
|
|
2924
|
+
out.push({ key, sourceAppId });
|
|
2925
|
+
}
|
|
2926
|
+
return out.slice(0, 30);
|
|
2927
|
+
}
|
|
2928
|
+
function ensureSingleHost(apps) {
|
|
2929
|
+
const next = apps.map((app) => ({
|
|
2930
|
+
...app,
|
|
2931
|
+
host: app.role !== "ai-server" && app.id !== AI_SERVER_APP_ID && app.host === true
|
|
2932
|
+
}));
|
|
2933
|
+
const marked = next.filter((app) => app.host);
|
|
2934
|
+
if (marked.length === 1) return next;
|
|
2935
|
+
if (marked.length > 1) {
|
|
2936
|
+
let kept = false;
|
|
2937
|
+
return next.map((app) => {
|
|
2938
|
+
if (!app.host) return app;
|
|
2939
|
+
if (kept) return { ...app, host: false };
|
|
2940
|
+
kept = true;
|
|
2941
|
+
return app;
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
const fallback = next.find((app) => app.role === "ui" || app.role === "app") || next.find((app) => app.role !== "ai-server");
|
|
2945
|
+
return next.map(
|
|
2946
|
+
(app) => fallback && app.id === fallback.id ? { ...app, host: true } : app
|
|
2947
|
+
);
|
|
2948
|
+
}
|
|
2949
|
+
function normalizeHostApps(raw) {
|
|
2950
|
+
if (!Array.isArray(raw)) return [];
|
|
2951
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2952
|
+
const apps = [];
|
|
2953
|
+
for (const item of raw) {
|
|
2954
|
+
const app = normalizeHostApp(item);
|
|
2955
|
+
if (!app || seen.has(app.id)) continue;
|
|
2956
|
+
seen.add(app.id);
|
|
2957
|
+
apps.push(app);
|
|
2958
|
+
}
|
|
2959
|
+
return apps;
|
|
2960
|
+
}
|
|
2961
|
+
function ensureAiServerApp(apps, preferredPort = AI_SERVER_DEFAULT_PORT) {
|
|
2962
|
+
const next = normalizeHostApps(apps);
|
|
2963
|
+
const existing = next.find((app) => app.id === AI_SERVER_APP_ID || app.role === "ai-server");
|
|
2964
|
+
if (existing) {
|
|
2965
|
+
existing.id = AI_SERVER_APP_ID;
|
|
2966
|
+
existing.role = "ai-server";
|
|
2967
|
+
existing.locked = true;
|
|
2968
|
+
existing.host = false;
|
|
2969
|
+
existing.name = existing.name || "AI server";
|
|
2970
|
+
return ensureSingleHost([existing, ...next.filter((app) => app !== existing)]);
|
|
2971
|
+
}
|
|
2972
|
+
return ensureSingleHost([defaultAiServerApp(preferredPort), ...next]);
|
|
2973
|
+
}
|
|
2974
|
+
function readText(file) {
|
|
2975
|
+
try {
|
|
2976
|
+
return import_node_fs4.default.readFileSync(file, "utf8");
|
|
2977
|
+
} catch {
|
|
2978
|
+
return null;
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
function readEnvFileMap(file) {
|
|
2982
|
+
const text = readText(file);
|
|
2983
|
+
if (!text) return {};
|
|
2984
|
+
const map = {};
|
|
2985
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
2986
|
+
const line = raw.trim();
|
|
2987
|
+
if (!line || line.startsWith("#")) continue;
|
|
2988
|
+
const eq = line.indexOf("=");
|
|
2989
|
+
if (eq < 1) continue;
|
|
2990
|
+
const key = line.slice(0, eq).trim();
|
|
2991
|
+
let value = line.slice(eq + 1).trim();
|
|
2992
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
2993
|
+
value = value.slice(1, -1);
|
|
2994
|
+
}
|
|
2995
|
+
if (key) map[key] = value;
|
|
2996
|
+
}
|
|
2997
|
+
return map;
|
|
2998
|
+
}
|
|
2999
|
+
function readProjectEnvLayers(folder) {
|
|
3000
|
+
const resolved = import_node_path8.default.resolve(folder);
|
|
3001
|
+
const layers = [];
|
|
3002
|
+
const merged = {};
|
|
3003
|
+
for (const name of ENV_FILES) {
|
|
3004
|
+
const file = import_node_path8.default.join(resolved, name);
|
|
3005
|
+
const values = readEnvFileMap(file);
|
|
3006
|
+
if (!Object.keys(values).length) continue;
|
|
3007
|
+
layers.push({ file: name, values });
|
|
3008
|
+
if (name === ".env.example") continue;
|
|
3009
|
+
Object.assign(merged, values);
|
|
3010
|
+
}
|
|
3011
|
+
return { merged, layers };
|
|
3012
|
+
}
|
|
3013
|
+
function readPackageJson(folder) {
|
|
3014
|
+
try {
|
|
3015
|
+
return JSON.parse(import_node_fs4.default.readFileSync(import_node_path8.default.join(folder, "package.json"), "utf8"));
|
|
3016
|
+
} catch {
|
|
3017
|
+
return null;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
function isChatScript(name, command) {
|
|
3021
|
+
return /\b(ai-server|ai-cli|dev:chat|start:chat)\b/i.test(`${name} ${command}`);
|
|
3022
|
+
}
|
|
3023
|
+
function isUiCommand(command) {
|
|
3024
|
+
return /\b(vite|next|nuxt|astro|remix|react-scripts|webpack-dev-server|parcel)\b/i.test(
|
|
3025
|
+
command
|
|
3026
|
+
);
|
|
3027
|
+
}
|
|
3028
|
+
function configPort(folder, files) {
|
|
3029
|
+
for (const rel of files) {
|
|
3030
|
+
const text = readText(import_node_path8.default.join(folder, rel));
|
|
3031
|
+
if (!text) continue;
|
|
3032
|
+
const match = text.match(/\bport\s*[:=]\s*(\d{2,5})/i) || text.match(/--port(?:\s+|=)(\d{2,5})/i);
|
|
3033
|
+
const port = parsePort(match?.[1]);
|
|
3034
|
+
if (port) return port;
|
|
3035
|
+
}
|
|
3036
|
+
return 0;
|
|
3037
|
+
}
|
|
3038
|
+
function hostAppsFingerprint(folder) {
|
|
3039
|
+
const resolved = import_node_path8.default.resolve(folder);
|
|
3040
|
+
const parts = [];
|
|
3041
|
+
for (const rel of [
|
|
3042
|
+
"package.json",
|
|
3043
|
+
...ENV_FILES,
|
|
3044
|
+
"vite.config.ts",
|
|
3045
|
+
"vite.config.js",
|
|
3046
|
+
"vite.config.mjs",
|
|
3047
|
+
"next.config.js",
|
|
3048
|
+
"next.config.mjs",
|
|
3049
|
+
"next.config.ts"
|
|
3050
|
+
]) {
|
|
3051
|
+
const file = import_node_path8.default.join(resolved, rel);
|
|
3052
|
+
if (!import_node_fs4.default.existsSync(file)) continue;
|
|
3053
|
+
try {
|
|
3054
|
+
const st = import_node_fs4.default.statSync(file);
|
|
3055
|
+
parts.push(`${rel}:${st.size}:${Math.floor(st.mtimeMs)}`);
|
|
3056
|
+
} catch {
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
return (0, import_node_crypto4.createHash)("sha256").update(parts.join("|") || resolved).digest("hex").slice(0, 32);
|
|
3060
|
+
}
|
|
3061
|
+
function pickScript(scripts, names) {
|
|
3062
|
+
return names.find((name) => scripts[name] && !isChatScript(name, scripts[name])) || null;
|
|
3063
|
+
}
|
|
3064
|
+
function detectHostAppsFromFiles(folder, opts = {}) {
|
|
3065
|
+
const resolved = import_node_path8.default.resolve(folder);
|
|
3066
|
+
const reasons = [];
|
|
3067
|
+
const { merged, layers } = readProjectEnvLayers(resolved);
|
|
3068
|
+
const pkg = readPackageJson(resolved);
|
|
3069
|
+
const scripts = pkg?.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
|
|
3070
|
+
const apps = [];
|
|
3071
|
+
const envAi = parsePort(merged.AI_SERVER_PORT) || parsePort(merged.AI_SERVER_URL);
|
|
3072
|
+
const aiPort = envAi || parsePort(opts.preferredAiPort, AI_SERVER_DEFAULT_PORT);
|
|
3073
|
+
apps.push({
|
|
3074
|
+
...defaultAiServerApp(aiPort),
|
|
3075
|
+
source: envAi ? "env" : "default"
|
|
3076
|
+
});
|
|
3077
|
+
const envPorts = {
|
|
3078
|
+
PORT: layers.filter((layer) => layer.file !== ".env.example").map((layer) => ({ file: layer.file, port: parsePort(layer.values.PORT) })).filter((row) => row.port),
|
|
3079
|
+
APP_URL: parsePort(merged.APP_URL || merged.NEXT_PUBLIC_APP_URL || merged.PUBLIC_URL),
|
|
3080
|
+
API_PORT: parsePort(merged.API_PORT || merged.API_URL || merged.VITE_API_URL)
|
|
3081
|
+
};
|
|
3082
|
+
const portValues = [...new Set(envPorts.PORT.map((row) => row.port))];
|
|
3083
|
+
if (portValues.length > 1) {
|
|
3084
|
+
reasons.push(
|
|
3085
|
+
`PORT differs across env files: ${envPorts.PORT.map((row) => `${row.file}=${row.port}`).join(", ")}`
|
|
3086
|
+
);
|
|
3087
|
+
}
|
|
3088
|
+
if (envPorts.PORT[0] && envPorts.APP_URL && envPorts.PORT[0].port !== envPorts.APP_URL) {
|
|
3089
|
+
reasons.push(
|
|
3090
|
+
`PORT=${envPorts.PORT[0].port} does not match APP_URL port ${envPorts.APP_URL}`
|
|
3091
|
+
);
|
|
3092
|
+
}
|
|
3093
|
+
const uiScript = pickScript(scripts, [
|
|
3094
|
+
"dev:client",
|
|
3095
|
+
"dev:ui",
|
|
3096
|
+
"dev:web",
|
|
3097
|
+
"dev:frontend",
|
|
3098
|
+
"client",
|
|
3099
|
+
"start:client"
|
|
3100
|
+
]) || (scripts.dev && !isChatScript("dev", scripts.dev) ? "dev" : null);
|
|
3101
|
+
const apiScript = pickScript(scripts, [
|
|
3102
|
+
"dev:server",
|
|
3103
|
+
"dev:backend",
|
|
3104
|
+
"dev:api",
|
|
3105
|
+
"server",
|
|
3106
|
+
"start:server"
|
|
3107
|
+
]);
|
|
3108
|
+
const vitePort = configPort(resolved, ["vite.config.ts", "vite.config.js", "vite.config.mjs"]) || 5173;
|
|
3109
|
+
const nextPort = configPort(resolved, ["next.config.js", "next.config.mjs", "next.config.ts"]) || 3e3;
|
|
3110
|
+
const portFromEnv = parsePort(merged.PORT);
|
|
3111
|
+
const hostFromEnv = parsePort(merged.HOST_PORT) || parsePort(merged.HOST_URL) || parsePort(merged.CORS_ORIGIN);
|
|
3112
|
+
const uiFromEnv = hostFromEnv || envPorts.APP_URL || (portFromEnv && portFromEnv !== aiPort ? portFromEnv : 0);
|
|
3113
|
+
if (uiScript) {
|
|
3114
|
+
const command = scripts[uiScript] || "";
|
|
3115
|
+
const scriptPort = parsePort(command);
|
|
3116
|
+
const fallback = isUiCommand(command) && /vite/i.test(command) ? vitePort : nextPort;
|
|
3117
|
+
const port = uiFromEnv || scriptPort || fallback;
|
|
3118
|
+
apps.push({
|
|
3119
|
+
id: "ui",
|
|
3120
|
+
name: opts.appName || pkg?.name || "App",
|
|
3121
|
+
role: isUiCommand(command) ? "ui" : "app",
|
|
3122
|
+
port,
|
|
3123
|
+
startCommand: `npm run ${uiScript}`,
|
|
3124
|
+
source: uiFromEnv ? "env" : scriptPort ? "package" : "package",
|
|
3125
|
+
host: true
|
|
3126
|
+
});
|
|
3127
|
+
} else if (uiFromEnv) {
|
|
3128
|
+
apps.push({
|
|
3129
|
+
id: "ui",
|
|
3130
|
+
name: opts.appName || pkg?.name || "App",
|
|
3131
|
+
role: "ui",
|
|
3132
|
+
port: uiFromEnv,
|
|
3133
|
+
startCommand: scripts.dev ? "npm run dev" : null,
|
|
3134
|
+
source: "env",
|
|
3135
|
+
host: true
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3138
|
+
if (apiScript && apiScript !== uiScript) {
|
|
3139
|
+
const command = scripts[apiScript] || "";
|
|
3140
|
+
const port = envPorts.API_PORT || parsePort(command) || configPort(resolved, []) || 4100;
|
|
3141
|
+
apps.push({
|
|
3142
|
+
id: "backend",
|
|
3143
|
+
name: "Backend",
|
|
3144
|
+
role: "backend",
|
|
3145
|
+
port,
|
|
3146
|
+
startCommand: `npm run ${apiScript}`,
|
|
3147
|
+
source: envPorts.API_PORT ? "env" : "package"
|
|
3148
|
+
});
|
|
3149
|
+
}
|
|
3150
|
+
const hostApps = apps.filter((app) => app.role !== "ai-server");
|
|
3151
|
+
if (pkg && Object.keys(scripts).length && hostApps.length === 0) {
|
|
3152
|
+
reasons.push("Found package.json scripts but no host app port in env or config");
|
|
3153
|
+
}
|
|
3154
|
+
return {
|
|
3155
|
+
apps: ensureAiServerApp(apps, aiPort),
|
|
3156
|
+
reasons,
|
|
3157
|
+
confused: reasons.length > 0
|
|
3158
|
+
};
|
|
3159
|
+
}
|
|
3160
|
+
function readHostAppsCache(folder, extra) {
|
|
3161
|
+
try {
|
|
3162
|
+
const raw = JSON.parse(
|
|
3163
|
+
import_node_fs4.default.readFileSync(hostAppsCachePath2(folder, extra), "utf8")
|
|
3164
|
+
);
|
|
3165
|
+
const apps = ensureAiServerApp(normalizeHostApps(raw.apps));
|
|
3166
|
+
if (!apps.length) return null;
|
|
3167
|
+
return {
|
|
3168
|
+
apps,
|
|
3169
|
+
fingerprint: typeof raw.fingerprint === "string" ? raw.fingerprint : void 0,
|
|
3170
|
+
updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : void 0
|
|
3171
|
+
};
|
|
3172
|
+
} catch {
|
|
3173
|
+
return null;
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
var readCollaboraterApps = readHostAppsCache;
|
|
3177
|
+
function writeHostAppsCache(folder, apps, extra = {}, loc) {
|
|
3178
|
+
const resolved = import_node_path8.default.resolve(folder);
|
|
3179
|
+
ensureProjectDataDir(dataInput(resolved, loc));
|
|
3180
|
+
const payload = {
|
|
3181
|
+
version: 1,
|
|
3182
|
+
fingerprint: hostAppsFingerprint(resolved),
|
|
3183
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3184
|
+
apps: ensureAiServerApp(apps),
|
|
3185
|
+
...extra
|
|
3186
|
+
};
|
|
3187
|
+
import_node_fs4.default.writeFileSync(
|
|
3188
|
+
hostAppsCachePath2(resolved, loc),
|
|
3189
|
+
`${JSON.stringify(payload, null, 2)}
|
|
3190
|
+
`,
|
|
3191
|
+
"utf8"
|
|
3192
|
+
);
|
|
3193
|
+
}
|
|
3194
|
+
var writeCollaboraterApps = writeHostAppsCache;
|
|
3195
|
+
function mergeDesiredHostApps(desired, detected) {
|
|
3196
|
+
const byId = new Map(detected.map((app) => [app.id, app]));
|
|
3197
|
+
const byRole = new Map(detected.map((app) => [app.role, app]));
|
|
3198
|
+
const merged = desired.map((app) => {
|
|
3199
|
+
const local = byId.get(app.id) || (app.role !== "custom" ? byRole.get(app.role) : void 0);
|
|
3200
|
+
return {
|
|
3201
|
+
...local,
|
|
3202
|
+
...app,
|
|
3203
|
+
startCommand: app.startCommand || local?.startCommand || null,
|
|
3204
|
+
locked: app.locked || app.id === AI_SERVER_APP_ID || app.role === "ai-server",
|
|
3205
|
+
host: app.role === "ai-server" || app.id === AI_SERVER_APP_ID ? false : app.host === true || app.host !== false && local?.host === true,
|
|
3206
|
+
envMaps: app.envMaps && app.envMaps.length ? app.envMaps : local?.envMaps || []
|
|
3207
|
+
};
|
|
3208
|
+
});
|
|
3209
|
+
return ensureAiServerApp(merged);
|
|
3210
|
+
}
|
|
3211
|
+
function appsFromInspectPorts(ports, scripts, name) {
|
|
3212
|
+
const apps = [];
|
|
3213
|
+
if (ports.ui || scripts.ui) {
|
|
3214
|
+
apps.push({
|
|
3215
|
+
id: "ui",
|
|
3216
|
+
name,
|
|
3217
|
+
role: "ui",
|
|
3218
|
+
port: parsePort(ports.ui, 5173),
|
|
3219
|
+
startCommand: scripts.ui ? `npm run ${scripts.ui}` : null,
|
|
3220
|
+
source: "ai"
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3223
|
+
if (ports.backend || scripts.backend) {
|
|
3224
|
+
apps.push({
|
|
3225
|
+
id: "backend",
|
|
3226
|
+
name: "Backend",
|
|
3227
|
+
role: "backend",
|
|
3228
|
+
port: parsePort(ports.backend, 4100),
|
|
3229
|
+
startCommand: scripts.backend ? `npm run ${scripts.backend}` : null,
|
|
3230
|
+
source: "ai"
|
|
3231
|
+
});
|
|
3232
|
+
}
|
|
3233
|
+
if (!apps.length && (ports.app || scripts.app)) {
|
|
3234
|
+
apps.push({
|
|
3235
|
+
id: "app",
|
|
3236
|
+
name,
|
|
3237
|
+
role: "app",
|
|
3238
|
+
port: parsePort(ports.app, 3e3),
|
|
3239
|
+
startCommand: scripts.app ? `npm run ${scripts.app}` : null,
|
|
3240
|
+
source: "ai"
|
|
3241
|
+
});
|
|
3242
|
+
}
|
|
3243
|
+
return apps;
|
|
3244
|
+
}
|
|
3245
|
+
function collectScriptAlternatives(folder, primary, opts = {}) {
|
|
3246
|
+
const pkg = readPackageJson(folder);
|
|
3247
|
+
const scripts = pkg?.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
|
|
3248
|
+
const usedCommands = new Set(
|
|
3249
|
+
primary.map((app) => app.startCommand?.trim()).filter((cmd) => Boolean(cmd))
|
|
3250
|
+
);
|
|
3251
|
+
const alternatives = [];
|
|
3252
|
+
const hostApp = primary.find((app) => app.role === "ui" || app.role === "app") || primary.find((app) => app.role !== "ai-server");
|
|
3253
|
+
const backendApp = primary.find((app) => app.role === "backend");
|
|
3254
|
+
for (const [name, command] of Object.entries(scripts)) {
|
|
3255
|
+
if (isChatScript(name, command)) continue;
|
|
3256
|
+
if (!/^(dev|start|serve)/i.test(name) && !isUiCommand(command)) continue;
|
|
3257
|
+
const startCommand = `npm run ${name}`;
|
|
3258
|
+
if (usedCommands.has(startCommand)) continue;
|
|
3259
|
+
const scriptPort = parsePort(command);
|
|
3260
|
+
const looksBackend = /\b(server|api|backend)\b/i.test(`${name} ${command}`) && !isUiCommand(command);
|
|
3261
|
+
const target = looksBackend ? backendApp || hostApp : hostApp;
|
|
3262
|
+
if (!target) continue;
|
|
3263
|
+
const fallbackPort = scriptPort || (looksBackend ? 4100 : isUiCommand(command) && /vite/i.test(command) ? 5173 : 3e3);
|
|
3264
|
+
alternatives.push({
|
|
3265
|
+
appId: target.id,
|
|
3266
|
+
port: fallbackPort,
|
|
3267
|
+
startCommand,
|
|
3268
|
+
label: `${name} \u2192 ${fallbackPort}`,
|
|
3269
|
+
source: "package"
|
|
3270
|
+
});
|
|
3271
|
+
if (alternatives.length >= 8) break;
|
|
3272
|
+
}
|
|
3273
|
+
const { layers } = readProjectEnvLayers(folder);
|
|
3274
|
+
if (hostApp) {
|
|
3275
|
+
for (const layer of layers) {
|
|
3276
|
+
if (layer.file === ".env.example") continue;
|
|
3277
|
+
const port = parsePort(layer.values.PORT);
|
|
3278
|
+
if (!port || port === hostApp.port) continue;
|
|
3279
|
+
if (alternatives.some(
|
|
3280
|
+
(alt) => alt.appId === hostApp.id && alt.port === port && !alt.startCommand
|
|
3281
|
+
)) {
|
|
3282
|
+
continue;
|
|
3283
|
+
}
|
|
3284
|
+
alternatives.push({
|
|
3285
|
+
appId: hostApp.id,
|
|
3286
|
+
port,
|
|
3287
|
+
startCommand: hostApp.startCommand || null,
|
|
3288
|
+
label: `${layer.file} PORT=${port}`,
|
|
3289
|
+
source: "env"
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
void opts;
|
|
3294
|
+
return alternatives;
|
|
3295
|
+
}
|
|
3296
|
+
function proposalConfidence(apps, reasons, usedAi) {
|
|
3297
|
+
const hostApps = apps.filter((app) => app.role !== "ai-server");
|
|
3298
|
+
if (!hostApps.length) return "low";
|
|
3299
|
+
if (reasons.length === 0 && hostApps.every((app) => app.startCommand)) {
|
|
3300
|
+
return usedAi ? "medium" : "high";
|
|
3301
|
+
}
|
|
3302
|
+
if (reasons.some((reason) => /differ|does not match|no host app/i.test(reason))) {
|
|
3303
|
+
return "low";
|
|
3304
|
+
}
|
|
3305
|
+
return "medium";
|
|
3306
|
+
}
|
|
3307
|
+
function projectSummaryFromDetect(folder, apps, opts = {}) {
|
|
3308
|
+
const pkg = readPackageJson(folder);
|
|
3309
|
+
const name = opts.appName || pkg?.name || import_node_path8.default.basename(import_node_path8.default.resolve(folder));
|
|
3310
|
+
const hostApps = apps.filter((app) => app.role !== "ai-server");
|
|
3311
|
+
if (!hostApps.length) {
|
|
3312
|
+
return `${name}: no UI/backend ports detected from config yet.`;
|
|
3313
|
+
}
|
|
3314
|
+
return `${name}: ${hostApps.map((app) => `${app.name} on ${app.port}${app.startCommand ? ` (${app.startCommand})` : ""}`).join(", ")}.`;
|
|
3315
|
+
}
|
|
3316
|
+
async function proposeHostAppsFromConfig(input) {
|
|
3317
|
+
const folder = import_node_path8.default.resolve(input.workspaceDir);
|
|
3318
|
+
const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
|
|
3319
|
+
const fingerprint = hostAppsFingerprint(folder);
|
|
3320
|
+
const allowAi = input.allowAi !== false;
|
|
3321
|
+
const detected = detectHostAppsFromFiles(folder, {
|
|
3322
|
+
preferredAiPort: preferredAi,
|
|
3323
|
+
appName: input.appName
|
|
3324
|
+
});
|
|
3325
|
+
let apps = ensureAiServerApp(detected.apps, preferredAi);
|
|
3326
|
+
let reasons = [...detected.reasons];
|
|
3327
|
+
let usedAi = false;
|
|
3328
|
+
let projectSummary = projectSummaryFromDetect(folder, apps, {
|
|
3329
|
+
appName: input.appName
|
|
3330
|
+
});
|
|
3331
|
+
const hostCount = apps.filter((app) => app.role !== "ai-server").length;
|
|
3332
|
+
const needsAi = allowAi && (detected.confused || hostCount === 0 || reasons.some((reason) => /differ|does not match/i.test(reason)));
|
|
3333
|
+
if (needsAi) {
|
|
3334
|
+
try {
|
|
3335
|
+
const inspected = await inspectConfigOnly({
|
|
3336
|
+
workspaceDir: folder,
|
|
3337
|
+
appName: input.appName,
|
|
3338
|
+
extraContext: [
|
|
3339
|
+
"File-based detection was uncertain or incomplete.",
|
|
3340
|
+
...reasons
|
|
3341
|
+
].join("\n")
|
|
3342
|
+
});
|
|
3343
|
+
usedAi = true;
|
|
3344
|
+
const fromAi = appsFromInspectPorts(
|
|
3345
|
+
inspected.ports,
|
|
3346
|
+
inspected.scripts,
|
|
3347
|
+
inspected.name || input.appName || "App"
|
|
3348
|
+
);
|
|
3349
|
+
if (fromAi.length) {
|
|
3350
|
+
apps = ensureAiServerApp(
|
|
3351
|
+
[defaultAiServerApp(preferredAi), ...fromAi],
|
|
3352
|
+
preferredAi
|
|
3353
|
+
);
|
|
3354
|
+
}
|
|
3355
|
+
if (inspected.summary) projectSummary = inspected.summary;
|
|
3356
|
+
if (inspected.issues?.length) {
|
|
3357
|
+
reasons = [...reasons, ...inspected.issues];
|
|
3358
|
+
}
|
|
3359
|
+
} catch (err) {
|
|
3360
|
+
reasons.push(
|
|
3361
|
+
`AI suggest skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
3362
|
+
);
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
const alternatives = collectScriptAlternatives(folder, apps, {
|
|
3366
|
+
appName: input.appName
|
|
3367
|
+
});
|
|
3368
|
+
const confidence = proposalConfidence(apps, reasons, usedAi);
|
|
3369
|
+
const needsReview = confidence !== "high" || detected.confused || apps.filter((app) => app.role !== "ai-server").length === 0;
|
|
3370
|
+
return {
|
|
3371
|
+
apps,
|
|
3372
|
+
alternatives,
|
|
3373
|
+
reasons,
|
|
3374
|
+
confidence,
|
|
3375
|
+
projectSummary,
|
|
3376
|
+
usedAi,
|
|
3377
|
+
needsReview,
|
|
3378
|
+
fingerprint
|
|
3379
|
+
};
|
|
3380
|
+
}
|
|
3381
|
+
async function resolveHostApps(input) {
|
|
3382
|
+
const folder = import_node_path8.default.resolve(input.workspaceDir);
|
|
3383
|
+
const loc = { sandboxId: input.sandboxId, dataDir: input.dataDir };
|
|
3384
|
+
const fingerprint = hostAppsFingerprint(folder);
|
|
3385
|
+
const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
|
|
3386
|
+
const detected = detectHostAppsFromFiles(folder, {
|
|
3387
|
+
preferredAiPort: preferredAi,
|
|
3388
|
+
appName: input.appName
|
|
3389
|
+
});
|
|
3390
|
+
const desired = normalizeHostApps(input.desired);
|
|
3391
|
+
if (desired.length) {
|
|
3392
|
+
const apps2 = mergeDesiredHostApps(desired, detected.apps);
|
|
3393
|
+
try {
|
|
3394
|
+
writeHostAppsCache(folder, apps2, { source: "desired" }, loc);
|
|
3395
|
+
} catch {
|
|
3396
|
+
}
|
|
3397
|
+
return {
|
|
3398
|
+
apps: apps2,
|
|
3399
|
+
source: "desired",
|
|
3400
|
+
cached: false,
|
|
3401
|
+
usedAi: false,
|
|
3402
|
+
confused: false,
|
|
3403
|
+
reasons: [],
|
|
3404
|
+
fingerprint
|
|
3405
|
+
};
|
|
3406
|
+
}
|
|
3407
|
+
if (!input.force) {
|
|
3408
|
+
const cached = readHostAppsCache(folder, loc);
|
|
3409
|
+
if (cached && cached.fingerprint === fingerprint) {
|
|
3410
|
+
return {
|
|
3411
|
+
apps: ensureAiServerApp(cached.apps, preferredAi),
|
|
3412
|
+
source: "cache",
|
|
3413
|
+
cached: true,
|
|
3414
|
+
usedAi: false,
|
|
3415
|
+
confused: false,
|
|
3416
|
+
reasons: [],
|
|
3417
|
+
fingerprint
|
|
3418
|
+
};
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
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)));
|
|
3422
|
+
if (needsAi) {
|
|
3423
|
+
try {
|
|
3424
|
+
const inspected = await inspectConfigOnly({
|
|
3425
|
+
workspaceDir: folder,
|
|
3426
|
+
appName: input.appName,
|
|
3427
|
+
extraContext: `Port detection was uncertain:
|
|
3428
|
+
${detected.reasons.join("\n")}
|
|
3429
|
+
Return the real local ports and npm script names.`
|
|
3430
|
+
});
|
|
3431
|
+
const fromAi = appsFromInspectPorts(
|
|
3432
|
+
inspected.ports,
|
|
3433
|
+
inspected.scripts,
|
|
3434
|
+
inspected.name || input.appName || "App"
|
|
3435
|
+
);
|
|
3436
|
+
const apps2 = ensureAiServerApp(
|
|
3437
|
+
[defaultAiServerApp(preferredAi), ...fromAi],
|
|
3438
|
+
preferredAi
|
|
3439
|
+
);
|
|
3440
|
+
writeHostAppsCache(
|
|
3441
|
+
folder,
|
|
3442
|
+
apps2,
|
|
3443
|
+
{
|
|
3444
|
+
source: "ai",
|
|
3445
|
+
reasons: detected.reasons,
|
|
3446
|
+
summary: inspected.summary
|
|
3447
|
+
},
|
|
3448
|
+
loc
|
|
3449
|
+
);
|
|
3450
|
+
return {
|
|
3451
|
+
apps: apps2,
|
|
3452
|
+
source: "ai",
|
|
3453
|
+
cached: false,
|
|
3454
|
+
usedAi: true,
|
|
3455
|
+
confused: detected.confused,
|
|
3456
|
+
reasons: detected.reasons,
|
|
3457
|
+
fingerprint
|
|
3458
|
+
};
|
|
3459
|
+
} catch (err) {
|
|
3460
|
+
detected.reasons.push(
|
|
3461
|
+
`AI inspect skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
3462
|
+
);
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
const apps = ensureAiServerApp(detected.apps, preferredAi);
|
|
3466
|
+
try {
|
|
3467
|
+
writeHostAppsCache(
|
|
3468
|
+
folder,
|
|
3469
|
+
apps,
|
|
3470
|
+
{
|
|
3471
|
+
source: detected.confused ? "env" : "env",
|
|
3472
|
+
reasons: detected.reasons
|
|
3473
|
+
},
|
|
3474
|
+
loc
|
|
3475
|
+
);
|
|
3476
|
+
} catch {
|
|
3477
|
+
}
|
|
3478
|
+
return {
|
|
3479
|
+
apps,
|
|
3480
|
+
source: apps.some((app) => app.source === "env") ? "env" : "default",
|
|
3481
|
+
cached: false,
|
|
3482
|
+
usedAi: false,
|
|
3483
|
+
confused: detected.confused,
|
|
3484
|
+
reasons: detected.reasons,
|
|
3485
|
+
fingerprint
|
|
2474
3486
|
};
|
|
2475
3487
|
}
|
|
2476
3488
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2477
3489
|
0 && (module.exports = {
|
|
2478
3490
|
ACCESS_IGNORE_BEGIN,
|
|
2479
3491
|
ACCESS_IGNORE_END,
|
|
3492
|
+
AI_SERVER_APP_ID,
|
|
3493
|
+
AI_SERVER_DEFAULT_PORT,
|
|
3494
|
+
COLLABORATER_DIR,
|
|
2480
3495
|
DEFAULT_AI_IGNORE_PATHS,
|
|
3496
|
+
HOST_APPS_FILE,
|
|
3497
|
+
MAINTAINER_PRO_HOME_DIR,
|
|
2481
3498
|
PROMPT_SECTION,
|
|
2482
3499
|
WORKING_PROVIDER,
|
|
2483
3500
|
buildClaudeUserPrompt,
|
|
@@ -2500,27 +3517,53 @@ async function inspectAndRepairWorkspace(input) {
|
|
|
2500
3517
|
createMaintainerProStoreFromEnv,
|
|
2501
3518
|
createSyncedChatStore,
|
|
2502
3519
|
createToolValidator,
|
|
3520
|
+
defaultAiServerApp,
|
|
3521
|
+
detectHostAppsFromFiles,
|
|
3522
|
+
ensureAiServerApp,
|
|
3523
|
+
ensureProjectDataDir,
|
|
3524
|
+
ensureSingleHost,
|
|
2503
3525
|
formatAccessPolicyPromptSection,
|
|
2504
3526
|
formatClientContext,
|
|
2505
3527
|
formatParentChainContext,
|
|
2506
3528
|
getProviderPreference,
|
|
3529
|
+
hostAppsCachePath,
|
|
3530
|
+
hostAppsFingerprint,
|
|
2507
3531
|
inspectAndRepairWorkspace,
|
|
3532
|
+
inspectConfigOnly,
|
|
2508
3533
|
isDevMode,
|
|
2509
3534
|
isIgnoredRelative,
|
|
2510
3535
|
isInsideWorkspace,
|
|
2511
3536
|
isPathAllowed,
|
|
3537
|
+
maintainerProHome,
|
|
3538
|
+
mergeDesiredHostApps,
|
|
3539
|
+
normalizeEnvMaps,
|
|
3540
|
+
normalizeHostApp,
|
|
3541
|
+
normalizeHostApps,
|
|
2512
3542
|
normalizeIgnorePaths,
|
|
2513
3543
|
parentChainForTurn,
|
|
2514
3544
|
parseAiResponse,
|
|
2515
3545
|
parseIgnorePathsEnv,
|
|
3546
|
+
parsePort,
|
|
2516
3547
|
previewText,
|
|
3548
|
+
projectHostAppsPath,
|
|
3549
|
+
projectIdForFolder,
|
|
3550
|
+
projectUploadsDir,
|
|
3551
|
+
proposeHostAppsFromConfig,
|
|
2517
3552
|
providerLabel,
|
|
3553
|
+
readCollaboraterApps,
|
|
3554
|
+
readHostAppsCache,
|
|
3555
|
+
readProjectEnvLayers,
|
|
2518
3556
|
renderManagedIgnoreBlock,
|
|
2519
3557
|
resolveCliBinary,
|
|
3558
|
+
resolveHostApps,
|
|
2520
3559
|
resolveIgnorePaths,
|
|
2521
3560
|
resolveLogLevel,
|
|
3561
|
+
resolveProjectDataDir,
|
|
2522
3562
|
resolveProvider,
|
|
3563
|
+
sanitizeProjectId,
|
|
2523
3564
|
saveChatAttachments,
|
|
2524
3565
|
toNextRoute,
|
|
2525
|
-
upsertManagedIgnoreFile
|
|
3566
|
+
upsertManagedIgnoreFile,
|
|
3567
|
+
writeCollaboraterApps,
|
|
3568
|
+
writeHostAppsCache
|
|
2526
3569
|
});
|