glassbox-trace 0.3.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/LICENSE +21 -0
- package/README.md +120 -0
- package/bin/glassbox.mjs +3 -0
- package/dist/glassbox.html +1653 -0
- package/package.json +54 -0
- package/src/cli.mjs +246 -0
- package/src/trace-core.js +768 -0
- package/src/viewer.html +875 -0
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "glassbox-trace",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "The flight recorder viewer for Claude Code / Agent SDK sessions: drop a session .jsonl on one HTML file and get a timeline, token and cost burn, and auto-diagnostics. Includes a CLI and a Claude Code Stop hook.",
|
|
5
|
+
"main": "src/trace-core.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node --test test/*.test.mjs",
|
|
8
|
+
"build": "node scripts/build.mjs",
|
|
9
|
+
"sanitize": "node scripts/sanitize.mjs",
|
|
10
|
+
"e2e": "node test/browser.e2e.mjs",
|
|
11
|
+
"check": "npm test && npm run build && npm run e2e",
|
|
12
|
+
"audit": "node test/audit.mjs",
|
|
13
|
+
"prepublishOnly": "npm test && npm run build"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Aldo Hushi",
|
|
17
|
+
"bin": {
|
|
18
|
+
"glassbox": "bin/glassbox.mjs"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"bin",
|
|
22
|
+
"src",
|
|
23
|
+
"dist/glassbox.html",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"claude-code",
|
|
32
|
+
"claude",
|
|
33
|
+
"agent",
|
|
34
|
+
"agent-sdk",
|
|
35
|
+
"trace",
|
|
36
|
+
"jsonl",
|
|
37
|
+
"viewer",
|
|
38
|
+
"observability",
|
|
39
|
+
"tokens",
|
|
40
|
+
"cost",
|
|
41
|
+
"accessibility"
|
|
42
|
+
],
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"playwright": "^1.49.0"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/aldohushi1-stack/glassbox#readme",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/aldohushi1-stack/glassbox.git"
|
|
50
|
+
},
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/aldohushi1-stack/glassbox/issues"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Glassbox CLI library — session discovery, embedding, checking. No dependencies.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const core = require('./trace-core.js');
|
|
9
|
+
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
|
|
10
|
+
|
|
11
|
+
export const claudeHome = (env = process.env) => env.GLASSBOX_HOME || path.join(env.HOME || env.USERPROFILE || os.homedir(), '.claude');
|
|
12
|
+
export const decodeProject = (n) => n.replace(/^-/, '/').replace(/-/g, '/');
|
|
13
|
+
|
|
14
|
+
export function findSessions(opts = {}) {
|
|
15
|
+
const home = opts.home || claudeHome();
|
|
16
|
+
const projectsDir = path.join(home, 'projects');
|
|
17
|
+
if (!fs.existsSync(projectsDir)) return [];
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const p of fs.readdirSync(projectsDir, { withFileTypes: true })) {
|
|
20
|
+
if (!p.isDirectory()) continue;
|
|
21
|
+
const pdir = path.join(projectsDir, p.name);
|
|
22
|
+
for (const f of fs.readdirSync(pdir, { withFileTypes: true })) {
|
|
23
|
+
if (!f.isFile() || !f.name.endsWith('.jsonl')) continue;
|
|
24
|
+
const file = path.join(pdir, f.name); const st = fs.statSync(file);
|
|
25
|
+
out.push({ id: f.name.replace(/\.jsonl$/, ''), file, project: decodeProject(p.name), projectDir: pdir, size: st.size, mtime: st.mtimeMs });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
out.sort((a, b) => b.mtime - a.mtime);
|
|
29
|
+
let list = out;
|
|
30
|
+
if (opts.project) list = list.filter((s) => s.project.includes(opts.project));
|
|
31
|
+
if (opts.grep) { const q = opts.grep.toLowerCase(); list = list.filter((s) => { try { return fs.readFileSync(s.file, 'utf8').toLowerCase().includes(q); } catch (e) { return false; } }); }
|
|
32
|
+
if (opts.last) list = list.slice(0, opts.last);
|
|
33
|
+
return list;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function sessionTitle(file) {
|
|
37
|
+
let fd; try { fd = fs.openSync(file, 'r'); const buf = Buffer.alloc(262144); const n = fs.readSync(fd, buf, 0, buf.length, 0); const head = buf.toString('utf8', 0, n); let first = null;
|
|
38
|
+
for (const line of head.split('\n')) { if (!line.trim()) continue; let r; try { r = JSON.parse(line); } catch (e) { continue; } if (r.type === 'summary' && r.summary) return r.summary; if (!first && r.type === 'user' && typeof (r.message && r.message.content) === 'string' && !/^\s*</.test(r.message.content)) first = r.message.content.replace(/\s+/g, ' ').slice(0, 90); }
|
|
39
|
+
return first || '(no prompt found)'; } catch (e) { return '(unreadable)'; } finally { if (fd != null) fs.closeSync(fd); }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Resolve a user-supplied target: a path to a .jsonl, or a session-id prefix.
|
|
43
|
+
export function resolveTarget(target, opts = {}) {
|
|
44
|
+
if (!target) { const s = findSessions({ home: opts.home, last: 1 })[0]; if (!s) throw new Error('No sessions found under ' + path.join(opts.home || claudeHome(), 'projects')); return s; }
|
|
45
|
+
if (fs.existsSync(target) && fs.statSync(target).isFile()) { const file = path.resolve(target); return { id: path.basename(file).replace(/\.jsonl$/, ''), file, project: null, projectDir: path.dirname(file), size: fs.statSync(file).size, mtime: fs.statSync(file).mtimeMs }; }
|
|
46
|
+
const all = findSessions({ home: opts.home });
|
|
47
|
+
const hits = all.filter((s) => s.id.startsWith(target));
|
|
48
|
+
if (hits.length === 1) return hits[0];
|
|
49
|
+
if (hits.length > 1) throw new Error(`"${target}" matches ${hits.length} sessions; give more of the id: ${hits.slice(0, 5).map((h) => h.id.slice(0, 12)).join(', ')}`);
|
|
50
|
+
throw new Error(`No session or file matches "${target}"`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Session file + its subagent transcripts, as the {name, text} list the core expects.
|
|
54
|
+
export function loadSessionFiles(s) {
|
|
55
|
+
const files = [{ name: s.id + '.jsonl', text: fs.readFileSync(s.file, 'utf8') }];
|
|
56
|
+
const sub = path.join(s.projectDir, s.id, 'subagents');
|
|
57
|
+
if (fs.existsSync(sub)) for (const f of fs.readdirSync(sub)) if (/\.(jsonl|json)$/.test(f)) files.push({ name: `${s.id}/subagents/${f}`, text: fs.readFileSync(path.join(sub, f), 'utf8') });
|
|
58
|
+
return files;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function analyse(files, rates) { const trace = core.parseTrace(files); const findings = core.diagnose(trace); const cost = core.estimateCost(trace, rates); return { trace, findings, cost }; }
|
|
62
|
+
|
|
63
|
+
const safe = (s) => s.replace(/<\/script/gi, '<\\/script').replace(/<!--/g, '<\\!--');
|
|
64
|
+
export function embed(files, opts = {}) {
|
|
65
|
+
const template = opts.template || path.join(ROOT, 'dist/glassbox.html');
|
|
66
|
+
if (!fs.existsSync(template)) throw new Error('dist/glassbox.html not found — run `npm run build` first');
|
|
67
|
+
const html = fs.readFileSync(template, 'utf8');
|
|
68
|
+
if (!html.includes('/*__EMBED__*/null')) throw new Error('template has no embed marker');
|
|
69
|
+
return html.replace('/*__EMBED__*/null', () => safe(JSON.stringify(files)));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function openInBrowser(file, env = process.env) {
|
|
73
|
+
const cmd = env.GLASSBOX_BROWSER;
|
|
74
|
+
let child;
|
|
75
|
+
if (cmd) child = spawn(cmd, [file], { stdio: 'ignore', detached: true, shell: true });
|
|
76
|
+
else if (process.platform === 'darwin') child = spawn('open', [file], { stdio: 'ignore', detached: true });
|
|
77
|
+
else if (process.platform === 'win32') child = spawn('cmd', ['/c', 'start', '', file], { stdio: 'ignore', detached: true });
|
|
78
|
+
else child = spawn('xdg-open', [file], { stdio: 'ignore', detached: true });
|
|
79
|
+
child.on('error', () => { }); child.unref();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const SEV = { error: 0, warn: 1, info: 2 };
|
|
83
|
+
export function checkReport({ trace, findings, cost }, opts = {}) {
|
|
84
|
+
const failOn = opts.failOn || 'error';
|
|
85
|
+
const failing = findings.filter((f) => SEV[f.severity] <= SEV[failOn]);
|
|
86
|
+
const T = trace.totals, m = trace.meta;
|
|
87
|
+
const fmt = core.fmtDur, fi = core.fmtInt;
|
|
88
|
+
const usd = (v) => v == null ? '—' : '$' + (v < 1 ? v.toFixed(3) : v.toFixed(2));
|
|
89
|
+
const summary = { session: m.sessionId, title: m.title || null, model: m.models, wallMs: T.wallMs, activeMs: T.activeMs, turns: T.turns, requests: T.requests, toolCalls: T.toolCalls, toolErrors: T.toolErrors, orphans: T.orphans, usage: T.usage, cacheHitRatio: T.cacheHitRatio, cost: cost.reported != null ? cost.reported : cost.total, costSource: cost.source };
|
|
90
|
+
const json = { summary, findings: findings.map((f) => ({ id: f.id, severity: f.severity, title: f.title, detail: f.detail, metric: f.metric })), failOn, failed: failing.length > 0 };
|
|
91
|
+
const lines = [];
|
|
92
|
+
lines.push(`Glassbox · ${m.sessionId ? m.sessionId.slice(0, 8) : 'session'} · ${m.models.join(', ') || 'unknown model'}`);
|
|
93
|
+
lines.push(` wall ${fmt(T.wallMs)} (active ${fmt(T.activeMs)}) · ${T.turns} turns · ${T.requests} requests · ${T.toolCalls} tool calls (${T.toolErrors} failed${T.orphans ? ', ' + T.orphans + ' unanswered' : ''})`);
|
|
94
|
+
lines.push(` context served ${fi(T.usage.input + T.usage.cacheRead + T.usage.cacheWrite)} tokens (${T.cacheHitRatio != null ? Math.round(T.cacheHitRatio * 100) + '% cached' : 'no usage'}) · output ${fi(T.usage.output)} (${fi(T.usage.thinking)} thinking) · est. cost ${usd(summary.cost)}`);
|
|
95
|
+
lines.push('');
|
|
96
|
+
if (!findings.length) lines.push(' no findings');
|
|
97
|
+
for (const f of findings) lines.push(` ${f.severity.toUpperCase().padEnd(5)} ${f.id.padEnd(16)} ${f.title}`);
|
|
98
|
+
lines.push('');
|
|
99
|
+
lines.push(failing.length ? ` FAIL: ${failing.length} finding${failing.length > 1 ? 's' : ''} at or above "${failOn}"` : ` OK: nothing at or above "${failOn}"`);
|
|
100
|
+
const md = ['# Glassbox report', '', `- session: ${m.sessionId || '—'} · model: ${m.models.join(', ') || '—'}`, `- wall ${fmt(T.wallMs)} (active ${fmt(T.activeMs)}) · ${T.turns} turns · ${T.requests} requests · ${T.toolCalls} tool calls (${T.toolErrors} failed)`, `- context served ${fi(T.usage.input + T.usage.cacheRead + T.usage.cacheWrite)} · output ${fi(T.usage.output)} · est. cost ${usd(summary.cost)}`, '', `## Findings (${findings.length})`, '', ...(findings.length ? findings.map((f) => `- **${f.severity.toUpperCase()}** \`${f.id}\` — ${f.title}\n ${f.detail}`) : ['Nothing flagged.']), ''].join('\n');
|
|
101
|
+
return { text: lines.join('\n'), json, markdown: md, failed: failing.length > 0 };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Claude Code hook: the agent reads its own flight recorder.
|
|
107
|
+
// Claude Code sends JSON on stdin for Stop / SessionEnd:
|
|
108
|
+
// { session_id, transcript_path, cwd, hook_event_name, stop_hook_active }
|
|
109
|
+
// We reply with JSON: a systemMessage (shown to the human) and, in feedback mode,
|
|
110
|
+
// decision:"block" + reason so the findings go back into the model's context once.
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
export function sessionFromTranscriptPath(p) {
|
|
113
|
+
const file = path.resolve(p);
|
|
114
|
+
return { id: path.basename(file).replace(/\.jsonl$/, ''), file, project: null, projectDir: path.dirname(file), size: fs.existsSync(file) ? fs.statSync(file).size : 0, mtime: fs.existsSync(file) ? fs.statSync(file).mtimeMs : 0 };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function hookResponse(input, opts = {}) {
|
|
118
|
+
const failOn = opts.failOn || 'warn';
|
|
119
|
+
const feedback = !!opts.feedback;
|
|
120
|
+
if (!input || !input.transcript_path || !fs.existsSync(input.transcript_path)) return { systemMessage: 'Glassbox: no transcript_path in hook input.', suppressOutput: true };
|
|
121
|
+
const s = sessionFromTranscriptPath(input.transcript_path);
|
|
122
|
+
const res = analyse(loadSessionFiles(s), opts.rates);
|
|
123
|
+
const rep = checkReport(res, { failOn });
|
|
124
|
+
const T = res.trace.totals;
|
|
125
|
+
const usd = (v) => v == null ? '' : ' · ' + (v < 1 ? '$' + v.toFixed(3) : '$' + v.toFixed(2));
|
|
126
|
+
const top = res.findings.filter((f) => SEV[f.severity] <= SEV[failOn]);
|
|
127
|
+
const head = `Glassbox · ${T.turns} turns · ${T.toolCalls} tool calls (${T.toolErrors} failed) · ctx peak ${core.fmtInt(Math.max(0, ...res.trace.requests.map((r) => r.contextTokens)))} tokens${usd(rep.json.summary.cost)}`;
|
|
128
|
+
const list = top.slice(0, 6).map((f) => `${f.severity.toUpperCase()} ${f.id}: ${f.title}`);
|
|
129
|
+
const out = { systemMessage: head + (list.length ? '\n' + list.join('\n') + (top.length > 6 ? `\n… ${top.length - 6} more (glassbox check)` : '') : '\nno findings at or above ' + failOn), suppressOutput: true };
|
|
130
|
+
// Feedback loop: only on Stop, only once (stop_hook_active guards against loops), only when something is worth saying.
|
|
131
|
+
if (feedback && input.hook_event_name === 'Stop' && !input.stop_hook_active && top.length) {
|
|
132
|
+
out.decision = 'block';
|
|
133
|
+
out.reason = `Glassbox read this session's flight recorder. ${head}\n\nFindings at or above "${failOn}":\n` + top.map((f) => `- ${f.severity.toUpperCase()} \`${f.id}\` — ${f.title}. ${f.detail}`).join('\n') + `\n\nIn one or two sentences, tell the user what you would do differently next session (no need to redo work), then stop.`;
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function readStdinJson() {
|
|
139
|
+
return new Promise((resolve) => { let buf = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', (d) => { buf += d; }); process.stdin.on('end', () => { try { resolve(JSON.parse(buf || '{}')); } catch (e) { resolve({}); } }); if (process.stdin.isTTY) resolve({}); });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const HOOK_RE = /\bglassbox(?:-trace)?\s+hook\b/;
|
|
143
|
+
export function settingsPath(home) { return path.join(home || claudeHome(), 'settings.json'); }
|
|
144
|
+
export function installHook(opts = {}) {
|
|
145
|
+
const file = settingsPath(opts.home);
|
|
146
|
+
let settings = {};
|
|
147
|
+
if (fs.existsSync(file)) { try { settings = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) { throw new Error(`${file} is not valid JSON — fix it first (nothing was changed)`); } fs.copyFileSync(file, file + '.glassbox-backup'); }
|
|
148
|
+
settings.hooks = settings.hooks || {};
|
|
149
|
+
const cmd = `${opts.command || 'npx -y glassbox-trace'} hook${opts.feedback ? ' --feedback' : ''}${opts.failOn ? ' --fail-on ' + opts.failOn : ''}`;
|
|
150
|
+
for (const ev of opts.events || ['Stop']) {
|
|
151
|
+
const list = Array.isArray(settings.hooks[ev]) ? settings.hooks[ev] : [];
|
|
152
|
+
const kept = list.filter((entry) => !(entry && Array.isArray(entry.hooks) && entry.hooks.some((h) => h && typeof h.command === 'string' && HOOK_RE.test(h.command))));
|
|
153
|
+
kept.push({ hooks: [{ type: 'command', command: cmd, timeout: 60 }] });
|
|
154
|
+
settings.hooks[ev] = kept;
|
|
155
|
+
}
|
|
156
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
157
|
+
fs.writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
|
|
158
|
+
return { file, command: cmd };
|
|
159
|
+
}
|
|
160
|
+
export function uninstallHook(opts = {}) {
|
|
161
|
+
const file = settingsPath(opts.home);
|
|
162
|
+
if (!fs.existsSync(file)) return { file, removed: 0 };
|
|
163
|
+
const settings = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
164
|
+
let removed = 0;
|
|
165
|
+
for (const ev of Object.keys(settings.hooks || {})) {
|
|
166
|
+
const list = settings.hooks[ev]; if (!Array.isArray(list)) continue;
|
|
167
|
+
const kept = list.filter((entry) => { const ours = entry && Array.isArray(entry.hooks) && entry.hooks.some((h) => h && typeof h.command === 'string' && HOOK_RE.test(h.command)); if (ours) removed++; return !ours; });
|
|
168
|
+
if (kept.length) settings.hooks[ev] = kept; else delete settings.hooks[ev];
|
|
169
|
+
}
|
|
170
|
+
fs.writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
|
|
171
|
+
return { file, removed };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function parseArgs(argv) {
|
|
175
|
+
const args = { _: [], flags: {} };
|
|
176
|
+
for (let i = 0; i < argv.length; i++) {
|
|
177
|
+
const a = argv[i];
|
|
178
|
+
if (a.startsWith('--')) { const [k, v] = a.slice(2).split('='); if (v !== undefined) args.flags[k] = v; else if (i + 1 < argv.length && !argv[i + 1].startsWith('-') && ['last', 'grep', 'project', 'out', 'fail-on', 'home', 'command', 'events'].includes(k)) args.flags[k] = argv[++i]; else args.flags[k] = true; }
|
|
179
|
+
else args._.push(a);
|
|
180
|
+
}
|
|
181
|
+
return args;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const HELP = `glassbox — the flight recorder viewer for Claude Code sessions
|
|
185
|
+
|
|
186
|
+
glassbox open the newest session in your browser
|
|
187
|
+
glassbox list [--last N] [--grep TEXT] [--project PATH]
|
|
188
|
+
list sessions, newest first
|
|
189
|
+
glassbox open [ID|FILE] [--out FILE.html] [--no-open]
|
|
190
|
+
build a self-contained HTML for a session (id prefix or .jsonl path)
|
|
191
|
+
glassbox check [ID|FILE] [--fail-on error|warn|info] [--json] [--markdown]
|
|
192
|
+
print findings; exit 1 when any finding is at/above --fail-on (default: error)
|
|
193
|
+
glassbox hook install [--feedback] [--fail-on warn]
|
|
194
|
+
add a Claude Code Stop hook so every session ends with a Glassbox summary;
|
|
195
|
+
--feedback also hands the findings back to the agent once, so it can learn from them
|
|
196
|
+
glassbox hook uninstall remove it (a .glassbox-backup of settings.json is kept)
|
|
197
|
+
glassbox hook (what Claude Code runs: reads the hook JSON on stdin, replies on stdout)
|
|
198
|
+
|
|
199
|
+
Options --home DIR use DIR instead of ~/.claude (or set GLASSBOX_HOME)
|
|
200
|
+
--version --help
|
|
201
|
+
Browser set GLASSBOX_BROWSER to a command to open HTML files with
|
|
202
|
+
|
|
203
|
+
Nothing leaves your machine. Subagent transcripts next to the session are included automatically.`;
|
|
204
|
+
|
|
205
|
+
export async function main(argv, io = {}) {
|
|
206
|
+
const out = io.stdout || ((s) => process.stdout.write(s + '\n'));
|
|
207
|
+
const err = io.stderr || ((s) => process.stderr.write(s + '\n'));
|
|
208
|
+
const args = parseArgs(argv);
|
|
209
|
+
const home = args.flags.home || io.home;
|
|
210
|
+
const cmd = args._[0] || 'open';
|
|
211
|
+
if (args.flags.help || cmd === 'help') { out(HELP); return 0; }
|
|
212
|
+
if (args.flags.version) { out(JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version); return 0; }
|
|
213
|
+
try {
|
|
214
|
+
if (cmd === 'list') {
|
|
215
|
+
const list = findSessions({ home, last: args.flags.last ? +args.flags.last : 20, grep: args.flags.grep, project: args.flags.project });
|
|
216
|
+
if (!list.length) { out('No sessions found under ' + path.join(home || claudeHome(), 'projects')); return 0; }
|
|
217
|
+
for (const s of list) out(`${s.id.slice(0, 8)} ${new Date(s.mtime).toISOString().slice(0, 16).replace('T', ' ')} ${String(Math.round(s.size / 1024)).padStart(6)} KB ${s.project.padEnd(28).slice(0, 28)} ${sessionTitle(s.file)}`);
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
if (cmd === 'check') {
|
|
221
|
+
const s = resolveTarget(args._[1], { home });
|
|
222
|
+
const res = checkReport(analyse(loadSessionFiles(s)), { failOn: args.flags['fail-on'] });
|
|
223
|
+
if (args.flags.json) out(JSON.stringify(res.json, null, 2)); else if (args.flags.markdown) out(res.markdown); else out(res.text);
|
|
224
|
+
return res.failed ? 1 : 0;
|
|
225
|
+
}
|
|
226
|
+
if (cmd === 'open') {
|
|
227
|
+
const s = resolveTarget(args._[1], { home });
|
|
228
|
+
const files = loadSessionFiles(s);
|
|
229
|
+
const html = embed(files, { template: io.template });
|
|
230
|
+
const outFile = args.flags.out ? path.resolve(args.flags.out) : path.join(os.tmpdir(), `glassbox-${s.id.slice(0, 8)}.html`);
|
|
231
|
+
fs.writeFileSync(outFile, html);
|
|
232
|
+
out(`${outFile} (${files.length} file${files.length > 1 ? 's' : ''}, ${Math.round(html.length / 1024)} KB)`);
|
|
233
|
+
if (!args.flags['no-open'] && !io.noOpen) openInBrowser(outFile);
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
236
|
+
if (cmd === 'hook') {
|
|
237
|
+
const sub = args._[1];
|
|
238
|
+
if (sub === 'install') { const r = installHook({ home, feedback: !!args.flags.feedback, failOn: args.flags['fail-on'], command: args.flags.command, events: args.flags.events ? String(args.flags.events).split(',') : undefined }); out(`Installed Stop hook in ${r.file}\n ${r.command}\nEvery session now ends with a Glassbox summary${args.flags.feedback ? ', and findings are handed back to the agent once' : ''}. Restart Claude Code to pick it up.`); return 0; }
|
|
239
|
+
if (sub === 'uninstall') { const r = uninstallHook({ home }); out(`Removed ${r.removed} Glassbox hook${r.removed === 1 ? '' : 's'} from ${r.file}`); return 0; }
|
|
240
|
+
const input = io.stdin !== undefined ? io.stdin : await readStdinJson();
|
|
241
|
+
out(JSON.stringify(hookResponse(input, { failOn: args.flags['fail-on'], feedback: !!args.flags.feedback })));
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
err('Unknown command: ' + cmd + '\n'); out(HELP); return 2;
|
|
245
|
+
} catch (e) { err('glassbox: ' + e.message); return 2; }
|
|
246
|
+
}
|