@yhong91/vibetime 0.1.52 → 0.1.54

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 +633 -59
  2. package/package.json +1 -1
package/bin/vibetime.mjs CHANGED
@@ -18,7 +18,7 @@ var __export = (target, all) => {
18
18
  // src/lib/fs.ts
19
19
  var fs_exports = {};
20
20
  __export(fs_exports, {
21
- GENERATED_MARKER: () => GENERATED_MARKER,
21
+ GENERATED_MARKER: () => GENERATED_MARKER2,
22
22
  countDirectoryEntries: () => countDirectoryEntries,
23
23
  listFilesByExtensions: () => listFilesByExtensions,
24
24
  listJsonlFiles: () => listJsonlFiles,
@@ -72,7 +72,7 @@ async function writeGeneratedFile(filePath, content, { dryRun, force, onWrite })
72
72
  onWrite(`Already installed ${filePath}`);
73
73
  return;
74
74
  }
75
- if (existing !== null && !existing.includes(GENERATED_MARKER) && !existing.includes(LEGACY_GENERATED_MARKER) && !force) {
75
+ if (existing !== null && !existing.includes(GENERATED_MARKER2) && !existing.includes(LEGACY_GENERATED_MARKER) && !force) {
76
76
  throw new Error(
77
77
  `Refusing to overwrite non-vibetime file: ${filePath}. Re-run with --force if this is intentional.`
78
78
  );
@@ -148,11 +148,11 @@ async function countDirectoryEntries(candidatePath) {
148
148
  throw error;
149
149
  }
150
150
  }
151
- var GENERATED_MARKER, LEGACY_GENERATED_MARKER;
151
+ var GENERATED_MARKER2, LEGACY_GENERATED_MARKER;
152
152
  var init_fs = __esm({
153
153
  "src/lib/fs.ts"() {
154
154
  "use strict";
155
- GENERATED_MARKER = "Generated by vibetime.";
155
+ GENERATED_MARKER2 = "Generated by vibetime.";
156
156
  LEGACY_GENERATED_MARKER = "Generated by codetime.";
157
157
  }
158
158
  });
@@ -884,8 +884,8 @@ var init_esm = __esm({
884
884
 
885
885
  // src/cli.ts
886
886
  import { spawn as spawn2, spawnSync } from "node:child_process";
887
- import { mkdir as mkdir5, open, rm, stat as stat13, writeFile as writeFile4 } from "node:fs/promises";
888
- import os11 from "node:os";
887
+ import { mkdir as mkdir5, open, rm, stat as stat14, writeFile as writeFile4 } from "node:fs/promises";
888
+ import os12 from "node:os";
889
889
  import path25 from "node:path";
890
890
  import { fileURLToPath } from "node:url";
891
891
 
@@ -2047,7 +2047,8 @@ function claudeStyleFileMetrics(tool, input) {
2047
2047
  }
2048
2048
 
2049
2049
  // src/lib/constants.ts
2050
- var PACKAGE_VERSION = true ? "0.1.52" : "0.1.1";
2050
+ var PACKAGE_VERSION = true ? "0.1.54" : "0.1.1";
2051
+ var GENERATED_MARKER = "Generated by vibetime.";
2051
2052
  var DEFAULT_API_URL = "http://121.196.224.82:3001";
2052
2053
  var DEFAULT_BACKFILL_BATCH_SIZE = 50;
2053
2054
  var DEFAULT_BACKFILL_BATCH_BYTES = 800 * 1024;
@@ -3330,6 +3331,9 @@ function isTurnIdle(lastEventAt) {
3330
3331
  }
3331
3332
  return Date.now() - lastMs > TURN_IDLE_MS;
3332
3333
  }
3334
+ function withoutSubagentTurnEvents(events) {
3335
+ return events.filter((event) => event.type !== "turn.started" && event.type !== "turn.completed");
3336
+ }
3333
3337
  function sessionIdFromFilePath(filePath, prefix) {
3334
3338
  const match = path6.basename(filePath).match(/([0-9a-f]{8}-[0-9a-f-]{27,})/);
3335
3339
  return match?.[1] || `${prefix}_${createStableHash(filePath).slice(0, 24)}`;
@@ -4252,7 +4256,8 @@ async function parseClaudeCoworkSessionFile(filePath, options) {
4252
4256
  const parentSourcePathHash = subagentInfo ? `sha256:${createStableHash(subagentInfo.mainTranscriptPath)}` : void 0;
4253
4257
  const parentSessionId = subagentInfo ? prefixCoworkId(subagentInfo.cliSessionId) : void 0;
4254
4258
  const events = await parser(filePath, options);
4255
- return events.map((event) => {
4259
+ const foldable = subagentInfo ? withoutSubagentTurnEvents(events).map((event) => ({ ...event, turnId: void 0 })) : events;
4260
+ return foldable.map((event) => {
4256
4261
  const sessionId = parentSessionId || prefixCoworkId(event.sessionId);
4257
4262
  const turnId = prefixCoworkId(event.turnId);
4258
4263
  const workspaceId = createWorkspaceId({ projectName: project, repoRoot: cwd });
@@ -5006,8 +5011,8 @@ async function codebuddyBackfillFiles(sourceRoot, home, env) {
5006
5011
  }
5007
5012
  const filePath = path9.join(traceDir, entry);
5008
5013
  try {
5009
- const stat14 = await import("node:fs/promises").then((fs) => fs.stat(filePath));
5010
- files.push({ path: filePath, modifiedAt: stat14.mtime.toISOString(), groupId: pidDir.name });
5014
+ const stat15 = await import("node:fs/promises").then((fs) => fs.stat(filePath));
5015
+ files.push({ path: filePath, modifiedAt: stat15.mtime.toISOString(), groupId: pidDir.name });
5011
5016
  } catch {
5012
5017
  }
5013
5018
  }
@@ -6938,6 +6943,154 @@ function createGrokBuildAdapter() {
6938
6943
  // src/adapters/kimi-code.ts
6939
6944
  import { readFile as readFile9 } from "node:fs/promises";
6940
6945
  import path14 from "node:path";
6946
+
6947
+ // src/lib/toml-hooks.ts
6948
+ init_fs();
6949
+ async function hasTomlHookCommand(filePath, command) {
6950
+ const text = await readTextIfExists(filePath);
6951
+ if (!text) {
6952
+ return false;
6953
+ }
6954
+ return parseTomlHookRules(text).some((rule) => rule.command === command);
6955
+ }
6956
+ function parseTomlHookRules(text) {
6957
+ const rules = [];
6958
+ const chunks = text.split(/^\[\[hooks\]\][ \t]*\r?\n?/m);
6959
+ for (const chunk of chunks.slice(1)) {
6960
+ const body = chunk.split(/^\[[^\]]+\]/m)[0] ?? "";
6961
+ const event = tomlStringField(body, "event");
6962
+ const command = tomlStringField(body, "command");
6963
+ if (!event || !command) {
6964
+ continue;
6965
+ }
6966
+ const matcher = tomlStringField(body, "matcher");
6967
+ const timeout = tomlNumberField(body, "timeout");
6968
+ const rule = { event, command };
6969
+ if (matcher !== void 0) {
6970
+ rule.matcher = matcher;
6971
+ }
6972
+ if (timeout !== void 0) {
6973
+ rule.timeout = timeout;
6974
+ }
6975
+ rules.push(rule);
6976
+ }
6977
+ return rules;
6978
+ }
6979
+ function mergeTomlHookRules(existingText, desired) {
6980
+ const existingKeys = new Set(parseTomlHookRules(existingText).map(ruleKey));
6981
+ const toAppend = desired.filter((rule) => !existingKeys.has(ruleKey(rule)));
6982
+ if (toAppend.length === 0) {
6983
+ return existingText;
6984
+ }
6985
+ let base = existingText;
6986
+ if (base.length > 0 && !base.endsWith("\n")) {
6987
+ base += "\n";
6988
+ }
6989
+ if (base.length > 0 && !base.endsWith("\n\n") && base.trim().length > 0) {
6990
+ base += "\n";
6991
+ }
6992
+ const blocks = toAppend.map(serializeTomlHookRule).join("\n\n");
6993
+ return `${base}${blocks}
6994
+ `;
6995
+ }
6996
+ function removeTomlHookRulesByCommand(existingText, commands) {
6997
+ const commandSet = new Set(commands.filter(Boolean));
6998
+ if (commandSet.size === 0 || !existingText) {
6999
+ return existingText;
7000
+ }
7001
+ const parts = [];
7002
+ let cursor = 0;
7003
+ const headerRe = /^\[\[hooks\]\][ \t]*\r?\n?/gm;
7004
+ let match = headerRe.exec(existingText);
7005
+ let removed = 0;
7006
+ while (match) {
7007
+ const headerStart = match.index;
7008
+ const bodyStart = headerRe.lastIndex;
7009
+ const rest = existingText.slice(bodyStart);
7010
+ const nextHeader = rest.search(/^\[[^\]]+\]/m);
7011
+ const bodyEnd = nextHeader === -1 ? existingText.length : bodyStart + nextHeader;
7012
+ const body = existingText.slice(bodyStart, bodyEnd);
7013
+ const command = tomlStringField(body, "command");
7014
+ parts.push(existingText.slice(cursor, headerStart));
7015
+ if (command && commandSet.has(command)) {
7016
+ removed += 1;
7017
+ } else {
7018
+ parts.push(existingText.slice(headerStart, bodyEnd));
7019
+ }
7020
+ cursor = bodyEnd;
7021
+ match = headerRe.exec(existingText);
7022
+ }
7023
+ if (removed === 0) {
7024
+ return existingText;
7025
+ }
7026
+ parts.push(existingText.slice(cursor));
7027
+ let next = parts.join("");
7028
+ next = next.replace(/\n{3,}/g, "\n\n");
7029
+ if (next.length > 0 && !next.endsWith("\n")) {
7030
+ next += "\n";
7031
+ }
7032
+ return next;
7033
+ }
7034
+ function looksLikeToml(text) {
7035
+ const sample = text.trim();
7036
+ if (!sample) {
7037
+ return true;
7038
+ }
7039
+ if (sample.startsWith("{")) {
7040
+ return false;
7041
+ }
7042
+ if (/^\s*\[\s*\{/.test(sample)) {
7043
+ return false;
7044
+ }
7045
+ return true;
7046
+ }
7047
+ function ruleKey(rule) {
7048
+ return `${rule.event}\0${rule.command}`;
7049
+ }
7050
+ function serializeTomlHookRule(rule) {
7051
+ const lines = [
7052
+ "[[hooks]]",
7053
+ `event = "${escapeTomlString(rule.event)}"`
7054
+ ];
7055
+ if (rule.matcher !== void 0 && rule.matcher !== "") {
7056
+ lines.push(`matcher = "${escapeTomlString(rule.matcher)}"`);
7057
+ }
7058
+ lines.push(`command = "${escapeTomlString(rule.command)}"`);
7059
+ if (rule.timeout !== void 0) {
7060
+ lines.push(`timeout = ${Math.floor(rule.timeout)}`);
7061
+ }
7062
+ return lines.join("\n");
7063
+ }
7064
+ function escapeTomlString(value) {
7065
+ return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("\n", "\\n").replaceAll(" ", "\\t");
7066
+ }
7067
+ function tomlStringField(body, key) {
7068
+ const re = new RegExp(
7069
+ `^\\s*${key}\\s*=\\s*(?:"((?:\\\\.|[^"\\\\])*)"|'([^']*)')\\s*$`,
7070
+ "m"
7071
+ );
7072
+ const match = body.match(re);
7073
+ if (!match) {
7074
+ return void 0;
7075
+ }
7076
+ if (match[1] !== void 0) {
7077
+ return match[1].replaceAll("\\n", "\n").replaceAll("\\t", " ").replaceAll('\\"', '"').replaceAll("\\\\", "\\");
7078
+ }
7079
+ return match[2];
7080
+ }
7081
+ function tomlNumberField(body, key) {
7082
+ const re = new RegExp(`^\\s*${key}\\s*=\\s*(-?\\d+)\\s*$`, "m");
7083
+ const match = body.match(re);
7084
+ if (!match?.[1]) {
7085
+ return void 0;
7086
+ }
7087
+ const n = Number.parseInt(match[1], 10);
7088
+ return Number.isFinite(n) ? n : void 0;
7089
+ }
7090
+
7091
+ // src/adapters/kimi-code.ts
7092
+ var HOOK_COMMAND2 = "vibetime hook --agent kimi-code";
7093
+ var HOOK_TIMEOUT_SECONDS = 10;
6941
7094
  function kimiCodeHome(home, env) {
6942
7095
  const override = env?.KIMI_CODE_HOME || env?.KIMI_HOME;
6943
7096
  if (override && override.trim()) {
@@ -6948,6 +7101,28 @@ function kimiCodeHome(home, env) {
6948
7101
  function kimiCodeSessionsDir(home, env) {
6949
7102
  return path14.join(kimiCodeHome(home, env), "sessions");
6950
7103
  }
7104
+ function kimiCodeConfigPath(home, env) {
7105
+ return path14.join(kimiCodeHome(home, env), "config.toml");
7106
+ }
7107
+ function kimiHookRules() {
7108
+ const events = [
7109
+ "SessionStart",
7110
+ "SessionEnd",
7111
+ "UserPromptSubmit",
7112
+ "TurnStarted",
7113
+ "PostToolUse",
7114
+ "PostToolUseFailure",
7115
+ "Stop",
7116
+ "StopFailure",
7117
+ "PermissionResult",
7118
+ "SubagentStop"
7119
+ ];
7120
+ return events.map((event) => ({
7121
+ event,
7122
+ command: HOOK_COMMAND2,
7123
+ timeout: HOOK_TIMEOUT_SECONDS
7124
+ }));
7125
+ }
6951
7126
  function baseKimiEvent(event) {
6952
7127
  return {
6953
7128
  schemaVersion: AGENT_TIME_SCHEMA_VERSION,
@@ -7387,13 +7562,17 @@ function createKimiCodeAdapter() {
7387
7562
  return kimiCodeHome(home, env);
7388
7563
  },
7389
7564
  installedPath(home, env) {
7390
- return path14.join(kimiCodeHome(home, env), "vibetime-marker");
7565
+ return kimiCodeConfigPath(home, env);
7391
7566
  },
7392
- async isInstalled() {
7393
- return false;
7567
+ async isInstalled(home, env) {
7568
+ return hasTomlHookCommand(kimiCodeConfigPath(home, env), HOOK_COMMAND2);
7394
7569
  },
7395
- installEntries(_home, _env) {
7396
- return [];
7570
+ installEntries(home, env) {
7571
+ return [{
7572
+ kind: "hooks-toml",
7573
+ path: kimiCodeConfigPath(home, env),
7574
+ content: { hooks: kimiHookRules() }
7575
+ }];
7397
7576
  },
7398
7577
  sourcePaths(home, env) {
7399
7578
  return [kimiCodeSessionsDir(home, env)];
@@ -7460,6 +7639,8 @@ async function parseOpenCodeSessionFile(dbPath, options) {
7460
7639
  for (const session of sessions) {
7461
7640
  const rawSessionId = session.id;
7462
7641
  const sessionId = rootIdByRawId.get(rawSessionId) || rawSessionId;
7642
+ const isSubagent = sessionId !== rawSessionId;
7643
+ const sessionEventStart = events.length;
7463
7644
  const cwd = session.directory || session.path || void 0;
7464
7645
  const project = cwd ? path15.basename(cwd) : void 0;
7465
7646
  const sessionTs = msToIso(session.time_created);
@@ -7776,6 +7957,9 @@ async function parseOpenCodeSessionFile(dbPath, options) {
7776
7957
  operation: "session end"
7777
7958
  }));
7778
7959
  }
7960
+ if (isSubagent) {
7961
+ events.push(...withoutSubagentTurnEvents(events.splice(sessionEventStart)).map((event) => ({ ...event, turnId: void 0 })));
7962
+ }
7779
7963
  }
7780
7964
  } finally {
7781
7965
  db.close();
@@ -7854,19 +8038,19 @@ function opencodeDataCandidates(home, env) {
7854
8038
  return [primary, path15.join(home, ".opencode", "opencode.db")];
7855
8039
  }
7856
8040
  async function opencodeBackfillFiles(sourceRoot, home = os6.homedir(), env) {
7857
- const { stat: stat14 } = await import("node:fs/promises");
8041
+ const { stat: stat15 } = await import("node:fs/promises");
7858
8042
  if (sourceRoot) {
7859
8043
  if (!sourceRoot.endsWith(".db")) {
7860
8044
  return [];
7861
8045
  }
7862
- const info = await stat14(sourceRoot).catch(() => null);
8046
+ const info = await stat15(sourceRoot).catch(() => null);
7863
8047
  if (!info) {
7864
8048
  return [];
7865
8049
  }
7866
8050
  return [{ path: sourceRoot, modifiedAt: info.mtime.toISOString() }];
7867
8051
  }
7868
8052
  for (const candidatePath of opencodeDataCandidates(home, env)) {
7869
- const info = await stat14(candidatePath).catch(() => null);
8053
+ const info = await stat15(candidatePath).catch(() => null);
7870
8054
  if (info) {
7871
8055
  return [{ path: candidatePath, modifiedAt: info.mtime.toISOString() }];
7872
8056
  }
@@ -7966,8 +8150,10 @@ function createOpenCodeAdapter() {
7966
8150
  }
7967
8151
 
7968
8152
  // src/adapters/pi.ts
7969
- import { readFile as readFile10 } from "node:fs/promises";
8153
+ import { readdir as readdir7, readFile as readFile10, stat as stat8 } from "node:fs/promises";
8154
+ import os7 from "node:os";
7970
8155
  import path16 from "node:path";
8156
+ init_fs();
7971
8157
  function parsePiSubagentLink(filePath, headerParentSession) {
7972
8158
  if (headerParentSession) {
7973
8159
  const parentFile = path16.isAbsolute(headerParentSession) ? headerParentSession : void 0;
@@ -8012,11 +8198,15 @@ async function resolveParentContext(link, options) {
8012
8198
  return void 0;
8013
8199
  }
8014
8200
  function foldEventsIntoParent(events, link) {
8015
- const childSessionId = events.find((event) => event.sessionId)?.sessionId;
8016
- return events.map((event) => {
8201
+ const folded = withoutSubagentTurnEvents(events);
8202
+ const childSessionId = folded.find((event) => event.sessionId)?.sessionId;
8203
+ return folded.map((event) => {
8017
8204
  const rewritten = {
8018
8205
  ...event,
8019
- sessionId: link.parentSessionId
8206
+ sessionId: link.parentSessionId,
8207
+ // Subagent turns don't exist in the host session; drop the reference so
8208
+ // buildSessionRollups doesn't synthesize phantom turn rollups.
8209
+ turnId: void 0
8020
8210
  };
8021
8211
  if (childSessionId && childSessionId !== link.parentSessionId) {
8022
8212
  rewritten.refs = {
@@ -8272,6 +8462,100 @@ async function parsePiSessionFile(filePath, options) {
8272
8462
  }
8273
8463
  return state.events.filter((event) => matchesBackfillFilters(event, options));
8274
8464
  }
8465
+ async function parsePiFile(filePath, options) {
8466
+ if (filePath.endsWith(".json")) {
8467
+ return parsePiWorkflowRunFile(filePath, options);
8468
+ }
8469
+ return parsePiSessionFile(filePath, options);
8470
+ }
8471
+ async function parsePiWorkflowRunFile(filePath, options) {
8472
+ let raw;
8473
+ try {
8474
+ raw = JSON.parse(await readFile10(filePath, "utf8"));
8475
+ } catch {
8476
+ return [];
8477
+ }
8478
+ if (!isPlainObject(raw)) {
8479
+ return [];
8480
+ }
8481
+ const sessionId = stringField(raw, "sessionId");
8482
+ const agents = Array.isArray(raw.agents) ? raw.agents.filter((agent) => isPlainObject(agent)) : [];
8483
+ if (!sessionId || agents.length === 0) {
8484
+ return [];
8485
+ }
8486
+ const runUpdatedAt = timestampFrom(raw.updatedAt);
8487
+ const parentSessionFile = await findPiSessionFileById(sessionId, options);
8488
+ const host = await resolveParentContext(
8489
+ { parentSessionId: sessionId, parentSessionFile, explicit: true },
8490
+ options
8491
+ );
8492
+ const cwd = host?.cwd;
8493
+ const project = host?.project;
8494
+ const state = new SessionParserState(filePath, options, (event) => basePiEvent({ ...event, cwd, project, model: event.model }));
8495
+ const push = (event, ln) => {
8496
+ state.push(
8497
+ { ...event, workspaceId: event.workspaceId || createWorkspaceId({ projectName: project, repoRoot: cwd }) },
8498
+ ln,
8499
+ "workflow-run",
8500
+ event.type
8501
+ );
8502
+ };
8503
+ for (const [index, agent] of agents.entries()) {
8504
+ const lineNumber = index + 1;
8505
+ const usage = objectField(agent, "tokenUsage");
8506
+ const input = numberField(usage, "input") || 0;
8507
+ const output = numberField(usage, "output") || 0;
8508
+ const cacheRead = numberField(usage, "cacheRead") || 0;
8509
+ const cacheWrite = numberField(usage, "cacheWrite") || 0;
8510
+ const breakdown = input + output + cacheRead + cacheWrite;
8511
+ const total = numberField(usage, "total") || breakdown || numberField(agent, "tokens") || 0;
8512
+ if (total <= 0) {
8513
+ continue;
8514
+ }
8515
+ const model = stringField(agent, "model")?.replace(/:(off|minimal|low|medium|high|xhigh|max)$/, "");
8516
+ const ts = timestampFrom(agent.endedAt) || timestampFrom(agent.startedAt) || runUpdatedAt || (/* @__PURE__ */ new Date()).toISOString();
8517
+ push(basePiEvent({
8518
+ ts,
8519
+ type: "model.usage",
8520
+ sessionId,
8521
+ cwd,
8522
+ project,
8523
+ model,
8524
+ confidence: breakdown > 0 ? "exact" : "derived",
8525
+ metrics: {
8526
+ tokensInput: breakdown > 0 ? input + cacheRead + cacheWrite || void 0 : void 0,
8527
+ tokensOutput: breakdown > 0 ? output || void 0 : void 0,
8528
+ tokensCachedInput: breakdown > 0 ? cacheRead + cacheWrite || void 0 : void 0,
8529
+ tokensCacheReadInput: breakdown > 0 ? cacheRead || void 0 : void 0,
8530
+ tokensCacheCreationInput: breakdown > 0 ? cacheWrite || void 0 : void 0,
8531
+ tokensTotal: total,
8532
+ costUsd: numberField(usage, "cost") || void 0
8533
+ },
8534
+ refs: stringRefs({
8535
+ sourceId: stringField(agent, "callId") || stringField(agent, "label")
8536
+ })
8537
+ }), lineNumber);
8538
+ }
8539
+ return state.events.filter((event) => matchesBackfillFilters(event, options));
8540
+ }
8541
+ async function findPiSessionFileById(sessionId, options) {
8542
+ const sessionsDir = piSessionDir(path16.resolve(stringOption(options.home) || os7.homedir()));
8543
+ try {
8544
+ const projectDirs = await readdir7(sessionsDir, { withFileTypes: true });
8545
+ for (const dir of projectDirs) {
8546
+ if (!dir.isDirectory()) {
8547
+ continue;
8548
+ }
8549
+ const names = await readdir7(path16.join(sessionsDir, dir.name));
8550
+ const hit = names.find((name) => name.endsWith(`_${sessionId}.jsonl`));
8551
+ if (hit) {
8552
+ return path16.join(sessionsDir, dir.name, hit);
8553
+ }
8554
+ }
8555
+ } catch {
8556
+ }
8557
+ return void 0;
8558
+ }
8275
8559
  function basePiEvent(event) {
8276
8560
  return {
8277
8561
  schemaVersion: AGENT_TIME_SCHEMA_VERSION,
@@ -8518,6 +8802,20 @@ function piSessionDir(home, env) {
8518
8802
  }
8519
8803
  return path16.join(piAgentDir(home, env), "sessions");
8520
8804
  }
8805
+ function piWorkflowProjectsDir(home) {
8806
+ return path16.join(home, ".pi", "workflows", "projects");
8807
+ }
8808
+ async function piBackfillFiles(sourceRoot, home = os7.homedir(), env) {
8809
+ const lists = sourceRoot ? [await listFilesByExtensions(sourceRoot, [".jsonl", ".json"])] : await Promise.all([
8810
+ listFilesByExtensions(piSessionDir(home, env), [".jsonl"]),
8811
+ listFilesByExtensions(piWorkflowProjectsDir(home), [".json"])
8812
+ ]);
8813
+ const files = lists.flat().sort();
8814
+ return Promise.all(files.map(async (filePath) => {
8815
+ const info = await stat8(filePath);
8816
+ return { path: filePath, modifiedAt: info.mtime.toISOString() };
8817
+ }));
8818
+ }
8521
8819
  function createPiAdapter() {
8522
8820
  return {
8523
8821
  id: "pi",
@@ -8546,20 +8844,20 @@ function createPiAdapter() {
8546
8844
  }];
8547
8845
  },
8548
8846
  sourcePaths(home, env) {
8549
- return [piSessionDir(home, env)];
8847
+ return [piSessionDir(home, env), piWorkflowProjectsDir(home)];
8550
8848
  },
8551
- parseSessionFile: parsePiSessionFile
8849
+ parseSessionFile: parsePiFile
8552
8850
  };
8553
8851
  }
8554
8852
 
8555
8853
  // src/adapters/qoder-cn.ts
8556
- import { readdir as readdir7, readFile as readFile11, stat as stat8 } from "node:fs/promises";
8557
- import os8 from "node:os";
8854
+ import { readdir as readdir8, readFile as readFile11, stat as stat9 } from "node:fs/promises";
8855
+ import os9 from "node:os";
8558
8856
  import path18 from "node:path";
8559
8857
 
8560
8858
  // src/adapters/qoder-local-db.ts
8561
8859
  import { access } from "node:fs/promises";
8562
- import os7 from "node:os";
8860
+ import os8 from "node:os";
8563
8861
  import path17 from "node:path";
8564
8862
  function takeQoderDbModelCall(calls, requestId, blockStart) {
8565
8863
  if (requestId) {
@@ -8577,7 +8875,7 @@ function takeQoderDbModelCall(calls, requestId, blockStart) {
8577
8875
  }
8578
8876
  return calls.ordered.shift();
8579
8877
  }
8580
- function appDataRoot(appDirName, home = os7.homedir()) {
8878
+ function appDataRoot(appDirName, home = os8.homedir()) {
8581
8879
  if (process.platform === "darwin") {
8582
8880
  return path17.join(home, "Library", "Application Support", appDirName);
8583
8881
  }
@@ -8830,7 +9128,7 @@ async function loadQoderCnSegmentModelCalls(filePath, isSubagentSession, modelMa
8830
9128
  const segmentsPath = path18.join(configDir2, "logs", "sessions", projectName, sessionId, "segments");
8831
9129
  const modelCalls = [];
8832
9130
  try {
8833
- const files = await readdir7(segmentsPath);
9131
+ const files = await readdir8(segmentsPath);
8834
9132
  for (const file of files) {
8835
9133
  if (!file.endsWith(".jsonl")) {
8836
9134
  continue;
@@ -8880,7 +9178,7 @@ async function parseQoderCnSessionFile(filePath, options) {
8880
9178
  let cwd;
8881
9179
  let project = projectContext.project;
8882
9180
  let model;
8883
- const home = path18.resolve(stringOption(options.home) || os8.homedir());
9181
+ const home = path18.resolve(stringOption(options.home) || os9.homedir());
8884
9182
  const modelMap = await loadQoderCnModelNames(configDir2, home);
8885
9183
  const isSubagentSession = filePath.includes("subagents");
8886
9184
  const segmentModelCalls = await loadQoderCnSegmentModelCalls(filePath, isSubagentSession, modelMap);
@@ -9448,7 +9746,7 @@ async function gitRootFromCwds2(cwds) {
9448
9746
  while (!seen.has(current)) {
9449
9747
  seen.add(current);
9450
9748
  try {
9451
- await stat8(path18.join(current, ".git"));
9749
+ await stat9(path18.join(current, ".git"));
9452
9750
  return current;
9453
9751
  } catch {
9454
9752
  }
@@ -9482,7 +9780,7 @@ function encodeQoderCnProjectPath(value) {
9482
9780
  }
9483
9781
  async function qoderCnProjectFromFilePath(filePath, options) {
9484
9782
  const projectDir = path18.basename(path18.dirname(filePath));
9485
- const home = options ? path18.resolve(stringOption(options.home) || os8.homedir()) : os8.homedir();
9783
+ const home = options ? path18.resolve(stringOption(options.home) || os9.homedir()) : os9.homedir();
9486
9784
  const resolved = await resolveQoderCnProjectPath(projectDir, home);
9487
9785
  if (resolved) {
9488
9786
  return path18.basename(resolved);
@@ -9505,7 +9803,7 @@ async function resolveQoderCnProjectPath(projectDir, home) {
9505
9803
  while (projectDir.startsWith(`${currentEncoded}-`)) {
9506
9804
  let matchedChild;
9507
9805
  try {
9508
- const entries = await readdir7(current, { withFileTypes: true });
9806
+ const entries = await readdir8(current, { withFileTypes: true });
9509
9807
  for (const entry of entries) {
9510
9808
  if (!entry.isDirectory()) {
9511
9809
  continue;
@@ -9598,8 +9896,8 @@ function createQoderCnAdapter() {
9598
9896
 
9599
9897
  // src/adapters/qoder.ts
9600
9898
  import { existsSync } from "node:fs";
9601
- import { readdir as readdir8, readFile as readFile12, stat as stat9 } from "node:fs/promises";
9602
- import os9 from "node:os";
9899
+ import { readdir as readdir9, readFile as readFile12, stat as stat10 } from "node:fs/promises";
9900
+ import os10 from "node:os";
9603
9901
  import path19 from "node:path";
9604
9902
  function parseQoderPaths(filePath) {
9605
9903
  const parts = filePath.split(path19.sep);
@@ -9708,7 +10006,7 @@ async function loadQoderSegmentModelCalls(filePath, isSubagentSession, modelMap)
9708
10006
  const segmentsPath = path19.join(configDir2, "logs", "sessions", projectName, sessionId, "segments");
9709
10007
  const modelCalls = [];
9710
10008
  try {
9711
- const files = await readdir8(segmentsPath);
10009
+ const files = await readdir9(segmentsPath);
9712
10010
  for (const file of files) {
9713
10011
  if (!file.endsWith(".jsonl")) {
9714
10012
  continue;
@@ -9758,7 +10056,7 @@ async function parseQoderSessionFile(filePath, options) {
9758
10056
  let cwd;
9759
10057
  let project = projectContext.project;
9760
10058
  let model;
9761
- const home = path19.resolve(stringOption(options.home) || os9.homedir());
10059
+ const home = path19.resolve(stringOption(options.home) || os10.homedir());
9762
10060
  const modelMap = await loadQoderModelNames(configDir2, home);
9763
10061
  const qwenworkRoot = isQwenworkConfigRoot(configDir2);
9764
10062
  const isSubagentSession = filePath.includes("subagents");
@@ -10292,7 +10590,7 @@ async function gitRootFromCwds3(cwds) {
10292
10590
  while (!seen.has(current)) {
10293
10591
  seen.add(current);
10294
10592
  try {
10295
- await stat9(path19.join(current, ".git"));
10593
+ await stat10(path19.join(current, ".git"));
10296
10594
  return current;
10297
10595
  } catch {
10298
10596
  }
@@ -10344,7 +10642,7 @@ function qoderEncodedProjectSuffix(projectDir, home) {
10344
10642
  }
10345
10643
  async function qoderProjectFromFilePath(filePath, options) {
10346
10644
  const projectDir = path19.basename(path19.dirname(filePath));
10347
- const home = options ? path19.resolve(stringOption(options.home) || os9.homedir()) : os9.homedir();
10645
+ const home = options ? path19.resolve(stringOption(options.home) || os10.homedir()) : os10.homedir();
10348
10646
  const resolved = await resolveQoderProjectPath(projectDir, home);
10349
10647
  if (resolved) {
10350
10648
  return path19.basename(resolved);
@@ -10368,7 +10666,7 @@ async function resolveQoderProjectPath(projectDir, home) {
10368
10666
  while (currentEncodedVariants.some((prefix) => projectDir.startsWith(`${prefix}-`))) {
10369
10667
  let matchedChild;
10370
10668
  try {
10371
- const entries = await readdir8(current, { withFileTypes: true });
10669
+ const entries = await readdir9(current, { withFileTypes: true });
10372
10670
  for (const entry of entries) {
10373
10671
  if (!entry.isDirectory()) {
10374
10672
  continue;
@@ -10500,7 +10798,7 @@ function normalizeId(id) {
10500
10798
  }
10501
10799
 
10502
10800
  // src/adapters/workbuddy.ts
10503
- import { readdir as readdir9, readFile as readFile13, stat as stat10 } from "node:fs/promises";
10801
+ import { readdir as readdir10, readFile as readFile13, stat as stat11 } from "node:fs/promises";
10504
10802
  import path20 from "node:path";
10505
10803
  function workbuddyProjectsDir(home, env) {
10506
10804
  const override = env?.WORKBUDDY_PROJECTS_DIR || env?.WORKBUDDY_HOME;
@@ -10943,17 +11241,17 @@ async function workbuddyBackfillFiles(sourceRoot, home, env) {
10943
11241
  const base = sourceRoot || workbuddyProjectsDir(home, env);
10944
11242
  const files = [];
10945
11243
  try {
10946
- const projects = await readdir9(base, { withFileTypes: true });
11244
+ const projects = await readdir10(base, { withFileTypes: true });
10947
11245
  for (const project of projects) {
10948
11246
  if (!project.isDirectory()) {
10949
11247
  continue;
10950
11248
  }
10951
11249
  const projectDir = path20.join(base, project.name);
10952
- const entries = await readdir9(projectDir, { withFileTypes: true });
11250
+ const entries = await readdir10(projectDir, { withFileTypes: true });
10953
11251
  for (const entry of entries) {
10954
11252
  if (entry.isFile() && entry.name.endsWith(".jsonl")) {
10955
11253
  const filePath = path20.join(projectDir, entry.name);
10956
- const info = await stat10(filePath);
11254
+ const info = await stat11(filePath);
10957
11255
  files.push({ path: filePath, modifiedAt: info.mtime.toISOString() });
10958
11256
  }
10959
11257
  }
@@ -11014,7 +11312,7 @@ function createWorkbuddyAdapter() {
11014
11312
 
11015
11313
  // src/adapters/zcode.ts
11016
11314
  import { execFile } from "node:child_process";
11017
- import { readFile as readFile14, stat as stat11 } from "node:fs/promises";
11315
+ import { readFile as readFile14, stat as stat12 } from "node:fs/promises";
11018
11316
  import path21 from "node:path";
11019
11317
  import { promisify as promisify2 } from "node:util";
11020
11318
  init_fs();
@@ -11033,7 +11331,7 @@ var providerNameCache = null;
11033
11331
  async function loadProviderNames(configPath2) {
11034
11332
  let fileMtime = 0;
11035
11333
  try {
11036
- const info = await stat11(configPath2);
11334
+ const info = await stat12(configPath2);
11037
11335
  fileMtime = info.mtimeMs;
11038
11336
  } catch {
11039
11337
  return /* @__PURE__ */ new Map();
@@ -11233,6 +11531,7 @@ function sessionContext(row, sessions) {
11233
11531
  return {
11234
11532
  rawSessionId: sessionId,
11235
11533
  sessionId: `zcode:${rootSessionId}`,
11534
+ isSubagent: rootSessionId !== sessionId,
11236
11535
  cwd,
11237
11536
  project,
11238
11537
  workspaceId: createWorkspaceId({ projectName: project, repoRoot: cwd }),
@@ -11249,7 +11548,7 @@ async function parseZCodeDb(filePath, options) {
11249
11548
  for (let i = 0; i < 12; i++) {
11250
11549
  const probe = path21.join(candidate, ".zcode", "v2", "config.json");
11251
11550
  try {
11252
- await stat11(probe);
11551
+ await stat12(probe);
11253
11552
  configPath2 = probe;
11254
11553
  break;
11255
11554
  } catch {
@@ -11324,6 +11623,9 @@ async function parseZCodeDb(filePath, options) {
11324
11623
  return;
11325
11624
  }
11326
11625
  const ctx = sessionContext(row, sessions);
11626
+ if (ctx.isSubagent) {
11627
+ return;
11628
+ }
11327
11629
  const turnId = `zcode:${rawTurnId}`;
11328
11630
  const started = makeEvent2({
11329
11631
  schemaVersion: AGENT_TIME_SCHEMA_VERSION,
@@ -11384,7 +11686,8 @@ async function parseZCodeDb(filePath, options) {
11384
11686
  project: ctx.project,
11385
11687
  cwd: ctx.cwd,
11386
11688
  sessionId: ctx.sessionId,
11387
- turnId: stringField(row, "turn_id") ? `zcode:${stringField(row, "turn_id")}` : void 0,
11689
+ // Subagent turns are dropped; keep their events out of turn rollups.
11690
+ turnId: !ctx.isSubagent && stringField(row, "turn_id") ? `zcode:${stringField(row, "turn_id")}` : void 0,
11388
11691
  agent: stringField(row, "agent") || "zcode",
11389
11692
  provider: nameMap.get(stringField(row, "provider_id") || "") || stringField(row, "provider_id"),
11390
11693
  model: modelId,
@@ -11410,7 +11713,7 @@ async function parseZCodeDb(filePath, options) {
11410
11713
  return;
11411
11714
  }
11412
11715
  const ctx = sessionContext(row, sessions);
11413
- const turnId = stringField(row, "turn_id") ? `zcode:${stringField(row, "turn_id")}` : void 0;
11716
+ const turnId = !ctx.isSubagent && stringField(row, "turn_id") ? `zcode:${stringField(row, "turn_id")}` : void 0;
11414
11717
  const spanId = `${ctx.sessionId}:tool:${toolCallId}`;
11415
11718
  const started = makeEvent2({
11416
11719
  schemaVersion: AGENT_TIME_SCHEMA_VERSION,
@@ -11471,7 +11774,7 @@ async function zcodeBackfillFiles(sourceRoot, home, env) {
11471
11774
  const candidate = sourceRoot || zcodeDbPath(home, env);
11472
11775
  const filePath = candidate.endsWith(".sqlite") ? candidate : path21.join(candidate, "db", "db.sqlite");
11473
11776
  try {
11474
- const info = await stat11(filePath);
11777
+ const info = await stat12(filePath);
11475
11778
  return [{ path: filePath, modifiedAt: info.mtime.toISOString() }];
11476
11779
  } catch {
11477
11780
  return [];
@@ -11503,7 +11806,7 @@ function createZCodeAdapter() {
11503
11806
  }
11504
11807
 
11505
11808
  // src/adapters/zed.ts
11506
- import os10 from "node:os";
11809
+ import os11 from "node:os";
11507
11810
  import path22 from "node:path";
11508
11811
  function zedThreadsCandidates(home, env) {
11509
11812
  const candidates = [];
@@ -11855,20 +12158,20 @@ async function parseZedSessionFile(dbPath, options) {
11855
12158
  }
11856
12159
  return events.filter((event) => matchesBackfillFilters(event, options));
11857
12160
  }
11858
- async function zedBackfillFiles(sourceRoot, home = os10.homedir(), env) {
11859
- const { stat: stat14 } = await import("node:fs/promises");
12161
+ async function zedBackfillFiles(sourceRoot, home = os11.homedir(), env) {
12162
+ const { stat: stat15 } = await import("node:fs/promises");
11860
12163
  if (sourceRoot) {
11861
12164
  if (!sourceRoot.endsWith(".db")) {
11862
12165
  return [];
11863
12166
  }
11864
- const info = await stat14(sourceRoot).catch(() => null);
12167
+ const info = await stat15(sourceRoot).catch(() => null);
11865
12168
  if (!info) {
11866
12169
  return [];
11867
12170
  }
11868
12171
  return [{ path: sourceRoot, modifiedAt: info.mtime.toISOString() }];
11869
12172
  }
11870
12173
  for (const candidatePath of zedThreadsCandidates(home, env)) {
11871
- const info = await stat14(candidatePath).catch(() => null);
12174
+ const info = await stat15(candidatePath).catch(() => null);
11872
12175
  if (info) {
11873
12176
  return [{ path: candidatePath, modifiedAt: info.mtime.toISOString() }];
11874
12177
  }
@@ -12260,11 +12563,26 @@ async function installEntry(entry, options) {
12260
12563
  await mergeHooksJson(entry.path, entry.content, options);
12261
12564
  return;
12262
12565
  }
12566
+ if (entry.kind === "hooks-toml" && typeof entry.content === "object") {
12567
+ await mergeHooksToml(entry.path, entry.content, options);
12568
+ return;
12569
+ }
12263
12570
  await writeGeneratedFile(entry.path, String(entry.content), {
12264
12571
  ...options,
12265
12572
  onWrite: options.onWrite
12266
12573
  });
12267
12574
  }
12575
+ async function uninstallEntry(entry, options) {
12576
+ if (entry.kind === "hooks-toml" && typeof entry.content === "object") {
12577
+ await uninstallHooksToml(entry.path, entry.content, options);
12578
+ return;
12579
+ }
12580
+ if (entry.kind === "hooks-json" && typeof entry.content === "object") {
12581
+ await uninstallHooksJson(entry.path, entry.content, options);
12582
+ return;
12583
+ }
12584
+ await uninstallGeneratedFile(entry.path, options);
12585
+ }
12268
12586
  async function mergeHooksJson(filePath, content, { dryRun, force, onWrite }) {
12269
12587
  const { mkdir: mkdir6, writeFile: writeFile5 } = await import("node:fs/promises");
12270
12588
  const pathMod = await import("node:path");
@@ -12361,6 +12679,218 @@ function hookCommandFromGroup(group) {
12361
12679
  const hook = group.hooks[0];
12362
12680
  return isPlainObject(hook) && typeof hook.command === "string" ? hook.command : void 0;
12363
12681
  }
12682
+ async function mergeHooksToml(filePath, content, { dryRun, force, onWrite }) {
12683
+ const { mkdir: mkdir6, writeFile: writeFile5 } = await import("node:fs/promises");
12684
+ const pathMod = await import("node:path");
12685
+ if (dryRun) {
12686
+ onWrite(`Would merge ${filePath}`);
12687
+ return;
12688
+ }
12689
+ const existingText = await readTextIfExists(filePath);
12690
+ if (existingText !== null && existingText.trim() !== "" && !looksLikeToml(existingText) && !force) {
12691
+ throw new Error(
12692
+ `Refusing to update non-TOML file: ${filePath}. Re-run with --force if this is intentional.`
12693
+ );
12694
+ }
12695
+ const desired = Array.isArray(content.hooks) ? content.hooks : [];
12696
+ const nextText = mergeTomlHookRules(existingText ?? "", desired);
12697
+ if (existingText === nextText || existingText === null && nextText === "") {
12698
+ onWrite(`Already installed ${filePath}`);
12699
+ return;
12700
+ }
12701
+ if (existingText !== null) {
12702
+ const existingKeys = new Set(
12703
+ parseTomlHookRules(existingText).map((rule) => `${rule.event}\0${rule.command}`)
12704
+ );
12705
+ const allPresent = desired.every((rule) => existingKeys.has(`${rule.event}\0${rule.command}`));
12706
+ if (allPresent && desired.length > 0) {
12707
+ onWrite(`Already installed ${filePath}`);
12708
+ return;
12709
+ }
12710
+ }
12711
+ await mkdir6(pathMod.dirname(filePath), { recursive: true });
12712
+ await writeFile5(filePath, nextText, "utf8");
12713
+ onWrite(`Installed ${filePath}`);
12714
+ }
12715
+ async function uninstallHooksToml(filePath, content, { dryRun, onWrite }) {
12716
+ const existingText = await readTextIfExists(filePath);
12717
+ if (existingText === null) {
12718
+ onWrite(`Already uninstalled ${filePath}`);
12719
+ return;
12720
+ }
12721
+ const commands = Array.from(new Set(
12722
+ (Array.isArray(content.hooks) ? content.hooks : []).map((rule) => rule.command).filter((cmd) => typeof cmd === "string" && cmd.length > 0)
12723
+ ));
12724
+ if (commands.length === 0) {
12725
+ onWrite(`Already uninstalled ${filePath}`);
12726
+ return;
12727
+ }
12728
+ const nextText = removeTomlHookRulesByCommand(existingText, commands);
12729
+ if (nextText === existingText) {
12730
+ onWrite(`Already uninstalled ${filePath}`);
12731
+ return;
12732
+ }
12733
+ if (dryRun) {
12734
+ onWrite(`Would uninstall ${filePath}`);
12735
+ return;
12736
+ }
12737
+ const { writeFile: writeFile5 } = await import("node:fs/promises");
12738
+ await writeFile5(filePath, nextText, "utf8");
12739
+ onWrite(`Uninstalled ${filePath}`);
12740
+ }
12741
+ function collectHookCommandsFromJsonContent(content) {
12742
+ const commands = /* @__PURE__ */ new Set();
12743
+ const walkGroups = (groups) => {
12744
+ if (!Array.isArray(groups)) {
12745
+ return;
12746
+ }
12747
+ for (const group of groups) {
12748
+ if (!isPlainObject(group) || !Array.isArray(group.hooks)) {
12749
+ continue;
12750
+ }
12751
+ for (const hook of group.hooks) {
12752
+ if (isPlainObject(hook) && typeof hook.command === "string" && hook.command) {
12753
+ commands.add(hook.command);
12754
+ }
12755
+ }
12756
+ }
12757
+ };
12758
+ if (isPlainObject(content.hooks)) {
12759
+ for (const groups of Object.values(content.hooks)) {
12760
+ walkGroups(groups);
12761
+ }
12762
+ }
12763
+ for (const [key, value] of Object.entries(content)) {
12764
+ if (key === "hooks" || key === "enable_json_hooks" || !isPlainObject(value)) {
12765
+ continue;
12766
+ }
12767
+ for (const groups of Object.values(value)) {
12768
+ walkGroups(groups);
12769
+ }
12770
+ }
12771
+ return [...commands];
12772
+ }
12773
+ function stripHookCommandsFromGroups(groups, commands) {
12774
+ if (!Array.isArray(groups)) {
12775
+ return groups;
12776
+ }
12777
+ return groups.map((group) => {
12778
+ if (!isPlainObject(group) || !Array.isArray(group.hooks)) {
12779
+ return group;
12780
+ }
12781
+ const nextHooks = group.hooks.filter(
12782
+ (hook) => !(isPlainObject(hook) && typeof hook.command === "string" && commands.has(hook.command))
12783
+ );
12784
+ if (nextHooks.length === 0) {
12785
+ return null;
12786
+ }
12787
+ return { ...group, hooks: nextHooks };
12788
+ }).filter(Boolean);
12789
+ }
12790
+ async function uninstallHooksJson(filePath, content, { dryRun, onWrite }) {
12791
+ const existingText = await readTextIfExists(filePath);
12792
+ if (existingText === null) {
12793
+ onWrite(`Already uninstalled ${filePath}`);
12794
+ return;
12795
+ }
12796
+ let existing;
12797
+ try {
12798
+ existing = JSON.parse(existingText);
12799
+ } catch {
12800
+ onWrite(`Skipped non-JSON file ${filePath}`);
12801
+ return;
12802
+ }
12803
+ if (!isPlainObject(existing)) {
12804
+ onWrite(`Skipped non-object JSON file ${filePath}`);
12805
+ return;
12806
+ }
12807
+ const commands = new Set(collectHookCommandsFromJsonContent(content));
12808
+ if (commands.size === 0) {
12809
+ onWrite(`Already uninstalled ${filePath}`);
12810
+ return;
12811
+ }
12812
+ const next = structuredClone(existing);
12813
+ let changed = false;
12814
+ if (isPlainObject(next.hooks)) {
12815
+ for (const [event, groups] of Object.entries(next.hooks)) {
12816
+ const stripped = stripHookCommandsFromGroups(groups, commands);
12817
+ if (JSON.stringify(stripped) !== JSON.stringify(groups)) {
12818
+ changed = true;
12819
+ if (Array.isArray(stripped) && stripped.length === 0) {
12820
+ delete next.hooks[event];
12821
+ } else {
12822
+ next.hooks[event] = stripped;
12823
+ }
12824
+ }
12825
+ }
12826
+ if (isPlainObject(next.hooks) && Object.keys(next.hooks).length === 0) {
12827
+ delete next.hooks;
12828
+ changed = true;
12829
+ }
12830
+ }
12831
+ for (const [key, value] of Object.entries(next)) {
12832
+ if (key === "hooks" || key === "enable_json_hooks" || !isPlainObject(value)) {
12833
+ continue;
12834
+ }
12835
+ if (!Object.prototype.hasOwnProperty.call(content, key)) {
12836
+ continue;
12837
+ }
12838
+ for (const [event, groups] of Object.entries(value)) {
12839
+ const stripped = stripHookCommandsFromGroups(groups, commands);
12840
+ if (JSON.stringify(stripped) !== JSON.stringify(groups)) {
12841
+ changed = true;
12842
+ if (Array.isArray(stripped) && stripped.length === 0) {
12843
+ delete value[event];
12844
+ } else {
12845
+ value[event] = stripped;
12846
+ }
12847
+ }
12848
+ }
12849
+ if (Object.keys(value).length === 0) {
12850
+ delete next[key];
12851
+ changed = true;
12852
+ }
12853
+ }
12854
+ if (Object.prototype.hasOwnProperty.call(content, "enable_json_hooks") && next.enable_json_hooks === true) {
12855
+ const stillHasNamedHooks = Object.entries(next).some(
12856
+ ([key, value]) => key !== "hooks" && key !== "enable_json_hooks" && isPlainObject(value) && Object.values(value).some((groups) => Array.isArray(groups) && groups.length > 0)
12857
+ );
12858
+ if (!stillHasNamedHooks) {
12859
+ delete next.enable_json_hooks;
12860
+ changed = true;
12861
+ }
12862
+ }
12863
+ if (!changed) {
12864
+ onWrite(`Already uninstalled ${filePath}`);
12865
+ return;
12866
+ }
12867
+ if (dryRun) {
12868
+ onWrite(`Would uninstall ${filePath}`);
12869
+ return;
12870
+ }
12871
+ const { writeFile: writeFile5 } = await import("node:fs/promises");
12872
+ await writeFile5(filePath, `${JSON.stringify(next, null, 2)}
12873
+ `, "utf8");
12874
+ onWrite(`Uninstalled ${filePath}`);
12875
+ }
12876
+ async function uninstallGeneratedFile(filePath, { dryRun, onWrite }) {
12877
+ const existingText = await readTextIfExists(filePath);
12878
+ if (existingText === null) {
12879
+ onWrite(`Already uninstalled ${filePath}`);
12880
+ return;
12881
+ }
12882
+ if (!existingText.includes(GENERATED_MARKER) && !existingText.includes("Generated by codetime.")) {
12883
+ onWrite(`Skipped non-vibetime file ${filePath}`);
12884
+ return;
12885
+ }
12886
+ if (dryRun) {
12887
+ onWrite(`Would uninstall ${filePath}`);
12888
+ return;
12889
+ }
12890
+ const { unlink } = await import("node:fs/promises");
12891
+ await unlink(filePath);
12892
+ onWrite(`Uninstalled ${filePath}`);
12893
+ }
12364
12894
 
12365
12895
  // src/lib/config.ts
12366
12896
  import { randomUUID } from "node:crypto";
@@ -12421,7 +12951,7 @@ function defaultMachineName() {
12421
12951
  init_fs();
12422
12952
 
12423
12953
  // src/lib/logger.ts
12424
- import { appendFile, mkdir as mkdir4, rename, stat as stat12 } from "node:fs/promises";
12954
+ import { appendFile, mkdir as mkdir4, rename, stat as stat13 } from "node:fs/promises";
12425
12955
  import { homedir as homedir2 } from "node:os";
12426
12956
  import path24 from "node:path";
12427
12957
  var MAX_BYTES = 1 * 1024 * 1024;
@@ -12439,7 +12969,7 @@ function serializeError(error) {
12439
12969
  }
12440
12970
  async function rotateIfNeeded(file) {
12441
12971
  try {
12442
- const info = await stat12(file);
12972
+ const info = await stat13(file);
12443
12973
  if (info.size > MAX_BYTES) {
12444
12974
  await rename(file, `${file}.1`).catch(() => {
12445
12975
  });
@@ -12753,6 +13283,7 @@ function createCli(ctx, registry) {
12753
13283
  });
12754
13284
  cli.command("detect", "Show supported local targets and install status").action((options) => detectCommand(normalizeOptions(options), ctx, registry).then(() => 0));
12755
13285
  cli.command("install", "Install integration files into detected or requested targets").option("--target <targets>", "Target integrations, comma-separated").option("--targets <targets>", "Target integrations, comma-separated").option("--all", "Install all supported integrations").option("--force", "Overwrite existing non-generated files when needed").action((options) => installCommand(normalizeOptions(options), ctx, registry));
13286
+ cli.command("uninstall", "Remove vibetime integration hooks/files from targets").option("--target <targets>", "Target integrations, comma-separated").option("--targets <targets>", "Target integrations, comma-separated").option("--all", "Uninstall all supported integrations").action((options) => uninstallCommand(normalizeOptions(options), ctx, registry));
12756
13287
  cli.command("upgrade", "Check for updates and upgrade to the latest version").option("--check", "Only check for updates, do not install").action((options) => upgradeCommand(normalizeOptions(options), ctx, registry));
12757
13288
  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));
12758
13289
  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));
@@ -12849,6 +13380,44 @@ async function installCommand(options, ctx, registry) {
12849
13380
  }
12850
13381
  return 0;
12851
13382
  }
13383
+ async function uninstallCommand(options, ctx, registry) {
13384
+ const home = resolveHome3(options, ctx);
13385
+ const env = ctx.env;
13386
+ const dryRun = Boolean(options["dry-run"]);
13387
+ const allAdapters = registry.all();
13388
+ const requested = requestedTargets(options);
13389
+ const unknown = requested.filter((id) => !allAdapters.some((a) => a.id === id));
13390
+ if (unknown.length > 0) {
13391
+ throw new Error(`Unknown target(s): ${unknown.join(", ")}`);
13392
+ }
13393
+ const installed = [];
13394
+ for (const adapter of allAdapters) {
13395
+ if (await adapter.isInstalled(home, env)) {
13396
+ installed.push(adapter.id);
13397
+ }
13398
+ }
13399
+ const selectedIds = requested.length > 0 ? requested : options.all ? allAdapters.map((a) => a.id) : installed;
13400
+ if (selectedIds.length === 0) {
13401
+ write(ctx.stderr, "No installed vibetime integrations were found. Use --target <id> or --all.\n");
13402
+ return 1;
13403
+ }
13404
+ for (const adapter of allAdapters.filter((a) => selectedIds.includes(a.id))) {
13405
+ const entries = adapter.installEntries(home, env);
13406
+ if (entries.length === 0) {
13407
+ write(ctx.stdout, `Nothing to uninstall for ${adapter.id}
13408
+ `);
13409
+ continue;
13410
+ }
13411
+ for (const entry of entries) {
13412
+ await uninstallEntry(entry, {
13413
+ dryRun,
13414
+ onWrite: (msg) => write(ctx.stdout, `${msg}
13415
+ `)
13416
+ });
13417
+ }
13418
+ }
13419
+ return 0;
13420
+ }
12852
13421
  var NPM_PACKAGE = "@yhong91/vibetime";
12853
13422
  async function fetchLatestVersion() {
12854
13423
  try {
@@ -13219,11 +13788,14 @@ async function listBackfillSourceFiles(source, options, ctx) {
13219
13788
  if (source.id === "zed") {
13220
13789
  return zedBackfillFiles(stringOption(options["source-root"]), resolveHome3(options, ctx), ctx.env);
13221
13790
  }
13791
+ if (source.id === "pi") {
13792
+ return piBackfillFiles(stringOption(options["source-root"]), resolveHome3(options, ctx), ctx.env);
13793
+ }
13222
13794
  const roots = stringOption(options["source-root"]) ? [requiredOption(options, "source-root")] : source.paths;
13223
13795
  const fileLists = await Promise.all(roots.map((r) => listJsonlFiles(r)));
13224
13796
  const files = fileLists.flat().sort().slice(0, numberOption(options.limit) || void 0);
13225
13797
  return Promise.all(files.map(async (filePath) => {
13226
- const info = await stat13(filePath);
13798
+ const info = await stat14(filePath);
13227
13799
  return { path: filePath, modifiedAt: info.mtime.toISOString() };
13228
13800
  }));
13229
13801
  }
@@ -13811,7 +14383,7 @@ function syncLocalRunnerEntryArgs(cliPath) {
13811
14383
  return [path25.resolve(path25.dirname(cliPath), "../bin/vibetime.mjs")];
13812
14384
  }
13813
14385
  function resolveHome3(options, ctx) {
13814
- return path25.resolve(stringOption(options.home) || ctx.env.HOME || os11.homedir());
14386
+ return path25.resolve(stringOption(options.home) || ctx.env.HOME || os12.homedir());
13815
14387
  }
13816
14388
  function requestedTargets(options) {
13817
14389
  const value = options.target || options.targets;
@@ -13982,6 +14554,7 @@ function helpText() {
13982
14554
  Usage:
13983
14555
  vibetime detect [--json] [--home <path>]
13984
14556
  vibetime install [--target codex,claude,opencode,pi] [--all] [--dry-run] [--force] [--home <path>]
14557
+ vibetime uninstall [--target codex,claude,opencode,pi] [--all] [--dry-run] [--home <path>]
13985
14558
  vibetime upgrade [--check]
13986
14559
  vibetime hook --agent <name>
13987
14560
  vibetime backfill discover|plan|import|verify --source codex|claude-code|opencode|pi|all --dry-run [--json] [--batch-size <count>] [--force [--purge-all]]
@@ -13996,6 +14569,7 @@ Setup:
13996
14569
  Commands:
13997
14570
  detect Show supported local targets and install status.
13998
14571
  install Install integration files into detected or requested targets.
14572
+ uninstall Remove vibetime hooks/files from detected or requested targets.
13999
14573
  upgrade Check for updates and upgrade to the latest version.
14000
14574
  hook Read agent hook JSON from stdin and report a throttled event.
14001
14575
  backfill Discover local history and create metadata-only import plans.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yhong91/vibetime",
3
3
  "type": "module",
4
- "version": "0.1.52",
4
+ "version": "0.1.54",
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": {