octwin-cli 0.8.1 → 0.8.4

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
  }
@@ -892,17 +933,47 @@ async function cmdValidate(flags) {
892
933
  const packDir = resolve(flags.dir ?? '.');
893
934
  const { id, version, files, blobs } = localValidate(packDir); // offline structural gate first (fast, no server/token)
894
935
  console.log(`✓ ${id}@${version} passes the offline structural check (${Object.keys(files).length} files, ${Object.keys(blobs).length} image(s))`);
895
- /** Every YAML file in the bundle, parsed once. A syntax error is the structural gate's to report. */
896
- const yamlDocs = () => Object.entries(files)
897
- .filter(([p]) => /\.ya?ml$/i.test(p))
898
- .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;
899
960
  try {
900
- return [[p, parseYaml(body)]];
961
+ parsed.push([p, parseYaml(body)]);
901
962
  }
902
- catch {
903
- 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)}`);
904
967
  }
905
- });
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;
906
977
  // Checks that need the pulled KB. All of them DEGRADE when it is absent — the KB
907
978
  // is a gitignored cache wiped by every pull, so failing hard would break a fresh
908
979
  // clone before the author could act. But a skip is ANNOUNCED, and remembered:
@@ -1335,6 +1406,16 @@ async function readDeployProgress(body) {
1335
1406
  const decoder = new TextDecoder();
1336
1407
  let buf = '';
1337
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;
1338
1419
  // Non-terminal frames with `status:'error'` are step failures the install
1339
1420
  // SOFTENS to non-fatal (e.g. a demo-seed row) — the reconcile keeps going and
1340
1421
  // still emits a `done`. We collect them so the deploy is NOT reported as a
@@ -1357,6 +1438,7 @@ async function readDeployProgress(body) {
1357
1438
  ev = JSON.parse(dataLine.slice(5).trim());
1358
1439
  }
1359
1440
  catch {
1441
+ unparseableFrames++;
1360
1442
  continue;
1361
1443
  }
1362
1444
  if (ev.stage === 'done' || ev.stage === 'error') {
@@ -1371,6 +1453,11 @@ async function readDeployProgress(body) {
1371
1453
  }
1372
1454
  }
1373
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
+ }
1374
1461
  return { terminal, stepErrors };
1375
1462
  }
1376
1463
  /** What a `--request-listing` / `--withdraw-listing` deploy says it is doing, for the header line. */
@@ -1448,8 +1535,22 @@ function printPublicListing(state, note, live, justAsked = false) {
1448
1535
  break;
1449
1536
  }
1450
1537
  }
1451
- function printDeploySuccess(id, version, t, r, listing) {
1452
- console.log(`✓ Deployed ${id}@${version} and installed onto ${targetLabel(t)}`);
1538
+ /**
1539
+ * THE HEADLINE MUST BE HONEST, because it is the line people actually read.
1540
+ *
1541
+ * The step errors were never missing — `readDeployProgress` has collected them since
1542
+ * 0.1.14, and the caller prints them and exits non-zero. But it printed them AFTER this
1543
+ * function, so the first and largest line said `✓ Deployed` and the ⚠ came underneath.
1544
+ * A pack author reported the deploy as a clean success while its demo seed had failed;
1545
+ * they were reading the transcript exactly as it was written.
1546
+ *
1547
+ * So `problems` is passed IN rather than handled by the caller afterwards: a signal that
1548
+ * arrives after the verdict is not a signal.
1549
+ */
1550
+ function printDeploySuccess(id, version, t, r, listing, problems = 0) {
1551
+ console.log(problems > 0
1552
+ ? `⚠ Deployed ${id}@${version} onto ${targetLabel(t)} WITH ${problems} failure(s) — data may be incomplete`
1553
+ : `✓ Deployed ${id}@${version} and installed onto ${targetLabel(t)}`);
1453
1554
  if (r?.warning)
1454
1555
  console.log(` ⚠ ${r.warning}`);
1455
1556
  const s = r?.summary;
@@ -1463,6 +1564,11 @@ function printDeploySuccess(id, version, t, r, listing) {
1463
1564
  parts.push(`${s.images} image(s) generated`);
1464
1565
  if (s.rules)
1465
1566
  parts.push(`${s.rules} availability rule(s)`);
1567
+ // A count of rows that threw. The seed keeps going past a bad row now, so a
1568
+ // partial seed is a real outcome and has to be said out loud — the alternative
1569
+ // reads as a complete one with fewer records than the author wrote.
1570
+ if (s.failed)
1571
+ parts.push(`${s.failed} row(s) FAILED`);
1466
1572
  if (parts.length)
1467
1573
  console.log(` Seeded: ${parts.join(', ')}`);
1468
1574
  }
@@ -1509,13 +1615,14 @@ async function cmdSeed(flags) {
1509
1615
  const { terminal: final, stepErrors } = await readDeployProgress(res.body);
1510
1616
  if (!final || final.stage === 'error')
1511
1617
  die(`seed failed${final?.message ? `: ${final.message}` : ' (stream ended early)'}`);
1512
- console.log(`
1513
- ${final.message ?? 'seed complete'}`);
1618
+ // Same rule as `printDeploySuccess`: the verdict leads. A ✓ above the failures
1619
+ // is the line that gets read and quoted.
1620
+ console.log(`\n${stepErrors.length ? '⚠' : '✓'} ${final.message ?? 'seed complete'}`);
1514
1621
  printSeedCounts(final.result?.seeded);
1515
1622
  if (stepErrors.length) {
1516
1623
  // A kind failed but the rest ran — the reconcile softens each step. Say which,
1517
1624
  // and exit non-zero so a scripted `seed && chat` doesn't read as clean.
1518
- console.error(`
1625
+ console.error(`
1519
1626
  ⚠ ${stepErrors.length} step${stepErrors.length === 1 ? '' : 's'} failed — data may be incomplete:`);
1520
1627
  for (const e of stepErrors)
1521
1628
  console.error(` • ${e}`);
@@ -1536,8 +1643,15 @@ async function cmdSeed(flags) {
1536
1643
  printAuthHint(res.status, url);
1537
1644
  exitNow(1);
1538
1645
  }
1539
- console.log('✓ seed complete');
1646
+ // No frames on this path — `seed_failed` in the body is the only evidence a kind threw.
1647
+ const failedKinds = Object.entries((json?.seed_failed ?? {}));
1648
+ console.log(failedKinds.length ? `⚠ seed INCOMPLETE — ${failedKinds.length} kind(s) failed` : '✓ seed complete');
1540
1649
  printSeedCounts(json?.seeded);
1650
+ if (failedKinds.length) {
1651
+ for (const [kind, why] of failedKinds)
1652
+ console.error(` • ${kind}: ${why}`);
1653
+ exitNow(1);
1654
+ }
1541
1655
  }
1542
1656
  /** Per-kind counts, one line each. Prints nothing when the pack declared nothing. */
1543
1657
  function printSeedCounts(seeded) {
@@ -1571,12 +1685,14 @@ async function cmdDeploy(flags) {
1571
1685
  const { terminal: final, stepErrors } = await readDeployProgress(res.body);
1572
1686
  if (!final || final.stage === 'error')
1573
1687
  die(`deploy failed${final?.message ? `: ${final.message}` : ' (stream ended early)'}`);
1574
- printDeploySuccess(id, version, t, final, { intent: listing, packDir });
1688
+ // The count goes IN, so the headline itself carries the verdict. It used to be
1689
+ // printed underneath a `✓ Deployed` line, which is what a reporting author read.
1690
+ printDeploySuccess(id, version, t, final, { intent: listing, packDir }, stepErrors.length);
1575
1691
  if (stepErrors.length) {
1576
- // The pack IS installed, but a step (e.g. the demo seed) failed — say so
1577
- // plainly and exit non-zero so CI / a `deploy && chat` chain doesn't treat
1578
- // an incomplete install as a clean success.
1579
- console.error(`\n⚠ Deployed with ${stepErrors.length} warning${stepErrors.length === 1 ? '' : 's'} — data may be incomplete:`);
1692
+ // The pack IS installed, but a step (e.g. the demo seed) failed — list what,
1693
+ // and exit non-zero so CI / a `deploy && chat` chain doesn't treat an
1694
+ // incomplete install as a clean success.
1695
+ console.error(`\nFailures:`);
1580
1696
  for (const e of stepErrors)
1581
1697
  console.error(` • ${e}`);
1582
1698
  exitNow(1);
@@ -1599,7 +1715,17 @@ async function cmdDeploy(flags) {
1599
1715
  console.error(typeof json === 'string' ? json : JSON.stringify(json, null, 2));
1600
1716
  exitNow(1);
1601
1717
  }
1602
- printDeploySuccess(id, version, t, json, { intent: listing, packDir });
1718
+ // This path had NO step-error check at all. With no SSE frames to read, the only
1719
+ // evidence a seed kind threw is `seed_failed` in the body — so a broken seed printed
1720
+ // a clean `✓ Deployed` here even after the streaming path was fixed in 0.1.14.
1721
+ const failedKinds = Object.entries((json?.seed_failed ?? {}));
1722
+ printDeploySuccess(id, version, t, json, { intent: listing, packDir }, failedKinds.length);
1723
+ if (failedKinds.length) {
1724
+ console.error(`\nFailures:`);
1725
+ for (const [kind, why] of failedKinds)
1726
+ console.error(` • ${kind}: ${why}`);
1727
+ exitNow(1);
1728
+ }
1603
1729
  }
1604
1730
  /**
1605
1731
  * The QUALIFIED pack id (`<owner>.<name>`) for a manifest's bare name.
@@ -4830,371 +4956,371 @@ async function cmdUsage(flags) {
4830
4956
  console.log('\nThis is MODEL spend. WhatsApp/Meta message billing is operator-only — not reachable by an API token.');
4831
4957
  }
4832
4958
  function help() {
4833
- console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
4834
-
4835
- octwin --version # print the CLI version (+ any upgrade notice)
4836
- octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
4837
- octwin validate [--dir .] [--remote] [--require-kb] # --remote runs the platform's FULL schema check + lint (all errors at once)
4838
- octwin login --url <platformUrl> --token oct_… # a deploy token from the console
4839
- octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
4840
- octwin projects [--archived] [--json] # the --project slugs this token can name
4841
- octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
4842
- [--request-listing | --withdraw-listing] # public marketplace — opt-in, see: octwin help deploy
4843
- octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
4844
- octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
4845
- octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
4846
- octwin work [recordId] [--queues] [--json] # inspect the work inbox (worked records) + timelines
4847
- octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
4848
- octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
4849
- octwin media generate "<prompt>" [--out <file.png>] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
4850
- octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
4851
- octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
4852
- octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
4853
- octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
4854
- octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
4855
- octwin automation [campaigns] [--json] # the jobs your declarations produced + health, last result each
4856
- octwin integrations [--json] # declared connections BESIDE what is configured (the silent-never-fires check)
4857
- octwin integrations deliveries [<id>] | events # the outbound delivery log / inbound events
4858
- octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]
4859
- octwin performance [--detail] [--json] # the project's business indicators (value/conversion/duration)
4860
- octwin usage [--json] # model calls, tokens and cost (project if pinned, else workspace)
4861
- octwin platform-kb [pull] [--if-stale|--check] [--dir .] [--url <url>] # no token needed
4862
- octwin test [--dir .] # = validate --remote (the full platform check)
4863
- octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
4864
- octwin memos [--all] [--json] # read the platform's replies + notices (a reply to your feedback lands here)
4865
-
4866
- Writes — exercise the state your pack creates (each needs the matching :write scope):
4867
- octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…"
4868
- octwin records tasks | task complete <taskId> [--outcome done|cancelled]
4869
- octwin work assign <id> --to user:<uuid>|none | note <id> "…" | stage <id> --to <stage>
4870
- octwin work decide <id> --action <a> [--param k=v] [--dry-run] # --dry-run previews, commits nothing
4871
- octwin orders transition <ref> --to <status> | refund <ref> --force
4872
- octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
4873
- octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
4874
- octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
4875
- octwin automation run <jobId> | pause <jobId> | resume <jobId> | send <campaignId>
4876
- octwin integrations test <key> # a LIVE call to the connection's health: operation
4877
- octwin integrations retry|cancel|send-now <deliveryId>
4878
- (octwin integrations preflight <key> needs only integrations:read — it makes no call)
4879
-
4880
- Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
4881
- \`octwin chat --as <h>\` calls continue the same conversation; press a rendered
4882
- button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
4883
- Get a deploy token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
4884
- octwin platform-kb pull → writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
4885
- Config (deploy): flags > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login (\`octwin login\` sets the default target).
4959
+ console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
4960
+
4961
+ octwin --version # print the CLI version (+ any upgrade notice)
4962
+ octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
4963
+ octwin validate [--dir .] [--remote] [--require-kb] # --remote runs the platform's FULL schema check + lint (all errors at once)
4964
+ octwin login --url <platformUrl> --token oct_… # a deploy token from the console
4965
+ octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
4966
+ octwin projects [--archived] [--json] # the --project slugs this token can name
4967
+ octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
4968
+ [--request-listing | --withdraw-listing] # public marketplace — opt-in, see: octwin help deploy
4969
+ octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
4970
+ octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
4971
+ octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
4972
+ octwin work [recordId] [--queues] [--json] # inspect the work inbox (worked records) + timelines
4973
+ octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
4974
+ octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
4975
+ octwin media generate "<prompt>" [--out <file.png>] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
4976
+ octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
4977
+ octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
4978
+ octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
4979
+ octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
4980
+ octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
4981
+ octwin automation [campaigns] [--json] # the jobs your declarations produced + health, last result each
4982
+ octwin integrations [--json] # declared connections BESIDE what is configured (the silent-never-fires check)
4983
+ octwin integrations deliveries [<id>] | events # the outbound delivery log / inbound events
4984
+ octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]
4985
+ octwin performance [--detail] [--json] # the project's business indicators (value/conversion/duration)
4986
+ octwin usage [--json] # model calls, tokens and cost (project if pinned, else workspace)
4987
+ octwin platform-kb [pull] [--if-stale|--check] [--dir .] [--url <url>] # no token needed
4988
+ octwin test [--dir .] # = validate --remote (the full platform check)
4989
+ octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
4990
+ octwin memos [--all] [--json] # read the platform's replies + notices (a reply to your feedback lands here)
4991
+
4992
+ Writes — exercise the state your pack creates (each needs the matching :write scope):
4993
+ octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…"
4994
+ octwin records tasks | task complete <taskId> [--outcome done|cancelled]
4995
+ octwin work assign <id> --to user:<uuid>|none | note <id> "…" | stage <id> --to <stage>
4996
+ octwin work decide <id> --action <a> [--param k=v] [--dry-run] # --dry-run previews, commits nothing
4997
+ octwin orders transition <ref> --to <status> | refund <ref> --force
4998
+ octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
4999
+ octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
5000
+ octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
5001
+ octwin automation run <jobId> | pause <jobId> | resume <jobId> | send <campaignId>
5002
+ octwin integrations test <key> # a LIVE call to the connection's health: operation
5003
+ octwin integrations retry|cancel|send-now <deliveryId>
5004
+ (octwin integrations preflight <key> needs only integrations:read — it makes no call)
5005
+
5006
+ Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
5007
+ \`octwin chat --as <h>\` calls continue the same conversation; press a rendered
5008
+ button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
5009
+ Get a deploy token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
5010
+ octwin platform-kb pull → writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
5011
+ Config (deploy): flags > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login (\`octwin login\` sets the default target).
4886
5012
  Per-command usage: octwin <command> --help`);
4887
5013
  }
4888
5014
  /** Per-subcommand usage — printed for `octwin <cmd> --help|-h` BEFORE any
4889
5015
  * network/auth work (a --help that 401s is worse than no help at all). */
4890
5016
  const COMMAND_HELP = {
4891
- init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
5017
+ init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
4892
5018
  Scaffold a pure-YAML starter pack into <dir>.`,
4893
- validate: `octwin validate [--dir .] [--remote] [--require-kb] [--strict-primitives]
4894
- Offline structural check, plus two checks driven by the pulled capability
4895
- reference (render-intent fields, primitive arguments). Those two SKIP when the
4896
- reference is missing — the run says so, and --require-kb turns the skip into a
4897
- failure for CI. --remote additionally runs the platform's FULL manifest +
4898
- flow-DSL validation and its flow lint (all errors at once) — same check as deploy.
4899
- --strict-primitives (with --remote) additionally type-checks LITERAL args:
4900
- values against each primitive's declared input schema; expression strings
5019
+ validate: `octwin validate [--dir .] [--remote] [--require-kb] [--strict-primitives]
5020
+ Offline structural check, plus two checks driven by the pulled capability
5021
+ reference (render-intent fields, primitive arguments). Those two SKIP when the
5022
+ reference is missing — the run says so, and --require-kb turns the skip into a
5023
+ failure for CI. --remote additionally runs the platform's FULL manifest +
5024
+ flow-DSL validation and its flow lint (all errors at once) — same check as deploy.
5025
+ --strict-primitives (with --remote) additionally type-checks LITERAL args:
5026
+ values against each primitive's declared input schema; expression strings
4901
5027
  ('$found.id', '{$t(…)}') are always exempt.`,
4902
- login: `octwin login --url <platformUrl> --token oct_…
4903
- Save a deploy token (console → Settings → API tokens) for that platform url,
4904
- make that url the DEFAULT deploy target for every later command, and echo the
5028
+ login: `octwin login --url <platformUrl> --token oct_…
5029
+ Save a deploy token (console → Settings → API tokens) for that platform url,
5030
+ make that url the DEFAULT deploy target for every later command, and echo the
4905
5031
  workspace + project pin + scopes the token reaches.`,
4906
- whoami: `octwin whoami [--url <url>] [--tenant <slug>]
5032
+ whoami: `octwin whoami [--url <url>] [--tenant <slug>]
4907
5033
  Verify the resolved token authenticates against the tenant.`,
4908
- projects: `octwin projects [--archived] [--json]
4909
- List the workspace's projects — the slugs every --project flag takes, with the
4910
- plan's project cap. --archived includes archived ones. A pack:deploy token
4911
- reaches this (it names a project in every other command).
4912
-
4913
- octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
4914
- Create a project. The URL slug is derived from the name unless --slug pins one.
4915
- --pack installs an ALREADY-published pack; the usual next step is instead
4916
- \`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
4917
-
4918
- octwin projects rm <slug> [--yes]
4919
- HARD delete — the project and everything cascading from it (conversations,
4920
- contacts, records, installs). No undo, and not the same as archiving.
4921
- WITHOUT --yes it only previews what would be destroyed, so the dry run is the
4922
- default. Together these make a disposable end-to-end environment:
4923
- octwin projects create "Scratch" && octwin deploy --project scratch --seed
4924
- octwin chat "hi" --project scratch
4925
- octwin projects rm scratch --yes
5034
+ projects: `octwin projects [--archived] [--json]
5035
+ List the workspace's projects — the slugs every --project flag takes, with the
5036
+ plan's project cap. --archived includes archived ones. A pack:deploy token
5037
+ reaches this (it names a project in every other command).
5038
+
5039
+ octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
5040
+ Create a project. The URL slug is derived from the name unless --slug pins one.
5041
+ --pack installs an ALREADY-published pack; the usual next step is instead
5042
+ \`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
5043
+
5044
+ octwin projects rm <slug> [--yes]
5045
+ HARD delete — the project and everything cascading from it (conversations,
5046
+ contacts, records, installs). No undo, and not the same as archiving.
5047
+ WITHOUT --yes it only previews what would be destroyed, so the dry run is the
5048
+ default. Together these make a disposable end-to-end environment:
5049
+ octwin projects create "Scratch" && octwin deploy --project scratch --seed
5050
+ octwin chat "hi" --project scratch
5051
+ octwin projects rm scratch --yes
4926
5052
  Both verbs need the \`projects:write\` scope — a pack:deploy token does NOT confer it.`,
4927
- deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
4928
- [--request-listing | --withdraw-listing]
4929
- Upload the pack bundle, validate server-side, install onto the project.
4930
- --seed additionally applies the pack's demo seed (streams progress).
4931
-
4932
- A plain deploy says NOTHING about the public marketplace — it is a test loop, so it
4933
- neither asks for a listing nor gives one up. The marketplace flags are opt-in:
4934
-
4935
- --request-listing ask an operator to review this pack for the public marketplace
4936
- (the pre-signup storefront at /packs). Requires 'public: true'
4937
- under 'listing:' in manifest.yaml — the manifest states that the
4938
- pack is a product, the flag is you choosing to ask.
4939
- --withdraw-listing retract the request, including an approved listing.
4940
-
4941
- An approval covers the CONTENT it was made against, so a later deploy that changes the
5053
+ deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
5054
+ [--request-listing | --withdraw-listing]
5055
+ Upload the pack bundle, validate server-side, install onto the project.
5056
+ --seed additionally applies the pack's demo seed (streams progress).
5057
+
5058
+ A plain deploy says NOTHING about the public marketplace — it is a test loop, so it
5059
+ neither asks for a listing nor gives one up. The marketplace flags are opt-in:
5060
+
5061
+ --request-listing ask an operator to review this pack for the public marketplace
5062
+ (the pre-signup storefront at /packs). Requires 'public: true'
5063
+ under 'listing:' in manifest.yaml — the manifest states that the
5064
+ pack is a product, the flag is you choosing to ask.
5065
+ --withdraw-listing retract the request, including an approved listing.
5066
+
5067
+ An approval covers the CONTENT it was made against, so a later deploy that changes the
4942
5068
  pack returns it to the review queue on its own — no flag needed, and the CLI says so.`,
4943
- seed: `octwin seed [--pack <packId>]
4944
- Apply the pack's demo/reference data to the project it is installed on, without
4945
- redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
4946
- and the demo operator topology. Reports what each kind produced.
4947
- Idempotent and safe to re-run — records upsert, and existing media is REUSED rather
4948
- than regenerated, so a second pass costs nothing. --pack is only needed when a
5069
+ seed: `octwin seed [--pack <packId>]
5070
+ Apply the pack's demo/reference data to the project it is installed on, without
5071
+ redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
5072
+ and the demo operator topology. Reports what each kind produced.
5073
+ Idempotent and safe to re-run — records upsert, and existing media is REUSED rather
5074
+ than regenerated, so a second pass costs nothing. --pack is only needed when a
4949
5075
  project somehow runs more than one.`,
4950
- status: `octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
4951
- Show installed vs live version + the flow list for this pack.
4952
- The pack id is read from manifest.yaml and QUALIFIED with your workspace slug
4953
- (a manifest declares a bare name; the owner is attached when you publish). Pass
4954
- <packId> explicitly to skip that lookup — \`octwin agents\` and \`octwin projects\`
5076
+ status: `octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
5077
+ Show installed vs live version + the flow list for this pack.
5078
+ The pack id is read from manifest.yaml and QUALIFIED with your workspace slug
5079
+ (a manifest declares a bare name; the owner is attached when you publish). Pass
5080
+ <packId> explicitly to skip that lookup — \`octwin agents\` and \`octwin projects\`
4955
5081
  both print the qualified form.`,
4956
- records: `octwin records [entity] [id] [--limit 50] [--offset n]
4957
- Inspect the pack's XRM data. No args = list entities. Worked records (cases,
4958
- tickets, anything routed to a queue) read best through \`octwin work\`.
4959
-
4960
- WRITES (need \`records:write\`; every one is re-checked by RBAC on the record):
4961
- octwin records create <entity> --set field=value [--set …] [--stage s] [--contact <id>]
4962
- octwin records patch <recordId> --entity <entity> --set field=value
4963
- octwin records stage <recordId> --to <stage> [--note "..."]
4964
- octwin records note <recordId> "the note text"
4965
- octwin records tasks # open follow-up tasks (\`tasks\` plan feature)
4966
- octwin records task complete <taskId> [--outcome done|cancelled] [--note "..."]
4967
-
4968
- --set coerces JSON scalars: \`--set rating=4.5\` sends a number, \`--set x=null\`
4969
- sends null. Use --fields-json '{"a":{"b":1}}' for anything nested.
4970
- \`patch\` needs --entity even though it has an id: the route resolves the field
4971
- validator from it. A leading \`create/patch/stage/note/tasks/task\` is read as a
5082
+ records: `octwin records [entity] [id] [--limit 50] [--offset n]
5083
+ Inspect the pack's XRM data. No args = list entities. Worked records (cases,
5084
+ tickets, anything routed to a queue) read best through \`octwin work\`.
5085
+
5086
+ WRITES (need \`records:write\`; every one is re-checked by RBAC on the record):
5087
+ octwin records create <entity> --set field=value [--set …] [--stage s] [--contact <id>]
5088
+ octwin records patch <recordId> --entity <entity> --set field=value
5089
+ octwin records stage <recordId> --to <stage> [--note "..."]
5090
+ octwin records note <recordId> "the note text"
5091
+ octwin records tasks # open follow-up tasks (\`tasks\` plan feature)
5092
+ octwin records task complete <taskId> [--outcome done|cancelled] [--note "..."]
5093
+
5094
+ --set coerces JSON scalars: \`--set rating=4.5\` sends a number, \`--set x=null\`
5095
+ sends null. Use --fields-json '{"a":{"b":1}}' for anything nested.
5096
+ \`patch\` needs --entity even though it has an id: the route resolves the field
5097
+ validator from it. A leading \`create/patch/stage/note/tasks/task\` is read as a
4972
5098
  VERB — to list an entity actually named one of those, use \`--entity <name>\`.`,
4973
- work: `octwin work [recordId] [--queues] [--limit 50] [--offset n] [--json]
4974
- Inspect the work inbox — every entity the pack declares worked (cases, orders
4975
- needing review, applications, …): the inbox, one item + its timeline
4976
- (+ applicable actions), or --queues for queue keys + open counts.
4977
-
4978
- WRITES (need \`work:write\`; \`stage\` needs \`records:write\`):
4979
- octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none
4980
- octwin work note <recordId> "the note text"
4981
- octwin work stage <recordId> --to <stage> [--note "..."]
4982
- octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]
4983
-
4984
- \`decide\` applies one of the entity's declared operator actions — \`octwin work <id>\`
4985
- lists them with their params. --dry-run previews the customer-facing copy and the
4986
- resulting stage WITHOUT committing (that route needs only \`work:read\`).
5099
+ work: `octwin work [recordId] [--queues] [--limit 50] [--offset n] [--json]
5100
+ Inspect the work inbox — every entity the pack declares worked (cases, orders
5101
+ needing review, applications, …): the inbox, one item + its timeline
5102
+ (+ applicable actions), or --queues for queue keys + open counts.
5103
+
5104
+ WRITES (need \`work:write\`; \`stage\` needs \`records:write\`):
5105
+ octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none
5106
+ octwin work note <recordId> "the note text"
5107
+ octwin work stage <recordId> --to <stage> [--note "..."]
5108
+ octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]
5109
+
5110
+ \`decide\` applies one of the entity's declared operator actions — \`octwin work <id>\`
5111
+ lists them with their params. --dry-run previews the customer-facing copy and the
5112
+ resulting stage WITHOUT committing (that route needs only \`work:read\`).
4987
5113
  \`stage\` is the XRM records verb (one transition spelling platform-wide).`,
4988
- logs: `octwin logs [conversationId] [--as <handle>] [--json]
4989
- No id = recent conversations (handle, status, last activity; --as filters).
4990
- With id = the full event timeline including what each turn rendered.
5114
+ logs: `octwin logs [conversationId] [--as <handle>] [--json]
5115
+ No id = recent conversations (handle, status, last activity; --as filters).
5116
+ With id = the full event timeline including what each turn rendered.
4991
5117
  --json = raw events (verbatim payloads).`,
4992
- pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
4993
- Write a DEPLOYED pack's source back to disk — the inverse of deploy.
4994
- A pack pushed with 'octwin deploy' lives on the platform as an artifact the
4995
- runtime serves but nothing hands back, so its only source copy is the machine
4996
- that pushed it. Pull it, fix it, redeploy it.
4997
- Defaults to the version installed on the target project; --version overrides.
4998
- --dir defaults to ./<packId>; a non-empty dir needs --force.
4999
- The pulled dir redeploys where it came from — the target is your saved login.
5118
+ pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
5119
+ Write a DEPLOYED pack's source back to disk — the inverse of deploy.
5120
+ A pack pushed with 'octwin deploy' lives on the platform as an artifact the
5121
+ runtime serves but nothing hands back, so its only source copy is the machine
5122
+ that pushed it. Pull it, fix it, redeploy it.
5123
+ Defaults to the version installed on the target project; --version overrides.
5124
+ --dir defaults to ./<packId>; a non-empty dir needs --force.
5125
+ The pulled dir redeploys where it came from — the target is your saved login.
5000
5126
  You may pull a pack your tenant OWNS (deployed); an operator token pulls any.`,
5001
- chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
5002
- octwin chat --script <file> [--as <handle>] [--json]
5003
- Drive ONE turn through the dev web channel and print every render with its
5004
- tap ids. Same --as handle = same conversation (multi-turn works).
5005
- --tap presses a rendered button/list row instead of sending text.
5006
- --media uploads a local file (or a media id from 'media generate --json') as
5007
- an image/document/audio inbound — any "message" rides as its caption; feeds a
5008
- running media-collect flow (e.g. activate-app).
5009
- --json dumps the raw SSE envelopes for the turn.
5010
-
5011
- --script drives a WHOLE conversation from a file, ONE TURN PER LINE, in one
5012
- process over one connection — waiting for each turn to settle before sending
5013
- the next. Use this for any multi-step flow: chaining shell invocations races
5014
- the agent loop, because a turn ends on a quiet gap that can arrive while the
5015
- server is still working (the symptom is placeholder-filled fields or a second
5016
- workflow run). Blank lines and # comments are skipped:
5017
-
5018
- # book an appointment end to end
5019
- احجز موعد
5020
- tap:t:invoke:book-appointment:doctor_id=D1
5021
- media:./licence.jpg | here is my licence
5127
+ chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
5128
+ octwin chat --script <file> [--as <handle>] [--json]
5129
+ Drive ONE turn through the dev web channel and print every render with its
5130
+ tap ids. Same --as handle = same conversation (multi-turn works).
5131
+ --tap presses a rendered button/list row instead of sending text.
5132
+ --media uploads a local file (or a media id from 'media generate --json') as
5133
+ an image/document/audio inbound — any "message" rides as its caption; feeds a
5134
+ running media-collect flow (e.g. activate-app).
5135
+ --json dumps the raw SSE envelopes for the turn.
5136
+
5137
+ --script drives a WHOLE conversation from a file, ONE TURN PER LINE, in one
5138
+ process over one connection — waiting for each turn to settle before sending
5139
+ the next. Use this for any multi-step flow: chaining shell invocations races
5140
+ the agent loop, because a turn ends on a quiet gap that can arrive while the
5141
+ server is still working (the symptom is placeholder-filled fields or a second
5142
+ workflow run). Blank lines and # comments are skipped:
5143
+
5144
+ # book an appointment end to end
5145
+ احجز موعد
5146
+ tap:t:invoke:book-appointment:doctor_id=D1
5147
+ media:./licence.jpg | here is my licence
5022
5148
  tap:t:resume:book-appointment:run_id=R1;_ctl_approved=true`,
5023
- media: `octwin media generate "<prompt>" [--out <file.png>] [--json]
5024
- AI-generate an image (needs a media:generate-scoped token), store it as a
5025
- public asset, and print its MEDIA- handle + serve URL. --out downloads the
5026
- bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
5149
+ media: `octwin media generate "<prompt>" [--out <file.png>] [--json]
5150
+ AI-generate an image (needs a media:generate-scoped token), store it as a
5151
+ public asset, and print its MEDIA- handle + serve URL. --out downloads the
5152
+ bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
5027
5153
  bytes }. Pair with 'octwin chat --media' to drive media flows.`,
5028
- agents: `octwin agents [packId::agentId] [--prompt] [--json]
5029
- No args = the roster with each agent's EFFECTIVE model and which layer set it.
5030
- With an agent = every governed setting (model / memory.last_messages /
5031
- working_memory) plus the layer that won — an operator PLATFORM default can
5032
- override what your manifest declares, and this is where you see that.
5033
- --prompt = the exact system prompt the LLM sees for this project (pack
5034
- instructions + platform protocol + any project overlay). Needs agents:read.
5035
- The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
5036
-
5037
- WRITES (need \`agents:write\`):
5038
- octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
5039
- [--enable-tool <toolId>] [--disable-tool <toolId>]
5040
-
5041
- Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
5042
- so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
5154
+ agents: `octwin agents [packId::agentId] [--prompt] [--json]
5155
+ No args = the roster with each agent's EFFECTIVE model and which layer set it.
5156
+ With an agent = every governed setting (model / memory.last_messages /
5157
+ working_memory) plus the layer that won — an operator PLATFORM default can
5158
+ override what your manifest declares, and this is where you see that.
5159
+ --prompt = the exact system prompt the LLM sees for this project (pack
5160
+ instructions + platform protocol + any project overlay). Needs agents:read.
5161
+ The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
5162
+
5163
+ WRITES (need \`agents:write\`):
5164
+ octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
5165
+ [--enable-tool <toolId>] [--disable-tool <toolId>]
5166
+
5167
+ Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
5168
+ so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
5043
5169
  ids refuses --model with a 403 — the platform default governs there.`,
5044
- orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
5045
- No args = the order list (#number, status/payment, total, contact). With a
5046
- reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
5047
- payment_ref, and the allowed status transitions. Needs orders:read + the
5048
- \`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
5049
- so \`pending\` on a gateway-less workspace is expected, not a bug.
5050
-
5051
- WRITES (need \`orders:write\`):
5052
- octwin orders transition <reference_id> --to <status>
5053
- octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
5054
-
5055
- Refund is irreversible and moves money, hence --force. The route answers 200 even
5056
- when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
5057
- on a refusal rather than reporting a refund that never happened. Only a payment in
5170
+ orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
5171
+ No args = the order list (#number, status/payment, total, contact). With a
5172
+ reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
5173
+ payment_ref, and the allowed status transitions. Needs orders:read + the
5174
+ \`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
5175
+ so \`pending\` on a gateway-less workspace is expected, not a bug.
5176
+
5177
+ WRITES (need \`orders:write\`):
5178
+ octwin orders transition <reference_id> --to <status>
5179
+ octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
5180
+
5181
+ Refund is irreversible and moves money, hence --force. The route answers 200 even
5182
+ when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
5183
+ on a refusal rather than reporting a refund that never happened. Only a payment in
5058
5184
  \`captured\` state can be refunded; \`payment_status\` is never settable directly.`,
5059
- analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
5060
- No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
5061
- With an entity = stage-by-stage conversion (default --funnel) over the last 30
5062
- days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
5185
+ analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
5186
+ No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
5187
+ With an entity = stage-by-stage conversion (default --funnel) over the last 30
5188
+ days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
5063
5189
  range-filtered). Needs records:read + a \`view\` grant on \`record.<entity>\`.`,
5064
- catalog: `octwin catalog [--readiness] [--json]
5065
- The commerce \`product\` records + price, availability, stock (null = not
5066
- inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
5067
- Graph checklist (LIVE Graph calls; needs a bound access token). Needs
5068
- catalog:read + the \`catalog\` plan feature.
5069
-
5070
- WRITES (need \`catalog:write\`):
5071
- octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
5072
- octwin catalog stock <retailerId> [--set-on-hand <n>]
5073
-
5074
- \`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
5075
- inventory-tracked (always sellable), which is different from 0. Lowering on_hand
5076
- below the units already reserved for open carts is refused. Creating/deleting
5190
+ catalog: `octwin catalog [--readiness] [--json]
5191
+ The commerce \`product\` records + price, availability, stock (null = not
5192
+ inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
5193
+ Graph checklist (LIVE Graph calls; needs a bound access token). Needs
5194
+ catalog:read + the \`catalog\` plan feature.
5195
+
5196
+ WRITES (need \`catalog:write\`):
5197
+ octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
5198
+ octwin catalog stock <retailerId> [--set-on-hand <n>]
5199
+
5200
+ \`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
5201
+ inventory-tracked (always sellable), which is different from 0. Lowering on_hand
5202
+ below the units already reserved for open carts is refused. Creating/deleting
5077
5203
  products and the Meta catalog binding/sync stay in the console.`,
5078
- scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
5079
- No args = the engine state (bookable resource types, upcoming slots, booked
5080
- seats). --slots <recordId> computes the slots for one bookable resource
5081
- (occupancy included; --days is clamped to 1-31 server-side) — the way to verify
5082
- the availability rules a \`deploy --seed\` created. Needs scheduling:read.
5083
-
5084
- RULES (list needs scheduling:read; add/rm need scheduling:write):
5085
- octwin scheduling rules --resource <resourceRecordId>
5086
- octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
5087
- [--slot-minutes 30] [--capacity 1]
5088
- octwin scheduling rule rm <ruleId>
5089
- octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
5090
- [--start 09:00 --end 13:00]
5091
- octwin scheduling exception rm <exceptionId>
5092
-
5093
- --dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
5204
+ scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
5205
+ No args = the engine state (bookable resource types, upcoming slots, booked
5206
+ seats). --slots <recordId> computes the slots for one bookable resource
5207
+ (occupancy included; --days is clamped to 1-31 server-side) — the way to verify
5208
+ the availability rules a \`deploy --seed\` created. Needs scheduling:read.
5209
+
5210
+ RULES (list needs scheduling:read; add/rm need scheduling:write):
5211
+ octwin scheduling rules --resource <resourceRecordId>
5212
+ octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
5213
+ [--slot-minutes 30] [--capacity 1]
5214
+ octwin scheduling rule rm <ruleId>
5215
+ octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
5216
+ [--start 09:00 --end 13:00]
5217
+ octwin scheduling exception rm <exceptionId>
5218
+
5219
+ --dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
5094
5220
  \`--slots\` is how you check what a rule actually produces.`,
5095
- automation: `octwin automation [campaigns] [--limit n] [--offset n] [--json]
5096
- No args = every job the pack's automation declaration produced, with its status,
5097
- interval and LAST RESULT (matched / acted / errors), under a health line whose
5098
- counts come from SQL rather than from filtering the page — the job list is capped
5099
- server-side, so a client-side count would depend on the cap. Needs automation:read.
5100
-
5101
- Jobs are DERIVED from declarations. There is no \`create\`: no automation block in
5102
- the pack means no jobs, and \`octwin deploy\` is what installs them.
5103
-
5104
- WRITES (automation:write):
5105
- octwin automation run <jobId> # run once, now — prints matched/acted/errors
5106
- octwin automation pause|resume <jobId>
5107
- octwin automation send <campaignId> # enqueue a campaign; enqueued != delivered
5108
-
5109
- <jobId> is the \`key\` the list shows (its uuid works too). The routes themselves
5110
- accept only a uuid — the CLI resolves the key for you, and names the keys that do
5111
- exist when it cannot. A 403 on a write can be an RBAC grant gap rather than a
5221
+ automation: `octwin automation [campaigns] [--limit n] [--offset n] [--json]
5222
+ No args = every job the pack's automation declaration produced, with its status,
5223
+ interval and LAST RESULT (matched / acted / errors), under a health line whose
5224
+ counts come from SQL rather than from filtering the page — the job list is capped
5225
+ server-side, so a client-side count would depend on the cap. Needs automation:read.
5226
+
5227
+ Jobs are DERIVED from declarations. There is no \`create\`: no automation block in
5228
+ the pack means no jobs, and \`octwin deploy\` is what installs them.
5229
+
5230
+ WRITES (automation:write):
5231
+ octwin automation run <jobId> # run once, now — prints matched/acted/errors
5232
+ octwin automation pause|resume <jobId>
5233
+ octwin automation send <campaignId> # enqueue a campaign; enqueued != delivered
5234
+
5235
+ <jobId> is the \`key\` the list shows (its uuid works too). The routes themselves
5236
+ accept only a uuid — the CLI resolves the key for you, and names the keys that do
5237
+ exist when it cannot. A 403 on a write can be an RBAC grant gap rather than a
5112
5238
  missing scope: the action is re-checked against the job.`,
5113
- integrations: `octwin integrations [--json]
5114
- What the pack DECLARES beside what is actually CONFIGURED, in one view — because a
5115
- connection that is declared and never configured is the commonest reason an
5116
- integration silently never fires, and neither list alone can show it. Flags the
5117
- gap explicitly. Needs integrations:read.
5118
-
5119
- DIAGNOSE ONE CONNECTION:
5120
- octwin integrations preflight <key> # every check, with a fix hint. Makes NO
5121
- # outbound call — needs only integrations:read
5122
- octwin integrations test <key> # a LIVE call to its health: operation
5123
- # (integrations:write). Exits 1 when it fails.
5124
-
5125
- THE DELIVERY LOG:
5126
- octwin integrations deliveries [--status s] [--operation id] [--limit n]
5127
- octwin integrations deliveries <id> # + the redacted request/response snapshots
5128
- octwin integrations retry|cancel|send-now <id> # integrations:write
5129
- octwin integrations events # INBOUND events (what arrived at your webhook)
5130
-
5131
- retry/cancel answer 409 when the delivery is in the wrong state; the message
5239
+ integrations: `octwin integrations [--json]
5240
+ What the pack DECLARES beside what is actually CONFIGURED, in one view — because a
5241
+ connection that is declared and never configured is the commonest reason an
5242
+ integration silently never fires, and neither list alone can show it. Flags the
5243
+ gap explicitly. Needs integrations:read.
5244
+
5245
+ DIAGNOSE ONE CONNECTION:
5246
+ octwin integrations preflight <key> # every check, with a fix hint. Makes NO
5247
+ # outbound call — needs only integrations:read
5248
+ octwin integrations test <key> # a LIVE call to its health: operation
5249
+ # (integrations:write). Exits 1 when it fails.
5250
+
5251
+ THE DELIVERY LOG:
5252
+ octwin integrations deliveries [--status s] [--operation id] [--limit n]
5253
+ octwin integrations deliveries <id> # + the redacted request/response snapshots
5254
+ octwin integrations retry|cancel|send-now <id> # integrations:write
5255
+ octwin integrations events # INBOUND events (what arrived at your webhook)
5256
+
5257
+ retry/cancel answer 409 when the delivery is in the wrong state; the message
5132
5258
  carries the rule.`,
5133
- journeys: `octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition]
5134
- [--stage <stageId>] [--limit n] [--json]
5135
- No args = the journeys the pack declares. With an id, one of six views —
5136
- --funnel (default) stage-by-stage reach and drop-off · --overview entered vs
5137
- converted plus the biggest drop-off · --goals completions, contacts, value and
5138
- p50 time · --trends per-bucket activity · --cost tokens and dollars per goal ·
5139
- --definition what was DECLARED, unmeasured (the one view that works with no
5140
- traffic). Needs journeys:read.
5141
-
5142
- --stage <stageId> lists the runs sitting at a stage right now (a live snapshot,
5143
- not the funnel's cumulative reached counts).
5144
-
5145
- Same flag grammar as \`octwin analytics\` on purpose: a journey funnel and an
5146
- entity funnel are the same question about different subjects. Journeys carry RBAC
5147
- on top of the scope, so an empty answer can be a missing \`view\` grant rather
5259
+ journeys: `octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition]
5260
+ [--stage <stageId>] [--limit n] [--json]
5261
+ No args = the journeys the pack declares. With an id, one of six views —
5262
+ --funnel (default) stage-by-stage reach and drop-off · --overview entered vs
5263
+ converted plus the biggest drop-off · --goals completions, contacts, value and
5264
+ p50 time · --trends per-bucket activity · --cost tokens and dollars per goal ·
5265
+ --definition what was DECLARED, unmeasured (the one view that works with no
5266
+ traffic). Needs journeys:read.
5267
+
5268
+ --stage <stageId> lists the runs sitting at a stage right now (a live snapshot,
5269
+ not the funnel's cumulative reached counts).
5270
+
5271
+ Same flag grammar as \`octwin analytics\` on purpose: a journey funnel and an
5272
+ entity funnel are the same question about different subjects. Journeys carry RBAC
5273
+ on top of the scope, so an empty answer can be a missing \`view\` grant rather
5148
5274
  than missing data — the output says which causes are possible.`,
5149
- performance: `octwin performance [--detail] [--json]
5150
- The project's business indicators — value produced, conversion, duration — each
5151
- with its delta against the previous window and a \`why\` naming the declaration it
5152
- came from. --detail adds the per-indicator breakdown.
5153
-
5154
- Needs records:read, NOT a performance scope (there is none), so a read-only token
5155
- already reaches it. Indicators are DERIVED: a pack that declares no journey goal
5275
+ performance: `octwin performance [--detail] [--json]
5276
+ The project's business indicators — value produced, conversion, duration — each
5277
+ with its delta against the previous window and a \`why\` naming the declaration it
5278
+ came from. --detail adds the per-indicator breakdown.
5279
+
5280
+ Needs records:read, NOT a performance scope (there is none), so a read-only token
5281
+ already reaches it. Indicators are DERIVED: a pack that declares no journey goal
5156
5282
  value and no pipelined entity produces none, which is a different thing from zero.`,
5157
- usage: `octwin usage [--json]
5158
- Model calls, tokens and cost for the resolved scope — project when one is pinned
5159
- or passed with --project, otherwise the whole workspace. Broken down by model,
5160
- kind, agent and channel.
5161
-
5162
- Needs no particular scope: any valid token reaches it.
5163
-
5164
- This is MODEL spend only. WhatsApp/Meta message billing is operator-only and
5283
+ usage: `octwin usage [--json]
5284
+ Model calls, tokens and cost for the resolved scope — project when one is pinned
5285
+ or passed with --project, otherwise the whole workspace. Broken down by model,
5286
+ kind, agent and channel.
5287
+
5288
+ Needs no particular scope: any valid token reaches it.
5289
+
5290
+ This is MODEL spend only. WhatsApp/Meta message billing is operator-only and
5165
5291
  deliberately outside the token scope registry — no API token can read it.`,
5166
- 'platform-kb': `octwin platform-kb [pull] [--if-stale] [--check] [--dir .] [--url <url>] [--token <t>]
5167
- Pull the platform capability reference (markdown + JSON catalogs) into
5168
- .octwin/platform-kb/ for the octwin-pack authoring skill, plus three maps:
5169
- INDEX.md (the corpus) · SYMBOLS.md (every name -> its file; grep this) ·
5170
- OUTLINE.md (every heading with its line number).
5171
-
5172
- NO TOKEN NEEDED — the reference is platform stdlib and is served anonymously,
5173
- and this command never sends one. --token is accepted and ignored, so an older
5174
- script that passes it keeps working.
5175
-
5176
- --if-stale poll the platform's content_hash first and skip the download when
5177
- nothing changed. Cheap enough to run at the start of every session.
5178
- --check report only, write nothing. Exit 0 = current, 2 = stale or never
5179
- pulled, 1 = could not tell (offline / no reference served). For
5292
+ 'platform-kb': `octwin platform-kb [pull] [--if-stale] [--check] [--dir .] [--url <url>] [--token <t>]
5293
+ Pull the platform capability reference (markdown + JSON catalogs) into
5294
+ .octwin/platform-kb/ for the octwin-pack authoring skill, plus three maps:
5295
+ INDEX.md (the corpus) · SYMBOLS.md (every name -> its file; grep this) ·
5296
+ OUTLINE.md (every heading with its line number).
5297
+
5298
+ NO TOKEN NEEDED — the reference is platform stdlib and is served anonymously,
5299
+ and this command never sends one. --token is accepted and ignored, so an older
5300
+ script that passes it keeps working.
5301
+
5302
+ --if-stale poll the platform's content_hash first and skip the download when
5303
+ nothing changed. Cheap enough to run at the start of every session.
5304
+ --check report only, write nothing. Exit 0 = current, 2 = stale or never
5305
+ pulled, 1 = could not tell (offline / no reference served). For
5180
5306
  scripts and agent loops that want to branch without parsing prose.`,
5181
- test: `octwin test [--dir .]
5307
+ test: `octwin test [--dir .]
5182
5308
  Alias for \`octwin validate --remote\` — the full platform check.`,
5183
- memos: `octwin memos [--all] [--json]
5184
- Read what the platform has told you: a REPLY to a report you sent with
5185
- \`octwin feedback\`, or a NOTICE published to every author (a new capability,
5186
- a deprecation, a breaking change). Bodies are printed in full.
5187
- Reading marks them read, so the reminder stops. --all re-reads history and
5188
- acks nothing. --json to branch on \`severity\`
5309
+ memos: `octwin memos [--all] [--json]
5310
+ Read what the platform has told you: a REPLY to a report you sent with
5311
+ \`octwin feedback\`, or a NOTICE published to every author (a new capability,
5312
+ a deprecation, a breaking change). Bodies are printed in full.
5313
+ Reading marks them read, so the reminder stops. --all re-reads history and
5314
+ acks nothing. --json to branch on \`severity\`
5189
5315
  (info | action_required | breaking).`,
5190
- feedback: `octwin feedback [--dir .]
5191
- Submit this pack's FEEDBACK.md to the platform team.
5192
- The octwin-pack skill writes that file in its last step — findings grouped by
5193
- owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
5194
- you to paste it into a chat.
5195
- Attaches the pack id + version from manifest.yaml, this CLI's version, and the
5196
- content_hash of the capability reference in .octwin/platform-kb/ — triage needs
5197
- the last two to tell "the platform is wrong" from "that was already fixed" or
5316
+ feedback: `octwin feedback [--dir .]
5317
+ Submit this pack's FEEDBACK.md to the platform team.
5318
+ The octwin-pack skill writes that file in its last step — findings grouped by
5319
+ owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
5320
+ you to paste it into a chat.
5321
+ Attaches the pack id + version from manifest.yaml, this CLI's version, and the
5322
+ content_hash of the capability reference in .octwin/platform-kb/ — triage needs
5323
+ the last two to tell "the platform is wrong" from "that was already fixed" or
5198
5324
  "you were reading a stale reference". Needs the \`pack:deploy\` scope.`,
5199
5325
  };
5200
5326
  async function main() {