octwin-cli 0.1.14 → 0.1.15

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 CHANGED
@@ -5,6 +5,41 @@ Format: [Keep a Changelog](https://keepachangelog.com/) — newest first, bucket
5
5
  **Added · Changed · Deprecated · Removed · Fixed · Security**. The platform-wide view lives in the
6
6
  repo root [`CHANGELOG.md`](../../CHANGELOG.md); this file is the CLI-only cut that ships with the package.
7
7
 
8
+ ## [0.1.15] - 2026-07-25
9
+
10
+ ### Added
11
+ - **`octwin agents [packId::agentId] [--prompt]` — see what your agent actually runs as.** The platform
12
+ gained *governed* agent settings: an operator platform default can override the `default_model`
13
+ (and history window / working-memory toggle) your manifest declares, and that override is invisible
14
+ from the pack source. The command prints each governed setting's **effective** value, **which layer
15
+ won** (project override → platform default → pack manifest), and warns explicitly when your pack's
16
+ declared value is *not* in force. `--prompt` prints the exact system prompt the LLM sees for this
17
+ project (pack instructions + platform protocol + any project overlay).
18
+ - **`octwin orders [reference_id]` — the commerce a conversation produced.** The list (number,
19
+ status/payment, total, contact) or one order with its line items, the
20
+ subtotal/tax/shipping/discount/total breakdown, `payment_ref`, and the allowed transitions. On a
21
+ `pending`/`none` payment it explains *why that's expected*: the forward payment lifecycle is
22
+ **webhook-owned** (not patchable), and the default gateway-less `manual` driver makes
23
+ `payment_request` take its `empty` port — so the flow should confirm pay-on-delivery, not error.
24
+ - **`octwin analytics [entity]` — stage conversion for any pipelined entity.** The funnel engine that
25
+ powered journeys is now generic, so any entity declared with a `pipeline:` charts conversion +
26
+ drop-off (`--overview` / `--milestones` / `--trends` / `--cost`, and `--stage <id>` for the records
27
+ currently at a stage). An empty result names **both** causes the platform folds into one response —
28
+ no `pipeline:`, or no `view` grant on `record.<entity>`.
29
+ - **`octwin catalog [--readiness]`** — commerce products with price, availability and stock
30
+ (`untracked` when the SKU isn't inventory-tracked), plus the WhatsApp catalog binding.
31
+ `--readiness` runs the Meta Graph checklist with each failing item's suggested fix.
32
+ - **`octwin scheduling [--slots <resourceRecordId>]`** — the engine state (bookable resource types,
33
+ upcoming slots, booked seats) or the computed slots for one resource. This is how you verify the
34
+ availability rules a `deploy --seed` created, which 0.1.14 could create but not read back.
35
+
36
+ ### Changed
37
+ - **Auth failures now name the scope the command needs.** Every 401/403 appends the required scope
38
+ (and the plan feature, where the route is entitlement-gated) from one client-side mirror of the
39
+ platform's scope registry — replacing the per-command hardcoded strings, several of which named no
40
+ scope at all. It also spells out that `pack:deploy` / `media:generate` are direct-grant only, so a
41
+ `tenant:admin` preset token does **not** confer them (the most common "but my token is admin" dead end).
42
+
8
43
  ## [0.1.14] - 2026-07-23
9
44
 
10
45
  ### Fixed
package/README.md CHANGED
@@ -70,6 +70,11 @@ octwin status # "✓ live and current" once it's warm
70
70
  | `octwin logs` | List recent conversations (handle, status, last activity; `--as` filters), or show one conversation's full event timeline — including what each turn rendered. `--json` for raw payloads. |
71
71
  | `octwin records` | Inspect the pack's XRM data (needs a `records:read` token). No args = list entities. |
72
72
  | `octwin cases` | Inspect casework (support tickets): the inbox, one case + its timeline and decisions, or `--queues` for queue keys + open counts. |
73
+ | `octwin agents` | The agent roster with each agent's **effective** model / history window and **which layer set it** (project override → platform default → pack manifest) — an operator platform default can override what your manifest declares. `--prompt` prints the exact system prompt the LLM sees. Needs `agents:read`. |
74
+ | `octwin orders` | The orders a conversation produced. No args = the list; with a `reference_id` = line items, the subtotal/tax/shipping/discount/total breakdown, `payment_ref`, and the allowed transitions. Needs `orders:read` + the `orders` plan feature. |
75
+ | `octwin analytics` | Stage-by-stage conversion for **any** entity declared with a `pipeline:` (`--overview` / `--milestones` / `--trends` / `--cost`; `--stage <id>` lists the records currently at a stage). Needs `records:read`. |
76
+ | `octwin catalog` | Commerce products with price / availability / stock, plus the WhatsApp catalog binding. `--readiness` runs the Meta Graph checklist. Needs `catalog:read` + the `catalog` plan feature. |
77
+ | `octwin scheduling` | The scheduling engine's state, or `--slots <resourceRecordId>` for the slots one bookable resource actually computes — how you verify the availability rules `deploy --seed` created. Needs `scheduling:read`. |
73
78
  | `octwin platform-kb pull` | Pull the platform's capability reference (built-ins, primitives, render intents, flow-DSL — as markdown + JSON) into `.octwin/platform-kb/`, for the **`octwin-pack`** Claude Code authoring plugin to consult. |
74
79
  | `octwin test` | Alias for `octwin validate --remote` — the platform's full manifest + flow-DSL check. |
75
80
  | `octwin help` | Show usage. Every subcommand also answers `--help`. |
@@ -90,6 +95,30 @@ octwin logs --as tester1 # find the conversation, then:
90
95
  octwin logs <conversationId> # the full timeline (taps, renders, tool events)
91
96
  ```
92
97
 
98
+ ### Reading back the state your pack created
99
+
100
+ `chat`/`logs` show what the bot *said*; these show what it *did*. A 401/403 on any of them names the
101
+ token scope (and plan feature) that command needs, so you can mint a wider token instead of guessing.
102
+
103
+ ```bash
104
+ octwin agents # effective model per agent + which layer set it
105
+ octwin agents my-pack::assistant --prompt # the exact system prompt the LLM sees
106
+ octwin orders # then: octwin orders <reference_id>
107
+ octwin analytics # then: octwin analytics <entity> [--stage <id>]
108
+ octwin catalog # products + stock + the WhatsApp binding
109
+ octwin scheduling --slots <resourceRecordId> # the slots your availability rules compute
110
+ ```
111
+
112
+ Three things worth knowing when you read the output:
113
+
114
+ - **A `pending` payment is usually correct.** The forward payment lifecycle is **webhook-owned**, and a
115
+ workspace with no gateway runs the credential-free `manual` driver — so `payment_request` takes its
116
+ `empty` port and your flow should confirm pay-on-delivery. `octwin orders <ref>` says this inline.
117
+ - **Your declared model may not be the one running.** An operator platform default overrides the pack
118
+ manifest; `octwin agents` is where that becomes visible.
119
+ - **An empty funnel has two causes** — the entity has no `pipeline:`, or your token's role has no `view`
120
+ grant on `record.<entity>`. The command prints both rather than a bare "no data".
121
+
93
122
  ## Configuration
94
123
 
95
124
  The deploy target has four settings. Three live in a committed **`pack.json`** at the root of your
package/dist/index.js CHANGED
@@ -19,6 +19,11 @@
19
19
  * octwin logs [conversationId] [--as h] [--json] # list conversations / show one's timeline
20
20
  * octwin chat "msg" [--as h] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn via the web channel (+ send media)
21
21
  * octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json] # AI-generate an image → MEDIA- handle (media:generate scope)
22
+ * octwin agents [packId::agentId] [--prompt] # effective model/memory + which layer won; --prompt = the resolved system prompt
23
+ * octwin orders [reference_id] # the orders a conversation produced — money breakdown + payment state (orders:read)
24
+ * octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion, any pipelined entity
25
+ * octwin catalog [--readiness] # commerce products + stock + the WhatsApp catalog binding (catalog:read)
26
+ * octwin scheduling [--slots <resourceRecordId>] # engine state / computed slots (scheduling:read)
22
27
  * octwin platform-kb [pull] [--dir .] # pull the platform capability reference for the authoring skill
23
28
  * octwin test [--dir .] # = validate --remote (the full platform check)
24
29
  *
@@ -99,12 +104,66 @@ function authFailureHint(status, url) {
99
104
  ? `the token was rejected — invalid / expired / revoked. If it JUST worked, this can be a one-off platform hiccup: retry once before re-logging in (octwin login --url ${url} --token oct_…)`
100
105
  : `the token is valid but not authorized here (missing scope, plan feature, or role)`;
101
106
  }
102
- /** Print the auth-scope hint below an HTTP-failure line when it's a 401/403 —
103
- * so every command explains a token problem, not just the inspect family
104
- * (author-feedback A7). No-op for other statuses. */
107
+ /**
108
+ * The scope (and plan feature, where the route is entitlement-gated) each command's
109
+ * endpoints require — a client-side mirror of the platform's scope registry
110
+ * (`src/platform/core/identity/scopes.ts` `SCOPE_REGISTRY`) narrowed to what the CLI
111
+ * calls. A 403 that names the missing scope is the difference between a two-minute
112
+ * fix (mint a wider token) and a support thread.
113
+ *
114
+ * `pack:deploy` and `media:generate` have access `special`: they match by DIRECT
115
+ * grant only, so even a `tenant:admin` preset token does NOT confer them
116
+ * (`scopeSatisfies` — the preset branches only reach `:read`/`:write`). That is the
117
+ * single most common "but my token is admin" confusion, hence the explicit note below.
118
+ */
119
+ const COMMAND_REQUIREMENTS = {
120
+ deploy: { scope: 'pack:deploy' },
121
+ validate: { scope: 'pack:deploy' },
122
+ status: { scope: 'pack:deploy' },
123
+ test: { scope: 'pack:deploy' },
124
+ 'platform-kb': { scope: 'pack:deploy' },
125
+ media: { scope: 'media:generate' },
126
+ records: { scope: 'records:read', feature: 'records' },
127
+ analytics: { scope: 'records:read', feature: 'records' },
128
+ cases: { scope: 'cases:read', feature: 'cases' },
129
+ logs: { scope: 'conversations:read' },
130
+ orders: { scope: 'orders:read', feature: 'orders' },
131
+ catalog: { scope: 'catalog:read', feature: 'catalog' },
132
+ scheduling: { scope: 'scheduling:read' },
133
+ agents: { scope: 'agents:read' },
134
+ };
135
+ /** The command currently running — set once in `main()` so any failure printer can
136
+ * name the scope that command needs without threading it through every call. */
137
+ let CURRENT_COMMAND;
138
+ /** `→ needs the \`orders:read\` scope …` — the requirement line for the running
139
+ * command, or '' when the command has no declared requirement. */
140
+ function scopeRequirementHint() {
141
+ const req = CURRENT_COMMAND ? COMMAND_REQUIREMENTS[CURRENT_COMMAND] : undefined;
142
+ if (!req)
143
+ return '';
144
+ const special = req.scope === 'pack:deploy' || req.scope === 'media:generate';
145
+ return `needs the \`${req.scope}\` scope`
146
+ + (special ? ' (granted DIRECTLY only — a `tenant:admin` token does not confer it)' : '')
147
+ + (req.feature ? `, and the \`${req.feature}\` plan feature on this workspace` : '');
148
+ }
149
+ /** Print the auth hints below an HTTP-failure line when it's a 401/403 — so every
150
+ * command explains a token problem, not just the inspect family (author-feedback A7):
151
+ * WHY it failed, then WHAT the command needs. No-op for other statuses. */
105
152
  function printAuthHint(status, url) {
106
- if (status === 401 || status === 403)
107
- console.error(` → ${authFailureHint(status, url)}`);
153
+ if (status !== 401 && status !== 403)
154
+ return;
155
+ console.error(` → ${authFailureHint(status, url)}`);
156
+ const req = scopeRequirementHint();
157
+ if (req)
158
+ console.error(` → ${req}`);
159
+ }
160
+ /** The same two hints folded into ONE line, for the `die(...)` call sites that
161
+ * report an auth failure inline instead of via `printAuthHint`. */
162
+ function authFailureDetail(status, url) {
163
+ if (status !== 401 && status !== 403)
164
+ return '';
165
+ const req = scopeRequirementHint();
166
+ return ` — ${authFailureHint(status, url)}${req ? `; ${req}` : ''}`;
108
167
  }
109
168
  /** Pretty-print a JSON error body (or raw text) for an HTTP failure line. */
110
169
  function errDetail(json) {
@@ -376,7 +435,12 @@ function commandTouchesPlatform(command, flags) {
376
435
  case 'records':
377
436
  case 'cases':
378
437
  case 'logs':
379
- case 'whoami': return true;
438
+ case 'whoami':
439
+ case 'agents':
440
+ case 'orders':
441
+ case 'analytics':
442
+ case 'catalog':
443
+ case 'scheduling': return true;
380
444
  default: return false;
381
445
  }
382
446
  }
@@ -860,15 +924,13 @@ async function cmdRecords(flags) {
860
924
  if (!recordId) {
861
925
  const limit = flags.limit ?? '50';
862
926
  const { status, json } = await apiGet(`${base}/xrm/records?entity=${encodeURIComponent(entity)}&limit=${limit}`, t);
863
- if (status === 403)
864
- die('forbidden — the paged record list needs the `records` plan feature on this tenant');
865
927
  if (status !== 200) {
866
928
  // Always show the server's reason (it names the unknown entity). Cases are
867
929
  // casework (worklist), not pack-declared XRM — point at the right command.
868
930
  if (entity === 'case' || entity === 'cases') {
869
931
  console.error(` '${entity}' is casework (worklist), not a pack-declared XRM entity — inspect tickets with: octwin cases`);
870
932
  }
871
- die(`could not read records (HTTP ${status})${errDetail(json)}${status === 401 ? ` — ${authFailureHint(status, url)}` : ''}`);
933
+ die(`could not read records (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
872
934
  }
873
935
  const rows = (json?.records ?? []);
874
936
  console.log(`${entity}: ${json?.total ?? rows.length} record(s)`);
@@ -879,14 +941,10 @@ async function cmdRecords(flags) {
879
941
  return;
880
942
  }
881
943
  const { status, json } = await apiGet(`${base}/xrm/records/${encodeURIComponent(recordId)}`, t);
882
- if (status === 403)
883
- die('forbidden — mint a token with the `records:read` scope');
884
944
  if (status === 404)
885
945
  die(`record '${recordId}' not found`);
886
- if (status === 401)
887
- die(`could not read record — ${authFailureHint(401, url)}`);
888
946
  if (status !== 200)
889
- die(`could not read record (HTTP ${status})`);
947
+ die(`could not read record (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
890
948
  console.log(JSON.stringify(json?.record ?? json, null, 2));
891
949
  }
892
950
  /** `octwin logs [conversationId] [--as <handle>] [--json]` — list conversations
@@ -904,7 +962,7 @@ async function cmdLogs(flags) {
904
962
  if (!convId) {
905
963
  const { status, json } = await apiGet(`${base}/conversations?limit=50`, t);
906
964
  if (status !== 200)
907
- die(`could not read conversations (HTTP ${status})${errDetail(json)}${authFailureHint(status, url)}`);
965
+ die(`could not read conversations (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
908
966
  let convs = (json?.conversations ?? []);
909
967
  if (asHandle)
910
968
  convs = convs.filter((c) => c.contact?.channel_contact_handle === asHandle);
@@ -930,7 +988,7 @@ async function cmdLogs(flags) {
930
988
  if (status === 404)
931
989
  die(`conversation '${convId}' not found`);
932
990
  if (status !== 200)
933
- die(`could not read conversation (HTTP ${status})${errDetail(json)}${authFailureHint(status, url)}`);
991
+ die(`could not read conversation (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
934
992
  const events = (json?.events ?? []);
935
993
  if (asJson) {
936
994
  console.log(JSON.stringify(events, null, 2));
@@ -1285,9 +1343,11 @@ async function cmdCases(flags) {
1285
1343
  if (!asJson)
1286
1344
  console.log(`→ Reading ${flags.queues === true ? 'case queues' : caseId ? `case ${caseId}` : 'the case inbox'} from ${targetLabel(t)} …`);
1287
1345
  const caseFail = (what, status, json) => {
1346
+ // A 403 here can also be an RBAC gap the scope hint can't see — a role whose
1347
+ // grants don't reach the queue passes the scope gate and still gets nothing.
1288
1348
  if (status === 403)
1289
- die(`forbidden — casework needs the 'cases' plan feature on this tenant, and a role whose grants reach the queue`);
1290
- die(`could not read ${what} (HTTP ${status})${errDetail(json)}${status === 401 ? ` — ${authFailureHint(status, url)}` : ''}`);
1349
+ console.error(' (a role whose grants reach the queue is also required)');
1350
+ die(`could not read ${what} (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
1291
1351
  };
1292
1352
  if (flags.queues === true) {
1293
1353
  const { status, json } = await apiGet(`${base}/case-queues`, t);
@@ -1356,6 +1416,483 @@ async function cmdCases(flags) {
1356
1416
  console.log(` Decisions: ${dispositions.map((d) => `${d.action}${d.next_status ? `→${d.next_status}` : ''}`).join(', ')}`);
1357
1417
  }
1358
1418
  }
1419
+ // ── money formatting (orders / catalog) ─────────────────────────────────────
1420
+ /** Format a MAJOR-unit decimal amount as currency — the catalog's `price` shape.
1421
+ * Mirrors the console's `fmtAmount` (console/src/lib/money.ts) so an amount reads
1422
+ * the same in the terminal as on the page. */
1423
+ function fmtAmount(value, currency) {
1424
+ if (value == null)
1425
+ return '—';
1426
+ if (!currency)
1427
+ return value.toFixed(2);
1428
+ try {
1429
+ return new Intl.NumberFormat('en', { style: 'currency', currency }).format(value);
1430
+ }
1431
+ catch {
1432
+ return `${value.toFixed(2)} ${currency}`;
1433
+ }
1434
+ }
1435
+ /** Format MINOR units (the offset-100 `*_minor` ints every order field is projected
1436
+ * to on the wire — see `toOrderView`) as currency. `fmtAmount(minor / 100, …)`. */
1437
+ function fmtMinor(minor, currency) {
1438
+ if (minor == null)
1439
+ return '—';
1440
+ return fmtAmount(minor / 100, currency);
1441
+ }
1442
+ /** Human wording for a governed `source` — the layer that won the cascade
1443
+ * (per-project override > platform default > pack manifest default). The four
1444
+ * values mirror the platform's `GovernedSource` union exactly. */
1445
+ const GOVERNED_SOURCE_LABEL = {
1446
+ project: 'this project overrides it',
1447
+ platform: 'a PLATFORM default overrides the pack',
1448
+ pack: 'the pack manifest value is in force',
1449
+ unset: 'no value at any layer',
1450
+ };
1451
+ function showValue(v) {
1452
+ if (v === undefined || v === null)
1453
+ return '(unset)';
1454
+ return typeof v === 'string' ? v : JSON.stringify(v);
1455
+ }
1456
+ /** Print one governed setting as `key: <effective> [why]`, and — when the pack's
1457
+ * own declared value is NOT what runs — an explicit second line naming what the
1458
+ * pack asked for. That override is invisible from the pack source, which is the
1459
+ * whole reason this command exists. */
1460
+ function printGoverned(label, g) {
1461
+ if (!g)
1462
+ return;
1463
+ const source = typeof g.source === 'string' ? g.source : '';
1464
+ console.log(` ${label.padEnd(22)} ${showValue(g.effective)} [${GOVERNED_SOURCE_LABEL[source] ?? source ?? '?'}]`);
1465
+ // Warn only when a higher layer displaced the pack's declared value with a
1466
+ // DIFFERENT one. A platform default that happens to equal the pack's value has
1467
+ // changed nothing an author needs to know about.
1468
+ const displaced = (source === 'project' || source === 'platform')
1469
+ && g.pack_default !== undefined
1470
+ && JSON.stringify(g.pack_default) !== JSON.stringify(g.effective);
1471
+ if (displaced)
1472
+ console.log(` ⚠ your pack declares ${showValue(g.pack_default)} — it is NOT in force`);
1473
+ if (g.opted_out)
1474
+ console.log(' (this workspace is opted OUT of the platform default for this key)');
1475
+ }
1476
+ /** `octwin agents [agentRef] [--prompt] [--json]` — the agent roster with the
1477
+ * EFFECTIVE model/memory settings and which layer won, plus (`--prompt`) the exact
1478
+ * system prompt the LLM sees for this project. Needs an `agents:read` token. */
1479
+ async function cmdAgents(flags) {
1480
+ const packDir = resolve(flags.dir ?? '.');
1481
+ const t = resolveTarget(flags, packDir);
1482
+ const { url } = t;
1483
+ const base = `${url}/api/self/p/agents`;
1484
+ const ref = flags._[0];
1485
+ const asJson = flags.json === true;
1486
+ const wantPrompt = flags.prompt === true;
1487
+ if (wantPrompt && !ref)
1488
+ die('usage: octwin agents <packId::agentId> --prompt (name the agent — `octwin agents` lists them)');
1489
+ if (!asJson)
1490
+ console.log(`→ Reading ${ref ? `agent ${ref}` : 'the agent roster'} from ${targetLabel(t)} …`);
1491
+ // --prompt — the resolved system prompt (pack instructions + platform protocol +
1492
+ // the per-project overlay). No LLM call; pure resolution server-side.
1493
+ if (wantPrompt) {
1494
+ const { status, json } = await apiGet(`${base}/${encodeURIComponent(ref)}/preview-prompt`, t);
1495
+ if (status === 404)
1496
+ die(`agent '${ref}' not found — run \`octwin agents\` for the roster`);
1497
+ // 503 = the pack is installed but not warm on this instance yet (same trap
1498
+ // `octwin status` explains): the agent loads on the next inbound.
1499
+ if (status === 503)
1500
+ die(`'${ref}' is not registered with Mastra on the instance you hit yet — it loads on the next inbound (chat once, then retry)`);
1501
+ if (status !== 200)
1502
+ die(`could not resolve the prompt (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
1503
+ if (asJson) {
1504
+ console.log(JSON.stringify(json, null, 2));
1505
+ return;
1506
+ }
1507
+ console.log(`Resolved system prompt for ${json?.agent?.pack_id}::${json?.agent?.pack_agent_id} (${json?.bytes ?? '?'} bytes`
1508
+ + `${json?.has_overlay ? ', includes this project\'s overlay' : ', no project overlay'}):\n`);
1509
+ console.log(json?.resolved_prompt ?? '(empty)');
1510
+ return;
1511
+ }
1512
+ if (!ref) {
1513
+ const { status, json } = await apiGet(base, t);
1514
+ if (status !== 200)
1515
+ die(`could not read agents (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
1516
+ if (asJson) {
1517
+ console.log(JSON.stringify(json, null, 2));
1518
+ return;
1519
+ }
1520
+ const agents = (json?.agents ?? []);
1521
+ if (agents.length === 0) {
1522
+ console.log('No agents — is a pack installed on this project? (`octwin status`)');
1523
+ return;
1524
+ }
1525
+ console.log(`Agents in ${targetLabel(t)}:`);
1526
+ for (const a of agents) {
1527
+ console.log(` ${a.pack_id}::${a.pack_agent_id} "${a.display_name}"${a.enabled === false ? ' [DISABLED]' : ''}`);
1528
+ printGoverned('model', a.governed?.model);
1529
+ if (a.last_error_at)
1530
+ console.log(` last error: ${a.last_error_at} — ${a.last_error_reason ?? '(no reason)'}`);
1531
+ }
1532
+ console.log('\nOne agent + its full settings: octwin agents <packId::agentId>');
1533
+ console.log('The prompt the LLM sees: octwin agents <packId::agentId> --prompt');
1534
+ return;
1535
+ }
1536
+ const { status, json } = await apiGet(`${base}/${encodeURIComponent(ref)}`, t);
1537
+ if (status === 404)
1538
+ die(`agent '${ref}' not found — run \`octwin agents\` for the roster`);
1539
+ if (status !== 200)
1540
+ die(`could not read agent (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
1541
+ if (asJson) {
1542
+ console.log(JSON.stringify(json, null, 2));
1543
+ return;
1544
+ }
1545
+ console.log(`${json.pack_id}::${json.pack_agent_id} "${json.display_name}"${json.enabled === false ? ' [DISABLED]' : ''}`);
1546
+ console.log(' Governed settings (project override > platform default > pack manifest):');
1547
+ printGoverned('model', json.governed?.model);
1548
+ printGoverned('memory.last_messages', json.governed?.last_messages);
1549
+ printGoverned('working_memory', json.governed?.working_memory_enabled);
1550
+ console.log(` tools: ${(json.available_tools ?? []).join(', ') || '(none)'}`);
1551
+ console.log(` instructions overlay: ${json.instructions_overlay ? `${String(json.instructions_overlay).length} chars (project-specific)` : '(none)'}`);
1552
+ if (json.last_invoked_at)
1553
+ console.log(` last invoked: ${json.last_invoked_at}`);
1554
+ if (json.last_error_at)
1555
+ console.log(` last error: ${json.last_error_at} — ${json.last_error_reason ?? '(no reason)'}`);
1556
+ console.log(`\nThe prompt the LLM actually sees: octwin agents ${ref} --prompt`);
1557
+ }
1558
+ // ── orders: the commerce lifecycle a conversation produces ───────────────────
1559
+ /** Why a `pending` / `none` payment is usually CORRECT, not a bug. The forward
1560
+ * payment lifecycle is webhook-owned (`payment_status` is deliberately not
1561
+ * patchable — only `refund` is an operator move), and the default driver is the
1562
+ * gateway-less `ManualPaymentAdapter`, on which `payment_request` takes its `empty`
1563
+ * port and mutates nothing. Settling a payment headlessly has no path today — see
1564
+ * docs/BACKLOG.md → "Headless payment settle" (a `--settle` flag lands here once
1565
+ * the platform grows a manual-driver-only reconcile endpoint). */
1566
+ function printPaymentNote(paymentStatus) {
1567
+ if (paymentStatus !== 'pending' && paymentStatus !== 'none')
1568
+ return;
1569
+ console.log(` → payment_status '${paymentStatus}' is expected without a live gateway: the forward payment`);
1570
+ console.log(' lifecycle is WEBHOOK-owned (not patchable), and the default `manual` driver has no hosted');
1571
+ console.log(' checkout — so `payment_request` takes its `empty` port and the flow should offer');
1572
+ console.log(' pay-on-delivery. Refund is the one operator-driven move.');
1573
+ }
1574
+ /** `octwin orders [referenceId] [--status s] [--payment p] [--limit n] [--json]` —
1575
+ * the orders a conversation created: money breakdown, payment state, allowed
1576
+ * transitions. Needs an `orders:read` token + the `orders` plan feature. */
1577
+ async function cmdOrders(flags) {
1578
+ const packDir = resolve(flags.dir ?? '.');
1579
+ const t = resolveTarget(flags, packDir);
1580
+ const { url } = t;
1581
+ const base = `${url}/api/self/p/orders`;
1582
+ const referenceId = flags._[0];
1583
+ const asJson = flags.json === true;
1584
+ if (!asJson)
1585
+ console.log(`→ Reading ${referenceId ? `order ${referenceId}` : 'orders'} from ${targetLabel(t)} …`);
1586
+ if (!referenceId) {
1587
+ const q = new URLSearchParams({ limit: flags.limit ?? '50' });
1588
+ if (typeof flags.status === 'string')
1589
+ q.set('status', flags.status);
1590
+ if (typeof flags.payment === 'string')
1591
+ q.set('payment', flags.payment);
1592
+ const { status, json } = await apiGet(`${base}?${q.toString()}`, t);
1593
+ if (status !== 200)
1594
+ die(`could not read orders (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
1595
+ if (asJson) {
1596
+ console.log(JSON.stringify(json, null, 2));
1597
+ return;
1598
+ }
1599
+ const rows = (json?.orders ?? []);
1600
+ console.log(`Orders in ${targetLabel(t)}: ${json?.total ?? rows.length} total`);
1601
+ if (rows.length === 0)
1602
+ console.log(' (none — drive a cart to `cart_submit` with `octwin chat`, or seed demo data)');
1603
+ for (const o of rows) {
1604
+ const who = o.contact?.channel_contact_handle ?? o.contact?.display_name ?? '—';
1605
+ console.log(` #${o.record_number} ${o.status}/${o.payment_status} ${fmtMinor(o.total_minor, o.currency)} ${who} ${o.reference_id}`);
1606
+ }
1607
+ console.log('\nOne order + its money breakdown: octwin orders <reference_id>');
1608
+ return;
1609
+ }
1610
+ const { status, json } = await apiGet(`${base}/${encodeURIComponent(referenceId)}`, t);
1611
+ if (status === 404)
1612
+ die(`order '${referenceId}' not found (pass the opaque reference_id, not the #number)`);
1613
+ if (status !== 200)
1614
+ die(`could not read order (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
1615
+ if (asJson) {
1616
+ console.log(JSON.stringify(json, null, 2));
1617
+ return;
1618
+ }
1619
+ const o = json?.order ?? {};
1620
+ const cur = o.currency;
1621
+ console.log(`Order #${o.record_number} ${o.status} payment: ${o.payment_status}`);
1622
+ console.log(` reference: ${o.reference_id}${o.payment_ref ? ` payment_ref: ${o.payment_ref}` : ''}`);
1623
+ for (const it of (o.items ?? [])) {
1624
+ console.log(` ${it.quantity} × ${it.name ?? it.retailer_id} @ ${fmtMinor(it.amount_minor, it.currency || cur)}`);
1625
+ }
1626
+ console.log(` subtotal ${fmtMinor(o.subtotal_minor, cur)}`
1627
+ + ` tax ${fmtMinor(o.tax_minor, cur)}`
1628
+ + ` shipping ${fmtMinor(o.shipping_minor, cur)}`
1629
+ + ` discount ${fmtMinor(o.discount_minor, cur)}`);
1630
+ console.log(` TOTAL ${fmtMinor(o.total_minor, cur)}`);
1631
+ if (o.note)
1632
+ console.log(` note: ${o.note}`);
1633
+ const transitions = (json?.transitions ?? []);
1634
+ console.log(` allowed transitions: ${transitions.join(', ') || '(none — terminal)'}`);
1635
+ printPaymentNote(String(o.payment_status ?? ''));
1636
+ }
1637
+ // ── analytics: the generic per-entity funnel (any pipelined entity) ──────────
1638
+ const ANALYTICS_MODES = ['funnel', 'overview', 'milestones', 'trends', 'cost'];
1639
+ /** Both "unknown/non-pipelined entity" and "your role lacks the grant" answer
1640
+ * 200 + `has_data:false` (a deliberate empty state, never a 403) — so a bare
1641
+ * "no data" would hide the real cause. Name both. */
1642
+ function printNoAnalyticsData(entity) {
1643
+ console.log(`No analytics for '${entity}'. Either:`);
1644
+ console.log(` • '${entity}' isn't declared with a \`pipeline:\` (a funnel needs stages), or`);
1645
+ console.log(` • your token's role has no \`view\` grant on \`record.${entity}\`.`);
1646
+ console.log('Run `octwin analytics` for the entities that DO carry a pipeline.');
1647
+ }
1648
+ /** `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost]
1649
+ * [--stage <id>] [--json]` — stage conversion over ANY pipelined XRM entity
1650
+ * (orders, carts, cases, bookings, or a pack's own). Needs `records:read`. */
1651
+ async function cmdAnalytics(flags) {
1652
+ const packDir = resolve(flags.dir ?? '.');
1653
+ const t = resolveTarget(flags, packDir);
1654
+ const { url } = t;
1655
+ const base = `${url}/api/self/p/xrm/analytics`;
1656
+ const entity = flags._[0];
1657
+ const asJson = flags.json === true;
1658
+ const stage = typeof flags.stage === 'string' ? flags.stage : undefined;
1659
+ const mode = ANALYTICS_MODES.find(m => flags[m] === true) ?? 'funnel';
1660
+ if (stage && !entity)
1661
+ die('usage: octwin analytics <entity> --stage <stageId> (a stage belongs to an entity)');
1662
+ if (!entity) {
1663
+ if (!asJson)
1664
+ console.log(`→ Reading the analytics entity list from ${targetLabel(t)} …`);
1665
+ const { status, json } = await apiGet(`${base}/entities`, t);
1666
+ if (status !== 200)
1667
+ die(`could not read analytics entities (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
1668
+ if (asJson) {
1669
+ console.log(JSON.stringify(json, null, 2));
1670
+ return;
1671
+ }
1672
+ const ents = (json?.entities ?? []);
1673
+ if (ents.length === 0) {
1674
+ console.log('No pipelined entities visible — a funnel needs an entity declared with a `pipeline:`,');
1675
+ console.log('and your role needs a `view` grant on it. (Journeys have their own analytics surface.)');
1676
+ return;
1677
+ }
1678
+ console.log(`Entities with a funnel in ${targetLabel(t)}:`);
1679
+ for (const e of ents) {
1680
+ console.log(` ${e.entity} (${e.stage_count} stage(s), ${e.milestone_count} milestone(s))`);
1681
+ }
1682
+ console.log('\nStage conversion: octwin analytics <entity> (add --overview / --milestones / --trends / --cost)');
1683
+ console.log('Who is at a stage: octwin analytics <entity> --stage <stageId>');
1684
+ return;
1685
+ }
1686
+ // Stage drill-down — records CURRENTLY at a stage. A live snapshot, deliberately
1687
+ // NOT range-filtered like the funnel's cumulative reached-≥ counts.
1688
+ if (stage) {
1689
+ if (!asJson)
1690
+ console.log(`→ Reading ${entity} records at stage '${stage}' from ${targetLabel(t)} …`);
1691
+ const limit = flags.limit ?? '50';
1692
+ const { status, json } = await apiGet(`${base}/${encodeURIComponent(entity)}/stages/${encodeURIComponent(stage)}/records?limit=${limit}`, t);
1693
+ if (status === 404)
1694
+ die(`unknown stage '${stage}' for '${entity}'${errDetail(json)}`);
1695
+ if (status !== 200)
1696
+ die(`could not read stage records (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
1697
+ if (asJson) {
1698
+ console.log(JSON.stringify(json, null, 2));
1699
+ return;
1700
+ }
1701
+ if (json?.has_data === false) {
1702
+ printNoAnalyticsData(entity);
1703
+ return;
1704
+ }
1705
+ const rows = (json?.records ?? []);
1706
+ console.log(`${entity} at '${stage}' (live snapshot): ${json?.total ?? rows.length} record(s)`);
1707
+ for (const r of rows) {
1708
+ const who = r.channel_contact_handle ?? r.display_name ?? '—';
1709
+ console.log(` #${r.record_number ?? '?'} ${r.title ?? '(untitled)'} ${who}${r.completed ? ' [completed]' : ''} ${r.record_id}`);
1710
+ }
1711
+ return;
1712
+ }
1713
+ if (!asJson)
1714
+ console.log(`→ Reading ${entity} ${mode} from ${targetLabel(t)} …`);
1715
+ const { status, json } = await apiGet(`${base}/${encodeURIComponent(entity)}/${mode}`, t);
1716
+ if (status !== 200)
1717
+ die(`could not read ${mode} (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
1718
+ if (asJson) {
1719
+ console.log(JSON.stringify(json, null, 2));
1720
+ return;
1721
+ }
1722
+ if (json?.has_data === false) {
1723
+ printNoAnalyticsData(entity);
1724
+ return;
1725
+ }
1726
+ const range = json?.range ? ` (${json.range.from} → ${json.range.to})` : '';
1727
+ console.log(`${entity} — ${mode}${range}:`);
1728
+ switch (mode) {
1729
+ case 'funnel':
1730
+ for (const s of (json?.funnel ?? [])) {
1731
+ const conv = s.conversion_from_prev_pct == null ? '' : ` ${s.conversion_from_prev_pct}% of prev`;
1732
+ const lost = s.drop_off_from_prev ? ` (−${s.drop_off_from_prev})` : '';
1733
+ console.log(` ${String(s.rank).padStart(2)}. ${String(s.stage).padEnd(24)} ${String(s.reached).padStart(6)}${conv}${lost}`);
1734
+ }
1735
+ break;
1736
+ case 'overview': {
1737
+ const s = json?.summary ?? {};
1738
+ console.log(` entered ${s.entered} → converted ${s.converted}${s.conversion_pct == null ? '' : ` (${s.conversion_pct}%)`}`);
1739
+ if (s.biggest_dropoff)
1740
+ console.log(` biggest drop-off: ${s.biggest_dropoff.from} → ${s.biggest_dropoff.to} (lost ${s.biggest_dropoff.lost})`);
1741
+ if (s.top_milestone)
1742
+ console.log(` top milestone: ${s.top_milestone.milestone} (${s.top_milestone.completions})`);
1743
+ break;
1744
+ }
1745
+ case 'milestones':
1746
+ for (const m of (json?.milestones ?? [])) {
1747
+ console.log(` ${String(m.milestone).padEnd(28)} ${String(m.completions).padStart(6)} completion(s), ${m.unique_contacts} contact(s)`);
1748
+ }
1749
+ break;
1750
+ case 'trends':
1751
+ for (const b of (json?.buckets ?? [])) {
1752
+ console.log(` ${b.bucket} active ${b.active_contacts} milestones ${b.milestone_completions}`);
1753
+ }
1754
+ for (const c of (json?.cohorts ?? [])) {
1755
+ console.log(` cohort ${c.bucket} entered ${c.entered} converted ${c.converted}`);
1756
+ }
1757
+ break;
1758
+ case 'cost':
1759
+ if (json?.cost_partial)
1760
+ console.log(' ⚠ partial — some token-usage rows carry no cost');
1761
+ for (const r of (json?.by_milestone ?? [])) {
1762
+ console.log(` ${String(r.id).padEnd(28)} ${r.conversations} conv, ${r.total_tokens} tokens, $${r.cost_usd}`);
1763
+ }
1764
+ for (const d of (json?.drivers ?? [])) {
1765
+ console.log(` driver flow=${d.source_flow_id ?? '—'} agent=${d.source_agent_id ?? '—'} conversions ${d.conversions}`);
1766
+ }
1767
+ break;
1768
+ }
1769
+ }
1770
+ // ── catalog: the commerce products + their WhatsApp binding ──────────────────
1771
+ /** `octwin catalog [--readiness] [--json]` — the `product` records a commerce pack
1772
+ * sells, their stock, and the WhatsApp catalog binding. Needs `catalog:read` + the
1773
+ * `catalog` plan feature. */
1774
+ async function cmdCatalog(flags) {
1775
+ const packDir = resolve(flags.dir ?? '.');
1776
+ const t = resolveTarget(flags, packDir);
1777
+ const { url } = t;
1778
+ const base = `${url}/api/self/p/catalog`;
1779
+ const asJson = flags.json === true;
1780
+ // --readiness makes LIVE Meta Graph calls (and 409s with no bound access token),
1781
+ // so it's opt-in rather than part of the default read.
1782
+ if (flags.readiness === true) {
1783
+ if (!asJson)
1784
+ console.log(`→ Checking WhatsApp commerce readiness for ${targetLabel(t)} (live Meta Graph calls) …`);
1785
+ const { status, json } = await apiGet(`${base}/readiness`, t);
1786
+ if (status === 409)
1787
+ die(`no Meta access token on this project's WhatsApp channel — readiness needs one${errDetail(json)}`);
1788
+ if (status !== 200)
1789
+ die(`could not read readiness (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
1790
+ if (asJson) {
1791
+ console.log(JSON.stringify(json, null, 2));
1792
+ return;
1793
+ }
1794
+ const r = json?.readiness ?? {};
1795
+ console.log(`WhatsApp commerce readiness: ${r.summary ?? '?'}`);
1796
+ for (const c of (r.checks ?? [])) {
1797
+ const mark = c.status === 'ok' ? '✓' : c.status === 'warn' ? '⚠' : c.status === 'skip' ? '·' : '✗';
1798
+ console.log(` ${mark} ${c.label}${c.detail ? ` — ${c.detail}` : ''}`);
1799
+ if (c.solution)
1800
+ console.log(` → ${c.solution}`);
1801
+ }
1802
+ return;
1803
+ }
1804
+ if (!asJson)
1805
+ console.log(`→ Reading the product catalog from ${targetLabel(t)} …`);
1806
+ const { status, json } = await apiGet(base, t);
1807
+ if (status !== 200)
1808
+ die(`could not read the catalog (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
1809
+ if (asJson) {
1810
+ console.log(JSON.stringify(json, null, 2));
1811
+ return;
1812
+ }
1813
+ const products = (json?.products ?? []);
1814
+ console.log(`Products in ${targetLabel(t)}: ${products.length}`);
1815
+ if (products.length === 0)
1816
+ console.log(' (none — a commerce pack seeds `product` records, or add them in the console Catalog)');
1817
+ for (const p of products) {
1818
+ // `available: null` = the SKU isn't inventory-tracked (always sellable).
1819
+ const stock = p.available == null ? 'untracked' : `${p.available}`;
1820
+ console.log(` ${String(p.retailer_id).padEnd(20)} ${String(p.name ?? '').padEnd(28)} ${fmtAmount(p.price, p.currency)}`
1821
+ + ` avail=${p.availability} stock=${stock} sync=${p.sync_status ?? '—'}`);
1822
+ }
1823
+ // A binding row can exist with no catalog_id yet (a WABA is configured but no Meta
1824
+ // catalog picked) — that is "not bound" for selling purposes, so say so.
1825
+ const b = json?.binding;
1826
+ console.log(b?.catalog_id
1827
+ ? `\nWhatsApp catalog binding: catalog ${b.catalog_id} (waba ${b.waba_id ?? '—'}), sync=${b.sync_status ?? '—'}, last=${b.last_sync_at ?? 'never'}`
1828
+ : `\nWhatsApp catalog binding: no Meta catalog bound${b?.waba_id ? ` (waba ${b.waba_id} is configured — pick a catalog)` : ''}`
1829
+ + ' — the catalog works web-only (`--readiness` explains what Meta needs).');
1830
+ }
1831
+ // ── scheduling: the availability engine + a slot preview ─────────────────────
1832
+ /** `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]`
1833
+ * — the scheduling engine's state, or the computed slots for one bookable resource
1834
+ * (the verification the `--seed` availability fan-out was missing). `scheduling:read`. */
1835
+ async function cmdScheduling(flags) {
1836
+ const packDir = resolve(flags.dir ?? '.');
1837
+ const t = resolveTarget(flags, packDir);
1838
+ const { url } = t;
1839
+ const base = `${url}/api/self/p/scheduling`;
1840
+ const asJson = flags.json === true;
1841
+ const resourceId = typeof flags.slots === 'string' ? flags.slots : undefined;
1842
+ if (flags.slots === true)
1843
+ die('usage: octwin scheduling --slots <resourceRecordId> (the record id of a bookable resource)');
1844
+ if (resourceId) {
1845
+ const q = new URLSearchParams({ include_booked: '1' }); // full occupancy, as the operator preview does
1846
+ if (typeof flags.from === 'string')
1847
+ q.set('from', flags.from);
1848
+ if (typeof flags.days === 'string')
1849
+ q.set('days', flags.days); // server-clamped to 1–31
1850
+ if (!asJson)
1851
+ console.log(`→ Computing slots for resource ${resourceId} in ${targetLabel(t)} …`);
1852
+ const { status, json } = await apiGet(`${base}/resources/${encodeURIComponent(resourceId)}/slots?${q.toString()}`, t);
1853
+ if (status === 404)
1854
+ die(`resource '${resourceId}' not found (pass an XRM record id — \`octwin records <entity>\` lists them)`);
1855
+ if (status === 400)
1856
+ die(`that record isn't a bookable resource${errDetail(json)}`);
1857
+ if (status !== 200)
1858
+ die(`could not compute slots (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
1859
+ if (asJson) {
1860
+ console.log(JSON.stringify(json, null, 2));
1861
+ return;
1862
+ }
1863
+ if (json?.has_scheduling === false) {
1864
+ console.log('This pack declares no `scheduling.yaml` — nothing to schedule.');
1865
+ return;
1866
+ }
1867
+ const slots = (json?.slots ?? []);
1868
+ console.log(`Slots for ${resourceId} (timezone ${json?.timezone ?? '?'}): ${slots.length}`);
1869
+ if (slots.length === 0)
1870
+ console.log(' (none — no availability rules cover this window; `octwin deploy --seed` seeds the demo rules)');
1871
+ for (const s of slots) {
1872
+ console.log(` ${s.slot_start} → ${s.slot_end} ${s.remaining}/${s.capacity} free`);
1873
+ }
1874
+ return;
1875
+ }
1876
+ if (!asJson)
1877
+ console.log(`→ Reading the scheduling engine state from ${targetLabel(t)} …`);
1878
+ const { status, json } = await apiGet(base, t);
1879
+ if (status !== 200)
1880
+ die(`could not read scheduling (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
1881
+ if (asJson) {
1882
+ console.log(JSON.stringify(json, null, 2));
1883
+ return;
1884
+ }
1885
+ if (json?.has_scheduling === false) {
1886
+ console.log('This pack declares no `scheduling.yaml` — nothing to schedule.');
1887
+ return;
1888
+ }
1889
+ console.log(`Scheduling in ${targetLabel(t)} — bookings land as '${json?.booking_entity}'`);
1890
+ for (const rt of (json?.resource_types ?? [])) {
1891
+ console.log(` ${rt.entity} ${rt.resources} resource(s), ${rt.window_days}-day booking window`);
1892
+ }
1893
+ console.log(` upcoming slots: ${json?.upcoming_slots ?? 0} booked seats: ${json?.booked_seats ?? 0}`);
1894
+ console.log('\nSlots for one resource: octwin scheduling --slots <resourceRecordId> (ids: octwin records <entity>)');
1895
+ }
1359
1896
  function help() {
1360
1897
  console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
1361
1898
 
@@ -1371,6 +1908,11 @@ function help() {
1371
1908
  octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
1372
1909
  octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
1373
1910
  octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
1911
+ octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
1912
+ octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
1913
+ octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
1914
+ octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
1915
+ octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
1374
1916
  octwin platform-kb [pull] [--dir .] [--url <url>] [--tenant <slug>] [--token <t>]
1375
1917
  octwin test [--dir .] # = validate --remote (the full platform check)
1376
1918
 
@@ -1423,6 +1965,35 @@ const COMMAND_HELP = {
1423
1965
  public asset, and print its MEDIA- handle + serve URL. --out downloads the
1424
1966
  bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
1425
1967
  width, height, bytes }. Pair with 'octwin chat --media' to drive media flows.`,
1968
+ agents: `octwin agents [packId::agentId] [--prompt] [--json]
1969
+ No args = the roster with each agent's EFFECTIVE model and which layer set it.
1970
+ With an agent = every governed setting (model / memory.last_messages /
1971
+ working_memory) plus the layer that won — an operator PLATFORM default can
1972
+ override what your manifest declares, and this is where you see that.
1973
+ --prompt = the exact system prompt the LLM sees for this project (pack
1974
+ instructions + platform protocol + any project overlay). Needs agents:read.
1975
+ The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.`,
1976
+ orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
1977
+ No args = the order list (#number, status/payment, total, contact). With a
1978
+ reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
1979
+ payment_ref, and the allowed status transitions. Needs orders:read + the
1980
+ \`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
1981
+ so \`pending\` on a gateway-less workspace is expected, not a bug.`,
1982
+ analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
1983
+ No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
1984
+ With an entity = stage-by-stage conversion (default --funnel) over the last 30
1985
+ days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
1986
+ range-filtered). Needs records:read + a \`view\` grant on \`record.<entity>\`.`,
1987
+ catalog: `octwin catalog [--readiness] [--json]
1988
+ The commerce \`product\` records + price, availability, stock (null = not
1989
+ inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
1990
+ Graph checklist (LIVE Graph calls; needs a bound access token). Needs
1991
+ catalog:read + the \`catalog\` plan feature.`,
1992
+ scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
1993
+ No args = the engine state (bookable resource types, upcoming slots, booked
1994
+ seats). --slots <recordId> computes the slots for one bookable resource
1995
+ (occupancy included; --days is clamped to 1-31 server-side) — the way to verify
1996
+ the availability rules a \`deploy --seed\` created. Needs scheduling:read.`,
1426
1997
  'platform-kb': `octwin platform-kb [pull] [--dir .] [--url <url>] [--tenant <slug>] [--token <t>]
1427
1998
  Pull the platform capability reference (markdown + JSON catalogs) into
1428
1999
  .octwin/platform-kb/ for the octwin-pack authoring skill.`,
@@ -1432,6 +2003,7 @@ const COMMAND_HELP = {
1432
2003
  async function main() {
1433
2004
  const [command, ...rest] = process.argv.slice(2);
1434
2005
  const flags = parseFlags(rest);
2006
+ CURRENT_COMMAND = command; // so an auth failure can name the scope THIS command needs
1435
2007
  // Per-subcommand --help/-h — intercepted BEFORE the command runs, so help can
1436
2008
  // never hit the network or die on auth (author-feedback A8).
1437
2009
  if (command && command in COMMAND_HELP && (flags.help === true || flags._.includes('-h'))) {
@@ -1472,6 +2044,21 @@ async function main() {
1472
2044
  case 'media':
1473
2045
  await cmdMedia(flags);
1474
2046
  break;
2047
+ case 'agents':
2048
+ await cmdAgents(flags);
2049
+ break;
2050
+ case 'orders':
2051
+ await cmdOrders(flags);
2052
+ break;
2053
+ case 'analytics':
2054
+ await cmdAnalytics(flags);
2055
+ break;
2056
+ case 'catalog':
2057
+ await cmdCatalog(flags);
2058
+ break;
2059
+ case 'scheduling':
2060
+ await cmdScheduling(flags);
2061
+ break;
1475
2062
  case 'platform-kb':
1476
2063
  await cmdPlatformKb(flags);
1477
2064
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octwin-cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Octwin external-pack developer CLI (by CEQUENS) — scaffold, validate, deploy, and check pure-YAML packs on your tenant.",
5
5
  "type": "module",
6
6
  "bin": {