@getverbal/cli 0.4.1 → 0.4.3

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.
Files changed (2) hide show
  1. package/dist/cli.js +134 -25
  2. package/package.json +1 -1
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.4.1";
116
+ var VERSION = "0.4.3";
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
- 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, {
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: configPath ? { VERBAL_CONFIG_PATH: configPath } : undefined
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 existingIndex = settings.hooks[hookName].findIndex((entry) => entry.hooks?.some((hook) => hook.command.includes(path2.basename(command))));
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@getverbal/cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Set up Verbal AI spend tracking in one command",
5
5
  "bin": {
6
6
  "getverbal": "dist/cli.js"