@prismer/runtime 2.0.4 → 2.0.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/cli.cjs +2123 -267
- package/dist/cli.js +2004 -148
- package/dist/index.cjs +2164 -306
- package/dist/index.d.cts +543 -182
- package/dist/index.d.ts +543 -182
- package/dist/index.js +2014 -156
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -166,6 +166,42 @@ var init_skill_loader2 = __esm({
|
|
|
166
166
|
});
|
|
167
167
|
|
|
168
168
|
// src/adapters/hermes/index.ts
|
|
169
|
+
function readPlainRecord(value) {
|
|
170
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
171
|
+
}
|
|
172
|
+
function readNonEmptyString(value) {
|
|
173
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
174
|
+
}
|
|
175
|
+
function sanitizeRoleSkillSlug(value) {
|
|
176
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "template";
|
|
177
|
+
}
|
|
178
|
+
function renderRoleTemplateSkill(skillName, agents, roleTemplate) {
|
|
179
|
+
const mcpServers = Array.isArray(roleTemplate?.mcpServers) ? roleTemplate.mcpServers : [];
|
|
180
|
+
const tools = /* @__PURE__ */ new Set();
|
|
181
|
+
for (const server of mcpServers) {
|
|
182
|
+
const rec = readPlainRecord(server);
|
|
183
|
+
const allowlist = Array.isArray(rec?.toolsAllowlist) ? rec.toolsAllowlist : [];
|
|
184
|
+
for (const tool of allowlist) {
|
|
185
|
+
if (typeof tool === "string" && tool.trim()) tools.add(tool.trim());
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const allowedTools = [...tools].join(" ");
|
|
189
|
+
const roleTemplateSlug = JSON.stringify(readNonEmptyString(roleTemplate?.slug) ?? null);
|
|
190
|
+
return `---
|
|
191
|
+
name: ${skillName}
|
|
192
|
+
description: Role-template operating playbook projected from Prismer RoleTemplate.
|
|
193
|
+
${allowedTools ? `allowed-tools: ${allowedTools}
|
|
194
|
+
` : ""}metadata:
|
|
195
|
+
prismer:
|
|
196
|
+
source: role-template
|
|
197
|
+
roleTemplateSlug: ${roleTemplateSlug}
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
# Role Template Playbook
|
|
201
|
+
|
|
202
|
+
${agents.trim()}
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
169
205
|
function parsePrismerGoals(value) {
|
|
170
206
|
if (!Array.isArray(value)) return [];
|
|
171
207
|
return value.map((entry) => {
|
|
@@ -575,13 +611,14 @@ function failure(status, body) {
|
|
|
575
611
|
error: { code: "adapter_dispatch_failed", message: `Hermes ${status}: ${body || "<no body>"}` }
|
|
576
612
|
};
|
|
577
613
|
}
|
|
578
|
-
async function consumeSse(body, task) {
|
|
614
|
+
async function consumeSse(body, task, state) {
|
|
579
615
|
const reader = body.getReader();
|
|
580
616
|
const decoder = new TextDecoder();
|
|
581
617
|
let buf = "";
|
|
582
618
|
let deltas = "";
|
|
583
619
|
let finalOutput;
|
|
584
620
|
let lastProgress = 0;
|
|
621
|
+
let approvalRequested = false;
|
|
585
622
|
for (; ; ) {
|
|
586
623
|
const readStart = Date.now();
|
|
587
624
|
const { value, done } = await reader.read();
|
|
@@ -630,6 +667,10 @@ async function consumeSse(body, task) {
|
|
|
630
667
|
const next = Math.min(0.99, lastProgress + 0.05);
|
|
631
668
|
lastProgress = next;
|
|
632
669
|
const toolName = typeof payload.tool === "string" ? payload.tool : typeof payload.name === "string" ? payload.name : void 0;
|
|
670
|
+
if (eventName === "tool.started" && toolName === APPROVAL_TOOL_NAME) {
|
|
671
|
+
approvalRequested = true;
|
|
672
|
+
if (state) state.approvalRequested = true;
|
|
673
|
+
}
|
|
633
674
|
const previewStr = typeof payload.preview === "string" ? payload.preview : payload.preview != null ? JSON.stringify(payload.preview).slice(0, 200) : void 0;
|
|
634
675
|
task.onProgress?.({
|
|
635
676
|
progress: next,
|
|
@@ -659,7 +700,53 @@ async function consumeSse(body, task) {
|
|
|
659
700
|
}
|
|
660
701
|
}
|
|
661
702
|
}
|
|
662
|
-
return
|
|
703
|
+
return {
|
|
704
|
+
output: finalOutput && finalOutput.length > 0 ? finalOutput : deltas,
|
|
705
|
+
approvalRequested
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
async function consumeChatCompletionsSse(body, task) {
|
|
709
|
+
const reader = body.getReader();
|
|
710
|
+
const decoder = new TextDecoder();
|
|
711
|
+
let buf = "";
|
|
712
|
+
let output = "";
|
|
713
|
+
let lastProgress = 0;
|
|
714
|
+
for (; ; ) {
|
|
715
|
+
const { value, done } = await reader.read();
|
|
716
|
+
if (done) break;
|
|
717
|
+
buf += decoder.decode(value, { stream: true });
|
|
718
|
+
const events = buf.split("\n\n");
|
|
719
|
+
buf = events.pop() ?? "";
|
|
720
|
+
for (const ev of events) {
|
|
721
|
+
const dataLine = ev.match(/^data: (.+)$/m);
|
|
722
|
+
if (!dataLine) continue;
|
|
723
|
+
const raw = dataLine[1].trim();
|
|
724
|
+
if (raw === "[DONE]") continue;
|
|
725
|
+
let payload;
|
|
726
|
+
try {
|
|
727
|
+
payload = JSON.parse(raw);
|
|
728
|
+
} catch (err) {
|
|
729
|
+
process.stderr.write(
|
|
730
|
+
`[hermes-adapter] chat-completions SSE parse skipped: ${err.message}
|
|
731
|
+
`
|
|
732
|
+
);
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
const delta = payload.choices?.[0]?.delta?.content;
|
|
736
|
+
if (typeof delta === "string") {
|
|
737
|
+
output += delta;
|
|
738
|
+
} else if (Array.isArray(delta)) {
|
|
739
|
+
for (const part of delta) {
|
|
740
|
+
if (part?.type === "text" && typeof part.text === "string") output += part.text;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (output.length > 0 && lastProgress < 0.99) {
|
|
744
|
+
lastProgress = Math.min(0.99, lastProgress + 0.05);
|
|
745
|
+
task.onProgress?.({ progress: lastProgress, message: "streaming" });
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return output;
|
|
663
750
|
}
|
|
664
751
|
async function checkHealth(baseUrl, apiKey) {
|
|
665
752
|
try {
|
|
@@ -740,7 +827,7 @@ function pidAlive(pid) {
|
|
|
740
827
|
function escapeRegExp(value) {
|
|
741
828
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
742
829
|
}
|
|
743
|
-
var import_node_child_process, import_node_fs2, import_node_module, import_node_os, import_node_path2, import_node_url, import_better_sqlite3, YAML, import_zod, import_meta, PRISMER_IM_SKILL_NAME, PRISMER_IM_SKILL_CONTENT, HermesProfileConfigSchema, hermesAdapter, HermesService;
|
|
830
|
+
var import_node_child_process, import_node_fs2, import_node_module, import_node_os, import_node_path2, import_node_url, import_better_sqlite3, YAML, import_zod, import_meta, PRISMER_IM_SKILL_NAME, PRISMER_ROLE_SKILL_PREFIX, PRISMER_IM_SKILL_CONTENT, HermesProfileConfigSchema, hermesAdapter, HermesService, APPROVAL_TOOL_NAME;
|
|
744
831
|
var init_hermes = __esm({
|
|
745
832
|
"src/adapters/hermes/index.ts"() {
|
|
746
833
|
"use strict";
|
|
@@ -757,6 +844,7 @@ var init_hermes = __esm({
|
|
|
757
844
|
init_skill_loader2();
|
|
758
845
|
import_meta = {};
|
|
759
846
|
PRISMER_IM_SKILL_NAME = "prismer-im-collab";
|
|
847
|
+
PRISMER_ROLE_SKILL_PREFIX = "prismer-role";
|
|
760
848
|
PRISMER_IM_SKILL_CONTENT = `---
|
|
761
849
|
name: prismer-im-collab
|
|
762
850
|
description: Coordinate reliably in Prismer conversations, use workspace assets through bounded MCP tools, and keep task work on the board.
|
|
@@ -1069,6 +1157,7 @@ No @ needed; the human is the only other party.
|
|
|
1069
1157
|
const startedAt = Date.now();
|
|
1070
1158
|
let runId;
|
|
1071
1159
|
const sysPrompt = typeof task.metadata?.systemPrompt === "string" ? task.metadata.systemPrompt : void 0;
|
|
1160
|
+
this.installRoleTemplateSkill();
|
|
1072
1161
|
const skillPrompt = await this.skillLoader.loadSystemPromptFragment().catch((err) => {
|
|
1073
1162
|
process.stderr.write(
|
|
1074
1163
|
`[hermes-adapter] failed to read skills for ${this.profileName}: ${err.message}
|
|
@@ -1076,7 +1165,21 @@ No @ needed; the human is the only other party.
|
|
|
1076
1165
|
);
|
|
1077
1166
|
return void 0;
|
|
1078
1167
|
});
|
|
1079
|
-
const
|
|
1168
|
+
const outboxDir = typeof task.metadata?.prismerOutboxDir === "string" ? task.metadata.prismerOutboxDir : void 0;
|
|
1169
|
+
const outboxDirective = outboxDir ? [
|
|
1170
|
+
"[Outbox directive \u2014 MANDATORY, overrides any prior outbox path]",
|
|
1171
|
+
"",
|
|
1172
|
+
"For THIS turn ONLY, your active outbox is:",
|
|
1173
|
+
` ${outboxDir}`,
|
|
1174
|
+
"",
|
|
1175
|
+
"Any user-deliverable file (PDF, image, CSV, archive, doc) MUST be",
|
|
1176
|
+
"written to that EXACT absolute path. Do NOT reuse the outbox path",
|
|
1177
|
+
"from any previous turn \u2014 those directories no longer accept new",
|
|
1178
|
+
"uploads. Do NOT copy files into the previous outbox; copy them",
|
|
1179
|
+
"into the path above. Files outside this directory will not become",
|
|
1180
|
+
'chat attachments and the user will see "no files were attached".'
|
|
1181
|
+
].join("\n") : "";
|
|
1182
|
+
const instructions = [outboxDirective, sysPrompt, skillPrompt].filter(Boolean).join("\n\n");
|
|
1080
1183
|
if (sysPrompt) {
|
|
1081
1184
|
try {
|
|
1082
1185
|
const profileDir = getHermesProfileDir(this.profileName);
|
|
@@ -1100,8 +1203,15 @@ No @ needed; the human is the only other party.
|
|
|
1100
1203
|
`
|
|
1101
1204
|
);
|
|
1102
1205
|
}
|
|
1206
|
+
const sseState = { approvalRequested: false };
|
|
1103
1207
|
try {
|
|
1104
1208
|
const nativeBridgePatch = await this.prepareNativeBridgePatch(task);
|
|
1209
|
+
const imageRefs = (task.assetRefs ?? []).filter(
|
|
1210
|
+
(r) => r.mime?.startsWith("image/")
|
|
1211
|
+
);
|
|
1212
|
+
if (imageRefs.length > 0) {
|
|
1213
|
+
return await this.dispatchMultimodalChat(task, instructions, imageRefs, startedAt);
|
|
1214
|
+
}
|
|
1105
1215
|
const createRes = await fetch(`${this.baseUrl}/v1/runs`, {
|
|
1106
1216
|
method: "POST",
|
|
1107
1217
|
headers: {
|
|
@@ -1152,10 +1262,10 @@ No @ needed; the human is the only other party.
|
|
|
1152
1262
|
}
|
|
1153
1263
|
};
|
|
1154
1264
|
}
|
|
1155
|
-
const
|
|
1265
|
+
const sseResult = await consumeSse(eventsRes.body, task, sseState);
|
|
1156
1266
|
return {
|
|
1157
1267
|
ok: true,
|
|
1158
|
-
output,
|
|
1268
|
+
output: sseResult.output,
|
|
1159
1269
|
metrics: { durationMs: Date.now() - startedAt },
|
|
1160
1270
|
metadata: {
|
|
1161
1271
|
hermes: this.bridgeSnapshot("dispatched", {
|
|
@@ -1163,7 +1273,13 @@ No @ needed; the human is the only other party.
|
|
|
1163
1273
|
runId,
|
|
1164
1274
|
baseUrl: this.baseUrl,
|
|
1165
1275
|
model: this.config.model
|
|
1166
|
-
})
|
|
1276
|
+
}),
|
|
1277
|
+
// Surfaced to dispatch.ts so a subsequent reaper kill on this
|
|
1278
|
+
// task is reclassified as `awaiting_human_approval` rather than
|
|
1279
|
+
// `daemon_task_timeout`. Lives under a stable key (not under
|
|
1280
|
+
// `hermes.*`) so non-hermes adapters can adopt the same signal
|
|
1281
|
+
// later without bleeding hermes-specific metadata shape.
|
|
1282
|
+
...sseResult.approvalRequested ? { approvalRequested: true } : {}
|
|
1167
1283
|
}
|
|
1168
1284
|
};
|
|
1169
1285
|
} catch (err) {
|
|
@@ -1173,6 +1289,7 @@ No @ needed; the human is the only other party.
|
|
|
1173
1289
|
);
|
|
1174
1290
|
const categorized = categorizeDispatchError(err, task.signal);
|
|
1175
1291
|
const bridgeKind = categorized.error?.code === "task_cancelled" ? "cancelled" : "failed";
|
|
1292
|
+
const approvalSignal = sseState.approvalRequested ? { approvalRequested: true } : {};
|
|
1176
1293
|
return {
|
|
1177
1294
|
...categorized,
|
|
1178
1295
|
metadata: {
|
|
@@ -1181,13 +1298,87 @@ No @ needed; the human is the only other party.
|
|
|
1181
1298
|
baseUrl: this.baseUrl,
|
|
1182
1299
|
model: this.config.model,
|
|
1183
1300
|
...bridgeKind === "failed" ? { error: err.message } : {}
|
|
1184
|
-
})
|
|
1301
|
+
}),
|
|
1302
|
+
...approvalSignal
|
|
1185
1303
|
}
|
|
1186
1304
|
};
|
|
1187
1305
|
} finally {
|
|
1188
1306
|
if (runId && this.currentRunId === runId) this.currentRunId = void 0;
|
|
1189
1307
|
}
|
|
1190
1308
|
}
|
|
1309
|
+
/**
|
|
1310
|
+
* 14b rev.3 §3.0.4 — Hermes multimodal path.
|
|
1311
|
+
*
|
|
1312
|
+
* Builds an OpenAI-shape chat/completions body where the user message
|
|
1313
|
+
* `content` is a multipart array (`{type:'text'}` + N × `{type:'image_url'}`).
|
|
1314
|
+
* Hermes `_normalize_multimodal_content` (api_server.py:170) collapses the
|
|
1315
|
+
* blocks into its native multimodal representation before routing into
|
|
1316
|
+
* the agent pipeline.
|
|
1317
|
+
*
|
|
1318
|
+
* `image_url.url` is the cdn URL when the daemon's reachability probe said
|
|
1319
|
+
* the upstream LLM can see it (D35 path A); otherwise it's a `data:` URI
|
|
1320
|
+
* carrying the base64 bytes the daemon pre-fetched (D35 path B fallback).
|
|
1321
|
+
* The adapter doesn't decide — `resolveAssetRefs` does, and the adapter
|
|
1322
|
+
* just shuttles `cdnUrl ?? data:base64` into the wire.
|
|
1323
|
+
*/
|
|
1324
|
+
async dispatchMultimodalChat(task, instructions, imageRefs, startedAt) {
|
|
1325
|
+
const userContent = [{ type: "text", text: task.prompt }];
|
|
1326
|
+
for (const r of imageRefs) {
|
|
1327
|
+
const url = r.reachable === "cdn" && r.cdnUrl ? r.cdnUrl : r.base64 ? `data:${r.mime ?? "image/png"};base64,${r.base64}` : r.cdnUrl ?? "";
|
|
1328
|
+
if (!url) {
|
|
1329
|
+
userContent.push({
|
|
1330
|
+
type: "text",
|
|
1331
|
+
text: `[image attachment id=${r.assetId} mime=${r.mime ?? "unknown"} unavailable: cdn not reachable and bytes exceeded inline cap]`
|
|
1332
|
+
});
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
userContent.push({ type: "image_url", image_url: { url, detail: "auto" } });
|
|
1336
|
+
}
|
|
1337
|
+
const messages = [];
|
|
1338
|
+
if (instructions) messages.push({ role: "system", content: instructions });
|
|
1339
|
+
messages.push({ role: "user", content: userContent });
|
|
1340
|
+
const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
1341
|
+
method: "POST",
|
|
1342
|
+
headers: {
|
|
1343
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
1344
|
+
"Content-Type": "application/json"
|
|
1345
|
+
},
|
|
1346
|
+
body: JSON.stringify({
|
|
1347
|
+
model: this.config.model,
|
|
1348
|
+
messages,
|
|
1349
|
+
stream: true
|
|
1350
|
+
}),
|
|
1351
|
+
signal: task.signal
|
|
1352
|
+
});
|
|
1353
|
+
if (!res.ok) {
|
|
1354
|
+
const body = await res.text().catch(() => "");
|
|
1355
|
+
return failure(res.status, body);
|
|
1356
|
+
}
|
|
1357
|
+
if (!res.body) {
|
|
1358
|
+
return {
|
|
1359
|
+
ok: false,
|
|
1360
|
+
error: {
|
|
1361
|
+
code: "adapter_dispatch_failed",
|
|
1362
|
+
message: "Hermes /v1/chat/completions returned no body"
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
const output = await consumeChatCompletionsSse(res.body, task);
|
|
1367
|
+
return {
|
|
1368
|
+
ok: true,
|
|
1369
|
+
output,
|
|
1370
|
+
metrics: { durationMs: Date.now() - startedAt },
|
|
1371
|
+
metadata: {
|
|
1372
|
+
hermes: this.bridgeSnapshot("dispatched", {
|
|
1373
|
+
endpoint: "/v1/chat/completions",
|
|
1374
|
+
multimodal: true,
|
|
1375
|
+
imageRefs: imageRefs.length,
|
|
1376
|
+
baseUrl: this.baseUrl,
|
|
1377
|
+
model: this.config.model
|
|
1378
|
+
})
|
|
1379
|
+
}
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1191
1382
|
async shutdown() {
|
|
1192
1383
|
if (this.currentRunId) await this.stopRun(this.currentRunId);
|
|
1193
1384
|
}
|
|
@@ -1209,6 +1400,25 @@ No @ needed; the human is the only other party.
|
|
|
1209
1400
|
]);
|
|
1210
1401
|
return { ...kanbanPatch, ...goalsPatch };
|
|
1211
1402
|
}
|
|
1403
|
+
installRoleTemplateSkill() {
|
|
1404
|
+
const roleTemplate = readPlainRecord(this.config.roleTemplate);
|
|
1405
|
+
const hermesConfig = readPlainRecord(roleTemplate?.hermesConfig);
|
|
1406
|
+
const agents = readNonEmptyString(hermesConfig?.agents);
|
|
1407
|
+
if (!agents) return;
|
|
1408
|
+
const slug = sanitizeRoleSkillSlug(readNonEmptyString(roleTemplate?.slug) ?? "template");
|
|
1409
|
+
const skillName = `${PRISMER_ROLE_SKILL_PREFIX}-${slug}`;
|
|
1410
|
+
const content = renderRoleTemplateSkill(skillName, agents, roleTemplate);
|
|
1411
|
+
try {
|
|
1412
|
+
const skillDir = (0, import_node_path2.join)(this.skillLoader.getSkillsRoot(), skillName);
|
|
1413
|
+
(0, import_node_fs2.mkdirSync)(skillDir, { recursive: true });
|
|
1414
|
+
(0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(skillDir, "SKILL.md"), content, "utf8");
|
|
1415
|
+
} catch (err) {
|
|
1416
|
+
process.stderr.write(
|
|
1417
|
+
`[hermes-adapter] failed to install role-template skill ${skillName} for ${this.profileName}: ${err.message}
|
|
1418
|
+
`
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1212
1422
|
async prepareNativeKanbanPatch(task) {
|
|
1213
1423
|
const sourceKind = typeof task.metadata?.sourceKind === "string" ? task.metadata.sourceKind : null;
|
|
1214
1424
|
const parentTaskId = typeof task.metadata?.parentTaskId === "string" ? task.metadata.parentTaskId : null;
|
|
@@ -1317,6 +1527,7 @@ No @ needed; the human is the only other party.
|
|
|
1317
1527
|
};
|
|
1318
1528
|
}
|
|
1319
1529
|
};
|
|
1530
|
+
APPROVAL_TOOL_NAME = "prismer.approval.request_human_approval";
|
|
1320
1531
|
}
|
|
1321
1532
|
});
|
|
1322
1533
|
|
|
@@ -1348,6 +1559,58 @@ var init_skill_loader3 = __esm({
|
|
|
1348
1559
|
});
|
|
1349
1560
|
|
|
1350
1561
|
// src/adapters/openclaw/index.ts
|
|
1562
|
+
function readPlainRecord2(value) {
|
|
1563
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1564
|
+
}
|
|
1565
|
+
function readNonEmptyString2(value) {
|
|
1566
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1567
|
+
}
|
|
1568
|
+
function sanitizeRoleSkillSlug2(value) {
|
|
1569
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "template";
|
|
1570
|
+
}
|
|
1571
|
+
function renderRoleTemplateSkill2(skillName, agents, roleTemplate) {
|
|
1572
|
+
const mcpServers = Array.isArray(roleTemplate?.mcpServers) ? roleTemplate.mcpServers : [];
|
|
1573
|
+
const tools = /* @__PURE__ */ new Set();
|
|
1574
|
+
for (const server of mcpServers) {
|
|
1575
|
+
const rec = readPlainRecord2(server);
|
|
1576
|
+
const allowlist = Array.isArray(rec?.toolsAllowlist) ? rec.toolsAllowlist : [];
|
|
1577
|
+
for (const tool of allowlist) {
|
|
1578
|
+
if (typeof tool === "string" && tool.trim()) tools.add(tool.trim());
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
const allowedTools = [...tools].join(" ");
|
|
1582
|
+
const roleTemplateSlug = JSON.stringify(readNonEmptyString2(roleTemplate?.slug) ?? null);
|
|
1583
|
+
return `---
|
|
1584
|
+
name: ${skillName}
|
|
1585
|
+
description: Role-template operating playbook projected from Prismer RoleTemplate.
|
|
1586
|
+
${allowedTools ? `allowed-tools: ${allowedTools}
|
|
1587
|
+
` : ""}metadata:
|
|
1588
|
+
prismer:
|
|
1589
|
+
source: role-template
|
|
1590
|
+
roleTemplateSlug: ${roleTemplateSlug}
|
|
1591
|
+
---
|
|
1592
|
+
|
|
1593
|
+
# Role Template Playbook
|
|
1594
|
+
|
|
1595
|
+
${agents.trim()}
|
|
1596
|
+
`;
|
|
1597
|
+
}
|
|
1598
|
+
function pickResponsesSource(ref) {
|
|
1599
|
+
if (ref.reachable === "cdn" && ref.cdnUrl) {
|
|
1600
|
+
return { type: "url", url: ref.cdnUrl };
|
|
1601
|
+
}
|
|
1602
|
+
if (ref.base64) {
|
|
1603
|
+
return {
|
|
1604
|
+
type: "base64",
|
|
1605
|
+
data: ref.base64,
|
|
1606
|
+
media_type: ref.mime ?? "application/octet-stream"
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
if (ref.cdnUrl) {
|
|
1610
|
+
return { type: "url", url: ref.cdnUrl };
|
|
1611
|
+
}
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1351
1614
|
function getOpenClawSkillsDir(profile) {
|
|
1352
1615
|
const config = OpenClawProfileConfigSchema.parse(profile.config);
|
|
1353
1616
|
return resolveOpenClawSkillsDir(profile, config);
|
|
@@ -1368,15 +1631,17 @@ async function checkHealth2(baseUrl, apiKey) {
|
|
|
1368
1631
|
return false;
|
|
1369
1632
|
}
|
|
1370
1633
|
}
|
|
1371
|
-
var import_zod2, import_node_os2, import_node_path3, OpenClawProfileConfigSchema, openclawAdapter, OpenClawService;
|
|
1634
|
+
var import_zod2, import_node_fs3, import_node_os2, import_node_path3, PRISMER_ROLE_SKILL_PREFIX2, OpenClawProfileConfigSchema, openclawAdapter, OpenClawService;
|
|
1372
1635
|
var init_openclaw = __esm({
|
|
1373
1636
|
"src/adapters/openclaw/index.ts"() {
|
|
1374
1637
|
"use strict";
|
|
1375
1638
|
import_zod2 = require("zod");
|
|
1639
|
+
import_node_fs3 = require("fs");
|
|
1376
1640
|
import_node_os2 = require("os");
|
|
1377
1641
|
import_node_path3 = require("path");
|
|
1378
1642
|
init_contract();
|
|
1379
1643
|
init_skill_loader3();
|
|
1644
|
+
PRISMER_ROLE_SKILL_PREFIX2 = "prismer-role";
|
|
1380
1645
|
OpenClawProfileConfigSchema = import_zod2.z.object({
|
|
1381
1646
|
/** OpenClaw chat-completions HTTP port. */
|
|
1382
1647
|
port: import_zod2.z.number().int().min(1).max(65535).default(18789),
|
|
@@ -1421,23 +1686,25 @@ var init_openclaw = __esm({
|
|
|
1421
1686
|
);
|
|
1422
1687
|
}
|
|
1423
1688
|
const skillsDir = resolveOpenClawSkillsDir(profile, config);
|
|
1424
|
-
return new OpenClawService(baseUrl, config.apiKey, config.model, new OpenClawSkillLoader(skillsDir));
|
|
1689
|
+
return new OpenClawService(baseUrl, config.apiKey, config.model, config, new OpenClawSkillLoader(skillsDir));
|
|
1425
1690
|
},
|
|
1426
1691
|
async health() {
|
|
1427
1692
|
return { available: true };
|
|
1428
1693
|
}
|
|
1429
1694
|
};
|
|
1430
1695
|
OpenClawService = class {
|
|
1431
|
-
constructor(baseUrl, apiKey, model, skillLoader) {
|
|
1696
|
+
constructor(baseUrl, apiKey, model, config, skillLoader) {
|
|
1432
1697
|
this.baseUrl = baseUrl;
|
|
1433
1698
|
this.apiKey = apiKey;
|
|
1434
1699
|
this.model = model;
|
|
1700
|
+
this.config = config;
|
|
1435
1701
|
this.skillLoader = skillLoader;
|
|
1436
1702
|
this.id = baseUrl;
|
|
1437
1703
|
}
|
|
1438
1704
|
baseUrl;
|
|
1439
1705
|
apiKey;
|
|
1440
1706
|
model;
|
|
1707
|
+
config;
|
|
1441
1708
|
skillLoader;
|
|
1442
1709
|
id;
|
|
1443
1710
|
async healthy() {
|
|
@@ -1446,17 +1713,47 @@ var init_openclaw = __esm({
|
|
|
1446
1713
|
async dispatch(task) {
|
|
1447
1714
|
const startedAt = Date.now();
|
|
1448
1715
|
const systemPrompt = typeof task.metadata?.systemPrompt === "string" ? task.metadata.systemPrompt : void 0;
|
|
1716
|
+
this.installRoleTemplateSkill();
|
|
1449
1717
|
const skillPrompt = await this.skillLoader.loadSystemPromptFragment().catch((err) => {
|
|
1450
1718
|
process.stderr.write(`[openclaw-adapter] failed to read skills: ${err.message}
|
|
1451
1719
|
`);
|
|
1452
1720
|
return void 0;
|
|
1453
1721
|
});
|
|
1454
1722
|
const systemContent = [systemPrompt, skillPrompt].filter(Boolean).join("\n\n");
|
|
1455
|
-
const
|
|
1456
|
-
|
|
1457
|
-
|
|
1723
|
+
const refs = task.assetRefs ?? [];
|
|
1724
|
+
const imageRefs = refs.filter((r) => r.mime?.startsWith("image/"));
|
|
1725
|
+
const fileRefs = refs.filter((r) => r.mime && !r.mime.startsWith("image/"));
|
|
1726
|
+
const inputContent = [
|
|
1727
|
+
{ type: "input_text", text: task.prompt }
|
|
1728
|
+
];
|
|
1729
|
+
for (const r of imageRefs) {
|
|
1730
|
+
const source = pickResponsesSource(r);
|
|
1731
|
+
if (!source) {
|
|
1732
|
+
inputContent.push({
|
|
1733
|
+
type: "input_text",
|
|
1734
|
+
text: `[image attachment id=${r.assetId} unavailable: cdn unreachable and bytes exceeded inline cap]`
|
|
1735
|
+
});
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
inputContent.push({ type: "input_image", source });
|
|
1739
|
+
}
|
|
1740
|
+
for (const r of fileRefs) {
|
|
1741
|
+
const source = pickResponsesSource(r);
|
|
1742
|
+
if (!source) {
|
|
1743
|
+
inputContent.push({
|
|
1744
|
+
type: "input_text",
|
|
1745
|
+
text: `[file attachment id=${r.assetId} mime=${r.mime ?? "unknown"} unavailable]`
|
|
1746
|
+
});
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1749
|
+
if (r.filename && source.type === "base64") {
|
|
1750
|
+
source.filename = r.filename;
|
|
1751
|
+
}
|
|
1752
|
+
inputContent.push({ type: "input_file", source });
|
|
1753
|
+
}
|
|
1754
|
+
const input = [{ type: "message", role: "user", content: inputContent }];
|
|
1458
1755
|
try {
|
|
1459
|
-
const res = await fetch(`${this.baseUrl}/v1/
|
|
1756
|
+
const res = await fetch(`${this.baseUrl}/v1/responses`, {
|
|
1460
1757
|
method: "POST",
|
|
1461
1758
|
headers: {
|
|
1462
1759
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -1464,7 +1761,11 @@ var init_openclaw = __esm({
|
|
|
1464
1761
|
},
|
|
1465
1762
|
body: JSON.stringify({
|
|
1466
1763
|
model: this.model,
|
|
1467
|
-
|
|
1764
|
+
input,
|
|
1765
|
+
// OpenClaw `/v1/responses` accepts top-level `instructions` for
|
|
1766
|
+
// the system prompt — keeping it out of the user message
|
|
1767
|
+
// preserves cleanish channel-context separation.
|
|
1768
|
+
...systemContent ? { instructions: systemContent } : {},
|
|
1468
1769
|
stream: false
|
|
1469
1770
|
}),
|
|
1470
1771
|
signal: task.signal
|
|
@@ -1480,7 +1781,21 @@ var init_openclaw = __esm({
|
|
|
1480
1781
|
};
|
|
1481
1782
|
}
|
|
1482
1783
|
const json = await res.json();
|
|
1483
|
-
let output =
|
|
1784
|
+
let output = "";
|
|
1785
|
+
if (Array.isArray(json.output)) {
|
|
1786
|
+
for (const item of json.output) {
|
|
1787
|
+
if (item?.type === "message" && Array.isArray(item.content)) {
|
|
1788
|
+
for (const part of item.content) {
|
|
1789
|
+
if (part?.type === "output_text" && typeof part.text === "string") {
|
|
1790
|
+
output += part.text;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
if (!output) {
|
|
1797
|
+
output = json.choices?.[0]?.message?.content ?? "";
|
|
1798
|
+
}
|
|
1484
1799
|
const ERR_PREFIX_RE = /^Cannot read properties of undefined \(reading '[^']+'\)\s*\n+/;
|
|
1485
1800
|
if (ERR_PREFIX_RE.test(output)) {
|
|
1486
1801
|
process.stderr.write(
|
|
@@ -1498,6 +1813,25 @@ var init_openclaw = __esm({
|
|
|
1498
1813
|
return categorizeDispatchError(err, task.signal);
|
|
1499
1814
|
}
|
|
1500
1815
|
}
|
|
1816
|
+
installRoleTemplateSkill() {
|
|
1817
|
+
const roleTemplate = readPlainRecord2(this.config.roleTemplate);
|
|
1818
|
+
const openclawConfig = readPlainRecord2(roleTemplate?.openclawConfig);
|
|
1819
|
+
const agents = readNonEmptyString2(openclawConfig?.agents);
|
|
1820
|
+
if (!agents) return;
|
|
1821
|
+
const slug = sanitizeRoleSkillSlug2(readNonEmptyString2(roleTemplate?.slug) ?? "template");
|
|
1822
|
+
const skillName = `${PRISMER_ROLE_SKILL_PREFIX2}-${slug}`;
|
|
1823
|
+
const content = renderRoleTemplateSkill2(skillName, agents, roleTemplate);
|
|
1824
|
+
try {
|
|
1825
|
+
const skillDir = (0, import_node_path3.join)(this.skillLoader.getSkillsRoot(), skillName);
|
|
1826
|
+
(0, import_node_fs3.mkdirSync)(skillDir, { recursive: true });
|
|
1827
|
+
(0, import_node_fs3.writeFileSync)((0, import_node_path3.join)(skillDir, "SKILL.md"), content, "utf8");
|
|
1828
|
+
} catch (err) {
|
|
1829
|
+
process.stderr.write(
|
|
1830
|
+
`[openclaw-adapter] failed to install role-template skill ${skillName}: ${err.message}
|
|
1831
|
+
`
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1501
1835
|
};
|
|
1502
1836
|
}
|
|
1503
1837
|
});
|
|
@@ -1618,7 +1952,7 @@ async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, sig
|
|
|
1618
1952
|
continue;
|
|
1619
1953
|
}
|
|
1620
1954
|
const skillDir = (0, import_node_path8.join)(skillsRoot, slug);
|
|
1621
|
-
await
|
|
1955
|
+
await import_node_fs8.promises.mkdir(skillDir, { recursive: true });
|
|
1622
1956
|
const localFiles = await walkLocalDir(skillDir);
|
|
1623
1957
|
let dirty = false;
|
|
1624
1958
|
let perFileFailures = 0;
|
|
@@ -1673,14 +2007,14 @@ async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, sig
|
|
|
1673
2007
|
perFileFailures++;
|
|
1674
2008
|
continue;
|
|
1675
2009
|
}
|
|
1676
|
-
await
|
|
1677
|
-
await
|
|
2010
|
+
await import_node_fs8.promises.mkdir((0, import_node_path8.dirname)(targetPath), { recursive: true });
|
|
2011
|
+
await import_node_fs8.promises.writeFile(targetPath, bytes);
|
|
1678
2012
|
localFiles.delete(file.path);
|
|
1679
2013
|
dirty = true;
|
|
1680
2014
|
}
|
|
1681
2015
|
for (const orphan of localFiles.keys()) {
|
|
1682
2016
|
try {
|
|
1683
|
-
await
|
|
2017
|
+
await import_node_fs8.promises.unlink((0, import_node_path8.join)(skillDir, orphan));
|
|
1684
2018
|
dirty = true;
|
|
1685
2019
|
} catch (err) {
|
|
1686
2020
|
if (err.code !== "ENOENT") {
|
|
@@ -1793,7 +2127,7 @@ async function walkLocalDir(root) {
|
|
|
1793
2127
|
async function walk2(dir) {
|
|
1794
2128
|
let entries;
|
|
1795
2129
|
try {
|
|
1796
|
-
entries = await
|
|
2130
|
+
entries = await import_node_fs8.promises.readdir(dir, { withFileTypes: true });
|
|
1797
2131
|
} catch (err) {
|
|
1798
2132
|
if (err.code === "ENOENT") return;
|
|
1799
2133
|
throw err;
|
|
@@ -1804,7 +2138,7 @@ async function walkLocalDir(root) {
|
|
|
1804
2138
|
await walk2(full);
|
|
1805
2139
|
} else if (ent.isFile()) {
|
|
1806
2140
|
try {
|
|
1807
|
-
const buf = await
|
|
2141
|
+
const buf = await import_node_fs8.promises.readFile(full);
|
|
1808
2142
|
const rel = (0, import_node_path8.relative)(root, full).split(import_node_path8.sep).join("/");
|
|
1809
2143
|
out.set(rel, sha256Buffer(buf));
|
|
1810
2144
|
} catch {
|
|
@@ -1878,11 +2212,11 @@ async function ackSkillSync(cloud, agentImUserId, input, signal) {
|
|
|
1878
2212
|
`);
|
|
1879
2213
|
}
|
|
1880
2214
|
}
|
|
1881
|
-
var
|
|
2215
|
+
var import_node_fs8, import_node_path8, import_node_crypto3, skillSyncWarnedProfiles;
|
|
1882
2216
|
var init_skill_sync = __esm({
|
|
1883
2217
|
"src/daemon/skill-sync.ts"() {
|
|
1884
2218
|
"use strict";
|
|
1885
|
-
|
|
2219
|
+
import_node_fs8 = require("fs");
|
|
1886
2220
|
import_node_path8 = require("path");
|
|
1887
2221
|
import_node_crypto3 = require("crypto");
|
|
1888
2222
|
init_hermes();
|
|
@@ -2323,7 +2657,7 @@ var require_package = __commonJS({
|
|
|
2323
2657
|
"package.json"(exports2, module2) {
|
|
2324
2658
|
module2.exports = {
|
|
2325
2659
|
name: "@prismer/runtime",
|
|
2326
|
-
version: "2.0.
|
|
2660
|
+
version: "2.0.6",
|
|
2327
2661
|
description: "Prismer Cloud daemon runtime \u2014 TS-only adapter host for hosted IM agents",
|
|
2328
2662
|
type: "module",
|
|
2329
2663
|
main: "dist/index.js",
|
|
@@ -2357,6 +2691,7 @@ var require_package = __commonJS({
|
|
|
2357
2691
|
},
|
|
2358
2692
|
dependencies: {
|
|
2359
2693
|
"@iarna/toml": "^2.2.5",
|
|
2694
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
2360
2695
|
"better-sqlite3": "^11.8.0",
|
|
2361
2696
|
commander: "^12.1.0",
|
|
2362
2697
|
qrcode: "^1.5.4",
|
|
@@ -2514,21 +2849,21 @@ function pidFilePath(paths) {
|
|
|
2514
2849
|
return (0, import_node_path12.join)(paths.root, "daemon.pid");
|
|
2515
2850
|
}
|
|
2516
2851
|
function writePidFile(paths, pid) {
|
|
2517
|
-
(0,
|
|
2852
|
+
(0, import_node_fs17.writeFileSync)(pidFilePath(paths), `${pid}
|
|
2518
2853
|
`, "utf8");
|
|
2519
2854
|
}
|
|
2520
2855
|
function readPidFile(paths) {
|
|
2521
2856
|
const p = pidFilePath(paths);
|
|
2522
|
-
if (!(0,
|
|
2523
|
-
const raw = (0,
|
|
2857
|
+
if (!(0, import_node_fs17.existsSync)(p)) return void 0;
|
|
2858
|
+
const raw = (0, import_node_fs17.readFileSync)(p, "utf8").trim();
|
|
2524
2859
|
const pid = Number.parseInt(raw, 10);
|
|
2525
2860
|
return Number.isFinite(pid) ? pid : void 0;
|
|
2526
2861
|
}
|
|
2527
2862
|
function clearPidFile(paths) {
|
|
2528
2863
|
const p = pidFilePath(paths);
|
|
2529
|
-
if ((0,
|
|
2864
|
+
if ((0, import_node_fs17.existsSync)(p)) {
|
|
2530
2865
|
try {
|
|
2531
|
-
(0,
|
|
2866
|
+
(0, import_node_fs17.unlinkSync)(p);
|
|
2532
2867
|
} catch {
|
|
2533
2868
|
}
|
|
2534
2869
|
}
|
|
@@ -2541,11 +2876,11 @@ function pidAlive2(pid) {
|
|
|
2541
2876
|
return false;
|
|
2542
2877
|
}
|
|
2543
2878
|
}
|
|
2544
|
-
var
|
|
2879
|
+
var import_node_fs17, import_node_path12, DEFAULT_CLOUD_BASE_URL, ANSI, PKG_VERSION, RUNTIME_SUBTITLE;
|
|
2545
2880
|
var init_util = __esm({
|
|
2546
2881
|
"src/cli/util.ts"() {
|
|
2547
2882
|
"use strict";
|
|
2548
|
-
|
|
2883
|
+
import_node_fs17 = require("fs");
|
|
2549
2884
|
import_node_path12 = require("path");
|
|
2550
2885
|
init_ui();
|
|
2551
2886
|
DEFAULT_CLOUD_BASE_URL = "https://prismer.cloud";
|
|
@@ -2789,9 +3124,9 @@ var WsClient = class extends import_node_events.EventEmitter {
|
|
|
2789
3124
|
|
|
2790
3125
|
// src/sync/store.ts
|
|
2791
3126
|
var import_better_sqlite32 = __toESM(require("better-sqlite3"), 1);
|
|
2792
|
-
var
|
|
3127
|
+
var import_node_fs4 = require("fs");
|
|
2793
3128
|
var import_node_path4 = require("path");
|
|
2794
|
-
var SCHEMA_VERSION =
|
|
3129
|
+
var SCHEMA_VERSION = 4;
|
|
2795
3130
|
var MIGRATIONS = [
|
|
2796
3131
|
{
|
|
2797
3132
|
version: 1,
|
|
@@ -2917,14 +3252,44 @@ var MIGRATIONS = [
|
|
|
2917
3252
|
CREATE INDEX IF NOT EXISTS idx_asset_meta_seq
|
|
2918
3253
|
ON asset_metadata_index(workspace_id, asset_index_seq);
|
|
2919
3254
|
`
|
|
3255
|
+
},
|
|
3256
|
+
{
|
|
3257
|
+
version: 4,
|
|
3258
|
+
up: `
|
|
3259
|
+
-- Wave 4 / W3 \xA74.3 \u2014 pending dispatch replies for crash recovery.
|
|
3260
|
+
--
|
|
3261
|
+
-- The two-phase reply protocol writes a row here BEFORE calling
|
|
3262
|
+
-- POST /dispatch/:taskId/prepare so that a crash between prepare
|
|
3263
|
+
-- and commit leaves a recoverable trail. On daemon cold-start, any
|
|
3264
|
+
-- row with status='prepared' triggers an idempotent commit retry.
|
|
3265
|
+
--
|
|
3266
|
+
-- The server-side cutoff for orphaned 'prepared' rows is 1h
|
|
3267
|
+
-- (PREPARED_REPLY_ORPHAN_MS in dispatch-reply.service.ts); rows
|
|
3268
|
+
-- older than that will fail commit with DISPATCH_REPLY_INVALID_STATE
|
|
3269
|
+
-- and must be reported to the user as 'dispatch lost'.
|
|
3270
|
+
CREATE TABLE IF NOT EXISTS pending_dispatch_replies (
|
|
3271
|
+
idempotency_key TEXT PRIMARY KEY,
|
|
3272
|
+
task_id TEXT NOT NULL,
|
|
3273
|
+
reply_id TEXT,
|
|
3274
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
3275
|
+
payload_json TEXT NOT NULL,
|
|
3276
|
+
prepared_at INTEGER,
|
|
3277
|
+
created_at INTEGER NOT NULL,
|
|
3278
|
+
last_attempt_at INTEGER,
|
|
3279
|
+
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
3280
|
+
last_error TEXT
|
|
3281
|
+
);
|
|
3282
|
+
CREATE INDEX IF NOT EXISTS idx_pending_reply_task ON pending_dispatch_replies (task_id);
|
|
3283
|
+
CREATE INDEX IF NOT EXISTS idx_pending_reply_status ON pending_dispatch_replies (status, created_at);
|
|
3284
|
+
`
|
|
2920
3285
|
}
|
|
2921
3286
|
];
|
|
2922
3287
|
function runSql(db, sql) {
|
|
2923
3288
|
db["exec"](sql);
|
|
2924
3289
|
}
|
|
2925
3290
|
function openLocalDb(path9) {
|
|
2926
|
-
if (path9 !== ":memory:" && !(0,
|
|
2927
|
-
(0,
|
|
3291
|
+
if (path9 !== ":memory:" && !(0, import_node_fs4.existsSync)((0, import_node_path4.dirname)(path9))) {
|
|
3292
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path9), { recursive: true });
|
|
2928
3293
|
}
|
|
2929
3294
|
const db = new import_better_sqlite32.default(path9);
|
|
2930
3295
|
db.pragma("journal_mode = WAL");
|
|
@@ -3161,7 +3526,7 @@ function listRoleTemplates() {
|
|
|
3161
3526
|
|
|
3162
3527
|
// src/config.ts
|
|
3163
3528
|
var TOML = __toESM(require("@iarna/toml"), 1);
|
|
3164
|
-
var
|
|
3529
|
+
var import_node_fs5 = require("fs");
|
|
3165
3530
|
var import_node_os3 = require("os");
|
|
3166
3531
|
var import_node_path5 = require("path");
|
|
3167
3532
|
var import_zod3 = require("zod");
|
|
@@ -3204,15 +3569,15 @@ function resolvePaths(home) {
|
|
|
3204
3569
|
};
|
|
3205
3570
|
}
|
|
3206
3571
|
function configExists(paths = resolvePaths()) {
|
|
3207
|
-
return (0,
|
|
3572
|
+
return (0, import_node_fs5.existsSync)(paths.configFile);
|
|
3208
3573
|
}
|
|
3209
3574
|
function loadConfig(paths = resolvePaths()) {
|
|
3210
|
-
if (!(0,
|
|
3575
|
+
if (!(0, import_node_fs5.existsSync)(paths.configFile)) {
|
|
3211
3576
|
throw new Error(
|
|
3212
3577
|
`Config not found at ${paths.configFile}. Run \`prismer setup\` to create it.`
|
|
3213
3578
|
);
|
|
3214
3579
|
}
|
|
3215
|
-
const raw = (0,
|
|
3580
|
+
const raw = (0, import_node_fs5.readFileSync)(paths.configFile, "utf8");
|
|
3216
3581
|
const parsed = TOML.parse(raw);
|
|
3217
3582
|
const merged = {
|
|
3218
3583
|
...parsed,
|
|
@@ -3227,14 +3592,14 @@ function loadConfig(paths = resolvePaths()) {
|
|
|
3227
3592
|
return result.data;
|
|
3228
3593
|
}
|
|
3229
3594
|
function saveConfig(config, paths = resolvePaths()) {
|
|
3230
|
-
if (!(0,
|
|
3231
|
-
(0,
|
|
3595
|
+
if (!(0, import_node_fs5.existsSync)(paths.root)) {
|
|
3596
|
+
(0, import_node_fs5.mkdirSync)(paths.root, { recursive: true });
|
|
3232
3597
|
}
|
|
3233
|
-
if (!(0,
|
|
3234
|
-
(0,
|
|
3598
|
+
if (!(0, import_node_fs5.existsSync)((0, import_node_path5.dirname)(paths.configFile))) {
|
|
3599
|
+
(0, import_node_fs5.mkdirSync)((0, import_node_path5.dirname)(paths.configFile), { recursive: true });
|
|
3235
3600
|
}
|
|
3236
3601
|
ConfigSchema.parse(config);
|
|
3237
|
-
(0,
|
|
3602
|
+
(0, import_node_fs5.writeFileSync)(paths.configFile, TOML.stringify(config), "utf8");
|
|
3238
3603
|
}
|
|
3239
3604
|
function deriveWsUrl(httpBase) {
|
|
3240
3605
|
const u = new URL(httpBase);
|
|
@@ -3407,7 +3772,7 @@ function envelope(type, payload, requestId) {
|
|
|
3407
3772
|
// src/asset-cache.ts
|
|
3408
3773
|
var import_node_crypto2 = require("crypto");
|
|
3409
3774
|
var import_node_dns = require("dns");
|
|
3410
|
-
var
|
|
3775
|
+
var import_node_fs6 = require("fs");
|
|
3411
3776
|
var import_node_net = require("net");
|
|
3412
3777
|
var import_node_path6 = require("path");
|
|
3413
3778
|
var import_undici = require("undici");
|
|
@@ -3432,8 +3797,8 @@ var AssetCache = class {
|
|
|
3432
3797
|
this.cloud = opts.cloud;
|
|
3433
3798
|
this.cacheDir = opts.cacheDir;
|
|
3434
3799
|
this.maxBytes = opts.maxBytes ?? 5 * 1024 * 1024 * 1024;
|
|
3435
|
-
if (!(0,
|
|
3436
|
-
(0,
|
|
3800
|
+
if (!(0, import_node_fs6.existsSync)(this.cacheDir)) {
|
|
3801
|
+
(0, import_node_fs6.mkdirSync)(this.cacheDir, { recursive: true });
|
|
3437
3802
|
}
|
|
3438
3803
|
}
|
|
3439
3804
|
/** Local file path for a content hash. Files split by first 2 chars for fs scaling. */
|
|
@@ -3443,7 +3808,7 @@ var AssetCache = class {
|
|
|
3443
3808
|
get(hash) {
|
|
3444
3809
|
const row = this.db.prepare("SELECT * FROM cached_assets WHERE content_hash = ?").get(hash);
|
|
3445
3810
|
if (!row) return void 0;
|
|
3446
|
-
if (!(0,
|
|
3811
|
+
if (!(0, import_node_fs6.existsSync)(row.local_path)) {
|
|
3447
3812
|
this.db.prepare("DELETE FROM cached_assets WHERE content_hash = ?").run(hash);
|
|
3448
3813
|
return void 0;
|
|
3449
3814
|
}
|
|
@@ -3507,12 +3872,12 @@ var AssetCache = class {
|
|
|
3507
3872
|
throw new Error(`Asset hash mismatch for ${hash}: downloaded ${actualHash}`);
|
|
3508
3873
|
}
|
|
3509
3874
|
const localPath = this.pathFor(hash);
|
|
3510
|
-
if (!(0,
|
|
3511
|
-
(0,
|
|
3875
|
+
if (!(0, import_node_fs6.existsSync)((0, import_node_path6.dirname)(localPath))) {
|
|
3876
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path6.dirname)(localPath), { recursive: true });
|
|
3512
3877
|
}
|
|
3513
3878
|
const tmpPath = `${localPath}.${process.pid}.${Date.now()}.tmp`;
|
|
3514
|
-
(0,
|
|
3515
|
-
(0,
|
|
3879
|
+
(0, import_node_fs6.writeFileSync)(tmpPath, buf);
|
|
3880
|
+
(0, import_node_fs6.renameSync)(tmpPath, localPath);
|
|
3516
3881
|
const mime = res.headers.get("content-type");
|
|
3517
3882
|
const now = Date.now();
|
|
3518
3883
|
this.db.prepare(
|
|
@@ -3571,12 +3936,12 @@ var AssetCache = class {
|
|
|
3571
3936
|
return { cached: existing, finalUrl, durationMs: Date.now() - started };
|
|
3572
3937
|
}
|
|
3573
3938
|
const localPath = this.pathFor(hash);
|
|
3574
|
-
if (!(0,
|
|
3575
|
-
(0,
|
|
3939
|
+
if (!(0, import_node_fs6.existsSync)((0, import_node_path6.dirname)(localPath))) {
|
|
3940
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path6.dirname)(localPath), { recursive: true });
|
|
3576
3941
|
}
|
|
3577
3942
|
const tmpPath = `${localPath}.${process.pid}.${Date.now()}.tmp`;
|
|
3578
|
-
(0,
|
|
3579
|
-
(0,
|
|
3943
|
+
(0, import_node_fs6.writeFileSync)(tmpPath, body);
|
|
3944
|
+
(0, import_node_fs6.renameSync)(tmpPath, localPath);
|
|
3580
3945
|
const now = Date.now();
|
|
3581
3946
|
this.db.prepare(
|
|
3582
3947
|
`INSERT OR REPLACE INTO cached_assets
|
|
@@ -3604,7 +3969,7 @@ var AssetCache = class {
|
|
|
3604
3969
|
if (actualHash !== hash) {
|
|
3605
3970
|
throw new Error(`Asset hash mismatch for ${hash}: local file is ${actualHash}`);
|
|
3606
3971
|
}
|
|
3607
|
-
const size = (0,
|
|
3972
|
+
const size = (0, import_node_fs6.statSync)(localPath).size;
|
|
3608
3973
|
const now = Date.now();
|
|
3609
3974
|
this.db.prepare(
|
|
3610
3975
|
`INSERT OR REPLACE INTO cached_assets
|
|
@@ -3632,7 +3997,7 @@ var AssetCache = class {
|
|
|
3632
3997
|
for (const row of candidates) {
|
|
3633
3998
|
if (total <= this.maxBytes) break;
|
|
3634
3999
|
try {
|
|
3635
|
-
if ((0,
|
|
4000
|
+
if ((0, import_node_fs6.existsSync)(row.local_path)) (0, import_node_fs6.unlinkSync)(row.local_path);
|
|
3636
4001
|
} catch {
|
|
3637
4002
|
}
|
|
3638
4003
|
this.db.prepare("DELETE FROM cached_assets WHERE content_hash = ?").run(row.content_hash);
|
|
@@ -3647,7 +4012,7 @@ function sha256(buf) {
|
|
|
3647
4012
|
return (0, import_node_crypto2.createHash)("sha256").update(buf).digest("hex");
|
|
3648
4013
|
}
|
|
3649
4014
|
function readFileBuffer(path9) {
|
|
3650
|
-
return (0,
|
|
4015
|
+
return (0, import_node_fs6.readFileSync)(path9);
|
|
3651
4016
|
}
|
|
3652
4017
|
var DEFAULT_URL_FETCH_MAX_BYTES = 5 * 1024 * 1024;
|
|
3653
4018
|
var DEFAULT_URL_FETCH_TIMEOUT_MS = 15e3;
|
|
@@ -3829,7 +4194,7 @@ async function fetchUrlWithGuards(rawUrl, opts) {
|
|
|
3829
4194
|
}
|
|
3830
4195
|
|
|
3831
4196
|
// src/daemon/asset/mirror.ts
|
|
3832
|
-
var
|
|
4197
|
+
var import_node_fs7 = require("fs");
|
|
3833
4198
|
var import_node_path7 = require("path");
|
|
3834
4199
|
function rowToBinding(row) {
|
|
3835
4200
|
return {
|
|
@@ -3851,16 +4216,16 @@ var WorkspaceMirror = class {
|
|
|
3851
4216
|
this.db = opts.db;
|
|
3852
4217
|
this.cloud = opts.cloud;
|
|
3853
4218
|
this.workspaceId = opts.workspaceId;
|
|
3854
|
-
if (!(0,
|
|
3855
|
-
(0,
|
|
4219
|
+
if (!(0, import_node_fs7.existsSync)(opts.workspaceStateDir)) {
|
|
4220
|
+
(0, import_node_fs7.mkdirSync)(opts.workspaceStateDir, { recursive: true });
|
|
3856
4221
|
}
|
|
3857
4222
|
this.cursorPath = (0, import_node_path7.join)(opts.workspaceStateDir, "asset-cursor.json");
|
|
3858
4223
|
}
|
|
3859
4224
|
/** Persisted cursor for this workspace, or null on first run / corrupted file. */
|
|
3860
4225
|
readCursor() {
|
|
3861
|
-
if (!(0,
|
|
4226
|
+
if (!(0, import_node_fs7.existsSync)(this.cursorPath)) return null;
|
|
3862
4227
|
try {
|
|
3863
|
-
const parsed = JSON.parse((0,
|
|
4228
|
+
const parsed = JSON.parse((0, import_node_fs7.readFileSync)(this.cursorPath, "utf8"));
|
|
3864
4229
|
if (parsed.workspaceId !== this.workspaceId) return null;
|
|
3865
4230
|
return parsed.cursor;
|
|
3866
4231
|
} catch {
|
|
@@ -3873,7 +4238,7 @@ var WorkspaceMirror = class {
|
|
|
3873
4238
|
cursor,
|
|
3874
4239
|
writtenAt: Date.now()
|
|
3875
4240
|
};
|
|
3876
|
-
(0,
|
|
4241
|
+
(0, import_node_fs7.writeFileSync)(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
3877
4242
|
}
|
|
3878
4243
|
/**
|
|
3879
4244
|
* Pull workspace_file changes since the persisted cursor and upsert into
|
|
@@ -4279,9 +4644,265 @@ function extractHttpUrls(text) {
|
|
|
4279
4644
|
}
|
|
4280
4645
|
|
|
4281
4646
|
// src/daemon/dispatch.ts
|
|
4282
|
-
var
|
|
4647
|
+
var import_node_fs9 = require("fs");
|
|
4283
4648
|
var path = __toESM(require("path"), 1);
|
|
4284
4649
|
init_skill_sync();
|
|
4650
|
+
|
|
4651
|
+
// src/daemon/task-heartbeat.ts
|
|
4652
|
+
var TaskHeartbeat = class {
|
|
4653
|
+
constructor(opts) {
|
|
4654
|
+
this.opts = opts;
|
|
4655
|
+
this.intervalMs = opts.intervalMs ?? 15e3;
|
|
4656
|
+
this.maxRetries = opts.maxRetries ?? 3;
|
|
4657
|
+
this.retryBackoffMs = opts.retryBackoffMs ?? 100;
|
|
4658
|
+
this.now = opts.now ?? Date.now;
|
|
4659
|
+
}
|
|
4660
|
+
opts;
|
|
4661
|
+
timer;
|
|
4662
|
+
version = 0;
|
|
4663
|
+
taskId;
|
|
4664
|
+
getPhase;
|
|
4665
|
+
/** Tracks the last step time pushed via touchStep() — informational. */
|
|
4666
|
+
lastStepAt;
|
|
4667
|
+
intervalMs;
|
|
4668
|
+
maxRetries;
|
|
4669
|
+
retryBackoffMs;
|
|
4670
|
+
now;
|
|
4671
|
+
/**
|
|
4672
|
+
* Begin heartbeating for `taskId`. `getPhase` is invoked at every tick to
|
|
4673
|
+
* pick up the live phase string — adapters can mutate their phase state
|
|
4674
|
+
* freely and the heartbeat will surface it on the next 15s boundary.
|
|
4675
|
+
*
|
|
4676
|
+
* Calling start() twice on the same instance replaces the active task
|
|
4677
|
+
* (previous timer is cleared). Returns silently when already started for
|
|
4678
|
+
* the same taskId.
|
|
4679
|
+
*/
|
|
4680
|
+
start(taskId, getPhase) {
|
|
4681
|
+
if (this.taskId === taskId && this.timer) return;
|
|
4682
|
+
this.stop();
|
|
4683
|
+
this.taskId = taskId;
|
|
4684
|
+
this.getPhase = getPhase;
|
|
4685
|
+
this.timer = setInterval(() => {
|
|
4686
|
+
void this.push();
|
|
4687
|
+
}, this.intervalMs);
|
|
4688
|
+
this.timer.unref?.();
|
|
4689
|
+
}
|
|
4690
|
+
/**
|
|
4691
|
+
* Stop the heartbeat loop. Idempotent.
|
|
4692
|
+
*/
|
|
4693
|
+
stop() {
|
|
4694
|
+
if (this.timer) {
|
|
4695
|
+
clearInterval(this.timer);
|
|
4696
|
+
this.timer = void 0;
|
|
4697
|
+
}
|
|
4698
|
+
this.taskId = void 0;
|
|
4699
|
+
this.getPhase = void 0;
|
|
4700
|
+
}
|
|
4701
|
+
/**
|
|
4702
|
+
* Phase transition — push immediately AND update what the timer-driven
|
|
4703
|
+
* tick will report next time. Adapter calls this whenever the lifecycle
|
|
4704
|
+
* advances (thinking → tool_use → reasoning → responding → ...).
|
|
4705
|
+
*
|
|
4706
|
+
* No-op when no task is active.
|
|
4707
|
+
*/
|
|
4708
|
+
setPhase(phase) {
|
|
4709
|
+
if (!this.taskId) return;
|
|
4710
|
+
this.getPhase = () => phase;
|
|
4711
|
+
void this.push();
|
|
4712
|
+
}
|
|
4713
|
+
/**
|
|
4714
|
+
* Mark "I just made observable progress." Updates lastStepAt; next push
|
|
4715
|
+
* (timer-driven or setPhase-driven) carries it. Lightweight — no immediate
|
|
4716
|
+
* send. Adapters call this around tool calls / token boundaries so the
|
|
4717
|
+
* cloud reaper can distinguish "alive but slow" from "truly stuck".
|
|
4718
|
+
*/
|
|
4719
|
+
touchStep() {
|
|
4720
|
+
this.lastStepAt = this.now();
|
|
4721
|
+
}
|
|
4722
|
+
/**
|
|
4723
|
+
* Manually trigger one heartbeat send (does not affect the timer). Mainly
|
|
4724
|
+
* useful for tests that don't want to await a 15s tick. Returns the
|
|
4725
|
+
* heartbeatVersion that was sent, or undefined if no task is active.
|
|
4726
|
+
*/
|
|
4727
|
+
async forceTick() {
|
|
4728
|
+
if (!this.taskId) return void 0;
|
|
4729
|
+
return this.push();
|
|
4730
|
+
}
|
|
4731
|
+
/** Test helper: current heartbeatVersion (last value sent, may be 0 before first tick). */
|
|
4732
|
+
get currentVersion() {
|
|
4733
|
+
return this.version;
|
|
4734
|
+
}
|
|
4735
|
+
async push() {
|
|
4736
|
+
if (!this.taskId || !this.getPhase) return void 0;
|
|
4737
|
+
const taskId = this.taskId;
|
|
4738
|
+
const phase = this.getPhase();
|
|
4739
|
+
const heartbeatVersion = ++this.version;
|
|
4740
|
+
const payload = {
|
|
4741
|
+
taskId,
|
|
4742
|
+
heartbeatVersion,
|
|
4743
|
+
currentPhase: phase,
|
|
4744
|
+
...this.lastStepAt !== void 0 ? { lastStepAt: this.lastStepAt } : {}
|
|
4745
|
+
};
|
|
4746
|
+
const msg = envelope("task.heartbeat", payload);
|
|
4747
|
+
let lastErr;
|
|
4748
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
|
|
4749
|
+
try {
|
|
4750
|
+
if (this.opts.ws.isOpen && !this.opts.ws.isOpen()) {
|
|
4751
|
+
return heartbeatVersion;
|
|
4752
|
+
}
|
|
4753
|
+
this.opts.ws.send(msg);
|
|
4754
|
+
return heartbeatVersion;
|
|
4755
|
+
} catch (err) {
|
|
4756
|
+
lastErr = err instanceof Error ? err : new Error(String(err));
|
|
4757
|
+
if (attempt < this.maxRetries) {
|
|
4758
|
+
const backoff = this.retryBackoffMs * Math.pow(2, attempt);
|
|
4759
|
+
await sleep(backoff);
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
if (lastErr) {
|
|
4764
|
+
try {
|
|
4765
|
+
this.opts.onGiveUp?.(taskId, lastErr);
|
|
4766
|
+
} catch {
|
|
4767
|
+
}
|
|
4768
|
+
}
|
|
4769
|
+
return heartbeatVersion;
|
|
4770
|
+
}
|
|
4771
|
+
};
|
|
4772
|
+
function sleep(ms) {
|
|
4773
|
+
return new Promise((resolve3) => {
|
|
4774
|
+
const handle = setTimeout(resolve3, ms);
|
|
4775
|
+
handle.unref?.();
|
|
4776
|
+
});
|
|
4777
|
+
}
|
|
4778
|
+
|
|
4779
|
+
// src/daemon/step-recorder.ts
|
|
4780
|
+
var StepRecorder = class {
|
|
4781
|
+
constructor(opts) {
|
|
4782
|
+
this.opts = opts;
|
|
4783
|
+
this.reasoningThrottleMs = opts.reasoningThrottleMs ?? 500;
|
|
4784
|
+
this.now = opts.now ?? Date.now;
|
|
4785
|
+
}
|
|
4786
|
+
opts;
|
|
4787
|
+
seq = 0;
|
|
4788
|
+
reasoningBuf = { texts: [], firstAt: 0 };
|
|
4789
|
+
reasoningTimer;
|
|
4790
|
+
reasoningThrottleMs;
|
|
4791
|
+
now;
|
|
4792
|
+
/** Push a phase transition step. Immediate (no throttle). */
|
|
4793
|
+
recordPhaseChange(phase) {
|
|
4794
|
+
this.emit("phase_change", { phase });
|
|
4795
|
+
}
|
|
4796
|
+
/**
|
|
4797
|
+
* Push a tool-call step. Immediate.
|
|
4798
|
+
*
|
|
4799
|
+
* `toolCallId` is the daemon-side correlation id used to pair with the
|
|
4800
|
+
* subsequent recordToolResult call. Free-form; adapter decides.
|
|
4801
|
+
*/
|
|
4802
|
+
recordToolCall(toolName, input, toolCallId) {
|
|
4803
|
+
this.emit("tool_call", {
|
|
4804
|
+
toolName,
|
|
4805
|
+
inputSummary: summarize(input),
|
|
4806
|
+
...toolCallId ? { toolCallId } : {}
|
|
4807
|
+
});
|
|
4808
|
+
}
|
|
4809
|
+
/** Push a tool-result step. Immediate. */
|
|
4810
|
+
recordToolResult(toolCallId, output) {
|
|
4811
|
+
this.emit("tool_result", {
|
|
4812
|
+
toolCallId,
|
|
4813
|
+
outputSummary: summarize(output)
|
|
4814
|
+
});
|
|
4815
|
+
}
|
|
4816
|
+
/**
|
|
4817
|
+
* Buffer a reasoning chunk for batched dispatch. Multiple chunks within
|
|
4818
|
+
* `reasoningThrottleMs` are concatenated and emitted as a single
|
|
4819
|
+
* `reasoning_chunk` step when the timer fires.
|
|
4820
|
+
*/
|
|
4821
|
+
recordReasoningChunk(text) {
|
|
4822
|
+
if (!text) return;
|
|
4823
|
+
if (this.reasoningBuf.texts.length === 0) {
|
|
4824
|
+
this.reasoningBuf.firstAt = this.now();
|
|
4825
|
+
}
|
|
4826
|
+
this.reasoningBuf.texts.push(text);
|
|
4827
|
+
if (this.reasoningTimer) return;
|
|
4828
|
+
this.reasoningTimer = setTimeout(() => {
|
|
4829
|
+
this.flushReasoning();
|
|
4830
|
+
}, this.reasoningThrottleMs);
|
|
4831
|
+
this.reasoningTimer.unref?.();
|
|
4832
|
+
}
|
|
4833
|
+
/** Push an error step. Immediate. */
|
|
4834
|
+
recordError(message, payload) {
|
|
4835
|
+
this.emit("error", { message, ...payload ?? {} });
|
|
4836
|
+
}
|
|
4837
|
+
/**
|
|
4838
|
+
* Force any pending reasoning buffer out the door. Call on shutdown so
|
|
4839
|
+
* trailing tokens aren't lost.
|
|
4840
|
+
*/
|
|
4841
|
+
flush() {
|
|
4842
|
+
this.flushReasoning();
|
|
4843
|
+
}
|
|
4844
|
+
/** Test helper. */
|
|
4845
|
+
get currentSeq() {
|
|
4846
|
+
return this.seq;
|
|
4847
|
+
}
|
|
4848
|
+
flushReasoning() {
|
|
4849
|
+
if (this.reasoningTimer) {
|
|
4850
|
+
clearTimeout(this.reasoningTimer);
|
|
4851
|
+
this.reasoningTimer = void 0;
|
|
4852
|
+
}
|
|
4853
|
+
if (this.reasoningBuf.texts.length === 0) return;
|
|
4854
|
+
const text = this.reasoningBuf.texts.join("");
|
|
4855
|
+
const firstAt = this.reasoningBuf.firstAt;
|
|
4856
|
+
this.reasoningBuf = { texts: [], firstAt: 0 };
|
|
4857
|
+
this.emit("reasoning_chunk", { text }, firstAt);
|
|
4858
|
+
}
|
|
4859
|
+
emit(kind, payload, occurredAt) {
|
|
4860
|
+
const frame = {
|
|
4861
|
+
seq: ++this.seq,
|
|
4862
|
+
kind,
|
|
4863
|
+
payload,
|
|
4864
|
+
occurredAt: occurredAt ?? this.now()
|
|
4865
|
+
};
|
|
4866
|
+
const msg = envelope("task.step.append", {
|
|
4867
|
+
taskRunId: this.opts.taskRunId,
|
|
4868
|
+
step: frame
|
|
4869
|
+
});
|
|
4870
|
+
try {
|
|
4871
|
+
if (this.opts.ws.isOpen && !this.opts.ws.isOpen()) {
|
|
4872
|
+
try {
|
|
4873
|
+
this.opts.onSend?.(frame);
|
|
4874
|
+
} catch {
|
|
4875
|
+
}
|
|
4876
|
+
return;
|
|
4877
|
+
}
|
|
4878
|
+
this.opts.ws.send(msg);
|
|
4879
|
+
try {
|
|
4880
|
+
this.opts.onSend?.(frame);
|
|
4881
|
+
} catch {
|
|
4882
|
+
}
|
|
4883
|
+
} catch {
|
|
4884
|
+
}
|
|
4885
|
+
}
|
|
4886
|
+
};
|
|
4887
|
+
var MAX_SUMMARY_CHARS = 512;
|
|
4888
|
+
function summarize(value) {
|
|
4889
|
+
let text;
|
|
4890
|
+
if (value == null) text = "";
|
|
4891
|
+
else if (typeof value === "string") text = value;
|
|
4892
|
+
else {
|
|
4893
|
+
try {
|
|
4894
|
+
text = JSON.stringify(value);
|
|
4895
|
+
} catch {
|
|
4896
|
+
text = String(value);
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
if (text.length > MAX_SUMMARY_CHARS) {
|
|
4900
|
+
return `${text.slice(0, MAX_SUMMARY_CHARS)}\u2026(+${text.length - MAX_SUMMARY_CHARS} chars)`;
|
|
4901
|
+
}
|
|
4902
|
+
return text;
|
|
4903
|
+
}
|
|
4904
|
+
|
|
4905
|
+
// src/daemon/dispatch.ts
|
|
4285
4906
|
var DEFAULT_CONTEXT_MAX = 8e3;
|
|
4286
4907
|
var GOAL_CONTEXT_MAX = 4;
|
|
4287
4908
|
var MEMORY_DIGEST_MAX_BYTES = 4e3;
|
|
@@ -4297,12 +4918,14 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4297
4918
|
const { taskId, agentImUserId } = payload;
|
|
4298
4919
|
let resolvedHashes = [];
|
|
4299
4920
|
let reply;
|
|
4921
|
+
let heartbeatRef;
|
|
4922
|
+
let recorderRef;
|
|
4300
4923
|
const outboxBase = deps.paths?.runsDir ? path.join(deps.paths.runsDir, taskId) : null;
|
|
4301
4924
|
const outboxDir = outboxBase ? path.join(outboxBase, "_outbox") : null;
|
|
4302
4925
|
const workDir = outboxBase ? path.join(outboxBase, "workdir") : null;
|
|
4303
4926
|
if (outboxDir) {
|
|
4304
4927
|
try {
|
|
4305
|
-
await
|
|
4928
|
+
await import_node_fs9.promises.mkdir(outboxDir, { recursive: true });
|
|
4306
4929
|
} catch (err) {
|
|
4307
4930
|
process.stderr.write(
|
|
4308
4931
|
`[daemon] outbox provision failed task=${taskId} dir=${outboxDir}: ${err.message}
|
|
@@ -4312,7 +4935,7 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4312
4935
|
}
|
|
4313
4936
|
if (workDir) {
|
|
4314
4937
|
try {
|
|
4315
|
-
await
|
|
4938
|
+
await import_node_fs9.promises.mkdir(workDir, { recursive: true });
|
|
4316
4939
|
} catch (err) {
|
|
4317
4940
|
process.stderr.write(
|
|
4318
4941
|
`[daemon] workdir provision failed task=${taskId} dir=${workDir}: ${err.message}
|
|
@@ -4378,7 +5001,13 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4378
5001
|
resolvedHashes.push(...r.resolvedHashes);
|
|
4379
5002
|
rewrittenContext = payload.context.map((e, i) => ({ ...e, content: r.texts[i] }));
|
|
4380
5003
|
}
|
|
4381
|
-
const assetResolution = await resolveAssetRefs(payload.assetRefs, deps.assetCache, taskId
|
|
5004
|
+
const assetResolution = await resolveAssetRefs(payload.assetRefs, deps.assetCache, taskId, {
|
|
5005
|
+
cloud: deps.cloud,
|
|
5006
|
+
enabled: deps.visionAux?.enabled !== false,
|
|
5007
|
+
cacheDir: deps.visionAux?.cacheDir ?? (deps.paths ? path.join(deps.paths.cacheDir, "vision-cache") : void 0),
|
|
5008
|
+
timeoutMs: deps.visionAux?.timeoutMs,
|
|
5009
|
+
signal: deps.signal
|
|
5010
|
+
});
|
|
4382
5011
|
resolvedHashes.push(...assetResolution.pinnedHashes);
|
|
4383
5012
|
const basePrompt = composePrompt(
|
|
4384
5013
|
rewrittenPrompt.text,
|
|
@@ -4413,9 +5042,44 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4413
5042
|
}
|
|
4414
5043
|
const operatingPrinciples = resolveOperatingPrinciples(profile.config);
|
|
4415
5044
|
const composedSystemPrompt = [profileSystemPrompt, operatingPrinciples].filter((part) => typeof part === "string" && part.length > 0).join("\n\n");
|
|
5045
|
+
let activePhase = "thinking";
|
|
5046
|
+
const heartbeat = new TaskHeartbeat({
|
|
5047
|
+
ws: deps.ws,
|
|
5048
|
+
onGiveUp: (tid, err) => process.stderr.write(
|
|
5049
|
+
`[daemon] task.heartbeat give-up task=${tid}: ${err.message}
|
|
5050
|
+
`
|
|
5051
|
+
)
|
|
5052
|
+
});
|
|
5053
|
+
heartbeatRef = heartbeat;
|
|
5054
|
+
heartbeat.start(taskId, () => activePhase);
|
|
5055
|
+
const recorder = new StepRecorder({ ws: deps.ws, taskRunId: taskId });
|
|
5056
|
+
recorderRef = recorder;
|
|
5057
|
+
recorder.recordPhaseChange(activePhase);
|
|
4416
5058
|
const taskInput = {
|
|
4417
5059
|
taskId,
|
|
4418
5060
|
prompt: adapterPrompt,
|
|
5061
|
+
heartbeat: {
|
|
5062
|
+
setPhase: (phase) => {
|
|
5063
|
+
activePhase = phase;
|
|
5064
|
+
heartbeat.setPhase(phase);
|
|
5065
|
+
recorder.recordPhaseChange(phase);
|
|
5066
|
+
},
|
|
5067
|
+
touchStep: () => heartbeat.touchStep()
|
|
5068
|
+
},
|
|
5069
|
+
recorder: {
|
|
5070
|
+
recordPhaseChange: (phase) => {
|
|
5071
|
+
activePhase = phase;
|
|
5072
|
+
heartbeat.setPhase(phase);
|
|
5073
|
+
recorder.recordPhaseChange(phase);
|
|
5074
|
+
},
|
|
5075
|
+
recordToolCall: (toolName, input, toolCallId) => recorder.recordToolCall(toolName, input, toolCallId),
|
|
5076
|
+
recordToolResult: (toolCallId, output) => recorder.recordToolResult(toolCallId, output),
|
|
5077
|
+
recordReasoningChunk: (text) => recorder.recordReasoningChunk(text),
|
|
5078
|
+
recordError: (message, payload2) => recorder.recordError(message, payload2)
|
|
5079
|
+
},
|
|
5080
|
+
// 14b rev.3 §9 P3 — multimodal-aware adapters (Hermes, OpenClaw) pick
|
|
5081
|
+
// up the resolved bytes/URLs here; text-only adapters ignore the field.
|
|
5082
|
+
...assetResolution.resolvedRefs.length > 0 ? { assetRefs: assetResolution.resolvedRefs } : {},
|
|
4419
5083
|
metadata: {
|
|
4420
5084
|
...payload.metadata,
|
|
4421
5085
|
conversationId: payload.conversationId,
|
|
@@ -4478,6 +5142,9 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4478
5142
|
}
|
|
4479
5143
|
}
|
|
4480
5144
|
const reaperAborted = deps.signal?.aborted && result.error?.code === "task_cancelled";
|
|
5145
|
+
const approvalRequested = Boolean(
|
|
5146
|
+
result.metadata?.approvalRequested
|
|
5147
|
+
);
|
|
4481
5148
|
let collectedAssetIds = [];
|
|
4482
5149
|
if (deps.outboxWatcher && outboxDir) {
|
|
4483
5150
|
try {
|
|
@@ -4489,14 +5156,23 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4489
5156
|
collectedAssetIds = deps.outboxWatcher.flushPending(taskId);
|
|
4490
5157
|
}
|
|
4491
5158
|
const reaperLimitMs = typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? payload.timeoutMs : 5 * 6e4;
|
|
5159
|
+
const finalError = approvalRequested ? {
|
|
5160
|
+
code: "awaiting_human_approval",
|
|
5161
|
+
message: "Agent requested human approval and is suspended pending decision. Cloud will redispatch with `approval.decided` once the human responds."
|
|
5162
|
+
} : reaperAborted ? {
|
|
5163
|
+
code: "daemon_task_timeout",
|
|
5164
|
+
message: `Daemon-side reaper aborted after ${reaperLimitMs}ms inactivity (no progress events from adapter)`
|
|
5165
|
+
} : result.error;
|
|
4492
5166
|
reply = {
|
|
4493
5167
|
taskId,
|
|
5168
|
+
// When approval is pending we keep ok=false (the run did not produce a
|
|
5169
|
+
// completion-style output) but the cloud-side handler treats the
|
|
5170
|
+
// `awaiting_human_approval` code as non-terminal — task stays alive,
|
|
5171
|
+
// no red failure pill, and the next dispatch (after the human decides)
|
|
5172
|
+
// continues normally.
|
|
4494
5173
|
ok: result.ok,
|
|
4495
5174
|
output: result.output,
|
|
4496
|
-
error:
|
|
4497
|
-
code: "daemon_task_timeout",
|
|
4498
|
-
message: `Daemon-side reaper aborted after ${reaperLimitMs}ms inactivity (no progress events from adapter)`
|
|
4499
|
-
} : result.error,
|
|
5175
|
+
error: finalError,
|
|
4500
5176
|
...collectedAssetIds.length > 0 ? { assetIds: collectedAssetIds } : {},
|
|
4501
5177
|
metrics: result.metrics,
|
|
4502
5178
|
...assetResolution.observability.length > 0 || urlObservations.length > 0 ? { assetObservability: [...assetResolution.observability, ...urlObservations] } : {}
|
|
@@ -4518,6 +5194,18 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4518
5194
|
}
|
|
4519
5195
|
};
|
|
4520
5196
|
} finally {
|
|
5197
|
+
try {
|
|
5198
|
+
recorderRef?.flush();
|
|
5199
|
+
} catch (err) {
|
|
5200
|
+
process.stderr.write(`[daemon] step recorder flush failed task=${taskId}: ${err.message}
|
|
5201
|
+
`);
|
|
5202
|
+
}
|
|
5203
|
+
try {
|
|
5204
|
+
heartbeatRef?.stop();
|
|
5205
|
+
} catch (err) {
|
|
5206
|
+
process.stderr.write(`[daemon] heartbeat stop failed task=${taskId}: ${err.message}
|
|
5207
|
+
`);
|
|
5208
|
+
}
|
|
4521
5209
|
for (const hash of new Set(resolvedHashes)) {
|
|
4522
5210
|
try {
|
|
4523
5211
|
deps.assetCache.unpin(hash);
|
|
@@ -4594,6 +5282,10 @@ function isTextLikeMime(mime) {
|
|
|
4594
5282
|
if (m.endsWith("+json") || m.endsWith("+xml") || m.endsWith("+csv")) return true;
|
|
4595
5283
|
return false;
|
|
4596
5284
|
}
|
|
5285
|
+
function isImageMime(mime) {
|
|
5286
|
+
if (!mime) return false;
|
|
5287
|
+
return mime.toLowerCase().split(";")[0].trim().startsWith("image/");
|
|
5288
|
+
}
|
|
4597
5289
|
var HASH_REF_RE = /(?:^|\s)#([^\s#]+)/g;
|
|
4598
5290
|
var HEX_COLOR_RE = /^[0-9a-fA-F]{3,8}$/;
|
|
4599
5291
|
var FILE_EXT_RE = /\.[a-zA-Z0-9]{1,10}$/;
|
|
@@ -4672,8 +5364,34 @@ async function resolveHashRefs(prompt, assetIndex, cloud) {
|
|
|
4672
5364
|
}
|
|
4673
5365
|
return { text: result, resolutions };
|
|
4674
5366
|
}
|
|
4675
|
-
|
|
4676
|
-
|
|
5367
|
+
var ASSET_BASE64_INLINE_MAX_BYTES = 10 * 1024 * 1024;
|
|
5368
|
+
async function probeCdnReachable(cdnUrl, timeoutMs = 1500) {
|
|
5369
|
+
try {
|
|
5370
|
+
const u = new URL(cdnUrl);
|
|
5371
|
+
if (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname.startsWith("192.168.")) {
|
|
5372
|
+
return false;
|
|
5373
|
+
}
|
|
5374
|
+
} catch {
|
|
5375
|
+
return false;
|
|
5376
|
+
}
|
|
5377
|
+
try {
|
|
5378
|
+
const r = await fetch(cdnUrl, {
|
|
5379
|
+
method: "HEAD",
|
|
5380
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
5381
|
+
});
|
|
5382
|
+
if (r.ok) return true;
|
|
5383
|
+
return false;
|
|
5384
|
+
} catch {
|
|
5385
|
+
return false;
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
async function resolveAssetRefs(refs, cache, taskId, visionAux) {
|
|
5389
|
+
const out = {
|
|
5390
|
+
promptBlocks: [],
|
|
5391
|
+
observability: [],
|
|
5392
|
+
pinnedHashes: [],
|
|
5393
|
+
resolvedRefs: []
|
|
5394
|
+
};
|
|
4677
5395
|
if (!refs || refs.length === 0) return out;
|
|
4678
5396
|
for (const ref of refs) {
|
|
4679
5397
|
let cached;
|
|
@@ -4702,7 +5420,7 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
4702
5420
|
const inlineCandidate = isTextLikeMime(effectiveMime);
|
|
4703
5421
|
if (inlineCandidate) {
|
|
4704
5422
|
try {
|
|
4705
|
-
const buf = (0,
|
|
5423
|
+
const buf = (0, import_node_fs9.readFileSync)(cached.localPath);
|
|
4706
5424
|
let strategy;
|
|
4707
5425
|
let body;
|
|
4708
5426
|
if (buf.byteLength <= ASSET_INLINE_FULL_MAX_BYTES) {
|
|
@@ -4738,7 +5456,42 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
4738
5456
|
continue;
|
|
4739
5457
|
}
|
|
4740
5458
|
}
|
|
4741
|
-
|
|
5459
|
+
let reachable = "unknown";
|
|
5460
|
+
let base64;
|
|
5461
|
+
if (ref.cdnUrl) {
|
|
5462
|
+
const ok2 = await probeCdnReachable(ref.cdnUrl);
|
|
5463
|
+
if (ok2) {
|
|
5464
|
+
reachable = "cdn";
|
|
5465
|
+
}
|
|
5466
|
+
}
|
|
5467
|
+
if (reachable !== "cdn") {
|
|
5468
|
+
try {
|
|
5469
|
+
const buf = (0, import_node_fs9.readFileSync)(cached.localPath);
|
|
5470
|
+
if (buf.byteLength <= ASSET_BASE64_INLINE_MAX_BYTES) {
|
|
5471
|
+
base64 = buf.toString("base64");
|
|
5472
|
+
reachable = "base64";
|
|
5473
|
+
} else {
|
|
5474
|
+
reachable = "unknown";
|
|
5475
|
+
}
|
|
5476
|
+
} catch (err) {
|
|
5477
|
+
const errMsg = `base64 fallback read failed: ${err.message}`;
|
|
5478
|
+
logAssetResolveFailure(taskId, ref, errMsg, effectiveMime);
|
|
5479
|
+
}
|
|
5480
|
+
}
|
|
5481
|
+
const resolvedRef = {
|
|
5482
|
+
...ref,
|
|
5483
|
+
mime: effectiveMime,
|
|
5484
|
+
localPath: cached.localPath,
|
|
5485
|
+
base64,
|
|
5486
|
+
reachable
|
|
5487
|
+
};
|
|
5488
|
+
out.resolvedRefs.push(resolvedRef);
|
|
5489
|
+
if (isImageMime(effectiveMime) && visionAux?.enabled) {
|
|
5490
|
+
const block = await describeImageAttachment(ref, effectiveMime, resolvedRef, visionAux, taskId);
|
|
5491
|
+
out.promptBlocks.push(block);
|
|
5492
|
+
} else {
|
|
5493
|
+
out.promptBlocks.push(formatAttachmentReminderBlock(ref, effectiveMime));
|
|
5494
|
+
}
|
|
4742
5495
|
out.observability.push({
|
|
4743
5496
|
assetId: ref.assetId,
|
|
4744
5497
|
contentHash: ref.contentHash,
|
|
@@ -4749,6 +5502,110 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
4749
5502
|
}
|
|
4750
5503
|
return out;
|
|
4751
5504
|
}
|
|
5505
|
+
async function describeImageAttachment(ref, mime, resolved, opts, taskId) {
|
|
5506
|
+
const cached = await readVisionAuxLocalCache(opts.cacheDir, ref.contentHash, mime);
|
|
5507
|
+
if (cached) return formatVisionAuxBlock(ref, mime, cached.description, true);
|
|
5508
|
+
const source = buildVisionAuxSource(resolved, mime);
|
|
5509
|
+
if (!source) {
|
|
5510
|
+
return formatVisionAuxUnavailableBlock(ref, mime, "no reachable URL or inline bytes");
|
|
5511
|
+
}
|
|
5512
|
+
try {
|
|
5513
|
+
const description = await callVisionAux(ref, mime, source, opts);
|
|
5514
|
+
await writeVisionAuxLocalCache(opts.cacheDir, ref.contentHash, mime, description);
|
|
5515
|
+
return formatVisionAuxBlock(ref, mime, description.description, false);
|
|
5516
|
+
} catch (err) {
|
|
5517
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5518
|
+
process.stderr.write(
|
|
5519
|
+
`[daemon] vision-aux failed task=${taskId ?? "unknown"} assetId=${ref.assetId} hash=${ref.contentHash}: ${message}
|
|
5520
|
+
`
|
|
5521
|
+
);
|
|
5522
|
+
return formatVisionAuxUnavailableBlock(ref, mime, message);
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
function buildVisionAuxSource(resolved, mime) {
|
|
5526
|
+
if (resolved.reachable === "cdn" && resolved.cdnUrl) return { kind: "url", url: resolved.cdnUrl };
|
|
5527
|
+
if (resolved.base64 && mime) return { kind: "data_url", url: `data:${mime};base64,${resolved.base64}` };
|
|
5528
|
+
return null;
|
|
5529
|
+
}
|
|
5530
|
+
async function callVisionAux(ref, mime, source, opts) {
|
|
5531
|
+
const secret = process.env.VISION_AUX_INTERNAL_SECRET || process.env.INTERNAL_API_SECRET || process.env.PRISMER_INTERNAL_SECRET;
|
|
5532
|
+
const headers = secret ? { "x-prismer-internal-secret": secret } : void 0;
|
|
5533
|
+
const res = await opts.cloud.request(
|
|
5534
|
+
"POST",
|
|
5535
|
+
"/api/internal/vision-aux/describe",
|
|
5536
|
+
{
|
|
5537
|
+
body: {
|
|
5538
|
+
assetId: ref.assetId,
|
|
5539
|
+
contentHash: ref.contentHash,
|
|
5540
|
+
mime: mime ?? ref.mime ?? "image/unknown",
|
|
5541
|
+
source
|
|
5542
|
+
},
|
|
5543
|
+
headers,
|
|
5544
|
+
timeoutMs: opts.timeoutMs ?? 12e3,
|
|
5545
|
+
signal: opts.signal
|
|
5546
|
+
}
|
|
5547
|
+
);
|
|
5548
|
+
const env = res.data;
|
|
5549
|
+
if (!res.ok || env?.ok === false || !env?.data?.description) {
|
|
5550
|
+
const message = res.error?.message || (typeof env?.error === "string" ? env.error : env?.error?.message) || env?.message || `HTTP ${res.status}`;
|
|
5551
|
+
throw new Error(message);
|
|
5552
|
+
}
|
|
5553
|
+
const now = /* @__PURE__ */ new Date();
|
|
5554
|
+
const ttlSec = Number.isFinite(env.data.cacheTtlSec) ? Number(env.data.cacheTtlSec) : 300;
|
|
5555
|
+
return {
|
|
5556
|
+
description: env.data.description,
|
|
5557
|
+
modelUsed: env.data.modelUsed || "unknown",
|
|
5558
|
+
provider: env.data.provider || "vision-aux",
|
|
5559
|
+
generatedAt: now.toISOString(),
|
|
5560
|
+
expiresAt: new Date(now.getTime() + Math.max(1, ttlSec) * 1e3).toISOString(),
|
|
5561
|
+
mime: mime ?? ref.mime ?? "image/unknown"
|
|
5562
|
+
};
|
|
5563
|
+
}
|
|
5564
|
+
async function readVisionAuxLocalCache(cacheDir, contentHash, mime) {
|
|
5565
|
+
if (!cacheDir) return null;
|
|
5566
|
+
const file = visionAuxCachePath(cacheDir, contentHash);
|
|
5567
|
+
try {
|
|
5568
|
+
const raw = await import_node_fs9.promises.readFile(file, "utf8");
|
|
5569
|
+
const parsed = parseVisionAuxCache(raw);
|
|
5570
|
+
if (!parsed) {
|
|
5571
|
+
await import_node_fs9.promises.unlink(file).catch(() => void 0);
|
|
5572
|
+
return null;
|
|
5573
|
+
}
|
|
5574
|
+
if (parsed.mime !== mime) return null;
|
|
5575
|
+
const expiresAt = Date.parse(parsed.expiresAt);
|
|
5576
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) return null;
|
|
5577
|
+
return parsed;
|
|
5578
|
+
} catch (err) {
|
|
5579
|
+
if (err.code !== "ENOENT") {
|
|
5580
|
+
await import_node_fs9.promises.unlink(file).catch(() => void 0);
|
|
5581
|
+
}
|
|
5582
|
+
return null;
|
|
5583
|
+
}
|
|
5584
|
+
}
|
|
5585
|
+
async function writeVisionAuxLocalCache(cacheDir, contentHash, mime, description) {
|
|
5586
|
+
if (!cacheDir || !mime) return;
|
|
5587
|
+
const file = visionAuxCachePath(cacheDir, contentHash);
|
|
5588
|
+
await import_node_fs9.promises.mkdir(path.dirname(file), { recursive: true });
|
|
5589
|
+
await import_node_fs9.promises.writeFile(file, JSON.stringify({ ...description, mime }, null, 2), "utf8");
|
|
5590
|
+
}
|
|
5591
|
+
function visionAuxCachePath(cacheDir, contentHash) {
|
|
5592
|
+
const safeHash = /^[a-f0-9]{32,128}$/i.test(contentHash) ? contentHash : createSafeCacheKey(contentHash);
|
|
5593
|
+
return path.join(cacheDir, `${safeHash}.json`);
|
|
5594
|
+
}
|
|
5595
|
+
function createSafeCacheKey(value) {
|
|
5596
|
+
return Buffer.from(value).toString("base64url").slice(0, 128) || "unknown";
|
|
5597
|
+
}
|
|
5598
|
+
function parseVisionAuxCache(raw) {
|
|
5599
|
+
try {
|
|
5600
|
+
const parsed = JSON.parse(raw);
|
|
5601
|
+
if (typeof parsed.description === "string" && typeof parsed.modelUsed === "string" && typeof parsed.provider === "string" && typeof parsed.generatedAt === "string" && typeof parsed.expiresAt === "string" && typeof parsed.mime === "string") {
|
|
5602
|
+
return parsed;
|
|
5603
|
+
}
|
|
5604
|
+
} catch {
|
|
5605
|
+
return null;
|
|
5606
|
+
}
|
|
5607
|
+
return null;
|
|
5608
|
+
}
|
|
4752
5609
|
function logAssetResolveFailure(taskId, ref, error, mime = ref.mime) {
|
|
4753
5610
|
const message = error.replace(/\s+/g, " ").slice(0, 500);
|
|
4754
5611
|
process.stderr.write(
|
|
@@ -4763,8 +5620,19 @@ function formatInlineAssetBlock(ref, mime, body, strategy) {
|
|
|
4763
5620
|
${body}
|
|
4764
5621
|
---`;
|
|
4765
5622
|
}
|
|
4766
|
-
function
|
|
4767
|
-
|
|
5623
|
+
function formatAttachmentReminderBlock(ref, mime) {
|
|
5624
|
+
const name = ref.filename ? ` name=${ref.filename}` : "";
|
|
5625
|
+
return `[Attached file] id=${ref.assetId} mime=${mime ?? "unknown"}${name} \u2014 the user uploaded this file; multimodal adapters receive the bytes directly.`;
|
|
5626
|
+
}
|
|
5627
|
+
function formatVisionAuxBlock(ref, mime, description, cached) {
|
|
5628
|
+
const name = ref.filename ? `: ${ref.filename}` : "";
|
|
5629
|
+
const cacheLabel = cached ? " local-cache" : "";
|
|
5630
|
+
return `[Image attachment${name}] id=${ref.assetId} mime=${mime ?? "unknown"}${cacheLabel}
|
|
5631
|
+
${description.trim()}`;
|
|
5632
|
+
}
|
|
5633
|
+
function formatVisionAuxUnavailableBlock(ref, mime, reason) {
|
|
5634
|
+
const name = ref.filename ? `: ${ref.filename}` : "";
|
|
5635
|
+
return `[Image attachment${name}; description unavailable due to vision-aux error] id=${ref.assetId} mime=${mime ?? "unknown"} reason=${reason.slice(0, 180)}`;
|
|
4768
5636
|
}
|
|
4769
5637
|
function formatErrorAssetBlock(ref, mime, error) {
|
|
4770
5638
|
return `[Attached file] id=${ref.assetId} mime=${mime ?? "unknown"} ERROR: ${error} \u2014 this file failed to load on the daemon side. Tell the user the specific error and ask them to re-upload or paste the content directly; do not guess at the cause.`;
|
|
@@ -5247,8 +6115,9 @@ var ServicePool = class {
|
|
|
5247
6115
|
// src/daemon/local-server.ts
|
|
5248
6116
|
var import_node_http = require("http");
|
|
5249
6117
|
var import_node_crypto4 = require("crypto");
|
|
5250
|
-
var
|
|
6118
|
+
var import_node_fs10 = require("fs");
|
|
5251
6119
|
var path2 = __toESM(require("path"), 1);
|
|
6120
|
+
var import_node_os5 = require("os");
|
|
5252
6121
|
var LocalServer = class {
|
|
5253
6122
|
constructor(opts) {
|
|
5254
6123
|
this.opts = opts;
|
|
@@ -5360,6 +6229,11 @@ var LocalServer = class {
|
|
|
5360
6229
|
void this.handleSnapshot(req, res);
|
|
5361
6230
|
return;
|
|
5362
6231
|
}
|
|
6232
|
+
const agentDumpMatch = /^\/v1\/agents\/([^/]+)\/dump-state$/.exec(url);
|
|
6233
|
+
if (req.method === "POST" && agentDumpMatch) {
|
|
6234
|
+
void this.handleAgentDumpState(req, res, decodeURIComponent(agentDumpMatch[1]));
|
|
6235
|
+
return;
|
|
6236
|
+
}
|
|
5363
6237
|
if (req.method === "POST" && url === "/local/asset/write") {
|
|
5364
6238
|
void this.handleAssetWrite(req, res);
|
|
5365
6239
|
return;
|
|
@@ -5410,7 +6284,7 @@ var LocalServer = class {
|
|
|
5410
6284
|
async handleSnapshot(req, res) {
|
|
5411
6285
|
const root = this.opts.snapshotRoot ?? "/workspace";
|
|
5412
6286
|
try {
|
|
5413
|
-
const st = await
|
|
6287
|
+
const st = await import_node_fs10.promises.stat(root);
|
|
5414
6288
|
if (!st.isDirectory()) {
|
|
5415
6289
|
respond(res, 400, { error: "snapshot_root_not_directory", rootPath: root });
|
|
5416
6290
|
return;
|
|
@@ -5437,6 +6311,54 @@ var LocalServer = class {
|
|
|
5437
6311
|
}
|
|
5438
6312
|
void req;
|
|
5439
6313
|
}
|
|
6314
|
+
/**
|
|
6315
|
+
* POST /v1/agents/:agentId/dump-state — agent-scoped daemon manifest.
|
|
6316
|
+
*
|
|
6317
|
+
* This is narrower than `/v1/snapshot`: it walks only the current agent's
|
|
6318
|
+
* profile/work dirs so cloud can attach the result to IMAgentSnapshot without
|
|
6319
|
+
* capturing unrelated agents hosted in the same daemon/container.
|
|
6320
|
+
*/
|
|
6321
|
+
async handleAgentDumpState(req, res, agentId) {
|
|
6322
|
+
const state = this.opts.getState();
|
|
6323
|
+
const agent = state.hostedAgents.find((item) => item.imUserId === agentId);
|
|
6324
|
+
if (!agent) {
|
|
6325
|
+
respond(res, 404, { error: "agent_not_hosted", agentId });
|
|
6326
|
+
return;
|
|
6327
|
+
}
|
|
6328
|
+
const roots = getAgentStateRoots(agent, this.opts.snapshotRoot ?? "/workspace");
|
|
6329
|
+
try {
|
|
6330
|
+
const manifests = await Promise.all(
|
|
6331
|
+
roots.map(async (root) => {
|
|
6332
|
+
const stat = await statOptional(root.rootPath);
|
|
6333
|
+
if (!stat) return { ...root, exists: false, files: [] };
|
|
6334
|
+
if (!stat.isDirectory()) return { ...root, exists: true, error: "not_directory", files: [] };
|
|
6335
|
+
return { ...root, exists: true, files: await walkAndDigest(root.rootPath, root.rootPath) };
|
|
6336
|
+
})
|
|
6337
|
+
);
|
|
6338
|
+
const files = manifests.flatMap(
|
|
6339
|
+
(manifest) => manifest.files.map((file) => ({
|
|
6340
|
+
...file,
|
|
6341
|
+
path: `${manifest.kind}/${file.path}`,
|
|
6342
|
+
rootKind: manifest.kind,
|
|
6343
|
+
rootPath: manifest.rootPath
|
|
6344
|
+
}))
|
|
6345
|
+
);
|
|
6346
|
+
respond(res, 200, {
|
|
6347
|
+
agentId,
|
|
6348
|
+
adapterName: agent.adapterName,
|
|
6349
|
+
dumpedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6350
|
+
roots: manifests,
|
|
6351
|
+
files
|
|
6352
|
+
});
|
|
6353
|
+
} catch (err) {
|
|
6354
|
+
respond(res, 500, {
|
|
6355
|
+
error: "agent_dump_state_failed",
|
|
6356
|
+
agentId,
|
|
6357
|
+
message: err instanceof Error ? err.message : String(err)
|
|
6358
|
+
});
|
|
6359
|
+
}
|
|
6360
|
+
void req;
|
|
6361
|
+
}
|
|
5440
6362
|
/**
|
|
5441
6363
|
* POST /local/asset/write — agent-gen adapter RPC.
|
|
5442
6364
|
*
|
|
@@ -5708,11 +6630,44 @@ function validateAgentDispatchRequest(body) {
|
|
|
5708
6630
|
if (payload.attachments != null && !Array.isArray(payload.attachments)) return "attachments must be an array";
|
|
5709
6631
|
return null;
|
|
5710
6632
|
}
|
|
6633
|
+
function getAgentStateRoots(agent, snapshotRoot) {
|
|
6634
|
+
const profileName = sanitizeProfileName(agent.name || agent.imUserId);
|
|
6635
|
+
const roots = [
|
|
6636
|
+
{
|
|
6637
|
+
kind: "workspace-agent",
|
|
6638
|
+
rootPath: path2.join(snapshotRoot, "agents", agent.imUserId)
|
|
6639
|
+
}
|
|
6640
|
+
];
|
|
6641
|
+
if (agent.adapterName === "hermes" || agent.adapterName === "claude-code") {
|
|
6642
|
+
roots.unshift({
|
|
6643
|
+
kind: "hermes-profile",
|
|
6644
|
+
rootPath: path2.join(process.env.HERMES_HOME || path2.join((0, import_node_os5.homedir)(), ".hermes"), "profiles", profileName)
|
|
6645
|
+
});
|
|
6646
|
+
}
|
|
6647
|
+
if (agent.adapterName === "openclaw") {
|
|
6648
|
+
roots.unshift({
|
|
6649
|
+
kind: "openclaw-profile",
|
|
6650
|
+
rootPath: path2.join(process.env.OPENCLAW_HOME || path2.join((0, import_node_os5.homedir)(), ".openclaw"), "profiles", profileName)
|
|
6651
|
+
});
|
|
6652
|
+
}
|
|
6653
|
+
return roots;
|
|
6654
|
+
}
|
|
6655
|
+
function sanitizeProfileName(value) {
|
|
6656
|
+
return value.replace(/[\\/]/g, "_").trim() || "default";
|
|
6657
|
+
}
|
|
6658
|
+
async function statOptional(rootPath) {
|
|
6659
|
+
try {
|
|
6660
|
+
return await import_node_fs10.promises.stat(rootPath);
|
|
6661
|
+
} catch (err) {
|
|
6662
|
+
if (err.code === "ENOENT") return null;
|
|
6663
|
+
throw err;
|
|
6664
|
+
}
|
|
6665
|
+
}
|
|
5711
6666
|
async function walkAndDigest(root, current) {
|
|
5712
6667
|
const out = [];
|
|
5713
6668
|
let entries;
|
|
5714
6669
|
try {
|
|
5715
|
-
entries = await
|
|
6670
|
+
entries = await import_node_fs10.promises.readdir(current);
|
|
5716
6671
|
} catch (err) {
|
|
5717
6672
|
if (err.code === "ENOENT") return out;
|
|
5718
6673
|
throw err;
|
|
@@ -5723,7 +6678,7 @@ async function walkAndDigest(root, current) {
|
|
|
5723
6678
|
const rel = path2.relative(root, full);
|
|
5724
6679
|
let st;
|
|
5725
6680
|
try {
|
|
5726
|
-
st = await
|
|
6681
|
+
st = await import_node_fs10.promises.stat(full);
|
|
5727
6682
|
} catch {
|
|
5728
6683
|
continue;
|
|
5729
6684
|
}
|
|
@@ -5734,7 +6689,7 @@ async function walkAndDigest(root, current) {
|
|
|
5734
6689
|
continue;
|
|
5735
6690
|
}
|
|
5736
6691
|
if (!st.isFile()) continue;
|
|
5737
|
-
const buf = await
|
|
6692
|
+
const buf = await import_node_fs10.promises.readFile(full);
|
|
5738
6693
|
const sha2563 = (0, import_node_crypto4.createHash)("sha256").update(buf).digest("hex");
|
|
5739
6694
|
out.push({
|
|
5740
6695
|
path: rel,
|
|
@@ -5749,9 +6704,9 @@ async function walkAndDigest(root, current) {
|
|
|
5749
6704
|
// src/daemon/runner.ts
|
|
5750
6705
|
var import_node_events3 = require("events");
|
|
5751
6706
|
var import_node_module2 = require("module");
|
|
5752
|
-
var
|
|
6707
|
+
var import_node_fs16 = require("fs");
|
|
5753
6708
|
var import_promises = require("fs/promises");
|
|
5754
|
-
var
|
|
6709
|
+
var import_node_os8 = require("os");
|
|
5755
6710
|
var import_node_path11 = require("path");
|
|
5756
6711
|
|
|
5757
6712
|
// src/adapters/claude-code/index.ts
|
|
@@ -7849,7 +8804,7 @@ function parsePrismerUri(uri) {
|
|
|
7849
8804
|
}
|
|
7850
8805
|
|
|
7851
8806
|
// src/daemon/asset/metadata-index.ts
|
|
7852
|
-
var
|
|
8807
|
+
var import_node_fs11 = require("fs");
|
|
7853
8808
|
var import_node_path9 = require("path");
|
|
7854
8809
|
var DEFAULT_LIMIT = 8;
|
|
7855
8810
|
var PULL_PAGE_SIZE = 500;
|
|
@@ -7878,16 +8833,16 @@ var AssetMetadataIndex = class {
|
|
|
7878
8833
|
this.db = opts.db;
|
|
7879
8834
|
this.cloud = opts.cloud;
|
|
7880
8835
|
this.workspaceId = opts.workspaceId;
|
|
7881
|
-
if (!(0,
|
|
7882
|
-
(0,
|
|
8836
|
+
if (!(0, import_node_fs11.existsSync)(opts.workspaceStateDir)) {
|
|
8837
|
+
(0, import_node_fs11.mkdirSync)(opts.workspaceStateDir, { recursive: true });
|
|
7883
8838
|
}
|
|
7884
8839
|
this.cursorPath = (0, import_node_path9.join)(opts.workspaceStateDir, "asset-metadata-cursor.json");
|
|
7885
8840
|
}
|
|
7886
8841
|
/** Persisted cursor for this workspace, or 0 on first run / corrupted file. */
|
|
7887
8842
|
readCursor() {
|
|
7888
|
-
if (!(0,
|
|
8843
|
+
if (!(0, import_node_fs11.existsSync)(this.cursorPath)) return 0;
|
|
7889
8844
|
try {
|
|
7890
|
-
const parsed = JSON.parse((0,
|
|
8845
|
+
const parsed = JSON.parse((0, import_node_fs11.readFileSync)(this.cursorPath, "utf8"));
|
|
7891
8846
|
if (parsed.workspaceId !== this.workspaceId) return 0;
|
|
7892
8847
|
return parsed.cursor;
|
|
7893
8848
|
} catch {
|
|
@@ -7900,7 +8855,7 @@ var AssetMetadataIndex = class {
|
|
|
7900
8855
|
cursor,
|
|
7901
8856
|
writtenAt: Date.now()
|
|
7902
8857
|
};
|
|
7903
|
-
(0,
|
|
8858
|
+
(0, import_node_fs11.writeFileSync)(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
7904
8859
|
}
|
|
7905
8860
|
/**
|
|
7906
8861
|
* Pull incremental asset metadata changes since the persisted cursor and
|
|
@@ -8017,7 +8972,7 @@ var AssetMetadataIndex = class {
|
|
|
8017
8972
|
};
|
|
8018
8973
|
|
|
8019
8974
|
// src/daemon/asset/rpc.ts
|
|
8020
|
-
var
|
|
8975
|
+
var import_node_fs12 = require("fs");
|
|
8021
8976
|
var ASSET_PATH_PREFIX = "/local/asset/";
|
|
8022
8977
|
var MAX_READ_BYTES = 256 * 1024;
|
|
8023
8978
|
var MAX_SEARCH_BYTES = 1024 * 1024;
|
|
@@ -8192,16 +9147,16 @@ function isTextLike(row) {
|
|
|
8192
9147
|
);
|
|
8193
9148
|
}
|
|
8194
9149
|
function readFileRange(path9, offset, length) {
|
|
8195
|
-
const sizeBytes = (0,
|
|
9150
|
+
const sizeBytes = (0, import_node_fs12.statSync)(path9).size;
|
|
8196
9151
|
if (offset >= sizeBytes) return { buffer: Buffer.alloc(0), bytesRead: 0, sizeBytes };
|
|
8197
|
-
const fd = (0,
|
|
9152
|
+
const fd = (0, import_node_fs12.openSync)(path9, "r");
|
|
8198
9153
|
try {
|
|
8199
9154
|
const target = Math.min(length, sizeBytes - offset);
|
|
8200
9155
|
const buffer = Buffer.alloc(target);
|
|
8201
|
-
const bytesRead = (0,
|
|
9156
|
+
const bytesRead = (0, import_node_fs12.readSync)(fd, buffer, 0, target, offset);
|
|
8202
9157
|
return { buffer: buffer.subarray(0, bytesRead), bytesRead, sizeBytes };
|
|
8203
9158
|
} finally {
|
|
8204
|
-
(0,
|
|
9159
|
+
(0, import_node_fs12.closeSync)(fd);
|
|
8205
9160
|
}
|
|
8206
9161
|
}
|
|
8207
9162
|
function respond3(res, status, body) {
|
|
@@ -8226,8 +9181,8 @@ async function readJson3(req) {
|
|
|
8226
9181
|
}
|
|
8227
9182
|
|
|
8228
9183
|
// src/daemon/asset/origin/drop-folder.ts
|
|
8229
|
-
var
|
|
8230
|
-
var
|
|
9184
|
+
var import_node_fs13 = require("fs");
|
|
9185
|
+
var import_node_os6 = require("os");
|
|
8231
9186
|
var path5 = __toESM(require("path"), 1);
|
|
8232
9187
|
var DropFolderAdapter = class {
|
|
8233
9188
|
kind = "drop-folder";
|
|
@@ -8235,7 +9190,7 @@ var DropFolderAdapter = class {
|
|
|
8235
9190
|
rootDir;
|
|
8236
9191
|
constructor(opts = {}) {
|
|
8237
9192
|
this.workspaceDir = opts.workspaceDir;
|
|
8238
|
-
this.rootDir = opts.rootDir ?? path5.join(opts.homeDir ?? (0,
|
|
9193
|
+
this.rootDir = opts.rootDir ?? path5.join(opts.homeDir ?? (0, import_node_os6.homedir)(), ".prismer");
|
|
8239
9194
|
}
|
|
8240
9195
|
/**
|
|
8241
9196
|
* Long-lived async iterable wrapping `scanOnce` on a 1s polling interval.
|
|
@@ -8245,7 +9200,7 @@ var DropFolderAdapter = class {
|
|
|
8245
9200
|
while (true) {
|
|
8246
9201
|
const batch = await this.scanOnce(workspaceId);
|
|
8247
9202
|
for (const obs of batch) yield obs;
|
|
8248
|
-
await
|
|
9203
|
+
await sleep2(1e3);
|
|
8249
9204
|
}
|
|
8250
9205
|
}
|
|
8251
9206
|
/**
|
|
@@ -8259,7 +9214,7 @@ var DropFolderAdapter = class {
|
|
|
8259
9214
|
const drop = this.dropDir(workspaceId);
|
|
8260
9215
|
let entries = [];
|
|
8261
9216
|
try {
|
|
8262
|
-
entries = await
|
|
9217
|
+
entries = await import_node_fs13.promises.readdir(drop);
|
|
8263
9218
|
} catch (err) {
|
|
8264
9219
|
if (err.code === "ENOENT") return [];
|
|
8265
9220
|
throw err;
|
|
@@ -8268,12 +9223,12 @@ var DropFolderAdapter = class {
|
|
|
8268
9223
|
for (const name of entries) {
|
|
8269
9224
|
if (name.startsWith(".")) continue;
|
|
8270
9225
|
const fullPath = path5.join(drop, name);
|
|
8271
|
-
const stat = await
|
|
9226
|
+
const stat = await import_node_fs13.promises.lstat(fullPath).catch(() => null);
|
|
8272
9227
|
if (!stat) continue;
|
|
8273
9228
|
if (stat.isSymbolicLink()) continue;
|
|
8274
9229
|
if (stat.isDirectory()) {
|
|
8275
9230
|
for (const nested of await walk(fullPath)) {
|
|
8276
|
-
const nstat = await
|
|
9231
|
+
const nstat = await import_node_fs13.promises.lstat(nested).catch(() => null);
|
|
8277
9232
|
if (!nstat || !nstat.isFile()) continue;
|
|
8278
9233
|
out.push(makeObservation(workspaceId, nested, drop, nstat.size, Math.floor(nstat.mtimeMs)));
|
|
8279
9234
|
}
|
|
@@ -8298,7 +9253,7 @@ var DropFolderAdapter = class {
|
|
|
8298
9253
|
}
|
|
8299
9254
|
async fetch(obs) {
|
|
8300
9255
|
const detail = obs.detail;
|
|
8301
|
-
const bytes = await
|
|
9256
|
+
const bytes = await import_node_fs13.promises.readFile(detail.path);
|
|
8302
9257
|
return {
|
|
8303
9258
|
bytes,
|
|
8304
9259
|
mime: mimeFromExtension(detail.path),
|
|
@@ -8320,8 +9275,8 @@ var DropFolderAdapter = class {
|
|
|
8320
9275
|
const rel = typeof detail.watchDir === "string" ? path5.relative(detail.watchDir, detail.path) : path5.basename(detail.path);
|
|
8321
9276
|
const safeRel = rel && !rel.startsWith("..") && !path5.isAbsolute(rel) ? rel : path5.basename(detail.path);
|
|
8322
9277
|
const destPath = path5.join(destDir, safeRel);
|
|
8323
|
-
await
|
|
8324
|
-
await
|
|
9278
|
+
await import_node_fs13.promises.mkdir(path5.dirname(destPath), { recursive: true });
|
|
9279
|
+
await import_node_fs13.promises.rename(detail.path, destPath);
|
|
8325
9280
|
} catch (err) {
|
|
8326
9281
|
if (err.code === "ENOENT") return;
|
|
8327
9282
|
throw err;
|
|
@@ -8349,11 +9304,11 @@ function makeObservation(workspaceId, filePath, watchDir, size, mtime) {
|
|
|
8349
9304
|
}
|
|
8350
9305
|
async function walk(root) {
|
|
8351
9306
|
const out = [];
|
|
8352
|
-
const entries = await
|
|
9307
|
+
const entries = await import_node_fs13.promises.readdir(root).catch(() => []);
|
|
8353
9308
|
for (const name of entries) {
|
|
8354
9309
|
if (name.startsWith(".")) continue;
|
|
8355
9310
|
const full = path5.join(root, name);
|
|
8356
|
-
const stat = await
|
|
9311
|
+
const stat = await import_node_fs13.promises.lstat(full).catch(() => null);
|
|
8357
9312
|
if (!stat) continue;
|
|
8358
9313
|
if (stat.isSymbolicLink()) continue;
|
|
8359
9314
|
if (stat.isDirectory()) {
|
|
@@ -8394,7 +9349,7 @@ function mimeFromExtension(p) {
|
|
|
8394
9349
|
const ext = path5.extname(p).toLowerCase();
|
|
8395
9350
|
return MIME_BY_EXT[ext] ?? null;
|
|
8396
9351
|
}
|
|
8397
|
-
function
|
|
9352
|
+
function sleep2(ms) {
|
|
8398
9353
|
return new Promise((r) => setTimeout(r, ms));
|
|
8399
9354
|
}
|
|
8400
9355
|
|
|
@@ -8949,7 +9904,7 @@ function stringValue(obj, key) {
|
|
|
8949
9904
|
}
|
|
8950
9905
|
|
|
8951
9906
|
// src/daemon/outbox-watcher.ts
|
|
8952
|
-
var
|
|
9907
|
+
var import_node_fs14 = require("fs");
|
|
8953
9908
|
var path7 = __toESM(require("path"), 1);
|
|
8954
9909
|
|
|
8955
9910
|
// src/daemon/asset/agent-output-policy.ts
|
|
@@ -9105,9 +10060,120 @@ function extensionOf(filename) {
|
|
|
9105
10060
|
return idx >= 0 ? name.slice(idx) : "";
|
|
9106
10061
|
}
|
|
9107
10062
|
|
|
10063
|
+
// src/daemon/asset/magic-bytes.ts
|
|
10064
|
+
var PDF_SIG = Buffer.from("%PDF-", "ascii");
|
|
10065
|
+
var PNG_SIG = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
10066
|
+
var JPEG_SIG = Buffer.from([255, 216, 255]);
|
|
10067
|
+
var GIF87 = Buffer.from("GIF87a", "ascii");
|
|
10068
|
+
var GIF89 = Buffer.from("GIF89a", "ascii");
|
|
10069
|
+
var ZIP_LOCAL = Buffer.from([80, 75, 3, 4]);
|
|
10070
|
+
var ZIP_EMPTY = Buffer.from([80, 75, 5, 6]);
|
|
10071
|
+
var ZIP_SPANNED = Buffer.from([80, 75, 7, 8]);
|
|
10072
|
+
function detectMagicBytes(bytes) {
|
|
10073
|
+
const head = bytes.subarray(0, Math.min(bytes.length, 4096));
|
|
10074
|
+
if (head.length === 0) return { detected: null, mime: null };
|
|
10075
|
+
if (head.length >= PDF_SIG.length && head.subarray(0, PDF_SIG.length).equals(PDF_SIG)) {
|
|
10076
|
+
return { detected: "pdf", mime: "application/pdf" };
|
|
10077
|
+
}
|
|
10078
|
+
if (head.length >= PNG_SIG.length && head.subarray(0, PNG_SIG.length).equals(PNG_SIG)) {
|
|
10079
|
+
return { detected: "png", mime: "image/png" };
|
|
10080
|
+
}
|
|
10081
|
+
if (head.length >= JPEG_SIG.length && head.subarray(0, JPEG_SIG.length).equals(JPEG_SIG)) {
|
|
10082
|
+
return { detected: "jpeg", mime: "image/jpeg" };
|
|
10083
|
+
}
|
|
10084
|
+
if (head.length >= GIF87.length && head.subarray(0, GIF87.length).equals(GIF87) || head.length >= GIF89.length && head.subarray(0, GIF89.length).equals(GIF89)) {
|
|
10085
|
+
return { detected: "gif", mime: "image/gif" };
|
|
10086
|
+
}
|
|
10087
|
+
if (head.length >= 4 && (head.subarray(0, 4).equals(ZIP_LOCAL) || head.subarray(0, 4).equals(ZIP_EMPTY) || head.subarray(0, 4).equals(ZIP_SPANNED))) {
|
|
10088
|
+
return { detected: "zip", mime: "application/zip" };
|
|
10089
|
+
}
|
|
10090
|
+
const text = head.toString("utf8");
|
|
10091
|
+
const trimmed = text.trimStart();
|
|
10092
|
+
if (/^<\?xml[^>]*>\s*<svg[\s>]/i.test(trimmed) || /^<svg[\s>]/i.test(trimmed)) {
|
|
10093
|
+
return { detected: "svg", mime: "image/svg+xml" };
|
|
10094
|
+
}
|
|
10095
|
+
if (/^<(!doctype html|html[\s>])/i.test(trimmed)) {
|
|
10096
|
+
return { detected: "html", mime: "text/html" };
|
|
10097
|
+
}
|
|
10098
|
+
if (/^[{[]/.test(trimmed)) {
|
|
10099
|
+
return { detected: "json", mime: "application/json" };
|
|
10100
|
+
}
|
|
10101
|
+
if (looksLikePrintableText(head)) {
|
|
10102
|
+
return { detected: "markdown-or-text", mime: "text/plain" };
|
|
10103
|
+
}
|
|
10104
|
+
return { detected: null, mime: null };
|
|
10105
|
+
}
|
|
10106
|
+
var ZIP_DERIVED_MIMES = /* @__PURE__ */ new Set([
|
|
10107
|
+
"application/zip",
|
|
10108
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
10109
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
10110
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
10111
|
+
"application/vnd.oasis.opendocument.text",
|
|
10112
|
+
"application/vnd.oasis.opendocument.spreadsheet",
|
|
10113
|
+
"application/vnd.oasis.opendocument.presentation",
|
|
10114
|
+
"application/epub+zip",
|
|
10115
|
+
"application/java-archive"
|
|
10116
|
+
]);
|
|
10117
|
+
function mimeMismatchReason(detected, declared) {
|
|
10118
|
+
if (detected.detected === null) return null;
|
|
10119
|
+
const dec = (declared ?? "").toLowerCase().split(";")[0]?.trim() ?? "";
|
|
10120
|
+
if (!dec || dec === "application/octet-stream") return null;
|
|
10121
|
+
switch (detected.detected) {
|
|
10122
|
+
case "pdf":
|
|
10123
|
+
return dec === "application/pdf" ? null : `declared ${dec} but content is application/pdf`;
|
|
10124
|
+
case "png":
|
|
10125
|
+
return dec === "image/png" ? null : `declared ${dec} but content is image/png`;
|
|
10126
|
+
case "jpeg":
|
|
10127
|
+
return dec === "image/jpeg" || dec === "image/jpg" ? null : `declared ${dec} but content is image/jpeg`;
|
|
10128
|
+
case "gif":
|
|
10129
|
+
return dec === "image/gif" ? null : `declared ${dec} but content is image/gif`;
|
|
10130
|
+
case "zip":
|
|
10131
|
+
if (ZIP_DERIVED_MIMES.has(dec)) return null;
|
|
10132
|
+
return `declared ${dec} but content is a ZIP-container (application/zip family)`;
|
|
10133
|
+
case "svg":
|
|
10134
|
+
if (dec === "image/svg+xml" || dec.startsWith("text/")) return null;
|
|
10135
|
+
return `declared ${dec} but content is image/svg+xml`;
|
|
10136
|
+
case "html":
|
|
10137
|
+
if (dec === "text/html" || dec === "application/xhtml+xml" || dec.startsWith("text/")) return null;
|
|
10138
|
+
if (dec === "application/pdf" || dec.startsWith("image/") || dec === "application/zip") {
|
|
10139
|
+
return `declared ${dec} but content is text/html`;
|
|
10140
|
+
}
|
|
10141
|
+
return null;
|
|
10142
|
+
case "json":
|
|
10143
|
+
if (dec === "application/json" || dec.startsWith("text/")) return null;
|
|
10144
|
+
if (dec === "application/pdf" || dec.startsWith("image/") || dec === "application/zip") {
|
|
10145
|
+
return `declared ${dec} but content is application/json`;
|
|
10146
|
+
}
|
|
10147
|
+
return null;
|
|
10148
|
+
case "markdown-or-text":
|
|
10149
|
+
if (dec === "application/pdf" || dec.startsWith("image/") || dec === "application/zip") {
|
|
10150
|
+
return `declared ${dec} but content is text (not a real ${dec} payload)`;
|
|
10151
|
+
}
|
|
10152
|
+
return null;
|
|
10153
|
+
default:
|
|
10154
|
+
return null;
|
|
10155
|
+
}
|
|
10156
|
+
}
|
|
10157
|
+
function looksLikePrintableText(buf) {
|
|
10158
|
+
if (buf.length === 0) return false;
|
|
10159
|
+
let printable = 0;
|
|
10160
|
+
let total = 0;
|
|
10161
|
+
for (let i = 0; i < buf.length; i++) {
|
|
10162
|
+
const b = buf[i];
|
|
10163
|
+
if (b === 0) return false;
|
|
10164
|
+
total++;
|
|
10165
|
+
if (b >= 32 && b <= 126 || b === 9 || b === 10 || b === 13 || // utf-8 lead bytes
|
|
10166
|
+
b >= 128) {
|
|
10167
|
+
printable++;
|
|
10168
|
+
}
|
|
10169
|
+
}
|
|
10170
|
+
return total > 0 && printable / total >= 0.95;
|
|
10171
|
+
}
|
|
10172
|
+
|
|
9108
10173
|
// src/daemon/outbox-watcher.ts
|
|
9109
10174
|
var DEFAULT_INTERVAL_MS = 2e3;
|
|
9110
10175
|
var RESERVED_SUBDIR = "_uploaded";
|
|
10176
|
+
var REJECTED_SUBDIR = "_rejected";
|
|
9111
10177
|
var OutboxWatcher = class {
|
|
9112
10178
|
constructor(opts) {
|
|
9113
10179
|
this.opts = opts;
|
|
@@ -9272,7 +10338,7 @@ var OutboxWatcher = class {
|
|
|
9272
10338
|
async scanDir(dir, kind, task) {
|
|
9273
10339
|
let entries = [];
|
|
9274
10340
|
try {
|
|
9275
|
-
entries = await
|
|
10341
|
+
entries = await import_node_fs14.promises.readdir(dir);
|
|
9276
10342
|
} catch (err) {
|
|
9277
10343
|
const code = err.code;
|
|
9278
10344
|
if (code === "ENOENT") return;
|
|
@@ -9282,10 +10348,11 @@ var OutboxWatcher = class {
|
|
|
9282
10348
|
for (const name of entries) {
|
|
9283
10349
|
if (name.startsWith(".")) continue;
|
|
9284
10350
|
if (name === RESERVED_SUBDIR) continue;
|
|
10351
|
+
if (name === REJECTED_SUBDIR) continue;
|
|
9285
10352
|
const full = path7.join(dir, name);
|
|
9286
10353
|
let st;
|
|
9287
10354
|
try {
|
|
9288
|
-
st = await
|
|
10355
|
+
st = await import_node_fs14.promises.stat(full);
|
|
9289
10356
|
} catch {
|
|
9290
10357
|
continue;
|
|
9291
10358
|
}
|
|
@@ -9296,10 +10363,77 @@ var OutboxWatcher = class {
|
|
|
9296
10363
|
await this.upload(full, st, kind, task);
|
|
9297
10364
|
this.uploaded.add(key);
|
|
9298
10365
|
} catch (err) {
|
|
9299
|
-
|
|
10366
|
+
const msg = err.message;
|
|
10367
|
+
this.log("warn", `upload ${full} failed: ${msg}`);
|
|
10368
|
+
if (/\bMIME_MISMATCH\b/i.test(msg) || /\bmime mismatch\b/i.test(msg)) {
|
|
10369
|
+
await this.quarantine(dir, full).catch(
|
|
10370
|
+
(moveErr) => this.log("warn", `quarantine ${full} failed: ${moveErr.message}`)
|
|
10371
|
+
);
|
|
10372
|
+
this.uploaded.add(key);
|
|
10373
|
+
await this.reportMimeMismatchToCloud(task, full, msg).catch(
|
|
10374
|
+
(reportErr) => this.log("warn", `report MIME_MISMATCH failed: ${reportErr.message}`)
|
|
10375
|
+
);
|
|
10376
|
+
}
|
|
9300
10377
|
}
|
|
9301
10378
|
}
|
|
9302
10379
|
}
|
|
10380
|
+
/**
|
|
10381
|
+
* F2B — report an OUTBOX_MIME_MISMATCH task event to cloud so the task log
|
|
10382
|
+
* gains an auditable record of the rejection. Without this, the failure is
|
|
10383
|
+
* silent (file quarantined locally, agent unaware) and the next dispatch
|
|
10384
|
+
* loops on the same bad strategy.
|
|
10385
|
+
*
|
|
10386
|
+
* Best-effort: every catch site swallows errors. We never want this
|
|
10387
|
+
* upstream call to block local quarantine or trigger a retry loop, because
|
|
10388
|
+
* the reject decision is already final by the time we get here.
|
|
10389
|
+
*
|
|
10390
|
+
* Endpoint contract: POST /api/im/tasks/:id/event
|
|
10391
|
+
* { code: 'OUTBOX_MIME_MISMATCH', payload: { file, daemonError }, message }
|
|
10392
|
+
* Only callable when `task?.taskId` is known — pre-dispatch outbox files
|
|
10393
|
+
* (no active task) are quarantined silently as before.
|
|
10394
|
+
*/
|
|
10395
|
+
async reportMimeMismatchToCloud(task, filePath, daemonErrorMessage) {
|
|
10396
|
+
if (!task?.taskId) return;
|
|
10397
|
+
const fileName = path7.basename(filePath);
|
|
10398
|
+
const reasonMatch = daemonErrorMessage.match(/MIME_MISMATCH[^:]*:\s*(.+)$/i);
|
|
10399
|
+
const reason = reasonMatch?.[1] ? reasonMatch[1].trim() : daemonErrorMessage;
|
|
10400
|
+
const body = {
|
|
10401
|
+
code: "OUTBOX_MIME_MISMATCH",
|
|
10402
|
+
payload: {
|
|
10403
|
+
file: fileName,
|
|
10404
|
+
reason,
|
|
10405
|
+
daemonError: daemonErrorMessage
|
|
10406
|
+
},
|
|
10407
|
+
message: `Your output file ${fileName} was rejected because its bytes do not match the claimed format. Use a real library (e.g. reportlab for PDF, openpyxl for XLSX) instead of renaming an extension.`
|
|
10408
|
+
};
|
|
10409
|
+
const res = await this.opts.cloud.request("POST", `/api/im/tasks/${encodeURIComponent(task.taskId)}/event`, {
|
|
10410
|
+
body
|
|
10411
|
+
});
|
|
10412
|
+
if (!res.ok) {
|
|
10413
|
+
throw new Error(`cloud rejected event: ${res.status} ${res.error?.code ?? ""} ${res.error?.message ?? ""}`);
|
|
10414
|
+
}
|
|
10415
|
+
}
|
|
10416
|
+
/**
|
|
10417
|
+
* Move a file under `<dir>/_rejected/` so it stops being a scan candidate
|
|
10418
|
+
* (the loop skips the reserved subdir) and operators can find the
|
|
10419
|
+
* offending artifact. Filename collision is rare per task; on collision
|
|
10420
|
+
* we append a millisecond suffix to keep the original observable.
|
|
10421
|
+
*/
|
|
10422
|
+
async quarantine(dir, full) {
|
|
10423
|
+
const rejectedDir = path7.join(dir, REJECTED_SUBDIR);
|
|
10424
|
+
await import_node_fs14.promises.mkdir(rejectedDir, { recursive: true });
|
|
10425
|
+
const base = path7.basename(full);
|
|
10426
|
+
let dest = path7.join(rejectedDir, base);
|
|
10427
|
+
try {
|
|
10428
|
+
await import_node_fs14.promises.access(dest);
|
|
10429
|
+
const ext = path7.extname(base);
|
|
10430
|
+
const stem = base.slice(0, base.length - ext.length);
|
|
10431
|
+
dest = path7.join(rejectedDir, `${stem}.${Date.now()}${ext}`);
|
|
10432
|
+
} catch {
|
|
10433
|
+
}
|
|
10434
|
+
await import_node_fs14.promises.rename(full, dest);
|
|
10435
|
+
this.log("warn", `quarantined ${full} \u2192 ${dest}`);
|
|
10436
|
+
}
|
|
9303
10437
|
async upload(filePath, st, kind, task) {
|
|
9304
10438
|
const wsId = this.opts.workspaceId();
|
|
9305
10439
|
if (!wsId) {
|
|
@@ -9310,13 +10444,18 @@ var OutboxWatcher = class {
|
|
|
9310
10444
|
this.log("warn", `skip ${filePath}: no active taskId (no dispatch/handoff received yet)`);
|
|
9311
10445
|
return;
|
|
9312
10446
|
}
|
|
9313
|
-
const bytes = await
|
|
10447
|
+
const bytes = await import_node_fs14.promises.readFile(filePath);
|
|
9314
10448
|
const fileName = path7.basename(filePath);
|
|
9315
10449
|
const inferredMime = inferAgentOutputMime(fileName);
|
|
9316
10450
|
const policy = validateAgentOutputAsset({ filename: fileName, mime: inferredMime, sizeBytes: bytes.length });
|
|
9317
10451
|
if (!policy.ok) {
|
|
9318
10452
|
throw new Error(`agent output rejected: ${policy.reason}`);
|
|
9319
10453
|
}
|
|
10454
|
+
const detection = detectMagicBytes(bytes);
|
|
10455
|
+
const reason = mimeMismatchReason(detection, policy.mime);
|
|
10456
|
+
if (reason) {
|
|
10457
|
+
throw new Error(`MIME_MISMATCH for ${fileName}: ${reason}`);
|
|
10458
|
+
}
|
|
9320
10459
|
const metadata = {
|
|
9321
10460
|
taskId: task.taskId,
|
|
9322
10461
|
...task.adapter ? { adapter: task.adapter } : {}
|
|
@@ -9352,7 +10491,7 @@ var OutboxWatcher = class {
|
|
|
9352
10491
|
|
|
9353
10492
|
// src/daemon/shell-executor.ts
|
|
9354
10493
|
var import_node_child_process4 = require("child_process");
|
|
9355
|
-
var
|
|
10494
|
+
var import_node_fs15 = require("fs");
|
|
9356
10495
|
var import_node_path10 = require("path");
|
|
9357
10496
|
var DEFAULT_OUTPUT_LIMIT = 256 * 1024;
|
|
9358
10497
|
var DEFAULT_TIMEOUT = 6e4;
|
|
@@ -9389,7 +10528,7 @@ async function executeShellDispatch(payload, deps) {
|
|
|
9389
10528
|
const command = readCommand(payload, execution);
|
|
9390
10529
|
if (!command.trim()) return fail(payload.taskId, "shell_command_required", "Shell command is required");
|
|
9391
10530
|
const cwd = resolveCwd(execution.cwd, deps.config.defaultCwd);
|
|
9392
|
-
if (!(0,
|
|
10531
|
+
if (!(0, import_node_fs15.existsSync)(cwd)) return fail(payload.taskId, "shell_cwd_missing", `cwd does not exist: ${cwd}`);
|
|
9393
10532
|
const timeoutMs = Math.min(
|
|
9394
10533
|
typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? payload.timeoutMs : deps.config.maxTimeoutMs,
|
|
9395
10534
|
deps.config.maxTimeoutMs
|
|
@@ -9481,45 +10620,391 @@ async function executeShellDispatch(payload, deps) {
|
|
|
9481
10620
|
});
|
|
9482
10621
|
});
|
|
9483
10622
|
}
|
|
9484
|
-
function readExecutionMetadata(payload) {
|
|
9485
|
-
const execution = payload.metadata?.execution;
|
|
9486
|
-
return execution && typeof execution === "object" && !Array.isArray(execution) ? execution : {};
|
|
10623
|
+
function readExecutionMetadata(payload) {
|
|
10624
|
+
const execution = payload.metadata?.execution;
|
|
10625
|
+
return execution && typeof execution === "object" && !Array.isArray(execution) ? execution : {};
|
|
10626
|
+
}
|
|
10627
|
+
function readCommand(payload, execution) {
|
|
10628
|
+
if (typeof execution.command === "string") return execution.command;
|
|
10629
|
+
if (Array.isArray(execution.command)) return execution.command.map(String).join(" ");
|
|
10630
|
+
return payload.prompt;
|
|
10631
|
+
}
|
|
10632
|
+
function resolveCwd(raw, fallback) {
|
|
10633
|
+
if (typeof raw !== "string" || raw.trim() === "") return (0, import_node_path10.resolve)(fallback);
|
|
10634
|
+
return (0, import_node_path10.resolve)(raw);
|
|
10635
|
+
}
|
|
10636
|
+
function clampNumber(value, fallback, min, max) {
|
|
10637
|
+
const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
10638
|
+
if (!Number.isFinite(n)) return fallback;
|
|
10639
|
+
return Math.max(min, Math.min(max, Math.floor(n)));
|
|
10640
|
+
}
|
|
10641
|
+
function fail(taskId, code, message, durationMs) {
|
|
10642
|
+
return { taskId, ok: false, error: { code, message }, metrics: durationMs ? { durationMs } : void 0 };
|
|
10643
|
+
}
|
|
10644
|
+
function formatOutput(input) {
|
|
10645
|
+
return [
|
|
10646
|
+
`$ ${input.command}`,
|
|
10647
|
+
`cwd: ${input.cwd}`,
|
|
10648
|
+
`shell: ${input.shell}`,
|
|
10649
|
+
`exitCode: ${input.exitCode}`,
|
|
10650
|
+
input.truncated ? "output: truncated" : null,
|
|
10651
|
+
input.stdout ? `
|
|
10652
|
+
[stdout]
|
|
10653
|
+
${input.stdout}` : null,
|
|
10654
|
+
input.stderr ? `
|
|
10655
|
+
[stderr]
|
|
10656
|
+
${input.stderr}` : null
|
|
10657
|
+
].filter(Boolean).join("\n");
|
|
10658
|
+
}
|
|
10659
|
+
|
|
10660
|
+
// src/daemon/runner.ts
|
|
10661
|
+
init_skill_sync();
|
|
10662
|
+
|
|
10663
|
+
// src/daemon/pending-reply-cache.ts
|
|
10664
|
+
function fromRaw(r) {
|
|
10665
|
+
return {
|
|
10666
|
+
idempotencyKey: r.idempotency_key,
|
|
10667
|
+
taskId: r.task_id,
|
|
10668
|
+
replyId: r.reply_id,
|
|
10669
|
+
status: r.status,
|
|
10670
|
+
payloadJson: r.payload_json,
|
|
10671
|
+
createdAt: r.created_at,
|
|
10672
|
+
preparedAt: r.prepared_at,
|
|
10673
|
+
lastAttemptAt: r.last_attempt_at,
|
|
10674
|
+
attemptCount: r.attempt_count,
|
|
10675
|
+
lastError: r.last_error
|
|
10676
|
+
};
|
|
10677
|
+
}
|
|
10678
|
+
function createPendingReplyCache(db) {
|
|
10679
|
+
const insert = db.prepare(
|
|
10680
|
+
`INSERT OR REPLACE INTO pending_dispatch_replies
|
|
10681
|
+
(idempotency_key, task_id, reply_id, status, payload_json,
|
|
10682
|
+
prepared_at, created_at, last_attempt_at, attempt_count, last_error)
|
|
10683
|
+
VALUES (?, ?, NULL, 'pending', ?, NULL, ?, NULL, 0, NULL)`
|
|
10684
|
+
);
|
|
10685
|
+
const markPreparedStmt = db.prepare(
|
|
10686
|
+
`UPDATE pending_dispatch_replies
|
|
10687
|
+
SET reply_id = ?, status = 'prepared', prepared_at = ?,
|
|
10688
|
+
last_attempt_at = ?, attempt_count = attempt_count + 1,
|
|
10689
|
+
last_error = NULL
|
|
10690
|
+
WHERE idempotency_key = ?`
|
|
10691
|
+
);
|
|
10692
|
+
const markAttemptStmt = db.prepare(
|
|
10693
|
+
`UPDATE pending_dispatch_replies
|
|
10694
|
+
SET last_attempt_at = ?, attempt_count = attempt_count + 1,
|
|
10695
|
+
last_error = ?
|
|
10696
|
+
WHERE idempotency_key = ?`
|
|
10697
|
+
);
|
|
10698
|
+
const clearStmt = db.prepare(
|
|
10699
|
+
`DELETE FROM pending_dispatch_replies WHERE idempotency_key = ?`
|
|
10700
|
+
);
|
|
10701
|
+
const listRecoveryStmt = db.prepare(
|
|
10702
|
+
`SELECT * FROM pending_dispatch_replies
|
|
10703
|
+
WHERE status IN ('pending', 'prepared')
|
|
10704
|
+
ORDER BY created_at ASC`
|
|
10705
|
+
);
|
|
10706
|
+
const countStmt = db.prepare(
|
|
10707
|
+
`SELECT status, COUNT(*) as cnt FROM pending_dispatch_replies
|
|
10708
|
+
WHERE status IN ('pending','prepared') GROUP BY status`
|
|
10709
|
+
);
|
|
10710
|
+
const findStmt = db.prepare(
|
|
10711
|
+
`SELECT * FROM pending_dispatch_replies WHERE idempotency_key = ?`
|
|
10712
|
+
);
|
|
10713
|
+
return {
|
|
10714
|
+
savePending(idempotencyKey, taskId, payloadJson, now = Date.now()) {
|
|
10715
|
+
insert.run(idempotencyKey, taskId, payloadJson, now);
|
|
10716
|
+
},
|
|
10717
|
+
markPrepared(idempotencyKey, replyId, now = Date.now()) {
|
|
10718
|
+
markPreparedStmt.run(replyId, now, now, idempotencyKey);
|
|
10719
|
+
},
|
|
10720
|
+
markAttempt(idempotencyKey, now = Date.now(), error) {
|
|
10721
|
+
markAttemptStmt.run(now, error ?? null, idempotencyKey);
|
|
10722
|
+
},
|
|
10723
|
+
clearPending(idempotencyKey) {
|
|
10724
|
+
clearStmt.run(idempotencyKey);
|
|
10725
|
+
},
|
|
10726
|
+
listForRecovery() {
|
|
10727
|
+
const rows = listRecoveryStmt.all();
|
|
10728
|
+
return rows.map(fromRaw);
|
|
10729
|
+
},
|
|
10730
|
+
countByStatus() {
|
|
10731
|
+
const rows = countStmt.all();
|
|
10732
|
+
const result = { pending: 0, prepared: 0 };
|
|
10733
|
+
for (const r of rows) {
|
|
10734
|
+
if (r.status === "pending") result.pending = r.cnt;
|
|
10735
|
+
else if (r.status === "prepared") result.prepared = r.cnt;
|
|
10736
|
+
}
|
|
10737
|
+
return result;
|
|
10738
|
+
},
|
|
10739
|
+
findByIdempotencyKey(idempotencyKey) {
|
|
10740
|
+
const row = findStmt.get(idempotencyKey);
|
|
10741
|
+
return row ? fromRaw(row) : null;
|
|
10742
|
+
}
|
|
10743
|
+
};
|
|
10744
|
+
}
|
|
10745
|
+
function generateIdempotencyKey() {
|
|
10746
|
+
const c = globalThis.crypto;
|
|
10747
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
10748
|
+
const bytes = new Uint8Array(16);
|
|
10749
|
+
const cryptoNode = globalThis.crypto;
|
|
10750
|
+
if (cryptoNode?.getRandomValues) {
|
|
10751
|
+
cryptoNode.getRandomValues(bytes);
|
|
10752
|
+
} else {
|
|
10753
|
+
for (let i = 0; i < 16; i += 1) bytes[i] = Math.floor(Math.random() * 256);
|
|
10754
|
+
}
|
|
10755
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
10756
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
10757
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
10758
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
10759
|
+
}
|
|
10760
|
+
|
|
10761
|
+
// src/daemon/dispatch-reply-transport.ts
|
|
10762
|
+
var DispatchReplyAbortedError = class extends Error {
|
|
10763
|
+
constructor(replyId) {
|
|
10764
|
+
super(`dispatch reply ${replyId} aborted by server reaper (likely >1h since prepare)`);
|
|
10765
|
+
this.replyId = replyId;
|
|
10766
|
+
this.name = "DispatchReplyAbortedError";
|
|
10767
|
+
}
|
|
10768
|
+
replyId;
|
|
10769
|
+
code = "DISPATCH_REPLY_INVALID_STATE";
|
|
10770
|
+
};
|
|
10771
|
+
function defaultLog2(line) {
|
|
10772
|
+
process.stderr.write(`${line}
|
|
10773
|
+
`);
|
|
10774
|
+
}
|
|
10775
|
+
async function sendDispatchReplyTwoPhase(args) {
|
|
10776
|
+
const { taskId, payload, cloud, cache } = args;
|
|
10777
|
+
const log8 = args.log ?? defaultLog2;
|
|
10778
|
+
const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
|
|
10779
|
+
const payloadJson = JSON.stringify({ ...payload, _taskId: taskId, _idempotencyKey: idempotencyKey });
|
|
10780
|
+
cache.savePending(idempotencyKey, taskId, payloadJson);
|
|
10781
|
+
const prepareRes = await cloud.request("POST", `/api/im/dispatch/${encodeURIComponent(taskId)}/prepare`, {
|
|
10782
|
+
body: { ...payload, idempotencyKey },
|
|
10783
|
+
timeoutMs: 3e4
|
|
10784
|
+
});
|
|
10785
|
+
if (!prepareRes.ok || !prepareRes.data) {
|
|
10786
|
+
const errCode = prepareRes.error?.code ?? "unknown";
|
|
10787
|
+
const errMsg = prepareRes.error?.message ?? `prepare failed (status=${prepareRes.status})`;
|
|
10788
|
+
cache.markAttempt(idempotencyKey, Date.now(), `prepare: ${errCode}: ${errMsg}`);
|
|
10789
|
+
throw new Error(`dispatch prepare failed: ${errCode}: ${errMsg}`);
|
|
10790
|
+
}
|
|
10791
|
+
const envelope2 = prepareRes.data;
|
|
10792
|
+
const inner = "data" in envelope2 && envelope2.data ? envelope2.data : envelope2;
|
|
10793
|
+
if (!inner || typeof inner.replyId !== "string") {
|
|
10794
|
+
cache.markAttempt(idempotencyKey, Date.now(), "prepare: missing replyId in response");
|
|
10795
|
+
throw new Error("dispatch prepare returned no replyId");
|
|
10796
|
+
}
|
|
10797
|
+
if ("ok" in envelope2 && envelope2.ok === false) {
|
|
10798
|
+
const errCode = envelope2.error?.code ?? "prepare_failed";
|
|
10799
|
+
const errMsg = envelope2.error?.message ?? "prepare ok=false";
|
|
10800
|
+
cache.markAttempt(idempotencyKey, Date.now(), `prepare: ${errCode}: ${errMsg}`);
|
|
10801
|
+
throw new Error(`dispatch prepare failed: ${errCode}: ${errMsg}`);
|
|
10802
|
+
}
|
|
10803
|
+
cache.markPrepared(idempotencyKey, inner.replyId);
|
|
10804
|
+
log8(
|
|
10805
|
+
`[daemon] dispatch.prepare task=${taskId} replyId=${inner.replyId} status=${inner.status} idem=${idempotencyKey}`
|
|
10806
|
+
);
|
|
10807
|
+
if (inner.status === "committed") {
|
|
10808
|
+
cache.clearPending(idempotencyKey);
|
|
10809
|
+
return {
|
|
10810
|
+
taskId,
|
|
10811
|
+
replyId: inner.replyId,
|
|
10812
|
+
status: "committed",
|
|
10813
|
+
alreadyCommitted: true
|
|
10814
|
+
};
|
|
10815
|
+
}
|
|
10816
|
+
try {
|
|
10817
|
+
const result = await commitDispatchReply({ taskId, replyId: inner.replyId, cloud, log: log8 });
|
|
10818
|
+
cache.clearPending(idempotencyKey);
|
|
10819
|
+
return { taskId, replyId: inner.replyId, ...result };
|
|
10820
|
+
} catch (err) {
|
|
10821
|
+
if (err instanceof DispatchReplyAbortedError) {
|
|
10822
|
+
cache.clearPending(idempotencyKey);
|
|
10823
|
+
log8(`[daemon] dispatch.commit aborted task=${taskId} replyId=${inner.replyId} \u2014 dispatch lost`);
|
|
10824
|
+
return {
|
|
10825
|
+
taskId,
|
|
10826
|
+
replyId: inner.replyId,
|
|
10827
|
+
status: "aborted",
|
|
10828
|
+
alreadyCommitted: false
|
|
10829
|
+
};
|
|
10830
|
+
}
|
|
10831
|
+
cache.markAttempt(idempotencyKey, Date.now(), `commit: ${err.message}`);
|
|
10832
|
+
throw err;
|
|
10833
|
+
}
|
|
10834
|
+
}
|
|
10835
|
+
async function commitDispatchReply(args) {
|
|
10836
|
+
const log8 = args.log ?? defaultLog2;
|
|
10837
|
+
const res = await args.cloud.request("POST", `/api/im/dispatch/${encodeURIComponent(args.taskId)}/commit`, {
|
|
10838
|
+
body: { replyId: args.replyId },
|
|
10839
|
+
timeoutMs: 3e4
|
|
10840
|
+
});
|
|
10841
|
+
if (!res.ok || !res.data) {
|
|
10842
|
+
const code = res.error?.code ?? "unknown";
|
|
10843
|
+
const msg = res.error?.message ?? `commit failed (status=${res.status})`;
|
|
10844
|
+
if (code === "DISPATCH_REPLY_INVALID_STATE") {
|
|
10845
|
+
throw new DispatchReplyAbortedError(args.replyId);
|
|
10846
|
+
}
|
|
10847
|
+
throw new Error(`dispatch commit failed: ${code}: ${msg}`);
|
|
10848
|
+
}
|
|
10849
|
+
const env = res.data;
|
|
10850
|
+
const inner = "data" in env && env.data ? env.data : env;
|
|
10851
|
+
if ("ok" in env && env.ok === false) {
|
|
10852
|
+
const code = env.error?.code ?? "commit_failed";
|
|
10853
|
+
if (code === "DISPATCH_REPLY_INVALID_STATE") {
|
|
10854
|
+
throw new DispatchReplyAbortedError(args.replyId);
|
|
10855
|
+
}
|
|
10856
|
+
throw new Error(`dispatch commit failed: ${code}: ${env.error?.message ?? "commit ok=false"}`);
|
|
10857
|
+
}
|
|
10858
|
+
log8(
|
|
10859
|
+
`[daemon] dispatch.commit task=${args.taskId} replyId=${args.replyId} alreadyCommitted=${inner.alreadyCommitted ?? false}`
|
|
10860
|
+
);
|
|
10861
|
+
return {
|
|
10862
|
+
status: "committed",
|
|
10863
|
+
alreadyCommitted: Boolean(inner.alreadyCommitted),
|
|
10864
|
+
...inner.messageId ? { messageId: inner.messageId } : {}
|
|
10865
|
+
};
|
|
9487
10866
|
}
|
|
9488
|
-
function
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
10867
|
+
async function recoverPendingReplies(deps) {
|
|
10868
|
+
const log8 = deps.log ?? defaultLog2;
|
|
10869
|
+
const rows = deps.cache.listForRecovery();
|
|
10870
|
+
if (rows.length === 0) {
|
|
10871
|
+
return { attempted: 0, committed: 0, aborted: 0, failed: 0 };
|
|
10872
|
+
}
|
|
10873
|
+
log8(`[daemon] dispatch.recovery: ${rows.length} pending row(s) to replay`);
|
|
10874
|
+
let committed = 0;
|
|
10875
|
+
let aborted = 0;
|
|
10876
|
+
let failed = 0;
|
|
10877
|
+
for (const row of rows) {
|
|
10878
|
+
try {
|
|
10879
|
+
const result = await replayRow(row, deps);
|
|
10880
|
+
if (result.status === "committed") committed += 1;
|
|
10881
|
+
else if (result.status === "aborted") aborted += 1;
|
|
10882
|
+
} catch (err) {
|
|
10883
|
+
failed += 1;
|
|
10884
|
+
log8(
|
|
10885
|
+
`[daemon] dispatch.recovery FAIL idem=${row.idempotencyKey} task=${row.taskId}: ${err.message}`
|
|
10886
|
+
);
|
|
10887
|
+
}
|
|
10888
|
+
}
|
|
10889
|
+
log8(
|
|
10890
|
+
`[daemon] dispatch.recovery done attempted=${rows.length} committed=${committed} aborted=${aborted} failed=${failed}`
|
|
10891
|
+
);
|
|
10892
|
+
return { attempted: rows.length, committed, aborted, failed };
|
|
9492
10893
|
}
|
|
9493
|
-
function
|
|
9494
|
-
|
|
9495
|
-
|
|
10894
|
+
async function replayRow(row, deps) {
|
|
10895
|
+
const log8 = deps.log ?? defaultLog2;
|
|
10896
|
+
if (row.status === "prepared" && row.replyId) {
|
|
10897
|
+
try {
|
|
10898
|
+
const result2 = await commitDispatchReply({
|
|
10899
|
+
taskId: row.taskId,
|
|
10900
|
+
replyId: row.replyId,
|
|
10901
|
+
cloud: deps.cloud,
|
|
10902
|
+
log: log8
|
|
10903
|
+
});
|
|
10904
|
+
deps.cache.clearPending(row.idempotencyKey);
|
|
10905
|
+
return { status: result2.status };
|
|
10906
|
+
} catch (err) {
|
|
10907
|
+
if (err instanceof DispatchReplyAbortedError) {
|
|
10908
|
+
deps.cache.clearPending(row.idempotencyKey);
|
|
10909
|
+
return { status: "aborted" };
|
|
10910
|
+
}
|
|
10911
|
+
throw err;
|
|
10912
|
+
}
|
|
10913
|
+
}
|
|
10914
|
+
let parsed;
|
|
10915
|
+
try {
|
|
10916
|
+
parsed = JSON.parse(row.payloadJson);
|
|
10917
|
+
} catch (err) {
|
|
10918
|
+
deps.cache.clearPending(row.idempotencyKey);
|
|
10919
|
+
throw new Error(`pending row ${row.idempotencyKey} has unparseable payload: ${err.message}`);
|
|
10920
|
+
}
|
|
10921
|
+
const { _taskId, _idempotencyKey, ...payload } = parsed;
|
|
10922
|
+
const result = await sendDispatchReplyTwoPhase({
|
|
10923
|
+
taskId: row.taskId,
|
|
10924
|
+
payload,
|
|
10925
|
+
cloud: deps.cloud,
|
|
10926
|
+
cache: deps.cache,
|
|
10927
|
+
idempotencyKey: row.idempotencyKey,
|
|
10928
|
+
log: deps.log
|
|
10929
|
+
});
|
|
10930
|
+
return { status: result.status };
|
|
9496
10931
|
}
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
|
|
10932
|
+
|
|
10933
|
+
// src/daemon/transport-probe.ts
|
|
10934
|
+
var import_node_os7 = require("os");
|
|
10935
|
+
function isPrivateAddress(addr) {
|
|
10936
|
+
if (!addr) return true;
|
|
10937
|
+
const h = String(addr).trim().toLowerCase();
|
|
10938
|
+
if (!h) return true;
|
|
10939
|
+
if (h === "localhost" || h === "::1") return true;
|
|
10940
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
|
|
10941
|
+
if (m) {
|
|
10942
|
+
const a = Number(m[1]);
|
|
10943
|
+
const b = Number(m[2]);
|
|
10944
|
+
if (a === 10) return true;
|
|
10945
|
+
if (a === 127) return true;
|
|
10946
|
+
if (a === 169 && b === 254) return true;
|
|
10947
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
10948
|
+
if (a === 192 && b === 168) return true;
|
|
10949
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
10950
|
+
return false;
|
|
10951
|
+
}
|
|
10952
|
+
if (h.endsWith(".local")) return true;
|
|
10953
|
+
if (h.endsWith(".localhost")) return true;
|
|
10954
|
+
return false;
|
|
9501
10955
|
}
|
|
9502
|
-
function
|
|
9503
|
-
|
|
10956
|
+
function buildTransportProbe(gatewayUrl) {
|
|
10957
|
+
if (!gatewayUrl) {
|
|
10958
|
+
return { transport: "ws", gatewayUrl: null, gatewayIsPrivate: true };
|
|
10959
|
+
}
|
|
10960
|
+
let host = null;
|
|
10961
|
+
try {
|
|
10962
|
+
host = new URL(gatewayUrl).hostname;
|
|
10963
|
+
} catch {
|
|
10964
|
+
host = null;
|
|
10965
|
+
}
|
|
10966
|
+
const gatewayIsPrivate = isPrivateAddress(host);
|
|
10967
|
+
return {
|
|
10968
|
+
transport: gatewayIsPrivate ? "ws" : "both",
|
|
10969
|
+
gatewayUrl,
|
|
10970
|
+
gatewayIsPrivate
|
|
10971
|
+
};
|
|
9504
10972
|
}
|
|
9505
|
-
function
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
10973
|
+
function pickLocalIPv4() {
|
|
10974
|
+
const nics = (0, import_node_os7.networkInterfaces)();
|
|
10975
|
+
let firstPrivate = null;
|
|
10976
|
+
for (const list of Object.values(nics)) {
|
|
10977
|
+
if (!list) continue;
|
|
10978
|
+
for (const ni of list) {
|
|
10979
|
+
if (ni.internal || ni.family !== "IPv4") continue;
|
|
10980
|
+
if (!isPrivateAddress(ni.address)) return ni.address;
|
|
10981
|
+
if (!firstPrivate) firstPrivate = ni.address;
|
|
10982
|
+
}
|
|
10983
|
+
}
|
|
10984
|
+
return firstPrivate;
|
|
10985
|
+
}
|
|
10986
|
+
async function reportTransportProbe(cloud, daemonId, probe) {
|
|
10987
|
+
const body = {
|
|
10988
|
+
daemonId,
|
|
10989
|
+
transport: probe.transport,
|
|
10990
|
+
gatewayUrl: probe.gatewayUrl,
|
|
10991
|
+
gatewayIsPrivate: probe.gatewayIsPrivate
|
|
10992
|
+
};
|
|
10993
|
+
const res = await cloud.request("POST", "/api/im/runtime/transport-report", {
|
|
10994
|
+
body,
|
|
10995
|
+
timeoutMs: 5e3
|
|
10996
|
+
});
|
|
10997
|
+
if (!res.ok) {
|
|
10998
|
+
return {
|
|
10999
|
+
ok: false,
|
|
11000
|
+
status: res.status,
|
|
11001
|
+
error: res.error?.message ?? `transport-report failed (${res.status})`
|
|
11002
|
+
};
|
|
11003
|
+
}
|
|
11004
|
+
return { ok: true, status: res.status };
|
|
9519
11005
|
}
|
|
9520
11006
|
|
|
9521
11007
|
// src/daemon/runner.ts
|
|
9522
|
-
init_skill_sync();
|
|
9523
11008
|
var import_meta2 = {};
|
|
9524
11009
|
var DEFAULT_LOCAL_PORT = 3210;
|
|
9525
11010
|
var log7 = createLogger("Daemon");
|
|
@@ -9561,6 +11046,7 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
9561
11046
|
workspaceId = "";
|
|
9562
11047
|
wsConnected = false;
|
|
9563
11048
|
hostedAgents = /* @__PURE__ */ new Map();
|
|
11049
|
+
rejectedHostedAgentIds = /* @__PURE__ */ new Map();
|
|
9564
11050
|
runningTasks = /* @__PURE__ */ new Map();
|
|
9565
11051
|
lastTaskError;
|
|
9566
11052
|
heartbeatTimer;
|
|
@@ -9568,6 +11054,14 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
9568
11054
|
// F16 (2026-05-20) — periodic skill resync timer (default 10min).
|
|
9569
11055
|
skillResyncTimer;
|
|
9570
11056
|
skillSyncInFlight = false;
|
|
11057
|
+
// Wave-4 E7 — local cache of in-flight two-phase dispatch replies. Survives
|
|
11058
|
+
// daemon crash so cold-start can resume `prepared` rows via idempotent
|
|
11059
|
+
// commit (server enforces (taskId, idempotencyKey) UNIQUE).
|
|
11060
|
+
pendingReplyCache;
|
|
11061
|
+
// One-shot guard so we only recover on the first post-authenticated tick,
|
|
11062
|
+
// not every reconnect (server-side commits are idempotent but the log noise
|
|
11063
|
+
// and DB pressure of re-running on every redeclare is wasteful).
|
|
11064
|
+
pendingReplyRecoveryDone = false;
|
|
9571
11065
|
async start() {
|
|
9572
11066
|
if (this.state !== "idle") throw new Error(`Runner already in state ${this.state}`);
|
|
9573
11067
|
this.state = "starting";
|
|
@@ -9582,6 +11076,7 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
9582
11076
|
process.env.PRISMER_API_KEY = this.config.api_key;
|
|
9583
11077
|
this.db = openLocalDb(this.paths.localDb);
|
|
9584
11078
|
this.cloud = new CloudClient({ baseUrl: this.config.cloud_api_base, apiKey: this.config.api_key });
|
|
11079
|
+
this.pendingReplyCache = createPendingReplyCache(this.db);
|
|
9585
11080
|
this.assetCache = new AssetCache({
|
|
9586
11081
|
db: this.db,
|
|
9587
11082
|
cloud: this.cloud,
|
|
@@ -9908,19 +11403,19 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
9908
11403
|
sampleCgroupResources() {
|
|
9909
11404
|
const baseline = { cpu: { usagePct: 0 }, mem: { usedBytes: 0, limitBytes: 0 } };
|
|
9910
11405
|
try {
|
|
9911
|
-
if (!(0,
|
|
11406
|
+
if (!(0, import_node_fs16.existsSync)("/sys/fs/cgroup/memory.current")) {
|
|
9912
11407
|
return baseline;
|
|
9913
11408
|
}
|
|
9914
|
-
const memUsedRaw = (0,
|
|
11409
|
+
const memUsedRaw = (0, import_node_fs16.readFileSync)("/sys/fs/cgroup/memory.current", "utf-8").trim();
|
|
9915
11410
|
const memUsed = Number.parseInt(memUsedRaw, 10);
|
|
9916
11411
|
let memLimit = 0;
|
|
9917
|
-
if ((0,
|
|
9918
|
-
const limitRaw = (0,
|
|
11412
|
+
if ((0, import_node_fs16.existsSync)("/sys/fs/cgroup/memory.max")) {
|
|
11413
|
+
const limitRaw = (0, import_node_fs16.readFileSync)("/sys/fs/cgroup/memory.max", "utf-8").trim();
|
|
9919
11414
|
memLimit = limitRaw === "max" ? 0 : Number.parseInt(limitRaw, 10) || 0;
|
|
9920
11415
|
}
|
|
9921
11416
|
let cpuUsagePct = 0;
|
|
9922
|
-
if ((0,
|
|
9923
|
-
const statLines = (0,
|
|
11417
|
+
if ((0, import_node_fs16.existsSync)("/sys/fs/cgroup/cpu.stat")) {
|
|
11418
|
+
const statLines = (0, import_node_fs16.readFileSync)("/sys/fs/cgroup/cpu.stat", "utf-8").split("\n");
|
|
9924
11419
|
const usecLine = statLines.find((line) => line.startsWith("usage_usec "));
|
|
9925
11420
|
if (usecLine) {
|
|
9926
11421
|
const usec = Number.parseInt(usecLine.split(" ")[1] ?? "0", 10);
|
|
@@ -9997,6 +11492,9 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
9997
11492
|
profilesByAgent.set(p.agent_im_user_id, list);
|
|
9998
11493
|
}
|
|
9999
11494
|
for (const a of agents) {
|
|
11495
|
+
if (this.rejectedHostedAgentIds.has(a.im_user_id)) {
|
|
11496
|
+
continue;
|
|
11497
|
+
}
|
|
10000
11498
|
let caps = [];
|
|
10001
11499
|
try {
|
|
10002
11500
|
caps = JSON.parse(a.capabilities);
|
|
@@ -10046,6 +11544,7 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10046
11544
|
* Used to populate the `agents` list in agent.host.declare.
|
|
10047
11545
|
*/
|
|
10048
11546
|
setHostedAgent(agent) {
|
|
11547
|
+
this.rejectedHostedAgentIds.delete(agent.imUserId);
|
|
10049
11548
|
this.hostedAgents.set(agent.imUserId, {
|
|
10050
11549
|
imUserId: agent.imUserId,
|
|
10051
11550
|
name: agent.name,
|
|
@@ -10078,6 +11577,7 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10078
11577
|
);
|
|
10079
11578
|
});
|
|
10080
11579
|
tx(payload);
|
|
11580
|
+
this.rejectedHostedAgentIds.delete(payload.imUserId);
|
|
10081
11581
|
this.loadAgentsFromDb();
|
|
10082
11582
|
if (this.wsConnected) this.sendDeclare();
|
|
10083
11583
|
return {
|
|
@@ -10098,10 +11598,10 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10098
11598
|
const rawJson = process.env.PRISMER_HOSTED_AGENT_JSON;
|
|
10099
11599
|
let raw;
|
|
10100
11600
|
if (rawFile) {
|
|
10101
|
-
if (!(0,
|
|
11601
|
+
if (!(0, import_node_fs16.existsSync)(rawFile)) {
|
|
10102
11602
|
throw new Error(`PRISMER_HOSTED_AGENT_FILE not found: ${rawFile}`);
|
|
10103
11603
|
}
|
|
10104
|
-
raw = (0,
|
|
11604
|
+
raw = (0, import_node_fs16.readFileSync)(rawFile, "utf8");
|
|
10105
11605
|
} else if (rawJson) {
|
|
10106
11606
|
raw = rawJson;
|
|
10107
11607
|
}
|
|
@@ -10188,6 +11688,19 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10188
11688
|
process.stdout.write(`[daemon] ws authenticated, sending agent.host.declare (${this.hostedAgents.size} agents)
|
|
10189
11689
|
`);
|
|
10190
11690
|
this.sendDeclare();
|
|
11691
|
+
if (!this.pendingReplyRecoveryDone) {
|
|
11692
|
+
this.pendingReplyRecoveryDone = true;
|
|
11693
|
+
void recoverPendingReplies({ cloud: this.cloud, cache: this.pendingReplyCache }).then((summary) => {
|
|
11694
|
+
if (summary.attempted === 0) return;
|
|
11695
|
+
process.stdout.write(
|
|
11696
|
+
`[daemon] dispatch.recovery summary attempted=${summary.attempted} committed=${summary.committed} aborted=${summary.aborted} failed=${summary.failed}
|
|
11697
|
+
`
|
|
11698
|
+
);
|
|
11699
|
+
}).catch((err) => {
|
|
11700
|
+
process.stderr.write(`[daemon] dispatch.recovery threw: ${err.message}
|
|
11701
|
+
`);
|
|
11702
|
+
});
|
|
11703
|
+
}
|
|
10191
11704
|
return;
|
|
10192
11705
|
}
|
|
10193
11706
|
this.handleIncoming(msg);
|
|
@@ -10197,7 +11710,7 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10197
11710
|
const payload = {
|
|
10198
11711
|
daemonId: this.config.daemon_id,
|
|
10199
11712
|
daemonVersion: this.opts.daemonVersion ?? "0.0.0",
|
|
10200
|
-
platform: (0,
|
|
11713
|
+
platform: (0, import_node_os8.platform)() === "win32" ? "win32" : (0, import_node_os8.platform)() === "linux" ? "linux" : "darwin",
|
|
10201
11714
|
agents: Array.from(this.hostedAgents.values()).map((a) => ({
|
|
10202
11715
|
imUserId: a.imUserId,
|
|
10203
11716
|
name: a.name,
|
|
@@ -10234,12 +11747,91 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10234
11747
|
case "asset.changed":
|
|
10235
11748
|
void this.onAssetChanged(msg.payload);
|
|
10236
11749
|
return;
|
|
11750
|
+
// v2.0 §4.8.1 (Wave 4-E4) — webhook reverse-channel RPC. Cloud's
|
|
11751
|
+
// dispatchExternalMention pushes an AgentDispatchRequest plus an
|
|
11752
|
+
// embedded `_rpcId`; the daemon runs the same handler as the
|
|
11753
|
+
// HTTP /dispatch path and echoes the reply back over WS with the
|
|
11754
|
+
// identical `_rpcId` so cloud's WsRpcService can settle the
|
|
11755
|
+
// pending promise.
|
|
11756
|
+
case "webhook.dispatch.request":
|
|
11757
|
+
void this.onWebhookDispatch(msg.payload);
|
|
11758
|
+
return;
|
|
10237
11759
|
default:
|
|
10238
11760
|
this.emit("unknown-message", msg);
|
|
10239
11761
|
}
|
|
10240
11762
|
}
|
|
11763
|
+
/**
|
|
11764
|
+
* v2.0 §4.8.1 — webhook dispatch over WS reverse channel.
|
|
11765
|
+
*
|
|
11766
|
+
* Acks via `webhook.dispatch.reply { _rpcId, ok, acceptedAt }` immediately
|
|
11767
|
+
* after `handleAgentMessageDispatch()` returns its synchronous response
|
|
11768
|
+
* — the actual reply (the agent's reply text/attachments) still flows
|
|
11769
|
+
* through `postMessageDispatchReply()` which posts to
|
|
11770
|
+
* `/api/im/dispatch/reply` after the adapter finishes. That mirrors the
|
|
11771
|
+
* HTTP /dispatch contract: ack first, asynchronous final reply via the
|
|
11772
|
+
* dispatch-reply REST endpoint.
|
|
11773
|
+
*
|
|
11774
|
+
* The `_rpcId` field is the only addition versus the HTTP path; we strip
|
|
11775
|
+
* it before passing the payload to the dispatch handler so existing
|
|
11776
|
+
* validation logic doesn't trip on an unknown field.
|
|
11777
|
+
*/
|
|
11778
|
+
async onWebhookDispatch(payload) {
|
|
11779
|
+
const rpcId = payload._rpcId;
|
|
11780
|
+
if (!rpcId) {
|
|
11781
|
+
process.stderr.write(`[daemon] webhook.dispatch.request without _rpcId \u2014 dropped
|
|
11782
|
+
`);
|
|
11783
|
+
return;
|
|
11784
|
+
}
|
|
11785
|
+
const { _rpcId: _, ...request } = payload;
|
|
11786
|
+
try {
|
|
11787
|
+
const handle = handleAgentMessageDispatch(request, {
|
|
11788
|
+
findAgent: (agentImUserId) => this.findMessageDispatchAgent(agentImUserId),
|
|
11789
|
+
postReply: (replyPayload) => this.postMessageDispatchReply(replyPayload),
|
|
11790
|
+
onError: (err, req) => {
|
|
11791
|
+
process.stderr.write(
|
|
11792
|
+
`[daemon] webhook.dispatch.request failed message=${req.messageId} agent=${req.mentionedAgentImUserId}: ${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
11793
|
+
`
|
|
11794
|
+
);
|
|
11795
|
+
}
|
|
11796
|
+
});
|
|
11797
|
+
this.ws.send({
|
|
11798
|
+
type: "webhook.dispatch.reply",
|
|
11799
|
+
payload: {
|
|
11800
|
+
_rpcId: rpcId,
|
|
11801
|
+
ok: handle.response.ok,
|
|
11802
|
+
acceptedAt: handle.response.acceptedAt,
|
|
11803
|
+
error: handle.response.error
|
|
11804
|
+
}
|
|
11805
|
+
});
|
|
11806
|
+
void handle.done.catch((err) => {
|
|
11807
|
+
process.stderr.write(
|
|
11808
|
+
`[daemon] webhook.dispatch final reply failed message=${request.messageId}: ${err instanceof Error ? err.message : String(err)}
|
|
11809
|
+
`
|
|
11810
|
+
);
|
|
11811
|
+
});
|
|
11812
|
+
} catch (err) {
|
|
11813
|
+
this.ws.send({
|
|
11814
|
+
type: "webhook.dispatch.reply",
|
|
11815
|
+
payload: {
|
|
11816
|
+
_rpcId: rpcId,
|
|
11817
|
+
ok: false,
|
|
11818
|
+
error: {
|
|
11819
|
+
code: "daemon_dispatch_failed",
|
|
11820
|
+
message: err instanceof Error ? err.message : String(err)
|
|
11821
|
+
}
|
|
11822
|
+
}
|
|
11823
|
+
});
|
|
11824
|
+
}
|
|
11825
|
+
}
|
|
10241
11826
|
async onHostAcked(payload) {
|
|
10242
11827
|
this.workspaceId = payload.workspaceId;
|
|
11828
|
+
const ownershipLockChanged = await this.applyHostAckOwnershipLock(payload);
|
|
11829
|
+
void this.reportTransport().catch((err) => {
|
|
11830
|
+
process.stderr.write(
|
|
11831
|
+
`[daemon] transport-probe report failed: ${err.message}
|
|
11832
|
+
`
|
|
11833
|
+
);
|
|
11834
|
+
});
|
|
10243
11835
|
if (this.memoryWiring && this.workspaceId) {
|
|
10244
11836
|
syncMemoryFromCloud(this.memoryWiring, this.cloud, [this.workspaceId]).catch(
|
|
10245
11837
|
(err) => log7.error("Initial memory sync failed", err.message)
|
|
@@ -10249,7 +11841,9 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10249
11841
|
(err) => log7.error("Asset sync failed", err.message)
|
|
10250
11842
|
);
|
|
10251
11843
|
await this.ensureDropFolderRuntime(this.workspaceId);
|
|
10252
|
-
|
|
11844
|
+
const profilesToSync = payload.profilesToSync ?? [];
|
|
11845
|
+
const profilesToDelete = payload.profilesToDelete ?? [];
|
|
11846
|
+
for (const id of profilesToSync) {
|
|
10253
11847
|
try {
|
|
10254
11848
|
await this.servicePool.drop(id);
|
|
10255
11849
|
await this.syncProfileFromCloud(id);
|
|
@@ -10257,7 +11851,7 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10257
11851
|
this.emit("sync-error", err);
|
|
10258
11852
|
}
|
|
10259
11853
|
}
|
|
10260
|
-
for (const id of
|
|
11854
|
+
for (const id of profilesToDelete) {
|
|
10261
11855
|
const row = this.db.prepare("SELECT agent_im_user_id FROM agent_profiles WHERE id = ?").get(id);
|
|
10262
11856
|
this.db.prepare("DELETE FROM agent_profiles WHERE id = ?").run(id);
|
|
10263
11857
|
if (row) {
|
|
@@ -10266,10 +11860,37 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10266
11860
|
process.stderr.write(`[daemon] tombstone profile=${id}
|
|
10267
11861
|
`);
|
|
10268
11862
|
}
|
|
10269
|
-
if (
|
|
10270
|
-
if ((
|
|
11863
|
+
if (profilesToDelete.length) this.loadAgentsFromDb();
|
|
11864
|
+
if ((ownershipLockChanged || profilesToSync.length > 0 || profilesToDelete.length > 0) && this.wsConnected) {
|
|
11865
|
+
this.sendDeclare();
|
|
11866
|
+
}
|
|
10271
11867
|
this.emit("host-acked", payload);
|
|
10272
11868
|
}
|
|
11869
|
+
async applyHostAckOwnershipLock(payload) {
|
|
11870
|
+
const rejected = payload.rejectedAgents ?? [];
|
|
11871
|
+
if (rejected.length === 0) return false;
|
|
11872
|
+
let changed = false;
|
|
11873
|
+
for (const rejection of rejected) {
|
|
11874
|
+
if (!rejection?.imUserId) continue;
|
|
11875
|
+
const agentImUserId = rejection.imUserId;
|
|
11876
|
+
this.rejectedHostedAgentIds.set(agentImUserId, {
|
|
11877
|
+
...rejection,
|
|
11878
|
+
rejectedAt: Date.now()
|
|
11879
|
+
});
|
|
11880
|
+
const wasHosted = this.hostedAgents.delete(agentImUserId);
|
|
11881
|
+
const profiles = this.db.prepare("SELECT id FROM agent_profiles WHERE agent_im_user_id = ?").all(agentImUserId);
|
|
11882
|
+
const deleted = this.db.prepare("DELETE FROM agents WHERE im_user_id = ?").run(agentImUserId);
|
|
11883
|
+
await Promise.all(profiles.map((profile) => this.servicePool.drop(profile.id)));
|
|
11884
|
+
if (wasHosted || deleted.changes > 0) {
|
|
11885
|
+
changed = true;
|
|
11886
|
+
process.stderr.write(
|
|
11887
|
+
`[daemon] ownership rejected agent=${agentImUserId} reason=${rejection.reason} owner=${rejection.ownerDaemonId ?? "(unknown)"} \u2014 stop declaring locally
|
|
11888
|
+
`
|
|
11889
|
+
);
|
|
11890
|
+
}
|
|
11891
|
+
}
|
|
11892
|
+
return changed;
|
|
11893
|
+
}
|
|
10273
11894
|
async onTaskDispatch(payload, requestId) {
|
|
10274
11895
|
const targetDaemonId = readTargetDaemonId(payload);
|
|
10275
11896
|
if (targetDaemonId && targetDaemonId !== this.config.daemon_id) {
|
|
@@ -10421,14 +12042,68 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10421
12042
|
};
|
|
10422
12043
|
}
|
|
10423
12044
|
async postMessageDispatchReply(payload) {
|
|
10424
|
-
const
|
|
10425
|
-
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
10429
|
-
|
|
10430
|
-
|
|
12045
|
+
const taskId = `external:${payload.replyToMessageId}`;
|
|
12046
|
+
try {
|
|
12047
|
+
const result = await sendDispatchReplyTwoPhase({
|
|
12048
|
+
taskId,
|
|
12049
|
+
payload: {
|
|
12050
|
+
conversationId: payload.conversationId,
|
|
12051
|
+
replyToken: payload.replyToken,
|
|
12052
|
+
replyToMessageId: payload.replyToMessageId,
|
|
12053
|
+
agentImUserId: payload.agentImUserId,
|
|
12054
|
+
status: payload.status,
|
|
12055
|
+
...payload.replyText !== void 0 ? { replyText: payload.replyText } : {},
|
|
12056
|
+
...payload.attachments ? { attachments: payload.attachments } : {},
|
|
12057
|
+
assetIds: payload.attachments?.map((a) => a.assetId).filter((id) => typeof id === "string") ?? [],
|
|
12058
|
+
completedAt: payload.completedAt,
|
|
12059
|
+
...payload.error ? { error: payload.error } : {}
|
|
12060
|
+
},
|
|
12061
|
+
cloud: this.cloud,
|
|
12062
|
+
cache: this.pendingReplyCache
|
|
12063
|
+
});
|
|
12064
|
+
if (result.status === "aborted") {
|
|
12065
|
+
throw new Error(`dispatch reply aborted by server reaper (replyId=${result.replyId})`);
|
|
12066
|
+
}
|
|
12067
|
+
} catch (err) {
|
|
12068
|
+
throw new Error(
|
|
12069
|
+
err.message?.length ? err.message : `dispatch reply failed`
|
|
12070
|
+
);
|
|
12071
|
+
}
|
|
12072
|
+
}
|
|
12073
|
+
/**
|
|
12074
|
+
* v2.0 §4.8.1 (Wave 4-E4) — submit the transport-probe report. Called
|
|
12075
|
+
* from `onHostAcked` so the daemon's container row exists in cloud
|
|
12076
|
+
* before we POST to /runtime/transport-report.
|
|
12077
|
+
*
|
|
12078
|
+
* The gatewayUrl reported here is `http://<local-ipv4>:<localPort>`
|
|
12079
|
+
* (typically `http://192.168.x.x:3210`). For a daemon without a local
|
|
12080
|
+
* HTTP server (startLocalServer=false in tests) we still post the
|
|
12081
|
+
* probe with `gatewayUrl=null` so cloud knows transport='ws' is the
|
|
12082
|
+
* only path.
|
|
12083
|
+
*/
|
|
12084
|
+
async reportTransport() {
|
|
12085
|
+
const hasLocalServer = this.opts.startLocalServer !== false;
|
|
12086
|
+
let gatewayUrl = null;
|
|
12087
|
+
if (hasLocalServer) {
|
|
12088
|
+
const localIp = pickLocalIPv4();
|
|
12089
|
+
if (localIp) {
|
|
12090
|
+
const port = this.opts.localPort ?? DEFAULT_LOCAL_PORT;
|
|
12091
|
+
gatewayUrl = `http://${localIp}:${port}`;
|
|
12092
|
+
}
|
|
12093
|
+
}
|
|
12094
|
+
const probe = buildTransportProbe(gatewayUrl);
|
|
12095
|
+
const result = await reportTransportProbe(this.cloud, this.config.daemon_id, probe);
|
|
12096
|
+
if (!result.ok) {
|
|
12097
|
+
process.stderr.write(
|
|
12098
|
+
`[daemon] transport-probe report rejected status=${result.status} error=${result.error ?? "-"}
|
|
12099
|
+
`
|
|
12100
|
+
);
|
|
12101
|
+
return;
|
|
10431
12102
|
}
|
|
12103
|
+
process.stdout.write(
|
|
12104
|
+
`[daemon] transport-probe reported transport=${probe.transport} gateway=${probe.gatewayUrl ?? "null"} private=${probe.gatewayIsPrivate}
|
|
12105
|
+
`
|
|
12106
|
+
);
|
|
10432
12107
|
}
|
|
10433
12108
|
onAgentChanged(payload) {
|
|
10434
12109
|
const a = this.hostedAgents.get(payload.agentImUserId);
|
|
@@ -10498,18 +12173,22 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10498
12173
|
const name = agent?.card?.name || agent?.displayName || agent?.username || profile.agentImUserId;
|
|
10499
12174
|
const now = Date.now();
|
|
10500
12175
|
const tx = this.db.transaction(() => {
|
|
10501
|
-
this.
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
|
|
10505
|
-
|
|
10506
|
-
|
|
10507
|
-
|
|
10508
|
-
|
|
10509
|
-
|
|
10510
|
-
|
|
10511
|
-
|
|
10512
|
-
|
|
12176
|
+
if (this.rejectedHostedAgentIds.has(profile.agentImUserId)) {
|
|
12177
|
+
this.db.prepare("DELETE FROM agents WHERE im_user_id = ?").run(profile.agentImUserId);
|
|
12178
|
+
} else {
|
|
12179
|
+
this.db.prepare(
|
|
12180
|
+
`INSERT OR REPLACE INTO agents
|
|
12181
|
+
(im_user_id, workspace_id, name, adapter_name, capabilities, status, version, synced_at, dirty)
|
|
12182
|
+
VALUES (?, ?, ?, ?, ?, 'offline', 1, ?, 0)`
|
|
12183
|
+
).run(
|
|
12184
|
+
profile.agentImUserId,
|
|
12185
|
+
profile.workspaceId,
|
|
12186
|
+
name,
|
|
12187
|
+
profile.adapterName,
|
|
12188
|
+
JSON.stringify(capabilities),
|
|
12189
|
+
now
|
|
12190
|
+
);
|
|
12191
|
+
}
|
|
10513
12192
|
this.db.prepare(
|
|
10514
12193
|
`INSERT OR REPLACE INTO agent_profiles
|
|
10515
12194
|
(id, workspace_id, agent_im_user_id, adapter_name, name, config, version, synced_at, dirty, deleted_at)
|
|
@@ -10871,9 +12550,9 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10871
12550
|
function resolveUserAssetWorkspaceDir(workspaceId) {
|
|
10872
12551
|
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
10873
12552
|
if (override) return (0, import_node_path11.join)(override, workspaceId);
|
|
10874
|
-
const home = (0,
|
|
12553
|
+
const home = (0, import_node_os8.homedir)();
|
|
10875
12554
|
const desktop = (0, import_node_path11.join)(home, "Desktop");
|
|
10876
|
-
const base = (0,
|
|
12555
|
+
const base = (0, import_node_fs16.existsSync)(desktop) ? desktop : home;
|
|
10877
12556
|
return (0, import_node_path11.join)(base, "Prismer Assets", workspaceId);
|
|
10878
12557
|
}
|
|
10879
12558
|
function readTargetDaemonId(payload) {
|
|
@@ -10950,7 +12629,7 @@ function safeJsonParse(raw) {
|
|
|
10950
12629
|
|
|
10951
12630
|
// src/pair.ts
|
|
10952
12631
|
var import_node_crypto10 = require("crypto");
|
|
10953
|
-
var
|
|
12632
|
+
var import_node_os9 = require("os");
|
|
10954
12633
|
var import_promises2 = require("timers/promises");
|
|
10955
12634
|
var import_qrcode = __toESM(require("qrcode"), 1);
|
|
10956
12635
|
async function pair(opts) {
|
|
@@ -10980,7 +12659,7 @@ async function pair(opts) {
|
|
|
10980
12659
|
"/api/im/pair/offer",
|
|
10981
12660
|
{
|
|
10982
12661
|
auth: false,
|
|
10983
|
-
body: { devicePub, deviceName: opts.deviceName ?? (0,
|
|
12662
|
+
body: { devicePub, deviceName: opts.deviceName ?? (0, import_node_os9.hostname)() }
|
|
10984
12663
|
}
|
|
10985
12664
|
);
|
|
10986
12665
|
if (!offerRes.ok) {
|
|
@@ -11061,8 +12740,8 @@ var import_commander20 = require("commander");
|
|
|
11061
12740
|
|
|
11062
12741
|
// src/cli/commands/adapter.ts
|
|
11063
12742
|
var import_node_child_process5 = require("child_process");
|
|
11064
|
-
var
|
|
11065
|
-
var
|
|
12743
|
+
var import_node_fs18 = require("fs");
|
|
12744
|
+
var import_node_os10 = require("os");
|
|
11066
12745
|
var import_node_path13 = require("path");
|
|
11067
12746
|
var import_commander = require("commander");
|
|
11068
12747
|
init_hermes();
|
|
@@ -11078,7 +12757,7 @@ var INSTALL_SPECS = {
|
|
|
11078
12757
|
binary: "claude",
|
|
11079
12758
|
hint: "Set ANTHROPIC_API_KEY in your shell profile, or rely on Claude Code OAuth login. Run `claude login` to sign in.",
|
|
11080
12759
|
authHints: ["ANTHROPIC_API_KEY or Claude Code OAuth login (`claude login`)"],
|
|
11081
|
-
hookTarget: { path: (0, import_node_path13.join)((0,
|
|
12760
|
+
hookTarget: { path: (0, import_node_path13.join)((0, import_node_os10.homedir)(), ".claude", "hooks.json"), kind: "json-file" }
|
|
11082
12761
|
},
|
|
11083
12762
|
openclaw: {
|
|
11084
12763
|
name: "openclaw",
|
|
@@ -11087,7 +12766,7 @@ var INSTALL_SPECS = {
|
|
|
11087
12766
|
binary: "openclaw",
|
|
11088
12767
|
hint: "OpenClaw runs as a gateway. Configure ~/.openclaw/openclaw.json and start it with `openclaw gateway` before tasks dispatch.",
|
|
11089
12768
|
authHints: ["~/.openclaw/openclaw.json gateway.auth.bearerTokens", "Prismer daemon api_key"],
|
|
11090
|
-
hookTarget: { path: (0, import_node_path13.join)((0,
|
|
12769
|
+
hookTarget: { path: (0, import_node_path13.join)((0, import_node_os10.homedir)(), ".openclaw", "hooks"), kind: "directory" }
|
|
11091
12770
|
},
|
|
11092
12771
|
codex: {
|
|
11093
12772
|
name: "codex",
|
|
@@ -11096,7 +12775,7 @@ var INSTALL_SPECS = {
|
|
|
11096
12775
|
binary: "codex",
|
|
11097
12776
|
hint: "Set OPENAI_API_KEY in your shell profile. Verify with `codex --help`.",
|
|
11098
12777
|
authHints: ["OPENAI_API_KEY or Codex CLI account login"],
|
|
11099
|
-
hookTarget: { path: (0, import_node_path13.join)((0,
|
|
12778
|
+
hookTarget: { path: (0, import_node_path13.join)((0, import_node_os10.homedir)(), ".codex", "hooks.json"), kind: "json-file" }
|
|
11100
12779
|
},
|
|
11101
12780
|
hermes: {
|
|
11102
12781
|
name: "hermes",
|
|
@@ -11105,7 +12784,7 @@ var INSTALL_SPECS = {
|
|
|
11105
12784
|
binary: "hermes",
|
|
11106
12785
|
hint: "Hermes runs as a long-lived HTTP gateway in your own Python venv. Start with `hermes -p <profile> gateway` after configuring ~/.hermes/.env.",
|
|
11107
12786
|
authHints: ["~/.hermes/.env API_SERVER_KEY", "Hermes profile provider credentials"],
|
|
11108
|
-
hookTarget: { path: (0, import_node_path13.join)((0,
|
|
12787
|
+
hookTarget: { path: (0, import_node_path13.join)((0, import_node_os10.homedir)(), ".hermes", "hooks.json"), kind: "json-file" }
|
|
11109
12788
|
}
|
|
11110
12789
|
};
|
|
11111
12790
|
function buildAdapterCommand() {
|
|
@@ -11320,15 +12999,15 @@ function runHooks(spec, opts) {
|
|
|
11320
12999
|
}
|
|
11321
13000
|
let backup;
|
|
11322
13001
|
if (planned.kind === "directory") {
|
|
11323
|
-
(0,
|
|
13002
|
+
(0, import_node_fs18.mkdirSync)(planned.path, { recursive: true });
|
|
11324
13003
|
const markerPath = (0, import_node_path13.join)(planned.path, "prismer.json");
|
|
11325
13004
|
backup = backupIfExists(markerPath);
|
|
11326
|
-
(0,
|
|
13005
|
+
(0, import_node_fs18.writeFileSync)(markerPath, JSON.stringify(prismerHookMarker(spec), null, 2) + "\n", "utf8");
|
|
11327
13006
|
} else {
|
|
11328
|
-
(0,
|
|
13007
|
+
(0, import_node_fs18.mkdirSync)((0, import_node_path13.dirname)(planned.path), { recursive: true });
|
|
11329
13008
|
backup = backupIfExists(planned.path);
|
|
11330
13009
|
const merged = mergeHookJson(planned.path, spec);
|
|
11331
|
-
(0,
|
|
13010
|
+
(0, import_node_fs18.writeFileSync)(planned.path, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
11332
13011
|
}
|
|
11333
13012
|
return {
|
|
11334
13013
|
ok: true,
|
|
@@ -11395,23 +13074,23 @@ function inspectAuthEnv(spec) {
|
|
|
11395
13074
|
hints: spec.authHints ?? []
|
|
11396
13075
|
};
|
|
11397
13076
|
case "hermes": {
|
|
11398
|
-
const envFile = (0, import_node_path13.join)((0,
|
|
13077
|
+
const envFile = (0, import_node_path13.join)((0, import_node_os10.homedir)(), ".hermes", ".env");
|
|
11399
13078
|
return {
|
|
11400
|
-
ok: (0,
|
|
13079
|
+
ok: (0, import_node_fs18.existsSync)(envFile) || Boolean(process.env.HERMES_API_KEY || process.env.API_SERVER_KEY),
|
|
11401
13080
|
present: [
|
|
11402
13081
|
...["HERMES_API_KEY", "API_SERVER_KEY"].filter((k) => Boolean(process.env[k])),
|
|
11403
|
-
...(0,
|
|
13082
|
+
...(0, import_node_fs18.existsSync)(envFile) ? [envFile] : []
|
|
11404
13083
|
],
|
|
11405
13084
|
hints: spec.authHints ?? []
|
|
11406
13085
|
};
|
|
11407
13086
|
}
|
|
11408
13087
|
case "openclaw": {
|
|
11409
|
-
const cfgFile = (0, import_node_path13.join)((0,
|
|
13088
|
+
const cfgFile = (0, import_node_path13.join)((0, import_node_os10.homedir)(), ".openclaw", "openclaw.json");
|
|
11410
13089
|
return {
|
|
11411
|
-
ok: (0,
|
|
13090
|
+
ok: (0, import_node_fs18.existsSync)(cfgFile) || Boolean(process.env.OPENCLAW_API_KEY),
|
|
11412
13091
|
present: [
|
|
11413
13092
|
...["OPENCLAW_API_KEY"].filter((k) => Boolean(process.env[k])),
|
|
11414
|
-
...(0,
|
|
13093
|
+
...(0, import_node_fs18.existsSync)(cfgFile) ? [cfgFile] : []
|
|
11415
13094
|
],
|
|
11416
13095
|
hints: spec.authHints ?? []
|
|
11417
13096
|
};
|
|
@@ -11426,16 +13105,16 @@ function inspectHook(spec) {
|
|
|
11426
13105
|
return { supported: false, markerPresent: false };
|
|
11427
13106
|
}
|
|
11428
13107
|
if (spec.name === "openclaw") {
|
|
11429
|
-
const jsonPath = (0, import_node_path13.join)((0,
|
|
13108
|
+
const jsonPath = (0, import_node_path13.join)((0, import_node_os10.homedir)(), ".openclaw", "hooks.json");
|
|
11430
13109
|
const markerPath = (0, import_node_path13.join)(target.path, "prismer.json");
|
|
11431
|
-
const jsonMarkerPresent = (0,
|
|
11432
|
-
const dirMarkerPresent = (0,
|
|
13110
|
+
const jsonMarkerPresent = (0, import_node_fs18.existsSync)(jsonPath) && fileContainsPrismerMarker(jsonPath);
|
|
13111
|
+
const dirMarkerPresent = (0, import_node_fs18.existsSync)(markerPath) && fileContainsPrismerMarker(markerPath);
|
|
11433
13112
|
return {
|
|
11434
13113
|
supported: true,
|
|
11435
13114
|
kind: "json-file-or-directory",
|
|
11436
13115
|
path: target.path,
|
|
11437
13116
|
jsonPath,
|
|
11438
|
-
exists: (0,
|
|
13117
|
+
exists: (0, import_node_fs18.existsSync)(target.path) || (0, import_node_fs18.existsSync)(jsonPath),
|
|
11439
13118
|
markerPath,
|
|
11440
13119
|
markerPresent: jsonMarkerPresent || dirMarkerPresent
|
|
11441
13120
|
};
|
|
@@ -11446,12 +13125,12 @@ function inspectHook(spec) {
|
|
|
11446
13125
|
supported: true,
|
|
11447
13126
|
kind: target.kind,
|
|
11448
13127
|
path: target.path,
|
|
11449
|
-
exists: (0,
|
|
13128
|
+
exists: (0, import_node_fs18.existsSync)(target.path),
|
|
11450
13129
|
markerPath,
|
|
11451
|
-
markerPresent: (0,
|
|
13130
|
+
markerPresent: (0, import_node_fs18.existsSync)(markerPath) && fileContainsPrismerMarker(markerPath)
|
|
11452
13131
|
};
|
|
11453
13132
|
}
|
|
11454
|
-
const exists = (0,
|
|
13133
|
+
const exists = (0, import_node_fs18.existsSync)(target.path);
|
|
11455
13134
|
if (!exists) {
|
|
11456
13135
|
return {
|
|
11457
13136
|
supported: true,
|
|
@@ -11487,14 +13166,14 @@ function prismerHookMarker(spec) {
|
|
|
11487
13166
|
}
|
|
11488
13167
|
function mergeHookJson(path9, spec) {
|
|
11489
13168
|
const marker = prismerHookMarker(spec);
|
|
11490
|
-
if (!(0,
|
|
13169
|
+
if (!(0, import_node_fs18.existsSync)(path9)) {
|
|
11491
13170
|
return {
|
|
11492
13171
|
prismer: marker
|
|
11493
13172
|
};
|
|
11494
13173
|
}
|
|
11495
13174
|
let parsed;
|
|
11496
13175
|
try {
|
|
11497
|
-
parsed = JSON.parse((0,
|
|
13176
|
+
parsed = JSON.parse((0, import_node_fs18.readFileSync)(path9, "utf8"));
|
|
11498
13177
|
} catch (err) {
|
|
11499
13178
|
throw new Error(`Cannot parse ${path9} as JSON: ${err.message}`);
|
|
11500
13179
|
}
|
|
@@ -11518,17 +13197,17 @@ function mergeHookJson(path9, spec) {
|
|
|
11518
13197
|
return next;
|
|
11519
13198
|
}
|
|
11520
13199
|
function backupIfExists(path9) {
|
|
11521
|
-
if (!(0,
|
|
13200
|
+
if (!(0, import_node_fs18.existsSync)(path9)) return null;
|
|
11522
13201
|
const suffix = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
11523
13202
|
const backup = `${path9}.bak.${suffix}`;
|
|
11524
|
-
const stat = (0,
|
|
13203
|
+
const stat = (0, import_node_fs18.statSync)(path9);
|
|
11525
13204
|
if (stat.isDirectory()) return null;
|
|
11526
|
-
(0,
|
|
13205
|
+
(0, import_node_fs18.copyFileSync)(path9, backup);
|
|
11527
13206
|
return backup;
|
|
11528
13207
|
}
|
|
11529
13208
|
function fileContainsPrismerMarker(path9) {
|
|
11530
13209
|
try {
|
|
11531
|
-
return (0,
|
|
13210
|
+
return (0, import_node_fs18.readFileSync)(path9, "utf8").includes("prismer-daemon-runtime");
|
|
11532
13211
|
} catch {
|
|
11533
13212
|
return false;
|
|
11534
13213
|
}
|
|
@@ -11888,8 +13567,74 @@ function buildAgentCommand() {
|
|
|
11888
13567
|
}
|
|
11889
13568
|
getUI().ok("Skill install requested", `agent=${opts.agent} skill=${slugOrId}`);
|
|
11890
13569
|
}, { code: "agent_install_skill_failed" }));
|
|
13570
|
+
cmd.command("snapshot <imUserId>").description("Create an agent-level snapshot").option("--include-memory", "Include memory dump reference").option("--label <label>", "Snapshot label").option("--json", "Output JSON").action(runAction(async (imUserId, opts) => {
|
|
13571
|
+
const cloud = makeCloudClient();
|
|
13572
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agents/${encodeURIComponent(imUserId)}/snapshot`, {
|
|
13573
|
+
includeMemory: Boolean(opts.includeMemory),
|
|
13574
|
+
label: opts.label
|
|
13575
|
+
}, "agent_snapshot_failed");
|
|
13576
|
+
if (opts.json) {
|
|
13577
|
+
printJson(data);
|
|
13578
|
+
return;
|
|
13579
|
+
}
|
|
13580
|
+
const rec = data;
|
|
13581
|
+
getUI().ok("Snapshot created", String(rec.id ?? "<unknown>"));
|
|
13582
|
+
getUI().line(`agent=${String(rec.agentImUserId ?? imUserId)} memory=${rec.includeMemory ? "included" : "stripped"}`);
|
|
13583
|
+
}, { code: "agent_snapshot_failed" }));
|
|
13584
|
+
cmd.command("publish <imUserId>").description("Publish an agent as an Agent Pack").requiredOption("--slug <slug>", "Agent Pack slug").option("--version <version>", "Package version", "1.0.0").option("--license <license>", "Package license", "proprietary").option("--title <title>", "Display title metadata").option("--description <description>", "Description metadata").option("--json", "Output JSON").action(runAction(async (imUserId, opts) => {
|
|
13585
|
+
const cloud = makeCloudClient();
|
|
13586
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agents/${encodeURIComponent(imUserId)}/publish`, {
|
|
13587
|
+
slug: opts.slug,
|
|
13588
|
+
version: opts.version,
|
|
13589
|
+
license: opts.license,
|
|
13590
|
+
metadata: {
|
|
13591
|
+
...opts.title ? { title: opts.title } : {},
|
|
13592
|
+
...opts.description ? { description: opts.description } : {}
|
|
13593
|
+
},
|
|
13594
|
+
stripMemory: true
|
|
13595
|
+
}, "agent_publish_failed");
|
|
13596
|
+
if (opts.json) {
|
|
13597
|
+
printJson(data);
|
|
13598
|
+
return;
|
|
13599
|
+
}
|
|
13600
|
+
const rec = data;
|
|
13601
|
+
getUI().ok("Agent Pack published", `${String(rec.slug ?? opts.slug)}@${String(rec.version ?? opts.version ?? "1.0.0")}`);
|
|
13602
|
+
getUI().line(`package=${String(rec.id ?? "<unknown>")}`);
|
|
13603
|
+
}, { code: "agent_publish_failed" }));
|
|
13604
|
+
cmd.command("fork <packIdOrSlug>").description("Fork an Agent Pack into a workspace").requiredOption("--workspace-id <id>", "Target workspace id").option("--display-name <name>", "New agent display name").option("--json", "Output JSON").action(runAction(async (packIdOrSlug, opts) => {
|
|
13605
|
+
const cloud = makeCloudClient();
|
|
13606
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agent-packs/${encodeURIComponent(packIdOrSlug)}/fork`, {
|
|
13607
|
+
targetWorkspaceId: opts.workspaceId,
|
|
13608
|
+
displayName: opts.displayName
|
|
13609
|
+
}, "agent_fork_failed");
|
|
13610
|
+
if (opts.json) {
|
|
13611
|
+
printJson(data);
|
|
13612
|
+
return;
|
|
13613
|
+
}
|
|
13614
|
+
const rec = data;
|
|
13615
|
+
getUI().ok("Agent Pack forked", `newAgent=${String(rec.newImUserId ?? "<unknown>")}`);
|
|
13616
|
+
getUI().line(`did=${String(rec.newDid ?? "not issued")}`);
|
|
13617
|
+
}, { code: "agent_fork_failed" }));
|
|
11891
13618
|
return cmd;
|
|
11892
13619
|
}
|
|
13620
|
+
function makeCloudClient() {
|
|
13621
|
+
const cfg = loadConfig(resolvePaths());
|
|
13622
|
+
return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
13623
|
+
}
|
|
13624
|
+
async function requestEnvelope(cloud, method, path9, body, code) {
|
|
13625
|
+
const res = await cloud.request(
|
|
13626
|
+
method,
|
|
13627
|
+
path9,
|
|
13628
|
+
body === void 0 ? void 0 : { body }
|
|
13629
|
+
);
|
|
13630
|
+
if (!res.ok || res.data?.ok === false) {
|
|
13631
|
+
const message = res.error?.message ?? res.data?.error?.message ?? "request failed";
|
|
13632
|
+
exitWithError(`${path9} failed (${res.status === 0 ? "network error" : `HTTP ${res.status}`}): ${message}`, {
|
|
13633
|
+
code: res.error?.code ?? res.data?.error?.code ?? code
|
|
13634
|
+
});
|
|
13635
|
+
}
|
|
13636
|
+
return res.data && typeof res.data === "object" && "data" in res.data ? res.data.data : res.data;
|
|
13637
|
+
}
|
|
11893
13638
|
function readLocalAgents(localDb) {
|
|
11894
13639
|
const db = openLocalDb(localDb);
|
|
11895
13640
|
try {
|
|
@@ -11994,7 +13739,7 @@ function whichBinary2(bin) {
|
|
|
11994
13739
|
// src/cli/commands/asset.ts
|
|
11995
13740
|
var import_node_crypto11 = require("crypto");
|
|
11996
13741
|
var import_commander3 = require("commander");
|
|
11997
|
-
var
|
|
13742
|
+
var import_node_fs19 = require("fs");
|
|
11998
13743
|
var import_node_path14 = require("path");
|
|
11999
13744
|
init_util();
|
|
12000
13745
|
init_ui();
|
|
@@ -12167,8 +13912,8 @@ async function prefetchAssetBytes(opts) {
|
|
|
12167
13912
|
return { considered: rows.length, fetched, bytes, failed };
|
|
12168
13913
|
}
|
|
12169
13914
|
async function uploadAssetPath(inputPath, opts) {
|
|
12170
|
-
if (!(0,
|
|
12171
|
-
const stat = (0,
|
|
13915
|
+
if (!(0, import_node_fs19.existsSync)(inputPath)) exitWithError(`path not found: ${inputPath}`);
|
|
13916
|
+
const stat = (0, import_node_fs19.lstatSync)(inputPath);
|
|
12172
13917
|
if (stat.isSymbolicLink()) exitWithError(`refusing to upload symlink: ${inputPath}`);
|
|
12173
13918
|
if (stat.isFile()) {
|
|
12174
13919
|
return uploadAsset(inputPath, { ...opts, folderPath: normalizeFolderPath(opts.folderPath) });
|
|
@@ -12186,9 +13931,9 @@ async function uploadAssetPath(inputPath, opts) {
|
|
|
12186
13931
|
return { ok: true, data: { count: items.length, items } };
|
|
12187
13932
|
}
|
|
12188
13933
|
async function uploadAsset(file, opts) {
|
|
12189
|
-
if (!(0,
|
|
13934
|
+
if (!(0, import_node_fs19.existsSync)(file)) exitWithError(`file not found: ${file}`);
|
|
12190
13935
|
const cfg = loadConfig(resolvePaths());
|
|
12191
|
-
const bytes = (0,
|
|
13936
|
+
const bytes = (0, import_node_fs19.readFileSync)(file);
|
|
12192
13937
|
const contentHash = (0, import_node_crypto11.createHash)("sha256").update(bytes).digest("hex");
|
|
12193
13938
|
const metadata = parseMetadata(opts.metadata);
|
|
12194
13939
|
if (opts.taskId && metadata.taskId === void 0) metadata.taskId = opts.taskId;
|
|
@@ -12324,10 +14069,10 @@ async function putDirectAssetBytes(plan, bytes, mime) {
|
|
|
12324
14069
|
}
|
|
12325
14070
|
function listFilesRecursive(root) {
|
|
12326
14071
|
const out = [];
|
|
12327
|
-
for (const name of (0,
|
|
14072
|
+
for (const name of (0, import_node_fs19.readdirSync)(root)) {
|
|
12328
14073
|
if (name.startsWith(".")) continue;
|
|
12329
14074
|
const full = (0, import_node_path14.join)(root, name);
|
|
12330
|
-
const stat = (0,
|
|
14075
|
+
const stat = (0, import_node_fs19.lstatSync)(full);
|
|
12331
14076
|
if (stat.isSymbolicLink()) continue;
|
|
12332
14077
|
if (stat.isDirectory()) {
|
|
12333
14078
|
out.push(...listFilesRecursive(full));
|
|
@@ -12361,7 +14106,7 @@ async function downloadAsset(assetId, outPath) {
|
|
|
12361
14106
|
exitWithError(`asset download failed (${res.status}): ${errorMessage(body)}`);
|
|
12362
14107
|
}
|
|
12363
14108
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
12364
|
-
(0,
|
|
14109
|
+
(0, import_node_fs19.writeFileSync)(outPath, bytes);
|
|
12365
14110
|
}
|
|
12366
14111
|
function parseMetadata(raw) {
|
|
12367
14112
|
if (!raw) return {};
|
|
@@ -12760,7 +14505,7 @@ function buildCookbookCommand() {
|
|
|
12760
14505
|
for (const suite of suites) {
|
|
12761
14506
|
checks.push(...await runSuite(suite, cloud, opts));
|
|
12762
14507
|
}
|
|
12763
|
-
const summary =
|
|
14508
|
+
const summary = summarize2(checks, Boolean(opts.strict));
|
|
12764
14509
|
if (opts.json) {
|
|
12765
14510
|
printJson(summary);
|
|
12766
14511
|
} else {
|
|
@@ -12925,7 +14670,7 @@ function parseSuites(raw) {
|
|
|
12925
14670
|
const expanded = selected.includes("all") ? ["status", "im", "task", "group", "asset", "sandbox"] : selected;
|
|
12926
14671
|
return [...new Set(expanded)];
|
|
12927
14672
|
}
|
|
12928
|
-
function
|
|
14673
|
+
function summarize2(checks, strict) {
|
|
12929
14674
|
const totals = { pass: 0, fail: 0, skip: 0 };
|
|
12930
14675
|
for (const check of checks) totals[check.status] += 1;
|
|
12931
14676
|
return {
|
|
@@ -13008,7 +14753,7 @@ function parsePositiveInt3(value) {
|
|
|
13008
14753
|
// src/cli/commands/daemon.ts
|
|
13009
14754
|
var import_commander8 = require("commander");
|
|
13010
14755
|
var import_node_child_process7 = require("child_process");
|
|
13011
|
-
var
|
|
14756
|
+
var import_node_fs20 = require("fs");
|
|
13012
14757
|
var import_promises3 = require("timers/promises");
|
|
13013
14758
|
var import_node_path15 = require("path");
|
|
13014
14759
|
init_util();
|
|
@@ -13084,9 +14829,9 @@ function buildDaemonCommand() {
|
|
|
13084
14829
|
if (existingPid && pidAlive2(existingPid)) {
|
|
13085
14830
|
exitWithError(`Daemon already running (pid ${existingPid}). Use \`prismer daemon stop\` first.`);
|
|
13086
14831
|
}
|
|
13087
|
-
if (!(0,
|
|
14832
|
+
if (!(0, import_node_fs20.existsSync)(paths.logsDir)) (0, import_node_fs20.mkdirSync)(paths.logsDir, { recursive: true });
|
|
13088
14833
|
const logFile = (0, import_node_path15.join)(paths.logsDir, "daemon.log");
|
|
13089
|
-
const fd = (0,
|
|
14834
|
+
const fd = (0, import_node_fs20.openSync)(logFile, "a");
|
|
13090
14835
|
const args = [process.argv[1], "daemon", "run"];
|
|
13091
14836
|
if (opts.port) args.push("--port", String(opts.port));
|
|
13092
14837
|
if (opts.localServer === false) args.push("--no-local-server");
|
|
@@ -13170,7 +14915,7 @@ function buildDaemonCommand() {
|
|
|
13170
14915
|
cmd.command("logs").description("Show daemon logs").option("--tail <n>", "Number of lines to show", (v) => Number.parseInt(v, 10), 80).option("--follow", "Follow log output").option("--json", "Accept --json for global flag compatibility (raw log bytes are streamed)").action(async (opts) => {
|
|
13171
14916
|
const paths = resolvePaths();
|
|
13172
14917
|
const logFile = (0, import_node_path15.join)(paths.logsDir, "daemon.log");
|
|
13173
|
-
if (!(0,
|
|
14918
|
+
if (!(0, import_node_fs20.existsSync)(logFile)) {
|
|
13174
14919
|
exitWithError(`No daemon log found at ${logFile}. Start the daemon with \`prismer daemon start\`.`);
|
|
13175
14920
|
}
|
|
13176
14921
|
const lines = Math.max(1, opts.tail);
|
|
@@ -13211,13 +14956,13 @@ async function tailFromEnd(path9, lines) {
|
|
|
13211
14956
|
}
|
|
13212
14957
|
}
|
|
13213
14958
|
async function followFile(path9) {
|
|
13214
|
-
let offset = (0,
|
|
14959
|
+
let offset = (0, import_node_fs20.statSync)(path9).size;
|
|
13215
14960
|
for (; ; ) {
|
|
13216
14961
|
await (0, import_promises3.setTimeout)(1e3);
|
|
13217
|
-
const size = (0,
|
|
14962
|
+
const size = (0, import_node_fs20.statSync)(path9).size;
|
|
13218
14963
|
if (size < offset) offset = 0;
|
|
13219
14964
|
if (size === offset) continue;
|
|
13220
|
-
const stream = (0,
|
|
14965
|
+
const stream = (0, import_node_fs20.createReadStream)(path9, { start: offset, end: size - 1, encoding: "utf8" });
|
|
13221
14966
|
for await (const chunk of stream) process.stdout.write(chunk);
|
|
13222
14967
|
offset = size;
|
|
13223
14968
|
}
|
|
@@ -13225,8 +14970,8 @@ async function followFile(path9) {
|
|
|
13225
14970
|
|
|
13226
14971
|
// src/cli/commands/events.ts
|
|
13227
14972
|
var import_commander9 = require("commander");
|
|
13228
|
-
var
|
|
13229
|
-
var
|
|
14973
|
+
var import_node_fs21 = require("fs");
|
|
14974
|
+
var import_node_os11 = require("os");
|
|
13230
14975
|
var import_node_path16 = require("path");
|
|
13231
14976
|
var import_node_readline = require("readline");
|
|
13232
14977
|
init_util();
|
|
@@ -13234,7 +14979,7 @@ var DEFAULT_LIMIT2 = 50;
|
|
|
13234
14979
|
function buildEventsCommand() {
|
|
13235
14980
|
return addEventOptions(new import_commander9.Command("events").description("Read local PARA events from ~/.prismer/para/events.jsonl")).action(async (opts) => {
|
|
13236
14981
|
const file = eventsPath();
|
|
13237
|
-
if (!(0,
|
|
14982
|
+
if (!(0, import_node_fs21.existsSync)(file)) {
|
|
13238
14983
|
printJson(unavailable(file));
|
|
13239
14984
|
process.exitCode = 1;
|
|
13240
14985
|
return;
|
|
@@ -13251,7 +14996,7 @@ function buildEventsCommand() {
|
|
|
13251
14996
|
function buildEventsStatsCommand() {
|
|
13252
14997
|
return addEventOptions(new import_commander9.Command("events:stats").description("Summarize local PARA events from ~/.prismer/para/events.jsonl")).action(async (opts) => {
|
|
13253
14998
|
const file = eventsPath();
|
|
13254
|
-
if (!(0,
|
|
14999
|
+
if (!(0, import_node_fs21.existsSync)(file)) {
|
|
13255
15000
|
printJson(unavailable(file));
|
|
13256
15001
|
process.exitCode = 1;
|
|
13257
15002
|
return;
|
|
@@ -13260,7 +15005,7 @@ function buildEventsStatsCommand() {
|
|
|
13260
15005
|
const events = await readEvents(file, filters);
|
|
13261
15006
|
printJson({
|
|
13262
15007
|
ok: true,
|
|
13263
|
-
data: { stats:
|
|
15008
|
+
data: { stats: summarize3(events), count: events.length, limit: filters.limit },
|
|
13264
15009
|
checks: { file, exists: true, filters: cleanFilters(filters) }
|
|
13265
15010
|
});
|
|
13266
15011
|
});
|
|
@@ -13269,7 +15014,7 @@ function addEventOptions(cmd) {
|
|
|
13269
15014
|
return cmd.option("--limit <n>", "Max events to return/read", parsePositiveInt4, DEFAULT_LIMIT2).option("--agent-id <id>", "Filter by agent id").option("--session-id <id>", "Filter by session id").option("--family <name>", "Filter by event family").option("--type <name>", "Filter by event type").option("--json", "Output JSON (default)");
|
|
13270
15015
|
}
|
|
13271
15016
|
function eventsPath() {
|
|
13272
|
-
return (0, import_node_path16.join)(process.env.PRISMER_HOME ?? (0, import_node_path16.join)((0,
|
|
15017
|
+
return (0, import_node_path16.join)(process.env.PRISMER_HOME ?? (0, import_node_path16.join)((0, import_node_os11.homedir)(), ".prismer"), "para", "events.jsonl");
|
|
13273
15018
|
}
|
|
13274
15019
|
function unavailable(file) {
|
|
13275
15020
|
return {
|
|
@@ -13285,7 +15030,7 @@ function unavailable(file) {
|
|
|
13285
15030
|
async function readEvents(file, filters) {
|
|
13286
15031
|
const out = [];
|
|
13287
15032
|
const rl = (0, import_node_readline.createInterface)({
|
|
13288
|
-
input: (0,
|
|
15033
|
+
input: (0, import_node_fs21.createReadStream)(file, { encoding: "utf8" }),
|
|
13289
15034
|
crlfDelay: Infinity
|
|
13290
15035
|
});
|
|
13291
15036
|
for await (const line of rl) {
|
|
@@ -13317,7 +15062,7 @@ function field(event, names) {
|
|
|
13317
15062
|
}
|
|
13318
15063
|
return void 0;
|
|
13319
15064
|
}
|
|
13320
|
-
function
|
|
15065
|
+
function summarize3(events) {
|
|
13321
15066
|
const byFamily = {};
|
|
13322
15067
|
const byType = {};
|
|
13323
15068
|
const byAgentId = {};
|
|
@@ -13355,7 +15100,7 @@ function parsePositiveInt4(v) {
|
|
|
13355
15100
|
// src/cli/commands/memory.ts
|
|
13356
15101
|
var import_better_sqlite35 = __toESM(require("better-sqlite3"), 1);
|
|
13357
15102
|
var import_commander10 = require("commander");
|
|
13358
|
-
var
|
|
15103
|
+
var import_node_fs22 = require("fs");
|
|
13359
15104
|
init_util();
|
|
13360
15105
|
var LOCAL_BASE = process.env.PRISMER_DAEMON_URL ?? "http://127.0.0.1:3210";
|
|
13361
15106
|
function buildMemoryCommand() {
|
|
@@ -13507,7 +15252,7 @@ function readCacheSnapshot(limit) {
|
|
|
13507
15252
|
const paths = resolvePaths();
|
|
13508
15253
|
const empty = {
|
|
13509
15254
|
dbPath: paths.localDb,
|
|
13510
|
-
dbExists: (0,
|
|
15255
|
+
dbExists: (0, import_node_fs22.existsSync)(paths.localDb),
|
|
13511
15256
|
tables: {
|
|
13512
15257
|
cached_assets: { exists: false, count: 0, sizeBytes: 0 },
|
|
13513
15258
|
workspace_files_mirror: { exists: false, count: 0 }
|
|
@@ -13671,8 +15416,8 @@ function buildPairCommand() {
|
|
|
13671
15416
|
|
|
13672
15417
|
// src/cli/commands/profile.ts
|
|
13673
15418
|
var import_commander12 = require("commander");
|
|
13674
|
-
var
|
|
13675
|
-
var
|
|
15419
|
+
var import_node_fs23 = require("fs");
|
|
15420
|
+
var import_node_os12 = require("os");
|
|
13676
15421
|
var import_node_path17 = require("path");
|
|
13677
15422
|
var import_node_child_process8 = require("child_process");
|
|
13678
15423
|
init_util();
|
|
@@ -13721,12 +15466,12 @@ function buildProfileCommand() {
|
|
|
13721
15466
|
const profile = await cloud.get(
|
|
13722
15467
|
`/api/im/agent_profiles/${encodeURIComponent(profileId)}`
|
|
13723
15468
|
);
|
|
13724
|
-
const tmpFile = (0, import_node_path17.join)((0,
|
|
13725
|
-
(0,
|
|
15469
|
+
const tmpFile = (0, import_node_path17.join)((0, import_node_os12.tmpdir)(), `prismer-profile-${profileId}.json`);
|
|
15470
|
+
(0, import_node_fs23.writeFileSync)(tmpFile, JSON.stringify(profile.config, null, 2), "utf8");
|
|
13726
15471
|
const editor = process.env.EDITOR || "vi";
|
|
13727
15472
|
const ed = (0, import_node_child_process8.spawnSync)(editor, [tmpFile], { stdio: "inherit" });
|
|
13728
15473
|
if (ed.status !== 0) exitWithError(`editor exited ${ed.status}`, { code: "editor_failed" });
|
|
13729
|
-
const newConfig = JSON.parse((0,
|
|
15474
|
+
const newConfig = JSON.parse((0, import_node_fs23.readFileSync)(tmpFile, "utf8"));
|
|
13730
15475
|
const res = await cloud.request("PATCH", `/api/im/agent_profiles/${encodeURIComponent(profileId)}`, {
|
|
13731
15476
|
body: { config: newConfig, version: profile.version }
|
|
13732
15477
|
});
|
|
@@ -13748,8 +15493,8 @@ function mkCloud4() {
|
|
|
13748
15493
|
function readJsonArg(arg) {
|
|
13749
15494
|
if (arg.startsWith("@")) {
|
|
13750
15495
|
const path9 = arg.slice(1);
|
|
13751
|
-
if (!(0,
|
|
13752
|
-
return JSON.parse((0,
|
|
15496
|
+
if (!(0, import_node_fs23.existsSync)(path9)) throw new Error(`File not found: ${path9}`);
|
|
15497
|
+
return JSON.parse((0, import_node_fs23.readFileSync)(path9, "utf8"));
|
|
13753
15498
|
}
|
|
13754
15499
|
return JSON.parse(arg);
|
|
13755
15500
|
}
|
|
@@ -13763,8 +15508,8 @@ async function resolveDefaultWorkspaceId(cloud) {
|
|
|
13763
15508
|
// src/cli/commands/reset.ts
|
|
13764
15509
|
var import_commander13 = require("commander");
|
|
13765
15510
|
var import_node_child_process9 = require("child_process");
|
|
13766
|
-
var
|
|
13767
|
-
var
|
|
15511
|
+
var import_node_fs24 = require("fs");
|
|
15512
|
+
var import_node_os13 = require("os");
|
|
13768
15513
|
var import_node_path18 = require("path");
|
|
13769
15514
|
var import_promises4 = require("timers/promises");
|
|
13770
15515
|
init_util();
|
|
@@ -13782,15 +15527,15 @@ function buildResetCommand() {
|
|
|
13782
15527
|
const hermesGatewayPids = findHermesGatewayPids2();
|
|
13783
15528
|
const plan = {
|
|
13784
15529
|
root: paths.root,
|
|
13785
|
-
exists: (0,
|
|
15530
|
+
exists: (0, import_node_fs24.existsSync)(paths.root),
|
|
13786
15531
|
runningPid,
|
|
13787
15532
|
mode: opts.wipe ? "wipe" : "archive",
|
|
13788
15533
|
archivePath,
|
|
13789
15534
|
assetSyncRoot,
|
|
13790
|
-
assetSyncExists: (0,
|
|
15535
|
+
assetSyncExists: (0, import_node_fs24.existsSync)(assetSyncRoot),
|
|
13791
15536
|
assetSyncArchivePath,
|
|
13792
15537
|
hermesHome,
|
|
13793
|
-
hermesExists: (0,
|
|
15538
|
+
hermesExists: (0, import_node_fs24.existsSync)(hermesHome),
|
|
13794
15539
|
hermesArchivePath,
|
|
13795
15540
|
hermesGatewayPids,
|
|
13796
15541
|
willStopDaemon: runningPid !== null,
|
|
@@ -13814,40 +15559,40 @@ function buildResetCommand() {
|
|
|
13814
15559
|
for (const gatewayPid of hermesGatewayPids) {
|
|
13815
15560
|
await stopProcess(gatewayPid, opts.timeout ?? 5e3, "Hermes gateway");
|
|
13816
15561
|
}
|
|
13817
|
-
if ((0,
|
|
15562
|
+
if ((0, import_node_fs24.existsSync)(paths.root)) {
|
|
13818
15563
|
if (opts.wipe) {
|
|
13819
|
-
(0,
|
|
15564
|
+
(0, import_node_fs24.rmSync)(paths.root, { recursive: true, force: true });
|
|
13820
15565
|
} else {
|
|
13821
15566
|
if (!archivePath) throw new Error("internal: archivePath missing");
|
|
13822
|
-
if (!(0,
|
|
13823
|
-
(0,
|
|
15567
|
+
if (!(0, import_node_fs24.existsSync)((0, import_node_path18.dirname)(archivePath))) {
|
|
15568
|
+
(0, import_node_fs24.mkdirSync)((0, import_node_path18.dirname)(archivePath), { recursive: true });
|
|
13824
15569
|
}
|
|
13825
|
-
(0,
|
|
15570
|
+
(0, import_node_fs24.renameSync)(paths.root, archivePath);
|
|
13826
15571
|
}
|
|
13827
15572
|
}
|
|
13828
|
-
if ((0,
|
|
15573
|
+
if ((0, import_node_fs24.existsSync)(assetSyncRoot)) {
|
|
13829
15574
|
if (opts.wipe) {
|
|
13830
|
-
(0,
|
|
15575
|
+
(0, import_node_fs24.rmSync)(assetSyncRoot, { recursive: true, force: true });
|
|
13831
15576
|
} else {
|
|
13832
15577
|
if (!assetSyncArchivePath) throw new Error("internal: assetSyncArchivePath missing");
|
|
13833
|
-
if (!(0,
|
|
13834
|
-
(0,
|
|
15578
|
+
if (!(0, import_node_fs24.existsSync)((0, import_node_path18.dirname)(assetSyncArchivePath))) {
|
|
15579
|
+
(0, import_node_fs24.mkdirSync)((0, import_node_path18.dirname)(assetSyncArchivePath), { recursive: true });
|
|
13835
15580
|
}
|
|
13836
|
-
(0,
|
|
15581
|
+
(0, import_node_fs24.renameSync)(assetSyncRoot, assetSyncArchivePath);
|
|
13837
15582
|
}
|
|
13838
15583
|
}
|
|
13839
|
-
if ((0,
|
|
15584
|
+
if ((0, import_node_fs24.existsSync)(hermesHome)) {
|
|
13840
15585
|
if (opts.wipe) {
|
|
13841
|
-
(0,
|
|
15586
|
+
(0, import_node_fs24.rmSync)(hermesHome, { recursive: true, force: true });
|
|
13842
15587
|
} else {
|
|
13843
15588
|
if (!hermesArchivePath) throw new Error("internal: hermesArchivePath missing");
|
|
13844
|
-
if (!(0,
|
|
13845
|
-
(0,
|
|
15589
|
+
if (!(0, import_node_fs24.existsSync)((0, import_node_path18.dirname)(hermesArchivePath))) {
|
|
15590
|
+
(0, import_node_fs24.mkdirSync)((0, import_node_path18.dirname)(hermesArchivePath), { recursive: true });
|
|
13846
15591
|
}
|
|
13847
|
-
(0,
|
|
15592
|
+
(0, import_node_fs24.renameSync)(hermesHome, hermesArchivePath);
|
|
13848
15593
|
}
|
|
13849
15594
|
}
|
|
13850
|
-
(0,
|
|
15595
|
+
(0, import_node_fs24.mkdirSync)(paths.root, { recursive: true });
|
|
13851
15596
|
if (opts.json) {
|
|
13852
15597
|
printJson({ ok: true, reset: true, plan });
|
|
13853
15598
|
return;
|
|
@@ -13916,13 +15661,13 @@ function timestamp() {
|
|
|
13916
15661
|
function resolveAssetSyncRoot() {
|
|
13917
15662
|
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
13918
15663
|
if (override) return override;
|
|
13919
|
-
const home = (0,
|
|
15664
|
+
const home = (0, import_node_os13.homedir)();
|
|
13920
15665
|
const desktop = (0, import_node_path18.join)(home, "Desktop");
|
|
13921
|
-
const base = (0,
|
|
15666
|
+
const base = (0, import_node_fs24.existsSync)(desktop) ? desktop : home;
|
|
13922
15667
|
return (0, import_node_path18.join)(base, "Prismer Assets");
|
|
13923
15668
|
}
|
|
13924
15669
|
function resolveHermesHome() {
|
|
13925
|
-
return process.env.HERMES_HOME || (0, import_node_path18.join)((0,
|
|
15670
|
+
return process.env.HERMES_HOME || (0, import_node_path18.join)((0, import_node_os13.homedir)(), ".hermes");
|
|
13926
15671
|
}
|
|
13927
15672
|
function findHermesGatewayPids2() {
|
|
13928
15673
|
try {
|
|
@@ -14152,10 +15897,10 @@ async function safeParseResponse(res) {
|
|
|
14152
15897
|
|
|
14153
15898
|
// src/cli/commands/setup.ts
|
|
14154
15899
|
var import_commander15 = require("commander");
|
|
14155
|
-
var
|
|
15900
|
+
var import_node_os14 = require("os");
|
|
14156
15901
|
var import_node_child_process10 = require("child_process");
|
|
14157
15902
|
var import_node_crypto12 = require("crypto");
|
|
14158
|
-
var
|
|
15903
|
+
var import_node_fs25 = require("fs");
|
|
14159
15904
|
var import_node_http2 = require("http");
|
|
14160
15905
|
init_util();
|
|
14161
15906
|
init_ui();
|
|
@@ -14197,7 +15942,7 @@ function buildSetupCommand() {
|
|
|
14197
15942
|
}
|
|
14198
15943
|
const result = await pair({
|
|
14199
15944
|
cloudBaseUrl,
|
|
14200
|
-
deviceName: opts.deviceName ?? (0,
|
|
15945
|
+
deviceName: opts.deviceName ?? (0, import_node_os14.hostname)(),
|
|
14201
15946
|
force: opts.force,
|
|
14202
15947
|
paths,
|
|
14203
15948
|
asUserEmail: opts.asUser
|
|
@@ -14214,13 +15959,13 @@ function buildSetupCommand() {
|
|
|
14214
15959
|
apiKey = await mintDaemonApiKey({
|
|
14215
15960
|
cloudBaseUrl,
|
|
14216
15961
|
token: authToken,
|
|
14217
|
-
deviceName: opts.deviceName ?? (0,
|
|
15962
|
+
deviceName: opts.deviceName ?? (0, import_node_os14.hostname)()
|
|
14218
15963
|
});
|
|
14219
15964
|
}
|
|
14220
15965
|
if (!apiKey && !authToken && !opts.check && opts.browser !== false) {
|
|
14221
15966
|
apiKey = await runBrowserSetup({
|
|
14222
15967
|
cloudBaseUrl,
|
|
14223
|
-
deviceName: opts.deviceName ?? (0,
|
|
15968
|
+
deviceName: opts.deviceName ?? (0, import_node_os14.hostname)(),
|
|
14224
15969
|
json: Boolean(opts.json)
|
|
14225
15970
|
});
|
|
14226
15971
|
}
|
|
@@ -14406,9 +16151,9 @@ function shouldArchiveLocalDb(previous, next) {
|
|
|
14406
16151
|
return previous.api_key !== next.api_key || previous.cloud_api_base !== next.cloud_api_base || previous.daemon_id !== next.daemon_id;
|
|
14407
16152
|
}
|
|
14408
16153
|
function archiveLocalDb(localDbPath) {
|
|
14409
|
-
if (!(0,
|
|
16154
|
+
if (!(0, import_node_fs25.existsSync)(localDbPath)) return;
|
|
14410
16155
|
const archived = `${localDbPath}.${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
|
|
14411
|
-
(0,
|
|
16156
|
+
(0, import_node_fs25.renameSync)(localDbPath, archived);
|
|
14412
16157
|
}
|
|
14413
16158
|
async function mintDaemonApiKey(input) {
|
|
14414
16159
|
const label = `Daemon: ${input.deviceName} ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
|
|
@@ -14477,7 +16222,7 @@ function buildSkillCommand() {
|
|
|
14477
16222
|
const cloud = mkCloud6();
|
|
14478
16223
|
const path9 = opts.agent ? `/api/im/agents/${encodeURIComponent(opts.agent)}/skills` : `/api/im/skills/${encodeURIComponent(slugOrId)}/install`;
|
|
14479
16224
|
const body = opts.agent ? { skillId: slugOrId, workspaceId: opts.workspaceId, version: opts.version } : { workspaceId: opts.workspaceId, version: opts.version };
|
|
14480
|
-
const data = await
|
|
16225
|
+
const data = await requestEnvelope2(cloud, "POST", path9, body, "skill_install_failed");
|
|
14481
16226
|
if (opts.json) {
|
|
14482
16227
|
printJson(data);
|
|
14483
16228
|
return;
|
|
@@ -14488,7 +16233,7 @@ function buildSkillCommand() {
|
|
|
14488
16233
|
const cloud = mkCloud6();
|
|
14489
16234
|
const path9 = opts.agent ? `/api/im/agents/${encodeURIComponent(opts.agent)}/skills` : `/api/im/skills/${encodeURIComponent(slugOrId)}/install`;
|
|
14490
16235
|
const body = opts.agent ? { skillId: slugOrId, workspaceId: opts.workspaceId } : void 0;
|
|
14491
|
-
const data = await
|
|
16236
|
+
const data = await requestEnvelope2(cloud, "DELETE", path9, body, "skill_uninstall_failed");
|
|
14492
16237
|
if (opts.json) {
|
|
14493
16238
|
printJson(data);
|
|
14494
16239
|
return;
|
|
@@ -14541,7 +16286,7 @@ function mkCloud6() {
|
|
|
14541
16286
|
const cfg = loadConfig(resolvePaths());
|
|
14542
16287
|
return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
14543
16288
|
}
|
|
14544
|
-
async function
|
|
16289
|
+
async function requestEnvelope2(cloud, method, path9, body, code) {
|
|
14545
16290
|
const res = await cloud.request(method, path9, {
|
|
14546
16291
|
body
|
|
14547
16292
|
});
|
|
@@ -14660,7 +16405,7 @@ function printSkill(skill, includeContent) {
|
|
|
14660
16405
|
|
|
14661
16406
|
// src/cli/commands/status.ts
|
|
14662
16407
|
var import_commander17 = require("commander");
|
|
14663
|
-
var
|
|
16408
|
+
var import_node_os15 = require("os");
|
|
14664
16409
|
init_util();
|
|
14665
16410
|
init_ui();
|
|
14666
16411
|
function buildStatusCommand() {
|
|
@@ -14691,6 +16436,7 @@ function buildStatusCommand() {
|
|
|
14691
16436
|
let me = null;
|
|
14692
16437
|
let devices = null;
|
|
14693
16438
|
let agents = null;
|
|
16439
|
+
let diagnose = null;
|
|
14694
16440
|
try {
|
|
14695
16441
|
const meRes = await cloud.request("GET", "/api/im/me", { timeoutMs: 3e3 });
|
|
14696
16442
|
cloudOk = meRes.ok;
|
|
@@ -14703,19 +16449,33 @@ function buildStatusCommand() {
|
|
|
14703
16449
|
if (Array.isArray(wsList) && wsList.length > 0) {
|
|
14704
16450
|
const wsId = wsList[0]?.id;
|
|
14705
16451
|
if (wsId) {
|
|
14706
|
-
const
|
|
14707
|
-
if (
|
|
14708
|
-
const
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
|
|
14713
|
-
|
|
14714
|
-
agents = agBody?.data;
|
|
16452
|
+
const rtRes = await cloud.request("GET", `/api/im/workspaces/${encodeURIComponent(wsId)}/runtime`, { timeoutMs: 3e3 });
|
|
16453
|
+
if (rtRes.ok) {
|
|
16454
|
+
const rtBody = rtRes.data;
|
|
16455
|
+
const snapshot = rtBody?.data;
|
|
16456
|
+
if (snapshot && Array.isArray(snapshot.devices)) {
|
|
16457
|
+
devices = snapshot.devices;
|
|
16458
|
+
agents = snapshot.devices.flatMap((d) => d.agents ?? []);
|
|
16459
|
+
}
|
|
14715
16460
|
}
|
|
14716
16461
|
}
|
|
14717
16462
|
}
|
|
14718
16463
|
}
|
|
16464
|
+
const daemonId = cfg.daemon_id;
|
|
16465
|
+
if (daemonId) {
|
|
16466
|
+
try {
|
|
16467
|
+
const diagRes = await cloud.request(
|
|
16468
|
+
"GET",
|
|
16469
|
+
`/api/im/runtime/diagnose?daemonId=${encodeURIComponent(daemonId)}`,
|
|
16470
|
+
{ timeoutMs: 3e3 }
|
|
16471
|
+
);
|
|
16472
|
+
if (diagRes.ok) {
|
|
16473
|
+
const body = diagRes.data;
|
|
16474
|
+
if (body?.data && typeof body.data === "object") diagnose = body.data;
|
|
16475
|
+
}
|
|
16476
|
+
} catch {
|
|
16477
|
+
}
|
|
16478
|
+
}
|
|
14719
16479
|
}
|
|
14720
16480
|
} catch {
|
|
14721
16481
|
cloudOk = false;
|
|
@@ -14732,7 +16492,8 @@ function buildStatusCommand() {
|
|
|
14732
16492
|
info: daemonStatus.info ?? {}
|
|
14733
16493
|
},
|
|
14734
16494
|
cloud: { base: cfg.cloud_api_base, reachable: cloudOk, me, devices, agents },
|
|
14735
|
-
local
|
|
16495
|
+
local,
|
|
16496
|
+
diagnose
|
|
14736
16497
|
};
|
|
14737
16498
|
if (opts.json) {
|
|
14738
16499
|
printJson(report);
|
|
@@ -14741,6 +16502,60 @@ function buildStatusCommand() {
|
|
|
14741
16502
|
printPretty(report);
|
|
14742
16503
|
});
|
|
14743
16504
|
}
|
|
16505
|
+
function computeDeviceIcon(lastSeenAt, now) {
|
|
16506
|
+
if (!lastSeenAt) return "\u25CB";
|
|
16507
|
+
const age = now.getTime() - Date.parse(lastSeenAt);
|
|
16508
|
+
if (!Number.isFinite(age) || age < 0) return "\u25CB";
|
|
16509
|
+
if (age < 5 * 6e4) return "\u25CF";
|
|
16510
|
+
if (age < 60 * 6e4) return "\u25D0";
|
|
16511
|
+
return "\u25CB";
|
|
16512
|
+
}
|
|
16513
|
+
function computeAgentIcon(status, lastHeartbeat, now) {
|
|
16514
|
+
const s = (status || "").toLowerCase();
|
|
16515
|
+
if (s === "error" || s === "failed") return "\u25C6";
|
|
16516
|
+
if (!lastHeartbeat) return s === "offline" ? "\u25CB" : "\u25D0";
|
|
16517
|
+
const age = now.getTime() - Date.parse(lastHeartbeat);
|
|
16518
|
+
if (!Number.isFinite(age) || age < 0 || age >= 60 * 6e4) return "\u25CB";
|
|
16519
|
+
if (age >= 5 * 6e4) return "\u25D0";
|
|
16520
|
+
return "\u25CF";
|
|
16521
|
+
}
|
|
16522
|
+
function pad(value, width) {
|
|
16523
|
+
if (value.length >= width) return value.slice(0, width);
|
|
16524
|
+
return value + " ".repeat(width - value.length);
|
|
16525
|
+
}
|
|
16526
|
+
function formatDeviceBindings(devices, now = /* @__PURE__ */ new Date()) {
|
|
16527
|
+
const lines = [];
|
|
16528
|
+
if (devices.length === 0) {
|
|
16529
|
+
lines.push(" No paired devices.");
|
|
16530
|
+
return lines;
|
|
16531
|
+
}
|
|
16532
|
+
const totalAgents = devices.reduce((s, d) => s + (d.agents?.length ?? 0), 0);
|
|
16533
|
+
const devWord = devices.length === 1 ? "device" : "devices";
|
|
16534
|
+
const agWord = totalAgents === 1 ? "agent" : "agents";
|
|
16535
|
+
lines.push(` Device & Agent Bindings (${devices.length} ${devWord} \xB7 ${totalAgents} ${agWord})`);
|
|
16536
|
+
lines.push("");
|
|
16537
|
+
for (let di = 0; di < devices.length; di++) {
|
|
16538
|
+
const d = devices[di];
|
|
16539
|
+
const dIcon = computeDeviceIcon(d.lastSeenAt, now);
|
|
16540
|
+
const dName = pad(d.name || d.deviceId, 40);
|
|
16541
|
+
const lastSeen = d.lastSeenAt ? formatRelative(d.lastSeenAt, now) : "never seen";
|
|
16542
|
+
lines.push(` ${dIcon} ${dName} ${lastSeen}`);
|
|
16543
|
+
const agents = d.agents ?? [];
|
|
16544
|
+
for (let ai = 0; ai < agents.length; ai++) {
|
|
16545
|
+
const a = agents[ai];
|
|
16546
|
+
const isLast = ai === agents.length - 1;
|
|
16547
|
+
const branch = isLast ? "\u2514\u2500" : "\u251C\u2500";
|
|
16548
|
+
const aIcon = computeAgentIcon(a.status, a.lastHeartbeat, now);
|
|
16549
|
+
const aName = pad(a.name || a.id, 24);
|
|
16550
|
+
const aStatus = pad(a.status || "?", 8);
|
|
16551
|
+
const aVersion = pad(a.version ? `v${a.version}` : "\u2014", 10);
|
|
16552
|
+
const tail = a.currentTaskId ? `task=${a.currentTaskId}` : a.lastHeartbeat ? `\u2764 ${formatRelative(a.lastHeartbeat, now)}` : "";
|
|
16553
|
+
lines.push(` ${branch} ${aIcon} ${aName} ${aStatus} ${aVersion} ${tail}`.trimEnd());
|
|
16554
|
+
}
|
|
16555
|
+
if (di < devices.length - 1) lines.push("");
|
|
16556
|
+
}
|
|
16557
|
+
return lines;
|
|
16558
|
+
}
|
|
14744
16559
|
async function readDaemonStatus() {
|
|
14745
16560
|
try {
|
|
14746
16561
|
const res = await fetch("http://127.0.0.1:3210/healthz", {
|
|
@@ -14784,7 +16599,7 @@ function printPretty(report) {
|
|
|
14784
16599
|
ui.blank();
|
|
14785
16600
|
ok("Config", report.paths.config);
|
|
14786
16601
|
if (report.daemon.running) {
|
|
14787
|
-
const daemonName = report.cloud.me && typeof report.cloud.me === "object" ? report.cloud.me.user?.username ?? (0,
|
|
16602
|
+
const daemonName = report.cloud.me && typeof report.cloud.me === "object" ? report.cloud.me.user?.username ?? (0, import_node_os15.hostname)() : (0, import_node_os15.hostname)();
|
|
14788
16603
|
const ws = report.daemon.wsConnected ? "connected" : "pending";
|
|
14789
16604
|
ok("Daemon", `${daemonName} pid=${report.daemon.pid} ws=${ws}`);
|
|
14790
16605
|
if (report.daemon.info) {
|
|
@@ -14813,16 +16628,7 @@ function printPretty(report) {
|
|
|
14813
16628
|
if (Array.isArray(report.cloud.devices) && report.cloud.devices.length > 0) {
|
|
14814
16629
|
const devs = report.cloud.devices;
|
|
14815
16630
|
ui.blank();
|
|
14816
|
-
|
|
14817
|
-
for (const d of devs) {
|
|
14818
|
-
const statusIcon = d.daemonStatus === "connected" ? "\u25CF" : "\u25CB";
|
|
14819
|
-
const kind = d.runtimeKind === "docker" ? "Local" : "K8s";
|
|
14820
|
-
const declared = d.hostedAgentSummary?.declared ?? 0;
|
|
14821
|
-
ui.line(` ${statusIcon} ${d.podName?.replace(/^daemon:/, "")?.slice(0, 28) ?? "?"} kind=${kind} agents=${declared}`);
|
|
14822
|
-
}
|
|
14823
|
-
}
|
|
14824
|
-
if (Array.isArray(report.cloud.agents) && report.cloud.agents.length > 0) {
|
|
14825
|
-
ui.line(` Hosted agents: ${report.cloud.agents.length}`);
|
|
16631
|
+
for (const ln of formatDeviceBindings(devs)) ui.line(ln);
|
|
14826
16632
|
}
|
|
14827
16633
|
if (report.local) {
|
|
14828
16634
|
ui.blank();
|
|
@@ -14832,6 +16638,58 @@ function printPretty(report) {
|
|
|
14832
16638
|
} else {
|
|
14833
16639
|
warn("Local DB", "unavailable");
|
|
14834
16640
|
}
|
|
16641
|
+
if (report.diagnose) {
|
|
16642
|
+
ui.blank();
|
|
16643
|
+
ui.header("Cloud dispatch reachability");
|
|
16644
|
+
ui.blank();
|
|
16645
|
+
printDiagnose(report.diagnose, report.daemon.pid ?? null);
|
|
16646
|
+
}
|
|
16647
|
+
}
|
|
16648
|
+
function printDiagnose(d, pid) {
|
|
16649
|
+
const ui = getUI();
|
|
16650
|
+
const heartbeatLine = d.lastHeartbeatAt ? `last heartbeat ${formatRelative(d.lastHeartbeatAt)}` : "no heartbeat seen";
|
|
16651
|
+
if (d.wsAlive) {
|
|
16652
|
+
ok("Daemon", `running${pid ? ` (pid ${pid})` : ""}`);
|
|
16653
|
+
ok("Cloud WS", `connected (${heartbeatLine})`);
|
|
16654
|
+
} else {
|
|
16655
|
+
warn("Daemon", `running${pid ? ` (pid ${pid})` : ""} \u2014 not visible to cloud WS`);
|
|
16656
|
+
fail2("Cloud WS", `disconnected (${heartbeatLine})`);
|
|
16657
|
+
}
|
|
16658
|
+
if (d.gatewayUrl) {
|
|
16659
|
+
if (d.gatewayIsPrivate) {
|
|
16660
|
+
warn("HTTP gateway", `${d.gatewayUrl} (private IP \u2014 only reachable on same network)`);
|
|
16661
|
+
} else if (d.httpReachable) {
|
|
16662
|
+
ok(
|
|
16663
|
+
"HTTP gateway",
|
|
16664
|
+
`${d.gatewayUrl} (reachable${d.httpLatencyMs != null ? `, ${d.httpLatencyMs}ms` : ""})`
|
|
16665
|
+
);
|
|
16666
|
+
} else {
|
|
16667
|
+
fail2("HTTP gateway", `${d.gatewayUrl} (unreachable${d.httpError ? ` \u2014 ${d.httpError}` : ""})`);
|
|
16668
|
+
}
|
|
16669
|
+
} else {
|
|
16670
|
+
ui.line(" HTTP gateway: not declared (WS-only daemon)");
|
|
16671
|
+
}
|
|
16672
|
+
const activeAgents = /* @__PURE__ */ (() => {
|
|
16673
|
+
return 0;
|
|
16674
|
+
})();
|
|
16675
|
+
if (activeAgents > 0) {
|
|
16676
|
+
ok("Active agents", String(activeAgents));
|
|
16677
|
+
}
|
|
16678
|
+
const recColour = d.recommendedTransport === "ws" ? ok : d.recommendedTransport === "http" ? warn : fail2;
|
|
16679
|
+
recColour("Recommended transport", d.recommendedTransport);
|
|
16680
|
+
if (d.lastProbeAt) {
|
|
16681
|
+
ui.line(` Last transport probe: ${formatRelative(d.lastProbeAt)}`);
|
|
16682
|
+
}
|
|
16683
|
+
}
|
|
16684
|
+
function formatRelative(iso, now = /* @__PURE__ */ new Date()) {
|
|
16685
|
+
const ts = Date.parse(iso);
|
|
16686
|
+
if (!Number.isFinite(ts)) return "unknown";
|
|
16687
|
+
const diff = Math.max(0, now.getTime() - ts);
|
|
16688
|
+
if (diff < 3e4) return "just now";
|
|
16689
|
+
if (diff < 6e4) return `${Math.round(diff / 1e3)}s ago`;
|
|
16690
|
+
if (diff < 36e5) return `${Math.round(diff / 6e4)}m ago`;
|
|
16691
|
+
if (diff < 864e5) return `${Math.round(diff / 36e5)}h ago`;
|
|
16692
|
+
return `${Math.round(diff / 864e5)}d ago`;
|
|
14835
16693
|
}
|
|
14836
16694
|
|
|
14837
16695
|
// src/cli/commands/task.ts
|
|
@@ -15194,7 +17052,7 @@ async function readResponseError(res) {
|
|
|
15194
17052
|
|
|
15195
17053
|
// src/cli/index.ts
|
|
15196
17054
|
init_ui();
|
|
15197
|
-
var VERSION = "2.0.
|
|
17055
|
+
var VERSION = "2.0.6";
|
|
15198
17056
|
function buildProgram() {
|
|
15199
17057
|
const program = new import_commander20.Command("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
|
|
15200
17058
|
program.addCommand(buildBannerCommand());
|