@seqyuan/ennote 0.4.0 → 0.6.0
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/README.md +116 -13
- package/bin/ennote.js +539 -28
- package/out/404.html +1 -1
- package/out/__next.__PAGE__.txt +4 -4
- package/out/__next._full.txt +16 -16
- package/out/__next._head.txt +4 -4
- package/out/__next._index.txt +8 -8
- package/out/__next._tree.txt +4 -4
- package/out/_next/static/chunks/{0l9f15ts9g8~w.css → 042svogz3qv71.css} +3 -2
- package/out/_next/static/chunks/05rc5pno5-uub.js +8 -0
- package/out/_next/static/chunks/0_yt3a8a9j1yd.js +1 -0
- package/out/_next/static/chunks/0gee9kb6nwlbz.js +1 -0
- package/out/_next/static/chunks/{0j5eflqzx5_i2.css → 0k-~g5-_lbznv.css} +1 -1
- package/out/_next/static/chunks/0r3uxu723bo0o.css +4 -0
- package/out/_not-found/__next._full.txt +14 -14
- package/out/_not-found/__next._head.txt +4 -4
- package/out/_not-found/__next._index.txt +8 -8
- package/out/_not-found/__next._not-found.__PAGE__.txt +2 -2
- package/out/_not-found/__next._not-found.txt +3 -3
- package/out/_not-found/__next._tree.txt +3 -3
- package/out/_not-found.html +1 -1
- package/out/_not-found.txt +14 -14
- package/out/graphs/__next._full.txt +16 -16
- package/out/graphs/__next._head.txt +4 -4
- package/out/graphs/__next._index.txt +8 -8
- package/out/graphs/__next._tree.txt +4 -4
- package/out/graphs/__next.graphs.__PAGE__.txt +4 -4
- package/out/graphs/__next.graphs.txt +3 -3
- package/out/graphs.html +1 -1
- package/out/graphs.txt +16 -16
- package/out/index.html +2 -2
- package/out/index.txt +16 -16
- package/out/roles/__next._full.txt +16 -16
- package/out/roles/__next._head.txt +4 -4
- package/out/roles/__next._index.txt +8 -8
- package/out/roles/__next._tree.txt +4 -4
- package/out/roles/__next.roles.__PAGE__.txt +4 -4
- package/out/roles/__next.roles.txt +3 -3
- package/out/roles.html +1 -1
- package/out/roles.txt +16 -16
- package/package.json +29 -5
- package/skills/bio-workspace/SKILL.md +11 -0
- package/skills/bio-workspace/skill.json +7 -0
- package/skills/skill-creator/SKILL.md +87 -0
- package/skills/skill-creator/skill.json +7 -0
- package/out/_next/static/chunks/0qavlry.eoeqb.js +0 -1
- package/out/_next/static/chunks/0uuaelr0o3sep.css +0 -4
- package/out/_next/static/chunks/0yuqqoxr67j_q.js +0 -8
- /package/out/_next/static/{Mlvju0_auR3eZP02RZD1- → 1IULeStFBlnqxNnZLaKnI}/_buildManifest.js +0 -0
- /package/out/_next/static/{Mlvju0_auR3eZP02RZD1- → 1IULeStFBlnqxNnZLaKnI}/_clientMiddlewareManifest.js +0 -0
- /package/out/_next/static/{Mlvju0_auR3eZP02RZD1- → 1IULeStFBlnqxNnZLaKnI}/_ssgManifest.js +0 -0
package/bin/ennote.js
CHANGED
|
@@ -16,7 +16,20 @@ const DEFAULT_PORT = "45131";
|
|
|
16
16
|
const DEFAULT_HOSTNAME = "127.0.0.1";
|
|
17
17
|
const START_TIMEOUT_MS = 45_000;
|
|
18
18
|
const STOP_TIMEOUT_MS = 15_000;
|
|
19
|
-
const
|
|
19
|
+
const PACK_INSPECT_TIMEOUT_MS = 15_000;
|
|
20
|
+
const PACK_REQUEST_TIMEOUT_MS = 15_000;
|
|
21
|
+
const DEFAULT_RELEASES_BASE = "https://github.com/seqyuan/ennote/releases/download";
|
|
22
|
+
|
|
23
|
+
// releasesBase honours ENNOTE_RELEASES_BASE so a mirror (or an air-gapped
|
|
24
|
+
// install serving the release assets locally) can be used instead of GitHub.
|
|
25
|
+
// The value is read at call time, not import time, so a wrapper script that
|
|
26
|
+
// sets it still affects this process. HTTPS is enforced by requireHTTPS.
|
|
27
|
+
function releasesBase() {
|
|
28
|
+
const override = String(process.env.ENNOTE_RELEASES_BASE ?? "").trim();
|
|
29
|
+
return override === "" ? DEFAULT_RELEASES_BASE : override.replace(/\/+$/, "");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const BINARY_NAMES = ["ennogate", "ennoworker"];
|
|
20
33
|
|
|
21
34
|
function platformSuffix(platform = os.platform(), arch = os.arch()) {
|
|
22
35
|
if (!new Set(["linux", "darwin"]).has(platform)) {
|
|
@@ -54,6 +67,18 @@ function statePaths(home) {
|
|
|
54
67
|
};
|
|
55
68
|
}
|
|
56
69
|
|
|
70
|
+
// workerStatePath is the owner-only handshake file ennoworker writes and
|
|
71
|
+
// ennogate supervises (see internal/runtimeinfo.WorkerState).
|
|
72
|
+
function workerStatePath(home) {
|
|
73
|
+
return path.join(home, "runtime", "worker-state.json");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function usageError(message) {
|
|
77
|
+
const error = new Error(message);
|
|
78
|
+
error.exitCode = 2;
|
|
79
|
+
return error;
|
|
80
|
+
}
|
|
81
|
+
|
|
57
82
|
function validateRuntime(paths) {
|
|
58
83
|
for (const [name, file] of [["ennogate", paths.gate], ["ennoworker", paths.worker]]) {
|
|
59
84
|
if (!fs.existsSync(file)) throw new Error(`Missing ${name} binary: ${file}`);
|
|
@@ -87,6 +112,53 @@ function removeFile(file) {
|
|
|
87
112
|
}
|
|
88
113
|
}
|
|
89
114
|
|
|
115
|
+
// compareVersions orders dotted numeric versions. A pre-release suffix sorts
|
|
116
|
+
// before the release it belongs to, so 0.5.0-rc1 < 0.5.0.
|
|
117
|
+
function compareVersions(left, right) {
|
|
118
|
+
const parse = (value) => {
|
|
119
|
+
const [core, ...pre] = String(value).split("-");
|
|
120
|
+
return { parts: core.split(".").map((part) => Number.parseInt(part, 10) || 0), pre: pre.join("-") };
|
|
121
|
+
};
|
|
122
|
+
const a = parse(left);
|
|
123
|
+
const b = parse(right);
|
|
124
|
+
const length = Math.max(a.parts.length, b.parts.length);
|
|
125
|
+
for (let index = 0; index < length; index += 1) {
|
|
126
|
+
const difference = (a.parts[index] ?? 0) - (b.parts[index] ?? 0);
|
|
127
|
+
if (difference !== 0) return difference;
|
|
128
|
+
}
|
|
129
|
+
if (a.pre === b.pre) return 0;
|
|
130
|
+
if (a.pre === "") return 1;
|
|
131
|
+
if (b.pre === "") return -1;
|
|
132
|
+
return a.pre < b.pre ? -1 : 1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// cachedBinaryVersions lists the versions that already have BOTH binaries for
|
|
136
|
+
// this platform, newest first. A version with only one binary is an abandoned
|
|
137
|
+
// partial download and must not be offered as a fallback. Selection is explicit
|
|
138
|
+
// rather than readdir order, which is filesystem-defined.
|
|
139
|
+
function cachedBinaryVersions(home, suffix) {
|
|
140
|
+
const binDir = runtimeBinDir(home);
|
|
141
|
+
let entries;
|
|
142
|
+
try {
|
|
143
|
+
entries = fs.readdirSync(binDir);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (error?.code === "ENOENT") return [];
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
const versions = new Set();
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
for (const name of BINARY_NAMES) {
|
|
151
|
+
const postfix = `-${suffix}`;
|
|
152
|
+
if (!entry.startsWith(`${name}-`) || !entry.endsWith(postfix)) continue;
|
|
153
|
+
const version = entry.slice(name.length + 1, entry.length - postfix.length);
|
|
154
|
+
if (version !== "") versions.add(version);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return [...versions]
|
|
158
|
+
.filter((version) => BINARY_NAMES.every((name) => fs.existsSync(path.join(binDir, `${name}-${version}-${suffix}`))))
|
|
159
|
+
.sort((a, b) => compareVersions(b, a));
|
|
160
|
+
}
|
|
161
|
+
|
|
90
162
|
function isPidAlive(pid) {
|
|
91
163
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
92
164
|
try {
|
|
@@ -246,33 +318,56 @@ function downloadText(url) {
|
|
|
246
318
|
});
|
|
247
319
|
}
|
|
248
320
|
|
|
249
|
-
async function ensureBinaries(home, pkg, log, platform = os.platform(), arch = os.arch()) {
|
|
321
|
+
async function ensureBinaries(home, pkg, log, platform = os.platform(), arch = os.arch(), download = {}) {
|
|
250
322
|
const suffix = platformSuffix(platform, arch);
|
|
251
323
|
const version = pkg.version;
|
|
252
324
|
const binDir = runtimeBinDir(home);
|
|
253
325
|
ensureDirectory(binDir);
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const sumsUrl = `${
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
326
|
+
const binaryPath = (name, at) => path.join(binDir, `${name}-${at}-${suffix}`);
|
|
327
|
+
const missing = BINARY_NAMES.filter((name) => !fs.existsSync(binaryPath(name, version)));
|
|
328
|
+
if (missing.length === 0) return { version, fallback: false };
|
|
329
|
+
|
|
330
|
+
const base = releasesBase();
|
|
331
|
+
const fetchText = download.downloadText ?? downloadText;
|
|
332
|
+
const fetchFile = download.downloadFile ?? downloadFile;
|
|
333
|
+
const sumsUrl = `${base}/v${version}/SHA256SUMS`;
|
|
334
|
+
log(`Downloading ${missing.length} binary(s) for ${suffix}...`);
|
|
335
|
+
try {
|
|
336
|
+
const sumsText = await fetchText(sumsUrl);
|
|
337
|
+
for (const name of missing) {
|
|
338
|
+
const filename = `${name}-${suffix}`;
|
|
339
|
+
const dest = binaryPath(name, version);
|
|
340
|
+
const expected = parseSHA256SUMS(sumsText, filename);
|
|
341
|
+
try {
|
|
342
|
+
await fetchFile(`${base}/v${version}/${filename}`, dest);
|
|
343
|
+
verifyFileSHA256(dest, expected);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
// Never leave a partial or unverified binary behind: it would look like
|
|
346
|
+
// a usable cache entry to the next run.
|
|
347
|
+
removeFile(dest);
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return { version, fallback: false };
|
|
352
|
+
} catch (error) {
|
|
353
|
+
// Downloading needs GitHub. A local-first runtime should still start from a
|
|
354
|
+
// binary that is already on disk rather than fail on a network the user may
|
|
355
|
+
// not have, so an older cached version is better than no server.
|
|
356
|
+
const fallback = cachedBinaryVersions(home, suffix)[0];
|
|
357
|
+
if (!fallback) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`failed to install Ennote ${version} binaries: ${error.message}; ` +
|
|
360
|
+
`attempted ${sumsUrl} and found no complete cached build under ${binDir} for ${suffix}. ` +
|
|
361
|
+
`Install the release assets manually or set ENNOTE_RELEASES_BASE to a mirror.`,
|
|
362
|
+
);
|
|
273
363
|
}
|
|
364
|
+
log(
|
|
365
|
+
`warning: failed to install Ennote ${version} binaries (${error.message}); ` +
|
|
366
|
+
`using cached ${fallback} for ${suffix} instead. ` +
|
|
367
|
+
`The Worker is older than the CLI's expected ${version}; re-run with network access to update.`,
|
|
368
|
+
);
|
|
369
|
+
return { version: fallback, fallback: true };
|
|
274
370
|
}
|
|
275
|
-
log("Binaries ready.");
|
|
276
371
|
}
|
|
277
372
|
|
|
278
373
|
async function startService(options) {
|
|
@@ -290,8 +385,8 @@ async function startService(options) {
|
|
|
290
385
|
if (!await canBind(port, hostname)) throw new Error(`Port ${port} is already in use on ${hostname}`);
|
|
291
386
|
|
|
292
387
|
const pkg = JSON.parse(fs.readFileSync(path.join(packageDir, "package.json"), "utf8"));
|
|
293
|
-
await ensureBinaries(home, pkg, log);
|
|
294
|
-
const runtime = runtimePaths(packageDir, home,
|
|
388
|
+
const binaries = await ensureBinaries(home, pkg, log);
|
|
389
|
+
const runtime = runtimePaths(packageDir, home, binaries.version);
|
|
295
390
|
validateRuntime(runtime);
|
|
296
391
|
ensureDirectory(home);
|
|
297
392
|
const output = fs.openSync(files.log, "a", 0o600);
|
|
@@ -302,6 +397,9 @@ async function startService(options) {
|
|
|
302
397
|
ENNOTE_HOME: home,
|
|
303
398
|
ENNOTE_STATIC_DIR: runtime.staticDir,
|
|
304
399
|
ENNOTE_WORKER_PATH: runtime.worker,
|
|
400
|
+
...(fs.existsSync(path.join(packageDir, "skills"))
|
|
401
|
+
? { ENNOTE_BUILTIN_SKILLS_DIR: path.join(packageDir, "skills") }
|
|
402
|
+
: {}),
|
|
305
403
|
PORT: String(port),
|
|
306
404
|
ENNOTE_HOSTNAME: hostname,
|
|
307
405
|
},
|
|
@@ -315,9 +413,14 @@ async function startService(options) {
|
|
|
315
413
|
port: String(port),
|
|
316
414
|
hostname,
|
|
317
415
|
url,
|
|
416
|
+
// Which build is actually serving: an offline start can run a cached
|
|
417
|
+
// version older than this CLI, and that has to be recoverable after the
|
|
418
|
+
// fact rather than only visible in one warning line.
|
|
419
|
+
binaryVersion: binaries.version,
|
|
420
|
+
binaryFallback: binaries.fallback,
|
|
318
421
|
startedAt: new Date().toISOString(),
|
|
319
422
|
});
|
|
320
|
-
log(`starting ennogate pid=${child.pid} url=${url}`);
|
|
423
|
+
log(`starting ennogate pid=${child.pid} url=${url} bin=${binaries.version}${binaries.fallback ? " (cached fallback)" : ""}`);
|
|
321
424
|
|
|
322
425
|
const deadline = Date.now() + START_TIMEOUT_MS;
|
|
323
426
|
while (Date.now() < deadline) {
|
|
@@ -359,7 +462,381 @@ async function serviceStatus(home) {
|
|
|
359
462
|
return { running: false, stale: Boolean(state) };
|
|
360
463
|
}
|
|
361
464
|
const healthy = await healthCheck(state.url);
|
|
362
|
-
return {
|
|
465
|
+
return {
|
|
466
|
+
running: healthy,
|
|
467
|
+
healthy,
|
|
468
|
+
pid: state.pid,
|
|
469
|
+
url: state.url,
|
|
470
|
+
port: state.port,
|
|
471
|
+
...(state.binaryVersion ? { binaryVersion: state.binaryVersion, binaryFallback: Boolean(state.binaryFallback) } : {}),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function isLoopbackHostname(hostname) {
|
|
476
|
+
const name = String(hostname ?? "").replace(/^\[|\]$/g, "").toLowerCase();
|
|
477
|
+
if (name === "localhost" || name === "::1") return true;
|
|
478
|
+
const match = name.match(/^127\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
479
|
+
return Boolean(match) && match.slice(1).every(part => Number(part) <= 255);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// loadWorkerEndpoint reads the Worker handshake written by ennoworker and
|
|
483
|
+
// refuses anything that is not a live loopback http Worker: ennote pack inspect
|
|
484
|
+
// talks to the real Worker instead of re-implementing pack parsing in Node.
|
|
485
|
+
function loadWorkerEndpoint(home) {
|
|
486
|
+
const file = workerStatePath(home);
|
|
487
|
+
if (!fs.existsSync(file)) {
|
|
488
|
+
throw new Error(`Ennote Worker is not running (${file} is missing). Start it with: ennote start`);
|
|
489
|
+
}
|
|
490
|
+
let state;
|
|
491
|
+
try {
|
|
492
|
+
state = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
493
|
+
} catch {
|
|
494
|
+
throw new Error(`Ennote Worker state ${file} is unreadable. Restart it with: ennote restart`);
|
|
495
|
+
}
|
|
496
|
+
let parsed;
|
|
497
|
+
try {
|
|
498
|
+
parsed = new URL(String(state?.url ?? ""));
|
|
499
|
+
} catch {
|
|
500
|
+
throw new Error(`Ennote Worker state ${file} has an invalid url: ${state?.url ?? ""}`);
|
|
501
|
+
}
|
|
502
|
+
if (parsed.protocol !== "http:") {
|
|
503
|
+
throw new Error(`refusing Worker url ${state.url}: expected http:`);
|
|
504
|
+
}
|
|
505
|
+
if (!isLoopbackHostname(parsed.hostname)) {
|
|
506
|
+
throw new Error(`refusing non-loopback Worker url ${state.url}`);
|
|
507
|
+
}
|
|
508
|
+
if (!isPidAlive(state.pid)) {
|
|
509
|
+
throw new Error(`Ennote Worker pid ${state.pid} is not running. Start it with: ennote start`);
|
|
510
|
+
}
|
|
511
|
+
if (typeof state.bootstrapToken !== "string" || state.bootstrapToken === "") {
|
|
512
|
+
throw new Error(`Ennote Worker state ${file} has no bootstrap token. Restart it with: ennote restart`);
|
|
513
|
+
}
|
|
514
|
+
return { url: parsed.origin, token: state.bootstrapToken, pid: state.pid, instanceId: state.instanceId };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function postJSON(url, body, headers, timeoutMs) {
|
|
518
|
+
return httpRequest("POST", url, body, headers, timeoutMs);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// httpRequest is the one place the CLI talks to the Worker. GET/DELETE carry no
|
|
522
|
+
// body; POST serializes it. The Worker url is already pinned to loopback http by
|
|
523
|
+
// loadWorkerEndpoint, so no transport negotiation happens here.
|
|
524
|
+
function httpRequest(method, url, body, headers, timeoutMs) {
|
|
525
|
+
return new Promise((resolve, reject) => {
|
|
526
|
+
const payload = body === undefined || body === null ? null : Buffer.from(JSON.stringify(body), "utf8");
|
|
527
|
+
const request = http.request(url, {
|
|
528
|
+
method,
|
|
529
|
+
headers: payload
|
|
530
|
+
? { "Content-Type": "application/json", "Content-Length": payload.length, ...headers }
|
|
531
|
+
: headers,
|
|
532
|
+
}, (response) => {
|
|
533
|
+
const chunks = [];
|
|
534
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
535
|
+
response.on("end", () => resolve({
|
|
536
|
+
statusCode: response.statusCode ?? 0,
|
|
537
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
538
|
+
}));
|
|
539
|
+
});
|
|
540
|
+
request.once("error", reject);
|
|
541
|
+
request.setTimeout(timeoutMs, () => {
|
|
542
|
+
request.destroy(new Error(`Worker request timed out after ${timeoutMs / 1000}s`));
|
|
543
|
+
});
|
|
544
|
+
request.end(payload ?? undefined);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// decodeWorkerResponse unwraps the Worker error envelope and the data envelope.
|
|
549
|
+
// A 204 (pack remove) has no data and yields null.
|
|
550
|
+
function decodeWorkerResponse(response, action) {
|
|
551
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
552
|
+
let message = `Worker returned HTTP ${response.statusCode}`;
|
|
553
|
+
let code;
|
|
554
|
+
try {
|
|
555
|
+
const parsed = JSON.parse(response.body);
|
|
556
|
+
code = parsed?.error?.code;
|
|
557
|
+
if (parsed?.error?.message) message = parsed.error.message;
|
|
558
|
+
} catch {
|
|
559
|
+
// A non-JSON error body keeps the status-only message.
|
|
560
|
+
}
|
|
561
|
+
const error = new Error(code ? `${message} (${code})` : message);
|
|
562
|
+
error.code = code;
|
|
563
|
+
error.statusCode = response.statusCode;
|
|
564
|
+
throw error;
|
|
565
|
+
}
|
|
566
|
+
if (response.statusCode === 204 || response.body.trim() === "") return null;
|
|
567
|
+
let parsed;
|
|
568
|
+
try {
|
|
569
|
+
parsed = JSON.parse(response.body);
|
|
570
|
+
} catch {
|
|
571
|
+
throw new Error(`Worker returned an unreadable ${action} response`);
|
|
572
|
+
}
|
|
573
|
+
if (!parsed || typeof parsed !== "object" || parsed.data == null) {
|
|
574
|
+
throw new Error(`Worker returned an unexpected ${action} response`);
|
|
575
|
+
}
|
|
576
|
+
return parsed.data;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// requestJSON performs one authenticated Worker call. Every pack command except
|
|
580
|
+
// install is GET/DELETE; install reuses postJSON so its body and headers stay in
|
|
581
|
+
// one place.
|
|
582
|
+
async function requestJSON(endpoint, method, pathname, options = {}) {
|
|
583
|
+
if (method === "POST") {
|
|
584
|
+
const response = await postJSON(
|
|
585
|
+
`${endpoint.url}${pathname}`,
|
|
586
|
+
options.body,
|
|
587
|
+
{ Authorization: `Bearer ${endpoint.token}` },
|
|
588
|
+
options.timeoutMs ?? PACK_REQUEST_TIMEOUT_MS,
|
|
589
|
+
);
|
|
590
|
+
return decodeWorkerResponse(response, options.action ?? "pack");
|
|
591
|
+
}
|
|
592
|
+
const response = await httpRequest(
|
|
593
|
+
method,
|
|
594
|
+
`${endpoint.url}${pathname}`,
|
|
595
|
+
undefined,
|
|
596
|
+
{ Authorization: `Bearer ${endpoint.token}` },
|
|
597
|
+
options.timeoutMs ?? PACK_REQUEST_TIMEOUT_MS,
|
|
598
|
+
);
|
|
599
|
+
return decodeWorkerResponse(response, options.action ?? "pack");
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async function requestPackInspect(endpoint, target, options = {}) {
|
|
603
|
+
const body = {
|
|
604
|
+
source: { kind: "path", path: target },
|
|
605
|
+
includeDocuments: Boolean(options.documents),
|
|
606
|
+
};
|
|
607
|
+
if (options.expectedDigest) body.expectedDigest = options.expectedDigest;
|
|
608
|
+
const response = await postJSON(
|
|
609
|
+
`${endpoint.url}/v1/packs/inspect`,
|
|
610
|
+
body,
|
|
611
|
+
{ Authorization: `Bearer ${endpoint.token}` },
|
|
612
|
+
options.timeoutMs ?? PACK_INSPECT_TIMEOUT_MS,
|
|
613
|
+
);
|
|
614
|
+
return decodeWorkerResponse(response, "inspect");
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function formatPackSummary(data) {
|
|
618
|
+
const pack = data.pack ?? {};
|
|
619
|
+
const container = data.container ?? {};
|
|
620
|
+
const signature = data.signature ?? {};
|
|
621
|
+
const lines = [];
|
|
622
|
+
lines.push(`${pack.name ?? "(unnamed pack)"} (${pack.id ?? "?"}@${pack.version ?? "?"})`);
|
|
623
|
+
if (pack.description) lines.push(pack.description);
|
|
624
|
+
lines.push(`Source: ${data.source?.path ?? "?"} (${container.kind ?? "?"}, ${container.bytes ?? 0} bytes)`);
|
|
625
|
+
lines.push(signature.present
|
|
626
|
+
? `Signature: ${signature.algorithm} at ${signature.path} — UNVERIFIED (inspect never verifies signatures)`
|
|
627
|
+
: "Signature: Unsigned");
|
|
628
|
+
lines.push(`Manifest: ${data.manifestDigest ?? "?"}`);
|
|
629
|
+
lines.push(`Pack: ${data.packDigest ?? "?"}`);
|
|
630
|
+
|
|
631
|
+
const flows = data.flows ?? [];
|
|
632
|
+
lines.push(`Flows (${flows.length})`);
|
|
633
|
+
for (const flow of flows) {
|
|
634
|
+
lines.push(` - ${flow.id} [${flow.format}] ${flow.taskCount} task(s), ${flow.checkTasks} check, ${flow.fanOutTasks} fan-out, ${flow.maxTotalTokens} tokens`);
|
|
635
|
+
}
|
|
636
|
+
const roles = data.roles ?? [];
|
|
637
|
+
lines.push(`Roles (${roles.length})`);
|
|
638
|
+
for (const role of roles) {
|
|
639
|
+
lines.push(` - ${role.handle} (${role.scope}) ${role.authority}, tools: ${(role.allowedTools ?? []).join(", ") || "none"}`);
|
|
640
|
+
}
|
|
641
|
+
const skills = data.skills ?? [];
|
|
642
|
+
lines.push(`Skills (${skills.length})`);
|
|
643
|
+
for (const skill of skills) {
|
|
644
|
+
lines.push(` - ${skill.kind === "external" ? skill.ref : skill.id} (${skill.kind})`);
|
|
645
|
+
}
|
|
646
|
+
// External MCP profile refs are reported but never resolved: the summary says
|
|
647
|
+
// so explicitly so a reviewer does not read the line as an installed profile.
|
|
648
|
+
const mcp = data.mcp ?? [];
|
|
649
|
+
if (mcp.length > 0) {
|
|
650
|
+
lines.push(`MCP (${mcp.length})`);
|
|
651
|
+
for (const profile of mcp) {
|
|
652
|
+
lines.push(` - ${profile.ref} (external ref, not resolved or executed)`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const envelope = data.capabilityEnvelope ?? {};
|
|
656
|
+
lines.push(`Checks: ${(envelope.checkCommands ?? []).join(", ") || "none"}`);
|
|
657
|
+
lines.push(`Writes: ${(envelope.writes ?? []).join(", ") || "none"}`);
|
|
658
|
+
lines.push(`Files: ${data.filesTotal ?? 0}${data.filesTruncated ? " (truncated)" : ""}`);
|
|
659
|
+
|
|
660
|
+
const warnings = data.warnings ?? [];
|
|
661
|
+
lines.push(`Warnings (${warnings.length})`);
|
|
662
|
+
for (const warning of warnings) lines.push(` - ${warning.code} (${warning.severity}): ${warning.message}`);
|
|
663
|
+
return lines.join("\n");
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async function packCommand(home, positionals, values) {
|
|
667
|
+
const action = positionals[1];
|
|
668
|
+
const target = positionals[2];
|
|
669
|
+
if (action === "inspect") {
|
|
670
|
+
if (!target) {
|
|
671
|
+
throw usageError("Usage: ennote pack inspect <file|dir> [--json] [--documents]");
|
|
672
|
+
}
|
|
673
|
+
const endpoint = loadWorkerEndpoint(home);
|
|
674
|
+
const data = await requestPackInspect(endpoint, path.resolve(target), {
|
|
675
|
+
documents: values.documents,
|
|
676
|
+
});
|
|
677
|
+
if (values.json) console.log(JSON.stringify(data));
|
|
678
|
+
else console.log(formatPackSummary(data));
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (action === "install") {
|
|
682
|
+
if (!target) {
|
|
683
|
+
throw usageError("Usage: ennote pack install <file|dir> [--dry-run] [--json] [--as-flow old=new] [--as-role old=new]");
|
|
684
|
+
}
|
|
685
|
+
// Renames are validated before the Worker is contacted, so a typo is a
|
|
686
|
+
// usage error instead of a refused install.
|
|
687
|
+
const renames = {
|
|
688
|
+
flows: parsePackRenames(values["as-flow"], "--as-flow"),
|
|
689
|
+
roles: parsePackRenames(values["as-role"], "--as-role"),
|
|
690
|
+
};
|
|
691
|
+
const endpoint = loadWorkerEndpoint(home);
|
|
692
|
+
const plan = await requestPackInstall(endpoint, path.resolve(target), {
|
|
693
|
+
dryRun: Boolean(values["dry-run"]),
|
|
694
|
+
renames,
|
|
695
|
+
});
|
|
696
|
+
if (values.json) console.log(JSON.stringify(plan));
|
|
697
|
+
else console.log(formatPackInstallPlan(plan, { dryRun: Boolean(values["dry-run"]) }));
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (action === "list") {
|
|
701
|
+
const endpoint = loadWorkerEndpoint(home);
|
|
702
|
+
const summaries = await requestJSON(endpoint, "GET", "/v1/packs", { action: "pack list" });
|
|
703
|
+
if (values.json) console.log(JSON.stringify(summaries ?? []));
|
|
704
|
+
else console.log(formatPackList(summaries ?? []));
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (action === "remove") {
|
|
708
|
+
if (!target) {
|
|
709
|
+
throw usageError("Usage: ennote pack remove <publisher>/<name> [--force] [--json]");
|
|
710
|
+
}
|
|
711
|
+
const packId = parsePackID(target);
|
|
712
|
+
const endpoint = loadWorkerEndpoint(home);
|
|
713
|
+
const [publisher, name] = packId.split("/");
|
|
714
|
+
const query = values.force ? "?force=1" : "";
|
|
715
|
+
const pathname = `/v1/packs/${encodeURIComponent(publisher)}/${encodeURIComponent(name)}${query}`;
|
|
716
|
+
// A 204 carries no body; the CLI still reports the same shape as --json so
|
|
717
|
+
// scripts do not have to special-case the empty response.
|
|
718
|
+
const data = (await requestJSON(endpoint, "DELETE", pathname, { action: "pack remove" })) ?? {
|
|
719
|
+
packId,
|
|
720
|
+
removed: true,
|
|
721
|
+
};
|
|
722
|
+
if (values.json) console.log(JSON.stringify(data));
|
|
723
|
+
else console.log(`Removed ${data.packId ?? packId}${values.force ? " (forced)" : ""}`);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
throw usageError(
|
|
727
|
+
`Unknown pack command: ${action ?? ""}\nUsage: ennote pack <inspect|install|list|remove> ...`,
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// parsePackRenames turns repeated --as-flow/--as-role old=new flags into the
|
|
732
|
+
// renames object. A rename only changes the installed store id.
|
|
733
|
+
function parsePackRenames(entries, flag) {
|
|
734
|
+
const renames = {};
|
|
735
|
+
for (const entry of entries ?? []) {
|
|
736
|
+
const text = String(entry);
|
|
737
|
+
const separator = text.indexOf("=");
|
|
738
|
+
if (separator <= 0 || separator === text.length - 1) {
|
|
739
|
+
throw usageError(`${flag} expects old=new, got ${text}`);
|
|
740
|
+
}
|
|
741
|
+
const from = text.slice(0, separator).trim();
|
|
742
|
+
const to = text.slice(separator + 1).trim();
|
|
743
|
+
if (from === "" || to === "") throw usageError(`${flag} expects old=new, got ${text}`);
|
|
744
|
+
if (Object.prototype.hasOwnProperty.call(renames, from) && renames[from] !== to) {
|
|
745
|
+
throw usageError(`${flag} maps ${from} twice (${renames[from]} and ${to})`);
|
|
746
|
+
}
|
|
747
|
+
renames[from] = to;
|
|
748
|
+
}
|
|
749
|
+
return renames;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// parsePackID validates <publisher>/<name> before it goes on the wire: a bad id
|
|
753
|
+
// is a usage error, not a Worker 400/404.
|
|
754
|
+
function parsePackID(value) {
|
|
755
|
+
const segments = String(value).split("/");
|
|
756
|
+
if (segments.length !== 2 || segments.some(segment => segment === "" || segment === "." || segment === "..")) {
|
|
757
|
+
throw usageError(`pack id must be <publisher>/<name>, got ${value}`);
|
|
758
|
+
}
|
|
759
|
+
return segments.join("/");
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function requestPackInstall(endpoint, target, options = {}) {
|
|
763
|
+
const body = { source: { kind: "path", path: target }, dryRun: Boolean(options.dryRun) };
|
|
764
|
+
const renames = options.renames ?? {};
|
|
765
|
+
const encoded = {};
|
|
766
|
+
if (Object.keys(renames.flows ?? {}).length > 0) encoded.flows = renames.flows;
|
|
767
|
+
if (Object.keys(renames.roles ?? {}).length > 0) encoded.roles = renames.roles;
|
|
768
|
+
if (Object.keys(encoded).length > 0) body.renames = encoded;
|
|
769
|
+
return requestJSON(endpoint, "POST", "/v1/packs/install", { body, action: "pack install" });
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function formatPackActionCounts(actions) {
|
|
773
|
+
const counts = new Map();
|
|
774
|
+
for (const action of actions) {
|
|
775
|
+
counts.set(action.action, (counts.get(action.action) ?? 0) + 1);
|
|
776
|
+
}
|
|
777
|
+
return [...counts.entries()]
|
|
778
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
779
|
+
.map(([name, count]) => `${count} ${name}`)
|
|
780
|
+
.join(", ");
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function formatPackInstallPlan(plan, options = {}) {
|
|
784
|
+
const pack = plan?.pack ?? {};
|
|
785
|
+
const lines = [];
|
|
786
|
+
const headline = plan?.reinstall === "noop"
|
|
787
|
+
? "Already installed"
|
|
788
|
+
: options.dryRun ? "Dry run (nothing written)" : "Installed";
|
|
789
|
+
lines.push(`${headline}: ${pack.name ?? "(unnamed pack)"} (${pack.id ?? "?"}@${pack.version ?? "?"})`);
|
|
790
|
+
lines.push(`Manifest: ${plan?.manifestDigest ?? "?"}`);
|
|
791
|
+
lines.push(`Pack: ${plan?.packDigest ?? "?"}`);
|
|
792
|
+
|
|
793
|
+
const actions = plan?.actions ?? [];
|
|
794
|
+
lines.push(`Actions (${actions.length}): ${formatPackActionCounts(actions) || "none"}`);
|
|
795
|
+
for (const action of actions) {
|
|
796
|
+
const scope = action.scope ? ` (${action.scope})` : "";
|
|
797
|
+
lines.push(` - ${action.action} ${action.kind} ${action.storeId}${scope}`);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const state = plan?.state;
|
|
801
|
+
if (state) {
|
|
802
|
+
lines.push(`State: ${state.installState ?? "?"}; flows ${state.flows?.length ?? 0}, roles ${state.roles?.length ?? 0}, skills ${state.skills?.length ?? 0}`);
|
|
803
|
+
const drafts = (state.flows ?? []).filter(flow => !flow.publishedRevision);
|
|
804
|
+
if (drafts.length > 0) {
|
|
805
|
+
lines.push(`Disabled: ${drafts.map(flow => flow.storeId).join(", ")} (draft Graph, no published revision — publish it to enable runs)`);
|
|
806
|
+
}
|
|
807
|
+
if (state.source?.path) lines.push(`Source: ${state.source.path}`);
|
|
808
|
+
const external = state.externalRefs ?? {};
|
|
809
|
+
const externalSkills = external.skills ?? [];
|
|
810
|
+
const externalMcp = external.mcp ?? [];
|
|
811
|
+
if (externalSkills.length > 0 || externalMcp.length > 0) {
|
|
812
|
+
lines.push(`Recorded (not installed): skills ${externalSkills.join(", ") || "none"}; mcp ${externalMcp.join(", ") || "none"}`);
|
|
813
|
+
}
|
|
814
|
+
} else if (options.dryRun) {
|
|
815
|
+
lines.push("State: not written (dry run)");
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const blockers = plan?.blockers ?? [];
|
|
819
|
+
lines.push(`Blockers (${blockers.length})`);
|
|
820
|
+
for (const blocker of blockers) {
|
|
821
|
+
lines.push(` - ${blocker.code} (${blocker.severity ?? "error"}): ${blocker.message}`);
|
|
822
|
+
}
|
|
823
|
+
const warnings = plan?.warnings ?? [];
|
|
824
|
+
lines.push(`Warnings (${warnings.length})`);
|
|
825
|
+
for (const warning of warnings) {
|
|
826
|
+
lines.push(` - ${warning.code} (${warning.severity ?? "warning"}): ${warning.message}`);
|
|
827
|
+
}
|
|
828
|
+
return lines.join("\n");
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function formatPackList(summaries) {
|
|
832
|
+
if (summaries.length === 0) return "No Agent Packs installed.";
|
|
833
|
+
const lines = [`Installed Agent Packs (${summaries.length})`];
|
|
834
|
+
for (const summary of summaries) {
|
|
835
|
+
const pack = summary.pack ?? {};
|
|
836
|
+
lines.push(` - ${pack.name ?? "(unnamed pack)"} (${pack.id ?? "?"}@${pack.version ?? "?"}) ${summary.installState ?? "?"}`);
|
|
837
|
+
lines.push(` flows ${summary.flowCount ?? 0}, roles ${summary.roleCount ?? 0}, skills ${summary.skillCount ?? 0}; installed ${summary.installedAt ?? "?"}`);
|
|
838
|
+
}
|
|
839
|
+
return lines.join("\n");
|
|
363
840
|
}
|
|
364
841
|
|
|
365
842
|
async function setPassword(home, reset) {
|
|
@@ -397,11 +874,20 @@ Commands:
|
|
|
397
874
|
logs Show recent service logs
|
|
398
875
|
passwd Set the local password
|
|
399
876
|
passwd --reset Remove the local password and require setup
|
|
877
|
+
pack inspect Inspect an Agent Pack (.enpack or directory) without installing
|
|
878
|
+
pack install Install an Agent Pack from a .enpack file or directory
|
|
879
|
+
pack list List installed Agent Packs
|
|
880
|
+
pack remove Uninstall an Agent Pack (<publisher>/<name>)
|
|
400
881
|
|
|
401
882
|
Options:
|
|
402
883
|
--port, -p Browser-facing port (default ${DEFAULT_PORT})
|
|
403
884
|
--hostname, -H Bind address (default ${DEFAULT_HOSTNAME})
|
|
404
|
-
--json Machine-readable output
|
|
885
|
+
--json Machine-readable output (prints the Worker data payload)
|
|
886
|
+
--documents pack inspect: include flow, role, and skill source text
|
|
887
|
+
--dry-run pack install: plan only and write nothing
|
|
888
|
+
--as-flow pack install: old=new rename of an installed Graph id
|
|
889
|
+
--as-role pack install: old=new rename of an installed Role handle
|
|
890
|
+
--force pack remove: override local edits and shared global Roles
|
|
405
891
|
--version, -v Show version
|
|
406
892
|
--help, -h Show this help`);
|
|
407
893
|
}
|
|
@@ -415,6 +901,11 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
415
901
|
port: { type: "string", short: "p", default: DEFAULT_PORT },
|
|
416
902
|
hostname: { type: "string", short: "H", default: DEFAULT_HOSTNAME },
|
|
417
903
|
json: { type: "boolean", default: false },
|
|
904
|
+
documents: { type: "boolean", default: false },
|
|
905
|
+
"dry-run": { type: "boolean", default: false },
|
|
906
|
+
"as-flow": { type: "string", multiple: true },
|
|
907
|
+
"as-role": { type: "string", multiple: true },
|
|
908
|
+
force: { type: "boolean", default: false },
|
|
418
909
|
reset: { type: "boolean", default: false },
|
|
419
910
|
version: { type: "boolean", short: "v", default: false },
|
|
420
911
|
help: { type: "boolean", short: "h", default: false },
|
|
@@ -451,6 +942,11 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
451
942
|
case "passwd":
|
|
452
943
|
result = await setPassword(home, values.reset);
|
|
453
944
|
break;
|
|
945
|
+
case "pack":
|
|
946
|
+
// Every pack command talks to the running Worker instead of re-implementing
|
|
947
|
+
// manifest parsing, installation, or store writes in Node.
|
|
948
|
+
await packCommand(home, positionals, values);
|
|
949
|
+
return;
|
|
454
950
|
default:
|
|
455
951
|
throw new Error(`Unknown command: ${command}`);
|
|
456
952
|
}
|
|
@@ -466,6 +962,7 @@ module.exports = {
|
|
|
466
962
|
runtimeBinDir,
|
|
467
963
|
runtimePaths,
|
|
468
964
|
statePaths,
|
|
965
|
+
workerStatePath,
|
|
469
966
|
serviceURL,
|
|
470
967
|
validateRuntime,
|
|
471
968
|
healthCheck,
|
|
@@ -473,17 +970,31 @@ module.exports = {
|
|
|
473
970
|
parseSHA256SUMS,
|
|
474
971
|
fileSHA256,
|
|
475
972
|
verifyFileSHA256,
|
|
973
|
+
compareVersions,
|
|
974
|
+
cachedBinaryVersions,
|
|
975
|
+
releasesBase,
|
|
476
976
|
ensureBinaries,
|
|
477
977
|
startService,
|
|
478
978
|
stopService,
|
|
479
979
|
serviceStatus,
|
|
480
980
|
setPassword,
|
|
981
|
+
isLoopbackHostname,
|
|
982
|
+
loadWorkerEndpoint,
|
|
983
|
+
requestPackInspect,
|
|
984
|
+
formatPackSummary,
|
|
985
|
+
requestJSON,
|
|
986
|
+
requestPackInstall,
|
|
987
|
+
parsePackRenames,
|
|
988
|
+
parsePackID,
|
|
989
|
+
formatPackInstallPlan,
|
|
990
|
+
formatPackList,
|
|
991
|
+
packCommand,
|
|
481
992
|
main,
|
|
482
993
|
};
|
|
483
994
|
|
|
484
995
|
if (require.main === module) {
|
|
485
996
|
main().catch((error) => {
|
|
486
997
|
console.error(error instanceof Error ? error.message : String(error));
|
|
487
|
-
process.exitCode = 1;
|
|
998
|
+
process.exitCode = Number.isInteger(error?.exitCode) ? error.exitCode : 1;
|
|
488
999
|
});
|
|
489
1000
|
}
|