@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 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 };