vibeusage 0.2.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.
@@ -0,0 +1,224 @@
1
+ const fs = require('node:fs/promises');
2
+ const path = require('node:path');
3
+
4
+ const { ensureDir, readJson, writeJson } = require('./fs');
5
+
6
+ async function upsertNotify({ configPath, notifyCmd, notifyOriginalPath, configLabel }) {
7
+ const originalText = await fs.readFile(configPath, 'utf8').catch(() => null);
8
+ if (originalText == null) {
9
+ const label = typeof configLabel === 'string' && configLabel.length > 0 ? configLabel : 'Config';
10
+ throw new Error(`${label} not found: ${configPath}`);
11
+ }
12
+
13
+ const existingNotify = extractNotify(originalText);
14
+ const already = arraysEqual(existingNotify, notifyCmd);
15
+
16
+ if (!already) {
17
+ // Persist original notify once (for uninstall + chaining).
18
+ if (existingNotify && existingNotify.length > 0) {
19
+ await ensureDir(path.dirname(notifyOriginalPath));
20
+ const existing = await readJson(notifyOriginalPath);
21
+ if (!existing) {
22
+ await writeJson(notifyOriginalPath, { notify: existingNotify, capturedAt: new Date().toISOString() });
23
+ }
24
+ }
25
+
26
+ const updated = setNotify(originalText, notifyCmd);
27
+ const backupPath = `${configPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
28
+ await fs.copyFile(configPath, backupPath);
29
+ await fs.writeFile(configPath, updated, 'utf8');
30
+ return { changed: true, backupPath };
31
+ }
32
+
33
+ return { changed: false, backupPath: null };
34
+ }
35
+
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' };
39
+
40
+ const original = await readJson(notifyOriginalPath);
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
+ }
47
+
48
+ const updated = originalNotify ? setNotify(text, originalNotify) : removeNotify(text);
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 };
55
+ }
56
+
57
+ async function loadNotifyOriginal(notifyOriginalPath) {
58
+ const original = await readJson(notifyOriginalPath);
59
+ return Array.isArray(original?.notify) ? original.notify : null;
60
+ }
61
+
62
+ async function readNotify(configPath) {
63
+ const text = await fs.readFile(configPath, 'utf8').catch(() => null);
64
+ if (text == null) return null;
65
+ return extractNotify(text);
66
+ }
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
+
118
+ function extractNotify(text) {
119
+ // Heuristic parse: find a line that starts with "notify =".
120
+ const lines = text.split(/\r?\n/);
121
+ for (const line of lines) {
122
+ const m = line.match(/^\s*notify\s*=\s*(.+)\s*$/);
123
+ if (m) {
124
+ const rhs = m[1].trim();
125
+ const parsed = parseTomlStringArray(rhs);
126
+ if (parsed) return parsed;
127
+ }
128
+ }
129
+ return null;
130
+ }
131
+
132
+ function setNotify(text, notifyCmd) {
133
+ const lines = text.split(/\r?\n/);
134
+ const notifyLine = `notify = ${formatTomlStringArray(notifyCmd)}`;
135
+
136
+ const out = [];
137
+ let replaced = false;
138
+ for (let i = 0; i < lines.length; i++) {
139
+ const line = lines[i];
140
+ const isNotify = /^\s*notify\s*=/.test(line);
141
+ if (isNotify) {
142
+ if (!replaced) {
143
+ out.push(notifyLine);
144
+ replaced = true;
145
+ }
146
+ continue;
147
+ }
148
+ out.push(line);
149
+ }
150
+
151
+ if (!replaced) {
152
+ // Insert at top-level, before the first table header.
153
+ const firstTableIdx = out.findIndex((l) => /^\s*\[/.test(l));
154
+ const headerIdx = firstTableIdx === -1 ? out.length : firstTableIdx;
155
+ out.splice(headerIdx, 0, notifyLine);
156
+ }
157
+
158
+ return out.join('\n').replace(/\n+$/, '\n');
159
+ }
160
+
161
+ function removeNotify(text) {
162
+ const lines = text.split(/\r?\n/);
163
+ const out = lines.filter((l) => !/^\s*notify\s*=/.test(l));
164
+ return out.join('\n').replace(/\n+$/, '\n');
165
+ }
166
+
167
+ function parseTomlStringArray(rhs) {
168
+ // Minimal parser for ["a", "b"] string arrays.
169
+ // Assumes there are no escapes in strings (good enough for our usage).
170
+ if (!rhs.startsWith('[') || !rhs.endsWith(']')) return null;
171
+ const inner = rhs.slice(1, -1).trim();
172
+ if (!inner) return [];
173
+
174
+ const parts = [];
175
+ let current = '';
176
+ let inString = false;
177
+ let quote = null;
178
+ for (let i = 0; i < inner.length; i++) {
179
+ const ch = inner[i];
180
+ if (!inString) {
181
+ if (ch === '"' || ch === "'") {
182
+ inString = true;
183
+ quote = ch;
184
+ current = '';
185
+ }
186
+ continue;
187
+ }
188
+ if (ch === quote) {
189
+ parts.push(current);
190
+ inString = false;
191
+ quote = null;
192
+ continue;
193
+ }
194
+ current += ch;
195
+ }
196
+
197
+ return parts.length > 0 ? parts : null;
198
+ }
199
+
200
+ function formatTomlStringArray(arr) {
201
+ return `[${arr.map((s) => JSON.stringify(String(s))).join(', ')}]`;
202
+ }
203
+
204
+ function arraysEqual(a, b) {
205
+ if (!Array.isArray(a) || !Array.isArray(b)) return false;
206
+ if (a.length !== b.length) return false;
207
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
208
+ return true;
209
+ }
210
+
211
+ module.exports = {
212
+ upsertNotify,
213
+ restoreNotify,
214
+ loadNotifyOriginal,
215
+ readNotify,
216
+ upsertCodexNotify,
217
+ restoreCodexNotify,
218
+ loadCodexNotifyOriginal,
219
+ readCodexNotify,
220
+ upsertEveryCodeNotify,
221
+ restoreEveryCodeNotify,
222
+ loadEveryCodeNotifyOriginal,
223
+ readEveryCodeNotify
224
+ };
@@ -0,0 +1,9 @@
1
+ function stripDebugFlag(argv, env = process.env) {
2
+ const filtered = Array.isArray(argv) ? argv.filter((arg) => arg !== '--debug') : [];
3
+ const debugEnv =
4
+ String(env?.VIBEUSAGE_DEBUG || '') === '1' ||
5
+ String(env?.VIBESCORE_DEBUG || '') === '1';
6
+ return { argv: filtered, debug: filtered.length !== (argv || []).length || debugEnv };
7
+ }
8
+
9
+ module.exports = { stripDebugFlag };
@@ -0,0 +1,190 @@
1
+ const os = require('node:os');
2
+ const path = require('node:path');
3
+ const fs = require('node:fs/promises');
4
+
5
+ const { readJson } = require('./fs');
6
+ const { readCodexNotify, readEveryCodeNotify } = require('./codex-config');
7
+ const { isClaudeHookConfigured, buildClaudeHookCommand } = require('./claude-config');
8
+ const {
9
+ resolveGeminiConfigDir,
10
+ resolveGeminiSettingsPath,
11
+ buildGeminiHookCommand,
12
+ isGeminiHookConfigured
13
+ } = require('./gemini-config');
14
+ const { resolveOpencodeConfigDir, isOpencodePluginInstalled } = require('./opencode-config');
15
+ const { normalizeState: normalizeUploadState } = require('./upload-throttle');
16
+ const { resolveTrackerPaths } = require('./tracker-paths');
17
+
18
+ async function collectTrackerDiagnostics({
19
+ home = os.homedir(),
20
+ codexHome = process.env.CODEX_HOME || path.join(home, '.codex'),
21
+ codeHome = process.env.CODE_HOME || path.join(home, '.code')
22
+ } = {}) {
23
+ const { trackerDir, binDir } = await resolveTrackerPaths({ home, migrate: true });
24
+ const configPath = path.join(trackerDir, 'config.json');
25
+ const queuePath = path.join(trackerDir, 'queue.jsonl');
26
+ const queueStatePath = path.join(trackerDir, 'queue.state.json');
27
+ const cursorsPath = path.join(trackerDir, 'cursors.json');
28
+ const notifySignalPath = path.join(trackerDir, 'notify.signal');
29
+ const throttlePath = path.join(trackerDir, 'sync.throttle');
30
+ const uploadThrottlePath = path.join(trackerDir, 'upload.throttle.json');
31
+ const autoRetryPath = path.join(trackerDir, 'auto.retry.json');
32
+ const codexConfigPath = path.join(codexHome, 'config.toml');
33
+ const codeConfigPath = path.join(codeHome, 'config.toml');
34
+ const claudeConfigPath = path.join(home, '.claude', 'settings.json');
35
+ const geminiConfigDir = resolveGeminiConfigDir({ home, env: process.env });
36
+ const geminiSettingsPath = resolveGeminiSettingsPath({ configDir: geminiConfigDir });
37
+ const opencodeConfigDir = resolveOpencodeConfigDir({ home, env: process.env });
38
+
39
+ const config = await readJson(configPath);
40
+ const cursors = await readJson(cursorsPath);
41
+ const queueState = (await readJson(queueStatePath)) || { offset: 0 };
42
+ const uploadThrottle = normalizeUploadState(await readJson(uploadThrottlePath));
43
+ const autoRetry = await readJson(autoRetryPath);
44
+
45
+ const queueSize = await safeStatSize(queuePath);
46
+ const offsetBytes = Number(queueState.offset || 0);
47
+ const pendingBytes = Math.max(0, queueSize - offsetBytes);
48
+
49
+ const lastNotify = (await safeReadText(notifySignalPath))?.trim() || null;
50
+ const lastNotifySpawn = parseEpochMsToIso((await safeReadText(throttlePath))?.trim() || null);
51
+
52
+ const codexNotifyRaw = await readCodexNotify(codexConfigPath);
53
+ const notifyConfigured = Array.isArray(codexNotifyRaw) && codexNotifyRaw.length > 0;
54
+ const codexNotify = notifyConfigured ? codexNotifyRaw.map((v) => redactValue(v, home)) : null;
55
+ const everyCodeNotifyRaw = await readEveryCodeNotify(codeConfigPath);
56
+ const everyCodeConfigured = Array.isArray(everyCodeNotifyRaw) && everyCodeNotifyRaw.length > 0;
57
+ const everyCodeNotify = everyCodeConfigured ? everyCodeNotifyRaw.map((v) => redactValue(v, home)) : null;
58
+ const claudeHookCommand = buildClaudeHookCommand(path.join(binDir, 'notify.cjs'));
59
+ const claudeHookConfigured = await isClaudeHookConfigured({
60
+ settingsPath: claudeConfigPath,
61
+ hookCommand: claudeHookCommand
62
+ });
63
+ const geminiHookCommand = buildGeminiHookCommand(path.join(binDir, 'notify.cjs'));
64
+ const geminiHookConfigured = await isGeminiHookConfigured({
65
+ settingsPath: geminiSettingsPath,
66
+ hookCommand: geminiHookCommand
67
+ });
68
+ const opencodePluginConfigured = await isOpencodePluginInstalled({ configDir: opencodeConfigDir });
69
+
70
+ const lastSuccessAt = uploadThrottle.lastSuccessMs ? new Date(uploadThrottle.lastSuccessMs).toISOString() : null;
71
+ const autoRetryAt = parseEpochMsToIso(autoRetry?.retryAtMs);
72
+
73
+ return {
74
+ ok: true,
75
+ version: 1,
76
+ generated_at: new Date().toISOString(),
77
+ env: {
78
+ node: process.version,
79
+ platform: process.platform,
80
+ arch: process.arch
81
+ },
82
+ paths: {
83
+ tracker_dir: redactValue(trackerDir, home),
84
+ codex_home: redactValue(codexHome, home),
85
+ codex_config: redactValue(codexConfigPath, home),
86
+ code_home: redactValue(codeHome, home),
87
+ code_config: redactValue(codeConfigPath, home),
88
+ claude_config: redactValue(claudeConfigPath, home),
89
+ gemini_config: redactValue(geminiSettingsPath, home),
90
+ opencode_config: redactValue(opencodeConfigDir, home)
91
+ },
92
+ config: {
93
+ base_url: typeof config?.baseUrl === 'string' ? config.baseUrl : null,
94
+ device_token: config?.deviceToken ? 'set' : 'unset',
95
+ device_id: maskId(config?.deviceId),
96
+ installed_at: typeof config?.installedAt === 'string' ? config.installedAt : null
97
+ },
98
+ parse: {
99
+ updated_at: typeof cursors?.updatedAt === 'string' ? cursors.updatedAt : null,
100
+ file_count: cursors?.files && typeof cursors.files === 'object' ? Object.keys(cursors.files).length : null
101
+ },
102
+ queue: {
103
+ size_bytes: queueSize,
104
+ offset_bytes: offsetBytes,
105
+ pending_bytes: pendingBytes,
106
+ updated_at: typeof queueState.updatedAt === 'string' ? queueState.updatedAt : null
107
+ },
108
+ notify: {
109
+ last_notify: lastNotify,
110
+ last_notify_triggered_sync: lastNotifySpawn,
111
+ codex_notify_configured: notifyConfigured,
112
+ codex_notify: codexNotify,
113
+ every_code_notify_configured: everyCodeConfigured,
114
+ every_code_notify: everyCodeNotify,
115
+ claude_hook_configured: claudeHookConfigured,
116
+ gemini_hook_configured: geminiHookConfigured,
117
+ opencode_plugin_configured: opencodePluginConfigured
118
+ },
119
+ upload: {
120
+ last_success_at: lastSuccessAt,
121
+ next_allowed_after: parseEpochMsToIso(uploadThrottle.nextAllowedAtMs || null),
122
+ backoff_until: parseEpochMsToIso(uploadThrottle.backoffUntilMs || null),
123
+ last_error: uploadThrottle.lastError
124
+ ? {
125
+ at: uploadThrottle.lastErrorAt || null,
126
+ message: redactError(String(uploadThrottle.lastError), home)
127
+ }
128
+ : null
129
+ },
130
+ auto_retry: autoRetryAt
131
+ ? {
132
+ next_retry_at: autoRetryAt,
133
+ reason: typeof autoRetry?.reason === 'string' ? autoRetry.reason : null,
134
+ pending_bytes: Number.isFinite(Number(autoRetry?.pendingBytes))
135
+ ? Math.max(0, Number(autoRetry.pendingBytes))
136
+ : null,
137
+ scheduled_at: typeof autoRetry?.scheduledAt === 'string' ? autoRetry.scheduledAt : null,
138
+ source: typeof autoRetry?.source === 'string' ? autoRetry.source : null
139
+ }
140
+ : null
141
+ };
142
+ }
143
+
144
+ function maskId(v) {
145
+ if (typeof v !== 'string') return null;
146
+ const s = v.trim();
147
+ if (s.length < 12) return null;
148
+ return `${s.slice(0, 8)}…${s.slice(-4)}`;
149
+ }
150
+
151
+ function redactValue(value, home) {
152
+ if (typeof value !== 'string') return value;
153
+ if (typeof home !== 'string' || home.length === 0) return value;
154
+ const homeNorm = home.endsWith(path.sep) ? home.slice(0, -1) : home;
155
+ return value.startsWith(homeNorm) ? `~${value.slice(homeNorm.length)}` : value;
156
+ }
157
+
158
+ function redactError(message, home) {
159
+ if (typeof message !== 'string') return message;
160
+ if (typeof home !== 'string' || home.length === 0) return message;
161
+ const homeNorm = home.endsWith(path.sep) ? home.slice(0, -1) : home;
162
+ return message.split(homeNorm).join('~');
163
+ }
164
+
165
+ async function safeStatSize(p) {
166
+ try {
167
+ const st = await fs.stat(p);
168
+ return st && st.isFile() ? st.size : 0;
169
+ } catch (_e) {
170
+ return 0;
171
+ }
172
+ }
173
+
174
+ async function safeReadText(p) {
175
+ try {
176
+ return await fs.readFile(p, 'utf8');
177
+ } catch (_e) {
178
+ return null;
179
+ }
180
+ }
181
+
182
+ function parseEpochMsToIso(v) {
183
+ const ms = Number(v);
184
+ if (!Number.isFinite(ms) || ms <= 0) return null;
185
+ const d = new Date(ms);
186
+ if (Number.isNaN(d.getTime())) return null;
187
+ return d.toISOString();
188
+ }
189
+
190
+ module.exports = { collectTrackerDiagnostics };
package/src/lib/fs.js ADDED
@@ -0,0 +1,62 @@
1
+ const fs = require('node:fs/promises');
2
+ const path = require('node:path');
3
+
4
+ async function ensureDir(p) {
5
+ await fs.mkdir(p, { recursive: true });
6
+ }
7
+
8
+ async function writeFileAtomic(filePath, content) {
9
+ const dir = path.dirname(filePath);
10
+ await ensureDir(dir);
11
+ const tmp = `${filePath}.tmp.${Date.now()}`;
12
+ await fs.writeFile(tmp, content, { encoding: 'utf8' });
13
+ await fs.rename(tmp, filePath);
14
+ }
15
+
16
+ async function readJson(filePath) {
17
+ try {
18
+ const raw = await fs.readFile(filePath, 'utf8');
19
+ return JSON.parse(raw);
20
+ } catch (_e) {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ async function writeJson(filePath, obj) {
26
+ await writeFileAtomic(filePath, JSON.stringify(obj, null, 2) + '\n');
27
+ }
28
+
29
+ async function chmod600IfPossible(filePath) {
30
+ try {
31
+ await fs.chmod(filePath, 0o600);
32
+ } catch (_e) {}
33
+ }
34
+
35
+ async function openLock(lockPath, { quietIfLocked }) {
36
+ try {
37
+ const handle = await fs.open(lockPath, 'wx');
38
+ return {
39
+ async release() {
40
+ await handle.close().catch(() => {});
41
+ }
42
+ };
43
+ } catch (e) {
44
+ if (e && e.code === 'EEXIST') {
45
+ if (!quietIfLocked) {
46
+ process.stdout.write('Another sync is already running.\n');
47
+ }
48
+ return null;
49
+ }
50
+ throw e;
51
+ }
52
+ }
53
+
54
+ module.exports = {
55
+ ensureDir,
56
+ writeFileAtomic,
57
+ readJson,
58
+ writeJson,
59
+ chmod600IfPossible,
60
+ openLock
61
+ };
62
+