@usevalt/cli 0.1.0 → 0.2.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 (3) hide show
  1. package/dist/index.js +602 -21
  2. package/package.json +20 -18
  3. package/LICENSE +0 -21
package/dist/index.js CHANGED
@@ -1,7 +1,13 @@
1
1
  #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
2
8
 
3
9
  // src/index.ts
4
- import { Command as Command13 } from "commander";
10
+ import { Command as Command15 } from "commander";
5
11
 
6
12
  // src/commands/init.ts
7
13
  import { Command } from "commander";
@@ -9,6 +15,13 @@ import { Command } from "commander";
9
15
  // src/lib/config.ts
10
16
  import { readFileSync, writeFileSync, existsSync } from "fs";
11
17
  import { resolve } from "path";
18
+ import { z } from "zod";
19
+ var configSchema = z.object({
20
+ apiKey: z.string().optional(),
21
+ endpoint: z.string().url().optional(),
22
+ projectId: z.string().optional(),
23
+ projectSlug: z.string().optional()
24
+ }).passthrough();
12
25
  var CONFIG_FILE = ".valtrc.json";
13
26
  function findConfigPath(startDir = process.cwd()) {
14
27
  let dir = startDir;
@@ -24,7 +37,13 @@ function readConfig(dir = process.cwd()) {
24
37
  const path = findConfigPath(dir);
25
38
  if (!path) return {};
26
39
  try {
27
- return JSON.parse(readFileSync(path, "utf-8"));
40
+ const raw = JSON.parse(readFileSync(path, "utf-8"));
41
+ const parsed = configSchema.safeParse(raw);
42
+ if (!parsed.success) {
43
+ console.warn(`[valt] Invalid config in ${path}: ${parsed.error.message}`);
44
+ return {};
45
+ }
46
+ return parsed.data;
28
47
  } catch {
29
48
  return {};
30
49
  }
@@ -91,8 +110,115 @@ function table(rows) {
91
110
 
92
111
  // src/commands/init.ts
93
112
  import { API_KEY_PREFIX } from "@usevalt/shared";
113
+
114
+ // src/schemas.ts
115
+ import { z as z2 } from "zod";
116
+ var ApiErrorResponseSchema = z2.object({
117
+ error: z2.string().optional(),
118
+ message: z2.string().optional()
119
+ });
120
+ var CertificateSchema = z2.object({
121
+ certificate_number: z2.string(),
122
+ status: z2.string(),
123
+ trust_score: z2.number(),
124
+ session_id: z2.string(),
125
+ git_commit: z2.string().nullable(),
126
+ issued_at: z2.string()
127
+ });
128
+ var CertificateGenerateResponseSchema = z2.object({
129
+ data: z2.object({
130
+ certificate_number: z2.string(),
131
+ trust_score: z2.number()
132
+ })
133
+ });
134
+ var CertificateListResponseSchema = z2.object({
135
+ data: z2.array(CertificateSchema)
136
+ });
137
+ var VerifyResponseSchema = z2.object({
138
+ verified: z2.boolean(),
139
+ certificate_number: z2.string().optional(),
140
+ trust_score: z2.number().optional(),
141
+ status: z2.string().optional(),
142
+ signature_valid: z2.boolean().optional(),
143
+ issued_at: z2.string().optional(),
144
+ reason: z2.string().optional()
145
+ });
146
+ var SessionDetailSchema = z2.object({
147
+ id: z2.string(),
148
+ tool: z2.string().nullable(),
149
+ model: z2.string().nullable(),
150
+ status: z2.string().nullable(),
151
+ trust_score: z2.number().nullable(),
152
+ total_cost_usd: z2.number().nullable(),
153
+ duration_ms: z2.number().nullable(),
154
+ git_branch: z2.string().nullable(),
155
+ started_at: z2.string().nullable(),
156
+ tags: z2.array(z2.string()).nullable()
157
+ });
158
+ var SessionDetailResponseSchema = z2.object({
159
+ data: SessionDetailSchema
160
+ });
161
+ var SessionSummarySchema = z2.object({
162
+ id: z2.string(),
163
+ tool: z2.string(),
164
+ status: z2.string(),
165
+ started_at: z2.string(),
166
+ duration_ms: z2.number().nullable(),
167
+ total_cost_usd: z2.number(),
168
+ trust_score: z2.number().nullable(),
169
+ prompt_summary: z2.string().nullable()
170
+ });
171
+ var SessionListResponseSchema = z2.object({
172
+ sessions: z2.array(SessionSummarySchema)
173
+ });
174
+ var SessionWithBreakdownSchema = z2.object({
175
+ trust_score: z2.number().nullable(),
176
+ trust_score_breakdown: z2.record(z2.string(), z2.number()).nullable()
177
+ });
178
+ var SessionWithBreakdownResponseSchema = z2.object({
179
+ data: SessionWithBreakdownSchema
180
+ });
181
+ var SessionSearchResponseSchema = z2.object({
182
+ sessions: z2.array(z2.object({ id: z2.string() }))
183
+ });
184
+ var CostByToolSchema = z2.object({
185
+ tool: z2.string(),
186
+ totalCost: z2.number(),
187
+ sessionCount: z2.number()
188
+ });
189
+ var CostsResponseSchema = z2.object({
190
+ data: z2.object({
191
+ total_cost_usd: z2.number(),
192
+ cost_by_tool: z2.array(CostByToolSchema),
193
+ daily_costs: z2.array(z2.object({
194
+ date: z2.string(),
195
+ cost: z2.number()
196
+ }))
197
+ })
198
+ });
199
+ var ConfigKeySchema = z2.enum(["apiKey", "endpoint", "projectId", "projectSlug"]);
200
+ var InitArgsSchema = z2.object({
201
+ apiKey: z2.string().optional(),
202
+ endpoint: z2.string().url("Endpoint must be a valid URL").optional()
203
+ });
204
+ function validateResponse(schema, data, label) {
205
+ const result = schema.safeParse(data);
206
+ if (!result.success) {
207
+ const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
208
+ throw new Error(`Invalid ${label} response: ${issues}`);
209
+ }
210
+ return result.data;
211
+ }
212
+
213
+ // src/commands/init.ts
94
214
  var initCommand = new Command("init").description("Initialize Valt in the current project").option("-k, --api-key <key>", "API key").option("-e, --endpoint <url>", "API endpoint").action(async (opts) => {
95
215
  try {
216
+ const argsResult = InitArgsSchema.safeParse(opts);
217
+ if (!argsResult.success) {
218
+ const issues = argsResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
219
+ error(`Invalid arguments: ${issues}`);
220
+ process.exit(1);
221
+ }
96
222
  const existing = readConfig();
97
223
  const config = { ...existing };
98
224
  if (opts.apiKey) {
@@ -121,19 +247,75 @@ var initCommand = new Command("init").description("Initialize Valt in the curren
121
247
 
122
248
  // src/commands/login.ts
123
249
  import { Command as Command2 } from "commander";
250
+ import os from "os";
251
+ var POLL_INTERVAL_MS = 2e3;
252
+ var TIMEOUT_MS = 5 * 60 * 1e3;
253
+ function getEndpoint2() {
254
+ return process.env.VALT_ENDPOINT ?? "https://usevalt.com";
255
+ }
256
+ function sleep(ms) {
257
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
258
+ }
259
+ async function openBrowser(url) {
260
+ try {
261
+ const open = (await import("open")).default;
262
+ await open(url);
263
+ } catch {
264
+ warn(`Could not open browser automatically. Please visit the URL above.`);
265
+ }
266
+ }
124
267
  var loginCommand = new Command2("login").description("Authenticate with Valt (opens browser)").action(async () => {
268
+ const endpoint = getEndpoint2();
269
+ info("Starting device authorization flow...");
270
+ let deviceData;
125
271
  try {
126
- info("Opening browser for authentication...");
127
- info("Login URL: https://usevalt.com/login");
128
- info("");
129
- info("To authenticate:");
130
- info(" 1. Go to https://usevalt.com/settings/api-keys");
131
- info(" 2. Create an API key");
132
- info(" 3. Run: valt init --api-key <your-key>");
272
+ const res = await fetch(`${endpoint}/api/auth/device`, {
273
+ method: "POST",
274
+ headers: { "Content-Type": "application/json" }
275
+ });
276
+ if (!res.ok) {
277
+ throw new Error(`Server returned ${res.status}`);
278
+ }
279
+ deviceData = await res.json();
133
280
  } catch (err) {
134
- error(`Login failed: ${err instanceof Error ? err.message : String(err)}`);
281
+ error(`Failed to initiate login: ${err instanceof Error ? err.message : String(err)}`);
135
282
  process.exit(1);
136
283
  }
284
+ const verificationUrl = `${endpoint}/auth/device?code=${deviceData.user_code}`;
285
+ info("");
286
+ info(`Your verification code: ${bold(deviceData.user_code)}`);
287
+ info("");
288
+ info(`Opening browser to: ${dim(verificationUrl)}`);
289
+ info("If the browser does not open, visit the URL above manually.");
290
+ info("");
291
+ await openBrowser(verificationUrl);
292
+ info("Waiting for approval...");
293
+ const startTime = Date.now();
294
+ while (Date.now() - startTime < TIMEOUT_MS) {
295
+ await sleep(POLL_INTERVAL_MS);
296
+ try {
297
+ const res = await fetch(
298
+ `${endpoint}/api/auth/device?code=${deviceData.device_code}`
299
+ );
300
+ if (!res.ok) continue;
301
+ const poll = await res.json();
302
+ if (poll.status === "approved" && poll.api_key) {
303
+ const keyPrefix = poll.api_key.slice(0, 8);
304
+ writeConfig({ apiKey: poll.api_key, endpoint }, os.homedir());
305
+ info("");
306
+ success(`Authenticated successfully. API key: ${dim(keyPrefix + "...")}`);
307
+ success(`Credentials saved to ~/.valtrc.json`);
308
+ return;
309
+ }
310
+ if (poll.status === "expired") {
311
+ error("Device code expired. Please try again.");
312
+ process.exit(1);
313
+ }
314
+ } catch {
315
+ }
316
+ }
317
+ error("Login timed out after 5 minutes. Please try again.");
318
+ process.exit(1);
137
319
  });
138
320
 
139
321
  // src/commands/status.ts
@@ -175,9 +357,10 @@ var statusCommand = new Command3("status").description("Show recent session acti
175
357
  const spinner = ora("Fetching sessions...").start();
176
358
  try {
177
359
  const limit = parseInt(opts.limit, 10) || 10;
178
- const data = await apiRequest(
360
+ const raw = await apiRequest(
179
361
  `/v1/sessions?limit=${limit}`
180
362
  );
363
+ const data = validateResponse(SessionListResponseSchema, raw, "session list");
181
364
  spinner.stop();
182
365
  if (data.sessions.length === 0) {
183
366
  info("No sessions found. Start tracking with the SDK.");
@@ -217,9 +400,10 @@ import ora2 from "ora";
217
400
  var verifyCommand = new Command4("verify").description("Check Valt Verified status for a commit").argument("<commit>", "Git commit SHA to verify").action(async (commit) => {
218
401
  const spinner = ora2("Verifying commit...").start();
219
402
  try {
220
- const data = await apiRequest(
403
+ const raw = await apiRequest(
221
404
  `/v1/certificates/verify?commit=${encodeURIComponent(commit)}`
222
405
  );
406
+ const data = validateResponse(VerifyResponseSchema, raw, "certificate verify");
223
407
  spinner.stop();
224
408
  if (data.verified) {
225
409
  success(`Valt Verified ${bold(data.certificate_number ?? "")}`);
@@ -288,6 +472,25 @@ var doctorCommand = new Command5("doctor").description("Check Valt configuration
288
472
  issues++;
289
473
  }
290
474
  }
475
+ const evalUrl = process.env["VALT_EVAL_URL"];
476
+ if (evalUrl) {
477
+ try {
478
+ const evalRes = await fetch(`${evalUrl}/eval/health`, {
479
+ signal: AbortSignal.timeout(5e3)
480
+ });
481
+ if (evalRes.ok) {
482
+ success(`Eval service: ${dim(evalUrl)} (Semgrep)`);
483
+ } else {
484
+ warn(`Eval service returned status ${evalRes.status}`);
485
+ issues++;
486
+ }
487
+ } catch {
488
+ warn("Eval service unreachable (security scores will use heuristic fallback)");
489
+ issues++;
490
+ }
491
+ } else {
492
+ info("Eval service: not configured (set VALT_EVAL_URL for Semgrep scans)");
493
+ }
291
494
  console.log("");
292
495
  if (issues === 0) {
293
496
  success("All checks passed!");
@@ -318,6 +521,10 @@ import { Command as Command7 } from "commander";
318
521
  var configCommand = new Command7("config").description("Manage Valt configuration");
319
522
  configCommand.command("set <key> <value>").description("Set a config value").action((key, value) => {
320
523
  try {
524
+ const keyResult = ConfigKeySchema.safeParse(key);
525
+ if (!keyResult.success) {
526
+ warn(`Unknown config key "${key}". Valid keys: ${ConfigKeySchema.options.join(", ")}`);
527
+ }
321
528
  const config = readConfig();
322
529
  config[key] = value;
323
530
  writeConfig(config);
@@ -359,12 +566,14 @@ configCommand.command("list").description("List all config values").action(() =>
359
566
  // src/commands/sessions.ts
360
567
  import { Command as Command8 } from "commander";
361
568
  import ora3 from "ora";
362
- var sessionsCommand = new Command8("sessions").description("View session details").argument("<id>", "Session ID").action(async (id) => {
569
+ var sessionsCommand = new Command8("sessions").description("View and list sessions");
570
+ sessionsCommand.command("show <id>").description("Show session details").action(async (id) => {
363
571
  const spinner = ora3("Fetching session...").start();
364
572
  try {
365
- const session = await apiRequest(
573
+ const raw = await apiRequest(
366
574
  `/v1/sessions/${encodeURIComponent(id)}`
367
575
  );
576
+ const session = validateResponse(SessionDetailResponseSchema, raw, "session detail");
368
577
  spinner.stop();
369
578
  const s = session.data;
370
579
  console.log(bold(`Session: ${id}`));
@@ -393,6 +602,55 @@ var sessionsCommand = new Command8("sessions").description("View session details
393
602
  process.exit(1);
394
603
  }
395
604
  });
605
+ sessionsCommand.command("list").description("List recent sessions").option("-n, --limit <count>", "Number of sessions to show", "10").option("-s, --status <status>", "Filter by status").action(async (opts) => {
606
+ const spinner = ora3("Fetching sessions...").start();
607
+ try {
608
+ const limit = parseInt(opts.limit, 10) || 10;
609
+ let url = `/v1/sessions?limit=${limit}`;
610
+ if (opts.status) {
611
+ url += `&status=${encodeURIComponent(opts.status)}`;
612
+ }
613
+ const raw = await apiRequest(url);
614
+ const data = validateResponse(SessionListResponseSchema, raw, "session list");
615
+ spinner.stop();
616
+ if (data.sessions.length === 0) {
617
+ info("No sessions found.");
618
+ return;
619
+ }
620
+ console.log(bold(`Sessions (${data.sessions.length})`));
621
+ console.log("");
622
+ const rows = [
623
+ ["ID", "TOOL", "STATUS", "SCORE", "COST", "DURATION", "STARTED"]
624
+ ];
625
+ for (const s of data.sessions) {
626
+ rows.push([
627
+ s.id.slice(0, 8),
628
+ s.tool,
629
+ s.status,
630
+ s.trust_score !== null ? String(s.trust_score) : "-",
631
+ formatCost(s.total_cost_usd),
632
+ s.duration_ms ? formatDuration(s.duration_ms) : "-",
633
+ formatDate(s.started_at)
634
+ ]);
635
+ }
636
+ table(rows);
637
+ } catch (err) {
638
+ spinner.stop();
639
+ if (err instanceof ApiError) {
640
+ error(err.message);
641
+ } else {
642
+ error(`Failed to list sessions: ${err instanceof Error ? err.message : String(err)}`);
643
+ }
644
+ process.exit(1);
645
+ }
646
+ });
647
+ sessionsCommand.argument("[id]", "Session ID to show").action(async (id) => {
648
+ if (id) {
649
+ await sessionsCommand.commands.find((c) => c.name() === "show").parseAsync(["node", "show", id]);
650
+ } else {
651
+ await sessionsCommand.commands.find((c) => c.name() === "list").parseAsync(["node", "list"]);
652
+ }
653
+ });
396
654
 
397
655
  // src/commands/cert.ts
398
656
  import { Command as Command9 } from "commander";
@@ -401,13 +659,14 @@ var certCommand = new Command9("cert").description("Manage certificates");
401
659
  certCommand.command("generate").description("Generate a certificate for a session").requiredOption("--session <id>", "Session ID").option("--force", "Force regeneration if certificate exists").action(async (opts) => {
402
660
  const spinner = ora4("Generating certificate...").start();
403
661
  try {
404
- const result = await apiRequest("/v1/certificates", {
662
+ const raw = await apiRequest("/v1/certificates", {
405
663
  method: "POST",
406
664
  body: JSON.stringify({
407
665
  session_id: opts.session,
408
666
  force: opts.force ?? false
409
667
  })
410
668
  });
669
+ const result = validateResponse(CertificateGenerateResponseSchema, raw, "certificate generate");
411
670
  spinner.stop();
412
671
  success(`Certificate generated: ${result.data.certificate_number} (score: ${result.data.trust_score})`);
413
672
  } catch (err) {
@@ -423,9 +682,10 @@ certCommand.command("generate").description("Generate a certificate for a sessio
423
682
  certCommand.command("show <number>").description("Show certificate details").action(async (certNumber) => {
424
683
  const spinner = ora4("Fetching certificate...").start();
425
684
  try {
426
- const result = await apiRequest(
685
+ const raw = await apiRequest(
427
686
  `/v1/certificates?certificate_number=${encodeURIComponent(certNumber)}`
428
687
  );
688
+ const result = validateResponse(CertificateListResponseSchema, raw, "certificate list");
429
689
  spinner.stop();
430
690
  const certs = result.data;
431
691
  if (!certs || certs.length === 0) {
@@ -452,6 +712,77 @@ certCommand.command("show <number>").description("Show certificate details").act
452
712
  process.exit(1);
453
713
  }
454
714
  });
715
+ certCommand.command("list").description("List certificates").option("-n, --limit <count>", "Number of certificates to show", "10").option("-s, --status <status>", "Filter by status (active, revoked)").action(async (opts) => {
716
+ const spinner = ora4("Fetching certificates...").start();
717
+ try {
718
+ const limit = parseInt(opts.limit, 10) || 10;
719
+ let url = `/v1/certificates?limit=${limit}`;
720
+ if (opts.status) {
721
+ url += `&status=${encodeURIComponent(opts.status)}`;
722
+ }
723
+ const raw = await apiRequest(url);
724
+ const result = validateResponse(CertificateListResponseSchema, raw, "certificate list");
725
+ spinner.stop();
726
+ const certs = result.data;
727
+ if (!certs || certs.length === 0) {
728
+ info("No certificates found.");
729
+ return;
730
+ }
731
+ console.log(bold(`Certificates (${certs.length})`));
732
+ console.log("");
733
+ const rows = [
734
+ ["NUMBER", "STATUS", "SCORE", "COMMIT", "ISSUED"]
735
+ ];
736
+ for (const cert of certs) {
737
+ rows.push([
738
+ cert.certificate_number,
739
+ cert.status,
740
+ String(cert.trust_score),
741
+ cert.git_commit?.slice(0, 8) ?? "-",
742
+ formatDate(cert.issued_at)
743
+ ]);
744
+ }
745
+ table(rows);
746
+ } catch (err) {
747
+ spinner.stop();
748
+ if (err instanceof ApiError) {
749
+ error(err.message);
750
+ } else {
751
+ error(`Failed to list certificates: ${err instanceof Error ? err.message : String(err)}`);
752
+ }
753
+ process.exit(1);
754
+ }
755
+ });
756
+ certCommand.command("verify <commit>").description("Verify a certificate for a commit").action(async (commit) => {
757
+ const spinner = ora4("Verifying...").start();
758
+ try {
759
+ const raw = await apiRequest(
760
+ `/v1/certificates/verify?commit=${encodeURIComponent(commit)}`
761
+ );
762
+ const data = validateResponse(VerifyResponseSchema, raw, "certificate verify");
763
+ spinner.stop();
764
+ if (data.verified) {
765
+ success(`Valt Verified ${bold(data.certificate_number ?? "")}`);
766
+ info(` Trust Score: ${data.trust_score ?? "-"}`);
767
+ info(` Status: ${data.status ?? "-"}`);
768
+ info(` Signature Valid: ${data.signature_valid ? "Yes" : "No"}`);
769
+ if (data.issued_at) info(` Issued: ${data.issued_at}`);
770
+ } else {
771
+ warn("Not Valt Verified");
772
+ if (data.reason) info(` Reason: ${data.reason}`);
773
+ }
774
+ } catch (err) {
775
+ spinner.stop();
776
+ if (err instanceof ApiError && err.status === 404) {
777
+ warn("No certificate found for this commit.");
778
+ } else if (err instanceof ApiError) {
779
+ error(err.message);
780
+ } else {
781
+ error(`Failed to verify: ${err instanceof Error ? err.message : String(err)}`);
782
+ }
783
+ process.exit(1);
784
+ }
785
+ });
455
786
 
456
787
  // src/commands/costs.ts
457
788
  import { Command as Command10 } from "commander";
@@ -460,9 +791,10 @@ var costsCommand = new Command10("costs").description("View cost breakdown").opt
460
791
  const spinner = ora5("Fetching costs...").start();
461
792
  try {
462
793
  const days = opts.period.replace("d", "");
463
- const result = await apiRequest(
794
+ const raw = await apiRequest(
464
795
  `/v1/analytics/costs?days=${encodeURIComponent(days)}`
465
796
  );
797
+ const result = validateResponse(CostsResponseSchema, raw, "costs");
466
798
  spinner.stop();
467
799
  const data = result.data;
468
800
  console.log(bold(`Total Cost: ${formatCost(data.total_cost_usd)}`));
@@ -494,9 +826,10 @@ var analyzeCommand = new Command11("analyze").description("Analyze a session or
494
826
  try {
495
827
  let sessionId = opts.session;
496
828
  if (opts.commit && !sessionId) {
497
- const sessions = await apiRequest(
829
+ const rawSessions = await apiRequest(
498
830
  `/v1/sessions?git_commit=${encodeURIComponent(opts.commit)}`
499
831
  );
832
+ const sessions = validateResponse(SessionSearchResponseSchema, rawSessions, "session search");
500
833
  if (!sessions.sessions || sessions.sessions.length === 0) {
501
834
  spinner.stop();
502
835
  error(`No session found for commit: ${opts.commit}`);
@@ -509,9 +842,10 @@ var analyzeCommand = new Command11("analyze").description("Analyze a session or
509
842
  error("Provide --session <id> or --commit <sha>");
510
843
  return;
511
844
  }
512
- const result = await apiRequest(
845
+ const raw = await apiRequest(
513
846
  `/v1/sessions/${encodeURIComponent(sessionId)}`
514
847
  );
848
+ const result = validateResponse(SessionWithBreakdownResponseSchema, raw, "session breakdown");
515
849
  spinner.stop();
516
850
  const s = result.data;
517
851
  console.log(bold("Trust Score Analysis"));
@@ -659,9 +993,254 @@ proxyCommand.command("stop").description("Stop the MCP proxy").action(() => {
659
993
  }
660
994
  });
661
995
 
996
+ // src/commands/hook.ts
997
+ import { Command as Command13 } from "commander";
998
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, existsSync as existsSync3 } from "fs";
999
+ import os2 from "os";
1000
+ var SESSION_FILE = `${os2.tmpdir()}/valt-session-${process.getuid?.() ?? "default"}.json`;
1001
+ function getEnvConfig() {
1002
+ const apiKey = process.env["VALT_API_KEY"];
1003
+ if (!apiKey) {
1004
+ error("VALT_API_KEY environment variable is required");
1005
+ process.exit(1);
1006
+ }
1007
+ const endpoint = process.env["VALT_ENDPOINT"] ?? "https://ingest.usevalt.com";
1008
+ const apiEndpoint = process.env["VALT_API_ENDPOINT"] ?? "https://usevalt.com";
1009
+ const projectId = process.env["VALT_PROJECT_ID"];
1010
+ return { apiKey, endpoint, apiEndpoint, projectId };
1011
+ }
1012
+ function readSessionFile() {
1013
+ try {
1014
+ if (!existsSync3(SESSION_FILE)) return null;
1015
+ const data = readFileSync3(SESSION_FILE, "utf-8");
1016
+ return JSON.parse(data);
1017
+ } catch {
1018
+ return null;
1019
+ }
1020
+ }
1021
+ async function sendEvents(endpoint, apiKey, events) {
1022
+ try {
1023
+ const res = await fetch(`${endpoint}/v1/events`, {
1024
+ method: "POST",
1025
+ headers: {
1026
+ "Content-Type": "application/json",
1027
+ Authorization: `Bearer ${apiKey}`
1028
+ },
1029
+ body: JSON.stringify({ events }),
1030
+ signal: AbortSignal.timeout(1e4)
1031
+ });
1032
+ return res.ok;
1033
+ } catch {
1034
+ return false;
1035
+ }
1036
+ }
1037
+ var sessionStartCommand = new Command13("session-start").description("Hook: called when a Claude Code session starts").action(async () => {
1038
+ try {
1039
+ const { apiKey, endpoint, apiEndpoint, projectId } = getEnvConfig();
1040
+ const sessionId = crypto.randomUUID();
1041
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1042
+ const state = { sessionId, startedAt, apiKey, endpoint, apiEndpoint, projectId };
1043
+ writeFileSync3(SESSION_FILE, JSON.stringify(state, null, 2), { mode: 384 });
1044
+ const sent = await sendEvents(endpoint, apiKey, [
1045
+ {
1046
+ event_id: crypto.randomUUID(),
1047
+ session_id: sessionId,
1048
+ event_type: "session.start",
1049
+ timestamp: startedAt,
1050
+ tool: "claude-code",
1051
+ metadata: {
1052
+ tool: "claude-code",
1053
+ model: process.env["CLAUDE_MODEL"] ?? "unknown",
1054
+ repository: process.env["CLAUDE_REPO"],
1055
+ branch: process.env["CLAUDE_BRANCH"],
1056
+ project_id: projectId
1057
+ }
1058
+ }
1059
+ ]);
1060
+ if (sent) {
1061
+ success(`Valt session started: ${sessionId}`);
1062
+ } else {
1063
+ warn("Session started locally but failed to send to Valt. Events will be captured.");
1064
+ }
1065
+ if (projectId) {
1066
+ try {
1067
+ const convParams = new URLSearchParams({
1068
+ project_id: projectId,
1069
+ type: "convention",
1070
+ limit: "20"
1071
+ });
1072
+ const convRes = await fetch(`${apiEndpoint}/api/v1/memories?${convParams.toString()}`, {
1073
+ headers: {
1074
+ "Content-Type": "application/json",
1075
+ Authorization: `Bearer ${apiKey}`
1076
+ },
1077
+ signal: AbortSignal.timeout(5e3)
1078
+ });
1079
+ if (convRes.ok) {
1080
+ const convBody = await convRes.json();
1081
+ const conventions = convBody.data ?? [];
1082
+ if (conventions.length > 0) {
1083
+ console.log("\n## Project Conventions");
1084
+ for (const c of conventions) {
1085
+ console.log(`- **${c.title}**: ${c.content}`);
1086
+ }
1087
+ console.log("");
1088
+ }
1089
+ }
1090
+ } catch {
1091
+ }
1092
+ }
1093
+ } catch (err) {
1094
+ error(`Failed to start session: ${err instanceof Error ? err.message : String(err)}`);
1095
+ }
1096
+ });
1097
+ var toolCallCommand = new Command13("tool-call").description("Hook: called on each Claude Code tool call (reads JSON from stdin)").action(async () => {
1098
+ try {
1099
+ const session = readSessionFile();
1100
+ if (!session) {
1101
+ warn("No active Valt session. Run `valt hook session-start` first.");
1102
+ return;
1103
+ }
1104
+ let stdinData = "";
1105
+ if (!process.stdin.isTTY) {
1106
+ stdinData = await new Promise((resolve4) => {
1107
+ const chunks = [];
1108
+ process.stdin.setEncoding("utf-8");
1109
+ process.stdin.on("data", (chunk) => chunks.push(String(chunk)));
1110
+ process.stdin.on("end", () => resolve4(chunks.join("")));
1111
+ setTimeout(() => resolve4(chunks.join("")), 1e3);
1112
+ });
1113
+ }
1114
+ let toolData = {};
1115
+ if (stdinData.trim()) {
1116
+ try {
1117
+ toolData = JSON.parse(stdinData);
1118
+ } catch {
1119
+ toolData = { raw_input: stdinData.trim() };
1120
+ }
1121
+ }
1122
+ const toolName = toolData["tool_name"] ?? toolData["name"] ?? "unknown";
1123
+ await sendEvents(session.endpoint, session.apiKey, [
1124
+ {
1125
+ event_id: crypto.randomUUID(),
1126
+ session_id: session.sessionId,
1127
+ event_type: "tool.call",
1128
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1129
+ tool: "claude-code",
1130
+ tool_name: toolName,
1131
+ metadata: toolData
1132
+ }
1133
+ ]);
1134
+ } catch (err) {
1135
+ warn(`Tool call tracking failed: ${err instanceof Error ? err.message : String(err)}`);
1136
+ }
1137
+ });
1138
+ var sessionEndCommand = new Command13("session-end").description("Hook: called when a Claude Code session ends").action(async () => {
1139
+ try {
1140
+ const session = readSessionFile();
1141
+ if (!session) {
1142
+ warn("No active Valt session to end.");
1143
+ return;
1144
+ }
1145
+ const now = /* @__PURE__ */ new Date();
1146
+ const startedAt = new Date(session.startedAt);
1147
+ const durationMs = now.getTime() - startedAt.getTime();
1148
+ const sent = await sendEvents(session.endpoint, session.apiKey, [
1149
+ {
1150
+ event_id: crypto.randomUUID(),
1151
+ session_id: session.sessionId,
1152
+ event_type: "session.end",
1153
+ timestamp: now.toISOString(),
1154
+ duration_ms: durationMs,
1155
+ tool: "claude-code",
1156
+ metadata: {
1157
+ started_at: session.startedAt,
1158
+ ended_at: now.toISOString()
1159
+ }
1160
+ }
1161
+ ]);
1162
+ try {
1163
+ unlinkSync2(SESSION_FILE);
1164
+ } catch {
1165
+ }
1166
+ if (sent) {
1167
+ success(`Valt session ended: ${session.sessionId} (${Math.round(durationMs / 1e3)}s)`);
1168
+ } else {
1169
+ warn("Session ended locally but failed to send to Valt.");
1170
+ }
1171
+ } catch (err) {
1172
+ error(`Failed to end session: ${err instanceof Error ? err.message : String(err)}`);
1173
+ }
1174
+ });
1175
+ var hookCommand = new Command13("hook").description("Claude Code hook handlers for Valt session tracking").addCommand(sessionStartCommand).addCommand(toolCallCommand).addCommand(sessionEndCommand);
1176
+
1177
+ // src/commands/start.ts
1178
+ import { Command as Command14 } from "commander";
1179
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync4 } from "fs";
1180
+ import { resolve as resolve3 } from "path";
1181
+ var HOOKS_PATH = ".claude/hooks.json";
1182
+ var VALT_HOOKS = {
1183
+ SessionStart: [{
1184
+ hooks: [{ type: "command", command: "npx @usevalt/cli hook session-start" }]
1185
+ }],
1186
+ PreToolCall: [{
1187
+ matcher: ".*",
1188
+ hooks: [{ type: "command", command: "echo '{}' | npx @usevalt/cli hook tool-call" }]
1189
+ }],
1190
+ Stop: [{
1191
+ hooks: [{ type: "command", command: "npx @usevalt/cli hook session-end" }]
1192
+ }]
1193
+ };
1194
+ var startCommand = new Command14("start").description("Configure Valt for Claude Code in the current project").action(() => {
1195
+ try {
1196
+ const apiKey = getApiKey();
1197
+ if (!apiKey) {
1198
+ error("No API key found. Set VALT_API_KEY or run `valt login` first.");
1199
+ process.exit(1);
1200
+ }
1201
+ const claudeDir = resolve3(process.cwd(), ".claude");
1202
+ if (!existsSync4(claudeDir)) {
1203
+ warn("No .claude/ directory found. This command is designed for Claude Code projects.");
1204
+ info("Creating .claude/ directory...");
1205
+ const { mkdirSync: mkdirSync2 } = __require("fs");
1206
+ mkdirSync2(claudeDir, { recursive: true });
1207
+ }
1208
+ const hooksFile = resolve3(process.cwd(), HOOKS_PATH);
1209
+ let existingHooks = {};
1210
+ if (existsSync4(hooksFile)) {
1211
+ try {
1212
+ existingHooks = JSON.parse(readFileSync4(hooksFile, "utf-8"));
1213
+ info("Found existing hooks.json -- merging Valt hooks.");
1214
+ } catch {
1215
+ warn("Existing hooks.json is not valid JSON. Creating a new one.");
1216
+ existingHooks = {};
1217
+ }
1218
+ }
1219
+ const hooks = existingHooks.hooks ?? {};
1220
+ for (const [event, valtEntries] of Object.entries(VALT_HOOKS)) {
1221
+ const existing = hooks[event];
1222
+ const hasValt = existing?.some(
1223
+ (entry) => entry.hooks?.some((h) => h.command?.includes("@usevalt/cli"))
1224
+ );
1225
+ if (hasValt) {
1226
+ continue;
1227
+ }
1228
+ hooks[event] = [...existing ?? [], ...valtEntries];
1229
+ }
1230
+ const merged = { ...existingHooks, hooks };
1231
+ writeFileSync4(hooksFile, JSON.stringify(merged, null, 2) + "\n", "utf-8");
1232
+ success("Valt is configured.");
1233
+ info(`Hooks written to ${bold(HOOKS_PATH)}`);
1234
+ info("Start a Claude Code session and your work will be tracked automatically.");
1235
+ } catch (err) {
1236
+ error(`Failed to configure: ${err instanceof Error ? err.message : String(err)}`);
1237
+ process.exit(1);
1238
+ }
1239
+ });
1240
+
662
1241
  // src/index.ts
663
- var program = new Command13();
664
- program.name("valt").description("Valt CLI \u2014 trust layer for AI-assisted development").version("0.1.0");
1242
+ var program = new Command15();
1243
+ program.name("valt").description("Valt CLI -- trust layer for AI-assisted development").version("0.2.0");
665
1244
  program.addCommand(initCommand);
666
1245
  program.addCommand(loginCommand);
667
1246
  program.addCommand(statusCommand);
@@ -674,4 +1253,6 @@ program.addCommand(certCommand);
674
1253
  program.addCommand(costsCommand);
675
1254
  program.addCommand(analyzeCommand);
676
1255
  program.addCommand(proxyCommand);
1256
+ program.addCommand(hookCommand);
1257
+ program.addCommand(startCommand);
677
1258
  program.parse();
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@usevalt/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Valt CLI — trust layer for AI-assisted development",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/valtdev/valt",
8
+ "url": "https://github.com/usevalt/valt",
9
9
  "directory": "packages/cli"
10
10
  },
11
11
  "keywords": [
@@ -25,21 +25,6 @@
25
25
  "files": [
26
26
  "dist"
27
27
  ],
28
- "dependencies": {
29
- "commander": "^13.0.0",
30
- "chalk": "^5.4.0",
31
- "ora": "^8.1.0",
32
- "@usevalt/shared": "0.1.0"
33
- },
34
- "devDependencies": {
35
- "tsup": "^8.4.0",
36
- "typescript": "^5.7.0",
37
- "vitest": "^3.2.0",
38
- "@types/node": "^22.0.0",
39
- "eslint": "^9.0.0",
40
- "@usevalt/typescript-config": "0.0.0",
41
- "@usevalt/eslint-config": "0.0.0"
42
- },
43
28
  "scripts": {
44
29
  "build": "tsup",
45
30
  "dev": "tsup --watch",
@@ -47,5 +32,22 @@
47
32
  "lint": "eslint src/",
48
33
  "typecheck": "tsc --noEmit",
49
34
  "clean": "rm -rf dist"
35
+ },
36
+ "dependencies": {
37
+ "@usevalt/shared": "workspace:*",
38
+ "chalk": "^5.4.0",
39
+ "commander": "^13.0.0",
40
+ "open": "^11.0.0",
41
+ "ora": "^8.1.0",
42
+ "zod": "^3.25.76"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^22.0.0",
46
+ "@usevalt/eslint-config": "workspace:*",
47
+ "@usevalt/typescript-config": "workspace:*",
48
+ "eslint": "^9.0.0",
49
+ "tsup": "^8.4.0",
50
+ "typescript": "^5.7.0",
51
+ "vitest": "^3.2.0"
50
52
  }
51
- }
53
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Valt
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.