@yhong91/vibetime 0.1.39 → 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 +216 -43
  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.39" : "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
  ];
@@ -4565,48 +4650,71 @@ function normalizeModelCandidate(value) {
4565
4650
  }
4566
4651
  return trimmed;
4567
4652
  }
4568
- function extractUsageFromGenerationSpan(span) {
4569
- const parsed = parseEmbeddedJson(span.toolOutput);
4570
- if (!Array.isArray(parsed) || parsed.length === 0) {
4571
- return void 0;
4572
- }
4573
- const first = parsed[0];
4574
- if (!isPlainObject(first)) {
4575
- return void 0;
4576
- }
4577
- const usage = objectField(first, "usage");
4578
- if (!isPlainObject(usage)) {
4653
+ function finitePositive(value) {
4654
+ if (typeof value !== "number" && typeof value !== "string") {
4579
4655
  return void 0;
4580
4656
  }
4657
+ const parsed = Number(value);
4658
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
4659
+ }
4660
+ function extractUsageFromGenerationSpan(span) {
4661
+ const parsed = parseEmbeddedJson(span.toolOutput);
4662
+ const first = Array.isArray(parsed) ? parsed[0] : void 0;
4663
+ const usage = isPlainObject(first) ? objectField(first, "usage") : {};
4581
4664
  const inputTokens = numberField(usage, "prompt_tokens");
4582
4665
  const outputTokens = numberField(usage, "completion_tokens");
4583
4666
  const totalTokens = numberField(usage, "total_tokens");
4584
4667
  const details = objectField(usage, "prompt_tokens_details");
4585
4668
  const cachedTokens = isPlainObject(details) ? numberField(details, "cached_tokens") : void 0;
4669
+ const otelAttributes = span.attributes || {};
4670
+ const otelInputTokens = finitePositive(otelAttributes["gen_ai.usage.input_tokens"]);
4671
+ const otelOutputTokens = finitePositive(otelAttributes["gen_ai.usage.output_tokens"]);
4672
+ const otelCacheReadTokens = finitePositive(otelAttributes["gen_ai.usage.cache_read_input_tokens"]);
4673
+ const otelCacheCreationTokens = finitePositive(otelAttributes["gen_ai.usage.cache_creation_input_tokens"]);
4674
+ const otelReasoningTokens = finitePositive(otelAttributes["gen_ai.usage.reasoning_output_tokens"]);
4675
+ const tokensInput = otelInputTokens ?? inputTokens;
4676
+ const tokensOutput = otelOutputTokens ?? outputTokens;
4677
+ const tokensCacheReadInput = otelCacheReadTokens ?? cachedTokens;
4678
+ const tokensCachedInput = (tokensCacheReadInput ?? 0) + (otelCacheCreationTokens ?? 0);
4679
+ const tokensTotal = totalTokens || (tokensInput ?? 0) + (tokensOutput ?? 0);
4680
+ if (!tokensInput && !tokensOutput && !tokensCachedInput && !otelReasoningTokens && !tokensTotal) {
4681
+ return void 0;
4682
+ }
4586
4683
  return {
4587
- tokensInput: inputTokens || void 0,
4588
- tokensCachedInput: cachedTokens || void 0,
4589
- tokensCacheReadInput: cachedTokens || void 0,
4590
- tokensOutput: outputTokens || void 0,
4591
- tokensTotal: totalTokens || void 0,
4684
+ tokensInput: tokensInput || void 0,
4685
+ tokensOutput: tokensOutput || void 0,
4686
+ tokensCachedInput: tokensCachedInput || void 0,
4687
+ tokensCacheReadInput: tokensCacheReadInput || void 0,
4688
+ tokensCacheCreationInput: otelCacheCreationTokens,
4689
+ tokensReasoningOutput: otelReasoningTokens,
4690
+ tokensTotal: tokensTotal || void 0,
4592
4691
  modelCalls: 1
4593
4692
  };
4594
4693
  }
4595
4694
  function modelUsageFromTrace(trace, generationIndex, totalGenerations) {
4596
4695
  const info = trace.modelInfo;
4597
- if (!info || !info.totalInputTokens && !info.totalOutputTokens && !trace.totalTokens) {
4696
+ const otelAttributes = trace.attributes || {};
4697
+ const otelInputTokens = finitePositive(otelAttributes["gen_ai.usage.input_tokens"]);
4698
+ const otelOutputTokens = finitePositive(otelAttributes["gen_ai.usage.output_tokens"]);
4699
+ const otelCacheReadTokens = finitePositive(otelAttributes["gen_ai.usage.cache_read_input_tokens"]);
4700
+ const otelCacheCreationTokens = finitePositive(otelAttributes["gen_ai.usage.cache_creation_input_tokens"]);
4701
+ const otelReasoningTokens = finitePositive(otelAttributes["gen_ai.usage.reasoning_output_tokens"]);
4702
+ if (!info && !trace.totalTokens && !otelInputTokens && !otelOutputTokens && !otelCacheReadTokens && !otelCacheCreationTokens && !otelReasoningTokens) {
4598
4703
  return void 0;
4599
4704
  }
4705
+ const inputTokens = otelInputTokens ?? (info?.totalInputTokens || 0);
4706
+ const outputTokens = otelOutputTokens ?? (info?.totalOutputTokens || 0);
4707
+ const cacheReadTokens = otelCacheReadTokens ?? info?.totalCachedTokens;
4708
+ const cachedTokens = (cacheReadTokens ?? 0) + (otelCacheCreationTokens ?? 0);
4709
+ const totalTokens = trace.totalTokens || inputTokens + outputTokens;
4600
4710
  if (generationIndex === 1) {
4601
- const inputTokens = info.totalInputTokens || 0;
4602
- const outputTokens = info.totalOutputTokens || 0;
4603
- const cachedTokens = info.totalCachedTokens || 0;
4604
- const totalTokens = trace.totalTokens || inputTokens + outputTokens;
4605
4711
  return {
4606
4712
  tokensInput: inputTokens || void 0,
4607
4713
  tokensCachedInput: cachedTokens || void 0,
4608
- tokensCacheReadInput: cachedTokens || void 0,
4714
+ tokensCacheReadInput: cacheReadTokens,
4715
+ tokensCacheCreationInput: otelCacheCreationTokens || void 0,
4609
4716
  tokensOutput: outputTokens || void 0,
4717
+ tokensReasoningOutput: otelReasoningTokens || void 0,
4610
4718
  tokensTotal: totalTokens || void 0,
4611
4719
  modelCalls: totalGenerations || 1
4612
4720
  };
@@ -7468,6 +7576,11 @@ function createPiAdapter() {
7468
7576
  };
7469
7577
  }
7470
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
+
7471
7584
  // src/adapters/qoder-local-db.ts
7472
7585
  import { access } from "node:fs/promises";
7473
7586
  import os7 from "node:os";
@@ -7514,6 +7627,7 @@ async function loadQoderDbModelCalls(appDirName, sessionId, modelMap) {
7514
7627
  }
7515
7628
  const db = new DatabaseSync(dbPath, { readOnly: true });
7516
7629
  try {
7630
+ calls.rootSessionId = resolveRootSessionId(db, sessionId);
7517
7631
  const preferredModel = resolveSessionPreferredModel(db, sessionId, modelMap);
7518
7632
  const rows = db.prepare(
7519
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`
@@ -7547,6 +7661,18 @@ async function loadQoderDbModelCalls(appDirName, sessionId, modelMap) {
7547
7661
  }
7548
7662
  return calls;
7549
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
+ }
7550
7676
  function resolveSessionPreferredModel(db, sessionId, modelMap) {
7551
7677
  let current = sessionId;
7552
7678
  for (let depth = 0; depth < 5 && current; depth++) {
@@ -7572,9 +7698,6 @@ function resolveSessionPreferredModel(db, sessionId, modelMap) {
7572
7698
  }
7573
7699
 
7574
7700
  // src/adapters/qoder-cn.ts
7575
- import { readdir as readdir7, readFile as readFile10, stat as stat8 } from "node:fs/promises";
7576
- import os8 from "node:os";
7577
- import path17 from "node:path";
7578
7701
  function parseQoderCnPaths(filePath) {
7579
7702
  const parts = filePath.split(path17.sep);
7580
7703
  const subagentsIdx = parts.lastIndexOf("subagents");
@@ -7617,9 +7740,8 @@ function rebuildEventIdentity2(event) {
7617
7740
  }
7618
7741
  };
7619
7742
  }
7620
- async function loadQoderCnModelNames(configDir2) {
7743
+ async function parseModelNamesFromDynamicTexts(dynamicTextsPath) {
7621
7744
  try {
7622
- const dynamicTextsPath = path17.join(configDir2, ".auth", "dynamic-texts.json");
7623
7745
  const content = await readFile10(dynamicTextsPath, "utf8");
7624
7746
  const json = JSON.parse(content);
7625
7747
  const texts = json.texts || {};
@@ -7635,6 +7757,19 @@ async function loadQoderCnModelNames(configDir2) {
7635
7757
  return {};
7636
7758
  }
7637
7759
  }
7760
+ async function loadQoderCnModelNames(configDir2) {
7761
+ const map = await parseModelNamesFromDynamicTexts(path17.join(configDir2, ".auth", "dynamic-texts.json"));
7762
+ const siblingConfigDir = configDir2.replace(/\.qoder-cn$/, ".qoder");
7763
+ if (siblingConfigDir !== configDir2) {
7764
+ const siblingMap = await parseModelNamesFromDynamicTexts(path17.join(siblingConfigDir, ".auth", "dynamic-texts.json"));
7765
+ for (const [key, val] of Object.entries(siblingMap)) {
7766
+ if (!(key in map)) {
7767
+ map[key] = val;
7768
+ }
7769
+ }
7770
+ }
7771
+ return map;
7772
+ }
7638
7773
  async function loadQoderCnSegmentModelCalls(filePath, isSubagentSession, modelMap) {
7639
7774
  const { configDir: configDir2, projectName, sessionId } = parseQoderCnPaths(filePath);
7640
7775
  const segmentsPath = path17.join(configDir2, "logs", "sessions", projectName, sessionId, "segments");
@@ -7680,7 +7815,8 @@ async function loadQoderCnSegmentModelCalls(filePath, isSubagentSession, modelMa
7680
7815
  async function parseQoderCnSessionFile(filePath, options) {
7681
7816
  const text = await readFile10(filePath, "utf8");
7682
7817
  const lines = text.split("\n").filter(Boolean);
7683
- const { configDir: configDir2 } = parseQoderCnPaths(filePath);
7818
+ const parsedPaths = parseQoderCnPaths(filePath);
7819
+ const { configDir: configDir2 } = parsedPaths;
7684
7820
  const projectContext = await qoderCnProjectContextFromLines(filePath, lines, options, configDir2);
7685
7821
  const pendingTools = /* @__PURE__ */ new Map();
7686
7822
  const seenUsageKeys = /* @__PURE__ */ new Set();
@@ -8008,6 +8144,16 @@ async function parseQoderCnSessionFile(filePath, options) {
8008
8144
  });
8009
8145
  }
8010
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
+ }
8011
8157
  return validEvents;
8012
8158
  }
8013
8159
  function baseQoderCnEvent(event) {
@@ -8420,9 +8566,8 @@ function rebuildEventIdentity3(event) {
8420
8566
  }
8421
8567
  };
8422
8568
  }
8423
- async function loadQoderModelNames(configDir2) {
8569
+ async function parseModelNamesFromDynamicTexts2(dynamicTextsPath) {
8424
8570
  try {
8425
- const dynamicTextsPath = path18.join(configDir2, ".auth", "dynamic-texts.json");
8426
8571
  const content = await readFile11(dynamicTextsPath, "utf8");
8427
8572
  const json = JSON.parse(content);
8428
8573
  const texts = json.texts || {};
@@ -8438,6 +8583,19 @@ async function loadQoderModelNames(configDir2) {
8438
8583
  return {};
8439
8584
  }
8440
8585
  }
8586
+ async function loadQoderModelNames(configDir2) {
8587
+ const map = await parseModelNamesFromDynamicTexts2(path18.join(configDir2, ".auth", "dynamic-texts.json"));
8588
+ const siblingConfigDir = configDir2.replace(/\.qoder$/, ".qoder-cn");
8589
+ if (siblingConfigDir !== configDir2) {
8590
+ const siblingMap = await parseModelNamesFromDynamicTexts2(path18.join(siblingConfigDir, ".auth", "dynamic-texts.json"));
8591
+ for (const [key, val] of Object.entries(siblingMap)) {
8592
+ if (!(key in map)) {
8593
+ map[key] = val;
8594
+ }
8595
+ }
8596
+ }
8597
+ return map;
8598
+ }
8441
8599
  async function loadQoderSegmentModelCalls(filePath, isSubagentSession, modelMap) {
8442
8600
  const { configDir: configDir2, projectName, sessionId } = parseQoderPaths(filePath);
8443
8601
  const segmentsPath = path18.join(configDir2, "logs", "sessions", projectName, sessionId, "segments");
@@ -8483,7 +8641,8 @@ async function loadQoderSegmentModelCalls(filePath, isSubagentSession, modelMap)
8483
8641
  async function parseQoderSessionFile(filePath, options) {
8484
8642
  const text = await readFile11(filePath, "utf8");
8485
8643
  const lines = text.split("\n").filter(Boolean);
8486
- const { configDir: configDir2 } = parseQoderPaths(filePath);
8644
+ const parsedPaths = parseQoderPaths(filePath);
8645
+ const { configDir: configDir2 } = parsedPaths;
8487
8646
  const projectContext = await qoderProjectContextFromLines(filePath, lines, options, configDir2);
8488
8647
  const pendingTools = /* @__PURE__ */ new Map();
8489
8648
  const seenUsageKeys = /* @__PURE__ */ new Set();
@@ -8811,6 +8970,16 @@ async function parseQoderSessionFile(filePath, options) {
8811
8970
  });
8812
8971
  }
8813
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
+ }
8814
8983
  return validEvents;
8815
8984
  }
8816
8985
  function baseQoderEvent(event) {
@@ -9846,7 +10015,7 @@ function modelMetrics(row) {
9846
10015
  toolCalls: numberField(row, "tool_call_count")
9847
10016
  };
9848
10017
  }
9849
- function resolveRootSessionId(sessionId, sessions) {
10018
+ function resolveRootSessionId2(sessionId, sessions) {
9850
10019
  let current = sessionId;
9851
10020
  const visited = /* @__PURE__ */ new Set();
9852
10021
  while (true) {
@@ -9868,7 +10037,7 @@ function sessionContext(row, sessions) {
9868
10037
  const session = sessions.get(sessionId);
9869
10038
  const cwd = stringField(session, "directory");
9870
10039
  const project = projectFromDirectory(cwd);
9871
- const rootSessionId = resolveRootSessionId(sessionId, sessions);
10040
+ const rootSessionId = resolveRootSessionId2(sessionId, sessions);
9872
10041
  return {
9873
10042
  rawSessionId: sessionId,
9874
10043
  sessionId: `zcode:${rootSessionId}`,
@@ -11242,9 +11411,10 @@ async function postRollupBatch(remote, rollups, options = {}) {
11242
11411
  failed: 0
11243
11412
  };
11244
11413
  }
11245
- async function deleteRollupsBySource(remote, source, machine) {
11414
+ async function deleteRollupsBySource(remote, source, machine, options = {}) {
11415
+ const query = `source=${encodeURIComponent(source)}${options.preserveTokens ? "&preserveTokens=1" : ""}`;
11246
11416
  const response = await remote.fetchImpl(
11247
- joinUrl(remote.baseUrl, `/v3/agent/sessions?source=${encodeURIComponent(source)}`),
11417
+ joinUrl(remote.baseUrl, `/v3/agent/sessions?${query}`),
11248
11418
  { method: "DELETE", headers: buildHeaders(remote.token, machine) }
11249
11419
  );
11250
11420
  if (!response.ok) {
@@ -11367,7 +11537,7 @@ function createCli(ctx, registry) {
11367
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));
11368
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));
11369
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));
11370
- 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));
11371
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));
11372
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));
11373
11543
  return cli;
@@ -11386,7 +11556,8 @@ function normalizeOptions(options) {
11386
11556
  importRun: "import-run",
11387
11557
  batchSize: "batch-size",
11388
11558
  batchBytes: "batch-bytes",
11389
- skipConflicts: "skip-conflicts"
11559
+ skipConflicts: "skip-conflicts",
11560
+ purgeAll: "purge-all"
11390
11561
  };
11391
11562
  for (const [camel, dashed] of Object.entries(aliases)) {
11392
11563
  if (normalized[camel] !== void 0 && normalized[dashed] === void 0) {
@@ -11958,11 +12129,13 @@ async function purgeForcedSources(sourceDefs, home, remoteKey, options, ctx) {
11958
12129
  debug(ctx, `Failed to clear backfill watermark: ${error.message}
11959
12130
  `);
11960
12131
  }
12132
+ const preserveTokens = !options["purge-all"];
11961
12133
  for (const item of sourceDefs) {
11962
12134
  try {
11963
- const deleted = await deleteSessionRollupsBySourceAPI(item.id, options, ctx);
12135
+ const deleted = await deleteSessionRollupsBySourceAPI(item.id, options, ctx, preserveTokens);
11964
12136
  if (!options.json) {
11965
- 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}
11966
12139
  `);
11967
12140
  }
11968
12141
  } catch (error) {
@@ -12092,7 +12265,7 @@ async function sendSessionRollupBatch(rollups, options, ctx) {
12092
12265
  });
12093
12266
  return result;
12094
12267
  }
12095
- async function deleteSessionRollupsBySourceAPI(source, options, ctx) {
12268
+ async function deleteSessionRollupsBySourceAPI(source, options, ctx, preserveTokens) {
12096
12269
  const remote = resolveRemoteFromOptions(options, ctx);
12097
12270
  if (!remote) {
12098
12271
  throw new Error("No fetch available for HTTP delete");
@@ -12102,7 +12275,7 @@ async function deleteSessionRollupsBySourceAPI(source, options, ctx) {
12102
12275
  id: ensureLocalMachineId(home),
12103
12276
  hostname: defaultMachineName(),
12104
12277
  platform: process.platform
12105
- });
12278
+ }, { preserveTokens });
12106
12279
  }
12107
12280
  function shouldUseIncrementalBackfill(options) {
12108
12281
  return !stringOption(options.since) && !stringOption(options.until) && !stringOption(options["source-root"]) && numberOption(options.limit) === void 0;
@@ -12561,7 +12734,7 @@ Usage:
12561
12734
  vibetime install [--target codex,claude,opencode,pi] [--all] [--dry-run] [--force] [--home <path>]
12562
12735
  vibetime upgrade [--check]
12563
12736
  vibetime hook --agent <name>
12564
- 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]]
12565
12738
  vibetime token set <token>
12566
12739
  vibetime token show
12567
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.39",
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": {