octwin-cli 0.8.0 → 0.8.3

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/dist/index.js CHANGED
@@ -448,12 +448,41 @@ function readManifestIdVersion(files) {
448
448
  */
449
449
  const DEFAULT_URL_KEY = 'default_url';
450
450
  function credsPath() { return join(homedir(), '.octwin', 'credentials.json'); }
451
+ /**
452
+ * The saved logins, or an empty map.
453
+ *
454
+ * ## A CORRUPT file is announced, not read as "never logged in"
455
+ *
456
+ * Both states produce `{}` and the advice attached to that answer — *run `octwin login`* — is
457
+ * right for the first and only accidentally right for the second: logging in again REWRITES
458
+ * the file, so it does clear the problem, while telling the author nothing about why a token
459
+ * they know they saved stopped being found. An interrupted write (a full disk, a killed
460
+ * process) is the likely cause and the one worth naming.
461
+ *
462
+ * Same three-state reasoning as [`kb-path.ts`](lib/kb-path.ts), reached the same way: absent
463
+ * and malformed are different facts and a reader that collapses them makes the caller guess.
464
+ * The warning goes to stderr so it cannot corrupt `--json` output on stdout.
465
+ */
466
+ /** Once per process — `readCreds` has three call sites and one broken file is one problem. */
467
+ let credsWarned = false;
451
468
  function readCreds() {
469
+ let raw;
452
470
  try {
453
- return JSON.parse(readFileSync(credsPath(), 'utf8'));
471
+ raw = readFileSync(credsPath(), 'utf8');
454
472
  }
455
473
  catch {
456
474
  return {};
475
+ } // absent — the ordinary "not logged in"
476
+ try {
477
+ return JSON.parse(raw);
478
+ }
479
+ catch (e) {
480
+ if (!credsWarned) {
481
+ credsWarned = true;
482
+ console.error(`⚠ ${credsPath()} is not readable JSON (${e.message.split(String.fromCharCode(10))[0]}).`);
483
+ console.error(' Treating it as no saved login. `octwin login` will rewrite it.');
484
+ }
485
+ return {};
457
486
  }
458
487
  }
459
488
  function writeCreds(map) {
@@ -559,7 +588,7 @@ async function outdatedNotice() {
559
588
  function readLocalKb(packDir) {
560
589
  const kbDir = findPlatformKbDir(packDir); // walks up — a repo-root pull covers every pack under it
561
590
  if (!kbDir)
562
- return null;
591
+ return null; // absent — nothing pulled, the ordinary case
563
592
  try {
564
593
  const idx = JSON.parse(readFileSync(join(kbDir, 'index.json'), 'utf8'));
565
594
  return {
@@ -567,7 +596,19 @@ function readLocalKb(packDir) {
567
596
  index: Array.isArray(idx.index) ? idx.index : [],
568
597
  };
569
598
  }
570
- catch {
599
+ catch (e) {
600
+ /**
601
+ * A directory that EXISTS and will not parse is announced, never returned as null.
602
+ *
603
+ * Null means "nothing pulled" to all six callers, and the drift nudge, `platform-kb
604
+ * status/diff` and the feedback report's `content_hash` all read it that way. So a
605
+ * half-written `index.json` — an interrupted pull, the likeliest way this breaks —
606
+ * presented as "you have not pulled yet", and the author re-ran the same command that
607
+ * left it that way. `kb-path.ts` states this rule for the DIRECTORY lookup and gives the
608
+ * reasoning; the index read next to it kept collapsing anyway.
609
+ */
610
+ console.error(`⚠ ${join(kbDir, 'index.json')} exists but could not be read (${e.message.split(String.fromCharCode(10))[0]}).`);
611
+ console.error(' Re-run `octwin platform-kb pull` — a previous pull was interrupted or the file is corrupt.');
571
612
  return null;
572
613
  }
573
614
  }
@@ -587,11 +628,12 @@ function diffKbIndex(prev, next) {
587
628
  }
588
629
  /**
589
630
  * Fetch the platform's KB identity (`?meta=1`) — the cheap poll behind both the
590
- * drift nudge and `--if-stale`. Returns null on anything that is not a clean
631
+ * drift nudge and `--if-stale`. Returns not-ok on anything that is not a clean
591
632
  * answer; the caller decides whether that is worth a word.
592
633
  *
593
- * `notAuthorized` is reported separately because it is the one failure with a
594
- * fix the author can act on.
634
+ * There is no `notAuthorized` case any more. `kbEndpoint` is anonymous, so this
635
+ * poll cannot be refused for lack of a scope — which used to be its most common
636
+ * failure, and the reason the drift nudge below carried a whole branch of advice.
595
637
  */
596
638
  async function fetchKbMeta(t, timeoutMs = 2_000) {
597
639
  const ep = kbEndpoint(t);
@@ -602,12 +644,12 @@ async function fetchKbMeta(t, timeoutMs = 2_000) {
602
644
  // socket checked out of the pool, and this poll runs on the way to a possible `exitNow` —
603
645
  // a held socket at exit is a pending libuv handle.
604
646
  await res.arrayBuffer().catch(() => undefined);
605
- return { ok: false, notAuthorized: res.status === 401 || res.status === 403 };
647
+ return { ok: false };
606
648
  }
607
649
  return { ok: true, meta: await res.json() };
608
650
  }
609
651
  catch {
610
- return { ok: false, notAuthorized: false };
652
+ return { ok: false };
611
653
  }
612
654
  }
613
655
  /** Nudge (to stderr) when the platform's capability KB has changed since the last
@@ -628,24 +670,20 @@ async function kbStaleNotice(flags) {
628
670
  const local = readLocalKb(packDir);
629
671
  if (!local?.content_hash)
630
672
  return []; // never pulled → the skill already says to pull
631
- const t = resolveTargetOrNull(flags);
632
- if (!t)
673
+ // URL only, deliberately: the KB poll is anonymous, so requiring a token here would
674
+ // silence the nudge for exactly the authors who most need it. This used to call
675
+ // `resolveTargetOrNull` (url AND token) against the tenant-scoped route, which needs
676
+ // `pack:deploy` — so an author inspecting data with a narrow (`records:read`-only)
677
+ // token got NO drift signal at all, silently, and a stale reference is precisely what
678
+ // makes an author invent a primitive from memory. The branch that apologised for that
679
+ // is gone with the cause; every remaining failure (offline, timeout, a platform
680
+ // serving no reference) stays silent, because observing must not break a command.
681
+ const t = readTarget(flags);
682
+ if (!t.url)
633
683
  return [];
634
684
  const polled = await fetchKbMeta(t);
635
- if (!polled.ok) {
636
- // The tenant-scoped meta poll needs `pack:deploy`, but this nudge rides on every
637
- // networked command — so an author inspecting data with a narrow (`records:read`-only)
638
- // token got NO drift signal at all, silently, and a stale reference is exactly what
639
- // makes an author invent a primitive from memory. Say so once; stay silent for every
640
- // other failure (offline, timeout, a platform without the route).
641
- if (polled.notAuthorized) {
642
- return [
643
- '\nⓘ can\'t check whether the platform capability reference drifted — that token lacks `pack:deploy`.',
644
- ' Check it without a token: octwin platform-kb --check (or refresh: octwin platform-kb --token oct_…)',
645
- ];
646
- }
685
+ if (!polled.ok)
647
686
  return [];
648
- }
649
687
  const meta = polled.meta;
650
688
  if (meta.content_hash && meta.content_hash !== local.content_hash) {
651
689
  // Per-entry summary (now that the index carries per-entry hashes) — the
@@ -895,17 +933,47 @@ async function cmdValidate(flags) {
895
933
  const packDir = resolve(flags.dir ?? '.');
896
934
  const { id, version, files, blobs } = localValidate(packDir); // offline structural gate first (fast, no server/token)
897
935
  console.log(`✓ ${id}@${version} passes the offline structural check (${Object.keys(files).length} files, ${Object.keys(blobs).length} image(s))`);
898
- /** Every YAML file in the bundle, parsed once. A syntax error is the structural gate's to report. */
899
- const yamlDocs = () => Object.entries(files)
900
- .filter(([p]) => /\.ya?ml$/i.test(p))
901
- .flatMap(([p, body]) => {
936
+ /**
937
+ * Every YAML file must PARSE — checked here, loudly, before anything reads one.
938
+ *
939
+ * This used to be a `try { … } catch { return [] }` inside `yamlDocs()`, on the stated
940
+ * ground that "a syntax error is the structural gate's to report". The structural gate
941
+ * (`validatePackBundle`) checks paths, extensions and traversal — it never parses YAML, so
942
+ * nothing reported it. The file was simply DROPPED from the list, every later check skipped
943
+ * it in silence, and `validate` printed all-green over a pack that could not be imported.
944
+ *
945
+ * Measured 2026-09-02: a duplicate `s_header:` key in a locale file passed `octwin validate`
946
+ * twice and was then refused by the platform's importer with `Map keys must be unique at
947
+ * line 24, column 3` — the SAME message this parser produces, from the same `yaml` version.
948
+ * The author's only clue was a red box in the console naming no file.
949
+ *
950
+ * Failing open is what made it dangerous: an unparseable file is not "no findings", it is
951
+ * "no idea", and those must never print the same. `parse()` already reports duplicate keys,
952
+ * bad indentation and unterminated strings — the ask was only to stop discarding it.
953
+ */
954
+ const firstLine = (m) => m.split(String.fromCharCode(10))[0] ?? m;
955
+ const parsed = [];
956
+ const yamlErrors = [];
957
+ for (const [p, body] of Object.entries(files)) {
958
+ if (!/\.ya?ml$/i.test(p))
959
+ continue;
902
960
  try {
903
- return [[p, parseYaml(body)]];
961
+ parsed.push([p, parseYaml(body)]);
904
962
  }
905
- catch {
906
- return [];
963
+ // First line only: the parser appends a source excerpt, which is useful in a terminal but
964
+ // turns one finding into five lines when several files fail at once.
965
+ catch (e) {
966
+ yamlErrors.push(`${p}: ${firstLine(e.message)}`);
907
967
  }
908
- });
968
+ }
969
+ if (yamlErrors.length > 0) {
970
+ for (const e of yamlErrors)
971
+ console.error(` ✗ ${e}`);
972
+ die(`YAML syntax (${yamlErrors.length} file${yamlErrors.length === 1 ? '' : 's'}) — the platform's importer refuses these too`);
973
+ }
974
+ console.log('✓ every YAML file parses (duplicate keys, indentation, unterminated strings)');
975
+ /** Every YAML file in the bundle, parsed once — all of them, since the gate above proved it. */
976
+ const yamlDocs = () => parsed;
909
977
  // Checks that need the pulled KB. All of them DEGRADE when it is absent — the KB
910
978
  // is a gitignored cache wiped by every pull, so failing hard would break a fresh
911
979
  // clone before the author could act. But a skip is ANNOUNCED, and remembered:
@@ -1207,20 +1275,32 @@ function resolveTargetOrNull(flags) {
1207
1275
  /**
1208
1276
  * Where to read the platform capability reference from, and how.
1209
1277
  *
1210
- * The KB is tenant-independent platform stdlib, and the platform now serves it
1211
- * anonymously at `/api/public/octwin-platform-kb` precisely so a COLD START does
1212
- * not need a login: `.octwin/` is gitignored, so a fresh clone or a fresh
1213
- * Claude Code session has no reference at all, and an authoring agent's first
1214
- * useful question came after a credential round-trip it had no way to satisfy.
1278
+ * ALWAYS the anonymous route. The KB is tenant-independent platform stdlib, and
1279
+ * the platform serves the SAME bundle from all three of its views -- its own
1280
+ * route file says so twice ("THREE views of the SAME `getPlatformKbBundle()`";
1281
+ * "the payload is identical") and forbids tenant data ever entering the reader.
1282
+ * So the authed route returns nothing extra, and asking for a credential to read
1283
+ * it can only ever subtract.
1215
1284
  *
1216
- * A token still WINS when one is available: the tenant-scoped route is the one
1217
- * that has always existed, it works against platforms that predate the public
1218
- * rung, and using it keeps the author's own instance the source of truth.
1285
+ * It subtracted, measurably. This used to prefer the tenant-scoped route
1286
+ * WHENEVER a token was saved, and that route is guarded by `pack:deploy` -- so a
1287
+ * developer holding a token WITHOUT that scope got a 403 on a pull that would
1288
+ * have succeeded with no token at all. Having logged in made the CLI strictly
1289
+ * worse than not having logged in, on the very first command an author runs. No
1290
+ * console preset could even reach `pack:deploy` before 2026-08-26, so that was
1291
+ * the likeliest token a new developer held.
1292
+ *
1293
+ * The two arguments the old comment gave for preferring a token did not survive
1294
+ * being checked: "keeps the author's own instance the source of truth" is
1295
+ * vacuous (both routes are `t.url` -- the same instance), and "works against
1296
+ * platforms that predate the public rung" is a backward-compatibility shim,
1297
+ * which this codebase does not carry.
1298
+ *
1299
+ * `--token` is still ACCEPTED here, and ignored, so a scripted `platform-kb pull
1300
+ * --token …` keeps working instead of turning into an unknown-flag error.
1219
1301
  */
1220
1302
  function kbEndpoint(t) {
1221
- return t.token
1222
- ? { url: `${t.url}/api/self/t/octwin-platform-kb`, headers: authHeaders(t), anonymous: false }
1223
- : { url: `${t.url}/api/public/octwin-platform-kb`, headers: {}, anonymous: true };
1303
+ return { url: `${t.url}/api/public/octwin-platform-kb`, headers: {} };
1224
1304
  }
1225
1305
  /** The raw resolution both wrappers share — may return empty url/token. */
1226
1306
  function readTarget(flags) {
@@ -1326,6 +1406,16 @@ async function readDeployProgress(body) {
1326
1406
  const decoder = new TextDecoder();
1327
1407
  let buf = '';
1328
1408
  let terminal = null;
1409
+ /**
1410
+ * Frames whose `data:` would not parse.
1411
+ *
1412
+ * Skipping one is right — a partial frame or a shape this CLI version does not know is not
1413
+ * a reason to abort a deploy that is succeeding. Skipping ALL of them silently is not: if
1414
+ * the server's envelope changed, every frame is discarded, `terminal` stays null, and the
1415
+ * caller reports "the stream ended without a terminal event" — true, unhelpful, and
1416
+ * indistinguishable from a dropped connection. The count turns that into a diagnosis.
1417
+ */
1418
+ let unparseableFrames = 0;
1329
1419
  // Non-terminal frames with `status:'error'` are step failures the install
1330
1420
  // SOFTENS to non-fatal (e.g. a demo-seed row) — the reconcile keeps going and
1331
1421
  // still emits a `done`. We collect them so the deploy is NOT reported as a
@@ -1348,6 +1438,7 @@ async function readDeployProgress(body) {
1348
1438
  ev = JSON.parse(dataLine.slice(5).trim());
1349
1439
  }
1350
1440
  catch {
1441
+ unparseableFrames++;
1351
1442
  continue;
1352
1443
  }
1353
1444
  if (ev.stage === 'done' || ev.stage === 'error') {
@@ -1362,6 +1453,11 @@ async function readDeployProgress(body) {
1362
1453
  }
1363
1454
  }
1364
1455
  }
1456
+ // Named only when it MATTERS: no terminal frame arrived and frames were discarded. Printing
1457
+ // it on a healthy deploy would be noise, and noise is how a real signal stops being read.
1458
+ if (terminal === null && unparseableFrames > 0) {
1459
+ console.error(`⚠ ${unparseableFrames} progress frame(s) could not be parsed — this CLI may be older than the platform. Try \`npm i -g octwin-cli@latest\`.`);
1460
+ }
1365
1461
  return { terminal, stepErrors };
1366
1462
  }
1367
1463
  /** What a `--request-listing` / `--withdraw-listing` deploy says it is doing, for the header line. */
@@ -1500,13 +1596,13 @@ async function cmdSeed(flags) {
1500
1596
  const { terminal: final, stepErrors } = await readDeployProgress(res.body);
1501
1597
  if (!final || final.stage === 'error')
1502
1598
  die(`seed failed${final?.message ? `: ${final.message}` : ' (stream ended early)'}`);
1503
- console.log(`
1599
+ console.log(`
1504
1600
  ✓ ${final.message ?? 'seed complete'}`);
1505
1601
  printSeedCounts(final.result?.seeded);
1506
1602
  if (stepErrors.length) {
1507
1603
  // A kind failed but the rest ran — the reconcile softens each step. Say which,
1508
1604
  // and exit non-zero so a scripted `seed && chat` doesn't read as clean.
1509
- console.error(`
1605
+ console.error(`
1510
1606
  ⚠ ${stepErrors.length} step${stepErrors.length === 1 ? '' : 's'} failed — data may be incomplete:`);
1511
1607
  for (const e of stepErrors)
1512
1608
  console.error(` • ${e}`);
@@ -1756,9 +1852,7 @@ async function cmdPlatformKb(flags) {
1756
1852
  const local = readLocalKb(packDir);
1757
1853
  const polled = await fetchKbMeta(t, 10_000);
1758
1854
  if (!polled.ok) {
1759
- console.error(polled.notAuthorized
1760
- ? '✗ cannot check — the platform refused the token, and this instance serves no anonymous reference.'
1761
- : `✗ cannot check — ${url} did not answer.`);
1855
+ console.error(`✗ cannot check — ${url} did not answer, or serves no capability reference.`);
1762
1856
  exitNow(1);
1763
1857
  }
1764
1858
  const remote = polled.meta.content_hash;
@@ -1787,7 +1881,7 @@ async function cmdPlatformKb(flags) {
1787
1881
  return;
1788
1882
  }
1789
1883
  }
1790
- console.log(`→ Pulling the platform capability reference from ${url}${ep.anonymous ? ' (anonymous — no token needed for the reference)' : ''} …`);
1884
+ console.log(`→ Pulling the platform capability reference from ${url} (no token needed for the reference) …`);
1791
1885
  const res = await fetchOrDie(ep.url, { headers: ep.headers }, 'platform-kb pull');
1792
1886
  const text = await res.text();
1793
1887
  if (!res.ok) {
@@ -4823,370 +4917,371 @@ async function cmdUsage(flags) {
4823
4917
  console.log('\nThis is MODEL spend. WhatsApp/Meta message billing is operator-only — not reachable by an API token.');
4824
4918
  }
4825
4919
  function help() {
4826
- console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
4827
-
4828
- octwin --version # print the CLI version (+ any upgrade notice)
4829
- octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
4830
- octwin validate [--dir .] [--remote] [--require-kb] # --remote runs the platform's FULL schema check + lint (all errors at once)
4831
- octwin login --url <platformUrl> --token oct_… # a deploy token from the console
4832
- octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
4833
- octwin projects [--archived] [--json] # the --project slugs this token can name
4834
- octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
4835
- [--request-listing | --withdraw-listing] # public marketplace — opt-in, see: octwin help deploy
4836
- octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
4837
- octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
4838
- octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
4839
- octwin work [recordId] [--queues] [--json] # inspect the work inbox (worked records) + timelines
4840
- octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
4841
- octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
4842
- octwin media generate "<prompt>" [--out <file.png>] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
4843
- octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
4844
- octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
4845
- octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
4846
- octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
4847
- octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
4848
- octwin automation [campaigns] [--json] # the jobs your declarations produced + health, last result each
4849
- octwin integrations [--json] # declared connections BESIDE what is configured (the silent-never-fires check)
4850
- octwin integrations deliveries [<id>] | events # the outbound delivery log / inbound events
4851
- octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]
4852
- octwin performance [--detail] [--json] # the project's business indicators (value/conversion/duration)
4853
- octwin usage [--json] # model calls, tokens and cost (project if pinned, else workspace)
4854
- octwin platform-kb [pull] [--if-stale|--check] [--dir .] [--url <url>] # no token needed
4855
- octwin test [--dir .] # = validate --remote (the full platform check)
4856
- octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
4857
- octwin memos [--all] [--json] # read the platform's replies + notices (a reply to your feedback lands here)
4858
-
4859
- Writes — exercise the state your pack creates (each needs the matching :write scope):
4860
- octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…"
4861
- octwin records tasks | task complete <taskId> [--outcome done|cancelled]
4862
- octwin work assign <id> --to user:<uuid>|none | note <id> "…" | stage <id> --to <stage>
4863
- octwin work decide <id> --action <a> [--param k=v] [--dry-run] # --dry-run previews, commits nothing
4864
- octwin orders transition <ref> --to <status> | refund <ref> --force
4865
- octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
4866
- octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
4867
- octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
4868
- octwin automation run <jobId> | pause <jobId> | resume <jobId> | send <campaignId>
4869
- octwin integrations test <key> # a LIVE call to the connection's health: operation
4870
- octwin integrations retry|cancel|send-now <deliveryId>
4871
- (octwin integrations preflight <key> needs only integrations:read — it makes no call)
4872
-
4873
- Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
4874
- \`octwin chat --as <h>\` calls continue the same conversation; press a rendered
4875
- button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
4876
- Get a deploy token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
4877
- octwin platform-kb pull → writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
4878
- Config (deploy): flags > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login (\`octwin login\` sets the default target).
4920
+ console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
4921
+
4922
+ octwin --version # print the CLI version (+ any upgrade notice)
4923
+ octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
4924
+ octwin validate [--dir .] [--remote] [--require-kb] # --remote runs the platform's FULL schema check + lint (all errors at once)
4925
+ octwin login --url <platformUrl> --token oct_… # a deploy token from the console
4926
+ octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
4927
+ octwin projects [--archived] [--json] # the --project slugs this token can name
4928
+ octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
4929
+ [--request-listing | --withdraw-listing] # public marketplace — opt-in, see: octwin help deploy
4930
+ octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
4931
+ octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
4932
+ octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
4933
+ octwin work [recordId] [--queues] [--json] # inspect the work inbox (worked records) + timelines
4934
+ octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
4935
+ octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
4936
+ octwin media generate "<prompt>" [--out <file.png>] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
4937
+ octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
4938
+ octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
4939
+ octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
4940
+ octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
4941
+ octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
4942
+ octwin automation [campaigns] [--json] # the jobs your declarations produced + health, last result each
4943
+ octwin integrations [--json] # declared connections BESIDE what is configured (the silent-never-fires check)
4944
+ octwin integrations deliveries [<id>] | events # the outbound delivery log / inbound events
4945
+ octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]
4946
+ octwin performance [--detail] [--json] # the project's business indicators (value/conversion/duration)
4947
+ octwin usage [--json] # model calls, tokens and cost (project if pinned, else workspace)
4948
+ octwin platform-kb [pull] [--if-stale|--check] [--dir .] [--url <url>] # no token needed
4949
+ octwin test [--dir .] # = validate --remote (the full platform check)
4950
+ octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
4951
+ octwin memos [--all] [--json] # read the platform's replies + notices (a reply to your feedback lands here)
4952
+
4953
+ Writes — exercise the state your pack creates (each needs the matching :write scope):
4954
+ octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…"
4955
+ octwin records tasks | task complete <taskId> [--outcome done|cancelled]
4956
+ octwin work assign <id> --to user:<uuid>|none | note <id> "…" | stage <id> --to <stage>
4957
+ octwin work decide <id> --action <a> [--param k=v] [--dry-run] # --dry-run previews, commits nothing
4958
+ octwin orders transition <ref> --to <status> | refund <ref> --force
4959
+ octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
4960
+ octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
4961
+ octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
4962
+ octwin automation run <jobId> | pause <jobId> | resume <jobId> | send <campaignId>
4963
+ octwin integrations test <key> # a LIVE call to the connection's health: operation
4964
+ octwin integrations retry|cancel|send-now <deliveryId>
4965
+ (octwin integrations preflight <key> needs only integrations:read — it makes no call)
4966
+
4967
+ Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
4968
+ \`octwin chat --as <h>\` calls continue the same conversation; press a rendered
4969
+ button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
4970
+ Get a deploy token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
4971
+ octwin platform-kb pull → writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
4972
+ Config (deploy): flags > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login (\`octwin login\` sets the default target).
4879
4973
  Per-command usage: octwin <command> --help`);
4880
4974
  }
4881
4975
  /** Per-subcommand usage — printed for `octwin <cmd> --help|-h` BEFORE any
4882
4976
  * network/auth work (a --help that 401s is worse than no help at all). */
4883
4977
  const COMMAND_HELP = {
4884
- init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
4978
+ init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
4885
4979
  Scaffold a pure-YAML starter pack into <dir>.`,
4886
- validate: `octwin validate [--dir .] [--remote] [--require-kb] [--strict-primitives]
4887
- Offline structural check, plus two checks driven by the pulled capability
4888
- reference (render-intent fields, primitive arguments). Those two SKIP when the
4889
- reference is missing — the run says so, and --require-kb turns the skip into a
4890
- failure for CI. --remote additionally runs the platform's FULL manifest +
4891
- flow-DSL validation and its flow lint (all errors at once) — same check as deploy.
4892
- --strict-primitives (with --remote) additionally type-checks LITERAL args:
4893
- values against each primitive's declared input schema; expression strings
4980
+ validate: `octwin validate [--dir .] [--remote] [--require-kb] [--strict-primitives]
4981
+ Offline structural check, plus two checks driven by the pulled capability
4982
+ reference (render-intent fields, primitive arguments). Those two SKIP when the
4983
+ reference is missing — the run says so, and --require-kb turns the skip into a
4984
+ failure for CI. --remote additionally runs the platform's FULL manifest +
4985
+ flow-DSL validation and its flow lint (all errors at once) — same check as deploy.
4986
+ --strict-primitives (with --remote) additionally type-checks LITERAL args:
4987
+ values against each primitive's declared input schema; expression strings
4894
4988
  ('$found.id', '{$t(…)}') are always exempt.`,
4895
- login: `octwin login --url <platformUrl> --token oct_…
4896
- Save a deploy token (console → Settings → API tokens) for that platform url,
4897
- make that url the DEFAULT deploy target for every later command, and echo the
4989
+ login: `octwin login --url <platformUrl> --token oct_…
4990
+ Save a deploy token (console → Settings → API tokens) for that platform url,
4991
+ make that url the DEFAULT deploy target for every later command, and echo the
4898
4992
  workspace + project pin + scopes the token reaches.`,
4899
- whoami: `octwin whoami [--url <url>] [--tenant <slug>]
4993
+ whoami: `octwin whoami [--url <url>] [--tenant <slug>]
4900
4994
  Verify the resolved token authenticates against the tenant.`,
4901
- projects: `octwin projects [--archived] [--json]
4902
- List the workspace's projects — the slugs every --project flag takes, with the
4903
- plan's project cap. --archived includes archived ones. A pack:deploy token
4904
- reaches this (it names a project in every other command).
4905
-
4906
- octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
4907
- Create a project. The URL slug is derived from the name unless --slug pins one.
4908
- --pack installs an ALREADY-published pack; the usual next step is instead
4909
- \`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
4910
-
4911
- octwin projects rm <slug> [--yes]
4912
- HARD delete — the project and everything cascading from it (conversations,
4913
- contacts, records, installs). No undo, and not the same as archiving.
4914
- WITHOUT --yes it only previews what would be destroyed, so the dry run is the
4915
- default. Together these make a disposable end-to-end environment:
4916
- octwin projects create "Scratch" && octwin deploy --project scratch --seed
4917
- octwin chat "hi" --project scratch
4918
- octwin projects rm scratch --yes
4995
+ projects: `octwin projects [--archived] [--json]
4996
+ List the workspace's projects — the slugs every --project flag takes, with the
4997
+ plan's project cap. --archived includes archived ones. A pack:deploy token
4998
+ reaches this (it names a project in every other command).
4999
+
5000
+ octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
5001
+ Create a project. The URL slug is derived from the name unless --slug pins one.
5002
+ --pack installs an ALREADY-published pack; the usual next step is instead
5003
+ \`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
5004
+
5005
+ octwin projects rm <slug> [--yes]
5006
+ HARD delete — the project and everything cascading from it (conversations,
5007
+ contacts, records, installs). No undo, and not the same as archiving.
5008
+ WITHOUT --yes it only previews what would be destroyed, so the dry run is the
5009
+ default. Together these make a disposable end-to-end environment:
5010
+ octwin projects create "Scratch" && octwin deploy --project scratch --seed
5011
+ octwin chat "hi" --project scratch
5012
+ octwin projects rm scratch --yes
4919
5013
  Both verbs need the \`projects:write\` scope — a pack:deploy token does NOT confer it.`,
4920
- deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
4921
- [--request-listing | --withdraw-listing]
4922
- Upload the pack bundle, validate server-side, install onto the project.
4923
- --seed additionally applies the pack's demo seed (streams progress).
4924
-
4925
- A plain deploy says NOTHING about the public marketplace — it is a test loop, so it
4926
- neither asks for a listing nor gives one up. The marketplace flags are opt-in:
4927
-
4928
- --request-listing ask an operator to review this pack for the public marketplace
4929
- (the pre-signup storefront at /packs). Requires 'public: true'
4930
- under 'listing:' in manifest.yaml — the manifest states that the
4931
- pack is a product, the flag is you choosing to ask.
4932
- --withdraw-listing retract the request, including an approved listing.
4933
-
4934
- An approval covers the CONTENT it was made against, so a later deploy that changes the
5014
+ deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
5015
+ [--request-listing | --withdraw-listing]
5016
+ Upload the pack bundle, validate server-side, install onto the project.
5017
+ --seed additionally applies the pack's demo seed (streams progress).
5018
+
5019
+ A plain deploy says NOTHING about the public marketplace — it is a test loop, so it
5020
+ neither asks for a listing nor gives one up. The marketplace flags are opt-in:
5021
+
5022
+ --request-listing ask an operator to review this pack for the public marketplace
5023
+ (the pre-signup storefront at /packs). Requires 'public: true'
5024
+ under 'listing:' in manifest.yaml — the manifest states that the
5025
+ pack is a product, the flag is you choosing to ask.
5026
+ --withdraw-listing retract the request, including an approved listing.
5027
+
5028
+ An approval covers the CONTENT it was made against, so a later deploy that changes the
4935
5029
  pack returns it to the review queue on its own — no flag needed, and the CLI says so.`,
4936
- seed: `octwin seed [--pack <packId>]
4937
- Apply the pack's demo/reference data to the project it is installed on, without
4938
- redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
4939
- and the demo operator topology. Reports what each kind produced.
4940
- Idempotent and safe to re-run — records upsert, and existing media is REUSED rather
4941
- than regenerated, so a second pass costs nothing. --pack is only needed when a
5030
+ seed: `octwin seed [--pack <packId>]
5031
+ Apply the pack's demo/reference data to the project it is installed on, without
5032
+ redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
5033
+ and the demo operator topology. Reports what each kind produced.
5034
+ Idempotent and safe to re-run — records upsert, and existing media is REUSED rather
5035
+ than regenerated, so a second pass costs nothing. --pack is only needed when a
4942
5036
  project somehow runs more than one.`,
4943
- status: `octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
4944
- Show installed vs live version + the flow list for this pack.
4945
- The pack id is read from manifest.yaml and QUALIFIED with your workspace slug
4946
- (a manifest declares a bare name; the owner is attached when you publish). Pass
4947
- <packId> explicitly to skip that lookup — \`octwin agents\` and \`octwin projects\`
5037
+ status: `octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
5038
+ Show installed vs live version + the flow list for this pack.
5039
+ The pack id is read from manifest.yaml and QUALIFIED with your workspace slug
5040
+ (a manifest declares a bare name; the owner is attached when you publish). Pass
5041
+ <packId> explicitly to skip that lookup — \`octwin agents\` and \`octwin projects\`
4948
5042
  both print the qualified form.`,
4949
- records: `octwin records [entity] [id] [--limit 50] [--offset n]
4950
- Inspect the pack's XRM data. No args = list entities. Worked records (cases,
4951
- tickets, anything routed to a queue) read best through \`octwin work\`.
4952
-
4953
- WRITES (need \`records:write\`; every one is re-checked by RBAC on the record):
4954
- octwin records create <entity> --set field=value [--set …] [--stage s] [--contact <id>]
4955
- octwin records patch <recordId> --entity <entity> --set field=value
4956
- octwin records stage <recordId> --to <stage> [--note "..."]
4957
- octwin records note <recordId> "the note text"
4958
- octwin records tasks # open follow-up tasks (\`tasks\` plan feature)
4959
- octwin records task complete <taskId> [--outcome done|cancelled] [--note "..."]
4960
-
4961
- --set coerces JSON scalars: \`--set rating=4.5\` sends a number, \`--set x=null\`
4962
- sends null. Use --fields-json '{"a":{"b":1}}' for anything nested.
4963
- \`patch\` needs --entity even though it has an id: the route resolves the field
4964
- validator from it. A leading \`create/patch/stage/note/tasks/task\` is read as a
5043
+ records: `octwin records [entity] [id] [--limit 50] [--offset n]
5044
+ Inspect the pack's XRM data. No args = list entities. Worked records (cases,
5045
+ tickets, anything routed to a queue) read best through \`octwin work\`.
5046
+
5047
+ WRITES (need \`records:write\`; every one is re-checked by RBAC on the record):
5048
+ octwin records create <entity> --set field=value [--set …] [--stage s] [--contact <id>]
5049
+ octwin records patch <recordId> --entity <entity> --set field=value
5050
+ octwin records stage <recordId> --to <stage> [--note "..."]
5051
+ octwin records note <recordId> "the note text"
5052
+ octwin records tasks # open follow-up tasks (\`tasks\` plan feature)
5053
+ octwin records task complete <taskId> [--outcome done|cancelled] [--note "..."]
5054
+
5055
+ --set coerces JSON scalars: \`--set rating=4.5\` sends a number, \`--set x=null\`
5056
+ sends null. Use --fields-json '{"a":{"b":1}}' for anything nested.
5057
+ \`patch\` needs --entity even though it has an id: the route resolves the field
5058
+ validator from it. A leading \`create/patch/stage/note/tasks/task\` is read as a
4965
5059
  VERB — to list an entity actually named one of those, use \`--entity <name>\`.`,
4966
- work: `octwin work [recordId] [--queues] [--limit 50] [--offset n] [--json]
4967
- Inspect the work inbox — every entity the pack declares worked (cases, orders
4968
- needing review, applications, …): the inbox, one item + its timeline
4969
- (+ applicable actions), or --queues for queue keys + open counts.
4970
-
4971
- WRITES (need \`work:write\`; \`stage\` needs \`records:write\`):
4972
- octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none
4973
- octwin work note <recordId> "the note text"
4974
- octwin work stage <recordId> --to <stage> [--note "..."]
4975
- octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]
4976
-
4977
- \`decide\` applies one of the entity's declared operator actions — \`octwin work <id>\`
4978
- lists them with their params. --dry-run previews the customer-facing copy and the
4979
- resulting stage WITHOUT committing (that route needs only \`work:read\`).
5060
+ work: `octwin work [recordId] [--queues] [--limit 50] [--offset n] [--json]
5061
+ Inspect the work inbox — every entity the pack declares worked (cases, orders
5062
+ needing review, applications, …): the inbox, one item + its timeline
5063
+ (+ applicable actions), or --queues for queue keys + open counts.
5064
+
5065
+ WRITES (need \`work:write\`; \`stage\` needs \`records:write\`):
5066
+ octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none
5067
+ octwin work note <recordId> "the note text"
5068
+ octwin work stage <recordId> --to <stage> [--note "..."]
5069
+ octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]
5070
+
5071
+ \`decide\` applies one of the entity's declared operator actions — \`octwin work <id>\`
5072
+ lists them with their params. --dry-run previews the customer-facing copy and the
5073
+ resulting stage WITHOUT committing (that route needs only \`work:read\`).
4980
5074
  \`stage\` is the XRM records verb (one transition spelling platform-wide).`,
4981
- logs: `octwin logs [conversationId] [--as <handle>] [--json]
4982
- No id = recent conversations (handle, status, last activity; --as filters).
4983
- With id = the full event timeline including what each turn rendered.
5075
+ logs: `octwin logs [conversationId] [--as <handle>] [--json]
5076
+ No id = recent conversations (handle, status, last activity; --as filters).
5077
+ With id = the full event timeline including what each turn rendered.
4984
5078
  --json = raw events (verbatim payloads).`,
4985
- pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
4986
- Write a DEPLOYED pack's source back to disk — the inverse of deploy.
4987
- A pack pushed with 'octwin deploy' lives on the platform as an artifact the
4988
- runtime serves but nothing hands back, so its only source copy is the machine
4989
- that pushed it. Pull it, fix it, redeploy it.
4990
- Defaults to the version installed on the target project; --version overrides.
4991
- --dir defaults to ./<packId>; a non-empty dir needs --force.
4992
- The pulled dir redeploys where it came from — the target is your saved login.
5079
+ pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
5080
+ Write a DEPLOYED pack's source back to disk — the inverse of deploy.
5081
+ A pack pushed with 'octwin deploy' lives on the platform as an artifact the
5082
+ runtime serves but nothing hands back, so its only source copy is the machine
5083
+ that pushed it. Pull it, fix it, redeploy it.
5084
+ Defaults to the version installed on the target project; --version overrides.
5085
+ --dir defaults to ./<packId>; a non-empty dir needs --force.
5086
+ The pulled dir redeploys where it came from — the target is your saved login.
4993
5087
  You may pull a pack your tenant OWNS (deployed); an operator token pulls any.`,
4994
- chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
4995
- octwin chat --script <file> [--as <handle>] [--json]
4996
- Drive ONE turn through the dev web channel and print every render with its
4997
- tap ids. Same --as handle = same conversation (multi-turn works).
4998
- --tap presses a rendered button/list row instead of sending text.
4999
- --media uploads a local file (or a media id from 'media generate --json') as
5000
- an image/document/audio inbound — any "message" rides as its caption; feeds a
5001
- running media-collect flow (e.g. activate-app).
5002
- --json dumps the raw SSE envelopes for the turn.
5003
-
5004
- --script drives a WHOLE conversation from a file, ONE TURN PER LINE, in one
5005
- process over one connection — waiting for each turn to settle before sending
5006
- the next. Use this for any multi-step flow: chaining shell invocations races
5007
- the agent loop, because a turn ends on a quiet gap that can arrive while the
5008
- server is still working (the symptom is placeholder-filled fields or a second
5009
- workflow run). Blank lines and # comments are skipped:
5010
-
5011
- # book an appointment end to end
5012
- احجز موعد
5013
- tap:t:invoke:book-appointment:doctor_id=D1
5014
- media:./licence.jpg | here is my licence
5088
+ chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
5089
+ octwin chat --script <file> [--as <handle>] [--json]
5090
+ Drive ONE turn through the dev web channel and print every render with its
5091
+ tap ids. Same --as handle = same conversation (multi-turn works).
5092
+ --tap presses a rendered button/list row instead of sending text.
5093
+ --media uploads a local file (or a media id from 'media generate --json') as
5094
+ an image/document/audio inbound — any "message" rides as its caption; feeds a
5095
+ running media-collect flow (e.g. activate-app).
5096
+ --json dumps the raw SSE envelopes for the turn.
5097
+
5098
+ --script drives a WHOLE conversation from a file, ONE TURN PER LINE, in one
5099
+ process over one connection — waiting for each turn to settle before sending
5100
+ the next. Use this for any multi-step flow: chaining shell invocations races
5101
+ the agent loop, because a turn ends on a quiet gap that can arrive while the
5102
+ server is still working (the symptom is placeholder-filled fields or a second
5103
+ workflow run). Blank lines and # comments are skipped:
5104
+
5105
+ # book an appointment end to end
5106
+ احجز موعد
5107
+ tap:t:invoke:book-appointment:doctor_id=D1
5108
+ media:./licence.jpg | here is my licence
5015
5109
  tap:t:resume:book-appointment:run_id=R1;_ctl_approved=true`,
5016
- media: `octwin media generate "<prompt>" [--out <file.png>] [--json]
5017
- AI-generate an image (needs a media:generate-scoped token), store it as a
5018
- public asset, and print its MEDIA- handle + serve URL. --out downloads the
5019
- bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
5110
+ media: `octwin media generate "<prompt>" [--out <file.png>] [--json]
5111
+ AI-generate an image (needs a media:generate-scoped token), store it as a
5112
+ public asset, and print its MEDIA- handle + serve URL. --out downloads the
5113
+ bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
5020
5114
  bytes }. Pair with 'octwin chat --media' to drive media flows.`,
5021
- agents: `octwin agents [packId::agentId] [--prompt] [--json]
5022
- No args = the roster with each agent's EFFECTIVE model and which layer set it.
5023
- With an agent = every governed setting (model / memory.last_messages /
5024
- working_memory) plus the layer that won — an operator PLATFORM default can
5025
- override what your manifest declares, and this is where you see that.
5026
- --prompt = the exact system prompt the LLM sees for this project (pack
5027
- instructions + platform protocol + any project overlay). Needs agents:read.
5028
- The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
5029
-
5030
- WRITES (need \`agents:write\`):
5031
- octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
5032
- [--enable-tool <toolId>] [--disable-tool <toolId>]
5033
-
5034
- Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
5035
- so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
5115
+ agents: `octwin agents [packId::agentId] [--prompt] [--json]
5116
+ No args = the roster with each agent's EFFECTIVE model and which layer set it.
5117
+ With an agent = every governed setting (model / memory.last_messages /
5118
+ working_memory) plus the layer that won — an operator PLATFORM default can
5119
+ override what your manifest declares, and this is where you see that.
5120
+ --prompt = the exact system prompt the LLM sees for this project (pack
5121
+ instructions + platform protocol + any project overlay). Needs agents:read.
5122
+ The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
5123
+
5124
+ WRITES (need \`agents:write\`):
5125
+ octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
5126
+ [--enable-tool <toolId>] [--disable-tool <toolId>]
5127
+
5128
+ Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
5129
+ so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
5036
5130
  ids refuses --model with a 403 — the platform default governs there.`,
5037
- orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
5038
- No args = the order list (#number, status/payment, total, contact). With a
5039
- reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
5040
- payment_ref, and the allowed status transitions. Needs orders:read + the
5041
- \`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
5042
- so \`pending\` on a gateway-less workspace is expected, not a bug.
5043
-
5044
- WRITES (need \`orders:write\`):
5045
- octwin orders transition <reference_id> --to <status>
5046
- octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
5047
-
5048
- Refund is irreversible and moves money, hence --force. The route answers 200 even
5049
- when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
5050
- on a refusal rather than reporting a refund that never happened. Only a payment in
5131
+ orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
5132
+ No args = the order list (#number, status/payment, total, contact). With a
5133
+ reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
5134
+ payment_ref, and the allowed status transitions. Needs orders:read + the
5135
+ \`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
5136
+ so \`pending\` on a gateway-less workspace is expected, not a bug.
5137
+
5138
+ WRITES (need \`orders:write\`):
5139
+ octwin orders transition <reference_id> --to <status>
5140
+ octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
5141
+
5142
+ Refund is irreversible and moves money, hence --force. The route answers 200 even
5143
+ when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
5144
+ on a refusal rather than reporting a refund that never happened. Only a payment in
5051
5145
  \`captured\` state can be refunded; \`payment_status\` is never settable directly.`,
5052
- analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
5053
- No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
5054
- With an entity = stage-by-stage conversion (default --funnel) over the last 30
5055
- days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
5146
+ analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
5147
+ No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
5148
+ With an entity = stage-by-stage conversion (default --funnel) over the last 30
5149
+ days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
5056
5150
  range-filtered). Needs records:read + a \`view\` grant on \`record.<entity>\`.`,
5057
- catalog: `octwin catalog [--readiness] [--json]
5058
- The commerce \`product\` records + price, availability, stock (null = not
5059
- inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
5060
- Graph checklist (LIVE Graph calls; needs a bound access token). Needs
5061
- catalog:read + the \`catalog\` plan feature.
5062
-
5063
- WRITES (need \`catalog:write\`):
5064
- octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
5065
- octwin catalog stock <retailerId> [--set-on-hand <n>]
5066
-
5067
- \`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
5068
- inventory-tracked (always sellable), which is different from 0. Lowering on_hand
5069
- below the units already reserved for open carts is refused. Creating/deleting
5151
+ catalog: `octwin catalog [--readiness] [--json]
5152
+ The commerce \`product\` records + price, availability, stock (null = not
5153
+ inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
5154
+ Graph checklist (LIVE Graph calls; needs a bound access token). Needs
5155
+ catalog:read + the \`catalog\` plan feature.
5156
+
5157
+ WRITES (need \`catalog:write\`):
5158
+ octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
5159
+ octwin catalog stock <retailerId> [--set-on-hand <n>]
5160
+
5161
+ \`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
5162
+ inventory-tracked (always sellable), which is different from 0. Lowering on_hand
5163
+ below the units already reserved for open carts is refused. Creating/deleting
5070
5164
  products and the Meta catalog binding/sync stay in the console.`,
5071
- scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
5072
- No args = the engine state (bookable resource types, upcoming slots, booked
5073
- seats). --slots <recordId> computes the slots for one bookable resource
5074
- (occupancy included; --days is clamped to 1-31 server-side) — the way to verify
5075
- the availability rules a \`deploy --seed\` created. Needs scheduling:read.
5076
-
5077
- RULES (list needs scheduling:read; add/rm need scheduling:write):
5078
- octwin scheduling rules --resource <resourceRecordId>
5079
- octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
5080
- [--slot-minutes 30] [--capacity 1]
5081
- octwin scheduling rule rm <ruleId>
5082
- octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
5083
- [--start 09:00 --end 13:00]
5084
- octwin scheduling exception rm <exceptionId>
5085
-
5086
- --dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
5165
+ scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
5166
+ No args = the engine state (bookable resource types, upcoming slots, booked
5167
+ seats). --slots <recordId> computes the slots for one bookable resource
5168
+ (occupancy included; --days is clamped to 1-31 server-side) — the way to verify
5169
+ the availability rules a \`deploy --seed\` created. Needs scheduling:read.
5170
+
5171
+ RULES (list needs scheduling:read; add/rm need scheduling:write):
5172
+ octwin scheduling rules --resource <resourceRecordId>
5173
+ octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
5174
+ [--slot-minutes 30] [--capacity 1]
5175
+ octwin scheduling rule rm <ruleId>
5176
+ octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
5177
+ [--start 09:00 --end 13:00]
5178
+ octwin scheduling exception rm <exceptionId>
5179
+
5180
+ --dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
5087
5181
  \`--slots\` is how you check what a rule actually produces.`,
5088
- automation: `octwin automation [campaigns] [--limit n] [--offset n] [--json]
5089
- No args = every job the pack's automation declaration produced, with its status,
5090
- interval and LAST RESULT (matched / acted / errors), under a health line whose
5091
- counts come from SQL rather than from filtering the page — the job list is capped
5092
- server-side, so a client-side count would depend on the cap. Needs automation:read.
5093
-
5094
- Jobs are DERIVED from declarations. There is no \`create\`: no automation block in
5095
- the pack means no jobs, and \`octwin deploy\` is what installs them.
5096
-
5097
- WRITES (automation:write):
5098
- octwin automation run <jobId> # run once, now — prints matched/acted/errors
5099
- octwin automation pause|resume <jobId>
5100
- octwin automation send <campaignId> # enqueue a campaign; enqueued != delivered
5101
-
5102
- <jobId> is the \`key\` the list shows (its uuid works too). The routes themselves
5103
- accept only a uuid — the CLI resolves the key for you, and names the keys that do
5104
- exist when it cannot. A 403 on a write can be an RBAC grant gap rather than a
5182
+ automation: `octwin automation [campaigns] [--limit n] [--offset n] [--json]
5183
+ No args = every job the pack's automation declaration produced, with its status,
5184
+ interval and LAST RESULT (matched / acted / errors), under a health line whose
5185
+ counts come from SQL rather than from filtering the page — the job list is capped
5186
+ server-side, so a client-side count would depend on the cap. Needs automation:read.
5187
+
5188
+ Jobs are DERIVED from declarations. There is no \`create\`: no automation block in
5189
+ the pack means no jobs, and \`octwin deploy\` is what installs them.
5190
+
5191
+ WRITES (automation:write):
5192
+ octwin automation run <jobId> # run once, now — prints matched/acted/errors
5193
+ octwin automation pause|resume <jobId>
5194
+ octwin automation send <campaignId> # enqueue a campaign; enqueued != delivered
5195
+
5196
+ <jobId> is the \`key\` the list shows (its uuid works too). The routes themselves
5197
+ accept only a uuid — the CLI resolves the key for you, and names the keys that do
5198
+ exist when it cannot. A 403 on a write can be an RBAC grant gap rather than a
5105
5199
  missing scope: the action is re-checked against the job.`,
5106
- integrations: `octwin integrations [--json]
5107
- What the pack DECLARES beside what is actually CONFIGURED, in one view — because a
5108
- connection that is declared and never configured is the commonest reason an
5109
- integration silently never fires, and neither list alone can show it. Flags the
5110
- gap explicitly. Needs integrations:read.
5111
-
5112
- DIAGNOSE ONE CONNECTION:
5113
- octwin integrations preflight <key> # every check, with a fix hint. Makes NO
5114
- # outbound call — needs only integrations:read
5115
- octwin integrations test <key> # a LIVE call to its health: operation
5116
- # (integrations:write). Exits 1 when it fails.
5117
-
5118
- THE DELIVERY LOG:
5119
- octwin integrations deliveries [--status s] [--operation id] [--limit n]
5120
- octwin integrations deliveries <id> # + the redacted request/response snapshots
5121
- octwin integrations retry|cancel|send-now <id> # integrations:write
5122
- octwin integrations events # INBOUND events (what arrived at your webhook)
5123
-
5124
- retry/cancel answer 409 when the delivery is in the wrong state; the message
5200
+ integrations: `octwin integrations [--json]
5201
+ What the pack DECLARES beside what is actually CONFIGURED, in one view — because a
5202
+ connection that is declared and never configured is the commonest reason an
5203
+ integration silently never fires, and neither list alone can show it. Flags the
5204
+ gap explicitly. Needs integrations:read.
5205
+
5206
+ DIAGNOSE ONE CONNECTION:
5207
+ octwin integrations preflight <key> # every check, with a fix hint. Makes NO
5208
+ # outbound call — needs only integrations:read
5209
+ octwin integrations test <key> # a LIVE call to its health: operation
5210
+ # (integrations:write). Exits 1 when it fails.
5211
+
5212
+ THE DELIVERY LOG:
5213
+ octwin integrations deliveries [--status s] [--operation id] [--limit n]
5214
+ octwin integrations deliveries <id> # + the redacted request/response snapshots
5215
+ octwin integrations retry|cancel|send-now <id> # integrations:write
5216
+ octwin integrations events # INBOUND events (what arrived at your webhook)
5217
+
5218
+ retry/cancel answer 409 when the delivery is in the wrong state; the message
5125
5219
  carries the rule.`,
5126
- journeys: `octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition]
5127
- [--stage <stageId>] [--limit n] [--json]
5128
- No args = the journeys the pack declares. With an id, one of six views —
5129
- --funnel (default) stage-by-stage reach and drop-off · --overview entered vs
5130
- converted plus the biggest drop-off · --goals completions, contacts, value and
5131
- p50 time · --trends per-bucket activity · --cost tokens and dollars per goal ·
5132
- --definition what was DECLARED, unmeasured (the one view that works with no
5133
- traffic). Needs journeys:read.
5134
-
5135
- --stage <stageId> lists the runs sitting at a stage right now (a live snapshot,
5136
- not the funnel's cumulative reached counts).
5137
-
5138
- Same flag grammar as \`octwin analytics\` on purpose: a journey funnel and an
5139
- entity funnel are the same question about different subjects. Journeys carry RBAC
5140
- on top of the scope, so an empty answer can be a missing \`view\` grant rather
5220
+ journeys: `octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition]
5221
+ [--stage <stageId>] [--limit n] [--json]
5222
+ No args = the journeys the pack declares. With an id, one of six views —
5223
+ --funnel (default) stage-by-stage reach and drop-off · --overview entered vs
5224
+ converted plus the biggest drop-off · --goals completions, contacts, value and
5225
+ p50 time · --trends per-bucket activity · --cost tokens and dollars per goal ·
5226
+ --definition what was DECLARED, unmeasured (the one view that works with no
5227
+ traffic). Needs journeys:read.
5228
+
5229
+ --stage <stageId> lists the runs sitting at a stage right now (a live snapshot,
5230
+ not the funnel's cumulative reached counts).
5231
+
5232
+ Same flag grammar as \`octwin analytics\` on purpose: a journey funnel and an
5233
+ entity funnel are the same question about different subjects. Journeys carry RBAC
5234
+ on top of the scope, so an empty answer can be a missing \`view\` grant rather
5141
5235
  than missing data — the output says which causes are possible.`,
5142
- performance: `octwin performance [--detail] [--json]
5143
- The project's business indicators — value produced, conversion, duration — each
5144
- with its delta against the previous window and a \`why\` naming the declaration it
5145
- came from. --detail adds the per-indicator breakdown.
5146
-
5147
- Needs records:read, NOT a performance scope (there is none), so a read-only token
5148
- already reaches it. Indicators are DERIVED: a pack that declares no journey goal
5236
+ performance: `octwin performance [--detail] [--json]
5237
+ The project's business indicators — value produced, conversion, duration — each
5238
+ with its delta against the previous window and a \`why\` naming the declaration it
5239
+ came from. --detail adds the per-indicator breakdown.
5240
+
5241
+ Needs records:read, NOT a performance scope (there is none), so a read-only token
5242
+ already reaches it. Indicators are DERIVED: a pack that declares no journey goal
5149
5243
  value and no pipelined entity produces none, which is a different thing from zero.`,
5150
- usage: `octwin usage [--json]
5151
- Model calls, tokens and cost for the resolved scope — project when one is pinned
5152
- or passed with --project, otherwise the whole workspace. Broken down by model,
5153
- kind, agent and channel.
5154
-
5155
- Needs no particular scope: any valid token reaches it.
5156
-
5157
- This is MODEL spend only. WhatsApp/Meta message billing is operator-only and
5244
+ usage: `octwin usage [--json]
5245
+ Model calls, tokens and cost for the resolved scope — project when one is pinned
5246
+ or passed with --project, otherwise the whole workspace. Broken down by model,
5247
+ kind, agent and channel.
5248
+
5249
+ Needs no particular scope: any valid token reaches it.
5250
+
5251
+ This is MODEL spend only. WhatsApp/Meta message billing is operator-only and
5158
5252
  deliberately outside the token scope registry — no API token can read it.`,
5159
- 'platform-kb': `octwin platform-kb [pull] [--if-stale] [--check] [--dir .] [--url <url>] [--token <t>]
5160
- Pull the platform capability reference (markdown + JSON catalogs) into
5161
- .octwin/platform-kb/ for the octwin-pack authoring skill, plus three maps:
5162
- INDEX.md (the corpus) · SYMBOLS.md (every name -> its file; grep this) ·
5163
- OUTLINE.md (every heading with its line number).
5164
-
5165
- NO TOKEN NEEDED — the reference is platform stdlib and is served anonymously.
5166
- A token is used when you have one (it also works against older platforms).
5167
-
5168
- --if-stale poll the platform's content_hash first and skip the download when
5169
- nothing changed. Cheap enough to run at the start of every session.
5170
- --check report only, write nothing. Exit 0 = current, 2 = stale or never
5171
- pulled, 1 = could not tell (offline / refused). For scripts and
5172
- agent loops that want to branch without parsing prose.`,
5173
- test: `octwin test [--dir .]
5253
+ 'platform-kb': `octwin platform-kb [pull] [--if-stale] [--check] [--dir .] [--url <url>] [--token <t>]
5254
+ Pull the platform capability reference (markdown + JSON catalogs) into
5255
+ .octwin/platform-kb/ for the octwin-pack authoring skill, plus three maps:
5256
+ INDEX.md (the corpus) · SYMBOLS.md (every name -> its file; grep this) ·
5257
+ OUTLINE.md (every heading with its line number).
5258
+
5259
+ NO TOKEN NEEDED — the reference is platform stdlib and is served anonymously,
5260
+ and this command never sends one. --token is accepted and ignored, so an older
5261
+ script that passes it keeps working.
5262
+
5263
+ --if-stale poll the platform's content_hash first and skip the download when
5264
+ nothing changed. Cheap enough to run at the start of every session.
5265
+ --check report only, write nothing. Exit 0 = current, 2 = stale or never
5266
+ pulled, 1 = could not tell (offline / no reference served). For
5267
+ scripts and agent loops that want to branch without parsing prose.`,
5268
+ test: `octwin test [--dir .]
5174
5269
  Alias for \`octwin validate --remote\` — the full platform check.`,
5175
- memos: `octwin memos [--all] [--json]
5176
- Read what the platform has told you: a REPLY to a report you sent with
5177
- \`octwin feedback\`, or a NOTICE published to every author (a new capability,
5178
- a deprecation, a breaking change). Bodies are printed in full.
5179
- Reading marks them read, so the reminder stops. --all re-reads history and
5180
- acks nothing. --json to branch on \`severity\`
5270
+ memos: `octwin memos [--all] [--json]
5271
+ Read what the platform has told you: a REPLY to a report you sent with
5272
+ \`octwin feedback\`, or a NOTICE published to every author (a new capability,
5273
+ a deprecation, a breaking change). Bodies are printed in full.
5274
+ Reading marks them read, so the reminder stops. --all re-reads history and
5275
+ acks nothing. --json to branch on \`severity\`
5181
5276
  (info | action_required | breaking).`,
5182
- feedback: `octwin feedback [--dir .]
5183
- Submit this pack's FEEDBACK.md to the platform team.
5184
- The octwin-pack skill writes that file in its last step — findings grouped by
5185
- owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
5186
- you to paste it into a chat.
5187
- Attaches the pack id + version from manifest.yaml, this CLI's version, and the
5188
- content_hash of the capability reference in .octwin/platform-kb/ — triage needs
5189
- the last two to tell "the platform is wrong" from "that was already fixed" or
5277
+ feedback: `octwin feedback [--dir .]
5278
+ Submit this pack's FEEDBACK.md to the platform team.
5279
+ The octwin-pack skill writes that file in its last step — findings grouped by
5280
+ owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
5281
+ you to paste it into a chat.
5282
+ Attaches the pack id + version from manifest.yaml, this CLI's version, and the
5283
+ content_hash of the capability reference in .octwin/platform-kb/ — triage needs
5284
+ the last two to tell "the platform is wrong" from "that was already fixed" or
5190
5285
  "you were reading a stale reference". Needs the \`pack:deploy\` scope.`,
5191
5286
  };
5192
5287
  async function main() {