vibeusage 0.3.3 → 0.3.5
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/README.md +239 -331
- package/README.zh-CN.md +230 -317
- package/package.json +1 -1
- package/src/commands/init.js +1 -1
- package/src/commands/status.js +17 -1
- package/src/commands/sync.js +81 -0
- package/src/commands/uninstall.js +24 -5
- package/src/lib/claude-plugin.js +381 -0
- package/src/lib/diagnostics.js +30 -2
- package/src/lib/doctor.js +1 -1
- package/src/lib/hermes-config.js +172 -0
- package/src/lib/hermes-usage-ledger.js +123 -0
- package/src/lib/integrations/claude.js +79 -31
- package/src/lib/integrations/context.js +6 -0
- package/src/lib/integrations/hermes.js +96 -0
- package/src/lib/integrations/index.js +2 -0
- package/src/lib/openclaw-session-plugin.js +67 -3
- package/src/templates/hermes-vibeusage-plugin/__init__.py +75 -0
- package/src/templates/hermes-vibeusage-plugin/plugin.yaml +9 -0
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -190,7 +190,7 @@ function renderWelcome() {
|
|
|
190
190
|
DIVIDER,
|
|
191
191
|
"",
|
|
192
192
|
"This tool will:",
|
|
193
|
-
" - Analyze your local AI CLI configurations (Codex, Every Code, Claude, Gemini, Opencode, OpenClaw)",
|
|
193
|
+
" - Analyze your local AI CLI configurations (Codex, Every Code, Claude, Gemini, Opencode, Hermes, OpenClaw)",
|
|
194
194
|
" - Set up lightweight hooks to track your flow state",
|
|
195
195
|
" - Link your device to your VibeScore account",
|
|
196
196
|
"",
|
package/src/commands/status.js
CHANGED
|
@@ -6,6 +6,7 @@ const { readJson } = require("../lib/fs");
|
|
|
6
6
|
const { collectLocalSubscriptions } = require("../lib/subscriptions");
|
|
7
7
|
const { normalizeState: normalizeUploadState } = require("../lib/upload-throttle");
|
|
8
8
|
const { collectTrackerDiagnostics } = require("../lib/diagnostics");
|
|
9
|
+
const { readLastHermesUsageEvent, resolveHermesUsageLedgerPaths } = require("../lib/hermes-usage-ledger");
|
|
9
10
|
const { createIntegrationContext, listIntegrations, probeIntegrations } = require("../lib/integrations");
|
|
10
11
|
const { resolveTrackerPaths } = require("../lib/tracker-paths");
|
|
11
12
|
|
|
@@ -20,6 +21,7 @@ async function cmdStatus(argv = []) {
|
|
|
20
21
|
const home = os.homedir();
|
|
21
22
|
const { trackerDir } = await resolveTrackerPaths({ home });
|
|
22
23
|
const configPath = path.join(trackerDir, "config.json");
|
|
24
|
+
const { ledgerPath: hermesLedgerPath } = resolveHermesUsageLedgerPaths({ trackerDir });
|
|
23
25
|
const queuePath = path.join(trackerDir, "queue.jsonl");
|
|
24
26
|
const queueStatePath = path.join(trackerDir, "queue.state.json");
|
|
25
27
|
const cursorsPath = path.join(trackerDir, "cursors.json");
|
|
@@ -80,8 +82,10 @@ async function cmdStatus(argv = []) {
|
|
|
80
82
|
const claudeProbe = probeByName.get("claude");
|
|
81
83
|
const geminiProbe = probeByName.get("gemini");
|
|
82
84
|
const opencodeProbe = probeByName.get("opencode");
|
|
85
|
+
const hermesProbe = probeByName.get("hermes");
|
|
83
86
|
const openclawSessionProbe = probeByName.get("openclaw-session");
|
|
84
87
|
const opencodeDbPresent = Boolean((await safeStat(opencodeDbPath))?.isFile?.());
|
|
88
|
+
const hermesLedgerPresent = Boolean((await safeStat(hermesLedgerPath))?.isFile?.());
|
|
85
89
|
const opencodeSqliteState =
|
|
86
90
|
cursors?.opencodeSqlite && typeof cursors.opencodeSqlite === "object"
|
|
87
91
|
? cursors.opencodeSqlite
|
|
@@ -90,6 +94,15 @@ async function cmdStatus(argv = []) {
|
|
|
90
94
|
typeof opencodeSqliteState.lastStatus === "string" && opencodeSqliteState.lastStatus.trim()
|
|
91
95
|
? opencodeSqliteState.lastStatus.trim()
|
|
92
96
|
: "never_checked";
|
|
97
|
+
const hermesLedgerState =
|
|
98
|
+
cursors?.hermesLedger && typeof cursors.hermesLedger === "object" ? cursors.hermesLedger : {};
|
|
99
|
+
const hermesLastLedgerEvent = await readLastHermesUsageEvent({ trackerDir });
|
|
100
|
+
const hermesLastEventAt =
|
|
101
|
+
typeof hermesLastLedgerEvent?.emitted_at === "string"
|
|
102
|
+
? hermesLastLedgerEvent.emitted_at
|
|
103
|
+
: typeof hermesLedgerState.lastEventAt === "string"
|
|
104
|
+
? hermesLedgerState.lastEventAt
|
|
105
|
+
: "never";
|
|
93
106
|
|
|
94
107
|
process.stdout.write(
|
|
95
108
|
[
|
|
@@ -108,9 +121,12 @@ async function cmdStatus(argv = []) {
|
|
|
108
121
|
autoRetryLine,
|
|
109
122
|
`- Codex notify: ${renderIntegrationStatus(descriptors.get("codex"), codexProbe)}`,
|
|
110
123
|
`- Every Code notify: ${renderIntegrationStatus(descriptors.get("every-code"), everyCodeProbe)}`,
|
|
111
|
-
`- Claude
|
|
124
|
+
`- Claude plugin: ${renderIntegrationStatus(descriptors.get("claude"), claudeProbe)}`,
|
|
112
125
|
`- Gemini hooks: ${renderIntegrationStatus(descriptors.get("gemini"), geminiProbe)}`,
|
|
113
126
|
`- Opencode plugin: ${renderIntegrationStatus(descriptors.get("opencode"), opencodeProbe)}`,
|
|
127
|
+
`- Hermes plugin: ${renderIntegrationStatus(descriptors.get("hermes"), hermesProbe)}`,
|
|
128
|
+
`- Hermes ledger: ${hermesLedgerPresent ? "present" : "missing"}`,
|
|
129
|
+
`- Hermes last ledger event: ${hermesLastEventAt}`,
|
|
114
130
|
`- OpenCode SQLite DB: ${opencodeDbPresent ? "present" : "missing"}`,
|
|
115
131
|
`- OpenCode SQLite reader: ${opencodeSqliteReader}`,
|
|
116
132
|
`- OpenClaw session plugin: ${renderIntegrationStatus(
|
package/src/commands/sync.js
CHANGED
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
} = require("../lib/rollout");
|
|
23
23
|
const { drainQueueToCloud } = require("../lib/uploader");
|
|
24
24
|
const { readOpenclawUsageLedger } = require("../lib/openclaw-usage-ledger");
|
|
25
|
+
const { readHermesUsageLedger } = require("../lib/hermes-usage-ledger");
|
|
25
26
|
const { collectLocalSubscriptions } = require("../lib/subscriptions");
|
|
26
27
|
const { createProgress, renderBar, formatNumber, formatBytes } = require("../lib/progress");
|
|
27
28
|
const { syncHeartbeat } = require("../lib/vibeusage-api");
|
|
@@ -114,6 +115,12 @@ async function cmdSync(argv) {
|
|
|
114
115
|
},
|
|
115
116
|
});
|
|
116
117
|
|
|
118
|
+
const hermesResult = await parseHermesUsageLedger({
|
|
119
|
+
trackerDir,
|
|
120
|
+
cursors,
|
|
121
|
+
queuePath,
|
|
122
|
+
});
|
|
123
|
+
|
|
117
124
|
const openclawResult = opts.fromOpenclaw
|
|
118
125
|
? await parseOpenclawSanitizedLedger({
|
|
119
126
|
trackerDir,
|
|
@@ -369,12 +376,14 @@ async function cmdSync(argv) {
|
|
|
369
376
|
if (!opts.auto) {
|
|
370
377
|
const totalParsed =
|
|
371
378
|
parseResult.filesProcessed +
|
|
379
|
+
hermesResult.filesProcessed +
|
|
372
380
|
openclawResult.filesProcessed +
|
|
373
381
|
claudeResult.filesProcessed +
|
|
374
382
|
geminiResult.filesProcessed +
|
|
375
383
|
opencodeResult.filesProcessed;
|
|
376
384
|
const totalBuckets =
|
|
377
385
|
parseResult.bucketsQueued +
|
|
386
|
+
hermesResult.bucketsQueued +
|
|
378
387
|
openclawResult.bucketsQueued +
|
|
379
388
|
claudeResult.bucketsQueued +
|
|
380
389
|
geminiResult.bucketsQueued +
|
|
@@ -436,6 +445,78 @@ function parseArgs(argv) {
|
|
|
436
445
|
|
|
437
446
|
module.exports = { cmdSync };
|
|
438
447
|
|
|
448
|
+
async function parseHermesUsageLedger({ trackerDir, cursors, queuePath }) {
|
|
449
|
+
const ledgerCursor =
|
|
450
|
+
cursors?.hermesLedger && typeof cursors.hermesLedger === "object" ? cursors.hermesLedger : {};
|
|
451
|
+
const offset = Math.max(0, Number(ledgerCursor.offset || 0));
|
|
452
|
+
const { events, endOffset } = await readHermesUsageLedger({ trackerDir, offset });
|
|
453
|
+
|
|
454
|
+
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
455
|
+
const touchedBuckets = new Set();
|
|
456
|
+
let eventsAggregated = 0;
|
|
457
|
+
|
|
458
|
+
for (const event of events) {
|
|
459
|
+
if (!event || typeof event !== "object") continue;
|
|
460
|
+
if (event.type !== "usage") continue;
|
|
461
|
+
const bucketStart = toUtcHalfHourStart(event.emitted_at);
|
|
462
|
+
if (!bucketStart) continue;
|
|
463
|
+
|
|
464
|
+
const model =
|
|
465
|
+
typeof event.model === "string" && event.model.trim() ? event.model.trim() : "unknown";
|
|
466
|
+
const source = "hermes";
|
|
467
|
+
const delta = {
|
|
468
|
+
input_tokens: Math.max(0, Number(event.input_tokens || 0)),
|
|
469
|
+
cached_input_tokens: Math.max(
|
|
470
|
+
0,
|
|
471
|
+
Number(event.cache_read_tokens || 0) + Number(event.cache_write_tokens || 0),
|
|
472
|
+
),
|
|
473
|
+
output_tokens: Math.max(0, Number(event.output_tokens || 0)),
|
|
474
|
+
reasoning_output_tokens: Math.max(0, Number(event.reasoning_tokens || 0)),
|
|
475
|
+
total_tokens: Math.max(0, Number(event.total_tokens || 0)),
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
if (
|
|
479
|
+
delta.input_tokens === 0 &&
|
|
480
|
+
delta.cached_input_tokens === 0 &&
|
|
481
|
+
delta.output_tokens === 0 &&
|
|
482
|
+
delta.reasoning_output_tokens === 0 &&
|
|
483
|
+
delta.total_tokens === 0
|
|
484
|
+
) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
|
|
489
|
+
addTotals(bucket.totals, delta);
|
|
490
|
+
touchedBuckets.add(bucketKey(source, model, bucketStart));
|
|
491
|
+
eventsAggregated += 1;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
495
|
+
const lastUsageEvent = [...events].reverse().find((event) => {
|
|
496
|
+
if (!event || event.type !== "usage") return false;
|
|
497
|
+
return Boolean(toUtcHalfHourStart(event.emitted_at));
|
|
498
|
+
});
|
|
499
|
+
hourlyState.updatedAt = new Date().toISOString();
|
|
500
|
+
cursors.hourly = hourlyState;
|
|
501
|
+
cursors.hermesLedger = {
|
|
502
|
+
version: 1,
|
|
503
|
+
offset: endOffset,
|
|
504
|
+
updatedAt: new Date().toISOString(),
|
|
505
|
+
lastEventAt:
|
|
506
|
+
typeof lastUsageEvent?.emitted_at === "string"
|
|
507
|
+
? lastUsageEvent.emitted_at
|
|
508
|
+
: typeof ledgerCursor.lastEventAt === "string"
|
|
509
|
+
? ledgerCursor.lastEventAt
|
|
510
|
+
: null,
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
filesProcessed: endOffset > offset ? 1 : 0,
|
|
515
|
+
eventsAggregated,
|
|
516
|
+
bucketsQueued,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
439
520
|
async function parseOpenclawSanitizedLedger({ trackerDir, cursors, queuePath }) {
|
|
440
521
|
const ledgerCursor =
|
|
441
522
|
cursors?.openclawLedger && typeof cursors.openclawLedger === "object"
|
|
@@ -18,9 +18,10 @@ 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
|
|
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
|
+
const hermesConfigExists = await isDir(integrationContext.hermes.hermesHome);
|
|
24
25
|
const integrationResults = await uninstallIntegrations(integrationContext);
|
|
25
26
|
const resultByName = new Map(integrationResults.map((result) => [result.name, result]));
|
|
26
27
|
|
|
@@ -58,11 +59,11 @@ async function cmdUninstall(argv) {
|
|
|
58
59
|
renderHookLine({
|
|
59
60
|
exists: claudeConfigExists,
|
|
60
61
|
result: resultByName.get("claude"),
|
|
61
|
-
missingText:
|
|
62
|
+
missingText: `- Claude plugin: skipped (${integrationContext.claude.configDir} not found)`,
|
|
62
63
|
removedText: (result) =>
|
|
63
|
-
`- Claude
|
|
64
|
-
noChangeText: "- Claude
|
|
65
|
-
skippedText: "- Claude
|
|
64
|
+
`- Claude plugin removed: ${result.detail || integrationContext.claude.settingsPath}`,
|
|
65
|
+
noChangeText: "- Claude plugin: no change",
|
|
66
|
+
skippedText: "- Claude plugin: skipped",
|
|
66
67
|
}),
|
|
67
68
|
renderHookLine({
|
|
68
69
|
exists: geminiConfigExists,
|
|
@@ -82,6 +83,15 @@ async function cmdUninstall(argv) {
|
|
|
82
83
|
noChangeText: "- Opencode plugin: no change",
|
|
83
84
|
skippedText: "- Opencode plugin: skipped (unexpected content)",
|
|
84
85
|
}),
|
|
86
|
+
renderHookLine({
|
|
87
|
+
exists: hermesConfigExists,
|
|
88
|
+
result: resultByName.get("hermes"),
|
|
89
|
+
missingText: `- Hermes plugin: skipped (${integrationContext.hermes.hermesHome} not found)`,
|
|
90
|
+
removedText: (result) =>
|
|
91
|
+
`- Hermes plugin removed: ${result.detail || integrationContext.hermes.pluginDir}`,
|
|
92
|
+
noChangeText: "- Hermes plugin: no change",
|
|
93
|
+
skippedText: "- Hermes plugin: skipped (unexpected content)",
|
|
94
|
+
}),
|
|
85
95
|
renderHookLine({
|
|
86
96
|
exists: true,
|
|
87
97
|
result: resultByName.get("openclaw-session"),
|
|
@@ -120,6 +130,15 @@ async function isFile(p) {
|
|
|
120
130
|
}
|
|
121
131
|
}
|
|
122
132
|
|
|
133
|
+
async function isDir(p) {
|
|
134
|
+
try {
|
|
135
|
+
const st = await fs.stat(p);
|
|
136
|
+
return st.isDirectory();
|
|
137
|
+
} catch (_e) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
123
142
|
function renderRestoreLine({ exists, result, missingText, restoredText, noChangeText, skippedText }) {
|
|
124
143
|
if (!exists) return missingText;
|
|
125
144
|
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
|
+
};
|