@ciphrix/cli 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -72,8 +72,8 @@ changes show the proposed action and ask for confirmation; automation can opt in
72
72
  - **Manage operational registers** — work with assets, vendors, risks, treatments, ownership, and files.
73
73
  - **Operate monitoring** — inspect checks, resources, runs, findings, integrations, and background jobs.
74
74
 
75
- Names and stable Ciphrix codes are accepted throughout, so neither people nor agents need to work with
76
- opaque database identifiers.
75
+ Established record commands accept names and stable Ciphrix codes where advertised. Access inventory,
76
+ review tasks and proposals require the exact IDs returned by their read commands; never guess identifiers.
77
77
 
78
78
  ## Built for agent workspaces
79
79
 
@@ -99,7 +99,9 @@ The skill works with compatible agent harnesses including Codex, Claude Code, Cu
99
99
  is also included in the npm package at [`skills/ciphrix/SKILL.md`](skills/ciphrix/SKILL.md). Installing
100
100
  the npm package never modifies an agent's configuration directories.
101
101
 
102
- Run `npx skills update` to update installed skills when a newer version is available.
102
+ Run `npx skills update` to update installed skills when a newer version is available. Keep the skill
103
+ aligned with your installed CLI version; its generated command reference and local help describe the
104
+ available grammar. Server capability discovery remains authoritative for which operations you can use.
103
105
 
104
106
  ### Ask in natural language, operate through the CLI
105
107
 
@@ -155,6 +157,47 @@ one command. Installed help is authoritative. The complete generated reference i
155
157
 
156
158
  </details>
157
159
 
160
+ ### Context, reviews and agent-assisted work
161
+
162
+ The onboarding group reads separate Business and Operating Context, saves agreed questionnaire answers,
163
+ and offers explicit optional setup and completion. Business Context readiness is decided by the server;
164
+ Operating Context and optional setup do not block journey completion. Saving context never automatically
165
+ starts agents or completes the journey. Local `--help` works offline; `onboarding --help-onboarding`
166
+ returns authenticated server guidance and the current status pack.
167
+
168
+ The access group separates inventory, reviewed import proposals, assigned reviewer decisions and campaign
169
+ results. A revoke decision records a removal request, not provider-side revocation. Recording decisions,
170
+ completing a reviewer task, and ending a campaign are separate operations. Imports merge only explicitly
171
+ selected reviewed rows; inspect per-row failures even when some rows applied.
172
+
173
+ Agent findings and advisory test/control assessments do not make formal human decisions. Assigned-task
174
+ discovery does not complete work. File staging and processing are explicit, separate operations; processing
175
+ may update context or create vendors. Evidence/CSV commands return expiring signed references without
176
+ automatically downloading files. Use each command's help for the exact inputs, next reads and effects.
177
+
178
+ ### Manifest compatibility, JSON and retries
179
+
180
+ The CLI discovers allowed tools and their effects from the authenticated server manifest. New command
181
+ groups require server-declared command bindings. An unavailable binding is reported as unsupported;
182
+ the CLI never guesses a different endpoint or interprets the manifest as authorization. Existing
183
+ commands keep their successful JSON formats. New capability commands emit complete success/error
184
+ envelopes with `--json`, preserving warnings, pagination, truncation and structured error details.
185
+ Failures and partially applied mutations return a nonzero exit status.
186
+
187
+ Confirmed writes require explicit consent. New commands in JSON/non-interactive mode require `--yes`
188
+ for confirmed operations; immediate writes do not accept that flag. Write controls are rejected for
189
+ reads. The optional `--idempotency-key` names one logical write: reuse identical input for a retry,
190
+ never a new key to bypass an unknown outcome. There are no automatic write retries. Confirmed replay
191
+ also needs a valid confirmation token; a fresh invocation re-preflights and may be blocked by changed
192
+ state or expiry. The CLI does not save confirmation tokens or promise recovery across invocations.
193
+ Inspect current records/jobs when uncertain. Replayed status packs and signed URLs are original
194
+ snapshots, not refreshed state or renewed downloads.
195
+
196
+ Mutation JSON files must be regular files, at most 1 MiB, containing an explicitly selected array.
197
+ Use clear flags or JSON null where supported, not the string `"null"`. Do not infer confirmations
198
+ from suggested matches. New repeated flags collect separate values; existing command conventions remain
199
+ compatible. See the generated command reference for all options and nested payload details.
200
+
158
201
  ## Safe by default
159
202
 
160
203
  - Credentials use the operating system keychain by default and are isolated by API environment.
package/dist/actions.d.ts CHANGED
@@ -8,6 +8,8 @@ export interface ApplyOptions {
8
8
  fallback: string;
9
9
  yes?: boolean | undefined;
10
10
  confirm?: ((question: string) => Promise<boolean>) | undefined;
11
+ idempotencyKey?: string | undefined;
12
+ quiet?: boolean | undefined;
11
13
  }
12
14
  /**
13
15
  * Runs a tool, handling the two-step confirm contract when the tool requires it.
@@ -16,4 +18,4 @@ export interface ApplyOptions {
16
18
  * - Tools that require confirmation: the first call is a preflight (summary + token), the user is asked
17
19
  * once, then the change is applied with the token and an idempotency key.
18
20
  */
19
- export declare const applyTool: ({ ctx, io, toolName, input, fallback, yes, confirm, }: ApplyOptions) => Promise<Record<string, unknown> | null>;
21
+ export declare const applyTool: ({ ctx, io, toolName, input, fallback, yes, confirm, idempotencyKey: suppliedKey, quiet, }: ApplyOptions) => Promise<Record<string, unknown> | null>;
package/dist/actions.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { promptConfirm, writeLine } from './io.js';
3
+ import { ApiError } from './api.js';
4
+ import { getTool } from './manifest.js';
3
5
  import { asRecord, asText, callTool, errorMessage } from './toolSurface.js';
4
6
  /**
5
7
  * Runs a tool, handling the two-step confirm contract when the tool requires it.
@@ -8,23 +10,30 @@ import { asRecord, asText, callTool, errorMessage } from './toolSurface.js';
8
10
  * - Tools that require confirmation: the first call is a preflight (summary + token), the user is asked
9
11
  * once, then the change is applied with the token and an idempotency key.
10
12
  */
11
- export const applyTool = async ({ ctx, io, toolName, input, fallback, yes = false, confirm, }) => {
13
+ export const applyTool = async ({ ctx, io, toolName, input, fallback, yes = false, confirm, idempotencyKey: suppliedKey, quiet = false, }) => {
12
14
  const ask = confirm ?? ((question) => promptConfirm(io, question));
13
- const idempotencyKey = randomUUID();
15
+ const idempotencyKey = suppliedKey ?? randomUUID();
16
+ const tool = await getTool(ctx, toolName);
17
+ if (tool.effect !== 'write')
18
+ throw new Error('Expected a write operation.');
14
19
  const first = await callTool(ctx, toolName, input, { idempotencyKey });
15
20
  if (first.status !== 'ok')
16
- throw new Error(errorMessage(first, fallback));
21
+ throw new ApiError(errorMessage(first, fallback), 400, asText(asRecord(first.error).code), asRecord(first.error).details, first);
17
22
  const confirmation = asRecord(asRecord(first.meta).confirmation);
18
23
  const token = asText(confirmation.token);
19
- if (!token)
24
+ if (tool.confirmation === 'none')
20
25
  return first;
21
- writeLine(io.stdout, asText(confirmation.summary, 'Apply this change?'));
26
+ if (!token)
27
+ throw new Error('The server did not return the required confirmation token. No change was applied.');
28
+ if (!quiet)
29
+ writeLine(io.stdout, asText(confirmation.summary, 'Apply this change?'));
22
30
  if (!yes && !(await ask('Apply this change?'))) {
23
- writeLine(io.stdout, 'Cancelled.');
31
+ if (!quiet)
32
+ writeLine(io.stdout, 'Cancelled.');
24
33
  return null;
25
34
  }
26
35
  const result = await callTool(ctx, toolName, input, { confirmationToken: token, idempotencyKey });
27
36
  if (result.status !== 'ok')
28
- throw new Error(errorMessage(result, fallback));
37
+ throw new ApiError(errorMessage(result, fallback), 400, asText(asRecord(result.error).code), asRecord(result.error).details, result);
29
38
  return result;
30
39
  };
package/dist/api.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export declare class ApiError extends Error {
2
2
  readonly status: number;
3
3
  readonly code: string | undefined;
4
- constructor(message: string, status: number, code?: string | undefined);
4
+ readonly details: unknown;
5
+ readonly envelope: unknown;
6
+ constructor(message: string, status: number, code?: string | undefined, details?: unknown, envelope?: unknown);
5
7
  }
6
8
  export interface RequestOptions {
7
9
  method?: string;
package/dist/api.js CHANGED
@@ -1,11 +1,15 @@
1
1
  export class ApiError extends Error {
2
2
  status;
3
3
  code;
4
- constructor(message, status, code = undefined) {
4
+ details;
5
+ envelope;
6
+ constructor(message, status, code = undefined, details = undefined, envelope = undefined) {
5
7
  super(message);
6
8
  this.name = 'ApiError';
7
9
  this.status = status;
8
10
  this.code = code;
11
+ this.details = details;
12
+ this.envelope = envelope;
9
13
  }
10
14
  }
11
15
  /** Default time allowed for an API request, including reading its response body. */
@@ -104,7 +108,7 @@ const errorPayload = (data) => {
104
108
  : typeof nested?.code === 'string'
105
109
  ? nested.code
106
110
  : undefined;
107
- const details = Array.isArray(nested?.details) ? nested?.details : [];
111
+ const details = nested?.details;
108
112
  return { message, code, details };
109
113
  };
110
114
  /**
@@ -172,7 +176,7 @@ export const createApiClient = (baseUrl, limits = {}) => ({
172
176
  }
173
177
  if (!response.ok) {
174
178
  const { message, code, details } = errorPayload(data);
175
- throw new ApiError(withDetails(message || `Request failed with status ${response.status}`, code, details), response.status, code);
179
+ throw new ApiError(withDetails(message || `Request failed with status ${response.status}`, code, Array.isArray(details) ? details : []), response.status, code, details, data);
176
180
  }
177
181
  return data;
178
182
  });
package/dist/cli.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { Command, CommanderError } from 'commander';
2
+ import { registerCapabilities, RenderedCommandError } from './commands/capabilities.js';
3
+ import { asRecord } from './toolSurface.js';
2
4
  import { formatBanner } from './banner.js';
3
5
  import { loginCommand } from './commands/login.js';
4
6
  import { logoutCommand } from './commands/logout.js';
@@ -247,7 +249,9 @@ export const createProgram = (io = createDefaultIO()) => {
247
249
  json: readBooleanOption(options, 'json'),
248
250
  });
249
251
  });
250
- const testRun = test.command('run').description('Create, result and delete runs');
252
+ const testRun = test
253
+ .command('run')
254
+ .description('Run lifecycle, human results/notes, linked evidence references and advisory assessments');
251
255
  testRun
252
256
  .command('create <name>')
253
257
  .description('Create a new pending run')
@@ -266,17 +270,21 @@ export const createProgram = (io = createDefaultIO()) => {
266
270
  testRun
267
271
  .command('result <name>')
268
272
  .description('Record a run result: passing | failing | skipped | pending (reopen)')
269
- .requiredOption('--status <status>', 'passing | failing | skipped | pending')
273
+ .option('--status <status>', 'human-decided result: passing | failing | skipped | pending; omit for note-only changes')
270
274
  .option('--note <text>', 'note for the run')
275
+ .option('--clear-note', 'clear the note without changing the result; conflicts with --note')
271
276
  .option('--run <id>', 'a specific run')
272
277
  .option('--api-url <url>', 'override the API base URL for this command')
273
278
  .option('--json', 'output raw JSON')
274
279
  .action(async (name, options) => {
280
+ if (readBooleanOption(options, 'clearNote') &&
281
+ readStringOption(options, 'note') !== undefined)
282
+ throw new Error('--clear-note conflicts with --note.');
275
283
  await testRunResultCommand({
276
284
  io,
277
285
  name,
278
- status: readStringOption(options, 'status') ?? '',
279
- note: readStringOption(options, 'note'),
286
+ status: readStringOption(options, 'status'),
287
+ note: readBooleanOption(options, 'clearNote') ? null : readStringOption(options, 'note'),
280
288
  runId: readStringOption(options, 'run'),
281
289
  apiUrl: readStringOption(options, 'apiUrl'),
282
290
  json: readBooleanOption(options, 'json'),
@@ -848,10 +856,11 @@ export const createProgram = (io = createDefaultIO()) => {
848
856
  });
849
857
  const control = program
850
858
  .command('control')
851
- .description('Controls: status, applicability, owner and design requirements');
859
+ .description('Controls: status, applicability, ownership, design requirements and advisory assessments');
852
860
  control
853
861
  .command('list')
854
862
  .description('List controls with their status, applicability and owner')
863
+ .option('--framework <code>', 'filter by catalogue framework code; repeat for multiple values', (value, previous = []) => [...previous, value])
855
864
  .option('--search <text>', 'filter by name or code')
856
865
  .option('--domain <value>', 'filter by control domain')
857
866
  .option('--status <csv>', 'filter by status')
@@ -864,6 +873,9 @@ export const createProgram = (io = createDefaultIO()) => {
864
873
  .action(async (options) => {
865
874
  await controlListCommand({
866
875
  io,
876
+ framework: Array.isArray(asRecord(options).framework)
877
+ ? asRecord(options).framework
878
+ : undefined,
867
879
  search: readStringOption(options, 'search'),
868
880
  domain: readStringOption(options, 'domain'),
869
881
  status: readStringOption(options, 'status'),
@@ -1567,6 +1579,7 @@ export const createProgram = (io = createDefaultIO()) => {
1567
1579
  json: readBooleanOption(options, 'json'),
1568
1580
  });
1569
1581
  });
1582
+ registerCapabilities(program, io);
1570
1583
  // A bare `ciphrix` shows the banner and help; commands never print the banner.
1571
1584
  program.action(() => {
1572
1585
  writeBlock(io.stdout, `${formatBanner(io.theme)}\n\n`);
@@ -1590,6 +1603,8 @@ export const main = async (argv = process.argv, io = createDefaultIO()) => {
1590
1603
  return 0;
1591
1604
  }
1592
1605
  catch (error) {
1606
+ if (error instanceof RenderedCommandError)
1607
+ return 1;
1593
1608
  if (error instanceof CommanderError) {
1594
1609
  return error.exitCode;
1595
1610
  }
@@ -0,0 +1,6 @@
1
+ import { Command } from 'commander';
2
+ import { type CliIO } from '../io.js';
3
+ import { type ToolContext } from '../toolSurface.js';
4
+ export declare class RenderedCommandError extends Error {
5
+ }
6
+ export declare const registerCapabilities: (program: Command, io: CliIO, context?: (apiUrl?: string) => Promise<ToolContext>) => void;
@@ -0,0 +1,288 @@
1
+ import { Command, Option } from 'commander';
2
+ import { applyTool } from '../actions.js';
3
+ import { ApiError } from '../api.js';
4
+ import { promptConfirm, writeLine } from '../io.js';
5
+ import { ManifestError, resolveBinding } from '../manifest.js';
6
+ import { asRecord, callTool, resolveToolContext } from '../toolSurface.js';
7
+ import { FileInputError, readUploadFile, stageLocalFile } from '../upload.js';
8
+ import { capabilities, onboardingOperations, } from './capabilityDefinitions.js';
9
+ export class RenderedCommandError extends Error {
10
+ }
11
+ class InputError extends Error {
12
+ }
13
+ const retryHelp = 'Writes use one idempotency key per operation. Reuse that key/input for retries; never bypass an unknown outcome with a new key. Confirmed calls also require valid confirmation: a fresh invocation re-preflights and may be blocked by changed state or expiry. Inspect current state/jobs when uncertain. Replayed status packs and signed URLs are snapshots, not refreshed reads.';
14
+ const parseJson = (text) => {
15
+ try {
16
+ return JSON.parse(text);
17
+ }
18
+ catch {
19
+ throw new InputError('Expected strict JSON. Quote string values as JSON strings.');
20
+ }
21
+ };
22
+ const inputFor = async (spec, args, options) => {
23
+ const input = {};
24
+ for (const [index, name] of (spec.argument?.split(' ') ?? []).entries())
25
+ input[name] = args[index];
26
+ for (const field of spec.fields) {
27
+ const value = options[field.flag.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())];
28
+ if (value === undefined)
29
+ continue;
30
+ if (Object.hasOwn(input, field.field))
31
+ throw new InputError(`Conflicting options for ${field.field}.`);
32
+ let parsed = value;
33
+ if (field.kind === 'clear')
34
+ parsed = null;
35
+ if (field.kind === 'integer') {
36
+ if (typeof value !== 'string' || !/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)))
37
+ throw new InputError(`--${field.flag} requires a nonnegative integer.`);
38
+ parsed = Number(value);
39
+ }
40
+ if (field.kind === 'json') {
41
+ if (typeof value !== 'string')
42
+ throw new InputError('Expected a JSON argument.');
43
+ parsed = parseJson(value);
44
+ }
45
+ if (field.kind === 'file') {
46
+ if (typeof value !== 'string')
47
+ throw new InputError('Expected a regular file path.');
48
+ parsed = parseJson((await readUploadFile(value, 1024 * 1024)).toString('utf8'));
49
+ if (!Array.isArray(parsed))
50
+ throw new InputError(`--${field.flag} requires a JSON array.`);
51
+ }
52
+ input[field.field] = parsed;
53
+ }
54
+ if (spec.path === 'access entry bulk-update')
55
+ input.operation = 'update';
56
+ return input;
57
+ };
58
+ const partialFailure = (value, depth = 0) => {
59
+ if (depth > 32 || !value || typeof value !== 'object')
60
+ return false;
61
+ if (Array.isArray(value))
62
+ return value.some((item) => partialFailure(item, depth + 1));
63
+ const record = asRecord(value);
64
+ if (['failures', 'failed', 'unmerged'].some((key) => Array.isArray(record[key]) && record[key].length > 0))
65
+ return true;
66
+ return Object.values(record).some((child) => partialFailure(child, depth + 1));
67
+ };
68
+ const execute = async (ctx, tool, input, io, options) => {
69
+ if (tool.effect === 'read') {
70
+ if (options.yes || options.idempotencyKey !== undefined)
71
+ throw new InputError('Write controls do not apply to a read operation.');
72
+ return callTool(ctx, tool.name, input);
73
+ }
74
+ if (tool.confirmation === 'none' && options.yes)
75
+ throw new InputError('This write applies immediately and does not accept --yes.');
76
+ if (tool.confirmation !== 'none' && !options.yes && (options.json || !io.stdout.isTTY))
77
+ throw new InputError('This confirmed write requires --yes in JSON or non-interactive mode. No change was applied.');
78
+ return applyTool({
79
+ ctx,
80
+ io,
81
+ quiet: options.json === true,
82
+ toolName: tool.name,
83
+ input,
84
+ fallback: 'The operation could not be completed.',
85
+ yes: options.yes === true,
86
+ ...(typeof options.idempotencyKey === 'string' && { idempotencyKey: options.idempotencyKey }),
87
+ confirm: (question) => promptConfirm(io, question),
88
+ });
89
+ };
90
+ const render = (io, json, envelope, next) => {
91
+ if (json)
92
+ writeLine(io.stdout, JSON.stringify(envelope));
93
+ else {
94
+ writeLine(io.stdout, JSON.stringify(envelope.data ?? envelope, null, 2));
95
+ if (Array.isArray(envelope.warnings))
96
+ for (const warning of envelope.warnings)
97
+ writeLine(io.stdout, `warning: ${String(warning)}`);
98
+ if (asRecord(envelope.meta).truncated)
99
+ writeLine(io.stdout, 'Results are truncated; inspect continuation details.');
100
+ if (next)
101
+ writeLine(io.stdout, `next: ciphrix ${next} --help`);
102
+ }
103
+ };
104
+ const run = async (io, options, next, action) => {
105
+ let failed;
106
+ let applying = false;
107
+ try {
108
+ const key = options.idempotencyKey;
109
+ if (key !== undefined && (typeof key !== 'string' || !key.trim() || key.length > 256)) {
110
+ throw new InputError('The idempotency key must contain 1 to 256 characters.');
111
+ }
112
+ const envelope = await action(() => {
113
+ applying = true;
114
+ });
115
+ if (!envelope) {
116
+ if (options.json)
117
+ writeLine(io.stdout, JSON.stringify({ status: 'cancelled' }));
118
+ return;
119
+ }
120
+ render(io, options.json === true, envelope, next);
121
+ failed = envelope.status !== 'ok' || (applying && partialFailure(envelope.data));
122
+ if (failed && !options.json)
123
+ writeLine(io.stderr, 'The operation failed or partially applied. Inspect the returned outcomes before taking further action.');
124
+ }
125
+ catch (error) {
126
+ const envelope = error instanceof ApiError && asRecord(error.envelope).status === 'error'
127
+ ? asRecord(error.envelope)
128
+ : {
129
+ status: 'error',
130
+ error: {
131
+ code: error instanceof InputError || error instanceof FileInputError
132
+ ? 'invalid_input'
133
+ : error instanceof ManifestError
134
+ ? 'unsupported_capability'
135
+ : error instanceof ApiError
136
+ ? (error.code ?? 'http_error')
137
+ : 'transport_error',
138
+ message: error instanceof InputError ||
139
+ error instanceof FileInputError ||
140
+ error instanceof ApiError ||
141
+ error instanceof ManifestError
142
+ ? error.message
143
+ : 'The request could not be completed. Check capabilities and inspect state before retrying a write.',
144
+ ...(error instanceof ApiError && {
145
+ httpStatus: error.status,
146
+ ...(error.details !== undefined && { details: error.details }),
147
+ }),
148
+ },
149
+ };
150
+ render(options.json ? io : { ...io, stdout: io.stderr }, options.json === true, envelope, '');
151
+ failed = true;
152
+ }
153
+ if (failed)
154
+ throw new RenderedCommandError();
155
+ };
156
+ const groupDescriptions = {
157
+ access: 'Access inventory, reviewed imports and assigned reviewer workflows',
158
+ 'access resource': 'Resources, owners and asset relationships',
159
+ 'access entry': 'Revisioned access inventory; inventory edits are not review decisions',
160
+ 'access import': 'Your import proposals, selected-row merges and explicit discard',
161
+ 'access review': 'Review campaigns, assigned decisions, completion and frozen outcomes',
162
+ agent: 'Agent activity, complete proposals and explicit human decisions',
163
+ 'agent run': 'Grouped agent activity; findings are not completed assessments',
164
+ 'agent action': 'Findings, reviewed proposals and declared approve/reject/choose options',
165
+ task: 'Your assigned stored work; discovery never completes tasks',
166
+ file: 'Owned local-file staging and explicit processing; no automatic downloads',
167
+ };
168
+ const parent = (program, path) => {
169
+ let current = program;
170
+ const traversed = [];
171
+ for (const segment of path) {
172
+ traversed.push(segment);
173
+ current =
174
+ current.commands.find((command) => command.name() === segment) ??
175
+ current
176
+ .command(segment)
177
+ .description(groupDescriptions[traversed.join(' ')] ?? `${segment} capabilities`);
178
+ }
179
+ return current;
180
+ };
181
+ const common = (command) => {
182
+ command
183
+ .option('--api-url <url>', 'override the API base URL')
184
+ .option('--json', 'emit one complete result/error envelope')
185
+ .option('--yes', 'confirmed writes only: explicitly authorize without prompting; rejected on reads/immediate writes')
186
+ .option('--idempotency-key <key>', 'writes only: key for one logical operation; reuse identical input, at most 256 characters')
187
+ .addHelpText('after', `\n${retryHelp}\n`);
188
+ };
189
+ const addField = (command, field) => {
190
+ const flag = `--${field.flag}${['boolean', 'clear'].includes(field.kind) ? '' : ' <value>'}`;
191
+ const option = new Option(flag, field.help);
192
+ if (field.required)
193
+ option.makeOptionMandatory();
194
+ if (field.kind === 'repeat')
195
+ option.argParser((value, previous = []) => [...previous, value]);
196
+ command.addOption(option);
197
+ };
198
+ export const registerCapabilities = (program, io, context = resolveToolContext) => {
199
+ for (const spec of capabilities) {
200
+ const segments = spec.path.split(' ');
201
+ const operation = segments.pop();
202
+ const command = parent(program, segments)
203
+ .command(operation +
204
+ (spec.argument
205
+ ? ` ${spec.argument
206
+ .split(' ')
207
+ .map((name) => `<${name}>`)
208
+ .join(' ')}`
209
+ : ''))
210
+ .description(spec.description);
211
+ for (const field of spec.fields)
212
+ addField(command, field);
213
+ if (spec.path === 'access entry bulk-update')
214
+ command.addHelpText('after', '\nRows: [{accessId, expectedRevision}]. Only update is exposed. field selects identity, personId, role or status; value is a JSON string, or null to clear personId. Each row can fail independently.\n');
215
+ if (spec.path === 'access import merge')
216
+ command.addHelpText('after', '\nRows require proposal_row_id. Optional reviewed corrections: resource_id, resource_name, website, identity, role, person_id, resource_owner_person_id, confirm_person_match, confirm_visual_evidence, expected_revision, access_id. Preserve snake_case. Never infer match/evidence confirmations or merge all rows implicitly. Read the proposal before selecting rows.\n');
217
+ if (spec.path === 'access review decide')
218
+ command.addHelpText('after', '\nItems: [{accessId, expectedRevision, decision, comment?}]. decision is keep or revoke; comment may be null. Revoke records a removal request, not provider revocation. Complete the assigned task separately after all decisions.\n');
219
+ common(command);
220
+ command.action(async (...values) => {
221
+ const options = asRecord(values[values.length - 2]);
222
+ await run(io, options, spec.next, async (write) => {
223
+ const input = await inputFor(spec, values.slice(0, -2), options);
224
+ const ctx = await context(typeof options.apiUrl === 'string' ? options.apiUrl : undefined);
225
+ const tool = await resolveBinding(ctx, segments, operation);
226
+ if (tool.effect === 'write')
227
+ write();
228
+ if (spec.path === 'file stage') {
229
+ if (tool.effect !== 'write' || tool.confirmation !== 'none' || options.yes)
230
+ throw new InputError('Staging requires an immediate write capability and does not accept --yes.');
231
+ const advertised = Number(tool.limits?.maxFileBytes);
232
+ const staged = await stageLocalFile(ctx, String(input.path), String(input.purpose), {
233
+ maxUploadBytes: Number.isSafeInteger(advertised) && advertised > 0
234
+ ? Math.min(advertised, 25 * 1024 * 1024)
235
+ : 25 * 1024 * 1024,
236
+ }, tool, typeof options.idempotencyKey === 'string' ? options.idempotencyKey : undefined);
237
+ return staged.envelope;
238
+ }
239
+ return execute(ctx, tool, input, io, options);
240
+ });
241
+ });
242
+ }
243
+ const onboarding = program
244
+ .command('onboarding')
245
+ .description('Learn organizational context, inspect optional setup and explicitly complete the journey. Operating Context does not block completion.');
246
+ for (const [operation, description] of onboardingOperations)
247
+ onboarding.option(`--${operation}${['get-context', 'set-context', 'init-framework'].includes(operation) ? ' <value>' : ''}`, description);
248
+ onboarding
249
+ .option('--question <id>', 'questionId from the selected schema')
250
+ .option('--value <json>', 'strict JSON answer, including quoted JSON strings')
251
+ .option('--additional-context <text>', 'answer explanation; explanation-only answers are supported');
252
+ common(onboarding);
253
+ onboarding.action(async (options) => {
254
+ await run(io, options, 'onboarding --status', async (write) => {
255
+ const selected = onboardingOperations.filter(([operation]) => options[operation.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())] !== undefined);
256
+ if (selected.length !== 1)
257
+ throw new InputError('Select exactly one onboarding operation; use --help for syntax.');
258
+ const operation = selected[0][0];
259
+ const input = {};
260
+ if (operation === 'get-context' || operation === 'set-context')
261
+ input.store = options[operation === 'get-context' ? 'getContext' : 'setContext'];
262
+ if (operation === 'set-context') {
263
+ if (typeof options.question !== 'string' ||
264
+ (options.value === undefined && options.additionalContext === undefined))
265
+ throw new InputError('Set context requires --question and --value and/or --additional-context.');
266
+ input.questionId = options.question;
267
+ if (options.value !== undefined) {
268
+ if (typeof options.value !== 'string')
269
+ throw new InputError('Expected a JSON answer.');
270
+ input.value = parseJson(options.value);
271
+ }
272
+ if (options.additionalContext !== undefined)
273
+ input.additionalContext = options.additionalContext;
274
+ }
275
+ else if (options.question !== undefined ||
276
+ options.value !== undefined ||
277
+ options.additionalContext !== undefined)
278
+ throw new InputError('Answer options require --set-context.');
279
+ if (operation === 'init-framework')
280
+ input.frameworkCode = options.initFramework;
281
+ const ctx = await context(typeof options.apiUrl === 'string' ? options.apiUrl : undefined);
282
+ const tool = await resolveBinding(ctx, ['onboarding'], operation === 'help-onboarding' ? 'help' : operation);
283
+ if (tool.effect === 'write')
284
+ write();
285
+ return execute(ctx, tool, input, io, options);
286
+ });
287
+ });
288
+ };
@@ -0,0 +1,17 @@
1
+ export type FieldKind = 'text' | 'integer' | 'boolean' | 'repeat' | 'json' | 'file' | 'clear';
2
+ export interface Field {
3
+ flag: string;
4
+ field: string;
5
+ help: string;
6
+ kind: FieldKind;
7
+ required: boolean;
8
+ }
9
+ export interface Capability {
10
+ path: string;
11
+ description: string;
12
+ argument?: string;
13
+ fields: Field[];
14
+ next: string;
15
+ }
16
+ export declare const capabilities: Capability[];
17
+ export declare const onboardingOperations: readonly [readonly ["status", "read current journey and separate context readiness"], readonly ["help-onboarding", "read authenticated server help, all options and current status"], readonly ["get-context", "read business/operating schema and answers"], readonly ["set-context", "save one agreed business/operating answer without completing the journey"], readonly ["run-vendor-agent", "start vendor discovery; may create vendors; requires Business Context 100%"], readonly ["run-risk-agent", "start initial risk generation; requires Business Context 100%"], readonly ["init-framework", "initialize a selected framework; may consume quota and notify administrators"], readonly ["complete", "explicitly complete with Business Context 100%; operating/setup remain optional"]];