vibeusage 0.2.23 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,26 +3,23 @@ const path = require("node:path");
3
3
  const fs = require("node:fs/promises");
4
4
 
5
5
  const { readJson } = require("./fs");
6
- const { readCodexNotify, readEveryCodeNotify } = require("./codex-config");
7
- const { isClaudeHookConfigured, buildClaudeHookCommand } = require("./claude-config");
8
- const {
9
- resolveGeminiConfigDir,
10
- resolveGeminiSettingsPath,
11
- buildGeminiHookCommand,
12
- isGeminiHookConfigured,
13
- } = require("./gemini-config");
14
- const { resolveOpencodeConfigDir, isOpencodePluginInstalled } = require("./opencode-config");
15
6
  const { normalizeState: normalizeUploadState } = require("./upload-throttle");
16
- const { probeOpenclawHookState } = require("./openclaw-hook");
17
- const { probeOpenclawSessionPluginState } = require("./openclaw-session-plugin");
18
- const { resolveTrackerPaths } = require("./tracker-paths");
7
+ const { createIntegrationContext, probeIntegrations } = require("./integrations");
19
8
 
20
9
  async function collectTrackerDiagnostics({
21
10
  home = os.homedir(),
22
11
  codexHome = process.env.CODEX_HOME || path.join(home, ".codex"),
23
12
  codeHome = process.env.CODE_HOME || path.join(home, ".code"),
24
13
  } = {}) {
25
- const { trackerDir, binDir } = await resolveTrackerPaths({ home });
14
+ const integrationContext = await createIntegrationContext({
15
+ home,
16
+ env: {
17
+ ...process.env,
18
+ CODEX_HOME: codexHome,
19
+ CODE_HOME: codeHome,
20
+ },
21
+ });
22
+ const trackerDir = integrationContext.trackerPaths.trackerDir;
26
23
  const configPath = path.join(trackerDir, "config.json");
27
24
  const queuePath = path.join(trackerDir, "queue.jsonl");
28
25
  const queueStatePath = path.join(trackerDir, "queue.state.json");
@@ -34,16 +31,14 @@ async function collectTrackerDiagnostics({
34
31
  const autoRetryPath = path.join(trackerDir, "auto.retry.json");
35
32
  const codexConfigPath = path.join(codexHome, "config.toml");
36
33
  const codeConfigPath = path.join(codeHome, "config.toml");
37
- const claudeConfigPath = path.join(home, ".claude", "settings.json");
38
- const geminiConfigDir = resolveGeminiConfigDir({ home, env: process.env });
39
- const geminiSettingsPath = resolveGeminiSettingsPath({ configDir: geminiConfigDir });
40
- const opencodeConfigDir = resolveOpencodeConfigDir({ home, env: process.env });
41
34
 
42
35
  const config = await readJson(configPath);
43
36
  const cursors = await readJson(cursorsPath);
44
37
  const queueState = (await readJson(queueStatePath)) || { offset: 0 };
45
38
  const uploadThrottle = normalizeUploadState(await readJson(uploadThrottlePath));
46
39
  const autoRetry = await readJson(autoRetryPath);
40
+ const probes = await probeIntegrations(integrationContext);
41
+ const probeByName = new Map(probes.map((probe) => [probe.name, probe]));
47
42
 
48
43
  const queueSize = await safeStatSize(queuePath);
49
44
  const offsetBytes = Number(queueState.offset || 0);
@@ -53,33 +48,20 @@ async function collectTrackerDiagnostics({
53
48
  const lastOpenclawSync = (await safeReadText(openclawSignalPath))?.trim() || null;
54
49
  const lastNotifySpawn = parseEpochMsToIso((await safeReadText(throttlePath))?.trim() || null);
55
50
 
56
- const codexNotifyRaw = await readCodexNotify(codexConfigPath);
57
- const notifyConfigured = Array.isArray(codexNotifyRaw) && codexNotifyRaw.length > 0;
58
- const codexNotify = notifyConfigured ? codexNotifyRaw.map((v) => redactValue(v, home)) : null;
59
- const everyCodeNotifyRaw = await readEveryCodeNotify(codeConfigPath);
60
- const everyCodeConfigured = Array.isArray(everyCodeNotifyRaw) && everyCodeNotifyRaw.length > 0;
61
- const everyCodeNotify = everyCodeConfigured
62
- ? everyCodeNotifyRaw.map((v) => redactValue(v, home))
51
+ const codexProbe = probeByName.get("codex");
52
+ const everyCodeProbe = probeByName.get("every-code");
53
+ const claudeProbe = probeByName.get("claude");
54
+ const geminiProbe = probeByName.get("gemini");
55
+ const opencodeProbe = probeByName.get("opencode");
56
+ const openclawSessionProbe = probeByName.get("openclaw-session");
57
+ const openclawLegacyProbe = probeByName.get("openclaw-legacy");
58
+
59
+ const codexNotify = Array.isArray(codexProbe?.currentNotify)
60
+ ? codexProbe.currentNotify.map((value) => redactValue(value, home))
61
+ : null;
62
+ const everyCodeNotify = Array.isArray(everyCodeProbe?.currentNotify)
63
+ ? everyCodeProbe.currentNotify.map((value) => redactValue(value, home))
63
64
  : null;
64
- const claudeHookCommand = buildClaudeHookCommand(path.join(binDir, "notify.cjs"));
65
- const claudeHookConfigured = await isClaudeHookConfigured({
66
- settingsPath: claudeConfigPath,
67
- hookCommand: claudeHookCommand,
68
- });
69
- const geminiHookCommand = buildGeminiHookCommand(path.join(binDir, "notify.cjs"));
70
- const geminiHookConfigured = await isGeminiHookConfigured({
71
- settingsPath: geminiSettingsPath,
72
- hookCommand: geminiHookCommand,
73
- });
74
- const opencodePluginConfigured = await isOpencodePluginInstalled({
75
- configDir: opencodeConfigDir,
76
- });
77
- const openclawSessionPluginState = await probeOpenclawSessionPluginState({
78
- home,
79
- trackerDir,
80
- env: process.env,
81
- });
82
- const openclawHookState = await probeOpenclawHookState({ home, trackerDir, env: process.env });
83
65
 
84
66
  const lastSuccessAt = uploadThrottle.lastSuccessMs
85
67
  ? new Date(uploadThrottle.lastSuccessMs).toISOString()
@@ -101,9 +83,9 @@ async function collectTrackerDiagnostics({
101
83
  codex_config: redactValue(codexConfigPath, home),
102
84
  code_home: redactValue(codeHome, home),
103
85
  code_config: redactValue(codeConfigPath, home),
104
- claude_config: redactValue(claudeConfigPath, home),
105
- gemini_config: redactValue(geminiSettingsPath, home),
106
- opencode_config: redactValue(opencodeConfigDir, home),
86
+ claude_config: redactValue(integrationContext.claude.settingsPath, home),
87
+ gemini_config: redactValue(integrationContext.gemini.settingsPath, home),
88
+ opencode_config: redactValue(integrationContext.opencode.configDir, home),
107
89
  },
108
90
  config: {
109
91
  base_url: typeof config?.baseUrl === "string" ? config.baseUrl : null,
@@ -128,19 +110,34 @@ async function collectTrackerDiagnostics({
128
110
  last_notify: lastNotify,
129
111
  last_openclaw_triggered_sync: lastOpenclawSync,
130
112
  last_notify_triggered_sync: lastNotifySpawn,
131
- codex_notify_configured: notifyConfigured,
113
+ codex_notify_status: codexProbe?.status || "unknown",
114
+ codex_notify_configured: Boolean(codexProbe?.configured),
132
115
  codex_notify: codexNotify,
133
- every_code_notify_configured: everyCodeConfigured,
116
+ every_code_notify_status: everyCodeProbe?.status || "unknown",
117
+ every_code_notify_configured: Boolean(everyCodeProbe?.configured),
134
118
  every_code_notify: everyCodeNotify,
135
- claude_hook_configured: claudeHookConfigured,
136
- gemini_hook_configured: geminiHookConfigured,
137
- opencode_plugin_configured: opencodePluginConfigured,
138
- openclaw_session_plugin_configured: Boolean(openclawSessionPluginState?.configured),
139
- openclaw_session_plugin_linked: Boolean(openclawSessionPluginState?.linked),
140
- openclaw_session_plugin_enabled: Boolean(openclawSessionPluginState?.enabled),
141
- openclaw_hook_configured: Boolean(openclawHookState?.configured),
142
- openclaw_hook_linked: Boolean(openclawHookState?.linked),
143
- openclaw_hook_enabled: Boolean(openclawHookState?.enabled),
119
+ claude_hook_status: claudeProbe?.status || "unknown",
120
+ claude_hook_configured: Boolean(claudeProbe?.configured),
121
+ gemini_hook_status: geminiProbe?.status || "unknown",
122
+ gemini_hook_configured: Boolean(geminiProbe?.configured),
123
+ opencode_plugin_status: opencodeProbe?.status || "unknown",
124
+ opencode_plugin_configured: Boolean(opencodeProbe?.configured),
125
+ openclaw_session_plugin_status: openclawSessionProbe?.status || "unknown",
126
+ openclaw_session_plugin_configured: Boolean(openclawSessionProbe?.configured),
127
+ openclaw_session_plugin_linked: Boolean(openclawSessionProbe?.linked),
128
+ openclaw_session_plugin_enabled: Boolean(openclawSessionProbe?.enabled),
129
+ openclaw_session_plugin_detail:
130
+ typeof openclawSessionProbe?.detail === "string"
131
+ ? redactError(openclawSessionProbe.detail, home)
132
+ : null,
133
+ openclaw_hook_status: openclawLegacyProbe?.status || "unknown",
134
+ openclaw_hook_configured: Boolean(openclawLegacyProbe?.configured),
135
+ openclaw_hook_linked: Boolean(openclawLegacyProbe?.linked),
136
+ openclaw_hook_enabled: Boolean(openclawLegacyProbe?.enabled),
137
+ openclaw_hook_detail:
138
+ typeof openclawLegacyProbe?.detail === "string"
139
+ ? redactError(openclawLegacyProbe.detail, home)
140
+ : null,
144
141
  },
145
142
  upload: {
146
143
  last_success_at: lastSuccessAt,
package/src/lib/doctor.js CHANGED
@@ -313,6 +313,7 @@ function buildDiagnosticsChecks(diagnostics) {
313
313
  notify.claude_hook_configured ||
314
314
  notify.gemini_hook_configured ||
315
315
  notify.opencode_plugin_configured ||
316
+ notify.openclaw_session_plugin_configured ||
316
317
  notify.openclaw_hook_configured,
317
318
  );
318
319
 
@@ -324,6 +325,32 @@ function buildDiagnosticsChecks(diagnostics) {
324
325
  meta: { configured: notifyConfigured },
325
326
  });
326
327
 
328
+ if (notify.openclaw_session_plugin_status === "unreadable") {
329
+ checks.push({
330
+ id: "notify.openclaw_session_plugin",
331
+ status: "warn",
332
+ detail: "OpenClaw session plugin config unreadable",
333
+ critical: false,
334
+ meta: {
335
+ status: notify.openclaw_session_plugin_status,
336
+ detail: notify.openclaw_session_plugin_detail || null,
337
+ },
338
+ });
339
+ }
340
+
341
+ if (notify.openclaw_hook_status === "unreadable") {
342
+ checks.push({
343
+ id: "notify.openclaw_hook",
344
+ status: "warn",
345
+ detail: "OpenClaw hook config unreadable",
346
+ critical: false,
347
+ meta: {
348
+ status: notify.openclaw_hook_status,
349
+ detail: notify.openclaw_hook_detail || null,
350
+ },
351
+ });
352
+ }
353
+
327
354
  const uploadError = diagnostics?.upload?.last_error || null;
328
355
  checks.push({
329
356
  id: "upload.last_error",
@@ -0,0 +1,106 @@
1
+ const { probeClaudeHook, upsertClaudeHook, removeClaudeHook } = require("../claude-config");
2
+ const { isDir, isFile } = require("./utils");
3
+
4
+ module.exports = {
5
+ name: "claude",
6
+ summaryLabel: "Claude",
7
+ statusLabel: "Claude hooks",
8
+ async probe(ctx) {
9
+ const hasConfigDir = await isDir(ctx.claude.configDir);
10
+ if (!hasConfigDir) {
11
+ return baseProbe(this, { status: "not_installed", detail: "Config not found" });
12
+ }
13
+
14
+ const hasSettings = await isFile(ctx.claude.settingsPath);
15
+ if (!hasSettings) {
16
+ return baseProbe(this, { status: "drifted", detail: "Run vibeusage init to install hooks" });
17
+ }
18
+
19
+ const hookState = await probeClaudeHook({
20
+ settingsPath: ctx.claude.settingsPath,
21
+ hookCommand: ctx.claude.hookCommand,
22
+ });
23
+
24
+ if (hookState.configured) {
25
+ return baseProbe(this, {
26
+ status: "ready",
27
+ detail: "Hooks installed",
28
+ configured: true,
29
+ hookState,
30
+ });
31
+ }
32
+
33
+ const sessionEndPresent = hookState.eventStates?.SessionEnd === true;
34
+ const stopPresent = hookState.eventStates?.Stop === true;
35
+ const status = hookState.anyPresent && sessionEndPresent && !stopPresent
36
+ ? "unsupported_legacy"
37
+ : "drifted";
38
+ return baseProbe(this, {
39
+ status,
40
+ detail:
41
+ status === "unsupported_legacy"
42
+ ? "Legacy hook config detected; run vibeusage init"
43
+ : "Run vibeusage init to reconcile hooks",
44
+ hookState,
45
+ });
46
+ },
47
+ async install(ctx) {
48
+ if (!(await isDir(ctx.claude.configDir))) {
49
+ return action(this, "skipped", false, "Config not found");
50
+ }
51
+ const result = await upsertClaudeHook({
52
+ settingsPath: ctx.claude.settingsPath,
53
+ hookCommand: ctx.claude.hookCommand,
54
+ });
55
+ return action(
56
+ this,
57
+ result.changed ? "installed" : "set",
58
+ Boolean(result.changed),
59
+ result.changed ? "Hooks installed" : "Hooks already installed",
60
+ );
61
+ },
62
+ async uninstall(ctx) {
63
+ if (!(await isFile(ctx.claude.settingsPath))) {
64
+ return action(this, "skipped", false, "settings.json not found");
65
+ }
66
+ const result = await removeClaudeHook({
67
+ settingsPath: ctx.claude.settingsPath,
68
+ hookCommand: ctx.claude.hookCommand,
69
+ });
70
+ if (result.removed) {
71
+ return action(this, "removed", true, ctx.claude.settingsPath);
72
+ }
73
+ if (result.skippedReason === "hook-missing") {
74
+ return action(this, "unchanged", false, "no change", {
75
+ skippedReason: result.skippedReason,
76
+ });
77
+ }
78
+ return action(this, "skipped", false, "settings.json not found");
79
+ },
80
+ renderStatusValue(probe) {
81
+ if (probe.status === "ready") return "set";
82
+ if (probe.status === "not_installed") return "unset";
83
+ return probe.status;
84
+ },
85
+ };
86
+
87
+ function baseProbe(descriptor, values) {
88
+ return {
89
+ name: descriptor.name,
90
+ summaryLabel: descriptor.summaryLabel,
91
+ statusLabel: descriptor.statusLabel,
92
+ configured: false,
93
+ ...values,
94
+ };
95
+ }
96
+
97
+ function action(descriptor, status, changed, detail, extras = {}) {
98
+ return {
99
+ name: descriptor.name,
100
+ label: descriptor.summaryLabel,
101
+ status,
102
+ changed,
103
+ detail,
104
+ ...extras,
105
+ };
106
+ }
@@ -0,0 +1,88 @@
1
+ const {
2
+ readCodexNotify,
3
+ upsertCodexNotify,
4
+ restoreCodexNotify,
5
+ } = require("../codex-config");
6
+ const { arraysEqual, isFile } = require("./utils");
7
+
8
+ module.exports = {
9
+ name: "codex",
10
+ summaryLabel: "Codex CLI",
11
+ statusLabel: "Codex notify",
12
+ async probe(ctx) {
13
+ const installed = await isFile(ctx.codex.configPath);
14
+ if (!installed) {
15
+ return baseProbe(this, { status: "not_installed", detail: "Config not found" });
16
+ }
17
+
18
+ const currentNotify = await readCodexNotify(ctx.codex.configPath);
19
+ const ready = arraysEqual(currentNotify, ctx.codex.notifyCmd);
20
+ return baseProbe(this, {
21
+ status: ready ? "ready" : "drifted",
22
+ detail: ready ? "Config already set" : "Run vibeusage init to reconcile notify",
23
+ configured: ready,
24
+ currentNotify,
25
+ });
26
+ },
27
+ async install(ctx) {
28
+ if (!(await isFile(ctx.codex.configPath))) {
29
+ return action(this, "skipped", false, "Config not found");
30
+ }
31
+ const result = await upsertCodexNotify({
32
+ codexConfigPath: ctx.codex.configPath,
33
+ notifyCmd: ctx.codex.notifyCmd,
34
+ notifyOriginalPath: ctx.codex.notifyOriginalPath,
35
+ });
36
+ return action(
37
+ this,
38
+ result.changed ? "updated" : "set",
39
+ Boolean(result.changed),
40
+ result.changed ? "Updated config" : "Config already set",
41
+ );
42
+ },
43
+ async uninstall(ctx) {
44
+ if (!(await isFile(ctx.codex.configPath))) {
45
+ return action(this, "skipped", false, "config.toml not found");
46
+ }
47
+ const result = await restoreCodexNotify({
48
+ codexConfigPath: ctx.codex.configPath,
49
+ notifyOriginalPath: ctx.codex.notifyOriginalPath,
50
+ notifyCmd: ctx.codex.notifyCmd,
51
+ });
52
+ if (result.restored) {
53
+ return action(this, "restored", true, ctx.codex.configPath);
54
+ }
55
+ if (result.skippedReason === "no-backup-not-installed") {
56
+ return action(this, "skipped", false, "no backup; not installed", {
57
+ skippedReason: result.skippedReason,
58
+ });
59
+ }
60
+ return action(this, "unchanged", false, "no change");
61
+ },
62
+ renderStatusValue(probe) {
63
+ if (probe.status === "ready") return JSON.stringify(probe.currentNotify || []);
64
+ if (probe.status === "not_installed") return "unset";
65
+ return probe.status;
66
+ },
67
+ };
68
+
69
+ function baseProbe(descriptor, values) {
70
+ return {
71
+ name: descriptor.name,
72
+ summaryLabel: descriptor.summaryLabel,
73
+ statusLabel: descriptor.statusLabel,
74
+ configured: false,
75
+ ...values,
76
+ };
77
+ }
78
+
79
+ function action(descriptor, status, changed, detail, extras = {}) {
80
+ return {
81
+ name: descriptor.name,
82
+ label: descriptor.summaryLabel,
83
+ status,
84
+ changed,
85
+ detail,
86
+ ...extras,
87
+ };
88
+ }
@@ -0,0 +1,76 @@
1
+ const os = require("node:os");
2
+ const path = require("node:path");
3
+
4
+ const { buildClaudeHookCommand } = require("../claude-config");
5
+ const {
6
+ resolveGeminiConfigDir,
7
+ resolveGeminiSettingsPath,
8
+ buildGeminiHookCommand,
9
+ } = require("../gemini-config");
10
+ const { resolveOpencodeConfigDir } = require("../opencode-config");
11
+ const { resolveOpenclawHookPaths } = require("../openclaw-hook");
12
+ const { resolveOpenclawSessionPluginPaths } = require("../openclaw-session-plugin");
13
+ const { resolveTrackerPaths } = require("../tracker-paths");
14
+
15
+ async function createIntegrationContext({
16
+ home = os.homedir(),
17
+ env = process.env,
18
+ trackerPaths = null,
19
+ notifyPath = null,
20
+ } = {}) {
21
+ const resolvedTrackerPaths = trackerPaths || (await resolveTrackerPaths({ home }));
22
+ const resolvedNotifyPath = notifyPath || path.join(resolvedTrackerPaths.binDir, "notify.cjs");
23
+
24
+ const codexHome = env.CODEX_HOME || path.join(home, ".codex");
25
+ const codeHome = env.CODE_HOME || path.join(home, ".code");
26
+ const claudeDir = path.join(home, ".claude");
27
+ const geminiConfigDir = resolveGeminiConfigDir({ home, env });
28
+ const opencodeConfigDir = resolveOpencodeConfigDir({ home, env });
29
+
30
+ return {
31
+ home,
32
+ env,
33
+ trackerPaths: resolvedTrackerPaths,
34
+ notifyPath: resolvedNotifyPath,
35
+ codex: {
36
+ configPath: path.join(codexHome, "config.toml"),
37
+ notifyCmd: ["/usr/bin/env", "node", resolvedNotifyPath],
38
+ notifyOriginalPath: path.join(resolvedTrackerPaths.trackerDir, "codex_notify_original.json"),
39
+ },
40
+ everyCode: {
41
+ configPath: path.join(codeHome, "config.toml"),
42
+ notifyCmd: ["/usr/bin/env", "node", resolvedNotifyPath, "--source=every-code"],
43
+ notifyOriginalPath: path.join(
44
+ resolvedTrackerPaths.trackerDir,
45
+ "code_notify_original.json",
46
+ ),
47
+ },
48
+ claude: {
49
+ configDir: claudeDir,
50
+ settingsPath: path.join(claudeDir, "settings.json"),
51
+ hookCommand: buildClaudeHookCommand(resolvedNotifyPath),
52
+ },
53
+ gemini: {
54
+ configDir: geminiConfigDir,
55
+ settingsPath: resolveGeminiSettingsPath({ configDir: geminiConfigDir }),
56
+ hookCommand: buildGeminiHookCommand(resolvedNotifyPath),
57
+ },
58
+ opencode: {
59
+ configDir: opencodeConfigDir,
60
+ },
61
+ openclawSession: resolveOpenclawSessionPluginPaths({
62
+ home,
63
+ trackerDir: resolvedTrackerPaths.trackerDir,
64
+ env,
65
+ }),
66
+ openclawLegacy: resolveOpenclawHookPaths({
67
+ home,
68
+ trackerDir: resolvedTrackerPaths.trackerDir,
69
+ env,
70
+ }),
71
+ };
72
+ }
73
+
74
+ module.exports = {
75
+ createIntegrationContext,
76
+ };
@@ -0,0 +1,88 @@
1
+ const {
2
+ readEveryCodeNotify,
3
+ upsertEveryCodeNotify,
4
+ restoreEveryCodeNotify,
5
+ } = require("../codex-config");
6
+ const { arraysEqual, isFile } = require("./utils");
7
+
8
+ module.exports = {
9
+ name: "every-code",
10
+ summaryLabel: "Every Code",
11
+ statusLabel: "Every Code notify",
12
+ async probe(ctx) {
13
+ const installed = await isFile(ctx.everyCode.configPath);
14
+ if (!installed) {
15
+ return baseProbe(this, { status: "not_installed", detail: "Config not found" });
16
+ }
17
+
18
+ const currentNotify = await readEveryCodeNotify(ctx.everyCode.configPath);
19
+ const ready = arraysEqual(currentNotify, ctx.everyCode.notifyCmd);
20
+ return baseProbe(this, {
21
+ status: ready ? "ready" : "drifted",
22
+ detail: ready ? "Config already set" : "Run vibeusage init to reconcile notify",
23
+ configured: ready,
24
+ currentNotify,
25
+ });
26
+ },
27
+ async install(ctx) {
28
+ if (!(await isFile(ctx.everyCode.configPath))) {
29
+ return action(this, "skipped", false, "Config not found");
30
+ }
31
+ const result = await upsertEveryCodeNotify({
32
+ codeConfigPath: ctx.everyCode.configPath,
33
+ notifyCmd: ctx.everyCode.notifyCmd,
34
+ notifyOriginalPath: ctx.everyCode.notifyOriginalPath,
35
+ });
36
+ return action(
37
+ this,
38
+ result.changed ? "updated" : "set",
39
+ Boolean(result.changed),
40
+ result.changed ? "Updated config" : "Config already set",
41
+ );
42
+ },
43
+ async uninstall(ctx) {
44
+ if (!(await isFile(ctx.everyCode.configPath))) {
45
+ return action(this, "skipped", false, "config.toml not found");
46
+ }
47
+ const result = await restoreEveryCodeNotify({
48
+ codeConfigPath: ctx.everyCode.configPath,
49
+ notifyOriginalPath: ctx.everyCode.notifyOriginalPath,
50
+ notifyCmd: ctx.everyCode.notifyCmd,
51
+ });
52
+ if (result.restored) {
53
+ return action(this, "restored", true, ctx.everyCode.configPath);
54
+ }
55
+ if (result.skippedReason === "no-backup-not-installed") {
56
+ return action(this, "skipped", false, "no backup; not installed", {
57
+ skippedReason: result.skippedReason,
58
+ });
59
+ }
60
+ return action(this, "unchanged", false, "no change");
61
+ },
62
+ renderStatusValue(probe) {
63
+ if (probe.status === "ready") return JSON.stringify(probe.currentNotify || []);
64
+ if (probe.status === "not_installed") return "unset";
65
+ return probe.status;
66
+ },
67
+ };
68
+
69
+ function baseProbe(descriptor, values) {
70
+ return {
71
+ name: descriptor.name,
72
+ summaryLabel: descriptor.summaryLabel,
73
+ statusLabel: descriptor.statusLabel,
74
+ configured: false,
75
+ ...values,
76
+ };
77
+ }
78
+
79
+ function action(descriptor, status, changed, detail, extras = {}) {
80
+ return {
81
+ name: descriptor.name,
82
+ label: descriptor.summaryLabel,
83
+ status,
84
+ changed,
85
+ detail,
86
+ ...extras,
87
+ };
88
+ }
@@ -0,0 +1,86 @@
1
+ const {
2
+ isGeminiHookConfigured,
3
+ upsertGeminiHook,
4
+ removeGeminiHook,
5
+ } = require("../gemini-config");
6
+ const { isDir, isFile } = require("./utils");
7
+
8
+ module.exports = {
9
+ name: "gemini",
10
+ summaryLabel: "Gemini",
11
+ statusLabel: "Gemini hooks",
12
+ async probe(ctx) {
13
+ const hasConfigDir = await isDir(ctx.gemini.configDir);
14
+ if (!hasConfigDir) {
15
+ return baseProbe(this, { status: "not_installed", detail: "Config not found" });
16
+ }
17
+ const configured = await isGeminiHookConfigured({
18
+ settingsPath: ctx.gemini.settingsPath,
19
+ hookCommand: ctx.gemini.hookCommand,
20
+ });
21
+ return baseProbe(this, {
22
+ status: configured ? "ready" : "drifted",
23
+ detail: configured ? "Hooks installed" : "Run vibeusage init to reconcile hooks",
24
+ configured,
25
+ });
26
+ },
27
+ async install(ctx) {
28
+ if (!(await isDir(ctx.gemini.configDir))) {
29
+ return action(this, "skipped", false, "Config not found");
30
+ }
31
+ const result = await upsertGeminiHook({
32
+ settingsPath: ctx.gemini.settingsPath,
33
+ hookCommand: ctx.gemini.hookCommand,
34
+ });
35
+ return action(
36
+ this,
37
+ result.changed ? "installed" : "set",
38
+ Boolean(result.changed),
39
+ result.changed ? "Hooks installed" : "Hooks already installed",
40
+ );
41
+ },
42
+ async uninstall(ctx) {
43
+ if (!(await isDir(ctx.gemini.configDir))) {
44
+ return action(this, "skipped", false, "config dir not found");
45
+ }
46
+ const result = await removeGeminiHook({
47
+ settingsPath: ctx.gemini.settingsPath,
48
+ hookCommand: ctx.gemini.hookCommand,
49
+ });
50
+ if (result.removed) {
51
+ return action(this, "removed", true, ctx.gemini.settingsPath);
52
+ }
53
+ if (result.skippedReason === "hook-missing") {
54
+ return action(this, "unchanged", false, "no change", {
55
+ skippedReason: result.skippedReason,
56
+ });
57
+ }
58
+ return action(this, "skipped", false, "settings.json not found");
59
+ },
60
+ renderStatusValue(probe) {
61
+ if (probe.status === "ready") return "set";
62
+ if (probe.status === "not_installed") return "unset";
63
+ return probe.status;
64
+ },
65
+ };
66
+
67
+ function baseProbe(descriptor, values) {
68
+ return {
69
+ name: descriptor.name,
70
+ summaryLabel: descriptor.summaryLabel,
71
+ statusLabel: descriptor.statusLabel,
72
+ configured: false,
73
+ ...values,
74
+ };
75
+ }
76
+
77
+ function action(descriptor, status, changed, detail, extras = {}) {
78
+ return {
79
+ name: descriptor.name,
80
+ label: descriptor.summaryLabel,
81
+ status,
82
+ changed,
83
+ detail,
84
+ ...extras,
85
+ };
86
+ }