@vibescore/tracker 0.0.1
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/LICENSE +21 -0
- package/README.md +26 -0
- package/bin/tracker.js +23 -0
- package/package.json +42 -0
- package/src/cli.js +59 -0
- package/src/commands/diagnostics.js +39 -0
- package/src/commands/init.js +256 -0
- package/src/commands/status.js +113 -0
- package/src/commands/sync.js +187 -0
- package/src/commands/uninstall.js +52 -0
- package/src/lib/browser-auth.js +139 -0
- package/src/lib/codex-config.js +157 -0
- package/src/lib/diagnostics.js +138 -0
- package/src/lib/fs.js +62 -0
- package/src/lib/insforge-client.js +59 -0
- package/src/lib/insforge.js +17 -0
- package/src/lib/progress.js +77 -0
- package/src/lib/prompt.js +20 -0
- package/src/lib/rollout.js +263 -0
- package/src/lib/upload-throttle.js +129 -0
- package/src/lib/uploader.js +88 -0
- package/src/lib/vibescore-api.js +163 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 victor-wu.eth
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @vibescore/tracker
|
|
2
|
+
|
|
3
|
+
Codex CLI token usage tracker (macOS-first, notify-driven).
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx --yes @vibescore/tracker init
|
|
9
|
+
npx --yes @vibescore/tracker sync
|
|
10
|
+
npx --yes @vibescore/tracker status
|
|
11
|
+
npx --yes @vibescore/tracker uninstall
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- Node.js >= 18
|
|
17
|
+
- macOS (current supported platform)
|
|
18
|
+
|
|
19
|
+
## Notes
|
|
20
|
+
|
|
21
|
+
- `init` installs a Codex CLI notify hook and issues a device token.
|
|
22
|
+
- `sync` parses `~/.codex/sessions/**/rollout-*.jsonl` and uploads token_count deltas.
|
|
23
|
+
|
|
24
|
+
## License
|
|
25
|
+
|
|
26
|
+
MIT
|
package/bin/tracker.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
|
|
4
|
+
const { run } = require('../src/cli');
|
|
5
|
+
|
|
6
|
+
const { argv, debug } = stripDebugFlag(process.argv.slice(2));
|
|
7
|
+
if (debug) process.env.VIBESCORE_DEBUG = '1';
|
|
8
|
+
|
|
9
|
+
run(argv).catch((err) => {
|
|
10
|
+
console.error(err?.stack || String(err));
|
|
11
|
+
if (debug) {
|
|
12
|
+
const original = err?.originalMessage;
|
|
13
|
+
if (original && original !== err?.message) {
|
|
14
|
+
console.error(`Original error: ${original}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function stripDebugFlag(argv) {
|
|
21
|
+
const filtered = argv.filter((arg) => arg !== '--debug');
|
|
22
|
+
return { argv: filtered, debug: filtered.length !== argv.length || process.env.VIBESCORE_DEBUG === '1' };
|
|
23
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vibescore/tracker",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Codex CLI token usage tracker (macOS-first, notify-driven).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"type": "commonjs",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test test/*.test.js",
|
|
12
|
+
"smoke": "node scripts/smoke/insforge-smoke.cjs",
|
|
13
|
+
"build:insforge": "node scripts/build-insforge-functions.cjs",
|
|
14
|
+
"build:insforge:check": "node scripts/build-insforge-functions.cjs --check",
|
|
15
|
+
"dashboard:dev": "npm --prefix dashboard run dev",
|
|
16
|
+
"dashboard:build": "npm --prefix dashboard run build",
|
|
17
|
+
"dashboard:preview": "npm --prefix dashboard run preview",
|
|
18
|
+
"dev:shim": "node scripts/dev-bin-shim.cjs",
|
|
19
|
+
"validate:copy": "node scripts/validate-copy-registry.cjs",
|
|
20
|
+
"copy:pull": "node scripts/copy-sync.cjs pull",
|
|
21
|
+
"copy:push": "node scripts/copy-sync.cjs push"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"tracker": "bin/tracker.js",
|
|
25
|
+
"vibescore-tracker": "bin/tracker.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"bin/",
|
|
29
|
+
"src/",
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"esbuild": "0.27.2"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@insforge/sdk": "^1.0.4"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const { cmdInit } = require('./commands/init');
|
|
2
|
+
const { cmdSync } = require('./commands/sync');
|
|
3
|
+
const { cmdStatus } = require('./commands/status');
|
|
4
|
+
const { cmdDiagnostics } = require('./commands/diagnostics');
|
|
5
|
+
const { cmdUninstall } = require('./commands/uninstall');
|
|
6
|
+
|
|
7
|
+
async function run(argv) {
|
|
8
|
+
const [command, ...rest] = argv;
|
|
9
|
+
|
|
10
|
+
if (!command || command === '-h' || command === '--help') {
|
|
11
|
+
printHelp();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
switch (command) {
|
|
16
|
+
case 'init':
|
|
17
|
+
await cmdInit(rest);
|
|
18
|
+
return;
|
|
19
|
+
case 'sync':
|
|
20
|
+
await cmdSync(rest);
|
|
21
|
+
return;
|
|
22
|
+
case 'status':
|
|
23
|
+
await cmdStatus(rest);
|
|
24
|
+
return;
|
|
25
|
+
case 'diagnostics':
|
|
26
|
+
await cmdDiagnostics(rest);
|
|
27
|
+
return;
|
|
28
|
+
case 'uninstall':
|
|
29
|
+
await cmdUninstall(rest);
|
|
30
|
+
return;
|
|
31
|
+
default:
|
|
32
|
+
throw new Error(`Unknown command: ${command}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function printHelp() {
|
|
37
|
+
// Keep this short; npx users want quick guidance.
|
|
38
|
+
process.stdout.write(
|
|
39
|
+
[
|
|
40
|
+
'@vibescore/tracker',
|
|
41
|
+
'',
|
|
42
|
+
'Usage:',
|
|
43
|
+
' npx @vibescore/tracker [--debug] init',
|
|
44
|
+
' npx @vibescore/tracker [--debug] sync [--auto] [--drain]',
|
|
45
|
+
' npx @vibescore/tracker [--debug] status',
|
|
46
|
+
' npx @vibescore/tracker [--debug] diagnostics [--out diagnostics.json]',
|
|
47
|
+
' npx @vibescore/tracker [--debug] uninstall [--purge]',
|
|
48
|
+
'',
|
|
49
|
+
'Notes:',
|
|
50
|
+
' - init installs a Codex notify hook and issues a device token (default: browser sign in/up).',
|
|
51
|
+
' - optional: set VIBESCORE_DASHBOARD_URL (or --dashboard-url) to use a hosted /connect page.',
|
|
52
|
+
' - sync parses ~/.codex/sessions/**/rollout-*.jsonl and uploads token_count deltas.',
|
|
53
|
+
' - --debug prints original backend errors when they are normalized.',
|
|
54
|
+
''
|
|
55
|
+
].join('\n')
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { run };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
|
|
3
|
+
const { writeFileAtomic, chmod600IfPossible } = require('../lib/fs');
|
|
4
|
+
const { collectTrackerDiagnostics } = require('../lib/diagnostics');
|
|
5
|
+
|
|
6
|
+
async function cmdDiagnostics(argv = []) {
|
|
7
|
+
const opts = parseArgs(argv);
|
|
8
|
+
const diagnostics = await collectTrackerDiagnostics();
|
|
9
|
+
const json = JSON.stringify(diagnostics, null, opts.compact ? 0 : 2) + '\n';
|
|
10
|
+
|
|
11
|
+
if (opts.out) {
|
|
12
|
+
const outPath = path.resolve(process.cwd(), opts.out);
|
|
13
|
+
await writeFileAtomic(outPath, json);
|
|
14
|
+
await chmod600IfPossible(outPath);
|
|
15
|
+
process.stderr.write(`Wrote diagnostics to: ${outPath}\n`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
process.stdout.write(json);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseArgs(argv) {
|
|
22
|
+
const out = {
|
|
23
|
+
out: null,
|
|
24
|
+
compact: false
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < argv.length; i++) {
|
|
28
|
+
const a = argv[i];
|
|
29
|
+
if (a === '--out') out.out = argv[++i] || null;
|
|
30
|
+
else if (a === '--compact') out.compact = true;
|
|
31
|
+
else if (a === '--pretty') out.compact = false;
|
|
32
|
+
else throw new Error(`Unknown option: ${a}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { cmdDiagnostics };
|
|
39
|
+
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
const os = require('node:os');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
|
|
5
|
+
const { ensureDir, writeFileAtomic, readJson, writeJson, chmod600IfPossible } = require('../lib/fs');
|
|
6
|
+
const { prompt, promptHidden } = require('../lib/prompt');
|
|
7
|
+
const { upsertCodexNotify, loadCodexNotifyOriginal } = require('../lib/codex-config');
|
|
8
|
+
const { beginBrowserAuth } = require('../lib/browser-auth');
|
|
9
|
+
const { issueDeviceTokenWithPassword, issueDeviceTokenWithAccessToken } = require('../lib/insforge');
|
|
10
|
+
|
|
11
|
+
async function cmdInit(argv) {
|
|
12
|
+
const opts = parseArgs(argv);
|
|
13
|
+
const home = os.homedir();
|
|
14
|
+
|
|
15
|
+
const rootDir = path.join(home, '.vibescore');
|
|
16
|
+
const trackerDir = path.join(rootDir, 'tracker');
|
|
17
|
+
const binDir = path.join(rootDir, 'bin');
|
|
18
|
+
|
|
19
|
+
await ensureDir(trackerDir);
|
|
20
|
+
await ensureDir(binDir);
|
|
21
|
+
|
|
22
|
+
const configPath = path.join(trackerDir, 'config.json');
|
|
23
|
+
const notifyOriginalPath = path.join(trackerDir, 'codex_notify_original.json');
|
|
24
|
+
|
|
25
|
+
const baseUrl = opts.baseUrl || process.env.VIBESCORE_INSFORGE_BASE_URL || 'https://5tmappuk.us-east.insforge.app';
|
|
26
|
+
let dashboardUrl = opts.dashboardUrl || process.env.VIBESCORE_DASHBOARD_URL || null;
|
|
27
|
+
const notifyPath = path.join(binDir, 'notify.cjs');
|
|
28
|
+
const appDir = path.join(trackerDir, 'app');
|
|
29
|
+
const trackerBinPath = path.join(appDir, 'bin', 'tracker.js');
|
|
30
|
+
|
|
31
|
+
const existingConfig = await readJson(configPath);
|
|
32
|
+
const deviceTokenFromEnv = process.env.VIBESCORE_DEVICE_TOKEN || null;
|
|
33
|
+
|
|
34
|
+
let deviceToken = deviceTokenFromEnv || existingConfig?.deviceToken || null;
|
|
35
|
+
let deviceId = existingConfig?.deviceId || null;
|
|
36
|
+
|
|
37
|
+
await installLocalTrackerApp({ appDir });
|
|
38
|
+
|
|
39
|
+
if (!deviceToken && !opts.noAuth) {
|
|
40
|
+
const deviceName = opts.deviceName || os.hostname();
|
|
41
|
+
|
|
42
|
+
if (opts.email || opts.password) {
|
|
43
|
+
const email = opts.email || (await prompt('Email: '));
|
|
44
|
+
const password = opts.password || (await promptHidden('Password: '));
|
|
45
|
+
const issued = await issueDeviceTokenWithPassword({ baseUrl, email, password, deviceName });
|
|
46
|
+
deviceToken = issued.token;
|
|
47
|
+
deviceId = issued.deviceId;
|
|
48
|
+
} else {
|
|
49
|
+
if (!dashboardUrl) dashboardUrl = await detectLocalDashboardUrl();
|
|
50
|
+
const flow = await beginBrowserAuth({ baseUrl, dashboardUrl, timeoutMs: 10 * 60_000, open: !opts.noOpen });
|
|
51
|
+
process.stdout.write(
|
|
52
|
+
[
|
|
53
|
+
'',
|
|
54
|
+
'Connect your account:',
|
|
55
|
+
`- Open: ${flow.authUrl}`,
|
|
56
|
+
'- Finish sign in/up in your browser, then come back here.',
|
|
57
|
+
''
|
|
58
|
+
].join('\n')
|
|
59
|
+
);
|
|
60
|
+
const callback = await flow.waitForCallback();
|
|
61
|
+
const issued = await issueDeviceTokenWithAccessToken({ baseUrl, accessToken: callback.accessToken, deviceName });
|
|
62
|
+
deviceToken = issued.token;
|
|
63
|
+
deviceId = issued.deviceId;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const config = {
|
|
68
|
+
baseUrl,
|
|
69
|
+
deviceToken,
|
|
70
|
+
deviceId,
|
|
71
|
+
installedAt: existingConfig?.installedAt || new Date().toISOString()
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
await writeJson(configPath, config);
|
|
75
|
+
await chmod600IfPossible(configPath);
|
|
76
|
+
|
|
77
|
+
// Install notify handler (non-blocking; chains the previous notify if present).
|
|
78
|
+
await writeFileAtomic(
|
|
79
|
+
notifyPath,
|
|
80
|
+
buildNotifyHandler({ trackerDir, trackerBinPath, packageName: '@vibescore/tracker' })
|
|
81
|
+
);
|
|
82
|
+
await fs.chmod(notifyPath, 0o755).catch(() => {});
|
|
83
|
+
|
|
84
|
+
// Configure Codex notify hook.
|
|
85
|
+
const codexConfigPath = path.join(home, '.codex', 'config.toml');
|
|
86
|
+
const notifyCmd = ['/usr/bin/env', 'node', notifyPath];
|
|
87
|
+
const result = await upsertCodexNotify({
|
|
88
|
+
codexConfigPath,
|
|
89
|
+
notifyCmd,
|
|
90
|
+
notifyOriginalPath
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const chained = await loadCodexNotifyOriginal(notifyOriginalPath);
|
|
94
|
+
|
|
95
|
+
process.stdout.write(
|
|
96
|
+
[
|
|
97
|
+
'Installed:',
|
|
98
|
+
`- Tracker config: ${configPath}`,
|
|
99
|
+
`- Notify handler: ${notifyPath}`,
|
|
100
|
+
`- Codex config: ${codexConfigPath}`,
|
|
101
|
+
result.changed ? '- Codex notify: updated' : '- Codex notify: already set',
|
|
102
|
+
chained ? '- Codex notify: chained (original preserved)' : '- Codex notify: no original',
|
|
103
|
+
deviceToken ? `- Device token: stored (${maskSecret(deviceToken)})` : '- Device token: not configured (set VIBESCORE_DEVICE_TOKEN and re-run init)',
|
|
104
|
+
''
|
|
105
|
+
].join('\n')
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseArgs(argv) {
|
|
110
|
+
const out = {
|
|
111
|
+
baseUrl: null,
|
|
112
|
+
dashboardUrl: null,
|
|
113
|
+
email: null,
|
|
114
|
+
password: null,
|
|
115
|
+
deviceName: null,
|
|
116
|
+
noAuth: false,
|
|
117
|
+
noOpen: false
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
for (let i = 0; i < argv.length; i++) {
|
|
121
|
+
const a = argv[i];
|
|
122
|
+
if (a === '--base-url') out.baseUrl = argv[++i] || null;
|
|
123
|
+
else if (a === '--dashboard-url') out.dashboardUrl = argv[++i] || null;
|
|
124
|
+
else if (a === '--email') out.email = argv[++i] || null;
|
|
125
|
+
else if (a === '--password') out.password = argv[++i] || null;
|
|
126
|
+
else if (a === '--device-name') out.deviceName = argv[++i] || null;
|
|
127
|
+
else if (a === '--no-auth') out.noAuth = true;
|
|
128
|
+
else if (a === '--no-open') out.noOpen = true;
|
|
129
|
+
else throw new Error(`Unknown option: ${a}`);
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function maskSecret(s) {
|
|
135
|
+
if (typeof s !== 'string' || s.length < 8) return '***';
|
|
136
|
+
return `${s.slice(0, 4)}…${s.slice(-4)}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildNotifyHandler({ trackerDir, packageName }) {
|
|
140
|
+
// Keep this file dependency-free: Node built-ins only.
|
|
141
|
+
// It must never block Codex; it spawns sync in the background and exits 0.
|
|
142
|
+
const queueSignalPath = path.join(trackerDir, 'notify.signal');
|
|
143
|
+
const originalPath = path.join(trackerDir, 'codex_notify_original.json');
|
|
144
|
+
const fallbackPkg = packageName || '@vibescore/tracker';
|
|
145
|
+
const trackerBinPath = path.join(trackerDir, 'app', 'bin', 'tracker.js');
|
|
146
|
+
|
|
147
|
+
return `#!/usr/bin/env node
|
|
148
|
+
'use strict';
|
|
149
|
+
|
|
150
|
+
const fs = require('node:fs');
|
|
151
|
+
const os = require('node:os');
|
|
152
|
+
const path = require('node:path');
|
|
153
|
+
const cp = require('node:child_process');
|
|
154
|
+
|
|
155
|
+
const payload = process.argv[2] || '';
|
|
156
|
+
const trackerDir = ${JSON.stringify(trackerDir)};
|
|
157
|
+
const signalPath = ${JSON.stringify(queueSignalPath)};
|
|
158
|
+
const originalPath = ${JSON.stringify(originalPath)};
|
|
159
|
+
const trackerBinPath = ${JSON.stringify(trackerBinPath)};
|
|
160
|
+
const fallbackPkg = ${JSON.stringify(fallbackPkg)};
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
fs.mkdirSync(trackerDir, { recursive: true });
|
|
164
|
+
fs.writeFileSync(signalPath, new Date().toISOString(), { encoding: 'utf8' });
|
|
165
|
+
} catch (_) {}
|
|
166
|
+
|
|
167
|
+
// Throttle spawn: at most once per 20 seconds.
|
|
168
|
+
try {
|
|
169
|
+
const throttlePath = path.join(trackerDir, 'sync.throttle');
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
let last = 0;
|
|
172
|
+
try { last = Number(fs.readFileSync(throttlePath, 'utf8')) || 0; } catch (_) {}
|
|
173
|
+
if (now - last > 20_000) {
|
|
174
|
+
try { fs.writeFileSync(throttlePath, String(now), 'utf8'); } catch (_) {}
|
|
175
|
+
if (fs.existsSync(trackerBinPath)) {
|
|
176
|
+
spawnDetached([process.execPath, trackerBinPath, 'sync', '--auto', '--from-notify']);
|
|
177
|
+
} else {
|
|
178
|
+
spawnDetached(['npx', '--yes', fallbackPkg, 'sync', '--auto', '--from-notify']);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch (_) {}
|
|
182
|
+
|
|
183
|
+
// Chain the original Codex notify if present.
|
|
184
|
+
try {
|
|
185
|
+
const original = JSON.parse(fs.readFileSync(originalPath, 'utf8'));
|
|
186
|
+
const cmd = Array.isArray(original?.notify) ? original.notify : null;
|
|
187
|
+
if (cmd && cmd.length > 0) {
|
|
188
|
+
const args = cmd.slice(1);
|
|
189
|
+
args.push(payload);
|
|
190
|
+
spawnDetached([cmd[0], ...args]);
|
|
191
|
+
}
|
|
192
|
+
} catch (_) {}
|
|
193
|
+
|
|
194
|
+
process.exit(0);
|
|
195
|
+
|
|
196
|
+
function spawnDetached(argv) {
|
|
197
|
+
try {
|
|
198
|
+
const child = cp.spawn(argv[0], argv.slice(1), {
|
|
199
|
+
detached: true,
|
|
200
|
+
stdio: 'ignore',
|
|
201
|
+
env: process.env
|
|
202
|
+
});
|
|
203
|
+
child.unref();
|
|
204
|
+
} catch (_) {}
|
|
205
|
+
}
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = { cmdInit };
|
|
210
|
+
|
|
211
|
+
async function detectLocalDashboardUrl() {
|
|
212
|
+
// Dev-only convenience: prefer a local dashboard (if running) so the user sees our own UI first.
|
|
213
|
+
// Vite defaults to 5173, but may auto-increment if the port is taken.
|
|
214
|
+
const hosts = ['127.0.0.1', 'localhost'];
|
|
215
|
+
const ports = [5173, 5174, 5175, 5176, 5177];
|
|
216
|
+
|
|
217
|
+
for (const port of ports) {
|
|
218
|
+
for (const host of hosts) {
|
|
219
|
+
const base = `http://${host}:${port}`;
|
|
220
|
+
const ok = await checkUrlReachable(base);
|
|
221
|
+
if (ok) return base;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function checkUrlReachable(url) {
|
|
228
|
+
const timeoutMs = 250;
|
|
229
|
+
try {
|
|
230
|
+
const controller = new AbortController();
|
|
231
|
+
const t = setTimeout(() => controller.abort(), timeoutMs);
|
|
232
|
+
const res = await fetch(url, { method: 'GET', signal: controller.signal });
|
|
233
|
+
clearTimeout(t);
|
|
234
|
+
return Boolean(res && res.ok);
|
|
235
|
+
} catch (_e) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function installLocalTrackerApp({ appDir }) {
|
|
241
|
+
// Copy the current package's runtime (bin + src) into ~/.vibescore so notify can run sync without npx.
|
|
242
|
+
const packageRoot = path.resolve(__dirname, '../..');
|
|
243
|
+
const srcFrom = path.join(packageRoot, 'src');
|
|
244
|
+
const binFrom = path.join(packageRoot, 'bin', 'tracker.js');
|
|
245
|
+
|
|
246
|
+
const srcTo = path.join(appDir, 'src');
|
|
247
|
+
const binToDir = path.join(appDir, 'bin');
|
|
248
|
+
const binTo = path.join(binToDir, 'tracker.js');
|
|
249
|
+
|
|
250
|
+
await fs.rm(appDir, { recursive: true, force: true }).catch(() => {});
|
|
251
|
+
await ensureDir(appDir);
|
|
252
|
+
await fs.cp(srcFrom, srcTo, { recursive: true });
|
|
253
|
+
await ensureDir(binToDir);
|
|
254
|
+
await fs.copyFile(binFrom, binTo);
|
|
255
|
+
await fs.chmod(binTo, 0o755).catch(() => {});
|
|
256
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const os = require('node:os');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
|
|
5
|
+
const { readJson } = require('../lib/fs');
|
|
6
|
+
const { readCodexNotify } = require('../lib/codex-config');
|
|
7
|
+
const { normalizeState: normalizeUploadState } = require('../lib/upload-throttle');
|
|
8
|
+
const { collectTrackerDiagnostics } = require('../lib/diagnostics');
|
|
9
|
+
|
|
10
|
+
async function cmdStatus(argv = []) {
|
|
11
|
+
const opts = parseArgs(argv);
|
|
12
|
+
if (opts.diagnostics) {
|
|
13
|
+
const diagnostics = await collectTrackerDiagnostics();
|
|
14
|
+
process.stdout.write(JSON.stringify(diagnostics, null, 2) + '\n');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const home = os.homedir();
|
|
19
|
+
const trackerDir = path.join(home, '.vibescore', 'tracker');
|
|
20
|
+
const configPath = path.join(trackerDir, 'config.json');
|
|
21
|
+
const queuePath = path.join(trackerDir, 'queue.jsonl');
|
|
22
|
+
const queueStatePath = path.join(trackerDir, 'queue.state.json');
|
|
23
|
+
const cursorsPath = path.join(trackerDir, 'cursors.json');
|
|
24
|
+
const notifySignalPath = path.join(trackerDir, 'notify.signal');
|
|
25
|
+
const throttlePath = path.join(trackerDir, 'sync.throttle');
|
|
26
|
+
const uploadThrottlePath = path.join(trackerDir, 'upload.throttle.json');
|
|
27
|
+
const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
|
|
28
|
+
const codexConfigPath = path.join(codexHome, 'config.toml');
|
|
29
|
+
|
|
30
|
+
const config = await readJson(configPath);
|
|
31
|
+
const cursors = await readJson(cursorsPath);
|
|
32
|
+
const queueState = (await readJson(queueStatePath)) || { offset: 0 };
|
|
33
|
+
const uploadThrottle = normalizeUploadState(await readJson(uploadThrottlePath));
|
|
34
|
+
|
|
35
|
+
const queueSize = await safeStatSize(queuePath);
|
|
36
|
+
const pendingBytes = Math.max(0, queueSize - (queueState.offset || 0));
|
|
37
|
+
|
|
38
|
+
const lastNotify = (await safeReadText(notifySignalPath))?.trim() || null;
|
|
39
|
+
const lastNotifySpawn = parseEpochMsToIso((await safeReadText(throttlePath))?.trim() || null);
|
|
40
|
+
|
|
41
|
+
const codexNotify = await readCodexNotify(codexConfigPath);
|
|
42
|
+
const notifyConfigured = Array.isArray(codexNotify) && codexNotify.length > 0;
|
|
43
|
+
|
|
44
|
+
const lastUpload = uploadThrottle.lastSuccessMs
|
|
45
|
+
? parseEpochMsToIso(uploadThrottle.lastSuccessMs)
|
|
46
|
+
: typeof queueState.updatedAt === 'string'
|
|
47
|
+
? queueState.updatedAt
|
|
48
|
+
: null;
|
|
49
|
+
const nextUpload = parseEpochMsToIso(uploadThrottle.nextAllowedAtMs || null);
|
|
50
|
+
const backoffUntil = parseEpochMsToIso(uploadThrottle.backoffUntilMs || null);
|
|
51
|
+
const lastUploadError = uploadThrottle.lastError
|
|
52
|
+
? `${uploadThrottle.lastErrorAt || 'unknown'} ${uploadThrottle.lastError}`
|
|
53
|
+
: null;
|
|
54
|
+
|
|
55
|
+
process.stdout.write(
|
|
56
|
+
[
|
|
57
|
+
'Status:',
|
|
58
|
+
`- Base URL: ${config?.baseUrl || 'unset'}`,
|
|
59
|
+
`- Device token: ${config?.deviceToken ? 'set' : 'unset'}`,
|
|
60
|
+
`- Queue: ${pendingBytes} bytes pending`,
|
|
61
|
+
`- Last parse: ${cursors?.updatedAt || 'never'}`,
|
|
62
|
+
`- Last notify: ${lastNotify || 'never'}`,
|
|
63
|
+
`- Last notify-triggered sync: ${lastNotifySpawn || 'never'}`,
|
|
64
|
+
`- Last upload: ${lastUpload || 'never'}`,
|
|
65
|
+
`- Next upload after: ${nextUpload || 'never'}`,
|
|
66
|
+
`- Backoff until: ${backoffUntil || 'never'}`,
|
|
67
|
+
lastUploadError ? `- Last upload error: ${lastUploadError}` : null,
|
|
68
|
+
`- Codex notify: ${notifyConfigured ? JSON.stringify(codexNotify) : 'unset'}`,
|
|
69
|
+
''
|
|
70
|
+
]
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.join('\n')
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseArgs(argv) {
|
|
77
|
+
const out = { diagnostics: false };
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < argv.length; i++) {
|
|
80
|
+
const a = argv[i];
|
|
81
|
+
if (a === '--diagnostics' || a === '--json') out.diagnostics = true;
|
|
82
|
+
else throw new Error(`Unknown option: ${a}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function safeStatSize(p) {
|
|
89
|
+
try {
|
|
90
|
+
const st = await fs.stat(p);
|
|
91
|
+
return st.size || 0;
|
|
92
|
+
} catch (_e) {
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function safeReadText(p) {
|
|
98
|
+
try {
|
|
99
|
+
return await fs.readFile(p, 'utf8');
|
|
100
|
+
} catch (_e) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseEpochMsToIso(v) {
|
|
106
|
+
const ms = Number(v);
|
|
107
|
+
if (!Number.isFinite(ms) || ms <= 0) return null;
|
|
108
|
+
const d = new Date(ms);
|
|
109
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
110
|
+
return d.toISOString();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { cmdStatus };
|