@maintainer-pro/ai-cli 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1094 -51
- package/dist/index.d.cts +176 -6
- package/dist/index.d.ts +176 -6
- package/dist/index.js +1063 -51
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -109,7 +109,7 @@ function buildUserFacingPrompt(systemPrompt, messages, context, attachmentPaths,
|
|
|
109
109
|
);
|
|
110
110
|
const attachmentsSection = wrapSection(
|
|
111
111
|
PROMPT_SECTION.attachments,
|
|
112
|
-
attachmentPaths && attachmentPaths.length > 0 ? `Screenshots / images attached by the user (open and inspect these image files):
|
|
112
|
+
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):
|
|
113
113
|
${attachmentPaths.map((p) => `- ${p}`).join("\n")}` : "",
|
|
114
114
|
"user attachments"
|
|
115
115
|
);
|
|
@@ -263,6 +263,7 @@ async function callClaudeCli(command, messages, context, options) {
|
|
|
263
263
|
resolved,
|
|
264
264
|
[
|
|
265
265
|
"--print",
|
|
266
|
+
"--dangerously-skip-permissions",
|
|
266
267
|
"--system-prompt",
|
|
267
268
|
options.systemPrompt,
|
|
268
269
|
"--no-session-persistence",
|
|
@@ -380,8 +381,8 @@ async function resolveCommandPath(command) {
|
|
|
380
381
|
}
|
|
381
382
|
const result = await execa2("which", [command], { reject: false });
|
|
382
383
|
if (result.exitCode !== 0) return null;
|
|
383
|
-
const
|
|
384
|
-
return
|
|
384
|
+
const path9 = result.stdout.trim().split(/\r?\n/)[0]?.trim();
|
|
385
|
+
return path9 || null;
|
|
385
386
|
} catch {
|
|
386
387
|
return null;
|
|
387
388
|
}
|
|
@@ -528,7 +529,7 @@ Only use those runtime tools for live state. Never invent runtime tools.` : "";
|
|
|
528
529
|
- You may only read or edit files inside the current project workspace directory.
|
|
529
530
|
- Never read, write, move, or delete files outside that workspace.
|
|
530
531
|
- Never touch paths that match this ignore list (relative to the workspace):
|
|
531
|
-
${input.ignorePaths.length ? input.ignorePaths.map((p) => `- ${p}`).join("\n") : "- (none beyond staying inside the workspace)"}
|
|
532
|
+
${(input.ignorePaths ?? []).filter((p) => !p.startsWith("!")).length ? (input.ignorePaths ?? []).filter((p) => !p.startsWith("!")).map((p) => `- ${p}`).join("\n") : "- (none beyond staying inside the workspace)"}
|
|
532
533
|
- 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.` : "";
|
|
533
534
|
return `You are a helpful product assistant for an app the user is looking at right now.
|
|
534
535
|
|
|
@@ -654,10 +655,11 @@ function isPathAllowed(workspaceDir, targetPath, ignorePaths) {
|
|
|
654
655
|
return !isIgnoredRelative(rel, ignorePaths);
|
|
655
656
|
}
|
|
656
657
|
function formatAccessPolicyPromptSection(input) {
|
|
657
|
-
const ignores = input.ignorePaths.length ? input.ignorePaths.map((p) => `- ${p}`).join("\n") : "- (none beyond staying inside the workspace)";
|
|
658
|
+
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)";
|
|
658
659
|
return `## File access boundaries (mandatory)
|
|
659
660
|
- You may only read or edit files inside the current project workspace directory.
|
|
660
|
-
- Never
|
|
661
|
+
- Never write, move, or delete files outside that workspace.
|
|
662
|
+
- You may read screenshot files listed in the attachments section (those live in the Maintainer Pro data folder, not in the project).
|
|
661
663
|
- Never touch paths that match this ignore list (relative to the workspace):
|
|
662
664
|
${ignores}
|
|
663
665
|
- 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.`;
|
|
@@ -741,13 +743,73 @@ function previewText(text, max = 160) {
|
|
|
741
743
|
}
|
|
742
744
|
|
|
743
745
|
// src/http/attachments.ts
|
|
744
|
-
import
|
|
746
|
+
import fs3 from "fs/promises";
|
|
747
|
+
import path4 from "path";
|
|
748
|
+
|
|
749
|
+
// src/project-data.ts
|
|
750
|
+
import { createHash } from "crypto";
|
|
751
|
+
import fs2 from "fs";
|
|
752
|
+
import os from "os";
|
|
745
753
|
import path3 from "path";
|
|
754
|
+
var MAINTAINER_PRO_HOME_DIR = ".maintainer-pro";
|
|
755
|
+
var HOST_APPS_FILE = "apps.json";
|
|
756
|
+
function maintainerProHome() {
|
|
757
|
+
const override = process.env.MAINTAINER_PRO_HOME?.trim();
|
|
758
|
+
if (override) return path3.resolve(override);
|
|
759
|
+
return path3.join(os.homedir(), MAINTAINER_PRO_HOME_DIR);
|
|
760
|
+
}
|
|
761
|
+
function projectIdForFolder(folder) {
|
|
762
|
+
const resolved = path3.resolve(folder || "");
|
|
763
|
+
const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
764
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
765
|
+
}
|
|
766
|
+
function sanitizeProjectId(id) {
|
|
767
|
+
return String(id || "").trim().replace(/[^\w.-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 80);
|
|
768
|
+
}
|
|
769
|
+
function resolveProjectDataDir(input) {
|
|
770
|
+
const override = String(input.dataDir || "").trim();
|
|
771
|
+
if (override) return path3.resolve(override);
|
|
772
|
+
const sandbox = sanitizeProjectId(String(input.sandboxId || ""));
|
|
773
|
+
const id = sandbox || projectIdForFolder(input.workspaceDir);
|
|
774
|
+
return path3.join(maintainerProHome(), "projects", id);
|
|
775
|
+
}
|
|
776
|
+
function projectUploadsDir(input) {
|
|
777
|
+
return path3.join(resolveProjectDataDir(input), "uploads");
|
|
778
|
+
}
|
|
779
|
+
function hostAppsCachePath(input) {
|
|
780
|
+
return path3.join(resolveProjectDataDir(input), HOST_APPS_FILE);
|
|
781
|
+
}
|
|
782
|
+
function ensureProjectDataDir(input) {
|
|
783
|
+
const dir = resolveProjectDataDir(input);
|
|
784
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
785
|
+
fs2.mkdirSync(path3.join(dir, "uploads"), { recursive: true });
|
|
786
|
+
try {
|
|
787
|
+
fs2.writeFileSync(
|
|
788
|
+
path3.join(dir, "workspace.json"),
|
|
789
|
+
`${JSON.stringify(
|
|
790
|
+
{
|
|
791
|
+
workspaceDir: path3.resolve(input.workspaceDir || ""),
|
|
792
|
+
sandboxId: input.sandboxId ? String(input.sandboxId) : null,
|
|
793
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
794
|
+
},
|
|
795
|
+
null,
|
|
796
|
+
2
|
|
797
|
+
)}
|
|
798
|
+
`,
|
|
799
|
+
"utf8"
|
|
800
|
+
);
|
|
801
|
+
} catch {
|
|
802
|
+
}
|
|
803
|
+
return dir;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/http/attachments.ts
|
|
746
807
|
var MAX_ATTACHMENTS = 5;
|
|
747
808
|
var MAX_BYTES = 4 * 1024 * 1024;
|
|
748
809
|
var ALLOWED = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif"]);
|
|
749
810
|
function extForMime(mime) {
|
|
750
|
-
|
|
811
|
+
const normalized = mime.split(";")[0]?.trim().toLowerCase() || "";
|
|
812
|
+
switch (normalized) {
|
|
751
813
|
case "image/jpeg":
|
|
752
814
|
case "image/jpg":
|
|
753
815
|
return ".jpg";
|
|
@@ -759,11 +821,31 @@ function extForMime(mime) {
|
|
|
759
821
|
return ".png";
|
|
760
822
|
}
|
|
761
823
|
}
|
|
762
|
-
|
|
824
|
+
function attachmentIdFromRef(ref) {
|
|
825
|
+
const trimmed = String(ref || "").trim();
|
|
826
|
+
if (!trimmed) return null;
|
|
827
|
+
const m = /^maintainer-pro:\/\/(.+)$/i.exec(trimmed);
|
|
828
|
+
if (m?.[1]) return m[1];
|
|
829
|
+
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(
|
|
830
|
+
trimmed
|
|
831
|
+
)) {
|
|
832
|
+
return trimmed;
|
|
833
|
+
}
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
function uploadsRoot(input) {
|
|
837
|
+
ensureProjectDataDir(input);
|
|
838
|
+
return projectUploadsDir(input);
|
|
839
|
+
}
|
|
840
|
+
async function saveChatAttachments(attachments, workspaceDir, extra) {
|
|
763
841
|
if (!attachments?.length) return [];
|
|
764
842
|
const selected = attachments.slice(0, MAX_ATTACHMENTS);
|
|
765
|
-
const
|
|
766
|
-
|
|
843
|
+
const loc = {
|
|
844
|
+
workspaceDir,
|
|
845
|
+
dataDir: extra?.dataDir,
|
|
846
|
+
sandboxId: extra?.sandboxId
|
|
847
|
+
};
|
|
848
|
+
const dir = uploadsRoot(loc);
|
|
767
849
|
const paths = [];
|
|
768
850
|
const stamp = Date.now();
|
|
769
851
|
for (let i = 0; i < selected.length; i++) {
|
|
@@ -781,11 +863,11 @@ async function saveChatAttachments(attachments, workspaceDir) {
|
|
|
781
863
|
}
|
|
782
864
|
const safeBase = (item.name || `screenshot-${i + 1}`).replace(/[^\w.\-]+/g, "_").slice(0, 64);
|
|
783
865
|
const fileName = `${stamp}-${i + 1}-${safeBase}${extForMime(mime)}`;
|
|
784
|
-
const filePath =
|
|
785
|
-
if (!isInsideWorkspace(
|
|
786
|
-
throw new Error("Attachment path escaped the project
|
|
866
|
+
const filePath = path4.resolve(dir, fileName);
|
|
867
|
+
if (!isInsideWorkspace(dir, filePath)) {
|
|
868
|
+
throw new Error("Attachment path escaped the project data folder");
|
|
787
869
|
}
|
|
788
|
-
await
|
|
870
|
+
await fs3.writeFile(filePath, buffer);
|
|
789
871
|
paths.push(filePath);
|
|
790
872
|
}
|
|
791
873
|
return paths;
|
|
@@ -936,6 +1018,8 @@ function createChatHandler(options) {
|
|
|
936
1018
|
const logger = options.logger ?? createLogger("ai-cli:chat");
|
|
937
1019
|
const validate = options.tools ? createToolValidator(options.tools) : null;
|
|
938
1020
|
const workspaceDir = options.workspaceDir ?? process.env.AI_CLI_WORKSPACE ?? process.cwd();
|
|
1021
|
+
const dataDir = options.dataDir;
|
|
1022
|
+
const sandboxId = options.sandboxId;
|
|
939
1023
|
const baseCallOptions = {
|
|
940
1024
|
systemPrompt: options.systemPrompt,
|
|
941
1025
|
workspaceDir,
|
|
@@ -1092,20 +1176,37 @@ function createChatHandler(options) {
|
|
|
1092
1176
|
}
|
|
1093
1177
|
let attachmentPaths = [];
|
|
1094
1178
|
let persistAttachmentPaths = [];
|
|
1095
|
-
if (options.db?.uploadAttachments) {
|
|
1179
|
+
if (options.db?.uploadAttachments && attachments?.length) {
|
|
1096
1180
|
const uploaded = await options.db.uploadAttachments(
|
|
1097
1181
|
conversationId,
|
|
1098
1182
|
attachments
|
|
1099
1183
|
);
|
|
1100
1184
|
attachmentPaths = uploaded.localPaths;
|
|
1101
1185
|
persistAttachmentPaths = uploaded.refs;
|
|
1102
|
-
} else {
|
|
1186
|
+
} else if (attachments?.length) {
|
|
1103
1187
|
attachmentPaths = await saveChatAttachments(
|
|
1104
1188
|
attachments,
|
|
1105
|
-
workspaceDir
|
|
1189
|
+
workspaceDir,
|
|
1190
|
+
{ dataDir, sandboxId }
|
|
1106
1191
|
);
|
|
1107
1192
|
persistAttachmentPaths = attachmentPaths;
|
|
1108
1193
|
}
|
|
1194
|
+
const storedUserId = typeof body.userMessageId === "string" && body.userMessageId ? body.userMessageId : void 0;
|
|
1195
|
+
if (!attachmentPaths.length && conversationId && options.db?.materializeMessageAttachments) {
|
|
1196
|
+
attachmentPaths = await options.db.materializeMessageAttachments(
|
|
1197
|
+
conversationId,
|
|
1198
|
+
storedUserId,
|
|
1199
|
+
workspaceDir
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
logger.debug(
|
|
1203
|
+
{
|
|
1204
|
+
conversationId,
|
|
1205
|
+
userMessageId: storedUserId,
|
|
1206
|
+
attachmentCount: attachmentPaths.length
|
|
1207
|
+
},
|
|
1208
|
+
"cli attachments"
|
|
1209
|
+
);
|
|
1109
1210
|
const turn = beginConversationTurn(conversationId ?? SHARED_CONVERSATION_ID);
|
|
1110
1211
|
trackedTurn = turn;
|
|
1111
1212
|
const signal = request.signal;
|
|
@@ -1200,6 +1301,7 @@ function createChatHandler(options) {
|
|
|
1200
1301
|
turn,
|
|
1201
1302
|
parentUserMessageId,
|
|
1202
1303
|
historyLength: messages.length,
|
|
1304
|
+
attachmentCount: attachmentPaths.length,
|
|
1203
1305
|
hasParentChain: Boolean(parentChainContext),
|
|
1204
1306
|
preview: previewText(
|
|
1205
1307
|
[...messages].reverse().find((m) => m.role === "user")?.content
|
|
@@ -1358,8 +1460,8 @@ function toNextRoute(handlers) {
|
|
|
1358
1460
|
}
|
|
1359
1461
|
|
|
1360
1462
|
// src/http/local-store.ts
|
|
1361
|
-
import
|
|
1362
|
-
import
|
|
1463
|
+
import fs4 from "fs/promises";
|
|
1464
|
+
import path5 from "path";
|
|
1363
1465
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1364
1466
|
function userHasAiReply(rows, workingId) {
|
|
1365
1467
|
return rows.some(
|
|
@@ -1368,7 +1470,7 @@ function userHasAiReply(rows, workingId) {
|
|
|
1368
1470
|
}
|
|
1369
1471
|
async function readConversation(filePath) {
|
|
1370
1472
|
try {
|
|
1371
|
-
const raw = await
|
|
1473
|
+
const raw = await fs4.readFile(filePath, "utf8");
|
|
1372
1474
|
return JSON.parse(raw);
|
|
1373
1475
|
} catch (err) {
|
|
1374
1476
|
if (err.code === "ENOENT") return null;
|
|
@@ -1376,11 +1478,11 @@ async function readConversation(filePath) {
|
|
|
1376
1478
|
}
|
|
1377
1479
|
}
|
|
1378
1480
|
async function writeConversation(filePath, data) {
|
|
1379
|
-
await
|
|
1380
|
-
await
|
|
1481
|
+
await fs4.mkdir(path5.dirname(filePath), { recursive: true });
|
|
1482
|
+
await fs4.writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
1381
1483
|
}
|
|
1382
1484
|
function createLocalDirectoryStore(baseDir) {
|
|
1383
|
-
const fileFor = (id) =>
|
|
1485
|
+
const fileFor = (id) => path5.join(baseDir, `${id}.json`);
|
|
1384
1486
|
return {
|
|
1385
1487
|
async ensureConversation(id) {
|
|
1386
1488
|
const existing = await readConversation(fileFor(id));
|
|
@@ -1681,12 +1783,12 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1681
1783
|
return existing?.messages ?? [];
|
|
1682
1784
|
},
|
|
1683
1785
|
async listConversations() {
|
|
1684
|
-
await
|
|
1685
|
-
const entries = await
|
|
1786
|
+
await fs4.mkdir(baseDir, { recursive: true });
|
|
1787
|
+
const entries = await fs4.readdir(baseDir);
|
|
1686
1788
|
const conversations = [];
|
|
1687
1789
|
for (const entry of entries) {
|
|
1688
1790
|
if (!entry.endsWith(".json")) continue;
|
|
1689
|
-
const filePath =
|
|
1791
|
+
const filePath = path5.join(baseDir, entry);
|
|
1690
1792
|
const existing = await readConversation(filePath);
|
|
1691
1793
|
if (!existing?.id) continue;
|
|
1692
1794
|
conversations.push({
|
|
@@ -1703,7 +1805,7 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1703
1805
|
if (events.length === 0) return;
|
|
1704
1806
|
const existing = await readConversation(fileFor(conversationId));
|
|
1705
1807
|
if (!existing) return;
|
|
1706
|
-
const toolFile =
|
|
1808
|
+
const toolFile = path5.join(baseDir, `${conversationId}.tools.jsonl`);
|
|
1707
1809
|
const lines = events.map(
|
|
1708
1810
|
(e) => JSON.stringify({
|
|
1709
1811
|
conversationId,
|
|
@@ -1711,16 +1813,15 @@ function createLocalDirectoryStore(baseDir) {
|
|
|
1711
1813
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1712
1814
|
})
|
|
1713
1815
|
).join("\n");
|
|
1714
|
-
await
|
|
1816
|
+
await fs4.appendFile(toolFile, `${lines}
|
|
1715
1817
|
`, "utf8");
|
|
1716
1818
|
}
|
|
1717
1819
|
};
|
|
1718
1820
|
}
|
|
1719
1821
|
|
|
1720
1822
|
// src/http/maintainer-pro-store.ts
|
|
1721
|
-
import
|
|
1722
|
-
import
|
|
1723
|
-
import path5 from "path";
|
|
1823
|
+
import fs5 from "fs/promises";
|
|
1824
|
+
import path6 from "path";
|
|
1724
1825
|
|
|
1725
1826
|
// src/http/synced-store.ts
|
|
1726
1827
|
function userHasAiReply2(rows, workingId) {
|
|
@@ -1747,7 +1848,7 @@ function asCachedMessage(raw) {
|
|
|
1747
1848
|
content: typeof raw.content === "string" ? raw.content : "",
|
|
1748
1849
|
provider: typeof raw.provider === "string" ? raw.provider : null,
|
|
1749
1850
|
createdAt: typeof raw.createdAt === "string" ? raw.createdAt : void 0,
|
|
1750
|
-
attachmentPaths: Array.isArray(raw.attachmentPaths) ? raw.attachmentPaths.filter((p) => typeof p === "string") : void 0,
|
|
1851
|
+
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,
|
|
1751
1852
|
senderType: raw.senderType === "client" || raw.senderType === "developer" || raw.senderType === "ai" ? raw.senderType : null,
|
|
1752
1853
|
senderName: typeof raw.senderName === "string" ? raw.senderName : null,
|
|
1753
1854
|
parentMessageId: typeof raw.parentMessageId === "string" ? raw.parentMessageId : null,
|
|
@@ -2061,6 +2162,14 @@ function createSyncedChatStore(options) {
|
|
|
2061
2162
|
return { refs: [], localPaths: [] };
|
|
2062
2163
|
}
|
|
2063
2164
|
return remote.uploadAttachments(conversationId, attachments);
|
|
2165
|
+
},
|
|
2166
|
+
async materializeMessageAttachments(conversationId, messageId, workspaceDir) {
|
|
2167
|
+
if (!remote.materializeMessageAttachments) return [];
|
|
2168
|
+
return remote.materializeMessageAttachments(
|
|
2169
|
+
conversationId,
|
|
2170
|
+
messageId,
|
|
2171
|
+
workspaceDir
|
|
2172
|
+
);
|
|
2064
2173
|
}
|
|
2065
2174
|
};
|
|
2066
2175
|
return store;
|
|
@@ -2093,7 +2202,8 @@ function createMaintainerProStore(options) {
|
|
|
2093
2202
|
const baseUrl = options.baseUrl;
|
|
2094
2203
|
const apiKey = options.apiKey;
|
|
2095
2204
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2096
|
-
const
|
|
2205
|
+
const logger = options.logger;
|
|
2206
|
+
const tempDir = options.tempDir ?? projectUploadsDir({ workspaceDir: process.cwd() });
|
|
2097
2207
|
const store = {
|
|
2098
2208
|
async ensureConversation(id) {
|
|
2099
2209
|
await mpFetch(
|
|
@@ -2221,7 +2331,7 @@ function createMaintainerProStore(options) {
|
|
|
2221
2331
|
if (!attachments?.length) {
|
|
2222
2332
|
return { refs: [], localPaths: [], attachmentIds: [] };
|
|
2223
2333
|
}
|
|
2224
|
-
await
|
|
2334
|
+
await fs5.mkdir(tempDir, { recursive: true });
|
|
2225
2335
|
const refs = [];
|
|
2226
2336
|
const localPaths = [];
|
|
2227
2337
|
const attachmentIds = [];
|
|
@@ -2244,15 +2354,89 @@ function createMaintainerProStore(options) {
|
|
|
2244
2354
|
const id = data.attachment.id;
|
|
2245
2355
|
attachmentIds.push(id);
|
|
2246
2356
|
refs.push(data.attachment.ref || `maintainer-pro://${id}`);
|
|
2247
|
-
const
|
|
2248
|
-
const localPath = path5.join(
|
|
2357
|
+
const localPath = path6.join(
|
|
2249
2358
|
tempDir,
|
|
2250
|
-
`${conversationId}-${id}${
|
|
2359
|
+
`${conversationId}-${id}${extForMime(item.mimeType)}`
|
|
2251
2360
|
);
|
|
2252
|
-
await
|
|
2361
|
+
await fs5.mkdir(path6.dirname(localPath), { recursive: true });
|
|
2362
|
+
await fs5.writeFile(localPath, Buffer.from(item.data, "base64"));
|
|
2253
2363
|
localPaths.push(localPath);
|
|
2254
2364
|
}
|
|
2255
2365
|
return { refs, localPaths, attachmentIds };
|
|
2366
|
+
},
|
|
2367
|
+
async materializeMessageAttachments(conversationId, messageId, workspaceDir) {
|
|
2368
|
+
if (!conversationId || !workspaceDir) return [];
|
|
2369
|
+
const data = await mpFetch(
|
|
2370
|
+
baseUrl,
|
|
2371
|
+
apiKey,
|
|
2372
|
+
`/api/v1/store/conversations/${encodeURIComponent(conversationId)}/messages`,
|
|
2373
|
+
{ method: "GET" },
|
|
2374
|
+
fetchImpl
|
|
2375
|
+
);
|
|
2376
|
+
const messages = data.messages ?? [];
|
|
2377
|
+
const byId = new Map(messages.map((row) => [row.id, row]));
|
|
2378
|
+
const refs = [];
|
|
2379
|
+
const fromMessageIds = [];
|
|
2380
|
+
const seenMsg = /* @__PURE__ */ new Set();
|
|
2381
|
+
const pushRow = (row) => {
|
|
2382
|
+
if (!row || seenMsg.has(row.id)) return;
|
|
2383
|
+
seenMsg.add(row.id);
|
|
2384
|
+
const before = refs.length;
|
|
2385
|
+
refs.push(...row.attachmentPaths ?? []);
|
|
2386
|
+
refs.push(
|
|
2387
|
+
...(row.attachmentIds ?? []).map((id) => `maintainer-pro://${id}`)
|
|
2388
|
+
);
|
|
2389
|
+
if (refs.length > before) fromMessageIds.push(row.id);
|
|
2390
|
+
};
|
|
2391
|
+
let current = messageId ? byId.get(messageId) : void 0;
|
|
2392
|
+
while (current && !seenMsg.has(current.id)) {
|
|
2393
|
+
pushRow(current);
|
|
2394
|
+
const parentId = current.parentMessageId?.trim();
|
|
2395
|
+
current = parentId ? byId.get(parentId) : void 0;
|
|
2396
|
+
}
|
|
2397
|
+
for (const row of messages) {
|
|
2398
|
+
if (row.role && row.role !== "user") continue;
|
|
2399
|
+
pushRow(row);
|
|
2400
|
+
}
|
|
2401
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2402
|
+
const ids = [];
|
|
2403
|
+
for (const ref of refs) {
|
|
2404
|
+
const id = attachmentIdFromRef(ref);
|
|
2405
|
+
if (!id || seen.has(id)) continue;
|
|
2406
|
+
seen.add(id);
|
|
2407
|
+
ids.push(id);
|
|
2408
|
+
}
|
|
2409
|
+
const localPaths = [];
|
|
2410
|
+
for (const id of ids.slice(0, 5)) {
|
|
2411
|
+
const url = `${baseUrl.replace(/\/$/, "")}/api/v1/store/attachments/${encodeURIComponent(id)}`;
|
|
2412
|
+
const res = await fetchImpl(url, {
|
|
2413
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
2414
|
+
});
|
|
2415
|
+
if (!res.ok) {
|
|
2416
|
+
continue;
|
|
2417
|
+
}
|
|
2418
|
+
const mime = res.headers.get("content-type") || "image/png";
|
|
2419
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
2420
|
+
if (!buffer.byteLength) continue;
|
|
2421
|
+
const localPath = path6.join(
|
|
2422
|
+
tempDir,
|
|
2423
|
+
`${id}${extForMime(mime)}`
|
|
2424
|
+
);
|
|
2425
|
+
await fs5.mkdir(tempDir, { recursive: true });
|
|
2426
|
+
await fs5.writeFile(localPath, buffer);
|
|
2427
|
+
localPaths.push(localPath);
|
|
2428
|
+
}
|
|
2429
|
+
logger?.debug(
|
|
2430
|
+
{
|
|
2431
|
+
conversationId,
|
|
2432
|
+
messageId,
|
|
2433
|
+
fromMessageIds,
|
|
2434
|
+
stored: ids.length,
|
|
2435
|
+
written: localPaths.length
|
|
2436
|
+
},
|
|
2437
|
+
"materialize attachments"
|
|
2438
|
+
);
|
|
2439
|
+
return localPaths;
|
|
2256
2440
|
}
|
|
2257
2441
|
};
|
|
2258
2442
|
if (options.sync === false) {
|
|
@@ -2278,6 +2462,11 @@ function createMaintainerProStoreFromEnv(options) {
|
|
|
2278
2462
|
return createMaintainerProStore({
|
|
2279
2463
|
baseUrl,
|
|
2280
2464
|
apiKey,
|
|
2465
|
+
tempDir: projectUploadsDir({
|
|
2466
|
+
workspaceDir: process.env.AI_CLI_WORKSPACE || process.cwd(),
|
|
2467
|
+
sandboxId: process.env.MAINTAINER_PRO_SANDBOX_ID,
|
|
2468
|
+
dataDir: process.env.MAINTAINER_PRO_DATA_DIR
|
|
2469
|
+
}),
|
|
2281
2470
|
logger: options?.logger,
|
|
2282
2471
|
log: options?.log,
|
|
2283
2472
|
sync: options?.sync,
|
|
@@ -2286,7 +2475,10 @@ function createMaintainerProStoreFromEnv(options) {
|
|
|
2286
2475
|
}
|
|
2287
2476
|
|
|
2288
2477
|
// src/workspace-inspect.ts
|
|
2478
|
+
import fs6 from "fs";
|
|
2479
|
+
import path7 from "path";
|
|
2289
2480
|
import { z } from "zod";
|
|
2481
|
+
var CONFIG_INSPECT_TIMEOUT_MS = 18e3;
|
|
2290
2482
|
var inspectJsonSchema = z.object({
|
|
2291
2483
|
kind: z.enum(["next", "vite", "html", "empty", "other"]).optional(),
|
|
2292
2484
|
name: z.string().max(200).optional(),
|
|
@@ -2356,6 +2548,154 @@ ${input.extraContext ? `${input.extraContext.trim()}
|
|
|
2356
2548
|
|
|
2357
2549
|
` : ""}Inspect this project, fix any setup gaps you can, and return the JSON report.`;
|
|
2358
2550
|
}
|
|
2551
|
+
function readSnippet(file, max = 4e3) {
|
|
2552
|
+
try {
|
|
2553
|
+
const text = fs6.readFileSync(file, "utf8");
|
|
2554
|
+
return text.length > max ? `${text.slice(0, max)}
|
|
2555
|
+
\u2026` : text;
|
|
2556
|
+
} catch {
|
|
2557
|
+
return null;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
function envKeysOnly(file) {
|
|
2561
|
+
try {
|
|
2562
|
+
const keys = fs6.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);
|
|
2563
|
+
return keys.length ? keys.join("\n") : null;
|
|
2564
|
+
} catch {
|
|
2565
|
+
return null;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
function gatherConfigSnippets(workspaceDir) {
|
|
2569
|
+
const folder = path7.resolve(workspaceDir);
|
|
2570
|
+
const parts = [];
|
|
2571
|
+
const pkg = readSnippet(path7.join(folder, "package.json"));
|
|
2572
|
+
if (pkg) parts.push(`### package.json
|
|
2573
|
+
\`\`\`json
|
|
2574
|
+
${pkg}
|
|
2575
|
+
\`\`\``);
|
|
2576
|
+
for (const lock of [
|
|
2577
|
+
"package-lock.json",
|
|
2578
|
+
"pnpm-lock.yaml",
|
|
2579
|
+
"yarn.lock",
|
|
2580
|
+
"bun.lockb"
|
|
2581
|
+
]) {
|
|
2582
|
+
if (fs6.existsSync(path7.join(folder, lock))) {
|
|
2583
|
+
parts.push(`### lockfile
|
|
2584
|
+
${lock}`);
|
|
2585
|
+
break;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
for (const rel of [
|
|
2589
|
+
"vite.config.ts",
|
|
2590
|
+
"vite.config.js",
|
|
2591
|
+
"vite.config.mjs",
|
|
2592
|
+
"next.config.js",
|
|
2593
|
+
"next.config.mjs",
|
|
2594
|
+
"next.config.ts"
|
|
2595
|
+
]) {
|
|
2596
|
+
const body = readSnippet(path7.join(folder, rel), 2500);
|
|
2597
|
+
if (body) parts.push(`### ${rel}
|
|
2598
|
+
\`\`\`
|
|
2599
|
+
${body}
|
|
2600
|
+
\`\`\``);
|
|
2601
|
+
}
|
|
2602
|
+
for (const rel of [".env.example", ".env", ".env.local", ".env.development"]) {
|
|
2603
|
+
const keys = envKeysOnly(path7.join(folder, rel));
|
|
2604
|
+
if (keys) parts.push(`### ${rel} keys (values omitted)
|
|
2605
|
+
\`\`\`
|
|
2606
|
+
${keys}
|
|
2607
|
+
\`\`\``);
|
|
2608
|
+
}
|
|
2609
|
+
return parts.join("\n\n") || "(no config files found)";
|
|
2610
|
+
}
|
|
2611
|
+
function configOnlySystemPrompt(input) {
|
|
2612
|
+
return `You are Maintainer Pro's read-only setup advisor.
|
|
2613
|
+
|
|
2614
|
+
Workspace: ${input.workspaceDir}
|
|
2615
|
+
App name: ${input.appName || "the app"}
|
|
2616
|
+
|
|
2617
|
+
You are given configuration file excerpts only.
|
|
2618
|
+
DO NOT edit, create, or delete any files.
|
|
2619
|
+
DO NOT run shell commands, install packages, or start servers.
|
|
2620
|
+
Only infer how this project runs locally from the excerpts.
|
|
2621
|
+
|
|
2622
|
+
Script names you report MUST appear in package.json scripts. Omit chat/ai-server scripts.
|
|
2623
|
+
|
|
2624
|
+
Reply with a short human summary AND one \`\`\`json fence with:
|
|
2625
|
+
|
|
2626
|
+
{
|
|
2627
|
+
"kind": "next" | "vite" | "html" | "empty" | "other",
|
|
2628
|
+
"name": "package or folder name",
|
|
2629
|
+
"summary": "one sentence about this project",
|
|
2630
|
+
"scripts": { "ui": "dev:client", "backend": "dev:server" },
|
|
2631
|
+
"ports": { "ui": 5173, "backend": 4100 },
|
|
2632
|
+
"issues": ["uncertainties"],
|
|
2633
|
+
"fixes": [],
|
|
2634
|
+
"ready": true
|
|
2635
|
+
}`;
|
|
2636
|
+
}
|
|
2637
|
+
function configOnlyUserMessage(input, snippets) {
|
|
2638
|
+
return `${input.extraContext ? `${input.extraContext.trim()}
|
|
2639
|
+
|
|
2640
|
+
` : ""}Analyze these config excerpts and suggest local apps/ports/scripts. Do not change anything on disk.
|
|
2641
|
+
|
|
2642
|
+
${snippets}`;
|
|
2643
|
+
}
|
|
2644
|
+
function toInspectResult(input, text, providerId) {
|
|
2645
|
+
const parsed = extractInspectJson(text);
|
|
2646
|
+
return {
|
|
2647
|
+
kind: parsed?.kind ?? "other",
|
|
2648
|
+
name: parsed?.name?.trim() || input.appName || "",
|
|
2649
|
+
summary: parsed?.summary?.trim() || text.replace(/```json[\s\S]*```/i, "").trim().slice(0, 400),
|
|
2650
|
+
scripts: parsed?.scripts ?? {},
|
|
2651
|
+
ports: parsed?.ports ?? {},
|
|
2652
|
+
issues: parsed?.issues ?? [],
|
|
2653
|
+
fixes: parsed?.fixes ?? [],
|
|
2654
|
+
ready: parsed?.ready ?? parsed?.issues?.length === 0,
|
|
2655
|
+
provider: providerId,
|
|
2656
|
+
rawText: text
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
async function inspectConfigOnly(input, opts = {}) {
|
|
2660
|
+
const workspaceDir = path7.resolve(input.workspaceDir);
|
|
2661
|
+
const timeoutMs = opts.timeoutMs ?? CONFIG_INSPECT_TIMEOUT_MS;
|
|
2662
|
+
const provider = await resolveProvider({ preference: "auto" });
|
|
2663
|
+
const snippets = gatherConfigSnippets(workspaceDir);
|
|
2664
|
+
const call = callAi(
|
|
2665
|
+
[{ role: "user", content: configOnlyUserMessage(input, snippets) }],
|
|
2666
|
+
{
|
|
2667
|
+
route: "/local-setup-propose",
|
|
2668
|
+
pageTitle: input.appName || "Setup suggestions",
|
|
2669
|
+
relevantFiles: ["package.json", ".env.example"],
|
|
2670
|
+
data: {
|
|
2671
|
+
workspaceDir,
|
|
2672
|
+
readOnly: true
|
|
2673
|
+
}
|
|
2674
|
+
},
|
|
2675
|
+
{
|
|
2676
|
+
systemPrompt: configOnlySystemPrompt(input),
|
|
2677
|
+
workspaceDir,
|
|
2678
|
+
technical: true
|
|
2679
|
+
}
|
|
2680
|
+
);
|
|
2681
|
+
const timed = await Promise.race([
|
|
2682
|
+
call.then((response) => ({ ok: true, response })),
|
|
2683
|
+
new Promise(
|
|
2684
|
+
(resolve) => setTimeout(
|
|
2685
|
+
() => resolve({ ok: false, error: `Config inspect timed out after ${timeoutMs}ms` }),
|
|
2686
|
+
timeoutMs
|
|
2687
|
+
)
|
|
2688
|
+
)
|
|
2689
|
+
]);
|
|
2690
|
+
if (!timed.ok) {
|
|
2691
|
+
throw new Error(timed.error);
|
|
2692
|
+
}
|
|
2693
|
+
return toInspectResult(
|
|
2694
|
+
input,
|
|
2695
|
+
timed.response.text,
|
|
2696
|
+
timed.response.provider || provider.id
|
|
2697
|
+
);
|
|
2698
|
+
}
|
|
2359
2699
|
async function inspectAndRepairWorkspace(input) {
|
|
2360
2700
|
const workspaceDir = input.workspaceDir;
|
|
2361
2701
|
const provider = await resolveProvider({ preference: "auto" });
|
|
@@ -2376,24 +2716,670 @@ async function inspectAndRepairWorkspace(input) {
|
|
|
2376
2716
|
technical: true
|
|
2377
2717
|
}
|
|
2378
2718
|
);
|
|
2379
|
-
|
|
2719
|
+
return toInspectResult(
|
|
2720
|
+
input,
|
|
2721
|
+
response.text,
|
|
2722
|
+
response.provider || provider.id
|
|
2723
|
+
);
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
// src/host-apps.ts
|
|
2727
|
+
import fs7 from "fs";
|
|
2728
|
+
import path8 from "path";
|
|
2729
|
+
import { createHash as createHash2 } from "crypto";
|
|
2730
|
+
var COLLABORATER_DIR = ".collaborater";
|
|
2731
|
+
var AI_SERVER_APP_ID = "ai-server";
|
|
2732
|
+
var AI_SERVER_DEFAULT_PORT = 3100;
|
|
2733
|
+
var ENV_FILES = [".env", ".env.local", ".env.development", ".env.example"];
|
|
2734
|
+
function isPort(value) {
|
|
2735
|
+
return Number.isInteger(value) && Number(value) >= 1024 && Number(value) <= 65535;
|
|
2736
|
+
}
|
|
2737
|
+
function parsePort(value, fallback = 0) {
|
|
2738
|
+
if (typeof value === "number" && isPort(value)) return value;
|
|
2739
|
+
const text = String(value ?? "").trim();
|
|
2740
|
+
if (!text) return fallback;
|
|
2741
|
+
if (/^\d{2,5}$/.test(text)) {
|
|
2742
|
+
const n = Number(text);
|
|
2743
|
+
return isPort(n) ? n : fallback;
|
|
2744
|
+
}
|
|
2745
|
+
try {
|
|
2746
|
+
const port = Number(new URL(text).port);
|
|
2747
|
+
return isPort(port) ? port : fallback;
|
|
2748
|
+
} catch {
|
|
2749
|
+
const match = text.match(/--port(?:\s+|=)(\d{2,5})/i) || text.match(/\bPORT[=:]\s*(\d{2,5})/i) || text.match(/:(\d{2,5})\b/);
|
|
2750
|
+
const n = match ? Number(match[1]) : 0;
|
|
2751
|
+
return isPort(n) ? n : fallback;
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
function dataInput(folder, extra) {
|
|
2380
2755
|
return {
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2756
|
+
workspaceDir: folder,
|
|
2757
|
+
sandboxId: extra?.sandboxId,
|
|
2758
|
+
dataDir: extra?.dataDir
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
function hostAppsCachePath2(folder, extra) {
|
|
2762
|
+
return hostAppsCachePath(dataInput(folder, extra));
|
|
2763
|
+
}
|
|
2764
|
+
function defaultAiServerApp(port = AI_SERVER_DEFAULT_PORT) {
|
|
2765
|
+
return {
|
|
2766
|
+
id: AI_SERVER_APP_ID,
|
|
2767
|
+
name: "AI server",
|
|
2768
|
+
role: "ai-server",
|
|
2769
|
+
port: parsePort(port, AI_SERVER_DEFAULT_PORT),
|
|
2770
|
+
startCommand: null,
|
|
2771
|
+
source: "default",
|
|
2772
|
+
locked: true
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
function normalizeHostApp(raw) {
|
|
2776
|
+
if (!raw || typeof raw !== "object") return null;
|
|
2777
|
+
const row = raw;
|
|
2778
|
+
const port = parsePort(row.port);
|
|
2779
|
+
if (!port) return null;
|
|
2780
|
+
const role = String(row.role || "custom");
|
|
2781
|
+
const allowed = ["ai-server", "ui", "backend", "app", "custom"];
|
|
2782
|
+
const id = String(row.id || "").trim() || (role === "ai-server" ? AI_SERVER_APP_ID : "");
|
|
2783
|
+
if (!id) return null;
|
|
2784
|
+
return {
|
|
2785
|
+
id: id.slice(0, 64),
|
|
2786
|
+
name: String(row.name || id).trim().slice(0, 80) || id,
|
|
2787
|
+
role: allowed.includes(role) ? role : "custom",
|
|
2788
|
+
port,
|
|
2789
|
+
startCommand: typeof row.startCommand === "string" && row.startCommand.trim() ? row.startCommand.trim().slice(0, 200) : null,
|
|
2790
|
+
source: ["default", "env", "package", "ai", "manual"].includes(
|
|
2791
|
+
row.source
|
|
2792
|
+
) ? row.source : "manual",
|
|
2793
|
+
locked: row.locked === true || id === AI_SERVER_APP_ID || role === "ai-server",
|
|
2794
|
+
host: role !== "ai-server" && id !== AI_SERVER_APP_ID && row.host === true,
|
|
2795
|
+
envMaps: normalizeEnvMaps(row.envMaps)
|
|
2796
|
+
};
|
|
2797
|
+
}
|
|
2798
|
+
function normalizeEnvMaps(raw) {
|
|
2799
|
+
if (!Array.isArray(raw)) return [];
|
|
2800
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2801
|
+
const out = [];
|
|
2802
|
+
for (const item of raw) {
|
|
2803
|
+
if (!item || typeof item !== "object") continue;
|
|
2804
|
+
const row = item;
|
|
2805
|
+
const key = String(row.key || "").trim();
|
|
2806
|
+
const sourceAppId = String(row.sourceAppId || "").trim().slice(0, 64);
|
|
2807
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || key.length > 80) continue;
|
|
2808
|
+
if (!sourceAppId || seen.has(key)) continue;
|
|
2809
|
+
seen.add(key);
|
|
2810
|
+
out.push({ key, sourceAppId });
|
|
2811
|
+
}
|
|
2812
|
+
return out.slice(0, 30);
|
|
2813
|
+
}
|
|
2814
|
+
function ensureSingleHost(apps) {
|
|
2815
|
+
const next = apps.map((app) => ({
|
|
2816
|
+
...app,
|
|
2817
|
+
host: app.role !== "ai-server" && app.id !== AI_SERVER_APP_ID && app.host === true
|
|
2818
|
+
}));
|
|
2819
|
+
const marked = next.filter((app) => app.host);
|
|
2820
|
+
if (marked.length === 1) return next;
|
|
2821
|
+
if (marked.length > 1) {
|
|
2822
|
+
let kept = false;
|
|
2823
|
+
return next.map((app) => {
|
|
2824
|
+
if (!app.host) return app;
|
|
2825
|
+
if (kept) return { ...app, host: false };
|
|
2826
|
+
kept = true;
|
|
2827
|
+
return app;
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
const fallback = next.find((app) => app.role === "ui" || app.role === "app") || next.find((app) => app.role !== "ai-server");
|
|
2831
|
+
return next.map(
|
|
2832
|
+
(app) => fallback && app.id === fallback.id ? { ...app, host: true } : app
|
|
2833
|
+
);
|
|
2834
|
+
}
|
|
2835
|
+
function normalizeHostApps(raw) {
|
|
2836
|
+
if (!Array.isArray(raw)) return [];
|
|
2837
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2838
|
+
const apps = [];
|
|
2839
|
+
for (const item of raw) {
|
|
2840
|
+
const app = normalizeHostApp(item);
|
|
2841
|
+
if (!app || seen.has(app.id)) continue;
|
|
2842
|
+
seen.add(app.id);
|
|
2843
|
+
apps.push(app);
|
|
2844
|
+
}
|
|
2845
|
+
return apps;
|
|
2846
|
+
}
|
|
2847
|
+
function ensureAiServerApp(apps, preferredPort = AI_SERVER_DEFAULT_PORT) {
|
|
2848
|
+
const next = normalizeHostApps(apps);
|
|
2849
|
+
const existing = next.find((app) => app.id === AI_SERVER_APP_ID || app.role === "ai-server");
|
|
2850
|
+
if (existing) {
|
|
2851
|
+
existing.id = AI_SERVER_APP_ID;
|
|
2852
|
+
existing.role = "ai-server";
|
|
2853
|
+
existing.locked = true;
|
|
2854
|
+
existing.host = false;
|
|
2855
|
+
existing.name = existing.name || "AI server";
|
|
2856
|
+
return ensureSingleHost([existing, ...next.filter((app) => app !== existing)]);
|
|
2857
|
+
}
|
|
2858
|
+
return ensureSingleHost([defaultAiServerApp(preferredPort), ...next]);
|
|
2859
|
+
}
|
|
2860
|
+
function readText(file) {
|
|
2861
|
+
try {
|
|
2862
|
+
return fs7.readFileSync(file, "utf8");
|
|
2863
|
+
} catch {
|
|
2864
|
+
return null;
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
function readEnvFileMap(file) {
|
|
2868
|
+
const text = readText(file);
|
|
2869
|
+
if (!text) return {};
|
|
2870
|
+
const map = {};
|
|
2871
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
2872
|
+
const line = raw.trim();
|
|
2873
|
+
if (!line || line.startsWith("#")) continue;
|
|
2874
|
+
const eq = line.indexOf("=");
|
|
2875
|
+
if (eq < 1) continue;
|
|
2876
|
+
const key = line.slice(0, eq).trim();
|
|
2877
|
+
let value = line.slice(eq + 1).trim();
|
|
2878
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
2879
|
+
value = value.slice(1, -1);
|
|
2880
|
+
}
|
|
2881
|
+
if (key) map[key] = value;
|
|
2882
|
+
}
|
|
2883
|
+
return map;
|
|
2884
|
+
}
|
|
2885
|
+
function readProjectEnvLayers(folder) {
|
|
2886
|
+
const resolved = path8.resolve(folder);
|
|
2887
|
+
const layers = [];
|
|
2888
|
+
const merged = {};
|
|
2889
|
+
for (const name of ENV_FILES) {
|
|
2890
|
+
const file = path8.join(resolved, name);
|
|
2891
|
+
const values = readEnvFileMap(file);
|
|
2892
|
+
if (!Object.keys(values).length) continue;
|
|
2893
|
+
layers.push({ file: name, values });
|
|
2894
|
+
if (name === ".env.example") continue;
|
|
2895
|
+
Object.assign(merged, values);
|
|
2896
|
+
}
|
|
2897
|
+
return { merged, layers };
|
|
2898
|
+
}
|
|
2899
|
+
function readPackageJson(folder) {
|
|
2900
|
+
try {
|
|
2901
|
+
return JSON.parse(fs7.readFileSync(path8.join(folder, "package.json"), "utf8"));
|
|
2902
|
+
} catch {
|
|
2903
|
+
return null;
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
function isChatScript(name, command) {
|
|
2907
|
+
return /\b(ai-server|ai-cli|dev:chat|start:chat)\b/i.test(`${name} ${command}`);
|
|
2908
|
+
}
|
|
2909
|
+
function isUiCommand(command) {
|
|
2910
|
+
return /\b(vite|next|nuxt|astro|remix|react-scripts|webpack-dev-server|parcel)\b/i.test(
|
|
2911
|
+
command
|
|
2912
|
+
);
|
|
2913
|
+
}
|
|
2914
|
+
function configPort(folder, files) {
|
|
2915
|
+
for (const rel of files) {
|
|
2916
|
+
const text = readText(path8.join(folder, rel));
|
|
2917
|
+
if (!text) continue;
|
|
2918
|
+
const match = text.match(/\bport\s*[:=]\s*(\d{2,5})/i) || text.match(/--port(?:\s+|=)(\d{2,5})/i);
|
|
2919
|
+
const port = parsePort(match?.[1]);
|
|
2920
|
+
if (port) return port;
|
|
2921
|
+
}
|
|
2922
|
+
return 0;
|
|
2923
|
+
}
|
|
2924
|
+
function hostAppsFingerprint(folder) {
|
|
2925
|
+
const resolved = path8.resolve(folder);
|
|
2926
|
+
const parts = [];
|
|
2927
|
+
for (const rel of [
|
|
2928
|
+
"package.json",
|
|
2929
|
+
...ENV_FILES,
|
|
2930
|
+
"vite.config.ts",
|
|
2931
|
+
"vite.config.js",
|
|
2932
|
+
"vite.config.mjs",
|
|
2933
|
+
"next.config.js",
|
|
2934
|
+
"next.config.mjs",
|
|
2935
|
+
"next.config.ts"
|
|
2936
|
+
]) {
|
|
2937
|
+
const file = path8.join(resolved, rel);
|
|
2938
|
+
if (!fs7.existsSync(file)) continue;
|
|
2939
|
+
try {
|
|
2940
|
+
const st = fs7.statSync(file);
|
|
2941
|
+
parts.push(`${rel}:${st.size}:${Math.floor(st.mtimeMs)}`);
|
|
2942
|
+
} catch {
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
return createHash2("sha256").update(parts.join("|") || resolved).digest("hex").slice(0, 32);
|
|
2946
|
+
}
|
|
2947
|
+
function pickScript(scripts, names) {
|
|
2948
|
+
return names.find((name) => scripts[name] && !isChatScript(name, scripts[name])) || null;
|
|
2949
|
+
}
|
|
2950
|
+
function detectHostAppsFromFiles(folder, opts = {}) {
|
|
2951
|
+
const resolved = path8.resolve(folder);
|
|
2952
|
+
const reasons = [];
|
|
2953
|
+
const { merged, layers } = readProjectEnvLayers(resolved);
|
|
2954
|
+
const pkg = readPackageJson(resolved);
|
|
2955
|
+
const scripts = pkg?.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
|
|
2956
|
+
const apps = [];
|
|
2957
|
+
const envAi = parsePort(merged.AI_SERVER_PORT) || parsePort(merged.AI_SERVER_URL);
|
|
2958
|
+
const aiPort = envAi || parsePort(opts.preferredAiPort, AI_SERVER_DEFAULT_PORT);
|
|
2959
|
+
apps.push({
|
|
2960
|
+
...defaultAiServerApp(aiPort),
|
|
2961
|
+
source: envAi ? "env" : "default"
|
|
2962
|
+
});
|
|
2963
|
+
const envPorts = {
|
|
2964
|
+
PORT: layers.filter((layer) => layer.file !== ".env.example").map((layer) => ({ file: layer.file, port: parsePort(layer.values.PORT) })).filter((row) => row.port),
|
|
2965
|
+
APP_URL: parsePort(merged.APP_URL || merged.NEXT_PUBLIC_APP_URL || merged.PUBLIC_URL),
|
|
2966
|
+
API_PORT: parsePort(merged.API_PORT || merged.API_URL || merged.VITE_API_URL)
|
|
2967
|
+
};
|
|
2968
|
+
const portValues = [...new Set(envPorts.PORT.map((row) => row.port))];
|
|
2969
|
+
if (portValues.length > 1) {
|
|
2970
|
+
reasons.push(
|
|
2971
|
+
`PORT differs across env files: ${envPorts.PORT.map((row) => `${row.file}=${row.port}`).join(", ")}`
|
|
2972
|
+
);
|
|
2973
|
+
}
|
|
2974
|
+
if (envPorts.PORT[0] && envPorts.APP_URL && envPorts.PORT[0].port !== envPorts.APP_URL) {
|
|
2975
|
+
reasons.push(
|
|
2976
|
+
`PORT=${envPorts.PORT[0].port} does not match APP_URL port ${envPorts.APP_URL}`
|
|
2977
|
+
);
|
|
2978
|
+
}
|
|
2979
|
+
const uiScript = pickScript(scripts, [
|
|
2980
|
+
"dev:client",
|
|
2981
|
+
"dev:ui",
|
|
2982
|
+
"dev:web",
|
|
2983
|
+
"dev:frontend",
|
|
2984
|
+
"client",
|
|
2985
|
+
"start:client"
|
|
2986
|
+
]) || (scripts.dev && !isChatScript("dev", scripts.dev) ? "dev" : null);
|
|
2987
|
+
const apiScript = pickScript(scripts, [
|
|
2988
|
+
"dev:server",
|
|
2989
|
+
"dev:backend",
|
|
2990
|
+
"dev:api",
|
|
2991
|
+
"server",
|
|
2992
|
+
"start:server"
|
|
2993
|
+
]);
|
|
2994
|
+
const vitePort = configPort(resolved, ["vite.config.ts", "vite.config.js", "vite.config.mjs"]) || 5173;
|
|
2995
|
+
const nextPort = configPort(resolved, ["next.config.js", "next.config.mjs", "next.config.ts"]) || 3e3;
|
|
2996
|
+
const portFromEnv = parsePort(merged.PORT);
|
|
2997
|
+
const hostFromEnv = parsePort(merged.HOST_PORT) || parsePort(merged.HOST_URL) || parsePort(merged.CORS_ORIGIN);
|
|
2998
|
+
const uiFromEnv = hostFromEnv || envPorts.APP_URL || (portFromEnv && portFromEnv !== aiPort ? portFromEnv : 0);
|
|
2999
|
+
if (uiScript) {
|
|
3000
|
+
const command = scripts[uiScript] || "";
|
|
3001
|
+
const scriptPort = parsePort(command);
|
|
3002
|
+
const fallback = isUiCommand(command) && /vite/i.test(command) ? vitePort : nextPort;
|
|
3003
|
+
const port = uiFromEnv || scriptPort || fallback;
|
|
3004
|
+
apps.push({
|
|
3005
|
+
id: "ui",
|
|
3006
|
+
name: opts.appName || pkg?.name || "App",
|
|
3007
|
+
role: isUiCommand(command) ? "ui" : "app",
|
|
3008
|
+
port,
|
|
3009
|
+
startCommand: `npm run ${uiScript}`,
|
|
3010
|
+
source: uiFromEnv ? "env" : scriptPort ? "package" : "package",
|
|
3011
|
+
host: true
|
|
3012
|
+
});
|
|
3013
|
+
} else if (uiFromEnv) {
|
|
3014
|
+
apps.push({
|
|
3015
|
+
id: "ui",
|
|
3016
|
+
name: opts.appName || pkg?.name || "App",
|
|
3017
|
+
role: "ui",
|
|
3018
|
+
port: uiFromEnv,
|
|
3019
|
+
startCommand: scripts.dev ? "npm run dev" : null,
|
|
3020
|
+
source: "env",
|
|
3021
|
+
host: true
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
if (apiScript && apiScript !== uiScript) {
|
|
3025
|
+
const command = scripts[apiScript] || "";
|
|
3026
|
+
const port = envPorts.API_PORT || parsePort(command) || configPort(resolved, []) || 4100;
|
|
3027
|
+
apps.push({
|
|
3028
|
+
id: "backend",
|
|
3029
|
+
name: "Backend",
|
|
3030
|
+
role: "backend",
|
|
3031
|
+
port,
|
|
3032
|
+
startCommand: `npm run ${apiScript}`,
|
|
3033
|
+
source: envPorts.API_PORT ? "env" : "package"
|
|
3034
|
+
});
|
|
3035
|
+
}
|
|
3036
|
+
const hostApps = apps.filter((app) => app.role !== "ai-server");
|
|
3037
|
+
if (pkg && Object.keys(scripts).length && hostApps.length === 0) {
|
|
3038
|
+
reasons.push("Found package.json scripts but no host app port in env or config");
|
|
3039
|
+
}
|
|
3040
|
+
return {
|
|
3041
|
+
apps: ensureAiServerApp(apps, aiPort),
|
|
3042
|
+
reasons,
|
|
3043
|
+
confused: reasons.length > 0
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3046
|
+
function readHostAppsCache(folder, extra) {
|
|
3047
|
+
try {
|
|
3048
|
+
const raw = JSON.parse(
|
|
3049
|
+
fs7.readFileSync(hostAppsCachePath2(folder, extra), "utf8")
|
|
3050
|
+
);
|
|
3051
|
+
const apps = ensureAiServerApp(normalizeHostApps(raw.apps));
|
|
3052
|
+
if (!apps.length) return null;
|
|
3053
|
+
return {
|
|
3054
|
+
apps,
|
|
3055
|
+
fingerprint: typeof raw.fingerprint === "string" ? raw.fingerprint : void 0,
|
|
3056
|
+
updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : void 0
|
|
3057
|
+
};
|
|
3058
|
+
} catch {
|
|
3059
|
+
return null;
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
var readCollaboraterApps = readHostAppsCache;
|
|
3063
|
+
function writeHostAppsCache(folder, apps, extra = {}, loc) {
|
|
3064
|
+
const resolved = path8.resolve(folder);
|
|
3065
|
+
ensureProjectDataDir(dataInput(resolved, loc));
|
|
3066
|
+
const payload = {
|
|
3067
|
+
version: 1,
|
|
3068
|
+
fingerprint: hostAppsFingerprint(resolved),
|
|
3069
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3070
|
+
apps: ensureAiServerApp(apps),
|
|
3071
|
+
...extra
|
|
3072
|
+
};
|
|
3073
|
+
fs7.writeFileSync(
|
|
3074
|
+
hostAppsCachePath2(resolved, loc),
|
|
3075
|
+
`${JSON.stringify(payload, null, 2)}
|
|
3076
|
+
`,
|
|
3077
|
+
"utf8"
|
|
3078
|
+
);
|
|
3079
|
+
}
|
|
3080
|
+
var writeCollaboraterApps = writeHostAppsCache;
|
|
3081
|
+
function mergeDesiredHostApps(desired, detected) {
|
|
3082
|
+
const byId = new Map(detected.map((app) => [app.id, app]));
|
|
3083
|
+
const byRole = new Map(detected.map((app) => [app.role, app]));
|
|
3084
|
+
const merged = desired.map((app) => {
|
|
3085
|
+
const local = byId.get(app.id) || (app.role !== "custom" ? byRole.get(app.role) : void 0);
|
|
3086
|
+
return {
|
|
3087
|
+
...local,
|
|
3088
|
+
...app,
|
|
3089
|
+
startCommand: app.startCommand || local?.startCommand || null,
|
|
3090
|
+
locked: app.locked || app.id === AI_SERVER_APP_ID || app.role === "ai-server",
|
|
3091
|
+
host: app.role === "ai-server" || app.id === AI_SERVER_APP_ID ? false : app.host === true || app.host !== false && local?.host === true,
|
|
3092
|
+
envMaps: app.envMaps && app.envMaps.length ? app.envMaps : local?.envMaps || []
|
|
3093
|
+
};
|
|
3094
|
+
});
|
|
3095
|
+
return ensureAiServerApp(merged);
|
|
3096
|
+
}
|
|
3097
|
+
function appsFromInspectPorts(ports, scripts, name) {
|
|
3098
|
+
const apps = [];
|
|
3099
|
+
if (ports.ui || scripts.ui) {
|
|
3100
|
+
apps.push({
|
|
3101
|
+
id: "ui",
|
|
3102
|
+
name,
|
|
3103
|
+
role: "ui",
|
|
3104
|
+
port: parsePort(ports.ui, 5173),
|
|
3105
|
+
startCommand: scripts.ui ? `npm run ${scripts.ui}` : null,
|
|
3106
|
+
source: "ai"
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
if (ports.backend || scripts.backend) {
|
|
3110
|
+
apps.push({
|
|
3111
|
+
id: "backend",
|
|
3112
|
+
name: "Backend",
|
|
3113
|
+
role: "backend",
|
|
3114
|
+
port: parsePort(ports.backend, 4100),
|
|
3115
|
+
startCommand: scripts.backend ? `npm run ${scripts.backend}` : null,
|
|
3116
|
+
source: "ai"
|
|
3117
|
+
});
|
|
3118
|
+
}
|
|
3119
|
+
if (!apps.length && (ports.app || scripts.app)) {
|
|
3120
|
+
apps.push({
|
|
3121
|
+
id: "app",
|
|
3122
|
+
name,
|
|
3123
|
+
role: "app",
|
|
3124
|
+
port: parsePort(ports.app, 3e3),
|
|
3125
|
+
startCommand: scripts.app ? `npm run ${scripts.app}` : null,
|
|
3126
|
+
source: "ai"
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
3129
|
+
return apps;
|
|
3130
|
+
}
|
|
3131
|
+
function collectScriptAlternatives(folder, primary, opts = {}) {
|
|
3132
|
+
const pkg = readPackageJson(folder);
|
|
3133
|
+
const scripts = pkg?.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
|
|
3134
|
+
const usedCommands = new Set(
|
|
3135
|
+
primary.map((app) => app.startCommand?.trim()).filter((cmd) => Boolean(cmd))
|
|
3136
|
+
);
|
|
3137
|
+
const alternatives = [];
|
|
3138
|
+
const hostApp = primary.find((app) => app.role === "ui" || app.role === "app") || primary.find((app) => app.role !== "ai-server");
|
|
3139
|
+
const backendApp = primary.find((app) => app.role === "backend");
|
|
3140
|
+
for (const [name, command] of Object.entries(scripts)) {
|
|
3141
|
+
if (isChatScript(name, command)) continue;
|
|
3142
|
+
if (!/^(dev|start|serve)/i.test(name) && !isUiCommand(command)) continue;
|
|
3143
|
+
const startCommand = `npm run ${name}`;
|
|
3144
|
+
if (usedCommands.has(startCommand)) continue;
|
|
3145
|
+
const scriptPort = parsePort(command);
|
|
3146
|
+
const looksBackend = /\b(server|api|backend)\b/i.test(`${name} ${command}`) && !isUiCommand(command);
|
|
3147
|
+
const target = looksBackend ? backendApp || hostApp : hostApp;
|
|
3148
|
+
if (!target) continue;
|
|
3149
|
+
const fallbackPort = scriptPort || (looksBackend ? 4100 : isUiCommand(command) && /vite/i.test(command) ? 5173 : 3e3);
|
|
3150
|
+
alternatives.push({
|
|
3151
|
+
appId: target.id,
|
|
3152
|
+
port: fallbackPort,
|
|
3153
|
+
startCommand,
|
|
3154
|
+
label: `${name} \u2192 ${fallbackPort}`,
|
|
3155
|
+
source: "package"
|
|
3156
|
+
});
|
|
3157
|
+
if (alternatives.length >= 8) break;
|
|
3158
|
+
}
|
|
3159
|
+
const { layers } = readProjectEnvLayers(folder);
|
|
3160
|
+
if (hostApp) {
|
|
3161
|
+
for (const layer of layers) {
|
|
3162
|
+
if (layer.file === ".env.example") continue;
|
|
3163
|
+
const port = parsePort(layer.values.PORT);
|
|
3164
|
+
if (!port || port === hostApp.port) continue;
|
|
3165
|
+
if (alternatives.some(
|
|
3166
|
+
(alt) => alt.appId === hostApp.id && alt.port === port && !alt.startCommand
|
|
3167
|
+
)) {
|
|
3168
|
+
continue;
|
|
3169
|
+
}
|
|
3170
|
+
alternatives.push({
|
|
3171
|
+
appId: hostApp.id,
|
|
3172
|
+
port,
|
|
3173
|
+
startCommand: hostApp.startCommand || null,
|
|
3174
|
+
label: `${layer.file} PORT=${port}`,
|
|
3175
|
+
source: "env"
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
void opts;
|
|
3180
|
+
return alternatives;
|
|
3181
|
+
}
|
|
3182
|
+
function proposalConfidence(apps, reasons, usedAi) {
|
|
3183
|
+
const hostApps = apps.filter((app) => app.role !== "ai-server");
|
|
3184
|
+
if (!hostApps.length) return "low";
|
|
3185
|
+
if (reasons.length === 0 && hostApps.every((app) => app.startCommand)) {
|
|
3186
|
+
return usedAi ? "medium" : "high";
|
|
3187
|
+
}
|
|
3188
|
+
if (reasons.some((reason) => /differ|does not match|no host app/i.test(reason))) {
|
|
3189
|
+
return "low";
|
|
3190
|
+
}
|
|
3191
|
+
return "medium";
|
|
3192
|
+
}
|
|
3193
|
+
function projectSummaryFromDetect(folder, apps, opts = {}) {
|
|
3194
|
+
const pkg = readPackageJson(folder);
|
|
3195
|
+
const name = opts.appName || pkg?.name || path8.basename(path8.resolve(folder));
|
|
3196
|
+
const hostApps = apps.filter((app) => app.role !== "ai-server");
|
|
3197
|
+
if (!hostApps.length) {
|
|
3198
|
+
return `${name}: no UI/backend ports detected from config yet.`;
|
|
3199
|
+
}
|
|
3200
|
+
return `${name}: ${hostApps.map((app) => `${app.name} on ${app.port}${app.startCommand ? ` (${app.startCommand})` : ""}`).join(", ")}.`;
|
|
3201
|
+
}
|
|
3202
|
+
async function proposeHostAppsFromConfig(input) {
|
|
3203
|
+
const folder = path8.resolve(input.workspaceDir);
|
|
3204
|
+
const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
|
|
3205
|
+
const fingerprint = hostAppsFingerprint(folder);
|
|
3206
|
+
const allowAi = input.allowAi !== false;
|
|
3207
|
+
const detected = detectHostAppsFromFiles(folder, {
|
|
3208
|
+
preferredAiPort: preferredAi,
|
|
3209
|
+
appName: input.appName
|
|
3210
|
+
});
|
|
3211
|
+
let apps = ensureAiServerApp(detected.apps, preferredAi);
|
|
3212
|
+
let reasons = [...detected.reasons];
|
|
3213
|
+
let usedAi = false;
|
|
3214
|
+
let projectSummary = projectSummaryFromDetect(folder, apps, {
|
|
3215
|
+
appName: input.appName
|
|
3216
|
+
});
|
|
3217
|
+
const hostCount = apps.filter((app) => app.role !== "ai-server").length;
|
|
3218
|
+
const needsAi = allowAi && (detected.confused || hostCount === 0 || reasons.some((reason) => /differ|does not match/i.test(reason)));
|
|
3219
|
+
if (needsAi) {
|
|
3220
|
+
try {
|
|
3221
|
+
const inspected = await inspectConfigOnly({
|
|
3222
|
+
workspaceDir: folder,
|
|
3223
|
+
appName: input.appName,
|
|
3224
|
+
extraContext: [
|
|
3225
|
+
"File-based detection was uncertain or incomplete.",
|
|
3226
|
+
...reasons
|
|
3227
|
+
].join("\n")
|
|
3228
|
+
});
|
|
3229
|
+
usedAi = true;
|
|
3230
|
+
const fromAi = appsFromInspectPorts(
|
|
3231
|
+
inspected.ports,
|
|
3232
|
+
inspected.scripts,
|
|
3233
|
+
inspected.name || input.appName || "App"
|
|
3234
|
+
);
|
|
3235
|
+
if (fromAi.length) {
|
|
3236
|
+
apps = ensureAiServerApp(
|
|
3237
|
+
[defaultAiServerApp(preferredAi), ...fromAi],
|
|
3238
|
+
preferredAi
|
|
3239
|
+
);
|
|
3240
|
+
}
|
|
3241
|
+
if (inspected.summary) projectSummary = inspected.summary;
|
|
3242
|
+
if (inspected.issues?.length) {
|
|
3243
|
+
reasons = [...reasons, ...inspected.issues];
|
|
3244
|
+
}
|
|
3245
|
+
} catch (err) {
|
|
3246
|
+
reasons.push(
|
|
3247
|
+
`AI suggest skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
3248
|
+
);
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
const alternatives = collectScriptAlternatives(folder, apps, {
|
|
3252
|
+
appName: input.appName
|
|
3253
|
+
});
|
|
3254
|
+
const confidence = proposalConfidence(apps, reasons, usedAi);
|
|
3255
|
+
const needsReview = confidence !== "high" || detected.confused || apps.filter((app) => app.role !== "ai-server").length === 0;
|
|
3256
|
+
return {
|
|
3257
|
+
apps,
|
|
3258
|
+
alternatives,
|
|
3259
|
+
reasons,
|
|
3260
|
+
confidence,
|
|
3261
|
+
projectSummary,
|
|
3262
|
+
usedAi,
|
|
3263
|
+
needsReview,
|
|
3264
|
+
fingerprint
|
|
3265
|
+
};
|
|
3266
|
+
}
|
|
3267
|
+
async function resolveHostApps(input) {
|
|
3268
|
+
const folder = path8.resolve(input.workspaceDir);
|
|
3269
|
+
const loc = { sandboxId: input.sandboxId, dataDir: input.dataDir };
|
|
3270
|
+
const fingerprint = hostAppsFingerprint(folder);
|
|
3271
|
+
const preferredAi = parsePort(input.preferredAiPort, AI_SERVER_DEFAULT_PORT);
|
|
3272
|
+
const detected = detectHostAppsFromFiles(folder, {
|
|
3273
|
+
preferredAiPort: preferredAi,
|
|
3274
|
+
appName: input.appName
|
|
3275
|
+
});
|
|
3276
|
+
const desired = normalizeHostApps(input.desired);
|
|
3277
|
+
if (desired.length) {
|
|
3278
|
+
const apps2 = mergeDesiredHostApps(desired, detected.apps);
|
|
3279
|
+
try {
|
|
3280
|
+
writeHostAppsCache(folder, apps2, { source: "desired" }, loc);
|
|
3281
|
+
} catch {
|
|
3282
|
+
}
|
|
3283
|
+
return {
|
|
3284
|
+
apps: apps2,
|
|
3285
|
+
source: "desired",
|
|
3286
|
+
cached: false,
|
|
3287
|
+
usedAi: false,
|
|
3288
|
+
confused: false,
|
|
3289
|
+
reasons: [],
|
|
3290
|
+
fingerprint
|
|
3291
|
+
};
|
|
3292
|
+
}
|
|
3293
|
+
if (!input.force) {
|
|
3294
|
+
const cached = readHostAppsCache(folder, loc);
|
|
3295
|
+
if (cached && cached.fingerprint === fingerprint) {
|
|
3296
|
+
return {
|
|
3297
|
+
apps: ensureAiServerApp(cached.apps, preferredAi),
|
|
3298
|
+
source: "cache",
|
|
3299
|
+
cached: true,
|
|
3300
|
+
usedAi: false,
|
|
3301
|
+
confused: false,
|
|
3302
|
+
reasons: [],
|
|
3303
|
+
fingerprint
|
|
3304
|
+
};
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
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)));
|
|
3308
|
+
if (needsAi) {
|
|
3309
|
+
try {
|
|
3310
|
+
const inspected = await inspectConfigOnly({
|
|
3311
|
+
workspaceDir: folder,
|
|
3312
|
+
appName: input.appName,
|
|
3313
|
+
extraContext: `Port detection was uncertain:
|
|
3314
|
+
${detected.reasons.join("\n")}
|
|
3315
|
+
Return the real local ports and npm script names.`
|
|
3316
|
+
});
|
|
3317
|
+
const fromAi = appsFromInspectPorts(
|
|
3318
|
+
inspected.ports,
|
|
3319
|
+
inspected.scripts,
|
|
3320
|
+
inspected.name || input.appName || "App"
|
|
3321
|
+
);
|
|
3322
|
+
const apps2 = ensureAiServerApp(
|
|
3323
|
+
[defaultAiServerApp(preferredAi), ...fromAi],
|
|
3324
|
+
preferredAi
|
|
3325
|
+
);
|
|
3326
|
+
writeHostAppsCache(
|
|
3327
|
+
folder,
|
|
3328
|
+
apps2,
|
|
3329
|
+
{
|
|
3330
|
+
source: "ai",
|
|
3331
|
+
reasons: detected.reasons,
|
|
3332
|
+
summary: inspected.summary
|
|
3333
|
+
},
|
|
3334
|
+
loc
|
|
3335
|
+
);
|
|
3336
|
+
return {
|
|
3337
|
+
apps: apps2,
|
|
3338
|
+
source: "ai",
|
|
3339
|
+
cached: false,
|
|
3340
|
+
usedAi: true,
|
|
3341
|
+
confused: detected.confused,
|
|
3342
|
+
reasons: detected.reasons,
|
|
3343
|
+
fingerprint
|
|
3344
|
+
};
|
|
3345
|
+
} catch (err) {
|
|
3346
|
+
detected.reasons.push(
|
|
3347
|
+
`AI inspect skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
3348
|
+
);
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
const apps = ensureAiServerApp(detected.apps, preferredAi);
|
|
3352
|
+
try {
|
|
3353
|
+
writeHostAppsCache(
|
|
3354
|
+
folder,
|
|
3355
|
+
apps,
|
|
3356
|
+
{
|
|
3357
|
+
source: detected.confused ? "env" : "env",
|
|
3358
|
+
reasons: detected.reasons
|
|
3359
|
+
},
|
|
3360
|
+
loc
|
|
3361
|
+
);
|
|
3362
|
+
} catch {
|
|
3363
|
+
}
|
|
3364
|
+
return {
|
|
3365
|
+
apps,
|
|
3366
|
+
source: apps.some((app) => app.source === "env") ? "env" : "default",
|
|
3367
|
+
cached: false,
|
|
3368
|
+
usedAi: false,
|
|
3369
|
+
confused: detected.confused,
|
|
3370
|
+
reasons: detected.reasons,
|
|
3371
|
+
fingerprint
|
|
2391
3372
|
};
|
|
2392
3373
|
}
|
|
2393
3374
|
export {
|
|
2394
3375
|
ACCESS_IGNORE_BEGIN,
|
|
2395
3376
|
ACCESS_IGNORE_END,
|
|
3377
|
+
AI_SERVER_APP_ID,
|
|
3378
|
+
AI_SERVER_DEFAULT_PORT,
|
|
3379
|
+
COLLABORATER_DIR,
|
|
2396
3380
|
DEFAULT_AI_IGNORE_PATHS,
|
|
3381
|
+
HOST_APPS_FILE,
|
|
3382
|
+
MAINTAINER_PRO_HOME_DIR,
|
|
2397
3383
|
PROMPT_SECTION,
|
|
2398
3384
|
WORKING_PROVIDER,
|
|
2399
3385
|
buildClaudeUserPrompt,
|
|
@@ -2416,27 +3402,53 @@ export {
|
|
|
2416
3402
|
createMaintainerProStoreFromEnv,
|
|
2417
3403
|
createSyncedChatStore,
|
|
2418
3404
|
createToolValidator,
|
|
3405
|
+
defaultAiServerApp,
|
|
3406
|
+
detectHostAppsFromFiles,
|
|
3407
|
+
ensureAiServerApp,
|
|
3408
|
+
ensureProjectDataDir,
|
|
3409
|
+
ensureSingleHost,
|
|
2419
3410
|
formatAccessPolicyPromptSection,
|
|
2420
3411
|
formatClientContext,
|
|
2421
3412
|
formatParentChainContext,
|
|
2422
3413
|
getProviderPreference,
|
|
3414
|
+
hostAppsCachePath2 as hostAppsCachePath,
|
|
3415
|
+
hostAppsFingerprint,
|
|
2423
3416
|
inspectAndRepairWorkspace,
|
|
3417
|
+
inspectConfigOnly,
|
|
2424
3418
|
isDevMode,
|
|
2425
3419
|
isIgnoredRelative,
|
|
2426
3420
|
isInsideWorkspace,
|
|
2427
3421
|
isPathAllowed,
|
|
3422
|
+
maintainerProHome,
|
|
3423
|
+
mergeDesiredHostApps,
|
|
3424
|
+
normalizeEnvMaps,
|
|
3425
|
+
normalizeHostApp,
|
|
3426
|
+
normalizeHostApps,
|
|
2428
3427
|
normalizeIgnorePaths,
|
|
2429
3428
|
parentChainForTurn,
|
|
2430
3429
|
parseAiResponse,
|
|
2431
3430
|
parseIgnorePathsEnv,
|
|
3431
|
+
parsePort,
|
|
2432
3432
|
previewText,
|
|
3433
|
+
hostAppsCachePath as projectHostAppsPath,
|
|
3434
|
+
projectIdForFolder,
|
|
3435
|
+
projectUploadsDir,
|
|
3436
|
+
proposeHostAppsFromConfig,
|
|
2433
3437
|
providerLabel,
|
|
3438
|
+
readCollaboraterApps,
|
|
3439
|
+
readHostAppsCache,
|
|
3440
|
+
readProjectEnvLayers,
|
|
2434
3441
|
renderManagedIgnoreBlock,
|
|
2435
3442
|
resolveCliBinary,
|
|
3443
|
+
resolveHostApps,
|
|
2436
3444
|
resolveIgnorePaths,
|
|
2437
3445
|
resolveLogLevel,
|
|
3446
|
+
resolveProjectDataDir,
|
|
2438
3447
|
resolveProvider,
|
|
3448
|
+
sanitizeProjectId,
|
|
2439
3449
|
saveChatAttachments,
|
|
2440
3450
|
toNextRoute,
|
|
2441
|
-
upsertManagedIgnoreFile
|
|
3451
|
+
upsertManagedIgnoreFile,
|
|
3452
|
+
writeCollaboraterApps,
|
|
3453
|
+
writeHostAppsCache
|
|
2442
3454
|
};
|