@vibescore/tracker 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,11 @@ _Real-time AI Analytics for Codex CLI_
13
13
 
14
14
  [**English**](README.md) • [**中文说明**](README.zh-CN.md)
15
15
 
16
- [**Documentation**](docs/) • [**Dashboard**](dashboard/) • [**Backend API**](BACKEND_API.md)
16
+ [**Documentation**](docs/) • [**Dashboard**](dashboard/) • [**Backend API**](BACKEND_API.md) • [**Dashboard API**](docs/dashboard/api.md)
17
+
18
+ <br/>
19
+
20
+ <img src="docs/screenshots/dashboard.png" width="900" alt="VibeScore Dashboard Preview"/>
17
21
 
18
22
  </div>
19
23
 
@@ -25,32 +29,61 @@ _Real-time AI Analytics for Codex CLI_
25
29
 
26
30
  > [!TIP] > **Core Index**: Our signature metric that reflects your flow state by analyzing token consumption rates and patterns.
27
31
 
32
+ ## 🔒 Privacy-First Architecture (Stealth Protocol)
33
+
34
+ We believe your code and thoughts are your own. VibeScore is built with strict privacy pillars to ensure your data never leaves your control.
35
+
36
+ - 🛡️ **Zero Code Infiltration**: We never touch your source code or prompts. Our sniffer only extracts numeric token counts (Input, Output, Reasoning, Cached).
37
+ - 📡 **Local Aggregation**: All token consumption analysis happens on your machine. We only relay quantized 30-minute usage buckets to the cloud.
38
+ - 🔐 **Hashed Identity**: Device tokens are hashed using SHA-256 server-side. Your raw credentials never exist in our database.
39
+ - 🔦 **Full Transparency**: Audit the sync logic yourself in `src/lib/rollout.js`. We literally only capture numbers and timestamps.
40
+
28
41
  ## 🚀 Key Features
29
42
 
30
- - 📡 **Live Sniffer**: Real-time interception of Codex CLI pipes using low-level hooks to capture every completion event.
43
+ - 📡 **Live Sniffer & Auto-Sync**: Real-time interception of Codex CLI pipes with **automatic background synchronization**. Once initialized, your tokens are tracked and synced without any manual commands.
44
+ - 🧭 **Multi-source Ingestion**: Supports Codex CLI and Every Code (tagged as `source=every-code`) without modifying Every Code.
31
45
  - 📊 **Matrix Dashboard**: A high-performance React + Vite dashboard featuring heatmaps, trend charts, and live logs.
32
46
  - ⚡ **AI Analytics**: Deep analysis of Input/Output tokens, with dedicated tracking for Cached and Reasoning components.
33
47
  - 🔒 **Identity Core**: Robust authentication and permission management to secure your development data.
34
48
 
49
+ ### 🌌 Visual Preview
50
+
51
+ <img src="docs/screenshots/landing.png" width="900" alt="VibeScore Landing Preview"/>
52
+
35
53
  ## 🛠️ Quick Start
36
54
 
37
55
  ### Installation
38
56
 
39
- Initialize your environment with a single command:
57
+ Initialize your environment once and forget it. VibeScore handles all synchronization in the background automatically.
40
58
 
41
59
  ```bash
42
60
  npx --yes @vibescore/tracker init
43
61
  ```
44
62
 
63
+ Note: If `~/.code/config.toml` exists (or `CODE_HOME`), `init` also configures Every Code `notify` automatically. No further user intervention is required for data sync.
64
+
45
65
  ### Sync & Status
46
66
 
67
+ ````bash
68
+ While sync happens automatically, you can manually trigger a synchronization or check status anytime:
69
+
47
70
  ```bash
48
- # Sync latest local session data
71
+ # Manually sync latest local session data (Optional)
49
72
  npx --yes @vibescore/tracker sync
50
73
 
51
74
  # Check current link status
52
75
  npx --yes @vibescore/tracker status
53
- ```
76
+ ````
77
+
78
+ ### Sources
79
+
80
+ - Codex CLI logs: `~/.codex/sessions/**/rollout-*.jsonl` (override with `CODEX_HOME`)
81
+ - Every Code logs: `~/.code/sessions/**/rollout-*.jsonl` (override with `CODE_HOME`)
82
+
83
+ ## 🔧 Environment Variables
84
+
85
+ - `VIBESCORE_HTTP_TIMEOUT_MS`: CLI HTTP timeout in ms (default `20000`, `0` disables, clamped to `1000..120000`).
86
+ - `VITE_VIBESCORE_HTTP_TIMEOUT_MS`: Dashboard request timeout in ms (default `15000`, `0` disables, clamped to `1000..30000`).
54
87
 
55
88
  ## 🧰 Troubleshooting
56
89
 
@@ -60,9 +93,9 @@ npx --yes @vibescore/tracker status
60
93
  - If you expect a non-zero streak, clear cached auth/heatmap data and sign in again:
61
94
 
62
95
  ```js
63
- localStorage.removeItem('vibescore.dashboard.auth.v1');
96
+ localStorage.removeItem("vibescore.dashboard.auth.v1");
64
97
  Object.keys(localStorage)
65
- .filter((k) => k.startsWith('vibescore.heatmap.'))
98
+ .filter((k) => k.startsWith("vibescore.heatmap."))
66
99
  .forEach((k) => localStorage.removeItem(k));
67
100
  location.reload();
68
101
  ```
package/README.zh-CN.md CHANGED
@@ -13,7 +13,11 @@ _Codex CLI 实时 AI 分析工具_
13
13
 
14
14
  [**English**](README.md) • [**中文说明**](README.zh-CN.md)
15
15
 
16
- [**文档**](docs/) • [**控制台**](dashboard/) • [**后端接口**](BACKEND_API.md)
16
+ [**文档**](docs/) • [**控制台**](dashboard/) • [**后端接口**](BACKEND_API.md) • [**Dashboard API**](docs/dashboard/api.md)
17
+
18
+ <br/>
19
+
20
+ <img src="docs/screenshots/dashboard.png" width="900" alt="VibeScore 控制台预览"/>
17
21
 
18
22
  </div>
19
23
 
@@ -25,32 +29,61 @@ _Codex CLI 实时 AI 分析工具_
25
29
 
26
30
  > [!TIP] > **Core Index (核心指数)**: 我们的标志性指标,通过分析 Token 消耗速率与模式,反映你的开发心流状态。
27
31
 
32
+ ## 🔒 隐私优先架构 (隐身协议)
33
+
34
+ 我们坚信你的代码和思想属于你自己。VibeScore 建立在严格的隐私支柱之上,确保你的数据始终处于受控状态。
35
+
36
+ - 🛡️ **代码零入侵**:我们绝不触碰你的源代码或 Prompt。我们的嗅探器仅提取数值化的 Token 计数(输入、输出、推理、缓存)。
37
+ - 📡 **本地聚合**:所有 Token 消耗分析均在你的机器上完成。我们仅将量化的 30 分钟使用桶(Usage Buckets)中继到云端。
38
+ - 🔐 **身份哈希**:设备令牌在服务端使用 SHA-256 进行哈希处理。你的原始凭据绝不会存在于我们的数据库中。
39
+ - 🔦 **全程透明**:你可以亲自审计 `src/lib/rollout.js` 中的同步逻辑。我们真正采集的只有数字和时间戳。
40
+
28
41
  ## 🚀 核心功能
29
42
 
30
- - 📡 **Live Sniffer (实时嗅探)**: 实时监听 Codex CLI 管道,通过底层 Hook 捕获每一次补全事件。
43
+ - 📡 **自动嗅探与同步 (Auto-Sync)**: 实时监听 Codex CLI 管道并具备**全自动后台同步**功能。初始化后,你的 Token 产出将自动追踪并同步,无需手动执行脚本。
44
+ - 🧭 **多来源采集**:支持 Codex CLI 与 Every Code(标记为 `source=every-code`),无需修改 Every Code 客户端。
31
45
  - 📊 **Matrix Dashboard (矩阵控制台)**: 基于 React + Vite 的高性能仪表盘,具备热力图、趋势图与实时日志。
32
46
  - ⚡ **AI Analytics (AI 分析)**: 深度分析 Input/Output Token,支持缓存 (Cached) 与推理 (Reasoning) 部分的分离监控。
33
47
  - 🔒 **Identity Core (身份核心)**: 完备的身份验证与权限管理,保护你的开发数据资产。
34
48
 
49
+ ### 🌌 视觉预览
50
+
51
+ <img src="docs/screenshots/landing.png" width="900" alt="VibeScore 落地页预览"/>
52
+
35
53
  ## 🛠️ 快速开始
36
54
 
37
55
  ### 安装
38
56
 
39
- 只需一行命令,即可初始化环境:
57
+ 只需一次初始化,即可变身为“自动驾驶”模式。VibeScore 会在后台处理所有数据同步,你只需专注开发。
40
58
 
41
59
  ```bash
42
60
  npx --yes @vibescore/tracker init
43
61
  ```
44
62
 
63
+ 说明:若存在 `~/.code/config.toml`(或 `CODE_HOME`),`init` 会自动配置 Every Code 的 `notify`。配置完成后,数据同步完全自动化,无需后续人工干预。
64
+
45
65
  ### 同步与状态查看
46
66
 
67
+ ````bash
68
+ 虽然同步是自动完成的,但你仍可以随时手动触发同步或查看状态:
69
+
47
70
  ```bash
48
- # 同步最新的本地会话数据
71
+ # 手动同步最新的本地会话数据 (可选)
49
72
  npx --yes @vibescore/tracker sync
50
73
 
51
74
  # 查看当前连接状态
52
75
  npx --yes @vibescore/tracker status
53
- ```
76
+ ````
77
+
78
+ ### 日志来源
79
+
80
+ - Codex CLI 日志:`~/.codex/sessions/**/rollout-*.jsonl`(可用 `CODEX_HOME` 覆盖)
81
+ - Every Code 日志:`~/.code/sessions/**/rollout-*.jsonl`(可用 `CODE_HOME` 覆盖)
82
+
83
+ ## 🔧 环境变量
84
+
85
+ - `VIBESCORE_HTTP_TIMEOUT_MS`:CLI 请求超时(毫秒,默认 `20000`,`0` 表示关闭,范围 `1000..120000`)。
86
+ - `VITE_VIBESCORE_HTTP_TIMEOUT_MS`:Dashboard 请求超时(毫秒,默认 `15000`,`0` 表示关闭,范围 `1000..30000`)。
54
87
 
55
88
  ## 🧰 常见问题
56
89
 
@@ -60,9 +93,9 @@ npx --yes @vibescore/tracker status
60
93
  - 如果你确认应该有 streak,请清理本地缓存并重新登录:
61
94
 
62
95
  ```js
63
- localStorage.removeItem('vibescore.dashboard.auth.v1');
96
+ localStorage.removeItem("vibescore.dashboard.auth.v1");
64
97
  Object.keys(localStorage)
65
- .filter((k) => k.startsWith('vibescore.heatmap.'))
98
+ .filter((k) => k.startsWith("vibescore.heatmap."))
66
99
  .forEach((k) => localStorage.removeItem(k));
67
100
  location.reload();
68
101
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibescore/tracker",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Codex CLI token usage tracker (macOS-first, notify-driven).",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
package/src/cli.js CHANGED
@@ -48,8 +48,9 @@ function printHelp() {
48
48
  '',
49
49
  'Notes:',
50
50
  ' - init installs a Codex notify hook and issues a device token (default: browser sign in/up).',
51
+ ' - when ~/.code/config.toml exists, init also installs an Every Code notify hook.',
51
52
  ' - optional: set VIBESCORE_DASHBOARD_URL (or --dashboard-url) to use a hosted landing page.',
52
- ' - sync parses ~/.codex/sessions/**/rollout-*.jsonl and uploads token_count deltas.',
53
+ ' - sync parses ~/.codex/sessions/**/rollout-*.jsonl and ~/.code/sessions/**/rollout-*.jsonl (Every Code), then uploads token_count deltas.',
53
54
  ' - --debug prints original backend errors when they are normalized.',
54
55
  ''
55
56
  ].join('\n')
@@ -1,13 +1,20 @@
1
1
  const os = require('node:os');
2
2
  const path = require('node:path');
3
3
  const fs = require('node:fs/promises');
4
+ const fssync = require('node:fs');
5
+ const cp = require('node:child_process');
4
6
 
5
7
  const { ensureDir, writeFileAtomic, readJson, writeJson, chmod600IfPossible } = require('../lib/fs');
6
8
  const { prompt, promptHidden } = require('../lib/prompt');
7
- const { upsertCodexNotify, loadCodexNotifyOriginal } = require('../lib/codex-config');
9
+ const {
10
+ upsertCodexNotify,
11
+ loadCodexNotifyOriginal,
12
+ upsertEveryCodeNotify,
13
+ loadEveryCodeNotifyOriginal
14
+ } = require('../lib/codex-config');
15
+ const { upsertClaudeHook, buildClaudeHookCommand } = require('../lib/claude-config');
8
16
  const { beginBrowserAuth } = require('../lib/browser-auth');
9
17
  const { issueDeviceTokenWithPassword, issueDeviceTokenWithAccessToken } = require('../lib/insforge');
10
- const { cmdSync } = require('./sync');
11
18
 
12
19
  async function cmdInit(argv) {
13
20
  const opts = parseArgs(argv);
@@ -84,6 +91,8 @@ async function cmdInit(argv) {
84
91
 
85
92
  // Configure Codex notify hook.
86
93
  const codexConfigPath = path.join(home, '.codex', 'config.toml');
94
+ const codeHome = process.env.CODE_HOME || path.join(home, '.code');
95
+ const codeConfigPath = path.join(codeHome, 'config.toml');
87
96
  const notifyCmd = ['/usr/bin/env', 'node', notifyPath];
88
97
  const result = await upsertCodexNotify({
89
98
  codexConfigPath,
@@ -92,6 +101,31 @@ async function cmdInit(argv) {
92
101
  });
93
102
 
94
103
  const chained = await loadCodexNotifyOriginal(notifyOriginalPath);
104
+ const codeNotifyOriginalPath = path.join(trackerDir, 'code_notify_original.json');
105
+ const codeConfigExists = await isFile(codeConfigPath);
106
+ let codeResult = null;
107
+ let codeChained = null;
108
+ if (codeConfigExists) {
109
+ const codeNotifyCmd = ['/usr/bin/env', 'node', notifyPath, '--source=every-code'];
110
+ codeResult = await upsertEveryCodeNotify({
111
+ codeConfigPath,
112
+ notifyCmd: codeNotifyCmd,
113
+ notifyOriginalPath: codeNotifyOriginalPath
114
+ });
115
+ codeChained = await loadEveryCodeNotifyOriginal(codeNotifyOriginalPath);
116
+ }
117
+
118
+ const claudeDir = path.join(home, '.claude');
119
+ const claudeSettingsPath = path.join(claudeDir, 'settings.json');
120
+ const claudeDirExists = await isDir(claudeDir);
121
+ let claudeResult = null;
122
+ if (claudeDirExists) {
123
+ const claudeHookCommand = buildClaudeHookCommand(notifyPath);
124
+ claudeResult = await upsertClaudeHook({
125
+ settingsPath: claudeSettingsPath,
126
+ hookCommand: claudeHookCommand
127
+ });
128
+ }
95
129
 
96
130
  process.stdout.write(
97
131
  [
@@ -101,16 +135,32 @@ async function cmdInit(argv) {
101
135
  `- Codex config: ${codexConfigPath}`,
102
136
  result.changed ? '- Codex notify: updated' : '- Codex notify: already set',
103
137
  chained ? '- Codex notify: chained (original preserved)' : '- Codex notify: no original',
138
+ codeConfigExists ? `- Every Code config: ${codeConfigPath}` : '- Every Code notify: skipped (config.toml not found)',
139
+ codeConfigExists && codeResult
140
+ ? codeResult.changed
141
+ ? '- Every Code notify: updated'
142
+ : '- Every Code notify: already set'
143
+ : null,
144
+ codeConfigExists
145
+ ? codeChained
146
+ ? '- Every Code notify: chained (original preserved)'
147
+ : '- Every Code notify: no original'
148
+ : null,
149
+ claudeDirExists
150
+ ? claudeResult?.changed
151
+ ? `- Claude hooks: updated (${claudeSettingsPath})`
152
+ : `- Claude hooks: already set (${claudeSettingsPath})`
153
+ : '- Claude hooks: skipped (~/.claude not found)',
104
154
  deviceToken ? `- Device token: stored (${maskSecret(deviceToken)})` : '- Device token: not configured (set VIBESCORE_DEVICE_TOKEN and re-run init)',
105
155
  ''
106
156
  ].join('\n')
107
157
  );
108
158
 
109
159
  try {
110
- await cmdSync([]);
160
+ spawnInitSync({ trackerBinPath, packageName: '@vibescore/tracker' });
111
161
  } catch (err) {
112
162
  const msg = err && err.message ? err.message : 'unknown error';
113
- process.stderr.write(`Initial sync failed: ${msg}\n`);
163
+ process.stderr.write(`Initial sync spawn failed: ${msg}\n`);
114
164
  }
115
165
  }
116
166
 
@@ -160,13 +210,32 @@ const os = require('node:os');
160
210
  const path = require('node:path');
161
211
  const cp = require('node:child_process');
162
212
 
163
- const payload = process.argv[2] || '';
213
+ const rawArgs = process.argv.slice(2);
214
+ let source = 'codex';
215
+ const payloadArgs = [];
216
+ for (let i = 0; i < rawArgs.length; i++) {
217
+ const arg = rawArgs[i];
218
+ if (arg === '--source') {
219
+ source = rawArgs[i + 1] || source;
220
+ i += 1;
221
+ continue;
222
+ }
223
+ if (arg.startsWith('--source=')) {
224
+ source = arg.slice('--source='.length) || source;
225
+ continue;
226
+ }
227
+ payloadArgs.push(arg);
228
+ }
229
+
164
230
  const trackerDir = ${JSON.stringify(trackerDir)};
165
231
  const signalPath = ${JSON.stringify(queueSignalPath)};
166
- const originalPath = ${JSON.stringify(originalPath)};
232
+ const codexOriginalPath = ${JSON.stringify(originalPath)};
233
+ const codeOriginalPath = ${JSON.stringify(path.join(trackerDir, 'code_notify_original.json'))};
167
234
  const trackerBinPath = ${JSON.stringify(trackerBinPath)};
168
235
  const depsMarkerPath = path.join(trackerDir, 'app', 'node_modules', '@insforge', 'sdk', 'package.json');
169
236
  const fallbackPkg = ${JSON.stringify(fallbackPkg)};
237
+ const selfPath = path.resolve(__filename);
238
+ const home = os.homedir();
170
239
 
171
240
  try {
172
241
  fs.mkdirSync(trackerDir, { recursive: true });
@@ -191,14 +260,17 @@ try {
191
260
  }
192
261
  } catch (_) {}
193
262
 
194
- // Chain the original Codex notify if present.
263
+ // Chain the original notify if present (Codex/Every Code only).
195
264
  try {
196
- const original = JSON.parse(fs.readFileSync(originalPath, 'utf8'));
197
- const cmd = Array.isArray(original?.notify) ? original.notify : null;
198
- if (cmd && cmd.length > 0) {
199
- const args = cmd.slice(1);
200
- args.push(payload);
201
- spawnDetached([cmd[0], ...args]);
265
+ const originalPath = source === 'every-code' ? codeOriginalPath : source === 'claude' ? null : codexOriginalPath;
266
+ if (originalPath) {
267
+ const original = JSON.parse(fs.readFileSync(originalPath, 'utf8'));
268
+ const cmd = Array.isArray(original?.notify) ? original.notify : null;
269
+ if (cmd && cmd.length > 0 && !isSelfNotify(cmd)) {
270
+ const args = cmd.slice(1);
271
+ if (payloadArgs.length > 0) args.push(...payloadArgs);
272
+ spawnDetached([cmd[0], ...args]);
273
+ }
202
274
  }
203
275
  } catch (_) {}
204
276
 
@@ -214,6 +286,22 @@ function spawnDetached(argv) {
214
286
  child.unref();
215
287
  } catch (_) {}
216
288
  }
289
+
290
+ function resolveMaybeHome(p) {
291
+ if (typeof p !== 'string') return null;
292
+ if (p.startsWith('~/')) return path.join(home, p.slice(2));
293
+ return path.resolve(p);
294
+ }
295
+
296
+ function isSelfNotify(cmd) {
297
+ for (const part of cmd) {
298
+ if (typeof part !== 'string') continue;
299
+ if (!part.includes('notify.cjs')) continue;
300
+ const resolved = resolveMaybeHome(part);
301
+ if (resolved && resolved === selfPath) return true;
302
+ }
303
+ return false;
304
+ }
217
305
  `;
218
306
  }
219
307
 
@@ -248,6 +336,24 @@ async function checkUrlReachable(url) {
248
336
  }
249
337
  }
250
338
 
339
+ async function isFile(p) {
340
+ try {
341
+ const st = await fs.stat(p);
342
+ return st.isFile();
343
+ } catch (_e) {
344
+ return false;
345
+ }
346
+ }
347
+
348
+ async function isDir(p) {
349
+ try {
350
+ const st = await fs.stat(p);
351
+ return st.isDirectory();
352
+ } catch (_e) {
353
+ return false;
354
+ }
355
+ }
356
+
251
357
  async function installLocalTrackerApp({ appDir }) {
252
358
  // Copy the current package's runtime (bin + src) into ~/.vibescore so notify can run sync without npx.
253
359
  const packageRoot = path.resolve(__dirname, '../..');
@@ -269,6 +375,21 @@ async function installLocalTrackerApp({ appDir }) {
269
375
  await copyRuntimeDependencies({ from: nodeModulesFrom, to: nodeModulesTo });
270
376
  }
271
377
 
378
+ function spawnInitSync({ trackerBinPath, packageName }) {
379
+ const fallbackPkg = packageName || '@vibescore/tracker';
380
+ const argv = ['sync'];
381
+ const hasLocalRuntime = typeof trackerBinPath === 'string' && fssync.existsSync(trackerBinPath);
382
+ const cmd = hasLocalRuntime
383
+ ? [process.execPath, trackerBinPath, ...argv]
384
+ : ['npx', '--yes', fallbackPkg, ...argv];
385
+ const child = cp.spawn(cmd[0], cmd.slice(1), {
386
+ detached: true,
387
+ stdio: 'ignore',
388
+ env: process.env
389
+ });
390
+ child.unref();
391
+ }
392
+
272
393
  async function copyRuntimeDependencies({ from, to }) {
273
394
  try {
274
395
  const st = await fs.stat(from);
@@ -3,7 +3,8 @@ const path = require('node:path');
3
3
  const fs = require('node:fs/promises');
4
4
 
5
5
  const { readJson } = require('../lib/fs');
6
- const { readCodexNotify } = require('../lib/codex-config');
6
+ const { readCodexNotify, readEveryCodeNotify } = require('../lib/codex-config');
7
+ const { isClaudeHookConfigured, buildClaudeHookCommand } = require('../lib/claude-config');
7
8
  const { normalizeState: normalizeUploadState } = require('../lib/upload-throttle');
8
9
  const { collectTrackerDiagnostics } = require('../lib/diagnostics');
9
10
 
@@ -27,6 +28,10 @@ async function cmdStatus(argv = []) {
27
28
  const autoRetryPath = path.join(trackerDir, 'auto.retry.json');
28
29
  const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
29
30
  const codexConfigPath = path.join(codexHome, 'config.toml');
31
+ const codeHome = process.env.CODE_HOME || path.join(home, '.code');
32
+ const codeConfigPath = path.join(codeHome, 'config.toml');
33
+ const claudeSettingsPath = path.join(home, '.claude', 'settings.json');
34
+ const claudeHookCommand = buildClaudeHookCommand(path.join(home, '.vibescore', 'bin', 'notify.cjs'));
30
35
 
31
36
  const config = await readJson(configPath);
32
37
  const cursors = await readJson(cursorsPath);
@@ -42,6 +47,12 @@ async function cmdStatus(argv = []) {
42
47
 
43
48
  const codexNotify = await readCodexNotify(codexConfigPath);
44
49
  const notifyConfigured = Array.isArray(codexNotify) && codexNotify.length > 0;
50
+ const everyCodeNotify = await readEveryCodeNotify(codeConfigPath);
51
+ const everyCodeConfigured = Array.isArray(everyCodeNotify) && everyCodeNotify.length > 0;
52
+ const claudeHookConfigured = await isClaudeHookConfigured({
53
+ settingsPath: claudeSettingsPath,
54
+ hookCommand: claudeHookCommand
55
+ });
45
56
 
46
57
  const lastUpload = uploadThrottle.lastSuccessMs
47
58
  ? parseEpochMsToIso(uploadThrottle.lastSuccessMs)
@@ -75,6 +86,8 @@ async function cmdStatus(argv = []) {
75
86
  lastUploadError ? `- Last upload error: ${lastUploadError}` : null,
76
87
  autoRetryLine,
77
88
  `- Codex notify: ${notifyConfigured ? JSON.stringify(codexNotify) : 'unset'}`,
89
+ `- Every Code notify: ${everyCodeConfigured ? JSON.stringify(everyCodeNotify) : 'unset'}`,
90
+ `- Claude hooks: ${claudeHookConfigured ? 'set' : 'unset'}`,
78
91
  ''
79
92
  ]
80
93
  .filter(Boolean)
@@ -4,7 +4,7 @@ const fs = require('node:fs/promises');
4
4
  const cp = require('node:child_process');
5
5
 
6
6
  const { ensureDir, readJson, writeJson, openLock } = require('../lib/fs');
7
- const { listRolloutFiles, parseRolloutIncremental } = require('../lib/rollout');
7
+ const { listRolloutFiles, listClaudeProjectFiles, parseRolloutIncremental, parseClaudeIncremental } = require('../lib/rollout');
8
8
  const { drainQueueToCloud } = require('../lib/uploader');
9
9
  const { createProgress, renderBar, formatNumber, formatBytes } = require('../lib/progress');
10
10
  const { syncHeartbeat } = require('../lib/vibescore-api');
@@ -43,8 +43,24 @@ async function cmdSync(argv) {
43
43
  let uploadThrottleState = uploadThrottle;
44
44
 
45
45
  const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
46
- const sessionsDir = path.join(codexHome, 'sessions');
47
- const rolloutFiles = await listRolloutFiles(sessionsDir);
46
+ const codeHome = process.env.CODE_HOME || path.join(home, '.code');
47
+ const claudeProjectsDir = path.join(home, '.claude', 'projects');
48
+
49
+ const sources = [
50
+ { source: 'codex', sessionsDir: path.join(codexHome, 'sessions') },
51
+ { source: 'every-code', sessionsDir: path.join(codeHome, 'sessions') }
52
+ ];
53
+
54
+ const rolloutFiles = [];
55
+ const seenSessions = new Set();
56
+ for (const entry of sources) {
57
+ if (seenSessions.has(entry.sessionsDir)) continue;
58
+ seenSessions.add(entry.sessionsDir);
59
+ const files = await listRolloutFiles(entry.sessionsDir);
60
+ for (const filePath of files) {
61
+ rolloutFiles.push({ path: filePath, source: entry.source });
62
+ }
63
+ }
48
64
 
49
65
  if (progress?.enabled) {
50
66
  progress.start(`Parsing ${renderBar(0)} 0/${formatNumber(rolloutFiles.length)} files | buckets 0`);
@@ -65,6 +81,29 @@ async function cmdSync(argv) {
65
81
  }
66
82
  });
67
83
 
84
+ const claudeFiles = await listClaudeProjectFiles(claudeProjectsDir);
85
+ let claudeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
86
+ if (claudeFiles.length > 0) {
87
+ if (progress?.enabled) {
88
+ progress.start(`Parsing Claude ${renderBar(0)} 0/${formatNumber(claudeFiles.length)} files | buckets 0`);
89
+ }
90
+ claudeResult = await parseClaudeIncremental({
91
+ projectFiles: claudeFiles,
92
+ cursors,
93
+ queuePath,
94
+ onProgress: (p) => {
95
+ if (!progress?.enabled) return;
96
+ const pct = p.total > 0 ? p.index / p.total : 1;
97
+ progress.update(
98
+ `Parsing Claude ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(
99
+ p.bucketsQueued
100
+ )}`
101
+ );
102
+ },
103
+ source: 'claude'
104
+ });
105
+ }
106
+
68
107
  cursors.updatedAt = new Date().toISOString();
69
108
  await writeJson(cursorsPath, cursors);
70
109
 
@@ -190,11 +229,13 @@ async function cmdSync(argv) {
190
229
  });
191
230
 
192
231
  if (!opts.auto) {
232
+ const totalParsed = parseResult.filesProcessed + claudeResult.filesProcessed;
233
+ const totalBuckets = parseResult.bucketsQueued + claudeResult.bucketsQueued;
193
234
  process.stdout.write(
194
235
  [
195
236
  'Sync finished:',
196
- `- Parsed files: ${parseResult.filesProcessed}`,
197
- `- New 30-min buckets queued: ${parseResult.bucketsQueued}`,
237
+ `- Parsed files: ${totalParsed}`,
238
+ `- New 30-min buckets queued: ${totalBuckets}`,
198
239
  deviceToken
199
240
  ? `- Uploaded: ${uploadResult.inserted} inserted, ${uploadResult.skipped} skipped`
200
241
  : '- Uploaded: skipped (no device token)',
@@ -2,8 +2,8 @@ const os = require('node:os');
2
2
  const path = require('node:path');
3
3
  const fs = require('node:fs/promises');
4
4
 
5
- const { readJson } = require('../lib/fs');
6
- const { restoreCodexNotify } = require('../lib/codex-config');
5
+ const { restoreCodexNotify, restoreEveryCodeNotify } = require('../lib/codex-config');
6
+ const { removeClaudeHook, buildClaudeHookCommand } = require('../lib/claude-config');
7
7
 
8
8
  async function cmdUninstall(argv) {
9
9
  const opts = parseArgs(argv);
@@ -11,15 +11,38 @@ async function cmdUninstall(argv) {
11
11
  const trackerDir = path.join(home, '.vibescore', 'tracker');
12
12
  const binDir = path.join(home, '.vibescore', 'bin');
13
13
  const codexConfigPath = path.join(home, '.codex', 'config.toml');
14
+ const codeHome = process.env.CODE_HOME || path.join(home, '.code');
15
+ const codeConfigPath = path.join(codeHome, 'config.toml');
16
+ const claudeSettingsPath = path.join(home, '.claude', 'settings.json');
17
+ const notifyPath = path.join(binDir, 'notify.cjs');
14
18
  const notifyOriginalPath = path.join(trackerDir, 'codex_notify_original.json');
19
+ const codeNotifyOriginalPath = path.join(trackerDir, 'code_notify_original.json');
20
+ const codexNotifyCmd = ['/usr/bin/env', 'node', notifyPath];
21
+ const codeNotifyCmd = ['/usr/bin/env', 'node', notifyPath, '--source=every-code'];
22
+ const claudeHookCommand = buildClaudeHookCommand(notifyPath);
15
23
 
16
- await restoreCodexNotify({
17
- codexConfigPath,
18
- notifyOriginalPath
19
- });
24
+ const codexConfigExists = await isFile(codexConfigPath);
25
+ const codeConfigExists = await isFile(codeConfigPath);
26
+ const claudeConfigExists = await isFile(claudeSettingsPath);
27
+ const codexRestore = codexConfigExists
28
+ ? await restoreCodexNotify({
29
+ codexConfigPath,
30
+ notifyOriginalPath,
31
+ notifyCmd: codexNotifyCmd
32
+ })
33
+ : { restored: false, skippedReason: 'config-missing' };
34
+ const codeRestore = codeConfigExists
35
+ ? await restoreEveryCodeNotify({
36
+ codeConfigPath,
37
+ notifyOriginalPath: codeNotifyOriginalPath,
38
+ notifyCmd: codeNotifyCmd
39
+ })
40
+ : { restored: false, skippedReason: 'config-missing' };
41
+ const claudeRemove = claudeConfigExists
42
+ ? await removeClaudeHook({ settingsPath: claudeSettingsPath, hookCommand: claudeHookCommand })
43
+ : { removed: false, skippedReason: 'config-missing' };
20
44
 
21
45
  // Remove installed notify handler.
22
- const notifyPath = path.join(binDir, 'notify.cjs');
23
46
  await fs.unlink(notifyPath).catch(() => {});
24
47
 
25
48
  // Remove local app runtime (installed by init for notify-driven sync).
@@ -32,7 +55,27 @@ async function cmdUninstall(argv) {
32
55
  process.stdout.write(
33
56
  [
34
57
  'Uninstalled:',
35
- `- Codex notify restored: ${codexConfigPath}`,
58
+ codexConfigExists
59
+ ? codexRestore?.restored
60
+ ? `- Codex notify restored: ${codexConfigPath}`
61
+ : codexRestore?.skippedReason === 'no-backup-not-installed'
62
+ ? '- Codex notify: skipped (no backup; not installed)'
63
+ : '- Codex notify: no change'
64
+ : '- Codex notify: skipped (config.toml not found)',
65
+ codeConfigExists
66
+ ? codeRestore?.restored
67
+ ? `- Every Code notify restored: ${codeConfigPath}`
68
+ : codeRestore?.skippedReason === 'no-backup-not-installed'
69
+ ? '- Every Code notify: skipped (no backup; not installed)'
70
+ : '- Every Code notify: no change'
71
+ : '- Every Code notify: skipped (config.toml not found)',
72
+ claudeConfigExists
73
+ ? claudeRemove?.removed
74
+ ? `- Claude hooks removed: ${claudeSettingsPath}`
75
+ : claudeRemove?.skippedReason === 'hook-missing'
76
+ ? '- Claude hooks: no change'
77
+ : '- Claude hooks: skipped'
78
+ : '- Claude hooks: skipped (settings.json not found)',
36
79
  opts.purge ? `- Purged: ${path.join(home, '.vibescore')}` : '- Purge: skipped (use --purge)',
37
80
  ''
38
81
  ].join('\n')
@@ -50,3 +93,12 @@ function parseArgs(argv) {
50
93
  }
51
94
 
52
95
  module.exports = { cmdUninstall };
96
+
97
+ async function isFile(p) {
98
+ try {
99
+ const st = await fs.stat(p);
100
+ return st.isFile();
101
+ } catch (_e) {
102
+ return false;
103
+ }
104
+ }