@prismer/runtime 1.9.9 → 1.9.22
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 +800 -2517
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +761 -2472
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +555 -2155
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -112
- package/dist/index.d.ts +6 -112
- package/dist/index.js +504 -2098
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
6
|
+
}) : x)(function(x) {
|
|
7
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
|
+
});
|
|
4
10
|
var __esm = (fn, res) => function __init() {
|
|
5
11
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
12
|
};
|
|
@@ -12,10 +18,10 @@ var __export = (target, all) => {
|
|
|
12
18
|
// src/cli/ui.ts
|
|
13
19
|
import * as fs from "fs";
|
|
14
20
|
import * as path from "path";
|
|
15
|
-
import { fileURLToPath
|
|
21
|
+
import { fileURLToPath } from "url";
|
|
16
22
|
function thisDirname() {
|
|
17
23
|
try {
|
|
18
|
-
return path.dirname(
|
|
24
|
+
return path.dirname(fileURLToPath(import.meta.url));
|
|
19
25
|
} catch {
|
|
20
26
|
return process.cwd();
|
|
21
27
|
}
|
|
@@ -447,7 +453,7 @@ __export(util_exports, {
|
|
|
447
453
|
info: () => info,
|
|
448
454
|
normalizeCloudUrl: () => normalizeCloudUrl,
|
|
449
455
|
ok: () => ok,
|
|
450
|
-
pidAlive: () =>
|
|
456
|
+
pidAlive: () => pidAlive,
|
|
451
457
|
pidFilePath: () => pidFilePath,
|
|
452
458
|
printBanner: () => printBanner,
|
|
453
459
|
printJson: () => printJson,
|
|
@@ -459,7 +465,7 @@ __export(util_exports, {
|
|
|
459
465
|
writePidFile: () => writePidFile
|
|
460
466
|
});
|
|
461
467
|
import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
462
|
-
import { join as
|
|
468
|
+
import { join as join3 } from "path";
|
|
463
469
|
function color(kind, text) {
|
|
464
470
|
if (process.env.NO_COLOR === "1" || process.env.NO_COLOR === "true") return text;
|
|
465
471
|
return `${ANSI[kind]}${text}${ANSI.reset}`;
|
|
@@ -524,10 +530,10 @@ function runAction(fn, opts = {}) {
|
|
|
524
530
|
function printBanner(opts = {}) {
|
|
525
531
|
const ui = getUI();
|
|
526
532
|
if (opts.compact) {
|
|
527
|
-
ui.smallHeader("Runtime CLI
|
|
533
|
+
ui.smallHeader("Runtime CLI v${VERSION}");
|
|
528
534
|
return;
|
|
529
535
|
}
|
|
530
|
-
ui.banner("Runtime CLI
|
|
536
|
+
ui.banner("Runtime CLI v${VERSION}", { full: true });
|
|
531
537
|
}
|
|
532
538
|
function ok(label, detail) {
|
|
533
539
|
getUI().ok(label, detail);
|
|
@@ -553,7 +559,7 @@ function table(rows, columns) {
|
|
|
553
559
|
getUI().table(rows, { columns });
|
|
554
560
|
}
|
|
555
561
|
function pidFilePath(paths) {
|
|
556
|
-
return
|
|
562
|
+
return join3(paths.root, "daemon.pid");
|
|
557
563
|
}
|
|
558
564
|
function writePidFile(paths, pid) {
|
|
559
565
|
writeFileSync3(pidFilePath(paths), `${pid}
|
|
@@ -575,7 +581,7 @@ function clearPidFile(paths) {
|
|
|
575
581
|
}
|
|
576
582
|
}
|
|
577
583
|
}
|
|
578
|
-
function
|
|
584
|
+
function pidAlive(pid) {
|
|
579
585
|
try {
|
|
580
586
|
process.kill(pid, 0);
|
|
581
587
|
return true;
|
|
@@ -602,7 +608,7 @@ var init_util = __esm({
|
|
|
602
608
|
});
|
|
603
609
|
|
|
604
610
|
// src/cli/index.ts
|
|
605
|
-
import { Command as
|
|
611
|
+
import { Command as Command18 } from "commander";
|
|
606
612
|
|
|
607
613
|
// src/cli/commands/adapter.ts
|
|
608
614
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -614,8 +620,8 @@ import {
|
|
|
614
620
|
statSync,
|
|
615
621
|
writeFileSync as writeFileSync4
|
|
616
622
|
} from "fs";
|
|
617
|
-
import { homedir as
|
|
618
|
-
import { dirname as dirname4, join as
|
|
623
|
+
import { homedir as homedir3 } from "os";
|
|
624
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
619
625
|
import { Command } from "commander";
|
|
620
626
|
|
|
621
627
|
// src/adapters/claude-code/index.ts
|
|
@@ -1133,36 +1139,15 @@ var codexAdapter = {
|
|
|
1133
1139
|
};
|
|
1134
1140
|
|
|
1135
1141
|
// src/adapters/hermes/index.ts
|
|
1136
|
-
import {
|
|
1142
|
+
import { spawn as spawn3 } from "child_process";
|
|
1137
1143
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
1138
|
-
import { createRequire } from "module";
|
|
1139
1144
|
import { homedir } from "os";
|
|
1140
1145
|
import { dirname, join } from "path";
|
|
1141
|
-
import { fileURLToPath } from "url";
|
|
1142
1146
|
import Database from "better-sqlite3";
|
|
1143
1147
|
import * as YAML from "yaml";
|
|
1144
1148
|
import { z as z3 } from "zod";
|
|
1145
1149
|
var PRISMER_IM_SKILL_NAME = "prismer-im-collab";
|
|
1146
|
-
var PRISMER_IM_SKILL_CONTENT =
|
|
1147
|
-
name: prismer-im-collab
|
|
1148
|
-
description: Coordinate reliably in Prismer conversations, use workspace assets through bounded MCP tools, and keep task work on the board.
|
|
1149
|
-
allowed-tools: prismer.agent.send prismer.conversation.listAgents prismer.task.create prismer.task.update prismer.task.complete prismer.approval.request_human_approval prismer.asset.search prismer.asset.describe prismer.asset.read
|
|
1150
|
-
metadata:
|
|
1151
|
-
prismer:
|
|
1152
|
-
version: "1.0.0"
|
|
1153
|
-
mcp_tools:
|
|
1154
|
-
- prismer.agent.send
|
|
1155
|
-
- prismer.conversation.listAgents
|
|
1156
|
-
- prismer.task.create
|
|
1157
|
-
- prismer.task.update
|
|
1158
|
-
- prismer.task.complete
|
|
1159
|
-
- prismer.approval.request_human_approval
|
|
1160
|
-
- prismer.asset.search
|
|
1161
|
-
- prismer.asset.describe
|
|
1162
|
-
- prismer.asset.read
|
|
1163
|
-
---
|
|
1164
|
-
|
|
1165
|
-
# Prismer IM Collaboration
|
|
1150
|
+
var PRISMER_IM_SKILL_CONTENT = `# Prismer IM Collaboration
|
|
1166
1151
|
|
|
1167
1152
|
You are an agent in a Prismer multi-agent workspace. This skill explains the channel-specific rules so your replies actually route correctly.
|
|
1168
1153
|
|
|
@@ -1192,19 +1177,6 @@ You will receive a \`[Channel context]\` block at the top of each prompt indicat
|
|
|
1192
1177
|
| Address another agent so the platform dispatches your message to them | Call \`prismer.agent.send\` (from the \`prismer-tasks\` MCP server). |
|
|
1193
1178
|
| Look up which agents you can talk to | Call \`prismer.conversation.listAgents\` before guessing usernames. |
|
|
1194
1179
|
| Create / update tasks on the workspace board | \`prismer.task.create\` / \`prismer.task.update\` from \`prismer-tasks\` MCP. |
|
|
1195
|
-
| Ask for structured human sign-off | \`prismer.approval.request_human_approval\`. |
|
|
1196
|
-
| Find uploaded workspace files | \`prismer.asset.search\`. |
|
|
1197
|
-
| Inspect file metadata before reading bytes | \`prismer.asset.describe\`. |
|
|
1198
|
-
| Read a bounded byte range from a file | \`prismer.asset.read\`. |
|
|
1199
|
-
|
|
1200
|
-
## Workspace asset rules
|
|
1201
|
-
|
|
1202
|
-
- Web-uploaded workspace files are available as asset metadata first; bytes are fetched lazily by the local daemon.
|
|
1203
|
-
- When a task needs file content, call \`prismer.asset.describe\` first, then read only the needed range with \`prismer.asset.read\`.
|
|
1204
|
-
- For many files, call \`prismer.asset.search\` to find candidates. Do not ask the user to paste file contents already present in workspace assets.
|
|
1205
|
-
- Do not claim you read a file unless an asset tool call succeeded.
|
|
1206
|
-
- For large tables/documents, read headers, samples, or targeted byte ranges. Never try to load the whole file into a reply.
|
|
1207
|
-
- Cite the asset id/URI and locator returned by the tool when using file evidence.
|
|
1208
1180
|
|
|
1209
1181
|
### When to use \`prismer.agent.send\` vs inline \`@\`
|
|
1210
1182
|
|
|
@@ -1218,7 +1190,6 @@ You will receive a \`[Channel context]\` block at the top of each prompt indicat
|
|
|
1218
1190
|
2. **Don't fabricate usernames.** If \`prismer.conversation.listAgents\` doesn't return a name, that agent doesn't exist; use plain text and address them by displayName (no @).
|
|
1219
1191
|
3. **Don't pretend to call a tool.** If you write "I sent a message via the tool" without actually invoking the tool, the message doesn't exist; the human will see the inconsistency.
|
|
1220
1192
|
4. **Check tool return values.** \`prismer.agent.send\` may return \`{ ok: false, error: 'agent_not_found' }\`; surface the error rather than pretending it worked.
|
|
1221
|
-
5. **Use structured approvals.** For destructive, irreversible, external, spend, access, or publish operations, call \`prismer.approval.request_human_approval\`; do not inline-mention a human asking for approval.
|
|
1222
1193
|
|
|
1223
1194
|
## Example: group discussion handoff
|
|
1224
1195
|
|
|
@@ -1306,23 +1277,7 @@ var HermesProfileConfigSchema = z3.object({
|
|
|
1306
1277
|
hermesSourceDir: z3.string().optional(),
|
|
1307
1278
|
nativeMirrorTimeoutMs: z3.number().int().positive().default(2e3),
|
|
1308
1279
|
/** Task authority level: executor (default) or orchestrator. */
|
|
1309
|
-
taskAuthority: z3.enum(["executor", "orchestrator"]).optional().default("executor")
|
|
1310
|
-
/** Human approval escalation policy. Enforcement is currently prompt/cloud-side. */
|
|
1311
|
-
approvalPolicy: z3.enum(["strict", "auto-low-risk", "autonomous"]).optional().default("auto-low-risk"),
|
|
1312
|
-
/** Agent-level MCP tool allowlist. Empty/null means all tools. Supports exact names and trailing * wildcards. */
|
|
1313
|
-
mcpAllowlist: z3.array(z3.string()).nullable().optional(),
|
|
1314
|
-
/** Agent-level operating principles injected through /v1/runs instructions. */
|
|
1315
|
-
operatingPrinciples: z3.union([z3.string(), z3.record(z3.string())]).optional(),
|
|
1316
|
-
/**
|
|
1317
|
-
* Role-template snapshot carried in AgentProfile.config by cloud ACP.
|
|
1318
|
-
* Expected fields used here:
|
|
1319
|
-
* - roleTemplate.mcpServers[].toolsAllowlist
|
|
1320
|
-
* - roleTemplate.operatingPrinciples
|
|
1321
|
-
*/
|
|
1322
|
-
roleTemplate: z3.object({
|
|
1323
|
-
mcpServers: z3.array(z3.object({ toolsAllowlist: z3.array(z3.string()).optional() }).passthrough()).optional(),
|
|
1324
|
-
operatingPrinciples: z3.union([z3.string(), z3.record(z3.string())]).optional()
|
|
1325
|
-
}).passthrough().optional()
|
|
1280
|
+
taskAuthority: z3.enum(["executor", "orchestrator"]).optional().default("executor")
|
|
1326
1281
|
});
|
|
1327
1282
|
var hermesAdapter = {
|
|
1328
1283
|
name: "hermes",
|
|
@@ -1337,22 +1292,14 @@ var hermesAdapter = {
|
|
|
1337
1292
|
errors: r.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`)
|
|
1338
1293
|
};
|
|
1339
1294
|
},
|
|
1340
|
-
async prepareProfile(profile) {
|
|
1341
|
-
const config = HermesProfileConfigSchema.parse(profile.config);
|
|
1342
|
-
if (config.configurePrismerProvider) {
|
|
1343
|
-
const profileName = getHermesProfileName(profile);
|
|
1344
|
-
const agentUsername = profile.agentUsername || profileName;
|
|
1345
|
-
configurePrismerProvider(profileName, config, agentUsername, profile.workspaceId);
|
|
1346
|
-
}
|
|
1347
|
-
},
|
|
1348
1295
|
async ensureService(profile) {
|
|
1349
1296
|
const config = HermesProfileConfigSchema.parse(profile.config);
|
|
1350
|
-
const profileName =
|
|
1351
|
-
const portOverride = config.port !== 8642 ? config.port : portForProfile(
|
|
1297
|
+
const profileName = config.hermesProfileName && config.hermesProfileName !== "default" ? config.hermesProfileName : profile.agentUsername || profile.id.slice(0, 8);
|
|
1298
|
+
const portOverride = config.port !== 8642 ? config.port : portForProfile(profileName, 8642);
|
|
1352
1299
|
const baseUrl = `http://127.0.0.1:${portOverride}`;
|
|
1353
1300
|
const agentUsername = profile.agentUsername || profileName;
|
|
1354
1301
|
if (config.configurePrismerProvider) {
|
|
1355
|
-
configurePrismerProvider(profileName, config, agentUsername
|
|
1302
|
+
configurePrismerProvider(profileName, config, agentUsername);
|
|
1356
1303
|
}
|
|
1357
1304
|
if (!await checkHealth(baseUrl, config.apiKey)) {
|
|
1358
1305
|
if (!config.autoStart) {
|
|
@@ -1362,7 +1309,6 @@ var hermesAdapter = {
|
|
|
1362
1309
|
(set autoStart: true on the AgentProfile to spawn it automatically)`
|
|
1363
1310
|
);
|
|
1364
1311
|
}
|
|
1365
|
-
await stopStaleHermesGateways(profileName, portOverride, config.startupTimeoutMs);
|
|
1366
1312
|
spawn3("hermes", ["-p", profileName, "gateway", "run"], {
|
|
1367
1313
|
detached: false,
|
|
1368
1314
|
stdio: "ignore",
|
|
@@ -1422,8 +1368,6 @@ var HermesService = class {
|
|
|
1422
1368
|
const startedAt = Date.now();
|
|
1423
1369
|
let runId;
|
|
1424
1370
|
const sysPrompt = typeof task.metadata?.systemPrompt === "string" ? task.metadata.systemPrompt : void 0;
|
|
1425
|
-
const operatingPrinciples = typeof task.metadata?.operatingPrinciples === "string" ? task.metadata.operatingPrinciples : void 0;
|
|
1426
|
-
const instructions = [sysPrompt, operatingPrinciples].filter(Boolean).join("\n\n");
|
|
1427
1371
|
if (sysPrompt) {
|
|
1428
1372
|
try {
|
|
1429
1373
|
const profileDir = getHermesProfileDir(this.profileName);
|
|
@@ -1460,7 +1404,7 @@ var HermesService = class {
|
|
|
1460
1404
|
// sent `messages: [...]` which Hermes 0.12 rejects with 400.
|
|
1461
1405
|
body: JSON.stringify({
|
|
1462
1406
|
input: task.prompt,
|
|
1463
|
-
instructions:
|
|
1407
|
+
instructions: sysPrompt,
|
|
1464
1408
|
session_id: typeof task.metadata?.conversationId === "string" ? task.metadata.conversationId : void 0,
|
|
1465
1409
|
stream: true
|
|
1466
1410
|
}),
|
|
@@ -1745,7 +1689,7 @@ function formatHermesGoalText(goals) {
|
|
|
1745
1689
|
return `${index + 1}. ${priority}${goal.title}${description}`;
|
|
1746
1690
|
}).join("\n");
|
|
1747
1691
|
}
|
|
1748
|
-
function configurePrismerProvider(profileName, config, agentUsername
|
|
1692
|
+
function configurePrismerProvider(profileName, config, agentUsername) {
|
|
1749
1693
|
const baseUrl = resolvePrismerProviderBaseUrl(config);
|
|
1750
1694
|
const apiKey = resolvePrismerApiKey(config);
|
|
1751
1695
|
if (!baseUrl || !apiKey) {
|
|
@@ -1788,16 +1732,9 @@ function configurePrismerProvider(profileName, config, agentUsername, workspaceI
|
|
|
1788
1732
|
PRISMER_API_KEY: apiKey,
|
|
1789
1733
|
PRISMER_BASE_URL: baseUrl.replace(/\/api\/v1\/?$/, "")
|
|
1790
1734
|
};
|
|
1791
|
-
if (workspaceId) {
|
|
1792
|
-
mcpEnv.PRISMER_WORKSPACE_ID = workspaceId;
|
|
1793
|
-
}
|
|
1794
1735
|
if (agentUsername) {
|
|
1795
1736
|
mcpEnv.PRISMER_AGENT_USERNAME = agentUsername;
|
|
1796
1737
|
}
|
|
1797
|
-
const allowlist = resolveMcpAllowlist(config);
|
|
1798
|
-
if (allowlist) {
|
|
1799
|
-
mcpEnv.PRISMER_MCP_ALLOWLIST = allowlist.join(",");
|
|
1800
|
-
}
|
|
1801
1738
|
mcpServers["prismer-tasks"] = {
|
|
1802
1739
|
command: "node",
|
|
1803
1740
|
args: [serverPath],
|
|
@@ -1818,17 +1755,20 @@ function resolvePrismerMcpServerPath(config) {
|
|
|
1818
1755
|
if (config.prismerMcpServerPath) return config.prismerMcpServerPath;
|
|
1819
1756
|
if (process.env.PRISMER_MCP_SERVER) return process.env.PRISMER_MCP_SERVER;
|
|
1820
1757
|
try {
|
|
1758
|
+
const { createRequire } = __require("module");
|
|
1821
1759
|
const req = createRequire(import.meta.url);
|
|
1822
1760
|
return req.resolve("@prismer/mcp-server");
|
|
1823
1761
|
} catch {
|
|
1824
1762
|
}
|
|
1825
1763
|
try {
|
|
1826
|
-
const
|
|
1827
|
-
const
|
|
1764
|
+
const { fileURLToPath: fileURLToPath2 } = __require("url");
|
|
1765
|
+
const { join: join14, dirname: dirname8 } = __require("path");
|
|
1766
|
+
const here = dirname8(fileURLToPath2(import.meta.url));
|
|
1767
|
+
const candidate = join14(here, "../../mcp/dist/index.js");
|
|
1828
1768
|
if (existsSync(candidate)) return candidate;
|
|
1829
|
-
const candidate2 =
|
|
1769
|
+
const candidate2 = join14(here, "../../../mcp/dist/index.js");
|
|
1830
1770
|
if (existsSync(candidate2)) return candidate2;
|
|
1831
|
-
const candidate3 =
|
|
1771
|
+
const candidate3 = join14(here, "../../../../mcp/dist/index.js");
|
|
1832
1772
|
if (existsSync(candidate3)) return candidate3;
|
|
1833
1773
|
} catch {
|
|
1834
1774
|
}
|
|
@@ -1997,29 +1937,25 @@ function runPythonJson(args, env, timeoutMs) {
|
|
|
1997
1937
|
});
|
|
1998
1938
|
});
|
|
1999
1939
|
}
|
|
2000
|
-
function readHermesConfig(
|
|
2001
|
-
if (!existsSync(
|
|
1940
|
+
function readHermesConfig(path7) {
|
|
1941
|
+
if (!existsSync(path7)) return {};
|
|
2002
1942
|
try {
|
|
2003
|
-
return YAML.parse(readFileSync(
|
|
1943
|
+
return YAML.parse(readFileSync(path7, "utf8")) ?? {};
|
|
2004
1944
|
} catch (err) {
|
|
2005
|
-
throw new Error(`Failed to parse Hermes config at ${
|
|
1945
|
+
throw new Error(`Failed to parse Hermes config at ${path7}: ${err.message}`);
|
|
2006
1946
|
}
|
|
2007
1947
|
}
|
|
2008
|
-
function writeEnvValue(
|
|
2009
|
-
mkdirSync(dirname(
|
|
2010
|
-
const lines = existsSync(
|
|
1948
|
+
function writeEnvValue(path7, key, value) {
|
|
1949
|
+
mkdirSync(dirname(path7), { recursive: true });
|
|
1950
|
+
const lines = existsSync(path7) ? readFileSync(path7, "utf8").split(/\r?\n/) : [];
|
|
2011
1951
|
const next = lines.filter((line) => !line.startsWith(`${key}=`) && line.trim() !== "");
|
|
2012
1952
|
next.push(`${key}=${quoteEnv(value)}`);
|
|
2013
|
-
writeFileSync(
|
|
1953
|
+
writeFileSync(path7, `${next.join("\n")}
|
|
2014
1954
|
`, "utf8");
|
|
2015
1955
|
}
|
|
2016
1956
|
function quoteEnv(value) {
|
|
2017
1957
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
2018
1958
|
}
|
|
2019
|
-
function getHermesProfileName(profile) {
|
|
2020
|
-
const configured = profile.config && typeof profile.config.hermesProfileName === "string" ? profile.config.hermesProfileName : void 0;
|
|
2021
|
-
return configured && configured !== "default" ? configured : profile.agentUsername || profile.id.slice(0, 8);
|
|
2022
|
-
}
|
|
2023
1959
|
function getHermesProfileDir(profileName) {
|
|
2024
1960
|
const root = process.env.HERMES_HOME || join(homedir(), ".hermes");
|
|
2025
1961
|
if (!profileName || profileName === "default") return root;
|
|
@@ -2041,18 +1977,6 @@ function resolvePrismerProviderBaseUrl(config) {
|
|
|
2041
1977
|
function resolvePrismerApiKey(config) {
|
|
2042
1978
|
return process.env[config.prismerApiKeyEnv] || process.env.PRISMER_API_KEY || "";
|
|
2043
1979
|
}
|
|
2044
|
-
function resolveMcpAllowlist(config) {
|
|
2045
|
-
if (Array.isArray(config.mcpAllowlist)) return normalizeAllowlist(config.mcpAllowlist);
|
|
2046
|
-
const prismerServer = config.roleTemplate?.mcpServers?.find((server) => {
|
|
2047
|
-
const record = server;
|
|
2048
|
-
return record.package === "@prismer/mcp-server" || record.name === "prismer-tasks";
|
|
2049
|
-
});
|
|
2050
|
-
if (!prismerServer) return null;
|
|
2051
|
-
return Array.isArray(prismerServer.toolsAllowlist) ? normalizeAllowlist(prismerServer.toolsAllowlist) : null;
|
|
2052
|
-
}
|
|
2053
|
-
function normalizeAllowlist(values) {
|
|
2054
|
-
return values.map((value) => value.trim()).filter(Boolean);
|
|
2055
|
-
}
|
|
2056
1980
|
function normalizeProviderName(value) {
|
|
2057
1981
|
return value.trim().toLowerCase().replace(/\s+/g, "-");
|
|
2058
1982
|
}
|
|
@@ -2158,71 +2082,9 @@ async function waitForHealthy(baseUrl, apiKey, timeoutMs) {
|
|
|
2158
2082
|
}
|
|
2159
2083
|
throw new Error(`Hermes did not become healthy at ${baseUrl} within ${timeoutMs}ms`);
|
|
2160
2084
|
}
|
|
2161
|
-
async function stopStaleHermesGateways(profileName, port, timeoutMs) {
|
|
2162
|
-
const pids = findHermesGatewayPids(profileName, port);
|
|
2163
|
-
for (const pid of pids) {
|
|
2164
|
-
try {
|
|
2165
|
-
process.kill(pid, "SIGTERM");
|
|
2166
|
-
} catch {
|
|
2167
|
-
continue;
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
const deadline = Date.now() + Math.min(timeoutMs, 5e3);
|
|
2171
|
-
while (Date.now() < deadline) {
|
|
2172
|
-
const alive = pids.filter(pidAlive);
|
|
2173
|
-
if (alive.length === 0) return;
|
|
2174
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
2175
|
-
}
|
|
2176
|
-
for (const pid of pids) {
|
|
2177
|
-
if (!pidAlive(pid)) continue;
|
|
2178
|
-
try {
|
|
2179
|
-
process.kill(pid, "SIGKILL");
|
|
2180
|
-
} catch {
|
|
2181
|
-
}
|
|
2182
|
-
}
|
|
2183
|
-
}
|
|
2184
|
-
function findHermesGatewayPids(profileName, port) {
|
|
2185
|
-
try {
|
|
2186
|
-
const out = execFileSync("ps", ["-axo", "pid=,command="], { encoding: "utf8" });
|
|
2187
|
-
const current = process.pid;
|
|
2188
|
-
const escapedProfile = escapeRegExp(profileName);
|
|
2189
|
-
const profilePattern = new RegExp(`(?:^|\\s)-p\\s+${escapedProfile}(?:\\s|$)`);
|
|
2190
|
-
const portPattern = new RegExp(`(?:^|\\s)API_SERVER_PORT=${port}(?:\\s|$)`);
|
|
2191
|
-
return out.split(/\r?\n/).map((line) => {
|
|
2192
|
-
const trimmed = line.trim();
|
|
2193
|
-
const match = /^(\d+)\s+(.+)$/.exec(trimmed);
|
|
2194
|
-
if (!match) return null;
|
|
2195
|
-
const pid = Number.parseInt(match[1], 10);
|
|
2196
|
-
const command = match[2];
|
|
2197
|
-
if (!Number.isFinite(pid) || pid === current) return null;
|
|
2198
|
-
if (!isHermesGatewayCommand(command)) return null;
|
|
2199
|
-
if (!profilePattern.test(command) && !portPattern.test(command)) return null;
|
|
2200
|
-
return pid;
|
|
2201
|
-
}).filter((pid) => pid !== null);
|
|
2202
|
-
} catch {
|
|
2203
|
-
return [];
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
function isHermesGatewayCommand(command) {
|
|
2207
|
-
if (!/\bgateway\b/.test(command) || !/\brun\b/.test(command)) return false;
|
|
2208
|
-
return /(^|\s)(?:\S*\/)?hermes(?:\s|$)/.test(command);
|
|
2209
|
-
}
|
|
2210
|
-
function pidAlive(pid) {
|
|
2211
|
-
try {
|
|
2212
|
-
process.kill(pid, 0);
|
|
2213
|
-
return true;
|
|
2214
|
-
} catch {
|
|
2215
|
-
return false;
|
|
2216
|
-
}
|
|
2217
|
-
}
|
|
2218
|
-
function escapeRegExp(value) {
|
|
2219
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2220
|
-
}
|
|
2221
2085
|
|
|
2222
2086
|
// src/adapters/openclaw/index.ts
|
|
2223
2087
|
import { z as z4 } from "zod";
|
|
2224
|
-
import { homedir as homedir2 } from "os";
|
|
2225
|
-
import { join as join2 } from "path";
|
|
2226
2088
|
var OpenClawProfileConfigSchema = z4.object({
|
|
2227
2089
|
/** OpenClaw chat-completions HTTP port. */
|
|
2228
2090
|
port: z4.number().int().min(1).max(65535).default(18789),
|
|
@@ -2236,13 +2098,7 @@ var OpenClawProfileConfigSchema = z4.object({
|
|
|
2236
2098
|
* by `agents.defaults.model.primary` in openclaw.json (e.g.
|
|
2237
2099
|
* `prismer/us-kimi-k2.5`, set by the bootstrap).
|
|
2238
2100
|
*/
|
|
2239
|
-
model: z4.string().min(1).default("openclaw")
|
|
2240
|
-
mcpAllowlist: z4.array(z4.string()).nullable().optional(),
|
|
2241
|
-
approvalPolicy: z4.enum(["strict", "auto-low-risk", "autonomous"]).optional().default("auto-low-risk"),
|
|
2242
|
-
roleTemplate: z4.object({
|
|
2243
|
-
mcpServers: z4.array(z4.object({ toolsAllowlist: z4.array(z4.string()).optional() }).passthrough()).optional()
|
|
2244
|
-
}).passthrough().optional(),
|
|
2245
|
-
skillsDir: z4.string().optional()
|
|
2101
|
+
model: z4.string().min(1).default("openclaw")
|
|
2246
2102
|
});
|
|
2247
2103
|
var openclawAdapter = {
|
|
2248
2104
|
name: "openclaw",
|
|
@@ -2266,24 +2122,22 @@ var openclawAdapter = {
|
|
|
2266
2122
|
openclaw gateway --port ${config.port}`
|
|
2267
2123
|
);
|
|
2268
2124
|
}
|
|
2269
|
-
return new OpenClawService(baseUrl, config.apiKey, config.model
|
|
2125
|
+
return new OpenClawService(baseUrl, config.apiKey, config.model);
|
|
2270
2126
|
},
|
|
2271
2127
|
async health() {
|
|
2272
2128
|
return { available: true };
|
|
2273
2129
|
}
|
|
2274
2130
|
};
|
|
2275
2131
|
var OpenClawService = class {
|
|
2276
|
-
constructor(baseUrl, apiKey, model
|
|
2132
|
+
constructor(baseUrl, apiKey, model) {
|
|
2277
2133
|
this.baseUrl = baseUrl;
|
|
2278
2134
|
this.apiKey = apiKey;
|
|
2279
2135
|
this.model = model;
|
|
2280
|
-
this.skillsDir = skillsDir;
|
|
2281
2136
|
this.id = baseUrl;
|
|
2282
2137
|
}
|
|
2283
2138
|
baseUrl;
|
|
2284
2139
|
apiKey;
|
|
2285
2140
|
model;
|
|
2286
|
-
skillsDir;
|
|
2287
2141
|
id;
|
|
2288
2142
|
async healthy() {
|
|
2289
2143
|
return checkHealth2(this.baseUrl, this.apiKey);
|
|
@@ -2344,15 +2198,6 @@ var OpenClawService = class {
|
|
|
2344
2198
|
}
|
|
2345
2199
|
}
|
|
2346
2200
|
};
|
|
2347
|
-
function getOpenClawSkillsDir(profile) {
|
|
2348
|
-
const config = OpenClawProfileConfigSchema.parse(profile.config);
|
|
2349
|
-
return resolveOpenClawSkillsDir(profile, config);
|
|
2350
|
-
}
|
|
2351
|
-
function resolveOpenClawSkillsDir(profile, config) {
|
|
2352
|
-
if (config.skillsDir) return config.skillsDir;
|
|
2353
|
-
const name = profile.agentUsername || profile.id.slice(0, 8);
|
|
2354
|
-
return join2(process.env.OPENCLAW_HOME || join2(homedir2(), ".openclaw"), "profiles", name, "skills");
|
|
2355
|
-
}
|
|
2356
2201
|
async function checkHealth2(baseUrl, apiKey) {
|
|
2357
2202
|
try {
|
|
2358
2203
|
const res = await fetch(`${baseUrl}/health`, {
|
|
@@ -2368,8 +2213,8 @@ async function checkHealth2(baseUrl, apiKey) {
|
|
|
2368
2213
|
// src/config.ts
|
|
2369
2214
|
import * as TOML from "@iarna/toml";
|
|
2370
2215
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2371
|
-
import { homedir as
|
|
2372
|
-
import { dirname as dirname2, join as
|
|
2216
|
+
import { homedir as homedir2 } from "os";
|
|
2217
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
2373
2218
|
import { z as z5 } from "zod";
|
|
2374
2219
|
var ConfigSchema = z5.object({
|
|
2375
2220
|
/** API key from `prismer setup`, or env override. */
|
|
@@ -2399,14 +2244,14 @@ var ConfigSchema = z5.object({
|
|
|
2399
2244
|
}).optional()
|
|
2400
2245
|
});
|
|
2401
2246
|
function resolvePaths(home) {
|
|
2402
|
-
const root = home ?? process.env.PRISMER_HOME ??
|
|
2247
|
+
const root = home ?? process.env.PRISMER_HOME ?? join2(homedir2(), ".prismer");
|
|
2403
2248
|
return {
|
|
2404
2249
|
root,
|
|
2405
|
-
configFile:
|
|
2406
|
-
localDb:
|
|
2407
|
-
cacheDir:
|
|
2408
|
-
logsDir:
|
|
2409
|
-
runsDir:
|
|
2250
|
+
configFile: join2(root, "config.toml"),
|
|
2251
|
+
localDb: join2(root, "local.db"),
|
|
2252
|
+
cacheDir: join2(root, "cache"),
|
|
2253
|
+
logsDir: join2(root, "logs"),
|
|
2254
|
+
runsDir: join2(root, "runs")
|
|
2410
2255
|
};
|
|
2411
2256
|
}
|
|
2412
2257
|
function configExists(paths = resolvePaths()) {
|
|
@@ -2461,7 +2306,7 @@ var INSTALL_SPECS = {
|
|
|
2461
2306
|
binary: "claude",
|
|
2462
2307
|
hint: "Set ANTHROPIC_API_KEY in your shell profile, or rely on Claude Code OAuth login. Run `claude login` to sign in.",
|
|
2463
2308
|
authHints: ["ANTHROPIC_API_KEY or Claude Code OAuth login (`claude login`)"],
|
|
2464
|
-
hookTarget: { path:
|
|
2309
|
+
hookTarget: { path: join4(homedir3(), ".claude", "hooks.json"), kind: "json-file" }
|
|
2465
2310
|
},
|
|
2466
2311
|
openclaw: {
|
|
2467
2312
|
name: "openclaw",
|
|
@@ -2470,7 +2315,7 @@ var INSTALL_SPECS = {
|
|
|
2470
2315
|
binary: "openclaw",
|
|
2471
2316
|
hint: "OpenClaw runs as a gateway. Configure ~/.openclaw/openclaw.json and start it with `openclaw gateway` before tasks dispatch.",
|
|
2472
2317
|
authHints: ["~/.openclaw/openclaw.json gateway.auth.bearerTokens", "Prismer daemon api_key"],
|
|
2473
|
-
hookTarget: { path:
|
|
2318
|
+
hookTarget: { path: join4(homedir3(), ".openclaw", "hooks"), kind: "directory" }
|
|
2474
2319
|
},
|
|
2475
2320
|
codex: {
|
|
2476
2321
|
name: "codex",
|
|
@@ -2479,7 +2324,7 @@ var INSTALL_SPECS = {
|
|
|
2479
2324
|
binary: "codex",
|
|
2480
2325
|
hint: "Set OPENAI_API_KEY in your shell profile. Verify with `codex --help`.",
|
|
2481
2326
|
authHints: ["OPENAI_API_KEY or Codex CLI account login"],
|
|
2482
|
-
hookTarget: { path:
|
|
2327
|
+
hookTarget: { path: join4(homedir3(), ".codex", "hooks.json"), kind: "json-file" }
|
|
2483
2328
|
},
|
|
2484
2329
|
hermes: {
|
|
2485
2330
|
name: "hermes",
|
|
@@ -2488,7 +2333,7 @@ var INSTALL_SPECS = {
|
|
|
2488
2333
|
binary: "hermes",
|
|
2489
2334
|
hint: "Hermes runs as a long-lived HTTP gateway in your own Python venv. Start with `hermes -p <profile> gateway` after configuring ~/.hermes/.env.",
|
|
2490
2335
|
authHints: ["~/.hermes/.env API_SERVER_KEY", "Hermes profile provider credentials"],
|
|
2491
|
-
hookTarget: { path:
|
|
2336
|
+
hookTarget: { path: join4(homedir3(), ".hermes", "hooks.json"), kind: "json-file" }
|
|
2492
2337
|
}
|
|
2493
2338
|
};
|
|
2494
2339
|
function buildAdapterCommand() {
|
|
@@ -2576,8 +2421,8 @@ function buildAdapterCommand() {
|
|
|
2576
2421
|
});
|
|
2577
2422
|
return cmd;
|
|
2578
2423
|
}
|
|
2579
|
-
function runInstall(spec,
|
|
2580
|
-
const { argv0, args } = installCommand(spec,
|
|
2424
|
+
function runInstall(spec, version2, dryRun) {
|
|
2425
|
+
const { argv0, args } = installCommand(spec, version2);
|
|
2581
2426
|
getUI().line(`> ${argv0} ${args.join(" ")}`);
|
|
2582
2427
|
if (dryRun) {
|
|
2583
2428
|
getUI().line("(dry-run \u2014 not executing)");
|
|
@@ -2704,7 +2549,7 @@ function runHooks(spec, opts) {
|
|
|
2704
2549
|
let backup;
|
|
2705
2550
|
if (planned.kind === "directory") {
|
|
2706
2551
|
mkdirSync3(planned.path, { recursive: true });
|
|
2707
|
-
const markerPath =
|
|
2552
|
+
const markerPath = join4(planned.path, "prismer.json");
|
|
2708
2553
|
backup = backupIfExists(markerPath);
|
|
2709
2554
|
writeFileSync4(markerPath, JSON.stringify(prismerHookMarker(spec), null, 2) + "\n", "utf8");
|
|
2710
2555
|
} else {
|
|
@@ -2721,16 +2566,16 @@ function runHooks(spec, opts) {
|
|
|
2721
2566
|
hook: inspectHook(spec)
|
|
2722
2567
|
};
|
|
2723
2568
|
}
|
|
2724
|
-
function installCommand(spec,
|
|
2725
|
-
const ref =
|
|
2569
|
+
function installCommand(spec, version2) {
|
|
2570
|
+
const ref = version2 === "latest" ? spec.package : `${spec.package}@${version2}`;
|
|
2726
2571
|
switch (spec.manager) {
|
|
2727
2572
|
case "npm":
|
|
2728
2573
|
return { argv0: "npm", args: ["install", "-g", ref] };
|
|
2729
2574
|
case "pipx":
|
|
2730
|
-
const pyRef =
|
|
2575
|
+
const pyRef = version2 === "latest" ? spec.package : `${spec.package}==${version2}`;
|
|
2731
2576
|
return { argv0: "pipx", args: ["install", pyRef] };
|
|
2732
2577
|
case "pip":
|
|
2733
|
-
const pipRef =
|
|
2578
|
+
const pipRef = version2 === "latest" ? spec.package : `${spec.package}==${version2}`;
|
|
2734
2579
|
return { argv0: "pip", args: ["install", "--user", pipRef] };
|
|
2735
2580
|
}
|
|
2736
2581
|
}
|
|
@@ -2778,7 +2623,7 @@ function inspectAuthEnv(spec) {
|
|
|
2778
2623
|
hints: spec.authHints ?? []
|
|
2779
2624
|
};
|
|
2780
2625
|
case "hermes": {
|
|
2781
|
-
const envFile =
|
|
2626
|
+
const envFile = join4(homedir3(), ".hermes", ".env");
|
|
2782
2627
|
return {
|
|
2783
2628
|
ok: existsSync5(envFile) || Boolean(process.env.HERMES_API_KEY || process.env.API_SERVER_KEY),
|
|
2784
2629
|
present: [
|
|
@@ -2789,7 +2634,7 @@ function inspectAuthEnv(spec) {
|
|
|
2789
2634
|
};
|
|
2790
2635
|
}
|
|
2791
2636
|
case "openclaw": {
|
|
2792
|
-
const cfgFile =
|
|
2637
|
+
const cfgFile = join4(homedir3(), ".openclaw", "openclaw.json");
|
|
2793
2638
|
return {
|
|
2794
2639
|
ok: existsSync5(cfgFile) || Boolean(process.env.OPENCLAW_API_KEY),
|
|
2795
2640
|
present: [
|
|
@@ -2809,8 +2654,8 @@ function inspectHook(spec) {
|
|
|
2809
2654
|
return { supported: false, markerPresent: false };
|
|
2810
2655
|
}
|
|
2811
2656
|
if (spec.name === "openclaw") {
|
|
2812
|
-
const jsonPath =
|
|
2813
|
-
const markerPath =
|
|
2657
|
+
const jsonPath = join4(homedir3(), ".openclaw", "hooks.json");
|
|
2658
|
+
const markerPath = join4(target.path, "prismer.json");
|
|
2814
2659
|
const jsonMarkerPresent = existsSync5(jsonPath) && fileContainsPrismerMarker(jsonPath);
|
|
2815
2660
|
const dirMarkerPresent = existsSync5(markerPath) && fileContainsPrismerMarker(markerPath);
|
|
2816
2661
|
return {
|
|
@@ -2824,7 +2669,7 @@ function inspectHook(spec) {
|
|
|
2824
2669
|
};
|
|
2825
2670
|
}
|
|
2826
2671
|
if (target.kind === "directory") {
|
|
2827
|
-
const markerPath =
|
|
2672
|
+
const markerPath = join4(target.path, "prismer.json");
|
|
2828
2673
|
return {
|
|
2829
2674
|
supported: true,
|
|
2830
2675
|
kind: target.kind,
|
|
@@ -2868,21 +2713,21 @@ function prismerHookMarker(spec) {
|
|
|
2868
2713
|
installed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2869
2714
|
};
|
|
2870
2715
|
}
|
|
2871
|
-
function mergeHookJson(
|
|
2716
|
+
function mergeHookJson(path7, spec) {
|
|
2872
2717
|
const marker = prismerHookMarker(spec);
|
|
2873
|
-
if (!existsSync5(
|
|
2718
|
+
if (!existsSync5(path7)) {
|
|
2874
2719
|
return {
|
|
2875
2720
|
prismer: marker
|
|
2876
2721
|
};
|
|
2877
2722
|
}
|
|
2878
2723
|
let parsed;
|
|
2879
2724
|
try {
|
|
2880
|
-
parsed = JSON.parse(readFileSync5(
|
|
2725
|
+
parsed = JSON.parse(readFileSync5(path7, "utf8"));
|
|
2881
2726
|
} catch (err) {
|
|
2882
|
-
throw new Error(`Cannot parse ${
|
|
2727
|
+
throw new Error(`Cannot parse ${path7} as JSON: ${err.message}`);
|
|
2883
2728
|
}
|
|
2884
2729
|
if (!isRecord(parsed)) {
|
|
2885
|
-
throw new Error(`Cannot merge ${
|
|
2730
|
+
throw new Error(`Cannot merge ${path7}: expected a JSON object`);
|
|
2886
2731
|
}
|
|
2887
2732
|
const next = { ...parsed };
|
|
2888
2733
|
next.prismer = marker;
|
|
@@ -2900,18 +2745,18 @@ function mergeHookJson(path9, spec) {
|
|
|
2900
2745
|
}
|
|
2901
2746
|
return next;
|
|
2902
2747
|
}
|
|
2903
|
-
function backupIfExists(
|
|
2904
|
-
if (!existsSync5(
|
|
2748
|
+
function backupIfExists(path7) {
|
|
2749
|
+
if (!existsSync5(path7)) return null;
|
|
2905
2750
|
const suffix = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2906
|
-
const backup = `${
|
|
2907
|
-
const stat = statSync(
|
|
2751
|
+
const backup = `${path7}.bak.${suffix}`;
|
|
2752
|
+
const stat = statSync(path7);
|
|
2908
2753
|
if (stat.isDirectory()) return null;
|
|
2909
|
-
copyFileSync(
|
|
2754
|
+
copyFileSync(path7, backup);
|
|
2910
2755
|
return backup;
|
|
2911
2756
|
}
|
|
2912
|
-
function fileContainsPrismerMarker(
|
|
2757
|
+
function fileContainsPrismerMarker(path7) {
|
|
2913
2758
|
try {
|
|
2914
|
-
return readFileSync5(
|
|
2759
|
+
return readFileSync5(path7, "utf8").includes("prismer-daemon-runtime");
|
|
2915
2760
|
} catch {
|
|
2916
2761
|
return false;
|
|
2917
2762
|
}
|
|
@@ -2929,7 +2774,7 @@ function readAdapterConfig() {
|
|
|
2929
2774
|
return {};
|
|
2930
2775
|
}
|
|
2931
2776
|
}
|
|
2932
|
-
function recordInstallInConfig(spec, binPath,
|
|
2777
|
+
function recordInstallInConfig(spec, binPath, version2) {
|
|
2933
2778
|
const paths = resolvePaths();
|
|
2934
2779
|
if (!configExists(paths)) {
|
|
2935
2780
|
process.stderr.write(
|
|
@@ -2951,7 +2796,7 @@ function recordInstallInConfig(spec, binPath, version) {
|
|
|
2951
2796
|
package_manager: spec.manager,
|
|
2952
2797
|
package_name: spec.package,
|
|
2953
2798
|
binary: binPath,
|
|
2954
|
-
version,
|
|
2799
|
+
version: version2,
|
|
2955
2800
|
installed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2956
2801
|
};
|
|
2957
2802
|
saveConfig({ ...cfg, adapters }, paths);
|
|
@@ -2984,8 +2829,8 @@ var CloudClient = class {
|
|
|
2984
2829
|
opts;
|
|
2985
2830
|
fetchImpl;
|
|
2986
2831
|
/** Low-level request. Caller decides on retry; sync-worker handles backoff. */
|
|
2987
|
-
async request(method,
|
|
2988
|
-
const url = this.urlFor(
|
|
2832
|
+
async request(method, path7, init) {
|
|
2833
|
+
const url = this.urlFor(path7);
|
|
2989
2834
|
const headers = {
|
|
2990
2835
|
"Content-Type": "application/json",
|
|
2991
2836
|
Accept: "application/json",
|
|
@@ -3044,8 +2889,8 @@ var CloudClient = class {
|
|
|
3044
2889
|
return { ok: true, status: res.status, data: body ?? void 0 };
|
|
3045
2890
|
}
|
|
3046
2891
|
/** Convenience: GET expecting `{ ok, data }` envelope. Returns `data` or throws. */
|
|
3047
|
-
async get(
|
|
3048
|
-
const res = await this.request("GET",
|
|
2892
|
+
async get(path7, opts) {
|
|
2893
|
+
const res = await this.request("GET", path7, opts);
|
|
3049
2894
|
if (!res.ok) throw new CloudError(res.status, res.error?.code ?? "unknown", res.error?.message ?? "request failed");
|
|
3050
2895
|
if (res.data && typeof res.data === "object" && "ok" in res.data) {
|
|
3051
2896
|
const env = res.data;
|
|
@@ -3054,14 +2899,14 @@ var CloudClient = class {
|
|
|
3054
2899
|
}
|
|
3055
2900
|
return res.data;
|
|
3056
2901
|
}
|
|
3057
|
-
urlFor(
|
|
2902
|
+
urlFor(path7) {
|
|
3058
2903
|
const trimmedBase = this.opts.baseUrl.replace(/\/$/, "");
|
|
3059
|
-
const trimmedPath =
|
|
2904
|
+
const trimmedPath = path7.startsWith("/") ? path7 : `/${path7}`;
|
|
3060
2905
|
return `${trimmedBase}${trimmedPath}`;
|
|
3061
2906
|
}
|
|
3062
2907
|
/** Raw byte download (for asset cache). Returns response so caller can stream body. */
|
|
3063
|
-
async fetchRaw(
|
|
3064
|
-
const url = this.urlFor(
|
|
2908
|
+
async fetchRaw(path7, init) {
|
|
2909
|
+
const url = this.urlFor(path7);
|
|
3065
2910
|
const headers = {
|
|
3066
2911
|
Authorization: `Bearer ${this.opts.apiKey}`,
|
|
3067
2912
|
...init?.headers ?? {}
|
|
@@ -3233,11 +3078,11 @@ var MIGRATIONS = [
|
|
|
3233
3078
|
function runSql(db, sql) {
|
|
3234
3079
|
db["exec"](sql);
|
|
3235
3080
|
}
|
|
3236
|
-
function openLocalDb(
|
|
3237
|
-
if (
|
|
3238
|
-
mkdirSync4(dirname5(
|
|
3081
|
+
function openLocalDb(path7) {
|
|
3082
|
+
if (path7 !== ":memory:" && !existsSync6(dirname5(path7))) {
|
|
3083
|
+
mkdirSync4(dirname5(path7), { recursive: true });
|
|
3239
3084
|
}
|
|
3240
|
-
const db = new Database2(
|
|
3085
|
+
const db = new Database2(path7);
|
|
3241
3086
|
db.pragma("journal_mode = WAL");
|
|
3242
3087
|
db.pragma("foreign_keys = ON");
|
|
3243
3088
|
runMigrations(db);
|
|
@@ -3617,7 +3462,7 @@ async function fetchCloudProfiles(cloud, imUserId) {
|
|
|
3617
3462
|
async function fetchLocalServer() {
|
|
3618
3463
|
const paths = resolvePaths();
|
|
3619
3464
|
const pid = readPidFile(paths);
|
|
3620
|
-
if (!pid || !
|
|
3465
|
+
if (!pid || !pidAlive(pid)) return { ok: false, error: "daemon process is not running", agents: [] };
|
|
3621
3466
|
try {
|
|
3622
3467
|
const [healthRes, agentsRes] = await Promise.all([
|
|
3623
3468
|
fetch(`${LOCAL_SERVER}/healthz`, { signal: AbortSignal.timeout(1e3) }),
|
|
@@ -3641,664 +3486,83 @@ function whichBinary2(bin) {
|
|
|
3641
3486
|
|
|
3642
3487
|
// src/cli/commands/asset.ts
|
|
3643
3488
|
import { Command as Command3 } from "commander";
|
|
3644
|
-
import { existsSync as
|
|
3645
|
-
import { basename
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
}
|
|
3662
|
-
var AssetCache = class {
|
|
3663
|
-
db;
|
|
3664
|
-
cloud;
|
|
3665
|
-
cacheDir;
|
|
3666
|
-
maxBytes;
|
|
3667
|
-
constructor(opts) {
|
|
3668
|
-
this.db = opts.db;
|
|
3669
|
-
this.cloud = opts.cloud;
|
|
3670
|
-
this.cacheDir = opts.cacheDir;
|
|
3671
|
-
this.maxBytes = opts.maxBytes ?? 5 * 1024 * 1024 * 1024;
|
|
3672
|
-
if (!existsSync7(this.cacheDir)) {
|
|
3673
|
-
mkdirSync5(this.cacheDir, { recursive: true });
|
|
3674
|
-
}
|
|
3675
|
-
}
|
|
3676
|
-
/** Local file path for a content hash. Files split by first 2 chars for fs scaling. */
|
|
3677
|
-
pathFor(hash) {
|
|
3678
|
-
return join6(this.cacheDir, hash.slice(0, 2), hash);
|
|
3679
|
-
}
|
|
3680
|
-
get(hash) {
|
|
3681
|
-
const row = this.db.prepare("SELECT * FROM cached_assets WHERE content_hash = ?").get(hash);
|
|
3682
|
-
if (!row) return void 0;
|
|
3683
|
-
if (!existsSync7(row.local_path)) {
|
|
3684
|
-
this.db.prepare("DELETE FROM cached_assets WHERE content_hash = ?").run(hash);
|
|
3685
|
-
return void 0;
|
|
3489
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
3490
|
+
import { basename } from "path";
|
|
3491
|
+
init_util();
|
|
3492
|
+
init_ui();
|
|
3493
|
+
function buildAssetCommand() {
|
|
3494
|
+
const cmd = new Command3("asset").description("Inspect IM assets");
|
|
3495
|
+
cmd.command("list").description("List assets for a workspace, optionally filtered by task").option("--workspace-id <id>", "Workspace id").option("--task-id <id>", "Task id filter").option("--json", "Output machine-readable JSON").action(runAction(async (opts) => {
|
|
3496
|
+
const cloud = mkCloud();
|
|
3497
|
+
const query = new URLSearchParams();
|
|
3498
|
+
if (opts.workspaceId) query.set("workspaceId", opts.workspaceId);
|
|
3499
|
+
if (opts.taskId) query.set("taskId", opts.taskId);
|
|
3500
|
+
const path7 = `/api/im/assets${query.toString() ? `?${query.toString()}` : ""}`;
|
|
3501
|
+
const res = await cloud.request("GET", path7);
|
|
3502
|
+
if (!res.ok) exitWithError(`asset list failed (${res.status}): ${res.error?.message ?? "request failed"}`, { code: res.error?.code ?? "asset_list_failed" });
|
|
3503
|
+
const body = res.data;
|
|
3504
|
+
if (isErrorEnvelope(body)) {
|
|
3505
|
+
exitWithError(`asset list failed (${res.status}): ${errorMessage(body)}`, { code: "asset_list_failed" });
|
|
3686
3506
|
}
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
/** Bump `last_used_at` (call when an asset is consulted). */
|
|
3691
|
-
touch(hash) {
|
|
3692
|
-
this.db.prepare("UPDATE cached_assets SET last_used_at = ? WHERE content_hash = ?").run(Date.now(), hash);
|
|
3693
|
-
}
|
|
3694
|
-
pin(hash) {
|
|
3695
|
-
this.db.prepare("UPDATE cached_assets SET pin = 1 WHERE content_hash = ?").run(hash);
|
|
3696
|
-
}
|
|
3697
|
-
unpin(hash) {
|
|
3698
|
-
this.db.prepare("UPDATE cached_assets SET pin = 0 WHERE content_hash = ?").run(hash);
|
|
3699
|
-
}
|
|
3700
|
-
/**
|
|
3701
|
-
* Get or fetch by hash. On miss, downloads the raw bytes and caches them.
|
|
3702
|
-
*
|
|
3703
|
-
* Why two endpoints: `GET /api/im/assets/by-hash/:hash` returns the JSON
|
|
3704
|
-
* `AssetDTO` (id, contentHash, storageUri, …) — *metadata only*. The raw
|
|
3705
|
-
* bytes live at `GET /api/im/assets/:id`. If `opts.assetId` is supplied
|
|
3706
|
-
* the caller already has the id from an `AssetRef`, so we go straight to
|
|
3707
|
-
* the byte endpoint; otherwise we resolve `hash → id` first (one extra
|
|
3708
|
-
* round-trip on the metadata route) before downloading the body.
|
|
3709
|
-
*
|
|
3710
|
-
* Earlier this method called `by-hash` and treated the JSON body as the
|
|
3711
|
-
* asset content — the daemon stored the 500-byte metadata blob on disk
|
|
3712
|
-
* and `resolveAssetRefs` inlined that JSON into the prompt. The agent
|
|
3713
|
-
* saw `{"id":"…","storageUri":"…"}` instead of the user's file and
|
|
3714
|
-
* concluded the upload had failed.
|
|
3715
|
-
*/
|
|
3716
|
-
async getOrFetch(hash, opts) {
|
|
3717
|
-
const existing = this.get(hash);
|
|
3718
|
-
if (existing) return existing;
|
|
3719
|
-
const wsParam = opts?.workspaceIdHint ? `?wsId=${encodeURIComponent(opts.workspaceIdHint)}` : "";
|
|
3720
|
-
let assetId = opts?.assetId;
|
|
3721
|
-
if (!assetId) {
|
|
3722
|
-
const metaRes = await this.cloud.fetchRaw(
|
|
3723
|
-
`/api/im/assets/by-hash/${encodeURIComponent(hash)}${wsParam}`,
|
|
3724
|
-
{ signal: opts?.signal }
|
|
3725
|
-
);
|
|
3726
|
-
if (!metaRes.ok) {
|
|
3727
|
-
throw new Error(`Asset metadata fetch failed for ${hash}: HTTP ${metaRes.status}`);
|
|
3728
|
-
}
|
|
3729
|
-
const metaJson = await metaRes.json();
|
|
3730
|
-
if (!metaJson?.ok || typeof metaJson.data?.id !== "string") {
|
|
3731
|
-
throw new Error(`Asset metadata payload missing id for ${hash}`);
|
|
3732
|
-
}
|
|
3733
|
-
assetId = metaJson.data.id;
|
|
3507
|
+
if (opts.json) {
|
|
3508
|
+
printJson(body ?? null);
|
|
3509
|
+
return;
|
|
3734
3510
|
}
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3511
|
+
printAssetList(body);
|
|
3512
|
+
}, { code: "asset_list_failed" }));
|
|
3513
|
+
cmd.command("upload <file>").description("Upload a local file as an IM asset").requiredOption("--workspace-id <id>", "Workspace id").option("--kind <kind>", "Asset kind", "file").option("--task-id <id>", "Source task id").option("--agent-id <id>", "Source agent IM user id").option("--container-id <id>", "Container id; also added to metadata for sandbox-output assets").option("--metadata <json>", "Additional JSON metadata").option("--mime <type>", "Override file MIME type").option("--json", "Output machine-readable JSON").action(runAction(async (file, opts) => {
|
|
3514
|
+
const body = await uploadAsset(file, opts);
|
|
3515
|
+
if (opts.json) {
|
|
3516
|
+
printJson(body ?? null);
|
|
3517
|
+
return;
|
|
3740
3518
|
}
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3519
|
+
printAssetDetail("Uploaded asset", file, body);
|
|
3520
|
+
}, { code: "asset_upload_failed" }));
|
|
3521
|
+
cmd.command("download <assetId>").description("Download asset bytes").requiredOption("--out <path>", "Destination file path").action(runAction(async (assetId, opts) => {
|
|
3522
|
+
await downloadAsset(assetId, opts.out);
|
|
3523
|
+
getUI().line(`Downloaded ${assetId} -> ${opts.out}`);
|
|
3524
|
+
}, { code: "asset_download_failed" }));
|
|
3525
|
+
cmd.command("get <assetId>").description("Fetch asset metadata by id").option("--json", "Output machine-readable JSON").action(runAction(async (assetId, opts) => {
|
|
3526
|
+
const cloud = mkCloud();
|
|
3527
|
+
const res = await cloud.request("GET", `/api/im/assets/${encodeURIComponent(assetId)}/detail`);
|
|
3528
|
+
if (!res.ok) exitWithError(`asset get failed (${res.status}): ${res.error?.message ?? "request failed"}`, { code: res.error?.code ?? "asset_get_failed" });
|
|
3529
|
+
const body = res.data;
|
|
3530
|
+
if (isErrorEnvelope(body)) {
|
|
3531
|
+
exitWithError(`asset get failed (${res.status}): ${errorMessage(body)}`, { code: "asset_get_failed" });
|
|
3745
3532
|
}
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3533
|
+
if (opts.json) {
|
|
3534
|
+
printJson(body ?? null);
|
|
3535
|
+
return;
|
|
3749
3536
|
}
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
const
|
|
3754
|
-
const
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
contentHash: hash,
|
|
3763
|
-
sizeBytes: buf.length,
|
|
3764
|
-
mime,
|
|
3765
|
-
localPath,
|
|
3766
|
-
fetchedAt: now,
|
|
3767
|
-
lastUsedAt: now,
|
|
3768
|
-
pin: false
|
|
3769
|
-
};
|
|
3770
|
-
}
|
|
3771
|
-
/** Insert an already-on-disk asset into the cache (e.g., asset just produced by adapter). */
|
|
3772
|
-
registerLocal(hash, localPath, mime) {
|
|
3773
|
-
const actualHash = sha256(readFileBuffer(localPath));
|
|
3774
|
-
if (actualHash !== hash) {
|
|
3775
|
-
throw new Error(`Asset hash mismatch for ${hash}: local file is ${actualHash}`);
|
|
3537
|
+
printAssetDetail("Asset", assetId, body);
|
|
3538
|
+
}, { code: "asset_get_failed" }));
|
|
3539
|
+
cmd.command("by-hash <sha256>").description("Fetch asset metadata by content hash").option("--workspace-id <id>", "Workspace id (required by the cloud API)").option("--json", "Output machine-readable JSON").action(runAction(async (sha2562, opts) => {
|
|
3540
|
+
const cloud = mkCloud();
|
|
3541
|
+
const query = new URLSearchParams();
|
|
3542
|
+
if (opts.workspaceId) query.set("wsId", opts.workspaceId);
|
|
3543
|
+
const path7 = `/api/im/assets/by-hash/${encodeURIComponent(sha2562)}${query.toString() ? `?${query.toString()}` : ""}`;
|
|
3544
|
+
const res = await cloud.request("GET", path7);
|
|
3545
|
+
if (!res.ok) exitWithError(`asset by-hash failed (${res.status}): ${res.error?.message ?? "request failed"}`, { code: res.error?.code ?? "asset_by_hash_failed" });
|
|
3546
|
+
const body = res.data;
|
|
3547
|
+
if (isErrorEnvelope(body)) {
|
|
3548
|
+
exitWithError(`asset by-hash failed (${res.status}): ${errorMessage(body)}`, { code: "asset_by_hash_failed" });
|
|
3776
3549
|
}
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
`INSERT OR REPLACE INTO cached_assets
|
|
3781
|
-
(content_hash, size_bytes, mime, local_path, fetched_at, last_used_at, pin)
|
|
3782
|
-
VALUES (?, ?, ?, ?, ?, ?, 1)`
|
|
3783
|
-
).run(hash, size, mime ?? null, localPath, now, now);
|
|
3784
|
-
return { contentHash: hash, sizeBytes: size, mime: mime ?? null, localPath, fetchedAt: now, lastUsedAt: now, pin: true };
|
|
3785
|
-
}
|
|
3786
|
-
/** Total bytes across all rows (incl. pinned). */
|
|
3787
|
-
totalBytes() {
|
|
3788
|
-
const row = this.db.prepare("SELECT COALESCE(SUM(size_bytes),0) AS total FROM cached_assets").get();
|
|
3789
|
-
return row.total;
|
|
3790
|
-
}
|
|
3791
|
-
/** Run LRU eviction until under cap. Pinned rows are never evicted. */
|
|
3792
|
-
evictIfOver() {
|
|
3793
|
-
let total = this.totalBytes();
|
|
3794
|
-
if (total <= this.maxBytes) return { removed: 0, freed: 0 };
|
|
3795
|
-
const candidates = this.db.prepare(
|
|
3796
|
-
`SELECT content_hash, size_bytes, local_path FROM cached_assets
|
|
3797
|
-
WHERE pin = 0
|
|
3798
|
-
ORDER BY last_used_at ASC`
|
|
3799
|
-
).all();
|
|
3800
|
-
let freed = 0;
|
|
3801
|
-
let removed = 0;
|
|
3802
|
-
for (const row of candidates) {
|
|
3803
|
-
if (total <= this.maxBytes) break;
|
|
3804
|
-
try {
|
|
3805
|
-
if (existsSync7(row.local_path)) unlinkSync2(row.local_path);
|
|
3806
|
-
} catch {
|
|
3807
|
-
}
|
|
3808
|
-
this.db.prepare("DELETE FROM cached_assets WHERE content_hash = ?").run(row.content_hash);
|
|
3809
|
-
freed += row.size_bytes;
|
|
3810
|
-
total -= row.size_bytes;
|
|
3811
|
-
removed += 1;
|
|
3550
|
+
if (opts.json) {
|
|
3551
|
+
printJson(body ?? null);
|
|
3552
|
+
return;
|
|
3812
3553
|
}
|
|
3813
|
-
|
|
3814
|
-
}
|
|
3815
|
-
|
|
3816
|
-
function sha256(buf) {
|
|
3817
|
-
return createHash("sha256").update(buf).digest("hex");
|
|
3554
|
+
printAssetDetail("Asset hash", sha2562, body);
|
|
3555
|
+
}, { code: "asset_by_hash_failed" }));
|
|
3556
|
+
return cmd;
|
|
3818
3557
|
}
|
|
3819
|
-
function
|
|
3820
|
-
|
|
3821
|
-
}
|
|
3822
|
-
|
|
3823
|
-
// src/daemon/asset/metadata-index.ts
|
|
3824
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
3825
|
-
import { join as join7 } from "path";
|
|
3826
|
-
var DEFAULT_LIMIT = 8;
|
|
3827
|
-
var PULL_PAGE_SIZE = 500;
|
|
3828
|
-
var THROTTLE_MS = 3e4;
|
|
3829
|
-
function rowToMetadata(row) {
|
|
3830
|
-
return {
|
|
3831
|
-
assetId: row.asset_id,
|
|
3832
|
-
contentHash: row.content_hash,
|
|
3833
|
-
filename: row.filename,
|
|
3834
|
-
folderPath: row.folder_path,
|
|
3835
|
-
mime: row.mime,
|
|
3836
|
-
kind: row.kind,
|
|
3837
|
-
sizeBytes: row.size_bytes,
|
|
3838
|
-
description: row.description,
|
|
3839
|
-
assetIndexSeq: row.asset_index_seq
|
|
3840
|
-
};
|
|
3841
|
-
}
|
|
3842
|
-
var AssetMetadataIndex = class {
|
|
3843
|
-
db;
|
|
3844
|
-
cloud;
|
|
3845
|
-
/** Workspace ID — exposed for prismer:// URI construction. */
|
|
3846
|
-
workspaceId;
|
|
3847
|
-
cursorPath;
|
|
3848
|
-
_lastSyncMs = 0;
|
|
3849
|
-
constructor(opts) {
|
|
3850
|
-
this.db = opts.db;
|
|
3851
|
-
this.cloud = opts.cloud;
|
|
3852
|
-
this.workspaceId = opts.workspaceId;
|
|
3853
|
-
if (!existsSync8(opts.workspaceStateDir)) {
|
|
3854
|
-
mkdirSync6(opts.workspaceStateDir, { recursive: true });
|
|
3855
|
-
}
|
|
3856
|
-
this.cursorPath = join7(opts.workspaceStateDir, "asset-metadata-cursor.json");
|
|
3857
|
-
}
|
|
3858
|
-
/** Persisted cursor for this workspace, or 0 on first run / corrupted file. */
|
|
3859
|
-
readCursor() {
|
|
3860
|
-
if (!existsSync8(this.cursorPath)) return 0;
|
|
3861
|
-
try {
|
|
3862
|
-
const parsed = JSON.parse(readFileSync7(this.cursorPath, "utf8"));
|
|
3863
|
-
if (parsed.workspaceId !== this.workspaceId) return 0;
|
|
3864
|
-
return parsed.cursor;
|
|
3865
|
-
} catch {
|
|
3866
|
-
return 0;
|
|
3867
|
-
}
|
|
3868
|
-
}
|
|
3869
|
-
writeCursor(cursor) {
|
|
3870
|
-
const payload = {
|
|
3871
|
-
workspaceId: this.workspaceId,
|
|
3872
|
-
cursor,
|
|
3873
|
-
writtenAt: Date.now()
|
|
3874
|
-
};
|
|
3875
|
-
writeFileSync6(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
3876
|
-
}
|
|
3877
|
-
/**
|
|
3878
|
-
* Pull incremental asset metadata changes since the persisted cursor and
|
|
3879
|
-
* upsert into the local index. Newer rows overwrite older ones by
|
|
3880
|
-
* (workspace_id, asset_id) primary key.
|
|
3881
|
-
*
|
|
3882
|
-
* Returns the count of items applied + the new cursor. Throttled: if called
|
|
3883
|
-
* within 30s of the last successful pull, returns immediately.
|
|
3884
|
-
*/
|
|
3885
|
-
async pullDelta(opts) {
|
|
3886
|
-
const now = Date.now();
|
|
3887
|
-
if (now - this._lastSyncMs < THROTTLE_MS) {
|
|
3888
|
-
return { applied: 0, cursor: this.readCursor() };
|
|
3889
|
-
}
|
|
3890
|
-
const since = this.readCursor();
|
|
3891
|
-
const sinceParam = since > 0 ? `&since=${since}` : "";
|
|
3892
|
-
const envelope2 = await this.cloud.get(
|
|
3893
|
-
`/api/im/assets/index?workspaceId=${encodeURIComponent(this.workspaceId)}&limit=${PULL_PAGE_SIZE}${sinceParam}`,
|
|
3894
|
-
{ signal: opts?.signal }
|
|
3895
|
-
);
|
|
3896
|
-
const upsert = this.db.prepare(`
|
|
3897
|
-
INSERT INTO asset_metadata_index
|
|
3898
|
-
(workspace_id, asset_id, content_hash, filename, folder_path, mime, kind, size_bytes, description, asset_index_seq, updated_at)
|
|
3899
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3900
|
-
ON CONFLICT(workspace_id, asset_id) DO UPDATE SET
|
|
3901
|
-
content_hash = excluded.content_hash,
|
|
3902
|
-
filename = excluded.filename,
|
|
3903
|
-
folder_path = excluded.folder_path,
|
|
3904
|
-
mime = excluded.mime,
|
|
3905
|
-
kind = excluded.kind,
|
|
3906
|
-
size_bytes = excluded.size_bytes,
|
|
3907
|
-
description = excluded.description,
|
|
3908
|
-
asset_index_seq = excluded.asset_index_seq,
|
|
3909
|
-
updated_at = excluded.updated_at
|
|
3910
|
-
`);
|
|
3911
|
-
const nowTs = Date.now();
|
|
3912
|
-
let applied = 0;
|
|
3913
|
-
const tx = this.db.transaction((items) => {
|
|
3914
|
-
for (const item of items) {
|
|
3915
|
-
const assetId = item.assetId ?? item.id;
|
|
3916
|
-
if (!assetId) {
|
|
3917
|
-
throw new Error("Asset metadata index item missing assetId/id");
|
|
3918
|
-
}
|
|
3919
|
-
upsert.run(
|
|
3920
|
-
this.workspaceId,
|
|
3921
|
-
assetId,
|
|
3922
|
-
item.contentHash,
|
|
3923
|
-
item.filename ?? null,
|
|
3924
|
-
item.folderPath ?? null,
|
|
3925
|
-
item.mime,
|
|
3926
|
-
item.kind,
|
|
3927
|
-
item.sizeBytes,
|
|
3928
|
-
item.description ?? null,
|
|
3929
|
-
item.assetIndexSeq,
|
|
3930
|
-
nowTs
|
|
3931
|
-
);
|
|
3932
|
-
applied += 1;
|
|
3933
|
-
}
|
|
3934
|
-
});
|
|
3935
|
-
try {
|
|
3936
|
-
tx(envelope2.items);
|
|
3937
|
-
this.writeCursor(envelope2.cursor);
|
|
3938
|
-
} catch (err) {
|
|
3939
|
-
throw err;
|
|
3940
|
-
}
|
|
3941
|
-
this._lastSyncMs = now;
|
|
3942
|
-
return { applied, cursor: envelope2.cursor };
|
|
3943
|
-
}
|
|
3944
|
-
/**
|
|
3945
|
-
* Search local index by filename or description substring.
|
|
3946
|
-
* Escapes LIKE wildcards (% and _). Default limit 8.
|
|
3947
|
-
*/
|
|
3948
|
-
search(query, limit) {
|
|
3949
|
-
const escaped = query.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
3950
|
-
const pattern = `%${escaped}%`;
|
|
3951
|
-
const limitVal = Math.min(Math.max(limit ?? DEFAULT_LIMIT, 1), 200);
|
|
3952
|
-
const rows = this.db.prepare(
|
|
3953
|
-
`SELECT * FROM asset_metadata_index
|
|
3954
|
-
WHERE workspace_id = ?
|
|
3955
|
-
AND (filename LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\')
|
|
3956
|
-
ORDER BY asset_index_seq DESC
|
|
3957
|
-
LIMIT ?`
|
|
3958
|
-
).all(this.workspaceId, pattern, pattern, limitVal);
|
|
3959
|
-
return rows.map(rowToMetadata);
|
|
3960
|
-
}
|
|
3961
|
-
/** Exact match on filename column. Returns undefined if not indexed. */
|
|
3962
|
-
resolveByFilename(filename) {
|
|
3963
|
-
const row = this.db.prepare("SELECT * FROM asset_metadata_index WHERE workspace_id = ? AND filename = ?").get(this.workspaceId, filename);
|
|
3964
|
-
return row ? rowToMetadata(row) : void 0;
|
|
3965
|
-
}
|
|
3966
|
-
resolveByAssetId(assetId) {
|
|
3967
|
-
const row = this.db.prepare("SELECT * FROM asset_metadata_index WHERE workspace_id = ? AND asset_id = ?").get(this.workspaceId, assetId);
|
|
3968
|
-
return row ? rowToMetadata(row) : void 0;
|
|
3969
|
-
}
|
|
3970
|
-
resolveByContentHash(contentHash) {
|
|
3971
|
-
const row = this.db.prepare("SELECT * FROM asset_metadata_index WHERE workspace_id = ? AND content_hash = ?").get(this.workspaceId, contentHash);
|
|
3972
|
-
return row ? rowToMetadata(row) : void 0;
|
|
3973
|
-
}
|
|
3974
|
-
/** Batch exact match — returns Map for O(1) access. */
|
|
3975
|
-
resolveByFilenames(filenames) {
|
|
3976
|
-
if (filenames.length === 0) return /* @__PURE__ */ new Map();
|
|
3977
|
-
const placeholders = filenames.map(() => "?").join(",");
|
|
3978
|
-
const params = [this.workspaceId, ...filenames];
|
|
3979
|
-
const rows = this.db.prepare(
|
|
3980
|
-
`SELECT * FROM asset_metadata_index
|
|
3981
|
-
WHERE workspace_id = ? AND filename IN (${placeholders})`
|
|
3982
|
-
).all(...params);
|
|
3983
|
-
const map = /* @__PURE__ */ new Map();
|
|
3984
|
-
for (const row of rows) {
|
|
3985
|
-
if (row.filename) map.set(row.filename, rowToMetadata(row));
|
|
3986
|
-
}
|
|
3987
|
-
return map;
|
|
3988
|
-
}
|
|
3989
|
-
};
|
|
3990
|
-
|
|
3991
|
-
// src/daemon/asset/mirror.ts
|
|
3992
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
3993
|
-
import { join as join8 } from "path";
|
|
3994
|
-
function rowToBinding(row) {
|
|
3995
|
-
return {
|
|
3996
|
-
workspaceId: row.workspace_id,
|
|
3997
|
-
path: row.path,
|
|
3998
|
-
assetId: row.asset_id,
|
|
3999
|
-
contentHash: row.content_hash,
|
|
4000
|
-
version: row.version,
|
|
4001
|
-
syncedAt: row.synced_at,
|
|
4002
|
-
dirty: row.dirty === 1
|
|
4003
|
-
};
|
|
4004
|
-
}
|
|
4005
|
-
var WorkspaceMirror = class {
|
|
4006
|
-
db;
|
|
4007
|
-
cloud;
|
|
4008
|
-
workspaceId;
|
|
4009
|
-
cursorPath;
|
|
4010
|
-
constructor(opts) {
|
|
4011
|
-
this.db = opts.db;
|
|
4012
|
-
this.cloud = opts.cloud;
|
|
4013
|
-
this.workspaceId = opts.workspaceId;
|
|
4014
|
-
if (!existsSync9(opts.workspaceStateDir)) {
|
|
4015
|
-
mkdirSync7(opts.workspaceStateDir, { recursive: true });
|
|
4016
|
-
}
|
|
4017
|
-
this.cursorPath = join8(opts.workspaceStateDir, "asset-cursor.json");
|
|
4018
|
-
}
|
|
4019
|
-
/** Persisted cursor for this workspace, or null on first run / corrupted file. */
|
|
4020
|
-
readCursor() {
|
|
4021
|
-
if (!existsSync9(this.cursorPath)) return null;
|
|
4022
|
-
try {
|
|
4023
|
-
const parsed = JSON.parse(readFileSync8(this.cursorPath, "utf8"));
|
|
4024
|
-
if (parsed.workspaceId !== this.workspaceId) return null;
|
|
4025
|
-
return parsed.cursor;
|
|
4026
|
-
} catch {
|
|
4027
|
-
return null;
|
|
4028
|
-
}
|
|
4029
|
-
}
|
|
4030
|
-
writeCursor(cursor) {
|
|
4031
|
-
const payload = {
|
|
4032
|
-
workspaceId: this.workspaceId,
|
|
4033
|
-
cursor,
|
|
4034
|
-
writtenAt: Date.now()
|
|
4035
|
-
};
|
|
4036
|
-
writeFileSync7(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
4037
|
-
}
|
|
4038
|
-
/**
|
|
4039
|
-
* Pull workspace_file changes since the persisted cursor and upsert into
|
|
4040
|
-
* the local mirror. Soft-deleted rows are removed from the mirror; active
|
|
4041
|
-
* rows are upserted with synced_at = now and dirty = 0.
|
|
4042
|
-
*
|
|
4043
|
-
* Returns the count of items applied + the new cursor (may be unchanged
|
|
4044
|
-
* when the cloud has nothing new). Idempotent — safe to retry.
|
|
4045
|
-
*/
|
|
4046
|
-
async pullDelta(opts) {
|
|
4047
|
-
const since = this.readCursor();
|
|
4048
|
-
const sinceParam = since ? `?since=${encodeURIComponent(since)}` : "";
|
|
4049
|
-
const envelope2 = await this.cloud.get(
|
|
4050
|
-
`/api/im/workspaces/${encodeURIComponent(this.workspaceId)}/files/sync${sinceParam}`,
|
|
4051
|
-
{ signal: opts?.signal }
|
|
4052
|
-
);
|
|
4053
|
-
const upsert = this.db.prepare(`
|
|
4054
|
-
INSERT INTO workspace_files_mirror (workspace_id, path, asset_id, content_hash, version, synced_at, dirty)
|
|
4055
|
-
VALUES (?, ?, ?, ?, ?, ?, 0)
|
|
4056
|
-
ON CONFLICT(workspace_id, path) DO UPDATE SET
|
|
4057
|
-
asset_id = excluded.asset_id,
|
|
4058
|
-
content_hash = excluded.content_hash,
|
|
4059
|
-
version = excluded.version,
|
|
4060
|
-
synced_at = excluded.synced_at,
|
|
4061
|
-
dirty = 0
|
|
4062
|
-
`);
|
|
4063
|
-
const remove = this.db.prepare(
|
|
4064
|
-
"DELETE FROM workspace_files_mirror WHERE workspace_id = ? AND path = ?"
|
|
4065
|
-
);
|
|
4066
|
-
const now = Date.now();
|
|
4067
|
-
let applied = 0;
|
|
4068
|
-
const tx = this.db.transaction((items) => {
|
|
4069
|
-
for (const item of items) {
|
|
4070
|
-
if (item.workspaceId !== this.workspaceId) continue;
|
|
4071
|
-
if (item.deletedAt) {
|
|
4072
|
-
remove.run(this.workspaceId, item.path);
|
|
4073
|
-
} else if (item.contentHash) {
|
|
4074
|
-
upsert.run(this.workspaceId, item.path, item.assetId, item.contentHash, item.version, now);
|
|
4075
|
-
} else {
|
|
4076
|
-
remove.run(this.workspaceId, item.path);
|
|
4077
|
-
}
|
|
4078
|
-
applied += 1;
|
|
4079
|
-
}
|
|
4080
|
-
});
|
|
4081
|
-
tx(envelope2.items);
|
|
4082
|
-
this.writeCursor(envelope2.cursor);
|
|
4083
|
-
return { applied, cursor: envelope2.cursor };
|
|
4084
|
-
}
|
|
4085
|
-
/** Lookup the active binding for a workspace path. Returns undefined if not mirrored. */
|
|
4086
|
-
lookupByPath(path9) {
|
|
4087
|
-
const row = this.db.prepare("SELECT * FROM workspace_files_mirror WHERE workspace_id = ? AND path = ?").get(this.workspaceId, path9);
|
|
4088
|
-
return row ? rowToBinding(row) : void 0;
|
|
4089
|
-
}
|
|
4090
|
-
/** Find an asset's contentHash via any path binding (asset may map to many paths). */
|
|
4091
|
-
hashForAssetId(assetId) {
|
|
4092
|
-
const row = this.db.prepare(
|
|
4093
|
-
"SELECT content_hash FROM workspace_files_mirror WHERE workspace_id = ? AND asset_id = ? LIMIT 1"
|
|
4094
|
-
).get(this.workspaceId, assetId);
|
|
4095
|
-
return row?.content_hash;
|
|
4096
|
-
}
|
|
4097
|
-
/** All active bindings for this workspace — used by status / debug surfaces. */
|
|
4098
|
-
list() {
|
|
4099
|
-
const rows = this.db.prepare("SELECT * FROM workspace_files_mirror WHERE workspace_id = ? ORDER BY path ASC").all(this.workspaceId);
|
|
4100
|
-
return rows.map(rowToBinding);
|
|
4101
|
-
}
|
|
4102
|
-
/** Wipe the mirror for this workspace (e.g., re-pair). Cursor file is left intact. */
|
|
4103
|
-
clear() {
|
|
4104
|
-
this.db.prepare("DELETE FROM workspace_files_mirror WHERE workspace_id = ?").run(this.workspaceId);
|
|
4105
|
-
}
|
|
4106
|
-
};
|
|
4107
|
-
|
|
4108
|
-
// src/cli/commands/asset.ts
|
|
4109
|
-
init_util();
|
|
4110
|
-
init_ui();
|
|
4111
|
-
function buildAssetCommand() {
|
|
4112
|
-
const cmd = new Command3("asset").description("Inspect IM assets");
|
|
4113
|
-
cmd.command("sync").description("Sync remote asset indexes into the local daemon DB; optionally prefetch bytes").requiredOption("--workspace-id <id>", "Workspace id").option("--bytes", "Also prefetch asset bytes into ~/.prismer/cache").option("--limit <n>", "Maximum number of assets to prefetch when --bytes is set", parsePositiveInt).option("--json", "Output machine-readable JSON").action(runAction(async (opts) => {
|
|
4114
|
-
const result = await syncAssets(opts);
|
|
4115
|
-
if (opts.json) {
|
|
4116
|
-
printJson({ ok: true, data: result });
|
|
4117
|
-
return;
|
|
4118
|
-
}
|
|
4119
|
-
getUI().line(`Synced asset metadata: ${result.metadata.applied} applied (cursor=${result.metadata.cursor})`);
|
|
4120
|
-
getUI().line(`Synced workspace files: ${result.workspaceFiles.applied} applied (cursor=${result.workspaceFiles.cursor ?? "null"})`);
|
|
4121
|
-
if (opts.bytes) {
|
|
4122
|
-
getUI().line(
|
|
4123
|
-
`Prefetched bytes: ${result.prefetch.fetched}/${result.prefetch.considered} assets, ${result.prefetch.bytes} bytes`
|
|
4124
|
-
);
|
|
4125
|
-
if (result.prefetch.failed.length > 0) {
|
|
4126
|
-
getUI().line(`Prefetch failures: ${result.prefetch.failed.length}`);
|
|
4127
|
-
}
|
|
4128
|
-
} else {
|
|
4129
|
-
getUI().line("Asset bytes were not prefetched; they are downloaded lazily on first use. Run with --bytes to fill ~/.prismer/cache.");
|
|
4130
|
-
}
|
|
4131
|
-
}, { code: "asset_sync_failed" }));
|
|
4132
|
-
cmd.command("list").description("List assets for a workspace, optionally filtered by task").option("--workspace-id <id>", "Workspace id").option("--task-id <id>", "Task id filter").option("--json", "Output machine-readable JSON").action(runAction(async (opts) => {
|
|
4133
|
-
const cloud = mkCloud();
|
|
4134
|
-
const query = new URLSearchParams();
|
|
4135
|
-
if (opts.workspaceId) query.set("workspaceId", opts.workspaceId);
|
|
4136
|
-
if (opts.taskId) query.set("taskId", opts.taskId);
|
|
4137
|
-
const path9 = `/api/im/assets${query.toString() ? `?${query.toString()}` : ""}`;
|
|
4138
|
-
const res = await cloud.request("GET", path9);
|
|
4139
|
-
if (!res.ok) exitWithError(`asset list failed (${res.status}): ${res.error?.message ?? "request failed"}`, { code: res.error?.code ?? "asset_list_failed" });
|
|
4140
|
-
const body = res.data;
|
|
4141
|
-
if (isErrorEnvelope(body)) {
|
|
4142
|
-
exitWithError(`asset list failed (${res.status}): ${errorMessage(body)}`, { code: "asset_list_failed" });
|
|
4143
|
-
}
|
|
4144
|
-
if (opts.json) {
|
|
4145
|
-
printJson(body ?? null);
|
|
4146
|
-
return;
|
|
4147
|
-
}
|
|
4148
|
-
printAssetList(body);
|
|
4149
|
-
}, { code: "asset_list_failed" }));
|
|
4150
|
-
cmd.command("upload <path>").description("Upload a local file or directory as IM assets").requiredOption("--workspace-id <id>", "Workspace id").option("--kind <kind>", "Asset kind", "file").option("--folder-path <path>", "Cloud folderPath prefix for the uploaded file(s)").option("--task-id <id>", "Source task id").option("--agent-id <id>", "Source agent IM user id").option("--container-id <id>", "Container id; also added to metadata for sandbox-output assets").option("--metadata <json>", "Additional JSON metadata").option("--mime <type>", "Override file MIME type").option("--json", "Output machine-readable JSON").action(runAction(async (inputPath, opts) => {
|
|
4151
|
-
const body = await uploadAssetPath(inputPath, opts);
|
|
4152
|
-
if (opts.json) {
|
|
4153
|
-
printJson(body ?? null);
|
|
4154
|
-
return;
|
|
4155
|
-
}
|
|
4156
|
-
if (isUploadManyResult(body)) {
|
|
4157
|
-
getUI().line(`Uploaded ${body.data.count} assets`);
|
|
4158
|
-
for (const item of body.data.items) {
|
|
4159
|
-
printAssetDetail("Uploaded asset", item.file, item.response);
|
|
4160
|
-
}
|
|
4161
|
-
} else {
|
|
4162
|
-
printAssetDetail("Uploaded asset", inputPath, body);
|
|
4163
|
-
}
|
|
4164
|
-
}, { code: "asset_upload_failed" }));
|
|
4165
|
-
cmd.command("download <assetId>").description("Download asset bytes").requiredOption("--out <path>", "Destination file path").action(runAction(async (assetId, opts) => {
|
|
4166
|
-
await downloadAsset(assetId, opts.out);
|
|
4167
|
-
getUI().line(`Downloaded ${assetId} -> ${opts.out}`);
|
|
4168
|
-
}, { code: "asset_download_failed" }));
|
|
4169
|
-
cmd.command("get <assetId>").description("Fetch asset metadata by id").option("--json", "Output machine-readable JSON").action(runAction(async (assetId, opts) => {
|
|
4170
|
-
const cloud = mkCloud();
|
|
4171
|
-
const res = await cloud.request("GET", `/api/im/assets/${encodeURIComponent(assetId)}/detail`);
|
|
4172
|
-
if (!res.ok) exitWithError(`asset get failed (${res.status}): ${res.error?.message ?? "request failed"}`, { code: res.error?.code ?? "asset_get_failed" });
|
|
4173
|
-
const body = res.data;
|
|
4174
|
-
if (isErrorEnvelope(body)) {
|
|
4175
|
-
exitWithError(`asset get failed (${res.status}): ${errorMessage(body)}`, { code: "asset_get_failed" });
|
|
4176
|
-
}
|
|
4177
|
-
if (opts.json) {
|
|
4178
|
-
printJson(body ?? null);
|
|
4179
|
-
return;
|
|
4180
|
-
}
|
|
4181
|
-
printAssetDetail("Asset", assetId, body);
|
|
4182
|
-
}, { code: "asset_get_failed" }));
|
|
4183
|
-
cmd.command("by-hash <sha256>").description("Fetch asset metadata by content hash").option("--workspace-id <id>", "Workspace id (required by the cloud API)").option("--json", "Output machine-readable JSON").action(runAction(async (sha2564, opts) => {
|
|
4184
|
-
const cloud = mkCloud();
|
|
4185
|
-
const query = new URLSearchParams();
|
|
4186
|
-
if (opts.workspaceId) query.set("wsId", opts.workspaceId);
|
|
4187
|
-
const path9 = `/api/im/assets/by-hash/${encodeURIComponent(sha2564)}${query.toString() ? `?${query.toString()}` : ""}`;
|
|
4188
|
-
const res = await cloud.request("GET", path9);
|
|
4189
|
-
if (!res.ok) exitWithError(`asset by-hash failed (${res.status}): ${res.error?.message ?? "request failed"}`, { code: res.error?.code ?? "asset_by_hash_failed" });
|
|
4190
|
-
const body = res.data;
|
|
4191
|
-
if (isErrorEnvelope(body)) {
|
|
4192
|
-
exitWithError(`asset by-hash failed (${res.status}): ${errorMessage(body)}`, { code: "asset_by_hash_failed" });
|
|
4193
|
-
}
|
|
4194
|
-
if (opts.json) {
|
|
4195
|
-
printJson(body ?? null);
|
|
4196
|
-
return;
|
|
4197
|
-
}
|
|
4198
|
-
printAssetDetail("Asset hash", sha2564, body);
|
|
4199
|
-
}, { code: "asset_by_hash_failed" }));
|
|
4200
|
-
return cmd;
|
|
4201
|
-
}
|
|
4202
|
-
function mkCloud() {
|
|
4203
|
-
const cfg = loadConfig(resolvePaths());
|
|
4204
|
-
return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
4205
|
-
}
|
|
4206
|
-
async function syncAssets(opts) {
|
|
4207
|
-
const paths = resolvePaths();
|
|
4208
|
-
const cfg = loadConfig(paths);
|
|
4209
|
-
const cloud = new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
4210
|
-
const db = openLocalDb(paths.localDb);
|
|
4211
|
-
try {
|
|
4212
|
-
const stateDir = `${paths.root}/${opts.workspaceId}`;
|
|
4213
|
-
const metadataIndex = new AssetMetadataIndex({
|
|
4214
|
-
db,
|
|
4215
|
-
cloud,
|
|
4216
|
-
workspaceId: opts.workspaceId,
|
|
4217
|
-
workspaceStateDir: stateDir
|
|
4218
|
-
});
|
|
4219
|
-
const mirror = new WorkspaceMirror({
|
|
4220
|
-
db,
|
|
4221
|
-
cloud,
|
|
4222
|
-
workspaceId: opts.workspaceId,
|
|
4223
|
-
workspaceStateDir: stateDir
|
|
4224
|
-
});
|
|
4225
|
-
const [metadata, workspaceFiles] = await Promise.all([
|
|
4226
|
-
metadataIndex.pullDelta(),
|
|
4227
|
-
mirror.pullDelta()
|
|
4228
|
-
]);
|
|
4229
|
-
const prefetch = opts.bytes ? await prefetchAssetBytes({
|
|
4230
|
-
db,
|
|
4231
|
-
cloud,
|
|
4232
|
-
workspaceId: opts.workspaceId,
|
|
4233
|
-
cacheDir: paths.cacheDir,
|
|
4234
|
-
maxBytes: cfg.cache?.max_bytes,
|
|
4235
|
-
limit: opts.limit
|
|
4236
|
-
}) : { considered: 0, fetched: 0, bytes: 0, failed: [] };
|
|
4237
|
-
return { workspaceId: opts.workspaceId, metadata, workspaceFiles, prefetch };
|
|
4238
|
-
} finally {
|
|
4239
|
-
db.close();
|
|
4240
|
-
}
|
|
4241
|
-
}
|
|
4242
|
-
async function prefetchAssetBytes(opts) {
|
|
4243
|
-
const limitClause = opts.limit ? " LIMIT ?" : "";
|
|
4244
|
-
const params = [opts.workspaceId];
|
|
4245
|
-
if (opts.limit) params.push(opts.limit);
|
|
4246
|
-
const rows = opts.db.prepare(
|
|
4247
|
-
`SELECT asset_id, content_hash, mime, size_bytes
|
|
4248
|
-
FROM asset_metadata_index
|
|
4249
|
-
WHERE workspace_id = ?
|
|
4250
|
-
ORDER BY asset_index_seq DESC${limitClause}`
|
|
4251
|
-
).all(...params);
|
|
4252
|
-
const cache = new AssetCache({
|
|
4253
|
-
db: opts.db,
|
|
4254
|
-
cloud: opts.cloud,
|
|
4255
|
-
cacheDir: opts.cacheDir,
|
|
4256
|
-
maxBytes: opts.maxBytes
|
|
4257
|
-
});
|
|
4258
|
-
let fetched = 0;
|
|
4259
|
-
let bytes = 0;
|
|
4260
|
-
const failed = [];
|
|
4261
|
-
for (const row of rows) {
|
|
4262
|
-
try {
|
|
4263
|
-
const cached = await cache.getOrFetch(row.content_hash, {
|
|
4264
|
-
workspaceIdHint: opts.workspaceId,
|
|
4265
|
-
assetId: row.asset_id
|
|
4266
|
-
});
|
|
4267
|
-
fetched += 1;
|
|
4268
|
-
bytes += cached.sizeBytes;
|
|
4269
|
-
} catch (err) {
|
|
4270
|
-
failed.push({
|
|
4271
|
-
assetId: row.asset_id,
|
|
4272
|
-
contentHash: row.content_hash,
|
|
4273
|
-
error: err.message
|
|
4274
|
-
});
|
|
4275
|
-
}
|
|
4276
|
-
}
|
|
4277
|
-
return { considered: rows.length, fetched, bytes, failed };
|
|
4278
|
-
}
|
|
4279
|
-
async function uploadAssetPath(inputPath, opts) {
|
|
4280
|
-
if (!existsSync10(inputPath)) exitWithError(`path not found: ${inputPath}`);
|
|
4281
|
-
const stat = lstatSync(inputPath);
|
|
4282
|
-
if (stat.isSymbolicLink()) exitWithError(`refusing to upload symlink: ${inputPath}`);
|
|
4283
|
-
if (stat.isFile()) {
|
|
4284
|
-
return uploadAsset(inputPath, { ...opts, folderPath: normalizeFolderPath(opts.folderPath) });
|
|
4285
|
-
}
|
|
4286
|
-
if (!stat.isDirectory()) exitWithError(`path is not a file or directory: ${inputPath}`);
|
|
4287
|
-
const files = listFilesRecursive(inputPath);
|
|
4288
|
-
if (files.length === 0) exitWithError(`directory has no uploadable files: ${inputPath}`);
|
|
4289
|
-
const items = [];
|
|
4290
|
-
for (const file of files) {
|
|
4291
|
-
const relDir = dirname7(relative(inputPath, file));
|
|
4292
|
-
const folderPath = combineFolderPath(opts.folderPath, relDir === "." ? "" : relDir);
|
|
4293
|
-
const response = await uploadAsset(file, { ...opts, folderPath });
|
|
4294
|
-
items.push({ file, folderPath: folderPath ?? null, response });
|
|
4295
|
-
}
|
|
4296
|
-
return { ok: true, data: { count: items.length, items } };
|
|
3558
|
+
function mkCloud() {
|
|
3559
|
+
const cfg = loadConfig(resolvePaths());
|
|
3560
|
+
return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
|
|
4297
3561
|
}
|
|
4298
3562
|
async function uploadAsset(file, opts) {
|
|
4299
|
-
if (!
|
|
3563
|
+
if (!existsSync7(file)) exitWithError(`file not found: ${file}`);
|
|
4300
3564
|
const cfg = loadConfig(resolvePaths());
|
|
4301
|
-
const bytes =
|
|
3565
|
+
const bytes = readFileSync6(file);
|
|
4302
3566
|
const metadata = parseMetadata(opts.metadata);
|
|
4303
3567
|
if (opts.taskId && metadata.taskId === void 0) metadata.taskId = opts.taskId;
|
|
4304
3568
|
if (opts.containerId && metadata.containerId === void 0) metadata.containerId = opts.containerId;
|
|
@@ -4307,7 +3571,6 @@ async function uploadAsset(file, opts) {
|
|
|
4307
3571
|
form.set("kind", opts.kind);
|
|
4308
3572
|
if (opts.agentId) form.set("sourceAgentImUserId", opts.agentId);
|
|
4309
3573
|
if (opts.taskId) form.set("sourceTaskId", opts.taskId);
|
|
4310
|
-
if (opts.folderPath !== void 0) form.set("folderPath", opts.folderPath);
|
|
4311
3574
|
if (Object.keys(metadata).length > 0) form.set("metadata", JSON.stringify(metadata));
|
|
4312
3575
|
form.set("file", new Blob([bytes], { type: opts.mime ?? "application/octet-stream" }), basename(file));
|
|
4313
3576
|
const url = `${cfg.cloud_api_base.replace(/\/$/, "")}/api/im/assets`;
|
|
@@ -4327,37 +3590,6 @@ async function uploadAsset(file, opts) {
|
|
|
4327
3590
|
}
|
|
4328
3591
|
return body;
|
|
4329
3592
|
}
|
|
4330
|
-
function listFilesRecursive(root) {
|
|
4331
|
-
const out = [];
|
|
4332
|
-
for (const name of readdirSync(root)) {
|
|
4333
|
-
if (name.startsWith(".")) continue;
|
|
4334
|
-
const full = join9(root, name);
|
|
4335
|
-
const stat = lstatSync(full);
|
|
4336
|
-
if (stat.isSymbolicLink()) continue;
|
|
4337
|
-
if (stat.isDirectory()) {
|
|
4338
|
-
out.push(...listFilesRecursive(full));
|
|
4339
|
-
} else if (stat.isFile()) {
|
|
4340
|
-
out.push(full);
|
|
4341
|
-
}
|
|
4342
|
-
}
|
|
4343
|
-
return out.sort();
|
|
4344
|
-
}
|
|
4345
|
-
function combineFolderPath(prefix, relDir) {
|
|
4346
|
-
const normalizedPrefix = normalizeFolderPath(prefix);
|
|
4347
|
-
const normalizedRel = normalizeFolderPath(relDir);
|
|
4348
|
-
if (normalizedPrefix && normalizedRel) return `${normalizedPrefix}/${normalizedRel}`;
|
|
4349
|
-
return normalizedPrefix ?? normalizedRel;
|
|
4350
|
-
}
|
|
4351
|
-
function normalizeFolderPath(value) {
|
|
4352
|
-
if (value === void 0) return void 0;
|
|
4353
|
-
const normalized = value.trim().replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
4354
|
-
return normalized.length > 0 ? normalized : "";
|
|
4355
|
-
}
|
|
4356
|
-
function isUploadManyResult(raw) {
|
|
4357
|
-
return Boolean(
|
|
4358
|
-
raw && typeof raw === "object" && raw.ok === true && raw.data && Array.isArray(raw.data.items)
|
|
4359
|
-
);
|
|
4360
|
-
}
|
|
4361
3593
|
async function downloadAsset(assetId, outPath) {
|
|
4362
3594
|
const cloud = mkCloud();
|
|
4363
3595
|
const res = await cloud.fetchRaw(`/api/im/assets/${encodeURIComponent(assetId)}`);
|
|
@@ -4366,7 +3598,7 @@ async function downloadAsset(assetId, outPath) {
|
|
|
4366
3598
|
exitWithError(`asset download failed (${res.status}): ${errorMessage(body)}`);
|
|
4367
3599
|
}
|
|
4368
3600
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
4369
|
-
|
|
3601
|
+
writeFileSync5(outPath, bytes);
|
|
4370
3602
|
}
|
|
4371
3603
|
function parseMetadata(raw) {
|
|
4372
3604
|
if (!raw) return {};
|
|
@@ -4451,13 +3683,6 @@ function stringField(obj, key) {
|
|
|
4451
3683
|
const value = obj?.[key];
|
|
4452
3684
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
4453
3685
|
}
|
|
4454
|
-
function parsePositiveInt(raw) {
|
|
4455
|
-
const n = Number(raw);
|
|
4456
|
-
if (!Number.isInteger(n) || n <= 0) {
|
|
4457
|
-
throw new Error("--limit must be a positive integer");
|
|
4458
|
-
}
|
|
4459
|
-
return n;
|
|
4460
|
-
}
|
|
4461
3686
|
|
|
4462
3687
|
// src/cli/commands/banner.ts
|
|
4463
3688
|
init_util();
|
|
@@ -4513,7 +3738,7 @@ ${name} (${role})`);
|
|
|
4513
3738
|
getUI().line(`Group created ${data.groupId ?? "(unknown)"}
|
|
4514
3739
|
${data.title ?? opts.name} (${memberCount} members)`);
|
|
4515
3740
|
});
|
|
4516
|
-
group.command("messages <groupId>").description("List group messages").option("--limit <n>", "Max messages",
|
|
3741
|
+
group.command("messages <groupId>").description("List group messages").option("--limit <n>", "Max messages", parsePositiveInt).option("--before <id>", "Return messages before this message id").option("--json", "Print raw JSON response").action(async (groupId, opts) => {
|
|
4517
3742
|
const data = await requestOrExit(
|
|
4518
3743
|
"GET",
|
|
4519
3744
|
withQuery(`/api/im/groups/${encodeURIComponent(groupId)}/messages`, {
|
|
@@ -4551,7 +3776,7 @@ ${data.title ?? opts.name} (${memberCount} members)`);
|
|
|
4551
3776
|
getUI().line(`Removed ${memberId} from group ${groupId}`);
|
|
4552
3777
|
});
|
|
4553
3778
|
cmd.addCommand(group);
|
|
4554
|
-
cmd.command("messages <conversationId>").description("List conversation messages").option("--limit <n>", "Max messages",
|
|
3779
|
+
cmd.command("messages <conversationId>").description("List conversation messages").option("--limit <n>", "Max messages", parsePositiveInt).option("--before <id>", "Return messages before this message id").option("--json", "Print raw JSON response").action(async (conversationId, opts) => {
|
|
4555
3780
|
const data = await requestOrExit(
|
|
4556
3781
|
"GET",
|
|
4557
3782
|
withQuery(`/api/im/messages/${encodeURIComponent(conversationId)}`, {
|
|
@@ -4567,15 +3792,15 @@ ${data.title ?? opts.name} (${memberCount} members)`);
|
|
|
4567
3792
|
});
|
|
4568
3793
|
return cmd;
|
|
4569
3794
|
}
|
|
4570
|
-
async function requestOrExit(method,
|
|
3795
|
+
async function requestOrExit(method, path7, body) {
|
|
4571
3796
|
let res;
|
|
4572
3797
|
try {
|
|
4573
|
-
res = await mkCloud2().request(method,
|
|
3798
|
+
res = await mkCloud2().request(method, path7, body === void 0 ? void 0 : { body });
|
|
4574
3799
|
} catch (err) {
|
|
4575
3800
|
exitWithError(sanitizeError(err.message));
|
|
4576
3801
|
}
|
|
4577
3802
|
if (!res.ok) {
|
|
4578
|
-
exitWithError(`${method} ${
|
|
3803
|
+
exitWithError(`${method} ${path7} failed (${res.status}): ${sanitizeError(res.error?.message ?? "unknown")}`);
|
|
4579
3804
|
}
|
|
4580
3805
|
return unwrapEnvelope(res.data);
|
|
4581
3806
|
}
|
|
@@ -4607,20 +3832,20 @@ function unwrapEnvelope(raw) {
|
|
|
4607
3832
|
function parseCsv(value) {
|
|
4608
3833
|
return value.split(",").map((v) => v.trim()).filter(Boolean);
|
|
4609
3834
|
}
|
|
4610
|
-
function
|
|
3835
|
+
function parsePositiveInt(value) {
|
|
4611
3836
|
const parsed = Number.parseInt(value, 10);
|
|
4612
3837
|
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
4613
3838
|
throw new Error("expected a positive integer");
|
|
4614
3839
|
}
|
|
4615
3840
|
return parsed;
|
|
4616
3841
|
}
|
|
4617
|
-
function withQuery(
|
|
3842
|
+
function withQuery(path7, params) {
|
|
4618
3843
|
const query = new URLSearchParams();
|
|
4619
3844
|
for (const [key, value] of Object.entries(params)) {
|
|
4620
3845
|
if (value !== void 0) query.set(key, String(value));
|
|
4621
3846
|
}
|
|
4622
3847
|
const suffix = query.toString();
|
|
4623
|
-
return suffix ? `${
|
|
3848
|
+
return suffix ? `${path7}?${suffix}` : path7;
|
|
4624
3849
|
}
|
|
4625
3850
|
function printMessageSummary(label, data) {
|
|
4626
3851
|
const msg = data.message ?? {};
|
|
@@ -4747,7 +3972,7 @@ init_util();
|
|
|
4747
3972
|
init_ui();
|
|
4748
3973
|
function buildCookbookCommand() {
|
|
4749
3974
|
const cmd = new Command7("cookbook").description("Run CLI-only 54release MVP regression suites");
|
|
4750
|
-
cmd.command("run").description("Run one or more cookbook smoke suites using the configured API key").option("--suite <name>", "status|im|task|group|asset|sandbox|all, comma-separated", "all").option("--workspace-id <id>", "Workspace id for workspace-scoped suites").option("--agent-id <id>", "Agent IM user id for task create smoke").option("--group-id <id>", "Group/conversation id for group message history smoke").option("--sandbox-id <id>", "Sandbox id for sandbox status smoke").option("--prompt <text>", "Prompt for optional task create smoke").option("--timeout-ms <ms>", "Request/task timeout",
|
|
3975
|
+
cmd.command("run").description("Run one or more cookbook smoke suites using the configured API key").option("--suite <name>", "status|im|task|group|asset|sandbox|all, comma-separated", "all").option("--workspace-id <id>", "Workspace id for workspace-scoped suites").option("--agent-id <id>", "Agent IM user id for task create smoke").option("--group-id <id>", "Group/conversation id for group message history smoke").option("--sandbox-id <id>", "Sandbox id for sandbox status smoke").option("--prompt <text>", "Prompt for optional task create smoke").option("--timeout-ms <ms>", "Request/task timeout", parsePositiveInt2, 6e4).option("--strict", "Treat skipped optional checks as failure").option("--json", "Output machine-readable JSON").action(runAction(async (opts) => {
|
|
4751
3976
|
const suites = parseSuites(opts.suite);
|
|
4752
3977
|
const cloud = mkCloud3(opts.timeoutMs);
|
|
4753
3978
|
const checks = [];
|
|
@@ -4789,8 +4014,8 @@ async function runStatusSuite(cloud) {
|
|
|
4789
4014
|
await check("status", "cloud health", "GET", "/api/health"),
|
|
4790
4015
|
await check("status", "current IM identity", "GET", "/api/im/me", void 0, cloud)
|
|
4791
4016
|
];
|
|
4792
|
-
async function check(suite, name, method,
|
|
4793
|
-
return requestCheck(client, suite, name, method,
|
|
4017
|
+
async function check(suite, name, method, path7, body, client = cloud) {
|
|
4018
|
+
return requestCheck(client, suite, name, method, path7, body);
|
|
4794
4019
|
}
|
|
4795
4020
|
}
|
|
4796
4021
|
async function runIMSuite(cloud) {
|
|
@@ -4864,27 +4089,27 @@ async function runSandboxSuite(cloud, opts) {
|
|
|
4864
4089
|
}
|
|
4865
4090
|
return checks;
|
|
4866
4091
|
}
|
|
4867
|
-
async function requestCheck(cloud, suite, name, method,
|
|
4092
|
+
async function requestCheck(cloud, suite, name, method, path7, body) {
|
|
4868
4093
|
let res;
|
|
4869
4094
|
try {
|
|
4870
|
-
res = await cloud.request(method,
|
|
4095
|
+
res = await cloud.request(method, path7, body === void 0 ? void 0 : { body });
|
|
4871
4096
|
} catch (err) {
|
|
4872
|
-
return { suite, name, status: "fail", endpoint:
|
|
4097
|
+
return { suite, name, status: "fail", endpoint: path7, detail: sanitize(err.message) };
|
|
4873
4098
|
}
|
|
4874
4099
|
if (!res.ok) {
|
|
4875
4100
|
return {
|
|
4876
4101
|
suite,
|
|
4877
4102
|
name,
|
|
4878
4103
|
status: "fail",
|
|
4879
|
-
endpoint:
|
|
4104
|
+
endpoint: path7,
|
|
4880
4105
|
detail: sanitize(`${res.status}: ${res.error?.message ?? "request failed"}`)
|
|
4881
4106
|
};
|
|
4882
4107
|
}
|
|
4883
4108
|
const data = unwrap(res.data);
|
|
4884
4109
|
if (isApiError(res.data)) {
|
|
4885
|
-
return { suite, name, status: "fail", endpoint:
|
|
4110
|
+
return { suite, name, status: "fail", endpoint: path7, detail: sanitize(apiErrorMessage(res.data)), data: res.data };
|
|
4886
4111
|
}
|
|
4887
|
-
return { suite, name, status: "pass", endpoint:
|
|
4112
|
+
return { suite, name, status: "pass", endpoint: path7, detail: summarizeData(data), data };
|
|
4888
4113
|
}
|
|
4889
4114
|
async function pollTask(cloud, taskId, timeoutMs) {
|
|
4890
4115
|
const started = Date.now();
|
|
@@ -4991,7 +4216,7 @@ function taskFrom(data) {
|
|
|
4991
4216
|
function sanitize(message) {
|
|
4992
4217
|
return message.replace(/sk-prismer-[A-Za-z0-9_-]+/g, "sk-prismer-***");
|
|
4993
4218
|
}
|
|
4994
|
-
function
|
|
4219
|
+
function parsePositiveInt2(value) {
|
|
4995
4220
|
const parsed = Number.parseInt(value, 10);
|
|
4996
4221
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
4997
4222
|
throw new Error("expected a positive integer");
|
|
@@ -5002,16 +4227,14 @@ function parsePositiveInt3(value) {
|
|
|
5002
4227
|
// src/cli/commands/daemon.ts
|
|
5003
4228
|
import { Command as Command8 } from "commander";
|
|
5004
4229
|
import { spawn as spawn5 } from "child_process";
|
|
5005
|
-
import { createReadStream, existsSync as
|
|
5006
|
-
import { setTimeout as
|
|
5007
|
-
import { join as
|
|
4230
|
+
import { createReadStream, existsSync as existsSync13, mkdirSync as mkdirSync8, openSync, statSync as statSync3 } from "fs";
|
|
4231
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
4232
|
+
import { join as join11 } from "path";
|
|
5008
4233
|
|
|
5009
4234
|
// src/daemon/runner.ts
|
|
5010
4235
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
5011
|
-
import { existsSync as
|
|
5012
|
-
import {
|
|
5013
|
-
import { homedir as homedir7, platform } from "os";
|
|
5014
|
-
import { join as join16 } from "path";
|
|
4236
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
|
|
4237
|
+
import { platform } from "os";
|
|
5015
4238
|
|
|
5016
4239
|
// src/adapters/registry.ts
|
|
5017
4240
|
var AdapterRegistry = class {
|
|
@@ -5038,34 +4261,193 @@ var AdapterRegistry = class {
|
|
|
5038
4261
|
get(name) {
|
|
5039
4262
|
return this.adapters.get(name);
|
|
5040
4263
|
}
|
|
5041
|
-
list() {
|
|
5042
|
-
return Array.from(this.adapters.values());
|
|
4264
|
+
list() {
|
|
4265
|
+
return Array.from(this.adapters.values());
|
|
4266
|
+
}
|
|
4267
|
+
size() {
|
|
4268
|
+
return this.adapters.size;
|
|
4269
|
+
}
|
|
4270
|
+
/**
|
|
4271
|
+
* Find adapters that satisfy a capability tag.
|
|
4272
|
+
*
|
|
4273
|
+
* Match rules (deterministic, sorted by name):
|
|
4274
|
+
* - exact: adapter declared the tag verbatim
|
|
4275
|
+
* - wildcard: adapter declared `code.*` → matches `code.write`
|
|
4276
|
+
*/
|
|
4277
|
+
findByCapability(capability) {
|
|
4278
|
+
const matches2 = [];
|
|
4279
|
+
for (const adapter of this.adapters.values()) {
|
|
4280
|
+
if (adapter.capabilities.includes(capability)) {
|
|
4281
|
+
matches2.push(adapter);
|
|
4282
|
+
continue;
|
|
4283
|
+
}
|
|
4284
|
+
if (adapter.capabilities.some(
|
|
4285
|
+
(tag) => tag.endsWith(".*") && capability.startsWith(tag.slice(0, -1))
|
|
4286
|
+
)) {
|
|
4287
|
+
matches2.push(adapter);
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
matches2.sort((a, b) => a.name.localeCompare(b.name));
|
|
4291
|
+
return matches2;
|
|
4292
|
+
}
|
|
4293
|
+
};
|
|
4294
|
+
|
|
4295
|
+
// src/asset-cache.ts
|
|
4296
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
4297
|
+
import { dirname as dirname6, join as join5 } from "path";
|
|
4298
|
+
function rowToCached(row) {
|
|
4299
|
+
return {
|
|
4300
|
+
contentHash: row.content_hash,
|
|
4301
|
+
sizeBytes: row.size_bytes,
|
|
4302
|
+
mime: row.mime,
|
|
4303
|
+
localPath: row.local_path,
|
|
4304
|
+
fetchedAt: row.fetched_at,
|
|
4305
|
+
lastUsedAt: row.last_used_at,
|
|
4306
|
+
pin: row.pin === 1
|
|
4307
|
+
};
|
|
4308
|
+
}
|
|
4309
|
+
var AssetCache = class {
|
|
4310
|
+
db;
|
|
4311
|
+
cloud;
|
|
4312
|
+
cacheDir;
|
|
4313
|
+
maxBytes;
|
|
4314
|
+
constructor(opts) {
|
|
4315
|
+
this.db = opts.db;
|
|
4316
|
+
this.cloud = opts.cloud;
|
|
4317
|
+
this.cacheDir = opts.cacheDir;
|
|
4318
|
+
this.maxBytes = opts.maxBytes ?? 5 * 1024 * 1024 * 1024;
|
|
4319
|
+
if (!existsSync8(this.cacheDir)) {
|
|
4320
|
+
mkdirSync5(this.cacheDir, { recursive: true });
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
/** Local file path for a content hash. Files split by first 2 chars for fs scaling. */
|
|
4324
|
+
pathFor(hash) {
|
|
4325
|
+
return join5(this.cacheDir, hash.slice(0, 2), hash);
|
|
4326
|
+
}
|
|
4327
|
+
get(hash) {
|
|
4328
|
+
const row = this.db.prepare("SELECT * FROM cached_assets WHERE content_hash = ?").get(hash);
|
|
4329
|
+
if (!row) return void 0;
|
|
4330
|
+
if (!existsSync8(row.local_path)) {
|
|
4331
|
+
this.db.prepare("DELETE FROM cached_assets WHERE content_hash = ?").run(hash);
|
|
4332
|
+
return void 0;
|
|
4333
|
+
}
|
|
4334
|
+
this.touch(hash);
|
|
4335
|
+
return rowToCached({ ...row, last_used_at: Date.now() });
|
|
4336
|
+
}
|
|
4337
|
+
/** Bump `last_used_at` (call when an asset is consulted). */
|
|
4338
|
+
touch(hash) {
|
|
4339
|
+
this.db.prepare("UPDATE cached_assets SET last_used_at = ? WHERE content_hash = ?").run(Date.now(), hash);
|
|
4340
|
+
}
|
|
4341
|
+
pin(hash) {
|
|
4342
|
+
this.db.prepare("UPDATE cached_assets SET pin = 1 WHERE content_hash = ?").run(hash);
|
|
4343
|
+
}
|
|
4344
|
+
unpin(hash) {
|
|
4345
|
+
this.db.prepare("UPDATE cached_assets SET pin = 0 WHERE content_hash = ?").run(hash);
|
|
4346
|
+
}
|
|
4347
|
+
/**
|
|
4348
|
+
* Get or fetch by hash. On miss, downloads the raw bytes and caches them.
|
|
4349
|
+
*
|
|
4350
|
+
* Why two endpoints: `GET /api/im/assets/by-hash/:hash` returns the JSON
|
|
4351
|
+
* `AssetDTO` (id, contentHash, storageUri, …) — *metadata only*. The raw
|
|
4352
|
+
* bytes live at `GET /api/im/assets/:id`. If `opts.assetId` is supplied
|
|
4353
|
+
* the caller already has the id from an `AssetRef`, so we go straight to
|
|
4354
|
+
* the byte endpoint; otherwise we resolve `hash → id` first (one extra
|
|
4355
|
+
* round-trip on the metadata route) before downloading the body.
|
|
4356
|
+
*
|
|
4357
|
+
* Earlier this method called `by-hash` and treated the JSON body as the
|
|
4358
|
+
* asset content — the daemon stored the 500-byte metadata blob on disk
|
|
4359
|
+
* and `resolveAssetRefs` inlined that JSON into the prompt. The agent
|
|
4360
|
+
* saw `{"id":"…","storageUri":"…"}` instead of the user's file and
|
|
4361
|
+
* concluded the upload had failed.
|
|
4362
|
+
*/
|
|
4363
|
+
async getOrFetch(hash, opts) {
|
|
4364
|
+
const existing = this.get(hash);
|
|
4365
|
+
if (existing) return existing;
|
|
4366
|
+
const wsParam = opts?.workspaceIdHint ? `?wsId=${encodeURIComponent(opts.workspaceIdHint)}` : "";
|
|
4367
|
+
let assetId = opts?.assetId;
|
|
4368
|
+
if (!assetId) {
|
|
4369
|
+
const metaRes = await this.cloud.fetchRaw(
|
|
4370
|
+
`/api/im/assets/by-hash/${encodeURIComponent(hash)}${wsParam}`,
|
|
4371
|
+
{ signal: opts?.signal }
|
|
4372
|
+
);
|
|
4373
|
+
if (!metaRes.ok) {
|
|
4374
|
+
throw new Error(`Asset metadata fetch failed for ${hash}: HTTP ${metaRes.status}`);
|
|
4375
|
+
}
|
|
4376
|
+
const metaJson = await metaRes.json();
|
|
4377
|
+
if (!metaJson?.ok || typeof metaJson.data?.id !== "string") {
|
|
4378
|
+
throw new Error(`Asset metadata payload missing id for ${hash}`);
|
|
4379
|
+
}
|
|
4380
|
+
assetId = metaJson.data.id;
|
|
4381
|
+
}
|
|
4382
|
+
const res = await this.cloud.fetchRaw(`/api/im/assets/${encodeURIComponent(assetId)}`, {
|
|
4383
|
+
signal: opts?.signal
|
|
4384
|
+
});
|
|
4385
|
+
if (!res.ok) {
|
|
4386
|
+
throw new Error(`Asset bytes fetch failed for ${hash} (id=${assetId}): HTTP ${res.status}`);
|
|
4387
|
+
}
|
|
4388
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
4389
|
+
const localPath = this.pathFor(hash);
|
|
4390
|
+
if (!existsSync8(dirname6(localPath))) {
|
|
4391
|
+
mkdirSync5(dirname6(localPath), { recursive: true });
|
|
4392
|
+
}
|
|
4393
|
+
writeFileSync6(localPath, buf);
|
|
4394
|
+
const mime = res.headers.get("content-type");
|
|
4395
|
+
const now = Date.now();
|
|
4396
|
+
this.db.prepare(
|
|
4397
|
+
`INSERT OR REPLACE INTO cached_assets
|
|
4398
|
+
(content_hash, size_bytes, mime, local_path, fetched_at, last_used_at, pin)
|
|
4399
|
+
VALUES (?, ?, ?, ?, ?, ?, 0)`
|
|
4400
|
+
).run(hash, buf.length, mime, localPath, now, now);
|
|
4401
|
+
this.evictIfOver();
|
|
4402
|
+
return {
|
|
4403
|
+
contentHash: hash,
|
|
4404
|
+
sizeBytes: buf.length,
|
|
4405
|
+
mime,
|
|
4406
|
+
localPath,
|
|
4407
|
+
fetchedAt: now,
|
|
4408
|
+
lastUsedAt: now,
|
|
4409
|
+
pin: false
|
|
4410
|
+
};
|
|
4411
|
+
}
|
|
4412
|
+
/** Insert an already-on-disk asset into the cache (e.g., asset just produced by adapter). */
|
|
4413
|
+
registerLocal(hash, localPath, mime) {
|
|
4414
|
+
const size = statSync2(localPath).size;
|
|
4415
|
+
const now = Date.now();
|
|
4416
|
+
this.db.prepare(
|
|
4417
|
+
`INSERT OR REPLACE INTO cached_assets
|
|
4418
|
+
(content_hash, size_bytes, mime, local_path, fetched_at, last_used_at, pin)
|
|
4419
|
+
VALUES (?, ?, ?, ?, ?, ?, 1)`
|
|
4420
|
+
).run(hash, size, mime ?? null, localPath, now, now);
|
|
4421
|
+
return { contentHash: hash, sizeBytes: size, mime: mime ?? null, localPath, fetchedAt: now, lastUsedAt: now, pin: true };
|
|
5043
4422
|
}
|
|
5044
|
-
|
|
5045
|
-
|
|
4423
|
+
/** Total bytes across all rows (incl. pinned). */
|
|
4424
|
+
totalBytes() {
|
|
4425
|
+
const row = this.db.prepare("SELECT COALESCE(SUM(size_bytes),0) AS total FROM cached_assets").get();
|
|
4426
|
+
return row.total;
|
|
5046
4427
|
}
|
|
5047
|
-
/**
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
)) {
|
|
5064
|
-
matches2.push(adapter);
|
|
4428
|
+
/** Run LRU eviction until under cap. Pinned rows are never evicted. */
|
|
4429
|
+
evictIfOver() {
|
|
4430
|
+
let total = this.totalBytes();
|
|
4431
|
+
if (total <= this.maxBytes) return { removed: 0, freed: 0 };
|
|
4432
|
+
const candidates = this.db.prepare(
|
|
4433
|
+
`SELECT content_hash, size_bytes, local_path FROM cached_assets
|
|
4434
|
+
WHERE pin = 0
|
|
4435
|
+
ORDER BY last_used_at ASC`
|
|
4436
|
+
).all();
|
|
4437
|
+
let freed = 0;
|
|
4438
|
+
let removed = 0;
|
|
4439
|
+
for (const row of candidates) {
|
|
4440
|
+
if (total <= this.maxBytes) break;
|
|
4441
|
+
try {
|
|
4442
|
+
if (existsSync8(row.local_path)) unlinkSync2(row.local_path);
|
|
4443
|
+
} catch {
|
|
5065
4444
|
}
|
|
4445
|
+
this.db.prepare("DELETE FROM cached_assets WHERE content_hash = ?").run(row.content_hash);
|
|
4446
|
+
freed += row.size_bytes;
|
|
4447
|
+
total -= row.size_bytes;
|
|
4448
|
+
removed += 1;
|
|
5066
4449
|
}
|
|
5067
|
-
|
|
5068
|
-
return matches2;
|
|
4450
|
+
return { removed, freed };
|
|
5069
4451
|
}
|
|
5070
4452
|
};
|
|
5071
4453
|
|
|
@@ -5322,20 +4704,20 @@ var UriResolver = class {
|
|
|
5322
4704
|
}
|
|
5323
4705
|
return { texts: all, resolvedHashes: dedupe(allHashes) };
|
|
5324
4706
|
}
|
|
5325
|
-
async lookupFile(workspaceId,
|
|
4707
|
+
async lookupFile(workspaceId, path7, signal) {
|
|
5326
4708
|
const local = this.db.prepare(
|
|
5327
4709
|
"SELECT asset_id, content_hash, version FROM workspace_files_mirror WHERE workspace_id = ? AND path = ?"
|
|
5328
|
-
).get(workspaceId,
|
|
4710
|
+
).get(workspaceId, path7);
|
|
5329
4711
|
if (local) return local;
|
|
5330
4712
|
const remote = await this.cloud.get(
|
|
5331
|
-
`/api/im/workspaces/${encodeURIComponent(workspaceId)}/files?path=${encodeURIComponent(
|
|
4713
|
+
`/api/im/workspaces/${encodeURIComponent(workspaceId)}/files?path=${encodeURIComponent(path7)}`,
|
|
5332
4714
|
{ signal }
|
|
5333
4715
|
);
|
|
5334
4716
|
this.db.prepare(
|
|
5335
4717
|
`INSERT OR REPLACE INTO workspace_files_mirror
|
|
5336
4718
|
(workspace_id, path, asset_id, content_hash, version, synced_at, dirty)
|
|
5337
4719
|
VALUES (?, ?, ?, ?, ?, ?, 0)`
|
|
5338
|
-
).run(workspaceId,
|
|
4720
|
+
).run(workspaceId, path7, remote.assetId, remote.contentHash, remote.version, Date.now());
|
|
5339
4721
|
return { asset_id: remote.assetId, content_hash: remote.contentHash, version: remote.version };
|
|
5340
4722
|
}
|
|
5341
4723
|
};
|
|
@@ -5344,96 +4726,11 @@ function dedupe(xs) {
|
|
|
5344
4726
|
}
|
|
5345
4727
|
|
|
5346
4728
|
// src/daemon/dispatch.ts
|
|
5347
|
-
import { readFileSync as
|
|
4729
|
+
import { readFileSync as readFileSync7, promises as fsp } from "fs";
|
|
5348
4730
|
import * as path2 from "path";
|
|
5349
|
-
|
|
5350
|
-
// src/daemon/skill-sync.ts
|
|
5351
|
-
import { promises as fsp } from "fs";
|
|
5352
|
-
import { join as join10 } from "path";
|
|
5353
|
-
import { createHash as createHash2 } from "crypto";
|
|
5354
|
-
async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, signal) {
|
|
5355
|
-
if (!agentImUserId) return { synced: 0, skipped: 0, unchanged: 0 };
|
|
5356
|
-
const data = await cloud.get(
|
|
5357
|
-
`/api/im/skills/installed?agentId=${encodeURIComponent(agentImUserId)}`,
|
|
5358
|
-
{ signal }
|
|
5359
|
-
);
|
|
5360
|
-
const entries = normalizeInstalledSkills(data);
|
|
5361
|
-
const skillsRoot = resolveSkillsRoot(profile);
|
|
5362
|
-
if (!skillsRoot) return { synced: 0, skipped: 0, unchanged: 0 };
|
|
5363
|
-
let synced = 0;
|
|
5364
|
-
let skipped = 0;
|
|
5365
|
-
let unchanged = 0;
|
|
5366
|
-
for (const entry of entries) {
|
|
5367
|
-
const slug = sanitizeSlug(trimmedStringFrom(entry.skill?.slug) ?? trimmedStringFrom(entry.slug));
|
|
5368
|
-
const content = contentStringFrom(entry.skill?.content) ?? contentStringFrom(entry.content);
|
|
5369
|
-
if (!slug || !content) {
|
|
5370
|
-
skipped++;
|
|
5371
|
-
if (slug && !content) {
|
|
5372
|
-
process.stderr.write(`[daemon] skill sync skipped ${slug}: missing content
|
|
5373
|
-
`);
|
|
5374
|
-
}
|
|
5375
|
-
continue;
|
|
5376
|
-
}
|
|
5377
|
-
const skillDir = join10(skillsRoot, slug);
|
|
5378
|
-
await fsp.mkdir(skillDir, { recursive: true });
|
|
5379
|
-
const target = join10(skillDir, "SKILL.md");
|
|
5380
|
-
const existing = await fsp.readFile(target, "utf8").catch(() => null);
|
|
5381
|
-
if (existing != null && sha2562(existing) === sha2562(content)) {
|
|
5382
|
-
unchanged++;
|
|
5383
|
-
continue;
|
|
5384
|
-
}
|
|
5385
|
-
await fsp.writeFile(target, content, "utf8");
|
|
5386
|
-
synced++;
|
|
5387
|
-
}
|
|
5388
|
-
return { synced, skipped, unchanged };
|
|
5389
|
-
}
|
|
5390
|
-
function resolveSkillsRoot(profile) {
|
|
5391
|
-
if (profile.adapterName === "hermes") return join10(getHermesProfileDir(getHermesProfileName(profile)), "skills");
|
|
5392
|
-
if (profile.adapterName === "openclaw") return getOpenClawSkillsDir(profile);
|
|
5393
|
-
return null;
|
|
5394
|
-
}
|
|
5395
|
-
function sha2562(value) {
|
|
5396
|
-
return createHash2("sha256").update(value).digest("hex");
|
|
5397
|
-
}
|
|
5398
|
-
function normalizeInstalledSkills(data) {
|
|
5399
|
-
if (Array.isArray(data)) return data.filter(isInstalledSkillEntry);
|
|
5400
|
-
if (!data || typeof data !== "object" || Array.isArray(data)) return [];
|
|
5401
|
-
const record = data;
|
|
5402
|
-
const nested = record.data;
|
|
5403
|
-
if (Array.isArray(nested)) return nested.filter(isInstalledSkillEntry);
|
|
5404
|
-
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
5405
|
-
const skills2 = nested.skills;
|
|
5406
|
-
if (Array.isArray(skills2)) return skills2.filter(isInstalledSkillEntry);
|
|
5407
|
-
}
|
|
5408
|
-
const skills = record.skills;
|
|
5409
|
-
if (Array.isArray(skills)) return skills.filter(isInstalledSkillEntry);
|
|
5410
|
-
return [];
|
|
5411
|
-
}
|
|
5412
|
-
function isInstalledSkillEntry(value) {
|
|
5413
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
5414
|
-
}
|
|
5415
|
-
function trimmedStringFrom(value) {
|
|
5416
|
-
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
5417
|
-
}
|
|
5418
|
-
function contentStringFrom(value) {
|
|
5419
|
-
return typeof value === "string" && value.trim() ? value : void 0;
|
|
5420
|
-
}
|
|
5421
|
-
function sanitizeSlug(value) {
|
|
5422
|
-
if (!value) return void 0;
|
|
5423
|
-
const safe = value.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
5424
|
-
return safe || void 0;
|
|
5425
|
-
}
|
|
5426
|
-
|
|
5427
|
-
// src/daemon/dispatch.ts
|
|
5428
4731
|
var DEFAULT_CONTEXT_MAX = 8e3;
|
|
5429
4732
|
var GOAL_CONTEXT_MAX = 4;
|
|
5430
4733
|
var MEMORY_DIGEST_MAX_BYTES = 4e3;
|
|
5431
|
-
var DEFAULT_OPERATING_PRINCIPLES = [
|
|
5432
|
-
"- Decide and act; only request human approval for destructive operations or irreversible external effects.",
|
|
5433
|
-
"- Assignable work should become explicit tasks; do not leave concrete work as chat discussion only.",
|
|
5434
|
-
"- Use workspace asset tools for uploaded files: search/describe first, then read bounded ranges; do not claim file contents unless a tool call succeeded.",
|
|
5435
|
-
"- Keep users informed with concise status and concrete next steps."
|
|
5436
|
-
].join("\n");
|
|
5437
4734
|
var ASSET_INLINE_FULL_MAX_BYTES = 64 * 1024;
|
|
5438
4735
|
var ASSET_INLINE_TRUNCATE_BYTES = 16 * 1024;
|
|
5439
4736
|
async function handleDispatch(payload, requestId, deps) {
|
|
@@ -5444,7 +4741,7 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
5444
4741
|
const outboxDir = outboxBase ? path2.join(outboxBase, "_outbox") : null;
|
|
5445
4742
|
if (outboxDir) {
|
|
5446
4743
|
try {
|
|
5447
|
-
await
|
|
4744
|
+
await fsp.mkdir(outboxDir, { recursive: true });
|
|
5448
4745
|
} catch (err) {
|
|
5449
4746
|
process.stderr.write(
|
|
5450
4747
|
`[daemon] outbox provision failed task=${taskId} dir=${outboxDir}: ${err.message}
|
|
@@ -5471,18 +4768,6 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
5471
4768
|
sendReply(deps.ws, reply, requestId);
|
|
5472
4769
|
return reply;
|
|
5473
4770
|
}
|
|
5474
|
-
try {
|
|
5475
|
-
const skillSync = await syncInstalledSkillsForDispatch(profile, agentImUserId, deps.cloud, deps.signal);
|
|
5476
|
-
if (skillSync.synced > 0 || skillSync.skipped > 0) {
|
|
5477
|
-
process.stderr.write(
|
|
5478
|
-
`[daemon] skill sync profile=${profile.id} adapter=${profile.adapterName} synced=${skillSync.synced} skipped=${skillSync.skipped}
|
|
5479
|
-
`
|
|
5480
|
-
);
|
|
5481
|
-
}
|
|
5482
|
-
} catch (err) {
|
|
5483
|
-
process.stderr.write(`[daemon] skill sync skipped profile=${profile.id}: ${err.message}
|
|
5484
|
-
`);
|
|
5485
|
-
}
|
|
5486
4771
|
let hashRefResult = { text: payload.prompt, resolutions: [] };
|
|
5487
4772
|
if (deps.assetMetadataIndexes && profile.workspaceId) {
|
|
5488
4773
|
const assetIndex = deps.assetMetadataIndexes.get(profile.workspaceId);
|
|
@@ -5532,7 +4817,6 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
5532
4817
|
`
|
|
5533
4818
|
);
|
|
5534
4819
|
}
|
|
5535
|
-
const operatingPrinciples = resolveOperatingPrinciples(profile.config);
|
|
5536
4820
|
const taskInput = {
|
|
5537
4821
|
taskId,
|
|
5538
4822
|
prompt: adapterPrompt,
|
|
@@ -5541,7 +4825,6 @@ async function handleDispatch(payload, requestId, deps) {
|
|
|
5541
4825
|
conversationId: payload.conversationId,
|
|
5542
4826
|
prismerGoals: goalContext.map(toGoalMirrorPayload),
|
|
5543
4827
|
...profileSystemPrompt ? { systemPrompt: profileSystemPrompt } : {},
|
|
5544
|
-
...operatingPrinciples ? { operatingPrinciples } : {},
|
|
5545
4828
|
// Wave-9: spawn-style adapters (claude-code, codex, openclaw) read
|
|
5546
4829
|
// this and inject `PRISMER_OUTBOX_DIR=<abs>` into the child env so
|
|
5547
4830
|
// the LLM tool can resolve the path even if it doesn't read the
|
|
@@ -5812,7 +5095,7 @@ async function resolveAssetRefs(refs, cache) {
|
|
|
5812
5095
|
const inlineCandidate = isTextLikeMime(effectiveMime);
|
|
5813
5096
|
if (inlineCandidate) {
|
|
5814
5097
|
try {
|
|
5815
|
-
const buf =
|
|
5098
|
+
const buf = readFileSync7(cached.localPath);
|
|
5816
5099
|
let strategy;
|
|
5817
5100
|
let body;
|
|
5818
5101
|
if (buf.byteLength <= ASSET_INLINE_FULL_MAX_BYTES) {
|
|
@@ -5886,36 +5169,6 @@ ${memory.digest.trim()}
|
|
|
5886
5169
|
|
|
5887
5170
|
${prompt}`;
|
|
5888
5171
|
}
|
|
5889
|
-
function resolveOperatingPrinciples(config) {
|
|
5890
|
-
const cfg = readRecord(config);
|
|
5891
|
-
const roleTemplate = readRecord(cfg.roleTemplate);
|
|
5892
|
-
const principles = stringifyPrinciples(cfg.operatingPrinciples) ?? stringifyPrinciples(roleTemplate.operatingPrinciples) ?? DEFAULT_OPERATING_PRINCIPLES;
|
|
5893
|
-
const policy = typeof cfg.approvalPolicy === "string" ? cfg.approvalPolicy : roleTemplate.approvalPolicy;
|
|
5894
|
-
if (policy === "strict") {
|
|
5895
|
-
return `${principles}
|
|
5896
|
-
- Approval policy: strict. Request human approval before destructive, external, spend, access, or publish operations.`;
|
|
5897
|
-
}
|
|
5898
|
-
if (policy === "autonomous") {
|
|
5899
|
-
return `${principles}
|
|
5900
|
-
- Approval policy: autonomous. Do not request human approval unless a platform policy explicitly requires it.`;
|
|
5901
|
-
}
|
|
5902
|
-
return `${principles}
|
|
5903
|
-
- Approval policy: auto-low-risk. Proceed on reversible low-risk work; request human approval for destructive or irreversible operations.`;
|
|
5904
|
-
}
|
|
5905
|
-
function stringifyPrinciples(value) {
|
|
5906
|
-
if (typeof value === "string" && value.trim()) return value.trim();
|
|
5907
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
5908
|
-
const record = value;
|
|
5909
|
-
for (const key of ["en", "zh"]) {
|
|
5910
|
-
const text = record[key];
|
|
5911
|
-
if (typeof text === "string" && text.trim()) return text.trim();
|
|
5912
|
-
}
|
|
5913
|
-
for (const text of Object.values(record)) {
|
|
5914
|
-
if (typeof text === "string" && text.trim()) return text.trim();
|
|
5915
|
-
}
|
|
5916
|
-
}
|
|
5917
|
-
return void 0;
|
|
5918
|
-
}
|
|
5919
5172
|
function appendOutboxInstruction(prompt, outboxDir, profileConfig) {
|
|
5920
5173
|
if (!outboxDir) return prompt;
|
|
5921
5174
|
if (profileConfig?.disableOutboxHints === true) return prompt;
|
|
@@ -5934,9 +5187,6 @@ function appendChannelContext(prompt, payload, profile) {
|
|
|
5934
5187
|
const type = payload.conversationType === "direct" || payload.conversationType === "group" ? payload.conversationType : "unknown";
|
|
5935
5188
|
const youAre = profile.agentUsername || payload.agentImUserId || "this agent";
|
|
5936
5189
|
const lines = ["[Channel context]", `Conversation type: ${type}`, `You are: ${youAre}`];
|
|
5937
|
-
if (payload.conversationId) {
|
|
5938
|
-
lines.push(`Conversation ID: ${payload.conversationId}`);
|
|
5939
|
-
}
|
|
5940
5190
|
const participants = Array.isArray(payload.participants) ? payload.participants : [];
|
|
5941
5191
|
if (participants.length > 0) {
|
|
5942
5192
|
if (type === "direct") {
|
|
@@ -6160,7 +5410,7 @@ function updatedTime(task) {
|
|
|
6160
5410
|
|
|
6161
5411
|
// src/daemon/local-server.ts
|
|
6162
5412
|
import { createServer } from "http";
|
|
6163
|
-
import { randomUUID, createHash
|
|
5413
|
+
import { randomUUID, createHash } from "crypto";
|
|
6164
5414
|
import { promises as fs2 } from "fs";
|
|
6165
5415
|
import * as path3 from "path";
|
|
6166
5416
|
var LocalServer = class {
|
|
@@ -6468,10 +5718,10 @@ async function walkAndDigest(root, current) {
|
|
|
6468
5718
|
}
|
|
6469
5719
|
if (!st.isFile()) continue;
|
|
6470
5720
|
const buf = await fs2.readFile(full);
|
|
6471
|
-
const
|
|
5721
|
+
const sha2562 = createHash("sha256").update(buf).digest("hex");
|
|
6472
5722
|
out.push({
|
|
6473
5723
|
path: rel,
|
|
6474
|
-
sha256:
|
|
5724
|
+
sha256: sha2562,
|
|
6475
5725
|
sizeBytes: st.size,
|
|
6476
5726
|
mtime: Math.floor(st.mtimeMs)
|
|
6477
5727
|
});
|
|
@@ -6487,7 +5737,7 @@ import * as path5 from "path";
|
|
|
6487
5737
|
import Database3 from "better-sqlite3";
|
|
6488
5738
|
import * as fs3 from "fs";
|
|
6489
5739
|
import * as path4 from "path";
|
|
6490
|
-
import { randomUUID as randomUUID2, createHash as
|
|
5740
|
+
import { randomUUID as randomUUID2, createHash as createHash2 } from "crypto";
|
|
6491
5741
|
|
|
6492
5742
|
// src/daemon/memory/crypto.ts
|
|
6493
5743
|
function sealPlaintext(content) {
|
|
@@ -6638,9 +5888,9 @@ var MemoryStore = class {
|
|
|
6638
5888
|
const row = db.prepare("SELECT * FROM memory_pages WHERE workspaceId = ? AND id = ?").get(this.opts.workspaceId, pageId);
|
|
6639
5889
|
return row ? this.rowToPage(row) : null;
|
|
6640
5890
|
}
|
|
6641
|
-
loadContent(pageId,
|
|
5891
|
+
loadContent(pageId, version2) {
|
|
6642
5892
|
const db = this.requireDb();
|
|
6643
|
-
const targetVersion =
|
|
5893
|
+
const targetVersion = version2 ?? db.prepare("SELECT version FROM memory_pages WHERE workspaceId = ? AND id = ?").get(this.opts.workspaceId, pageId)?.version;
|
|
6644
5894
|
if (targetVersion === void 0) return null;
|
|
6645
5895
|
const row = db.prepare(
|
|
6646
5896
|
`SELECT c.pageId, c.version, c.payloadKind, c.payloadValue
|
|
@@ -6678,7 +5928,7 @@ var MemoryStore = class {
|
|
|
6678
5928
|
}
|
|
6679
5929
|
const db = this.requireDb();
|
|
6680
5930
|
const now = Date.now();
|
|
6681
|
-
const contentHash =
|
|
5931
|
+
const contentHash = sha256(input.content);
|
|
6682
5932
|
const payload = sealPlaintext(input.content);
|
|
6683
5933
|
if (payload.kind !== "inline") {
|
|
6684
5934
|
throw new Error("MemoryStore.write: non-inline payload not yet supported in phase-0");
|
|
@@ -6872,8 +6122,8 @@ var MemoryStore = class {
|
|
|
6872
6122
|
};
|
|
6873
6123
|
}
|
|
6874
6124
|
};
|
|
6875
|
-
function
|
|
6876
|
-
return
|
|
6125
|
+
function sha256(s) {
|
|
6126
|
+
return createHash2("sha256").update(s, "utf8").digest("hex");
|
|
6877
6127
|
}
|
|
6878
6128
|
|
|
6879
6129
|
// src/daemon/memory/search.ts
|
|
@@ -7731,8 +6981,8 @@ function finalizeSelected(runtime, workspaceId, paths, snippetMaxBytes = DEFAULT
|
|
|
7731
6981
|
const slot = runtime.peek(workspaceId);
|
|
7732
6982
|
if (!slot) return [];
|
|
7733
6983
|
const out = [];
|
|
7734
|
-
for (const
|
|
7735
|
-
const page = slot.store.loadByPath(
|
|
6984
|
+
for (const path7 of paths) {
|
|
6985
|
+
const page = slot.store.loadByPath(path7);
|
|
7736
6986
|
if (!page) continue;
|
|
7737
6987
|
const content = slot.store.loadContent(page.id)?.content ?? "";
|
|
7738
6988
|
out.push({
|
|
@@ -7990,33 +7240,186 @@ function handleRecallFinalize(runtime, body, res) {
|
|
|
7990
7240
|
if (!Array.isArray(b.paths)) {
|
|
7991
7241
|
return respond400(res, "paths[] is required");
|
|
7992
7242
|
}
|
|
7993
|
-
const paths = [];
|
|
7994
|
-
for (const p of b.paths) {
|
|
7995
|
-
if (typeof p === "string" && p.length > 0) paths.push(p);
|
|
7243
|
+
const paths = [];
|
|
7244
|
+
for (const p of b.paths) {
|
|
7245
|
+
if (typeof p === "string" && p.length > 0) paths.push(p);
|
|
7246
|
+
}
|
|
7247
|
+
const snippetMaxBytes = typeof b.snippetMaxBytes === "number" && b.snippetMaxBytes > 0 ? b.snippetMaxBytes : void 0;
|
|
7248
|
+
const results = finalizeSelected(runtime, b.workspaceId, paths, snippetMaxBytes);
|
|
7249
|
+
respond2(res, 200, { workspaceId: b.workspaceId, results });
|
|
7250
|
+
return true;
|
|
7251
|
+
}
|
|
7252
|
+
function parsePrismerUri(uri) {
|
|
7253
|
+
const PREFIX2 = "prismer://workspace/";
|
|
7254
|
+
if (!uri.startsWith(PREFIX2)) return null;
|
|
7255
|
+
const rest = uri.slice(PREFIX2.length);
|
|
7256
|
+
const segMemory = "/memory/";
|
|
7257
|
+
const memoryIdx = rest.indexOf(segMemory);
|
|
7258
|
+
if (memoryIdx <= 0) return null;
|
|
7259
|
+
const workspaceId = rest.slice(0, memoryIdx);
|
|
7260
|
+
const path7 = rest.slice(memoryIdx + segMemory.length);
|
|
7261
|
+
if (!workspaceId || !path7) return null;
|
|
7262
|
+
return { workspaceId, path: path7 };
|
|
7263
|
+
}
|
|
7264
|
+
|
|
7265
|
+
// src/daemon/asset/metadata-index.ts
|
|
7266
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
7267
|
+
import { join as join9 } from "path";
|
|
7268
|
+
var DEFAULT_LIMIT = 8;
|
|
7269
|
+
var PULL_PAGE_SIZE = 500;
|
|
7270
|
+
var THROTTLE_MS = 3e4;
|
|
7271
|
+
function rowToMetadata(row) {
|
|
7272
|
+
return {
|
|
7273
|
+
assetId: row.asset_id,
|
|
7274
|
+
contentHash: row.content_hash,
|
|
7275
|
+
filename: row.filename,
|
|
7276
|
+
folderPath: row.folder_path,
|
|
7277
|
+
mime: row.mime,
|
|
7278
|
+
kind: row.kind,
|
|
7279
|
+
sizeBytes: row.size_bytes,
|
|
7280
|
+
description: row.description,
|
|
7281
|
+
assetIndexSeq: row.asset_index_seq
|
|
7282
|
+
};
|
|
7283
|
+
}
|
|
7284
|
+
var AssetMetadataIndex = class {
|
|
7285
|
+
db;
|
|
7286
|
+
cloud;
|
|
7287
|
+
/** Workspace ID — exposed for prismer:// URI construction. */
|
|
7288
|
+
workspaceId;
|
|
7289
|
+
cursorPath;
|
|
7290
|
+
_lastSyncMs = 0;
|
|
7291
|
+
constructor(opts) {
|
|
7292
|
+
this.db = opts.db;
|
|
7293
|
+
this.cloud = opts.cloud;
|
|
7294
|
+
this.workspaceId = opts.workspaceId;
|
|
7295
|
+
if (!existsSync10(opts.workspaceStateDir)) {
|
|
7296
|
+
mkdirSync7(opts.workspaceStateDir, { recursive: true });
|
|
7297
|
+
}
|
|
7298
|
+
this.cursorPath = join9(opts.workspaceStateDir, "asset-metadata-cursor.json");
|
|
7299
|
+
}
|
|
7300
|
+
/** Persisted cursor for this workspace, or 0 on first run / corrupted file. */
|
|
7301
|
+
readCursor() {
|
|
7302
|
+
if (!existsSync10(this.cursorPath)) return 0;
|
|
7303
|
+
try {
|
|
7304
|
+
const parsed = JSON.parse(readFileSync8(this.cursorPath, "utf8"));
|
|
7305
|
+
if (parsed.workspaceId !== this.workspaceId) return 0;
|
|
7306
|
+
return parsed.cursor;
|
|
7307
|
+
} catch {
|
|
7308
|
+
return 0;
|
|
7309
|
+
}
|
|
7310
|
+
}
|
|
7311
|
+
writeCursor(cursor) {
|
|
7312
|
+
const payload = {
|
|
7313
|
+
workspaceId: this.workspaceId,
|
|
7314
|
+
cursor,
|
|
7315
|
+
writtenAt: Date.now()
|
|
7316
|
+
};
|
|
7317
|
+
writeFileSync7(this.cursorPath, JSON.stringify(payload, null, 2));
|
|
7318
|
+
}
|
|
7319
|
+
/**
|
|
7320
|
+
* Pull incremental asset metadata changes since the persisted cursor and
|
|
7321
|
+
* upsert into the local index. Newer rows overwrite older ones by
|
|
7322
|
+
* (workspace_id, asset_id) primary key.
|
|
7323
|
+
*
|
|
7324
|
+
* Returns the count of items applied + the new cursor. Throttled: if called
|
|
7325
|
+
* within 30s of the last successful pull, returns immediately.
|
|
7326
|
+
*/
|
|
7327
|
+
async pullDelta(opts) {
|
|
7328
|
+
const now = Date.now();
|
|
7329
|
+
if (now - this._lastSyncMs < THROTTLE_MS) {
|
|
7330
|
+
return { applied: 0, cursor: this.readCursor() };
|
|
7331
|
+
}
|
|
7332
|
+
const since = this.readCursor();
|
|
7333
|
+
const sinceParam = since > 0 ? `&since=${since}` : "";
|
|
7334
|
+
const envelope2 = await this.cloud.get(
|
|
7335
|
+
`/api/im/assets/index?workspaceId=${encodeURIComponent(this.workspaceId)}&limit=${PULL_PAGE_SIZE}${sinceParam}`,
|
|
7336
|
+
{ signal: opts?.signal }
|
|
7337
|
+
);
|
|
7338
|
+
const upsert = this.db.prepare(`
|
|
7339
|
+
INSERT INTO asset_metadata_index
|
|
7340
|
+
(workspace_id, asset_id, content_hash, filename, folder_path, mime, kind, size_bytes, description, asset_index_seq, updated_at)
|
|
7341
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
7342
|
+
ON CONFLICT(workspace_id, asset_id) DO UPDATE SET
|
|
7343
|
+
content_hash = excluded.content_hash,
|
|
7344
|
+
filename = excluded.filename,
|
|
7345
|
+
folder_path = excluded.folder_path,
|
|
7346
|
+
mime = excluded.mime,
|
|
7347
|
+
kind = excluded.kind,
|
|
7348
|
+
size_bytes = excluded.size_bytes,
|
|
7349
|
+
description = excluded.description,
|
|
7350
|
+
asset_index_seq = excluded.asset_index_seq,
|
|
7351
|
+
updated_at = excluded.updated_at
|
|
7352
|
+
`);
|
|
7353
|
+
const nowTs = Date.now();
|
|
7354
|
+
let applied = 0;
|
|
7355
|
+
const tx = this.db.transaction((items) => {
|
|
7356
|
+
for (const item of items) {
|
|
7357
|
+
upsert.run(
|
|
7358
|
+
this.workspaceId,
|
|
7359
|
+
item.assetId,
|
|
7360
|
+
item.contentHash,
|
|
7361
|
+
item.filename ?? null,
|
|
7362
|
+
item.folderPath ?? null,
|
|
7363
|
+
item.mime,
|
|
7364
|
+
item.kind,
|
|
7365
|
+
item.sizeBytes,
|
|
7366
|
+
item.description ?? null,
|
|
7367
|
+
item.assetIndexSeq,
|
|
7368
|
+
nowTs
|
|
7369
|
+
);
|
|
7370
|
+
applied += 1;
|
|
7371
|
+
}
|
|
7372
|
+
});
|
|
7373
|
+
try {
|
|
7374
|
+
tx(envelope2.items);
|
|
7375
|
+
this.writeCursor(envelope2.cursor);
|
|
7376
|
+
} catch (err) {
|
|
7377
|
+
throw err;
|
|
7378
|
+
}
|
|
7379
|
+
this._lastSyncMs = now;
|
|
7380
|
+
return { applied, cursor: envelope2.cursor };
|
|
7381
|
+
}
|
|
7382
|
+
/**
|
|
7383
|
+
* Search local index by filename or description substring.
|
|
7384
|
+
* Escapes LIKE wildcards (% and _). Default limit 8.
|
|
7385
|
+
*/
|
|
7386
|
+
search(query, limit) {
|
|
7387
|
+
const escaped = query.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
7388
|
+
const pattern = `%${escaped}%`;
|
|
7389
|
+
const limitVal = Math.min(Math.max(limit ?? DEFAULT_LIMIT, 1), 200);
|
|
7390
|
+
const rows = this.db.prepare(
|
|
7391
|
+
`SELECT * FROM asset_metadata_index
|
|
7392
|
+
WHERE workspace_id = ?
|
|
7393
|
+
AND (filename LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\')
|
|
7394
|
+
ORDER BY asset_index_seq DESC
|
|
7395
|
+
LIMIT ?`
|
|
7396
|
+
).all(this.workspaceId, pattern, pattern, limitVal);
|
|
7397
|
+
return rows.map(rowToMetadata);
|
|
7398
|
+
}
|
|
7399
|
+
/** Exact match on filename column. Returns undefined if not indexed. */
|
|
7400
|
+
resolveByFilename(filename) {
|
|
7401
|
+
const row = this.db.prepare("SELECT * FROM asset_metadata_index WHERE workspace_id = ? AND filename = ?").get(this.workspaceId, filename);
|
|
7402
|
+
return row ? rowToMetadata(row) : void 0;
|
|
7403
|
+
}
|
|
7404
|
+
/** Batch exact match — returns Map for O(1) access. */
|
|
7405
|
+
resolveByFilenames(filenames) {
|
|
7406
|
+
if (filenames.length === 0) return /* @__PURE__ */ new Map();
|
|
7407
|
+
const placeholders = filenames.map(() => "?").join(",");
|
|
7408
|
+
const params = [this.workspaceId, ...filenames];
|
|
7409
|
+
const rows = this.db.prepare(
|
|
7410
|
+
`SELECT * FROM asset_metadata_index
|
|
7411
|
+
WHERE workspace_id = ? AND filename IN (${placeholders})`
|
|
7412
|
+
).all(...params);
|
|
7413
|
+
const map = /* @__PURE__ */ new Map();
|
|
7414
|
+
for (const row of rows) {
|
|
7415
|
+
if (row.filename) map.set(row.filename, rowToMetadata(row));
|
|
7416
|
+
}
|
|
7417
|
+
return map;
|
|
7996
7418
|
}
|
|
7997
|
-
|
|
7998
|
-
const results = finalizeSelected(runtime, b.workspaceId, paths, snippetMaxBytes);
|
|
7999
|
-
respond2(res, 200, { workspaceId: b.workspaceId, results });
|
|
8000
|
-
return true;
|
|
8001
|
-
}
|
|
8002
|
-
function parsePrismerUri(uri) {
|
|
8003
|
-
const PREFIX2 = "prismer://workspace/";
|
|
8004
|
-
if (!uri.startsWith(PREFIX2)) return null;
|
|
8005
|
-
const rest = uri.slice(PREFIX2.length);
|
|
8006
|
-
const segMemory = "/memory/";
|
|
8007
|
-
const memoryIdx = rest.indexOf(segMemory);
|
|
8008
|
-
if (memoryIdx <= 0) return null;
|
|
8009
|
-
const workspaceId = rest.slice(0, memoryIdx);
|
|
8010
|
-
const path9 = rest.slice(memoryIdx + segMemory.length);
|
|
8011
|
-
if (!workspaceId || !path9) return null;
|
|
8012
|
-
return { workspaceId, path: path9 };
|
|
8013
|
-
}
|
|
7419
|
+
};
|
|
8014
7420
|
|
|
8015
7421
|
// src/daemon/asset/rpc.ts
|
|
8016
|
-
import { closeSync, openSync, readSync, statSync as statSync3 } from "fs";
|
|
8017
7422
|
var ASSET_PATH_PREFIX = "/local/asset/";
|
|
8018
|
-
var MAX_READ_BYTES = 256 * 1024;
|
|
8019
|
-
var MAX_SEARCH_BYTES = 1024 * 1024;
|
|
8020
7423
|
function attachAssetRpc(opts) {
|
|
8021
7424
|
return async (req, res) => {
|
|
8022
7425
|
const url = req.url ?? "/";
|
|
@@ -8027,17 +7430,10 @@ function attachAssetRpc(opts) {
|
|
|
8027
7430
|
try {
|
|
8028
7431
|
if (method === "POST" && subpath === "search") {
|
|
8029
7432
|
const body = await readJson3(req);
|
|
8030
|
-
return
|
|
8031
|
-
}
|
|
8032
|
-
if (method === "POST" && subpath === "describe") {
|
|
8033
|
-
const body = await readJson3(req);
|
|
8034
|
-
return handleDescribe(opts, body, res);
|
|
8035
|
-
}
|
|
8036
|
-
if (method === "POST" && subpath === "read") {
|
|
8037
|
-
const body = await readJson3(req);
|
|
8038
|
-
return await handleRead(opts, body, res);
|
|
7433
|
+
return handleSearch2(opts.resolveIndex, body, res);
|
|
8039
7434
|
}
|
|
8040
|
-
|
|
7435
|
+
respond3(res, 404, { error: "asset_route_not_found", path: url });
|
|
7436
|
+
return true;
|
|
8041
7437
|
} catch (err) {
|
|
8042
7438
|
respond3(res, 500, {
|
|
8043
7439
|
error: "asset_rpc_failed",
|
|
@@ -8047,699 +7443,55 @@ function attachAssetRpc(opts) {
|
|
|
8047
7443
|
}
|
|
8048
7444
|
};
|
|
8049
7445
|
}
|
|
8050
|
-
|
|
7446
|
+
function handleSearch2(resolveIndex, body, res) {
|
|
8051
7447
|
if (!body || typeof body !== "object") {
|
|
8052
7448
|
return respond4002(res, "request body must be a JSON object");
|
|
8053
7449
|
}
|
|
8054
7450
|
const b = body;
|
|
8055
|
-
if (typeof b.query !== "string" || !b.query.trim()) {
|
|
8056
|
-
return respond4002(res, "query is required (non-empty string)");
|
|
8057
|
-
}
|
|
8058
|
-
if (typeof b.uri === "string" && b.uri.trim()) {
|
|
8059
|
-
return searchAssetContent(opts, b.uri.trim(), b.query.trim(), res);
|
|
8060
|
-
}
|
|
8061
7451
|
if (typeof b.workspaceId !== "string" || !b.workspaceId) {
|
|
8062
7452
|
return respond4002(res, "workspaceId is required (string)");
|
|
8063
7453
|
}
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
});
|
|
8088
|
-
return true;
|
|
8089
|
-
}
|
|
8090
|
-
async function
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
return true;
|
|
8096
|
-
}
|
|
8097
|
-
const b = body;
|
|
8098
|
-
const offset = typeof b.offset === "number" && b.offset > 0 ? Math.floor(b.offset) : 0;
|
|
8099
|
-
const requested = typeof b.length === "number" && b.length > 0 ? Math.floor(b.length) : MAX_READ_BYTES;
|
|
8100
|
-
const length = Math.min(requested, MAX_READ_BYTES);
|
|
8101
|
-
const cached = await opts.assetCache.getOrFetch(resolved.row.contentHash, {
|
|
8102
|
-
workspaceIdHint: resolved.workspaceId,
|
|
8103
|
-
assetId: resolved.row.assetId
|
|
8104
|
-
});
|
|
8105
|
-
const chunk = readFileRange(cached.localPath, offset, length);
|
|
8106
|
-
const textLike = isTextLike(resolved.row);
|
|
8107
|
-
respond3(res, 200, {
|
|
8108
|
-
asset: resolved.row,
|
|
8109
|
-
locator: { byteOffset: offset, byteLength: chunk.bytesRead },
|
|
8110
|
-
truncated: requested > MAX_READ_BYTES || offset + chunk.bytesRead < chunk.sizeBytes,
|
|
8111
|
-
encoding: textLike ? "utf8" : "base64",
|
|
8112
|
-
content: textLike ? chunk.buffer.toString("utf8") : chunk.buffer.toString("base64")
|
|
8113
|
-
});
|
|
8114
|
-
return true;
|
|
8115
|
-
}
|
|
8116
|
-
async function searchAssetContent(opts, uri, query, res) {
|
|
8117
|
-
const resolved = resolveAssetRef(opts, { uri });
|
|
8118
|
-
if ("error" in resolved) return respond4002(res, resolved.error);
|
|
8119
|
-
if (!opts.assetCache) {
|
|
8120
|
-
respond3(res, 503, { error: "asset_cache_unavailable" });
|
|
8121
|
-
return true;
|
|
8122
|
-
}
|
|
8123
|
-
if (!isTextLike(resolved.row)) {
|
|
8124
|
-
respond3(res, 200, {
|
|
8125
|
-
items: [],
|
|
8126
|
-
unsupported: true,
|
|
8127
|
-
reason: "content search currently supports text-like assets only",
|
|
8128
|
-
asset: resolved.row
|
|
8129
|
-
});
|
|
8130
|
-
return true;
|
|
8131
|
-
}
|
|
8132
|
-
const cached = await opts.assetCache.getOrFetch(resolved.row.contentHash, {
|
|
8133
|
-
workspaceIdHint: resolved.workspaceId,
|
|
8134
|
-
assetId: resolved.row.assetId
|
|
8135
|
-
});
|
|
8136
|
-
const chunk = readFileRange(cached.localPath, 0, MAX_SEARCH_BYTES);
|
|
8137
|
-
const text = chunk.buffer.toString("utf8");
|
|
8138
|
-
const needle = query.toLowerCase();
|
|
8139
|
-
const lines = text.split(/\r?\n/);
|
|
8140
|
-
const items = [];
|
|
8141
|
-
let byteOffset = 0;
|
|
8142
|
-
for (let i = 0; i < lines.length && items.length < 20; i += 1) {
|
|
8143
|
-
const line = lines[i] ?? "";
|
|
8144
|
-
if (line.toLowerCase().includes(needle)) {
|
|
8145
|
-
items.push({ line: i + 1, byteOffset, preview: line.slice(0, 500) });
|
|
8146
|
-
}
|
|
8147
|
-
byteOffset += Buffer.byteLength(line) + 1;
|
|
8148
|
-
}
|
|
8149
|
-
respond3(res, 200, {
|
|
8150
|
-
items,
|
|
8151
|
-
asset: resolved.row,
|
|
8152
|
-
searched: { byteOffset: 0, byteLength: chunk.bytesRead, truncated: chunk.bytesRead < chunk.sizeBytes }
|
|
8153
|
-
});
|
|
8154
|
-
return true;
|
|
8155
|
-
}
|
|
8156
|
-
function resolveAssetRef(opts, body) {
|
|
8157
|
-
if (!body || typeof body !== "object") return { error: "request body must be a JSON object" };
|
|
8158
|
-
const b = body;
|
|
8159
|
-
let workspaceId = typeof b.workspaceId === "string" ? b.workspaceId : "";
|
|
8160
|
-
let assetId = typeof b.assetId === "string" ? b.assetId : "";
|
|
8161
|
-
let contentHash = typeof b.contentHash === "string" ? b.contentHash : "";
|
|
8162
|
-
if (typeof b.uri === "string" && b.uri.trim()) {
|
|
8163
|
-
const [uri] = parseUris(b.uri.trim());
|
|
8164
|
-
if (!uri) return { error: "uri must be prismer://workspace/<workspaceId>/asset/<hash>" };
|
|
8165
|
-
if (uri.type !== "asset") return { error: "file uri lookup is not available in this RPC yet; pass asset uri or assetId" };
|
|
8166
|
-
workspaceId = uri.workspaceId ?? workspaceId;
|
|
8167
|
-
contentHash = uri.rest;
|
|
8168
|
-
}
|
|
8169
|
-
if (!workspaceId) return { error: "workspaceId is required (string)" };
|
|
8170
|
-
const index = opts.resolveIndex(workspaceId);
|
|
8171
|
-
if (!index) return { error: "workspace asset index is not synced yet" };
|
|
8172
|
-
const row = assetId ? index.resolveByAssetId(assetId) : contentHash ? index.resolveByContentHash(contentHash) : void 0;
|
|
8173
|
-
if (!row) return { error: "asset not found in local metadata index" };
|
|
8174
|
-
return { workspaceId, row };
|
|
8175
|
-
}
|
|
8176
|
-
function isTextLike(row) {
|
|
8177
|
-
const mime = row.mime ?? "";
|
|
8178
|
-
const name = row.filename ?? "";
|
|
8179
|
-
return mime.startsWith("text/") || mime.includes("json") || mime.includes("xml") || mime.includes("markdown") || mime.includes("csv") || /\.(md|markdown|txt|json|csv|tsv|ndjson|js|jsx|ts|tsx|py|go|rs|java|kt|swift|rb|php|css|html|htm|xhtml|sql|yaml|yml|toml|log)$/i.test(
|
|
8180
|
-
name
|
|
8181
|
-
);
|
|
8182
|
-
}
|
|
8183
|
-
function readFileRange(path9, offset, length) {
|
|
8184
|
-
const sizeBytes = statSync3(path9).size;
|
|
8185
|
-
if (offset >= sizeBytes) return { buffer: Buffer.alloc(0), bytesRead: 0, sizeBytes };
|
|
8186
|
-
const fd = openSync(path9, "r");
|
|
8187
|
-
try {
|
|
8188
|
-
const target = Math.min(length, sizeBytes - offset);
|
|
8189
|
-
const buffer = Buffer.alloc(target);
|
|
8190
|
-
const bytesRead = readSync(fd, buffer, 0, target, offset);
|
|
8191
|
-
return { buffer: buffer.subarray(0, bytesRead), bytesRead, sizeBytes };
|
|
8192
|
-
} finally {
|
|
8193
|
-
closeSync(fd);
|
|
8194
|
-
}
|
|
8195
|
-
}
|
|
8196
|
-
function respond3(res, status, body) {
|
|
8197
|
-
res.statusCode = status;
|
|
8198
|
-
res.setHeader("Content-Type", "application/json");
|
|
8199
|
-
res.end(JSON.stringify(body));
|
|
8200
|
-
}
|
|
8201
|
-
function respond4002(res, message) {
|
|
8202
|
-
respond3(res, 400, { error: "invalid_request", message });
|
|
8203
|
-
return true;
|
|
8204
|
-
}
|
|
8205
|
-
async function readJson3(req) {
|
|
8206
|
-
let raw = "";
|
|
8207
|
-
req.setEncoding("utf8");
|
|
8208
|
-
for await (const chunk of req) raw += chunk;
|
|
8209
|
-
if (!raw) return {};
|
|
8210
|
-
try {
|
|
8211
|
-
return JSON.parse(raw);
|
|
8212
|
-
} catch {
|
|
8213
|
-
throw new Error("invalid_json");
|
|
8214
|
-
}
|
|
8215
|
-
}
|
|
8216
|
-
|
|
8217
|
-
// src/daemon/asset/origin/drop-folder.ts
|
|
8218
|
-
import { promises as fs4 } from "fs";
|
|
8219
|
-
import { homedir as homedir6 } from "os";
|
|
8220
|
-
import * as path6 from "path";
|
|
8221
|
-
var DropFolderAdapter = class {
|
|
8222
|
-
kind = "drop-folder";
|
|
8223
|
-
workspaceDir;
|
|
8224
|
-
rootDir;
|
|
8225
|
-
constructor(opts = {}) {
|
|
8226
|
-
this.workspaceDir = opts.workspaceDir;
|
|
8227
|
-
this.rootDir = opts.rootDir ?? path6.join(opts.homeDir ?? homedir6(), ".prismer");
|
|
8228
|
-
}
|
|
8229
|
-
/**
|
|
8230
|
-
* Long-lived async iterable wrapping `scanOnce` on a 1s polling interval.
|
|
8231
|
-
* Stops when the iterator is broken (e.g. consumer returns / throws).
|
|
8232
|
-
*/
|
|
8233
|
-
async *observe(workspaceId) {
|
|
8234
|
-
while (true) {
|
|
8235
|
-
const batch = await this.scanOnce(workspaceId);
|
|
8236
|
-
for (const obs of batch) yield obs;
|
|
8237
|
-
await sleep(1e3);
|
|
8238
|
-
}
|
|
8239
|
-
}
|
|
8240
|
-
/**
|
|
8241
|
-
* Single-tick scan exposed for tests and one-shot CLI usage. Reads every
|
|
8242
|
-
* file currently under the workspace's drop directory, returning one
|
|
8243
|
-
* observation per file. The caller is expected to feed each into the
|
|
8244
|
-
* outbox; idempotency on the outbox side collapses re-observations of
|
|
8245
|
-
* unchanged files.
|
|
8246
|
-
*/
|
|
8247
|
-
async scanOnce(workspaceId) {
|
|
8248
|
-
const drop = this.dropDir(workspaceId);
|
|
8249
|
-
let entries = [];
|
|
8250
|
-
try {
|
|
8251
|
-
entries = await fs4.readdir(drop);
|
|
8252
|
-
} catch (err) {
|
|
8253
|
-
if (err.code === "ENOENT") return [];
|
|
8254
|
-
throw err;
|
|
8255
|
-
}
|
|
8256
|
-
const out = [];
|
|
8257
|
-
for (const name of entries) {
|
|
8258
|
-
if (name.startsWith(".")) continue;
|
|
8259
|
-
const fullPath = path6.join(drop, name);
|
|
8260
|
-
const stat = await fs4.lstat(fullPath).catch(() => null);
|
|
8261
|
-
if (!stat) continue;
|
|
8262
|
-
if (stat.isSymbolicLink()) continue;
|
|
8263
|
-
if (stat.isDirectory()) {
|
|
8264
|
-
for (const nested of await walk(fullPath)) {
|
|
8265
|
-
const nstat = await fs4.lstat(nested).catch(() => null);
|
|
8266
|
-
if (!nstat || !nstat.isFile()) continue;
|
|
8267
|
-
out.push(makeObservation(workspaceId, nested, drop, nstat.size, Math.floor(nstat.mtimeMs)));
|
|
8268
|
-
}
|
|
8269
|
-
continue;
|
|
8270
|
-
}
|
|
8271
|
-
if (!stat.isFile()) continue;
|
|
8272
|
-
out.push(makeObservation(workspaceId, fullPath, drop, stat.size, Math.floor(stat.mtimeMs)));
|
|
8273
|
-
}
|
|
8274
|
-
return out;
|
|
8275
|
-
}
|
|
8276
|
-
async identifySource(obs) {
|
|
8277
|
-
const detail = obs.detail;
|
|
8278
|
-
const rel = path6.relative(detail.watchDir, detail.path);
|
|
8279
|
-
const folderDir = path6.dirname(rel);
|
|
8280
|
-
return {
|
|
8281
|
-
sourceRef: `folder://${obs.workspaceId}/${rel.split(path6.sep).join("/")}`,
|
|
8282
|
-
hints: {
|
|
8283
|
-
filename: path6.basename(detail.path),
|
|
8284
|
-
folderPath: folderDir === "." ? "" : folderDir.split(path6.sep).join("/")
|
|
8285
|
-
}
|
|
8286
|
-
};
|
|
8287
|
-
}
|
|
8288
|
-
async fetch(obs) {
|
|
8289
|
-
const detail = obs.detail;
|
|
8290
|
-
const bytes = await fs4.readFile(detail.path);
|
|
8291
|
-
return {
|
|
8292
|
-
bytes,
|
|
8293
|
-
mime: mimeFromExtension(detail.path),
|
|
8294
|
-
size: bytes.length
|
|
8295
|
-
};
|
|
8296
|
-
}
|
|
8297
|
-
/** Hook called by UploadRunner after a successful upload of one observation. */
|
|
8298
|
-
async onUploadSuccess(obs) {
|
|
8299
|
-
await this.moveTo(obs, this.uploadedDir(obs.workspaceId));
|
|
8300
|
-
}
|
|
8301
|
-
/** Hook called by UploadRunner after attempts hit maxAttempts. */
|
|
8302
|
-
async onUploadFailure(obs) {
|
|
8303
|
-
await this.moveTo(obs, this.failedDir(obs.workspaceId));
|
|
8304
|
-
}
|
|
8305
|
-
async moveTo(obs, destDir) {
|
|
8306
|
-
const detail = obs.detail;
|
|
8307
|
-
if (!detail || typeof detail.path !== "string") return;
|
|
8308
|
-
try {
|
|
8309
|
-
const rel = typeof detail.watchDir === "string" ? path6.relative(detail.watchDir, detail.path) : path6.basename(detail.path);
|
|
8310
|
-
const safeRel = rel && !rel.startsWith("..") && !path6.isAbsolute(rel) ? rel : path6.basename(detail.path);
|
|
8311
|
-
const destPath = path6.join(destDir, safeRel);
|
|
8312
|
-
await fs4.mkdir(path6.dirname(destPath), { recursive: true });
|
|
8313
|
-
await fs4.rename(detail.path, destPath);
|
|
8314
|
-
} catch (err) {
|
|
8315
|
-
if (err.code === "ENOENT") return;
|
|
8316
|
-
throw err;
|
|
8317
|
-
}
|
|
8318
|
-
}
|
|
8319
|
-
dropDir(workspaceId) {
|
|
8320
|
-
if (this.workspaceDir) return path6.join(this.workspaceDir, "drop");
|
|
8321
|
-
return path6.join(this.rootDir, "workspaces", workspaceId, "drop");
|
|
8322
|
-
}
|
|
8323
|
-
uploadedDir(workspaceId) {
|
|
8324
|
-
if (this.workspaceDir) return path6.join(this.workspaceDir, "uploaded");
|
|
8325
|
-
return path6.join(this.rootDir, "workspaces", workspaceId, "uploaded");
|
|
8326
|
-
}
|
|
8327
|
-
failedDir(workspaceId) {
|
|
8328
|
-
if (this.workspaceDir) return path6.join(this.workspaceDir, "upload-failed");
|
|
8329
|
-
return path6.join(this.rootDir, "workspaces", workspaceId, "upload-failed");
|
|
8330
|
-
}
|
|
8331
|
-
};
|
|
8332
|
-
function makeObservation(workspaceId, filePath, watchDir, size, mtime) {
|
|
8333
|
-
return {
|
|
8334
|
-
workspaceId,
|
|
8335
|
-
detail: { path: filePath, watchDir, size, mtime },
|
|
8336
|
-
observedAt: mtime
|
|
8337
|
-
};
|
|
8338
|
-
}
|
|
8339
|
-
async function walk(root) {
|
|
8340
|
-
const out = [];
|
|
8341
|
-
const entries = await fs4.readdir(root).catch(() => []);
|
|
8342
|
-
for (const name of entries) {
|
|
8343
|
-
if (name.startsWith(".")) continue;
|
|
8344
|
-
const full = path6.join(root, name);
|
|
8345
|
-
const stat = await fs4.lstat(full).catch(() => null);
|
|
8346
|
-
if (!stat) continue;
|
|
8347
|
-
if (stat.isSymbolicLink()) continue;
|
|
8348
|
-
if (stat.isDirectory()) {
|
|
8349
|
-
out.push(...await walk(full));
|
|
8350
|
-
} else if (stat.isFile()) {
|
|
8351
|
-
out.push(full);
|
|
8352
|
-
}
|
|
8353
|
-
}
|
|
8354
|
-
return out;
|
|
8355
|
-
}
|
|
8356
|
-
var MIME_BY_EXT = {
|
|
8357
|
-
".txt": "text/plain",
|
|
8358
|
-
".md": "text/markdown",
|
|
8359
|
-
".csv": "text/csv",
|
|
8360
|
-
".json": "application/json",
|
|
8361
|
-
".yaml": "application/yaml",
|
|
8362
|
-
".yml": "application/yaml",
|
|
8363
|
-
".pdf": "application/pdf",
|
|
8364
|
-
".png": "image/png",
|
|
8365
|
-
".jpg": "image/jpeg",
|
|
8366
|
-
".jpeg": "image/jpeg",
|
|
8367
|
-
".gif": "image/gif",
|
|
8368
|
-
".webp": "image/webp",
|
|
8369
|
-
".svg": "image/svg+xml",
|
|
8370
|
-
".html": "text/html",
|
|
8371
|
-
".htm": "text/html",
|
|
8372
|
-
".xhtml": "application/xhtml+xml",
|
|
8373
|
-
".xml": "application/xml",
|
|
8374
|
-
".zip": "application/zip",
|
|
8375
|
-
".tar": "application/x-tar",
|
|
8376
|
-
".gz": "application/gzip",
|
|
8377
|
-
".mp3": "audio/mpeg",
|
|
8378
|
-
".mp4": "video/mp4",
|
|
8379
|
-
".mov": "video/quicktime",
|
|
8380
|
-
".wav": "audio/wav"
|
|
8381
|
-
};
|
|
8382
|
-
function mimeFromExtension(p) {
|
|
8383
|
-
const ext = path6.extname(p).toLowerCase();
|
|
8384
|
-
return MIME_BY_EXT[ext] ?? null;
|
|
8385
|
-
}
|
|
8386
|
-
function sleep(ms) {
|
|
8387
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
8388
|
-
}
|
|
8389
|
-
|
|
8390
|
-
// src/daemon/asset/origin/outbox.ts
|
|
8391
|
-
import Database4 from "better-sqlite3";
|
|
8392
|
-
import { createHash as createHash5, randomUUID as randomUUID5 } from "crypto";
|
|
8393
|
-
import { z as z7 } from "zod";
|
|
8394
|
-
var OriginObservationRecordSchema = z7.object({
|
|
8395
|
-
workspaceId: z7.string().min(1),
|
|
8396
|
-
originKind: z7.enum(["upload", "drop-folder", "agent-gen"]),
|
|
8397
|
-
sourceRef: z7.string().min(1),
|
|
8398
|
-
payloadJson: z7.string().min(2),
|
|
8399
|
-
hintsJson: z7.string().min(2),
|
|
8400
|
-
observedAt: z7.number().int().nonnegative()
|
|
8401
|
-
});
|
|
8402
|
-
var DDL_STATEMENTS = [
|
|
8403
|
-
`CREATE TABLE IF NOT EXISTS asset_origin_outbox (
|
|
8404
|
-
id TEXT PRIMARY KEY,
|
|
8405
|
-
workspaceId TEXT NOT NULL,
|
|
8406
|
-
originKind TEXT NOT NULL,
|
|
8407
|
-
sourceRef TEXT NOT NULL,
|
|
8408
|
-
payloadJson TEXT NOT NULL,
|
|
8409
|
-
hintsJson TEXT NOT NULL,
|
|
8410
|
-
observedAt INTEGER NOT NULL,
|
|
8411
|
-
idempotencyKey TEXT NOT NULL UNIQUE,
|
|
8412
|
-
status TEXT NOT NULL CHECK (status IN ('pending', 'claimed', 'uploaded')),
|
|
8413
|
-
attempts INTEGER NOT NULL DEFAULT 0,
|
|
8414
|
-
lastError TEXT,
|
|
8415
|
-
assetId TEXT,
|
|
8416
|
-
contentHash TEXT,
|
|
8417
|
-
createdAt INTEGER NOT NULL,
|
|
8418
|
-
updatedAt INTEGER NOT NULL
|
|
8419
|
-
)`,
|
|
8420
|
-
`CREATE INDEX IF NOT EXISTS idx_asset_origin_outbox_status
|
|
8421
|
-
ON asset_origin_outbox(status, createdAt)`,
|
|
8422
|
-
`CREATE TABLE IF NOT EXISTS asset_origin_outbox_dead_letter (
|
|
8423
|
-
id TEXT PRIMARY KEY,
|
|
8424
|
-
workspaceId TEXT NOT NULL,
|
|
8425
|
-
originKind TEXT NOT NULL,
|
|
8426
|
-
sourceRef TEXT NOT NULL,
|
|
8427
|
-
payloadJson TEXT NOT NULL,
|
|
8428
|
-
hintsJson TEXT NOT NULL,
|
|
8429
|
-
observedAt INTEGER NOT NULL,
|
|
8430
|
-
errorJson TEXT NOT NULL,
|
|
8431
|
-
attempts INTEGER NOT NULL,
|
|
8432
|
-
createdAt INTEGER NOT NULL
|
|
8433
|
-
)`
|
|
8434
|
-
];
|
|
8435
|
-
var OriginOutbox = class {
|
|
8436
|
-
db;
|
|
8437
|
-
constructor(opts) {
|
|
8438
|
-
this.db = new Database4(opts.dbPath);
|
|
8439
|
-
this.db.pragma("journal_mode = WAL");
|
|
8440
|
-
for (const ddl of DDL_STATEMENTS) this.db.prepare(ddl).run();
|
|
8441
|
-
}
|
|
8442
|
-
/**
|
|
8443
|
-
* Validate + persist an observation. Schema-invalid input lands in
|
|
8444
|
-
* `asset_origin_outbox_dead_letter` (never silently dropped). Re-enqueue
|
|
8445
|
-
* with the same {originKind, workspaceId, sourceRef, observedAt} returns
|
|
8446
|
-
* the existing row id without inserting again.
|
|
8447
|
-
*/
|
|
8448
|
-
enqueue(record) {
|
|
8449
|
-
const now = Date.now();
|
|
8450
|
-
const parsed = OriginObservationRecordSchema.safeParse(record);
|
|
8451
|
-
if (!parsed.success) {
|
|
8452
|
-
const dlId = `dl_${randomUUID5()}`;
|
|
8453
|
-
const raw = record && typeof record === "object" ? record : {};
|
|
8454
|
-
this.db.prepare(
|
|
8455
|
-
`INSERT INTO asset_origin_outbox_dead_letter
|
|
8456
|
-
(id, workspaceId, originKind, sourceRef, payloadJson, hintsJson, observedAt, errorJson, attempts, createdAt)
|
|
8457
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
8458
|
-
).run(
|
|
8459
|
-
dlId,
|
|
8460
|
-
coerceToString(raw.workspaceId, ""),
|
|
8461
|
-
coerceToString(raw.originKind, ""),
|
|
8462
|
-
coerceToString(raw.sourceRef, ""),
|
|
8463
|
-
coerceToJson(raw.payloadJson),
|
|
8464
|
-
coerceToJson(raw.hintsJson),
|
|
8465
|
-
typeof raw.observedAt === "number" ? raw.observedAt : 0,
|
|
8466
|
-
JSON.stringify(parsed.error.errors),
|
|
8467
|
-
0,
|
|
8468
|
-
now
|
|
8469
|
-
);
|
|
8470
|
-
return { id: dlId, deadLetter: true };
|
|
8471
|
-
}
|
|
8472
|
-
const validated = parsed.data;
|
|
8473
|
-
const idempotencyKey = computeIdempotencyKey(validated);
|
|
8474
|
-
const rowId = `out_${randomUUID5()}`;
|
|
8475
|
-
try {
|
|
8476
|
-
this.db.prepare(
|
|
8477
|
-
`INSERT INTO asset_origin_outbox
|
|
8478
|
-
(id, workspaceId, originKind, sourceRef, payloadJson, hintsJson, observedAt,
|
|
8479
|
-
idempotencyKey, status, attempts, createdAt, updatedAt)
|
|
8480
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, ?)`
|
|
8481
|
-
).run(
|
|
8482
|
-
rowId,
|
|
8483
|
-
validated.workspaceId,
|
|
8484
|
-
validated.originKind,
|
|
8485
|
-
validated.sourceRef,
|
|
8486
|
-
validated.payloadJson,
|
|
8487
|
-
validated.hintsJson,
|
|
8488
|
-
validated.observedAt,
|
|
8489
|
-
idempotencyKey,
|
|
8490
|
-
now,
|
|
8491
|
-
now
|
|
8492
|
-
);
|
|
8493
|
-
return { id: rowId, deadLetter: false };
|
|
8494
|
-
} catch (err) {
|
|
8495
|
-
const code = err.code;
|
|
8496
|
-
if (code === "SQLITE_CONSTRAINT_UNIQUE") {
|
|
8497
|
-
const existing = this.db.prepare("SELECT id FROM asset_origin_outbox WHERE idempotencyKey = ?").get(idempotencyKey);
|
|
8498
|
-
if (existing) return { id: existing.id, deadLetter: false };
|
|
8499
|
-
}
|
|
8500
|
-
throw err;
|
|
8501
|
-
}
|
|
8502
|
-
}
|
|
8503
|
-
/**
|
|
8504
|
-
* Atomically claim the oldest pending row, transition status to 'claimed',
|
|
8505
|
-
* and return it. Returns null when the queue is empty. The caller MUST
|
|
8506
|
-
* follow up with `markUploaded` (success) or `recordFailure` (error) so
|
|
8507
|
-
* the row doesn't sit in 'claimed' forever — Phase-0 is a single worker
|
|
8508
|
-
* so we don't need a claim-expiry sweeper yet.
|
|
8509
|
-
*/
|
|
8510
|
-
claimNext() {
|
|
8511
|
-
const pickAndClaim = this.db.transaction(() => {
|
|
8512
|
-
const row = this.db.prepare(
|
|
8513
|
-
`SELECT id, workspaceId, originKind, sourceRef, payloadJson, hintsJson,
|
|
8514
|
-
observedAt, attempts, lastError, createdAt
|
|
8515
|
-
FROM asset_origin_outbox
|
|
8516
|
-
WHERE status = 'pending'
|
|
8517
|
-
ORDER BY createdAt ASC
|
|
8518
|
-
LIMIT 1`
|
|
8519
|
-
).get();
|
|
8520
|
-
if (!row) return null;
|
|
8521
|
-
this.db.prepare("UPDATE asset_origin_outbox SET status = 'claimed', updatedAt = ? WHERE id = ?").run(Date.now(), row.id);
|
|
8522
|
-
return row;
|
|
8523
|
-
});
|
|
8524
|
-
const claimed = pickAndClaim();
|
|
8525
|
-
if (!claimed) return null;
|
|
8526
|
-
return {
|
|
8527
|
-
id: claimed.id,
|
|
8528
|
-
row: rawToRow(claimed)
|
|
8529
|
-
};
|
|
8530
|
-
}
|
|
8531
|
-
markUploaded(id, meta) {
|
|
8532
|
-
this.db.prepare(
|
|
8533
|
-
`UPDATE asset_origin_outbox
|
|
8534
|
-
SET status = 'uploaded', assetId = ?, contentHash = ?, updatedAt = ?
|
|
8535
|
-
WHERE id = ?`
|
|
8536
|
-
).run(meta.assetId, meta.contentHash, Date.now(), id);
|
|
8537
|
-
}
|
|
8538
|
-
/**
|
|
8539
|
-
* Record one failed upload attempt. Increments `attempts`, stores the
|
|
8540
|
-
* latest error, and either re-queues for retry (status -> 'pending') or
|
|
8541
|
-
* spills to dead-letter once attempts hit maxAttempts.
|
|
8542
|
-
*/
|
|
8543
|
-
recordFailure(id, error, opts) {
|
|
8544
|
-
const now = Date.now();
|
|
8545
|
-
const updated = this.db.transaction(() => {
|
|
8546
|
-
const row = this.db.prepare(
|
|
8547
|
-
`SELECT id, workspaceId, originKind, sourceRef, payloadJson, hintsJson,
|
|
8548
|
-
observedAt, attempts, createdAt
|
|
8549
|
-
FROM asset_origin_outbox
|
|
8550
|
-
WHERE id = ?`
|
|
8551
|
-
).get(id);
|
|
8552
|
-
if (!row) return;
|
|
8553
|
-
const nextAttempts = row.attempts + 1;
|
|
8554
|
-
if (nextAttempts >= opts.maxAttempts) {
|
|
8555
|
-
this.db.prepare(
|
|
8556
|
-
`INSERT INTO asset_origin_outbox_dead_letter
|
|
8557
|
-
(id, workspaceId, originKind, sourceRef, payloadJson, hintsJson, observedAt, errorJson, attempts, createdAt)
|
|
8558
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
8559
|
-
).run(
|
|
8560
|
-
`dl_${randomUUID5()}`,
|
|
8561
|
-
row.workspaceId,
|
|
8562
|
-
row.originKind,
|
|
8563
|
-
row.sourceRef,
|
|
8564
|
-
row.payloadJson,
|
|
8565
|
-
row.hintsJson,
|
|
8566
|
-
row.observedAt,
|
|
8567
|
-
JSON.stringify({ message: error, attempts: nextAttempts }),
|
|
8568
|
-
nextAttempts,
|
|
8569
|
-
now
|
|
8570
|
-
);
|
|
8571
|
-
this.db.prepare("DELETE FROM asset_origin_outbox WHERE id = ?").run(id);
|
|
8572
|
-
} else {
|
|
8573
|
-
this.db.prepare(
|
|
8574
|
-
`UPDATE asset_origin_outbox
|
|
8575
|
-
SET status = 'pending', attempts = ?, lastError = ?, updatedAt = ?
|
|
8576
|
-
WHERE id = ?`
|
|
8577
|
-
).run(nextAttempts, error, now, id);
|
|
8578
|
-
}
|
|
8579
|
-
});
|
|
8580
|
-
updated();
|
|
8581
|
-
}
|
|
8582
|
-
/**
|
|
8583
|
-
* Re-promote any rows orphaned in `'claimed'` state back to `'pending'`.
|
|
8584
|
-
*
|
|
8585
|
-
* Why this exists: claimNext() transitions a row to 'claimed' under a
|
|
8586
|
-
* transaction, but the upload that follows runs OUTSIDE the transaction
|
|
8587
|
-
* (network call). If the daemon dies between the claim and the
|
|
8588
|
-
* markUploaded/recordFailure call, the row is stuck — claimNext() only
|
|
8589
|
-
* selects status='pending', so it would never be picked up again.
|
|
8590
|
-
*
|
|
8591
|
-
* Phase-0 single-worker safety: there is at most one in-flight claim at
|
|
8592
|
-
* any time, so on next process startup ANY 'claimed' row is by definition
|
|
8593
|
-
* orphaned. We promote it back to 'pending' (preserving attempts /
|
|
8594
|
-
* lastError) so the next drainOnce can retry it.
|
|
8595
|
-
*
|
|
8596
|
-
* If we ever go multi-worker, this naive sweep becomes unsafe and must be
|
|
8597
|
-
* replaced with a lease/heartbeat scheme.
|
|
8598
|
-
*
|
|
8599
|
-
* Returns the number of rows recovered (for logging / observability).
|
|
8600
|
-
*/
|
|
8601
|
-
resetClaimed() {
|
|
8602
|
-
const result = this.db.prepare(
|
|
8603
|
-
"UPDATE asset_origin_outbox SET status = 'pending', updatedAt = ? WHERE status = 'claimed'"
|
|
8604
|
-
).run(Date.now());
|
|
8605
|
-
return Number(result.changes);
|
|
8606
|
-
}
|
|
8607
|
-
pendingCount() {
|
|
8608
|
-
return this.db.prepare("SELECT COUNT(*) AS n FROM asset_origin_outbox WHERE status = 'pending'").get().n;
|
|
8609
|
-
}
|
|
8610
|
-
uploadedCount() {
|
|
8611
|
-
return this.db.prepare("SELECT COUNT(*) AS n FROM asset_origin_outbox WHERE status = 'uploaded'").get().n;
|
|
8612
|
-
}
|
|
8613
|
-
deadLetterCount() {
|
|
8614
|
-
return this.db.prepare("SELECT COUNT(*) AS n FROM asset_origin_outbox_dead_letter").get().n;
|
|
8615
|
-
}
|
|
8616
|
-
close() {
|
|
8617
|
-
this.db.close();
|
|
8618
|
-
}
|
|
8619
|
-
};
|
|
8620
|
-
function computeIdempotencyKey(record) {
|
|
8621
|
-
return createHash5("sha256").update(record.originKind).update("|").update(record.workspaceId).update("|").update(record.sourceRef).update("|").update(String(record.observedAt)).digest("hex");
|
|
8622
|
-
}
|
|
8623
|
-
function coerceToString(value, fallback) {
|
|
8624
|
-
if (typeof value === "string") return value;
|
|
8625
|
-
if (value == null) return fallback;
|
|
8626
|
-
return String(value);
|
|
8627
|
-
}
|
|
8628
|
-
function coerceToJson(value) {
|
|
8629
|
-
if (typeof value === "string") return value;
|
|
8630
|
-
if (value == null) return "{}";
|
|
8631
|
-
try {
|
|
8632
|
-
return JSON.stringify(value);
|
|
8633
|
-
} catch {
|
|
8634
|
-
return "{}";
|
|
8635
|
-
}
|
|
8636
|
-
}
|
|
8637
|
-
function rawToRow(raw) {
|
|
8638
|
-
return {
|
|
8639
|
-
id: raw.id,
|
|
8640
|
-
workspaceId: raw.workspaceId,
|
|
8641
|
-
originKind: raw.originKind,
|
|
8642
|
-
sourceRef: raw.sourceRef,
|
|
8643
|
-
payloadJson: raw.payloadJson,
|
|
8644
|
-
hintsJson: raw.hintsJson,
|
|
8645
|
-
observedAt: raw.observedAt,
|
|
8646
|
-
attempts: raw.attempts,
|
|
8647
|
-
lastError: raw.lastError,
|
|
8648
|
-
createdAt: raw.createdAt
|
|
8649
|
-
};
|
|
8650
|
-
}
|
|
8651
|
-
|
|
8652
|
-
// src/daemon/asset/origin/upload-runner.ts
|
|
8653
|
-
var UploadRunner = class {
|
|
8654
|
-
constructor(opts) {
|
|
8655
|
-
this.opts = opts;
|
|
8656
|
-
opts.outbox.resetClaimed();
|
|
8657
|
-
}
|
|
8658
|
-
opts;
|
|
8659
|
-
/**
|
|
8660
|
-
* Process every currently-pending outbox row exactly once. Failed rows
|
|
8661
|
-
* stay 'pending' (with attempts+1) for the next drainOnce call; rows
|
|
8662
|
-
* that hit maxAttempts spill to dead-letter and trigger onUploadFailure.
|
|
8663
|
-
*
|
|
8664
|
-
* Returns the count of rows processed (success + failure).
|
|
8665
|
-
*/
|
|
8666
|
-
async drainOnce() {
|
|
8667
|
-
let count = 0;
|
|
8668
|
-
while (true) {
|
|
8669
|
-
const claim = this.opts.outbox.claimNext();
|
|
8670
|
-
if (!claim) break;
|
|
8671
|
-
count += 1;
|
|
8672
|
-
const row = claim.row;
|
|
8673
|
-
const adapter = this.opts.adapters[row.originKind];
|
|
8674
|
-
if (!adapter) {
|
|
8675
|
-
this.opts.outbox.recordFailure(claim.id, `no adapter registered for kind=${row.originKind}`, {
|
|
8676
|
-
maxAttempts: 1
|
|
8677
|
-
// unrecoverable — fast-fail to dead-letter
|
|
8678
|
-
});
|
|
8679
|
-
continue;
|
|
8680
|
-
}
|
|
8681
|
-
try {
|
|
8682
|
-
const obs = rowToObservation(row);
|
|
8683
|
-
const bytes = await adapter.fetch(obs);
|
|
8684
|
-
const id = await adapter.identifySource(obs);
|
|
8685
|
-
const filename = id.hints?.filename ?? path7.basename(row.sourceRef);
|
|
8686
|
-
const metadata = {
|
|
8687
|
-
sourceRef: row.sourceRef,
|
|
8688
|
-
sourceKind: row.originKind
|
|
8689
|
-
};
|
|
8690
|
-
if (id.hints?.description) metadata.description = id.hints.description;
|
|
8691
|
-
if (id.hints?.sourceAgentImUserId) {
|
|
8692
|
-
metadata.sourceAgentImUserId = id.hints.sourceAgentImUserId;
|
|
8693
|
-
}
|
|
8694
|
-
const folderPath = id.hints?.folderPath || void 0;
|
|
8695
|
-
const result = await this.opts.cloud.uploadAsset({
|
|
8696
|
-
workspaceId: row.workspaceId,
|
|
8697
|
-
filename,
|
|
8698
|
-
bytes: bytes.bytes,
|
|
8699
|
-
mime: bytes.mime,
|
|
8700
|
-
size: bytes.size,
|
|
8701
|
-
metadata,
|
|
8702
|
-
folderPath
|
|
8703
|
-
});
|
|
8704
|
-
this.opts.outbox.markUploaded(claim.id, result);
|
|
8705
|
-
await adapter.onUploadSuccess?.(obs);
|
|
8706
|
-
} catch (err) {
|
|
8707
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
8708
|
-
const before = this.opts.outbox.deadLetterCount();
|
|
8709
|
-
this.opts.outbox.recordFailure(claim.id, message, { maxAttempts: this.opts.maxAttempts });
|
|
8710
|
-
const after = this.opts.outbox.deadLetterCount();
|
|
8711
|
-
if (after > before) {
|
|
8712
|
-
const obs = rowToObservation(row);
|
|
8713
|
-
await adapter.onUploadFailure?.(obs, err);
|
|
8714
|
-
}
|
|
8715
|
-
}
|
|
8716
|
-
}
|
|
8717
|
-
return count;
|
|
8718
|
-
}
|
|
8719
|
-
};
|
|
8720
|
-
function rowToObservation(row) {
|
|
8721
|
-
let detail = {};
|
|
7454
|
+
if (typeof b.query !== "string" || !b.query.trim()) {
|
|
7455
|
+
return respond4002(res, "query is required (non-empty string)");
|
|
7456
|
+
}
|
|
7457
|
+
const index = resolveIndex(b.workspaceId);
|
|
7458
|
+
if (!index) {
|
|
7459
|
+
respond3(res, 404, {
|
|
7460
|
+
error: "workspace_index_not_found",
|
|
7461
|
+
workspaceId: b.workspaceId,
|
|
7462
|
+
message: "No asset metadata index for this workspace. Ensure the daemon has synced asset metadata."
|
|
7463
|
+
});
|
|
7464
|
+
return true;
|
|
7465
|
+
}
|
|
7466
|
+
const limit = typeof b.limit === "number" && b.limit > 0 ? b.limit : void 0;
|
|
7467
|
+
const items = index.search(b.query.trim(), limit);
|
|
7468
|
+
respond3(res, 200, { items });
|
|
7469
|
+
return true;
|
|
7470
|
+
}
|
|
7471
|
+
function respond3(res, status, body) {
|
|
7472
|
+
res.statusCode = status;
|
|
7473
|
+
res.setHeader("Content-Type", "application/json");
|
|
7474
|
+
res.end(JSON.stringify(body));
|
|
7475
|
+
}
|
|
7476
|
+
function respond4002(res, message) {
|
|
7477
|
+
respond3(res, 400, { error: "invalid_request", message });
|
|
7478
|
+
return true;
|
|
7479
|
+
}
|
|
7480
|
+
async function readJson3(req) {
|
|
7481
|
+
let raw = "";
|
|
7482
|
+
req.setEncoding("utf8");
|
|
7483
|
+
for await (const chunk of req) raw += chunk;
|
|
7484
|
+
if (!raw) return {};
|
|
8722
7485
|
try {
|
|
8723
|
-
|
|
8724
|
-
if (parsed && typeof parsed === "object") detail = parsed;
|
|
7486
|
+
return JSON.parse(raw);
|
|
8725
7487
|
} catch {
|
|
7488
|
+
throw new Error("invalid_json");
|
|
8726
7489
|
}
|
|
8727
|
-
return {
|
|
8728
|
-
workspaceId: row.workspaceId,
|
|
8729
|
-
detail,
|
|
8730
|
-
observedAt: row.observedAt
|
|
8731
|
-
};
|
|
8732
7490
|
}
|
|
8733
|
-
var path7 = {
|
|
8734
|
-
basename(p) {
|
|
8735
|
-
const idx = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
|
|
8736
|
-
return idx === -1 ? p : p.slice(idx + 1);
|
|
8737
|
-
}
|
|
8738
|
-
};
|
|
8739
7491
|
|
|
8740
7492
|
// src/daemon/outbox-watcher.ts
|
|
8741
|
-
import { promises as
|
|
8742
|
-
import * as
|
|
7493
|
+
import { promises as fs4 } from "fs";
|
|
7494
|
+
import * as path6 from "path";
|
|
8743
7495
|
var DEFAULT_INTERVAL_MS = 2e3;
|
|
8744
7496
|
var RESERVED_SUBDIR = "_uploaded";
|
|
8745
7497
|
var OutboxWatcher = class {
|
|
@@ -8906,7 +7658,7 @@ var OutboxWatcher = class {
|
|
|
8906
7658
|
async scanDir(dir, kind, task) {
|
|
8907
7659
|
let entries = [];
|
|
8908
7660
|
try {
|
|
8909
|
-
entries = await
|
|
7661
|
+
entries = await fs4.readdir(dir);
|
|
8910
7662
|
} catch (err) {
|
|
8911
7663
|
const code = err.code;
|
|
8912
7664
|
if (code === "ENOENT") return;
|
|
@@ -8916,10 +7668,10 @@ var OutboxWatcher = class {
|
|
|
8916
7668
|
for (const name of entries) {
|
|
8917
7669
|
if (name.startsWith(".")) continue;
|
|
8918
7670
|
if (name === RESERVED_SUBDIR) continue;
|
|
8919
|
-
const full =
|
|
7671
|
+
const full = path6.join(dir, name);
|
|
8920
7672
|
let st;
|
|
8921
7673
|
try {
|
|
8922
|
-
st = await
|
|
7674
|
+
st = await fs4.stat(full);
|
|
8923
7675
|
} catch {
|
|
8924
7676
|
continue;
|
|
8925
7677
|
}
|
|
@@ -8944,8 +7696,8 @@ var OutboxWatcher = class {
|
|
|
8944
7696
|
this.log("warn", `skip ${filePath}: no active taskId (no dispatch/handoff received yet)`);
|
|
8945
7697
|
return;
|
|
8946
7698
|
}
|
|
8947
|
-
const bytes = await
|
|
8948
|
-
const fileName =
|
|
7699
|
+
const bytes = await fs4.readFile(filePath);
|
|
7700
|
+
const fileName = path6.basename(filePath);
|
|
8949
7701
|
const blob = new Blob([new Uint8Array(bytes)]);
|
|
8950
7702
|
const form = new FormData();
|
|
8951
7703
|
form.append("workspaceId", wsId);
|
|
@@ -9040,7 +7792,7 @@ var ServicePool = class {
|
|
|
9040
7792
|
|
|
9041
7793
|
// src/daemon/shell-executor.ts
|
|
9042
7794
|
import { spawn as spawn4 } from "child_process";
|
|
9043
|
-
import { existsSync as
|
|
7795
|
+
import { existsSync as existsSync11 } from "fs";
|
|
9044
7796
|
import { resolve as resolve2 } from "path";
|
|
9045
7797
|
var DEFAULT_OUTPUT_LIMIT = 256 * 1024;
|
|
9046
7798
|
var DEFAULT_TIMEOUT = 6e4;
|
|
@@ -9077,7 +7829,7 @@ async function executeShellDispatch(payload, deps) {
|
|
|
9077
7829
|
const command = readCommand(payload, execution);
|
|
9078
7830
|
if (!command.trim()) return fail2(payload.taskId, "shell_command_required", "Shell command is required");
|
|
9079
7831
|
const cwd = resolveCwd(execution.cwd, deps.config.defaultCwd);
|
|
9080
|
-
if (!
|
|
7832
|
+
if (!existsSync11(cwd)) return fail2(payload.taskId, "shell_cwd_missing", `cwd does not exist: ${cwd}`);
|
|
9081
7833
|
const timeoutMs = Math.min(
|
|
9082
7834
|
typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? payload.timeoutMs : deps.config.maxTimeoutMs,
|
|
9083
7835
|
deps.config.maxTimeoutMs
|
|
@@ -9333,15 +8085,6 @@ var Runner = class extends EventEmitter3 {
|
|
|
9333
8085
|
outboxWatcher;
|
|
9334
8086
|
memoryWiring;
|
|
9335
8087
|
assetMetadataIndexes = /* @__PURE__ */ new Map();
|
|
9336
|
-
workspaceMirrors = /* @__PURE__ */ new Map();
|
|
9337
|
-
assetOriginOutbox;
|
|
9338
|
-
dropFolderAdapter;
|
|
9339
|
-
dropFolderUploadRunner;
|
|
9340
|
-
dropFolderTimer;
|
|
9341
|
-
dropFolderWorkspaceId = "";
|
|
9342
|
-
dropFolderWorkspaceDir = "";
|
|
9343
|
-
dropFolderTickInFlight = false;
|
|
9344
|
-
dropFolderStable = /* @__PURE__ */ new Map();
|
|
9345
8088
|
state = "idle";
|
|
9346
8089
|
startedAt = 0;
|
|
9347
8090
|
workspaceId = "";
|
|
@@ -9383,7 +8126,6 @@ var Runner = class extends EventEmitter3 {
|
|
|
9383
8126
|
this.servicePool = new ServicePool();
|
|
9384
8127
|
this.installStaticHostedAgentFromEnv();
|
|
9385
8128
|
this.loadAgentsFromDb();
|
|
9386
|
-
await this.prepareLocalProfiles();
|
|
9387
8129
|
this.syncQueue = new SyncQueue(this.db);
|
|
9388
8130
|
this.syncWorker = new SyncWorker({ queue: this.syncQueue, flush: (row) => this.flushSyncRow(row) });
|
|
9389
8131
|
this.syncWorker.start();
|
|
@@ -9405,10 +8147,9 @@ var Runner = class extends EventEmitter3 {
|
|
|
9405
8147
|
);
|
|
9406
8148
|
}
|
|
9407
8149
|
if (this.workspaceId) {
|
|
9408
|
-
this.
|
|
9409
|
-
(err) => console.error("[Daemon] Initial asset sync failed:", err.message)
|
|
8150
|
+
this.syncAssetMetadata(this.workspaceId).catch(
|
|
8151
|
+
(err) => console.error("[Daemon] Initial asset metadata sync failed:", err.message)
|
|
9410
8152
|
);
|
|
9411
|
-
await this.ensureDropFolderRuntime(this.workspaceId);
|
|
9412
8153
|
}
|
|
9413
8154
|
const containerId = process.env.PRISMER_CONTAINER_ID;
|
|
9414
8155
|
const isContainer = !!containerId || process.env.PRISMER_RUNTIME_MODE === "container";
|
|
@@ -9459,8 +8200,7 @@ var Runner = class extends EventEmitter3 {
|
|
|
9459
8200
|
// an actual agent process.
|
|
9460
8201
|
attachMemory: this.memoryWiring ? attachMemoryRpc({ runtime: this.memoryWiring.runtime }) : void 0,
|
|
9461
8202
|
attachAsset: attachAssetRpc({
|
|
9462
|
-
resolveIndex: (workspaceId) => this.assetMetadataIndexes.get(workspaceId)
|
|
9463
|
-
assetCache: this.assetCache
|
|
8203
|
+
resolveIndex: (workspaceId) => this.assetMetadataIndexes.get(workspaceId)
|
|
9464
8204
|
})
|
|
9465
8205
|
});
|
|
9466
8206
|
await this.localServer.start();
|
|
@@ -9509,7 +8249,6 @@ var Runner = class extends EventEmitter3 {
|
|
|
9509
8249
|
this.memoryWiring = void 0;
|
|
9510
8250
|
this.ws?.close();
|
|
9511
8251
|
this.outboxWatcher?.stop();
|
|
9512
|
-
this.stopDropFolderRuntime();
|
|
9513
8252
|
await this.servicePool?.shutdown();
|
|
9514
8253
|
await this.localServer?.stop();
|
|
9515
8254
|
try {
|
|
@@ -9566,7 +8305,6 @@ var Runner = class extends EventEmitter3 {
|
|
|
9566
8305
|
runningTaskIds: Array.from(this.runningTasks.keys()),
|
|
9567
8306
|
observability: {
|
|
9568
8307
|
adapters: this.snapshotAdapterObservability(),
|
|
9569
|
-
assetSync: this.snapshotAssetSyncObservability(),
|
|
9570
8308
|
...this.lastTaskError ? { lastTaskError: this.lastTaskError } : {}
|
|
9571
8309
|
}
|
|
9572
8310
|
};
|
|
@@ -9602,35 +8340,6 @@ var Runner = class extends EventEmitter3 {
|
|
|
9602
8340
|
});
|
|
9603
8341
|
}
|
|
9604
8342
|
}
|
|
9605
|
-
async prepareLocalProfiles() {
|
|
9606
|
-
const rows = this.db.prepare(
|
|
9607
|
-
`SELECT id, workspace_id, agent_im_user_id, adapter_name, name, config, version, synced_at
|
|
9608
|
-
FROM agent_profiles
|
|
9609
|
-
WHERE deleted_at IS NULL`
|
|
9610
|
-
).all();
|
|
9611
|
-
for (const row of rows) {
|
|
9612
|
-
const adapter = this.registry.get(row.adapter_name);
|
|
9613
|
-
if (!adapter?.prepareProfile) continue;
|
|
9614
|
-
try {
|
|
9615
|
-
await adapter.prepareProfile({
|
|
9616
|
-
id: row.id,
|
|
9617
|
-
workspaceId: row.workspace_id,
|
|
9618
|
-
agentImUserId: row.agent_im_user_id,
|
|
9619
|
-
adapterName: row.adapter_name,
|
|
9620
|
-
name: row.name,
|
|
9621
|
-
config: parseProfileConfig(row.config),
|
|
9622
|
-
version: row.version,
|
|
9623
|
-
createdAt: new Date(row.synced_at ?? Date.now()),
|
|
9624
|
-
updatedAt: new Date(row.synced_at ?? Date.now())
|
|
9625
|
-
});
|
|
9626
|
-
} catch (err) {
|
|
9627
|
-
process.stderr.write(
|
|
9628
|
-
`[daemon] local profile preflight skipped agent=${row.agent_im_user_id} profile=${row.id}: ${err.message}
|
|
9629
|
-
`
|
|
9630
|
-
);
|
|
9631
|
-
}
|
|
9632
|
-
}
|
|
9633
|
-
}
|
|
9634
8343
|
/**
|
|
9635
8344
|
* Register an in-process AgentProfile snapshot (called by agent CLI / sync layer).
|
|
9636
8345
|
* Used to populate the `agents` list in agent.host.declare.
|
|
@@ -9688,10 +8397,10 @@ var Runner = class extends EventEmitter3 {
|
|
|
9688
8397
|
const rawJson = process.env.PRISMER_HOSTED_AGENT_JSON;
|
|
9689
8398
|
let raw;
|
|
9690
8399
|
if (rawFile) {
|
|
9691
|
-
if (!
|
|
8400
|
+
if (!existsSync12(rawFile)) {
|
|
9692
8401
|
throw new Error(`PRISMER_HOSTED_AGENT_FILE not found: ${rawFile}`);
|
|
9693
8402
|
}
|
|
9694
|
-
raw =
|
|
8403
|
+
raw = readFileSync9(rawFile, "utf8");
|
|
9695
8404
|
} else if (rawJson) {
|
|
9696
8405
|
raw = rawJson;
|
|
9697
8406
|
}
|
|
@@ -9793,7 +8502,7 @@ var Runner = class extends EventEmitter3 {
|
|
|
9793
8502
|
name: a.name,
|
|
9794
8503
|
adapterName: a.adapterName,
|
|
9795
8504
|
capabilities: a.capabilities,
|
|
9796
|
-
profiles: Array.from(a.profiles.entries()).map(([id,
|
|
8505
|
+
profiles: Array.from(a.profiles.entries()).map(([id, version2]) => ({ id, version: version2 }))
|
|
9797
8506
|
}))
|
|
9798
8507
|
};
|
|
9799
8508
|
this.ws.send(envelope("agent.host.declare", payload, this.config.daemon_id));
|
|
@@ -9835,29 +8544,17 @@ var Runner = class extends EventEmitter3 {
|
|
|
9835
8544
|
(err) => console.error("[Daemon] Initial memory sync failed:", err.message)
|
|
9836
8545
|
);
|
|
9837
8546
|
}
|
|
9838
|
-
this.
|
|
9839
|
-
(err) => console.error("[Daemon] Asset sync failed:", err.message)
|
|
8547
|
+
this.syncAssetMetadata(this.workspaceId).catch(
|
|
8548
|
+
(err) => console.error("[Daemon] Asset metadata sync failed:", err.message)
|
|
9840
8549
|
);
|
|
9841
|
-
await this.ensureDropFolderRuntime(this.workspaceId);
|
|
9842
8550
|
for (const id of payload.profilesToSync) {
|
|
9843
8551
|
try {
|
|
9844
|
-
await this.servicePool.drop(id);
|
|
9845
8552
|
await this.syncProfileFromCloud(id);
|
|
9846
8553
|
} catch (err) {
|
|
9847
8554
|
this.emit("sync-error", err);
|
|
9848
8555
|
}
|
|
9849
8556
|
}
|
|
9850
|
-
|
|
9851
|
-
const row = this.db.prepare("SELECT agent_im_user_id FROM agent_profiles WHERE id = ?").get(id);
|
|
9852
|
-
this.db.prepare("DELETE FROM agent_profiles WHERE id = ?").run(id);
|
|
9853
|
-
if (row) {
|
|
9854
|
-
this.db.prepare("DELETE FROM agents WHERE im_user_id = ?").run(row.agent_im_user_id);
|
|
9855
|
-
}
|
|
9856
|
-
process.stderr.write(`[daemon] tombstone profile=${id}
|
|
9857
|
-
`);
|
|
9858
|
-
}
|
|
9859
|
-
if (payload.profilesToDelete?.length) this.loadAgentsFromDb();
|
|
9860
|
-
if ((payload.profilesToSync.length > 0 || (payload.profilesToDelete?.length ?? 0) > 0) && this.wsConnected) this.sendDeclare();
|
|
8557
|
+
if (payload.profilesToSync.length > 0 && this.wsConnected) this.sendDeclare();
|
|
9861
8558
|
this.emit("host-acked", payload);
|
|
9862
8559
|
}
|
|
9863
8560
|
async onTaskDispatch(payload, requestId) {
|
|
@@ -9898,14 +8595,8 @@ var Runner = class extends EventEmitter3 {
|
|
|
9898
8595
|
}
|
|
9899
8596
|
});
|
|
9900
8597
|
this.ws.send(envelope("task.dispatch.reply", reply, requestId));
|
|
9901
|
-
if (!reply.ok) {
|
|
9902
|
-
const code = reply.error?.code ?? "unknown";
|
|
9903
|
-
const message = reply.error?.message ?? "unknown error";
|
|
9904
|
-
process.stderr.write(`[daemon] dispatch failed task=${payload.taskId} code=${code} message=${message}
|
|
9905
|
-
`);
|
|
9906
|
-
}
|
|
9907
8598
|
} else {
|
|
9908
|
-
|
|
8599
|
+
await handleDispatch(payload, requestId, {
|
|
9909
8600
|
registry: this.registry,
|
|
9910
8601
|
cloud: this.cloud,
|
|
9911
8602
|
uriResolver: this.uriResolver,
|
|
@@ -9921,12 +8612,6 @@ var Runner = class extends EventEmitter3 {
|
|
|
9921
8612
|
if (running) running.lastProgressAt = Date.now();
|
|
9922
8613
|
}
|
|
9923
8614
|
});
|
|
9924
|
-
if (!reply.ok) {
|
|
9925
|
-
const code = reply.error?.code ?? "unknown";
|
|
9926
|
-
const message = reply.error?.message ?? "unknown error";
|
|
9927
|
-
process.stderr.write(`[daemon] dispatch failed task=${payload.taskId} code=${code} message=${message}
|
|
9928
|
-
`);
|
|
9929
|
-
}
|
|
9930
8615
|
}
|
|
9931
8616
|
process.stdout.write(`[daemon] dispatch done task=${payload.taskId}
|
|
9932
8617
|
`);
|
|
@@ -9954,7 +8639,6 @@ var Runner = class extends EventEmitter3 {
|
|
|
9954
8639
|
}
|
|
9955
8640
|
async onAgentProfileChanged(payload) {
|
|
9956
8641
|
try {
|
|
9957
|
-
await this.servicePool.drop(payload.profileId);
|
|
9958
8642
|
await this.syncProfileFromCloud(payload.profileId);
|
|
9959
8643
|
if (this.wsConnected) this.sendDeclare();
|
|
9960
8644
|
} catch (err) {
|
|
@@ -9962,21 +8646,7 @@ var Runner = class extends EventEmitter3 {
|
|
|
9962
8646
|
}
|
|
9963
8647
|
}
|
|
9964
8648
|
async syncProfileFromCloud(profileId) {
|
|
9965
|
-
|
|
9966
|
-
try {
|
|
9967
|
-
profile = await this.cloud.get(`/api/im/agent_profiles/${encodeURIComponent(profileId)}`);
|
|
9968
|
-
} catch (err) {
|
|
9969
|
-
if (err instanceof CloudError && err.status === 404) {
|
|
9970
|
-
const row = this.db.prepare("SELECT agent_im_user_id FROM agent_profiles WHERE id = ?").get(profileId);
|
|
9971
|
-
this.db.prepare("DELETE FROM agent_profiles WHERE id = ?").run(profileId);
|
|
9972
|
-
if (row) {
|
|
9973
|
-
this.db.prepare("DELETE FROM agents WHERE im_user_id = ?").run(row.agent_im_user_id);
|
|
9974
|
-
}
|
|
9975
|
-
this.loadAgentsFromDb();
|
|
9976
|
-
return;
|
|
9977
|
-
}
|
|
9978
|
-
throw err;
|
|
9979
|
-
}
|
|
8649
|
+
const profile = await this.cloud.get(`/api/im/agent_profiles/${encodeURIComponent(profileId)}`);
|
|
9980
8650
|
const agent = await this.resolveOwnedAgent(profile.agentImUserId);
|
|
9981
8651
|
const adapter = this.registry.get(profile.adapterName);
|
|
9982
8652
|
const capabilities = agent?.card?.capabilities?.length ? agent.card.capabilities : adapter?.capabilities ?? [];
|
|
@@ -10011,14 +8681,6 @@ var Runner = class extends EventEmitter3 {
|
|
|
10011
8681
|
);
|
|
10012
8682
|
});
|
|
10013
8683
|
tx();
|
|
10014
|
-
try {
|
|
10015
|
-
await adapter?.prepareProfile?.(profile);
|
|
10016
|
-
} catch (err) {
|
|
10017
|
-
process.stderr.write(
|
|
10018
|
-
`[daemon] profile preflight skipped agent=${profile.agentImUserId} profile=${profile.id}: ${err.message}
|
|
10019
|
-
`
|
|
10020
|
-
);
|
|
10021
|
-
}
|
|
10022
8684
|
this.loadAgentsFromDb();
|
|
10023
8685
|
process.stdout.write(
|
|
10024
8686
|
`[daemon] profile synced agent=${profile.agentImUserId} profile=${profile.id} adapter=${profile.adapterName}
|
|
@@ -10047,10 +8709,15 @@ var Runner = class extends EventEmitter3 {
|
|
|
10047
8709
|
}
|
|
10048
8710
|
async onAssetChanged(payload) {
|
|
10049
8711
|
if (!payload.workspaceId) return;
|
|
8712
|
+
const index = this.assetMetadataIndexes.get(payload.workspaceId);
|
|
8713
|
+
if (!index) return;
|
|
10050
8714
|
try {
|
|
10051
|
-
await
|
|
8715
|
+
const result = await index.pullDelta();
|
|
8716
|
+
if (result.applied > 0) {
|
|
8717
|
+
console.log(`[Daemon] asset.changed workspace=${payload.workspaceId} applied=${result.applied}`);
|
|
8718
|
+
}
|
|
10052
8719
|
} catch (err) {
|
|
10053
|
-
console.error(`[Daemon] asset.changed
|
|
8720
|
+
console.error(`[Daemon] asset.changed pullDelta failed workspace=${payload.workspaceId}:`, err.message);
|
|
10054
8721
|
}
|
|
10055
8722
|
}
|
|
10056
8723
|
onWorkspaceFileChanged(payload) {
|
|
@@ -10088,15 +8755,6 @@ var Runner = class extends EventEmitter3 {
|
|
|
10088
8755
|
}
|
|
10089
8756
|
};
|
|
10090
8757
|
}
|
|
10091
|
-
snapshotAssetSyncObservability() {
|
|
10092
|
-
return {
|
|
10093
|
-
workspaceId: this.dropFolderWorkspaceId || null,
|
|
10094
|
-
rootDir: this.dropFolderWorkspaceDir || null,
|
|
10095
|
-
dropDir: this.dropFolderWorkspaceDir ? join16(this.dropFolderWorkspaceDir, "drop") : null,
|
|
10096
|
-
uploadedDir: this.dropFolderWorkspaceDir ? join16(this.dropFolderWorkspaceDir, "uploaded") : null,
|
|
10097
|
-
failedDir: this.dropFolderWorkspaceDir ? join16(this.dropFolderWorkspaceDir, "upload-failed") : null
|
|
10098
|
-
};
|
|
10099
|
-
}
|
|
10100
8758
|
/**
|
|
10101
8759
|
* Ensure an AssetMetadataIndex exists for the given workspace and pull
|
|
10102
8760
|
* delta from cloud. Idempotent — creates the index on first call, reuses
|
|
@@ -10119,132 +8777,6 @@ var Runner = class extends EventEmitter3 {
|
|
|
10119
8777
|
console.log(`[AssetMeta] workspace=${workspaceId}: ${result.applied} applied, cursor=${result.cursor}`);
|
|
10120
8778
|
}
|
|
10121
8779
|
}
|
|
10122
|
-
/**
|
|
10123
|
-
* Ensure a WorkspaceMirror exists for the workspace and pull path→asset
|
|
10124
|
-
* bindings from cloud. Cold-start daemons previously only had this table
|
|
10125
|
-
* populated by WS push or per-path URI fallback, so local asset state looked
|
|
10126
|
-
* empty even though cloud files existed.
|
|
10127
|
-
*/
|
|
10128
|
-
async syncWorkspaceFiles(workspaceId) {
|
|
10129
|
-
let mirror = this.workspaceMirrors.get(workspaceId);
|
|
10130
|
-
if (!mirror) {
|
|
10131
|
-
const stateDir = `${this.paths.root}/${workspaceId}`;
|
|
10132
|
-
mirror = new WorkspaceMirror({
|
|
10133
|
-
db: this.db,
|
|
10134
|
-
cloud: this.cloud,
|
|
10135
|
-
workspaceId,
|
|
10136
|
-
workspaceStateDir: stateDir
|
|
10137
|
-
});
|
|
10138
|
-
this.workspaceMirrors.set(workspaceId, mirror);
|
|
10139
|
-
}
|
|
10140
|
-
const result = await mirror.pullDelta();
|
|
10141
|
-
if (result.applied > 0) {
|
|
10142
|
-
console.log(`[WorkspaceFiles] workspace=${workspaceId}: ${result.applied} applied, cursor=${result.cursor ?? "null"}`);
|
|
10143
|
-
}
|
|
10144
|
-
}
|
|
10145
|
-
async syncAssetState(workspaceId) {
|
|
10146
|
-
if (!this.paths?.root) return;
|
|
10147
|
-
await Promise.all([
|
|
10148
|
-
this.syncAssetMetadata(workspaceId),
|
|
10149
|
-
this.syncWorkspaceFiles(workspaceId)
|
|
10150
|
-
]);
|
|
10151
|
-
}
|
|
10152
|
-
/**
|
|
10153
|
-
* Desktop asset ingress: watch a user-visible local asset folder
|
|
10154
|
-
* and upload new files as IMAssets, preserving nested relative dirs in
|
|
10155
|
-
* IMAsset.folderPath. The lower-level adapter/outbox/uploader already
|
|
10156
|
-
* existed; this method wires them into the real daemon lifecycle.
|
|
10157
|
-
*/
|
|
10158
|
-
async ensureDropFolderRuntime(workspaceId) {
|
|
10159
|
-
if (!workspaceId || !this.paths?.root) return;
|
|
10160
|
-
if (this.dropFolderWorkspaceId === workspaceId && this.dropFolderTimer) return;
|
|
10161
|
-
this.stopDropFolderRuntime();
|
|
10162
|
-
this.dropFolderWorkspaceId = workspaceId;
|
|
10163
|
-
const workspaceRoot = resolveUserAssetWorkspaceDir(workspaceId);
|
|
10164
|
-
this.dropFolderWorkspaceDir = workspaceRoot;
|
|
10165
|
-
await mkdir(join16(workspaceRoot, "drop"), { recursive: true });
|
|
10166
|
-
await mkdir(join16(workspaceRoot, "uploaded"), { recursive: true });
|
|
10167
|
-
await mkdir(join16(workspaceRoot, "upload-failed"), { recursive: true });
|
|
10168
|
-
this.assetOriginOutbox = new OriginOutbox({ dbPath: join16(this.paths.root, "asset-origin.db") });
|
|
10169
|
-
this.dropFolderAdapter = new DropFolderAdapter({ workspaceDir: workspaceRoot });
|
|
10170
|
-
this.dropFolderUploadRunner = new UploadRunner({
|
|
10171
|
-
outbox: this.assetOriginOutbox,
|
|
10172
|
-
cloud: new DaemonAssetUploadClient(this.config),
|
|
10173
|
-
adapters: { "drop-folder": this.dropFolderAdapter },
|
|
10174
|
-
maxAttempts: 3
|
|
10175
|
-
});
|
|
10176
|
-
const tick = () => {
|
|
10177
|
-
this.runDropFolderTick().catch((err) => {
|
|
10178
|
-
process.stderr.write(`[daemon] drop-folder sync failed: ${err.message}
|
|
10179
|
-
`);
|
|
10180
|
-
});
|
|
10181
|
-
};
|
|
10182
|
-
this.dropFolderTimer = setInterval(tick, 1e3);
|
|
10183
|
-
tick();
|
|
10184
|
-
process.stdout.write(`[daemon] drop-folder asset sync watching ${join16(workspaceRoot, "drop")}
|
|
10185
|
-
`);
|
|
10186
|
-
}
|
|
10187
|
-
stopDropFolderRuntime() {
|
|
10188
|
-
if (this.dropFolderTimer) {
|
|
10189
|
-
clearInterval(this.dropFolderTimer);
|
|
10190
|
-
this.dropFolderTimer = void 0;
|
|
10191
|
-
}
|
|
10192
|
-
try {
|
|
10193
|
-
this.assetOriginOutbox?.close();
|
|
10194
|
-
} catch {
|
|
10195
|
-
}
|
|
10196
|
-
this.assetOriginOutbox = void 0;
|
|
10197
|
-
this.dropFolderAdapter = void 0;
|
|
10198
|
-
this.dropFolderUploadRunner = void 0;
|
|
10199
|
-
this.dropFolderWorkspaceId = "";
|
|
10200
|
-
this.dropFolderWorkspaceDir = "";
|
|
10201
|
-
this.dropFolderTickInFlight = false;
|
|
10202
|
-
this.dropFolderStable.clear();
|
|
10203
|
-
}
|
|
10204
|
-
async runDropFolderTick() {
|
|
10205
|
-
if (this.dropFolderTickInFlight) return;
|
|
10206
|
-
const workspaceId = this.dropFolderWorkspaceId;
|
|
10207
|
-
const adapter = this.dropFolderAdapter;
|
|
10208
|
-
const outbox = this.assetOriginOutbox;
|
|
10209
|
-
const runner = this.dropFolderUploadRunner;
|
|
10210
|
-
if (!workspaceId || !adapter || !outbox || !runner) return;
|
|
10211
|
-
this.dropFolderTickInFlight = true;
|
|
10212
|
-
try {
|
|
10213
|
-
const observations = await adapter.scanOnce(workspaceId);
|
|
10214
|
-
for (const obs of observations) {
|
|
10215
|
-
const id = await adapter.identifySource(obs);
|
|
10216
|
-
if (!this.isDropFolderObservationStable(id.sourceRef, obs.detail)) continue;
|
|
10217
|
-
outbox.enqueue({
|
|
10218
|
-
workspaceId: obs.workspaceId,
|
|
10219
|
-
originKind: "drop-folder",
|
|
10220
|
-
sourceRef: id.sourceRef,
|
|
10221
|
-
payloadJson: JSON.stringify(obs.detail),
|
|
10222
|
-
hintsJson: JSON.stringify(id.hints ?? {}),
|
|
10223
|
-
observedAt: obs.observedAt
|
|
10224
|
-
});
|
|
10225
|
-
}
|
|
10226
|
-
const processed = await runner.drainOnce();
|
|
10227
|
-
if (processed > 0) {
|
|
10228
|
-
this.dropFolderStable.clear();
|
|
10229
|
-
process.stdout.write(`[daemon] drop-folder uploaded/processed ${processed} asset(s)
|
|
10230
|
-
`);
|
|
10231
|
-
await this.syncAssetState(workspaceId).catch(
|
|
10232
|
-
(err) => process.stderr.write(`[daemon] drop-folder post-sync failed: ${err.message}
|
|
10233
|
-
`)
|
|
10234
|
-
);
|
|
10235
|
-
}
|
|
10236
|
-
} finally {
|
|
10237
|
-
this.dropFolderTickInFlight = false;
|
|
10238
|
-
}
|
|
10239
|
-
}
|
|
10240
|
-
isDropFolderObservationStable(sourceRef, detail) {
|
|
10241
|
-
const raw = detail && typeof detail === "object" ? detail : {};
|
|
10242
|
-
const size = typeof raw.size === "number" ? raw.size : -1;
|
|
10243
|
-
const mtime = typeof raw.mtime === "number" ? raw.mtime : -1;
|
|
10244
|
-
const previous = this.dropFolderStable.get(sourceRef);
|
|
10245
|
-
this.dropFolderStable.set(sourceRef, { size, mtime });
|
|
10246
|
-
return Boolean(previous && previous.size === size && previous.mtime === mtime);
|
|
10247
|
-
}
|
|
10248
8780
|
/**
|
|
10249
8781
|
* SyncWorker FlushFn — pushes local writes to cloud.
|
|
10250
8782
|
*
|
|
@@ -10281,44 +8813,44 @@ var Runner = class extends EventEmitter3 {
|
|
|
10281
8813
|
const id = encodeURIComponent(row.resource_id);
|
|
10282
8814
|
const body = row.operation === "delete" ? void 0 : safeJsonParse(row.payload);
|
|
10283
8815
|
let method;
|
|
10284
|
-
let
|
|
8816
|
+
let path7;
|
|
10285
8817
|
switch (op) {
|
|
10286
8818
|
case "workspace.create":
|
|
10287
8819
|
method = "POST";
|
|
10288
|
-
|
|
8820
|
+
path7 = "/api/im/workspaces";
|
|
10289
8821
|
break;
|
|
10290
8822
|
case "workspace.update":
|
|
10291
8823
|
method = "PATCH";
|
|
10292
|
-
|
|
8824
|
+
path7 = `/api/im/workspaces/${id}`;
|
|
10293
8825
|
break;
|
|
10294
8826
|
case "workspace.delete":
|
|
10295
8827
|
method = "DELETE";
|
|
10296
|
-
|
|
8828
|
+
path7 = `/api/im/workspaces/${id}`;
|
|
10297
8829
|
break;
|
|
10298
8830
|
// agent.create must use /register — POST /api/im/agents is not exposed.
|
|
10299
8831
|
case "agent.create":
|
|
10300
8832
|
method = "POST";
|
|
10301
|
-
|
|
8833
|
+
path7 = "/api/im/register";
|
|
10302
8834
|
break;
|
|
10303
8835
|
case "agent.update":
|
|
10304
8836
|
method = "PATCH";
|
|
10305
|
-
|
|
8837
|
+
path7 = `/api/im/agents/${id}`;
|
|
10306
8838
|
break;
|
|
10307
8839
|
case "agent.delete":
|
|
10308
8840
|
method = "DELETE";
|
|
10309
|
-
|
|
8841
|
+
path7 = `/api/im/agents/${id}`;
|
|
10310
8842
|
break;
|
|
10311
8843
|
case "agent_profile.create":
|
|
10312
8844
|
method = "POST";
|
|
10313
|
-
|
|
8845
|
+
path7 = "/api/im/agent_profiles";
|
|
10314
8846
|
break;
|
|
10315
8847
|
case "agent_profile.update":
|
|
10316
8848
|
method = "PATCH";
|
|
10317
|
-
|
|
8849
|
+
path7 = `/api/im/agent_profiles/${id}`;
|
|
10318
8850
|
break;
|
|
10319
8851
|
case "agent_profile.delete":
|
|
10320
8852
|
method = "DELETE";
|
|
10321
|
-
|
|
8853
|
+
path7 = `/api/im/agent_profiles/${id}`;
|
|
10322
8854
|
break;
|
|
10323
8855
|
default:
|
|
10324
8856
|
process.stderr.write(`[daemon] sync flush op=${op} id=${row.id} result=drop (unknown op)
|
|
@@ -10327,7 +8859,7 @@ var Runner = class extends EventEmitter3 {
|
|
|
10327
8859
|
}
|
|
10328
8860
|
let res;
|
|
10329
8861
|
try {
|
|
10330
|
-
res = await this.cloud.request(method,
|
|
8862
|
+
res = await this.cloud.request(method, path7, { body });
|
|
10331
8863
|
} catch (err) {
|
|
10332
8864
|
process.stderr.write(
|
|
10333
8865
|
`[daemon] sync flush op=${op} id=${row.id} result=retry (threw: ${err.message})
|
|
@@ -10350,89 +8882,6 @@ var Runner = class extends EventEmitter3 {
|
|
|
10350
8882
|
return { ok: res.ok, status: res.status, message: res.error?.message };
|
|
10351
8883
|
}
|
|
10352
8884
|
};
|
|
10353
|
-
var DaemonAssetUploadClient = class {
|
|
10354
|
-
constructor(config) {
|
|
10355
|
-
this.config = config;
|
|
10356
|
-
}
|
|
10357
|
-
config;
|
|
10358
|
-
async uploadAsset(req) {
|
|
10359
|
-
const form = new FormData();
|
|
10360
|
-
form.set("workspaceId", req.workspaceId);
|
|
10361
|
-
form.set("kind", "file");
|
|
10362
|
-
form.set("metadata", JSON.stringify(req.metadata));
|
|
10363
|
-
if (req.folderPath !== void 0) form.set("folderPath", req.folderPath);
|
|
10364
|
-
form.set("file", new Blob([req.bytes], { type: req.mime ?? "application/octet-stream" }), req.filename);
|
|
10365
|
-
const url = `${this.config.cloud_api_base.replace(/\/$/, "")}/api/im/assets`;
|
|
10366
|
-
const controller = new AbortController();
|
|
10367
|
-
const timer = setTimeout(() => controller.abort(), 6e4);
|
|
10368
|
-
let res;
|
|
10369
|
-
try {
|
|
10370
|
-
res = await fetch(url, {
|
|
10371
|
-
method: "POST",
|
|
10372
|
-
headers: { Authorization: `Bearer ${this.config.api_key}` },
|
|
10373
|
-
body: form,
|
|
10374
|
-
signal: controller.signal
|
|
10375
|
-
});
|
|
10376
|
-
} catch (err) {
|
|
10377
|
-
if (err.name === "AbortError") {
|
|
10378
|
-
throw new Error("asset upload timed out after 60000ms");
|
|
10379
|
-
}
|
|
10380
|
-
throw err;
|
|
10381
|
-
} finally {
|
|
10382
|
-
clearTimeout(timer);
|
|
10383
|
-
}
|
|
10384
|
-
const text = await res.text();
|
|
10385
|
-
let body;
|
|
10386
|
-
try {
|
|
10387
|
-
body = text ? JSON.parse(text) : void 0;
|
|
10388
|
-
} catch {
|
|
10389
|
-
body = text;
|
|
10390
|
-
}
|
|
10391
|
-
if (!res.ok || isCloudErrorEnvelope(body)) {
|
|
10392
|
-
throw new Error(`asset upload failed (${res.status}): ${cloudUploadErrorMessage(body)}`);
|
|
10393
|
-
}
|
|
10394
|
-
const data = unwrapCloudData(body);
|
|
10395
|
-
const assetId = stringValue(data, "id");
|
|
10396
|
-
const contentHash = stringValue(data, "contentHash");
|
|
10397
|
-
if (!assetId || !contentHash) {
|
|
10398
|
-
throw new Error("asset upload response missing id/contentHash");
|
|
10399
|
-
}
|
|
10400
|
-
return { assetId, contentHash };
|
|
10401
|
-
}
|
|
10402
|
-
};
|
|
10403
|
-
function isCloudErrorEnvelope(raw) {
|
|
10404
|
-
return Boolean(raw && typeof raw === "object" && "ok" in raw && raw.ok === false);
|
|
10405
|
-
}
|
|
10406
|
-
function cloudUploadErrorMessage(raw) {
|
|
10407
|
-
if (raw && typeof raw === "object") {
|
|
10408
|
-
const error = raw.error;
|
|
10409
|
-
if (typeof error === "string") return error;
|
|
10410
|
-
if (error && typeof error === "object") {
|
|
10411
|
-
const message = error.message;
|
|
10412
|
-
if (typeof message === "string") return message;
|
|
10413
|
-
}
|
|
10414
|
-
}
|
|
10415
|
-
return typeof raw === "string" ? raw : "request failed";
|
|
10416
|
-
}
|
|
10417
|
-
function unwrapCloudData(raw) {
|
|
10418
|
-
if (raw && typeof raw === "object") {
|
|
10419
|
-
const data = raw.data;
|
|
10420
|
-
if (data && typeof data === "object" && !Array.isArray(data)) return data;
|
|
10421
|
-
}
|
|
10422
|
-
return {};
|
|
10423
|
-
}
|
|
10424
|
-
function stringValue(obj, key) {
|
|
10425
|
-
const value = obj[key];
|
|
10426
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
10427
|
-
}
|
|
10428
|
-
function resolveUserAssetWorkspaceDir(workspaceId) {
|
|
10429
|
-
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
10430
|
-
if (override) return join16(override, workspaceId);
|
|
10431
|
-
const home = homedir7();
|
|
10432
|
-
const desktop = join16(home, "Desktop");
|
|
10433
|
-
const base = existsSync13(desktop) ? desktop : home;
|
|
10434
|
-
return join16(base, "Prismer Assets", workspaceId);
|
|
10435
|
-
}
|
|
10436
8885
|
function readTargetDaemonId(payload) {
|
|
10437
8886
|
if (typeof payload.targetDaemonId === "string" && payload.targetDaemonId.length > 0) {
|
|
10438
8887
|
return payload.targetDaemonId;
|
|
@@ -10483,14 +8932,6 @@ function validateStaticHostedAgent(raw) {
|
|
|
10483
8932
|
}
|
|
10484
8933
|
};
|
|
10485
8934
|
}
|
|
10486
|
-
function parseProfileConfig(raw) {
|
|
10487
|
-
try {
|
|
10488
|
-
const parsed = JSON.parse(raw);
|
|
10489
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
10490
|
-
} catch {
|
|
10491
|
-
return {};
|
|
10492
|
-
}
|
|
10493
|
-
}
|
|
10494
8935
|
function safeJsonParse(raw) {
|
|
10495
8936
|
try {
|
|
10496
8937
|
return JSON.parse(raw);
|
|
@@ -10517,7 +8958,7 @@ function buildDaemonCommand() {
|
|
|
10517
8958
|
async function runForeground(opts) {
|
|
10518
8959
|
const paths = resolvePaths();
|
|
10519
8960
|
const existingPid = readPidFile(paths);
|
|
10520
|
-
if (existingPid &&
|
|
8961
|
+
if (existingPid && pidAlive(existingPid)) {
|
|
10521
8962
|
exitWithError(`Daemon already running (pid ${existingPid}). Use \`prismer daemon stop\` first.`);
|
|
10522
8963
|
}
|
|
10523
8964
|
writePidFile(paths, process.pid);
|
|
@@ -10541,7 +8982,7 @@ function buildDaemonCommand() {
|
|
|
10541
8982
|
while (!configExists(paths)) {
|
|
10542
8983
|
if (stopRequested) return;
|
|
10543
8984
|
getUI().info(`[daemon] waiting for ${paths.configFile} \u2014 run \`prismer setup\` to create it`);
|
|
10544
|
-
await
|
|
8985
|
+
await sleep(5e3);
|
|
10545
8986
|
}
|
|
10546
8987
|
runner = new Runner({
|
|
10547
8988
|
startLocalServer: opts.localServer !== false,
|
|
@@ -10570,12 +9011,12 @@ function buildDaemonCommand() {
|
|
|
10570
9011
|
async function startBackground(opts) {
|
|
10571
9012
|
const paths = resolvePaths();
|
|
10572
9013
|
const existingPid = readPidFile(paths);
|
|
10573
|
-
if (existingPid &&
|
|
9014
|
+
if (existingPid && pidAlive(existingPid)) {
|
|
10574
9015
|
exitWithError(`Daemon already running (pid ${existingPid}). Use \`prismer daemon stop\` first.`);
|
|
10575
9016
|
}
|
|
10576
|
-
if (!
|
|
10577
|
-
const logFile =
|
|
10578
|
-
const fd =
|
|
9017
|
+
if (!existsSync13(paths.logsDir)) mkdirSync8(paths.logsDir, { recursive: true });
|
|
9018
|
+
const logFile = join11(paths.logsDir, "daemon.log");
|
|
9019
|
+
const fd = openSync(logFile, "a");
|
|
10579
9020
|
const args = [process.argv[1], "daemon", "run"];
|
|
10580
9021
|
if (opts.port) args.push("--port", String(opts.port));
|
|
10581
9022
|
if (opts.localServer === false) args.push("--no-local-server");
|
|
@@ -10587,9 +9028,9 @@ function buildDaemonCommand() {
|
|
|
10587
9028
|
child.unref();
|
|
10588
9029
|
const deadline = Date.now() + 5e3;
|
|
10589
9030
|
while (Date.now() < deadline) {
|
|
10590
|
-
await
|
|
9031
|
+
await sleep(200);
|
|
10591
9032
|
const pid = readPidFile(paths);
|
|
10592
|
-
if (pid &&
|
|
9033
|
+
if (pid && pidAlive(pid)) {
|
|
10593
9034
|
if (opts.json) printJson({ ok: true, pid, logFile });
|
|
10594
9035
|
else getUI().ok("Daemon started", `pid ${pid} \xB7 logs ${logFile}`);
|
|
10595
9036
|
return;
|
|
@@ -10604,7 +9045,7 @@ function buildDaemonCommand() {
|
|
|
10604
9045
|
const paths = resolvePaths();
|
|
10605
9046
|
const pid = readPidFile(paths);
|
|
10606
9047
|
if (!pid) exitWithError("No daemon.pid found \u2014 daemon does not appear to be running.");
|
|
10607
|
-
if (!
|
|
9048
|
+
if (!pidAlive(pid)) {
|
|
10608
9049
|
clearPidFile(paths);
|
|
10609
9050
|
getUI().warn("Daemon was not alive", `pid ${pid}; cleared stale pid file`);
|
|
10610
9051
|
return;
|
|
@@ -10616,12 +9057,12 @@ function buildDaemonCommand() {
|
|
|
10616
9057
|
}
|
|
10617
9058
|
const deadline = Date.now() + (opts.timeout ?? 5e3);
|
|
10618
9059
|
while (Date.now() < deadline) {
|
|
10619
|
-
if (!
|
|
9060
|
+
if (!pidAlive(pid)) {
|
|
10620
9061
|
clearPidFile(paths);
|
|
10621
9062
|
getUI().ok("Daemon stopped", `pid ${pid}`);
|
|
10622
9063
|
return;
|
|
10623
9064
|
}
|
|
10624
|
-
await
|
|
9065
|
+
await sleep(200);
|
|
10625
9066
|
}
|
|
10626
9067
|
process.stderr.write(`Daemon did not exit within timeout; pid ${pid} may still be alive.
|
|
10627
9068
|
`);
|
|
@@ -10641,7 +9082,7 @@ function buildDaemonCommand() {
|
|
|
10641
9082
|
cmd.command("status").description("Show daemon process + WS state").option("--json", "Output JSON (default)").action(async () => {
|
|
10642
9083
|
const paths = resolvePaths();
|
|
10643
9084
|
const pid = readPidFile(paths);
|
|
10644
|
-
const alive = pid ?
|
|
9085
|
+
const alive = pid ? pidAlive(pid) : false;
|
|
10645
9086
|
if (!pid || !alive) {
|
|
10646
9087
|
printJson({ running: false, pid: pid ?? null, paths: { config: paths.configFile } });
|
|
10647
9088
|
return;
|
|
@@ -10658,8 +9099,8 @@ function buildDaemonCommand() {
|
|
|
10658
9099
|
});
|
|
10659
9100
|
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) => {
|
|
10660
9101
|
const paths = resolvePaths();
|
|
10661
|
-
const logFile =
|
|
10662
|
-
if (!
|
|
9102
|
+
const logFile = join11(paths.logsDir, "daemon.log");
|
|
9103
|
+
if (!existsSync13(logFile)) {
|
|
10663
9104
|
exitWithError(`No daemon log found at ${logFile}. Start the daemon with \`prismer daemon start\`.`);
|
|
10664
9105
|
}
|
|
10665
9106
|
const lines = Math.max(1, opts.tail);
|
|
@@ -10669,9 +9110,9 @@ function buildDaemonCommand() {
|
|
|
10669
9110
|
});
|
|
10670
9111
|
return cmd;
|
|
10671
9112
|
}
|
|
10672
|
-
async function tailFromEnd(
|
|
9113
|
+
async function tailFromEnd(path7, lines) {
|
|
10673
9114
|
const { open } = await import("fs/promises");
|
|
10674
|
-
const handle = await open(
|
|
9115
|
+
const handle = await open(path7, "r");
|
|
10675
9116
|
try {
|
|
10676
9117
|
const stat = await handle.stat();
|
|
10677
9118
|
let size = stat.size;
|
|
@@ -10699,14 +9140,14 @@ async function tailFromEnd(path9, lines) {
|
|
|
10699
9140
|
await handle.close();
|
|
10700
9141
|
}
|
|
10701
9142
|
}
|
|
10702
|
-
async function followFile(
|
|
10703
|
-
let offset =
|
|
9143
|
+
async function followFile(path7) {
|
|
9144
|
+
let offset = statSync3(path7).size;
|
|
10704
9145
|
for (; ; ) {
|
|
10705
|
-
await
|
|
10706
|
-
const size =
|
|
9146
|
+
await sleep(1e3);
|
|
9147
|
+
const size = statSync3(path7).size;
|
|
10707
9148
|
if (size < offset) offset = 0;
|
|
10708
9149
|
if (size === offset) continue;
|
|
10709
|
-
const stream = createReadStream(
|
|
9150
|
+
const stream = createReadStream(path7, { start: offset, end: size - 1, encoding: "utf8" });
|
|
10710
9151
|
for await (const chunk of stream) process.stdout.write(chunk);
|
|
10711
9152
|
offset = size;
|
|
10712
9153
|
}
|
|
@@ -10715,15 +9156,15 @@ async function followFile(path9) {
|
|
|
10715
9156
|
// src/cli/commands/events.ts
|
|
10716
9157
|
init_util();
|
|
10717
9158
|
import { Command as Command9 } from "commander";
|
|
10718
|
-
import { createReadStream as createReadStream2, existsSync as
|
|
10719
|
-
import { homedir as
|
|
10720
|
-
import { join as
|
|
9159
|
+
import { createReadStream as createReadStream2, existsSync as existsSync14 } from "fs";
|
|
9160
|
+
import { homedir as homedir5 } from "os";
|
|
9161
|
+
import { join as join12 } from "path";
|
|
10721
9162
|
import { createInterface } from "readline";
|
|
10722
9163
|
var DEFAULT_LIMIT2 = 50;
|
|
10723
9164
|
function buildEventsCommand() {
|
|
10724
9165
|
return addEventOptions(new Command9("events").description("Read local PARA events from ~/.prismer/para/events.jsonl")).action(async (opts) => {
|
|
10725
9166
|
const file = eventsPath();
|
|
10726
|
-
if (!
|
|
9167
|
+
if (!existsSync14(file)) {
|
|
10727
9168
|
printJson(unavailable(file));
|
|
10728
9169
|
process.exitCode = 1;
|
|
10729
9170
|
return;
|
|
@@ -10740,7 +9181,7 @@ function buildEventsCommand() {
|
|
|
10740
9181
|
function buildEventsStatsCommand() {
|
|
10741
9182
|
return addEventOptions(new Command9("events:stats").description("Summarize local PARA events from ~/.prismer/para/events.jsonl")).action(async (opts) => {
|
|
10742
9183
|
const file = eventsPath();
|
|
10743
|
-
if (!
|
|
9184
|
+
if (!existsSync14(file)) {
|
|
10744
9185
|
printJson(unavailable(file));
|
|
10745
9186
|
process.exitCode = 1;
|
|
10746
9187
|
return;
|
|
@@ -10755,10 +9196,10 @@ function buildEventsStatsCommand() {
|
|
|
10755
9196
|
});
|
|
10756
9197
|
}
|
|
10757
9198
|
function addEventOptions(cmd) {
|
|
10758
|
-
return cmd.option("--limit <n>", "Max events to return/read",
|
|
9199
|
+
return cmd.option("--limit <n>", "Max events to return/read", parsePositiveInt3, 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)");
|
|
10759
9200
|
}
|
|
10760
9201
|
function eventsPath() {
|
|
10761
|
-
return
|
|
9202
|
+
return join12(process.env.PRISMER_HOME ?? join12(homedir5(), ".prismer"), "para", "events.jsonl");
|
|
10762
9203
|
}
|
|
10763
9204
|
function unavailable(file) {
|
|
10764
9205
|
return {
|
|
@@ -10836,15 +9277,15 @@ function normalizeFilters(opts) {
|
|
|
10836
9277
|
function cleanFilters(filters) {
|
|
10837
9278
|
return Object.fromEntries(Object.entries(filters).filter(([, value]) => value !== void 0));
|
|
10838
9279
|
}
|
|
10839
|
-
function
|
|
9280
|
+
function parsePositiveInt3(v) {
|
|
10840
9281
|
const n = Number.parseInt(v, 10);
|
|
10841
9282
|
return Number.isFinite(n) && n > 0 ? n : DEFAULT_LIMIT2;
|
|
10842
9283
|
}
|
|
10843
9284
|
|
|
10844
9285
|
// src/cli/commands/memory.ts
|
|
10845
|
-
import
|
|
9286
|
+
import Database4 from "better-sqlite3";
|
|
10846
9287
|
import { Command as Command10 } from "commander";
|
|
10847
|
-
import { existsSync as
|
|
9288
|
+
import { existsSync as existsSync15 } from "fs";
|
|
10848
9289
|
init_util();
|
|
10849
9290
|
var LOCAL_BASE = process.env.PRISMER_DAEMON_URL ?? "http://127.0.0.1:3210";
|
|
10850
9291
|
function buildMemoryCommand() {
|
|
@@ -10870,7 +9311,7 @@ function buildMemoryCommand() {
|
|
|
10870
9311
|
}));
|
|
10871
9312
|
process.exitCode = 1;
|
|
10872
9313
|
});
|
|
10873
|
-
cmd.command("list").description("List memory records, or local cache records when gateway is unavailable").option("--workspace-id <id>", "Workspace to list (required for daemon RPC)").option("--page-type <type>", "Filter by pageType (hub/leaf/decision/glossary/archive)").option("--limit <n>", "Max items",
|
|
9314
|
+
cmd.command("list").description("List memory records, or local cache records when gateway is unavailable").option("--workspace-id <id>", "Workspace to list (required for daemon RPC)").option("--page-type <type>", "Filter by pageType (hub/leaf/decision/glossary/archive)").option("--limit <n>", "Max items", parsePositiveInt4, 20).option("--json", "Output JSON (default)").action(async (opts) => {
|
|
10874
9315
|
const limit = clampLimit(opts.limit);
|
|
10875
9316
|
const daemonPaths = [];
|
|
10876
9317
|
if (opts.workspaceId) {
|
|
@@ -10889,7 +9330,7 @@ function buildMemoryCommand() {
|
|
|
10889
9330
|
printJson(unavailable2("list", snapshot, { items: snapshot.items, limit }));
|
|
10890
9331
|
process.exitCode = 1;
|
|
10891
9332
|
});
|
|
10892
|
-
cmd.command("search").description("Search memory records, or local cache metadata when gateway is unavailable").argument("[query]", "Search text").option("--query <text>", "Search text").option("--workspace-id <id>", "Workspace to search (required for daemon RPC)").option("--limit <n>", "Max items",
|
|
9333
|
+
cmd.command("search").description("Search memory records, or local cache metadata when gateway is unavailable").argument("[query]", "Search text").option("--query <text>", "Search text").option("--workspace-id <id>", "Workspace to search (required for daemon RPC)").option("--limit <n>", "Max items", parsePositiveInt4, 20).option("--json", "Output JSON (default)").action(async (argQuery, opts) => {
|
|
10893
9334
|
const query = opts.query ?? argQuery ?? "";
|
|
10894
9335
|
const limit = clampLimit(opts.limit);
|
|
10895
9336
|
const encoded = encodeURIComponent(query);
|
|
@@ -10933,9 +9374,9 @@ function buildMemoryCommand() {
|
|
|
10933
9374
|
}
|
|
10934
9375
|
async function tryDaemon(methods, paths) {
|
|
10935
9376
|
for (const method of methods) {
|
|
10936
|
-
for (const
|
|
9377
|
+
for (const path7 of paths) {
|
|
10937
9378
|
try {
|
|
10938
|
-
const res = await fetch(`${LOCAL_BASE}${
|
|
9379
|
+
const res = await fetch(`${LOCAL_BASE}${path7}`, { method, signal: AbortSignal.timeout(1500) });
|
|
10939
9380
|
if (res.status === 404) continue;
|
|
10940
9381
|
const body = await readJson4(res);
|
|
10941
9382
|
if (!res.ok) {
|
|
@@ -10943,12 +9384,12 @@ async function tryDaemon(methods, paths) {
|
|
|
10943
9384
|
ok: false,
|
|
10944
9385
|
error: {
|
|
10945
9386
|
code: `daemon_http_${res.status}`,
|
|
10946
|
-
message: messageFromBody(body) ?? `Daemon endpoint ${method} ${
|
|
9387
|
+
message: messageFromBody(body) ?? `Daemon endpoint ${method} ${path7} returned HTTP ${res.status}`
|
|
10947
9388
|
},
|
|
10948
|
-
checks: { daemon: { url: LOCAL_BASE, endpoint:
|
|
9389
|
+
checks: { daemon: { url: LOCAL_BASE, endpoint: path7, reachable: true } }
|
|
10949
9390
|
};
|
|
10950
9391
|
}
|
|
10951
|
-
return normalizeDaemonBody(body, { method, path:
|
|
9392
|
+
return normalizeDaemonBody(body, { method, path: path7 });
|
|
10952
9393
|
} catch {
|
|
10953
9394
|
continue;
|
|
10954
9395
|
}
|
|
@@ -10996,7 +9437,7 @@ function readCacheSnapshot(limit) {
|
|
|
10996
9437
|
const paths = resolvePaths();
|
|
10997
9438
|
const empty = {
|
|
10998
9439
|
dbPath: paths.localDb,
|
|
10999
|
-
dbExists:
|
|
9440
|
+
dbExists: existsSync15(paths.localDb),
|
|
11000
9441
|
tables: {
|
|
11001
9442
|
cached_assets: { exists: false, count: 0, sizeBytes: 0 },
|
|
11002
9443
|
workspace_files_mirror: { exists: false, count: 0 }
|
|
@@ -11006,7 +9447,7 @@ function readCacheSnapshot(limit) {
|
|
|
11006
9447
|
if (!empty.dbExists) return empty;
|
|
11007
9448
|
let db;
|
|
11008
9449
|
try {
|
|
11009
|
-
db = new
|
|
9450
|
+
db = new Database4(paths.localDb, { readonly: true, fileMustExist: true });
|
|
11010
9451
|
const hasAssets = tableExists(db, "cached_assets");
|
|
11011
9452
|
const hasFiles = tableExists(db, "workspace_files_mirror");
|
|
11012
9453
|
const assets = hasAssets ? db.prepare("SELECT COUNT(*) AS count, COALESCE(SUM(size_bytes), 0) AS sizeBytes FROM cached_assets").get() : { count: 0, sizeBytes: 0 };
|
|
@@ -11095,7 +9536,7 @@ function searchableText(item) {
|
|
|
11095
9536
|
item.localPath
|
|
11096
9537
|
].filter(Boolean).join(" ");
|
|
11097
9538
|
}
|
|
11098
|
-
function
|
|
9539
|
+
function parsePositiveInt4(v) {
|
|
11099
9540
|
const n = Number.parseInt(v, 10);
|
|
11100
9541
|
return Number.isFinite(n) && n > 0 ? n : 20;
|
|
11101
9542
|
}
|
|
@@ -11120,16 +9561,15 @@ import { Command as Command11 } from "commander";
|
|
|
11120
9561
|
// src/pair.ts
|
|
11121
9562
|
import { generateKeyPairSync } from "crypto";
|
|
11122
9563
|
import { hostname as hostname2 } from "os";
|
|
11123
|
-
import { setTimeout as
|
|
9564
|
+
import { setTimeout as sleep2 } from "timers/promises";
|
|
11124
9565
|
import qrcode from "qrcode";
|
|
11125
9566
|
|
|
11126
9567
|
// src/daemon-id.ts
|
|
11127
|
-
import { randomBytes } from "crypto";
|
|
11128
9568
|
import { hostname } from "os";
|
|
11129
9569
|
var PREFIX = "daemon-";
|
|
11130
9570
|
function newDaemonId() {
|
|
11131
9571
|
const host = hostname().replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "unknown";
|
|
11132
|
-
return `${PREFIX}${host}
|
|
9572
|
+
return `${PREFIX}${host}`;
|
|
11133
9573
|
}
|
|
11134
9574
|
|
|
11135
9575
|
// src/pair.ts
|
|
@@ -11200,7 +9640,7 @@ Scan with Lumin to approve, or open: ${offer.qrUrl}
|
|
|
11200
9640
|
const pollIntervalMs = opts.pollIntervalMs ?? 5e3;
|
|
11201
9641
|
for (let i = 0; i < maxAttempts; i += 1) {
|
|
11202
9642
|
if (i > 0 || !localOnlyMode) {
|
|
11203
|
-
await
|
|
9643
|
+
await sleep2(pollIntervalMs);
|
|
11204
9644
|
}
|
|
11205
9645
|
const res = await cloud.request(
|
|
11206
9646
|
"GET",
|
|
@@ -11281,9 +9721,9 @@ function buildPairCommand() {
|
|
|
11281
9721
|
|
|
11282
9722
|
// src/cli/commands/profile.ts
|
|
11283
9723
|
import { Command as Command12 } from "commander";
|
|
11284
|
-
import { existsSync as
|
|
9724
|
+
import { existsSync as existsSync16, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
|
|
11285
9725
|
import { tmpdir } from "os";
|
|
11286
|
-
import { join as
|
|
9726
|
+
import { join as join13 } from "path";
|
|
11287
9727
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
11288
9728
|
|
|
11289
9729
|
// src/templates/roles/product-manager.json
|
|
@@ -11407,12 +9847,12 @@ function buildProfileCommand() {
|
|
|
11407
9847
|
const profile = await cloud.get(
|
|
11408
9848
|
`/api/im/agent_profiles/${encodeURIComponent(profileId)}`
|
|
11409
9849
|
);
|
|
11410
|
-
const tmpFile =
|
|
11411
|
-
|
|
9850
|
+
const tmpFile = join13(tmpdir(), `prismer-profile-${profileId}.json`);
|
|
9851
|
+
writeFileSync8(tmpFile, JSON.stringify(profile.config, null, 2), "utf8");
|
|
11412
9852
|
const editor = process.env.EDITOR || "vi";
|
|
11413
9853
|
const ed = spawnSync4(editor, [tmpFile], { stdio: "inherit" });
|
|
11414
9854
|
if (ed.status !== 0) exitWithError(`editor exited ${ed.status}`, { code: "editor_failed" });
|
|
11415
|
-
const newConfig = JSON.parse(
|
|
9855
|
+
const newConfig = JSON.parse(readFileSync10(tmpFile, "utf8"));
|
|
11416
9856
|
const res = await cloud.request("PATCH", `/api/im/agent_profiles/${encodeURIComponent(profileId)}`, {
|
|
11417
9857
|
body: { config: newConfig, version: profile.version }
|
|
11418
9858
|
});
|
|
@@ -11433,9 +9873,9 @@ function mkCloud4() {
|
|
|
11433
9873
|
}
|
|
11434
9874
|
function readJsonArg(arg) {
|
|
11435
9875
|
if (arg.startsWith("@")) {
|
|
11436
|
-
const
|
|
11437
|
-
if (!
|
|
11438
|
-
return JSON.parse(
|
|
9876
|
+
const path7 = arg.slice(1);
|
|
9877
|
+
if (!existsSync16(path7)) throw new Error(`File not found: ${path7}`);
|
|
9878
|
+
return JSON.parse(readFileSync10(path7, "utf8"));
|
|
11439
9879
|
}
|
|
11440
9880
|
return JSON.parse(arg);
|
|
11441
9881
|
}
|
|
@@ -11446,200 +9886,13 @@ async function resolveDefaultWorkspaceId(cloud) {
|
|
|
11446
9886
|
return def.id;
|
|
11447
9887
|
}
|
|
11448
9888
|
|
|
11449
|
-
// src/cli/commands/reset.ts
|
|
11450
|
-
import { Command as Command13 } from "commander";
|
|
11451
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
11452
|
-
import { existsSync as existsSync18, mkdirSync as mkdirSync10, rmSync, renameSync as renameSync2 } from "fs";
|
|
11453
|
-
import { homedir as homedir9 } from "os";
|
|
11454
|
-
import { dirname as dirname10, join as join20 } from "path";
|
|
11455
|
-
import { setTimeout as sleep4 } from "timers/promises";
|
|
11456
|
-
init_util();
|
|
11457
|
-
init_ui();
|
|
11458
|
-
function buildResetCommand() {
|
|
11459
|
-
return new Command13("reset").description("Stop daemon and reset local runtime state so setup can bind another account").option("--yes", "Actually perform the reset").option("--check", "Print the reset plan without changing files").option("--wipe", "Delete local state instead of archiving it").option("--timeout <ms>", "Time to wait for daemon shutdown (default 5000)", (v) => Number.parseInt(v, 10), 5e3).option("--json", "Output machine-readable JSON").action(runAction(async (opts) => {
|
|
11460
|
-
const paths = resolvePaths();
|
|
11461
|
-
const pid = readPidFile(paths);
|
|
11462
|
-
const runningPid = pid && pidAlive2(pid) ? pid : null;
|
|
11463
|
-
const archivePath = opts.wipe ? null : `${paths.root}.reset.${timestamp()}`;
|
|
11464
|
-
const assetSyncRoot = resolveAssetSyncRoot();
|
|
11465
|
-
const assetSyncArchivePath = opts.wipe ? null : `${assetSyncRoot}.reset.${timestamp()}`;
|
|
11466
|
-
const hermesHome = resolveHermesHome();
|
|
11467
|
-
const hermesArchivePath = opts.wipe ? null : `${hermesHome}.reset.${timestamp()}`;
|
|
11468
|
-
const hermesGatewayPids = findHermesGatewayPids2();
|
|
11469
|
-
const plan = {
|
|
11470
|
-
root: paths.root,
|
|
11471
|
-
exists: existsSync18(paths.root),
|
|
11472
|
-
runningPid,
|
|
11473
|
-
mode: opts.wipe ? "wipe" : "archive",
|
|
11474
|
-
archivePath,
|
|
11475
|
-
assetSyncRoot,
|
|
11476
|
-
assetSyncExists: existsSync18(assetSyncRoot),
|
|
11477
|
-
assetSyncArchivePath,
|
|
11478
|
-
hermesHome,
|
|
11479
|
-
hermesExists: existsSync18(hermesHome),
|
|
11480
|
-
hermesArchivePath,
|
|
11481
|
-
hermesGatewayPids,
|
|
11482
|
-
willStopDaemon: runningPid !== null,
|
|
11483
|
-
nextStep: "prismer setup"
|
|
11484
|
-
};
|
|
11485
|
-
if (opts.check) {
|
|
11486
|
-
printPlan(plan, opts.json);
|
|
11487
|
-
return;
|
|
11488
|
-
}
|
|
11489
|
-
if (!opts.yes) {
|
|
11490
|
-
exitWithError("Refusing to reset without --yes. Run `prismer reset --check` to preview.", {
|
|
11491
|
-
code: "confirmation_required"
|
|
11492
|
-
});
|
|
11493
|
-
}
|
|
11494
|
-
if (runningPid !== null) {
|
|
11495
|
-
await stopDaemon(runningPid, opts.timeout ?? 5e3);
|
|
11496
|
-
clearPidFile(paths);
|
|
11497
|
-
} else if (pid) {
|
|
11498
|
-
clearPidFile(paths);
|
|
11499
|
-
}
|
|
11500
|
-
for (const gatewayPid of hermesGatewayPids) {
|
|
11501
|
-
await stopProcess(gatewayPid, opts.timeout ?? 5e3, "Hermes gateway");
|
|
11502
|
-
}
|
|
11503
|
-
if (existsSync18(paths.root)) {
|
|
11504
|
-
if (opts.wipe) {
|
|
11505
|
-
rmSync(paths.root, { recursive: true, force: true });
|
|
11506
|
-
} else {
|
|
11507
|
-
if (!archivePath) throw new Error("internal: archivePath missing");
|
|
11508
|
-
if (!existsSync18(dirname10(archivePath))) {
|
|
11509
|
-
mkdirSync10(dirname10(archivePath), { recursive: true });
|
|
11510
|
-
}
|
|
11511
|
-
renameSync2(paths.root, archivePath);
|
|
11512
|
-
}
|
|
11513
|
-
}
|
|
11514
|
-
if (existsSync18(assetSyncRoot)) {
|
|
11515
|
-
if (opts.wipe) {
|
|
11516
|
-
rmSync(assetSyncRoot, { recursive: true, force: true });
|
|
11517
|
-
} else {
|
|
11518
|
-
if (!assetSyncArchivePath) throw new Error("internal: assetSyncArchivePath missing");
|
|
11519
|
-
if (!existsSync18(dirname10(assetSyncArchivePath))) {
|
|
11520
|
-
mkdirSync10(dirname10(assetSyncArchivePath), { recursive: true });
|
|
11521
|
-
}
|
|
11522
|
-
renameSync2(assetSyncRoot, assetSyncArchivePath);
|
|
11523
|
-
}
|
|
11524
|
-
}
|
|
11525
|
-
if (existsSync18(hermesHome)) {
|
|
11526
|
-
if (opts.wipe) {
|
|
11527
|
-
rmSync(hermesHome, { recursive: true, force: true });
|
|
11528
|
-
} else {
|
|
11529
|
-
if (!hermesArchivePath) throw new Error("internal: hermesArchivePath missing");
|
|
11530
|
-
if (!existsSync18(dirname10(hermesArchivePath))) {
|
|
11531
|
-
mkdirSync10(dirname10(hermesArchivePath), { recursive: true });
|
|
11532
|
-
}
|
|
11533
|
-
renameSync2(hermesHome, hermesArchivePath);
|
|
11534
|
-
}
|
|
11535
|
-
}
|
|
11536
|
-
mkdirSync10(paths.root, { recursive: true });
|
|
11537
|
-
if (opts.json) {
|
|
11538
|
-
printJson({ ok: true, reset: true, plan });
|
|
11539
|
-
return;
|
|
11540
|
-
}
|
|
11541
|
-
const ui = getUI();
|
|
11542
|
-
ui.ok("Runtime reset complete", paths.root);
|
|
11543
|
-
if (archivePath) ui.line(` archived: ${archivePath}`);
|
|
11544
|
-
if (assetSyncArchivePath && plan.assetSyncExists) ui.line(` asset sync archived: ${assetSyncArchivePath}`);
|
|
11545
|
-
if (hermesArchivePath && plan.hermesExists) ui.line(` Hermes archived: ${hermesArchivePath}`);
|
|
11546
|
-
if (hermesGatewayPids.length > 0) ui.line(` stopped Hermes gateway pid(s): ${hermesGatewayPids.join(", ")}`);
|
|
11547
|
-
ui.line(" next: prismer setup");
|
|
11548
|
-
}, { code: "reset_failed" }));
|
|
11549
|
-
}
|
|
11550
|
-
function printPlan(plan, json) {
|
|
11551
|
-
if (json) {
|
|
11552
|
-
printJson({ ok: true, check: true, plan });
|
|
11553
|
-
return;
|
|
11554
|
-
}
|
|
11555
|
-
const ui = getUI();
|
|
11556
|
-
ui.header("Runtime reset plan");
|
|
11557
|
-
ui.blank();
|
|
11558
|
-
ui.line(` root: ${plan.root}`);
|
|
11559
|
-
ui.line(` exists: ${plan.exists ? "yes" : "no"}`);
|
|
11560
|
-
ui.line(` daemon: ${plan.runningPid ? `running pid ${plan.runningPid}` : "not running"}`);
|
|
11561
|
-
ui.line(` mode: ${plan.mode}`);
|
|
11562
|
-
if (plan.archivePath) ui.line(` archive: ${plan.archivePath}`);
|
|
11563
|
-
ui.line(` asset sync: ${plan.assetSyncExists ? plan.assetSyncRoot : `${plan.assetSyncRoot} (not found)`}`);
|
|
11564
|
-
if (plan.assetSyncArchivePath) ui.line(` asset archive: ${plan.assetSyncArchivePath}`);
|
|
11565
|
-
ui.line(` Hermes: ${plan.hermesExists ? plan.hermesHome : `${plan.hermesHome} (not found)`}`);
|
|
11566
|
-
if (plan.hermesArchivePath) ui.line(` Hermes archive: ${plan.hermesArchivePath}`);
|
|
11567
|
-
ui.line(
|
|
11568
|
-
` Hermes gateway: ${plan.hermesGatewayPids.length > 0 ? `running pid(s) ${plan.hermesGatewayPids.join(", ")}` : "not running"}`
|
|
11569
|
-
);
|
|
11570
|
-
ui.blank();
|
|
11571
|
-
ui.line("Run `prismer reset --yes` to apply, then `prismer setup` to bind a new account.");
|
|
11572
|
-
}
|
|
11573
|
-
async function stopDaemon(pid, timeoutMs) {
|
|
11574
|
-
await stopProcess(pid, timeoutMs, "daemon");
|
|
11575
|
-
}
|
|
11576
|
-
async function stopProcess(pid, timeoutMs, label) {
|
|
11577
|
-
try {
|
|
11578
|
-
process.kill(pid, "SIGTERM");
|
|
11579
|
-
} catch (err) {
|
|
11580
|
-
throw new Error(`SIGTERM failed for ${label} pid ${pid}: ${err.message}`);
|
|
11581
|
-
}
|
|
11582
|
-
const deadline = Date.now() + timeoutMs;
|
|
11583
|
-
while (Date.now() < deadline) {
|
|
11584
|
-
if (!pidAlive2(pid)) return;
|
|
11585
|
-
await sleep4(100);
|
|
11586
|
-
}
|
|
11587
|
-
try {
|
|
11588
|
-
process.kill(pid, "SIGKILL");
|
|
11589
|
-
} catch (err) {
|
|
11590
|
-
throw new Error(`SIGKILL failed for ${label} pid ${pid}: ${err.message}`);
|
|
11591
|
-
}
|
|
11592
|
-
const killDeadline = Date.now() + 1e3;
|
|
11593
|
-
while (Date.now() < killDeadline) {
|
|
11594
|
-
if (!pidAlive2(pid)) return;
|
|
11595
|
-
await sleep4(100);
|
|
11596
|
-
}
|
|
11597
|
-
throw new Error(`${label} pid ${pid} did not exit within ${timeoutMs + 1e3}ms`);
|
|
11598
|
-
}
|
|
11599
|
-
function timestamp() {
|
|
11600
|
-
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
11601
|
-
}
|
|
11602
|
-
function resolveAssetSyncRoot() {
|
|
11603
|
-
const override = process.env.PRISMER_ASSET_SYNC_ROOT;
|
|
11604
|
-
if (override) return override;
|
|
11605
|
-
const home = homedir9();
|
|
11606
|
-
const desktop = join20(home, "Desktop");
|
|
11607
|
-
const base = existsSync18(desktop) ? desktop : home;
|
|
11608
|
-
return join20(base, "Prismer Assets");
|
|
11609
|
-
}
|
|
11610
|
-
function resolveHermesHome() {
|
|
11611
|
-
return process.env.HERMES_HOME || join20(homedir9(), ".hermes");
|
|
11612
|
-
}
|
|
11613
|
-
function findHermesGatewayPids2() {
|
|
11614
|
-
try {
|
|
11615
|
-
const out = execFileSync2("ps", ["-axo", "pid=,command="], { encoding: "utf8" });
|
|
11616
|
-
const current = process.pid;
|
|
11617
|
-
return out.split(/\r?\n/).map((line) => {
|
|
11618
|
-
const trimmed = line.trim();
|
|
11619
|
-
const match = /^(\d+)\s+(.+)$/.exec(trimmed);
|
|
11620
|
-
if (!match) return null;
|
|
11621
|
-
const pid = Number.parseInt(match[1], 10);
|
|
11622
|
-
const command = match[2];
|
|
11623
|
-
if (!Number.isFinite(pid) || pid === current) return null;
|
|
11624
|
-
if (!isHermesGatewayCommand2(command)) return null;
|
|
11625
|
-
return pid;
|
|
11626
|
-
}).filter((pid) => pid !== null);
|
|
11627
|
-
} catch {
|
|
11628
|
-
return [];
|
|
11629
|
-
}
|
|
11630
|
-
}
|
|
11631
|
-
function isHermesGatewayCommand2(command) {
|
|
11632
|
-
if (!/\bgateway\b/.test(command) || !/\brun\b/.test(command)) return false;
|
|
11633
|
-
return /(^|\s)(?:\S*\/)?hermes(?:\s|$)/.test(command);
|
|
11634
|
-
}
|
|
11635
|
-
|
|
11636
9889
|
// src/cli/commands/sandbox.ts
|
|
11637
|
-
import { Command as
|
|
9890
|
+
import { Command as Command13 } from "commander";
|
|
11638
9891
|
init_util();
|
|
11639
9892
|
init_ui();
|
|
11640
9893
|
function buildSandboxCommand() {
|
|
11641
|
-
const cmd = new
|
|
11642
|
-
cmd.command("list").description("List sandbox containers in a workspace").requiredOption("--workspace-id <id>", "Workspace id").option("--status <status>", "Status filter").option("--limit <n>", "Max rows",
|
|
9894
|
+
const cmd = new Command13("sandbox").description("Inspect and smoke-test sandbox lifecycle");
|
|
9895
|
+
cmd.command("list").description("List sandbox containers in a workspace").requiredOption("--workspace-id <id>", "Workspace id").option("--status <status>", "Status filter").option("--limit <n>", "Max rows", parsePositiveInt5, 50).option("--json", "Output machine-readable JSON").action(runAction(async (opts) => {
|
|
11643
9896
|
const query = new URLSearchParams({ workspaceId: opts.workspaceId, limit: String(opts.limit) });
|
|
11644
9897
|
if (opts.status) query.set("status", opts.status);
|
|
11645
9898
|
const body = await cloudRequest("GET", `/api/sandboxes?${query.toString()}`);
|
|
@@ -11698,7 +9951,7 @@ function buildSandboxCommand() {
|
|
|
11698
9951
|
}
|
|
11699
9952
|
printSnapshotResult(id, body);
|
|
11700
9953
|
}, { code: "sandbox_snapshot_failed" }));
|
|
11701
|
-
cmd.command("runCmd <id> <command...>").description("Run a command in a sandbox container").option("--timeout-ms <ms>", "Command timeout",
|
|
9954
|
+
cmd.command("runCmd <id> <command...>").description("Run a command in a sandbox container").option("--timeout-ms <ms>", "Command timeout", parsePositiveInt5, 6e4).option("--json", "Output machine-readable JSON").allowUnknownOption(true).action(runAction(async (id, command, opts) => {
|
|
11702
9955
|
if (!Array.isArray(command) || command.length === 0) exitWithError("runCmd requires a command after --", { code: "invalid_argument" });
|
|
11703
9956
|
const body = await cloudRequest("POST", `/api/sandboxes/${encodeURIComponent(id)}/runCmd`, {
|
|
11704
9957
|
command,
|
|
@@ -11739,12 +9992,12 @@ function errorMessage2(raw) {
|
|
|
11739
9992
|
if (typeof raw.error === "string") return raw.error;
|
|
11740
9993
|
return raw.error?.message ?? "request failed";
|
|
11741
9994
|
}
|
|
11742
|
-
async function cloudRequest(method,
|
|
9995
|
+
async function cloudRequest(method, path7, body) {
|
|
11743
9996
|
const cloud = mkCloud5();
|
|
11744
|
-
const res = await cloud.request(method,
|
|
11745
|
-
if (!res.ok) exitWithError(`${method} ${
|
|
9997
|
+
const res = await cloud.request(method, path7, body === void 0 ? void 0 : { body });
|
|
9998
|
+
if (!res.ok) exitWithError(`${method} ${path7} failed (${res.status}): ${res.error?.message ?? "request failed"}`);
|
|
11746
9999
|
const responseBody = res.data;
|
|
11747
|
-
if (isErrorEnvelope2(responseBody)) exitWithError(`${method} ${
|
|
10000
|
+
if (isErrorEnvelope2(responseBody)) exitWithError(`${method} ${path7} failed (${res.status}): ${errorMessage2(responseBody)}`);
|
|
11748
10001
|
return responseBody;
|
|
11749
10002
|
}
|
|
11750
10003
|
function printSandboxList(raw) {
|
|
@@ -11819,7 +10072,7 @@ function writeLine2(label, value) {
|
|
|
11819
10072
|
if (value === void 0 || value === null || value === "") return;
|
|
11820
10073
|
getUI().line(` ${label}: ${String(value)}`);
|
|
11821
10074
|
}
|
|
11822
|
-
function
|
|
10075
|
+
function parsePositiveInt5(value) {
|
|
11823
10076
|
const parsed = Number.parseInt(value, 10);
|
|
11824
10077
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
11825
10078
|
throw new Error("expected a positive integer");
|
|
@@ -11837,16 +10090,16 @@ async function safeParseResponse(res) {
|
|
|
11837
10090
|
}
|
|
11838
10091
|
|
|
11839
10092
|
// src/cli/commands/setup.ts
|
|
11840
|
-
import { Command as
|
|
10093
|
+
import { Command as Command14 } from "commander";
|
|
11841
10094
|
import { hostname as hostname3 } from "os";
|
|
11842
10095
|
import { spawn as spawn6 } from "child_process";
|
|
11843
|
-
import { randomBytes
|
|
11844
|
-
import { existsSync as
|
|
10096
|
+
import { randomBytes } from "crypto";
|
|
10097
|
+
import { existsSync as existsSync17, renameSync } from "fs";
|
|
11845
10098
|
import { createServer as createServer2 } from "http";
|
|
11846
10099
|
init_util();
|
|
11847
10100
|
init_ui();
|
|
11848
10101
|
function buildSetupCommand() {
|
|
11849
|
-
return new
|
|
10102
|
+
return new Command14("setup").description("Set up this local runtime and bind it to Prismer Cloud").argument("[api-key]", "Prismer daemon API key; primarily for manual recovery and automation").option(
|
|
11850
10103
|
"--cloud <url>",
|
|
11851
10104
|
`Cloud base URL (default: ${DEFAULT_CLOUD_BASE_URL}; bare host:port is auto-prefixed http://)`,
|
|
11852
10105
|
(v) => v
|
|
@@ -11859,7 +10112,7 @@ function buildSetupCommand() {
|
|
|
11859
10112
|
} catch (err) {
|
|
11860
10113
|
exitWithError(err.message, { code: "invalid_cloud_url" });
|
|
11861
10114
|
}
|
|
11862
|
-
let apiKey = apiKeyArg ??
|
|
10115
|
+
let apiKey = apiKeyArg ?? process.env.PRISMER_API_KEY;
|
|
11863
10116
|
const authToken = opts.token ?? process.env.PRISMER_AUTH_TOKEN;
|
|
11864
10117
|
if (!opts.json) {
|
|
11865
10118
|
printBanner({ compact: true });
|
|
@@ -11867,6 +10120,7 @@ function buildSetupCommand() {
|
|
|
11867
10120
|
getUI().blank();
|
|
11868
10121
|
}
|
|
11869
10122
|
const shouldStart = opts.start !== false;
|
|
10123
|
+
stopRunningDaemon(paths);
|
|
11870
10124
|
if (opts.pair || opts.asUser) {
|
|
11871
10125
|
if (!opts.json) warn("Legacy pair setup path", "plain `prismer setup --start` is the canonical runtime binding flow");
|
|
11872
10126
|
if (opts.asUser && process.env.LOCAL_ONLY !== "1") {
|
|
@@ -11957,11 +10211,10 @@ function buildSetupCommand() {
|
|
|
11957
10211
|
return;
|
|
11958
10212
|
}
|
|
11959
10213
|
const previousConfig = configExists(paths) ? loadConfig(paths) : null;
|
|
11960
|
-
const daemonId = previousConfig && previousConfig.api_key === apiKey && previousConfig.cloud_api_base === cloudBaseUrl ? previousConfig.daemon_id : newDaemonId();
|
|
11961
10214
|
const config = {
|
|
11962
10215
|
api_key: apiKey,
|
|
11963
10216
|
cloud_api_base: cloudBaseUrl,
|
|
11964
|
-
daemon_id:
|
|
10217
|
+
daemon_id: newDaemonId()
|
|
11965
10218
|
};
|
|
11966
10219
|
if (previousConfig && shouldArchiveLocalDb(previousConfig, config)) {
|
|
11967
10220
|
archiveLocalDb(paths.localDb);
|
|
@@ -11991,7 +10244,7 @@ function buildSetupCommand() {
|
|
|
11991
10244
|
}, { code: "setup_failed" }));
|
|
11992
10245
|
}
|
|
11993
10246
|
async function runBrowserSetup(input) {
|
|
11994
|
-
const state =
|
|
10247
|
+
const state = randomBytes(18).toString("base64url");
|
|
11995
10248
|
const server = createServer2();
|
|
11996
10249
|
const apiKeyPromise = waitForSetupCallback(server, state);
|
|
11997
10250
|
await new Promise((resolve3, reject) => {
|
|
@@ -12088,13 +10341,35 @@ function startDaemonDetached(home) {
|
|
|
12088
10341
|
});
|
|
12089
10342
|
child.unref();
|
|
12090
10343
|
}
|
|
10344
|
+
function stopRunningDaemon(paths) {
|
|
10345
|
+
const pid = readPidFile(paths);
|
|
10346
|
+
if (!pid || !pidAlive(pid)) {
|
|
10347
|
+
if (pid) clearPidFile(paths);
|
|
10348
|
+
return;
|
|
10349
|
+
}
|
|
10350
|
+
try {
|
|
10351
|
+
process.kill(pid, "SIGTERM");
|
|
10352
|
+
} catch {
|
|
10353
|
+
return;
|
|
10354
|
+
}
|
|
10355
|
+
const deadline = Date.now() + 5e3;
|
|
10356
|
+
while (Date.now() < deadline) {
|
|
10357
|
+
if (!pidAlive(pid)) {
|
|
10358
|
+
clearPidFile(paths);
|
|
10359
|
+
return;
|
|
10360
|
+
}
|
|
10361
|
+
const start = Date.now();
|
|
10362
|
+
while (Date.now() - start < 200) {
|
|
10363
|
+
}
|
|
10364
|
+
}
|
|
10365
|
+
}
|
|
12091
10366
|
function shouldArchiveLocalDb(previous, next) {
|
|
12092
10367
|
return previous.api_key !== next.api_key || previous.cloud_api_base !== next.cloud_api_base || previous.daemon_id !== next.daemon_id;
|
|
12093
10368
|
}
|
|
12094
10369
|
function archiveLocalDb(localDbPath) {
|
|
12095
|
-
if (!
|
|
10370
|
+
if (!existsSync17(localDbPath)) return;
|
|
12096
10371
|
const archived = `${localDbPath}.${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
|
|
12097
|
-
|
|
10372
|
+
renameSync(localDbPath, archived);
|
|
12098
10373
|
}
|
|
12099
10374
|
async function mintDaemonApiKey(input) {
|
|
12100
10375
|
const label = `Daemon: ${input.deviceName} ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
|
|
@@ -12114,12 +10389,11 @@ async function mintDaemonApiKey(input) {
|
|
|
12114
10389
|
}
|
|
12115
10390
|
|
|
12116
10391
|
// src/cli/commands/status.ts
|
|
12117
|
-
import { Command as
|
|
12118
|
-
import { hostname as hostname4 } from "os";
|
|
10392
|
+
import { Command as Command15 } from "commander";
|
|
12119
10393
|
init_util();
|
|
12120
10394
|
init_ui();
|
|
12121
10395
|
function buildStatusCommand() {
|
|
12122
|
-
return new
|
|
10396
|
+
return new Command15("status").description("Show daemon + config + cloud status").option("--json", "Output machine-readable JSON").action(async (opts) => {
|
|
12123
10397
|
const paths = resolvePaths();
|
|
12124
10398
|
if (!configExists(paths)) {
|
|
12125
10399
|
const report2 = { configured: false, paired: false, paths: { config: paths.configFile } };
|
|
@@ -12187,7 +10461,11 @@ function buildStatusCommand() {
|
|
|
12187
10461
|
info: daemonStatus.info ?? {}
|
|
12188
10462
|
},
|
|
12189
10463
|
cloud: { base: cfg.cloud_api_base, reachable: cloudOk, me, devices, agents },
|
|
12190
|
-
local
|
|
10464
|
+
local,
|
|
10465
|
+
binding: {
|
|
10466
|
+
daemonId: cfg.daemon_id,
|
|
10467
|
+
apiKey: cfg.api_key
|
|
10468
|
+
}
|
|
12191
10469
|
};
|
|
12192
10470
|
if (opts.json) {
|
|
12193
10471
|
printJson(report);
|
|
@@ -12210,8 +10488,8 @@ async function readDaemonStatus() {
|
|
|
12210
10488
|
const paths = resolvePaths();
|
|
12211
10489
|
const pid = readPidFile(paths);
|
|
12212
10490
|
if (pid) {
|
|
12213
|
-
const { pidAlive:
|
|
12214
|
-
if (
|
|
10491
|
+
const { pidAlive: pidAlive2 } = await Promise.resolve().then(() => (init_util(), util_exports));
|
|
10492
|
+
if (pidAlive2(pid)) return { running: true, pid };
|
|
12215
10493
|
}
|
|
12216
10494
|
return { running: false };
|
|
12217
10495
|
}
|
|
@@ -12239,9 +10517,8 @@ function printPretty(report) {
|
|
|
12239
10517
|
ui.blank();
|
|
12240
10518
|
ok("Config", report.paths.config);
|
|
12241
10519
|
if (report.daemon.running) {
|
|
12242
|
-
const daemonName = report.cloud.me && typeof report.cloud.me === "object" ? report.cloud.me.user?.username ?? hostname4() : hostname4();
|
|
12243
10520
|
const ws = report.daemon.wsConnected ? "connected" : "pending";
|
|
12244
|
-
ok("Daemon",
|
|
10521
|
+
ok("Daemon", `pid=${report.daemon.pid} ws=${ws}`);
|
|
12245
10522
|
if (report.daemon.info) {
|
|
12246
10523
|
const info2 = report.daemon.info;
|
|
12247
10524
|
if (info2.version) ui.line(` Version: ${info2.version}`);
|
|
@@ -12256,7 +10533,8 @@ function printPretty(report) {
|
|
|
12256
10533
|
ok("Cloud", report.cloud.base);
|
|
12257
10534
|
const me = report.cloud.me;
|
|
12258
10535
|
if (me?.user) {
|
|
12259
|
-
|
|
10536
|
+
const roleTag = me.user.role ? ` role=${me.user.role}` : "";
|
|
10537
|
+
ui.line(` Account: ${me.user.displayName ?? me.user.username ?? "?"}${roleTag}`);
|
|
12260
10538
|
}
|
|
12261
10539
|
if (me?.credits) {
|
|
12262
10540
|
ui.line(` Credits: ${typeof me.credits.balance === "number" ? me.credits.balance.toLocaleString() : "?"}`);
|
|
@@ -12265,6 +10543,12 @@ function printPretty(report) {
|
|
|
12265
10543
|
fail("Cloud", `${report.cloud.base} unreachable or unauthorized`);
|
|
12266
10544
|
tip("prismer setup --force");
|
|
12267
10545
|
}
|
|
10546
|
+
if (report.binding) {
|
|
10547
|
+
ui.blank();
|
|
10548
|
+
ui.line(` Daemon ID: ${report.binding.daemonId}`);
|
|
10549
|
+
const masked = report.binding.apiKey.slice(0, 14) + "\u2022\u2022\u2022\u2022" + report.binding.apiKey.slice(-4);
|
|
10550
|
+
ui.line(` API Key: ${masked}`);
|
|
10551
|
+
}
|
|
12268
10552
|
if (Array.isArray(report.cloud.devices) && report.cloud.devices.length > 0) {
|
|
12269
10553
|
const devs = report.cloud.devices;
|
|
12270
10554
|
ui.blank();
|
|
@@ -12275,6 +10559,8 @@ function printPretty(report) {
|
|
|
12275
10559
|
const declared = d.hostedAgentSummary?.declared ?? 0;
|
|
12276
10560
|
ui.line(` ${statusIcon} ${d.podName?.replace(/^daemon:/, "")?.slice(0, 28) ?? "?"} kind=${kind} agents=${declared}`);
|
|
12277
10561
|
}
|
|
10562
|
+
} else {
|
|
10563
|
+
ui.line(` Workspace Devices: none`);
|
|
12278
10564
|
}
|
|
12279
10565
|
if (Array.isArray(report.cloud.agents) && report.cloud.agents.length > 0) {
|
|
12280
10566
|
ui.line(` Hosted agents: ${report.cloud.agents.length}`);
|
|
@@ -12290,14 +10576,14 @@ function printPretty(report) {
|
|
|
12290
10576
|
}
|
|
12291
10577
|
|
|
12292
10578
|
// src/cli/commands/task.ts
|
|
12293
|
-
import { Command as
|
|
12294
|
-
import { setTimeout as
|
|
10579
|
+
import { Command as Command16 } from "commander";
|
|
10580
|
+
import { setTimeout as sleep3 } from "timers/promises";
|
|
12295
10581
|
init_util();
|
|
12296
10582
|
function describeStatus(status) {
|
|
12297
10583
|
return status === 0 ? "network error" : `HTTP ${status}`;
|
|
12298
10584
|
}
|
|
12299
10585
|
function buildTaskCommand() {
|
|
12300
|
-
const cmd = new
|
|
10586
|
+
const cmd = new Command16("task").description("Manage tasks");
|
|
12301
10587
|
cmd.command("create").description("Create + dispatch a task to the given agent and wait for completion").requiredOption("--agent <imUserId>", "Assignee agent IMUser.id").requiredOption("--prompt <text>", "Prompt to send to the adapter").option("--profile <profileId>", "AgentProfile id (cloud picks first if omitted)").option("--capability <name>", "Capability tag", "chat").option("--title <text>", "Task title (defaults to prompt prefix)").option("--timeout-ms <ms>", "Per-task timeout", (v) => Number.parseInt(v, 10)).option("--no-wait", "Just create + print task id; don't poll").option("--json", "Output JSON (default)").action(runAction(async (opts) => {
|
|
12302
10588
|
const cloud = mkCloud6();
|
|
12303
10589
|
const body = {
|
|
@@ -12374,7 +10660,7 @@ function unwrap2(raw) {
|
|
|
12374
10660
|
async function pollUntilTerminal(cloud, taskId, timeoutMs) {
|
|
12375
10661
|
const deadline = Date.now() + (timeoutMs ?? 5 * 6e4);
|
|
12376
10662
|
while (Date.now() < deadline) {
|
|
12377
|
-
await
|
|
10663
|
+
await sleep3(1e3);
|
|
12378
10664
|
const res = await cloud.request(
|
|
12379
10665
|
"GET",
|
|
12380
10666
|
`/api/im/tasks/${encodeURIComponent(taskId)}`
|
|
@@ -12411,11 +10697,11 @@ function taskFrom2(raw) {
|
|
|
12411
10697
|
}
|
|
12412
10698
|
|
|
12413
10699
|
// src/cli/commands/workspace.ts
|
|
12414
|
-
import { Command as
|
|
10700
|
+
import { Command as Command17 } from "commander";
|
|
12415
10701
|
init_util();
|
|
12416
10702
|
init_ui();
|
|
12417
10703
|
function buildWorkspaceCommand() {
|
|
12418
|
-
const cmd = new
|
|
10704
|
+
const cmd = new Command17("workspace").description("Manage workspaces, runtime snapshots, and workspace files");
|
|
12419
10705
|
cmd.command("list").description("List workspaces").option("--json", "Output machine-readable JSON").action(runAction(async (opts) => {
|
|
12420
10706
|
const cloud = mkCloud7();
|
|
12421
10707
|
const data = await cloud.get("/api/im/workspaces");
|
|
@@ -12649,15 +10935,18 @@ async function readResponseError(res) {
|
|
|
12649
10935
|
|
|
12650
10936
|
// src/cli/index.ts
|
|
12651
10937
|
init_ui();
|
|
12652
|
-
|
|
10938
|
+
|
|
10939
|
+
// package.json
|
|
10940
|
+
var version = "1.9.22";
|
|
10941
|
+
|
|
10942
|
+
// src/cli/index.ts
|
|
12653
10943
|
function buildProgram() {
|
|
12654
|
-
const program = new
|
|
10944
|
+
const program = new Command18("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(version);
|
|
12655
10945
|
program.addCommand(buildBannerCommand());
|
|
12656
10946
|
program.addCommand(buildSetupCommand());
|
|
12657
10947
|
program.addCommand(buildConfigCommand());
|
|
12658
10948
|
program.addCommand(buildDaemonCommand());
|
|
12659
10949
|
program.addCommand(buildPairCommand());
|
|
12660
|
-
program.addCommand(buildResetCommand());
|
|
12661
10950
|
program.addCommand(buildStatusCommand());
|
|
12662
10951
|
program.addCommand(buildAdapterCommand());
|
|
12663
10952
|
program.addCommand(buildAgentCommand());
|