vibeusage 0.3.1 → 0.3.2

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.
@@ -1,123 +0,0 @@
1
- const { probeOpenclawHookState, removeOpenclawHookConfig } = require("../openclaw-hook");
2
-
3
- module.exports = {
4
- name: "openclaw-legacy",
5
- summaryLabel: "OpenClaw Hook (legacy)",
6
- statusLabel: "OpenClaw hook (legacy)",
7
- async probe(ctx) {
8
- const state = await probeOpenclawHookState({
9
- home: ctx.home,
10
- trackerDir: ctx.trackerPaths.trackerDir,
11
- env: ctx.env,
12
- });
13
- if (state?.skippedReason === "openclaw-config-missing") {
14
- return baseProbe(this, { status: "not_installed", detail: "OpenClaw config not found" });
15
- }
16
- if (state?.skippedReason === "openclaw-config-unreadable") {
17
- return baseProbe(this, {
18
- status: "unreadable",
19
- detail: state.error
20
- ? `OpenClaw config unreadable: ${state.error}`
21
- : "OpenClaw config unreadable",
22
- linked: Boolean(state.linked),
23
- enabled: Boolean(state.enabled),
24
- });
25
- }
26
- if (state?.configured || state?.linked || state?.enabled) {
27
- return baseProbe(this, {
28
- status: "unsupported_legacy",
29
- detail: "Legacy OpenClaw hook detected; run vibeusage init",
30
- linked: Boolean(state.linked),
31
- enabled: Boolean(state.enabled),
32
- });
33
- }
34
- return baseProbe(this, { status: "not_installed", detail: "Legacy hook not installed" });
35
- },
36
- async install(ctx) {
37
- const state = await probeOpenclawHookState({
38
- home: ctx.home,
39
- trackerDir: ctx.trackerPaths.trackerDir,
40
- env: ctx.env,
41
- });
42
- if (state?.skippedReason === "openclaw-config-unreadable") {
43
- return action(
44
- this,
45
- "skipped",
46
- false,
47
- state.error ? `OpenClaw config unreadable: ${state.error}` : "OpenClaw config unreadable",
48
- { skippedReason: state.skippedReason },
49
- );
50
- }
51
- if (!(state?.configured || state?.linked || state?.enabled)) {
52
- return action(this, "unchanged", false, "no change");
53
- }
54
- const result = await removeOpenclawHookConfig({
55
- home: ctx.home,
56
- trackerDir: ctx.trackerPaths.trackerDir,
57
- env: ctx.env,
58
- });
59
- if (result?.removed) {
60
- return action(this, "updated", true, "Removed legacy command hook");
61
- }
62
- if (result?.skippedReason === "openclaw-config-unreadable") {
63
- return action(
64
- this,
65
- "skipped",
66
- false,
67
- result.error ? `OpenClaw config unreadable: ${result.error}` : "OpenClaw config unreadable",
68
- { skippedReason: result.skippedReason },
69
- );
70
- }
71
- return action(this, "unchanged", false, "no change");
72
- },
73
- async uninstall(ctx) {
74
- const result = await removeOpenclawHookConfig({
75
- home: ctx.home,
76
- trackerDir: ctx.trackerPaths.trackerDir,
77
- env: ctx.env,
78
- });
79
- if (result?.removed) {
80
- return action(this, "removed", true, result.openclawConfigPath);
81
- }
82
- if (result?.skippedReason === "openclaw-config-missing") {
83
- return action(this, "skipped", false, "openclaw config not found", {
84
- skippedReason: result.skippedReason,
85
- });
86
- }
87
- if (result?.skippedReason === "openclaw-config-unreadable") {
88
- return action(
89
- this,
90
- "skipped",
91
- false,
92
- result.error ? `openclaw config unreadable: ${result.error}` : "openclaw config unreadable",
93
- { skippedReason: result.skippedReason },
94
- );
95
- }
96
- return action(this, "unchanged", false, "no change");
97
- },
98
- renderStatusValue(probe) {
99
- if (probe.status === "not_installed") return "unset";
100
- return probe.status;
101
- },
102
- };
103
-
104
- function baseProbe(descriptor, values) {
105
- return {
106
- name: descriptor.name,
107
- summaryLabel: descriptor.summaryLabel,
108
- statusLabel: descriptor.statusLabel,
109
- configured: false,
110
- ...values,
111
- };
112
- }
113
-
114
- function action(descriptor, status, changed, detail, extras = {}) {
115
- return {
116
- name: descriptor.name,
117
- label: descriptor.summaryLabel,
118
- status,
119
- changed,
120
- detail,
121
- ...extras,
122
- };
123
- }
@@ -1,420 +0,0 @@
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({
34
- home = os.homedir(),
35
- trackerDir,
36
- packageName = "vibeusage",
37
- env = process.env,
38
- } = {}) {
39
- const paths = resolveOpenclawHookPaths({ home, trackerDir, env });
40
-
41
- await ensureOpenclawHookFiles({
42
- hookDir: paths.hookDir,
43
- trackerDir,
44
- packageName,
45
- openclawHome: paths.openclawHome,
46
- });
47
-
48
- const installResult = runOpenclawCli(["hooks", "install", "--link", paths.hookDir], env);
49
- if (installResult.skippedReason) {
50
- return { configured: false, ...paths, ...installResult };
51
- }
52
-
53
- const state = await probeOpenclawHookState({ home, trackerDir, env });
54
- return {
55
- configured: state.configured,
56
- changed: /Linked hook path:/i.test(installResult.stdout || ""),
57
- ...paths,
58
- stdout: installResult.stdout,
59
- stderr: installResult.stderr,
60
- code: installResult.code,
61
- };
62
- }
63
-
64
- async function ensureOpenclawHookFiles({
65
- hookDir,
66
- trackerDir,
67
- packageName = "vibeusage",
68
- openclawHome,
69
- } = {}) {
70
- if (!hookDir || !trackerDir) throw new Error("hookDir and trackerDir are required");
71
-
72
- const hookEntryDir = path.join(hookDir, OPENCLAW_HOOK_NAME);
73
- await fs.mkdir(hookEntryDir, { recursive: true });
74
-
75
- const hookMdPath = path.join(hookEntryDir, "HOOK.md");
76
- const handlerPath = path.join(hookEntryDir, "handler.js");
77
-
78
- await fs.writeFile(hookMdPath, buildHookMarkdown(), "utf8");
79
- await fs.writeFile(
80
- handlerPath,
81
- buildHookHandler({
82
- trackerDir,
83
- packageName,
84
- openclawHome: openclawHome || path.join(os.homedir(), ".openclaw"),
85
- }),
86
- "utf8",
87
- );
88
- }
89
-
90
- async function probeOpenclawHookState({ home = os.homedir(), trackerDir, env = process.env } = {}) {
91
- const paths = resolveOpenclawHookPaths({ home, trackerDir, env });
92
- const { openclawConfigPath, hookDir, hookEntryDir, hookName } = paths;
93
-
94
- const hookFilesReady =
95
- fssync.existsSync(path.join(hookEntryDir, "HOOK.md")) &&
96
- fssync.existsSync(path.join(hookEntryDir, "handler.js"));
97
-
98
- let cfg = null;
99
- try {
100
- const raw = await fs.readFile(openclawConfigPath, "utf8");
101
- cfg = JSON.parse(raw);
102
- } catch (err) {
103
- if (err?.code === "ENOENT" || err?.code === "ENOTDIR") {
104
- return {
105
- configured: false,
106
- enabled: false,
107
- linked: false,
108
- hookFilesReady,
109
- skippedReason: "openclaw-config-missing",
110
- ...paths,
111
- };
112
- }
113
- return {
114
- configured: false,
115
- enabled: false,
116
- linked: false,
117
- hookFilesReady,
118
- skippedReason: "openclaw-config-unreadable",
119
- error: err?.message || String(err),
120
- ...paths,
121
- };
122
- }
123
-
124
- const enabled = Boolean(cfg?.hooks?.internal?.entries?.[hookName]?.enabled);
125
- const extraDirs = Array.isArray(cfg?.hooks?.internal?.load?.extraDirs)
126
- ? cfg.hooks.internal.load.extraDirs
127
- : [];
128
- const normalizedHookDir = path.resolve(hookDir);
129
- const linked = extraDirs.some((entry) => path.resolve(String(entry || "")) === normalizedHookDir);
130
-
131
- return {
132
- configured: enabled && linked,
133
- enabled,
134
- linked,
135
- hookFilesReady,
136
- ...paths,
137
- };
138
- }
139
-
140
- async function removeOpenclawHookConfig({
141
- home = os.homedir(),
142
- trackerDir,
143
- env = process.env,
144
- } = {}) {
145
- const paths = resolveOpenclawHookPaths({ home, trackerDir, env });
146
- const { openclawConfigPath, hookDir, hookName } = paths;
147
-
148
- let cfg;
149
- try {
150
- cfg = JSON.parse(await fs.readFile(openclawConfigPath, "utf8"));
151
- } catch (err) {
152
- if (err?.code === "ENOENT" || err?.code === "ENOTDIR") {
153
- return { removed: false, skippedReason: "openclaw-config-missing", ...paths };
154
- }
155
- return {
156
- removed: false,
157
- skippedReason: "openclaw-config-unreadable",
158
- error: err?.message || String(err),
159
- ...paths,
160
- };
161
- }
162
-
163
- let changed = false;
164
- const hooks = cfg?.hooks;
165
- const internal = hooks?.internal;
166
-
167
- if (internal?.entries && Object.prototype.hasOwnProperty.call(internal.entries, hookName)) {
168
- delete internal.entries[hookName];
169
- changed = true;
170
- if (Object.keys(internal.entries).length === 0) delete internal.entries;
171
- }
172
-
173
- if (internal?.load && Array.isArray(internal.load.extraDirs)) {
174
- const before = internal.load.extraDirs;
175
- const target = path.resolve(hookDir);
176
- const after = before.filter((entry) => path.resolve(String(entry || "")) !== target);
177
- if (after.length !== before.length) {
178
- internal.load.extraDirs = after;
179
- changed = true;
180
- if (after.length === 0) delete internal.load.extraDirs;
181
- if (Object.keys(internal.load).length === 0) delete internal.load;
182
- }
183
- }
184
-
185
- if (internal?.installs && typeof internal.installs === "object") {
186
- const installs = internal.installs;
187
- if (Object.prototype.hasOwnProperty.call(installs, hookName)) {
188
- delete installs[hookName];
189
- changed = true;
190
- }
191
-
192
- const target = path.resolve(hookDir);
193
- for (const [id, entry] of Object.entries(installs)) {
194
- const sourcePath = normalizeString(entry?.sourcePath);
195
- const installPath = normalizeString(entry?.installPath);
196
- if (
197
- (sourcePath && path.resolve(sourcePath) === target) ||
198
- (installPath && path.resolve(installPath) === target)
199
- ) {
200
- delete installs[id];
201
- changed = true;
202
- }
203
- }
204
-
205
- if (Object.keys(installs).length === 0) delete internal.installs;
206
- }
207
-
208
- if (internal && Object.keys(internal).length === 0) {
209
- delete hooks.internal;
210
- changed = true;
211
- }
212
- if (hooks && Object.keys(hooks).length === 0) {
213
- delete cfg.hooks;
214
- changed = true;
215
- }
216
-
217
- if (changed) {
218
- await fs.writeFile(openclawConfigPath, `${JSON.stringify(cfg, null, 2)}\n`, "utf8");
219
- }
220
-
221
- await fs.rm(hookDir, { recursive: true, force: true }).catch(() => {});
222
-
223
- return { removed: changed, ...paths };
224
- }
225
-
226
- function runOpenclawCli(args, env = process.env) {
227
- let res;
228
- try {
229
- res = cp.spawnSync("openclaw", args, {
230
- env,
231
- encoding: "utf8",
232
- timeout: 30_000,
233
- });
234
- } catch (err) {
235
- return {
236
- code: 1,
237
- skippedReason: err?.code === "ENOENT" ? "openclaw-cli-missing" : "openclaw-cli-error",
238
- error: err?.message || String(err),
239
- stdout: "",
240
- stderr: "",
241
- };
242
- }
243
-
244
- if (res.error?.code === "ENOENT") {
245
- return {
246
- code: 1,
247
- skippedReason: "openclaw-cli-missing",
248
- error: res.error.message,
249
- stdout: res.stdout || "",
250
- stderr: res.stderr || "",
251
- };
252
- }
253
-
254
- if ((res.status || 0) !== 0) {
255
- return {
256
- code: Number(res.status || 1),
257
- skippedReason: "openclaw-hooks-install-failed",
258
- error: (res.stderr || res.stdout || "").trim() || "openclaw hooks install failed",
259
- stdout: res.stdout || "",
260
- stderr: res.stderr || "",
261
- };
262
- }
263
-
264
- return {
265
- code: 0,
266
- stdout: res.stdout || "",
267
- stderr: res.stderr || "",
268
- };
269
- }
270
-
271
- function buildHookMarkdown() {
272
- return `---
273
- name: ${OPENCLAW_HOOK_NAME}
274
- description: "Trigger vibeusage sync when OpenClaw sessions roll over"
275
- metadata:
276
- { "openclaw": { "emoji": "📈", "events": ["command:new", "command:reset", "command:stop"], "requires": { "bins": ["node"] } } }
277
- ---
278
-
279
- # VibeUsage OpenClaw Sync Hook
280
-
281
- Triggers non-blocking 'vibeusage sync --auto --from-openclaw' runs when OpenClaw command events indicate session rollover/reset/stop.
282
- `;
283
- }
284
-
285
- function buildHookHandler({ trackerDir, packageName = "vibeusage", openclawHome }) {
286
- const trackerBinPath = path.join(trackerDir, "app", "bin", "tracker.js");
287
- const fallbackPkg = packageName || "vibeusage";
288
- const safeOpenclawHome = openclawHome || path.join(os.homedir(), ".openclaw");
289
-
290
- return (
291
- `'use strict';\n` +
292
- `const fs = require('node:fs');\n` +
293
- `const path = require('node:path');\n` +
294
- `const cp = require('node:child_process');\n` +
295
- `const trackerDir = ${JSON.stringify(trackerDir)};\n` +
296
- `const trackerBinPath = ${JSON.stringify(trackerBinPath)};\n` +
297
- `const fallbackPkg = ${JSON.stringify(fallbackPkg)};\n` +
298
- `const openclawHome = ${JSON.stringify(safeOpenclawHome)};\n` +
299
- `const throttlePath = path.join(trackerDir, 'openclaw.sync.throttle');\n` +
300
- `const depsMarkerPath = path.join(trackerDir, 'app', 'node_modules', '@insforge', 'sdk', 'package.json');\n` +
301
- `const THROTTLE_MS = 15_000;\n` +
302
- `\n` +
303
- `module.exports = async function handler(event) {\n` +
304
- ` try {\n` +
305
- ` if (!event || event.type !== 'command') return;\n` +
306
- ` if (event.action !== 'new' && event.action !== 'reset' && event.action !== 'stop') return;\n` +
307
- `\n` +
308
- ` const sessionKey = normalize(event.sessionKey);\n` +
309
- ` const agentId = parseAgentId(sessionKey);\n` +
310
- ` if (!agentId) return;\n` +
311
- `\n` +
312
- ` const sessionEntry = resolveSessionEntry(event);\n` +
313
- ` const sessionId = normalize(sessionEntry && sessionEntry.sessionId) || resolveSessionId(event);\n` +
314
- ` if (!sessionId) return;\n` +
315
- `\n` +
316
- ` const now = Date.now();\n` +
317
- ` let last = 0;\n` +
318
- ` try { last = Number(fs.readFileSync(throttlePath, 'utf8')) || 0; } catch (_) {}\n` +
319
- ` if (now - last < THROTTLE_MS) return;\n` +
320
- ` try {\n` +
321
- ` fs.mkdirSync(trackerDir, { recursive: true });\n` +
322
- ` fs.writeFileSync(throttlePath, String(now), 'utf8');\n` +
323
- ` } catch (_) {}\n` +
324
- `\n` +
325
- ` const env = {\n` +
326
- ` ...process.env,\n` +
327
- ` VIBEUSAGE_OPENCLAW_AGENT_ID: agentId,\n` +
328
- ` VIBEUSAGE_OPENCLAW_SESSION_KEY: sessionKey,\n` +
329
- ` VIBEUSAGE_OPENCLAW_PREV_SESSION_ID: sessionId,\n` +
330
- ` VIBEUSAGE_OPENCLAW_HOME: openclawHome\n` +
331
- ` };\n` +
332
- ` const prevTotalTokens = toNonNegativeInt(sessionEntry && sessionEntry.totalTokens);\n` +
333
- ` const prevInputTokens = toNonNegativeInt(sessionEntry && sessionEntry.inputTokens);\n` +
334
- ` const prevOutputTokens = toNonNegativeInt(sessionEntry && sessionEntry.outputTokens);\n` +
335
- ` const prevModel = normalize(sessionEntry && sessionEntry.model);\n` +
336
- ` const prevUpdatedAt = toIso(sessionEntry && sessionEntry.updatedAt);\n` +
337
- ` if (prevTotalTokens != null) env.VIBEUSAGE_OPENCLAW_PREV_TOTAL_TOKENS = String(prevTotalTokens);\n` +
338
- ` if (prevInputTokens != null) env.VIBEUSAGE_OPENCLAW_PREV_INPUT_TOKENS = String(prevInputTokens);\n` +
339
- ` if (prevOutputTokens != null) env.VIBEUSAGE_OPENCLAW_PREV_OUTPUT_TOKENS = String(prevOutputTokens);\n` +
340
- ` if (prevModel) env.VIBEUSAGE_OPENCLAW_PREV_MODEL = prevModel;\n` +
341
- ` if (prevUpdatedAt) env.VIBEUSAGE_OPENCLAW_PREV_UPDATED_AT = prevUpdatedAt;\n` +
342
- `\n` +
343
- ` const hasLocalRuntime = fs.existsSync(trackerBinPath);\n` +
344
- ` const hasLocalDeps = fs.existsSync(depsMarkerPath);\n` +
345
- ` const cmd = hasLocalRuntime && hasLocalDeps\n` +
346
- ` ? [process.execPath, trackerBinPath, 'sync', '--auto', '--from-openclaw']\n` +
347
- ` : ['npx', '--yes', fallbackPkg, 'sync', '--auto', '--from-openclaw'];\n` +
348
- `\n` +
349
- ` const child = cp.spawn(cmd[0], cmd.slice(1), { detached: true, stdio: 'ignore', env });\n` +
350
- ` child.unref();\n` +
351
- ` } catch (_) {}\n` +
352
- `};\n` +
353
- `\n` +
354
- `function normalize(v) {\n` +
355
- ` if (typeof v !== 'string') return null;\n` +
356
- ` const s = v.trim();\n` +
357
- ` return s.length > 0 ? s : null;\n` +
358
- `}\n` +
359
- `\n` +
360
- `function resolveSessionEntry(event) {\n` +
361
- ` const ctx = (event && event.context && typeof event.context === 'object') ? event.context : {};\n` +
362
- ` if (!event || event.type !== 'command') return null;\n` +
363
- ` if (event.action === 'stop') return (ctx.sessionEntry && typeof ctx.sessionEntry === 'object') ? ctx.sessionEntry : null;\n` +
364
- ` if (ctx.previousSessionEntry && typeof ctx.previousSessionEntry === 'object') return ctx.previousSessionEntry;\n` +
365
- ` if (ctx.sessionEntry && typeof ctx.sessionEntry === 'object') return ctx.sessionEntry;\n` +
366
- ` return null;\n` +
367
- `}\n` +
368
- `\n` +
369
- `function toNonNegativeInt(v) {\n` +
370
- ` const n = Number(v);\n` +
371
- ` if (!Number.isFinite(n) || n < 0) return null;\n` +
372
- ` return Math.floor(n);\n` +
373
- `}\n` +
374
- `\n` +
375
- `function toIso(v) {\n` +
376
- ` if (typeof v === 'string') {\n` +
377
- ` const s = normalize(v);\n` +
378
- ` if (s && !Number.isNaN(Date.parse(s))) return s;\n` +
379
- ` }\n` +
380
- ` const n = Number(v);\n` +
381
- ` if (!Number.isFinite(n) || n <= 0) return null;\n` +
382
- ` const ms = n < 1e12 ? Math.floor(n * 1000) : Math.floor(n);\n` +
383
- ` const d = new Date(ms);\n` +
384
- ` return Number.isNaN(d.getTime()) ? null : d.toISOString();\n` +
385
- `}\n` +
386
- `\n` +
387
- `function parseAgentId(sessionKey) {\n` +
388
- ` const s = normalize(sessionKey);\n` +
389
- ` if (!s || !s.startsWith('agent:')) return null;\n` +
390
- ` const parts = s.split(':');\n` +
391
- ` return parts.length >= 2 ? normalize(parts[1]) : null;\n` +
392
- `}\n` +
393
- `\n` +
394
- `function resolveSessionId(event) {\n` +
395
- ` const ctx = (event && event.context && typeof event.context === 'object') ? event.context : {};\n` +
396
- ` return (\n` +
397
- ` normalize(ctx.previousSessionEntry && ctx.previousSessionEntry.sessionId) ||\n` +
398
- ` normalize(ctx.previousSessionId) ||\n` +
399
- ` normalize(ctx.sessionEntry && ctx.sessionEntry.sessionId) ||\n` +
400
- ` normalize(ctx.sessionId)\n` +
401
- ` );\n` +
402
- `}\n`
403
- );
404
- }
405
-
406
- function normalizeString(value) {
407
- if (typeof value !== "string") return null;
408
- const trimmed = value.trim();
409
- return trimmed.length > 0 ? trimmed : null;
410
- }
411
-
412
- module.exports = {
413
- OPENCLAW_HOOK_NAME,
414
- OPENCLAW_HOOK_DIRNAME,
415
- resolveOpenclawHookPaths,
416
- ensureOpenclawHookFiles,
417
- installOpenclawHook,
418
- probeOpenclawHookState,
419
- removeOpenclawHookConfig,
420
- };