vibeusage 0.2.20 → 0.2.22

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.
Files changed (40) hide show
  1. package/README.md +306 -173
  2. package/README.old.md +324 -0
  3. package/README.zh-CN.md +304 -188
  4. package/package.json +32 -30
  5. package/src/cli.js +41 -37
  6. package/src/commands/activate-if-needed.js +41 -0
  7. package/src/commands/diagnostics.js +8 -9
  8. package/src/commands/doctor.js +31 -26
  9. package/src/commands/init.js +324 -208
  10. package/src/commands/status.js +86 -80
  11. package/src/commands/sync.js +182 -130
  12. package/src/commands/uninstall.js +69 -58
  13. package/src/lib/activation-check.js +290 -0
  14. package/src/lib/browser-auth.js +52 -54
  15. package/src/lib/claude-config.js +25 -25
  16. package/src/lib/cli-ui.js +35 -35
  17. package/src/lib/codex-config.js +40 -36
  18. package/src/lib/debug-flags.js +2 -2
  19. package/src/lib/diagnostics.js +73 -55
  20. package/src/lib/doctor.js +139 -132
  21. package/src/lib/fs.js +17 -17
  22. package/src/lib/gemini-config.js +44 -40
  23. package/src/lib/init-flow.js +16 -22
  24. package/src/lib/insforge-client.js +10 -10
  25. package/src/lib/insforge.js +9 -3
  26. package/src/lib/openclaw-hook.js +91 -67
  27. package/src/lib/openclaw-session-plugin.js +520 -0
  28. package/src/lib/opencode-config.js +31 -32
  29. package/src/lib/opencode-usage-audit.js +34 -31
  30. package/src/lib/progress.js +12 -13
  31. package/src/lib/project-usage-purge.js +23 -17
  32. package/src/lib/prompt.js +8 -4
  33. package/src/lib/rollout.js +342 -241
  34. package/src/lib/runtime-config.js +34 -22
  35. package/src/lib/subscriptions.js +94 -92
  36. package/src/lib/tracker-paths.js +6 -6
  37. package/src/lib/upload-throttle.js +35 -16
  38. package/src/lib/uploader.js +33 -29
  39. package/src/lib/vibeusage-api.js +72 -56
  40. package/src/lib/vibeusage-public-repo.js +41 -24
@@ -0,0 +1,520 @@
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_SESSION_PLUGIN_ID = "openclaw-session-sync";
8
+ const OPENCLAW_SESSION_PLUGIN_DIRNAME = "openclaw-plugin";
9
+
10
+ function resolveOpenclawSessionPluginPaths({
11
+ home = os.homedir(),
12
+ trackerDir,
13
+ env = process.env,
14
+ } = {}) {
15
+ if (!trackerDir) throw new Error("trackerDir is required");
16
+
17
+ const openclawConfigPath =
18
+ normalizeString(env.OPENCLAW_CONFIG_PATH) || path.join(home, ".openclaw", "openclaw.json");
19
+
20
+ const openclawHome =
21
+ normalizeString(env.VIBEUSAGE_OPENCLAW_HOME) ||
22
+ normalizeString(env.OPENCLAW_STATE_DIR) ||
23
+ path.join(home, ".openclaw");
24
+
25
+ const pluginDir = path.join(trackerDir, OPENCLAW_SESSION_PLUGIN_DIRNAME);
26
+ const pluginEntryDir = path.join(pluginDir, OPENCLAW_SESSION_PLUGIN_ID);
27
+
28
+ return {
29
+ pluginId: OPENCLAW_SESSION_PLUGIN_ID,
30
+ pluginDir,
31
+ pluginEntryDir,
32
+ openclawConfigPath,
33
+ openclawHome,
34
+ };
35
+ }
36
+
37
+ async function installOpenclawSessionPlugin({
38
+ home = os.homedir(),
39
+ trackerDir,
40
+ packageName = "vibeusage",
41
+ env = process.env,
42
+ } = {}) {
43
+ const paths = resolveOpenclawSessionPluginPaths({ home, trackerDir, env });
44
+
45
+ await ensureOpenclawSessionPluginFiles({
46
+ pluginDir: paths.pluginDir,
47
+ trackerDir,
48
+ packageName,
49
+ openclawHome: paths.openclawHome,
50
+ });
51
+
52
+ const installResult = runOpenclawCli(["plugins", "install", "--link", paths.pluginEntryDir], env);
53
+ if (installResult.skippedReason) {
54
+ return { configured: false, ...paths, ...installResult };
55
+ }
56
+
57
+ const enableResult = runOpenclawCli(["plugins", "enable", paths.pluginId], env);
58
+ if (enableResult.skippedReason) {
59
+ return {
60
+ configured: false,
61
+ ...paths,
62
+ skippedReason: enableResult.skippedReason,
63
+ error: enableResult.error,
64
+ stdout: `${installResult.stdout || ""}\n${enableResult.stdout || ""}`.trim(),
65
+ stderr: `${installResult.stderr || ""}\n${enableResult.stderr || ""}`.trim(),
66
+ code: enableResult.code,
67
+ };
68
+ }
69
+
70
+ const state = await probeOpenclawSessionPluginState({ home, trackerDir, env });
71
+ return {
72
+ configured: state.configured,
73
+ changed:
74
+ /Linked plugin path:/i.test(installResult.stdout || "") ||
75
+ /Enabled plugin/i.test(enableResult.stdout || "") ||
76
+ /already enabled/i.test(enableResult.stdout || ""),
77
+ ...paths,
78
+ stdout: `${installResult.stdout || ""}\n${enableResult.stdout || ""}`.trim(),
79
+ stderr: `${installResult.stderr || ""}\n${enableResult.stderr || ""}`.trim(),
80
+ code: enableResult.code,
81
+ };
82
+ }
83
+
84
+ async function ensureOpenclawSessionPluginFiles({
85
+ pluginDir,
86
+ trackerDir,
87
+ packageName = "vibeusage",
88
+ openclawHome,
89
+ } = {}) {
90
+ if (!pluginDir || !trackerDir) throw new Error("pluginDir and trackerDir are required");
91
+
92
+ const pluginEntryDir = path.join(pluginDir, OPENCLAW_SESSION_PLUGIN_ID);
93
+ await fs.mkdir(pluginEntryDir, { recursive: true });
94
+
95
+ const packageJsonPath = path.join(pluginEntryDir, "package.json");
96
+ const pluginMetaPath = path.join(pluginEntryDir, "openclaw.plugin.json");
97
+ const indexPath = path.join(pluginEntryDir, "index.js");
98
+
99
+ await fs.writeFile(packageJsonPath, buildSessionPluginPackageJson(), "utf8");
100
+ await fs.writeFile(pluginMetaPath, buildSessionPluginMeta(), "utf8");
101
+ await fs.writeFile(
102
+ indexPath,
103
+ buildSessionPluginIndex({
104
+ trackerDir,
105
+ packageName,
106
+ openclawHome: openclawHome || path.join(os.homedir(), ".openclaw"),
107
+ }),
108
+ "utf8",
109
+ );
110
+ }
111
+
112
+ async function probeOpenclawSessionPluginState({
113
+ home = os.homedir(),
114
+ trackerDir,
115
+ env = process.env,
116
+ } = {}) {
117
+ const paths = resolveOpenclawSessionPluginPaths({ home, trackerDir, env });
118
+ const { openclawConfigPath, pluginEntryDir, pluginId } = paths;
119
+
120
+ const pluginFilesReady =
121
+ fssync.existsSync(path.join(pluginEntryDir, "package.json")) &&
122
+ fssync.existsSync(path.join(pluginEntryDir, "index.js"));
123
+
124
+ let cfg = null;
125
+ try {
126
+ const raw = await fs.readFile(openclawConfigPath, "utf8");
127
+ cfg = JSON.parse(raw);
128
+ } catch (err) {
129
+ if (err?.code === "ENOENT" || err?.code === "ENOTDIR") {
130
+ return {
131
+ configured: false,
132
+ enabled: false,
133
+ linked: false,
134
+ installed: false,
135
+ pluginFilesReady,
136
+ skippedReason: "openclaw-config-missing",
137
+ ...paths,
138
+ };
139
+ }
140
+ return {
141
+ configured: false,
142
+ enabled: false,
143
+ linked: false,
144
+ installed: false,
145
+ pluginFilesReady,
146
+ skippedReason: "openclaw-config-unreadable",
147
+ error: err?.message || String(err),
148
+ ...paths,
149
+ };
150
+ }
151
+
152
+ const pluginEntry = cfg?.plugins?.entries?.[pluginId];
153
+ const enabled = pluginEntry ? pluginEntry.enabled !== false : false;
154
+
155
+ const loadPaths = Array.isArray(cfg?.plugins?.load?.paths) ? cfg.plugins.load.paths : [];
156
+ const normalizedPluginEntryDir = path.resolve(pluginEntryDir);
157
+ const linked = loadPaths.some(
158
+ (entry) => path.resolve(String(entry || "")) === normalizedPluginEntryDir,
159
+ );
160
+
161
+ const installs =
162
+ cfg?.plugins?.installs && typeof cfg.plugins.installs === "object" ? cfg.plugins.installs : {};
163
+ const installEntry = installs[pluginId];
164
+ const installed = Boolean(installEntry);
165
+
166
+ return {
167
+ configured: enabled && linked && pluginFilesReady,
168
+ enabled,
169
+ linked,
170
+ installed,
171
+ pluginFilesReady,
172
+ ...paths,
173
+ };
174
+ }
175
+
176
+ async function removeOpenclawSessionPluginConfig({
177
+ home = os.homedir(),
178
+ trackerDir,
179
+ env = process.env,
180
+ } = {}) {
181
+ const paths = resolveOpenclawSessionPluginPaths({ home, trackerDir, env });
182
+ const { openclawConfigPath, pluginEntryDir, pluginId } = paths;
183
+
184
+ let cfg;
185
+ try {
186
+ cfg = JSON.parse(await fs.readFile(openclawConfigPath, "utf8"));
187
+ } catch (err) {
188
+ if (err?.code === "ENOENT" || err?.code === "ENOTDIR") {
189
+ return { removed: false, skippedReason: "openclaw-config-missing", ...paths };
190
+ }
191
+ return {
192
+ removed: false,
193
+ skippedReason: "openclaw-config-unreadable",
194
+ error: err?.message || String(err),
195
+ ...paths,
196
+ };
197
+ }
198
+
199
+ let changed = false;
200
+ const plugins = cfg?.plugins;
201
+
202
+ if (plugins?.entries && Object.prototype.hasOwnProperty.call(plugins.entries, pluginId)) {
203
+ delete plugins.entries[pluginId];
204
+ changed = true;
205
+ if (Object.keys(plugins.entries).length === 0) delete plugins.entries;
206
+ }
207
+
208
+ if (plugins?.load && Array.isArray(plugins.load.paths)) {
209
+ const target = path.resolve(pluginEntryDir);
210
+ const after = plugins.load.paths.filter(
211
+ (entry) => path.resolve(String(entry || "")) !== target,
212
+ );
213
+ if (after.length !== plugins.load.paths.length) {
214
+ plugins.load.paths = after;
215
+ changed = true;
216
+ if (after.length === 0) delete plugins.load.paths;
217
+ if (Object.keys(plugins.load).length === 0) delete plugins.load;
218
+ }
219
+ }
220
+
221
+ if (plugins?.installs && typeof plugins.installs === "object") {
222
+ const installs = plugins.installs;
223
+ if (Object.prototype.hasOwnProperty.call(installs, pluginId)) {
224
+ delete installs[pluginId];
225
+ changed = true;
226
+ }
227
+
228
+ const target = path.resolve(pluginEntryDir);
229
+ for (const [id, entry] of Object.entries(installs)) {
230
+ const sourcePath = normalizeString(entry?.sourcePath);
231
+ const installPath = normalizeString(entry?.installPath);
232
+ if (
233
+ (sourcePath && path.resolve(sourcePath) === target) ||
234
+ (installPath && path.resolve(installPath) === target)
235
+ ) {
236
+ delete installs[id];
237
+ changed = true;
238
+ }
239
+ }
240
+
241
+ if (Object.keys(installs).length === 0) delete plugins.installs;
242
+ }
243
+
244
+ if (plugins && Object.keys(plugins).length === 0) {
245
+ delete cfg.plugins;
246
+ changed = true;
247
+ }
248
+
249
+ if (changed) {
250
+ await fs.writeFile(openclawConfigPath, `${JSON.stringify(cfg, null, 2)}\n`, "utf8");
251
+ }
252
+
253
+ const hadFiles = await fs
254
+ .stat(pluginEntryDir)
255
+ .then((st) => st.isDirectory())
256
+ .catch(() => false);
257
+ await fs.rm(pluginEntryDir, { recursive: true, force: true }).catch(() => {});
258
+
259
+ return { removed: changed || hadFiles, ...paths };
260
+ }
261
+
262
+ function runOpenclawCli(args, env = process.env) {
263
+ let res;
264
+ try {
265
+ res = cp.spawnSync("openclaw", args, {
266
+ env,
267
+ encoding: "utf8",
268
+ timeout: 30_000,
269
+ });
270
+ } catch (err) {
271
+ return {
272
+ code: 1,
273
+ skippedReason: err?.code === "ENOENT" ? "openclaw-cli-missing" : "openclaw-cli-error",
274
+ error: err?.message || String(err),
275
+ stdout: "",
276
+ stderr: "",
277
+ };
278
+ }
279
+
280
+ if (res.error?.code === "ENOENT") {
281
+ return {
282
+ code: 1,
283
+ skippedReason: "openclaw-cli-missing",
284
+ error: res.error.message,
285
+ stdout: res.stdout || "",
286
+ stderr: res.stderr || "",
287
+ };
288
+ }
289
+
290
+ if ((res.status || 0) !== 0) {
291
+ return {
292
+ code: Number(res.status || 1),
293
+ skippedReason: "openclaw-plugins-install-failed",
294
+ error: (res.stderr || res.stdout || "").trim() || "openclaw plugins install failed",
295
+ stdout: res.stdout || "",
296
+ stderr: res.stderr || "",
297
+ };
298
+ }
299
+
300
+ return {
301
+ code: 0,
302
+ stdout: res.stdout || "",
303
+ stderr: res.stderr || "",
304
+ };
305
+ }
306
+
307
+ function buildSessionPluginPackageJson() {
308
+ return `${JSON.stringify(
309
+ {
310
+ name: "@vibeusage/openclaw-session-sync",
311
+ version: "0.0.0",
312
+ private: true,
313
+ type: "module",
314
+ openclaw: {
315
+ extensions: ["./index.js"],
316
+ },
317
+ },
318
+ null,
319
+ 2,
320
+ )}\n`;
321
+ }
322
+
323
+ function buildSessionPluginMeta() {
324
+ return `${JSON.stringify(
325
+ {
326
+ id: OPENCLAW_SESSION_PLUGIN_ID,
327
+ name: "VibeUsage OpenClaw Session Sync",
328
+ description: "Trigger vibeusage sync on OpenClaw agent/session lifecycle events.",
329
+ configSchema: {
330
+ type: "object",
331
+ additionalProperties: false,
332
+ properties: {},
333
+ },
334
+ },
335
+ null,
336
+ 2,
337
+ )}\n`;
338
+ }
339
+
340
+ function buildSessionPluginIndex({ trackerDir, packageName = "vibeusage", openclawHome }) {
341
+ const trackerBinPath = path.join(trackerDir, "app", "bin", "tracker.js");
342
+ const fallbackPkg = packageName || "vibeusage";
343
+ const safeOpenclawHome = openclawHome || path.join(os.homedir(), ".openclaw");
344
+
345
+ return (
346
+ `import fs from 'node:fs';\n` +
347
+ `import path from 'node:path';\n` +
348
+ `import cp from 'node:child_process';\n` +
349
+ `\n` +
350
+ `const trackerDir = ${JSON.stringify(trackerDir)};\n` +
351
+ `const trackerBinPath = ${JSON.stringify(trackerBinPath)};\n` +
352
+ `const fallbackPkg = ${JSON.stringify(fallbackPkg)};\n` +
353
+ `const openclawHome = ${JSON.stringify(safeOpenclawHome)};\n` +
354
+ `const depsMarkerPath = path.join(trackerDir, 'app', 'node_modules', '@insforge', 'sdk', 'package.json');\n` +
355
+ `const triggerStatePath = path.join(trackerDir, 'openclaw.session-sync.trigger-state.json');\n` +
356
+ `const SESSION_TRIGGER_THROTTLE_MS = 15_000;\n` +
357
+ `\n` +
358
+ `export default function register(api) {\n` +
359
+ ` api.on('agent_end', async (_event, ctx) => {\n` +
360
+ ` try {\n` +
361
+ ` const sessionKey = normalize(ctx && ctx.sessionKey);\n` +
362
+ ` if (!sessionKey) return;\n` +
363
+ `\n` +
364
+ ` const agentId = normalize(ctx && ctx.agentId) || parseAgentId(sessionKey);\n` +
365
+ ` if (!agentId) return;\n` +
366
+ `\n` +
367
+ ` const sessionInfo = resolveSessionInfo(agentId, sessionKey);\n` +
368
+ ` const sessionId = normalize(sessionInfo && sessionInfo.sessionId);\n` +
369
+ ` if (!sessionId) return;\n` +
370
+ `\n` +
371
+ ` if (!allowTrigger('agent_end', agentId, sessionId)) return;\n` +
372
+ `\n` +
373
+ ` spawnSync({\n` +
374
+ ` args: ['sync', '--auto', '--from-openclaw'],\n` +
375
+ ` env: buildSessionEnv({\n` +
376
+ ` agentId,\n` +
377
+ ` sessionId,\n` +
378
+ ` sessionKey,\n` +
379
+ ` sessionEntry: sessionInfo && sessionInfo.entry\n` +
380
+ ` })\n` +
381
+ ` });\n` +
382
+ ` } catch (_) {}\n` +
383
+ ` });\n` +
384
+ `\n` +
385
+ ` api.on('gateway_start', async () => {\n` +
386
+ ` try {\n` +
387
+ ` if (!allowTrigger('gateway_start', 'gateway', 'startup')) return;\n` +
388
+ ` spawnSync({ args: ['sync', '--auto'] });\n` +
389
+ ` } catch (_) {}\n` +
390
+ ` });\n` +
391
+ `\n` +
392
+ ` api.on('gateway_stop', async () => {\n` +
393
+ ` try {\n` +
394
+ ` if (!allowTrigger('gateway_stop', 'gateway', 'stop')) return;\n` +
395
+ ` spawnSync({ args: ['sync', '--auto'] });\n` +
396
+ ` } catch (_) {}\n` +
397
+ ` });\n` +
398
+ `}\n` +
399
+ `\n` +
400
+ `function spawnSync({ args, env = {} }) {\n` +
401
+ ` const hasLocalRuntime = fs.existsSync(trackerBinPath);\n` +
402
+ ` const hasLocalDeps = fs.existsSync(depsMarkerPath);\n` +
403
+ ` const argv = Array.isArray(args) && args.length > 0 ? args : ['sync', '--auto'];\n` +
404
+ ` const cmd = hasLocalRuntime && hasLocalDeps\n` +
405
+ ` ? [process.execPath, trackerBinPath, ...argv]\n` +
406
+ ` : ['npx', '--yes', fallbackPkg, ...argv];\n` +
407
+ ` const child = cp.spawn(cmd[0], cmd.slice(1), {\n` +
408
+ ` detached: true,\n` +
409
+ ` stdio: 'ignore',\n` +
410
+ ` env: { ...process.env, ...env }\n` +
411
+ ` });\n` +
412
+ ` child.unref();\n` +
413
+ `}\n` +
414
+ `\n` +
415
+ `function buildSessionEnv({ agentId, sessionId, sessionKey, sessionEntry }) {\n` +
416
+ ` const out = {\n` +
417
+ ` VIBEUSAGE_OPENCLAW_AGENT_ID: agentId,\n` +
418
+ ` VIBEUSAGE_OPENCLAW_PREV_SESSION_ID: sessionId,\n` +
419
+ ` VIBEUSAGE_OPENCLAW_HOME: openclawHome\n` +
420
+ ` };\n` +
421
+ ` const key = normalize(sessionKey);\n` +
422
+ ` if (key) out.VIBEUSAGE_OPENCLAW_SESSION_KEY = key;\n` +
423
+ ` const prevTotalTokens = toNonNegativeInt(sessionEntry && sessionEntry.totalTokens);\n` +
424
+ ` const prevInputTokens = toNonNegativeInt(sessionEntry && sessionEntry.inputTokens);\n` +
425
+ ` const prevOutputTokens = toNonNegativeInt(sessionEntry && sessionEntry.outputTokens);\n` +
426
+ ` const prevModel = normalize(sessionEntry && sessionEntry.model);\n` +
427
+ ` const prevUpdatedAt = toIso(sessionEntry && sessionEntry.updatedAt);\n` +
428
+ ` if (prevTotalTokens != null) out.VIBEUSAGE_OPENCLAW_PREV_TOTAL_TOKENS = String(prevTotalTokens);\n` +
429
+ ` if (prevInputTokens != null) out.VIBEUSAGE_OPENCLAW_PREV_INPUT_TOKENS = String(prevInputTokens);\n` +
430
+ ` if (prevOutputTokens != null) out.VIBEUSAGE_OPENCLAW_PREV_OUTPUT_TOKENS = String(prevOutputTokens);\n` +
431
+ ` if (prevModel) out.VIBEUSAGE_OPENCLAW_PREV_MODEL = prevModel;\n` +
432
+ ` if (prevUpdatedAt) out.VIBEUSAGE_OPENCLAW_PREV_UPDATED_AT = prevUpdatedAt;\n` +
433
+ ` return out;\n` +
434
+ `}\n` +
435
+ `\n` +
436
+ `function resolveSessionInfo(agentId, sessionKey) {\n` +
437
+ ` const key = normalize(sessionKey);\n` +
438
+ ` if (!key) return null;\n` +
439
+ ` const sessionsPath = path.join(openclawHome, 'agents', agentId, 'sessions', 'sessions.json');\n` +
440
+ ` try {\n` +
441
+ ` const raw = fs.readFileSync(sessionsPath, 'utf8');\n` +
442
+ ` const parsed = JSON.parse(raw);\n` +
443
+ ` if (!parsed || typeof parsed !== 'object') return null;\n` +
444
+ ` const entry = parsed[key];\n` +
445
+ ` if (!entry || typeof entry !== 'object') return null;\n` +
446
+ ` return {\n` +
447
+ ` sessionKey: key,\n` +
448
+ ` sessionId: normalize(entry.sessionId),\n` +
449
+ ` entry\n` +
450
+ ` };\n` +
451
+ ` } catch (_) {}\n` +
452
+ ` return null;\n` +
453
+ `}\n` +
454
+ `\n` +
455
+ `function parseAgentId(sessionKey) {\n` +
456
+ ` const s = normalize(sessionKey);\n` +
457
+ ` if (!s || !s.startsWith('agent:')) return null;\n` +
458
+ ` const parts = s.split(':');\n` +
459
+ ` return parts.length >= 2 ? normalize(parts[1]) : null;\n` +
460
+ `}\n` +
461
+ `\n` +
462
+ `function allowTrigger(kind, scope, target) {\n` +
463
+ ` const key = [kind, scope || 'na', target || 'na'].join(':');\n` +
464
+ ` const now = Date.now();\n` +
465
+ ` let state = {};\n` +
466
+ ` try {\n` +
467
+ ` state = JSON.parse(fs.readFileSync(triggerStatePath, 'utf8'));\n` +
468
+ ` if (!state || typeof state !== 'object') state = {};\n` +
469
+ ` } catch (_) {}\n` +
470
+ ` const last = Number(state[key] || 0);\n` +
471
+ ` if (Number.isFinite(last) && now - last < SESSION_TRIGGER_THROTTLE_MS) return false;\n` +
472
+ ` state[key] = now;\n` +
473
+ ` try {\n` +
474
+ ` fs.mkdirSync(path.dirname(triggerStatePath), { recursive: true });\n` +
475
+ ` fs.writeFileSync(triggerStatePath, JSON.stringify(state), 'utf8');\n` +
476
+ ` } catch (_) {}\n` +
477
+ ` return true;\n` +
478
+ `}\n` +
479
+ `\n` +
480
+ `function normalize(v) {\n` +
481
+ ` if (typeof v !== 'string') return null;\n` +
482
+ ` const s = v.trim();\n` +
483
+ ` return s.length > 0 ? s : null;\n` +
484
+ `}\n` +
485
+ `\n` +
486
+ `function toNonNegativeInt(v) {\n` +
487
+ ` const n = Number(v);\n` +
488
+ ` if (!Number.isFinite(n) || n < 0) return null;\n` +
489
+ ` return Math.floor(n);\n` +
490
+ `}\n` +
491
+ `\n` +
492
+ `function toIso(v) {\n` +
493
+ ` if (typeof v === 'string') {\n` +
494
+ ` const s = normalize(v);\n` +
495
+ ` if (s && !Number.isNaN(Date.parse(s))) return s;\n` +
496
+ ` }\n` +
497
+ ` const n = Number(v);\n` +
498
+ ` if (!Number.isFinite(n) || n <= 0) return null;\n` +
499
+ ` const ms = n < 1e12 ? Math.floor(n * 1000) : Math.floor(n);\n` +
500
+ ` const d = new Date(ms);\n` +
501
+ ` return Number.isNaN(d.getTime()) ? null : d.toISOString();\n` +
502
+ `}\n`
503
+ );
504
+ }
505
+
506
+ function normalizeString(value) {
507
+ if (typeof value !== "string") return null;
508
+ const trimmed = value.trim();
509
+ return trimmed.length > 0 ? trimmed : null;
510
+ }
511
+
512
+ module.exports = {
513
+ OPENCLAW_SESSION_PLUGIN_ID,
514
+ OPENCLAW_SESSION_PLUGIN_DIRNAME,
515
+ resolveOpenclawSessionPluginPaths,
516
+ ensureOpenclawSessionPluginFiles,
517
+ installOpenclawSessionPlugin,
518
+ probeOpenclawSessionPluginState,
519
+ removeOpenclawSessionPluginConfig,
520
+ };
@@ -1,28 +1,30 @@
1
- const os = require('node:os');
2
- const path = require('node:path');
3
- const fs = require('node:fs/promises');
1
+ const os = require("node:os");
2
+ const path = require("node:path");
3
+ const fs = require("node:fs/promises");
4
4
 
5
- const { ensureDir } = require('./fs');
5
+ const { ensureDir } = require("./fs");
6
6
 
7
- const DEFAULT_PLUGIN_NAME = 'vibeusage-tracker.js';
8
- const PLUGIN_MARKER = 'VIBEUSAGE_TRACKER_PLUGIN';
9
- const DEFAULT_EVENT = 'session.updated';
7
+ const DEFAULT_PLUGIN_NAME = "vibeusage-tracker.js";
8
+ const PLUGIN_MARKER = "VIBEUSAGE_TRACKER_PLUGIN";
9
+ const DEFAULT_EVENT = "session.updated";
10
10
 
11
11
  function resolveOpencodeConfigDir({ home = os.homedir(), env = process.env } = {}) {
12
- const explicit = typeof env.OPENCODE_CONFIG_DIR === 'string' ? env.OPENCODE_CONFIG_DIR.trim() : '';
12
+ const explicit =
13
+ typeof env.OPENCODE_CONFIG_DIR === "string" ? env.OPENCODE_CONFIG_DIR.trim() : "";
13
14
  if (explicit) return path.resolve(explicit);
14
- const xdg = typeof env.XDG_CONFIG_HOME === 'string' ? env.XDG_CONFIG_HOME.trim() : '';
15
- const base = xdg || path.join(home, '.config');
16
- return path.join(base, 'opencode');
15
+ const xdg = typeof env.XDG_CONFIG_HOME === "string" ? env.XDG_CONFIG_HOME.trim() : "";
16
+ const base = xdg || path.join(home, ".config");
17
+ return path.join(base, "opencode");
17
18
  }
18
19
 
19
20
  function resolveOpencodePluginDir({ configDir }) {
20
- return path.join(configDir, 'plugin');
21
+ return path.join(configDir, "plugin");
21
22
  }
22
23
 
23
24
  function buildOpencodePlugin({ notifyPath }) {
24
- const safeNotifyPath = typeof notifyPath === 'string' ? notifyPath : '';
25
- return `// ${PLUGIN_MARKER}\n` +
25
+ const safeNotifyPath = typeof notifyPath === "string" ? notifyPath : "";
26
+ return (
27
+ `// ${PLUGIN_MARKER}\n` +
26
28
  `const notifyPath = ${JSON.stringify(safeNotifyPath)};\n` +
27
29
  `export const VibeUsagePlugin = async ({ $ }) => {\n` +
28
30
  ` return {\n` +
@@ -30,24 +32,21 @@ function buildOpencodePlugin({ notifyPath }) {
30
32
  ` if (!event || event.type !== ${JSON.stringify(DEFAULT_EVENT)}) return;\n` +
31
33
  ` try {\n` +
32
34
  ` if (!notifyPath) return;\n` +
33
- ` const proc = $\`/usr/bin/env node ${'${notifyPath}'} --source=opencode\`;\n` +
35
+ ` const proc = $\`/usr/bin/env node ${"${notifyPath}"} --source=opencode\`;\n` +
34
36
  ` if (proc && typeof proc.catch === 'function') proc.catch(() => {});\n` +
35
37
  ` } catch (_) {}\n` +
36
38
  ` }\n` +
37
39
  ` };\n` +
38
- `};\n`;
40
+ `};\n`
41
+ );
39
42
  }
40
43
 
41
- async function upsertOpencodePlugin({
42
- configDir,
43
- notifyPath,
44
- pluginName = DEFAULT_PLUGIN_NAME
45
- }) {
46
- if (!configDir) return { changed: false, pluginPath: null, skippedReason: 'config-missing' };
44
+ async function upsertOpencodePlugin({ configDir, notifyPath, pluginName = DEFAULT_PLUGIN_NAME }) {
45
+ if (!configDir) return { changed: false, pluginPath: null, skippedReason: "config-missing" };
47
46
  const pluginDir = resolveOpencodePluginDir({ configDir });
48
47
  const pluginPath = path.join(pluginDir, pluginName);
49
48
  const next = buildOpencodePlugin({ notifyPath });
50
- const existing = await fs.readFile(pluginPath, 'utf8').catch(() => null);
49
+ const existing = await fs.readFile(pluginPath, "utf8").catch(() => null);
51
50
 
52
51
  if (existing === next) {
53
52
  return { changed: false, pluginPath, skippedReason: null };
@@ -57,20 +56,20 @@ async function upsertOpencodePlugin({
57
56
 
58
57
  let backupPath = null;
59
58
  if (existing != null) {
60
- backupPath = `${pluginPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
59
+ backupPath = `${pluginPath}.bak.${new Date().toISOString().replace(/[:.]/g, "-")}`;
61
60
  await fs.copyFile(pluginPath, backupPath).catch(() => {});
62
61
  }
63
62
 
64
- await fs.writeFile(pluginPath, next, 'utf8');
63
+ await fs.writeFile(pluginPath, next, "utf8");
65
64
  return { changed: true, pluginPath, backupPath, skippedReason: null };
66
65
  }
67
66
 
68
67
  async function removeOpencodePlugin({ configDir, pluginName = DEFAULT_PLUGIN_NAME }) {
69
- if (!configDir) return { removed: false, skippedReason: 'config-missing' };
68
+ if (!configDir) return { removed: false, skippedReason: "config-missing" };
70
69
  const pluginPath = path.join(resolveOpencodePluginDir({ configDir }), pluginName);
71
- const existing = await fs.readFile(pluginPath, 'utf8').catch(() => null);
72
- if (existing == null) return { removed: false, skippedReason: 'plugin-missing' };
73
- if (!hasPluginMarker(existing)) return { removed: false, skippedReason: 'unexpected-content' };
70
+ const existing = await fs.readFile(pluginPath, "utf8").catch(() => null);
71
+ if (existing == null) return { removed: false, skippedReason: "plugin-missing" };
72
+ if (!hasPluginMarker(existing)) return { removed: false, skippedReason: "unexpected-content" };
74
73
  await fs.unlink(pluginPath).catch(() => {});
75
74
  return { removed: true, skippedReason: null };
76
75
  }
@@ -78,13 +77,13 @@ async function removeOpencodePlugin({ configDir, pluginName = DEFAULT_PLUGIN_NAM
78
77
  async function isOpencodePluginInstalled({ configDir, pluginName = DEFAULT_PLUGIN_NAME }) {
79
78
  if (!configDir) return false;
80
79
  const pluginPath = path.join(resolveOpencodePluginDir({ configDir }), pluginName);
81
- const existing = await fs.readFile(pluginPath, 'utf8').catch(() => null);
80
+ const existing = await fs.readFile(pluginPath, "utf8").catch(() => null);
82
81
  if (!existing) return false;
83
82
  return hasPluginMarker(existing);
84
83
  }
85
84
 
86
85
  function hasPluginMarker(text) {
87
- return typeof text === 'string' && text.includes(PLUGIN_MARKER);
86
+ return typeof text === "string" && text.includes(PLUGIN_MARKER);
88
87
  }
89
88
 
90
89
  module.exports = {
@@ -96,5 +95,5 @@ module.exports = {
96
95
  buildOpencodePlugin,
97
96
  upsertOpencodePlugin,
98
97
  removeOpencodePlugin,
99
- isOpencodePluginInstalled
98
+ isOpencodePluginInstalled,
100
99
  };