blun-king-cli 9.1.602 → 9.1.604
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 +118 -99
- package/LIESMICH.txt +74 -57
- package/README.md +119 -76
- package/bin/agentspine-king-goal-inbox.mjs +127 -127
- package/bin/agentspine-king-goal-intake.mjs +106 -106
- package/bin/agentspine-king-host-runner.mjs +109 -109
- package/bin/agentspine-king-snapshot-policy.cjs +80 -80
- package/bin/agentspine-king-status-policy.mjs +226 -226
- package/bin/agentspine-king-worker-host.mjs +160 -160
- package/bin/core-bootstrap.js +77 -77
- package/bin/launcher-mode.js +93 -93
- package/bin/launcher-restart-policy.cjs +131 -131
- package/bin/launcher-runtime.js +491 -491
- package/bin/managed-node.js +186 -186
- package/bin/managed-plugin-selection.cjs +115 -115
- package/bin/native-module-repair.js +201 -201
- package/bin/node-runtime.js +34 -34
- package/bin/node-version.js +49 -49
- package/bin/plugin-bootstrap.js +162 -162
- package/bin/private-paths.js +1606 -1606
- package/bin/runtime-exit-ledger.cjs +144 -144
- package/bin/runtime-exit-ledger.d.cts +23 -23
- package/bin/standard-tools-bootstrap.js +272 -272
- package/bin/turn-thinking-policy.cjs +82 -82
- package/bin/update-copy.js +200 -200
- package/bin/update-lease.js +597 -597
- package/bin/update-notice.js +1568 -1568
- package/bin/verify-agent-behavior.cjs +122 -122
- package/bin/verify-agent-components.cjs +104 -104
- package/bin/verify-bundled-agent-sources.cjs +98 -76
- package/bin/windows-node-crash-dump.cjs +289 -289
- package/blun.mjs +8041 -6407
- package/codebase-index/codebase_index.py +470 -470
- package/dist-web/assets/{CodeBlockNode-CXnTLMNr.js → CodeBlockNode-TKKkJX8L.js} +1 -1
- package/dist-web/assets/{CodeBlockShell.vue_vue_type_style_index_0_lang-CCF8Ntnq.js → CodeBlockShell.vue_vue_type_style_index_0_lang-CUj2Ds5x.js} +1 -1
- package/dist-web/assets/DesignSystemView-DwK1aQMo.js +12 -0
- package/dist-web/assets/DesignSystemView-szjTj7ns.css +1 -0
- package/dist-web/assets/{Tooltip-CFQEzqC-.js → Tooltip-CTWv7WCU.js} +1 -1
- package/dist-web/assets/{abnfDiagram-N423BO3Z-Z1APZG5m.js → abnfDiagram-N423BO3Z-DA8V8Zjh.js} +1 -1
- package/dist-web/assets/{arc-C4VyWloT.js → arc-YtddKbdY.js} +1 -1
- package/dist-web/assets/{architectureDiagram-T3A2C74G-DpddbUcM.js → architectureDiagram-T3A2C74G-CGEiObmo.js} +1 -1
- package/dist-web/assets/{blockDiagram-VBNYF7ZC-BqccOSMr.js → blockDiagram-VBNYF7ZC-Bd5Vt50e.js} +1 -1
- package/dist-web/assets/{c4Diagram-5PPSVZJV-OHAcJrTy.js → c4Diagram-5PPSVZJV-D2ApPM54.js} +1 -1
- package/dist-web/assets/channel-BnluexuN.js +1 -0
- package/dist-web/assets/{chunk-2GRJ4B5K-DqJHs8Fw.js → chunk-2GRJ4B5K-BDcIEZSl.js} +1 -1
- package/dist-web/assets/{chunk-2Q5K7J3B-B1nCd91o.js → chunk-2Q5K7J3B-6aSabCX7.js} +1 -1
- package/dist-web/assets/{chunk-5RXB4S5H-neG70kZx.js → chunk-5RXB4S5H-BFRQvIJS.js} +1 -1
- package/dist-web/assets/{chunk-5VM5RSS4-l7biGqeH.js → chunk-5VM5RSS4-ChAa-DNY.js} +1 -1
- package/dist-web/assets/{chunk-6Q2QTUOP-BO-F11Gs.js → chunk-6Q2QTUOP-BIfrb2Jc.js} +1 -1
- package/dist-web/assets/{chunk-GF5L2VYU-BtC69fw0.js → chunk-GF5L2VYU-BR0gvtWI.js} +1 -1
- package/dist-web/assets/{chunk-JWPE2WC7-DIOubEqr.js → chunk-JWPE2WC7-BgXKsx5E.js} +1 -1
- package/dist-web/assets/{chunk-KBJHAD2P-DDUeH_Ba.js → chunk-KBJHAD2P-I8mrZhpz.js} +1 -1
- package/dist-web/assets/{chunk-RYQCIY6F-B9gFMVbS.js → chunk-RYQCIY6F-BMPaem92.js} +1 -1
- package/dist-web/assets/{chunk-XXDRQBXY-Bn1Xz6TQ.js → chunk-XXDRQBXY-COy0J-0j.js} +1 -1
- package/dist-web/assets/classDiagram-JCYQIIEL-BliFdSD9.js +1 -0
- package/dist-web/assets/classDiagram-v2-OCEON4UE-BliFdSD9.js +1 -0
- package/dist-web/assets/{cose-bilkent-JH36ORCC-DqigO3T4.js → cose-bilkent-JH36ORCC-MosTTRdo.js} +1 -1
- package/dist-web/assets/{cynefin-VYW2F7L2-rpD93tQ-.js → cynefin-VYW2F7L2-5tjPgrkP.js} +1 -1
- package/dist-web/assets/{cynefinDiagram-MW4NZA55-DbTqnkVJ.js → cynefinDiagram-MW4NZA55-CbDcdInR.js} +1 -1
- package/dist-web/assets/{dagre-VZM6K2ZE-DcM8aEVY.js → dagre-VZM6K2ZE-YaBom1ZH.js} +1 -1
- package/dist-web/assets/{diagram-7IWD3JNH-UymgsBNa.js → diagram-7IWD3JNH-Da4m-Hgz.js} +1 -1
- package/dist-web/assets/{diagram-B4RE2ZJO-BiYu5WPF.js → diagram-B4RE2ZJO-CAUUegxg.js} +1 -1
- package/dist-web/assets/{diagram-LBJQPF4R-BD3kkgOO.js → diagram-LBJQPF4R-o21mWKG4.js} +1 -1
- package/dist-web/assets/{diagram-Q27KOJAE-D1mYQ_cI.js → diagram-Q27KOJAE-BA3z3sXf.js} +1 -1
- package/dist-web/assets/{diagram-UB23O5K3-BtX8u6HG.js → diagram-UB23O5K3-CGRdSSDK.js} +1 -1
- package/dist-web/assets/{ebnfDiagram-BXEA7PRR-CBAnFkRF.js → ebnfDiagram-BXEA7PRR-Bw-iU9FP.js} +1 -1
- package/dist-web/assets/{erDiagram-JOGREHBK-3vKcWsxm.js → erDiagram-JOGREHBK-BwM1yiR0.js} +1 -1
- package/dist-web/assets/{flowDiagram-UKHOOZJN-D9wBbHm2.js → flowDiagram-UKHOOZJN-z05eOR1Q.js} +1 -1
- package/dist-web/assets/{ganttDiagram-PKOTCBZU-Cd_dViF-.js → ganttDiagram-PKOTCBZU-BMUubNTl.js} +1 -1
- package/dist-web/assets/{gitGraphDiagram-DS77QQ5N-BiEEtTo7.js → gitGraphDiagram-DS77QQ5N-Dccbi1wU.js} +1 -1
- package/dist-web/assets/{index-DdEakFJM.js → index-BBbC5LRF.js} +1 -1
- package/dist-web/assets/{index-BmKj7CB1.css → index-C9eDH3u-.css} +1 -1
- package/dist-web/assets/{index-Ba1OvLjf.js → index-D48p0M2S.js} +2 -2
- package/dist-web/assets/{index-CiPA8Kex.js → index-h0KEOmkU.js} +14 -14
- package/dist-web/assets/{index10-CNr4U-Nw.js → index10-Qbuw1N6u.js} +1 -1
- package/dist-web/assets/{index11-Dto_yZNz.js → index11-g8v025wN.js} +1 -1
- package/dist-web/assets/{index5-B2PldaO2.js → index5-BYNcV8BY.js} +1 -1
- package/dist-web/assets/{index6-CBdZmmyx.js → index6-BetANtnV.js} +1 -1
- package/dist-web/assets/{index7-Dz9oCsUF.js → index7-BcPbgCKN.js} +1 -1
- package/dist-web/assets/{index8-B6k25jtU.js → index8-B1lp-RvF.js} +2 -2
- package/dist-web/assets/{infoDiagram-6WML65LV-D4ph2sIH.js → infoDiagram-6WML65LV-BF_pbUnN.js} +1 -1
- package/dist-web/assets/{ishikawaDiagram-WSZJBQD7-DjKjbMm9.js → ishikawaDiagram-WSZJBQD7-Dedv4urS.js} +1 -1
- package/dist-web/assets/{journeyDiagram-NVQOT4AX-DttHDfPQ.js → journeyDiagram-NVQOT4AX-B1Ex3b6o.js} +1 -1
- package/dist-web/assets/{kanban-definition-27J2QSJJ-C5gB-W0d.js → kanban-definition-27J2QSJJ-BtCGliBH.js} +1 -1
- package/dist-web/assets/{linear-BRGe4-d6.js → linear-ByItc8Qr.js} +1 -1
- package/dist-web/assets/{mermaid.core-D-9Sx1No.js → mermaid.core-B1M2O7nN.js} +4 -4
- package/dist-web/assets/{mindmap-definition-FAOFIHXS-Cg_hoVIq.js → mindmap-definition-FAOFIHXS-D0UHKUyj.js} +1 -1
- package/dist-web/assets/{pegDiagram-VL7TDLO6-CSX9hitD.js → pegDiagram-VL7TDLO6-D7zYCVmG.js} +1 -1
- package/dist-web/assets/{pieDiagram-7S7Q4E2Y-0L0mxahZ.js → pieDiagram-7S7Q4E2Y-Bd0i9CHa.js} +1 -1
- package/dist-web/assets/{quadrantDiagram-CIZ2JOQS-U483z0er.js → quadrantDiagram-CIZ2JOQS-MG-Ug47d.js} +1 -1
- package/dist-web/assets/{railroadDiagram-AXF67PYL-D80a2_DJ.js → railroadDiagram-AXF67PYL-AZdl90Gj.js} +1 -1
- package/dist-web/assets/{requirementDiagram-LRYGKXZP-CeyHfq8t.js → requirementDiagram-LRYGKXZP-DjplF9kt.js} +1 -1
- package/dist-web/assets/{sankeyDiagram-W5VNT64P-CCISkkrR.js → sankeyDiagram-W5VNT64P-Bi9Zgfjs.js} +1 -1
- package/dist-web/assets/{sequenceDiagram-SI44F4Z6-jQfm2XS7.js → sequenceDiagram-SI44F4Z6-ChEpdCvb.js} +1 -1
- package/dist-web/assets/{sizeCapture-X5ZJPWSS-lRZsmCoS.js → sizeCapture-X5ZJPWSS-B5RFUdaa.js} +1 -1
- package/dist-web/assets/{stateDiagram-OKZ733FA-ClCgTdUm.js → stateDiagram-OKZ733FA-BkRblSKp.js} +1 -1
- package/dist-web/assets/stateDiagram-v2-UEYNNEHI-fVkQcACW.js +1 -0
- package/dist-web/assets/{swimlanes-SLNWSIFB-BCdOUN05.js → swimlanes-SLNWSIFB-DLXH6Zhc.js} +2 -2
- package/dist-web/assets/swimlanesDiagram-ULZ7WXOC-Bg-BiZJQ.js +8 -0
- package/dist-web/assets/{timeline-definition-Z64GVDOM-DpWutSKa.js → timeline-definition-Z64GVDOM-C_N88siO.js} +1 -1
- package/dist-web/assets/{vennDiagram-T6HMQDX7-DVC5JVsc.js → vennDiagram-T6HMQDX7-DMsnvDt0.js} +1 -1
- package/dist-web/assets/{vue.runtime.esm-bundler-BwwRtQwQ.js → vue.runtime.esm-bundler-B-3uIy85.js} +1 -1
- package/dist-web/assets/{wardleyDiagram-T6FBY63Y-BK2mrq7L.js → wardleyDiagram-T6FBY63Y-C9SJIPzq.js} +1 -1
- package/dist-web/assets/{xychartDiagram-ELKLHX3M-DUJu9F7-.js → xychartDiagram-ELKLHX3M-DZLWA6Go.js} +1 -1
- package/dist-web/index.html +32 -32
- package/dist-web/vis/index.html.gz +0 -0
- package/package.json +1 -1
- package/scripts/fix-node-pty-perms.js +53 -53
- package/standard-skills/blun-app-design-system/SKILL.md +30 -30
- package/standard-skills/blun-app-design-system/provenance.json +17 -17
- package/standard-skills/blun-app-design-system/references/tokens.md +48 -48
- package/standard-skills/blun-app-design-system/scripts/check-blun-design.mjs +49 -49
- package/telegram-plugin/blun.plugin.json +20 -20
- package/telegram-plugin/commands/access.md +37 -37
- package/telegram-plugin/dist/bridge.mjs +117 -100
- package/telegram-plugin/dist/mcp-server.mjs +185 -161
- package/worker-host.mjs +1458 -1022
- package/dist-web/assets/DesignSystemView-6HgAUEmC.css +0 -1
- package/dist-web/assets/DesignSystemView-BbMOu-XO.js +0 -12
- package/dist-web/assets/channel-B2VOQOI-.js +0 -1
- package/dist-web/assets/classDiagram-JCYQIIEL-BA1TjAxj.js +0 -1
- package/dist-web/assets/classDiagram-v2-OCEON4UE-BA1TjAxj.js +0 -1
- package/dist-web/assets/stateDiagram-v2-UEYNNEHI-fi_lrTAx.js +0 -1
- package/dist-web/assets/swimlanesDiagram-ULZ7WXOC-BUd_uYK3.js +0 -8
|
@@ -1,144 +1,144 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const os = require('node:os');
|
|
5
|
-
const path = require('node:path');
|
|
6
|
-
|
|
7
|
-
const DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
8
|
-
const DEFAULT_RETAIN_BYTES = 512 * 1024;
|
|
9
|
-
|
|
10
|
-
function runtimeExitLedgerPath(homeDir) {
|
|
11
|
-
return path.join(homeDir, 'diagnostics', 'runtime-exits.jsonl');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function sanitizeExitText(input, maxChars = 2048) {
|
|
15
|
-
let value = String(input ?? '');
|
|
16
|
-
const homes = new Set([os.homedir(), process.env.USERPROFILE, process.env.HOME]
|
|
17
|
-
.filter((entry) => typeof entry === 'string' && entry.length > 2));
|
|
18
|
-
for (const home of homes) {
|
|
19
|
-
for (const variant of [path.normalize(home), home.replaceAll('\\', '/')]) {
|
|
20
|
-
const escaped = variant.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
21
|
-
value = value.replace(new RegExp(escaped, process.platform === 'win32' ? 'giu' : 'gu'), '$HOME');
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
value = value
|
|
25
|
-
.replace(/[A-Za-z]:[\\/]Users[\\/][^\\/\s"']+/giu, '$HOME')
|
|
26
|
-
.replace(/\/home\/[^/\s"']+/gu, '$HOME')
|
|
27
|
-
.replace(/(Authorization\s*:\s*Bearer\s+)[^\s,;]+/giu, '$1<redacted>')
|
|
28
|
-
.replace(/\bsk-[A-Za-z0-9_-]{10,}\b/gu, '<redacted>')
|
|
29
|
-
.replace(/\b\d{6,}:[A-Za-z0-9_-]{20,}\b/gu, '<redacted>')
|
|
30
|
-
.replace(/("(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password)"\s*:\s*)"(?:\\.|[^"\\])*"/giu, '$1"<redacted>"')
|
|
31
|
-
.replace(/((?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password)\s*[=:]\s*)[^\s,;}]+/giu, '$1<redacted>')
|
|
32
|
-
.replace(/(https?:\/\/[^\s/:@]+:)[^\s/@]+@/giu, '$1<redacted>@');
|
|
33
|
-
return value.length <= maxChars ? value : `${value.slice(0, maxChars)}...[truncated]`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function buildRuntimeExitRecord(input) {
|
|
37
|
-
const now = typeof input.now === 'function' ? input.now() : new Date();
|
|
38
|
-
const record = { schemaVersion: 1, ts: new Date(now).toISOString() };
|
|
39
|
-
for (const key of ['source', 'kind', 'signal', 'phase', 'cliVersion', 'profile', 'sessionId', 'lastTool', 'detail']) {
|
|
40
|
-
if (input[key] !== undefined && input[key] !== '') record[key] = sanitizeExitText(input[key]);
|
|
41
|
-
}
|
|
42
|
-
for (const [key, value] of Object.entries({ exitCode: input.exitCode, pid: input.pid ?? process.pid,
|
|
43
|
-
ppid: input.ppid ?? process.ppid, childPid: input.childPid })) {
|
|
44
|
-
if (Number.isInteger(value)) record[key] = value;
|
|
45
|
-
}
|
|
46
|
-
if (typeof input.loaded === 'boolean') record.loaded = input.loaded;
|
|
47
|
-
if (input.error !== undefined && input.error !== null) {
|
|
48
|
-
record.error = {
|
|
49
|
-
name: sanitizeExitText(input.error.name || 'Error', 160),
|
|
50
|
-
message: sanitizeExitText(input.error.message || input.error),
|
|
51
|
-
stack: sanitizeExitText(input.error.stack || '', 8192),
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
return record;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function directoryIdentity(directory) {
|
|
58
|
-
const stat = fs.lstatSync(directory, { bigint: true });
|
|
59
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('Unsafe exit directory');
|
|
60
|
-
return stat;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function sameIdentity(left, right) {
|
|
64
|
-
return left.dev === right.dev && left.ino === right.ino;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function regularLedger(stat) {
|
|
68
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n) throw new Error('Unsafe exit ledger');
|
|
69
|
-
return stat;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function trimLedger(descriptor, maxBytes, retainBytes, incomingBytes) {
|
|
73
|
-
const size = Number(regularLedger(fs.fstatSync(descriptor, { bigint: true })).size);
|
|
74
|
-
if (!Number.isSafeInteger(size)) throw new Error('Invalid exit ledger size');
|
|
75
|
-
if (size + incomingBytes <= maxBytes) return;
|
|
76
|
-
const keepBytes = Math.max(0, Math.min(size, retainBytes, maxBytes - incomingBytes));
|
|
77
|
-
const contents = Buffer.alloc(keepBytes);
|
|
78
|
-
const read = fs.readSync(descriptor, contents, 0, keepBytes, size - keepBytes);
|
|
79
|
-
const tail = contents.subarray(0, read);
|
|
80
|
-
const lineStart = size > keepBytes ? tail.indexOf(0x0a) + 1 : 0;
|
|
81
|
-
fs.ftruncateSync(descriptor, 0);
|
|
82
|
-
fs.writeFileSync(descriptor, lineStart > 0 || size === keepBytes ? tail.subarray(lineStart) : '');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function recordRuntimeExit(input = {}) {
|
|
86
|
-
let descriptor;
|
|
87
|
-
let appendDescriptor;
|
|
88
|
-
try {
|
|
89
|
-
const homeDir = typeof input.homeDir === 'string' ? input.homeDir.trim() : '';
|
|
90
|
-
if (!homeDir || !path.isAbsolute(homeDir)) return false;
|
|
91
|
-
// Pin the private home and diagnostic directory before opening any output.
|
|
92
|
-
fs.mkdirSync(homeDir, { recursive: true, mode: 0o700 });
|
|
93
|
-
const homeIdentity = directoryIdentity(homeDir);
|
|
94
|
-
const file = runtimeExitLedgerPath(homeDir);
|
|
95
|
-
const directory = path.dirname(file);
|
|
96
|
-
try { fs.mkdirSync(directory, { mode: 0o700 }); }
|
|
97
|
-
catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
98
|
-
const directoryStat = directoryIdentity(directory);
|
|
99
|
-
let before;
|
|
100
|
-
try { before = regularLedger(fs.lstatSync(file, { bigint: true })); }
|
|
101
|
-
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
102
|
-
const flags = fs.constants.O_RDWR | (fs.constants.O_NOFOLLOW ?? 0)
|
|
103
|
-
| (fs.constants.O_NONBLOCK ?? 0) | (before ? 0 : fs.constants.O_CREAT | fs.constants.O_EXCL);
|
|
104
|
-
descriptor = fs.openSync(file, flags, 0o600);
|
|
105
|
-
const opened = regularLedger(fs.fstatSync(descriptor, { bigint: true }));
|
|
106
|
-
if ((before && !sameIdentity(before, opened))
|
|
107
|
-
|| !sameIdentity(opened, regularLedger(fs.lstatSync(file, { bigint: true })))
|
|
108
|
-
|| !sameIdentity(homeIdentity, directoryIdentity(homeDir))
|
|
109
|
-
|| !sameIdentity(directoryStat, directoryIdentity(directory))) throw new Error('Exit ledger changed');
|
|
110
|
-
const maxBytes = Math.max(1024, Number.isInteger(input.maxBytes) ? input.maxBytes : DEFAULT_MAX_BYTES);
|
|
111
|
-
const retainBytes = Math.max(0, Number.isInteger(input.retainBytes) ? input.retainBytes : DEFAULT_RETAIN_BYTES);
|
|
112
|
-
const record = buildRuntimeExitRecord(input);
|
|
113
|
-
let line = `${JSON.stringify(record)}\n`;
|
|
114
|
-
// Retain identity/exit fields even when a diagnostic detail exceeds the cap.
|
|
115
|
-
for (const key of ['error', 'detail', 'lastTool', 'sessionId', 'profile', 'cliVersion', 'phase', 'source', 'kind', 'signal']) {
|
|
116
|
-
if (Buffer.byteLength(line) <= maxBytes) break;
|
|
117
|
-
delete record[key];
|
|
118
|
-
record.truncated = true;
|
|
119
|
-
line = `${JSON.stringify(record)}\n`;
|
|
120
|
-
}
|
|
121
|
-
trimLedger(descriptor, maxBytes, retainBytes, Buffer.byteLength(line));
|
|
122
|
-
// Windows append-only handles cannot truncate. Keep both handles bound to
|
|
123
|
-
// the same checked inode, using the second handle only for atomic append.
|
|
124
|
-
appendDescriptor = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_APPEND
|
|
125
|
-
| (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
|
|
126
|
-
if (!sameIdentity(opened, regularLedger(fs.fstatSync(appendDescriptor, { bigint: true })))
|
|
127
|
-
|| !sameIdentity(opened, regularLedger(fs.lstatSync(file, { bigint: true })))
|
|
128
|
-
|| !sameIdentity(homeIdentity, directoryIdentity(homeDir))
|
|
129
|
-
|| !sameIdentity(directoryStat, directoryIdentity(directory))) throw new Error('Exit ledger changed');
|
|
130
|
-
fs.appendFileSync(appendDescriptor, line, { encoding: 'utf8' });
|
|
131
|
-
try { fs.fchmodSync(descriptor, 0o600); } catch {}
|
|
132
|
-
return true;
|
|
133
|
-
} catch {
|
|
134
|
-
return false;
|
|
135
|
-
} finally {
|
|
136
|
-
if (appendDescriptor !== undefined) { try { fs.closeSync(appendDescriptor); } catch {} }
|
|
137
|
-
if (descriptor !== undefined) { try { fs.closeSync(descriptor); } catch {} }
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
module.exports = {
|
|
142
|
-
DEFAULT_MAX_BYTES, DEFAULT_RETAIN_BYTES, buildRuntimeExitRecord, recordRuntimeExit,
|
|
143
|
-
runtimeExitLedgerPath, sanitizeExitText,
|
|
144
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
8
|
+
const DEFAULT_RETAIN_BYTES = 512 * 1024;
|
|
9
|
+
|
|
10
|
+
function runtimeExitLedgerPath(homeDir) {
|
|
11
|
+
return path.join(homeDir, 'diagnostics', 'runtime-exits.jsonl');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function sanitizeExitText(input, maxChars = 2048) {
|
|
15
|
+
let value = String(input ?? '');
|
|
16
|
+
const homes = new Set([os.homedir(), process.env.USERPROFILE, process.env.HOME]
|
|
17
|
+
.filter((entry) => typeof entry === 'string' && entry.length > 2));
|
|
18
|
+
for (const home of homes) {
|
|
19
|
+
for (const variant of [path.normalize(home), home.replaceAll('\\', '/')]) {
|
|
20
|
+
const escaped = variant.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
21
|
+
value = value.replace(new RegExp(escaped, process.platform === 'win32' ? 'giu' : 'gu'), '$HOME');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
value = value
|
|
25
|
+
.replace(/[A-Za-z]:[\\/]Users[\\/][^\\/\s"']+/giu, '$HOME')
|
|
26
|
+
.replace(/\/home\/[^/\s"']+/gu, '$HOME')
|
|
27
|
+
.replace(/(Authorization\s*:\s*Bearer\s+)[^\s,;]+/giu, '$1<redacted>')
|
|
28
|
+
.replace(/\bsk-[A-Za-z0-9_-]{10,}\b/gu, '<redacted>')
|
|
29
|
+
.replace(/\b\d{6,}:[A-Za-z0-9_-]{20,}\b/gu, '<redacted>')
|
|
30
|
+
.replace(/("(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password)"\s*:\s*)"(?:\\.|[^"\\])*"/giu, '$1"<redacted>"')
|
|
31
|
+
.replace(/((?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password)\s*[=:]\s*)[^\s,;}]+/giu, '$1<redacted>')
|
|
32
|
+
.replace(/(https?:\/\/[^\s/:@]+:)[^\s/@]+@/giu, '$1<redacted>@');
|
|
33
|
+
return value.length <= maxChars ? value : `${value.slice(0, maxChars)}...[truncated]`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildRuntimeExitRecord(input) {
|
|
37
|
+
const now = typeof input.now === 'function' ? input.now() : new Date();
|
|
38
|
+
const record = { schemaVersion: 1, ts: new Date(now).toISOString() };
|
|
39
|
+
for (const key of ['source', 'kind', 'signal', 'phase', 'cliVersion', 'profile', 'sessionId', 'lastTool', 'detail']) {
|
|
40
|
+
if (input[key] !== undefined && input[key] !== '') record[key] = sanitizeExitText(input[key]);
|
|
41
|
+
}
|
|
42
|
+
for (const [key, value] of Object.entries({ exitCode: input.exitCode, pid: input.pid ?? process.pid,
|
|
43
|
+
ppid: input.ppid ?? process.ppid, childPid: input.childPid })) {
|
|
44
|
+
if (Number.isInteger(value)) record[key] = value;
|
|
45
|
+
}
|
|
46
|
+
if (typeof input.loaded === 'boolean') record.loaded = input.loaded;
|
|
47
|
+
if (input.error !== undefined && input.error !== null) {
|
|
48
|
+
record.error = {
|
|
49
|
+
name: sanitizeExitText(input.error.name || 'Error', 160),
|
|
50
|
+
message: sanitizeExitText(input.error.message || input.error),
|
|
51
|
+
stack: sanitizeExitText(input.error.stack || '', 8192),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return record;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function directoryIdentity(directory) {
|
|
58
|
+
const stat = fs.lstatSync(directory, { bigint: true });
|
|
59
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('Unsafe exit directory');
|
|
60
|
+
return stat;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sameIdentity(left, right) {
|
|
64
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function regularLedger(stat) {
|
|
68
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n) throw new Error('Unsafe exit ledger');
|
|
69
|
+
return stat;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function trimLedger(descriptor, maxBytes, retainBytes, incomingBytes) {
|
|
73
|
+
const size = Number(regularLedger(fs.fstatSync(descriptor, { bigint: true })).size);
|
|
74
|
+
if (!Number.isSafeInteger(size)) throw new Error('Invalid exit ledger size');
|
|
75
|
+
if (size + incomingBytes <= maxBytes) return;
|
|
76
|
+
const keepBytes = Math.max(0, Math.min(size, retainBytes, maxBytes - incomingBytes));
|
|
77
|
+
const contents = Buffer.alloc(keepBytes);
|
|
78
|
+
const read = fs.readSync(descriptor, contents, 0, keepBytes, size - keepBytes);
|
|
79
|
+
const tail = contents.subarray(0, read);
|
|
80
|
+
const lineStart = size > keepBytes ? tail.indexOf(0x0a) + 1 : 0;
|
|
81
|
+
fs.ftruncateSync(descriptor, 0);
|
|
82
|
+
fs.writeFileSync(descriptor, lineStart > 0 || size === keepBytes ? tail.subarray(lineStart) : '');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function recordRuntimeExit(input = {}) {
|
|
86
|
+
let descriptor;
|
|
87
|
+
let appendDescriptor;
|
|
88
|
+
try {
|
|
89
|
+
const homeDir = typeof input.homeDir === 'string' ? input.homeDir.trim() : '';
|
|
90
|
+
if (!homeDir || !path.isAbsolute(homeDir)) return false;
|
|
91
|
+
// Pin the private home and diagnostic directory before opening any output.
|
|
92
|
+
fs.mkdirSync(homeDir, { recursive: true, mode: 0o700 });
|
|
93
|
+
const homeIdentity = directoryIdentity(homeDir);
|
|
94
|
+
const file = runtimeExitLedgerPath(homeDir);
|
|
95
|
+
const directory = path.dirname(file);
|
|
96
|
+
try { fs.mkdirSync(directory, { mode: 0o700 }); }
|
|
97
|
+
catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
98
|
+
const directoryStat = directoryIdentity(directory);
|
|
99
|
+
let before;
|
|
100
|
+
try { before = regularLedger(fs.lstatSync(file, { bigint: true })); }
|
|
101
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
102
|
+
const flags = fs.constants.O_RDWR | (fs.constants.O_NOFOLLOW ?? 0)
|
|
103
|
+
| (fs.constants.O_NONBLOCK ?? 0) | (before ? 0 : fs.constants.O_CREAT | fs.constants.O_EXCL);
|
|
104
|
+
descriptor = fs.openSync(file, flags, 0o600);
|
|
105
|
+
const opened = regularLedger(fs.fstatSync(descriptor, { bigint: true }));
|
|
106
|
+
if ((before && !sameIdentity(before, opened))
|
|
107
|
+
|| !sameIdentity(opened, regularLedger(fs.lstatSync(file, { bigint: true })))
|
|
108
|
+
|| !sameIdentity(homeIdentity, directoryIdentity(homeDir))
|
|
109
|
+
|| !sameIdentity(directoryStat, directoryIdentity(directory))) throw new Error('Exit ledger changed');
|
|
110
|
+
const maxBytes = Math.max(1024, Number.isInteger(input.maxBytes) ? input.maxBytes : DEFAULT_MAX_BYTES);
|
|
111
|
+
const retainBytes = Math.max(0, Number.isInteger(input.retainBytes) ? input.retainBytes : DEFAULT_RETAIN_BYTES);
|
|
112
|
+
const record = buildRuntimeExitRecord(input);
|
|
113
|
+
let line = `${JSON.stringify(record)}\n`;
|
|
114
|
+
// Retain identity/exit fields even when a diagnostic detail exceeds the cap.
|
|
115
|
+
for (const key of ['error', 'detail', 'lastTool', 'sessionId', 'profile', 'cliVersion', 'phase', 'source', 'kind', 'signal']) {
|
|
116
|
+
if (Buffer.byteLength(line) <= maxBytes) break;
|
|
117
|
+
delete record[key];
|
|
118
|
+
record.truncated = true;
|
|
119
|
+
line = `${JSON.stringify(record)}\n`;
|
|
120
|
+
}
|
|
121
|
+
trimLedger(descriptor, maxBytes, retainBytes, Buffer.byteLength(line));
|
|
122
|
+
// Windows append-only handles cannot truncate. Keep both handles bound to
|
|
123
|
+
// the same checked inode, using the second handle only for atomic append.
|
|
124
|
+
appendDescriptor = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_APPEND
|
|
125
|
+
| (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
|
|
126
|
+
if (!sameIdentity(opened, regularLedger(fs.fstatSync(appendDescriptor, { bigint: true })))
|
|
127
|
+
|| !sameIdentity(opened, regularLedger(fs.lstatSync(file, { bigint: true })))
|
|
128
|
+
|| !sameIdentity(homeIdentity, directoryIdentity(homeDir))
|
|
129
|
+
|| !sameIdentity(directoryStat, directoryIdentity(directory))) throw new Error('Exit ledger changed');
|
|
130
|
+
fs.appendFileSync(appendDescriptor, line, { encoding: 'utf8' });
|
|
131
|
+
try { fs.fchmodSync(descriptor, 0o600); } catch {}
|
|
132
|
+
return true;
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
} finally {
|
|
136
|
+
if (appendDescriptor !== undefined) { try { fs.closeSync(appendDescriptor); } catch {} }
|
|
137
|
+
if (descriptor !== undefined) { try { fs.closeSync(descriptor); } catch {} }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
DEFAULT_MAX_BYTES, DEFAULT_RETAIN_BYTES, buildRuntimeExitRecord, recordRuntimeExit,
|
|
143
|
+
runtimeExitLedgerPath, sanitizeExitText,
|
|
144
|
+
};
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
export interface RuntimeExitInput {
|
|
2
|
-
homeDir?: string;
|
|
3
|
-
source?: string;
|
|
4
|
-
kind?: string;
|
|
5
|
-
signal?: string;
|
|
6
|
-
phase?: string;
|
|
7
|
-
cliVersion?: string;
|
|
8
|
-
profile?: string;
|
|
9
|
-
sessionId?: string;
|
|
10
|
-
lastTool?: string;
|
|
11
|
-
detail?: string;
|
|
12
|
-
exitCode?: number;
|
|
13
|
-
pid?: number;
|
|
14
|
-
ppid?: number;
|
|
15
|
-
childPid?: number;
|
|
16
|
-
loaded?: boolean;
|
|
17
|
-
error?: unknown;
|
|
18
|
-
maxBytes?: number;
|
|
19
|
-
retainBytes?: number;
|
|
20
|
-
now?: () => Date | number | string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function recordRuntimeExit(input?: RuntimeExitInput): boolean;
|
|
1
|
+
export interface RuntimeExitInput {
|
|
2
|
+
homeDir?: string;
|
|
3
|
+
source?: string;
|
|
4
|
+
kind?: string;
|
|
5
|
+
signal?: string;
|
|
6
|
+
phase?: string;
|
|
7
|
+
cliVersion?: string;
|
|
8
|
+
profile?: string;
|
|
9
|
+
sessionId?: string;
|
|
10
|
+
lastTool?: string;
|
|
11
|
+
detail?: string;
|
|
12
|
+
exitCode?: number;
|
|
13
|
+
pid?: number;
|
|
14
|
+
ppid?: number;
|
|
15
|
+
childPid?: number;
|
|
16
|
+
loaded?: boolean;
|
|
17
|
+
error?: unknown;
|
|
18
|
+
maxBytes?: number;
|
|
19
|
+
retainBytes?: number;
|
|
20
|
+
now?: () => Date | number | string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function recordRuntimeExit(input?: RuntimeExitInput): boolean;
|