@maintainer-pro/ai-bridge 0.1.32 → 0.1.33
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/README.md +1 -1
- package/package.json +3 -3
- package/src/daemon.mjs +522 -80
- package/src/intake-server.mjs +516 -0
- package/src/share-rewrite.mjs +12 -2
- package/src/website-feedback.mjs +130 -0
package/src/daemon.mjs
CHANGED
|
@@ -43,6 +43,18 @@ import {
|
|
|
43
43
|
mergeCookieHeader,
|
|
44
44
|
storeCorsCookies,
|
|
45
45
|
} from "./cors-cookies.mjs";
|
|
46
|
+
import {
|
|
47
|
+
isIntakeArtifactName,
|
|
48
|
+
startIntakeServer,
|
|
49
|
+
} from "./intake-server.mjs";
|
|
50
|
+
import {
|
|
51
|
+
flushWebsiteFeedback,
|
|
52
|
+
isIntakeBuiltWorkspace,
|
|
53
|
+
markIntakeBuilt,
|
|
54
|
+
queueWebsiteFeedback,
|
|
55
|
+
websiteFeedbackShouldFlush,
|
|
56
|
+
WEBSITE_FEEDBACK_CHECK_MS,
|
|
57
|
+
} from "./website-feedback.mjs";
|
|
46
58
|
import { WebSocket as WsWebSocket } from "ws";
|
|
47
59
|
|
|
48
60
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -69,8 +81,52 @@ const WS_WATCHDOG_MS = 5_000;
|
|
|
69
81
|
|
|
70
82
|
/** @type {import("@maintainer-pro/ai-cli").Logger} */
|
|
71
83
|
let logger = createLogger("ai-bridge");
|
|
72
|
-
const
|
|
73
|
-
|
|
84
|
+
const SECRET_KEY_RE =
|
|
85
|
+
/token|secret|password|authorization|cookie|api[_-]?key|bearer|pair/i;
|
|
86
|
+
|
|
87
|
+
function sanitizeLogValue(value, depth = 0) {
|
|
88
|
+
if (value == null) return value;
|
|
89
|
+
if (typeof value === "string") {
|
|
90
|
+
return value.length > 280 ? `${value.slice(0, 280)}…` : value;
|
|
91
|
+
}
|
|
92
|
+
if (typeof value !== "object") return value;
|
|
93
|
+
if (depth > 2) return Array.isArray(value) ? `[${value.length} items]` : "[object]";
|
|
94
|
+
if (Array.isArray(value)) {
|
|
95
|
+
return value.slice(0, 16).map((item) => sanitizeLogValue(item, depth + 1));
|
|
96
|
+
}
|
|
97
|
+
/** @type {Record<string, unknown>} */
|
|
98
|
+
const out = {};
|
|
99
|
+
for (const [key, item] of Object.entries(value)) {
|
|
100
|
+
out[key] = SECRET_KEY_RE.test(key)
|
|
101
|
+
? "[redacted]"
|
|
102
|
+
: sanitizeLogValue(item, depth + 1);
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function logExtra(extra) {
|
|
108
|
+
return extra && typeof extra === "object" && !Array.isArray(extra)
|
|
109
|
+
? extra
|
|
110
|
+
: extra != null
|
|
111
|
+
? { detail: extra }
|
|
112
|
+
: undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const log = (msg, extra) => {
|
|
116
|
+
const fields = logExtra(extra);
|
|
117
|
+
if (fields) logger.info(fields, String(msg));
|
|
118
|
+
else logger.info(String(msg));
|
|
119
|
+
};
|
|
120
|
+
const dbg = (msg, extra) => {
|
|
121
|
+
const fields = logExtra(extra);
|
|
122
|
+
if (fields) logger.debug(fields, String(msg));
|
|
123
|
+
else logger.debug(String(msg));
|
|
124
|
+
};
|
|
125
|
+
const warn = (msg, extra) => {
|
|
126
|
+
const fields = logExtra(extra);
|
|
127
|
+
if (fields) logger.warn(fields, String(msg));
|
|
128
|
+
else logger.warn(String(msg));
|
|
129
|
+
};
|
|
74
130
|
const fail = (msg) => {
|
|
75
131
|
logger.fatal(msg);
|
|
76
132
|
process.exit(1);
|
|
@@ -79,6 +135,9 @@ const fail = (msg) => {
|
|
|
79
135
|
/** @type {Map<string, Array<{ at: string, level: string, message: string }>>} */
|
|
80
136
|
const activityBySandbox = new Map();
|
|
81
137
|
|
|
138
|
+
/** @type {Map<string, { port: number, close: () => Promise<void> }>} */
|
|
139
|
+
const intakeServers = new Map();
|
|
140
|
+
|
|
82
141
|
/** @type {(payload: Record<string, unknown>) => boolean} */
|
|
83
142
|
let bridgeSend = () => false;
|
|
84
143
|
|
|
@@ -94,7 +153,7 @@ function activity(sandboxId, level, message) {
|
|
|
94
153
|
level: level === "error" || level === "warn" ? level : "info",
|
|
95
154
|
message: text.slice(0, 500),
|
|
96
155
|
});
|
|
97
|
-
activityBySandbox.set(sandboxId, list.slice(-
|
|
156
|
+
activityBySandbox.set(sandboxId, list.slice(-80));
|
|
98
157
|
}
|
|
99
158
|
|
|
100
159
|
function activityLogFor(sandboxId) {
|
|
@@ -152,16 +211,30 @@ function resultSummary(result) {
|
|
|
152
211
|
if (result.folderPath) bits.push(`folder=${result.folderPath}`);
|
|
153
212
|
if (result.port) bits.push(`chat=${result.port}`);
|
|
154
213
|
if (result.appUrl) bits.push(`app=${result.appUrl}`);
|
|
214
|
+
if (result.publicUrl) bits.push(`share=${result.publicUrl}`);
|
|
215
|
+
if (result.clientKind) bits.push(`kind=${result.clientKind}`);
|
|
216
|
+
if (result.intakeStatus) bits.push(`intake=${result.intakeStatus}`);
|
|
155
217
|
if (Array.isArray(result.origins) && result.origins.length) {
|
|
156
218
|
bits.push(`origins=${result.origins.join(",")}`);
|
|
157
219
|
}
|
|
158
220
|
if (typeof result.up === "boolean") bits.push(`chatUp=${result.up}`);
|
|
221
|
+
if (typeof result.running === "boolean") bits.push(`running=${result.running}`);
|
|
159
222
|
if (typeof result.waitingForStart === "boolean") {
|
|
160
223
|
bits.push(`waitingForStart=${result.waitingForStart}`);
|
|
161
224
|
}
|
|
162
225
|
if (Array.isArray(result.startedHosts) && result.startedHosts.length) {
|
|
163
226
|
bits.push(`started=${result.startedHosts.join(",")}`);
|
|
164
227
|
}
|
|
228
|
+
if (Array.isArray(result.hostApps) && result.hostApps.length) {
|
|
229
|
+
bits.push(
|
|
230
|
+
`apps=${result.hostApps
|
|
231
|
+
.map((app) => `${app?.id || app?.role || "?"}:${app?.port || "?"}`)
|
|
232
|
+
.join(",")}`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (Array.isArray(result.processIssues) && result.processIssues.length) {
|
|
236
|
+
bits.push(`issues=${result.processIssues.length}`);
|
|
237
|
+
}
|
|
165
238
|
if (result.warning) bits.push(`warning=${result.warning}`);
|
|
166
239
|
return bits.join(" ");
|
|
167
240
|
}
|
|
@@ -384,6 +457,7 @@ function fetchErrorDetail(err) {
|
|
|
384
457
|
|
|
385
458
|
async function api(baseUrl, token, method, pathname, body) {
|
|
386
459
|
const url = `${String(baseUrl || "").replace(/\/$/, "")}${pathname}`;
|
|
460
|
+
dbg(`admin ${method} ${pathname}`);
|
|
387
461
|
if (!baseUrl) {
|
|
388
462
|
throw new Error("Admin URL is missing. Pair this computer again.");
|
|
389
463
|
}
|
|
@@ -415,10 +489,14 @@ async function api(baseUrl, token, method, pathname, body) {
|
|
|
415
489
|
data = { raw: text };
|
|
416
490
|
}
|
|
417
491
|
if (!res.ok) {
|
|
492
|
+
warn(`admin ${method} ${pathname} → ${res.status}`, {
|
|
493
|
+
error: data?.error || text.slice(0, 200),
|
|
494
|
+
});
|
|
418
495
|
throw new Error(
|
|
419
496
|
data?.error || `Bridge API ${method} ${pathname} failed (${res.status})`
|
|
420
497
|
);
|
|
421
498
|
}
|
|
499
|
+
dbg(`admin ${method} ${pathname} → ${res.status}`);
|
|
422
500
|
return data;
|
|
423
501
|
}
|
|
424
502
|
|
|
@@ -943,6 +1021,63 @@ function persistWorkspaceEntry(cfg, ws) {
|
|
|
943
1021
|
saveConfig(cfg);
|
|
944
1022
|
}
|
|
945
1023
|
|
|
1024
|
+
function liveBridgeConfig() {
|
|
1025
|
+
return bridgeCfg || loadConfig();
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function noteIntakeWebsiteFeedback(ws, userMessage) {
|
|
1029
|
+
const cfg = liveBridgeConfig();
|
|
1030
|
+
const live =
|
|
1031
|
+
(cfg?.workspaces || []).find((row) => row.sandboxId === ws?.sandboxId) || ws;
|
|
1032
|
+
if (!isIntakeBuiltWorkspace(live)) return;
|
|
1033
|
+
if (!queueWebsiteFeedback(live, userMessage)) return;
|
|
1034
|
+
persistWorkspaceEntry(cfg, live);
|
|
1035
|
+
if (websiteFeedbackShouldFlush(live)) {
|
|
1036
|
+
void flushIntakeWebsiteFeedback(live, cfg);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const websiteFeedbackFlushing = new Set();
|
|
1041
|
+
|
|
1042
|
+
async function flushIntakeWebsiteFeedback(ws, cfg = liveBridgeConfig(), opts = {}) {
|
|
1043
|
+
if (!cfg?.adminUrl || !cfg.token || !ws?.sandboxId) return;
|
|
1044
|
+
if (!websiteFeedbackShouldFlush(ws, opts)) return;
|
|
1045
|
+
if (websiteFeedbackFlushing.has(ws.sandboxId)) return;
|
|
1046
|
+
websiteFeedbackFlushing.add(ws.sandboxId);
|
|
1047
|
+
try {
|
|
1048
|
+
const result = await flushWebsiteFeedback(ws, {
|
|
1049
|
+
loadAiCli,
|
|
1050
|
+
memorySource: {
|
|
1051
|
+
baseUrl: cfg.adminUrl,
|
|
1052
|
+
token: cfg.token,
|
|
1053
|
+
sandboxId: ws.sandboxId,
|
|
1054
|
+
},
|
|
1055
|
+
api: (method, pathname, body) =>
|
|
1056
|
+
api(cfg.adminUrl, cfg.token, method, pathname, body),
|
|
1057
|
+
});
|
|
1058
|
+
persistWorkspaceEntry(cfg, ws);
|
|
1059
|
+
if (result.appended) {
|
|
1060
|
+
activity(ws.sandboxId, "info", "Saved studio feedback for later websites");
|
|
1061
|
+
}
|
|
1062
|
+
} catch (err) {
|
|
1063
|
+
warn(
|
|
1064
|
+
`website memory flush ${shortId(ws.sandboxId)}: ${
|
|
1065
|
+
err instanceof Error ? err.message : String(err)
|
|
1066
|
+
}`
|
|
1067
|
+
);
|
|
1068
|
+
} finally {
|
|
1069
|
+
websiteFeedbackFlushing.delete(ws.sandboxId);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
async function flushAllIntakeWebsiteFeedback(opts = {}) {
|
|
1074
|
+
const cfg = liveBridgeConfig();
|
|
1075
|
+
const rows = Array.isArray(cfg?.workspaces) ? cfg.workspaces : [];
|
|
1076
|
+
for (const ws of rows) {
|
|
1077
|
+
await flushIntakeWebsiteFeedback(ws, cfg, opts);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
946
1081
|
/**
|
|
947
1082
|
* Sandbox store keys live on the workspace (and ~/.maintainer-pro/bridge.json),
|
|
948
1083
|
* not in the app .env. Never use the bridge machine token as MAINTAINER_PRO_API_KEY.
|
|
@@ -1824,6 +1959,9 @@ async function restoreHostsAfterReconnect(cfg) {
|
|
|
1824
1959
|
status.host.origins.join(",") || "none"
|
|
1825
1960
|
}`
|
|
1826
1961
|
);
|
|
1962
|
+
} else if (isIntakePendingWorkspace(ws)) {
|
|
1963
|
+
await startIntakeForWorkspace(ws, cfg);
|
|
1964
|
+
log(`intake form restored for ${label} on ${ws.sitePort}`);
|
|
1827
1965
|
} else {
|
|
1828
1966
|
log(
|
|
1829
1967
|
`no apps running for ${label} — waiting for Start Apps from Maintainer Pro`
|
|
@@ -2032,12 +2170,13 @@ function detectProjectKind(dir) {
|
|
|
2032
2170
|
return "html";
|
|
2033
2171
|
}
|
|
2034
2172
|
|
|
2035
|
-
// Only README / license → treat as empty scaffold target
|
|
2173
|
+
// Only README / license / intake artifacts → treat as empty scaffold target
|
|
2036
2174
|
const meaningful = names.filter(
|
|
2037
2175
|
(n) =>
|
|
2038
2176
|
!/^readme/i.test(n) &&
|
|
2039
2177
|
!/^license/i.test(n) &&
|
|
2040
|
-
n !== "package.json"
|
|
2178
|
+
n !== "package.json" &&
|
|
2179
|
+
!isIntakeArtifactName(n)
|
|
2041
2180
|
);
|
|
2042
2181
|
if (meaningful.length === 0 && !has("package.json")) return "empty";
|
|
2043
2182
|
|
|
@@ -2052,37 +2191,26 @@ function escapeHtml(value) {
|
|
|
2052
2191
|
.replaceAll('"', """);
|
|
2053
2192
|
}
|
|
2054
2193
|
|
|
2055
|
-
function
|
|
2056
|
-
return
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
AiUi.init({
|
|
2076
|
-
apiUrl: cfg.apiUrl || "/api/chat",
|
|
2077
|
-
title: "AI Assistant",
|
|
2078
|
-
maintainerProUrl: cfg.maintainerProUrl || undefined,
|
|
2079
|
-
maintainerProApiKey: cfg.maintainerProApiKey || undefined,
|
|
2080
|
-
});
|
|
2081
|
-
})();
|
|
2082
|
-
</script>
|
|
2083
|
-
</body>
|
|
2084
|
-
</html>
|
|
2085
|
-
`;
|
|
2194
|
+
function intakeHostApp(appName, port, folder) {
|
|
2195
|
+
return {
|
|
2196
|
+
id: "ui",
|
|
2197
|
+
name: appName || "Website",
|
|
2198
|
+
role: "ui",
|
|
2199
|
+
port: Number(port),
|
|
2200
|
+
startCommand: null,
|
|
2201
|
+
source: "manual",
|
|
2202
|
+
host: true,
|
|
2203
|
+
folderPath: folder || null,
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
function isIntakePendingFolder(dir) {
|
|
2208
|
+
return detectProjectKind(dir) === "empty";
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
function isIntakePendingWorkspace(ws) {
|
|
2212
|
+
const folder = path.resolve(ws?.folderPath || "");
|
|
2213
|
+
return Boolean(Number(ws?.sitePort)) && isIntakePendingFolder(folder);
|
|
2086
2214
|
}
|
|
2087
2215
|
|
|
2088
2216
|
/**
|
|
@@ -2096,6 +2224,7 @@ function configureClient(opts) {
|
|
|
2096
2224
|
appName,
|
|
2097
2225
|
mode, // auto | empty | existing | skip
|
|
2098
2226
|
hostAppUrl,
|
|
2227
|
+
sitePort,
|
|
2099
2228
|
} = opts;
|
|
2100
2229
|
const aiOrigin = `http://localhost:${port}`;
|
|
2101
2230
|
/** @type {string[]} */
|
|
@@ -2120,21 +2249,22 @@ function configureClient(opts) {
|
|
|
2120
2249
|
}
|
|
2121
2250
|
|
|
2122
2251
|
if (kind === "empty" || mode === "empty") {
|
|
2123
|
-
const
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2252
|
+
const origin = sitePort
|
|
2253
|
+
? `http://localhost:${sitePort}`
|
|
2254
|
+
: hostAppUrl || aiOrigin;
|
|
2255
|
+
notes.push(
|
|
2256
|
+
sitePort
|
|
2257
|
+
? `Serving the business intake form on port ${sitePort}. The generated site will use the same port.`
|
|
2258
|
+
: "Empty folder — waiting for a reserved site port."
|
|
2259
|
+
);
|
|
2131
2260
|
return {
|
|
2132
2261
|
kind: "empty",
|
|
2133
2262
|
notes,
|
|
2134
2263
|
filesWritten,
|
|
2135
|
-
corsOrigin:
|
|
2136
|
-
appUrl:
|
|
2137
|
-
sameOrigin:
|
|
2264
|
+
corsOrigin: origin,
|
|
2265
|
+
appUrl: origin,
|
|
2266
|
+
sameOrigin: false,
|
|
2267
|
+
sitePort: sitePort || null,
|
|
2138
2268
|
};
|
|
2139
2269
|
}
|
|
2140
2270
|
|
|
@@ -2182,6 +2312,172 @@ function configureClient(opts) {
|
|
|
2182
2312
|
};
|
|
2183
2313
|
}
|
|
2184
2314
|
|
|
2315
|
+
async function stopIntakeForWorkspace(sandboxId) {
|
|
2316
|
+
const row = intakeServers.get(sandboxId);
|
|
2317
|
+
if (!row) return;
|
|
2318
|
+
intakeServers.delete(sandboxId);
|
|
2319
|
+
try {
|
|
2320
|
+
await row.close();
|
|
2321
|
+
} catch {
|
|
2322
|
+
/* ignore */
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
async function startIntakeForWorkspace(ws, cfg) {
|
|
2327
|
+
const port = Number(ws.sitePort);
|
|
2328
|
+
if (!port || !ws.sandboxId) return null;
|
|
2329
|
+
const existing = intakeServers.get(ws.sandboxId);
|
|
2330
|
+
if (existing && existing.port === port && (await portIsLive(port, 600))) {
|
|
2331
|
+
dbg(`intake already listening ${shortId(ws.sandboxId)} :${port}`);
|
|
2332
|
+
return existing;
|
|
2333
|
+
}
|
|
2334
|
+
if (existing) {
|
|
2335
|
+
log(`intake restart ${shortId(ws.sandboxId)} :${existing.port} → :${port}`);
|
|
2336
|
+
await stopIntakeForWorkspace(ws.sandboxId);
|
|
2337
|
+
}
|
|
2338
|
+
if (await portIsLive(port, 600)) {
|
|
2339
|
+
warn(`intake skip ${shortId(ws.sandboxId)}: port ${port} already in use`);
|
|
2340
|
+
return existing || null;
|
|
2341
|
+
}
|
|
2342
|
+
const instance = await startIntakeServer({
|
|
2343
|
+
port,
|
|
2344
|
+
appName: ws.applicationName || ws.sandboxName,
|
|
2345
|
+
getStatus: () => ({
|
|
2346
|
+
status: ws.intakeStatus || "form",
|
|
2347
|
+
message: ws.intakeMessage || "",
|
|
2348
|
+
startedAt: Number(ws.intakeStartedAt) || 0,
|
|
2349
|
+
etaSeconds: 5 * 60,
|
|
2350
|
+
}),
|
|
2351
|
+
onSubmit: async (intake) => {
|
|
2352
|
+
if (ws.intakeStatus === "building") {
|
|
2353
|
+
warn(`intake submit ignored ${shortId(ws.sandboxId)}: already building`);
|
|
2354
|
+
throw new Error("A website is already being created.");
|
|
2355
|
+
}
|
|
2356
|
+
ws.intakeStatus = "building";
|
|
2357
|
+
ws.intakeStartedAt = Date.now();
|
|
2358
|
+
ws.intakeMessage = "This usually takes about 5 minutes.";
|
|
2359
|
+
persistWorkspaceEntry(cfg, ws);
|
|
2360
|
+
const trade =
|
|
2361
|
+
intake && typeof intake === "object"
|
|
2362
|
+
? String(intake.business?.tradeName || intake.slug || "")
|
|
2363
|
+
: "";
|
|
2364
|
+
activity(
|
|
2365
|
+
ws.sandboxId,
|
|
2366
|
+
"info",
|
|
2367
|
+
`Intake received${trade ? ` (${trade})` : ""} — generating website`
|
|
2368
|
+
);
|
|
2369
|
+
log(`intake submit ${shortId(ws.sandboxId)} :${port}`, {
|
|
2370
|
+
trade: trade || null,
|
|
2371
|
+
pages: Array.isArray(intake?.pages) ? intake.pages.length : 0,
|
|
2372
|
+
});
|
|
2373
|
+
void generateSiteForWorkspace(ws, cfg, intake);
|
|
2374
|
+
},
|
|
2375
|
+
});
|
|
2376
|
+
intakeServers.set(ws.sandboxId, instance);
|
|
2377
|
+
if (!ws.intakeStatus) ws.intakeStatus = "form";
|
|
2378
|
+
persistWorkspaceEntry(cfg, ws);
|
|
2379
|
+
activity(ws.sandboxId, "info", `Intake form listening on port ${port}`);
|
|
2380
|
+
log(`intake listening ${shortId(ws.sandboxId)} :${port} status=${ws.intakeStatus || "form"}`);
|
|
2381
|
+
return instance;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
async function generateSiteForWorkspace(ws, cfg, intake) {
|
|
2385
|
+
const folder = path.resolve(ws.folderPath || "");
|
|
2386
|
+
const sitePort = Number(ws.sitePort);
|
|
2387
|
+
const label = ws.sandboxName || shortId(ws.sandboxId);
|
|
2388
|
+
const startedAt = Date.now();
|
|
2389
|
+
log(`generate site start ${label}`, { folder, sitePort });
|
|
2390
|
+
try {
|
|
2391
|
+
const cli = await loadAiCli();
|
|
2392
|
+
if (typeof cli.generateWebsiteFromIntake !== "function") {
|
|
2393
|
+
throw new Error(
|
|
2394
|
+
"ai-cli is missing generateWebsiteFromIntake — update @maintainer-pro/ai-cli"
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2397
|
+
activity(
|
|
2398
|
+
ws.sandboxId,
|
|
2399
|
+
"info",
|
|
2400
|
+
`Generating website for ${label} with the coding agent`
|
|
2401
|
+
);
|
|
2402
|
+
log(`generate site calling coding agent ${label} in ${folder}`);
|
|
2403
|
+
const generated = await cli.generateWebsiteFromIntake({
|
|
2404
|
+
workspaceDir: folder,
|
|
2405
|
+
intake,
|
|
2406
|
+
sitePort,
|
|
2407
|
+
appName: ws.applicationName || ws.sandboxName,
|
|
2408
|
+
memorySource: {
|
|
2409
|
+
baseUrl: cfg.adminUrl,
|
|
2410
|
+
token: cfg.token,
|
|
2411
|
+
sandboxId: ws.sandboxId,
|
|
2412
|
+
},
|
|
2413
|
+
});
|
|
2414
|
+
log(`generate site files ready ${label}`, {
|
|
2415
|
+
provider: generated?.provider || null,
|
|
2416
|
+
kind: detectProjectKind(folder),
|
|
2417
|
+
ms: Date.now() - startedAt,
|
|
2418
|
+
});
|
|
2419
|
+
markIntakeBuilt(ws);
|
|
2420
|
+
ws.intakeStatus = "ready";
|
|
2421
|
+
ws.intakeStartedAt = null;
|
|
2422
|
+
ws.intakeMessage = "Starting the preview…";
|
|
2423
|
+
ws.clientKind = detectProjectKind(folder);
|
|
2424
|
+
persistWorkspaceEntry(cfg, ws);
|
|
2425
|
+
activity(ws.sandboxId, "info", "Website files created — starting preview");
|
|
2426
|
+
|
|
2427
|
+
await stopIntakeForWorkspace(ws.sandboxId);
|
|
2428
|
+
await sleep(400);
|
|
2429
|
+
if (await portIsLive(sitePort, 600)) {
|
|
2430
|
+
await killPort(sitePort);
|
|
2431
|
+
await sleep(400);
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
const detected = await resolveWorkspaceHostApps(ws, {
|
|
2435
|
+
cfg,
|
|
2436
|
+
force: true,
|
|
2437
|
+
allowAi: false,
|
|
2438
|
+
ignoreDesired: true,
|
|
2439
|
+
});
|
|
2440
|
+
const apps = listedHostApps(detected.apps || ws.hostApps || []);
|
|
2441
|
+
const ui =
|
|
2442
|
+
apps.find((app) => app.host || app.role === "ui" || app.role === "app") || {
|
|
2443
|
+
id: "ui",
|
|
2444
|
+
name: ws.applicationName || "Website",
|
|
2445
|
+
role: "ui",
|
|
2446
|
+
source: "package",
|
|
2447
|
+
};
|
|
2448
|
+
ws.hostApps = listedHostApps([
|
|
2449
|
+
{
|
|
2450
|
+
...ui,
|
|
2451
|
+
id: ui.id || "ui",
|
|
2452
|
+
role: ui.role === "backend" ? "ui" : ui.role || "ui",
|
|
2453
|
+
port: sitePort,
|
|
2454
|
+
startCommand: ui.startCommand || "npm run dev",
|
|
2455
|
+
source: ui.source || "package",
|
|
2456
|
+
host: true,
|
|
2457
|
+
folderPath: folder,
|
|
2458
|
+
},
|
|
2459
|
+
...apps.filter(
|
|
2460
|
+
(app) => app.id !== (ui.id || "ui") && app.role === "backend"
|
|
2461
|
+
),
|
|
2462
|
+
]);
|
|
2463
|
+
ws.appsRequested = true;
|
|
2464
|
+
persistWorkspaceEntry(cfg, ws);
|
|
2465
|
+
await startAppsForWorkspace(ws, cfg);
|
|
2466
|
+
activity(ws.sandboxId, "info", `Website preview on port ${sitePort}`);
|
|
2467
|
+
log(`generate site done ${label} ${Date.now() - startedAt}ms :${sitePort}`);
|
|
2468
|
+
} catch (err) {
|
|
2469
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2470
|
+
warn(`generate site failed ${label} ${Date.now() - startedAt}ms: ${message}`);
|
|
2471
|
+
ws.intakeStatus = "error";
|
|
2472
|
+
ws.intakeStartedAt = null;
|
|
2473
|
+
ws.intakeMessage = message.slice(0, 400);
|
|
2474
|
+
persistWorkspaceEntry(cfg, ws);
|
|
2475
|
+
activity(ws.sandboxId, "error", `Website generation failed: ${message}`);
|
|
2476
|
+
ws.intakeStatus = "form";
|
|
2477
|
+
persistWorkspaceEntry(cfg, ws);
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2185
2481
|
function collectOfferedFolders() {
|
|
2186
2482
|
return [browseRootFromCwd()];
|
|
2187
2483
|
}
|
|
@@ -2383,9 +2679,20 @@ function shareProxyContext(msg, ws, appId) {
|
|
|
2383
2679
|
port: localPortForProxy(ws, appId),
|
|
2384
2680
|
portUrls: { ...fromWs, ...fromMsg },
|
|
2385
2681
|
bypassCors: msg?.bypassCors === true,
|
|
2682
|
+
injectChat: !isIntakeShareWorkspace(ws),
|
|
2386
2683
|
};
|
|
2387
2684
|
}
|
|
2388
2685
|
|
|
2686
|
+
function isIntakeShareWorkspace(ws) {
|
|
2687
|
+
const status = String(ws?.intakeStatus || "");
|
|
2688
|
+
return (
|
|
2689
|
+
status === "form" ||
|
|
2690
|
+
status === "building" ||
|
|
2691
|
+
status === "error" ||
|
|
2692
|
+
isIntakePendingWorkspace(ws)
|
|
2693
|
+
);
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2389
2696
|
function sendProcessedProxyHttp(id, ctx, status, headers, body) {
|
|
2390
2697
|
const processed = processShareHttpResponse({
|
|
2391
2698
|
...ctx,
|
|
@@ -2393,6 +2700,15 @@ function sendProcessedProxyHttp(id, ctx, status, headers, body) {
|
|
|
2393
2700
|
headers,
|
|
2394
2701
|
body,
|
|
2395
2702
|
});
|
|
2703
|
+
const pathOnly = String(ctx.path || "").split("?")[0] || "/";
|
|
2704
|
+
if (processed.status >= 400 || pathOnly.includes("/__mp/")) {
|
|
2705
|
+
log(`proxy ${processed.status} ${pathOnly} :${ctx.port || 0}`, {
|
|
2706
|
+
slug: ctx.slug,
|
|
2707
|
+
bytes: processed.body?.length || 0,
|
|
2708
|
+
});
|
|
2709
|
+
} else {
|
|
2710
|
+
dbg(`proxy ${processed.status} ${pathOnly} :${ctx.port || 0}`);
|
|
2711
|
+
}
|
|
2396
2712
|
replyProxyHttp(id, processed.status, processed.headers, processed.body);
|
|
2397
2713
|
}
|
|
2398
2714
|
|
|
@@ -2807,6 +3123,7 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2807
3123
|
if (!id) return;
|
|
2808
3124
|
const ws = workspaceForProxy(sandboxId);
|
|
2809
3125
|
if (!ws) {
|
|
3126
|
+
warn(`proxy miss sandbox=${shortId(sandboxId)} app=${appId} id=${shortId(id)}`);
|
|
2810
3127
|
bridgeSend({
|
|
2811
3128
|
type: "proxy.http.error",
|
|
2812
3129
|
id,
|
|
@@ -2829,6 +3146,9 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2829
3146
|
}
|
|
2830
3147
|
const port = localPortForProxy(ws, appId);
|
|
2831
3148
|
if (!port) {
|
|
3149
|
+
warn(
|
|
3150
|
+
`proxy no port sandbox=${shortId(sandboxId)} app=${appId} ${String(msg.method || "GET")} ${safeProxyPath(msg.path)}`
|
|
3151
|
+
);
|
|
2832
3152
|
bridgeSend({
|
|
2833
3153
|
type: "proxy.http.error",
|
|
2834
3154
|
id,
|
|
@@ -2840,6 +3160,21 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2840
3160
|
const path = safeProxyPath(msg.path);
|
|
2841
3161
|
const ctx = shareProxyContext(msg, ws, appId);
|
|
2842
3162
|
ctx.port = port;
|
|
3163
|
+
const pathOnly = String(path || "").split("?")[0] || "/";
|
|
3164
|
+
const notableProxy =
|
|
3165
|
+
method !== "GET" && method !== "HEAD" ||
|
|
3166
|
+
pathOnly.includes("/__mp/") ||
|
|
3167
|
+
pathOnly === "/" ||
|
|
3168
|
+
pathOnly === "";
|
|
3169
|
+
if (notableProxy) {
|
|
3170
|
+
log(`proxy ${method} ${pathOnly} → :${port} ${appId}`, {
|
|
3171
|
+
sandbox: shortId(sandboxId),
|
|
3172
|
+
slug: ctx.slug,
|
|
3173
|
+
injectChat: ctx.injectChat !== false,
|
|
3174
|
+
});
|
|
3175
|
+
} else {
|
|
3176
|
+
dbg(`proxy ${method} ${pathOnly} → :${port} ${appId}`);
|
|
3177
|
+
}
|
|
2843
3178
|
if (replyShareInterceptor(id, ctx, path)) return;
|
|
2844
3179
|
const prepared = prepareShareHttpRequest(
|
|
2845
3180
|
proxyReqHeaders(msg.headers),
|
|
@@ -3833,6 +4168,9 @@ async function startAiServerForWorkspace(ws, opts = {}) {
|
|
|
3833
4168
|
logger: createLogger(`ai-server:${shortId(ws.sandboxId)}`),
|
|
3834
4169
|
label: `ai-server:${shortId(ws.sandboxId)}`,
|
|
3835
4170
|
listen: true,
|
|
4171
|
+
onTurnComplete: ({ userMessage }) => {
|
|
4172
|
+
noteIntakeWebsiteFeedback(ws, userMessage);
|
|
4173
|
+
},
|
|
3836
4174
|
});
|
|
3837
4175
|
embeddedChat.set(ws.sandboxId, instance);
|
|
3838
4176
|
ws.port = instance.port;
|
|
@@ -3921,6 +4259,7 @@ async function stopWorkspaceApps(ws) {
|
|
|
3921
4259
|
}
|
|
3922
4260
|
}
|
|
3923
4261
|
log(`stopping local apps on ports ${[...ports].join(", ") || "(none)"}`);
|
|
4262
|
+
await stopIntakeForWorkspace(ws.sandboxId);
|
|
3924
4263
|
await stopEmbeddedChat(ws.sandboxId);
|
|
3925
4264
|
await closeRememberedTerminals(ws.sandboxId);
|
|
3926
4265
|
for (const port of ports) {
|
|
@@ -3950,8 +4289,11 @@ async function waitUntilReachable(url, timeoutMs, label, onWait) {
|
|
|
3950
4289
|
function reservedPortsFor(cfg, sandboxId) {
|
|
3951
4290
|
const reserved = new Set();
|
|
3952
4291
|
for (const other of cfg.workspaces || []) {
|
|
3953
|
-
if (other.sandboxId
|
|
3954
|
-
|
|
4292
|
+
if (other.sandboxId === sandboxId) continue;
|
|
4293
|
+
if (other.port) reserved.add(Number(other.port));
|
|
4294
|
+
if (other.sitePort) reserved.add(Number(other.sitePort));
|
|
4295
|
+
for (const app of listedHostApps(other.hostApps)) {
|
|
4296
|
+
if (app.port) reserved.add(Number(app.port));
|
|
3955
4297
|
}
|
|
3956
4298
|
}
|
|
3957
4299
|
return reserved;
|
|
@@ -4025,6 +4367,42 @@ async function startAppsForWorkspace(ws, cfg, opts = {}) {
|
|
|
4025
4367
|
|
|
4026
4368
|
const chatEnv = chatLaunchEnv(ws, cfg);
|
|
4027
4369
|
|
|
4370
|
+
if (isIntakePendingWorkspace(ws)) {
|
|
4371
|
+
await startIntakeForWorkspace(ws, cfg);
|
|
4372
|
+
if (!cfg.noAiServer) {
|
|
4373
|
+
if (status.probe.chatUp) {
|
|
4374
|
+
ws.port = status.probe.chatPort;
|
|
4375
|
+
} else {
|
|
4376
|
+
await startAiServerForWorkspace(ws, {
|
|
4377
|
+
reserved,
|
|
4378
|
+
cfg,
|
|
4379
|
+
port: plan.aiPort,
|
|
4380
|
+
env: chatEnv,
|
|
4381
|
+
});
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
status = await reconcileWorkspacePresence(ws, cfg, { timeoutMs: 1200 });
|
|
4385
|
+
return {
|
|
4386
|
+
up: status.aiServerUp,
|
|
4387
|
+
startedHosts: (await portIsLive(ws.sitePort, 800)) ? ["ui"] : [],
|
|
4388
|
+
sandboxId: ws.sandboxId,
|
|
4389
|
+
folderPath: ws.folderPath,
|
|
4390
|
+
port: ws.port,
|
|
4391
|
+
appUrl: status.host.appUrl || ws.appUrl,
|
|
4392
|
+
origins: status.host.origins,
|
|
4393
|
+
publicUrl:
|
|
4394
|
+
proxyUrlForApp(
|
|
4395
|
+
ws,
|
|
4396
|
+
hostAppOf(ws) || { id: "ui", role: "ui", host: true }
|
|
4397
|
+
) || null,
|
|
4398
|
+
hostApps: status.probe.hostApps || ws.hostApps || [],
|
|
4399
|
+
processIssues: issuesForSandbox(ws.sandboxId).map(
|
|
4400
|
+
({ role: _role, ...issue }) => issue
|
|
4401
|
+
),
|
|
4402
|
+
warning: null,
|
|
4403
|
+
};
|
|
4404
|
+
}
|
|
4405
|
+
|
|
4028
4406
|
if (!cfg.noAiServer) {
|
|
4029
4407
|
if (status.probe.chatUp) {
|
|
4030
4408
|
log(`chat already running on ${status.probe.chatPort} — not restarting`);
|
|
@@ -4454,12 +4832,7 @@ async function setupWorkspace(cfg, action) {
|
|
|
4454
4832
|
);
|
|
4455
4833
|
log(`setup begin sandbox=${shortId(sandboxId)} folder=${folderPath || "(none)"}`);
|
|
4456
4834
|
const requestedPort = Number(action.payload?.port) || 3100;
|
|
4457
|
-
const reserved =
|
|
4458
|
-
for (const other of cfg.workspaces || []) {
|
|
4459
|
-
if (other.sandboxId !== sandboxId && other.port) {
|
|
4460
|
-
reserved.add(Number(other.port));
|
|
4461
|
-
}
|
|
4462
|
-
}
|
|
4835
|
+
const reserved = reservedPortsFor(cfg, sandboxId);
|
|
4463
4836
|
let port = requestedPort;
|
|
4464
4837
|
if (await isChatServerOnPort(requestedPort)) {
|
|
4465
4838
|
reserved.add(requestedPort);
|
|
@@ -4578,12 +4951,27 @@ async function setupWorkspace(cfg, action) {
|
|
|
4578
4951
|
);
|
|
4579
4952
|
}
|
|
4580
4953
|
|
|
4954
|
+
let detectedKind = detectProjectKind(resolved);
|
|
4955
|
+
if (clientMode === "empty") detectedKind = "empty";
|
|
4956
|
+
log(`setup detect sandbox=${shortId(sandboxId)} kind=${detectedKind} mode=${clientMode}`);
|
|
4957
|
+
let sitePort = 0;
|
|
4958
|
+
if (
|
|
4959
|
+
(detectedKind === "empty" || clientMode === "empty") &&
|
|
4960
|
+
clientMode !== "skip" &&
|
|
4961
|
+
clientMode !== "existing"
|
|
4962
|
+
) {
|
|
4963
|
+
const preferredSite = Number(existingWs?.sitePort) || 3000;
|
|
4964
|
+
sitePort = await findFreePort(preferredSite, reserved);
|
|
4965
|
+
log(`setup site port ${sitePort} reserved for intake / Next.js`);
|
|
4966
|
+
}
|
|
4967
|
+
|
|
4581
4968
|
const client = configureClient({
|
|
4582
4969
|
dir: resolved,
|
|
4583
4970
|
port,
|
|
4584
4971
|
appName,
|
|
4585
4972
|
mode: clientMode,
|
|
4586
4973
|
hostAppUrl,
|
|
4974
|
+
sitePort,
|
|
4587
4975
|
});
|
|
4588
4976
|
|
|
4589
4977
|
const aiOrigin = `http://localhost:${port}`;
|
|
@@ -4602,10 +4990,14 @@ async function setupWorkspace(cfg, action) {
|
|
|
4602
4990
|
sandboxName: config.sandbox?.name,
|
|
4603
4991
|
applicationName: config.sandbox?.applicationName,
|
|
4604
4992
|
clientKind: client.kind,
|
|
4605
|
-
appUrl,
|
|
4606
|
-
sameOrigin: Boolean(client.sameOrigin),
|
|
4607
|
-
appsRequested: existingWs?.appsRequested ||
|
|
4608
|
-
|
|
4993
|
+
appUrl: sitePort ? `http://localhost:${sitePort}` : appUrl,
|
|
4994
|
+
sameOrigin: sitePort ? false : Boolean(client.sameOrigin),
|
|
4995
|
+
appsRequested: existingWs?.appsRequested || Boolean(sitePort),
|
|
4996
|
+
sitePort: sitePort || existingWs?.sitePort || null,
|
|
4997
|
+
intakeStatus: sitePort ? "form" : existingWs?.intakeStatus || null,
|
|
4998
|
+
hostApps: sitePort
|
|
4999
|
+
? [intakeHostApp(appName, sitePort, resolved)]
|
|
5000
|
+
: existingWs?.hostApps,
|
|
4609
5001
|
store: {
|
|
4610
5002
|
serverKey: String(config.env?.MAINTAINER_PRO_API_KEY || "").trim(),
|
|
4611
5003
|
clientKey: String(
|
|
@@ -4619,13 +5011,21 @@ async function setupWorkspace(cfg, action) {
|
|
|
4619
5011
|
else cfg.workspaces.push(entry);
|
|
4620
5012
|
saveConfig(cfg);
|
|
4621
5013
|
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
5014
|
+
let ports = { apps: entry.hostApps || [], confused: false, reasons: [] };
|
|
5015
|
+
if (!sitePort) {
|
|
5016
|
+
log(`setup existing app ${shortId(sandboxId)} kind=${client.kind}`);
|
|
5017
|
+
ports = await resolveWorkspaceHostApps(entry, {
|
|
5018
|
+
cfg,
|
|
5019
|
+
allowAi: false,
|
|
5020
|
+
});
|
|
5021
|
+
} else {
|
|
5022
|
+
log(`setup empty folder ${shortId(sandboxId)} — starting intake on :${sitePort}`);
|
|
5023
|
+
await startIntakeForWorkspace(entry, cfg);
|
|
5024
|
+
}
|
|
4626
5025
|
const needsReview =
|
|
4627
|
-
|
|
4628
|
-
|
|
5026
|
+
!sitePort &&
|
|
5027
|
+
(Boolean(ports.confused) ||
|
|
5028
|
+
!(ports.apps || []).some((app) => app && app.role !== "ai-server"));
|
|
4629
5029
|
if (needsReview) {
|
|
4630
5030
|
activity(
|
|
4631
5031
|
sandboxId,
|
|
@@ -4636,13 +5036,16 @@ async function setupWorkspace(cfg, action) {
|
|
|
4636
5036
|
// Do not auto-repair the repo on attach — interactive Review setup handles suggestions.
|
|
4637
5037
|
const projectInfo = entry.projectInfo || null;
|
|
4638
5038
|
|
|
4639
|
-
await inspectHostJobs(entry);
|
|
5039
|
+
if (!sitePort) await inspectHostJobs(entry);
|
|
4640
5040
|
|
|
4641
|
-
const
|
|
4642
|
-
|
|
4643
|
-
|
|
5041
|
+
const intakeUp = sitePort ? await portIsLive(sitePort, 1200) : false;
|
|
5042
|
+
const openUrl = sitePort
|
|
5043
|
+
? `http://localhost:${sitePort}`
|
|
5044
|
+
: client.sameOrigin
|
|
5045
|
+
? `http://localhost:${entry.port}`
|
|
5046
|
+
: entry.appUrl || appUrl;
|
|
4644
5047
|
const aiServerUp = await isChatServerOnPort(entry.port);
|
|
4645
|
-
if (aiServerUp) {
|
|
5048
|
+
if (aiServerUp || intakeUp) {
|
|
4646
5049
|
clearProcessProblem(sandboxId, "ai_server_launch", "ai");
|
|
4647
5050
|
clearProcessProblem(sandboxId, "apps_not_started");
|
|
4648
5051
|
}
|
|
@@ -4650,17 +5053,21 @@ async function setupWorkspace(cfg, action) {
|
|
|
4650
5053
|
const processIssues = issuesForSandbox(sandboxId).map(
|
|
4651
5054
|
({ role: _role, ...issue }) => issue
|
|
4652
5055
|
);
|
|
4653
|
-
const waitingForStart = !aiServerUp;
|
|
5056
|
+
const waitingForStart = sitePort ? !intakeUp : !aiServerUp;
|
|
4654
5057
|
const warning = waitingForStart
|
|
4655
5058
|
? needsReview
|
|
4656
5059
|
? "Folder attached. Review the suggested apps and ports in Maintainer Pro, then Start Apps."
|
|
4657
|
-
:
|
|
5060
|
+
: sitePort
|
|
5061
|
+
? "Folder attached. The intake form should be on the share link."
|
|
5062
|
+
: "Folder is attached in Maintainer Pro. Review apps if needed, then use Start Apps."
|
|
4658
5063
|
: processIssues[0]?.message || null;
|
|
4659
5064
|
|
|
4660
5065
|
for (const note of client.notes) log(`setup note ${note}`);
|
|
4661
5066
|
log(
|
|
4662
|
-
|
|
4663
|
-
? `setup done —
|
|
5067
|
+
sitePort
|
|
5068
|
+
? `setup done — intake on ${openUrl} kind=${client.kind}`
|
|
5069
|
+
: waitingForStart
|
|
5070
|
+
? `setup done — waiting for Start (${openUrl}) kind=${client.kind}`
|
|
4664
5071
|
: `setup done — chat already up (${openUrl}) kind=${client.kind}`
|
|
4665
5072
|
);
|
|
4666
5073
|
|
|
@@ -4681,7 +5088,7 @@ async function setupWorkspace(cfg, action) {
|
|
|
4681
5088
|
clientFiles: client.filesWritten,
|
|
4682
5089
|
clientNotes: client.notes,
|
|
4683
5090
|
aiServerUp,
|
|
4684
|
-
startedHosts: [],
|
|
5091
|
+
startedHosts: intakeUp ? ["ui"] : [],
|
|
4685
5092
|
openUrl,
|
|
4686
5093
|
processIssues,
|
|
4687
5094
|
warning,
|
|
@@ -4788,7 +5195,9 @@ async function runActions(cfg, actions) {
|
|
|
4788
5195
|
action.code = canonicalActionCode(action.code);
|
|
4789
5196
|
const startedAt = Date.now();
|
|
4790
5197
|
const label = actionLabel(action);
|
|
4791
|
-
log(`${label} start
|
|
5198
|
+
log(`${label} start`, {
|
|
5199
|
+
payload: sanitizeLogValue(action.payload || {}),
|
|
5200
|
+
});
|
|
4792
5201
|
let ok = true;
|
|
4793
5202
|
/** @type {Record<string, unknown>} */
|
|
4794
5203
|
let result = {};
|
|
@@ -4865,6 +5274,7 @@ async function runActions(cfg, actions) {
|
|
|
4865
5274
|
hostApps: hostApps?.apps || [],
|
|
4866
5275
|
projectInfo: ws?.projectInfo || null,
|
|
4867
5276
|
};
|
|
5277
|
+
log(`${label} recheck cli=${folderProbe.cliProvider || "none"} missing=${folderProbe.missingCli === true}`);
|
|
4868
5278
|
} else if (action.code === "redetect_ports" || action.code === "propose_setup") {
|
|
4869
5279
|
const sandboxId = action.sandboxId || action.payload?.sandboxId;
|
|
4870
5280
|
const ws = (cfg.workspaces || []).find((w) => w.sandboxId === sandboxId);
|
|
@@ -5138,7 +5548,8 @@ async function runActions(cfg, actions) {
|
|
|
5138
5548
|
log(
|
|
5139
5549
|
`${label} ${ok ? "ok" : "failed"} ${elapsed}ms${
|
|
5140
5550
|
summary ? ` ${summary}` : ""
|
|
5141
|
-
}
|
|
5551
|
+
}`,
|
|
5552
|
+
{ result: sanitizeLogValue(result) }
|
|
5142
5553
|
);
|
|
5143
5554
|
|
|
5144
5555
|
try {
|
|
@@ -5521,6 +5932,12 @@ async function main() {
|
|
|
5521
5932
|
);
|
|
5522
5933
|
await warnIfMissingCli();
|
|
5523
5934
|
await restoreHostsAfterReconnect(cfg);
|
|
5935
|
+
const websiteFeedbackTimer = setInterval(() => {
|
|
5936
|
+
void flushAllIntakeWebsiteFeedback();
|
|
5937
|
+
}, WEBSITE_FEEDBACK_CHECK_MS);
|
|
5938
|
+
if (typeof websiteFeedbackTimer.unref === "function") {
|
|
5939
|
+
websiteFeedbackTimer.unref();
|
|
5940
|
+
}
|
|
5524
5941
|
|
|
5525
5942
|
/** @type {unknown[]} */
|
|
5526
5943
|
const claimedActions = [];
|
|
@@ -5536,15 +5953,26 @@ async function main() {
|
|
|
5536
5953
|
}
|
|
5537
5954
|
await sendPresenceOverWs();
|
|
5538
5955
|
} catch (err) {
|
|
5539
|
-
warn(err instanceof Error ? err.message : String(err));
|
|
5956
|
+
warn(`action runner failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
5540
5957
|
} finally {
|
|
5541
5958
|
workBusy = false;
|
|
5542
|
-
if (claimedActions.length)
|
|
5959
|
+
if (claimedActions.length) {
|
|
5960
|
+
log(`action runner still has ${claimedActions.length} queued`);
|
|
5961
|
+
void runWork();
|
|
5962
|
+
}
|
|
5543
5963
|
}
|
|
5544
5964
|
};
|
|
5545
5965
|
|
|
5546
5966
|
const queueActions = (actions) => {
|
|
5547
5967
|
if (!Array.isArray(actions) || actions.length === 0) return;
|
|
5968
|
+
log(
|
|
5969
|
+
`queue ${actions.length} action(s): ${actions.map((row) => row.code).join(", ")}`,
|
|
5970
|
+
{
|
|
5971
|
+
ids: actions.map((row) => shortId(row.id)),
|
|
5972
|
+
busy: workBusy,
|
|
5973
|
+
waiting: claimedActions.length,
|
|
5974
|
+
}
|
|
5975
|
+
);
|
|
5548
5976
|
claimedActions.push(...actions);
|
|
5549
5977
|
void runWork();
|
|
5550
5978
|
};
|
|
@@ -5750,10 +6178,14 @@ async function main() {
|
|
|
5750
6178
|
if (!msg || typeof msg !== "object") return;
|
|
5751
6179
|
if (msg.type === "pong" || msg.type === "hello") return;
|
|
5752
6180
|
if (msg.type === "actions") {
|
|
6181
|
+
log(`ws inbound actions (${Array.isArray(msg.actions) ? msg.actions.length : 0})`);
|
|
5753
6182
|
queueActions(msg.actions);
|
|
5754
6183
|
return;
|
|
5755
6184
|
}
|
|
5756
6185
|
if (msg.type === "heartbeat.ok") {
|
|
6186
|
+
const remotes = Array.isArray(msg.workspaces) ? msg.workspaces.length : 0;
|
|
6187
|
+
const incoming = Array.isArray(msg.actions) ? msg.actions.length : 0;
|
|
6188
|
+
dbg(`ws inbound heartbeat.ok workspaces=${remotes} actions=${incoming}`);
|
|
5757
6189
|
syncAssignedWorkspaces(cfg, msg.workspaces);
|
|
5758
6190
|
queueActions(msg.actions);
|
|
5759
6191
|
return;
|
|
@@ -5779,12 +6211,17 @@ async function main() {
|
|
|
5779
6211
|
return;
|
|
5780
6212
|
}
|
|
5781
6213
|
if (msg.type === "chat.run") {
|
|
6214
|
+
log(
|
|
6215
|
+
`ws inbound chat.run sandbox=${shortId(msg.sandboxId)} conv=${shortId(msg.conversationId)}`
|
|
6216
|
+
);
|
|
5782
6217
|
void handleChatRun(msg);
|
|
5783
6218
|
return;
|
|
5784
6219
|
}
|
|
5785
6220
|
if (msg.type === "error") {
|
|
5786
6221
|
warn(`ws: ${msg.error || "error"}`);
|
|
6222
|
+
return;
|
|
5787
6223
|
}
|
|
6224
|
+
dbg(`ws inbound ${msg.type}`);
|
|
5788
6225
|
};
|
|
5789
6226
|
|
|
5790
6227
|
const socketState = () => {
|
|
@@ -5942,6 +6379,7 @@ async function main() {
|
|
|
5942
6379
|
clearHeartbeatTimer();
|
|
5943
6380
|
clearPingTimer();
|
|
5944
6381
|
clearReconnectTimer();
|
|
6382
|
+
clearInterval(websiteFeedbackTimer);
|
|
5945
6383
|
if (watchdogTimer) {
|
|
5946
6384
|
clearInterval(watchdogTimer);
|
|
5947
6385
|
watchdogTimer = null;
|
|
@@ -5949,7 +6387,11 @@ async function main() {
|
|
|
5949
6387
|
dropSocket(socket);
|
|
5950
6388
|
socket = null;
|
|
5951
6389
|
log("shutting down (other terminals stay open)");
|
|
5952
|
-
void
|
|
6390
|
+
void flushAllIntakeWebsiteFeedback({ force: true })
|
|
6391
|
+
.catch(() => {})
|
|
6392
|
+
.finally(() =>
|
|
6393
|
+
stopAllEmbeddedChat().finally(() => process.exit(0))
|
|
6394
|
+
);
|
|
5953
6395
|
};
|
|
5954
6396
|
process.on("SIGINT", shutdown);
|
|
5955
6397
|
process.on("SIGTERM", shutdown);
|