vibeusage 0.2.21 → 0.2.23

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 -32
  5. package/src/cli.js +37 -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 +285 -218
  10. package/src/commands/status.js +86 -83
  11. package/src/commands/sync.js +178 -130
  12. package/src/commands/uninstall.js +66 -62
  13. package/src/lib/activation-check.js +341 -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 +70 -57
  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 +89 -66
  27. package/src/lib/openclaw-session-plugin.js +116 -92
  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
@@ -1,40 +1,54 @@
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
- const crypto = require('node:crypto');
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
+ const crypto = require("node:crypto");
7
7
 
8
- const { ensureDir, writeFileAtomic, readJson, writeJson, chmod600IfPossible } = require('../lib/fs');
9
- const { prompt, promptHidden } = require('../lib/prompt');
8
+ const {
9
+ ensureDir,
10
+ writeFileAtomic,
11
+ readJson,
12
+ writeJson,
13
+ chmod600IfPossible,
14
+ } = require("../lib/fs");
15
+ const { prompt, promptHidden } = require("../lib/prompt");
10
16
  const {
11
17
  upsertCodexNotify,
12
18
  upsertEveryCodeNotify,
13
19
  readCodexNotify,
14
- readEveryCodeNotify
15
- } = require('../lib/codex-config');
16
- const { upsertClaudeHook, buildClaudeHookCommand, isClaudeHookConfigured } = require('../lib/claude-config');
20
+ readEveryCodeNotify,
21
+ } = require("../lib/codex-config");
22
+ const {
23
+ upsertClaudeHook,
24
+ buildClaudeHookCommand,
25
+ isClaudeHookConfigured,
26
+ } = require("../lib/claude-config");
17
27
  const {
18
28
  resolveGeminiConfigDir,
19
29
  resolveGeminiSettingsPath,
20
30
  buildGeminiHookCommand,
21
31
  upsertGeminiHook,
22
- isGeminiHookConfigured
23
- } = require('../lib/gemini-config');
24
- const { resolveOpencodeConfigDir, upsertOpencodePlugin, isOpencodePluginInstalled } = require('../lib/opencode-config');
25
- const { removeOpenclawHookConfig, probeOpenclawHookState } = require('../lib/openclaw-hook');
32
+ isGeminiHookConfigured,
33
+ } = require("../lib/gemini-config");
34
+ const {
35
+ resolveOpencodeConfigDir,
36
+ upsertOpencodePlugin,
37
+ isOpencodePluginInstalled,
38
+ } = require("../lib/opencode-config");
39
+ const { removeOpenclawHookConfig, probeOpenclawHookState } = require("../lib/openclaw-hook");
26
40
  const {
27
41
  installOpenclawSessionPlugin,
28
- probeOpenclawSessionPluginState
29
- } = require('../lib/openclaw-session-plugin');
30
- const { beginBrowserAuth, openInBrowser } = require('../lib/browser-auth');
42
+ probeOpenclawSessionPluginState,
43
+ } = require("../lib/openclaw-session-plugin");
44
+ const { beginBrowserAuth, openInBrowser } = require("../lib/browser-auth");
31
45
  const {
32
46
  issueDeviceTokenWithPassword,
33
47
  issueDeviceTokenWithAccessToken,
34
- issueDeviceTokenWithLinkCode
35
- } = require('../lib/insforge');
36
- const { resolveTrackerPaths } = require('../lib/tracker-paths');
37
- const { resolveRuntimeConfig } = require('../lib/runtime-config');
48
+ issueDeviceTokenWithLinkCode,
49
+ } = require("../lib/insforge");
50
+ const { resolveTrackerPaths } = require("../lib/tracker-paths");
51
+ const { resolveRuntimeConfig } = require("../lib/runtime-config");
38
52
  const {
39
53
  BOLD,
40
54
  DIM,
@@ -43,21 +57,21 @@ const {
43
57
  color,
44
58
  isInteractive,
45
59
  promptMenu,
46
- createSpinner
47
- } = require('../lib/cli-ui');
48
- const { renderLocalReport, renderAuthTransition, renderSuccessBox } = require('../lib/init-flow');
60
+ createSpinner,
61
+ } = require("../lib/cli-ui");
62
+ const { renderLocalReport, renderAuthTransition, renderSuccessBox } = require("../lib/init-flow");
49
63
 
50
64
  const ASCII_LOGO = [
51
- '██╗ ██╗██╗██████╗ ███████╗██╗ ██╗███████╗ █████╗ ██████╗ ███████╗',
52
- '██║ ██║██║██╔══██╗██╔════╝██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝',
53
- '██║ ██║██║██████╔╝█████╗ ██║ ██║███████╗███████║██║ ███╗█████╗',
54
- '╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██║╚════██║██╔══██║██║ ██║██╔══╝',
55
- ' ╚████╔╝ ██║██████╔╝███████╗╚██████╔╝███████║██║ ██║╚██████╔╝███████╗',
56
- ' ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝'
57
- ].join('\n');
65
+ "██╗ ██╗██╗██████╗ ███████╗██╗ ██╗███████╗ █████╗ ██████╗ ███████╗",
66
+ "██║ ██║██║██╔══██╗██╔════╝██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝",
67
+ "██║ ██║██║██████╔╝█████╗ ██║ ██║███████╗███████║██║ ███╗█████╗",
68
+ "╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██║╚════██║██╔══██║██║ ██║██╔══╝",
69
+ " ╚████╔╝ ██║██████╔╝███████╗╚██████╔╝███████║██║ ██║╚██████╔╝███████╗",
70
+ " ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝",
71
+ ].join("\n");
58
72
 
59
- const DIVIDER = '----------------------------------------------';
60
- const DEFAULT_DASHBOARD_URL = 'https://www.vibeusage.cc';
73
+ const DIVIDER = "----------------------------------------------";
74
+ const DEFAULT_DASHBOARD_URL = "https://www.vibeusage.cc";
61
75
 
62
76
  async function cmdInit(argv) {
63
77
  const opts = parseArgs(argv);
@@ -65,37 +79,39 @@ async function cmdInit(argv) {
65
79
 
66
80
  const { rootDir, trackerDir, binDir } = await resolveTrackerPaths({ home });
67
81
 
68
- const configPath = path.join(trackerDir, 'config.json');
69
- const notifyOriginalPath = path.join(trackerDir, 'codex_notify_original.json');
70
- const linkCodeStatePath = path.join(trackerDir, 'link_code_state.json');
82
+ const configPath = path.join(trackerDir, "config.json");
83
+ const notifyOriginalPath = path.join(trackerDir, "codex_notify_original.json");
84
+ const linkCodeStatePath = path.join(trackerDir, "link_code_state.json");
71
85
 
72
86
  const existingConfig = await readJson(configPath);
73
87
  const runtime = resolveRuntimeConfig({
74
88
  cli: { baseUrl: opts.baseUrl, dashboardUrl: opts.dashboardUrl },
75
89
  config: existingConfig || {},
76
- env: process.env
90
+ env: process.env,
77
91
  });
78
92
  const baseUrl = runtime.baseUrl;
79
93
  let dashboardUrl = runtime.dashboardUrl || DEFAULT_DASHBOARD_URL;
80
- const notifyPath = path.join(binDir, 'notify.cjs');
81
- const appDir = path.join(trackerDir, 'app');
82
- const trackerBinPath = path.join(appDir, 'bin', 'tracker.js');
94
+ const notifyPath = path.join(binDir, "notify.cjs");
95
+ const appDir = path.join(trackerDir, "app");
96
+ const trackerBinPath = path.join(appDir, "bin", "tracker.js");
83
97
 
84
98
  renderWelcome();
85
99
 
86
100
  if (opts.dryRun) {
87
- process.stdout.write(`${color('Dry run: preview only (no changes applied).', DIM)}\n\n`);
101
+ process.stdout.write(`${color("Dry run: preview only (no changes applied).", DIM)}\n\n`);
88
102
  }
89
103
 
90
104
  if (isInteractive() && !opts.yes && !opts.dryRun) {
91
105
  const choice = await promptMenu({
92
- message: '? Proceed with installation?',
93
- options: ['Yes, configure my environment', 'No, exit'],
94
- defaultIndex: 0
106
+ message: "? Proceed with installation?",
107
+ options: ["Yes, configure my environment", "No, exit"],
108
+ defaultIndex: 0,
95
109
  });
96
- const normalizedChoice = String(choice || '').trim().toLowerCase();
97
- if (normalizedChoice.startsWith('no') || normalizedChoice.includes('exit')) {
98
- process.stdout.write('Setup cancelled.\n');
110
+ const normalizedChoice = String(choice || "")
111
+ .trim()
112
+ .toLowerCase();
113
+ if (normalizedChoice.startsWith("no") || normalizedChoice.includes("exit")) {
114
+ process.stdout.write("Setup cancelled.\n");
99
115
  return;
100
116
  }
101
117
  }
@@ -106,18 +122,18 @@ async function cmdInit(argv) {
106
122
  home,
107
123
  trackerDir,
108
124
  notifyPath,
109
- runtime
125
+ runtime,
110
126
  });
111
127
  renderLocalReport({ summary: preview.summary, isDryRun: true });
112
128
  if (preview.pendingBrowserAuth) {
113
- process.stdout.write('Account linking would be required for full setup.\n');
129
+ process.stdout.write("Account linking would be required for full setup.\n");
114
130
  } else if (!preview.deviceToken) {
115
- renderAccountNotLinked({ context: 'dry-run' });
131
+ renderAccountNotLinked({ context: "dry-run" });
116
132
  }
117
133
  return;
118
134
  }
119
135
 
120
- const spinner = createSpinner({ text: 'Analyzing and configuring local environment...' });
136
+ const spinner = createSpinner({ text: "Analyzing and configuring local environment..." });
121
137
  spinner.start();
122
138
  let setup;
123
139
  try {
@@ -134,7 +150,7 @@ async function cmdInit(argv) {
134
150
  appDir,
135
151
  trackerBinPath,
136
152
  runtime,
137
- existingConfig
153
+ existingConfig,
138
154
  });
139
155
  } catch (err) {
140
156
  spinner.stop();
@@ -149,7 +165,12 @@ async function cmdInit(argv) {
149
165
 
150
166
  if (setup.pendingBrowserAuth) {
151
167
  const deviceName = opts.deviceName || os.hostname();
152
- const flow = await beginBrowserAuth({ baseUrl, dashboardUrl, timeoutMs: 10 * 60_000, open: false });
168
+ const flow = await beginBrowserAuth({
169
+ baseUrl,
170
+ dashboardUrl,
171
+ timeoutMs: 10 * 60_000,
172
+ open: false,
173
+ });
153
174
  const canAutoOpen = !opts.noOpen;
154
175
  renderAuthTransition({ authUrl: flow.authUrl, canAutoOpen });
155
176
  if (canAutoOpen) {
@@ -157,7 +178,11 @@ async function cmdInit(argv) {
157
178
  openInBrowser(flow.authUrl);
158
179
  }
159
180
  const callback = await flow.waitForCallback();
160
- const issued = await issueDeviceTokenWithAccessToken({ baseUrl, accessToken: callback.accessToken, deviceName });
181
+ const issued = await issueDeviceTokenWithAccessToken({
182
+ baseUrl,
183
+ accessToken: callback.accessToken,
184
+ deviceName,
185
+ });
161
186
  deviceToken = issued.token;
162
187
  deviceId = issued.deviceId;
163
188
  await writeJson(configPath, { baseUrl, deviceToken, deviceId, installedAt: setup.installedAt });
@@ -172,9 +197,9 @@ async function cmdInit(argv) {
172
197
  }
173
198
 
174
199
  try {
175
- spawnInitSync({ trackerBinPath, packageName: 'vibeusage' });
200
+ spawnInitSync({ trackerBinPath, packageName: "vibeusage" });
176
201
  } catch (err) {
177
- const msg = err && err.message ? err.message : 'unknown error';
202
+ const msg = err && err.message ? err.message : "unknown error";
178
203
  process.stderr.write(`Initial sync spawn failed: ${msg}\n`);
179
204
  }
180
205
  }
@@ -183,29 +208,38 @@ function renderWelcome() {
183
208
  process.stdout.write(
184
209
  [
185
210
  ASCII_LOGO,
186
- '',
211
+ "",
187
212
  `${BOLD}Welcome to VibeScore CLI${RESET}`,
188
213
  DIVIDER,
189
214
  `${CYAN}Privacy First: Your content stays local. We only upload token counts and minimal metadata, never prompts or responses.${RESET}`,
190
215
  DIVIDER,
191
- '',
192
- 'This tool will:',
193
- ' - Analyze your local AI CLI configurations (Codex, Every Code, Claude, Gemini, Opencode, OpenClaw)',
194
- ' - Set up lightweight hooks to track your flow state',
195
- ' - Link your device to your VibeScore account',
196
- '',
197
- '(Nothing will be changed until you confirm below)',
198
- ''
199
- ].join('\n')
216
+ "",
217
+ "This tool will:",
218
+ " - Analyze your local AI CLI configurations (Codex, Every Code, Claude, Gemini, Opencode, OpenClaw)",
219
+ " - Set up lightweight hooks to track your flow state",
220
+ " - Link your device to your VibeScore account",
221
+ "",
222
+ "(Nothing will be changed until you confirm below)",
223
+ "",
224
+ ].join("\n"),
200
225
  );
201
226
  }
202
227
 
203
228
  function renderAccountNotLinked({ context } = {}) {
204
- if (context === 'dry-run') {
205
- process.stdout.write(['', 'Account not linked (dry run).', 'Run init without --dry-run to link your account.', ''].join('\n'));
229
+ if (context === "dry-run") {
230
+ process.stdout.write(
231
+ [
232
+ "",
233
+ "Account not linked (dry run).",
234
+ "Run init without --dry-run to link your account.",
235
+ "",
236
+ ].join("\n"),
237
+ );
206
238
  return;
207
239
  }
208
- process.stdout.write(['', 'Account not linked.', 'Set VIBEUSAGE_DEVICE_TOKEN then re-run init.', ''].join('\n'));
240
+ process.stdout.write(
241
+ ["", "Account not linked.", "Set VIBEUSAGE_DEVICE_TOKEN then re-run init.", ""].join("\n"),
242
+ );
209
243
  }
210
244
 
211
245
  function shouldUseBrowserAuth({ deviceToken, opts }) {
@@ -237,7 +271,7 @@ async function runSetup({
237
271
  appDir,
238
272
  trackerBinPath,
239
273
  runtime,
240
- existingConfig
274
+ existingConfig,
241
275
  }) {
242
276
  await ensureDir(trackerDir);
243
277
  await ensureDir(binDir);
@@ -252,7 +286,7 @@ async function runSetup({
252
286
  const deviceName = opts.deviceName || os.hostname();
253
287
  const platform = normalizePlatform(process.platform);
254
288
  const linkCode = String(opts.linkCode);
255
- const linkCodeHash = crypto.createHash('sha256').update(linkCode).digest('hex');
289
+ const linkCodeHash = crypto.createHash("sha256").update(linkCode).digest("hex");
256
290
  const existingLinkState = await readJson(linkCodeStatePath);
257
291
  let requestId =
258
292
  existingLinkState?.linkCodeHash === linkCodeHash && existingLinkState?.requestId
@@ -263,7 +297,7 @@ async function runSetup({
263
297
  await writeJson(linkCodeStatePath, {
264
298
  linkCodeHash,
265
299
  requestId,
266
- createdAt: new Date().toISOString()
300
+ createdAt: new Date().toISOString(),
267
301
  });
268
302
  await chmod600IfPossible(linkCodeStatePath);
269
303
  }
@@ -272,7 +306,7 @@ async function runSetup({
272
306
  linkCode,
273
307
  requestId,
274
308
  deviceName,
275
- platform
309
+ platform,
276
310
  });
277
311
  deviceToken = issued.token;
278
312
  deviceId = issued.deviceId;
@@ -281,8 +315,8 @@ async function runSetup({
281
315
  const deviceName = opts.deviceName || os.hostname();
282
316
 
283
317
  if (opts.email || opts.password) {
284
- const email = opts.email || (await prompt('Email: '));
285
- const password = opts.password || (await promptHidden('Password: '));
318
+ const email = opts.email || (await prompt("Email: "));
319
+ const password = opts.password || (await promptHidden("Password: "));
286
320
  const issued = await issueDeviceTokenWithPassword({ baseUrl, email, password, deviceName });
287
321
  deviceToken = issued.token;
288
322
  deviceId = issued.deviceId;
@@ -295,7 +329,7 @@ async function runSetup({
295
329
  baseUrl,
296
330
  deviceToken,
297
331
  deviceId,
298
- installedAt
332
+ installedAt,
299
333
  };
300
334
 
301
335
  await writeJson(configPath, config);
@@ -303,7 +337,7 @@ async function runSetup({
303
337
 
304
338
  await writeFileAtomic(
305
339
  notifyPath,
306
- buildNotifyHandler({ trackerDir, trackerBinPath, packageName: 'vibeusage' })
340
+ buildNotifyHandler({ trackerDir, trackerBinPath, packageName: "vibeusage" }),
307
341
  );
308
342
  await fs.chmod(notifyPath, 0o755).catch(() => {});
309
343
 
@@ -311,7 +345,7 @@ async function runSetup({
311
345
  home,
312
346
  trackerDir,
313
347
  notifyPath,
314
- notifyOriginalPath
348
+ notifyOriginalPath,
315
349
  });
316
350
 
317
351
  return {
@@ -319,21 +353,21 @@ async function runSetup({
319
353
  pendingBrowserAuth,
320
354
  deviceToken,
321
355
  deviceId,
322
- installedAt
356
+ installedAt,
323
357
  };
324
358
  }
325
359
 
326
360
  function buildIntegrationTargets({ home, trackerDir, notifyPath }) {
327
- const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
328
- const codexConfigPath = path.join(codexHome, 'config.toml');
329
- const codeHome = process.env.CODE_HOME || path.join(home, '.code');
330
- const codeConfigPath = path.join(codeHome, 'config.toml');
331
- const notifyOriginalPath = path.join(trackerDir, 'codex_notify_original.json');
332
- const codeNotifyOriginalPath = path.join(trackerDir, 'code_notify_original.json');
333
- const notifyCmd = ['/usr/bin/env', 'node', notifyPath];
334
- const codeNotifyCmd = ['/usr/bin/env', 'node', notifyPath, '--source=every-code'];
335
- const claudeDir = path.join(home, '.claude');
336
- const claudeSettingsPath = path.join(claudeDir, 'settings.json');
361
+ const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
362
+ const codexConfigPath = path.join(codexHome, "config.toml");
363
+ const codeHome = process.env.CODE_HOME || path.join(home, ".code");
364
+ const codeConfigPath = path.join(codeHome, "config.toml");
365
+ const notifyOriginalPath = path.join(trackerDir, "codex_notify_original.json");
366
+ const codeNotifyOriginalPath = path.join(trackerDir, "code_notify_original.json");
367
+ const notifyCmd = ["/usr/bin/env", "node", notifyPath];
368
+ const codeNotifyCmd = ["/usr/bin/env", "node", notifyPath, "--source=every-code"];
369
+ const claudeDir = path.join(home, ".claude");
370
+ const claudeSettingsPath = path.join(claudeDir, "settings.json");
337
371
  const claudeHookCommand = buildClaudeHookCommand(notifyPath);
338
372
  const geminiConfigDir = resolveGeminiConfigDir({ home, env: process.env });
339
373
  const geminiSettingsPath = resolveGeminiSettingsPath({ configDir: geminiConfigDir });
@@ -354,7 +388,7 @@ function buildIntegrationTargets({ home, trackerDir, notifyPath }) {
354
388
  geminiConfigDir,
355
389
  geminiSettingsPath,
356
390
  geminiHookCommand,
357
- opencodeConfigDir
391
+ opencodeConfigDir,
358
392
  };
359
393
  }
360
394
 
@@ -369,89 +403,107 @@ async function applyIntegrationSetup({ home, trackerDir, notifyPath, notifyOrigi
369
403
  const result = await upsertCodexNotify({
370
404
  codexConfigPath: context.codexConfigPath,
371
405
  notifyCmd: context.notifyCmd,
372
- notifyOriginalPath: context.notifyOriginalPath
406
+ notifyOriginalPath: context.notifyOriginalPath,
373
407
  });
374
408
  summary.push({
375
- label: 'Codex CLI',
376
- status: result.changed ? 'updated' : 'set',
377
- detail: result.changed ? 'Updated config' : 'Config already set'
409
+ label: "Codex CLI",
410
+ status: result.changed ? "updated" : "set",
411
+ detail: result.changed ? "Updated config" : "Config already set",
378
412
  });
379
413
  } else {
380
- summary.push({ label: 'Codex CLI', status: 'skipped', detail: renderSkipDetail(codexProbe) });
414
+ summary.push({ label: "Codex CLI", status: "skipped", detail: renderSkipDetail(codexProbe) });
381
415
  }
382
416
 
383
417
  const claudeDirExists = await isDir(context.claudeDir);
384
418
  if (claudeDirExists) {
385
419
  await upsertClaudeHook({
386
420
  settingsPath: context.claudeSettingsPath,
387
- hookCommand: context.claudeHookCommand
421
+ hookCommand: context.claudeHookCommand,
388
422
  });
389
- summary.push({ label: 'Claude', status: 'installed', detail: 'Hooks installed' });
423
+ summary.push({ label: "Claude", status: "installed", detail: "Hooks installed" });
390
424
  } else {
391
- summary.push({ label: 'Claude', status: 'skipped', detail: 'Config not found' });
425
+ summary.push({ label: "Claude", status: "skipped", detail: "Config not found" });
392
426
  }
393
427
 
394
428
  const geminiConfigExists = await isDir(context.geminiConfigDir);
395
429
  if (geminiConfigExists) {
396
430
  await upsertGeminiHook({
397
431
  settingsPath: context.geminiSettingsPath,
398
- hookCommand: context.geminiHookCommand
432
+ hookCommand: context.geminiHookCommand,
399
433
  });
400
- summary.push({ label: 'Gemini', status: 'installed', detail: 'Hooks installed' });
434
+ summary.push({ label: "Gemini", status: "installed", detail: "Hooks installed" });
401
435
  } else {
402
- summary.push({ label: 'Gemini', status: 'skipped', detail: 'Config not found' });
436
+ summary.push({ label: "Gemini", status: "skipped", detail: "Config not found" });
403
437
  }
404
438
 
405
439
  const opencodeResult = await upsertOpencodePlugin({
406
440
  configDir: context.opencodeConfigDir,
407
- notifyPath
441
+ notifyPath,
408
442
  });
409
- if (opencodeResult?.skippedReason === 'config-missing') {
410
- summary.push({ label: 'Opencode Plugin', status: 'skipped', detail: 'Config not found' });
443
+ if (opencodeResult?.skippedReason === "config-missing") {
444
+ summary.push({ label: "Opencode Plugin", status: "skipped", detail: "Config not found" });
411
445
  } else {
412
- summary.push({ label: 'Opencode Plugin', status: opencodeResult?.changed ? 'installed' : 'set', detail: 'Plugin installed' });
446
+ summary.push({
447
+ label: "Opencode Plugin",
448
+ status: opencodeResult?.changed ? "installed" : "set",
449
+ detail: "Plugin installed",
450
+ });
413
451
  }
414
452
 
415
- const openclawBefore = await probeOpenclawSessionPluginState({ home, trackerDir, env: process.env });
453
+ const openclawBefore = await probeOpenclawSessionPluginState({
454
+ home,
455
+ trackerDir,
456
+ env: process.env,
457
+ });
416
458
  const openclawInstall = await installOpenclawSessionPlugin({
417
459
  home,
418
460
  trackerDir,
419
- packageName: 'vibeusage',
420
- env: process.env
461
+ packageName: "vibeusage",
462
+ env: process.env,
421
463
  });
422
- if (openclawInstall?.skippedReason === 'openclaw-cli-missing') {
423
- summary.push({ label: 'OpenClaw Session Plugin', status: 'skipped', detail: 'OpenClaw CLI not found' });
424
- } else if (openclawInstall?.skippedReason === 'openclaw-plugins-install-failed') {
464
+ if (openclawInstall?.skippedReason === "openclaw-cli-missing") {
465
+ summary.push({
466
+ label: "OpenClaw Session Plugin",
467
+ status: "skipped",
468
+ detail: "OpenClaw CLI not found",
469
+ });
470
+ } else if (openclawInstall?.skippedReason === "openclaw-plugins-install-failed") {
425
471
  summary.push({
426
- label: 'OpenClaw Session Plugin',
427
- status: 'skipped',
428
- detail: `Install failed${openclawInstall.error ? `: ${openclawInstall.error}` : ''}`
472
+ label: "OpenClaw Session Plugin",
473
+ status: "skipped",
474
+ detail: `Install failed${openclawInstall.error ? `: ${openclawInstall.error}` : ""}`,
429
475
  });
430
- } else if (openclawInstall?.skippedReason === 'openclaw-config-unreadable') {
476
+ } else if (openclawInstall?.skippedReason === "openclaw-config-unreadable") {
431
477
  summary.push({
432
- label: 'OpenClaw Session Plugin',
433
- status: 'skipped',
434
- detail: openclawInstall.error ? `OpenClaw config unreadable: ${openclawInstall.error}` : 'OpenClaw config unreadable'
478
+ label: "OpenClaw Session Plugin",
479
+ status: "skipped",
480
+ detail: openclawInstall.error
481
+ ? `OpenClaw config unreadable: ${openclawInstall.error}`
482
+ : "OpenClaw config unreadable",
435
483
  });
436
484
  } else if (openclawInstall?.configured) {
437
485
  summary.push({
438
- label: 'OpenClaw Session Plugin',
439
- status: openclawBefore?.configured ? 'set' : 'installed',
486
+ label: "OpenClaw Session Plugin",
487
+ status: openclawBefore?.configured ? "set" : "installed",
440
488
  detail: openclawBefore?.configured
441
- ? 'Session plugin already linked'
442
- : 'Session plugin linked (restart OpenClaw gateway to activate)'
489
+ ? "Session plugin already linked"
490
+ : "Session plugin linked (restart OpenClaw gateway to activate)",
443
491
  });
444
492
  } else {
445
- summary.push({ label: 'OpenClaw Session Plugin', status: 'skipped', detail: 'OpenClaw session plugin unavailable' });
493
+ summary.push({
494
+ label: "OpenClaw Session Plugin",
495
+ status: "skipped",
496
+ detail: "OpenClaw session plugin unavailable",
497
+ });
446
498
  }
447
499
 
448
500
  const legacyHookState = await probeOpenclawHookState({ home, trackerDir, env: process.env });
449
501
  if (legacyHookState?.configured || legacyHookState?.linked || legacyHookState?.enabled) {
450
502
  await removeOpenclawHookConfig({ home, trackerDir, env: process.env });
451
503
  summary.push({
452
- label: 'OpenClaw Hook (legacy)',
453
- status: 'updated',
454
- detail: 'Removed legacy command hook (migrated to session plugin)'
504
+ label: "OpenClaw Hook (legacy)",
505
+ status: "updated",
506
+ detail: "Removed legacy command hook (migrated to session plugin)",
455
507
  });
456
508
  }
457
509
 
@@ -460,15 +512,15 @@ async function applyIntegrationSetup({ home, trackerDir, notifyPath, notifyOrigi
460
512
  const result = await upsertEveryCodeNotify({
461
513
  codeConfigPath: context.codeConfigPath,
462
514
  notifyCmd: context.codeNotifyCmd,
463
- notifyOriginalPath: context.codeNotifyOriginalPath
515
+ notifyOriginalPath: context.codeNotifyOriginalPath,
464
516
  });
465
517
  summary.push({
466
- label: 'Every Code',
467
- status: result.changed ? 'updated' : 'set',
468
- detail: result.changed ? 'Updated config' : 'Config already set'
518
+ label: "Every Code",
519
+ status: result.changed ? "updated" : "set",
520
+ detail: result.changed ? "Updated config" : "Config already set",
469
521
  });
470
522
  } else {
471
- summary.push({ label: 'Every Code', status: 'skipped', detail: renderSkipDetail(codeProbe) });
523
+ summary.push({ label: "Every Code", status: "skipped", detail: renderSkipDetail(codeProbe) });
472
524
  }
473
525
 
474
526
  return summary;
@@ -483,82 +535,96 @@ async function previewIntegrations({ context }) {
483
535
  const existing = await readCodexNotify(context.codexConfigPath);
484
536
  const matches = arraysEqual(existing, context.notifyCmd);
485
537
  summary.push({
486
- label: 'Codex CLI',
487
- status: matches ? 'set' : 'updated',
488
- detail: matches ? 'Already configured' : 'Will update config'
538
+ label: "Codex CLI",
539
+ status: matches ? "set" : "updated",
540
+ detail: matches ? "Already configured" : "Will update config",
489
541
  });
490
542
  } else {
491
- summary.push({ label: 'Codex CLI', status: 'skipped', detail: renderSkipDetail(codexProbe) });
543
+ summary.push({ label: "Codex CLI", status: "skipped", detail: renderSkipDetail(codexProbe) });
492
544
  }
493
545
 
494
546
  const claudeDirExists = await isDir(context.claudeDir);
495
547
  if (claudeDirExists) {
496
548
  const configured = await isClaudeHookConfigured({
497
549
  settingsPath: context.claudeSettingsPath,
498
- hookCommand: context.claudeHookCommand
550
+ hookCommand: context.claudeHookCommand,
499
551
  });
500
552
  summary.push({
501
- label: 'Claude',
502
- status: 'installed',
503
- detail: configured ? 'Hooks already installed' : 'Will install hooks'
553
+ label: "Claude",
554
+ status: "installed",
555
+ detail: configured ? "Hooks already installed" : "Will install hooks",
504
556
  });
505
557
  } else {
506
- summary.push({ label: 'Claude', status: 'skipped', detail: 'Config not found' });
558
+ summary.push({ label: "Claude", status: "skipped", detail: "Config not found" });
507
559
  }
508
560
 
509
561
  const geminiConfigExists = await isDir(context.geminiConfigDir);
510
562
  if (geminiConfigExists) {
511
563
  const configured = await isGeminiHookConfigured({
512
564
  settingsPath: context.geminiSettingsPath,
513
- hookCommand: context.geminiHookCommand
565
+ hookCommand: context.geminiHookCommand,
514
566
  });
515
567
  summary.push({
516
- label: 'Gemini',
517
- status: 'installed',
518
- detail: configured ? 'Hooks already installed' : 'Will install hooks'
568
+ label: "Gemini",
569
+ status: "installed",
570
+ detail: configured ? "Hooks already installed" : "Will install hooks",
519
571
  });
520
572
  } else {
521
- summary.push({ label: 'Gemini', status: 'skipped', detail: 'Config not found' });
573
+ summary.push({ label: "Gemini", status: "skipped", detail: "Config not found" });
522
574
  }
523
575
 
524
576
  const opencodeDirExists = await isDir(context.opencodeConfigDir);
525
577
  const installed = await isOpencodePluginInstalled({ configDir: context.opencodeConfigDir });
526
578
  const opencodeDetail = installed
527
- ? 'Plugin already installed'
579
+ ? "Plugin already installed"
528
580
  : opencodeDirExists
529
- ? 'Will install plugin'
530
- : 'Will create config and install plugin';
581
+ ? "Will install plugin"
582
+ : "Will create config and install plugin";
531
583
  summary.push({
532
- label: 'Opencode Plugin',
533
- status: 'installed',
534
- detail: opencodeDetail
584
+ label: "Opencode Plugin",
585
+ status: "installed",
586
+ detail: opencodeDetail,
535
587
  });
536
588
 
537
- const openclawState = await probeOpenclawSessionPluginState({ home, trackerDir: context.trackerDir, env: process.env });
538
- if (openclawState?.skippedReason === 'openclaw-config-missing') {
539
- summary.push({ label: 'OpenClaw Session Plugin', status: 'skipped', detail: 'OpenClaw config not found' });
540
- } else if (openclawState?.skippedReason === 'openclaw-config-unreadable') {
589
+ const openclawState = await probeOpenclawSessionPluginState({
590
+ home,
591
+ trackerDir: context.trackerDir,
592
+ env: process.env,
593
+ });
594
+ if (openclawState?.skippedReason === "openclaw-config-missing") {
541
595
  summary.push({
542
- label: 'OpenClaw Session Plugin',
543
- status: 'skipped',
544
- detail: openclawState.error ? `OpenClaw config unreadable: ${openclawState.error}` : 'OpenClaw config unreadable'
596
+ label: "OpenClaw Session Plugin",
597
+ status: "skipped",
598
+ detail: "OpenClaw config not found",
599
+ });
600
+ } else if (openclawState?.skippedReason === "openclaw-config-unreadable") {
601
+ summary.push({
602
+ label: "OpenClaw Session Plugin",
603
+ status: "skipped",
604
+ detail: openclawState.error
605
+ ? `OpenClaw config unreadable: ${openclawState.error}`
606
+ : "OpenClaw config unreadable",
545
607
  });
546
608
  } else {
547
609
  summary.push({
548
- label: 'OpenClaw Session Plugin',
549
- status: openclawState?.configured ? 'set' : 'installed',
610
+ label: "OpenClaw Session Plugin",
611
+ status: openclawState?.configured ? "set" : "installed",
550
612
  detail: openclawState?.configured
551
- ? 'Session plugin already linked'
552
- : 'Will link session plugin (restart OpenClaw gateway to activate)'
613
+ ? "Session plugin already linked"
614
+ : "Will link session plugin (restart OpenClaw gateway to activate)",
553
615
  });
554
616
  }
555
617
 
556
- const legacyHookState = await probeOpenclawHookState({ home, trackerDir: context.trackerDir, env: process.env });
618
+ const legacyHookState = await probeOpenclawHookState({
619
+ home,
620
+ trackerDir: context.trackerDir,
621
+ env: process.env,
622
+ });
557
623
  if (legacyHookState?.configured || legacyHookState?.linked || legacyHookState?.enabled) {
558
624
  summary.push({
559
- label: 'OpenClaw Hook (legacy)',
560
- status: 'updated',
561
- detail: 'Will remove legacy command hook during migration'
625
+ label: "OpenClaw Hook (legacy)",
626
+ status: "updated",
627
+ detail: "Will remove legacy command hook during migration",
562
628
  });
563
629
  }
564
630
 
@@ -567,22 +633,22 @@ async function previewIntegrations({ context }) {
567
633
  const existing = await readEveryCodeNotify(context.codeConfigPath);
568
634
  const matches = arraysEqual(existing, context.codeNotifyCmd);
569
635
  summary.push({
570
- label: 'Every Code',
571
- status: matches ? 'set' : 'updated',
572
- detail: matches ? 'Already configured' : 'Will update config'
636
+ label: "Every Code",
637
+ status: matches ? "set" : "updated",
638
+ detail: matches ? "Already configured" : "Will update config",
573
639
  });
574
640
  } else {
575
- summary.push({ label: 'Every Code', status: 'skipped', detail: renderSkipDetail(codeProbe) });
641
+ summary.push({ label: "Every Code", status: "skipped", detail: renderSkipDetail(codeProbe) });
576
642
  }
577
643
 
578
644
  return summary;
579
645
  }
580
646
 
581
647
  function renderSkipDetail(probe) {
582
- if (!probe || probe.reason === 'missing') return 'Config not found';
583
- if (probe.reason === 'permission-denied') return 'Permission denied';
584
- if (probe.reason === 'not-file') return 'Invalid config';
585
- return 'Unavailable';
648
+ if (!probe || probe.reason === "missing") return "Config not found";
649
+ if (probe.reason === "permission-denied") return "Permission denied";
650
+ if (probe.reason === "not-file") return "Invalid config";
651
+ return "Unavailable";
586
652
  }
587
653
 
588
654
  function arraysEqual(a, b) {
@@ -603,21 +669,21 @@ function parseArgs(argv) {
603
669
  noAuth: false,
604
670
  noOpen: false,
605
671
  yes: false,
606
- dryRun: false
672
+ dryRun: false,
607
673
  };
608
674
 
609
675
  for (let i = 0; i < argv.length; i++) {
610
676
  const a = argv[i];
611
- if (a === '--base-url') out.baseUrl = argv[++i] || null;
612
- else if (a === '--dashboard-url') out.dashboardUrl = argv[++i] || null;
613
- else if (a === '--email') out.email = argv[++i] || null;
614
- else if (a === '--password') out.password = argv[++i] || null;
615
- else if (a === '--device-name') out.deviceName = argv[++i] || null;
616
- else if (a === '--link-code') out.linkCode = argv[++i] || null;
617
- else if (a === '--no-auth') out.noAuth = true;
618
- else if (a === '--no-open') out.noOpen = true;
619
- else if (a === '--yes') out.yes = true;
620
- else if (a === '--dry-run') out.dryRun = true;
677
+ if (a === "--base-url") out.baseUrl = argv[++i] || null;
678
+ else if (a === "--dashboard-url") out.dashboardUrl = argv[++i] || null;
679
+ else if (a === "--email") out.email = argv[++i] || null;
680
+ else if (a === "--password") out.password = argv[++i] || null;
681
+ else if (a === "--device-name") out.deviceName = argv[++i] || null;
682
+ else if (a === "--link-code") out.linkCode = argv[++i] || null;
683
+ else if (a === "--no-auth") out.noAuth = true;
684
+ else if (a === "--no-open") out.noOpen = true;
685
+ else if (a === "--yes") out.yes = true;
686
+ else if (a === "--dry-run") out.dryRun = true;
621
687
  else throw new Error(`Unknown option: ${a}`);
622
688
  }
623
689
  return out;
@@ -629,19 +695,19 @@ function sleep(ms) {
629
695
  }
630
696
 
631
697
  function normalizePlatform(value) {
632
- if (value === 'darwin') return 'macos';
633
- if (value === 'win32') return 'windows';
634
- if (value === 'linux') return 'linux';
635
- return 'unknown';
698
+ if (value === "darwin") return "macos";
699
+ if (value === "win32") return "windows";
700
+ if (value === "linux") return "linux";
701
+ return "unknown";
636
702
  }
637
703
 
638
704
  function buildNotifyHandler({ trackerDir, packageName }) {
639
705
  // Keep this file dependency-free: Node built-ins only.
640
706
  // It must never block Codex; it spawns sync in the background and exits 0.
641
- const queueSignalPath = path.join(trackerDir, 'notify.signal');
642
- const originalPath = path.join(trackerDir, 'codex_notify_original.json');
643
- const fallbackPkg = packageName || 'vibeusage';
644
- const trackerBinPath = path.join(trackerDir, 'app', 'bin', 'tracker.js');
707
+ const queueSignalPath = path.join(trackerDir, "notify.signal");
708
+ const originalPath = path.join(trackerDir, "codex_notify_original.json");
709
+ const fallbackPkg = packageName || "vibeusage";
710
+ const trackerBinPath = path.join(trackerDir, "app", "bin", "tracker.js");
645
711
 
646
712
  return `#!/usr/bin/env node
647
713
  'use strict';
@@ -671,7 +737,7 @@ for (let i = 0; i < rawArgs.length; i++) {
671
737
  const trackerDir = ${JSON.stringify(trackerDir)};
672
738
  const signalPath = ${JSON.stringify(queueSignalPath)};
673
739
  const codexOriginalPath = ${JSON.stringify(originalPath)};
674
- const codeOriginalPath = ${JSON.stringify(path.join(trackerDir, 'code_notify_original.json'))};
740
+ const codeOriginalPath = ${JSON.stringify(path.join(trackerDir, "code_notify_original.json"))};
675
741
  const trackerBinPath = ${JSON.stringify(trackerBinPath)};
676
742
  const depsMarkerPath = path.join(trackerDir, 'app', 'node_modules', '@insforge', 'sdk', 'package.json');
677
743
  const configPath = path.join(trackerDir, 'config.json');
@@ -791,11 +857,12 @@ async function probeFile(p) {
791
857
  try {
792
858
  const st = await fs.stat(p);
793
859
  if (st.isFile()) return { exists: true, reason: null };
794
- return { exists: false, reason: 'not-file' };
860
+ return { exists: false, reason: "not-file" };
795
861
  } catch (e) {
796
- if (e?.code === 'ENOENT' || e?.code === 'ENOTDIR') return { exists: false, reason: 'missing' };
797
- if (e?.code === 'EACCES' || e?.code === 'EPERM') return { exists: false, reason: 'permission-denied' };
798
- return { exists: false, reason: 'error', code: e?.code || 'unknown' };
862
+ if (e?.code === "ENOENT" || e?.code === "ENOTDIR") return { exists: false, reason: "missing" };
863
+ if (e?.code === "EACCES" || e?.code === "EPERM")
864
+ return { exists: false, reason: "permission-denied" };
865
+ return { exists: false, reason: "error", code: e?.code || "unknown" };
799
866
  }
800
867
  }
801
868
 
@@ -810,10 +877,10 @@ async function isDir(p) {
810
877
 
811
878
  async function installLocalTrackerApp({ appDir }) {
812
879
  // Copy the current package's runtime (bin + src) into ~/.vibeusage so notify can run sync without npx.
813
- const packageRoot = path.resolve(__dirname, '../..');
814
- const srcFrom = path.join(packageRoot, 'src');
815
- const binFrom = path.join(packageRoot, 'bin', 'tracker.js');
816
- const nodeModulesFrom = path.join(packageRoot, 'node_modules');
880
+ const packageRoot = path.resolve(__dirname, "../..");
881
+ const srcFrom = path.join(packageRoot, "src");
882
+ const binFrom = path.join(packageRoot, "bin", "tracker.js");
883
+ const nodeModulesFrom = path.join(packageRoot, "node_modules");
817
884
 
818
885
  // When running from the installed local runtime (or when appDir is symlinked to this package),
819
886
  // source and destination resolve to the same place. Do not delete appDir in that case.
@@ -821,10 +888,10 @@ async function installLocalTrackerApp({ appDir }) {
821
888
  return;
822
889
  }
823
890
 
824
- const srcTo = path.join(appDir, 'src');
825
- const binToDir = path.join(appDir, 'bin');
826
- const binTo = path.join(binToDir, 'tracker.js');
827
- const nodeModulesTo = path.join(appDir, 'node_modules');
891
+ const srcTo = path.join(appDir, "src");
892
+ const binToDir = path.join(appDir, "bin");
893
+ const binTo = path.join(binToDir, "tracker.js");
894
+ const nodeModulesTo = path.join(appDir, "node_modules");
828
895
 
829
896
  await fs.rm(appDir, { recursive: true, force: true }).catch(() => {});
830
897
  await ensureDir(appDir);
@@ -851,22 +918,22 @@ async function safeRealpath(p) {
851
918
  }
852
919
 
853
920
  function spawnInitSync({ trackerBinPath, packageName }) {
854
- const fallbackPkg = packageName || 'vibeusage';
855
- const argv = ['sync', '--drain'];
856
- const hasLocalRuntime = typeof trackerBinPath === 'string' && fssync.existsSync(trackerBinPath);
921
+ const fallbackPkg = packageName || "vibeusage";
922
+ const argv = ["sync", "--drain"];
923
+ const hasLocalRuntime = typeof trackerBinPath === "string" && fssync.existsSync(trackerBinPath);
857
924
  const cmd = hasLocalRuntime
858
925
  ? [process.execPath, trackerBinPath, ...argv]
859
- : ['npx', '--yes', fallbackPkg, ...argv];
926
+ : ["npx", "--yes", fallbackPkg, ...argv];
860
927
  const child = cp.spawn(cmd[0], cmd.slice(1), {
861
928
  detached: true,
862
- stdio: 'ignore',
863
- env: process.env
929
+ stdio: "ignore",
930
+ env: process.env,
864
931
  });
865
- child.on('error', (err) => {
866
- const msg = err && err.message ? err.message : 'unknown error';
867
- const detail = isDebugEnabled() ? ` (${msg})` : '';
932
+ child.on("error", (err) => {
933
+ const msg = err && err.message ? err.message : "unknown error";
934
+ const detail = isDebugEnabled() ? ` (${msg})` : "";
868
935
  process.stderr.write(`Minor issue: Background sync could not start${detail}.\n`);
869
- process.stderr.write('Run: npx --yes vibeusage sync\n');
936
+ process.stderr.write("Run: npx --yes vibeusage sync\n");
870
937
  });
871
938
  child.unref();
872
939
  }
@@ -887,5 +954,5 @@ async function copyRuntimeDependencies({ from, to }) {
887
954
  }
888
955
 
889
956
  function isDebugEnabled() {
890
- return process.env.VIBEUSAGE_DEBUG === '1';
957
+ return process.env.VIBEUSAGE_DEBUG === "1";
891
958
  }