@r4security/cli 0.0.2 → 0.0.5

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/lib/index.js CHANGED
@@ -1,38 +1,433 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command12 } from "commander";
4
+ import { Command as Command18 } from "commander";
5
5
 
6
- // src/commands/auth/login.ts
7
- import { Command } from "commander";
6
+ // src/commands/agent/init.ts
7
+ import { Command as Command2 } from "commander";
8
8
  import readline from "node:readline";
9
+ import ora2 from "ora";
9
10
 
10
- // src/lib/config.ts
11
+ // src/commands/doctor.ts
12
+ import { Command } from "commander";
13
+ import chalk2 from "chalk";
14
+ import ora from "ora";
15
+
16
+ // src/lib/doctor.ts
17
+ import R4 from "@r4security/sdk";
18
+
19
+ // src/lib/client.ts
20
+ var CliClient = class {
21
+ apiKey;
22
+ baseUrl;
23
+ constructor(apiKey, baseUrl) {
24
+ this.apiKey = apiKey;
25
+ this.baseUrl = baseUrl.replace(/\/$/, "");
26
+ }
27
+ /**
28
+ * Make an authenticated request to the machine API.
29
+ * Handles error responses consistently with the SDK pattern.
30
+ */
31
+ async request(method, path5, body) {
32
+ const url = `${this.baseUrl}${path5}`;
33
+ const response = await fetch(url, {
34
+ method,
35
+ headers: {
36
+ "X-API-Key": this.apiKey,
37
+ "Content-Type": "application/json"
38
+ },
39
+ body: body ? JSON.stringify(body) : void 0
40
+ });
41
+ if (!response.ok) {
42
+ const errorBody = await response.json().catch(() => ({}));
43
+ const error = errorBody?.error;
44
+ const errorMessage = error?.message || `HTTP ${response.status}: ${response.statusText}`;
45
+ const errorCode = typeof error?.code === "string" ? ` [${error.code}]` : "";
46
+ throw new Error(`R4 API Error${errorCode}: ${errorMessage}`);
47
+ }
48
+ return response.json();
49
+ }
50
+ /** Register or re-confirm the local agent runtime public key. */
51
+ async registerAgentPublicKey(body) {
52
+ return this.request(
53
+ "POST",
54
+ "/api/v1/machine/vault/public-key",
55
+ body
56
+ );
57
+ }
58
+ /** List all visible vaults for the authenticated machine principal. */
59
+ async listVaults(projectId) {
60
+ const search = projectId ? `?projectId=${encodeURIComponent(projectId)}` : "";
61
+ return this.request("GET", `/api/v1/machine/vault${search}`);
62
+ }
63
+ /** Retrieve the active wrapped DEK for the current agent on a vault. */
64
+ async getAgentWrappedKey(vaultId) {
65
+ return this.request(
66
+ "GET",
67
+ `/api/v1/machine/vault/${encodeURIComponent(vaultId)}/wrapped-key`
68
+ );
69
+ }
70
+ /** List lightweight metadata for all items in a vault. */
71
+ async listVaultItems(vaultId) {
72
+ return this.request(
73
+ "GET",
74
+ `/api/v1/machine/vault/${encodeURIComponent(vaultId)}/items`
75
+ );
76
+ }
77
+ /** List all projects. GET /api/v1/machine/project */
78
+ async listProjects() {
79
+ return this.request("GET", "/api/v1/machine/project");
80
+ }
81
+ /** Get project details. GET /api/v1/machine/project/:id */
82
+ async getProject(id) {
83
+ return this.request("GET", `/api/v1/machine/project/${id}`);
84
+ }
85
+ /** Create a new project. POST /api/v1/machine/project */
86
+ async createProject(data) {
87
+ return this.request("POST", "/api/v1/machine/project", data);
88
+ }
89
+ };
90
+
91
+ // src/lib/private-key.ts
92
+ import crypto from "node:crypto";
11
93
  import fs from "node:fs";
12
94
  import os from "node:os";
13
95
  import path from "node:path";
14
- var CONFIG_DIR = path.join(os.homedir(), ".r4");
15
- var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
16
- function loadConfig() {
17
- try {
18
- const raw = fs.readFileSync(CONFIG_PATH, "utf8");
19
- return JSON.parse(raw);
20
- } catch {
21
- return {};
96
+ var DEFAULT_KEYS_DIR = path.join(os.homedir(), ".r4", "keys");
97
+ function sanitizeProfileName(profileName) {
98
+ const sanitized = profileName.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
99
+ return sanitized || "default";
100
+ }
101
+ function getDefaultPrivateKeyPath(profileName) {
102
+ return path.join(DEFAULT_KEYS_DIR, `${sanitizeProfileName(profileName)}.pem`);
103
+ }
104
+ function resolvePrivateKeyPath(params) {
105
+ if (params.cliPath) {
106
+ return { value: params.cliPath, source: "--private-key-path flag" };
107
+ }
108
+ if (params.envPath) {
109
+ return { value: params.envPath, source: "R4_PRIVATE_KEY_PATH env var" };
110
+ }
111
+ if (params.profilePath) {
112
+ return { value: params.profilePath, source: "profile config" };
113
+ }
114
+ const defaultPath = getDefaultPrivateKeyPath(params.profileName);
115
+ if (fs.existsSync(defaultPath) || params.allowDefaultIfMissing === true) {
116
+ return { value: defaultPath, source: "default profile key path" };
22
117
  }
118
+ return { value: void 0, source: null };
23
119
  }
24
- function saveConfig(config) {
25
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
26
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf8");
120
+ function loadPrivateKey(privateKeyPath) {
121
+ return fs.readFileSync(path.resolve(privateKeyPath), "utf8").trim();
27
122
  }
28
- function clearConfig() {
29
- try {
30
- fs.unlinkSync(CONFIG_PATH);
31
- } catch {
123
+ function derivePublicKey(privateKeyPem) {
124
+ return crypto.createPublicKey(privateKeyPem).export({
125
+ type: "spki",
126
+ format: "pem"
127
+ }).toString();
128
+ }
129
+ function ensurePrivateKey(privateKeyPath) {
130
+ const resolvedPath = path.resolve(privateKeyPath);
131
+ if (fs.existsSync(resolvedPath)) {
132
+ return {
133
+ privateKeyPem: loadPrivateKey(resolvedPath),
134
+ created: false
135
+ };
32
136
  }
137
+ const keyPair = crypto.generateKeyPairSync("rsa", {
138
+ modulusLength: 2048,
139
+ publicKeyEncoding: {
140
+ type: "spki",
141
+ format: "pem"
142
+ },
143
+ privateKeyEncoding: {
144
+ type: "pkcs8",
145
+ format: "pem"
146
+ }
147
+ });
148
+ fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
149
+ fs.writeFileSync(resolvedPath, keyPair.privateKey.trim() + "\n", {
150
+ encoding: "utf8",
151
+ mode: 384
152
+ });
153
+ return {
154
+ privateKeyPem: keyPair.privateKey.trim(),
155
+ created: true
156
+ };
33
157
  }
34
- function getConfigPath() {
35
- return CONFIG_PATH;
158
+
159
+ // src/lib/doctor.ts
160
+ function getAgentIdFromRegistration(registration) {
161
+ const entryCount = registration?.transparency?.entries.length ?? 0;
162
+ if (!registration?.transparency || entryCount === 0) {
163
+ return void 0;
164
+ }
165
+ return registration.transparency.entries[entryCount - 1]?.agentId;
166
+ }
167
+ function toDetailList(vaults) {
168
+ if (vaults.length === 0) {
169
+ return "0 visible vaults.";
170
+ }
171
+ const visibleNames = vaults.slice(0, 5).map((vault) => vault.name);
172
+ const suffix = vaults.length > visibleNames.length ? ", ..." : "";
173
+ return `${vaults.length} visible vault${vaults.length === 1 ? "" : "s"}: ${visibleNames.join(", ")}${suffix}`;
174
+ }
175
+ function isWrappedKeyMissing(error) {
176
+ return error instanceof Error && (error.message.includes("[wrapped_key_not_found]") || error.message.includes("No wrapped key found for this agent and vault."));
177
+ }
178
+ function doctorHasFailures(report) {
179
+ return report.checks.some((check) => check.status === "fail");
180
+ }
181
+ async function runDoctorChecks(connection) {
182
+ const report = {
183
+ profileName: connection.profileName,
184
+ baseUrl: connection.baseUrl,
185
+ dev: connection.dev,
186
+ projectId: connection.projectId,
187
+ privateKeyPath: connection.privateKeyPath,
188
+ trustStorePath: connection.trustStorePath,
189
+ agentName: connection.profile.agentName,
190
+ checks: [],
191
+ vaults: []
192
+ };
193
+ report.checks.push({
194
+ id: "base-url",
195
+ label: "Base URL",
196
+ status: "pass",
197
+ detail: `${connection.baseUrl}${connection.dev ? " (dev mode)" : ""}`
198
+ });
199
+ if (!connection.apiKey) {
200
+ report.checks.push({
201
+ id: "api-key",
202
+ label: "API Key",
203
+ status: "fail",
204
+ detail: "No API key is configured for the active profile."
205
+ });
206
+ report.checks.push({
207
+ id: "project-filter",
208
+ label: "Project Filter",
209
+ status: "pass",
210
+ detail: connection.projectId ? `Filtering to project ${connection.projectId}.` : "No project filter is set."
211
+ });
212
+ return report;
213
+ }
214
+ report.checks.push({
215
+ id: "api-key",
216
+ label: "API Key",
217
+ status: "pass",
218
+ detail: `Loaded from ${connection.apiKeySource}.`
219
+ });
220
+ report.checks.push({
221
+ id: "project-filter",
222
+ label: "Project Filter",
223
+ status: "pass",
224
+ detail: connection.projectId ? `Filtering vault reads to project ${connection.projectId}.` : "No project filter is set."
225
+ });
226
+ const client = new CliClient(connection.apiKey, connection.baseUrl);
227
+ let registration;
228
+ if (!connection.privateKeyPath) {
229
+ report.checks.push({
230
+ id: "private-key",
231
+ label: "Private Key",
232
+ status: "fail",
233
+ detail: "No local private-key path is configured, so zero-trust checks cannot run."
234
+ });
235
+ report.checks.push({
236
+ id: "public-key",
237
+ label: "Public Key Registration",
238
+ status: "skip",
239
+ detail: "Skipped because no local private key is configured."
240
+ });
241
+ report.checks.push({
242
+ id: "agent-identity",
243
+ label: "Agent Identity",
244
+ status: "skip",
245
+ detail: "Skipped because public-key registration could not run."
246
+ });
247
+ } else {
248
+ try {
249
+ const privateKeyPem = loadPrivateKey(connection.privateKeyPath);
250
+ const publicKeyPem = derivePublicKey(privateKeyPem);
251
+ registration = await client.registerAgentPublicKey({ publicKey: publicKeyPem });
252
+ report.agentId = getAgentIdFromRegistration(registration) ?? connection.profile.agentId;
253
+ report.registration = {
254
+ encryptionKeyId: registration.encryptionKeyId,
255
+ fingerprint: registration.fingerprint
256
+ };
257
+ report.checks.push({
258
+ id: "private-key",
259
+ label: "Private Key",
260
+ status: "pass",
261
+ detail: `Loaded ${connection.privateKeyPath}.`
262
+ });
263
+ report.checks.push({
264
+ id: "public-key",
265
+ label: "Public Key Registration",
266
+ status: "pass",
267
+ detail: `Registered encryption key ${registration.encryptionKeyId}.`
268
+ });
269
+ report.checks.push({
270
+ id: "agent-identity",
271
+ label: "Agent Identity",
272
+ status: report.agentId ? "pass" : "warn",
273
+ detail: report.agentId ? `Agent ${report.agentId}${report.agentName ? ` (${report.agentName})` : ""}.` : "The API returned no agent ID in the current registration proof."
274
+ });
275
+ } catch (error) {
276
+ report.checks.push({
277
+ id: "private-key",
278
+ label: "Private Key",
279
+ status: "pass",
280
+ detail: `Configured at ${connection.privateKeyPath}.`
281
+ });
282
+ report.checks.push({
283
+ id: "public-key",
284
+ label: "Public Key Registration",
285
+ status: "fail",
286
+ detail: error instanceof Error ? error.message : String(error)
287
+ });
288
+ report.checks.push({
289
+ id: "agent-identity",
290
+ label: "Agent Identity",
291
+ status: "skip",
292
+ detail: "Skipped because public-key registration did not succeed."
293
+ });
294
+ }
295
+ }
296
+ let vaults = [];
297
+ try {
298
+ const response = await client.listVaults(connection.projectId);
299
+ vaults = response.vaults;
300
+ report.checks.push({
301
+ id: "visible-vaults",
302
+ label: "Visible Vaults",
303
+ status: "pass",
304
+ detail: toDetailList(vaults)
305
+ });
306
+ } catch (error) {
307
+ report.checks.push({
308
+ id: "visible-vaults",
309
+ label: "Visible Vaults",
310
+ status: "fail",
311
+ detail: error instanceof Error ? error.message : String(error)
312
+ });
313
+ report.checks.push({
314
+ id: "wrapped-keys",
315
+ label: "Wrapped Keys",
316
+ status: "skip",
317
+ detail: "Skipped because visible vaults could not be listed."
318
+ });
319
+ report.checks.push({
320
+ id: "zero-trust",
321
+ label: "Trust & Decryption",
322
+ status: "skip",
323
+ detail: "Skipped because visible vaults could not be listed."
324
+ });
325
+ return report;
326
+ }
327
+ if (vaults.length === 0) {
328
+ report.checks.push({
329
+ id: "wrapped-keys",
330
+ label: "Wrapped Keys",
331
+ status: "skip",
332
+ detail: "No visible vaults means there were no wrapped keys to verify."
333
+ });
334
+ report.checks.push({
335
+ id: "zero-trust",
336
+ label: "Trust & Decryption",
337
+ status: "skip",
338
+ detail: "No visible vaults were returned, so no vault trust proof was exercised."
339
+ });
340
+ return report;
341
+ }
342
+ let missingWrappedKeys = 0;
343
+ let wrappedKeyErrors = 0;
344
+ for (const vault of vaults) {
345
+ try {
346
+ await client.getAgentWrappedKey(vault.id);
347
+ report.vaults.push({
348
+ id: vault.id,
349
+ name: vault.name,
350
+ itemCount: vault.itemCount,
351
+ wrappedKeyStatus: "present"
352
+ });
353
+ } catch (error) {
354
+ if (isWrappedKeyMissing(error)) {
355
+ missingWrappedKeys += 1;
356
+ report.vaults.push({
357
+ id: vault.id,
358
+ name: vault.name,
359
+ itemCount: vault.itemCount,
360
+ wrappedKeyStatus: "missing",
361
+ detail: error instanceof Error ? error.message : String(error)
362
+ });
363
+ continue;
364
+ }
365
+ wrappedKeyErrors += 1;
366
+ report.vaults.push({
367
+ id: vault.id,
368
+ name: vault.name,
369
+ itemCount: vault.itemCount,
370
+ wrappedKeyStatus: "error",
371
+ detail: error instanceof Error ? error.message : String(error)
372
+ });
373
+ }
374
+ }
375
+ if (wrappedKeyErrors > 0) {
376
+ report.checks.push({
377
+ id: "wrapped-keys",
378
+ label: "Wrapped Keys",
379
+ status: "fail",
380
+ detail: `${wrappedKeyErrors} vault read${wrappedKeyErrors === 1 ? "" : "s"} hit an unexpected wrapped-key error.`
381
+ });
382
+ } else if (missingWrappedKeys > 0) {
383
+ report.checks.push({
384
+ id: "wrapped-keys",
385
+ label: "Wrapped Keys",
386
+ status: "warn",
387
+ detail: `${missingWrappedKeys} of ${vaults.length} visible vault${vaults.length === 1 ? "" : "s"} do not have wrapped keys for this agent.`
388
+ });
389
+ } else {
390
+ report.checks.push({
391
+ id: "wrapped-keys",
392
+ label: "Wrapped Keys",
393
+ status: "pass",
394
+ detail: `Wrapped keys are present for all ${vaults.length} visible vault${vaults.length === 1 ? "" : "s"}.`
395
+ });
396
+ }
397
+ if (!connection.privateKeyPath) {
398
+ report.checks.push({
399
+ id: "zero-trust",
400
+ label: "Trust & Decryption",
401
+ status: "fail",
402
+ detail: "A local private key is required for end-to-end zero-trust verification."
403
+ });
404
+ return report;
405
+ }
406
+ try {
407
+ const r4 = await R4.create({
408
+ apiKey: connection.apiKey,
409
+ baseUrl: connection.baseUrl,
410
+ dev: connection.dev,
411
+ projectId: connection.projectId,
412
+ privateKeyPath: connection.privateKeyPath,
413
+ trustStorePath: connection.trustStorePath
414
+ });
415
+ report.envKeyCount = Object.keys(r4.env).length;
416
+ report.checks.push({
417
+ id: "zero-trust",
418
+ label: "Trust & Decryption",
419
+ status: "pass",
420
+ detail: `Verified trust/transparency checks and decrypted ${report.envKeyCount} env key${report.envKeyCount === 1 ? "" : "s"} locally.`
421
+ });
422
+ } catch (error) {
423
+ report.checks.push({
424
+ id: "zero-trust",
425
+ label: "Trust & Decryption",
426
+ status: "fail",
427
+ detail: error instanceof Error ? error.message : String(error)
428
+ });
429
+ }
430
+ return report;
36
431
  }
37
432
 
38
433
  // src/lib/output.ts
@@ -75,13 +470,32 @@ function printError(message) {
75
470
  }
76
471
 
77
472
  // src/lib/errors.ts
473
+ function formatErrorMessage(message) {
474
+ if (message.includes("[wrapped_key_not_found]") || message.includes("No wrapped key found for this agent and vault.")) {
475
+ return `${message}
476
+
477
+ Remediation:
478
+ - Make sure the vault or vault item is shared with this agent, one of its security groups, or one of its projects.
479
+ - If you just created or rotated the local runtime key, re-run \`r4 agent init\` and then re-wrap access for the agent.
480
+ - Run \`r4 doctor\` to see which visible vaults are missing wrapped keys.`;
481
+ }
482
+ if (message.includes("failed to register the local agent public key")) {
483
+ return `${message}
484
+
485
+ Remediation:
486
+ - Use an AGENT-scoped API key.
487
+ - Point \`--private-key-path\` at the matching local PEM file, or run \`r4 agent init\` to generate and register one automatically.
488
+ - If you intentionally target a non-production environment, include \`--dev\` or set \`R4_DEV=1\`.`;
489
+ }
490
+ return message;
491
+ }
78
492
  function withErrorHandler(fn) {
79
493
  return async (...args) => {
80
494
  try {
81
495
  await fn(...args);
82
496
  } catch (err) {
83
497
  if (err instanceof Error) {
84
- printError(err.message);
498
+ printError(formatErrorMessage(err.message));
85
499
  } else {
86
500
  printError("An unexpected error occurred");
87
501
  }
@@ -90,6 +504,150 @@ function withErrorHandler(fn) {
90
504
  };
91
505
  }
92
506
 
507
+ // src/lib/resolve-auth.ts
508
+ import path3 from "node:path";
509
+
510
+ // src/lib/config.ts
511
+ import fs2 from "node:fs";
512
+ import os2 from "node:os";
513
+ import path2 from "node:path";
514
+ var CONFIG_DIR = path2.join(os2.homedir(), ".r4");
515
+ var CONFIG_PATH = path2.join(CONFIG_DIR, "config.json");
516
+ var DEFAULT_PROFILE_NAME = "default";
517
+ function emptyConfig() {
518
+ return {
519
+ version: 2,
520
+ currentProfile: DEFAULT_PROFILE_NAME,
521
+ profiles: {
522
+ [DEFAULT_PROFILE_NAME]: {}
523
+ }
524
+ };
525
+ }
526
+ function sanitizeProfileConfig(profile) {
527
+ if (!profile || typeof profile !== "object") {
528
+ return {};
529
+ }
530
+ const value = profile;
531
+ const nextProfile = {};
532
+ if (typeof value.apiKey === "string" && value.apiKey.trim()) {
533
+ nextProfile.apiKey = value.apiKey.trim();
534
+ }
535
+ if (typeof value.baseUrl === "string" && value.baseUrl.trim()) {
536
+ nextProfile.baseUrl = value.baseUrl.trim();
537
+ }
538
+ if (typeof value.dev === "boolean") {
539
+ nextProfile.dev = value.dev;
540
+ }
541
+ if (typeof value.projectId === "string" && value.projectId.trim()) {
542
+ nextProfile.projectId = value.projectId.trim();
543
+ }
544
+ if (typeof value.privateKeyPath === "string" && value.privateKeyPath.trim()) {
545
+ nextProfile.privateKeyPath = value.privateKeyPath.trim();
546
+ }
547
+ if (typeof value.trustStorePath === "string" && value.trustStorePath.trim()) {
548
+ nextProfile.trustStorePath = value.trustStorePath.trim();
549
+ }
550
+ if (typeof value.agentId === "string" && value.agentId.trim()) {
551
+ nextProfile.agentId = value.agentId.trim();
552
+ }
553
+ if (typeof value.agentName === "string" && value.agentName.trim()) {
554
+ nextProfile.agentName = value.agentName.trim();
555
+ }
556
+ return nextProfile;
557
+ }
558
+ function hasLegacyTopLevelProfileFields(config) {
559
+ return Boolean(
560
+ config.apiKey || config.baseUrl || config.dev !== void 0 || config.projectId || config.privateKeyPath || config.trustStorePath || config.agentId || config.agentName
561
+ );
562
+ }
563
+ function normalizeConfig(raw) {
564
+ if (!raw || typeof raw !== "object") {
565
+ return emptyConfig();
566
+ }
567
+ const parsed = raw;
568
+ const nextConfig = emptyConfig();
569
+ if (parsed.profiles && typeof parsed.profiles === "object") {
570
+ const entries = Object.entries(parsed.profiles).filter(([name]) => typeof name === "string" && name.trim().length > 0).map(([name, profile]) => [name.trim(), sanitizeProfileConfig(profile)]);
571
+ if (entries.length > 0) {
572
+ nextConfig.profiles = Object.fromEntries(entries);
573
+ }
574
+ }
575
+ if (hasLegacyTopLevelProfileFields(parsed)) {
576
+ nextConfig.profiles[DEFAULT_PROFILE_NAME] = {
577
+ ...nextConfig.profiles[DEFAULT_PROFILE_NAME],
578
+ ...sanitizeProfileConfig(parsed)
579
+ };
580
+ }
581
+ const configuredCurrentProfile = typeof parsed.currentProfile === "string" && parsed.currentProfile.trim() ? parsed.currentProfile.trim() : DEFAULT_PROFILE_NAME;
582
+ nextConfig.currentProfile = nextConfig.profiles[configuredCurrentProfile] !== void 0 ? configuredCurrentProfile : Object.keys(nextConfig.profiles)[0] ?? DEFAULT_PROFILE_NAME;
583
+ if (Object.keys(nextConfig.profiles).length === 0) {
584
+ return emptyConfig();
585
+ }
586
+ return nextConfig;
587
+ }
588
+ function loadConfig() {
589
+ try {
590
+ const raw = fs2.readFileSync(CONFIG_PATH, "utf8");
591
+ return normalizeConfig(JSON.parse(raw));
592
+ } catch {
593
+ return emptyConfig();
594
+ }
595
+ }
596
+ function saveConfig(config) {
597
+ fs2.mkdirSync(CONFIG_DIR, { recursive: true });
598
+ fs2.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf8");
599
+ }
600
+ function clearConfig() {
601
+ try {
602
+ fs2.unlinkSync(CONFIG_PATH);
603
+ } catch {
604
+ }
605
+ }
606
+ function getConfigPath() {
607
+ return CONFIG_PATH;
608
+ }
609
+ function getProfileNames(config) {
610
+ return Object.keys(config.profiles).sort();
611
+ }
612
+ function getCurrentProfileName(config) {
613
+ return config.currentProfile;
614
+ }
615
+ function getSelectedProfileName(config, profileOverride, envProfile) {
616
+ const selectedProfile = profileOverride || envProfile || config.currentProfile;
617
+ return selectedProfile?.trim() || DEFAULT_PROFILE_NAME;
618
+ }
619
+ function getProfileConfig(config, profileName) {
620
+ return config.profiles[profileName] ?? {};
621
+ }
622
+ function updateProfile(config, profileName, profile) {
623
+ return {
624
+ ...config,
625
+ profiles: {
626
+ ...config.profiles,
627
+ [profileName]: profile
628
+ }
629
+ };
630
+ }
631
+ function setCurrentProfile(config, profileName) {
632
+ return {
633
+ ...config,
634
+ currentProfile: profileName
635
+ };
636
+ }
637
+ function removeProfile(config, profileName) {
638
+ const remainingProfiles = Object.fromEntries(
639
+ Object.entries(config.profiles).filter(([name]) => name !== profileName)
640
+ );
641
+ if (Object.keys(remainingProfiles).length === 0) {
642
+ return emptyConfig();
643
+ }
644
+ return {
645
+ version: 2,
646
+ currentProfile: config.currentProfile === profileName ? Object.keys(remainingProfiles).sort()[0] ?? DEFAULT_PROFILE_NAME : config.currentProfile,
647
+ profiles: remainingProfiles
648
+ };
649
+ }
650
+
93
651
  // src/lib/runtime-config.ts
94
652
  var R4_DEFAULT_API_BASE_URL = "https://r4.dev";
95
653
  var R4_DEV_API_BASE_URL = "https://dev.r4.dev";
@@ -126,28 +684,470 @@ function resolveRuntimeModeFromCli(opts, config) {
126
684
  configDev: config.dev
127
685
  });
128
686
  }
129
- function applyGlobalRuntimeOptionsToConfig(config, opts) {
130
- const nextConfig = { ...config };
687
+ function applyGlobalRuntimeOptionsToProfile(profile, opts) {
688
+ const nextProfile = { ...profile };
131
689
  if (opts.baseUrl) {
132
- nextConfig.baseUrl = opts.baseUrl;
133
- delete nextConfig.dev;
690
+ nextProfile.baseUrl = opts.baseUrl;
691
+ delete nextProfile.dev;
134
692
  } else if (opts.dev === true) {
135
- nextConfig.dev = true;
136
- delete nextConfig.baseUrl;
693
+ nextProfile.dev = true;
694
+ delete nextProfile.baseUrl;
137
695
  }
138
696
  if (opts.projectId) {
139
- nextConfig.projectId = opts.projectId;
697
+ nextProfile.projectId = opts.projectId;
140
698
  }
141
699
  if (opts.privateKeyPath) {
142
- nextConfig.privateKeyPath = opts.privateKeyPath;
700
+ nextProfile.privateKeyPath = opts.privateKeyPath;
143
701
  }
144
702
  if (opts.trustStorePath) {
145
- nextConfig.trustStorePath = opts.trustStorePath;
703
+ nextProfile.trustStorePath = opts.trustStorePath;
146
704
  }
147
- return nextConfig;
705
+ return nextProfile;
148
706
  }
149
707
 
150
- // src/commands/auth/login.ts
708
+ // src/lib/resolve-auth.ts
709
+ function buildApiKeyFromParts(accessKey, secretKey) {
710
+ if (!accessKey || !secretKey) {
711
+ return void 0;
712
+ }
713
+ return `${accessKey}.${secretKey}`;
714
+ }
715
+ function resolveApiKey(opts, profile, profileName) {
716
+ if (opts.apiKey) {
717
+ return {
718
+ value: opts.apiKey,
719
+ source: "--api-key flag",
720
+ incompleteSplitEnv: false
721
+ };
722
+ }
723
+ if (process.env.R4_API_KEY) {
724
+ return {
725
+ value: process.env.R4_API_KEY,
726
+ source: "R4_API_KEY env var",
727
+ incompleteSplitEnv: false
728
+ };
729
+ }
730
+ const splitEnvApiKey = buildApiKeyFromParts(
731
+ process.env.R4_ACCESS_KEY,
732
+ process.env.R4_SECRET_KEY
733
+ );
734
+ if (splitEnvApiKey) {
735
+ return {
736
+ value: splitEnvApiKey,
737
+ source: "R4_ACCESS_KEY + R4_SECRET_KEY env vars",
738
+ incompleteSplitEnv: false
739
+ };
740
+ }
741
+ if (profile.apiKey) {
742
+ return {
743
+ value: profile.apiKey,
744
+ source: `profile "${profileName}"`,
745
+ incompleteSplitEnv: false
746
+ };
747
+ }
748
+ return {
749
+ value: void 0,
750
+ source: "none",
751
+ incompleteSplitEnv: Boolean(process.env.R4_ACCESS_KEY || process.env.R4_SECRET_KEY)
752
+ };
753
+ }
754
+ function resolveProjectId(opts, profile) {
755
+ return opts.projectId || process.env.R4_PROJECT_ID || profile.projectId;
756
+ }
757
+ function resolveTrustStorePath(opts, profile, privateKeyPath) {
758
+ if (opts.trustStorePath) {
759
+ return { value: opts.trustStorePath, source: "--trust-store-path flag" };
760
+ }
761
+ if (process.env.R4_TRUST_STORE_PATH) {
762
+ return { value: process.env.R4_TRUST_STORE_PATH, source: "R4_TRUST_STORE_PATH env var" };
763
+ }
764
+ if (profile.trustStorePath) {
765
+ return { value: profile.trustStorePath, source: "profile config" };
766
+ }
767
+ if (privateKeyPath) {
768
+ return {
769
+ value: `${path3.resolve(privateKeyPath)}.trust.json`,
770
+ source: "default beside private key"
771
+ };
772
+ }
773
+ return { value: void 0, source: null };
774
+ }
775
+ function resolveConnection(opts, params) {
776
+ const configFile = loadConfig();
777
+ const profileName = getSelectedProfileName(
778
+ configFile,
779
+ opts.profile,
780
+ process.env.R4_PROFILE
781
+ );
782
+ const profile = getProfileConfig(configFile, profileName);
783
+ const runtimeMode = resolveRuntimeModeFromCli(opts, profile);
784
+ const apiKey = resolveApiKey(opts, profile, profileName);
785
+ const privateKey = resolvePrivateKeyPath({
786
+ cliPath: opts.privateKeyPath,
787
+ envPath: process.env.R4_PRIVATE_KEY_PATH,
788
+ profilePath: profile.privateKeyPath,
789
+ profileName
790
+ });
791
+ const trustStorePath = resolveTrustStorePath(opts, profile, privateKey.value);
792
+ if (params?.requireApiKey && !apiKey.value) {
793
+ throw new Error(
794
+ "No API key found. Provide one via:\n --api-key <key> CLI flag\n R4_API_KEY environment variable\n R4_ACCESS_KEY + R4_SECRET_KEY environment variables\n r4 auth login save it to a profile\n r4 agent init bootstrap the full first-run flow" + (apiKey.incompleteSplitEnv ? "\n\nBoth R4_ACCESS_KEY and R4_SECRET_KEY must be set together." : "")
795
+ );
796
+ }
797
+ if (params?.requirePrivateKey && !privateKey.value) {
798
+ throw new Error(
799
+ `No private key path found. Provide one via:
800
+ --private-key-path <path> CLI flag
801
+ R4_PRIVATE_KEY_PATH environment variable
802
+ profile config saved privateKeyPath
803
+ r4 agent init generate a key at ${getDefaultPrivateKeyPath(profileName)}`
804
+ );
805
+ }
806
+ return {
807
+ apiKey: apiKey.value,
808
+ apiKeySource: apiKey.source,
809
+ baseUrl: runtimeMode.baseUrl,
810
+ dev: runtimeMode.devMode,
811
+ projectId: resolveProjectId(opts, profile),
812
+ privateKeyPath: privateKey.value,
813
+ privateKeySource: privateKey.source,
814
+ trustStorePath: trustStorePath.value,
815
+ trustStoreSource: trustStorePath.source,
816
+ profileName,
817
+ profile,
818
+ configFile
819
+ };
820
+ }
821
+ function resolveAuth(opts) {
822
+ const connection = resolveConnection(opts, {
823
+ requireApiKey: true,
824
+ requirePrivateKey: true
825
+ });
826
+ return {
827
+ apiKey: connection.apiKey,
828
+ projectId: connection.projectId,
829
+ baseUrl: connection.baseUrl,
830
+ dev: connection.dev,
831
+ privateKeyPath: connection.privateKeyPath,
832
+ trustStorePath: connection.trustStorePath
833
+ };
834
+ }
835
+
836
+ // src/commands/doctor.ts
837
+ function renderStatus(status) {
838
+ switch (status) {
839
+ case "pass":
840
+ return chalk2.green("PASS");
841
+ case "warn":
842
+ return chalk2.yellow("WARN");
843
+ case "fail":
844
+ return chalk2.red("FAIL");
845
+ default:
846
+ return chalk2.gray("SKIP");
847
+ }
848
+ }
849
+ function printDoctorReport(report) {
850
+ console.log();
851
+ console.log(chalk2.bold(" R4 Doctor"));
852
+ console.log();
853
+ printDetail(
854
+ [
855
+ ["Profile", report.profileName],
856
+ ["Base URL", report.baseUrl],
857
+ ["Mode", report.dev ? "dev" : "prod"],
858
+ ["Project ID", report.projectId || chalk2.dim("(not set)")],
859
+ ["Private Key", report.privateKeyPath || chalk2.dim("(not set)")],
860
+ ["Trust Store", report.trustStorePath || chalk2.dim("(not set)")],
861
+ ["Agent ID", report.agentId || report.agentName || chalk2.dim("(unknown)")]
862
+ ],
863
+ false
864
+ );
865
+ console.log();
866
+ printTable(
867
+ ["Check", "Status", "Detail"],
868
+ report.checks.map((check) => [
869
+ check.label,
870
+ renderStatus(check.status),
871
+ check.detail
872
+ ]),
873
+ false
874
+ );
875
+ if (report.vaults.length > 0) {
876
+ console.log();
877
+ console.log(chalk2.bold(" Vaults"));
878
+ console.log();
879
+ printTable(
880
+ ["Vault", "Items", "Wrapped Key", "Detail"],
881
+ report.vaults.map((vault) => [
882
+ vault.name,
883
+ String(vault.itemCount),
884
+ vault.wrappedKeyStatus,
885
+ vault.detail ?? "-"
886
+ ]),
887
+ false
888
+ );
889
+ }
890
+ console.log();
891
+ }
892
+ function doctorCommand(commandName = "doctor", description = "Verify CLI auth, runtime key registration, vault access, and zero-trust health") {
893
+ return new Command(commandName).description(description).action(
894
+ withErrorHandler(async (_opts, cmd) => {
895
+ const globalOpts = cmd.optsWithGlobals();
896
+ const connection = resolveConnection(globalOpts);
897
+ const spinner = ora("Running CLI health checks...").start();
898
+ const report = await runDoctorChecks(connection);
899
+ spinner.stop();
900
+ if (globalOpts.json) {
901
+ console.log(JSON.stringify(report, null, 2));
902
+ if (doctorHasFailures(report)) {
903
+ process.exitCode = 1;
904
+ }
905
+ return;
906
+ }
907
+ printDoctorReport(report);
908
+ if (doctorHasFailures(report)) {
909
+ process.exitCode = 1;
910
+ }
911
+ })
912
+ );
913
+ }
914
+
915
+ // src/lib/credentials-file.ts
916
+ import fs3 from "node:fs";
917
+ import path4 from "node:path";
918
+ function normalizeFieldName(fieldName) {
919
+ return fieldName.trim().toLowerCase().replace(/[^a-z0-9]+/g, "");
920
+ }
921
+ function stripWrappingQuotes(value) {
922
+ const trimmed = value.trim();
923
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
924
+ return trimmed.slice(1, -1).trim();
925
+ }
926
+ return trimmed;
927
+ }
928
+ function parseBooleanLike(value) {
929
+ const normalized = value.trim().toLowerCase();
930
+ if (["1", "true", "yes", "on", "dev", "development"].includes(normalized)) {
931
+ return true;
932
+ }
933
+ if (["0", "false", "no", "off", "prod", "production"].includes(normalized)) {
934
+ return false;
935
+ }
936
+ return void 0;
937
+ }
938
+ function applyField(bundle, fieldName, rawValue) {
939
+ const value = stripWrappingQuotes(rawValue);
940
+ if (!value) {
941
+ return;
942
+ }
943
+ const normalized = normalizeFieldName(fieldName);
944
+ if (["apikey", "r4apikey", "machineapikey"].includes(normalized)) {
945
+ bundle.apiKey = value;
946
+ return;
947
+ }
948
+ if (["accesskey", "r4accesskey", "accesskeyid", "access", "keyid"].includes(normalized)) {
949
+ bundle.accessKey = value;
950
+ return;
951
+ }
952
+ if (["secretkey", "r4secretkey", "secret", "accesssecret", "secretaccesskey"].includes(normalized)) {
953
+ bundle.secretKey = value;
954
+ return;
955
+ }
956
+ if (["baseurl", "r4baseurl", "apiurl", "apibaseurl", "targetbaseurl", "url"].includes(normalized)) {
957
+ bundle.baseUrl = value;
958
+ return;
959
+ }
960
+ if (["environment", "env", "targetenvironment"].includes(normalized)) {
961
+ const dev = parseBooleanLike(value);
962
+ if (dev !== void 0) {
963
+ bundle.dev = dev;
964
+ }
965
+ return;
966
+ }
967
+ if (["projectid", "r4projectid", "project", "projectscope", "scopeprojectid"].includes(normalized)) {
968
+ bundle.projectId = value;
969
+ return;
970
+ }
971
+ if (["agentid", "r4agentid", "machineid"].includes(normalized)) {
972
+ bundle.agentId = value;
973
+ return;
974
+ }
975
+ if (["agentname", "r4agentname", "machinename"].includes(normalized)) {
976
+ bundle.agentName = value;
977
+ }
978
+ }
979
+ function finalizeBundle(bundle) {
980
+ if (!bundle.apiKey && bundle.accessKey && bundle.secretKey) {
981
+ bundle.apiKey = `${bundle.accessKey}.${bundle.secretKey}`;
982
+ }
983
+ return bundle;
984
+ }
985
+ function parseJsonContent(content) {
986
+ const parsed = JSON.parse(content);
987
+ const bundle = {};
988
+ const sources = [parsed];
989
+ for (const key of ["credentials", "profile"]) {
990
+ const nested = parsed[key];
991
+ if (nested && typeof nested === "object") {
992
+ sources.push(nested);
993
+ }
994
+ }
995
+ for (const source of sources) {
996
+ for (const [key, value] of Object.entries(source)) {
997
+ if (typeof value === "string") {
998
+ applyField(bundle, key, value);
999
+ } else if (typeof value === "boolean") {
1000
+ applyField(bundle, key, String(value));
1001
+ }
1002
+ }
1003
+ }
1004
+ return finalizeBundle(bundle);
1005
+ }
1006
+ function parseEnvContent(content) {
1007
+ const bundle = {};
1008
+ for (const rawLine of content.split(/\r?\n/)) {
1009
+ const line = rawLine.trim();
1010
+ if (!line || line.startsWith("#")) {
1011
+ continue;
1012
+ }
1013
+ const separatorIndex = line.indexOf("=");
1014
+ if (separatorIndex === -1) {
1015
+ continue;
1016
+ }
1017
+ const key = line.slice(0, separatorIndex).trim();
1018
+ const value = line.slice(separatorIndex + 1).trim();
1019
+ applyField(bundle, key, value);
1020
+ }
1021
+ return finalizeBundle(bundle);
1022
+ }
1023
+ function splitCsvLine(line) {
1024
+ const cells = [];
1025
+ let current = "";
1026
+ let inQuotes = false;
1027
+ for (let index = 0; index < line.length; index += 1) {
1028
+ const character = line[index];
1029
+ if (character === '"') {
1030
+ const nextCharacter = line[index + 1];
1031
+ if (inQuotes && nextCharacter === '"') {
1032
+ current += '"';
1033
+ index += 1;
1034
+ } else {
1035
+ inQuotes = !inQuotes;
1036
+ }
1037
+ continue;
1038
+ }
1039
+ if (character === "," && !inQuotes) {
1040
+ cells.push(current.trim());
1041
+ current = "";
1042
+ continue;
1043
+ }
1044
+ current += character;
1045
+ }
1046
+ cells.push(current.trim());
1047
+ return cells;
1048
+ }
1049
+ function parseHeaderlessCsvRow(values) {
1050
+ const bundle = {};
1051
+ if (values.length >= 1) {
1052
+ bundle.accessKey = stripWrappingQuotes(values[0] ?? "");
1053
+ }
1054
+ if (values.length >= 2) {
1055
+ bundle.secretKey = stripWrappingQuotes(values[1] ?? "");
1056
+ }
1057
+ if (values.length >= 3) {
1058
+ const thirdValue = stripWrappingQuotes(values[2] ?? "");
1059
+ if (thirdValue.startsWith("http://") || thirdValue.startsWith("https://")) {
1060
+ bundle.baseUrl = thirdValue;
1061
+ } else if (thirdValue) {
1062
+ bundle.projectId = thirdValue;
1063
+ }
1064
+ }
1065
+ if (values.length >= 4) {
1066
+ const fourthValue = stripWrappingQuotes(values[3] ?? "");
1067
+ if (!bundle.projectId) {
1068
+ bundle.projectId = fourthValue;
1069
+ } else if (!bundle.baseUrl) {
1070
+ bundle.baseUrl = fourthValue;
1071
+ }
1072
+ }
1073
+ return finalizeBundle(bundle);
1074
+ }
1075
+ function parseCsvContent(content) {
1076
+ const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
1077
+ if (lines.length === 0) {
1078
+ return {};
1079
+ }
1080
+ const firstRow = splitCsvLine(lines[0] ?? "");
1081
+ const firstHeaders = firstRow.map(normalizeFieldName);
1082
+ const looksLikeHeader = firstHeaders.some(
1083
+ (header) => [
1084
+ "apikey",
1085
+ "accesskey",
1086
+ "secretkey",
1087
+ "baseurl",
1088
+ "environment",
1089
+ "projectid",
1090
+ "agentid",
1091
+ "agentname"
1092
+ ].includes(header)
1093
+ );
1094
+ if (!looksLikeHeader) {
1095
+ return parseHeaderlessCsvRow(firstRow);
1096
+ }
1097
+ const dataRow = lines.slice(1).find((line) => line.trim().length > 0);
1098
+ if (!dataRow) {
1099
+ return {};
1100
+ }
1101
+ const values = splitCsvLine(dataRow);
1102
+ const bundle = {};
1103
+ for (const [index, header] of firstRow.entries()) {
1104
+ applyField(bundle, header, values[index] ?? "");
1105
+ }
1106
+ return finalizeBundle(bundle);
1107
+ }
1108
+ function parsePlainTextContent(content) {
1109
+ const trimmed = content.trim();
1110
+ if (!trimmed) {
1111
+ return {};
1112
+ }
1113
+ if (trimmed.includes(".") && !/\s/.test(trimmed)) {
1114
+ return { apiKey: trimmed };
1115
+ }
1116
+ const commaValues = splitCsvLine(trimmed);
1117
+ if (commaValues.length >= 2) {
1118
+ return parseHeaderlessCsvRow(commaValues);
1119
+ }
1120
+ return {};
1121
+ }
1122
+ function parseCredentialsFile(credentialsFilePath) {
1123
+ const resolvedPath = path4.resolve(credentialsFilePath);
1124
+ const content = fs3.readFileSync(resolvedPath, "utf8");
1125
+ const extension = path4.extname(resolvedPath).toLowerCase();
1126
+ if (extension === ".json") {
1127
+ return parseJsonContent(content);
1128
+ }
1129
+ if (extension === ".env") {
1130
+ return parseEnvContent(content);
1131
+ }
1132
+ if (extension === ".csv") {
1133
+ return parseCsvContent(content);
1134
+ }
1135
+ if (content.includes("=") && !content.trim().startsWith("{")) {
1136
+ return parseEnvContent(content);
1137
+ }
1138
+ if (content.includes(",") || content.includes("\n")) {
1139
+ const csvBundle = parseCsvContent(content);
1140
+ if (csvBundle.apiKey || csvBundle.accessKey || csvBundle.secretKey) {
1141
+ return csvBundle;
1142
+ }
1143
+ }
1144
+ if (content.trim().startsWith("{")) {
1145
+ return parseJsonContent(content);
1146
+ }
1147
+ return parsePlainTextContent(content);
1148
+ }
1149
+
1150
+ // src/commands/agent/init.ts
151
1151
  function prompt(question) {
152
1152
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
153
1153
  return new Promise((resolve) => {
@@ -157,82 +1157,309 @@ function prompt(question) {
157
1157
  });
158
1158
  });
159
1159
  }
160
- function loginCommand() {
161
- return new Command("login").description("Save your API key and runtime key paths to the config file").action(
162
- withErrorHandler(async (_opts, cmd) => {
163
- const globalOpts = cmd.optsWithGlobals();
164
- let apiKey = globalOpts.apiKey || process.env.R4_API_KEY;
165
- if (!apiKey) {
166
- apiKey = await prompt("Enter your R4 API key: ");
167
- }
168
- if (!apiKey) {
169
- throw new Error("No API key provided.");
170
- }
171
- if (!apiKey.includes(".")) {
172
- warn("API key format is usually {accessKey}.{secret}");
1160
+ function getAgentId(bundle) {
1161
+ const entryCount = bundle.transparency?.entries.length ?? 0;
1162
+ if (!bundle.transparency || entryCount === 0) {
1163
+ return void 0;
1164
+ }
1165
+ return bundle.transparency.entries[entryCount - 1]?.agentId;
1166
+ }
1167
+ function applyCredentialBundleToProfile(profile, bundle, globalOpts, privateKeyPath) {
1168
+ let nextProfile = { ...profile };
1169
+ if (!globalOpts.baseUrl && globalOpts.dev !== true) {
1170
+ if (bundle.baseUrl) {
1171
+ nextProfile.baseUrl = bundle.baseUrl;
1172
+ delete nextProfile.dev;
1173
+ } else if (bundle.dev === true) {
1174
+ nextProfile.dev = true;
1175
+ delete nextProfile.baseUrl;
1176
+ } else if (bundle.dev === false) {
1177
+ delete nextProfile.dev;
1178
+ }
1179
+ }
1180
+ if (!globalOpts.projectId && bundle.projectId) {
1181
+ nextProfile.projectId = bundle.projectId;
1182
+ }
1183
+ if (bundle.agentId) {
1184
+ nextProfile.agentId = bundle.agentId;
1185
+ }
1186
+ if (bundle.agentName) {
1187
+ nextProfile.agentName = bundle.agentName;
1188
+ }
1189
+ nextProfile.privateKeyPath = privateKeyPath;
1190
+ nextProfile = applyGlobalRuntimeOptionsToProfile(nextProfile, globalOpts);
1191
+ return nextProfile;
1192
+ }
1193
+ function initCommand() {
1194
+ return new Command2("init").description("Bootstrap local agent auth, key generation, public-key registration, and health checks").option(
1195
+ "--credentials-file <path>",
1196
+ "Read credentials from a CSV, .env, JSON, or plain-text handoff file"
1197
+ ).action(
1198
+ withErrorHandler(
1199
+ async (opts, cmd) => {
1200
+ const globalOpts = cmd.optsWithGlobals();
1201
+ const config = loadConfig();
1202
+ const profileName = getSelectedProfileName(
1203
+ config,
1204
+ globalOpts.profile,
1205
+ process.env.R4_PROFILE
1206
+ );
1207
+ const existingProfile = getProfileConfig(config, profileName);
1208
+ const bundle = opts.credentialsFile ? parseCredentialsFile(opts.credentialsFile) : {};
1209
+ let apiKey = globalOpts.apiKey || process.env.R4_API_KEY || buildApiKeyFromParts(
1210
+ process.env.R4_ACCESS_KEY,
1211
+ process.env.R4_SECRET_KEY
1212
+ ) || bundle.apiKey || existingProfile.apiKey;
1213
+ if (!apiKey) {
1214
+ apiKey = await prompt("Enter your R4 API key: ");
1215
+ }
1216
+ if (!apiKey) {
1217
+ throw new Error("No API key provided.");
1218
+ }
1219
+ if (!apiKey.includes(".")) {
1220
+ warn("API key format is usually {accessKey}.{secret}");
1221
+ }
1222
+ if (globalOpts.baseUrl && globalOpts.dev) {
1223
+ warn("--base-url takes precedence over --dev and will be saved as the active runtime URL.");
1224
+ }
1225
+ const privateKeyPath = resolvePrivateKeyPath({
1226
+ cliPath: globalOpts.privateKeyPath,
1227
+ envPath: process.env.R4_PRIVATE_KEY_PATH,
1228
+ profilePath: existingProfile.privateKeyPath,
1229
+ profileName,
1230
+ allowDefaultIfMissing: true
1231
+ }).value;
1232
+ if (!privateKeyPath) {
1233
+ throw new Error("Unable to resolve a local private-key path for agent bootstrap.");
1234
+ }
1235
+ const keySpinner = ora2("Ensuring a local RSA private key exists...").start();
1236
+ const { privateKeyPem, created } = ensurePrivateKey(privateKeyPath);
1237
+ keySpinner.stop();
1238
+ const publicKeyPem = derivePublicKey(privateKeyPem);
1239
+ const nextProfile = applyCredentialBundleToProfile(
1240
+ existingProfile,
1241
+ bundle,
1242
+ globalOpts,
1243
+ privateKeyPath
1244
+ );
1245
+ const runtimeMode = resolveRuntimeModeFromCli(globalOpts, nextProfile);
1246
+ const client = new CliClient(apiKey, runtimeMode.baseUrl);
1247
+ const registerSpinner = ora2("Registering the local public key...").start();
1248
+ const registration = await client.registerAgentPublicKey({
1249
+ publicKey: publicKeyPem
1250
+ });
1251
+ registerSpinner.stop();
1252
+ const savedProfile = {
1253
+ ...nextProfile,
1254
+ apiKey,
1255
+ agentId: getAgentId(registration) ?? nextProfile.agentId
1256
+ };
1257
+ const savedConfig = setCurrentProfile(
1258
+ updateProfile(config, profileName, savedProfile),
1259
+ profileName
1260
+ );
1261
+ saveConfig(savedConfig);
1262
+ success(
1263
+ created ? `Generated a private key at ${privateKeyPath}` : `Reused the existing private key at ${privateKeyPath}`
1264
+ );
1265
+ success(`Saved profile "${profileName}" to the local CLI config`);
1266
+ const doctorConnection = resolveConnection(
1267
+ { ...globalOpts, profile: profileName },
1268
+ { requireApiKey: true, requirePrivateKey: true }
1269
+ );
1270
+ const doctorSpinner = ora2("Running health checks...").start();
1271
+ const report = await runDoctorChecks(doctorConnection);
1272
+ doctorSpinner.stop();
1273
+ printDoctorReport(report);
1274
+ if (doctorHasFailures(report)) {
1275
+ throw new Error(
1276
+ "Agent init saved your local profile, but the health check still has failures."
1277
+ );
1278
+ }
1279
+ success(
1280
+ `Agent profile "${profileName}" is ready for ${runtimeMode.devMode ? "dev" : "prod"} use.`
1281
+ );
173
1282
  }
174
- if (globalOpts.baseUrl && globalOpts.dev) {
175
- warn("--base-url takes precedence over --dev and will be saved as the active runtime URL.");
1283
+ )
1284
+ );
1285
+ }
1286
+
1287
+ // src/commands/agent/index.ts
1288
+ function registerAgentCommands(program2) {
1289
+ const agent = program2.command("agent").description("Bootstrap and manage local agent runtime setup");
1290
+ agent.addCommand(initCommand());
1291
+ }
1292
+
1293
+ // src/commands/auth/diagnose.ts
1294
+ function diagnoseCommand() {
1295
+ return doctorCommand(
1296
+ "diagnose",
1297
+ "Verify auth, public-key registration, vault access, and zero-trust health"
1298
+ );
1299
+ }
1300
+
1301
+ // src/commands/auth/login.ts
1302
+ import { Command as Command3 } from "commander";
1303
+ import readline2 from "node:readline";
1304
+ function prompt2(question) {
1305
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
1306
+ return new Promise((resolve) => {
1307
+ rl.question(question, (answer) => {
1308
+ rl.close();
1309
+ resolve(answer.trim());
1310
+ });
1311
+ });
1312
+ }
1313
+ function applyCredentialBundleToProfile2(profile, bundle, globalOpts) {
1314
+ let nextProfile = { ...profile };
1315
+ if (!globalOpts.baseUrl && globalOpts.dev !== true) {
1316
+ if (bundle.baseUrl) {
1317
+ nextProfile.baseUrl = bundle.baseUrl;
1318
+ delete nextProfile.dev;
1319
+ } else if (bundle.dev === true) {
1320
+ nextProfile.dev = true;
1321
+ delete nextProfile.baseUrl;
1322
+ } else if (bundle.dev === false) {
1323
+ delete nextProfile.dev;
1324
+ }
1325
+ }
1326
+ if (!globalOpts.projectId && bundle.projectId) {
1327
+ nextProfile.projectId = bundle.projectId;
1328
+ }
1329
+ if (bundle.agentId) {
1330
+ nextProfile.agentId = bundle.agentId;
1331
+ }
1332
+ if (bundle.agentName) {
1333
+ nextProfile.agentName = bundle.agentName;
1334
+ }
1335
+ return applyGlobalRuntimeOptionsToProfile(nextProfile, globalOpts);
1336
+ }
1337
+ function loginCommand() {
1338
+ return new Command3("login").description("Save your API key and runtime settings to the active CLI profile").option(
1339
+ "--credentials-file <path>",
1340
+ "Read credentials from a CSV, .env, JSON, or plain-text handoff file"
1341
+ ).addHelpText(
1342
+ "after",
1343
+ `
1344
+ First run:
1345
+ r4 agent init --credentials-file ./agent-creds.csv --dev
1346
+
1347
+ That bootstrap flow can read the credentials bundle, generate a local RSA key if
1348
+ needed, register the public key with the machine API, save the profile, and run
1349
+ \`r4 doctor\`.
1350
+
1351
+ Manual save-only flow:
1352
+ r4 auth login --profile loom-dev --credentials-file ./agent-creds.csv --dev
1353
+ r4 doctor --profile loom-dev
1354
+ `
1355
+ ).action(
1356
+ withErrorHandler(
1357
+ async (opts, cmd) => {
1358
+ const globalOpts = cmd.optsWithGlobals();
1359
+ const config = loadConfig();
1360
+ const profileName = getSelectedProfileName(
1361
+ config,
1362
+ globalOpts.profile,
1363
+ process.env.R4_PROFILE
1364
+ );
1365
+ const existingProfile = getProfileConfig(config, profileName);
1366
+ const bundle = opts.credentialsFile ? parseCredentialsFile(opts.credentialsFile) : {};
1367
+ let apiKey = globalOpts.apiKey || process.env.R4_API_KEY || buildApiKeyFromParts(
1368
+ process.env.R4_ACCESS_KEY,
1369
+ process.env.R4_SECRET_KEY
1370
+ ) || bundle.apiKey || existingProfile.apiKey;
1371
+ if (!apiKey) {
1372
+ apiKey = await prompt2("Enter your R4 API key: ");
1373
+ }
1374
+ if (!apiKey) {
1375
+ throw new Error("No API key provided.");
1376
+ }
1377
+ if (!apiKey.includes(".")) {
1378
+ warn("API key format is usually {accessKey}.{secret}");
1379
+ }
1380
+ if (globalOpts.baseUrl && globalOpts.dev) {
1381
+ warn("--base-url takes precedence over --dev and will be saved as the active runtime URL.");
1382
+ }
1383
+ const nextProfile = applyCredentialBundleToProfile2(
1384
+ existingProfile,
1385
+ bundle,
1386
+ globalOpts
1387
+ );
1388
+ const savedConfig = setCurrentProfile(
1389
+ updateProfile(config, profileName, {
1390
+ ...nextProfile,
1391
+ apiKey
1392
+ }),
1393
+ profileName
1394
+ );
1395
+ saveConfig(savedConfig);
1396
+ success(`Saved profile "${profileName}" to ${getConfigPath()}`);
176
1397
  }
177
- const config = applyGlobalRuntimeOptionsToConfig(loadConfig(), globalOpts);
178
- config.apiKey = apiKey;
179
- saveConfig(config);
180
- success(`Runtime settings saved to ${getConfigPath()}`);
181
- })
1398
+ )
182
1399
  );
183
1400
  }
184
1401
 
185
1402
  // src/commands/auth/logout.ts
186
- import { Command as Command2 } from "commander";
1403
+ import { Command as Command4 } from "commander";
187
1404
  function logoutCommand() {
188
- return new Command2("logout").description("Remove saved API key from the config file").action(
189
- withErrorHandler(async () => {
190
- clearConfig();
191
- success(`Config removed from ${getConfigPath()}`);
1405
+ return new Command4("logout").description("Remove saved credentials from the active profile").option("--all", "Remove every saved profile and delete the config file").action(
1406
+ withErrorHandler(async (opts, cmd) => {
1407
+ if (opts.all) {
1408
+ clearConfig();
1409
+ success(`Removed all saved profiles from ${getConfigPath()}`);
1410
+ return;
1411
+ }
1412
+ const globalOpts = cmd.optsWithGlobals();
1413
+ const config = loadConfig();
1414
+ const profileName = getSelectedProfileName(
1415
+ config,
1416
+ globalOpts.profile,
1417
+ process.env.R4_PROFILE
1418
+ );
1419
+ if (!config.profiles[profileName]) {
1420
+ throw new Error(`Profile "${profileName}" does not exist.`);
1421
+ }
1422
+ if (Object.keys(config.profiles).length === 1) {
1423
+ clearConfig();
1424
+ } else {
1425
+ const nextConfig = removeProfile(config, profileName);
1426
+ saveConfig(nextConfig);
1427
+ }
1428
+ success(`Removed profile "${profileName}" from ${getConfigPath()}`);
192
1429
  })
193
1430
  );
194
1431
  }
195
1432
 
196
1433
  // src/commands/auth/status.ts
197
- import { Command as Command3 } from "commander";
198
- import chalk2 from "chalk";
1434
+ import { Command as Command5 } from "commander";
1435
+ import chalk3 from "chalk";
199
1436
  function maskKey(key) {
200
- if (key.length <= 8) return key;
201
- return key.substring(0, 8) + "...";
1437
+ if (key.length <= 8) {
1438
+ return key;
1439
+ }
1440
+ return `${key.substring(0, 8)}...`;
202
1441
  }
203
1442
  function statusCommand() {
204
- return new Command3("status").description("Show current authentication status").action(
1443
+ return new Command5("status").description("Show current authentication and profile status").action(
205
1444
  withErrorHandler(async (_opts, cmd) => {
206
1445
  const globalOpts = cmd.optsWithGlobals();
207
- const config = loadConfig();
208
- const runtimeMode = resolveRuntimeModeFromCli(globalOpts, config);
209
- let source;
210
- let apiKey;
211
- const privateKeyPath = globalOpts.privateKeyPath || process.env.R4_PRIVATE_KEY_PATH || config.privateKeyPath;
212
- const trustStorePath = globalOpts.trustStorePath || process.env.R4_TRUST_STORE_PATH || config.trustStorePath;
213
- if (globalOpts.apiKey) {
214
- source = "--api-key flag";
215
- apiKey = globalOpts.apiKey;
216
- } else if (process.env.R4_API_KEY) {
217
- source = "R4_API_KEY env var";
218
- apiKey = process.env.R4_API_KEY;
219
- } else if (config.apiKey) {
220
- source = `config file (${getConfigPath()})`;
221
- apiKey = config.apiKey;
222
- } else {
223
- source = "none";
224
- }
1446
+ const connection = resolveConnection(globalOpts);
225
1447
  if (globalOpts.json) {
226
1448
  console.log(
227
1449
  JSON.stringify(
228
1450
  {
229
- authenticated: !!apiKey,
230
- source,
231
- apiKey: apiKey ? maskKey(apiKey) : null,
232
- baseUrl: runtimeMode.baseUrl,
233
- devMode: runtimeMode.devMode,
234
- privateKeyPath: privateKeyPath || null,
235
- trustStorePath: trustStorePath || null
1451
+ authenticated: Boolean(connection.apiKey),
1452
+ profile: connection.profileName,
1453
+ source: connection.apiKeySource,
1454
+ apiKey: connection.apiKey ? maskKey(connection.apiKey) : null,
1455
+ agentId: connection.profile.agentId || null,
1456
+ agentName: connection.profile.agentName || null,
1457
+ baseUrl: connection.baseUrl,
1458
+ devMode: connection.dev,
1459
+ projectId: connection.projectId || null,
1460
+ privateKeyPath: connection.privateKeyPath || null,
1461
+ trustStorePath: connection.trustStorePath || null,
1462
+ configPath: getConfigPath()
236
1463
  },
237
1464
  null,
238
1465
  2
@@ -241,58 +1468,60 @@ function statusCommand() {
241
1468
  return;
242
1469
  }
243
1470
  console.log();
244
- if (apiKey) {
245
- console.log(chalk2.bold(" Authenticated"));
1471
+ if (!connection.apiKey) {
1472
+ console.log(chalk3.bold(" Not authenticated"));
246
1473
  console.log();
247
- printDetail(
248
- [
249
- ["Source", source],
250
- ["API Key", maskKey(apiKey)],
251
- ["Mode", runtimeMode.devMode ? "dev" : "prod"],
252
- ["Base URL", runtimeMode.baseUrl],
253
- ["Private Key", privateKeyPath || chalk2.dim("(not set)")],
254
- ["Trust Store", trustStorePath || chalk2.dim("(default beside key file)")]
255
- ],
256
- false
257
- );
258
- } else {
259
- console.log(chalk2.bold(" Not authenticated"));
1474
+ console.log(` Active profile: ${chalk3.cyan(connection.profileName)}`);
1475
+ console.log(" Run " + chalk3.cyan("r4 auth login") + " or " + chalk3.cyan("r4 agent init") + " to save credentials.");
260
1476
  console.log();
261
- console.log(" Run " + chalk2.cyan("r4 auth login") + " to save your API key.");
1477
+ return;
262
1478
  }
1479
+ console.log(chalk3.bold(" Authenticated"));
1480
+ console.log();
1481
+ printDetail(
1482
+ [
1483
+ ["Profile", connection.profileName],
1484
+ ["Source", connection.apiKeySource],
1485
+ ["API Key", maskKey(connection.apiKey)],
1486
+ ["Agent", connection.profile.agentName || connection.profile.agentId || chalk3.dim("(unknown)")],
1487
+ ["Mode", connection.dev ? "dev" : "prod"],
1488
+ ["Base URL", connection.baseUrl],
1489
+ ["Project ID", connection.projectId || chalk3.dim("(not set)")],
1490
+ ["Private Key", connection.privateKeyPath || chalk3.dim("(not set)")],
1491
+ ["Trust Store", connection.trustStorePath || chalk3.dim("(not set)")],
1492
+ ["Config File", getConfigPath()]
1493
+ ],
1494
+ false
1495
+ );
263
1496
  console.log();
264
1497
  })
265
1498
  );
266
1499
  }
267
1500
 
268
1501
  // src/commands/auth/whoami.ts
269
- import { Command as Command4 } from "commander";
270
- import chalk3 from "chalk";
271
- function maskKey2(key) {
272
- if (key.length <= 8) return key;
273
- return key.substring(0, 8) + "...";
274
- }
1502
+ import { Command as Command6 } from "commander";
1503
+ import chalk4 from "chalk";
275
1504
  function whoamiCommand() {
276
- return new Command4("whoami").description("Show current authenticated identity").action(
1505
+ return new Command6("whoami").description("Show the current profile, agent identity, and runtime target").action(
277
1506
  withErrorHandler(async (_opts, cmd) => {
278
1507
  const globalOpts = cmd.optsWithGlobals();
279
- const config = loadConfig();
280
- const runtimeMode = resolveRuntimeModeFromCli(globalOpts, config);
281
- const apiKey = globalOpts.apiKey || process.env.R4_API_KEY || config.apiKey;
282
- const projectId = globalOpts.projectId || process.env.R4_PROJECT_ID || config.projectId;
283
- const privateKeyPath = globalOpts.privateKeyPath || process.env.R4_PRIVATE_KEY_PATH || config.privateKeyPath;
284
- const trustStorePath = globalOpts.trustStorePath || process.env.R4_TRUST_STORE_PATH || config.trustStorePath;
1508
+ const connection = resolveConnection(globalOpts);
1509
+ const agentIdentity = connection.profile.agentName || connection.profile.agentId || null;
285
1510
  if (globalOpts.json) {
286
1511
  console.log(
287
1512
  JSON.stringify(
288
1513
  {
289
- authenticated: !!apiKey,
290
- apiKey: apiKey ? maskKey2(apiKey) : null,
291
- projectId: projectId || null,
292
- baseUrl: runtimeMode.baseUrl,
293
- devMode: runtimeMode.devMode,
294
- privateKeyPath: privateKeyPath || null,
295
- trustStorePath: trustStorePath || null,
1514
+ profile: connection.profileName,
1515
+ authenticated: Boolean(connection.apiKey),
1516
+ agent: {
1517
+ id: connection.profile.agentId || null,
1518
+ name: connection.profile.agentName || null
1519
+ },
1520
+ baseUrl: connection.baseUrl,
1521
+ devMode: connection.dev,
1522
+ projectId: connection.projectId || null,
1523
+ privateKeyPath: connection.privateKeyPath || null,
1524
+ trustStorePath: connection.trustStorePath || null,
296
1525
  configPath: getConfigPath()
297
1526
  },
298
1527
  null,
@@ -302,23 +1531,18 @@ function whoamiCommand() {
302
1531
  return;
303
1532
  }
304
1533
  console.log();
305
- if (!apiKey) {
306
- console.log(chalk3.bold(" Not authenticated"));
307
- console.log();
308
- console.log(" Run " + chalk3.cyan("r4 auth login") + " to save your API key.");
309
- console.log();
310
- return;
311
- }
312
- console.log(chalk3.bold(" Authenticated"));
1534
+ console.log(chalk4.bold(connection.apiKey ? " Current Identity" : " Current Profile"));
313
1535
  console.log();
314
1536
  printDetail(
315
1537
  [
316
- ["API Key", maskKey2(apiKey)],
317
- ["Project ID", projectId || chalk3.dim("(not set)")],
318
- ["Mode", runtimeMode.devMode ? "dev" : "prod"],
319
- ["Base URL", runtimeMode.baseUrl],
320
- ["Private Key", privateKeyPath || chalk3.dim("(not set)")],
321
- ["Trust Store", trustStorePath || chalk3.dim("(default beside key file)")],
1538
+ ["Profile", connection.profileName],
1539
+ ["Authenticated", connection.apiKey ? "yes" : "no"],
1540
+ ["Agent", agentIdentity || chalk4.dim("(unknown)")],
1541
+ ["Base URL", connection.baseUrl],
1542
+ ["Mode", connection.dev ? "dev" : "prod"],
1543
+ ["Project ID", connection.projectId || chalk4.dim("(not set)")],
1544
+ ["Private Key", connection.privateKeyPath || chalk4.dim("(not set)")],
1545
+ ["Trust Store", connection.trustStorePath || chalk4.dim("(not set)")],
322
1546
  ["Config File", getConfigPath()]
323
1547
  ],
324
1548
  false
@@ -335,49 +1559,79 @@ function registerAuthCommands(program2) {
335
1559
  auth.addCommand(logoutCommand());
336
1560
  auth.addCommand(statusCommand());
337
1561
  auth.addCommand(whoamiCommand());
1562
+ auth.addCommand(diagnoseCommand());
338
1563
  }
339
1564
 
340
- // src/commands/vault/list.ts
341
- import { Command as Command5 } from "commander";
342
- import ora from "ora";
343
- import R4 from "@r4security/sdk";
1565
+ // src/commands/profile/list.ts
1566
+ import { Command as Command7 } from "commander";
1567
+ function listCommand() {
1568
+ return new Command7("list").description("List saved CLI profiles").action(
1569
+ withErrorHandler(async (_opts, cmd) => {
1570
+ const globalOpts = cmd.optsWithGlobals();
1571
+ const config = loadConfig();
1572
+ const currentProfile = getCurrentProfileName(config);
1573
+ const rows = getProfileNames(config).map((profileName) => {
1574
+ const profile = config.profiles[profileName] ?? {};
1575
+ return [
1576
+ profileName,
1577
+ profileName === currentProfile ? "yes" : "",
1578
+ profile.baseUrl || (profile.dev ? "https://dev.r4.dev" : "(default)"),
1579
+ profile.projectId || "-",
1580
+ profile.agentId || profile.agentName || "-"
1581
+ ];
1582
+ });
1583
+ console.log();
1584
+ printTable(
1585
+ ["Name", "Current", "Base URL", "Project ID", "Agent"],
1586
+ rows,
1587
+ !!globalOpts.json,
1588
+ rows.map(([name, current, baseUrl, projectId, agent]) => ({
1589
+ name,
1590
+ current: current === "yes",
1591
+ baseUrl,
1592
+ projectId: projectId === "-" ? null : projectId,
1593
+ agent: agent === "-" ? null : agent
1594
+ }))
1595
+ );
1596
+ console.log();
1597
+ })
1598
+ );
1599
+ }
344
1600
 
345
- // src/lib/resolve-auth.ts
346
- function resolveAuth(opts) {
347
- const config = loadConfig();
348
- const runtimeMode = resolveRuntimeModeFromCli(opts, config);
349
- const apiKey = opts.apiKey || process.env.R4_API_KEY || config.apiKey;
350
- if (!apiKey) {
351
- throw new Error(
352
- "No API key found. Provide one via:\n --api-key <key> CLI flag\n R4_API_KEY environment variable\n r4 auth login save to config file"
353
- );
354
- }
355
- const privateKeyPath = opts.privateKeyPath || process.env.R4_PRIVATE_KEY_PATH || config.privateKeyPath;
356
- if (!privateKeyPath) {
357
- throw new Error(
358
- "No private key path found. Provide one via:\n --private-key-path <path> CLI flag\n R4_PRIVATE_KEY_PATH environment variable\n ~/.r4/config.json config file (privateKeyPath field)"
359
- );
360
- }
361
- const projectId = opts.projectId || process.env.R4_PROJECT_ID || config.projectId;
362
- const trustStorePath = opts.trustStorePath || process.env.R4_TRUST_STORE_PATH || config.trustStorePath;
363
- return {
364
- apiKey,
365
- projectId,
366
- baseUrl: runtimeMode.baseUrl,
367
- dev: runtimeMode.devMode,
368
- privateKeyPath,
369
- trustStorePath
370
- };
1601
+ // src/commands/profile/use.ts
1602
+ import { Command as Command8 } from "commander";
1603
+ function useCommand() {
1604
+ return new Command8("use").description("Switch the active CLI profile").argument("<name>", "Profile name").action(
1605
+ withErrorHandler(async (profileName) => {
1606
+ const config = loadConfig();
1607
+ const profile = getProfileConfig(config, profileName);
1608
+ if (Object.keys(profile).length === 0 && !config.profiles[profileName]) {
1609
+ throw new Error(`Profile "${profileName}" does not exist.`);
1610
+ }
1611
+ saveConfig(setCurrentProfile(config, profileName));
1612
+ success(`Now using profile "${profileName}"`);
1613
+ })
1614
+ );
1615
+ }
1616
+
1617
+ // src/commands/profile/index.ts
1618
+ function registerProfileCommands(program2) {
1619
+ const profile = program2.command("profile").description("Manage saved CLI profiles");
1620
+ profile.addCommand(listCommand());
1621
+ profile.addCommand(useCommand());
371
1622
  }
372
1623
 
373
1624
  // src/commands/vault/list.ts
374
- function listCommand() {
375
- return new Command5("list").description("List all locally decrypted environment variables").action(
1625
+ import { Command as Command9 } from "commander";
1626
+ import ora3 from "ora";
1627
+ import R42 from "@r4security/sdk";
1628
+ function listCommand2() {
1629
+ return new Command9("list").description("List all locally decrypted environment variables").action(
376
1630
  withErrorHandler(async (_opts, cmd) => {
377
1631
  const globalOpts = cmd.optsWithGlobals();
378
1632
  const config = resolveAuth(globalOpts);
379
- const spinner = ora("Fetching environment variables...").start();
380
- const r4 = await R4.create(config);
1633
+ const spinner = ora3("Fetching environment variables...").start();
1634
+ const r4 = await R42.create(config);
381
1635
  spinner.stop();
382
1636
  const env = r4.env;
383
1637
  const keys = Object.keys(env).sort();
@@ -397,39 +1651,12 @@ function listCommand() {
397
1651
  );
398
1652
  }
399
1653
 
400
- // src/commands/vault/get.ts
401
- import { Command as Command6 } from "commander";
402
- import ora2 from "ora";
403
- import R42 from "@r4security/sdk";
404
- function getCommand() {
405
- return new Command6("get").description("Get a specific locally decrypted environment variable value").argument("<key>", "Environment variable key (SCREAMING_SNAKE_CASE)").action(
406
- withErrorHandler(
407
- async (keyArg, _opts, cmd) => {
408
- const globalOpts = cmd.optsWithGlobals();
409
- const config = resolveAuth(globalOpts);
410
- const spinner = ora2("Fetching environment variables...").start();
411
- const r4 = await R42.create(config);
412
- spinner.stop();
413
- const env = r4.env;
414
- const key = keyArg.toUpperCase();
415
- const value = env[key];
416
- if (value === void 0) {
417
- const available = Object.keys(env).sort().join(", ") || "(none)";
418
- throw new Error(`Key "${key}" not found. Available keys: ${available}`);
419
- }
420
- if (globalOpts.json) {
421
- console.log(JSON.stringify({ key, value }, null, 2));
422
- return;
423
- }
424
- process.stdout.write(value);
425
- }
426
- )
427
- );
428
- }
1654
+ // src/commands/vault/list-items.ts
1655
+ import { Command as Command11 } from "commander";
429
1656
 
430
1657
  // src/commands/vault/items.ts
431
- import { Command as Command7 } from "commander";
432
- import ora3 from "ora";
1658
+ import { Command as Command10 } from "commander";
1659
+ import ora4 from "ora";
433
1660
  import R43 from "@r4security/sdk";
434
1661
  function deriveItems(env) {
435
1662
  const keys = Object.keys(env).sort();
@@ -438,12 +1665,71 @@ function deriveItems(env) {
438
1665
  fields: [{ name: key, key }]
439
1666
  }));
440
1667
  }
1668
+ async function loadVaultMetadataRows(globalOpts) {
1669
+ const connection = resolveConnection(globalOpts, { requireApiKey: true });
1670
+ const client = new CliClient(connection.apiKey, connection.baseUrl);
1671
+ const { vaults } = await client.listVaults(connection.projectId);
1672
+ const rows = [];
1673
+ for (const vault of vaults) {
1674
+ const response = await client.listVaultItems(vault.id);
1675
+ const groupNames = Object.fromEntries(
1676
+ response.vaultItemGroups.map((group) => [group.id, group.name])
1677
+ );
1678
+ for (const item of response.items) {
1679
+ rows.push({
1680
+ vaultId: vault.id,
1681
+ vaultName: vault.name,
1682
+ itemId: item.id,
1683
+ itemName: item.name,
1684
+ type: item.type,
1685
+ fieldCount: item.fieldCount,
1686
+ groupName: item.groupId ? groupNames[item.groupId] ?? item.groupId : null,
1687
+ websites: item.websites
1688
+ });
1689
+ }
1690
+ }
1691
+ return rows.sort((left, right) => {
1692
+ const vaultCompare = left.vaultName.localeCompare(right.vaultName);
1693
+ return vaultCompare !== 0 ? vaultCompare : left.itemName.localeCompare(right.itemName);
1694
+ });
1695
+ }
1696
+ async function renderVaultMetadataRows(globalOpts) {
1697
+ const spinner = ora4("Fetching vault item metadata...").start();
1698
+ const rows = await loadVaultMetadataRows(globalOpts);
1699
+ spinner.stop();
1700
+ if (globalOpts.json) {
1701
+ console.log(JSON.stringify(rows, null, 2));
1702
+ return;
1703
+ }
1704
+ if (rows.length === 0) {
1705
+ console.log("\n No vault items found.\n");
1706
+ return;
1707
+ }
1708
+ console.log();
1709
+ printTable(
1710
+ ["Vault", "Item", "Type", "Fields", "Group", "Websites"],
1711
+ rows.map((row) => [
1712
+ row.vaultName,
1713
+ row.itemName,
1714
+ row.type || "-",
1715
+ String(row.fieldCount),
1716
+ row.groupName || "-",
1717
+ row.websites.join(", ") || "-"
1718
+ ]),
1719
+ false
1720
+ );
1721
+ console.log();
1722
+ }
441
1723
  function itemsCommand() {
442
- return new Command7("items").description("List all vault items represented in the locally decrypted env map").action(
443
- withErrorHandler(async (_opts, cmd) => {
1724
+ return new Command10("items").description("List vault items from the decrypted env map, or use --metadata-only for raw machine metadata").option("--metadata-only", "List item metadata without local decryption").action(
1725
+ withErrorHandler(async (opts, cmd) => {
444
1726
  const globalOpts = cmd.optsWithGlobals();
1727
+ if (opts.metadataOnly) {
1728
+ await renderVaultMetadataRows(globalOpts);
1729
+ return;
1730
+ }
445
1731
  const config = resolveAuth(globalOpts);
446
- const spinner = ora3("Fetching vault items...").start();
1732
+ const spinner = ora4("Fetching vault items...").start();
447
1733
  const r4 = await R43.create(config);
448
1734
  spinner.stop();
449
1735
  const env = r4.env;
@@ -468,18 +1754,95 @@ function itemsCommand() {
468
1754
  );
469
1755
  }
470
1756
 
471
- // src/commands/vault/search.ts
472
- import { Command as Command8 } from "commander";
473
- import ora4 from "ora";
1757
+ // src/commands/vault/list-items.ts
1758
+ function listItemsCommand() {
1759
+ return new Command11("list-items").description("List vault item metadata only, without requiring local decryption").action(
1760
+ withErrorHandler(async (_opts, cmd) => {
1761
+ const globalOpts = cmd.optsWithGlobals();
1762
+ await renderVaultMetadataRows(globalOpts);
1763
+ })
1764
+ );
1765
+ }
1766
+
1767
+ // src/commands/vault/list-vaults.ts
1768
+ import { Command as Command12 } from "commander";
1769
+ import ora5 from "ora";
1770
+ function listVaultsCommand() {
1771
+ return new Command12("list-vaults").description("List visible vaults without requiring local decryption").action(
1772
+ withErrorHandler(async (_opts, cmd) => {
1773
+ const globalOpts = cmd.optsWithGlobals();
1774
+ const connection = resolveConnection(globalOpts, { requireApiKey: true });
1775
+ const client = new CliClient(connection.apiKey, connection.baseUrl);
1776
+ const spinner = ora5("Fetching visible vaults...").start();
1777
+ const response = await client.listVaults(connection.projectId);
1778
+ spinner.stop();
1779
+ if (globalOpts.json) {
1780
+ console.log(JSON.stringify(response.vaults, null, 2));
1781
+ return;
1782
+ }
1783
+ if (response.vaults.length === 0) {
1784
+ console.log("\n No visible vaults found.\n");
1785
+ return;
1786
+ }
1787
+ console.log();
1788
+ printTable(
1789
+ ["ID", "Name", "Classification", "Items", "Created At"],
1790
+ response.vaults.map((vault) => [
1791
+ vault.id,
1792
+ vault.name,
1793
+ vault.dataClassification || "-",
1794
+ String(vault.itemCount),
1795
+ vault.createdAt
1796
+ ]),
1797
+ false
1798
+ );
1799
+ console.log();
1800
+ })
1801
+ );
1802
+ }
1803
+
1804
+ // src/commands/vault/get.ts
1805
+ import { Command as Command13 } from "commander";
1806
+ import ora6 from "ora";
474
1807
  import R44 from "@r4security/sdk";
1808
+ function getCommand() {
1809
+ return new Command13("get").description("Get a specific locally decrypted environment variable value").argument("<key>", "Environment variable key (SCREAMING_SNAKE_CASE)").action(
1810
+ withErrorHandler(
1811
+ async (keyArg, _opts, cmd) => {
1812
+ const globalOpts = cmd.optsWithGlobals();
1813
+ const config = resolveAuth(globalOpts);
1814
+ const spinner = ora6("Fetching environment variables...").start();
1815
+ const r4 = await R44.create(config);
1816
+ spinner.stop();
1817
+ const env = r4.env;
1818
+ const key = keyArg.toUpperCase();
1819
+ const value = env[key];
1820
+ if (value === void 0) {
1821
+ const available = Object.keys(env).sort().join(", ") || "(none)";
1822
+ throw new Error(`Key "${key}" not found. Available keys: ${available}`);
1823
+ }
1824
+ if (globalOpts.json) {
1825
+ console.log(JSON.stringify({ key, value }, null, 2));
1826
+ return;
1827
+ }
1828
+ process.stdout.write(value);
1829
+ }
1830
+ )
1831
+ );
1832
+ }
1833
+
1834
+ // src/commands/vault/search.ts
1835
+ import { Command as Command14 } from "commander";
1836
+ import ora7 from "ora";
1837
+ import R45 from "@r4security/sdk";
475
1838
  function searchCommand() {
476
- return new Command8("search").description("Search vault items by name").argument("<query>", "Search query (case-insensitive match against key names)").action(
1839
+ return new Command14("search").description("Search vault items by name").argument("<query>", "Search query (case-insensitive match against key names)").action(
477
1840
  withErrorHandler(
478
1841
  async (query, _opts, cmd) => {
479
1842
  const globalOpts = cmd.optsWithGlobals();
480
1843
  const config = resolveAuth(globalOpts);
481
- const spinner = ora4("Searching vault items...").start();
482
- const r4 = await R44.create(config);
1844
+ const spinner = ora7("Searching vault items...").start();
1845
+ const r4 = await R45.create(config);
483
1846
  spinner.stop();
484
1847
  const env = r4.env;
485
1848
  const lowerQuery = query.toLowerCase();
@@ -507,68 +1870,24 @@ function searchCommand() {
507
1870
  // src/commands/vault/index.ts
508
1871
  function registerVaultCommands(program2) {
509
1872
  const vault = program2.command("vault").description("Manage vault secrets");
510
- vault.addCommand(listCommand());
1873
+ vault.addCommand(listCommand2());
1874
+ vault.addCommand(listVaultsCommand());
1875
+ vault.addCommand(listItemsCommand());
511
1876
  vault.addCommand(getCommand());
512
1877
  vault.addCommand(itemsCommand());
513
1878
  vault.addCommand(searchCommand());
514
1879
  }
515
1880
 
516
1881
  // src/commands/project/list.ts
517
- import { Command as Command9 } from "commander";
518
- import ora5 from "ora";
519
-
520
- // src/lib/client.ts
521
- var CliClient = class {
522
- apiKey;
523
- baseUrl;
524
- constructor(apiKey, baseUrl) {
525
- this.apiKey = apiKey;
526
- this.baseUrl = baseUrl.replace(/\/$/, "");
527
- }
528
- /**
529
- * Make an authenticated request to the machine API.
530
- * Handles error responses consistently with the SDK pattern.
531
- */
532
- async request(method, path2, body) {
533
- const url = `${this.baseUrl}${path2}`;
534
- const response = await fetch(url, {
535
- method,
536
- headers: {
537
- "X-API-Key": this.apiKey,
538
- "Content-Type": "application/json"
539
- },
540
- body: body ? JSON.stringify(body) : void 0
541
- });
542
- if (!response.ok) {
543
- const errorBody = await response.json().catch(() => ({}));
544
- const error = errorBody?.error;
545
- const errorMessage = error?.message || `HTTP ${response.status}: ${response.statusText}`;
546
- throw new Error(`R4 API Error: ${errorMessage}`);
547
- }
548
- return response.json();
549
- }
550
- /** List all projects. GET /api/v1/machine/project */
551
- async listProjects() {
552
- return this.request("GET", "/api/v1/machine/project");
553
- }
554
- /** Get project details. GET /api/v1/machine/project/:id */
555
- async getProject(id) {
556
- return this.request("GET", `/api/v1/machine/project/${id}`);
557
- }
558
- /** Create a new project. POST /api/v1/machine/project */
559
- async createProject(data) {
560
- return this.request("POST", "/api/v1/machine/project", data);
561
- }
562
- };
563
-
564
- // src/commands/project/list.ts
565
- function listCommand2() {
566
- return new Command9("list").description("List all projects").action(
1882
+ import { Command as Command15 } from "commander";
1883
+ import ora8 from "ora";
1884
+ function listCommand3() {
1885
+ return new Command15("list").description("List all projects").action(
567
1886
  withErrorHandler(async (_opts, cmd) => {
568
1887
  const globalOpts = cmd.optsWithGlobals();
569
- const config = resolveAuth(globalOpts);
570
- const client = new CliClient(config.apiKey, config.baseUrl || "https://r4.dev");
571
- const spinner = ora5("Fetching projects...").start();
1888
+ const connection = resolveConnection(globalOpts, { requireApiKey: true });
1889
+ const client = new CliClient(connection.apiKey, connection.baseUrl);
1890
+ const spinner = ora8("Fetching projects...").start();
572
1891
  const response = await client.listProjects();
573
1892
  spinner.stop();
574
1893
  const rows = response.projects.map((p) => [
@@ -592,16 +1911,16 @@ function listCommand2() {
592
1911
  }
593
1912
 
594
1913
  // src/commands/project/get.ts
595
- import { Command as Command10 } from "commander";
596
- import chalk4 from "chalk";
597
- import ora6 from "ora";
1914
+ import { Command as Command16 } from "commander";
1915
+ import chalk5 from "chalk";
1916
+ import ora9 from "ora";
598
1917
  function getCommand2() {
599
- return new Command10("get").description("Get project details").argument("<id>", "Project ID").action(
1918
+ return new Command16("get").description("Get project details").argument("<id>", "Project ID").action(
600
1919
  withErrorHandler(async (id, _opts, cmd) => {
601
1920
  const globalOpts = cmd.optsWithGlobals();
602
- const config = resolveAuth(globalOpts);
603
- const client = new CliClient(config.apiKey, config.baseUrl || "https://r4.dev");
604
- const spinner = ora6("Fetching project...").start();
1921
+ const connection = resolveConnection(globalOpts, { requireApiKey: true });
1922
+ const client = new CliClient(connection.apiKey, connection.baseUrl);
1923
+ const spinner = ora9("Fetching project...").start();
605
1924
  const project = await client.getProject(id);
606
1925
  spinner.stop();
607
1926
  if (globalOpts.json) {
@@ -609,7 +1928,7 @@ function getCommand2() {
609
1928
  return;
610
1929
  }
611
1930
  console.log();
612
- console.log(chalk4.bold(` ${project.name}`));
1931
+ console.log(chalk5.bold(` ${project.name}`));
613
1932
  console.log();
614
1933
  printDetail(
615
1934
  [
@@ -625,7 +1944,7 @@ function getCommand2() {
625
1944
  );
626
1945
  if (project.vaults.length > 0) {
627
1946
  console.log();
628
- console.log(chalk4.bold(" Vaults"));
1947
+ console.log(chalk5.bold(" Vaults"));
629
1948
  console.log();
630
1949
  printTable(
631
1950
  ["ID", "Name", "Encrypted"],
@@ -635,7 +1954,7 @@ function getCommand2() {
635
1954
  }
636
1955
  if (project.licenses.length > 0) {
637
1956
  console.log();
638
- console.log(chalk4.bold(" Licenses"));
1957
+ console.log(chalk5.bold(" Licenses"));
639
1958
  console.log();
640
1959
  printTable(
641
1960
  ["ID", "Name", "Type"],
@@ -645,7 +1964,7 @@ function getCommand2() {
645
1964
  }
646
1965
  if (project.licenseGroups.length > 0) {
647
1966
  console.log();
648
- console.log(chalk4.bold(" License Groups"));
1967
+ console.log(chalk5.bold(" License Groups"));
649
1968
  console.log();
650
1969
  printTable(
651
1970
  ["ID", "Name"],
@@ -659,11 +1978,11 @@ function getCommand2() {
659
1978
  }
660
1979
 
661
1980
  // src/commands/project/create.ts
662
- import { Command as Command11 } from "commander";
663
- import readline2 from "node:readline";
664
- import ora7 from "ora";
665
- function prompt2(question) {
666
- const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
1981
+ import { Command as Command17 } from "commander";
1982
+ import readline3 from "node:readline";
1983
+ import ora10 from "ora";
1984
+ function prompt3(question) {
1985
+ const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
667
1986
  return new Promise((resolve) => {
668
1987
  rl.question(question, (answer) => {
669
1988
  rl.close();
@@ -672,19 +1991,19 @@ function prompt2(question) {
672
1991
  });
673
1992
  }
674
1993
  function createCommand() {
675
- return new Command11("create").description("Create a new project").option("--name <name>", "Project name").option("--description <description>", "Project description").option("--external-id <externalId>", "External identifier").action(
1994
+ return new Command17("create").description("Create a new project").option("--name <name>", "Project name").option("--description <description>", "Project description").option("--external-id <externalId>", "External identifier").action(
676
1995
  withErrorHandler(async (opts, cmd) => {
677
1996
  const globalOpts = cmd.optsWithGlobals();
678
- const config = resolveAuth(globalOpts);
679
- const client = new CliClient(config.apiKey, config.baseUrl || "https://r4.dev");
1997
+ const connection = resolveConnection(globalOpts, { requireApiKey: true });
1998
+ const client = new CliClient(connection.apiKey, connection.baseUrl);
680
1999
  let name = opts.name;
681
2000
  if (!name) {
682
- name = await prompt2("Project name: ");
2001
+ name = await prompt3("Project name: ");
683
2002
  }
684
2003
  if (!name) {
685
2004
  throw new Error("Project name is required.");
686
2005
  }
687
- const spinner = ora7("Creating project...").start();
2006
+ const spinner = ora10("Creating project...").start();
688
2007
  const response = await client.createProject({
689
2008
  name,
690
2009
  description: opts.description,
@@ -703,23 +2022,23 @@ function createCommand() {
703
2022
  // src/commands/project/index.ts
704
2023
  function registerProjectCommands(program2) {
705
2024
  const project = program2.command("project").description("Manage projects");
706
- project.addCommand(listCommand2());
2025
+ project.addCommand(listCommand3());
707
2026
  project.addCommand(getCommand2());
708
2027
  project.addCommand(createCommand());
709
2028
  }
710
2029
 
711
2030
  // src/commands/run/index.ts
712
2031
  import { spawn } from "node:child_process";
713
- import ora8 from "ora";
714
- import R45 from "@r4security/sdk";
2032
+ import ora11 from "ora";
2033
+ import R46 from "@r4security/sdk";
715
2034
  function registerRunCommand(program2) {
716
2035
  program2.command("run").description("Run a command with vault secrets injected as environment variables").argument("<command...>", "Command and arguments to execute").option("--prefix <prefix>", "Add prefix to all injected env var names").action(
717
2036
  withErrorHandler(
718
2037
  async (commandParts, opts, cmd) => {
719
2038
  const globalOpts = cmd.optsWithGlobals();
720
2039
  const config = resolveAuth(globalOpts);
721
- const spinner = ora8("Loading vault secrets...").start();
722
- const r4 = await R45.create(config);
2040
+ const spinner = ora11("Loading vault secrets...").start();
2041
+ const r4 = await R46.create(config);
723
2042
  spinner.stop();
724
2043
  const env = r4.env;
725
2044
  const secretEnv = {};
@@ -745,11 +2064,14 @@ function registerRunCommand(program2) {
745
2064
  }
746
2065
 
747
2066
  // src/index.ts
748
- var program = new Command12();
749
- program.name("r4").description("R4 CLI \u2014 manage vaults, projects, and secrets from the terminal").version("0.0.2").option("--api-key <key>", "API key (overrides R4_API_KEY env var and config file)").option("--project-id <id>", "Optional project ID filter (overrides R4_PROJECT_ID env var and config file)").option("--dev", "Use https://dev.r4.dev unless an explicit base URL override is set").option("--base-url <url>", "API base URL (default: https://r4.dev)").option("--private-key-path <path>", "Path to the agent private key PEM (overrides R4_PRIVATE_KEY_PATH env var and config file)").option("--trust-store-path <path>", "Path to the local signer trust-store JSON (overrides R4_TRUST_STORE_PATH env var and config file)").option("--json", "Output as JSON for scripting and piping", false);
2067
+ var program = new Command18();
2068
+ program.name("r4").description("R4 CLI \u2014 manage vaults, projects, and secrets from the terminal").version("0.0.5").option("--api-key <key>", "API key (overrides R4_API_KEY env var and config file)").option("--profile <name>", "CLI profile name (overrides R4_PROFILE and the saved current profile)").option("--project-id <id>", "Optional project ID filter (overrides R4_PROJECT_ID env var and config file)").option("--dev", "Use https://dev.r4.dev unless an explicit base URL override is set").option("--base-url <url>", "API base URL (default: https://r4.dev)").option("--private-key-path <path>", "Path to the agent private key PEM (overrides R4_PRIVATE_KEY_PATH env var and config file)").option("--trust-store-path <path>", "Path to the local signer trust-store JSON (overrides R4_TRUST_STORE_PATH env var and config file)").option("--json", "Output as JSON for scripting and piping", false);
2069
+ registerAgentCommands(program);
750
2070
  registerAuthCommands(program);
2071
+ registerProfileCommands(program);
751
2072
  registerVaultCommands(program);
752
2073
  registerProjectCommands(program);
753
2074
  registerRunCommand(program);
2075
+ program.addCommand(doctorCommand());
754
2076
  program.parse();
755
2077
  //# sourceMappingURL=index.js.map