@yhong91/vibetime 0.1.40 → 0.1.41

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.
Files changed (2) hide show
  1. package/bin/vibetime.mjs +143 -17
  2. package/package.json +1 -1
package/bin/vibetime.mjs CHANGED
@@ -1928,7 +1928,7 @@ function countTextLines(text) {
1928
1928
  }
1929
1929
 
1930
1930
  // src/lib/constants.ts
1931
- var PACKAGE_VERSION = true ? "0.1.40" : "0.1.1";
1931
+ var PACKAGE_VERSION = true ? "0.1.41" : "0.1.1";
1932
1932
  var DEFAULT_API_URL = "http://121.196.224.82:3001";
1933
1933
  var DEFAULT_BACKFILL_BATCH_SIZE = 50;
1934
1934
  var DEFAULT_BACKFILL_BATCH_BYTES = 800 * 1024;
@@ -3308,7 +3308,11 @@ async function isHooksJsonInstalled(filePath, command) {
3308
3308
  }
3309
3309
 
3310
3310
  // src/adapters/claude-code.ts
3311
+ init_fs();
3311
3312
  async function parseClaudeCodeSessionFile(filePath, options) {
3313
+ if (path7.basename(filePath) === "usage.jsonl" && path7.basename(path7.dirname(filePath)) === ".viberouter") {
3314
+ return parseClaudeRouterUsageFile(filePath, options);
3315
+ }
3312
3316
  const text = await readFile3(filePath, "utf8");
3313
3317
  const lines = text.split("\n").filter(Boolean);
3314
3318
  const projectContext = await claudeProjectContextFromLines(filePath, lines, options);
@@ -3575,6 +3579,86 @@ async function parseClaudeCodeSessionFile(filePath, options) {
3575
3579
  }
3576
3580
  return state.events.filter((event) => validateCanonicalEvent(event).valid);
3577
3581
  }
3582
+ async function parseClaudeRouterUsageFile(filePath, options) {
3583
+ const rows = (await readFile3(filePath, "utf8")).split("\n").map(parseJsonLine).filter((row) => Boolean(
3584
+ row && row.surface === "claude" && (numberField(row, "timestamp") ?? 0) > 0 && Object.keys(objectField(row, "usage")).length > 0
3585
+ ));
3586
+ if (rows.length === 0) {
3587
+ return [];
3588
+ }
3589
+ const timestamps = rows.map((row) => numberField(row, "timestamp") ?? 0);
3590
+ const firstTs = Math.min(...timestamps) - 6e4;
3591
+ const lastTs = Math.max(...timestamps) + 6e4;
3592
+ const home = path7.resolve(stringOption(options.home) || path7.dirname(path7.dirname(filePath)));
3593
+ const transcriptCounts = /* @__PURE__ */ new Map();
3594
+ for (const transcript of await listJsonlFiles(path7.join(claudeConfigDir(home, process.env), "projects"))) {
3595
+ const seen = /* @__PURE__ */ new Set();
3596
+ for (const line of (await readFile3(transcript, "utf8")).split("\n")) {
3597
+ const raw = parseJsonLine(line);
3598
+ const ts = raw ? Date.parse(timestampFrom(raw.timestamp) || "") : Number.NaN;
3599
+ if (!raw || stringField(raw, "type") !== "assistant" || ts < firstTs || ts > lastTs) {
3600
+ continue;
3601
+ }
3602
+ const message = objectField(raw, "message");
3603
+ const messageId = stringField(message, "id");
3604
+ const usageKey = messageId ? `${messageId}:${stringField(raw, "requestId")}` : null;
3605
+ if (usageKey && seen.has(usageKey)) {
3606
+ continue;
3607
+ }
3608
+ if (usageKey) {
3609
+ seen.add(usageKey);
3610
+ }
3611
+ const usage = claudeUsageFromMessage(message);
3612
+ if (!usage) {
3613
+ continue;
3614
+ }
3615
+ const key = `${usage.tokensInput || 0}:${usage.tokensOutput || 0}`;
3616
+ transcriptCounts.set(key, (transcriptCounts.get(key) || 0) + 1);
3617
+ }
3618
+ }
3619
+ const sourcePathHash = `sha256:${createStableHash(filePath)}`;
3620
+ const events = [];
3621
+ for (const row of rows) {
3622
+ const usage = objectField(row, "usage");
3623
+ const input = numberField(usage, "inputTokens") || 0;
3624
+ const output = numberField(usage, "outputTokens") || 0;
3625
+ const key = `${input}:${output}`;
3626
+ const matched = transcriptCounts.get(key) || 0;
3627
+ if (matched > 0) {
3628
+ transcriptCounts.set(key, matched - 1);
3629
+ continue;
3630
+ }
3631
+ const timestamp = numberField(row, "timestamp") ?? 0;
3632
+ const requestId = stringField(row, "requestId") || createStableHash(row).slice(0, 24);
3633
+ const cacheCreation = numberField(usage, "cacheCreationInputTokens") || 0;
3634
+ const cacheRead = numberField(usage, "cacheReadInputTokens") || 0;
3635
+ const cached = numberField(usage, "cachedInputTokens") || cacheCreation + cacheRead;
3636
+ const event = baseClaudeEvent({
3637
+ ts: new Date(timestamp).toISOString(),
3638
+ type: "model.usage",
3639
+ sessionId: `viberouter:${new Date(timestamp).toISOString().slice(0, 10)}`,
3640
+ project: "unknown",
3641
+ model: stringField(row, "resolvedModel") || stringField(row, "requestedModel") || stringField(row, "model"),
3642
+ provider: stringField(row, "provider"),
3643
+ confidence: "exact",
3644
+ metrics: {
3645
+ tokensInput: input || void 0,
3646
+ tokensCachedInput: cached || void 0,
3647
+ tokensCacheCreationInput: cacheCreation || void 0,
3648
+ tokensCacheReadInput: cacheRead || void 0,
3649
+ tokensOutput: output || void 0,
3650
+ tokensReasoningOutput: numberField(usage, "reasoningOutputTokens") || void 0,
3651
+ tokensTotal: numberField(usage, "totalTokens") || input + output || void 0,
3652
+ modelCalls: 1
3653
+ },
3654
+ refs: stringRefs({ sourceId: requestId, sourcePathHash, importKey: `claude-code:viberouter:${requestId}` })
3655
+ });
3656
+ if (validateCanonicalEvent(event).valid) {
3657
+ events.push(event);
3658
+ }
3659
+ }
3660
+ return events;
3661
+ }
3578
3662
  function baseClaudeEvent(event) {
3579
3663
  return {
3580
3664
  schemaVersion: AGENT_TIME_SCHEMA_VERSION,
@@ -3911,6 +3995,7 @@ function createClaudeCodeAdapter() {
3911
3995
  const base = claudeConfigDir(home, env);
3912
3996
  return [
3913
3997
  path7.join(base, "projects"),
3998
+ path7.join(home, ".viberouter", "usage.jsonl"),
3914
3999
  path7.join(base, ".claude.json"),
3915
4000
  path7.join(home, ".claude.json")
3916
4001
  ];
@@ -7491,6 +7576,11 @@ function createPiAdapter() {
7491
7576
  };
7492
7577
  }
7493
7578
 
7579
+ // src/adapters/qoder-cn.ts
7580
+ import { readdir as readdir7, readFile as readFile10, stat as stat8 } from "node:fs/promises";
7581
+ import os8 from "node:os";
7582
+ import path17 from "node:path";
7583
+
7494
7584
  // src/adapters/qoder-local-db.ts
7495
7585
  import { access } from "node:fs/promises";
7496
7586
  import os7 from "node:os";
@@ -7537,6 +7627,7 @@ async function loadQoderDbModelCalls(appDirName, sessionId, modelMap) {
7537
7627
  }
7538
7628
  const db = new DatabaseSync(dbPath, { readOnly: true });
7539
7629
  try {
7630
+ calls.rootSessionId = resolveRootSessionId(db, sessionId);
7540
7631
  const preferredModel = resolveSessionPreferredModel(db, sessionId, modelMap);
7541
7632
  const rows = db.prepare(
7542
7633
  `select request_id, token_info, model_info from chat_message where session_id = ? and role = 'assistant' and token_info != '' order by gmt_create asc`
@@ -7570,6 +7661,18 @@ async function loadQoderDbModelCalls(appDirName, sessionId, modelMap) {
7570
7661
  }
7571
7662
  return calls;
7572
7663
  }
7664
+ function resolveRootSessionId(db, sessionId) {
7665
+ let current = sessionId;
7666
+ for (let depth = 0; depth < 5; depth += 1) {
7667
+ const row = db.prepare("select parent_session_id from chat_session where session_id = ?").get(current);
7668
+ const parent = row ? stringField(row, "parent_session_id") : void 0;
7669
+ if (!parent) {
7670
+ return current === sessionId ? void 0 : current;
7671
+ }
7672
+ current = parent;
7673
+ }
7674
+ return current === sessionId ? void 0 : current;
7675
+ }
7573
7676
  function resolveSessionPreferredModel(db, sessionId, modelMap) {
7574
7677
  let current = sessionId;
7575
7678
  for (let depth = 0; depth < 5 && current; depth++) {
@@ -7595,9 +7698,6 @@ function resolveSessionPreferredModel(db, sessionId, modelMap) {
7595
7698
  }
7596
7699
 
7597
7700
  // src/adapters/qoder-cn.ts
7598
- import { readdir as readdir7, readFile as readFile10, stat as stat8 } from "node:fs/promises";
7599
- import os8 from "node:os";
7600
- import path17 from "node:path";
7601
7701
  function parseQoderCnPaths(filePath) {
7602
7702
  const parts = filePath.split(path17.sep);
7603
7703
  const subagentsIdx = parts.lastIndexOf("subagents");
@@ -7715,7 +7815,8 @@ async function loadQoderCnSegmentModelCalls(filePath, isSubagentSession, modelMa
7715
7815
  async function parseQoderCnSessionFile(filePath, options) {
7716
7816
  const text = await readFile10(filePath, "utf8");
7717
7817
  const lines = text.split("\n").filter(Boolean);
7718
- const { configDir: configDir2 } = parseQoderCnPaths(filePath);
7818
+ const parsedPaths = parseQoderCnPaths(filePath);
7819
+ const { configDir: configDir2 } = parsedPaths;
7719
7820
  const projectContext = await qoderCnProjectContextFromLines(filePath, lines, options, configDir2);
7720
7821
  const pendingTools = /* @__PURE__ */ new Map();
7721
7822
  const seenUsageKeys = /* @__PURE__ */ new Set();
@@ -8043,6 +8144,16 @@ async function parseQoderCnSessionFile(filePath, options) {
8043
8144
  });
8044
8145
  }
8045
8146
  }
8147
+ dbModelCalls ??= await loadQoderDbModelCalls("QoderCN", parsedPaths.sessionId, modelMap);
8148
+ if (dbModelCalls.rootSessionId) {
8149
+ const parentPath = path17.join(path17.dirname(filePath), `${dbModelCalls.rootSessionId}.jsonl`);
8150
+ const parentSourcePathHash = `sha256:${createStableHash(parentPath)}`;
8151
+ return validEvents.map((event) => rebuildEventIdentity2({
8152
+ ...event,
8153
+ sessionId: dbModelCalls.rootSessionId,
8154
+ refs: { ...event.refs, sourcePathHash: parentSourcePathHash }
8155
+ }));
8156
+ }
8046
8157
  return validEvents;
8047
8158
  }
8048
8159
  function baseQoderCnEvent(event) {
@@ -8530,7 +8641,8 @@ async function loadQoderSegmentModelCalls(filePath, isSubagentSession, modelMap)
8530
8641
  async function parseQoderSessionFile(filePath, options) {
8531
8642
  const text = await readFile11(filePath, "utf8");
8532
8643
  const lines = text.split("\n").filter(Boolean);
8533
- const { configDir: configDir2 } = parseQoderPaths(filePath);
8644
+ const parsedPaths = parseQoderPaths(filePath);
8645
+ const { configDir: configDir2 } = parsedPaths;
8534
8646
  const projectContext = await qoderProjectContextFromLines(filePath, lines, options, configDir2);
8535
8647
  const pendingTools = /* @__PURE__ */ new Map();
8536
8648
  const seenUsageKeys = /* @__PURE__ */ new Set();
@@ -8858,6 +8970,16 @@ async function parseQoderSessionFile(filePath, options) {
8858
8970
  });
8859
8971
  }
8860
8972
  }
8973
+ dbModelCalls ??= await loadQoderDbModelCalls("Qoder", parsedPaths.sessionId, modelMap);
8974
+ if (dbModelCalls.rootSessionId) {
8975
+ const parentPath = path18.join(path18.dirname(filePath), `${dbModelCalls.rootSessionId}.jsonl`);
8976
+ const parentSourcePathHash = `sha256:${createStableHash(parentPath)}`;
8977
+ return validEvents.map((event) => rebuildEventIdentity3({
8978
+ ...event,
8979
+ sessionId: dbModelCalls.rootSessionId,
8980
+ refs: { ...event.refs, sourcePathHash: parentSourcePathHash }
8981
+ }));
8982
+ }
8861
8983
  return validEvents;
8862
8984
  }
8863
8985
  function baseQoderEvent(event) {
@@ -9893,7 +10015,7 @@ function modelMetrics(row) {
9893
10015
  toolCalls: numberField(row, "tool_call_count")
9894
10016
  };
9895
10017
  }
9896
- function resolveRootSessionId(sessionId, sessions) {
10018
+ function resolveRootSessionId2(sessionId, sessions) {
9897
10019
  let current = sessionId;
9898
10020
  const visited = /* @__PURE__ */ new Set();
9899
10021
  while (true) {
@@ -9915,7 +10037,7 @@ function sessionContext(row, sessions) {
9915
10037
  const session = sessions.get(sessionId);
9916
10038
  const cwd = stringField(session, "directory");
9917
10039
  const project = projectFromDirectory(cwd);
9918
- const rootSessionId = resolveRootSessionId(sessionId, sessions);
10040
+ const rootSessionId = resolveRootSessionId2(sessionId, sessions);
9919
10041
  return {
9920
10042
  rawSessionId: sessionId,
9921
10043
  sessionId: `zcode:${rootSessionId}`,
@@ -11289,9 +11411,10 @@ async function postRollupBatch(remote, rollups, options = {}) {
11289
11411
  failed: 0
11290
11412
  };
11291
11413
  }
11292
- async function deleteRollupsBySource(remote, source, machine) {
11414
+ async function deleteRollupsBySource(remote, source, machine, options = {}) {
11415
+ const query = `source=${encodeURIComponent(source)}${options.preserveTokens ? "&preserveTokens=1" : ""}`;
11293
11416
  const response = await remote.fetchImpl(
11294
- joinUrl(remote.baseUrl, `/v3/agent/sessions?source=${encodeURIComponent(source)}`),
11417
+ joinUrl(remote.baseUrl, `/v3/agent/sessions?${query}`),
11295
11418
  { method: "DELETE", headers: buildHeaders(remote.token, machine) }
11296
11419
  );
11297
11420
  if (!response.ok) {
@@ -11414,7 +11537,7 @@ function createCli(ctx, registry) {
11414
11537
  cli.command("hook", "Read agent hook JSON from stdin and report a throttled event").option("--agent <name>", "Agent name").option("--project <name>", "Project name").option("--min-interval <seconds>", "Minimum seconds between similar hook reports").action((options) => hookCommand(normalizeOptions(options), ctx));
11415
11538
  cli.command("sync-local-trigger", "Trigger one background local sync with throttle and locking").option("--min-interval <seconds>", "Minimum seconds between sync triggers").action((options) => syncLocalTriggerCommand(normalizeOptions(options), ctx, registry));
11416
11539
  cli.command("sync-local-runner", "Internal background local sync runner").option("--lock-file <path>", "Lock file for the active sync").option("--state-file <path>", "State file for trigger metadata").action((options) => syncLocalRunnerCommand(normalizeOptions(options), ctx, registry));
11417
- cli.command("backfill [action]", "Inspect local history import candidates").option("--source <source>", "Backfill source").option("--since <time>", "Only include history after this time").option("--until <time>", "Only include history before this time").option("--project <name>", "Project filter").option("--source-root <path>", "Override source history root").option("--include-source-path", "Include local source paths in output").option("--import-run <id>", "Import run id for verify/resume workflows").option("--limit <count>", "Maximum session files to parse").option("--batch-size <count>", "Max rollups per request (also bounded by --batch-bytes)").option("--batch-bytes <bytes>", "Soft byte cap for the JSON body of a single ingest POST").option("--replace", "Replace conflicting records during import (default)").option("--skip-conflicts", "Skip conflicting records instead of replacing them").option("--force", "Force full re-import: clear watermark and re-process all files").action((action, options) => backfillCommand({ ...normalizeOptions(options), action }, ctx, registry));
11540
+ cli.command("backfill [action]", "Inspect local history import candidates").option("--source <source>", "Backfill source").option("--since <time>", "Only include history after this time").option("--until <time>", "Only include history before this time").option("--project <name>", "Project filter").option("--source-root <path>", "Override source history root").option("--include-source-path", "Include local source paths in output").option("--import-run <id>", "Import run id for verify/resume workflows").option("--limit <count>", "Maximum session files to parse").option("--batch-size <count>", "Max rollups per request (also bounded by --batch-bytes)").option("--batch-bytes <bytes>", "Soft byte cap for the JSON body of a single ingest POST").option("--replace", "Replace conflicting records during import (default)").option("--skip-conflicts", "Skip conflicting records instead of replacing them").option("--force", "Force full re-import: clear watermark and re-process all files (keeps server rollups that already have tokens)").option("--purge-all", "With --force: also delete token-bearing server rollups before re-importing (DANGEROUS \u2014 tokens whose local sources were rotated away are lost forever)").action((action, options) => backfillCommand({ ...normalizeOptions(options), action }, ctx, registry));
11418
11541
  cli.command("token [action] [value]", "Set, show, or clear the persisted API token").option("--remote <url>", "Override API base URL when setting a token").action((action, value, options) => tokenCommand(action, value, normalizeOptions(options), ctx));
11419
11542
  cli.command("machine [action]", "List or rename machines (requires login)").option("--name <name>", "New display name (used by `machine rename`)").option("--id <id>", "Machine id (defaults to current machine)").action((action, options) => machineCommand(action, normalizeOptions(options), ctx));
11420
11543
  return cli;
@@ -11433,7 +11556,8 @@ function normalizeOptions(options) {
11433
11556
  importRun: "import-run",
11434
11557
  batchSize: "batch-size",
11435
11558
  batchBytes: "batch-bytes",
11436
- skipConflicts: "skip-conflicts"
11559
+ skipConflicts: "skip-conflicts",
11560
+ purgeAll: "purge-all"
11437
11561
  };
11438
11562
  for (const [camel, dashed] of Object.entries(aliases)) {
11439
11563
  if (normalized[camel] !== void 0 && normalized[dashed] === void 0) {
@@ -12005,11 +12129,13 @@ async function purgeForcedSources(sourceDefs, home, remoteKey, options, ctx) {
12005
12129
  debug(ctx, `Failed to clear backfill watermark: ${error.message}
12006
12130
  `);
12007
12131
  }
12132
+ const preserveTokens = !options["purge-all"];
12008
12133
  for (const item of sourceDefs) {
12009
12134
  try {
12010
- const deleted = await deleteSessionRollupsBySourceAPI(item.id, options, ctx);
12135
+ const deleted = await deleteSessionRollupsBySourceAPI(item.id, options, ctx, preserveTokens);
12011
12136
  if (!options.json) {
12012
- write(ctx.stdout, `purged ${item.id}: ${deleted} old rollups
12137
+ const suffix = preserveTokens ? " (token-bearing rollups kept)" : "";
12138
+ write(ctx.stdout, `purged ${item.id}: ${deleted} old rollups${suffix}
12013
12139
  `);
12014
12140
  }
12015
12141
  } catch (error) {
@@ -12139,7 +12265,7 @@ async function sendSessionRollupBatch(rollups, options, ctx) {
12139
12265
  });
12140
12266
  return result;
12141
12267
  }
12142
- async function deleteSessionRollupsBySourceAPI(source, options, ctx) {
12268
+ async function deleteSessionRollupsBySourceAPI(source, options, ctx, preserveTokens) {
12143
12269
  const remote = resolveRemoteFromOptions(options, ctx);
12144
12270
  if (!remote) {
12145
12271
  throw new Error("No fetch available for HTTP delete");
@@ -12149,7 +12275,7 @@ async function deleteSessionRollupsBySourceAPI(source, options, ctx) {
12149
12275
  id: ensureLocalMachineId(home),
12150
12276
  hostname: defaultMachineName(),
12151
12277
  platform: process.platform
12152
- });
12278
+ }, { preserveTokens });
12153
12279
  }
12154
12280
  function shouldUseIncrementalBackfill(options) {
12155
12281
  return !stringOption(options.since) && !stringOption(options.until) && !stringOption(options["source-root"]) && numberOption(options.limit) === void 0;
@@ -12608,7 +12734,7 @@ Usage:
12608
12734
  vibetime install [--target codex,claude,opencode,pi] [--all] [--dry-run] [--force] [--home <path>]
12609
12735
  vibetime upgrade [--check]
12610
12736
  vibetime hook --agent <name>
12611
- vibetime backfill discover|plan|import|verify --source codex|claude-code|opencode|pi|all --dry-run [--json] [--batch-size <count>]
12737
+ vibetime backfill discover|plan|import|verify --source codex|claude-code|opencode|pi|all --dry-run [--json] [--batch-size <count>] [--force [--purge-all]]
12612
12738
  vibetime token set <token>
12613
12739
  vibetime token show
12614
12740
  vibetime token clear
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yhong91/vibetime",
3
3
  "type": "module",
4
- "version": "0.1.40",
4
+ "version": "0.1.41",
5
5
  "description": "vibetime CLI — install AI-agent hooks (Claude Code, Codex, OpenCode, Pi) and report activity to vibetime.",
6
6
  "license": "MIT",
7
7
  "publishConfig": {