tokenoscopy 0.3.2 → 0.4.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 CHANGED
@@ -4,12 +4,14 @@
4
4
 
5
5
  ```bash
6
6
  npm install -g tokenoscopy
7
- tokenoscopy init --key tok_…
7
+ tokenoscopy init
8
8
  ```
9
9
 
10
- Sign in at [tokenoscopy.com](https://tokenoscopy.com) and the **Setup** screen
11
- hands you that command with your key already written into it, then waits for
12
- your first session and shows it to you as it lands.
10
+ No key to paste: `init` opens your browser to pair the machine with your
11
+ workspace, you confirm the code it shows, and it finishes in the terminal.
12
+ The **Setup** screen at [tokenoscopy.com](https://tokenoscopy.com) then waits
13
+ for your first session and shows it to you as it lands. Headless or CI? Pass
14
+ a key from Settings → Ingest keys with `--key tok_…` (or `TOKENOSCOPY_KEY`).
13
15
 
14
16
  Install globally rather than relying on `npx`: the budget guard runs before
15
17
  every Bash and MCP tool call, and `npx` re-resolves the package each time.
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { detectAuth, GUARD_TIMEOUT_MS, parseTranscript, detectAuthMode, withRelPath, redactValue, ALLOW_CACHE_MS, truncatePayload, naturalDedupKey } from './chunk-AZYPMFOF.js';
2
+ import { detectAuth, GUARD_TIMEOUT_MS, parseTranscript, detectAuthMode, withRelPath, redactValue, ALLOW_CACHE_MS, truncatePayload, naturalDedupKey } from './chunk-KU3AJVVR.js';
3
3
  import { existsSync, readFileSync, readdirSync, statSync, watch, copyFileSync, writeFileSync, mkdirSync } from 'fs';
4
4
  import { homedir, userInfo, hostname, tmpdir } from 'os';
5
5
  import { join } from 'path';
6
- import { execFileSync } from 'child_process';
6
+ import { execFileSync, spawn } from 'child_process';
7
7
  import { createInterface } from 'readline/promises';
8
8
 
9
9
  var DEFAULT_API_URL = "https://tokenoscopy.com";
@@ -421,6 +421,121 @@ async function runGuard() {
421
421
  return 0;
422
422
  }
423
423
  }
424
+ function interpretPoll(status, body) {
425
+ const b = body && typeof body === "object" ? body : {};
426
+ if (status === 202) {
427
+ const interval = typeof b.interval === "number" && b.interval > 0 ? b.interval : 3;
428
+ return { kind: "pending", interval };
429
+ }
430
+ if (status === 200 && typeof b.key === "string" && b.key.startsWith("tok_")) {
431
+ return {
432
+ kind: "paired",
433
+ key: b.key,
434
+ hostname: typeof b.hostname === "string" ? b.hostname : "this machine"
435
+ };
436
+ }
437
+ if (status === 410 || status === 404) {
438
+ return { kind: "gone", reason: typeof b.error === "string" ? b.error : "gone" };
439
+ }
440
+ return { kind: "retry" };
441
+ }
442
+ function goneMessage(reason) {
443
+ switch (reason) {
444
+ case "expired":
445
+ return "The pairing code expired before it was confirmed.";
446
+ case "consumed":
447
+ return "That pairing was already collected by another init.";
448
+ default:
449
+ return "That pairing is no longer valid.";
450
+ }
451
+ }
452
+ function openBrowser(url) {
453
+ const cmd = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
454
+ try {
455
+ const child = spawn(cmd[0], cmd[1], {
456
+ stdio: "ignore",
457
+ detached: true
458
+ });
459
+ child.on("error", () => void 0);
460
+ child.unref();
461
+ } catch {
462
+ }
463
+ }
464
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
465
+ async function startPairing(apiUrl) {
466
+ try {
467
+ const res = await fetch(`${apiUrl}/api/pair/start`, {
468
+ method: "POST",
469
+ headers: { "Content-Type": "application/json" },
470
+ body: JSON.stringify({ hostname: hostname() }),
471
+ signal: AbortSignal.timeout(8e3)
472
+ });
473
+ if (!res.ok) return null;
474
+ const body = await res.json();
475
+ if (typeof body.device_code !== "string" || typeof body.user_code !== "string" || typeof body.verification_url !== "string") {
476
+ return null;
477
+ }
478
+ return {
479
+ device_code: body.device_code,
480
+ user_code: body.user_code,
481
+ verification_url: body.verification_url,
482
+ expires_in: typeof body.expires_in === "number" ? body.expires_in : 600,
483
+ interval: typeof body.interval === "number" ? body.interval : 3
484
+ };
485
+ } catch {
486
+ return null;
487
+ }
488
+ }
489
+ async function pairMachine(apiUrl, opts = { browser: true }) {
490
+ const out = opts.out ?? ((line) => process.stderr.write(`${line}
491
+ `));
492
+ const start = await startPairing(apiUrl);
493
+ if (!start) {
494
+ out(`\u2717 Couldn't start pairing with ${apiUrl}.`);
495
+ return null;
496
+ }
497
+ out(` Open ${start.verification_url}`);
498
+ out(` Code ${start.user_code} (this machine: ${hostname()})`);
499
+ if (opts.browser) openBrowser(start.verification_url);
500
+ out(" waiting for you to confirm in the browser\u2026");
501
+ const deadline = Date.now() + start.expires_in * 1e3;
502
+ let interval = start.interval;
503
+ while (Date.now() < deadline) {
504
+ await sleep(interval * 1e3);
505
+ let outcome;
506
+ try {
507
+ const res = await fetch(`${apiUrl}/api/pair/poll`, {
508
+ method: "POST",
509
+ headers: { "Content-Type": "application/json" },
510
+ body: JSON.stringify({ device_code: start.device_code }),
511
+ signal: AbortSignal.timeout(8e3)
512
+ });
513
+ let body = null;
514
+ try {
515
+ body = await res.json();
516
+ } catch {
517
+ body = null;
518
+ }
519
+ outcome = interpretPoll(res.status, body);
520
+ } catch {
521
+ outcome = { kind: "retry" };
522
+ }
523
+ if (outcome.kind === "paired") {
524
+ out(`\u2713 paired as ${outcome.hostname}`);
525
+ return outcome.key;
526
+ }
527
+ if (outcome.kind === "gone") {
528
+ out(`\u2717 ${goneMessage(outcome.reason)}`);
529
+ return null;
530
+ }
531
+ if (outcome.kind === "pending") interval = outcome.interval;
532
+ else interval = Math.min(interval * 2, 15);
533
+ }
534
+ out("\u2717 Timed out waiting for the browser.");
535
+ return null;
536
+ }
537
+
538
+ // src/commands/init.ts
424
539
  var MARKER = "tokenoscopy";
425
540
  function isOurs(group) {
426
541
  return JSON.stringify(group).toLowerCase().includes(MARKER);
@@ -489,11 +604,20 @@ async function runInit(args) {
489
604
  const flags = /* @__PURE__ */ new Map();
490
605
  for (let i = 0; i < args.length; i++) {
491
606
  const a = args[i];
492
- if (a === "--project" || a === "--print") flags.set(a, true);
607
+ if (a === "--project" || a === "--print" || a === "--no-browser") flags.set(a, true);
493
608
  else if (a.startsWith("--")) flags.set(a, args[++i] ?? "");
494
609
  }
495
610
  const apiUrl = flags.get("--url") || process.env.TOKENOSCOPY_API_URL || DEFAULT_API_URL;
496
611
  let key = flags.get("--key") || process.env.TOKENOSCOPY_KEY || "";
612
+ if (!key && process.stdin.isTTY) {
613
+ const paired = await pairMachine(apiUrl, { browser: !flags.get("--no-browser") });
614
+ if (paired) key = paired;
615
+ else {
616
+ process.stderr.write(
617
+ " Paste a key from Settings \u2192 Ingest keys instead, or re-run with --key tok_\u2026\n"
618
+ );
619
+ }
620
+ }
497
621
  if (!key) {
498
622
  const rl = createInterface({ input: process.stdin, output: process.stderr });
499
623
  key = (await rl.question("Paste your ingest key (tok_\u2026): ")).trim();
@@ -627,7 +751,9 @@ Usage: tokenoscopy <command> [options]
627
751
 
628
752
  Commands:
629
753
  init Wire Claude Code hooks + transcript flush to your workspace
630
- --key tok_\u2026 ingest key (or TOKENOSCOPY_KEY)
754
+ (no key: opens the browser to pair this machine)
755
+ --key tok_\u2026 ingest key (or TOKENOSCOPY_KEY) \u2014 headless / CI
756
+ --no-browser print the pairing link instead of opening it
631
757
  --url https://\u2026 API base (default https://tokenoscopy.com)
632
758
  --project write hooks to ./.claude/settings.json (CI)
633
759
  --print print the hook config instead of writing
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { classifyAuth, costIsRealMoney, detectAuth, detectAuthMode, parseTranscript, readClaudeJson, redactText, redactValue, repoLabelFromCwd } from './chunk-AZYPMFOF.js';
1
+ export { classifyAuth, costIsRealMoney, detectAuth, detectAuthMode, parseTranscript, readClaudeJson, redactText, redactValue, repoLabelFromCwd } from './chunk-KU3AJVVR.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenoscopy",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Flight recorder & spend control for coding-agent fleets. Records Claude Code sessions (hooks + transcripts), replays them, and enforces budgets.",
5
5
  "license": "MIT",
6
6
  "type": "module",
File without changes