@usevalt/cli 0.5.0 → 0.6.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.
Files changed (2) hide show
  1. package/dist/index.js +518 -33
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -491,6 +491,32 @@ var doctorCommand = new Command5("doctor").description("Check Valt configuration
491
491
  } else {
492
492
  info("Eval service: not configured (set VALT_EVAL_URL for Semgrep scans)");
493
493
  }
494
+ const signingPrivateKey = process.env["VALT_SIGNING_PRIVATE_KEY"];
495
+ const signingPublicKey = process.env["VALT_SIGNING_PUBLIC_KEY"];
496
+ if (signingPrivateKey && signingPublicKey) {
497
+ success("Signing keys: configured (persistent)");
498
+ } else {
499
+ warn("Signing keys: not configured (using ephemeral keys \u2014 certificates will not survive restart)");
500
+ issues++;
501
+ }
502
+ const hooksPath = `${process.env["HOME"] ?? "~"}/.claude/hooks.json`;
503
+ try {
504
+ const { existsSync: exists, readFileSync: read } = await import("fs");
505
+ if (exists(hooksPath)) {
506
+ const hooksContent = read(hooksPath, "utf-8");
507
+ const hookCount = (hooksContent.match(/valt/g) ?? []).length;
508
+ if (hookCount >= 3) {
509
+ success(`Claude Code hooks: ${dim(hooksPath)} (${hookCount} hooks)`);
510
+ } else {
511
+ warn("Claude Code hooks: partially configured. Run `valt setup`.");
512
+ issues++;
513
+ }
514
+ } else {
515
+ info("Claude Code hooks: not installed. Run `valt setup` to enable.");
516
+ }
517
+ } catch {
518
+ info("Claude Code hooks: could not check.");
519
+ }
494
520
  console.log("");
495
521
  if (issues === 0) {
496
522
  success("All checks passed!");
@@ -995,13 +1021,43 @@ proxyCommand.command("stop").description("Stop the MCP proxy").action(() => {
995
1021
 
996
1022
  // src/commands/hook.ts
997
1023
  import { Command as Command13 } from "commander";
998
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, existsSync as existsSync3 } from "fs";
1024
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, existsSync as existsSync3, readdirSync } from "fs";
999
1025
  import { createReadStream } from "fs";
1000
1026
  import { createInterface } from "readline";
1001
1027
  import { execSync } from "child_process";
1002
- import { basename } from "path";
1028
+ import { basename, join } from "path";
1029
+ import { createHash } from "crypto";
1003
1030
  import os2 from "os";
1004
- var SESSION_FILE = `${os2.tmpdir()}/valt-session-${process.getuid?.() ?? "default"}.json`;
1031
+ function getSessionFile(claudeSessionId) {
1032
+ return join(os2.tmpdir(), `valt-session-${claudeSessionId}.json`);
1033
+ }
1034
+ function findLatestSessionFile() {
1035
+ try {
1036
+ const tmpDir = os2.tmpdir();
1037
+ const files = readdirSync(tmpDir).filter((f) => f.startsWith("valt-session-") && f.endsWith(".json")).map((f) => ({
1038
+ name: f,
1039
+ path: join(tmpDir, f),
1040
+ mtime: (() => {
1041
+ try {
1042
+ return readFileSync3(join(tmpDir, f), "utf-8");
1043
+ } catch {
1044
+ return "";
1045
+ }
1046
+ })()
1047
+ })).filter((f) => f.mtime.length > 0).sort((a, b) => {
1048
+ try {
1049
+ const aData = JSON.parse(a.mtime);
1050
+ const bData = JSON.parse(b.mtime);
1051
+ return (bData.startedAt ?? "").localeCompare(aData.startedAt ?? "");
1052
+ } catch {
1053
+ return 0;
1054
+ }
1055
+ });
1056
+ return files[0]?.path ?? null;
1057
+ } catch {
1058
+ return null;
1059
+ }
1060
+ }
1005
1061
  async function readStdin() {
1006
1062
  if (process.stdin.isTTY) return {};
1007
1063
  try {
@@ -1042,14 +1098,53 @@ function detectProjectSlug() {
1042
1098
  }
1043
1099
  return basename(process.cwd());
1044
1100
  }
1045
- function readSessionFile() {
1101
+ function detectDeveloper() {
1102
+ const result = {};
1103
+ if (process.env["VALT_USER_EMAIL"]) {
1104
+ result.email = process.env["VALT_USER_EMAIL"];
1105
+ }
1106
+ if (!result.email) {
1107
+ try {
1108
+ result.email = execSync("git config user.email", {
1109
+ encoding: "utf-8",
1110
+ timeout: 3e3,
1111
+ stdio: ["pipe", "pipe", "pipe"]
1112
+ }).trim() || void 0;
1113
+ } catch {
1114
+ }
1115
+ }
1046
1116
  try {
1047
- if (!existsSync3(SESSION_FILE)) return null;
1048
- const data = readFileSync3(SESSION_FILE, "utf-8");
1049
- return JSON.parse(data);
1117
+ result.name = execSync("git config user.name", {
1118
+ encoding: "utf-8",
1119
+ timeout: 3e3,
1120
+ stdio: ["pipe", "pipe", "pipe"]
1121
+ }).trim() || void 0;
1050
1122
  } catch {
1051
- return null;
1052
1123
  }
1124
+ if (!result.name) {
1125
+ result.name = process.env["USER"] ?? process.env["LOGNAME"];
1126
+ }
1127
+ return result;
1128
+ }
1129
+ function readSessionFile(claudeSessionId) {
1130
+ if (claudeSessionId) {
1131
+ const path = getSessionFile(claudeSessionId);
1132
+ try {
1133
+ if (existsSync3(path)) {
1134
+ return JSON.parse(readFileSync3(path, "utf-8"));
1135
+ }
1136
+ } catch {
1137
+ }
1138
+ }
1139
+ const fallbackPath = findLatestSessionFile();
1140
+ if (fallbackPath) {
1141
+ try {
1142
+ return JSON.parse(readFileSync3(fallbackPath, "utf-8"));
1143
+ } catch {
1144
+ return null;
1145
+ }
1146
+ }
1147
+ return null;
1053
1148
  }
1054
1149
  async function sendEvents(endpoint, apiKey, events) {
1055
1150
  try {
@@ -1071,7 +1166,6 @@ var MODEL_PRICING = {
1071
1166
  "claude-opus-4-6": { input: 15, output: 75, cacheRead: 1.5 },
1072
1167
  "claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3 },
1073
1168
  "claude-haiku-4-5": { input: 0.8, output: 4, cacheRead: 0.08 },
1074
- // Legacy model names
1075
1169
  "claude-3-5-sonnet": { input: 3, output: 15, cacheRead: 0.3 },
1076
1170
  "claude-3-5-haiku": { input: 0.8, output: 4, cacheRead: 0.08 },
1077
1171
  "claude-3-opus": { input: 15, output: 75, cacheRead: 1.5 }
@@ -1081,6 +1175,75 @@ function calculateCost(model, promptTokens, completionTokens, cacheReadTokens) {
1081
1175
  if (!pricing) return 0;
1082
1176
  return promptTokens / 1e6 * pricing.input + completionTokens / 1e6 * pricing.output + cacheReadTokens / 1e6 * pricing.cacheRead;
1083
1177
  }
1178
+ function classifyToolCall(toolName, toolInput) {
1179
+ switch (toolName) {
1180
+ case "Read":
1181
+ return { eventType: "file.read", filePath: toolInput["file_path"] };
1182
+ case "Write":
1183
+ case "Edit":
1184
+ case "NotebookEdit":
1185
+ return { eventType: "file.write", filePath: toolInput["file_path"] ?? toolInput["notebook_path"] };
1186
+ case "Bash": {
1187
+ const command = toolInput["command"] ?? "";
1188
+ return { eventType: "command.execute", command };
1189
+ }
1190
+ case "Glob":
1191
+ case "Grep":
1192
+ return { eventType: "file.read", filePath: toolInput["path"] ?? toolInput["pattern"] };
1193
+ default:
1194
+ return { eventType: "tool.call" };
1195
+ }
1196
+ }
1197
+ function isTestCommand(command) {
1198
+ const testPatterns = [
1199
+ /\b(jest|vitest|mocha|ava)\b/,
1200
+ /\bpnpm\s+(test|t)\b/,
1201
+ /\bnpm\s+(test|t)\b/,
1202
+ /\bnpx\s+(jest|vitest)\b/,
1203
+ /\bpytest\b/,
1204
+ /\bpython\s+-m\s+pytest\b/,
1205
+ /\bcargo\s+test\b/,
1206
+ /\bgo\s+test\b/,
1207
+ /\bmake\s+test\b/
1208
+ ];
1209
+ return testPatterns.some((p) => p.test(command));
1210
+ }
1211
+ function parseTestResults(stdout) {
1212
+ const vitestMatch = stdout.match(/Tests\s+(\d+)\s+passed.*?(\d+)\s+failed/i) ?? stdout.match(/Tests\s+(\d+)\s+passed\s+\((\d+)\)/i);
1213
+ if (vitestMatch) {
1214
+ const passed = parseInt(vitestMatch[1], 10);
1215
+ const failedOrTotal = parseInt(vitestMatch[2], 10);
1216
+ const failed = stdout.includes("failed") ? failedOrTotal : 0;
1217
+ const total = stdout.includes("failed") ? passed + failed : failedOrTotal;
1218
+ return { testsRun: total, testsPassed: passed, testsFailed: failed, framework: "vitest" };
1219
+ }
1220
+ const jestMatch = stdout.match(/Tests:\s+(?:(\d+)\s+failed,\s+)?(\d+)\s+passed,\s+(\d+)\s+total/i);
1221
+ if (jestMatch) {
1222
+ const failed = parseInt(jestMatch[1] ?? "0", 10);
1223
+ const passed = parseInt(jestMatch[2], 10);
1224
+ const total = parseInt(jestMatch[3], 10);
1225
+ return { testsRun: total, testsPassed: passed, testsFailed: failed, framework: "jest" };
1226
+ }
1227
+ const pytestMatch = stdout.match(/(\d+)\s+passed(?:,\s+(\d+)\s+failed)?/i);
1228
+ if (pytestMatch) {
1229
+ const passed = parseInt(pytestMatch[1], 10);
1230
+ const failed = parseInt(pytestMatch[2] ?? "0", 10);
1231
+ return { testsRun: passed + failed, testsPassed: passed, testsFailed: failed, framework: "pytest" };
1232
+ }
1233
+ const goMatch = stdout.match(/^(ok|FAIL)\s+\S+/m);
1234
+ if (goMatch) {
1235
+ const passed = goMatch[1] === "ok" ? 1 : 0;
1236
+ const failed = goMatch[1] === "FAIL" ? 1 : 0;
1237
+ return { testsRun: 1, testsPassed: passed, testsFailed: failed, framework: "go" };
1238
+ }
1239
+ const cargoMatch = stdout.match(/test result: \w+\.\s+(\d+)\s+passed;\s+(\d+)\s+failed/i);
1240
+ if (cargoMatch) {
1241
+ const passed = parseInt(cargoMatch[1], 10);
1242
+ const failed = parseInt(cargoMatch[2], 10);
1243
+ return { testsRun: passed + failed, testsPassed: passed, testsFailed: failed, framework: "cargo" };
1244
+ }
1245
+ return null;
1246
+ }
1084
1247
  async function parseTranscript(transcriptPath) {
1085
1248
  const result = {
1086
1249
  totalCostUsd: 0,
@@ -1133,13 +1296,35 @@ var sessionStartCommand = new Command13("session-start").description("Hook: call
1133
1296
  try {
1134
1297
  const { apiKey, endpoint, apiEndpoint } = resolveConfig();
1135
1298
  const projectSlug = detectProjectSlug();
1299
+ const developer = detectDeveloper();
1136
1300
  const hookData = await readStdin();
1137
- const claudeSessionId = hookData["session_id"];
1301
+ const claudeSessionId = hookData["session_id"] ?? crypto.randomUUID();
1138
1302
  const sessionId = crypto.randomUUID();
1139
1303
  const model = hookData["model"] || process.env["CLAUDE_MODEL"] || "unknown";
1140
1304
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1141
- const state = { sessionId, startedAt, apiKey, endpoint, apiEndpoint, projectSlug, model };
1142
- writeFileSync3(SESSION_FILE, JSON.stringify(state, null, 2), { mode: 384 });
1305
+ const cwd = hookData["cwd"] || process.cwd();
1306
+ const state = {
1307
+ sessionId,
1308
+ claudeSessionId,
1309
+ startedAt,
1310
+ apiKey,
1311
+ endpoint,
1312
+ apiEndpoint,
1313
+ projectSlug,
1314
+ model,
1315
+ cwd
1316
+ };
1317
+ writeFileSync3(getSessionFile(claudeSessionId), JSON.stringify(state, null, 2), { mode: 384 });
1318
+ let gitCommitBefore;
1319
+ try {
1320
+ gitCommitBefore = execSync("git rev-parse HEAD", {
1321
+ encoding: "utf-8",
1322
+ timeout: 3e3,
1323
+ stdio: ["pipe", "pipe", "pipe"],
1324
+ cwd
1325
+ }).trim();
1326
+ } catch {
1327
+ }
1143
1328
  const sent = await sendEvents(endpoint, apiKey, [
1144
1329
  {
1145
1330
  event_id: crypto.randomUUID(),
@@ -1155,7 +1340,11 @@ var sessionStartCommand = new Command13("session-start").description("Hook: call
1155
1340
  repository: process.env["CLAUDE_REPO"],
1156
1341
  branch: process.env["CLAUDE_BRANCH"],
1157
1342
  project_slug: projectSlug,
1158
- cwd: hookData["cwd"] || process.cwd()
1343
+ cwd,
1344
+ commit_sha: gitCommitBefore,
1345
+ // 1.9: Developer attribution
1346
+ developer_email: developer.email,
1347
+ developer_name: developer.name
1159
1348
  }
1160
1349
  }
1161
1350
  ]);
@@ -1168,36 +1357,276 @@ var sessionStartCommand = new Command13("session-start").description("Hook: call
1168
1357
  warn(`Failed to start session: ${err instanceof Error ? err.message : String(err)}`);
1169
1358
  }
1170
1359
  });
1171
- var toolCallCommand = new Command13("tool-call").description("Hook: called on each Claude Code tool call (reads JSON from stdin)").action(async () => {
1360
+ var toolCallCommand = new Command13("tool-call").description("Hook: called on each Claude Code tool call (PreToolUse)").action(async () => {
1172
1361
  try {
1173
- const session = readSessionFile();
1362
+ const hookData = await readStdin();
1363
+ const claudeSessionId = hookData["session_id"];
1364
+ const session = readSessionFile(claudeSessionId);
1174
1365
  if (!session) return;
1175
- const toolData = await readStdin();
1176
- const toolName = toolData["tool_name"] ?? toolData["name"] ?? "unknown";
1177
- await sendEvents(session.endpoint, session.apiKey, [
1178
- {
1366
+ const toolName = hookData["tool_name"] ?? hookData["name"] ?? "unknown";
1367
+ const toolInput = hookData["tool_input"] ?? {};
1368
+ if (session.apiEndpoint) {
1369
+ try {
1370
+ const gateController = new AbortController();
1371
+ const gateTimeout = setTimeout(() => gateController.abort(), 3e3);
1372
+ const toolInputSummary = JSON.stringify(toolInput).slice(0, 500);
1373
+ const gateRes = await fetch(
1374
+ `${session.apiEndpoint}/api/v1/sessions/${session.sessionId}/gate`,
1375
+ {
1376
+ method: "POST",
1377
+ headers: {
1378
+ "Content-Type": "application/json",
1379
+ Authorization: `Bearer ${session.apiKey}`
1380
+ },
1381
+ body: JSON.stringify({
1382
+ tool_name: toolName,
1383
+ tool_input_summary: toolInputSummary
1384
+ }),
1385
+ signal: gateController.signal
1386
+ }
1387
+ );
1388
+ clearTimeout(gateTimeout);
1389
+ if (gateRes.ok) {
1390
+ const gateBody = await gateRes.json();
1391
+ const gateAction = gateBody.data?.action;
1392
+ if (gateAction === "pause") {
1393
+ warn(`Gate: ${gateBody.data?.reason ?? "Paused by administrator"}`);
1394
+ process.exit(2);
1395
+ }
1396
+ if (gateAction === "block") {
1397
+ warn(`Gate: ${gateBody.data?.reason ?? "Blocked by administrator"}`);
1398
+ process.exit(2);
1399
+ }
1400
+ }
1401
+ } catch {
1402
+ }
1403
+ }
1404
+ const { eventType, filePath, command } = classifyToolCall(toolName, toolInput);
1405
+ const events = [];
1406
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1407
+ events.push({
1408
+ event_id: crypto.randomUUID(),
1409
+ session_id: session.sessionId,
1410
+ event_type: "tool.call",
1411
+ timestamp,
1412
+ tool: "claude-code",
1413
+ tool_name: toolName,
1414
+ metadata: {
1415
+ tool_name: toolName,
1416
+ file_path: filePath,
1417
+ command
1418
+ }
1419
+ });
1420
+ if (eventType !== "tool.call") {
1421
+ events.push({
1179
1422
  event_id: crypto.randomUUID(),
1180
1423
  session_id: session.sessionId,
1181
- event_type: "tool.call",
1182
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1424
+ event_type: eventType,
1425
+ timestamp,
1183
1426
  tool: "claude-code",
1184
1427
  tool_name: toolName,
1185
- metadata: toolData
1428
+ file_path: filePath,
1429
+ metadata: {
1430
+ tool_name: toolName,
1431
+ ...filePath ? { file_path: filePath } : {},
1432
+ ...command ? { command } : {}
1433
+ }
1434
+ });
1435
+ if (eventType === "command.execute" && command && isTestCommand(command)) {
1436
+ events.push({
1437
+ event_id: crypto.randomUUID(),
1438
+ session_id: session.sessionId,
1439
+ event_type: "test.run",
1440
+ timestamp,
1441
+ tool: "claude-code",
1442
+ metadata: { command, source: "pre-tool-detect" }
1443
+ });
1186
1444
  }
1187
- ]);
1445
+ }
1446
+ await sendEvents(session.endpoint, session.apiKey, events);
1188
1447
  } catch {
1189
1448
  }
1190
1449
  });
1191
- var sessionEndCommand = new Command13("session-end").description("Hook: called when a Claude Code session ends").action(async () => {
1450
+ var postToolUseCommand = new Command13("post-tool-use").description("Hook: called after a tool call completes (PostToolUse)").action(async () => {
1451
+ try {
1452
+ const hookData = await readStdin();
1453
+ const claudeSessionId = hookData["session_id"];
1454
+ const session = readSessionFile(claudeSessionId);
1455
+ if (!session) return;
1456
+ const toolName = hookData["tool_name"] ?? "unknown";
1457
+ const toolResponse = hookData["tool_response"];
1458
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1459
+ const events = [{
1460
+ event_id: crypto.randomUUID(),
1461
+ session_id: session.sessionId,
1462
+ event_type: "tool.result",
1463
+ timestamp,
1464
+ tool: "claude-code",
1465
+ tool_name: toolName,
1466
+ metadata: {
1467
+ tool_name: toolName,
1468
+ exit_code: toolResponse?.["exit_code"],
1469
+ success: toolResponse?.["success"] ?? true
1470
+ }
1471
+ }];
1472
+ if (toolName === "Bash" && toolResponse) {
1473
+ const stdout = toolResponse["stdout"] ?? toolResponse["output"] ?? "";
1474
+ if (stdout) {
1475
+ const testResults = parseTestResults(stdout);
1476
+ if (testResults) {
1477
+ events.push({
1478
+ event_id: crypto.randomUUID(),
1479
+ session_id: session.sessionId,
1480
+ event_type: "test.run",
1481
+ timestamp,
1482
+ tool: "claude-code",
1483
+ metadata: {
1484
+ tests_run: testResults.testsRun,
1485
+ tests_passed: testResults.testsPassed,
1486
+ tests_failed: testResults.testsFailed,
1487
+ framework: testResults.framework,
1488
+ source: "post-tool-parse"
1489
+ }
1490
+ });
1491
+ }
1492
+ }
1493
+ }
1494
+ await sendEvents(session.endpoint, session.apiKey, events);
1495
+ } catch {
1496
+ }
1497
+ });
1498
+ var toolErrorCommand = new Command13("tool-error").description("Hook: called when a tool call fails (PostToolUseFailure)").action(async () => {
1192
1499
  try {
1193
- const session = readSessionFile();
1500
+ const hookData = await readStdin();
1501
+ const claudeSessionId = hookData["session_id"];
1502
+ const session = readSessionFile(claudeSessionId);
1194
1503
  if (!session) return;
1504
+ const toolName = hookData["tool_name"] ?? "unknown";
1505
+ const errorMsg = hookData["error"] ?? "unknown error";
1506
+ const isInterrupt = hookData["is_interrupt"] ?? false;
1507
+ await sendEvents(session.endpoint, session.apiKey, [{
1508
+ event_id: crypto.randomUUID(),
1509
+ session_id: session.sessionId,
1510
+ event_type: "tool.error",
1511
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1512
+ tool: "claude-code",
1513
+ tool_name: toolName,
1514
+ metadata: {
1515
+ tool_name: toolName,
1516
+ error_message: errorMsg,
1517
+ is_interrupt: isInterrupt
1518
+ }
1519
+ }]);
1520
+ } catch {
1521
+ }
1522
+ });
1523
+ var subagentStartCommand = new Command13("subagent-start").description("Hook: called when a subagent starts").action(async () => {
1524
+ try {
1195
1525
  const hookData = await readStdin();
1526
+ const claudeSessionId = hookData["session_id"];
1527
+ const session = readSessionFile(claudeSessionId);
1528
+ if (!session) return;
1529
+ await sendEvents(session.endpoint, session.apiKey, [{
1530
+ event_id: crypto.randomUUID(),
1531
+ session_id: session.sessionId,
1532
+ event_type: "subagent.start",
1533
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1534
+ tool: "claude-code",
1535
+ metadata: {
1536
+ agent_id: hookData["agent_id"],
1537
+ agent_type: hookData["agent_type"]
1538
+ }
1539
+ }]);
1540
+ } catch {
1541
+ }
1542
+ });
1543
+ var subagentStopCommand = new Command13("subagent-stop").description("Hook: called when a subagent stops").action(async () => {
1544
+ try {
1545
+ const hookData = await readStdin();
1546
+ const claudeSessionId = hookData["session_id"];
1547
+ const session = readSessionFile(claudeSessionId);
1548
+ if (!session) return;
1549
+ await sendEvents(session.endpoint, session.apiKey, [{
1550
+ event_id: crypto.randomUUID(),
1551
+ session_id: session.sessionId,
1552
+ event_type: "subagent.stop",
1553
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1554
+ tool: "claude-code",
1555
+ metadata: {
1556
+ agent_id: hookData["agent_id"],
1557
+ agent_type: hookData["agent_type"],
1558
+ transcript_path: hookData["agent_transcript_path"]
1559
+ }
1560
+ }]);
1561
+ } catch {
1562
+ }
1563
+ });
1564
+ var promptSubmitCommand = new Command13("prompt-submit").description("Hook: called when a user submits a prompt").action(async () => {
1565
+ try {
1566
+ const hookData = await readStdin();
1567
+ const claudeSessionId = hookData["session_id"];
1568
+ const session = readSessionFile(claudeSessionId);
1569
+ if (!session) return;
1570
+ const promptText = hookData["prompt"] ?? "";
1571
+ const promptHashValue = createHash("sha256").update(promptText).digest("hex");
1572
+ await sendEvents(session.endpoint, session.apiKey, [{
1573
+ event_id: crypto.randomUUID(),
1574
+ session_id: session.sessionId,
1575
+ event_type: "prompt.submit",
1576
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1577
+ tool: "claude-code",
1578
+ metadata: {
1579
+ prompt_hash: promptHashValue,
1580
+ prompt_length: promptText.length
1581
+ }
1582
+ }]);
1583
+ } catch {
1584
+ }
1585
+ });
1586
+ var sessionEndCommand = new Command13("session-end").description("Hook: called when a Claude Code session ends").action(async () => {
1587
+ try {
1588
+ const hookData = await readStdin();
1589
+ const claudeSessionId = hookData["session_id"];
1590
+ let session = readSessionFile(claudeSessionId);
1591
+ let sessionFilePath;
1592
+ if (session) {
1593
+ sessionFilePath = claudeSessionId ? getSessionFile(claudeSessionId) : findLatestSessionFile() ?? void 0;
1594
+ } else {
1595
+ const config = (() => {
1596
+ try {
1597
+ return resolveConfig();
1598
+ } catch {
1599
+ return null;
1600
+ }
1601
+ })();
1602
+ if (!config) return;
1603
+ session = {
1604
+ sessionId: claudeSessionId ?? crypto.randomUUID(),
1605
+ claudeSessionId: claudeSessionId ?? "unknown",
1606
+ startedAt: new Date(Date.now() - 6e4).toISOString(),
1607
+ // Assume 1 min ago
1608
+ apiKey: config.apiKey,
1609
+ endpoint: config.endpoint,
1610
+ apiEndpoint: config.apiEndpoint,
1611
+ cwd: hookData["cwd"] || process.cwd()
1612
+ };
1613
+ warn("Session file not found \u2014 sending session data from stdin/config fallback.");
1614
+ }
1196
1615
  const now = /* @__PURE__ */ new Date();
1197
1616
  const startedAt = new Date(session.startedAt);
1198
1617
  const durationMs = now.getTime() - startedAt.getTime();
1199
1618
  const transcriptPath = hookData["transcript_path"];
1200
1619
  const metrics = transcriptPath ? await parseTranscript(transcriptPath) : null;
1620
+ let gitCommitAfter;
1621
+ try {
1622
+ gitCommitAfter = execSync("git rev-parse HEAD", {
1623
+ encoding: "utf-8",
1624
+ timeout: 3e3,
1625
+ stdio: ["pipe", "pipe", "pipe"],
1626
+ cwd: session.cwd
1627
+ }).trim();
1628
+ } catch {
1629
+ }
1201
1630
  const events = [
1202
1631
  {
1203
1632
  event_id: crypto.randomUUID(),
@@ -1211,6 +1640,7 @@ var sessionEndCommand = new Command13("session-end").description("Hook: called w
1211
1640
  started_at: session.startedAt,
1212
1641
  ended_at: now.toISOString(),
1213
1642
  reason: hookData["reason"] ?? "unknown",
1643
+ git_commit_after: gitCommitAfter,
1214
1644
  ...metrics ? {
1215
1645
  prompt_tokens: metrics.promptTokens,
1216
1646
  completion_tokens: metrics.completionTokens,
@@ -1239,9 +1669,11 @@ var sessionEndCommand = new Command13("session-end").description("Hook: called w
1239
1669
  }
1240
1670
  sendEvents(session.endpoint, session.apiKey, events).catch(() => {
1241
1671
  });
1242
- try {
1243
- unlinkSync2(SESSION_FILE);
1244
- } catch {
1672
+ if (sessionFilePath) {
1673
+ try {
1674
+ unlinkSync2(sessionFilePath);
1675
+ } catch {
1676
+ }
1245
1677
  }
1246
1678
  const costStr = metrics?.totalCostUsd ? ` $${metrics.totalCostUsd.toFixed(4)}` : "";
1247
1679
  const tokenStr = metrics?.promptTokens ? ` ${metrics.promptTokens + metrics.completionTokens} tokens` : "";
@@ -1249,15 +1681,15 @@ var sessionEndCommand = new Command13("session-end").description("Hook: called w
1249
1681
  } catch {
1250
1682
  }
1251
1683
  });
1252
- var hookCommand = new Command13("hook").description("Claude Code hook handlers for Valt session tracking").addCommand(sessionStartCommand).addCommand(toolCallCommand).addCommand(sessionEndCommand);
1684
+ var hookCommand = new Command13("hook").description("Claude Code hook handlers for Valt session tracking").addCommand(sessionStartCommand).addCommand(toolCallCommand).addCommand(postToolUseCommand).addCommand(toolErrorCommand).addCommand(subagentStartCommand).addCommand(subagentStopCommand).addCommand(promptSubmitCommand).addCommand(sessionEndCommand);
1253
1685
 
1254
1686
  // src/commands/setup.ts
1255
1687
  import { Command as Command14 } from "commander";
1256
1688
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
1257
- import { join } from "path";
1689
+ import { join as join2 } from "path";
1258
1690
  import os3 from "os";
1259
- var CLAUDE_DIR = join(os3.homedir(), ".claude");
1260
- var HOOKS_FILE = join(CLAUDE_DIR, "hooks.json");
1691
+ var CLAUDE_DIR = join2(os3.homedir(), ".claude");
1692
+ var HOOKS_FILE = join2(CLAUDE_DIR, "hooks.json");
1261
1693
  var HOOK_PREFIX = "npx --yes @usevalt/cli";
1262
1694
  function getValtHooks() {
1263
1695
  return {
@@ -1282,13 +1714,66 @@ function getValtHooks() {
1282
1714
  ]
1283
1715
  }
1284
1716
  ],
1717
+ PostToolUse: [
1718
+ {
1719
+ matcher: "*",
1720
+ hooks: [
1721
+ {
1722
+ type: "command",
1723
+ command: `${HOOK_PREFIX} hook post-tool-use`
1724
+ }
1725
+ ]
1726
+ }
1727
+ ],
1728
+ PostToolUseFailure: [
1729
+ {
1730
+ matcher: "*",
1731
+ hooks: [
1732
+ {
1733
+ type: "command",
1734
+ command: `${HOOK_PREFIX} hook tool-error`
1735
+ }
1736
+ ]
1737
+ }
1738
+ ],
1739
+ SubagentStart: [
1740
+ {
1741
+ hooks: [
1742
+ {
1743
+ type: "command",
1744
+ command: `${HOOK_PREFIX} hook subagent-start`
1745
+ }
1746
+ ]
1747
+ }
1748
+ ],
1749
+ SubagentStop: [
1750
+ {
1751
+ hooks: [
1752
+ {
1753
+ type: "command",
1754
+ command: `${HOOK_PREFIX} hook subagent-stop`
1755
+ }
1756
+ ]
1757
+ }
1758
+ ],
1759
+ UserPromptSubmit: [
1760
+ {
1761
+ hooks: [
1762
+ {
1763
+ type: "command",
1764
+ command: `${HOOK_PREFIX} hook prompt-submit`
1765
+ }
1766
+ ]
1767
+ }
1768
+ ],
1285
1769
  SessionEnd: [
1286
1770
  {
1287
1771
  matcher: "*",
1288
1772
  hooks: [
1289
1773
  {
1290
1774
  type: "command",
1291
- command: `${HOOK_PREFIX} hook session-end`
1775
+ command: `${HOOK_PREFIX} hook session-end`,
1776
+ timeout: 1e4
1292
1777
  }
1293
1778
  ]
1294
1779
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usevalt/cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Valt CLI — trust layer for AI-assisted development",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -39,8 +39,8 @@
39
39
  "tsup": "^8.4.0",
40
40
  "typescript": "^5.7.0",
41
41
  "vitest": "^3.2.0",
42
- "@usevalt/typescript-config": "0.0.0",
43
- "@usevalt/eslint-config": "0.0.0"
42
+ "@usevalt/eslint-config": "0.0.0",
43
+ "@usevalt/typescript-config": "0.0.0"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsup",