@prismer/runtime 1.9.6 → 1.9.21

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 CHANGED
@@ -6,6 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
9
16
  var __copyProps = (to, from, except, desc) => {
10
17
  if (from && typeof from === "object" || typeof from === "function") {
11
18
  for (let key of __getOwnPropNames(from))
@@ -23,190 +30,783 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
30
  mod
24
31
  ));
25
32
 
26
- // src/cli/index.ts
27
- var import_commander18 = require("commander");
28
-
29
- // src/cli/commands/adapter.ts
30
- var import_node_child_process4 = require("child_process");
31
- var import_node_fs4 = require("fs");
32
- var import_node_os3 = require("os");
33
- var import_node_path4 = require("path");
34
- var import_commander = require("commander");
35
-
36
- // src/adapters/claude-code/index.ts
37
- var import_node_child_process = require("child_process");
38
- var import_zod = require("zod");
39
-
40
- // src/adapters/claude-code/output-parser.ts
41
- var MAX_OUTPUT_CHARS = 64 * 1024;
42
- function parseHeadlessOutput(stdout) {
43
- let out = "";
44
- let toolCalls = 0;
45
- for (const line of stdout.split("\n")) {
46
- const trimmed = line.trim();
47
- if (!trimmed) continue;
48
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
49
- try {
50
- const obj = JSON.parse(trimmed);
51
- if (obj.event === "tool_call" || obj.tool) {
52
- toolCalls += 1;
53
- continue;
54
- }
55
- if (typeof obj.text === "string") {
56
- out += obj.text;
57
- continue;
58
- }
59
- } catch {
60
- }
61
- }
62
- out += line + "\n";
33
+ // src/cli/ui.ts
34
+ function thisDirname() {
35
+ try {
36
+ return path.dirname((0, import_node_url.fileURLToPath)(import_meta2.url));
37
+ } catch {
38
+ return process.cwd();
63
39
  }
64
- const truncated = out.length > MAX_OUTPUT_CHARS;
65
- if (truncated) out = out.slice(0, MAX_OUTPUT_CHARS) + "\n\u2026[truncated]";
66
- return { output: out.trim(), toolCalls, truncated };
67
40
  }
68
-
69
- // src/adapters/claude-code/index.ts
70
- var McpServerSchema = import_zod.z.object({
71
- name: import_zod.z.string(),
72
- command: import_zod.z.string(),
73
- args: import_zod.z.array(import_zod.z.string()).optional(),
74
- env: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional()
75
- });
76
- var CCConfigSchema = import_zod.z.object({
77
- cwd: import_zod.z.string().min(1),
78
- // claude 2.x accepts aliases ('sonnet', 'haiku', 'opus') and full ids
79
- // (e.g. 'claude-sonnet-4-6'). The 1.x default 'claude-3-5-sonnet' is no
80
- // longer a valid model id under the current CLI.
81
- model: import_zod.z.string().default("sonnet"),
82
- systemPrompt: import_zod.z.string().optional(),
83
- envVars: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
84
- mcpServers: import_zod.z.array(McpServerSchema).optional(),
85
- allowedTools: import_zod.z.array(import_zod.z.string()).optional(),
86
- maxTurns: import_zod.z.number().int().positive().default(20),
87
- // External-model route (1.9.x extension — optional, no breaking change)
88
- baseURL: import_zod.z.string().url().optional(),
89
- apiKeyRef: import_zod.z.string().regex(/^(env|keychain):[A-Za-z0-9_][A-Za-z0-9_.\-]*$/).optional(),
90
- // informational tag — dispatch does not branch on this value yet
91
- route: import_zod.z.enum(["default", "prismer", "omniroute"]).default("default")
92
- });
93
- var claudeCodeAdapter = {
94
- name: "claude-code",
95
- kind: "interactive",
96
- capabilities: ["shell", "code", "mcp", "edit"],
97
- workspaceSchema: CCConfigSchema,
98
- validate(config) {
99
- const r = CCConfigSchema.safeParse(config);
100
- if (r.success) return { ok: true };
101
- return {
102
- ok: false,
103
- errors: r.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`)
104
- };
105
- },
106
- async dispatch(profile, task) {
107
- const config = CCConfigSchema.parse(profile.config);
108
- const args = ["--print"];
109
- if (config.model) {
110
- args.push("--model", config.model);
111
- }
112
- if (config.allowedTools && config.allowedTools.length > 0) {
113
- args.push("--allowed-tools", config.allowedTools.join(","));
114
- }
115
- if (config.systemPrompt) {
116
- args.push("--system-prompt", config.systemPrompt);
117
- }
118
- args.push(task.prompt);
119
- const startedAt = Date.now();
120
- const env = { ...process.env, ...config.envVars ?? {} };
121
- if (config.baseURL) {
122
- env.ANTHROPIC_API_BASE = config.baseURL;
41
+ function findIconPath(size = "big") {
42
+ const name = size === "big" ? "icon" : "smallicon";
43
+ const here = thisDirname();
44
+ const candidates = [
45
+ // npm-installed: node_modules/@prismer/runtime/dist/cli.js → ../assets
46
+ path.resolve(here, "../assets", name),
47
+ // alternate dist layout (sub-bundle): dist/bin/cli.js → ../../assets
48
+ path.resolve(here, "../../assets", name),
49
+ // source/typecheck: src/cli/ui.ts → ../../assets
50
+ path.resolve(here, "../../assets", name),
51
+ // dev mode: cwd happens to be runtime root
52
+ path.resolve(process.cwd(), "assets", name),
53
+ path.resolve(process.cwd(), "sdk/prismer-cloud/runtime/assets", name)
54
+ ];
55
+ for (const candidate of candidates) {
56
+ try {
57
+ if (fs.existsSync(candidate)) return candidate;
58
+ } catch {
123
59
  }
124
- const outboxDir = typeof task.metadata?.prismerOutboxDir === "string" ? task.metadata.prismerOutboxDir : void 0;
125
- if (outboxDir) {
126
- env.PRISMER_OUTBOX_DIR = outboxDir;
60
+ }
61
+ return null;
62
+ }
63
+ function getUI() {
64
+ if (!_ui) _ui = new UI();
65
+ return _ui;
66
+ }
67
+ function setUI(ui) {
68
+ _ui = ui;
69
+ }
70
+ function applyCommonFlags(argv) {
71
+ let mode = "pretty";
72
+ const isTTY = process.stdout.isTTY === true;
73
+ const noColorEnv = Boolean(process.env["NO_COLOR"]);
74
+ let color2 = isTTY && !noColorEnv;
75
+ const rest = [];
76
+ for (const arg of argv) {
77
+ switch (arg) {
78
+ case "--no-color":
79
+ color2 = false;
80
+ break;
81
+ case "--color":
82
+ color2 = true;
83
+ break;
84
+ case "--json":
85
+ case "--pretty-json":
86
+ mode = "json";
87
+ if (arg === "--json") rest.push(arg);
88
+ break;
89
+ case "--quiet":
90
+ mode = "quiet";
91
+ break;
92
+ default:
93
+ rest.push(arg);
127
94
  }
128
- if (config.apiKeyRef) {
129
- const resolved = resolveKeyRef(config.apiKeyRef);
130
- if (resolved) {
131
- env.ANTHROPIC_API_KEY = resolved;
132
- } else {
133
- const platformHint = config.apiKeyRef.startsWith("keychain:") && process.platform !== "darwin" ? " (keychain: scheme requires darwin)" : "";
134
- process.stderr.write(
135
- `[claude-code] warning: apiKeyRef "${config.apiKeyRef}" could not be resolved${platformHint}; ANTHROPIC_API_KEY will not be injected
136
- `
137
- );
95
+ }
96
+ return { mode, color: color2, restArgv: rest };
97
+ }
98
+ var fs, path, import_node_url, import_meta2, BRAILLE_FRAMES, COMPACT_BANNER, UI, _ui;
99
+ var init_ui = __esm({
100
+ "src/cli/ui.ts"() {
101
+ "use strict";
102
+ fs = __toESM(require("fs"), 1);
103
+ path = __toESM(require("path"), 1);
104
+ import_node_url = require("url");
105
+ import_meta2 = {};
106
+ BRAILLE_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
107
+ COMPACT_BANNER = ["\u25C7 PRISMER", " Runtime CLI"];
108
+ UI = class {
109
+ mode;
110
+ colorEnabled;
111
+ stream;
112
+ errStream;
113
+ constructor(opts) {
114
+ this.mode = opts?.mode ?? "pretty";
115
+ this.stream = opts?.stream ?? process.stdout;
116
+ this.errStream = opts?.errStream ?? process.stderr;
117
+ if (opts?.color !== void 0) {
118
+ this.colorEnabled = opts.color;
119
+ } else {
120
+ const isTTY = this.stream.isTTY === true;
121
+ const noColor = Boolean(process.env["NO_COLOR"]);
122
+ this.colorEnabled = isTTY && !noColor;
123
+ }
138
124
  }
139
- }
140
- const child = (0, import_node_child_process.spawn)("claude", args, {
141
- cwd: config.cwd,
142
- env,
143
- stdio: ["ignore", "pipe", "pipe"]
144
- });
145
- let stdout = "";
146
- let stderr = "";
147
- child.stdout.on("data", (d) => {
148
- stdout += d.toString();
149
- });
150
- child.stderr.on("data", (d) => {
151
- stderr += d.toString();
152
- });
153
- const onAbort = () => {
154
- try {
155
- child.kill("SIGTERM");
156
- } catch {
125
+ // ---- Internal color helpers ----
126
+ ansi(open, close, text) {
127
+ if (!this.colorEnabled) return text;
128
+ return `\x1B[${open}m${text}\x1B[${close}m`;
157
129
  }
158
- };
159
- if (task.signal) {
160
- if (task.signal.aborted) onAbort();
161
- else task.signal.addEventListener("abort", onAbort, { once: true });
162
- }
163
- let timeoutHandle;
164
- if (task.timeoutMs && task.timeoutMs > 0) {
165
- timeoutHandle = setTimeout(() => {
166
- try {
167
- child.kill("SIGTERM");
168
- } catch {
130
+ green(t) {
131
+ return this.ansi(32, 39, t);
132
+ }
133
+ red(t) {
134
+ return this.ansi(31, 39, t);
135
+ }
136
+ yellow(t) {
137
+ return this.ansi(33, 39, t);
138
+ }
139
+ cyan(t) {
140
+ return this.ansi(36, 39, t);
141
+ }
142
+ dim(t) {
143
+ return this.ansi(2, 22, t);
144
+ }
145
+ bold(t) {
146
+ return this.ansi(1, 22, t);
147
+ }
148
+ gray(t) {
149
+ return this.ansi(90, 39, t);
150
+ }
151
+ brandMark() {
152
+ return this.cyan("\u25C7");
153
+ }
154
+ colorBrandLine(line) {
155
+ let out = "";
156
+ for (const ch of line) {
157
+ if (ch === "\u2592") {
158
+ out += this.cyan(ch);
159
+ } else if (ch === "\u2593") {
160
+ out += this.dim(ch);
161
+ } else {
162
+ out += ch;
163
+ }
169
164
  }
170
- }, task.timeoutMs);
171
- }
172
- const exitCode = await new Promise((resolve3) => {
173
- child.on("exit", (code) => resolve3(code));
174
- child.on("error", () => resolve3(-1));
175
- });
176
- if (timeoutHandle) clearTimeout(timeoutHandle);
177
- const durationMs = Date.now() - startedAt;
178
- if (task.signal?.aborted) {
179
- return { ok: false, error: { code: "task_cancelled", message: "Task cancelled by client" } };
180
- }
181
- if (exitCode !== 0) {
182
- return {
183
- ok: false,
184
- error: {
185
- code: exitCode === null ? "adapter_dispatch_failed" : "adapter_dispatch_failed",
186
- message: `claude exit ${exitCode}: ${stderr.slice(0, 1024) || "<no stderr>"}`
187
- },
188
- metrics: { durationMs }
189
- };
190
- }
191
- const parsed = parseHeadlessOutput(stdout);
192
- return {
193
- ok: true,
194
- output: parsed.output,
195
- metrics: { durationMs }
196
- };
197
- },
198
- async health() {
199
- return new Promise((resolve3) => {
200
- const proc = (0, import_node_child_process.spawn)("claude", ["--version"], { stdio: "ignore" });
201
- proc.on(
202
- "exit",
203
- (code) => resolve3(
204
- code === 0 ? { available: true } : {
205
- available: false,
206
- reason: "claude CLI not in PATH",
207
- hint: "npm install -g @anthropic-ai/claude-code"
165
+ return out;
166
+ }
167
+ // ---- Core write helpers ----
168
+ write(text) {
169
+ this.stream.write(text);
170
+ }
171
+ writeErr(text) {
172
+ this.errStream.write(text);
173
+ }
174
+ // ---- Level 1: Header ----
175
+ header(text) {
176
+ if (this.mode === "json") return;
177
+ const prefix = text.startsWith("Prismer") ? this.brandMark() + " " : "";
178
+ this.write(prefix + this.bold(text) + "\n");
179
+ }
180
+ smallHeader(subtitle) {
181
+ if (this.mode === "json" || this.mode === "quiet") return;
182
+ const iconPath = findIconPath("small");
183
+ if (iconPath !== null) {
184
+ try {
185
+ const raw = fs.readFileSync(iconPath, "utf-8").replace(/\n+$/, "");
186
+ for (const line of raw.split("\n")) {
187
+ this.write(this.cyan(line) + "\n");
188
+ }
189
+ } catch {
190
+ this.write(this.brandMark() + " " + this.bold("Prismer") + "\n");
208
191
  }
209
- )
192
+ } else {
193
+ this.write(this.brandMark() + " " + this.bold("Prismer") + "\n");
194
+ }
195
+ if (subtitle !== void 0 && subtitle.length > 0) {
196
+ this.write(this.dim(" " + subtitle) + "\n");
197
+ }
198
+ this.blank();
199
+ }
200
+ banner(subtitle, opts) {
201
+ if (this.mode === "json" || this.mode === "quiet") return;
202
+ const envColumns = process.env["COLUMNS"] !== void 0 ? parseInt(process.env["COLUMNS"], 10) : NaN;
203
+ const width = this.stream.columns ?? process.stdout.columns ?? (Number.isFinite(envColumns) ? envColumns : 80);
204
+ const iconPath = findIconPath("big");
205
+ const shouldUseFull = opts?.full === true || width >= 120;
206
+ if (shouldUseFull && iconPath !== null) {
207
+ try {
208
+ const raw = fs.readFileSync(iconPath, "utf-8");
209
+ const lines = raw.split("\n");
210
+ for (const line of lines) {
211
+ const brandedLine = line.replace("Prismer Cloud SDK", "Prismer Runtime CLI");
212
+ const stripped = brandedLine.trimEnd();
213
+ if (stripped.length === 0) {
214
+ this.write("\n");
215
+ continue;
216
+ }
217
+ const clipped = stripped.length >= width ? stripped.slice(0, Math.max(width - 1, 1)) : stripped;
218
+ this.write(this.colorBrandLine(clipped) + "\n");
219
+ }
220
+ } catch {
221
+ this.write(this.cyan(COMPACT_BANNER[0] ?? "\u25C7 PRISMER") + "\n");
222
+ this.write(this.dim(COMPACT_BANNER[1] ?? " Runtime CLI") + "\n");
223
+ }
224
+ } else {
225
+ this.write(this.cyan(COMPACT_BANNER[0] ?? "\u25C7 PRISMER") + "\n");
226
+ this.write(this.dim(COMPACT_BANNER[1] ?? " Runtime CLI") + "\n");
227
+ }
228
+ if (subtitle !== void 0 && subtitle.length > 0) {
229
+ this.write(this.dim(" " + subtitle) + "\n");
230
+ }
231
+ this.blank();
232
+ }
233
+ // ---- Level 2: Primary data ----
234
+ blank() {
235
+ if (this.mode === "json") return;
236
+ this.write("\n");
237
+ }
238
+ line(text) {
239
+ if (this.mode === "json") return;
240
+ this.write(text + "\n");
241
+ }
242
+ info(text) {
243
+ this.line(text);
244
+ }
245
+ // ---- Level 3: Secondary ----
246
+ secondary(text, indent = 2) {
247
+ if (this.mode === "json") return;
248
+ this.write(" ".repeat(indent) + this.dim(text) + "\n");
249
+ }
250
+ // ---- Level 4: Action tips ----
251
+ tip(text) {
252
+ if (this.mode === "json") return;
253
+ this.write(this.cyan("Tip:") + " " + text + "\n");
254
+ }
255
+ next(text) {
256
+ if (this.mode === "json") return;
257
+ this.write(this.cyan("Next:") + " " + text + "\n");
258
+ }
259
+ // ---- Level 5: Status indicators ----
260
+ ok(text, detail) {
261
+ if (this.mode === "json") return;
262
+ const suffix = detail ? " " + this.dim(detail) : "";
263
+ this.write(" " + this.green("\u2713") + " " + text + suffix + "\n");
264
+ }
265
+ success(text, detail) {
266
+ this.ok(text, detail);
267
+ }
268
+ fail(text, detail) {
269
+ if (this.mode === "json") return;
270
+ const suffix = detail ? " " + this.dim(detail) : "";
271
+ this.write(" " + this.red("\u2717") + " " + text + suffix + "\n");
272
+ }
273
+ online(text) {
274
+ if (this.mode === "json") return;
275
+ this.write(" " + this.green("\u25CF") + " " + text + "\n");
276
+ }
277
+ offline(text) {
278
+ if (this.mode === "json") return;
279
+ this.write(" " + this.gray("\u25CB") + " " + text + "\n");
280
+ }
281
+ notInstalled(text) {
282
+ if (this.mode === "json") return;
283
+ this.write(" " + this.dim("\xB7") + " " + this.dim(text) + "\n");
284
+ }
285
+ pending(text) {
286
+ if (this.mode === "json") return;
287
+ this.write(" " + this.yellow("\u27F3") + " " + text + "\n");
288
+ }
289
+ warn(text, detail) {
290
+ if (this.mode === "json") return;
291
+ const suffix = detail ? " " + this.dim(detail) : "";
292
+ this.write(" " + this.yellow("!") + " " + text + suffix + "\n");
293
+ }
294
+ // ---- Level 6: Error block ----
295
+ error(what, cause, fix) {
296
+ if (this.mode === "json") return;
297
+ this.writeErr(this.red("\u2717") + " " + what + "\n");
298
+ if (cause !== void 0) {
299
+ this.writeErr(" " + this.dim("Cause:") + " " + this.dim(cause) + "\n");
300
+ }
301
+ if (fix !== void 0) {
302
+ this.writeErr(" " + this.cyan("Fix:") + " " + fix + "\n");
303
+ }
304
+ }
305
+ table(rowsOrOpts, maybeOpts) {
306
+ if (this.mode === "json") return;
307
+ const rows = Array.isArray(rowsOrOpts) ? rowsOrOpts : rowsOrOpts.rows;
308
+ const opts = Array.isArray(rowsOrOpts) ? maybeOpts : { columns: rowsOrOpts.columns, maxWidth: rowsOrOpts.maxWidth };
309
+ if (!opts) throw new Error("table() requires columns");
310
+ const maxWidth = opts.maxWidth ?? (process.stdout.columns || 80);
311
+ const cols = opts.columns;
312
+ const widths = cols.map((col) => col.length);
313
+ for (const row of rows) {
314
+ cols.forEach((col, i) => {
315
+ const val = row[col] ?? "";
316
+ const w = widths[i] ?? 0;
317
+ if (val.length > w) widths[i] = val.length;
318
+ });
319
+ }
320
+ const totalWidth = widths.reduce((a, b) => a + b, 0) + (cols.length - 1) * 2 + 2;
321
+ if (totalWidth > maxWidth) {
322
+ for (let i = 0; i < rows.length; i++) {
323
+ const row = rows[i];
324
+ if (!row) continue;
325
+ for (const col of cols) {
326
+ const val = row[col] ?? "";
327
+ this.write(" " + this.bold(col + ":") + " " + val + "\n");
328
+ }
329
+ if (i < rows.length - 1) this.write("\n");
330
+ }
331
+ return;
332
+ }
333
+ const header2 = cols.map((col, i) => col.toUpperCase().padEnd(widths[i] ?? col.length)).join(" ");
334
+ this.write(" " + this.dim(header2) + "\n");
335
+ for (const row of rows) {
336
+ const line = cols.map((col, i) => (row[col] ?? "").padEnd(widths[i] ?? col.length)).join(" ");
337
+ this.write(" " + line + "\n");
338
+ }
339
+ }
340
+ // ---- Spinner ----
341
+ spinner(text) {
342
+ if (this.mode === "quiet" || this.mode === "json") {
343
+ return {
344
+ update() {
345
+ },
346
+ stop() {
347
+ }
348
+ };
349
+ }
350
+ const isTTY = this.stream.isTTY === true;
351
+ if (!isTTY || !this.colorEnabled) {
352
+ this.write(" " + this.yellow("\u27F3") + " " + text + "\n");
353
+ return {
354
+ update: (t) => {
355
+ this.write(" " + this.yellow("\u27F3") + " " + t + "\n");
356
+ },
357
+ stop: (final) => {
358
+ if (final) this.write(" " + this.green("\u2713") + " " + final + "\n");
359
+ }
360
+ };
361
+ }
362
+ let current = text;
363
+ let frameIdx = 0;
364
+ let stopped = false;
365
+ const write = this.write.bind(this);
366
+ const colorFn = this.yellow.bind(this);
367
+ const greenFn = this.green.bind(this);
368
+ function renderFrame() {
369
+ const frame = BRAILLE_FRAMES[frameIdx % BRAILLE_FRAMES.length] ?? "\u280B";
370
+ const line = " " + colorFn(frame) + " " + current;
371
+ write("\r" + line);
372
+ frameIdx++;
373
+ }
374
+ renderFrame();
375
+ const timer = setInterval(renderFrame, 80);
376
+ return {
377
+ update(t) {
378
+ if (stopped) return;
379
+ current = t;
380
+ },
381
+ stop(final) {
382
+ if (stopped) return;
383
+ stopped = true;
384
+ clearInterval(timer);
385
+ write("\r\x1B[2K");
386
+ if (final) write(" " + greenFn("\u2713") + " " + final + "\n");
387
+ }
388
+ };
389
+ }
390
+ // ---- Progress bar ----
391
+ progress(text, total) {
392
+ if (this.mode === "quiet" || this.mode === "json") {
393
+ return {
394
+ update() {
395
+ },
396
+ stop() {
397
+ }
398
+ };
399
+ }
400
+ const isTTY = this.stream.isTTY === true;
401
+ const start = Date.now();
402
+ const write = this.write.bind(this);
403
+ const colorFn = this.cyan.bind(this);
404
+ const dimFn = this.dim.bind(this);
405
+ const greenFn = this.green.bind(this);
406
+ let last = 0;
407
+ let lastDetail = "";
408
+ let stopped = false;
409
+ const render = () => {
410
+ if (stopped) return;
411
+ const frac = total > 0 ? Math.min(1, Math.max(0, last / total)) : 0;
412
+ const pct = Math.floor(frac * 100);
413
+ const width = 20;
414
+ const filled = Math.floor(frac * width);
415
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
416
+ const elapsed = (Date.now() - start) / 1e3;
417
+ const eta = frac > 0.01 ? Math.max(0, elapsed / frac - elapsed) : 0;
418
+ const etaStr = frac >= 1 ? "" : ` \xB7 ${eta < 1 ? "<1s" : Math.round(eta) + "s"} left`;
419
+ const detailStr = lastDetail ? ` \xB7 ${lastDetail}` : "";
420
+ const line = ` ${text} [${colorFn(bar)}] ${String(pct).padStart(3)}%${detailStr}${dimFn(etaStr)}`;
421
+ if (isTTY && this.colorEnabled) {
422
+ write("\r\x1B[2K" + line);
423
+ } else {
424
+ write(line + "\n");
425
+ }
426
+ };
427
+ render();
428
+ return {
429
+ update: (current, detail) => {
430
+ if (stopped) return;
431
+ last = current;
432
+ if (detail !== void 0) lastDetail = detail;
433
+ render();
434
+ },
435
+ stop: (final) => {
436
+ if (stopped) return;
437
+ stopped = true;
438
+ if (isTTY && this.colorEnabled) write("\r\x1B[2K");
439
+ if (final) write(" " + greenFn("\u2713") + " " + final + "\n");
440
+ }
441
+ };
442
+ }
443
+ // ---- JSON output ----
444
+ json(payload, opts) {
445
+ const indent = opts?.pretty ? 2 : void 0;
446
+ this.write(JSON.stringify(payload, null, indent) + "\n");
447
+ }
448
+ result(pretty, jsonPayload) {
449
+ if (this.mode === "pretty") {
450
+ pretty();
451
+ } else {
452
+ this.json(jsonPayload);
453
+ }
454
+ }
455
+ };
456
+ _ui = null;
457
+ }
458
+ });
459
+
460
+ // src/cli/util.ts
461
+ var util_exports = {};
462
+ __export(util_exports, {
463
+ DEFAULT_CLOUD_BASE_URL: () => DEFAULT_CLOUD_BASE_URL,
464
+ clearPidFile: () => clearPidFile,
465
+ color: () => color,
466
+ exitWithError: () => exitWithError,
467
+ fail: () => fail,
468
+ header: () => header,
469
+ info: () => info,
470
+ normalizeCloudUrl: () => normalizeCloudUrl,
471
+ ok: () => ok,
472
+ pidAlive: () => pidAlive,
473
+ pidFilePath: () => pidFilePath,
474
+ printBanner: () => printBanner,
475
+ printJson: () => printJson,
476
+ readPidFile: () => readPidFile,
477
+ runAction: () => runAction,
478
+ table: () => table,
479
+ tip: () => tip,
480
+ warn: () => warn,
481
+ writePidFile: () => writePidFile
482
+ });
483
+ function color(kind, text) {
484
+ if (process.env.NO_COLOR === "1" || process.env.NO_COLOR === "true") return text;
485
+ return `${ANSI[kind]}${text}${ANSI.reset}`;
486
+ }
487
+ function printJson(v) {
488
+ getUI().json(v, { pretty: true });
489
+ }
490
+ function exitWithError(message, opts) {
491
+ const o = typeof opts === "number" ? { exitCode: opts } : opts ?? {};
492
+ const exitCode = o.exitCode ?? 1;
493
+ const ui = getUI();
494
+ if (ui.mode === "json") {
495
+ const payload = {
496
+ ok: false,
497
+ error: { code: o.code ?? "cli_error", message },
498
+ ...o.details ? { details: o.details } : {}
499
+ };
500
+ ui.json(payload, { pretty: true });
501
+ } else {
502
+ process.stderr.write(`Error: ${message}
503
+ `);
504
+ }
505
+ process.exit(exitCode);
506
+ }
507
+ function normalizeCloudUrl(input) {
508
+ const raw = input.trim();
509
+ if (!raw) throw new Error("Cloud URL is empty.");
510
+ if (/^[a-z][a-z0-9+\-.]*:\/\//i.test(raw)) {
511
+ let parsed;
512
+ try {
513
+ parsed = new URL(raw);
514
+ } catch {
515
+ throw new Error(`Invalid --cloud URL: ${raw}`);
516
+ }
517
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
518
+ throw new Error(`Invalid --cloud URL scheme: ${parsed.protocol} (expected http:// or https://)`);
519
+ }
520
+ return raw.replace(/\/$/, "");
521
+ }
522
+ if (!/^[A-Za-z0-9.\-_:[\]/]+$/.test(raw)) {
523
+ throw new Error(`Invalid --cloud URL: ${raw} (must be http://\u2026 or https://\u2026 or host:port)`);
524
+ }
525
+ const candidate = `http://${raw}`;
526
+ try {
527
+ new URL(candidate);
528
+ } catch {
529
+ throw new Error(`Invalid --cloud URL: ${raw}`);
530
+ }
531
+ return candidate.replace(/\/$/, "");
532
+ }
533
+ function runAction(fn, opts = {}) {
534
+ return async (...args) => {
535
+ try {
536
+ await fn(...args);
537
+ } catch (err) {
538
+ const raw = err instanceof Error ? err.message : String(err);
539
+ const message = opts.sanitize ? opts.sanitize(raw) : raw;
540
+ exitWithError(message, { code: opts.code });
541
+ }
542
+ };
543
+ }
544
+ function printBanner(opts = {}) {
545
+ const ui = getUI();
546
+ if (opts.compact) {
547
+ ui.smallHeader("Runtime CLI v1.9.7");
548
+ return;
549
+ }
550
+ ui.banner("Runtime CLI v1.9.7", { full: true });
551
+ }
552
+ function ok(label, detail) {
553
+ getUI().ok(label, detail);
554
+ }
555
+ function warn(label, detail) {
556
+ getUI().warn(label, detail);
557
+ }
558
+ function fail(label, detail) {
559
+ getUI().fail(label, detail);
560
+ }
561
+ function tip(command, detail) {
562
+ const text = detail ? `${command} ${detail}` : command;
563
+ getUI().tip(text);
564
+ }
565
+ function info(message) {
566
+ getUI().info(message);
567
+ }
568
+ function header(title) {
569
+ getUI().header(title);
570
+ getUI().blank();
571
+ }
572
+ function table(rows, columns) {
573
+ getUI().table(rows, { columns });
574
+ }
575
+ function pidFilePath(paths) {
576
+ return (0, import_node_path3.join)(paths.root, "daemon.pid");
577
+ }
578
+ function writePidFile(paths, pid) {
579
+ (0, import_node_fs3.writeFileSync)(pidFilePath(paths), `${pid}
580
+ `, "utf8");
581
+ }
582
+ function readPidFile(paths) {
583
+ const p = pidFilePath(paths);
584
+ if (!(0, import_node_fs3.existsSync)(p)) return void 0;
585
+ const raw = (0, import_node_fs3.readFileSync)(p, "utf8").trim();
586
+ const pid = Number.parseInt(raw, 10);
587
+ return Number.isFinite(pid) ? pid : void 0;
588
+ }
589
+ function clearPidFile(paths) {
590
+ const p = pidFilePath(paths);
591
+ if ((0, import_node_fs3.existsSync)(p)) {
592
+ try {
593
+ (0, import_node_fs3.unlinkSync)(p);
594
+ } catch {
595
+ }
596
+ }
597
+ }
598
+ function pidAlive(pid) {
599
+ try {
600
+ process.kill(pid, 0);
601
+ return true;
602
+ } catch {
603
+ return false;
604
+ }
605
+ }
606
+ var import_node_fs3, import_node_path3, DEFAULT_CLOUD_BASE_URL, ANSI;
607
+ var init_util = __esm({
608
+ "src/cli/util.ts"() {
609
+ "use strict";
610
+ import_node_fs3 = require("fs");
611
+ import_node_path3 = require("path");
612
+ init_ui();
613
+ DEFAULT_CLOUD_BASE_URL = "https://prismer.cloud";
614
+ ANSI = {
615
+ reset: "\x1B[0m",
616
+ bold: "\x1B[1m",
617
+ dim: "\x1B[2m",
618
+ cyan: "\x1B[36m",
619
+ green: "\x1B[32m",
620
+ yellow: "\x1B[33m",
621
+ red: "\x1B[31m"
622
+ };
623
+ }
624
+ });
625
+
626
+ // src/cli/index.ts
627
+ var import_commander18 = require("commander");
628
+
629
+ // src/cli/commands/adapter.ts
630
+ var import_node_child_process4 = require("child_process");
631
+ var import_node_fs4 = require("fs");
632
+ var import_node_os3 = require("os");
633
+ var import_node_path4 = require("path");
634
+ var import_commander = require("commander");
635
+
636
+ // src/adapters/claude-code/index.ts
637
+ var import_node_child_process = require("child_process");
638
+ var import_zod = require("zod");
639
+
640
+ // src/adapters/claude-code/output-parser.ts
641
+ var MAX_OUTPUT_CHARS = 64 * 1024;
642
+ function parseHeadlessOutput(stdout) {
643
+ let out = "";
644
+ let toolCalls = 0;
645
+ for (const line of stdout.split("\n")) {
646
+ const trimmed = line.trim();
647
+ if (!trimmed) continue;
648
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
649
+ try {
650
+ const obj = JSON.parse(trimmed);
651
+ if (obj.event === "tool_call" || obj.tool) {
652
+ toolCalls += 1;
653
+ continue;
654
+ }
655
+ if (typeof obj.text === "string") {
656
+ out += obj.text;
657
+ continue;
658
+ }
659
+ } catch {
660
+ }
661
+ }
662
+ out += line + "\n";
663
+ }
664
+ const truncated = out.length > MAX_OUTPUT_CHARS;
665
+ if (truncated) out = out.slice(0, MAX_OUTPUT_CHARS) + "\n\u2026[truncated]";
666
+ return { output: out.trim(), toolCalls, truncated };
667
+ }
668
+
669
+ // src/adapters/claude-code/index.ts
670
+ var McpServerSchema = import_zod.z.object({
671
+ name: import_zod.z.string(),
672
+ command: import_zod.z.string(),
673
+ args: import_zod.z.array(import_zod.z.string()).optional(),
674
+ env: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional()
675
+ });
676
+ var CCConfigSchema = import_zod.z.object({
677
+ cwd: import_zod.z.string().min(1),
678
+ // claude 2.x accepts aliases ('sonnet', 'haiku', 'opus') and full ids
679
+ // (e.g. 'claude-sonnet-4-6'). The 1.x default 'claude-3-5-sonnet' is no
680
+ // longer a valid model id under the current CLI.
681
+ model: import_zod.z.string().default("sonnet"),
682
+ systemPrompt: import_zod.z.string().optional(),
683
+ envVars: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
684
+ mcpServers: import_zod.z.array(McpServerSchema).optional(),
685
+ allowedTools: import_zod.z.array(import_zod.z.string()).optional(),
686
+ maxTurns: import_zod.z.number().int().positive().default(20),
687
+ // External-model route (1.9.x extension — optional, no breaking change)
688
+ baseURL: import_zod.z.string().url().optional(),
689
+ apiKeyRef: import_zod.z.string().regex(/^(env|keychain):[A-Za-z0-9_][A-Za-z0-9_.\-]*$/).optional(),
690
+ // informational tag — dispatch does not branch on this value yet
691
+ route: import_zod.z.enum(["default", "prismer", "omniroute"]).default("default")
692
+ });
693
+ var claudeCodeAdapter = {
694
+ name: "claude-code",
695
+ kind: "interactive",
696
+ capabilities: ["shell", "code", "mcp", "edit"],
697
+ workspaceSchema: CCConfigSchema,
698
+ validate(config) {
699
+ const r = CCConfigSchema.safeParse(config);
700
+ if (r.success) return { ok: true };
701
+ return {
702
+ ok: false,
703
+ errors: r.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`)
704
+ };
705
+ },
706
+ async dispatch(profile, task) {
707
+ const config = CCConfigSchema.parse(profile.config);
708
+ const args = ["--print"];
709
+ if (config.model) {
710
+ args.push("--model", config.model);
711
+ }
712
+ if (config.allowedTools && config.allowedTools.length > 0) {
713
+ args.push("--allowed-tools", config.allowedTools.join(","));
714
+ }
715
+ if (config.systemPrompt) {
716
+ args.push("--system-prompt", config.systemPrompt);
717
+ }
718
+ args.push(task.prompt);
719
+ const startedAt = Date.now();
720
+ const env = { ...process.env, ...config.envVars ?? {} };
721
+ if (config.baseURL) {
722
+ env.ANTHROPIC_API_BASE = config.baseURL;
723
+ }
724
+ const outboxDir = typeof task.metadata?.prismerOutboxDir === "string" ? task.metadata.prismerOutboxDir : void 0;
725
+ if (outboxDir) {
726
+ env.PRISMER_OUTBOX_DIR = outboxDir;
727
+ }
728
+ if (config.apiKeyRef) {
729
+ const resolved = resolveKeyRef(config.apiKeyRef);
730
+ if (resolved) {
731
+ env.ANTHROPIC_API_KEY = resolved;
732
+ } else {
733
+ const platformHint = config.apiKeyRef.startsWith("keychain:") && process.platform !== "darwin" ? " (keychain: scheme requires darwin)" : "";
734
+ process.stderr.write(
735
+ `[claude-code] warning: apiKeyRef "${config.apiKeyRef}" could not be resolved${platformHint}; ANTHROPIC_API_KEY will not be injected
736
+ `
737
+ );
738
+ }
739
+ }
740
+ const child = (0, import_node_child_process.spawn)("claude", args, {
741
+ cwd: config.cwd,
742
+ env,
743
+ stdio: ["ignore", "pipe", "pipe"]
744
+ });
745
+ let stdout = "";
746
+ let stderr = "";
747
+ child.stdout.on("data", (d) => {
748
+ stdout += d.toString();
749
+ });
750
+ child.stderr.on("data", (d) => {
751
+ stderr += d.toString();
752
+ });
753
+ const onAbort = () => {
754
+ try {
755
+ child.kill("SIGTERM");
756
+ } catch {
757
+ }
758
+ };
759
+ if (task.signal) {
760
+ if (task.signal.aborted) onAbort();
761
+ else task.signal.addEventListener("abort", onAbort, { once: true });
762
+ }
763
+ let timeoutHandle;
764
+ if (task.timeoutMs && task.timeoutMs > 0) {
765
+ timeoutHandle = setTimeout(() => {
766
+ try {
767
+ child.kill("SIGTERM");
768
+ } catch {
769
+ }
770
+ }, task.timeoutMs);
771
+ }
772
+ const exitCode = await new Promise((resolve3) => {
773
+ child.on("exit", (code) => resolve3(code));
774
+ child.on("error", () => resolve3(-1));
775
+ });
776
+ if (timeoutHandle) clearTimeout(timeoutHandle);
777
+ const durationMs = Date.now() - startedAt;
778
+ if (task.signal?.aborted) {
779
+ return { ok: false, error: { code: "task_cancelled", message: "Task cancelled by client" } };
780
+ }
781
+ if (exitCode !== 0) {
782
+ return {
783
+ ok: false,
784
+ error: {
785
+ code: exitCode === null ? "adapter_dispatch_failed" : "adapter_dispatch_failed",
786
+ message: `claude exit ${exitCode}: ${stderr.slice(0, 1024) || "<no stderr>"}`
787
+ },
788
+ metrics: { durationMs }
789
+ };
790
+ }
791
+ const parsed = parseHeadlessOutput(stdout);
792
+ return {
793
+ ok: true,
794
+ output: parsed.output,
795
+ metrics: { durationMs }
796
+ };
797
+ },
798
+ async health() {
799
+ return new Promise((resolve3) => {
800
+ const proc = (0, import_node_child_process.spawn)("claude", ["--version"], { stdio: "ignore" });
801
+ proc.on(
802
+ "exit",
803
+ (code) => resolve3(
804
+ code === 0 ? { available: true } : {
805
+ available: false,
806
+ reason: "claude CLI not in PATH",
807
+ hint: "npm install -g @anthropic-ai/claude-code"
808
+ }
809
+ )
210
810
  );
211
811
  proc.on(
212
812
  "error",
@@ -659,7 +1259,7 @@ var HermesProfileConfigSchema = import_zod3.z.object({
659
1259
  */
660
1260
  prismerMcpServerPath: import_zod3.z.string().optional(),
661
1261
  /** Model sent to Prismer's /api/v1/chat/completions endpoint. */
662
- model: import_zod3.z.string().min(1).default("us-kimi-k2.5"),
1262
+ model: import_zod3.z.string().min(1).default("us-kimi-k2.6"),
663
1263
  /** Named custom provider written into Hermes config.yaml. */
664
1264
  prismerProviderName: import_zod3.z.string().min(1).default("prismer"),
665
1265
  /** Override cloud provider base. Defaults to PRISMER_BASE_URL + /api/v1. */
@@ -685,7 +1285,9 @@ var HermesProfileConfigSchema = import_zod3.z.object({
685
1285
  * surface but the local source tree contains hermes_cli/kanban_db.py.
686
1286
  */
687
1287
  hermesSourceDir: import_zod3.z.string().optional(),
688
- nativeMirrorTimeoutMs: import_zod3.z.number().int().positive().default(2e3)
1288
+ nativeMirrorTimeoutMs: import_zod3.z.number().int().positive().default(2e3),
1289
+ /** Task authority level: executor (default) or orchestrator. */
1290
+ taskAuthority: import_zod3.z.enum(["executor", "orchestrator"]).optional().default("executor")
689
1291
  });
690
1292
  var hermesAdapter = {
691
1293
  name: "hermes",
@@ -1170,13 +1772,13 @@ function resolvePrismerMcpServerPath(config) {
1170
1772
  }
1171
1773
  try {
1172
1774
  const { fileURLToPath: fileURLToPath2 } = require("url");
1173
- const { join: join13, dirname: dirname8 } = require("path");
1775
+ const { join: join14, dirname: dirname8 } = require("path");
1174
1776
  const here = dirname8(fileURLToPath2(import_meta.url));
1175
- const candidate = join13(here, "../../mcp/dist/index.js");
1777
+ const candidate = join14(here, "../../mcp/dist/index.js");
1176
1778
  if ((0, import_node_fs.existsSync)(candidate)) return candidate;
1177
- const candidate2 = join13(here, "../../../mcp/dist/index.js");
1779
+ const candidate2 = join14(here, "../../../mcp/dist/index.js");
1178
1780
  if ((0, import_node_fs.existsSync)(candidate2)) return candidate2;
1179
- const candidate3 = join13(here, "../../../../mcp/dist/index.js");
1781
+ const candidate3 = join14(here, "../../../../mcp/dist/index.js");
1180
1782
  if ((0, import_node_fs.existsSync)(candidate3)) return candidate3;
1181
1783
  } catch {
1182
1784
  }
@@ -1585,674 +2187,126 @@ var OpenClawService = class {
1585
2187
  const ERR_PREFIX_RE = /^Cannot read properties of undefined \(reading '[^']+'\)\s*\n+/;
1586
2188
  if (ERR_PREFIX_RE.test(output)) {
1587
2189
  process.stderr.write(
1588
- `[openclaw-adapter] stripping known stderr-bleed prefix from chat output (OpenClaw 2026.4.x bug)
1589
- `
1590
- );
1591
- output = output.replace(ERR_PREFIX_RE, "");
1592
- }
1593
- return {
1594
- ok: true,
1595
- output,
1596
- metrics: { durationMs: Date.now() - startedAt }
1597
- };
1598
- } catch (err) {
1599
- if (task.signal?.aborted || err?.name === "AbortError") {
1600
- return { ok: false, error: { code: "task_cancelled", message: "Task cancelled by client" } };
1601
- }
1602
- return {
1603
- ok: false,
1604
- error: { code: "adapter_dispatch_failed", message: err.message }
1605
- };
1606
- }
1607
- }
1608
- };
1609
- async function checkHealth2(baseUrl, apiKey) {
1610
- try {
1611
- const res = await fetch(`${baseUrl}/health`, {
1612
- headers: { Authorization: `Bearer ${apiKey}` },
1613
- signal: AbortSignal.timeout(2e3)
1614
- });
1615
- return res.ok;
1616
- } catch {
1617
- return false;
1618
- }
1619
- }
1620
-
1621
- // src/config.ts
1622
- var TOML = __toESM(require("@iarna/toml"), 1);
1623
- var import_node_fs2 = require("fs");
1624
- var import_node_os2 = require("os");
1625
- var import_node_path2 = require("path");
1626
- var import_zod5 = require("zod");
1627
- var ConfigSchema = import_zod5.z.object({
1628
- /** API key from `prismer setup`, or env override. */
1629
- api_key: import_zod5.z.string().min(1),
1630
- /** Cloud REST + WS base; `ws://` is derived by stripping `http`. */
1631
- cloud_api_base: import_zod5.z.string().url(),
1632
- /** Stable per-machine daemon identifier. Generated once on first setup. */
1633
- daemon_id: import_zod5.z.string().min(1),
1634
- /** Optional adapter-specific overrides keyed by adapter name. */
1635
- adapters: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.record(import_zod5.z.string(), import_zod5.z.unknown())).optional(),
1636
- /** Local daemon shell execution. Default disabled. */
1637
- shell: import_zod5.z.object({
1638
- enabled: import_zod5.z.boolean().default(false),
1639
- default_cwd: import_zod5.z.string().optional(),
1640
- defaultCwd: import_zod5.z.string().optional(),
1641
- shell: import_zod5.z.enum(["bash", "zsh", "sh"]).optional(),
1642
- max_timeout_ms: import_zod5.z.number().int().positive().optional(),
1643
- maxTimeoutMs: import_zod5.z.number().int().positive().optional(),
1644
- max_output_bytes: import_zod5.z.number().int().positive().optional(),
1645
- maxOutputBytes: import_zod5.z.number().int().positive().optional(),
1646
- allowed_workspaces: import_zod5.z.array(import_zod5.z.string()).optional(),
1647
- allowedWorkspaces: import_zod5.z.array(import_zod5.z.string()).optional()
1648
- }).optional(),
1649
- /** Local cache settings. */
1650
- cache: import_zod5.z.object({
1651
- max_bytes: import_zod5.z.number().int().positive().default(5 * 1024 * 1024 * 1024)
1652
- }).optional()
1653
- });
1654
- function resolvePaths(home) {
1655
- const root = home ?? process.env.PRISMER_HOME ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".prismer");
1656
- return {
1657
- root,
1658
- configFile: (0, import_node_path2.join)(root, "config.toml"),
1659
- localDb: (0, import_node_path2.join)(root, "local.db"),
1660
- cacheDir: (0, import_node_path2.join)(root, "cache"),
1661
- logsDir: (0, import_node_path2.join)(root, "logs"),
1662
- runsDir: (0, import_node_path2.join)(root, "runs")
1663
- };
1664
- }
1665
- function configExists(paths = resolvePaths()) {
1666
- return (0, import_node_fs2.existsSync)(paths.configFile);
1667
- }
1668
- function loadConfig(paths = resolvePaths()) {
1669
- if (!(0, import_node_fs2.existsSync)(paths.configFile)) {
1670
- throw new Error(
1671
- `Config not found at ${paths.configFile}. Run \`prismer setup\` to create it.`
1672
- );
1673
- }
1674
- const raw = (0, import_node_fs2.readFileSync)(paths.configFile, "utf8");
1675
- const parsed = TOML.parse(raw);
1676
- const merged = {
1677
- ...parsed,
1678
- api_key: process.env.PRISMER_API_KEY ?? parsed.api_key,
1679
- cloud_api_base: process.env.PRISMER_BASE_URL ?? parsed.cloud_api_base
1680
- };
1681
- const result = ConfigSchema.safeParse(merged);
1682
- if (!result.success) {
1683
- const detail = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
1684
- throw new Error(`Invalid config at ${paths.configFile}: ${detail}`);
1685
- }
1686
- return result.data;
1687
- }
1688
- function saveConfig(config, paths = resolvePaths()) {
1689
- if (!(0, import_node_fs2.existsSync)(paths.root)) {
1690
- (0, import_node_fs2.mkdirSync)(paths.root, { recursive: true });
1691
- }
1692
- if (!(0, import_node_fs2.existsSync)((0, import_node_path2.dirname)(paths.configFile))) {
1693
- (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(paths.configFile), { recursive: true });
1694
- }
1695
- ConfigSchema.parse(config);
1696
- (0, import_node_fs2.writeFileSync)(paths.configFile, TOML.stringify(config), "utf8");
1697
- }
1698
- function deriveWsUrl(httpBase) {
1699
- const u = new URL(httpBase);
1700
- u.protocol = u.protocol === "https:" ? "wss:" : "ws:";
1701
- u.pathname = (u.pathname.replace(/\/$/, "") || "") + "/ws";
1702
- return u.toString();
1703
- }
1704
-
1705
- // src/cli/util.ts
1706
- var import_node_fs3 = require("fs");
1707
- var import_node_path3 = require("path");
1708
-
1709
- // src/cli/ui.ts
1710
- var fs = __toESM(require("fs"), 1);
1711
- var path = __toESM(require("path"), 1);
1712
- var import_node_url = require("url");
1713
- var import_meta2 = {};
1714
- var BRAILLE_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1715
- var COMPACT_BANNER = ["\u25C7 PRISMER", " Runtime CLI"];
1716
- function thisDirname() {
1717
- try {
1718
- return path.dirname((0, import_node_url.fileURLToPath)(import_meta2.url));
1719
- } catch {
1720
- return process.cwd();
1721
- }
1722
- }
1723
- function findIconPath(size = "big") {
1724
- const name = size === "big" ? "icon" : "smallicon";
1725
- const here = thisDirname();
1726
- const candidates = [
1727
- // npm-installed: node_modules/@prismer/runtime/dist/cli.js → ../assets
1728
- path.resolve(here, "../assets", name),
1729
- // alternate dist layout (sub-bundle): dist/bin/cli.js → ../../assets
1730
- path.resolve(here, "../../assets", name),
1731
- // source/typecheck: src/cli/ui.ts → ../../assets
1732
- path.resolve(here, "../../assets", name),
1733
- // dev mode: cwd happens to be runtime root
1734
- path.resolve(process.cwd(), "assets", name),
1735
- path.resolve(process.cwd(), "sdk/prismer-cloud/runtime/assets", name)
1736
- ];
1737
- for (const candidate of candidates) {
1738
- try {
1739
- if (fs.existsSync(candidate)) return candidate;
1740
- } catch {
1741
- }
1742
- }
1743
- return null;
1744
- }
1745
- var UI = class {
1746
- mode;
1747
- colorEnabled;
1748
- stream;
1749
- errStream;
1750
- constructor(opts) {
1751
- this.mode = opts?.mode ?? "pretty";
1752
- this.stream = opts?.stream ?? process.stdout;
1753
- this.errStream = opts?.errStream ?? process.stderr;
1754
- if (opts?.color !== void 0) {
1755
- this.colorEnabled = opts.color;
1756
- } else {
1757
- const isTTY = this.stream.isTTY === true;
1758
- const noColor = Boolean(process.env["NO_COLOR"]);
1759
- this.colorEnabled = isTTY && !noColor;
1760
- }
1761
- }
1762
- // ---- Internal color helpers ----
1763
- ansi(open, close, text) {
1764
- if (!this.colorEnabled) return text;
1765
- return `\x1B[${open}m${text}\x1B[${close}m`;
1766
- }
1767
- green(t) {
1768
- return this.ansi(32, 39, t);
1769
- }
1770
- red(t) {
1771
- return this.ansi(31, 39, t);
1772
- }
1773
- yellow(t) {
1774
- return this.ansi(33, 39, t);
1775
- }
1776
- cyan(t) {
1777
- return this.ansi(36, 39, t);
1778
- }
1779
- dim(t) {
1780
- return this.ansi(2, 22, t);
1781
- }
1782
- bold(t) {
1783
- return this.ansi(1, 22, t);
1784
- }
1785
- gray(t) {
1786
- return this.ansi(90, 39, t);
1787
- }
1788
- brandMark() {
1789
- return this.cyan("\u25C7");
1790
- }
1791
- colorBrandLine(line) {
1792
- let out = "";
1793
- for (const ch of line) {
1794
- if (ch === "\u2592") {
1795
- out += this.cyan(ch);
1796
- } else if (ch === "\u2593") {
1797
- out += this.dim(ch);
1798
- } else {
1799
- out += ch;
1800
- }
1801
- }
1802
- return out;
1803
- }
1804
- // ---- Core write helpers ----
1805
- write(text) {
1806
- this.stream.write(text);
1807
- }
1808
- writeErr(text) {
1809
- this.errStream.write(text);
1810
- }
1811
- // ---- Level 1: Header ----
1812
- header(text) {
1813
- if (this.mode === "json") return;
1814
- const prefix = text.startsWith("Prismer") ? this.brandMark() + " " : "";
1815
- this.write(prefix + this.bold(text) + "\n");
1816
- }
1817
- smallHeader(subtitle) {
1818
- if (this.mode === "json" || this.mode === "quiet") return;
1819
- const iconPath = findIconPath("small");
1820
- if (iconPath !== null) {
1821
- try {
1822
- const raw = fs.readFileSync(iconPath, "utf-8").replace(/\n+$/, "");
1823
- for (const line of raw.split("\n")) {
1824
- this.write(this.cyan(line) + "\n");
1825
- }
1826
- } catch {
1827
- this.write(this.brandMark() + " " + this.bold("Prismer") + "\n");
1828
- }
1829
- } else {
1830
- this.write(this.brandMark() + " " + this.bold("Prismer") + "\n");
1831
- }
1832
- if (subtitle !== void 0 && subtitle.length > 0) {
1833
- this.write(this.dim(" " + subtitle) + "\n");
1834
- }
1835
- this.blank();
1836
- }
1837
- banner(subtitle, opts) {
1838
- if (this.mode === "json" || this.mode === "quiet") return;
1839
- const envColumns = process.env["COLUMNS"] !== void 0 ? parseInt(process.env["COLUMNS"], 10) : NaN;
1840
- const width = this.stream.columns ?? process.stdout.columns ?? (Number.isFinite(envColumns) ? envColumns : 80);
1841
- const iconPath = findIconPath("big");
1842
- const shouldUseFull = opts?.full === true || width >= 120;
1843
- if (shouldUseFull && iconPath !== null) {
1844
- try {
1845
- const raw = fs.readFileSync(iconPath, "utf-8");
1846
- const lines = raw.split("\n");
1847
- for (const line of lines) {
1848
- const brandedLine = line.replace("Prismer Cloud SDK", "Prismer Runtime CLI");
1849
- const stripped = brandedLine.trimEnd();
1850
- if (stripped.length === 0) {
1851
- this.write("\n");
1852
- continue;
1853
- }
1854
- const clipped = stripped.length >= width ? stripped.slice(0, Math.max(width - 1, 1)) : stripped;
1855
- this.write(this.colorBrandLine(clipped) + "\n");
1856
- }
1857
- } catch {
1858
- this.write(this.cyan(COMPACT_BANNER[0] ?? "\u25C7 PRISMER") + "\n");
1859
- this.write(this.dim(COMPACT_BANNER[1] ?? " Runtime CLI") + "\n");
1860
- }
1861
- } else {
1862
- this.write(this.cyan(COMPACT_BANNER[0] ?? "\u25C7 PRISMER") + "\n");
1863
- this.write(this.dim(COMPACT_BANNER[1] ?? " Runtime CLI") + "\n");
1864
- }
1865
- if (subtitle !== void 0 && subtitle.length > 0) {
1866
- this.write(this.dim(" " + subtitle) + "\n");
1867
- }
1868
- this.blank();
1869
- }
1870
- // ---- Level 2: Primary data ----
1871
- blank() {
1872
- if (this.mode === "json") return;
1873
- this.write("\n");
1874
- }
1875
- line(text) {
1876
- if (this.mode === "json") return;
1877
- this.write(text + "\n");
1878
- }
1879
- info(text) {
1880
- this.line(text);
1881
- }
1882
- // ---- Level 3: Secondary ----
1883
- secondary(text, indent = 2) {
1884
- if (this.mode === "json") return;
1885
- this.write(" ".repeat(indent) + this.dim(text) + "\n");
1886
- }
1887
- // ---- Level 4: Action tips ----
1888
- tip(text) {
1889
- if (this.mode === "json") return;
1890
- this.write(this.cyan("Tip:") + " " + text + "\n");
1891
- }
1892
- next(text) {
1893
- if (this.mode === "json") return;
1894
- this.write(this.cyan("Next:") + " " + text + "\n");
1895
- }
1896
- // ---- Level 5: Status indicators ----
1897
- ok(text, detail) {
1898
- if (this.mode === "json") return;
1899
- const suffix = detail ? " " + this.dim(detail) : "";
1900
- this.write(" " + this.green("\u2713") + " " + text + suffix + "\n");
1901
- }
1902
- success(text, detail) {
1903
- this.ok(text, detail);
1904
- }
1905
- fail(text, detail) {
1906
- if (this.mode === "json") return;
1907
- const suffix = detail ? " " + this.dim(detail) : "";
1908
- this.write(" " + this.red("\u2717") + " " + text + suffix + "\n");
1909
- }
1910
- online(text) {
1911
- if (this.mode === "json") return;
1912
- this.write(" " + this.green("\u25CF") + " " + text + "\n");
1913
- }
1914
- offline(text) {
1915
- if (this.mode === "json") return;
1916
- this.write(" " + this.gray("\u25CB") + " " + text + "\n");
1917
- }
1918
- notInstalled(text) {
1919
- if (this.mode === "json") return;
1920
- this.write(" " + this.dim("\xB7") + " " + this.dim(text) + "\n");
1921
- }
1922
- pending(text) {
1923
- if (this.mode === "json") return;
1924
- this.write(" " + this.yellow("\u27F3") + " " + text + "\n");
1925
- }
1926
- warn(text, detail) {
1927
- if (this.mode === "json") return;
1928
- const suffix = detail ? " " + this.dim(detail) : "";
1929
- this.write(" " + this.yellow("!") + " " + text + suffix + "\n");
1930
- }
1931
- // ---- Level 6: Error block ----
1932
- error(what, cause, fix) {
1933
- if (this.mode === "json") return;
1934
- this.writeErr(this.red("\u2717") + " " + what + "\n");
1935
- if (cause !== void 0) {
1936
- this.writeErr(" " + this.dim("Cause:") + " " + this.dim(cause) + "\n");
1937
- }
1938
- if (fix !== void 0) {
1939
- this.writeErr(" " + this.cyan("Fix:") + " " + fix + "\n");
1940
- }
1941
- }
1942
- table(rowsOrOpts, maybeOpts) {
1943
- if (this.mode === "json") return;
1944
- const rows = Array.isArray(rowsOrOpts) ? rowsOrOpts : rowsOrOpts.rows;
1945
- const opts = Array.isArray(rowsOrOpts) ? maybeOpts : { columns: rowsOrOpts.columns, maxWidth: rowsOrOpts.maxWidth };
1946
- if (!opts) throw new Error("table() requires columns");
1947
- const maxWidth = opts.maxWidth ?? (process.stdout.columns || 80);
1948
- const cols = opts.columns;
1949
- const widths = cols.map((col) => col.length);
1950
- for (const row of rows) {
1951
- cols.forEach((col, i) => {
1952
- const val = row[col] ?? "";
1953
- const w = widths[i] ?? 0;
1954
- if (val.length > w) widths[i] = val.length;
1955
- });
1956
- }
1957
- const totalWidth = widths.reduce((a, b) => a + b, 0) + (cols.length - 1) * 2 + 2;
1958
- if (totalWidth > maxWidth) {
1959
- for (let i = 0; i < rows.length; i++) {
1960
- const row = rows[i];
1961
- if (!row) continue;
1962
- for (const col of cols) {
1963
- const val = row[col] ?? "";
1964
- this.write(" " + this.bold(col + ":") + " " + val + "\n");
1965
- }
1966
- if (i < rows.length - 1) this.write("\n");
2190
+ `[openclaw-adapter] stripping known stderr-bleed prefix from chat output (OpenClaw 2026.4.x bug)
2191
+ `
2192
+ );
2193
+ output = output.replace(ERR_PREFIX_RE, "");
1967
2194
  }
1968
- return;
1969
- }
1970
- const header = cols.map((col, i) => col.toUpperCase().padEnd(widths[i] ?? col.length)).join(" ");
1971
- this.write(" " + this.dim(header) + "\n");
1972
- for (const row of rows) {
1973
- const line = cols.map((col, i) => (row[col] ?? "").padEnd(widths[i] ?? col.length)).join(" ");
1974
- this.write(" " + line + "\n");
1975
- }
1976
- }
1977
- // ---- Spinner ----
1978
- spinner(text) {
1979
- if (this.mode === "quiet" || this.mode === "json") {
1980
- return {
1981
- update() {
1982
- },
1983
- stop() {
1984
- }
1985
- };
1986
- }
1987
- const isTTY = this.stream.isTTY === true;
1988
- if (!isTTY || !this.colorEnabled) {
1989
- this.write(" " + this.yellow("\u27F3") + " " + text + "\n");
1990
2195
  return {
1991
- update: (t) => {
1992
- this.write(" " + this.yellow("\u27F3") + " " + t + "\n");
1993
- },
1994
- stop: (final) => {
1995
- if (final) this.write(" " + this.green("\u2713") + " " + final + "\n");
1996
- }
2196
+ ok: true,
2197
+ output,
2198
+ metrics: { durationMs: Date.now() - startedAt }
1997
2199
  };
1998
- }
1999
- let current = text;
2000
- let frameIdx = 0;
2001
- let stopped = false;
2002
- const write = this.write.bind(this);
2003
- const colorFn = this.yellow.bind(this);
2004
- const greenFn = this.green.bind(this);
2005
- function renderFrame() {
2006
- const frame = BRAILLE_FRAMES[frameIdx % BRAILLE_FRAMES.length] ?? "\u280B";
2007
- const line = " " + colorFn(frame) + " " + current;
2008
- write("\r" + line);
2009
- frameIdx++;
2010
- }
2011
- renderFrame();
2012
- const timer = setInterval(renderFrame, 80);
2013
- return {
2014
- update(t) {
2015
- if (stopped) return;
2016
- current = t;
2017
- },
2018
- stop(final) {
2019
- if (stopped) return;
2020
- stopped = true;
2021
- clearInterval(timer);
2022
- write("\r\x1B[2K");
2023
- if (final) write(" " + greenFn("\u2713") + " " + final + "\n");
2200
+ } catch (err) {
2201
+ if (task.signal?.aborted || err?.name === "AbortError") {
2202
+ return { ok: false, error: { code: "task_cancelled", message: "Task cancelled by client" } };
2024
2203
  }
2025
- };
2026
- }
2027
- // ---- Progress bar ----
2028
- progress(text, total) {
2029
- if (this.mode === "quiet" || this.mode === "json") {
2030
2204
  return {
2031
- update() {
2032
- },
2033
- stop() {
2034
- }
2205
+ ok: false,
2206
+ error: { code: "adapter_dispatch_failed", message: err.message }
2035
2207
  };
2036
2208
  }
2037
- const isTTY = this.stream.isTTY === true;
2038
- const start = Date.now();
2039
- const write = this.write.bind(this);
2040
- const colorFn = this.cyan.bind(this);
2041
- const dimFn = this.dim.bind(this);
2042
- const greenFn = this.green.bind(this);
2043
- let last = 0;
2044
- let lastDetail = "";
2045
- let stopped = false;
2046
- const render = () => {
2047
- if (stopped) return;
2048
- const frac = total > 0 ? Math.min(1, Math.max(0, last / total)) : 0;
2049
- const pct = Math.floor(frac * 100);
2050
- const width = 20;
2051
- const filled = Math.floor(frac * width);
2052
- const bar = "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
2053
- const elapsed = (Date.now() - start) / 1e3;
2054
- const eta = frac > 0.01 ? Math.max(0, elapsed / frac - elapsed) : 0;
2055
- const etaStr = frac >= 1 ? "" : ` \xB7 ${eta < 1 ? "<1s" : Math.round(eta) + "s"} left`;
2056
- const detailStr = lastDetail ? ` \xB7 ${lastDetail}` : "";
2057
- const line = ` ${text} [${colorFn(bar)}] ${String(pct).padStart(3)}%${detailStr}${dimFn(etaStr)}`;
2058
- if (isTTY && this.colorEnabled) {
2059
- write("\r\x1B[2K" + line);
2060
- } else {
2061
- write(line + "\n");
2062
- }
2063
- };
2064
- render();
2065
- return {
2066
- update: (current, detail) => {
2067
- if (stopped) return;
2068
- last = current;
2069
- if (detail !== void 0) lastDetail = detail;
2070
- render();
2071
- },
2072
- stop: (final) => {
2073
- if (stopped) return;
2074
- stopped = true;
2075
- if (isTTY && this.colorEnabled) write("\r\x1B[2K");
2076
- if (final) write(" " + greenFn("\u2713") + " " + final + "\n");
2077
- }
2078
- };
2079
- }
2080
- // ---- JSON output ----
2081
- json(payload, opts) {
2082
- const indent = opts?.pretty ? 2 : void 0;
2083
- this.write(JSON.stringify(payload, null, indent) + "\n");
2084
- }
2085
- result(pretty, jsonPayload) {
2086
- if (this.mode === "pretty") {
2087
- pretty();
2088
- } else {
2089
- this.json(jsonPayload);
2090
- }
2091
2209
  }
2092
2210
  };
2093
- var _ui = null;
2094
- function getUI() {
2095
- if (!_ui) _ui = new UI();
2096
- return _ui;
2097
- }
2098
- function setUI(ui) {
2099
- _ui = ui;
2100
- }
2101
- function applyCommonFlags(argv) {
2102
- let mode = "pretty";
2103
- const isTTY = process.stdout.isTTY === true;
2104
- const noColorEnv = Boolean(process.env["NO_COLOR"]);
2105
- let color2 = isTTY && !noColorEnv;
2106
- const rest = [];
2107
- for (const arg of argv) {
2108
- switch (arg) {
2109
- case "--no-color":
2110
- color2 = false;
2111
- break;
2112
- case "--color":
2113
- color2 = true;
2114
- break;
2115
- case "--json":
2116
- case "--pretty-json":
2117
- mode = "json";
2118
- if (arg === "--json") rest.push(arg);
2119
- break;
2120
- case "--quiet":
2121
- mode = "quiet";
2122
- break;
2123
- default:
2124
- rest.push(arg);
2125
- }
2211
+ async function checkHealth2(baseUrl, apiKey) {
2212
+ try {
2213
+ const res = await fetch(`${baseUrl}/health`, {
2214
+ headers: { Authorization: `Bearer ${apiKey}` },
2215
+ signal: AbortSignal.timeout(2e3)
2216
+ });
2217
+ return res.ok;
2218
+ } catch {
2219
+ return false;
2126
2220
  }
2127
- return { mode, color: color2, restArgv: rest };
2128
2221
  }
2129
2222
 
2130
- // src/cli/util.ts
2131
- var DEFAULT_CLOUD_BASE_URL = "https://prismer.cloud";
2132
- var ANSI = {
2133
- reset: "\x1B[0m",
2134
- bold: "\x1B[1m",
2135
- dim: "\x1B[2m",
2136
- cyan: "\x1B[36m",
2137
- green: "\x1B[32m",
2138
- yellow: "\x1B[33m",
2139
- red: "\x1B[31m"
2140
- };
2141
- function color(kind, text) {
2142
- if (process.env.NO_COLOR === "1" || process.env.NO_COLOR === "true") return text;
2143
- return `${ANSI[kind]}${text}${ANSI.reset}`;
2144
- }
2145
- function printJson(v) {
2146
- getUI().json(v, { pretty: true });
2223
+ // src/config.ts
2224
+ var TOML = __toESM(require("@iarna/toml"), 1);
2225
+ var import_node_fs2 = require("fs");
2226
+ var import_node_os2 = require("os");
2227
+ var import_node_path2 = require("path");
2228
+ var import_zod5 = require("zod");
2229
+ var ConfigSchema = import_zod5.z.object({
2230
+ /** API key from `prismer setup`, or env override. */
2231
+ api_key: import_zod5.z.string().min(1),
2232
+ /** Cloud REST + WS base; `ws://` is derived by stripping `http`. */
2233
+ cloud_api_base: import_zod5.z.string().url(),
2234
+ /** Stable per-machine daemon identifier. Generated once on first setup. */
2235
+ daemon_id: import_zod5.z.string().min(1),
2236
+ /** Optional adapter-specific overrides keyed by adapter name. */
2237
+ adapters: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.record(import_zod5.z.string(), import_zod5.z.unknown())).optional(),
2238
+ /** Local daemon shell execution. Default disabled. */
2239
+ shell: import_zod5.z.object({
2240
+ enabled: import_zod5.z.boolean().default(false),
2241
+ default_cwd: import_zod5.z.string().optional(),
2242
+ defaultCwd: import_zod5.z.string().optional(),
2243
+ shell: import_zod5.z.enum(["bash", "zsh", "sh"]).optional(),
2244
+ max_timeout_ms: import_zod5.z.number().int().positive().optional(),
2245
+ maxTimeoutMs: import_zod5.z.number().int().positive().optional(),
2246
+ max_output_bytes: import_zod5.z.number().int().positive().optional(),
2247
+ maxOutputBytes: import_zod5.z.number().int().positive().optional(),
2248
+ allowed_workspaces: import_zod5.z.array(import_zod5.z.string()).optional(),
2249
+ allowedWorkspaces: import_zod5.z.array(import_zod5.z.string()).optional()
2250
+ }).optional(),
2251
+ /** Local cache settings. */
2252
+ cache: import_zod5.z.object({
2253
+ max_bytes: import_zod5.z.number().int().positive().default(5 * 1024 * 1024 * 1024)
2254
+ }).optional()
2255
+ });
2256
+ function resolvePaths(home) {
2257
+ const root = home ?? process.env.PRISMER_HOME ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".prismer");
2258
+ return {
2259
+ root,
2260
+ configFile: (0, import_node_path2.join)(root, "config.toml"),
2261
+ localDb: (0, import_node_path2.join)(root, "local.db"),
2262
+ cacheDir: (0, import_node_path2.join)(root, "cache"),
2263
+ logsDir: (0, import_node_path2.join)(root, "logs"),
2264
+ runsDir: (0, import_node_path2.join)(root, "runs")
2265
+ };
2147
2266
  }
2148
- function exitWithError(message, opts) {
2149
- const o = typeof opts === "number" ? { exitCode: opts } : opts ?? {};
2150
- const exitCode = o.exitCode ?? 1;
2151
- const ui = getUI();
2152
- if (ui.mode === "json") {
2153
- const payload = {
2154
- ok: false,
2155
- error: { code: o.code ?? "cli_error", message },
2156
- ...o.details ? { details: o.details } : {}
2157
- };
2158
- ui.json(payload, { pretty: true });
2159
- } else {
2160
- process.stderr.write(`Error: ${message}
2161
- `);
2162
- }
2163
- process.exit(exitCode);
2267
+ function configExists(paths = resolvePaths()) {
2268
+ return (0, import_node_fs2.existsSync)(paths.configFile);
2164
2269
  }
2165
- function normalizeCloudUrl(input) {
2166
- const raw = input.trim();
2167
- if (!raw) throw new Error("Cloud URL is empty.");
2168
- if (/^[a-z][a-z0-9+\-.]*:\/\//i.test(raw)) {
2169
- let parsed;
2170
- try {
2171
- parsed = new URL(raw);
2172
- } catch {
2173
- throw new Error(`Invalid --cloud URL: ${raw}`);
2174
- }
2175
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2176
- throw new Error(`Invalid --cloud URL scheme: ${parsed.protocol} (expected http:// or https://)`);
2177
- }
2178
- return raw.replace(/\/$/, "");
2179
- }
2180
- if (!/^[A-Za-z0-9.\-_:[\]/]+$/.test(raw)) {
2181
- throw new Error(`Invalid --cloud URL: ${raw} (must be http://\u2026 or https://\u2026 or host:port)`);
2182
- }
2183
- const candidate = `http://${raw}`;
2184
- try {
2185
- new URL(candidate);
2186
- } catch {
2187
- throw new Error(`Invalid --cloud URL: ${raw}`);
2270
+ function loadConfig(paths = resolvePaths()) {
2271
+ if (!(0, import_node_fs2.existsSync)(paths.configFile)) {
2272
+ throw new Error(
2273
+ `Config not found at ${paths.configFile}. Run \`prismer setup\` to create it.`
2274
+ );
2188
2275
  }
2189
- return candidate.replace(/\/$/, "");
2190
- }
2191
- function runAction(fn, opts = {}) {
2192
- return async (...args) => {
2193
- try {
2194
- await fn(...args);
2195
- } catch (err) {
2196
- const raw = err instanceof Error ? err.message : String(err);
2197
- const message = opts.sanitize ? opts.sanitize(raw) : raw;
2198
- exitWithError(message, { code: opts.code });
2199
- }
2276
+ const raw = (0, import_node_fs2.readFileSync)(paths.configFile, "utf8");
2277
+ const parsed = TOML.parse(raw);
2278
+ const merged = {
2279
+ ...parsed,
2280
+ api_key: process.env.PRISMER_API_KEY ?? parsed.api_key,
2281
+ cloud_api_base: process.env.PRISMER_BASE_URL ?? parsed.cloud_api_base
2200
2282
  };
2201
- }
2202
- function printBanner(opts = {}) {
2203
- const ui = getUI();
2204
- if (opts.compact) {
2205
- ui.smallHeader("Runtime CLI v1.9.3");
2206
- return;
2283
+ const result = ConfigSchema.safeParse(merged);
2284
+ if (!result.success) {
2285
+ const detail = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
2286
+ throw new Error(`Invalid config at ${paths.configFile}: ${detail}`);
2207
2287
  }
2208
- ui.banner("Runtime CLI v1.9.3", { full: true });
2209
- }
2210
- function ok(label, detail) {
2211
- getUI().ok(label, detail);
2212
- }
2213
- function warn(label, detail) {
2214
- getUI().warn(label, detail);
2215
- }
2216
- function fail(label, detail) {
2217
- getUI().fail(label, detail);
2218
- }
2219
- function tip(command, detail) {
2220
- const text = detail ? `${command} ${detail}` : command;
2221
- getUI().tip(text);
2222
- }
2223
- function pidFilePath(paths) {
2224
- return (0, import_node_path3.join)(paths.root, "daemon.pid");
2225
- }
2226
- function writePidFile(paths, pid) {
2227
- (0, import_node_fs3.writeFileSync)(pidFilePath(paths), `${pid}
2228
- `, "utf8");
2229
- }
2230
- function readPidFile(paths) {
2231
- const p = pidFilePath(paths);
2232
- if (!(0, import_node_fs3.existsSync)(p)) return void 0;
2233
- const raw = (0, import_node_fs3.readFileSync)(p, "utf8").trim();
2234
- const pid = Number.parseInt(raw, 10);
2235
- return Number.isFinite(pid) ? pid : void 0;
2288
+ return result.data;
2236
2289
  }
2237
- function clearPidFile(paths) {
2238
- const p = pidFilePath(paths);
2239
- if ((0, import_node_fs3.existsSync)(p)) {
2240
- try {
2241
- (0, import_node_fs3.unlinkSync)(p);
2242
- } catch {
2243
- }
2290
+ function saveConfig(config, paths = resolvePaths()) {
2291
+ if (!(0, import_node_fs2.existsSync)(paths.root)) {
2292
+ (0, import_node_fs2.mkdirSync)(paths.root, { recursive: true });
2244
2293
  }
2245
- }
2246
- function pidAlive(pid) {
2247
- try {
2248
- process.kill(pid, 0);
2249
- return true;
2250
- } catch {
2251
- return false;
2294
+ if (!(0, import_node_fs2.existsSync)((0, import_node_path2.dirname)(paths.configFile))) {
2295
+ (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(paths.configFile), { recursive: true });
2252
2296
  }
2297
+ ConfigSchema.parse(config);
2298
+ (0, import_node_fs2.writeFileSync)(paths.configFile, TOML.stringify(config), "utf8");
2299
+ }
2300
+ function deriveWsUrl(httpBase) {
2301
+ const u = new URL(httpBase);
2302
+ u.protocol = u.protocol === "https:" ? "wss:" : "ws:";
2303
+ u.pathname = (u.pathname.replace(/\/$/, "") || "") + "/ws";
2304
+ return u.toString();
2253
2305
  }
2254
2306
 
2255
2307
  // src/cli/commands/adapter.ts
2308
+ init_util();
2309
+ init_ui();
2256
2310
  var BUILTIN_ADAPTERS = [hermesAdapter, claudeCodeAdapter, openclawAdapter, codexAdapter];
2257
2311
  var INSTALL_SPECS = {
2258
2312
  "claude-code": {
@@ -3003,6 +3057,32 @@ var MIGRATIONS = [
3003
3057
  );
3004
3058
  CREATE INDEX IF NOT EXISTS idx_files_hash ON workspace_files_mirror (content_hash);
3005
3059
  `
3060
+ },
3061
+ {
3062
+ version: 3,
3063
+ up: `
3064
+ -- Asset metadata index (#filename reference resolution \u2014 daemon/asset/metadata-index.ts)
3065
+ CREATE TABLE IF NOT EXISTS asset_metadata_index (
3066
+ workspace_id TEXT NOT NULL,
3067
+ asset_id TEXT NOT NULL,
3068
+ content_hash TEXT NOT NULL,
3069
+ filename TEXT,
3070
+ folder_path TEXT,
3071
+ mime TEXT NOT NULL,
3072
+ kind TEXT NOT NULL,
3073
+ size_bytes INTEGER NOT NULL DEFAULT 0,
3074
+ description TEXT,
3075
+ asset_index_seq INTEGER NOT NULL,
3076
+ updated_at INTEGER NOT NULL,
3077
+ PRIMARY KEY (workspace_id, asset_id)
3078
+ );
3079
+
3080
+ CREATE INDEX IF NOT EXISTS idx_asset_meta_filename
3081
+ ON asset_metadata_index(workspace_id, filename);
3082
+
3083
+ CREATE INDEX IF NOT EXISTS idx_asset_meta_seq
3084
+ ON asset_metadata_index(workspace_id, asset_index_seq);
3085
+ `
3006
3086
  }
3007
3087
  ];
3008
3088
  function runSql(db, sql) {
@@ -3035,6 +3115,8 @@ function runMigrations(db) {
3035
3115
  }
3036
3116
 
3037
3117
  // src/cli/commands/agent.ts
3118
+ init_util();
3119
+ init_ui();
3038
3120
  var ADAPTER_BINARY = {
3039
3121
  "claude-code": "claude",
3040
3122
  codex: "codex",
@@ -3416,6 +3498,8 @@ function whichBinary2(bin) {
3416
3498
  var import_commander3 = require("commander");
3417
3499
  var import_node_fs6 = require("fs");
3418
3500
  var import_node_path6 = require("path");
3501
+ init_util();
3502
+ init_ui();
3419
3503
  function buildAssetCommand() {
3420
3504
  const cmd = new import_commander3.Command("asset").description("Inspect IM assets");
3421
3505
  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) => {
@@ -3612,6 +3696,7 @@ function stringField(obj, key) {
3612
3696
 
3613
3697
  // src/cli/commands/banner.ts
3614
3698
  var import_commander4 = require("commander");
3699
+ init_util();
3615
3700
  function buildBannerCommand() {
3616
3701
  return new import_commander4.Command("banner").description("Show the Prismer runtime CLI banner").option("--compact", "Show a single-line banner").option("--json", "Accept --json for global flag compatibility (banner is suppressed in JSON mode)").action((opts) => {
3617
3702
  printBanner({ compact: opts.compact });
@@ -3620,6 +3705,8 @@ function buildBannerCommand() {
3620
3705
 
3621
3706
  // src/cli/commands/chat.ts
3622
3707
  var import_commander5 = require("commander");
3708
+ init_util();
3709
+ init_ui();
3623
3710
  function buildChatCommand() {
3624
3711
  const cmd = new import_commander5.Command("chat").description("Use IM chat and group APIs");
3625
3712
  cmd.command("me").description("Show the current IM identity").option("--json", "Print raw JSON response").action(async (opts) => {
@@ -3794,6 +3881,8 @@ function sanitizeError(message) {
3794
3881
 
3795
3882
  // src/cli/commands/config.ts
3796
3883
  var import_commander6 = require("commander");
3884
+ init_util();
3885
+ init_ui();
3797
3886
  var SETTABLE_KEYS = ["cloud_api_base", "api_key", "daemon_id"];
3798
3887
  function redactApiKey(key) {
3799
3888
  if (!key.startsWith("sk-prismer-")) return "***";
@@ -3889,6 +3978,8 @@ function buildConfigCommand() {
3889
3978
 
3890
3979
  // src/cli/commands/cookbook.ts
3891
3980
  var import_commander7 = require("commander");
3981
+ init_util();
3982
+ init_ui();
3892
3983
  function buildCookbookCommand() {
3893
3984
  const cmd = new import_commander7.Command("cookbook").description("Run CLI-only 54release MVP regression suites");
3894
3985
  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) => {
@@ -4146,13 +4237,13 @@ function parsePositiveInt2(value) {
4146
4237
  // src/cli/commands/daemon.ts
4147
4238
  var import_commander8 = require("commander");
4148
4239
  var import_node_child_process7 = require("child_process");
4149
- var import_node_fs13 = require("fs");
4240
+ var import_node_fs14 = require("fs");
4150
4241
  var import_promises = require("timers/promises");
4151
- var import_node_path9 = require("path");
4242
+ var import_node_path10 = require("path");
4152
4243
 
4153
4244
  // src/daemon/runner.ts
4154
4245
  var import_node_events3 = require("events");
4155
- var import_node_fs12 = require("fs");
4246
+ var import_node_fs13 = require("fs");
4156
4247
  var import_node_os4 = require("os");
4157
4248
 
4158
4249
  // src/adapters/registry.ts
@@ -4687,6 +4778,14 @@ async function handleDispatch(payload, requestId, deps) {
4687
4778
  sendReply(deps.ws, reply, requestId);
4688
4779
  return reply;
4689
4780
  }
4781
+ let hashRefResult = { text: payload.prompt, resolutions: [] };
4782
+ if (deps.assetMetadataIndexes && profile.workspaceId) {
4783
+ const assetIndex = deps.assetMetadataIndexes.get(profile.workspaceId);
4784
+ if (assetIndex) {
4785
+ hashRefResult = await resolveHashRefs(payload.prompt, assetIndex, deps.cloud);
4786
+ payload.prompt = hashRefResult.text;
4787
+ }
4788
+ }
4690
4789
  const rewrittenPrompt = await deps.uriResolver.rewrite(payload.prompt, { pin: true });
4691
4790
  resolvedHashes.push(...rewrittenPrompt.resolvedHashes);
4692
4791
  let rewrittenContext = [];
@@ -4901,6 +5000,84 @@ function isTextLikeMime(mime) {
4901
5000
  if (m.endsWith("+json") || m.endsWith("+xml") || m.endsWith("+csv")) return true;
4902
5001
  return false;
4903
5002
  }
5003
+ var HASH_REF_RE = /(?:^|\s)#([^\s#]+)/g;
5004
+ var HEX_COLOR_RE = /^[0-9a-fA-F]{3,8}$/;
5005
+ var FILE_EXT_RE = /\.[a-zA-Z0-9]{1,10}$/;
5006
+ var TRAILING_PUNCT_RE = /[,.;:!?)\]}'"]+$/;
5007
+ async function resolveHashRefs(prompt, assetIndex, cloud) {
5008
+ const resolutions = [];
5009
+ const candidates = [];
5010
+ let match;
5011
+ const re = new RegExp(HASH_REF_RE.source, "g");
5012
+ while ((match = re.exec(prompt)) !== null) {
5013
+ const refName = match[1];
5014
+ const leading = match[0].startsWith("#") ? 0 : 1;
5015
+ const start = match.index + leading;
5016
+ const end = match.index + match[0].length;
5017
+ if (HEX_COLOR_RE.test(refName)) continue;
5018
+ let cleanRef = refName;
5019
+ let stripped = "";
5020
+ const punctMatch = TRAILING_PUNCT_RE.exec(cleanRef);
5021
+ if (punctMatch) {
5022
+ stripped = punctMatch[0];
5023
+ cleanRef = cleanRef.slice(0, -stripped.length);
5024
+ }
5025
+ if (!cleanRef) continue;
5026
+ if (HEX_COLOR_RE.test(cleanRef)) continue;
5027
+ const hasExtension = FILE_EXT_RE.test(cleanRef);
5028
+ candidates.push({ ref: cleanRef, start, end: end - stripped.length, hasExtension });
5029
+ }
5030
+ if (candidates.length === 0) {
5031
+ return { text: prompt, resolutions: [] };
5032
+ }
5033
+ const allFilenames = candidates.map((c) => c.ref);
5034
+ const localResults = assetIndex.resolveByFilenames(allFilenames);
5035
+ const needsCloud = candidates.filter(
5036
+ (c) => c.hasExtension && !localResults.has(c.ref)
5037
+ );
5038
+ const cloudResults = /* @__PURE__ */ new Map();
5039
+ if (needsCloud.length > 0) {
5040
+ await Promise.allSettled(
5041
+ needsCloud.map(async (c) => {
5042
+ try {
5043
+ const items = await cloud.get(
5044
+ `/api/im/assets?workspaceId=${encodeURIComponent(assetIndex.workspaceId)}&q=${encodeURIComponent(c.ref)}&limit=1`
5045
+ );
5046
+ if (Array.isArray(items) && items.length > 0) {
5047
+ const item = items[0];
5048
+ cloudResults.set(c.ref, item.contentHash);
5049
+ }
5050
+ } catch {
5051
+ }
5052
+ })
5053
+ );
5054
+ }
5055
+ for (const c of candidates) {
5056
+ const local = localResults.get(c.ref);
5057
+ if (local) {
5058
+ resolutions.push({
5059
+ ref: c.ref,
5060
+ start: c.start,
5061
+ end: c.end,
5062
+ resolvedUri: `prismer://workspace/${encodeURIComponent(assetIndex.workspaceId)}/asset/${local.contentHash}`
5063
+ });
5064
+ } else if (c.hasExtension) {
5065
+ const cloudHash = cloudResults.get(c.ref);
5066
+ resolutions.push({
5067
+ ref: c.ref,
5068
+ start: c.start,
5069
+ end: c.end,
5070
+ resolvedUri: cloudHash ? `prismer://workspace/${encodeURIComponent(assetIndex.workspaceId)}/asset/${cloudHash}` : void 0
5071
+ });
5072
+ }
5073
+ }
5074
+ let result = prompt;
5075
+ const sorted = [...resolutions].filter((r) => r.resolvedUri).sort((a, b) => b.start - a.start);
5076
+ for (const r of sorted) {
5077
+ result = result.slice(0, r.start) + r.resolvedUri + result.slice(r.end);
5078
+ }
5079
+ return { text: result, resolutions };
5080
+ }
4904
5081
  async function resolveAssetRefs(refs, cache) {
4905
5082
  const out = { promptBlocks: [], observability: [], pinnedHashes: [] };
4906
5083
  if (!refs || refs.length === 0) return out;
@@ -4973,8 +5150,8 @@ async function resolveAssetRefs(refs, cache) {
4973
5150
  return out;
4974
5151
  }
4975
5152
  function formatInlineAssetBlock(ref, mime, body, strategy) {
4976
- const header = `[Attached file] id=${ref.assetId} mime=${mime ?? "unknown"}${strategy === "inline-text-truncated" ? " (truncated)" : ""}`;
4977
- return `${header}
5153
+ const header2 = `[Attached file] id=${ref.assetId} mime=${mime ?? "unknown"}${strategy === "inline-text-truncated" ? " (truncated)" : ""}`;
5154
+ return `${header2}
4978
5155
  ---
4979
5156
  ${body}
4980
5157
  ---`;
@@ -5277,20 +5454,35 @@ var LocalServer = class {
5277
5454
  respond(res, 204, null);
5278
5455
  return;
5279
5456
  }
5457
+ const handlers = [];
5280
5458
  if (this.opts.attachMemory) {
5281
- void this.opts.attachMemory(req, res).then((handled) => {
5282
- if (handled) return;
5283
- this.routeStandard(req, res);
5284
- }).catch((err) => {
5285
- respond(res, 500, {
5286
- error: "attach_memory_threw",
5287
- message: err instanceof Error ? err.message : String(err)
5288
- });
5289
- });
5459
+ handlers.push({ name: "memory", fn: this.opts.attachMemory });
5460
+ }
5461
+ if (this.opts.attachAsset) {
5462
+ handlers.push({ name: "asset", fn: this.opts.attachAsset });
5463
+ }
5464
+ if (handlers.length > 0) {
5465
+ void this.runHandlers(req, res, handlers, 0);
5290
5466
  return;
5291
5467
  }
5292
5468
  this.routeStandard(req, res);
5293
5469
  }
5470
+ async runHandlers(req, res, handlers, idx) {
5471
+ if (idx >= handlers.length) {
5472
+ this.routeStandard(req, res);
5473
+ return;
5474
+ }
5475
+ try {
5476
+ const handled = await handlers[idx].fn(req, res);
5477
+ if (handled) return;
5478
+ await this.runHandlers(req, res, handlers, idx + 1);
5479
+ } catch (err) {
5480
+ respond(res, 500, {
5481
+ error: `attach_${handlers[idx].name}_threw`,
5482
+ message: err instanceof Error ? err.message : String(err)
5483
+ });
5484
+ }
5485
+ }
5294
5486
  routeStandard(req, res) {
5295
5487
  const url = req.url ?? "/";
5296
5488
  if (req.method === "GET" && url === "/healthz") {
@@ -5305,7 +5497,8 @@ var LocalServer = class {
5305
5497
  wsConnected: state.wsConnected,
5306
5498
  hostedAgents: state.hostedAgents,
5307
5499
  observability: state.observability,
5308
- memoryReady: this.opts.attachMemory != null
5500
+ memoryReady: this.opts.attachMemory != null,
5501
+ assetReady: this.opts.attachAsset != null
5309
5502
  });
5310
5503
  return;
5311
5504
  }
@@ -5759,13 +5952,14 @@ var MemoryStore = class {
5759
5952
  const pageId = existing?.id ?? `page_${(0, import_node_crypto2.randomUUID)().replace(/-/g, "").slice(0, 22)}`;
5760
5953
  const newVersion = (existing?.version ?? 0) + 1;
5761
5954
  const createdAt = existing?.createdAt ?? now;
5955
+ const staleFlag = input.stale ? 1 : 0;
5762
5956
  const insertPage = db.prepare(`
5763
5957
  INSERT INTO memory_pages (
5764
5958
  id, workspaceId, path, title, description, contentHash, version,
5765
5959
  pageType, visibilityKind, visibilityImUserId, encrypted, stale,
5766
5960
  archivedAt, sourceAssetId, sourceRefsJson, syncStatus,
5767
5961
  createdAt, updatedAt
5768
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, NULL, ?, ?, 'local-only', ?, ?)
5962
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, NULL, ?, ?, 'local-only', ?, ?)
5769
5963
  ON CONFLICT(workspaceId, path) DO UPDATE SET
5770
5964
  title = excluded.title,
5771
5965
  description = excluded.description,
@@ -5776,6 +5970,7 @@ var MemoryStore = class {
5776
5970
  visibilityImUserId = excluded.visibilityImUserId,
5777
5971
  sourceAssetId = excluded.sourceAssetId,
5778
5972
  sourceRefsJson = excluded.sourceRefsJson,
5973
+ stale = excluded.stale,
5779
5974
  updatedAt = excluded.updatedAt
5780
5975
  `);
5781
5976
  const insertVersion = db.prepare(`
@@ -5803,6 +5998,7 @@ var MemoryStore = class {
5803
5998
  input.pageType ?? "leaf",
5804
5999
  visibility.kind,
5805
6000
  visibilityImUserId,
6001
+ staleFlag,
5806
6002
  input.sourceAssetId ?? null,
5807
6003
  sourceRefsJson,
5808
6004
  createdAt,
@@ -5883,6 +6079,16 @@ var MemoryStore = class {
5883
6079
  dbPath: this.opts.dbPath
5884
6080
  };
5885
6081
  }
6082
+ /**
6083
+ * Record sync cursor for incremental sync. Used by cloud-sync.ts to
6084
+ * persist the high-water mark for future cursor-based catch-up.
6085
+ */
6086
+ recordCursor(workspaceId, cursor) {
6087
+ const now = Date.now();
6088
+ this.requireDb().prepare(
6089
+ `INSERT OR REPLACE INTO memory_inbox_cursor (workspaceId, cursor, updatedAt) VALUES (?, ?, ?)`
6090
+ ).run(workspaceId, cursor, now);
6091
+ }
5886
6092
  /**
5887
6093
  * Internal accessor for outbox.ts — outbox writes its own table within the
5888
6094
  * same DB. Returning the live Database handle keeps outbox transactions
@@ -6533,6 +6739,84 @@ function defaultLog() {
6533
6739
  };
6534
6740
  }
6535
6741
 
6742
+ // src/daemon/memory/cloud-sync.ts
6743
+ var LOG = "[CloudMemorySync]";
6744
+ var CLOUD_PAGE_LIMIT = 300;
6745
+ async function initialSyncFromCloud(runtime, cloud, workspaceId) {
6746
+ const slot = runtime.peek(workspaceId);
6747
+ if (!slot) {
6748
+ console.log(`${LOG} No store for workspace=${workspaceId} \u2014 skipping`);
6749
+ return { pulled: 0, skipped: 0 };
6750
+ }
6751
+ const cursorRow = slot.store.rawDb().prepare("SELECT cursor FROM memory_inbox_cursor WHERE workspaceId = ?").get(workspaceId);
6752
+ if (cursorRow) {
6753
+ console.log(`${LOG} Workspace=${workspaceId} already synced (cursor: ${cursorRow.cursor.slice(0, 20)}...) \u2014 skip`);
6754
+ return { pulled: 0, skipped: 0 };
6755
+ }
6756
+ console.log(`${LOG} Fetching cloud pages for workspace=${workspaceId}...`);
6757
+ const resp = await cloud.request(
6758
+ "GET",
6759
+ `/api/im/memory/pages?workspaceId=${encodeURIComponent(workspaceId)}&limit=${CLOUD_PAGE_LIMIT}&stale=all`,
6760
+ { timeoutMs: 15e3 }
6761
+ );
6762
+ if (!resp.ok) {
6763
+ console.warn(
6764
+ `${LOG} Cloud GET /memory/pages returned ${resp.status}: ${resp.error?.message ?? "unknown"}`
6765
+ );
6766
+ return { pulled: 0, skipped: 0 };
6767
+ }
6768
+ const envelope2 = resp.data;
6769
+ if (!envelope2 || !envelope2.ok) {
6770
+ console.log(`${LOG} Cloud returned non-ok envelope for workspace=${workspaceId}`);
6771
+ return { pulled: 0, skipped: 0 };
6772
+ }
6773
+ const pages = envelope2.data;
6774
+ if (!pages || !Array.isArray(pages) || pages.length === 0) {
6775
+ console.log(`${LOG} No cloud pages to sync for workspace=${workspaceId}`);
6776
+ return { pulled: 0, skipped: 0 };
6777
+ }
6778
+ let pulled = 0;
6779
+ let skipped = 0;
6780
+ for (const page of pages) {
6781
+ let content = page.content ?? "";
6782
+ if (!content) {
6783
+ try {
6784
+ const detailResp = await cloud.request(
6785
+ "GET",
6786
+ `/api/im/memory/pages/${encodeURIComponent(page.id)}?workspaceId=${encodeURIComponent(workspaceId)}`,
6787
+ { timeoutMs: 5e3 }
6788
+ );
6789
+ if (detailResp.ok && detailResp.data?.data?.content) {
6790
+ content = detailResp.data.data.content;
6791
+ }
6792
+ } catch {
6793
+ }
6794
+ }
6795
+ const visibility = page.visibility === "agent" ? { kind: "agent", imUserId: "" } : { kind: "workspace" };
6796
+ try {
6797
+ slot.store.write({
6798
+ workspaceId,
6799
+ path: page.path,
6800
+ title: page.title ?? void 0,
6801
+ content: content || "",
6802
+ pageType: page.pageType || "leaf",
6803
+ visibility,
6804
+ actorImUserId: "cloud-sync",
6805
+ actorKind: "agent"
6806
+ });
6807
+ pulled++;
6808
+ } catch (err) {
6809
+ console.warn(`${LOG} write failed for ${page.path}:`, err.message);
6810
+ skipped++;
6811
+ }
6812
+ }
6813
+ slot.store.recordCursor(workspaceId, `synced:${Date.now()}`);
6814
+ console.log(
6815
+ `${LOG} Synced ${pulled} pages${skipped ? `, ${skipped} skipped` : ""} for workspace=${workspaceId}`
6816
+ );
6817
+ return { pulled, skipped };
6818
+ }
6819
+
6536
6820
  // src/daemon/memory/runner-wiring.ts
6537
6821
  function attachMemoryRunner(opts) {
6538
6822
  const runtime = new MemoryRuntime({ baseDir: opts.baseDir, deviceId: opts.deviceId });
@@ -6563,6 +6847,24 @@ function attachMemoryRunner(opts) {
6563
6847
  }
6564
6848
  };
6565
6849
  }
6850
+ async function syncMemoryFromCloud(wiring, cloud, workspaceIds) {
6851
+ const uniqueIds = [...new Set(workspaceIds.filter(Boolean))];
6852
+ if (uniqueIds.length === 0) return;
6853
+ console.log(`[MemorySync] Initial cloud-to-local sync for ${uniqueIds.length} workspace(s)...`);
6854
+ for (const wsId of uniqueIds) {
6855
+ try {
6856
+ wiring.runtime.resolve(wsId);
6857
+ const result = await initialSyncFromCloud(wiring.runtime, cloud, wsId);
6858
+ if (result.pulled > 0 || result.skipped > 0) {
6859
+ console.log(
6860
+ `[MemorySync] workspace=${wsId}: ${result.pulled} pulled, ${result.skipped} skipped`
6861
+ );
6862
+ }
6863
+ } catch (err) {
6864
+ console.error(`[MemorySync] workspace=${wsId} failed:`, err.message);
6865
+ }
6866
+ }
6867
+ }
6566
6868
 
6567
6869
  // src/daemon/memory/fork/select-memories.ts
6568
6870
  var SELECT_MEMORIES_SYSTEM_PROMPT = [
@@ -6970,8 +7272,235 @@ function parsePrismerUri(uri) {
6970
7272
  return { workspaceId, path: path7 };
6971
7273
  }
6972
7274
 
6973
- // src/daemon/outbox-watcher.ts
7275
+ // src/daemon/asset/metadata-index.ts
6974
7276
  var import_node_fs10 = require("fs");
7277
+ var import_node_path8 = require("path");
7278
+ var DEFAULT_LIMIT = 8;
7279
+ var PULL_PAGE_SIZE = 500;
7280
+ var THROTTLE_MS = 3e4;
7281
+ function rowToMetadata(row) {
7282
+ return {
7283
+ assetId: row.asset_id,
7284
+ contentHash: row.content_hash,
7285
+ filename: row.filename,
7286
+ folderPath: row.folder_path,
7287
+ mime: row.mime,
7288
+ kind: row.kind,
7289
+ sizeBytes: row.size_bytes,
7290
+ description: row.description,
7291
+ assetIndexSeq: row.asset_index_seq
7292
+ };
7293
+ }
7294
+ var AssetMetadataIndex = class {
7295
+ db;
7296
+ cloud;
7297
+ /** Workspace ID — exposed for prismer:// URI construction. */
7298
+ workspaceId;
7299
+ cursorPath;
7300
+ _lastSyncMs = 0;
7301
+ constructor(opts) {
7302
+ this.db = opts.db;
7303
+ this.cloud = opts.cloud;
7304
+ this.workspaceId = opts.workspaceId;
7305
+ if (!(0, import_node_fs10.existsSync)(opts.workspaceStateDir)) {
7306
+ (0, import_node_fs10.mkdirSync)(opts.workspaceStateDir, { recursive: true });
7307
+ }
7308
+ this.cursorPath = (0, import_node_path8.join)(opts.workspaceStateDir, "asset-metadata-cursor.json");
7309
+ }
7310
+ /** Persisted cursor for this workspace, or 0 on first run / corrupted file. */
7311
+ readCursor() {
7312
+ if (!(0, import_node_fs10.existsSync)(this.cursorPath)) return 0;
7313
+ try {
7314
+ const parsed = JSON.parse((0, import_node_fs10.readFileSync)(this.cursorPath, "utf8"));
7315
+ if (parsed.workspaceId !== this.workspaceId) return 0;
7316
+ return parsed.cursor;
7317
+ } catch {
7318
+ return 0;
7319
+ }
7320
+ }
7321
+ writeCursor(cursor) {
7322
+ const payload = {
7323
+ workspaceId: this.workspaceId,
7324
+ cursor,
7325
+ writtenAt: Date.now()
7326
+ };
7327
+ (0, import_node_fs10.writeFileSync)(this.cursorPath, JSON.stringify(payload, null, 2));
7328
+ }
7329
+ /**
7330
+ * Pull incremental asset metadata changes since the persisted cursor and
7331
+ * upsert into the local index. Newer rows overwrite older ones by
7332
+ * (workspace_id, asset_id) primary key.
7333
+ *
7334
+ * Returns the count of items applied + the new cursor. Throttled: if called
7335
+ * within 30s of the last successful pull, returns immediately.
7336
+ */
7337
+ async pullDelta(opts) {
7338
+ const now = Date.now();
7339
+ if (now - this._lastSyncMs < THROTTLE_MS) {
7340
+ return { applied: 0, cursor: this.readCursor() };
7341
+ }
7342
+ const since = this.readCursor();
7343
+ const sinceParam = since > 0 ? `&since=${since}` : "";
7344
+ const envelope2 = await this.cloud.get(
7345
+ `/api/im/assets/index?workspaceId=${encodeURIComponent(this.workspaceId)}&limit=${PULL_PAGE_SIZE}${sinceParam}`,
7346
+ { signal: opts?.signal }
7347
+ );
7348
+ const upsert = this.db.prepare(`
7349
+ INSERT INTO asset_metadata_index
7350
+ (workspace_id, asset_id, content_hash, filename, folder_path, mime, kind, size_bytes, description, asset_index_seq, updated_at)
7351
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
7352
+ ON CONFLICT(workspace_id, asset_id) DO UPDATE SET
7353
+ content_hash = excluded.content_hash,
7354
+ filename = excluded.filename,
7355
+ folder_path = excluded.folder_path,
7356
+ mime = excluded.mime,
7357
+ kind = excluded.kind,
7358
+ size_bytes = excluded.size_bytes,
7359
+ description = excluded.description,
7360
+ asset_index_seq = excluded.asset_index_seq,
7361
+ updated_at = excluded.updated_at
7362
+ `);
7363
+ const nowTs = Date.now();
7364
+ let applied = 0;
7365
+ const tx = this.db.transaction((items) => {
7366
+ for (const item of items) {
7367
+ upsert.run(
7368
+ this.workspaceId,
7369
+ item.assetId,
7370
+ item.contentHash,
7371
+ item.filename ?? null,
7372
+ item.folderPath ?? null,
7373
+ item.mime,
7374
+ item.kind,
7375
+ item.sizeBytes,
7376
+ item.description ?? null,
7377
+ item.assetIndexSeq,
7378
+ nowTs
7379
+ );
7380
+ applied += 1;
7381
+ }
7382
+ });
7383
+ try {
7384
+ tx(envelope2.items);
7385
+ this.writeCursor(envelope2.cursor);
7386
+ } catch (err) {
7387
+ throw err;
7388
+ }
7389
+ this._lastSyncMs = now;
7390
+ return { applied, cursor: envelope2.cursor };
7391
+ }
7392
+ /**
7393
+ * Search local index by filename or description substring.
7394
+ * Escapes LIKE wildcards (% and _). Default limit 8.
7395
+ */
7396
+ search(query, limit) {
7397
+ const escaped = query.replace(/%/g, "\\%").replace(/_/g, "\\_");
7398
+ const pattern = `%${escaped}%`;
7399
+ const limitVal = Math.min(Math.max(limit ?? DEFAULT_LIMIT, 1), 200);
7400
+ const rows = this.db.prepare(
7401
+ `SELECT * FROM asset_metadata_index
7402
+ WHERE workspace_id = ?
7403
+ AND (filename LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\')
7404
+ ORDER BY asset_index_seq DESC
7405
+ LIMIT ?`
7406
+ ).all(this.workspaceId, pattern, pattern, limitVal);
7407
+ return rows.map(rowToMetadata);
7408
+ }
7409
+ /** Exact match on filename column. Returns undefined if not indexed. */
7410
+ resolveByFilename(filename) {
7411
+ const row = this.db.prepare("SELECT * FROM asset_metadata_index WHERE workspace_id = ? AND filename = ?").get(this.workspaceId, filename);
7412
+ return row ? rowToMetadata(row) : void 0;
7413
+ }
7414
+ /** Batch exact match — returns Map for O(1) access. */
7415
+ resolveByFilenames(filenames) {
7416
+ if (filenames.length === 0) return /* @__PURE__ */ new Map();
7417
+ const placeholders = filenames.map(() => "?").join(",");
7418
+ const params = [this.workspaceId, ...filenames];
7419
+ const rows = this.db.prepare(
7420
+ `SELECT * FROM asset_metadata_index
7421
+ WHERE workspace_id = ? AND filename IN (${placeholders})`
7422
+ ).all(...params);
7423
+ const map = /* @__PURE__ */ new Map();
7424
+ for (const row of rows) {
7425
+ if (row.filename) map.set(row.filename, rowToMetadata(row));
7426
+ }
7427
+ return map;
7428
+ }
7429
+ };
7430
+
7431
+ // src/daemon/asset/rpc.ts
7432
+ var ASSET_PATH_PREFIX = "/local/asset/";
7433
+ function attachAssetRpc(opts) {
7434
+ return async (req, res) => {
7435
+ const url = req.url ?? "/";
7436
+ if (!url.startsWith(ASSET_PATH_PREFIX)) return false;
7437
+ const [pathOnly = ""] = url.split("?", 2);
7438
+ const subpath = pathOnly.slice(ASSET_PATH_PREFIX.length);
7439
+ const method = req.method ?? "GET";
7440
+ try {
7441
+ if (method === "POST" && subpath === "search") {
7442
+ const body = await readJson3(req);
7443
+ return handleSearch2(opts.resolveIndex, body, res);
7444
+ }
7445
+ respond3(res, 404, { error: "asset_route_not_found", path: url });
7446
+ return true;
7447
+ } catch (err) {
7448
+ respond3(res, 500, {
7449
+ error: "asset_rpc_failed",
7450
+ message: err instanceof Error ? err.message : String(err)
7451
+ });
7452
+ return true;
7453
+ }
7454
+ };
7455
+ }
7456
+ function handleSearch2(resolveIndex, body, res) {
7457
+ if (!body || typeof body !== "object") {
7458
+ return respond4002(res, "request body must be a JSON object");
7459
+ }
7460
+ const b = body;
7461
+ if (typeof b.workspaceId !== "string" || !b.workspaceId) {
7462
+ return respond4002(res, "workspaceId is required (string)");
7463
+ }
7464
+ if (typeof b.query !== "string" || !b.query.trim()) {
7465
+ return respond4002(res, "query is required (non-empty string)");
7466
+ }
7467
+ const index = resolveIndex(b.workspaceId);
7468
+ if (!index) {
7469
+ respond3(res, 404, {
7470
+ error: "workspace_index_not_found",
7471
+ workspaceId: b.workspaceId,
7472
+ message: "No asset metadata index for this workspace. Ensure the daemon has synced asset metadata."
7473
+ });
7474
+ return true;
7475
+ }
7476
+ const limit = typeof b.limit === "number" && b.limit > 0 ? b.limit : void 0;
7477
+ const items = index.search(b.query.trim(), limit);
7478
+ respond3(res, 200, { items });
7479
+ return true;
7480
+ }
7481
+ function respond3(res, status, body) {
7482
+ res.statusCode = status;
7483
+ res.setHeader("Content-Type", "application/json");
7484
+ res.end(JSON.stringify(body));
7485
+ }
7486
+ function respond4002(res, message) {
7487
+ respond3(res, 400, { error: "invalid_request", message });
7488
+ return true;
7489
+ }
7490
+ async function readJson3(req) {
7491
+ let raw = "";
7492
+ req.setEncoding("utf8");
7493
+ for await (const chunk of req) raw += chunk;
7494
+ if (!raw) return {};
7495
+ try {
7496
+ return JSON.parse(raw);
7497
+ } catch {
7498
+ throw new Error("invalid_json");
7499
+ }
7500
+ }
7501
+
7502
+ // src/daemon/outbox-watcher.ts
7503
+ var import_node_fs11 = require("fs");
6975
7504
  var path6 = __toESM(require("path"), 1);
6976
7505
  var DEFAULT_INTERVAL_MS = 2e3;
6977
7506
  var RESERVED_SUBDIR = "_uploaded";
@@ -7139,7 +7668,7 @@ var OutboxWatcher = class {
7139
7668
  async scanDir(dir, kind, task) {
7140
7669
  let entries = [];
7141
7670
  try {
7142
- entries = await import_node_fs10.promises.readdir(dir);
7671
+ entries = await import_node_fs11.promises.readdir(dir);
7143
7672
  } catch (err) {
7144
7673
  const code = err.code;
7145
7674
  if (code === "ENOENT") return;
@@ -7152,7 +7681,7 @@ var OutboxWatcher = class {
7152
7681
  const full = path6.join(dir, name);
7153
7682
  let st;
7154
7683
  try {
7155
- st = await import_node_fs10.promises.stat(full);
7684
+ st = await import_node_fs11.promises.stat(full);
7156
7685
  } catch {
7157
7686
  continue;
7158
7687
  }
@@ -7177,7 +7706,7 @@ var OutboxWatcher = class {
7177
7706
  this.log("warn", `skip ${filePath}: no active taskId (no dispatch/handoff received yet)`);
7178
7707
  return;
7179
7708
  }
7180
- const bytes = await import_node_fs10.promises.readFile(filePath);
7709
+ const bytes = await import_node_fs11.promises.readFile(filePath);
7181
7710
  const fileName = path6.basename(filePath);
7182
7711
  const blob = new Blob([new Uint8Array(bytes)]);
7183
7712
  const form = new FormData();
@@ -7273,8 +7802,8 @@ var ServicePool = class {
7273
7802
 
7274
7803
  // src/daemon/shell-executor.ts
7275
7804
  var import_node_child_process6 = require("child_process");
7276
- var import_node_fs11 = require("fs");
7277
- var import_node_path8 = require("path");
7805
+ var import_node_fs12 = require("fs");
7806
+ var import_node_path9 = require("path");
7278
7807
  var DEFAULT_OUTPUT_LIMIT = 256 * 1024;
7279
7808
  var DEFAULT_TIMEOUT = 6e4;
7280
7809
  var HARD_MAX_TIMEOUT = 30 * 6e4;
@@ -7310,7 +7839,7 @@ async function executeShellDispatch(payload, deps) {
7310
7839
  const command = readCommand(payload, execution);
7311
7840
  if (!command.trim()) return fail2(payload.taskId, "shell_command_required", "Shell command is required");
7312
7841
  const cwd = resolveCwd(execution.cwd, deps.config.defaultCwd);
7313
- if (!(0, import_node_fs11.existsSync)(cwd)) return fail2(payload.taskId, "shell_cwd_missing", `cwd does not exist: ${cwd}`);
7842
+ if (!(0, import_node_fs12.existsSync)(cwd)) return fail2(payload.taskId, "shell_cwd_missing", `cwd does not exist: ${cwd}`);
7314
7843
  const timeoutMs = Math.min(
7315
7844
  typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? payload.timeoutMs : deps.config.maxTimeoutMs,
7316
7845
  deps.config.maxTimeoutMs
@@ -7412,8 +7941,8 @@ function readCommand(payload, execution) {
7412
7941
  return payload.prompt;
7413
7942
  }
7414
7943
  function resolveCwd(raw, fallback) {
7415
- if (typeof raw !== "string" || raw.trim() === "") return (0, import_node_path8.resolve)(fallback);
7416
- return (0, import_node_path8.resolve)(raw);
7944
+ if (typeof raw !== "string" || raw.trim() === "") return (0, import_node_path9.resolve)(fallback);
7945
+ return (0, import_node_path9.resolve)(raw);
7417
7946
  }
7418
7947
  function clampNumber(value, fallback, min, max) {
7419
7948
  const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
@@ -7565,6 +8094,7 @@ var Runner = class extends import_node_events3.EventEmitter {
7565
8094
  localServer;
7566
8095
  outboxWatcher;
7567
8096
  memoryWiring;
8097
+ assetMetadataIndexes = /* @__PURE__ */ new Map();
7568
8098
  state = "idle";
7569
8099
  startedAt = 0;
7570
8100
  workspaceId = "";
@@ -7581,6 +8111,9 @@ var Runner = class extends import_node_events3.EventEmitter {
7581
8111
  this.paths = this.opts.paths ?? resolvePaths();
7582
8112
  this.config = this.opts.configOverride ?? loadConfig(this.paths);
7583
8113
  this.shellConfig = resolveShellConfig(this.config.shell);
8114
+ if (process.env.PRISMER_WORKSPACE_ID) {
8115
+ this.workspaceId = process.env.PRISMER_WORKSPACE_ID;
8116
+ }
7584
8117
  process.env.PRISMER_BASE_URL = this.config.cloud_api_base;
7585
8118
  process.env.PRISMER_API_KEY = this.config.api_key;
7586
8119
  this.db = openLocalDb(this.paths.localDb);
@@ -7618,6 +8151,16 @@ var Runner = class extends import_node_events3.EventEmitter {
7618
8151
  baseDir: `${this.paths.root}/memory`,
7619
8152
  deviceId: this.config.daemon_id
7620
8153
  });
8154
+ if (this.memoryWiring && this.workspaceId) {
8155
+ syncMemoryFromCloud(this.memoryWiring, this.cloud, [this.workspaceId]).catch(
8156
+ (err) => console.error("[Daemon] Initial memory sync failed:", err.message)
8157
+ );
8158
+ }
8159
+ if (this.workspaceId) {
8160
+ this.syncAssetMetadata(this.workspaceId).catch(
8161
+ (err) => console.error("[Daemon] Initial asset metadata sync failed:", err.message)
8162
+ );
8163
+ }
7621
8164
  const containerId = process.env.PRISMER_CONTAINER_ID;
7622
8165
  const isContainer = !!containerId || process.env.PRISMER_RUNTIME_MODE === "container";
7623
8166
  this.outboxWatcher = new OutboxWatcher({
@@ -7665,7 +8208,10 @@ var Runner = class extends import_node_events3.EventEmitter {
7665
8208
  // wiring step so it could be reviewed alongside the host-adapter
7666
8209
  // consumer (Hermes T2-B), which is what surfaces these routes to
7667
8210
  // an actual agent process.
7668
- attachMemory: this.memoryWiring ? attachMemoryRpc({ runtime: this.memoryWiring.runtime }) : void 0
8211
+ attachMemory: this.memoryWiring ? attachMemoryRpc({ runtime: this.memoryWiring.runtime }) : void 0,
8212
+ attachAsset: attachAssetRpc({
8213
+ resolveIndex: (workspaceId) => this.assetMetadataIndexes.get(workspaceId)
8214
+ })
7669
8215
  });
7670
8216
  await this.localServer.start();
7671
8217
  }
@@ -7861,10 +8407,10 @@ var Runner = class extends import_node_events3.EventEmitter {
7861
8407
  const rawJson = process.env.PRISMER_HOSTED_AGENT_JSON;
7862
8408
  let raw;
7863
8409
  if (rawFile) {
7864
- if (!(0, import_node_fs12.existsSync)(rawFile)) {
8410
+ if (!(0, import_node_fs13.existsSync)(rawFile)) {
7865
8411
  throw new Error(`PRISMER_HOSTED_AGENT_FILE not found: ${rawFile}`);
7866
8412
  }
7867
- raw = (0, import_node_fs12.readFileSync)(rawFile, "utf8");
8413
+ raw = (0, import_node_fs13.readFileSync)(rawFile, "utf8");
7868
8414
  } else if (rawJson) {
7869
8415
  raw = rawJson;
7870
8416
  }
@@ -7994,12 +8540,23 @@ var Runner = class extends import_node_events3.EventEmitter {
7994
8540
  case "workspace_file.changed":
7995
8541
  this.onWorkspaceFileChanged(msg.payload);
7996
8542
  return;
8543
+ case "asset.changed":
8544
+ void this.onAssetChanged(msg.payload);
8545
+ return;
7997
8546
  default:
7998
8547
  this.emit("unknown-message", msg);
7999
8548
  }
8000
8549
  }
8001
8550
  async onHostAcked(payload) {
8002
8551
  this.workspaceId = payload.workspaceId;
8552
+ if (this.memoryWiring && this.workspaceId) {
8553
+ syncMemoryFromCloud(this.memoryWiring, this.cloud, [this.workspaceId]).catch(
8554
+ (err) => console.error("[Daemon] Initial memory sync failed:", err.message)
8555
+ );
8556
+ }
8557
+ this.syncAssetMetadata(this.workspaceId).catch(
8558
+ (err) => console.error("[Daemon] Asset metadata sync failed:", err.message)
8559
+ );
8003
8560
  for (const id of payload.profilesToSync) {
8004
8561
  try {
8005
8562
  await this.syncProfileFromCloud(id);
@@ -8059,6 +8616,7 @@ var Runner = class extends import_node_events3.EventEmitter {
8059
8616
  paths: this.paths,
8060
8617
  signal: ctrl.signal,
8061
8618
  ensureService: (profile, adapter) => this.servicePool.ensureService(profile, adapter),
8619
+ assetMetadataIndexes: this.assetMetadataIndexes,
8062
8620
  onProgress: () => {
8063
8621
  const running = this.runningTasks.get(payload.taskId);
8064
8622
  if (running) running.lastProgressAt = Date.now();
@@ -8159,6 +8717,19 @@ var Runner = class extends import_node_events3.EventEmitter {
8159
8717
  this.emit("sync-error", err);
8160
8718
  }
8161
8719
  }
8720
+ async onAssetChanged(payload) {
8721
+ if (!payload.workspaceId) return;
8722
+ const index = this.assetMetadataIndexes.get(payload.workspaceId);
8723
+ if (!index) return;
8724
+ try {
8725
+ const result = await index.pullDelta();
8726
+ if (result.applied > 0) {
8727
+ console.log(`[Daemon] asset.changed workspace=${payload.workspaceId} applied=${result.applied}`);
8728
+ }
8729
+ } catch (err) {
8730
+ console.error(`[Daemon] asset.changed pullDelta failed workspace=${payload.workspaceId}:`, err.message);
8731
+ }
8732
+ }
8162
8733
  onWorkspaceFileChanged(payload) {
8163
8734
  if (payload.operation === "delete") {
8164
8735
  this.db.prepare("DELETE FROM workspace_files_mirror WHERE workspace_id = ? AND path = ?").run(payload.workspaceId, payload.path);
@@ -8194,6 +8765,28 @@ var Runner = class extends import_node_events3.EventEmitter {
8194
8765
  }
8195
8766
  };
8196
8767
  }
8768
+ /**
8769
+ * Ensure an AssetMetadataIndex exists for the given workspace and pull
8770
+ * delta from cloud. Idempotent — creates the index on first call, reuses
8771
+ * it on subsequent calls. Same cursor-catch-up semantics as WorkspaceMirror.
8772
+ */
8773
+ async syncAssetMetadata(workspaceId) {
8774
+ let index = this.assetMetadataIndexes.get(workspaceId);
8775
+ if (!index) {
8776
+ const stateDir = `${this.paths.root}/${workspaceId}`;
8777
+ index = new AssetMetadataIndex({
8778
+ db: this.db,
8779
+ cloud: this.cloud,
8780
+ workspaceId,
8781
+ workspaceStateDir: stateDir
8782
+ });
8783
+ this.assetMetadataIndexes.set(workspaceId, index);
8784
+ }
8785
+ const result = await index.pullDelta();
8786
+ if (result.applied > 0) {
8787
+ console.log(`[AssetMeta] workspace=${workspaceId}: ${result.applied} applied, cursor=${result.cursor}`);
8788
+ }
8789
+ }
8197
8790
  /**
8198
8791
  * SyncWorker FlushFn — pushes local writes to cloud.
8199
8792
  *
@@ -8358,6 +8951,8 @@ function safeJsonParse(raw) {
8358
8951
  }
8359
8952
 
8360
8953
  // src/cli/commands/daemon.ts
8954
+ init_util();
8955
+ init_ui();
8361
8956
  function buildDaemonCommand() {
8362
8957
  const cmd = new import_commander8.Command("daemon").description("Manage the prismer daemon process");
8363
8958
  cmd.command("start").description("Start the daemon in the background (use --foreground for Docker/systemd)").option("--port <port>", "Local server port (default 3210)", (v) => Number.parseInt(v, 10)).option("--no-local-server", "Skip starting the local 127.0.0.1 server").option("--foreground", "Run in the foreground instead of daemonizing").option("--json", "Output machine-readable JSON").action(async (opts) => {
@@ -8429,9 +9024,9 @@ function buildDaemonCommand() {
8429
9024
  if (existingPid && pidAlive(existingPid)) {
8430
9025
  exitWithError(`Daemon already running (pid ${existingPid}). Use \`prismer daemon stop\` first.`);
8431
9026
  }
8432
- if (!(0, import_node_fs13.existsSync)(paths.logsDir)) (0, import_node_fs13.mkdirSync)(paths.logsDir, { recursive: true });
8433
- const logFile = (0, import_node_path9.join)(paths.logsDir, "daemon.log");
8434
- const fd = (0, import_node_fs13.openSync)(logFile, "a");
9027
+ if (!(0, import_node_fs14.existsSync)(paths.logsDir)) (0, import_node_fs14.mkdirSync)(paths.logsDir, { recursive: true });
9028
+ const logFile = (0, import_node_path10.join)(paths.logsDir, "daemon.log");
9029
+ const fd = (0, import_node_fs14.openSync)(logFile, "a");
8435
9030
  const args = [process.argv[1], "daemon", "run"];
8436
9031
  if (opts.port) args.push("--port", String(opts.port));
8437
9032
  if (opts.localServer === false) args.push("--no-local-server");
@@ -8514,8 +9109,8 @@ function buildDaemonCommand() {
8514
9109
  });
8515
9110
  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) => {
8516
9111
  const paths = resolvePaths();
8517
- const logFile = (0, import_node_path9.join)(paths.logsDir, "daemon.log");
8518
- if (!(0, import_node_fs13.existsSync)(logFile)) {
9112
+ const logFile = (0, import_node_path10.join)(paths.logsDir, "daemon.log");
9113
+ if (!(0, import_node_fs14.existsSync)(logFile)) {
8519
9114
  exitWithError(`No daemon log found at ${logFile}. Start the daemon with \`prismer daemon start\`.`);
8520
9115
  }
8521
9116
  const lines = Math.max(1, opts.tail);
@@ -8556,13 +9151,13 @@ async function tailFromEnd(path7, lines) {
8556
9151
  }
8557
9152
  }
8558
9153
  async function followFile(path7) {
8559
- let offset = (0, import_node_fs13.statSync)(path7).size;
9154
+ let offset = (0, import_node_fs14.statSync)(path7).size;
8560
9155
  for (; ; ) {
8561
9156
  await (0, import_promises.setTimeout)(1e3);
8562
- const size = (0, import_node_fs13.statSync)(path7).size;
9157
+ const size = (0, import_node_fs14.statSync)(path7).size;
8563
9158
  if (size < offset) offset = 0;
8564
9159
  if (size === offset) continue;
8565
- const stream = (0, import_node_fs13.createReadStream)(path7, { start: offset, end: size - 1, encoding: "utf8" });
9160
+ const stream = (0, import_node_fs14.createReadStream)(path7, { start: offset, end: size - 1, encoding: "utf8" });
8566
9161
  for await (const chunk of stream) process.stdout.write(chunk);
8567
9162
  offset = size;
8568
9163
  }
@@ -8570,15 +9165,16 @@ async function followFile(path7) {
8570
9165
 
8571
9166
  // src/cli/commands/events.ts
8572
9167
  var import_commander9 = require("commander");
8573
- var import_node_fs14 = require("fs");
9168
+ var import_node_fs15 = require("fs");
8574
9169
  var import_node_os5 = require("os");
8575
- var import_node_path10 = require("path");
9170
+ var import_node_path11 = require("path");
8576
9171
  var import_node_readline = require("readline");
8577
- var DEFAULT_LIMIT = 50;
9172
+ init_util();
9173
+ var DEFAULT_LIMIT2 = 50;
8578
9174
  function buildEventsCommand() {
8579
9175
  return addEventOptions(new import_commander9.Command("events").description("Read local PARA events from ~/.prismer/para/events.jsonl")).action(async (opts) => {
8580
9176
  const file = eventsPath();
8581
- if (!(0, import_node_fs14.existsSync)(file)) {
9177
+ if (!(0, import_node_fs15.existsSync)(file)) {
8582
9178
  printJson(unavailable(file));
8583
9179
  process.exitCode = 1;
8584
9180
  return;
@@ -8595,7 +9191,7 @@ function buildEventsCommand() {
8595
9191
  function buildEventsStatsCommand() {
8596
9192
  return addEventOptions(new import_commander9.Command("events:stats").description("Summarize local PARA events from ~/.prismer/para/events.jsonl")).action(async (opts) => {
8597
9193
  const file = eventsPath();
8598
- if (!(0, import_node_fs14.existsSync)(file)) {
9194
+ if (!(0, import_node_fs15.existsSync)(file)) {
8599
9195
  printJson(unavailable(file));
8600
9196
  process.exitCode = 1;
8601
9197
  return;
@@ -8610,10 +9206,10 @@ function buildEventsStatsCommand() {
8610
9206
  });
8611
9207
  }
8612
9208
  function addEventOptions(cmd) {
8613
- return cmd.option("--limit <n>", "Max events to return/read", parsePositiveInt3, DEFAULT_LIMIT).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)");
9209
+ 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)");
8614
9210
  }
8615
9211
  function eventsPath() {
8616
- return (0, import_node_path10.join)(process.env.PRISMER_HOME ?? (0, import_node_path10.join)((0, import_node_os5.homedir)(), ".prismer"), "para", "events.jsonl");
9212
+ return (0, import_node_path11.join)(process.env.PRISMER_HOME ?? (0, import_node_path11.join)((0, import_node_os5.homedir)(), ".prismer"), "para", "events.jsonl");
8617
9213
  }
8618
9214
  function unavailable(file) {
8619
9215
  return {
@@ -8629,7 +9225,7 @@ function unavailable(file) {
8629
9225
  async function readEvents(file, filters) {
8630
9226
  const out = [];
8631
9227
  const rl = (0, import_node_readline.createInterface)({
8632
- input: (0, import_node_fs14.createReadStream)(file, { encoding: "utf8" }),
9228
+ input: (0, import_node_fs15.createReadStream)(file, { encoding: "utf8" }),
8633
9229
  crlfDelay: Infinity
8634
9230
  });
8635
9231
  for await (const line of rl) {
@@ -8681,7 +9277,7 @@ function bump(map, key) {
8681
9277
  }
8682
9278
  function normalizeFilters(opts) {
8683
9279
  return {
8684
- limit: Math.max(1, Math.min(1e4, Number.isFinite(opts.limit) ? opts.limit : DEFAULT_LIMIT)),
9280
+ limit: Math.max(1, Math.min(1e4, Number.isFinite(opts.limit) ? opts.limit : DEFAULT_LIMIT2)),
8685
9281
  agentId: opts.agentId,
8686
9282
  sessionId: opts.sessionId,
8687
9283
  family: opts.family,
@@ -8693,13 +9289,14 @@ function cleanFilters(filters) {
8693
9289
  }
8694
9290
  function parsePositiveInt3(v) {
8695
9291
  const n = Number.parseInt(v, 10);
8696
- return Number.isFinite(n) && n > 0 ? n : DEFAULT_LIMIT;
9292
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_LIMIT2;
8697
9293
  }
8698
9294
 
8699
9295
  // src/cli/commands/memory.ts
8700
9296
  var import_better_sqlite34 = __toESM(require("better-sqlite3"), 1);
8701
9297
  var import_commander10 = require("commander");
8702
- var import_node_fs15 = require("fs");
9298
+ var import_node_fs16 = require("fs");
9299
+ init_util();
8703
9300
  var LOCAL_BASE = process.env.PRISMER_DAEMON_URL ?? "http://127.0.0.1:3210";
8704
9301
  function buildMemoryCommand() {
8705
9302
  const cmd = new import_commander10.Command("memory").description("Inspect local daemon memory/cache state");
@@ -8791,7 +9388,7 @@ async function tryDaemon(methods, paths) {
8791
9388
  try {
8792
9389
  const res = await fetch(`${LOCAL_BASE}${path7}`, { method, signal: AbortSignal.timeout(1500) });
8793
9390
  if (res.status === 404) continue;
8794
- const body = await readJson3(res);
9391
+ const body = await readJson4(res);
8795
9392
  if (!res.ok) {
8796
9393
  return {
8797
9394
  ok: false,
@@ -8810,7 +9407,7 @@ async function tryDaemon(methods, paths) {
8810
9407
  }
8811
9408
  return void 0;
8812
9409
  }
8813
- async function readJson3(res) {
9410
+ async function readJson4(res) {
8814
9411
  const text = await res.text();
8815
9412
  if (!text) return null;
8816
9413
  try {
@@ -8850,7 +9447,7 @@ function readCacheSnapshot(limit) {
8850
9447
  const paths = resolvePaths();
8851
9448
  const empty = {
8852
9449
  dbPath: paths.localDb,
8853
- dbExists: (0, import_node_fs15.existsSync)(paths.localDb),
9450
+ dbExists: (0, import_node_fs16.existsSync)(paths.localDb),
8854
9451
  tables: {
8855
9452
  cached_assets: { exists: false, count: 0, sizeBytes: 0 },
8856
9453
  workspace_files_mirror: { exists: false, count: 0 }
@@ -8904,9 +9501,9 @@ function readCacheSnapshot(limit) {
8904
9501
  db?.close();
8905
9502
  }
8906
9503
  }
8907
- function tableExists(db, table) {
8908
- const row = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
8909
- return row?.name === table;
9504
+ function tableExists(db, table2) {
9505
+ const row = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table2);
9506
+ return row?.name === table2;
8910
9507
  }
8911
9508
  function assetRow(row) {
8912
9509
  const hash = String(row.content_hash ?? "");
@@ -8972,16 +9569,17 @@ function messageFromBody(body) {
8972
9569
  var import_commander11 = require("commander");
8973
9570
 
8974
9571
  // src/pair.ts
8975
- var import_node_crypto6 = require("crypto");
8976
- var import_node_os6 = require("os");
9572
+ var import_node_crypto5 = require("crypto");
9573
+ var import_node_os7 = require("os");
8977
9574
  var import_promises2 = require("timers/promises");
8978
9575
  var import_qrcode = __toESM(require("qrcode"), 1);
8979
9576
 
8980
9577
  // src/daemon-id.ts
8981
- var import_node_crypto5 = require("crypto");
9578
+ var import_node_os6 = require("os");
8982
9579
  var PREFIX = "daemon-";
8983
9580
  function newDaemonId() {
8984
- return `${PREFIX}${(0, import_node_crypto5.randomUUID)()}`;
9581
+ const host = (0, import_node_os6.hostname)().replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "unknown";
9582
+ return `${PREFIX}${host}`;
8985
9583
  }
8986
9584
 
8987
9585
  // src/pair.ts
@@ -8999,7 +9597,7 @@ async function pair(opts) {
8999
9597
  "pair: --as-user requires LOCAL_ONLY=1. Without that gate, this would skip mobile approval and silently mint a key for the named user."
9000
9598
  );
9001
9599
  }
9002
- const { publicKey } = (0, import_node_crypto6.generateKeyPairSync)("ed25519");
9600
+ const { publicKey } = (0, import_node_crypto5.generateKeyPairSync)("ed25519");
9003
9601
  const devicePub = publicKey.export({ format: "der", type: "spki" }).toString("base64");
9004
9602
  const cloud = new CloudClient({
9005
9603
  baseUrl: opts.cloudBaseUrl,
@@ -9012,7 +9610,7 @@ async function pair(opts) {
9012
9610
  "/api/im/pair/offer",
9013
9611
  {
9014
9612
  auth: false,
9015
- body: { devicePub, deviceName: opts.deviceName ?? (0, import_node_os6.hostname)() }
9613
+ body: { devicePub, deviceName: opts.deviceName ?? (0, import_node_os7.hostname)() }
9016
9614
  }
9017
9615
  );
9018
9616
  if (!offerRes.ok) {
@@ -9089,6 +9687,8 @@ function unwrapEnvelope2(raw) {
9089
9687
  }
9090
9688
 
9091
9689
  // src/cli/commands/pair.ts
9690
+ init_util();
9691
+ init_ui();
9092
9692
  function buildPairCommand() {
9093
9693
  return new import_commander11.Command("pair").description("Legacy QR approval path; use `prismer setup` to bind this runtime").option(
9094
9694
  "--cloud <url>",
@@ -9131,9 +9731,9 @@ function buildPairCommand() {
9131
9731
 
9132
9732
  // src/cli/commands/profile.ts
9133
9733
  var import_commander12 = require("commander");
9134
- var import_node_fs16 = require("fs");
9135
- var import_node_os7 = require("os");
9136
- var import_node_path11 = require("path");
9734
+ var import_node_fs17 = require("fs");
9735
+ var import_node_os8 = require("os");
9736
+ var import_node_path12 = require("path");
9137
9737
  var import_node_child_process8 = require("child_process");
9138
9738
 
9139
9739
  // src/templates/roles/product-manager.json
@@ -9143,7 +9743,7 @@ var product_manager_default = {
9143
9743
  description: "Writes PRDs, defines requirements, verifies implementations",
9144
9744
  applicableAdapters: ["hermes", "openclaw", "claude-code"],
9145
9745
  configSchema: {
9146
- model: "claude-3-5-sonnet",
9746
+ model: "us-kimi-k2.6",
9147
9747
  systemPrompt: "\u4F60\u662F\u4E00\u4F4D\u8D44\u6DF1\u4EA7\u54C1\u7ECF\u7406\u3002\u4F60\u7684\u4EFB\u52A1\uFF1A\u2460\u5199\u6E05\u6670\u7684 PRD\uFF08\u7528\u6237\u6545\u4E8B + \u9A8C\u6536\u6807\u51C6 + \u8FB9\u754C\u6761\u4EF6\uFF09\u2461\u8BC4\u5BA1\u5DE5\u7A0B\u5B9E\u73B0\uFF08\u6838\u5BF9\u9A8C\u6536\u6807\u51C6 + \u7ED9\u53CD\u9988\uFF09\u2462\u5728\u7FA4\u804A\u91CC @engineer \u89E6\u53D1\u5B9E\u73B0\uFF0C@verifier \u89E6\u53D1\u9A8C\u6536\u3002\n\n\u7FA4\u804A\u534F\u4F5C\u7EA6\u5B9A\uFF1A\n- \u4F60\u7684\u8F93\u51FA PRD \u5E94\u8BE5\u4E0A\u4F20\u4E3A workspace file `docs/PROJECT-prd.md`\n- \u4F60\u5B8C\u6210 PRD \u540E\uFF0C\u53D1\u6D88\u606F @engineer-name \u8BA9\u4ED6\u5B9E\u73B0\uFF0C\u9644 prismer://file/<wsId>/docs/PROJECT-prd.md \u94FE\u63A5\n- \u6536\u5230 engineer \u5B8C\u6210\u6D88\u606F\u540E\uFF0C\u4E3B\u52A8 review\uFF0C\u5199\u53CD\u9988\u5230\u7FA4\u91CC",
9148
9748
  allowedTools: ["Read", "Write", "WebSearch"],
9149
9749
  maxTokens: 8e3
@@ -9157,7 +9757,7 @@ var engineer_default = {
9157
9757
  description: "Implements features per PRD, writes code, runs tests",
9158
9758
  applicableAdapters: ["hermes", "openclaw", "claude-code"],
9159
9759
  configSchema: {
9160
- model: "claude-3-5-sonnet",
9760
+ model: "us-kimi-k2.6",
9161
9761
  systemPrompt: "\u4F60\u662F\u8D44\u6DF1\u5DE5\u7A0B\u5E08\u3002\u4F60\u7684\u4EFB\u52A1\uFF1A\u2460\u6839\u636E PRD \u5B9E\u73B0 feature \u2461\u5199\u6D4B\u8BD5\u8986\u76D6 \u2462\u628A\u4EE3\u7801\u4E0A\u4F20\u4E3A workspace file \u540E\u5728\u7FA4\u804A\u6C47\u62A5\u3002\n\n\u7FA4\u804A\u534F\u4F5C\u7EA6\u5B9A\uFF1A\n- \u6536\u5230 PRD \u540E\u5148 Read prismer://file \u94FE\u63A5\u62C9\u5230\u7684\u672C\u5730\u8DEF\u5F84\n- \u5B9E\u73B0\u5B8C\u6BD5\u540E\u7528 daemon \u4E0A\u4F20 src/<feature>.* \u5230 workspace_files\n- \u5728\u7FA4\u804A\u56DE\u590D @pm-name\uFF0C\u9644\u5B9E\u73B0\u7684 prismer://file URI",
9162
9762
  allowedTools: ["Read", "Write", "Edit", "Bash", "Grep"],
9163
9763
  maxTokens: 16e3
@@ -9171,7 +9771,7 @@ var ceo_default = {
9171
9771
  description: "Sets strategic direction, asks tough questions, makes go/no-go calls",
9172
9772
  applicableAdapters: ["hermes", "openclaw", "claude-code"],
9173
9773
  configSchema: {
9174
- model: "claude-3-5-sonnet",
9774
+ model: "us-kimi-k2.6",
9175
9775
  systemPrompt: "\u4F60\u662F CEO\u3002\u4F60\u7684\u4EFB\u52A1\uFF1A\u2460\u57FA\u4E8E\u5E02\u573A\u53CD\u9988\u548C\u6570\u636E\u505A\u6218\u7565\u51B3\u7B56 \u2461\u8BC4\u5BA1 PRD \u662F\u5426\u5BF9\u9F50\u6218\u7565 \u2462\u5728\u6267\u884C\u4E2D\u63D0\u51FA\u5C16\u9510\u7684'\u4E3A\u4EC0\u4E48'\u95EE\u9898\u3002",
9176
9776
  allowedTools: ["Read", "WebSearch"],
9177
9777
  maxTokens: 4e3
@@ -9185,7 +9785,7 @@ var researcher_default = {
9185
9785
  description: "Investigates topics, gathers sources, writes research memos with citations",
9186
9786
  applicableAdapters: ["hermes", "openclaw", "claude-code"],
9187
9787
  configSchema: {
9188
- model: "claude-3-5-sonnet",
9788
+ model: "us-kimi-k2.6",
9189
9789
  systemPrompt: "\u4F60\u662F\u4E00\u4F4D\u8D44\u6DF1\u7814\u7A76\u5458\u3002\u4EFB\u52A1\uFF1A\u2460\u4F9D\u636E\u95EE\u9898\u5236\u5B9A\u8C03\u7814\u63D0\u7EB2 \u2461\u6293\u53D6/\u9605\u8BFB\u8D44\u6599\u5E76\u63D0\u53D6\u8981\u70B9 \u2462\u4EA7\u51FA\u5E26\u5F15\u7528\u7684\u7814\u7A76\u5907\u5FD8\u5F55\u3002\n\n\u7FA4\u804A\u534F\u4F5C\u7EA6\u5B9A\uFF1A\n- \u7814\u7A76\u4EA7\u51FA\u4E0A\u4F20\u4E3A workspace file `research/<topic>.md`\n- \u5B8C\u6210\u540E\u5728\u7FA4\u804A\u56DE\u590D @pm-name\uFF0C\u9644 prismer://file/<wsId>/research/<topic>.md\n- \u5F15\u7528\u5FC5\u987B\u7ED9\u51FA\u539F\u59CB\u94FE\u63A5\u6216 prismer://asset URI\uFF1B\u4E0D\u5F97\u4F2A\u9020\u6765\u6E90",
9190
9790
  allowedTools: ["Read", "Write", "WebSearch", "WebFetch"],
9191
9791
  maxTokens: 12e3
@@ -9211,6 +9811,7 @@ function listRoleTemplates() {
9211
9811
  }
9212
9812
 
9213
9813
  // src/cli/commands/profile.ts
9814
+ init_util();
9214
9815
  function buildProfileCommand() {
9215
9816
  const cmd = new import_commander12.Command("profile").description("Manage AgentProfile (per-agent adapter config)");
9216
9817
  cmd.command("templates").description("List built-in role templates (PM / Engineer / CEO \u2026)").option("--json", "Output JSON (default)").action(() => {
@@ -9221,7 +9822,7 @@ function buildProfileCommand() {
9221
9822
  const data = await cloud.get(`/api/im/agent_profiles?agentId=${encodeURIComponent(opts.agent)}`);
9222
9823
  printJson(data);
9223
9824
  }, { code: "profile_list_failed" }));
9224
- cmd.command("create").description("Create an AgentProfile").requiredOption("--agent <imUserId>", "Agent IMUser.id this profile belongs to").requiredOption("--name <name>", "Profile display name (unique within workspace+agent)").option("--adapter <name>", "Adapter name (defaults to template.applicableAdapters[0] or hermes)").option("--config <jsonOrPath>", "Inline JSON or @path/to/file containing adapter config").option("--from-template <name>", "Use a built-in role template (run `prismer profile templates` to list)").option("--workspace-id <id>", "Workspace id (cloud derives Personal default if omitted)").option("--json", "Output JSON (default)").action(runAction(async (opts) => {
9825
+ cmd.command("create").description("Create an AgentProfile").requiredOption("--agent <imUserId>", "Agent IMUser.id this profile belongs to").requiredOption("--name <name>", "Profile display name (unique within workspace+agent)").option("--adapter <name>", "Adapter name (defaults to template.applicableAdapters[0] or hermes)").option("--config <jsonOrPath>", "Inline JSON or @path/to/file containing adapter config").option("--from-template <name>", "Use a built-in role template (run `prismer profile templates` to list)").option("--model <name>", "Model id (overrides template default; fetched from cloud if omitted)").option("--workspace-id <id>", "Workspace id (cloud derives Personal default if omitted)").option("--json", "Output JSON (default)").action(runAction(async (opts) => {
9225
9826
  let configObj = {};
9226
9827
  let adapterName = opts.adapter ?? "hermes";
9227
9828
  if (opts.fromTemplate) {
@@ -9234,6 +9835,9 @@ function buildProfileCommand() {
9234
9835
  const inline = readJsonArg(opts.config);
9235
9836
  configObj = { ...configObj, ...inline };
9236
9837
  }
9838
+ if (opts.model) {
9839
+ configObj.model = opts.model;
9840
+ }
9237
9841
  const cloud = mkCloud4();
9238
9842
  const wsId = opts.workspaceId ?? await resolveDefaultWorkspaceId(cloud);
9239
9843
  const res = await cloud.request("POST", "/api/im/agent_profiles", {
@@ -9253,12 +9857,12 @@ function buildProfileCommand() {
9253
9857
  const profile = await cloud.get(
9254
9858
  `/api/im/agent_profiles/${encodeURIComponent(profileId)}`
9255
9859
  );
9256
- const tmpFile = (0, import_node_path11.join)((0, import_node_os7.tmpdir)(), `prismer-profile-${profileId}.json`);
9257
- (0, import_node_fs16.writeFileSync)(tmpFile, JSON.stringify(profile.config, null, 2), "utf8");
9860
+ const tmpFile = (0, import_node_path12.join)((0, import_node_os8.tmpdir)(), `prismer-profile-${profileId}.json`);
9861
+ (0, import_node_fs17.writeFileSync)(tmpFile, JSON.stringify(profile.config, null, 2), "utf8");
9258
9862
  const editor = process.env.EDITOR || "vi";
9259
9863
  const ed = (0, import_node_child_process8.spawnSync)(editor, [tmpFile], { stdio: "inherit" });
9260
9864
  if (ed.status !== 0) exitWithError(`editor exited ${ed.status}`, { code: "editor_failed" });
9261
- const newConfig = JSON.parse((0, import_node_fs16.readFileSync)(tmpFile, "utf8"));
9865
+ const newConfig = JSON.parse((0, import_node_fs17.readFileSync)(tmpFile, "utf8"));
9262
9866
  const res = await cloud.request("PATCH", `/api/im/agent_profiles/${encodeURIComponent(profileId)}`, {
9263
9867
  body: { config: newConfig, version: profile.version }
9264
9868
  });
@@ -9280,8 +9884,8 @@ function mkCloud4() {
9280
9884
  function readJsonArg(arg) {
9281
9885
  if (arg.startsWith("@")) {
9282
9886
  const path7 = arg.slice(1);
9283
- if (!(0, import_node_fs16.existsSync)(path7)) throw new Error(`File not found: ${path7}`);
9284
- return JSON.parse((0, import_node_fs16.readFileSync)(path7, "utf8"));
9887
+ if (!(0, import_node_fs17.existsSync)(path7)) throw new Error(`File not found: ${path7}`);
9888
+ return JSON.parse((0, import_node_fs17.readFileSync)(path7, "utf8"));
9285
9889
  }
9286
9890
  return JSON.parse(arg);
9287
9891
  }
@@ -9294,6 +9898,8 @@ async function resolveDefaultWorkspaceId(cloud) {
9294
9898
 
9295
9899
  // src/cli/commands/sandbox.ts
9296
9900
  var import_commander13 = require("commander");
9901
+ init_util();
9902
+ init_ui();
9297
9903
  function buildSandboxCommand() {
9298
9904
  const cmd = new import_commander13.Command("sandbox").description("Inspect and smoke-test sandbox lifecycle");
9299
9905
  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) => {
@@ -9495,11 +10101,13 @@ async function safeParseResponse(res) {
9495
10101
 
9496
10102
  // src/cli/commands/setup.ts
9497
10103
  var import_commander14 = require("commander");
9498
- var import_node_os8 = require("os");
10104
+ var import_node_os9 = require("os");
9499
10105
  var import_node_child_process9 = require("child_process");
9500
- var import_node_crypto7 = require("crypto");
9501
- var import_node_fs17 = require("fs");
10106
+ var import_node_crypto6 = require("crypto");
10107
+ var import_node_fs18 = require("fs");
9502
10108
  var import_node_http2 = require("http");
10109
+ init_util();
10110
+ init_ui();
9503
10111
  function buildSetupCommand() {
9504
10112
  return new import_commander14.Command("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(
9505
10113
  "--cloud <url>",
@@ -9522,6 +10130,7 @@ function buildSetupCommand() {
9522
10130
  getUI().blank();
9523
10131
  }
9524
10132
  const shouldStart = opts.start !== false;
10133
+ stopRunningDaemon(paths);
9525
10134
  if (opts.pair || opts.asUser) {
9526
10135
  if (!opts.json) warn("Legacy pair setup path", "plain `prismer setup --start` is the canonical runtime binding flow");
9527
10136
  if (opts.asUser && process.env.LOCAL_ONLY !== "1") {
@@ -9538,7 +10147,7 @@ function buildSetupCommand() {
9538
10147
  }
9539
10148
  const result = await pair({
9540
10149
  cloudBaseUrl,
9541
- deviceName: opts.deviceName ?? (0, import_node_os8.hostname)(),
10150
+ deviceName: opts.deviceName ?? (0, import_node_os9.hostname)(),
9542
10151
  force: opts.force,
9543
10152
  paths,
9544
10153
  asUserEmail: opts.asUser
@@ -9555,13 +10164,13 @@ function buildSetupCommand() {
9555
10164
  apiKey = await mintDaemonApiKey({
9556
10165
  cloudBaseUrl,
9557
10166
  token: authToken,
9558
- deviceName: opts.deviceName ?? (0, import_node_os8.hostname)()
10167
+ deviceName: opts.deviceName ?? (0, import_node_os9.hostname)()
9559
10168
  });
9560
10169
  }
9561
10170
  if (!apiKey && !authToken && !opts.check && opts.browser !== false) {
9562
10171
  apiKey = await runBrowserSetup({
9563
10172
  cloudBaseUrl,
9564
- deviceName: opts.deviceName ?? (0, import_node_os8.hostname)(),
10173
+ deviceName: opts.deviceName ?? (0, import_node_os9.hostname)(),
9565
10174
  json: Boolean(opts.json)
9566
10175
  });
9567
10176
  }
@@ -9600,9 +10209,13 @@ function buildSetupCommand() {
9600
10209
  if (configExists(paths) && !opts.force) {
9601
10210
  const cfg = loadConfig(paths);
9602
10211
  if (opts.json) {
9603
- printJson({ ok: true, alreadyConfigured: true, config: paths.configFile, daemonId: cfg.daemon_id });
10212
+ printJson({ ok: true, alreadyConfigured: true, config: paths.configFile, daemonId: cfg.daemon_id, daemonStartRequested: shouldStart });
9604
10213
  } else {
9605
10214
  warn("Already configured", paths.configFile);
10215
+ if (shouldStart) {
10216
+ startDaemonDetached(paths.root);
10217
+ ok("Daemon start requested");
10218
+ }
9606
10219
  tip("prismer setup --force <api-key>", "overwrite config");
9607
10220
  }
9608
10221
  return;
@@ -9641,7 +10254,7 @@ function buildSetupCommand() {
9641
10254
  }, { code: "setup_failed" }));
9642
10255
  }
9643
10256
  async function runBrowserSetup(input) {
9644
- const state = (0, import_node_crypto7.randomBytes)(18).toString("base64url");
10257
+ const state = (0, import_node_crypto6.randomBytes)(18).toString("base64url");
9645
10258
  const server = (0, import_node_http2.createServer)();
9646
10259
  const apiKeyPromise = waitForSetupCallback(server, state);
9647
10260
  await new Promise((resolve3, reject) => {
@@ -9685,14 +10298,15 @@ function waitForSetupCallback(server, expectedState) {
9685
10298
  }
9686
10299
  const state = url.searchParams.get("state");
9687
10300
  const key = url.searchParams.get("key");
10301
+ const PAGE = '<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Prismer Setup</title><style>body{background:#0a0a0a;color:#e5e5e5;font-family:system-ui,-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;text-align:center}div{max-width:480px;padding:40px}h1{font-size:28px;margin-bottom:16px}p{font-size:15px;color:#888;line-height:1.6}.check{color:#4ade80;font-size:48px;margin-bottom:16px}.cross{color:#ef4444;font-size:48px;margin-bottom:16px}</style></head><body><div>';
9688
10302
  if (state !== expectedState || !key || !/^sk-prismer-/.test(key)) {
9689
10303
  res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
9690
- res.end("<h1>Prismer setup failed</h1><p>Invalid setup callback. You can close this tab and retry.</p>");
10304
+ res.end(`${PAGE}<div class="cross">&#10007;</div><h1>Prismer setup failed</h1><p>Invalid setup callback. You can close this tab and retry.</p></div></body></html>`);
9691
10305
  reject(new Error("setup: invalid browser callback"));
9692
10306
  return;
9693
10307
  }
9694
10308
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
9695
- res.end("<h1>Prismer setup complete</h1><p>You can close this tab and return to your terminal.</p>");
10309
+ res.end(`${PAGE}<div class="check">&#10003;</div><h1>Prismer setup complete</h1><p>You can close this tab and return to your terminal.</p></div></body></html>`);
9696
10310
  resolve3(key);
9697
10311
  } catch (err) {
9698
10312
  reject(err);
@@ -9737,13 +10351,35 @@ function startDaemonDetached(home) {
9737
10351
  });
9738
10352
  child.unref();
9739
10353
  }
10354
+ function stopRunningDaemon(paths) {
10355
+ const pid = readPidFile(paths);
10356
+ if (!pid || !pidAlive(pid)) {
10357
+ if (pid) clearPidFile(paths);
10358
+ return;
10359
+ }
10360
+ try {
10361
+ process.kill(pid, "SIGTERM");
10362
+ } catch {
10363
+ return;
10364
+ }
10365
+ const deadline = Date.now() + 5e3;
10366
+ while (Date.now() < deadline) {
10367
+ if (!pidAlive(pid)) {
10368
+ clearPidFile(paths);
10369
+ return;
10370
+ }
10371
+ const start = Date.now();
10372
+ while (Date.now() - start < 200) {
10373
+ }
10374
+ }
10375
+ }
9740
10376
  function shouldArchiveLocalDb(previous, next) {
9741
10377
  return previous.api_key !== next.api_key || previous.cloud_api_base !== next.cloud_api_base || previous.daemon_id !== next.daemon_id;
9742
10378
  }
9743
10379
  function archiveLocalDb(localDbPath) {
9744
- if (!(0, import_node_fs17.existsSync)(localDbPath)) return;
10380
+ if (!(0, import_node_fs18.existsSync)(localDbPath)) return;
9745
10381
  const archived = `${localDbPath}.${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
9746
- (0, import_node_fs17.renameSync)(localDbPath, archived);
10382
+ (0, import_node_fs18.renameSync)(localDbPath, archived);
9747
10383
  }
9748
10384
  async function mintDaemonApiKey(input) {
9749
10385
  const label = `Daemon: ${input.deviceName} ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
@@ -9764,6 +10400,8 @@ async function mintDaemonApiKey(input) {
9764
10400
 
9765
10401
  // src/cli/commands/status.ts
9766
10402
  var import_commander15 = require("commander");
10403
+ init_util();
10404
+ init_ui();
9767
10405
  function buildStatusCommand() {
9768
10406
  return new import_commander15.Command("status").description("Show daemon + config + cloud status").option("--json", "Output machine-readable JSON").action(async (opts) => {
9769
10407
  const paths = resolvePaths();
@@ -9790,10 +10428,34 @@ function buildStatusCommand() {
9790
10428
  const cloud = new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
9791
10429
  let cloudOk = false;
9792
10430
  let me = null;
10431
+ let devices = null;
10432
+ let agents = null;
9793
10433
  try {
9794
- const res = await cloud.request("GET", "/api/im/me", { timeoutMs: 3e3 });
9795
- cloudOk = res.ok;
9796
- me = res.data ?? null;
10434
+ const meRes = await cloud.request("GET", "/api/im/me", { timeoutMs: 3e3 });
10435
+ cloudOk = meRes.ok;
10436
+ me = meRes.data ?? null;
10437
+ if (cloudOk) {
10438
+ const wsRes = await cloud.request("GET", "/api/im/workspaces", { timeoutMs: 3e3 });
10439
+ if (wsRes.ok) {
10440
+ const wsBody = wsRes.data;
10441
+ const wsList = wsBody?.data;
10442
+ if (Array.isArray(wsList) && wsList.length > 0) {
10443
+ const wsId = wsList[0]?.id;
10444
+ if (wsId) {
10445
+ const devRes = await cloud.request("GET", `/api/workspace/runtime-installations?workspaceId=${encodeURIComponent(wsId)}&includeStopped=false`, { timeoutMs: 3e3 });
10446
+ if (devRes.ok) {
10447
+ const devBody = devRes.data;
10448
+ devices = devBody?.data;
10449
+ }
10450
+ const agRes = await cloud.request("GET", `/api/im/workspaces/${encodeURIComponent(wsId)}/agents`, { timeoutMs: 3e3 });
10451
+ if (agRes.ok) {
10452
+ const agBody = agRes.data;
10453
+ agents = agBody?.data;
10454
+ }
10455
+ }
10456
+ }
10457
+ }
10458
+ }
9797
10459
  } catch {
9798
10460
  cloudOk = false;
9799
10461
  }
@@ -9805,10 +10467,15 @@ function buildStatusCommand() {
9805
10467
  daemon: {
9806
10468
  running: daemonRunning,
9807
10469
  pid: daemonStatus.pid ?? pid ?? null,
9808
- wsConnected: daemonStatus.wsConnected ?? null
10470
+ wsConnected: daemonStatus.wsConnected ?? null,
10471
+ info: daemonStatus.info ?? {}
9809
10472
  },
9810
- cloud: { base: cfg.cloud_api_base, reachable: cloudOk, me },
9811
- local
10473
+ cloud: { base: cfg.cloud_api_base, reachable: cloudOk, me, devices, agents },
10474
+ local,
10475
+ binding: {
10476
+ daemonId: cfg.daemon_id,
10477
+ apiKey: cfg.api_key
10478
+ }
9812
10479
  };
9813
10480
  if (opts.json) {
9814
10481
  printJson(report);
@@ -9822,12 +10489,19 @@ async function readDaemonStatus() {
9822
10489
  const res = await fetch("http://127.0.0.1:3210/healthz", {
9823
10490
  signal: AbortSignal.timeout(1e3)
9824
10491
  });
9825
- if (!res.ok) return { running: false };
9826
- const data = await res.json();
9827
- return { running: true, pid: data.pid, wsConnected: data.wsConnected };
10492
+ if (res.ok) {
10493
+ const data = await res.json();
10494
+ return { running: true, pid: data.pid, wsConnected: data.wsConnected, info: data };
10495
+ }
9828
10496
  } catch {
9829
- return { running: false };
9830
10497
  }
10498
+ const paths = resolvePaths();
10499
+ const pid = readPidFile(paths);
10500
+ if (pid) {
10501
+ const { pidAlive: pidAlive2 } = await Promise.resolve().then(() => (init_util(), util_exports));
10502
+ if (pidAlive2(pid)) return { running: true, pid };
10503
+ }
10504
+ return { running: false };
9831
10505
  }
9832
10506
  function readLocalCounts(localDbPath) {
9833
10507
  try {
@@ -9853,21 +10527,59 @@ function printPretty(report) {
9853
10527
  ui.blank();
9854
10528
  ok("Config", report.paths.config);
9855
10529
  if (report.daemon.running) {
9856
- ok("Daemon", `pid=${report.daemon.pid ?? "?"} ws=${report.daemon.wsConnected ? "connected" : "pending"}`);
10530
+ const ws = report.daemon.wsConnected ? "connected" : "pending";
10531
+ ok("Daemon", `pid=${report.daemon.pid} ws=${ws}`);
10532
+ if (report.daemon.info) {
10533
+ const info2 = report.daemon.info;
10534
+ if (info2.version) ui.line(` Version: ${info2.version}`);
10535
+ if (info2.uptime) ui.line(` Uptime: ${Math.round(info2.uptime / 60)}m`);
10536
+ if (info2.memoryMb) ui.line(` Memory: ${info2.memoryMb} MB`);
10537
+ }
9857
10538
  } else {
9858
10539
  warn("Daemon", "not running");
9859
10540
  tip("prismer daemon start");
9860
10541
  }
9861
- if (report.cloud.reachable) ok("Cloud", report.cloud.base);
9862
- else {
10542
+ if (report.cloud.reachable) {
10543
+ ok("Cloud", report.cloud.base);
10544
+ const me = report.cloud.me;
10545
+ if (me?.user) {
10546
+ const roleTag = me.user.role ? ` role=${me.user.role}` : "";
10547
+ ui.line(` Account: ${me.user.displayName ?? me.user.username ?? "?"}${roleTag}`);
10548
+ }
10549
+ if (me?.credits) {
10550
+ ui.line(` Credits: ${typeof me.credits.balance === "number" ? me.credits.balance.toLocaleString() : "?"}`);
10551
+ }
10552
+ } else {
9863
10553
  fail("Cloud", `${report.cloud.base} unreachable or unauthorized`);
9864
10554
  tip("prismer setup --force");
9865
10555
  }
10556
+ if (report.binding) {
10557
+ ui.blank();
10558
+ ui.line(` Daemon ID: ${report.binding.daemonId}`);
10559
+ const masked = report.binding.apiKey.slice(0, 14) + "\u2022\u2022\u2022\u2022" + report.binding.apiKey.slice(-4);
10560
+ ui.line(` API Key: ${masked}`);
10561
+ }
10562
+ if (Array.isArray(report.cloud.devices) && report.cloud.devices.length > 0) {
10563
+ const devs = report.cloud.devices;
10564
+ ui.blank();
10565
+ ui.line(` Workspace Devices (${devs.length}):`);
10566
+ for (const d of devs) {
10567
+ const statusIcon = d.daemonStatus === "connected" ? "\u25CF" : "\u25CB";
10568
+ const kind = d.runtimeKind === "docker" ? "Local" : "K8s";
10569
+ const declared = d.hostedAgentSummary?.declared ?? 0;
10570
+ ui.line(` ${statusIcon} ${d.podName?.replace(/^daemon:/, "")?.slice(0, 28) ?? "?"} kind=${kind} agents=${declared}`);
10571
+ }
10572
+ } else {
10573
+ ui.line(` Workspace Devices: none`);
10574
+ }
10575
+ if (Array.isArray(report.cloud.agents) && report.cloud.agents.length > 0) {
10576
+ ui.line(` Hosted agents: ${report.cloud.agents.length}`);
10577
+ }
9866
10578
  if (report.local) {
9867
10579
  ui.blank();
9868
- ui.line(` Agents: ${report.local.agents}`);
9869
- ui.line(` Profiles: ${report.local.profiles}`);
9870
- ui.line(` Tasks: ${report.local.runningTasks} running locally`);
10580
+ ui.line(` Local agents: ${report.local.agents}`);
10581
+ ui.line(` Profiles: ${report.local.profiles}`);
10582
+ ui.line(` Running tasks: ${report.local.runningTasks}`);
9871
10583
  } else {
9872
10584
  warn("Local DB", "unavailable");
9873
10585
  }
@@ -9876,6 +10588,7 @@ function printPretty(report) {
9876
10588
  // src/cli/commands/task.ts
9877
10589
  var import_commander16 = require("commander");
9878
10590
  var import_promises3 = require("timers/promises");
10591
+ init_util();
9879
10592
  function describeStatus(status) {
9880
10593
  return status === 0 ? "network error" : `HTTP ${status}`;
9881
10594
  }
@@ -9995,6 +10708,8 @@ function taskFrom2(raw) {
9995
10708
 
9996
10709
  // src/cli/commands/workspace.ts
9997
10710
  var import_commander17 = require("commander");
10711
+ init_util();
10712
+ init_ui();
9998
10713
  function buildWorkspaceCommand() {
9999
10714
  const cmd = new import_commander17.Command("workspace").description("Manage workspaces, runtime snapshots, and workspace files");
10000
10715
  cmd.command("list").description("List workspaces").option("--json", "Output machine-readable JSON").action(runAction(async (opts) => {
@@ -10229,7 +10944,8 @@ async function readResponseError(res) {
10229
10944
  }
10230
10945
 
10231
10946
  // src/cli/index.ts
10232
- var VERSION = "1.9.3";
10947
+ init_ui();
10948
+ var VERSION = "1.9.7";
10233
10949
  function buildProgram() {
10234
10950
  const program = new import_commander18.Command("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
10235
10951
  program.addCommand(buildBannerCommand());