vibeusage 0.3.2 → 0.3.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeusage",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Codex CLI token usage tracker (macOS-first, notify-driven).",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -108,7 +108,7 @@ async function cmdStatus(argv = []) {
108
108
  autoRetryLine,
109
109
  `- Codex notify: ${renderIntegrationStatus(descriptors.get("codex"), codexProbe)}`,
110
110
  `- Every Code notify: ${renderIntegrationStatus(descriptors.get("every-code"), everyCodeProbe)}`,
111
- `- Claude hooks: ${renderIntegrationStatus(descriptors.get("claude"), claudeProbe)}`,
111
+ `- Claude plugin: ${renderIntegrationStatus(descriptors.get("claude"), claudeProbe)}`,
112
112
  `- Gemini hooks: ${renderIntegrationStatus(descriptors.get("gemini"), geminiProbe)}`,
113
113
  `- Opencode plugin: ${renderIntegrationStatus(descriptors.get("opencode"), opencodeProbe)}`,
114
114
  `- OpenCode SQLite DB: ${opencodeDbPresent ? "present" : "missing"}`,
@@ -18,7 +18,7 @@ async function cmdUninstall(argv) {
18
18
  });
19
19
  const codexConfigExists = await isFile(integrationContext.codex.configPath);
20
20
  const codeConfigExists = await isFile(integrationContext.everyCode.configPath);
21
- const claudeConfigExists = await isFile(integrationContext.claude.settingsPath);
21
+ const claudeConfigExists = await isDir(integrationContext.claude.configDir);
22
22
  const geminiConfigExists = await isDir(integrationContext.gemini.configDir);
23
23
  const opencodeConfigExists = await isDir(integrationContext.opencode.configDir);
24
24
  const integrationResults = await uninstallIntegrations(integrationContext);
@@ -58,11 +58,11 @@ async function cmdUninstall(argv) {
58
58
  renderHookLine({
59
59
  exists: claudeConfigExists,
60
60
  result: resultByName.get("claude"),
61
- missingText: "- Claude hooks: skipped (settings.json not found)",
61
+ missingText: `- Claude plugin: skipped (${integrationContext.claude.configDir} not found)`,
62
62
  removedText: (result) =>
63
- `- Claude hooks removed: ${result.detail || integrationContext.claude.settingsPath}`,
64
- noChangeText: "- Claude hooks: no change",
65
- skippedText: "- Claude hooks: skipped",
63
+ `- Claude plugin removed: ${result.detail || integrationContext.claude.settingsPath}`,
64
+ noChangeText: "- Claude plugin: no change",
65
+ skippedText: "- Claude plugin: skipped",
66
66
  }),
67
67
  renderHookLine({
68
68
  exists: geminiConfigExists,
@@ -120,6 +120,15 @@ async function isFile(p) {
120
120
  }
121
121
  }
122
122
 
123
+ async function isDir(p) {
124
+ try {
125
+ const st = await fs.stat(p);
126
+ return st.isDirectory();
127
+ } catch (_e) {
128
+ return false;
129
+ }
130
+ }
131
+
123
132
  function renderRestoreLine({ exists, result, missingText, restoredText, noChangeText, skippedText }) {
124
133
  if (!exists) return missingText;
125
134
  if (!result) return noChangeText;
@@ -0,0 +1,381 @@
1
+ const os = require("node:os");
2
+ const path = require("node:path");
3
+ const fs = require("node:fs/promises");
4
+ const cp = require("node:child_process");
5
+
6
+ const { ensureDir, readJsonStrict, writeFileAtomic } = require("./fs");
7
+ const { buildClaudeHookCommand } = require("./claude-config");
8
+
9
+ const CLAUDE_PLUGIN_MARKETPLACE_NAME = "vibeusage-local";
10
+ const CLAUDE_PLUGIN_ID = "vibeusage-claude-sync";
11
+ const CLAUDE_PLUGIN_VERSION = "0.0.0";
12
+
13
+ function resolveClaudePluginPaths({ home = os.homedir(), trackerDir } = {}) {
14
+ if (!trackerDir) throw new Error("trackerDir is required");
15
+
16
+ const claudeDir = path.join(home, ".claude");
17
+ const pluginsDir = path.join(claudeDir, "plugins");
18
+ const marketplaceDir = path.join(trackerDir, "claude-marketplace");
19
+ const pluginRootDir = path.join(marketplaceDir, "plugins", CLAUDE_PLUGIN_ID);
20
+ const pluginRef = `${CLAUDE_PLUGIN_ID}@${CLAUDE_PLUGIN_MARKETPLACE_NAME}`;
21
+
22
+ return {
23
+ claudeDir,
24
+ settingsPath: path.join(claudeDir, "settings.json"),
25
+ pluginsDir,
26
+ knownMarketplacesPath: path.join(pluginsDir, "known_marketplaces.json"),
27
+ installedPluginsPath: path.join(pluginsDir, "installed_plugins.json"),
28
+ marketplaceDir,
29
+ marketplaceManifestPath: path.join(marketplaceDir, ".claude-plugin", "marketplace.json"),
30
+ pluginRootDir,
31
+ pluginManifestPath: path.join(pluginRootDir, ".claude-plugin", "plugin.json"),
32
+ pluginHooksPath: path.join(pluginRootDir, "hooks", "hooks.json"),
33
+ pluginRef,
34
+ };
35
+ }
36
+
37
+ async function ensureClaudePluginFiles({ trackerDir, notifyPath } = {}) {
38
+ if (!trackerDir || !notifyPath) {
39
+ throw new Error("trackerDir and notifyPath are required");
40
+ }
41
+
42
+ const paths = resolveClaudePluginPaths({ trackerDir });
43
+
44
+ await ensureDir(path.dirname(paths.marketplaceManifestPath));
45
+ await ensureDir(path.dirname(paths.pluginManifestPath));
46
+ await ensureDir(path.dirname(paths.pluginHooksPath));
47
+
48
+ await writeFileAtomic(paths.marketplaceManifestPath, buildMarketplaceManifest());
49
+ await writeFileAtomic(paths.pluginManifestPath, buildPluginManifest());
50
+ await writeFileAtomic(paths.pluginHooksPath, buildPluginHooks({ notifyPath }));
51
+
52
+ return paths;
53
+ }
54
+
55
+ async function probeClaudePluginState({ home = os.homedir(), trackerDir } = {}) {
56
+ const paths = resolveClaudePluginPaths({ home, trackerDir });
57
+ const settings = await readJsonStrict(paths.settingsPath);
58
+ const known = await readJsonStrict(paths.knownMarketplacesPath);
59
+ const installed = await readJsonStrict(paths.installedPluginsPath);
60
+ const pluginFilesReady =
61
+ (await isFile(paths.marketplaceManifestPath)) &&
62
+ (await isFile(paths.pluginManifestPath)) &&
63
+ (await isFile(paths.pluginHooksPath));
64
+
65
+ if (settings.status === "invalid" || settings.status === "error") {
66
+ return unreadableState(paths, "Claude settings unreadable");
67
+ }
68
+ if (known.status === "invalid" || known.status === "error") {
69
+ return unreadableState(paths, "Claude marketplace registry unreadable");
70
+ }
71
+ if (installed.status === "invalid" || installed.status === "error") {
72
+ return unreadableState(paths, "Claude plugin registry unreadable");
73
+ }
74
+
75
+ const enabled = settings.value?.enabledPlugins?.[paths.pluginRef] === true;
76
+ const declaredMarketplace = marketplaceMatchesPath({
77
+ marketplaceEntry: known.value?.[CLAUDE_PLUGIN_MARKETPLACE_NAME],
78
+ marketplaceDir: paths.marketplaceDir,
79
+ });
80
+ const installedEntries = Array.isArray(installed.value?.plugins?.[paths.pluginRef])
81
+ ? installed.value.plugins[paths.pluginRef]
82
+ : [];
83
+ const installedUserEntry =
84
+ installedEntries.find((entry) => String(entry?.scope || "") === "user") || null;
85
+
86
+ return {
87
+ configured: Boolean(enabled && installedUserEntry && pluginFilesReady && declaredMarketplace),
88
+ enabled,
89
+ installed: Boolean(installedUserEntry),
90
+ marketplaceDeclared: declaredMarketplace,
91
+ pluginFilesReady,
92
+ pluginRef: paths.pluginRef,
93
+ unreadable: false,
94
+ detail: null,
95
+ ...paths,
96
+ };
97
+ }
98
+
99
+ async function installClaudePlugin({
100
+ home = os.homedir(),
101
+ trackerDir,
102
+ notifyPath,
103
+ env = process.env,
104
+ } = {}) {
105
+ const paths = resolveClaudePluginPaths({ home, trackerDir });
106
+ await ensureClaudePluginFiles({ trackerDir, notifyPath });
107
+
108
+ const initialState = await probeClaudePluginState({ home, trackerDir });
109
+ if (initialState.unreadable) {
110
+ return { configured: false, skippedReason: "claude-config-unreadable", ...initialState };
111
+ }
112
+
113
+ const marketplaceCmd = initialState.marketplaceDeclared
114
+ ? ["plugin", "marketplace", "update", CLAUDE_PLUGIN_MARKETPLACE_NAME]
115
+ : ["plugin", "marketplace", "add", paths.marketplaceDir, "--scope", "user"];
116
+ const marketplaceResult = runClaudeCli(marketplaceCmd, env);
117
+ if (marketplaceResult.skippedReason) {
118
+ return { configured: false, ...paths, ...marketplaceResult };
119
+ }
120
+
121
+ let actionResult;
122
+ if (!initialState.installed) {
123
+ actionResult = runClaudeCli(["plugin", "install", paths.pluginRef, "--scope", "user"], env);
124
+ } else if (!initialState.enabled) {
125
+ actionResult = runClaudeCli(["plugin", "enable", paths.pluginRef, "--scope", "user"], env);
126
+ } else {
127
+ actionResult = runClaudeCli(["plugin", "update", paths.pluginRef, "--scope", "user"], env);
128
+ }
129
+ if (actionResult.skippedReason) {
130
+ return { configured: false, ...paths, ...actionResult };
131
+ }
132
+
133
+ const nextState = await probeClaudePluginState({ home, trackerDir });
134
+ return {
135
+ configured: nextState.configured,
136
+ changed: !initialState.configured || !initialState.enabled || !initialState.marketplaceDeclared,
137
+ stdout: `${marketplaceResult.stdout || ""}\n${actionResult.stdout || ""}`.trim(),
138
+ stderr: `${marketplaceResult.stderr || ""}\n${actionResult.stderr || ""}`.trim(),
139
+ ...nextState,
140
+ };
141
+ }
142
+
143
+ async function removeClaudePluginConfig({
144
+ home = os.homedir(),
145
+ trackerDir,
146
+ env = process.env,
147
+ } = {}) {
148
+ const paths = resolveClaudePluginPaths({ home, trackerDir });
149
+ const initialState = await probeClaudePluginState({ home, trackerDir });
150
+ const hadMarketplaceDir = await isDir(paths.marketplaceDir);
151
+
152
+ let changed = false;
153
+ let skippedReason = null;
154
+ if (initialState.installed || initialState.enabled) {
155
+ const uninstallResult = runClaudeCli(["plugin", "uninstall", paths.pluginRef, "--scope", "user"], env);
156
+ if (uninstallResult.skippedReason) {
157
+ return { removed: false, ...paths, ...uninstallResult };
158
+ }
159
+ changed = true;
160
+ }
161
+
162
+ const siblingRefs = await listMarketplaceSiblingPluginRefs({
163
+ installedPluginsPath: paths.installedPluginsPath,
164
+ marketplaceName: CLAUDE_PLUGIN_MARKETPLACE_NAME,
165
+ excludePluginRef: paths.pluginRef,
166
+ });
167
+
168
+ const shouldRemoveMarketplace =
169
+ initialState.marketplaceDeclared && siblingRefs.unreadable !== true && siblingRefs.refs.length === 0;
170
+
171
+ if (shouldRemoveMarketplace) {
172
+ const removeMarketplaceResult = runClaudeCli(
173
+ ["plugin", "marketplace", "remove", CLAUDE_PLUGIN_MARKETPLACE_NAME],
174
+ env,
175
+ );
176
+ if (removeMarketplaceResult.skippedReason && removeMarketplaceResult.skippedReason !== "claude-cli-error") {
177
+ return { removed: changed, ...paths, ...removeMarketplaceResult };
178
+ }
179
+ changed = true;
180
+ } else if (!changed) {
181
+ skippedReason = "plugin-missing";
182
+ }
183
+
184
+ if (shouldRemoveMarketplace) {
185
+ await fs.rm(paths.marketplaceDir, { recursive: true, force: true }).catch(() => {});
186
+ }
187
+ return {
188
+ removed: changed || (hadMarketplaceDir && shouldRemoveMarketplace),
189
+ skippedReason,
190
+ siblingPluginRefs: siblingRefs.refs,
191
+ ...paths,
192
+ };
193
+ }
194
+
195
+ function runClaudeCli(args, env = process.env) {
196
+ let res;
197
+ try {
198
+ res = cp.spawnSync("claude", args, {
199
+ env,
200
+ encoding: "utf8",
201
+ timeout: 30_000,
202
+ });
203
+ } catch (err) {
204
+ return {
205
+ code: 1,
206
+ skippedReason: err?.code === "ENOENT" ? "claude-cli-missing" : "claude-cli-error",
207
+ error: err?.message || String(err),
208
+ stdout: "",
209
+ stderr: "",
210
+ };
211
+ }
212
+
213
+ if (res.error?.code === "ENOENT") {
214
+ return {
215
+ code: 1,
216
+ skippedReason: "claude-cli-missing",
217
+ error: res.error.message,
218
+ stdout: res.stdout || "",
219
+ stderr: res.stderr || "",
220
+ };
221
+ }
222
+
223
+ if ((res.status || 0) !== 0) {
224
+ return {
225
+ code: Number(res.status || 1),
226
+ skippedReason: "claude-cli-error",
227
+ error: (res.stderr || res.stdout || "").trim() || "claude plugin command failed",
228
+ stdout: res.stdout || "",
229
+ stderr: res.stderr || "",
230
+ };
231
+ }
232
+
233
+ return {
234
+ code: 0,
235
+ stdout: res.stdout || "",
236
+ stderr: res.stderr || "",
237
+ };
238
+ }
239
+
240
+ function buildMarketplaceManifest() {
241
+ return `${JSON.stringify(
242
+ {
243
+ $schema: "https://anthropic.com/claude-code/marketplace.schema.json",
244
+ name: CLAUDE_PLUGIN_MARKETPLACE_NAME,
245
+ description: "Local VibeUsage Claude plugin marketplace.",
246
+ owner: {
247
+ name: "VibeUsage",
248
+ email: "support@vibeusage.cc",
249
+ },
250
+ version: CLAUDE_PLUGIN_VERSION,
251
+ plugins: [
252
+ {
253
+ name: CLAUDE_PLUGIN_ID,
254
+ source: `./plugins/${CLAUDE_PLUGIN_ID}`,
255
+ description: "Trigger VibeUsage Claude notify bridge on Claude session lifecycle events.",
256
+ version: CLAUDE_PLUGIN_VERSION,
257
+ strict: false,
258
+ },
259
+ ],
260
+ },
261
+ null,
262
+ 2,
263
+ )}\n`;
264
+ }
265
+
266
+ function buildPluginManifest() {
267
+ return `${JSON.stringify(
268
+ {
269
+ name: CLAUDE_PLUGIN_ID,
270
+ description: "Trigger VibeUsage Claude notify bridge on Claude session lifecycle events.",
271
+ version: CLAUDE_PLUGIN_VERSION,
272
+ },
273
+ null,
274
+ 2,
275
+ )}\n`;
276
+ }
277
+
278
+ function buildPluginHooks({ notifyPath }) {
279
+ const hookCommand = buildClaudeHookCommand(notifyPath);
280
+ return `${JSON.stringify(
281
+ {
282
+ description: "Run VibeUsage Claude notify bridge on Stop and SessionEnd events.",
283
+ hooks: {
284
+ Stop: [
285
+ {
286
+ hooks: [{ type: "command", command: hookCommand }],
287
+ },
288
+ ],
289
+ SessionEnd: [
290
+ {
291
+ hooks: [{ type: "command", command: hookCommand }],
292
+ },
293
+ ],
294
+ },
295
+ },
296
+ null,
297
+ 2,
298
+ )}\n`;
299
+ }
300
+
301
+ function unreadableState(paths, detail) {
302
+ return {
303
+ configured: false,
304
+ enabled: false,
305
+ installed: false,
306
+ marketplaceDeclared: false,
307
+ pluginFilesReady: false,
308
+ pluginRef: paths.pluginRef,
309
+ unreadable: true,
310
+ detail,
311
+ ...paths,
312
+ };
313
+ }
314
+
315
+ function marketplaceMatchesPath({ marketplaceEntry, marketplaceDir } = {}) {
316
+ if (!marketplaceEntry || typeof marketplaceEntry !== "object") return false;
317
+
318
+ const source = marketplaceEntry.source;
319
+ if (!source || typeof source !== "object") return false;
320
+ if (source.source !== "path") return false;
321
+ if (typeof source.path !== "string" || source.path.length === 0) return false;
322
+
323
+ return path.resolve(source.path) === path.resolve(marketplaceDir);
324
+ }
325
+
326
+ async function listMarketplaceSiblingPluginRefs({
327
+ installedPluginsPath,
328
+ marketplaceName,
329
+ excludePluginRef,
330
+ } = {}) {
331
+ const installed = await readJsonStrict(installedPluginsPath);
332
+ if (installed.status === "invalid") {
333
+ return { unreadable: true, refs: [] };
334
+ }
335
+
336
+ const plugins =
337
+ installed.value?.plugins && typeof installed.value.plugins === "object"
338
+ ? installed.value.plugins
339
+ : {};
340
+ const refs = Object.entries(plugins)
341
+ .filter(([ref, entries]) => {
342
+ if (ref === excludePluginRef) return false;
343
+ if (!ref.endsWith(`@${marketplaceName}`)) return false;
344
+ return Array.isArray(entries) && entries.length > 0;
345
+ })
346
+ .map(([ref]) => ref);
347
+
348
+ return { unreadable: false, refs };
349
+ }
350
+
351
+ function isNonEmptyObject(value) {
352
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
353
+ }
354
+
355
+ async function isFile(targetPath) {
356
+ try {
357
+ const stat = await fs.stat(targetPath);
358
+ return stat.isFile();
359
+ } catch (_err) {
360
+ return false;
361
+ }
362
+ }
363
+
364
+ async function isDir(targetPath) {
365
+ try {
366
+ const stat = await fs.stat(targetPath);
367
+ return stat.isDirectory();
368
+ } catch (_err) {
369
+ return false;
370
+ }
371
+ }
372
+
373
+ module.exports = {
374
+ CLAUDE_PLUGIN_MARKETPLACE_NAME,
375
+ CLAUDE_PLUGIN_ID,
376
+ resolveClaudePluginPaths,
377
+ ensureClaudePluginFiles,
378
+ probeClaudePluginState,
379
+ installClaudePlugin,
380
+ removeClaudePluginConfig,
381
+ };
@@ -141,8 +141,8 @@ async function collectTrackerDiagnostics({
141
141
  every_code_notify_status: everyCodeProbe?.status || "unknown",
142
142
  every_code_notify_configured: Boolean(everyCodeProbe?.configured),
143
143
  every_code_notify: everyCodeNotify,
144
- claude_hook_status: claudeProbe?.status || "unknown",
145
- claude_hook_configured: Boolean(claudeProbe?.configured),
144
+ claude_plugin_status: claudeProbe?.status || "unknown",
145
+ claude_plugin_configured: Boolean(claudeProbe?.configured),
146
146
  gemini_hook_status: geminiProbe?.status || "unknown",
147
147
  gemini_hook_configured: Boolean(geminiProbe?.configured),
148
148
  opencode_plugin_status: opencodeProbe?.status || "unknown",
package/src/lib/doctor.js CHANGED
@@ -310,7 +310,7 @@ function buildDiagnosticsChecks(diagnostics) {
310
310
  const notifyConfigured = Boolean(
311
311
  notify.codex_notify_configured ||
312
312
  notify.every_code_notify_configured ||
313
- notify.claude_hook_configured ||
313
+ notify.claude_plugin_configured ||
314
314
  notify.gemini_hook_configured ||
315
315
  notify.opencode_plugin_configured ||
316
316
  notify.openclaw_session_plugin_configured,
@@ -1,81 +1,129 @@
1
- const { probeClaudeHook, upsertClaudeHook, removeClaudeHook } = require("../claude-config");
1
+ const { probeClaudeHook, removeClaudeHook } = require("../claude-config");
2
+ const {
3
+ installClaudePlugin,
4
+ probeClaudePluginState,
5
+ removeClaudePluginConfig,
6
+ } = require("../claude-plugin");
2
7
  const { isDir, isFile } = require("./utils");
3
8
 
4
9
  module.exports = {
5
10
  name: "claude",
6
11
  summaryLabel: "Claude",
7
- statusLabel: "Claude hooks",
12
+ statusLabel: "Claude plugin",
8
13
  async probe(ctx) {
9
14
  const hasConfigDir = await isDir(ctx.claude.configDir);
10
15
  if (!hasConfigDir) {
11
16
  return baseProbe(this, { status: "not_installed", detail: "Config not found" });
12
17
  }
13
18
 
19
+ const pluginState = await probeClaudePluginState({
20
+ home: ctx.home,
21
+ trackerDir: ctx.trackerPaths.trackerDir,
22
+ });
23
+ if (pluginState.unreadable) {
24
+ return baseProbe(this, { status: "unreadable", detail: pluginState.detail });
25
+ }
26
+ if (pluginState.configured) {
27
+ return baseProbe(this, {
28
+ status: "ready",
29
+ detail: "Plugin installed",
30
+ configured: true,
31
+ pluginState,
32
+ });
33
+ }
34
+
14
35
  const hasSettings = await isFile(ctx.claude.settingsPath);
15
36
  if (!hasSettings) {
16
- return baseProbe(this, { status: "drifted", detail: "Run vibeusage init to install hooks" });
37
+ return baseProbe(this, {
38
+ status: pluginState.pluginFilesReady || pluginState.marketplaceDeclared ? "drifted" : "not_installed",
39
+ detail:
40
+ pluginState.pluginFilesReady || pluginState.marketplaceDeclared
41
+ ? "Run vibeusage init to reconcile plugin"
42
+ : "Run vibeusage init to install plugin",
43
+ pluginState,
44
+ });
17
45
  }
18
46
 
19
47
  const hookState = await probeClaudeHook({
20
48
  settingsPath: ctx.claude.settingsPath,
21
49
  hookCommand: ctx.claude.hookCommand,
22
50
  });
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
51
+ const status = hookState.anyPresent
36
52
  ? "unsupported_legacy"
37
- : "drifted";
53
+ : pluginState.installed || pluginState.marketplaceDeclared || pluginState.pluginFilesReady
54
+ ? "drifted"
55
+ : "not_installed";
38
56
  return baseProbe(this, {
39
57
  status,
40
58
  detail:
41
59
  status === "unsupported_legacy"
42
60
  ? "Legacy hook config detected; run vibeusage init"
43
- : "Run vibeusage init to reconcile hooks",
61
+ : status === "drifted"
62
+ ? "Run vibeusage init to reconcile plugin"
63
+ : "Run vibeusage init to install plugin",
44
64
  hookState,
65
+ pluginState,
45
66
  });
46
67
  },
47
68
  async install(ctx) {
48
69
  if (!(await isDir(ctx.claude.configDir))) {
49
70
  return action(this, "skipped", false, "Config not found");
50
71
  }
51
- const result = await upsertClaudeHook({
52
- settingsPath: ctx.claude.settingsPath,
53
- hookCommand: ctx.claude.hookCommand,
72
+ const result = await installClaudePlugin({
73
+ home: ctx.home,
74
+ trackerDir: ctx.trackerPaths.trackerDir,
75
+ notifyPath: ctx.notifyPath,
76
+ env: ctx.env,
54
77
  });
78
+ if (result.skippedReason === "claude-cli-missing") {
79
+ return action(this, "skipped", false, "Claude CLI not found", {
80
+ skippedReason: result.skippedReason,
81
+ });
82
+ }
83
+ if (!result.configured) {
84
+ return action(this, "skipped", false, result.error || "Claude plugin install incomplete", {
85
+ skippedReason: result.skippedReason || "claude-plugin-install-incomplete",
86
+ });
87
+ }
88
+ if (result.configured && (await isFile(ctx.claude.settingsPath))) {
89
+ await removeClaudeHook({
90
+ settingsPath: ctx.claude.settingsPath,
91
+ hookCommand: ctx.claude.hookCommand,
92
+ }).catch(() => {});
93
+ }
55
94
  return action(
56
95
  this,
57
96
  result.changed ? "installed" : "set",
58
97
  Boolean(result.changed),
59
- result.changed ? "Hooks installed" : "Hooks already installed",
98
+ result.changed ? "Plugin installed" : "Plugin already installed",
60
99
  );
61
100
  },
62
101
  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,
102
+ const result = await removeClaudePluginConfig({
103
+ home: ctx.home,
104
+ trackerDir: ctx.trackerPaths.trackerDir,
105
+ env: ctx.env,
69
106
  });
107
+ if (await isFile(ctx.claude.settingsPath)) {
108
+ await removeClaudeHook({
109
+ settingsPath: ctx.claude.settingsPath,
110
+ hookCommand: ctx.claude.hookCommand,
111
+ }).catch(() => {});
112
+ }
113
+ if (result.skippedReason === "claude-cli-missing") {
114
+ return action(this, "skipped", false, "Claude CLI not found", {
115
+ skippedReason: result.skippedReason,
116
+ });
117
+ }
70
118
  if (result.removed) {
71
- return action(this, "removed", true, ctx.claude.settingsPath);
119
+ return action(this, "removed", true, result.pluginRef || "Claude plugin removed");
72
120
  }
73
- if (result.skippedReason === "hook-missing") {
121
+ if (result.skippedReason === "plugin-missing") {
74
122
  return action(this, "unchanged", false, "no change", {
75
123
  skippedReason: result.skippedReason,
76
124
  });
77
125
  }
78
- return action(this, "skipped", false, "settings.json not found");
126
+ return action(this, "skipped", false, "Claude plugin not found");
79
127
  },
80
128
  renderStatusValue(probe) {
81
129
  if (probe.status === "ready") return "set";