@maintainer-pro/ai-cli 0.1.5 → 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 +1093 -51
- package/dist/index.d.cts +176 -6
- package/dist/index.d.ts +176 -6
- package/dist/index.js +1062 -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
|
);
|
|
@@ -463,8 +494,8 @@ async function resolveCommandPath(command) {
|
|
|
463
494
|
}
|
|
464
495
|
const result = await (0, import_execa2.execa)("which", [command], { reject: false });
|
|
465
496
|
if (result.exitCode !== 0) return null;
|
|
466
|
-
const
|
|
467
|
-
return
|
|
497
|
+
const path9 = result.stdout.trim().split(/\r?\n/)[0]?.trim();
|
|
498
|
+
return path9 || null;
|
|
468
499
|
} catch {
|
|
469
500
|
return null;
|
|
470
501
|
}
|
|
@@ -611,7 +642,7 @@ Only use those runtime tools for live state. Never invent runtime tools.` : "";
|
|
|
611
642
|
- You may only read or edit files inside the current project workspace directory.
|
|
612
643
|
- Never read, write, move, or delete files outside that workspace.
|
|
613
644
|
- 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)"}
|
|
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)"}
|
|
615
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.` : "";
|
|
616
647
|
return `You are a helpful product assistant for an app the user is looking at right now.
|
|
617
648
|
|
|
@@ -737,10 +768,11 @@ function isPathAllowed(workspaceDir, targetPath, ignorePaths) {
|
|
|
737
768
|
return !isIgnoredRelative(rel, ignorePaths);
|
|
738
769
|
}
|
|
739
770
|
function formatAccessPolicyPromptSection(input) {
|
|
740
|
-
const ignores = input.ignorePaths.length ? input.ignorePaths.map((p) => `- ${p}`).join("\n") : "- (none beyond staying inside the workspace)";
|
|
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)";
|
|
741
772
|
return `## File access boundaries (mandatory)
|
|
742
773
|
- You may only read or edit files inside the current project workspace directory.
|
|
743
|
-
- Never
|
|
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).
|
|
744
776
|
- Never touch paths that match this ignore list (relative to the workspace):
|
|
745
777
|
${ignores}
|
|
746
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.`;
|
|
@@ -778,7 +810,7 @@ ${block}` : block;
|
|
|
778
810
|
}
|
|
779
811
|
|
|
780
812
|
// src/http/handler.ts
|
|
781
|
-
var
|
|
813
|
+
var import_node_crypto2 = require("crypto");
|
|
782
814
|
|
|
783
815
|
// src/log.ts
|
|
784
816
|
var import_pino = __toESM(require("pino"), 1);
|
|
@@ -825,12 +857,72 @@ function previewText(text, max = 160) {
|
|
|
825
857
|
|
|
826
858
|
// src/http/attachments.ts
|
|
827
859
|
var import_promises = __toESM(require("fs/promises"), 1);
|
|
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);
|
|
828
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
|
|
829
920
|
var MAX_ATTACHMENTS = 5;
|
|
830
921
|
var MAX_BYTES = 4 * 1024 * 1024;
|
|
831
922
|
var ALLOWED = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif"]);
|
|
832
923
|
function extForMime(mime) {
|
|
833
|
-
|
|
924
|
+
const normalized = mime.split(";")[0]?.trim().toLowerCase() || "";
|
|
925
|
+
switch (normalized) {
|
|
834
926
|
case "image/jpeg":
|
|
835
927
|
case "image/jpg":
|
|
836
928
|
return ".jpg";
|
|
@@ -842,11 +934,31 @@ function extForMime(mime) {
|
|
|
842
934
|
return ".png";
|
|
843
935
|
}
|
|
844
936
|
}
|
|
845
|
-
|
|
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) {
|
|
846
954
|
if (!attachments?.length) return [];
|
|
847
955
|
const selected = attachments.slice(0, MAX_ATTACHMENTS);
|
|
848
|
-
const
|
|
849
|
-
|
|
956
|
+
const loc = {
|
|
957
|
+
workspaceDir,
|
|
958
|
+
dataDir: extra?.dataDir,
|
|
959
|
+
sandboxId: extra?.sandboxId
|
|
960
|
+
};
|
|
961
|
+
const dir = uploadsRoot(loc);
|
|
850
962
|
const paths = [];
|
|
851
963
|
const stamp = Date.now();
|
|
852
964
|
for (let i = 0; i < selected.length; i++) {
|
|
@@ -864,9 +976,9 @@ async function saveChatAttachments(attachments, workspaceDir) {
|
|
|
864
976
|
}
|
|
865
977
|
const safeBase = (item.name || `screenshot-${i + 1}`).replace(/[^\w.\-]+/g, "_").slice(0, 64);
|
|
866
978
|
const fileName = `${stamp}-${i + 1}-${safeBase}${extForMime(mime)}`;
|
|
867
|
-
const filePath =
|
|
868
|
-
if (!isInsideWorkspace(
|
|
869
|
-
throw new Error("Attachment path escaped the project
|
|
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");
|
|
870
982
|
}
|
|
871
983
|
await import_promises.default.writeFile(filePath, buffer);
|
|
872
984
|
paths.push(filePath);
|
|
@@ -1019,6 +1131,8 @@ function createChatHandler(options) {
|
|
|
1019
1131
|
const logger = options.logger ?? createLogger("ai-cli:chat");
|
|
1020
1132
|
const validate = options.tools ? createToolValidator(options.tools) : null;
|
|
1021
1133
|
const workspaceDir = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
|
|
1134
|
+
const dataDir = options.dataDir;
|
|
1135
|
+
const sandboxId = options.sandboxId;
|
|
1022
1136
|
const baseCallOptions = {
|
|
1023
1137
|
systemPrompt: options.systemPrompt,
|
|
1024
1138
|
workspaceDir,
|
|
@@ -1175,20 +1289,37 @@ function createChatHandler(options) {
|
|
|
1175
1289
|
}
|
|
1176
1290
|
let attachmentPaths = [];
|
|
1177
1291
|
let persistAttachmentPaths = [];
|
|
1178
|
-
if (options.db?.uploadAttachments) {
|
|
1292
|
+
if (options.db?.uploadAttachments && attachments?.length) {
|
|
1179
1293
|
const uploaded = await options.db.uploadAttachments(
|
|
1180
1294
|
conversationId,
|
|
1181
1295
|
attachments
|
|
1182
1296
|
);
|
|
1183
1297
|
attachmentPaths = uploaded.localPaths;
|
|
1184
1298
|
persistAttachmentPaths = uploaded.refs;
|
|
1185
|
-
} else {
|
|
1299
|
+
} else if (attachments?.length) {
|
|
1186
1300
|
attachmentPaths = await saveChatAttachments(
|
|
1187
1301
|
attachments,
|
|
1188
|
-
workspaceDir
|
|
1302
|
+
workspaceDir,
|
|
1303
|
+
{ dataDir, sandboxId }
|
|
1189
1304
|
);
|
|
1190
1305
|
persistAttachmentPaths = attachmentPaths;
|
|
1191
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
|
+
);
|
|
1192
1323
|
const turn = beginConversationTurn(conversationId ?? SHARED_CONVERSATION_ID);
|
|
1193
1324
|
trackedTurn = turn;
|
|
1194
1325
|
const signal = request.signal;
|
|
@@ -1283,6 +1414,7 @@ function createChatHandler(options) {
|
|
|
1283
1414
|
turn,
|
|
1284
1415
|
parentUserMessageId,
|
|
1285
1416
|
historyLength: messages.length,
|
|
1417
|
+
attachmentCount: attachmentPaths.length,
|
|
1286
1418
|
hasParentChain: Boolean(parentChainContext),
|
|
1287
1419
|
preview: previewText(
|
|
1288
1420
|
[...messages].reverse().find((m) => m.role === "user")?.content
|
|
@@ -1327,7 +1459,7 @@ function createChatHandler(options) {
|
|
|
1327
1459
|
await options.onToolCalls(validatedToolCalls, context);
|
|
1328
1460
|
}
|
|
1329
1461
|
let nextQueued = null;
|
|
1330
|
-
const assistantMessageId = typeof body.assistantMessageId === "string" && body.assistantMessageId ? body.assistantMessageId : (0,
|
|
1462
|
+
const assistantMessageId = typeof body.assistantMessageId === "string" && body.assistantMessageId ? body.assistantMessageId : (0, import_node_crypto2.randomUUID)();
|
|
1331
1463
|
let replyId = assistantMessageId;
|
|
1332
1464
|
if (options.db && conversationId) {
|
|
1333
1465
|
await options.db.ensureConversation(conversationId);
|
|
@@ -1442,8 +1574,8 @@ function toNextRoute(handlers) {
|
|
|
1442
1574
|
|
|
1443
1575
|
// src/http/local-store.ts
|
|
1444
1576
|
var import_promises2 = __toESM(require("fs/promises"), 1);
|
|
1445
|
-
var
|
|
1446
|
-
var
|
|
1577
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
1578
|
+
var import_node_crypto3 = require("crypto");
|
|
1447
1579
|
function userHasAiReply(rows, workingId) {
|
|
1448
1580
|
return rows.some(
|
|
1449
1581
|
(m) => m.role === "assistant" && m.parentMessageId === workingId && m.provider !== "working" && Boolean(m.content?.trim())
|
|
@@ -1459,11 +1591,11 @@ async function readConversation(filePath) {
|
|
|
1459
1591
|
}
|
|
1460
1592
|
}
|
|
1461
1593
|
async function writeConversation(filePath, data) {
|
|
1462
|
-
await import_promises2.default.mkdir(
|
|
1594
|
+
await import_promises2.default.mkdir(import_node_path5.default.dirname(filePath), { recursive: true });
|
|
1463
1595
|
await import_promises2.default.writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
1464
1596
|
}
|
|
1465
1597
|
function createLocalDirectoryStore(baseDir) {
|
|
1466
|
-
const fileFor = (id) =>
|
|
1598
|
+
const fileFor = (id) => import_node_path5.default.join(baseDir, `${id}.json`);
|
|
1467
1599
|
return {
|
|
1468
1600
|
async ensureConversation(id) {
|
|
1469
1601
|
const existing = await readConversation(fileFor(id));
|
|
@@ -1487,7 +1619,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1487
1619
|
const isAi = input.role === "assistant" && input.provider !== "working" && input.senderType !== "developer" && input.provider !== "developer";
|
|
1488
1620
|
const busy = existing.messages.some((m) => m.queueStatus === "working");
|
|
1489
1621
|
const makeWorking = (parentMessageId) => ({
|
|
1490
|
-
id: (0,
|
|
1622
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
1491
1623
|
conversationId: input.conversationId,
|
|
1492
1624
|
role: "assistant",
|
|
1493
1625
|
content: "",
|
|
@@ -1525,7 +1657,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1525
1657
|
return { ...existingById2, nextQueued: null };
|
|
1526
1658
|
}
|
|
1527
1659
|
const message2 = {
|
|
1528
|
-
id: input.id ?? (0,
|
|
1660
|
+
id: input.id ?? (0, import_node_crypto3.randomUUID)(),
|
|
1529
1661
|
conversationId: input.conversationId,
|
|
1530
1662
|
role: input.role,
|
|
1531
1663
|
content: input.content,
|
|
@@ -1557,7 +1689,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1557
1689
|
}
|
|
1558
1690
|
}
|
|
1559
1691
|
const message2 = {
|
|
1560
|
-
id: input.id ?? (0,
|
|
1692
|
+
id: input.id ?? (0, import_node_crypto3.randomUUID)(),
|
|
1561
1693
|
conversationId: input.conversationId,
|
|
1562
1694
|
role: "assistant",
|
|
1563
1695
|
content: "",
|
|
@@ -1576,7 +1708,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1576
1708
|
return { ...existingById, nextQueued: null };
|
|
1577
1709
|
}
|
|
1578
1710
|
const message = {
|
|
1579
|
-
id: input.id ?? (0,
|
|
1711
|
+
id: input.id ?? (0, import_node_crypto3.randomUUID)(),
|
|
1580
1712
|
conversationId: input.conversationId,
|
|
1581
1713
|
role: input.role,
|
|
1582
1714
|
content: input.content,
|
|
@@ -1647,7 +1779,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1647
1779
|
next2.queueStatus = "working";
|
|
1648
1780
|
next2.queuePosition = null;
|
|
1649
1781
|
existing.messages.push({
|
|
1650
|
-
id: (0,
|
|
1782
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
1651
1783
|
conversationId,
|
|
1652
1784
|
role: "assistant",
|
|
1653
1785
|
content: "",
|
|
@@ -1688,7 +1820,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1688
1820
|
next.queueStatus = "working";
|
|
1689
1821
|
next.queuePosition = null;
|
|
1690
1822
|
existing.messages.push({
|
|
1691
|
-
id: (0,
|
|
1823
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
1692
1824
|
conversationId,
|
|
1693
1825
|
role: "assistant",
|
|
1694
1826
|
content: "",
|
|
@@ -1735,7 +1867,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1735
1867
|
next.queueStatus = "working";
|
|
1736
1868
|
next.queuePosition = null;
|
|
1737
1869
|
existing.messages.push({
|
|
1738
|
-
id: (0,
|
|
1870
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
1739
1871
|
conversationId,
|
|
1740
1872
|
role: "assistant",
|
|
1741
1873
|
content: "",
|
|
@@ -1769,7 +1901,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1769
1901
|
const conversations = [];
|
|
1770
1902
|
for (const entry of entries) {
|
|
1771
1903
|
if (!entry.endsWith(".json")) continue;
|
|
1772
|
-
const filePath =
|
|
1904
|
+
const filePath = import_node_path5.default.join(baseDir, entry);
|
|
1773
1905
|
const existing = await readConversation(filePath);
|
|
1774
1906
|
if (!existing?.id) continue;
|
|
1775
1907
|
conversations.push({
|
|
@@ -1786,7 +1918,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1786
1918
|
if (events.length === 0) return;
|
|
1787
1919
|
const existing = await readConversation(fileFor(conversationId));
|
|
1788
1920
|
if (!existing) return;
|
|
1789
|
-
const toolFile =
|
|
1921
|
+
const toolFile = import_node_path5.default.join(baseDir, `${conversationId}.tools.jsonl`);
|
|
1790
1922
|
const lines = events.map(
|
|
1791
1923
|
(e) => JSON.stringify({
|
|
1792
1924
|
conversationId,
|
|
@@ -1802,8 +1934,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1802
1934
|
|
|
1803
1935
|
// src/http/maintainer-pro-store.ts
|
|
1804
1936
|
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
1805
|
-
var
|
|
1806
|
-
var import_node_path5 = __toESM(require("path"), 1);
|
|
1937
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
1807
1938
|
|
|
1808
1939
|
// src/http/synced-store.ts
|
|
1809
1940
|
function userHasAiReply2(rows, workingId) {
|
|
@@ -1830,7 +1961,7 @@ function asCachedMessage(raw) {
|
|
|
1830
1961
|
content: typeof raw.content === "string" ? raw.content : "",
|
|
1831
1962
|
provider: typeof raw.provider === "string" ? raw.provider : null,
|
|
1832
1963
|
createdAt: typeof raw.createdAt === "string" ? raw.createdAt : void 0,
|
|
1833
|
-
attachmentPaths: Array.isArray(raw.attachmentPaths) ? raw.attachmentPaths.filter((p) => typeof p === "string") : 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,
|
|
1834
1965
|
senderType: raw.senderType === "client" || raw.senderType === "developer" || raw.senderType === "ai" ? raw.senderType : null,
|
|
1835
1966
|
senderName: typeof raw.senderName === "string" ? raw.senderName : null,
|
|
1836
1967
|
parentMessageId: typeof raw.parentMessageId === "string" ? raw.parentMessageId : null,
|
|
@@ -2144,6 +2275,14 @@ function createSyncedChatStore(options) {
|
|
|
2144
2275
|
return { refs: [], localPaths: [] };
|
|
2145
2276
|
}
|
|
2146
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
|
+
);
|
|
2147
2286
|
}
|
|
2148
2287
|
};
|
|
2149
2288
|
return store;
|
|
@@ -2176,7 +2315,8 @@ function createMaintainerProStore(options) {
|
|
|
2176
2315
|
const baseUrl = options.baseUrl;
|
|
2177
2316
|
const apiKey = options.apiKey;
|
|
2178
2317
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2179
|
-
const
|
|
2318
|
+
const logger = options.logger;
|
|
2319
|
+
const tempDir = options.tempDir ?? projectUploadsDir({ workspaceDir: process.cwd() });
|
|
2180
2320
|
const store = {
|
|
2181
2321
|
async ensureConversation(id) {
|
|
2182
2322
|
await mpFetch(
|
|
@@ -2327,15 +2467,89 @@ function createMaintainerProStore(options) {
|
|
|
2327
2467
|
const id = data.attachment.id;
|
|
2328
2468
|
attachmentIds.push(id);
|
|
2329
2469
|
refs.push(data.attachment.ref || `maintainer-pro://${id}`);
|
|
2330
|
-
const
|
|
2331
|
-
const localPath = import_node_path5.default.join(
|
|
2470
|
+
const localPath = import_node_path6.default.join(
|
|
2332
2471
|
tempDir,
|
|
2333
|
-
`${conversationId}-${id}${
|
|
2472
|
+
`${conversationId}-${id}${extForMime(item.mimeType)}`
|
|
2334
2473
|
);
|
|
2474
|
+
await import_promises3.default.mkdir(import_node_path6.default.dirname(localPath), { recursive: true });
|
|
2335
2475
|
await import_promises3.default.writeFile(localPath, Buffer.from(item.data, "base64"));
|
|
2336
2476
|
localPaths.push(localPath);
|
|
2337
2477
|
}
|
|
2338
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;
|
|
2339
2553
|
}
|
|
2340
2554
|
};
|
|
2341
2555
|
if (options.sync === false) {
|
|
@@ -2361,6 +2575,11 @@ function createMaintainerProStoreFromEnv(options) {
|
|
|
2361
2575
|
return createMaintainerProStore({
|
|
2362
2576
|
baseUrl,
|
|
2363
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
|
+
}),
|
|
2364
2583
|
logger: options?.logger,
|
|
2365
2584
|
log: options?.log,
|
|
2366
2585
|
sync: options?.sync,
|
|
@@ -2369,7 +2588,10 @@ function createMaintainerProStoreFromEnv(options) {
|
|
|
2369
2588
|
}
|
|
2370
2589
|
|
|
2371
2590
|
// src/workspace-inspect.ts
|
|
2591
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
2592
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
2372
2593
|
var import_zod = require("zod");
|
|
2594
|
+
var CONFIG_INSPECT_TIMEOUT_MS = 18e3;
|
|
2373
2595
|
var inspectJsonSchema = import_zod.z.object({
|
|
2374
2596
|
kind: import_zod.z.enum(["next", "vite", "html", "empty", "other"]).optional(),
|
|
2375
2597
|
name: import_zod.z.string().max(200).optional(),
|
|
@@ -2439,6 +2661,154 @@ ${input.extraContext ? `${input.extraContext.trim()}
|
|
|
2439
2661
|
|
|
2440
2662
|
` : ""}Inspect this project, fix any setup gaps you can, and return the JSON report.`;
|
|
2441
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
|
+
}
|
|
2442
2812
|
async function inspectAndRepairWorkspace(input) {
|
|
2443
2813
|
const workspaceDir = input.workspaceDir;
|
|
2444
2814
|
const provider = await resolveProvider({ preference: "auto" });
|
|
@@ -2459,25 +2829,671 @@ async function inspectAndRepairWorkspace(input) {
|
|
|
2459
2829
|
technical: true
|
|
2460
2830
|
}
|
|
2461
2831
|
);
|
|
2462
|
-
|
|
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) {
|
|
2463
2868
|
return {
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
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
|
|
2474
3485
|
};
|
|
2475
3486
|
}
|
|
2476
3487
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2477
3488
|
0 && (module.exports = {
|
|
2478
3489
|
ACCESS_IGNORE_BEGIN,
|
|
2479
3490
|
ACCESS_IGNORE_END,
|
|
3491
|
+
AI_SERVER_APP_ID,
|
|
3492
|
+
AI_SERVER_DEFAULT_PORT,
|
|
3493
|
+
COLLABORATER_DIR,
|
|
2480
3494
|
DEFAULT_AI_IGNORE_PATHS,
|
|
3495
|
+
HOST_APPS_FILE,
|
|
3496
|
+
MAINTAINER_PRO_HOME_DIR,
|
|
2481
3497
|
PROMPT_SECTION,
|
|
2482
3498
|
WORKING_PROVIDER,
|
|
2483
3499
|
buildClaudeUserPrompt,
|
|
@@ -2500,27 +3516,53 @@ async function inspectAndRepairWorkspace(input) {
|
|
|
2500
3516
|
createMaintainerProStoreFromEnv,
|
|
2501
3517
|
createSyncedChatStore,
|
|
2502
3518
|
createToolValidator,
|
|
3519
|
+
defaultAiServerApp,
|
|
3520
|
+
detectHostAppsFromFiles,
|
|
3521
|
+
ensureAiServerApp,
|
|
3522
|
+
ensureProjectDataDir,
|
|
3523
|
+
ensureSingleHost,
|
|
2503
3524
|
formatAccessPolicyPromptSection,
|
|
2504
3525
|
formatClientContext,
|
|
2505
3526
|
formatParentChainContext,
|
|
2506
3527
|
getProviderPreference,
|
|
3528
|
+
hostAppsCachePath,
|
|
3529
|
+
hostAppsFingerprint,
|
|
2507
3530
|
inspectAndRepairWorkspace,
|
|
3531
|
+
inspectConfigOnly,
|
|
2508
3532
|
isDevMode,
|
|
2509
3533
|
isIgnoredRelative,
|
|
2510
3534
|
isInsideWorkspace,
|
|
2511
3535
|
isPathAllowed,
|
|
3536
|
+
maintainerProHome,
|
|
3537
|
+
mergeDesiredHostApps,
|
|
3538
|
+
normalizeEnvMaps,
|
|
3539
|
+
normalizeHostApp,
|
|
3540
|
+
normalizeHostApps,
|
|
2512
3541
|
normalizeIgnorePaths,
|
|
2513
3542
|
parentChainForTurn,
|
|
2514
3543
|
parseAiResponse,
|
|
2515
3544
|
parseIgnorePathsEnv,
|
|
3545
|
+
parsePort,
|
|
2516
3546
|
previewText,
|
|
3547
|
+
projectHostAppsPath,
|
|
3548
|
+
projectIdForFolder,
|
|
3549
|
+
projectUploadsDir,
|
|
3550
|
+
proposeHostAppsFromConfig,
|
|
2517
3551
|
providerLabel,
|
|
3552
|
+
readCollaboraterApps,
|
|
3553
|
+
readHostAppsCache,
|
|
3554
|
+
readProjectEnvLayers,
|
|
2518
3555
|
renderManagedIgnoreBlock,
|
|
2519
3556
|
resolveCliBinary,
|
|
3557
|
+
resolveHostApps,
|
|
2520
3558
|
resolveIgnorePaths,
|
|
2521
3559
|
resolveLogLevel,
|
|
3560
|
+
resolveProjectDataDir,
|
|
2522
3561
|
resolveProvider,
|
|
3562
|
+
sanitizeProjectId,
|
|
2523
3563
|
saveChatAttachments,
|
|
2524
3564
|
toNextRoute,
|
|
2525
|
-
upsertManagedIgnoreFile
|
|
3565
|
+
upsertManagedIgnoreFile,
|
|
3566
|
+
writeCollaboraterApps,
|
|
3567
|
+
writeHostAppsCache
|
|
2526
3568
|
});
|