vibeusage 0.2.18 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.js +1 -0
- package/src/commands/init.js +51 -1
- package/src/commands/status.js +3 -0
- package/src/commands/uninstall.js +7 -0
- package/src/lib/diagnostics.js +6 -1
- package/src/lib/doctor.js +2 -1
- package/src/lib/openclaw-hook.js +358 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -57,6 +57,7 @@ function printHelp() {
|
|
|
57
57
|
' - --dry-run previews changes without writing files.',
|
|
58
58
|
' - optional: --link-code <code> skips browser login when provided by Dashboard.',
|
|
59
59
|
' - Every Code notify installs when ~/.code/config.toml exists.',
|
|
60
|
+
' - OpenClaw hook auto-links when OpenClaw is installed (requires gateway restart).',
|
|
60
61
|
' - auto sync waits for a device token.',
|
|
61
62
|
' - optional: VIBEUSAGE_DASHBOARD_URL or --dashboard-url for hosted landing.',
|
|
62
63
|
' - sync parses ~/.codex/sessions/**/rollout-*.jsonl and ~/.code/sessions/**/rollout-*.jsonl, then uploads token deltas.',
|
package/src/commands/init.js
CHANGED
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
isGeminiHookConfigured
|
|
23
23
|
} = require('../lib/gemini-config');
|
|
24
24
|
const { resolveOpencodeConfigDir, upsertOpencodePlugin, isOpencodePluginInstalled } = require('../lib/opencode-config');
|
|
25
|
+
const { installOpenclawHook, probeOpenclawHookState } = require('../lib/openclaw-hook');
|
|
25
26
|
const { beginBrowserAuth, openInBrowser } = require('../lib/browser-auth');
|
|
26
27
|
const {
|
|
27
28
|
issueDeviceTokenWithPassword,
|
|
@@ -185,7 +186,7 @@ function renderWelcome() {
|
|
|
185
186
|
DIVIDER,
|
|
186
187
|
'',
|
|
187
188
|
'This tool will:',
|
|
188
|
-
' - Analyze your local AI CLI configurations (Codex, Every Code, Claude, Gemini, Opencode)',
|
|
189
|
+
' - Analyze your local AI CLI configurations (Codex, Every Code, Claude, Gemini, Opencode, OpenClaw)',
|
|
189
190
|
' - Set up lightweight hooks to track your flow state',
|
|
190
191
|
' - Link your device to your VibeScore account',
|
|
191
192
|
'',
|
|
@@ -336,6 +337,7 @@ function buildIntegrationTargets({ home, trackerDir, notifyPath }) {
|
|
|
336
337
|
const opencodeConfigDir = resolveOpencodeConfigDir({ home, env: process.env });
|
|
337
338
|
|
|
338
339
|
return {
|
|
340
|
+
trackerDir,
|
|
339
341
|
codexConfigPath,
|
|
340
342
|
codeConfigPath,
|
|
341
343
|
notifyOriginalPath,
|
|
@@ -406,6 +408,34 @@ async function applyIntegrationSetup({ home, trackerDir, notifyPath, notifyOrigi
|
|
|
406
408
|
summary.push({ label: 'Opencode Plugin', status: opencodeResult?.changed ? 'installed' : 'set', detail: 'Plugin installed' });
|
|
407
409
|
}
|
|
408
410
|
|
|
411
|
+
const openclawBefore = await probeOpenclawHookState({ home, trackerDir, env: process.env });
|
|
412
|
+
const openclawInstall = await installOpenclawHook({ home, trackerDir, packageName: 'vibeusage', env: process.env });
|
|
413
|
+
if (openclawInstall?.skippedReason === 'openclaw-cli-missing') {
|
|
414
|
+
summary.push({ label: 'OpenClaw Hook', status: 'skipped', detail: 'OpenClaw CLI not found' });
|
|
415
|
+
} else if (openclawInstall?.skippedReason === 'openclaw-hooks-install-failed') {
|
|
416
|
+
summary.push({
|
|
417
|
+
label: 'OpenClaw Hook',
|
|
418
|
+
status: 'skipped',
|
|
419
|
+
detail: `Install failed${openclawInstall.error ? `: ${openclawInstall.error}` : ''}`
|
|
420
|
+
});
|
|
421
|
+
} else if (openclawInstall?.skippedReason === 'openclaw-config-unreadable') {
|
|
422
|
+
summary.push({
|
|
423
|
+
label: 'OpenClaw Hook',
|
|
424
|
+
status: 'skipped',
|
|
425
|
+
detail: openclawInstall.error ? `OpenClaw config unreadable: ${openclawInstall.error}` : 'OpenClaw config unreadable'
|
|
426
|
+
});
|
|
427
|
+
} else if (openclawInstall?.configured) {
|
|
428
|
+
summary.push({
|
|
429
|
+
label: 'OpenClaw Hook',
|
|
430
|
+
status: openclawBefore?.configured ? 'set' : 'installed',
|
|
431
|
+
detail: openclawBefore?.configured
|
|
432
|
+
? 'Hook already linked'
|
|
433
|
+
: 'Hook linked (restart OpenClaw gateway to activate)'
|
|
434
|
+
});
|
|
435
|
+
} else {
|
|
436
|
+
summary.push({ label: 'OpenClaw Hook', status: 'skipped', detail: 'OpenClaw hook unavailable' });
|
|
437
|
+
}
|
|
438
|
+
|
|
409
439
|
const codeProbe = await probeFile(context.codeConfigPath);
|
|
410
440
|
if (codeProbe.exists) {
|
|
411
441
|
const result = await upsertEveryCodeNotify({
|
|
@@ -427,6 +457,7 @@ async function applyIntegrationSetup({ home, trackerDir, notifyPath, notifyOrigi
|
|
|
427
457
|
|
|
428
458
|
async function previewIntegrations({ context }) {
|
|
429
459
|
const summary = [];
|
|
460
|
+
const home = os.homedir();
|
|
430
461
|
|
|
431
462
|
const codexProbe = await probeFile(context.codexConfigPath);
|
|
432
463
|
if (codexProbe.exists) {
|
|
@@ -484,6 +515,25 @@ async function previewIntegrations({ context }) {
|
|
|
484
515
|
detail: opencodeDetail
|
|
485
516
|
});
|
|
486
517
|
|
|
518
|
+
const openclawState = await probeOpenclawHookState({ home, trackerDir: context.trackerDir, env: process.env });
|
|
519
|
+
if (openclawState?.skippedReason === 'openclaw-config-missing') {
|
|
520
|
+
summary.push({ label: 'OpenClaw Hook', status: 'skipped', detail: 'OpenClaw config not found' });
|
|
521
|
+
} else if (openclawState?.skippedReason === 'openclaw-config-unreadable') {
|
|
522
|
+
summary.push({
|
|
523
|
+
label: 'OpenClaw Hook',
|
|
524
|
+
status: 'skipped',
|
|
525
|
+
detail: openclawState.error ? `OpenClaw config unreadable: ${openclawState.error}` : 'OpenClaw config unreadable'
|
|
526
|
+
});
|
|
527
|
+
} else {
|
|
528
|
+
summary.push({
|
|
529
|
+
label: 'OpenClaw Hook',
|
|
530
|
+
status: openclawState?.configured ? 'set' : 'installed',
|
|
531
|
+
detail: openclawState?.configured
|
|
532
|
+
? 'Hook already linked'
|
|
533
|
+
: 'Will link hook (restart OpenClaw gateway to activate)'
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
487
537
|
const codeProbe = await probeFile(context.codeConfigPath);
|
|
488
538
|
if (codeProbe.exists) {
|
|
489
539
|
const existing = await readEveryCodeNotify(context.codeConfigPath);
|
package/src/commands/status.js
CHANGED
|
@@ -15,6 +15,7 @@ const { resolveOpencodeConfigDir, isOpencodePluginInstalled } = require('../lib/
|
|
|
15
15
|
const { collectLocalSubscriptions } = require('../lib/subscriptions');
|
|
16
16
|
const { normalizeState: normalizeUploadState } = require('../lib/upload-throttle');
|
|
17
17
|
const { collectTrackerDiagnostics } = require('../lib/diagnostics');
|
|
18
|
+
const { probeOpenclawHookState } = require('../lib/openclaw-hook');
|
|
18
19
|
const { resolveTrackerPaths } = require('../lib/tracker-paths');
|
|
19
20
|
|
|
20
21
|
async function cmdStatus(argv = []) {
|
|
@@ -74,6 +75,7 @@ async function cmdStatus(argv = []) {
|
|
|
74
75
|
hookCommand: geminiHookCommand
|
|
75
76
|
});
|
|
76
77
|
const opencodePluginConfigured = await isOpencodePluginInstalled({ configDir: opencodeConfigDir });
|
|
78
|
+
const openclawHookState = await probeOpenclawHookState({ home, trackerDir, env: process.env });
|
|
77
79
|
|
|
78
80
|
const lastUpload = uploadThrottle.lastSuccessMs
|
|
79
81
|
? parseEpochMsToIso(uploadThrottle.lastSuccessMs)
|
|
@@ -123,6 +125,7 @@ async function cmdStatus(argv = []) {
|
|
|
123
125
|
`- Claude hooks: ${claudeHookConfigured ? 'set' : 'unset'}`,
|
|
124
126
|
`- Gemini hooks: ${geminiHookConfigured ? 'set' : 'unset'}`,
|
|
125
127
|
`- Opencode plugin: ${opencodePluginConfigured ? 'set' : 'unset'}`,
|
|
128
|
+
`- OpenClaw hook: ${openclawHookState?.configured ? 'set' : 'unset'}`,
|
|
126
129
|
...subscriptionLines,
|
|
127
130
|
''
|
|
128
131
|
]
|
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
removeGeminiHook
|
|
12
12
|
} = require('../lib/gemini-config');
|
|
13
13
|
const { resolveOpencodeConfigDir, removeOpencodePlugin } = require('../lib/opencode-config');
|
|
14
|
+
const { removeOpenclawHookConfig } = require('../lib/openclaw-hook');
|
|
14
15
|
const { resolveTrackerPaths } = require('../lib/tracker-paths');
|
|
15
16
|
|
|
16
17
|
async function cmdUninstall(argv) {
|
|
@@ -61,6 +62,7 @@ async function cmdUninstall(argv) {
|
|
|
61
62
|
const opencodeRemove = opencodeConfigExists
|
|
62
63
|
? await removeOpencodePlugin({ configDir: opencodeConfigDir })
|
|
63
64
|
: { removed: false, skippedReason: 'config-missing' };
|
|
65
|
+
const openclawHookRemove = await removeOpenclawHookConfig({ home, trackerDir, env: process.env });
|
|
64
66
|
|
|
65
67
|
// Remove installed notify handler.
|
|
66
68
|
await fs.unlink(notifyPath).catch(() => {});
|
|
@@ -112,6 +114,11 @@ async function cmdUninstall(argv) {
|
|
|
112
114
|
? '- Opencode plugin: skipped (unexpected content)'
|
|
113
115
|
: '- Opencode plugin: skipped'
|
|
114
116
|
: `- Opencode plugin: skipped (${opencodeConfigDir} not found)`,
|
|
117
|
+
openclawHookRemove?.removed
|
|
118
|
+
? `- OpenClaw hook removed: ${openclawHookRemove.openclawConfigPath}`
|
|
119
|
+
: openclawHookRemove?.skippedReason === 'openclaw-config-missing'
|
|
120
|
+
? '- OpenClaw hook: skipped (openclaw config not found)'
|
|
121
|
+
: '- OpenClaw hook: no change',
|
|
115
122
|
opts.purge ? `- Purged: ${path.join(home, '.vibeusage')}` : '- Purge: skipped (use --purge)',
|
|
116
123
|
''
|
|
117
124
|
].join('\n')
|
package/src/lib/diagnostics.js
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
} = require('./gemini-config');
|
|
14
14
|
const { resolveOpencodeConfigDir, isOpencodePluginInstalled } = require('./opencode-config');
|
|
15
15
|
const { normalizeState: normalizeUploadState } = require('./upload-throttle');
|
|
16
|
+
const { probeOpenclawHookState } = require('./openclaw-hook');
|
|
16
17
|
const { resolveTrackerPaths } = require('./tracker-paths');
|
|
17
18
|
|
|
18
19
|
async function collectTrackerDiagnostics({
|
|
@@ -68,6 +69,7 @@ async function collectTrackerDiagnostics({
|
|
|
68
69
|
hookCommand: geminiHookCommand
|
|
69
70
|
});
|
|
70
71
|
const opencodePluginConfigured = await isOpencodePluginInstalled({ configDir: opencodeConfigDir });
|
|
72
|
+
const openclawHookState = await probeOpenclawHookState({ home, trackerDir, env: process.env });
|
|
71
73
|
|
|
72
74
|
const lastSuccessAt = uploadThrottle.lastSuccessMs ? new Date(uploadThrottle.lastSuccessMs).toISOString() : null;
|
|
73
75
|
const autoRetryAt = parseEpochMsToIso(autoRetry?.retryAtMs);
|
|
@@ -117,7 +119,10 @@ async function collectTrackerDiagnostics({
|
|
|
117
119
|
every_code_notify: everyCodeNotify,
|
|
118
120
|
claude_hook_configured: claudeHookConfigured,
|
|
119
121
|
gemini_hook_configured: geminiHookConfigured,
|
|
120
|
-
opencode_plugin_configured: opencodePluginConfigured
|
|
122
|
+
opencode_plugin_configured: opencodePluginConfigured,
|
|
123
|
+
openclaw_hook_configured: Boolean(openclawHookState?.configured),
|
|
124
|
+
openclaw_hook_linked: Boolean(openclawHookState?.linked),
|
|
125
|
+
openclaw_hook_enabled: Boolean(openclawHookState?.enabled)
|
|
121
126
|
},
|
|
122
127
|
upload: {
|
|
123
128
|
last_success_at: lastSuccessAt,
|
package/src/lib/doctor.js
CHANGED
|
@@ -305,7 +305,8 @@ function buildDiagnosticsChecks(diagnostics) {
|
|
|
305
305
|
notify.every_code_notify_configured ||
|
|
306
306
|
notify.claude_hook_configured ||
|
|
307
307
|
notify.gemini_hook_configured ||
|
|
308
|
-
notify.opencode_plugin_configured
|
|
308
|
+
notify.opencode_plugin_configured ||
|
|
309
|
+
notify.openclaw_hook_configured
|
|
309
310
|
);
|
|
310
311
|
|
|
311
312
|
checks.push({
|
|
@@ -0,0 +1,358 @@
|
|
|
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
|
+
const cp = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
const OPENCLAW_HOOK_NAME = 'vibeusage-openclaw-sync';
|
|
8
|
+
const OPENCLAW_HOOK_DIRNAME = 'openclaw-hook';
|
|
9
|
+
|
|
10
|
+
function resolveOpenclawHookPaths({ home = os.homedir(), trackerDir, env = process.env } = {}) {
|
|
11
|
+
if (!trackerDir) throw new Error('trackerDir is required');
|
|
12
|
+
|
|
13
|
+
const openclawConfigPath =
|
|
14
|
+
normalizeString(env.OPENCLAW_CONFIG_PATH) || path.join(home, '.openclaw', 'openclaw.json');
|
|
15
|
+
|
|
16
|
+
const openclawHome =
|
|
17
|
+
normalizeString(env.VIBEUSAGE_OPENCLAW_HOME) ||
|
|
18
|
+
normalizeString(env.OPENCLAW_STATE_DIR) ||
|
|
19
|
+
path.join(home, '.openclaw');
|
|
20
|
+
|
|
21
|
+
const hookDir = path.join(trackerDir, OPENCLAW_HOOK_DIRNAME);
|
|
22
|
+
const hookEntryDir = path.join(hookDir, OPENCLAW_HOOK_NAME);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
hookName: OPENCLAW_HOOK_NAME,
|
|
26
|
+
hookDir,
|
|
27
|
+
hookEntryDir,
|
|
28
|
+
openclawConfigPath,
|
|
29
|
+
openclawHome
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function installOpenclawHook({ home = os.homedir(), trackerDir, packageName = 'vibeusage', env = process.env } = {}) {
|
|
34
|
+
const paths = resolveOpenclawHookPaths({ home, trackerDir, env });
|
|
35
|
+
|
|
36
|
+
await ensureOpenclawHookFiles({
|
|
37
|
+
hookDir: paths.hookDir,
|
|
38
|
+
trackerDir,
|
|
39
|
+
packageName,
|
|
40
|
+
openclawHome: paths.openclawHome
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const installResult = runOpenclawCli(['hooks', 'install', '--link', paths.hookDir], env);
|
|
44
|
+
if (installResult.skippedReason) {
|
|
45
|
+
return { configured: false, ...paths, ...installResult };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const state = await probeOpenclawHookState({ home, trackerDir, env });
|
|
49
|
+
return {
|
|
50
|
+
configured: state.configured,
|
|
51
|
+
changed: /Linked hook path:/i.test(installResult.stdout || ''),
|
|
52
|
+
...paths,
|
|
53
|
+
stdout: installResult.stdout,
|
|
54
|
+
stderr: installResult.stderr,
|
|
55
|
+
code: installResult.code
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function ensureOpenclawHookFiles({ hookDir, trackerDir, packageName = 'vibeusage', openclawHome } = {}) {
|
|
60
|
+
if (!hookDir || !trackerDir) throw new Error('hookDir and trackerDir are required');
|
|
61
|
+
|
|
62
|
+
const hookEntryDir = path.join(hookDir, OPENCLAW_HOOK_NAME);
|
|
63
|
+
await fs.mkdir(hookEntryDir, { recursive: true });
|
|
64
|
+
|
|
65
|
+
const hookMdPath = path.join(hookEntryDir, 'HOOK.md');
|
|
66
|
+
const handlerPath = path.join(hookEntryDir, 'handler.js');
|
|
67
|
+
|
|
68
|
+
await fs.writeFile(hookMdPath, buildHookMarkdown(), 'utf8');
|
|
69
|
+
await fs.writeFile(
|
|
70
|
+
handlerPath,
|
|
71
|
+
buildHookHandler({ trackerDir, packageName, openclawHome: openclawHome || path.join(os.homedir(), '.openclaw') }),
|
|
72
|
+
'utf8'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function probeOpenclawHookState({ home = os.homedir(), trackerDir, env = process.env } = {}) {
|
|
77
|
+
const paths = resolveOpenclawHookPaths({ home, trackerDir, env });
|
|
78
|
+
const { openclawConfigPath, hookDir, hookEntryDir, hookName } = paths;
|
|
79
|
+
|
|
80
|
+
const hookFilesReady =
|
|
81
|
+
fssync.existsSync(path.join(hookEntryDir, 'HOOK.md')) && fssync.existsSync(path.join(hookEntryDir, 'handler.js'));
|
|
82
|
+
|
|
83
|
+
let cfg = null;
|
|
84
|
+
try {
|
|
85
|
+
const raw = await fs.readFile(openclawConfigPath, 'utf8');
|
|
86
|
+
cfg = JSON.parse(raw);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (err?.code === 'ENOENT' || err?.code === 'ENOTDIR') {
|
|
89
|
+
return {
|
|
90
|
+
configured: false,
|
|
91
|
+
enabled: false,
|
|
92
|
+
linked: false,
|
|
93
|
+
hookFilesReady,
|
|
94
|
+
skippedReason: 'openclaw-config-missing',
|
|
95
|
+
...paths
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
configured: false,
|
|
100
|
+
enabled: false,
|
|
101
|
+
linked: false,
|
|
102
|
+
hookFilesReady,
|
|
103
|
+
skippedReason: 'openclaw-config-unreadable',
|
|
104
|
+
error: err?.message || String(err),
|
|
105
|
+
...paths
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const enabled = Boolean(cfg?.hooks?.internal?.entries?.[hookName]?.enabled);
|
|
110
|
+
const extraDirs = Array.isArray(cfg?.hooks?.internal?.load?.extraDirs) ? cfg.hooks.internal.load.extraDirs : [];
|
|
111
|
+
const normalizedHookDir = path.resolve(hookDir);
|
|
112
|
+
const linked = extraDirs.some((entry) => path.resolve(String(entry || '')) === normalizedHookDir);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
configured: enabled && linked,
|
|
116
|
+
enabled,
|
|
117
|
+
linked,
|
|
118
|
+
hookFilesReady,
|
|
119
|
+
...paths
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function removeOpenclawHookConfig({ home = os.homedir(), trackerDir, env = process.env } = {}) {
|
|
124
|
+
const paths = resolveOpenclawHookPaths({ home, trackerDir, env });
|
|
125
|
+
const { openclawConfigPath, hookDir, hookName } = paths;
|
|
126
|
+
|
|
127
|
+
let cfg;
|
|
128
|
+
try {
|
|
129
|
+
cfg = JSON.parse(await fs.readFile(openclawConfigPath, 'utf8'));
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (err?.code === 'ENOENT' || err?.code === 'ENOTDIR') {
|
|
132
|
+
return { removed: false, skippedReason: 'openclaw-config-missing', ...paths };
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
removed: false,
|
|
136
|
+
skippedReason: 'openclaw-config-unreadable',
|
|
137
|
+
error: err?.message || String(err),
|
|
138
|
+
...paths
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let changed = false;
|
|
143
|
+
const hooks = cfg?.hooks;
|
|
144
|
+
const internal = hooks?.internal;
|
|
145
|
+
|
|
146
|
+
if (internal?.entries && Object.prototype.hasOwnProperty.call(internal.entries, hookName)) {
|
|
147
|
+
delete internal.entries[hookName];
|
|
148
|
+
changed = true;
|
|
149
|
+
if (Object.keys(internal.entries).length === 0) delete internal.entries;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (internal?.load && Array.isArray(internal.load.extraDirs)) {
|
|
153
|
+
const before = internal.load.extraDirs;
|
|
154
|
+
const target = path.resolve(hookDir);
|
|
155
|
+
const after = before.filter((entry) => path.resolve(String(entry || '')) !== target);
|
|
156
|
+
if (after.length !== before.length) {
|
|
157
|
+
internal.load.extraDirs = after;
|
|
158
|
+
changed = true;
|
|
159
|
+
if (after.length === 0) delete internal.load.extraDirs;
|
|
160
|
+
if (Object.keys(internal.load).length === 0) delete internal.load;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (internal?.installs && typeof internal.installs === 'object') {
|
|
165
|
+
const installs = internal.installs;
|
|
166
|
+
if (Object.prototype.hasOwnProperty.call(installs, hookName)) {
|
|
167
|
+
delete installs[hookName];
|
|
168
|
+
changed = true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const target = path.resolve(hookDir);
|
|
172
|
+
for (const [id, entry] of Object.entries(installs)) {
|
|
173
|
+
const sourcePath = normalizeString(entry?.sourcePath);
|
|
174
|
+
const installPath = normalizeString(entry?.installPath);
|
|
175
|
+
if (
|
|
176
|
+
(sourcePath && path.resolve(sourcePath) === target) ||
|
|
177
|
+
(installPath && path.resolve(installPath) === target)
|
|
178
|
+
) {
|
|
179
|
+
delete installs[id];
|
|
180
|
+
changed = true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (Object.keys(installs).length === 0) delete internal.installs;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (internal && Object.keys(internal).length === 0) {
|
|
188
|
+
delete hooks.internal;
|
|
189
|
+
changed = true;
|
|
190
|
+
}
|
|
191
|
+
if (hooks && Object.keys(hooks).length === 0) {
|
|
192
|
+
delete cfg.hooks;
|
|
193
|
+
changed = true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (changed) {
|
|
197
|
+
await fs.writeFile(openclawConfigPath, `${JSON.stringify(cfg, null, 2)}\n`, 'utf8');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await fs.rm(hookDir, { recursive: true, force: true }).catch(() => {});
|
|
201
|
+
|
|
202
|
+
return { removed: changed, ...paths };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function runOpenclawCli(args, env = process.env) {
|
|
206
|
+
let res;
|
|
207
|
+
try {
|
|
208
|
+
res = cp.spawnSync('openclaw', args, {
|
|
209
|
+
env,
|
|
210
|
+
encoding: 'utf8',
|
|
211
|
+
timeout: 30_000
|
|
212
|
+
});
|
|
213
|
+
} catch (err) {
|
|
214
|
+
return {
|
|
215
|
+
code: 1,
|
|
216
|
+
skippedReason: err?.code === 'ENOENT' ? 'openclaw-cli-missing' : 'openclaw-cli-error',
|
|
217
|
+
error: err?.message || String(err),
|
|
218
|
+
stdout: '',
|
|
219
|
+
stderr: ''
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (res.error?.code === 'ENOENT') {
|
|
224
|
+
return {
|
|
225
|
+
code: 1,
|
|
226
|
+
skippedReason: 'openclaw-cli-missing',
|
|
227
|
+
error: res.error.message,
|
|
228
|
+
stdout: res.stdout || '',
|
|
229
|
+
stderr: res.stderr || ''
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if ((res.status || 0) !== 0) {
|
|
234
|
+
return {
|
|
235
|
+
code: Number(res.status || 1),
|
|
236
|
+
skippedReason: 'openclaw-hooks-install-failed',
|
|
237
|
+
error: (res.stderr || res.stdout || '').trim() || 'openclaw hooks install failed',
|
|
238
|
+
stdout: res.stdout || '',
|
|
239
|
+
stderr: res.stderr || ''
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
code: 0,
|
|
245
|
+
stdout: res.stdout || '',
|
|
246
|
+
stderr: res.stderr || ''
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function buildHookMarkdown() {
|
|
251
|
+
return `---
|
|
252
|
+
name: ${OPENCLAW_HOOK_NAME}
|
|
253
|
+
description: "Trigger vibeusage sync when OpenClaw sessions roll over"
|
|
254
|
+
metadata:
|
|
255
|
+
{ "openclaw": { "emoji": "📈", "events": ["command:new", "command:stop"], "requires": { "bins": ["node"] } } }
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
# VibeUsage OpenClaw Sync Hook
|
|
259
|
+
|
|
260
|
+
Triggers non-blocking 'vibeusage sync --auto --from-openclaw' runs when OpenClaw command events indicate a session rollover/stop.
|
|
261
|
+
`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function buildHookHandler({ trackerDir, packageName = 'vibeusage', openclawHome }) {
|
|
265
|
+
const trackerBinPath = path.join(trackerDir, 'app', 'bin', 'tracker.js');
|
|
266
|
+
const fallbackPkg = packageName || 'vibeusage';
|
|
267
|
+
const safeOpenclawHome = openclawHome || path.join(os.homedir(), '.openclaw');
|
|
268
|
+
|
|
269
|
+
return `'use strict';\n` +
|
|
270
|
+
`const fs = require('node:fs');\n` +
|
|
271
|
+
`const path = require('node:path');\n` +
|
|
272
|
+
`const cp = require('node:child_process');\n` +
|
|
273
|
+
`const trackerDir = ${JSON.stringify(trackerDir)};\n` +
|
|
274
|
+
`const trackerBinPath = ${JSON.stringify(trackerBinPath)};\n` +
|
|
275
|
+
`const fallbackPkg = ${JSON.stringify(fallbackPkg)};\n` +
|
|
276
|
+
`const openclawHome = ${JSON.stringify(safeOpenclawHome)};\n` +
|
|
277
|
+
`const throttlePath = path.join(trackerDir, 'openclaw.sync.throttle');\n` +
|
|
278
|
+
`const depsMarkerPath = path.join(trackerDir, 'app', 'node_modules', '@insforge', 'sdk', 'package.json');\n` +
|
|
279
|
+
`const THROTTLE_MS = 15_000;\n` +
|
|
280
|
+
`\n` +
|
|
281
|
+
`module.exports = async function handler(event) {\n` +
|
|
282
|
+
` try {\n` +
|
|
283
|
+
` if (!event || event.type !== 'command') return;\n` +
|
|
284
|
+
` if (event.action !== 'new' && event.action !== 'stop') return;\n` +
|
|
285
|
+
`\n` +
|
|
286
|
+
` const sessionKey = normalize(event.sessionKey);\n` +
|
|
287
|
+
` const agentId = parseAgentId(sessionKey);\n` +
|
|
288
|
+
` if (!agentId) return;\n` +
|
|
289
|
+
`\n` +
|
|
290
|
+
` const sessionId = resolveSessionId(event);\n` +
|
|
291
|
+
` if (!sessionId) return;\n` +
|
|
292
|
+
`\n` +
|
|
293
|
+
` const now = Date.now();\n` +
|
|
294
|
+
` let last = 0;\n` +
|
|
295
|
+
` try { last = Number(fs.readFileSync(throttlePath, 'utf8')) || 0; } catch (_) {}\n` +
|
|
296
|
+
` if (now - last < THROTTLE_MS) return;\n` +
|
|
297
|
+
` try {\n` +
|
|
298
|
+
` fs.mkdirSync(trackerDir, { recursive: true });\n` +
|
|
299
|
+
` fs.writeFileSync(throttlePath, String(now), 'utf8');\n` +
|
|
300
|
+
` } catch (_) {}\n` +
|
|
301
|
+
`\n` +
|
|
302
|
+
` const env = {\n` +
|
|
303
|
+
` ...process.env,\n` +
|
|
304
|
+
` VIBEUSAGE_OPENCLAW_AGENT_ID: agentId,\n` +
|
|
305
|
+
` VIBEUSAGE_OPENCLAW_PREV_SESSION_ID: sessionId,\n` +
|
|
306
|
+
` VIBEUSAGE_OPENCLAW_HOME: openclawHome\n` +
|
|
307
|
+
` };\n` +
|
|
308
|
+
`\n` +
|
|
309
|
+
` const hasLocalRuntime = fs.existsSync(trackerBinPath);\n` +
|
|
310
|
+
` const hasLocalDeps = fs.existsSync(depsMarkerPath);\n` +
|
|
311
|
+
` const cmd = hasLocalRuntime && hasLocalDeps\n` +
|
|
312
|
+
` ? [process.execPath, trackerBinPath, 'sync', '--auto', '--from-openclaw']\n` +
|
|
313
|
+
` : ['npx', '--yes', fallbackPkg, 'sync', '--auto', '--from-openclaw'];\n` +
|
|
314
|
+
`\n` +
|
|
315
|
+
` const child = cp.spawn(cmd[0], cmd.slice(1), { detached: true, stdio: 'ignore', env });\n` +
|
|
316
|
+
` child.unref();\n` +
|
|
317
|
+
` } catch (_) {}\n` +
|
|
318
|
+
`};\n` +
|
|
319
|
+
`\n` +
|
|
320
|
+
`function normalize(v) {\n` +
|
|
321
|
+
` if (typeof v !== 'string') return null;\n` +
|
|
322
|
+
` const s = v.trim();\n` +
|
|
323
|
+
` return s.length > 0 ? s : null;\n` +
|
|
324
|
+
`}\n` +
|
|
325
|
+
`\n` +
|
|
326
|
+
`function parseAgentId(sessionKey) {\n` +
|
|
327
|
+
` const s = normalize(sessionKey);\n` +
|
|
328
|
+
` if (!s || !s.startsWith('agent:')) return null;\n` +
|
|
329
|
+
` const parts = s.split(':');\n` +
|
|
330
|
+
` return parts.length >= 2 ? normalize(parts[1]) : null;\n` +
|
|
331
|
+
`}\n` +
|
|
332
|
+
`\n` +
|
|
333
|
+
`function resolveSessionId(event) {\n` +
|
|
334
|
+
` const ctx = (event && event.context && typeof event.context === 'object') ? event.context : {};\n` +
|
|
335
|
+
` return (\n` +
|
|
336
|
+
` normalize(ctx.previousSessionEntry && ctx.previousSessionEntry.sessionId) ||\n` +
|
|
337
|
+
` normalize(ctx.previousSessionId) ||\n` +
|
|
338
|
+
` normalize(ctx.sessionEntry && ctx.sessionEntry.sessionId) ||\n` +
|
|
339
|
+
` normalize(ctx.sessionId)\n` +
|
|
340
|
+
` );\n` +
|
|
341
|
+
`}\n`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function normalizeString(value) {
|
|
345
|
+
if (typeof value !== 'string') return null;
|
|
346
|
+
const trimmed = value.trim();
|
|
347
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
module.exports = {
|
|
351
|
+
OPENCLAW_HOOK_NAME,
|
|
352
|
+
OPENCLAW_HOOK_DIRNAME,
|
|
353
|
+
resolveOpenclawHookPaths,
|
|
354
|
+
ensureOpenclawHookFiles,
|
|
355
|
+
installOpenclawHook,
|
|
356
|
+
probeOpenclawHookState,
|
|
357
|
+
removeOpenclawHookConfig
|
|
358
|
+
};
|