tokenoscopy 0.3.1 → 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/{chunk-AZYPMFOF.js → chunk-KU3AJVVR.js} +10 -45
- package/dist/cli.js +156 -68
- package/dist/index.js +1 -22
- package/package.json +1 -1
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.
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
1
6
|
// src/auth-mode.ts
|
|
2
|
-
import { readFileSync } from "fs";
|
|
3
|
-
import { homedir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
5
7
|
function keyWasRejected(key, cfg) {
|
|
6
8
|
const rejected = cfg?.customApiKeyResponses?.rejected ?? [];
|
|
7
9
|
return rejected.includes(key.slice(-20));
|
|
@@ -157,9 +159,6 @@ function redactValue(value) {
|
|
|
157
159
|
}
|
|
158
160
|
return value;
|
|
159
161
|
}
|
|
160
|
-
|
|
161
|
-
// ../shared/src/events.ts
|
|
162
|
-
import { z } from "zod";
|
|
163
162
|
var agentProductSchema = z.enum([
|
|
164
163
|
"claude_code",
|
|
165
164
|
"codex",
|
|
@@ -266,12 +265,12 @@ var sessionSummarySchema = z.object({
|
|
|
266
265
|
/** Count of transcript records the parser did not recognize (drift signal). */
|
|
267
266
|
unknown_record_count: z.number().int().nonnegative().default(0)
|
|
268
267
|
});
|
|
269
|
-
|
|
268
|
+
z.object({
|
|
270
269
|
schema_version: z.literal(1),
|
|
271
270
|
summary: sessionSummarySchema,
|
|
272
271
|
events: z.array(sessionEventSchema)
|
|
273
272
|
});
|
|
274
|
-
|
|
273
|
+
z.object({
|
|
275
274
|
hook_event_name: z.string(),
|
|
276
275
|
session_id: z.string().min(8),
|
|
277
276
|
transcript_path: z.string().optional(),
|
|
@@ -288,14 +287,14 @@ var hookEventSchema = z.object({
|
|
|
288
287
|
agent_id: z.string().optional(),
|
|
289
288
|
agent_type: z.string().optional()
|
|
290
289
|
}).passthrough();
|
|
291
|
-
|
|
290
|
+
z.object({
|
|
292
291
|
session_id: z.string().min(8),
|
|
293
292
|
tool_name: z.string(),
|
|
294
293
|
cwd: z.string().optional(),
|
|
295
294
|
git_repo: z.string().optional()
|
|
296
295
|
});
|
|
297
296
|
var guardDecisionSchema = z.enum(["allow", "ask", "deny"]);
|
|
298
|
-
|
|
297
|
+
z.object({
|
|
299
298
|
decision: guardDecisionSchema,
|
|
300
299
|
reason: z.string().optional(),
|
|
301
300
|
budget: z.object({
|
|
@@ -357,22 +356,6 @@ function estimateCostUsd(model, usage) {
|
|
|
357
356
|
return Math.round(cost * 1e6) / 1e6;
|
|
358
357
|
}
|
|
359
358
|
|
|
360
|
-
// ../shared/src/diagnose.ts
|
|
361
|
-
var DIAG = {
|
|
362
|
-
/** A session counts as live if it produced an event this recently. */
|
|
363
|
-
LIVE_WINDOW_MS: 15 * 6e4,
|
|
364
|
-
RUNAWAY_MIN_COST: 10,
|
|
365
|
-
RUNAWAY_CTX_TOKENS: 4e5,
|
|
366
|
-
RUNAWAY_CYCLE: 40,
|
|
367
|
-
RUNAWAY_REPEAT: 10,
|
|
368
|
-
BLOAT_CTX_TOKENS: 2e5,
|
|
369
|
-
BLOAT_MIN_COST: 5,
|
|
370
|
-
CYCLE_MIN: 25,
|
|
371
|
-
REPEAT_MIN: 5,
|
|
372
|
-
/** Requests that rebuilt the cache, summed, before it's worth a habit change. */
|
|
373
|
-
REBUILD_MIN_COST: 3
|
|
374
|
-
};
|
|
375
|
-
|
|
376
359
|
// ../shared/src/guard-contract.ts
|
|
377
360
|
var GUARD_TIMEOUT_MS = 1500;
|
|
378
361
|
var ALLOW_CACHE_MS = 15e3;
|
|
@@ -466,9 +449,6 @@ function withRelPath(event, root) {
|
|
|
466
449
|
return { ...event, payload: { ...event.payload, rel_path: rel } };
|
|
467
450
|
}
|
|
468
451
|
|
|
469
|
-
// ../shared/src/anatomy.ts
|
|
470
|
-
var CACHE_TTL_MS = 5 * 6e4;
|
|
471
|
-
|
|
472
452
|
// src/transcript.ts
|
|
473
453
|
var SKIPPED_TYPES = /* @__PURE__ */ new Set([
|
|
474
454
|
"summary",
|
|
@@ -739,19 +719,4 @@ function parseTranscript(jsonl) {
|
|
|
739
719
|
return { summary, events, warnings };
|
|
740
720
|
}
|
|
741
721
|
|
|
742
|
-
export {
|
|
743
|
-
classifyAuth,
|
|
744
|
-
readClaudeJson,
|
|
745
|
-
detectAuth,
|
|
746
|
-
detectAuthMode,
|
|
747
|
-
costIsRealMoney,
|
|
748
|
-
redactText,
|
|
749
|
-
redactValue,
|
|
750
|
-
GUARD_TIMEOUT_MS,
|
|
751
|
-
ALLOW_CACHE_MS,
|
|
752
|
-
naturalDedupKey,
|
|
753
|
-
truncatePayload,
|
|
754
|
-
withRelPath,
|
|
755
|
-
repoLabelFromCwd,
|
|
756
|
-
parseTranscript
|
|
757
|
-
};
|
|
722
|
+
export { ALLOW_CACHE_MS, GUARD_TIMEOUT_MS, classifyAuth, costIsRealMoney, detectAuth, detectAuthMode, naturalDedupKey, parseTranscript, readClaudeJson, redactText, redactValue, repoLabelFromCwd, truncatePayload, withRelPath };
|
package/dist/cli.js
CHANGED
|
@@ -1,25 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
parseTranscript,
|
|
9
|
-
redactValue,
|
|
10
|
-
truncatePayload,
|
|
11
|
-
withRelPath
|
|
12
|
-
} from "./chunk-AZYPMFOF.js";
|
|
2
|
+
import { detectAuth, GUARD_TIMEOUT_MS, parseTranscript, detectAuthMode, withRelPath, redactValue, ALLOW_CACHE_MS, truncatePayload, naturalDedupKey } from './chunk-KU3AJVVR.js';
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, statSync, watch, copyFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
4
|
+
import { homedir, userInfo, hostname, tmpdir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { execFileSync, spawn } from 'child_process';
|
|
7
|
+
import { createInterface } from 'readline/promises';
|
|
13
8
|
|
|
14
|
-
// src/commands/doctor.ts
|
|
15
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
16
|
-
import { homedir as homedir2 } from "os";
|
|
17
|
-
import { join as join2 } from "path";
|
|
18
|
-
|
|
19
|
-
// src/config.ts
|
|
20
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
21
|
-
import { homedir } from "os";
|
|
22
|
-
import { join } from "path";
|
|
23
9
|
var DEFAULT_API_URL = "https://tokenoscopy.com";
|
|
24
10
|
function configDir() {
|
|
25
11
|
return process.env.TOKENOSCOPY_CONFIG_DIR ?? join(homedir(), ".config", "tokenoscopy");
|
|
@@ -59,10 +45,10 @@ async function runDoctor() {
|
|
|
59
45
|
out("\u2717 no config \u2014 run `npx tokenoscopy init`");
|
|
60
46
|
failures++;
|
|
61
47
|
}
|
|
62
|
-
const settingsPath =
|
|
63
|
-
if (
|
|
48
|
+
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
49
|
+
if (existsSync(settingsPath)) {
|
|
64
50
|
try {
|
|
65
|
-
const settings = JSON.parse(
|
|
51
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
66
52
|
const raw = JSON.stringify(settings.hooks ?? {});
|
|
67
53
|
const hasHttp = raw.includes("/api/ingest/hook");
|
|
68
54
|
const hasFlush = raw.includes("tokenoscopy flush");
|
|
@@ -87,8 +73,8 @@ async function runDoctor() {
|
|
|
87
73
|
}[auth.mode];
|
|
88
74
|
out(`${auth.mode === "unknown" ? "\u26A0" : "\u2713"} billing lane: ${laneLabel}`);
|
|
89
75
|
out(` evidence: ${auth.evidence}`);
|
|
90
|
-
const projectsDir =
|
|
91
|
-
if (
|
|
76
|
+
const projectsDir = join(homedir(), ".claude", "projects");
|
|
77
|
+
if (existsSync(projectsDir)) {
|
|
92
78
|
out(`\u2713 transcript directory readable (${projectsDir})`);
|
|
93
79
|
} else {
|
|
94
80
|
out(`\u26A0 no transcript directory yet (${projectsDir}) \u2014 appears after the first session`);
|
|
@@ -131,13 +117,6 @@ async function runDoctor() {
|
|
|
131
117
|
${failures} problem(s) found.`);
|
|
132
118
|
return failures === 0 ? 0 : 1;
|
|
133
119
|
}
|
|
134
|
-
|
|
135
|
-
// src/commands/flush.ts
|
|
136
|
-
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
137
|
-
|
|
138
|
-
// src/identity.ts
|
|
139
|
-
import { execFileSync } from "child_process";
|
|
140
|
-
import { hostname, userInfo } from "os";
|
|
141
120
|
function git(args, cwd) {
|
|
142
121
|
try {
|
|
143
122
|
const out = execFileSync("git", args, {
|
|
@@ -274,14 +253,14 @@ async function runFlush(args) {
|
|
|
274
253
|
}
|
|
275
254
|
}
|
|
276
255
|
}
|
|
277
|
-
if (!transcriptPath || !
|
|
256
|
+
if (!transcriptPath || !existsSync(transcriptPath)) {
|
|
278
257
|
process.stderr.write("tokenoscopy flush: no transcript to flush\n");
|
|
279
258
|
return 0;
|
|
280
259
|
}
|
|
281
260
|
let upload;
|
|
282
261
|
try {
|
|
283
262
|
const { summary, events, warnings } = parseTranscript(
|
|
284
|
-
|
|
263
|
+
readFileSync(transcriptPath, "utf8")
|
|
285
264
|
);
|
|
286
265
|
const identity = detectIdentity(summary.cwd);
|
|
287
266
|
summary.actor.machine_id = identity.machine;
|
|
@@ -351,18 +330,13 @@ async function runFlush(args) {
|
|
|
351
330
|
}
|
|
352
331
|
return 0;
|
|
353
332
|
}
|
|
354
|
-
|
|
355
|
-
// src/commands/guard.ts
|
|
356
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
357
|
-
import { tmpdir } from "os";
|
|
358
|
-
import { join as join3 } from "path";
|
|
359
333
|
function cachePath(sessionId) {
|
|
360
334
|
const safe = sessionId.replace(/[^a-zA-Z0-9-]/g, "").slice(0, 64);
|
|
361
|
-
return
|
|
335
|
+
return join(tmpdir(), `tokenoscopy-guard-${safe}.json`);
|
|
362
336
|
}
|
|
363
337
|
function readAllowCache(sessionId) {
|
|
364
338
|
try {
|
|
365
|
-
const raw = JSON.parse(
|
|
339
|
+
const raw = JSON.parse(readFileSync(cachePath(sessionId), "utf8"));
|
|
366
340
|
return typeof raw.until === "number" && raw.until > Date.now();
|
|
367
341
|
} catch {
|
|
368
342
|
return false;
|
|
@@ -370,7 +344,7 @@ function readAllowCache(sessionId) {
|
|
|
370
344
|
}
|
|
371
345
|
function writeAllowCache(sessionId) {
|
|
372
346
|
try {
|
|
373
|
-
|
|
347
|
+
writeFileSync(
|
|
374
348
|
cachePath(sessionId),
|
|
375
349
|
JSON.stringify({ until: Date.now() + ALLOW_CACHE_MS })
|
|
376
350
|
);
|
|
@@ -447,13 +421,121 @@ async function runGuard() {
|
|
|
447
421
|
return 0;
|
|
448
422
|
}
|
|
449
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
|
+
}
|
|
450
537
|
|
|
451
538
|
// src/commands/init.ts
|
|
452
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
453
|
-
import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3, copyFileSync } from "fs";
|
|
454
|
-
import { homedir as homedir3 } from "os";
|
|
455
|
-
import { join as join4 } from "path";
|
|
456
|
-
import { createInterface } from "readline/promises";
|
|
457
539
|
var MARKER = "tokenoscopy";
|
|
458
540
|
function isOurs(group) {
|
|
459
541
|
return JSON.stringify(group).toLowerCase().includes(MARKER);
|
|
@@ -462,7 +544,7 @@ function resolveInvocation() {
|
|
|
462
544
|
const override = process.env.TOKENOSCOPY_BIN?.trim();
|
|
463
545
|
if (override) return override;
|
|
464
546
|
try {
|
|
465
|
-
const found =
|
|
547
|
+
const found = execFileSync("command", ["-v", "tokenoscopy"], {
|
|
466
548
|
encoding: "utf8",
|
|
467
549
|
shell: "/bin/sh",
|
|
468
550
|
timeout: 2e3,
|
|
@@ -522,11 +604,20 @@ async function runInit(args) {
|
|
|
522
604
|
const flags = /* @__PURE__ */ new Map();
|
|
523
605
|
for (let i = 0; i < args.length; i++) {
|
|
524
606
|
const a = args[i];
|
|
525
|
-
if (a === "--project" || a === "--print") flags.set(a, true);
|
|
607
|
+
if (a === "--project" || a === "--print" || a === "--no-browser") flags.set(a, true);
|
|
526
608
|
else if (a.startsWith("--")) flags.set(a, args[++i] ?? "");
|
|
527
609
|
}
|
|
528
610
|
const apiUrl = flags.get("--url") || process.env.TOKENOSCOPY_API_URL || DEFAULT_API_URL;
|
|
529
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
|
+
}
|
|
530
621
|
if (!key) {
|
|
531
622
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
532
623
|
key = (await rl.question("Paste your ingest key (tok_\u2026): ")).trim();
|
|
@@ -553,11 +644,11 @@ async function runInit(args) {
|
|
|
553
644
|
}
|
|
554
645
|
const config = { apiUrl, key };
|
|
555
646
|
saveConfig(config);
|
|
556
|
-
const settingsPath = flags.get("--project") ?
|
|
647
|
+
const settingsPath = flags.get("--project") ? join(process.cwd(), ".claude", "settings.json") : join(homedir(), ".claude", "settings.json");
|
|
557
648
|
let settings = {};
|
|
558
|
-
if (
|
|
649
|
+
if (existsSync(settingsPath)) {
|
|
559
650
|
try {
|
|
560
|
-
settings = JSON.parse(
|
|
651
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
561
652
|
copyFileSync(settingsPath, `${settingsPath}.tokenoscopy-backup`);
|
|
562
653
|
} catch {
|
|
563
654
|
process.stderr.write(`\u2717 ${settingsPath} exists but isn't valid JSON \u2014 fix it first.
|
|
@@ -574,12 +665,12 @@ async function runInit(args) {
|
|
|
574
665
|
`);
|
|
575
666
|
return 0;
|
|
576
667
|
}
|
|
577
|
-
const dir =
|
|
578
|
-
if (!
|
|
579
|
-
const { mkdirSync: mkdirSync2 } = await import(
|
|
668
|
+
const dir = join(settingsPath, "..");
|
|
669
|
+
if (!existsSync(dir)) {
|
|
670
|
+
const { mkdirSync: mkdirSync2 } = await import('fs');
|
|
580
671
|
mkdirSync2(dir, { recursive: true });
|
|
581
672
|
}
|
|
582
|
-
|
|
673
|
+
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
583
674
|
`);
|
|
584
675
|
process.stderr.write(
|
|
585
676
|
[
|
|
@@ -592,15 +683,10 @@ async function runInit(args) {
|
|
|
592
683
|
);
|
|
593
684
|
return 0;
|
|
594
685
|
}
|
|
595
|
-
|
|
596
|
-
// src/commands/tail.ts
|
|
597
|
-
import { watch, existsSync as existsSync5, readdirSync, statSync } from "fs";
|
|
598
|
-
import { homedir as homedir4 } from "os";
|
|
599
|
-
import { join as join5 } from "path";
|
|
600
686
|
var DEBOUNCE_MS = 3e3;
|
|
601
687
|
async function runTail(args) {
|
|
602
|
-
const projectsDir = process.env.TOKENOSCOPY_PROJECTS_DIR ??
|
|
603
|
-
if (!
|
|
688
|
+
const projectsDir = process.env.TOKENOSCOPY_PROJECTS_DIR ?? join(homedir(), ".claude", "projects");
|
|
689
|
+
if (!existsSync(projectsDir)) {
|
|
604
690
|
process.stderr.write(`tokenoscopy tail: ${projectsDir} does not exist
|
|
605
691
|
`);
|
|
606
692
|
return 1;
|
|
@@ -612,7 +698,7 @@ async function runTail(args) {
|
|
|
612
698
|
const cutoff = Date.now() - days * 24 * 3600 * 1e3;
|
|
613
699
|
let count = 0;
|
|
614
700
|
for (const dir of readdirSync(projectsDir)) {
|
|
615
|
-
const full =
|
|
701
|
+
const full = join(projectsDir, dir);
|
|
616
702
|
let entries;
|
|
617
703
|
try {
|
|
618
704
|
entries = readdirSync(full);
|
|
@@ -621,7 +707,7 @@ async function runTail(args) {
|
|
|
621
707
|
}
|
|
622
708
|
for (const f of entries) {
|
|
623
709
|
if (!f.endsWith(".jsonl")) continue;
|
|
624
|
-
const p =
|
|
710
|
+
const p = join(full, f);
|
|
625
711
|
try {
|
|
626
712
|
if (statSync(p).mtimeMs >= cutoff) {
|
|
627
713
|
await runFlush(["--transcript", p]);
|
|
@@ -640,14 +726,14 @@ async function runTail(args) {
|
|
|
640
726
|
const pending = /* @__PURE__ */ new Map();
|
|
641
727
|
watch(projectsDir, { recursive: true }, (_event, filename) => {
|
|
642
728
|
if (!filename || !filename.endsWith(".jsonl")) return;
|
|
643
|
-
const full =
|
|
729
|
+
const full = join(projectsDir, filename);
|
|
644
730
|
const existing = pending.get(full);
|
|
645
731
|
if (existing) clearTimeout(existing);
|
|
646
732
|
pending.set(
|
|
647
733
|
full,
|
|
648
734
|
setTimeout(() => {
|
|
649
735
|
pending.delete(full);
|
|
650
|
-
if (
|
|
736
|
+
if (existsSync(full)) {
|
|
651
737
|
void runFlush(["--transcript", full]);
|
|
652
738
|
}
|
|
653
739
|
}, DEBOUNCE_MS)
|
|
@@ -665,7 +751,9 @@ Usage: tokenoscopy <command> [options]
|
|
|
665
751
|
|
|
666
752
|
Commands:
|
|
667
753
|
init Wire Claude Code hooks + transcript flush to your workspace
|
|
668
|
-
|
|
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
|
|
669
757
|
--url https://\u2026 API base (default https://tokenoscopy.com)
|
|
670
758
|
--project write hooks to ./.claude/settings.json (CI)
|
|
671
759
|
--print print the hook config instead of writing
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1 @@
|
|
|
1
|
-
|
|
2
|
-
classifyAuth,
|
|
3
|
-
costIsRealMoney,
|
|
4
|
-
detectAuth,
|
|
5
|
-
detectAuthMode,
|
|
6
|
-
parseTranscript,
|
|
7
|
-
readClaudeJson,
|
|
8
|
-
redactText,
|
|
9
|
-
redactValue,
|
|
10
|
-
repoLabelFromCwd
|
|
11
|
-
} from "./chunk-AZYPMFOF.js";
|
|
12
|
-
export {
|
|
13
|
-
classifyAuth,
|
|
14
|
-
costIsRealMoney,
|
|
15
|
-
detectAuth,
|
|
16
|
-
detectAuthMode,
|
|
17
|
-
parseTranscript,
|
|
18
|
-
readClaudeJson,
|
|
19
|
-
redactText,
|
|
20
|
-
redactValue,
|
|
21
|
-
repoLabelFromCwd
|
|
22
|
-
};
|
|
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",
|