octwin-cli 0.8.17 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +3 -3
- package/dist/cli.js +274 -150
- package/dist/lib/rules.js +36 -0
- package/package.json +1 -1
- package/templates/starter/automation.yaml +10 -0
- package/templates/starter/xrm.yaml +1 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ repo root [`CHANGELOG.md`](../../CHANGELOG.md); this file is the CLI-only cut th
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.0] - 2026-10-21
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **`octwin automation` reads your RULES — what you wrote — not job rows.** BREAKING for scripts: the list
|
|
14
|
+
is each rule of the project in the platform's own words (the sentence the console's Outbound → Automatic
|
|
15
|
+
tab shows), on or paused, its week (sent / not delivered / failed / skipped) and its next time, under the
|
|
16
|
+
week's numbers and what needs a look; `--json` is `{ summary, rules }`. New: `coming` (every time still to
|
|
17
|
+
come), `happened` (every fire, its final word, why a message did not go) and `why <id>` (one line's steps
|
|
18
|
+
and the words sent — what a scripted test asserts a fire with). `pause` / `resume` / `run` take a rule key;
|
|
19
|
+
the key of the older job in your own `automation.yaml` finds the rule it runs as (`src/lib/rules.ts`). It
|
|
20
|
+
listed job rows until now — since the platform ran every pack's automation as rules, those were only its
|
|
21
|
+
own two sweeps, so nothing you declared showed in the terminal. `campaigns` and `send` are unchanged.
|
|
22
|
+
Upgrade before the platform retires the jobs routes: an older CLI's `octwin automation` will answer 404.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **`octwin records` lists a project's handoffs and cases even when the pack declares no types of its
|
|
26
|
+
own.** The catalog now carries the platform's entities with a `listed` flag per entity; the command
|
|
27
|
+
lists the listed ones (a missing flag, from an older server, reads as listed), so it neither prints
|
|
28
|
+
every unused platform type nor says "no record types" over a project full of handoffs.
|
|
29
|
+
|
|
10
30
|
## [0.8.17] - 2026-10-12
|
|
11
31
|
|
|
12
32
|
### Added
|
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ refused by name rather than leaving stdout empty.
|
|
|
89
89
|
| `octwin analytics` | Stage-by-stage conversion for **any** entity declared with a `pipeline:` (`--overview` / `--milestones` / `--trends` / `--cost`; `--stage <id>` lists the records currently at a stage). Needs `records:read`. Page `--stage` with `octwin analytics <entity> --stage <id> --limit n --offset n`. |
|
|
90
90
|
| `octwin catalog` | Commerce products with price / availability / stock, plus the WhatsApp catalog binding. `--readiness` runs the Meta Graph checklist. Needs `catalog:read` + the `catalog` plan feature. Page with `octwin catalog --limit n --offset n`. |
|
|
91
91
|
| `octwin scheduling` | The scheduling engine's state, or `--slots <resourceRecordId>` for the slots one bookable resource actually computes — how you verify the availability rules `deploy --seed` created. Needs `scheduling:read`. `octwin scheduling --slots <resourceRecordId> --from YYYY-MM-DD --days n` reads one resource's computed slots. |
|
|
92
|
-
| `octwin automation` |
|
|
92
|
+
| `octwin automation` | Your pack's **rules**, each in the platform's own words — on or paused, its week (sent / not delivered / failed / skipped) and its next time — under the week's numbers and what needs a look. `coming [--range today\|tomorrow\|week\|all] [--entity e] [--q text] [--cursor c]` lists every time still to come, `happened [--chip all\|not_delivered\|failed\|skipped\|moves\|tasks] [--period 7d\|30d] [--q text] [--cursor c]` every fire with its final word and why a message did not go, `why <id> [--kind timer\|fire\|queued]` one line's steps and the words sent. An older `segment` / `stale_records` / `sla_sweep` job or `on:` hook is listed as the rule it runs as. `campaigns` (or `--campaigns`) lists the campaigns, paged with `--limit n --offset n`. Needs `automation:read`. |
|
|
93
93
|
| `octwin integrations` | What the pack **declares** beside what is actually **configured**, and it flags the gap: a connection declared and never configured is the commonest reason an integration silently never fires, and neither list alone shows it. `preflight <key>` diagnoses without calling out (`integrations:read`); `deliveries [id]` is the outbound log; `events` is what arrived inbound. Needs `integrations:read`. `octwin integrations deliveries --status s --operation id --limit n --offset n` is the outbound delivery log. |
|
|
94
94
|
| `octwin journeys` | The customer journeys your pack declares, measured: `--funnel` (default) · `--overview` · `--goals` · `--trends` · `--cost` · `--definition` (what was declared, works with no traffic), plus `--stage <id>` for the runs sitting at a stage now. Same flag grammar as `octwin analytics` — a journey funnel and an entity funnel are the same question about different subjects. Needs `journeys:read`. Page `--stage` with `octwin journeys <id> --stage <stageId> --limit n --offset n`. |
|
|
95
95
|
| `octwin performance` | The project's business indicators — value produced, conversion, duration — each with its delta against the previous window and a `why` naming the declaration it came from. `--detail` adds the breakdown. Needs `records:read`, **not** a `performance:*` scope (there is none). `octwin performance --range <days>` sets the window and `octwin performance --channel <whatsapp|web>` narrows every figure to one channel — the console's two controls. |
|
|
@@ -119,7 +119,7 @@ lists the verbs and their exact flags.
|
|
|
119
119
|
| `octwin catalog` | `availability <sku> --to "in stock"` · `stock <sku> [--set-on-hand n]` |
|
|
120
120
|
| `octwin scheduling` | `rules --resource <id>` · `rule add --resource <id> --dow 1 --start 09:00 --end 17:00 [--slot-minutes 30] [--capacity 1]` · `rule rm <ruleId>` · `exception add --resource <id> --date YYYY-MM-DD --kind closed\|extra [--start --end]` · `exception rm <id>` |
|
|
121
121
|
| `octwin agents` | `set <ref> [--model m] [--enabled true\|false] [--overlay "…"\|none] [--enable-tool t] [--disable-tool t]` |
|
|
122
|
-
| `octwin automation` | `
|
|
122
|
+
| `octwin automation` | `pause\|resume <ruleKey>` · `run <ruleKey>` (a rule on a timetable, now) · `send <campaignId>` — a rule key is what the list prints; the key of the older job in your own `automation.yaml` finds the rule it runs as |
|
|
123
123
|
| `octwin integrations` | `test <key>` (a live call to the connection's `health:` operation) · `retry\|cancel\|send-now <deliveryId>` |
|
|
124
124
|
|
|
125
125
|
`--set k=v` coerces JSON scalars (`--set rating=4.5` sends a number); `--fields-json` takes anything
|
|
@@ -155,7 +155,7 @@ octwin orders # then: octwin orders <reference_i
|
|
|
155
155
|
octwin analytics # then: octwin analytics <entity> [--stage <id>]
|
|
156
156
|
octwin catalog # products + stock + the WhatsApp binding
|
|
157
157
|
octwin scheduling --slots <resourceRecordId> # the slots your availability rules compute
|
|
158
|
-
octwin automation #
|
|
158
|
+
octwin automation # your rules in words; then: coming · happened · why <id>
|
|
159
159
|
octwin integrations # declared vs configured — the silent-never-fires check
|
|
160
160
|
octwin journeys # then: octwin journeys <id> [--overview|--goals|--cost]
|
|
161
161
|
octwin performance # value, conversion and duration, each with its `why`
|
package/dist/cli.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
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]
|
|
31
|
+
* octwin automation [coming|happened|why <id>|campaigns] # the project's rules in words, what is coming up, what happened (automation:read)
|
|
32
32
|
* octwin integrations [deliveries|events|preflight|test …] # declared vs configured, and the delivery log (integrations:read)
|
|
33
33
|
* octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] # declared journeys, measured (journeys:read)
|
|
34
34
|
* octwin performance [--detail] # the project's business indicators (records:read — there is no performance scope)
|
|
@@ -85,6 +85,7 @@ import { classifyPackPath, isSkippedDir } from './lib/pack-source.js';
|
|
|
85
85
|
import { BASE_FILE, deployBaseOf, hashBundle, planPull, readBase, writeBase } from './lib/base.js';
|
|
86
86
|
import { formatChecksRefusal } from './lib/refusal.js';
|
|
87
87
|
import { readPage, morePageHint } from './lib/page.js';
|
|
88
|
+
import { phraseText, resolveRuleKey, shortTime } from './lib/rules.js';
|
|
88
89
|
import { chatLabel, answersTheTurn } from './lib/chat-line.js';
|
|
89
90
|
import { kbOneLiner, buildKbIndexMarkdown, buildKbOutlineMarkdown, } from './lib/kb-index.js';
|
|
90
91
|
import { buildSymbols, linkExplainers, renderSymbolsMarkdown } from './lib/kb-symbols.js';
|
|
@@ -240,9 +241,9 @@ const VERB_REQUIREMENTS = {
|
|
|
240
241
|
// which does NOT confer this — that 403 is otherwise baffling.
|
|
241
242
|
'projects create': { scope: 'projects:write' },
|
|
242
243
|
'projects rm': { scope: 'projects:write' },
|
|
243
|
-
//
|
|
244
|
-
// `campaigns`
|
|
245
|
-
// entry here would print "needs automation:write" on a read failure.
|
|
244
|
+
// Rules are declared, so there is no `create` — only acting on one (or a campaign).
|
|
245
|
+
// `coming` / `happened` / `why` / `campaigns` are absent on purpose: they are READS sharing
|
|
246
|
+
// the verb slot, and an entry here would print "needs automation:write" on a read failure.
|
|
246
247
|
'automation run': { scope: 'automation:write' },
|
|
247
248
|
'automation pause': { scope: 'automation:write' },
|
|
248
249
|
'automation resume': { scope: 'automation:write' },
|
|
@@ -2763,12 +2764,16 @@ async function cmdRecords(flags) {
|
|
|
2763
2764
|
out.print('No AI Agent is loaded in this project.');
|
|
2764
2765
|
return;
|
|
2765
2766
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2767
|
+
// The LISTED types, as the console's switcher shows them: the catalog carries every platform
|
|
2768
|
+
// entity for lookups by name. A server older than the `listed` field sent declared types only,
|
|
2769
|
+
// so a missing one reads as listed.
|
|
2770
|
+
const ents = (json?.entities ?? []).filter(e => e.listed !== false);
|
|
2771
|
+
if (json?.has_xrm === false && ents.length === 0) {
|
|
2772
|
+
// No record TYPES of its own and no platform records yet — the platform's (an order, a case)
|
|
2773
|
+
// still read by name.
|
|
2768
2774
|
out.print('This pack declares no record types of its own. Platform records still read by name: octwin records order | product | case | …');
|
|
2769
2775
|
return;
|
|
2770
2776
|
}
|
|
2771
|
-
const ents = (json?.entities ?? []);
|
|
2772
2777
|
if (ents.length === 0) {
|
|
2773
2778
|
out.print('No entities visible — mint a token with the `records:read` scope to inspect data.');
|
|
2774
2779
|
return;
|
|
@@ -4927,49 +4932,51 @@ async function cmdScheduling(flags) {
|
|
|
4927
4932
|
out.print(` upcoming slots: ${json?.upcoming_slots ?? 0} booked seats: ${json?.booked_seats ?? 0}`);
|
|
4928
4933
|
out.print('\nSlots for one resource: octwin scheduling --slots <resourceRecordId> (ids: octwin records <entity>)');
|
|
4929
4934
|
}
|
|
4930
|
-
// ── automation:
|
|
4935
|
+
// ── automation: the project's rules + campaigns ──────────────────────────────
|
|
4931
4936
|
/**
|
|
4932
|
-
* Verbs
|
|
4937
|
+
* Verbs of `octwin automation` — a view (`coming`, `happened`, `why`, `campaigns`) or an act (`run`, `pause`,
|
|
4938
|
+
* `resume`, `send`) — not a rule key.
|
|
4933
4939
|
*
|
|
4934
|
-
*
|
|
4935
|
-
*
|
|
4936
|
-
*
|
|
4937
|
-
*
|
|
4938
|
-
* say the right thing.
|
|
4940
|
+
* Until 0.9.0 the command read JOB ROWS (`…/automation/jobs`). Since the platform's migration 126 those are only its
|
|
4941
|
+
* own two sweeps: a pack's `rules:` — and its older `segment` / `stale_records` / `sla_sweep` jobs and `on:` hooks,
|
|
4942
|
+
* which run as compiled rules — were invisible in the terminal. It reads the rules routes the console's Outbound →
|
|
4943
|
+
* Automatic tab reads now, so the words are the server's.
|
|
4939
4944
|
*/
|
|
4940
|
-
const AUTOMATION_VERBS = new Set(['run', 'pause', 'resume', 'campaigns', 'send']);
|
|
4945
|
+
const AUTOMATION_VERBS = new Set(['coming', 'happened', 'why', 'run', 'pause', 'resume', 'campaigns', 'send']);
|
|
4946
|
+
const AUTOMATION_WRITES = new Set(['run', 'pause', 'resume', 'send']);
|
|
4941
4947
|
/**
|
|
4942
|
-
* Turn
|
|
4943
|
-
*
|
|
4944
|
-
* (`UUID_RE` is the one already declared for `--media`, deliberately reused rather
|
|
4945
|
-
* than a second copy of the same pattern.)
|
|
4946
|
-
*
|
|
4947
|
-
* The write routes take a UUID path param and reject anything else with a bare
|
|
4948
|
-
* *"Malformed identifier in the URL — expected a UUID"* 400. But the thing an author
|
|
4949
|
-
* has in front of them is the declaration KEY (`cart_recovery_nudge`) — that is what
|
|
4950
|
-
* the list prints, and it is the name in their own YAML. Measured: passing the key
|
|
4951
|
-
* 400s on all three write verbs.
|
|
4952
|
-
*
|
|
4953
|
-
* So the key is resolved here, against the list route, rather than documented as a
|
|
4954
|
-
* gotcha. A UUID passes straight through, and an unknown key fails naming the keys
|
|
4955
|
-
* that DO exist — which is the answer to the question the author is actually asking.
|
|
4948
|
+
* Turn the campaign an author typed into the UUID the send route demands — the id `octwin automation campaigns`
|
|
4949
|
+
* prints, or the campaign's key. An unknown one fails naming those that exist.
|
|
4956
4950
|
*/
|
|
4957
|
-
async function
|
|
4951
|
+
async function resolveCampaignId(typed, t) {
|
|
4958
4952
|
if (UUID_RE.test(typed))
|
|
4959
4953
|
return typed;
|
|
4960
|
-
const {
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4954
|
+
const { url } = t;
|
|
4955
|
+
const base = `${url}/api/self/p/automation`;
|
|
4956
|
+
const { status, json } = await apiGet(`${base}/campaigns`, t);
|
|
4957
|
+
if (status !== 200)
|
|
4958
|
+
die(`could not resolve '${typed}' — reading campaigns failed (HTTP ${status})${errDetail(json)}`);
|
|
4964
4959
|
const rows = readPage(json).rows;
|
|
4965
4960
|
const hit = rows.find(r => r.key === typed || r.id === typed);
|
|
4966
4961
|
if (hit?.id)
|
|
4967
4962
|
return hit.id;
|
|
4968
4963
|
const keys = rows.map(r => r.key ?? r.id).filter(Boolean);
|
|
4969
|
-
die(`no ${
|
|
4970
|
-
+ (keys.length ? ` Available: ${keys.join(', ')}` : ` This project declares none.`));
|
|
4964
|
+
die(`no campaign '${typed}' in this project.` + (keys.length ? ` Available: ${keys.join(', ')}` : ' This project has none.'));
|
|
4971
4965
|
}
|
|
4972
|
-
/** `
|
|
4966
|
+
/** The rule an author typed, against the project's rules (`resolveRuleKey`) — or the command dies naming them. */
|
|
4967
|
+
async function resolveRule(typed, t) {
|
|
4968
|
+
const { url } = t;
|
|
4969
|
+
const base = `${url}/api/self/p/automation`;
|
|
4970
|
+
const { status, json } = await apiGet(`${base}/rules`, t);
|
|
4971
|
+
if (status !== 200)
|
|
4972
|
+
die(`could not resolve '${typed}' — reading the rules failed (HTTP ${status})${errDetail(json)}`);
|
|
4973
|
+
const keys = (json?.rules ?? []).map(r => r.key).filter((k) => typeof k === 'string');
|
|
4974
|
+
const r = resolveRuleKey(typed, keys);
|
|
4975
|
+
if ('error' in r)
|
|
4976
|
+
die(r.error);
|
|
4977
|
+
return r.key;
|
|
4978
|
+
}
|
|
4979
|
+
/** `pause` / `resume` / `run` a rule, `send` a campaign — the writes behind `octwin automation`. */
|
|
4973
4980
|
async function cmdAutomationWrite(flags) {
|
|
4974
4981
|
const t = resolveTarget(flags);
|
|
4975
4982
|
const { url } = t;
|
|
@@ -4977,75 +4984,64 @@ async function cmdAutomationWrite(flags) {
|
|
|
4977
4984
|
const verb = flags._[0];
|
|
4978
4985
|
const typed = flags._[1];
|
|
4979
4986
|
const asJson = flags.json === true;
|
|
4980
|
-
if (
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
const { status, json } = await apiSend('PATCH', `${base}/jobs/${encodeURIComponent(id)}/status`, { status: verb === 'pause' ? 'paused' : 'active' }, t);
|
|
4985
|
-
if (status === 404)
|
|
4986
|
-
die(`job '${typed}' not found in ${targetLabel(t)} (ids: octwin automation)`);
|
|
4987
|
-
// RBAC re-checks the ACTION on the job, so a 403 here can be a grant gap
|
|
4988
|
-
// rather than a missing scope — same caveat as a record write.
|
|
4989
|
-
if (status !== 200)
|
|
4990
|
-
writeFail(`${verb} job '${typed}'`, status, json, url, true);
|
|
4991
|
-
if (asJson) {
|
|
4992
|
-
out.result(json);
|
|
4993
|
-
return;
|
|
4994
|
-
}
|
|
4995
|
-
const j = json?.job ?? {};
|
|
4996
|
-
out.print(`✓ job '${j.key ?? typed}' is now ${j.status}`);
|
|
4997
|
-
if (j.next_run_at)
|
|
4998
|
-
out.print(` next run: ${j.next_run_at}`);
|
|
4999
|
-
return;
|
|
5000
|
-
}
|
|
5001
|
-
if (verb === 'run') {
|
|
4987
|
+
if (verb === 'send') {
|
|
4988
|
+
if (!typed)
|
|
4989
|
+
die('usage: octwin automation send <campaignId> (ids: octwin automation campaigns)');
|
|
4990
|
+
const id = await resolveCampaignId(typed, t);
|
|
5002
4991
|
if (!asJson)
|
|
5003
|
-
out.print(`→
|
|
5004
|
-
const { status, json } = await apiSend('POST', `${base}/
|
|
4992
|
+
out.print(`→ Sending campaign '${typed}' in ${targetLabel(t)} …`);
|
|
4993
|
+
const { status, json } = await apiSend('POST', `${base}/campaigns/${encodeURIComponent(id)}/send`, undefined, t);
|
|
5005
4994
|
if (status === 404)
|
|
5006
|
-
die(`
|
|
4995
|
+
die(`campaign '${typed}' not found in ${targetLabel(t)} (ids: octwin automation campaigns)`);
|
|
5007
4996
|
if (status !== 200)
|
|
5008
|
-
writeFail(`
|
|
4997
|
+
writeFail(`send campaign '${typed}'`, status, json, url, true);
|
|
5009
4998
|
if (asJson) {
|
|
5010
4999
|
out.result(json);
|
|
5011
5000
|
return;
|
|
5012
5001
|
}
|
|
5013
5002
|
const r = json?.result ?? {};
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
//
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5003
|
+
const already = Number(r.already_queued ?? 0);
|
|
5004
|
+
out.print(`✓ campaign '${typed}': matched ${r.matched ?? 0}, enqueued ${r.enqueued ?? 0}${already ? `, ${already} already had it` : ''} [${r.stage ?? '?'}]`);
|
|
5005
|
+
// A campaign that stopped half-way is FINISHED by the same command: it queues only the contacts
|
|
5006
|
+
// with no delivery yet, so `already` is the earlier send's work, not a skip.
|
|
5007
|
+
if (already)
|
|
5008
|
+
out.print(' finished an earlier send — each contact gets this campaign once');
|
|
5009
|
+
if ((r.enqueued ?? 0) + already < (r.matched ?? 0))
|
|
5010
|
+
out.print(' partial: some matched records were not enqueued (records sharing a contact, or a record with none)');
|
|
5011
|
+
out.print(' enqueued ≠ delivered — watch the sends land with `octwin automation happened`');
|
|
5021
5012
|
return;
|
|
5022
5013
|
}
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
const
|
|
5014
|
+
if (!typed)
|
|
5015
|
+
die(`usage: octwin automation ${verb} <ruleKey> (keys: octwin automation)`);
|
|
5016
|
+
const key = await resolveRule(typed, t);
|
|
5017
|
+
const path = `${base}/rules/${encodeURIComponent(key)}/${verb}`;
|
|
5018
|
+
if (!asJson && verb === 'run')
|
|
5019
|
+
out.print(`→ Running rule '${key}' in ${targetLabel(t)} …`);
|
|
5020
|
+
const { status, json } = await apiSend('POST', path, undefined, t);
|
|
5027
5021
|
if (status === 404)
|
|
5028
|
-
die(`
|
|
5022
|
+
die(`rule '${key}' not found in ${targetLabel(t)} (keys: octwin automation)`);
|
|
5023
|
+
// RBAC re-checks the ACTION on `automation.rule`, so a 403 can be a grant gap rather than a missing scope; a 409
|
|
5024
|
+
// is the rule saying why not (only a rule on a timetable can be run now) — the server's words.
|
|
5029
5025
|
if (status !== 200)
|
|
5030
|
-
writeFail(
|
|
5026
|
+
writeFail(`${verb} rule '${key}'`, status, json, url, true);
|
|
5031
5027
|
if (asJson) {
|
|
5032
5028
|
out.result(json);
|
|
5033
5029
|
return;
|
|
5034
5030
|
}
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
out.print(
|
|
5044
|
-
|
|
5031
|
+
if (verb === 'run') {
|
|
5032
|
+
out.print(`✓ '${key}' runs now — it reaches ${json?.reaches ?? 0} record(s); next time after this run: ${shortTime(json?.next)}`);
|
|
5033
|
+
out.print(' see what it did: octwin automation happened');
|
|
5034
|
+
}
|
|
5035
|
+
else if (verb === 'pause') {
|
|
5036
|
+
out.print(`✓ '${key}' is paused — its times still arm, and nothing runs (nothing is caught up when it resumes)`);
|
|
5037
|
+
}
|
|
5038
|
+
else {
|
|
5039
|
+
out.print(json?.resumed === false ? ` '${key}' was not paused` : `✓ '${key}' runs again`);
|
|
5040
|
+
}
|
|
5045
5041
|
}
|
|
5046
5042
|
/**
|
|
5047
|
-
* A campaign's delivery progress — the per-status counts of
|
|
5048
|
-
*
|
|
5043
|
+
* A campaign's delivery progress — the per-status counts of its deliveries. Not sent yet (a draft) is all zeros, and
|
|
5044
|
+
* reads as a dash rather than "sent 0".
|
|
5049
5045
|
*/
|
|
5050
5046
|
function campaignProgressLine(p) {
|
|
5051
5047
|
const n = (k) => Number(p?.[k] ?? 0);
|
|
@@ -5053,27 +5049,147 @@ function campaignProgressLine(p) {
|
|
|
5053
5049
|
return '—'.padEnd(28);
|
|
5054
5050
|
return `sent ${n('sent')} · failed ${n('failed')} · pending ${n('pending')}${n('cancelled') ? ` · cancelled ${n('cancelled')}` : ''}`.padEnd(28);
|
|
5055
5051
|
}
|
|
5052
|
+
/** A line's record and customer, as one tail — `· Booking #7 · Sara`. */
|
|
5053
|
+
function lineTail(l) {
|
|
5054
|
+
const rec = l?.record ? ` · ${l.record.noun ?? l.record.entity ?? 'record'} #${l.record.number ?? '?'}` : '';
|
|
5055
|
+
const who = l?.contact ? ` · ${l.contact.name ?? 'customer'}` : '';
|
|
5056
|
+
return `${rec}${who}`;
|
|
5057
|
+
}
|
|
5058
|
+
/** A query string of the values that are set — `range=week&q=cairo`. Each caller names the flags it passes. */
|
|
5059
|
+
function setQs(values) {
|
|
5060
|
+
const p = new URLSearchParams();
|
|
5061
|
+
for (const [k, v] of Object.entries(values))
|
|
5062
|
+
if (typeof v === 'string')
|
|
5063
|
+
p.set(k, v);
|
|
5064
|
+
return p.toString();
|
|
5065
|
+
}
|
|
5066
|
+
/** `octwin automation coming` — every time still to come: a rule's, or a flow's own message (a send). */
|
|
5067
|
+
async function automationComing(flags, t) {
|
|
5068
|
+
const { url } = t;
|
|
5069
|
+
const base = `${url}/api/self/p/automation`;
|
|
5070
|
+
const asJson = flags.json === true;
|
|
5071
|
+
if (!asJson)
|
|
5072
|
+
out.print(`→ Reading what is coming up in ${targetLabel(t)} …`);
|
|
5073
|
+
const qs = setQs({ range: flags.range, entity: flags.entity, q: flags.q, cursor: flags.cursor });
|
|
5074
|
+
const { status, json } = await apiGet(`${base}/upcoming?${qs || 'range=week'}`, t);
|
|
5075
|
+
if (status !== 200)
|
|
5076
|
+
die(`could not read what is coming up (HTTP ${status})${errDetail(json)}${authFailureDetail(status, t.url)}`);
|
|
5077
|
+
if (asJson) {
|
|
5078
|
+
out.result(json);
|
|
5079
|
+
return;
|
|
5080
|
+
}
|
|
5081
|
+
const lines = (json?.lines ?? []);
|
|
5082
|
+
out.print(`Coming up in ${targetLabel(t)} (${typeof flags.range === 'string' ? flags.range : 'week'}): ${json?.total ?? lines.length}`);
|
|
5083
|
+
if (lines.length === 0)
|
|
5084
|
+
out.print(' (nothing — a rule\'s time arms when its record is made or changed)');
|
|
5085
|
+
for (const l of lines) {
|
|
5086
|
+
const mark = l.skipped ? ' [skipped]' : l.paused ? ' [paused]' : '';
|
|
5087
|
+
out.print(` ${shortTime(l.at)} ${phraseText(l.headline)}${lineTail(l)}${mark} ${l.kind === 'queued' ? 'message' : 'timer'} ${l.id}`);
|
|
5088
|
+
}
|
|
5089
|
+
if (json?.next)
|
|
5090
|
+
out.print(`\nMore: octwin automation coming --cursor ${json.next}`);
|
|
5091
|
+
if (lines.length)
|
|
5092
|
+
out.print('Why one goes, and what it will say: octwin automation why <id>');
|
|
5093
|
+
}
|
|
5094
|
+
/** `octwin automation happened` — every fire of the period: its final word, and why a message did not go. */
|
|
5095
|
+
async function automationHappened(flags, t) {
|
|
5096
|
+
const { url } = t;
|
|
5097
|
+
const base = `${url}/api/self/p/automation`;
|
|
5098
|
+
const asJson = flags.json === true;
|
|
5099
|
+
if (!asJson)
|
|
5100
|
+
out.print(`→ Reading what happened in ${targetLabel(t)} …`);
|
|
5101
|
+
const qs = setQs({ chip: flags.chip, period: flags.period, q: flags.q, cursor: flags.cursor });
|
|
5102
|
+
const { status, json } = await apiGet(`${base}/history?${qs || 'period=7d'}`, t);
|
|
5103
|
+
if (status !== 200)
|
|
5104
|
+
die(`could not read what happened (HTTP ${status})${errDetail(json)}${authFailureDetail(status, t.url)}`);
|
|
5105
|
+
if (asJson) {
|
|
5106
|
+
out.result(json);
|
|
5107
|
+
return;
|
|
5108
|
+
}
|
|
5109
|
+
const lines = (json?.lines ?? []);
|
|
5110
|
+
const c = json?.counts ?? {};
|
|
5111
|
+
out.print(`Happened in ${targetLabel(t)} (${typeof flags.period === 'string' ? flags.period : '7d'}): ${json?.total ?? lines.length}`
|
|
5112
|
+
+ ` — ${c.not_delivered ?? 0} not delivered, ${c.failed ?? 0} failed, ${c.skipped ?? 0} skipped`);
|
|
5113
|
+
if (lines.length === 0)
|
|
5114
|
+
out.print(' (nothing yet)');
|
|
5115
|
+
for (const l of lines) {
|
|
5116
|
+
const why = l.code || l.reason ? ` (${l.code ?? l.reason})` : '';
|
|
5117
|
+
out.print(` ${shortTime(l.at)} ${String(l.outcome ?? '—').padEnd(13)} ${phraseText(l.headline)}${lineTail(l)}${why} ${l.id}`);
|
|
5118
|
+
}
|
|
5119
|
+
if (json?.next)
|
|
5120
|
+
out.print(`\nMore: octwin automation happened --cursor ${json.next}`);
|
|
5121
|
+
if (lines.length)
|
|
5122
|
+
out.print('Each step and the words sent: octwin automation why <id>');
|
|
5123
|
+
}
|
|
5056
5124
|
/**
|
|
5057
|
-
* `octwin automation
|
|
5058
|
-
*
|
|
5125
|
+
* `octwin automation why <id>` — one line's *Why?*, as the console's drawer: the rule in words, each step with its
|
|
5126
|
+
* outcome, the words the customer reads. The line's kind picks the route (`--kind timer|fire|queued`); without it
|
|
5127
|
+
* each is asked in turn — a coming time, a fire, a send.
|
|
5128
|
+
*/
|
|
5129
|
+
async function automationWhy(flags, t) {
|
|
5130
|
+
const { url } = t;
|
|
5131
|
+
const base = `${url}/api/self/p/automation`;
|
|
5132
|
+
const asJson = flags.json === true;
|
|
5133
|
+
const id = flags._[1];
|
|
5134
|
+
if (!id)
|
|
5135
|
+
die('usage: octwin automation why <id> [--kind timer|fire|queued] (ids: octwin automation coming | happened)');
|
|
5136
|
+
const kinds = typeof flags.kind === 'string' ? [flags.kind] : ['timer', 'fire', 'queued'];
|
|
5137
|
+
for (const kind of kinds) {
|
|
5138
|
+
if (!['timer', 'fire', 'queued'].includes(kind))
|
|
5139
|
+
die(`--kind is timer, fire or queued (got '${kind}')`);
|
|
5140
|
+
const { status, json } = await apiGet(`${base}/why/${kind}/${encodeURIComponent(id)}`, t);
|
|
5141
|
+
if (status === 404)
|
|
5142
|
+
continue;
|
|
5143
|
+
if (status !== 200)
|
|
5144
|
+
die(`could not read why (HTTP ${status})${errDetail(json)}${authFailureDetail(status, t.url)}`);
|
|
5145
|
+
if (asJson) {
|
|
5146
|
+
out.result(json);
|
|
5147
|
+
return;
|
|
5148
|
+
}
|
|
5149
|
+
const v = json ?? {};
|
|
5150
|
+
out.print(`${phraseText(v.line?.headline)}${lineTail(v.line)} — ${shortTime(v.line?.at)}`);
|
|
5151
|
+
if (v.rule)
|
|
5152
|
+
out.print(` rule ${v.rule.key}: ${phraseText(v.rule.sentence)}`);
|
|
5153
|
+
if (v.line?.outcome)
|
|
5154
|
+
out.print(` outcome: ${v.line.outcome}${v.line.code ? ` (${v.line.code})` : ''}${v.line.reason ? ` — ${v.line.reason}` : ''}`);
|
|
5155
|
+
for (const check of (v.checks ?? []))
|
|
5156
|
+
out.print(` checked again before it acts: ${phraseText(check)}`);
|
|
5157
|
+
if (v.words?.text)
|
|
5158
|
+
out.print(` the customer reads${v.words.tag ? ` (${v.words.tag})` : ''}: ${v.words.text}`);
|
|
5159
|
+
for (const s of (v.steps ?? [])) {
|
|
5160
|
+
const send = s.send ? ` · ${s.send.status}${s.send.reason ? ` (${s.send.reason})` : ''}` : '';
|
|
5161
|
+
out.print(` step ${s.verb}: ${s.skipped ? `held back (${s.skipped})` : s.state}${send}${s.detail ? ` — ${s.detail}` : ''}`);
|
|
5162
|
+
}
|
|
5163
|
+
if (v.queued)
|
|
5164
|
+
out.print(` a flow's own message: ${v.queued.status}${v.queued.error ? ` — ${v.queued.error}` : ''}`);
|
|
5165
|
+
return;
|
|
5166
|
+
}
|
|
5167
|
+
die(`nothing under '${id}' in ${targetLabel(t)} — a coming time may have just happened, and a fire is kept 30 days`);
|
|
5168
|
+
}
|
|
5169
|
+
/**
|
|
5170
|
+
* `octwin automation [--json]` — the project's rules, each in the server's words, under the week's numbers.
|
|
5059
5171
|
*
|
|
5060
|
-
* Needs `automation:read`. The
|
|
5061
|
-
*
|
|
5062
|
-
* filtering the page — past the cap a client-side count would depend on the cap
|
|
5063
|
-
* instead of the data.
|
|
5172
|
+
* Needs `automation:read`. The words are the console's (`rules/describe.ts` writes each rule as a sentence), so the
|
|
5173
|
+
* terminal says what the Automatic tab says.
|
|
5064
5174
|
*/
|
|
5065
5175
|
async function cmdAutomation(flags) {
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5176
|
+
const verb = typeof flags._[0] === 'string' ? flags._[0] : null;
|
|
5177
|
+
if (verb && AUTOMATION_WRITES.has(verb))
|
|
5178
|
+
return cmdAutomationWrite(flags);
|
|
5179
|
+
if (verb && !AUTOMATION_VERBS.has(verb)) {
|
|
5180
|
+
die(`unknown automation verb '${verb}' — octwin automation [coming | happened | why <id> | campaigns | run|pause|resume <ruleKey> | send <campaignId>]`);
|
|
5070
5181
|
}
|
|
5071
5182
|
const t = resolveTarget(flags);
|
|
5072
5183
|
const { url } = t;
|
|
5073
5184
|
const base = `${url}/api/self/p/automation`;
|
|
5074
5185
|
const asJson = flags.json === true;
|
|
5075
|
-
|
|
5076
|
-
|
|
5186
|
+
if (verb === 'coming')
|
|
5187
|
+
return automationComing(flags, t);
|
|
5188
|
+
if (verb === 'happened')
|
|
5189
|
+
return automationHappened(flags, t);
|
|
5190
|
+
if (verb === 'why')
|
|
5191
|
+
return automationWhy(flags, t);
|
|
5192
|
+
if (verb === 'campaigns' || flags.campaigns === true) {
|
|
5077
5193
|
if (!asJson)
|
|
5078
5194
|
out.print(`→ Reading campaigns from ${targetLabel(t)} …`);
|
|
5079
5195
|
const { status, json } = await apiGet(`${base}/campaigns?${pagingQs(flags)}`, t);
|
|
@@ -5100,40 +5216,37 @@ async function cmdAutomation(flags) {
|
|
|
5100
5216
|
return;
|
|
5101
5217
|
}
|
|
5102
5218
|
if (!asJson)
|
|
5103
|
-
out.print(`→ Reading
|
|
5104
|
-
const [
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
if (jobs.status !== 200)
|
|
5109
|
-
die(`could not read automation jobs (HTTP ${jobs.status})${errDetail(jobs.json)}${authFailureDetail(jobs.status, url)}`);
|
|
5219
|
+
out.print(`→ Reading the rules of ${targetLabel(t)} …`);
|
|
5220
|
+
const [rules, summary] = await Promise.all([apiGet(`${base}/rules`, t), apiGet(`${base}/summary`, t)]);
|
|
5221
|
+
if (rules.status !== 200)
|
|
5222
|
+
die(`could not read the rules (HTTP ${rules.status})${errDetail(rules.json)}${authFailureDetail(rules.status, url)}`);
|
|
5223
|
+
const s = summary.status === 200 ? summary.json : null;
|
|
5110
5224
|
if (asJson) {
|
|
5111
|
-
out.result({
|
|
5225
|
+
out.result({ summary: s, rules: rules.json?.rules ?? [] });
|
|
5112
5226
|
return;
|
|
5113
5227
|
}
|
|
5114
|
-
const
|
|
5115
|
-
const
|
|
5116
|
-
out.print(`Automation in ${targetLabel(t)}: ${
|
|
5117
|
-
+ ` — ${
|
|
5118
|
-
|
|
5119
|
-
out.print(
|
|
5120
|
-
out.print(' No `automation.yaml` block → no jobs. `octwin deploy` installs them.)');
|
|
5121
|
-
return;
|
|
5228
|
+
const list = (rules.json?.rules ?? []);
|
|
5229
|
+
const w = s?.week ?? {};
|
|
5230
|
+
out.print(`Automation in ${targetLabel(t)}: ${list.length} rule(s)`
|
|
5231
|
+
+ (s ? ` — this week ${w.sent ?? 0} sent, ${w.notDelivered ?? 0} not delivered, ${w.failed ?? 0} failed, ${s.counts?.upcoming ?? 0} coming up` : ''));
|
|
5232
|
+
for (const n of (s?.needsLook ?? [])) {
|
|
5233
|
+
out.print(` ⚠ ${n.rule?.key ?? '?'}: ${n.kind === 'paused' ? 'paused' : n.kind === 'not_delivered' ? `${n.n} not delivered${n.codes?.length ? ` (${n.codes.join(', ')})` : ''}` : `${n.n} failed`}`);
|
|
5122
5234
|
}
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
const ran = (j.last_run_at ? `last run ${j.last_run_at}` : 'not run yet')
|
|
5127
|
-
+ (j.last_checked_at ? `, checked ${j.last_checked_at}` : ', never checked');
|
|
5128
|
-
const result = j.last_result
|
|
5129
|
-
? ` matched ${r.matched ?? 0}/acted ${r.acted ?? 0}${(r.errors ?? 0) > 0 ? `/ERRORS ${r.errors}` : ''}`
|
|
5130
|
-
: '';
|
|
5131
|
-
out.print(` ${String(j.key ?? j.id).padEnd(26)} ${String(j.status).padEnd(7)} ${j.kind}/${j.entity ?? '—'}`
|
|
5132
|
-
+ ` every ${j.interval_seconds}s ${ran}${result}`);
|
|
5235
|
+
if (list.length === 0) {
|
|
5236
|
+
out.print(' (none — a pack writes rules in automation.yaml `rules:`; `octwin deploy` installs them)');
|
|
5237
|
+
return;
|
|
5133
5238
|
}
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5239
|
+
for (const r of list) {
|
|
5240
|
+
const wk = r.week ?? {};
|
|
5241
|
+
out.print(` ${String(r.key)} [${r.paused ? 'paused' : 'on'}] next ${shortTime(r.next)}`
|
|
5242
|
+
+ ` · week ${wk.total ?? 0}: ${wk.sent ?? 0} sent, ${wk.notDelivered ?? 0} not delivered, ${wk.failed ?? 0} failed, ${wk.skipped ?? 0} skipped`);
|
|
5243
|
+
out.print(` ${phraseText(r.sentence)}`);
|
|
5244
|
+
}
|
|
5245
|
+
out.print('\nComing up: octwin automation coming [--range today|tomorrow|week|all]');
|
|
5246
|
+
out.print('Happened: octwin automation happened [--chip not_delivered|failed|skipped|moves|tasks] [--period 7d|30d]');
|
|
5247
|
+
out.print('Why one line: octwin automation why <id>');
|
|
5248
|
+
out.print('Act: octwin automation pause|resume|run <ruleKey> (needs automation:write)');
|
|
5249
|
+
out.print('Campaigns: octwin automation campaigns');
|
|
5137
5250
|
}
|
|
5138
5251
|
// ── integrations: declared connections, their credentials, and the delivery log ──
|
|
5139
5252
|
/** Verbs that act on a connection or a delivery, rather than naming one. */
|
|
@@ -5952,7 +6065,7 @@ function help() {
|
|
|
5952
6065
|
octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
|
|
5953
6066
|
octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
|
|
5954
6067
|
octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
|
|
5955
|
-
octwin automation [campaigns] [--json]
|
|
6068
|
+
octwin automation [coming|happened|why <id>|campaigns] [--json] # the rules in words; coming up; happened, and why
|
|
5956
6069
|
octwin integrations [--json] # declared connections BESIDE what is configured (the silent-never-fires check)
|
|
5957
6070
|
octwin integrations deliveries [<id>] | events # the outbound delivery log / inbound events
|
|
5958
6071
|
octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]
|
|
@@ -5975,7 +6088,7 @@ Writes — exercise the state your pack creates (each needs the matching :write
|
|
|
5975
6088
|
octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
|
|
5976
6089
|
octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
|
|
5977
6090
|
octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
|
|
5978
|
-
octwin automation
|
|
6091
|
+
octwin automation pause|resume|run <ruleKey> | send <campaignId>
|
|
5979
6092
|
octwin integrations test <key> # a LIVE call to the connection's health: operation
|
|
5980
6093
|
octwin integrations retry|cancel|send-now <deliveryId>
|
|
5981
6094
|
(octwin integrations preflight <key> needs only integrations:read — it makes no call)
|
|
@@ -6260,24 +6373,35 @@ octwin projects rm <slug> [--yes]
|
|
|
6260
6373
|
|
|
6261
6374
|
--dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
|
|
6262
6375
|
\`--slots\` is how you check what a rule actually produces.`,
|
|
6263
|
-
automation: `octwin automation [
|
|
6264
|
-
No args = every
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
octwin automation
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6376
|
+
automation: `octwin automation [coming | happened | why <id> | campaigns | --campaigns] [--json]
|
|
6377
|
+
No args = every RULE of the project — each in the platform's own words (the
|
|
6378
|
+
sentence the console's Outbound → Automatic tab shows), on or paused, its week
|
|
6379
|
+
(sent / not delivered / failed / skipped) and its next time — under the week's
|
|
6380
|
+
numbers and what needs a look. Needs automation:read.
|
|
6381
|
+
|
|
6382
|
+
A pack writes rules in automation.yaml \`rules:\`; its older \`segment\` /
|
|
6383
|
+
\`stale_records\` / \`sla_sweep\` jobs and xrm \`on:\` hooks run as rules too,
|
|
6384
|
+
listed under the key they compile to (\`segment:<job key>\`).
|
|
6385
|
+
|
|
6386
|
+
READ:
|
|
6387
|
+
octwin automation coming [--range today|tomorrow|week|all] [--entity e] [--q text] [--cursor c]
|
|
6388
|
+
# every time still to come — a rule's, or a flow's own message
|
|
6389
|
+
octwin automation happened [--chip all|not_delivered|failed|skipped|moves|tasks] [--period 7d|30d] [--cursor c]
|
|
6390
|
+
# every fire: its final word, and why a message did not go
|
|
6391
|
+
octwin automation why <id> [--kind timer|fire|queued]
|
|
6392
|
+
# one line: the rule in words, each step, the words sent
|
|
6393
|
+
octwin automation campaigns [--limit n] [--offset n]
|
|
6394
|
+
|
|
6395
|
+
WRITES (automation:write):
|
|
6396
|
+
octwin automation pause|resume <ruleKey> # paused: its times arm, nothing runs, nothing caught up
|
|
6397
|
+
octwin automation run <ruleKey> # a rule on a timetable, now
|
|
6398
|
+
octwin automation send <campaignId> # enqueue a campaign; enqueued != delivered
|
|
6399
|
+
|
|
6400
|
+
<ruleKey> is the key the list prints; the key of the older job in your own
|
|
6401
|
+
automation.yaml finds the rule it runs as. A 403 on a write can be an RBAC grant
|
|
6402
|
+
gap rather than a missing scope: the action is re-checked on automation.rule.
|
|
6403
|
+
(Until 0.9.0 this command listed job rows — since the platform ran every pack's
|
|
6404
|
+
automation as rules, those were only its own two sweeps.)`,
|
|
6281
6405
|
integrations: `octwin integrations [--json]
|
|
6282
6406
|
What the pack DECLARES beside what is actually CONFIGURED, in one view — because a
|
|
6283
6407
|
connection that is declared and never configured is the commonest reason an
|
|
@@ -6410,7 +6534,7 @@ export const COMMANDS = {
|
|
|
6410
6534
|
analytics: { run: cmdAnalytics, json: 'document', flags: ['funnel', 'overview', 'milestones', 'trends', 'cost', 'stage', 'limit', 'offset'] },
|
|
6411
6535
|
catalog: { run: cmdCatalog, json: 'document', flags: ['limit', 'offset', 'readiness', 'set-on-hand', 'to'] },
|
|
6412
6536
|
scheduling: { run: cmdScheduling, json: 'document', flags: ['capacity', 'date', 'days', 'dow', 'end', 'from', 'kind', 'resource', 'slot-minutes', 'slots', 'start'] },
|
|
6413
|
-
automation: { run: cmdAutomation, json: 'document', flags: ['campaigns', 'limit', 'offset'] },
|
|
6537
|
+
automation: { run: cmdAutomation, json: 'document', flags: ['campaigns', 'chip', 'cursor', 'entity', 'kind', 'limit', 'offset', 'period', 'q', 'range'] },
|
|
6414
6538
|
integrations: { run: cmdIntegrations, json: 'document', flags: ['limit', 'offset', 'operation', 'status'] },
|
|
6415
6539
|
journeys: { run: cmdJourneys, json: 'document', flags: ['funnel', 'overview', 'goals', 'trends', 'cost', 'definition', 'stage', 'limit', 'offset'] },
|
|
6416
6540
|
performance: { run: cmdPerformance, json: 'document', flags: ['detail', 'range', 'channel'] },
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `octwin automation` needs to speak RULES — the project's rules as the platform's Automatic tab reads them
|
|
3
|
+
* (`/api/self/p/automation/…`). Pure, so the two decisions it makes are tested on their own:
|
|
4
|
+
*
|
|
5
|
+
* - **which rule an author means** (`resolveRuleKey`) — the key `octwin automation` prints, or the key of the older
|
|
6
|
+
* `automation.yaml` job it was compiled from: a `segment` job `cart_nudge` runs as the rule `segment:cart_nudge`,
|
|
7
|
+
* and the job key is what the author has in their own YAML;
|
|
8
|
+
* - **the server's words** (`phraseText`) — a rule's sentence and a line's headline arrive as `Phrase`s (text, a
|
|
9
|
+
* strong part, a quote with named slots), the same the console draws; the terminal prints them flat.
|
|
10
|
+
*/
|
|
11
|
+
/** A sentence of `Phrase`s as one line of text — a quote in quotes, a slot as `{name}`. */
|
|
12
|
+
export function phraseText(phrases) {
|
|
13
|
+
return (phrases ?? []).map(p => p.text ?? p.strong
|
|
14
|
+
?? (p.quote ? `"${p.quote.map(q => q.text ?? `{${q.slot ?? ''}}`).join('')}"` : '')).join('');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The rule an author typed: its key exactly, else the one rule whose key ends in `:<typed>` (an older job's key, or a
|
|
18
|
+
* hook's moment). Two such rules, or none, is an answer that names what does exist.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveRuleKey(typed, keys) {
|
|
21
|
+
if (keys.includes(typed))
|
|
22
|
+
return { key: typed };
|
|
23
|
+
const hits = keys.filter(k => k.endsWith(`:${typed}`));
|
|
24
|
+
if (hits.length === 1)
|
|
25
|
+
return { key: hits[0] };
|
|
26
|
+
if (hits.length > 1)
|
|
27
|
+
return { error: `'${typed}' could be ${hits.length} rules — ${hits.join(', ')}. Use the full key.` };
|
|
28
|
+
return { error: `no rule '${typed}' in this project.` + (keys.length ? ` Rules: ${keys.join(', ')}` : ' It runs none.') };
|
|
29
|
+
}
|
|
30
|
+
/** A time as the terminal shows it — `2026-10-22 09:00Z`; a missing one is a dash. */
|
|
31
|
+
export function shortTime(iso) {
|
|
32
|
+
if (!iso)
|
|
33
|
+
return '—';
|
|
34
|
+
const d = new Date(iso);
|
|
35
|
+
return Number.isNaN(d.getTime()) ? iso : `${d.toISOString().slice(0, 16).replace('T', ' ')}Z`;
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# What the pack does by itself — rules: WHEN something changes (`on:`) or a time arrives (`at:`), FOR a
|
|
2
|
+
# record, DO actions. Guide: `craft-automation`; every change, time and action: `rules-guide`.
|
|
3
|
+
rules:
|
|
4
|
+
# The journey's goal, reached by the RECORD — so a request an operator creates counts too, not
|
|
5
|
+
# only chat ones. Guide: `craft-measure`.
|
|
6
|
+
- key: request_made
|
|
7
|
+
for: request
|
|
8
|
+
on: created
|
|
9
|
+
do:
|
|
10
|
+
- track: { journey: request, goal_reached: requested }
|
|
@@ -16,11 +16,7 @@ entities:
|
|
|
16
16
|
pipeline:
|
|
17
17
|
stages: [new, scheduled, done, cancelled]
|
|
18
18
|
terminal: [done, cancelled]
|
|
19
|
-
#
|
|
20
|
-
# only chat ones. Guide: `craft-measure`.
|
|
21
|
-
on:
|
|
22
|
-
create:
|
|
23
|
-
- track: { journey: request, goal_reached: requested }
|
|
19
|
+
# What happens by itself when a request is made, moves or goes quiet is a rule — `automation.yaml`.
|
|
24
20
|
|
|
25
21
|
# Rows every new project starts with (`octwin deploy --seed`) — a customer may see them.
|
|
26
22
|
demo:
|