@getverbal/cli 0.4.2 → 0.5.0
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 +134 -25
- package/package.json +10 -5
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.5.0";
|
|
117
117
|
|
|
118
118
|
// src/auth/credentials.ts
|
|
119
119
|
import { readFileSync, writeFileSync, rmSync, mkdirSync, chmodSync, renameSync } from "node:fs";
|
|
@@ -343,37 +343,71 @@ import os2 from "node:os";
|
|
|
343
343
|
import path2 from "node:path";
|
|
344
344
|
import { fileURLToPath } from "node:url";
|
|
345
345
|
import { execSync as execSync5 } from "node:child_process";
|
|
346
|
+
function isGetverbalOnPath() {
|
|
347
|
+
const cmd = process.platform === "win32" ? "where getverbal" : "which getverbal";
|
|
348
|
+
try {
|
|
349
|
+
execSync5(cmd, { stdio: "pipe" });
|
|
350
|
+
return true;
|
|
351
|
+
} catch {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
346
355
|
function installClaudeHooks(useGlobal = false) {
|
|
347
356
|
const settingsPath = path2.join(os2.homedir(), ".claude", "settings.json");
|
|
348
357
|
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
358
|
const configPath = findMcpConfigPath();
|
|
359
|
+
const configEnv = configPath ? { VERBAL_CONFIG_PATH: configPath } : undefined;
|
|
360
|
+
const useCliCommands = useGlobal || isGetverbalOnPath();
|
|
352
361
|
const settings = readJson(settingsPath) ?? {};
|
|
353
362
|
settings.hooks = settings.hooks ?? {};
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (fs2.existsSync(preflightHook)) {
|
|
372
|
-
upsertHook(settings, "UserPromptSubmit", preflightHook, {
|
|
363
|
+
if (useCliCommands) {
|
|
364
|
+
upsertHook(settings, "Stop", "getverbal ingest-claude-hook", {
|
|
365
|
+
timeout: 30,
|
|
366
|
+
statusMessage: "Tracking usage in Verbal",
|
|
367
|
+
async: true,
|
|
368
|
+
env: { HOOK_EVENT: "Stop", ...configEnv }
|
|
369
|
+
});
|
|
370
|
+
upsertHook(settings, "SessionEnd", "getverbal ingest-claude-hook", {
|
|
371
|
+
timeout: 30,
|
|
372
|
+
statusMessage: "Finalizing usage data",
|
|
373
|
+
env: { HOOK_EVENT: "SessionEnd", ...configEnv }
|
|
374
|
+
});
|
|
375
|
+
upsertHook(settings, "Stop", "getverbal session-review", {
|
|
376
|
+
timeout: 5,
|
|
377
|
+
statusMessage: "Checking session review timer"
|
|
378
|
+
});
|
|
379
|
+
upsertHook(settings, "UserPromptSubmit", "getverbal preflight", {
|
|
373
380
|
timeout: 10,
|
|
374
381
|
statusMessage: "Coaching prompt",
|
|
375
|
-
env:
|
|
382
|
+
env: configEnv
|
|
376
383
|
});
|
|
384
|
+
} else {
|
|
385
|
+
const stopHook = path2.join(hooksRoot, "ingest-stop-hook.sh");
|
|
386
|
+
const endHook = path2.join(hooksRoot, "ingest-session-end-hook.sh");
|
|
387
|
+
upsertHook(settings, "Stop", stopHook, {
|
|
388
|
+
timeout: 30,
|
|
389
|
+
statusMessage: "Tracking usage in Verbal",
|
|
390
|
+
async: true,
|
|
391
|
+
env: configEnv ? { HOOK_EVENT: "Stop", ...configEnv } : { HOOK_EVENT: "Stop" }
|
|
392
|
+
});
|
|
393
|
+
upsertHook(settings, "SessionEnd", endHook, {
|
|
394
|
+
timeout: 30,
|
|
395
|
+
statusMessage: "Finalizing usage data",
|
|
396
|
+
env: configEnv ? { HOOK_EVENT: "SessionEnd", ...configEnv } : { HOOK_EVENT: "SessionEnd" }
|
|
397
|
+
});
|
|
398
|
+
const reviewPromptHook = path2.join(hooksRoot, "session-review-prompt.js");
|
|
399
|
+
upsertHook(settings, "Stop", `node ${reviewPromptHook}`, {
|
|
400
|
+
timeout: 5,
|
|
401
|
+
statusMessage: "Checking session review timer"
|
|
402
|
+
});
|
|
403
|
+
const preflightHook = path2.join(hooksRoot, "preflight-hook.sh");
|
|
404
|
+
if (fs2.existsSync(preflightHook)) {
|
|
405
|
+
upsertHook(settings, "UserPromptSubmit", preflightHook, {
|
|
406
|
+
timeout: 10,
|
|
407
|
+
statusMessage: "Coaching prompt",
|
|
408
|
+
env: configEnv
|
|
409
|
+
});
|
|
410
|
+
}
|
|
377
411
|
}
|
|
378
412
|
fs2.mkdirSync(path2.dirname(settingsPath), { recursive: true });
|
|
379
413
|
fs2.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
@@ -402,6 +436,13 @@ function tryUnloadPlist(plistPath) {
|
|
|
402
436
|
execSync5(`launchctl unload "${plistPath}"`, { stdio: "pipe" });
|
|
403
437
|
} catch {}
|
|
404
438
|
}
|
|
439
|
+
function hookIdentityKey(command) {
|
|
440
|
+
for (const keyword of HOOK_IDENTITY_KEYWORDS) {
|
|
441
|
+
if (command.includes(keyword))
|
|
442
|
+
return keyword;
|
|
443
|
+
}
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
405
446
|
function upsertHook(settings, hookName, command, options) {
|
|
406
447
|
const hookEntry = {
|
|
407
448
|
matcher: "",
|
|
@@ -409,7 +450,12 @@ function upsertHook(settings, hookName, command, options) {
|
|
|
409
450
|
};
|
|
410
451
|
settings.hooks = settings.hooks ?? {};
|
|
411
452
|
settings.hooks[hookName] = settings.hooks[hookName] ?? [];
|
|
412
|
-
const
|
|
453
|
+
const newKey = hookIdentityKey(command);
|
|
454
|
+
const existingIndex = settings.hooks[hookName].findIndex((entry) => entry.hooks?.some((hook) => {
|
|
455
|
+
if (newKey !== null && hookIdentityKey(hook.command) === newKey)
|
|
456
|
+
return true;
|
|
457
|
+
return hook.command.includes(path2.basename(command));
|
|
458
|
+
}));
|
|
413
459
|
if (existingIndex >= 0) {
|
|
414
460
|
settings.hooks[hookName][existingIndex] = hookEntry;
|
|
415
461
|
} else {
|
|
@@ -437,10 +483,19 @@ function findMcpConfigPath() {
|
|
|
437
483
|
current = parent;
|
|
438
484
|
}
|
|
439
485
|
}
|
|
440
|
-
var __filename2, __dirname2;
|
|
486
|
+
var __filename2, __dirname2, HOOK_IDENTITY_KEYWORDS;
|
|
441
487
|
var init_hooks_installer = __esm(() => {
|
|
442
488
|
__filename2 = fileURLToPath(import.meta.url);
|
|
443
489
|
__dirname2 = path2.dirname(__filename2);
|
|
490
|
+
HOOK_IDENTITY_KEYWORDS = [
|
|
491
|
+
"ingest-claude-hook",
|
|
492
|
+
"ingest-stop-hook",
|
|
493
|
+
"ingest-session-end-hook",
|
|
494
|
+
"session-review-prompt",
|
|
495
|
+
"session-review",
|
|
496
|
+
"preflight-hook",
|
|
497
|
+
"preflight"
|
|
498
|
+
];
|
|
444
499
|
});
|
|
445
500
|
|
|
446
501
|
// src/import/index.ts
|
|
@@ -17750,6 +17805,58 @@ async function main(args = process.argv) {
|
|
|
17750
17805
|
}
|
|
17751
17806
|
savePreflightState2(stateDir, updatedState);
|
|
17752
17807
|
process.stdout.write(JSON.stringify(result) + `
|
|
17808
|
+
`);
|
|
17809
|
+
return;
|
|
17810
|
+
}
|
|
17811
|
+
case "session-review": {
|
|
17812
|
+
const STATE_FILE = "/tmp/verbal-session-prompts.json";
|
|
17813
|
+
const THRESHOLD_MS = 1500000;
|
|
17814
|
+
const PRUNE_MS = 86400000;
|
|
17815
|
+
let input = {};
|
|
17816
|
+
try {
|
|
17817
|
+
const text = await readStdinText();
|
|
17818
|
+
if (text.trim().length > 0) {
|
|
17819
|
+
input = JSON.parse(text);
|
|
17820
|
+
}
|
|
17821
|
+
} catch {
|
|
17822
|
+
process.exit(0);
|
|
17823
|
+
}
|
|
17824
|
+
if (input.stop_hook_active)
|
|
17825
|
+
process.exit(0);
|
|
17826
|
+
const sessionId = input.session_id;
|
|
17827
|
+
if (!sessionId)
|
|
17828
|
+
process.exit(0);
|
|
17829
|
+
let state = {};
|
|
17830
|
+
try {
|
|
17831
|
+
state = JSON.parse(fs15.readFileSync(STATE_FILE, "utf8"));
|
|
17832
|
+
} catch {
|
|
17833
|
+
state = {};
|
|
17834
|
+
}
|
|
17835
|
+
const now = Date.now();
|
|
17836
|
+
for (const [id, entry2] of Object.entries(state)) {
|
|
17837
|
+
if (now - entry2.start > PRUNE_MS) {
|
|
17838
|
+
delete state[id];
|
|
17839
|
+
}
|
|
17840
|
+
}
|
|
17841
|
+
const entry = state[sessionId];
|
|
17842
|
+
if (!entry) {
|
|
17843
|
+
state[sessionId] = { start: now, prompted: false };
|
|
17844
|
+
try {
|
|
17845
|
+
fs15.writeFileSync(STATE_FILE, JSON.stringify(state));
|
|
17846
|
+
} catch {}
|
|
17847
|
+
process.exit(0);
|
|
17848
|
+
}
|
|
17849
|
+
if (entry.prompted)
|
|
17850
|
+
process.exit(0);
|
|
17851
|
+
const elapsed = now - entry.start;
|
|
17852
|
+
if (elapsed < THRESHOLD_MS)
|
|
17853
|
+
process.exit(0);
|
|
17854
|
+
entry.prompted = true;
|
|
17855
|
+
try {
|
|
17856
|
+
fs15.writeFileSync(STATE_FILE, JSON.stringify(state));
|
|
17857
|
+
} catch {}
|
|
17858
|
+
const elapsedMin = Math.round(elapsed / 60000);
|
|
17859
|
+
process.stdout.write(`You've been working for ~${elapsedMin} minutes. When the user seems ready to wrap up or there is a natural pause, offer a session review: say "Want to quickly rate this session? It helps track your AI ROI." If they say yes, call submit_session_review with session_id="${sessionId}", usefulness_rating (1-5), effectiveness_rating (1-5), and time_saved_bucket.
|
|
17753
17860
|
`);
|
|
17754
17861
|
return;
|
|
17755
17862
|
}
|
|
@@ -17997,6 +18104,8 @@ function printHelp() {
|
|
|
17997
18104
|
(no flags = auto-detect, watches all available sources)
|
|
17998
18105
|
verbal-hooks preflight [--mode silent|notify|blocking] [--threshold 60]
|
|
17999
18106
|
(reads prompt from stdin, prints coaching to stderr)
|
|
18107
|
+
verbal-hooks session-review
|
|
18108
|
+
(Stop hook: prompts for session review after 25 min; reads stdin)
|
|
18000
18109
|
verbal-hooks status
|
|
18001
18110
|
verbal-hooks uninstall --claude --codex
|
|
18002
18111
|
`);
|
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getverbal/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Set up Verbal AI spend tracking in one command",
|
|
5
5
|
"bin": {
|
|
6
6
|
"getverbal": "dist/cli.js"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./dist/cli.js",
|
|
10
|
-
"files": [
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
11
15
|
"scripts": {
|
|
12
16
|
"build": "bun build src/cli.ts --target=node --outdir=dist --format=esm",
|
|
13
17
|
"build:tsc": "tsc -p tsconfig.json",
|
|
@@ -17,14 +21,15 @@
|
|
|
17
21
|
"prepublishOnly": "npm run typecheck && npm run build"
|
|
18
22
|
},
|
|
19
23
|
"dependencies": {
|
|
20
|
-
"
|
|
21
|
-
"open": "^10.0.0",
|
|
24
|
+
"@getverbal/cli": "^0.4.2",
|
|
22
25
|
"@modelcontextprotocol/sdk": "^1.27.0",
|
|
26
|
+
"open": "^10.0.0",
|
|
27
|
+
"picocolors": "^1.1.0",
|
|
23
28
|
"zod": "^3.25.0"
|
|
24
29
|
},
|
|
25
30
|
"devDependencies": {
|
|
26
|
-
"typescript": "^5.5.0",
|
|
27
31
|
"@types/node": "^20.0.0",
|
|
32
|
+
"typescript": "^5.5.0",
|
|
28
33
|
"vitest": "^3.0.0"
|
|
29
34
|
},
|
|
30
35
|
"engines": {
|