@prismer/runtime 2.0.4 → 2.0.5
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 +2067 -251
- package/dist/cli.js +1948 -132
- package/dist/index.cjs +2077 -259
- package/dist/index.d.cts +533 -181
- package/dist/index.d.ts +533 -181
- package/dist/index.js +1962 -144
- 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.5",
|
|
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
|
|
@@ -9520,6 +10659,352 @@ ${input.stderr}` : null
|
|
|
9520
10659
|
|
|
9521
10660
|
// src/daemon/runner.ts
|
|
9522
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
|
+
};
|
|
10866
|
+
}
|
|
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 };
|
|
10893
|
+
}
|
|
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 };
|
|
10931
|
+
}
|
|
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;
|
|
10955
|
+
}
|
|
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
|
+
};
|
|
10972
|
+
}
|
|
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 };
|
|
11005
|
+
}
|
|
11006
|
+
|
|
11007
|
+
// src/daemon/runner.ts
|
|
9523
11008
|
var import_meta2 = {};
|
|
9524
11009
|
var DEFAULT_LOCAL_PORT = 3210;
|
|
9525
11010
|
var log7 = createLogger("Daemon");
|
|
@@ -9568,6 +11053,14 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
9568
11053
|
// F16 (2026-05-20) — periodic skill resync timer (default 10min).
|
|
9569
11054
|
skillResyncTimer;
|
|
9570
11055
|
skillSyncInFlight = false;
|
|
11056
|
+
// Wave-4 E7 — local cache of in-flight two-phase dispatch replies. Survives
|
|
11057
|
+
// daemon crash so cold-start can resume `prepared` rows via idempotent
|
|
11058
|
+
// commit (server enforces (taskId, idempotencyKey) UNIQUE).
|
|
11059
|
+
pendingReplyCache;
|
|
11060
|
+
// One-shot guard so we only recover on the first post-authenticated tick,
|
|
11061
|
+
// not every reconnect (server-side commits are idempotent but the log noise
|
|
11062
|
+
// and DB pressure of re-running on every redeclare is wasteful).
|
|
11063
|
+
pendingReplyRecoveryDone = false;
|
|
9571
11064
|
async start() {
|
|
9572
11065
|
if (this.state !== "idle") throw new Error(`Runner already in state ${this.state}`);
|
|
9573
11066
|
this.state = "starting";
|
|
@@ -9582,6 +11075,7 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
9582
11075
|
process.env.PRISMER_API_KEY = this.config.api_key;
|
|
9583
11076
|
this.db = openLocalDb(this.paths.localDb);
|
|
9584
11077
|
this.cloud = new CloudClient({ baseUrl: this.config.cloud_api_base, apiKey: this.config.api_key });
|
|
11078
|
+
this.pendingReplyCache = createPendingReplyCache(this.db);
|
|
9585
11079
|
this.assetCache = new AssetCache({
|
|
9586
11080
|
db: this.db,
|
|
9587
11081
|
cloud: this.cloud,
|
|
@@ -9908,19 +11402,19 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
9908
11402
|
sampleCgroupResources() {
|
|
9909
11403
|
const baseline = { cpu: { usagePct: 0 }, mem: { usedBytes: 0, limitBytes: 0 } };
|
|
9910
11404
|
try {
|
|
9911
|
-
if (!(0,
|
|
11405
|
+
if (!(0, import_node_fs16.existsSync)("/sys/fs/cgroup/memory.current")) {
|
|
9912
11406
|
return baseline;
|
|
9913
11407
|
}
|
|
9914
|
-
const memUsedRaw = (0,
|
|
11408
|
+
const memUsedRaw = (0, import_node_fs16.readFileSync)("/sys/fs/cgroup/memory.current", "utf-8").trim();
|
|
9915
11409
|
const memUsed = Number.parseInt(memUsedRaw, 10);
|
|
9916
11410
|
let memLimit = 0;
|
|
9917
|
-
if ((0,
|
|
9918
|
-
const limitRaw = (0,
|
|
11411
|
+
if ((0, import_node_fs16.existsSync)("/sys/fs/cgroup/memory.max")) {
|
|
11412
|
+
const limitRaw = (0, import_node_fs16.readFileSync)("/sys/fs/cgroup/memory.max", "utf-8").trim();
|
|
9919
11413
|
memLimit = limitRaw === "max" ? 0 : Number.parseInt(limitRaw, 10) || 0;
|
|
9920
11414
|
}
|
|
9921
11415
|
let cpuUsagePct = 0;
|
|
9922
|
-
if ((0,
|
|
9923
|
-
const statLines = (0,
|
|
11416
|
+
if ((0, import_node_fs16.existsSync)("/sys/fs/cgroup/cpu.stat")) {
|
|
11417
|
+
const statLines = (0, import_node_fs16.readFileSync)("/sys/fs/cgroup/cpu.stat", "utf-8").split("\n");
|
|
9924
11418
|
const usecLine = statLines.find((line) => line.startsWith("usage_usec "));
|
|
9925
11419
|
if (usecLine) {
|
|
9926
11420
|
const usec = Number.parseInt(usecLine.split(" ")[1] ?? "0", 10);
|
|
@@ -10098,10 +11592,10 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10098
11592
|
const rawJson = process.env.PRISMER_HOSTED_AGENT_JSON;
|
|
10099
11593
|
let raw;
|
|
10100
11594
|
if (rawFile) {
|
|
10101
|
-
if (!(0,
|
|
11595
|
+
if (!(0, import_node_fs16.existsSync)(rawFile)) {
|
|
10102
11596
|
throw new Error(`PRISMER_HOSTED_AGENT_FILE not found: ${rawFile}`);
|
|
10103
11597
|
}
|
|
10104
|
-
raw = (0,
|
|
11598
|
+
raw = (0, import_node_fs16.readFileSync)(rawFile, "utf8");
|
|
10105
11599
|
} else if (rawJson) {
|
|
10106
11600
|
raw = rawJson;
|
|
10107
11601
|
}
|
|
@@ -10188,6 +11682,19 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10188
11682
|
process.stdout.write(`[daemon] ws authenticated, sending agent.host.declare (${this.hostedAgents.size} agents)
|
|
10189
11683
|
`);
|
|
10190
11684
|
this.sendDeclare();
|
|
11685
|
+
if (!this.pendingReplyRecoveryDone) {
|
|
11686
|
+
this.pendingReplyRecoveryDone = true;
|
|
11687
|
+
void recoverPendingReplies({ cloud: this.cloud, cache: this.pendingReplyCache }).then((summary) => {
|
|
11688
|
+
if (summary.attempted === 0) return;
|
|
11689
|
+
process.stdout.write(
|
|
11690
|
+
`[daemon] dispatch.recovery summary attempted=${summary.attempted} committed=${summary.committed} aborted=${summary.aborted} failed=${summary.failed}
|
|
11691
|
+
`
|
|
11692
|
+
);
|
|
11693
|
+
}).catch((err) => {
|
|
11694
|
+
process.stderr.write(`[daemon] dispatch.recovery threw: ${err.message}
|
|
11695
|
+
`);
|
|
11696
|
+
});
|
|
11697
|
+
}
|
|
10191
11698
|
return;
|
|
10192
11699
|
}
|
|
10193
11700
|
this.handleIncoming(msg);
|
|
@@ -10197,7 +11704,7 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10197
11704
|
const payload = {
|
|
10198
11705
|
daemonId: this.config.daemon_id,
|
|
10199
11706
|
daemonVersion: this.opts.daemonVersion ?? "0.0.0",
|
|
10200
|
-
platform: (0,
|
|
11707
|
+
platform: (0, import_node_os8.platform)() === "win32" ? "win32" : (0, import_node_os8.platform)() === "linux" ? "linux" : "darwin",
|
|
10201
11708
|
agents: Array.from(this.hostedAgents.values()).map((a) => ({
|
|
10202
11709
|
imUserId: a.imUserId,
|
|
10203
11710
|
name: a.name,
|
|
@@ -10234,12 +11741,90 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10234
11741
|
case "asset.changed":
|
|
10235
11742
|
void this.onAssetChanged(msg.payload);
|
|
10236
11743
|
return;
|
|
11744
|
+
// v2.0 §4.8.1 (Wave 4-E4) — webhook reverse-channel RPC. Cloud's
|
|
11745
|
+
// dispatchExternalMention pushes an AgentDispatchRequest plus an
|
|
11746
|
+
// embedded `_rpcId`; the daemon runs the same handler as the
|
|
11747
|
+
// HTTP /dispatch path and echoes the reply back over WS with the
|
|
11748
|
+
// identical `_rpcId` so cloud's WsRpcService can settle the
|
|
11749
|
+
// pending promise.
|
|
11750
|
+
case "webhook.dispatch.request":
|
|
11751
|
+
void this.onWebhookDispatch(msg.payload);
|
|
11752
|
+
return;
|
|
10237
11753
|
default:
|
|
10238
11754
|
this.emit("unknown-message", msg);
|
|
10239
11755
|
}
|
|
10240
11756
|
}
|
|
11757
|
+
/**
|
|
11758
|
+
* v2.0 §4.8.1 — webhook dispatch over WS reverse channel.
|
|
11759
|
+
*
|
|
11760
|
+
* Acks via `webhook.dispatch.reply { _rpcId, ok, acceptedAt }` immediately
|
|
11761
|
+
* after `handleAgentMessageDispatch()` returns its synchronous response
|
|
11762
|
+
* — the actual reply (the agent's reply text/attachments) still flows
|
|
11763
|
+
* through `postMessageDispatchReply()` which posts to
|
|
11764
|
+
* `/api/im/dispatch/reply` after the adapter finishes. That mirrors the
|
|
11765
|
+
* HTTP /dispatch contract: ack first, asynchronous final reply via the
|
|
11766
|
+
* dispatch-reply REST endpoint.
|
|
11767
|
+
*
|
|
11768
|
+
* The `_rpcId` field is the only addition versus the HTTP path; we strip
|
|
11769
|
+
* it before passing the payload to the dispatch handler so existing
|
|
11770
|
+
* validation logic doesn't trip on an unknown field.
|
|
11771
|
+
*/
|
|
11772
|
+
async onWebhookDispatch(payload) {
|
|
11773
|
+
const rpcId = payload._rpcId;
|
|
11774
|
+
if (!rpcId) {
|
|
11775
|
+
process.stderr.write(`[daemon] webhook.dispatch.request without _rpcId \u2014 dropped
|
|
11776
|
+
`);
|
|
11777
|
+
return;
|
|
11778
|
+
}
|
|
11779
|
+
const { _rpcId: _, ...request } = payload;
|
|
11780
|
+
try {
|
|
11781
|
+
const handle = handleAgentMessageDispatch(request, {
|
|
11782
|
+
findAgent: (agentImUserId) => this.findMessageDispatchAgent(agentImUserId),
|
|
11783
|
+
postReply: (replyPayload) => this.postMessageDispatchReply(replyPayload),
|
|
11784
|
+
onError: (err, req) => {
|
|
11785
|
+
process.stderr.write(
|
|
11786
|
+
`[daemon] webhook.dispatch.request failed message=${req.messageId} agent=${req.mentionedAgentImUserId}: ${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
11787
|
+
`
|
|
11788
|
+
);
|
|
11789
|
+
}
|
|
11790
|
+
});
|
|
11791
|
+
this.ws.send({
|
|
11792
|
+
type: "webhook.dispatch.reply",
|
|
11793
|
+
payload: {
|
|
11794
|
+
_rpcId: rpcId,
|
|
11795
|
+
ok: handle.response.ok,
|
|
11796
|
+
acceptedAt: handle.response.acceptedAt,
|
|
11797
|
+
error: handle.response.error
|
|
11798
|
+
}
|
|
11799
|
+
});
|
|
11800
|
+
void handle.done.catch((err) => {
|
|
11801
|
+
process.stderr.write(
|
|
11802
|
+
`[daemon] webhook.dispatch final reply failed message=${request.messageId}: ${err instanceof Error ? err.message : String(err)}
|
|
11803
|
+
`
|
|
11804
|
+
);
|
|
11805
|
+
});
|
|
11806
|
+
} catch (err) {
|
|
11807
|
+
this.ws.send({
|
|
11808
|
+
type: "webhook.dispatch.reply",
|
|
11809
|
+
payload: {
|
|
11810
|
+
_rpcId: rpcId,
|
|
11811
|
+
ok: false,
|
|
11812
|
+
error: {
|
|
11813
|
+
code: "daemon_dispatch_failed",
|
|
11814
|
+
message: err instanceof Error ? err.message : String(err)
|
|
11815
|
+
}
|
|
11816
|
+
}
|
|
11817
|
+
});
|
|
11818
|
+
}
|
|
11819
|
+
}
|
|
10241
11820
|
async onHostAcked(payload) {
|
|
10242
11821
|
this.workspaceId = payload.workspaceId;
|
|
11822
|
+
void this.reportTransport().catch((err) => {
|
|
11823
|
+
process.stderr.write(
|
|
11824
|
+
`[daemon] transport-probe report failed: ${err.message}
|
|
11825
|
+
`
|
|
11826
|
+
);
|
|
11827
|
+
});
|
|
10243
11828
|
if (this.memoryWiring && this.workspaceId) {
|
|
10244
11829
|
syncMemoryFromCloud(this.memoryWiring, this.cloud, [this.workspaceId]).catch(
|
|
10245
11830
|
(err) => log7.error("Initial memory sync failed", err.message)
|
|
@@ -10421,14 +12006,68 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10421
12006
|
};
|
|
10422
12007
|
}
|
|
10423
12008
|
async postMessageDispatchReply(payload) {
|
|
10424
|
-
const
|
|
10425
|
-
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
10429
|
-
|
|
10430
|
-
|
|
12009
|
+
const taskId = `external:${payload.replyToMessageId}`;
|
|
12010
|
+
try {
|
|
12011
|
+
const result = await sendDispatchReplyTwoPhase({
|
|
12012
|
+
taskId,
|
|
12013
|
+
payload: {
|
|
12014
|
+
conversationId: payload.conversationId,
|
|
12015
|
+
replyToken: payload.replyToken,
|
|
12016
|
+
replyToMessageId: payload.replyToMessageId,
|
|
12017
|
+
agentImUserId: payload.agentImUserId,
|
|
12018
|
+
status: payload.status,
|
|
12019
|
+
...payload.replyText !== void 0 ? { replyText: payload.replyText } : {},
|
|
12020
|
+
...payload.attachments ? { attachments: payload.attachments } : {},
|
|
12021
|
+
assetIds: payload.attachments?.map((a) => a.assetId).filter((id) => typeof id === "string") ?? [],
|
|
12022
|
+
completedAt: payload.completedAt,
|
|
12023
|
+
...payload.error ? { error: payload.error } : {}
|
|
12024
|
+
},
|
|
12025
|
+
cloud: this.cloud,
|
|
12026
|
+
cache: this.pendingReplyCache
|
|
12027
|
+
});
|
|
12028
|
+
if (result.status === "aborted") {
|
|
12029
|
+
throw new Error(`dispatch reply aborted by server reaper (replyId=${result.replyId})`);
|
|
12030
|
+
}
|
|
12031
|
+
} catch (err) {
|
|
12032
|
+
throw new Error(
|
|
12033
|
+
err.message?.length ? err.message : `dispatch reply failed`
|
|
12034
|
+
);
|
|
12035
|
+
}
|
|
12036
|
+
}
|
|
12037
|
+
/**
|
|
12038
|
+
* v2.0 §4.8.1 (Wave 4-E4) — submit the transport-probe report. Called
|
|
12039
|
+
* from `onHostAcked` so the daemon's container row exists in cloud
|
|
12040
|
+
* before we POST to /runtime/transport-report.
|
|
12041
|
+
*
|
|
12042
|
+
* The gatewayUrl reported here is `http://<local-ipv4>:<localPort>`
|
|
12043
|
+
* (typically `http://192.168.x.x:3210`). For a daemon without a local
|
|
12044
|
+
* HTTP server (startLocalServer=false in tests) we still post the
|
|
12045
|
+
* probe with `gatewayUrl=null` so cloud knows transport='ws' is the
|
|
12046
|
+
* only path.
|
|
12047
|
+
*/
|
|
12048
|
+
async reportTransport() {
|
|
12049
|
+
const hasLocalServer = this.opts.startLocalServer !== false;
|
|
12050
|
+
let gatewayUrl = null;
|
|
12051
|
+
if (hasLocalServer) {
|
|
12052
|
+
const localIp = pickLocalIPv4();
|
|
12053
|
+
if (localIp) {
|
|
12054
|
+
const port = this.opts.localPort ?? DEFAULT_LOCAL_PORT;
|
|
12055
|
+
gatewayUrl = `http://${localIp}:${port}`;
|
|
12056
|
+
}
|
|
12057
|
+
}
|
|
12058
|
+
const probe = buildTransportProbe(gatewayUrl);
|
|
12059
|
+
const result = await reportTransportProbe(this.cloud, this.config.daemon_id, probe);
|
|
12060
|
+
if (!result.ok) {
|
|
12061
|
+
process.stderr.write(
|
|
12062
|
+
`[daemon] transport-probe report rejected status=${result.status} error=${result.error ?? "-"}
|
|
12063
|
+
`
|
|
12064
|
+
);
|
|
12065
|
+
return;
|
|
10431
12066
|
}
|
|
12067
|
+
process.stdout.write(
|
|
12068
|
+
`[daemon] transport-probe reported transport=${probe.transport} gateway=${probe.gatewayUrl ?? "null"} private=${probe.gatewayIsPrivate}
|
|
12069
|
+
`
|
|
12070
|
+
);
|
|
10432
12071
|
}
|
|
10433
12072
|
onAgentChanged(payload) {
|
|
10434
12073
|
const a = this.hostedAgents.get(payload.agentImUserId);
|
|
@@ -10871,9 +12510,9 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10871
12510
|
function resolveUserAssetWorkspaceDir(workspaceId) {
|
|
10872
12511
|
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
10873
12512
|
if (override) return (0, import_node_path11.join)(override, workspaceId);
|
|
10874
|
-
const home = (0,
|
|
12513
|
+
const home = (0, import_node_os8.homedir)();
|
|
10875
12514
|
const desktop = (0, import_node_path11.join)(home, "Desktop");
|
|
10876
|
-
const base = (0,
|
|
12515
|
+
const base = (0, import_node_fs16.existsSync)(desktop) ? desktop : home;
|
|
10877
12516
|
return (0, import_node_path11.join)(base, "Prismer Assets", workspaceId);
|
|
10878
12517
|
}
|
|
10879
12518
|
function readTargetDaemonId(payload) {
|
|
@@ -10950,7 +12589,7 @@ function safeJsonParse(raw) {
|
|
|
10950
12589
|
|
|
10951
12590
|
// src/pair.ts
|
|
10952
12591
|
var import_node_crypto10 = require("crypto");
|
|
10953
|
-
var
|
|
12592
|
+
var import_node_os9 = require("os");
|
|
10954
12593
|
var import_promises2 = require("timers/promises");
|
|
10955
12594
|
var import_qrcode = __toESM(require("qrcode"), 1);
|
|
10956
12595
|
async function pair(opts) {
|
|
@@ -10980,7 +12619,7 @@ async function pair(opts) {
|
|
|
10980
12619
|
"/api/im/pair/offer",
|
|
10981
12620
|
{
|
|
10982
12621
|
auth: false,
|
|
10983
|
-
body: { devicePub, deviceName: opts.deviceName ?? (0,
|
|
12622
|
+
body: { devicePub, deviceName: opts.deviceName ?? (0, import_node_os9.hostname)() }
|
|
10984
12623
|
}
|
|
10985
12624
|
);
|
|
10986
12625
|
if (!offerRes.ok) {
|
|
@@ -11061,8 +12700,8 @@ var import_commander20 = require("commander");
|
|
|
11061
12700
|
|
|
11062
12701
|
// src/cli/commands/adapter.ts
|
|
11063
12702
|
var import_node_child_process5 = require("child_process");
|
|
11064
|
-
var
|
|
11065
|
-
var
|
|
12703
|
+
var import_node_fs18 = require("fs");
|
|
12704
|
+
var import_node_os10 = require("os");
|
|
11066
12705
|
var import_node_path13 = require("path");
|
|
11067
12706
|
var import_commander = require("commander");
|
|
11068
12707
|
init_hermes();
|
|
@@ -11078,7 +12717,7 @@ var INSTALL_SPECS = {
|
|
|
11078
12717
|
binary: "claude",
|
|
11079
12718
|
hint: "Set ANTHROPIC_API_KEY in your shell profile, or rely on Claude Code OAuth login. Run `claude login` to sign in.",
|
|
11080
12719
|
authHints: ["ANTHROPIC_API_KEY or Claude Code OAuth login (`claude login`)"],
|
|
11081
|
-
hookTarget: { path: (0, import_node_path13.join)((0,
|
|
12720
|
+
hookTarget: { path: (0, import_node_path13.join)((0, import_node_os10.homedir)(), ".claude", "hooks.json"), kind: "json-file" }
|
|
11082
12721
|
},
|
|
11083
12722
|
openclaw: {
|
|
11084
12723
|
name: "openclaw",
|
|
@@ -11087,7 +12726,7 @@ var INSTALL_SPECS = {
|
|
|
11087
12726
|
binary: "openclaw",
|
|
11088
12727
|
hint: "OpenClaw runs as a gateway. Configure ~/.openclaw/openclaw.json and start it with `openclaw gateway` before tasks dispatch.",
|
|
11089
12728
|
authHints: ["~/.openclaw/openclaw.json gateway.auth.bearerTokens", "Prismer daemon api_key"],
|
|
11090
|
-
hookTarget: { path: (0, import_node_path13.join)((0,
|
|
12729
|
+
hookTarget: { path: (0, import_node_path13.join)((0, import_node_os10.homedir)(), ".openclaw", "hooks"), kind: "directory" }
|
|
11091
12730
|
},
|
|
11092
12731
|
codex: {
|
|
11093
12732
|
name: "codex",
|
|
@@ -11096,7 +12735,7 @@ var INSTALL_SPECS = {
|
|
|
11096
12735
|
binary: "codex",
|
|
11097
12736
|
hint: "Set OPENAI_API_KEY in your shell profile. Verify with `codex --help`.",
|
|
11098
12737
|
authHints: ["OPENAI_API_KEY or Codex CLI account login"],
|
|
11099
|
-
hookTarget: { path: (0, import_node_path13.join)((0,
|
|
12738
|
+
hookTarget: { path: (0, import_node_path13.join)((0, import_node_os10.homedir)(), ".codex", "hooks.json"), kind: "json-file" }
|
|
11100
12739
|
},
|
|
11101
12740
|
hermes: {
|
|
11102
12741
|
name: "hermes",
|
|
@@ -11105,7 +12744,7 @@ var INSTALL_SPECS = {
|
|
|
11105
12744
|
binary: "hermes",
|
|
11106
12745
|
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
12746
|
authHints: ["~/.hermes/.env API_SERVER_KEY", "Hermes profile provider credentials"],
|
|
11108
|
-
hookTarget: { path: (0, import_node_path13.join)((0,
|
|
12747
|
+
hookTarget: { path: (0, import_node_path13.join)((0, import_node_os10.homedir)(), ".hermes", "hooks.json"), kind: "json-file" }
|
|
11109
12748
|
}
|
|
11110
12749
|
};
|
|
11111
12750
|
function buildAdapterCommand() {
|
|
@@ -11320,15 +12959,15 @@ function runHooks(spec, opts) {
|
|
|
11320
12959
|
}
|
|
11321
12960
|
let backup;
|
|
11322
12961
|
if (planned.kind === "directory") {
|
|
11323
|
-
(0,
|
|
12962
|
+
(0, import_node_fs18.mkdirSync)(planned.path, { recursive: true });
|
|
11324
12963
|
const markerPath = (0, import_node_path13.join)(planned.path, "prismer.json");
|
|
11325
12964
|
backup = backupIfExists(markerPath);
|
|
11326
|
-
(0,
|
|
12965
|
+
(0, import_node_fs18.writeFileSync)(markerPath, JSON.stringify(prismerHookMarker(spec), null, 2) + "\n", "utf8");
|
|
11327
12966
|
} else {
|
|
11328
|
-
(0,
|
|
12967
|
+
(0, import_node_fs18.mkdirSync)((0, import_node_path13.dirname)(planned.path), { recursive: true });
|
|
11329
12968
|
backup = backupIfExists(planned.path);
|
|
11330
12969
|
const merged = mergeHookJson(planned.path, spec);
|
|
11331
|
-
(0,
|
|
12970
|
+
(0, import_node_fs18.writeFileSync)(planned.path, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
11332
12971
|
}
|
|
11333
12972
|
return {
|
|
11334
12973
|
ok: true,
|
|
@@ -11395,23 +13034,23 @@ function inspectAuthEnv(spec) {
|
|
|
11395
13034
|
hints: spec.authHints ?? []
|
|
11396
13035
|
};
|
|
11397
13036
|
case "hermes": {
|
|
11398
|
-
const envFile = (0, import_node_path13.join)((0,
|
|
13037
|
+
const envFile = (0, import_node_path13.join)((0, import_node_os10.homedir)(), ".hermes", ".env");
|
|
11399
13038
|
return {
|
|
11400
|
-
ok: (0,
|
|
13039
|
+
ok: (0, import_node_fs18.existsSync)(envFile) || Boolean(process.env.HERMES_API_KEY || process.env.API_SERVER_KEY),
|
|
11401
13040
|
present: [
|
|
11402
13041
|
...["HERMES_API_KEY", "API_SERVER_KEY"].filter((k) => Boolean(process.env[k])),
|
|
11403
|
-
...(0,
|
|
13042
|
+
...(0, import_node_fs18.existsSync)(envFile) ? [envFile] : []
|
|
11404
13043
|
],
|
|
11405
13044
|
hints: spec.authHints ?? []
|
|
11406
13045
|
};
|
|
11407
13046
|
}
|
|
11408
13047
|
case "openclaw": {
|
|
11409
|
-
const cfgFile = (0, import_node_path13.join)((0,
|
|
13048
|
+
const cfgFile = (0, import_node_path13.join)((0, import_node_os10.homedir)(), ".openclaw", "openclaw.json");
|
|
11410
13049
|
return {
|
|
11411
|
-
ok: (0,
|
|
13050
|
+
ok: (0, import_node_fs18.existsSync)(cfgFile) || Boolean(process.env.OPENCLAW_API_KEY),
|
|
11412
13051
|
present: [
|
|
11413
13052
|
...["OPENCLAW_API_KEY"].filter((k) => Boolean(process.env[k])),
|
|
11414
|
-
...(0,
|
|
13053
|
+
...(0, import_node_fs18.existsSync)(cfgFile) ? [cfgFile] : []
|
|
11415
13054
|
],
|
|
11416
13055
|
hints: spec.authHints ?? []
|
|
11417
13056
|
};
|
|
@@ -11426,16 +13065,16 @@ function inspectHook(spec) {
|
|
|
11426
13065
|
return { supported: false, markerPresent: false };
|
|
11427
13066
|
}
|
|
11428
13067
|
if (spec.name === "openclaw") {
|
|
11429
|
-
const jsonPath = (0, import_node_path13.join)((0,
|
|
13068
|
+
const jsonPath = (0, import_node_path13.join)((0, import_node_os10.homedir)(), ".openclaw", "hooks.json");
|
|
11430
13069
|
const markerPath = (0, import_node_path13.join)(target.path, "prismer.json");
|
|
11431
|
-
const jsonMarkerPresent = (0,
|
|
11432
|
-
const dirMarkerPresent = (0,
|
|
13070
|
+
const jsonMarkerPresent = (0, import_node_fs18.existsSync)(jsonPath) && fileContainsPrismerMarker(jsonPath);
|
|
13071
|
+
const dirMarkerPresent = (0, import_node_fs18.existsSync)(markerPath) && fileContainsPrismerMarker(markerPath);
|
|
11433
13072
|
return {
|
|
11434
13073
|
supported: true,
|
|
11435
13074
|
kind: "json-file-or-directory",
|
|
11436
13075
|
path: target.path,
|
|
11437
13076
|
jsonPath,
|
|
11438
|
-
exists: (0,
|
|
13077
|
+
exists: (0, import_node_fs18.existsSync)(target.path) || (0, import_node_fs18.existsSync)(jsonPath),
|
|
11439
13078
|
markerPath,
|
|
11440
13079
|
markerPresent: jsonMarkerPresent || dirMarkerPresent
|
|
11441
13080
|
};
|
|
@@ -11446,12 +13085,12 @@ function inspectHook(spec) {
|
|
|
11446
13085
|
supported: true,
|
|
11447
13086
|
kind: target.kind,
|
|
11448
13087
|
path: target.path,
|
|
11449
|
-
exists: (0,
|
|
13088
|
+
exists: (0, import_node_fs18.existsSync)(target.path),
|
|
11450
13089
|
markerPath,
|
|
11451
|
-
markerPresent: (0,
|
|
13090
|
+
markerPresent: (0, import_node_fs18.existsSync)(markerPath) && fileContainsPrismerMarker(markerPath)
|
|
11452
13091
|
};
|
|
11453
13092
|
}
|
|
11454
|
-
const exists = (0,
|
|
13093
|
+
const exists = (0, import_node_fs18.existsSync)(target.path);
|
|
11455
13094
|
if (!exists) {
|
|
11456
13095
|
return {
|
|
11457
13096
|
supported: true,
|
|
@@ -11487,14 +13126,14 @@ function prismerHookMarker(spec) {
|
|
|
11487
13126
|
}
|
|
11488
13127
|
function mergeHookJson(path9, spec) {
|
|
11489
13128
|
const marker = prismerHookMarker(spec);
|
|
11490
|
-
if (!(0,
|
|
13129
|
+
if (!(0, import_node_fs18.existsSync)(path9)) {
|
|
11491
13130
|
return {
|
|
11492
13131
|
prismer: marker
|
|
11493
13132
|
};
|
|
11494
13133
|
}
|
|
11495
13134
|
let parsed;
|
|
11496
13135
|
try {
|
|
11497
|
-
parsed = JSON.parse((0,
|
|
13136
|
+
parsed = JSON.parse((0, import_node_fs18.readFileSync)(path9, "utf8"));
|
|
11498
13137
|
} catch (err) {
|
|
11499
13138
|
throw new Error(`Cannot parse ${path9} as JSON: ${err.message}`);
|
|
11500
13139
|
}
|
|
@@ -11518,17 +13157,17 @@ function mergeHookJson(path9, spec) {
|
|
|
11518
13157
|
return next;
|
|
11519
13158
|
}
|
|
11520
13159
|
function backupIfExists(path9) {
|
|
11521
|
-
if (!(0,
|
|
13160
|
+
if (!(0, import_node_fs18.existsSync)(path9)) return null;
|
|
11522
13161
|
const suffix = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
11523
13162
|
const backup = `${path9}.bak.${suffix}`;
|
|
11524
|
-
const stat = (0,
|
|
13163
|
+
const stat = (0, import_node_fs18.statSync)(path9);
|
|
11525
13164
|
if (stat.isDirectory()) return null;
|
|
11526
|
-
(0,
|
|
13165
|
+
(0, import_node_fs18.copyFileSync)(path9, backup);
|
|
11527
13166
|
return backup;
|
|
11528
13167
|
}
|
|
11529
13168
|
function fileContainsPrismerMarker(path9) {
|
|
11530
13169
|
try {
|
|
11531
|
-
return (0,
|
|
13170
|
+
return (0, import_node_fs18.readFileSync)(path9, "utf8").includes("prismer-daemon-runtime");
|
|
11532
13171
|
} catch {
|
|
11533
13172
|
return false;
|
|
11534
13173
|
}
|
|
@@ -11888,8 +13527,74 @@ function buildAgentCommand() {
|
|
|
11888
13527
|
}
|
|
11889
13528
|
getUI().ok("Skill install requested", `agent=${opts.agent} skill=${slugOrId}`);
|
|
11890
13529
|
}, { code: "agent_install_skill_failed" }));
|
|
13530
|
+
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) => {
|
|
13531
|
+
const cloud = makeCloudClient();
|
|
13532
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agents/${encodeURIComponent(imUserId)}/snapshot`, {
|
|
13533
|
+
includeMemory: Boolean(opts.includeMemory),
|
|
13534
|
+
label: opts.label
|
|
13535
|
+
}, "agent_snapshot_failed");
|
|
13536
|
+
if (opts.json) {
|
|
13537
|
+
printJson(data);
|
|
13538
|
+
return;
|
|
13539
|
+
}
|
|
13540
|
+
const rec = data;
|
|
13541
|
+
getUI().ok("Snapshot created", String(rec.id ?? "<unknown>"));
|
|
13542
|
+
getUI().line(`agent=${String(rec.agentImUserId ?? imUserId)} memory=${rec.includeMemory ? "included" : "stripped"}`);
|
|
13543
|
+
}, { code: "agent_snapshot_failed" }));
|
|
13544
|
+
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) => {
|
|
13545
|
+
const cloud = makeCloudClient();
|
|
13546
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agents/${encodeURIComponent(imUserId)}/publish`, {
|
|
13547
|
+
slug: opts.slug,
|
|
13548
|
+
version: opts.version,
|
|
13549
|
+
license: opts.license,
|
|
13550
|
+
metadata: {
|
|
13551
|
+
...opts.title ? { title: opts.title } : {},
|
|
13552
|
+
...opts.description ? { description: opts.description } : {}
|
|
13553
|
+
},
|
|
13554
|
+
stripMemory: true
|
|
13555
|
+
}, "agent_publish_failed");
|
|
13556
|
+
if (opts.json) {
|
|
13557
|
+
printJson(data);
|
|
13558
|
+
return;
|
|
13559
|
+
}
|
|
13560
|
+
const rec = data;
|
|
13561
|
+
getUI().ok("Agent Pack published", `${String(rec.slug ?? opts.slug)}@${String(rec.version ?? opts.version ?? "1.0.0")}`);
|
|
13562
|
+
getUI().line(`package=${String(rec.id ?? "<unknown>")}`);
|
|
13563
|
+
}, { code: "agent_publish_failed" }));
|
|
13564
|
+
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) => {
|
|
13565
|
+
const cloud = makeCloudClient();
|
|
13566
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agent-packs/${encodeURIComponent(packIdOrSlug)}/fork`, {
|
|
13567
|
+
targetWorkspaceId: opts.workspaceId,
|
|
13568
|
+
displayName: opts.displayName
|
|
13569
|
+
}, "agent_fork_failed");
|
|
13570
|
+
if (opts.json) {
|
|
13571
|
+
printJson(data);
|
|
13572
|
+
return;
|
|
13573
|
+
}
|
|
13574
|
+
const rec = data;
|
|
13575
|
+
getUI().ok("Agent Pack forked", `newAgent=${String(rec.newImUserId ?? "<unknown>")}`);
|
|
13576
|
+
getUI().line(`did=${String(rec.newDid ?? "not issued")}`);
|
|
13577
|
+
}, { code: "agent_fork_failed" }));
|
|
11891
13578
|
return cmd;
|
|
11892
13579
|
}
|
|
13580
|
+
function makeCloudClient() {
|
|
13581
|
+
const cfg = loadConfig(resolvePaths());
|
|
13582
|
+
return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
13583
|
+
}
|
|
13584
|
+
async function requestEnvelope(cloud, method, path9, body, code) {
|
|
13585
|
+
const res = await cloud.request(
|
|
13586
|
+
method,
|
|
13587
|
+
path9,
|
|
13588
|
+
body === void 0 ? void 0 : { body }
|
|
13589
|
+
);
|
|
13590
|
+
if (!res.ok || res.data?.ok === false) {
|
|
13591
|
+
const message = res.error?.message ?? res.data?.error?.message ?? "request failed";
|
|
13592
|
+
exitWithError(`${path9} failed (${res.status === 0 ? "network error" : `HTTP ${res.status}`}): ${message}`, {
|
|
13593
|
+
code: res.error?.code ?? res.data?.error?.code ?? code
|
|
13594
|
+
});
|
|
13595
|
+
}
|
|
13596
|
+
return res.data && typeof res.data === "object" && "data" in res.data ? res.data.data : res.data;
|
|
13597
|
+
}
|
|
11893
13598
|
function readLocalAgents(localDb) {
|
|
11894
13599
|
const db = openLocalDb(localDb);
|
|
11895
13600
|
try {
|
|
@@ -11994,7 +13699,7 @@ function whichBinary2(bin) {
|
|
|
11994
13699
|
// src/cli/commands/asset.ts
|
|
11995
13700
|
var import_node_crypto11 = require("crypto");
|
|
11996
13701
|
var import_commander3 = require("commander");
|
|
11997
|
-
var
|
|
13702
|
+
var import_node_fs19 = require("fs");
|
|
11998
13703
|
var import_node_path14 = require("path");
|
|
11999
13704
|
init_util();
|
|
12000
13705
|
init_ui();
|
|
@@ -12167,8 +13872,8 @@ async function prefetchAssetBytes(opts) {
|
|
|
12167
13872
|
return { considered: rows.length, fetched, bytes, failed };
|
|
12168
13873
|
}
|
|
12169
13874
|
async function uploadAssetPath(inputPath, opts) {
|
|
12170
|
-
if (!(0,
|
|
12171
|
-
const stat = (0,
|
|
13875
|
+
if (!(0, import_node_fs19.existsSync)(inputPath)) exitWithError(`path not found: ${inputPath}`);
|
|
13876
|
+
const stat = (0, import_node_fs19.lstatSync)(inputPath);
|
|
12172
13877
|
if (stat.isSymbolicLink()) exitWithError(`refusing to upload symlink: ${inputPath}`);
|
|
12173
13878
|
if (stat.isFile()) {
|
|
12174
13879
|
return uploadAsset(inputPath, { ...opts, folderPath: normalizeFolderPath(opts.folderPath) });
|
|
@@ -12186,9 +13891,9 @@ async function uploadAssetPath(inputPath, opts) {
|
|
|
12186
13891
|
return { ok: true, data: { count: items.length, items } };
|
|
12187
13892
|
}
|
|
12188
13893
|
async function uploadAsset(file, opts) {
|
|
12189
|
-
if (!(0,
|
|
13894
|
+
if (!(0, import_node_fs19.existsSync)(file)) exitWithError(`file not found: ${file}`);
|
|
12190
13895
|
const cfg = loadConfig(resolvePaths());
|
|
12191
|
-
const bytes = (0,
|
|
13896
|
+
const bytes = (0, import_node_fs19.readFileSync)(file);
|
|
12192
13897
|
const contentHash = (0, import_node_crypto11.createHash)("sha256").update(bytes).digest("hex");
|
|
12193
13898
|
const metadata = parseMetadata(opts.metadata);
|
|
12194
13899
|
if (opts.taskId && metadata.taskId === void 0) metadata.taskId = opts.taskId;
|
|
@@ -12324,10 +14029,10 @@ async function putDirectAssetBytes(plan, bytes, mime) {
|
|
|
12324
14029
|
}
|
|
12325
14030
|
function listFilesRecursive(root) {
|
|
12326
14031
|
const out = [];
|
|
12327
|
-
for (const name of (0,
|
|
14032
|
+
for (const name of (0, import_node_fs19.readdirSync)(root)) {
|
|
12328
14033
|
if (name.startsWith(".")) continue;
|
|
12329
14034
|
const full = (0, import_node_path14.join)(root, name);
|
|
12330
|
-
const stat = (0,
|
|
14035
|
+
const stat = (0, import_node_fs19.lstatSync)(full);
|
|
12331
14036
|
if (stat.isSymbolicLink()) continue;
|
|
12332
14037
|
if (stat.isDirectory()) {
|
|
12333
14038
|
out.push(...listFilesRecursive(full));
|
|
@@ -12361,7 +14066,7 @@ async function downloadAsset(assetId, outPath) {
|
|
|
12361
14066
|
exitWithError(`asset download failed (${res.status}): ${errorMessage(body)}`);
|
|
12362
14067
|
}
|
|
12363
14068
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
12364
|
-
(0,
|
|
14069
|
+
(0, import_node_fs19.writeFileSync)(outPath, bytes);
|
|
12365
14070
|
}
|
|
12366
14071
|
function parseMetadata(raw) {
|
|
12367
14072
|
if (!raw) return {};
|
|
@@ -12760,7 +14465,7 @@ function buildCookbookCommand() {
|
|
|
12760
14465
|
for (const suite of suites) {
|
|
12761
14466
|
checks.push(...await runSuite(suite, cloud, opts));
|
|
12762
14467
|
}
|
|
12763
|
-
const summary =
|
|
14468
|
+
const summary = summarize2(checks, Boolean(opts.strict));
|
|
12764
14469
|
if (opts.json) {
|
|
12765
14470
|
printJson(summary);
|
|
12766
14471
|
} else {
|
|
@@ -12925,7 +14630,7 @@ function parseSuites(raw) {
|
|
|
12925
14630
|
const expanded = selected.includes("all") ? ["status", "im", "task", "group", "asset", "sandbox"] : selected;
|
|
12926
14631
|
return [...new Set(expanded)];
|
|
12927
14632
|
}
|
|
12928
|
-
function
|
|
14633
|
+
function summarize2(checks, strict) {
|
|
12929
14634
|
const totals = { pass: 0, fail: 0, skip: 0 };
|
|
12930
14635
|
for (const check of checks) totals[check.status] += 1;
|
|
12931
14636
|
return {
|
|
@@ -13008,7 +14713,7 @@ function parsePositiveInt3(value) {
|
|
|
13008
14713
|
// src/cli/commands/daemon.ts
|
|
13009
14714
|
var import_commander8 = require("commander");
|
|
13010
14715
|
var import_node_child_process7 = require("child_process");
|
|
13011
|
-
var
|
|
14716
|
+
var import_node_fs20 = require("fs");
|
|
13012
14717
|
var import_promises3 = require("timers/promises");
|
|
13013
14718
|
var import_node_path15 = require("path");
|
|
13014
14719
|
init_util();
|
|
@@ -13084,9 +14789,9 @@ function buildDaemonCommand() {
|
|
|
13084
14789
|
if (existingPid && pidAlive2(existingPid)) {
|
|
13085
14790
|
exitWithError(`Daemon already running (pid ${existingPid}). Use \`prismer daemon stop\` first.`);
|
|
13086
14791
|
}
|
|
13087
|
-
if (!(0,
|
|
14792
|
+
if (!(0, import_node_fs20.existsSync)(paths.logsDir)) (0, import_node_fs20.mkdirSync)(paths.logsDir, { recursive: true });
|
|
13088
14793
|
const logFile = (0, import_node_path15.join)(paths.logsDir, "daemon.log");
|
|
13089
|
-
const fd = (0,
|
|
14794
|
+
const fd = (0, import_node_fs20.openSync)(logFile, "a");
|
|
13090
14795
|
const args = [process.argv[1], "daemon", "run"];
|
|
13091
14796
|
if (opts.port) args.push("--port", String(opts.port));
|
|
13092
14797
|
if (opts.localServer === false) args.push("--no-local-server");
|
|
@@ -13170,7 +14875,7 @@ function buildDaemonCommand() {
|
|
|
13170
14875
|
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
14876
|
const paths = resolvePaths();
|
|
13172
14877
|
const logFile = (0, import_node_path15.join)(paths.logsDir, "daemon.log");
|
|
13173
|
-
if (!(0,
|
|
14878
|
+
if (!(0, import_node_fs20.existsSync)(logFile)) {
|
|
13174
14879
|
exitWithError(`No daemon log found at ${logFile}. Start the daemon with \`prismer daemon start\`.`);
|
|
13175
14880
|
}
|
|
13176
14881
|
const lines = Math.max(1, opts.tail);
|
|
@@ -13211,13 +14916,13 @@ async function tailFromEnd(path9, lines) {
|
|
|
13211
14916
|
}
|
|
13212
14917
|
}
|
|
13213
14918
|
async function followFile(path9) {
|
|
13214
|
-
let offset = (0,
|
|
14919
|
+
let offset = (0, import_node_fs20.statSync)(path9).size;
|
|
13215
14920
|
for (; ; ) {
|
|
13216
14921
|
await (0, import_promises3.setTimeout)(1e3);
|
|
13217
|
-
const size = (0,
|
|
14922
|
+
const size = (0, import_node_fs20.statSync)(path9).size;
|
|
13218
14923
|
if (size < offset) offset = 0;
|
|
13219
14924
|
if (size === offset) continue;
|
|
13220
|
-
const stream = (0,
|
|
14925
|
+
const stream = (0, import_node_fs20.createReadStream)(path9, { start: offset, end: size - 1, encoding: "utf8" });
|
|
13221
14926
|
for await (const chunk of stream) process.stdout.write(chunk);
|
|
13222
14927
|
offset = size;
|
|
13223
14928
|
}
|
|
@@ -13225,8 +14930,8 @@ async function followFile(path9) {
|
|
|
13225
14930
|
|
|
13226
14931
|
// src/cli/commands/events.ts
|
|
13227
14932
|
var import_commander9 = require("commander");
|
|
13228
|
-
var
|
|
13229
|
-
var
|
|
14933
|
+
var import_node_fs21 = require("fs");
|
|
14934
|
+
var import_node_os11 = require("os");
|
|
13230
14935
|
var import_node_path16 = require("path");
|
|
13231
14936
|
var import_node_readline = require("readline");
|
|
13232
14937
|
init_util();
|
|
@@ -13234,7 +14939,7 @@ var DEFAULT_LIMIT2 = 50;
|
|
|
13234
14939
|
function buildEventsCommand() {
|
|
13235
14940
|
return addEventOptions(new import_commander9.Command("events").description("Read local PARA events from ~/.prismer/para/events.jsonl")).action(async (opts) => {
|
|
13236
14941
|
const file = eventsPath();
|
|
13237
|
-
if (!(0,
|
|
14942
|
+
if (!(0, import_node_fs21.existsSync)(file)) {
|
|
13238
14943
|
printJson(unavailable(file));
|
|
13239
14944
|
process.exitCode = 1;
|
|
13240
14945
|
return;
|
|
@@ -13251,7 +14956,7 @@ function buildEventsCommand() {
|
|
|
13251
14956
|
function buildEventsStatsCommand() {
|
|
13252
14957
|
return addEventOptions(new import_commander9.Command("events:stats").description("Summarize local PARA events from ~/.prismer/para/events.jsonl")).action(async (opts) => {
|
|
13253
14958
|
const file = eventsPath();
|
|
13254
|
-
if (!(0,
|
|
14959
|
+
if (!(0, import_node_fs21.existsSync)(file)) {
|
|
13255
14960
|
printJson(unavailable(file));
|
|
13256
14961
|
process.exitCode = 1;
|
|
13257
14962
|
return;
|
|
@@ -13260,7 +14965,7 @@ function buildEventsStatsCommand() {
|
|
|
13260
14965
|
const events = await readEvents(file, filters);
|
|
13261
14966
|
printJson({
|
|
13262
14967
|
ok: true,
|
|
13263
|
-
data: { stats:
|
|
14968
|
+
data: { stats: summarize3(events), count: events.length, limit: filters.limit },
|
|
13264
14969
|
checks: { file, exists: true, filters: cleanFilters(filters) }
|
|
13265
14970
|
});
|
|
13266
14971
|
});
|
|
@@ -13269,7 +14974,7 @@ function addEventOptions(cmd) {
|
|
|
13269
14974
|
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
14975
|
}
|
|
13271
14976
|
function eventsPath() {
|
|
13272
|
-
return (0, import_node_path16.join)(process.env.PRISMER_HOME ?? (0, import_node_path16.join)((0,
|
|
14977
|
+
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
14978
|
}
|
|
13274
14979
|
function unavailable(file) {
|
|
13275
14980
|
return {
|
|
@@ -13285,7 +14990,7 @@ function unavailable(file) {
|
|
|
13285
14990
|
async function readEvents(file, filters) {
|
|
13286
14991
|
const out = [];
|
|
13287
14992
|
const rl = (0, import_node_readline.createInterface)({
|
|
13288
|
-
input: (0,
|
|
14993
|
+
input: (0, import_node_fs21.createReadStream)(file, { encoding: "utf8" }),
|
|
13289
14994
|
crlfDelay: Infinity
|
|
13290
14995
|
});
|
|
13291
14996
|
for await (const line of rl) {
|
|
@@ -13317,7 +15022,7 @@ function field(event, names) {
|
|
|
13317
15022
|
}
|
|
13318
15023
|
return void 0;
|
|
13319
15024
|
}
|
|
13320
|
-
function
|
|
15025
|
+
function summarize3(events) {
|
|
13321
15026
|
const byFamily = {};
|
|
13322
15027
|
const byType = {};
|
|
13323
15028
|
const byAgentId = {};
|
|
@@ -13355,7 +15060,7 @@ function parsePositiveInt4(v) {
|
|
|
13355
15060
|
// src/cli/commands/memory.ts
|
|
13356
15061
|
var import_better_sqlite35 = __toESM(require("better-sqlite3"), 1);
|
|
13357
15062
|
var import_commander10 = require("commander");
|
|
13358
|
-
var
|
|
15063
|
+
var import_node_fs22 = require("fs");
|
|
13359
15064
|
init_util();
|
|
13360
15065
|
var LOCAL_BASE = process.env.PRISMER_DAEMON_URL ?? "http://127.0.0.1:3210";
|
|
13361
15066
|
function buildMemoryCommand() {
|
|
@@ -13507,7 +15212,7 @@ function readCacheSnapshot(limit) {
|
|
|
13507
15212
|
const paths = resolvePaths();
|
|
13508
15213
|
const empty = {
|
|
13509
15214
|
dbPath: paths.localDb,
|
|
13510
|
-
dbExists: (0,
|
|
15215
|
+
dbExists: (0, import_node_fs22.existsSync)(paths.localDb),
|
|
13511
15216
|
tables: {
|
|
13512
15217
|
cached_assets: { exists: false, count: 0, sizeBytes: 0 },
|
|
13513
15218
|
workspace_files_mirror: { exists: false, count: 0 }
|
|
@@ -13671,8 +15376,8 @@ function buildPairCommand() {
|
|
|
13671
15376
|
|
|
13672
15377
|
// src/cli/commands/profile.ts
|
|
13673
15378
|
var import_commander12 = require("commander");
|
|
13674
|
-
var
|
|
13675
|
-
var
|
|
15379
|
+
var import_node_fs23 = require("fs");
|
|
15380
|
+
var import_node_os12 = require("os");
|
|
13676
15381
|
var import_node_path17 = require("path");
|
|
13677
15382
|
var import_node_child_process8 = require("child_process");
|
|
13678
15383
|
init_util();
|
|
@@ -13721,12 +15426,12 @@ function buildProfileCommand() {
|
|
|
13721
15426
|
const profile = await cloud.get(
|
|
13722
15427
|
`/api/im/agent_profiles/${encodeURIComponent(profileId)}`
|
|
13723
15428
|
);
|
|
13724
|
-
const tmpFile = (0, import_node_path17.join)((0,
|
|
13725
|
-
(0,
|
|
15429
|
+
const tmpFile = (0, import_node_path17.join)((0, import_node_os12.tmpdir)(), `prismer-profile-${profileId}.json`);
|
|
15430
|
+
(0, import_node_fs23.writeFileSync)(tmpFile, JSON.stringify(profile.config, null, 2), "utf8");
|
|
13726
15431
|
const editor = process.env.EDITOR || "vi";
|
|
13727
15432
|
const ed = (0, import_node_child_process8.spawnSync)(editor, [tmpFile], { stdio: "inherit" });
|
|
13728
15433
|
if (ed.status !== 0) exitWithError(`editor exited ${ed.status}`, { code: "editor_failed" });
|
|
13729
|
-
const newConfig = JSON.parse((0,
|
|
15434
|
+
const newConfig = JSON.parse((0, import_node_fs23.readFileSync)(tmpFile, "utf8"));
|
|
13730
15435
|
const res = await cloud.request("PATCH", `/api/im/agent_profiles/${encodeURIComponent(profileId)}`, {
|
|
13731
15436
|
body: { config: newConfig, version: profile.version }
|
|
13732
15437
|
});
|
|
@@ -13748,8 +15453,8 @@ function mkCloud4() {
|
|
|
13748
15453
|
function readJsonArg(arg) {
|
|
13749
15454
|
if (arg.startsWith("@")) {
|
|
13750
15455
|
const path9 = arg.slice(1);
|
|
13751
|
-
if (!(0,
|
|
13752
|
-
return JSON.parse((0,
|
|
15456
|
+
if (!(0, import_node_fs23.existsSync)(path9)) throw new Error(`File not found: ${path9}`);
|
|
15457
|
+
return JSON.parse((0, import_node_fs23.readFileSync)(path9, "utf8"));
|
|
13753
15458
|
}
|
|
13754
15459
|
return JSON.parse(arg);
|
|
13755
15460
|
}
|
|
@@ -13763,8 +15468,8 @@ async function resolveDefaultWorkspaceId(cloud) {
|
|
|
13763
15468
|
// src/cli/commands/reset.ts
|
|
13764
15469
|
var import_commander13 = require("commander");
|
|
13765
15470
|
var import_node_child_process9 = require("child_process");
|
|
13766
|
-
var
|
|
13767
|
-
var
|
|
15471
|
+
var import_node_fs24 = require("fs");
|
|
15472
|
+
var import_node_os13 = require("os");
|
|
13768
15473
|
var import_node_path18 = require("path");
|
|
13769
15474
|
var import_promises4 = require("timers/promises");
|
|
13770
15475
|
init_util();
|
|
@@ -13782,15 +15487,15 @@ function buildResetCommand() {
|
|
|
13782
15487
|
const hermesGatewayPids = findHermesGatewayPids2();
|
|
13783
15488
|
const plan = {
|
|
13784
15489
|
root: paths.root,
|
|
13785
|
-
exists: (0,
|
|
15490
|
+
exists: (0, import_node_fs24.existsSync)(paths.root),
|
|
13786
15491
|
runningPid,
|
|
13787
15492
|
mode: opts.wipe ? "wipe" : "archive",
|
|
13788
15493
|
archivePath,
|
|
13789
15494
|
assetSyncRoot,
|
|
13790
|
-
assetSyncExists: (0,
|
|
15495
|
+
assetSyncExists: (0, import_node_fs24.existsSync)(assetSyncRoot),
|
|
13791
15496
|
assetSyncArchivePath,
|
|
13792
15497
|
hermesHome,
|
|
13793
|
-
hermesExists: (0,
|
|
15498
|
+
hermesExists: (0, import_node_fs24.existsSync)(hermesHome),
|
|
13794
15499
|
hermesArchivePath,
|
|
13795
15500
|
hermesGatewayPids,
|
|
13796
15501
|
willStopDaemon: runningPid !== null,
|
|
@@ -13814,40 +15519,40 @@ function buildResetCommand() {
|
|
|
13814
15519
|
for (const gatewayPid of hermesGatewayPids) {
|
|
13815
15520
|
await stopProcess(gatewayPid, opts.timeout ?? 5e3, "Hermes gateway");
|
|
13816
15521
|
}
|
|
13817
|
-
if ((0,
|
|
15522
|
+
if ((0, import_node_fs24.existsSync)(paths.root)) {
|
|
13818
15523
|
if (opts.wipe) {
|
|
13819
|
-
(0,
|
|
15524
|
+
(0, import_node_fs24.rmSync)(paths.root, { recursive: true, force: true });
|
|
13820
15525
|
} else {
|
|
13821
15526
|
if (!archivePath) throw new Error("internal: archivePath missing");
|
|
13822
|
-
if (!(0,
|
|
13823
|
-
(0,
|
|
15527
|
+
if (!(0, import_node_fs24.existsSync)((0, import_node_path18.dirname)(archivePath))) {
|
|
15528
|
+
(0, import_node_fs24.mkdirSync)((0, import_node_path18.dirname)(archivePath), { recursive: true });
|
|
13824
15529
|
}
|
|
13825
|
-
(0,
|
|
15530
|
+
(0, import_node_fs24.renameSync)(paths.root, archivePath);
|
|
13826
15531
|
}
|
|
13827
15532
|
}
|
|
13828
|
-
if ((0,
|
|
15533
|
+
if ((0, import_node_fs24.existsSync)(assetSyncRoot)) {
|
|
13829
15534
|
if (opts.wipe) {
|
|
13830
|
-
(0,
|
|
15535
|
+
(0, import_node_fs24.rmSync)(assetSyncRoot, { recursive: true, force: true });
|
|
13831
15536
|
} else {
|
|
13832
15537
|
if (!assetSyncArchivePath) throw new Error("internal: assetSyncArchivePath missing");
|
|
13833
|
-
if (!(0,
|
|
13834
|
-
(0,
|
|
15538
|
+
if (!(0, import_node_fs24.existsSync)((0, import_node_path18.dirname)(assetSyncArchivePath))) {
|
|
15539
|
+
(0, import_node_fs24.mkdirSync)((0, import_node_path18.dirname)(assetSyncArchivePath), { recursive: true });
|
|
13835
15540
|
}
|
|
13836
|
-
(0,
|
|
15541
|
+
(0, import_node_fs24.renameSync)(assetSyncRoot, assetSyncArchivePath);
|
|
13837
15542
|
}
|
|
13838
15543
|
}
|
|
13839
|
-
if ((0,
|
|
15544
|
+
if ((0, import_node_fs24.existsSync)(hermesHome)) {
|
|
13840
15545
|
if (opts.wipe) {
|
|
13841
|
-
(0,
|
|
15546
|
+
(0, import_node_fs24.rmSync)(hermesHome, { recursive: true, force: true });
|
|
13842
15547
|
} else {
|
|
13843
15548
|
if (!hermesArchivePath) throw new Error("internal: hermesArchivePath missing");
|
|
13844
|
-
if (!(0,
|
|
13845
|
-
(0,
|
|
15549
|
+
if (!(0, import_node_fs24.existsSync)((0, import_node_path18.dirname)(hermesArchivePath))) {
|
|
15550
|
+
(0, import_node_fs24.mkdirSync)((0, import_node_path18.dirname)(hermesArchivePath), { recursive: true });
|
|
13846
15551
|
}
|
|
13847
|
-
(0,
|
|
15552
|
+
(0, import_node_fs24.renameSync)(hermesHome, hermesArchivePath);
|
|
13848
15553
|
}
|
|
13849
15554
|
}
|
|
13850
|
-
(0,
|
|
15555
|
+
(0, import_node_fs24.mkdirSync)(paths.root, { recursive: true });
|
|
13851
15556
|
if (opts.json) {
|
|
13852
15557
|
printJson({ ok: true, reset: true, plan });
|
|
13853
15558
|
return;
|
|
@@ -13916,13 +15621,13 @@ function timestamp() {
|
|
|
13916
15621
|
function resolveAssetSyncRoot() {
|
|
13917
15622
|
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
13918
15623
|
if (override) return override;
|
|
13919
|
-
const home = (0,
|
|
15624
|
+
const home = (0, import_node_os13.homedir)();
|
|
13920
15625
|
const desktop = (0, import_node_path18.join)(home, "Desktop");
|
|
13921
|
-
const base = (0,
|
|
15626
|
+
const base = (0, import_node_fs24.existsSync)(desktop) ? desktop : home;
|
|
13922
15627
|
return (0, import_node_path18.join)(base, "Prismer Assets");
|
|
13923
15628
|
}
|
|
13924
15629
|
function resolveHermesHome() {
|
|
13925
|
-
return process.env.HERMES_HOME || (0, import_node_path18.join)((0,
|
|
15630
|
+
return process.env.HERMES_HOME || (0, import_node_path18.join)((0, import_node_os13.homedir)(), ".hermes");
|
|
13926
15631
|
}
|
|
13927
15632
|
function findHermesGatewayPids2() {
|
|
13928
15633
|
try {
|
|
@@ -14152,10 +15857,10 @@ async function safeParseResponse(res) {
|
|
|
14152
15857
|
|
|
14153
15858
|
// src/cli/commands/setup.ts
|
|
14154
15859
|
var import_commander15 = require("commander");
|
|
14155
|
-
var
|
|
15860
|
+
var import_node_os14 = require("os");
|
|
14156
15861
|
var import_node_child_process10 = require("child_process");
|
|
14157
15862
|
var import_node_crypto12 = require("crypto");
|
|
14158
|
-
var
|
|
15863
|
+
var import_node_fs25 = require("fs");
|
|
14159
15864
|
var import_node_http2 = require("http");
|
|
14160
15865
|
init_util();
|
|
14161
15866
|
init_ui();
|
|
@@ -14197,7 +15902,7 @@ function buildSetupCommand() {
|
|
|
14197
15902
|
}
|
|
14198
15903
|
const result = await pair({
|
|
14199
15904
|
cloudBaseUrl,
|
|
14200
|
-
deviceName: opts.deviceName ?? (0,
|
|
15905
|
+
deviceName: opts.deviceName ?? (0, import_node_os14.hostname)(),
|
|
14201
15906
|
force: opts.force,
|
|
14202
15907
|
paths,
|
|
14203
15908
|
asUserEmail: opts.asUser
|
|
@@ -14214,13 +15919,13 @@ function buildSetupCommand() {
|
|
|
14214
15919
|
apiKey = await mintDaemonApiKey({
|
|
14215
15920
|
cloudBaseUrl,
|
|
14216
15921
|
token: authToken,
|
|
14217
|
-
deviceName: opts.deviceName ?? (0,
|
|
15922
|
+
deviceName: opts.deviceName ?? (0, import_node_os14.hostname)()
|
|
14218
15923
|
});
|
|
14219
15924
|
}
|
|
14220
15925
|
if (!apiKey && !authToken && !opts.check && opts.browser !== false) {
|
|
14221
15926
|
apiKey = await runBrowserSetup({
|
|
14222
15927
|
cloudBaseUrl,
|
|
14223
|
-
deviceName: opts.deviceName ?? (0,
|
|
15928
|
+
deviceName: opts.deviceName ?? (0, import_node_os14.hostname)(),
|
|
14224
15929
|
json: Boolean(opts.json)
|
|
14225
15930
|
});
|
|
14226
15931
|
}
|
|
@@ -14406,9 +16111,9 @@ function shouldArchiveLocalDb(previous, next) {
|
|
|
14406
16111
|
return previous.api_key !== next.api_key || previous.cloud_api_base !== next.cloud_api_base || previous.daemon_id !== next.daemon_id;
|
|
14407
16112
|
}
|
|
14408
16113
|
function archiveLocalDb(localDbPath) {
|
|
14409
|
-
if (!(0,
|
|
16114
|
+
if (!(0, import_node_fs25.existsSync)(localDbPath)) return;
|
|
14410
16115
|
const archived = `${localDbPath}.${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
|
|
14411
|
-
(0,
|
|
16116
|
+
(0, import_node_fs25.renameSync)(localDbPath, archived);
|
|
14412
16117
|
}
|
|
14413
16118
|
async function mintDaemonApiKey(input) {
|
|
14414
16119
|
const label = `Daemon: ${input.deviceName} ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
|
|
@@ -14477,7 +16182,7 @@ function buildSkillCommand() {
|
|
|
14477
16182
|
const cloud = mkCloud6();
|
|
14478
16183
|
const path9 = opts.agent ? `/api/im/agents/${encodeURIComponent(opts.agent)}/skills` : `/api/im/skills/${encodeURIComponent(slugOrId)}/install`;
|
|
14479
16184
|
const body = opts.agent ? { skillId: slugOrId, workspaceId: opts.workspaceId, version: opts.version } : { workspaceId: opts.workspaceId, version: opts.version };
|
|
14480
|
-
const data = await
|
|
16185
|
+
const data = await requestEnvelope2(cloud, "POST", path9, body, "skill_install_failed");
|
|
14481
16186
|
if (opts.json) {
|
|
14482
16187
|
printJson(data);
|
|
14483
16188
|
return;
|
|
@@ -14488,7 +16193,7 @@ function buildSkillCommand() {
|
|
|
14488
16193
|
const cloud = mkCloud6();
|
|
14489
16194
|
const path9 = opts.agent ? `/api/im/agents/${encodeURIComponent(opts.agent)}/skills` : `/api/im/skills/${encodeURIComponent(slugOrId)}/install`;
|
|
14490
16195
|
const body = opts.agent ? { skillId: slugOrId, workspaceId: opts.workspaceId } : void 0;
|
|
14491
|
-
const data = await
|
|
16196
|
+
const data = await requestEnvelope2(cloud, "DELETE", path9, body, "skill_uninstall_failed");
|
|
14492
16197
|
if (opts.json) {
|
|
14493
16198
|
printJson(data);
|
|
14494
16199
|
return;
|
|
@@ -14541,7 +16246,7 @@ function mkCloud6() {
|
|
|
14541
16246
|
const cfg = loadConfig(resolvePaths());
|
|
14542
16247
|
return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
14543
16248
|
}
|
|
14544
|
-
async function
|
|
16249
|
+
async function requestEnvelope2(cloud, method, path9, body, code) {
|
|
14545
16250
|
const res = await cloud.request(method, path9, {
|
|
14546
16251
|
body
|
|
14547
16252
|
});
|
|
@@ -14660,7 +16365,7 @@ function printSkill(skill, includeContent) {
|
|
|
14660
16365
|
|
|
14661
16366
|
// src/cli/commands/status.ts
|
|
14662
16367
|
var import_commander17 = require("commander");
|
|
14663
|
-
var
|
|
16368
|
+
var import_node_os15 = require("os");
|
|
14664
16369
|
init_util();
|
|
14665
16370
|
init_ui();
|
|
14666
16371
|
function buildStatusCommand() {
|
|
@@ -14691,6 +16396,7 @@ function buildStatusCommand() {
|
|
|
14691
16396
|
let me = null;
|
|
14692
16397
|
let devices = null;
|
|
14693
16398
|
let agents = null;
|
|
16399
|
+
let diagnose = null;
|
|
14694
16400
|
try {
|
|
14695
16401
|
const meRes = await cloud.request("GET", "/api/im/me", { timeoutMs: 3e3 });
|
|
14696
16402
|
cloudOk = meRes.ok;
|
|
@@ -14703,19 +16409,33 @@ function buildStatusCommand() {
|
|
|
14703
16409
|
if (Array.isArray(wsList) && wsList.length > 0) {
|
|
14704
16410
|
const wsId = wsList[0]?.id;
|
|
14705
16411
|
if (wsId) {
|
|
14706
|
-
const
|
|
14707
|
-
if (
|
|
14708
|
-
const
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
|
|
14713
|
-
|
|
14714
|
-
agents = agBody?.data;
|
|
16412
|
+
const rtRes = await cloud.request("GET", `/api/im/workspaces/${encodeURIComponent(wsId)}/runtime`, { timeoutMs: 3e3 });
|
|
16413
|
+
if (rtRes.ok) {
|
|
16414
|
+
const rtBody = rtRes.data;
|
|
16415
|
+
const snapshot = rtBody?.data;
|
|
16416
|
+
if (snapshot && Array.isArray(snapshot.devices)) {
|
|
16417
|
+
devices = snapshot.devices;
|
|
16418
|
+
agents = snapshot.devices.flatMap((d) => d.agents ?? []);
|
|
16419
|
+
}
|
|
14715
16420
|
}
|
|
14716
16421
|
}
|
|
14717
16422
|
}
|
|
14718
16423
|
}
|
|
16424
|
+
const daemonId = cfg.daemon_id;
|
|
16425
|
+
if (daemonId) {
|
|
16426
|
+
try {
|
|
16427
|
+
const diagRes = await cloud.request(
|
|
16428
|
+
"GET",
|
|
16429
|
+
`/api/im/runtime/diagnose?daemonId=${encodeURIComponent(daemonId)}`,
|
|
16430
|
+
{ timeoutMs: 3e3 }
|
|
16431
|
+
);
|
|
16432
|
+
if (diagRes.ok) {
|
|
16433
|
+
const body = diagRes.data;
|
|
16434
|
+
if (body?.data && typeof body.data === "object") diagnose = body.data;
|
|
16435
|
+
}
|
|
16436
|
+
} catch {
|
|
16437
|
+
}
|
|
16438
|
+
}
|
|
14719
16439
|
}
|
|
14720
16440
|
} catch {
|
|
14721
16441
|
cloudOk = false;
|
|
@@ -14732,7 +16452,8 @@ function buildStatusCommand() {
|
|
|
14732
16452
|
info: daemonStatus.info ?? {}
|
|
14733
16453
|
},
|
|
14734
16454
|
cloud: { base: cfg.cloud_api_base, reachable: cloudOk, me, devices, agents },
|
|
14735
|
-
local
|
|
16455
|
+
local,
|
|
16456
|
+
diagnose
|
|
14736
16457
|
};
|
|
14737
16458
|
if (opts.json) {
|
|
14738
16459
|
printJson(report);
|
|
@@ -14741,6 +16462,60 @@ function buildStatusCommand() {
|
|
|
14741
16462
|
printPretty(report);
|
|
14742
16463
|
});
|
|
14743
16464
|
}
|
|
16465
|
+
function computeDeviceIcon(lastSeenAt, now) {
|
|
16466
|
+
if (!lastSeenAt) return "\u25CB";
|
|
16467
|
+
const age = now.getTime() - Date.parse(lastSeenAt);
|
|
16468
|
+
if (!Number.isFinite(age) || age < 0) return "\u25CB";
|
|
16469
|
+
if (age < 5 * 6e4) return "\u25CF";
|
|
16470
|
+
if (age < 60 * 6e4) return "\u25D0";
|
|
16471
|
+
return "\u25CB";
|
|
16472
|
+
}
|
|
16473
|
+
function computeAgentIcon(status, lastHeartbeat, now) {
|
|
16474
|
+
const s = (status || "").toLowerCase();
|
|
16475
|
+
if (s === "error" || s === "failed") return "\u25C6";
|
|
16476
|
+
if (!lastHeartbeat) return s === "offline" ? "\u25CB" : "\u25D0";
|
|
16477
|
+
const age = now.getTime() - Date.parse(lastHeartbeat);
|
|
16478
|
+
if (!Number.isFinite(age) || age < 0 || age >= 60 * 6e4) return "\u25CB";
|
|
16479
|
+
if (age >= 5 * 6e4) return "\u25D0";
|
|
16480
|
+
return "\u25CF";
|
|
16481
|
+
}
|
|
16482
|
+
function pad(value, width) {
|
|
16483
|
+
if (value.length >= width) return value.slice(0, width);
|
|
16484
|
+
return value + " ".repeat(width - value.length);
|
|
16485
|
+
}
|
|
16486
|
+
function formatDeviceBindings(devices, now = /* @__PURE__ */ new Date()) {
|
|
16487
|
+
const lines = [];
|
|
16488
|
+
if (devices.length === 0) {
|
|
16489
|
+
lines.push(" No paired devices.");
|
|
16490
|
+
return lines;
|
|
16491
|
+
}
|
|
16492
|
+
const totalAgents = devices.reduce((s, d) => s + (d.agents?.length ?? 0), 0);
|
|
16493
|
+
const devWord = devices.length === 1 ? "device" : "devices";
|
|
16494
|
+
const agWord = totalAgents === 1 ? "agent" : "agents";
|
|
16495
|
+
lines.push(` Device & Agent Bindings (${devices.length} ${devWord} \xB7 ${totalAgents} ${agWord})`);
|
|
16496
|
+
lines.push("");
|
|
16497
|
+
for (let di = 0; di < devices.length; di++) {
|
|
16498
|
+
const d = devices[di];
|
|
16499
|
+
const dIcon = computeDeviceIcon(d.lastSeenAt, now);
|
|
16500
|
+
const dName = pad(d.name || d.deviceId, 40);
|
|
16501
|
+
const lastSeen = d.lastSeenAt ? formatRelative(d.lastSeenAt, now) : "never seen";
|
|
16502
|
+
lines.push(` ${dIcon} ${dName} ${lastSeen}`);
|
|
16503
|
+
const agents = d.agents ?? [];
|
|
16504
|
+
for (let ai = 0; ai < agents.length; ai++) {
|
|
16505
|
+
const a = agents[ai];
|
|
16506
|
+
const isLast = ai === agents.length - 1;
|
|
16507
|
+
const branch = isLast ? "\u2514\u2500" : "\u251C\u2500";
|
|
16508
|
+
const aIcon = computeAgentIcon(a.status, a.lastHeartbeat, now);
|
|
16509
|
+
const aName = pad(a.name || a.id, 24);
|
|
16510
|
+
const aStatus = pad(a.status || "?", 8);
|
|
16511
|
+
const aVersion = pad(a.version ? `v${a.version}` : "\u2014", 10);
|
|
16512
|
+
const tail = a.currentTaskId ? `task=${a.currentTaskId}` : a.lastHeartbeat ? `\u2764 ${formatRelative(a.lastHeartbeat, now)}` : "";
|
|
16513
|
+
lines.push(` ${branch} ${aIcon} ${aName} ${aStatus} ${aVersion} ${tail}`.trimEnd());
|
|
16514
|
+
}
|
|
16515
|
+
if (di < devices.length - 1) lines.push("");
|
|
16516
|
+
}
|
|
16517
|
+
return lines;
|
|
16518
|
+
}
|
|
14744
16519
|
async function readDaemonStatus() {
|
|
14745
16520
|
try {
|
|
14746
16521
|
const res = await fetch("http://127.0.0.1:3210/healthz", {
|
|
@@ -14784,7 +16559,7 @@ function printPretty(report) {
|
|
|
14784
16559
|
ui.blank();
|
|
14785
16560
|
ok("Config", report.paths.config);
|
|
14786
16561
|
if (report.daemon.running) {
|
|
14787
|
-
const daemonName = report.cloud.me && typeof report.cloud.me === "object" ? report.cloud.me.user?.username ?? (0,
|
|
16562
|
+
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
16563
|
const ws = report.daemon.wsConnected ? "connected" : "pending";
|
|
14789
16564
|
ok("Daemon", `${daemonName} pid=${report.daemon.pid} ws=${ws}`);
|
|
14790
16565
|
if (report.daemon.info) {
|
|
@@ -14813,16 +16588,7 @@ function printPretty(report) {
|
|
|
14813
16588
|
if (Array.isArray(report.cloud.devices) && report.cloud.devices.length > 0) {
|
|
14814
16589
|
const devs = report.cloud.devices;
|
|
14815
16590
|
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}`);
|
|
16591
|
+
for (const ln of formatDeviceBindings(devs)) ui.line(ln);
|
|
14826
16592
|
}
|
|
14827
16593
|
if (report.local) {
|
|
14828
16594
|
ui.blank();
|
|
@@ -14832,6 +16598,58 @@ function printPretty(report) {
|
|
|
14832
16598
|
} else {
|
|
14833
16599
|
warn("Local DB", "unavailable");
|
|
14834
16600
|
}
|
|
16601
|
+
if (report.diagnose) {
|
|
16602
|
+
ui.blank();
|
|
16603
|
+
ui.header("Cloud dispatch reachability");
|
|
16604
|
+
ui.blank();
|
|
16605
|
+
printDiagnose(report.diagnose, report.daemon.pid ?? null);
|
|
16606
|
+
}
|
|
16607
|
+
}
|
|
16608
|
+
function printDiagnose(d, pid) {
|
|
16609
|
+
const ui = getUI();
|
|
16610
|
+
const heartbeatLine = d.lastHeartbeatAt ? `last heartbeat ${formatRelative(d.lastHeartbeatAt)}` : "no heartbeat seen";
|
|
16611
|
+
if (d.wsAlive) {
|
|
16612
|
+
ok("Daemon", `running${pid ? ` (pid ${pid})` : ""}`);
|
|
16613
|
+
ok("Cloud WS", `connected (${heartbeatLine})`);
|
|
16614
|
+
} else {
|
|
16615
|
+
warn("Daemon", `running${pid ? ` (pid ${pid})` : ""} \u2014 not visible to cloud WS`);
|
|
16616
|
+
fail2("Cloud WS", `disconnected (${heartbeatLine})`);
|
|
16617
|
+
}
|
|
16618
|
+
if (d.gatewayUrl) {
|
|
16619
|
+
if (d.gatewayIsPrivate) {
|
|
16620
|
+
warn("HTTP gateway", `${d.gatewayUrl} (private IP \u2014 only reachable on same network)`);
|
|
16621
|
+
} else if (d.httpReachable) {
|
|
16622
|
+
ok(
|
|
16623
|
+
"HTTP gateway",
|
|
16624
|
+
`${d.gatewayUrl} (reachable${d.httpLatencyMs != null ? `, ${d.httpLatencyMs}ms` : ""})`
|
|
16625
|
+
);
|
|
16626
|
+
} else {
|
|
16627
|
+
fail2("HTTP gateway", `${d.gatewayUrl} (unreachable${d.httpError ? ` \u2014 ${d.httpError}` : ""})`);
|
|
16628
|
+
}
|
|
16629
|
+
} else {
|
|
16630
|
+
ui.line(" HTTP gateway: not declared (WS-only daemon)");
|
|
16631
|
+
}
|
|
16632
|
+
const activeAgents = /* @__PURE__ */ (() => {
|
|
16633
|
+
return 0;
|
|
16634
|
+
})();
|
|
16635
|
+
if (activeAgents > 0) {
|
|
16636
|
+
ok("Active agents", String(activeAgents));
|
|
16637
|
+
}
|
|
16638
|
+
const recColour = d.recommendedTransport === "ws" ? ok : d.recommendedTransport === "http" ? warn : fail2;
|
|
16639
|
+
recColour("Recommended transport", d.recommendedTransport);
|
|
16640
|
+
if (d.lastProbeAt) {
|
|
16641
|
+
ui.line(` Last transport probe: ${formatRelative(d.lastProbeAt)}`);
|
|
16642
|
+
}
|
|
16643
|
+
}
|
|
16644
|
+
function formatRelative(iso, now = /* @__PURE__ */ new Date()) {
|
|
16645
|
+
const ts = Date.parse(iso);
|
|
16646
|
+
if (!Number.isFinite(ts)) return "unknown";
|
|
16647
|
+
const diff = Math.max(0, now.getTime() - ts);
|
|
16648
|
+
if (diff < 3e4) return "just now";
|
|
16649
|
+
if (diff < 6e4) return `${Math.round(diff / 1e3)}s ago`;
|
|
16650
|
+
if (diff < 36e5) return `${Math.round(diff / 6e4)}m ago`;
|
|
16651
|
+
if (diff < 864e5) return `${Math.round(diff / 36e5)}h ago`;
|
|
16652
|
+
return `${Math.round(diff / 864e5)}d ago`;
|
|
14835
16653
|
}
|
|
14836
16654
|
|
|
14837
16655
|
// src/cli/commands/task.ts
|
|
@@ -15194,7 +17012,7 @@ async function readResponseError(res) {
|
|
|
15194
17012
|
|
|
15195
17013
|
// src/cli/index.ts
|
|
15196
17014
|
init_ui();
|
|
15197
|
-
var VERSION = "2.0.
|
|
17015
|
+
var VERSION = "2.0.5";
|
|
15198
17016
|
function buildProgram() {
|
|
15199
17017
|
const program = new import_commander20.Command("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
|
|
15200
17018
|
program.addCommand(buildBannerCommand());
|