vibeusage 0.2.23 → 0.3.1

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 (143) hide show
  1. package/README.md +25 -20
  2. package/README.zh-CN.md +7 -2
  3. package/node_modules/@insforge/sdk/LICENSE +201 -201
  4. package/node_modules/@insforge/sdk/README.md +326 -259
  5. package/node_modules/@insforge/sdk/dist/index.d.mts +377 -182
  6. package/node_modules/@insforge/sdk/dist/index.d.ts +377 -182
  7. package/node_modules/@insforge/sdk/dist/index.js +1172 -677
  8. package/node_modules/@insforge/sdk/dist/index.js.map +1 -1
  9. package/node_modules/@insforge/sdk/dist/index.mjs +1171 -677
  10. package/node_modules/@insforge/sdk/dist/index.mjs.map +1 -1
  11. package/node_modules/@insforge/sdk/package.json +68 -68
  12. package/node_modules/@insforge/shared-schemas/dist/ai-api.schema.d.ts +1120 -43
  13. package/node_modules/@insforge/shared-schemas/dist/ai-api.schema.d.ts.map +1 -1
  14. package/node_modules/@insforge/shared-schemas/dist/ai-api.schema.js +179 -5
  15. package/node_modules/@insforge/shared-schemas/dist/ai-api.schema.js.map +1 -1
  16. package/node_modules/@insforge/shared-schemas/dist/ai.schema.d.ts +25 -25
  17. package/node_modules/@insforge/shared-schemas/dist/ai.schema.d.ts.map +1 -1
  18. package/node_modules/@insforge/shared-schemas/dist/ai.schema.js +2 -2
  19. package/node_modules/@insforge/shared-schemas/dist/ai.schema.js.map +1 -1
  20. package/node_modules/@insforge/shared-schemas/dist/auth-api.schema.d.ts +197 -51
  21. package/node_modules/@insforge/shared-schemas/dist/auth-api.schema.d.ts.map +1 -1
  22. package/node_modules/@insforge/shared-schemas/dist/auth-api.schema.js +87 -23
  23. package/node_modules/@insforge/shared-schemas/dist/auth-api.schema.js.map +1 -1
  24. package/node_modules/@insforge/shared-schemas/dist/auth.schema.d.ts +32 -3
  25. package/node_modules/@insforge/shared-schemas/dist/auth.schema.d.ts.map +1 -1
  26. package/node_modules/@insforge/shared-schemas/dist/auth.schema.js +21 -3
  27. package/node_modules/@insforge/shared-schemas/dist/auth.schema.js.map +1 -1
  28. package/node_modules/@insforge/shared-schemas/dist/cloud-events.schema.d.ts +380 -0
  29. package/node_modules/@insforge/shared-schemas/dist/cloud-events.schema.d.ts.map +1 -1
  30. package/node_modules/@insforge/shared-schemas/dist/cloud-events.schema.js +74 -0
  31. package/node_modules/@insforge/shared-schemas/dist/cloud-events.schema.js.map +1 -1
  32. package/node_modules/@insforge/shared-schemas/dist/database-api.schema.d.ts +13 -13
  33. package/node_modules/@insforge/shared-schemas/dist/database-api.schema.js +1 -1
  34. package/node_modules/@insforge/shared-schemas/dist/database-api.schema.js.map +1 -1
  35. package/node_modules/@insforge/shared-schemas/dist/deployments-api.schema.d.ts +735 -0
  36. package/node_modules/@insforge/shared-schemas/dist/deployments-api.schema.d.ts.map +1 -0
  37. package/node_modules/@insforge/shared-schemas/dist/deployments-api.schema.js +209 -0
  38. package/node_modules/@insforge/shared-schemas/dist/deployments-api.schema.js.map +1 -0
  39. package/node_modules/@insforge/shared-schemas/dist/deployments.schema.d.ts +37 -0
  40. package/node_modules/@insforge/shared-schemas/dist/deployments.schema.d.ts.map +1 -0
  41. package/node_modules/@insforge/shared-schemas/dist/deployments.schema.js +25 -0
  42. package/node_modules/@insforge/shared-schemas/dist/deployments.schema.js.map +1 -0
  43. package/node_modules/@insforge/shared-schemas/dist/docs.schema.d.ts +5 -1
  44. package/node_modules/@insforge/shared-schemas/dist/docs.schema.d.ts.map +1 -1
  45. package/node_modules/@insforge/shared-schemas/dist/docs.schema.js +34 -4
  46. package/node_modules/@insforge/shared-schemas/dist/docs.schema.js.map +1 -1
  47. package/node_modules/@insforge/shared-schemas/dist/email-api.schema.js +1 -1
  48. package/node_modules/@insforge/shared-schemas/dist/email-api.schema.js.map +1 -1
  49. package/node_modules/@insforge/shared-schemas/dist/functions-api.schema.d.ts +186 -6
  50. package/node_modules/@insforge/shared-schemas/dist/functions-api.schema.d.ts.map +1 -1
  51. package/node_modules/@insforge/shared-schemas/dist/functions-api.schema.js +21 -2
  52. package/node_modules/@insforge/shared-schemas/dist/functions-api.schema.js.map +1 -1
  53. package/node_modules/@insforge/shared-schemas/dist/functions.schema.d.ts +5 -5
  54. package/node_modules/@insforge/shared-schemas/dist/functions.schema.js +1 -1
  55. package/node_modules/@insforge/shared-schemas/dist/functions.schema.js.map +1 -1
  56. package/node_modules/@insforge/shared-schemas/dist/index.d.ts +24 -18
  57. package/node_modules/@insforge/shared-schemas/dist/index.d.ts.map +1 -1
  58. package/node_modules/@insforge/shared-schemas/dist/index.js +24 -18
  59. package/node_modules/@insforge/shared-schemas/dist/index.js.map +1 -1
  60. package/node_modules/@insforge/shared-schemas/dist/logs-api.schema.js +1 -1
  61. package/node_modules/@insforge/shared-schemas/dist/logs-api.schema.js.map +1 -1
  62. package/node_modules/@insforge/shared-schemas/dist/logs.schema.d.ts +43 -0
  63. package/node_modules/@insforge/shared-schemas/dist/logs.schema.d.ts.map +1 -1
  64. package/node_modules/@insforge/shared-schemas/dist/logs.schema.js +11 -0
  65. package/node_modules/@insforge/shared-schemas/dist/logs.schema.js.map +1 -1
  66. package/node_modules/@insforge/shared-schemas/dist/metadata.schema.d.ts +229 -172
  67. package/node_modules/@insforge/shared-schemas/dist/metadata.schema.d.ts.map +1 -1
  68. package/node_modules/@insforge/shared-schemas/dist/metadata.schema.js +27 -7
  69. package/node_modules/@insforge/shared-schemas/dist/metadata.schema.js.map +1 -1
  70. package/node_modules/@insforge/shared-schemas/dist/rate-limit-api.schema.d.ts +51 -0
  71. package/node_modules/@insforge/shared-schemas/dist/rate-limit-api.schema.d.ts.map +1 -0
  72. package/node_modules/@insforge/shared-schemas/dist/rate-limit-api.schema.js +31 -0
  73. package/node_modules/@insforge/shared-schemas/dist/rate-limit-api.schema.js.map +1 -0
  74. package/node_modules/@insforge/shared-schemas/dist/rate-limit.schema.d.ts +31 -0
  75. package/node_modules/@insforge/shared-schemas/dist/rate-limit.schema.d.ts.map +1 -0
  76. package/node_modules/@insforge/shared-schemas/dist/rate-limit.schema.js +12 -0
  77. package/node_modules/@insforge/shared-schemas/dist/rate-limit.schema.js.map +1 -0
  78. package/node_modules/@insforge/shared-schemas/dist/realtime-api.schema.d.ts +39 -20
  79. package/node_modules/@insforge/shared-schemas/dist/realtime-api.schema.d.ts.map +1 -1
  80. package/node_modules/@insforge/shared-schemas/dist/realtime-api.schema.js +5 -1
  81. package/node_modules/@insforge/shared-schemas/dist/realtime-api.schema.js.map +1 -1
  82. package/node_modules/@insforge/shared-schemas/dist/realtime.schema.d.ts +12 -4
  83. package/node_modules/@insforge/shared-schemas/dist/realtime.schema.d.ts.map +1 -1
  84. package/node_modules/@insforge/shared-schemas/dist/realtime.schema.js +6 -0
  85. package/node_modules/@insforge/shared-schemas/dist/realtime.schema.js.map +1 -1
  86. package/node_modules/@insforge/shared-schemas/dist/schedules-api.schema.d.ts +287 -0
  87. package/node_modules/@insforge/shared-schemas/dist/schedules-api.schema.d.ts.map +1 -0
  88. package/node_modules/@insforge/shared-schemas/dist/schedules-api.schema.js +81 -0
  89. package/node_modules/@insforge/shared-schemas/dist/schedules-api.schema.js.map +1 -0
  90. package/node_modules/@insforge/shared-schemas/dist/schedules.schema.d.ts +77 -0
  91. package/node_modules/@insforge/shared-schemas/dist/schedules.schema.d.ts.map +1 -0
  92. package/node_modules/@insforge/shared-schemas/dist/schedules.schema.js +36 -0
  93. package/node_modules/@insforge/shared-schemas/dist/schedules.schema.js.map +1 -0
  94. package/node_modules/@insforge/shared-schemas/dist/secrets-api.schema.d.ts +113 -0
  95. package/node_modules/@insforge/shared-schemas/dist/secrets-api.schema.d.ts.map +1 -0
  96. package/node_modules/@insforge/shared-schemas/dist/secrets-api.schema.js +31 -0
  97. package/node_modules/@insforge/shared-schemas/dist/secrets-api.schema.js.map +1 -0
  98. package/node_modules/@insforge/shared-schemas/dist/secrets.schema.d.ts +31 -0
  99. package/node_modules/@insforge/shared-schemas/dist/secrets.schema.d.ts.map +1 -0
  100. package/node_modules/@insforge/shared-schemas/dist/secrets.schema.js +13 -0
  101. package/node_modules/@insforge/shared-schemas/dist/secrets.schema.js.map +1 -0
  102. package/node_modules/@insforge/shared-schemas/dist/storage-api.schema.d.ts +27 -2
  103. package/node_modules/@insforge/shared-schemas/dist/storage-api.schema.d.ts.map +1 -1
  104. package/node_modules/@insforge/shared-schemas/dist/storage-api.schema.js +9 -1
  105. package/node_modules/@insforge/shared-schemas/dist/storage-api.schema.js.map +1 -1
  106. package/node_modules/@insforge/shared-schemas/dist/storage.schema.d.ts +17 -0
  107. package/node_modules/@insforge/shared-schemas/dist/storage.schema.d.ts.map +1 -1
  108. package/node_modules/@insforge/shared-schemas/dist/storage.schema.js +6 -0
  109. package/node_modules/@insforge/shared-schemas/dist/storage.schema.js.map +1 -1
  110. package/node_modules/@insforge/shared-schemas/package.json +2 -1
  111. package/package.json +5 -6
  112. package/src/cli.js +2 -2
  113. package/src/commands/init.js +20 -362
  114. package/src/commands/status.js +58 -51
  115. package/src/commands/sync.js +37 -25
  116. package/src/commands/uninstall.js +121 -104
  117. package/src/lib/claude-config.js +130 -35
  118. package/src/lib/diagnostics.js +88 -57
  119. package/src/lib/doctor.js +50 -0
  120. package/src/lib/insforge-client.js +13 -9
  121. package/src/lib/integrations/claude.js +106 -0
  122. package/src/lib/integrations/codex.js +88 -0
  123. package/src/lib/integrations/context.js +76 -0
  124. package/src/lib/integrations/every-code.js +88 -0
  125. package/src/lib/integrations/gemini.js +86 -0
  126. package/src/lib/integrations/index.js +85 -0
  127. package/src/lib/integrations/openclaw-legacy.js +123 -0
  128. package/src/lib/integrations/openclaw-session.js +132 -0
  129. package/src/lib/integrations/opencode.js +86 -0
  130. package/src/lib/integrations/utils.js +39 -0
  131. package/src/lib/opencode-sqlite.js +113 -0
  132. package/src/lib/opencode-usage-audit.js +3 -2
  133. package/src/lib/rollout.js +227 -1
  134. package/src/lib/runtime-config.js +7 -5
  135. package/src/lib/vibeusage-api.js +11 -7
  136. package/src/shared/copy-registry.cjs +142 -0
  137. package/src/shared/copy-registry.cjs.d.ts +33 -0
  138. package/src/shared/runtime-defaults.cjs +11 -0
  139. package/src/shared/runtime-defaults.cjs.d.ts +3 -0
  140. package/src/shared/vibeusage-function-contract.cjs +34 -0
  141. package/src/shared/vibeusage-function-contract.cjs.d.ts +4 -0
  142. package/src/commands/activate-if-needed.js +0 -41
  143. package/src/lib/activation-check.js +0 -341
@@ -68,9 +68,11 @@ async function cmdSync(argv) {
68
68
  const xdgDataHome = process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
69
69
  const opencodeHome = process.env.OPENCODE_HOME || path.join(xdgDataHome, "opencode");
70
70
  const opencodeStorageDir = path.join(opencodeHome, "storage");
71
+ const opencodeDbPath = path.join(opencodeHome, "opencode.db");
71
72
 
72
- // OpenClaw hook integration: allow a hook to request incremental parsing for a single session jsonl.
73
- // We still parse all regular sources so model/source attribution stays complete (e.g. Kimi sessions).
73
+ // OpenClaw session-plugin integration: allow a plugin-triggered sync to request incremental parsing
74
+ // for a single session jsonl. We still parse all regular sources so model/source attribution stays
75
+ // complete (e.g. Kimi sessions).
74
76
  const openclawSignal = opts.fromOpenclaw
75
77
  ? resolveOpenclawSignal({ home, env: process.env })
76
78
  : null;
@@ -119,7 +121,7 @@ async function cmdSync(argv) {
119
121
 
120
122
  let openclawResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
121
123
  if (openclawFiles.length > 0) {
122
- // Only runs when explicitly triggered by OpenClaw hooks.
124
+ // Only runs when explicitly triggered by OpenClaw session-plugin events.
123
125
  openclawResult = await parseOpenclawIncremental({
124
126
  sessionFiles: openclawFiles,
125
127
  cursors,
@@ -194,29 +196,28 @@ async function cmdSync(argv) {
194
196
 
195
197
  const opencodeFiles = await listOpencodeMessageFiles(opencodeStorageDir);
196
198
  let opencodeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
197
- if (opencodeFiles.length > 0) {
198
- if (progress?.enabled) {
199
- progress.start(
200
- `Parsing Opencode ${renderBar(0)} 0/${formatNumber(opencodeFiles.length)} files | buckets 0`,
201
- );
202
- }
203
- opencodeResult = await parseOpencodeIncremental({
204
- messageFiles: opencodeFiles,
205
- cursors,
206
- queuePath,
207
- projectQueuePath,
208
- onProgress: (p) => {
209
- if (!progress?.enabled) return;
210
- const pct = p.total > 0 ? p.index / p.total : 1;
211
- progress.update(
212
- `Parsing Opencode ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
213
- p.total,
214
- )} files | buckets ${formatNumber(p.bucketsQueued)}`,
215
- );
216
- },
217
- source: "opencode",
218
- });
199
+ if (progress?.enabled && opencodeFiles.length > 0) {
200
+ progress.start(
201
+ `Parsing Opencode ${renderBar(0)} 0/${formatNumber(opencodeFiles.length)} files | buckets 0`,
202
+ );
219
203
  }
204
+ opencodeResult = await parseOpencodeIncremental({
205
+ messageFiles: opencodeFiles,
206
+ opencodeDbPath,
207
+ cursors,
208
+ queuePath,
209
+ projectQueuePath,
210
+ onProgress: (p) => {
211
+ if (!progress?.enabled) return;
212
+ const pct = p.total > 0 ? p.index / p.total : 1;
213
+ progress.update(
214
+ `Parsing Opencode ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
215
+ p.total,
216
+ )} files | buckets ${formatNumber(p.bucketsQueued)}`,
217
+ );
218
+ },
219
+ source: "opencode",
220
+ });
220
221
 
221
222
  if (cursors?.projectHourly?.projects && projectQueuePath && projectQueueStatePath) {
222
223
  for (const [projectKey, meta] of Object.entries(cursors.projectHourly.projects)) {
@@ -414,6 +415,17 @@ async function cmdSync(argv) {
414
415
  .filter(Boolean)
415
416
  .join("\n"),
416
417
  );
418
+ if (
419
+ opencodeResult.sqliteStatus === "missing-sqlite3" ||
420
+ opencodeResult.sqliteStatus === "query-failed"
421
+ ) {
422
+ const reason = opencodeResult.sqliteErrorCode
423
+ ? `${opencodeResult.sqliteStatus} (${opencodeResult.sqliteErrorCode})`
424
+ : opencodeResult.sqliteStatus;
425
+ process.stderr.write(
426
+ `Warning: OpenCode usage may be incomplete because the SQLite reader is ${reason}. Run \`vibeusage doctor\` for details.\n`,
427
+ );
428
+ }
417
429
  }
418
430
  } finally {
419
431
  progress?.stop();
@@ -2,73 +2,27 @@ const os = require("node:os");
2
2
  const path = require("node:path");
3
3
  const fs = require("node:fs/promises");
4
4
 
5
- const { restoreCodexNotify, restoreEveryCodeNotify } = require("../lib/codex-config");
6
- const { removeClaudeHook, buildClaudeHookCommand } = require("../lib/claude-config");
7
- const {
8
- resolveGeminiConfigDir,
9
- resolveGeminiSettingsPath,
10
- buildGeminiHookCommand,
11
- removeGeminiHook,
12
- } = require("../lib/gemini-config");
13
- const { resolveOpencodeConfigDir, removeOpencodePlugin } = require("../lib/opencode-config");
14
- const { removeOpenclawHookConfig } = require("../lib/openclaw-hook");
15
- const { removeOpenclawSessionPluginConfig } = require("../lib/openclaw-session-plugin");
16
5
  const { resolveTrackerPaths } = require("../lib/tracker-paths");
6
+ const { createIntegrationContext, uninstallIntegrations } = require("../lib/integrations");
17
7
 
18
8
  async function cmdUninstall(argv) {
19
9
  const opts = parseArgs(argv);
20
10
  const home = os.homedir();
21
11
  const { trackerDir, binDir } = await resolveTrackerPaths({ home });
22
- const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
23
- const codexConfigPath = path.join(codexHome, "config.toml");
24
- const codeHome = process.env.CODE_HOME || path.join(home, ".code");
25
- const codeConfigPath = path.join(codeHome, "config.toml");
26
- const claudeSettingsPath = path.join(home, ".claude", "settings.json");
27
- const geminiConfigDir = resolveGeminiConfigDir({ home, env: process.env });
28
- const geminiSettingsPath = resolveGeminiSettingsPath({ configDir: geminiConfigDir });
29
- const opencodeConfigDir = resolveOpencodeConfigDir({ home, env: process.env });
30
12
  const notifyPath = path.join(binDir, "notify.cjs");
31
- const notifyOriginalPath = path.join(trackerDir, "codex_notify_original.json");
32
- const codeNotifyOriginalPath = path.join(trackerDir, "code_notify_original.json");
33
- const codexNotifyCmd = ["/usr/bin/env", "node", notifyPath];
34
- const codeNotifyCmd = ["/usr/bin/env", "node", notifyPath, "--source=every-code"];
35
- const claudeHookCommand = buildClaudeHookCommand(notifyPath);
36
- const geminiHookCommand = buildGeminiHookCommand(notifyPath);
37
-
38
- const codexConfigExists = await isFile(codexConfigPath);
39
- const codeConfigExists = await isFile(codeConfigPath);
40
- const claudeConfigExists = await isFile(claudeSettingsPath);
41
- const geminiConfigExists = await isDir(geminiConfigDir);
42
- const opencodeConfigExists = await isDir(opencodeConfigDir);
43
- const codexRestore = codexConfigExists
44
- ? await restoreCodexNotify({
45
- codexConfigPath,
46
- notifyOriginalPath,
47
- notifyCmd: codexNotifyCmd,
48
- })
49
- : { restored: false, skippedReason: "config-missing" };
50
- const codeRestore = codeConfigExists
51
- ? await restoreEveryCodeNotify({
52
- codeConfigPath,
53
- notifyOriginalPath: codeNotifyOriginalPath,
54
- notifyCmd: codeNotifyCmd,
55
- })
56
- : { restored: false, skippedReason: "config-missing" };
57
- const claudeRemove = claudeConfigExists
58
- ? await removeClaudeHook({ settingsPath: claudeSettingsPath, hookCommand: claudeHookCommand })
59
- : { removed: false, skippedReason: "config-missing" };
60
- const geminiRemove = geminiConfigExists
61
- ? await removeGeminiHook({ settingsPath: geminiSettingsPath, hookCommand: geminiHookCommand })
62
- : { removed: false, skippedReason: "config-missing" };
63
- const opencodeRemove = opencodeConfigExists
64
- ? await removeOpencodePlugin({ configDir: opencodeConfigDir })
65
- : { removed: false, skippedReason: "config-missing" };
66
- const openclawSessionPluginRemove = await removeOpenclawSessionPluginConfig({
13
+ const integrationContext = await createIntegrationContext({
67
14
  home,
68
- trackerDir,
69
15
  env: process.env,
16
+ trackerPaths: { trackerDir, binDir, rootDir: path.dirname(trackerDir) },
17
+ notifyPath,
70
18
  });
71
- const openclawHookRemove = await removeOpenclawHookConfig({ home, trackerDir, env: process.env });
19
+ const codexConfigExists = await isFile(integrationContext.codex.configPath);
20
+ const codeConfigExists = await isFile(integrationContext.everyCode.configPath);
21
+ const claudeConfigExists = await isFile(integrationContext.claude.settingsPath);
22
+ const geminiConfigExists = await isDir(integrationContext.gemini.configDir);
23
+ const opencodeConfigExists = await isDir(integrationContext.opencode.configDir);
24
+ const integrationResults = await uninstallIntegrations(integrationContext);
25
+ const resultByName = new Map(integrationResults.map((result) => [result.name, result]));
72
26
 
73
27
  // Remove installed notify handler.
74
28
  await fs.unlink(notifyPath).catch(() => {});
@@ -83,53 +37,73 @@ async function cmdUninstall(argv) {
83
37
  process.stdout.write(
84
38
  [
85
39
  "Uninstalled:",
86
- codexConfigExists
87
- ? codexRestore?.restored
88
- ? `- Codex notify restored: ${codexConfigPath}`
89
- : codexRestore?.skippedReason === "no-backup-not-installed"
90
- ? "- Codex notify: skipped (no backup; not installed)"
91
- : "- Codex notify: no change"
92
- : "- Codex notify: skipped (config.toml not found)",
93
- codeConfigExists
94
- ? codeRestore?.restored
95
- ? `- Every Code notify restored: ${codeConfigPath}`
96
- : codeRestore?.skippedReason === "no-backup-not-installed"
97
- ? "- Every Code notify: skipped (no backup; not installed)"
98
- : "- Every Code notify: no change"
99
- : "- Every Code notify: skipped (config.toml not found)",
100
- claudeConfigExists
101
- ? claudeRemove?.removed
102
- ? `- Claude hooks removed: ${claudeSettingsPath}`
103
- : claudeRemove?.skippedReason === "hook-missing"
104
- ? "- Claude hooks: no change"
105
- : "- Claude hooks: skipped"
106
- : "- Claude hooks: skipped (settings.json not found)",
107
- geminiConfigExists
108
- ? geminiRemove?.removed
109
- ? `- Gemini hooks removed: ${geminiSettingsPath}`
110
- : geminiRemove?.skippedReason === "hook-missing"
111
- ? "- Gemini hooks: no change"
112
- : "- Gemini hooks: skipped"
113
- : `- Gemini hooks: skipped (${geminiConfigDir} not found)`,
114
- opencodeConfigExists
115
- ? opencodeRemove?.removed
116
- ? `- Opencode plugin removed: ${opencodeConfigDir}`
117
- : opencodeRemove?.skippedReason === "plugin-missing"
118
- ? "- Opencode plugin: no change"
119
- : opencodeRemove?.skippedReason === "unexpected-content"
120
- ? "- Opencode plugin: skipped (unexpected content)"
121
- : "- Opencode plugin: skipped"
122
- : `- Opencode plugin: skipped (${opencodeConfigDir} not found)`,
123
- openclawSessionPluginRemove?.removed
124
- ? `- OpenClaw session plugin removed: ${openclawSessionPluginRemove.openclawConfigPath}`
125
- : openclawSessionPluginRemove?.skippedReason === "openclaw-config-missing"
126
- ? "- OpenClaw session plugin: skipped (openclaw config not found)"
127
- : "- OpenClaw session plugin: no change",
128
- openclawHookRemove?.removed
129
- ? `- OpenClaw hook (legacy) removed: ${openclawHookRemove.openclawConfigPath}`
130
- : openclawHookRemove?.skippedReason === "openclaw-config-missing"
131
- ? "- OpenClaw hook (legacy): skipped (openclaw config not found)"
132
- : "- OpenClaw hook (legacy): no change",
40
+ renderRestoreLine({
41
+ exists: codexConfigExists,
42
+ result: resultByName.get("codex"),
43
+ missingText: "- Codex notify: skipped (config.toml not found)",
44
+ restoredText: (result) =>
45
+ `- Codex notify restored: ${result.detail || integrationContext.codex.configPath}`,
46
+ noChangeText: "- Codex notify: no change",
47
+ skippedText: "- Codex notify: skipped (no backup; not installed)",
48
+ }),
49
+ renderRestoreLine({
50
+ exists: codeConfigExists,
51
+ result: resultByName.get("every-code"),
52
+ missingText: "- Every Code notify: skipped (config.toml not found)",
53
+ restoredText: (result) =>
54
+ `- Every Code notify restored: ${result.detail || integrationContext.everyCode.configPath}`,
55
+ noChangeText: "- Every Code notify: no change",
56
+ skippedText: "- Every Code notify: skipped (no backup; not installed)",
57
+ }),
58
+ renderHookLine({
59
+ exists: claudeConfigExists,
60
+ result: resultByName.get("claude"),
61
+ missingText: "- Claude hooks: skipped (settings.json not found)",
62
+ removedText: (result) =>
63
+ `- Claude hooks removed: ${result.detail || integrationContext.claude.settingsPath}`,
64
+ noChangeText: "- Claude hooks: no change",
65
+ skippedText: "- Claude hooks: skipped",
66
+ }),
67
+ renderHookLine({
68
+ exists: geminiConfigExists,
69
+ result: resultByName.get("gemini"),
70
+ missingText: `- Gemini hooks: skipped (${integrationContext.gemini.configDir} not found)`,
71
+ removedText: (result) =>
72
+ `- Gemini hooks removed: ${result.detail || integrationContext.gemini.settingsPath}`,
73
+ noChangeText: "- Gemini hooks: no change",
74
+ skippedText: "- Gemini hooks: skipped",
75
+ }),
76
+ renderHookLine({
77
+ exists: opencodeConfigExists,
78
+ result: resultByName.get("opencode"),
79
+ missingText: `- Opencode plugin: skipped (${integrationContext.opencode.configDir} not found)`,
80
+ removedText: (result) =>
81
+ `- Opencode plugin removed: ${result.detail || integrationContext.opencode.configDir}`,
82
+ noChangeText: "- Opencode plugin: no change",
83
+ skippedText: "- Opencode plugin: skipped (unexpected content)",
84
+ }),
85
+ renderHookLine({
86
+ exists: true,
87
+ result: resultByName.get("openclaw-session"),
88
+ missingText: "- OpenClaw session plugin: skipped (openclaw config not found)",
89
+ removedText: (result) =>
90
+ `- OpenClaw session plugin removed: ${result.detail || result.openclawConfigPath || "unknown"}`,
91
+ noChangeText: "- OpenClaw session plugin: no change",
92
+ skippedText: "- OpenClaw session plugin: no change",
93
+ unreadableText: (result) =>
94
+ `- OpenClaw session plugin: skipped (${result.detail || "openclaw config unreadable"})`,
95
+ }),
96
+ renderHookLine({
97
+ exists: true,
98
+ result: resultByName.get("openclaw-legacy"),
99
+ missingText: "- OpenClaw hook (legacy): skipped (openclaw config not found)",
100
+ removedText: (result) =>
101
+ `- OpenClaw hook (legacy) removed: ${result.detail || result.openclawConfigPath || "unknown"}`,
102
+ noChangeText: "- OpenClaw hook (legacy): no change",
103
+ skippedText: "- OpenClaw hook (legacy): no change",
104
+ unreadableText: (result) =>
105
+ `- OpenClaw hook (legacy): skipped (${result.detail || "openclaw config unreadable"})`,
106
+ }),
133
107
  opts.purge ? `- Purged: ${path.join(home, ".vibeusage")}` : "- Purge: skipped (use --purge)",
134
108
  "",
135
109
  ].join("\n"),
@@ -157,6 +131,49 @@ async function isFile(p) {
157
131
  }
158
132
  }
159
133
 
134
+ function renderRestoreLine({ exists, result, missingText, restoredText, noChangeText, skippedText }) {
135
+ if (!exists) return missingText;
136
+ if (!result) return noChangeText;
137
+ if (result.status === "restored" || result.status === "removed") {
138
+ return restoredText(result);
139
+ }
140
+ if (result.skippedReason === "no-backup-not-installed") {
141
+ return skippedText;
142
+ }
143
+ return noChangeText;
144
+ }
145
+
146
+ function renderHookLine({
147
+ exists,
148
+ result,
149
+ missingText,
150
+ removedText,
151
+ noChangeText,
152
+ skippedText,
153
+ unreadableText = null,
154
+ }) {
155
+ if (!exists) return missingText;
156
+ if (!result) return noChangeText;
157
+ if (result.status === "removed" || result.status === "updated") {
158
+ return removedText(result);
159
+ }
160
+ if (result.skippedReason === "hook-missing") {
161
+ return noChangeText;
162
+ }
163
+ if (result.skippedReason === "unexpected-content") {
164
+ return skippedText;
165
+ }
166
+ if (result.skippedReason === "openclaw-config-missing") {
167
+ return missingText;
168
+ }
169
+ if (result.skippedReason === "openclaw-config-unreadable") {
170
+ return typeof unreadableText === "function"
171
+ ? unreadableText(result)
172
+ : unreadableText || skippedText;
173
+ }
174
+ return noChangeText;
175
+ }
176
+
160
177
  async function isDir(p) {
161
178
  try {
162
179
  const st = await fs.stat(p);
@@ -3,57 +3,64 @@ const path = require("node:path");
3
3
 
4
4
  const { ensureDir, readJson, writeJson } = require("./fs");
5
5
 
6
- const DEFAULT_EVENT = "SessionEnd";
6
+ const DEFAULT_EVENTS = ["Stop", "SessionEnd"];
7
7
 
8
- async function upsertClaudeHook({ settingsPath, hookCommand, event = DEFAULT_EVENT }) {
8
+ async function upsertClaudeHook({ settingsPath, hookCommand, events = DEFAULT_EVENTS }) {
9
9
  const existing = await readJson(settingsPath);
10
10
  const settings = normalizeSettings(existing);
11
11
  const hooks = normalizeHooks(settings.hooks);
12
- const entries = normalizeEntries(hooks[event]);
13
-
14
- const normalized = normalizeEntriesForCommand(entries, hookCommand);
15
- if (normalized.changed) {
16
- const nextHooks = { ...hooks, [event]: normalized.entries };
17
- const nextSettings = { ...settings, hooks: nextHooks };
18
- const backupPath = await writeClaudeSettings({ settingsPath, settings: nextSettings });
19
- return { changed: true, backupPath };
20
- }
12
+ const targetEvents = normalizeEvents(events);
13
+ let changed = false;
14
+ const nextHooks = { ...hooks };
15
+
16
+ for (const event of targetEvents) {
17
+ const entries = normalizeEntries(nextHooks[event]);
18
+ const normalized = normalizeEntriesForCommand(entries, hookCommand);
19
+ if (normalized.changed) {
20
+ nextHooks[event] = normalized.entries;
21
+ changed = true;
22
+ continue;
23
+ }
24
+
25
+ if (hasHook(entries, hookCommand)) continue;
21
26
 
22
- if (hasHook(entries, hookCommand)) {
23
- return { changed: false, backupPath: null };
27
+ nextHooks[event] = entries.concat([{ hooks: [{ type: "command", command: hookCommand }] }]);
28
+ changed = true;
24
29
  }
25
30
 
26
- const nextEntries = entries.concat([{ hooks: [{ type: "command", command: hookCommand }] }]);
27
- const nextHooks = { ...hooks, [event]: nextEntries };
28
- const nextSettings = { ...settings, hooks: nextHooks };
31
+ if (!changed) return { changed: false, backupPath: null };
29
32
 
33
+ const nextSettings = { ...settings, hooks: nextHooks };
30
34
  const backupPath = await writeClaudeSettings({ settingsPath, settings: nextSettings });
31
35
  return { changed: true, backupPath };
32
36
  }
33
37
 
34
- async function removeClaudeHook({ settingsPath, hookCommand, event = DEFAULT_EVENT }) {
38
+ async function removeClaudeHook({ settingsPath, hookCommand, events = DEFAULT_EVENTS }) {
35
39
  const existing = await readJson(settingsPath);
36
40
  if (!existing) return { removed: false, skippedReason: "settings-missing" };
37
41
 
38
42
  const settings = normalizeSettings(existing);
39
43
  const hooks = normalizeHooks(settings.hooks);
40
- const entries = normalizeEntries(hooks[event]);
41
- if (entries.length === 0) return { removed: false, skippedReason: "hook-missing" };
42
-
44
+ const targetEvents = normalizeEvents(events);
43
45
  let removed = false;
44
- const nextEntries = [];
45
- for (const entry of entries) {
46
- const res = stripHookFromEntry(entry, hookCommand);
47
- if (res.removed) removed = true;
48
- if (res.entry) nextEntries.push(res.entry);
46
+ const nextHooks = { ...hooks };
47
+ for (const event of targetEvents) {
48
+ const entries = normalizeEntries(hooks[event]);
49
+ if (entries.length === 0) continue;
50
+
51
+ const nextEntries = [];
52
+ for (const entry of entries) {
53
+ const res = stripHookFromEntry(entry, hookCommand);
54
+ if (res.removed) removed = true;
55
+ if (res.entry) nextEntries.push(res.entry);
56
+ }
57
+
58
+ if (nextEntries.length > 0) nextHooks[event] = nextEntries;
59
+ else delete nextHooks[event];
49
60
  }
50
61
 
51
62
  if (!removed) return { removed: false, skippedReason: "hook-missing" };
52
63
 
53
- const nextHooks = { ...hooks };
54
- if (nextEntries.length > 0) nextHooks[event] = nextEntries;
55
- else delete nextHooks[event];
56
-
57
64
  const nextSettings = { ...settings };
58
65
  if (Object.keys(nextHooks).length > 0) nextSettings.hooks = nextHooks;
59
66
  else delete nextSettings.hooks;
@@ -62,13 +69,31 @@ async function removeClaudeHook({ settingsPath, hookCommand, event = DEFAULT_EVE
62
69
  return { removed: true, skippedReason: null, backupPath };
63
70
  }
64
71
 
65
- async function isClaudeHookConfigured({ settingsPath, hookCommand, event = DEFAULT_EVENT }) {
72
+ async function isClaudeHookConfigured({ settingsPath, hookCommand, events = DEFAULT_EVENTS }) {
73
+ const probe = await probeClaudeHook({ settingsPath, hookCommand, events });
74
+ return probe.configured;
75
+ }
76
+
77
+ async function probeClaudeHook({ settingsPath, hookCommand, events = DEFAULT_EVENTS }) {
66
78
  const settings = await readJson(settingsPath);
67
- if (!settings || typeof settings !== "object") return false;
79
+ if (!settings || typeof settings !== "object") {
80
+ return { configured: false, anyPresent: false, eventStates: {} };
81
+ }
68
82
  const hooks = settings.hooks;
69
- if (!hooks || typeof hooks !== "object") return false;
70
- const entries = normalizeEntries(hooks[event]);
71
- return hasHook(entries, hookCommand);
83
+ if (!hooks || typeof hooks !== "object") {
84
+ return { configured: false, anyPresent: false, eventStates: {} };
85
+ }
86
+ const targetEvents = normalizeEvents(events);
87
+ const eventStates = {};
88
+ for (const event of targetEvents) {
89
+ eventStates[event] = hasHook(normalizeEntries(hooks[event]), hookCommand);
90
+ }
91
+ const anyPresent = Object.values(eventStates).some(Boolean);
92
+ return {
93
+ configured: targetEvents.every((event) => eventStates[event] === true),
94
+ anyPresent,
95
+ eventStates,
96
+ };
72
97
  }
73
98
 
74
99
  function buildClaudeHookCommand(notifyPath) {
@@ -88,9 +113,27 @@ function normalizeEntries(raw) {
88
113
  return Array.isArray(raw) ? raw.slice() : [];
89
114
  }
90
115
 
116
+ function normalizeEvents(raw) {
117
+ const values = Array.isArray(raw) ? raw : [raw];
118
+ const out = [];
119
+ for (const value of values) {
120
+ if (typeof value !== "string") continue;
121
+ const normalized = value.trim();
122
+ if (!normalized || out.includes(normalized)) continue;
123
+ out.push(normalized);
124
+ }
125
+ return out.length > 0 ? out : DEFAULT_EVENTS.slice();
126
+ }
127
+
91
128
  function normalizeCommand(cmd) {
92
129
  if (Array.isArray(cmd)) return cmd.map((v) => String(v)).join("\u0000");
93
- if (typeof cmd === "string") return cmd.trim();
130
+ if (typeof cmd === "string") {
131
+ const raw = cmd.trim();
132
+ if (!raw) return null;
133
+ const parsed = splitShellCommand(raw);
134
+ if (parsed) return parsed.join("\u0000");
135
+ return raw;
136
+ }
94
137
  return null;
95
138
  }
96
139
 
@@ -166,6 +209,56 @@ function quoteArg(value) {
166
209
  return `"${v.replace(/"/g, '\\"')}"`;
167
210
  }
168
211
 
212
+ function splitShellCommand(raw) {
213
+ const parts = [];
214
+ let current = "";
215
+ let quote = null;
216
+ let escaping = false;
217
+
218
+ for (let i = 0; i < raw.length; i += 1) {
219
+ const ch = raw[i];
220
+
221
+ if (escaping) {
222
+ current += ch;
223
+ escaping = false;
224
+ continue;
225
+ }
226
+
227
+ if (ch === "\\") {
228
+ escaping = true;
229
+ continue;
230
+ }
231
+
232
+ if (quote) {
233
+ if (ch === quote) {
234
+ quote = null;
235
+ } else {
236
+ current += ch;
237
+ }
238
+ continue;
239
+ }
240
+
241
+ if (ch === '"' || ch === "'") {
242
+ quote = ch;
243
+ continue;
244
+ }
245
+
246
+ if (/\s/.test(ch)) {
247
+ if (current) {
248
+ parts.push(current);
249
+ current = "";
250
+ }
251
+ continue;
252
+ }
253
+
254
+ current += ch;
255
+ }
256
+
257
+ if (escaping || quote) return null;
258
+ if (current) parts.push(current);
259
+ return parts.length > 0 ? parts : null;
260
+ }
261
+
169
262
  async function writeClaudeSettings({ settingsPath, settings }) {
170
263
  await ensureDir(path.dirname(settingsPath));
171
264
  let backupPath = null;
@@ -183,8 +276,10 @@ async function writeClaudeSettings({ settingsPath, settings }) {
183
276
  }
184
277
 
185
278
  module.exports = {
279
+ DEFAULT_EVENTS,
186
280
  upsertClaudeHook,
187
281
  removeClaudeHook,
188
282
  isClaudeHookConfigured,
283
+ probeClaudeHook,
189
284
  buildClaudeHookCommand,
190
285
  };