tokentracker-cli 0.6.7 → 0.8.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/dashboard/dist/assets/{main-CuwxhzW4.js → main-CBVhF0BE.js} +169 -168
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +2 -2
- package/src/commands/init.js +16 -4
- package/src/commands/status.js +13 -0
- package/src/commands/sync.js +154 -2
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +285 -1
- package/src/lib/source-metadata.js +5 -3
package/src/lib/rollout.js
CHANGED
|
@@ -10,6 +10,9 @@ const { ensureDir } = require("./fs");
|
|
|
10
10
|
const DEFAULT_SOURCE = "codex";
|
|
11
11
|
const DEFAULT_MODEL = "unknown";
|
|
12
12
|
const BUCKET_SEPARATOR = "|";
|
|
13
|
+
const CLAUDE_MEM_OBSERVER_PATH_SEGMENT = "--claude-mem-observer-sessions";
|
|
14
|
+
const CLAUDE_MEM_OBSERVER_PROJECT_REF =
|
|
15
|
+
"https://local.tokentracker/claude-mem/observer-sessions";
|
|
13
16
|
|
|
14
17
|
async function listRolloutFiles(sessionsDir) {
|
|
15
18
|
const out = [];
|
|
@@ -1820,6 +1823,12 @@ async function resolveProjectMetaForPath(startDir, cache) {
|
|
|
1820
1823
|
if (!startDir || typeof startDir !== "string") return null;
|
|
1821
1824
|
if (cache && cache.has(startDir)) return cache.get(startDir);
|
|
1822
1825
|
|
|
1826
|
+
if (startDir.includes(CLAUDE_MEM_OBSERVER_PATH_SEGMENT)) {
|
|
1827
|
+
const meta = { projectRef: CLAUDE_MEM_OBSERVER_PROJECT_REF, repoRoot: startDir };
|
|
1828
|
+
if (cache) cache.set(startDir, meta);
|
|
1829
|
+
return meta;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1823
1832
|
const visited = [];
|
|
1824
1833
|
let current = startDir;
|
|
1825
1834
|
const root = path.parse(startDir).root;
|
|
@@ -4478,8 +4487,40 @@ function resolveOmpHome(env = process.env) {
|
|
|
4478
4487
|
return path.join(home, ".omp");
|
|
4479
4488
|
}
|
|
4480
4489
|
|
|
4490
|
+
// PI_CODING_AGENT_DIR is documented by both pi-coding-agent and oh-my-pi as
|
|
4491
|
+
// their agent directory override. When set, attribute it to whichever tool the
|
|
4492
|
+
// user actually has installed: ~/.pi present → "pi", otherwise "omp" (the
|
|
4493
|
+
// historical default in this codebase, preserved for back-compat).
|
|
4494
|
+
//
|
|
4495
|
+
// Users with both tools installed can disambiguate explicitly with
|
|
4496
|
+
// TOKENTRACKER_PI_AGENT_DIR / TOKENTRACKER_OMP_AGENT_DIR, which take
|
|
4497
|
+
// precedence in their respective resolvers.
|
|
4498
|
+
function decidePiCodingAgentDirOwner(env = process.env) {
|
|
4499
|
+
const home = env.HOME || require("node:os").homedir();
|
|
4500
|
+
// Require an actual directory — a stray file (lockfile, junk) at ~/.pi
|
|
4501
|
+
// shouldn't reroute an existing oh-my-pi user's PI_CODING_AGENT_DIR override.
|
|
4502
|
+
try {
|
|
4503
|
+
if (fssync.statSync(path.join(home, ".pi")).isDirectory()) return "pi";
|
|
4504
|
+
} catch {
|
|
4505
|
+
// ENOENT or EACCES — treat as "no pi install signal".
|
|
4506
|
+
}
|
|
4507
|
+
return "omp";
|
|
4508
|
+
}
|
|
4509
|
+
|
|
4510
|
+
function expandHomePath(dir, env = process.env) {
|
|
4511
|
+
if (typeof dir !== "string" || !dir) return dir;
|
|
4512
|
+
if (dir !== "~" && !dir.startsWith("~/")) return dir;
|
|
4513
|
+
const home = env.HOME || require("node:os").homedir();
|
|
4514
|
+
return dir === "~" ? home : path.join(home, dir.slice(2));
|
|
4515
|
+
}
|
|
4516
|
+
|
|
4481
4517
|
function resolveOmpAgentDir(env = process.env) {
|
|
4482
|
-
if (env.
|
|
4518
|
+
if (env.TOKENTRACKER_OMP_AGENT_DIR) {
|
|
4519
|
+
return expandHomePath(env.TOKENTRACKER_OMP_AGENT_DIR, env);
|
|
4520
|
+
}
|
|
4521
|
+
if (env.PI_CODING_AGENT_DIR && decidePiCodingAgentDirOwner(env) === "omp") {
|
|
4522
|
+
return expandHomePath(env.PI_CODING_AGENT_DIR, env);
|
|
4523
|
+
}
|
|
4483
4524
|
return path.join(resolveOmpHome(env), "agent");
|
|
4484
4525
|
}
|
|
4485
4526
|
|
|
@@ -4692,6 +4733,243 @@ async function parseOmpIncremental({
|
|
|
4692
4733
|
return { recordsProcessed, eventsAggregated, bucketsQueued };
|
|
4693
4734
|
}
|
|
4694
4735
|
|
|
4736
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4737
|
+
// pi (@mariozechner/pi-coding-agent) — passive JSONL reader
|
|
4738
|
+
// (~/.pi/agent/sessions/**/*.jsonl)
|
|
4739
|
+
//
|
|
4740
|
+
// Same on-disk session format as oh-my-pi (omp): one JSONL file per session,
|
|
4741
|
+
// first line type:"session" header, then a tree of message/model_change/etc.
|
|
4742
|
+
// records. Token usage lives on type:"message" entries with role:"assistant"
|
|
4743
|
+
// under message.usage.
|
|
4744
|
+
//
|
|
4745
|
+
// PI_CODING_AGENT_DIR is shared with omp (both upstream tools document it).
|
|
4746
|
+
// resolvePiAgentDir / resolveOmpAgentDir use decidePiCodingAgentDirOwner to
|
|
4747
|
+
// route the override to exactly one provider so the same sessions dir is
|
|
4748
|
+
// never scanned twice.
|
|
4749
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4750
|
+
|
|
4751
|
+
function resolvePiHome(env = process.env) {
|
|
4752
|
+
const home = env.HOME || require("node:os").homedir();
|
|
4753
|
+
return path.join(home, ".pi");
|
|
4754
|
+
}
|
|
4755
|
+
|
|
4756
|
+
function resolvePiAgentDir(env = process.env) {
|
|
4757
|
+
if (env.TOKENTRACKER_PI_AGENT_DIR) {
|
|
4758
|
+
return expandHomePath(env.TOKENTRACKER_PI_AGENT_DIR, env);
|
|
4759
|
+
}
|
|
4760
|
+
if (env.PI_CODING_AGENT_DIR && decidePiCodingAgentDirOwner(env) === "pi") {
|
|
4761
|
+
return expandHomePath(env.PI_CODING_AGENT_DIR, env);
|
|
4762
|
+
}
|
|
4763
|
+
return path.join(resolvePiHome(env), "agent");
|
|
4764
|
+
}
|
|
4765
|
+
|
|
4766
|
+
// Defense in depth for invariant 2 (no double-count). Two explicit overrides
|
|
4767
|
+
// pointing at the same path (e.g. TOKENTRACKER_OMP_AGENT_DIR === TOKENTRACKER_PI_AGENT_DIR,
|
|
4768
|
+
// or TOKENTRACKER_OMP_AGENT_DIR === PI_CODING_AGENT_DIR with ~/.pi present) bypass
|
|
4769
|
+
// the install-signal disambiguator and would otherwise have both providers scan
|
|
4770
|
+
// the same sessions directory under different `source` tags.
|
|
4771
|
+
function piAgentDirCollidesWithOmp(env = process.env) {
|
|
4772
|
+
return path.resolve(resolvePiAgentDir(env)) === path.resolve(resolveOmpAgentDir(env));
|
|
4773
|
+
}
|
|
4774
|
+
|
|
4775
|
+
function resolvePiSessionFiles(env = process.env) {
|
|
4776
|
+
const sessionsDir = path.join(resolvePiAgentDir(env), "sessions");
|
|
4777
|
+
if (!fssync.existsSync(sessionsDir)) return [];
|
|
4778
|
+
const files = [];
|
|
4779
|
+
try {
|
|
4780
|
+
for (const cwdDir of fssync.readdirSync(sessionsDir)) {
|
|
4781
|
+
const cwdPath = path.join(sessionsDir, cwdDir);
|
|
4782
|
+
let stat;
|
|
4783
|
+
try { stat = fssync.statSync(cwdPath); } catch { continue; }
|
|
4784
|
+
if (!stat.isDirectory()) continue;
|
|
4785
|
+
let entries;
|
|
4786
|
+
try { entries = fssync.readdirSync(cwdPath); } catch { continue; }
|
|
4787
|
+
for (const entry of entries) {
|
|
4788
|
+
if (!entry.endsWith(".jsonl")) continue;
|
|
4789
|
+
files.push(path.join(cwdPath, entry));
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
} catch {
|
|
4793
|
+
// ignore — return what we have
|
|
4794
|
+
}
|
|
4795
|
+
files.sort((a, b) => a.localeCompare(b));
|
|
4796
|
+
return files;
|
|
4797
|
+
}
|
|
4798
|
+
|
|
4799
|
+
function resolvePiDefaultModel() {
|
|
4800
|
+
// pi has no global default model; model is per-message.
|
|
4801
|
+
return "pi-unknown";
|
|
4802
|
+
}
|
|
4803
|
+
|
|
4804
|
+
async function parsePiIncremental({
|
|
4805
|
+
sessionFiles,
|
|
4806
|
+
cursors,
|
|
4807
|
+
queuePath,
|
|
4808
|
+
onProgress,
|
|
4809
|
+
env,
|
|
4810
|
+
defaultModel,
|
|
4811
|
+
} = {}) {
|
|
4812
|
+
await ensureDir(path.dirname(queuePath));
|
|
4813
|
+
const piState = cursors.pi && typeof cursors.pi === "object" ? cursors.pi : {};
|
|
4814
|
+
const seenIds = new Set(Array.isArray(piState.seenIds) ? piState.seenIds : []);
|
|
4815
|
+
const fileOffsets =
|
|
4816
|
+
piState.fileOffsets && typeof piState.fileOffsets === "object"
|
|
4817
|
+
? { ...piState.fileOffsets }
|
|
4818
|
+
: {};
|
|
4819
|
+
|
|
4820
|
+
const files = Array.isArray(sessionFiles)
|
|
4821
|
+
? sessionFiles
|
|
4822
|
+
: resolvePiSessionFiles(env || process.env);
|
|
4823
|
+
const fallbackModel = defaultModel || resolvePiDefaultModel();
|
|
4824
|
+
|
|
4825
|
+
if (files.length === 0) {
|
|
4826
|
+
cursors.pi = {
|
|
4827
|
+
...piState,
|
|
4828
|
+
seenIds: Array.from(seenIds),
|
|
4829
|
+
fileOffsets,
|
|
4830
|
+
updatedAt: new Date().toISOString(),
|
|
4831
|
+
};
|
|
4832
|
+
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
4833
|
+
}
|
|
4834
|
+
|
|
4835
|
+
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
4836
|
+
const touchedBuckets = new Set();
|
|
4837
|
+
const cb = typeof onProgress === "function" ? onProgress : null;
|
|
4838
|
+
let recordsProcessed = 0;
|
|
4839
|
+
let eventsAggregated = 0;
|
|
4840
|
+
|
|
4841
|
+
for (let fileIdx = 0; fileIdx < files.length; fileIdx++) {
|
|
4842
|
+
const filePath = files[fileIdx];
|
|
4843
|
+
let stat;
|
|
4844
|
+
try { stat = fssync.statSync(filePath); } catch { continue; }
|
|
4845
|
+
|
|
4846
|
+
const prevEntry = fileOffsets[filePath] || {};
|
|
4847
|
+
const prevSize = Number(prevEntry.size) || 0;
|
|
4848
|
+
const prevIno = prevEntry.ino;
|
|
4849
|
+
const inodeChanged = typeof prevIno === "number" && prevIno !== stat.ino;
|
|
4850
|
+
const startOffset = stat.size < prevSize || inodeChanged ? 0 : prevSize;
|
|
4851
|
+
if (stat.size <= startOffset) continue;
|
|
4852
|
+
|
|
4853
|
+
let stream;
|
|
4854
|
+
try {
|
|
4855
|
+
stream = fssync.createReadStream(filePath, {
|
|
4856
|
+
encoding: "utf8",
|
|
4857
|
+
start: startOffset,
|
|
4858
|
+
});
|
|
4859
|
+
} catch { continue; }
|
|
4860
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
4861
|
+
|
|
4862
|
+
for await (const line of rl) {
|
|
4863
|
+
if (!line || !line.trim()) continue;
|
|
4864
|
+
let entry;
|
|
4865
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
4866
|
+
|
|
4867
|
+
if (!entry || entry.type !== "message") continue;
|
|
4868
|
+
|
|
4869
|
+
const msg = entry.message;
|
|
4870
|
+
if (!msg || msg.role !== "assistant") continue;
|
|
4871
|
+
|
|
4872
|
+
const usage = msg.usage;
|
|
4873
|
+
if (!usage || typeof usage !== "object") continue;
|
|
4874
|
+
|
|
4875
|
+
const entryId = typeof entry.id === "string" && entry.id ? entry.id : null;
|
|
4876
|
+
if (!entryId) continue;
|
|
4877
|
+
if (seenIds.has(entryId)) continue;
|
|
4878
|
+
|
|
4879
|
+
recordsProcessed++;
|
|
4880
|
+
|
|
4881
|
+
const input = toNonNegativeInt(usage.input);
|
|
4882
|
+
const output = toNonNegativeInt(usage.output);
|
|
4883
|
+
const cacheRead = toNonNegativeInt(usage.cacheRead);
|
|
4884
|
+
const cacheWrite = toNonNegativeInt(usage.cacheWrite);
|
|
4885
|
+
const reasoningTokens = toNonNegativeInt(usage.reasoningTokens);
|
|
4886
|
+
|
|
4887
|
+
if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0) {
|
|
4888
|
+
seenIds.add(entryId);
|
|
4889
|
+
continue;
|
|
4890
|
+
}
|
|
4891
|
+
|
|
4892
|
+
let tsMs = null;
|
|
4893
|
+
if (Number.isFinite(Number(msg.timestamp)) && Number(msg.timestamp) > 0) {
|
|
4894
|
+
tsMs = Number(msg.timestamp);
|
|
4895
|
+
} else if (typeof entry.timestamp === "string" && entry.timestamp) {
|
|
4896
|
+
const parsed = Date.parse(entry.timestamp);
|
|
4897
|
+
if (Number.isFinite(parsed) && parsed > 0) tsMs = parsed;
|
|
4898
|
+
}
|
|
4899
|
+
if (tsMs == null) {
|
|
4900
|
+
seenIds.add(entryId);
|
|
4901
|
+
continue;
|
|
4902
|
+
}
|
|
4903
|
+
|
|
4904
|
+
const tsIso = new Date(tsMs).toISOString();
|
|
4905
|
+
const bucketStart = toUtcHalfHourStart(tsIso);
|
|
4906
|
+
if (!bucketStart) continue;
|
|
4907
|
+
|
|
4908
|
+
const totalTokens =
|
|
4909
|
+
Number.isFinite(Number(usage.totalTokens)) && Number(usage.totalTokens) > 0
|
|
4910
|
+
? toNonNegativeInt(usage.totalTokens)
|
|
4911
|
+
: input + output + cacheRead + cacheWrite + reasoningTokens;
|
|
4912
|
+
|
|
4913
|
+
const model = normalizeModelInput(msg.model) || fallbackModel;
|
|
4914
|
+
|
|
4915
|
+
const delta = {
|
|
4916
|
+
input_tokens: input,
|
|
4917
|
+
cached_input_tokens: cacheRead,
|
|
4918
|
+
cache_creation_input_tokens: cacheWrite,
|
|
4919
|
+
output_tokens: output,
|
|
4920
|
+
reasoning_output_tokens: reasoningTokens,
|
|
4921
|
+
total_tokens: totalTokens,
|
|
4922
|
+
conversation_count: 1,
|
|
4923
|
+
};
|
|
4924
|
+
|
|
4925
|
+
const bucket = getHourlyBucket(hourlyState, "pi", model, bucketStart);
|
|
4926
|
+
addTotals(bucket.totals, delta);
|
|
4927
|
+
touchedBuckets.add(bucketKey("pi", model, bucketStart));
|
|
4928
|
+
seenIds.add(entryId);
|
|
4929
|
+
eventsAggregated++;
|
|
4930
|
+
|
|
4931
|
+
if (cb) {
|
|
4932
|
+
cb({
|
|
4933
|
+
index: fileIdx + 1,
|
|
4934
|
+
total: files.length,
|
|
4935
|
+
recordsProcessed,
|
|
4936
|
+
eventsAggregated,
|
|
4937
|
+
bucketsQueued: touchedBuckets.size,
|
|
4938
|
+
});
|
|
4939
|
+
}
|
|
4940
|
+
}
|
|
4941
|
+
|
|
4942
|
+
let postStat = stat;
|
|
4943
|
+
try { postStat = fssync.statSync(filePath); } catch {}
|
|
4944
|
+
fileOffsets[filePath] = {
|
|
4945
|
+
size: postStat.size,
|
|
4946
|
+
mtimeMs: postStat.mtimeMs,
|
|
4947
|
+
ino: postStat.ino,
|
|
4948
|
+
};
|
|
4949
|
+
}
|
|
4950
|
+
|
|
4951
|
+
const seenArr = Array.from(seenIds);
|
|
4952
|
+
const cappedSeen =
|
|
4953
|
+
seenArr.length > 10_000 ? seenArr.slice(seenArr.length - 10_000) : seenArr;
|
|
4954
|
+
|
|
4955
|
+
const bucketsQueued = await enqueueTouchedBuckets({
|
|
4956
|
+
queuePath,
|
|
4957
|
+
hourlyState,
|
|
4958
|
+
touchedBuckets,
|
|
4959
|
+
});
|
|
4960
|
+
const updatedAt = new Date().toISOString();
|
|
4961
|
+
hourlyState.updatedAt = updatedAt;
|
|
4962
|
+
cursors.hourly = hourlyState;
|
|
4963
|
+
cursors.pi = {
|
|
4964
|
+
...piState,
|
|
4965
|
+
seenIds: cappedSeen,
|
|
4966
|
+
fileOffsets,
|
|
4967
|
+
updatedAt,
|
|
4968
|
+
};
|
|
4969
|
+
|
|
4970
|
+
return { recordsProcessed, eventsAggregated, bucketsQueued };
|
|
4971
|
+
}
|
|
4972
|
+
|
|
4695
4973
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
4696
4974
|
// Craft Agents (lukilabs/craft-agents-oss) — passive JSONL reader
|
|
4697
4975
|
//
|
|
@@ -5248,6 +5526,12 @@ module.exports = {
|
|
|
5248
5526
|
resolveOmpSessionFiles,
|
|
5249
5527
|
resolveOmpDefaultModel,
|
|
5250
5528
|
parseOmpIncremental,
|
|
5529
|
+
resolvePiHome,
|
|
5530
|
+
resolvePiAgentDir,
|
|
5531
|
+
resolvePiSessionFiles,
|
|
5532
|
+
resolvePiDefaultModel,
|
|
5533
|
+
parsePiIncremental,
|
|
5534
|
+
piAgentDirCollidesWithOmp,
|
|
5251
5535
|
resolveCraftConfigDir,
|
|
5252
5536
|
resolveCraftWorkspaceRoots,
|
|
5253
5537
|
resolveCraftSessionFiles,
|
|
@@ -14,16 +14,18 @@ function isAccountLevelSource(source) {
|
|
|
14
14
|
|
|
15
15
|
function normalizeUsageScope(value) {
|
|
16
16
|
const raw = String(value || "").trim().toLowerCase();
|
|
17
|
-
|
|
17
|
+
if (!raw || raw === "all" || raw === "raw") return "all";
|
|
18
|
+
if (raw === "personal" || raw === "local") return "personal";
|
|
19
|
+
return "all";
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
function filterRowsByUsageScope(rows, scope = "
|
|
22
|
+
function filterRowsByUsageScope(rows, scope = "all") {
|
|
21
23
|
const normalizedScope = normalizeUsageScope(scope);
|
|
22
24
|
if (normalizedScope === "all") return Array.isArray(rows) ? rows : [];
|
|
23
25
|
return (Array.isArray(rows) ? rows : []).filter((row) => !isAccountLevelSource(row?.source));
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
function listExcludedSources(rows, scope = "
|
|
28
|
+
function listExcludedSources(rows, scope = "all") {
|
|
27
29
|
const normalizedScope = normalizeUsageScope(scope);
|
|
28
30
|
if (normalizedScope === "all") return [];
|
|
29
31
|
const seen = new Set();
|