@vibescore/tracker 0.0.5 → 0.0.7

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
@@ -25,9 +25,19 @@ _Real-time AI Analytics for Codex CLI_
25
25
 
26
26
  > [!TIP] > **Core Index**: Our signature metric that reflects your flow state by analyzing token consumption rates and patterns.
27
27
 
28
+ ## 🔒 Privacy-First Architecture (Stealth Protocol)
29
+
30
+ We believe your code and thoughts are your own. VibeScore is built with strict privacy pillars to ensure your data never leaves your control.
31
+
32
+ - 🛡️ **Zero Code Infiltration**: We never touch your source code or prompts. Our sniffer only extracts numeric token counts (Input, Output, Reasoning, Cached).
33
+ - 📡 **Local Aggregation**: All token consumption analysis happens on your machine. We only relay quantized 30-minute usage buckets to the cloud.
34
+ - 🔐 **Hashed Identity**: Device tokens are hashed using SHA-256 server-side. Your raw credentials never exist in our database.
35
+ - 🔦 **Full Transparency**: Audit the sync logic yourself in `src/lib/rollout.js`. We literally only capture numbers and timestamps.
36
+
28
37
  ## 🚀 Key Features
29
38
 
30
39
  - 📡 **Live Sniffer**: Real-time interception of Codex CLI pipes using low-level hooks to capture every completion event.
40
+ - 🧭 **Multi-source Ingestion**: Supports Codex CLI and Every Code (tagged as `source=every-code`) without modifying Every Code.
31
41
  - 📊 **Matrix Dashboard**: A high-performance React + Vite dashboard featuring heatmaps, trend charts, and live logs.
32
42
  - ⚡ **AI Analytics**: Deep analysis of Input/Output tokens, with dedicated tracking for Cached and Reasoning components.
33
43
  - 🔒 **Identity Core**: Robust authentication and permission management to secure your development data.
@@ -42,6 +52,8 @@ Initialize your environment with a single command:
42
52
  npx --yes @vibescore/tracker init
43
53
  ```
44
54
 
55
+ Note: If `~/.code/config.toml` exists (or `CODE_HOME`), `init` also configures Every Code `notify` automatically; it will not create the file when missing.
56
+
45
57
  ### Sync & Status
46
58
 
47
59
  ```bash
@@ -52,6 +64,11 @@ npx --yes @vibescore/tracker sync
52
64
  npx --yes @vibescore/tracker status
53
65
  ```
54
66
 
67
+ ### Sources
68
+
69
+ - Codex CLI logs: `~/.codex/sessions/**/rollout-*.jsonl` (override with `CODEX_HOME`)
70
+ - Every Code logs: `~/.code/sessions/**/rollout-*.jsonl` (override with `CODE_HOME`)
71
+
55
72
  ## 🧰 Troubleshooting
56
73
 
57
74
  ### Streak shows 0 days while totals look correct
@@ -60,9 +77,9 @@ npx --yes @vibescore/tracker status
60
77
  - If you expect a non-zero streak, clear cached auth/heatmap data and sign in again:
61
78
 
62
79
  ```js
63
- localStorage.removeItem('vibescore.dashboard.auth.v1');
80
+ localStorage.removeItem("vibescore.dashboard.auth.v1");
64
81
  Object.keys(localStorage)
65
- .filter((k) => k.startsWith('vibescore.heatmap.'))
82
+ .filter((k) => k.startsWith("vibescore.heatmap."))
66
83
  .forEach((k) => localStorage.removeItem(k));
67
84
  location.reload();
68
85
  ```
package/README.zh-CN.md CHANGED
@@ -25,9 +25,19 @@ _Codex CLI 实时 AI 分析工具_
25
25
 
26
26
  > [!TIP] > **Core Index (核心指数)**: 我们的标志性指标,通过分析 Token 消耗速率与模式,反映你的开发心流状态。
27
27
 
28
+ ## 🔒 隐私优先架构 (隐身协议)
29
+
30
+ 我们坚信你的代码和思想属于你自己。VibeScore 建立在严格的隐私支柱之上,确保你的数据始终处于受控状态。
31
+
32
+ - 🛡️ **代码零入侵**:我们绝不触碰你的源代码或 Prompt。我们的嗅探器仅提取数值化的 Token 计数(输入、输出、推理、缓存)。
33
+ - 📡 **本地聚合**:所有 Token 消耗分析均在你的机器上完成。我们仅将量化的 30 分钟使用桶(Usage Buckets)中继到云端。
34
+ - 🔐 **身份哈希**:设备令牌在服务端使用 SHA-256 进行哈希处理。你的原始凭据绝不会存在于我们的数据库中。
35
+ - 🔦 **全程透明**:你可以亲自审计 `src/lib/rollout.js` 中的同步逻辑。我们真正采集的只有数字和时间戳。
36
+
28
37
  ## 🚀 核心功能
29
38
 
30
39
  - 📡 **Live Sniffer (实时嗅探)**: 实时监听 Codex CLI 管道,通过底层 Hook 捕获每一次补全事件。
40
+ - 🧭 **多来源采集**:支持 Codex CLI 与 Every Code(标记为 `source=every-code`),无需修改 Every Code 客户端。
31
41
  - 📊 **Matrix Dashboard (矩阵控制台)**: 基于 React + Vite 的高性能仪表盘,具备热力图、趋势图与实时日志。
32
42
  - ⚡ **AI Analytics (AI 分析)**: 深度分析 Input/Output Token,支持缓存 (Cached) 与推理 (Reasoning) 部分的分离监控。
33
43
  - 🔒 **Identity Core (身份核心)**: 完备的身份验证与权限管理,保护你的开发数据资产。
@@ -42,6 +52,8 @@ _Codex CLI 实时 AI 分析工具_
42
52
  npx --yes @vibescore/tracker init
43
53
  ```
44
54
 
55
+ 说明:若存在 `~/.code/config.toml`(或 `CODE_HOME`),`init` 会自动配置 Every Code 的 `notify`;缺失时不会创建该文件。
56
+
45
57
  ### 同步与状态查看
46
58
 
47
59
  ```bash
@@ -52,6 +64,11 @@ npx --yes @vibescore/tracker sync
52
64
  npx --yes @vibescore/tracker status
53
65
  ```
54
66
 
67
+ ### 日志来源
68
+
69
+ - Codex CLI 日志:`~/.codex/sessions/**/rollout-*.jsonl`(可用 `CODEX_HOME` 覆盖)
70
+ - Every Code 日志:`~/.code/sessions/**/rollout-*.jsonl`(可用 `CODE_HOME` 覆盖)
71
+
55
72
  ## 🧰 常见问题
56
73
 
57
74
  ### Streak 显示 0 天但总量正常
@@ -60,9 +77,9 @@ npx --yes @vibescore/tracker status
60
77
  - 如果你确认应该有 streak,请清理本地缓存并重新登录:
61
78
 
62
79
  ```js
63
- localStorage.removeItem('vibescore.dashboard.auth.v1');
80
+ localStorage.removeItem("vibescore.dashboard.auth.v1");
64
81
  Object.keys(localStorage)
65
- .filter((k) => k.startsWith('vibescore.heatmap.'))
82
+ .filter((k) => k.startsWith("vibescore.heatmap."))
66
83
  .forEach((k) => localStorage.removeItem(k));
67
84
  location.reload();
68
85
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibescore/tracker",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
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')
@@ -4,7 +4,12 @@ const fs = require('node:fs/promises');
4
4
 
5
5
  const { ensureDir, writeFileAtomic, readJson, writeJson, chmod600IfPossible } = require('../lib/fs');
6
6
  const { prompt, promptHidden } = require('../lib/prompt');
7
- const { upsertCodexNotify, loadCodexNotifyOriginal } = require('../lib/codex-config');
7
+ const {
8
+ upsertCodexNotify,
9
+ loadCodexNotifyOriginal,
10
+ upsertEveryCodeNotify,
11
+ loadEveryCodeNotifyOriginal
12
+ } = require('../lib/codex-config');
8
13
  const { beginBrowserAuth } = require('../lib/browser-auth');
9
14
  const { issueDeviceTokenWithPassword, issueDeviceTokenWithAccessToken } = require('../lib/insforge');
10
15
  const { cmdSync } = require('./sync');
@@ -84,6 +89,8 @@ async function cmdInit(argv) {
84
89
 
85
90
  // Configure Codex notify hook.
86
91
  const codexConfigPath = path.join(home, '.codex', 'config.toml');
92
+ const codeHome = process.env.CODE_HOME || path.join(home, '.code');
93
+ const codeConfigPath = path.join(codeHome, 'config.toml');
87
94
  const notifyCmd = ['/usr/bin/env', 'node', notifyPath];
88
95
  const result = await upsertCodexNotify({
89
96
  codexConfigPath,
@@ -92,6 +99,19 @@ async function cmdInit(argv) {
92
99
  });
93
100
 
94
101
  const chained = await loadCodexNotifyOriginal(notifyOriginalPath);
102
+ const codeNotifyOriginalPath = path.join(trackerDir, 'code_notify_original.json');
103
+ const codeConfigExists = await isFile(codeConfigPath);
104
+ let codeResult = null;
105
+ let codeChained = null;
106
+ if (codeConfigExists) {
107
+ const codeNotifyCmd = ['/usr/bin/env', 'node', notifyPath, '--source=every-code'];
108
+ codeResult = await upsertEveryCodeNotify({
109
+ codeConfigPath,
110
+ notifyCmd: codeNotifyCmd,
111
+ notifyOriginalPath: codeNotifyOriginalPath
112
+ });
113
+ codeChained = await loadEveryCodeNotifyOriginal(codeNotifyOriginalPath);
114
+ }
95
115
 
96
116
  process.stdout.write(
97
117
  [
@@ -101,6 +121,17 @@ async function cmdInit(argv) {
101
121
  `- Codex config: ${codexConfigPath}`,
102
122
  result.changed ? '- Codex notify: updated' : '- Codex notify: already set',
103
123
  chained ? '- Codex notify: chained (original preserved)' : '- Codex notify: no original',
124
+ codeConfigExists ? `- Every Code config: ${codeConfigPath}` : '- Every Code notify: skipped (config.toml not found)',
125
+ codeConfigExists && codeResult
126
+ ? codeResult.changed
127
+ ? '- Every Code notify: updated'
128
+ : '- Every Code notify: already set'
129
+ : null,
130
+ codeConfigExists
131
+ ? codeChained
132
+ ? '- Every Code notify: chained (original preserved)'
133
+ : '- Every Code notify: no original'
134
+ : null,
104
135
  deviceToken ? `- Device token: stored (${maskSecret(deviceToken)})` : '- Device token: not configured (set VIBESCORE_DEVICE_TOKEN and re-run init)',
105
136
  ''
106
137
  ].join('\n')
@@ -160,12 +191,32 @@ const os = require('node:os');
160
191
  const path = require('node:path');
161
192
  const cp = require('node:child_process');
162
193
 
163
- const payload = process.argv[2] || '';
194
+ const rawArgs = process.argv.slice(2);
195
+ let source = 'codex';
196
+ const payloadArgs = [];
197
+ for (let i = 0; i < rawArgs.length; i++) {
198
+ const arg = rawArgs[i];
199
+ if (arg === '--source') {
200
+ source = rawArgs[i + 1] || source;
201
+ i += 1;
202
+ continue;
203
+ }
204
+ if (arg.startsWith('--source=')) {
205
+ source = arg.slice('--source='.length) || source;
206
+ continue;
207
+ }
208
+ payloadArgs.push(arg);
209
+ }
210
+
164
211
  const trackerDir = ${JSON.stringify(trackerDir)};
165
212
  const signalPath = ${JSON.stringify(queueSignalPath)};
166
- const originalPath = ${JSON.stringify(originalPath)};
213
+ const codexOriginalPath = ${JSON.stringify(originalPath)};
214
+ const codeOriginalPath = ${JSON.stringify(path.join(trackerDir, 'code_notify_original.json'))};
167
215
  const trackerBinPath = ${JSON.stringify(trackerBinPath)};
216
+ const depsMarkerPath = path.join(trackerDir, 'app', 'node_modules', '@insforge', 'sdk', 'package.json');
168
217
  const fallbackPkg = ${JSON.stringify(fallbackPkg)};
218
+ const selfPath = path.resolve(__filename);
219
+ const home = os.homedir();
169
220
 
170
221
  try {
171
222
  fs.mkdirSync(trackerDir, { recursive: true });
@@ -180,7 +231,9 @@ try {
180
231
  try { last = Number(fs.readFileSync(throttlePath, 'utf8')) || 0; } catch (_) {}
181
232
  if (now - last > 20_000) {
182
233
  try { fs.writeFileSync(throttlePath, String(now), 'utf8'); } catch (_) {}
183
- if (fs.existsSync(trackerBinPath)) {
234
+ const hasLocalRuntime = fs.existsSync(trackerBinPath);
235
+ const hasLocalDeps = fs.existsSync(depsMarkerPath);
236
+ if (hasLocalRuntime && hasLocalDeps) {
184
237
  spawnDetached([process.execPath, trackerBinPath, 'sync', '--auto', '--from-notify']);
185
238
  } else {
186
239
  spawnDetached(['npx', '--yes', fallbackPkg, 'sync', '--auto', '--from-notify']);
@@ -188,13 +241,14 @@ try {
188
241
  }
189
242
  } catch (_) {}
190
243
 
191
- // Chain the original Codex notify if present.
244
+ // Chain the original notify if present.
192
245
  try {
246
+ const originalPath = source === 'every-code' ? codeOriginalPath : codexOriginalPath;
193
247
  const original = JSON.parse(fs.readFileSync(originalPath, 'utf8'));
194
248
  const cmd = Array.isArray(original?.notify) ? original.notify : null;
195
- if (cmd && cmd.length > 0) {
249
+ if (cmd && cmd.length > 0 && !isSelfNotify(cmd)) {
196
250
  const args = cmd.slice(1);
197
- args.push(payload);
251
+ if (payloadArgs.length > 0) args.push(...payloadArgs);
198
252
  spawnDetached([cmd[0], ...args]);
199
253
  }
200
254
  } catch (_) {}
@@ -211,6 +265,22 @@ function spawnDetached(argv) {
211
265
  child.unref();
212
266
  } catch (_) {}
213
267
  }
268
+
269
+ function resolveMaybeHome(p) {
270
+ if (typeof p !== 'string') return null;
271
+ if (p.startsWith('~/')) return path.join(home, p.slice(2));
272
+ return path.resolve(p);
273
+ }
274
+
275
+ function isSelfNotify(cmd) {
276
+ for (const part of cmd) {
277
+ if (typeof part !== 'string') continue;
278
+ if (!part.includes('notify.cjs')) continue;
279
+ const resolved = resolveMaybeHome(part);
280
+ if (resolved && resolved === selfPath) return true;
281
+ }
282
+ return false;
283
+ }
214
284
  `;
215
285
  }
216
286
 
@@ -245,15 +315,26 @@ async function checkUrlReachable(url) {
245
315
  }
246
316
  }
247
317
 
318
+ async function isFile(p) {
319
+ try {
320
+ const st = await fs.stat(p);
321
+ return st.isFile();
322
+ } catch (_e) {
323
+ return false;
324
+ }
325
+ }
326
+
248
327
  async function installLocalTrackerApp({ appDir }) {
249
328
  // Copy the current package's runtime (bin + src) into ~/.vibescore so notify can run sync without npx.
250
329
  const packageRoot = path.resolve(__dirname, '../..');
251
330
  const srcFrom = path.join(packageRoot, 'src');
252
331
  const binFrom = path.join(packageRoot, 'bin', 'tracker.js');
332
+ const nodeModulesFrom = path.join(packageRoot, 'node_modules');
253
333
 
254
334
  const srcTo = path.join(appDir, 'src');
255
335
  const binToDir = path.join(appDir, 'bin');
256
336
  const binTo = path.join(binToDir, 'tracker.js');
337
+ const nodeModulesTo = path.join(appDir, 'node_modules');
257
338
 
258
339
  await fs.rm(appDir, { recursive: true, force: true }).catch(() => {});
259
340
  await ensureDir(appDir);
@@ -261,4 +342,20 @@ async function installLocalTrackerApp({ appDir }) {
261
342
  await ensureDir(binToDir);
262
343
  await fs.copyFile(binFrom, binTo);
263
344
  await fs.chmod(binTo, 0o755).catch(() => {});
345
+ await copyRuntimeDependencies({ from: nodeModulesFrom, to: nodeModulesTo });
346
+ }
347
+
348
+ async function copyRuntimeDependencies({ from, to }) {
349
+ try {
350
+ const st = await fs.stat(from);
351
+ if (!st.isDirectory()) return;
352
+ } catch (_e) {
353
+ return;
354
+ }
355
+
356
+ try {
357
+ await fs.cp(from, to, { recursive: true });
358
+ } catch (_e) {
359
+ // Best-effort: missing dependencies will fall back to npx at notify time.
360
+ }
264
361
  }
@@ -3,7 +3,7 @@ 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
7
  const { normalizeState: normalizeUploadState } = require('../lib/upload-throttle');
8
8
  const { collectTrackerDiagnostics } = require('../lib/diagnostics');
9
9
 
@@ -27,6 +27,8 @@ async function cmdStatus(argv = []) {
27
27
  const autoRetryPath = path.join(trackerDir, 'auto.retry.json');
28
28
  const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
29
29
  const codexConfigPath = path.join(codexHome, 'config.toml');
30
+ const codeHome = process.env.CODE_HOME || path.join(home, '.code');
31
+ const codeConfigPath = path.join(codeHome, 'config.toml');
30
32
 
31
33
  const config = await readJson(configPath);
32
34
  const cursors = await readJson(cursorsPath);
@@ -42,6 +44,8 @@ async function cmdStatus(argv = []) {
42
44
 
43
45
  const codexNotify = await readCodexNotify(codexConfigPath);
44
46
  const notifyConfigured = Array.isArray(codexNotify) && codexNotify.length > 0;
47
+ const everyCodeNotify = await readEveryCodeNotify(codeConfigPath);
48
+ const everyCodeConfigured = Array.isArray(everyCodeNotify) && everyCodeNotify.length > 0;
45
49
 
46
50
  const lastUpload = uploadThrottle.lastSuccessMs
47
51
  ? parseEpochMsToIso(uploadThrottle.lastSuccessMs)
@@ -75,6 +79,7 @@ async function cmdStatus(argv = []) {
75
79
  lastUploadError ? `- Last upload error: ${lastUploadError}` : null,
76
80
  autoRetryLine,
77
81
  `- Codex notify: ${notifyConfigured ? JSON.stringify(codexNotify) : 'unset'}`,
82
+ `- Every Code notify: ${everyCodeConfigured ? JSON.stringify(everyCodeNotify) : 'unset'}`,
78
83
  ''
79
84
  ]
80
85
  .filter(Boolean)
@@ -43,8 +43,23 @@ 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
+
48
+ const sources = [
49
+ { source: 'codex', sessionsDir: path.join(codexHome, 'sessions') },
50
+ { source: 'every-code', sessionsDir: path.join(codeHome, 'sessions') }
51
+ ];
52
+
53
+ const rolloutFiles = [];
54
+ const seenSessions = new Set();
55
+ for (const entry of sources) {
56
+ if (seenSessions.has(entry.sessionsDir)) continue;
57
+ seenSessions.add(entry.sessionsDir);
58
+ const files = await listRolloutFiles(entry.sessionsDir);
59
+ for (const filePath of files) {
60
+ rolloutFiles.push({ path: filePath, source: entry.source });
61
+ }
62
+ }
48
63
 
49
64
  if (progress?.enabled) {
50
65
  progress.start(`Parsing ${renderBar(0)} 0/${formatNumber(rolloutFiles.length)} files | buckets 0`);
@@ -2,8 +2,7 @@ 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');
7
6
 
8
7
  async function cmdUninstall(argv) {
9
8
  const opts = parseArgs(argv);
@@ -11,15 +10,32 @@ async function cmdUninstall(argv) {
11
10
  const trackerDir = path.join(home, '.vibescore', 'tracker');
12
11
  const binDir = path.join(home, '.vibescore', 'bin');
13
12
  const codexConfigPath = path.join(home, '.codex', 'config.toml');
13
+ const codeHome = process.env.CODE_HOME || path.join(home, '.code');
14
+ const codeConfigPath = path.join(codeHome, 'config.toml');
15
+ const notifyPath = path.join(binDir, 'notify.cjs');
14
16
  const notifyOriginalPath = path.join(trackerDir, 'codex_notify_original.json');
17
+ const codeNotifyOriginalPath = path.join(trackerDir, 'code_notify_original.json');
18
+ const codexNotifyCmd = ['/usr/bin/env', 'node', notifyPath];
19
+ const codeNotifyCmd = ['/usr/bin/env', 'node', notifyPath, '--source=every-code'];
15
20
 
16
- await restoreCodexNotify({
17
- codexConfigPath,
18
- notifyOriginalPath
19
- });
21
+ const codexConfigExists = await isFile(codexConfigPath);
22
+ const codeConfigExists = await isFile(codeConfigPath);
23
+ const codexRestore = codexConfigExists
24
+ ? await restoreCodexNotify({
25
+ codexConfigPath,
26
+ notifyOriginalPath,
27
+ notifyCmd: codexNotifyCmd
28
+ })
29
+ : { restored: false, skippedReason: 'config-missing' };
30
+ const codeRestore = codeConfigExists
31
+ ? await restoreEveryCodeNotify({
32
+ codeConfigPath,
33
+ notifyOriginalPath: codeNotifyOriginalPath,
34
+ notifyCmd: codeNotifyCmd
35
+ })
36
+ : { restored: false, skippedReason: 'config-missing' };
20
37
 
21
38
  // Remove installed notify handler.
22
- const notifyPath = path.join(binDir, 'notify.cjs');
23
39
  await fs.unlink(notifyPath).catch(() => {});
24
40
 
25
41
  // Remove local app runtime (installed by init for notify-driven sync).
@@ -32,7 +48,20 @@ async function cmdUninstall(argv) {
32
48
  process.stdout.write(
33
49
  [
34
50
  'Uninstalled:',
35
- `- Codex notify restored: ${codexConfigPath}`,
51
+ codexConfigExists
52
+ ? codexRestore?.restored
53
+ ? `- Codex notify restored: ${codexConfigPath}`
54
+ : codexRestore?.skippedReason === 'no-backup-not-installed'
55
+ ? '- Codex notify: skipped (no backup; not installed)'
56
+ : '- Codex notify: no change'
57
+ : '- Codex notify: skipped (config.toml not found)',
58
+ codeConfigExists
59
+ ? codeRestore?.restored
60
+ ? `- Every Code notify restored: ${codeConfigPath}`
61
+ : codeRestore?.skippedReason === 'no-backup-not-installed'
62
+ ? '- Every Code notify: skipped (no backup; not installed)'
63
+ : '- Every Code notify: no change'
64
+ : '- Every Code notify: skipped (config.toml not found)',
36
65
  opts.purge ? `- Purged: ${path.join(home, '.vibescore')}` : '- Purge: skipped (use --purge)',
37
66
  ''
38
67
  ].join('\n')
@@ -50,3 +79,12 @@ function parseArgs(argv) {
50
79
  }
51
80
 
52
81
  module.exports = { cmdUninstall };
82
+
83
+ async function isFile(p) {
84
+ try {
85
+ const st = await fs.stat(p);
86
+ return st.isFile();
87
+ } catch (_e) {
88
+ return false;
89
+ }
90
+ }
@@ -3,10 +3,11 @@ const path = require('node:path');
3
3
 
4
4
  const { ensureDir, readJson, writeJson } = require('./fs');
5
5
 
6
- async function upsertCodexNotify({ codexConfigPath, notifyCmd, notifyOriginalPath }) {
7
- const originalText = await fs.readFile(codexConfigPath, 'utf8').catch(() => null);
6
+ async function upsertNotify({ configPath, notifyCmd, notifyOriginalPath, configLabel }) {
7
+ const originalText = await fs.readFile(configPath, 'utf8').catch(() => null);
8
8
  if (originalText == null) {
9
- throw new Error(`Codex config not found: ${codexConfigPath}`);
9
+ const label = typeof configLabel === 'string' && configLabel.length > 0 ? configLabel : 'Config';
10
+ throw new Error(`${label} not found: ${configPath}`);
10
11
  }
11
12
 
12
13
  const existingNotify = extractNotify(originalText);
@@ -23,39 +24,97 @@ async function upsertCodexNotify({ codexConfigPath, notifyCmd, notifyOriginalPat
23
24
  }
24
25
 
25
26
  const updated = setNotify(originalText, notifyCmd);
26
- const backupPath = `${codexConfigPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
27
- await fs.copyFile(codexConfigPath, backupPath);
28
- await fs.writeFile(codexConfigPath, updated, 'utf8');
27
+ const backupPath = `${configPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
28
+ await fs.copyFile(configPath, backupPath);
29
+ await fs.writeFile(configPath, updated, 'utf8');
29
30
  return { changed: true, backupPath };
30
31
  }
31
32
 
32
33
  return { changed: false, backupPath: null };
33
34
  }
34
35
 
35
- async function restoreCodexNotify({ codexConfigPath, notifyOriginalPath }) {
36
- const text = await fs.readFile(codexConfigPath, 'utf8').catch(() => null);
37
- if (text == null) return;
36
+ async function restoreNotify({ configPath, notifyOriginalPath, expectedNotify }) {
37
+ const text = await fs.readFile(configPath, 'utf8').catch(() => null);
38
+ if (text == null) return { restored: false, skippedReason: 'config-missing' };
38
39
 
39
40
  const original = await readJson(notifyOriginalPath);
40
41
  const originalNotify = Array.isArray(original?.notify) ? original.notify : null;
42
+ const currentNotify = extractNotify(text);
43
+
44
+ if (!originalNotify && expectedNotify && !arraysEqual(currentNotify, expectedNotify)) {
45
+ return { restored: false, skippedReason: 'no-backup-not-installed' };
46
+ }
41
47
 
42
48
  const updated = originalNotify ? setNotify(text, originalNotify) : removeNotify(text);
43
- const backupPath = `${codexConfigPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
44
- await fs.copyFile(codexConfigPath, backupPath).catch(() => {});
45
- await fs.writeFile(codexConfigPath, updated, 'utf8');
49
+ if (updated === text) return { restored: false, skippedReason: 'no-change' };
50
+
51
+ const backupPath = `${configPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
52
+ await fs.copyFile(configPath, backupPath).catch(() => {});
53
+ await fs.writeFile(configPath, updated, 'utf8');
54
+ return { restored: true, skippedReason: null };
46
55
  }
47
56
 
48
- async function loadCodexNotifyOriginal(notifyOriginalPath) {
57
+ async function loadNotifyOriginal(notifyOriginalPath) {
49
58
  const original = await readJson(notifyOriginalPath);
50
59
  return Array.isArray(original?.notify) ? original.notify : null;
51
60
  }
52
61
 
53
- async function readCodexNotify(codexConfigPath) {
54
- const text = await fs.readFile(codexConfigPath, 'utf8').catch(() => null);
62
+ async function readNotify(configPath) {
63
+ const text = await fs.readFile(configPath, 'utf8').catch(() => null);
55
64
  if (text == null) return null;
56
65
  return extractNotify(text);
57
66
  }
58
67
 
68
+ async function upsertCodexNotify({ codexConfigPath, notifyCmd, notifyOriginalPath }) {
69
+ return upsertNotify({
70
+ configPath: codexConfigPath,
71
+ notifyCmd,
72
+ notifyOriginalPath,
73
+ configLabel: 'Codex config'
74
+ });
75
+ }
76
+
77
+ async function restoreCodexNotify({ codexConfigPath, notifyOriginalPath, notifyCmd }) {
78
+ return restoreNotify({
79
+ configPath: codexConfigPath,
80
+ notifyOriginalPath,
81
+ expectedNotify: notifyCmd
82
+ });
83
+ }
84
+
85
+ async function loadCodexNotifyOriginal(notifyOriginalPath) {
86
+ return loadNotifyOriginal(notifyOriginalPath);
87
+ }
88
+
89
+ async function readCodexNotify(codexConfigPath) {
90
+ return readNotify(codexConfigPath);
91
+ }
92
+
93
+ async function upsertEveryCodeNotify({ codeConfigPath, notifyCmd, notifyOriginalPath }) {
94
+ return upsertNotify({
95
+ configPath: codeConfigPath,
96
+ notifyCmd,
97
+ notifyOriginalPath,
98
+ configLabel: 'Every Code config'
99
+ });
100
+ }
101
+
102
+ async function restoreEveryCodeNotify({ codeConfigPath, notifyOriginalPath, notifyCmd }) {
103
+ return restoreNotify({
104
+ configPath: codeConfigPath,
105
+ notifyOriginalPath,
106
+ expectedNotify: notifyCmd
107
+ });
108
+ }
109
+
110
+ async function loadEveryCodeNotifyOriginal(notifyOriginalPath) {
111
+ return loadNotifyOriginal(notifyOriginalPath);
112
+ }
113
+
114
+ async function readEveryCodeNotify(codeConfigPath) {
115
+ return readNotify(codeConfigPath);
116
+ }
117
+
59
118
  function extractNotify(text) {
60
119
  // Heuristic parse: find a line that starts with "notify =".
61
120
  const lines = text.split(/\r?\n/);
@@ -150,8 +209,16 @@ function arraysEqual(a, b) {
150
209
  }
151
210
 
152
211
  module.exports = {
212
+ upsertNotify,
213
+ restoreNotify,
214
+ loadNotifyOriginal,
215
+ readNotify,
153
216
  upsertCodexNotify,
154
217
  restoreCodexNotify,
155
218
  loadCodexNotifyOriginal,
156
- readCodexNotify
219
+ readCodexNotify,
220
+ upsertEveryCodeNotify,
221
+ restoreEveryCodeNotify,
222
+ loadEveryCodeNotifyOriginal,
223
+ readEveryCodeNotify
157
224
  };
@@ -3,12 +3,13 @@ const path = require('node:path');
3
3
  const fs = require('node:fs/promises');
4
4
 
5
5
  const { readJson } = require('./fs');
6
- const { readCodexNotify } = require('./codex-config');
6
+ const { readCodexNotify, readEveryCodeNotify } = require('./codex-config');
7
7
  const { normalizeState: normalizeUploadState } = require('./upload-throttle');
8
8
 
9
9
  async function collectTrackerDiagnostics({
10
10
  home = os.homedir(),
11
- codexHome = process.env.CODEX_HOME || path.join(home, '.codex')
11
+ codexHome = process.env.CODEX_HOME || path.join(home, '.codex'),
12
+ codeHome = process.env.CODE_HOME || path.join(home, '.code')
12
13
  } = {}) {
13
14
  const trackerDir = path.join(home, '.vibescore', 'tracker');
14
15
  const configPath = path.join(trackerDir, 'config.json');
@@ -20,6 +21,7 @@ async function collectTrackerDiagnostics({
20
21
  const uploadThrottlePath = path.join(trackerDir, 'upload.throttle.json');
21
22
  const autoRetryPath = path.join(trackerDir, 'auto.retry.json');
22
23
  const codexConfigPath = path.join(codexHome, 'config.toml');
24
+ const codeConfigPath = path.join(codeHome, 'config.toml');
23
25
 
24
26
  const config = await readJson(configPath);
25
27
  const cursors = await readJson(cursorsPath);
@@ -37,6 +39,9 @@ async function collectTrackerDiagnostics({
37
39
  const codexNotifyRaw = await readCodexNotify(codexConfigPath);
38
40
  const notifyConfigured = Array.isArray(codexNotifyRaw) && codexNotifyRaw.length > 0;
39
41
  const codexNotify = notifyConfigured ? codexNotifyRaw.map((v) => redactValue(v, home)) : null;
42
+ const everyCodeNotifyRaw = await readEveryCodeNotify(codeConfigPath);
43
+ const everyCodeConfigured = Array.isArray(everyCodeNotifyRaw) && everyCodeNotifyRaw.length > 0;
44
+ const everyCodeNotify = everyCodeConfigured ? everyCodeNotifyRaw.map((v) => redactValue(v, home)) : null;
40
45
 
41
46
  const lastSuccessAt = uploadThrottle.lastSuccessMs ? new Date(uploadThrottle.lastSuccessMs).toISOString() : null;
42
47
  const autoRetryAt = parseEpochMsToIso(autoRetry?.retryAtMs);
@@ -53,7 +58,9 @@ async function collectTrackerDiagnostics({
53
58
  paths: {
54
59
  tracker_dir: redactValue(trackerDir, home),
55
60
  codex_home: redactValue(codexHome, home),
56
- codex_config: redactValue(codexConfigPath, home)
61
+ codex_config: redactValue(codexConfigPath, home),
62
+ code_home: redactValue(codeHome, home),
63
+ code_config: redactValue(codeConfigPath, home)
57
64
  },
58
65
  config: {
59
66
  base_url: typeof config?.baseUrl === 'string' ? config.baseUrl : null,
@@ -75,7 +82,9 @@ async function collectTrackerDiagnostics({
75
82
  last_notify: lastNotify,
76
83
  last_notify_triggered_sync: lastNotifySpawn,
77
84
  codex_notify_configured: notifyConfigured,
78
- codex_notify: codexNotify
85
+ codex_notify: codexNotify,
86
+ every_code_notify_configured: everyCodeConfigured,
87
+ every_code_notify: everyCodeNotify
79
88
  },
80
89
  upload: {
81
90
  last_success_at: lastSuccessAt,
@@ -5,6 +5,9 @@ const readline = require('node:readline');
5
5
 
6
6
  const { ensureDir } = require('./fs');
7
7
 
8
+ const DEFAULT_SOURCE = 'codex';
9
+ const SOURCE_SEPARATOR = '|';
10
+
8
11
  async function listRolloutFiles(sessionsDir) {
9
12
  const out = [];
10
13
  const years = await safeReadDir(sessionsDir);
@@ -33,7 +36,7 @@ async function listRolloutFiles(sessionsDir) {
33
36
  return out;
34
37
  }
35
38
 
36
- async function parseRolloutIncremental({ rolloutFiles, cursors, queuePath, onProgress }) {
39
+ async function parseRolloutIncremental({ rolloutFiles, cursors, queuePath, onProgress, source }) {
37
40
  await ensureDir(path.dirname(queuePath));
38
41
  let filesProcessed = 0;
39
42
  let eventsAggregated = 0;
@@ -42,13 +45,18 @@ async function parseRolloutIncremental({ rolloutFiles, cursors, queuePath, onPro
42
45
  const totalFiles = Array.isArray(rolloutFiles) ? rolloutFiles.length : 0;
43
46
  const hourlyState = normalizeHourlyState(cursors?.hourly);
44
47
  const touchedBuckets = new Set();
48
+ const defaultSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
45
49
 
46
50
  if (!cursors.files || typeof cursors.files !== 'object') {
47
51
  cursors.files = {};
48
52
  }
49
53
 
50
54
  for (let idx = 0; idx < rolloutFiles.length; idx++) {
51
- const filePath = rolloutFiles[idx];
55
+ const entry = rolloutFiles[idx];
56
+ const filePath = typeof entry === 'string' ? entry : entry?.path;
57
+ if (!filePath) continue;
58
+ const fileSource =
59
+ typeof entry === 'string' ? defaultSource : normalizeSourceInput(entry?.source) || defaultSource;
52
60
  const st = await fs.stat(filePath).catch(() => null);
53
61
  if (!st || !st.isFile()) continue;
54
62
 
@@ -65,7 +73,8 @@ async function parseRolloutIncremental({ rolloutFiles, cursors, queuePath, onPro
65
73
  lastTotal,
66
74
  lastModel,
67
75
  hourlyState,
68
- touchedBuckets
76
+ touchedBuckets,
77
+ source: fileSource
69
78
  });
70
79
 
71
80
  cursors.files[key] = {
@@ -98,7 +107,15 @@ async function parseRolloutIncremental({ rolloutFiles, cursors, queuePath, onPro
98
107
  return { filesProcessed, eventsAggregated, bucketsQueued };
99
108
  }
100
109
 
101
- async function parseRolloutFile({ filePath, startOffset, lastTotal, lastModel, hourlyState, touchedBuckets }) {
110
+ async function parseRolloutFile({
111
+ filePath,
112
+ startOffset,
113
+ lastTotal,
114
+ lastModel,
115
+ hourlyState,
116
+ touchedBuckets,
117
+ source
118
+ }) {
102
119
  const st = await fs.stat(filePath);
103
120
  const endOffset = st.size;
104
121
  if (startOffset >= endOffset) {
@@ -130,13 +147,13 @@ async function parseRolloutFile({ filePath, startOffset, lastTotal, lastModel, h
130
147
  continue;
131
148
  }
132
149
 
133
- const payload = obj?.payload;
134
- if (!payload || payload.type !== 'token_count') continue;
150
+ const token = extractTokenCount(obj);
151
+ if (!token) continue;
135
152
 
136
- const info = payload.info;
153
+ const info = token.info;
137
154
  if (!info || typeof info !== 'object') continue;
138
155
 
139
- const tokenTimestamp = typeof obj.timestamp === 'string' ? obj.timestamp : null;
156
+ const tokenTimestamp = typeof token.timestamp === 'string' ? token.timestamp : null;
140
157
  if (!tokenTimestamp) continue;
141
158
 
142
159
  const lastUsage = info.last_token_usage;
@@ -152,9 +169,9 @@ async function parseRolloutFile({ filePath, startOffset, lastTotal, lastModel, h
152
169
  const bucketStart = toUtcHalfHourStart(tokenTimestamp);
153
170
  if (!bucketStart) continue;
154
171
 
155
- const bucket = getHourlyBucket(hourlyState, bucketStart);
172
+ const bucket = getHourlyBucket(hourlyState, source, bucketStart);
156
173
  addTotals(bucket.totals, delta);
157
- touchedBuckets.add(bucketStart);
174
+ touchedBuckets.add(bucketKey(source, bucketStart));
158
175
  eventsAggregated += 1;
159
176
  }
160
177
 
@@ -166,13 +183,17 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
166
183
 
167
184
  const toAppend = [];
168
185
  for (const bucketStart of touchedBuckets) {
169
- const bucket = hourlyState.buckets[bucketStart];
186
+ const parsedKey = parseBucketKey(bucketStart);
187
+ const source = parsedKey.source || DEFAULT_SOURCE;
188
+ const hourStart = parsedKey.hourStart;
189
+ const bucket = hourlyState.buckets[bucketKey(source, hourStart)] || hourlyState.buckets[bucketStart];
170
190
  if (!bucket || !bucket.totals) continue;
171
191
  const key = totalsKey(bucket.totals);
172
192
  if (bucket.queuedKey === key) continue;
173
193
  toAppend.push(
174
194
  JSON.stringify({
175
- hour_start: bucketStart,
195
+ source,
196
+ hour_start: hourStart,
176
197
  input_tokens: bucket.totals.input_tokens,
177
198
  cached_input_tokens: bucket.totals.cached_input_tokens,
178
199
  output_tokens: bucket.totals.output_tokens,
@@ -192,7 +213,15 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
192
213
 
193
214
  function normalizeHourlyState(raw) {
194
215
  const state = raw && typeof raw === 'object' ? raw : {};
195
- const buckets = state.buckets && typeof state.buckets === 'object' ? state.buckets : {};
216
+ const rawBuckets = state.buckets && typeof state.buckets === 'object' ? state.buckets : {};
217
+ const buckets = {};
218
+ for (const [key, value] of Object.entries(rawBuckets)) {
219
+ if (key.includes(SOURCE_SEPARATOR)) {
220
+ buckets[key] = value;
221
+ continue;
222
+ }
223
+ buckets[bucketKey(DEFAULT_SOURCE, key)] = value;
224
+ }
196
225
  return {
197
226
  version: 1,
198
227
  buckets,
@@ -200,12 +229,14 @@ function normalizeHourlyState(raw) {
200
229
  };
201
230
  }
202
231
 
203
- function getHourlyBucket(state, hourStart) {
232
+ function getHourlyBucket(state, source, hourStart) {
204
233
  const buckets = state.buckets;
205
- let bucket = buckets[hourStart];
234
+ const normalizedSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
235
+ const key = bucketKey(normalizedSource, hourStart);
236
+ let bucket = buckets[key];
206
237
  if (!bucket || typeof bucket !== 'object') {
207
238
  bucket = { totals: initTotals(), queuedKey: null };
208
- buckets[hourStart] = bucket;
239
+ buckets[key] = bucket;
209
240
  return bucket;
210
241
  }
211
242
 
@@ -267,6 +298,37 @@ function toUtcHalfHourStart(ts) {
267
298
  return bucketStart.toISOString();
268
299
  }
269
300
 
301
+ function bucketKey(source, hourStart) {
302
+ const safeSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
303
+ return `${safeSource}${SOURCE_SEPARATOR}${hourStart}`;
304
+ }
305
+
306
+ function parseBucketKey(key) {
307
+ if (typeof key !== 'string') return { source: DEFAULT_SOURCE, hourStart: '' };
308
+ const idx = key.indexOf(SOURCE_SEPARATOR);
309
+ if (idx <= 0) return { source: DEFAULT_SOURCE, hourStart: key };
310
+ return { source: key.slice(0, idx), hourStart: key.slice(idx + 1) };
311
+ }
312
+
313
+ function normalizeSourceInput(value) {
314
+ if (typeof value !== 'string') return null;
315
+ const trimmed = value.trim().toLowerCase();
316
+ return trimmed.length > 0 ? trimmed : null;
317
+ }
318
+
319
+ function extractTokenCount(obj) {
320
+ const payload = obj?.payload;
321
+ if (!payload) return null;
322
+ if (payload.type === 'token_count') {
323
+ return { info: payload.info, timestamp: obj?.timestamp || null };
324
+ }
325
+ const msg = payload.msg;
326
+ if (msg && msg.type === 'token_count') {
327
+ return { info: msg.info, timestamp: obj?.timestamp || null };
328
+ }
329
+ return null;
330
+ }
331
+
270
332
  function pickDelta(lastUsage, totalUsage, prevTotals) {
271
333
  const hasLast = isNonEmptyObject(lastUsage);
272
334
  const hasTotal = isNonEmptyObject(totalUsage);
@@ -5,6 +5,9 @@ const readline = require('node:readline');
5
5
  const { ensureDir, readJson, writeJson } = require('./fs');
6
6
  const { ingestHourly } = require('./vibescore-api');
7
7
 
8
+ const DEFAULT_SOURCE = 'codex';
9
+ const SOURCE_SEPARATOR = '|';
10
+
8
11
  async function drainQueueToCloud({ baseUrl, deviceToken, queuePath, queueStatePath, maxBatches, batchSize, onProgress }) {
9
12
  await ensureDir(require('node:path').dirname(queueStatePath));
10
13
 
@@ -70,7 +73,9 @@ async function readBatch(queuePath, startOffset, maxBuckets) {
70
73
  }
71
74
  const hourStart = typeof bucket?.hour_start === 'string' ? bucket.hour_start : null;
72
75
  if (!hourStart) continue;
73
- bucketMap.set(hourStart, bucket);
76
+ const source = normalizeSource(bucket?.source) || DEFAULT_SOURCE;
77
+ bucket.source = source;
78
+ bucketMap.set(bucketKey(source, hourStart), bucket);
74
79
  linesRead += 1;
75
80
  if (linesRead >= maxBuckets) break;
76
81
  }
@@ -89,4 +94,14 @@ async function safeFileSize(p) {
89
94
  }
90
95
  }
91
96
 
97
+ function bucketKey(source, hourStart) {
98
+ return `${source}${SOURCE_SEPARATOR}${hourStart}`;
99
+ }
100
+
101
+ function normalizeSource(value) {
102
+ if (typeof value !== 'string') return null;
103
+ const trimmed = value.trim().toLowerCase();
104
+ return trimmed.length > 0 ? trimmed : null;
105
+ }
106
+
92
107
  module.exports = { drainQueueToCloud };