@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 +40 -7
- package/README.zh-CN.md +40 -7
- package/package.json +1 -1
- package/src/cli.js +2 -1
- package/src/commands/init.js +134 -13
- package/src/commands/status.js +14 -1
- package/src/commands/sync.js +46 -5
- package/src/commands/uninstall.js +60 -8
- package/src/lib/claude-config.js +190 -0
- package/src/lib/codex-config.js +83 -16
- package/src/lib/diagnostics.js +22 -4
- package/src/lib/rollout.js +249 -18
- package/src/lib/uploader.js +25 -1
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
|
|
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
|
|
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
|
-
#
|
|
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(
|
|
96
|
+
localStorage.removeItem("vibescore.dashboard.auth.v1");
|
|
64
97
|
Object.keys(localStorage)
|
|
65
|
-
.filter((k) => k.startsWith(
|
|
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
|
-
- 📡
|
|
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(
|
|
96
|
+
localStorage.removeItem("vibescore.dashboard.auth.v1");
|
|
64
97
|
Object.keys(localStorage)
|
|
65
|
-
.filter((k) => k.startsWith(
|
|
98
|
+
.filter((k) => k.startsWith("vibescore.heatmap."))
|
|
66
99
|
.forEach((k) => localStorage.removeItem(k));
|
|
67
100
|
location.reload();
|
|
68
101
|
```
|
package/package.json
CHANGED
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')
|
package/src/commands/init.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
263
|
+
// Chain the original notify if present (Codex/Every Code only).
|
|
195
264
|
try {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
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);
|
package/src/commands/status.js
CHANGED
|
@@ -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)
|
package/src/commands/sync.js
CHANGED
|
@@ -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
|
|
47
|
-
const
|
|
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: ${
|
|
197
|
-
`- New 30-min buckets queued: ${
|
|
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 {
|
|
6
|
-
const {
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
+
}
|