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 +6 -4
- package/dist/cli.js +130 -4
- package/dist/index.js +1 -1
- package/package.json +1 -1
- /package/dist/{chunk-AZYPMFOF.js → chunk-KU3AJVVR.js} +0 -0
package/README.md
CHANGED
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -g tokenoscopy
|
|
7
|
-
tokenoscopy init
|
|
7
|
+
tokenoscopy init
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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
|
+
"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
|