vibeusage 0.2.23 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -14
- package/README.zh-CN.md +7 -2
- package/package.json +4 -5
- package/src/cli.js +2 -2
- package/src/commands/init.js +20 -362
- package/src/commands/status.js +36 -51
- package/src/commands/sync.js +4 -3
- package/src/commands/uninstall.js +121 -104
- package/src/lib/claude-config.js +130 -35
- package/src/lib/diagnostics.js +54 -57
- package/src/lib/doctor.js +27 -0
- package/src/lib/integrations/claude.js +106 -0
- package/src/lib/integrations/codex.js +88 -0
- package/src/lib/integrations/context.js +76 -0
- package/src/lib/integrations/every-code.js +88 -0
- package/src/lib/integrations/gemini.js +86 -0
- package/src/lib/integrations/index.js +85 -0
- package/src/lib/integrations/openclaw-legacy.js +123 -0
- package/src/lib/integrations/openclaw-session.js +132 -0
- package/src/lib/integrations/opencode.js +86 -0
- package/src/lib/integrations/utils.js +39 -0
- package/src/lib/runtime-config.js +7 -5
- package/src/lib/vibeusage-api.js +9 -5
- package/src/shared/copy-registry.cjs +142 -0
- package/src/shared/copy-registry.cjs.d.ts +33 -0
- package/src/shared/runtime-defaults.cjs +11 -0
- package/src/shared/runtime-defaults.cjs.d.ts +3 -0
- package/src/shared/vibeusage-function-contract.cjs +34 -0
- package/src/shared/vibeusage-function-contract.cjs.d.ts +4 -0
- package/src/commands/activate-if-needed.js +0 -41
- package/src/lib/activation-check.js +0 -341
package/src/commands/status.js
CHANGED
|
@@ -3,20 +3,10 @@ const path = require("node:path");
|
|
|
3
3
|
const fs = require("node:fs/promises");
|
|
4
4
|
|
|
5
5
|
const { readJson } = require("../lib/fs");
|
|
6
|
-
const { readCodexNotify, readEveryCodeNotify } = require("../lib/codex-config");
|
|
7
|
-
const { isClaudeHookConfigured, buildClaudeHookCommand } = require("../lib/claude-config");
|
|
8
|
-
const {
|
|
9
|
-
resolveGeminiConfigDir,
|
|
10
|
-
resolveGeminiSettingsPath,
|
|
11
|
-
isGeminiHookConfigured,
|
|
12
|
-
buildGeminiHookCommand,
|
|
13
|
-
} = require("../lib/gemini-config");
|
|
14
|
-
const { resolveOpencodeConfigDir, isOpencodePluginInstalled } = require("../lib/opencode-config");
|
|
15
6
|
const { collectLocalSubscriptions } = require("../lib/subscriptions");
|
|
16
7
|
const { normalizeState: normalizeUploadState } = require("../lib/upload-throttle");
|
|
17
8
|
const { collectTrackerDiagnostics } = require("../lib/diagnostics");
|
|
18
|
-
const {
|
|
19
|
-
const { probeOpenclawSessionPluginState } = require("../lib/openclaw-session-plugin");
|
|
9
|
+
const { createIntegrationContext, listIntegrations, probeIntegrations } = require("../lib/integrations");
|
|
20
10
|
const { resolveTrackerPaths } = require("../lib/tracker-paths");
|
|
21
11
|
|
|
22
12
|
async function cmdStatus(argv = []) {
|
|
@@ -28,7 +18,7 @@ async function cmdStatus(argv = []) {
|
|
|
28
18
|
}
|
|
29
19
|
|
|
30
20
|
const home = os.homedir();
|
|
31
|
-
const { trackerDir
|
|
21
|
+
const { trackerDir } = await resolveTrackerPaths({ home });
|
|
32
22
|
const configPath = path.join(trackerDir, "config.json");
|
|
33
23
|
const queuePath = path.join(trackerDir, "queue.jsonl");
|
|
34
24
|
const queueStatePath = path.join(trackerDir, "queue.state.json");
|
|
@@ -38,17 +28,6 @@ async function cmdStatus(argv = []) {
|
|
|
38
28
|
const throttlePath = path.join(trackerDir, "sync.throttle");
|
|
39
29
|
const uploadThrottlePath = path.join(trackerDir, "upload.throttle.json");
|
|
40
30
|
const autoRetryPath = path.join(trackerDir, "auto.retry.json");
|
|
41
|
-
const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
|
|
42
|
-
const codexConfigPath = path.join(codexHome, "config.toml");
|
|
43
|
-
const codeHome = process.env.CODE_HOME || path.join(home, ".code");
|
|
44
|
-
const codeConfigPath = path.join(codeHome, "config.toml");
|
|
45
|
-
const claudeSettingsPath = path.join(home, ".claude", "settings.json");
|
|
46
|
-
const geminiConfigDir = resolveGeminiConfigDir({ home, env: process.env });
|
|
47
|
-
const geminiSettingsPath = resolveGeminiSettingsPath({ configDir: geminiConfigDir });
|
|
48
|
-
const opencodeConfigDir = resolveOpencodeConfigDir({ home, env: process.env });
|
|
49
|
-
const notifyPath = path.join(binDir, "notify.cjs");
|
|
50
|
-
const claudeHookCommand = buildClaudeHookCommand(notifyPath);
|
|
51
|
-
const geminiHookCommand = buildGeminiHookCommand(notifyPath);
|
|
52
31
|
|
|
53
32
|
const config = await readJson(configPath);
|
|
54
33
|
const cursors = await readJson(cursorsPath);
|
|
@@ -63,27 +42,10 @@ async function cmdStatus(argv = []) {
|
|
|
63
42
|
const lastOpenclawSync = (await safeReadText(openclawSignalPath))?.trim() || null;
|
|
64
43
|
const lastNotifySpawn = parseEpochMsToIso((await safeReadText(throttlePath))?.trim() || null);
|
|
65
44
|
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const claudeHookConfigured = await isClaudeHookConfigured({
|
|
71
|
-
settingsPath: claudeSettingsPath,
|
|
72
|
-
hookCommand: claudeHookCommand,
|
|
73
|
-
});
|
|
74
|
-
const geminiHookConfigured = await isGeminiHookConfigured({
|
|
75
|
-
settingsPath: geminiSettingsPath,
|
|
76
|
-
hookCommand: geminiHookCommand,
|
|
77
|
-
});
|
|
78
|
-
const opencodePluginConfigured = await isOpencodePluginInstalled({
|
|
79
|
-
configDir: opencodeConfigDir,
|
|
80
|
-
});
|
|
81
|
-
const openclawSessionPluginState = await probeOpenclawSessionPluginState({
|
|
82
|
-
home,
|
|
83
|
-
trackerDir,
|
|
84
|
-
env: process.env,
|
|
85
|
-
});
|
|
86
|
-
const openclawHookState = await probeOpenclawHookState({ home, trackerDir, env: process.env });
|
|
45
|
+
const integrationContext = await createIntegrationContext({ home, env: process.env });
|
|
46
|
+
const probes = await probeIntegrations(integrationContext);
|
|
47
|
+
const descriptors = new Map(listIntegrations().map((integration) => [integration.name, integration]));
|
|
48
|
+
const probeByName = new Map(probes.map((probe) => [probe.name, probe]));
|
|
87
49
|
|
|
88
50
|
const lastUpload = uploadThrottle.lastSuccessMs
|
|
89
51
|
? parseEpochMsToIso(uploadThrottle.lastSuccessMs)
|
|
@@ -110,6 +72,13 @@ async function cmdStatus(argv = []) {
|
|
|
110
72
|
});
|
|
111
73
|
const subscriptionLines =
|
|
112
74
|
subscriptions.length > 0 ? subscriptions.map(formatSubscriptionLine) : [];
|
|
75
|
+
const codexProbe = probeByName.get("codex");
|
|
76
|
+
const everyCodeProbe = probeByName.get("every-code");
|
|
77
|
+
const claudeProbe = probeByName.get("claude");
|
|
78
|
+
const geminiProbe = probeByName.get("gemini");
|
|
79
|
+
const opencodeProbe = probeByName.get("opencode");
|
|
80
|
+
const openclawSessionProbe = probeByName.get("openclaw-session");
|
|
81
|
+
const openclawLegacyProbe = probeByName.get("openclaw-legacy");
|
|
113
82
|
|
|
114
83
|
process.stdout.write(
|
|
115
84
|
[
|
|
@@ -126,13 +95,19 @@ async function cmdStatus(argv = []) {
|
|
|
126
95
|
`- Backoff until: ${backoffUntil || "never"}`,
|
|
127
96
|
lastUploadError ? `- Last upload error: ${lastUploadError}` : null,
|
|
128
97
|
autoRetryLine,
|
|
129
|
-
`- Codex notify: ${
|
|
130
|
-
`- Every Code notify: ${
|
|
131
|
-
`- Claude hooks: ${
|
|
132
|
-
`- Gemini hooks: ${
|
|
133
|
-
`- Opencode plugin: ${
|
|
134
|
-
`- OpenClaw session plugin: ${
|
|
135
|
-
|
|
98
|
+
`- Codex notify: ${renderIntegrationStatus(descriptors.get("codex"), codexProbe)}`,
|
|
99
|
+
`- Every Code notify: ${renderIntegrationStatus(descriptors.get("every-code"), everyCodeProbe)}`,
|
|
100
|
+
`- Claude hooks: ${renderIntegrationStatus(descriptors.get("claude"), claudeProbe)}`,
|
|
101
|
+
`- Gemini hooks: ${renderIntegrationStatus(descriptors.get("gemini"), geminiProbe)}`,
|
|
102
|
+
`- Opencode plugin: ${renderIntegrationStatus(descriptors.get("opencode"), opencodeProbe)}`,
|
|
103
|
+
`- OpenClaw session plugin: ${renderIntegrationStatus(
|
|
104
|
+
descriptors.get("openclaw-session"),
|
|
105
|
+
openclawSessionProbe,
|
|
106
|
+
)}`,
|
|
107
|
+
`- OpenClaw hook (legacy): ${renderIntegrationStatus(
|
|
108
|
+
descriptors.get("openclaw-legacy"),
|
|
109
|
+
openclawLegacyProbe,
|
|
110
|
+
)}`,
|
|
136
111
|
...subscriptionLines,
|
|
137
112
|
"",
|
|
138
113
|
]
|
|
@@ -141,6 +116,16 @@ async function cmdStatus(argv = []) {
|
|
|
141
116
|
);
|
|
142
117
|
}
|
|
143
118
|
|
|
119
|
+
function renderIntegrationStatus(descriptor, probe) {
|
|
120
|
+
if (!probe) return "unknown";
|
|
121
|
+
if (descriptor && typeof descriptor.renderStatusValue === "function") {
|
|
122
|
+
return descriptor.renderStatusValue(probe);
|
|
123
|
+
}
|
|
124
|
+
if (probe.status === "ready") return "set";
|
|
125
|
+
if (probe.status === "not_installed") return "unset";
|
|
126
|
+
return probe.status || "unknown";
|
|
127
|
+
}
|
|
128
|
+
|
|
144
129
|
function formatSubscriptionLine(entry = {}) {
|
|
145
130
|
const tool = String(entry.tool || "");
|
|
146
131
|
const provider = String(entry.provider || "");
|
package/src/commands/sync.js
CHANGED
|
@@ -69,8 +69,9 @@ async function cmdSync(argv) {
|
|
|
69
69
|
const opencodeHome = process.env.OPENCODE_HOME || path.join(xdgDataHome, "opencode");
|
|
70
70
|
const opencodeStorageDir = path.join(opencodeHome, "storage");
|
|
71
71
|
|
|
72
|
-
// OpenClaw
|
|
73
|
-
// We still parse all regular sources so model/source attribution stays
|
|
72
|
+
// OpenClaw session-plugin integration: allow a plugin-triggered sync to request incremental parsing
|
|
73
|
+
// for a single session jsonl. We still parse all regular sources so model/source attribution stays
|
|
74
|
+
// complete (e.g. Kimi sessions).
|
|
74
75
|
const openclawSignal = opts.fromOpenclaw
|
|
75
76
|
? resolveOpenclawSignal({ home, env: process.env })
|
|
76
77
|
: null;
|
|
@@ -119,7 +120,7 @@ async function cmdSync(argv) {
|
|
|
119
120
|
|
|
120
121
|
let openclawResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
121
122
|
if (openclawFiles.length > 0) {
|
|
122
|
-
// Only runs when explicitly triggered by OpenClaw
|
|
123
|
+
// Only runs when explicitly triggered by OpenClaw session-plugin events.
|
|
123
124
|
openclawResult = await parseOpenclawIncremental({
|
|
124
125
|
sessionFiles: openclawFiles,
|
|
125
126
|
cursors,
|
|
@@ -2,73 +2,27 @@ const os = require("node:os");
|
|
|
2
2
|
const path = require("node:path");
|
|
3
3
|
const fs = require("node:fs/promises");
|
|
4
4
|
|
|
5
|
-
const { restoreCodexNotify, restoreEveryCodeNotify } = require("../lib/codex-config");
|
|
6
|
-
const { removeClaudeHook, buildClaudeHookCommand } = require("../lib/claude-config");
|
|
7
|
-
const {
|
|
8
|
-
resolveGeminiConfigDir,
|
|
9
|
-
resolveGeminiSettingsPath,
|
|
10
|
-
buildGeminiHookCommand,
|
|
11
|
-
removeGeminiHook,
|
|
12
|
-
} = require("../lib/gemini-config");
|
|
13
|
-
const { resolveOpencodeConfigDir, removeOpencodePlugin } = require("../lib/opencode-config");
|
|
14
|
-
const { removeOpenclawHookConfig } = require("../lib/openclaw-hook");
|
|
15
|
-
const { removeOpenclawSessionPluginConfig } = require("../lib/openclaw-session-plugin");
|
|
16
5
|
const { resolveTrackerPaths } = require("../lib/tracker-paths");
|
|
6
|
+
const { createIntegrationContext, uninstallIntegrations } = require("../lib/integrations");
|
|
17
7
|
|
|
18
8
|
async function cmdUninstall(argv) {
|
|
19
9
|
const opts = parseArgs(argv);
|
|
20
10
|
const home = os.homedir();
|
|
21
11
|
const { trackerDir, binDir } = await resolveTrackerPaths({ home });
|
|
22
|
-
const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
|
|
23
|
-
const codexConfigPath = path.join(codexHome, "config.toml");
|
|
24
|
-
const codeHome = process.env.CODE_HOME || path.join(home, ".code");
|
|
25
|
-
const codeConfigPath = path.join(codeHome, "config.toml");
|
|
26
|
-
const claudeSettingsPath = path.join(home, ".claude", "settings.json");
|
|
27
|
-
const geminiConfigDir = resolveGeminiConfigDir({ home, env: process.env });
|
|
28
|
-
const geminiSettingsPath = resolveGeminiSettingsPath({ configDir: geminiConfigDir });
|
|
29
|
-
const opencodeConfigDir = resolveOpencodeConfigDir({ home, env: process.env });
|
|
30
12
|
const notifyPath = path.join(binDir, "notify.cjs");
|
|
31
|
-
const
|
|
32
|
-
const codeNotifyOriginalPath = path.join(trackerDir, "code_notify_original.json");
|
|
33
|
-
const codexNotifyCmd = ["/usr/bin/env", "node", notifyPath];
|
|
34
|
-
const codeNotifyCmd = ["/usr/bin/env", "node", notifyPath, "--source=every-code"];
|
|
35
|
-
const claudeHookCommand = buildClaudeHookCommand(notifyPath);
|
|
36
|
-
const geminiHookCommand = buildGeminiHookCommand(notifyPath);
|
|
37
|
-
|
|
38
|
-
const codexConfigExists = await isFile(codexConfigPath);
|
|
39
|
-
const codeConfigExists = await isFile(codeConfigPath);
|
|
40
|
-
const claudeConfigExists = await isFile(claudeSettingsPath);
|
|
41
|
-
const geminiConfigExists = await isDir(geminiConfigDir);
|
|
42
|
-
const opencodeConfigExists = await isDir(opencodeConfigDir);
|
|
43
|
-
const codexRestore = codexConfigExists
|
|
44
|
-
? await restoreCodexNotify({
|
|
45
|
-
codexConfigPath,
|
|
46
|
-
notifyOriginalPath,
|
|
47
|
-
notifyCmd: codexNotifyCmd,
|
|
48
|
-
})
|
|
49
|
-
: { restored: false, skippedReason: "config-missing" };
|
|
50
|
-
const codeRestore = codeConfigExists
|
|
51
|
-
? await restoreEveryCodeNotify({
|
|
52
|
-
codeConfigPath,
|
|
53
|
-
notifyOriginalPath: codeNotifyOriginalPath,
|
|
54
|
-
notifyCmd: codeNotifyCmd,
|
|
55
|
-
})
|
|
56
|
-
: { restored: false, skippedReason: "config-missing" };
|
|
57
|
-
const claudeRemove = claudeConfigExists
|
|
58
|
-
? await removeClaudeHook({ settingsPath: claudeSettingsPath, hookCommand: claudeHookCommand })
|
|
59
|
-
: { removed: false, skippedReason: "config-missing" };
|
|
60
|
-
const geminiRemove = geminiConfigExists
|
|
61
|
-
? await removeGeminiHook({ settingsPath: geminiSettingsPath, hookCommand: geminiHookCommand })
|
|
62
|
-
: { removed: false, skippedReason: "config-missing" };
|
|
63
|
-
const opencodeRemove = opencodeConfigExists
|
|
64
|
-
? await removeOpencodePlugin({ configDir: opencodeConfigDir })
|
|
65
|
-
: { removed: false, skippedReason: "config-missing" };
|
|
66
|
-
const openclawSessionPluginRemove = await removeOpenclawSessionPluginConfig({
|
|
13
|
+
const integrationContext = await createIntegrationContext({
|
|
67
14
|
home,
|
|
68
|
-
trackerDir,
|
|
69
15
|
env: process.env,
|
|
16
|
+
trackerPaths: { trackerDir, binDir, rootDir: path.dirname(trackerDir) },
|
|
17
|
+
notifyPath,
|
|
70
18
|
});
|
|
71
|
-
const
|
|
19
|
+
const codexConfigExists = await isFile(integrationContext.codex.configPath);
|
|
20
|
+
const codeConfigExists = await isFile(integrationContext.everyCode.configPath);
|
|
21
|
+
const claudeConfigExists = await isFile(integrationContext.claude.settingsPath);
|
|
22
|
+
const geminiConfigExists = await isDir(integrationContext.gemini.configDir);
|
|
23
|
+
const opencodeConfigExists = await isDir(integrationContext.opencode.configDir);
|
|
24
|
+
const integrationResults = await uninstallIntegrations(integrationContext);
|
|
25
|
+
const resultByName = new Map(integrationResults.map((result) => [result.name, result]));
|
|
72
26
|
|
|
73
27
|
// Remove installed notify handler.
|
|
74
28
|
await fs.unlink(notifyPath).catch(() => {});
|
|
@@ -83,53 +37,73 @@ async function cmdUninstall(argv) {
|
|
|
83
37
|
process.stdout.write(
|
|
84
38
|
[
|
|
85
39
|
"Uninstalled:",
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
: "- Codex notify:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
: "
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
40
|
+
renderRestoreLine({
|
|
41
|
+
exists: codexConfigExists,
|
|
42
|
+
result: resultByName.get("codex"),
|
|
43
|
+
missingText: "- Codex notify: skipped (config.toml not found)",
|
|
44
|
+
restoredText: (result) =>
|
|
45
|
+
`- Codex notify restored: ${result.detail || integrationContext.codex.configPath}`,
|
|
46
|
+
noChangeText: "- Codex notify: no change",
|
|
47
|
+
skippedText: "- Codex notify: skipped (no backup; not installed)",
|
|
48
|
+
}),
|
|
49
|
+
renderRestoreLine({
|
|
50
|
+
exists: codeConfigExists,
|
|
51
|
+
result: resultByName.get("every-code"),
|
|
52
|
+
missingText: "- Every Code notify: skipped (config.toml not found)",
|
|
53
|
+
restoredText: (result) =>
|
|
54
|
+
`- Every Code notify restored: ${result.detail || integrationContext.everyCode.configPath}`,
|
|
55
|
+
noChangeText: "- Every Code notify: no change",
|
|
56
|
+
skippedText: "- Every Code notify: skipped (no backup; not installed)",
|
|
57
|
+
}),
|
|
58
|
+
renderHookLine({
|
|
59
|
+
exists: claudeConfigExists,
|
|
60
|
+
result: resultByName.get("claude"),
|
|
61
|
+
missingText: "- Claude hooks: skipped (settings.json not found)",
|
|
62
|
+
removedText: (result) =>
|
|
63
|
+
`- Claude hooks removed: ${result.detail || integrationContext.claude.settingsPath}`,
|
|
64
|
+
noChangeText: "- Claude hooks: no change",
|
|
65
|
+
skippedText: "- Claude hooks: skipped",
|
|
66
|
+
}),
|
|
67
|
+
renderHookLine({
|
|
68
|
+
exists: geminiConfigExists,
|
|
69
|
+
result: resultByName.get("gemini"),
|
|
70
|
+
missingText: `- Gemini hooks: skipped (${integrationContext.gemini.configDir} not found)`,
|
|
71
|
+
removedText: (result) =>
|
|
72
|
+
`- Gemini hooks removed: ${result.detail || integrationContext.gemini.settingsPath}`,
|
|
73
|
+
noChangeText: "- Gemini hooks: no change",
|
|
74
|
+
skippedText: "- Gemini hooks: skipped",
|
|
75
|
+
}),
|
|
76
|
+
renderHookLine({
|
|
77
|
+
exists: opencodeConfigExists,
|
|
78
|
+
result: resultByName.get("opencode"),
|
|
79
|
+
missingText: `- Opencode plugin: skipped (${integrationContext.opencode.configDir} not found)`,
|
|
80
|
+
removedText: (result) =>
|
|
81
|
+
`- Opencode plugin removed: ${result.detail || integrationContext.opencode.configDir}`,
|
|
82
|
+
noChangeText: "- Opencode plugin: no change",
|
|
83
|
+
skippedText: "- Opencode plugin: skipped (unexpected content)",
|
|
84
|
+
}),
|
|
85
|
+
renderHookLine({
|
|
86
|
+
exists: true,
|
|
87
|
+
result: resultByName.get("openclaw-session"),
|
|
88
|
+
missingText: "- OpenClaw session plugin: skipped (openclaw config not found)",
|
|
89
|
+
removedText: (result) =>
|
|
90
|
+
`- OpenClaw session plugin removed: ${result.detail || result.openclawConfigPath || "unknown"}`,
|
|
91
|
+
noChangeText: "- OpenClaw session plugin: no change",
|
|
92
|
+
skippedText: "- OpenClaw session plugin: no change",
|
|
93
|
+
unreadableText: (result) =>
|
|
94
|
+
`- OpenClaw session plugin: skipped (${result.detail || "openclaw config unreadable"})`,
|
|
95
|
+
}),
|
|
96
|
+
renderHookLine({
|
|
97
|
+
exists: true,
|
|
98
|
+
result: resultByName.get("openclaw-legacy"),
|
|
99
|
+
missingText: "- OpenClaw hook (legacy): skipped (openclaw config not found)",
|
|
100
|
+
removedText: (result) =>
|
|
101
|
+
`- OpenClaw hook (legacy) removed: ${result.detail || result.openclawConfigPath || "unknown"}`,
|
|
102
|
+
noChangeText: "- OpenClaw hook (legacy): no change",
|
|
103
|
+
skippedText: "- OpenClaw hook (legacy): no change",
|
|
104
|
+
unreadableText: (result) =>
|
|
105
|
+
`- OpenClaw hook (legacy): skipped (${result.detail || "openclaw config unreadable"})`,
|
|
106
|
+
}),
|
|
133
107
|
opts.purge ? `- Purged: ${path.join(home, ".vibeusage")}` : "- Purge: skipped (use --purge)",
|
|
134
108
|
"",
|
|
135
109
|
].join("\n"),
|
|
@@ -157,6 +131,49 @@ async function isFile(p) {
|
|
|
157
131
|
}
|
|
158
132
|
}
|
|
159
133
|
|
|
134
|
+
function renderRestoreLine({ exists, result, missingText, restoredText, noChangeText, skippedText }) {
|
|
135
|
+
if (!exists) return missingText;
|
|
136
|
+
if (!result) return noChangeText;
|
|
137
|
+
if (result.status === "restored" || result.status === "removed") {
|
|
138
|
+
return restoredText(result);
|
|
139
|
+
}
|
|
140
|
+
if (result.skippedReason === "no-backup-not-installed") {
|
|
141
|
+
return skippedText;
|
|
142
|
+
}
|
|
143
|
+
return noChangeText;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderHookLine({
|
|
147
|
+
exists,
|
|
148
|
+
result,
|
|
149
|
+
missingText,
|
|
150
|
+
removedText,
|
|
151
|
+
noChangeText,
|
|
152
|
+
skippedText,
|
|
153
|
+
unreadableText = null,
|
|
154
|
+
}) {
|
|
155
|
+
if (!exists) return missingText;
|
|
156
|
+
if (!result) return noChangeText;
|
|
157
|
+
if (result.status === "removed" || result.status === "updated") {
|
|
158
|
+
return removedText(result);
|
|
159
|
+
}
|
|
160
|
+
if (result.skippedReason === "hook-missing") {
|
|
161
|
+
return noChangeText;
|
|
162
|
+
}
|
|
163
|
+
if (result.skippedReason === "unexpected-content") {
|
|
164
|
+
return skippedText;
|
|
165
|
+
}
|
|
166
|
+
if (result.skippedReason === "openclaw-config-missing") {
|
|
167
|
+
return missingText;
|
|
168
|
+
}
|
|
169
|
+
if (result.skippedReason === "openclaw-config-unreadable") {
|
|
170
|
+
return typeof unreadableText === "function"
|
|
171
|
+
? unreadableText(result)
|
|
172
|
+
: unreadableText || skippedText;
|
|
173
|
+
}
|
|
174
|
+
return noChangeText;
|
|
175
|
+
}
|
|
176
|
+
|
|
160
177
|
async function isDir(p) {
|
|
161
178
|
try {
|
|
162
179
|
const st = await fs.stat(p);
|
package/src/lib/claude-config.js
CHANGED
|
@@ -3,57 +3,64 @@ const path = require("node:path");
|
|
|
3
3
|
|
|
4
4
|
const { ensureDir, readJson, writeJson } = require("./fs");
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const DEFAULT_EVENTS = ["Stop", "SessionEnd"];
|
|
7
7
|
|
|
8
|
-
async function upsertClaudeHook({ settingsPath, hookCommand,
|
|
8
|
+
async function upsertClaudeHook({ settingsPath, hookCommand, events = DEFAULT_EVENTS }) {
|
|
9
9
|
const existing = await readJson(settingsPath);
|
|
10
10
|
const settings = normalizeSettings(existing);
|
|
11
11
|
const hooks = normalizeHooks(settings.hooks);
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
const targetEvents = normalizeEvents(events);
|
|
13
|
+
let changed = false;
|
|
14
|
+
const nextHooks = { ...hooks };
|
|
15
|
+
|
|
16
|
+
for (const event of targetEvents) {
|
|
17
|
+
const entries = normalizeEntries(nextHooks[event]);
|
|
18
|
+
const normalized = normalizeEntriesForCommand(entries, hookCommand);
|
|
19
|
+
if (normalized.changed) {
|
|
20
|
+
nextHooks[event] = normalized.entries;
|
|
21
|
+
changed = true;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (hasHook(entries, hookCommand)) continue;
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
nextHooks[event] = entries.concat([{ hooks: [{ type: "command", command: hookCommand }] }]);
|
|
28
|
+
changed = true;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
const nextHooks = { ...hooks, [event]: nextEntries };
|
|
28
|
-
const nextSettings = { ...settings, hooks: nextHooks };
|
|
31
|
+
if (!changed) return { changed: false, backupPath: null };
|
|
29
32
|
|
|
33
|
+
const nextSettings = { ...settings, hooks: nextHooks };
|
|
30
34
|
const backupPath = await writeClaudeSettings({ settingsPath, settings: nextSettings });
|
|
31
35
|
return { changed: true, backupPath };
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
async function removeClaudeHook({ settingsPath, hookCommand,
|
|
38
|
+
async function removeClaudeHook({ settingsPath, hookCommand, events = DEFAULT_EVENTS }) {
|
|
35
39
|
const existing = await readJson(settingsPath);
|
|
36
40
|
if (!existing) return { removed: false, skippedReason: "settings-missing" };
|
|
37
41
|
|
|
38
42
|
const settings = normalizeSettings(existing);
|
|
39
43
|
const hooks = normalizeHooks(settings.hooks);
|
|
40
|
-
const
|
|
41
|
-
if (entries.length === 0) return { removed: false, skippedReason: "hook-missing" };
|
|
42
|
-
|
|
44
|
+
const targetEvents = normalizeEvents(events);
|
|
43
45
|
let removed = false;
|
|
44
|
-
const
|
|
45
|
-
for (const
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
|
|
46
|
+
const nextHooks = { ...hooks };
|
|
47
|
+
for (const event of targetEvents) {
|
|
48
|
+
const entries = normalizeEntries(hooks[event]);
|
|
49
|
+
if (entries.length === 0) continue;
|
|
50
|
+
|
|
51
|
+
const nextEntries = [];
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const res = stripHookFromEntry(entry, hookCommand);
|
|
54
|
+
if (res.removed) removed = true;
|
|
55
|
+
if (res.entry) nextEntries.push(res.entry);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (nextEntries.length > 0) nextHooks[event] = nextEntries;
|
|
59
|
+
else delete nextHooks[event];
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
if (!removed) return { removed: false, skippedReason: "hook-missing" };
|
|
52
63
|
|
|
53
|
-
const nextHooks = { ...hooks };
|
|
54
|
-
if (nextEntries.length > 0) nextHooks[event] = nextEntries;
|
|
55
|
-
else delete nextHooks[event];
|
|
56
|
-
|
|
57
64
|
const nextSettings = { ...settings };
|
|
58
65
|
if (Object.keys(nextHooks).length > 0) nextSettings.hooks = nextHooks;
|
|
59
66
|
else delete nextSettings.hooks;
|
|
@@ -62,13 +69,31 @@ async function removeClaudeHook({ settingsPath, hookCommand, event = DEFAULT_EVE
|
|
|
62
69
|
return { removed: true, skippedReason: null, backupPath };
|
|
63
70
|
}
|
|
64
71
|
|
|
65
|
-
async function isClaudeHookConfigured({ settingsPath, hookCommand,
|
|
72
|
+
async function isClaudeHookConfigured({ settingsPath, hookCommand, events = DEFAULT_EVENTS }) {
|
|
73
|
+
const probe = await probeClaudeHook({ settingsPath, hookCommand, events });
|
|
74
|
+
return probe.configured;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function probeClaudeHook({ settingsPath, hookCommand, events = DEFAULT_EVENTS }) {
|
|
66
78
|
const settings = await readJson(settingsPath);
|
|
67
|
-
if (!settings || typeof settings !== "object")
|
|
79
|
+
if (!settings || typeof settings !== "object") {
|
|
80
|
+
return { configured: false, anyPresent: false, eventStates: {} };
|
|
81
|
+
}
|
|
68
82
|
const hooks = settings.hooks;
|
|
69
|
-
if (!hooks || typeof hooks !== "object")
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
if (!hooks || typeof hooks !== "object") {
|
|
84
|
+
return { configured: false, anyPresent: false, eventStates: {} };
|
|
85
|
+
}
|
|
86
|
+
const targetEvents = normalizeEvents(events);
|
|
87
|
+
const eventStates = {};
|
|
88
|
+
for (const event of targetEvents) {
|
|
89
|
+
eventStates[event] = hasHook(normalizeEntries(hooks[event]), hookCommand);
|
|
90
|
+
}
|
|
91
|
+
const anyPresent = Object.values(eventStates).some(Boolean);
|
|
92
|
+
return {
|
|
93
|
+
configured: targetEvents.every((event) => eventStates[event] === true),
|
|
94
|
+
anyPresent,
|
|
95
|
+
eventStates,
|
|
96
|
+
};
|
|
72
97
|
}
|
|
73
98
|
|
|
74
99
|
function buildClaudeHookCommand(notifyPath) {
|
|
@@ -88,9 +113,27 @@ function normalizeEntries(raw) {
|
|
|
88
113
|
return Array.isArray(raw) ? raw.slice() : [];
|
|
89
114
|
}
|
|
90
115
|
|
|
116
|
+
function normalizeEvents(raw) {
|
|
117
|
+
const values = Array.isArray(raw) ? raw : [raw];
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const value of values) {
|
|
120
|
+
if (typeof value !== "string") continue;
|
|
121
|
+
const normalized = value.trim();
|
|
122
|
+
if (!normalized || out.includes(normalized)) continue;
|
|
123
|
+
out.push(normalized);
|
|
124
|
+
}
|
|
125
|
+
return out.length > 0 ? out : DEFAULT_EVENTS.slice();
|
|
126
|
+
}
|
|
127
|
+
|
|
91
128
|
function normalizeCommand(cmd) {
|
|
92
129
|
if (Array.isArray(cmd)) return cmd.map((v) => String(v)).join("\u0000");
|
|
93
|
-
if (typeof cmd === "string")
|
|
130
|
+
if (typeof cmd === "string") {
|
|
131
|
+
const raw = cmd.trim();
|
|
132
|
+
if (!raw) return null;
|
|
133
|
+
const parsed = splitShellCommand(raw);
|
|
134
|
+
if (parsed) return parsed.join("\u0000");
|
|
135
|
+
return raw;
|
|
136
|
+
}
|
|
94
137
|
return null;
|
|
95
138
|
}
|
|
96
139
|
|
|
@@ -166,6 +209,56 @@ function quoteArg(value) {
|
|
|
166
209
|
return `"${v.replace(/"/g, '\\"')}"`;
|
|
167
210
|
}
|
|
168
211
|
|
|
212
|
+
function splitShellCommand(raw) {
|
|
213
|
+
const parts = [];
|
|
214
|
+
let current = "";
|
|
215
|
+
let quote = null;
|
|
216
|
+
let escaping = false;
|
|
217
|
+
|
|
218
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
219
|
+
const ch = raw[i];
|
|
220
|
+
|
|
221
|
+
if (escaping) {
|
|
222
|
+
current += ch;
|
|
223
|
+
escaping = false;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (ch === "\\") {
|
|
228
|
+
escaping = true;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (quote) {
|
|
233
|
+
if (ch === quote) {
|
|
234
|
+
quote = null;
|
|
235
|
+
} else {
|
|
236
|
+
current += ch;
|
|
237
|
+
}
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (ch === '"' || ch === "'") {
|
|
242
|
+
quote = ch;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (/\s/.test(ch)) {
|
|
247
|
+
if (current) {
|
|
248
|
+
parts.push(current);
|
|
249
|
+
current = "";
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
current += ch;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (escaping || quote) return null;
|
|
258
|
+
if (current) parts.push(current);
|
|
259
|
+
return parts.length > 0 ? parts : null;
|
|
260
|
+
}
|
|
261
|
+
|
|
169
262
|
async function writeClaudeSettings({ settingsPath, settings }) {
|
|
170
263
|
await ensureDir(path.dirname(settingsPath));
|
|
171
264
|
let backupPath = null;
|
|
@@ -183,8 +276,10 @@ async function writeClaudeSettings({ settingsPath, settings }) {
|
|
|
183
276
|
}
|
|
184
277
|
|
|
185
278
|
module.exports = {
|
|
279
|
+
DEFAULT_EVENTS,
|
|
186
280
|
upsertClaudeHook,
|
|
187
281
|
removeClaudeHook,
|
|
188
282
|
isClaudeHookConfigured,
|
|
283
|
+
probeClaudeHook,
|
|
189
284
|
buildClaudeHookCommand,
|
|
190
285
|
};
|