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