@prismer/runtime 1.9.7 → 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/index.js CHANGED
@@ -1,9 +1,610 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
1
3
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
4
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
5
  }) : x)(function(x) {
4
6
  if (typeof require !== "undefined") return require.apply(this, arguments);
5
7
  throw Error('Dynamic require of "' + x + '" is not supported');
6
8
  });
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
+ };
16
+
17
+ // src/cli/ui.ts
18
+ import * as fs4 from "fs";
19
+ import * as path6 from "path";
20
+ import { fileURLToPath } from "url";
21
+ function thisDirname() {
22
+ try {
23
+ return path6.dirname(fileURLToPath(import.meta.url));
24
+ } catch {
25
+ return process.cwd();
26
+ }
27
+ }
28
+ function findIconPath(size = "big") {
29
+ const name = size === "big" ? "icon" : "smallicon";
30
+ const here = thisDirname();
31
+ const candidates = [
32
+ // npm-installed: node_modules/@prismer/runtime/dist/cli.js → ../assets
33
+ path6.resolve(here, "../assets", name),
34
+ // alternate dist layout (sub-bundle): dist/bin/cli.js → ../../assets
35
+ path6.resolve(here, "../../assets", name),
36
+ // source/typecheck: src/cli/ui.ts → ../../assets
37
+ path6.resolve(here, "../../assets", name),
38
+ // dev mode: cwd happens to be runtime root
39
+ path6.resolve(process.cwd(), "assets", name),
40
+ path6.resolve(process.cwd(), "sdk/prismer-cloud/runtime/assets", name)
41
+ ];
42
+ for (const candidate of candidates) {
43
+ try {
44
+ if (fs4.existsSync(candidate)) return candidate;
45
+ } catch {
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+ function getUI() {
51
+ if (!_ui) _ui = new UI();
52
+ return _ui;
53
+ }
54
+ function setUI(ui) {
55
+ _ui = ui;
56
+ }
57
+ function applyCommonFlags(argv) {
58
+ let mode = "pretty";
59
+ const isTTY = process.stdout.isTTY === true;
60
+ const noColorEnv = Boolean(process.env["NO_COLOR"]);
61
+ let color2 = isTTY && !noColorEnv;
62
+ const rest = [];
63
+ for (const arg of argv) {
64
+ switch (arg) {
65
+ case "--no-color":
66
+ color2 = false;
67
+ break;
68
+ case "--color":
69
+ color2 = true;
70
+ break;
71
+ case "--json":
72
+ case "--pretty-json":
73
+ mode = "json";
74
+ if (arg === "--json") rest.push(arg);
75
+ break;
76
+ case "--quiet":
77
+ mode = "quiet";
78
+ break;
79
+ default:
80
+ rest.push(arg);
81
+ }
82
+ }
83
+ return { mode, color: color2, restArgv: rest };
84
+ }
85
+ var BRAILLE_FRAMES, COMPACT_BANNER, UI, _ui;
86
+ var init_ui = __esm({
87
+ "src/cli/ui.ts"() {
88
+ "use strict";
89
+ BRAILLE_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
90
+ COMPACT_BANNER = ["\u25C7 PRISMER", " Runtime CLI"];
91
+ UI = class {
92
+ mode;
93
+ colorEnabled;
94
+ stream;
95
+ errStream;
96
+ constructor(opts) {
97
+ this.mode = opts?.mode ?? "pretty";
98
+ this.stream = opts?.stream ?? process.stdout;
99
+ this.errStream = opts?.errStream ?? process.stderr;
100
+ if (opts?.color !== void 0) {
101
+ this.colorEnabled = opts.color;
102
+ } else {
103
+ const isTTY = this.stream.isTTY === true;
104
+ const noColor = Boolean(process.env["NO_COLOR"]);
105
+ this.colorEnabled = isTTY && !noColor;
106
+ }
107
+ }
108
+ // ---- Internal color helpers ----
109
+ ansi(open, close, text) {
110
+ if (!this.colorEnabled) return text;
111
+ return `\x1B[${open}m${text}\x1B[${close}m`;
112
+ }
113
+ green(t) {
114
+ return this.ansi(32, 39, t);
115
+ }
116
+ red(t) {
117
+ return this.ansi(31, 39, t);
118
+ }
119
+ yellow(t) {
120
+ return this.ansi(33, 39, t);
121
+ }
122
+ cyan(t) {
123
+ return this.ansi(36, 39, t);
124
+ }
125
+ dim(t) {
126
+ return this.ansi(2, 22, t);
127
+ }
128
+ bold(t) {
129
+ return this.ansi(1, 22, t);
130
+ }
131
+ gray(t) {
132
+ return this.ansi(90, 39, t);
133
+ }
134
+ brandMark() {
135
+ return this.cyan("\u25C7");
136
+ }
137
+ colorBrandLine(line) {
138
+ let out = "";
139
+ for (const ch of line) {
140
+ if (ch === "\u2592") {
141
+ out += this.cyan(ch);
142
+ } else if (ch === "\u2593") {
143
+ out += this.dim(ch);
144
+ } else {
145
+ out += ch;
146
+ }
147
+ }
148
+ return out;
149
+ }
150
+ // ---- Core write helpers ----
151
+ write(text) {
152
+ this.stream.write(text);
153
+ }
154
+ writeErr(text) {
155
+ this.errStream.write(text);
156
+ }
157
+ // ---- Level 1: Header ----
158
+ header(text) {
159
+ if (this.mode === "json") return;
160
+ const prefix = text.startsWith("Prismer") ? this.brandMark() + " " : "";
161
+ this.write(prefix + this.bold(text) + "\n");
162
+ }
163
+ smallHeader(subtitle) {
164
+ if (this.mode === "json" || this.mode === "quiet") return;
165
+ const iconPath = findIconPath("small");
166
+ if (iconPath !== null) {
167
+ try {
168
+ const raw = fs4.readFileSync(iconPath, "utf-8").replace(/\n+$/, "");
169
+ for (const line of raw.split("\n")) {
170
+ this.write(this.cyan(line) + "\n");
171
+ }
172
+ } catch {
173
+ this.write(this.brandMark() + " " + this.bold("Prismer") + "\n");
174
+ }
175
+ } else {
176
+ this.write(this.brandMark() + " " + this.bold("Prismer") + "\n");
177
+ }
178
+ if (subtitle !== void 0 && subtitle.length > 0) {
179
+ this.write(this.dim(" " + subtitle) + "\n");
180
+ }
181
+ this.blank();
182
+ }
183
+ banner(subtitle, opts) {
184
+ if (this.mode === "json" || this.mode === "quiet") return;
185
+ const envColumns = process.env["COLUMNS"] !== void 0 ? parseInt(process.env["COLUMNS"], 10) : NaN;
186
+ const width = this.stream.columns ?? process.stdout.columns ?? (Number.isFinite(envColumns) ? envColumns : 80);
187
+ const iconPath = findIconPath("big");
188
+ const shouldUseFull = opts?.full === true || width >= 120;
189
+ if (shouldUseFull && iconPath !== null) {
190
+ try {
191
+ const raw = fs4.readFileSync(iconPath, "utf-8");
192
+ const lines = raw.split("\n");
193
+ for (const line of lines) {
194
+ const brandedLine = line.replace("Prismer Cloud SDK", "Prismer Runtime CLI");
195
+ const stripped = brandedLine.trimEnd();
196
+ if (stripped.length === 0) {
197
+ this.write("\n");
198
+ continue;
199
+ }
200
+ const clipped = stripped.length >= width ? stripped.slice(0, Math.max(width - 1, 1)) : stripped;
201
+ this.write(this.colorBrandLine(clipped) + "\n");
202
+ }
203
+ } catch {
204
+ this.write(this.cyan(COMPACT_BANNER[0] ?? "\u25C7 PRISMER") + "\n");
205
+ this.write(this.dim(COMPACT_BANNER[1] ?? " Runtime CLI") + "\n");
206
+ }
207
+ } else {
208
+ this.write(this.cyan(COMPACT_BANNER[0] ?? "\u25C7 PRISMER") + "\n");
209
+ this.write(this.dim(COMPACT_BANNER[1] ?? " Runtime CLI") + "\n");
210
+ }
211
+ if (subtitle !== void 0 && subtitle.length > 0) {
212
+ this.write(this.dim(" " + subtitle) + "\n");
213
+ }
214
+ this.blank();
215
+ }
216
+ // ---- Level 2: Primary data ----
217
+ blank() {
218
+ if (this.mode === "json") return;
219
+ this.write("\n");
220
+ }
221
+ line(text) {
222
+ if (this.mode === "json") return;
223
+ this.write(text + "\n");
224
+ }
225
+ info(text) {
226
+ this.line(text);
227
+ }
228
+ // ---- Level 3: Secondary ----
229
+ secondary(text, indent = 2) {
230
+ if (this.mode === "json") return;
231
+ this.write(" ".repeat(indent) + this.dim(text) + "\n");
232
+ }
233
+ // ---- Level 4: Action tips ----
234
+ tip(text) {
235
+ if (this.mode === "json") return;
236
+ this.write(this.cyan("Tip:") + " " + text + "\n");
237
+ }
238
+ next(text) {
239
+ if (this.mode === "json") return;
240
+ this.write(this.cyan("Next:") + " " + text + "\n");
241
+ }
242
+ // ---- Level 5: Status indicators ----
243
+ ok(text, detail) {
244
+ if (this.mode === "json") return;
245
+ const suffix = detail ? " " + this.dim(detail) : "";
246
+ this.write(" " + this.green("\u2713") + " " + text + suffix + "\n");
247
+ }
248
+ success(text, detail) {
249
+ this.ok(text, detail);
250
+ }
251
+ fail(text, detail) {
252
+ if (this.mode === "json") return;
253
+ const suffix = detail ? " " + this.dim(detail) : "";
254
+ this.write(" " + this.red("\u2717") + " " + text + suffix + "\n");
255
+ }
256
+ online(text) {
257
+ if (this.mode === "json") return;
258
+ this.write(" " + this.green("\u25CF") + " " + text + "\n");
259
+ }
260
+ offline(text) {
261
+ if (this.mode === "json") return;
262
+ this.write(" " + this.gray("\u25CB") + " " + text + "\n");
263
+ }
264
+ notInstalled(text) {
265
+ if (this.mode === "json") return;
266
+ this.write(" " + this.dim("\xB7") + " " + this.dim(text) + "\n");
267
+ }
268
+ pending(text) {
269
+ if (this.mode === "json") return;
270
+ this.write(" " + this.yellow("\u27F3") + " " + text + "\n");
271
+ }
272
+ warn(text, detail) {
273
+ if (this.mode === "json") return;
274
+ const suffix = detail ? " " + this.dim(detail) : "";
275
+ this.write(" " + this.yellow("!") + " " + text + suffix + "\n");
276
+ }
277
+ // ---- Level 6: Error block ----
278
+ error(what, cause, fix) {
279
+ if (this.mode === "json") return;
280
+ this.writeErr(this.red("\u2717") + " " + what + "\n");
281
+ if (cause !== void 0) {
282
+ this.writeErr(" " + this.dim("Cause:") + " " + this.dim(cause) + "\n");
283
+ }
284
+ if (fix !== void 0) {
285
+ this.writeErr(" " + this.cyan("Fix:") + " " + fix + "\n");
286
+ }
287
+ }
288
+ table(rowsOrOpts, maybeOpts) {
289
+ if (this.mode === "json") return;
290
+ const rows = Array.isArray(rowsOrOpts) ? rowsOrOpts : rowsOrOpts.rows;
291
+ const opts = Array.isArray(rowsOrOpts) ? maybeOpts : { columns: rowsOrOpts.columns, maxWidth: rowsOrOpts.maxWidth };
292
+ if (!opts) throw new Error("table() requires columns");
293
+ const maxWidth = opts.maxWidth ?? (process.stdout.columns || 80);
294
+ const cols = opts.columns;
295
+ const widths = cols.map((col) => col.length);
296
+ for (const row of rows) {
297
+ cols.forEach((col, i) => {
298
+ const val = row[col] ?? "";
299
+ const w = widths[i] ?? 0;
300
+ if (val.length > w) widths[i] = val.length;
301
+ });
302
+ }
303
+ const totalWidth = widths.reduce((a, b) => a + b, 0) + (cols.length - 1) * 2 + 2;
304
+ if (totalWidth > maxWidth) {
305
+ for (let i = 0; i < rows.length; i++) {
306
+ const row = rows[i];
307
+ if (!row) continue;
308
+ for (const col of cols) {
309
+ const val = row[col] ?? "";
310
+ this.write(" " + this.bold(col + ":") + " " + val + "\n");
311
+ }
312
+ if (i < rows.length - 1) this.write("\n");
313
+ }
314
+ return;
315
+ }
316
+ const header2 = cols.map((col, i) => col.toUpperCase().padEnd(widths[i] ?? col.length)).join(" ");
317
+ this.write(" " + this.dim(header2) + "\n");
318
+ for (const row of rows) {
319
+ const line = cols.map((col, i) => (row[col] ?? "").padEnd(widths[i] ?? col.length)).join(" ");
320
+ this.write(" " + line + "\n");
321
+ }
322
+ }
323
+ // ---- Spinner ----
324
+ spinner(text) {
325
+ if (this.mode === "quiet" || this.mode === "json") {
326
+ return {
327
+ update() {
328
+ },
329
+ stop() {
330
+ }
331
+ };
332
+ }
333
+ const isTTY = this.stream.isTTY === true;
334
+ if (!isTTY || !this.colorEnabled) {
335
+ this.write(" " + this.yellow("\u27F3") + " " + text + "\n");
336
+ return {
337
+ update: (t) => {
338
+ this.write(" " + this.yellow("\u27F3") + " " + t + "\n");
339
+ },
340
+ stop: (final) => {
341
+ if (final) this.write(" " + this.green("\u2713") + " " + final + "\n");
342
+ }
343
+ };
344
+ }
345
+ let current = text;
346
+ let frameIdx = 0;
347
+ let stopped = false;
348
+ const write = this.write.bind(this);
349
+ const colorFn = this.yellow.bind(this);
350
+ const greenFn = this.green.bind(this);
351
+ function renderFrame() {
352
+ const frame = BRAILLE_FRAMES[frameIdx % BRAILLE_FRAMES.length] ?? "\u280B";
353
+ const line = " " + colorFn(frame) + " " + current;
354
+ write("\r" + line);
355
+ frameIdx++;
356
+ }
357
+ renderFrame();
358
+ const timer = setInterval(renderFrame, 80);
359
+ return {
360
+ update(t) {
361
+ if (stopped) return;
362
+ current = t;
363
+ },
364
+ stop(final) {
365
+ if (stopped) return;
366
+ stopped = true;
367
+ clearInterval(timer);
368
+ write("\r\x1B[2K");
369
+ if (final) write(" " + greenFn("\u2713") + " " + final + "\n");
370
+ }
371
+ };
372
+ }
373
+ // ---- Progress bar ----
374
+ progress(text, total) {
375
+ if (this.mode === "quiet" || this.mode === "json") {
376
+ return {
377
+ update() {
378
+ },
379
+ stop() {
380
+ }
381
+ };
382
+ }
383
+ const isTTY = this.stream.isTTY === true;
384
+ const start = Date.now();
385
+ const write = this.write.bind(this);
386
+ const colorFn = this.cyan.bind(this);
387
+ const dimFn = this.dim.bind(this);
388
+ const greenFn = this.green.bind(this);
389
+ let last = 0;
390
+ let lastDetail = "";
391
+ let stopped = false;
392
+ const render = () => {
393
+ if (stopped) return;
394
+ const frac = total > 0 ? Math.min(1, Math.max(0, last / total)) : 0;
395
+ const pct = Math.floor(frac * 100);
396
+ const width = 20;
397
+ const filled = Math.floor(frac * width);
398
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
399
+ const elapsed = (Date.now() - start) / 1e3;
400
+ const eta = frac > 0.01 ? Math.max(0, elapsed / frac - elapsed) : 0;
401
+ const etaStr = frac >= 1 ? "" : ` \xB7 ${eta < 1 ? "<1s" : Math.round(eta) + "s"} left`;
402
+ const detailStr = lastDetail ? ` \xB7 ${lastDetail}` : "";
403
+ const line = ` ${text} [${colorFn(bar)}] ${String(pct).padStart(3)}%${detailStr}${dimFn(etaStr)}`;
404
+ if (isTTY && this.colorEnabled) {
405
+ write("\r\x1B[2K" + line);
406
+ } else {
407
+ write(line + "\n");
408
+ }
409
+ };
410
+ render();
411
+ return {
412
+ update: (current, detail) => {
413
+ if (stopped) return;
414
+ last = current;
415
+ if (detail !== void 0) lastDetail = detail;
416
+ render();
417
+ },
418
+ stop: (final) => {
419
+ if (stopped) return;
420
+ stopped = true;
421
+ if (isTTY && this.colorEnabled) write("\r\x1B[2K");
422
+ if (final) write(" " + greenFn("\u2713") + " " + final + "\n");
423
+ }
424
+ };
425
+ }
426
+ // ---- JSON output ----
427
+ json(payload, opts) {
428
+ const indent = opts?.pretty ? 2 : void 0;
429
+ this.write(JSON.stringify(payload, null, indent) + "\n");
430
+ }
431
+ result(pretty, jsonPayload) {
432
+ if (this.mode === "pretty") {
433
+ pretty();
434
+ } else {
435
+ this.json(jsonPayload);
436
+ }
437
+ }
438
+ };
439
+ _ui = null;
440
+ }
441
+ });
442
+
443
+ // src/cli/util.ts
444
+ var util_exports = {};
445
+ __export(util_exports, {
446
+ DEFAULT_CLOUD_BASE_URL: () => DEFAULT_CLOUD_BASE_URL,
447
+ clearPidFile: () => clearPidFile,
448
+ color: () => color,
449
+ exitWithError: () => exitWithError,
450
+ fail: () => fail2,
451
+ header: () => header,
452
+ info: () => info,
453
+ normalizeCloudUrl: () => normalizeCloudUrl,
454
+ ok: () => ok,
455
+ pidAlive: () => pidAlive,
456
+ pidFilePath: () => pidFilePath,
457
+ printBanner: () => printBanner,
458
+ printJson: () => printJson,
459
+ readPidFile: () => readPidFile,
460
+ runAction: () => runAction,
461
+ table: () => table,
462
+ tip: () => tip,
463
+ warn: () => warn,
464
+ writePidFile: () => writePidFile
465
+ });
466
+ import { existsSync as existsSync11, readFileSync as readFileSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
467
+ import { join as join10 } from "path";
468
+ function color(kind, text) {
469
+ if (process.env.NO_COLOR === "1" || process.env.NO_COLOR === "true") return text;
470
+ return `${ANSI[kind]}${text}${ANSI.reset}`;
471
+ }
472
+ function printJson(v) {
473
+ getUI().json(v, { pretty: true });
474
+ }
475
+ function exitWithError(message, opts) {
476
+ const o = typeof opts === "number" ? { exitCode: opts } : opts ?? {};
477
+ const exitCode = o.exitCode ?? 1;
478
+ const ui = getUI();
479
+ if (ui.mode === "json") {
480
+ const payload = {
481
+ ok: false,
482
+ error: { code: o.code ?? "cli_error", message },
483
+ ...o.details ? { details: o.details } : {}
484
+ };
485
+ ui.json(payload, { pretty: true });
486
+ } else {
487
+ process.stderr.write(`Error: ${message}
488
+ `);
489
+ }
490
+ process.exit(exitCode);
491
+ }
492
+ function normalizeCloudUrl(input) {
493
+ const raw = input.trim();
494
+ if (!raw) throw new Error("Cloud URL is empty.");
495
+ if (/^[a-z][a-z0-9+\-.]*:\/\//i.test(raw)) {
496
+ let parsed;
497
+ try {
498
+ parsed = new URL(raw);
499
+ } catch {
500
+ throw new Error(`Invalid --cloud URL: ${raw}`);
501
+ }
502
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
503
+ throw new Error(`Invalid --cloud URL scheme: ${parsed.protocol} (expected http:// or https://)`);
504
+ }
505
+ return raw.replace(/\/$/, "");
506
+ }
507
+ if (!/^[A-Za-z0-9.\-_:[\]/]+$/.test(raw)) {
508
+ throw new Error(`Invalid --cloud URL: ${raw} (must be http://\u2026 or https://\u2026 or host:port)`);
509
+ }
510
+ const candidate = `http://${raw}`;
511
+ try {
512
+ new URL(candidate);
513
+ } catch {
514
+ throw new Error(`Invalid --cloud URL: ${raw}`);
515
+ }
516
+ return candidate.replace(/\/$/, "");
517
+ }
518
+ function runAction(fn, opts = {}) {
519
+ return async (...args) => {
520
+ try {
521
+ await fn(...args);
522
+ } catch (err) {
523
+ const raw = err instanceof Error ? err.message : String(err);
524
+ const message = opts.sanitize ? opts.sanitize(raw) : raw;
525
+ exitWithError(message, { code: opts.code });
526
+ }
527
+ };
528
+ }
529
+ function printBanner(opts = {}) {
530
+ const ui = getUI();
531
+ if (opts.compact) {
532
+ ui.smallHeader("Runtime CLI v1.9.7");
533
+ return;
534
+ }
535
+ ui.banner("Runtime CLI v1.9.7", { full: true });
536
+ }
537
+ function ok(label, detail) {
538
+ getUI().ok(label, detail);
539
+ }
540
+ function warn(label, detail) {
541
+ getUI().warn(label, detail);
542
+ }
543
+ function fail2(label, detail) {
544
+ getUI().fail(label, detail);
545
+ }
546
+ function tip(command, detail) {
547
+ const text = detail ? `${command} ${detail}` : command;
548
+ getUI().tip(text);
549
+ }
550
+ function info(message) {
551
+ getUI().info(message);
552
+ }
553
+ function header(title) {
554
+ getUI().header(title);
555
+ getUI().blank();
556
+ }
557
+ function table(rows, columns) {
558
+ getUI().table(rows, { columns });
559
+ }
560
+ function pidFilePath(paths) {
561
+ return join10(paths.root, "daemon.pid");
562
+ }
563
+ function writePidFile(paths, pid) {
564
+ writeFileSync6(pidFilePath(paths), `${pid}
565
+ `, "utf8");
566
+ }
567
+ function readPidFile(paths) {
568
+ const p = pidFilePath(paths);
569
+ if (!existsSync11(p)) return void 0;
570
+ const raw = readFileSync8(p, "utf8").trim();
571
+ const pid = Number.parseInt(raw, 10);
572
+ return Number.isFinite(pid) ? pid : void 0;
573
+ }
574
+ function clearPidFile(paths) {
575
+ const p = pidFilePath(paths);
576
+ if (existsSync11(p)) {
577
+ try {
578
+ unlinkSync2(p);
579
+ } catch {
580
+ }
581
+ }
582
+ }
583
+ function pidAlive(pid) {
584
+ try {
585
+ process.kill(pid, 0);
586
+ return true;
587
+ } catch {
588
+ return false;
589
+ }
590
+ }
591
+ var DEFAULT_CLOUD_BASE_URL, ANSI;
592
+ var init_util = __esm({
593
+ "src/cli/util.ts"() {
594
+ "use strict";
595
+ init_ui();
596
+ DEFAULT_CLOUD_BASE_URL = "https://prismer.cloud";
597
+ ANSI = {
598
+ reset: "\x1B[0m",
599
+ bold: "\x1B[1m",
600
+ dim: "\x1B[2m",
601
+ cyan: "\x1B[36m",
602
+ green: "\x1B[32m",
603
+ yellow: "\x1B[33m",
604
+ red: "\x1B[31m"
605
+ };
606
+ }
607
+ });
7
608
 
8
609
  // src/adapters/registry.ts
9
610
  var AdapterRegistry = class {
@@ -172,7 +773,7 @@ var HermesProfileConfigSchema = z.object({
172
773
  */
173
774
  prismerMcpServerPath: z.string().optional(),
174
775
  /** Model sent to Prismer's /api/v1/chat/completions endpoint. */
175
- model: z.string().min(1).default("us-kimi-k2.5"),
776
+ model: z.string().min(1).default("us-kimi-k2.6"),
176
777
  /** Named custom provider written into Hermes config.yaml. */
177
778
  prismerProviderName: z.string().min(1).default("prismer"),
178
779
  /** Override cloud provider base. Defaults to PRISMER_BASE_URL + /api/v1. */
@@ -198,7 +799,9 @@ var HermesProfileConfigSchema = z.object({
198
799
  * surface but the local source tree contains hermes_cli/kanban_db.py.
199
800
  */
200
801
  hermesSourceDir: z.string().optional(),
201
- nativeMirrorTimeoutMs: z.number().int().positive().default(2e3)
802
+ nativeMirrorTimeoutMs: z.number().int().positive().default(2e3),
803
+ /** Task authority level: executor (default) or orchestrator. */
804
+ taskAuthority: z.enum(["executor", "orchestrator"]).optional().default("executor")
202
805
  });
203
806
  var hermesAdapter = {
204
807
  name: "hermes",
@@ -683,13 +1286,13 @@ function resolvePrismerMcpServerPath(config) {
683
1286
  }
684
1287
  try {
685
1288
  const { fileURLToPath: fileURLToPath2 } = __require("url");
686
- const { join: join14, dirname: dirname8 } = __require("path");
1289
+ const { join: join15, dirname: dirname8 } = __require("path");
687
1290
  const here = dirname8(fileURLToPath2(import.meta.url));
688
- const candidate = join14(here, "../../mcp/dist/index.js");
1291
+ const candidate = join15(here, "../../mcp/dist/index.js");
689
1292
  if (existsSync(candidate)) return candidate;
690
- const candidate2 = join14(here, "../../../mcp/dist/index.js");
1293
+ const candidate2 = join15(here, "../../../mcp/dist/index.js");
691
1294
  if (existsSync(candidate2)) return candidate2;
692
- const candidate3 = join14(here, "../../../../mcp/dist/index.js");
1295
+ const candidate3 = join15(here, "../../../../mcp/dist/index.js");
693
1296
  if (existsSync(candidate3)) return candidate3;
694
1297
  } catch {
695
1298
  }
@@ -1111,7 +1714,7 @@ var WsClient = class extends EventEmitter {
1111
1714
  import Database2 from "better-sqlite3";
1112
1715
  import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
1113
1716
  import { dirname as dirname2 } from "path";
1114
- var SCHEMA_VERSION = 2;
1717
+ var SCHEMA_VERSION = 3;
1115
1718
  var MIGRATIONS = [
1116
1719
  {
1117
1720
  version: 1,
@@ -1211,6 +1814,32 @@ var MIGRATIONS = [
1211
1814
  );
1212
1815
  CREATE INDEX IF NOT EXISTS idx_files_hash ON workspace_files_mirror (content_hash);
1213
1816
  `
1817
+ },
1818
+ {
1819
+ version: 3,
1820
+ up: `
1821
+ -- Asset metadata index (#filename reference resolution \u2014 daemon/asset/metadata-index.ts)
1822
+ CREATE TABLE IF NOT EXISTS asset_metadata_index (
1823
+ workspace_id TEXT NOT NULL,
1824
+ asset_id TEXT NOT NULL,
1825
+ content_hash TEXT NOT NULL,
1826
+ filename TEXT,
1827
+ folder_path TEXT,
1828
+ mime TEXT NOT NULL,
1829
+ kind TEXT NOT NULL,
1830
+ size_bytes INTEGER NOT NULL DEFAULT 0,
1831
+ description TEXT,
1832
+ asset_index_seq INTEGER NOT NULL,
1833
+ updated_at INTEGER NOT NULL,
1834
+ PRIMARY KEY (workspace_id, asset_id)
1835
+ );
1836
+
1837
+ CREATE INDEX IF NOT EXISTS idx_asset_meta_filename
1838
+ ON asset_metadata_index(workspace_id, filename);
1839
+
1840
+ CREATE INDEX IF NOT EXISTS idx_asset_meta_seq
1841
+ ON asset_metadata_index(workspace_id, asset_index_seq);
1842
+ `
1214
1843
  }
1215
1844
  ];
1216
1845
  function runSql(db, sql) {
@@ -1386,7 +2015,7 @@ var product_manager_default = {
1386
2015
  description: "Writes PRDs, defines requirements, verifies implementations",
1387
2016
  applicableAdapters: ["hermes", "openclaw", "claude-code"],
1388
2017
  configSchema: {
1389
- model: "claude-3-5-sonnet",
2018
+ model: "us-kimi-k2.6",
1390
2019
  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",
1391
2020
  allowedTools: ["Read", "Write", "WebSearch"],
1392
2021
  maxTokens: 8e3
@@ -1400,7 +2029,7 @@ var engineer_default = {
1400
2029
  description: "Implements features per PRD, writes code, runs tests",
1401
2030
  applicableAdapters: ["hermes", "openclaw", "claude-code"],
1402
2031
  configSchema: {
1403
- model: "claude-3-5-sonnet",
2032
+ model: "us-kimi-k2.6",
1404
2033
  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",
1405
2034
  allowedTools: ["Read", "Write", "Edit", "Bash", "Grep"],
1406
2035
  maxTokens: 16e3
@@ -1414,7 +2043,7 @@ var ceo_default = {
1414
2043
  description: "Sets strategic direction, asks tough questions, makes go/no-go calls",
1415
2044
  applicableAdapters: ["hermes", "openclaw", "claude-code"],
1416
2045
  configSchema: {
1417
- model: "claude-3-5-sonnet",
2046
+ model: "us-kimi-k2.6",
1418
2047
  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",
1419
2048
  allowedTools: ["Read", "WebSearch"],
1420
2049
  maxTokens: 4e3
@@ -1428,7 +2057,7 @@ var researcher_default = {
1428
2057
  description: "Investigates topics, gathers sources, writes research memos with citations",
1429
2058
  applicableAdapters: ["hermes", "openclaw", "claude-code"],
1430
2059
  configSchema: {
1431
- model: "claude-3-5-sonnet",
2060
+ model: "us-kimi-k2.6",
1432
2061
  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",
1433
2062
  allowedTools: ["Read", "Write", "WebSearch", "WebFetch"],
1434
2063
  maxTokens: 12e3
@@ -1538,10 +2167,11 @@ function deriveWsUrl(httpBase) {
1538
2167
  }
1539
2168
 
1540
2169
  // src/daemon-id.ts
1541
- import { randomUUID } from "crypto";
2170
+ import { hostname } from "os";
1542
2171
  var PREFIX = "daemon-";
1543
2172
  function newDaemonId() {
1544
- return `${PREFIX}${randomUUID()}`;
2173
+ const host = hostname().replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "unknown";
2174
+ return `${PREFIX}${host}`;
1545
2175
  }
1546
2176
  function isDaemonId(s) {
1547
2177
  return s.startsWith(PREFIX) && s.length > PREFIX.length;
@@ -2262,6 +2892,14 @@ async function handleDispatch(payload, requestId, deps) {
2262
2892
  sendReply(deps.ws, reply, requestId);
2263
2893
  return reply;
2264
2894
  }
2895
+ let hashRefResult = { text: payload.prompt, resolutions: [] };
2896
+ if (deps.assetMetadataIndexes && profile.workspaceId) {
2897
+ const assetIndex = deps.assetMetadataIndexes.get(profile.workspaceId);
2898
+ if (assetIndex) {
2899
+ hashRefResult = await resolveHashRefs(payload.prompt, assetIndex, deps.cloud);
2900
+ payload.prompt = hashRefResult.text;
2901
+ }
2902
+ }
2265
2903
  const rewrittenPrompt = await deps.uriResolver.rewrite(payload.prompt, { pin: true });
2266
2904
  resolvedHashes.push(...rewrittenPrompt.resolvedHashes);
2267
2905
  let rewrittenContext = [];
@@ -2476,6 +3114,84 @@ function isTextLikeMime(mime) {
2476
3114
  if (m.endsWith("+json") || m.endsWith("+xml") || m.endsWith("+csv")) return true;
2477
3115
  return false;
2478
3116
  }
3117
+ var HASH_REF_RE = /(?:^|\s)#([^\s#]+)/g;
3118
+ var HEX_COLOR_RE = /^[0-9a-fA-F]{3,8}$/;
3119
+ var FILE_EXT_RE = /\.[a-zA-Z0-9]{1,10}$/;
3120
+ var TRAILING_PUNCT_RE = /[,.;:!?)\]}'"]+$/;
3121
+ async function resolveHashRefs(prompt, assetIndex, cloud) {
3122
+ const resolutions = [];
3123
+ const candidates = [];
3124
+ let match;
3125
+ const re = new RegExp(HASH_REF_RE.source, "g");
3126
+ while ((match = re.exec(prompt)) !== null) {
3127
+ const refName = match[1];
3128
+ const leading = match[0].startsWith("#") ? 0 : 1;
3129
+ const start = match.index + leading;
3130
+ const end = match.index + match[0].length;
3131
+ if (HEX_COLOR_RE.test(refName)) continue;
3132
+ let cleanRef = refName;
3133
+ let stripped = "";
3134
+ const punctMatch = TRAILING_PUNCT_RE.exec(cleanRef);
3135
+ if (punctMatch) {
3136
+ stripped = punctMatch[0];
3137
+ cleanRef = cleanRef.slice(0, -stripped.length);
3138
+ }
3139
+ if (!cleanRef) continue;
3140
+ if (HEX_COLOR_RE.test(cleanRef)) continue;
3141
+ const hasExtension = FILE_EXT_RE.test(cleanRef);
3142
+ candidates.push({ ref: cleanRef, start, end: end - stripped.length, hasExtension });
3143
+ }
3144
+ if (candidates.length === 0) {
3145
+ return { text: prompt, resolutions: [] };
3146
+ }
3147
+ const allFilenames = candidates.map((c) => c.ref);
3148
+ const localResults = assetIndex.resolveByFilenames(allFilenames);
3149
+ const needsCloud = candidates.filter(
3150
+ (c) => c.hasExtension && !localResults.has(c.ref)
3151
+ );
3152
+ const cloudResults = /* @__PURE__ */ new Map();
3153
+ if (needsCloud.length > 0) {
3154
+ await Promise.allSettled(
3155
+ needsCloud.map(async (c) => {
3156
+ try {
3157
+ const items = await cloud.get(
3158
+ `/api/im/assets?workspaceId=${encodeURIComponent(assetIndex.workspaceId)}&q=${encodeURIComponent(c.ref)}&limit=1`
3159
+ );
3160
+ if (Array.isArray(items) && items.length > 0) {
3161
+ const item = items[0];
3162
+ cloudResults.set(c.ref, item.contentHash);
3163
+ }
3164
+ } catch {
3165
+ }
3166
+ })
3167
+ );
3168
+ }
3169
+ for (const c of candidates) {
3170
+ const local = localResults.get(c.ref);
3171
+ if (local) {
3172
+ resolutions.push({
3173
+ ref: c.ref,
3174
+ start: c.start,
3175
+ end: c.end,
3176
+ resolvedUri: `prismer://workspace/${encodeURIComponent(assetIndex.workspaceId)}/asset/${local.contentHash}`
3177
+ });
3178
+ } else if (c.hasExtension) {
3179
+ const cloudHash = cloudResults.get(c.ref);
3180
+ resolutions.push({
3181
+ ref: c.ref,
3182
+ start: c.start,
3183
+ end: c.end,
3184
+ resolvedUri: cloudHash ? `prismer://workspace/${encodeURIComponent(assetIndex.workspaceId)}/asset/${cloudHash}` : void 0
3185
+ });
3186
+ }
3187
+ }
3188
+ let result = prompt;
3189
+ const sorted = [...resolutions].filter((r) => r.resolvedUri).sort((a, b) => b.start - a.start);
3190
+ for (const r of sorted) {
3191
+ result = result.slice(0, r.start) + r.resolvedUri + result.slice(r.end);
3192
+ }
3193
+ return { text: result, resolutions };
3194
+ }
2479
3195
  async function resolveAssetRefs(refs, cache) {
2480
3196
  const out = { promptBlocks: [], observability: [], pinnedHashes: [] };
2481
3197
  if (!refs || refs.length === 0) return out;
@@ -2548,8 +3264,8 @@ async function resolveAssetRefs(refs, cache) {
2548
3264
  return out;
2549
3265
  }
2550
3266
  function formatInlineAssetBlock(ref, mime, body, strategy) {
2551
- const header = `[Attached file] id=${ref.assetId} mime=${mime ?? "unknown"}${strategy === "inline-text-truncated" ? " (truncated)" : ""}`;
2552
- return `${header}
3267
+ const header2 = `[Attached file] id=${ref.assetId} mime=${mime ?? "unknown"}${strategy === "inline-text-truncated" ? " (truncated)" : ""}`;
3268
+ return `${header2}
2553
3269
  ---
2554
3270
  ${body}
2555
3271
  ---`;
@@ -2864,7 +3580,7 @@ var ServicePool = class {
2864
3580
 
2865
3581
  // src/daemon/local-server.ts
2866
3582
  import { createServer } from "http";
2867
- import { randomUUID as randomUUID2, createHash } from "crypto";
3583
+ import { randomUUID, createHash } from "crypto";
2868
3584
  import { promises as fs } from "fs";
2869
3585
  import * as path2 from "path";
2870
3586
  var LocalServer = class {
@@ -2898,20 +3614,35 @@ var LocalServer = class {
2898
3614
  respond(res, 204, null);
2899
3615
  return;
2900
3616
  }
3617
+ const handlers = [];
2901
3618
  if (this.opts.attachMemory) {
2902
- void this.opts.attachMemory(req, res).then((handled) => {
2903
- if (handled) return;
2904
- this.routeStandard(req, res);
2905
- }).catch((err) => {
2906
- respond(res, 500, {
2907
- error: "attach_memory_threw",
2908
- message: err instanceof Error ? err.message : String(err)
2909
- });
2910
- });
3619
+ handlers.push({ name: "memory", fn: this.opts.attachMemory });
3620
+ }
3621
+ if (this.opts.attachAsset) {
3622
+ handlers.push({ name: "asset", fn: this.opts.attachAsset });
3623
+ }
3624
+ if (handlers.length > 0) {
3625
+ void this.runHandlers(req, res, handlers, 0);
2911
3626
  return;
2912
3627
  }
2913
3628
  this.routeStandard(req, res);
2914
3629
  }
3630
+ async runHandlers(req, res, handlers, idx) {
3631
+ if (idx >= handlers.length) {
3632
+ this.routeStandard(req, res);
3633
+ return;
3634
+ }
3635
+ try {
3636
+ const handled = await handlers[idx].fn(req, res);
3637
+ if (handled) return;
3638
+ await this.runHandlers(req, res, handlers, idx + 1);
3639
+ } catch (err) {
3640
+ respond(res, 500, {
3641
+ error: `attach_${handlers[idx].name}_threw`,
3642
+ message: err instanceof Error ? err.message : String(err)
3643
+ });
3644
+ }
3645
+ }
2915
3646
  routeStandard(req, res) {
2916
3647
  const url = req.url ?? "/";
2917
3648
  if (req.method === "GET" && url === "/healthz") {
@@ -2926,7 +3657,8 @@ var LocalServer = class {
2926
3657
  wsConnected: state.wsConnected,
2927
3658
  hostedAgents: state.hostedAgents,
2928
3659
  observability: state.observability,
2929
- memoryReady: this.opts.attachMemory != null
3660
+ memoryReady: this.opts.attachMemory != null,
3661
+ assetReady: this.opts.attachAsset != null
2930
3662
  });
2931
3663
  return;
2932
3664
  }
@@ -3078,7 +3810,7 @@ var LocalServer = class {
3078
3810
  respond(res, 400, { error: "missing_taskId" });
3079
3811
  return;
3080
3812
  }
3081
- const runId = randomUUID2();
3813
+ const runId = randomUUID();
3082
3814
  try {
3083
3815
  this.opts.onDispatch?.(payload, runId);
3084
3816
  } catch (err) {
@@ -3169,7 +3901,7 @@ async function walkAndDigest(root, current) {
3169
3901
 
3170
3902
  // src/daemon/runner.ts
3171
3903
  import { EventEmitter as EventEmitter3 } from "events";
3172
- import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
3904
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
3173
3905
  import { platform } from "os";
3174
3906
 
3175
3907
  // src/adapters/claude-code/index.ts
@@ -3821,7 +4553,7 @@ import * as path4 from "path";
3821
4553
  import Database3 from "better-sqlite3";
3822
4554
  import * as fs2 from "fs";
3823
4555
  import * as path3 from "path";
3824
- import { randomUUID as randomUUID3, createHash as createHash2 } from "crypto";
4556
+ import { randomUUID as randomUUID2, createHash as createHash2 } from "crypto";
3825
4557
 
3826
4558
  // src/daemon/memory/crypto.ts
3827
4559
  function sealPlaintext(content) {
@@ -4023,16 +4755,17 @@ var MemoryStore = class {
4023
4755
  const existing = db.prepare(
4024
4756
  "SELECT id, version, createdAt FROM memory_pages WHERE workspaceId = ? AND path = ?"
4025
4757
  ).get(this.opts.workspaceId, input.path);
4026
- const pageId = existing?.id ?? `page_${randomUUID3().replace(/-/g, "").slice(0, 22)}`;
4758
+ const pageId = existing?.id ?? `page_${randomUUID2().replace(/-/g, "").slice(0, 22)}`;
4027
4759
  const newVersion = (existing?.version ?? 0) + 1;
4028
4760
  const createdAt = existing?.createdAt ?? now;
4761
+ const staleFlag = input.stale ? 1 : 0;
4029
4762
  const insertPage = db.prepare(`
4030
4763
  INSERT INTO memory_pages (
4031
4764
  id, workspaceId, path, title, description, contentHash, version,
4032
4765
  pageType, visibilityKind, visibilityImUserId, encrypted, stale,
4033
4766
  archivedAt, sourceAssetId, sourceRefsJson, syncStatus,
4034
4767
  createdAt, updatedAt
4035
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, NULL, ?, ?, 'local-only', ?, ?)
4768
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, NULL, ?, ?, 'local-only', ?, ?)
4036
4769
  ON CONFLICT(workspaceId, path) DO UPDATE SET
4037
4770
  title = excluded.title,
4038
4771
  description = excluded.description,
@@ -4043,6 +4776,7 @@ var MemoryStore = class {
4043
4776
  visibilityImUserId = excluded.visibilityImUserId,
4044
4777
  sourceAssetId = excluded.sourceAssetId,
4045
4778
  sourceRefsJson = excluded.sourceRefsJson,
4779
+ stale = excluded.stale,
4046
4780
  updatedAt = excluded.updatedAt
4047
4781
  `);
4048
4782
  const insertVersion = db.prepare(`
@@ -4070,6 +4804,7 @@ var MemoryStore = class {
4070
4804
  input.pageType ?? "leaf",
4071
4805
  visibility.kind,
4072
4806
  visibilityImUserId,
4807
+ staleFlag,
4073
4808
  input.sourceAssetId ?? null,
4074
4809
  sourceRefsJson,
4075
4810
  createdAt,
@@ -4150,6 +4885,16 @@ var MemoryStore = class {
4150
4885
  dbPath: this.opts.dbPath
4151
4886
  };
4152
4887
  }
4888
+ /**
4889
+ * Record sync cursor for incremental sync. Used by cloud-sync.ts to
4890
+ * persist the high-water mark for future cursor-based catch-up.
4891
+ */
4892
+ recordCursor(workspaceId, cursor) {
4893
+ const now = Date.now();
4894
+ this.requireDb().prepare(
4895
+ `INSERT OR REPLACE INTO memory_inbox_cursor (workspaceId, cursor, updatedAt) VALUES (?, ?, ?)`
4896
+ ).run(workspaceId, cursor, now);
4897
+ }
4153
4898
  /**
4154
4899
  * Internal accessor for outbox.ts — outbox writes its own table within the
4155
4900
  * same DB. Returning the live Database handle keeps outbox transactions
@@ -4300,7 +5045,7 @@ function clamp(n, lo, hi) {
4300
5045
  }
4301
5046
 
4302
5047
  // src/daemon/memory/outbox.ts
4303
- import { randomUUID as randomUUID4 } from "crypto";
5048
+ import { randomUUID as randomUUID3 } from "crypto";
4304
5049
 
4305
5050
  // src/daemon/memory/envelope.ts
4306
5051
  import { z as z6 } from "zod";
@@ -4433,7 +5178,7 @@ var MemoryOutbox = class {
4433
5178
  const now = Date.now();
4434
5179
  const parsed = MemoryOutboxEnvelope.safeParse(event);
4435
5180
  if (!parsed.success) {
4436
- const dlId = `dl_${randomUUID4()}`;
5181
+ const dlId = `dl_${randomUUID3()}`;
4437
5182
  db.prepare(
4438
5183
  `INSERT INTO memory_outbox_dead_letter (id, eventType, rawJson, errorJson, createdAt)
4439
5184
  VALUES (?, ?, ?, ?, ?)`
@@ -4450,7 +5195,7 @@ var MemoryOutbox = class {
4450
5195
  if (validated.eventType === "memory.feedback") {
4451
5196
  const check = validateFeedbackTarget(validated);
4452
5197
  if (!check.ok) {
4453
- const dlId = `dl_${randomUUID4()}`;
5198
+ const dlId = `dl_${randomUUID3()}`;
4454
5199
  db.prepare(
4455
5200
  `INSERT INTO memory_outbox_dead_letter (id, eventType, rawJson, errorJson, createdAt)
4456
5201
  VALUES (?, ?, ?, ?, ?)`
@@ -4464,7 +5209,7 @@ var MemoryOutbox = class {
4464
5209
  return { id: dlId, deadLetter: true };
4465
5210
  }
4466
5211
  }
4467
- const rowId = `out_${randomUUID4()}`;
5212
+ const rowId = `out_${randomUUID3()}`;
4468
5213
  try {
4469
5214
  db.prepare(
4470
5215
  `INSERT INTO memory_outbox (id, eventType, envelopeJson, idempotencyKey, status, createdAt)
@@ -4800,6 +5545,84 @@ function defaultLog() {
4800
5545
  };
4801
5546
  }
4802
5547
 
5548
+ // src/daemon/memory/cloud-sync.ts
5549
+ var LOG = "[CloudMemorySync]";
5550
+ var CLOUD_PAGE_LIMIT = 300;
5551
+ async function initialSyncFromCloud(runtime, cloud, workspaceId) {
5552
+ const slot = runtime.peek(workspaceId);
5553
+ if (!slot) {
5554
+ console.log(`${LOG} No store for workspace=${workspaceId} \u2014 skipping`);
5555
+ return { pulled: 0, skipped: 0 };
5556
+ }
5557
+ const cursorRow = slot.store.rawDb().prepare("SELECT cursor FROM memory_inbox_cursor WHERE workspaceId = ?").get(workspaceId);
5558
+ if (cursorRow) {
5559
+ console.log(`${LOG} Workspace=${workspaceId} already synced (cursor: ${cursorRow.cursor.slice(0, 20)}...) \u2014 skip`);
5560
+ return { pulled: 0, skipped: 0 };
5561
+ }
5562
+ console.log(`${LOG} Fetching cloud pages for workspace=${workspaceId}...`);
5563
+ const resp = await cloud.request(
5564
+ "GET",
5565
+ `/api/im/memory/pages?workspaceId=${encodeURIComponent(workspaceId)}&limit=${CLOUD_PAGE_LIMIT}&stale=all`,
5566
+ { timeoutMs: 15e3 }
5567
+ );
5568
+ if (!resp.ok) {
5569
+ console.warn(
5570
+ `${LOG} Cloud GET /memory/pages returned ${resp.status}: ${resp.error?.message ?? "unknown"}`
5571
+ );
5572
+ return { pulled: 0, skipped: 0 };
5573
+ }
5574
+ const envelope2 = resp.data;
5575
+ if (!envelope2 || !envelope2.ok) {
5576
+ console.log(`${LOG} Cloud returned non-ok envelope for workspace=${workspaceId}`);
5577
+ return { pulled: 0, skipped: 0 };
5578
+ }
5579
+ const pages = envelope2.data;
5580
+ if (!pages || !Array.isArray(pages) || pages.length === 0) {
5581
+ console.log(`${LOG} No cloud pages to sync for workspace=${workspaceId}`);
5582
+ return { pulled: 0, skipped: 0 };
5583
+ }
5584
+ let pulled = 0;
5585
+ let skipped = 0;
5586
+ for (const page of pages) {
5587
+ let content = page.content ?? "";
5588
+ if (!content) {
5589
+ try {
5590
+ const detailResp = await cloud.request(
5591
+ "GET",
5592
+ `/api/im/memory/pages/${encodeURIComponent(page.id)}?workspaceId=${encodeURIComponent(workspaceId)}`,
5593
+ { timeoutMs: 5e3 }
5594
+ );
5595
+ if (detailResp.ok && detailResp.data?.data?.content) {
5596
+ content = detailResp.data.data.content;
5597
+ }
5598
+ } catch {
5599
+ }
5600
+ }
5601
+ const visibility = page.visibility === "agent" ? { kind: "agent", imUserId: "" } : { kind: "workspace" };
5602
+ try {
5603
+ slot.store.write({
5604
+ workspaceId,
5605
+ path: page.path,
5606
+ title: page.title ?? void 0,
5607
+ content: content || "",
5608
+ pageType: page.pageType || "leaf",
5609
+ visibility,
5610
+ actorImUserId: "cloud-sync",
5611
+ actorKind: "agent"
5612
+ });
5613
+ pulled++;
5614
+ } catch (err) {
5615
+ console.warn(`${LOG} write failed for ${page.path}:`, err.message);
5616
+ skipped++;
5617
+ }
5618
+ }
5619
+ slot.store.recordCursor(workspaceId, `synced:${Date.now()}`);
5620
+ console.log(
5621
+ `${LOG} Synced ${pulled} pages${skipped ? `, ${skipped} skipped` : ""} for workspace=${workspaceId}`
5622
+ );
5623
+ return { pulled, skipped };
5624
+ }
5625
+
4803
5626
  // src/daemon/memory/runner-wiring.ts
4804
5627
  function attachMemoryRunner(opts) {
4805
5628
  const runtime = new MemoryRuntime({ baseDir: opts.baseDir, deviceId: opts.deviceId });
@@ -4830,6 +5653,24 @@ function attachMemoryRunner(opts) {
4830
5653
  }
4831
5654
  };
4832
5655
  }
5656
+ async function syncMemoryFromCloud(wiring, cloud, workspaceIds) {
5657
+ const uniqueIds = [...new Set(workspaceIds.filter(Boolean))];
5658
+ if (uniqueIds.length === 0) return;
5659
+ console.log(`[MemorySync] Initial cloud-to-local sync for ${uniqueIds.length} workspace(s)...`);
5660
+ for (const wsId of uniqueIds) {
5661
+ try {
5662
+ wiring.runtime.resolve(wsId);
5663
+ const result = await initialSyncFromCloud(wiring.runtime, cloud, wsId);
5664
+ if (result.pulled > 0 || result.skipped > 0) {
5665
+ console.log(
5666
+ `[MemorySync] workspace=${wsId}: ${result.pulled} pulled, ${result.skipped} skipped`
5667
+ );
5668
+ }
5669
+ } catch (err) {
5670
+ console.error(`[MemorySync] workspace=${wsId} failed:`, err.message);
5671
+ }
5672
+ }
5673
+ }
4833
5674
 
4834
5675
  // src/daemon/memory/fork/select-memories.ts
4835
5676
  var SELECT_MEMORIES_SYSTEM_PROMPT = [
@@ -4948,7 +5789,7 @@ function clamp2(n, lo, hi) {
4948
5789
  }
4949
5790
 
4950
5791
  // src/daemon/memory/fork/tracing.ts
4951
- import { randomUUID as randomUUID5 } from "crypto";
5792
+ import { randomUUID as randomUUID4 } from "crypto";
4952
5793
 
4953
5794
  // src/daemon/memory/fork/runner.ts
4954
5795
  var DEFAULT_SNIPPET_MAX_BYTES = 500;
@@ -5204,37 +6045,264 @@ function handleRecallManifest(runtime, query, res) {
5204
6045
  respond2(res, 200, manifest);
5205
6046
  return true;
5206
6047
  }
5207
- function handleRecallFinalize(runtime, body, res) {
6048
+ function handleRecallFinalize(runtime, body, res) {
6049
+ if (!body || typeof body !== "object") {
6050
+ return respond400(res, "json body required");
6051
+ }
6052
+ const b = body;
6053
+ if (typeof b.workspaceId !== "string" || !b.workspaceId) {
6054
+ return respond400(res, "workspaceId is required");
6055
+ }
6056
+ if (!Array.isArray(b.paths)) {
6057
+ return respond400(res, "paths[] is required");
6058
+ }
6059
+ const paths = [];
6060
+ for (const p of b.paths) {
6061
+ if (typeof p === "string" && p.length > 0) paths.push(p);
6062
+ }
6063
+ const snippetMaxBytes = typeof b.snippetMaxBytes === "number" && b.snippetMaxBytes > 0 ? b.snippetMaxBytes : void 0;
6064
+ const results = finalizeSelected(runtime, b.workspaceId, paths, snippetMaxBytes);
6065
+ respond2(res, 200, { workspaceId: b.workspaceId, results });
6066
+ return true;
6067
+ }
6068
+ function parsePrismerUri(uri) {
6069
+ const PREFIX2 = "prismer://workspace/";
6070
+ if (!uri.startsWith(PREFIX2)) return null;
6071
+ const rest = uri.slice(PREFIX2.length);
6072
+ const segMemory = "/memory/";
6073
+ const memoryIdx = rest.indexOf(segMemory);
6074
+ if (memoryIdx <= 0) return null;
6075
+ const workspaceId = rest.slice(0, memoryIdx);
6076
+ const path7 = rest.slice(memoryIdx + segMemory.length);
6077
+ if (!workspaceId || !path7) return null;
6078
+ return { workspaceId, path: path7 };
6079
+ }
6080
+
6081
+ // src/daemon/asset/metadata-index.ts
6082
+ import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
6083
+ import { join as join8 } from "path";
6084
+ var DEFAULT_LIMIT = 8;
6085
+ var PULL_PAGE_SIZE = 500;
6086
+ var THROTTLE_MS = 3e4;
6087
+ function rowToMetadata(row) {
6088
+ return {
6089
+ assetId: row.asset_id,
6090
+ contentHash: row.content_hash,
6091
+ filename: row.filename,
6092
+ folderPath: row.folder_path,
6093
+ mime: row.mime,
6094
+ kind: row.kind,
6095
+ sizeBytes: row.size_bytes,
6096
+ description: row.description,
6097
+ assetIndexSeq: row.asset_index_seq
6098
+ };
6099
+ }
6100
+ var AssetMetadataIndex = class {
6101
+ db;
6102
+ cloud;
6103
+ /** Workspace ID — exposed for prismer:// URI construction. */
6104
+ workspaceId;
6105
+ cursorPath;
6106
+ _lastSyncMs = 0;
6107
+ constructor(opts) {
6108
+ this.db = opts.db;
6109
+ this.cloud = opts.cloud;
6110
+ this.workspaceId = opts.workspaceId;
6111
+ if (!existsSync7(opts.workspaceStateDir)) {
6112
+ mkdirSync7(opts.workspaceStateDir, { recursive: true });
6113
+ }
6114
+ this.cursorPath = join8(opts.workspaceStateDir, "asset-metadata-cursor.json");
6115
+ }
6116
+ /** Persisted cursor for this workspace, or 0 on first run / corrupted file. */
6117
+ readCursor() {
6118
+ if (!existsSync7(this.cursorPath)) return 0;
6119
+ try {
6120
+ const parsed = JSON.parse(readFileSync5(this.cursorPath, "utf8"));
6121
+ if (parsed.workspaceId !== this.workspaceId) return 0;
6122
+ return parsed.cursor;
6123
+ } catch {
6124
+ return 0;
6125
+ }
6126
+ }
6127
+ writeCursor(cursor) {
6128
+ const payload = {
6129
+ workspaceId: this.workspaceId,
6130
+ cursor,
6131
+ writtenAt: Date.now()
6132
+ };
6133
+ writeFileSync5(this.cursorPath, JSON.stringify(payload, null, 2));
6134
+ }
6135
+ /**
6136
+ * Pull incremental asset metadata changes since the persisted cursor and
6137
+ * upsert into the local index. Newer rows overwrite older ones by
6138
+ * (workspace_id, asset_id) primary key.
6139
+ *
6140
+ * Returns the count of items applied + the new cursor. Throttled: if called
6141
+ * within 30s of the last successful pull, returns immediately.
6142
+ */
6143
+ async pullDelta(opts) {
6144
+ const now = Date.now();
6145
+ if (now - this._lastSyncMs < THROTTLE_MS) {
6146
+ return { applied: 0, cursor: this.readCursor() };
6147
+ }
6148
+ const since = this.readCursor();
6149
+ const sinceParam = since > 0 ? `&since=${since}` : "";
6150
+ const envelope2 = await this.cloud.get(
6151
+ `/api/im/assets/index?workspaceId=${encodeURIComponent(this.workspaceId)}&limit=${PULL_PAGE_SIZE}${sinceParam}`,
6152
+ { signal: opts?.signal }
6153
+ );
6154
+ const upsert = this.db.prepare(`
6155
+ INSERT INTO asset_metadata_index
6156
+ (workspace_id, asset_id, content_hash, filename, folder_path, mime, kind, size_bytes, description, asset_index_seq, updated_at)
6157
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6158
+ ON CONFLICT(workspace_id, asset_id) DO UPDATE SET
6159
+ content_hash = excluded.content_hash,
6160
+ filename = excluded.filename,
6161
+ folder_path = excluded.folder_path,
6162
+ mime = excluded.mime,
6163
+ kind = excluded.kind,
6164
+ size_bytes = excluded.size_bytes,
6165
+ description = excluded.description,
6166
+ asset_index_seq = excluded.asset_index_seq,
6167
+ updated_at = excluded.updated_at
6168
+ `);
6169
+ const nowTs = Date.now();
6170
+ let applied = 0;
6171
+ const tx = this.db.transaction((items) => {
6172
+ for (const item of items) {
6173
+ upsert.run(
6174
+ this.workspaceId,
6175
+ item.assetId,
6176
+ item.contentHash,
6177
+ item.filename ?? null,
6178
+ item.folderPath ?? null,
6179
+ item.mime,
6180
+ item.kind,
6181
+ item.sizeBytes,
6182
+ item.description ?? null,
6183
+ item.assetIndexSeq,
6184
+ nowTs
6185
+ );
6186
+ applied += 1;
6187
+ }
6188
+ });
6189
+ try {
6190
+ tx(envelope2.items);
6191
+ this.writeCursor(envelope2.cursor);
6192
+ } catch (err) {
6193
+ throw err;
6194
+ }
6195
+ this._lastSyncMs = now;
6196
+ return { applied, cursor: envelope2.cursor };
6197
+ }
6198
+ /**
6199
+ * Search local index by filename or description substring.
6200
+ * Escapes LIKE wildcards (% and _). Default limit 8.
6201
+ */
6202
+ search(query, limit) {
6203
+ const escaped = query.replace(/%/g, "\\%").replace(/_/g, "\\_");
6204
+ const pattern = `%${escaped}%`;
6205
+ const limitVal = Math.min(Math.max(limit ?? DEFAULT_LIMIT, 1), 200);
6206
+ const rows = this.db.prepare(
6207
+ `SELECT * FROM asset_metadata_index
6208
+ WHERE workspace_id = ?
6209
+ AND (filename LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\')
6210
+ ORDER BY asset_index_seq DESC
6211
+ LIMIT ?`
6212
+ ).all(this.workspaceId, pattern, pattern, limitVal);
6213
+ return rows.map(rowToMetadata);
6214
+ }
6215
+ /** Exact match on filename column. Returns undefined if not indexed. */
6216
+ resolveByFilename(filename) {
6217
+ const row = this.db.prepare("SELECT * FROM asset_metadata_index WHERE workspace_id = ? AND filename = ?").get(this.workspaceId, filename);
6218
+ return row ? rowToMetadata(row) : void 0;
6219
+ }
6220
+ /** Batch exact match — returns Map for O(1) access. */
6221
+ resolveByFilenames(filenames) {
6222
+ if (filenames.length === 0) return /* @__PURE__ */ new Map();
6223
+ const placeholders = filenames.map(() => "?").join(",");
6224
+ const params = [this.workspaceId, ...filenames];
6225
+ const rows = this.db.prepare(
6226
+ `SELECT * FROM asset_metadata_index
6227
+ WHERE workspace_id = ? AND filename IN (${placeholders})`
6228
+ ).all(...params);
6229
+ const map = /* @__PURE__ */ new Map();
6230
+ for (const row of rows) {
6231
+ if (row.filename) map.set(row.filename, rowToMetadata(row));
6232
+ }
6233
+ return map;
6234
+ }
6235
+ };
6236
+
6237
+ // src/daemon/asset/rpc.ts
6238
+ var ASSET_PATH_PREFIX = "/local/asset/";
6239
+ function attachAssetRpc(opts) {
6240
+ return async (req, res) => {
6241
+ const url = req.url ?? "/";
6242
+ if (!url.startsWith(ASSET_PATH_PREFIX)) return false;
6243
+ const [pathOnly = ""] = url.split("?", 2);
6244
+ const subpath = pathOnly.slice(ASSET_PATH_PREFIX.length);
6245
+ const method = req.method ?? "GET";
6246
+ try {
6247
+ if (method === "POST" && subpath === "search") {
6248
+ const body = await readJson3(req);
6249
+ return handleSearch2(opts.resolveIndex, body, res);
6250
+ }
6251
+ respond3(res, 404, { error: "asset_route_not_found", path: url });
6252
+ return true;
6253
+ } catch (err) {
6254
+ respond3(res, 500, {
6255
+ error: "asset_rpc_failed",
6256
+ message: err instanceof Error ? err.message : String(err)
6257
+ });
6258
+ return true;
6259
+ }
6260
+ };
6261
+ }
6262
+ function handleSearch2(resolveIndex, body, res) {
5208
6263
  if (!body || typeof body !== "object") {
5209
- return respond400(res, "json body required");
6264
+ return respond4002(res, "request body must be a JSON object");
5210
6265
  }
5211
6266
  const b = body;
5212
6267
  if (typeof b.workspaceId !== "string" || !b.workspaceId) {
5213
- return respond400(res, "workspaceId is required");
6268
+ return respond4002(res, "workspaceId is required (string)");
5214
6269
  }
5215
- if (!Array.isArray(b.paths)) {
5216
- return respond400(res, "paths[] is required");
6270
+ if (typeof b.query !== "string" || !b.query.trim()) {
6271
+ return respond4002(res, "query is required (non-empty string)");
5217
6272
  }
5218
- const paths = [];
5219
- for (const p of b.paths) {
5220
- if (typeof p === "string" && p.length > 0) paths.push(p);
6273
+ const index = resolveIndex(b.workspaceId);
6274
+ if (!index) {
6275
+ respond3(res, 404, {
6276
+ error: "workspace_index_not_found",
6277
+ workspaceId: b.workspaceId,
6278
+ message: "No asset metadata index for this workspace. Ensure the daemon has synced asset metadata."
6279
+ });
6280
+ return true;
5221
6281
  }
5222
- const snippetMaxBytes = typeof b.snippetMaxBytes === "number" && b.snippetMaxBytes > 0 ? b.snippetMaxBytes : void 0;
5223
- const results = finalizeSelected(runtime, b.workspaceId, paths, snippetMaxBytes);
5224
- respond2(res, 200, { workspaceId: b.workspaceId, results });
6282
+ const limit = typeof b.limit === "number" && b.limit > 0 ? b.limit : void 0;
6283
+ const items = index.search(b.query.trim(), limit);
6284
+ respond3(res, 200, { items });
5225
6285
  return true;
5226
6286
  }
5227
- function parsePrismerUri(uri) {
5228
- const PREFIX2 = "prismer://workspace/";
5229
- if (!uri.startsWith(PREFIX2)) return null;
5230
- const rest = uri.slice(PREFIX2.length);
5231
- const segMemory = "/memory/";
5232
- const memoryIdx = rest.indexOf(segMemory);
5233
- if (memoryIdx <= 0) return null;
5234
- const workspaceId = rest.slice(0, memoryIdx);
5235
- const path7 = rest.slice(memoryIdx + segMemory.length);
5236
- if (!workspaceId || !path7) return null;
5237
- return { workspaceId, path: path7 };
6287
+ function respond3(res, status, body) {
6288
+ res.statusCode = status;
6289
+ res.setHeader("Content-Type", "application/json");
6290
+ res.end(JSON.stringify(body));
6291
+ }
6292
+ function respond4002(res, message) {
6293
+ respond3(res, 400, { error: "invalid_request", message });
6294
+ return true;
6295
+ }
6296
+ async function readJson3(req) {
6297
+ let raw = "";
6298
+ req.setEncoding("utf8");
6299
+ for await (const chunk of req) raw += chunk;
6300
+ if (!raw) return {};
6301
+ try {
6302
+ return JSON.parse(raw);
6303
+ } catch {
6304
+ throw new Error("invalid_json");
6305
+ }
5238
6306
  }
5239
6307
 
5240
6308
  // src/daemon/outbox-watcher.ts
@@ -5494,7 +6562,7 @@ var OutboxWatcher = class {
5494
6562
 
5495
6563
  // src/daemon/shell-executor.ts
5496
6564
  import { spawn as spawn4 } from "child_process";
5497
- import { existsSync as existsSync7 } from "fs";
6565
+ import { existsSync as existsSync8 } from "fs";
5498
6566
  import { resolve } from "path";
5499
6567
  var DEFAULT_OUTPUT_LIMIT = 256 * 1024;
5500
6568
  var DEFAULT_TIMEOUT = 6e4;
@@ -5531,7 +6599,7 @@ async function executeShellDispatch(payload, deps) {
5531
6599
  const command = readCommand(payload, execution);
5532
6600
  if (!command.trim()) return fail(payload.taskId, "shell_command_required", "Shell command is required");
5533
6601
  const cwd = resolveCwd(execution.cwd, deps.config.defaultCwd);
5534
- if (!existsSync7(cwd)) return fail(payload.taskId, "shell_cwd_missing", `cwd does not exist: ${cwd}`);
6602
+ if (!existsSync8(cwd)) return fail(payload.taskId, "shell_cwd_missing", `cwd does not exist: ${cwd}`);
5535
6603
  const timeoutMs = Math.min(
5536
6604
  typeof payload.timeoutMs === "number" && payload.timeoutMs > 0 ? payload.timeoutMs : deps.config.maxTimeoutMs,
5537
6605
  deps.config.maxTimeoutMs
@@ -5683,6 +6751,7 @@ var Runner = class extends EventEmitter3 {
5683
6751
  localServer;
5684
6752
  outboxWatcher;
5685
6753
  memoryWiring;
6754
+ assetMetadataIndexes = /* @__PURE__ */ new Map();
5686
6755
  state = "idle";
5687
6756
  startedAt = 0;
5688
6757
  workspaceId = "";
@@ -5699,6 +6768,9 @@ var Runner = class extends EventEmitter3 {
5699
6768
  this.paths = this.opts.paths ?? resolvePaths();
5700
6769
  this.config = this.opts.configOverride ?? loadConfig(this.paths);
5701
6770
  this.shellConfig = resolveShellConfig(this.config.shell);
6771
+ if (process.env.PRISMER_WORKSPACE_ID) {
6772
+ this.workspaceId = process.env.PRISMER_WORKSPACE_ID;
6773
+ }
5702
6774
  process.env.PRISMER_BASE_URL = this.config.cloud_api_base;
5703
6775
  process.env.PRISMER_API_KEY = this.config.api_key;
5704
6776
  this.db = openLocalDb(this.paths.localDb);
@@ -5736,6 +6808,16 @@ var Runner = class extends EventEmitter3 {
5736
6808
  baseDir: `${this.paths.root}/memory`,
5737
6809
  deviceId: this.config.daemon_id
5738
6810
  });
6811
+ if (this.memoryWiring && this.workspaceId) {
6812
+ syncMemoryFromCloud(this.memoryWiring, this.cloud, [this.workspaceId]).catch(
6813
+ (err) => console.error("[Daemon] Initial memory sync failed:", err.message)
6814
+ );
6815
+ }
6816
+ if (this.workspaceId) {
6817
+ this.syncAssetMetadata(this.workspaceId).catch(
6818
+ (err) => console.error("[Daemon] Initial asset metadata sync failed:", err.message)
6819
+ );
6820
+ }
5739
6821
  const containerId = process.env.PRISMER_CONTAINER_ID;
5740
6822
  const isContainer = !!containerId || process.env.PRISMER_RUNTIME_MODE === "container";
5741
6823
  this.outboxWatcher = new OutboxWatcher({
@@ -5782,1374 +6864,875 @@ var Runner = class extends EventEmitter3 {
5782
6864
  // shipped the rpc.ts route table but T1 deliberately deferred the
5783
6865
  // wiring step so it could be reviewed alongside the host-adapter
5784
6866
  // consumer (Hermes T2-B), which is what surfaces these routes to
5785
- // an actual agent process.
5786
- attachMemory: this.memoryWiring ? attachMemoryRpc({ runtime: this.memoryWiring.runtime }) : void 0
5787
- });
5788
- await this.localServer.start();
5789
- }
5790
- this.heartbeatTimer = setInterval(() => {
5791
- if (this.wsConnected) this.sendDeclare();
5792
- }, 3e4);
5793
- this.taskReaperTimer = setInterval(() => {
5794
- const now = Date.now();
5795
- for (const [taskId, entry] of this.runningTasks) {
5796
- const limit = Math.max(entry.timeoutMs, 5 * 6e4);
5797
- if (now - entry.lastProgressAt <= limit) continue;
5798
- process.stderr.write(
5799
- `[daemon] task ${taskId} inactive > ${limit}ms \u2014 aborting
5800
- `
5801
- );
5802
- try {
5803
- entry.ctrl.abort();
5804
- } catch {
5805
- }
5806
- }
5807
- }, 6e4);
5808
- this.state = "running";
5809
- this.emit("ready");
5810
- }
5811
- async stop() {
5812
- if (this.state === "idle" || this.state === "stopping") return;
5813
- this.state = "stopping";
5814
- for (const entry of this.runningTasks.values()) {
5815
- try {
5816
- entry.ctrl.abort();
5817
- } catch {
5818
- }
5819
- }
5820
- this.runningTasks.clear();
5821
- if (this.taskReaperTimer) {
5822
- clearInterval(this.taskReaperTimer);
5823
- this.taskReaperTimer = void 0;
5824
- }
5825
- if (this.heartbeatTimer) {
5826
- clearInterval(this.heartbeatTimer);
5827
- this.heartbeatTimer = void 0;
5828
- }
5829
- this.syncWorker?.stop();
5830
- this.memoryWiring?.stop();
5831
- this.memoryWiring = void 0;
5832
- this.ws?.close();
5833
- this.outboxWatcher?.stop();
5834
- await this.servicePool?.shutdown();
5835
- await this.localServer?.stop();
5836
- try {
5837
- this.db?.close();
5838
- } catch {
5839
- }
5840
- this.state = "idle";
5841
- this.emit("stopped");
5842
- }
5843
- isRunning() {
5844
- return this.state === "running";
5845
- }
5846
- /**
5847
- * Phase 1 escape hatch — see `DispatchPayload.shellCommand`. Spawns
5848
- * `bash -c <cmd>` with cwd=/workspace, mirrors output to pod logs.
5849
- * Fire-and-forget; nothing here writes back to cloud.
5850
- */
5851
- async runShellCommand(taskId, command) {
5852
- const { spawn: spawn7 } = await import("child_process");
5853
- process.stdout.write(`[daemon] shellCommand task=${taskId} cmd=${command}
5854
- `);
5855
- const child = spawn7("bash", ["-c", command], {
5856
- cwd: "/workspace",
5857
- stdio: ["ignore", "pipe", "pipe"],
5858
- env: { ...process.env }
5859
- });
5860
- child.stdout?.on(
5861
- "data",
5862
- (d) => process.stdout.write(`[shellCommand:${taskId}] ${d.toString()}`)
5863
- );
5864
- child.stderr?.on(
5865
- "data",
5866
- (d) => process.stderr.write(`[shellCommand:${taskId}] ${d.toString()}`)
5867
- );
5868
- child.on(
5869
- "exit",
5870
- (code) => process.stdout.write(`[daemon] shellCommand task=${taskId} exit=${code}
5871
- `)
5872
- );
5873
- }
5874
- snapshotState() {
5875
- return {
5876
- daemonId: this.config?.daemon_id ?? "",
5877
- cloudBaseUrl: this.config?.cloud_api_base,
5878
- workspaceId: this.workspaceId || null,
5879
- pid: process.pid,
5880
- startedAt: this.startedAt,
5881
- wsConnected: this.wsConnected,
5882
- hostedAgents: Array.from(this.hostedAgents.values()).map((a) => ({
5883
- imUserId: a.imUserId,
5884
- name: a.name,
5885
- adapterName: a.adapterName
5886
- })),
5887
- runningTaskIds: Array.from(this.runningTasks.keys()),
5888
- observability: {
5889
- adapters: this.snapshotAdapterObservability(),
5890
- ...this.lastTaskError ? { lastTaskError: this.lastTaskError } : {}
5891
- }
5892
- };
5893
- }
5894
- /**
5895
- * Re-read the local `agents` table (populated by `prismer agent register`)
5896
- * and re-populate `hostedAgents` map. Profiles per agent come from local
5897
- * `agent_profiles` table (filled by host.acked sync).
5898
- */
5899
- loadAgentsFromDb() {
5900
- this.hostedAgents.clear();
5901
- const agents = this.db.prepare("SELECT * FROM agents").all();
5902
- const profilesByAgent = /* @__PURE__ */ new Map();
5903
- const allProfiles = this.db.prepare("SELECT id, agent_im_user_id, version FROM agent_profiles WHERE deleted_at IS NULL").all();
5904
- for (const p of allProfiles) {
5905
- const list = profilesByAgent.get(p.agent_im_user_id) ?? [];
5906
- list.push({ id: p.id, version: p.version });
5907
- profilesByAgent.set(p.agent_im_user_id, list);
5908
- }
5909
- for (const a of agents) {
5910
- let caps = [];
5911
- try {
5912
- caps = JSON.parse(a.capabilities);
5913
- } catch {
5914
- caps = [];
5915
- }
5916
- this.setHostedAgent({
5917
- imUserId: a.im_user_id,
5918
- name: a.name,
5919
- adapterName: a.adapter_name,
5920
- capabilities: caps,
5921
- profiles: profilesByAgent.get(a.im_user_id) ?? []
5922
- });
5923
- }
5924
- }
5925
- /**
5926
- * Register an in-process AgentProfile snapshot (called by agent CLI / sync layer).
5927
- * Used to populate the `agents` list in agent.host.declare.
5928
- */
5929
- setHostedAgent(agent) {
5930
- this.hostedAgents.set(agent.imUserId, {
5931
- imUserId: agent.imUserId,
5932
- name: agent.name,
5933
- adapterName: agent.adapterName,
5934
- capabilities: agent.capabilities,
5935
- profiles: new Map(agent.profiles.map((p) => [p.id, p.version]))
5936
- });
5937
- }
5938
- async installHostedAgent(payload) {
5939
- const now = Date.now();
5940
- const tx = this.db.transaction((p) => {
5941
- this.db.prepare(
5942
- `INSERT OR REPLACE INTO agents
5943
- (im_user_id, workspace_id, name, adapter_name, capabilities, status, version, synced_at, dirty)
5944
- VALUES (?, ?, ?, ?, ?, 'offline', 1, ?, 0)`
5945
- ).run(p.imUserId, p.workspaceId, p.name, p.adapterName, JSON.stringify(p.capabilities), now);
5946
- this.db.prepare(
5947
- `INSERT OR REPLACE INTO agent_profiles
5948
- (id, workspace_id, agent_im_user_id, adapter_name, name, config, version, synced_at, dirty, deleted_at)
5949
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, NULL)`
5950
- ).run(
5951
- p.profile.id,
5952
- p.workspaceId,
5953
- p.imUserId,
5954
- p.profile.adapterName,
5955
- p.profile.name,
5956
- JSON.stringify(p.profile.config ?? {}),
5957
- p.profile.version,
5958
- now
5959
- );
5960
- });
5961
- tx(payload);
5962
- this.loadAgentsFromDb();
5963
- if (this.wsConnected) this.sendDeclare();
5964
- return {
5965
- ok: true,
5966
- daemonId: this.config.daemon_id,
5967
- installedAgent: {
5968
- imUserId: payload.imUserId,
5969
- name: payload.name,
5970
- adapterName: payload.adapterName,
5971
- profileId: payload.profile.id
5972
- },
5973
- hostedAgents: this.snapshotState().hostedAgents
5974
- };
5975
- }
5976
- installStaticHostedAgentFromEnv() {
5977
- const required = truthy(process.env.PRISMER_STATIC_BINDING_REQUIRED);
5978
- const rawFile = process.env.PRISMER_HOSTED_AGENT_FILE;
5979
- const rawJson = process.env.PRISMER_HOSTED_AGENT_JSON;
5980
- let raw;
5981
- if (rawFile) {
5982
- if (!existsSync8(rawFile)) {
5983
- throw new Error(`PRISMER_HOSTED_AGENT_FILE not found: ${rawFile}`);
5984
- }
5985
- raw = readFileSync5(rawFile, "utf8");
5986
- } else if (rawJson) {
5987
- raw = rawJson;
5988
- }
5989
- if (!raw) {
5990
- if (required) {
5991
- throw new Error("static binding required, but PRISMER_HOSTED_AGENT_JSON/PRISMER_HOSTED_AGENT_FILE is missing");
5992
- }
5993
- return;
5994
- }
5995
- let payload;
5996
- try {
5997
- payload = validateStaticHostedAgent(JSON.parse(raw));
5998
- } catch (err) {
5999
- throw new Error(`invalid static hosted agent binding: ${err.message}`);
6867
+ // an actual agent process.
6868
+ attachMemory: this.memoryWiring ? attachMemoryRpc({ runtime: this.memoryWiring.runtime }) : void 0,
6869
+ attachAsset: attachAssetRpc({
6870
+ resolveIndex: (workspaceId) => this.assetMetadataIndexes.get(workspaceId)
6871
+ })
6872
+ });
6873
+ await this.localServer.start();
6000
6874
  }
6001
- const now = Date.now();
6002
- const tx = this.db.transaction((p) => {
6003
- this.db.prepare(
6004
- `INSERT OR REPLACE INTO agents
6005
- (im_user_id, workspace_id, name, adapter_name, capabilities, status, version, synced_at, dirty)
6006
- VALUES (?, ?, ?, ?, ?, 'offline', 1, ?, 0)`
6007
- ).run(p.imUserId, p.workspaceId, p.name, p.adapterName, JSON.stringify(p.capabilities), now);
6008
- this.db.prepare(
6009
- `INSERT OR REPLACE INTO agent_profiles
6010
- (id, workspace_id, agent_im_user_id, adapter_name, name, config, version, synced_at, dirty, deleted_at)
6011
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, NULL)`
6012
- ).run(
6013
- p.profile.id,
6014
- p.workspaceId,
6015
- p.imUserId,
6016
- p.profile.adapterName,
6017
- p.profile.name,
6018
- JSON.stringify(p.profile.config ?? {}),
6019
- p.profile.version,
6020
- now
6021
- );
6022
- });
6023
- tx(payload);
6024
- process.stdout.write(
6025
- `[daemon] static binding loaded agent=${payload.imUserId} profile=${payload.profile.id} adapter=${payload.adapterName}
6875
+ this.heartbeatTimer = setInterval(() => {
6876
+ if (this.wsConnected) this.sendDeclare();
6877
+ }, 3e4);
6878
+ this.taskReaperTimer = setInterval(() => {
6879
+ const now = Date.now();
6880
+ for (const [taskId, entry] of this.runningTasks) {
6881
+ const limit = Math.max(entry.timeoutMs, 5 * 6e4);
6882
+ if (now - entry.lastProgressAt <= limit) continue;
6883
+ process.stderr.write(
6884
+ `[daemon] task ${taskId} inactive > ${limit}ms \u2014 aborting
6026
6885
  `
6027
- );
6028
- }
6029
- // ───────────────────────────── internals ─────────────────────────────
6030
- wireWsHandlers() {
6031
- this.ws.on("open", () => {
6032
- this.wsConnected = true;
6033
- process.stdout.write(`[daemon] ws open, awaiting server authenticated ack
6034
- `);
6035
- });
6036
- this.ws.on("close", (code, reason) => {
6037
- this.wsConnected = false;
6038
- process.stdout.write(`[daemon] ws closed (code=${code ?? "?"}, reason=${reason ?? ""})
6039
- `);
6040
- });
6041
- this.ws.on("error", (err) => {
6042
- process.stderr.write(`[daemon] ws error: ${err.message}
6043
- `);
6044
- });
6045
- this.ws.on("auth-failed", () => {
6046
- process.stderr.write(`[daemon] ws auth-failed (close code 4001) \u2014 check API key
6047
- `);
6048
- this.emit("auth-failed");
6049
- });
6050
- this.ws.on("reconnect-scheduled", (delayMs) => {
6051
- process.stdout.write(`[daemon] ws reconnect in ${delayMs}ms
6052
- `);
6053
- });
6054
- this.ws.on("message", (msg) => {
6055
- const m = msg;
6056
- if (m?.type) process.stdout.write(`[daemon] ws msg \u2190 ${m.type}
6057
- `);
6058
- if (m?.type === "error") {
6059
- process.stderr.write(`[daemon] ws error payload: ${JSON.stringify(msg)}
6060
- `);
6061
- const code = m.payload?.code;
6062
- if (code === "AUTH_FAILED" || code === "AUTH_REQUIRED" || code === "auth_invalid") {
6063
- process.stderr.write(`[daemon] application auth failure (${code}) \u2014 stopping daemon
6064
- `);
6065
- this.emit("auth-failed");
6886
+ );
6887
+ try {
6888
+ entry.ctrl.abort();
6889
+ } catch {
6066
6890
  }
6067
6891
  }
6068
- if (m?.type === "authenticated") {
6069
- process.stdout.write(`[daemon] ws authenticated, sending agent.host.declare (${this.hostedAgents.size} agents)
6070
- `);
6071
- this.sendDeclare();
6072
- return;
6073
- }
6074
- this.handleIncoming(msg);
6075
- });
6076
- }
6077
- sendDeclare() {
6078
- const payload = {
6079
- daemonId: this.config.daemon_id,
6080
- daemonVersion: this.opts.daemonVersion ?? "0.0.0",
6081
- platform: platform() === "win32" ? "win32" : platform() === "linux" ? "linux" : "darwin",
6082
- agents: Array.from(this.hostedAgents.values()).map((a) => ({
6083
- imUserId: a.imUserId,
6084
- name: a.name,
6085
- adapterName: a.adapterName,
6086
- capabilities: a.capabilities,
6087
- profiles: Array.from(a.profiles.entries()).map(([id, version]) => ({ id, version }))
6088
- }))
6089
- };
6090
- this.ws.send(envelope("agent.host.declare", payload, this.config.daemon_id));
6091
- }
6092
- handleIncoming(msg) {
6093
- switch (msg.type) {
6094
- case "host.acked":
6095
- void this.onHostAcked(msg.payload);
6096
- return;
6097
- case "task.dispatch.request":
6098
- void this.onTaskDispatch(msg.payload, msg.requestId);
6099
- return;
6100
- case "task.cancel":
6101
- this.onTaskCancel(msg.payload);
6102
- return;
6103
- case "agent.changed":
6104
- this.onAgentChanged(msg.payload);
6105
- return;
6106
- case "agent_profile.changed":
6107
- void this.onAgentProfileChanged(msg.payload);
6108
- return;
6109
- case "workspace.changed":
6110
- void this.onWorkspaceChanged(msg.payload);
6111
- return;
6112
- case "workspace_file.changed":
6113
- this.onWorkspaceFileChanged(msg.payload);
6114
- return;
6115
- default:
6116
- this.emit("unknown-message", msg);
6117
- }
6892
+ }, 6e4);
6893
+ this.state = "running";
6894
+ this.emit("ready");
6118
6895
  }
6119
- async onHostAcked(payload) {
6120
- this.workspaceId = payload.workspaceId;
6121
- for (const id of payload.profilesToSync) {
6896
+ async stop() {
6897
+ if (this.state === "idle" || this.state === "stopping") return;
6898
+ this.state = "stopping";
6899
+ for (const entry of this.runningTasks.values()) {
6122
6900
  try {
6123
- await this.syncProfileFromCloud(id);
6124
- } catch (err) {
6125
- this.emit("sync-error", err);
6901
+ entry.ctrl.abort();
6902
+ } catch {
6126
6903
  }
6127
6904
  }
6128
- if (payload.profilesToSync.length > 0 && this.wsConnected) this.sendDeclare();
6129
- this.emit("host-acked", payload);
6130
- }
6131
- async onTaskDispatch(payload, requestId) {
6132
- const targetDaemonId = readTargetDaemonId(payload);
6133
- if (targetDaemonId && targetDaemonId !== this.config.daemon_id) {
6134
- process.stdout.write(
6135
- `[daemon] dispatch skip task=${payload.taskId} targetDaemonId=${targetDaemonId} local=${this.config.daemon_id}
6136
- `
6137
- );
6138
- return;
6905
+ this.runningTasks.clear();
6906
+ if (this.taskReaperTimer) {
6907
+ clearInterval(this.taskReaperTimer);
6908
+ this.taskReaperTimer = void 0;
6139
6909
  }
6140
- if (this.runningTasks.has(payload.taskId)) {
6141
- process.stdout.write(`[daemon] dispatch dup task=${payload.taskId} (already in-flight, skipping)
6142
- `);
6143
- return;
6910
+ if (this.heartbeatTimer) {
6911
+ clearInterval(this.heartbeatTimer);
6912
+ this.heartbeatTimer = void 0;
6144
6913
  }
6145
- process.stdout.write(
6146
- `[daemon] dispatch start task=${payload.taskId} route=${payload.runtimeRoute ?? "agent"} agent=${payload.agentImUserId ?? "-"} daemon=${payload.targetDaemonId ?? "-"}
6147
- `
6148
- );
6149
- const ctrl = new AbortController();
6150
- this.runningTasks.set(payload.taskId, {
6151
- ctrl,
6152
- startedAt: Date.now(),
6153
- lastProgressAt: Date.now(),
6154
- timeoutMs: typeof payload.timeoutMs === "number" ? payload.timeoutMs : 0
6155
- });
6914
+ this.syncWorker?.stop();
6915
+ this.memoryWiring?.stop();
6916
+ this.memoryWiring = void 0;
6917
+ this.ws?.close();
6918
+ this.outboxWatcher?.stop();
6919
+ await this.servicePool?.shutdown();
6920
+ await this.localServer?.stop();
6156
6921
  try {
6157
- if (isShellDispatch(payload)) {
6158
- const reply = await executeShellDispatch(payload, {
6159
- config: this.shellConfig,
6160
- workspaceId: this.workspaceId,
6161
- signal: ctrl.signal,
6162
- onProgress: (progressPayload) => {
6163
- const running = this.runningTasks.get(payload.taskId);
6164
- if (running) running.lastProgressAt = Date.now();
6165
- this.ws.send(envelope("task.dispatch.progress", progressPayload));
6166
- }
6167
- });
6168
- this.ws.send(envelope("task.dispatch.reply", reply, requestId));
6169
- } else {
6170
- await handleDispatch(payload, requestId, {
6171
- registry: this.registry,
6172
- cloud: this.cloud,
6173
- uriResolver: this.uriResolver,
6174
- assetCache: this.assetCache,
6175
- ws: this.ws,
6176
- outboxWatcher: this.outboxWatcher,
6177
- paths: this.paths,
6178
- signal: ctrl.signal,
6179
- ensureService: (profile, adapter) => this.servicePool.ensureService(profile, adapter),
6180
- onProgress: () => {
6181
- const running = this.runningTasks.get(payload.taskId);
6182
- if (running) running.lastProgressAt = Date.now();
6183
- }
6184
- });
6185
- }
6186
- process.stdout.write(`[daemon] dispatch done task=${payload.taskId}
6187
- `);
6188
- } catch (err) {
6189
- this.lastTaskError = {
6190
- taskId: payload.taskId,
6191
- message: err.message,
6192
- at: (/* @__PURE__ */ new Date()).toISOString()
6193
- };
6194
- process.stderr.write(`[daemon] dispatch threw task=${payload.taskId}: ${err.stack ?? err.message}
6195
- `);
6196
- } finally {
6197
- this.runningTasks.delete(payload.taskId);
6922
+ this.db?.close();
6923
+ } catch {
6198
6924
  }
6925
+ this.state = "idle";
6926
+ this.emit("stopped");
6199
6927
  }
6200
- onTaskCancel(payload) {
6201
- const entry = this.runningTasks.get(payload.taskId);
6202
- if (entry) entry.ctrl.abort();
6928
+ isRunning() {
6929
+ return this.state === "running";
6930
+ }
6931
+ /**
6932
+ * Phase 1 escape hatch — see `DispatchPayload.shellCommand`. Spawns
6933
+ * `bash -c <cmd>` with cwd=/workspace, mirrors output to pod logs.
6934
+ * Fire-and-forget; nothing here writes back to cloud.
6935
+ */
6936
+ async runShellCommand(taskId, command) {
6937
+ const { spawn: spawn7 } = await import("child_process");
6938
+ process.stdout.write(`[daemon] shellCommand task=${taskId} cmd=${command}
6939
+ `);
6940
+ const child = spawn7("bash", ["-c", command], {
6941
+ cwd: "/workspace",
6942
+ stdio: ["ignore", "pipe", "pipe"],
6943
+ env: { ...process.env }
6944
+ });
6945
+ child.stdout?.on(
6946
+ "data",
6947
+ (d) => process.stdout.write(`[shellCommand:${taskId}] ${d.toString()}`)
6948
+ );
6949
+ child.stderr?.on(
6950
+ "data",
6951
+ (d) => process.stderr.write(`[shellCommand:${taskId}] ${d.toString()}`)
6952
+ );
6953
+ child.on(
6954
+ "exit",
6955
+ (code) => process.stdout.write(`[daemon] shellCommand task=${taskId} exit=${code}
6956
+ `)
6957
+ );
6203
6958
  }
6204
- onAgentChanged(payload) {
6205
- const a = this.hostedAgents.get(payload.agentImUserId);
6206
- if (!a) return;
6207
- if (typeof payload.fields.displayName === "string") a.name = payload.fields.displayName;
6208
- if (Array.isArray(payload.fields.capabilities)) a.capabilities = payload.fields.capabilities;
6959
+ snapshotState() {
6960
+ return {
6961
+ daemonId: this.config?.daemon_id ?? "",
6962
+ cloudBaseUrl: this.config?.cloud_api_base,
6963
+ workspaceId: this.workspaceId || null,
6964
+ pid: process.pid,
6965
+ startedAt: this.startedAt,
6966
+ wsConnected: this.wsConnected,
6967
+ hostedAgents: Array.from(this.hostedAgents.values()).map((a) => ({
6968
+ imUserId: a.imUserId,
6969
+ name: a.name,
6970
+ adapterName: a.adapterName
6971
+ })),
6972
+ runningTaskIds: Array.from(this.runningTasks.keys()),
6973
+ observability: {
6974
+ adapters: this.snapshotAdapterObservability(),
6975
+ ...this.lastTaskError ? { lastTaskError: this.lastTaskError } : {}
6976
+ }
6977
+ };
6209
6978
  }
6210
- async onAgentProfileChanged(payload) {
6211
- try {
6212
- await this.syncProfileFromCloud(payload.profileId);
6213
- if (this.wsConnected) this.sendDeclare();
6214
- } catch (err) {
6215
- this.emit("sync-error", err);
6979
+ /**
6980
+ * Re-read the local `agents` table (populated by `prismer agent register`)
6981
+ * and re-populate `hostedAgents` map. Profiles per agent come from local
6982
+ * `agent_profiles` table (filled by host.acked sync).
6983
+ */
6984
+ loadAgentsFromDb() {
6985
+ this.hostedAgents.clear();
6986
+ const agents = this.db.prepare("SELECT * FROM agents").all();
6987
+ const profilesByAgent = /* @__PURE__ */ new Map();
6988
+ const allProfiles = this.db.prepare("SELECT id, agent_im_user_id, version FROM agent_profiles WHERE deleted_at IS NULL").all();
6989
+ for (const p of allProfiles) {
6990
+ const list = profilesByAgent.get(p.agent_im_user_id) ?? [];
6991
+ list.push({ id: p.id, version: p.version });
6992
+ profilesByAgent.set(p.agent_im_user_id, list);
6993
+ }
6994
+ for (const a of agents) {
6995
+ let caps = [];
6996
+ try {
6997
+ caps = JSON.parse(a.capabilities);
6998
+ } catch {
6999
+ caps = [];
7000
+ }
7001
+ this.setHostedAgent({
7002
+ imUserId: a.im_user_id,
7003
+ name: a.name,
7004
+ adapterName: a.adapter_name,
7005
+ capabilities: caps,
7006
+ profiles: profilesByAgent.get(a.im_user_id) ?? []
7007
+ });
6216
7008
  }
6217
7009
  }
6218
- async syncProfileFromCloud(profileId) {
6219
- const profile = await this.cloud.get(`/api/im/agent_profiles/${encodeURIComponent(profileId)}`);
6220
- const agent = await this.resolveOwnedAgent(profile.agentImUserId);
6221
- const adapter = this.registry.get(profile.adapterName);
6222
- const capabilities = agent?.card?.capabilities?.length ? agent.card.capabilities : adapter?.capabilities ?? [];
6223
- const name = agent?.card?.name || agent?.displayName || agent?.username || profile.agentImUserId;
7010
+ /**
7011
+ * Register an in-process AgentProfile snapshot (called by agent CLI / sync layer).
7012
+ * Used to populate the `agents` list in agent.host.declare.
7013
+ */
7014
+ setHostedAgent(agent) {
7015
+ this.hostedAgents.set(agent.imUserId, {
7016
+ imUserId: agent.imUserId,
7017
+ name: agent.name,
7018
+ adapterName: agent.adapterName,
7019
+ capabilities: agent.capabilities,
7020
+ profiles: new Map(agent.profiles.map((p) => [p.id, p.version]))
7021
+ });
7022
+ }
7023
+ async installHostedAgent(payload) {
6224
7024
  const now = Date.now();
6225
- const tx = this.db.transaction(() => {
7025
+ const tx = this.db.transaction((p) => {
6226
7026
  this.db.prepare(
6227
7027
  `INSERT OR REPLACE INTO agents
6228
7028
  (im_user_id, workspace_id, name, adapter_name, capabilities, status, version, synced_at, dirty)
6229
7029
  VALUES (?, ?, ?, ?, ?, 'offline', 1, ?, 0)`
6230
- ).run(
6231
- profile.agentImUserId,
6232
- profile.workspaceId,
6233
- name,
6234
- profile.adapterName,
6235
- JSON.stringify(capabilities),
6236
- now
6237
- );
7030
+ ).run(p.imUserId, p.workspaceId, p.name, p.adapterName, JSON.stringify(p.capabilities), now);
6238
7031
  this.db.prepare(
6239
7032
  `INSERT OR REPLACE INTO agent_profiles
6240
7033
  (id, workspace_id, agent_im_user_id, adapter_name, name, config, version, synced_at, dirty, deleted_at)
6241
7034
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, NULL)`
6242
7035
  ).run(
6243
- profile.id,
6244
- profile.workspaceId,
6245
- profile.agentImUserId,
6246
- profile.adapterName,
6247
- profile.name,
6248
- JSON.stringify(profile.config ?? {}),
6249
- profile.version,
7036
+ p.profile.id,
7037
+ p.workspaceId,
7038
+ p.imUserId,
7039
+ p.profile.adapterName,
7040
+ p.profile.name,
7041
+ JSON.stringify(p.profile.config ?? {}),
7042
+ p.profile.version,
6250
7043
  now
6251
7044
  );
6252
7045
  });
6253
- tx();
7046
+ tx(payload);
6254
7047
  this.loadAgentsFromDb();
6255
- process.stdout.write(
6256
- `[daemon] profile synced agent=${profile.agentImUserId} profile=${profile.id} adapter=${profile.adapterName}
6257
- `
6258
- );
6259
- }
6260
- async resolveOwnedAgent(agentImUserId) {
6261
- try {
6262
- const agents = await this.cloud.get("/api/im/me/agents");
6263
- return agents.find((agent) => agent.id === agentImUserId) ?? null;
6264
- } catch (err) {
6265
- process.stderr.write(
6266
- `[daemon] owned agent lookup skipped agent=${agentImUserId}: ${err.message}
6267
- `
6268
- );
6269
- return null;
6270
- }
6271
- }
6272
- async onWorkspaceChanged(payload) {
6273
- if (payload.workspaceId !== this.workspaceId) return;
6274
- try {
6275
- await this.cloud.get(`/api/im/workspaces/${encodeURIComponent(payload.workspaceId)}`);
6276
- } catch (err) {
6277
- this.emit("sync-error", err);
6278
- }
6279
- }
6280
- onWorkspaceFileChanged(payload) {
6281
- if (payload.operation === "delete") {
6282
- this.db.prepare("DELETE FROM workspace_files_mirror WHERE workspace_id = ? AND path = ?").run(payload.workspaceId, payload.path);
6283
- return;
6284
- }
6285
- if (payload.assetId && payload.contentHash) {
6286
- this.db.prepare(
6287
- `INSERT OR REPLACE INTO workspace_files_mirror
6288
- (workspace_id, path, asset_id, content_hash, version, synced_at, dirty)
6289
- VALUES (?, ?, ?, ?, ?, ?, 0)`
6290
- ).run(
6291
- payload.workspaceId,
6292
- payload.path,
6293
- payload.assetId,
6294
- payload.contentHash,
6295
- payload.version,
6296
- Date.now()
6297
- );
6298
- }
6299
- }
6300
- snapshotAdapterObservability() {
6301
- const agents = Array.from(this.hostedAgents.values());
6302
- const counts = agents.reduce((acc, agent) => {
6303
- acc[agent.adapterName] = (acc[agent.adapterName] ?? 0) + 1;
6304
- return acc;
6305
- }, {});
7048
+ if (this.wsConnected) this.sendDeclare();
6306
7049
  return {
6307
- hostedCounts: counts,
6308
- servicePoolSize: this.servicePool?.size() ?? 0,
6309
- hermes: {
6310
- hostedAgents: counts.hermes ?? 0,
6311
- runningTaskIds: Array.from(this.runningTasks.keys())
6312
- }
6313
- };
6314
- }
6315
- /**
6316
- * SyncWorker FlushFn — pushes local writes to cloud.
6317
- *
6318
- * Maps:
6319
- * workspace → PATCH /api/im/workspaces/:id
6320
- * agent_profile → PATCH /api/im/agent_profiles/:id
6321
- * agent → PATCH /api/im/agents/:imUserId
6322
- * On 'create' we POST instead. On 'delete' we DELETE.
6323
- */
6324
- /**
6325
- * SyncWorker FlushFn pushes one local sync row to cloud via CloudClient.
6326
- *
6327
- * resource_type × operation → endpoint:
6328
- * workspace.create → POST /api/im/workspaces
6329
- * workspace.update PATCH /api/im/workspaces/:id
6330
- * workspace.delete → DELETE /api/im/workspaces/:id
6331
- * agent.create → POST /api/im/register (the only public path
6332
- * that creates an
6333
- * IMUser of role='agent')
6334
- * agent.update → PATCH /api/im/agents/:id
6335
- * agent.delete → DELETE /api/im/agents/:id
6336
- * agent_profile.create → POST /api/im/agent_profiles
6337
- * agent_profile.update → PATCH /api/im/agent_profiles/:id
6338
- * agent_profile.delete → DELETE /api/im/agent_profiles/:id
6339
- *
6340
- * Error classification per docs/refactor/13-error-handling.md §2.1 / §2.7:
6341
- * 2xx → ok:true (SyncWorker drops the row)
6342
- * 408 / 429 → retryable (SyncWorker re-queues with exponential backoff)
6343
- * 5xx / net err → retryable
6344
- * 4xx (other) → permanent (SyncWorker marks failed; 409 is conflict)
6345
- */
6346
- async flushSyncRow(row) {
6347
- const op = `${row.resource_type}.${row.operation}`;
6348
- const id = encodeURIComponent(row.resource_id);
6349
- const body = row.operation === "delete" ? void 0 : safeJsonParse(row.payload);
6350
- let method;
6351
- let path7;
6352
- switch (op) {
6353
- case "workspace.create":
6354
- method = "POST";
6355
- path7 = "/api/im/workspaces";
6356
- break;
6357
- case "workspace.update":
6358
- method = "PATCH";
6359
- path7 = `/api/im/workspaces/${id}`;
6360
- break;
6361
- case "workspace.delete":
6362
- method = "DELETE";
6363
- path7 = `/api/im/workspaces/${id}`;
6364
- break;
6365
- // agent.create must use /register — POST /api/im/agents is not exposed.
6366
- case "agent.create":
6367
- method = "POST";
6368
- path7 = "/api/im/register";
6369
- break;
6370
- case "agent.update":
6371
- method = "PATCH";
6372
- path7 = `/api/im/agents/${id}`;
6373
- break;
6374
- case "agent.delete":
6375
- method = "DELETE";
6376
- path7 = `/api/im/agents/${id}`;
6377
- break;
6378
- case "agent_profile.create":
6379
- method = "POST";
6380
- path7 = "/api/im/agent_profiles";
6381
- break;
6382
- case "agent_profile.update":
6383
- method = "PATCH";
6384
- path7 = `/api/im/agent_profiles/${id}`;
6385
- break;
6386
- case "agent_profile.delete":
6387
- method = "DELETE";
6388
- path7 = `/api/im/agent_profiles/${id}`;
6389
- break;
6390
- default:
6391
- process.stderr.write(`[daemon] sync flush op=${op} id=${row.id} result=drop (unknown op)
6392
- `);
6393
- return { ok: false, status: 400, message: `Unknown sync op: ${op}` };
7050
+ ok: true,
7051
+ daemonId: this.config.daemon_id,
7052
+ installedAgent: {
7053
+ imUserId: payload.imUserId,
7054
+ name: payload.name,
7055
+ adapterName: payload.adapterName,
7056
+ profileId: payload.profile.id
7057
+ },
7058
+ hostedAgents: this.snapshotState().hostedAgents
7059
+ };
7060
+ }
7061
+ installStaticHostedAgentFromEnv() {
7062
+ const required = truthy(process.env.PRISMER_STATIC_BINDING_REQUIRED);
7063
+ const rawFile = process.env.PRISMER_HOSTED_AGENT_FILE;
7064
+ const rawJson = process.env.PRISMER_HOSTED_AGENT_JSON;
7065
+ let raw;
7066
+ if (rawFile) {
7067
+ if (!existsSync9(rawFile)) {
7068
+ throw new Error(`PRISMER_HOSTED_AGENT_FILE not found: ${rawFile}`);
7069
+ }
7070
+ raw = readFileSync6(rawFile, "utf8");
7071
+ } else if (rawJson) {
7072
+ raw = rawJson;
6394
7073
  }
6395
- let res;
7074
+ if (!raw) {
7075
+ if (required) {
7076
+ throw new Error("static binding required, but PRISMER_HOSTED_AGENT_JSON/PRISMER_HOSTED_AGENT_FILE is missing");
7077
+ }
7078
+ return;
7079
+ }
7080
+ let payload;
6396
7081
  try {
6397
- res = await this.cloud.request(method, path7, { body });
7082
+ payload = validateStaticHostedAgent(JSON.parse(raw));
6398
7083
  } catch (err) {
6399
- process.stderr.write(
6400
- `[daemon] sync flush op=${op} id=${row.id} result=retry (threw: ${err.message})
6401
- `
6402
- );
6403
- return { ok: false, status: 0, message: err.message };
6404
- }
6405
- let label;
6406
- if (res.ok) {
6407
- label = "ok";
6408
- } else if (res.status === 0 || res.status === 408 || res.status === 429 || res.status >= 500) {
6409
- label = "retry";
6410
- } else {
6411
- label = "drop";
7084
+ throw new Error(`invalid static hosted agent binding: ${err.message}`);
6412
7085
  }
7086
+ const now = Date.now();
7087
+ const tx = this.db.transaction((p) => {
7088
+ this.db.prepare(
7089
+ `INSERT OR REPLACE INTO agents
7090
+ (im_user_id, workspace_id, name, adapter_name, capabilities, status, version, synced_at, dirty)
7091
+ VALUES (?, ?, ?, ?, ?, 'offline', 1, ?, 0)`
7092
+ ).run(p.imUserId, p.workspaceId, p.name, p.adapterName, JSON.stringify(p.capabilities), now);
7093
+ this.db.prepare(
7094
+ `INSERT OR REPLACE INTO agent_profiles
7095
+ (id, workspace_id, agent_im_user_id, adapter_name, name, config, version, synced_at, dirty, deleted_at)
7096
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, NULL)`
7097
+ ).run(
7098
+ p.profile.id,
7099
+ p.workspaceId,
7100
+ p.imUserId,
7101
+ p.profile.adapterName,
7102
+ p.profile.name,
7103
+ JSON.stringify(p.profile.config ?? {}),
7104
+ p.profile.version,
7105
+ now
7106
+ );
7107
+ });
7108
+ tx(payload);
6413
7109
  process.stdout.write(
6414
- `[daemon] sync flush op=${op} id=${row.id} result=${label}${res.ok ? "" : ` status=${res.status}`}
7110
+ `[daemon] static binding loaded agent=${payload.imUserId} profile=${payload.profile.id} adapter=${payload.adapterName}
6415
7111
  `
6416
7112
  );
6417
- return { ok: res.ok, status: res.status, message: res.error?.message };
6418
- }
6419
- };
6420
- function readTargetDaemonId(payload) {
6421
- if (typeof payload.targetDaemonId === "string" && payload.targetDaemonId.length > 0) {
6422
- return payload.targetDaemonId;
6423
- }
6424
- const execution = payload.metadata?.execution;
6425
- if (execution && typeof execution === "object" && !Array.isArray(execution)) {
6426
- const value = execution.targetDaemonId;
6427
- return typeof value === "string" && value.length > 0 ? value : null;
6428
- }
6429
- return null;
6430
- }
6431
- function truthy(raw) {
6432
- return raw === "1" || raw === "true" || raw === "yes";
6433
- }
6434
- function validateStaticHostedAgent(raw) {
6435
- if (!raw || typeof raw !== "object") throw new Error("binding must be a JSON object");
6436
- const obj = raw;
6437
- const profile = obj.profile;
6438
- const capabilities = obj.capabilities;
6439
- if (typeof obj.workspaceId !== "string" || obj.workspaceId.length === 0) throw new Error("workspaceId is required");
6440
- if (typeof obj.imUserId !== "string" || obj.imUserId.length === 0) throw new Error("imUserId is required");
6441
- if (typeof obj.name !== "string" || obj.name.length === 0) throw new Error("name is required");
6442
- if (typeof obj.adapterName !== "string" || obj.adapterName.length === 0) throw new Error("adapterName is required");
6443
- if (!Array.isArray(capabilities) || capabilities.some((v) => typeof v !== "string")) {
6444
- throw new Error("capabilities must be a string array");
6445
- }
6446
- if (!profile || typeof profile !== "object") throw new Error("profile is required");
6447
- if (typeof profile.id !== "string" || profile.id.length === 0) throw new Error("profile.id is required");
6448
- if (typeof profile.name !== "string" || profile.name.length === 0) throw new Error("profile.name is required");
6449
- if (typeof profile.adapterName !== "string" || profile.adapterName.length === 0) {
6450
- throw new Error("profile.adapterName is required");
6451
- }
6452
- if (profile.config !== void 0 && (!profile.config || typeof profile.config !== "object" || Array.isArray(profile.config))) {
6453
- throw new Error("profile.config must be a JSON object");
6454
- }
6455
- return {
6456
- workspaceId: obj.workspaceId,
6457
- imUserId: obj.imUserId,
6458
- name: obj.name,
6459
- adapterName: obj.adapterName,
6460
- capabilities,
6461
- profile: {
6462
- id: profile.id,
6463
- name: profile.name,
6464
- adapterName: profile.adapterName,
6465
- config: profile.config ?? {},
6466
- version: typeof profile.version === "number" && Number.isFinite(profile.version) ? profile.version : 1
6467
- }
6468
- };
6469
- }
6470
- function safeJsonParse(raw) {
6471
- try {
6472
- return JSON.parse(raw);
6473
- } catch {
6474
- return raw;
6475
- }
6476
- }
6477
-
6478
- // src/pair.ts
6479
- import { generateKeyPairSync } from "crypto";
6480
- import { hostname } from "os";
6481
- import { setTimeout as sleep } from "timers/promises";
6482
- import qrcode from "qrcode";
6483
- async function pair(opts) {
6484
- const paths = opts.paths ?? resolvePaths();
6485
- if (configExists(paths) && !opts.force) {
6486
- throw new Error(
6487
- `Config already exists at ${paths.configFile}. Pass --force to overwrite, or run \`prismer status\` to inspect.`
6488
- );
6489
- }
6490
- const isLocalOnly = opts.isLocalOnly ?? (() => process.env.LOCAL_ONLY === "1");
6491
- const localOnlyMode = !!opts.asUserEmail;
6492
- if (localOnlyMode && !isLocalOnly()) {
6493
- throw new Error(
6494
- "pair: --as-user requires LOCAL_ONLY=1. Without that gate, this would skip mobile approval and silently mint a key for the named user."
6495
- );
6496
7113
  }
6497
- const { publicKey } = generateKeyPairSync("ed25519");
6498
- const devicePub = publicKey.export({ format: "der", type: "spki" }).toString("base64");
6499
- const cloud = new CloudClient({
6500
- baseUrl: opts.cloudBaseUrl,
6501
- apiKey: "pending",
6502
- // not used: we pass auth:false
6503
- fetchImpl: opts.fetchImpl
6504
- });
6505
- const offerRes = await cloud.request(
6506
- "POST",
6507
- "/api/im/pair/offer",
6508
- {
6509
- auth: false,
6510
- body: { devicePub, deviceName: opts.deviceName ?? hostname() }
6511
- }
6512
- );
6513
- if (!offerRes.ok) {
6514
- throw new Error(`pair: offer failed (${offerRes.status}): ${offerRes.error?.message}`);
7114
+ // ───────────────────────────── internals ─────────────────────────────
7115
+ wireWsHandlers() {
7116
+ this.ws.on("open", () => {
7117
+ this.wsConnected = true;
7118
+ process.stdout.write(`[daemon] ws open, awaiting server authenticated ack
7119
+ `);
7120
+ });
7121
+ this.ws.on("close", (code, reason) => {
7122
+ this.wsConnected = false;
7123
+ process.stdout.write(`[daemon] ws closed (code=${code ?? "?"}, reason=${reason ?? ""})
7124
+ `);
7125
+ });
7126
+ this.ws.on("error", (err) => {
7127
+ process.stderr.write(`[daemon] ws error: ${err.message}
7128
+ `);
7129
+ });
7130
+ this.ws.on("auth-failed", () => {
7131
+ process.stderr.write(`[daemon] ws auth-failed (close code 4001) \u2014 check API key
7132
+ `);
7133
+ this.emit("auth-failed");
7134
+ });
7135
+ this.ws.on("reconnect-scheduled", (delayMs) => {
7136
+ process.stdout.write(`[daemon] ws reconnect in ${delayMs}ms
7137
+ `);
7138
+ });
7139
+ this.ws.on("message", (msg) => {
7140
+ const m = msg;
7141
+ if (m?.type) process.stdout.write(`[daemon] ws msg \u2190 ${m.type}
7142
+ `);
7143
+ if (m?.type === "error") {
7144
+ process.stderr.write(`[daemon] ws error payload: ${JSON.stringify(msg)}
7145
+ `);
7146
+ const code = m.payload?.code;
7147
+ if (code === "AUTH_FAILED" || code === "AUTH_REQUIRED" || code === "auth_invalid") {
7148
+ process.stderr.write(`[daemon] application auth failure (${code}) \u2014 stopping daemon
7149
+ `);
7150
+ this.emit("auth-failed");
7151
+ }
7152
+ }
7153
+ if (m?.type === "authenticated") {
7154
+ process.stdout.write(`[daemon] ws authenticated, sending agent.host.declare (${this.hostedAgents.size} agents)
7155
+ `);
7156
+ this.sendDeclare();
7157
+ return;
7158
+ }
7159
+ this.handleIncoming(msg);
7160
+ });
6515
7161
  }
6516
- const offer = unwrapEnvelope(offerRes.data);
6517
- if (!offer.nonce || !offer.qrUrl) {
6518
- throw new Error("pair: cloud returned no nonce/qrUrl");
7162
+ sendDeclare() {
7163
+ const payload = {
7164
+ daemonId: this.config.daemon_id,
7165
+ daemonVersion: this.opts.daemonVersion ?? "0.0.0",
7166
+ platform: platform() === "win32" ? "win32" : platform() === "linux" ? "linux" : "darwin",
7167
+ agents: Array.from(this.hostedAgents.values()).map((a) => ({
7168
+ imUserId: a.imUserId,
7169
+ name: a.name,
7170
+ adapterName: a.adapterName,
7171
+ capabilities: a.capabilities,
7172
+ profiles: Array.from(a.profiles.entries()).map(([id, version]) => ({ id, version }))
7173
+ }))
7174
+ };
7175
+ this.ws.send(envelope("agent.host.declare", payload, this.config.daemon_id));
6519
7176
  }
6520
- if (localOnlyMode) {
6521
- const approveRes = await cloud.request(
6522
- "POST",
6523
- "/api/im/pair/local-only-approve",
6524
- {
6525
- auth: false,
6526
- body: { nonce: offer.nonce, asUserEmail: opts.asUserEmail }
6527
- }
6528
- );
6529
- if (!approveRes.ok) {
6530
- throw new Error(
6531
- `pair: local-only-approve failed (${approveRes.status}): ${approveRes.error?.message ?? "unknown"}`
6532
- );
7177
+ handleIncoming(msg) {
7178
+ switch (msg.type) {
7179
+ case "host.acked":
7180
+ void this.onHostAcked(msg.payload);
7181
+ return;
7182
+ case "task.dispatch.request":
7183
+ void this.onTaskDispatch(msg.payload, msg.requestId);
7184
+ return;
7185
+ case "task.cancel":
7186
+ this.onTaskCancel(msg.payload);
7187
+ return;
7188
+ case "agent.changed":
7189
+ this.onAgentChanged(msg.payload);
7190
+ return;
7191
+ case "agent_profile.changed":
7192
+ void this.onAgentProfileChanged(msg.payload);
7193
+ return;
7194
+ case "workspace.changed":
7195
+ void this.onWorkspaceChanged(msg.payload);
7196
+ return;
7197
+ case "workspace_file.changed":
7198
+ this.onWorkspaceFileChanged(msg.payload);
7199
+ return;
7200
+ case "asset.changed":
7201
+ void this.onAssetChanged(msg.payload);
7202
+ return;
7203
+ default:
7204
+ this.emit("unknown-message", msg);
6533
7205
  }
6534
- process.stdout.write(`[pair] LOCAL_ONLY approved as ${opts.asUserEmail} \u2014 no QR shown
6535
- `);
6536
- } else {
6537
- const qrAscii = await qrcode.toString(offer.qrUrl, { type: "terminal", small: true });
6538
- process.stdout.write(qrAscii);
6539
- process.stdout.write(`
6540
- Scan with Lumin to approve, or open: ${offer.qrUrl}
6541
-
6542
- `);
6543
- opts.onQrReady?.(offer.qrUrl);
6544
7206
  }
6545
- const pollPath = `/api/im/pair/poll/${encodeURIComponent(offer.nonce)}?devicePub=${encodeURIComponent(devicePub)}`;
6546
- const maxAttempts = opts.maxPollAttempts ?? 60;
6547
- const pollIntervalMs = opts.pollIntervalMs ?? 5e3;
6548
- for (let i = 0; i < maxAttempts; i += 1) {
6549
- if (i > 0 || !localOnlyMode) {
6550
- await sleep(pollIntervalMs);
7207
+ async onHostAcked(payload) {
7208
+ this.workspaceId = payload.workspaceId;
7209
+ if (this.memoryWiring && this.workspaceId) {
7210
+ syncMemoryFromCloud(this.memoryWiring, this.cloud, [this.workspaceId]).catch(
7211
+ (err) => console.error("[Daemon] Initial memory sync failed:", err.message)
7212
+ );
6551
7213
  }
6552
- const res = await cloud.request(
6553
- "GET",
6554
- pollPath,
6555
- { auth: false }
7214
+ this.syncAssetMetadata(this.workspaceId).catch(
7215
+ (err) => console.error("[Daemon] Asset metadata sync failed:", err.message)
6556
7216
  );
6557
- if (res.ok) {
6558
- const body = unwrapEnvelope(res.data);
6559
- if (body.apiKey) {
6560
- const config = {
6561
- api_key: body.apiKey,
6562
- cloud_api_base: opts.cloudBaseUrl,
6563
- daemon_id: newDaemonId()
6564
- };
6565
- saveConfig(config, paths);
6566
- return { config, paths };
7217
+ for (const id of payload.profilesToSync) {
7218
+ try {
7219
+ await this.syncProfileFromCloud(id);
7220
+ } catch (err) {
7221
+ this.emit("sync-error", err);
6567
7222
  }
6568
7223
  }
6569
- if (res.status === 404 || res.status === 202) {
6570
- continue;
6571
- }
6572
- if (res.status >= 400 && res.status !== 404) {
6573
- throw new Error(`pair: poll failed (${res.status}): ${res.error?.message}`);
6574
- }
6575
- }
6576
- throw new Error("pair: timed out waiting for approval (5 min)");
6577
- }
6578
- function unwrapEnvelope(raw) {
6579
- if (raw && typeof raw === "object" && "ok" in raw) {
6580
- const env = raw;
6581
- if (env.ok && env.data) return env.data;
6582
- }
6583
- return raw;
6584
- }
6585
-
6586
- // src/cli/index.ts
6587
- import { Command as Command18 } from "commander";
6588
-
6589
- // src/cli/commands/adapter.ts
6590
- import { spawnSync as spawnSync2 } from "child_process";
6591
- import {
6592
- copyFileSync,
6593
- existsSync as existsSync11,
6594
- mkdirSync as mkdirSync7,
6595
- readFileSync as readFileSync8,
6596
- statSync as statSync2,
6597
- writeFileSync as writeFileSync6
6598
- } from "fs";
6599
- import { homedir as homedir4 } from "os";
6600
- import { dirname as dirname7, join as join10 } from "path";
6601
- import { Command } from "commander";
6602
-
6603
- // src/cli/util.ts
6604
- import { existsSync as existsSync10, readFileSync as readFileSync7, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
6605
- import { join as join9 } from "path";
6606
-
6607
- // src/cli/ui.ts
6608
- import * as fs4 from "fs";
6609
- import * as path6 from "path";
6610
- import { fileURLToPath } from "url";
6611
- var BRAILLE_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
6612
- var COMPACT_BANNER = ["\u25C7 PRISMER", " Runtime CLI"];
6613
- function thisDirname() {
6614
- try {
6615
- return path6.dirname(fileURLToPath(import.meta.url));
6616
- } catch {
6617
- return process.cwd();
7224
+ if (payload.profilesToSync.length > 0 && this.wsConnected) this.sendDeclare();
7225
+ this.emit("host-acked", payload);
6618
7226
  }
6619
- }
6620
- function findIconPath(size = "big") {
6621
- const name = size === "big" ? "icon" : "smallicon";
6622
- const here = thisDirname();
6623
- const candidates = [
6624
- // npm-installed: node_modules/@prismer/runtime/dist/cli.js → ../assets
6625
- path6.resolve(here, "../assets", name),
6626
- // alternate dist layout (sub-bundle): dist/bin/cli.js → ../../assets
6627
- path6.resolve(here, "../../assets", name),
6628
- // source/typecheck: src/cli/ui.ts → ../../assets
6629
- path6.resolve(here, "../../assets", name),
6630
- // dev mode: cwd happens to be runtime root
6631
- path6.resolve(process.cwd(), "assets", name),
6632
- path6.resolve(process.cwd(), "sdk/prismer-cloud/runtime/assets", name)
6633
- ];
6634
- for (const candidate of candidates) {
6635
- try {
6636
- if (fs4.existsSync(candidate)) return candidate;
6637
- } catch {
7227
+ async onTaskDispatch(payload, requestId) {
7228
+ const targetDaemonId = readTargetDaemonId(payload);
7229
+ if (targetDaemonId && targetDaemonId !== this.config.daemon_id) {
7230
+ process.stdout.write(
7231
+ `[daemon] dispatch skip task=${payload.taskId} targetDaemonId=${targetDaemonId} local=${this.config.daemon_id}
7232
+ `
7233
+ );
7234
+ return;
6638
7235
  }
6639
- }
6640
- return null;
6641
- }
6642
- var UI = class {
6643
- mode;
6644
- colorEnabled;
6645
- stream;
6646
- errStream;
6647
- constructor(opts) {
6648
- this.mode = opts?.mode ?? "pretty";
6649
- this.stream = opts?.stream ?? process.stdout;
6650
- this.errStream = opts?.errStream ?? process.stderr;
6651
- if (opts?.color !== void 0) {
6652
- this.colorEnabled = opts.color;
6653
- } else {
6654
- const isTTY = this.stream.isTTY === true;
6655
- const noColor = Boolean(process.env["NO_COLOR"]);
6656
- this.colorEnabled = isTTY && !noColor;
7236
+ if (this.runningTasks.has(payload.taskId)) {
7237
+ process.stdout.write(`[daemon] dispatch dup task=${payload.taskId} (already in-flight, skipping)
7238
+ `);
7239
+ return;
6657
7240
  }
6658
- }
6659
- // ---- Internal color helpers ----
6660
- ansi(open, close, text) {
6661
- if (!this.colorEnabled) return text;
6662
- return `\x1B[${open}m${text}\x1B[${close}m`;
6663
- }
6664
- green(t) {
6665
- return this.ansi(32, 39, t);
6666
- }
6667
- red(t) {
6668
- return this.ansi(31, 39, t);
6669
- }
6670
- yellow(t) {
6671
- return this.ansi(33, 39, t);
6672
- }
6673
- cyan(t) {
6674
- return this.ansi(36, 39, t);
6675
- }
6676
- dim(t) {
6677
- return this.ansi(2, 22, t);
6678
- }
6679
- bold(t) {
6680
- return this.ansi(1, 22, t);
6681
- }
6682
- gray(t) {
6683
- return this.ansi(90, 39, t);
6684
- }
6685
- brandMark() {
6686
- return this.cyan("\u25C7");
6687
- }
6688
- colorBrandLine(line) {
6689
- let out = "";
6690
- for (const ch of line) {
6691
- if (ch === "\u2592") {
6692
- out += this.cyan(ch);
6693
- } else if (ch === "\u2593") {
6694
- out += this.dim(ch);
7241
+ process.stdout.write(
7242
+ `[daemon] dispatch start task=${payload.taskId} route=${payload.runtimeRoute ?? "agent"} agent=${payload.agentImUserId ?? "-"} daemon=${payload.targetDaemonId ?? "-"}
7243
+ `
7244
+ );
7245
+ const ctrl = new AbortController();
7246
+ this.runningTasks.set(payload.taskId, {
7247
+ ctrl,
7248
+ startedAt: Date.now(),
7249
+ lastProgressAt: Date.now(),
7250
+ timeoutMs: typeof payload.timeoutMs === "number" ? payload.timeoutMs : 0
7251
+ });
7252
+ try {
7253
+ if (isShellDispatch(payload)) {
7254
+ const reply = await executeShellDispatch(payload, {
7255
+ config: this.shellConfig,
7256
+ workspaceId: this.workspaceId,
7257
+ signal: ctrl.signal,
7258
+ onProgress: (progressPayload) => {
7259
+ const running = this.runningTasks.get(payload.taskId);
7260
+ if (running) running.lastProgressAt = Date.now();
7261
+ this.ws.send(envelope("task.dispatch.progress", progressPayload));
7262
+ }
7263
+ });
7264
+ this.ws.send(envelope("task.dispatch.reply", reply, requestId));
6695
7265
  } else {
6696
- out += ch;
7266
+ await handleDispatch(payload, requestId, {
7267
+ registry: this.registry,
7268
+ cloud: this.cloud,
7269
+ uriResolver: this.uriResolver,
7270
+ assetCache: this.assetCache,
7271
+ ws: this.ws,
7272
+ outboxWatcher: this.outboxWatcher,
7273
+ paths: this.paths,
7274
+ signal: ctrl.signal,
7275
+ ensureService: (profile, adapter) => this.servicePool.ensureService(profile, adapter),
7276
+ assetMetadataIndexes: this.assetMetadataIndexes,
7277
+ onProgress: () => {
7278
+ const running = this.runningTasks.get(payload.taskId);
7279
+ if (running) running.lastProgressAt = Date.now();
7280
+ }
7281
+ });
6697
7282
  }
7283
+ process.stdout.write(`[daemon] dispatch done task=${payload.taskId}
7284
+ `);
7285
+ } catch (err) {
7286
+ this.lastTaskError = {
7287
+ taskId: payload.taskId,
7288
+ message: err.message,
7289
+ at: (/* @__PURE__ */ new Date()).toISOString()
7290
+ };
7291
+ process.stderr.write(`[daemon] dispatch threw task=${payload.taskId}: ${err.stack ?? err.message}
7292
+ `);
7293
+ } finally {
7294
+ this.runningTasks.delete(payload.taskId);
6698
7295
  }
6699
- return out;
6700
7296
  }
6701
- // ---- Core write helpers ----
6702
- write(text) {
6703
- this.stream.write(text);
7297
+ onTaskCancel(payload) {
7298
+ const entry = this.runningTasks.get(payload.taskId);
7299
+ if (entry) entry.ctrl.abort();
7300
+ }
7301
+ onAgentChanged(payload) {
7302
+ const a = this.hostedAgents.get(payload.agentImUserId);
7303
+ if (!a) return;
7304
+ if (typeof payload.fields.displayName === "string") a.name = payload.fields.displayName;
7305
+ if (Array.isArray(payload.fields.capabilities)) a.capabilities = payload.fields.capabilities;
6704
7306
  }
6705
- writeErr(text) {
6706
- this.errStream.write(text);
7307
+ async onAgentProfileChanged(payload) {
7308
+ try {
7309
+ await this.syncProfileFromCloud(payload.profileId);
7310
+ if (this.wsConnected) this.sendDeclare();
7311
+ } catch (err) {
7312
+ this.emit("sync-error", err);
7313
+ }
6707
7314
  }
6708
- // ---- Level 1: Header ----
6709
- header(text) {
6710
- if (this.mode === "json") return;
6711
- const prefix = text.startsWith("Prismer") ? this.brandMark() + " " : "";
6712
- this.write(prefix + this.bold(text) + "\n");
7315
+ async syncProfileFromCloud(profileId) {
7316
+ const profile = await this.cloud.get(`/api/im/agent_profiles/${encodeURIComponent(profileId)}`);
7317
+ const agent = await this.resolveOwnedAgent(profile.agentImUserId);
7318
+ const adapter = this.registry.get(profile.adapterName);
7319
+ const capabilities = agent?.card?.capabilities?.length ? agent.card.capabilities : adapter?.capabilities ?? [];
7320
+ const name = agent?.card?.name || agent?.displayName || agent?.username || profile.agentImUserId;
7321
+ const now = Date.now();
7322
+ const tx = this.db.transaction(() => {
7323
+ this.db.prepare(
7324
+ `INSERT OR REPLACE INTO agents
7325
+ (im_user_id, workspace_id, name, adapter_name, capabilities, status, version, synced_at, dirty)
7326
+ VALUES (?, ?, ?, ?, ?, 'offline', 1, ?, 0)`
7327
+ ).run(
7328
+ profile.agentImUserId,
7329
+ profile.workspaceId,
7330
+ name,
7331
+ profile.adapterName,
7332
+ JSON.stringify(capabilities),
7333
+ now
7334
+ );
7335
+ this.db.prepare(
7336
+ `INSERT OR REPLACE INTO agent_profiles
7337
+ (id, workspace_id, agent_im_user_id, adapter_name, name, config, version, synced_at, dirty, deleted_at)
7338
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, NULL)`
7339
+ ).run(
7340
+ profile.id,
7341
+ profile.workspaceId,
7342
+ profile.agentImUserId,
7343
+ profile.adapterName,
7344
+ profile.name,
7345
+ JSON.stringify(profile.config ?? {}),
7346
+ profile.version,
7347
+ now
7348
+ );
7349
+ });
7350
+ tx();
7351
+ this.loadAgentsFromDb();
7352
+ process.stdout.write(
7353
+ `[daemon] profile synced agent=${profile.agentImUserId} profile=${profile.id} adapter=${profile.adapterName}
7354
+ `
7355
+ );
6713
7356
  }
6714
- smallHeader(subtitle) {
6715
- if (this.mode === "json" || this.mode === "quiet") return;
6716
- const iconPath = findIconPath("small");
6717
- if (iconPath !== null) {
6718
- try {
6719
- const raw = fs4.readFileSync(iconPath, "utf-8").replace(/\n+$/, "");
6720
- for (const line of raw.split("\n")) {
6721
- this.write(this.cyan(line) + "\n");
6722
- }
6723
- } catch {
6724
- this.write(this.brandMark() + " " + this.bold("Prismer") + "\n");
6725
- }
6726
- } else {
6727
- this.write(this.brandMark() + " " + this.bold("Prismer") + "\n");
7357
+ async resolveOwnedAgent(agentImUserId) {
7358
+ try {
7359
+ const agents = await this.cloud.get("/api/im/me/agents");
7360
+ return agents.find((agent) => agent.id === agentImUserId) ?? null;
7361
+ } catch (err) {
7362
+ process.stderr.write(
7363
+ `[daemon] owned agent lookup skipped agent=${agentImUserId}: ${err.message}
7364
+ `
7365
+ );
7366
+ return null;
6728
7367
  }
6729
- if (subtitle !== void 0 && subtitle.length > 0) {
6730
- this.write(this.dim(" " + subtitle) + "\n");
7368
+ }
7369
+ async onWorkspaceChanged(payload) {
7370
+ if (payload.workspaceId !== this.workspaceId) return;
7371
+ try {
7372
+ await this.cloud.get(`/api/im/workspaces/${encodeURIComponent(payload.workspaceId)}`);
7373
+ } catch (err) {
7374
+ this.emit("sync-error", err);
6731
7375
  }
6732
- this.blank();
6733
7376
  }
6734
- banner(subtitle, opts) {
6735
- if (this.mode === "json" || this.mode === "quiet") return;
6736
- const envColumns = process.env["COLUMNS"] !== void 0 ? parseInt(process.env["COLUMNS"], 10) : NaN;
6737
- const width = this.stream.columns ?? process.stdout.columns ?? (Number.isFinite(envColumns) ? envColumns : 80);
6738
- const iconPath = findIconPath("big");
6739
- const shouldUseFull = opts?.full === true || width >= 120;
6740
- if (shouldUseFull && iconPath !== null) {
6741
- try {
6742
- const raw = fs4.readFileSync(iconPath, "utf-8");
6743
- const lines = raw.split("\n");
6744
- for (const line of lines) {
6745
- const brandedLine = line.replace("Prismer Cloud SDK", "Prismer Runtime CLI");
6746
- const stripped = brandedLine.trimEnd();
6747
- if (stripped.length === 0) {
6748
- this.write("\n");
6749
- continue;
6750
- }
6751
- const clipped = stripped.length >= width ? stripped.slice(0, Math.max(width - 1, 1)) : stripped;
6752
- this.write(this.colorBrandLine(clipped) + "\n");
6753
- }
6754
- } catch {
6755
- this.write(this.cyan(COMPACT_BANNER[0] ?? "\u25C7 PRISMER") + "\n");
6756
- this.write(this.dim(COMPACT_BANNER[1] ?? " Runtime CLI") + "\n");
7377
+ async onAssetChanged(payload) {
7378
+ if (!payload.workspaceId) return;
7379
+ const index = this.assetMetadataIndexes.get(payload.workspaceId);
7380
+ if (!index) return;
7381
+ try {
7382
+ const result = await index.pullDelta();
7383
+ if (result.applied > 0) {
7384
+ console.log(`[Daemon] asset.changed workspace=${payload.workspaceId} applied=${result.applied}`);
6757
7385
  }
6758
- } else {
6759
- this.write(this.cyan(COMPACT_BANNER[0] ?? "\u25C7 PRISMER") + "\n");
6760
- this.write(this.dim(COMPACT_BANNER[1] ?? " Runtime CLI") + "\n");
6761
- }
6762
- if (subtitle !== void 0 && subtitle.length > 0) {
6763
- this.write(this.dim(" " + subtitle) + "\n");
6764
- }
6765
- this.blank();
6766
- }
6767
- // ---- Level 2: Primary data ----
6768
- blank() {
6769
- if (this.mode === "json") return;
6770
- this.write("\n");
6771
- }
6772
- line(text) {
6773
- if (this.mode === "json") return;
6774
- this.write(text + "\n");
6775
- }
6776
- info(text) {
6777
- this.line(text);
6778
- }
6779
- // ---- Level 3: Secondary ----
6780
- secondary(text, indent = 2) {
6781
- if (this.mode === "json") return;
6782
- this.write(" ".repeat(indent) + this.dim(text) + "\n");
6783
- }
6784
- // ---- Level 4: Action tips ----
6785
- tip(text) {
6786
- if (this.mode === "json") return;
6787
- this.write(this.cyan("Tip:") + " " + text + "\n");
6788
- }
6789
- next(text) {
6790
- if (this.mode === "json") return;
6791
- this.write(this.cyan("Next:") + " " + text + "\n");
6792
- }
6793
- // ---- Level 5: Status indicators ----
6794
- ok(text, detail) {
6795
- if (this.mode === "json") return;
6796
- const suffix = detail ? " " + this.dim(detail) : "";
6797
- this.write(" " + this.green("\u2713") + " " + text + suffix + "\n");
6798
- }
6799
- success(text, detail) {
6800
- this.ok(text, detail);
6801
- }
6802
- fail(text, detail) {
6803
- if (this.mode === "json") return;
6804
- const suffix = detail ? " " + this.dim(detail) : "";
6805
- this.write(" " + this.red("\u2717") + " " + text + suffix + "\n");
6806
- }
6807
- online(text) {
6808
- if (this.mode === "json") return;
6809
- this.write(" " + this.green("\u25CF") + " " + text + "\n");
6810
- }
6811
- offline(text) {
6812
- if (this.mode === "json") return;
6813
- this.write(" " + this.gray("\u25CB") + " " + text + "\n");
6814
- }
6815
- notInstalled(text) {
6816
- if (this.mode === "json") return;
6817
- this.write(" " + this.dim("\xB7") + " " + this.dim(text) + "\n");
6818
- }
6819
- pending(text) {
6820
- if (this.mode === "json") return;
6821
- this.write(" " + this.yellow("\u27F3") + " " + text + "\n");
6822
- }
6823
- warn(text, detail) {
6824
- if (this.mode === "json") return;
6825
- const suffix = detail ? " " + this.dim(detail) : "";
6826
- this.write(" " + this.yellow("!") + " " + text + suffix + "\n");
6827
- }
6828
- // ---- Level 6: Error block ----
6829
- error(what, cause, fix) {
6830
- if (this.mode === "json") return;
6831
- this.writeErr(this.red("\u2717") + " " + what + "\n");
6832
- if (cause !== void 0) {
6833
- this.writeErr(" " + this.dim("Cause:") + " " + this.dim(cause) + "\n");
6834
- }
6835
- if (fix !== void 0) {
6836
- this.writeErr(" " + this.cyan("Fix:") + " " + fix + "\n");
6837
- }
6838
- }
6839
- table(rowsOrOpts, maybeOpts) {
6840
- if (this.mode === "json") return;
6841
- const rows = Array.isArray(rowsOrOpts) ? rowsOrOpts : rowsOrOpts.rows;
6842
- const opts = Array.isArray(rowsOrOpts) ? maybeOpts : { columns: rowsOrOpts.columns, maxWidth: rowsOrOpts.maxWidth };
6843
- if (!opts) throw new Error("table() requires columns");
6844
- const maxWidth = opts.maxWidth ?? (process.stdout.columns || 80);
6845
- const cols = opts.columns;
6846
- const widths = cols.map((col) => col.length);
6847
- for (const row of rows) {
6848
- cols.forEach((col, i) => {
6849
- const val = row[col] ?? "";
6850
- const w = widths[i] ?? 0;
6851
- if (val.length > w) widths[i] = val.length;
6852
- });
7386
+ } catch (err) {
7387
+ console.error(`[Daemon] asset.changed pullDelta failed workspace=${payload.workspaceId}:`, err.message);
6853
7388
  }
6854
- const totalWidth = widths.reduce((a, b) => a + b, 0) + (cols.length - 1) * 2 + 2;
6855
- if (totalWidth > maxWidth) {
6856
- for (let i = 0; i < rows.length; i++) {
6857
- const row = rows[i];
6858
- if (!row) continue;
6859
- for (const col of cols) {
6860
- const val = row[col] ?? "";
6861
- this.write(" " + this.bold(col + ":") + " " + val + "\n");
6862
- }
6863
- if (i < rows.length - 1) this.write("\n");
6864
- }
7389
+ }
7390
+ onWorkspaceFileChanged(payload) {
7391
+ if (payload.operation === "delete") {
7392
+ this.db.prepare("DELETE FROM workspace_files_mirror WHERE workspace_id = ? AND path = ?").run(payload.workspaceId, payload.path);
6865
7393
  return;
6866
7394
  }
6867
- const header = cols.map((col, i) => col.toUpperCase().padEnd(widths[i] ?? col.length)).join(" ");
6868
- this.write(" " + this.dim(header) + "\n");
6869
- for (const row of rows) {
6870
- const line = cols.map((col, i) => (row[col] ?? "").padEnd(widths[i] ?? col.length)).join(" ");
6871
- this.write(" " + line + "\n");
7395
+ if (payload.assetId && payload.contentHash) {
7396
+ this.db.prepare(
7397
+ `INSERT OR REPLACE INTO workspace_files_mirror
7398
+ (workspace_id, path, asset_id, content_hash, version, synced_at, dirty)
7399
+ VALUES (?, ?, ?, ?, ?, ?, 0)`
7400
+ ).run(
7401
+ payload.workspaceId,
7402
+ payload.path,
7403
+ payload.assetId,
7404
+ payload.contentHash,
7405
+ payload.version,
7406
+ Date.now()
7407
+ );
6872
7408
  }
6873
7409
  }
6874
- // ---- Spinner ----
6875
- spinner(text) {
6876
- if (this.mode === "quiet" || this.mode === "json") {
6877
- return {
6878
- update() {
6879
- },
6880
- stop() {
6881
- }
6882
- };
6883
- }
6884
- const isTTY = this.stream.isTTY === true;
6885
- if (!isTTY || !this.colorEnabled) {
6886
- this.write(" " + this.yellow("\u27F3") + " " + text + "\n");
6887
- return {
6888
- update: (t) => {
6889
- this.write(" " + this.yellow("\u27F3") + " " + t + "\n");
6890
- },
6891
- stop: (final) => {
6892
- if (final) this.write(" " + this.green("\u2713") + " " + final + "\n");
6893
- }
6894
- };
6895
- }
6896
- let current = text;
6897
- let frameIdx = 0;
6898
- let stopped = false;
6899
- const write = this.write.bind(this);
6900
- const colorFn = this.yellow.bind(this);
6901
- const greenFn = this.green.bind(this);
6902
- function renderFrame() {
6903
- const frame = BRAILLE_FRAMES[frameIdx % BRAILLE_FRAMES.length] ?? "\u280B";
6904
- const line = " " + colorFn(frame) + " " + current;
6905
- write("\r" + line);
6906
- frameIdx++;
6907
- }
6908
- renderFrame();
6909
- const timer = setInterval(renderFrame, 80);
7410
+ snapshotAdapterObservability() {
7411
+ const agents = Array.from(this.hostedAgents.values());
7412
+ const counts = agents.reduce((acc, agent) => {
7413
+ acc[agent.adapterName] = (acc[agent.adapterName] ?? 0) + 1;
7414
+ return acc;
7415
+ }, {});
6910
7416
  return {
6911
- update(t) {
6912
- if (stopped) return;
6913
- current = t;
6914
- },
6915
- stop(final) {
6916
- if (stopped) return;
6917
- stopped = true;
6918
- clearInterval(timer);
6919
- write("\r\x1B[2K");
6920
- if (final) write(" " + greenFn("\u2713") + " " + final + "\n");
7417
+ hostedCounts: counts,
7418
+ servicePoolSize: this.servicePool?.size() ?? 0,
7419
+ hermes: {
7420
+ hostedAgents: counts.hermes ?? 0,
7421
+ runningTaskIds: Array.from(this.runningTasks.keys())
6921
7422
  }
6922
7423
  };
6923
7424
  }
6924
- // ---- Progress bar ----
6925
- progress(text, total) {
6926
- if (this.mode === "quiet" || this.mode === "json") {
6927
- return {
6928
- update() {
6929
- },
6930
- stop() {
6931
- }
6932
- };
7425
+ /**
7426
+ * Ensure an AssetMetadataIndex exists for the given workspace and pull
7427
+ * delta from cloud. Idempotent creates the index on first call, reuses
7428
+ * it on subsequent calls. Same cursor-catch-up semantics as WorkspaceMirror.
7429
+ */
7430
+ async syncAssetMetadata(workspaceId) {
7431
+ let index = this.assetMetadataIndexes.get(workspaceId);
7432
+ if (!index) {
7433
+ const stateDir = `${this.paths.root}/${workspaceId}`;
7434
+ index = new AssetMetadataIndex({
7435
+ db: this.db,
7436
+ cloud: this.cloud,
7437
+ workspaceId,
7438
+ workspaceStateDir: stateDir
7439
+ });
7440
+ this.assetMetadataIndexes.set(workspaceId, index);
6933
7441
  }
6934
- const isTTY = this.stream.isTTY === true;
6935
- const start = Date.now();
6936
- const write = this.write.bind(this);
6937
- const colorFn = this.cyan.bind(this);
6938
- const dimFn = this.dim.bind(this);
6939
- const greenFn = this.green.bind(this);
6940
- let last = 0;
6941
- let lastDetail = "";
6942
- let stopped = false;
6943
- const render = () => {
6944
- if (stopped) return;
6945
- const frac = total > 0 ? Math.min(1, Math.max(0, last / total)) : 0;
6946
- const pct = Math.floor(frac * 100);
6947
- const width = 20;
6948
- const filled = Math.floor(frac * width);
6949
- const bar = "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
6950
- const elapsed = (Date.now() - start) / 1e3;
6951
- const eta = frac > 0.01 ? Math.max(0, elapsed / frac - elapsed) : 0;
6952
- const etaStr = frac >= 1 ? "" : ` \xB7 ${eta < 1 ? "<1s" : Math.round(eta) + "s"} left`;
6953
- const detailStr = lastDetail ? ` \xB7 ${lastDetail}` : "";
6954
- const line = ` ${text} [${colorFn(bar)}] ${String(pct).padStart(3)}%${detailStr}${dimFn(etaStr)}`;
6955
- if (isTTY && this.colorEnabled) {
6956
- write("\r\x1B[2K" + line);
6957
- } else {
6958
- write(line + "\n");
6959
- }
6960
- };
6961
- render();
6962
- return {
6963
- update: (current, detail) => {
6964
- if (stopped) return;
6965
- last = current;
6966
- if (detail !== void 0) lastDetail = detail;
6967
- render();
6968
- },
6969
- stop: (final) => {
6970
- if (stopped) return;
6971
- stopped = true;
6972
- if (isTTY && this.colorEnabled) write("\r\x1B[2K");
6973
- if (final) write(" " + greenFn("\u2713") + " " + final + "\n");
6974
- }
6975
- };
6976
- }
6977
- // ---- JSON output ----
6978
- json(payload, opts) {
6979
- const indent = opts?.pretty ? 2 : void 0;
6980
- this.write(JSON.stringify(payload, null, indent) + "\n");
6981
- }
6982
- result(pretty, jsonPayload) {
6983
- if (this.mode === "pretty") {
6984
- pretty();
6985
- } else {
6986
- this.json(jsonPayload);
7442
+ const result = await index.pullDelta();
7443
+ if (result.applied > 0) {
7444
+ console.log(`[AssetMeta] workspace=${workspaceId}: ${result.applied} applied, cursor=${result.cursor}`);
6987
7445
  }
6988
7446
  }
6989
- };
6990
- var _ui = null;
6991
- function getUI() {
6992
- if (!_ui) _ui = new UI();
6993
- return _ui;
6994
- }
6995
- function setUI(ui) {
6996
- _ui = ui;
6997
- }
6998
- function applyCommonFlags(argv) {
6999
- let mode = "pretty";
7000
- const isTTY = process.stdout.isTTY === true;
7001
- const noColorEnv = Boolean(process.env["NO_COLOR"]);
7002
- let color2 = isTTY && !noColorEnv;
7003
- const rest = [];
7004
- for (const arg of argv) {
7005
- switch (arg) {
7006
- case "--no-color":
7007
- color2 = false;
7447
+ /**
7448
+ * SyncWorker FlushFn — pushes local writes to cloud.
7449
+ *
7450
+ * Maps:
7451
+ * workspace → PATCH /api/im/workspaces/:id
7452
+ * agent_profile → PATCH /api/im/agent_profiles/:id
7453
+ * agent → PATCH /api/im/agents/:imUserId
7454
+ * On 'create' we POST instead. On 'delete' we DELETE.
7455
+ */
7456
+ /**
7457
+ * SyncWorker FlushFn — pushes one local sync row to cloud via CloudClient.
7458
+ *
7459
+ * resource_type × operation → endpoint:
7460
+ * workspace.create → POST /api/im/workspaces
7461
+ * workspace.update → PATCH /api/im/workspaces/:id
7462
+ * workspace.delete → DELETE /api/im/workspaces/:id
7463
+ * agent.create → POST /api/im/register (the only public path
7464
+ * that creates an
7465
+ * IMUser of role='agent')
7466
+ * agent.update → PATCH /api/im/agents/:id
7467
+ * agent.delete → DELETE /api/im/agents/:id
7468
+ * agent_profile.create → POST /api/im/agent_profiles
7469
+ * agent_profile.update → PATCH /api/im/agent_profiles/:id
7470
+ * agent_profile.delete → DELETE /api/im/agent_profiles/:id
7471
+ *
7472
+ * Error classification per docs/refactor/13-error-handling.md §2.1 / §2.7:
7473
+ * 2xx → ok:true (SyncWorker drops the row)
7474
+ * 408 / 429 → retryable (SyncWorker re-queues with exponential backoff)
7475
+ * 5xx / net err → retryable
7476
+ * 4xx (other) → permanent (SyncWorker marks failed; 409 is conflict)
7477
+ */
7478
+ async flushSyncRow(row) {
7479
+ const op = `${row.resource_type}.${row.operation}`;
7480
+ const id = encodeURIComponent(row.resource_id);
7481
+ const body = row.operation === "delete" ? void 0 : safeJsonParse(row.payload);
7482
+ let method;
7483
+ let path7;
7484
+ switch (op) {
7485
+ case "workspace.create":
7486
+ method = "POST";
7487
+ path7 = "/api/im/workspaces";
7008
7488
  break;
7009
- case "--color":
7010
- color2 = true;
7489
+ case "workspace.update":
7490
+ method = "PATCH";
7491
+ path7 = `/api/im/workspaces/${id}`;
7011
7492
  break;
7012
- case "--json":
7013
- case "--pretty-json":
7014
- mode = "json";
7015
- if (arg === "--json") rest.push(arg);
7493
+ case "workspace.delete":
7494
+ method = "DELETE";
7495
+ path7 = `/api/im/workspaces/${id}`;
7016
7496
  break;
7017
- case "--quiet":
7018
- mode = "quiet";
7497
+ // agent.create must use /register — POST /api/im/agents is not exposed.
7498
+ case "agent.create":
7499
+ method = "POST";
7500
+ path7 = "/api/im/register";
7501
+ break;
7502
+ case "agent.update":
7503
+ method = "PATCH";
7504
+ path7 = `/api/im/agents/${id}`;
7505
+ break;
7506
+ case "agent.delete":
7507
+ method = "DELETE";
7508
+ path7 = `/api/im/agents/${id}`;
7509
+ break;
7510
+ case "agent_profile.create":
7511
+ method = "POST";
7512
+ path7 = "/api/im/agent_profiles";
7513
+ break;
7514
+ case "agent_profile.update":
7515
+ method = "PATCH";
7516
+ path7 = `/api/im/agent_profiles/${id}`;
7517
+ break;
7518
+ case "agent_profile.delete":
7519
+ method = "DELETE";
7520
+ path7 = `/api/im/agent_profiles/${id}`;
7019
7521
  break;
7020
7522
  default:
7021
- rest.push(arg);
7523
+ process.stderr.write(`[daemon] sync flush op=${op} id=${row.id} result=drop (unknown op)
7524
+ `);
7525
+ return { ok: false, status: 400, message: `Unknown sync op: ${op}` };
7526
+ }
7527
+ let res;
7528
+ try {
7529
+ res = await this.cloud.request(method, path7, { body });
7530
+ } catch (err) {
7531
+ process.stderr.write(
7532
+ `[daemon] sync flush op=${op} id=${row.id} result=retry (threw: ${err.message})
7533
+ `
7534
+ );
7535
+ return { ok: false, status: 0, message: err.message };
7536
+ }
7537
+ let label;
7538
+ if (res.ok) {
7539
+ label = "ok";
7540
+ } else if (res.status === 0 || res.status === 408 || res.status === 429 || res.status >= 500) {
7541
+ label = "retry";
7542
+ } else {
7543
+ label = "drop";
7544
+ }
7545
+ process.stdout.write(
7546
+ `[daemon] sync flush op=${op} id=${row.id} result=${label}${res.ok ? "" : ` status=${res.status}`}
7547
+ `
7548
+ );
7549
+ return { ok: res.ok, status: res.status, message: res.error?.message };
7550
+ }
7551
+ };
7552
+ function readTargetDaemonId(payload) {
7553
+ if (typeof payload.targetDaemonId === "string" && payload.targetDaemonId.length > 0) {
7554
+ return payload.targetDaemonId;
7555
+ }
7556
+ const execution = payload.metadata?.execution;
7557
+ if (execution && typeof execution === "object" && !Array.isArray(execution)) {
7558
+ const value = execution.targetDaemonId;
7559
+ return typeof value === "string" && value.length > 0 ? value : null;
7560
+ }
7561
+ return null;
7562
+ }
7563
+ function truthy(raw) {
7564
+ return raw === "1" || raw === "true" || raw === "yes";
7565
+ }
7566
+ function validateStaticHostedAgent(raw) {
7567
+ if (!raw || typeof raw !== "object") throw new Error("binding must be a JSON object");
7568
+ const obj = raw;
7569
+ const profile = obj.profile;
7570
+ const capabilities = obj.capabilities;
7571
+ if (typeof obj.workspaceId !== "string" || obj.workspaceId.length === 0) throw new Error("workspaceId is required");
7572
+ if (typeof obj.imUserId !== "string" || obj.imUserId.length === 0) throw new Error("imUserId is required");
7573
+ if (typeof obj.name !== "string" || obj.name.length === 0) throw new Error("name is required");
7574
+ if (typeof obj.adapterName !== "string" || obj.adapterName.length === 0) throw new Error("adapterName is required");
7575
+ if (!Array.isArray(capabilities) || capabilities.some((v) => typeof v !== "string")) {
7576
+ throw new Error("capabilities must be a string array");
7577
+ }
7578
+ if (!profile || typeof profile !== "object") throw new Error("profile is required");
7579
+ if (typeof profile.id !== "string" || profile.id.length === 0) throw new Error("profile.id is required");
7580
+ if (typeof profile.name !== "string" || profile.name.length === 0) throw new Error("profile.name is required");
7581
+ if (typeof profile.adapterName !== "string" || profile.adapterName.length === 0) {
7582
+ throw new Error("profile.adapterName is required");
7583
+ }
7584
+ if (profile.config !== void 0 && (!profile.config || typeof profile.config !== "object" || Array.isArray(profile.config))) {
7585
+ throw new Error("profile.config must be a JSON object");
7586
+ }
7587
+ return {
7588
+ workspaceId: obj.workspaceId,
7589
+ imUserId: obj.imUserId,
7590
+ name: obj.name,
7591
+ adapterName: obj.adapterName,
7592
+ capabilities,
7593
+ profile: {
7594
+ id: profile.id,
7595
+ name: profile.name,
7596
+ adapterName: profile.adapterName,
7597
+ config: profile.config ?? {},
7598
+ version: typeof profile.version === "number" && Number.isFinite(profile.version) ? profile.version : 1
7022
7599
  }
7600
+ };
7601
+ }
7602
+ function safeJsonParse(raw) {
7603
+ try {
7604
+ return JSON.parse(raw);
7605
+ } catch {
7606
+ return raw;
7023
7607
  }
7024
- return { mode, color: color2, restArgv: rest };
7025
7608
  }
7026
7609
 
7027
- // src/cli/util.ts
7028
- var DEFAULT_CLOUD_BASE_URL = "https://prismer.cloud";
7029
- var ANSI = {
7030
- reset: "\x1B[0m",
7031
- bold: "\x1B[1m",
7032
- dim: "\x1B[2m",
7033
- cyan: "\x1B[36m",
7034
- green: "\x1B[32m",
7035
- yellow: "\x1B[33m",
7036
- red: "\x1B[31m"
7037
- };
7038
- function color(kind, text) {
7039
- if (process.env.NO_COLOR === "1" || process.env.NO_COLOR === "true") return text;
7040
- return `${ANSI[kind]}${text}${ANSI.reset}`;
7041
- }
7042
- function printJson(v) {
7043
- getUI().json(v, { pretty: true });
7044
- }
7045
- function exitWithError(message, opts) {
7046
- const o = typeof opts === "number" ? { exitCode: opts } : opts ?? {};
7047
- const exitCode = o.exitCode ?? 1;
7048
- const ui = getUI();
7049
- if (ui.mode === "json") {
7050
- const payload = {
7051
- ok: false,
7052
- error: { code: o.code ?? "cli_error", message },
7053
- ...o.details ? { details: o.details } : {}
7054
- };
7055
- ui.json(payload, { pretty: true });
7056
- } else {
7057
- process.stderr.write(`Error: ${message}
7058
- `);
7610
+ // src/pair.ts
7611
+ import { generateKeyPairSync } from "crypto";
7612
+ import { hostname as hostname2 } from "os";
7613
+ import { setTimeout as sleep } from "timers/promises";
7614
+ import qrcode from "qrcode";
7615
+ async function pair(opts) {
7616
+ const paths = opts.paths ?? resolvePaths();
7617
+ if (configExists(paths) && !opts.force) {
7618
+ throw new Error(
7619
+ `Config already exists at ${paths.configFile}. Pass --force to overwrite, or run \`prismer status\` to inspect.`
7620
+ );
7059
7621
  }
7060
- process.exit(exitCode);
7061
- }
7062
- function normalizeCloudUrl(input) {
7063
- const raw = input.trim();
7064
- if (!raw) throw new Error("Cloud URL is empty.");
7065
- if (/^[a-z][a-z0-9+\-.]*:\/\//i.test(raw)) {
7066
- let parsed;
7067
- try {
7068
- parsed = new URL(raw);
7069
- } catch {
7070
- throw new Error(`Invalid --cloud URL: ${raw}`);
7071
- }
7072
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
7073
- throw new Error(`Invalid --cloud URL scheme: ${parsed.protocol} (expected http:// or https://)`);
7074
- }
7075
- return raw.replace(/\/$/, "");
7622
+ const isLocalOnly = opts.isLocalOnly ?? (() => process.env.LOCAL_ONLY === "1");
7623
+ const localOnlyMode = !!opts.asUserEmail;
7624
+ if (localOnlyMode && !isLocalOnly()) {
7625
+ throw new Error(
7626
+ "pair: --as-user requires LOCAL_ONLY=1. Without that gate, this would skip mobile approval and silently mint a key for the named user."
7627
+ );
7076
7628
  }
7077
- if (!/^[A-Za-z0-9.\-_:[\]/]+$/.test(raw)) {
7078
- throw new Error(`Invalid --cloud URL: ${raw} (must be http://\u2026 or https://\u2026 or host:port)`);
7629
+ const { publicKey } = generateKeyPairSync("ed25519");
7630
+ const devicePub = publicKey.export({ format: "der", type: "spki" }).toString("base64");
7631
+ const cloud = new CloudClient({
7632
+ baseUrl: opts.cloudBaseUrl,
7633
+ apiKey: "pending",
7634
+ // not used: we pass auth:false
7635
+ fetchImpl: opts.fetchImpl
7636
+ });
7637
+ const offerRes = await cloud.request(
7638
+ "POST",
7639
+ "/api/im/pair/offer",
7640
+ {
7641
+ auth: false,
7642
+ body: { devicePub, deviceName: opts.deviceName ?? hostname2() }
7643
+ }
7644
+ );
7645
+ if (!offerRes.ok) {
7646
+ throw new Error(`pair: offer failed (${offerRes.status}): ${offerRes.error?.message}`);
7079
7647
  }
7080
- const candidate = `http://${raw}`;
7081
- try {
7082
- new URL(candidate);
7083
- } catch {
7084
- throw new Error(`Invalid --cloud URL: ${raw}`);
7648
+ const offer = unwrapEnvelope(offerRes.data);
7649
+ if (!offer.nonce || !offer.qrUrl) {
7650
+ throw new Error("pair: cloud returned no nonce/qrUrl");
7085
7651
  }
7086
- return candidate.replace(/\/$/, "");
7087
- }
7088
- function runAction(fn, opts = {}) {
7089
- return async (...args) => {
7090
- try {
7091
- await fn(...args);
7092
- } catch (err) {
7093
- const raw = err instanceof Error ? err.message : String(err);
7094
- const message = opts.sanitize ? opts.sanitize(raw) : raw;
7095
- exitWithError(message, { code: opts.code });
7652
+ if (localOnlyMode) {
7653
+ const approveRes = await cloud.request(
7654
+ "POST",
7655
+ "/api/im/pair/local-only-approve",
7656
+ {
7657
+ auth: false,
7658
+ body: { nonce: offer.nonce, asUserEmail: opts.asUserEmail }
7659
+ }
7660
+ );
7661
+ if (!approveRes.ok) {
7662
+ throw new Error(
7663
+ `pair: local-only-approve failed (${approveRes.status}): ${approveRes.error?.message ?? "unknown"}`
7664
+ );
7096
7665
  }
7097
- };
7098
- }
7099
- function printBanner(opts = {}) {
7100
- const ui = getUI();
7101
- if (opts.compact) {
7102
- ui.smallHeader("Runtime CLI v1.9.7");
7103
- return;
7666
+ process.stdout.write(`[pair] LOCAL_ONLY approved as ${opts.asUserEmail} \u2014 no QR shown
7667
+ `);
7668
+ } else {
7669
+ const qrAscii = await qrcode.toString(offer.qrUrl, { type: "terminal", small: true });
7670
+ process.stdout.write(qrAscii);
7671
+ process.stdout.write(`
7672
+ Scan with Lumin to approve, or open: ${offer.qrUrl}
7673
+
7674
+ `);
7675
+ opts.onQrReady?.(offer.qrUrl);
7104
7676
  }
7105
- ui.banner("Runtime CLI v1.9.7", { full: true });
7106
- }
7107
- function ok(label, detail) {
7108
- getUI().ok(label, detail);
7109
- }
7110
- function warn(label, detail) {
7111
- getUI().warn(label, detail);
7112
- }
7113
- function fail2(label, detail) {
7114
- getUI().fail(label, detail);
7115
- }
7116
- function tip(command, detail) {
7117
- const text = detail ? `${command} ${detail}` : command;
7118
- getUI().tip(text);
7119
- }
7120
- function pidFilePath(paths) {
7121
- return join9(paths.root, "daemon.pid");
7122
- }
7123
- function writePidFile(paths, pid) {
7124
- writeFileSync5(pidFilePath(paths), `${pid}
7125
- `, "utf8");
7126
- }
7127
- function readPidFile(paths) {
7128
- const p = pidFilePath(paths);
7129
- if (!existsSync10(p)) return void 0;
7130
- const raw = readFileSync7(p, "utf8").trim();
7131
- const pid = Number.parseInt(raw, 10);
7132
- return Number.isFinite(pid) ? pid : void 0;
7133
- }
7134
- function clearPidFile(paths) {
7135
- const p = pidFilePath(paths);
7136
- if (existsSync10(p)) {
7137
- try {
7138
- unlinkSync2(p);
7139
- } catch {
7677
+ const pollPath = `/api/im/pair/poll/${encodeURIComponent(offer.nonce)}?devicePub=${encodeURIComponent(devicePub)}`;
7678
+ const maxAttempts = opts.maxPollAttempts ?? 60;
7679
+ const pollIntervalMs = opts.pollIntervalMs ?? 5e3;
7680
+ for (let i = 0; i < maxAttempts; i += 1) {
7681
+ if (i > 0 || !localOnlyMode) {
7682
+ await sleep(pollIntervalMs);
7683
+ }
7684
+ const res = await cloud.request(
7685
+ "GET",
7686
+ pollPath,
7687
+ { auth: false }
7688
+ );
7689
+ if (res.ok) {
7690
+ const body = unwrapEnvelope(res.data);
7691
+ if (body.apiKey) {
7692
+ const config = {
7693
+ api_key: body.apiKey,
7694
+ cloud_api_base: opts.cloudBaseUrl,
7695
+ daemon_id: newDaemonId()
7696
+ };
7697
+ saveConfig(config, paths);
7698
+ return { config, paths };
7699
+ }
7700
+ }
7701
+ if (res.status === 404 || res.status === 202) {
7702
+ continue;
7703
+ }
7704
+ if (res.status >= 400 && res.status !== 404) {
7705
+ throw new Error(`pair: poll failed (${res.status}): ${res.error?.message}`);
7140
7706
  }
7141
7707
  }
7708
+ throw new Error("pair: timed out waiting for approval (5 min)");
7142
7709
  }
7143
- function pidAlive(pid) {
7144
- try {
7145
- process.kill(pid, 0);
7146
- return true;
7147
- } catch {
7148
- return false;
7710
+ function unwrapEnvelope(raw) {
7711
+ if (raw && typeof raw === "object" && "ok" in raw) {
7712
+ const env = raw;
7713
+ if (env.ok && env.data) return env.data;
7149
7714
  }
7715
+ return raw;
7150
7716
  }
7151
7717
 
7718
+ // src/cli/index.ts
7719
+ import { Command as Command18 } from "commander";
7720
+
7152
7721
  // src/cli/commands/adapter.ts
7722
+ import { spawnSync as spawnSync2 } from "child_process";
7723
+ import {
7724
+ copyFileSync,
7725
+ existsSync as existsSync12,
7726
+ mkdirSync as mkdirSync8,
7727
+ readFileSync as readFileSync9,
7728
+ statSync as statSync2,
7729
+ writeFileSync as writeFileSync7
7730
+ } from "fs";
7731
+ import { homedir as homedir4 } from "os";
7732
+ import { dirname as dirname7, join as join11 } from "path";
7733
+ import { Command } from "commander";
7734
+ init_util();
7735
+ init_ui();
7153
7736
  var BUILTIN_ADAPTERS = [hermesAdapter, claudeCodeAdapter, openclawAdapter, codexAdapter];
7154
7737
  var INSTALL_SPECS = {
7155
7738
  "claude-code": {
@@ -7159,7 +7742,7 @@ var INSTALL_SPECS = {
7159
7742
  binary: "claude",
7160
7743
  hint: "Set ANTHROPIC_API_KEY in your shell profile, or rely on Claude Code OAuth login. Run `claude login` to sign in.",
7161
7744
  authHints: ["ANTHROPIC_API_KEY or Claude Code OAuth login (`claude login`)"],
7162
- hookTarget: { path: join10(homedir4(), ".claude", "hooks.json"), kind: "json-file" }
7745
+ hookTarget: { path: join11(homedir4(), ".claude", "hooks.json"), kind: "json-file" }
7163
7746
  },
7164
7747
  openclaw: {
7165
7748
  name: "openclaw",
@@ -7168,7 +7751,7 @@ var INSTALL_SPECS = {
7168
7751
  binary: "openclaw",
7169
7752
  hint: "OpenClaw runs as a gateway. Configure ~/.openclaw/openclaw.json and start it with `openclaw gateway` before tasks dispatch.",
7170
7753
  authHints: ["~/.openclaw/openclaw.json gateway.auth.bearerTokens", "Prismer daemon api_key"],
7171
- hookTarget: { path: join10(homedir4(), ".openclaw", "hooks"), kind: "directory" }
7754
+ hookTarget: { path: join11(homedir4(), ".openclaw", "hooks"), kind: "directory" }
7172
7755
  },
7173
7756
  codex: {
7174
7757
  name: "codex",
@@ -7177,7 +7760,7 @@ var INSTALL_SPECS = {
7177
7760
  binary: "codex",
7178
7761
  hint: "Set OPENAI_API_KEY in your shell profile. Verify with `codex --help`.",
7179
7762
  authHints: ["OPENAI_API_KEY or Codex CLI account login"],
7180
- hookTarget: { path: join10(homedir4(), ".codex", "hooks.json"), kind: "json-file" }
7763
+ hookTarget: { path: join11(homedir4(), ".codex", "hooks.json"), kind: "json-file" }
7181
7764
  },
7182
7765
  hermes: {
7183
7766
  name: "hermes",
@@ -7186,7 +7769,7 @@ var INSTALL_SPECS = {
7186
7769
  binary: "hermes",
7187
7770
  hint: "Hermes runs as a long-lived HTTP gateway in your own Python venv. Start with `hermes -p <profile> gateway` after configuring ~/.hermes/.env.",
7188
7771
  authHints: ["~/.hermes/.env API_SERVER_KEY", "Hermes profile provider credentials"],
7189
- hookTarget: { path: join10(homedir4(), ".hermes", "hooks.json"), kind: "json-file" }
7772
+ hookTarget: { path: join11(homedir4(), ".hermes", "hooks.json"), kind: "json-file" }
7190
7773
  }
7191
7774
  };
7192
7775
  function buildAdapterCommand() {
@@ -7401,15 +7984,15 @@ function runHooks(spec, opts) {
7401
7984
  }
7402
7985
  let backup;
7403
7986
  if (planned.kind === "directory") {
7404
- mkdirSync7(planned.path, { recursive: true });
7405
- const markerPath = join10(planned.path, "prismer.json");
7987
+ mkdirSync8(planned.path, { recursive: true });
7988
+ const markerPath = join11(planned.path, "prismer.json");
7406
7989
  backup = backupIfExists(markerPath);
7407
- writeFileSync6(markerPath, JSON.stringify(prismerHookMarker(spec), null, 2) + "\n", "utf8");
7990
+ writeFileSync7(markerPath, JSON.stringify(prismerHookMarker(spec), null, 2) + "\n", "utf8");
7408
7991
  } else {
7409
- mkdirSync7(dirname7(planned.path), { recursive: true });
7992
+ mkdirSync8(dirname7(planned.path), { recursive: true });
7410
7993
  backup = backupIfExists(planned.path);
7411
7994
  const merged = mergeHookJson(planned.path, spec);
7412
- writeFileSync6(planned.path, JSON.stringify(merged, null, 2) + "\n", "utf8");
7995
+ writeFileSync7(planned.path, JSON.stringify(merged, null, 2) + "\n", "utf8");
7413
7996
  }
7414
7997
  return {
7415
7998
  ok: true,
@@ -7476,23 +8059,23 @@ function inspectAuthEnv(spec) {
7476
8059
  hints: spec.authHints ?? []
7477
8060
  };
7478
8061
  case "hermes": {
7479
- const envFile = join10(homedir4(), ".hermes", ".env");
8062
+ const envFile = join11(homedir4(), ".hermes", ".env");
7480
8063
  return {
7481
- ok: existsSync11(envFile) || Boolean(process.env.HERMES_API_KEY || process.env.API_SERVER_KEY),
8064
+ ok: existsSync12(envFile) || Boolean(process.env.HERMES_API_KEY || process.env.API_SERVER_KEY),
7482
8065
  present: [
7483
8066
  ...["HERMES_API_KEY", "API_SERVER_KEY"].filter((k) => Boolean(process.env[k])),
7484
- ...existsSync11(envFile) ? [envFile] : []
8067
+ ...existsSync12(envFile) ? [envFile] : []
7485
8068
  ],
7486
8069
  hints: spec.authHints ?? []
7487
8070
  };
7488
8071
  }
7489
8072
  case "openclaw": {
7490
- const cfgFile = join10(homedir4(), ".openclaw", "openclaw.json");
8073
+ const cfgFile = join11(homedir4(), ".openclaw", "openclaw.json");
7491
8074
  return {
7492
- ok: existsSync11(cfgFile) || Boolean(process.env.OPENCLAW_API_KEY),
8075
+ ok: existsSync12(cfgFile) || Boolean(process.env.OPENCLAW_API_KEY),
7493
8076
  present: [
7494
8077
  ...["OPENCLAW_API_KEY"].filter((k) => Boolean(process.env[k])),
7495
- ...existsSync11(cfgFile) ? [cfgFile] : []
8078
+ ...existsSync12(cfgFile) ? [cfgFile] : []
7496
8079
  ],
7497
8080
  hints: spec.authHints ?? []
7498
8081
  };
@@ -7507,32 +8090,32 @@ function inspectHook(spec) {
7507
8090
  return { supported: false, markerPresent: false };
7508
8091
  }
7509
8092
  if (spec.name === "openclaw") {
7510
- const jsonPath = join10(homedir4(), ".openclaw", "hooks.json");
7511
- const markerPath = join10(target.path, "prismer.json");
7512
- const jsonMarkerPresent = existsSync11(jsonPath) && fileContainsPrismerMarker(jsonPath);
7513
- const dirMarkerPresent = existsSync11(markerPath) && fileContainsPrismerMarker(markerPath);
8093
+ const jsonPath = join11(homedir4(), ".openclaw", "hooks.json");
8094
+ const markerPath = join11(target.path, "prismer.json");
8095
+ const jsonMarkerPresent = existsSync12(jsonPath) && fileContainsPrismerMarker(jsonPath);
8096
+ const dirMarkerPresent = existsSync12(markerPath) && fileContainsPrismerMarker(markerPath);
7514
8097
  return {
7515
8098
  supported: true,
7516
8099
  kind: "json-file-or-directory",
7517
8100
  path: target.path,
7518
8101
  jsonPath,
7519
- exists: existsSync11(target.path) || existsSync11(jsonPath),
8102
+ exists: existsSync12(target.path) || existsSync12(jsonPath),
7520
8103
  markerPath,
7521
8104
  markerPresent: jsonMarkerPresent || dirMarkerPresent
7522
8105
  };
7523
8106
  }
7524
8107
  if (target.kind === "directory") {
7525
- const markerPath = join10(target.path, "prismer.json");
8108
+ const markerPath = join11(target.path, "prismer.json");
7526
8109
  return {
7527
8110
  supported: true,
7528
8111
  kind: target.kind,
7529
8112
  path: target.path,
7530
- exists: existsSync11(target.path),
8113
+ exists: existsSync12(target.path),
7531
8114
  markerPath,
7532
- markerPresent: existsSync11(markerPath) && fileContainsPrismerMarker(markerPath)
8115
+ markerPresent: existsSync12(markerPath) && fileContainsPrismerMarker(markerPath)
7533
8116
  };
7534
8117
  }
7535
- const exists = existsSync11(target.path);
8118
+ const exists = existsSync12(target.path);
7536
8119
  if (!exists) {
7537
8120
  return {
7538
8121
  supported: true,
@@ -7568,14 +8151,14 @@ function prismerHookMarker(spec) {
7568
8151
  }
7569
8152
  function mergeHookJson(path7, spec) {
7570
8153
  const marker = prismerHookMarker(spec);
7571
- if (!existsSync11(path7)) {
8154
+ if (!existsSync12(path7)) {
7572
8155
  return {
7573
8156
  prismer: marker
7574
8157
  };
7575
8158
  }
7576
8159
  let parsed;
7577
8160
  try {
7578
- parsed = JSON.parse(readFileSync8(path7, "utf8"));
8161
+ parsed = JSON.parse(readFileSync9(path7, "utf8"));
7579
8162
  } catch (err) {
7580
8163
  throw new Error(`Cannot parse ${path7} as JSON: ${err.message}`);
7581
8164
  }
@@ -7599,7 +8182,7 @@ function mergeHookJson(path7, spec) {
7599
8182
  return next;
7600
8183
  }
7601
8184
  function backupIfExists(path7) {
7602
- if (!existsSync11(path7)) return null;
8185
+ if (!existsSync12(path7)) return null;
7603
8186
  const suffix = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7604
8187
  const backup = `${path7}.bak.${suffix}`;
7605
8188
  const stat = statSync2(path7);
@@ -7609,7 +8192,7 @@ function backupIfExists(path7) {
7609
8192
  }
7610
8193
  function fileContainsPrismerMarker(path7) {
7611
8194
  try {
7612
- return readFileSync8(path7, "utf8").includes("prismer-daemon-runtime");
8195
+ return readFileSync9(path7, "utf8").includes("prismer-daemon-runtime");
7613
8196
  } catch {
7614
8197
  return false;
7615
8198
  }
@@ -7672,6 +8255,8 @@ function clearInstallInConfig(name) {
7672
8255
  // src/cli/commands/agent.ts
7673
8256
  import { Command as Command2 } from "commander";
7674
8257
  import { spawnSync as spawnSync3 } from "child_process";
8258
+ init_util();
8259
+ init_ui();
7675
8260
  var ADAPTER_BINARY = {
7676
8261
  "claude-code": "claude",
7677
8262
  codex: "codex",
@@ -8051,8 +8636,10 @@ function whichBinary2(bin) {
8051
8636
 
8052
8637
  // src/cli/commands/asset.ts
8053
8638
  import { Command as Command3 } from "commander";
8054
- import { existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
8639
+ import { existsSync as existsSync13, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
8055
8640
  import { basename as basename2 } from "path";
8641
+ init_util();
8642
+ init_ui();
8056
8643
  function buildAssetCommand() {
8057
8644
  const cmd = new Command3("asset").description("Inspect IM assets");
8058
8645
  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) => {
@@ -8123,9 +8710,9 @@ function mkCloud() {
8123
8710
  return new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
8124
8711
  }
8125
8712
  async function uploadAsset(file, opts) {
8126
- if (!existsSync12(file)) exitWithError(`file not found: ${file}`);
8713
+ if (!existsSync13(file)) exitWithError(`file not found: ${file}`);
8127
8714
  const cfg = loadConfig(resolvePaths());
8128
- const bytes = readFileSync9(file);
8715
+ const bytes = readFileSync10(file);
8129
8716
  const metadata = parseMetadata(opts.metadata);
8130
8717
  if (opts.taskId && metadata.taskId === void 0) metadata.taskId = opts.taskId;
8131
8718
  if (opts.containerId && metadata.containerId === void 0) metadata.containerId = opts.containerId;
@@ -8161,7 +8748,7 @@ async function downloadAsset(assetId, outPath) {
8161
8748
  exitWithError(`asset download failed (${res.status}): ${errorMessage(body)}`);
8162
8749
  }
8163
8750
  const bytes = Buffer.from(await res.arrayBuffer());
8164
- writeFileSync7(outPath, bytes);
8751
+ writeFileSync8(outPath, bytes);
8165
8752
  }
8166
8753
  function parseMetadata(raw) {
8167
8754
  if (!raw) return {};
@@ -8248,6 +8835,7 @@ function stringField(obj, key) {
8248
8835
  }
8249
8836
 
8250
8837
  // src/cli/commands/banner.ts
8838
+ init_util();
8251
8839
  import { Command as Command4 } from "commander";
8252
8840
  function buildBannerCommand() {
8253
8841
  return new Command4("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) => {
@@ -8257,6 +8845,8 @@ function buildBannerCommand() {
8257
8845
 
8258
8846
  // src/cli/commands/chat.ts
8259
8847
  import { Command as Command5 } from "commander";
8848
+ init_util();
8849
+ init_ui();
8260
8850
  function buildChatCommand() {
8261
8851
  const cmd = new Command5("chat").description("Use IM chat and group APIs");
8262
8852
  cmd.command("me").description("Show the current IM identity").option("--json", "Print raw JSON response").action(async (opts) => {
@@ -8431,6 +9021,8 @@ function sanitizeError(message) {
8431
9021
 
8432
9022
  // src/cli/commands/config.ts
8433
9023
  import { Command as Command6 } from "commander";
9024
+ init_util();
9025
+ init_ui();
8434
9026
  var SETTABLE_KEYS = ["cloud_api_base", "api_key", "daemon_id"];
8435
9027
  function redactApiKey(key) {
8436
9028
  if (!key.startsWith("sk-prismer-")) return "***";
@@ -8526,6 +9118,8 @@ function buildConfigCommand() {
8526
9118
 
8527
9119
  // src/cli/commands/cookbook.ts
8528
9120
  import { Command as Command7 } from "commander";
9121
+ init_util();
9122
+ init_ui();
8529
9123
  function buildCookbookCommand() {
8530
9124
  const cmd = new Command7("cookbook").description("Run CLI-only 54release MVP regression suites");
8531
9125
  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) => {
@@ -8783,9 +9377,11 @@ function parsePositiveInt2(value) {
8783
9377
  // src/cli/commands/daemon.ts
8784
9378
  import { Command as Command8 } from "commander";
8785
9379
  import { spawn as spawn5 } from "child_process";
8786
- import { createReadStream, existsSync as existsSync13, mkdirSync as mkdirSync8, openSync, statSync as statSync3 } from "fs";
9380
+ import { createReadStream, existsSync as existsSync14, mkdirSync as mkdirSync9, openSync, statSync as statSync3 } from "fs";
8787
9381
  import { setTimeout as sleep2 } from "timers/promises";
8788
- import { join as join11 } from "path";
9382
+ import { join as join12 } from "path";
9383
+ init_util();
9384
+ init_ui();
8789
9385
  function buildDaemonCommand() {
8790
9386
  const cmd = new Command8("daemon").description("Manage the prismer daemon process");
8791
9387
  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) => {
@@ -8857,8 +9453,8 @@ function buildDaemonCommand() {
8857
9453
  if (existingPid && pidAlive(existingPid)) {
8858
9454
  exitWithError(`Daemon already running (pid ${existingPid}). Use \`prismer daemon stop\` first.`);
8859
9455
  }
8860
- if (!existsSync13(paths.logsDir)) mkdirSync8(paths.logsDir, { recursive: true });
8861
- const logFile = join11(paths.logsDir, "daemon.log");
9456
+ if (!existsSync14(paths.logsDir)) mkdirSync9(paths.logsDir, { recursive: true });
9457
+ const logFile = join12(paths.logsDir, "daemon.log");
8862
9458
  const fd = openSync(logFile, "a");
8863
9459
  const args = [process.argv[1], "daemon", "run"];
8864
9460
  if (opts.port) args.push("--port", String(opts.port));
@@ -8942,8 +9538,8 @@ function buildDaemonCommand() {
8942
9538
  });
8943
9539
  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) => {
8944
9540
  const paths = resolvePaths();
8945
- const logFile = join11(paths.logsDir, "daemon.log");
8946
- if (!existsSync13(logFile)) {
9541
+ const logFile = join12(paths.logsDir, "daemon.log");
9542
+ if (!existsSync14(logFile)) {
8947
9543
  exitWithError(`No daemon log found at ${logFile}. Start the daemon with \`prismer daemon start\`.`);
8948
9544
  }
8949
9545
  const lines = Math.max(1, opts.tail);
@@ -8997,16 +9593,17 @@ async function followFile(path7) {
8997
9593
  }
8998
9594
 
8999
9595
  // src/cli/commands/events.ts
9596
+ init_util();
9000
9597
  import { Command as Command9 } from "commander";
9001
- import { createReadStream as createReadStream2, existsSync as existsSync14 } from "fs";
9598
+ import { createReadStream as createReadStream2, existsSync as existsSync15 } from "fs";
9002
9599
  import { homedir as homedir5 } from "os";
9003
- import { join as join12 } from "path";
9600
+ import { join as join13 } from "path";
9004
9601
  import { createInterface } from "readline";
9005
- var DEFAULT_LIMIT = 50;
9602
+ var DEFAULT_LIMIT2 = 50;
9006
9603
  function buildEventsCommand() {
9007
9604
  return addEventOptions(new Command9("events").description("Read local PARA events from ~/.prismer/para/events.jsonl")).action(async (opts) => {
9008
9605
  const file = eventsPath();
9009
- if (!existsSync14(file)) {
9606
+ if (!existsSync15(file)) {
9010
9607
  printJson(unavailable(file));
9011
9608
  process.exitCode = 1;
9012
9609
  return;
@@ -9023,7 +9620,7 @@ function buildEventsCommand() {
9023
9620
  function buildEventsStatsCommand() {
9024
9621
  return addEventOptions(new Command9("events:stats").description("Summarize local PARA events from ~/.prismer/para/events.jsonl")).action(async (opts) => {
9025
9622
  const file = eventsPath();
9026
- if (!existsSync14(file)) {
9623
+ if (!existsSync15(file)) {
9027
9624
  printJson(unavailable(file));
9028
9625
  process.exitCode = 1;
9029
9626
  return;
@@ -9038,10 +9635,10 @@ function buildEventsStatsCommand() {
9038
9635
  });
9039
9636
  }
9040
9637
  function addEventOptions(cmd) {
9041
- 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)");
9638
+ 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)");
9042
9639
  }
9043
9640
  function eventsPath() {
9044
- return join12(process.env.PRISMER_HOME ?? join12(homedir5(), ".prismer"), "para", "events.jsonl");
9641
+ return join13(process.env.PRISMER_HOME ?? join13(homedir5(), ".prismer"), "para", "events.jsonl");
9045
9642
  }
9046
9643
  function unavailable(file) {
9047
9644
  return {
@@ -9109,7 +9706,7 @@ function bump(map, key) {
9109
9706
  }
9110
9707
  function normalizeFilters(opts) {
9111
9708
  return {
9112
- limit: Math.max(1, Math.min(1e4, Number.isFinite(opts.limit) ? opts.limit : DEFAULT_LIMIT)),
9709
+ limit: Math.max(1, Math.min(1e4, Number.isFinite(opts.limit) ? opts.limit : DEFAULT_LIMIT2)),
9113
9710
  agentId: opts.agentId,
9114
9711
  sessionId: opts.sessionId,
9115
9712
  family: opts.family,
@@ -9121,13 +9718,14 @@ function cleanFilters(filters) {
9121
9718
  }
9122
9719
  function parsePositiveInt3(v) {
9123
9720
  const n = Number.parseInt(v, 10);
9124
- return Number.isFinite(n) && n > 0 ? n : DEFAULT_LIMIT;
9721
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_LIMIT2;
9125
9722
  }
9126
9723
 
9127
9724
  // src/cli/commands/memory.ts
9128
9725
  import Database4 from "better-sqlite3";
9129
9726
  import { Command as Command10 } from "commander";
9130
- import { existsSync as existsSync15 } from "fs";
9727
+ import { existsSync as existsSync16 } from "fs";
9728
+ init_util();
9131
9729
  var LOCAL_BASE = process.env.PRISMER_DAEMON_URL ?? "http://127.0.0.1:3210";
9132
9730
  function buildMemoryCommand() {
9133
9731
  const cmd = new Command10("memory").description("Inspect local daemon memory/cache state");
@@ -9219,7 +9817,7 @@ async function tryDaemon(methods, paths) {
9219
9817
  try {
9220
9818
  const res = await fetch(`${LOCAL_BASE}${path7}`, { method, signal: AbortSignal.timeout(1500) });
9221
9819
  if (res.status === 404) continue;
9222
- const body = await readJson3(res);
9820
+ const body = await readJson4(res);
9223
9821
  if (!res.ok) {
9224
9822
  return {
9225
9823
  ok: false,
@@ -9238,7 +9836,7 @@ async function tryDaemon(methods, paths) {
9238
9836
  }
9239
9837
  return void 0;
9240
9838
  }
9241
- async function readJson3(res) {
9839
+ async function readJson4(res) {
9242
9840
  const text = await res.text();
9243
9841
  if (!text) return null;
9244
9842
  try {
@@ -9278,7 +9876,7 @@ function readCacheSnapshot(limit) {
9278
9876
  const paths = resolvePaths();
9279
9877
  const empty = {
9280
9878
  dbPath: paths.localDb,
9281
- dbExists: existsSync15(paths.localDb),
9879
+ dbExists: existsSync16(paths.localDb),
9282
9880
  tables: {
9283
9881
  cached_assets: { exists: false, count: 0, sizeBytes: 0 },
9284
9882
  workspace_files_mirror: { exists: false, count: 0 }
@@ -9332,9 +9930,9 @@ function readCacheSnapshot(limit) {
9332
9930
  db?.close();
9333
9931
  }
9334
9932
  }
9335
- function tableExists(db, table) {
9336
- const row = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
9337
- return row?.name === table;
9933
+ function tableExists(db, table2) {
9934
+ const row = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table2);
9935
+ return row?.name === table2;
9338
9936
  }
9339
9937
  function assetRow(row) {
9340
9938
  const hash = String(row.content_hash ?? "");
@@ -9398,6 +9996,8 @@ function messageFromBody(body) {
9398
9996
 
9399
9997
  // src/cli/commands/pair.ts
9400
9998
  import { Command as Command11 } from "commander";
9999
+ init_util();
10000
+ init_ui();
9401
10001
  function buildPairCommand() {
9402
10002
  return new Command11("pair").description("Legacy QR approval path; use `prismer setup` to bind this runtime").option(
9403
10003
  "--cloud <url>",
@@ -9440,10 +10040,11 @@ function buildPairCommand() {
9440
10040
 
9441
10041
  // src/cli/commands/profile.ts
9442
10042
  import { Command as Command12 } from "commander";
9443
- import { existsSync as existsSync16, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
10043
+ import { existsSync as existsSync17, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
9444
10044
  import { tmpdir } from "os";
9445
- import { join as join13 } from "path";
10045
+ import { join as join14 } from "path";
9446
10046
  import { spawnSync as spawnSync4 } from "child_process";
10047
+ init_util();
9447
10048
  function buildProfileCommand() {
9448
10049
  const cmd = new Command12("profile").description("Manage AgentProfile (per-agent adapter config)");
9449
10050
  cmd.command("templates").description("List built-in role templates (PM / Engineer / CEO \u2026)").option("--json", "Output JSON (default)").action(() => {
@@ -9454,7 +10055,7 @@ function buildProfileCommand() {
9454
10055
  const data = await cloud.get(`/api/im/agent_profiles?agentId=${encodeURIComponent(opts.agent)}`);
9455
10056
  printJson(data);
9456
10057
  }, { code: "profile_list_failed" }));
9457
- 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) => {
10058
+ 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) => {
9458
10059
  let configObj = {};
9459
10060
  let adapterName = opts.adapter ?? "hermes";
9460
10061
  if (opts.fromTemplate) {
@@ -9467,6 +10068,9 @@ function buildProfileCommand() {
9467
10068
  const inline = readJsonArg(opts.config);
9468
10069
  configObj = { ...configObj, ...inline };
9469
10070
  }
10071
+ if (opts.model) {
10072
+ configObj.model = opts.model;
10073
+ }
9470
10074
  const cloud = mkCloud4();
9471
10075
  const wsId = opts.workspaceId ?? await resolveDefaultWorkspaceId(cloud);
9472
10076
  const res = await cloud.request("POST", "/api/im/agent_profiles", {
@@ -9486,12 +10090,12 @@ function buildProfileCommand() {
9486
10090
  const profile = await cloud.get(
9487
10091
  `/api/im/agent_profiles/${encodeURIComponent(profileId)}`
9488
10092
  );
9489
- const tmpFile = join13(tmpdir(), `prismer-profile-${profileId}.json`);
9490
- writeFileSync8(tmpFile, JSON.stringify(profile.config, null, 2), "utf8");
10093
+ const tmpFile = join14(tmpdir(), `prismer-profile-${profileId}.json`);
10094
+ writeFileSync9(tmpFile, JSON.stringify(profile.config, null, 2), "utf8");
9491
10095
  const editor = process.env.EDITOR || "vi";
9492
10096
  const ed = spawnSync4(editor, [tmpFile], { stdio: "inherit" });
9493
10097
  if (ed.status !== 0) exitWithError(`editor exited ${ed.status}`, { code: "editor_failed" });
9494
- const newConfig = JSON.parse(readFileSync10(tmpFile, "utf8"));
10098
+ const newConfig = JSON.parse(readFileSync11(tmpFile, "utf8"));
9495
10099
  const res = await cloud.request("PATCH", `/api/im/agent_profiles/${encodeURIComponent(profileId)}`, {
9496
10100
  body: { config: newConfig, version: profile.version }
9497
10101
  });
@@ -9513,8 +10117,8 @@ function mkCloud4() {
9513
10117
  function readJsonArg(arg) {
9514
10118
  if (arg.startsWith("@")) {
9515
10119
  const path7 = arg.slice(1);
9516
- if (!existsSync16(path7)) throw new Error(`File not found: ${path7}`);
9517
- return JSON.parse(readFileSync10(path7, "utf8"));
10120
+ if (!existsSync17(path7)) throw new Error(`File not found: ${path7}`);
10121
+ return JSON.parse(readFileSync11(path7, "utf8"));
9518
10122
  }
9519
10123
  return JSON.parse(arg);
9520
10124
  }
@@ -9527,6 +10131,8 @@ async function resolveDefaultWorkspaceId(cloud) {
9527
10131
 
9528
10132
  // src/cli/commands/sandbox.ts
9529
10133
  import { Command as Command13 } from "commander";
10134
+ init_util();
10135
+ init_ui();
9530
10136
  function buildSandboxCommand() {
9531
10137
  const cmd = new Command13("sandbox").description("Inspect and smoke-test sandbox lifecycle");
9532
10138
  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) => {
@@ -9728,11 +10334,13 @@ async function safeParseResponse(res) {
9728
10334
 
9729
10335
  // src/cli/commands/setup.ts
9730
10336
  import { Command as Command14 } from "commander";
9731
- import { hostname as hostname2 } from "os";
10337
+ import { hostname as hostname3 } from "os";
9732
10338
  import { spawn as spawn6 } from "child_process";
9733
10339
  import { randomBytes } from "crypto";
9734
- import { existsSync as existsSync17, renameSync } from "fs";
10340
+ import { existsSync as existsSync18, renameSync } from "fs";
9735
10341
  import { createServer as createServer2 } from "http";
10342
+ init_util();
10343
+ init_ui();
9736
10344
  function buildSetupCommand() {
9737
10345
  return new Command14("setup").description("Set up this local runtime and bind it to Prismer Cloud").argument("[api-key]", "Prismer daemon API key; primarily for manual recovery and automation").option(
9738
10346
  "--cloud <url>",
@@ -9755,6 +10363,7 @@ function buildSetupCommand() {
9755
10363
  getUI().blank();
9756
10364
  }
9757
10365
  const shouldStart = opts.start !== false;
10366
+ stopRunningDaemon(paths);
9758
10367
  if (opts.pair || opts.asUser) {
9759
10368
  if (!opts.json) warn("Legacy pair setup path", "plain `prismer setup --start` is the canonical runtime binding flow");
9760
10369
  if (opts.asUser && process.env.LOCAL_ONLY !== "1") {
@@ -9771,7 +10380,7 @@ function buildSetupCommand() {
9771
10380
  }
9772
10381
  const result = await pair({
9773
10382
  cloudBaseUrl,
9774
- deviceName: opts.deviceName ?? hostname2(),
10383
+ deviceName: opts.deviceName ?? hostname3(),
9775
10384
  force: opts.force,
9776
10385
  paths,
9777
10386
  asUserEmail: opts.asUser
@@ -9788,13 +10397,13 @@ function buildSetupCommand() {
9788
10397
  apiKey = await mintDaemonApiKey({
9789
10398
  cloudBaseUrl,
9790
10399
  token: authToken,
9791
- deviceName: opts.deviceName ?? hostname2()
10400
+ deviceName: opts.deviceName ?? hostname3()
9792
10401
  });
9793
10402
  }
9794
10403
  if (!apiKey && !authToken && !opts.check && opts.browser !== false) {
9795
10404
  apiKey = await runBrowserSetup({
9796
10405
  cloudBaseUrl,
9797
- deviceName: opts.deviceName ?? hostname2(),
10406
+ deviceName: opts.deviceName ?? hostname3(),
9798
10407
  json: Boolean(opts.json)
9799
10408
  });
9800
10409
  }
@@ -9975,11 +10584,33 @@ function startDaemonDetached(home) {
9975
10584
  });
9976
10585
  child.unref();
9977
10586
  }
10587
+ function stopRunningDaemon(paths) {
10588
+ const pid = readPidFile(paths);
10589
+ if (!pid || !pidAlive(pid)) {
10590
+ if (pid) clearPidFile(paths);
10591
+ return;
10592
+ }
10593
+ try {
10594
+ process.kill(pid, "SIGTERM");
10595
+ } catch {
10596
+ return;
10597
+ }
10598
+ const deadline = Date.now() + 5e3;
10599
+ while (Date.now() < deadline) {
10600
+ if (!pidAlive(pid)) {
10601
+ clearPidFile(paths);
10602
+ return;
10603
+ }
10604
+ const start = Date.now();
10605
+ while (Date.now() - start < 200) {
10606
+ }
10607
+ }
10608
+ }
9978
10609
  function shouldArchiveLocalDb(previous, next) {
9979
10610
  return previous.api_key !== next.api_key || previous.cloud_api_base !== next.cloud_api_base || previous.daemon_id !== next.daemon_id;
9980
10611
  }
9981
10612
  function archiveLocalDb(localDbPath) {
9982
- if (!existsSync17(localDbPath)) return;
10613
+ if (!existsSync18(localDbPath)) return;
9983
10614
  const archived = `${localDbPath}.${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
9984
10615
  renameSync(localDbPath, archived);
9985
10616
  }
@@ -10002,6 +10633,8 @@ async function mintDaemonApiKey(input) {
10002
10633
 
10003
10634
  // src/cli/commands/status.ts
10004
10635
  import { Command as Command15 } from "commander";
10636
+ init_util();
10637
+ init_ui();
10005
10638
  function buildStatusCommand() {
10006
10639
  return new Command15("status").description("Show daemon + config + cloud status").option("--json", "Output machine-readable JSON").action(async (opts) => {
10007
10640
  const paths = resolvePaths();
@@ -10028,10 +10661,34 @@ function buildStatusCommand() {
10028
10661
  const cloud = new CloudClient({ baseUrl: cfg.cloud_api_base, apiKey: cfg.api_key });
10029
10662
  let cloudOk = false;
10030
10663
  let me = null;
10664
+ let devices = null;
10665
+ let agents = null;
10031
10666
  try {
10032
- const res = await cloud.request("GET", "/api/im/me", { timeoutMs: 3e3 });
10033
- cloudOk = res.ok;
10034
- me = res.data ?? null;
10667
+ const meRes = await cloud.request("GET", "/api/im/me", { timeoutMs: 3e3 });
10668
+ cloudOk = meRes.ok;
10669
+ me = meRes.data ?? null;
10670
+ if (cloudOk) {
10671
+ const wsRes = await cloud.request("GET", "/api/im/workspaces", { timeoutMs: 3e3 });
10672
+ if (wsRes.ok) {
10673
+ const wsBody = wsRes.data;
10674
+ const wsList = wsBody?.data;
10675
+ if (Array.isArray(wsList) && wsList.length > 0) {
10676
+ const wsId = wsList[0]?.id;
10677
+ if (wsId) {
10678
+ const devRes = await cloud.request("GET", `/api/workspace/runtime-installations?workspaceId=${encodeURIComponent(wsId)}&includeStopped=false`, { timeoutMs: 3e3 });
10679
+ if (devRes.ok) {
10680
+ const devBody = devRes.data;
10681
+ devices = devBody?.data;
10682
+ }
10683
+ const agRes = await cloud.request("GET", `/api/im/workspaces/${encodeURIComponent(wsId)}/agents`, { timeoutMs: 3e3 });
10684
+ if (agRes.ok) {
10685
+ const agBody = agRes.data;
10686
+ agents = agBody?.data;
10687
+ }
10688
+ }
10689
+ }
10690
+ }
10691
+ }
10035
10692
  } catch {
10036
10693
  cloudOk = false;
10037
10694
  }
@@ -10043,10 +10700,15 @@ function buildStatusCommand() {
10043
10700
  daemon: {
10044
10701
  running: daemonRunning,
10045
10702
  pid: daemonStatus.pid ?? pid ?? null,
10046
- wsConnected: daemonStatus.wsConnected ?? null
10703
+ wsConnected: daemonStatus.wsConnected ?? null,
10704
+ info: daemonStatus.info ?? {}
10047
10705
  },
10048
- cloud: { base: cfg.cloud_api_base, reachable: cloudOk, me },
10049
- local
10706
+ cloud: { base: cfg.cloud_api_base, reachable: cloudOk, me, devices, agents },
10707
+ local,
10708
+ binding: {
10709
+ daemonId: cfg.daemon_id,
10710
+ apiKey: cfg.api_key
10711
+ }
10050
10712
  };
10051
10713
  if (opts.json) {
10052
10714
  printJson(report);
@@ -10060,12 +10722,19 @@ async function readDaemonStatus() {
10060
10722
  const res = await fetch("http://127.0.0.1:3210/healthz", {
10061
10723
  signal: AbortSignal.timeout(1e3)
10062
10724
  });
10063
- if (!res.ok) return { running: false };
10064
- const data = await res.json();
10065
- return { running: true, pid: data.pid, wsConnected: data.wsConnected };
10725
+ if (res.ok) {
10726
+ const data = await res.json();
10727
+ return { running: true, pid: data.pid, wsConnected: data.wsConnected, info: data };
10728
+ }
10066
10729
  } catch {
10067
- return { running: false };
10068
10730
  }
10731
+ const paths = resolvePaths();
10732
+ const pid = readPidFile(paths);
10733
+ if (pid) {
10734
+ const { pidAlive: pidAlive2 } = await Promise.resolve().then(() => (init_util(), util_exports));
10735
+ if (pidAlive2(pid)) return { running: true, pid };
10736
+ }
10737
+ return { running: false };
10069
10738
  }
10070
10739
  function readLocalCounts(localDbPath) {
10071
10740
  try {
@@ -10091,21 +10760,59 @@ function printPretty(report) {
10091
10760
  ui.blank();
10092
10761
  ok("Config", report.paths.config);
10093
10762
  if (report.daemon.running) {
10094
- ok("Daemon", `pid=${report.daemon.pid ?? "?"} ws=${report.daemon.wsConnected ? "connected" : "pending"}`);
10763
+ const ws = report.daemon.wsConnected ? "connected" : "pending";
10764
+ ok("Daemon", `pid=${report.daemon.pid} ws=${ws}`);
10765
+ if (report.daemon.info) {
10766
+ const info2 = report.daemon.info;
10767
+ if (info2.version) ui.line(` Version: ${info2.version}`);
10768
+ if (info2.uptime) ui.line(` Uptime: ${Math.round(info2.uptime / 60)}m`);
10769
+ if (info2.memoryMb) ui.line(` Memory: ${info2.memoryMb} MB`);
10770
+ }
10095
10771
  } else {
10096
10772
  warn("Daemon", "not running");
10097
10773
  tip("prismer daemon start");
10098
10774
  }
10099
- if (report.cloud.reachable) ok("Cloud", report.cloud.base);
10100
- else {
10775
+ if (report.cloud.reachable) {
10776
+ ok("Cloud", report.cloud.base);
10777
+ const me = report.cloud.me;
10778
+ if (me?.user) {
10779
+ const roleTag = me.user.role ? ` role=${me.user.role}` : "";
10780
+ ui.line(` Account: ${me.user.displayName ?? me.user.username ?? "?"}${roleTag}`);
10781
+ }
10782
+ if (me?.credits) {
10783
+ ui.line(` Credits: ${typeof me.credits.balance === "number" ? me.credits.balance.toLocaleString() : "?"}`);
10784
+ }
10785
+ } else {
10101
10786
  fail2("Cloud", `${report.cloud.base} unreachable or unauthorized`);
10102
10787
  tip("prismer setup --force");
10103
10788
  }
10789
+ if (report.binding) {
10790
+ ui.blank();
10791
+ ui.line(` Daemon ID: ${report.binding.daemonId}`);
10792
+ const masked = report.binding.apiKey.slice(0, 14) + "\u2022\u2022\u2022\u2022" + report.binding.apiKey.slice(-4);
10793
+ ui.line(` API Key: ${masked}`);
10794
+ }
10795
+ if (Array.isArray(report.cloud.devices) && report.cloud.devices.length > 0) {
10796
+ const devs = report.cloud.devices;
10797
+ ui.blank();
10798
+ ui.line(` Workspace Devices (${devs.length}):`);
10799
+ for (const d of devs) {
10800
+ const statusIcon = d.daemonStatus === "connected" ? "\u25CF" : "\u25CB";
10801
+ const kind = d.runtimeKind === "docker" ? "Local" : "K8s";
10802
+ const declared = d.hostedAgentSummary?.declared ?? 0;
10803
+ ui.line(` ${statusIcon} ${d.podName?.replace(/^daemon:/, "")?.slice(0, 28) ?? "?"} kind=${kind} agents=${declared}`);
10804
+ }
10805
+ } else {
10806
+ ui.line(` Workspace Devices: none`);
10807
+ }
10808
+ if (Array.isArray(report.cloud.agents) && report.cloud.agents.length > 0) {
10809
+ ui.line(` Hosted agents: ${report.cloud.agents.length}`);
10810
+ }
10104
10811
  if (report.local) {
10105
10812
  ui.blank();
10106
- ui.line(` Agents: ${report.local.agents}`);
10107
- ui.line(` Profiles: ${report.local.profiles}`);
10108
- ui.line(` Tasks: ${report.local.runningTasks} running locally`);
10813
+ ui.line(` Local agents: ${report.local.agents}`);
10814
+ ui.line(` Profiles: ${report.local.profiles}`);
10815
+ ui.line(` Running tasks: ${report.local.runningTasks}`);
10109
10816
  } else {
10110
10817
  warn("Local DB", "unavailable");
10111
10818
  }
@@ -10114,6 +10821,7 @@ function printPretty(report) {
10114
10821
  // src/cli/commands/task.ts
10115
10822
  import { Command as Command16 } from "commander";
10116
10823
  import { setTimeout as sleep3 } from "timers/promises";
10824
+ init_util();
10117
10825
  function describeStatus(status) {
10118
10826
  return status === 0 ? "network error" : `HTTP ${status}`;
10119
10827
  }
@@ -10233,6 +10941,8 @@ function taskFrom2(raw) {
10233
10941
 
10234
10942
  // src/cli/commands/workspace.ts
10235
10943
  import { Command as Command17 } from "commander";
10944
+ init_util();
10945
+ init_ui();
10236
10946
  function buildWorkspaceCommand() {
10237
10947
  const cmd = new Command17("workspace").description("Manage workspaces, runtime snapshots, and workspace files");
10238
10948
  cmd.command("list").description("List workspaces").option("--json", "Output machine-readable JSON").action(runAction(async (opts) => {
@@ -10467,6 +11177,7 @@ async function readResponseError(res) {
10467
11177
  }
10468
11178
 
10469
11179
  // src/cli/index.ts
11180
+ init_ui();
10470
11181
  var VERSION = "1.9.7";
10471
11182
  function buildProgram() {
10472
11183
  const program = new Command18("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);