@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/CHANGELOG.md +24 -0
- package/dist/cli.cjs +1634 -923
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1459 -746
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1667 -962
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -1
- package/dist/index.d.ts +90 -1
- package/dist/index.js +2127 -1416
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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:
|
|
1289
|
+
const { join: join15, dirname: dirname8 } = __require("path");
|
|
687
1290
|
const here = dirname8(fileURLToPath2(import.meta.url));
|
|
688
|
-
const candidate =
|
|
1291
|
+
const candidate = join15(here, "../../mcp/dist/index.js");
|
|
689
1292
|
if (existsSync(candidate)) return candidate;
|
|
690
|
-
const candidate2 =
|
|
1293
|
+
const candidate2 = join15(here, "../../../mcp/dist/index.js");
|
|
691
1294
|
if (existsSync(candidate2)) return candidate2;
|
|
692
|
-
const candidate3 =
|
|
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 =
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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 {
|
|
2170
|
+
import { hostname } from "os";
|
|
1542
2171
|
var PREFIX = "daemon-";
|
|
1543
2172
|
function newDaemonId() {
|
|
1544
|
-
|
|
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
|
|
2552
|
-
return `${
|
|
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
|
|
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
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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_${
|
|
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,
|
|
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
|
|
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_${
|
|
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_${
|
|
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_${
|
|
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
|
|
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
|
|
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
|
|
6268
|
+
return respond4002(res, "workspaceId is required (string)");
|
|
5214
6269
|
}
|
|
5215
|
-
if (!
|
|
5216
|
-
return
|
|
6270
|
+
if (typeof b.query !== "string" || !b.query.trim()) {
|
|
6271
|
+
return respond4002(res, "query is required (non-empty string)");
|
|
5217
6272
|
}
|
|
5218
|
-
const
|
|
5219
|
-
|
|
5220
|
-
|
|
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
|
|
5223
|
-
const
|
|
5224
|
-
|
|
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
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
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
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
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
|
-
|
|
6030
|
-
|
|
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
|
-
|
|
6069
|
-
|
|
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
|
|
6120
|
-
this.
|
|
6121
|
-
|
|
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
|
-
|
|
6124
|
-
} catch
|
|
6125
|
-
this.emit("sync-error", err);
|
|
6901
|
+
entry.ctrl.abort();
|
|
6902
|
+
} catch {
|
|
6126
6903
|
}
|
|
6127
6904
|
}
|
|
6128
|
-
|
|
6129
|
-
this.
|
|
6130
|
-
|
|
6131
|
-
|
|
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.
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
return;
|
|
6910
|
+
if (this.heartbeatTimer) {
|
|
6911
|
+
clearInterval(this.heartbeatTimer);
|
|
6912
|
+
this.heartbeatTimer = void 0;
|
|
6144
6913
|
}
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
);
|
|
6149
|
-
|
|
6150
|
-
this.
|
|
6151
|
-
|
|
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
|
-
|
|
6158
|
-
|
|
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
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
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
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
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
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
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
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
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
|
-
|
|
6245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7082
|
+
payload = validateStaticHostedAgent(JSON.parse(raw));
|
|
6398
7083
|
} catch (err) {
|
|
6399
|
-
|
|
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]
|
|
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
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
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
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
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
|
-
|
|
6521
|
-
|
|
6522
|
-
"
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
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
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
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
|
-
|
|
6553
|
-
"
|
|
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
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
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 (
|
|
6570
|
-
|
|
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
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
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
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
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
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
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
|
-
|
|
6706
|
-
|
|
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
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
const
|
|
6712
|
-
|
|
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
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
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
|
-
|
|
6730
|
-
|
|
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
|
-
|
|
6735
|
-
if (
|
|
6736
|
-
const
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
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
|
-
}
|
|
6759
|
-
|
|
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
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
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
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
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
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
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
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
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
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
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
|
|
6935
|
-
|
|
6936
|
-
|
|
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
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
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 "
|
|
7010
|
-
|
|
7489
|
+
case "workspace.update":
|
|
7490
|
+
method = "PATCH";
|
|
7491
|
+
path7 = `/api/im/workspaces/${id}`;
|
|
7011
7492
|
break;
|
|
7012
|
-
case "
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
if (arg === "--json") rest.push(arg);
|
|
7493
|
+
case "workspace.delete":
|
|
7494
|
+
method = "DELETE";
|
|
7495
|
+
path7 = `/api/im/workspaces/${id}`;
|
|
7016
7496
|
break;
|
|
7017
|
-
|
|
7018
|
-
|
|
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
|
-
|
|
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/
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
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
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
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
|
-
|
|
7078
|
-
|
|
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
|
|
7081
|
-
|
|
7082
|
-
new
|
|
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
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
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
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
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
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
}
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
}
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
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
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
return
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
7405
|
-
const markerPath =
|
|
7987
|
+
mkdirSync8(planned.path, { recursive: true });
|
|
7988
|
+
const markerPath = join11(planned.path, "prismer.json");
|
|
7406
7989
|
backup = backupIfExists(markerPath);
|
|
7407
|
-
|
|
7990
|
+
writeFileSync7(markerPath, JSON.stringify(prismerHookMarker(spec), null, 2) + "\n", "utf8");
|
|
7408
7991
|
} else {
|
|
7409
|
-
|
|
7992
|
+
mkdirSync8(dirname7(planned.path), { recursive: true });
|
|
7410
7993
|
backup = backupIfExists(planned.path);
|
|
7411
7994
|
const merged = mergeHookJson(planned.path, spec);
|
|
7412
|
-
|
|
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 =
|
|
8062
|
+
const envFile = join11(homedir4(), ".hermes", ".env");
|
|
7480
8063
|
return {
|
|
7481
|
-
ok:
|
|
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
|
-
...
|
|
8067
|
+
...existsSync12(envFile) ? [envFile] : []
|
|
7485
8068
|
],
|
|
7486
8069
|
hints: spec.authHints ?? []
|
|
7487
8070
|
};
|
|
7488
8071
|
}
|
|
7489
8072
|
case "openclaw": {
|
|
7490
|
-
const cfgFile =
|
|
8073
|
+
const cfgFile = join11(homedir4(), ".openclaw", "openclaw.json");
|
|
7491
8074
|
return {
|
|
7492
|
-
ok:
|
|
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
|
-
...
|
|
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 =
|
|
7511
|
-
const markerPath =
|
|
7512
|
-
const jsonMarkerPresent =
|
|
7513
|
-
const dirMarkerPresent =
|
|
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:
|
|
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 =
|
|
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:
|
|
8113
|
+
exists: existsSync12(target.path),
|
|
7531
8114
|
markerPath,
|
|
7532
|
-
markerPresent:
|
|
8115
|
+
markerPresent: existsSync12(markerPath) && fileContainsPrismerMarker(markerPath)
|
|
7533
8116
|
};
|
|
7534
8117
|
}
|
|
7535
|
-
const exists =
|
|
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 (!
|
|
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(
|
|
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 (!
|
|
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
|
|
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
|
|
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 (!
|
|
8713
|
+
if (!existsSync13(file)) exitWithError(`file not found: ${file}`);
|
|
8127
8714
|
const cfg = loadConfig(resolvePaths());
|
|
8128
|
-
const bytes =
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (!
|
|
8861
|
-
const logFile =
|
|
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 =
|
|
8946
|
-
if (!
|
|
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
|
|
9598
|
+
import { createReadStream as createReadStream2, existsSync as existsSync15 } from "fs";
|
|
9002
9599
|
import { homedir as homedir5 } from "os";
|
|
9003
|
-
import { join as
|
|
9600
|
+
import { join as join13 } from "path";
|
|
9004
9601
|
import { createInterface } from "readline";
|
|
9005
|
-
var
|
|
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 (!
|
|
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 (!
|
|
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,
|
|
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
|
|
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 :
|
|
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 :
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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,
|
|
9336
|
-
const row = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(
|
|
9337
|
-
return row?.name ===
|
|
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
|
|
10043
|
+
import { existsSync as existsSync17, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
|
|
9444
10044
|
import { tmpdir } from "os";
|
|
9445
|
-
import { join as
|
|
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 =
|
|
9490
|
-
|
|
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(
|
|
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 (!
|
|
9517
|
-
return JSON.parse(
|
|
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
|
|
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
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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 (!
|
|
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
|
|
10033
|
-
cloudOk =
|
|
10034
|
-
me =
|
|
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 (
|
|
10064
|
-
|
|
10065
|
-
|
|
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
|
-
|
|
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)
|
|
10100
|
-
|
|
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(`
|
|
10107
|
-
ui.line(` Profiles:
|
|
10108
|
-
ui.line(`
|
|
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);
|