@prismer/runtime 2.0.3 → 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 +2139 -295
- package/dist/cli.js +1978 -134
- package/dist/index.cjs +2107 -261
- package/dist/index.d.cts +533 -181
- package/dist/index.d.ts +533 -181
- package/dist/index.js +1992 -146
- 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
|
});
|
|
@@ -1545,11 +1879,39 @@ async function syncAllAgentSkills(profiles, cloud, options = {}) {
|
|
|
1545
1879
|
}
|
|
1546
1880
|
async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, signal) {
|
|
1547
1881
|
if (!agentImUserId) return { synced: 0, skipped: 0, unchanged: 0 };
|
|
1548
|
-
|
|
1882
|
+
let data = await cloud.get(
|
|
1549
1883
|
`/api/im/skills/installed?agentId=${encodeURIComponent(agentImUserId)}`,
|
|
1550
1884
|
{ signal }
|
|
1551
1885
|
);
|
|
1552
|
-
|
|
1886
|
+
let entries = normalizeInstalledSkills(data);
|
|
1887
|
+
if (entries.length === 0 && (profile.adapterName === "hermes" || profile.adapterName === "openclaw")) {
|
|
1888
|
+
try {
|
|
1889
|
+
const backfill = await cloud.request(
|
|
1890
|
+
"POST",
|
|
1891
|
+
`/api/im/agents/${encodeURIComponent(agentImUserId)}/skills/install-builtins`,
|
|
1892
|
+
{ body: {}, signal }
|
|
1893
|
+
);
|
|
1894
|
+
if (backfill.ok && backfill.data && typeof backfill.data === "object" && "data" in backfill.data) {
|
|
1895
|
+
const installed = backfill.data.data?.installed ?? 0;
|
|
1896
|
+
if (installed > 0) {
|
|
1897
|
+
process.stdout.write(
|
|
1898
|
+
`[daemon] skill sync: backfilled ${installed} built-in skills for agent ${agentImUserId.slice(-8)}
|
|
1899
|
+
`
|
|
1900
|
+
);
|
|
1901
|
+
data = await cloud.get(
|
|
1902
|
+
`/api/im/skills/installed?agentId=${encodeURIComponent(agentImUserId)}`,
|
|
1903
|
+
{ signal }
|
|
1904
|
+
);
|
|
1905
|
+
entries = normalizeInstalledSkills(data);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
} catch (err) {
|
|
1909
|
+
process.stderr.write(
|
|
1910
|
+
`[daemon] skill sync: backfill skipped for ${agentImUserId.slice(-8)}: ${err.message}
|
|
1911
|
+
`
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1553
1915
|
const skillsRoot = resolveSkillsRoot(profile);
|
|
1554
1916
|
if (!skillsRoot) return { synced: 0, skipped: 0, unchanged: 0 };
|
|
1555
1917
|
let synced = 0;
|
|
@@ -1590,7 +1952,7 @@ async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, sig
|
|
|
1590
1952
|
continue;
|
|
1591
1953
|
}
|
|
1592
1954
|
const skillDir = (0, import_node_path8.join)(skillsRoot, slug);
|
|
1593
|
-
await
|
|
1955
|
+
await import_node_fs8.promises.mkdir(skillDir, { recursive: true });
|
|
1594
1956
|
const localFiles = await walkLocalDir(skillDir);
|
|
1595
1957
|
let dirty = false;
|
|
1596
1958
|
let perFileFailures = 0;
|
|
@@ -1645,14 +2007,14 @@ async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, sig
|
|
|
1645
2007
|
perFileFailures++;
|
|
1646
2008
|
continue;
|
|
1647
2009
|
}
|
|
1648
|
-
await
|
|
1649
|
-
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);
|
|
1650
2012
|
localFiles.delete(file.path);
|
|
1651
2013
|
dirty = true;
|
|
1652
2014
|
}
|
|
1653
2015
|
for (const orphan of localFiles.keys()) {
|
|
1654
2016
|
try {
|
|
1655
|
-
await
|
|
2017
|
+
await import_node_fs8.promises.unlink((0, import_node_path8.join)(skillDir, orphan));
|
|
1656
2018
|
dirty = true;
|
|
1657
2019
|
} catch (err) {
|
|
1658
2020
|
if (err.code !== "ENOENT") {
|
|
@@ -1765,7 +2127,7 @@ async function walkLocalDir(root) {
|
|
|
1765
2127
|
async function walk2(dir) {
|
|
1766
2128
|
let entries;
|
|
1767
2129
|
try {
|
|
1768
|
-
entries = await
|
|
2130
|
+
entries = await import_node_fs8.promises.readdir(dir, { withFileTypes: true });
|
|
1769
2131
|
} catch (err) {
|
|
1770
2132
|
if (err.code === "ENOENT") return;
|
|
1771
2133
|
throw err;
|
|
@@ -1776,7 +2138,7 @@ async function walkLocalDir(root) {
|
|
|
1776
2138
|
await walk2(full);
|
|
1777
2139
|
} else if (ent.isFile()) {
|
|
1778
2140
|
try {
|
|
1779
|
-
const buf = await
|
|
2141
|
+
const buf = await import_node_fs8.promises.readFile(full);
|
|
1780
2142
|
const rel = (0, import_node_path8.relative)(root, full).split(import_node_path8.sep).join("/");
|
|
1781
2143
|
out.set(rel, sha256Buffer(buf));
|
|
1782
2144
|
} catch {
|
|
@@ -1850,11 +2212,11 @@ async function ackSkillSync(cloud, agentImUserId, input, signal) {
|
|
|
1850
2212
|
`);
|
|
1851
2213
|
}
|
|
1852
2214
|
}
|
|
1853
|
-
var
|
|
2215
|
+
var import_node_fs8, import_node_path8, import_node_crypto3, skillSyncWarnedProfiles;
|
|
1854
2216
|
var init_skill_sync = __esm({
|
|
1855
2217
|
"src/daemon/skill-sync.ts"() {
|
|
1856
2218
|
"use strict";
|
|
1857
|
-
|
|
2219
|
+
import_node_fs8 = require("fs");
|
|
1858
2220
|
import_node_path8 = require("path");
|
|
1859
2221
|
import_node_crypto3 = require("crypto");
|
|
1860
2222
|
init_hermes();
|
|
@@ -2295,7 +2657,7 @@ var require_package = __commonJS({
|
|
|
2295
2657
|
"package.json"(exports2, module2) {
|
|
2296
2658
|
module2.exports = {
|
|
2297
2659
|
name: "@prismer/runtime",
|
|
2298
|
-
version: "2.0.
|
|
2660
|
+
version: "2.0.5",
|
|
2299
2661
|
description: "Prismer Cloud daemon runtime \u2014 TS-only adapter host for hosted IM agents",
|
|
2300
2662
|
type: "module",
|
|
2301
2663
|
main: "dist/index.js",
|
|
@@ -2329,6 +2691,7 @@ var require_package = __commonJS({
|
|
|
2329
2691
|
},
|
|
2330
2692
|
dependencies: {
|
|
2331
2693
|
"@iarna/toml": "^2.2.5",
|
|
2694
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
2332
2695
|
"better-sqlite3": "^11.8.0",
|
|
2333
2696
|
commander: "^12.1.0",
|
|
2334
2697
|
qrcode: "^1.5.4",
|
|
@@ -2486,21 +2849,21 @@ function pidFilePath(paths) {
|
|
|
2486
2849
|
return (0, import_node_path12.join)(paths.root, "daemon.pid");
|
|
2487
2850
|
}
|
|
2488
2851
|
function writePidFile(paths, pid) {
|
|
2489
|
-
(0,
|
|
2852
|
+
(0, import_node_fs17.writeFileSync)(pidFilePath(paths), `${pid}
|
|
2490
2853
|
`, "utf8");
|
|
2491
2854
|
}
|
|
2492
2855
|
function readPidFile(paths) {
|
|
2493
2856
|
const p = pidFilePath(paths);
|
|
2494
|
-
if (!(0,
|
|
2495
|
-
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();
|
|
2496
2859
|
const pid = Number.parseInt(raw, 10);
|
|
2497
2860
|
return Number.isFinite(pid) ? pid : void 0;
|
|
2498
2861
|
}
|
|
2499
2862
|
function clearPidFile(paths) {
|
|
2500
2863
|
const p = pidFilePath(paths);
|
|
2501
|
-
if ((0,
|
|
2864
|
+
if ((0, import_node_fs17.existsSync)(p)) {
|
|
2502
2865
|
try {
|
|
2503
|
-
(0,
|
|
2866
|
+
(0, import_node_fs17.unlinkSync)(p);
|
|
2504
2867
|
} catch {
|
|
2505
2868
|
}
|
|
2506
2869
|
}
|
|
@@ -2513,11 +2876,11 @@ function pidAlive2(pid) {
|
|
|
2513
2876
|
return false;
|
|
2514
2877
|
}
|
|
2515
2878
|
}
|
|
2516
|
-
var
|
|
2879
|
+
var import_node_fs17, import_node_path12, DEFAULT_CLOUD_BASE_URL, ANSI, PKG_VERSION, RUNTIME_SUBTITLE;
|
|
2517
2880
|
var init_util = __esm({
|
|
2518
2881
|
"src/cli/util.ts"() {
|
|
2519
2882
|
"use strict";
|
|
2520
|
-
|
|
2883
|
+
import_node_fs17 = require("fs");
|
|
2521
2884
|
import_node_path12 = require("path");
|
|
2522
2885
|
init_ui();
|
|
2523
2886
|
DEFAULT_CLOUD_BASE_URL = "https://prismer.cloud";
|
|
@@ -2761,9 +3124,9 @@ var WsClient = class extends import_node_events.EventEmitter {
|
|
|
2761
3124
|
|
|
2762
3125
|
// src/sync/store.ts
|
|
2763
3126
|
var import_better_sqlite32 = __toESM(require("better-sqlite3"), 1);
|
|
2764
|
-
var
|
|
3127
|
+
var import_node_fs4 = require("fs");
|
|
2765
3128
|
var import_node_path4 = require("path");
|
|
2766
|
-
var SCHEMA_VERSION =
|
|
3129
|
+
var SCHEMA_VERSION = 4;
|
|
2767
3130
|
var MIGRATIONS = [
|
|
2768
3131
|
{
|
|
2769
3132
|
version: 1,
|
|
@@ -2889,14 +3252,44 @@ var MIGRATIONS = [
|
|
|
2889
3252
|
CREATE INDEX IF NOT EXISTS idx_asset_meta_seq
|
|
2890
3253
|
ON asset_metadata_index(workspace_id, asset_index_seq);
|
|
2891
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
|
+
`
|
|
2892
3285
|
}
|
|
2893
3286
|
];
|
|
2894
3287
|
function runSql(db, sql) {
|
|
2895
3288
|
db["exec"](sql);
|
|
2896
3289
|
}
|
|
2897
3290
|
function openLocalDb(path9) {
|
|
2898
|
-
if (path9 !== ":memory:" && !(0,
|
|
2899
|
-
(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 });
|
|
2900
3293
|
}
|
|
2901
3294
|
const db = new import_better_sqlite32.default(path9);
|
|
2902
3295
|
db.pragma("journal_mode = WAL");
|
|
@@ -3133,7 +3526,7 @@ function listRoleTemplates() {
|
|
|
3133
3526
|
|
|
3134
3527
|
// src/config.ts
|
|
3135
3528
|
var TOML = __toESM(require("@iarna/toml"), 1);
|
|
3136
|
-
var
|
|
3529
|
+
var import_node_fs5 = require("fs");
|
|
3137
3530
|
var import_node_os3 = require("os");
|
|
3138
3531
|
var import_node_path5 = require("path");
|
|
3139
3532
|
var import_zod3 = require("zod");
|
|
@@ -3176,15 +3569,15 @@ function resolvePaths(home) {
|
|
|
3176
3569
|
};
|
|
3177
3570
|
}
|
|
3178
3571
|
function configExists(paths = resolvePaths()) {
|
|
3179
|
-
return (0,
|
|
3572
|
+
return (0, import_node_fs5.existsSync)(paths.configFile);
|
|
3180
3573
|
}
|
|
3181
3574
|
function loadConfig(paths = resolvePaths()) {
|
|
3182
|
-
if (!(0,
|
|
3575
|
+
if (!(0, import_node_fs5.existsSync)(paths.configFile)) {
|
|
3183
3576
|
throw new Error(
|
|
3184
3577
|
`Config not found at ${paths.configFile}. Run \`prismer setup\` to create it.`
|
|
3185
3578
|
);
|
|
3186
3579
|
}
|
|
3187
|
-
const raw = (0,
|
|
3580
|
+
const raw = (0, import_node_fs5.readFileSync)(paths.configFile, "utf8");
|
|
3188
3581
|
const parsed = TOML.parse(raw);
|
|
3189
3582
|
const merged = {
|
|
3190
3583
|
...parsed,
|
|
@@ -3199,14 +3592,14 @@ function loadConfig(paths = resolvePaths()) {
|
|
|
3199
3592
|
return result.data;
|
|
3200
3593
|
}
|
|
3201
3594
|
function saveConfig(config, paths = resolvePaths()) {
|
|
3202
|
-
if (!(0,
|
|
3203
|
-
(0,
|
|
3595
|
+
if (!(0, import_node_fs5.existsSync)(paths.root)) {
|
|
3596
|
+
(0, import_node_fs5.mkdirSync)(paths.root, { recursive: true });
|
|
3204
3597
|
}
|
|
3205
|
-
if (!(0,
|
|
3206
|
-
(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 });
|
|
3207
3600
|
}
|
|
3208
3601
|
ConfigSchema.parse(config);
|
|
3209
|
-
(0,
|
|
3602
|
+
(0, import_node_fs5.writeFileSync)(paths.configFile, TOML.stringify(config), "utf8");
|
|
3210
3603
|
}
|
|
3211
3604
|
function deriveWsUrl(httpBase) {
|
|
3212
3605
|
const u = new URL(httpBase);
|
|
@@ -3379,7 +3772,7 @@ function envelope(type, payload, requestId) {
|
|
|
3379
3772
|
// src/asset-cache.ts
|
|
3380
3773
|
var import_node_crypto2 = require("crypto");
|
|
3381
3774
|
var import_node_dns = require("dns");
|
|
3382
|
-
var
|
|
3775
|
+
var import_node_fs6 = require("fs");
|
|
3383
3776
|
var import_node_net = require("net");
|
|
3384
3777
|
var import_node_path6 = require("path");
|
|
3385
3778
|
var import_undici = require("undici");
|
|
@@ -3404,8 +3797,8 @@ var AssetCache = class {
|
|
|
3404
3797
|
this.cloud = opts.cloud;
|
|
3405
3798
|
this.cacheDir = opts.cacheDir;
|
|
3406
3799
|
this.maxBytes = opts.maxBytes ?? 5 * 1024 * 1024 * 1024;
|
|
3407
|
-
if (!(0,
|
|
3408
|
-
(0,
|
|
3800
|
+
if (!(0, import_node_fs6.existsSync)(this.cacheDir)) {
|
|
3801
|
+
(0, import_node_fs6.mkdirSync)(this.cacheDir, { recursive: true });
|
|
3409
3802
|
}
|
|
3410
3803
|
}
|
|
3411
3804
|
/** Local file path for a content hash. Files split by first 2 chars for fs scaling. */
|
|
@@ -3415,7 +3808,7 @@ var AssetCache = class {
|
|
|
3415
3808
|
get(hash) {
|
|
3416
3809
|
const row = this.db.prepare("SELECT * FROM cached_assets WHERE content_hash = ?").get(hash);
|
|
3417
3810
|
if (!row) return void 0;
|
|
3418
|
-
if (!(0,
|
|
3811
|
+
if (!(0, import_node_fs6.existsSync)(row.local_path)) {
|
|
3419
3812
|
this.db.prepare("DELETE FROM cached_assets WHERE content_hash = ?").run(hash);
|
|
3420
3813
|
return void 0;
|
|
3421
3814
|
}
|
|
@@ -3479,12 +3872,12 @@ var AssetCache = class {
|
|
|
3479
3872
|
throw new Error(`Asset hash mismatch for ${hash}: downloaded ${actualHash}`);
|
|
3480
3873
|
}
|
|
3481
3874
|
const localPath = this.pathFor(hash);
|
|
3482
|
-
if (!(0,
|
|
3483
|
-
(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 });
|
|
3484
3877
|
}
|
|
3485
3878
|
const tmpPath = `${localPath}.${process.pid}.${Date.now()}.tmp`;
|
|
3486
|
-
(0,
|
|
3487
|
-
(0,
|
|
3879
|
+
(0, import_node_fs6.writeFileSync)(tmpPath, buf);
|
|
3880
|
+
(0, import_node_fs6.renameSync)(tmpPath, localPath);
|
|
3488
3881
|
const mime = res.headers.get("content-type");
|
|
3489
3882
|
const now = Date.now();
|
|
3490
3883
|
this.db.prepare(
|
|
@@ -3543,12 +3936,12 @@ var AssetCache = class {
|
|
|
3543
3936
|
return { cached: existing, finalUrl, durationMs: Date.now() - started };
|
|
3544
3937
|
}
|
|
3545
3938
|
const localPath = this.pathFor(hash);
|
|
3546
|
-
if (!(0,
|
|
3547
|
-
(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 });
|
|
3548
3941
|
}
|
|
3549
3942
|
const tmpPath = `${localPath}.${process.pid}.${Date.now()}.tmp`;
|
|
3550
|
-
(0,
|
|
3551
|
-
(0,
|
|
3943
|
+
(0, import_node_fs6.writeFileSync)(tmpPath, body);
|
|
3944
|
+
(0, import_node_fs6.renameSync)(tmpPath, localPath);
|
|
3552
3945
|
const now = Date.now();
|
|
3553
3946
|
this.db.prepare(
|
|
3554
3947
|
`INSERT OR REPLACE INTO cached_assets
|
|
@@ -3576,7 +3969,7 @@ var AssetCache = class {
|
|
|
3576
3969
|
if (actualHash !== hash) {
|
|
3577
3970
|
throw new Error(`Asset hash mismatch for ${hash}: local file is ${actualHash}`);
|
|
3578
3971
|
}
|
|
3579
|
-
const size = (0,
|
|
3972
|
+
const size = (0, import_node_fs6.statSync)(localPath).size;
|
|
3580
3973
|
const now = Date.now();
|
|
3581
3974
|
this.db.prepare(
|
|
3582
3975
|
`INSERT OR REPLACE INTO cached_assets
|
|
@@ -3604,7 +3997,7 @@ var AssetCache = class {
|
|
|
3604
3997
|
for (const row of candidates) {
|
|
3605
3998
|
if (total <= this.maxBytes) break;
|
|
3606
3999
|
try {
|
|
3607
|
-
if ((0,
|
|
4000
|
+
if ((0, import_node_fs6.existsSync)(row.local_path)) (0, import_node_fs6.unlinkSync)(row.local_path);
|
|
3608
4001
|
} catch {
|
|
3609
4002
|
}
|
|
3610
4003
|
this.db.prepare("DELETE FROM cached_assets WHERE content_hash = ?").run(row.content_hash);
|
|
@@ -3619,7 +4012,7 @@ function sha256(buf) {
|
|
|
3619
4012
|
return (0, import_node_crypto2.createHash)("sha256").update(buf).digest("hex");
|
|
3620
4013
|
}
|
|
3621
4014
|
function readFileBuffer(path9) {
|
|
3622
|
-
return (0,
|
|
4015
|
+
return (0, import_node_fs6.readFileSync)(path9);
|
|
3623
4016
|
}
|
|
3624
4017
|
var DEFAULT_URL_FETCH_MAX_BYTES = 5 * 1024 * 1024;
|
|
3625
4018
|
var DEFAULT_URL_FETCH_TIMEOUT_MS = 15e3;
|
|
@@ -3801,7 +4194,7 @@ async function fetchUrlWithGuards(rawUrl, opts) {
|
|
|
3801
4194
|
}
|
|
3802
4195
|
|
|
3803
4196
|
// src/daemon/asset/mirror.ts
|
|
3804
|
-
var
|
|
4197
|
+
var import_node_fs7 = require("fs");
|
|
3805
4198
|
var import_node_path7 = require("path");
|
|
3806
4199
|
function rowToBinding(row) {
|
|
3807
4200
|
return {
|
|
@@ -3823,16 +4216,16 @@ var WorkspaceMirror = class {
|
|
|
3823
4216
|
this.db = opts.db;
|
|
3824
4217
|
this.cloud = opts.cloud;
|
|
3825
4218
|
this.workspaceId = opts.workspaceId;
|
|
3826
|
-
if (!(0,
|
|
3827
|
-
(0,
|
|
4219
|
+
if (!(0, import_node_fs7.existsSync)(opts.workspaceStateDir)) {
|
|
4220
|
+
(0, import_node_fs7.mkdirSync)(opts.workspaceStateDir, { recursive: true });
|
|
3828
4221
|
}
|
|
3829
4222
|
this.cursorPath = (0, import_node_path7.join)(opts.workspaceStateDir, "asset-cursor.json");
|
|
3830
4223
|
}
|
|
3831
4224
|
/** Persisted cursor for this workspace, or null on first run / corrupted file. */
|
|
3832
4225
|
readCursor() {
|
|
3833
|
-
if (!(0,
|
|
4226
|
+
if (!(0, import_node_fs7.existsSync)(this.cursorPath)) return null;
|
|
3834
4227
|
try {
|
|
3835
|
-
const parsed = JSON.parse((0,
|
|
4228
|
+
const parsed = JSON.parse((0, import_node_fs7.readFileSync)(this.cursorPath, "utf8"));
|
|
3836
4229
|
if (parsed.workspaceId !== this.workspaceId) return null;
|
|
3837
4230
|
return parsed.cursor;
|
|
3838
4231
|
} catch {
|
|
@@ -3845,7 +4238,7 @@ var WorkspaceMirror = class {
|
|
|
3845
4238
|
cursor,
|
|
3846
4239
|
writtenAt: Date.now()
|
|
3847
4240
|
};
|
|
3848
|
-
(0,
|
|
4241
|
+
(0, import_node_fs7.writeFileSync)(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
3849
4242
|
}
|
|
3850
4243
|
/**
|
|
3851
4244
|
* Pull workspace_file changes since the persisted cursor and upsert into
|
|
@@ -4251,9 +4644,265 @@ function extractHttpUrls(text) {
|
|
|
4251
4644
|
}
|
|
4252
4645
|
|
|
4253
4646
|
// src/daemon/dispatch.ts
|
|
4254
|
-
var
|
|
4647
|
+
var import_node_fs9 = require("fs");
|
|
4255
4648
|
var path = __toESM(require("path"), 1);
|
|
4256
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
|
|
4257
4906
|
var DEFAULT_CONTEXT_MAX = 8e3;
|
|
4258
4907
|
var GOAL_CONTEXT_MAX = 4;
|
|
4259
4908
|
var MEMORY_DIGEST_MAX_BYTES = 4e3;
|
|
@@ -4269,12 +4918,14 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4269
4918
|
const { taskId, agentImUserId } = payload;
|
|
4270
4919
|
let resolvedHashes = [];
|
|
4271
4920
|
let reply;
|
|
4921
|
+
let heartbeatRef;
|
|
4922
|
+
let recorderRef;
|
|
4272
4923
|
const outboxBase = deps.paths?.runsDir ? path.join(deps.paths.runsDir, taskId) : null;
|
|
4273
4924
|
const outboxDir = outboxBase ? path.join(outboxBase, "_outbox") : null;
|
|
4274
4925
|
const workDir = outboxBase ? path.join(outboxBase, "workdir") : null;
|
|
4275
4926
|
if (outboxDir) {
|
|
4276
4927
|
try {
|
|
4277
|
-
await
|
|
4928
|
+
await import_node_fs9.promises.mkdir(outboxDir, { recursive: true });
|
|
4278
4929
|
} catch (err) {
|
|
4279
4930
|
process.stderr.write(
|
|
4280
4931
|
`[daemon] outbox provision failed task=${taskId} dir=${outboxDir}: ${err.message}
|
|
@@ -4284,7 +4935,7 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4284
4935
|
}
|
|
4285
4936
|
if (workDir) {
|
|
4286
4937
|
try {
|
|
4287
|
-
await
|
|
4938
|
+
await import_node_fs9.promises.mkdir(workDir, { recursive: true });
|
|
4288
4939
|
} catch (err) {
|
|
4289
4940
|
process.stderr.write(
|
|
4290
4941
|
`[daemon] workdir provision failed task=${taskId} dir=${workDir}: ${err.message}
|
|
@@ -4350,7 +5001,13 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4350
5001
|
resolvedHashes.push(...r.resolvedHashes);
|
|
4351
5002
|
rewrittenContext = payload.context.map((e, i) => ({ ...e, content: r.texts[i] }));
|
|
4352
5003
|
}
|
|
4353
|
-
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
|
+
});
|
|
4354
5011
|
resolvedHashes.push(...assetResolution.pinnedHashes);
|
|
4355
5012
|
const basePrompt = composePrompt(
|
|
4356
5013
|
rewrittenPrompt.text,
|
|
@@ -4385,9 +5042,44 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4385
5042
|
}
|
|
4386
5043
|
const operatingPrinciples = resolveOperatingPrinciples(profile.config);
|
|
4387
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);
|
|
4388
5058
|
const taskInput = {
|
|
4389
5059
|
taskId,
|
|
4390
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 } : {},
|
|
4391
5083
|
metadata: {
|
|
4392
5084
|
...payload.metadata,
|
|
4393
5085
|
conversationId: payload.conversationId,
|
|
@@ -4450,6 +5142,9 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4450
5142
|
}
|
|
4451
5143
|
}
|
|
4452
5144
|
const reaperAborted = deps.signal?.aborted && result.error?.code === "task_cancelled";
|
|
5145
|
+
const approvalRequested = Boolean(
|
|
5146
|
+
result.metadata?.approvalRequested
|
|
5147
|
+
);
|
|
4453
5148
|
let collectedAssetIds = [];
|
|
4454
5149
|
if (deps.outboxWatcher && outboxDir) {
|
|
4455
5150
|
try {
|
|
@@ -4461,14 +5156,23 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4461
5156
|
collectedAssetIds = deps.outboxWatcher.flushPending(taskId);
|
|
4462
5157
|
}
|
|
4463
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;
|
|
4464
5166
|
reply = {
|
|
4465
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.
|
|
4466
5173
|
ok: result.ok,
|
|
4467
5174
|
output: result.output,
|
|
4468
|
-
error:
|
|
4469
|
-
code: "daemon_task_timeout",
|
|
4470
|
-
message: `Daemon-side reaper aborted after ${reaperLimitMs}ms inactivity (no progress events from adapter)`
|
|
4471
|
-
} : result.error,
|
|
5175
|
+
error: finalError,
|
|
4472
5176
|
...collectedAssetIds.length > 0 ? { assetIds: collectedAssetIds } : {},
|
|
4473
5177
|
metrics: result.metrics,
|
|
4474
5178
|
...assetResolution.observability.length > 0 || urlObservations.length > 0 ? { assetObservability: [...assetResolution.observability, ...urlObservations] } : {}
|
|
@@ -4490,6 +5194,18 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
4490
5194
|
}
|
|
4491
5195
|
};
|
|
4492
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
|
+
}
|
|
4493
5209
|
for (const hash of new Set(resolvedHashes)) {
|
|
4494
5210
|
try {
|
|
4495
5211
|
deps.assetCache.unpin(hash);
|
|
@@ -4566,6 +5282,10 @@ function isTextLikeMime(mime) {
|
|
|
4566
5282
|
if (m.endsWith("+json") || m.endsWith("+xml") || m.endsWith("+csv")) return true;
|
|
4567
5283
|
return false;
|
|
4568
5284
|
}
|
|
5285
|
+
function isImageMime(mime) {
|
|
5286
|
+
if (!mime) return false;
|
|
5287
|
+
return mime.toLowerCase().split(";")[0].trim().startsWith("image/");
|
|
5288
|
+
}
|
|
4569
5289
|
var HASH_REF_RE = /(?:^|\s)#([^\s#]+)/g;
|
|
4570
5290
|
var HEX_COLOR_RE = /^[0-9a-fA-F]{3,8}$/;
|
|
4571
5291
|
var FILE_EXT_RE = /\.[a-zA-Z0-9]{1,10}$/;
|
|
@@ -4644,8 +5364,34 @@ async function resolveHashRefs(prompt, assetIndex, cloud) {
|
|
|
4644
5364
|
}
|
|
4645
5365
|
return { text: result, resolutions };
|
|
4646
5366
|
}
|
|
4647
|
-
|
|
4648
|
-
|
|
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
|
+
};
|
|
4649
5395
|
if (!refs || refs.length === 0) return out;
|
|
4650
5396
|
for (const ref of refs) {
|
|
4651
5397
|
let cached;
|
|
@@ -4674,7 +5420,7 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
4674
5420
|
const inlineCandidate = isTextLikeMime(effectiveMime);
|
|
4675
5421
|
if (inlineCandidate) {
|
|
4676
5422
|
try {
|
|
4677
|
-
const buf = (0,
|
|
5423
|
+
const buf = (0, import_node_fs9.readFileSync)(cached.localPath);
|
|
4678
5424
|
let strategy;
|
|
4679
5425
|
let body;
|
|
4680
5426
|
if (buf.byteLength <= ASSET_INLINE_FULL_MAX_BYTES) {
|
|
@@ -4710,7 +5456,42 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
4710
5456
|
continue;
|
|
4711
5457
|
}
|
|
4712
5458
|
}
|
|
4713
|
-
|
|
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
|
+
}
|
|
4714
5495
|
out.observability.push({
|
|
4715
5496
|
assetId: ref.assetId,
|
|
4716
5497
|
contentHash: ref.contentHash,
|
|
@@ -4721,6 +5502,110 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
4721
5502
|
}
|
|
4722
5503
|
return out;
|
|
4723
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
|
+
}
|
|
4724
5609
|
function logAssetResolveFailure(taskId, ref, error, mime = ref.mime) {
|
|
4725
5610
|
const message = error.replace(/\s+/g, " ").slice(0, 500);
|
|
4726
5611
|
process.stderr.write(
|
|
@@ -4735,8 +5620,19 @@ function formatInlineAssetBlock(ref, mime, body, strategy) {
|
|
|
4735
5620
|
${body}
|
|
4736
5621
|
---`;
|
|
4737
5622
|
}
|
|
4738
|
-
function
|
|
4739
|
-
|
|
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)}`;
|
|
4740
5636
|
}
|
|
4741
5637
|
function formatErrorAssetBlock(ref, mime, error) {
|
|
4742
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.`;
|
|
@@ -5219,8 +6115,9 @@ var ServicePool = class {
|
|
|
5219
6115
|
// src/daemon/local-server.ts
|
|
5220
6116
|
var import_node_http = require("http");
|
|
5221
6117
|
var import_node_crypto4 = require("crypto");
|
|
5222
|
-
var
|
|
6118
|
+
var import_node_fs10 = require("fs");
|
|
5223
6119
|
var path2 = __toESM(require("path"), 1);
|
|
6120
|
+
var import_node_os5 = require("os");
|
|
5224
6121
|
var LocalServer = class {
|
|
5225
6122
|
constructor(opts) {
|
|
5226
6123
|
this.opts = opts;
|
|
@@ -5332,6 +6229,11 @@ var LocalServer = class {
|
|
|
5332
6229
|
void this.handleSnapshot(req, res);
|
|
5333
6230
|
return;
|
|
5334
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
|
+
}
|
|
5335
6237
|
if (req.method === "POST" && url === "/local/asset/write") {
|
|
5336
6238
|
void this.handleAssetWrite(req, res);
|
|
5337
6239
|
return;
|
|
@@ -5382,7 +6284,7 @@ var LocalServer = class {
|
|
|
5382
6284
|
async handleSnapshot(req, res) {
|
|
5383
6285
|
const root = this.opts.snapshotRoot ?? "/workspace";
|
|
5384
6286
|
try {
|
|
5385
|
-
const st = await
|
|
6287
|
+
const st = await import_node_fs10.promises.stat(root);
|
|
5386
6288
|
if (!st.isDirectory()) {
|
|
5387
6289
|
respond(res, 400, { error: "snapshot_root_not_directory", rootPath: root });
|
|
5388
6290
|
return;
|
|
@@ -5409,6 +6311,54 @@ var LocalServer = class {
|
|
|
5409
6311
|
}
|
|
5410
6312
|
void req;
|
|
5411
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
|
+
}
|
|
5412
6362
|
/**
|
|
5413
6363
|
* POST /local/asset/write — agent-gen adapter RPC.
|
|
5414
6364
|
*
|
|
@@ -5680,11 +6630,44 @@ function validateAgentDispatchRequest(body) {
|
|
|
5680
6630
|
if (payload.attachments != null && !Array.isArray(payload.attachments)) return "attachments must be an array";
|
|
5681
6631
|
return null;
|
|
5682
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
|
+
}
|
|
5683
6666
|
async function walkAndDigest(root, current) {
|
|
5684
6667
|
const out = [];
|
|
5685
6668
|
let entries;
|
|
5686
6669
|
try {
|
|
5687
|
-
entries = await
|
|
6670
|
+
entries = await import_node_fs10.promises.readdir(current);
|
|
5688
6671
|
} catch (err) {
|
|
5689
6672
|
if (err.code === "ENOENT") return out;
|
|
5690
6673
|
throw err;
|
|
@@ -5695,7 +6678,7 @@ async function walkAndDigest(root, current) {
|
|
|
5695
6678
|
const rel = path2.relative(root, full);
|
|
5696
6679
|
let st;
|
|
5697
6680
|
try {
|
|
5698
|
-
st = await
|
|
6681
|
+
st = await import_node_fs10.promises.stat(full);
|
|
5699
6682
|
} catch {
|
|
5700
6683
|
continue;
|
|
5701
6684
|
}
|
|
@@ -5706,7 +6689,7 @@ async function walkAndDigest(root, current) {
|
|
|
5706
6689
|
continue;
|
|
5707
6690
|
}
|
|
5708
6691
|
if (!st.isFile()) continue;
|
|
5709
|
-
const buf = await
|
|
6692
|
+
const buf = await import_node_fs10.promises.readFile(full);
|
|
5710
6693
|
const sha2563 = (0, import_node_crypto4.createHash)("sha256").update(buf).digest("hex");
|
|
5711
6694
|
out.push({
|
|
5712
6695
|
path: rel,
|
|
@@ -5721,9 +6704,9 @@ async function walkAndDigest(root, current) {
|
|
|
5721
6704
|
// src/daemon/runner.ts
|
|
5722
6705
|
var import_node_events3 = require("events");
|
|
5723
6706
|
var import_node_module2 = require("module");
|
|
5724
|
-
var
|
|
6707
|
+
var import_node_fs16 = require("fs");
|
|
5725
6708
|
var import_promises = require("fs/promises");
|
|
5726
|
-
var
|
|
6709
|
+
var import_node_os8 = require("os");
|
|
5727
6710
|
var import_node_path11 = require("path");
|
|
5728
6711
|
|
|
5729
6712
|
// src/adapters/claude-code/index.ts
|
|
@@ -7821,7 +8804,7 @@ function parsePrismerUri(uri) {
|
|
|
7821
8804
|
}
|
|
7822
8805
|
|
|
7823
8806
|
// src/daemon/asset/metadata-index.ts
|
|
7824
|
-
var
|
|
8807
|
+
var import_node_fs11 = require("fs");
|
|
7825
8808
|
var import_node_path9 = require("path");
|
|
7826
8809
|
var DEFAULT_LIMIT = 8;
|
|
7827
8810
|
var PULL_PAGE_SIZE = 500;
|
|
@@ -7850,16 +8833,16 @@ var AssetMetadataIndex = class {
|
|
|
7850
8833
|
this.db = opts.db;
|
|
7851
8834
|
this.cloud = opts.cloud;
|
|
7852
8835
|
this.workspaceId = opts.workspaceId;
|
|
7853
|
-
if (!(0,
|
|
7854
|
-
(0,
|
|
8836
|
+
if (!(0, import_node_fs11.existsSync)(opts.workspaceStateDir)) {
|
|
8837
|
+
(0, import_node_fs11.mkdirSync)(opts.workspaceStateDir, { recursive: true });
|
|
7855
8838
|
}
|
|
7856
8839
|
this.cursorPath = (0, import_node_path9.join)(opts.workspaceStateDir, "asset-metadata-cursor.json");
|
|
7857
8840
|
}
|
|
7858
8841
|
/** Persisted cursor for this workspace, or 0 on first run / corrupted file. */
|
|
7859
8842
|
readCursor() {
|
|
7860
|
-
if (!(0,
|
|
8843
|
+
if (!(0, import_node_fs11.existsSync)(this.cursorPath)) return 0;
|
|
7861
8844
|
try {
|
|
7862
|
-
const parsed = JSON.parse((0,
|
|
8845
|
+
const parsed = JSON.parse((0, import_node_fs11.readFileSync)(this.cursorPath, "utf8"));
|
|
7863
8846
|
if (parsed.workspaceId !== this.workspaceId) return 0;
|
|
7864
8847
|
return parsed.cursor;
|
|
7865
8848
|
} catch {
|
|
@@ -7872,7 +8855,7 @@ var AssetMetadataIndex = class {
|
|
|
7872
8855
|
cursor,
|
|
7873
8856
|
writtenAt: Date.now()
|
|
7874
8857
|
};
|
|
7875
|
-
(0,
|
|
8858
|
+
(0, import_node_fs11.writeFileSync)(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
7876
8859
|
}
|
|
7877
8860
|
/**
|
|
7878
8861
|
* Pull incremental asset metadata changes since the persisted cursor and
|
|
@@ -7989,7 +8972,7 @@ var AssetMetadataIndex = class {
|
|
|
7989
8972
|
};
|
|
7990
8973
|
|
|
7991
8974
|
// src/daemon/asset/rpc.ts
|
|
7992
|
-
var
|
|
8975
|
+
var import_node_fs12 = require("fs");
|
|
7993
8976
|
var ASSET_PATH_PREFIX = "/local/asset/";
|
|
7994
8977
|
var MAX_READ_BYTES = 256 * 1024;
|
|
7995
8978
|
var MAX_SEARCH_BYTES = 1024 * 1024;
|
|
@@ -8164,16 +9147,16 @@ function isTextLike(row) {
|
|
|
8164
9147
|
);
|
|
8165
9148
|
}
|
|
8166
9149
|
function readFileRange(path9, offset, length) {
|
|
8167
|
-
const sizeBytes = (0,
|
|
9150
|
+
const sizeBytes = (0, import_node_fs12.statSync)(path9).size;
|
|
8168
9151
|
if (offset >= sizeBytes) return { buffer: Buffer.alloc(0), bytesRead: 0, sizeBytes };
|
|
8169
|
-
const fd = (0,
|
|
9152
|
+
const fd = (0, import_node_fs12.openSync)(path9, "r");
|
|
8170
9153
|
try {
|
|
8171
9154
|
const target = Math.min(length, sizeBytes - offset);
|
|
8172
9155
|
const buffer = Buffer.alloc(target);
|
|
8173
|
-
const bytesRead = (0,
|
|
9156
|
+
const bytesRead = (0, import_node_fs12.readSync)(fd, buffer, 0, target, offset);
|
|
8174
9157
|
return { buffer: buffer.subarray(0, bytesRead), bytesRead, sizeBytes };
|
|
8175
9158
|
} finally {
|
|
8176
|
-
(0,
|
|
9159
|
+
(0, import_node_fs12.closeSync)(fd);
|
|
8177
9160
|
}
|
|
8178
9161
|
}
|
|
8179
9162
|
function respond3(res, status, body) {
|
|
@@ -8198,8 +9181,8 @@ async function readJson3(req) {
|
|
|
8198
9181
|
}
|
|
8199
9182
|
|
|
8200
9183
|
// src/daemon/asset/origin/drop-folder.ts
|
|
8201
|
-
var
|
|
8202
|
-
var
|
|
9184
|
+
var import_node_fs13 = require("fs");
|
|
9185
|
+
var import_node_os6 = require("os");
|
|
8203
9186
|
var path5 = __toESM(require("path"), 1);
|
|
8204
9187
|
var DropFolderAdapter = class {
|
|
8205
9188
|
kind = "drop-folder";
|
|
@@ -8207,7 +9190,7 @@ var DropFolderAdapter = class {
|
|
|
8207
9190
|
rootDir;
|
|
8208
9191
|
constructor(opts = {}) {
|
|
8209
9192
|
this.workspaceDir = opts.workspaceDir;
|
|
8210
|
-
this.rootDir = opts.rootDir ?? path5.join(opts.homeDir ?? (0,
|
|
9193
|
+
this.rootDir = opts.rootDir ?? path5.join(opts.homeDir ?? (0, import_node_os6.homedir)(), ".prismer");
|
|
8211
9194
|
}
|
|
8212
9195
|
/**
|
|
8213
9196
|
* Long-lived async iterable wrapping `scanOnce` on a 1s polling interval.
|
|
@@ -8217,7 +9200,7 @@ var DropFolderAdapter = class {
|
|
|
8217
9200
|
while (true) {
|
|
8218
9201
|
const batch = await this.scanOnce(workspaceId);
|
|
8219
9202
|
for (const obs of batch) yield obs;
|
|
8220
|
-
await
|
|
9203
|
+
await sleep2(1e3);
|
|
8221
9204
|
}
|
|
8222
9205
|
}
|
|
8223
9206
|
/**
|
|
@@ -8231,7 +9214,7 @@ var DropFolderAdapter = class {
|
|
|
8231
9214
|
const drop = this.dropDir(workspaceId);
|
|
8232
9215
|
let entries = [];
|
|
8233
9216
|
try {
|
|
8234
|
-
entries = await
|
|
9217
|
+
entries = await import_node_fs13.promises.readdir(drop);
|
|
8235
9218
|
} catch (err) {
|
|
8236
9219
|
if (err.code === "ENOENT") return [];
|
|
8237
9220
|
throw err;
|
|
@@ -8240,12 +9223,12 @@ var DropFolderAdapter = class {
|
|
|
8240
9223
|
for (const name of entries) {
|
|
8241
9224
|
if (name.startsWith(".")) continue;
|
|
8242
9225
|
const fullPath = path5.join(drop, name);
|
|
8243
|
-
const stat = await
|
|
9226
|
+
const stat = await import_node_fs13.promises.lstat(fullPath).catch(() => null);
|
|
8244
9227
|
if (!stat) continue;
|
|
8245
9228
|
if (stat.isSymbolicLink()) continue;
|
|
8246
9229
|
if (stat.isDirectory()) {
|
|
8247
9230
|
for (const nested of await walk(fullPath)) {
|
|
8248
|
-
const nstat = await
|
|
9231
|
+
const nstat = await import_node_fs13.promises.lstat(nested).catch(() => null);
|
|
8249
9232
|
if (!nstat || !nstat.isFile()) continue;
|
|
8250
9233
|
out.push(makeObservation(workspaceId, nested, drop, nstat.size, Math.floor(nstat.mtimeMs)));
|
|
8251
9234
|
}
|
|
@@ -8270,7 +9253,7 @@ var DropFolderAdapter = class {
|
|
|
8270
9253
|
}
|
|
8271
9254
|
async fetch(obs) {
|
|
8272
9255
|
const detail = obs.detail;
|
|
8273
|
-
const bytes = await
|
|
9256
|
+
const bytes = await import_node_fs13.promises.readFile(detail.path);
|
|
8274
9257
|
return {
|
|
8275
9258
|
bytes,
|
|
8276
9259
|
mime: mimeFromExtension(detail.path),
|
|
@@ -8292,8 +9275,8 @@ var DropFolderAdapter = class {
|
|
|
8292
9275
|
const rel = typeof detail.watchDir === "string" ? path5.relative(detail.watchDir, detail.path) : path5.basename(detail.path);
|
|
8293
9276
|
const safeRel = rel && !rel.startsWith("..") && !path5.isAbsolute(rel) ? rel : path5.basename(detail.path);
|
|
8294
9277
|
const destPath = path5.join(destDir, safeRel);
|
|
8295
|
-
await
|
|
8296
|
-
await
|
|
9278
|
+
await import_node_fs13.promises.mkdir(path5.dirname(destPath), { recursive: true });
|
|
9279
|
+
await import_node_fs13.promises.rename(detail.path, destPath);
|
|
8297
9280
|
} catch (err) {
|
|
8298
9281
|
if (err.code === "ENOENT") return;
|
|
8299
9282
|
throw err;
|
|
@@ -8321,11 +9304,11 @@ function makeObservation(workspaceId, filePath, watchDir, size, mtime) {
|
|
|
8321
9304
|
}
|
|
8322
9305
|
async function walk(root) {
|
|
8323
9306
|
const out = [];
|
|
8324
|
-
const entries = await
|
|
9307
|
+
const entries = await import_node_fs13.promises.readdir(root).catch(() => []);
|
|
8325
9308
|
for (const name of entries) {
|
|
8326
9309
|
if (name.startsWith(".")) continue;
|
|
8327
9310
|
const full = path5.join(root, name);
|
|
8328
|
-
const stat = await
|
|
9311
|
+
const stat = await import_node_fs13.promises.lstat(full).catch(() => null);
|
|
8329
9312
|
if (!stat) continue;
|
|
8330
9313
|
if (stat.isSymbolicLink()) continue;
|
|
8331
9314
|
if (stat.isDirectory()) {
|
|
@@ -8366,7 +9349,7 @@ function mimeFromExtension(p) {
|
|
|
8366
9349
|
const ext = path5.extname(p).toLowerCase();
|
|
8367
9350
|
return MIME_BY_EXT[ext] ?? null;
|
|
8368
9351
|
}
|
|
8369
|
-
function
|
|
9352
|
+
function sleep2(ms) {
|
|
8370
9353
|
return new Promise((r) => setTimeout(r, ms));
|
|
8371
9354
|
}
|
|
8372
9355
|
|
|
@@ -8921,7 +9904,7 @@ function stringValue(obj, key) {
|
|
|
8921
9904
|
}
|
|
8922
9905
|
|
|
8923
9906
|
// src/daemon/outbox-watcher.ts
|
|
8924
|
-
var
|
|
9907
|
+
var import_node_fs14 = require("fs");
|
|
8925
9908
|
var path7 = __toESM(require("path"), 1);
|
|
8926
9909
|
|
|
8927
9910
|
// src/daemon/asset/agent-output-policy.ts
|
|
@@ -9077,9 +10060,120 @@ function extensionOf(filename) {
|
|
|
9077
10060
|
return idx >= 0 ? name.slice(idx) : "";
|
|
9078
10061
|
}
|
|
9079
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
|
+
|
|
9080
10173
|
// src/daemon/outbox-watcher.ts
|
|
9081
10174
|
var DEFAULT_INTERVAL_MS = 2e3;
|
|
9082
10175
|
var RESERVED_SUBDIR = "_uploaded";
|
|
10176
|
+
var REJECTED_SUBDIR = "_rejected";
|
|
9083
10177
|
var OutboxWatcher = class {
|
|
9084
10178
|
constructor(opts) {
|
|
9085
10179
|
this.opts = opts;
|
|
@@ -9244,7 +10338,7 @@ var OutboxWatcher = class {
|
|
|
9244
10338
|
async scanDir(dir, kind, task) {
|
|
9245
10339
|
let entries = [];
|
|
9246
10340
|
try {
|
|
9247
|
-
entries = await
|
|
10341
|
+
entries = await import_node_fs14.promises.readdir(dir);
|
|
9248
10342
|
} catch (err) {
|
|
9249
10343
|
const code = err.code;
|
|
9250
10344
|
if (code === "ENOENT") return;
|
|
@@ -9254,10 +10348,11 @@ var OutboxWatcher = class {
|
|
|
9254
10348
|
for (const name of entries) {
|
|
9255
10349
|
if (name.startsWith(".")) continue;
|
|
9256
10350
|
if (name === RESERVED_SUBDIR) continue;
|
|
10351
|
+
if (name === REJECTED_SUBDIR) continue;
|
|
9257
10352
|
const full = path7.join(dir, name);
|
|
9258
10353
|
let st;
|
|
9259
10354
|
try {
|
|
9260
|
-
st = await
|
|
10355
|
+
st = await import_node_fs14.promises.stat(full);
|
|
9261
10356
|
} catch {
|
|
9262
10357
|
continue;
|
|
9263
10358
|
}
|
|
@@ -9268,10 +10363,77 @@ var OutboxWatcher = class {
|
|
|
9268
10363
|
await this.upload(full, st, kind, task);
|
|
9269
10364
|
this.uploaded.add(key);
|
|
9270
10365
|
} catch (err) {
|
|
9271
|
-
|
|
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
|
+
}
|
|
9272
10377
|
}
|
|
9273
10378
|
}
|
|
9274
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
|
+
}
|
|
9275
10437
|
async upload(filePath, st, kind, task) {
|
|
9276
10438
|
const wsId = this.opts.workspaceId();
|
|
9277
10439
|
if (!wsId) {
|
|
@@ -9282,13 +10444,18 @@ var OutboxWatcher = class {
|
|
|
9282
10444
|
this.log("warn", `skip ${filePath}: no active taskId (no dispatch/handoff received yet)`);
|
|
9283
10445
|
return;
|
|
9284
10446
|
}
|
|
9285
|
-
const bytes = await
|
|
10447
|
+
const bytes = await import_node_fs14.promises.readFile(filePath);
|
|
9286
10448
|
const fileName = path7.basename(filePath);
|
|
9287
10449
|
const inferredMime = inferAgentOutputMime(fileName);
|
|
9288
10450
|
const policy = validateAgentOutputAsset({ filename: fileName, mime: inferredMime, sizeBytes: bytes.length });
|
|
9289
10451
|
if (!policy.ok) {
|
|
9290
10452
|
throw new Error(`agent output rejected: ${policy.reason}`);
|
|
9291
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
|
+
}
|
|
9292
10459
|
const metadata = {
|
|
9293
10460
|
taskId: task.taskId,
|
|
9294
10461
|
...task.adapter ? { adapter: task.adapter } : {}
|
|
@@ -9324,7 +10491,7 @@ var OutboxWatcher = class {
|
|
|
9324
10491
|
|
|
9325
10492
|
// src/daemon/shell-executor.ts
|
|
9326
10493
|
var import_node_child_process4 = require("child_process");
|
|
9327
|
-
var
|
|
10494
|
+
var import_node_fs15 = require("fs");
|
|
9328
10495
|
var import_node_path10 = require("path");
|
|
9329
10496
|
var DEFAULT_OUTPUT_LIMIT = 256 * 1024;
|
|
9330
10497
|
var DEFAULT_TIMEOUT = 6e4;
|
|
@@ -9361,7 +10528,7 @@ async function executeShellDispatch(payload, deps) {
|
|
|
9361
10528
|
const command = readCommand(payload, execution);
|
|
9362
10529
|
if (!command.trim()) return fail(payload.taskId, "shell_command_required", "Shell command is required");
|
|
9363
10530
|
const cwd = resolveCwd(execution.cwd, deps.config.defaultCwd);
|
|
9364
|
-
if (!(0,
|
|
10531
|
+
if (!(0, import_node_fs15.existsSync)(cwd)) return fail(payload.taskId, "shell_cwd_missing", `cwd does not exist: ${cwd}`);
|
|
9365
10532
|
const timeoutMs = Math.min(
|
|
9366
10533
|
typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? payload.timeoutMs : deps.config.maxTimeoutMs,
|
|
9367
10534
|
deps.config.maxTimeoutMs
|
|
@@ -9492,6 +10659,352 @@ ${input.stderr}` : null
|
|
|
9492
10659
|
|
|
9493
10660
|
// src/daemon/runner.ts
|
|
9494
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
|
|
9495
11008
|
var import_meta2 = {};
|
|
9496
11009
|
var DEFAULT_LOCAL_PORT = 3210;
|
|
9497
11010
|
var log7 = createLogger("Daemon");
|
|
@@ -9540,6 +11053,14 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
9540
11053
|
// F16 (2026-05-20) — periodic skill resync timer (default 10min).
|
|
9541
11054
|
skillResyncTimer;
|
|
9542
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;
|
|
9543
11064
|
async start() {
|
|
9544
11065
|
if (this.state !== "idle") throw new Error(`Runner already in state ${this.state}`);
|
|
9545
11066
|
this.state = "starting";
|
|
@@ -9554,6 +11075,7 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
9554
11075
|
process.env.PRISMER_API_KEY = this.config.api_key;
|
|
9555
11076
|
this.db = openLocalDb(this.paths.localDb);
|
|
9556
11077
|
this.cloud = new CloudClient({ baseUrl: this.config.cloud_api_base, apiKey: this.config.api_key });
|
|
11078
|
+
this.pendingReplyCache = createPendingReplyCache(this.db);
|
|
9557
11079
|
this.assetCache = new AssetCache({
|
|
9558
11080
|
db: this.db,
|
|
9559
11081
|
cloud: this.cloud,
|
|
@@ -9880,19 +11402,19 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
9880
11402
|
sampleCgroupResources() {
|
|
9881
11403
|
const baseline = { cpu: { usagePct: 0 }, mem: { usedBytes: 0, limitBytes: 0 } };
|
|
9882
11404
|
try {
|
|
9883
|
-
if (!(0,
|
|
11405
|
+
if (!(0, import_node_fs16.existsSync)("/sys/fs/cgroup/memory.current")) {
|
|
9884
11406
|
return baseline;
|
|
9885
11407
|
}
|
|
9886
|
-
const memUsedRaw = (0,
|
|
11408
|
+
const memUsedRaw = (0, import_node_fs16.readFileSync)("/sys/fs/cgroup/memory.current", "utf-8").trim();
|
|
9887
11409
|
const memUsed = Number.parseInt(memUsedRaw, 10);
|
|
9888
11410
|
let memLimit = 0;
|
|
9889
|
-
if ((0,
|
|
9890
|
-
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();
|
|
9891
11413
|
memLimit = limitRaw === "max" ? 0 : Number.parseInt(limitRaw, 10) || 0;
|
|
9892
11414
|
}
|
|
9893
11415
|
let cpuUsagePct = 0;
|
|
9894
|
-
if ((0,
|
|
9895
|
-
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");
|
|
9896
11418
|
const usecLine = statLines.find((line) => line.startsWith("usage_usec "));
|
|
9897
11419
|
if (usecLine) {
|
|
9898
11420
|
const usec = Number.parseInt(usecLine.split(" ")[1] ?? "0", 10);
|
|
@@ -10070,10 +11592,10 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10070
11592
|
const rawJson = process.env.PRISMER_HOSTED_AGENT_JSON;
|
|
10071
11593
|
let raw;
|
|
10072
11594
|
if (rawFile) {
|
|
10073
|
-
if (!(0,
|
|
11595
|
+
if (!(0, import_node_fs16.existsSync)(rawFile)) {
|
|
10074
11596
|
throw new Error(`PRISMER_HOSTED_AGENT_FILE not found: ${rawFile}`);
|
|
10075
11597
|
}
|
|
10076
|
-
raw = (0,
|
|
11598
|
+
raw = (0, import_node_fs16.readFileSync)(rawFile, "utf8");
|
|
10077
11599
|
} else if (rawJson) {
|
|
10078
11600
|
raw = rawJson;
|
|
10079
11601
|
}
|
|
@@ -10160,6 +11682,19 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10160
11682
|
process.stdout.write(`[daemon] ws authenticated, sending agent.host.declare (${this.hostedAgents.size} agents)
|
|
10161
11683
|
`);
|
|
10162
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
|
+
}
|
|
10163
11698
|
return;
|
|
10164
11699
|
}
|
|
10165
11700
|
this.handleIncoming(msg);
|
|
@@ -10169,7 +11704,7 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10169
11704
|
const payload = {
|
|
10170
11705
|
daemonId: this.config.daemon_id,
|
|
10171
11706
|
daemonVersion: this.opts.daemonVersion ?? "0.0.0",
|
|
10172
|
-
platform: (0,
|
|
11707
|
+
platform: (0, import_node_os8.platform)() === "win32" ? "win32" : (0, import_node_os8.platform)() === "linux" ? "linux" : "darwin",
|
|
10173
11708
|
agents: Array.from(this.hostedAgents.values()).map((a) => ({
|
|
10174
11709
|
imUserId: a.imUserId,
|
|
10175
11710
|
name: a.name,
|
|
@@ -10206,12 +11741,90 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10206
11741
|
case "asset.changed":
|
|
10207
11742
|
void this.onAssetChanged(msg.payload);
|
|
10208
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;
|
|
10209
11753
|
default:
|
|
10210
11754
|
this.emit("unknown-message", msg);
|
|
10211
11755
|
}
|
|
10212
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
|
+
}
|
|
10213
11820
|
async onHostAcked(payload) {
|
|
10214
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
|
+
});
|
|
10215
11828
|
if (this.memoryWiring && this.workspaceId) {
|
|
10216
11829
|
syncMemoryFromCloud(this.memoryWiring, this.cloud, [this.workspaceId]).catch(
|
|
10217
11830
|
(err) => log7.error("Initial memory sync failed", err.message)
|
|
@@ -10393,14 +12006,68 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10393
12006
|
};
|
|
10394
12007
|
}
|
|
10395
12008
|
async postMessageDispatchReply(payload) {
|
|
10396
|
-
const
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
|
|
10401
|
-
|
|
10402
|
-
|
|
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;
|
|
10403
12066
|
}
|
|
12067
|
+
process.stdout.write(
|
|
12068
|
+
`[daemon] transport-probe reported transport=${probe.transport} gateway=${probe.gatewayUrl ?? "null"} private=${probe.gatewayIsPrivate}
|
|
12069
|
+
`
|
|
12070
|
+
);
|
|
10404
12071
|
}
|
|
10405
12072
|
onAgentChanged(payload) {
|
|
10406
12073
|
const a = this.hostedAgents.get(payload.agentImUserId);
|
|
@@ -10843,9 +12510,9 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
10843
12510
|
function resolveUserAssetWorkspaceDir(workspaceId) {
|
|
10844
12511
|
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
10845
12512
|
if (override) return (0, import_node_path11.join)(override, workspaceId);
|
|
10846
|
-
const home = (0,
|
|
12513
|
+
const home = (0, import_node_os8.homedir)();
|
|
10847
12514
|
const desktop = (0, import_node_path11.join)(home, "Desktop");
|
|
10848
|
-
const base = (0,
|
|
12515
|
+
const base = (0, import_node_fs16.existsSync)(desktop) ? desktop : home;
|
|
10849
12516
|
return (0, import_node_path11.join)(base, "Prismer Assets", workspaceId);
|
|
10850
12517
|
}
|
|
10851
12518
|
function readTargetDaemonId(payload) {
|
|
@@ -10922,7 +12589,7 @@ function safeJsonParse(raw) {
|
|
|
10922
12589
|
|
|
10923
12590
|
// src/pair.ts
|
|
10924
12591
|
var import_node_crypto10 = require("crypto");
|
|
10925
|
-
var
|
|
12592
|
+
var import_node_os9 = require("os");
|
|
10926
12593
|
var import_promises2 = require("timers/promises");
|
|
10927
12594
|
var import_qrcode = __toESM(require("qrcode"), 1);
|
|
10928
12595
|
async function pair(opts) {
|
|
@@ -10952,7 +12619,7 @@ async function pair(opts) {
|
|
|
10952
12619
|
"/api/im/pair/offer",
|
|
10953
12620
|
{
|
|
10954
12621
|
auth: false,
|
|
10955
|
-
body: { devicePub, deviceName: opts.deviceName ?? (0,
|
|
12622
|
+
body: { devicePub, deviceName: opts.deviceName ?? (0, import_node_os9.hostname)() }
|
|
10956
12623
|
}
|
|
10957
12624
|
);
|
|
10958
12625
|
if (!offerRes.ok) {
|
|
@@ -11033,8 +12700,8 @@ var import_commander20 = require("commander");
|
|
|
11033
12700
|
|
|
11034
12701
|
// src/cli/commands/adapter.ts
|
|
11035
12702
|
var import_node_child_process5 = require("child_process");
|
|
11036
|
-
var
|
|
11037
|
-
var
|
|
12703
|
+
var import_node_fs18 = require("fs");
|
|
12704
|
+
var import_node_os10 = require("os");
|
|
11038
12705
|
var import_node_path13 = require("path");
|
|
11039
12706
|
var import_commander = require("commander");
|
|
11040
12707
|
init_hermes();
|
|
@@ -11050,7 +12717,7 @@ var INSTALL_SPECS = {
|
|
|
11050
12717
|
binary: "claude",
|
|
11051
12718
|
hint: "Set ANTHROPIC_API_KEY in your shell profile, or rely on Claude Code OAuth login. Run `claude login` to sign in.",
|
|
11052
12719
|
authHints: ["ANTHROPIC_API_KEY or Claude Code OAuth login (`claude login`)"],
|
|
11053
|
-
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" }
|
|
11054
12721
|
},
|
|
11055
12722
|
openclaw: {
|
|
11056
12723
|
name: "openclaw",
|
|
@@ -11059,7 +12726,7 @@ var INSTALL_SPECS = {
|
|
|
11059
12726
|
binary: "openclaw",
|
|
11060
12727
|
hint: "OpenClaw runs as a gateway. Configure ~/.openclaw/openclaw.json and start it with `openclaw gateway` before tasks dispatch.",
|
|
11061
12728
|
authHints: ["~/.openclaw/openclaw.json gateway.auth.bearerTokens", "Prismer daemon api_key"],
|
|
11062
|
-
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" }
|
|
11063
12730
|
},
|
|
11064
12731
|
codex: {
|
|
11065
12732
|
name: "codex",
|
|
@@ -11068,7 +12735,7 @@ var INSTALL_SPECS = {
|
|
|
11068
12735
|
binary: "codex",
|
|
11069
12736
|
hint: "Set OPENAI_API_KEY in your shell profile. Verify with `codex --help`.",
|
|
11070
12737
|
authHints: ["OPENAI_API_KEY or Codex CLI account login"],
|
|
11071
|
-
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" }
|
|
11072
12739
|
},
|
|
11073
12740
|
hermes: {
|
|
11074
12741
|
name: "hermes",
|
|
@@ -11077,7 +12744,7 @@ var INSTALL_SPECS = {
|
|
|
11077
12744
|
binary: "hermes",
|
|
11078
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.",
|
|
11079
12746
|
authHints: ["~/.hermes/.env API_SERVER_KEY", "Hermes profile provider credentials"],
|
|
11080
|
-
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" }
|
|
11081
12748
|
}
|
|
11082
12749
|
};
|
|
11083
12750
|
function buildAdapterCommand() {
|
|
@@ -11292,15 +12959,15 @@ function runHooks(spec, opts) {
|
|
|
11292
12959
|
}
|
|
11293
12960
|
let backup;
|
|
11294
12961
|
if (planned.kind === "directory") {
|
|
11295
|
-
(0,
|
|
12962
|
+
(0, import_node_fs18.mkdirSync)(planned.path, { recursive: true });
|
|
11296
12963
|
const markerPath = (0, import_node_path13.join)(planned.path, "prismer.json");
|
|
11297
12964
|
backup = backupIfExists(markerPath);
|
|
11298
|
-
(0,
|
|
12965
|
+
(0, import_node_fs18.writeFileSync)(markerPath, JSON.stringify(prismerHookMarker(spec), null, 2) + "\n", "utf8");
|
|
11299
12966
|
} else {
|
|
11300
|
-
(0,
|
|
12967
|
+
(0, import_node_fs18.mkdirSync)((0, import_node_path13.dirname)(planned.path), { recursive: true });
|
|
11301
12968
|
backup = backupIfExists(planned.path);
|
|
11302
12969
|
const merged = mergeHookJson(planned.path, spec);
|
|
11303
|
-
(0,
|
|
12970
|
+
(0, import_node_fs18.writeFileSync)(planned.path, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
11304
12971
|
}
|
|
11305
12972
|
return {
|
|
11306
12973
|
ok: true,
|
|
@@ -11367,23 +13034,23 @@ function inspectAuthEnv(spec) {
|
|
|
11367
13034
|
hints: spec.authHints ?? []
|
|
11368
13035
|
};
|
|
11369
13036
|
case "hermes": {
|
|
11370
|
-
const envFile = (0, import_node_path13.join)((0,
|
|
13037
|
+
const envFile = (0, import_node_path13.join)((0, import_node_os10.homedir)(), ".hermes", ".env");
|
|
11371
13038
|
return {
|
|
11372
|
-
ok: (0,
|
|
13039
|
+
ok: (0, import_node_fs18.existsSync)(envFile) || Boolean(process.env.HERMES_API_KEY || process.env.API_SERVER_KEY),
|
|
11373
13040
|
present: [
|
|
11374
13041
|
...["HERMES_API_KEY", "API_SERVER_KEY"].filter((k) => Boolean(process.env[k])),
|
|
11375
|
-
...(0,
|
|
13042
|
+
...(0, import_node_fs18.existsSync)(envFile) ? [envFile] : []
|
|
11376
13043
|
],
|
|
11377
13044
|
hints: spec.authHints ?? []
|
|
11378
13045
|
};
|
|
11379
13046
|
}
|
|
11380
13047
|
case "openclaw": {
|
|
11381
|
-
const cfgFile = (0, import_node_path13.join)((0,
|
|
13048
|
+
const cfgFile = (0, import_node_path13.join)((0, import_node_os10.homedir)(), ".openclaw", "openclaw.json");
|
|
11382
13049
|
return {
|
|
11383
|
-
ok: (0,
|
|
13050
|
+
ok: (0, import_node_fs18.existsSync)(cfgFile) || Boolean(process.env.OPENCLAW_API_KEY),
|
|
11384
13051
|
present: [
|
|
11385
13052
|
...["OPENCLAW_API_KEY"].filter((k) => Boolean(process.env[k])),
|
|
11386
|
-
...(0,
|
|
13053
|
+
...(0, import_node_fs18.existsSync)(cfgFile) ? [cfgFile] : []
|
|
11387
13054
|
],
|
|
11388
13055
|
hints: spec.authHints ?? []
|
|
11389
13056
|
};
|
|
@@ -11398,16 +13065,16 @@ function inspectHook(spec) {
|
|
|
11398
13065
|
return { supported: false, markerPresent: false };
|
|
11399
13066
|
}
|
|
11400
13067
|
if (spec.name === "openclaw") {
|
|
11401
|
-
const jsonPath = (0, import_node_path13.join)((0,
|
|
13068
|
+
const jsonPath = (0, import_node_path13.join)((0, import_node_os10.homedir)(), ".openclaw", "hooks.json");
|
|
11402
13069
|
const markerPath = (0, import_node_path13.join)(target.path, "prismer.json");
|
|
11403
|
-
const jsonMarkerPresent = (0,
|
|
11404
|
-
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);
|
|
11405
13072
|
return {
|
|
11406
13073
|
supported: true,
|
|
11407
13074
|
kind: "json-file-or-directory",
|
|
11408
13075
|
path: target.path,
|
|
11409
13076
|
jsonPath,
|
|
11410
|
-
exists: (0,
|
|
13077
|
+
exists: (0, import_node_fs18.existsSync)(target.path) || (0, import_node_fs18.existsSync)(jsonPath),
|
|
11411
13078
|
markerPath,
|
|
11412
13079
|
markerPresent: jsonMarkerPresent || dirMarkerPresent
|
|
11413
13080
|
};
|
|
@@ -11418,12 +13085,12 @@ function inspectHook(spec) {
|
|
|
11418
13085
|
supported: true,
|
|
11419
13086
|
kind: target.kind,
|
|
11420
13087
|
path: target.path,
|
|
11421
|
-
exists: (0,
|
|
13088
|
+
exists: (0, import_node_fs18.existsSync)(target.path),
|
|
11422
13089
|
markerPath,
|
|
11423
|
-
markerPresent: (0,
|
|
13090
|
+
markerPresent: (0, import_node_fs18.existsSync)(markerPath) && fileContainsPrismerMarker(markerPath)
|
|
11424
13091
|
};
|
|
11425
13092
|
}
|
|
11426
|
-
const exists = (0,
|
|
13093
|
+
const exists = (0, import_node_fs18.existsSync)(target.path);
|
|
11427
13094
|
if (!exists) {
|
|
11428
13095
|
return {
|
|
11429
13096
|
supported: true,
|
|
@@ -11459,14 +13126,14 @@ function prismerHookMarker(spec) {
|
|
|
11459
13126
|
}
|
|
11460
13127
|
function mergeHookJson(path9, spec) {
|
|
11461
13128
|
const marker = prismerHookMarker(spec);
|
|
11462
|
-
if (!(0,
|
|
13129
|
+
if (!(0, import_node_fs18.existsSync)(path9)) {
|
|
11463
13130
|
return {
|
|
11464
13131
|
prismer: marker
|
|
11465
13132
|
};
|
|
11466
13133
|
}
|
|
11467
13134
|
let parsed;
|
|
11468
13135
|
try {
|
|
11469
|
-
parsed = JSON.parse((0,
|
|
13136
|
+
parsed = JSON.parse((0, import_node_fs18.readFileSync)(path9, "utf8"));
|
|
11470
13137
|
} catch (err) {
|
|
11471
13138
|
throw new Error(`Cannot parse ${path9} as JSON: ${err.message}`);
|
|
11472
13139
|
}
|
|
@@ -11490,17 +13157,17 @@ function mergeHookJson(path9, spec) {
|
|
|
11490
13157
|
return next;
|
|
11491
13158
|
}
|
|
11492
13159
|
function backupIfExists(path9) {
|
|
11493
|
-
if (!(0,
|
|
13160
|
+
if (!(0, import_node_fs18.existsSync)(path9)) return null;
|
|
11494
13161
|
const suffix = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
11495
13162
|
const backup = `${path9}.bak.${suffix}`;
|
|
11496
|
-
const stat = (0,
|
|
13163
|
+
const stat = (0, import_node_fs18.statSync)(path9);
|
|
11497
13164
|
if (stat.isDirectory()) return null;
|
|
11498
|
-
(0,
|
|
13165
|
+
(0, import_node_fs18.copyFileSync)(path9, backup);
|
|
11499
13166
|
return backup;
|
|
11500
13167
|
}
|
|
11501
13168
|
function fileContainsPrismerMarker(path9) {
|
|
11502
13169
|
try {
|
|
11503
|
-
return (0,
|
|
13170
|
+
return (0, import_node_fs18.readFileSync)(path9, "utf8").includes("prismer-daemon-runtime");
|
|
11504
13171
|
} catch {
|
|
11505
13172
|
return false;
|
|
11506
13173
|
}
|
|
@@ -11860,8 +13527,74 @@ function buildAgentCommand() {
|
|
|
11860
13527
|
}
|
|
11861
13528
|
getUI().ok("Skill install requested", `agent=${opts.agent} skill=${slugOrId}`);
|
|
11862
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" }));
|
|
11863
13578
|
return cmd;
|
|
11864
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
|
+
}
|
|
11865
13598
|
function readLocalAgents(localDb) {
|
|
11866
13599
|
const db = openLocalDb(localDb);
|
|
11867
13600
|
try {
|
|
@@ -11966,7 +13699,7 @@ function whichBinary2(bin) {
|
|
|
11966
13699
|
// src/cli/commands/asset.ts
|
|
11967
13700
|
var import_node_crypto11 = require("crypto");
|
|
11968
13701
|
var import_commander3 = require("commander");
|
|
11969
|
-
var
|
|
13702
|
+
var import_node_fs19 = require("fs");
|
|
11970
13703
|
var import_node_path14 = require("path");
|
|
11971
13704
|
init_util();
|
|
11972
13705
|
init_ui();
|
|
@@ -12139,8 +13872,8 @@ async function prefetchAssetBytes(opts) {
|
|
|
12139
13872
|
return { considered: rows.length, fetched, bytes, failed };
|
|
12140
13873
|
}
|
|
12141
13874
|
async function uploadAssetPath(inputPath, opts) {
|
|
12142
|
-
if (!(0,
|
|
12143
|
-
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);
|
|
12144
13877
|
if (stat.isSymbolicLink()) exitWithError(`refusing to upload symlink: ${inputPath}`);
|
|
12145
13878
|
if (stat.isFile()) {
|
|
12146
13879
|
return uploadAsset(inputPath, { ...opts, folderPath: normalizeFolderPath(opts.folderPath) });
|
|
@@ -12158,9 +13891,9 @@ async function uploadAssetPath(inputPath, opts) {
|
|
|
12158
13891
|
return { ok: true, data: { count: items.length, items } };
|
|
12159
13892
|
}
|
|
12160
13893
|
async function uploadAsset(file, opts) {
|
|
12161
|
-
if (!(0,
|
|
13894
|
+
if (!(0, import_node_fs19.existsSync)(file)) exitWithError(`file not found: ${file}`);
|
|
12162
13895
|
const cfg = loadConfig(resolvePaths());
|
|
12163
|
-
const bytes = (0,
|
|
13896
|
+
const bytes = (0, import_node_fs19.readFileSync)(file);
|
|
12164
13897
|
const contentHash = (0, import_node_crypto11.createHash)("sha256").update(bytes).digest("hex");
|
|
12165
13898
|
const metadata = parseMetadata(opts.metadata);
|
|
12166
13899
|
if (opts.taskId && metadata.taskId === void 0) metadata.taskId = opts.taskId;
|
|
@@ -12296,10 +14029,10 @@ async function putDirectAssetBytes(plan, bytes, mime) {
|
|
|
12296
14029
|
}
|
|
12297
14030
|
function listFilesRecursive(root) {
|
|
12298
14031
|
const out = [];
|
|
12299
|
-
for (const name of (0,
|
|
14032
|
+
for (const name of (0, import_node_fs19.readdirSync)(root)) {
|
|
12300
14033
|
if (name.startsWith(".")) continue;
|
|
12301
14034
|
const full = (0, import_node_path14.join)(root, name);
|
|
12302
|
-
const stat = (0,
|
|
14035
|
+
const stat = (0, import_node_fs19.lstatSync)(full);
|
|
12303
14036
|
if (stat.isSymbolicLink()) continue;
|
|
12304
14037
|
if (stat.isDirectory()) {
|
|
12305
14038
|
out.push(...listFilesRecursive(full));
|
|
@@ -12333,7 +14066,7 @@ async function downloadAsset(assetId, outPath) {
|
|
|
12333
14066
|
exitWithError(`asset download failed (${res.status}): ${errorMessage(body)}`);
|
|
12334
14067
|
}
|
|
12335
14068
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
12336
|
-
(0,
|
|
14069
|
+
(0, import_node_fs19.writeFileSync)(outPath, bytes);
|
|
12337
14070
|
}
|
|
12338
14071
|
function parseMetadata(raw) {
|
|
12339
14072
|
if (!raw) return {};
|
|
@@ -12732,7 +14465,7 @@ function buildCookbookCommand() {
|
|
|
12732
14465
|
for (const suite of suites) {
|
|
12733
14466
|
checks.push(...await runSuite(suite, cloud, opts));
|
|
12734
14467
|
}
|
|
12735
|
-
const summary =
|
|
14468
|
+
const summary = summarize2(checks, Boolean(opts.strict));
|
|
12736
14469
|
if (opts.json) {
|
|
12737
14470
|
printJson(summary);
|
|
12738
14471
|
} else {
|
|
@@ -12897,7 +14630,7 @@ function parseSuites(raw) {
|
|
|
12897
14630
|
const expanded = selected.includes("all") ? ["status", "im", "task", "group", "asset", "sandbox"] : selected;
|
|
12898
14631
|
return [...new Set(expanded)];
|
|
12899
14632
|
}
|
|
12900
|
-
function
|
|
14633
|
+
function summarize2(checks, strict) {
|
|
12901
14634
|
const totals = { pass: 0, fail: 0, skip: 0 };
|
|
12902
14635
|
for (const check of checks) totals[check.status] += 1;
|
|
12903
14636
|
return {
|
|
@@ -12980,7 +14713,7 @@ function parsePositiveInt3(value) {
|
|
|
12980
14713
|
// src/cli/commands/daemon.ts
|
|
12981
14714
|
var import_commander8 = require("commander");
|
|
12982
14715
|
var import_node_child_process7 = require("child_process");
|
|
12983
|
-
var
|
|
14716
|
+
var import_node_fs20 = require("fs");
|
|
12984
14717
|
var import_promises3 = require("timers/promises");
|
|
12985
14718
|
var import_node_path15 = require("path");
|
|
12986
14719
|
init_util();
|
|
@@ -13056,9 +14789,9 @@ function buildDaemonCommand() {
|
|
|
13056
14789
|
if (existingPid && pidAlive2(existingPid)) {
|
|
13057
14790
|
exitWithError(`Daemon already running (pid ${existingPid}). Use \`prismer daemon stop\` first.`);
|
|
13058
14791
|
}
|
|
13059
|
-
if (!(0,
|
|
14792
|
+
if (!(0, import_node_fs20.existsSync)(paths.logsDir)) (0, import_node_fs20.mkdirSync)(paths.logsDir, { recursive: true });
|
|
13060
14793
|
const logFile = (0, import_node_path15.join)(paths.logsDir, "daemon.log");
|
|
13061
|
-
const fd = (0,
|
|
14794
|
+
const fd = (0, import_node_fs20.openSync)(logFile, "a");
|
|
13062
14795
|
const args = [process.argv[1], "daemon", "run"];
|
|
13063
14796
|
if (opts.port) args.push("--port", String(opts.port));
|
|
13064
14797
|
if (opts.localServer === false) args.push("--no-local-server");
|
|
@@ -13142,7 +14875,7 @@ function buildDaemonCommand() {
|
|
|
13142
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) => {
|
|
13143
14876
|
const paths = resolvePaths();
|
|
13144
14877
|
const logFile = (0, import_node_path15.join)(paths.logsDir, "daemon.log");
|
|
13145
|
-
if (!(0,
|
|
14878
|
+
if (!(0, import_node_fs20.existsSync)(logFile)) {
|
|
13146
14879
|
exitWithError(`No daemon log found at ${logFile}. Start the daemon with \`prismer daemon start\`.`);
|
|
13147
14880
|
}
|
|
13148
14881
|
const lines = Math.max(1, opts.tail);
|
|
@@ -13183,13 +14916,13 @@ async function tailFromEnd(path9, lines) {
|
|
|
13183
14916
|
}
|
|
13184
14917
|
}
|
|
13185
14918
|
async function followFile(path9) {
|
|
13186
|
-
let offset = (0,
|
|
14919
|
+
let offset = (0, import_node_fs20.statSync)(path9).size;
|
|
13187
14920
|
for (; ; ) {
|
|
13188
14921
|
await (0, import_promises3.setTimeout)(1e3);
|
|
13189
|
-
const size = (0,
|
|
14922
|
+
const size = (0, import_node_fs20.statSync)(path9).size;
|
|
13190
14923
|
if (size < offset) offset = 0;
|
|
13191
14924
|
if (size === offset) continue;
|
|
13192
|
-
const stream = (0,
|
|
14925
|
+
const stream = (0, import_node_fs20.createReadStream)(path9, { start: offset, end: size - 1, encoding: "utf8" });
|
|
13193
14926
|
for await (const chunk of stream) process.stdout.write(chunk);
|
|
13194
14927
|
offset = size;
|
|
13195
14928
|
}
|
|
@@ -13197,8 +14930,8 @@ async function followFile(path9) {
|
|
|
13197
14930
|
|
|
13198
14931
|
// src/cli/commands/events.ts
|
|
13199
14932
|
var import_commander9 = require("commander");
|
|
13200
|
-
var
|
|
13201
|
-
var
|
|
14933
|
+
var import_node_fs21 = require("fs");
|
|
14934
|
+
var import_node_os11 = require("os");
|
|
13202
14935
|
var import_node_path16 = require("path");
|
|
13203
14936
|
var import_node_readline = require("readline");
|
|
13204
14937
|
init_util();
|
|
@@ -13206,7 +14939,7 @@ var DEFAULT_LIMIT2 = 50;
|
|
|
13206
14939
|
function buildEventsCommand() {
|
|
13207
14940
|
return addEventOptions(new import_commander9.Command("events").description("Read local PARA events from ~/.prismer/para/events.jsonl")).action(async (opts) => {
|
|
13208
14941
|
const file = eventsPath();
|
|
13209
|
-
if (!(0,
|
|
14942
|
+
if (!(0, import_node_fs21.existsSync)(file)) {
|
|
13210
14943
|
printJson(unavailable(file));
|
|
13211
14944
|
process.exitCode = 1;
|
|
13212
14945
|
return;
|
|
@@ -13223,7 +14956,7 @@ function buildEventsCommand() {
|
|
|
13223
14956
|
function buildEventsStatsCommand() {
|
|
13224
14957
|
return addEventOptions(new import_commander9.Command("events:stats").description("Summarize local PARA events from ~/.prismer/para/events.jsonl")).action(async (opts) => {
|
|
13225
14958
|
const file = eventsPath();
|
|
13226
|
-
if (!(0,
|
|
14959
|
+
if (!(0, import_node_fs21.existsSync)(file)) {
|
|
13227
14960
|
printJson(unavailable(file));
|
|
13228
14961
|
process.exitCode = 1;
|
|
13229
14962
|
return;
|
|
@@ -13232,7 +14965,7 @@ function buildEventsStatsCommand() {
|
|
|
13232
14965
|
const events = await readEvents(file, filters);
|
|
13233
14966
|
printJson({
|
|
13234
14967
|
ok: true,
|
|
13235
|
-
data: { stats:
|
|
14968
|
+
data: { stats: summarize3(events), count: events.length, limit: filters.limit },
|
|
13236
14969
|
checks: { file, exists: true, filters: cleanFilters(filters) }
|
|
13237
14970
|
});
|
|
13238
14971
|
});
|
|
@@ -13241,7 +14974,7 @@ function addEventOptions(cmd) {
|
|
|
13241
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)");
|
|
13242
14975
|
}
|
|
13243
14976
|
function eventsPath() {
|
|
13244
|
-
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");
|
|
13245
14978
|
}
|
|
13246
14979
|
function unavailable(file) {
|
|
13247
14980
|
return {
|
|
@@ -13257,7 +14990,7 @@ function unavailable(file) {
|
|
|
13257
14990
|
async function readEvents(file, filters) {
|
|
13258
14991
|
const out = [];
|
|
13259
14992
|
const rl = (0, import_node_readline.createInterface)({
|
|
13260
|
-
input: (0,
|
|
14993
|
+
input: (0, import_node_fs21.createReadStream)(file, { encoding: "utf8" }),
|
|
13261
14994
|
crlfDelay: Infinity
|
|
13262
14995
|
});
|
|
13263
14996
|
for await (const line of rl) {
|
|
@@ -13289,7 +15022,7 @@ function field(event, names) {
|
|
|
13289
15022
|
}
|
|
13290
15023
|
return void 0;
|
|
13291
15024
|
}
|
|
13292
|
-
function
|
|
15025
|
+
function summarize3(events) {
|
|
13293
15026
|
const byFamily = {};
|
|
13294
15027
|
const byType = {};
|
|
13295
15028
|
const byAgentId = {};
|
|
@@ -13327,7 +15060,7 @@ function parsePositiveInt4(v) {
|
|
|
13327
15060
|
// src/cli/commands/memory.ts
|
|
13328
15061
|
var import_better_sqlite35 = __toESM(require("better-sqlite3"), 1);
|
|
13329
15062
|
var import_commander10 = require("commander");
|
|
13330
|
-
var
|
|
15063
|
+
var import_node_fs22 = require("fs");
|
|
13331
15064
|
init_util();
|
|
13332
15065
|
var LOCAL_BASE = process.env.PRISMER_DAEMON_URL ?? "http://127.0.0.1:3210";
|
|
13333
15066
|
function buildMemoryCommand() {
|
|
@@ -13479,7 +15212,7 @@ function readCacheSnapshot(limit) {
|
|
|
13479
15212
|
const paths = resolvePaths();
|
|
13480
15213
|
const empty = {
|
|
13481
15214
|
dbPath: paths.localDb,
|
|
13482
|
-
dbExists: (0,
|
|
15215
|
+
dbExists: (0, import_node_fs22.existsSync)(paths.localDb),
|
|
13483
15216
|
tables: {
|
|
13484
15217
|
cached_assets: { exists: false, count: 0, sizeBytes: 0 },
|
|
13485
15218
|
workspace_files_mirror: { exists: false, count: 0 }
|
|
@@ -13643,8 +15376,8 @@ function buildPairCommand() {
|
|
|
13643
15376
|
|
|
13644
15377
|
// src/cli/commands/profile.ts
|
|
13645
15378
|
var import_commander12 = require("commander");
|
|
13646
|
-
var
|
|
13647
|
-
var
|
|
15379
|
+
var import_node_fs23 = require("fs");
|
|
15380
|
+
var import_node_os12 = require("os");
|
|
13648
15381
|
var import_node_path17 = require("path");
|
|
13649
15382
|
var import_node_child_process8 = require("child_process");
|
|
13650
15383
|
init_util();
|
|
@@ -13693,12 +15426,12 @@ function buildProfileCommand() {
|
|
|
13693
15426
|
const profile = await cloud.get(
|
|
13694
15427
|
`/api/im/agent_profiles/${encodeURIComponent(profileId)}`
|
|
13695
15428
|
);
|
|
13696
|
-
const tmpFile = (0, import_node_path17.join)((0,
|
|
13697
|
-
(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");
|
|
13698
15431
|
const editor = process.env.EDITOR || "vi";
|
|
13699
15432
|
const ed = (0, import_node_child_process8.spawnSync)(editor, [tmpFile], { stdio: "inherit" });
|
|
13700
15433
|
if (ed.status !== 0) exitWithError(`editor exited ${ed.status}`, { code: "editor_failed" });
|
|
13701
|
-
const newConfig = JSON.parse((0,
|
|
15434
|
+
const newConfig = JSON.parse((0, import_node_fs23.readFileSync)(tmpFile, "utf8"));
|
|
13702
15435
|
const res = await cloud.request("PATCH", `/api/im/agent_profiles/${encodeURIComponent(profileId)}`, {
|
|
13703
15436
|
body: { config: newConfig, version: profile.version }
|
|
13704
15437
|
});
|
|
@@ -13720,8 +15453,8 @@ function mkCloud4() {
|
|
|
13720
15453
|
function readJsonArg(arg) {
|
|
13721
15454
|
if (arg.startsWith("@")) {
|
|
13722
15455
|
const path9 = arg.slice(1);
|
|
13723
|
-
if (!(0,
|
|
13724
|
-
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"));
|
|
13725
15458
|
}
|
|
13726
15459
|
return JSON.parse(arg);
|
|
13727
15460
|
}
|
|
@@ -13735,8 +15468,8 @@ async function resolveDefaultWorkspaceId(cloud) {
|
|
|
13735
15468
|
// src/cli/commands/reset.ts
|
|
13736
15469
|
var import_commander13 = require("commander");
|
|
13737
15470
|
var import_node_child_process9 = require("child_process");
|
|
13738
|
-
var
|
|
13739
|
-
var
|
|
15471
|
+
var import_node_fs24 = require("fs");
|
|
15472
|
+
var import_node_os13 = require("os");
|
|
13740
15473
|
var import_node_path18 = require("path");
|
|
13741
15474
|
var import_promises4 = require("timers/promises");
|
|
13742
15475
|
init_util();
|
|
@@ -13754,15 +15487,15 @@ function buildResetCommand() {
|
|
|
13754
15487
|
const hermesGatewayPids = findHermesGatewayPids2();
|
|
13755
15488
|
const plan = {
|
|
13756
15489
|
root: paths.root,
|
|
13757
|
-
exists: (0,
|
|
15490
|
+
exists: (0, import_node_fs24.existsSync)(paths.root),
|
|
13758
15491
|
runningPid,
|
|
13759
15492
|
mode: opts.wipe ? "wipe" : "archive",
|
|
13760
15493
|
archivePath,
|
|
13761
15494
|
assetSyncRoot,
|
|
13762
|
-
assetSyncExists: (0,
|
|
15495
|
+
assetSyncExists: (0, import_node_fs24.existsSync)(assetSyncRoot),
|
|
13763
15496
|
assetSyncArchivePath,
|
|
13764
15497
|
hermesHome,
|
|
13765
|
-
hermesExists: (0,
|
|
15498
|
+
hermesExists: (0, import_node_fs24.existsSync)(hermesHome),
|
|
13766
15499
|
hermesArchivePath,
|
|
13767
15500
|
hermesGatewayPids,
|
|
13768
15501
|
willStopDaemon: runningPid !== null,
|
|
@@ -13786,40 +15519,40 @@ function buildResetCommand() {
|
|
|
13786
15519
|
for (const gatewayPid of hermesGatewayPids) {
|
|
13787
15520
|
await stopProcess(gatewayPid, opts.timeout ?? 5e3, "Hermes gateway");
|
|
13788
15521
|
}
|
|
13789
|
-
if ((0,
|
|
15522
|
+
if ((0, import_node_fs24.existsSync)(paths.root)) {
|
|
13790
15523
|
if (opts.wipe) {
|
|
13791
|
-
(0,
|
|
15524
|
+
(0, import_node_fs24.rmSync)(paths.root, { recursive: true, force: true });
|
|
13792
15525
|
} else {
|
|
13793
15526
|
if (!archivePath) throw new Error("internal: archivePath missing");
|
|
13794
|
-
if (!(0,
|
|
13795
|
-
(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 });
|
|
13796
15529
|
}
|
|
13797
|
-
(0,
|
|
15530
|
+
(0, import_node_fs24.renameSync)(paths.root, archivePath);
|
|
13798
15531
|
}
|
|
13799
15532
|
}
|
|
13800
|
-
if ((0,
|
|
15533
|
+
if ((0, import_node_fs24.existsSync)(assetSyncRoot)) {
|
|
13801
15534
|
if (opts.wipe) {
|
|
13802
|
-
(0,
|
|
15535
|
+
(0, import_node_fs24.rmSync)(assetSyncRoot, { recursive: true, force: true });
|
|
13803
15536
|
} else {
|
|
13804
15537
|
if (!assetSyncArchivePath) throw new Error("internal: assetSyncArchivePath missing");
|
|
13805
|
-
if (!(0,
|
|
13806
|
-
(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 });
|
|
13807
15540
|
}
|
|
13808
|
-
(0,
|
|
15541
|
+
(0, import_node_fs24.renameSync)(assetSyncRoot, assetSyncArchivePath);
|
|
13809
15542
|
}
|
|
13810
15543
|
}
|
|
13811
|
-
if ((0,
|
|
15544
|
+
if ((0, import_node_fs24.existsSync)(hermesHome)) {
|
|
13812
15545
|
if (opts.wipe) {
|
|
13813
|
-
(0,
|
|
15546
|
+
(0, import_node_fs24.rmSync)(hermesHome, { recursive: true, force: true });
|
|
13814
15547
|
} else {
|
|
13815
15548
|
if (!hermesArchivePath) throw new Error("internal: hermesArchivePath missing");
|
|
13816
|
-
if (!(0,
|
|
13817
|
-
(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 });
|
|
13818
15551
|
}
|
|
13819
|
-
(0,
|
|
15552
|
+
(0, import_node_fs24.renameSync)(hermesHome, hermesArchivePath);
|
|
13820
15553
|
}
|
|
13821
15554
|
}
|
|
13822
|
-
(0,
|
|
15555
|
+
(0, import_node_fs24.mkdirSync)(paths.root, { recursive: true });
|
|
13823
15556
|
if (opts.json) {
|
|
13824
15557
|
printJson({ ok: true, reset: true, plan });
|
|
13825
15558
|
return;
|
|
@@ -13888,13 +15621,13 @@ function timestamp() {
|
|
|
13888
15621
|
function resolveAssetSyncRoot() {
|
|
13889
15622
|
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
13890
15623
|
if (override) return override;
|
|
13891
|
-
const home = (0,
|
|
15624
|
+
const home = (0, import_node_os13.homedir)();
|
|
13892
15625
|
const desktop = (0, import_node_path18.join)(home, "Desktop");
|
|
13893
|
-
const base = (0,
|
|
15626
|
+
const base = (0, import_node_fs24.existsSync)(desktop) ? desktop : home;
|
|
13894
15627
|
return (0, import_node_path18.join)(base, "Prismer Assets");
|
|
13895
15628
|
}
|
|
13896
15629
|
function resolveHermesHome() {
|
|
13897
|
-
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");
|
|
13898
15631
|
}
|
|
13899
15632
|
function findHermesGatewayPids2() {
|
|
13900
15633
|
try {
|
|
@@ -14124,10 +15857,10 @@ async function safeParseResponse(res) {
|
|
|
14124
15857
|
|
|
14125
15858
|
// src/cli/commands/setup.ts
|
|
14126
15859
|
var import_commander15 = require("commander");
|
|
14127
|
-
var
|
|
15860
|
+
var import_node_os14 = require("os");
|
|
14128
15861
|
var import_node_child_process10 = require("child_process");
|
|
14129
15862
|
var import_node_crypto12 = require("crypto");
|
|
14130
|
-
var
|
|
15863
|
+
var import_node_fs25 = require("fs");
|
|
14131
15864
|
var import_node_http2 = require("http");
|
|
14132
15865
|
init_util();
|
|
14133
15866
|
init_ui();
|
|
@@ -14169,7 +15902,7 @@ function buildSetupCommand() {
|
|
|
14169
15902
|
}
|
|
14170
15903
|
const result = await pair({
|
|
14171
15904
|
cloudBaseUrl,
|
|
14172
|
-
deviceName: opts.deviceName ?? (0,
|
|
15905
|
+
deviceName: opts.deviceName ?? (0, import_node_os14.hostname)(),
|
|
14173
15906
|
force: opts.force,
|
|
14174
15907
|
paths,
|
|
14175
15908
|
asUserEmail: opts.asUser
|
|
@@ -14186,13 +15919,13 @@ function buildSetupCommand() {
|
|
|
14186
15919
|
apiKey = await mintDaemonApiKey({
|
|
14187
15920
|
cloudBaseUrl,
|
|
14188
15921
|
token: authToken,
|
|
14189
|
-
deviceName: opts.deviceName ?? (0,
|
|
15922
|
+
deviceName: opts.deviceName ?? (0, import_node_os14.hostname)()
|
|
14190
15923
|
});
|
|
14191
15924
|
}
|
|
14192
15925
|
if (!apiKey && !authToken && !opts.check && opts.browser !== false) {
|
|
14193
15926
|
apiKey = await runBrowserSetup({
|
|
14194
15927
|
cloudBaseUrl,
|
|
14195
|
-
deviceName: opts.deviceName ?? (0,
|
|
15928
|
+
deviceName: opts.deviceName ?? (0, import_node_os14.hostname)(),
|
|
14196
15929
|
json: Boolean(opts.json)
|
|
14197
15930
|
});
|
|
14198
15931
|
}
|
|
@@ -14378,9 +16111,9 @@ function shouldArchiveLocalDb(previous, next) {
|
|
|
14378
16111
|
return previous.api_key !== next.api_key || previous.cloud_api_base !== next.cloud_api_base || previous.daemon_id !== next.daemon_id;
|
|
14379
16112
|
}
|
|
14380
16113
|
function archiveLocalDb(localDbPath) {
|
|
14381
|
-
if (!(0,
|
|
16114
|
+
if (!(0, import_node_fs25.existsSync)(localDbPath)) return;
|
|
14382
16115
|
const archived = `${localDbPath}.${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
|
|
14383
|
-
(0,
|
|
16116
|
+
(0, import_node_fs25.renameSync)(localDbPath, archived);
|
|
14384
16117
|
}
|
|
14385
16118
|
async function mintDaemonApiKey(input) {
|
|
14386
16119
|
const label = `Daemon: ${input.deviceName} ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
|
|
@@ -14449,7 +16182,7 @@ function buildSkillCommand() {
|
|
|
14449
16182
|
const cloud = mkCloud6();
|
|
14450
16183
|
const path9 = opts.agent ? `/api/im/agents/${encodeURIComponent(opts.agent)}/skills` : `/api/im/skills/${encodeURIComponent(slugOrId)}/install`;
|
|
14451
16184
|
const body = opts.agent ? { skillId: slugOrId, workspaceId: opts.workspaceId, version: opts.version } : { workspaceId: opts.workspaceId, version: opts.version };
|
|
14452
|
-
const data = await
|
|
16185
|
+
const data = await requestEnvelope2(cloud, "POST", path9, body, "skill_install_failed");
|
|
14453
16186
|
if (opts.json) {
|
|
14454
16187
|
printJson(data);
|
|
14455
16188
|
return;
|
|
@@ -14460,7 +16193,7 @@ function buildSkillCommand() {
|
|
|
14460
16193
|
const cloud = mkCloud6();
|
|
14461
16194
|
const path9 = opts.agent ? `/api/im/agents/${encodeURIComponent(opts.agent)}/skills` : `/api/im/skills/${encodeURIComponent(slugOrId)}/install`;
|
|
14462
16195
|
const body = opts.agent ? { skillId: slugOrId, workspaceId: opts.workspaceId } : void 0;
|
|
14463
|
-
const data = await
|
|
16196
|
+
const data = await requestEnvelope2(cloud, "DELETE", path9, body, "skill_uninstall_failed");
|
|
14464
16197
|
if (opts.json) {
|
|
14465
16198
|
printJson(data);
|
|
14466
16199
|
return;
|
|
@@ -14513,7 +16246,7 @@ function mkCloud6() {
|
|
|
14513
16246
|
const cfg = loadConfig(resolvePaths());
|
|
14514
16247
|
return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
14515
16248
|
}
|
|
14516
|
-
async function
|
|
16249
|
+
async function requestEnvelope2(cloud, method, path9, body, code) {
|
|
14517
16250
|
const res = await cloud.request(method, path9, {
|
|
14518
16251
|
body
|
|
14519
16252
|
});
|
|
@@ -14632,7 +16365,7 @@ function printSkill(skill, includeContent) {
|
|
|
14632
16365
|
|
|
14633
16366
|
// src/cli/commands/status.ts
|
|
14634
16367
|
var import_commander17 = require("commander");
|
|
14635
|
-
var
|
|
16368
|
+
var import_node_os15 = require("os");
|
|
14636
16369
|
init_util();
|
|
14637
16370
|
init_ui();
|
|
14638
16371
|
function buildStatusCommand() {
|
|
@@ -14663,6 +16396,7 @@ function buildStatusCommand() {
|
|
|
14663
16396
|
let me = null;
|
|
14664
16397
|
let devices = null;
|
|
14665
16398
|
let agents = null;
|
|
16399
|
+
let diagnose = null;
|
|
14666
16400
|
try {
|
|
14667
16401
|
const meRes = await cloud.request("GET", "/api/im/me", { timeoutMs: 3e3 });
|
|
14668
16402
|
cloudOk = meRes.ok;
|
|
@@ -14675,19 +16409,33 @@ function buildStatusCommand() {
|
|
|
14675
16409
|
if (Array.isArray(wsList) && wsList.length > 0) {
|
|
14676
16410
|
const wsId = wsList[0]?.id;
|
|
14677
16411
|
if (wsId) {
|
|
14678
|
-
const
|
|
14679
|
-
if (
|
|
14680
|
-
const
|
|
14681
|
-
|
|
14682
|
-
|
|
14683
|
-
|
|
14684
|
-
|
|
14685
|
-
|
|
14686
|
-
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
|
+
}
|
|
14687
16420
|
}
|
|
14688
16421
|
}
|
|
14689
16422
|
}
|
|
14690
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
|
+
}
|
|
14691
16439
|
}
|
|
14692
16440
|
} catch {
|
|
14693
16441
|
cloudOk = false;
|
|
@@ -14704,7 +16452,8 @@ function buildStatusCommand() {
|
|
|
14704
16452
|
info: daemonStatus.info ?? {}
|
|
14705
16453
|
},
|
|
14706
16454
|
cloud: { base: cfg.cloud_api_base, reachable: cloudOk, me, devices, agents },
|
|
14707
|
-
local
|
|
16455
|
+
local,
|
|
16456
|
+
diagnose
|
|
14708
16457
|
};
|
|
14709
16458
|
if (opts.json) {
|
|
14710
16459
|
printJson(report);
|
|
@@ -14713,6 +16462,60 @@ function buildStatusCommand() {
|
|
|
14713
16462
|
printPretty(report);
|
|
14714
16463
|
});
|
|
14715
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
|
+
}
|
|
14716
16519
|
async function readDaemonStatus() {
|
|
14717
16520
|
try {
|
|
14718
16521
|
const res = await fetch("http://127.0.0.1:3210/healthz", {
|
|
@@ -14756,7 +16559,7 @@ function printPretty(report) {
|
|
|
14756
16559
|
ui.blank();
|
|
14757
16560
|
ok("Config", report.paths.config);
|
|
14758
16561
|
if (report.daemon.running) {
|
|
14759
|
-
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)();
|
|
14760
16563
|
const ws = report.daemon.wsConnected ? "connected" : "pending";
|
|
14761
16564
|
ok("Daemon", `${daemonName} pid=${report.daemon.pid} ws=${ws}`);
|
|
14762
16565
|
if (report.daemon.info) {
|
|
@@ -14785,16 +16588,7 @@ function printPretty(report) {
|
|
|
14785
16588
|
if (Array.isArray(report.cloud.devices) && report.cloud.devices.length > 0) {
|
|
14786
16589
|
const devs = report.cloud.devices;
|
|
14787
16590
|
ui.blank();
|
|
14788
|
-
|
|
14789
|
-
for (const d of devs) {
|
|
14790
|
-
const statusIcon = d.daemonStatus === "connected" ? "\u25CF" : "\u25CB";
|
|
14791
|
-
const kind = d.runtimeKind === "docker" ? "Local" : "K8s";
|
|
14792
|
-
const declared = d.hostedAgentSummary?.declared ?? 0;
|
|
14793
|
-
ui.line(` ${statusIcon} ${d.podName?.replace(/^daemon:/, "")?.slice(0, 28) ?? "?"} kind=${kind} agents=${declared}`);
|
|
14794
|
-
}
|
|
14795
|
-
}
|
|
14796
|
-
if (Array.isArray(report.cloud.agents) && report.cloud.agents.length > 0) {
|
|
14797
|
-
ui.line(` Hosted agents: ${report.cloud.agents.length}`);
|
|
16591
|
+
for (const ln of formatDeviceBindings(devs)) ui.line(ln);
|
|
14798
16592
|
}
|
|
14799
16593
|
if (report.local) {
|
|
14800
16594
|
ui.blank();
|
|
@@ -14804,6 +16598,58 @@ function printPretty(report) {
|
|
|
14804
16598
|
} else {
|
|
14805
16599
|
warn("Local DB", "unavailable");
|
|
14806
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`;
|
|
14807
16653
|
}
|
|
14808
16654
|
|
|
14809
16655
|
// src/cli/commands/task.ts
|
|
@@ -15166,7 +17012,7 @@ async function readResponseError(res) {
|
|
|
15166
17012
|
|
|
15167
17013
|
// src/cli/index.ts
|
|
15168
17014
|
init_ui();
|
|
15169
|
-
var VERSION = "2.0.
|
|
17015
|
+
var VERSION = "2.0.5";
|
|
15170
17016
|
function buildProgram() {
|
|
15171
17017
|
const program = new import_commander20.Command("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
|
|
15172
17018
|
program.addCommand(buildBannerCommand());
|