@getverbal/cli 0.3.0 → 0.4.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/dist/cli.js +1927 -534
- package/package.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -113,7 +113,7 @@ function compareSemver(a, b) {
|
|
|
113
113
|
}
|
|
114
114
|
return 0;
|
|
115
115
|
}
|
|
116
|
-
var VERSION = "0.
|
|
116
|
+
var VERSION = "0.4.1";
|
|
117
117
|
|
|
118
118
|
// src/auth/credentials.ts
|
|
119
119
|
import { readFileSync, writeFileSync, rmSync, mkdirSync, chmodSync, renameSync } from "node:fs";
|
|
@@ -180,19 +180,282 @@ async function clearCredentials() {
|
|
|
180
180
|
var APP_DIR = "getverbal", CREDENTIALS_FILE = "credentials.json";
|
|
181
181
|
var init_credentials = () => {};
|
|
182
182
|
|
|
183
|
+
// src/mcp/preflight-state.ts
|
|
184
|
+
var exports_preflight_state = {};
|
|
185
|
+
__export(exports_preflight_state, {
|
|
186
|
+
updatePreflightStats: () => updatePreflightStats,
|
|
187
|
+
savePreflightState: () => savePreflightState,
|
|
188
|
+
loadPreflightState: () => loadPreflightState,
|
|
189
|
+
isCooldownActive: () => isCooldownActive,
|
|
190
|
+
getDefaultStateDir: () => getDefaultStateDir,
|
|
191
|
+
drainPendingFeedback: () => drainPendingFeedback,
|
|
192
|
+
addPendingFeedback: () => addPendingFeedback,
|
|
193
|
+
activateCooldown: () => activateCooldown,
|
|
194
|
+
DEFAULT_PREFLIGHT_CONFIG: () => DEFAULT_PREFLIGHT_CONFIG
|
|
195
|
+
});
|
|
196
|
+
import fs from "node:fs";
|
|
197
|
+
import path from "node:path";
|
|
198
|
+
import os from "node:os";
|
|
199
|
+
function defaultState() {
|
|
200
|
+
return {
|
|
201
|
+
config: { ...DEFAULT_PREFLIGHT_CONFIG },
|
|
202
|
+
stats: { total_shown: 0, total_accepted: 0, total_dismissed: 0, total_skipped: 0, avg_score_when_shown: 0 },
|
|
203
|
+
cooldown: { active_until: null },
|
|
204
|
+
pending_feedback: []
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function loadPreflightState(stateDir) {
|
|
208
|
+
const statePath = path.join(stateDir, STATE_FILENAME);
|
|
209
|
+
try {
|
|
210
|
+
if (!fs.existsSync(statePath))
|
|
211
|
+
return defaultState();
|
|
212
|
+
const raw = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
213
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
214
|
+
const obj = raw;
|
|
215
|
+
return {
|
|
216
|
+
config: { ...DEFAULT_PREFLIGHT_CONFIG, ...obj.config },
|
|
217
|
+
stats: { ...defaultState().stats, ...obj.stats },
|
|
218
|
+
cooldown: obj.cooldown ?? { active_until: null },
|
|
219
|
+
pending_feedback: Array.isArray(obj.pending_feedback) ? obj.pending_feedback : []
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
} catch {}
|
|
223
|
+
return defaultState();
|
|
224
|
+
}
|
|
225
|
+
function savePreflightState(stateDir, state) {
|
|
226
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
227
|
+
fs.writeFileSync(path.join(stateDir, STATE_FILENAME), JSON.stringify(state, null, 2));
|
|
228
|
+
}
|
|
229
|
+
function getDefaultStateDir() {
|
|
230
|
+
return path.join(os.homedir(), ".verbal");
|
|
231
|
+
}
|
|
232
|
+
function updatePreflightStats(state, action, score) {
|
|
233
|
+
const stats = { ...state.stats };
|
|
234
|
+
if (action === "shown") {
|
|
235
|
+
const prevTotal = stats.total_shown;
|
|
236
|
+
stats.total_shown += 1;
|
|
237
|
+
stats.avg_score_when_shown = Math.round((stats.avg_score_when_shown * prevTotal + score) / stats.total_shown);
|
|
238
|
+
} else if (action === "accepted") {
|
|
239
|
+
stats.total_accepted += 1;
|
|
240
|
+
} else if (action === "dismissed") {
|
|
241
|
+
stats.total_dismissed += 1;
|
|
242
|
+
} else if (action === "skipped") {
|
|
243
|
+
stats.total_skipped += 1;
|
|
244
|
+
}
|
|
245
|
+
return { ...state, stats };
|
|
246
|
+
}
|
|
247
|
+
function isCooldownActive(state) {
|
|
248
|
+
if (!state.cooldown.active_until)
|
|
249
|
+
return false;
|
|
250
|
+
return new Date(state.cooldown.active_until) > new Date;
|
|
251
|
+
}
|
|
252
|
+
function activateCooldown(state, minutes) {
|
|
253
|
+
const until = new Date(Date.now() + minutes * 60 * 1000).toISOString();
|
|
254
|
+
return { ...state, cooldown: { active_until: until } };
|
|
255
|
+
}
|
|
256
|
+
function addPendingFeedback(state, entry) {
|
|
257
|
+
return { ...state, pending_feedback: [...state.pending_feedback, entry] };
|
|
258
|
+
}
|
|
259
|
+
function drainPendingFeedback(state) {
|
|
260
|
+
return {
|
|
261
|
+
feedback: [...state.pending_feedback],
|
|
262
|
+
updatedState: { ...state, pending_feedback: [] }
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
var DEFAULT_PREFLIGHT_CONFIG, STATE_FILENAME = "preflight-state.json";
|
|
266
|
+
var init_preflight_state = __esm(() => {
|
|
267
|
+
DEFAULT_PREFLIGHT_CONFIG = {
|
|
268
|
+
threshold: 60,
|
|
269
|
+
cooldown_minutes: 15,
|
|
270
|
+
mode: "notify",
|
|
271
|
+
enable_llm_rewrite: false
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// src/agent-hooks/launchagent.ts
|
|
276
|
+
function escapeXml(str) {
|
|
277
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
278
|
+
}
|
|
279
|
+
function buildUnifiedWatcherPlist(options) {
|
|
280
|
+
const { nodePath, cliPath, configPath } = options;
|
|
281
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
282
|
+
` + `<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
283
|
+
` + `<plist version="1.0">
|
|
284
|
+
` + `<dict>
|
|
285
|
+
` + ` <key>Label</key>
|
|
286
|
+
` + ` <string>ai.verbal.watcher</string>
|
|
287
|
+
` + ` <key>ProgramArguments</key>
|
|
288
|
+
` + ` <array>
|
|
289
|
+
` + ` <string>${escapeXml(nodePath)}</string>
|
|
290
|
+
` + ` <string>${escapeXml(cliPath)}</string>
|
|
291
|
+
` + ` <string>watch</string>
|
|
292
|
+
` + ` </array>
|
|
293
|
+
` + ` <key>RunAtLoad</key>
|
|
294
|
+
` + ` <true/>
|
|
295
|
+
` + ` <key>KeepAlive</key>
|
|
296
|
+
` + ` <true/>
|
|
297
|
+
` + ` <key>EnvironmentVariables</key>
|
|
298
|
+
` + ` <dict>
|
|
299
|
+
` + (configPath ? ` <key>VERBAL_CONFIG_PATH</key>
|
|
300
|
+
<string>${escapeXml(configPath)}</string>
|
|
301
|
+
` : "") + ` </dict>
|
|
302
|
+
` + `</dict>
|
|
303
|
+
` + `</plist>
|
|
304
|
+
`;
|
|
305
|
+
}
|
|
306
|
+
function buildCodexLaunchAgentPlist(options) {
|
|
307
|
+
const { nodePath, cliPath, configPath } = options;
|
|
308
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
309
|
+
` + `<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
310
|
+
` + `<plist version="1.0">
|
|
311
|
+
` + `<dict>
|
|
312
|
+
` + ` <key>Label</key>
|
|
313
|
+
` + ` <string>ai.verbal.codex-hooks</string>
|
|
314
|
+
` + ` <key>ProgramArguments</key>
|
|
315
|
+
` + ` <array>
|
|
316
|
+
` + ` <string>${escapeXml(nodePath)}</string>
|
|
317
|
+
` + ` <string>${escapeXml(cliPath)}</string>
|
|
318
|
+
` + ` <string>watch</string>
|
|
319
|
+
` + ` <string>--codex</string>
|
|
320
|
+
` + ` </array>
|
|
321
|
+
` + ` <key>RunAtLoad</key>
|
|
322
|
+
` + ` <true/>
|
|
323
|
+
` + ` <key>KeepAlive</key>
|
|
324
|
+
` + ` <true/>
|
|
325
|
+
` + ` <key>EnvironmentVariables</key>
|
|
326
|
+
` + ` <dict>
|
|
327
|
+
` + (configPath ? ` <key>VERBAL_CONFIG_PATH</key>
|
|
328
|
+
<string>${escapeXml(configPath)}</string>
|
|
329
|
+
` : "") + ` </dict>
|
|
330
|
+
` + `</dict>
|
|
331
|
+
` + `</plist>
|
|
332
|
+
`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/agent-hooks/hooks-installer.ts
|
|
336
|
+
var exports_hooks_installer = {};
|
|
337
|
+
__export(exports_hooks_installer, {
|
|
338
|
+
installUnifiedWatcher: () => installUnifiedWatcher,
|
|
339
|
+
installClaudeHooks: () => installClaudeHooks
|
|
340
|
+
});
|
|
341
|
+
import fs2 from "node:fs";
|
|
342
|
+
import os2 from "node:os";
|
|
343
|
+
import path2 from "node:path";
|
|
344
|
+
import { fileURLToPath } from "node:url";
|
|
345
|
+
import { execSync as execSync5 } from "node:child_process";
|
|
346
|
+
function installClaudeHooks(useGlobal = false) {
|
|
347
|
+
const settingsPath = path2.join(os2.homedir(), ".claude", "settings.json");
|
|
348
|
+
const hooksRoot = path2.resolve(__dirname2, "..", "..", "hooks");
|
|
349
|
+
const stopHook = path2.join(hooksRoot, "ingest-stop-hook.sh");
|
|
350
|
+
const endHook = path2.join(hooksRoot, "ingest-session-end-hook.sh");
|
|
351
|
+
const configPath = findMcpConfigPath();
|
|
352
|
+
const settings = readJson(settingsPath) ?? {};
|
|
353
|
+
settings.hooks = settings.hooks ?? {};
|
|
354
|
+
upsertHook(settings, "Stop", stopHook, {
|
|
355
|
+
timeout: 30,
|
|
356
|
+
statusMessage: "Tracking usage in Verbal",
|
|
357
|
+
async: true,
|
|
358
|
+
env: configPath ? { VERBAL_CONFIG_PATH: configPath } : undefined
|
|
359
|
+
});
|
|
360
|
+
upsertHook(settings, "SessionEnd", endHook, {
|
|
361
|
+
timeout: 30,
|
|
362
|
+
statusMessage: "Finalizing usage data",
|
|
363
|
+
env: configPath ? { VERBAL_CONFIG_PATH: configPath } : undefined
|
|
364
|
+
});
|
|
365
|
+
const reviewPromptHook = path2.join(hooksRoot, "session-review-prompt.js");
|
|
366
|
+
upsertHook(settings, "Stop", `node ${reviewPromptHook}`, {
|
|
367
|
+
timeout: 5,
|
|
368
|
+
statusMessage: "Checking session review timer"
|
|
369
|
+
});
|
|
370
|
+
const preflightHook = path2.join(hooksRoot, "preflight-hook.sh");
|
|
371
|
+
if (fs2.existsSync(preflightHook)) {
|
|
372
|
+
upsertHook(settings, "UserPromptSubmit", preflightHook, {
|
|
373
|
+
timeout: 10,
|
|
374
|
+
statusMessage: "Coaching prompt",
|
|
375
|
+
env: configPath ? { VERBAL_CONFIG_PATH: configPath } : undefined
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
fs2.mkdirSync(path2.dirname(settingsPath), { recursive: true });
|
|
379
|
+
fs2.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
380
|
+
}
|
|
381
|
+
function installUnifiedWatcher(useGlobal = false) {
|
|
382
|
+
const plistPath = path2.join(os2.homedir(), "Library", "LaunchAgents", "ai.verbal.watcher.plist");
|
|
383
|
+
const cliPath = useGlobal ? "getverbal" : path2.resolve(__dirname2, "cli.js");
|
|
384
|
+
const nodePath = process.execPath;
|
|
385
|
+
const configPath = findMcpConfigPath() ?? undefined;
|
|
386
|
+
const plist = buildUnifiedWatcherPlist({ nodePath, cliPath, configPath });
|
|
387
|
+
fs2.mkdirSync(path2.dirname(plistPath), { recursive: true });
|
|
388
|
+
const oldPlistPath = path2.join(os2.homedir(), "Library", "LaunchAgents", "ai.verbal.codex-hooks.plist");
|
|
389
|
+
tryUnloadPlist(oldPlistPath);
|
|
390
|
+
tryUnloadPlist(plistPath);
|
|
391
|
+
fs2.writeFileSync(plistPath, plist);
|
|
392
|
+
try {
|
|
393
|
+
execSync5(`launchctl load "${plistPath}"`, { stdio: "pipe" });
|
|
394
|
+
} catch {
|
|
395
|
+
console.error(`Note: Run 'launchctl load "${plistPath}"' to start the background watcher.`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function tryUnloadPlist(plistPath) {
|
|
399
|
+
if (!fs2.existsSync(plistPath))
|
|
400
|
+
return;
|
|
401
|
+
try {
|
|
402
|
+
execSync5(`launchctl unload "${plistPath}"`, { stdio: "pipe" });
|
|
403
|
+
} catch {}
|
|
404
|
+
}
|
|
405
|
+
function upsertHook(settings, hookName, command, options) {
|
|
406
|
+
const hookEntry = {
|
|
407
|
+
matcher: "",
|
|
408
|
+
hooks: [{ type: "command", command, ...options }]
|
|
409
|
+
};
|
|
410
|
+
settings.hooks = settings.hooks ?? {};
|
|
411
|
+
settings.hooks[hookName] = settings.hooks[hookName] ?? [];
|
|
412
|
+
const existingIndex = settings.hooks[hookName].findIndex((entry) => entry.hooks?.some((hook) => hook.command.includes(path2.basename(command))));
|
|
413
|
+
if (existingIndex >= 0) {
|
|
414
|
+
settings.hooks[hookName][existingIndex] = hookEntry;
|
|
415
|
+
} else {
|
|
416
|
+
settings.hooks[hookName].push(hookEntry);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function readJson(filePath) {
|
|
420
|
+
try {
|
|
421
|
+
if (!fs2.existsSync(filePath))
|
|
422
|
+
return null;
|
|
423
|
+
return JSON.parse(fs2.readFileSync(filePath, "utf8"));
|
|
424
|
+
} catch {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function findMcpConfigPath() {
|
|
429
|
+
let current = process.cwd();
|
|
430
|
+
for (;; ) {
|
|
431
|
+
const candidate = path2.join(current, ".mcp.json");
|
|
432
|
+
if (fs2.existsSync(candidate))
|
|
433
|
+
return candidate;
|
|
434
|
+
const parent = path2.dirname(current);
|
|
435
|
+
if (parent === current)
|
|
436
|
+
return null;
|
|
437
|
+
current = parent;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
var __filename2, __dirname2;
|
|
441
|
+
var init_hooks_installer = __esm(() => {
|
|
442
|
+
__filename2 = fileURLToPath(import.meta.url);
|
|
443
|
+
__dirname2 = path2.dirname(__filename2);
|
|
444
|
+
});
|
|
445
|
+
|
|
183
446
|
// src/import/index.ts
|
|
184
447
|
var exports_import = {};
|
|
185
448
|
__export(exports_import, {
|
|
186
449
|
detectImportSources: () => detectImportSources
|
|
187
450
|
});
|
|
188
|
-
import { existsSync as
|
|
189
|
-
import { join as
|
|
190
|
-
import { homedir as
|
|
451
|
+
import { existsSync as existsSync7, readdirSync } from "node:fs";
|
|
452
|
+
import { join as join11 } from "node:path";
|
|
453
|
+
import { homedir as homedir10 } from "node:os";
|
|
191
454
|
function detectImportSources() {
|
|
192
455
|
const sources = [];
|
|
193
|
-
const home =
|
|
194
|
-
const claudeDir =
|
|
195
|
-
if (
|
|
456
|
+
const home = homedir10();
|
|
457
|
+
const claudeDir = join11(home, ".claude");
|
|
458
|
+
if (existsSync7(claudeDir)) {
|
|
196
459
|
const sessions = countSessions(claudeDir, "projects");
|
|
197
460
|
if (sessions > 0) {
|
|
198
461
|
sources.push({
|
|
@@ -203,8 +466,8 @@ function detectImportSources() {
|
|
|
203
466
|
});
|
|
204
467
|
}
|
|
205
468
|
}
|
|
206
|
-
const codexDir =
|
|
207
|
-
if (
|
|
469
|
+
const codexDir = join11(home, ".codex");
|
|
470
|
+
if (existsSync7(codexDir)) {
|
|
208
471
|
const sessions = countSessions(codexDir, "sessions");
|
|
209
472
|
if (sessions > 0) {
|
|
210
473
|
sources.push({
|
|
@@ -219,8 +482,8 @@ function detectImportSources() {
|
|
|
219
482
|
}
|
|
220
483
|
function countSessions(dir, subdir) {
|
|
221
484
|
try {
|
|
222
|
-
const target =
|
|
223
|
-
if (!
|
|
485
|
+
const target = join11(dir, subdir);
|
|
486
|
+
if (!existsSync7(target))
|
|
224
487
|
return 0;
|
|
225
488
|
return readdirSync(target, { recursive: true }).filter((f) => String(f).endsWith(".jsonl") || String(f).endsWith(".json")).length;
|
|
226
489
|
} catch {
|
|
@@ -344,10 +607,10 @@ var init_pricing = __esm(() => {
|
|
|
344
607
|
});
|
|
345
608
|
|
|
346
609
|
// src/mcp/git-context.ts
|
|
347
|
-
import { execSync as
|
|
610
|
+
import { execSync as execSync6 } from "child_process";
|
|
348
611
|
function gitCommand(cmd, cwd) {
|
|
349
612
|
try {
|
|
350
|
-
return
|
|
613
|
+
return execSync6(cmd, {
|
|
351
614
|
cwd,
|
|
352
615
|
encoding: "utf8",
|
|
353
616
|
stdio: ["pipe", "pipe", "ignore"]
|
|
@@ -401,9 +664,9 @@ function getGitContext(cwd) {
|
|
|
401
664
|
var init_git_context = () => {};
|
|
402
665
|
|
|
403
666
|
// src/agent-hooks/runtime-context.ts
|
|
404
|
-
import
|
|
405
|
-
import
|
|
406
|
-
import
|
|
667
|
+
import fs3 from "node:fs";
|
|
668
|
+
import os3 from "node:os";
|
|
669
|
+
import path3 from "node:path";
|
|
407
670
|
import crypto from "node:crypto";
|
|
408
671
|
function collectRuntimeContext(cwd = process.cwd()) {
|
|
409
672
|
return {
|
|
@@ -419,7 +682,7 @@ function readMcpServers(cwd) {
|
|
|
419
682
|
if (!configPath)
|
|
420
683
|
return [];
|
|
421
684
|
try {
|
|
422
|
-
const raw =
|
|
685
|
+
const raw = fs3.readFileSync(configPath, "utf8");
|
|
423
686
|
const parsed = JSON.parse(raw);
|
|
424
687
|
const servers = parsed.mcpServers ?? {};
|
|
425
688
|
return Object.entries(servers).map(([name, conf]) => {
|
|
@@ -443,22 +706,22 @@ function readMcpServers(cwd) {
|
|
|
443
706
|
}
|
|
444
707
|
function readInstalledSkills() {
|
|
445
708
|
const roots = [
|
|
446
|
-
process.env.CODEX_HOME ?
|
|
447
|
-
|
|
709
|
+
process.env.CODEX_HOME ? path3.join(process.env.CODEX_HOME, "skills") : "",
|
|
710
|
+
path3.join(os3.homedir(), ".codex", "skills"),
|
|
448
711
|
"/opt/codex/skills"
|
|
449
712
|
].filter(Boolean);
|
|
450
713
|
const seen = new Set;
|
|
451
714
|
const skills = [];
|
|
452
715
|
for (const root of roots) {
|
|
453
|
-
if (!
|
|
716
|
+
if (!fs3.existsSync(root))
|
|
454
717
|
continue;
|
|
455
|
-
const entries =
|
|
718
|
+
const entries = fs3.readdirSync(root, { withFileTypes: true });
|
|
456
719
|
for (const entry of entries) {
|
|
457
720
|
if (!entry.isDirectory())
|
|
458
721
|
continue;
|
|
459
|
-
const skillDir =
|
|
460
|
-
const skillDoc =
|
|
461
|
-
if (!
|
|
722
|
+
const skillDir = path3.join(root, entry.name);
|
|
723
|
+
const skillDoc = path3.join(skillDir, "SKILL.md");
|
|
724
|
+
if (!fs3.existsSync(skillDoc))
|
|
462
725
|
continue;
|
|
463
726
|
const key = `${entry.name}:${skillDir}`;
|
|
464
727
|
if (seen.has(key))
|
|
@@ -476,10 +739,10 @@ function readActiveSkills() {
|
|
|
476
739
|
function findMcpConfig(startDir) {
|
|
477
740
|
let current = startDir;
|
|
478
741
|
for (;; ) {
|
|
479
|
-
const candidate =
|
|
480
|
-
if (
|
|
742
|
+
const candidate = path3.join(current, ".mcp.json");
|
|
743
|
+
if (fs3.existsSync(candidate))
|
|
481
744
|
return candidate;
|
|
482
|
-
const parent =
|
|
745
|
+
const parent = path3.dirname(current);
|
|
483
746
|
if (parent === current)
|
|
484
747
|
return null;
|
|
485
748
|
current = parent;
|
|
@@ -638,9 +901,9 @@ var init_session_tracker = __esm(() => {
|
|
|
638
901
|
|
|
639
902
|
// src/mcp/ingestor.ts
|
|
640
903
|
import crypto2 from "node:crypto";
|
|
641
|
-
import
|
|
642
|
-
import
|
|
643
|
-
import
|
|
904
|
+
import fs4 from "node:fs";
|
|
905
|
+
import os4 from "node:os";
|
|
906
|
+
import path4 from "node:path";
|
|
644
907
|
|
|
645
908
|
class VerbalIngestor {
|
|
646
909
|
config;
|
|
@@ -666,7 +929,7 @@ class VerbalIngestor {
|
|
|
666
929
|
billingType: config.billingType ?? "api",
|
|
667
930
|
maxRetries: config.maxRetries ?? 3,
|
|
668
931
|
retryBaseDelayMs: config.retryBaseDelayMs ?? 200,
|
|
669
|
-
deadLetterPath: config.deadLetterPath ??
|
|
932
|
+
deadLetterPath: config.deadLetterPath ?? path4.join(os4.homedir(), ".verbal", "dead-letter.jsonl")
|
|
670
933
|
};
|
|
671
934
|
this.runtimeContext = collectRuntimeContext();
|
|
672
935
|
this.gitContext = this.config.gitContextEnabled ? getGitContext(process.cwd()) : null;
|
|
@@ -811,135 +1074,998 @@ class VerbalIngestor {
|
|
|
811
1074
|
isUuid(value) {
|
|
812
1075
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
813
1076
|
}
|
|
814
|
-
async postToIngest(body) {
|
|
815
|
-
const payload = JSON.stringify(body);
|
|
816
|
-
const headers = {
|
|
817
|
-
"Content-Type": "application/json",
|
|
818
|
-
Authorization: `Bearer ${this.config.apiKey}`
|
|
819
|
-
};
|
|
820
|
-
if (this.config.hmacSecret) {
|
|
821
|
-
const signature = crypto2.createHmac("sha256", this.config.hmacSecret).update(payload).digest("hex");
|
|
822
|
-
headers["x-verbal-signature"] = signature;
|
|
1077
|
+
async postToIngest(body) {
|
|
1078
|
+
const payload = JSON.stringify(body);
|
|
1079
|
+
const headers = {
|
|
1080
|
+
"Content-Type": "application/json",
|
|
1081
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
1082
|
+
};
|
|
1083
|
+
if (this.config.hmacSecret) {
|
|
1084
|
+
const signature = crypto2.createHmac("sha256", this.config.hmacSecret).update(payload).digest("hex");
|
|
1085
|
+
headers["x-verbal-signature"] = signature;
|
|
1086
|
+
}
|
|
1087
|
+
const eventCount = "events" in body ? body.events.length : 1;
|
|
1088
|
+
for (let attempt = 0;attempt <= this.config.maxRetries; attempt++) {
|
|
1089
|
+
const controller = new AbortController;
|
|
1090
|
+
const timeout = setTimeout(() => controller.abort(), this.config.httpTimeoutMs);
|
|
1091
|
+
try {
|
|
1092
|
+
const response = await fetch(this.config.endpoint, {
|
|
1093
|
+
method: "POST",
|
|
1094
|
+
headers,
|
|
1095
|
+
body: payload,
|
|
1096
|
+
signal: controller.signal
|
|
1097
|
+
});
|
|
1098
|
+
if (response.ok) {
|
|
1099
|
+
return {
|
|
1100
|
+
ok: true,
|
|
1101
|
+
status: response.status,
|
|
1102
|
+
statusText: response.statusText,
|
|
1103
|
+
sent: eventCount,
|
|
1104
|
+
failed: 0
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
if (attempt < this.config.maxRetries && response.status >= 500) {
|
|
1108
|
+
await this.sleepWithJitter(attempt);
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
await this.writeDeadLetter(body, `HTTP ${response.status} ${response.statusText}`);
|
|
1112
|
+
return {
|
|
1113
|
+
ok: false,
|
|
1114
|
+
status: response.status,
|
|
1115
|
+
statusText: response.statusText,
|
|
1116
|
+
sent: 0,
|
|
1117
|
+
failed: eventCount
|
|
1118
|
+
};
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
if (attempt < this.config.maxRetries) {
|
|
1121
|
+
await this.sleepWithJitter(attempt);
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
const statusText = error instanceof Error ? error.message : "Unknown error";
|
|
1125
|
+
await this.writeDeadLetter(body, statusText);
|
|
1126
|
+
return {
|
|
1127
|
+
ok: false,
|
|
1128
|
+
statusText,
|
|
1129
|
+
sent: 0,
|
|
1130
|
+
failed: eventCount
|
|
1131
|
+
};
|
|
1132
|
+
} finally {
|
|
1133
|
+
clearTimeout(timeout);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
await this.writeDeadLetter(body, "Retry budget exhausted");
|
|
1137
|
+
return {
|
|
1138
|
+
ok: false,
|
|
1139
|
+
statusText: "Retry budget exhausted",
|
|
1140
|
+
sent: 0,
|
|
1141
|
+
failed: eventCount
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
async sleepWithJitter(attempt) {
|
|
1145
|
+
const base = this.config.retryBaseDelayMs * Math.pow(2, attempt);
|
|
1146
|
+
const jitter = Math.floor(Math.random() * this.config.retryBaseDelayMs);
|
|
1147
|
+
const delay = base + jitter;
|
|
1148
|
+
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
1149
|
+
}
|
|
1150
|
+
async writeDeadLetter(body, reason) {
|
|
1151
|
+
try {
|
|
1152
|
+
const dir = path4.dirname(this.config.deadLetterPath);
|
|
1153
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
1154
|
+
const record = {
|
|
1155
|
+
timestamp: new Date().toISOString(),
|
|
1156
|
+
reason,
|
|
1157
|
+
body
|
|
1158
|
+
};
|
|
1159
|
+
fs4.appendFileSync(this.config.deadLetterPath, `${JSON.stringify(record)}
|
|
1160
|
+
`, "utf8");
|
|
1161
|
+
} catch (error) {
|
|
1162
|
+
console.error("[VerbalIngestor] Failed to write dead-letter record", error);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
destroy() {
|
|
1166
|
+
if (this.flushTimer) {
|
|
1167
|
+
clearInterval(this.flushTimer);
|
|
1168
|
+
this.flushTimer = undefined;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
function attachExitHooks(ingestor) {
|
|
1173
|
+
const cleanup = () => {
|
|
1174
|
+
ingestor.flush().catch((err) => {
|
|
1175
|
+
console.error("[VerbalIngestor] Exit flush error:", err);
|
|
1176
|
+
});
|
|
1177
|
+
ingestor.destroy();
|
|
1178
|
+
sessionTracker.destroy();
|
|
1179
|
+
};
|
|
1180
|
+
process.on("beforeExit", cleanup);
|
|
1181
|
+
process.on("SIGINT", () => {
|
|
1182
|
+
cleanup();
|
|
1183
|
+
process.exit(0);
|
|
1184
|
+
});
|
|
1185
|
+
process.on("SIGTERM", () => {
|
|
1186
|
+
cleanup();
|
|
1187
|
+
process.exit(0);
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
var init_ingestor = __esm(() => {
|
|
1191
|
+
init_pricing();
|
|
1192
|
+
init_git_context();
|
|
1193
|
+
init_runtime_context();
|
|
1194
|
+
init_session_tracker();
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
// src/platforms/state.ts
|
|
1198
|
+
import fs5 from "node:fs";
|
|
1199
|
+
import path5 from "node:path";
|
|
1200
|
+
function loadPlatformState(stateDir, platformId) {
|
|
1201
|
+
const statePath = path5.join(stateDir, `platform-${platformId}-state.json`);
|
|
1202
|
+
try {
|
|
1203
|
+
if (!fs5.existsSync(statePath))
|
|
1204
|
+
return null;
|
|
1205
|
+
return JSON.parse(fs5.readFileSync(statePath, "utf8"));
|
|
1206
|
+
} catch {
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
function savePlatformState(stateDir, platformId, state) {
|
|
1211
|
+
const statePath = path5.join(stateDir, `platform-${platformId}-state.json`);
|
|
1212
|
+
fs5.mkdirSync(stateDir, { recursive: true });
|
|
1213
|
+
fs5.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
1214
|
+
}
|
|
1215
|
+
var init_state = () => {};
|
|
1216
|
+
|
|
1217
|
+
// src/shared/parsing-utils.ts
|
|
1218
|
+
import fs6 from "node:fs";
|
|
1219
|
+
function readJsonLines(filePath) {
|
|
1220
|
+
const content = fs6.readFileSync(filePath, "utf8");
|
|
1221
|
+
const lines = content.split(`
|
|
1222
|
+
`).filter((line) => line.trim().length > 0);
|
|
1223
|
+
const results = [];
|
|
1224
|
+
for (const line of lines) {
|
|
1225
|
+
try {
|
|
1226
|
+
results.push(JSON.parse(line));
|
|
1227
|
+
} catch {
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return results;
|
|
1232
|
+
}
|
|
1233
|
+
function flattenContent(content) {
|
|
1234
|
+
if (!content)
|
|
1235
|
+
return;
|
|
1236
|
+
if (typeof content === "string")
|
|
1237
|
+
return content;
|
|
1238
|
+
if (Array.isArray(content)) {
|
|
1239
|
+
const parts = content.map((item) => {
|
|
1240
|
+
if (!item || typeof item !== "object")
|
|
1241
|
+
return "";
|
|
1242
|
+
const typed = item;
|
|
1243
|
+
if (typed.type === "thinking" || typed.type === "tool_use" || typed.type === "reasoning")
|
|
1244
|
+
return "";
|
|
1245
|
+
return typed.text ?? typed.input_text ?? typed.output_text ?? "";
|
|
1246
|
+
}).filter(Boolean);
|
|
1247
|
+
return parts.length ? parts.join(`
|
|
1248
|
+
`) : undefined;
|
|
1249
|
+
}
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
function estimateTokens(prompt, response) {
|
|
1253
|
+
const tokensIn = prompt ? Math.ceil(prompt.length / 4) : 0;
|
|
1254
|
+
const tokensOut = response ? Math.ceil(response.length / 4) : 0;
|
|
1255
|
+
return { tokensIn, tokensOut };
|
|
1256
|
+
}
|
|
1257
|
+
var init_parsing_utils = () => {};
|
|
1258
|
+
|
|
1259
|
+
// src/platforms/parsers/claude-code.ts
|
|
1260
|
+
import fs7 from "node:fs";
|
|
1261
|
+
import path6 from "node:path";
|
|
1262
|
+
import os5 from "node:os";
|
|
1263
|
+
function getEntryId(entry) {
|
|
1264
|
+
return entry.uuid ?? entry.message?.id ?? entry.id ?? entry.messageId;
|
|
1265
|
+
}
|
|
1266
|
+
function findEntryIndex(entries, id) {
|
|
1267
|
+
return entries.findIndex((entry) => getEntryId(entry) === id);
|
|
1268
|
+
}
|
|
1269
|
+
function extractMessage(entry) {
|
|
1270
|
+
const message = entry.message;
|
|
1271
|
+
const role = message?.role ?? entry.type;
|
|
1272
|
+
const content = flattenContent(message?.content ?? entry.content);
|
|
1273
|
+
return { role, content };
|
|
1274
|
+
}
|
|
1275
|
+
function findConversationFiles(baseDir) {
|
|
1276
|
+
if (!fs7.existsSync(baseDir))
|
|
1277
|
+
return [];
|
|
1278
|
+
const results = [];
|
|
1279
|
+
const walk = (dir) => {
|
|
1280
|
+
let entries;
|
|
1281
|
+
try {
|
|
1282
|
+
entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
1283
|
+
} catch {
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
for (const entry of entries) {
|
|
1287
|
+
const full = path6.join(dir, entry.name);
|
|
1288
|
+
if (entry.isDirectory()) {
|
|
1289
|
+
walk(full);
|
|
1290
|
+
} else if (entry.isFile() && full.endsWith(".jsonl")) {
|
|
1291
|
+
results.push(full);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
};
|
|
1295
|
+
walk(baseDir);
|
|
1296
|
+
return results;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
class ClaudeCodeParser {
|
|
1300
|
+
info = {
|
|
1301
|
+
id: "claude-code",
|
|
1302
|
+
name: "Claude Code",
|
|
1303
|
+
provider: "anthropic",
|
|
1304
|
+
billing_type: "subscription"
|
|
1305
|
+
};
|
|
1306
|
+
baseDir;
|
|
1307
|
+
constructor(baseDir) {
|
|
1308
|
+
this.baseDir = baseDir ?? path6.join(os5.homedir(), ".claude", "projects");
|
|
1309
|
+
}
|
|
1310
|
+
discover() {
|
|
1311
|
+
return findConversationFiles(this.baseDir);
|
|
1312
|
+
}
|
|
1313
|
+
async parse(state, opts) {
|
|
1314
|
+
const claudeState = state;
|
|
1315
|
+
const sessions = claudeState?.sessions ?? {};
|
|
1316
|
+
const warnings = [];
|
|
1317
|
+
const events = [];
|
|
1318
|
+
const files = this.discover();
|
|
1319
|
+
for (const filePath of files) {
|
|
1320
|
+
const relative = path6.relative(this.baseDir, filePath);
|
|
1321
|
+
const sessionId = relative.replace(/\.jsonl$/, "").replace(/[\\/]/g, ":");
|
|
1322
|
+
let entries;
|
|
1323
|
+
try {
|
|
1324
|
+
entries = readJsonLines(filePath);
|
|
1325
|
+
} catch (err) {
|
|
1326
|
+
warnings.push(`Failed to read ${filePath}: ${String(err)}`);
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
if (entries.length === 0)
|
|
1330
|
+
continue;
|
|
1331
|
+
const lastId = sessions[sessionId];
|
|
1332
|
+
const startIndex = lastId ? findEntryIndex(entries, lastId) + 1 : 0;
|
|
1333
|
+
const newEntries = entries.slice(Math.max(startIndex, 0));
|
|
1334
|
+
if (newEntries.length === 0)
|
|
1335
|
+
continue;
|
|
1336
|
+
let lastUserContent;
|
|
1337
|
+
for (const entry of newEntries) {
|
|
1338
|
+
const { role, content } = extractMessage(entry);
|
|
1339
|
+
if (role === "user" || role === "human") {
|
|
1340
|
+
lastUserContent = content;
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
if (role === "assistant") {
|
|
1344
|
+
const usage = entry.message?.usage ?? entry.usage;
|
|
1345
|
+
if (!usage)
|
|
1346
|
+
continue;
|
|
1347
|
+
const tokensIn = usage.input_tokens ?? 0;
|
|
1348
|
+
const tokensOut = usage.output_tokens ?? 0;
|
|
1349
|
+
if (!tokensIn && !tokensOut)
|
|
1350
|
+
continue;
|
|
1351
|
+
const entryTimestamp = entry.timestamp ?? new Date().toISOString();
|
|
1352
|
+
if (opts.since && entryTimestamp < opts.since)
|
|
1353
|
+
continue;
|
|
1354
|
+
if (opts.until && entryTimestamp > opts.until)
|
|
1355
|
+
continue;
|
|
1356
|
+
const entryId = getEntryId(entry) ?? `${sessionId}:${events.length}`;
|
|
1357
|
+
const dedupKey = `claude-code:${sessionId}:${entryId}`;
|
|
1358
|
+
const event = {
|
|
1359
|
+
event_id: entryId,
|
|
1360
|
+
request_id: dedupKey,
|
|
1361
|
+
session_id: sessionId,
|
|
1362
|
+
provider: "anthropic",
|
|
1363
|
+
model: entry.message?.model ?? entry.model ?? "claude",
|
|
1364
|
+
tokens_in: tokensIn,
|
|
1365
|
+
tokens_out: tokensOut,
|
|
1366
|
+
cache_read_tokens: usage.cache_read_input_tokens ?? 0,
|
|
1367
|
+
cache_write_tokens: usage.cache_creation_input_tokens ?? 0,
|
|
1368
|
+
timestamp: entryTimestamp,
|
|
1369
|
+
billing_type: "subscription",
|
|
1370
|
+
tags: ["claude-code"],
|
|
1371
|
+
metadata: {
|
|
1372
|
+
stop_reason: entry.message?.stop_reason ?? entry.stop_reason,
|
|
1373
|
+
source_file: filePath
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
if (opts.captureMode === "full") {
|
|
1377
|
+
event.prompt = lastUserContent;
|
|
1378
|
+
event.response = content;
|
|
1379
|
+
}
|
|
1380
|
+
events.push(event);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
const lastEntry = newEntries[newEntries.length - 1];
|
|
1384
|
+
const lastEntryId = getEntryId(lastEntry);
|
|
1385
|
+
if (lastEntryId) {
|
|
1386
|
+
sessions[sessionId] = lastEntryId;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
const newState = {
|
|
1390
|
+
...claudeState,
|
|
1391
|
+
sessions,
|
|
1392
|
+
lastSyncTimestamp: new Date().toISOString()
|
|
1393
|
+
};
|
|
1394
|
+
return { events, newState, warnings };
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
var init_claude_code = __esm(() => {
|
|
1398
|
+
init_parsing_utils();
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
// src/platforms/parsers/codex.ts
|
|
1402
|
+
import fs8 from "node:fs";
|
|
1403
|
+
import path7 from "node:path";
|
|
1404
|
+
import os6 from "node:os";
|
|
1405
|
+
function readCodexFile(filePath) {
|
|
1406
|
+
const content = fs8.readFileSync(filePath, "utf8").trim();
|
|
1407
|
+
if (!content)
|
|
1408
|
+
return { entries: [], sessionId: path7.basename(filePath) };
|
|
1409
|
+
const isJsonl = filePath.endsWith(".jsonl");
|
|
1410
|
+
if (!isJsonl && content.startsWith("{")) {
|
|
1411
|
+
try {
|
|
1412
|
+
const parsed = JSON.parse(content);
|
|
1413
|
+
return {
|
|
1414
|
+
entries: parsed.items ?? [],
|
|
1415
|
+
sessionId: parsed.session?.id ?? path7.basename(filePath)
|
|
1416
|
+
};
|
|
1417
|
+
} catch {}
|
|
1418
|
+
}
|
|
1419
|
+
const lines = content.split(`
|
|
1420
|
+
`).filter(Boolean);
|
|
1421
|
+
const entries = lines.map((line) => JSON.parse(line));
|
|
1422
|
+
const sessionMeta = entries.find((entry) => entry.type === "session_meta");
|
|
1423
|
+
const sessionId = sessionMeta?.payload?.id ?? path7.basename(filePath);
|
|
1424
|
+
return { entries, sessionId };
|
|
1425
|
+
}
|
|
1426
|
+
function extractMessage2(entry) {
|
|
1427
|
+
if (entry.type === "response_item" && entry.payload && typeof entry.payload === "object") {
|
|
1428
|
+
const payload = entry.payload;
|
|
1429
|
+
if (payload.type === "message") {
|
|
1430
|
+
const role = payload.role;
|
|
1431
|
+
const content = flattenContent(payload.content);
|
|
1432
|
+
if (role)
|
|
1433
|
+
return { role, content };
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
if (entry.type === "message" && entry.role) {
|
|
1437
|
+
return { role: entry.role, content: flattenContent(entry.content) };
|
|
1438
|
+
}
|
|
1439
|
+
if (entry.type === "user" || entry.type === "assistant") {
|
|
1440
|
+
const role = entry.type;
|
|
1441
|
+
const content = flattenContent(entry.content ?? entry.message);
|
|
1442
|
+
return { role, content };
|
|
1443
|
+
}
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
function findSessionFiles(baseDir) {
|
|
1447
|
+
if (!fs8.existsSync(baseDir))
|
|
1448
|
+
return [];
|
|
1449
|
+
const results = [];
|
|
1450
|
+
const walk = (dir) => {
|
|
1451
|
+
let entries;
|
|
1452
|
+
try {
|
|
1453
|
+
entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
1454
|
+
} catch {
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
for (const entry of entries) {
|
|
1458
|
+
const full = path7.join(dir, entry.name);
|
|
1459
|
+
if (entry.isDirectory()) {
|
|
1460
|
+
walk(full);
|
|
1461
|
+
} else if (entry.isFile() && (full.endsWith(".json") || full.endsWith(".jsonl"))) {
|
|
1462
|
+
results.push(full);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
walk(baseDir);
|
|
1467
|
+
return results;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
class CodexParser {
|
|
1471
|
+
info = {
|
|
1472
|
+
id: "codex",
|
|
1473
|
+
name: "OpenAI Codex",
|
|
1474
|
+
provider: "openai",
|
|
1475
|
+
billing_type: "api"
|
|
1476
|
+
};
|
|
1477
|
+
baseDir;
|
|
1478
|
+
constructor(baseDir) {
|
|
1479
|
+
this.baseDir = baseDir ?? path7.join(os6.homedir(), ".codex", "sessions");
|
|
1480
|
+
}
|
|
1481
|
+
discover() {
|
|
1482
|
+
return findSessionFiles(this.baseDir);
|
|
1483
|
+
}
|
|
1484
|
+
async parse(state, opts) {
|
|
1485
|
+
const codexState = state;
|
|
1486
|
+
const fileStates = codexState?.files ?? {};
|
|
1487
|
+
const warnings = [];
|
|
1488
|
+
const events = [];
|
|
1489
|
+
const files = this.discover();
|
|
1490
|
+
for (const filePath of files) {
|
|
1491
|
+
let fileEntries;
|
|
1492
|
+
let sessionId;
|
|
1493
|
+
try {
|
|
1494
|
+
const result = readCodexFile(filePath);
|
|
1495
|
+
fileEntries = result.entries;
|
|
1496
|
+
sessionId = result.sessionId;
|
|
1497
|
+
} catch (err) {
|
|
1498
|
+
warnings.push(`Failed to read ${filePath}: ${String(err)}`);
|
|
1499
|
+
continue;
|
|
1500
|
+
}
|
|
1501
|
+
if (fileEntries.length === 0)
|
|
1502
|
+
continue;
|
|
1503
|
+
const fileState = fileStates[filePath] ?? { lastIndex: -1 };
|
|
1504
|
+
const startIndex = Math.max(fileState.lastIndex + 1, 0);
|
|
1505
|
+
const newEntries = fileEntries.slice(startIndex);
|
|
1506
|
+
if (newEntries.length === 0) {
|
|
1507
|
+
fileStates[filePath] = fileState;
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
let lastUserContent;
|
|
1511
|
+
let currentModel;
|
|
1512
|
+
let currentProvider;
|
|
1513
|
+
let lastTokenUsage;
|
|
1514
|
+
let pendingEvent;
|
|
1515
|
+
let pendingPrompt;
|
|
1516
|
+
let pendingResponse;
|
|
1517
|
+
let eventIndex = 0;
|
|
1518
|
+
for (const entry of newEntries) {
|
|
1519
|
+
if (entry.type === "session_meta" && entry.payload && typeof entry.payload === "object") {
|
|
1520
|
+
const payload = entry.payload;
|
|
1521
|
+
currentProvider = payload.model_provider ?? currentProvider;
|
|
1522
|
+
currentModel = payload.model ?? currentModel;
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
if (entry.type === "turn_context" && entry.payload && typeof entry.payload === "object") {
|
|
1526
|
+
const payload = entry.payload;
|
|
1527
|
+
currentModel = payload.model ?? currentModel;
|
|
1528
|
+
continue;
|
|
1529
|
+
}
|
|
1530
|
+
if (entry.type === "event_msg" && entry.payload) {
|
|
1531
|
+
const payload = entry.payload;
|
|
1532
|
+
if (payload.type === "token_count" && payload.info?.last_token_usage) {
|
|
1533
|
+
lastTokenUsage = payload.info.last_token_usage;
|
|
1534
|
+
if (pendingEvent) {
|
|
1535
|
+
const usage = lastTokenUsage;
|
|
1536
|
+
pendingEvent.tokens_in = usage.input_tokens ?? 0;
|
|
1537
|
+
pendingEvent.tokens_out = usage.output_tokens ?? 0;
|
|
1538
|
+
const entryTs = pendingEvent.timestamp;
|
|
1539
|
+
if (!opts.since || entryTs >= opts.since) {
|
|
1540
|
+
if (!opts.until || entryTs <= opts.until) {
|
|
1541
|
+
events.push(pendingEvent);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
pendingEvent = undefined;
|
|
1545
|
+
pendingPrompt = undefined;
|
|
1546
|
+
pendingResponse = undefined;
|
|
1547
|
+
lastTokenUsage = undefined;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
const message = extractMessage2(entry);
|
|
1553
|
+
if (message) {
|
|
1554
|
+
if (message.role === "user") {
|
|
1555
|
+
lastUserContent = message.content;
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
if (message.role === "assistant") {
|
|
1559
|
+
if (pendingEvent) {
|
|
1560
|
+
const { tokensIn, tokensOut } = estimateTokens(pendingPrompt, pendingResponse);
|
|
1561
|
+
pendingEvent.tokens_in = tokensIn;
|
|
1562
|
+
pendingEvent.tokens_out = tokensOut;
|
|
1563
|
+
pendingEvent.metadata = {
|
|
1564
|
+
...pendingEvent.metadata,
|
|
1565
|
+
token_estimate_method: "chars/4"
|
|
1566
|
+
};
|
|
1567
|
+
const entryTs = pendingEvent.timestamp;
|
|
1568
|
+
if (!opts.since || entryTs >= opts.since) {
|
|
1569
|
+
if (!opts.until || entryTs <= opts.until) {
|
|
1570
|
+
events.push(pendingEvent);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
pendingEvent = undefined;
|
|
1574
|
+
pendingPrompt = undefined;
|
|
1575
|
+
pendingResponse = undefined;
|
|
1576
|
+
}
|
|
1577
|
+
const entryTimestamp = entry.timestamp ?? new Date().toISOString();
|
|
1578
|
+
const entryId = entry.id ?? `${sessionId}:${startIndex + eventIndex}`;
|
|
1579
|
+
const dedupKey = `codex:${sessionId}:${startIndex + eventIndex}`;
|
|
1580
|
+
eventIndex++;
|
|
1581
|
+
const event = {
|
|
1582
|
+
event_id: entryId,
|
|
1583
|
+
request_id: dedupKey,
|
|
1584
|
+
session_id: sessionId,
|
|
1585
|
+
provider: currentProvider ?? "openai",
|
|
1586
|
+
model: currentModel ?? "codex",
|
|
1587
|
+
tokens_in: 0,
|
|
1588
|
+
tokens_out: 0,
|
|
1589
|
+
timestamp: entryTimestamp,
|
|
1590
|
+
billing_type: "api",
|
|
1591
|
+
tags: ["codex"],
|
|
1592
|
+
metadata: {
|
|
1593
|
+
source_file: filePath,
|
|
1594
|
+
source: "codex"
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
if (opts.captureMode === "full") {
|
|
1598
|
+
event.prompt = lastUserContent;
|
|
1599
|
+
event.response = message.content;
|
|
1600
|
+
}
|
|
1601
|
+
if (lastTokenUsage) {
|
|
1602
|
+
event.tokens_in = lastTokenUsage.input_tokens ?? 0;
|
|
1603
|
+
event.tokens_out = lastTokenUsage.output_tokens ?? 0;
|
|
1604
|
+
lastTokenUsage = undefined;
|
|
1605
|
+
const entryTs = event.timestamp;
|
|
1606
|
+
if (!opts.since || entryTs >= opts.since) {
|
|
1607
|
+
if (!opts.until || entryTs <= opts.until) {
|
|
1608
|
+
events.push(event);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
} else {
|
|
1612
|
+
pendingEvent = event;
|
|
1613
|
+
pendingPrompt = lastUserContent;
|
|
1614
|
+
pendingResponse = message.content;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
if (pendingEvent) {
|
|
1621
|
+
const { tokensIn, tokensOut } = estimateTokens(pendingPrompt, pendingResponse);
|
|
1622
|
+
pendingEvent.tokens_in = tokensIn;
|
|
1623
|
+
pendingEvent.tokens_out = tokensOut;
|
|
1624
|
+
pendingEvent.metadata = {
|
|
1625
|
+
...pendingEvent.metadata,
|
|
1626
|
+
token_estimate_method: "chars/4"
|
|
1627
|
+
};
|
|
1628
|
+
const entryTs = pendingEvent.timestamp;
|
|
1629
|
+
if (!opts.since || entryTs >= opts.since) {
|
|
1630
|
+
if (!opts.until || entryTs <= opts.until) {
|
|
1631
|
+
events.push(pendingEvent);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
fileStates[filePath] = { lastIndex: fileEntries.length - 1 };
|
|
1636
|
+
}
|
|
1637
|
+
const newState = {
|
|
1638
|
+
...codexState,
|
|
1639
|
+
files: fileStates,
|
|
1640
|
+
lastSyncTimestamp: new Date().toISOString()
|
|
1641
|
+
};
|
|
1642
|
+
return { events, newState, warnings };
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
var init_codex = __esm(() => {
|
|
1646
|
+
init_parsing_utils();
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
// src/platforms/parsers/cursor.ts
|
|
1650
|
+
import fs9 from "node:fs";
|
|
1651
|
+
import path8 from "node:path";
|
|
1652
|
+
import os7 from "node:os";
|
|
1653
|
+
function globVscdbFiles(baseDir) {
|
|
1654
|
+
if (!fs9.existsSync(baseDir))
|
|
1655
|
+
return [];
|
|
1656
|
+
const results = [];
|
|
1657
|
+
let workspaceDirs;
|
|
1658
|
+
try {
|
|
1659
|
+
workspaceDirs = fs9.readdirSync(baseDir, { withFileTypes: true });
|
|
1660
|
+
} catch {
|
|
1661
|
+
return [];
|
|
1662
|
+
}
|
|
1663
|
+
for (const entry of workspaceDirs) {
|
|
1664
|
+
if (!entry.isDirectory())
|
|
1665
|
+
continue;
|
|
1666
|
+
const candidate = path8.join(baseDir, entry.name, "state.vscdb");
|
|
1667
|
+
if (fs9.existsSync(candidate)) {
|
|
1668
|
+
results.push(candidate);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
return results;
|
|
1672
|
+
}
|
|
1673
|
+
function extractBubbleId(bubble) {
|
|
1674
|
+
return bubble.bubbleId ?? bubble.id;
|
|
1675
|
+
}
|
|
1676
|
+
function extractBubbleText(bubble) {
|
|
1677
|
+
if (typeof bubble.text === "string" && bubble.text.length > 0)
|
|
1678
|
+
return bubble.text;
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
function isAssistantBubble(bubble) {
|
|
1682
|
+
const type = (bubble.type ?? "").toLowerCase();
|
|
1683
|
+
const role = (bubble.role ?? "").toLowerCase();
|
|
1684
|
+
return type === "ai" || type === "assistant" || role === "assistant";
|
|
1685
|
+
}
|
|
1686
|
+
function isUserBubble(bubble) {
|
|
1687
|
+
const type = (bubble.type ?? "").toLowerCase();
|
|
1688
|
+
const role = (bubble.role ?? "").toLowerCase();
|
|
1689
|
+
return type === "user" || role === "user" || type === "human";
|
|
1690
|
+
}
|
|
1691
|
+
function bubbleTimestamp(bubble) {
|
|
1692
|
+
const ms = bubble.serverTimestamp ?? bubble.timestamp;
|
|
1693
|
+
if (ms)
|
|
1694
|
+
return new Date(ms).toISOString();
|
|
1695
|
+
return new Date().toISOString();
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
class CursorParser {
|
|
1699
|
+
info = {
|
|
1700
|
+
id: "cursor",
|
|
1701
|
+
name: "Cursor",
|
|
1702
|
+
provider: "anthropic",
|
|
1703
|
+
billing_type: "subscription"
|
|
1704
|
+
};
|
|
1705
|
+
baseDir;
|
|
1706
|
+
constructor(baseDir) {
|
|
1707
|
+
if (baseDir) {
|
|
1708
|
+
this.baseDir = baseDir;
|
|
1709
|
+
} else if (process.platform === "win32") {
|
|
1710
|
+
this.baseDir = path8.join(process.env.APPDATA || path8.join(os7.homedir(), "AppData", "Roaming"), "Cursor", "User", "workspaceStorage");
|
|
1711
|
+
} else if (process.platform === "linux") {
|
|
1712
|
+
this.baseDir = path8.join(os7.homedir(), ".config", "Cursor", "User", "workspaceStorage");
|
|
1713
|
+
} else {
|
|
1714
|
+
this.baseDir = path8.join(os7.homedir(), "Library", "Application Support", "Cursor", "User", "workspaceStorage");
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
discover() {
|
|
1718
|
+
return globVscdbFiles(this.baseDir);
|
|
1719
|
+
}
|
|
1720
|
+
async parse(state, opts) {
|
|
1721
|
+
const cursorState = state;
|
|
1722
|
+
const workspaces = { ...cursorState?.workspaces ?? {} };
|
|
1723
|
+
const warnings = [];
|
|
1724
|
+
const events = [];
|
|
1725
|
+
let Database;
|
|
1726
|
+
try {
|
|
1727
|
+
const mod = await import("bun:sqlite");
|
|
1728
|
+
Database = mod.Database;
|
|
1729
|
+
} catch {
|
|
1730
|
+
warnings.push("Cursor parser requires Bun runtime for SQLite access. Run via `bun run` or `bunx getverbal sync`.");
|
|
1731
|
+
return {
|
|
1732
|
+
events: [],
|
|
1733
|
+
newState: { ...cursorState ?? {}, workspaces, lastSyncTimestamp: new Date().toISOString() },
|
|
1734
|
+
warnings
|
|
1735
|
+
};
|
|
823
1736
|
}
|
|
824
|
-
const
|
|
825
|
-
for (
|
|
826
|
-
const
|
|
827
|
-
|
|
1737
|
+
const dbFiles = this.discover();
|
|
1738
|
+
for (const dbPath of dbFiles) {
|
|
1739
|
+
const workspaceHash = path8.basename(path8.dirname(dbPath));
|
|
1740
|
+
let mtime;
|
|
828
1741
|
try {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1742
|
+
mtime = fs9.statSync(dbPath).mtime.toISOString();
|
|
1743
|
+
} catch (err) {
|
|
1744
|
+
warnings.push(`Cannot stat ${dbPath}: ${String(err)}`);
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1747
|
+
const prevWorkspaceState = workspaces[workspaceHash];
|
|
1748
|
+
if (prevWorkspaceState?.fullyProcessed && prevWorkspaceState.lastMtime === mtime) {
|
|
1749
|
+
continue;
|
|
1750
|
+
}
|
|
1751
|
+
let db;
|
|
1752
|
+
try {
|
|
1753
|
+
db = new Database(dbPath, { readonly: true });
|
|
1754
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
1755
|
+
const rows = db.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'").all();
|
|
1756
|
+
let lastComposerId = prevWorkspaceState?.lastComposerId ?? "";
|
|
1757
|
+
for (const row of rows) {
|
|
1758
|
+
let composerData;
|
|
1759
|
+
try {
|
|
1760
|
+
composerData = JSON.parse(row.value);
|
|
1761
|
+
} catch {
|
|
1762
|
+
warnings.push(`Failed to parse composer JSON for key ${row.key} in ${dbPath}`);
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
const composerId = composerData.composerId ?? composerData.id ?? row.key;
|
|
1766
|
+
const bubbles = composerData.allBubbles ?? composerData.bubbles ?? composerData.conversation ?? [];
|
|
1767
|
+
if (bubbles.length === 0)
|
|
1768
|
+
continue;
|
|
1769
|
+
let lastUserText;
|
|
1770
|
+
for (let i = 0;i < bubbles.length; i++) {
|
|
1771
|
+
const bubble = bubbles[i];
|
|
1772
|
+
if (isUserBubble(bubble)) {
|
|
1773
|
+
lastUserText = extractBubbleText(bubble);
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
if (!isAssistantBubble(bubble))
|
|
1777
|
+
continue;
|
|
1778
|
+
const bubbleId = extractBubbleId(bubble) ?? `${composerId}:${i}`;
|
|
1779
|
+
const dedupKey = `cursor:${workspaceHash}:${composerId}:${bubbleId}`;
|
|
1780
|
+
const timestamp = bubbleTimestamp(bubble);
|
|
1781
|
+
if (opts.since && timestamp < opts.since)
|
|
1782
|
+
continue;
|
|
1783
|
+
if (opts.until && timestamp > opts.until)
|
|
1784
|
+
continue;
|
|
1785
|
+
const responseText = extractBubbleText(bubble);
|
|
1786
|
+
const model = bubble.modelType ?? bubble.model ?? composerData.model ?? "claude";
|
|
1787
|
+
let tokensIn = bubble.inputTokenCount ?? 0;
|
|
1788
|
+
let tokensOut = bubble.outputTokenCount ?? bubble.numTokens ?? 0;
|
|
1789
|
+
let tokenEstimateMethod;
|
|
1790
|
+
if (!tokensIn && !tokensOut) {
|
|
1791
|
+
const est = estimateTokens(lastUserText, responseText);
|
|
1792
|
+
tokensIn = est.tokensIn;
|
|
1793
|
+
tokensOut = est.tokensOut;
|
|
1794
|
+
tokenEstimateMethod = "chars/4";
|
|
1795
|
+
}
|
|
1796
|
+
const event = {
|
|
1797
|
+
event_id: bubbleId,
|
|
1798
|
+
request_id: dedupKey,
|
|
1799
|
+
session_id: `cursor:${workspaceHash}:${composerId}`,
|
|
1800
|
+
provider: "anthropic",
|
|
1801
|
+
model,
|
|
1802
|
+
tokens_in: tokensIn,
|
|
1803
|
+
tokens_out: tokensOut,
|
|
1804
|
+
timestamp,
|
|
1805
|
+
billing_type: "subscription",
|
|
1806
|
+
tags: ["cursor"],
|
|
1807
|
+
metadata: {
|
|
1808
|
+
source_file: dbPath,
|
|
1809
|
+
workspace_hash: workspaceHash,
|
|
1810
|
+
composer_id: composerId,
|
|
1811
|
+
bubble_id: bubbleId,
|
|
1812
|
+
...tokenEstimateMethod ? { token_estimate_method: tokenEstimateMethod } : {}
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1815
|
+
if (opts.captureMode === "full") {
|
|
1816
|
+
event.prompt = lastUserText;
|
|
1817
|
+
event.response = responseText;
|
|
1818
|
+
}
|
|
1819
|
+
events.push(event);
|
|
1820
|
+
}
|
|
1821
|
+
lastComposerId = composerId;
|
|
847
1822
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
statusText: response.statusText,
|
|
853
|
-
sent: 0,
|
|
854
|
-
failed: eventCount
|
|
1823
|
+
workspaces[workspaceHash] = {
|
|
1824
|
+
lastMtime: mtime,
|
|
1825
|
+
lastComposerId,
|
|
1826
|
+
fullyProcessed: true
|
|
855
1827
|
};
|
|
856
|
-
} catch (
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1828
|
+
} catch (err) {
|
|
1829
|
+
const msg = String(err);
|
|
1830
|
+
if (msg.includes("SQLITE_BUSY") || msg.includes("SQLITE_LOCKED")) {
|
|
1831
|
+
warnings.push(`Database locked, skipping ${dbPath}: ${msg}`);
|
|
1832
|
+
} else {
|
|
1833
|
+
warnings.push(`Error reading ${dbPath}: ${msg}`);
|
|
860
1834
|
}
|
|
861
|
-
const statusText = error instanceof Error ? error.message : "Unknown error";
|
|
862
|
-
await this.writeDeadLetter(body, statusText);
|
|
863
|
-
return {
|
|
864
|
-
ok: false,
|
|
865
|
-
statusText,
|
|
866
|
-
sent: 0,
|
|
867
|
-
failed: eventCount
|
|
868
|
-
};
|
|
869
1835
|
} finally {
|
|
870
|
-
|
|
1836
|
+
try {
|
|
1837
|
+
db?.close();
|
|
1838
|
+
} catch {}
|
|
871
1839
|
}
|
|
872
1840
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
sent: 0,
|
|
878
|
-
failed: eventCount
|
|
1841
|
+
const newState = {
|
|
1842
|
+
...cursorState ?? {},
|
|
1843
|
+
workspaces,
|
|
1844
|
+
lastSyncTimestamp: new Date().toISOString()
|
|
879
1845
|
};
|
|
1846
|
+
return { events, newState, warnings };
|
|
880
1847
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1848
|
+
}
|
|
1849
|
+
var init_cursor = __esm(() => {
|
|
1850
|
+
init_parsing_utils();
|
|
1851
|
+
});
|
|
1852
|
+
|
|
1853
|
+
// src/platforms/parsers/gemini.ts
|
|
1854
|
+
import fs10 from "node:fs";
|
|
1855
|
+
import path9 from "node:path";
|
|
1856
|
+
import os8 from "node:os";
|
|
1857
|
+
function flattenParts(parts) {
|
|
1858
|
+
if (!parts || parts.length === 0)
|
|
1859
|
+
return;
|
|
1860
|
+
const texts = parts.map((p) => p.text ?? "").filter(Boolean);
|
|
1861
|
+
return texts.length ? texts.join(`
|
|
1862
|
+
`) : undefined;
|
|
1863
|
+
}
|
|
1864
|
+
function chatDirs(home) {
|
|
1865
|
+
return [
|
|
1866
|
+
path9.join(home, ".gemini", "tmp"),
|
|
1867
|
+
path9.join(home, ".gemini", "chats"),
|
|
1868
|
+
path9.join(home, ".config", "gemini", "chats")
|
|
1869
|
+
];
|
|
1870
|
+
}
|
|
1871
|
+
function findJsonFiles(baseDir) {
|
|
1872
|
+
if (!fs10.existsSync(baseDir))
|
|
1873
|
+
return [];
|
|
1874
|
+
const results = [];
|
|
1875
|
+
const walk = (dir) => {
|
|
1876
|
+
let entries;
|
|
888
1877
|
try {
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
timestamp: new Date().toISOString(),
|
|
893
|
-
reason,
|
|
894
|
-
body
|
|
895
|
-
};
|
|
896
|
-
fs2.appendFileSync(this.config.deadLetterPath, `${JSON.stringify(record)}
|
|
897
|
-
`, "utf8");
|
|
898
|
-
} catch (error) {
|
|
899
|
-
console.error("[VerbalIngestor] Failed to write dead-letter record", error);
|
|
1878
|
+
entries = fs10.readdirSync(dir, { withFileTypes: true });
|
|
1879
|
+
} catch {
|
|
1880
|
+
return;
|
|
900
1881
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1882
|
+
for (const entry of entries) {
|
|
1883
|
+
const full = path9.join(dir, entry.name);
|
|
1884
|
+
if (entry.isDirectory()) {
|
|
1885
|
+
walk(full);
|
|
1886
|
+
} else if (entry.isFile() && full.endsWith(".json")) {
|
|
1887
|
+
results.push(full);
|
|
1888
|
+
}
|
|
906
1889
|
}
|
|
1890
|
+
};
|
|
1891
|
+
walk(baseDir);
|
|
1892
|
+
return results;
|
|
1893
|
+
}
|
|
1894
|
+
function extractMessages(session) {
|
|
1895
|
+
return session.messages ?? session.contents ?? session.history ?? [];
|
|
1896
|
+
}
|
|
1897
|
+
function deriveSessionId(filePath, home) {
|
|
1898
|
+
const rel = path9.relative(home, filePath).replace(/\.json$/, "").replace(/[\\/]/g, ":");
|
|
1899
|
+
return rel || path9.basename(filePath, ".json");
|
|
1900
|
+
}
|
|
1901
|
+
function deriveProjectHash(filePath) {
|
|
1902
|
+
const parts = filePath.split(path9.sep);
|
|
1903
|
+
const tmpIdx = parts.lastIndexOf("tmp");
|
|
1904
|
+
if (tmpIdx !== -1 && parts.length > tmpIdx + 1) {
|
|
1905
|
+
return parts[tmpIdx + 1];
|
|
907
1906
|
}
|
|
1907
|
+
return "default";
|
|
908
1908
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1909
|
+
|
|
1910
|
+
class GeminiParser {
|
|
1911
|
+
info = {
|
|
1912
|
+
id: "gemini",
|
|
1913
|
+
name: "Gemini CLI",
|
|
1914
|
+
provider: "google",
|
|
1915
|
+
billing_type: "api"
|
|
916
1916
|
};
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1917
|
+
home;
|
|
1918
|
+
constructor(home) {
|
|
1919
|
+
this.home = home ?? os8.homedir();
|
|
1920
|
+
}
|
|
1921
|
+
discover() {
|
|
1922
|
+
const results = [];
|
|
1923
|
+
for (const dir of chatDirs(this.home)) {
|
|
1924
|
+
results.push(...findJsonFiles(dir));
|
|
1925
|
+
}
|
|
1926
|
+
return [...new Set(results)];
|
|
1927
|
+
}
|
|
1928
|
+
async parse(state, opts) {
|
|
1929
|
+
const geminiState = state;
|
|
1930
|
+
const fileStates = { ...geminiState?.files ?? {} };
|
|
1931
|
+
const warnings = [];
|
|
1932
|
+
const events = [];
|
|
1933
|
+
const files = this.discover();
|
|
1934
|
+
for (const filePath of files) {
|
|
1935
|
+
let mtime;
|
|
1936
|
+
try {
|
|
1937
|
+
mtime = fs10.statSync(filePath).mtime.toISOString();
|
|
1938
|
+
} catch (err) {
|
|
1939
|
+
warnings.push(`Cannot stat ${filePath}: ${String(err)}`);
|
|
1940
|
+
continue;
|
|
1941
|
+
}
|
|
1942
|
+
const prevFileState = fileStates[filePath];
|
|
1943
|
+
if (prevFileState && prevFileState.lastMtime === mtime) {
|
|
1944
|
+
continue;
|
|
1945
|
+
}
|
|
1946
|
+
let session;
|
|
1947
|
+
try {
|
|
1948
|
+
const raw = fs10.readFileSync(filePath, "utf8");
|
|
1949
|
+
session = JSON.parse(raw);
|
|
1950
|
+
} catch (err) {
|
|
1951
|
+
warnings.push(`Failed to read/parse ${filePath}: ${String(err)}`);
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
const messages = extractMessages(session);
|
|
1955
|
+
if (messages.length === 0) {
|
|
1956
|
+
fileStates[filePath] = { lastMtime: mtime, lastMessageIndex: -1 };
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1959
|
+
const startIndex = (prevFileState?.lastMessageIndex ?? -1) + 1;
|
|
1960
|
+
const newMessages = messages.slice(startIndex);
|
|
1961
|
+
if (newMessages.length === 0) {
|
|
1962
|
+
fileStates[filePath] = { lastMtime: mtime, lastMessageIndex: messages.length - 1 };
|
|
1963
|
+
continue;
|
|
1964
|
+
}
|
|
1965
|
+
const sessionId = deriveSessionId(filePath, this.home);
|
|
1966
|
+
const projectHash = deriveProjectHash(filePath);
|
|
1967
|
+
const modelFromMeta = session.metadata?.model ?? session.model ?? "gemini";
|
|
1968
|
+
let lastUserText;
|
|
1969
|
+
for (let i = 0;i < newMessages.length; i++) {
|
|
1970
|
+
const msg = newMessages[i];
|
|
1971
|
+
const role = (msg.role ?? "").toLowerCase();
|
|
1972
|
+
const absoluteIndex = startIndex + i;
|
|
1973
|
+
const text = flattenParts(msg.parts);
|
|
1974
|
+
if (role === "user") {
|
|
1975
|
+
lastUserText = text;
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1978
|
+
if (role !== "model" && role !== "assistant")
|
|
1979
|
+
continue;
|
|
1980
|
+
const usage = msg.usageMetadata ?? msg.metadata?.usageMetadata;
|
|
1981
|
+
const timestamp = mtime;
|
|
1982
|
+
if (opts.since && timestamp < opts.since)
|
|
1983
|
+
continue;
|
|
1984
|
+
if (opts.until && timestamp > opts.until)
|
|
1985
|
+
continue;
|
|
1986
|
+
let tokensIn = usage?.promptTokenCount ?? 0;
|
|
1987
|
+
let tokensOut = usage?.candidatesTokenCount ?? 0;
|
|
1988
|
+
let tokenEstimateMethod;
|
|
1989
|
+
if (!tokensIn && !tokensOut) {
|
|
1990
|
+
const est = estimateTokens(lastUserText, text);
|
|
1991
|
+
tokensIn = est.tokensIn;
|
|
1992
|
+
tokensOut = est.tokensOut;
|
|
1993
|
+
tokenEstimateMethod = "chars/4";
|
|
1994
|
+
}
|
|
1995
|
+
const dedupKey = `gemini:${projectHash}:${sessionId}:${absoluteIndex}`;
|
|
1996
|
+
const event = {
|
|
1997
|
+
event_id: `${sessionId}:${absoluteIndex}`,
|
|
1998
|
+
request_id: dedupKey,
|
|
1999
|
+
session_id: sessionId,
|
|
2000
|
+
provider: "google",
|
|
2001
|
+
model: modelFromMeta,
|
|
2002
|
+
tokens_in: tokensIn,
|
|
2003
|
+
tokens_out: tokensOut,
|
|
2004
|
+
timestamp,
|
|
2005
|
+
billing_type: "api",
|
|
2006
|
+
tags: ["gemini"],
|
|
2007
|
+
metadata: {
|
|
2008
|
+
source_file: filePath,
|
|
2009
|
+
project_hash: projectHash,
|
|
2010
|
+
message_index: absoluteIndex,
|
|
2011
|
+
...tokenEstimateMethod ? { token_estimate_method: tokenEstimateMethod } : {}
|
|
2012
|
+
}
|
|
2013
|
+
};
|
|
2014
|
+
if (opts.captureMode === "full") {
|
|
2015
|
+
event.prompt = lastUserText;
|
|
2016
|
+
event.response = text;
|
|
2017
|
+
}
|
|
2018
|
+
events.push(event);
|
|
2019
|
+
}
|
|
2020
|
+
fileStates[filePath] = { lastMtime: mtime, lastMessageIndex: messages.length - 1 };
|
|
2021
|
+
}
|
|
2022
|
+
const newState = {
|
|
2023
|
+
...geminiState ?? {},
|
|
2024
|
+
files: fileStates,
|
|
2025
|
+
lastSyncTimestamp: new Date().toISOString()
|
|
2026
|
+
};
|
|
2027
|
+
return { events, newState, warnings };
|
|
2028
|
+
}
|
|
926
2029
|
}
|
|
927
|
-
var
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
2030
|
+
var init_gemini = __esm(() => {
|
|
2031
|
+
init_parsing_utils();
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
// src/platforms/parsers/index.ts
|
|
2035
|
+
var init_parsers = __esm(() => {
|
|
2036
|
+
init_claude_code();
|
|
2037
|
+
init_codex();
|
|
2038
|
+
init_cursor();
|
|
2039
|
+
init_gemini();
|
|
2040
|
+
});
|
|
2041
|
+
|
|
2042
|
+
// src/platforms/registry.ts
|
|
2043
|
+
function getAllParsers() {
|
|
2044
|
+
return PARSERS;
|
|
2045
|
+
}
|
|
2046
|
+
function getParser(platformId) {
|
|
2047
|
+
return PARSERS.find((p) => p.info.id === platformId);
|
|
2048
|
+
}
|
|
2049
|
+
var PARSERS;
|
|
2050
|
+
var init_registry = __esm(() => {
|
|
2051
|
+
init_parsers();
|
|
2052
|
+
PARSERS = [
|
|
2053
|
+
new ClaudeCodeParser,
|
|
2054
|
+
new CodexParser,
|
|
2055
|
+
new CursorParser,
|
|
2056
|
+
new GeminiParser
|
|
2057
|
+
];
|
|
932
2058
|
});
|
|
933
2059
|
|
|
934
2060
|
// src/agent-hooks/state.ts
|
|
935
|
-
import
|
|
936
|
-
import
|
|
2061
|
+
import fs11 from "node:fs";
|
|
2062
|
+
import path10 from "node:path";
|
|
937
2063
|
function loadClaudeState(stateDir) {
|
|
938
|
-
const statePath =
|
|
2064
|
+
const statePath = path10.join(stateDir, "last-ingested.json");
|
|
939
2065
|
try {
|
|
940
|
-
if (!
|
|
2066
|
+
if (!fs11.existsSync(statePath))
|
|
941
2067
|
return {};
|
|
942
|
-
const raw = JSON.parse(
|
|
2068
|
+
const raw = JSON.parse(fs11.readFileSync(statePath, "utf8"));
|
|
943
2069
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
944
2070
|
if ("sessions" in raw) {
|
|
945
2071
|
const sessions = raw.sessions;
|
|
@@ -953,16 +2079,16 @@ function loadClaudeState(stateDir) {
|
|
|
953
2079
|
return {};
|
|
954
2080
|
}
|
|
955
2081
|
function saveClaudeState(stateDir, state) {
|
|
956
|
-
const statePath =
|
|
957
|
-
|
|
958
|
-
|
|
2082
|
+
const statePath = path10.join(stateDir, "last-ingested.json");
|
|
2083
|
+
fs11.mkdirSync(stateDir, { recursive: true });
|
|
2084
|
+
fs11.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
959
2085
|
}
|
|
960
2086
|
function loadCodexState(stateDir) {
|
|
961
|
-
const statePath =
|
|
2087
|
+
const statePath = path10.join(stateDir, "codex-last-ingested.json");
|
|
962
2088
|
try {
|
|
963
|
-
if (!
|
|
2089
|
+
if (!fs11.existsSync(statePath))
|
|
964
2090
|
return { files: {} };
|
|
965
|
-
const raw = JSON.parse(
|
|
2091
|
+
const raw = JSON.parse(fs11.readFileSync(statePath, "utf8"));
|
|
966
2092
|
if (raw && typeof raw === "object" && raw.files && typeof raw.files === "object") {
|
|
967
2093
|
return raw;
|
|
968
2094
|
}
|
|
@@ -972,16 +2098,16 @@ function loadCodexState(stateDir) {
|
|
|
972
2098
|
return { files: {} };
|
|
973
2099
|
}
|
|
974
2100
|
function saveCodexState(stateDir, state) {
|
|
975
|
-
const statePath =
|
|
976
|
-
|
|
977
|
-
|
|
2101
|
+
const statePath = path10.join(stateDir, "codex-last-ingested.json");
|
|
2102
|
+
fs11.mkdirSync(stateDir, { recursive: true });
|
|
2103
|
+
fs11.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
978
2104
|
}
|
|
979
2105
|
function loadTokscaleState(stateDir) {
|
|
980
|
-
const statePath =
|
|
2106
|
+
const statePath = path10.join(stateDir, "tokscale-state.json");
|
|
981
2107
|
try {
|
|
982
|
-
if (!
|
|
2108
|
+
if (!fs11.existsSync(statePath))
|
|
983
2109
|
return {};
|
|
984
|
-
const raw = JSON.parse(
|
|
2110
|
+
const raw = JSON.parse(fs11.readFileSync(statePath, "utf8"));
|
|
985
2111
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
986
2112
|
return raw;
|
|
987
2113
|
}
|
|
@@ -990,156 +2116,100 @@ function loadTokscaleState(stateDir) {
|
|
|
990
2116
|
}
|
|
991
2117
|
return {};
|
|
992
2118
|
}
|
|
993
|
-
|
|
994
|
-
const statePath = path3.join(stateDir, "tokscale-state.json");
|
|
995
|
-
fs3.mkdirSync(stateDir, { recursive: true });
|
|
996
|
-
fs3.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
997
|
-
}
|
|
998
|
-
var init_state = () => {};
|
|
2119
|
+
var init_state2 = () => {};
|
|
999
2120
|
|
|
1000
|
-
// src/
|
|
1001
|
-
var
|
|
1002
|
-
__export(
|
|
1003
|
-
|
|
1004
|
-
runTokscale: () => runTokscale,
|
|
1005
|
-
mapTokscaleToEvents: () => mapTokscaleToEvents
|
|
2121
|
+
// src/platforms/orchestrator.ts
|
|
2122
|
+
var exports_orchestrator = {};
|
|
2123
|
+
__export(exports_orchestrator, {
|
|
2124
|
+
syncPlatforms: () => syncPlatforms
|
|
1006
2125
|
});
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
child.on("close", (code) => {
|
|
1056
|
-
if (code !== 0) {
|
|
1057
|
-
const stderr = Buffer.concat(errChunks).toString("utf8");
|
|
1058
|
-
reject(new Error(`tokscale exited with code ${code}: ${stderr}`));
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
const stdout = Buffer.concat(chunks).toString("utf8");
|
|
1062
|
-
try {
|
|
1063
|
-
resolve2(JSON.parse(stdout));
|
|
1064
|
-
} catch (parseErr) {
|
|
1065
|
-
reject(new Error(`Failed to parse tokscale JSON output: ${parseErr.message}`));
|
|
1066
|
-
}
|
|
1067
|
-
});
|
|
1068
|
-
});
|
|
1069
|
-
}
|
|
1070
|
-
function mapTokscaleToEvents(output, project) {
|
|
1071
|
-
const events = [];
|
|
1072
|
-
for (const daily of output.daily) {
|
|
1073
|
-
for (const source of daily.sources) {
|
|
1074
|
-
const event = {
|
|
1075
|
-
event_type: "usage",
|
|
1076
|
-
provider: normalizeProvider(source.providerId),
|
|
1077
|
-
model: source.modelId,
|
|
1078
|
-
tokens_in: source.tokens.input,
|
|
1079
|
-
tokens_out: source.tokens.output,
|
|
1080
|
-
cache_read_tokens: source.tokens.cacheRead,
|
|
1081
|
-
cache_write_tokens: source.tokens.cacheWrite,
|
|
1082
|
-
cost: source.cost,
|
|
1083
|
-
currency: "USD",
|
|
1084
|
-
billing_type: inferBillingType(source.source),
|
|
1085
|
-
timestamp: `${daily.utcDate}T12:00:00Z`,
|
|
1086
|
-
session_id: `tokscale-${daily.utcDate}`,
|
|
1087
|
-
request_id: `tokscale:${daily.utcDate}:${source.source}:${source.modelId}`,
|
|
1088
|
-
tags: ["tokscale", source.source],
|
|
1089
|
-
...project ? { project } : {},
|
|
1090
|
-
metadata: {
|
|
1091
|
-
import_source: "tokscale",
|
|
1092
|
-
tokscale_intensity: daily.intensity,
|
|
1093
|
-
tokscale_message_count: source.messageCount,
|
|
1094
|
-
reasoning_tokens: source.tokens.reasoning,
|
|
1095
|
-
day_total_cost: daily.totalCost,
|
|
1096
|
-
day_total_tokens: daily.totalTokens
|
|
1097
|
-
}
|
|
1098
|
-
};
|
|
1099
|
-
events.push(event);
|
|
2126
|
+
async function syncPlatforms(config, options = {}) {
|
|
2127
|
+
const targets = options.platforms ? options.platforms.map((id) => getParser(id)).filter((p) => p !== undefined) : getAllParsers();
|
|
2128
|
+
const tokscaleState = loadTokscaleState(config.stateDir);
|
|
2129
|
+
const since = options.since ?? tokscaleState.lastSyncDate;
|
|
2130
|
+
const platformResults = [];
|
|
2131
|
+
let totalEvents = 0;
|
|
2132
|
+
let totalCost = 0;
|
|
2133
|
+
const ingestor = new VerbalIngestor({
|
|
2134
|
+
apiKey: config.apiKey,
|
|
2135
|
+
endpoint: config.ingestUrl,
|
|
2136
|
+
hmacSecret: config.hmacSecret,
|
|
2137
|
+
orgId: config.orgId,
|
|
2138
|
+
batchSize: config.batchSize,
|
|
2139
|
+
flushIntervalMs: config.flushIntervalMs,
|
|
2140
|
+
httpTimeoutMs: config.httpTimeoutMs
|
|
2141
|
+
});
|
|
2142
|
+
for (const parser of targets) {
|
|
2143
|
+
try {
|
|
2144
|
+
const state = loadPlatformState(config.stateDir, parser.info.id);
|
|
2145
|
+
const result = await parser.parse(state, {
|
|
2146
|
+
since,
|
|
2147
|
+
until: options.until,
|
|
2148
|
+
captureMode: options.captureMode
|
|
2149
|
+
});
|
|
2150
|
+
const usageEvents = result.events.map((ev) => mapParsedEventToUsageEvent(ev, options.project));
|
|
2151
|
+
let platformCost = 0;
|
|
2152
|
+
for (const event of usageEvents) {
|
|
2153
|
+
platformCost += event.cost ?? 0;
|
|
2154
|
+
await ingestor.track(event);
|
|
2155
|
+
}
|
|
2156
|
+
savePlatformState(config.stateDir, parser.info.id, result.newState);
|
|
2157
|
+
platformResults.push({
|
|
2158
|
+
id: parser.info.id,
|
|
2159
|
+
events: result.events.length,
|
|
2160
|
+
totalCost: platformCost,
|
|
2161
|
+
warnings: result.warnings
|
|
2162
|
+
});
|
|
2163
|
+
totalEvents += result.events.length;
|
|
2164
|
+
totalCost += platformCost;
|
|
2165
|
+
} catch (err) {
|
|
2166
|
+
platformResults.push({
|
|
2167
|
+
id: parser.info.id,
|
|
2168
|
+
events: 0,
|
|
2169
|
+
totalCost: 0,
|
|
2170
|
+
warnings: [],
|
|
2171
|
+
error: `Parser failed: ${err.message}`
|
|
2172
|
+
});
|
|
2173
|
+
continue;
|
|
1100
2174
|
}
|
|
1101
2175
|
}
|
|
1102
|
-
|
|
2176
|
+
await ingestor.flush();
|
|
2177
|
+
ingestor.destroy();
|
|
2178
|
+
return { platforms: platformResults, totalEvents, totalCost };
|
|
1103
2179
|
}
|
|
1104
|
-
|
|
1105
|
-
const
|
|
1106
|
-
const since = options.since ?? state.lastSyncDate;
|
|
1107
|
-
const until = options.until;
|
|
1108
|
-
const platforms = options.platforms;
|
|
1109
|
-
const output = await runTokscale({ since, until, platforms });
|
|
1110
|
-
const events = mapTokscaleToEvents(output, options.project);
|
|
1111
|
-
if (events.length > 0) {
|
|
1112
|
-
const ingestor = new VerbalIngestor({
|
|
1113
|
-
apiKey: config.apiKey,
|
|
1114
|
-
endpoint: config.ingestUrl,
|
|
1115
|
-
hmacSecret: config.hmacSecret,
|
|
1116
|
-
orgId: config.orgId,
|
|
1117
|
-
batchSize: config.batchSize,
|
|
1118
|
-
flushIntervalMs: config.flushIntervalMs,
|
|
1119
|
-
httpTimeoutMs: config.httpTimeoutMs
|
|
1120
|
-
});
|
|
1121
|
-
for (const event of events) {
|
|
1122
|
-
await ingestor.track(event);
|
|
1123
|
-
}
|
|
1124
|
-
await ingestor.flush();
|
|
1125
|
-
}
|
|
1126
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
1127
|
-
const newState = {
|
|
1128
|
-
lastSyncDate: output.metadata.endDate ?? today,
|
|
1129
|
-
lastSyncTimestamp: new Date().toISOString(),
|
|
1130
|
-
platformsSynced: platforms
|
|
1131
|
-
};
|
|
1132
|
-
saveTokscaleState(config.stateDir, newState);
|
|
1133
|
-
const totalCost = events.reduce((sum, e) => sum + (e.cost ?? 0), 0);
|
|
2180
|
+
function mapParsedEventToUsageEvent(event, project) {
|
|
2181
|
+
const cost = event.billing_type === "subscription" ? 0 : calculateCost(event.model, event.tokens_in, event.tokens_out, event.cache_read_tokens, event.cache_write_tokens);
|
|
1134
2182
|
return {
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
2183
|
+
event_id: event.event_id,
|
|
2184
|
+
event_type: "usage",
|
|
2185
|
+
provider: event.provider,
|
|
2186
|
+
model: event.model,
|
|
2187
|
+
tokens_in: event.tokens_in,
|
|
2188
|
+
tokens_out: event.tokens_out,
|
|
2189
|
+
total_tokens: event.tokens_in + event.tokens_out,
|
|
2190
|
+
cache_read_tokens: event.cache_read_tokens,
|
|
2191
|
+
cache_write_tokens: event.cache_write_tokens,
|
|
2192
|
+
cost,
|
|
2193
|
+
billing_type: event.billing_type,
|
|
2194
|
+
timestamp: event.timestamp,
|
|
2195
|
+
session_id: event.session_id,
|
|
2196
|
+
request_id: event.request_id,
|
|
2197
|
+
tags: event.tags,
|
|
2198
|
+
...project ? { project } : {},
|
|
2199
|
+
prompt: event.prompt,
|
|
2200
|
+
response: event.response,
|
|
2201
|
+
metadata: {
|
|
2202
|
+
...event.metadata,
|
|
2203
|
+
import_source: "native-parser"
|
|
2204
|
+
}
|
|
1138
2205
|
};
|
|
1139
2206
|
}
|
|
1140
|
-
var
|
|
2207
|
+
var init_orchestrator = __esm(() => {
|
|
1141
2208
|
init_ingestor();
|
|
2209
|
+
init_pricing();
|
|
1142
2210
|
init_state();
|
|
2211
|
+
init_registry();
|
|
2212
|
+
init_state2();
|
|
1143
2213
|
});
|
|
1144
2214
|
|
|
1145
2215
|
// src/update-check/check.ts
|
|
@@ -1148,9 +2218,9 @@ __export(exports_check, {
|
|
|
1148
2218
|
checkForUpdate: () => checkForUpdate
|
|
1149
2219
|
});
|
|
1150
2220
|
import { readFileSync as readFileSync8, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "node:fs";
|
|
1151
|
-
import { join as
|
|
2221
|
+
import { join as join13 } from "node:path";
|
|
1152
2222
|
function getCachePath() {
|
|
1153
|
-
return
|
|
2223
|
+
return join13(getCredentialsDir(), CACHE_FILE);
|
|
1154
2224
|
}
|
|
1155
2225
|
function readCache() {
|
|
1156
2226
|
try {
|
|
@@ -1240,9 +2310,9 @@ var exports_import2 = {};
|
|
|
1240
2310
|
__export(exports_import2, {
|
|
1241
2311
|
runImport: () => runImport
|
|
1242
2312
|
});
|
|
1243
|
-
import { existsSync as
|
|
1244
|
-
import { join as
|
|
1245
|
-
import { homedir as
|
|
2313
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
2314
|
+
import { join as join14 } from "node:path";
|
|
2315
|
+
import { homedir as homedir12 } from "node:os";
|
|
1246
2316
|
import { createInterface as createInterface3 } from "node:readline";
|
|
1247
2317
|
async function runImport() {
|
|
1248
2318
|
console.log(`
|
|
@@ -1310,10 +2380,9 @@ async function runImport() {
|
|
|
1310
2380
|
async function handleLocalImport(source, creds) {
|
|
1311
2381
|
console.log(`
|
|
1312
2382
|
Importing ${source.id}...`);
|
|
1313
|
-
const {
|
|
1314
|
-
const stateDir =
|
|
1315
|
-
const
|
|
1316
|
-
const result = await syncFromTokscale2({
|
|
2383
|
+
const { syncPlatforms: syncPlatforms2 } = await Promise.resolve().then(() => (init_orchestrator(), exports_orchestrator));
|
|
2384
|
+
const stateDir = join14(homedir12(), ".config", "getverbal");
|
|
2385
|
+
const result = await syncPlatforms2({
|
|
1317
2386
|
apiKey: creds.api_key,
|
|
1318
2387
|
ingestUrl: creds.ingest_url,
|
|
1319
2388
|
batchSize: 100,
|
|
@@ -1321,12 +2390,12 @@ async function handleLocalImport(source, creds) {
|
|
|
1321
2390
|
httpTimeoutMs: 30000,
|
|
1322
2391
|
stateDir
|
|
1323
2392
|
}, {
|
|
1324
|
-
platforms: [
|
|
2393
|
+
platforms: [source.id]
|
|
1325
2394
|
});
|
|
1326
|
-
console.log(import_picocolors6.default.green(` ✓ ${result.
|
|
2395
|
+
console.log(import_picocolors6.default.green(` ✓ ${result.totalEvents} events imported from ${source.id}`));
|
|
1327
2396
|
}
|
|
1328
2397
|
async function handleFileImport(filePath, source, creds) {
|
|
1329
|
-
if (!
|
|
2398
|
+
if (!existsSync9(filePath)) {
|
|
1330
2399
|
console.error(import_picocolors6.default.red(` File not found: ${filePath}`));
|
|
1331
2400
|
process.exitCode = 1;
|
|
1332
2401
|
return;
|
|
@@ -1371,7 +2440,7 @@ var exports_update = {};
|
|
|
1371
2440
|
__export(exports_update, {
|
|
1372
2441
|
runUpdate: () => runUpdate
|
|
1373
2442
|
});
|
|
1374
|
-
import { execSync as
|
|
2443
|
+
import { execSync as execSync8 } from "node:child_process";
|
|
1375
2444
|
async function runUpdate(currentVersion) {
|
|
1376
2445
|
console.log(`
|
|
1377
2446
|
${import_picocolors7.default.bold("Verbal")} — Update
|
|
@@ -1405,7 +2474,7 @@ async function runUpdate(currentVersion) {
|
|
|
1405
2474
|
const installCmd = pm === "bun" ? `bun install -g @getverbal/cli@${latest}` : `npm install -g @getverbal/cli@${latest}`;
|
|
1406
2475
|
console.log(` Running: ${import_picocolors7.default.dim(installCmd)}`);
|
|
1407
2476
|
try {
|
|
1408
|
-
|
|
2477
|
+
execSync8(installCmd, { stdio: "inherit" });
|
|
1409
2478
|
console.log(import_picocolors7.default.green(`
|
|
1410
2479
|
Updated getverbal ${currentVersion} → ${latest}`));
|
|
1411
2480
|
} catch {
|
|
@@ -1417,7 +2486,7 @@ async function runUpdate(currentVersion) {
|
|
|
1417
2486
|
}
|
|
1418
2487
|
function detectPackageManager() {
|
|
1419
2488
|
try {
|
|
1420
|
-
const which =
|
|
2489
|
+
const which = execSync8("which getverbal", { encoding: "utf-8" }).trim();
|
|
1421
2490
|
if (which.includes(".bun/"))
|
|
1422
2491
|
return "bun";
|
|
1423
2492
|
} catch {}
|
|
@@ -1625,10 +2694,10 @@ function assignProp(target, prop, value) {
|
|
|
1625
2694
|
configurable: true
|
|
1626
2695
|
});
|
|
1627
2696
|
}
|
|
1628
|
-
function getElementAtPath(obj,
|
|
1629
|
-
if (!
|
|
2697
|
+
function getElementAtPath(obj, path11) {
|
|
2698
|
+
if (!path11)
|
|
1630
2699
|
return obj;
|
|
1631
|
-
return
|
|
2700
|
+
return path11.reduce((acc, key) => acc?.[key], obj);
|
|
1632
2701
|
}
|
|
1633
2702
|
function promiseAllObject(promisesObj) {
|
|
1634
2703
|
const keys = Object.keys(promisesObj);
|
|
@@ -1874,11 +2943,11 @@ function aborted(x, startIndex = 0) {
|
|
|
1874
2943
|
}
|
|
1875
2944
|
return false;
|
|
1876
2945
|
}
|
|
1877
|
-
function prefixIssues(
|
|
2946
|
+
function prefixIssues(path11, issues) {
|
|
1878
2947
|
return issues.map((iss) => {
|
|
1879
2948
|
var _a;
|
|
1880
2949
|
(_a = iss).path ?? (_a.path = []);
|
|
1881
|
-
iss.path.unshift(
|
|
2950
|
+
iss.path.unshift(path11);
|
|
1882
2951
|
return iss;
|
|
1883
2952
|
});
|
|
1884
2953
|
}
|
|
@@ -10394,8 +11463,8 @@ var require_utils = __commonJS((exports, module) => {
|
|
|
10394
11463
|
}
|
|
10395
11464
|
return ind;
|
|
10396
11465
|
}
|
|
10397
|
-
function removeDotSegments(
|
|
10398
|
-
let input =
|
|
11466
|
+
function removeDotSegments(path11) {
|
|
11467
|
+
let input = path11;
|
|
10399
11468
|
const output = [];
|
|
10400
11469
|
let nextSlash = -1;
|
|
10401
11470
|
let len = 0;
|
|
@@ -10585,8 +11654,8 @@ var require_schemes = __commonJS((exports, module) => {
|
|
|
10585
11654
|
wsComponent.secure = undefined;
|
|
10586
11655
|
}
|
|
10587
11656
|
if (wsComponent.resourceName) {
|
|
10588
|
-
const [
|
|
10589
|
-
wsComponent.path =
|
|
11657
|
+
const [path11, query] = wsComponent.resourceName.split("?");
|
|
11658
|
+
wsComponent.path = path11 && path11 !== "/" ? path11 : undefined;
|
|
10590
11659
|
wsComponent.query = query;
|
|
10591
11660
|
wsComponent.resourceName = undefined;
|
|
10592
11661
|
}
|
|
@@ -13730,12 +14799,12 @@ var require_dist = __commonJS((exports, module) => {
|
|
|
13730
14799
|
throw new Error(`Unknown format "${name}"`);
|
|
13731
14800
|
return f;
|
|
13732
14801
|
};
|
|
13733
|
-
function addFormats(ajv, list,
|
|
14802
|
+
function addFormats(ajv, list, fs12, exportName) {
|
|
13734
14803
|
var _a;
|
|
13735
14804
|
var _b;
|
|
13736
14805
|
(_a = (_b = ajv.opts.code).formats) !== null && _a !== undefined || (_b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`);
|
|
13737
14806
|
for (const f of list)
|
|
13738
|
-
ajv.addFormat(f,
|
|
14807
|
+
ajv.addFormat(f, fs12[f]);
|
|
13739
14808
|
}
|
|
13740
14809
|
module.exports = exports = formatsPlugin;
|
|
13741
14810
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -14866,8 +15935,8 @@ var exports_server = {};
|
|
|
14866
15935
|
__export(exports_server, {
|
|
14867
15936
|
startMcpServer: () => startMcpServer
|
|
14868
15937
|
});
|
|
14869
|
-
import
|
|
14870
|
-
import
|
|
15938
|
+
import os9 from "node:os";
|
|
15939
|
+
import path11 from "node:path";
|
|
14871
15940
|
function ensureString(value) {
|
|
14872
15941
|
if (typeof value === "string" && value.trim().length > 0)
|
|
14873
15942
|
return value;
|
|
@@ -14902,7 +15971,7 @@ var init_server3 = __esm(() => {
|
|
|
14902
15971
|
init_types();
|
|
14903
15972
|
init_ingestor();
|
|
14904
15973
|
init_tools();
|
|
14905
|
-
|
|
15974
|
+
init_orchestrator();
|
|
14906
15975
|
init_session_tracker();
|
|
14907
15976
|
config2 = {
|
|
14908
15977
|
apiKey: process.env.VERBAL_API_KEY ?? "",
|
|
@@ -15128,9 +16197,35 @@ var init_server3 = __esm(() => {
|
|
|
15128
16197
|
}
|
|
15129
16198
|
}
|
|
15130
16199
|
},
|
|
16200
|
+
{
|
|
16201
|
+
name: "sync_platforms",
|
|
16202
|
+
description: "Sync usage data from local AI tool sessions using native parsers. Scans Claude Code, Codex, Cursor, Gemini, and other supported platforms directly without requiring external tools.",
|
|
16203
|
+
inputSchema: {
|
|
16204
|
+
type: "object",
|
|
16205
|
+
properties: {
|
|
16206
|
+
since: {
|
|
16207
|
+
type: "string",
|
|
16208
|
+
description: "Start date (YYYY-MM-DD). Defaults to last sync date."
|
|
16209
|
+
},
|
|
16210
|
+
until: {
|
|
16211
|
+
type: "string",
|
|
16212
|
+
description: "End date (YYYY-MM-DD). Defaults to today."
|
|
16213
|
+
},
|
|
16214
|
+
platforms: {
|
|
16215
|
+
type: "array",
|
|
16216
|
+
items: { type: "string" },
|
|
16217
|
+
description: "Platforms to sync (e.g. claude-code, codex, cursor, gemini). Defaults to all."
|
|
16218
|
+
},
|
|
16219
|
+
project: {
|
|
16220
|
+
type: "string",
|
|
16221
|
+
description: "Project identifier to tag events with."
|
|
16222
|
+
}
|
|
16223
|
+
}
|
|
16224
|
+
}
|
|
16225
|
+
},
|
|
15131
16226
|
{
|
|
15132
16227
|
name: "sync_tokscale",
|
|
15133
|
-
description: "Sync usage data from local AI tool sessions
|
|
16228
|
+
description: "[Deprecated: use sync_platforms] Sync usage data from local AI tool sessions. Alias for sync_platforms.",
|
|
15134
16229
|
inputSchema: {
|
|
15135
16230
|
type: "object",
|
|
15136
16231
|
properties: {
|
|
@@ -15145,7 +16240,7 @@ var init_server3 = __esm(() => {
|
|
|
15145
16240
|
platforms: {
|
|
15146
16241
|
type: "array",
|
|
15147
16242
|
items: { type: "string" },
|
|
15148
|
-
description: "Platforms to sync (e.g. claude, codex, cursor). Defaults to all."
|
|
16243
|
+
description: "Platforms to sync (e.g. claude-code, codex, cursor, gemini). Defaults to all."
|
|
15149
16244
|
},
|
|
15150
16245
|
project: {
|
|
15151
16246
|
type: "string",
|
|
@@ -15400,7 +16495,7 @@ var init_server3 = __esm(() => {
|
|
|
15400
16495
|
]
|
|
15401
16496
|
};
|
|
15402
16497
|
}
|
|
15403
|
-
if (name === "sync_tokscale") {
|
|
16498
|
+
if (name === "sync_platforms" || name === "sync_tokscale") {
|
|
15404
16499
|
const syncConfig = {
|
|
15405
16500
|
apiKey: config2.apiKey,
|
|
15406
16501
|
ingestUrl: config2.endpoint,
|
|
@@ -15409,7 +16504,7 @@ var init_server3 = __esm(() => {
|
|
|
15409
16504
|
batchSize: config2.batchSize,
|
|
15410
16505
|
flushIntervalMs: 0,
|
|
15411
16506
|
httpTimeoutMs: config2.httpTimeoutMs,
|
|
15412
|
-
stateDir:
|
|
16507
|
+
stateDir: path11.join(os9.homedir(), ".claude", "verbal-logs")
|
|
15413
16508
|
};
|
|
15414
16509
|
const syncOptions = {
|
|
15415
16510
|
since: typeof args.since === "string" ? args.since : undefined,
|
|
@@ -15417,12 +16512,15 @@ var init_server3 = __esm(() => {
|
|
|
15417
16512
|
platforms: Array.isArray(args.platforms) ? args.platforms.filter((p) => typeof p === "string") : undefined,
|
|
15418
16513
|
project: typeof args.project === "string" ? args.project : config2.defaultProject
|
|
15419
16514
|
};
|
|
15420
|
-
const result = await
|
|
16515
|
+
const result = await syncPlatforms(syncConfig, syncOptions);
|
|
16516
|
+
const lines = result.platforms.map((p) => p.error ? ` ${p.id}: ERROR — ${p.error}` : ` ${p.id}: ${p.events} events, $${p.totalCost.toFixed(2)}`);
|
|
15421
16517
|
return {
|
|
15422
16518
|
content: [
|
|
15423
16519
|
{
|
|
15424
16520
|
type: "text",
|
|
15425
|
-
text: `
|
|
16521
|
+
text: `Platform sync complete: ${result.totalEvents} events, $${result.totalCost.toFixed(2)}
|
|
16522
|
+
${lines.join(`
|
|
16523
|
+
`)}`
|
|
15426
16524
|
}
|
|
15427
16525
|
]
|
|
15428
16526
|
};
|
|
@@ -15544,9 +16642,9 @@ async function runMcpServe() {
|
|
|
15544
16642
|
}
|
|
15545
16643
|
|
|
15546
16644
|
// src/agent-hooks/config.ts
|
|
15547
|
-
import
|
|
15548
|
-
import
|
|
15549
|
-
import
|
|
16645
|
+
import fs12 from "node:fs";
|
|
16646
|
+
import os10 from "node:os";
|
|
16647
|
+
import path12 from "node:path";
|
|
15550
16648
|
function loadHookConfig(options = {}) {
|
|
15551
16649
|
const env = options.env ?? process.env;
|
|
15552
16650
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -15562,6 +16660,7 @@ function loadHookConfig(options = {}) {
|
|
|
15562
16660
|
apiKey,
|
|
15563
16661
|
ingestUrl,
|
|
15564
16662
|
traceUrl,
|
|
16663
|
+
apiUrl: env.VERBAL_API_URL ?? mcpEnv.VERBAL_API_URL ?? deriveApiUrl(ingestUrl),
|
|
15565
16664
|
hmacSecret: env.VERBAL_HMAC_SECRET ?? mcpEnv.VERBAL_HMAC_SECRET,
|
|
15566
16665
|
orgId: env.VERBAL_ORG_ID ?? mcpEnv.VERBAL_ORG_ID,
|
|
15567
16666
|
defaultProject: env.VERBAL_DEFAULT_PROJECT ?? mcpEnv.VERBAL_DEFAULT_PROJECT,
|
|
@@ -15578,8 +16677,8 @@ function loadHookConfig(options = {}) {
|
|
|
15578
16677
|
gitContextEnabled: parseBoolean(env.VERBAL_GIT_CONTEXT_ENABLED ?? mcpEnv.VERBAL_GIT_CONTEXT_ENABLED, DEFAULTS.gitContextEnabled),
|
|
15579
16678
|
costCalcEnabled: parseBoolean(env.VERBAL_COST_CALC_ENABLED ?? mcpEnv.VERBAL_COST_CALC_ENABLED, DEFAULTS.costCalcEnabled),
|
|
15580
16679
|
billingType: parseBillingType(env.VERBAL_BILLING_TYPE ?? mcpEnv.VERBAL_BILLING_TYPE),
|
|
15581
|
-
claudeStateDir: env.VERBAL_CLAUDE_STATE_DIR ??
|
|
15582
|
-
codexStateDir: env.VERBAL_CODEX_STATE_DIR ??
|
|
16680
|
+
claudeStateDir: env.VERBAL_CLAUDE_STATE_DIR ?? path12.join(os10.homedir(), ".claude", "verbal-logs"),
|
|
16681
|
+
codexStateDir: env.VERBAL_CODEX_STATE_DIR ?? path12.join(os10.homedir(), ".codex", "verbal-logs")
|
|
15583
16682
|
};
|
|
15584
16683
|
}
|
|
15585
16684
|
function redactText(value, patterns) {
|
|
@@ -15637,7 +16736,7 @@ function buildRedactionPatterns(value, disableDefault) {
|
|
|
15637
16736
|
}
|
|
15638
16737
|
function loadMcpEnv(configPath) {
|
|
15639
16738
|
try {
|
|
15640
|
-
const raw =
|
|
16739
|
+
const raw = fs12.readFileSync(configPath, "utf8");
|
|
15641
16740
|
const parsed = JSON.parse(raw);
|
|
15642
16741
|
return parsed.mcpServers?.verbal?.env ?? {};
|
|
15643
16742
|
} catch {
|
|
@@ -15647,15 +16746,26 @@ function loadMcpEnv(configPath) {
|
|
|
15647
16746
|
function findMcpConfig2(startDir) {
|
|
15648
16747
|
let current = startDir;
|
|
15649
16748
|
for (;; ) {
|
|
15650
|
-
const candidate =
|
|
15651
|
-
if (
|
|
16749
|
+
const candidate = path12.join(current, ".mcp.json");
|
|
16750
|
+
if (fs12.existsSync(candidate))
|
|
15652
16751
|
return candidate;
|
|
15653
|
-
const parent =
|
|
16752
|
+
const parent = path12.dirname(current);
|
|
15654
16753
|
if (parent === current)
|
|
15655
16754
|
return null;
|
|
15656
16755
|
current = parent;
|
|
15657
16756
|
}
|
|
15658
16757
|
}
|
|
16758
|
+
function deriveApiUrl(ingestUrl) {
|
|
16759
|
+
if (!ingestUrl)
|
|
16760
|
+
return "";
|
|
16761
|
+
try {
|
|
16762
|
+
const url = new URL(ingestUrl);
|
|
16763
|
+
url.pathname = "";
|
|
16764
|
+
return url.origin;
|
|
16765
|
+
} catch {
|
|
16766
|
+
return ingestUrl.replace(/\/api\/v1\/ingest\/?$/, "").replace(/\/+$/, "");
|
|
16767
|
+
}
|
|
16768
|
+
}
|
|
15659
16769
|
function deriveTraceUrl(ingestUrl) {
|
|
15660
16770
|
if (!ingestUrl)
|
|
15661
16771
|
return "";
|
|
@@ -15859,27 +16969,27 @@ var init_tool_extraction = __esm(() => {
|
|
|
15859
16969
|
});
|
|
15860
16970
|
|
|
15861
16971
|
// src/agent-hooks/claude.ts
|
|
15862
|
-
import
|
|
16972
|
+
import fs13 from "node:fs";
|
|
15863
16973
|
async function ingestClaudeHookInput(input, config3) {
|
|
15864
16974
|
if (input.stop_hook_active)
|
|
15865
16975
|
return;
|
|
15866
16976
|
if (!input.session_id || !input.transcript_path) {
|
|
15867
16977
|
throw new Error("Claude hook input missing session_id or transcript_path");
|
|
15868
16978
|
}
|
|
15869
|
-
if (!
|
|
16979
|
+
if (!fs13.existsSync(input.transcript_path)) {
|
|
15870
16980
|
throw new Error(`Transcript file not found: ${input.transcript_path}`);
|
|
15871
16981
|
}
|
|
15872
|
-
const entries =
|
|
16982
|
+
const entries = readJsonLines2(input.transcript_path);
|
|
15873
16983
|
const state = loadClaudeState(config3.claudeStateDir);
|
|
15874
16984
|
const lastId = state[input.session_id];
|
|
15875
|
-
const startIndex = lastId ?
|
|
16985
|
+
const startIndex = lastId ? findEntryIndex2(entries, lastId) + 1 : 0;
|
|
15876
16986
|
const newEntries = entries.slice(Math.max(startIndex, 0));
|
|
15877
16987
|
if (newEntries.length === 0)
|
|
15878
16988
|
return;
|
|
15879
16989
|
let lastUserContent;
|
|
15880
16990
|
const ingestor2 = createIngestor(config3);
|
|
15881
16991
|
for (const entry of newEntries) {
|
|
15882
|
-
const { role, content } =
|
|
16992
|
+
const { role, content } = extractMessage3(entry);
|
|
15883
16993
|
if (role === "user" || role === "human") {
|
|
15884
16994
|
lastUserContent = content;
|
|
15885
16995
|
continue;
|
|
@@ -15892,7 +17002,7 @@ async function ingestClaudeHookInput(input, config3) {
|
|
|
15892
17002
|
const tokensOut = usage.output_tokens ?? 0;
|
|
15893
17003
|
if (!tokensIn && !tokensOut)
|
|
15894
17004
|
continue;
|
|
15895
|
-
const eventId =
|
|
17005
|
+
const eventId = getEntryId2(entry);
|
|
15896
17006
|
const event = {
|
|
15897
17007
|
event_id: eventId,
|
|
15898
17008
|
event_type: "usage",
|
|
@@ -16019,24 +17129,24 @@ async function ingestClaudeHookInput(input, config3) {
|
|
|
16019
17129
|
await sendTrace(config3, input.session_id, "claude-tool-calls", toolTraceEvents);
|
|
16020
17130
|
}
|
|
16021
17131
|
const lastEntry = newEntries[newEntries.length - 1];
|
|
16022
|
-
const lastEntryId =
|
|
17132
|
+
const lastEntryId = getEntryId2(lastEntry);
|
|
16023
17133
|
if (lastEntryId) {
|
|
16024
17134
|
state[input.session_id] = lastEntryId;
|
|
16025
17135
|
saveClaudeState(config3.claudeStateDir, state);
|
|
16026
17136
|
}
|
|
16027
17137
|
}
|
|
16028
|
-
function
|
|
16029
|
-
const content =
|
|
17138
|
+
function readJsonLines2(filePath) {
|
|
17139
|
+
const content = fs13.readFileSync(filePath, "utf8");
|
|
16030
17140
|
return content.split(`
|
|
16031
17141
|
`).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
|
|
16032
17142
|
}
|
|
16033
|
-
function
|
|
17143
|
+
function extractMessage3(entry) {
|
|
16034
17144
|
const message = entry.message;
|
|
16035
17145
|
const role = message?.role ?? entry.type;
|
|
16036
|
-
const content =
|
|
17146
|
+
const content = flattenContent2(message?.content ?? entry.content);
|
|
16037
17147
|
return { role, content };
|
|
16038
17148
|
}
|
|
16039
|
-
function
|
|
17149
|
+
function flattenContent2(content) {
|
|
16040
17150
|
if (!content)
|
|
16041
17151
|
return;
|
|
16042
17152
|
if (typeof content === "string")
|
|
@@ -16055,26 +17165,26 @@ function flattenContent(content) {
|
|
|
16055
17165
|
}
|
|
16056
17166
|
return;
|
|
16057
17167
|
}
|
|
16058
|
-
function
|
|
17168
|
+
function getEntryId2(entry) {
|
|
16059
17169
|
return entry.uuid ?? entry.message?.id ?? entry.id ?? entry.messageId;
|
|
16060
17170
|
}
|
|
16061
|
-
function
|
|
16062
|
-
return entries.findIndex((entry) =>
|
|
17171
|
+
function findEntryIndex2(entries, id) {
|
|
17172
|
+
return entries.findIndex((entry) => getEntryId2(entry) === id);
|
|
16063
17173
|
}
|
|
16064
17174
|
var init_claude = __esm(() => {
|
|
16065
17175
|
init_config();
|
|
16066
17176
|
init_ingest();
|
|
16067
|
-
|
|
17177
|
+
init_state2();
|
|
16068
17178
|
init_tool_extraction();
|
|
16069
17179
|
});
|
|
16070
17180
|
|
|
16071
17181
|
// src/agent-hooks/codex.ts
|
|
16072
|
-
import
|
|
16073
|
-
import
|
|
16074
|
-
import
|
|
17182
|
+
import fs14 from "node:fs";
|
|
17183
|
+
import path13 from "node:path";
|
|
17184
|
+
import os11 from "node:os";
|
|
16075
17185
|
async function ingestCodexSessions(config3, options = {}) {
|
|
16076
|
-
const baseDir = options.path ??
|
|
16077
|
-
const files =
|
|
17186
|
+
const baseDir = options.path ?? path13.join(os11.homedir(), ".codex", "sessions");
|
|
17187
|
+
const files = findSessionFiles2(baseDir);
|
|
16078
17188
|
if (files.length === 0)
|
|
16079
17189
|
return;
|
|
16080
17190
|
const state = loadCodexState(config3.codexStateDir);
|
|
@@ -16086,11 +17196,11 @@ async function ingestCodexSessions(config3, options = {}) {
|
|
|
16086
17196
|
saveCodexState(config3.codexStateDir, state);
|
|
16087
17197
|
}
|
|
16088
17198
|
async function watchCodexSessions(config3, options = {}) {
|
|
16089
|
-
const baseDir = options.path ??
|
|
17199
|
+
const baseDir = options.path ?? path13.join(os11.homedir(), ".codex", "sessions");
|
|
16090
17200
|
const intervalMs = options.intervalMs ?? 3000;
|
|
16091
17201
|
const state = loadCodexState(config3.codexStateDir);
|
|
16092
17202
|
setInterval(async () => {
|
|
16093
|
-
const files =
|
|
17203
|
+
const files = findSessionFiles2(baseDir);
|
|
16094
17204
|
for (const filePath of files) {
|
|
16095
17205
|
const updated = await ingestCodexFile(filePath, config3, state.files[filePath]);
|
|
16096
17206
|
if (updated)
|
|
@@ -16100,7 +17210,7 @@ async function watchCodexSessions(config3, options = {}) {
|
|
|
16100
17210
|
}, intervalMs);
|
|
16101
17211
|
}
|
|
16102
17212
|
async function ingestCodexFile(filePath, config3, fileState) {
|
|
16103
|
-
const { entries, sessionId } =
|
|
17213
|
+
const { entries, sessionId } = readCodexFile2(filePath);
|
|
16104
17214
|
if (entries.length === 0)
|
|
16105
17215
|
return null;
|
|
16106
17216
|
const state = fileState ?? { lastIndex: -1 };
|
|
@@ -16156,7 +17266,7 @@ async function ingestCodexFile(filePath, config3, fileState) {
|
|
|
16156
17266
|
}
|
|
16157
17267
|
continue;
|
|
16158
17268
|
}
|
|
16159
|
-
const message =
|
|
17269
|
+
const message = extractMessage4(entry);
|
|
16160
17270
|
if (message) {
|
|
16161
17271
|
if (message.role === "user") {
|
|
16162
17272
|
lastUserContent = message.content;
|
|
@@ -16175,7 +17285,7 @@ async function ingestCodexFile(filePath, config3, fileState) {
|
|
|
16175
17285
|
if (pending && !lastTokenUsage) {
|
|
16176
17286
|
if (config3.allowTokenEstimates) {
|
|
16177
17287
|
const pendingEvent = pending.event;
|
|
16178
|
-
const { tokensIn, tokensOut } =
|
|
17288
|
+
const { tokensIn, tokensOut } = estimateTokens2(pending.prompt, pending.response);
|
|
16179
17289
|
pendingEvent.tokens_in = tokensIn;
|
|
16180
17290
|
pendingEvent.tokens_out = tokensOut;
|
|
16181
17291
|
pendingEvent.total_tokens = tokensIn + tokensOut;
|
|
@@ -16216,7 +17326,7 @@ async function ingestCodexFile(filePath, config3, fileState) {
|
|
|
16216
17326
|
lastTokenUsage = undefined;
|
|
16217
17327
|
events.push(event);
|
|
16218
17328
|
} else if (config3.allowTokenEstimates) {
|
|
16219
|
-
const { tokensIn, tokensOut } =
|
|
17329
|
+
const { tokensIn, tokensOut } = estimateTokens2(lastUserContent, message.content);
|
|
16220
17330
|
event.tokens_in = tokensIn;
|
|
16221
17331
|
event.tokens_out = tokensOut;
|
|
16222
17332
|
event.total_tokens = tokensIn + tokensOut;
|
|
@@ -16319,17 +17429,17 @@ async function ingestCodexFile(filePath, config3, fileState) {
|
|
|
16319
17429
|
state.pending = pending;
|
|
16320
17430
|
return state;
|
|
16321
17431
|
}
|
|
16322
|
-
function
|
|
16323
|
-
const content =
|
|
17432
|
+
function readCodexFile2(filePath) {
|
|
17433
|
+
const content = fs14.readFileSync(filePath, "utf8").trim();
|
|
16324
17434
|
if (!content)
|
|
16325
|
-
return { entries: [], sessionId:
|
|
17435
|
+
return { entries: [], sessionId: path13.basename(filePath) };
|
|
16326
17436
|
const isJsonl = filePath.endsWith(".jsonl");
|
|
16327
17437
|
if (!isJsonl && content.startsWith("{")) {
|
|
16328
17438
|
try {
|
|
16329
17439
|
const parsed = JSON.parse(content);
|
|
16330
17440
|
return {
|
|
16331
17441
|
entries: parsed.items ?? [],
|
|
16332
|
-
sessionId: parsed.session?.id ??
|
|
17442
|
+
sessionId: parsed.session?.id ?? path13.basename(filePath)
|
|
16333
17443
|
};
|
|
16334
17444
|
} catch {}
|
|
16335
17445
|
}
|
|
@@ -16337,30 +17447,30 @@ function readCodexFile(filePath) {
|
|
|
16337
17447
|
`).filter(Boolean);
|
|
16338
17448
|
const entries = lines.map((line) => JSON.parse(line));
|
|
16339
17449
|
const sessionMeta = entries.find((entry) => entry.type === "session_meta");
|
|
16340
|
-
const sessionId = sessionMeta?.payload?.id ??
|
|
17450
|
+
const sessionId = sessionMeta?.payload?.id ?? path13.basename(filePath);
|
|
16341
17451
|
return { entries, sessionId };
|
|
16342
17452
|
}
|
|
16343
|
-
function
|
|
17453
|
+
function extractMessage4(entry) {
|
|
16344
17454
|
if (entry.type === "response_item" && entry.payload && typeof entry.payload === "object") {
|
|
16345
17455
|
const payload = entry.payload;
|
|
16346
17456
|
if (payload.type === "message") {
|
|
16347
17457
|
const role = payload.role;
|
|
16348
|
-
const content =
|
|
17458
|
+
const content = flattenContent3(payload.content);
|
|
16349
17459
|
if (role)
|
|
16350
17460
|
return { role, content };
|
|
16351
17461
|
}
|
|
16352
17462
|
}
|
|
16353
17463
|
if (entry.type === "message" && entry.role) {
|
|
16354
|
-
return { role: entry.role, content:
|
|
17464
|
+
return { role: entry.role, content: flattenContent3(entry.content) };
|
|
16355
17465
|
}
|
|
16356
17466
|
if (entry.type === "user" || entry.type === "assistant") {
|
|
16357
17467
|
const role = entry.type;
|
|
16358
|
-
const content =
|
|
17468
|
+
const content = flattenContent3(entry.content ?? entry.message);
|
|
16359
17469
|
return { role, content };
|
|
16360
17470
|
}
|
|
16361
17471
|
return null;
|
|
16362
17472
|
}
|
|
16363
|
-
function
|
|
17473
|
+
function flattenContent3(content) {
|
|
16364
17474
|
if (!content)
|
|
16365
17475
|
return;
|
|
16366
17476
|
if (typeof content === "string")
|
|
@@ -16379,19 +17489,19 @@ function flattenContent2(content) {
|
|
|
16379
17489
|
}
|
|
16380
17490
|
return;
|
|
16381
17491
|
}
|
|
16382
|
-
function
|
|
17492
|
+
function estimateTokens2(prompt2, response) {
|
|
16383
17493
|
const tokensIn = prompt2 ? Math.ceil(prompt2.length / 4) : 0;
|
|
16384
17494
|
const tokensOut = response ? Math.ceil(response.length / 4) : 0;
|
|
16385
17495
|
return { tokensIn, tokensOut };
|
|
16386
17496
|
}
|
|
16387
|
-
function
|
|
16388
|
-
if (!
|
|
17497
|
+
function findSessionFiles2(baseDir) {
|
|
17498
|
+
if (!fs14.existsSync(baseDir))
|
|
16389
17499
|
return [];
|
|
16390
17500
|
const results = [];
|
|
16391
17501
|
const walk = (dir) => {
|
|
16392
|
-
const entries =
|
|
17502
|
+
const entries = fs14.readdirSync(dir, { withFileTypes: true });
|
|
16393
17503
|
for (const entry of entries) {
|
|
16394
|
-
const full =
|
|
17504
|
+
const full = path13.join(dir, entry.name);
|
|
16395
17505
|
if (entry.isDirectory()) {
|
|
16396
17506
|
walk(full);
|
|
16397
17507
|
} else if (entry.isFile() && (full.endsWith(".json") || full.endsWith(".jsonl"))) {
|
|
@@ -16402,52 +17512,191 @@ function findSessionFiles(baseDir) {
|
|
|
16402
17512
|
walk(baseDir);
|
|
16403
17513
|
return results;
|
|
16404
17514
|
}
|
|
16405
|
-
var
|
|
17515
|
+
var init_codex2 = __esm(() => {
|
|
16406
17516
|
init_config();
|
|
16407
17517
|
init_ingest();
|
|
16408
|
-
|
|
17518
|
+
init_state2();
|
|
16409
17519
|
init_tool_extraction();
|
|
16410
17520
|
});
|
|
16411
17521
|
|
|
16412
|
-
// src/
|
|
16413
|
-
|
|
16414
|
-
|
|
16415
|
-
|
|
16416
|
-
|
|
16417
|
-
|
|
16418
|
-
|
|
16419
|
-
|
|
16420
|
-
|
|
16421
|
-
|
|
16422
|
-
|
|
16423
|
-
|
|
16424
|
-
|
|
16425
|
-
|
|
16426
|
-
|
|
16427
|
-
|
|
16428
|
-
|
|
16429
|
-
|
|
16430
|
-
|
|
16431
|
-
|
|
16432
|
-
|
|
16433
|
-
|
|
16434
|
-
|
|
16435
|
-
|
|
16436
|
-
|
|
16437
|
-
|
|
16438
|
-
|
|
16439
|
-
|
|
17522
|
+
// src/mcp/preflight.ts
|
|
17523
|
+
var exports_preflight = {};
|
|
17524
|
+
__export(exports_preflight, {
|
|
17525
|
+
runPreflight: () => runPreflight,
|
|
17526
|
+
formatPreflightForTerminal: () => formatPreflightForTerminal,
|
|
17527
|
+
buildSuggestion: () => buildSuggestion,
|
|
17528
|
+
ISSUE_SUGGESTIONS: () => ISSUE_SUGGESTIONS
|
|
17529
|
+
});
|
|
17530
|
+
function buildSuggestion(issues) {
|
|
17531
|
+
const top = issues.slice(0, 2);
|
|
17532
|
+
if (top.length === 0)
|
|
17533
|
+
return "";
|
|
17534
|
+
const tips = top.map((key) => ISSUE_SUGGESTIONS[key]).filter((tip) => Boolean(tip));
|
|
17535
|
+
if (tips.length === 0)
|
|
17536
|
+
return "";
|
|
17537
|
+
if (tips.length === 1)
|
|
17538
|
+
return tips[0];
|
|
17539
|
+
return `${tips[0]} Also: ${tips[1]}`;
|
|
17540
|
+
}
|
|
17541
|
+
async function runPreflight(prompt2, options) {
|
|
17542
|
+
const { config: config3, apiKey, apiUrl, cooldownActive } = options;
|
|
17543
|
+
if (prompt2.trim().length < 20) {
|
|
17544
|
+
return { skipped: true, skip_reason: "short_prompt" };
|
|
17545
|
+
}
|
|
17546
|
+
if (cooldownActive) {
|
|
17547
|
+
return { skipped: true, skip_reason: "cooldown" };
|
|
17548
|
+
}
|
|
17549
|
+
const requestThreshold = config3.enable_llm_rewrite ? config3.threshold : 101;
|
|
17550
|
+
let response;
|
|
17551
|
+
try {
|
|
17552
|
+
response = await fetch(`${apiUrl}/api/v1/optimize`, {
|
|
17553
|
+
method: "POST",
|
|
17554
|
+
headers: {
|
|
17555
|
+
Authorization: `Bearer ${apiKey}`,
|
|
17556
|
+
"Content-Type": "application/json"
|
|
17557
|
+
},
|
|
17558
|
+
body: JSON.stringify({
|
|
17559
|
+
prompt: prompt2,
|
|
17560
|
+
threshold: requestThreshold
|
|
17561
|
+
})
|
|
17562
|
+
});
|
|
17563
|
+
} catch {
|
|
17564
|
+
return { skipped: true, skip_reason: "error" };
|
|
17565
|
+
}
|
|
17566
|
+
if (response.status === 402) {
|
|
17567
|
+
return { skipped: true, skip_reason: "tier_gate" };
|
|
17568
|
+
}
|
|
17569
|
+
if (!response.ok) {
|
|
17570
|
+
return { skipped: true, skip_reason: "error" };
|
|
17571
|
+
}
|
|
17572
|
+
let data;
|
|
17573
|
+
try {
|
|
17574
|
+
data = await response.json();
|
|
17575
|
+
} catch {
|
|
17576
|
+
return { skipped: true, skip_reason: "error" };
|
|
17577
|
+
}
|
|
17578
|
+
const score = data.original_score;
|
|
17579
|
+
const issues = Array.isArray(data.issues) ? data.issues : [];
|
|
17580
|
+
if (score >= config3.threshold) {
|
|
17581
|
+
return {
|
|
17582
|
+
skipped: true,
|
|
17583
|
+
skip_reason: "above_threshold",
|
|
17584
|
+
score,
|
|
17585
|
+
issues
|
|
17586
|
+
};
|
|
17587
|
+
}
|
|
17588
|
+
const suggestion = buildSuggestion(issues);
|
|
17589
|
+
return {
|
|
17590
|
+
skipped: false,
|
|
17591
|
+
score,
|
|
17592
|
+
issues,
|
|
17593
|
+
suggestion,
|
|
17594
|
+
optimized_prompt: config3.enable_llm_rewrite ? data.optimized_prompt : null,
|
|
17595
|
+
improvement_delta: config3.enable_llm_rewrite ? data.improvement_delta : null
|
|
17596
|
+
};
|
|
17597
|
+
}
|
|
17598
|
+
function formatPreflightForTerminal(result) {
|
|
17599
|
+
if (result.skipped)
|
|
17600
|
+
return "";
|
|
17601
|
+
const lines = [];
|
|
17602
|
+
lines.push(`⚡ Prompt score: ${result.score ?? "?"}/100`);
|
|
17603
|
+
if (result.suggestion) {
|
|
17604
|
+
lines.push(`\uD83D\uDCA1 ${result.suggestion}`);
|
|
17605
|
+
}
|
|
17606
|
+
if (result.optimized_prompt) {
|
|
17607
|
+
const delta = result.improvement_delta != null ? ` (+${result.improvement_delta} pts)` : "";
|
|
17608
|
+
lines.push(`✨ Optimized prompt${delta}:`);
|
|
17609
|
+
lines.push("");
|
|
17610
|
+
lines.push(result.optimized_prompt);
|
|
17611
|
+
}
|
|
17612
|
+
return lines.join(`
|
|
17613
|
+
`);
|
|
17614
|
+
}
|
|
17615
|
+
var ISSUE_SUGGESTIONS;
|
|
17616
|
+
var init_preflight = __esm(() => {
|
|
17617
|
+
ISSUE_SUGGESTIONS = {
|
|
17618
|
+
vague_instruction: "Be more specific — describe the exact outcome you want, not just the task category.",
|
|
17619
|
+
no_output_format: 'Specify the output format (e.g. "return a JSON object", "write a numbered list", "respond in markdown").',
|
|
17620
|
+
missing_constraints: "Add constraints (e.g. word count, language, framework version) to narrow the solution space.",
|
|
17621
|
+
no_context: "Include relevant context (e.g. language, tech stack, existing code) so the model has what it needs.",
|
|
17622
|
+
ambiguous_subject: "Clarify the subject — what exactly should be modified, created, or explained?",
|
|
17623
|
+
missing_examples: "Provide an example input/output pair to show the pattern you expect.",
|
|
17624
|
+
overly_broad: "Break this into smaller, focused requests rather than asking for everything at once.",
|
|
17625
|
+
passive_voice: 'Use active, imperative phrasing (e.g. "Generate X" instead of "X should be generated").',
|
|
17626
|
+
no_success_criteria: 'State what "done" looks like so the model can self-verify its response.',
|
|
17627
|
+
redundant_preamble: 'Start with the request — cut the "I want you to act as" or similar preambles.'
|
|
17628
|
+
};
|
|
17629
|
+
});
|
|
17630
|
+
|
|
17631
|
+
// src/agent-hooks/cursor-watcher.ts
|
|
17632
|
+
var exports_cursor_watcher = {};
|
|
17633
|
+
__export(exports_cursor_watcher, {
|
|
17634
|
+
watchCursorWorkspaces: () => watchCursorWorkspaces
|
|
17635
|
+
});
|
|
17636
|
+
async function watchCursorWorkspaces(config3, options = {}) {
|
|
17637
|
+
const intervalMs = options.intervalMs ?? 5000;
|
|
17638
|
+
const parser = new CursorParser;
|
|
17639
|
+
const stateDir = config3.claudeStateDir;
|
|
17640
|
+
const sync = async () => {
|
|
17641
|
+
try {
|
|
17642
|
+
const state = loadPlatformState(stateDir, "cursor");
|
|
17643
|
+
const result = await parser.parse(state, {});
|
|
17644
|
+
if (result.events.length > 0) {
|
|
17645
|
+
const ingestor2 = createIngestor(config3);
|
|
17646
|
+
for (const event of result.events) {
|
|
17647
|
+
const cost = event.billing_type === "subscription" ? 0 : calculateCost(event.model, event.tokens_in, event.tokens_out, event.cache_read_tokens, event.cache_write_tokens);
|
|
17648
|
+
const usageEvent = {
|
|
17649
|
+
event_id: event.event_id,
|
|
17650
|
+
event_type: "usage",
|
|
17651
|
+
provider: event.provider,
|
|
17652
|
+
model: event.model,
|
|
17653
|
+
tokens_in: event.tokens_in,
|
|
17654
|
+
tokens_out: event.tokens_out,
|
|
17655
|
+
total_tokens: event.tokens_in + event.tokens_out,
|
|
17656
|
+
cache_read_tokens: event.cache_read_tokens,
|
|
17657
|
+
cache_write_tokens: event.cache_write_tokens,
|
|
17658
|
+
cost,
|
|
17659
|
+
billing_type: event.billing_type,
|
|
17660
|
+
timestamp: event.timestamp,
|
|
17661
|
+
session_id: event.session_id,
|
|
17662
|
+
request_id: event.request_id,
|
|
17663
|
+
tags: event.tags,
|
|
17664
|
+
metadata: { ...event.metadata, import_source: "cursor-watcher" }
|
|
17665
|
+
};
|
|
17666
|
+
await ingestor2.track(usageEvent);
|
|
17667
|
+
}
|
|
17668
|
+
await ingestor2.flush();
|
|
17669
|
+
ingestor2.destroy();
|
|
17670
|
+
}
|
|
17671
|
+
savePlatformState(stateDir, "cursor", result.newState);
|
|
17672
|
+
if (result.warnings.length > 0) {
|
|
17673
|
+
for (const warning of result.warnings) {
|
|
17674
|
+
console.error(`[Cursor watcher] ${warning}`);
|
|
17675
|
+
}
|
|
17676
|
+
}
|
|
17677
|
+
} catch (err) {
|
|
17678
|
+
console.error("[Cursor watcher] Error:", err instanceof Error ? err.message : err);
|
|
17679
|
+
}
|
|
17680
|
+
};
|
|
17681
|
+
await sync();
|
|
17682
|
+
setInterval(sync, intervalMs);
|
|
16440
17683
|
}
|
|
17684
|
+
var init_cursor_watcher = __esm(() => {
|
|
17685
|
+
init_cursor();
|
|
17686
|
+
init_state();
|
|
17687
|
+
init_ingest();
|
|
17688
|
+
init_pricing();
|
|
17689
|
+
});
|
|
16441
17690
|
|
|
16442
17691
|
// src/agent-hooks/cli.ts
|
|
16443
17692
|
var exports_cli = {};
|
|
16444
17693
|
__export(exports_cli, {
|
|
16445
17694
|
main: () => main
|
|
16446
17695
|
});
|
|
16447
|
-
import
|
|
16448
|
-
import
|
|
16449
|
-
import
|
|
16450
|
-
import { fileURLToPath } from "node:url";
|
|
17696
|
+
import fs15 from "node:fs";
|
|
17697
|
+
import os12 from "node:os";
|
|
17698
|
+
import path14 from "node:path";
|
|
17699
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
16451
17700
|
async function main(args = process.argv) {
|
|
16452
17701
|
const command = args[2];
|
|
16453
17702
|
switch (command) {
|
|
@@ -16457,6 +17706,53 @@ async function main(args = process.argv) {
|
|
|
16457
17706
|
await ingestClaudeHookInput(input, config3);
|
|
16458
17707
|
return;
|
|
16459
17708
|
}
|
|
17709
|
+
case "preflight": {
|
|
17710
|
+
const config3 = loadHookConfig();
|
|
17711
|
+
const flags = parseFlags(args.slice(3));
|
|
17712
|
+
const prompt2 = await readStdinText();
|
|
17713
|
+
if (!prompt2 || prompt2.trim().length === 0) {
|
|
17714
|
+
return;
|
|
17715
|
+
}
|
|
17716
|
+
const { runPreflight: runPreflight2, formatPreflightForTerminal: formatPreflightForTerminal2 } = await Promise.resolve().then(() => (init_preflight(), exports_preflight));
|
|
17717
|
+
const {
|
|
17718
|
+
loadPreflightState: loadPreflightState2,
|
|
17719
|
+
savePreflightState: savePreflightState2,
|
|
17720
|
+
getDefaultStateDir: getDefaultStateDir2,
|
|
17721
|
+
isCooldownActive: isCooldownActive2,
|
|
17722
|
+
updatePreflightStats: updatePreflightStats2,
|
|
17723
|
+
addPendingFeedback: addPendingFeedback2
|
|
17724
|
+
} = await Promise.resolve().then(() => (init_preflight_state(), exports_preflight_state));
|
|
17725
|
+
const stateDir = getDefaultStateDir2();
|
|
17726
|
+
const state = loadPreflightState2(stateDir);
|
|
17727
|
+
const mode = flags.mode ?? state.config.mode;
|
|
17728
|
+
const threshold = flags.threshold ?? state.config.threshold;
|
|
17729
|
+
const result = await runPreflight2(prompt2.trim(), {
|
|
17730
|
+
config: { threshold, mode, enable_llm_rewrite: state.config.enable_llm_rewrite, cooldown_minutes: state.config.cooldown_minutes },
|
|
17731
|
+
apiKey: config3.apiKey,
|
|
17732
|
+
apiUrl: config3.apiUrl,
|
|
17733
|
+
cooldownActive: isCooldownActive2(state)
|
|
17734
|
+
});
|
|
17735
|
+
let updatedState = result.skipped ? updatePreflightStats2(state, "skipped", result.score ?? 0) : updatePreflightStats2(state, "shown", result.score ?? 0);
|
|
17736
|
+
if (!result.skipped && mode !== "silent") {
|
|
17737
|
+
const formatted = formatPreflightForTerminal2(result);
|
|
17738
|
+
if (formatted) {
|
|
17739
|
+
process.stderr.write(formatted + `
|
|
17740
|
+
`);
|
|
17741
|
+
}
|
|
17742
|
+
}
|
|
17743
|
+
if (!result.skipped) {
|
|
17744
|
+
updatedState = addPendingFeedback2(updatedState, {
|
|
17745
|
+
timestamp: new Date().toISOString(),
|
|
17746
|
+
score: result.score ?? 0,
|
|
17747
|
+
action: "dismissed",
|
|
17748
|
+
issues: result.issues ?? []
|
|
17749
|
+
});
|
|
17750
|
+
}
|
|
17751
|
+
savePreflightState2(stateDir, updatedState);
|
|
17752
|
+
process.stdout.write(JSON.stringify(result) + `
|
|
17753
|
+
`);
|
|
17754
|
+
return;
|
|
17755
|
+
}
|
|
16460
17756
|
case "ingest-now": {
|
|
16461
17757
|
const config3 = loadHookConfig();
|
|
16462
17758
|
const flags = parseFlags(args.slice(3));
|
|
@@ -16467,9 +17763,9 @@ async function main(args = process.argv) {
|
|
|
16467
17763
|
const input = { session_id: `manual-${Date.now()}`, transcript_path: flags.path };
|
|
16468
17764
|
await ingestClaudeHookInput(input, config3);
|
|
16469
17765
|
}
|
|
16470
|
-
if (flags.tokscale) {
|
|
16471
|
-
const {
|
|
16472
|
-
const result = await
|
|
17766
|
+
if (flags.sync || flags.tokscale) {
|
|
17767
|
+
const { syncPlatforms: syncPlatforms2 } = await Promise.resolve().then(() => (init_orchestrator(), exports_orchestrator));
|
|
17768
|
+
const result = await syncPlatforms2({
|
|
16473
17769
|
apiKey: config3.apiKey,
|
|
16474
17770
|
ingestUrl: config3.ingestUrl,
|
|
16475
17771
|
hmacSecret: config3.hmacSecret,
|
|
@@ -16484,29 +17780,55 @@ async function main(args = process.argv) {
|
|
|
16484
17780
|
platforms: flags.platforms,
|
|
16485
17781
|
project: config3.defaultProject
|
|
16486
17782
|
});
|
|
16487
|
-
console.log(`
|
|
17783
|
+
console.log(`Platform sync: ${result.totalEvents} events, $${result.totalCost.toFixed(2)} total`);
|
|
17784
|
+
for (const p of result.platforms) {
|
|
17785
|
+
if (p.error)
|
|
17786
|
+
console.log(` ${p.id}: ERROR — ${p.error}`);
|
|
17787
|
+
else
|
|
17788
|
+
console.log(` ${p.id}: ${p.events} events, $${p.totalCost.toFixed(2)}`);
|
|
17789
|
+
}
|
|
16488
17790
|
}
|
|
16489
17791
|
return;
|
|
16490
17792
|
}
|
|
16491
17793
|
case "watch": {
|
|
16492
17794
|
const config3 = loadHookConfig();
|
|
16493
17795
|
const flags = parseFlags(args.slice(3));
|
|
16494
|
-
|
|
16495
|
-
|
|
16496
|
-
|
|
16497
|
-
|
|
16498
|
-
});
|
|
17796
|
+
const watchCodex = flags.codex || !flags.codex && !flags.cursor;
|
|
17797
|
+
const watchCursor = flags.cursor || !flags.codex && !flags.cursor;
|
|
17798
|
+
const promises = [];
|
|
17799
|
+
if (watchCodex) {
|
|
17800
|
+
promises.push(watchCodexSessions(config3, { path: flags.path }));
|
|
17801
|
+
}
|
|
17802
|
+
if (watchCursor) {
|
|
17803
|
+
const { watchCursorWorkspaces: watchCursorWorkspaces2 } = await Promise.resolve().then(() => (init_cursor_watcher(), exports_cursor_watcher));
|
|
17804
|
+
promises.push(watchCursorWorkspaces2(config3));
|
|
17805
|
+
}
|
|
17806
|
+
if (promises.length === 0) {
|
|
17807
|
+
console.log("Nothing to watch. Use --codex or --cursor.");
|
|
17808
|
+
return;
|
|
16499
17809
|
}
|
|
16500
|
-
|
|
17810
|
+
await Promise.race(promises);
|
|
17811
|
+
await new Promise(() => {
|
|
17812
|
+
return;
|
|
17813
|
+
});
|
|
16501
17814
|
return;
|
|
16502
17815
|
}
|
|
16503
17816
|
case "install": {
|
|
16504
17817
|
const flags = parseFlags(args.slice(3));
|
|
16505
17818
|
if (flags.claude) {
|
|
16506
|
-
installClaudeHooks();
|
|
17819
|
+
const { installClaudeHooks: installClaude } = await Promise.resolve().then(() => (init_hooks_installer(), exports_hooks_installer));
|
|
17820
|
+
installClaude();
|
|
17821
|
+
console.log(`Installed Claude hooks in ${path14.join(os12.homedir(), ".claude", "settings.json")}`);
|
|
16507
17822
|
}
|
|
16508
17823
|
if (flags.codex) {
|
|
16509
|
-
|
|
17824
|
+
if (process.platform === "darwin") {
|
|
17825
|
+
const { installUnifiedWatcher: installUnifiedWatcher2 } = await Promise.resolve().then(() => (init_hooks_installer(), exports_hooks_installer));
|
|
17826
|
+
installUnifiedWatcher2();
|
|
17827
|
+
const plistPath = path14.join(os12.homedir(), "Library", "LaunchAgents", "ai.verbal.watcher.plist");
|
|
17828
|
+
console.log(`Wrote LaunchAgent to ${plistPath}`);
|
|
17829
|
+
} else {
|
|
17830
|
+
installCodexLaunchAgent();
|
|
17831
|
+
}
|
|
16510
17832
|
}
|
|
16511
17833
|
return;
|
|
16512
17834
|
}
|
|
@@ -16541,7 +17863,9 @@ function parseFlags(args) {
|
|
|
16541
17863
|
const flags = {
|
|
16542
17864
|
codex: false,
|
|
16543
17865
|
claude: false,
|
|
17866
|
+
cursor: false,
|
|
16544
17867
|
tokscale: false,
|
|
17868
|
+
sync: false,
|
|
16545
17869
|
path: undefined,
|
|
16546
17870
|
since: undefined,
|
|
16547
17871
|
until: undefined,
|
|
@@ -16553,8 +17877,12 @@ function parseFlags(args) {
|
|
|
16553
17877
|
flags.codex = true;
|
|
16554
17878
|
if (arg === "--claude")
|
|
16555
17879
|
flags.claude = true;
|
|
17880
|
+
if (arg === "--cursor")
|
|
17881
|
+
flags.cursor = true;
|
|
16556
17882
|
if (arg === "--tokscale")
|
|
16557
17883
|
flags.tokscale = true;
|
|
17884
|
+
if (arg === "--sync")
|
|
17885
|
+
flags.sync = true;
|
|
16558
17886
|
if (arg === "--path") {
|
|
16559
17887
|
flags.path = args[i + 1];
|
|
16560
17888
|
i++;
|
|
@@ -16572,86 +17900,46 @@ function parseFlags(args) {
|
|
|
16572
17900
|
flags.platforms = raw ? raw.split(",").map((p) => p.trim()).filter(Boolean) : [];
|
|
16573
17901
|
i++;
|
|
16574
17902
|
}
|
|
17903
|
+
if (arg === "--mode") {
|
|
17904
|
+
flags.mode = args[i + 1];
|
|
17905
|
+
i++;
|
|
17906
|
+
}
|
|
17907
|
+
if (arg === "--threshold") {
|
|
17908
|
+
flags.threshold = parseInt(args[i + 1], 10);
|
|
17909
|
+
i++;
|
|
17910
|
+
}
|
|
16575
17911
|
}
|
|
16576
17912
|
return flags;
|
|
16577
17913
|
}
|
|
16578
|
-
function installClaudeHooks() {
|
|
16579
|
-
const settingsPath = path7.join(os6.homedir(), ".claude", "settings.json");
|
|
16580
|
-
const hooksRoot = path7.resolve(__dirname2, "..", "..", "hooks");
|
|
16581
|
-
const stopHook = path7.join(hooksRoot, "ingest-stop-hook.sh");
|
|
16582
|
-
const endHook = path7.join(hooksRoot, "ingest-session-end-hook.sh");
|
|
16583
|
-
const configPath = findMcpConfigPath();
|
|
16584
|
-
const settings = readJson(settingsPath) ?? {};
|
|
16585
|
-
settings.hooks = settings.hooks ?? {};
|
|
16586
|
-
upsertHook(settings, "Stop", stopHook, {
|
|
16587
|
-
timeout: 30,
|
|
16588
|
-
statusMessage: "Tracking usage in Verbal",
|
|
16589
|
-
async: true,
|
|
16590
|
-
env: configPath ? { VERBAL_CONFIG_PATH: configPath } : undefined
|
|
16591
|
-
});
|
|
16592
|
-
upsertHook(settings, "SessionEnd", endHook, {
|
|
16593
|
-
timeout: 30,
|
|
16594
|
-
statusMessage: "Finalizing usage data",
|
|
16595
|
-
env: configPath ? { VERBAL_CONFIG_PATH: configPath } : undefined
|
|
16596
|
-
});
|
|
16597
|
-
const reviewPromptHook = path7.join(hooksRoot, "session-review-prompt.js");
|
|
16598
|
-
upsertHook(settings, "Stop", `node ${reviewPromptHook}`, {
|
|
16599
|
-
timeout: 5,
|
|
16600
|
-
statusMessage: "Checking session review timer"
|
|
16601
|
-
});
|
|
16602
|
-
fs7.mkdirSync(path7.dirname(settingsPath), { recursive: true });
|
|
16603
|
-
fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
16604
|
-
console.log(`Installed Claude hooks in ${settingsPath}`);
|
|
16605
|
-
}
|
|
16606
17914
|
function uninstallClaudeHooks() {
|
|
16607
|
-
const settingsPath =
|
|
16608
|
-
const settings =
|
|
17915
|
+
const settingsPath = path14.join(os12.homedir(), ".claude", "settings.json");
|
|
17916
|
+
const settings = readJson2(settingsPath);
|
|
16609
17917
|
if (!settings?.hooks)
|
|
16610
17918
|
return;
|
|
16611
17919
|
removeHook(settings, "Stop", "ingest-stop-hook.sh");
|
|
16612
17920
|
removeHook(settings, "Stop", "session-review-prompt.js");
|
|
16613
17921
|
removeHook(settings, "SessionEnd", "ingest-session-end-hook.sh");
|
|
16614
|
-
|
|
17922
|
+
fs15.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
16615
17923
|
console.log(`Removed Verbal Claude hooks from ${settingsPath}`);
|
|
16616
17924
|
}
|
|
16617
17925
|
function installCodexLaunchAgent() {
|
|
16618
|
-
const plistPath =
|
|
16619
|
-
const cliPath =
|
|
17926
|
+
const plistPath = path14.join(os12.homedir(), "Library", "LaunchAgents", "ai.verbal.codex-hooks.plist");
|
|
17927
|
+
const cliPath = path14.resolve(__dirname3, "cli.js");
|
|
16620
17928
|
const nodePath = process.execPath;
|
|
16621
|
-
const configPath =
|
|
17929
|
+
const configPath = findMcpConfigPath2() ?? undefined;
|
|
16622
17930
|
const plist = buildCodexLaunchAgentPlist({ nodePath, cliPath, configPath });
|
|
16623
|
-
|
|
16624
|
-
|
|
17931
|
+
fs15.mkdirSync(path14.dirname(plistPath), { recursive: true });
|
|
17932
|
+
fs15.writeFileSync(plistPath, plist);
|
|
16625
17933
|
console.log(`Wrote LaunchAgent to ${plistPath}`);
|
|
16626
17934
|
console.log("Load with: launchctl load ~/Library/LaunchAgents/ai.verbal.codex-hooks.plist");
|
|
16627
17935
|
}
|
|
16628
17936
|
function uninstallCodexLaunchAgent() {
|
|
16629
|
-
const plistPath =
|
|
16630
|
-
if (
|
|
16631
|
-
|
|
17937
|
+
const plistPath = path14.join(os12.homedir(), "Library", "LaunchAgents", "ai.verbal.codex-hooks.plist");
|
|
17938
|
+
if (fs15.existsSync(plistPath)) {
|
|
17939
|
+
fs15.unlinkSync(plistPath);
|
|
16632
17940
|
console.log(`Removed ${plistPath}`);
|
|
16633
17941
|
}
|
|
16634
17942
|
}
|
|
16635
|
-
function upsertHook(settings, hookName, command, options) {
|
|
16636
|
-
const hookEntry = {
|
|
16637
|
-
matcher: "",
|
|
16638
|
-
hooks: [
|
|
16639
|
-
{
|
|
16640
|
-
type: "command",
|
|
16641
|
-
command,
|
|
16642
|
-
...options
|
|
16643
|
-
}
|
|
16644
|
-
]
|
|
16645
|
-
};
|
|
16646
|
-
settings.hooks = settings.hooks ?? {};
|
|
16647
|
-
settings.hooks[hookName] = settings.hooks[hookName] ?? [];
|
|
16648
|
-
const existingIndex = settings.hooks[hookName].findIndex((entry) => entry.hooks?.some((hook) => hook.command.includes(path7.basename(command))));
|
|
16649
|
-
if (existingIndex >= 0) {
|
|
16650
|
-
settings.hooks[hookName][existingIndex] = hookEntry;
|
|
16651
|
-
} else {
|
|
16652
|
-
settings.hooks[hookName].push(hookEntry);
|
|
16653
|
-
}
|
|
16654
|
-
}
|
|
16655
17943
|
function removeHook(settings, hookName, commandFragment) {
|
|
16656
17944
|
if (!settings.hooks?.[hookName])
|
|
16657
17945
|
return;
|
|
@@ -16660,22 +17948,22 @@ function removeHook(settings, hookName, commandFragment) {
|
|
|
16660
17948
|
return !hooks.some((hook) => hook.command.includes(commandFragment));
|
|
16661
17949
|
});
|
|
16662
17950
|
}
|
|
16663
|
-
function
|
|
17951
|
+
function readJson2(filePath) {
|
|
16664
17952
|
try {
|
|
16665
|
-
if (!
|
|
17953
|
+
if (!fs15.existsSync(filePath))
|
|
16666
17954
|
return null;
|
|
16667
|
-
return JSON.parse(
|
|
17955
|
+
return JSON.parse(fs15.readFileSync(filePath, "utf8"));
|
|
16668
17956
|
} catch {
|
|
16669
17957
|
return null;
|
|
16670
17958
|
}
|
|
16671
17959
|
}
|
|
16672
|
-
function
|
|
17960
|
+
function findMcpConfigPath2() {
|
|
16673
17961
|
let current = process.cwd();
|
|
16674
17962
|
for (;; ) {
|
|
16675
|
-
const candidate =
|
|
16676
|
-
if (
|
|
17963
|
+
const candidate = path14.join(current, ".mcp.json");
|
|
17964
|
+
if (fs15.existsSync(candidate))
|
|
16677
17965
|
return candidate;
|
|
16678
|
-
const parent =
|
|
17966
|
+
const parent = path14.dirname(current);
|
|
16679
17967
|
if (parent === current)
|
|
16680
17968
|
return null;
|
|
16681
17969
|
current = parent;
|
|
@@ -16691,24 +17979,35 @@ async function readStdinJson() {
|
|
|
16691
17979
|
const content = Buffer.concat(chunks).toString("utf8");
|
|
16692
17980
|
return JSON.parse(content);
|
|
16693
17981
|
}
|
|
17982
|
+
async function readStdinText() {
|
|
17983
|
+
const chunks = [];
|
|
17984
|
+
for await (const chunk of process.stdin) {
|
|
17985
|
+
chunks.push(Buffer.from(chunk));
|
|
17986
|
+
}
|
|
17987
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
17988
|
+
}
|
|
16694
17989
|
function printHelp() {
|
|
16695
17990
|
console.log(`verbal-hooks usage:
|
|
16696
17991
|
verbal-hooks install --claude --codex
|
|
16697
17992
|
verbal-hooks ingest-now --codex [--path <dir>]
|
|
16698
17993
|
verbal-hooks ingest-now --claude --path <transcript.jsonl>
|
|
16699
|
-
verbal-hooks ingest-now --
|
|
16700
|
-
verbal-hooks
|
|
17994
|
+
verbal-hooks ingest-now --sync [--since YYYY-MM-DD] [--until YYYY-MM-DD] [--platforms claude-code,codex,...]
|
|
17995
|
+
verbal-hooks ingest-now --tokscale [--since YYYY-MM-DD] [--until YYYY-MM-DD] [--platforms claude-code,codex,...]
|
|
17996
|
+
verbal-hooks watch [--codex [--path <dir>]] [--cursor]
|
|
17997
|
+
(no flags = auto-detect, watches all available sources)
|
|
17998
|
+
verbal-hooks preflight [--mode silent|notify|blocking] [--threshold 60]
|
|
17999
|
+
(reads prompt from stdin, prints coaching to stderr)
|
|
16701
18000
|
verbal-hooks status
|
|
16702
18001
|
verbal-hooks uninstall --claude --codex
|
|
16703
18002
|
`);
|
|
16704
18003
|
}
|
|
16705
|
-
var
|
|
18004
|
+
var __filename3, __dirname3;
|
|
16706
18005
|
var init_cli = __esm(() => {
|
|
16707
18006
|
init_config();
|
|
16708
18007
|
init_claude();
|
|
16709
|
-
|
|
16710
|
-
|
|
16711
|
-
|
|
18008
|
+
init_codex2();
|
|
18009
|
+
__filename3 = fileURLToPath2(import.meta.url);
|
|
18010
|
+
__dirname3 = path14.dirname(__filename3);
|
|
16712
18011
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
16713
18012
|
main(process.argv).catch((error2) => {
|
|
16714
18013
|
console.error(error2 instanceof Error ? error2.message : error2);
|
|
@@ -16733,10 +18032,10 @@ __export(exports_notify, {
|
|
|
16733
18032
|
printUpdateNotice: () => printUpdateNotice
|
|
16734
18033
|
});
|
|
16735
18034
|
import { readFileSync as readFileSync10 } from "node:fs";
|
|
16736
|
-
import { join as
|
|
18035
|
+
import { join as join15 } from "node:path";
|
|
16737
18036
|
function printUpdateNotice(currentVersion) {
|
|
16738
18037
|
try {
|
|
16739
|
-
const cachePath =
|
|
18038
|
+
const cachePath = join15(getCredentialsDir(), "update-check.json");
|
|
16740
18039
|
const raw = readFileSync10(cachePath, "utf-8");
|
|
16741
18040
|
const cache = JSON.parse(raw);
|
|
16742
18041
|
if (compareSemver(currentVersion, cache.latest) >= 0)
|
|
@@ -16773,10 +18072,10 @@ var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
|
16773
18072
|
// src/commands/init.ts
|
|
16774
18073
|
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
16775
18074
|
import { createInterface } from "node:readline/promises";
|
|
16776
|
-
import { execSync as
|
|
16777
|
-
import { existsSync as
|
|
16778
|
-
import { join as
|
|
16779
|
-
import { homedir as
|
|
18075
|
+
import { execSync as execSync7 } from "node:child_process";
|
|
18076
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
|
|
18077
|
+
import { join as join12 } from "node:path";
|
|
18078
|
+
import { homedir as homedir11 } from "node:os";
|
|
16780
18079
|
|
|
16781
18080
|
// src/auth/browser-auth.ts
|
|
16782
18081
|
import { createServer } from "node:http";
|
|
@@ -17237,19 +18536,49 @@ async function detectCodex() {
|
|
|
17237
18536
|
}
|
|
17238
18537
|
}
|
|
17239
18538
|
|
|
18539
|
+
// src/detect/gemini.ts
|
|
18540
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
18541
|
+
import { homedir as homedir6 } from "node:os";
|
|
18542
|
+
import { join as join6 } from "node:path";
|
|
18543
|
+
async function detectGemini() {
|
|
18544
|
+
const home = homedir6();
|
|
18545
|
+
const paths = [
|
|
18546
|
+
join6(home, ".gemini"),
|
|
18547
|
+
join6(home, ".config", "gemini")
|
|
18548
|
+
];
|
|
18549
|
+
const found = paths.find((p) => existsSync5(p));
|
|
18550
|
+
if (!found) {
|
|
18551
|
+
return {
|
|
18552
|
+
tool: "gemini",
|
|
18553
|
+
detected: false,
|
|
18554
|
+
configPath: "",
|
|
18555
|
+
existingConfig: false,
|
|
18556
|
+
details: "Gemini CLI data not found"
|
|
18557
|
+
};
|
|
18558
|
+
}
|
|
18559
|
+
return {
|
|
18560
|
+
tool: "gemini",
|
|
18561
|
+
detected: true,
|
|
18562
|
+
configPath: "",
|
|
18563
|
+
existingConfig: false,
|
|
18564
|
+
details: `Gemini CLI data at ${found}`
|
|
18565
|
+
};
|
|
18566
|
+
}
|
|
18567
|
+
|
|
17240
18568
|
// src/detect/index.ts
|
|
17241
18569
|
async function detectAll() {
|
|
17242
18570
|
const results = await Promise.allSettled([
|
|
17243
18571
|
detectClaudeCode(),
|
|
17244
18572
|
detectClaudeDesktop(),
|
|
17245
18573
|
detectCursor(),
|
|
17246
|
-
detectCodex()
|
|
18574
|
+
detectCodex(),
|
|
18575
|
+
detectGemini()
|
|
17247
18576
|
]);
|
|
17248
18577
|
return results.map((result, index) => {
|
|
17249
18578
|
if (result.status === "fulfilled") {
|
|
17250
18579
|
return result.value;
|
|
17251
18580
|
}
|
|
17252
|
-
const tools = ["claude-code", "claude-desktop", "cursor", "codex"];
|
|
18581
|
+
const tools = ["claude-code", "claude-desktop", "cursor", "codex", "gemini"];
|
|
17253
18582
|
const tool = tools[index];
|
|
17254
18583
|
return {
|
|
17255
18584
|
tool,
|
|
@@ -17262,28 +18591,28 @@ async function detectAll() {
|
|
|
17262
18591
|
}
|
|
17263
18592
|
|
|
17264
18593
|
// src/configure/index.ts
|
|
17265
|
-
import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync2, existsSync as
|
|
18594
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync2, existsSync as existsSync6 } from "node:fs";
|
|
17266
18595
|
import { dirname, resolve } from "node:path";
|
|
17267
18596
|
|
|
17268
18597
|
// src/configure/claude-code.ts
|
|
17269
|
-
import { join as
|
|
18598
|
+
import { join as join7 } from "node:path";
|
|
17270
18599
|
async function configureClaudeCode(_detection, credentials, billingType, useGlobal = false) {
|
|
17271
|
-
const configPath =
|
|
18600
|
+
const configPath = join7(process.cwd(), ".mcp.json");
|
|
17272
18601
|
mergeConfig(configPath, credentials, billingType, useGlobal);
|
|
17273
18602
|
}
|
|
17274
18603
|
|
|
17275
18604
|
// src/configure/claude-desktop.ts
|
|
17276
|
-
import { homedir as
|
|
17277
|
-
import { join as
|
|
18605
|
+
import { homedir as homedir7 } from "node:os";
|
|
18606
|
+
import { join as join8 } from "node:path";
|
|
17278
18607
|
function getConfigPath2() {
|
|
17279
18608
|
const platform = process.platform;
|
|
17280
18609
|
if (platform === "darwin") {
|
|
17281
|
-
return
|
|
18610
|
+
return join8(homedir7(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
17282
18611
|
}
|
|
17283
18612
|
if (platform === "win32") {
|
|
17284
|
-
return
|
|
18613
|
+
return join8(process.env["APPDATA"] ?? homedir7(), "Claude", "claude_desktop_config.json");
|
|
17285
18614
|
}
|
|
17286
|
-
return
|
|
18615
|
+
return join8(homedir7(), ".config", "claude", "claude_desktop_config.json");
|
|
17287
18616
|
}
|
|
17288
18617
|
async function configureClaudeDesktop(_detection, credentials, billingType, useGlobal = false) {
|
|
17289
18618
|
const configPath = getConfigPath2();
|
|
@@ -17291,27 +18620,31 @@ async function configureClaudeDesktop(_detection, credentials, billingType, useG
|
|
|
17291
18620
|
}
|
|
17292
18621
|
|
|
17293
18622
|
// src/configure/cursor.ts
|
|
17294
|
-
import { homedir as
|
|
17295
|
-
import { join as
|
|
18623
|
+
import { homedir as homedir8 } from "node:os";
|
|
18624
|
+
import { join as join9 } from "node:path";
|
|
17296
18625
|
async function configureCursor(_detection, credentials, billingType, useGlobal = false) {
|
|
17297
|
-
const configPath =
|
|
18626
|
+
const configPath = join9(homedir8(), ".cursor", "mcp.json");
|
|
17298
18627
|
mergeConfig(configPath, credentials, billingType, useGlobal);
|
|
17299
18628
|
}
|
|
17300
18629
|
|
|
17301
18630
|
// src/configure/codex.ts
|
|
17302
|
-
import { homedir as
|
|
17303
|
-
import { join as
|
|
18631
|
+
import { homedir as homedir9 } from "node:os";
|
|
18632
|
+
import { join as join10 } from "node:path";
|
|
17304
18633
|
async function configureCodex(_detection, credentials, billingType, useGlobal = false) {
|
|
17305
|
-
const configPath =
|
|
18634
|
+
const configPath = join10(homedir9(), ".codex", "config.json");
|
|
17306
18635
|
mergeConfig(configPath, credentials, billingType, useGlobal);
|
|
17307
18636
|
}
|
|
17308
18637
|
|
|
18638
|
+
// src/configure/gemini.ts
|
|
18639
|
+
async function configureGemini(_detection, _credentials, _billingType, _useGlobal) {}
|
|
18640
|
+
|
|
17309
18641
|
// src/configure/index.ts
|
|
17310
18642
|
var TOOL_DEFAULT_BILLING_TYPE = {
|
|
17311
18643
|
"claude-code": "api",
|
|
17312
18644
|
"claude-desktop": "subscription",
|
|
17313
18645
|
cursor: "subscription",
|
|
17314
|
-
codex: "api"
|
|
18646
|
+
codex: "api",
|
|
18647
|
+
gemini: "api"
|
|
17315
18648
|
};
|
|
17316
18649
|
function buildVerbalMcpEntry(credentials, billingType = "api", useGlobal = false) {
|
|
17317
18650
|
return {
|
|
@@ -17333,7 +18666,7 @@ function buildVerbalMcpEntry(credentials, billingType = "api", useGlobal = false
|
|
|
17333
18666
|
function mergeConfig(configPath, credentials, billingType, useGlobal = false) {
|
|
17334
18667
|
mkdirSync2(dirname(configPath), { recursive: true });
|
|
17335
18668
|
let config = {};
|
|
17336
|
-
if (
|
|
18669
|
+
if (existsSync6(configPath)) {
|
|
17337
18670
|
try {
|
|
17338
18671
|
config = JSON.parse(readFileSync6(configPath, "utf-8"));
|
|
17339
18672
|
} catch {
|
|
@@ -17350,7 +18683,7 @@ function mergeConfig(configPath, credentials, billingType, useGlobal = false) {
|
|
|
17350
18683
|
}
|
|
17351
18684
|
function removeVerbalConfig(configPath) {
|
|
17352
18685
|
const resolvedPath = resolve(configPath);
|
|
17353
|
-
if (!
|
|
18686
|
+
if (!existsSync6(resolvedPath)) {
|
|
17354
18687
|
return;
|
|
17355
18688
|
}
|
|
17356
18689
|
let config;
|
|
@@ -17398,10 +18731,9 @@ async function configureAll(detections, credentials, useGlobal = false) {
|
|
|
17398
18731
|
case "codex":
|
|
17399
18732
|
await configureCodex(detection, credentials, billingType, useGlobal);
|
|
17400
18733
|
break;
|
|
17401
|
-
|
|
17402
|
-
|
|
17403
|
-
|
|
17404
|
-
}
|
|
18734
|
+
case "gemini":
|
|
18735
|
+
await configureGemini(detection, credentials, billingType, useGlobal);
|
|
18736
|
+
break;
|
|
17405
18737
|
}
|
|
17406
18738
|
success = true;
|
|
17407
18739
|
} catch (err) {
|
|
@@ -17449,6 +18781,7 @@ async function verifyConnection(credentials) {
|
|
|
17449
18781
|
}
|
|
17450
18782
|
|
|
17451
18783
|
// src/commands/init.ts
|
|
18784
|
+
init_preflight_state();
|
|
17452
18785
|
async function confirm(rl, message, defaultYes) {
|
|
17453
18786
|
const hint = defaultYes ? "(Y/n)" : "(y/N)";
|
|
17454
18787
|
const answer = await rl.question(`${message} ${hint} `);
|
|
@@ -17458,8 +18791,8 @@ async function confirm(rl, message, defaultYes) {
|
|
|
17458
18791
|
return trimmed === "y" || trimmed === "yes";
|
|
17459
18792
|
}
|
|
17460
18793
|
function isInGitignore(filename) {
|
|
17461
|
-
const gitignorePath =
|
|
17462
|
-
if (!
|
|
18794
|
+
const gitignorePath = join12(process.cwd(), ".gitignore");
|
|
18795
|
+
if (!existsSync8(gitignorePath))
|
|
17463
18796
|
return false;
|
|
17464
18797
|
try {
|
|
17465
18798
|
const content = readFileSync7(gitignorePath, "utf-8");
|
|
@@ -17479,6 +18812,8 @@ function toolDisplayName(tool) {
|
|
|
17479
18812
|
return "Cursor";
|
|
17480
18813
|
case "codex":
|
|
17481
18814
|
return "Codex";
|
|
18815
|
+
case "gemini":
|
|
18816
|
+
return "Gemini CLI";
|
|
17482
18817
|
default: {
|
|
17483
18818
|
const _exhaustive = tool;
|
|
17484
18819
|
return String(_exhaustive);
|
|
@@ -17487,7 +18822,7 @@ function toolDisplayName(tool) {
|
|
|
17487
18822
|
}
|
|
17488
18823
|
function isGloballyInstalled() {
|
|
17489
18824
|
try {
|
|
17490
|
-
const which =
|
|
18825
|
+
const which = execSync7("which getverbal", { encoding: "utf-8" }).trim();
|
|
17491
18826
|
return !which.includes("npx") && !which.includes(".npm/_npx");
|
|
17492
18827
|
} catch {
|
|
17493
18828
|
return false;
|
|
@@ -17504,7 +18839,7 @@ async function promoteToGlobal(rl) {
|
|
|
17504
18839
|
console.log(`
|
|
17505
18840
|
${import_picocolors.default.dim("ℹ")} Installing globally...`);
|
|
17506
18841
|
try {
|
|
17507
|
-
|
|
18842
|
+
execSync7(`npm install -g @getverbal/cli@${VERSION}`, { stdio: "inherit" });
|
|
17508
18843
|
console.log(import_picocolors.default.green(` ✓ Installed getverbal v${VERSION} globally`));
|
|
17509
18844
|
return true;
|
|
17510
18845
|
} catch {
|
|
@@ -17544,6 +18879,32 @@ async function runInit() {
|
|
|
17544
18879
|
process.exit(1);
|
|
17545
18880
|
}
|
|
17546
18881
|
}
|
|
18882
|
+
{
|
|
18883
|
+
const stateDir = getDefaultStateDir();
|
|
18884
|
+
const preflightState = loadPreflightState(stateDir);
|
|
18885
|
+
let enableLlmRewrite = false;
|
|
18886
|
+
try {
|
|
18887
|
+
const probeRes = await fetch(`${credentials.api_url}/api/v1/optimize`, {
|
|
18888
|
+
method: "POST",
|
|
18889
|
+
headers: {
|
|
18890
|
+
"Content-Type": "application/json",
|
|
18891
|
+
Authorization: `Bearer ${credentials.api_key}`
|
|
18892
|
+
},
|
|
18893
|
+
body: JSON.stringify({ prompt: ".", threshold: 101 })
|
|
18894
|
+
});
|
|
18895
|
+
if (probeRes.status === 200) {
|
|
18896
|
+
enableLlmRewrite = true;
|
|
18897
|
+
}
|
|
18898
|
+
} catch {}
|
|
18899
|
+
preflightState.config.enable_llm_rewrite = enableLlmRewrite;
|
|
18900
|
+
savePreflightState(stateDir, preflightState);
|
|
18901
|
+
if (enableLlmRewrite) {
|
|
18902
|
+
console.log(import_picocolors.default.green(" ✓ Preflight LLM rewrite enabled (Pro/Team)"));
|
|
18903
|
+
} else {
|
|
18904
|
+
console.log(import_picocolors.default.dim(" ✗ Preflight LLM rewrite disabled (Free tier)"));
|
|
18905
|
+
}
|
|
18906
|
+
console.log();
|
|
18907
|
+
}
|
|
17547
18908
|
console.log(import_picocolors.default.bold("Detecting AI tools..."));
|
|
17548
18909
|
const detections = await detectAll();
|
|
17549
18910
|
for (const d of detections) {
|
|
@@ -17598,6 +18959,39 @@ async function runInit() {
|
|
|
17598
18959
|
}
|
|
17599
18960
|
console.log();
|
|
17600
18961
|
}
|
|
18962
|
+
const confirmedTools = confirmed.map((d) => d.tool);
|
|
18963
|
+
if (confirmedTools.includes("claude-code")) {
|
|
18964
|
+
try {
|
|
18965
|
+
const { installClaudeHooks: installClaudeHooks2 } = await Promise.resolve().then(() => (init_hooks_installer(), exports_hooks_installer));
|
|
18966
|
+
installClaudeHooks2(useGlobal);
|
|
18967
|
+
console.log(import_picocolors.default.green(" ✓ Claude Code real-time hooks installed"));
|
|
18968
|
+
} catch (err) {
|
|
18969
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
18970
|
+
console.log(import_picocolors.default.yellow(` ⚠ Claude Code hooks: ${msg}`));
|
|
18971
|
+
}
|
|
18972
|
+
}
|
|
18973
|
+
const needsWatcher = confirmedTools.includes("codex") || confirmedTools.includes("cursor");
|
|
18974
|
+
if (needsWatcher && process.platform === "darwin") {
|
|
18975
|
+
try {
|
|
18976
|
+
const { installUnifiedWatcher: installUnifiedWatcher2 } = await Promise.resolve().then(() => (init_hooks_installer(), exports_hooks_installer));
|
|
18977
|
+
installUnifiedWatcher2(useGlobal);
|
|
18978
|
+
console.log(import_picocolors.default.green(" ✓ Background watcher installed (Codex + Cursor)"));
|
|
18979
|
+
} catch (err) {
|
|
18980
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
18981
|
+
console.log(import_picocolors.default.yellow(` ⚠ Background watcher: ${msg}`));
|
|
18982
|
+
}
|
|
18983
|
+
}
|
|
18984
|
+
if (confirmedTools.includes("gemini")) {
|
|
18985
|
+
console.log(import_picocolors.default.dim(" ℹ Gemini: batch sync only (run `getverbal import --sync --platforms gemini`)"));
|
|
18986
|
+
}
|
|
18987
|
+
if (confirmedTools.includes("cursor")) {
|
|
18988
|
+
try {
|
|
18989
|
+
await import("bun:sqlite");
|
|
18990
|
+
} catch {
|
|
18991
|
+
console.log(import_picocolors.default.yellow(" ⚠ Cursor detected but bun:sqlite not available."));
|
|
18992
|
+
console.log(import_picocolors.default.yellow(" Cursor sync requires Bun runtime. Run via: bunx getverbal sync"));
|
|
18993
|
+
}
|
|
18994
|
+
}
|
|
17601
18995
|
process.stdout.write(import_picocolors.default.dim("Verifying connection..."));
|
|
17602
18996
|
try {
|
|
17603
18997
|
await verifyConnection(credentials);
|
|
@@ -17608,8 +19002,8 @@ async function runInit() {
|
|
|
17608
19002
|
console.log(import_picocolors.default.yellow(`⚠ Verification warning: ${message}`));
|
|
17609
19003
|
}
|
|
17610
19004
|
console.log();
|
|
17611
|
-
const mcpJsonPath =
|
|
17612
|
-
if (
|
|
19005
|
+
const mcpJsonPath = join12(process.cwd(), ".mcp.json");
|
|
19006
|
+
if (existsSync8(mcpJsonPath) && !isInGitignore(".mcp.json")) {
|
|
17613
19007
|
console.log(import_picocolors.default.yellow("⚠ .mcp.json contains your API key. Add it to .gitignore to avoid leaking credentials:"));
|
|
17614
19008
|
console.log(import_picocolors.default.dim(' echo ".mcp.json" >> .gitignore'));
|
|
17615
19009
|
console.log();
|
|
@@ -17635,10 +19029,9 @@ async function runInit() {
|
|
|
17635
19029
|
console.log(`
|
|
17636
19030
|
Importing ${source.id}...`);
|
|
17637
19031
|
try {
|
|
17638
|
-
const {
|
|
17639
|
-
const stateDir =
|
|
17640
|
-
const
|
|
17641
|
-
const result = await syncFromTokscale2({
|
|
19032
|
+
const { syncPlatforms: syncPlatforms2 } = await Promise.resolve().then(() => (init_orchestrator(), exports_orchestrator));
|
|
19033
|
+
const stateDir = join12(homedir11(), ".config", "getverbal");
|
|
19034
|
+
const result = await syncPlatforms2({
|
|
17642
19035
|
apiKey: credentials.api_key,
|
|
17643
19036
|
ingestUrl: credentials.ingest_url,
|
|
17644
19037
|
batchSize: 100,
|
|
@@ -17646,9 +19039,9 @@ async function runInit() {
|
|
|
17646
19039
|
httpTimeoutMs: 30000,
|
|
17647
19040
|
stateDir
|
|
17648
19041
|
}, {
|
|
17649
|
-
platforms: [
|
|
19042
|
+
platforms: [source.id]
|
|
17650
19043
|
});
|
|
17651
|
-
console.log(import_picocolors.default.green(` ✓ ${result.
|
|
19044
|
+
console.log(import_picocolors.default.green(` ✓ ${result.totalEvents} sessions imported`));
|
|
17652
19045
|
} catch (importErr) {
|
|
17653
19046
|
const message = importErr instanceof Error ? importErr.message : String(importErr);
|
|
17654
19047
|
console.log(import_picocolors.default.yellow(` ⚠ Import skipped: ${message}`));
|