@usevalt/cli 0.5.0 → 0.6.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/index.js +538 -33
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -491,6 +491,52 @@ 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 EXPECTED_HOOKS = [
|
|
503
|
+
"SessionStart",
|
|
504
|
+
"PreToolUse",
|
|
505
|
+
"PostToolUse",
|
|
506
|
+
"PostToolUseFailure",
|
|
507
|
+
"SubagentStart",
|
|
508
|
+
"SubagentStop",
|
|
509
|
+
"UserPromptSubmit",
|
|
510
|
+
"SessionEnd"
|
|
511
|
+
];
|
|
512
|
+
const hooksPath = `${process.env["HOME"] ?? "~"}/.claude/hooks.json`;
|
|
513
|
+
try {
|
|
514
|
+
const { existsSync: exists, readFileSync: read } = await import("fs");
|
|
515
|
+
if (exists(hooksPath)) {
|
|
516
|
+
const hooksContent = read(hooksPath, "utf-8");
|
|
517
|
+
const hooksConfig = JSON.parse(hooksContent);
|
|
518
|
+
const registeredHooks = hooksConfig.hooks ?? {};
|
|
519
|
+
const presentHooks = EXPECTED_HOOKS.filter((name) => {
|
|
520
|
+
const hookEntries = registeredHooks[name];
|
|
521
|
+
return Array.isArray(hookEntries) && JSON.stringify(hookEntries).includes("valt");
|
|
522
|
+
});
|
|
523
|
+
const missingHooks = EXPECTED_HOOKS.filter((h) => !presentHooks.includes(h));
|
|
524
|
+
if (missingHooks.length === 0) {
|
|
525
|
+
success(`Claude Code hooks: all ${EXPECTED_HOOKS.length} registered`);
|
|
526
|
+
} else if (presentHooks.length === 0) {
|
|
527
|
+
info("Claude Code hooks: not installed. Run `valt setup` to enable.");
|
|
528
|
+
} else {
|
|
529
|
+
warn(
|
|
530
|
+
`Claude Code hooks: ${presentHooks.length}/${EXPECTED_HOOKS.length} registered. Missing: ${missingHooks.join(", ")}. Run \`valt setup\` to update.`
|
|
531
|
+
);
|
|
532
|
+
issues++;
|
|
533
|
+
}
|
|
534
|
+
} else {
|
|
535
|
+
info("Claude Code hooks: not installed. Run `valt setup` to enable.");
|
|
536
|
+
}
|
|
537
|
+
} catch {
|
|
538
|
+
info("Claude Code hooks: could not check.");
|
|
539
|
+
}
|
|
494
540
|
console.log("");
|
|
495
541
|
if (issues === 0) {
|
|
496
542
|
success("All checks passed!");
|
|
@@ -995,13 +1041,43 @@ proxyCommand.command("stop").description("Stop the MCP proxy").action(() => {
|
|
|
995
1041
|
|
|
996
1042
|
// src/commands/hook.ts
|
|
997
1043
|
import { Command as Command13 } from "commander";
|
|
998
|
-
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, existsSync as existsSync3 } from "fs";
|
|
1044
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, existsSync as existsSync3, readdirSync } from "fs";
|
|
999
1045
|
import { createReadStream } from "fs";
|
|
1000
1046
|
import { createInterface } from "readline";
|
|
1001
1047
|
import { execSync } from "child_process";
|
|
1002
|
-
import { basename } from "path";
|
|
1048
|
+
import { basename, join } from "path";
|
|
1049
|
+
import { createHash } from "crypto";
|
|
1003
1050
|
import os2 from "os";
|
|
1004
|
-
|
|
1051
|
+
function getSessionFile(claudeSessionId) {
|
|
1052
|
+
return join(os2.tmpdir(), `valt-session-${claudeSessionId}.json`);
|
|
1053
|
+
}
|
|
1054
|
+
function findLatestSessionFile() {
|
|
1055
|
+
try {
|
|
1056
|
+
const tmpDir = os2.tmpdir();
|
|
1057
|
+
const files = readdirSync(tmpDir).filter((f) => f.startsWith("valt-session-") && f.endsWith(".json")).map((f) => ({
|
|
1058
|
+
name: f,
|
|
1059
|
+
path: join(tmpDir, f),
|
|
1060
|
+
mtime: (() => {
|
|
1061
|
+
try {
|
|
1062
|
+
return readFileSync3(join(tmpDir, f), "utf-8");
|
|
1063
|
+
} catch {
|
|
1064
|
+
return "";
|
|
1065
|
+
}
|
|
1066
|
+
})()
|
|
1067
|
+
})).filter((f) => f.mtime.length > 0).sort((a, b) => {
|
|
1068
|
+
try {
|
|
1069
|
+
const aData = JSON.parse(a.mtime);
|
|
1070
|
+
const bData = JSON.parse(b.mtime);
|
|
1071
|
+
return (bData.startedAt ?? "").localeCompare(aData.startedAt ?? "");
|
|
1072
|
+
} catch {
|
|
1073
|
+
return 0;
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
return files[0]?.path ?? null;
|
|
1077
|
+
} catch {
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1005
1081
|
async function readStdin() {
|
|
1006
1082
|
if (process.stdin.isTTY) return {};
|
|
1007
1083
|
try {
|
|
@@ -1042,14 +1118,53 @@ function detectProjectSlug() {
|
|
|
1042
1118
|
}
|
|
1043
1119
|
return basename(process.cwd());
|
|
1044
1120
|
}
|
|
1045
|
-
function
|
|
1121
|
+
function detectDeveloper() {
|
|
1122
|
+
const result = {};
|
|
1123
|
+
if (process.env["VALT_USER_EMAIL"]) {
|
|
1124
|
+
result.email = process.env["VALT_USER_EMAIL"];
|
|
1125
|
+
}
|
|
1126
|
+
if (!result.email) {
|
|
1127
|
+
try {
|
|
1128
|
+
result.email = execSync("git config user.email", {
|
|
1129
|
+
encoding: "utf-8",
|
|
1130
|
+
timeout: 3e3,
|
|
1131
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1132
|
+
}).trim() || void 0;
|
|
1133
|
+
} catch {
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1046
1136
|
try {
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1137
|
+
result.name = execSync("git config user.name", {
|
|
1138
|
+
encoding: "utf-8",
|
|
1139
|
+
timeout: 3e3,
|
|
1140
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1141
|
+
}).trim() || void 0;
|
|
1050
1142
|
} catch {
|
|
1051
|
-
return null;
|
|
1052
1143
|
}
|
|
1144
|
+
if (!result.name) {
|
|
1145
|
+
result.name = process.env["USER"] ?? process.env["LOGNAME"];
|
|
1146
|
+
}
|
|
1147
|
+
return result;
|
|
1148
|
+
}
|
|
1149
|
+
function readSessionFile(claudeSessionId) {
|
|
1150
|
+
if (claudeSessionId) {
|
|
1151
|
+
const path = getSessionFile(claudeSessionId);
|
|
1152
|
+
try {
|
|
1153
|
+
if (existsSync3(path)) {
|
|
1154
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
1155
|
+
}
|
|
1156
|
+
} catch {
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
const fallbackPath = findLatestSessionFile();
|
|
1160
|
+
if (fallbackPath) {
|
|
1161
|
+
try {
|
|
1162
|
+
return JSON.parse(readFileSync3(fallbackPath, "utf-8"));
|
|
1163
|
+
} catch {
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return null;
|
|
1053
1168
|
}
|
|
1054
1169
|
async function sendEvents(endpoint, apiKey, events) {
|
|
1055
1170
|
try {
|
|
@@ -1071,7 +1186,6 @@ var MODEL_PRICING = {
|
|
|
1071
1186
|
"claude-opus-4-6": { input: 15, output: 75, cacheRead: 1.5 },
|
|
1072
1187
|
"claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3 },
|
|
1073
1188
|
"claude-haiku-4-5": { input: 0.8, output: 4, cacheRead: 0.08 },
|
|
1074
|
-
// Legacy model names
|
|
1075
1189
|
"claude-3-5-sonnet": { input: 3, output: 15, cacheRead: 0.3 },
|
|
1076
1190
|
"claude-3-5-haiku": { input: 0.8, output: 4, cacheRead: 0.08 },
|
|
1077
1191
|
"claude-3-opus": { input: 15, output: 75, cacheRead: 1.5 }
|
|
@@ -1081,6 +1195,75 @@ function calculateCost(model, promptTokens, completionTokens, cacheReadTokens) {
|
|
|
1081
1195
|
if (!pricing) return 0;
|
|
1082
1196
|
return promptTokens / 1e6 * pricing.input + completionTokens / 1e6 * pricing.output + cacheReadTokens / 1e6 * pricing.cacheRead;
|
|
1083
1197
|
}
|
|
1198
|
+
function classifyToolCall(toolName, toolInput) {
|
|
1199
|
+
switch (toolName) {
|
|
1200
|
+
case "Read":
|
|
1201
|
+
return { eventType: "file.read", filePath: toolInput["file_path"] };
|
|
1202
|
+
case "Write":
|
|
1203
|
+
case "Edit":
|
|
1204
|
+
case "NotebookEdit":
|
|
1205
|
+
return { eventType: "file.write", filePath: toolInput["file_path"] ?? toolInput["notebook_path"] };
|
|
1206
|
+
case "Bash": {
|
|
1207
|
+
const command = toolInput["command"] ?? "";
|
|
1208
|
+
return { eventType: "command.execute", command };
|
|
1209
|
+
}
|
|
1210
|
+
case "Glob":
|
|
1211
|
+
case "Grep":
|
|
1212
|
+
return { eventType: "file.read", filePath: toolInput["path"] ?? toolInput["pattern"] };
|
|
1213
|
+
default:
|
|
1214
|
+
return { eventType: "tool.call" };
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
function isTestCommand(command) {
|
|
1218
|
+
const testPatterns = [
|
|
1219
|
+
/\b(jest|vitest|mocha|ava)\b/,
|
|
1220
|
+
/\bpnpm\s+(test|t)\b/,
|
|
1221
|
+
/\bnpm\s+(test|t)\b/,
|
|
1222
|
+
/\bnpx\s+(jest|vitest)\b/,
|
|
1223
|
+
/\bpytest\b/,
|
|
1224
|
+
/\bpython\s+-m\s+pytest\b/,
|
|
1225
|
+
/\bcargo\s+test\b/,
|
|
1226
|
+
/\bgo\s+test\b/,
|
|
1227
|
+
/\bmake\s+test\b/
|
|
1228
|
+
];
|
|
1229
|
+
return testPatterns.some((p) => p.test(command));
|
|
1230
|
+
}
|
|
1231
|
+
function parseTestResults(stdout) {
|
|
1232
|
+
const vitestMatch = stdout.match(/Tests\s+(\d+)\s+passed.*?(\d+)\s+failed/i) ?? stdout.match(/Tests\s+(\d+)\s+passed\s+\((\d+)\)/i);
|
|
1233
|
+
if (vitestMatch) {
|
|
1234
|
+
const passed = parseInt(vitestMatch[1], 10);
|
|
1235
|
+
const failedOrTotal = parseInt(vitestMatch[2], 10);
|
|
1236
|
+
const failed = stdout.includes("failed") ? failedOrTotal : 0;
|
|
1237
|
+
const total = stdout.includes("failed") ? passed + failed : failedOrTotal;
|
|
1238
|
+
return { testsRun: total, testsPassed: passed, testsFailed: failed, framework: "vitest" };
|
|
1239
|
+
}
|
|
1240
|
+
const jestMatch = stdout.match(/Tests:\s+(?:(\d+)\s+failed,\s+)?(\d+)\s+passed,\s+(\d+)\s+total/i);
|
|
1241
|
+
if (jestMatch) {
|
|
1242
|
+
const failed = parseInt(jestMatch[1] ?? "0", 10);
|
|
1243
|
+
const passed = parseInt(jestMatch[2], 10);
|
|
1244
|
+
const total = parseInt(jestMatch[3], 10);
|
|
1245
|
+
return { testsRun: total, testsPassed: passed, testsFailed: failed, framework: "jest" };
|
|
1246
|
+
}
|
|
1247
|
+
const pytestMatch = stdout.match(/(\d+)\s+passed(?:,\s+(\d+)\s+failed)?/i);
|
|
1248
|
+
if (pytestMatch) {
|
|
1249
|
+
const passed = parseInt(pytestMatch[1], 10);
|
|
1250
|
+
const failed = parseInt(pytestMatch[2] ?? "0", 10);
|
|
1251
|
+
return { testsRun: passed + failed, testsPassed: passed, testsFailed: failed, framework: "pytest" };
|
|
1252
|
+
}
|
|
1253
|
+
const goMatch = stdout.match(/^(ok|FAIL)\s+\S+/m);
|
|
1254
|
+
if (goMatch) {
|
|
1255
|
+
const passed = goMatch[1] === "ok" ? 1 : 0;
|
|
1256
|
+
const failed = goMatch[1] === "FAIL" ? 1 : 0;
|
|
1257
|
+
return { testsRun: 1, testsPassed: passed, testsFailed: failed, framework: "go" };
|
|
1258
|
+
}
|
|
1259
|
+
const cargoMatch = stdout.match(/test result: \w+\.\s+(\d+)\s+passed;\s+(\d+)\s+failed/i);
|
|
1260
|
+
if (cargoMatch) {
|
|
1261
|
+
const passed = parseInt(cargoMatch[1], 10);
|
|
1262
|
+
const failed = parseInt(cargoMatch[2], 10);
|
|
1263
|
+
return { testsRun: passed + failed, testsPassed: passed, testsFailed: failed, framework: "cargo" };
|
|
1264
|
+
}
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1084
1267
|
async function parseTranscript(transcriptPath) {
|
|
1085
1268
|
const result = {
|
|
1086
1269
|
totalCostUsd: 0,
|
|
@@ -1133,13 +1316,35 @@ var sessionStartCommand = new Command13("session-start").description("Hook: call
|
|
|
1133
1316
|
try {
|
|
1134
1317
|
const { apiKey, endpoint, apiEndpoint } = resolveConfig();
|
|
1135
1318
|
const projectSlug = detectProjectSlug();
|
|
1319
|
+
const developer = detectDeveloper();
|
|
1136
1320
|
const hookData = await readStdin();
|
|
1137
|
-
const claudeSessionId = hookData["session_id"];
|
|
1321
|
+
const claudeSessionId = hookData["session_id"] ?? crypto.randomUUID();
|
|
1138
1322
|
const sessionId = crypto.randomUUID();
|
|
1139
1323
|
const model = hookData["model"] || process.env["CLAUDE_MODEL"] || "unknown";
|
|
1140
1324
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1141
|
-
const
|
|
1142
|
-
|
|
1325
|
+
const cwd = hookData["cwd"] || process.cwd();
|
|
1326
|
+
const state = {
|
|
1327
|
+
sessionId,
|
|
1328
|
+
claudeSessionId,
|
|
1329
|
+
startedAt,
|
|
1330
|
+
apiKey,
|
|
1331
|
+
endpoint,
|
|
1332
|
+
apiEndpoint,
|
|
1333
|
+
projectSlug,
|
|
1334
|
+
model,
|
|
1335
|
+
cwd
|
|
1336
|
+
};
|
|
1337
|
+
writeFileSync3(getSessionFile(claudeSessionId), JSON.stringify(state, null, 2), { mode: 384 });
|
|
1338
|
+
let gitCommitBefore;
|
|
1339
|
+
try {
|
|
1340
|
+
gitCommitBefore = execSync("git rev-parse HEAD", {
|
|
1341
|
+
encoding: "utf-8",
|
|
1342
|
+
timeout: 3e3,
|
|
1343
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1344
|
+
cwd
|
|
1345
|
+
}).trim();
|
|
1346
|
+
} catch {
|
|
1347
|
+
}
|
|
1143
1348
|
const sent = await sendEvents(endpoint, apiKey, [
|
|
1144
1349
|
{
|
|
1145
1350
|
event_id: crypto.randomUUID(),
|
|
@@ -1155,7 +1360,11 @@ var sessionStartCommand = new Command13("session-start").description("Hook: call
|
|
|
1155
1360
|
repository: process.env["CLAUDE_REPO"],
|
|
1156
1361
|
branch: process.env["CLAUDE_BRANCH"],
|
|
1157
1362
|
project_slug: projectSlug,
|
|
1158
|
-
cwd
|
|
1363
|
+
cwd,
|
|
1364
|
+
commit_sha: gitCommitBefore,
|
|
1365
|
+
// 1.9: Developer attribution
|
|
1366
|
+
developer_email: developer.email,
|
|
1367
|
+
developer_name: developer.name
|
|
1159
1368
|
}
|
|
1160
1369
|
}
|
|
1161
1370
|
]);
|
|
@@ -1168,36 +1377,276 @@ var sessionStartCommand = new Command13("session-start").description("Hook: call
|
|
|
1168
1377
|
warn(`Failed to start session: ${err instanceof Error ? err.message : String(err)}`);
|
|
1169
1378
|
}
|
|
1170
1379
|
});
|
|
1171
|
-
var toolCallCommand = new Command13("tool-call").description("Hook: called on each Claude Code tool call (
|
|
1380
|
+
var toolCallCommand = new Command13("tool-call").description("Hook: called on each Claude Code tool call (PreToolUse)").action(async () => {
|
|
1172
1381
|
try {
|
|
1173
|
-
const
|
|
1382
|
+
const hookData = await readStdin();
|
|
1383
|
+
const claudeSessionId = hookData["session_id"];
|
|
1384
|
+
const session = readSessionFile(claudeSessionId);
|
|
1174
1385
|
if (!session) return;
|
|
1175
|
-
const
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
{
|
|
1386
|
+
const toolName = hookData["tool_name"] ?? hookData["name"] ?? "unknown";
|
|
1387
|
+
const toolInput = hookData["tool_input"] ?? {};
|
|
1388
|
+
if (session.apiEndpoint) {
|
|
1389
|
+
try {
|
|
1390
|
+
const gateController = new AbortController();
|
|
1391
|
+
const gateTimeout = setTimeout(() => gateController.abort(), 3e3);
|
|
1392
|
+
const toolInputSummary = JSON.stringify(toolInput).slice(0, 500);
|
|
1393
|
+
const gateRes = await fetch(
|
|
1394
|
+
`${session.apiEndpoint}/api/v1/sessions/${session.sessionId}/gate`,
|
|
1395
|
+
{
|
|
1396
|
+
method: "POST",
|
|
1397
|
+
headers: {
|
|
1398
|
+
"Content-Type": "application/json",
|
|
1399
|
+
Authorization: `Bearer ${session.apiKey}`
|
|
1400
|
+
},
|
|
1401
|
+
body: JSON.stringify({
|
|
1402
|
+
tool_name: toolName,
|
|
1403
|
+
tool_input_summary: toolInputSummary
|
|
1404
|
+
}),
|
|
1405
|
+
signal: gateController.signal
|
|
1406
|
+
}
|
|
1407
|
+
);
|
|
1408
|
+
clearTimeout(gateTimeout);
|
|
1409
|
+
if (gateRes.ok) {
|
|
1410
|
+
const gateBody = await gateRes.json();
|
|
1411
|
+
const gateAction = gateBody.data?.action;
|
|
1412
|
+
if (gateAction === "pause") {
|
|
1413
|
+
warn(`Gate: ${gateBody.data?.reason ?? "Paused by administrator"}`);
|
|
1414
|
+
process.exit(2);
|
|
1415
|
+
}
|
|
1416
|
+
if (gateAction === "block") {
|
|
1417
|
+
warn(`Gate: ${gateBody.data?.reason ?? "Blocked by administrator"}`);
|
|
1418
|
+
process.exit(2);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
} catch {
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
const { eventType, filePath, command } = classifyToolCall(toolName, toolInput);
|
|
1425
|
+
const events = [];
|
|
1426
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1427
|
+
events.push({
|
|
1428
|
+
event_id: crypto.randomUUID(),
|
|
1429
|
+
session_id: session.sessionId,
|
|
1430
|
+
event_type: "tool.call",
|
|
1431
|
+
timestamp,
|
|
1432
|
+
tool: "claude-code",
|
|
1433
|
+
tool_name: toolName,
|
|
1434
|
+
metadata: {
|
|
1435
|
+
tool_name: toolName,
|
|
1436
|
+
file_path: filePath,
|
|
1437
|
+
command
|
|
1438
|
+
}
|
|
1439
|
+
});
|
|
1440
|
+
if (eventType !== "tool.call") {
|
|
1441
|
+
events.push({
|
|
1179
1442
|
event_id: crypto.randomUUID(),
|
|
1180
1443
|
session_id: session.sessionId,
|
|
1181
|
-
event_type:
|
|
1182
|
-
timestamp
|
|
1444
|
+
event_type: eventType,
|
|
1445
|
+
timestamp,
|
|
1183
1446
|
tool: "claude-code",
|
|
1184
1447
|
tool_name: toolName,
|
|
1185
|
-
|
|
1448
|
+
file_path: filePath,
|
|
1449
|
+
metadata: {
|
|
1450
|
+
tool_name: toolName,
|
|
1451
|
+
...filePath ? { file_path: filePath } : {},
|
|
1452
|
+
...command ? { command } : {}
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
if (eventType === "command.execute" && command && isTestCommand(command)) {
|
|
1456
|
+
events.push({
|
|
1457
|
+
event_id: crypto.randomUUID(),
|
|
1458
|
+
session_id: session.sessionId,
|
|
1459
|
+
event_type: "test.run",
|
|
1460
|
+
timestamp,
|
|
1461
|
+
tool: "claude-code",
|
|
1462
|
+
metadata: { command, source: "pre-tool-detect" }
|
|
1463
|
+
});
|
|
1186
1464
|
}
|
|
1187
|
-
|
|
1465
|
+
}
|
|
1466
|
+
await sendEvents(session.endpoint, session.apiKey, events);
|
|
1188
1467
|
} catch {
|
|
1189
1468
|
}
|
|
1190
1469
|
});
|
|
1191
|
-
var
|
|
1470
|
+
var postToolUseCommand = new Command13("post-tool-use").description("Hook: called after a tool call completes (PostToolUse)").action(async () => {
|
|
1192
1471
|
try {
|
|
1193
|
-
const
|
|
1472
|
+
const hookData = await readStdin();
|
|
1473
|
+
const claudeSessionId = hookData["session_id"];
|
|
1474
|
+
const session = readSessionFile(claudeSessionId);
|
|
1194
1475
|
if (!session) return;
|
|
1476
|
+
const toolName = hookData["tool_name"] ?? "unknown";
|
|
1477
|
+
const toolResponse = hookData["tool_response"];
|
|
1478
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1479
|
+
const events = [{
|
|
1480
|
+
event_id: crypto.randomUUID(),
|
|
1481
|
+
session_id: session.sessionId,
|
|
1482
|
+
event_type: "tool.result",
|
|
1483
|
+
timestamp,
|
|
1484
|
+
tool: "claude-code",
|
|
1485
|
+
tool_name: toolName,
|
|
1486
|
+
metadata: {
|
|
1487
|
+
tool_name: toolName,
|
|
1488
|
+
exit_code: toolResponse?.["exit_code"],
|
|
1489
|
+
success: toolResponse?.["success"] ?? true
|
|
1490
|
+
}
|
|
1491
|
+
}];
|
|
1492
|
+
if (toolName === "Bash" && toolResponse) {
|
|
1493
|
+
const stdout = toolResponse["stdout"] ?? toolResponse["output"] ?? "";
|
|
1494
|
+
if (stdout) {
|
|
1495
|
+
const testResults = parseTestResults(stdout);
|
|
1496
|
+
if (testResults) {
|
|
1497
|
+
events.push({
|
|
1498
|
+
event_id: crypto.randomUUID(),
|
|
1499
|
+
session_id: session.sessionId,
|
|
1500
|
+
event_type: "test.run",
|
|
1501
|
+
timestamp,
|
|
1502
|
+
tool: "claude-code",
|
|
1503
|
+
metadata: {
|
|
1504
|
+
tests_run: testResults.testsRun,
|
|
1505
|
+
tests_passed: testResults.testsPassed,
|
|
1506
|
+
tests_failed: testResults.testsFailed,
|
|
1507
|
+
framework: testResults.framework,
|
|
1508
|
+
source: "post-tool-parse"
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
await sendEvents(session.endpoint, session.apiKey, events);
|
|
1515
|
+
} catch {
|
|
1516
|
+
}
|
|
1517
|
+
});
|
|
1518
|
+
var toolErrorCommand = new Command13("tool-error").description("Hook: called when a tool call fails (PostToolUseFailure)").action(async () => {
|
|
1519
|
+
try {
|
|
1520
|
+
const hookData = await readStdin();
|
|
1521
|
+
const claudeSessionId = hookData["session_id"];
|
|
1522
|
+
const session = readSessionFile(claudeSessionId);
|
|
1523
|
+
if (!session) return;
|
|
1524
|
+
const toolName = hookData["tool_name"] ?? "unknown";
|
|
1525
|
+
const errorMsg = hookData["error"] ?? "unknown error";
|
|
1526
|
+
const isInterrupt = hookData["is_interrupt"] ?? false;
|
|
1527
|
+
await sendEvents(session.endpoint, session.apiKey, [{
|
|
1528
|
+
event_id: crypto.randomUUID(),
|
|
1529
|
+
session_id: session.sessionId,
|
|
1530
|
+
event_type: "tool.error",
|
|
1531
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1532
|
+
tool: "claude-code",
|
|
1533
|
+
tool_name: toolName,
|
|
1534
|
+
metadata: {
|
|
1535
|
+
tool_name: toolName,
|
|
1536
|
+
error_message: errorMsg,
|
|
1537
|
+
is_interrupt: isInterrupt
|
|
1538
|
+
}
|
|
1539
|
+
}]);
|
|
1540
|
+
} catch {
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
var subagentStartCommand = new Command13("subagent-start").description("Hook: called when a subagent starts").action(async () => {
|
|
1544
|
+
try {
|
|
1195
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.start",
|
|
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
|
+
}
|
|
1559
|
+
}]);
|
|
1560
|
+
} catch {
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
var subagentStopCommand = new Command13("subagent-stop").description("Hook: called when a subagent stops").action(async () => {
|
|
1564
|
+
try {
|
|
1565
|
+
const hookData = await readStdin();
|
|
1566
|
+
const claudeSessionId = hookData["session_id"];
|
|
1567
|
+
const session = readSessionFile(claudeSessionId);
|
|
1568
|
+
if (!session) return;
|
|
1569
|
+
await sendEvents(session.endpoint, session.apiKey, [{
|
|
1570
|
+
event_id: crypto.randomUUID(),
|
|
1571
|
+
session_id: session.sessionId,
|
|
1572
|
+
event_type: "subagent.stop",
|
|
1573
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1574
|
+
tool: "claude-code",
|
|
1575
|
+
metadata: {
|
|
1576
|
+
agent_id: hookData["agent_id"],
|
|
1577
|
+
agent_type: hookData["agent_type"],
|
|
1578
|
+
transcript_path: hookData["agent_transcript_path"]
|
|
1579
|
+
}
|
|
1580
|
+
}]);
|
|
1581
|
+
} catch {
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
var promptSubmitCommand = new Command13("prompt-submit").description("Hook: called when a user submits a prompt").action(async () => {
|
|
1585
|
+
try {
|
|
1586
|
+
const hookData = await readStdin();
|
|
1587
|
+
const claudeSessionId = hookData["session_id"];
|
|
1588
|
+
const session = readSessionFile(claudeSessionId);
|
|
1589
|
+
if (!session) return;
|
|
1590
|
+
const promptText = hookData["prompt"] ?? "";
|
|
1591
|
+
const promptHashValue = createHash("sha256").update(promptText).digest("hex");
|
|
1592
|
+
await sendEvents(session.endpoint, session.apiKey, [{
|
|
1593
|
+
event_id: crypto.randomUUID(),
|
|
1594
|
+
session_id: session.sessionId,
|
|
1595
|
+
event_type: "prompt.submit",
|
|
1596
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1597
|
+
tool: "claude-code",
|
|
1598
|
+
metadata: {
|
|
1599
|
+
prompt_hash: promptHashValue,
|
|
1600
|
+
prompt_length: promptText.length
|
|
1601
|
+
}
|
|
1602
|
+
}]);
|
|
1603
|
+
} catch {
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
var sessionEndCommand = new Command13("session-end").description("Hook: called when a Claude Code session ends").action(async () => {
|
|
1607
|
+
try {
|
|
1608
|
+
const hookData = await readStdin();
|
|
1609
|
+
const claudeSessionId = hookData["session_id"];
|
|
1610
|
+
let session = readSessionFile(claudeSessionId);
|
|
1611
|
+
let sessionFilePath;
|
|
1612
|
+
if (session) {
|
|
1613
|
+
sessionFilePath = claudeSessionId ? getSessionFile(claudeSessionId) : findLatestSessionFile() ?? void 0;
|
|
1614
|
+
} else {
|
|
1615
|
+
const config = (() => {
|
|
1616
|
+
try {
|
|
1617
|
+
return resolveConfig();
|
|
1618
|
+
} catch {
|
|
1619
|
+
return null;
|
|
1620
|
+
}
|
|
1621
|
+
})();
|
|
1622
|
+
if (!config) return;
|
|
1623
|
+
session = {
|
|
1624
|
+
sessionId: claudeSessionId ?? crypto.randomUUID(),
|
|
1625
|
+
claudeSessionId: claudeSessionId ?? "unknown",
|
|
1626
|
+
startedAt: new Date(Date.now() - 6e4).toISOString(),
|
|
1627
|
+
// Assume 1 min ago
|
|
1628
|
+
apiKey: config.apiKey,
|
|
1629
|
+
endpoint: config.endpoint,
|
|
1630
|
+
apiEndpoint: config.apiEndpoint,
|
|
1631
|
+
cwd: hookData["cwd"] || process.cwd()
|
|
1632
|
+
};
|
|
1633
|
+
warn("Session file not found \u2014 sending session data from stdin/config fallback.");
|
|
1634
|
+
}
|
|
1196
1635
|
const now = /* @__PURE__ */ new Date();
|
|
1197
1636
|
const startedAt = new Date(session.startedAt);
|
|
1198
1637
|
const durationMs = now.getTime() - startedAt.getTime();
|
|
1199
1638
|
const transcriptPath = hookData["transcript_path"];
|
|
1200
1639
|
const metrics = transcriptPath ? await parseTranscript(transcriptPath) : null;
|
|
1640
|
+
let gitCommitAfter;
|
|
1641
|
+
try {
|
|
1642
|
+
gitCommitAfter = execSync("git rev-parse HEAD", {
|
|
1643
|
+
encoding: "utf-8",
|
|
1644
|
+
timeout: 3e3,
|
|
1645
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1646
|
+
cwd: session.cwd
|
|
1647
|
+
}).trim();
|
|
1648
|
+
} catch {
|
|
1649
|
+
}
|
|
1201
1650
|
const events = [
|
|
1202
1651
|
{
|
|
1203
1652
|
event_id: crypto.randomUUID(),
|
|
@@ -1211,6 +1660,7 @@ var sessionEndCommand = new Command13("session-end").description("Hook: called w
|
|
|
1211
1660
|
started_at: session.startedAt,
|
|
1212
1661
|
ended_at: now.toISOString(),
|
|
1213
1662
|
reason: hookData["reason"] ?? "unknown",
|
|
1663
|
+
git_commit_after: gitCommitAfter,
|
|
1214
1664
|
...metrics ? {
|
|
1215
1665
|
prompt_tokens: metrics.promptTokens,
|
|
1216
1666
|
completion_tokens: metrics.completionTokens,
|
|
@@ -1239,9 +1689,11 @@ var sessionEndCommand = new Command13("session-end").description("Hook: called w
|
|
|
1239
1689
|
}
|
|
1240
1690
|
sendEvents(session.endpoint, session.apiKey, events).catch(() => {
|
|
1241
1691
|
});
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1692
|
+
if (sessionFilePath) {
|
|
1693
|
+
try {
|
|
1694
|
+
unlinkSync2(sessionFilePath);
|
|
1695
|
+
} catch {
|
|
1696
|
+
}
|
|
1245
1697
|
}
|
|
1246
1698
|
const costStr = metrics?.totalCostUsd ? ` $${metrics.totalCostUsd.toFixed(4)}` : "";
|
|
1247
1699
|
const tokenStr = metrics?.promptTokens ? ` ${metrics.promptTokens + metrics.completionTokens} tokens` : "";
|
|
@@ -1249,15 +1701,15 @@ var sessionEndCommand = new Command13("session-end").description("Hook: called w
|
|
|
1249
1701
|
} catch {
|
|
1250
1702
|
}
|
|
1251
1703
|
});
|
|
1252
|
-
var hookCommand = new Command13("hook").description("Claude Code hook handlers for Valt session tracking").addCommand(sessionStartCommand).addCommand(toolCallCommand).addCommand(sessionEndCommand);
|
|
1704
|
+
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
1705
|
|
|
1254
1706
|
// src/commands/setup.ts
|
|
1255
1707
|
import { Command as Command14 } from "commander";
|
|
1256
1708
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
|
|
1257
|
-
import { join } from "path";
|
|
1709
|
+
import { join as join2 } from "path";
|
|
1258
1710
|
import os3 from "os";
|
|
1259
|
-
var CLAUDE_DIR =
|
|
1260
|
-
var HOOKS_FILE =
|
|
1711
|
+
var CLAUDE_DIR = join2(os3.homedir(), ".claude");
|
|
1712
|
+
var HOOKS_FILE = join2(CLAUDE_DIR, "hooks.json");
|
|
1261
1713
|
var HOOK_PREFIX = "npx --yes @usevalt/cli";
|
|
1262
1714
|
function getValtHooks() {
|
|
1263
1715
|
return {
|
|
@@ -1282,13 +1734,66 @@ function getValtHooks() {
|
|
|
1282
1734
|
]
|
|
1283
1735
|
}
|
|
1284
1736
|
],
|
|
1737
|
+
PostToolUse: [
|
|
1738
|
+
{
|
|
1739
|
+
matcher: "*",
|
|
1740
|
+
hooks: [
|
|
1741
|
+
{
|
|
1742
|
+
type: "command",
|
|
1743
|
+
command: `${HOOK_PREFIX} hook post-tool-use`
|
|
1744
|
+
}
|
|
1745
|
+
]
|
|
1746
|
+
}
|
|
1747
|
+
],
|
|
1748
|
+
PostToolUseFailure: [
|
|
1749
|
+
{
|
|
1750
|
+
matcher: "*",
|
|
1751
|
+
hooks: [
|
|
1752
|
+
{
|
|
1753
|
+
type: "command",
|
|
1754
|
+
command: `${HOOK_PREFIX} hook tool-error`
|
|
1755
|
+
}
|
|
1756
|
+
]
|
|
1757
|
+
}
|
|
1758
|
+
],
|
|
1759
|
+
SubagentStart: [
|
|
1760
|
+
{
|
|
1761
|
+
hooks: [
|
|
1762
|
+
{
|
|
1763
|
+
type: "command",
|
|
1764
|
+
command: `${HOOK_PREFIX} hook subagent-start`
|
|
1765
|
+
}
|
|
1766
|
+
]
|
|
1767
|
+
}
|
|
1768
|
+
],
|
|
1769
|
+
SubagentStop: [
|
|
1770
|
+
{
|
|
1771
|
+
hooks: [
|
|
1772
|
+
{
|
|
1773
|
+
type: "command",
|
|
1774
|
+
command: `${HOOK_PREFIX} hook subagent-stop`
|
|
1775
|
+
}
|
|
1776
|
+
]
|
|
1777
|
+
}
|
|
1778
|
+
],
|
|
1779
|
+
UserPromptSubmit: [
|
|
1780
|
+
{
|
|
1781
|
+
hooks: [
|
|
1782
|
+
{
|
|
1783
|
+
type: "command",
|
|
1784
|
+
command: `${HOOK_PREFIX} hook prompt-submit`
|
|
1785
|
+
}
|
|
1786
|
+
]
|
|
1787
|
+
}
|
|
1788
|
+
],
|
|
1285
1789
|
SessionEnd: [
|
|
1286
1790
|
{
|
|
1287
1791
|
matcher: "*",
|
|
1288
1792
|
hooks: [
|
|
1289
1793
|
{
|
|
1290
1794
|
type: "command",
|
|
1291
|
-
command: `${HOOK_PREFIX} hook session-end
|
|
1795
|
+
command: `${HOOK_PREFIX} hook session-end`,
|
|
1796
|
+
timeout: 1e4
|
|
1292
1797
|
}
|
|
1293
1798
|
]
|
|
1294
1799
|
}
|