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/src/lib/diagnostics.js
CHANGED
|
@@ -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
|
-
|
|
145
|
-
|
|
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.
|
|
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,
|
|
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
|
|
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, {
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
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 ? "
|
|
98
|
+
result.changed ? "Plugin installed" : "Plugin already installed",
|
|
60
99
|
);
|
|
61
100
|
},
|
|
62
101
|
async uninstall(ctx) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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,
|
|
119
|
+
return action(this, "removed", true, result.pluginRef || "Claude plugin removed");
|
|
72
120
|
}
|
|
73
|
-
if (result.skippedReason === "
|
|
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, "
|
|
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
|
|