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 +35 -0
- package/README.md +29 -0
- package/dist/index.js +605 -18
- package/package.json +1 -1
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
|
-
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
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
|
|
107
|
-
|
|
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':
|
|
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)}${
|
|
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)}
|
|
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)}
|
|
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
|
-
|
|
1290
|
-
die(`could not read ${what} (HTTP ${status})${errDetail(json)}${
|
|
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