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.
@@ -3,6 +3,7 @@ const path = require("node:path");
3
3
  const fs = require("node:fs/promises");
4
4
 
5
5
  const { readJson } = require("./fs");
6
+ const { readLastHermesUsageEvent, resolveHermesUsageLedgerPaths } = require("./hermes-usage-ledger");
6
7
  const { normalizeState: normalizeUploadState } = require("./upload-throttle");
7
8
  const { createIntegrationContext, probeIntegrations } = require("./integrations");
8
9
 
@@ -25,6 +26,7 @@ async function collectTrackerDiagnostics({
25
26
  });
26
27
  const trackerDir = integrationContext.trackerPaths.trackerDir;
27
28
  const configPath = path.join(trackerDir, "config.json");
29
+ const { ledgerPath: hermesLedgerPath } = resolveHermesUsageLedgerPaths({ trackerDir });
28
30
  const queuePath = path.join(trackerDir, "queue.jsonl");
29
31
  const queueStatePath = path.join(trackerDir, "queue.state.json");
30
32
  const cursorsPath = path.join(trackerDir, "cursors.json");
@@ -45,7 +47,10 @@ async function collectTrackerDiagnostics({
45
47
  const probeByName = new Map(probes.map((probe) => [probe.name, probe]));
46
48
  const opencodeSqliteCursor =
47
49
  cursors?.opencodeSqlite && typeof cursors.opencodeSqlite === "object" ? cursors.opencodeSqlite : {};
50
+ const hermesLedgerCursor =
51
+ cursors?.hermesLedger && typeof cursors.hermesLedger === "object" ? cursors.hermesLedger : {};
48
52
  const opencodeDbStat = await safeStat(opencodeDbPath);
53
+ const hermesLedgerStat = await safeStat(hermesLedgerPath);
49
54
 
50
55
  const queueSize = await safeStatSize(queuePath);
51
56
  const offsetBytes = Number(queueState.offset || 0);
@@ -60,6 +65,7 @@ async function collectTrackerDiagnostics({
60
65
  const claudeProbe = probeByName.get("claude");
61
66
  const geminiProbe = probeByName.get("gemini");
62
67
  const opencodeProbe = probeByName.get("opencode");
68
+ const hermesProbe = probeByName.get("hermes");
63
69
  const openclawSessionProbe = probeByName.get("openclaw-session");
64
70
 
65
71
  const codexNotify = Array.isArray(codexProbe?.currentNotify)
@@ -73,6 +79,7 @@ async function collectTrackerDiagnostics({
73
79
  ? new Date(uploadThrottle.lastSuccessMs).toISOString()
74
80
  : null;
75
81
  const autoRetryAt = parseEpochMsToIso(autoRetry?.retryAtMs);
82
+ const hermesLastLedgerEvent = await readLastHermesUsageEvent({ trackerDir });
76
83
 
77
84
  return {
78
85
  ok: true,
@@ -92,6 +99,9 @@ async function collectTrackerDiagnostics({
92
99
  claude_config: redactValue(integrationContext.claude.settingsPath, home),
93
100
  gemini_config: redactValue(integrationContext.gemini.settingsPath, home),
94
101
  opencode_config: redactValue(integrationContext.opencode.configDir, home),
102
+ hermes_home: redactValue(integrationContext.hermes.hermesHome, home),
103
+ hermes_plugin_dir: redactValue(integrationContext.hermes.pluginDir, home),
104
+ hermes_ledger: redactValue(hermesLedgerPath, home),
95
105
  },
96
106
  config: {
97
107
  base_url: typeof config?.baseUrl === "string" ? config.baseUrl : null,
@@ -131,6 +141,22 @@ async function collectTrackerDiagnostics({
131
141
  ? opencodeSqliteCursor.lastErrorCode
132
142
  : null,
133
143
  },
144
+ hermes: {
145
+ ledger_present: Boolean(hermesLedgerStat?.isFile?.()),
146
+ ledger_size_bytes: hermesLedgerStat?.isFile?.() ? Number(hermesLedgerStat.size || 0) : 0,
147
+ ledger_offset:
148
+ Number.isFinite(Number(hermesLedgerCursor.offset)) ? Math.max(0, Number(hermesLedgerCursor.offset)) : 0,
149
+ ledger_updated_at:
150
+ typeof hermesLedgerCursor.updatedAt === "string" ? hermesLedgerCursor.updatedAt : null,
151
+ last_event_at:
152
+ typeof hermesLastLedgerEvent?.emitted_at === "string"
153
+ ? hermesLastLedgerEvent.emitted_at
154
+ : typeof hermesLedgerCursor.lastEventAt === "string"
155
+ ? hermesLedgerCursor.lastEventAt
156
+ : null,
157
+ last_event_type:
158
+ typeof hermesLastLedgerEvent?.type === "string" ? hermesLastLedgerEvent.type : null,
159
+ },
134
160
  notify: {
135
161
  last_notify: lastNotify,
136
162
  last_openclaw_triggered_sync: lastOpenclawSync,
@@ -141,12 +167,14 @@ async function collectTrackerDiagnostics({
141
167
  every_code_notify_status: everyCodeProbe?.status || "unknown",
142
168
  every_code_notify_configured: Boolean(everyCodeProbe?.configured),
143
169
  every_code_notify: everyCodeNotify,
144
- claude_hook_status: claudeProbe?.status || "unknown",
145
- claude_hook_configured: Boolean(claudeProbe?.configured),
170
+ claude_plugin_status: claudeProbe?.status || "unknown",
171
+ claude_plugin_configured: Boolean(claudeProbe?.configured),
146
172
  gemini_hook_status: geminiProbe?.status || "unknown",
147
173
  gemini_hook_configured: Boolean(geminiProbe?.configured),
148
174
  opencode_plugin_status: opencodeProbe?.status || "unknown",
149
175
  opencode_plugin_configured: Boolean(opencodeProbe?.configured),
176
+ hermes_plugin_status: hermesProbe?.status || "unknown",
177
+ hermes_plugin_configured: Boolean(hermesProbe?.configured),
150
178
  openclaw_session_plugin_status: openclawSessionProbe?.status || "unknown",
151
179
  openclaw_session_plugin_configured: Boolean(openclawSessionProbe?.configured),
152
180
  openclaw_session_plugin_linked: Boolean(openclawSessionProbe?.linked),
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,
@@ -0,0 +1,172 @@
1
+ const os = require("node:os");
2
+ const path = require("node:path");
3
+ const fs = require("node:fs/promises");
4
+ const fssync = require("node:fs");
5
+
6
+ const { ensureDir, writeFileAtomic } = require("./fs");
7
+
8
+ const HERMES_PLUGIN_ID = "vibeusage";
9
+ const HERMES_PLUGIN_MARKER = "VIBEUSAGE_HERMES_PLUGIN";
10
+ const HERMES_PLUGIN_TEMPLATE_VERSION = 1;
11
+
12
+ function resolveHermesHome({ home = os.homedir(), env = process.env } = {}) {
13
+ const explicit = typeof env.HERMES_HOME === "string" ? env.HERMES_HOME.trim() : "";
14
+ if (explicit) return path.resolve(explicit);
15
+ return path.join(home, ".hermes");
16
+ }
17
+
18
+ function resolveHermesPluginPaths({ home = os.homedir(), env = process.env, trackerDir } = {}) {
19
+ if (!trackerDir) throw new Error("trackerDir is required");
20
+
21
+ const hermesHome = resolveHermesHome({ home, env });
22
+ const pluginDir = path.join(hermesHome, "plugins", HERMES_PLUGIN_ID);
23
+ return {
24
+ hermesHome,
25
+ pluginId: HERMES_PLUGIN_ID,
26
+ pluginDir,
27
+ pluginYamlPath: path.join(pluginDir, "plugin.yaml"),
28
+ pluginInitPath: path.join(pluginDir, "__init__.py"),
29
+ ledgerPath: path.join(trackerDir, "hermes.usage.jsonl"),
30
+ };
31
+ }
32
+
33
+ async function probeHermesPlugin({ home = os.homedir(), env = process.env, trackerDir } = {}) {
34
+ const paths = resolveHermesPluginPaths({ home, env, trackerDir });
35
+ const expectedYaml = buildHermesPluginYaml();
36
+ const expectedInit = buildHermesPluginInit({ ledgerPath: paths.ledgerPath });
37
+ const hermesHomeExists = await pathExists(paths.hermesHome);
38
+
39
+ if (!hermesHomeExists) {
40
+ return {
41
+ configured: false,
42
+ status: "not_installed",
43
+ detail: "Hermes home not found",
44
+ initPreviewStatus: "updated",
45
+ initPreviewDetail: "Will install plugin",
46
+ ...paths,
47
+ };
48
+ }
49
+
50
+ const pluginDirExists = await pathExists(paths.pluginDir);
51
+ if (!pluginDirExists) {
52
+ return {
53
+ configured: false,
54
+ status: "not_installed",
55
+ detail: "Plugin not installed",
56
+ initPreviewStatus: "updated",
57
+ initPreviewDetail: "Will install plugin",
58
+ ...paths,
59
+ };
60
+ }
61
+
62
+ const yamlState = await readTextStrict(paths.pluginYamlPath);
63
+ const initState = await readTextStrict(paths.pluginInitPath);
64
+ if (yamlState.status === "error" || initState.status === "error") {
65
+ return {
66
+ configured: false,
67
+ status: "unreadable",
68
+ detail: yamlState.error || initState.error || "Plugin unreadable",
69
+ ...paths,
70
+ };
71
+ }
72
+ if (yamlState.status === "missing" || initState.status === "missing") {
73
+ return {
74
+ configured: false,
75
+ status: "drifted",
76
+ detail: "Run vibeusage init to reconcile plugin",
77
+ ...paths,
78
+ };
79
+ }
80
+
81
+ const configured = yamlState.value === expectedYaml && initState.value === expectedInit;
82
+ return {
83
+ configured,
84
+ status: configured ? "ready" : "drifted",
85
+ detail: configured ? "Plugin installed" : "Run vibeusage init to reconcile plugin",
86
+ ...paths,
87
+ };
88
+ }
89
+
90
+ async function installHermesPlugin({ home = os.homedir(), env = process.env, trackerDir } = {}) {
91
+ const paths = resolveHermesPluginPaths({ home, env, trackerDir });
92
+ const nextYaml = buildHermesPluginYaml();
93
+ const nextInit = buildHermesPluginInit({ ledgerPath: paths.ledgerPath });
94
+ const currentYaml = await fs.readFile(paths.pluginYamlPath, "utf8").catch(() => null);
95
+ const currentInit = await fs.readFile(paths.pluginInitPath, "utf8").catch(() => null);
96
+ const changed = currentYaml !== nextYaml || currentInit !== nextInit;
97
+
98
+ await ensureDir(paths.pluginDir);
99
+ await writeFileAtomic(paths.pluginYamlPath, nextYaml);
100
+ await writeFileAtomic(paths.pluginInitPath, nextInit);
101
+ return { configured: true, changed, ...paths };
102
+ }
103
+
104
+ async function removeHermesPlugin({ home = os.homedir(), env = process.env, trackerDir } = {}) {
105
+ const paths = resolveHermesPluginPaths({ home, env, trackerDir });
106
+ const hadPluginDir = await pathExists(paths.pluginDir);
107
+ if (!hadPluginDir) {
108
+ return { removed: false, skippedReason: "plugin-missing", ...paths };
109
+ }
110
+
111
+ const yamlText = await fs.readFile(paths.pluginYamlPath, "utf8").catch(() => null);
112
+ const initText = await fs.readFile(paths.pluginInitPath, "utf8").catch(() => null);
113
+ const markerPresent = hasHermesPluginMarker(yamlText) && hasHermesPluginMarker(initText);
114
+ if (!markerPresent) {
115
+ return { removed: false, skippedReason: "unexpected-content", ...paths };
116
+ }
117
+
118
+ await fs.rm(paths.pluginDir, { recursive: true, force: true }).catch(() => {});
119
+ return { removed: true, ...paths };
120
+ }
121
+
122
+ function buildHermesPluginYaml() {
123
+ return readTemplateFile("plugin.yaml");
124
+ }
125
+
126
+ function buildHermesPluginInit({ ledgerPath }) {
127
+ const safeLedgerPath = typeof ledgerPath === "string" ? ledgerPath : "";
128
+ return readTemplateFile("__init__.py").replace("__LEDGER_PATH__", safeLedgerPath);
129
+ }
130
+
131
+ function hasHermesPluginMarker(text) {
132
+ return typeof text === "string" && text.includes(HERMES_PLUGIN_MARKER);
133
+ }
134
+
135
+ async function readTextStrict(filePath) {
136
+ try {
137
+ return { status: "ok", value: await fs.readFile(filePath, "utf8"), error: null };
138
+ } catch (err) {
139
+ if (err?.code === "ENOENT" || err?.code === "ENOTDIR") {
140
+ return { status: "missing", value: null, error: null };
141
+ }
142
+ return { status: "error", value: null, error: err?.message || String(err) };
143
+ }
144
+ }
145
+
146
+ async function pathExists(targetPath) {
147
+ try {
148
+ await fs.stat(targetPath);
149
+ return true;
150
+ } catch (_err) {
151
+ return false;
152
+ }
153
+ }
154
+
155
+ function readTemplateFile(name) {
156
+ const templatePath = path.join(__dirname, "..", "templates", "hermes-vibeusage-plugin", name);
157
+ return fssync.readFileSync(templatePath, "utf8");
158
+ }
159
+
160
+ module.exports = {
161
+ HERMES_PLUGIN_ID,
162
+ HERMES_PLUGIN_MARKER,
163
+ HERMES_PLUGIN_TEMPLATE_VERSION,
164
+ resolveHermesHome,
165
+ resolveHermesPluginPaths,
166
+ probeHermesPlugin,
167
+ installHermesPlugin,
168
+ removeHermesPlugin,
169
+ buildHermesPluginYaml,
170
+ buildHermesPluginInit,
171
+ hasHermesPluginMarker,
172
+ };
@@ -0,0 +1,123 @@
1
+ const fs = require("node:fs/promises");
2
+ const path = require("node:path");
3
+
4
+ const ALLOWED_HERMES_LEDGER_FIELDS = [
5
+ "version",
6
+ "type",
7
+ "source",
8
+ "session_id",
9
+ "platform",
10
+ "model",
11
+ "provider",
12
+ "api_mode",
13
+ "api_call_count",
14
+ "input_tokens",
15
+ "output_tokens",
16
+ "cache_read_tokens",
17
+ "cache_write_tokens",
18
+ "reasoning_tokens",
19
+ "total_tokens",
20
+ "finish_reason",
21
+ "emitted_at",
22
+ ];
23
+
24
+ function resolveHermesUsageLedgerPaths({ trackerDir } = {}) {
25
+ if (!trackerDir) throw new Error("trackerDir is required");
26
+ return {
27
+ ledgerPath: path.join(trackerDir, "hermes.usage.jsonl"),
28
+ };
29
+ }
30
+
31
+ async function readHermesUsageLedger({ trackerDir, offset = 0 } = {}) {
32
+ const { ledgerPath } = resolveHermesUsageLedgerPaths({ trackerDir });
33
+ const buffer = await fs.readFile(ledgerPath).catch((err) => {
34
+ if (err && err.code === "ENOENT") return null;
35
+ throw err;
36
+ });
37
+
38
+ if (!buffer) {
39
+ return { events: [], endOffset: 0 };
40
+ }
41
+
42
+ const startOffset = Math.max(0, Number(offset || 0));
43
+ const fileEndOffset = buffer.length;
44
+ if (startOffset >= fileEndOffset) {
45
+ return { events: [], endOffset: fileEndOffset };
46
+ }
47
+
48
+ const newlineIndex = buffer.lastIndexOf(0x0a);
49
+ if (newlineIndex < startOffset) {
50
+ return { events: [], endOffset: startOffset };
51
+ }
52
+
53
+ const endOffset = newlineIndex + 1;
54
+ const raw = buffer.subarray(startOffset, endOffset).toString("utf8");
55
+ const events = raw
56
+ .split(/\r?\n/)
57
+ .map((line) => line.trim())
58
+ .filter(Boolean)
59
+ .map((line) => {
60
+ try {
61
+ return JSON.parse(line);
62
+ } catch (_err) {
63
+ return null;
64
+ }
65
+ })
66
+ .filter(Boolean)
67
+ .map(stripToAllowedHermesFields)
68
+ .filter(isHermesLedgerEvent);
69
+
70
+ return { events, endOffset };
71
+ }
72
+
73
+ async function readLastHermesUsageEvent({ trackerDir } = {}) {
74
+ const { ledgerPath } = resolveHermesUsageLedgerPaths({ trackerDir });
75
+ const text = await fs.readFile(ledgerPath, "utf8").catch((err) => {
76
+ if (err && err.code === "ENOENT") return null;
77
+ throw err;
78
+ });
79
+ if (!text) return null;
80
+
81
+ const lines = text.split(/\r?\n/);
82
+ for (let idx = lines.length - 1; idx >= 0; idx -= 1) {
83
+ const line = String(lines[idx] || "").trim();
84
+ if (!line) continue;
85
+ try {
86
+ const parsed = stripToAllowedHermesFields(JSON.parse(line));
87
+ if (isHermesLedgerEvent(parsed)) return parsed;
88
+ } catch (_err) {
89
+ // ignore malformed line
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+
95
+ function stripToAllowedHermesFields(event) {
96
+ const out = {};
97
+ for (const field of ALLOWED_HERMES_LEDGER_FIELDS) {
98
+ if (!Object.prototype.hasOwnProperty.call(event, field)) continue;
99
+ out[field] = event[field];
100
+ }
101
+ return out;
102
+ }
103
+
104
+ function isHermesLedgerEvent(event) {
105
+ if (!event || typeof event !== "object") return false;
106
+ if (normalizeString(event.source) !== "hermes") return false;
107
+ const type = normalizeString(event.type);
108
+ if (!type || !["session_start", "usage", "session_end"].includes(type)) return false;
109
+ return true;
110
+ }
111
+
112
+ function normalizeString(value) {
113
+ if (typeof value !== "string") return null;
114
+ const trimmed = value.trim();
115
+ return trimmed.length > 0 ? trimmed : null;
116
+ }
117
+
118
+ module.exports = {
119
+ ALLOWED_HERMES_LEDGER_FIELDS,
120
+ resolveHermesUsageLedgerPaths,
121
+ readHermesUsageLedger,
122
+ readLastHermesUsageEvent,
123
+ };
@@ -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";
@@ -9,6 +9,7 @@ const {
9
9
  } = require("../gemini-config");
10
10
  const { resolveOpencodeConfigDir } = require("../opencode-config");
11
11
  const { resolveOpenclawSessionPluginPaths } = require("../openclaw-session-plugin");
12
+ const { resolveHermesPluginPaths } = require("../hermes-config");
12
13
  const { resolveTrackerPaths } = require("../tracker-paths");
13
14
 
14
15
  async function createIntegrationContext({
@@ -57,6 +58,11 @@ async function createIntegrationContext({
57
58
  opencode: {
58
59
  configDir: opencodeConfigDir,
59
60
  },
61
+ hermes: resolveHermesPluginPaths({
62
+ home,
63
+ env,
64
+ trackerDir: resolvedTrackerPaths.trackerDir,
65
+ }),
60
66
  openclawSession: resolveOpenclawSessionPluginPaths({
61
67
  home,
62
68
  trackerDir: resolvedTrackerPaths.trackerDir,
@@ -0,0 +1,96 @@
1
+ const { isDir } = require("./utils");
2
+ const { probeHermesPlugin, installHermesPlugin, removeHermesPlugin } = require("../hermes-config");
3
+
4
+ module.exports = {
5
+ name: "hermes",
6
+ summaryLabel: "Hermes Plugin",
7
+ statusLabel: "Hermes plugin",
8
+ async probe(ctx) {
9
+ const hermesHomeExists = await isDir(ctx.hermes.hermesHome);
10
+ if (!hermesHomeExists) {
11
+ return baseProbe(this, {
12
+ status: "not_installed",
13
+ detail: "Hermes home not found",
14
+ initPreviewStatus: "skipped",
15
+ initPreviewDetail: "Hermes not detected",
16
+ });
17
+ }
18
+
19
+ const state = await probeHermesPlugin({
20
+ home: ctx.home,
21
+ env: ctx.env,
22
+ trackerDir: ctx.trackerPaths.trackerDir,
23
+ });
24
+ return baseProbe(this, state);
25
+ },
26
+ async install(ctx) {
27
+ if (!(await isDir(ctx.hermes.hermesHome))) {
28
+ return action(this, "skipped", false, "Hermes home not found");
29
+ }
30
+ const result = await installHermesPlugin({
31
+ home: ctx.home,
32
+ env: ctx.env,
33
+ trackerDir: ctx.trackerPaths.trackerDir,
34
+ });
35
+ return action(
36
+ this,
37
+ result.changed ? "installed" : "set",
38
+ Boolean(result.changed),
39
+ result.changed ? "Plugin installed" : "Plugin already installed",
40
+ );
41
+ },
42
+ async uninstall(ctx) {
43
+ if (!(await isDir(ctx.hermes.hermesHome))) {
44
+ return action(this, "skipped", false, "Hermes home not found", {
45
+ skippedReason: "hermes-home-missing",
46
+ });
47
+ }
48
+ const result = await removeHermesPlugin({
49
+ home: ctx.home,
50
+ env: ctx.env,
51
+ trackerDir: ctx.trackerPaths.trackerDir,
52
+ });
53
+ if (result.removed) {
54
+ return action(this, "removed", true, result.pluginDir || ctx.hermes.pluginDir);
55
+ }
56
+ if (result.skippedReason === "plugin-missing") {
57
+ return action(this, "unchanged", false, "no change", {
58
+ skippedReason: result.skippedReason,
59
+ });
60
+ }
61
+ if (result.skippedReason === "unexpected-content") {
62
+ return action(this, "skipped", false, "unexpected content", {
63
+ skippedReason: result.skippedReason,
64
+ });
65
+ }
66
+ return action(this, "skipped", false, "Hermes home not found", {
67
+ skippedReason: result.skippedReason || "hermes-home-missing",
68
+ });
69
+ },
70
+ renderStatusValue(probe) {
71
+ if (probe.status === "ready") return "set";
72
+ if (probe.status === "not_installed") return "unset";
73
+ return probe.status;
74
+ },
75
+ };
76
+
77
+ function baseProbe(descriptor, values) {
78
+ return {
79
+ name: descriptor.name,
80
+ summaryLabel: descriptor.summaryLabel,
81
+ statusLabel: descriptor.statusLabel,
82
+ configured: false,
83
+ ...values,
84
+ };
85
+ }
86
+
87
+ function action(descriptor, status, changed, detail, extras = {}) {
88
+ return {
89
+ name: descriptor.name,
90
+ label: descriptor.summaryLabel,
91
+ status,
92
+ changed,
93
+ detail,
94
+ ...extras,
95
+ };
96
+ }
@@ -4,6 +4,7 @@ const everyCode = require("./every-code");
4
4
  const claude = require("./claude");
5
5
  const gemini = require("./gemini");
6
6
  const opencode = require("./opencode");
7
+ const hermes = require("./hermes");
7
8
  const openclawSession = require("./openclaw-session");
8
9
 
9
10
  const INTEGRATIONS = [
@@ -12,6 +13,7 @@ const INTEGRATIONS = [
12
13
  claude,
13
14
  gemini,
14
15
  opencode,
16
+ hermes,
15
17
  openclawSession,
16
18
  ];
17
19