@commercelayer/agent-cli 1.0.0-dev.16 → 1.0.0-dev.18

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
@@ -76,6 +76,7 @@ Destructive verbs (deletes, `orders cancel`, `payments void` and `refund`, `sand
76
76
 
77
77
  - Without a login, nothing is checked on this machine: the request goes without a token, and the API's answer (`login_required`, or `state_required` for an organization not claimed) comes through as it is. So do `live_unavailable` (a plan without live), `forbidden` (the login is not an active admin of it) and `not_supported` for a verb that comes with a later step of go-live.
78
78
  - A verb that never runs live (sandbox, plans, sales calls, `auth login`, `auth switch`, `shop`) stops on this machine with `state_required`.
79
+ - `c11r events writes --live --org <slug>` (`--since 2026-11-12T00:00:00Z`, an ISO time, `--noun`, `--cursor`, `--limit`) is the audit of live writes, and runs only in live: without `--live` it is `missing_value` with the full command, before any call.
79
80
  - `c11r status --live --org <slug>` is the organization's live status, with `live_credential` and the warning `live_credential_held` while the API holds one.
80
81
  - `c11r auth logout --live --org <slug>` revokes that credential in Commerce Layer (`DELETE /v1/auth/live`): `{ org, revoked, credential }`. The login stays on this machine. `--dry-run` says what it would revoke.
81
82
  - A verb that needs a yes is confirmed by the API in live, with a sentence naming the action, the amount and the mode (`Refunds EUR 1.00 on order N, in LIVE mode, real money.`): the CLI sends it without `confirm=true` and shows that `confirmation_required` as it is, with the API's next command, or asks it with those words in a terminal. `--yes` sends `confirm=true` on every live write, `payments capture` included.
@@ -32,9 +32,10 @@ export const organizations = {
32
32
  const req = resourceRequest('organizations', run.verb, run.parsed.id, run.parsed.values);
33
33
  // Ending every other token of the organization is confirmed: by the schema's confirm_if, or
34
34
  // here for a schema without it.
35
- if (run.verb.name === 'connect' && run.parsed.values.revoke_others && !run.globals.dryRun) {
36
- if (!run.verb.confirmIf.includes('revoke_others'))
37
- await confirm(`Connect ${run.parsed.id} and end every other organization token of it`, run.line, { yes: run.globals.yes, prompter: run.prompter ?? terminal });
35
+ // With confirm_if the engine has it confirmed, by the human here or by the API's 428 with its
36
+ // sentence; only a schema without it is confirmed here, and then sends confirm=true itself.
37
+ if (run.verb.name === 'connect' && run.parsed.values.revoke_others && !run.globals.dryRun && !run.verb.confirmIf.includes('revoke_others')) {
38
+ await confirm(`Connect ${run.parsed.id} and end every other organization token of it`, run.line, { yes: run.globals.yes, prompter: run.prompter ?? terminal });
38
39
  req.confirm = true;
39
40
  req.body = { ...req.body, confirm: true };
40
41
  }
@@ -33,12 +33,12 @@ export const loops = {
33
33
  description: 'Loops: a skill on a schedule, under a policy written in advance. Definitions are in loops.yml in the project folder; each pass is one c11r loops run.',
34
34
  id: { accepts: ['name'], example: 'orders-15m' },
35
35
  verbs: [
36
- { verb: 'list', method: 'GET', state: 'none', effect: 'Lists the loops of loops.yml.' },
37
- { verb: 'get', method: 'GET', path: '/v1/loops/{id}', state: 'none', effect: 'Returns a loop from loops.yml, with its last pass when the organization has run it.' },
38
- { verb: 'create', method: 'POST', path: '/v1/loops/{id}', state: 'none', body: policyFlags(true), effect: 'Adds a loop to loops.yml, after checking its policy against the skill noun\'s schema. Nothing runs until a pass.' },
39
- { verb: 'update', method: 'PATCH', path: '/v1/loops/{id}', state: 'none', body: policyFlags(false), effect: 'Changes a loop in loops.yml. A flag given replaces what is there: --allow replaces the whole list.' },
40
- { verb: 'delete', method: 'DELETE', path: '/v1/loops/{id}', state: 'none', destructive: true, deletes: true, effect: 'Removes a loop from loops.yml, and its cursor from the organization.' },
41
- { verb: 'run', method: 'POST', path: '/v1/loops/{id}/run', effect: 'One pass: what is waiting since the last pass, and the actions the policy allows. Answers done, waiting (new: true for what appeared since the last pass) and failed. With --dry-run nothing changes and the last pass time stays.' },
36
+ { verb: 'list', live: false, method: 'GET', state: 'none', effect: 'Lists the loops of loops.yml.' },
37
+ { verb: 'get', live: false, method: 'GET', path: '/v1/loops/{id}', state: 'none', effect: 'Returns a loop from loops.yml, with its last pass when the organization has run it.' },
38
+ { verb: 'create', live: false, method: 'POST', path: '/v1/loops/{id}', state: 'none', body: policyFlags(true), effect: 'Adds a loop to loops.yml, after checking its policy against the skill noun\'s schema. Nothing runs until a pass.' },
39
+ { verb: 'update', live: false, method: 'PATCH', path: '/v1/loops/{id}', state: 'none', body: policyFlags(false), effect: 'Changes a loop in loops.yml. A flag given replaces what is there: --allow replaces the whole list.' },
40
+ { verb: 'delete', live: false, method: 'DELETE', path: '/v1/loops/{id}', state: 'none', destructive: true, deletes: true, effect: 'Removes a loop from loops.yml, and its cursor from the organization.' },
41
+ { verb: 'run', live: false, method: 'POST', path: '/v1/loops/{id}/run', effect: 'One pass: what is waiting since the last pass, and the actions the policy allows. Answers done, waiting (new: true for what appeared since the last pass) and failed. With --dry-run nothing changes and the last pass time stays.' },
42
42
  ],
43
43
  }),
44
44
  async run(run) {
package/dist/engine.js CHANGED
@@ -42,6 +42,8 @@ function refusedBeforeEffect(e) {
42
42
  return status >= 400 && status < 500;
43
43
  return e.code === 'unknown_field' || e.code === 'unknown_verb';
44
44
  }
45
+ /** Verbs that only run in live mode. */
46
+ const LIVE_ONLY = new Set(['events writes']);
45
47
  async function executeOnce(argv, opts) {
46
48
  const cmd = parseCommand(argv);
47
49
  const builtins = opts.builtins ?? {};
@@ -87,6 +89,12 @@ async function executeOnce(argv, opts) {
87
89
  if (live && !verb.live) {
88
90
  fail('state_required', `${named} does not run in live mode.`, { required_state: 'claimed', path: 'live', next_step: fixWith(line.replace(/\s--live\b/, '')) });
89
91
  }
92
+ // A verb that only reads live data (events writes, the audit of live writes): without --live the
93
+ // API answers the login a bare 401, so the command says what it needs first.
94
+ if (!live && LIVE_ONLY.has(`${schema.noun} ${verb.name}`)) {
95
+ const org = cmd.globals.org ? '' : ' --org ORG_SLUG';
96
+ fail('missing_value', `${named} reads live data only: it needs --live and the organization, named with --org.`, { path: 'live', next_step: fixWith(`${line}${org} --live`) });
97
+ }
90
98
  // Live mode acts on an organization named on the command, never the folder's link or the
91
99
  // default, which are a sandbox's: --org, or C11R_ORG.
92
100
  const liveOrg = live ? (cmd.globals.org ?? (process.env.C11R_ORG || null)) : null;
@@ -122,7 +130,10 @@ async function executeOnce(argv, opts) {
122
130
  // amount and the mode ("Refunds EUR 1.00 on order N, in LIVE mode, real money."): the CLI sends
123
131
  // without confirm=true and shows that 428 as it is, or asks it in a terminal. --yes sends
124
132
  // confirm=true on every live write, payments capture included.
125
- const apiAsks = live && (verb.confirm || confirmFlags.length > 0);
133
+ // So does a login verb (organizations disconnect), whose sentence names what stops answering.
134
+ const apiAsks = (live || verb.credential === 'login') && (verb.confirm || confirmFlags.length > 0);
135
+ if (apiAsks && cmd.globals.yes && !cmd.globals.dryRun)
136
+ confirmed = true;
126
137
  if (live && cmd.globals.yes && verb.method !== 'GET')
127
138
  confirmed = true;
128
139
  if ((verb.destructive || verb.confirm || confirmFlags.length) && !cmd.globals.dryRun && !apiAsks) {
@@ -156,10 +167,11 @@ async function executeOnce(argv, opts) {
156
167
  }
157
168
  catch (e) {
158
169
  // The API's own confirmation, in live: as it is with no terminal; asked with its words in one.
159
- if (live && !confirmed && e instanceof CliError && e.code === 'confirmation_required') {
170
+ if (apiAsks && !confirmed && e instanceof CliError && e.code === 'confirmation_required') {
160
171
  const p = opts.prompter ?? terminal;
172
+ // Without a command of the API's, the next step is this one again, without --yes.
161
173
  if (!p.interactive)
162
- throw e;
174
+ throw e.body.next_step?.command ? e : new CliError({ ...e.body, next_step: { action: 'fix_request', command: line, url: null } }, e.kind, e.state);
163
175
  await confirm(e.body.message, line, { yes: false, prompter: p, sentence: e.body.message });
164
176
  confirmed = true;
165
177
  return sendOnce(req);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commercelayer/agent-cli",
3
- "version": "1.0.0-dev.16",
3
+ "version": "1.0.0-dev.18",
4
4
  "description": "c11r: the Commerce Layer CLI for coding agents. One grammar, c11r <noun> <verb>, from a sandbox to a running organization.",
5
5
  "homepage": "https://commercelayer.ai",
6
6
  "keywords": [