@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 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 path6 = result.stdout.trim().split(/\r?\n/)[0]?.trim();
467
- return path6 || null;
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 read, write, move, or delete files outside that workspace.
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 import_node_crypto = require("crypto");
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
- switch (mime) {
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
- async function saveChatAttachments(attachments, workspaceDir) {
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 dir = import_node_path3.default.join(workspaceDir, ".maintainer-pro", "uploads");
849
- await import_promises.default.mkdir(dir, { recursive: true });
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 = import_node_path3.default.resolve(dir, fileName);
868
- if (!isInsideWorkspace(workspaceDir, filePath)) {
869
- throw new Error("Attachment path escaped the project workspace");
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, import_node_crypto.randomUUID)();
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 import_node_path4 = __toESM(require("path"), 1);
1446
- var import_node_crypto2 = require("crypto");
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(import_node_path4.default.dirname(filePath), { recursive: true });
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) => import_node_path4.default.join(baseDir, `${id}.json`);
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, import_node_crypto2.randomUUID)(),
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, import_node_crypto2.randomUUID)(),
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, import_node_crypto2.randomUUID)(),
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, import_node_crypto2.randomUUID)(),
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, import_node_crypto2.randomUUID)(),
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, import_node_crypto2.randomUUID)(),
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, import_node_crypto2.randomUUID)(),
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 = import_node_path4.default.join(baseDir, entry);
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 = import_node_path4.default.join(baseDir, `${conversationId}.tools.jsonl`);
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 import_node_os = __toESM(require("os"), 1);
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 tempDir = options.tempDir ?? import_node_path5.default.join(import_node_os.default.tmpdir(), "maintainer-pro-store");
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 ext = item.mimeType.includes("jpeg") || item.mimeType.includes("jpg") ? ".jpg" : item.mimeType.includes("webp") ? ".webp" : item.mimeType.includes("gif") ? ".gif" : ".png";
2331
- const localPath = import_node_path5.default.join(
2470
+ const localPath = import_node_path6.default.join(
2332
2471
  tempDir,
2333
- `${conversationId}-${id}${ext}`
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
- const parsed = extractInspectJson(response.text);
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
- kind: parsed?.kind ?? "other",
2465
- name: parsed?.name?.trim() || input.appName || "",
2466
- summary: parsed?.summary?.trim() || response.text.replace(/```json[\s\S]*```/i, "").trim().slice(0, 400),
2467
- scripts: parsed?.scripts ?? {},
2468
- ports: parsed?.ports ?? {},
2469
- issues: parsed?.issues ?? [],
2470
- fixes: parsed?.fixes ?? [],
2471
- ready: parsed?.ready ?? parsed?.issues?.length === 0,
2472
- provider: response.provider || provider.id,
2473
- rawText: response.text
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
  });