@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.
Files changed (2) hide show
  1. package/dist/index.js +124 -29
  2. 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: process.env["CLAUDE_MODEL"] ?? "unknown",
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
- let stdinData = "";
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 sent = await sendEvents(session.endpoint, session.apiKey, [
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
- if (sent) {
1150
- success(`Valt session ended: ${session.sessionId.slice(0, 8)} (${Math.round(durationMs / 1e3)}s)`);
1151
- } else {
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usevalt/cli",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "description": "Valt CLI — trust layer for AI-assisted development",
5
5
  "license": "MIT",
6
6
  "repository": {