@usevalt/cli 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +124 -29
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -996,10 +996,27 @@ proxyCommand.command("stop").description("Stop the MCP proxy").action(() => {
|
|
|
996
996
|
// src/commands/hook.ts
|
|
997
997
|
import { Command as Command13 } from "commander";
|
|
998
998
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, existsSync as existsSync3 } from "fs";
|
|
999
|
+
import { createReadStream } from "fs";
|
|
1000
|
+
import { createInterface } from "readline";
|
|
999
1001
|
import { execSync } from "child_process";
|
|
1000
1002
|
import { basename } from "path";
|
|
1001
1003
|
import os2 from "os";
|
|
1002
1004
|
var SESSION_FILE = `${os2.tmpdir()}/valt-session-${process.getuid?.() ?? "default"}.json`;
|
|
1005
|
+
async function readStdin() {
|
|
1006
|
+
if (process.stdin.isTTY) return {};
|
|
1007
|
+
try {
|
|
1008
|
+
const chunks = [];
|
|
1009
|
+
const data = await new Promise((resolve4) => {
|
|
1010
|
+
process.stdin.setEncoding("utf-8");
|
|
1011
|
+
process.stdin.on("data", (chunk) => chunks.push(String(chunk)));
|
|
1012
|
+
process.stdin.on("end", () => resolve4(chunks.join("")));
|
|
1013
|
+
setTimeout(() => resolve4(chunks.join("")), 500);
|
|
1014
|
+
});
|
|
1015
|
+
return data.trim() ? JSON.parse(data) : {};
|
|
1016
|
+
} catch {
|
|
1017
|
+
return {};
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1003
1020
|
function resolveConfig() {
|
|
1004
1021
|
const apiKey = getApiKey();
|
|
1005
1022
|
if (!apiKey) {
|
|
@@ -1050,13 +1067,78 @@ async function sendEvents(endpoint, apiKey, events) {
|
|
|
1050
1067
|
return false;
|
|
1051
1068
|
}
|
|
1052
1069
|
}
|
|
1070
|
+
var MODEL_PRICING = {
|
|
1071
|
+
"claude-opus-4-6": { input: 15, output: 75, cacheRead: 1.5 },
|
|
1072
|
+
"claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3 },
|
|
1073
|
+
"claude-haiku-4-5": { input: 0.8, output: 4, cacheRead: 0.08 },
|
|
1074
|
+
// Legacy model names
|
|
1075
|
+
"claude-3-5-sonnet": { input: 3, output: 15, cacheRead: 0.3 },
|
|
1076
|
+
"claude-3-5-haiku": { input: 0.8, output: 4, cacheRead: 0.08 },
|
|
1077
|
+
"claude-3-opus": { input: 15, output: 75, cacheRead: 1.5 }
|
|
1078
|
+
};
|
|
1079
|
+
function calculateCost(model, promptTokens, completionTokens, cacheReadTokens) {
|
|
1080
|
+
const pricing = MODEL_PRICING[model] ?? Object.entries(MODEL_PRICING).find(([key]) => model.startsWith(key))?.[1];
|
|
1081
|
+
if (!pricing) return 0;
|
|
1082
|
+
return promptTokens / 1e6 * pricing.input + completionTokens / 1e6 * pricing.output + cacheReadTokens / 1e6 * pricing.cacheRead;
|
|
1083
|
+
}
|
|
1084
|
+
async function parseTranscript(transcriptPath) {
|
|
1085
|
+
const result = {
|
|
1086
|
+
totalCostUsd: 0,
|
|
1087
|
+
promptTokens: 0,
|
|
1088
|
+
completionTokens: 0,
|
|
1089
|
+
cacheReadTokens: 0,
|
|
1090
|
+
cacheCreationTokens: 0,
|
|
1091
|
+
model: "unknown"
|
|
1092
|
+
};
|
|
1093
|
+
if (!existsSync3(transcriptPath)) return result;
|
|
1094
|
+
try {
|
|
1095
|
+
const rl = createInterface({
|
|
1096
|
+
input: createReadStream(transcriptPath, "utf-8"),
|
|
1097
|
+
crlfDelay: Infinity
|
|
1098
|
+
});
|
|
1099
|
+
for await (const line of rl) {
|
|
1100
|
+
if (!line.trim()) continue;
|
|
1101
|
+
try {
|
|
1102
|
+
const entry = JSON.parse(line);
|
|
1103
|
+
const usage = entry.message?.usage ?? entry.usage;
|
|
1104
|
+
if (usage) {
|
|
1105
|
+
result.promptTokens += usage.input_tokens ?? 0;
|
|
1106
|
+
result.completionTokens += usage.output_tokens ?? 0;
|
|
1107
|
+
result.cacheReadTokens += usage.cache_read_input_tokens ?? 0;
|
|
1108
|
+
result.cacheCreationTokens += usage.cache_creation_input_tokens ?? 0;
|
|
1109
|
+
}
|
|
1110
|
+
const model = entry.message?.model ?? entry.model;
|
|
1111
|
+
if (model && result.model === "unknown") {
|
|
1112
|
+
result.model = model;
|
|
1113
|
+
}
|
|
1114
|
+
if (typeof entry.costUSD === "number") {
|
|
1115
|
+
result.totalCostUsd += entry.costUSD;
|
|
1116
|
+
}
|
|
1117
|
+
} catch {
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
} catch {
|
|
1121
|
+
}
|
|
1122
|
+
if (result.totalCostUsd === 0 && result.promptTokens > 0) {
|
|
1123
|
+
result.totalCostUsd = calculateCost(
|
|
1124
|
+
result.model,
|
|
1125
|
+
result.promptTokens,
|
|
1126
|
+
result.completionTokens,
|
|
1127
|
+
result.cacheReadTokens
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
return result;
|
|
1131
|
+
}
|
|
1053
1132
|
var sessionStartCommand = new Command13("session-start").description("Hook: called when a Claude Code session starts").action(async () => {
|
|
1054
1133
|
try {
|
|
1055
1134
|
const { apiKey, endpoint, apiEndpoint } = resolveConfig();
|
|
1056
1135
|
const projectSlug = detectProjectSlug();
|
|
1136
|
+
const hookData = await readStdin();
|
|
1137
|
+
const claudeSessionId = hookData["session_id"];
|
|
1057
1138
|
const sessionId = crypto.randomUUID();
|
|
1139
|
+
const model = hookData["model"] || process.env["CLAUDE_MODEL"] || "unknown";
|
|
1058
1140
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1059
|
-
const state = { sessionId, startedAt, apiKey, endpoint, apiEndpoint, projectSlug };
|
|
1141
|
+
const state = { sessionId, startedAt, apiKey, endpoint, apiEndpoint, projectSlug, model };
|
|
1060
1142
|
writeFileSync3(SESSION_FILE, JSON.stringify(state, null, 2), { mode: 384 });
|
|
1061
1143
|
const sent = await sendEvents(endpoint, apiKey, [
|
|
1062
1144
|
{
|
|
@@ -1065,13 +1147,15 @@ var sessionStartCommand = new Command13("session-start").description("Hook: call
|
|
|
1065
1147
|
event_type: "session.start",
|
|
1066
1148
|
timestamp: startedAt,
|
|
1067
1149
|
tool: "claude-code",
|
|
1150
|
+
model,
|
|
1068
1151
|
metadata: {
|
|
1069
1152
|
tool: "claude-code",
|
|
1070
|
-
model
|
|
1153
|
+
model,
|
|
1154
|
+
claude_session_id: claudeSessionId,
|
|
1071
1155
|
repository: process.env["CLAUDE_REPO"],
|
|
1072
1156
|
branch: process.env["CLAUDE_BRANCH"],
|
|
1073
1157
|
project_slug: projectSlug,
|
|
1074
|
-
cwd: process.cwd()
|
|
1158
|
+
cwd: hookData["cwd"] || process.cwd()
|
|
1075
1159
|
}
|
|
1076
1160
|
}
|
|
1077
1161
|
]);
|
|
@@ -1088,24 +1172,7 @@ var toolCallCommand = new Command13("tool-call").description("Hook: called on ea
|
|
|
1088
1172
|
try {
|
|
1089
1173
|
const session = readSessionFile();
|
|
1090
1174
|
if (!session) return;
|
|
1091
|
-
|
|
1092
|
-
if (!process.stdin.isTTY) {
|
|
1093
|
-
stdinData = await new Promise((resolve4) => {
|
|
1094
|
-
const chunks = [];
|
|
1095
|
-
process.stdin.setEncoding("utf-8");
|
|
1096
|
-
process.stdin.on("data", (chunk) => chunks.push(String(chunk)));
|
|
1097
|
-
process.stdin.on("end", () => resolve4(chunks.join("")));
|
|
1098
|
-
setTimeout(() => resolve4(chunks.join("")), 1e3);
|
|
1099
|
-
});
|
|
1100
|
-
}
|
|
1101
|
-
let toolData = {};
|
|
1102
|
-
if (stdinData.trim()) {
|
|
1103
|
-
try {
|
|
1104
|
-
toolData = JSON.parse(stdinData);
|
|
1105
|
-
} catch {
|
|
1106
|
-
toolData = { raw_input: stdinData.trim() };
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1175
|
+
const toolData = await readStdin();
|
|
1109
1176
|
const toolName = toolData["tool_name"] ?? toolData["name"] ?? "unknown";
|
|
1110
1177
|
await sendEvents(session.endpoint, session.apiKey, [
|
|
1111
1178
|
{
|
|
@@ -1125,10 +1192,13 @@ var sessionEndCommand = new Command13("session-end").description("Hook: called w
|
|
|
1125
1192
|
try {
|
|
1126
1193
|
const session = readSessionFile();
|
|
1127
1194
|
if (!session) return;
|
|
1195
|
+
const hookData = await readStdin();
|
|
1128
1196
|
const now = /* @__PURE__ */ new Date();
|
|
1129
1197
|
const startedAt = new Date(session.startedAt);
|
|
1130
1198
|
const durationMs = now.getTime() - startedAt.getTime();
|
|
1131
|
-
const
|
|
1199
|
+
const transcriptPath = hookData["transcript_path"];
|
|
1200
|
+
const metrics = transcriptPath ? await parseTranscript(transcriptPath) : null;
|
|
1201
|
+
const events = [
|
|
1132
1202
|
{
|
|
1133
1203
|
event_id: crypto.randomUUID(),
|
|
1134
1204
|
session_id: session.sessionId,
|
|
@@ -1136,21 +1206,46 @@ var sessionEndCommand = new Command13("session-end").description("Hook: called w
|
|
|
1136
1206
|
timestamp: now.toISOString(),
|
|
1137
1207
|
duration_ms: durationMs,
|
|
1138
1208
|
tool: "claude-code",
|
|
1209
|
+
model: metrics?.model ?? session.model,
|
|
1139
1210
|
metadata: {
|
|
1140
1211
|
started_at: session.startedAt,
|
|
1141
|
-
ended_at: now.toISOString()
|
|
1212
|
+
ended_at: now.toISOString(),
|
|
1213
|
+
reason: hookData["reason"] ?? "unknown",
|
|
1214
|
+
...metrics ? {
|
|
1215
|
+
prompt_tokens: metrics.promptTokens,
|
|
1216
|
+
completion_tokens: metrics.completionTokens,
|
|
1217
|
+
cache_read_tokens: metrics.cacheReadTokens,
|
|
1218
|
+
cache_creation_tokens: metrics.cacheCreationTokens
|
|
1219
|
+
} : {}
|
|
1142
1220
|
}
|
|
1143
1221
|
}
|
|
1144
|
-
]
|
|
1222
|
+
];
|
|
1223
|
+
if (metrics && (metrics.totalCostUsd > 0 || metrics.promptTokens > 0)) {
|
|
1224
|
+
events.push({
|
|
1225
|
+
event_id: crypto.randomUUID(),
|
|
1226
|
+
session_id: session.sessionId,
|
|
1227
|
+
event_type: "cost",
|
|
1228
|
+
timestamp: now.toISOString(),
|
|
1229
|
+
tool: "claude-code",
|
|
1230
|
+
tokens_prompt: metrics.promptTokens,
|
|
1231
|
+
tokens_completion: metrics.completionTokens,
|
|
1232
|
+
cost_usd: metrics.totalCostUsd,
|
|
1233
|
+
metadata: {
|
|
1234
|
+
model: metrics.model,
|
|
1235
|
+
cache_read_tokens: metrics.cacheReadTokens,
|
|
1236
|
+
cache_creation_tokens: metrics.cacheCreationTokens
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
sendEvents(session.endpoint, session.apiKey, events).catch(() => {
|
|
1241
|
+
});
|
|
1145
1242
|
try {
|
|
1146
1243
|
unlinkSync2(SESSION_FILE);
|
|
1147
1244
|
} catch {
|
|
1148
1245
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
}
|
|
1152
|
-
warn("Session ended locally but failed to send to Valt.");
|
|
1153
|
-
}
|
|
1246
|
+
const costStr = metrics?.totalCostUsd ? ` $${metrics.totalCostUsd.toFixed(4)}` : "";
|
|
1247
|
+
const tokenStr = metrics?.promptTokens ? ` ${metrics.promptTokens + metrics.completionTokens} tokens` : "";
|
|
1248
|
+
success(`Valt session ended: ${session.sessionId.slice(0, 8)} (${Math.round(durationMs / 1e3)}s${costStr}${tokenStr})`);
|
|
1154
1249
|
} catch {
|
|
1155
1250
|
}
|
|
1156
1251
|
});
|