@prismer/runtime 2.0.4 → 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +2123 -267
- package/dist/cli.js +2004 -148
- package/dist/index.cjs +2164 -306
- package/dist/index.d.cts +543 -182
- package/dist/index.d.ts +543 -182
- package/dist/index.js +2014 -156
- package/package.json +2 -1
package/dist/cli.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_process3, import_node_fs2, import_node_module, import_node_os, import_node_path2, import_node_url, import_better_sqlite3, YAML, import_zod3, import_meta, PRISMER_IM_SKILL_NAME, PRISMER_IM_SKILL_CONTENT, HermesProfileConfigSchema, hermesAdapter, HermesService;
|
|
830
|
+
var import_node_child_process3, import_node_fs2, import_node_module, import_node_os, import_node_path2, import_node_url, import_better_sqlite3, YAML, import_zod3, 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_zod4, import_node_os2, import_node_path3, OpenClawProfileConfigSchema, openclawAdapter, OpenClawService;
|
|
1634
|
+
var import_zod4, 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_zod4 = 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_zod4.z.object({
|
|
1381
1646
|
/** OpenClaw chat-completions HTTP port. */
|
|
1382
1647
|
port: import_zod4.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
|
});
|
|
@@ -1934,7 +2268,7 @@ var require_package = __commonJS({
|
|
|
1934
2268
|
"package.json"(exports2, module2) {
|
|
1935
2269
|
module2.exports = {
|
|
1936
2270
|
name: "@prismer/runtime",
|
|
1937
|
-
version: "2.0.
|
|
2271
|
+
version: "2.0.6",
|
|
1938
2272
|
description: "Prismer Cloud daemon runtime \u2014 TS-only adapter host for hosted IM agents",
|
|
1939
2273
|
type: "module",
|
|
1940
2274
|
main: "dist/index.js",
|
|
@@ -1968,6 +2302,7 @@ var require_package = __commonJS({
|
|
|
1968
2302
|
},
|
|
1969
2303
|
dependencies: {
|
|
1970
2304
|
"@iarna/toml": "^2.2.5",
|
|
2305
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
1971
2306
|
"better-sqlite3": "^11.8.0",
|
|
1972
2307
|
commander: "^12.1.0",
|
|
1973
2308
|
qrcode: "^1.5.4",
|
|
@@ -2125,21 +2460,21 @@ function pidFilePath(paths) {
|
|
|
2125
2460
|
return (0, import_node_path5.join)(paths.root, "daemon.pid");
|
|
2126
2461
|
}
|
|
2127
2462
|
function writePidFile(paths, pid) {
|
|
2128
|
-
(0,
|
|
2463
|
+
(0, import_node_fs5.writeFileSync)(pidFilePath(paths), `${pid}
|
|
2129
2464
|
`, "utf8");
|
|
2130
2465
|
}
|
|
2131
2466
|
function readPidFile(paths) {
|
|
2132
2467
|
const p = pidFilePath(paths);
|
|
2133
|
-
if (!(0,
|
|
2134
|
-
const raw = (0,
|
|
2468
|
+
if (!(0, import_node_fs5.existsSync)(p)) return void 0;
|
|
2469
|
+
const raw = (0, import_node_fs5.readFileSync)(p, "utf8").trim();
|
|
2135
2470
|
const pid = Number.parseInt(raw, 10);
|
|
2136
2471
|
return Number.isFinite(pid) ? pid : void 0;
|
|
2137
2472
|
}
|
|
2138
2473
|
function clearPidFile(paths) {
|
|
2139
2474
|
const p = pidFilePath(paths);
|
|
2140
|
-
if ((0,
|
|
2475
|
+
if ((0, import_node_fs5.existsSync)(p)) {
|
|
2141
2476
|
try {
|
|
2142
|
-
(0,
|
|
2477
|
+
(0, import_node_fs5.unlinkSync)(p);
|
|
2143
2478
|
} catch {
|
|
2144
2479
|
}
|
|
2145
2480
|
}
|
|
@@ -2152,11 +2487,11 @@ function pidAlive2(pid) {
|
|
|
2152
2487
|
return false;
|
|
2153
2488
|
}
|
|
2154
2489
|
}
|
|
2155
|
-
var
|
|
2490
|
+
var import_node_fs5, import_node_path5, DEFAULT_CLOUD_BASE_URL, ANSI, PKG_VERSION, RUNTIME_SUBTITLE;
|
|
2156
2491
|
var init_util = __esm({
|
|
2157
2492
|
"src/cli/util.ts"() {
|
|
2158
2493
|
"use strict";
|
|
2159
|
-
|
|
2494
|
+
import_node_fs5 = require("fs");
|
|
2160
2495
|
import_node_path5 = require("path");
|
|
2161
2496
|
init_ui();
|
|
2162
2497
|
DEFAULT_CLOUD_BASE_URL = "https://prismer.cloud";
|
|
@@ -2296,7 +2631,7 @@ async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, sig
|
|
|
2296
2631
|
continue;
|
|
2297
2632
|
}
|
|
2298
2633
|
const skillDir = (0, import_node_path12.join)(skillsRoot, slug);
|
|
2299
|
-
await
|
|
2634
|
+
await import_node_fs12.promises.mkdir(skillDir, { recursive: true });
|
|
2300
2635
|
const localFiles = await walkLocalDir(skillDir);
|
|
2301
2636
|
let dirty = false;
|
|
2302
2637
|
let perFileFailures = 0;
|
|
@@ -2351,14 +2686,14 @@ async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, sig
|
|
|
2351
2686
|
perFileFailures++;
|
|
2352
2687
|
continue;
|
|
2353
2688
|
}
|
|
2354
|
-
await
|
|
2355
|
-
await
|
|
2689
|
+
await import_node_fs12.promises.mkdir((0, import_node_path12.dirname)(targetPath), { recursive: true });
|
|
2690
|
+
await import_node_fs12.promises.writeFile(targetPath, bytes);
|
|
2356
2691
|
localFiles.delete(file.path);
|
|
2357
2692
|
dirty = true;
|
|
2358
2693
|
}
|
|
2359
2694
|
for (const orphan of localFiles.keys()) {
|
|
2360
2695
|
try {
|
|
2361
|
-
await
|
|
2696
|
+
await import_node_fs12.promises.unlink((0, import_node_path12.join)(skillDir, orphan));
|
|
2362
2697
|
dirty = true;
|
|
2363
2698
|
} catch (err) {
|
|
2364
2699
|
if (err.code !== "ENOENT") {
|
|
@@ -2471,7 +2806,7 @@ async function walkLocalDir(root) {
|
|
|
2471
2806
|
async function walk2(dir) {
|
|
2472
2807
|
let entries;
|
|
2473
2808
|
try {
|
|
2474
|
-
entries = await
|
|
2809
|
+
entries = await import_node_fs12.promises.readdir(dir, { withFileTypes: true });
|
|
2475
2810
|
} catch (err) {
|
|
2476
2811
|
if (err.code === "ENOENT") return;
|
|
2477
2812
|
throw err;
|
|
@@ -2482,7 +2817,7 @@ async function walkLocalDir(root) {
|
|
|
2482
2817
|
await walk2(full);
|
|
2483
2818
|
} else if (ent.isFile()) {
|
|
2484
2819
|
try {
|
|
2485
|
-
const buf = await
|
|
2820
|
+
const buf = await import_node_fs12.promises.readFile(full);
|
|
2486
2821
|
const rel = (0, import_node_path12.relative)(root, full).split(import_node_path12.sep).join("/");
|
|
2487
2822
|
out.set(rel, sha256Buffer(buf));
|
|
2488
2823
|
} catch {
|
|
@@ -2556,11 +2891,11 @@ async function ackSkillSync(cloud, agentImUserId, input, signal) {
|
|
|
2556
2891
|
`);
|
|
2557
2892
|
}
|
|
2558
2893
|
}
|
|
2559
|
-
var
|
|
2894
|
+
var import_node_fs12, import_node_path12, import_node_crypto3, skillSyncWarnedProfiles;
|
|
2560
2895
|
var init_skill_sync = __esm({
|
|
2561
2896
|
"src/daemon/skill-sync.ts"() {
|
|
2562
2897
|
"use strict";
|
|
2563
|
-
|
|
2898
|
+
import_node_fs12 = require("fs");
|
|
2564
2899
|
import_node_path12 = require("path");
|
|
2565
2900
|
import_node_crypto3 = require("crypto");
|
|
2566
2901
|
init_hermes();
|
|
@@ -2574,7 +2909,7 @@ var import_commander20 = require("commander");
|
|
|
2574
2909
|
|
|
2575
2910
|
// src/cli/commands/adapter.ts
|
|
2576
2911
|
var import_node_child_process4 = require("child_process");
|
|
2577
|
-
var
|
|
2912
|
+
var import_node_fs6 = require("fs");
|
|
2578
2913
|
var import_node_os4 = require("os");
|
|
2579
2914
|
var import_node_path6 = require("path");
|
|
2580
2915
|
var import_commander = require("commander");
|
|
@@ -3073,7 +3408,7 @@ init_openclaw();
|
|
|
3073
3408
|
|
|
3074
3409
|
// src/config.ts
|
|
3075
3410
|
var TOML = __toESM(require("@iarna/toml"), 1);
|
|
3076
|
-
var
|
|
3411
|
+
var import_node_fs4 = require("fs");
|
|
3077
3412
|
var import_node_os3 = require("os");
|
|
3078
3413
|
var import_node_path4 = require("path");
|
|
3079
3414
|
var import_zod5 = require("zod");
|
|
@@ -3116,15 +3451,15 @@ function resolvePaths(home) {
|
|
|
3116
3451
|
};
|
|
3117
3452
|
}
|
|
3118
3453
|
function configExists(paths = resolvePaths()) {
|
|
3119
|
-
return (0,
|
|
3454
|
+
return (0, import_node_fs4.existsSync)(paths.configFile);
|
|
3120
3455
|
}
|
|
3121
3456
|
function loadConfig(paths = resolvePaths()) {
|
|
3122
|
-
if (!(0,
|
|
3457
|
+
if (!(0, import_node_fs4.existsSync)(paths.configFile)) {
|
|
3123
3458
|
throw new Error(
|
|
3124
3459
|
`Config not found at ${paths.configFile}. Run \`prismer setup\` to create it.`
|
|
3125
3460
|
);
|
|
3126
3461
|
}
|
|
3127
|
-
const raw = (0,
|
|
3462
|
+
const raw = (0, import_node_fs4.readFileSync)(paths.configFile, "utf8");
|
|
3128
3463
|
const parsed = TOML.parse(raw);
|
|
3129
3464
|
const merged = {
|
|
3130
3465
|
...parsed,
|
|
@@ -3139,14 +3474,14 @@ function loadConfig(paths = resolvePaths()) {
|
|
|
3139
3474
|
return result.data;
|
|
3140
3475
|
}
|
|
3141
3476
|
function saveConfig(config, paths = resolvePaths()) {
|
|
3142
|
-
if (!(0,
|
|
3143
|
-
(0,
|
|
3477
|
+
if (!(0, import_node_fs4.existsSync)(paths.root)) {
|
|
3478
|
+
(0, import_node_fs4.mkdirSync)(paths.root, { recursive: true });
|
|
3144
3479
|
}
|
|
3145
|
-
if (!(0,
|
|
3146
|
-
(0,
|
|
3480
|
+
if (!(0, import_node_fs4.existsSync)((0, import_node_path4.dirname)(paths.configFile))) {
|
|
3481
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(paths.configFile), { recursive: true });
|
|
3147
3482
|
}
|
|
3148
3483
|
ConfigSchema.parse(config);
|
|
3149
|
-
(0,
|
|
3484
|
+
(0, import_node_fs4.writeFileSync)(paths.configFile, TOML.stringify(config), "utf8");
|
|
3150
3485
|
}
|
|
3151
3486
|
function deriveWsUrl(httpBase) {
|
|
3152
3487
|
const u = new URL(httpBase);
|
|
@@ -3409,15 +3744,15 @@ function runHooks(spec, opts) {
|
|
|
3409
3744
|
}
|
|
3410
3745
|
let backup;
|
|
3411
3746
|
if (planned.kind === "directory") {
|
|
3412
|
-
(0,
|
|
3747
|
+
(0, import_node_fs6.mkdirSync)(planned.path, { recursive: true });
|
|
3413
3748
|
const markerPath = (0, import_node_path6.join)(planned.path, "prismer.json");
|
|
3414
3749
|
backup = backupIfExists(markerPath);
|
|
3415
|
-
(0,
|
|
3750
|
+
(0, import_node_fs6.writeFileSync)(markerPath, JSON.stringify(prismerHookMarker(spec), null, 2) + "\n", "utf8");
|
|
3416
3751
|
} else {
|
|
3417
|
-
(0,
|
|
3752
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path6.dirname)(planned.path), { recursive: true });
|
|
3418
3753
|
backup = backupIfExists(planned.path);
|
|
3419
3754
|
const merged = mergeHookJson(planned.path, spec);
|
|
3420
|
-
(0,
|
|
3755
|
+
(0, import_node_fs6.writeFileSync)(planned.path, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
3421
3756
|
}
|
|
3422
3757
|
return {
|
|
3423
3758
|
ok: true,
|
|
@@ -3486,10 +3821,10 @@ function inspectAuthEnv(spec) {
|
|
|
3486
3821
|
case "hermes": {
|
|
3487
3822
|
const envFile = (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".hermes", ".env");
|
|
3488
3823
|
return {
|
|
3489
|
-
ok: (0,
|
|
3824
|
+
ok: (0, import_node_fs6.existsSync)(envFile) || Boolean(process.env.HERMES_API_KEY || process.env.API_SERVER_KEY),
|
|
3490
3825
|
present: [
|
|
3491
3826
|
...["HERMES_API_KEY", "API_SERVER_KEY"].filter((k) => Boolean(process.env[k])),
|
|
3492
|
-
...(0,
|
|
3827
|
+
...(0, import_node_fs6.existsSync)(envFile) ? [envFile] : []
|
|
3493
3828
|
],
|
|
3494
3829
|
hints: spec.authHints ?? []
|
|
3495
3830
|
};
|
|
@@ -3497,10 +3832,10 @@ function inspectAuthEnv(spec) {
|
|
|
3497
3832
|
case "openclaw": {
|
|
3498
3833
|
const cfgFile = (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".openclaw", "openclaw.json");
|
|
3499
3834
|
return {
|
|
3500
|
-
ok: (0,
|
|
3835
|
+
ok: (0, import_node_fs6.existsSync)(cfgFile) || Boolean(process.env.OPENCLAW_API_KEY),
|
|
3501
3836
|
present: [
|
|
3502
3837
|
...["OPENCLAW_API_KEY"].filter((k) => Boolean(process.env[k])),
|
|
3503
|
-
...(0,
|
|
3838
|
+
...(0, import_node_fs6.existsSync)(cfgFile) ? [cfgFile] : []
|
|
3504
3839
|
],
|
|
3505
3840
|
hints: spec.authHints ?? []
|
|
3506
3841
|
};
|
|
@@ -3517,14 +3852,14 @@ function inspectHook(spec) {
|
|
|
3517
3852
|
if (spec.name === "openclaw") {
|
|
3518
3853
|
const jsonPath = (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".openclaw", "hooks.json");
|
|
3519
3854
|
const markerPath = (0, import_node_path6.join)(target.path, "prismer.json");
|
|
3520
|
-
const jsonMarkerPresent = (0,
|
|
3521
|
-
const dirMarkerPresent = (0,
|
|
3855
|
+
const jsonMarkerPresent = (0, import_node_fs6.existsSync)(jsonPath) && fileContainsPrismerMarker(jsonPath);
|
|
3856
|
+
const dirMarkerPresent = (0, import_node_fs6.existsSync)(markerPath) && fileContainsPrismerMarker(markerPath);
|
|
3522
3857
|
return {
|
|
3523
3858
|
supported: true,
|
|
3524
3859
|
kind: "json-file-or-directory",
|
|
3525
3860
|
path: target.path,
|
|
3526
3861
|
jsonPath,
|
|
3527
|
-
exists: (0,
|
|
3862
|
+
exists: (0, import_node_fs6.existsSync)(target.path) || (0, import_node_fs6.existsSync)(jsonPath),
|
|
3528
3863
|
markerPath,
|
|
3529
3864
|
markerPresent: jsonMarkerPresent || dirMarkerPresent
|
|
3530
3865
|
};
|
|
@@ -3535,12 +3870,12 @@ function inspectHook(spec) {
|
|
|
3535
3870
|
supported: true,
|
|
3536
3871
|
kind: target.kind,
|
|
3537
3872
|
path: target.path,
|
|
3538
|
-
exists: (0,
|
|
3873
|
+
exists: (0, import_node_fs6.existsSync)(target.path),
|
|
3539
3874
|
markerPath,
|
|
3540
|
-
markerPresent: (0,
|
|
3875
|
+
markerPresent: (0, import_node_fs6.existsSync)(markerPath) && fileContainsPrismerMarker(markerPath)
|
|
3541
3876
|
};
|
|
3542
3877
|
}
|
|
3543
|
-
const exists = (0,
|
|
3878
|
+
const exists = (0, import_node_fs6.existsSync)(target.path);
|
|
3544
3879
|
if (!exists) {
|
|
3545
3880
|
return {
|
|
3546
3881
|
supported: true,
|
|
@@ -3576,14 +3911,14 @@ function prismerHookMarker(spec) {
|
|
|
3576
3911
|
}
|
|
3577
3912
|
function mergeHookJson(path9, spec) {
|
|
3578
3913
|
const marker = prismerHookMarker(spec);
|
|
3579
|
-
if (!(0,
|
|
3914
|
+
if (!(0, import_node_fs6.existsSync)(path9)) {
|
|
3580
3915
|
return {
|
|
3581
3916
|
prismer: marker
|
|
3582
3917
|
};
|
|
3583
3918
|
}
|
|
3584
3919
|
let parsed;
|
|
3585
3920
|
try {
|
|
3586
|
-
parsed = JSON.parse((0,
|
|
3921
|
+
parsed = JSON.parse((0, import_node_fs6.readFileSync)(path9, "utf8"));
|
|
3587
3922
|
} catch (err) {
|
|
3588
3923
|
throw new Error(`Cannot parse ${path9} as JSON: ${err.message}`);
|
|
3589
3924
|
}
|
|
@@ -3607,17 +3942,17 @@ function mergeHookJson(path9, spec) {
|
|
|
3607
3942
|
return next;
|
|
3608
3943
|
}
|
|
3609
3944
|
function backupIfExists(path9) {
|
|
3610
|
-
if (!(0,
|
|
3945
|
+
if (!(0, import_node_fs6.existsSync)(path9)) return null;
|
|
3611
3946
|
const suffix = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3612
3947
|
const backup = `${path9}.bak.${suffix}`;
|
|
3613
|
-
const stat = (0,
|
|
3948
|
+
const stat = (0, import_node_fs6.statSync)(path9);
|
|
3614
3949
|
if (stat.isDirectory()) return null;
|
|
3615
|
-
(0,
|
|
3950
|
+
(0, import_node_fs6.copyFileSync)(path9, backup);
|
|
3616
3951
|
return backup;
|
|
3617
3952
|
}
|
|
3618
3953
|
function fileContainsPrismerMarker(path9) {
|
|
3619
3954
|
try {
|
|
3620
|
-
return (0,
|
|
3955
|
+
return (0, import_node_fs6.readFileSync)(path9, "utf8").includes("prismer-daemon-runtime");
|
|
3621
3956
|
} catch {
|
|
3622
3957
|
return false;
|
|
3623
3958
|
}
|
|
@@ -3815,7 +4150,7 @@ function mapStatusToCode(status) {
|
|
|
3815
4150
|
|
|
3816
4151
|
// src/sync/store.ts
|
|
3817
4152
|
var import_better_sqlite32 = __toESM(require("better-sqlite3"), 1);
|
|
3818
|
-
var
|
|
4153
|
+
var import_node_fs7 = require("fs");
|
|
3819
4154
|
var import_node_path7 = require("path");
|
|
3820
4155
|
var MIGRATIONS = [
|
|
3821
4156
|
{
|
|
@@ -3942,14 +4277,44 @@ var MIGRATIONS = [
|
|
|
3942
4277
|
CREATE INDEX IF NOT EXISTS idx_asset_meta_seq
|
|
3943
4278
|
ON asset_metadata_index(workspace_id, asset_index_seq);
|
|
3944
4279
|
`
|
|
4280
|
+
},
|
|
4281
|
+
{
|
|
4282
|
+
version: 4,
|
|
4283
|
+
up: `
|
|
4284
|
+
-- Wave 4 / W3 \xA74.3 \u2014 pending dispatch replies for crash recovery.
|
|
4285
|
+
--
|
|
4286
|
+
-- The two-phase reply protocol writes a row here BEFORE calling
|
|
4287
|
+
-- POST /dispatch/:taskId/prepare so that a crash between prepare
|
|
4288
|
+
-- and commit leaves a recoverable trail. On daemon cold-start, any
|
|
4289
|
+
-- row with status='prepared' triggers an idempotent commit retry.
|
|
4290
|
+
--
|
|
4291
|
+
-- The server-side cutoff for orphaned 'prepared' rows is 1h
|
|
4292
|
+
-- (PREPARED_REPLY_ORPHAN_MS in dispatch-reply.service.ts); rows
|
|
4293
|
+
-- older than that will fail commit with DISPATCH_REPLY_INVALID_STATE
|
|
4294
|
+
-- and must be reported to the user as 'dispatch lost'.
|
|
4295
|
+
CREATE TABLE IF NOT EXISTS pending_dispatch_replies (
|
|
4296
|
+
idempotency_key TEXT PRIMARY KEY,
|
|
4297
|
+
task_id TEXT NOT NULL,
|
|
4298
|
+
reply_id TEXT,
|
|
4299
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
4300
|
+
payload_json TEXT NOT NULL,
|
|
4301
|
+
prepared_at INTEGER,
|
|
4302
|
+
created_at INTEGER NOT NULL,
|
|
4303
|
+
last_attempt_at INTEGER,
|
|
4304
|
+
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
4305
|
+
last_error TEXT
|
|
4306
|
+
);
|
|
4307
|
+
CREATE INDEX IF NOT EXISTS idx_pending_reply_task ON pending_dispatch_replies (task_id);
|
|
4308
|
+
CREATE INDEX IF NOT EXISTS idx_pending_reply_status ON pending_dispatch_replies (status, created_at);
|
|
4309
|
+
`
|
|
3945
4310
|
}
|
|
3946
4311
|
];
|
|
3947
4312
|
function runSql(db, sql) {
|
|
3948
4313
|
db["exec"](sql);
|
|
3949
4314
|
}
|
|
3950
4315
|
function openLocalDb(path9) {
|
|
3951
|
-
if (path9 !== ":memory:" && !(0,
|
|
3952
|
-
(0,
|
|
4316
|
+
if (path9 !== ":memory:" && !(0, import_node_fs7.existsSync)((0, import_node_path7.dirname)(path9))) {
|
|
4317
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path7.dirname)(path9), { recursive: true });
|
|
3953
4318
|
}
|
|
3954
4319
|
const db = new import_better_sqlite32.default(path9);
|
|
3955
4320
|
db.pragma("journal_mode = WAL");
|
|
@@ -4271,8 +4636,74 @@ function buildAgentCommand() {
|
|
|
4271
4636
|
}
|
|
4272
4637
|
getUI().ok("Skill install requested", `agent=${opts.agent} skill=${slugOrId}`);
|
|
4273
4638
|
}, { code: "agent_install_skill_failed" }));
|
|
4639
|
+
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) => {
|
|
4640
|
+
const cloud = makeCloudClient();
|
|
4641
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agents/${encodeURIComponent(imUserId)}/snapshot`, {
|
|
4642
|
+
includeMemory: Boolean(opts.includeMemory),
|
|
4643
|
+
label: opts.label
|
|
4644
|
+
}, "agent_snapshot_failed");
|
|
4645
|
+
if (opts.json) {
|
|
4646
|
+
printJson(data);
|
|
4647
|
+
return;
|
|
4648
|
+
}
|
|
4649
|
+
const rec = data;
|
|
4650
|
+
getUI().ok("Snapshot created", String(rec.id ?? "<unknown>"));
|
|
4651
|
+
getUI().line(`agent=${String(rec.agentImUserId ?? imUserId)} memory=${rec.includeMemory ? "included" : "stripped"}`);
|
|
4652
|
+
}, { code: "agent_snapshot_failed" }));
|
|
4653
|
+
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) => {
|
|
4654
|
+
const cloud = makeCloudClient();
|
|
4655
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agents/${encodeURIComponent(imUserId)}/publish`, {
|
|
4656
|
+
slug: opts.slug,
|
|
4657
|
+
version: opts.version,
|
|
4658
|
+
license: opts.license,
|
|
4659
|
+
metadata: {
|
|
4660
|
+
...opts.title ? { title: opts.title } : {},
|
|
4661
|
+
...opts.description ? { description: opts.description } : {}
|
|
4662
|
+
},
|
|
4663
|
+
stripMemory: true
|
|
4664
|
+
}, "agent_publish_failed");
|
|
4665
|
+
if (opts.json) {
|
|
4666
|
+
printJson(data);
|
|
4667
|
+
return;
|
|
4668
|
+
}
|
|
4669
|
+
const rec = data;
|
|
4670
|
+
getUI().ok("Agent Pack published", `${String(rec.slug ?? opts.slug)}@${String(rec.version ?? opts.version ?? "1.0.0")}`);
|
|
4671
|
+
getUI().line(`package=${String(rec.id ?? "<unknown>")}`);
|
|
4672
|
+
}, { code: "agent_publish_failed" }));
|
|
4673
|
+
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) => {
|
|
4674
|
+
const cloud = makeCloudClient();
|
|
4675
|
+
const data = await requestEnvelope(cloud, "POST", `/api/im/agent-packs/${encodeURIComponent(packIdOrSlug)}/fork`, {
|
|
4676
|
+
targetWorkspaceId: opts.workspaceId,
|
|
4677
|
+
displayName: opts.displayName
|
|
4678
|
+
}, "agent_fork_failed");
|
|
4679
|
+
if (opts.json) {
|
|
4680
|
+
printJson(data);
|
|
4681
|
+
return;
|
|
4682
|
+
}
|
|
4683
|
+
const rec = data;
|
|
4684
|
+
getUI().ok("Agent Pack forked", `newAgent=${String(rec.newImUserId ?? "<unknown>")}`);
|
|
4685
|
+
getUI().line(`did=${String(rec.newDid ?? "not issued")}`);
|
|
4686
|
+
}, { code: "agent_fork_failed" }));
|
|
4274
4687
|
return cmd;
|
|
4275
4688
|
}
|
|
4689
|
+
function makeCloudClient() {
|
|
4690
|
+
const cfg = loadConfig(resolvePaths());
|
|
4691
|
+
return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
4692
|
+
}
|
|
4693
|
+
async function requestEnvelope(cloud, method, path9, body, code) {
|
|
4694
|
+
const res = await cloud.request(
|
|
4695
|
+
method,
|
|
4696
|
+
path9,
|
|
4697
|
+
body === void 0 ? void 0 : { body }
|
|
4698
|
+
);
|
|
4699
|
+
if (!res.ok || res.data?.ok === false) {
|
|
4700
|
+
const message = res.error?.message ?? res.data?.error?.message ?? "request failed";
|
|
4701
|
+
exitWithError(`${path9} failed (${res.status === 0 ? "network error" : `HTTP ${res.status}`}): ${message}`, {
|
|
4702
|
+
code: res.error?.code ?? res.data?.error?.code ?? code
|
|
4703
|
+
});
|
|
4704
|
+
}
|
|
4705
|
+
return res.data && typeof res.data === "object" && "data" in res.data ? res.data.data : res.data;
|
|
4706
|
+
}
|
|
4276
4707
|
function readLocalAgents(localDb) {
|
|
4277
4708
|
const db = openLocalDb(localDb);
|
|
4278
4709
|
try {
|
|
@@ -4377,13 +4808,13 @@ function whichBinary2(bin) {
|
|
|
4377
4808
|
// src/cli/commands/asset.ts
|
|
4378
4809
|
var import_node_crypto2 = require("crypto");
|
|
4379
4810
|
var import_commander3 = require("commander");
|
|
4380
|
-
var
|
|
4811
|
+
var import_node_fs11 = require("fs");
|
|
4381
4812
|
var import_node_path11 = require("path");
|
|
4382
4813
|
|
|
4383
4814
|
// src/asset-cache.ts
|
|
4384
4815
|
var import_node_crypto = require("crypto");
|
|
4385
4816
|
var import_node_dns = require("dns");
|
|
4386
|
-
var
|
|
4817
|
+
var import_node_fs8 = require("fs");
|
|
4387
4818
|
var import_node_net = require("net");
|
|
4388
4819
|
var import_node_path8 = require("path");
|
|
4389
4820
|
var import_undici = require("undici");
|
|
@@ -4408,8 +4839,8 @@ var AssetCache = class {
|
|
|
4408
4839
|
this.cloud = opts.cloud;
|
|
4409
4840
|
this.cacheDir = opts.cacheDir;
|
|
4410
4841
|
this.maxBytes = opts.maxBytes ?? 5 * 1024 * 1024 * 1024;
|
|
4411
|
-
if (!(0,
|
|
4412
|
-
(0,
|
|
4842
|
+
if (!(0, import_node_fs8.existsSync)(this.cacheDir)) {
|
|
4843
|
+
(0, import_node_fs8.mkdirSync)(this.cacheDir, { recursive: true });
|
|
4413
4844
|
}
|
|
4414
4845
|
}
|
|
4415
4846
|
/** Local file path for a content hash. Files split by first 2 chars for fs scaling. */
|
|
@@ -4419,7 +4850,7 @@ var AssetCache = class {
|
|
|
4419
4850
|
get(hash) {
|
|
4420
4851
|
const row = this.db.prepare("SELECT * FROM cached_assets WHERE content_hash = ?").get(hash);
|
|
4421
4852
|
if (!row) return void 0;
|
|
4422
|
-
if (!(0,
|
|
4853
|
+
if (!(0, import_node_fs8.existsSync)(row.local_path)) {
|
|
4423
4854
|
this.db.prepare("DELETE FROM cached_assets WHERE content_hash = ?").run(hash);
|
|
4424
4855
|
return void 0;
|
|
4425
4856
|
}
|
|
@@ -4483,12 +4914,12 @@ var AssetCache = class {
|
|
|
4483
4914
|
throw new Error(`Asset hash mismatch for ${hash}: downloaded ${actualHash}`);
|
|
4484
4915
|
}
|
|
4485
4916
|
const localPath = this.pathFor(hash);
|
|
4486
|
-
if (!(0,
|
|
4487
|
-
(0,
|
|
4917
|
+
if (!(0, import_node_fs8.existsSync)((0, import_node_path8.dirname)(localPath))) {
|
|
4918
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path8.dirname)(localPath), { recursive: true });
|
|
4488
4919
|
}
|
|
4489
4920
|
const tmpPath = `${localPath}.${process.pid}.${Date.now()}.tmp`;
|
|
4490
|
-
(0,
|
|
4491
|
-
(0,
|
|
4921
|
+
(0, import_node_fs8.writeFileSync)(tmpPath, buf);
|
|
4922
|
+
(0, import_node_fs8.renameSync)(tmpPath, localPath);
|
|
4492
4923
|
const mime = res.headers.get("content-type");
|
|
4493
4924
|
const now = Date.now();
|
|
4494
4925
|
this.db.prepare(
|
|
@@ -4547,12 +4978,12 @@ var AssetCache = class {
|
|
|
4547
4978
|
return { cached: existing, finalUrl, durationMs: Date.now() - started };
|
|
4548
4979
|
}
|
|
4549
4980
|
const localPath = this.pathFor(hash);
|
|
4550
|
-
if (!(0,
|
|
4551
|
-
(0,
|
|
4981
|
+
if (!(0, import_node_fs8.existsSync)((0, import_node_path8.dirname)(localPath))) {
|
|
4982
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path8.dirname)(localPath), { recursive: true });
|
|
4552
4983
|
}
|
|
4553
4984
|
const tmpPath = `${localPath}.${process.pid}.${Date.now()}.tmp`;
|
|
4554
|
-
(0,
|
|
4555
|
-
(0,
|
|
4985
|
+
(0, import_node_fs8.writeFileSync)(tmpPath, body);
|
|
4986
|
+
(0, import_node_fs8.renameSync)(tmpPath, localPath);
|
|
4556
4987
|
const now = Date.now();
|
|
4557
4988
|
this.db.prepare(
|
|
4558
4989
|
`INSERT OR REPLACE INTO cached_assets
|
|
@@ -4580,7 +5011,7 @@ var AssetCache = class {
|
|
|
4580
5011
|
if (actualHash !== hash) {
|
|
4581
5012
|
throw new Error(`Asset hash mismatch for ${hash}: local file is ${actualHash}`);
|
|
4582
5013
|
}
|
|
4583
|
-
const size = (0,
|
|
5014
|
+
const size = (0, import_node_fs8.statSync)(localPath).size;
|
|
4584
5015
|
const now = Date.now();
|
|
4585
5016
|
this.db.prepare(
|
|
4586
5017
|
`INSERT OR REPLACE INTO cached_assets
|
|
@@ -4608,7 +5039,7 @@ var AssetCache = class {
|
|
|
4608
5039
|
for (const row of candidates) {
|
|
4609
5040
|
if (total <= this.maxBytes) break;
|
|
4610
5041
|
try {
|
|
4611
|
-
if ((0,
|
|
5042
|
+
if ((0, import_node_fs8.existsSync)(row.local_path)) (0, import_node_fs8.unlinkSync)(row.local_path);
|
|
4612
5043
|
} catch {
|
|
4613
5044
|
}
|
|
4614
5045
|
this.db.prepare("DELETE FROM cached_assets WHERE content_hash = ?").run(row.content_hash);
|
|
@@ -4623,7 +5054,7 @@ function sha256(buf) {
|
|
|
4623
5054
|
return (0, import_node_crypto.createHash)("sha256").update(buf).digest("hex");
|
|
4624
5055
|
}
|
|
4625
5056
|
function readFileBuffer(path9) {
|
|
4626
|
-
return (0,
|
|
5057
|
+
return (0, import_node_fs8.readFileSync)(path9);
|
|
4627
5058
|
}
|
|
4628
5059
|
var DEFAULT_URL_FETCH_MAX_BYTES = 5 * 1024 * 1024;
|
|
4629
5060
|
var DEFAULT_URL_FETCH_TIMEOUT_MS = 15e3;
|
|
@@ -4805,7 +5236,7 @@ async function fetchUrlWithGuards(rawUrl, opts) {
|
|
|
4805
5236
|
}
|
|
4806
5237
|
|
|
4807
5238
|
// src/daemon/asset/metadata-index.ts
|
|
4808
|
-
var
|
|
5239
|
+
var import_node_fs9 = require("fs");
|
|
4809
5240
|
var import_node_path9 = require("path");
|
|
4810
5241
|
var DEFAULT_LIMIT = 8;
|
|
4811
5242
|
var PULL_PAGE_SIZE = 500;
|
|
@@ -4834,16 +5265,16 @@ var AssetMetadataIndex = class {
|
|
|
4834
5265
|
this.db = opts.db;
|
|
4835
5266
|
this.cloud = opts.cloud;
|
|
4836
5267
|
this.workspaceId = opts.workspaceId;
|
|
4837
|
-
if (!(0,
|
|
4838
|
-
(0,
|
|
5268
|
+
if (!(0, import_node_fs9.existsSync)(opts.workspaceStateDir)) {
|
|
5269
|
+
(0, import_node_fs9.mkdirSync)(opts.workspaceStateDir, { recursive: true });
|
|
4839
5270
|
}
|
|
4840
5271
|
this.cursorPath = (0, import_node_path9.join)(opts.workspaceStateDir, "asset-metadata-cursor.json");
|
|
4841
5272
|
}
|
|
4842
5273
|
/** Persisted cursor for this workspace, or 0 on first run / corrupted file. */
|
|
4843
5274
|
readCursor() {
|
|
4844
|
-
if (!(0,
|
|
5275
|
+
if (!(0, import_node_fs9.existsSync)(this.cursorPath)) return 0;
|
|
4845
5276
|
try {
|
|
4846
|
-
const parsed = JSON.parse((0,
|
|
5277
|
+
const parsed = JSON.parse((0, import_node_fs9.readFileSync)(this.cursorPath, "utf8"));
|
|
4847
5278
|
if (parsed.workspaceId !== this.workspaceId) return 0;
|
|
4848
5279
|
return parsed.cursor;
|
|
4849
5280
|
} catch {
|
|
@@ -4856,7 +5287,7 @@ var AssetMetadataIndex = class {
|
|
|
4856
5287
|
cursor,
|
|
4857
5288
|
writtenAt: Date.now()
|
|
4858
5289
|
};
|
|
4859
|
-
(0,
|
|
5290
|
+
(0, import_node_fs9.writeFileSync)(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
4860
5291
|
}
|
|
4861
5292
|
/**
|
|
4862
5293
|
* Pull incremental asset metadata changes since the persisted cursor and
|
|
@@ -4973,7 +5404,7 @@ var AssetMetadataIndex = class {
|
|
|
4973
5404
|
};
|
|
4974
5405
|
|
|
4975
5406
|
// src/daemon/asset/mirror.ts
|
|
4976
|
-
var
|
|
5407
|
+
var import_node_fs10 = require("fs");
|
|
4977
5408
|
var import_node_path10 = require("path");
|
|
4978
5409
|
function rowToBinding(row) {
|
|
4979
5410
|
return {
|
|
@@ -4995,16 +5426,16 @@ var WorkspaceMirror = class {
|
|
|
4995
5426
|
this.db = opts.db;
|
|
4996
5427
|
this.cloud = opts.cloud;
|
|
4997
5428
|
this.workspaceId = opts.workspaceId;
|
|
4998
|
-
if (!(0,
|
|
4999
|
-
(0,
|
|
5429
|
+
if (!(0, import_node_fs10.existsSync)(opts.workspaceStateDir)) {
|
|
5430
|
+
(0, import_node_fs10.mkdirSync)(opts.workspaceStateDir, { recursive: true });
|
|
5000
5431
|
}
|
|
5001
5432
|
this.cursorPath = (0, import_node_path10.join)(opts.workspaceStateDir, "asset-cursor.json");
|
|
5002
5433
|
}
|
|
5003
5434
|
/** Persisted cursor for this workspace, or null on first run / corrupted file. */
|
|
5004
5435
|
readCursor() {
|
|
5005
|
-
if (!(0,
|
|
5436
|
+
if (!(0, import_node_fs10.existsSync)(this.cursorPath)) return null;
|
|
5006
5437
|
try {
|
|
5007
|
-
const parsed = JSON.parse((0,
|
|
5438
|
+
const parsed = JSON.parse((0, import_node_fs10.readFileSync)(this.cursorPath, "utf8"));
|
|
5008
5439
|
if (parsed.workspaceId !== this.workspaceId) return null;
|
|
5009
5440
|
return parsed.cursor;
|
|
5010
5441
|
} catch {
|
|
@@ -5017,7 +5448,7 @@ var WorkspaceMirror = class {
|
|
|
5017
5448
|
cursor,
|
|
5018
5449
|
writtenAt: Date.now()
|
|
5019
5450
|
};
|
|
5020
|
-
(0,
|
|
5451
|
+
(0, import_node_fs10.writeFileSync)(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
5021
5452
|
}
|
|
5022
5453
|
/**
|
|
5023
5454
|
* Pull workspace_file changes since the persisted cursor and upsert into
|
|
@@ -5261,8 +5692,8 @@ async function prefetchAssetBytes(opts) {
|
|
|
5261
5692
|
return { considered: rows.length, fetched, bytes, failed };
|
|
5262
5693
|
}
|
|
5263
5694
|
async function uploadAssetPath(inputPath, opts) {
|
|
5264
|
-
if (!(0,
|
|
5265
|
-
const stat = (0,
|
|
5695
|
+
if (!(0, import_node_fs11.existsSync)(inputPath)) exitWithError(`path not found: ${inputPath}`);
|
|
5696
|
+
const stat = (0, import_node_fs11.lstatSync)(inputPath);
|
|
5266
5697
|
if (stat.isSymbolicLink()) exitWithError(`refusing to upload symlink: ${inputPath}`);
|
|
5267
5698
|
if (stat.isFile()) {
|
|
5268
5699
|
return uploadAsset(inputPath, { ...opts, folderPath: normalizeFolderPath(opts.folderPath) });
|
|
@@ -5280,9 +5711,9 @@ async function uploadAssetPath(inputPath, opts) {
|
|
|
5280
5711
|
return { ok: true, data: { count: items.length, items } };
|
|
5281
5712
|
}
|
|
5282
5713
|
async function uploadAsset(file, opts) {
|
|
5283
|
-
if (!(0,
|
|
5714
|
+
if (!(0, import_node_fs11.existsSync)(file)) exitWithError(`file not found: ${file}`);
|
|
5284
5715
|
const cfg = loadConfig(resolvePaths());
|
|
5285
|
-
const bytes = (0,
|
|
5716
|
+
const bytes = (0, import_node_fs11.readFileSync)(file);
|
|
5286
5717
|
const contentHash = (0, import_node_crypto2.createHash)("sha256").update(bytes).digest("hex");
|
|
5287
5718
|
const metadata = parseMetadata(opts.metadata);
|
|
5288
5719
|
if (opts.taskId && metadata.taskId === void 0) metadata.taskId = opts.taskId;
|
|
@@ -5418,10 +5849,10 @@ async function putDirectAssetBytes(plan, bytes, mime) {
|
|
|
5418
5849
|
}
|
|
5419
5850
|
function listFilesRecursive(root) {
|
|
5420
5851
|
const out = [];
|
|
5421
|
-
for (const name of (0,
|
|
5852
|
+
for (const name of (0, import_node_fs11.readdirSync)(root)) {
|
|
5422
5853
|
if (name.startsWith(".")) continue;
|
|
5423
5854
|
const full = (0, import_node_path11.join)(root, name);
|
|
5424
|
-
const stat = (0,
|
|
5855
|
+
const stat = (0, import_node_fs11.lstatSync)(full);
|
|
5425
5856
|
if (stat.isSymbolicLink()) continue;
|
|
5426
5857
|
if (stat.isDirectory()) {
|
|
5427
5858
|
out.push(...listFilesRecursive(full));
|
|
@@ -5455,7 +5886,7 @@ async function downloadAsset(assetId, outPath) {
|
|
|
5455
5886
|
exitWithError(`asset download failed (${res.status}): ${errorMessage(body)}`);
|
|
5456
5887
|
}
|
|
5457
5888
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
5458
|
-
(0,
|
|
5889
|
+
(0, import_node_fs11.writeFileSync)(outPath, bytes);
|
|
5459
5890
|
}
|
|
5460
5891
|
function parseMetadata(raw) {
|
|
5461
5892
|
if (!raw) return {};
|
|
@@ -6102,16 +6533,16 @@ function parsePositiveInt3(value) {
|
|
|
6102
6533
|
// src/cli/commands/daemon.ts
|
|
6103
6534
|
var import_commander8 = require("commander");
|
|
6104
6535
|
var import_node_child_process7 = require("child_process");
|
|
6105
|
-
var
|
|
6536
|
+
var import_node_fs20 = require("fs");
|
|
6106
6537
|
var import_promises2 = require("timers/promises");
|
|
6107
6538
|
var import_node_path15 = require("path");
|
|
6108
6539
|
|
|
6109
6540
|
// src/daemon/runner.ts
|
|
6110
6541
|
var import_node_events3 = require("events");
|
|
6111
6542
|
var import_node_module2 = require("module");
|
|
6112
|
-
var
|
|
6543
|
+
var import_node_fs19 = require("fs");
|
|
6113
6544
|
var import_promises = require("fs/promises");
|
|
6114
|
-
var
|
|
6545
|
+
var import_node_os8 = require("os");
|
|
6115
6546
|
var import_node_path14 = require("path");
|
|
6116
6547
|
|
|
6117
6548
|
// src/adapters/registry.ts
|
|
@@ -6589,9 +7020,265 @@ function createLogger(tag) {
|
|
|
6589
7020
|
}
|
|
6590
7021
|
|
|
6591
7022
|
// src/daemon/dispatch.ts
|
|
6592
|
-
var
|
|
7023
|
+
var import_node_fs13 = require("fs");
|
|
6593
7024
|
var path2 = __toESM(require("path"), 1);
|
|
6594
7025
|
init_skill_sync();
|
|
7026
|
+
|
|
7027
|
+
// src/daemon/task-heartbeat.ts
|
|
7028
|
+
var TaskHeartbeat = class {
|
|
7029
|
+
constructor(opts) {
|
|
7030
|
+
this.opts = opts;
|
|
7031
|
+
this.intervalMs = opts.intervalMs ?? 15e3;
|
|
7032
|
+
this.maxRetries = opts.maxRetries ?? 3;
|
|
7033
|
+
this.retryBackoffMs = opts.retryBackoffMs ?? 100;
|
|
7034
|
+
this.now = opts.now ?? Date.now;
|
|
7035
|
+
}
|
|
7036
|
+
opts;
|
|
7037
|
+
timer;
|
|
7038
|
+
version = 0;
|
|
7039
|
+
taskId;
|
|
7040
|
+
getPhase;
|
|
7041
|
+
/** Tracks the last step time pushed via touchStep() — informational. */
|
|
7042
|
+
lastStepAt;
|
|
7043
|
+
intervalMs;
|
|
7044
|
+
maxRetries;
|
|
7045
|
+
retryBackoffMs;
|
|
7046
|
+
now;
|
|
7047
|
+
/**
|
|
7048
|
+
* Begin heartbeating for `taskId`. `getPhase` is invoked at every tick to
|
|
7049
|
+
* pick up the live phase string — adapters can mutate their phase state
|
|
7050
|
+
* freely and the heartbeat will surface it on the next 15s boundary.
|
|
7051
|
+
*
|
|
7052
|
+
* Calling start() twice on the same instance replaces the active task
|
|
7053
|
+
* (previous timer is cleared). Returns silently when already started for
|
|
7054
|
+
* the same taskId.
|
|
7055
|
+
*/
|
|
7056
|
+
start(taskId, getPhase) {
|
|
7057
|
+
if (this.taskId === taskId && this.timer) return;
|
|
7058
|
+
this.stop();
|
|
7059
|
+
this.taskId = taskId;
|
|
7060
|
+
this.getPhase = getPhase;
|
|
7061
|
+
this.timer = setInterval(() => {
|
|
7062
|
+
void this.push();
|
|
7063
|
+
}, this.intervalMs);
|
|
7064
|
+
this.timer.unref?.();
|
|
7065
|
+
}
|
|
7066
|
+
/**
|
|
7067
|
+
* Stop the heartbeat loop. Idempotent.
|
|
7068
|
+
*/
|
|
7069
|
+
stop() {
|
|
7070
|
+
if (this.timer) {
|
|
7071
|
+
clearInterval(this.timer);
|
|
7072
|
+
this.timer = void 0;
|
|
7073
|
+
}
|
|
7074
|
+
this.taskId = void 0;
|
|
7075
|
+
this.getPhase = void 0;
|
|
7076
|
+
}
|
|
7077
|
+
/**
|
|
7078
|
+
* Phase transition — push immediately AND update what the timer-driven
|
|
7079
|
+
* tick will report next time. Adapter calls this whenever the lifecycle
|
|
7080
|
+
* advances (thinking → tool_use → reasoning → responding → ...).
|
|
7081
|
+
*
|
|
7082
|
+
* No-op when no task is active.
|
|
7083
|
+
*/
|
|
7084
|
+
setPhase(phase) {
|
|
7085
|
+
if (!this.taskId) return;
|
|
7086
|
+
this.getPhase = () => phase;
|
|
7087
|
+
void this.push();
|
|
7088
|
+
}
|
|
7089
|
+
/**
|
|
7090
|
+
* Mark "I just made observable progress." Updates lastStepAt; next push
|
|
7091
|
+
* (timer-driven or setPhase-driven) carries it. Lightweight — no immediate
|
|
7092
|
+
* send. Adapters call this around tool calls / token boundaries so the
|
|
7093
|
+
* cloud reaper can distinguish "alive but slow" from "truly stuck".
|
|
7094
|
+
*/
|
|
7095
|
+
touchStep() {
|
|
7096
|
+
this.lastStepAt = this.now();
|
|
7097
|
+
}
|
|
7098
|
+
/**
|
|
7099
|
+
* Manually trigger one heartbeat send (does not affect the timer). Mainly
|
|
7100
|
+
* useful for tests that don't want to await a 15s tick. Returns the
|
|
7101
|
+
* heartbeatVersion that was sent, or undefined if no task is active.
|
|
7102
|
+
*/
|
|
7103
|
+
async forceTick() {
|
|
7104
|
+
if (!this.taskId) return void 0;
|
|
7105
|
+
return this.push();
|
|
7106
|
+
}
|
|
7107
|
+
/** Test helper: current heartbeatVersion (last value sent, may be 0 before first tick). */
|
|
7108
|
+
get currentVersion() {
|
|
7109
|
+
return this.version;
|
|
7110
|
+
}
|
|
7111
|
+
async push() {
|
|
7112
|
+
if (!this.taskId || !this.getPhase) return void 0;
|
|
7113
|
+
const taskId = this.taskId;
|
|
7114
|
+
const phase = this.getPhase();
|
|
7115
|
+
const heartbeatVersion = ++this.version;
|
|
7116
|
+
const payload = {
|
|
7117
|
+
taskId,
|
|
7118
|
+
heartbeatVersion,
|
|
7119
|
+
currentPhase: phase,
|
|
7120
|
+
...this.lastStepAt !== void 0 ? { lastStepAt: this.lastStepAt } : {}
|
|
7121
|
+
};
|
|
7122
|
+
const msg = envelope("task.heartbeat", payload);
|
|
7123
|
+
let lastErr;
|
|
7124
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
|
|
7125
|
+
try {
|
|
7126
|
+
if (this.opts.ws.isOpen && !this.opts.ws.isOpen()) {
|
|
7127
|
+
return heartbeatVersion;
|
|
7128
|
+
}
|
|
7129
|
+
this.opts.ws.send(msg);
|
|
7130
|
+
return heartbeatVersion;
|
|
7131
|
+
} catch (err) {
|
|
7132
|
+
lastErr = err instanceof Error ? err : new Error(String(err));
|
|
7133
|
+
if (attempt < this.maxRetries) {
|
|
7134
|
+
const backoff = this.retryBackoffMs * Math.pow(2, attempt);
|
|
7135
|
+
await sleep(backoff);
|
|
7136
|
+
}
|
|
7137
|
+
}
|
|
7138
|
+
}
|
|
7139
|
+
if (lastErr) {
|
|
7140
|
+
try {
|
|
7141
|
+
this.opts.onGiveUp?.(taskId, lastErr);
|
|
7142
|
+
} catch {
|
|
7143
|
+
}
|
|
7144
|
+
}
|
|
7145
|
+
return heartbeatVersion;
|
|
7146
|
+
}
|
|
7147
|
+
};
|
|
7148
|
+
function sleep(ms) {
|
|
7149
|
+
return new Promise((resolve3) => {
|
|
7150
|
+
const handle = setTimeout(resolve3, ms);
|
|
7151
|
+
handle.unref?.();
|
|
7152
|
+
});
|
|
7153
|
+
}
|
|
7154
|
+
|
|
7155
|
+
// src/daemon/step-recorder.ts
|
|
7156
|
+
var StepRecorder = class {
|
|
7157
|
+
constructor(opts) {
|
|
7158
|
+
this.opts = opts;
|
|
7159
|
+
this.reasoningThrottleMs = opts.reasoningThrottleMs ?? 500;
|
|
7160
|
+
this.now = opts.now ?? Date.now;
|
|
7161
|
+
}
|
|
7162
|
+
opts;
|
|
7163
|
+
seq = 0;
|
|
7164
|
+
reasoningBuf = { texts: [], firstAt: 0 };
|
|
7165
|
+
reasoningTimer;
|
|
7166
|
+
reasoningThrottleMs;
|
|
7167
|
+
now;
|
|
7168
|
+
/** Push a phase transition step. Immediate (no throttle). */
|
|
7169
|
+
recordPhaseChange(phase) {
|
|
7170
|
+
this.emit("phase_change", { phase });
|
|
7171
|
+
}
|
|
7172
|
+
/**
|
|
7173
|
+
* Push a tool-call step. Immediate.
|
|
7174
|
+
*
|
|
7175
|
+
* `toolCallId` is the daemon-side correlation id used to pair with the
|
|
7176
|
+
* subsequent recordToolResult call. Free-form; adapter decides.
|
|
7177
|
+
*/
|
|
7178
|
+
recordToolCall(toolName, input, toolCallId) {
|
|
7179
|
+
this.emit("tool_call", {
|
|
7180
|
+
toolName,
|
|
7181
|
+
inputSummary: summarize2(input),
|
|
7182
|
+
...toolCallId ? { toolCallId } : {}
|
|
7183
|
+
});
|
|
7184
|
+
}
|
|
7185
|
+
/** Push a tool-result step. Immediate. */
|
|
7186
|
+
recordToolResult(toolCallId, output) {
|
|
7187
|
+
this.emit("tool_result", {
|
|
7188
|
+
toolCallId,
|
|
7189
|
+
outputSummary: summarize2(output)
|
|
7190
|
+
});
|
|
7191
|
+
}
|
|
7192
|
+
/**
|
|
7193
|
+
* Buffer a reasoning chunk for batched dispatch. Multiple chunks within
|
|
7194
|
+
* `reasoningThrottleMs` are concatenated and emitted as a single
|
|
7195
|
+
* `reasoning_chunk` step when the timer fires.
|
|
7196
|
+
*/
|
|
7197
|
+
recordReasoningChunk(text) {
|
|
7198
|
+
if (!text) return;
|
|
7199
|
+
if (this.reasoningBuf.texts.length === 0) {
|
|
7200
|
+
this.reasoningBuf.firstAt = this.now();
|
|
7201
|
+
}
|
|
7202
|
+
this.reasoningBuf.texts.push(text);
|
|
7203
|
+
if (this.reasoningTimer) return;
|
|
7204
|
+
this.reasoningTimer = setTimeout(() => {
|
|
7205
|
+
this.flushReasoning();
|
|
7206
|
+
}, this.reasoningThrottleMs);
|
|
7207
|
+
this.reasoningTimer.unref?.();
|
|
7208
|
+
}
|
|
7209
|
+
/** Push an error step. Immediate. */
|
|
7210
|
+
recordError(message, payload) {
|
|
7211
|
+
this.emit("error", { message, ...payload ?? {} });
|
|
7212
|
+
}
|
|
7213
|
+
/**
|
|
7214
|
+
* Force any pending reasoning buffer out the door. Call on shutdown so
|
|
7215
|
+
* trailing tokens aren't lost.
|
|
7216
|
+
*/
|
|
7217
|
+
flush() {
|
|
7218
|
+
this.flushReasoning();
|
|
7219
|
+
}
|
|
7220
|
+
/** Test helper. */
|
|
7221
|
+
get currentSeq() {
|
|
7222
|
+
return this.seq;
|
|
7223
|
+
}
|
|
7224
|
+
flushReasoning() {
|
|
7225
|
+
if (this.reasoningTimer) {
|
|
7226
|
+
clearTimeout(this.reasoningTimer);
|
|
7227
|
+
this.reasoningTimer = void 0;
|
|
7228
|
+
}
|
|
7229
|
+
if (this.reasoningBuf.texts.length === 0) return;
|
|
7230
|
+
const text = this.reasoningBuf.texts.join("");
|
|
7231
|
+
const firstAt = this.reasoningBuf.firstAt;
|
|
7232
|
+
this.reasoningBuf = { texts: [], firstAt: 0 };
|
|
7233
|
+
this.emit("reasoning_chunk", { text }, firstAt);
|
|
7234
|
+
}
|
|
7235
|
+
emit(kind, payload, occurredAt) {
|
|
7236
|
+
const frame = {
|
|
7237
|
+
seq: ++this.seq,
|
|
7238
|
+
kind,
|
|
7239
|
+
payload,
|
|
7240
|
+
occurredAt: occurredAt ?? this.now()
|
|
7241
|
+
};
|
|
7242
|
+
const msg = envelope("task.step.append", {
|
|
7243
|
+
taskRunId: this.opts.taskRunId,
|
|
7244
|
+
step: frame
|
|
7245
|
+
});
|
|
7246
|
+
try {
|
|
7247
|
+
if (this.opts.ws.isOpen && !this.opts.ws.isOpen()) {
|
|
7248
|
+
try {
|
|
7249
|
+
this.opts.onSend?.(frame);
|
|
7250
|
+
} catch {
|
|
7251
|
+
}
|
|
7252
|
+
return;
|
|
7253
|
+
}
|
|
7254
|
+
this.opts.ws.send(msg);
|
|
7255
|
+
try {
|
|
7256
|
+
this.opts.onSend?.(frame);
|
|
7257
|
+
} catch {
|
|
7258
|
+
}
|
|
7259
|
+
} catch {
|
|
7260
|
+
}
|
|
7261
|
+
}
|
|
7262
|
+
};
|
|
7263
|
+
var MAX_SUMMARY_CHARS = 512;
|
|
7264
|
+
function summarize2(value) {
|
|
7265
|
+
let text;
|
|
7266
|
+
if (value == null) text = "";
|
|
7267
|
+
else if (typeof value === "string") text = value;
|
|
7268
|
+
else {
|
|
7269
|
+
try {
|
|
7270
|
+
text = JSON.stringify(value);
|
|
7271
|
+
} catch {
|
|
7272
|
+
text = String(value);
|
|
7273
|
+
}
|
|
7274
|
+
}
|
|
7275
|
+
if (text.length > MAX_SUMMARY_CHARS) {
|
|
7276
|
+
return `${text.slice(0, MAX_SUMMARY_CHARS)}\u2026(+${text.length - MAX_SUMMARY_CHARS} chars)`;
|
|
7277
|
+
}
|
|
7278
|
+
return text;
|
|
7279
|
+
}
|
|
7280
|
+
|
|
7281
|
+
// src/daemon/dispatch.ts
|
|
6595
7282
|
var DEFAULT_CONTEXT_MAX = 8e3;
|
|
6596
7283
|
var GOAL_CONTEXT_MAX = 4;
|
|
6597
7284
|
var MEMORY_DIGEST_MAX_BYTES = 4e3;
|
|
@@ -6607,12 +7294,14 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6607
7294
|
const { taskId, agentImUserId } = payload;
|
|
6608
7295
|
let resolvedHashes = [];
|
|
6609
7296
|
let reply;
|
|
7297
|
+
let heartbeatRef;
|
|
7298
|
+
let recorderRef;
|
|
6610
7299
|
const outboxBase = deps.paths?.runsDir ? path2.join(deps.paths.runsDir, taskId) : null;
|
|
6611
7300
|
const outboxDir = outboxBase ? path2.join(outboxBase, "_outbox") : null;
|
|
6612
7301
|
const workDir = outboxBase ? path2.join(outboxBase, "workdir") : null;
|
|
6613
7302
|
if (outboxDir) {
|
|
6614
7303
|
try {
|
|
6615
|
-
await
|
|
7304
|
+
await import_node_fs13.promises.mkdir(outboxDir, { recursive: true });
|
|
6616
7305
|
} catch (err) {
|
|
6617
7306
|
process.stderr.write(
|
|
6618
7307
|
`[daemon] outbox provision failed task=${taskId} dir=${outboxDir}: ${err.message}
|
|
@@ -6622,7 +7311,7 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6622
7311
|
}
|
|
6623
7312
|
if (workDir) {
|
|
6624
7313
|
try {
|
|
6625
|
-
await
|
|
7314
|
+
await import_node_fs13.promises.mkdir(workDir, { recursive: true });
|
|
6626
7315
|
} catch (err) {
|
|
6627
7316
|
process.stderr.write(
|
|
6628
7317
|
`[daemon] workdir provision failed task=${taskId} dir=${workDir}: ${err.message}
|
|
@@ -6688,7 +7377,13 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6688
7377
|
resolvedHashes.push(...r.resolvedHashes);
|
|
6689
7378
|
rewrittenContext = payload.context.map((e, i) => ({ ...e, content: r.texts[i] }));
|
|
6690
7379
|
}
|
|
6691
|
-
const assetResolution = await resolveAssetRefs(payload.assetRefs, deps.assetCache, taskId
|
|
7380
|
+
const assetResolution = await resolveAssetRefs(payload.assetRefs, deps.assetCache, taskId, {
|
|
7381
|
+
cloud: deps.cloud,
|
|
7382
|
+
enabled: deps.visionAux?.enabled !== false,
|
|
7383
|
+
cacheDir: deps.visionAux?.cacheDir ?? (deps.paths ? path2.join(deps.paths.cacheDir, "vision-cache") : void 0),
|
|
7384
|
+
timeoutMs: deps.visionAux?.timeoutMs,
|
|
7385
|
+
signal: deps.signal
|
|
7386
|
+
});
|
|
6692
7387
|
resolvedHashes.push(...assetResolution.pinnedHashes);
|
|
6693
7388
|
const basePrompt = composePrompt(
|
|
6694
7389
|
rewrittenPrompt.text,
|
|
@@ -6723,10 +7418,45 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6723
7418
|
}
|
|
6724
7419
|
const operatingPrinciples = resolveOperatingPrinciples(profile.config);
|
|
6725
7420
|
const composedSystemPrompt = [profileSystemPrompt, operatingPrinciples].filter((part) => typeof part === "string" && part.length > 0).join("\n\n");
|
|
7421
|
+
let activePhase = "thinking";
|
|
7422
|
+
const heartbeat = new TaskHeartbeat({
|
|
7423
|
+
ws: deps.ws,
|
|
7424
|
+
onGiveUp: (tid, err) => process.stderr.write(
|
|
7425
|
+
`[daemon] task.heartbeat give-up task=${tid}: ${err.message}
|
|
7426
|
+
`
|
|
7427
|
+
)
|
|
7428
|
+
});
|
|
7429
|
+
heartbeatRef = heartbeat;
|
|
7430
|
+
heartbeat.start(taskId, () => activePhase);
|
|
7431
|
+
const recorder = new StepRecorder({ ws: deps.ws, taskRunId: taskId });
|
|
7432
|
+
recorderRef = recorder;
|
|
7433
|
+
recorder.recordPhaseChange(activePhase);
|
|
6726
7434
|
const taskInput = {
|
|
6727
7435
|
taskId,
|
|
6728
7436
|
prompt: adapterPrompt,
|
|
6729
|
-
|
|
7437
|
+
heartbeat: {
|
|
7438
|
+
setPhase: (phase) => {
|
|
7439
|
+
activePhase = phase;
|
|
7440
|
+
heartbeat.setPhase(phase);
|
|
7441
|
+
recorder.recordPhaseChange(phase);
|
|
7442
|
+
},
|
|
7443
|
+
touchStep: () => heartbeat.touchStep()
|
|
7444
|
+
},
|
|
7445
|
+
recorder: {
|
|
7446
|
+
recordPhaseChange: (phase) => {
|
|
7447
|
+
activePhase = phase;
|
|
7448
|
+
heartbeat.setPhase(phase);
|
|
7449
|
+
recorder.recordPhaseChange(phase);
|
|
7450
|
+
},
|
|
7451
|
+
recordToolCall: (toolName, input, toolCallId) => recorder.recordToolCall(toolName, input, toolCallId),
|
|
7452
|
+
recordToolResult: (toolCallId, output) => recorder.recordToolResult(toolCallId, output),
|
|
7453
|
+
recordReasoningChunk: (text) => recorder.recordReasoningChunk(text),
|
|
7454
|
+
recordError: (message, payload2) => recorder.recordError(message, payload2)
|
|
7455
|
+
},
|
|
7456
|
+
// 14b rev.3 §9 P3 — multimodal-aware adapters (Hermes, OpenClaw) pick
|
|
7457
|
+
// up the resolved bytes/URLs here; text-only adapters ignore the field.
|
|
7458
|
+
...assetResolution.resolvedRefs.length > 0 ? { assetRefs: assetResolution.resolvedRefs } : {},
|
|
7459
|
+
metadata: {
|
|
6730
7460
|
...payload.metadata,
|
|
6731
7461
|
conversationId: payload.conversationId,
|
|
6732
7462
|
prismerGoals: goalContext.map(toGoalMirrorPayload),
|
|
@@ -6788,6 +7518,9 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6788
7518
|
}
|
|
6789
7519
|
}
|
|
6790
7520
|
const reaperAborted = deps.signal?.aborted && result.error?.code === "task_cancelled";
|
|
7521
|
+
const approvalRequested = Boolean(
|
|
7522
|
+
result.metadata?.approvalRequested
|
|
7523
|
+
);
|
|
6791
7524
|
let collectedAssetIds = [];
|
|
6792
7525
|
if (deps.outboxWatcher && outboxDir) {
|
|
6793
7526
|
try {
|
|
@@ -6799,14 +7532,23 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6799
7532
|
collectedAssetIds = deps.outboxWatcher.flushPending(taskId);
|
|
6800
7533
|
}
|
|
6801
7534
|
const reaperLimitMs = typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? payload.timeoutMs : 5 * 6e4;
|
|
7535
|
+
const finalError = approvalRequested ? {
|
|
7536
|
+
code: "awaiting_human_approval",
|
|
7537
|
+
message: "Agent requested human approval and is suspended pending decision. Cloud will redispatch with `approval.decided` once the human responds."
|
|
7538
|
+
} : reaperAborted ? {
|
|
7539
|
+
code: "daemon_task_timeout",
|
|
7540
|
+
message: `Daemon-side reaper aborted after ${reaperLimitMs}ms inactivity (no progress events from adapter)`
|
|
7541
|
+
} : result.error;
|
|
6802
7542
|
reply = {
|
|
6803
7543
|
taskId,
|
|
7544
|
+
// When approval is pending we keep ok=false (the run did not produce a
|
|
7545
|
+
// completion-style output) but the cloud-side handler treats the
|
|
7546
|
+
// `awaiting_human_approval` code as non-terminal — task stays alive,
|
|
7547
|
+
// no red failure pill, and the next dispatch (after the human decides)
|
|
7548
|
+
// continues normally.
|
|
6804
7549
|
ok: result.ok,
|
|
6805
7550
|
output: result.output,
|
|
6806
|
-
error:
|
|
6807
|
-
code: "daemon_task_timeout",
|
|
6808
|
-
message: `Daemon-side reaper aborted after ${reaperLimitMs}ms inactivity (no progress events from adapter)`
|
|
6809
|
-
} : result.error,
|
|
7551
|
+
error: finalError,
|
|
6810
7552
|
...collectedAssetIds.length > 0 ? { assetIds: collectedAssetIds } : {},
|
|
6811
7553
|
metrics: result.metrics,
|
|
6812
7554
|
...assetResolution.observability.length > 0 || urlObservations.length > 0 ? { assetObservability: [...assetResolution.observability, ...urlObservations] } : {}
|
|
@@ -6828,6 +7570,18 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
6828
7570
|
}
|
|
6829
7571
|
};
|
|
6830
7572
|
} finally {
|
|
7573
|
+
try {
|
|
7574
|
+
recorderRef?.flush();
|
|
7575
|
+
} catch (err) {
|
|
7576
|
+
process.stderr.write(`[daemon] step recorder flush failed task=${taskId}: ${err.message}
|
|
7577
|
+
`);
|
|
7578
|
+
}
|
|
7579
|
+
try {
|
|
7580
|
+
heartbeatRef?.stop();
|
|
7581
|
+
} catch (err) {
|
|
7582
|
+
process.stderr.write(`[daemon] heartbeat stop failed task=${taskId}: ${err.message}
|
|
7583
|
+
`);
|
|
7584
|
+
}
|
|
6831
7585
|
for (const hash of new Set(resolvedHashes)) {
|
|
6832
7586
|
try {
|
|
6833
7587
|
deps.assetCache.unpin(hash);
|
|
@@ -6904,6 +7658,10 @@ function isTextLikeMime(mime) {
|
|
|
6904
7658
|
if (m.endsWith("+json") || m.endsWith("+xml") || m.endsWith("+csv")) return true;
|
|
6905
7659
|
return false;
|
|
6906
7660
|
}
|
|
7661
|
+
function isImageMime(mime) {
|
|
7662
|
+
if (!mime) return false;
|
|
7663
|
+
return mime.toLowerCase().split(";")[0].trim().startsWith("image/");
|
|
7664
|
+
}
|
|
6907
7665
|
var HASH_REF_RE = /(?:^|\s)#([^\s#]+)/g;
|
|
6908
7666
|
var HEX_COLOR_RE = /^[0-9a-fA-F]{3,8}$/;
|
|
6909
7667
|
var FILE_EXT_RE = /\.[a-zA-Z0-9]{1,10}$/;
|
|
@@ -6982,8 +7740,34 @@ async function resolveHashRefs(prompt, assetIndex, cloud) {
|
|
|
6982
7740
|
}
|
|
6983
7741
|
return { text: result, resolutions };
|
|
6984
7742
|
}
|
|
6985
|
-
|
|
6986
|
-
|
|
7743
|
+
var ASSET_BASE64_INLINE_MAX_BYTES = 10 * 1024 * 1024;
|
|
7744
|
+
async function probeCdnReachable(cdnUrl, timeoutMs = 1500) {
|
|
7745
|
+
try {
|
|
7746
|
+
const u = new URL(cdnUrl);
|
|
7747
|
+
if (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname.startsWith("192.168.")) {
|
|
7748
|
+
return false;
|
|
7749
|
+
}
|
|
7750
|
+
} catch {
|
|
7751
|
+
return false;
|
|
7752
|
+
}
|
|
7753
|
+
try {
|
|
7754
|
+
const r = await fetch(cdnUrl, {
|
|
7755
|
+
method: "HEAD",
|
|
7756
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
7757
|
+
});
|
|
7758
|
+
if (r.ok) return true;
|
|
7759
|
+
return false;
|
|
7760
|
+
} catch {
|
|
7761
|
+
return false;
|
|
7762
|
+
}
|
|
7763
|
+
}
|
|
7764
|
+
async function resolveAssetRefs(refs, cache, taskId, visionAux) {
|
|
7765
|
+
const out = {
|
|
7766
|
+
promptBlocks: [],
|
|
7767
|
+
observability: [],
|
|
7768
|
+
pinnedHashes: [],
|
|
7769
|
+
resolvedRefs: []
|
|
7770
|
+
};
|
|
6987
7771
|
if (!refs || refs.length === 0) return out;
|
|
6988
7772
|
for (const ref of refs) {
|
|
6989
7773
|
let cached;
|
|
@@ -7012,7 +7796,7 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
7012
7796
|
const inlineCandidate = isTextLikeMime(effectiveMime);
|
|
7013
7797
|
if (inlineCandidate) {
|
|
7014
7798
|
try {
|
|
7015
|
-
const buf = (0,
|
|
7799
|
+
const buf = (0, import_node_fs13.readFileSync)(cached.localPath);
|
|
7016
7800
|
let strategy;
|
|
7017
7801
|
let body;
|
|
7018
7802
|
if (buf.byteLength <= ASSET_INLINE_FULL_MAX_BYTES) {
|
|
@@ -7048,7 +7832,42 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
7048
7832
|
continue;
|
|
7049
7833
|
}
|
|
7050
7834
|
}
|
|
7051
|
-
|
|
7835
|
+
let reachable = "unknown";
|
|
7836
|
+
let base64;
|
|
7837
|
+
if (ref.cdnUrl) {
|
|
7838
|
+
const ok2 = await probeCdnReachable(ref.cdnUrl);
|
|
7839
|
+
if (ok2) {
|
|
7840
|
+
reachable = "cdn";
|
|
7841
|
+
}
|
|
7842
|
+
}
|
|
7843
|
+
if (reachable !== "cdn") {
|
|
7844
|
+
try {
|
|
7845
|
+
const buf = (0, import_node_fs13.readFileSync)(cached.localPath);
|
|
7846
|
+
if (buf.byteLength <= ASSET_BASE64_INLINE_MAX_BYTES) {
|
|
7847
|
+
base64 = buf.toString("base64");
|
|
7848
|
+
reachable = "base64";
|
|
7849
|
+
} else {
|
|
7850
|
+
reachable = "unknown";
|
|
7851
|
+
}
|
|
7852
|
+
} catch (err) {
|
|
7853
|
+
const errMsg = `base64 fallback read failed: ${err.message}`;
|
|
7854
|
+
logAssetResolveFailure(taskId, ref, errMsg, effectiveMime);
|
|
7855
|
+
}
|
|
7856
|
+
}
|
|
7857
|
+
const resolvedRef = {
|
|
7858
|
+
...ref,
|
|
7859
|
+
mime: effectiveMime,
|
|
7860
|
+
localPath: cached.localPath,
|
|
7861
|
+
base64,
|
|
7862
|
+
reachable
|
|
7863
|
+
};
|
|
7864
|
+
out.resolvedRefs.push(resolvedRef);
|
|
7865
|
+
if (isImageMime(effectiveMime) && visionAux?.enabled) {
|
|
7866
|
+
const block = await describeImageAttachment(ref, effectiveMime, resolvedRef, visionAux, taskId);
|
|
7867
|
+
out.promptBlocks.push(block);
|
|
7868
|
+
} else {
|
|
7869
|
+
out.promptBlocks.push(formatAttachmentReminderBlock(ref, effectiveMime));
|
|
7870
|
+
}
|
|
7052
7871
|
out.observability.push({
|
|
7053
7872
|
assetId: ref.assetId,
|
|
7054
7873
|
contentHash: ref.contentHash,
|
|
@@ -7059,6 +7878,110 @@ async function resolveAssetRefs(refs, cache, taskId) {
|
|
|
7059
7878
|
}
|
|
7060
7879
|
return out;
|
|
7061
7880
|
}
|
|
7881
|
+
async function describeImageAttachment(ref, mime, resolved, opts, taskId) {
|
|
7882
|
+
const cached = await readVisionAuxLocalCache(opts.cacheDir, ref.contentHash, mime);
|
|
7883
|
+
if (cached) return formatVisionAuxBlock(ref, mime, cached.description, true);
|
|
7884
|
+
const source = buildVisionAuxSource(resolved, mime);
|
|
7885
|
+
if (!source) {
|
|
7886
|
+
return formatVisionAuxUnavailableBlock(ref, mime, "no reachable URL or inline bytes");
|
|
7887
|
+
}
|
|
7888
|
+
try {
|
|
7889
|
+
const description = await callVisionAux(ref, mime, source, opts);
|
|
7890
|
+
await writeVisionAuxLocalCache(opts.cacheDir, ref.contentHash, mime, description);
|
|
7891
|
+
return formatVisionAuxBlock(ref, mime, description.description, false);
|
|
7892
|
+
} catch (err) {
|
|
7893
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7894
|
+
process.stderr.write(
|
|
7895
|
+
`[daemon] vision-aux failed task=${taskId ?? "unknown"} assetId=${ref.assetId} hash=${ref.contentHash}: ${message}
|
|
7896
|
+
`
|
|
7897
|
+
);
|
|
7898
|
+
return formatVisionAuxUnavailableBlock(ref, mime, message);
|
|
7899
|
+
}
|
|
7900
|
+
}
|
|
7901
|
+
function buildVisionAuxSource(resolved, mime) {
|
|
7902
|
+
if (resolved.reachable === "cdn" && resolved.cdnUrl) return { kind: "url", url: resolved.cdnUrl };
|
|
7903
|
+
if (resolved.base64 && mime) return { kind: "data_url", url: `data:${mime};base64,${resolved.base64}` };
|
|
7904
|
+
return null;
|
|
7905
|
+
}
|
|
7906
|
+
async function callVisionAux(ref, mime, source, opts) {
|
|
7907
|
+
const secret = process.env.VISION_AUX_INTERNAL_SECRET || process.env.INTERNAL_API_SECRET || process.env.PRISMER_INTERNAL_SECRET;
|
|
7908
|
+
const headers = secret ? { "x-prismer-internal-secret": secret } : void 0;
|
|
7909
|
+
const res = await opts.cloud.request(
|
|
7910
|
+
"POST",
|
|
7911
|
+
"/api/internal/vision-aux/describe",
|
|
7912
|
+
{
|
|
7913
|
+
body: {
|
|
7914
|
+
assetId: ref.assetId,
|
|
7915
|
+
contentHash: ref.contentHash,
|
|
7916
|
+
mime: mime ?? ref.mime ?? "image/unknown",
|
|
7917
|
+
source
|
|
7918
|
+
},
|
|
7919
|
+
headers,
|
|
7920
|
+
timeoutMs: opts.timeoutMs ?? 12e3,
|
|
7921
|
+
signal: opts.signal
|
|
7922
|
+
}
|
|
7923
|
+
);
|
|
7924
|
+
const env = res.data;
|
|
7925
|
+
if (!res.ok || env?.ok === false || !env?.data?.description) {
|
|
7926
|
+
const message = res.error?.message || (typeof env?.error === "string" ? env.error : env?.error?.message) || env?.message || `HTTP ${res.status}`;
|
|
7927
|
+
throw new Error(message);
|
|
7928
|
+
}
|
|
7929
|
+
const now = /* @__PURE__ */ new Date();
|
|
7930
|
+
const ttlSec = Number.isFinite(env.data.cacheTtlSec) ? Number(env.data.cacheTtlSec) : 300;
|
|
7931
|
+
return {
|
|
7932
|
+
description: env.data.description,
|
|
7933
|
+
modelUsed: env.data.modelUsed || "unknown",
|
|
7934
|
+
provider: env.data.provider || "vision-aux",
|
|
7935
|
+
generatedAt: now.toISOString(),
|
|
7936
|
+
expiresAt: new Date(now.getTime() + Math.max(1, ttlSec) * 1e3).toISOString(),
|
|
7937
|
+
mime: mime ?? ref.mime ?? "image/unknown"
|
|
7938
|
+
};
|
|
7939
|
+
}
|
|
7940
|
+
async function readVisionAuxLocalCache(cacheDir, contentHash, mime) {
|
|
7941
|
+
if (!cacheDir) return null;
|
|
7942
|
+
const file = visionAuxCachePath(cacheDir, contentHash);
|
|
7943
|
+
try {
|
|
7944
|
+
const raw = await import_node_fs13.promises.readFile(file, "utf8");
|
|
7945
|
+
const parsed = parseVisionAuxCache(raw);
|
|
7946
|
+
if (!parsed) {
|
|
7947
|
+
await import_node_fs13.promises.unlink(file).catch(() => void 0);
|
|
7948
|
+
return null;
|
|
7949
|
+
}
|
|
7950
|
+
if (parsed.mime !== mime) return null;
|
|
7951
|
+
const expiresAt = Date.parse(parsed.expiresAt);
|
|
7952
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) return null;
|
|
7953
|
+
return parsed;
|
|
7954
|
+
} catch (err) {
|
|
7955
|
+
if (err.code !== "ENOENT") {
|
|
7956
|
+
await import_node_fs13.promises.unlink(file).catch(() => void 0);
|
|
7957
|
+
}
|
|
7958
|
+
return null;
|
|
7959
|
+
}
|
|
7960
|
+
}
|
|
7961
|
+
async function writeVisionAuxLocalCache(cacheDir, contentHash, mime, description) {
|
|
7962
|
+
if (!cacheDir || !mime) return;
|
|
7963
|
+
const file = visionAuxCachePath(cacheDir, contentHash);
|
|
7964
|
+
await import_node_fs13.promises.mkdir(path2.dirname(file), { recursive: true });
|
|
7965
|
+
await import_node_fs13.promises.writeFile(file, JSON.stringify({ ...description, mime }, null, 2), "utf8");
|
|
7966
|
+
}
|
|
7967
|
+
function visionAuxCachePath(cacheDir, contentHash) {
|
|
7968
|
+
const safeHash = /^[a-f0-9]{32,128}$/i.test(contentHash) ? contentHash : createSafeCacheKey(contentHash);
|
|
7969
|
+
return path2.join(cacheDir, `${safeHash}.json`);
|
|
7970
|
+
}
|
|
7971
|
+
function createSafeCacheKey(value) {
|
|
7972
|
+
return Buffer.from(value).toString("base64url").slice(0, 128) || "unknown";
|
|
7973
|
+
}
|
|
7974
|
+
function parseVisionAuxCache(raw) {
|
|
7975
|
+
try {
|
|
7976
|
+
const parsed = JSON.parse(raw);
|
|
7977
|
+
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") {
|
|
7978
|
+
return parsed;
|
|
7979
|
+
}
|
|
7980
|
+
} catch {
|
|
7981
|
+
return null;
|
|
7982
|
+
}
|
|
7983
|
+
return null;
|
|
7984
|
+
}
|
|
7062
7985
|
function logAssetResolveFailure(taskId, ref, error, mime = ref.mime) {
|
|
7063
7986
|
const message = error.replace(/\s+/g, " ").slice(0, 500);
|
|
7064
7987
|
process.stderr.write(
|
|
@@ -7073,8 +7996,19 @@ function formatInlineAssetBlock(ref, mime, body, strategy) {
|
|
|
7073
7996
|
${body}
|
|
7074
7997
|
---`;
|
|
7075
7998
|
}
|
|
7076
|
-
function
|
|
7077
|
-
|
|
7999
|
+
function formatAttachmentReminderBlock(ref, mime) {
|
|
8000
|
+
const name = ref.filename ? ` name=${ref.filename}` : "";
|
|
8001
|
+
return `[Attached file] id=${ref.assetId} mime=${mime ?? "unknown"}${name} \u2014 the user uploaded this file; multimodal adapters receive the bytes directly.`;
|
|
8002
|
+
}
|
|
8003
|
+
function formatVisionAuxBlock(ref, mime, description, cached) {
|
|
8004
|
+
const name = ref.filename ? `: ${ref.filename}` : "";
|
|
8005
|
+
const cacheLabel = cached ? " local-cache" : "";
|
|
8006
|
+
return `[Image attachment${name}] id=${ref.assetId} mime=${mime ?? "unknown"}${cacheLabel}
|
|
8007
|
+
${description.trim()}`;
|
|
8008
|
+
}
|
|
8009
|
+
function formatVisionAuxUnavailableBlock(ref, mime, reason) {
|
|
8010
|
+
const name = ref.filename ? `: ${ref.filename}` : "";
|
|
8011
|
+
return `[Image attachment${name}; description unavailable due to vision-aux error] id=${ref.assetId} mime=${mime ?? "unknown"} reason=${reason.slice(0, 180)}`;
|
|
7078
8012
|
}
|
|
7079
8013
|
function formatErrorAssetBlock(ref, mime, error) {
|
|
7080
8014
|
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.`;
|
|
@@ -7414,8 +8348,9 @@ function updatedTime(task) {
|
|
|
7414
8348
|
// src/daemon/local-server.ts
|
|
7415
8349
|
var import_node_http = require("http");
|
|
7416
8350
|
var import_node_crypto4 = require("crypto");
|
|
7417
|
-
var
|
|
8351
|
+
var import_node_fs14 = require("fs");
|
|
7418
8352
|
var path3 = __toESM(require("path"), 1);
|
|
8353
|
+
var import_node_os5 = require("os");
|
|
7419
8354
|
|
|
7420
8355
|
// src/daemon/message-dispatch.ts
|
|
7421
8356
|
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
@@ -7626,6 +8561,11 @@ var LocalServer = class {
|
|
|
7626
8561
|
void this.handleSnapshot(req, res);
|
|
7627
8562
|
return;
|
|
7628
8563
|
}
|
|
8564
|
+
const agentDumpMatch = /^\/v1\/agents\/([^/]+)\/dump-state$/.exec(url);
|
|
8565
|
+
if (req.method === "POST" && agentDumpMatch) {
|
|
8566
|
+
void this.handleAgentDumpState(req, res, decodeURIComponent(agentDumpMatch[1]));
|
|
8567
|
+
return;
|
|
8568
|
+
}
|
|
7629
8569
|
if (req.method === "POST" && url === "/local/asset/write") {
|
|
7630
8570
|
void this.handleAssetWrite(req, res);
|
|
7631
8571
|
return;
|
|
@@ -7676,7 +8616,7 @@ var LocalServer = class {
|
|
|
7676
8616
|
async handleSnapshot(req, res) {
|
|
7677
8617
|
const root = this.opts.snapshotRoot ?? "/workspace";
|
|
7678
8618
|
try {
|
|
7679
|
-
const st = await
|
|
8619
|
+
const st = await import_node_fs14.promises.stat(root);
|
|
7680
8620
|
if (!st.isDirectory()) {
|
|
7681
8621
|
respond(res, 400, { error: "snapshot_root_not_directory", rootPath: root });
|
|
7682
8622
|
return;
|
|
@@ -7703,6 +8643,54 @@ var LocalServer = class {
|
|
|
7703
8643
|
}
|
|
7704
8644
|
void req;
|
|
7705
8645
|
}
|
|
8646
|
+
/**
|
|
8647
|
+
* POST /v1/agents/:agentId/dump-state — agent-scoped daemon manifest.
|
|
8648
|
+
*
|
|
8649
|
+
* This is narrower than `/v1/snapshot`: it walks only the current agent's
|
|
8650
|
+
* profile/work dirs so cloud can attach the result to IMAgentSnapshot without
|
|
8651
|
+
* capturing unrelated agents hosted in the same daemon/container.
|
|
8652
|
+
*/
|
|
8653
|
+
async handleAgentDumpState(req, res, agentId) {
|
|
8654
|
+
const state = this.opts.getState();
|
|
8655
|
+
const agent = state.hostedAgents.find((item) => item.imUserId === agentId);
|
|
8656
|
+
if (!agent) {
|
|
8657
|
+
respond(res, 404, { error: "agent_not_hosted", agentId });
|
|
8658
|
+
return;
|
|
8659
|
+
}
|
|
8660
|
+
const roots = getAgentStateRoots(agent, this.opts.snapshotRoot ?? "/workspace");
|
|
8661
|
+
try {
|
|
8662
|
+
const manifests = await Promise.all(
|
|
8663
|
+
roots.map(async (root) => {
|
|
8664
|
+
const stat = await statOptional(root.rootPath);
|
|
8665
|
+
if (!stat) return { ...root, exists: false, files: [] };
|
|
8666
|
+
if (!stat.isDirectory()) return { ...root, exists: true, error: "not_directory", files: [] };
|
|
8667
|
+
return { ...root, exists: true, files: await walkAndDigest(root.rootPath, root.rootPath) };
|
|
8668
|
+
})
|
|
8669
|
+
);
|
|
8670
|
+
const files = manifests.flatMap(
|
|
8671
|
+
(manifest) => manifest.files.map((file) => ({
|
|
8672
|
+
...file,
|
|
8673
|
+
path: `${manifest.kind}/${file.path}`,
|
|
8674
|
+
rootKind: manifest.kind,
|
|
8675
|
+
rootPath: manifest.rootPath
|
|
8676
|
+
}))
|
|
8677
|
+
);
|
|
8678
|
+
respond(res, 200, {
|
|
8679
|
+
agentId,
|
|
8680
|
+
adapterName: agent.adapterName,
|
|
8681
|
+
dumpedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8682
|
+
roots: manifests,
|
|
8683
|
+
files
|
|
8684
|
+
});
|
|
8685
|
+
} catch (err) {
|
|
8686
|
+
respond(res, 500, {
|
|
8687
|
+
error: "agent_dump_state_failed",
|
|
8688
|
+
agentId,
|
|
8689
|
+
message: err instanceof Error ? err.message : String(err)
|
|
8690
|
+
});
|
|
8691
|
+
}
|
|
8692
|
+
void req;
|
|
8693
|
+
}
|
|
7706
8694
|
/**
|
|
7707
8695
|
* POST /local/asset/write — agent-gen adapter RPC.
|
|
7708
8696
|
*
|
|
@@ -7974,11 +8962,44 @@ function validateAgentDispatchRequest(body) {
|
|
|
7974
8962
|
if (payload.attachments != null && !Array.isArray(payload.attachments)) return "attachments must be an array";
|
|
7975
8963
|
return null;
|
|
7976
8964
|
}
|
|
8965
|
+
function getAgentStateRoots(agent, snapshotRoot) {
|
|
8966
|
+
const profileName = sanitizeProfileName(agent.name || agent.imUserId);
|
|
8967
|
+
const roots = [
|
|
8968
|
+
{
|
|
8969
|
+
kind: "workspace-agent",
|
|
8970
|
+
rootPath: path3.join(snapshotRoot, "agents", agent.imUserId)
|
|
8971
|
+
}
|
|
8972
|
+
];
|
|
8973
|
+
if (agent.adapterName === "hermes" || agent.adapterName === "claude-code") {
|
|
8974
|
+
roots.unshift({
|
|
8975
|
+
kind: "hermes-profile",
|
|
8976
|
+
rootPath: path3.join(process.env.HERMES_HOME || path3.join((0, import_node_os5.homedir)(), ".hermes"), "profiles", profileName)
|
|
8977
|
+
});
|
|
8978
|
+
}
|
|
8979
|
+
if (agent.adapterName === "openclaw") {
|
|
8980
|
+
roots.unshift({
|
|
8981
|
+
kind: "openclaw-profile",
|
|
8982
|
+
rootPath: path3.join(process.env.OPENCLAW_HOME || path3.join((0, import_node_os5.homedir)(), ".openclaw"), "profiles", profileName)
|
|
8983
|
+
});
|
|
8984
|
+
}
|
|
8985
|
+
return roots;
|
|
8986
|
+
}
|
|
8987
|
+
function sanitizeProfileName(value) {
|
|
8988
|
+
return value.replace(/[\\/]/g, "_").trim() || "default";
|
|
8989
|
+
}
|
|
8990
|
+
async function statOptional(rootPath) {
|
|
8991
|
+
try {
|
|
8992
|
+
return await import_node_fs14.promises.stat(rootPath);
|
|
8993
|
+
} catch (err) {
|
|
8994
|
+
if (err.code === "ENOENT") return null;
|
|
8995
|
+
throw err;
|
|
8996
|
+
}
|
|
8997
|
+
}
|
|
7977
8998
|
async function walkAndDigest(root, current) {
|
|
7978
8999
|
const out = [];
|
|
7979
9000
|
let entries;
|
|
7980
9001
|
try {
|
|
7981
|
-
entries = await
|
|
9002
|
+
entries = await import_node_fs14.promises.readdir(current);
|
|
7982
9003
|
} catch (err) {
|
|
7983
9004
|
if (err.code === "ENOENT") return out;
|
|
7984
9005
|
throw err;
|
|
@@ -7989,7 +9010,7 @@ async function walkAndDigest(root, current) {
|
|
|
7989
9010
|
const rel = path3.relative(root, full);
|
|
7990
9011
|
let st;
|
|
7991
9012
|
try {
|
|
7992
|
-
st = await
|
|
9013
|
+
st = await import_node_fs14.promises.stat(full);
|
|
7993
9014
|
} catch {
|
|
7994
9015
|
continue;
|
|
7995
9016
|
}
|
|
@@ -8000,7 +9021,7 @@ async function walkAndDigest(root, current) {
|
|
|
8000
9021
|
continue;
|
|
8001
9022
|
}
|
|
8002
9023
|
if (!st.isFile()) continue;
|
|
8003
|
-
const buf = await
|
|
9024
|
+
const buf = await import_node_fs14.promises.readFile(full);
|
|
8004
9025
|
const sha2563 = (0, import_node_crypto4.createHash)("sha256").update(buf).digest("hex");
|
|
8005
9026
|
out.push({
|
|
8006
9027
|
path: rel,
|
|
@@ -9551,7 +10572,7 @@ function parsePrismerUri(uri) {
|
|
|
9551
10572
|
}
|
|
9552
10573
|
|
|
9553
10574
|
// src/daemon/asset/rpc.ts
|
|
9554
|
-
var
|
|
10575
|
+
var import_node_fs15 = require("fs");
|
|
9555
10576
|
var ASSET_PATH_PREFIX = "/local/asset/";
|
|
9556
10577
|
var MAX_READ_BYTES = 256 * 1024;
|
|
9557
10578
|
var MAX_SEARCH_BYTES = 1024 * 1024;
|
|
@@ -9726,16 +10747,16 @@ function isTextLike(row) {
|
|
|
9726
10747
|
);
|
|
9727
10748
|
}
|
|
9728
10749
|
function readFileRange(path9, offset, length) {
|
|
9729
|
-
const sizeBytes = (0,
|
|
10750
|
+
const sizeBytes = (0, import_node_fs15.statSync)(path9).size;
|
|
9730
10751
|
if (offset >= sizeBytes) return { buffer: Buffer.alloc(0), bytesRead: 0, sizeBytes };
|
|
9731
|
-
const fd = (0,
|
|
10752
|
+
const fd = (0, import_node_fs15.openSync)(path9, "r");
|
|
9732
10753
|
try {
|
|
9733
10754
|
const target = Math.min(length, sizeBytes - offset);
|
|
9734
10755
|
const buffer = Buffer.alloc(target);
|
|
9735
|
-
const bytesRead = (0,
|
|
10756
|
+
const bytesRead = (0, import_node_fs15.readSync)(fd, buffer, 0, target, offset);
|
|
9736
10757
|
return { buffer: buffer.subarray(0, bytesRead), bytesRead, sizeBytes };
|
|
9737
10758
|
} finally {
|
|
9738
|
-
(0,
|
|
10759
|
+
(0, import_node_fs15.closeSync)(fd);
|
|
9739
10760
|
}
|
|
9740
10761
|
}
|
|
9741
10762
|
function respond3(res, status, body) {
|
|
@@ -9760,8 +10781,8 @@ async function readJson3(req) {
|
|
|
9760
10781
|
}
|
|
9761
10782
|
|
|
9762
10783
|
// src/daemon/asset/origin/drop-folder.ts
|
|
9763
|
-
var
|
|
9764
|
-
var
|
|
10784
|
+
var import_node_fs16 = require("fs");
|
|
10785
|
+
var import_node_os6 = require("os");
|
|
9765
10786
|
var path6 = __toESM(require("path"), 1);
|
|
9766
10787
|
var DropFolderAdapter = class {
|
|
9767
10788
|
kind = "drop-folder";
|
|
@@ -9769,7 +10790,7 @@ var DropFolderAdapter = class {
|
|
|
9769
10790
|
rootDir;
|
|
9770
10791
|
constructor(opts = {}) {
|
|
9771
10792
|
this.workspaceDir = opts.workspaceDir;
|
|
9772
|
-
this.rootDir = opts.rootDir ?? path6.join(opts.homeDir ?? (0,
|
|
10793
|
+
this.rootDir = opts.rootDir ?? path6.join(opts.homeDir ?? (0, import_node_os6.homedir)(), ".prismer");
|
|
9773
10794
|
}
|
|
9774
10795
|
/**
|
|
9775
10796
|
* Long-lived async iterable wrapping `scanOnce` on a 1s polling interval.
|
|
@@ -9779,7 +10800,7 @@ var DropFolderAdapter = class {
|
|
|
9779
10800
|
while (true) {
|
|
9780
10801
|
const batch = await this.scanOnce(workspaceId);
|
|
9781
10802
|
for (const obs of batch) yield obs;
|
|
9782
|
-
await
|
|
10803
|
+
await sleep2(1e3);
|
|
9783
10804
|
}
|
|
9784
10805
|
}
|
|
9785
10806
|
/**
|
|
@@ -9793,7 +10814,7 @@ var DropFolderAdapter = class {
|
|
|
9793
10814
|
const drop = this.dropDir(workspaceId);
|
|
9794
10815
|
let entries = [];
|
|
9795
10816
|
try {
|
|
9796
|
-
entries = await
|
|
10817
|
+
entries = await import_node_fs16.promises.readdir(drop);
|
|
9797
10818
|
} catch (err) {
|
|
9798
10819
|
if (err.code === "ENOENT") return [];
|
|
9799
10820
|
throw err;
|
|
@@ -9802,12 +10823,12 @@ var DropFolderAdapter = class {
|
|
|
9802
10823
|
for (const name of entries) {
|
|
9803
10824
|
if (name.startsWith(".")) continue;
|
|
9804
10825
|
const fullPath = path6.join(drop, name);
|
|
9805
|
-
const stat = await
|
|
10826
|
+
const stat = await import_node_fs16.promises.lstat(fullPath).catch(() => null);
|
|
9806
10827
|
if (!stat) continue;
|
|
9807
10828
|
if (stat.isSymbolicLink()) continue;
|
|
9808
10829
|
if (stat.isDirectory()) {
|
|
9809
10830
|
for (const nested of await walk(fullPath)) {
|
|
9810
|
-
const nstat = await
|
|
10831
|
+
const nstat = await import_node_fs16.promises.lstat(nested).catch(() => null);
|
|
9811
10832
|
if (!nstat || !nstat.isFile()) continue;
|
|
9812
10833
|
out.push(makeObservation(workspaceId, nested, drop, nstat.size, Math.floor(nstat.mtimeMs)));
|
|
9813
10834
|
}
|
|
@@ -9832,7 +10853,7 @@ var DropFolderAdapter = class {
|
|
|
9832
10853
|
}
|
|
9833
10854
|
async fetch(obs) {
|
|
9834
10855
|
const detail = obs.detail;
|
|
9835
|
-
const bytes = await
|
|
10856
|
+
const bytes = await import_node_fs16.promises.readFile(detail.path);
|
|
9836
10857
|
return {
|
|
9837
10858
|
bytes,
|
|
9838
10859
|
mime: mimeFromExtension(detail.path),
|
|
@@ -9854,8 +10875,8 @@ var DropFolderAdapter = class {
|
|
|
9854
10875
|
const rel = typeof detail.watchDir === "string" ? path6.relative(detail.watchDir, detail.path) : path6.basename(detail.path);
|
|
9855
10876
|
const safeRel = rel && !rel.startsWith("..") && !path6.isAbsolute(rel) ? rel : path6.basename(detail.path);
|
|
9856
10877
|
const destPath = path6.join(destDir, safeRel);
|
|
9857
|
-
await
|
|
9858
|
-
await
|
|
10878
|
+
await import_node_fs16.promises.mkdir(path6.dirname(destPath), { recursive: true });
|
|
10879
|
+
await import_node_fs16.promises.rename(detail.path, destPath);
|
|
9859
10880
|
} catch (err) {
|
|
9860
10881
|
if (err.code === "ENOENT") return;
|
|
9861
10882
|
throw err;
|
|
@@ -9883,11 +10904,11 @@ function makeObservation(workspaceId, filePath, watchDir, size, mtime) {
|
|
|
9883
10904
|
}
|
|
9884
10905
|
async function walk(root) {
|
|
9885
10906
|
const out = [];
|
|
9886
|
-
const entries = await
|
|
10907
|
+
const entries = await import_node_fs16.promises.readdir(root).catch(() => []);
|
|
9887
10908
|
for (const name of entries) {
|
|
9888
10909
|
if (name.startsWith(".")) continue;
|
|
9889
10910
|
const full = path6.join(root, name);
|
|
9890
|
-
const stat = await
|
|
10911
|
+
const stat = await import_node_fs16.promises.lstat(full).catch(() => null);
|
|
9891
10912
|
if (!stat) continue;
|
|
9892
10913
|
if (stat.isSymbolicLink()) continue;
|
|
9893
10914
|
if (stat.isDirectory()) {
|
|
@@ -9928,7 +10949,7 @@ function mimeFromExtension(p) {
|
|
|
9928
10949
|
const ext = path6.extname(p).toLowerCase();
|
|
9929
10950
|
return MIME_BY_EXT[ext] ?? null;
|
|
9930
10951
|
}
|
|
9931
|
-
function
|
|
10952
|
+
function sleep2(ms) {
|
|
9932
10953
|
return new Promise((r) => setTimeout(r, ms));
|
|
9933
10954
|
}
|
|
9934
10955
|
|
|
@@ -10483,7 +11504,7 @@ function stringValue(obj, key) {
|
|
|
10483
11504
|
}
|
|
10484
11505
|
|
|
10485
11506
|
// src/daemon/outbox-watcher.ts
|
|
10486
|
-
var
|
|
11507
|
+
var import_node_fs17 = require("fs");
|
|
10487
11508
|
var path8 = __toESM(require("path"), 1);
|
|
10488
11509
|
|
|
10489
11510
|
// src/daemon/asset/agent-output-policy.ts
|
|
@@ -10639,9 +11660,120 @@ function extensionOf(filename) {
|
|
|
10639
11660
|
return idx >= 0 ? name.slice(idx) : "";
|
|
10640
11661
|
}
|
|
10641
11662
|
|
|
11663
|
+
// src/daemon/asset/magic-bytes.ts
|
|
11664
|
+
var PDF_SIG = Buffer.from("%PDF-", "ascii");
|
|
11665
|
+
var PNG_SIG = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
11666
|
+
var JPEG_SIG = Buffer.from([255, 216, 255]);
|
|
11667
|
+
var GIF87 = Buffer.from("GIF87a", "ascii");
|
|
11668
|
+
var GIF89 = Buffer.from("GIF89a", "ascii");
|
|
11669
|
+
var ZIP_LOCAL = Buffer.from([80, 75, 3, 4]);
|
|
11670
|
+
var ZIP_EMPTY = Buffer.from([80, 75, 5, 6]);
|
|
11671
|
+
var ZIP_SPANNED = Buffer.from([80, 75, 7, 8]);
|
|
11672
|
+
function detectMagicBytes(bytes) {
|
|
11673
|
+
const head = bytes.subarray(0, Math.min(bytes.length, 4096));
|
|
11674
|
+
if (head.length === 0) return { detected: null, mime: null };
|
|
11675
|
+
if (head.length >= PDF_SIG.length && head.subarray(0, PDF_SIG.length).equals(PDF_SIG)) {
|
|
11676
|
+
return { detected: "pdf", mime: "application/pdf" };
|
|
11677
|
+
}
|
|
11678
|
+
if (head.length >= PNG_SIG.length && head.subarray(0, PNG_SIG.length).equals(PNG_SIG)) {
|
|
11679
|
+
return { detected: "png", mime: "image/png" };
|
|
11680
|
+
}
|
|
11681
|
+
if (head.length >= JPEG_SIG.length && head.subarray(0, JPEG_SIG.length).equals(JPEG_SIG)) {
|
|
11682
|
+
return { detected: "jpeg", mime: "image/jpeg" };
|
|
11683
|
+
}
|
|
11684
|
+
if (head.length >= GIF87.length && head.subarray(0, GIF87.length).equals(GIF87) || head.length >= GIF89.length && head.subarray(0, GIF89.length).equals(GIF89)) {
|
|
11685
|
+
return { detected: "gif", mime: "image/gif" };
|
|
11686
|
+
}
|
|
11687
|
+
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))) {
|
|
11688
|
+
return { detected: "zip", mime: "application/zip" };
|
|
11689
|
+
}
|
|
11690
|
+
const text = head.toString("utf8");
|
|
11691
|
+
const trimmed = text.trimStart();
|
|
11692
|
+
if (/^<\?xml[^>]*>\s*<svg[\s>]/i.test(trimmed) || /^<svg[\s>]/i.test(trimmed)) {
|
|
11693
|
+
return { detected: "svg", mime: "image/svg+xml" };
|
|
11694
|
+
}
|
|
11695
|
+
if (/^<(!doctype html|html[\s>])/i.test(trimmed)) {
|
|
11696
|
+
return { detected: "html", mime: "text/html" };
|
|
11697
|
+
}
|
|
11698
|
+
if (/^[{[]/.test(trimmed)) {
|
|
11699
|
+
return { detected: "json", mime: "application/json" };
|
|
11700
|
+
}
|
|
11701
|
+
if (looksLikePrintableText(head)) {
|
|
11702
|
+
return { detected: "markdown-or-text", mime: "text/plain" };
|
|
11703
|
+
}
|
|
11704
|
+
return { detected: null, mime: null };
|
|
11705
|
+
}
|
|
11706
|
+
var ZIP_DERIVED_MIMES = /* @__PURE__ */ new Set([
|
|
11707
|
+
"application/zip",
|
|
11708
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
11709
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
11710
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
11711
|
+
"application/vnd.oasis.opendocument.text",
|
|
11712
|
+
"application/vnd.oasis.opendocument.spreadsheet",
|
|
11713
|
+
"application/vnd.oasis.opendocument.presentation",
|
|
11714
|
+
"application/epub+zip",
|
|
11715
|
+
"application/java-archive"
|
|
11716
|
+
]);
|
|
11717
|
+
function mimeMismatchReason(detected, declared) {
|
|
11718
|
+
if (detected.detected === null) return null;
|
|
11719
|
+
const dec = (declared ?? "").toLowerCase().split(";")[0]?.trim() ?? "";
|
|
11720
|
+
if (!dec || dec === "application/octet-stream") return null;
|
|
11721
|
+
switch (detected.detected) {
|
|
11722
|
+
case "pdf":
|
|
11723
|
+
return dec === "application/pdf" ? null : `declared ${dec} but content is application/pdf`;
|
|
11724
|
+
case "png":
|
|
11725
|
+
return dec === "image/png" ? null : `declared ${dec} but content is image/png`;
|
|
11726
|
+
case "jpeg":
|
|
11727
|
+
return dec === "image/jpeg" || dec === "image/jpg" ? null : `declared ${dec} but content is image/jpeg`;
|
|
11728
|
+
case "gif":
|
|
11729
|
+
return dec === "image/gif" ? null : `declared ${dec} but content is image/gif`;
|
|
11730
|
+
case "zip":
|
|
11731
|
+
if (ZIP_DERIVED_MIMES.has(dec)) return null;
|
|
11732
|
+
return `declared ${dec} but content is a ZIP-container (application/zip family)`;
|
|
11733
|
+
case "svg":
|
|
11734
|
+
if (dec === "image/svg+xml" || dec.startsWith("text/")) return null;
|
|
11735
|
+
return `declared ${dec} but content is image/svg+xml`;
|
|
11736
|
+
case "html":
|
|
11737
|
+
if (dec === "text/html" || dec === "application/xhtml+xml" || dec.startsWith("text/")) return null;
|
|
11738
|
+
if (dec === "application/pdf" || dec.startsWith("image/") || dec === "application/zip") {
|
|
11739
|
+
return `declared ${dec} but content is text/html`;
|
|
11740
|
+
}
|
|
11741
|
+
return null;
|
|
11742
|
+
case "json":
|
|
11743
|
+
if (dec === "application/json" || dec.startsWith("text/")) return null;
|
|
11744
|
+
if (dec === "application/pdf" || dec.startsWith("image/") || dec === "application/zip") {
|
|
11745
|
+
return `declared ${dec} but content is application/json`;
|
|
11746
|
+
}
|
|
11747
|
+
return null;
|
|
11748
|
+
case "markdown-or-text":
|
|
11749
|
+
if (dec === "application/pdf" || dec.startsWith("image/") || dec === "application/zip") {
|
|
11750
|
+
return `declared ${dec} but content is text (not a real ${dec} payload)`;
|
|
11751
|
+
}
|
|
11752
|
+
return null;
|
|
11753
|
+
default:
|
|
11754
|
+
return null;
|
|
11755
|
+
}
|
|
11756
|
+
}
|
|
11757
|
+
function looksLikePrintableText(buf) {
|
|
11758
|
+
if (buf.length === 0) return false;
|
|
11759
|
+
let printable = 0;
|
|
11760
|
+
let total = 0;
|
|
11761
|
+
for (let i = 0; i < buf.length; i++) {
|
|
11762
|
+
const b = buf[i];
|
|
11763
|
+
if (b === 0) return false;
|
|
11764
|
+
total++;
|
|
11765
|
+
if (b >= 32 && b <= 126 || b === 9 || b === 10 || b === 13 || // utf-8 lead bytes
|
|
11766
|
+
b >= 128) {
|
|
11767
|
+
printable++;
|
|
11768
|
+
}
|
|
11769
|
+
}
|
|
11770
|
+
return total > 0 && printable / total >= 0.95;
|
|
11771
|
+
}
|
|
11772
|
+
|
|
10642
11773
|
// src/daemon/outbox-watcher.ts
|
|
10643
11774
|
var DEFAULT_INTERVAL_MS = 2e3;
|
|
10644
11775
|
var RESERVED_SUBDIR = "_uploaded";
|
|
11776
|
+
var REJECTED_SUBDIR = "_rejected";
|
|
10645
11777
|
var OutboxWatcher = class {
|
|
10646
11778
|
constructor(opts) {
|
|
10647
11779
|
this.opts = opts;
|
|
@@ -10806,7 +11938,7 @@ var OutboxWatcher = class {
|
|
|
10806
11938
|
async scanDir(dir, kind, task) {
|
|
10807
11939
|
let entries = [];
|
|
10808
11940
|
try {
|
|
10809
|
-
entries = await
|
|
11941
|
+
entries = await import_node_fs17.promises.readdir(dir);
|
|
10810
11942
|
} catch (err) {
|
|
10811
11943
|
const code = err.code;
|
|
10812
11944
|
if (code === "ENOENT") return;
|
|
@@ -10816,10 +11948,11 @@ var OutboxWatcher = class {
|
|
|
10816
11948
|
for (const name of entries) {
|
|
10817
11949
|
if (name.startsWith(".")) continue;
|
|
10818
11950
|
if (name === RESERVED_SUBDIR) continue;
|
|
11951
|
+
if (name === REJECTED_SUBDIR) continue;
|
|
10819
11952
|
const full = path8.join(dir, name);
|
|
10820
11953
|
let st;
|
|
10821
11954
|
try {
|
|
10822
|
-
st = await
|
|
11955
|
+
st = await import_node_fs17.promises.stat(full);
|
|
10823
11956
|
} catch {
|
|
10824
11957
|
continue;
|
|
10825
11958
|
}
|
|
@@ -10830,10 +11963,77 @@ var OutboxWatcher = class {
|
|
|
10830
11963
|
await this.upload(full, st, kind, task);
|
|
10831
11964
|
this.uploaded.add(key);
|
|
10832
11965
|
} catch (err) {
|
|
10833
|
-
|
|
11966
|
+
const msg = err.message;
|
|
11967
|
+
this.log("warn", `upload ${full} failed: ${msg}`);
|
|
11968
|
+
if (/\bMIME_MISMATCH\b/i.test(msg) || /\bmime mismatch\b/i.test(msg)) {
|
|
11969
|
+
await this.quarantine(dir, full).catch(
|
|
11970
|
+
(moveErr) => this.log("warn", `quarantine ${full} failed: ${moveErr.message}`)
|
|
11971
|
+
);
|
|
11972
|
+
this.uploaded.add(key);
|
|
11973
|
+
await this.reportMimeMismatchToCloud(task, full, msg).catch(
|
|
11974
|
+
(reportErr) => this.log("warn", `report MIME_MISMATCH failed: ${reportErr.message}`)
|
|
11975
|
+
);
|
|
11976
|
+
}
|
|
10834
11977
|
}
|
|
10835
11978
|
}
|
|
10836
11979
|
}
|
|
11980
|
+
/**
|
|
11981
|
+
* F2B — report an OUTBOX_MIME_MISMATCH task event to cloud so the task log
|
|
11982
|
+
* gains an auditable record of the rejection. Without this, the failure is
|
|
11983
|
+
* silent (file quarantined locally, agent unaware) and the next dispatch
|
|
11984
|
+
* loops on the same bad strategy.
|
|
11985
|
+
*
|
|
11986
|
+
* Best-effort: every catch site swallows errors. We never want this
|
|
11987
|
+
* upstream call to block local quarantine or trigger a retry loop, because
|
|
11988
|
+
* the reject decision is already final by the time we get here.
|
|
11989
|
+
*
|
|
11990
|
+
* Endpoint contract: POST /api/im/tasks/:id/event
|
|
11991
|
+
* { code: 'OUTBOX_MIME_MISMATCH', payload: { file, daemonError }, message }
|
|
11992
|
+
* Only callable when `task?.taskId` is known — pre-dispatch outbox files
|
|
11993
|
+
* (no active task) are quarantined silently as before.
|
|
11994
|
+
*/
|
|
11995
|
+
async reportMimeMismatchToCloud(task, filePath, daemonErrorMessage) {
|
|
11996
|
+
if (!task?.taskId) return;
|
|
11997
|
+
const fileName = path8.basename(filePath);
|
|
11998
|
+
const reasonMatch = daemonErrorMessage.match(/MIME_MISMATCH[^:]*:\s*(.+)$/i);
|
|
11999
|
+
const reason = reasonMatch?.[1] ? reasonMatch[1].trim() : daemonErrorMessage;
|
|
12000
|
+
const body = {
|
|
12001
|
+
code: "OUTBOX_MIME_MISMATCH",
|
|
12002
|
+
payload: {
|
|
12003
|
+
file: fileName,
|
|
12004
|
+
reason,
|
|
12005
|
+
daemonError: daemonErrorMessage
|
|
12006
|
+
},
|
|
12007
|
+
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.`
|
|
12008
|
+
};
|
|
12009
|
+
const res = await this.opts.cloud.request("POST", `/api/im/tasks/${encodeURIComponent(task.taskId)}/event`, {
|
|
12010
|
+
body
|
|
12011
|
+
});
|
|
12012
|
+
if (!res.ok) {
|
|
12013
|
+
throw new Error(`cloud rejected event: ${res.status} ${res.error?.code ?? ""} ${res.error?.message ?? ""}`);
|
|
12014
|
+
}
|
|
12015
|
+
}
|
|
12016
|
+
/**
|
|
12017
|
+
* Move a file under `<dir>/_rejected/` so it stops being a scan candidate
|
|
12018
|
+
* (the loop skips the reserved subdir) and operators can find the
|
|
12019
|
+
* offending artifact. Filename collision is rare per task; on collision
|
|
12020
|
+
* we append a millisecond suffix to keep the original observable.
|
|
12021
|
+
*/
|
|
12022
|
+
async quarantine(dir, full) {
|
|
12023
|
+
const rejectedDir = path8.join(dir, REJECTED_SUBDIR);
|
|
12024
|
+
await import_node_fs17.promises.mkdir(rejectedDir, { recursive: true });
|
|
12025
|
+
const base = path8.basename(full);
|
|
12026
|
+
let dest = path8.join(rejectedDir, base);
|
|
12027
|
+
try {
|
|
12028
|
+
await import_node_fs17.promises.access(dest);
|
|
12029
|
+
const ext = path8.extname(base);
|
|
12030
|
+
const stem = base.slice(0, base.length - ext.length);
|
|
12031
|
+
dest = path8.join(rejectedDir, `${stem}.${Date.now()}${ext}`);
|
|
12032
|
+
} catch {
|
|
12033
|
+
}
|
|
12034
|
+
await import_node_fs17.promises.rename(full, dest);
|
|
12035
|
+
this.log("warn", `quarantined ${full} \u2192 ${dest}`);
|
|
12036
|
+
}
|
|
10837
12037
|
async upload(filePath, st, kind, task) {
|
|
10838
12038
|
const wsId = this.opts.workspaceId();
|
|
10839
12039
|
if (!wsId) {
|
|
@@ -10844,13 +12044,18 @@ var OutboxWatcher = class {
|
|
|
10844
12044
|
this.log("warn", `skip ${filePath}: no active taskId (no dispatch/handoff received yet)`);
|
|
10845
12045
|
return;
|
|
10846
12046
|
}
|
|
10847
|
-
const bytes = await
|
|
12047
|
+
const bytes = await import_node_fs17.promises.readFile(filePath);
|
|
10848
12048
|
const fileName = path8.basename(filePath);
|
|
10849
12049
|
const inferredMime = inferAgentOutputMime(fileName);
|
|
10850
12050
|
const policy = validateAgentOutputAsset({ filename: fileName, mime: inferredMime, sizeBytes: bytes.length });
|
|
10851
12051
|
if (!policy.ok) {
|
|
10852
12052
|
throw new Error(`agent output rejected: ${policy.reason}`);
|
|
10853
12053
|
}
|
|
12054
|
+
const detection = detectMagicBytes(bytes);
|
|
12055
|
+
const reason = mimeMismatchReason(detection, policy.mime);
|
|
12056
|
+
if (reason) {
|
|
12057
|
+
throw new Error(`MIME_MISMATCH for ${fileName}: ${reason}`);
|
|
12058
|
+
}
|
|
10854
12059
|
const metadata = {
|
|
10855
12060
|
taskId: task.taskId,
|
|
10856
12061
|
...task.adapter ? { adapter: task.adapter } : {}
|
|
@@ -10932,7 +12137,7 @@ var ServicePool = class {
|
|
|
10932
12137
|
|
|
10933
12138
|
// src/daemon/shell-executor.ts
|
|
10934
12139
|
var import_node_child_process6 = require("child_process");
|
|
10935
|
-
var
|
|
12140
|
+
var import_node_fs18 = require("fs");
|
|
10936
12141
|
var import_node_path13 = require("path");
|
|
10937
12142
|
var DEFAULT_OUTPUT_LIMIT = 256 * 1024;
|
|
10938
12143
|
var DEFAULT_TIMEOUT = 6e4;
|
|
@@ -10969,7 +12174,7 @@ async function executeShellDispatch(payload, deps) {
|
|
|
10969
12174
|
const command = readCommand(payload, execution);
|
|
10970
12175
|
if (!command.trim()) return fail2(payload.taskId, "shell_command_required", "Shell command is required");
|
|
10971
12176
|
const cwd = resolveCwd(execution.cwd, deps.config.defaultCwd);
|
|
10972
|
-
if (!(0,
|
|
12177
|
+
if (!(0, import_node_fs18.existsSync)(cwd)) return fail2(payload.taskId, "shell_cwd_missing", `cwd does not exist: ${cwd}`);
|
|
10973
12178
|
const timeoutMs = Math.min(
|
|
10974
12179
|
typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? payload.timeoutMs : deps.config.maxTimeoutMs,
|
|
10975
12180
|
deps.config.maxTimeoutMs
|
|
@@ -11204,6 +12409,350 @@ var WsClient = class extends import_node_events2.EventEmitter {
|
|
|
11204
12409
|
}
|
|
11205
12410
|
};
|
|
11206
12411
|
|
|
12412
|
+
// src/daemon/pending-reply-cache.ts
|
|
12413
|
+
function fromRaw(r) {
|
|
12414
|
+
return {
|
|
12415
|
+
idempotencyKey: r.idempotency_key,
|
|
12416
|
+
taskId: r.task_id,
|
|
12417
|
+
replyId: r.reply_id,
|
|
12418
|
+
status: r.status,
|
|
12419
|
+
payloadJson: r.payload_json,
|
|
12420
|
+
createdAt: r.created_at,
|
|
12421
|
+
preparedAt: r.prepared_at,
|
|
12422
|
+
lastAttemptAt: r.last_attempt_at,
|
|
12423
|
+
attemptCount: r.attempt_count,
|
|
12424
|
+
lastError: r.last_error
|
|
12425
|
+
};
|
|
12426
|
+
}
|
|
12427
|
+
function createPendingReplyCache(db) {
|
|
12428
|
+
const insert = db.prepare(
|
|
12429
|
+
`INSERT OR REPLACE INTO pending_dispatch_replies
|
|
12430
|
+
(idempotency_key, task_id, reply_id, status, payload_json,
|
|
12431
|
+
prepared_at, created_at, last_attempt_at, attempt_count, last_error)
|
|
12432
|
+
VALUES (?, ?, NULL, 'pending', ?, NULL, ?, NULL, 0, NULL)`
|
|
12433
|
+
);
|
|
12434
|
+
const markPreparedStmt = db.prepare(
|
|
12435
|
+
`UPDATE pending_dispatch_replies
|
|
12436
|
+
SET reply_id = ?, status = 'prepared', prepared_at = ?,
|
|
12437
|
+
last_attempt_at = ?, attempt_count = attempt_count + 1,
|
|
12438
|
+
last_error = NULL
|
|
12439
|
+
WHERE idempotency_key = ?`
|
|
12440
|
+
);
|
|
12441
|
+
const markAttemptStmt = db.prepare(
|
|
12442
|
+
`UPDATE pending_dispatch_replies
|
|
12443
|
+
SET last_attempt_at = ?, attempt_count = attempt_count + 1,
|
|
12444
|
+
last_error = ?
|
|
12445
|
+
WHERE idempotency_key = ?`
|
|
12446
|
+
);
|
|
12447
|
+
const clearStmt = db.prepare(
|
|
12448
|
+
`DELETE FROM pending_dispatch_replies WHERE idempotency_key = ?`
|
|
12449
|
+
);
|
|
12450
|
+
const listRecoveryStmt = db.prepare(
|
|
12451
|
+
`SELECT * FROM pending_dispatch_replies
|
|
12452
|
+
WHERE status IN ('pending', 'prepared')
|
|
12453
|
+
ORDER BY created_at ASC`
|
|
12454
|
+
);
|
|
12455
|
+
const countStmt = db.prepare(
|
|
12456
|
+
`SELECT status, COUNT(*) as cnt FROM pending_dispatch_replies
|
|
12457
|
+
WHERE status IN ('pending','prepared') GROUP BY status`
|
|
12458
|
+
);
|
|
12459
|
+
const findStmt = db.prepare(
|
|
12460
|
+
`SELECT * FROM pending_dispatch_replies WHERE idempotency_key = ?`
|
|
12461
|
+
);
|
|
12462
|
+
return {
|
|
12463
|
+
savePending(idempotencyKey, taskId, payloadJson, now = Date.now()) {
|
|
12464
|
+
insert.run(idempotencyKey, taskId, payloadJson, now);
|
|
12465
|
+
},
|
|
12466
|
+
markPrepared(idempotencyKey, replyId, now = Date.now()) {
|
|
12467
|
+
markPreparedStmt.run(replyId, now, now, idempotencyKey);
|
|
12468
|
+
},
|
|
12469
|
+
markAttempt(idempotencyKey, now = Date.now(), error) {
|
|
12470
|
+
markAttemptStmt.run(now, error ?? null, idempotencyKey);
|
|
12471
|
+
},
|
|
12472
|
+
clearPending(idempotencyKey) {
|
|
12473
|
+
clearStmt.run(idempotencyKey);
|
|
12474
|
+
},
|
|
12475
|
+
listForRecovery() {
|
|
12476
|
+
const rows = listRecoveryStmt.all();
|
|
12477
|
+
return rows.map(fromRaw);
|
|
12478
|
+
},
|
|
12479
|
+
countByStatus() {
|
|
12480
|
+
const rows = countStmt.all();
|
|
12481
|
+
const result = { pending: 0, prepared: 0 };
|
|
12482
|
+
for (const r of rows) {
|
|
12483
|
+
if (r.status === "pending") result.pending = r.cnt;
|
|
12484
|
+
else if (r.status === "prepared") result.prepared = r.cnt;
|
|
12485
|
+
}
|
|
12486
|
+
return result;
|
|
12487
|
+
},
|
|
12488
|
+
findByIdempotencyKey(idempotencyKey) {
|
|
12489
|
+
const row = findStmt.get(idempotencyKey);
|
|
12490
|
+
return row ? fromRaw(row) : null;
|
|
12491
|
+
}
|
|
12492
|
+
};
|
|
12493
|
+
}
|
|
12494
|
+
function generateIdempotencyKey() {
|
|
12495
|
+
const c = globalThis.crypto;
|
|
12496
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
12497
|
+
const bytes = new Uint8Array(16);
|
|
12498
|
+
const cryptoNode = globalThis.crypto;
|
|
12499
|
+
if (cryptoNode?.getRandomValues) {
|
|
12500
|
+
cryptoNode.getRandomValues(bytes);
|
|
12501
|
+
} else {
|
|
12502
|
+
for (let i = 0; i < 16; i += 1) bytes[i] = Math.floor(Math.random() * 256);
|
|
12503
|
+
}
|
|
12504
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
12505
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
12506
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
12507
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
12508
|
+
}
|
|
12509
|
+
|
|
12510
|
+
// src/daemon/dispatch-reply-transport.ts
|
|
12511
|
+
var DispatchReplyAbortedError = class extends Error {
|
|
12512
|
+
constructor(replyId) {
|
|
12513
|
+
super(`dispatch reply ${replyId} aborted by server reaper (likely >1h since prepare)`);
|
|
12514
|
+
this.replyId = replyId;
|
|
12515
|
+
this.name = "DispatchReplyAbortedError";
|
|
12516
|
+
}
|
|
12517
|
+
replyId;
|
|
12518
|
+
code = "DISPATCH_REPLY_INVALID_STATE";
|
|
12519
|
+
};
|
|
12520
|
+
function defaultLog2(line) {
|
|
12521
|
+
process.stderr.write(`${line}
|
|
12522
|
+
`);
|
|
12523
|
+
}
|
|
12524
|
+
async function sendDispatchReplyTwoPhase(args) {
|
|
12525
|
+
const { taskId, payload, cloud, cache } = args;
|
|
12526
|
+
const log8 = args.log ?? defaultLog2;
|
|
12527
|
+
const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
|
|
12528
|
+
const payloadJson = JSON.stringify({ ...payload, _taskId: taskId, _idempotencyKey: idempotencyKey });
|
|
12529
|
+
cache.savePending(idempotencyKey, taskId, payloadJson);
|
|
12530
|
+
const prepareRes = await cloud.request("POST", `/api/im/dispatch/${encodeURIComponent(taskId)}/prepare`, {
|
|
12531
|
+
body: { ...payload, idempotencyKey },
|
|
12532
|
+
timeoutMs: 3e4
|
|
12533
|
+
});
|
|
12534
|
+
if (!prepareRes.ok || !prepareRes.data) {
|
|
12535
|
+
const errCode = prepareRes.error?.code ?? "unknown";
|
|
12536
|
+
const errMsg = prepareRes.error?.message ?? `prepare failed (status=${prepareRes.status})`;
|
|
12537
|
+
cache.markAttempt(idempotencyKey, Date.now(), `prepare: ${errCode}: ${errMsg}`);
|
|
12538
|
+
throw new Error(`dispatch prepare failed: ${errCode}: ${errMsg}`);
|
|
12539
|
+
}
|
|
12540
|
+
const envelope2 = prepareRes.data;
|
|
12541
|
+
const inner = "data" in envelope2 && envelope2.data ? envelope2.data : envelope2;
|
|
12542
|
+
if (!inner || typeof inner.replyId !== "string") {
|
|
12543
|
+
cache.markAttempt(idempotencyKey, Date.now(), "prepare: missing replyId in response");
|
|
12544
|
+
throw new Error("dispatch prepare returned no replyId");
|
|
12545
|
+
}
|
|
12546
|
+
if ("ok" in envelope2 && envelope2.ok === false) {
|
|
12547
|
+
const errCode = envelope2.error?.code ?? "prepare_failed";
|
|
12548
|
+
const errMsg = envelope2.error?.message ?? "prepare ok=false";
|
|
12549
|
+
cache.markAttempt(idempotencyKey, Date.now(), `prepare: ${errCode}: ${errMsg}`);
|
|
12550
|
+
throw new Error(`dispatch prepare failed: ${errCode}: ${errMsg}`);
|
|
12551
|
+
}
|
|
12552
|
+
cache.markPrepared(idempotencyKey, inner.replyId);
|
|
12553
|
+
log8(
|
|
12554
|
+
`[daemon] dispatch.prepare task=${taskId} replyId=${inner.replyId} status=${inner.status} idem=${idempotencyKey}`
|
|
12555
|
+
);
|
|
12556
|
+
if (inner.status === "committed") {
|
|
12557
|
+
cache.clearPending(idempotencyKey);
|
|
12558
|
+
return {
|
|
12559
|
+
taskId,
|
|
12560
|
+
replyId: inner.replyId,
|
|
12561
|
+
status: "committed",
|
|
12562
|
+
alreadyCommitted: true
|
|
12563
|
+
};
|
|
12564
|
+
}
|
|
12565
|
+
try {
|
|
12566
|
+
const result = await commitDispatchReply({ taskId, replyId: inner.replyId, cloud, log: log8 });
|
|
12567
|
+
cache.clearPending(idempotencyKey);
|
|
12568
|
+
return { taskId, replyId: inner.replyId, ...result };
|
|
12569
|
+
} catch (err) {
|
|
12570
|
+
if (err instanceof DispatchReplyAbortedError) {
|
|
12571
|
+
cache.clearPending(idempotencyKey);
|
|
12572
|
+
log8(`[daemon] dispatch.commit aborted task=${taskId} replyId=${inner.replyId} \u2014 dispatch lost`);
|
|
12573
|
+
return {
|
|
12574
|
+
taskId,
|
|
12575
|
+
replyId: inner.replyId,
|
|
12576
|
+
status: "aborted",
|
|
12577
|
+
alreadyCommitted: false
|
|
12578
|
+
};
|
|
12579
|
+
}
|
|
12580
|
+
cache.markAttempt(idempotencyKey, Date.now(), `commit: ${err.message}`);
|
|
12581
|
+
throw err;
|
|
12582
|
+
}
|
|
12583
|
+
}
|
|
12584
|
+
async function commitDispatchReply(args) {
|
|
12585
|
+
const log8 = args.log ?? defaultLog2;
|
|
12586
|
+
const res = await args.cloud.request("POST", `/api/im/dispatch/${encodeURIComponent(args.taskId)}/commit`, {
|
|
12587
|
+
body: { replyId: args.replyId },
|
|
12588
|
+
timeoutMs: 3e4
|
|
12589
|
+
});
|
|
12590
|
+
if (!res.ok || !res.data) {
|
|
12591
|
+
const code = res.error?.code ?? "unknown";
|
|
12592
|
+
const msg = res.error?.message ?? `commit failed (status=${res.status})`;
|
|
12593
|
+
if (code === "DISPATCH_REPLY_INVALID_STATE") {
|
|
12594
|
+
throw new DispatchReplyAbortedError(args.replyId);
|
|
12595
|
+
}
|
|
12596
|
+
throw new Error(`dispatch commit failed: ${code}: ${msg}`);
|
|
12597
|
+
}
|
|
12598
|
+
const env = res.data;
|
|
12599
|
+
const inner = "data" in env && env.data ? env.data : env;
|
|
12600
|
+
if ("ok" in env && env.ok === false) {
|
|
12601
|
+
const code = env.error?.code ?? "commit_failed";
|
|
12602
|
+
if (code === "DISPATCH_REPLY_INVALID_STATE") {
|
|
12603
|
+
throw new DispatchReplyAbortedError(args.replyId);
|
|
12604
|
+
}
|
|
12605
|
+
throw new Error(`dispatch commit failed: ${code}: ${env.error?.message ?? "commit ok=false"}`);
|
|
12606
|
+
}
|
|
12607
|
+
log8(
|
|
12608
|
+
`[daemon] dispatch.commit task=${args.taskId} replyId=${args.replyId} alreadyCommitted=${inner.alreadyCommitted ?? false}`
|
|
12609
|
+
);
|
|
12610
|
+
return {
|
|
12611
|
+
status: "committed",
|
|
12612
|
+
alreadyCommitted: Boolean(inner.alreadyCommitted),
|
|
12613
|
+
...inner.messageId ? { messageId: inner.messageId } : {}
|
|
12614
|
+
};
|
|
12615
|
+
}
|
|
12616
|
+
async function recoverPendingReplies(deps) {
|
|
12617
|
+
const log8 = deps.log ?? defaultLog2;
|
|
12618
|
+
const rows = deps.cache.listForRecovery();
|
|
12619
|
+
if (rows.length === 0) {
|
|
12620
|
+
return { attempted: 0, committed: 0, aborted: 0, failed: 0 };
|
|
12621
|
+
}
|
|
12622
|
+
log8(`[daemon] dispatch.recovery: ${rows.length} pending row(s) to replay`);
|
|
12623
|
+
let committed = 0;
|
|
12624
|
+
let aborted = 0;
|
|
12625
|
+
let failed = 0;
|
|
12626
|
+
for (const row of rows) {
|
|
12627
|
+
try {
|
|
12628
|
+
const result = await replayRow(row, deps);
|
|
12629
|
+
if (result.status === "committed") committed += 1;
|
|
12630
|
+
else if (result.status === "aborted") aborted += 1;
|
|
12631
|
+
} catch (err) {
|
|
12632
|
+
failed += 1;
|
|
12633
|
+
log8(
|
|
12634
|
+
`[daemon] dispatch.recovery FAIL idem=${row.idempotencyKey} task=${row.taskId}: ${err.message}`
|
|
12635
|
+
);
|
|
12636
|
+
}
|
|
12637
|
+
}
|
|
12638
|
+
log8(
|
|
12639
|
+
`[daemon] dispatch.recovery done attempted=${rows.length} committed=${committed} aborted=${aborted} failed=${failed}`
|
|
12640
|
+
);
|
|
12641
|
+
return { attempted: rows.length, committed, aborted, failed };
|
|
12642
|
+
}
|
|
12643
|
+
async function replayRow(row, deps) {
|
|
12644
|
+
const log8 = deps.log ?? defaultLog2;
|
|
12645
|
+
if (row.status === "prepared" && row.replyId) {
|
|
12646
|
+
try {
|
|
12647
|
+
const result2 = await commitDispatchReply({
|
|
12648
|
+
taskId: row.taskId,
|
|
12649
|
+
replyId: row.replyId,
|
|
12650
|
+
cloud: deps.cloud,
|
|
12651
|
+
log: log8
|
|
12652
|
+
});
|
|
12653
|
+
deps.cache.clearPending(row.idempotencyKey);
|
|
12654
|
+
return { status: result2.status };
|
|
12655
|
+
} catch (err) {
|
|
12656
|
+
if (err instanceof DispatchReplyAbortedError) {
|
|
12657
|
+
deps.cache.clearPending(row.idempotencyKey);
|
|
12658
|
+
return { status: "aborted" };
|
|
12659
|
+
}
|
|
12660
|
+
throw err;
|
|
12661
|
+
}
|
|
12662
|
+
}
|
|
12663
|
+
let parsed;
|
|
12664
|
+
try {
|
|
12665
|
+
parsed = JSON.parse(row.payloadJson);
|
|
12666
|
+
} catch (err) {
|
|
12667
|
+
deps.cache.clearPending(row.idempotencyKey);
|
|
12668
|
+
throw new Error(`pending row ${row.idempotencyKey} has unparseable payload: ${err.message}`);
|
|
12669
|
+
}
|
|
12670
|
+
const { _taskId, _idempotencyKey, ...payload } = parsed;
|
|
12671
|
+
const result = await sendDispatchReplyTwoPhase({
|
|
12672
|
+
taskId: row.taskId,
|
|
12673
|
+
payload,
|
|
12674
|
+
cloud: deps.cloud,
|
|
12675
|
+
cache: deps.cache,
|
|
12676
|
+
idempotencyKey: row.idempotencyKey,
|
|
12677
|
+
log: deps.log
|
|
12678
|
+
});
|
|
12679
|
+
return { status: result.status };
|
|
12680
|
+
}
|
|
12681
|
+
|
|
12682
|
+
// src/daemon/transport-probe.ts
|
|
12683
|
+
var import_node_os7 = require("os");
|
|
12684
|
+
function isPrivateAddress(addr) {
|
|
12685
|
+
if (!addr) return true;
|
|
12686
|
+
const h = String(addr).trim().toLowerCase();
|
|
12687
|
+
if (!h) return true;
|
|
12688
|
+
if (h === "localhost" || h === "::1") return true;
|
|
12689
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
|
|
12690
|
+
if (m) {
|
|
12691
|
+
const a = Number(m[1]);
|
|
12692
|
+
const b = Number(m[2]);
|
|
12693
|
+
if (a === 10) return true;
|
|
12694
|
+
if (a === 127) return true;
|
|
12695
|
+
if (a === 169 && b === 254) return true;
|
|
12696
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
12697
|
+
if (a === 192 && b === 168) return true;
|
|
12698
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
12699
|
+
return false;
|
|
12700
|
+
}
|
|
12701
|
+
if (h.endsWith(".local")) return true;
|
|
12702
|
+
if (h.endsWith(".localhost")) return true;
|
|
12703
|
+
return false;
|
|
12704
|
+
}
|
|
12705
|
+
function buildTransportProbe(gatewayUrl) {
|
|
12706
|
+
if (!gatewayUrl) {
|
|
12707
|
+
return { transport: "ws", gatewayUrl: null, gatewayIsPrivate: true };
|
|
12708
|
+
}
|
|
12709
|
+
let host = null;
|
|
12710
|
+
try {
|
|
12711
|
+
host = new URL(gatewayUrl).hostname;
|
|
12712
|
+
} catch {
|
|
12713
|
+
host = null;
|
|
12714
|
+
}
|
|
12715
|
+
const gatewayIsPrivate = isPrivateAddress(host);
|
|
12716
|
+
return {
|
|
12717
|
+
transport: gatewayIsPrivate ? "ws" : "both",
|
|
12718
|
+
gatewayUrl,
|
|
12719
|
+
gatewayIsPrivate
|
|
12720
|
+
};
|
|
12721
|
+
}
|
|
12722
|
+
function pickLocalIPv4() {
|
|
12723
|
+
const nics = (0, import_node_os7.networkInterfaces)();
|
|
12724
|
+
let firstPrivate = null;
|
|
12725
|
+
for (const list of Object.values(nics)) {
|
|
12726
|
+
if (!list) continue;
|
|
12727
|
+
for (const ni of list) {
|
|
12728
|
+
if (ni.internal || ni.family !== "IPv4") continue;
|
|
12729
|
+
if (!isPrivateAddress(ni.address)) return ni.address;
|
|
12730
|
+
if (!firstPrivate) firstPrivate = ni.address;
|
|
12731
|
+
}
|
|
12732
|
+
}
|
|
12733
|
+
return firstPrivate;
|
|
12734
|
+
}
|
|
12735
|
+
async function reportTransportProbe(cloud, daemonId, probe) {
|
|
12736
|
+
const body = {
|
|
12737
|
+
daemonId,
|
|
12738
|
+
transport: probe.transport,
|
|
12739
|
+
gatewayUrl: probe.gatewayUrl,
|
|
12740
|
+
gatewayIsPrivate: probe.gatewayIsPrivate
|
|
12741
|
+
};
|
|
12742
|
+
const res = await cloud.request("POST", "/api/im/runtime/transport-report", {
|
|
12743
|
+
body,
|
|
12744
|
+
timeoutMs: 5e3
|
|
12745
|
+
});
|
|
12746
|
+
if (!res.ok) {
|
|
12747
|
+
return {
|
|
12748
|
+
ok: false,
|
|
12749
|
+
status: res.status,
|
|
12750
|
+
error: res.error?.message ?? `transport-report failed (${res.status})`
|
|
12751
|
+
};
|
|
12752
|
+
}
|
|
12753
|
+
return { ok: true, status: res.status };
|
|
12754
|
+
}
|
|
12755
|
+
|
|
11207
12756
|
// src/daemon/runner.ts
|
|
11208
12757
|
var import_meta3 = {};
|
|
11209
12758
|
var DEFAULT_LOCAL_PORT = 3210;
|
|
@@ -11246,6 +12795,7 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11246
12795
|
workspaceId = "";
|
|
11247
12796
|
wsConnected = false;
|
|
11248
12797
|
hostedAgents = /* @__PURE__ */ new Map();
|
|
12798
|
+
rejectedHostedAgentIds = /* @__PURE__ */ new Map();
|
|
11249
12799
|
runningTasks = /* @__PURE__ */ new Map();
|
|
11250
12800
|
lastTaskError;
|
|
11251
12801
|
heartbeatTimer;
|
|
@@ -11253,6 +12803,14 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11253
12803
|
// F16 (2026-05-20) — periodic skill resync timer (default 10min).
|
|
11254
12804
|
skillResyncTimer;
|
|
11255
12805
|
skillSyncInFlight = false;
|
|
12806
|
+
// Wave-4 E7 — local cache of in-flight two-phase dispatch replies. Survives
|
|
12807
|
+
// daemon crash so cold-start can resume `prepared` rows via idempotent
|
|
12808
|
+
// commit (server enforces (taskId, idempotencyKey) UNIQUE).
|
|
12809
|
+
pendingReplyCache;
|
|
12810
|
+
// One-shot guard so we only recover on the first post-authenticated tick,
|
|
12811
|
+
// not every reconnect (server-side commits are idempotent but the log noise
|
|
12812
|
+
// and DB pressure of re-running on every redeclare is wasteful).
|
|
12813
|
+
pendingReplyRecoveryDone = false;
|
|
11256
12814
|
async start() {
|
|
11257
12815
|
if (this.state !== "idle") throw new Error(`Runner already in state ${this.state}`);
|
|
11258
12816
|
this.state = "starting";
|
|
@@ -11267,6 +12825,7 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11267
12825
|
process.env.PRISMER_API_KEY = this.config.api_key;
|
|
11268
12826
|
this.db = openLocalDb(this.paths.localDb);
|
|
11269
12827
|
this.cloud = new CloudClient({ baseUrl: this.config.cloud_api_base, apiKey: this.config.api_key });
|
|
12828
|
+
this.pendingReplyCache = createPendingReplyCache(this.db);
|
|
11270
12829
|
this.assetCache = new AssetCache({
|
|
11271
12830
|
db: this.db,
|
|
11272
12831
|
cloud: this.cloud,
|
|
@@ -11593,19 +13152,19 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11593
13152
|
sampleCgroupResources() {
|
|
11594
13153
|
const baseline = { cpu: { usagePct: 0 }, mem: { usedBytes: 0, limitBytes: 0 } };
|
|
11595
13154
|
try {
|
|
11596
|
-
if (!(0,
|
|
13155
|
+
if (!(0, import_node_fs19.existsSync)("/sys/fs/cgroup/memory.current")) {
|
|
11597
13156
|
return baseline;
|
|
11598
13157
|
}
|
|
11599
|
-
const memUsedRaw = (0,
|
|
13158
|
+
const memUsedRaw = (0, import_node_fs19.readFileSync)("/sys/fs/cgroup/memory.current", "utf-8").trim();
|
|
11600
13159
|
const memUsed = Number.parseInt(memUsedRaw, 10);
|
|
11601
13160
|
let memLimit = 0;
|
|
11602
|
-
if ((0,
|
|
11603
|
-
const limitRaw = (0,
|
|
13161
|
+
if ((0, import_node_fs19.existsSync)("/sys/fs/cgroup/memory.max")) {
|
|
13162
|
+
const limitRaw = (0, import_node_fs19.readFileSync)("/sys/fs/cgroup/memory.max", "utf-8").trim();
|
|
11604
13163
|
memLimit = limitRaw === "max" ? 0 : Number.parseInt(limitRaw, 10) || 0;
|
|
11605
13164
|
}
|
|
11606
13165
|
let cpuUsagePct = 0;
|
|
11607
|
-
if ((0,
|
|
11608
|
-
const statLines = (0,
|
|
13166
|
+
if ((0, import_node_fs19.existsSync)("/sys/fs/cgroup/cpu.stat")) {
|
|
13167
|
+
const statLines = (0, import_node_fs19.readFileSync)("/sys/fs/cgroup/cpu.stat", "utf-8").split("\n");
|
|
11609
13168
|
const usecLine = statLines.find((line) => line.startsWith("usage_usec "));
|
|
11610
13169
|
if (usecLine) {
|
|
11611
13170
|
const usec = Number.parseInt(usecLine.split(" ")[1] ?? "0", 10);
|
|
@@ -11682,6 +13241,9 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11682
13241
|
profilesByAgent.set(p.agent_im_user_id, list);
|
|
11683
13242
|
}
|
|
11684
13243
|
for (const a of agents) {
|
|
13244
|
+
if (this.rejectedHostedAgentIds.has(a.im_user_id)) {
|
|
13245
|
+
continue;
|
|
13246
|
+
}
|
|
11685
13247
|
let caps = [];
|
|
11686
13248
|
try {
|
|
11687
13249
|
caps = JSON.parse(a.capabilities);
|
|
@@ -11731,6 +13293,7 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11731
13293
|
* Used to populate the `agents` list in agent.host.declare.
|
|
11732
13294
|
*/
|
|
11733
13295
|
setHostedAgent(agent) {
|
|
13296
|
+
this.rejectedHostedAgentIds.delete(agent.imUserId);
|
|
11734
13297
|
this.hostedAgents.set(agent.imUserId, {
|
|
11735
13298
|
imUserId: agent.imUserId,
|
|
11736
13299
|
name: agent.name,
|
|
@@ -11763,6 +13326,7 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11763
13326
|
);
|
|
11764
13327
|
});
|
|
11765
13328
|
tx(payload);
|
|
13329
|
+
this.rejectedHostedAgentIds.delete(payload.imUserId);
|
|
11766
13330
|
this.loadAgentsFromDb();
|
|
11767
13331
|
if (this.wsConnected) this.sendDeclare();
|
|
11768
13332
|
return {
|
|
@@ -11783,10 +13347,10 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11783
13347
|
const rawJson = process.env.PRISMER_HOSTED_AGENT_JSON;
|
|
11784
13348
|
let raw;
|
|
11785
13349
|
if (rawFile) {
|
|
11786
|
-
if (!(0,
|
|
13350
|
+
if (!(0, import_node_fs19.existsSync)(rawFile)) {
|
|
11787
13351
|
throw new Error(`PRISMER_HOSTED_AGENT_FILE not found: ${rawFile}`);
|
|
11788
13352
|
}
|
|
11789
|
-
raw = (0,
|
|
13353
|
+
raw = (0, import_node_fs19.readFileSync)(rawFile, "utf8");
|
|
11790
13354
|
} else if (rawJson) {
|
|
11791
13355
|
raw = rawJson;
|
|
11792
13356
|
}
|
|
@@ -11873,6 +13437,19 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11873
13437
|
process.stdout.write(`[daemon] ws authenticated, sending agent.host.declare (${this.hostedAgents.size} agents)
|
|
11874
13438
|
`);
|
|
11875
13439
|
this.sendDeclare();
|
|
13440
|
+
if (!this.pendingReplyRecoveryDone) {
|
|
13441
|
+
this.pendingReplyRecoveryDone = true;
|
|
13442
|
+
void recoverPendingReplies({ cloud: this.cloud, cache: this.pendingReplyCache }).then((summary) => {
|
|
13443
|
+
if (summary.attempted === 0) return;
|
|
13444
|
+
process.stdout.write(
|
|
13445
|
+
`[daemon] dispatch.recovery summary attempted=${summary.attempted} committed=${summary.committed} aborted=${summary.aborted} failed=${summary.failed}
|
|
13446
|
+
`
|
|
13447
|
+
);
|
|
13448
|
+
}).catch((err) => {
|
|
13449
|
+
process.stderr.write(`[daemon] dispatch.recovery threw: ${err.message}
|
|
13450
|
+
`);
|
|
13451
|
+
});
|
|
13452
|
+
}
|
|
11876
13453
|
return;
|
|
11877
13454
|
}
|
|
11878
13455
|
this.handleIncoming(msg);
|
|
@@ -11882,7 +13459,7 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11882
13459
|
const payload = {
|
|
11883
13460
|
daemonId: this.config.daemon_id,
|
|
11884
13461
|
daemonVersion: this.opts.daemonVersion ?? "0.0.0",
|
|
11885
|
-
platform: (0,
|
|
13462
|
+
platform: (0, import_node_os8.platform)() === "win32" ? "win32" : (0, import_node_os8.platform)() === "linux" ? "linux" : "darwin",
|
|
11886
13463
|
agents: Array.from(this.hostedAgents.values()).map((a) => ({
|
|
11887
13464
|
imUserId: a.imUserId,
|
|
11888
13465
|
name: a.name,
|
|
@@ -11919,12 +13496,91 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11919
13496
|
case "asset.changed":
|
|
11920
13497
|
void this.onAssetChanged(msg.payload);
|
|
11921
13498
|
return;
|
|
13499
|
+
// v2.0 §4.8.1 (Wave 4-E4) — webhook reverse-channel RPC. Cloud's
|
|
13500
|
+
// dispatchExternalMention pushes an AgentDispatchRequest plus an
|
|
13501
|
+
// embedded `_rpcId`; the daemon runs the same handler as the
|
|
13502
|
+
// HTTP /dispatch path and echoes the reply back over WS with the
|
|
13503
|
+
// identical `_rpcId` so cloud's WsRpcService can settle the
|
|
13504
|
+
// pending promise.
|
|
13505
|
+
case "webhook.dispatch.request":
|
|
13506
|
+
void this.onWebhookDispatch(msg.payload);
|
|
13507
|
+
return;
|
|
11922
13508
|
default:
|
|
11923
13509
|
this.emit("unknown-message", msg);
|
|
11924
13510
|
}
|
|
11925
13511
|
}
|
|
13512
|
+
/**
|
|
13513
|
+
* v2.0 §4.8.1 — webhook dispatch over WS reverse channel.
|
|
13514
|
+
*
|
|
13515
|
+
* Acks via `webhook.dispatch.reply { _rpcId, ok, acceptedAt }` immediately
|
|
13516
|
+
* after `handleAgentMessageDispatch()` returns its synchronous response
|
|
13517
|
+
* — the actual reply (the agent's reply text/attachments) still flows
|
|
13518
|
+
* through `postMessageDispatchReply()` which posts to
|
|
13519
|
+
* `/api/im/dispatch/reply` after the adapter finishes. That mirrors the
|
|
13520
|
+
* HTTP /dispatch contract: ack first, asynchronous final reply via the
|
|
13521
|
+
* dispatch-reply REST endpoint.
|
|
13522
|
+
*
|
|
13523
|
+
* The `_rpcId` field is the only addition versus the HTTP path; we strip
|
|
13524
|
+
* it before passing the payload to the dispatch handler so existing
|
|
13525
|
+
* validation logic doesn't trip on an unknown field.
|
|
13526
|
+
*/
|
|
13527
|
+
async onWebhookDispatch(payload) {
|
|
13528
|
+
const rpcId = payload._rpcId;
|
|
13529
|
+
if (!rpcId) {
|
|
13530
|
+
process.stderr.write(`[daemon] webhook.dispatch.request without _rpcId \u2014 dropped
|
|
13531
|
+
`);
|
|
13532
|
+
return;
|
|
13533
|
+
}
|
|
13534
|
+
const { _rpcId: _, ...request } = payload;
|
|
13535
|
+
try {
|
|
13536
|
+
const handle = handleAgentMessageDispatch(request, {
|
|
13537
|
+
findAgent: (agentImUserId) => this.findMessageDispatchAgent(agentImUserId),
|
|
13538
|
+
postReply: (replyPayload) => this.postMessageDispatchReply(replyPayload),
|
|
13539
|
+
onError: (err, req) => {
|
|
13540
|
+
process.stderr.write(
|
|
13541
|
+
`[daemon] webhook.dispatch.request failed message=${req.messageId} agent=${req.mentionedAgentImUserId}: ${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
13542
|
+
`
|
|
13543
|
+
);
|
|
13544
|
+
}
|
|
13545
|
+
});
|
|
13546
|
+
this.ws.send({
|
|
13547
|
+
type: "webhook.dispatch.reply",
|
|
13548
|
+
payload: {
|
|
13549
|
+
_rpcId: rpcId,
|
|
13550
|
+
ok: handle.response.ok,
|
|
13551
|
+
acceptedAt: handle.response.acceptedAt,
|
|
13552
|
+
error: handle.response.error
|
|
13553
|
+
}
|
|
13554
|
+
});
|
|
13555
|
+
void handle.done.catch((err) => {
|
|
13556
|
+
process.stderr.write(
|
|
13557
|
+
`[daemon] webhook.dispatch final reply failed message=${request.messageId}: ${err instanceof Error ? err.message : String(err)}
|
|
13558
|
+
`
|
|
13559
|
+
);
|
|
13560
|
+
});
|
|
13561
|
+
} catch (err) {
|
|
13562
|
+
this.ws.send({
|
|
13563
|
+
type: "webhook.dispatch.reply",
|
|
13564
|
+
payload: {
|
|
13565
|
+
_rpcId: rpcId,
|
|
13566
|
+
ok: false,
|
|
13567
|
+
error: {
|
|
13568
|
+
code: "daemon_dispatch_failed",
|
|
13569
|
+
message: err instanceof Error ? err.message : String(err)
|
|
13570
|
+
}
|
|
13571
|
+
}
|
|
13572
|
+
});
|
|
13573
|
+
}
|
|
13574
|
+
}
|
|
11926
13575
|
async onHostAcked(payload) {
|
|
11927
13576
|
this.workspaceId = payload.workspaceId;
|
|
13577
|
+
const ownershipLockChanged = await this.applyHostAckOwnershipLock(payload);
|
|
13578
|
+
void this.reportTransport().catch((err) => {
|
|
13579
|
+
process.stderr.write(
|
|
13580
|
+
`[daemon] transport-probe report failed: ${err.message}
|
|
13581
|
+
`
|
|
13582
|
+
);
|
|
13583
|
+
});
|
|
11928
13584
|
if (this.memoryWiring && this.workspaceId) {
|
|
11929
13585
|
syncMemoryFromCloud(this.memoryWiring, this.cloud, [this.workspaceId]).catch(
|
|
11930
13586
|
(err) => log7.error("Initial memory sync failed", err.message)
|
|
@@ -11934,7 +13590,9 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11934
13590
|
(err) => log7.error("Asset sync failed", err.message)
|
|
11935
13591
|
);
|
|
11936
13592
|
await this.ensureDropFolderRuntime(this.workspaceId);
|
|
11937
|
-
|
|
13593
|
+
const profilesToSync = payload.profilesToSync ?? [];
|
|
13594
|
+
const profilesToDelete = payload.profilesToDelete ?? [];
|
|
13595
|
+
for (const id of profilesToSync) {
|
|
11938
13596
|
try {
|
|
11939
13597
|
await this.servicePool.drop(id);
|
|
11940
13598
|
await this.syncProfileFromCloud(id);
|
|
@@ -11942,7 +13600,7 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11942
13600
|
this.emit("sync-error", err);
|
|
11943
13601
|
}
|
|
11944
13602
|
}
|
|
11945
|
-
for (const id of
|
|
13603
|
+
for (const id of profilesToDelete) {
|
|
11946
13604
|
const row = this.db.prepare("SELECT agent_im_user_id FROM agent_profiles WHERE id = ?").get(id);
|
|
11947
13605
|
this.db.prepare("DELETE FROM agent_profiles WHERE id = ?").run(id);
|
|
11948
13606
|
if (row) {
|
|
@@ -11951,10 +13609,37 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
11951
13609
|
process.stderr.write(`[daemon] tombstone profile=${id}
|
|
11952
13610
|
`);
|
|
11953
13611
|
}
|
|
11954
|
-
if (
|
|
11955
|
-
if ((
|
|
13612
|
+
if (profilesToDelete.length) this.loadAgentsFromDb();
|
|
13613
|
+
if ((ownershipLockChanged || profilesToSync.length > 0 || profilesToDelete.length > 0) && this.wsConnected) {
|
|
13614
|
+
this.sendDeclare();
|
|
13615
|
+
}
|
|
11956
13616
|
this.emit("host-acked", payload);
|
|
11957
13617
|
}
|
|
13618
|
+
async applyHostAckOwnershipLock(payload) {
|
|
13619
|
+
const rejected = payload.rejectedAgents ?? [];
|
|
13620
|
+
if (rejected.length === 0) return false;
|
|
13621
|
+
let changed = false;
|
|
13622
|
+
for (const rejection of rejected) {
|
|
13623
|
+
if (!rejection?.imUserId) continue;
|
|
13624
|
+
const agentImUserId = rejection.imUserId;
|
|
13625
|
+
this.rejectedHostedAgentIds.set(agentImUserId, {
|
|
13626
|
+
...rejection,
|
|
13627
|
+
rejectedAt: Date.now()
|
|
13628
|
+
});
|
|
13629
|
+
const wasHosted = this.hostedAgents.delete(agentImUserId);
|
|
13630
|
+
const profiles = this.db.prepare("SELECT id FROM agent_profiles WHERE agent_im_user_id = ?").all(agentImUserId);
|
|
13631
|
+
const deleted = this.db.prepare("DELETE FROM agents WHERE im_user_id = ?").run(agentImUserId);
|
|
13632
|
+
await Promise.all(profiles.map((profile) => this.servicePool.drop(profile.id)));
|
|
13633
|
+
if (wasHosted || deleted.changes > 0) {
|
|
13634
|
+
changed = true;
|
|
13635
|
+
process.stderr.write(
|
|
13636
|
+
`[daemon] ownership rejected agent=${agentImUserId} reason=${rejection.reason} owner=${rejection.ownerDaemonId ?? "(unknown)"} \u2014 stop declaring locally
|
|
13637
|
+
`
|
|
13638
|
+
);
|
|
13639
|
+
}
|
|
13640
|
+
}
|
|
13641
|
+
return changed;
|
|
13642
|
+
}
|
|
11958
13643
|
async onTaskDispatch(payload, requestId) {
|
|
11959
13644
|
const targetDaemonId = readTargetDaemonId(payload);
|
|
11960
13645
|
if (targetDaemonId && targetDaemonId !== this.config.daemon_id) {
|
|
@@ -12106,15 +13791,69 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
12106
13791
|
};
|
|
12107
13792
|
}
|
|
12108
13793
|
async postMessageDispatchReply(payload) {
|
|
12109
|
-
const
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
12115
|
-
|
|
13794
|
+
const taskId = `external:${payload.replyToMessageId}`;
|
|
13795
|
+
try {
|
|
13796
|
+
const result = await sendDispatchReplyTwoPhase({
|
|
13797
|
+
taskId,
|
|
13798
|
+
payload: {
|
|
13799
|
+
conversationId: payload.conversationId,
|
|
13800
|
+
replyToken: payload.replyToken,
|
|
13801
|
+
replyToMessageId: payload.replyToMessageId,
|
|
13802
|
+
agentImUserId: payload.agentImUserId,
|
|
13803
|
+
status: payload.status,
|
|
13804
|
+
...payload.replyText !== void 0 ? { replyText: payload.replyText } : {},
|
|
13805
|
+
...payload.attachments ? { attachments: payload.attachments } : {},
|
|
13806
|
+
assetIds: payload.attachments?.map((a) => a.assetId).filter((id) => typeof id === "string") ?? [],
|
|
13807
|
+
completedAt: payload.completedAt,
|
|
13808
|
+
...payload.error ? { error: payload.error } : {}
|
|
13809
|
+
},
|
|
13810
|
+
cloud: this.cloud,
|
|
13811
|
+
cache: this.pendingReplyCache
|
|
13812
|
+
});
|
|
13813
|
+
if (result.status === "aborted") {
|
|
13814
|
+
throw new Error(`dispatch reply aborted by server reaper (replyId=${result.replyId})`);
|
|
13815
|
+
}
|
|
13816
|
+
} catch (err) {
|
|
13817
|
+
throw new Error(
|
|
13818
|
+
err.message?.length ? err.message : `dispatch reply failed`
|
|
13819
|
+
);
|
|
12116
13820
|
}
|
|
12117
13821
|
}
|
|
13822
|
+
/**
|
|
13823
|
+
* v2.0 §4.8.1 (Wave 4-E4) — submit the transport-probe report. Called
|
|
13824
|
+
* from `onHostAcked` so the daemon's container row exists in cloud
|
|
13825
|
+
* before we POST to /runtime/transport-report.
|
|
13826
|
+
*
|
|
13827
|
+
* The gatewayUrl reported here is `http://<local-ipv4>:<localPort>`
|
|
13828
|
+
* (typically `http://192.168.x.x:3210`). For a daemon without a local
|
|
13829
|
+
* HTTP server (startLocalServer=false in tests) we still post the
|
|
13830
|
+
* probe with `gatewayUrl=null` so cloud knows transport='ws' is the
|
|
13831
|
+
* only path.
|
|
13832
|
+
*/
|
|
13833
|
+
async reportTransport() {
|
|
13834
|
+
const hasLocalServer = this.opts.startLocalServer !== false;
|
|
13835
|
+
let gatewayUrl = null;
|
|
13836
|
+
if (hasLocalServer) {
|
|
13837
|
+
const localIp = pickLocalIPv4();
|
|
13838
|
+
if (localIp) {
|
|
13839
|
+
const port = this.opts.localPort ?? DEFAULT_LOCAL_PORT;
|
|
13840
|
+
gatewayUrl = `http://${localIp}:${port}`;
|
|
13841
|
+
}
|
|
13842
|
+
}
|
|
13843
|
+
const probe = buildTransportProbe(gatewayUrl);
|
|
13844
|
+
const result = await reportTransportProbe(this.cloud, this.config.daemon_id, probe);
|
|
13845
|
+
if (!result.ok) {
|
|
13846
|
+
process.stderr.write(
|
|
13847
|
+
`[daemon] transport-probe report rejected status=${result.status} error=${result.error ?? "-"}
|
|
13848
|
+
`
|
|
13849
|
+
);
|
|
13850
|
+
return;
|
|
13851
|
+
}
|
|
13852
|
+
process.stdout.write(
|
|
13853
|
+
`[daemon] transport-probe reported transport=${probe.transport} gateway=${probe.gatewayUrl ?? "null"} private=${probe.gatewayIsPrivate}
|
|
13854
|
+
`
|
|
13855
|
+
);
|
|
13856
|
+
}
|
|
12118
13857
|
onAgentChanged(payload) {
|
|
12119
13858
|
const a = this.hostedAgents.get(payload.agentImUserId);
|
|
12120
13859
|
if (!a) return;
|
|
@@ -12183,18 +13922,22 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
12183
13922
|
const name = agent?.card?.name || agent?.displayName || agent?.username || profile.agentImUserId;
|
|
12184
13923
|
const now = Date.now();
|
|
12185
13924
|
const tx = this.db.transaction(() => {
|
|
12186
|
-
this.
|
|
12187
|
-
|
|
12188
|
-
|
|
12189
|
-
|
|
12190
|
-
|
|
12191
|
-
|
|
12192
|
-
|
|
12193
|
-
|
|
12194
|
-
|
|
12195
|
-
|
|
12196
|
-
|
|
12197
|
-
|
|
13925
|
+
if (this.rejectedHostedAgentIds.has(profile.agentImUserId)) {
|
|
13926
|
+
this.db.prepare("DELETE FROM agents WHERE im_user_id = ?").run(profile.agentImUserId);
|
|
13927
|
+
} else {
|
|
13928
|
+
this.db.prepare(
|
|
13929
|
+
`INSERT OR REPLACE INTO agents
|
|
13930
|
+
(im_user_id, workspace_id, name, adapter_name, capabilities, status, version, synced_at, dirty)
|
|
13931
|
+
VALUES (?, ?, ?, ?, ?, 'offline', 1, ?, 0)`
|
|
13932
|
+
).run(
|
|
13933
|
+
profile.agentImUserId,
|
|
13934
|
+
profile.workspaceId,
|
|
13935
|
+
name,
|
|
13936
|
+
profile.adapterName,
|
|
13937
|
+
JSON.stringify(capabilities),
|
|
13938
|
+
now
|
|
13939
|
+
);
|
|
13940
|
+
}
|
|
12198
13941
|
this.db.prepare(
|
|
12199
13942
|
`INSERT OR REPLACE INTO agent_profiles
|
|
12200
13943
|
(id, workspace_id, agent_im_user_id, adapter_name, name, config, version, synced_at, dirty, deleted_at)
|
|
@@ -12556,9 +14299,9 @@ var Runner = class extends import_node_events3.EventEmitter {
|
|
|
12556
14299
|
function resolveUserAssetWorkspaceDir(workspaceId) {
|
|
12557
14300
|
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
12558
14301
|
if (override) return (0, import_node_path14.join)(override, workspaceId);
|
|
12559
|
-
const home = (0,
|
|
14302
|
+
const home = (0, import_node_os8.homedir)();
|
|
12560
14303
|
const desktop = (0, import_node_path14.join)(home, "Desktop");
|
|
12561
|
-
const base = (0,
|
|
14304
|
+
const base = (0, import_node_fs19.existsSync)(desktop) ? desktop : home;
|
|
12562
14305
|
return (0, import_node_path14.join)(base, "Prismer Assets", workspaceId);
|
|
12563
14306
|
}
|
|
12564
14307
|
function readTargetDaemonId(payload) {
|
|
@@ -12707,9 +14450,9 @@ function buildDaemonCommand() {
|
|
|
12707
14450
|
if (existingPid && pidAlive2(existingPid)) {
|
|
12708
14451
|
exitWithError(`Daemon already running (pid ${existingPid}). Use \`prismer daemon stop\` first.`);
|
|
12709
14452
|
}
|
|
12710
|
-
if (!(0,
|
|
14453
|
+
if (!(0, import_node_fs20.existsSync)(paths.logsDir)) (0, import_node_fs20.mkdirSync)(paths.logsDir, { recursive: true });
|
|
12711
14454
|
const logFile = (0, import_node_path15.join)(paths.logsDir, "daemon.log");
|
|
12712
|
-
const fd = (0,
|
|
14455
|
+
const fd = (0, import_node_fs20.openSync)(logFile, "a");
|
|
12713
14456
|
const args = [process.argv[1], "daemon", "run"];
|
|
12714
14457
|
if (opts.port) args.push("--port", String(opts.port));
|
|
12715
14458
|
if (opts.localServer === false) args.push("--no-local-server");
|
|
@@ -12793,7 +14536,7 @@ function buildDaemonCommand() {
|
|
|
12793
14536
|
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) => {
|
|
12794
14537
|
const paths = resolvePaths();
|
|
12795
14538
|
const logFile = (0, import_node_path15.join)(paths.logsDir, "daemon.log");
|
|
12796
|
-
if (!(0,
|
|
14539
|
+
if (!(0, import_node_fs20.existsSync)(logFile)) {
|
|
12797
14540
|
exitWithError(`No daemon log found at ${logFile}. Start the daemon with \`prismer daemon start\`.`);
|
|
12798
14541
|
}
|
|
12799
14542
|
const lines = Math.max(1, opts.tail);
|
|
@@ -12834,13 +14577,13 @@ async function tailFromEnd(path9, lines) {
|
|
|
12834
14577
|
}
|
|
12835
14578
|
}
|
|
12836
14579
|
async function followFile(path9) {
|
|
12837
|
-
let offset = (0,
|
|
14580
|
+
let offset = (0, import_node_fs20.statSync)(path9).size;
|
|
12838
14581
|
for (; ; ) {
|
|
12839
14582
|
await (0, import_promises2.setTimeout)(1e3);
|
|
12840
|
-
const size = (0,
|
|
14583
|
+
const size = (0, import_node_fs20.statSync)(path9).size;
|
|
12841
14584
|
if (size < offset) offset = 0;
|
|
12842
14585
|
if (size === offset) continue;
|
|
12843
|
-
const stream = (0,
|
|
14586
|
+
const stream = (0, import_node_fs20.createReadStream)(path9, { start: offset, end: size - 1, encoding: "utf8" });
|
|
12844
14587
|
for await (const chunk of stream) process.stdout.write(chunk);
|
|
12845
14588
|
offset = size;
|
|
12846
14589
|
}
|
|
@@ -12848,8 +14591,8 @@ async function followFile(path9) {
|
|
|
12848
14591
|
|
|
12849
14592
|
// src/cli/commands/events.ts
|
|
12850
14593
|
var import_commander9 = require("commander");
|
|
12851
|
-
var
|
|
12852
|
-
var
|
|
14594
|
+
var import_node_fs21 = require("fs");
|
|
14595
|
+
var import_node_os9 = require("os");
|
|
12853
14596
|
var import_node_path16 = require("path");
|
|
12854
14597
|
var import_node_readline = require("readline");
|
|
12855
14598
|
init_util();
|
|
@@ -12857,7 +14600,7 @@ var DEFAULT_LIMIT2 = 50;
|
|
|
12857
14600
|
function buildEventsCommand() {
|
|
12858
14601
|
return addEventOptions(new import_commander9.Command("events").description("Read local PARA events from ~/.prismer/para/events.jsonl")).action(async (opts) => {
|
|
12859
14602
|
const file = eventsPath();
|
|
12860
|
-
if (!(0,
|
|
14603
|
+
if (!(0, import_node_fs21.existsSync)(file)) {
|
|
12861
14604
|
printJson(unavailable(file));
|
|
12862
14605
|
process.exitCode = 1;
|
|
12863
14606
|
return;
|
|
@@ -12874,7 +14617,7 @@ function buildEventsCommand() {
|
|
|
12874
14617
|
function buildEventsStatsCommand() {
|
|
12875
14618
|
return addEventOptions(new import_commander9.Command("events:stats").description("Summarize local PARA events from ~/.prismer/para/events.jsonl")).action(async (opts) => {
|
|
12876
14619
|
const file = eventsPath();
|
|
12877
|
-
if (!(0,
|
|
14620
|
+
if (!(0, import_node_fs21.existsSync)(file)) {
|
|
12878
14621
|
printJson(unavailable(file));
|
|
12879
14622
|
process.exitCode = 1;
|
|
12880
14623
|
return;
|
|
@@ -12883,7 +14626,7 @@ function buildEventsStatsCommand() {
|
|
|
12883
14626
|
const events = await readEvents(file, filters);
|
|
12884
14627
|
printJson({
|
|
12885
14628
|
ok: true,
|
|
12886
|
-
data: { stats:
|
|
14629
|
+
data: { stats: summarize3(events), count: events.length, limit: filters.limit },
|
|
12887
14630
|
checks: { file, exists: true, filters: cleanFilters(filters) }
|
|
12888
14631
|
});
|
|
12889
14632
|
});
|
|
@@ -12892,7 +14635,7 @@ function addEventOptions(cmd) {
|
|
|
12892
14635
|
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)");
|
|
12893
14636
|
}
|
|
12894
14637
|
function eventsPath() {
|
|
12895
|
-
return (0, import_node_path16.join)(process.env.PRISMER_HOME ?? (0, import_node_path16.join)((0,
|
|
14638
|
+
return (0, import_node_path16.join)(process.env.PRISMER_HOME ?? (0, import_node_path16.join)((0, import_node_os9.homedir)(), ".prismer"), "para", "events.jsonl");
|
|
12896
14639
|
}
|
|
12897
14640
|
function unavailable(file) {
|
|
12898
14641
|
return {
|
|
@@ -12908,7 +14651,7 @@ function unavailable(file) {
|
|
|
12908
14651
|
async function readEvents(file, filters) {
|
|
12909
14652
|
const out = [];
|
|
12910
14653
|
const rl = (0, import_node_readline.createInterface)({
|
|
12911
|
-
input: (0,
|
|
14654
|
+
input: (0, import_node_fs21.createReadStream)(file, { encoding: "utf8" }),
|
|
12912
14655
|
crlfDelay: Infinity
|
|
12913
14656
|
});
|
|
12914
14657
|
for await (const line of rl) {
|
|
@@ -12940,7 +14683,7 @@ function field(event, names) {
|
|
|
12940
14683
|
}
|
|
12941
14684
|
return void 0;
|
|
12942
14685
|
}
|
|
12943
|
-
function
|
|
14686
|
+
function summarize3(events) {
|
|
12944
14687
|
const byFamily = {};
|
|
12945
14688
|
const byType = {};
|
|
12946
14689
|
const byAgentId = {};
|
|
@@ -12978,7 +14721,7 @@ function parsePositiveInt4(v) {
|
|
|
12978
14721
|
// src/cli/commands/memory.ts
|
|
12979
14722
|
var import_better_sqlite35 = __toESM(require("better-sqlite3"), 1);
|
|
12980
14723
|
var import_commander10 = require("commander");
|
|
12981
|
-
var
|
|
14724
|
+
var import_node_fs22 = require("fs");
|
|
12982
14725
|
init_util();
|
|
12983
14726
|
var LOCAL_BASE = process.env.PRISMER_DAEMON_URL ?? "http://127.0.0.1:3210";
|
|
12984
14727
|
function buildMemoryCommand() {
|
|
@@ -13130,7 +14873,7 @@ function readCacheSnapshot(limit) {
|
|
|
13130
14873
|
const paths = resolvePaths();
|
|
13131
14874
|
const empty = {
|
|
13132
14875
|
dbPath: paths.localDb,
|
|
13133
|
-
dbExists: (0,
|
|
14876
|
+
dbExists: (0, import_node_fs22.existsSync)(paths.localDb),
|
|
13134
14877
|
tables: {
|
|
13135
14878
|
cached_assets: { exists: false, count: 0, sizeBytes: 0 },
|
|
13136
14879
|
workspace_files_mirror: { exists: false, count: 0 }
|
|
@@ -13253,16 +14996,16 @@ var import_commander11 = require("commander");
|
|
|
13253
14996
|
|
|
13254
14997
|
// src/pair.ts
|
|
13255
14998
|
var import_node_crypto11 = require("crypto");
|
|
13256
|
-
var
|
|
14999
|
+
var import_node_os11 = require("os");
|
|
13257
15000
|
var import_promises3 = require("timers/promises");
|
|
13258
15001
|
var import_qrcode = __toESM(require("qrcode"), 1);
|
|
13259
15002
|
|
|
13260
15003
|
// src/daemon-id.ts
|
|
13261
15004
|
var import_node_crypto10 = require("crypto");
|
|
13262
|
-
var
|
|
15005
|
+
var import_node_os10 = require("os");
|
|
13263
15006
|
var PREFIX = "daemon-";
|
|
13264
15007
|
function sanitizeHostname() {
|
|
13265
|
-
return (0,
|
|
15008
|
+
return (0, import_node_os10.hostname)().replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "unknown";
|
|
13266
15009
|
}
|
|
13267
15010
|
function newDaemonId(opts = {}) {
|
|
13268
15011
|
const host = opts.hostnameOverride ?? sanitizeHostname();
|
|
@@ -13301,7 +15044,7 @@ async function pair(opts) {
|
|
|
13301
15044
|
"/api/im/pair/offer",
|
|
13302
15045
|
{
|
|
13303
15046
|
auth: false,
|
|
13304
|
-
body: { devicePub, deviceName: opts.deviceName ?? (0,
|
|
15047
|
+
body: { devicePub, deviceName: opts.deviceName ?? (0, import_node_os11.hostname)() }
|
|
13305
15048
|
}
|
|
13306
15049
|
);
|
|
13307
15050
|
if (!offerRes.ok) {
|
|
@@ -13422,8 +15165,8 @@ function buildPairCommand() {
|
|
|
13422
15165
|
|
|
13423
15166
|
// src/cli/commands/profile.ts
|
|
13424
15167
|
var import_commander12 = require("commander");
|
|
13425
|
-
var
|
|
13426
|
-
var
|
|
15168
|
+
var import_node_fs23 = require("fs");
|
|
15169
|
+
var import_node_os12 = require("os");
|
|
13427
15170
|
var import_node_path17 = require("path");
|
|
13428
15171
|
var import_node_child_process8 = require("child_process");
|
|
13429
15172
|
|
|
@@ -13548,12 +15291,12 @@ function buildProfileCommand() {
|
|
|
13548
15291
|
const profile = await cloud.get(
|
|
13549
15292
|
`/api/im/agent_profiles/${encodeURIComponent(profileId)}`
|
|
13550
15293
|
);
|
|
13551
|
-
const tmpFile = (0, import_node_path17.join)((0,
|
|
13552
|
-
(0,
|
|
15294
|
+
const tmpFile = (0, import_node_path17.join)((0, import_node_os12.tmpdir)(), `prismer-profile-${profileId}.json`);
|
|
15295
|
+
(0, import_node_fs23.writeFileSync)(tmpFile, JSON.stringify(profile.config, null, 2), "utf8");
|
|
13553
15296
|
const editor = process.env.EDITOR || "vi";
|
|
13554
15297
|
const ed = (0, import_node_child_process8.spawnSync)(editor, [tmpFile], { stdio: "inherit" });
|
|
13555
15298
|
if (ed.status !== 0) exitWithError(`editor exited ${ed.status}`, { code: "editor_failed" });
|
|
13556
|
-
const newConfig = JSON.parse((0,
|
|
15299
|
+
const newConfig = JSON.parse((0, import_node_fs23.readFileSync)(tmpFile, "utf8"));
|
|
13557
15300
|
const res = await cloud.request("PATCH", `/api/im/agent_profiles/${encodeURIComponent(profileId)}`, {
|
|
13558
15301
|
body: { config: newConfig, version: profile.version }
|
|
13559
15302
|
});
|
|
@@ -13575,8 +15318,8 @@ function mkCloud4() {
|
|
|
13575
15318
|
function readJsonArg(arg) {
|
|
13576
15319
|
if (arg.startsWith("@")) {
|
|
13577
15320
|
const path9 = arg.slice(1);
|
|
13578
|
-
if (!(0,
|
|
13579
|
-
return JSON.parse((0,
|
|
15321
|
+
if (!(0, import_node_fs23.existsSync)(path9)) throw new Error(`File not found: ${path9}`);
|
|
15322
|
+
return JSON.parse((0, import_node_fs23.readFileSync)(path9, "utf8"));
|
|
13580
15323
|
}
|
|
13581
15324
|
return JSON.parse(arg);
|
|
13582
15325
|
}
|
|
@@ -13590,8 +15333,8 @@ async function resolveDefaultWorkspaceId(cloud) {
|
|
|
13590
15333
|
// src/cli/commands/reset.ts
|
|
13591
15334
|
var import_commander13 = require("commander");
|
|
13592
15335
|
var import_node_child_process9 = require("child_process");
|
|
13593
|
-
var
|
|
13594
|
-
var
|
|
15336
|
+
var import_node_fs24 = require("fs");
|
|
15337
|
+
var import_node_os13 = require("os");
|
|
13595
15338
|
var import_node_path18 = require("path");
|
|
13596
15339
|
var import_promises4 = require("timers/promises");
|
|
13597
15340
|
init_util();
|
|
@@ -13609,15 +15352,15 @@ function buildResetCommand() {
|
|
|
13609
15352
|
const hermesGatewayPids = findHermesGatewayPids2();
|
|
13610
15353
|
const plan = {
|
|
13611
15354
|
root: paths.root,
|
|
13612
|
-
exists: (0,
|
|
15355
|
+
exists: (0, import_node_fs24.existsSync)(paths.root),
|
|
13613
15356
|
runningPid,
|
|
13614
15357
|
mode: opts.wipe ? "wipe" : "archive",
|
|
13615
15358
|
archivePath,
|
|
13616
15359
|
assetSyncRoot,
|
|
13617
|
-
assetSyncExists: (0,
|
|
15360
|
+
assetSyncExists: (0, import_node_fs24.existsSync)(assetSyncRoot),
|
|
13618
15361
|
assetSyncArchivePath,
|
|
13619
15362
|
hermesHome,
|
|
13620
|
-
hermesExists: (0,
|
|
15363
|
+
hermesExists: (0, import_node_fs24.existsSync)(hermesHome),
|
|
13621
15364
|
hermesArchivePath,
|
|
13622
15365
|
hermesGatewayPids,
|
|
13623
15366
|
willStopDaemon: runningPid !== null,
|
|
@@ -13641,40 +15384,40 @@ function buildResetCommand() {
|
|
|
13641
15384
|
for (const gatewayPid of hermesGatewayPids) {
|
|
13642
15385
|
await stopProcess(gatewayPid, opts.timeout ?? 5e3, "Hermes gateway");
|
|
13643
15386
|
}
|
|
13644
|
-
if ((0,
|
|
15387
|
+
if ((0, import_node_fs24.existsSync)(paths.root)) {
|
|
13645
15388
|
if (opts.wipe) {
|
|
13646
|
-
(0,
|
|
15389
|
+
(0, import_node_fs24.rmSync)(paths.root, { recursive: true, force: true });
|
|
13647
15390
|
} else {
|
|
13648
15391
|
if (!archivePath) throw new Error("internal: archivePath missing");
|
|
13649
|
-
if (!(0,
|
|
13650
|
-
(0,
|
|
15392
|
+
if (!(0, import_node_fs24.existsSync)((0, import_node_path18.dirname)(archivePath))) {
|
|
15393
|
+
(0, import_node_fs24.mkdirSync)((0, import_node_path18.dirname)(archivePath), { recursive: true });
|
|
13651
15394
|
}
|
|
13652
|
-
(0,
|
|
15395
|
+
(0, import_node_fs24.renameSync)(paths.root, archivePath);
|
|
13653
15396
|
}
|
|
13654
15397
|
}
|
|
13655
|
-
if ((0,
|
|
15398
|
+
if ((0, import_node_fs24.existsSync)(assetSyncRoot)) {
|
|
13656
15399
|
if (opts.wipe) {
|
|
13657
|
-
(0,
|
|
15400
|
+
(0, import_node_fs24.rmSync)(assetSyncRoot, { recursive: true, force: true });
|
|
13658
15401
|
} else {
|
|
13659
15402
|
if (!assetSyncArchivePath) throw new Error("internal: assetSyncArchivePath missing");
|
|
13660
|
-
if (!(0,
|
|
13661
|
-
(0,
|
|
15403
|
+
if (!(0, import_node_fs24.existsSync)((0, import_node_path18.dirname)(assetSyncArchivePath))) {
|
|
15404
|
+
(0, import_node_fs24.mkdirSync)((0, import_node_path18.dirname)(assetSyncArchivePath), { recursive: true });
|
|
13662
15405
|
}
|
|
13663
|
-
(0,
|
|
15406
|
+
(0, import_node_fs24.renameSync)(assetSyncRoot, assetSyncArchivePath);
|
|
13664
15407
|
}
|
|
13665
15408
|
}
|
|
13666
|
-
if ((0,
|
|
15409
|
+
if ((0, import_node_fs24.existsSync)(hermesHome)) {
|
|
13667
15410
|
if (opts.wipe) {
|
|
13668
|
-
(0,
|
|
15411
|
+
(0, import_node_fs24.rmSync)(hermesHome, { recursive: true, force: true });
|
|
13669
15412
|
} else {
|
|
13670
15413
|
if (!hermesArchivePath) throw new Error("internal: hermesArchivePath missing");
|
|
13671
|
-
if (!(0,
|
|
13672
|
-
(0,
|
|
15414
|
+
if (!(0, import_node_fs24.existsSync)((0, import_node_path18.dirname)(hermesArchivePath))) {
|
|
15415
|
+
(0, import_node_fs24.mkdirSync)((0, import_node_path18.dirname)(hermesArchivePath), { recursive: true });
|
|
13673
15416
|
}
|
|
13674
|
-
(0,
|
|
15417
|
+
(0, import_node_fs24.renameSync)(hermesHome, hermesArchivePath);
|
|
13675
15418
|
}
|
|
13676
15419
|
}
|
|
13677
|
-
(0,
|
|
15420
|
+
(0, import_node_fs24.mkdirSync)(paths.root, { recursive: true });
|
|
13678
15421
|
if (opts.json) {
|
|
13679
15422
|
printJson({ ok: true, reset: true, plan });
|
|
13680
15423
|
return;
|
|
@@ -13743,13 +15486,13 @@ function timestamp() {
|
|
|
13743
15486
|
function resolveAssetSyncRoot() {
|
|
13744
15487
|
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
13745
15488
|
if (override) return override;
|
|
13746
|
-
const home = (0,
|
|
15489
|
+
const home = (0, import_node_os13.homedir)();
|
|
13747
15490
|
const desktop = (0, import_node_path18.join)(home, "Desktop");
|
|
13748
|
-
const base = (0,
|
|
15491
|
+
const base = (0, import_node_fs24.existsSync)(desktop) ? desktop : home;
|
|
13749
15492
|
return (0, import_node_path18.join)(base, "Prismer Assets");
|
|
13750
15493
|
}
|
|
13751
15494
|
function resolveHermesHome() {
|
|
13752
|
-
return process.env.HERMES_HOME || (0, import_node_path18.join)((0,
|
|
15495
|
+
return process.env.HERMES_HOME || (0, import_node_path18.join)((0, import_node_os13.homedir)(), ".hermes");
|
|
13753
15496
|
}
|
|
13754
15497
|
function findHermesGatewayPids2() {
|
|
13755
15498
|
try {
|
|
@@ -13979,10 +15722,10 @@ async function safeParseResponse(res) {
|
|
|
13979
15722
|
|
|
13980
15723
|
// src/cli/commands/setup.ts
|
|
13981
15724
|
var import_commander15 = require("commander");
|
|
13982
|
-
var
|
|
15725
|
+
var import_node_os14 = require("os");
|
|
13983
15726
|
var import_node_child_process10 = require("child_process");
|
|
13984
15727
|
var import_node_crypto12 = require("crypto");
|
|
13985
|
-
var
|
|
15728
|
+
var import_node_fs25 = require("fs");
|
|
13986
15729
|
var import_node_http2 = require("http");
|
|
13987
15730
|
init_util();
|
|
13988
15731
|
init_ui();
|
|
@@ -14024,7 +15767,7 @@ function buildSetupCommand() {
|
|
|
14024
15767
|
}
|
|
14025
15768
|
const result = await pair({
|
|
14026
15769
|
cloudBaseUrl,
|
|
14027
|
-
deviceName: opts.deviceName ?? (0,
|
|
15770
|
+
deviceName: opts.deviceName ?? (0, import_node_os14.hostname)(),
|
|
14028
15771
|
force: opts.force,
|
|
14029
15772
|
paths,
|
|
14030
15773
|
asUserEmail: opts.asUser
|
|
@@ -14041,13 +15784,13 @@ function buildSetupCommand() {
|
|
|
14041
15784
|
apiKey = await mintDaemonApiKey({
|
|
14042
15785
|
cloudBaseUrl,
|
|
14043
15786
|
token: authToken,
|
|
14044
|
-
deviceName: opts.deviceName ?? (0,
|
|
15787
|
+
deviceName: opts.deviceName ?? (0, import_node_os14.hostname)()
|
|
14045
15788
|
});
|
|
14046
15789
|
}
|
|
14047
15790
|
if (!apiKey && !authToken && !opts.check && opts.browser !== false) {
|
|
14048
15791
|
apiKey = await runBrowserSetup({
|
|
14049
15792
|
cloudBaseUrl,
|
|
14050
|
-
deviceName: opts.deviceName ?? (0,
|
|
15793
|
+
deviceName: opts.deviceName ?? (0, import_node_os14.hostname)(),
|
|
14051
15794
|
json: Boolean(opts.json)
|
|
14052
15795
|
});
|
|
14053
15796
|
}
|
|
@@ -14233,9 +15976,9 @@ function shouldArchiveLocalDb(previous, next) {
|
|
|
14233
15976
|
return previous.api_key !== next.api_key || previous.cloud_api_base !== next.cloud_api_base || previous.daemon_id !== next.daemon_id;
|
|
14234
15977
|
}
|
|
14235
15978
|
function archiveLocalDb(localDbPath) {
|
|
14236
|
-
if (!(0,
|
|
15979
|
+
if (!(0, import_node_fs25.existsSync)(localDbPath)) return;
|
|
14237
15980
|
const archived = `${localDbPath}.${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
|
|
14238
|
-
(0,
|
|
15981
|
+
(0, import_node_fs25.renameSync)(localDbPath, archived);
|
|
14239
15982
|
}
|
|
14240
15983
|
async function mintDaemonApiKey(input) {
|
|
14241
15984
|
const label = `Daemon: ${input.deviceName} ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
|
|
@@ -14304,7 +16047,7 @@ function buildSkillCommand() {
|
|
|
14304
16047
|
const cloud = mkCloud6();
|
|
14305
16048
|
const path9 = opts.agent ? `/api/im/agents/${encodeURIComponent(opts.agent)}/skills` : `/api/im/skills/${encodeURIComponent(slugOrId)}/install`;
|
|
14306
16049
|
const body = opts.agent ? { skillId: slugOrId, workspaceId: opts.workspaceId, version: opts.version } : { workspaceId: opts.workspaceId, version: opts.version };
|
|
14307
|
-
const data = await
|
|
16050
|
+
const data = await requestEnvelope2(cloud, "POST", path9, body, "skill_install_failed");
|
|
14308
16051
|
if (opts.json) {
|
|
14309
16052
|
printJson(data);
|
|
14310
16053
|
return;
|
|
@@ -14315,7 +16058,7 @@ function buildSkillCommand() {
|
|
|
14315
16058
|
const cloud = mkCloud6();
|
|
14316
16059
|
const path9 = opts.agent ? `/api/im/agents/${encodeURIComponent(opts.agent)}/skills` : `/api/im/skills/${encodeURIComponent(slugOrId)}/install`;
|
|
14317
16060
|
const body = opts.agent ? { skillId: slugOrId, workspaceId: opts.workspaceId } : void 0;
|
|
14318
|
-
const data = await
|
|
16061
|
+
const data = await requestEnvelope2(cloud, "DELETE", path9, body, "skill_uninstall_failed");
|
|
14319
16062
|
if (opts.json) {
|
|
14320
16063
|
printJson(data);
|
|
14321
16064
|
return;
|
|
@@ -14368,7 +16111,7 @@ function mkCloud6() {
|
|
|
14368
16111
|
const cfg = loadConfig(resolvePaths());
|
|
14369
16112
|
return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
14370
16113
|
}
|
|
14371
|
-
async function
|
|
16114
|
+
async function requestEnvelope2(cloud, method, path9, body, code) {
|
|
14372
16115
|
const res = await cloud.request(method, path9, {
|
|
14373
16116
|
body
|
|
14374
16117
|
});
|
|
@@ -14487,7 +16230,7 @@ function printSkill(skill, includeContent) {
|
|
|
14487
16230
|
|
|
14488
16231
|
// src/cli/commands/status.ts
|
|
14489
16232
|
var import_commander17 = require("commander");
|
|
14490
|
-
var
|
|
16233
|
+
var import_node_os15 = require("os");
|
|
14491
16234
|
init_util();
|
|
14492
16235
|
init_ui();
|
|
14493
16236
|
function buildStatusCommand() {
|
|
@@ -14518,6 +16261,7 @@ function buildStatusCommand() {
|
|
|
14518
16261
|
let me = null;
|
|
14519
16262
|
let devices = null;
|
|
14520
16263
|
let agents = null;
|
|
16264
|
+
let diagnose = null;
|
|
14521
16265
|
try {
|
|
14522
16266
|
const meRes = await cloud.request("GET", "/api/im/me", { timeoutMs: 3e3 });
|
|
14523
16267
|
cloudOk = meRes.ok;
|
|
@@ -14530,19 +16274,33 @@ function buildStatusCommand() {
|
|
|
14530
16274
|
if (Array.isArray(wsList) && wsList.length > 0) {
|
|
14531
16275
|
const wsId = wsList[0]?.id;
|
|
14532
16276
|
if (wsId) {
|
|
14533
|
-
const
|
|
14534
|
-
if (
|
|
14535
|
-
const
|
|
14536
|
-
|
|
14537
|
-
|
|
14538
|
-
|
|
14539
|
-
|
|
14540
|
-
|
|
14541
|
-
agents = agBody?.data;
|
|
16277
|
+
const rtRes = await cloud.request("GET", `/api/im/workspaces/${encodeURIComponent(wsId)}/runtime`, { timeoutMs: 3e3 });
|
|
16278
|
+
if (rtRes.ok) {
|
|
16279
|
+
const rtBody = rtRes.data;
|
|
16280
|
+
const snapshot = rtBody?.data;
|
|
16281
|
+
if (snapshot && Array.isArray(snapshot.devices)) {
|
|
16282
|
+
devices = snapshot.devices;
|
|
16283
|
+
agents = snapshot.devices.flatMap((d) => d.agents ?? []);
|
|
16284
|
+
}
|
|
14542
16285
|
}
|
|
14543
16286
|
}
|
|
14544
16287
|
}
|
|
14545
16288
|
}
|
|
16289
|
+
const daemonId = cfg.daemon_id;
|
|
16290
|
+
if (daemonId) {
|
|
16291
|
+
try {
|
|
16292
|
+
const diagRes = await cloud.request(
|
|
16293
|
+
"GET",
|
|
16294
|
+
`/api/im/runtime/diagnose?daemonId=${encodeURIComponent(daemonId)}`,
|
|
16295
|
+
{ timeoutMs: 3e3 }
|
|
16296
|
+
);
|
|
16297
|
+
if (diagRes.ok) {
|
|
16298
|
+
const body = diagRes.data;
|
|
16299
|
+
if (body?.data && typeof body.data === "object") diagnose = body.data;
|
|
16300
|
+
}
|
|
16301
|
+
} catch {
|
|
16302
|
+
}
|
|
16303
|
+
}
|
|
14546
16304
|
}
|
|
14547
16305
|
} catch {
|
|
14548
16306
|
cloudOk = false;
|
|
@@ -14559,7 +16317,8 @@ function buildStatusCommand() {
|
|
|
14559
16317
|
info: daemonStatus.info ?? {}
|
|
14560
16318
|
},
|
|
14561
16319
|
cloud: { base: cfg.cloud_api_base, reachable: cloudOk, me, devices, agents },
|
|
14562
|
-
local
|
|
16320
|
+
local,
|
|
16321
|
+
diagnose
|
|
14563
16322
|
};
|
|
14564
16323
|
if (opts.json) {
|
|
14565
16324
|
printJson(report);
|
|
@@ -14568,6 +16327,60 @@ function buildStatusCommand() {
|
|
|
14568
16327
|
printPretty(report);
|
|
14569
16328
|
});
|
|
14570
16329
|
}
|
|
16330
|
+
function computeDeviceIcon(lastSeenAt, now) {
|
|
16331
|
+
if (!lastSeenAt) return "\u25CB";
|
|
16332
|
+
const age = now.getTime() - Date.parse(lastSeenAt);
|
|
16333
|
+
if (!Number.isFinite(age) || age < 0) return "\u25CB";
|
|
16334
|
+
if (age < 5 * 6e4) return "\u25CF";
|
|
16335
|
+
if (age < 60 * 6e4) return "\u25D0";
|
|
16336
|
+
return "\u25CB";
|
|
16337
|
+
}
|
|
16338
|
+
function computeAgentIcon(status, lastHeartbeat, now) {
|
|
16339
|
+
const s = (status || "").toLowerCase();
|
|
16340
|
+
if (s === "error" || s === "failed") return "\u25C6";
|
|
16341
|
+
if (!lastHeartbeat) return s === "offline" ? "\u25CB" : "\u25D0";
|
|
16342
|
+
const age = now.getTime() - Date.parse(lastHeartbeat);
|
|
16343
|
+
if (!Number.isFinite(age) || age < 0 || age >= 60 * 6e4) return "\u25CB";
|
|
16344
|
+
if (age >= 5 * 6e4) return "\u25D0";
|
|
16345
|
+
return "\u25CF";
|
|
16346
|
+
}
|
|
16347
|
+
function pad(value, width) {
|
|
16348
|
+
if (value.length >= width) return value.slice(0, width);
|
|
16349
|
+
return value + " ".repeat(width - value.length);
|
|
16350
|
+
}
|
|
16351
|
+
function formatDeviceBindings(devices, now = /* @__PURE__ */ new Date()) {
|
|
16352
|
+
const lines = [];
|
|
16353
|
+
if (devices.length === 0) {
|
|
16354
|
+
lines.push(" No paired devices.");
|
|
16355
|
+
return lines;
|
|
16356
|
+
}
|
|
16357
|
+
const totalAgents = devices.reduce((s, d) => s + (d.agents?.length ?? 0), 0);
|
|
16358
|
+
const devWord = devices.length === 1 ? "device" : "devices";
|
|
16359
|
+
const agWord = totalAgents === 1 ? "agent" : "agents";
|
|
16360
|
+
lines.push(` Device & Agent Bindings (${devices.length} ${devWord} \xB7 ${totalAgents} ${agWord})`);
|
|
16361
|
+
lines.push("");
|
|
16362
|
+
for (let di = 0; di < devices.length; di++) {
|
|
16363
|
+
const d = devices[di];
|
|
16364
|
+
const dIcon = computeDeviceIcon(d.lastSeenAt, now);
|
|
16365
|
+
const dName = pad(d.name || d.deviceId, 40);
|
|
16366
|
+
const lastSeen = d.lastSeenAt ? formatRelative(d.lastSeenAt, now) : "never seen";
|
|
16367
|
+
lines.push(` ${dIcon} ${dName} ${lastSeen}`);
|
|
16368
|
+
const agents = d.agents ?? [];
|
|
16369
|
+
for (let ai = 0; ai < agents.length; ai++) {
|
|
16370
|
+
const a = agents[ai];
|
|
16371
|
+
const isLast = ai === agents.length - 1;
|
|
16372
|
+
const branch = isLast ? "\u2514\u2500" : "\u251C\u2500";
|
|
16373
|
+
const aIcon = computeAgentIcon(a.status, a.lastHeartbeat, now);
|
|
16374
|
+
const aName = pad(a.name || a.id, 24);
|
|
16375
|
+
const aStatus = pad(a.status || "?", 8);
|
|
16376
|
+
const aVersion = pad(a.version ? `v${a.version}` : "\u2014", 10);
|
|
16377
|
+
const tail = a.currentTaskId ? `task=${a.currentTaskId}` : a.lastHeartbeat ? `\u2764 ${formatRelative(a.lastHeartbeat, now)}` : "";
|
|
16378
|
+
lines.push(` ${branch} ${aIcon} ${aName} ${aStatus} ${aVersion} ${tail}`.trimEnd());
|
|
16379
|
+
}
|
|
16380
|
+
if (di < devices.length - 1) lines.push("");
|
|
16381
|
+
}
|
|
16382
|
+
return lines;
|
|
16383
|
+
}
|
|
14571
16384
|
async function readDaemonStatus() {
|
|
14572
16385
|
try {
|
|
14573
16386
|
const res = await fetch("http://127.0.0.1:3210/healthz", {
|
|
@@ -14611,7 +16424,7 @@ function printPretty(report) {
|
|
|
14611
16424
|
ui.blank();
|
|
14612
16425
|
ok("Config", report.paths.config);
|
|
14613
16426
|
if (report.daemon.running) {
|
|
14614
|
-
const daemonName = report.cloud.me && typeof report.cloud.me === "object" ? report.cloud.me.user?.username ?? (0,
|
|
16427
|
+
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)();
|
|
14615
16428
|
const ws = report.daemon.wsConnected ? "connected" : "pending";
|
|
14616
16429
|
ok("Daemon", `${daemonName} pid=${report.daemon.pid} ws=${ws}`);
|
|
14617
16430
|
if (report.daemon.info) {
|
|
@@ -14640,16 +16453,7 @@ function printPretty(report) {
|
|
|
14640
16453
|
if (Array.isArray(report.cloud.devices) && report.cloud.devices.length > 0) {
|
|
14641
16454
|
const devs = report.cloud.devices;
|
|
14642
16455
|
ui.blank();
|
|
14643
|
-
|
|
14644
|
-
for (const d of devs) {
|
|
14645
|
-
const statusIcon = d.daemonStatus === "connected" ? "\u25CF" : "\u25CB";
|
|
14646
|
-
const kind = d.runtimeKind === "docker" ? "Local" : "K8s";
|
|
14647
|
-
const declared = d.hostedAgentSummary?.declared ?? 0;
|
|
14648
|
-
ui.line(` ${statusIcon} ${d.podName?.replace(/^daemon:/, "")?.slice(0, 28) ?? "?"} kind=${kind} agents=${declared}`);
|
|
14649
|
-
}
|
|
14650
|
-
}
|
|
14651
|
-
if (Array.isArray(report.cloud.agents) && report.cloud.agents.length > 0) {
|
|
14652
|
-
ui.line(` Hosted agents: ${report.cloud.agents.length}`);
|
|
16456
|
+
for (const ln of formatDeviceBindings(devs)) ui.line(ln);
|
|
14653
16457
|
}
|
|
14654
16458
|
if (report.local) {
|
|
14655
16459
|
ui.blank();
|
|
@@ -14659,6 +16463,58 @@ function printPretty(report) {
|
|
|
14659
16463
|
} else {
|
|
14660
16464
|
warn("Local DB", "unavailable");
|
|
14661
16465
|
}
|
|
16466
|
+
if (report.diagnose) {
|
|
16467
|
+
ui.blank();
|
|
16468
|
+
ui.header("Cloud dispatch reachability");
|
|
16469
|
+
ui.blank();
|
|
16470
|
+
printDiagnose(report.diagnose, report.daemon.pid ?? null);
|
|
16471
|
+
}
|
|
16472
|
+
}
|
|
16473
|
+
function printDiagnose(d, pid) {
|
|
16474
|
+
const ui = getUI();
|
|
16475
|
+
const heartbeatLine = d.lastHeartbeatAt ? `last heartbeat ${formatRelative(d.lastHeartbeatAt)}` : "no heartbeat seen";
|
|
16476
|
+
if (d.wsAlive) {
|
|
16477
|
+
ok("Daemon", `running${pid ? ` (pid ${pid})` : ""}`);
|
|
16478
|
+
ok("Cloud WS", `connected (${heartbeatLine})`);
|
|
16479
|
+
} else {
|
|
16480
|
+
warn("Daemon", `running${pid ? ` (pid ${pid})` : ""} \u2014 not visible to cloud WS`);
|
|
16481
|
+
fail("Cloud WS", `disconnected (${heartbeatLine})`);
|
|
16482
|
+
}
|
|
16483
|
+
if (d.gatewayUrl) {
|
|
16484
|
+
if (d.gatewayIsPrivate) {
|
|
16485
|
+
warn("HTTP gateway", `${d.gatewayUrl} (private IP \u2014 only reachable on same network)`);
|
|
16486
|
+
} else if (d.httpReachable) {
|
|
16487
|
+
ok(
|
|
16488
|
+
"HTTP gateway",
|
|
16489
|
+
`${d.gatewayUrl} (reachable${d.httpLatencyMs != null ? `, ${d.httpLatencyMs}ms` : ""})`
|
|
16490
|
+
);
|
|
16491
|
+
} else {
|
|
16492
|
+
fail("HTTP gateway", `${d.gatewayUrl} (unreachable${d.httpError ? ` \u2014 ${d.httpError}` : ""})`);
|
|
16493
|
+
}
|
|
16494
|
+
} else {
|
|
16495
|
+
ui.line(" HTTP gateway: not declared (WS-only daemon)");
|
|
16496
|
+
}
|
|
16497
|
+
const activeAgents = /* @__PURE__ */ (() => {
|
|
16498
|
+
return 0;
|
|
16499
|
+
})();
|
|
16500
|
+
if (activeAgents > 0) {
|
|
16501
|
+
ok("Active agents", String(activeAgents));
|
|
16502
|
+
}
|
|
16503
|
+
const recColour = d.recommendedTransport === "ws" ? ok : d.recommendedTransport === "http" ? warn : fail;
|
|
16504
|
+
recColour("Recommended transport", d.recommendedTransport);
|
|
16505
|
+
if (d.lastProbeAt) {
|
|
16506
|
+
ui.line(` Last transport probe: ${formatRelative(d.lastProbeAt)}`);
|
|
16507
|
+
}
|
|
16508
|
+
}
|
|
16509
|
+
function formatRelative(iso, now = /* @__PURE__ */ new Date()) {
|
|
16510
|
+
const ts = Date.parse(iso);
|
|
16511
|
+
if (!Number.isFinite(ts)) return "unknown";
|
|
16512
|
+
const diff = Math.max(0, now.getTime() - ts);
|
|
16513
|
+
if (diff < 3e4) return "just now";
|
|
16514
|
+
if (diff < 6e4) return `${Math.round(diff / 1e3)}s ago`;
|
|
16515
|
+
if (diff < 36e5) return `${Math.round(diff / 6e4)}m ago`;
|
|
16516
|
+
if (diff < 864e5) return `${Math.round(diff / 36e5)}h ago`;
|
|
16517
|
+
return `${Math.round(diff / 864e5)}d ago`;
|
|
14662
16518
|
}
|
|
14663
16519
|
|
|
14664
16520
|
// src/cli/commands/task.ts
|
|
@@ -15021,7 +16877,7 @@ async function readResponseError(res) {
|
|
|
15021
16877
|
|
|
15022
16878
|
// src/cli/index.ts
|
|
15023
16879
|
init_ui();
|
|
15024
|
-
var VERSION = "2.0.
|
|
16880
|
+
var VERSION = "2.0.6";
|
|
15025
16881
|
function buildProgram() {
|
|
15026
16882
|
const program = new import_commander20.Command("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
|
|
15027
16883
|
program.addCommand(buildBannerCommand());
|