glassbox-trace 0.7.0 → 0.7.1

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.
@@ -408,7 +408,7 @@ kbd{font:11px var(--mono);border:1px solid var(--line);border-bottom-width:2px;b
408
408
  })(typeof globalThis !== 'undefined' ? globalThis : this, function () {
409
409
  'use strict';
410
410
 
411
- const VERSION = '0.7.0';
411
+ const VERSION = '0.7.1';
412
412
 
413
413
  // ---------------------------------------------------------------------------
414
414
  // Rate card (USD per million tokens). Prefix-matched against model ids so dated
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glassbox-trace",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Other tools show what happened in a Claude Code session; Glassbox tells you what went wrong. Timeline, token and cost burn, auto-diagnostics with evidence and advice, session compare, live tail, a CLI and a Claude Code Stop hook that feeds the findings back to the agent.",
5
5
  "main": "src/trace-core.js",
6
6
  "scripts": {
package/src/cli.mjs CHANGED
@@ -6,6 +6,7 @@ import { spawn } from 'node:child_process';
6
6
  import crypto from 'node:crypto';
7
7
  import { createRequire } from 'node:module';
8
8
  import { fileURLToPath } from 'node:url';
9
+ import { readTextFile } from './textfile.mjs';
9
10
  const require = createRequire(import.meta.url);
10
11
  const core = require('./trace-core.js');
11
12
  // fileURLToPath, not URL.pathname: on Windows the latter gives "/C:/…" which path.resolve turns into "\C:\…".
@@ -89,7 +90,7 @@ export function analyse(files, rates) { const trace = core.parseTrace(files); co
89
90
  // Entries override or extend the built-in card; cache-write rates default to 1.25× / 2× input.
90
91
  export function loadRates(file) {
91
92
  if (!file) return null;
92
- let json; try { json = JSON.parse(fs.readFileSync(String(file), 'utf8')); } catch (e) { throw new Error(`--rates ${file}: ${e.code === 'ENOENT' ? 'no such file' : 'not valid JSON'}`); }
93
+ let json; try { json = JSON.parse(readTextFile(String(file))); } catch (e) { throw new Error(`--rates ${file}: ${e.code === 'ENOENT' ? 'no such file' : 'not valid JSON'}`); }
93
94
  if (!json || typeof json !== 'object' || Array.isArray(json)) throw new Error(`--rates ${file}: expected an object keyed by model id`);
94
95
  const out = Object.assign({}, core.RATES);
95
96
  for (const [model, r] of Object.entries(json)) {
@@ -136,7 +137,7 @@ export class Legend {
136
137
  constructor(data) { this.salt = data && data.salt || crypto.randomBytes(32).toString('hex'); this.files = data && data.files || {}; this.created = data && data.created || new Date().toISOString(); this.byPath = new Map(Object.entries(this.files).map(([k, v]) => [core.normalisePath(v), k])); this.dirty = !data; }
137
138
  static load(file) {
138
139
  if (!file || !fs.existsSync(file)) return new Legend(null);
139
- let json; try { json = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) { throw new Error(`--legend ${file}: not valid JSON (delete it to start a new legend)`); }
140
+ let json; try { json = JSON.parse(readTextFile(file)); } catch (e) { throw new Error(`--legend ${file}: not valid JSON (delete it to start a new legend)`); }
140
141
  if (!json || typeof json.salt !== 'string' || typeof json.files !== 'object') throw new Error(`--legend ${file}: not a Glassbox legend`);
141
142
  return new Legend(json);
142
143
  }
@@ -293,7 +294,7 @@ export function settingsPath(home) { return path.join(home || claudeHome(), 'set
293
294
  export function installHook(opts = {}) {
294
295
  const file = settingsPath(opts.home);
295
296
  let settings = {};
296
- 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'); }
297
+ if (fs.existsSync(file)) { try { settings = JSON.parse(readTextFile(file)); } catch (e) { throw new Error(`${file} is not valid JSON — fix it first (nothing was changed)`); } fs.copyFileSync(file, file + '.glassbox-backup'); }
297
298
  settings.hooks = settings.hooks || {};
298
299
  const cmd = `${opts.command || 'npx -y glassbox-trace'} hook${opts.feedback ? ' --feedback' : ''}${opts.context ? ' --context' : ''}${opts.failOn ? ' --fail-on ' + opts.failOn : ''}`;
299
300
  for (const ev of opts.events || (opts.context ? ['Stop', 'SessionStart'] : ['Stop'])) {
@@ -309,7 +310,7 @@ export function installHook(opts = {}) {
309
310
  export function uninstallHook(opts = {}) {
310
311
  const file = settingsPath(opts.home);
311
312
  if (!fs.existsSync(file)) return { file, removed: 0 };
312
- const settings = JSON.parse(fs.readFileSync(file, 'utf8'));
313
+ const settings = JSON.parse(readTextFile(file));
313
314
  let removed = 0;
314
315
  for (const ev of Object.keys(settings.hooks || {})) {
315
316
  const list = settings.hooks[ev]; if (!Array.isArray(list)) continue;
@@ -417,7 +418,7 @@ export async function main(argv, io = {}) {
417
418
  if (cmd === 'reveal') {
418
419
  if (!args._[1]) throw new Error('reveal needs a file: glassbox reveal report.md --legend audit.legend.json');
419
420
  if (!legendFile || !fs.existsSync(legendFile)) throw new Error('reveal needs --legend FILE (the legend written by check --redact --legend)');
420
- const r = legend.reveal(fs.readFileSync(args._[1], 'utf8'));
421
+ const r = legend.reveal(readTextFile(args._[1]));
421
422
  out(r.text.replace(/\n$/, ''));
422
423
  if (r.unknown) err(`reveal: ${r.unknown} key${r.unknown === 1 ? '' : 's'} not in this legend (left as they are)`);
423
424
  return 0;
package/src/collect.mjs CHANGED
@@ -9,6 +9,7 @@
9
9
  import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { createRequire } from 'node:module';
12
+ import { readTextFile } from './textfile.mjs';
12
13
  const require = createRequire(import.meta.url);
13
14
  const core = require('./trace-core.js');
14
15
 
@@ -28,7 +29,7 @@ export function readSources(dir, opts = {}) {
28
29
  const entries = fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && /\.json$/i.test(e.name)).sort((a, b) => a.name.localeCompare(b.name));
29
30
  for (const e of entries) {
30
31
  const file = path.join(dir, e.name);
31
- let doc; try { doc = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (err) { skipped.push({ file: e.name, reason: 'not JSON' }); continue; }
32
+ let doc; try { doc = JSON.parse(readTextFile(file)); } catch (err) { skipped.push({ file: e.name, reason: 'not JSON' }); continue; }
32
33
  const src = sourceFromJson(e.name.replace(/\.json$/i, ''), doc);
33
34
  if (!src) { skipped.push({ file: e.name, reason: 'not a glassbox check report' }); continue; }
34
35
  if (!src.redacted && !opts.allowUnredacted) { skipped.push({ file: e.name, reason: 'not redacted (run check with --redact, or pass --allow-unredacted)' }); continue; }
@@ -0,0 +1,17 @@
1
+ // Text files a person may have saved on Windows. PowerShell 5.1's `>` writes UTF-16LE with a byte-order
2
+ // mark; `Set-Content -Encoding UTF8` and older Notepad write a UTF-8 BOM. Read all of them as the same text,
3
+ // so a report, rate card or legend works however it was saved. Transcripts go through parseLines, which
4
+ // already drops a UTF-8 BOM; everything a user hands to the CLI goes through here.
5
+ import fs from 'node:fs';
6
+
7
+ export function decodeText(input) {
8
+ const buf = Buffer.isBuffer(input) ? input : Buffer.from(input);
9
+ if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) return buf.subarray(2).toString('utf16le');
10
+ if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) { const le = Buffer.from(buf.subarray(2)); le.swap16(); return le.toString('utf16le'); }
11
+ if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) return buf.subarray(3).toString('utf8');
12
+ return buf.toString('utf8');
13
+ }
14
+
15
+ export function readTextFile(file) {
16
+ return decodeText(fs.readFileSync(file));
17
+ }
package/src/trace-core.js CHANGED
@@ -9,7 +9,7 @@
9
9
  })(typeof globalThis !== 'undefined' ? globalThis : this, function () {
10
10
  'use strict';
11
11
 
12
- const VERSION = '0.7.0';
12
+ const VERSION = '0.7.1';
13
13
 
14
14
  // ---------------------------------------------------------------------------
15
15
  // Rate card (USD per million tokens). Prefix-matched against model ids so dated