octwin-cli 0.7.3 → 0.8.1
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 +65 -0
- package/README.md +12 -0
- package/dist/index.js +1207 -297
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
* octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion, any pipelined entity
|
|
29
29
|
* octwin catalog [--readiness] # commerce products + stock + the WhatsApp catalog binding (catalog:read)
|
|
30
30
|
* octwin scheduling [--slots <resourceRecordId>] # engine state / computed slots (scheduling:read)
|
|
31
|
+
* octwin automation [campaigns] # the jobs your declarations produced + health + last result (automation:read)
|
|
32
|
+
* octwin integrations [deliveries|events|preflight|test …] # declared vs configured, and the delivery log (integrations:read)
|
|
33
|
+
* octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] # declared journeys, measured (journeys:read)
|
|
34
|
+
* octwin performance [--detail] # the project's business indicators (records:read — there is no performance scope)
|
|
35
|
+
* octwin usage # model calls, tokens and cost (any valid token; NOT Meta message billing)
|
|
31
36
|
* octwin platform-kb [pull] [--if-stale|--check] [--dir .] # the platform capability reference (no token needed)
|
|
32
37
|
* octwin test [--dir .] # = validate --remote (the full platform check)
|
|
33
38
|
*
|
|
@@ -216,6 +221,21 @@ const VERB_REQUIREMENTS = {
|
|
|
216
221
|
// which does NOT confer this — that 403 is otherwise baffling.
|
|
217
222
|
'projects create': { scope: 'projects:write' },
|
|
218
223
|
'projects rm': { scope: 'projects:write' },
|
|
224
|
+
// Jobs are declaration-derived, so there is no `create` — only acting on one.
|
|
225
|
+
// `campaigns` is absent on purpose: it is a READ sharing the verb slot, and an
|
|
226
|
+
// entry here would print "needs automation:write" on a read failure.
|
|
227
|
+
'automation run': { scope: 'automation:write' },
|
|
228
|
+
'automation pause': { scope: 'automation:write' },
|
|
229
|
+
'automation resume': { scope: 'automation:write' },
|
|
230
|
+
'automation send': { scope: 'automation:write' },
|
|
231
|
+
// `preflight` is the odd one: it is a diagnosis, so the route gates it on
|
|
232
|
+
// `view`, not `act`. Naming the READ scope here is what stops a 403 on it
|
|
233
|
+
// sending the author to mint a write token they do not need.
|
|
234
|
+
'integrations preflight': { scope: 'integrations:read' },
|
|
235
|
+
'integrations test': { scope: 'integrations:write' },
|
|
236
|
+
'integrations retry': { scope: 'integrations:write' },
|
|
237
|
+
'integrations cancel': { scope: 'integrations:write' },
|
|
238
|
+
'integrations send-now': { scope: 'integrations:write' },
|
|
219
239
|
};
|
|
220
240
|
const COMMAND_REQUIREMENTS = {
|
|
221
241
|
deploy: { scope: 'pack:deploy' },
|
|
@@ -250,6 +270,17 @@ const COMMAND_REQUIREMENTS = {
|
|
|
250
270
|
// hint names the scope a NON-deploy token would be missing — a `pack:deploy`
|
|
251
271
|
// holder never sees this line, because they never get the 403.
|
|
252
272
|
projects: { scope: 'projects:read' },
|
|
273
|
+
automation: { scope: 'automation:read' },
|
|
274
|
+
integrations: { scope: 'integrations:read' },
|
|
275
|
+
journeys: { scope: 'journeys:read' },
|
|
276
|
+
// `records:read`, not a `performance:*` scope — there is none. The indicators are
|
|
277
|
+
// derived from record + journey data, and the route is gated accordingly, so the
|
|
278
|
+
// Read-only token preset already reaches this.
|
|
279
|
+
performance: { scope: 'records:read' },
|
|
280
|
+
// `usage` is deliberately absent: its route is `requireTenantAccess` only, so any
|
|
281
|
+
// valid token reaches it. Declaring a requirement would print "needs the X scope"
|
|
282
|
+
// on a failure whose real cause is an unreachable instance — the same reasoning as
|
|
283
|
+
// `platform-kb` above.
|
|
253
284
|
};
|
|
254
285
|
/** The command currently running — set once in `main()` so any failure printer can
|
|
255
286
|
* name the scope that command needs without threading it through every call.
|
|
@@ -556,11 +587,12 @@ function diffKbIndex(prev, next) {
|
|
|
556
587
|
}
|
|
557
588
|
/**
|
|
558
589
|
* Fetch the platform's KB identity (`?meta=1`) — the cheap poll behind both the
|
|
559
|
-
* drift nudge and `--if-stale`. Returns
|
|
590
|
+
* drift nudge and `--if-stale`. Returns not-ok on anything that is not a clean
|
|
560
591
|
* answer; the caller decides whether that is worth a word.
|
|
561
592
|
*
|
|
562
|
-
* `notAuthorized`
|
|
563
|
-
*
|
|
593
|
+
* There is no `notAuthorized` case any more. `kbEndpoint` is anonymous, so this
|
|
594
|
+
* poll cannot be refused for lack of a scope — which used to be its most common
|
|
595
|
+
* failure, and the reason the drift nudge below carried a whole branch of advice.
|
|
564
596
|
*/
|
|
565
597
|
async function fetchKbMeta(t, timeoutMs = 2_000) {
|
|
566
598
|
const ep = kbEndpoint(t);
|
|
@@ -571,12 +603,12 @@ async function fetchKbMeta(t, timeoutMs = 2_000) {
|
|
|
571
603
|
// socket checked out of the pool, and this poll runs on the way to a possible `exitNow` —
|
|
572
604
|
// a held socket at exit is a pending libuv handle.
|
|
573
605
|
await res.arrayBuffer().catch(() => undefined);
|
|
574
|
-
return { ok: false
|
|
606
|
+
return { ok: false };
|
|
575
607
|
}
|
|
576
608
|
return { ok: true, meta: await res.json() };
|
|
577
609
|
}
|
|
578
610
|
catch {
|
|
579
|
-
return { ok: false
|
|
611
|
+
return { ok: false };
|
|
580
612
|
}
|
|
581
613
|
}
|
|
582
614
|
/** Nudge (to stderr) when the platform's capability KB has changed since the last
|
|
@@ -597,24 +629,20 @@ async function kbStaleNotice(flags) {
|
|
|
597
629
|
const local = readLocalKb(packDir);
|
|
598
630
|
if (!local?.content_hash)
|
|
599
631
|
return []; // never pulled → the skill already says to pull
|
|
600
|
-
|
|
601
|
-
|
|
632
|
+
// URL only, deliberately: the KB poll is anonymous, so requiring a token here would
|
|
633
|
+
// silence the nudge for exactly the authors who most need it. This used to call
|
|
634
|
+
// `resolveTargetOrNull` (url AND token) against the tenant-scoped route, which needs
|
|
635
|
+
// `pack:deploy` — so an author inspecting data with a narrow (`records:read`-only)
|
|
636
|
+
// token got NO drift signal at all, silently, and a stale reference is precisely what
|
|
637
|
+
// makes an author invent a primitive from memory. The branch that apologised for that
|
|
638
|
+
// is gone with the cause; every remaining failure (offline, timeout, a platform
|
|
639
|
+
// serving no reference) stays silent, because observing must not break a command.
|
|
640
|
+
const t = readTarget(flags);
|
|
641
|
+
if (!t.url)
|
|
602
642
|
return [];
|
|
603
643
|
const polled = await fetchKbMeta(t);
|
|
604
|
-
if (!polled.ok)
|
|
605
|
-
// The tenant-scoped meta poll needs `pack:deploy`, but this nudge rides on every
|
|
606
|
-
// networked command — so an author inspecting data with a narrow (`records:read`-only)
|
|
607
|
-
// token got NO drift signal at all, silently, and a stale reference is exactly what
|
|
608
|
-
// makes an author invent a primitive from memory. Say so once; stay silent for every
|
|
609
|
-
// other failure (offline, timeout, a platform without the route).
|
|
610
|
-
if (polled.notAuthorized) {
|
|
611
|
-
return [
|
|
612
|
-
'\nⓘ can\'t check whether the platform capability reference drifted — that token lacks `pack:deploy`.',
|
|
613
|
-
' Check it without a token: octwin platform-kb --check (or refresh: octwin platform-kb --token oct_…)',
|
|
614
|
-
];
|
|
615
|
-
}
|
|
644
|
+
if (!polled.ok)
|
|
616
645
|
return [];
|
|
617
|
-
}
|
|
618
646
|
const meta = polled.meta;
|
|
619
647
|
if (meta.content_hash && meta.content_hash !== local.content_hash) {
|
|
620
648
|
// Per-entry summary (now that the index carries per-entry hashes) — the
|
|
@@ -791,6 +819,11 @@ function commandTouchesPlatform(command, flags) {
|
|
|
791
819
|
case 'analytics':
|
|
792
820
|
case 'catalog':
|
|
793
821
|
case 'scheduling':
|
|
822
|
+
case 'automation':
|
|
823
|
+
case 'integrations':
|
|
824
|
+
case 'journeys':
|
|
825
|
+
case 'performance':
|
|
826
|
+
case 'usage':
|
|
794
827
|
case 'projects':
|
|
795
828
|
case 'seed': return true;
|
|
796
829
|
default: return false;
|
|
@@ -1171,20 +1204,32 @@ function resolveTargetOrNull(flags) {
|
|
|
1171
1204
|
/**
|
|
1172
1205
|
* Where to read the platform capability reference from, and how.
|
|
1173
1206
|
*
|
|
1174
|
-
* The KB is tenant-independent platform stdlib, and
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1177
|
-
*
|
|
1178
|
-
*
|
|
1207
|
+
* ALWAYS the anonymous route. The KB is tenant-independent platform stdlib, and
|
|
1208
|
+
* the platform serves the SAME bundle from all three of its views -- its own
|
|
1209
|
+
* route file says so twice ("THREE views of the SAME `getPlatformKbBundle()`";
|
|
1210
|
+
* "the payload is identical") and forbids tenant data ever entering the reader.
|
|
1211
|
+
* So the authed route returns nothing extra, and asking for a credential to read
|
|
1212
|
+
* it can only ever subtract.
|
|
1179
1213
|
*
|
|
1180
|
-
*
|
|
1181
|
-
*
|
|
1182
|
-
*
|
|
1214
|
+
* It subtracted, measurably. This used to prefer the tenant-scoped route
|
|
1215
|
+
* WHENEVER a token was saved, and that route is guarded by `pack:deploy` -- so a
|
|
1216
|
+
* developer holding a token WITHOUT that scope got a 403 on a pull that would
|
|
1217
|
+
* have succeeded with no token at all. Having logged in made the CLI strictly
|
|
1218
|
+
* worse than not having logged in, on the very first command an author runs. No
|
|
1219
|
+
* console preset could even reach `pack:deploy` before 2026-08-26, so that was
|
|
1220
|
+
* the likeliest token a new developer held.
|
|
1221
|
+
*
|
|
1222
|
+
* The two arguments the old comment gave for preferring a token did not survive
|
|
1223
|
+
* being checked: "keeps the author's own instance the source of truth" is
|
|
1224
|
+
* vacuous (both routes are `t.url` -- the same instance), and "works against
|
|
1225
|
+
* platforms that predate the public rung" is a backward-compatibility shim,
|
|
1226
|
+
* which this codebase does not carry.
|
|
1227
|
+
*
|
|
1228
|
+
* `--token` is still ACCEPTED here, and ignored, so a scripted `platform-kb pull
|
|
1229
|
+
* --token …` keeps working instead of turning into an unknown-flag error.
|
|
1183
1230
|
*/
|
|
1184
1231
|
function kbEndpoint(t) {
|
|
1185
|
-
return t.
|
|
1186
|
-
? { url: `${t.url}/api/self/t/octwin-platform-kb`, headers: authHeaders(t), anonymous: false }
|
|
1187
|
-
: { url: `${t.url}/api/public/octwin-platform-kb`, headers: {}, anonymous: true };
|
|
1232
|
+
return { url: `${t.url}/api/public/octwin-platform-kb`, headers: {} };
|
|
1188
1233
|
}
|
|
1189
1234
|
/** The raw resolution both wrappers share — may return empty url/token. */
|
|
1190
1235
|
function readTarget(flags) {
|
|
@@ -1464,13 +1509,13 @@ async function cmdSeed(flags) {
|
|
|
1464
1509
|
const { terminal: final, stepErrors } = await readDeployProgress(res.body);
|
|
1465
1510
|
if (!final || final.stage === 'error')
|
|
1466
1511
|
die(`seed failed${final?.message ? `: ${final.message}` : ' (stream ended early)'}`);
|
|
1467
|
-
console.log(`
|
|
1512
|
+
console.log(`
|
|
1468
1513
|
✓ ${final.message ?? 'seed complete'}`);
|
|
1469
1514
|
printSeedCounts(final.result?.seeded);
|
|
1470
1515
|
if (stepErrors.length) {
|
|
1471
1516
|
// A kind failed but the rest ran — the reconcile softens each step. Say which,
|
|
1472
1517
|
// and exit non-zero so a scripted `seed && chat` doesn't read as clean.
|
|
1473
|
-
console.error(`
|
|
1518
|
+
console.error(`
|
|
1474
1519
|
⚠ ${stepErrors.length} step${stepErrors.length === 1 ? '' : 's'} failed — data may be incomplete:`);
|
|
1475
1520
|
for (const e of stepErrors)
|
|
1476
1521
|
console.error(` • ${e}`);
|
|
@@ -1720,9 +1765,7 @@ async function cmdPlatformKb(flags) {
|
|
|
1720
1765
|
const local = readLocalKb(packDir);
|
|
1721
1766
|
const polled = await fetchKbMeta(t, 10_000);
|
|
1722
1767
|
if (!polled.ok) {
|
|
1723
|
-
console.error(
|
|
1724
|
-
? '✗ cannot check — the platform refused the token, and this instance serves no anonymous reference.'
|
|
1725
|
-
: `✗ cannot check — ${url} did not answer.`);
|
|
1768
|
+
console.error(`✗ cannot check — ${url} did not answer, or serves no capability reference.`);
|
|
1726
1769
|
exitNow(1);
|
|
1727
1770
|
}
|
|
1728
1771
|
const remote = polled.meta.content_hash;
|
|
@@ -1751,7 +1794,7 @@ async function cmdPlatformKb(flags) {
|
|
|
1751
1794
|
return;
|
|
1752
1795
|
}
|
|
1753
1796
|
}
|
|
1754
|
-
console.log(`→ Pulling the platform capability reference from ${url}
|
|
1797
|
+
console.log(`→ Pulling the platform capability reference from ${url} (no token needed for the reference) …`);
|
|
1755
1798
|
const res = await fetchOrDie(ep.url, { headers: ep.headers }, 'platform-kb pull');
|
|
1756
1799
|
const text = await res.text();
|
|
1757
1800
|
if (!res.ok) {
|
|
@@ -1919,11 +1962,21 @@ async function apiGet(endpoint, t) {
|
|
|
1919
1962
|
* `content-type` + `authHeaders` block was inlined at each of the four original
|
|
1920
1963
|
* write sites, and fifteen more copies is how one of them ends up subtly different.
|
|
1921
1964
|
* A `204` (media delete) has no body to parse, hence the empty-text guard.
|
|
1965
|
+
*
|
|
1966
|
+
* `body: undefined` sends NO `content-type` either. The header used to be
|
|
1967
|
+
* unconditional, so a genuinely body-less write announced `application/json` and
|
|
1968
|
+
* then sent nothing — Fastify tried to parse the empty body and answered a bare
|
|
1969
|
+
* `400 Bad Request` with no hint of the cause. Latent until 2026-08-27 because every
|
|
1970
|
+
* caller until then passed an object; the first body-less POST (`automation run`)
|
|
1971
|
+
* hit it immediately, and a caller having to know "pass `{}` or you get a 400" is
|
|
1972
|
+
* exactly the per-site divergence this helper exists to prevent.
|
|
1922
1973
|
*/
|
|
1923
1974
|
async function apiSend(method, endpoint, body, t) {
|
|
1924
1975
|
const res = await fetchOrDie(endpoint, {
|
|
1925
1976
|
method,
|
|
1926
|
-
headers:
|
|
1977
|
+
headers: body === undefined
|
|
1978
|
+
? authHeaders(t)
|
|
1979
|
+
: { 'content-type': 'application/json', ...authHeaders(t) },
|
|
1927
1980
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
1928
1981
|
}, 'request');
|
|
1929
1982
|
const text = await res.text();
|
|
@@ -3364,7 +3417,9 @@ async function cmdAgents(flags) {
|
|
|
3364
3417
|
return;
|
|
3365
3418
|
}
|
|
3366
3419
|
if (!ref) {
|
|
3367
|
-
|
|
3420
|
+
// Template literal, not bare `base` — see the note in `cmdScheduling`: the route
|
|
3421
|
+
// guard's extractor cannot read a bare identifier, so this URL was exempt.
|
|
3422
|
+
const { status, json } = await apiGet(`${base}`, t);
|
|
3368
3423
|
if (status !== 200)
|
|
3369
3424
|
die(`could not read agents (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
3370
3425
|
if (asJson) {
|
|
@@ -3998,7 +4053,9 @@ async function cmdScheduling(flags) {
|
|
|
3998
4053
|
}
|
|
3999
4054
|
if (!asJson)
|
|
4000
4055
|
console.log(`→ Reading the scheduling engine state from ${targetLabel(t)} …`);
|
|
4001
|
-
|
|
4056
|
+
// A template literal, not the bare `base` — `cli-routes.test.ts` cannot read a
|
|
4057
|
+
// bare identifier, so this URL was silently exempt from the route guard.
|
|
4058
|
+
const { status, json } = await apiGet(`${base}`, t);
|
|
4002
4059
|
if (status !== 200)
|
|
4003
4060
|
die(`could not read scheduling (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
4004
4061
|
if (asJson) {
|
|
@@ -4016,290 +4073,1128 @@ async function cmdScheduling(flags) {
|
|
|
4016
4073
|
console.log(` upcoming slots: ${json?.upcoming_slots ?? 0} booked seats: ${json?.booked_seats ?? 0}`);
|
|
4017
4074
|
console.log('\nSlots for one resource: octwin scheduling --slots <resourceRecordId> (ids: octwin records <entity>)');
|
|
4018
4075
|
}
|
|
4076
|
+
// ── automation: declaration-derived jobs + campaigns ─────────────────────────
|
|
4077
|
+
/**
|
|
4078
|
+
* Verbs that mean "act on a job", not "an id".
|
|
4079
|
+
*
|
|
4080
|
+
* `pause` / `resume` rather than a literal `status active|paused`: the route body
|
|
4081
|
+
* takes the status, but the RBAC layer it calls checks the ACTION
|
|
4082
|
+
* (`assertCan(…, action: 'pause' | 'resume')`), and `VERB_REQUIREMENTS` is keyed
|
|
4083
|
+
* per verb — so one verb per intent makes both the permission hint and the 403
|
|
4084
|
+
* say the right thing.
|
|
4085
|
+
*/
|
|
4086
|
+
const AUTOMATION_VERBS = new Set(['run', 'pause', 'resume', 'campaigns', 'send']);
|
|
4087
|
+
/**
|
|
4088
|
+
* Turn whatever the author typed into the UUID the route demands.
|
|
4089
|
+
*
|
|
4090
|
+
* (`UUID_RE` is the one already declared for `--media`, deliberately reused rather
|
|
4091
|
+
* than a second copy of the same pattern.)
|
|
4092
|
+
*
|
|
4093
|
+
* The write routes take a UUID path param and reject anything else with a bare
|
|
4094
|
+
* *"Malformed identifier in the URL — expected a UUID"* 400. But the thing an author
|
|
4095
|
+
* has in front of them is the declaration KEY (`cart_recovery_nudge`) — that is what
|
|
4096
|
+
* the list prints, and it is the name in their own YAML. Measured: passing the key
|
|
4097
|
+
* 400s on all three write verbs.
|
|
4098
|
+
*
|
|
4099
|
+
* So the key is resolved here, against the list route, rather than documented as a
|
|
4100
|
+
* gotcha. A UUID passes straight through, and an unknown key fails naming the keys
|
|
4101
|
+
* that DO exist — which is the answer to the question the author is actually asking.
|
|
4102
|
+
*/
|
|
4103
|
+
async function resolveAutomationId(kind, typed, base, t) {
|
|
4104
|
+
if (UUID_RE.test(typed))
|
|
4105
|
+
return typed;
|
|
4106
|
+
const { status, json } = await apiGet(`${base}/${kind}`, t);
|
|
4107
|
+
if (status !== 200) {
|
|
4108
|
+
die(`could not resolve '${typed}' — reading ${kind} failed (HTTP ${status})${errDetail(json)}`);
|
|
4109
|
+
}
|
|
4110
|
+
const rows = readPage(json).rows;
|
|
4111
|
+
const hit = rows.find(r => r.key === typed || r.id === typed);
|
|
4112
|
+
if (hit?.id)
|
|
4113
|
+
return hit.id;
|
|
4114
|
+
const keys = rows.map(r => r.key ?? r.id).filter(Boolean);
|
|
4115
|
+
die(`no ${kind === 'jobs' ? 'job' : 'campaign'} '${typed}' in this project.`
|
|
4116
|
+
+ (keys.length ? ` Available: ${keys.join(', ')}` : ` This project declares none.`));
|
|
4117
|
+
}
|
|
4118
|
+
/** `pause`/`resume`/`run`/`send` — the writes behind `octwin automation`. */
|
|
4119
|
+
async function cmdAutomationWrite(flags) {
|
|
4120
|
+
const t = resolveTarget(flags);
|
|
4121
|
+
const { url } = t;
|
|
4122
|
+
const base = `${url}/api/self/p/automation`;
|
|
4123
|
+
const verb = flags._[0];
|
|
4124
|
+
const typed = flags._[1];
|
|
4125
|
+
const asJson = flags.json === true;
|
|
4126
|
+
if (!typed)
|
|
4127
|
+
die(`usage: octwin automation ${verb} <${verb === 'send' ? 'campaignId' : 'jobId'}> (ids: octwin automation${verb === 'send' ? ' campaigns' : ''})`);
|
|
4128
|
+
const id = await resolveAutomationId(verb === 'send' ? 'campaigns' : 'jobs', typed, base, t);
|
|
4129
|
+
if (verb === 'pause' || verb === 'resume') {
|
|
4130
|
+
const { status, json } = await apiSend('PATCH', `${base}/jobs/${encodeURIComponent(id)}/status`, { status: verb === 'pause' ? 'paused' : 'active' }, t);
|
|
4131
|
+
if (status === 404)
|
|
4132
|
+
die(`job '${typed}' not found in ${targetLabel(t)} (ids: octwin automation)`);
|
|
4133
|
+
// RBAC re-checks the ACTION on the job, so a 403 here can be a grant gap
|
|
4134
|
+
// rather than a missing scope — same caveat as a record write.
|
|
4135
|
+
if (status !== 200)
|
|
4136
|
+
writeFail(`${verb} job '${typed}'`, status, json, url, true);
|
|
4137
|
+
if (asJson) {
|
|
4138
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4139
|
+
return;
|
|
4140
|
+
}
|
|
4141
|
+
const j = json?.job ?? {};
|
|
4142
|
+
console.log(`✓ job '${j.key ?? typed}' is now ${j.status}`);
|
|
4143
|
+
if (j.next_run_at)
|
|
4144
|
+
console.log(` next run: ${j.next_run_at}`);
|
|
4145
|
+
return;
|
|
4146
|
+
}
|
|
4147
|
+
if (verb === 'run') {
|
|
4148
|
+
if (!asJson)
|
|
4149
|
+
console.log(`→ Running job '${typed}' in ${targetLabel(t)} …`);
|
|
4150
|
+
const { status, json } = await apiSend('POST', `${base}/jobs/${encodeURIComponent(id)}/run`, undefined, t);
|
|
4151
|
+
if (status === 404)
|
|
4152
|
+
die(`job '${typed}' not found in ${targetLabel(t)} (ids: octwin automation)`);
|
|
4153
|
+
if (status !== 200)
|
|
4154
|
+
writeFail(`run job '${typed}'`, status, json, url, true);
|
|
4155
|
+
if (asJson) {
|
|
4156
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4157
|
+
return;
|
|
4158
|
+
}
|
|
4159
|
+
const r = json?.result ?? {};
|
|
4160
|
+
console.log(`✓ ran '${typed}': matched ${r.matched ?? 0}, acted ${r.acted ?? 0}, errors ${r.errors ?? 0}`);
|
|
4161
|
+
// `acted < matched` is the route's own definition of a PARTIAL run, so say so
|
|
4162
|
+
// rather than leaving three numbers for the author to compare.
|
|
4163
|
+
if ((r.errors ?? 0) > 0)
|
|
4164
|
+
console.log(' ⚠ some rows errored — see the job\'s last_result in `octwin automation`');
|
|
4165
|
+
else if ((r.acted ?? 0) < (r.matched ?? 0))
|
|
4166
|
+
console.log(' partial: matched rows were skipped (cooldown, or already acted on)');
|
|
4167
|
+
return;
|
|
4168
|
+
}
|
|
4169
|
+
// send — one campaign
|
|
4170
|
+
if (!asJson)
|
|
4171
|
+
console.log(`→ Sending campaign '${typed}' in ${targetLabel(t)} …`);
|
|
4172
|
+
const { status, json } = await apiSend('POST', `${base}/campaigns/${encodeURIComponent(id)}/send`, undefined, t);
|
|
4173
|
+
if (status === 404)
|
|
4174
|
+
die(`campaign '${typed}' not found in ${targetLabel(t)} (ids: octwin automation campaigns)`);
|
|
4175
|
+
if (status !== 200)
|
|
4176
|
+
writeFail(`send campaign '${typed}'`, status, json, url, true);
|
|
4177
|
+
if (asJson) {
|
|
4178
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4179
|
+
return;
|
|
4180
|
+
}
|
|
4181
|
+
const r = json?.result ?? {};
|
|
4182
|
+
console.log(`✓ campaign '${typed}': matched ${r.matched ?? 0}, enqueued ${r.enqueued ?? 0}`);
|
|
4183
|
+
if ((r.enqueued ?? 0) < (r.matched ?? 0))
|
|
4184
|
+
console.log(' partial: some matched contacts were not enqueued (cooldown, or no reachable channel)');
|
|
4185
|
+
console.log(' enqueued ≠ delivered — watch the sends land with `octwin logs`');
|
|
4186
|
+
}
|
|
4187
|
+
/**
|
|
4188
|
+
* `octwin automation [--campaigns] [--json]` — the jobs a pack's declarations
|
|
4189
|
+
* produced, with their last result, plus the health counts.
|
|
4190
|
+
*
|
|
4191
|
+
* Needs `automation:read`. The job list is CAPPED server-side and the counts are
|
|
4192
|
+
* computed in SQL, so the header numbers come from `/health` rather than from
|
|
4193
|
+
* filtering the page — past the cap a client-side count would depend on the cap
|
|
4194
|
+
* instead of the data.
|
|
4195
|
+
*/
|
|
4196
|
+
async function cmdAutomation(flags) {
|
|
4197
|
+
if (typeof flags._[0] === 'string' && AUTOMATION_VERBS.has(flags._[0])) {
|
|
4198
|
+
// `campaigns` is a READ that shares the verb slot with the writes.
|
|
4199
|
+
if (flags._[0] !== 'campaigns')
|
|
4200
|
+
return cmdAutomationWrite(flags);
|
|
4201
|
+
}
|
|
4202
|
+
const t = resolveTarget(flags);
|
|
4203
|
+
const { url } = t;
|
|
4204
|
+
const base = `${url}/api/self/p/automation`;
|
|
4205
|
+
const asJson = flags.json === true;
|
|
4206
|
+
const campaigns = flags._[0] === 'campaigns' || flags.campaigns === true;
|
|
4207
|
+
if (campaigns) {
|
|
4208
|
+
if (!asJson)
|
|
4209
|
+
console.log(`→ Reading campaigns from ${targetLabel(t)} …`);
|
|
4210
|
+
const { status, json } = await apiGet(`${base}/campaigns?${pagingQs(flags)}`, t);
|
|
4211
|
+
if (status !== 200)
|
|
4212
|
+
die(`could not read campaigns (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
4213
|
+
if (asJson) {
|
|
4214
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4215
|
+
return;
|
|
4216
|
+
}
|
|
4217
|
+
const page = readPage(json);
|
|
4218
|
+
console.log(`Campaigns in ${targetLabel(t)}: ${page.total ?? page.rows.length}`);
|
|
4219
|
+
if (page.rows.length === 0)
|
|
4220
|
+
console.log(' (none — a campaign comes from a `campaigns:` block in the pack\'s automation declaration)');
|
|
4221
|
+
for (const c of page.rows) {
|
|
4222
|
+
const label = pickLabel(c.label) ?? c.key ?? c.id;
|
|
4223
|
+
console.log(` ${String(label).padEnd(28)} ${String(c.audience_size ?? c.matched ?? '—').padStart(6)} contact(s) ${c.key ?? c.id}`);
|
|
4224
|
+
}
|
|
4225
|
+
const more = morePageHint(page, 'octwin automation campaigns');
|
|
4226
|
+
if (more)
|
|
4227
|
+
console.log(more);
|
|
4228
|
+
console.log('\nSend one: octwin automation send <campaignId> (needs automation:write)');
|
|
4229
|
+
return;
|
|
4230
|
+
}
|
|
4231
|
+
if (!asJson)
|
|
4232
|
+
console.log(`→ Reading automation jobs from ${targetLabel(t)} …`);
|
|
4233
|
+
const [jobs, health] = await Promise.all([
|
|
4234
|
+
apiGet(`${base}/jobs`, t),
|
|
4235
|
+
apiGet(`${base}/health`, t),
|
|
4236
|
+
]);
|
|
4237
|
+
if (jobs.status !== 200)
|
|
4238
|
+
die(`could not read automation jobs (HTTP ${jobs.status})${errDetail(jobs.json)}${authFailureDetail(jobs.status, url)}`);
|
|
4239
|
+
if (asJson) {
|
|
4240
|
+
console.log(JSON.stringify({ jobs: jobs.json, health: health.json }, null, 2));
|
|
4241
|
+
return;
|
|
4242
|
+
}
|
|
4243
|
+
const page = readPage(jobs.json);
|
|
4244
|
+
const h = health.status === 200 ? (health.json ?? {}) : {};
|
|
4245
|
+
console.log(`Automation in ${targetLabel(t)}: ${h.total ?? page.rows.length} job(s)`
|
|
4246
|
+
+ ` — ${h.active ?? '?'} active, ${h.paused ?? '?'} paused, ${h.failing ?? '?'} failing, ${h.never_ran ?? '?'} never ran`);
|
|
4247
|
+
if (page.rows.length === 0) {
|
|
4248
|
+
console.log(' (none — jobs are DERIVED from the pack\'s automation declaration, not created here.');
|
|
4249
|
+
console.log(' No `automation.yaml` block → no jobs. `octwin deploy` installs them.)');
|
|
4250
|
+
return;
|
|
4251
|
+
}
|
|
4252
|
+
for (const j of page.rows) {
|
|
4253
|
+
const r = j.last_result ?? {};
|
|
4254
|
+
const ran = j.last_run_at ? `last ${j.last_run_at}` : 'never ran';
|
|
4255
|
+
const result = j.last_result
|
|
4256
|
+
? ` matched ${r.matched ?? 0}/acted ${r.acted ?? 0}${(r.errors ?? 0) > 0 ? `/ERRORS ${r.errors}` : ''}`
|
|
4257
|
+
: '';
|
|
4258
|
+
console.log(` ${String(j.key ?? j.id).padEnd(26)} ${String(j.status).padEnd(7)} ${j.kind}/${j.entity ?? '—'}`
|
|
4259
|
+
+ ` every ${j.interval_seconds}s ${ran}${result}`);
|
|
4260
|
+
}
|
|
4261
|
+
console.log('\nRun one now: octwin automation run <jobId> (jobId = the `key` above, or its uuid)');
|
|
4262
|
+
console.log('Pause/resume: octwin automation pause|resume <jobId>');
|
|
4263
|
+
console.log('Campaigns: octwin automation campaigns');
|
|
4264
|
+
}
|
|
4265
|
+
// ── integrations: declared connections, their credentials, and the delivery log ──
|
|
4266
|
+
/** Verbs that act on a connection or a delivery, rather than naming one. */
|
|
4267
|
+
const INTEGRATION_VERBS = new Set(['test', 'preflight', 'deliveries', 'retry', 'cancel', 'send-now', 'events']);
|
|
4268
|
+
/** The three delivery actions — each its own verb so the scope hint can differ. */
|
|
4269
|
+
const DELIVERY_ACTIONS = {
|
|
4270
|
+
'retry': { path: 'retry', what: 'retry' },
|
|
4271
|
+
'cancel': { path: 'cancel', what: 'cancel' },
|
|
4272
|
+
'send-now': { path: 'send-now', what: 'send' },
|
|
4273
|
+
};
|
|
4274
|
+
/** `octwin integrations <verb> …` — the connection + delivery verbs. */
|
|
4275
|
+
async function cmdIntegrationsVerb(flags) {
|
|
4276
|
+
const t = resolveTarget(flags);
|
|
4277
|
+
const { url } = t;
|
|
4278
|
+
const base = `${url}/api/self/p/integrations`;
|
|
4279
|
+
const verb = flags._[0];
|
|
4280
|
+
const arg = flags._[1];
|
|
4281
|
+
const asJson = flags.json === true;
|
|
4282
|
+
// ── deliveries: the outbound log ──────────────────────────────────────────
|
|
4283
|
+
if (verb === 'deliveries') {
|
|
4284
|
+
if (arg) {
|
|
4285
|
+
const { status, json } = await apiGet(`${base}/deliveries/${encodeURIComponent(arg)}`, t);
|
|
4286
|
+
if (status === 404)
|
|
4287
|
+
die(`delivery '${arg}' not found`);
|
|
4288
|
+
if (status !== 200)
|
|
4289
|
+
die(`could not read delivery (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
4290
|
+
if (asJson) {
|
|
4291
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4292
|
+
return;
|
|
4293
|
+
}
|
|
4294
|
+
const d = json?.delivery ?? {};
|
|
4295
|
+
console.log(`Delivery ${d.id} ${d.status} ${d.connection_key}/${d.operation_id}`);
|
|
4296
|
+
console.log(` attempts ${d.attempts}${d.http_status ? ` HTTP ${d.http_status}` : ''}${d.port ? ` port: ${d.port}` : ''}`);
|
|
4297
|
+
if (d.last_error)
|
|
4298
|
+
console.log(` last error: ${d.last_error}`);
|
|
4299
|
+
if (d.next_attempt_at)
|
|
4300
|
+
console.log(` next attempt: ${d.next_attempt_at}`);
|
|
4301
|
+
console.log(` from ${d.source_kind ?? '—'}${d.source_hook ? ` (${d.source_hook})` : ''}${d.source_record_id ? ` record ${d.source_record_id}` : ''}`);
|
|
4302
|
+
// The snapshots are redacted at WRITE time, which is why the detail view may print them.
|
|
4303
|
+
if (d.request)
|
|
4304
|
+
console.log(` request: ${JSON.stringify(d.request)}`);
|
|
4305
|
+
if (d.response)
|
|
4306
|
+
console.log(` response: ${JSON.stringify(d.response)}`);
|
|
4307
|
+
return;
|
|
4308
|
+
}
|
|
4309
|
+
const q = new URLSearchParams(pagingQs(flags));
|
|
4310
|
+
if (typeof flags.status === 'string')
|
|
4311
|
+
q.set('status', flags.status);
|
|
4312
|
+
if (typeof flags.operation === 'string')
|
|
4313
|
+
q.set('operation', flags.operation);
|
|
4314
|
+
if (!asJson)
|
|
4315
|
+
console.log(`→ Reading the delivery log from ${targetLabel(t)} …`);
|
|
4316
|
+
const { status, json } = await apiGet(`${base}/deliveries?${q.toString()}`, t);
|
|
4317
|
+
if (status !== 200)
|
|
4318
|
+
die(`could not read deliveries (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
4319
|
+
if (asJson) {
|
|
4320
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4321
|
+
return;
|
|
4322
|
+
}
|
|
4323
|
+
const page = readPage(json);
|
|
4324
|
+
const c = json?.counts;
|
|
4325
|
+
console.log(`Deliveries in ${targetLabel(t)}: ${page.total ?? page.rows.length}`
|
|
4326
|
+
+ (c ? ` — queued ${c.queued ?? 0}, sent ${c.sent ?? 0}, failed ${c.failed ?? 0}, cancelled ${c.cancelled ?? 0}` : ''));
|
|
4327
|
+
if (page.rows.length === 0)
|
|
4328
|
+
console.log(' (none — a delivery is produced by an `integrations:` operation firing on a record hook)');
|
|
4329
|
+
for (const d of page.rows) {
|
|
4330
|
+
const err = d.last_error ? ` ${String(d.last_error).slice(0, 60)}` : '';
|
|
4331
|
+
console.log(` ${String(d.status).padEnd(9)} ${String(d.connection_key ?? '—').padEnd(16)} ${String(d.operation_id ?? '—').padEnd(20)}`
|
|
4332
|
+
+ ` try ${d.attempts}${d.http_status ? ` HTTP ${d.http_status}` : ''} ${d.id}${err}`);
|
|
4333
|
+
}
|
|
4334
|
+
const more = morePageHint(page, 'octwin integrations deliveries');
|
|
4335
|
+
if (more)
|
|
4336
|
+
console.log(more);
|
|
4337
|
+
console.log('\nOne delivery + its request/response: octwin integrations deliveries <id>');
|
|
4338
|
+
console.log('Act on one: octwin integrations retry|cancel|send-now <id>');
|
|
4339
|
+
return;
|
|
4340
|
+
}
|
|
4341
|
+
// ── inbound events ────────────────────────────────────────────────────────
|
|
4342
|
+
if (verb === 'events') {
|
|
4343
|
+
if (!asJson)
|
|
4344
|
+
console.log(`→ Reading inbound integration events from ${targetLabel(t)} …`);
|
|
4345
|
+
const { status, json } = await apiGet(`${base}/inbound-events?${pagingQs(flags)}`, t);
|
|
4346
|
+
if (status !== 200)
|
|
4347
|
+
die(`could not read inbound events (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
4348
|
+
if (asJson) {
|
|
4349
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4350
|
+
return;
|
|
4351
|
+
}
|
|
4352
|
+
const events = (json?.events ?? readPage(json).rows);
|
|
4353
|
+
console.log(`Inbound events in ${targetLabel(t)}: ${events.length}`);
|
|
4354
|
+
if (events.length === 0)
|
|
4355
|
+
console.log(' (none — an inbound event arrives at POST /api/integrations/<tenant>/<project>/<inboundKey>)');
|
|
4356
|
+
for (const e of events) {
|
|
4357
|
+
console.log(` ${e.received_at ?? e.created_at ?? '—'} ${e.inbound_key ?? '—'} ${e.status ?? e.outcome ?? '—'}${e.detail ? ` ${e.detail}` : ''}`);
|
|
4358
|
+
}
|
|
4359
|
+
return;
|
|
4360
|
+
}
|
|
4361
|
+
// ── a delivery action ─────────────────────────────────────────────────────
|
|
4362
|
+
const action = DELIVERY_ACTIONS[verb];
|
|
4363
|
+
if (action) {
|
|
4364
|
+
if (!arg)
|
|
4365
|
+
die(`usage: octwin integrations ${verb} <deliveryId> (ids: octwin integrations deliveries)`);
|
|
4366
|
+
const { status, json } = await apiSend('POST', `${base}/deliveries/${encodeURIComponent(arg)}/${action.path}`, undefined, t);
|
|
4367
|
+
// 409 is the route's own "wrong state" answer, and it carries the rule — print
|
|
4368
|
+
// it rather than a generic failure, because the fix is choosing another delivery.
|
|
4369
|
+
if (status === 409)
|
|
4370
|
+
die(`cannot ${action.what} delivery '${arg}'${errDetail(json)}`);
|
|
4371
|
+
if (status === 404)
|
|
4372
|
+
die(`delivery '${arg}' not found`);
|
|
4373
|
+
if (status !== 200)
|
|
4374
|
+
writeFail(`${action.what} delivery '${arg}'`, status, json, url);
|
|
4375
|
+
if (asJson) {
|
|
4376
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4377
|
+
return;
|
|
4378
|
+
}
|
|
4379
|
+
const d = json?.delivery ?? {};
|
|
4380
|
+
console.log(`✓ delivery ${d.id ?? arg} is now ${d.status}${d.next_attempt_at ? ` (next attempt ${d.next_attempt_at})` : ''}`);
|
|
4381
|
+
return;
|
|
4382
|
+
}
|
|
4383
|
+
// ── preflight / test on one connection ────────────────────────────────────
|
|
4384
|
+
if (!arg)
|
|
4385
|
+
die(`usage: octwin integrations ${verb} <connectionKey> (keys: octwin integrations)`);
|
|
4386
|
+
if (verb === 'preflight') {
|
|
4387
|
+
if (!asJson)
|
|
4388
|
+
console.log(`→ Preflighting connection '${arg}' in ${targetLabel(t)} …`);
|
|
4389
|
+
const { status, json } = await apiSend('POST', `${base}/connections/${encodeURIComponent(arg)}/preflight`, undefined, t);
|
|
4390
|
+
if (status !== 200)
|
|
4391
|
+
die(`could not preflight '${arg}' (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
4392
|
+
if (asJson) {
|
|
4393
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4394
|
+
return;
|
|
4395
|
+
}
|
|
4396
|
+
const marks = { pass: '✓', fail: '✗', warn: '⚠', skipped: '–' };
|
|
4397
|
+
console.log(`Preflight '${json?.connection_key ?? arg}': ${json?.ok ? 'READY' : 'NOT READY'}`);
|
|
4398
|
+
for (const c of (json?.checks ?? [])) {
|
|
4399
|
+
console.log(` ${marks[c.status] ?? '?'} ${String(c.label).padEnd(30)} ${c.detail}`);
|
|
4400
|
+
if (c.fix)
|
|
4401
|
+
console.log(` fix: ${c.fix}`);
|
|
4402
|
+
}
|
|
4403
|
+
// Preflight is a DIAGNOSIS and needs only `integrations:read`; `test` makes a
|
|
4404
|
+
// live call and needs write. Worth saying, because the two read alike.
|
|
4405
|
+
if (!json?.ok)
|
|
4406
|
+
console.log('\nPreflight makes no live call. Once it is READY: octwin integrations test <key>');
|
|
4407
|
+
return;
|
|
4408
|
+
}
|
|
4409
|
+
// test — a live call against the connection's declared `health:` operation
|
|
4410
|
+
if (!asJson)
|
|
4411
|
+
console.log(`→ Testing connection '${arg}' against its health operation …`);
|
|
4412
|
+
const { status, json } = await apiSend('POST', `${base}/connections/${encodeURIComponent(arg)}/test`, undefined, t);
|
|
4413
|
+
if (status === 409)
|
|
4414
|
+
die(`no pack is installed on ${targetLabel(t)}${errDetail(json)}`);
|
|
4415
|
+
if (status === 404)
|
|
4416
|
+
die(`${errDetail(json).replace(/^ — /, '') || `connection '${arg}' is not declared by the installed pack`}`);
|
|
4417
|
+
// A 400 here is a real answer, not a usage error: the route returns
|
|
4418
|
+
// `{ ok:false, detail }` when the live call fails, and that detail IS the result.
|
|
4419
|
+
if (status === 400 && json && typeof json === 'object' && 'ok' in json) {
|
|
4420
|
+
if (asJson) {
|
|
4421
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4422
|
+
process.exitCode = 1;
|
|
4423
|
+
return;
|
|
4424
|
+
}
|
|
4425
|
+
console.log(`✗ '${arg}' failed: ${json.detail ?? '(no detail)'}`);
|
|
4426
|
+
console.log(' Diagnose without calling out: octwin integrations preflight ' + arg);
|
|
4427
|
+
process.exitCode = 1;
|
|
4428
|
+
return;
|
|
4429
|
+
}
|
|
4430
|
+
if (status !== 200)
|
|
4431
|
+
writeFail(`test connection '${arg}'`, status, json, url);
|
|
4432
|
+
if (asJson) {
|
|
4433
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4434
|
+
return;
|
|
4435
|
+
}
|
|
4436
|
+
console.log(`${json?.ok ? '✓' : '✗'} '${arg}': ${json?.detail ?? '(no detail)'}`);
|
|
4437
|
+
if (json?.http_status)
|
|
4438
|
+
console.log(` HTTP ${json.http_status} port: ${json.port ?? '—'}`);
|
|
4439
|
+
if (json?.data !== undefined && json?.data !== null)
|
|
4440
|
+
console.log(` data: ${JSON.stringify(json.data).slice(0, 300)}`);
|
|
4441
|
+
if (!json?.ok)
|
|
4442
|
+
process.exitCode = 1;
|
|
4443
|
+
}
|
|
4444
|
+
/**
|
|
4445
|
+
* `octwin integrations [--json]` — what the pack DECLARES beside what is actually
|
|
4446
|
+
* configured, in one view.
|
|
4447
|
+
*
|
|
4448
|
+
* Needs `integrations:read`. The two halves are deliberately joined: a declared
|
|
4449
|
+
* connection with no configured row is the single most common reason an
|
|
4450
|
+
* integration silently never fires, and reading either list alone cannot show it.
|
|
4451
|
+
*/
|
|
4452
|
+
async function cmdIntegrations(flags) {
|
|
4453
|
+
if (typeof flags._[0] === 'string' && INTEGRATION_VERBS.has(flags._[0]))
|
|
4454
|
+
return cmdIntegrationsVerb(flags);
|
|
4455
|
+
const t = resolveTarget(flags);
|
|
4456
|
+
const { url } = t;
|
|
4457
|
+
const base = `${url}/api/self/p/integrations`;
|
|
4458
|
+
const asJson = flags.json === true;
|
|
4459
|
+
if (!asJson)
|
|
4460
|
+
console.log(`→ Reading integrations from ${targetLabel(t)} …`);
|
|
4461
|
+
const [declared, configured] = await Promise.all([
|
|
4462
|
+
apiGet(`${base}/declared`, t),
|
|
4463
|
+
apiGet(`${base}/connections`, t),
|
|
4464
|
+
]);
|
|
4465
|
+
if (declared.status !== 200)
|
|
4466
|
+
die(`could not read declared integrations (HTTP ${declared.status})${errDetail(declared.json)}${authFailureDetail(declared.status, url)}`);
|
|
4467
|
+
if (asJson) {
|
|
4468
|
+
console.log(JSON.stringify({ declared: declared.json, configured: configured.json }, null, 2));
|
|
4469
|
+
return;
|
|
4470
|
+
}
|
|
4471
|
+
const d = declared.json ?? {};
|
|
4472
|
+
const rows = (configured.status === 200 ? (configured.json?.connections ?? []) : []);
|
|
4473
|
+
const byKey = new Map(rows.map(r => [r.connection_key, r]));
|
|
4474
|
+
const conns = (d.connections ?? []);
|
|
4475
|
+
if (!d.pack_id) {
|
|
4476
|
+
console.log('No pack is installed on this project — nothing declares an integration.');
|
|
4477
|
+
return;
|
|
4478
|
+
}
|
|
4479
|
+
if (conns.length === 0 && (d.operations ?? []).length === 0 && (d.inbound ?? []).length === 0) {
|
|
4480
|
+
console.log(`Pack '${d.pack_id}' declares no integrations — no \`integrations.yaml\`.`);
|
|
4481
|
+
return;
|
|
4482
|
+
}
|
|
4483
|
+
console.log(`Integrations declared by '${d.pack_id}':`);
|
|
4484
|
+
for (const c of conns) {
|
|
4485
|
+
const row = byKey.get(c.key);
|
|
4486
|
+
const state = !row
|
|
4487
|
+
? 'NOT CONFIGURED'
|
|
4488
|
+
: row.status !== 'active'
|
|
4489
|
+
? row.status
|
|
4490
|
+
: row.has_credential ? `ready (…${row.credential_hint ?? '••••'})` : 'no credential';
|
|
4491
|
+
const test = row?.last_test_at
|
|
4492
|
+
? ` last test ${row.last_test_ok ? 'ok' : 'FAILED'} ${row.last_test_at}`
|
|
4493
|
+
: '';
|
|
4494
|
+
console.log(` ${String(c.key).padEnd(20)} ${state.padEnd(22)} ${c.auth_kind} in ${c.auth_in}${test}`);
|
|
4495
|
+
if (!row && c.setup_hint)
|
|
4496
|
+
console.log(` setup: ${c.setup_hint}`);
|
|
4497
|
+
if (row?.last_test_detail && row.last_test_ok === false)
|
|
4498
|
+
console.log(` ${row.last_test_detail}`);
|
|
4499
|
+
}
|
|
4500
|
+
const ops = (d.operations ?? []);
|
|
4501
|
+
if (ops.length) {
|
|
4502
|
+
console.log(`\n operations: ${ops.map(o => o.id ?? o.key).join(', ')}`);
|
|
4503
|
+
}
|
|
4504
|
+
const inbound = (d.inbound ?? []);
|
|
4505
|
+
if (inbound.length) {
|
|
4506
|
+
console.log(` inbound keys: ${inbound.map(i => i.key ?? i.id).join(', ')}`);
|
|
4507
|
+
}
|
|
4508
|
+
// A declared-but-unconfigured connection is the failure this view exists to make
|
|
4509
|
+
// visible, so it gets the next step rather than being left as a status word.
|
|
4510
|
+
const missing = conns.filter(c => !byKey.has(c.key)).map(c => c.key);
|
|
4511
|
+
if (missing.length) {
|
|
4512
|
+
console.log(`\n⚠ ${missing.length} connection(s) declared but never configured: ${missing.join(', ')}`);
|
|
4513
|
+
console.log(' Nothing using them will fire. Configure them in the console → Integrations,');
|
|
4514
|
+
console.log(` then: octwin integrations preflight ${missing[0]}`);
|
|
4515
|
+
}
|
|
4516
|
+
console.log('\nDiagnose one: octwin integrations preflight <key> Live call: octwin integrations test <key>');
|
|
4517
|
+
console.log('Outbound log: octwin integrations deliveries Inbound: octwin integrations events');
|
|
4518
|
+
}
|
|
4519
|
+
// ── journeys: the pack's declared customer journeys, measured ────────────────
|
|
4520
|
+
/**
|
|
4521
|
+
* The five journey analytics modes, plus `definition`.
|
|
4522
|
+
*
|
|
4523
|
+
* Deliberately the SAME flag grammar as `octwin analytics`
|
|
4524
|
+
* (`--funnel|--overview|--trends|--cost`) rather than a second shape for the same
|
|
4525
|
+
* idea — a journey funnel and an entity funnel are the same question asked of a
|
|
4526
|
+
* different subject. `goals` is the journey-only member (an entity has
|
|
4527
|
+
* milestones); `definition` prints what the pack declared, unmeasured.
|
|
4528
|
+
*/
|
|
4529
|
+
const JOURNEY_MODES = ['funnel', 'overview', 'goals', 'trends', 'cost', 'definition'];
|
|
4530
|
+
/** Both "no such journey" and "no `view` grant" answer 200 + `has_data:false` — a
|
|
4531
|
+
* deliberate empty state, never a 403 — so a bare "no data" would hide the cause. */
|
|
4532
|
+
function printNoJourneyData(journeyId) {
|
|
4533
|
+
console.log(`No data for journey '${journeyId}'. Either:`);
|
|
4534
|
+
console.log(` • the pack declares no journey with that id (list them: octwin journeys), or`);
|
|
4535
|
+
console.log(` • your token's role has no \`view\` grant on it, or`);
|
|
4536
|
+
console.log(` • nothing has entered the journey in the window yet — drive one with \`octwin chat\`.`);
|
|
4537
|
+
}
|
|
4538
|
+
/**
|
|
4539
|
+
* `octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition]
|
|
4540
|
+
* [--stage <stageId>] [--json]` — the journeys a pack declares, and how they perform.
|
|
4541
|
+
*
|
|
4542
|
+
* Needs `journeys:read`. Journeys carry RBAC ON TOP of the scope, so a token can
|
|
4543
|
+
* hold the scope and still see an empty journey — `printNoJourneyData` names that
|
|
4544
|
+
* rather than reporting it as absence of data.
|
|
4545
|
+
*/
|
|
4546
|
+
async function cmdJourneys(flags) {
|
|
4547
|
+
const t = resolveTarget(flags);
|
|
4548
|
+
const { url } = t;
|
|
4549
|
+
const base = `${url}/api/self/p/journeys`;
|
|
4550
|
+
const journeyId = flags._[0];
|
|
4551
|
+
const asJson = flags.json === true;
|
|
4552
|
+
const stage = typeof flags.stage === 'string' ? flags.stage : undefined;
|
|
4553
|
+
const mode = JOURNEY_MODES.find(m => flags[m] === true) ?? 'funnel';
|
|
4554
|
+
if (stage && !journeyId)
|
|
4555
|
+
die('usage: octwin journeys <journeyId> --stage <stageId> (a stage belongs to a journey)');
|
|
4556
|
+
// ── the list ──────────────────────────────────────────────────────────────
|
|
4557
|
+
if (!journeyId) {
|
|
4558
|
+
if (!asJson)
|
|
4559
|
+
console.log(`→ Reading declared journeys from ${targetLabel(t)} …`);
|
|
4560
|
+
/**
|
|
4561
|
+
* A template literal, not the bare `base`, so `cli-routes.test.ts` can SEE this
|
|
4562
|
+
* URL — its extractor only reads a template literal in the first argument
|
|
4563
|
+
* position, and a bare identifier slips past unchecked. That guard exists
|
|
4564
|
+
* because six deleted routes shipped as silent 404s; a call it cannot read is a
|
|
4565
|
+
* call it cannot protect.
|
|
4566
|
+
*
|
|
4567
|
+
* The first draft of this very comment QUOTED the call shape it was describing,
|
|
4568
|
+
* which made the comment itself match the extractor's pattern — the scan
|
|
4569
|
+
* consumed the prose and skipped the real call one line below. So the note that
|
|
4570
|
+
* explains the guard silently disabled it. Do not spell the scanned pattern
|
|
4571
|
+
* inside a comment in a file that is itself scanned.
|
|
4572
|
+
*/
|
|
4573
|
+
const { status, json } = await apiGet(`${base}`, t);
|
|
4574
|
+
if (status !== 200)
|
|
4575
|
+
die(`could not read journeys (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
4576
|
+
if (asJson) {
|
|
4577
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4578
|
+
return;
|
|
4579
|
+
}
|
|
4580
|
+
const js = (json?.journeys ?? []);
|
|
4581
|
+
if (js.length === 0) {
|
|
4582
|
+
console.log('No journeys declared — a journey comes from the pack\'s `journeys.yaml`.');
|
|
4583
|
+
console.log('(Per-ENTITY stage funnels are a different surface: octwin analytics)');
|
|
4584
|
+
return;
|
|
4585
|
+
}
|
|
4586
|
+
console.log(`Journeys in ${targetLabel(t)}:`);
|
|
4587
|
+
for (const j of js)
|
|
4588
|
+
console.log(` ${String(j.id).padEnd(24)} ${pickLabel(j.label) ?? ''}`);
|
|
4589
|
+
console.log('\nOne journey: octwin journeys <journeyId> (add --overview / --goals / --trends / --cost / --definition)');
|
|
4590
|
+
console.log('Who is at a stage: octwin journeys <journeyId> --stage <stageId>');
|
|
4591
|
+
return;
|
|
4592
|
+
}
|
|
4593
|
+
// ── stage drill-down: the runs currently at a stage ────────────────────────
|
|
4594
|
+
if (stage) {
|
|
4595
|
+
if (!asJson)
|
|
4596
|
+
console.log(`→ Reading '${journeyId}' runs at stage '${stage}' …`);
|
|
4597
|
+
const { status, json } = await apiGet(`${base}/${encodeURIComponent(journeyId)}/stages/${encodeURIComponent(stage)}/runs?${pagingQs(flags)}`, t);
|
|
4598
|
+
if (status === 404)
|
|
4599
|
+
die(`unknown stage '${stage}' for journey '${journeyId}'${errDetail(json)}`);
|
|
4600
|
+
if (status !== 200)
|
|
4601
|
+
die(`could not read stage runs (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
4602
|
+
if (asJson) {
|
|
4603
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4604
|
+
return;
|
|
4605
|
+
}
|
|
4606
|
+
if (json?.has_data === false) {
|
|
4607
|
+
printNoJourneyData(journeyId);
|
|
4608
|
+
return;
|
|
4609
|
+
}
|
|
4610
|
+
const page = readPage(json);
|
|
4611
|
+
console.log(`'${journeyId}' at '${stage}' (live snapshot): ${page.total ?? page.rows.length} run(s)`);
|
|
4612
|
+
for (const r of page.rows) {
|
|
4613
|
+
const who = r.channel_contact_handle ?? r.display_name ?? r.contact_id ?? '—';
|
|
4614
|
+
console.log(` ${who} entered ${r.entered_at ?? r.created_at ?? '—'}${r.completed_at ? ` completed ${r.completed_at}` : ''}`);
|
|
4615
|
+
}
|
|
4616
|
+
const more = morePageHint(page, `octwin journeys ${journeyId} --stage ${stage}`);
|
|
4617
|
+
if (more)
|
|
4618
|
+
console.log(more);
|
|
4619
|
+
return;
|
|
4620
|
+
}
|
|
4621
|
+
if (!asJson)
|
|
4622
|
+
console.log(`→ Reading '${journeyId}' ${mode} from ${targetLabel(t)} …`);
|
|
4623
|
+
const { status, json } = await apiGet(`${base}/${encodeURIComponent(journeyId)}/${mode}`, t);
|
|
4624
|
+
if (status === 404)
|
|
4625
|
+
die(`journey '${journeyId}' not found (list them: octwin journeys)`);
|
|
4626
|
+
if (status !== 200)
|
|
4627
|
+
die(`could not read ${mode} (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
4628
|
+
if (asJson) {
|
|
4629
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4630
|
+
return;
|
|
4631
|
+
}
|
|
4632
|
+
if (json?.has_data === false) {
|
|
4633
|
+
printNoJourneyData(journeyId);
|
|
4634
|
+
return;
|
|
4635
|
+
}
|
|
4636
|
+
const range = json?.range ? ` (${String(json.range.from).slice(0, 10)} → ${String(json.range.to).slice(0, 10)})` : '';
|
|
4637
|
+
console.log(`Journey '${journeyId}' — ${mode}${range}:`);
|
|
4638
|
+
switch (mode) {
|
|
4639
|
+
case 'definition': {
|
|
4640
|
+
/**
|
|
4641
|
+
* Unmeasured: what the pack DECLARED. The one mode that answers "is this journey
|
|
4642
|
+
* even wired the way I think" with no traffic at all.
|
|
4643
|
+
*
|
|
4644
|
+
* The payload nests under `definition` and its own field names differ from the
|
|
4645
|
+
* measured modes: a stage carries `order` (not the funnel's `rank`), and a goal
|
|
4646
|
+
* names the `stage` it fires on. Read off the live route rather than assumed —
|
|
4647
|
+
* the first draft printed `?.` for every rank because it reused `rank`.
|
|
4648
|
+
*/
|
|
4649
|
+
const def = json?.definition ?? {};
|
|
4650
|
+
const stages = (def.stages ?? []);
|
|
4651
|
+
const goals = (def.goals ?? []);
|
|
4652
|
+
const events = (def.events ?? []);
|
|
4653
|
+
for (const s of stages) {
|
|
4654
|
+
console.log(` ${String(s.order ?? '?').padStart(2)}. ${String(s.id).padEnd(24)} ${pickLabel(s.label) ?? ''}`);
|
|
4655
|
+
}
|
|
4656
|
+
if (goals.length) {
|
|
4657
|
+
console.log(' goals:');
|
|
4658
|
+
for (const g of goals) {
|
|
4659
|
+
console.log(` ${String(g.id).padEnd(24)} ${String(pickLabel(g.label) ?? '').padEnd(22)}`
|
|
4660
|
+
+ `${g.stage ? ` on stage '${g.stage}'` : ''}${g.value != null ? ` value ${g.value}` : ''}`);
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
// An event is keyed by `name` and carries what it MOVES — `advances_to` a stage
|
|
4664
|
+
// and optionally `completes` a goal. That wiring is the whole reason to read a
|
|
4665
|
+
// definition, so it gets a row each rather than a comma list of names.
|
|
4666
|
+
if (events.length) {
|
|
4667
|
+
console.log(' events (what moves the journey):');
|
|
4668
|
+
for (const e of events) {
|
|
4669
|
+
console.log(` ${String(e.name).padEnd(24)} ${String(pickLabel(e.label) ?? '').padEnd(22)}`
|
|
4670
|
+
+ `${e.advances_to ? ` → stage '${e.advances_to}'` : ''}${e.completes ? `, completes '${e.completes}'` : ''}`);
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
4673
|
+
break;
|
|
4674
|
+
}
|
|
4675
|
+
case 'funnel':
|
|
4676
|
+
for (const s of (json?.funnel ?? [])) {
|
|
4677
|
+
const conv = s.conversion_from_prev_pct == null ? '' : ` ${s.conversion_from_prev_pct}% of prev`;
|
|
4678
|
+
const lost = s.drop_off_from_prev ? ` (−${s.drop_off_from_prev})` : '';
|
|
4679
|
+
console.log(` ${String(s.rank).padStart(2)}. ${String(pickLabel(s.label) ?? s.stage_id).padEnd(24)} ${String(s.reached).padStart(6)}${conv}${lost}`);
|
|
4680
|
+
}
|
|
4681
|
+
break;
|
|
4682
|
+
case 'overview': {
|
|
4683
|
+
const s = json?.summary ?? {};
|
|
4684
|
+
console.log(` entered ${s.entered} → converted ${s.converted}${s.conversion_pct == null ? '' : ` (${s.conversion_pct}%)`}`
|
|
4685
|
+
+ `${s.converted_basis ? ` [basis: ${s.converted_basis}]` : ''}`);
|
|
4686
|
+
if (s.biggest_dropoff)
|
|
4687
|
+
console.log(` biggest drop-off: ${s.biggest_dropoff.from} → ${s.biggest_dropoff.to} (lost ${s.biggest_dropoff.lost})`);
|
|
4688
|
+
if (s.top_goal)
|
|
4689
|
+
console.log(` top goal: ${pickLabel(s.top_goal.label) ?? s.top_goal.goal_id} (${s.top_goal.completions})`);
|
|
4690
|
+
break;
|
|
4691
|
+
}
|
|
4692
|
+
case 'goals':
|
|
4693
|
+
for (const g of (json?.goals ?? [])) {
|
|
4694
|
+
const p50 = g.p50_seconds == null ? '' : ` p50 ${Math.round(g.p50_seconds / 60)}m`;
|
|
4695
|
+
console.log(` ${String(pickLabel(g.label) ?? g.goal_id).padEnd(28)} ${String(g.completions).padStart(6)} completion(s),`
|
|
4696
|
+
+ ` ${g.unique_contacts} contact(s)${g.total_value ? `, value ${g.total_value}` : ''}${p50}`);
|
|
4697
|
+
}
|
|
4698
|
+
break;
|
|
4699
|
+
case 'trends':
|
|
4700
|
+
for (const b of (json?.buckets ?? [])) {
|
|
4701
|
+
console.log(` ${String(b.bucket).slice(0, 10)} active ${b.active_contacts} goals ${b.goal_completions}`);
|
|
4702
|
+
}
|
|
4703
|
+
break;
|
|
4704
|
+
case 'cost':
|
|
4705
|
+
for (const g of (json?.by_goal ?? [])) {
|
|
4706
|
+
const unknown = g.cost_unknown_rows ? ` (${g.cost_unknown_rows} row(s) unpriced)` : '';
|
|
4707
|
+
console.log(` ${String(pickLabel(g.label) ?? g.id).padEnd(24)} ${String(g.conversations).padStart(5)} conv,`
|
|
4708
|
+
+ ` ${String(g.total_tokens).padStart(8)} tokens, $${(g.cost_usd ?? 0).toFixed(4)}${unknown}`);
|
|
4709
|
+
}
|
|
4710
|
+
break;
|
|
4711
|
+
}
|
|
4712
|
+
}
|
|
4713
|
+
// ── performance: the project's business indicators ──────────────────────────
|
|
4714
|
+
/**
|
|
4715
|
+
* `octwin performance [--detail] [--json]` — the indicators the project's own
|
|
4716
|
+
* declarations produce: value, conversion, duration, per journey.
|
|
4717
|
+
*
|
|
4718
|
+
* Needs `records:read` — **not** a `performance:*` scope, which does not exist.
|
|
4719
|
+
* That means the Read-only token preset already reaches this.
|
|
4720
|
+
*/
|
|
4721
|
+
async function cmdPerformance(flags) {
|
|
4722
|
+
const t = resolveTarget(flags);
|
|
4723
|
+
const { url } = t;
|
|
4724
|
+
const base = `${url}/api/self/p/performance`;
|
|
4725
|
+
const asJson = flags.json === true;
|
|
4726
|
+
const detail = flags.detail === true;
|
|
4727
|
+
if (!asJson)
|
|
4728
|
+
console.log(`→ Reading business performance from ${targetLabel(t)} …`);
|
|
4729
|
+
// Two explicit calls rather than `apiGet(detail ? … : base)`: the route guard's
|
|
4730
|
+
// extractor only reads a template literal in the FIRST argument position, so a
|
|
4731
|
+
// ternary hides both URLs from it.
|
|
4732
|
+
const { status, json } = detail
|
|
4733
|
+
? await apiGet(`${base}/detail`, t)
|
|
4734
|
+
: await apiGet(`${base}`, t);
|
|
4735
|
+
if (status !== 200)
|
|
4736
|
+
die(`could not read performance (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
4737
|
+
if (asJson) {
|
|
4738
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4739
|
+
return;
|
|
4740
|
+
}
|
|
4741
|
+
if (json?.has_data === false) {
|
|
4742
|
+
console.log('No performance indicators — they are DERIVED from declarations (a journey with a');
|
|
4743
|
+
console.log('goal value, a pipelined entity), so a pack that declares none produces none.');
|
|
4744
|
+
return;
|
|
4745
|
+
}
|
|
4746
|
+
const r = json?.range ?? {};
|
|
4747
|
+
console.log(`Performance in ${targetLabel(t)}`
|
|
4748
|
+
+ `${r.from ? ` (${String(r.from).slice(0, 10)} → ${String(r.to).slice(0, 10)}, ${r.days ?? '?'}d, by ${json?.bucket ?? 'day'})` : ''}:`);
|
|
4749
|
+
const inds = (json?.indicators ?? []);
|
|
4750
|
+
if (inds.length === 0)
|
|
4751
|
+
console.log(' (none)');
|
|
4752
|
+
for (const i of inds) {
|
|
4753
|
+
const unit = i.unit === 'pct' ? '%' : '';
|
|
4754
|
+
// `delta_pct` is signed and against the PREVIOUS window — sign it explicitly so
|
|
4755
|
+
// a fall is never read as a rise.
|
|
4756
|
+
const delta = i.delta_pct == null ? '' : ` ${i.delta_pct >= 0 ? '+' : ''}${i.delta_pct}% vs prev`;
|
|
4757
|
+
const frac = i.numerator != null && i.denominator != null
|
|
4758
|
+
? ` (${i.numerator}/${i.denominator}${i.denominator_of ? ` ${i.denominator_of}` : ''})`
|
|
4759
|
+
: '';
|
|
4760
|
+
console.log(` ${String(pickLabel(i.heading) ?? i.kind).padEnd(16)} ${String(pickLabel(i.label) ?? '').padEnd(20)}`
|
|
4761
|
+
+ ` ${String(i.value ?? '—').padStart(9)}${unit}${delta}${frac}`);
|
|
4762
|
+
if (i.why)
|
|
4763
|
+
console.log(` why: ${i.why}`);
|
|
4764
|
+
if (i.biggest_dropoff)
|
|
4765
|
+
console.log(` biggest drop-off: ${i.biggest_dropoff.from} → ${i.biggest_dropoff.to} (lost ${i.biggest_dropoff.lost})`);
|
|
4766
|
+
}
|
|
4767
|
+
if (!detail)
|
|
4768
|
+
console.log('\nPer-indicator breakdown: octwin performance --detail');
|
|
4769
|
+
}
|
|
4770
|
+
// ── usage: model calls, tokens and cost ─────────────────────────────────────
|
|
4771
|
+
/** One `{ key, calls, total_tokens, cost_usd }` breakdown row. */
|
|
4772
|
+
function printUsageRows(title, rows) {
|
|
4773
|
+
if (!rows?.length)
|
|
4774
|
+
return;
|
|
4775
|
+
console.log(` ${title}:`);
|
|
4776
|
+
for (const r of rows) {
|
|
4777
|
+
console.log(` ${String(r.key ?? r.day).padEnd(40)} ${String(r.calls ?? '—').padStart(6)} call(s)`
|
|
4778
|
+
+ ` ${String(r.total_tokens ?? 0).padStart(10)} tokens $${(r.cost_usd ?? 0).toFixed(4)}`
|
|
4779
|
+
+ `${r.cost_partial ? ' (partial — some rows unpriced)' : ''}`);
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4782
|
+
/**
|
|
4783
|
+
* `octwin usage [--json]` — model calls, tokens and cost for the resolved scope.
|
|
4784
|
+
*
|
|
4785
|
+
* Needs NO scope beyond a valid token (the route is `requireTenantAccess`), which
|
|
4786
|
+
* is why it has no `COMMAND_REQUIREMENTS` entry: declaring one would print
|
|
4787
|
+
* "needs the X scope" on a failure whose cause is something else.
|
|
4788
|
+
*
|
|
4789
|
+
* Project-scoped when a project is resolved, tenant-wide otherwise — both routes
|
|
4790
|
+
* exist and the narrower one is the more useful default while testing a pack.
|
|
4791
|
+
* This is spend on MODEL calls; WhatsApp/Meta billing is operator-only and no
|
|
4792
|
+
* token can reach it.
|
|
4793
|
+
*/
|
|
4794
|
+
async function cmdUsage(flags) {
|
|
4795
|
+
const t = resolveTarget(flags);
|
|
4796
|
+
const { url } = t;
|
|
4797
|
+
const asJson = flags.json === true;
|
|
4798
|
+
const scoped = Boolean(t.project);
|
|
4799
|
+
if (!asJson)
|
|
4800
|
+
console.log(`→ Reading model usage for ${scoped ? targetLabel(t) : 'the whole workspace'} …`);
|
|
4801
|
+
// Both URLs written out in place, for the same reason as `performance` above: an
|
|
4802
|
+
// `endpoint` variable would leave BOTH invisible to `cli-routes.test.ts`.
|
|
4803
|
+
const { status, json } = scoped
|
|
4804
|
+
? await apiGet(`${url}/api/self/p/usage`, t)
|
|
4805
|
+
: await apiGet(`${url}/api/self/t/usage`, t);
|
|
4806
|
+
if (status !== 200)
|
|
4807
|
+
die(`could not read usage (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
4808
|
+
if (asJson) {
|
|
4809
|
+
console.log(JSON.stringify(json, null, 2));
|
|
4810
|
+
return;
|
|
4811
|
+
}
|
|
4812
|
+
const u = json?.usage ?? {};
|
|
4813
|
+
const tot = u.totals ?? {};
|
|
4814
|
+
const r = json?.range ?? {};
|
|
4815
|
+
console.log(`Model usage — ${scoped ? `project '${json?.project?.slug ?? t.project}'` : `workspace '${json?.tenant?.slug ?? ''}'`}`
|
|
4816
|
+
+ `${r.from ? ` (${String(r.from).slice(0, 10)} → ${String(r.to).slice(0, 10)})` : ''}`);
|
|
4817
|
+
console.log(` ${tot.calls ?? 0} call(s) ${tot.total_tokens ?? 0} tokens`
|
|
4818
|
+
+ ` (${tot.prompt_tokens ?? 0} in / ${tot.completion_tokens ?? 0} out) $${(tot.cost_usd ?? 0).toFixed(4)}`
|
|
4819
|
+
+ `${tot.cost_partial ? ' ⚠ partial: some calls had no price' : ''}`);
|
|
4820
|
+
if ((tot.calls ?? 0) === 0) {
|
|
4821
|
+
console.log(' (nothing in the window — drive a turn with `octwin chat`)');
|
|
4822
|
+
return;
|
|
4823
|
+
}
|
|
4824
|
+
printUsageRows('by model', u.by_model);
|
|
4825
|
+
printUsageRows('by kind', u.by_kind);
|
|
4826
|
+
printUsageRows('by agent', u.by_agent);
|
|
4827
|
+
printUsageRows('by channel', u.by_channel);
|
|
4828
|
+
// Not WhatsApp/Meta spend: that is operator-only and deliberately outside the
|
|
4829
|
+
// token scope registry, so this command cannot show it at all.
|
|
4830
|
+
console.log('\nThis is MODEL spend. WhatsApp/Meta message billing is operator-only — not reachable by an API token.');
|
|
4831
|
+
}
|
|
4019
4832
|
function help() {
|
|
4020
|
-
console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
|
|
4021
|
-
|
|
4022
|
-
octwin --version # print the CLI version (+ any upgrade notice)
|
|
4023
|
-
octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
4024
|
-
octwin validate [--dir .] [--remote] [--require-kb] # --remote runs the platform's FULL schema check + lint (all errors at once)
|
|
4025
|
-
octwin login --url <platformUrl> --token oct_… # a deploy token from the console
|
|
4026
|
-
octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
|
|
4027
|
-
octwin projects [--archived] [--json] # the --project slugs this token can name
|
|
4028
|
-
octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
4029
|
-
[--request-listing | --withdraw-listing] # public marketplace — opt-in, see: octwin help deploy
|
|
4030
|
-
octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
|
|
4031
|
-
octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
|
|
4032
|
-
octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
|
|
4033
|
-
octwin work [recordId] [--queues] [--json] # inspect the work inbox (worked records) + timelines
|
|
4034
|
-
octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
|
|
4035
|
-
octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
|
|
4036
|
-
octwin media generate "<prompt>" [--out <file.png>] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
|
|
4037
|
-
octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
|
|
4038
|
-
octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
|
|
4039
|
-
octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
|
|
4040
|
-
octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
|
|
4041
|
-
octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
|
|
4042
|
-
octwin
|
|
4043
|
-
octwin
|
|
4044
|
-
octwin
|
|
4045
|
-
octwin
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
octwin
|
|
4049
|
-
octwin
|
|
4050
|
-
octwin
|
|
4051
|
-
octwin
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
octwin
|
|
4055
|
-
octwin
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
octwin
|
|
4062
|
-
|
|
4833
|
+
console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
|
|
4834
|
+
|
|
4835
|
+
octwin --version # print the CLI version (+ any upgrade notice)
|
|
4836
|
+
octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
4837
|
+
octwin validate [--dir .] [--remote] [--require-kb] # --remote runs the platform's FULL schema check + lint (all errors at once)
|
|
4838
|
+
octwin login --url <platformUrl> --token oct_… # a deploy token from the console
|
|
4839
|
+
octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
|
|
4840
|
+
octwin projects [--archived] [--json] # the --project slugs this token can name
|
|
4841
|
+
octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
4842
|
+
[--request-listing | --withdraw-listing] # public marketplace — opt-in, see: octwin help deploy
|
|
4843
|
+
octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
|
|
4844
|
+
octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
|
|
4845
|
+
octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
|
|
4846
|
+
octwin work [recordId] [--queues] [--json] # inspect the work inbox (worked records) + timelines
|
|
4847
|
+
octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
|
|
4848
|
+
octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
|
|
4849
|
+
octwin media generate "<prompt>" [--out <file.png>] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
|
|
4850
|
+
octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
|
|
4851
|
+
octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
|
|
4852
|
+
octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
|
|
4853
|
+
octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
|
|
4854
|
+
octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
|
|
4855
|
+
octwin automation [campaigns] [--json] # the jobs your declarations produced + health, last result each
|
|
4856
|
+
octwin integrations [--json] # declared connections BESIDE what is configured (the silent-never-fires check)
|
|
4857
|
+
octwin integrations deliveries [<id>] | events # the outbound delivery log / inbound events
|
|
4858
|
+
octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]
|
|
4859
|
+
octwin performance [--detail] [--json] # the project's business indicators (value/conversion/duration)
|
|
4860
|
+
octwin usage [--json] # model calls, tokens and cost (project if pinned, else workspace)
|
|
4861
|
+
octwin platform-kb [pull] [--if-stale|--check] [--dir .] [--url <url>] # no token needed
|
|
4862
|
+
octwin test [--dir .] # = validate --remote (the full platform check)
|
|
4863
|
+
octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
|
|
4864
|
+
octwin memos [--all] [--json] # read the platform's replies + notices (a reply to your feedback lands here)
|
|
4865
|
+
|
|
4866
|
+
Writes — exercise the state your pack creates (each needs the matching :write scope):
|
|
4867
|
+
octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…"
|
|
4868
|
+
octwin records tasks | task complete <taskId> [--outcome done|cancelled]
|
|
4869
|
+
octwin work assign <id> --to user:<uuid>|none | note <id> "…" | stage <id> --to <stage>
|
|
4870
|
+
octwin work decide <id> --action <a> [--param k=v] [--dry-run] # --dry-run previews, commits nothing
|
|
4871
|
+
octwin orders transition <ref> --to <status> | refund <ref> --force
|
|
4872
|
+
octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
|
|
4873
|
+
octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
|
|
4874
|
+
octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
|
|
4875
|
+
octwin automation run <jobId> | pause <jobId> | resume <jobId> | send <campaignId>
|
|
4876
|
+
octwin integrations test <key> # a LIVE call to the connection's health: operation
|
|
4877
|
+
octwin integrations retry|cancel|send-now <deliveryId>
|
|
4878
|
+
(octwin integrations preflight <key> needs only integrations:read — it makes no call)
|
|
4879
|
+
|
|
4880
|
+
Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
|
|
4881
|
+
\`octwin chat --as <h>\` calls continue the same conversation; press a rendered
|
|
4882
|
+
button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
|
|
4883
|
+
Get a deploy token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
|
|
4884
|
+
octwin platform-kb pull → writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
|
|
4885
|
+
Config (deploy): flags > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login (\`octwin login\` sets the default target).
|
|
4063
4886
|
Per-command usage: octwin <command> --help`);
|
|
4064
4887
|
}
|
|
4065
4888
|
/** Per-subcommand usage — printed for `octwin <cmd> --help|-h` BEFORE any
|
|
4066
4889
|
* network/auth work (a --help that 401s is worse than no help at all). */
|
|
4067
4890
|
const COMMAND_HELP = {
|
|
4068
|
-
init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
4891
|
+
init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
4069
4892
|
Scaffold a pure-YAML starter pack into <dir>.`,
|
|
4070
|
-
validate: `octwin validate [--dir .] [--remote] [--require-kb] [--strict-primitives]
|
|
4071
|
-
Offline structural check, plus two checks driven by the pulled capability
|
|
4072
|
-
reference (render-intent fields, primitive arguments). Those two SKIP when the
|
|
4073
|
-
reference is missing — the run says so, and --require-kb turns the skip into a
|
|
4074
|
-
failure for CI. --remote additionally runs the platform's FULL manifest +
|
|
4075
|
-
flow-DSL validation and its flow lint (all errors at once) — same check as deploy.
|
|
4076
|
-
--strict-primitives (with --remote) additionally type-checks LITERAL args:
|
|
4077
|
-
values against each primitive's declared input schema; expression strings
|
|
4893
|
+
validate: `octwin validate [--dir .] [--remote] [--require-kb] [--strict-primitives]
|
|
4894
|
+
Offline structural check, plus two checks driven by the pulled capability
|
|
4895
|
+
reference (render-intent fields, primitive arguments). Those two SKIP when the
|
|
4896
|
+
reference is missing — the run says so, and --require-kb turns the skip into a
|
|
4897
|
+
failure for CI. --remote additionally runs the platform's FULL manifest +
|
|
4898
|
+
flow-DSL validation and its flow lint (all errors at once) — same check as deploy.
|
|
4899
|
+
--strict-primitives (with --remote) additionally type-checks LITERAL args:
|
|
4900
|
+
values against each primitive's declared input schema; expression strings
|
|
4078
4901
|
('$found.id', '{$t(…)}') are always exempt.`,
|
|
4079
|
-
login: `octwin login --url <platformUrl> --token oct_…
|
|
4080
|
-
Save a deploy token (console → Settings → API tokens) for that platform url,
|
|
4081
|
-
make that url the DEFAULT deploy target for every later command, and echo the
|
|
4902
|
+
login: `octwin login --url <platformUrl> --token oct_…
|
|
4903
|
+
Save a deploy token (console → Settings → API tokens) for that platform url,
|
|
4904
|
+
make that url the DEFAULT deploy target for every later command, and echo the
|
|
4082
4905
|
workspace + project pin + scopes the token reaches.`,
|
|
4083
|
-
whoami: `octwin whoami [--url <url>] [--tenant <slug>]
|
|
4906
|
+
whoami: `octwin whoami [--url <url>] [--tenant <slug>]
|
|
4084
4907
|
Verify the resolved token authenticates against the tenant.`,
|
|
4085
|
-
projects: `octwin projects [--archived] [--json]
|
|
4086
|
-
List the workspace's projects — the slugs every --project flag takes, with the
|
|
4087
|
-
plan's project cap. --archived includes archived ones. A pack:deploy token
|
|
4088
|
-
reaches this (it names a project in every other command).
|
|
4089
|
-
|
|
4090
|
-
octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
|
|
4091
|
-
Create a project. The URL slug is derived from the name unless --slug pins one.
|
|
4092
|
-
--pack installs an ALREADY-published pack; the usual next step is instead
|
|
4093
|
-
\`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
|
|
4094
|
-
|
|
4095
|
-
octwin projects rm <slug> [--yes]
|
|
4096
|
-
HARD delete — the project and everything cascading from it (conversations,
|
|
4097
|
-
contacts, records, installs). No undo, and not the same as archiving.
|
|
4098
|
-
WITHOUT --yes it only previews what would be destroyed, so the dry run is the
|
|
4099
|
-
default. Together these make a disposable end-to-end environment:
|
|
4100
|
-
octwin projects create "Scratch" && octwin deploy --project scratch --seed
|
|
4101
|
-
octwin chat "hi" --project scratch
|
|
4102
|
-
octwin projects rm scratch --yes
|
|
4908
|
+
projects: `octwin projects [--archived] [--json]
|
|
4909
|
+
List the workspace's projects — the slugs every --project flag takes, with the
|
|
4910
|
+
plan's project cap. --archived includes archived ones. A pack:deploy token
|
|
4911
|
+
reaches this (it names a project in every other command).
|
|
4912
|
+
|
|
4913
|
+
octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
|
|
4914
|
+
Create a project. The URL slug is derived from the name unless --slug pins one.
|
|
4915
|
+
--pack installs an ALREADY-published pack; the usual next step is instead
|
|
4916
|
+
\`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
|
|
4917
|
+
|
|
4918
|
+
octwin projects rm <slug> [--yes]
|
|
4919
|
+
HARD delete — the project and everything cascading from it (conversations,
|
|
4920
|
+
contacts, records, installs). No undo, and not the same as archiving.
|
|
4921
|
+
WITHOUT --yes it only previews what would be destroyed, so the dry run is the
|
|
4922
|
+
default. Together these make a disposable end-to-end environment:
|
|
4923
|
+
octwin projects create "Scratch" && octwin deploy --project scratch --seed
|
|
4924
|
+
octwin chat "hi" --project scratch
|
|
4925
|
+
octwin projects rm scratch --yes
|
|
4103
4926
|
Both verbs need the \`projects:write\` scope — a pack:deploy token does NOT confer it.`,
|
|
4104
|
-
deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
4105
|
-
[--request-listing | --withdraw-listing]
|
|
4106
|
-
Upload the pack bundle, validate server-side, install onto the project.
|
|
4107
|
-
--seed additionally applies the pack's demo seed (streams progress).
|
|
4108
|
-
|
|
4109
|
-
A plain deploy says NOTHING about the public marketplace — it is a test loop, so it
|
|
4110
|
-
neither asks for a listing nor gives one up. The marketplace flags are opt-in:
|
|
4111
|
-
|
|
4112
|
-
--request-listing ask an operator to review this pack for the public marketplace
|
|
4113
|
-
(the pre-signup storefront at /packs). Requires 'public: true'
|
|
4114
|
-
under 'listing:' in manifest.yaml — the manifest states that the
|
|
4115
|
-
pack is a product, the flag is you choosing to ask.
|
|
4116
|
-
--withdraw-listing retract the request, including an approved listing.
|
|
4117
|
-
|
|
4118
|
-
An approval covers the CONTENT it was made against, so a later deploy that changes the
|
|
4927
|
+
deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
4928
|
+
[--request-listing | --withdraw-listing]
|
|
4929
|
+
Upload the pack bundle, validate server-side, install onto the project.
|
|
4930
|
+
--seed additionally applies the pack's demo seed (streams progress).
|
|
4931
|
+
|
|
4932
|
+
A plain deploy says NOTHING about the public marketplace — it is a test loop, so it
|
|
4933
|
+
neither asks for a listing nor gives one up. The marketplace flags are opt-in:
|
|
4934
|
+
|
|
4935
|
+
--request-listing ask an operator to review this pack for the public marketplace
|
|
4936
|
+
(the pre-signup storefront at /packs). Requires 'public: true'
|
|
4937
|
+
under 'listing:' in manifest.yaml — the manifest states that the
|
|
4938
|
+
pack is a product, the flag is you choosing to ask.
|
|
4939
|
+
--withdraw-listing retract the request, including an approved listing.
|
|
4940
|
+
|
|
4941
|
+
An approval covers the CONTENT it was made against, so a later deploy that changes the
|
|
4119
4942
|
pack returns it to the review queue on its own — no flag needed, and the CLI says so.`,
|
|
4120
|
-
seed: `octwin seed [--pack <packId>]
|
|
4121
|
-
Apply the pack's demo/reference data to the project it is installed on, without
|
|
4122
|
-
redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
|
|
4123
|
-
and the demo operator topology. Reports what each kind produced.
|
|
4124
|
-
Idempotent and safe to re-run — records upsert, and existing media is REUSED rather
|
|
4125
|
-
than regenerated, so a second pass costs nothing. --pack is only needed when a
|
|
4943
|
+
seed: `octwin seed [--pack <packId>]
|
|
4944
|
+
Apply the pack's demo/reference data to the project it is installed on, without
|
|
4945
|
+
redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
|
|
4946
|
+
and the demo operator topology. Reports what each kind produced.
|
|
4947
|
+
Idempotent and safe to re-run — records upsert, and existing media is REUSED rather
|
|
4948
|
+
than regenerated, so a second pass costs nothing. --pack is only needed when a
|
|
4126
4949
|
project somehow runs more than one.`,
|
|
4127
|
-
status: `octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
|
|
4128
|
-
Show installed vs live version + the flow list for this pack.
|
|
4129
|
-
The pack id is read from manifest.yaml and QUALIFIED with your workspace slug
|
|
4130
|
-
(a manifest declares a bare name; the owner is attached when you publish). Pass
|
|
4131
|
-
<packId> explicitly to skip that lookup — \`octwin agents\` and \`octwin projects\`
|
|
4950
|
+
status: `octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
|
|
4951
|
+
Show installed vs live version + the flow list for this pack.
|
|
4952
|
+
The pack id is read from manifest.yaml and QUALIFIED with your workspace slug
|
|
4953
|
+
(a manifest declares a bare name; the owner is attached when you publish). Pass
|
|
4954
|
+
<packId> explicitly to skip that lookup — \`octwin agents\` and \`octwin projects\`
|
|
4132
4955
|
both print the qualified form.`,
|
|
4133
|
-
records: `octwin records [entity] [id] [--limit 50] [--offset n]
|
|
4134
|
-
Inspect the pack's XRM data. No args = list entities. Worked records (cases,
|
|
4135
|
-
tickets, anything routed to a queue) read best through \`octwin work\`.
|
|
4136
|
-
|
|
4137
|
-
WRITES (need \`records:write\`; every one is re-checked by RBAC on the record):
|
|
4138
|
-
octwin records create <entity> --set field=value [--set …] [--stage s] [--contact <id>]
|
|
4139
|
-
octwin records patch <recordId> --entity <entity> --set field=value
|
|
4140
|
-
octwin records stage <recordId> --to <stage> [--note "..."]
|
|
4141
|
-
octwin records note <recordId> "the note text"
|
|
4142
|
-
octwin records tasks # open follow-up tasks (\`tasks\` plan feature)
|
|
4143
|
-
octwin records task complete <taskId> [--outcome done|cancelled] [--note "..."]
|
|
4144
|
-
|
|
4145
|
-
--set coerces JSON scalars: \`--set rating=4.5\` sends a number, \`--set x=null\`
|
|
4146
|
-
sends null. Use --fields-json '{"a":{"b":1}}' for anything nested.
|
|
4147
|
-
\`patch\` needs --entity even though it has an id: the route resolves the field
|
|
4148
|
-
validator from it. A leading \`create/patch/stage/note/tasks/task\` is read as a
|
|
4956
|
+
records: `octwin records [entity] [id] [--limit 50] [--offset n]
|
|
4957
|
+
Inspect the pack's XRM data. No args = list entities. Worked records (cases,
|
|
4958
|
+
tickets, anything routed to a queue) read best through \`octwin work\`.
|
|
4959
|
+
|
|
4960
|
+
WRITES (need \`records:write\`; every one is re-checked by RBAC on the record):
|
|
4961
|
+
octwin records create <entity> --set field=value [--set …] [--stage s] [--contact <id>]
|
|
4962
|
+
octwin records patch <recordId> --entity <entity> --set field=value
|
|
4963
|
+
octwin records stage <recordId> --to <stage> [--note "..."]
|
|
4964
|
+
octwin records note <recordId> "the note text"
|
|
4965
|
+
octwin records tasks # open follow-up tasks (\`tasks\` plan feature)
|
|
4966
|
+
octwin records task complete <taskId> [--outcome done|cancelled] [--note "..."]
|
|
4967
|
+
|
|
4968
|
+
--set coerces JSON scalars: \`--set rating=4.5\` sends a number, \`--set x=null\`
|
|
4969
|
+
sends null. Use --fields-json '{"a":{"b":1}}' for anything nested.
|
|
4970
|
+
\`patch\` needs --entity even though it has an id: the route resolves the field
|
|
4971
|
+
validator from it. A leading \`create/patch/stage/note/tasks/task\` is read as a
|
|
4149
4972
|
VERB — to list an entity actually named one of those, use \`--entity <name>\`.`,
|
|
4150
|
-
work: `octwin work [recordId] [--queues] [--limit 50] [--offset n] [--json]
|
|
4151
|
-
Inspect the work inbox — every entity the pack declares worked (cases, orders
|
|
4152
|
-
needing review, applications, …): the inbox, one item + its timeline
|
|
4153
|
-
(+ applicable actions), or --queues for queue keys + open counts.
|
|
4154
|
-
|
|
4155
|
-
WRITES (need \`work:write\`; \`stage\` needs \`records:write\`):
|
|
4156
|
-
octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none
|
|
4157
|
-
octwin work note <recordId> "the note text"
|
|
4158
|
-
octwin work stage <recordId> --to <stage> [--note "..."]
|
|
4159
|
-
octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]
|
|
4160
|
-
|
|
4161
|
-
\`decide\` applies one of the entity's declared operator actions — \`octwin work <id>\`
|
|
4162
|
-
lists them with their params. --dry-run previews the customer-facing copy and the
|
|
4163
|
-
resulting stage WITHOUT committing (that route needs only \`work:read\`).
|
|
4973
|
+
work: `octwin work [recordId] [--queues] [--limit 50] [--offset n] [--json]
|
|
4974
|
+
Inspect the work inbox — every entity the pack declares worked (cases, orders
|
|
4975
|
+
needing review, applications, …): the inbox, one item + its timeline
|
|
4976
|
+
(+ applicable actions), or --queues for queue keys + open counts.
|
|
4977
|
+
|
|
4978
|
+
WRITES (need \`work:write\`; \`stage\` needs \`records:write\`):
|
|
4979
|
+
octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none
|
|
4980
|
+
octwin work note <recordId> "the note text"
|
|
4981
|
+
octwin work stage <recordId> --to <stage> [--note "..."]
|
|
4982
|
+
octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]
|
|
4983
|
+
|
|
4984
|
+
\`decide\` applies one of the entity's declared operator actions — \`octwin work <id>\`
|
|
4985
|
+
lists them with their params. --dry-run previews the customer-facing copy and the
|
|
4986
|
+
resulting stage WITHOUT committing (that route needs only \`work:read\`).
|
|
4164
4987
|
\`stage\` is the XRM records verb (one transition spelling platform-wide).`,
|
|
4165
|
-
logs: `octwin logs [conversationId] [--as <handle>] [--json]
|
|
4166
|
-
No id = recent conversations (handle, status, last activity; --as filters).
|
|
4167
|
-
With id = the full event timeline including what each turn rendered.
|
|
4988
|
+
logs: `octwin logs [conversationId] [--as <handle>] [--json]
|
|
4989
|
+
No id = recent conversations (handle, status, last activity; --as filters).
|
|
4990
|
+
With id = the full event timeline including what each turn rendered.
|
|
4168
4991
|
--json = raw events (verbatim payloads).`,
|
|
4169
|
-
pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
|
|
4170
|
-
Write a DEPLOYED pack's source back to disk — the inverse of deploy.
|
|
4171
|
-
A pack pushed with 'octwin deploy' lives on the platform as an artifact the
|
|
4172
|
-
runtime serves but nothing hands back, so its only source copy is the machine
|
|
4173
|
-
that pushed it. Pull it, fix it, redeploy it.
|
|
4174
|
-
Defaults to the version installed on the target project; --version overrides.
|
|
4175
|
-
--dir defaults to ./<packId>; a non-empty dir needs --force.
|
|
4176
|
-
The pulled dir redeploys where it came from — the target is your saved login.
|
|
4992
|
+
pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
|
|
4993
|
+
Write a DEPLOYED pack's source back to disk — the inverse of deploy.
|
|
4994
|
+
A pack pushed with 'octwin deploy' lives on the platform as an artifact the
|
|
4995
|
+
runtime serves but nothing hands back, so its only source copy is the machine
|
|
4996
|
+
that pushed it. Pull it, fix it, redeploy it.
|
|
4997
|
+
Defaults to the version installed on the target project; --version overrides.
|
|
4998
|
+
--dir defaults to ./<packId>; a non-empty dir needs --force.
|
|
4999
|
+
The pulled dir redeploys where it came from — the target is your saved login.
|
|
4177
5000
|
You may pull a pack your tenant OWNS (deployed); an operator token pulls any.`,
|
|
4178
|
-
chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
|
|
4179
|
-
octwin chat --script <file> [--as <handle>] [--json]
|
|
4180
|
-
Drive ONE turn through the dev web channel and print every render with its
|
|
4181
|
-
tap ids. Same --as handle = same conversation (multi-turn works).
|
|
4182
|
-
--tap presses a rendered button/list row instead of sending text.
|
|
4183
|
-
--media uploads a local file (or a media id from 'media generate --json') as
|
|
4184
|
-
an image/document/audio inbound — any "message" rides as its caption; feeds a
|
|
4185
|
-
running media-collect flow (e.g. activate-app).
|
|
4186
|
-
--json dumps the raw SSE envelopes for the turn.
|
|
4187
|
-
|
|
4188
|
-
--script drives a WHOLE conversation from a file, ONE TURN PER LINE, in one
|
|
4189
|
-
process over one connection — waiting for each turn to settle before sending
|
|
4190
|
-
the next. Use this for any multi-step flow: chaining shell invocations races
|
|
4191
|
-
the agent loop, because a turn ends on a quiet gap that can arrive while the
|
|
4192
|
-
server is still working (the symptom is placeholder-filled fields or a second
|
|
4193
|
-
workflow run). Blank lines and # comments are skipped:
|
|
4194
|
-
|
|
4195
|
-
# book an appointment end to end
|
|
4196
|
-
احجز موعد
|
|
4197
|
-
tap:t:invoke:book-appointment:doctor_id=D1
|
|
4198
|
-
media:./licence.jpg | here is my licence
|
|
5001
|
+
chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
|
|
5002
|
+
octwin chat --script <file> [--as <handle>] [--json]
|
|
5003
|
+
Drive ONE turn through the dev web channel and print every render with its
|
|
5004
|
+
tap ids. Same --as handle = same conversation (multi-turn works).
|
|
5005
|
+
--tap presses a rendered button/list row instead of sending text.
|
|
5006
|
+
--media uploads a local file (or a media id from 'media generate --json') as
|
|
5007
|
+
an image/document/audio inbound — any "message" rides as its caption; feeds a
|
|
5008
|
+
running media-collect flow (e.g. activate-app).
|
|
5009
|
+
--json dumps the raw SSE envelopes for the turn.
|
|
5010
|
+
|
|
5011
|
+
--script drives a WHOLE conversation from a file, ONE TURN PER LINE, in one
|
|
5012
|
+
process over one connection — waiting for each turn to settle before sending
|
|
5013
|
+
the next. Use this for any multi-step flow: chaining shell invocations races
|
|
5014
|
+
the agent loop, because a turn ends on a quiet gap that can arrive while the
|
|
5015
|
+
server is still working (the symptom is placeholder-filled fields or a second
|
|
5016
|
+
workflow run). Blank lines and # comments are skipped:
|
|
5017
|
+
|
|
5018
|
+
# book an appointment end to end
|
|
5019
|
+
احجز موعد
|
|
5020
|
+
tap:t:invoke:book-appointment:doctor_id=D1
|
|
5021
|
+
media:./licence.jpg | here is my licence
|
|
4199
5022
|
tap:t:resume:book-appointment:run_id=R1;_ctl_approved=true`,
|
|
4200
|
-
media: `octwin media generate "<prompt>" [--out <file.png>] [--json]
|
|
4201
|
-
AI-generate an image (needs a media:generate-scoped token), store it as a
|
|
4202
|
-
public asset, and print its MEDIA- handle + serve URL. --out downloads the
|
|
4203
|
-
bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
|
|
5023
|
+
media: `octwin media generate "<prompt>" [--out <file.png>] [--json]
|
|
5024
|
+
AI-generate an image (needs a media:generate-scoped token), store it as a
|
|
5025
|
+
public asset, and print its MEDIA- handle + serve URL. --out downloads the
|
|
5026
|
+
bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
|
|
4204
5027
|
bytes }. Pair with 'octwin chat --media' to drive media flows.`,
|
|
4205
|
-
agents: `octwin agents [packId::agentId] [--prompt] [--json]
|
|
4206
|
-
No args = the roster with each agent's EFFECTIVE model and which layer set it.
|
|
4207
|
-
With an agent = every governed setting (model / memory.last_messages /
|
|
4208
|
-
working_memory) plus the layer that won — an operator PLATFORM default can
|
|
4209
|
-
override what your manifest declares, and this is where you see that.
|
|
4210
|
-
--prompt = the exact system prompt the LLM sees for this project (pack
|
|
4211
|
-
instructions + platform protocol + any project overlay). Needs agents:read.
|
|
4212
|
-
The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
|
|
4213
|
-
|
|
4214
|
-
WRITES (need \`agents:write\`):
|
|
4215
|
-
octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
|
|
4216
|
-
[--enable-tool <toolId>] [--disable-tool <toolId>]
|
|
4217
|
-
|
|
4218
|
-
Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
|
|
4219
|
-
so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
|
|
5028
|
+
agents: `octwin agents [packId::agentId] [--prompt] [--json]
|
|
5029
|
+
No args = the roster with each agent's EFFECTIVE model and which layer set it.
|
|
5030
|
+
With an agent = every governed setting (model / memory.last_messages /
|
|
5031
|
+
working_memory) plus the layer that won — an operator PLATFORM default can
|
|
5032
|
+
override what your manifest declares, and this is where you see that.
|
|
5033
|
+
--prompt = the exact system prompt the LLM sees for this project (pack
|
|
5034
|
+
instructions + platform protocol + any project overlay). Needs agents:read.
|
|
5035
|
+
The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
|
|
5036
|
+
|
|
5037
|
+
WRITES (need \`agents:write\`):
|
|
5038
|
+
octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
|
|
5039
|
+
[--enable-tool <toolId>] [--disable-tool <toolId>]
|
|
5040
|
+
|
|
5041
|
+
Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
|
|
5042
|
+
so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
|
|
4220
5043
|
ids refuses --model with a 403 — the platform default governs there.`,
|
|
4221
|
-
orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
|
|
4222
|
-
No args = the order list (#number, status/payment, total, contact). With a
|
|
4223
|
-
reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
|
|
4224
|
-
payment_ref, and the allowed status transitions. Needs orders:read + the
|
|
4225
|
-
\`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
|
|
4226
|
-
so \`pending\` on a gateway-less workspace is expected, not a bug.
|
|
4227
|
-
|
|
4228
|
-
WRITES (need \`orders:write\`):
|
|
4229
|
-
octwin orders transition <reference_id> --to <status>
|
|
4230
|
-
octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
|
|
4231
|
-
|
|
4232
|
-
Refund is irreversible and moves money, hence --force. The route answers 200 even
|
|
4233
|
-
when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
|
|
4234
|
-
on a refusal rather than reporting a refund that never happened. Only a payment in
|
|
5044
|
+
orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
|
|
5045
|
+
No args = the order list (#number, status/payment, total, contact). With a
|
|
5046
|
+
reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
|
|
5047
|
+
payment_ref, and the allowed status transitions. Needs orders:read + the
|
|
5048
|
+
\`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
|
|
5049
|
+
so \`pending\` on a gateway-less workspace is expected, not a bug.
|
|
5050
|
+
|
|
5051
|
+
WRITES (need \`orders:write\`):
|
|
5052
|
+
octwin orders transition <reference_id> --to <status>
|
|
5053
|
+
octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
|
|
5054
|
+
|
|
5055
|
+
Refund is irreversible and moves money, hence --force. The route answers 200 even
|
|
5056
|
+
when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
|
|
5057
|
+
on a refusal rather than reporting a refund that never happened. Only a payment in
|
|
4235
5058
|
\`captured\` state can be refunded; \`payment_status\` is never settable directly.`,
|
|
4236
|
-
analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
|
|
4237
|
-
No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
|
|
4238
|
-
With an entity = stage-by-stage conversion (default --funnel) over the last 30
|
|
4239
|
-
days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
|
|
5059
|
+
analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
|
|
5060
|
+
No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
|
|
5061
|
+
With an entity = stage-by-stage conversion (default --funnel) over the last 30
|
|
5062
|
+
days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
|
|
4240
5063
|
range-filtered). Needs records:read + a \`view\` grant on \`record.<entity>\`.`,
|
|
4241
|
-
catalog: `octwin catalog [--readiness] [--json]
|
|
4242
|
-
The commerce \`product\` records + price, availability, stock (null = not
|
|
4243
|
-
inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
|
|
4244
|
-
Graph checklist (LIVE Graph calls; needs a bound access token). Needs
|
|
4245
|
-
catalog:read + the \`catalog\` plan feature.
|
|
4246
|
-
|
|
4247
|
-
WRITES (need \`catalog:write\`):
|
|
4248
|
-
octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
|
|
4249
|
-
octwin catalog stock <retailerId> [--set-on-hand <n>]
|
|
4250
|
-
|
|
4251
|
-
\`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
|
|
4252
|
-
inventory-tracked (always sellable), which is different from 0. Lowering on_hand
|
|
4253
|
-
below the units already reserved for open carts is refused. Creating/deleting
|
|
5064
|
+
catalog: `octwin catalog [--readiness] [--json]
|
|
5065
|
+
The commerce \`product\` records + price, availability, stock (null = not
|
|
5066
|
+
inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
|
|
5067
|
+
Graph checklist (LIVE Graph calls; needs a bound access token). Needs
|
|
5068
|
+
catalog:read + the \`catalog\` plan feature.
|
|
5069
|
+
|
|
5070
|
+
WRITES (need \`catalog:write\`):
|
|
5071
|
+
octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
|
|
5072
|
+
octwin catalog stock <retailerId> [--set-on-hand <n>]
|
|
5073
|
+
|
|
5074
|
+
\`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
|
|
5075
|
+
inventory-tracked (always sellable), which is different from 0. Lowering on_hand
|
|
5076
|
+
below the units already reserved for open carts is refused. Creating/deleting
|
|
4254
5077
|
products and the Meta catalog binding/sync stay in the console.`,
|
|
4255
|
-
scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
|
|
4256
|
-
No args = the engine state (bookable resource types, upcoming slots, booked
|
|
4257
|
-
seats). --slots <recordId> computes the slots for one bookable resource
|
|
4258
|
-
(occupancy included; --days is clamped to 1-31 server-side) — the way to verify
|
|
4259
|
-
the availability rules a \`deploy --seed\` created. Needs scheduling:read.
|
|
4260
|
-
|
|
4261
|
-
RULES (list needs scheduling:read; add/rm need scheduling:write):
|
|
4262
|
-
octwin scheduling rules --resource <resourceRecordId>
|
|
4263
|
-
octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
|
|
4264
|
-
[--slot-minutes 30] [--capacity 1]
|
|
4265
|
-
octwin scheduling rule rm <ruleId>
|
|
4266
|
-
octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
|
|
4267
|
-
[--start 09:00 --end 13:00]
|
|
4268
|
-
octwin scheduling exception rm <exceptionId>
|
|
4269
|
-
|
|
4270
|
-
--dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
|
|
5078
|
+
scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
|
|
5079
|
+
No args = the engine state (bookable resource types, upcoming slots, booked
|
|
5080
|
+
seats). --slots <recordId> computes the slots for one bookable resource
|
|
5081
|
+
(occupancy included; --days is clamped to 1-31 server-side) — the way to verify
|
|
5082
|
+
the availability rules a \`deploy --seed\` created. Needs scheduling:read.
|
|
5083
|
+
|
|
5084
|
+
RULES (list needs scheduling:read; add/rm need scheduling:write):
|
|
5085
|
+
octwin scheduling rules --resource <resourceRecordId>
|
|
5086
|
+
octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
|
|
5087
|
+
[--slot-minutes 30] [--capacity 1]
|
|
5088
|
+
octwin scheduling rule rm <ruleId>
|
|
5089
|
+
octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
|
|
5090
|
+
[--start 09:00 --end 13:00]
|
|
5091
|
+
octwin scheduling exception rm <exceptionId>
|
|
5092
|
+
|
|
5093
|
+
--dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
|
|
4271
5094
|
\`--slots\` is how you check what a rule actually produces.`,
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
5095
|
+
automation: `octwin automation [campaigns] [--limit n] [--offset n] [--json]
|
|
5096
|
+
No args = every job the pack's automation declaration produced, with its status,
|
|
5097
|
+
interval and LAST RESULT (matched / acted / errors), under a health line whose
|
|
5098
|
+
counts come from SQL rather than from filtering the page — the job list is capped
|
|
5099
|
+
server-side, so a client-side count would depend on the cap. Needs automation:read.
|
|
5100
|
+
|
|
5101
|
+
Jobs are DERIVED from declarations. There is no \`create\`: no automation block in
|
|
5102
|
+
the pack means no jobs, and \`octwin deploy\` is what installs them.
|
|
5103
|
+
|
|
5104
|
+
WRITES (automation:write):
|
|
5105
|
+
octwin automation run <jobId> # run once, now — prints matched/acted/errors
|
|
5106
|
+
octwin automation pause|resume <jobId>
|
|
5107
|
+
octwin automation send <campaignId> # enqueue a campaign; enqueued != delivered
|
|
5108
|
+
|
|
5109
|
+
<jobId> is the \`key\` the list shows (its uuid works too). The routes themselves
|
|
5110
|
+
accept only a uuid — the CLI resolves the key for you, and names the keys that do
|
|
5111
|
+
exist when it cannot. A 403 on a write can be an RBAC grant gap rather than a
|
|
5112
|
+
missing scope: the action is re-checked against the job.`,
|
|
5113
|
+
integrations: `octwin integrations [--json]
|
|
5114
|
+
What the pack DECLARES beside what is actually CONFIGURED, in one view — because a
|
|
5115
|
+
connection that is declared and never configured is the commonest reason an
|
|
5116
|
+
integration silently never fires, and neither list alone can show it. Flags the
|
|
5117
|
+
gap explicitly. Needs integrations:read.
|
|
5118
|
+
|
|
5119
|
+
DIAGNOSE ONE CONNECTION:
|
|
5120
|
+
octwin integrations preflight <key> # every check, with a fix hint. Makes NO
|
|
5121
|
+
# outbound call — needs only integrations:read
|
|
5122
|
+
octwin integrations test <key> # a LIVE call to its health: operation
|
|
5123
|
+
# (integrations:write). Exits 1 when it fails.
|
|
5124
|
+
|
|
5125
|
+
THE DELIVERY LOG:
|
|
5126
|
+
octwin integrations deliveries [--status s] [--operation id] [--limit n]
|
|
5127
|
+
octwin integrations deliveries <id> # + the redacted request/response snapshots
|
|
5128
|
+
octwin integrations retry|cancel|send-now <id> # integrations:write
|
|
5129
|
+
octwin integrations events # INBOUND events (what arrived at your webhook)
|
|
5130
|
+
|
|
5131
|
+
retry/cancel answer 409 when the delivery is in the wrong state; the message
|
|
5132
|
+
carries the rule.`,
|
|
5133
|
+
journeys: `octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition]
|
|
5134
|
+
[--stage <stageId>] [--limit n] [--json]
|
|
5135
|
+
No args = the journeys the pack declares. With an id, one of six views —
|
|
5136
|
+
--funnel (default) stage-by-stage reach and drop-off · --overview entered vs
|
|
5137
|
+
converted plus the biggest drop-off · --goals completions, contacts, value and
|
|
5138
|
+
p50 time · --trends per-bucket activity · --cost tokens and dollars per goal ·
|
|
5139
|
+
--definition what was DECLARED, unmeasured (the one view that works with no
|
|
5140
|
+
traffic). Needs journeys:read.
|
|
5141
|
+
|
|
5142
|
+
--stage <stageId> lists the runs sitting at a stage right now (a live snapshot,
|
|
5143
|
+
not the funnel's cumulative reached counts).
|
|
5144
|
+
|
|
5145
|
+
Same flag grammar as \`octwin analytics\` on purpose: a journey funnel and an
|
|
5146
|
+
entity funnel are the same question about different subjects. Journeys carry RBAC
|
|
5147
|
+
on top of the scope, so an empty answer can be a missing \`view\` grant rather
|
|
5148
|
+
than missing data — the output says which causes are possible.`,
|
|
5149
|
+
performance: `octwin performance [--detail] [--json]
|
|
5150
|
+
The project's business indicators — value produced, conversion, duration — each
|
|
5151
|
+
with its delta against the previous window and a \`why\` naming the declaration it
|
|
5152
|
+
came from. --detail adds the per-indicator breakdown.
|
|
5153
|
+
|
|
5154
|
+
Needs records:read, NOT a performance scope (there is none), so a read-only token
|
|
5155
|
+
already reaches it. Indicators are DERIVED: a pack that declares no journey goal
|
|
5156
|
+
value and no pipelined entity produces none, which is a different thing from zero.`,
|
|
5157
|
+
usage: `octwin usage [--json]
|
|
5158
|
+
Model calls, tokens and cost for the resolved scope — project when one is pinned
|
|
5159
|
+
or passed with --project, otherwise the whole workspace. Broken down by model,
|
|
5160
|
+
kind, agent and channel.
|
|
5161
|
+
|
|
5162
|
+
Needs no particular scope: any valid token reaches it.
|
|
5163
|
+
|
|
5164
|
+
This is MODEL spend only. WhatsApp/Meta message billing is operator-only and
|
|
5165
|
+
deliberately outside the token scope registry — no API token can read it.`,
|
|
5166
|
+
'platform-kb': `octwin platform-kb [pull] [--if-stale] [--check] [--dir .] [--url <url>] [--token <t>]
|
|
5167
|
+
Pull the platform capability reference (markdown + JSON catalogs) into
|
|
5168
|
+
.octwin/platform-kb/ for the octwin-pack authoring skill, plus three maps:
|
|
5169
|
+
INDEX.md (the corpus) · SYMBOLS.md (every name -> its file; grep this) ·
|
|
5170
|
+
OUTLINE.md (every heading with its line number).
|
|
5171
|
+
|
|
5172
|
+
NO TOKEN NEEDED — the reference is platform stdlib and is served anonymously,
|
|
5173
|
+
and this command never sends one. --token is accepted and ignored, so an older
|
|
5174
|
+
script that passes it keeps working.
|
|
5175
|
+
|
|
5176
|
+
--if-stale poll the platform's content_hash first and skip the download when
|
|
5177
|
+
nothing changed. Cheap enough to run at the start of every session.
|
|
5178
|
+
--check report only, write nothing. Exit 0 = current, 2 = stale or never
|
|
5179
|
+
pulled, 1 = could not tell (offline / no reference served). For
|
|
5180
|
+
scripts and agent loops that want to branch without parsing prose.`,
|
|
5181
|
+
test: `octwin test [--dir .]
|
|
4287
5182
|
Alias for \`octwin validate --remote\` — the full platform check.`,
|
|
4288
|
-
memos: `octwin memos [--all] [--json]
|
|
4289
|
-
Read what the platform has told you: a REPLY to a report you sent with
|
|
4290
|
-
\`octwin feedback\`, or a NOTICE published to every author (a new capability,
|
|
4291
|
-
a deprecation, a breaking change). Bodies are printed in full.
|
|
4292
|
-
Reading marks them read, so the reminder stops. --all re-reads history and
|
|
4293
|
-
acks nothing. --json to branch on \`severity\`
|
|
5183
|
+
memos: `octwin memos [--all] [--json]
|
|
5184
|
+
Read what the platform has told you: a REPLY to a report you sent with
|
|
5185
|
+
\`octwin feedback\`, or a NOTICE published to every author (a new capability,
|
|
5186
|
+
a deprecation, a breaking change). Bodies are printed in full.
|
|
5187
|
+
Reading marks them read, so the reminder stops. --all re-reads history and
|
|
5188
|
+
acks nothing. --json to branch on \`severity\`
|
|
4294
5189
|
(info | action_required | breaking).`,
|
|
4295
|
-
feedback: `octwin feedback [--dir .]
|
|
4296
|
-
Submit this pack's FEEDBACK.md to the platform team.
|
|
4297
|
-
The octwin-pack skill writes that file in its last step — findings grouped by
|
|
4298
|
-
owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
|
|
4299
|
-
you to paste it into a chat.
|
|
4300
|
-
Attaches the pack id + version from manifest.yaml, this CLI's version, and the
|
|
4301
|
-
content_hash of the capability reference in .octwin/platform-kb/ — triage needs
|
|
4302
|
-
the last two to tell "the platform is wrong" from "that was already fixed" or
|
|
5190
|
+
feedback: `octwin feedback [--dir .]
|
|
5191
|
+
Submit this pack's FEEDBACK.md to the platform team.
|
|
5192
|
+
The octwin-pack skill writes that file in its last step — findings grouped by
|
|
5193
|
+
owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
|
|
5194
|
+
you to paste it into a chat.
|
|
5195
|
+
Attaches the pack id + version from manifest.yaml, this CLI's version, and the
|
|
5196
|
+
content_hash of the capability reference in .octwin/platform-kb/ — triage needs
|
|
5197
|
+
the last two to tell "the platform is wrong" from "that was already fixed" or
|
|
4303
5198
|
"you were reading a stale reference". Needs the \`pack:deploy\` scope.`,
|
|
4304
5199
|
};
|
|
4305
5200
|
async function main() {
|
|
@@ -4393,6 +5288,21 @@ async function main() {
|
|
|
4393
5288
|
case 'scheduling':
|
|
4394
5289
|
await cmdScheduling(flags);
|
|
4395
5290
|
break;
|
|
5291
|
+
case 'automation':
|
|
5292
|
+
await cmdAutomation(flags);
|
|
5293
|
+
break;
|
|
5294
|
+
case 'integrations':
|
|
5295
|
+
await cmdIntegrations(flags);
|
|
5296
|
+
break;
|
|
5297
|
+
case 'journeys':
|
|
5298
|
+
await cmdJourneys(flags);
|
|
5299
|
+
break;
|
|
5300
|
+
case 'performance':
|
|
5301
|
+
await cmdPerformance(flags);
|
|
5302
|
+
break;
|
|
5303
|
+
case 'usage':
|
|
5304
|
+
await cmdUsage(flags);
|
|
5305
|
+
break;
|
|
4396
5306
|
case 'platform-kb':
|
|
4397
5307
|
await cmdPlatformKb(flags);
|
|
4398
5308
|
break;
|