@usevalt/cli 0.4.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.
- package/dist/index.js +559 -46
- 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
|
-
|
|
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 {
|
|
@@ -1010,7 +1066,7 @@ async function readStdin() {
|
|
|
1010
1066
|
process.stdin.setEncoding("utf-8");
|
|
1011
1067
|
process.stdin.on("data", (chunk) => chunks.push(String(chunk)));
|
|
1012
1068
|
process.stdin.on("end", () => resolve4(chunks.join("")));
|
|
1013
|
-
setTimeout(() => resolve4(chunks.join("")),
|
|
1069
|
+
setTimeout(() => resolve4(chunks.join("")), 500);
|
|
1014
1070
|
});
|
|
1015
1071
|
return data.trim() ? JSON.parse(data) : {};
|
|
1016
1072
|
} catch {
|
|
@@ -1042,14 +1098,53 @@ function detectProjectSlug() {
|
|
|
1042
1098
|
}
|
|
1043
1099
|
return basename(process.cwd());
|
|
1044
1100
|
}
|
|
1045
|
-
function
|
|
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
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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 {
|
|
@@ -1067,13 +1162,96 @@ async function sendEvents(endpoint, apiKey, events) {
|
|
|
1067
1162
|
return false;
|
|
1068
1163
|
}
|
|
1069
1164
|
}
|
|
1165
|
+
var MODEL_PRICING = {
|
|
1166
|
+
"claude-opus-4-6": { input: 15, output: 75, cacheRead: 1.5 },
|
|
1167
|
+
"claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3 },
|
|
1168
|
+
"claude-haiku-4-5": { input: 0.8, output: 4, cacheRead: 0.08 },
|
|
1169
|
+
"claude-3-5-sonnet": { input: 3, output: 15, cacheRead: 0.3 },
|
|
1170
|
+
"claude-3-5-haiku": { input: 0.8, output: 4, cacheRead: 0.08 },
|
|
1171
|
+
"claude-3-opus": { input: 15, output: 75, cacheRead: 1.5 }
|
|
1172
|
+
};
|
|
1173
|
+
function calculateCost(model, promptTokens, completionTokens, cacheReadTokens) {
|
|
1174
|
+
const pricing = MODEL_PRICING[model] ?? Object.entries(MODEL_PRICING).find(([key]) => model.startsWith(key))?.[1];
|
|
1175
|
+
if (!pricing) return 0;
|
|
1176
|
+
return promptTokens / 1e6 * pricing.input + completionTokens / 1e6 * pricing.output + cacheReadTokens / 1e6 * pricing.cacheRead;
|
|
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
|
+
}
|
|
1070
1247
|
async function parseTranscript(transcriptPath) {
|
|
1071
1248
|
const result = {
|
|
1072
1249
|
totalCostUsd: 0,
|
|
1073
1250
|
promptTokens: 0,
|
|
1074
1251
|
completionTokens: 0,
|
|
1075
1252
|
cacheReadTokens: 0,
|
|
1076
|
-
cacheCreationTokens: 0
|
|
1253
|
+
cacheCreationTokens: 0,
|
|
1254
|
+
model: "unknown"
|
|
1077
1255
|
};
|
|
1078
1256
|
if (!existsSync3(transcriptPath)) return result;
|
|
1079
1257
|
try {
|
|
@@ -1085,11 +1263,16 @@ async function parseTranscript(transcriptPath) {
|
|
|
1085
1263
|
if (!line.trim()) continue;
|
|
1086
1264
|
try {
|
|
1087
1265
|
const entry = JSON.parse(line);
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
result.
|
|
1091
|
-
result.
|
|
1092
|
-
result.
|
|
1266
|
+
const usage = entry.message?.usage ?? entry.usage;
|
|
1267
|
+
if (usage) {
|
|
1268
|
+
result.promptTokens += usage.input_tokens ?? 0;
|
|
1269
|
+
result.completionTokens += usage.output_tokens ?? 0;
|
|
1270
|
+
result.cacheReadTokens += usage.cache_read_input_tokens ?? 0;
|
|
1271
|
+
result.cacheCreationTokens += usage.cache_creation_input_tokens ?? 0;
|
|
1272
|
+
}
|
|
1273
|
+
const model = entry.message?.model ?? entry.model;
|
|
1274
|
+
if (model && result.model === "unknown") {
|
|
1275
|
+
result.model = model;
|
|
1093
1276
|
}
|
|
1094
1277
|
if (typeof entry.costUSD === "number") {
|
|
1095
1278
|
result.totalCostUsd += entry.costUSD;
|
|
@@ -1099,19 +1282,49 @@ async function parseTranscript(transcriptPath) {
|
|
|
1099
1282
|
}
|
|
1100
1283
|
} catch {
|
|
1101
1284
|
}
|
|
1285
|
+
if (result.totalCostUsd === 0 && result.promptTokens > 0) {
|
|
1286
|
+
result.totalCostUsd = calculateCost(
|
|
1287
|
+
result.model,
|
|
1288
|
+
result.promptTokens,
|
|
1289
|
+
result.completionTokens,
|
|
1290
|
+
result.cacheReadTokens
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1102
1293
|
return result;
|
|
1103
1294
|
}
|
|
1104
1295
|
var sessionStartCommand = new Command13("session-start").description("Hook: called when a Claude Code session starts").action(async () => {
|
|
1105
1296
|
try {
|
|
1106
1297
|
const { apiKey, endpoint, apiEndpoint } = resolveConfig();
|
|
1107
1298
|
const projectSlug = detectProjectSlug();
|
|
1299
|
+
const developer = detectDeveloper();
|
|
1108
1300
|
const hookData = await readStdin();
|
|
1109
|
-
const claudeSessionId = hookData["session_id"];
|
|
1301
|
+
const claudeSessionId = hookData["session_id"] ?? crypto.randomUUID();
|
|
1110
1302
|
const sessionId = crypto.randomUUID();
|
|
1111
1303
|
const model = hookData["model"] || process.env["CLAUDE_MODEL"] || "unknown";
|
|
1112
1304
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1113
|
-
const
|
|
1114
|
-
|
|
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
|
+
}
|
|
1115
1328
|
const sent = await sendEvents(endpoint, apiKey, [
|
|
1116
1329
|
{
|
|
1117
1330
|
event_id: crypto.randomUUID(),
|
|
@@ -1127,7 +1340,11 @@ var sessionStartCommand = new Command13("session-start").description("Hook: call
|
|
|
1127
1340
|
repository: process.env["CLAUDE_REPO"],
|
|
1128
1341
|
branch: process.env["CLAUDE_BRANCH"],
|
|
1129
1342
|
project_slug: projectSlug,
|
|
1130
|
-
cwd
|
|
1343
|
+
cwd,
|
|
1344
|
+
commit_sha: gitCommitBefore,
|
|
1345
|
+
// 1.9: Developer attribution
|
|
1346
|
+
developer_email: developer.email,
|
|
1347
|
+
developer_name: developer.name
|
|
1131
1348
|
}
|
|
1132
1349
|
}
|
|
1133
1350
|
]);
|
|
@@ -1140,36 +1357,276 @@ var sessionStartCommand = new Command13("session-start").description("Hook: call
|
|
|
1140
1357
|
warn(`Failed to start session: ${err instanceof Error ? err.message : String(err)}`);
|
|
1141
1358
|
}
|
|
1142
1359
|
});
|
|
1143
|
-
var toolCallCommand = new Command13("tool-call").description("Hook: called on each Claude Code tool call (
|
|
1360
|
+
var toolCallCommand = new Command13("tool-call").description("Hook: called on each Claude Code tool call (PreToolUse)").action(async () => {
|
|
1144
1361
|
try {
|
|
1145
|
-
const
|
|
1362
|
+
const hookData = await readStdin();
|
|
1363
|
+
const claudeSessionId = hookData["session_id"];
|
|
1364
|
+
const session = readSessionFile(claudeSessionId);
|
|
1146
1365
|
if (!session) return;
|
|
1147
|
-
const
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
{
|
|
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({
|
|
1151
1422
|
event_id: crypto.randomUUID(),
|
|
1152
1423
|
session_id: session.sessionId,
|
|
1153
|
-
event_type:
|
|
1154
|
-
timestamp
|
|
1424
|
+
event_type: eventType,
|
|
1425
|
+
timestamp,
|
|
1155
1426
|
tool: "claude-code",
|
|
1156
1427
|
tool_name: toolName,
|
|
1157
|
-
|
|
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
|
+
});
|
|
1158
1444
|
}
|
|
1159
|
-
|
|
1445
|
+
}
|
|
1446
|
+
await sendEvents(session.endpoint, session.apiKey, events);
|
|
1160
1447
|
} catch {
|
|
1161
1448
|
}
|
|
1162
1449
|
});
|
|
1163
|
-
var
|
|
1450
|
+
var postToolUseCommand = new Command13("post-tool-use").description("Hook: called after a tool call completes (PostToolUse)").action(async () => {
|
|
1164
1451
|
try {
|
|
1165
|
-
const
|
|
1452
|
+
const hookData = await readStdin();
|
|
1453
|
+
const claudeSessionId = hookData["session_id"];
|
|
1454
|
+
const session = readSessionFile(claudeSessionId);
|
|
1166
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 () => {
|
|
1499
|
+
try {
|
|
1500
|
+
const hookData = await readStdin();
|
|
1501
|
+
const claudeSessionId = hookData["session_id"];
|
|
1502
|
+
const session = readSessionFile(claudeSessionId);
|
|
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 {
|
|
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 {
|
|
1167
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
|
+
}
|
|
1168
1615
|
const now = /* @__PURE__ */ new Date();
|
|
1169
1616
|
const startedAt = new Date(session.startedAt);
|
|
1170
1617
|
const durationMs = now.getTime() - startedAt.getTime();
|
|
1171
1618
|
const transcriptPath = hookData["transcript_path"];
|
|
1172
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
|
+
}
|
|
1173
1630
|
const events = [
|
|
1174
1631
|
{
|
|
1175
1632
|
event_id: crypto.randomUUID(),
|
|
@@ -1178,10 +1635,12 @@ var sessionEndCommand = new Command13("session-end").description("Hook: called w
|
|
|
1178
1635
|
timestamp: now.toISOString(),
|
|
1179
1636
|
duration_ms: durationMs,
|
|
1180
1637
|
tool: "claude-code",
|
|
1638
|
+
model: metrics?.model ?? session.model,
|
|
1181
1639
|
metadata: {
|
|
1182
1640
|
started_at: session.startedAt,
|
|
1183
1641
|
ended_at: now.toISOString(),
|
|
1184
1642
|
reason: hookData["reason"] ?? "unknown",
|
|
1643
|
+
git_commit_after: gitCommitAfter,
|
|
1185
1644
|
...metrics ? {
|
|
1186
1645
|
prompt_tokens: metrics.promptTokens,
|
|
1187
1646
|
completion_tokens: metrics.completionTokens,
|
|
@@ -1202,34 +1661,35 @@ var sessionEndCommand = new Command13("session-end").description("Hook: called w
|
|
|
1202
1661
|
tokens_completion: metrics.completionTokens,
|
|
1203
1662
|
cost_usd: metrics.totalCostUsd,
|
|
1204
1663
|
metadata: {
|
|
1664
|
+
model: metrics.model,
|
|
1205
1665
|
cache_read_tokens: metrics.cacheReadTokens,
|
|
1206
1666
|
cache_creation_tokens: metrics.cacheCreationTokens
|
|
1207
1667
|
}
|
|
1208
1668
|
});
|
|
1209
1669
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
success(`Valt session ended: ${session.sessionId.slice(0, 8)} (${Math.round(durationMs / 1e3)}s${costStr})`);
|
|
1218
|
-
} else {
|
|
1219
|
-
warn("Session ended locally but failed to send to Valt.");
|
|
1670
|
+
sendEvents(session.endpoint, session.apiKey, events).catch(() => {
|
|
1671
|
+
});
|
|
1672
|
+
if (sessionFilePath) {
|
|
1673
|
+
try {
|
|
1674
|
+
unlinkSync2(sessionFilePath);
|
|
1675
|
+
} catch {
|
|
1676
|
+
}
|
|
1220
1677
|
}
|
|
1678
|
+
const costStr = metrics?.totalCostUsd ? ` $${metrics.totalCostUsd.toFixed(4)}` : "";
|
|
1679
|
+
const tokenStr = metrics?.promptTokens ? ` ${metrics.promptTokens + metrics.completionTokens} tokens` : "";
|
|
1680
|
+
success(`Valt session ended: ${session.sessionId.slice(0, 8)} (${Math.round(durationMs / 1e3)}s${costStr}${tokenStr})`);
|
|
1221
1681
|
} catch {
|
|
1222
1682
|
}
|
|
1223
1683
|
});
|
|
1224
|
-
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);
|
|
1225
1685
|
|
|
1226
1686
|
// src/commands/setup.ts
|
|
1227
1687
|
import { Command as Command14 } from "commander";
|
|
1228
1688
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
|
|
1229
|
-
import { join } from "path";
|
|
1689
|
+
import { join as join2 } from "path";
|
|
1230
1690
|
import os3 from "os";
|
|
1231
|
-
var CLAUDE_DIR =
|
|
1232
|
-
var HOOKS_FILE =
|
|
1691
|
+
var CLAUDE_DIR = join2(os3.homedir(), ".claude");
|
|
1692
|
+
var HOOKS_FILE = join2(CLAUDE_DIR, "hooks.json");
|
|
1233
1693
|
var HOOK_PREFIX = "npx --yes @usevalt/cli";
|
|
1234
1694
|
function getValtHooks() {
|
|
1235
1695
|
return {
|
|
@@ -1254,13 +1714,66 @@ function getValtHooks() {
|
|
|
1254
1714
|
]
|
|
1255
1715
|
}
|
|
1256
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
|
+
],
|
|
1257
1769
|
SessionEnd: [
|
|
1258
1770
|
{
|
|
1259
1771
|
matcher: "*",
|
|
1260
1772
|
hooks: [
|
|
1261
1773
|
{
|
|
1262
1774
|
type: "command",
|
|
1263
|
-
command: `${HOOK_PREFIX} hook session-end
|
|
1775
|
+
command: `${HOOK_PREFIX} hook session-end`,
|
|
1776
|
+
timeout: 1e4
|
|
1264
1777
|
}
|
|
1265
1778
|
]
|
|
1266
1779
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usevalt/cli",
|
|
3
|
-
"version": "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/
|
|
43
|
-
"@usevalt/
|
|
42
|
+
"@usevalt/eslint-config": "0.0.0",
|
|
43
|
+
"@usevalt/typescript-config": "0.0.0"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsup",
|