tokentracker-cli 0.7.0 → 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 +32 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +276 -1
package/src/lib/rollout.js
CHANGED
|
@@ -4487,8 +4487,40 @@ function resolveOmpHome(env = process.env) {
|
|
|
4487
4487
|
return path.join(home, ".omp");
|
|
4488
4488
|
}
|
|
4489
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
|
+
|
|
4490
4517
|
function resolveOmpAgentDir(env = process.env) {
|
|
4491
|
-
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
|
+
}
|
|
4492
4524
|
return path.join(resolveOmpHome(env), "agent");
|
|
4493
4525
|
}
|
|
4494
4526
|
|
|
@@ -4701,6 +4733,243 @@ async function parseOmpIncremental({
|
|
|
4701
4733
|
return { recordsProcessed, eventsAggregated, bucketsQueued };
|
|
4702
4734
|
}
|
|
4703
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
|
+
|
|
4704
4973
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
4705
4974
|
// Craft Agents (lukilabs/craft-agents-oss) — passive JSONL reader
|
|
4706
4975
|
//
|
|
@@ -5257,6 +5526,12 @@ module.exports = {
|
|
|
5257
5526
|
resolveOmpSessionFiles,
|
|
5258
5527
|
resolveOmpDefaultModel,
|
|
5259
5528
|
parseOmpIncremental,
|
|
5529
|
+
resolvePiHome,
|
|
5530
|
+
resolvePiAgentDir,
|
|
5531
|
+
resolvePiSessionFiles,
|
|
5532
|
+
resolvePiDefaultModel,
|
|
5533
|
+
parsePiIncremental,
|
|
5534
|
+
piAgentDirCollidesWithOmp,
|
|
5260
5535
|
resolveCraftConfigDir,
|
|
5261
5536
|
resolveCraftWorkspaceRoots,
|
|
5262
5537
|
resolveCraftSessionFiles,
|