@arbiterhq/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-06-28
4
+
5
+ First public npm release under **`@arbiterhq/cli`**. The CLI binary remains **`arbiter`**; only the npm scope changed from the previously planned `@arbiter` org.
6
+
7
+ ### Added
8
+
9
+ - `arbiter login` — save API credentials locally
10
+ - `arbiter init` — create `arbiter.yaml` (optional `--from-remote`)
11
+ - `arbiter validate` — local manifest validation
12
+ - `arbiter plan` — manifest diff against remote workspace
13
+ - `arbiter apply` — sync manifest (`--yes` for CI)
14
+ - `arbiter drift` — workspace drift and adoption suggestions
15
+ - `arbiter status` — endpoint, manifest version, state signature
16
+ - `arbiter whoami` — credential session context
17
+
18
+ ### Dependencies
19
+
20
+ - Requires `@arbiterhq/sdk@^0.2.0` (publish SDK before CLI)
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # @arbiterhq/cli
2
+
3
+ Official `arbiter` command line interface. Stateless wrapper over [`@arbiterhq/sdk`](../sdk).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @arbiterhq/cli
9
+ ```
10
+
11
+ Requires Node.js 18+. The CLI depends on `@arbiterhq/sdk` from npm — install the SDK first if you use it directly in agent code:
12
+
13
+ ```bash
14
+ npm install @arbiterhq/sdk
15
+ ```
16
+
17
+ ### Local development (monorepo contributors)
18
+
19
+ ```bash
20
+ cd packages/cli
21
+ npm install
22
+ npm run build
23
+ npm link
24
+ ```
25
+
26
+ ## Authentication
27
+
28
+ ```bash
29
+ arbiter login
30
+ # or
31
+ export ARBITER_API_KEY=your_key
32
+ export ARBITER_BASE_URL=https://api.arbitertrust.com
33
+ ```
34
+
35
+ Credentials are stored at `~/.config/arbiter/credentials.json`. `ARBITER_API_KEY` takes precedence over the file.
36
+
37
+ ### Base URL resolution
38
+
39
+ 1. `ARBITER_BASE_URL` environment variable
40
+ 2. `baseUrl` saved in credentials file (set via `arbiter login --base-url`)
41
+ 3. Production default: `https://api.arbitertrust.com`
42
+
43
+ **Local development:** `export ARBITER_BASE_URL=http://localhost:3001` or `arbiter login --base-url http://localhost:3001`
44
+
45
+ `login` performs a local filesystem write only — no network validation.
46
+
47
+ ## Commands
48
+
49
+ | Command | Network | Description |
50
+ |---------|---------|-------------|
51
+ | `arbiter login` | No | Save credentials locally |
52
+ | `arbiter init` | Optional (`--from-remote`) | Create `arbiter.yaml` |
53
+ | `arbiter validate` | No | Local structural validation |
54
+ | `arbiter plan` | Yes | Diff manifest vs remote |
55
+ | `arbiter apply` | Yes | Sync manifest to remote |
56
+ | `arbiter apply --yes` | Yes | Non-interactive apply for CI |
57
+ | `arbiter drift` | Yes | Drift + adoption suggestions |
58
+ | `arbiter status` | Yes | Endpoint, manifest version, signature |
59
+ | `arbiter whoami` | No | Session context |
60
+
61
+ ## Exit codes
62
+
63
+ | Code | `plan` / `drift` | Other commands |
64
+ |------|------------------|----------------|
65
+ | `0` | In sync / success | Success |
66
+ | `1` | Error | Error |
67
+ | `2` | Drift or pending changes | — |
68
+
69
+ ## CI example
70
+
71
+ ```bash
72
+ arbiter validate
73
+ arbiter plan
74
+ arbiter apply --yes
75
+ ```
76
+
77
+ ## Options
78
+
79
+ Global manifest path override:
80
+
81
+ ```bash
82
+ arbiter plan --manifest ./config/arbiter.yaml
83
+ ```
84
+
85
+ Disable colors in pipelines:
86
+
87
+ ```bash
88
+ NO_COLOR=1 arbiter plan
89
+ ```
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,992 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/apply.ts
7
+ import readline from "readline/promises";
8
+ import { stdin as input, stdout as output } from "process";
9
+ import { ArbiterError } from "@arbiterhq/sdk";
10
+
11
+ // src/config/client.ts
12
+ import { ArbiterAdmin } from "@arbiterhq/sdk";
13
+
14
+ // src/config/credentials.ts
15
+ import fs from "fs";
16
+ import path2 from "path";
17
+
18
+ // src/config/paths.ts
19
+ import os from "os";
20
+ import path from "path";
21
+ var CREDENTIALS_DIR = path.join(os.homedir(), ".config", "arbiter");
22
+ var CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");
23
+ var DEFAULT_MANIFEST_FILE = "arbiter.yaml";
24
+ var DEFAULT_BASE_URL = "https://api.arbitertrust.com";
25
+
26
+ // src/config/credentials.ts
27
+ function loadCredentialsFile() {
28
+ if (!fs.existsSync(CREDENTIALS_FILE)) {
29
+ return null;
30
+ }
31
+ const raw = fs.readFileSync(CREDENTIALS_FILE, "utf8");
32
+ const parsed = JSON.parse(raw);
33
+ if (typeof parsed.apiKey !== "string" || parsed.apiKey.length === 0) {
34
+ throw new Error(`Invalid credentials file at ${CREDENTIALS_FILE}: apiKey is required`);
35
+ }
36
+ return parsed;
37
+ }
38
+ function saveCredentialsFile(credentials) {
39
+ fs.mkdirSync(CREDENTIALS_DIR, { recursive: true });
40
+ const payload = {
41
+ apiKey: credentials.apiKey,
42
+ baseUrl: credentials.baseUrl ?? DEFAULT_BASE_URL
43
+ };
44
+ if (credentials.workspace !== void 0) {
45
+ payload.workspace = credentials.workspace;
46
+ }
47
+ if (credentials.environment !== void 0) {
48
+ payload.environment = credentials.environment;
49
+ }
50
+ fs.writeFileSync(CREDENTIALS_FILE, `${JSON.stringify(payload, null, 2)}
51
+ `, "utf8");
52
+ }
53
+ function resolveSession() {
54
+ const envApiKey = process.env.ARBITER_API_KEY;
55
+ const fileCredentials = loadCredentialsFile();
56
+ const apiKey = envApiKey ?? fileCredentials?.apiKey;
57
+ if (!apiKey) {
58
+ throw new Error(
59
+ "No API key found. Run `arbiter login` or set ARBITER_API_KEY."
60
+ );
61
+ }
62
+ const baseUrl = process.env.ARBITER_BASE_URL ?? fileCredentials?.baseUrl ?? DEFAULT_BASE_URL;
63
+ let source = "file";
64
+ if (envApiKey && fileCredentials?.apiKey) {
65
+ source = "env+file";
66
+ } else if (envApiKey) {
67
+ source = "env";
68
+ }
69
+ return {
70
+ apiKey,
71
+ baseUrl,
72
+ workspace: fileCredentials?.workspace ?? null,
73
+ environment: fileCredentials?.environment ?? null,
74
+ source
75
+ };
76
+ }
77
+ function maskApiKey(apiKey) {
78
+ if (apiKey.length <= 8) {
79
+ return "********";
80
+ }
81
+ return `${apiKey.slice(0, 4)}...${apiKey.slice(-4)}`;
82
+ }
83
+ function credentialsFilePath() {
84
+ return path2.normalize(CREDENTIALS_FILE);
85
+ }
86
+
87
+ // src/config/client.ts
88
+ function createArbiterClient() {
89
+ const session = resolveSession();
90
+ return new ArbiterAdmin({
91
+ apiKey: session.apiKey,
92
+ baseUrl: session.baseUrl
93
+ });
94
+ }
95
+
96
+ // src/config/manifest.ts
97
+ import fs2 from "fs";
98
+ import path3 from "path";
99
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
100
+
101
+ // src/config/manifestSchema.ts
102
+ import { z } from "zod";
103
+ var SNAKE_CASE_ACTION = /^[a-z][a-z0-9_]*$/;
104
+ var RESOURCE_SOURCES = ["dashboard", "sdk", "discovered"];
105
+ var RESOURCE_MANAGEMENTS = ["dashboard", "sdk", "manifest", "discovered"];
106
+ var PERMISSION_STATUSES = [
107
+ "pending_review",
108
+ "allowed",
109
+ "denied",
110
+ "approval_required"
111
+ ];
112
+ var AGENT_STATUSES = [
113
+ "pending_review",
114
+ "active",
115
+ "disabled",
116
+ "expired",
117
+ "archived"
118
+ ];
119
+ var RISK_TIERS = ["low", "medium", "high", "critical"];
120
+ var OWNER_TYPES = ["user", "team", "service_account"];
121
+ var POLICY_OPERATORS = [
122
+ ">",
123
+ ">=",
124
+ "<",
125
+ "<=",
126
+ "=",
127
+ "==",
128
+ "!=",
129
+ "contains",
130
+ "not_contains",
131
+ "starts_with",
132
+ "ends_with",
133
+ "in",
134
+ "not_in",
135
+ "exists",
136
+ "not_exists",
137
+ "is_null",
138
+ "is_not_null",
139
+ "between",
140
+ "not_between"
141
+ ];
142
+ var POLICY_DECISIONS = ["allow", "deny", "hold"];
143
+ var POLICY_CONDITION_OPERATORS = ["AND", "OR"];
144
+ var VALUELESS_OPERATORS = /* @__PURE__ */ new Set(["exists", "not_exists", "is_null", "is_not_null"]);
145
+ var PolicyScalarValueSchema = z.union([z.number(), z.string(), z.boolean(), z.null()]);
146
+ var PolicyArrayValueSchema = z.array(PolicyScalarValueSchema).max(100);
147
+ var PolicyValueSchema = z.union([PolicyScalarValueSchema, PolicyArrayValueSchema]);
148
+ var ManifestOwnerSchema = z.object({
149
+ slug: z.string().min(1),
150
+ type: z.enum(OWNER_TYPES)
151
+ });
152
+ var GrantMutationSchema = z.object({
153
+ action: z.string().min(1).regex(SNAKE_CASE_ACTION, "Action must be snake_case"),
154
+ grantedBy: z.string().min(1).optional(),
155
+ grantedReason: z.string().max(500).optional()
156
+ });
157
+ var ManifestAgentSchema = z.object({
158
+ externalId: z.string().min(1).optional(),
159
+ name: z.string().min(1, "Agent name is required"),
160
+ description: z.string().optional(),
161
+ source: z.enum(RESOURCE_SOURCES).optional(),
162
+ management: z.enum(RESOURCE_MANAGEMENTS).optional(),
163
+ owner: ManifestOwnerSchema.optional(),
164
+ ownerSlug: z.string().min(1).optional(),
165
+ ownerType: z.enum(OWNER_TYPES).optional(),
166
+ riskTier: z.enum(RISK_TIERS).optional(),
167
+ status: z.enum(AGENT_STATUSES).optional(),
168
+ expiresAt: z.string().datetime().optional(),
169
+ approvedBy: z.string().optional(),
170
+ grants: z.array(GrantMutationSchema).optional()
171
+ });
172
+ var ManifestPermissionSchema = z.object({
173
+ action: z.string().min(1).regex(SNAKE_CASE_ACTION, "Action must be snake_case"),
174
+ description: z.string().optional(),
175
+ status: z.enum(PERMISSION_STATUSES).optional(),
176
+ source: z.enum(RESOURCE_SOURCES).optional(),
177
+ management: z.enum(RESOURCE_MANAGEMENTS).optional(),
178
+ assignedAgents: z.array(z.string().min(1)).optional(),
179
+ displayName: z.string().nullable().optional(),
180
+ category: z.string().nullable().optional(),
181
+ documentation: z.string().nullable().optional(),
182
+ tags: z.array(z.string()).nullable().optional(),
183
+ metadata: z.record(z.unknown()).nullable().optional()
184
+ });
185
+ var PolicyConditionSchema = z.object({
186
+ field: z.string().min(1).max(256),
187
+ operator: z.enum(POLICY_OPERATORS),
188
+ value: PolicyValueSchema.optional(),
189
+ type: z.string().nullable().optional()
190
+ }).superRefine((condition, ctx) => {
191
+ const requiresValue = !VALUELESS_OPERATORS.has(condition.operator);
192
+ if (requiresValue && condition.value === void 0) {
193
+ ctx.addIssue({
194
+ code: z.ZodIssueCode.custom,
195
+ message: `Operator "${condition.operator}" requires a value`,
196
+ path: ["value"]
197
+ });
198
+ }
199
+ if ((condition.operator === "in" || condition.operator === "not_in") && condition.value !== void 0 && !Array.isArray(condition.value)) {
200
+ ctx.addIssue({
201
+ code: z.ZodIssueCode.custom,
202
+ message: `Operator "${condition.operator}" requires an array value`,
203
+ path: ["value"]
204
+ });
205
+ }
206
+ });
207
+ var ManifestPolicySchema = z.object({
208
+ id: z.string().min(1).optional(),
209
+ action: z.string().regex(SNAKE_CASE_ACTION, "Action must be snake_case"),
210
+ field: z.string().min(1).optional(),
211
+ operator: z.enum(POLICY_OPERATORS).optional(),
212
+ value: PolicyValueSchema.optional(),
213
+ conditions: z.array(PolicyConditionSchema).min(1).optional(),
214
+ conditionOperator: z.enum(POLICY_CONDITION_OPERATORS).optional(),
215
+ priority: z.number().int().optional(),
216
+ displayName: z.string().min(1).optional(),
217
+ decision: z.enum(POLICY_DECISIONS),
218
+ source: z.enum(RESOURCE_SOURCES).optional(),
219
+ management: z.enum(RESOURCE_MANAGEMENTS).optional()
220
+ }).superRefine((data, ctx) => {
221
+ const hasLegacy = data.field !== void 0 && data.operator !== void 0 && data.value !== void 0;
222
+ const hasConditions = (data.conditions?.length ?? 0) > 0;
223
+ if (!hasLegacy && !hasConditions) {
224
+ ctx.addIssue({
225
+ code: z.ZodIssueCode.custom,
226
+ message: "Manifest policy requires field/operator/value or conditions",
227
+ path: ["conditions"]
228
+ });
229
+ }
230
+ });
231
+ var ManifestWorkspaceSchema = z.object({
232
+ agents: z.array(ManifestAgentSchema),
233
+ permissions: z.array(ManifestPermissionSchema),
234
+ policies: z.array(ManifestPolicySchema)
235
+ });
236
+ var ManifestDocumentSchema = z.object({
237
+ apiVersion: z.string().optional(),
238
+ kind: z.string().optional(),
239
+ metadata: z.object({
240
+ name: z.string().optional(),
241
+ environment: z.string().optional()
242
+ }).optional(),
243
+ spec: ManifestWorkspaceSchema.optional(),
244
+ agents: z.array(ManifestAgentSchema).optional(),
245
+ permissions: z.array(ManifestPermissionSchema).optional(),
246
+ policies: z.array(ManifestPolicySchema).optional()
247
+ }).superRefine((value, ctx) => {
248
+ const hasSpec = value.spec !== void 0;
249
+ const hasRoot = value.agents !== void 0 || value.permissions !== void 0 || value.policies !== void 0;
250
+ if (!hasSpec && !hasRoot) {
251
+ ctx.addIssue({
252
+ code: z.ZodIssueCode.custom,
253
+ message: "Manifest must define spec.agents or root-level agents"
254
+ });
255
+ }
256
+ });
257
+
258
+ // src/config/manifestAdapter.ts
259
+ function toSdkManifest(workspace) {
260
+ return {
261
+ agents: workspace.agents.map((agent) => ({ ...agent })),
262
+ permissions: workspace.permissions.map((permission) => ({ ...permission })),
263
+ policies: workspace.policies.map((policy) => ({ ...policy }))
264
+ };
265
+ }
266
+
267
+ // src/config/manifest.ts
268
+ function resolveManifestPath(manifestFlag) {
269
+ return path3.resolve(process.cwd(), manifestFlag ?? DEFAULT_MANIFEST_FILE);
270
+ }
271
+ function loadManifestFile(manifestPath) {
272
+ if (!fs2.existsSync(manifestPath)) {
273
+ throw new Error(`Manifest not found: ${manifestPath}`);
274
+ }
275
+ const content = fs2.readFileSync(manifestPath, "utf8");
276
+ return parseManifestContent(content, manifestPath);
277
+ }
278
+ function parseManifestContent(content, manifestPath) {
279
+ let document;
280
+ try {
281
+ document = parseYaml(content);
282
+ } catch (error) {
283
+ const message = error instanceof Error ? error.message : "Invalid YAML";
284
+ throw new Error(`${manifestPath}: ${message}`);
285
+ }
286
+ const parsed = ManifestDocumentSchema.safeParse(document);
287
+ if (!parsed.success) {
288
+ const issues = formatZodIssues(parsed.error.issues);
289
+ throw new Error(`Manifest validation failed:
290
+ ${issues.join("\n")}`);
291
+ }
292
+ const doc = parsed.data;
293
+ const workspaceInput = doc.spec ?? {
294
+ agents: doc.agents ?? [],
295
+ permissions: doc.permissions ?? [],
296
+ policies: doc.policies ?? []
297
+ };
298
+ const workspace = toSdkManifest(ManifestWorkspaceSchema.parse(workspaceInput));
299
+ return {
300
+ path: manifestPath,
301
+ workspace,
302
+ apiVersion: doc.apiVersion ?? null,
303
+ metadataName: doc.metadata?.name ?? null,
304
+ environment: doc.metadata?.environment ?? null
305
+ };
306
+ }
307
+ function validateManifestFile(manifestPath) {
308
+ if (!fs2.existsSync(manifestPath)) {
309
+ return [{ path: manifestPath, message: "File not found", line: null }];
310
+ }
311
+ const content = fs2.readFileSync(manifestPath, "utf8");
312
+ try {
313
+ parseYaml(content);
314
+ } catch (error) {
315
+ const message = error instanceof Error ? error.message : "Invalid YAML";
316
+ const line = extractYamlLine(message);
317
+ return [{ path: manifestPath, message, line }];
318
+ }
319
+ let document;
320
+ try {
321
+ document = parseYaml(content);
322
+ } catch (error) {
323
+ const message = error instanceof Error ? error.message : "Invalid YAML";
324
+ return [{ path: manifestPath, message, line: extractYamlLine(message) }];
325
+ }
326
+ const parsed = ManifestDocumentSchema.safeParse(document);
327
+ if (!parsed.success) {
328
+ return parsed.error.issues.map((issue) => ({
329
+ path: issue.path.join(".") || "root",
330
+ message: issue.message,
331
+ line: null
332
+ }));
333
+ }
334
+ const doc = parsed.data;
335
+ const workspaceInput = doc.spec ?? {
336
+ agents: doc.agents ?? [],
337
+ permissions: doc.permissions ?? [],
338
+ policies: doc.policies ?? []
339
+ };
340
+ const workspaceParsed = ManifestWorkspaceSchema.safeParse(workspaceInput);
341
+ if (!workspaceParsed.success) {
342
+ return workspaceParsed.error.issues.map((issue) => ({
343
+ path: issue.path.join(".") || "root",
344
+ message: issue.message,
345
+ line: null
346
+ }));
347
+ }
348
+ return [];
349
+ }
350
+ function formatZodIssues(issues) {
351
+ return issues.map((issue) => {
352
+ const location = issue.path.length > 0 ? issue.path.join(".") : "root";
353
+ return ` - ${location}: ${issue.message}`;
354
+ });
355
+ }
356
+ function extractYamlLine(message) {
357
+ const match = message.match(/at line (\d+)/i);
358
+ if (!match?.[1]) {
359
+ return null;
360
+ }
361
+ return Number.parseInt(match[1], 10);
362
+ }
363
+ function starterManifestTemplate() {
364
+ const document = {
365
+ apiVersion: "arbiter.dev/v1",
366
+ kind: "WorkspaceGovernance",
367
+ metadata: {
368
+ name: "my-workspace",
369
+ environment: "development"
370
+ },
371
+ spec: {
372
+ agents: [
373
+ {
374
+ externalId: "finance-agent",
375
+ name: "finance-agent",
376
+ description: "Handles payment operations"
377
+ }
378
+ ],
379
+ permissions: [
380
+ {
381
+ action: "send_payment",
382
+ status: "approval_required"
383
+ }
384
+ ],
385
+ policies: [
386
+ {
387
+ id: "payment-amount-hold",
388
+ action: "send_payment",
389
+ field: "amount",
390
+ operator: ">",
391
+ value: 1e3,
392
+ decision: "hold"
393
+ }
394
+ ]
395
+ }
396
+ };
397
+ return stringifyYaml(document, { indent: 2 });
398
+ }
399
+ function workspaceToManifestDocument(workspace, metadata) {
400
+ const document = {
401
+ apiVersion: "arbiter.dev/v1",
402
+ kind: "WorkspaceGovernance",
403
+ metadata: {
404
+ name: metadata?.name ?? "imported-workspace",
405
+ environment: metadata?.environment ?? "production"
406
+ },
407
+ spec: workspace
408
+ };
409
+ return stringifyYaml(document, { indent: 2 });
410
+ }
411
+
412
+ // src/output/format.ts
413
+ var ANSI = {
414
+ reset: "\x1B[0m",
415
+ green: "\x1B[32m",
416
+ yellow: "\x1B[33m",
417
+ red: "\x1B[31m",
418
+ cyan: "\x1B[36m",
419
+ magenta: "\x1B[35m",
420
+ dim: "\x1B[2m",
421
+ bold: "\x1B[1m"
422
+ };
423
+ function isColorEnabled() {
424
+ if (process.env.NO_COLOR !== void 0) {
425
+ return false;
426
+ }
427
+ return Boolean(process.stdout.isTTY);
428
+ }
429
+ function colorize(text, color) {
430
+ if (!isColorEnabled()) {
431
+ return text;
432
+ }
433
+ return `${ANSI[color]}${text}${ANSI.reset}`;
434
+ }
435
+ function writeLine(line) {
436
+ process.stdout.write(`${line}
437
+ `);
438
+ }
439
+ function writeError(line) {
440
+ process.stderr.write(`${line}
441
+ `);
442
+ }
443
+ function divider() {
444
+ writeLine("\u2500".repeat(64));
445
+ }
446
+
447
+ // src/output/plan.ts
448
+ function renderPlanResult(result, options) {
449
+ writeLine(colorize("Arbiter Plan", "bold"));
450
+ writeLine(`Manifest: ${options.manifestPath}`);
451
+ divider();
452
+ if (!result.drift) {
453
+ writeLine(colorize("No changes. Workspace is in sync.", "green"));
454
+ return;
455
+ }
456
+ const deep = result;
457
+ for (const item of deep.changes.create) {
458
+ writeLine(formatChangeLine("[+]", "CREATE", item, "green"));
459
+ }
460
+ for (const item of deep.changes.update) {
461
+ writeLine(formatChangeLine("[~]", "UPDATE", item, "yellow"));
462
+ }
463
+ for (const item of deep.changes.archive) {
464
+ writeLine(formatChangeLine("[-]", "ARCHIVE", item, "red"));
465
+ }
466
+ for (const suggestion of deep.adoptionSuggestions) {
467
+ writeLine(formatAdoptLine(suggestion));
468
+ }
469
+ if (options.verbose) {
470
+ const unchangedTotal = deep.summary.agents.unchanged + deep.summary.permissions.unchanged + deep.summary.policies.unchanged;
471
+ if (unchangedTotal > 0) {
472
+ writeLine(
473
+ colorize(
474
+ `[=] UNCHANGED ${unchangedTotal} resources (agents: ${deep.summary.agents.unchanged}, permissions: ${deep.summary.permissions.unchanged}, policies: ${deep.summary.policies.unchanged})`,
475
+ "dim"
476
+ )
477
+ );
478
+ }
479
+ }
480
+ divider();
481
+ writeLine(
482
+ `Plan: ${deep.summary.agents.create + deep.summary.permissions.create + deep.summary.policies.create} to add, ${deep.summary.agents.update + deep.summary.permissions.update + deep.summary.policies.update} to change, ${deep.summary.agents.archive + deep.summary.permissions.archive + deep.summary.policies.archive} to archive, ${deep.adoptionSuggestions.length} adopt suggestions.`
483
+ );
484
+ }
485
+ function planHasActionableChanges(result) {
486
+ if (!result.drift) {
487
+ return false;
488
+ }
489
+ const deep = result;
490
+ const summary = deep.summary;
491
+ const mutations = summary.agents.create + summary.agents.update + summary.agents.archive + summary.permissions.create + summary.permissions.update + summary.permissions.archive + summary.policies.create + summary.policies.update + summary.policies.archive;
492
+ return mutations > 0 || deep.adoptionSuggestions.length > 0;
493
+ }
494
+ function formatChangeLine(indicator, verb, item, color) {
495
+ const resourceType = capitalize(item.type);
496
+ const identifier = resourceIdentifier(item);
497
+ return colorize(`${indicator} ${verb} ${resourceType} ${identifier}`, color);
498
+ }
499
+ function formatAdoptLine(suggestion) {
500
+ const resourceType = capitalize(suggestion.type);
501
+ const identifier = adoptIdentifier(suggestion);
502
+ return colorize(`[?] ADOPT ${resourceType} ${identifier}`, "cyan");
503
+ }
504
+ function resourceIdentifier(item) {
505
+ if (item.type === "agent" && item.externalId) {
506
+ return item.externalId;
507
+ }
508
+ if (item.type === "permission" && item.action) {
509
+ return item.action;
510
+ }
511
+ if (item.type === "policy" && item.id) {
512
+ return item.id;
513
+ }
514
+ if (item.type === "grant" && item.externalId && item.action) {
515
+ return `${item.externalId} -> ${item.action}`;
516
+ }
517
+ if (item.externalId) {
518
+ return item.externalId;
519
+ }
520
+ if (item.action) {
521
+ return item.action;
522
+ }
523
+ if (item.id) {
524
+ return item.id;
525
+ }
526
+ return "unknown";
527
+ }
528
+ function adoptIdentifier(suggestion) {
529
+ if (suggestion.externalId) {
530
+ return suggestion.externalId;
531
+ }
532
+ if (suggestion.action) {
533
+ return suggestion.action;
534
+ }
535
+ if (suggestion.id) {
536
+ return suggestion.id;
537
+ }
538
+ return "unknown";
539
+ }
540
+ function capitalize(value) {
541
+ return value.charAt(0).toUpperCase() + value.slice(1);
542
+ }
543
+
544
+ // src/commands/apply.ts
545
+ async function runApply(options) {
546
+ try {
547
+ const manifestPath = resolveManifestPath(options.manifest);
548
+ const loaded = loadManifestFile(manifestPath);
549
+ const arbiter = createArbiterClient();
550
+ const planResult = await arbiter.plan(loaded.workspace);
551
+ renderPlanResult(planResult, {
552
+ verbose: options.verbose === true,
553
+ manifestPath
554
+ });
555
+ if (!options.yes) {
556
+ const confirmed = await promptForConfirmation();
557
+ if (!confirmed) {
558
+ writeLine(colorize("Apply cancelled.", "yellow"));
559
+ return 0;
560
+ }
561
+ }
562
+ const applyResult = await arbiter.syncRegistryManifest(loaded.workspace);
563
+ divider();
564
+ writeLine(colorize("Apply complete.", "green"));
565
+ writeLine(
566
+ `Summary: agents(+${applyResult.summary.agents.created}/~${applyResult.summary.agents.updated}/-${applyResult.summary.agents.archived}), permissions(+${applyResult.summary.permissions.created}/~${applyResult.summary.permissions.updated}/-${applyResult.summary.permissions.archived}), policies(+${applyResult.summary.policies.created}/~${applyResult.summary.policies.updated}/-${applyResult.summary.policies.archived})`
567
+ );
568
+ if (applyResult.stateSignature) {
569
+ writeLine(`State signature: ${applyResult.stateSignature}`);
570
+ }
571
+ return 0;
572
+ } catch (error) {
573
+ writeError(formatCommandError(error));
574
+ return 1;
575
+ }
576
+ }
577
+ async function promptForConfirmation() {
578
+ if (!process.stdin.isTTY) {
579
+ throw new Error(
580
+ "Non-interactive environment detected. Re-run with --yes to apply without a prompt."
581
+ );
582
+ }
583
+ const rl = readline.createInterface({ input, output });
584
+ try {
585
+ const answer = (await rl.question("Apply these changes? [y/N] ")).trim().toLowerCase();
586
+ return answer === "y" || answer === "yes";
587
+ } finally {
588
+ rl.close();
589
+ }
590
+ }
591
+ function formatCommandError(error) {
592
+ if (error instanceof ArbiterError) {
593
+ return `Error: ${error.message}`;
594
+ }
595
+ if (error instanceof Error) {
596
+ return `Error: ${error.message}`;
597
+ }
598
+ return "Error: Unknown failure";
599
+ }
600
+
601
+ // src/commands/drift.ts
602
+ import { ArbiterError as ArbiterError2 } from "@arbiterhq/sdk";
603
+
604
+ // src/output/drift.ts
605
+ function renderDriftResult(result) {
606
+ writeLine(colorize("Arbiter Drift", "bold"));
607
+ divider();
608
+ if (!result.drift) {
609
+ writeLine(colorize("In sync. No drift or adoption suggestions detected.", "green"));
610
+ return;
611
+ }
612
+ const configDrift = result.records.filter(
613
+ (record) => record.reason === "manifest_create" || record.reason === "manifest_update" || record.reason === "manifest_archive"
614
+ );
615
+ const adoptionRecords = result.records.filter(
616
+ (record) => record.reason === "discovered_unadopted" || record.reason === "pending_review"
617
+ );
618
+ if (configDrift.length > 0) {
619
+ writeLine(colorize("Configuration Drift", "magenta"));
620
+ for (const record of configDrift) {
621
+ writeLine(formatDriftRecord(record));
622
+ }
623
+ writeLine("");
624
+ }
625
+ if (adoptionRecords.length > 0 || result.adoptionSuggestions.length > 0) {
626
+ writeLine(colorize("Adoption Suggestions", "cyan"));
627
+ for (const record of adoptionRecords) {
628
+ writeLine(formatDriftRecord(record));
629
+ }
630
+ for (const suggestion of result.adoptionSuggestions) {
631
+ const id = suggestion.externalId ?? suggestion.action ?? suggestion.id ?? "unknown";
632
+ writeLine(
633
+ colorize(
634
+ `[?] ADOPT ${capitalize2(suggestion.type)} ${id} (discovered runtime asset)`,
635
+ "cyan"
636
+ )
637
+ );
638
+ }
639
+ }
640
+ divider();
641
+ writeLine(
642
+ `Drift summary: ${configDrift.length} configuration records, ${adoptionRecords.length + result.adoptionSuggestions.length} adoption suggestions.`
643
+ );
644
+ }
645
+ function formatDriftRecord(record) {
646
+ const id = record.externalId ?? record.action ?? record.id ?? "unknown";
647
+ const label = `${capitalize2(record.type)} ${id}`;
648
+ const reason = record.reason.replace(/_/g, " ");
649
+ return colorize(`[!] ${label} \u2014 ${reason}`, "yellow");
650
+ }
651
+ function capitalize2(value) {
652
+ return value.charAt(0).toUpperCase() + value.slice(1);
653
+ }
654
+
655
+ // src/commands/drift.ts
656
+ async function runDrift() {
657
+ try {
658
+ const arbiter = createArbiterClient();
659
+ const result = await arbiter.getDrift();
660
+ renderDriftResult(result);
661
+ if (result.drift) {
662
+ return 2;
663
+ }
664
+ return 0;
665
+ } catch (error) {
666
+ writeError(formatCommandError2(error));
667
+ return 1;
668
+ }
669
+ }
670
+ function formatCommandError2(error) {
671
+ if (error instanceof ArbiterError2) {
672
+ return `Error: ${error.message}`;
673
+ }
674
+ if (error instanceof Error) {
675
+ return `Error: ${error.message}`;
676
+ }
677
+ return "Error: Unknown failure";
678
+ }
679
+
680
+ // src/commands/init.ts
681
+ import fs3 from "fs";
682
+ import path4 from "path";
683
+ async function runInit(options) {
684
+ const manifestPath = resolveManifestPath(options.manifest);
685
+ if (fs3.existsSync(manifestPath) && !options.force) {
686
+ throw new Error(
687
+ `${manifestPath} already exists. Use --force to overwrite.`
688
+ );
689
+ }
690
+ let content;
691
+ if (options.fromRemote) {
692
+ const session = resolveSession();
693
+ const arbiter = createArbiterClient();
694
+ const drift = await arbiter.getDrift();
695
+ const workspace = buildWorkspaceFromDrift(drift.adoptionSuggestions, drift.records);
696
+ content = workspaceToManifestDocument(workspace, {
697
+ name: session.workspace ?? "imported-workspace",
698
+ environment: session.environment ?? "production"
699
+ });
700
+ writeLine(colorize("Imported adoption suggestions from remote workspace.", "cyan"));
701
+ } else {
702
+ content = starterManifestTemplate();
703
+ }
704
+ fs3.writeFileSync(manifestPath, `${content}
705
+ `, "utf8");
706
+ writeLine(colorize("Created manifest template.", "green"));
707
+ writeLine(`Path: ${path4.resolve(manifestPath)}`);
708
+ writeLine("Next: arbiter validate \u2192 arbiter plan \u2192 arbiter apply --yes");
709
+ }
710
+ function buildWorkspaceFromDrift(suggestions, records) {
711
+ const workspace = {
712
+ agents: [],
713
+ permissions: [],
714
+ policies: []
715
+ };
716
+ const seenAgents = /* @__PURE__ */ new Set();
717
+ const seenPermissions = /* @__PURE__ */ new Set();
718
+ const seenPolicies = /* @__PURE__ */ new Set();
719
+ for (const suggestion of suggestions) {
720
+ if (suggestion.type === "agent" && suggestion.externalId && !seenAgents.has(suggestion.externalId)) {
721
+ workspace.agents.push({
722
+ externalId: suggestion.externalId,
723
+ name: suggestion.externalId
724
+ });
725
+ seenAgents.add(suggestion.externalId);
726
+ }
727
+ if (suggestion.type === "permission" && suggestion.action && !seenPermissions.has(suggestion.action)) {
728
+ workspace.permissions.push({
729
+ action: suggestion.action,
730
+ status: "pending_review"
731
+ });
732
+ seenPermissions.add(suggestion.action);
733
+ }
734
+ if (suggestion.type === "policy" && suggestion.id && !seenPolicies.has(suggestion.id)) {
735
+ workspace.policies.push({
736
+ id: suggestion.id,
737
+ action: "discovered_action",
738
+ field: "amount",
739
+ operator: ">",
740
+ value: 0,
741
+ decision: "hold"
742
+ });
743
+ seenPolicies.add(suggestion.id);
744
+ }
745
+ }
746
+ for (const record of records) {
747
+ if (record.type === "agent" && record.externalId && !seenAgents.has(record.externalId)) {
748
+ workspace.agents.push({
749
+ externalId: record.externalId,
750
+ name: record.externalId
751
+ });
752
+ seenAgents.add(record.externalId);
753
+ }
754
+ if (record.type === "permission" && record.action && !seenPermissions.has(record.action)) {
755
+ workspace.permissions.push({
756
+ action: record.action,
757
+ status: "pending_review"
758
+ });
759
+ seenPermissions.add(record.action);
760
+ }
761
+ if (record.type === "policy" && record.id && !seenPolicies.has(record.id)) {
762
+ workspace.policies.push({
763
+ id: record.id,
764
+ action: "discovered_action",
765
+ field: "amount",
766
+ operator: ">",
767
+ value: 0,
768
+ decision: "hold"
769
+ });
770
+ seenPolicies.add(record.id);
771
+ }
772
+ }
773
+ return workspace;
774
+ }
775
+
776
+ // src/commands/login.ts
777
+ import readline2 from "readline/promises";
778
+ import { stdin as input2, stdout as output2 } from "process";
779
+ async function runLogin(options) {
780
+ const rl = readline2.createInterface({ input: input2, output: output2 });
781
+ try {
782
+ const apiKey = options.apiKey ?? (await rl.question("API key: ")).trim();
783
+ if (apiKey.length === 0) {
784
+ throw new Error("API key is required");
785
+ }
786
+ const workspace = options.workspace ?? (await rl.question("Workspace name (optional): ")).trim();
787
+ const environment = options.environment ?? (await rl.question("Environment (optional): ")).trim();
788
+ const baseUrlInput = options.baseUrl ?? (await rl.question(`Endpoint [${DEFAULT_BASE_URL}]: `)).trim();
789
+ const baseUrl = baseUrlInput.length > 0 ? baseUrlInput : DEFAULT_BASE_URL;
790
+ const existing = loadCredentialsFile();
791
+ const credentials = {
792
+ apiKey,
793
+ baseUrl
794
+ };
795
+ const resolvedWorkspace = workspace.length > 0 ? workspace : existing?.workspace;
796
+ const resolvedEnvironment = environment.length > 0 ? environment : existing?.environment;
797
+ if (resolvedWorkspace !== void 0) {
798
+ credentials.workspace = resolvedWorkspace;
799
+ }
800
+ if (resolvedEnvironment !== void 0) {
801
+ credentials.environment = resolvedEnvironment;
802
+ }
803
+ saveCredentialsFile(credentials);
804
+ writeLine(colorize("Credentials saved.", "green"));
805
+ writeLine(`Path: ${credentialsFilePath()}`);
806
+ writeLine("Validation occurs on the next command that contacts the API.");
807
+ } finally {
808
+ rl.close();
809
+ }
810
+ }
811
+
812
+ // src/commands/plan.ts
813
+ import { ArbiterError as ArbiterError3 } from "@arbiterhq/sdk";
814
+ async function runPlan(options) {
815
+ try {
816
+ const manifestPath = resolveManifestPath(options.manifest);
817
+ const loaded = loadManifestFile(manifestPath);
818
+ const arbiter = createArbiterClient();
819
+ const result = await arbiter.plan(loaded.workspace);
820
+ renderPlanResult(result, {
821
+ verbose: options.verbose === true,
822
+ manifestPath
823
+ });
824
+ if (planHasActionableChanges(result)) {
825
+ return 2;
826
+ }
827
+ return 0;
828
+ } catch (error) {
829
+ writeError(formatCommandError3(error));
830
+ return 1;
831
+ }
832
+ }
833
+ function formatCommandError3(error) {
834
+ if (error instanceof ArbiterError3) {
835
+ return `Error: ${error.message}`;
836
+ }
837
+ if (error instanceof Error) {
838
+ return `Error: ${error.message}`;
839
+ }
840
+ return "Error: Unknown failure";
841
+ }
842
+
843
+ // src/commands/status.ts
844
+ import { ArbiterError as ArbiterError4 } from "@arbiterhq/sdk";
845
+ async function runStatus(options) {
846
+ try {
847
+ const session = resolveSession();
848
+ const manifestPath = resolveManifestPath(options.manifest);
849
+ let apiVersion = "n/a";
850
+ let manifestName = "n/a";
851
+ try {
852
+ const loaded = loadManifestFile(manifestPath);
853
+ apiVersion = loaded.apiVersion ?? "n/a";
854
+ manifestName = loaded.metadataName ?? manifestPath;
855
+ } catch {
856
+ manifestName = `${manifestPath} (missing)`;
857
+ }
858
+ const arbiter = createArbiterClient();
859
+ const signature = await arbiter.getStateSignature();
860
+ writeLine(colorize("Arbiter Status", "bold"));
861
+ divider();
862
+ writeLine(`Endpoint: ${session.baseUrl}`);
863
+ writeLine(`Manifest: ${manifestName}`);
864
+ writeLine(`Manifest version: ${apiVersion}`);
865
+ writeLine(`State signature: ${signature.stateSignature}`);
866
+ writeLine(`Workspace label: ${session.workspace ?? "not set"}`);
867
+ writeLine(`Environment: ${session.environment ?? "not set"}`);
868
+ return 0;
869
+ } catch (error) {
870
+ writeError(formatCommandError4(error));
871
+ return 1;
872
+ }
873
+ }
874
+ function formatCommandError4(error) {
875
+ if (error instanceof ArbiterError4) {
876
+ return `Error: ${error.message}`;
877
+ }
878
+ if (error instanceof Error) {
879
+ return `Error: ${error.message}`;
880
+ }
881
+ return "Error: Unknown failure";
882
+ }
883
+
884
+ // src/commands/validate.ts
885
+ function runValidate(options) {
886
+ const manifestPath = resolveManifestPath(options.manifest);
887
+ const issues = validateManifestFile(manifestPath);
888
+ if (issues.length > 0) {
889
+ writeError(colorize("Manifest validation failed:", "red"));
890
+ for (const issue of issues) {
891
+ const line = issue.line !== null ? ` (line ${issue.line})` : "";
892
+ writeError(` ${issue.path}${line}: ${issue.message}`);
893
+ }
894
+ return 1;
895
+ }
896
+ writeLine(colorize(`\u2713 ${manifestPath} is structurally valid`, "green"));
897
+ return 0;
898
+ }
899
+
900
+ // src/commands/whoami.ts
901
+ function runWhoami() {
902
+ try {
903
+ const session = resolveSession();
904
+ const file = loadCredentialsFile();
905
+ const hasEnvKey = process.env.ARBITER_API_KEY !== void 0;
906
+ writeLine(colorize("Arbiter Session", "bold"));
907
+ divider();
908
+ writeLine(`Workspace: ${session.workspace ?? "not set"}`);
909
+ writeLine(`Environment: ${session.environment ?? "not set"}`);
910
+ writeLine(`Endpoint: ${session.baseUrl}`);
911
+ writeLine(`Authenticated: true`);
912
+ writeLine(`Credential: ${credentialsFilePath()}`);
913
+ writeLine(`API key: ${maskApiKey(session.apiKey)} (${session.source})`);
914
+ if (hasEnvKey) {
915
+ writeLine("Note: ARBITER_API_KEY overrides stored credentials");
916
+ }
917
+ if (!file && hasEnvKey) {
918
+ writeLine("Note: Using environment variable only");
919
+ }
920
+ return 0;
921
+ } catch {
922
+ writeLine(colorize("Authenticated: false", "red"));
923
+ writeLine("Run `arbiter login` or set ARBITER_API_KEY.");
924
+ return 1;
925
+ }
926
+ }
927
+
928
+ // src/cli.ts
929
+ function buildCli() {
930
+ const program2 = new Command();
931
+ program2.name("arbiter").description("Arbiter governance control plane CLI").version("0.1.0");
932
+ program2.command("login").description("Store API credentials locally (no network validation)").option("--api-key <key>", "API key value").option("--workspace <name>", "Workspace label").option("--environment <name>", "Environment label").option("--base-url <url>", "API base URL").action(async (options) => {
933
+ await runLogin({
934
+ ...options.apiKey !== void 0 ? { apiKey: options.apiKey } : {},
935
+ ...options.workspace !== void 0 ? { workspace: options.workspace } : {},
936
+ ...options.environment !== void 0 ? { environment: options.environment } : {},
937
+ ...options.baseUrl !== void 0 ? { baseUrl: options.baseUrl } : {}
938
+ });
939
+ });
940
+ program2.command("init").description("Create a starter arbiter.yaml manifest").option("-m, --manifest <path>", "Manifest file path", "arbiter.yaml").option("--from-remote", "Import adoption suggestions from the remote workspace").option("--force", "Overwrite an existing manifest file").action(async (options) => {
941
+ await runInit({
942
+ ...options.manifest !== void 0 ? { manifest: options.manifest } : {},
943
+ ...options.fromRemote !== void 0 ? { fromRemote: options.fromRemote } : {},
944
+ ...options.force !== void 0 ? { force: options.force } : {}
945
+ });
946
+ });
947
+ program2.command("validate").description("Validate local arbiter.yaml structure (no network calls)").option("-m, --manifest <path>", "Manifest file path", "arbiter.yaml").action((options) => {
948
+ const code = runValidate({
949
+ ...options.manifest !== void 0 ? { manifest: options.manifest } : {}
950
+ });
951
+ process.exit(code);
952
+ });
953
+ program2.command("plan").description("Show manifest diff against the remote workspace").option("-m, --manifest <path>", "Manifest file path", "arbiter.yaml").option("--verbose", "Include unchanged resource summary").action(async (options) => {
954
+ const code = await runPlan({
955
+ ...options.manifest !== void 0 ? { manifest: options.manifest } : {},
956
+ ...options.verbose !== void 0 ? { verbose: options.verbose } : {}
957
+ });
958
+ process.exit(code);
959
+ });
960
+ program2.command("apply").description("Apply manifest changes to the remote workspace").option("-m, --manifest <path>", "Manifest file path", "arbiter.yaml").option("--yes", "Skip confirmation prompt (required for CI/CD)").option("--verbose", "Include unchanged resource summary in plan preview").action(async (options) => {
961
+ const code = await runApply({
962
+ ...options.manifest !== void 0 ? { manifest: options.manifest } : {},
963
+ ...options.yes !== void 0 ? { yes: options.yes } : {},
964
+ ...options.verbose !== void 0 ? { verbose: options.verbose } : {}
965
+ });
966
+ process.exit(code);
967
+ });
968
+ program2.command("drift").description("Show workspace drift and adoption suggestions").action(async () => {
969
+ const code = await runDrift();
970
+ process.exit(code);
971
+ });
972
+ program2.command("status").description("Show endpoint, manifest version, and state signature").option("-m, --manifest <path>", "Manifest file path", "arbiter.yaml").action(async (options) => {
973
+ const code = await runStatus({
974
+ ...options.manifest !== void 0 ? { manifest: options.manifest } : {}
975
+ });
976
+ process.exit(code);
977
+ });
978
+ program2.command("whoami").description("Show active credential session context").action(() => {
979
+ const code = runWhoami();
980
+ process.exit(code);
981
+ });
982
+ return program2;
983
+ }
984
+
985
+ // src/index.ts
986
+ var program = buildCli();
987
+ program.parseAsync(process.argv).catch((error) => {
988
+ const message = error instanceof Error ? error.message : "Unknown CLI failure";
989
+ process.stderr.write(`Error: ${message}
990
+ `);
991
+ process.exit(1);
992
+ });
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@arbiterhq/cli",
3
+ "version": "0.1.0",
4
+ "description": "Official Arbiter command line interface",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "arbiter": "dist/index.js"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "files": [
13
+ "dist",
14
+ "README.md",
15
+ "CHANGELOG.md"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format esm --dts --clean",
22
+ "typecheck": "tsc --noEmit",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "dependencies": {
26
+ "@arbiterhq/sdk": "^0.2.0",
27
+ "commander": "^13.1.0",
28
+ "yaml": "^2.8.0",
29
+ "zod": "^3.25.76"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^22.15.21",
33
+ "tsup": "^8.5.0",
34
+ "typescript": "^5.8.3"
35
+ },
36
+ "keywords": [
37
+ "arbiter",
38
+ "cli",
39
+ "governance"
40
+ ],
41
+ "homepage": "https://github.com/sumitbirru1-halo/arbiter-sdk#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/sumitbirru1-halo/arbiter-sdk/issues"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/sumitbirru1-halo/arbiter-sdk.git",
48
+ "directory": "packages/cli"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ }
53
+ }