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