octwin-cli 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -15,13 +15,13 @@
15
15
  * octwin projects [--archived] # the --project slugs this token can name
16
16
  * octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
17
17
  * octwin pull <packId> [--dir <out>] [--version v] [--force] # write a DEPLOYED pack's source back to disk
18
- * octwin status [--dir .] # did my deploy land? which version is live?
18
+ * octwin status [<packId>] # did my deploy land? which version is live?
19
19
  * octwin records [entity] [id] # inspect the pack's XRM data (records:read token)
20
20
  * octwin work [recordId] [--queues] # inspect the work inbox (worked records) — list / one item + timeline
21
21
  * octwin logs [conversationId] [--as h] [--json] # list conversations / show one's timeline
22
22
  * octwin chat "msg" [--as h] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn via the web channel (+ send media)
23
23
  * octwin chat --script <file> [--as h] # drive a WHOLE conversation, one turn per line (the reliable way to test a flow)
24
- * octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json] # AI-generate an image → MEDIA- handle (media:generate scope)
24
+ * octwin media generate "<prompt>" [--out <file.png>] [--json] # AI-generate an image → MEDIA- handle (media:generate scope)
25
25
  * octwin agents [packId::agentId] [--prompt] # effective model/memory + which layer won; --prompt = the resolved system prompt
26
26
  * octwin orders [reference_id] # the orders a conversation produced — money breakdown + payment state (orders:read)
27
27
  * octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion, any pipelined entity
@@ -62,6 +62,7 @@ import { validatePackBundle, describePackNameProblem, asPackName } from './lib/v
62
62
  import { loadAllowedRenderKeys, findRenderKeyViolations, describeRenderFinding } from './lib/render-check.js';
63
63
  import { loadPrimitiveArgSpecs, findArgViolations, describeArgFinding } from './lib/args-check.js';
64
64
  import { yamlLineOf } from './lib/yaml-pos.js';
65
+ import { localhostFamilyHint } from './lib/net-hint.js';
65
66
  import { loadBuiltinNames, findBuiltinViolations, describeBuiltinFinding } from './lib/builtin-check.js';
66
67
  import { loadTemplateSpecs, findTemplateViolations, describeTemplateFinding } from './lib/template-check.js';
67
68
  import { loadSystemEntities, findEntityViolations, describeEntityFinding } from './lib/entity-check.js';
@@ -164,7 +165,8 @@ async function fetchOrDie(url, init, what) {
164
165
  return await fetch(url, init);
165
166
  }
166
167
  catch (err) {
167
- die(`${what} platform unreachable at ${url} (${err?.message ?? err})`);
168
+ const hint = localhostFamilyHint(url, err);
169
+ die(`${what} — platform unreachable at ${url} (${err?.message ?? err})${hint ? `\n ${hint}` : ''}`);
168
170
  }
169
171
  }
170
172
  /** One consistent explanation for auth failures on admin reads. A 401 can also
@@ -618,6 +620,54 @@ async function notifyIfKbStale(flags) {
618
620
  }
619
621
  catch { /* a KB check must never break the CLI */ }
620
622
  }
623
+ /**
624
+ * Nudge (to stderr) when the platform has memos this workspace has not read — a reply to
625
+ * a `octwin feedback` report, or a notice we published (new capability, deprecation,
626
+ * breaking change).
627
+ *
628
+ * The sibling of `notifyIfKbStale`, and the same three decisions apply for the same
629
+ * reasons: it rides commands that ALREADY hit the platform so it costs one tiny GET on
630
+ * top of networked work; it never throws, because observing must not break a command;
631
+ * and it is **deliberately not TTY-gated**, because the primary reader of this CLI is an
632
+ * authoring agent whose output is piped — the one reader that cannot notice an unread
633
+ * reply on its own would otherwise be the only one never told.
634
+ *
635
+ * WHY THIS EXISTS AT ALL. `octwin feedback` was one-way. Measured 2026-08-18: four
636
+ * reports sat unread for nine days, and one author hardcoded English across 16 flows to
637
+ * work around a bug that had been fixed two days earlier, because nothing could tell
638
+ * them. A nudge that does not name the command to run does not motivate an agent, so the
639
+ * line always ends in `octwin memos`.
640
+ *
641
+ * Unlike the KB check there is NO local state to compare: read state is per-tenant on the
642
+ * platform, so an agent on a fresh machine or in a fresh container still learns about an
643
+ * unread memo. That is the whole reason it is not a local marker file.
644
+ */
645
+ async function notifyIfMemosWaiting(flags) {
646
+ try {
647
+ const t = resolveTargetOrNull(flags);
648
+ if (!t)
649
+ return;
650
+ const { status, json } = await apiGet(`${t.url}/api/self/p/memos?meta=1`, t);
651
+ // Silent on ANY failure — an older platform has no such route, and a nudge is never
652
+ // worth a diagnostic of its own. Unlike the KB poll there is no scope to explain: the
653
+ // route is gated on tenant access precisely so every token can answer it.
654
+ if (status !== 200 || !json || typeof json !== 'object')
655
+ return;
656
+ const unread = Number(json.unread ?? 0);
657
+ if (!Number.isFinite(unread) || unread <= 0)
658
+ return;
659
+ const actionable = Number(json.unread_actionable ?? 0);
660
+ const what = unread === 1 ? '1 memo' : `${unread} memos`;
661
+ const tail = actionable > 0 ? ` (${actionable} needing action)` : '';
662
+ console.error(`\n✉ ${what} from the platform${tail} — read them: octwin memos`);
663
+ if (actionable > 0) {
664
+ // Said separately, because the whole point of the severity axis is that an agent
665
+ // mid-build should stop and read rather than finish first.
666
+ console.error(' One or more may change what you are building — read before continuing.');
667
+ }
668
+ }
669
+ catch { /* a memo check must never break the CLI */ }
670
+ }
621
671
  /** Which commands already made a platform call, so the trailing KB-drift poll
622
672
  * rides on existing network work (never on offline `validate` / `init`;
623
673
  * `platform-kb` refreshes the reference itself, so it needs no nudge). */
@@ -630,6 +680,9 @@ function commandTouchesPlatform(command, flags) {
630
680
  case 'chat':
631
681
  case 'media':
632
682
  case 'pull':
683
+ // `memos` is networked but needs NO memo nudge — it just read them. It still gets the
684
+ // KB drift check, which is a different question.
685
+ case 'memos': return true;
633
686
  case 'records':
634
687
  case 'work':
635
688
  case 'logs':
@@ -748,7 +801,7 @@ async function cmdValidate(flags) {
748
801
  const withLines = (fs) => fs.map(f => ({ ...f, line: files[f.file] ? yamlLineOf(files[f.file], f.path) : null }));
749
802
  const render = loadAllowedRenderKeys(packDir);
750
803
  if (render.keys) {
751
- const findings = withLines(yamlDocs().flatMap(([p, doc]) => findRenderKeyViolations(doc, p, render.keys)));
804
+ const findings = withLines(yamlDocs().flatMap(([p, doc]) => findRenderKeyViolations(doc, p, render.keys, render.nested)));
752
805
  if (findings.length) {
753
806
  console.error(`✗ ${findings.length} render-intent field error${findings.length === 1 ? '' : 's'}:`);
754
807
  for (const f of findings)
@@ -896,10 +949,16 @@ async function cmdValidate(flags) {
896
949
  const t = resolveTarget(flags);
897
950
  const { url } = t;
898
951
  console.log(`→ Validating against ${targetLabel(t)} @ ${url} …`);
952
+ // `--strict-primitives` (opt-in): the server additionally type-checks LITERAL
953
+ // `args:` values against each primitive's per-key input schema. Expression
954
+ // strings are always exempt — this judges only plain YAML scalars.
899
955
  const res = await fetchOrDie(`${url}/api/self/p/packs/validate`, {
900
956
  method: 'POST',
901
957
  headers: { 'content-type': 'application/json', ...authHeaders(t) },
902
- body: JSON.stringify({ files, blobs }),
958
+ body: JSON.stringify({
959
+ files, blobs,
960
+ ...(flags['strict-primitives'] === true ? { strict_primitives: true } : {}),
961
+ }),
903
962
  }, 'remote validate');
904
963
  const text = await res.text();
905
964
  let json;
@@ -1257,13 +1316,13 @@ async function cmdSeed(flags) {
1257
1316
  const { terminal: final, stepErrors } = await readDeployProgress(res.body);
1258
1317
  if (!final || final.stage === 'error')
1259
1318
  die(`seed failed${final?.message ? `: ${final.message}` : ' (stream ended early)'}`);
1260
- console.log(`
1319
+ console.log(`
1261
1320
  ✓ ${final.message ?? 'seed complete'}`);
1262
1321
  printSeedCounts(final.result?.seeded);
1263
1322
  if (stepErrors.length) {
1264
1323
  // A kind failed but the rest ran — the reconcile softens each step. Say which,
1265
1324
  // and exit non-zero so a scripted `seed && chat` doesn't read as clean.
1266
- console.error(`
1325
+ console.error(`
1267
1326
  ⚠ ${stepErrors.length} step${stepErrors.length === 1 ? '' : 's'} failed — data may be incomplete:`);
1268
1327
  for (const e of stepErrors)
1269
1328
  console.error(` • ${e}`);
@@ -1345,6 +1404,33 @@ async function cmdDeploy(flags) {
1345
1404
  }
1346
1405
  printDeploySuccess(id, version, t, json);
1347
1406
  }
1407
+ /**
1408
+ * The QUALIFIED pack id (`<owner>.<name>`) for a manifest's bare name.
1409
+ *
1410
+ * A manifest declares a bare name and cannot legally declare anything else — the platform
1411
+ * and this CLI both reject a `.` in it, because the owner segment is attached at publish
1412
+ * time from the authenticated publisher. Every route that takes a pack id in its PATH
1413
+ * requires the qualified form, so a command holding only a manifest has to ask who it is.
1414
+ *
1415
+ * `whoami`'s `tenant_slug` IS the owner segment. Without this, `octwin status` sent the bare
1416
+ * name, `asPackId` rejected it, and the route's 404 was reported as "not installed" — so the
1417
+ * command was structurally broken for every pack from the day ids gained owners
1418
+ * (2026-08-01) until this was fixed, while the KB still taught it as step 4 of the loop.
1419
+ */
1420
+ async function qualifiedPackId(t, bareName) {
1421
+ if (bareName.includes('.'))
1422
+ return bareName; // already qualified (explicit override)
1423
+ const res = await fetchOrDie(`${t.url}/api/self/t/whoami`, { headers: authHeaders(t) }, 'workspace lookup');
1424
+ if (!res.ok) {
1425
+ printAuthHint(res.status, t.url);
1426
+ die(`could not resolve your workspace to qualify the pack id (HTTP ${res.status}) — pass the full '<owner>.${bareName}' instead`);
1427
+ }
1428
+ const j = await res.json();
1429
+ if (typeof j.tenant_slug !== 'string' || !j.tenant_slug) {
1430
+ die(`the platform did not report a workspace slug — pass the full '<owner>.${bareName}' instead`);
1431
+ }
1432
+ return `${j.tenant_slug}.${bareName}`;
1433
+ }
1348
1434
  async function cmdStatus(flags) {
1349
1435
  const packDir = resolve(flags.dir ?? '.');
1350
1436
  const t = resolveTarget(flags);
@@ -1355,8 +1441,11 @@ async function cmdStatus(flags) {
1355
1441
  const doc = parseYaml(readFileSync(manifestPath, 'utf8'));
1356
1442
  if (typeof doc?.id !== 'string')
1357
1443
  die('manifest.yaml must declare a string `id`');
1358
- const id = doc.id;
1444
+ const bare = doc.id;
1359
1445
  const localVersion = typeof doc?.version === 'string' ? doc.version : '?';
1446
+ // An explicit `octwin status <packId>` wins, so an author who already knows the qualified
1447
+ // id (from `octwin agents` / `octwin projects`) can skip the lookup.
1448
+ const id = await qualifiedPackId(t, flags._[1] ?? bare);
1360
1449
  console.log(`→ Checking ${id}@${localVersion} on ${targetLabel(t)} @ ${url} …`);
1361
1450
  const res = await fetchOrDie(`${url}/api/self/p/packs/${id}/runtime`, {
1362
1451
  headers: authHeaders(t),
@@ -1370,8 +1459,16 @@ async function cmdStatus(flags) {
1370
1459
  json = text;
1371
1460
  }
1372
1461
  if (!res.ok) {
1373
- if (res.status === 404)
1374
- die(`'${id}' is not installed on ${targetLabel(t)} yet run \`octwin deploy\` first`);
1462
+ // A 404 here means one of four different things, and collapsing them into "not
1463
+ // installed" sent one author chasing a deploy that had already succeeded. The route
1464
+ // says which; relay it and only add the deploy hint to the case it fits.
1465
+ if (res.status === 404) {
1466
+ const why = typeof json === 'object' && json !== null && typeof json.error === 'string' ? json.error : text;
1467
+ const notInstalled = /no active install|not installed/i.test(why);
1468
+ die(notInstalled
1469
+ ? `${why}\n Run \`octwin deploy\` first.`
1470
+ : `status check failed (HTTP 404) — ${why}`);
1471
+ }
1375
1472
  console.error(`✗ status check failed (HTTP ${res.status})`);
1376
1473
  printAuthHint(res.status, url);
1377
1474
  console.error(typeof json === 'string' ? json : JSON.stringify(json, null, 2));
@@ -2012,6 +2109,60 @@ async function cmdFeedback(flags) {
2012
2109
  console.log(' ⓘ no local capability reference found, so the report carries no KB version.');
2013
2110
  console.log(' Pull it before your next session: octwin platform-kb');
2014
2111
  }
2112
+ console.log(' A reply arrives as a memo — this CLI will tell you when one is waiting.');
2113
+ }
2114
+ /**
2115
+ * `octwin memos [--json] [--all]` — read what the platform has told you.
2116
+ *
2117
+ * The other half of `octwin feedback`. Two kinds arrive here: a REPLY to a report you
2118
+ * sent, and a NOTICE we published to every author (a new capability, a deprecation, a
2119
+ * breaking change). Both are markdown, printed in full — this is a channel for reading,
2120
+ * not a list to page through, so there is no `show <id>`.
2121
+ *
2122
+ * Reading ACKS: the default prints unread memos and marks them read, so the nudge stops.
2123
+ * `--all` re-reads history and acks nothing, which is also the escape hatch if a piped
2124
+ * `--json` consumer crashed halfway through.
2125
+ */
2126
+ async function cmdMemos(flags) {
2127
+ const t = resolveTarget(flags);
2128
+ const all = flags.all === true;
2129
+ const { status, json } = await apiGet(`${t.url}/api/self/p/memos${all ? '?all=1' : ''}`, t);
2130
+ if (status !== 200) {
2131
+ die(`could not read memos (HTTP ${status})${errDetail(json)}${authFailureDetail(status, t.url)}`);
2132
+ }
2133
+ const rows = (json?.rows ?? []);
2134
+ if (flags.json === true) {
2135
+ console.log(JSON.stringify(rows, null, 2));
2136
+ return;
2137
+ }
2138
+ if (rows.length === 0) {
2139
+ console.log(all ? 'No memos yet.' : 'No unread memos. (History: octwin memos --all)');
2140
+ return;
2141
+ }
2142
+ const mark = (s) => s === 'breaking' ? '⛔ BREAKING' : s === 'action_required' ? '⚠ ACTION' : 'ⓘ';
2143
+ for (const m of rows) {
2144
+ const when = String(m.published_at ?? '').slice(0, 10);
2145
+ const about = m.pack_id ? ` · ${m.pack_id}` : '';
2146
+ const seen = m.read_at ? ' · (already read)' : '';
2147
+ console.log(`\n${'─'.repeat(72)}`);
2148
+ console.log(`${mark(m.severity)} ${m.title}`);
2149
+ console.log(`${m.kind === 'reply' ? 'reply to your report' : 'platform notice'} · ${when}${about}${seen}`);
2150
+ console.log(`${'─'.repeat(72)}\n`);
2151
+ console.log(m.body_md.trimEnd());
2152
+ }
2153
+ console.log();
2154
+ // Ack only what we just showed as unread, and only on the default path — `--all`
2155
+ // deliberately leaves read state alone so history can be re-read any number of times.
2156
+ if (!all) {
2157
+ const ids = rows.filter(m => !m.read_at).map(m => m.id);
2158
+ if (ids.length > 0) {
2159
+ const res = await apiSend('POST', `${t.url}/api/self/p/memos/read`, { ids }, t);
2160
+ // A failed ack is not a failed command — the author HAS read them. Say so rather
2161
+ // than dying after printing everything, and the nudge simply fires again.
2162
+ if (res.status !== 200)
2163
+ console.error(`ⓘ could not mark them read (HTTP ${res.status}) — they will be offered again.`);
2164
+ }
2165
+ }
2015
2166
  }
2016
2167
  /** `octwin logs [conversationId] [--as <handle>] [--json]` — list conversations
2017
2168
  * or show one's event timeline (full text + the renders each turn produced). */
@@ -2028,7 +2179,11 @@ async function cmdLogs(flags) {
2028
2179
  const { status, json } = await apiGet(`${base}/conversations?limit=50`, t);
2029
2180
  if (status !== 200)
2030
2181
  die(`could not read conversations (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
2031
- let convs = (json?.conversations ?? []);
2182
+ // `rows` the shared `makePage` envelope every other list command here reads. This one
2183
+ // read `json.conversations`, a noun the route stopped emitting on 2026-08-09, so `convs`
2184
+ // was ALWAYS empty and the command reported "No conversations yet" seconds after a chat
2185
+ // that had plainly worked. Two pack authors filed it as a replica-lag bug; nothing lagged.
2186
+ let convs = (json?.rows ?? []);
2032
2187
  if (asHandle)
2033
2188
  convs = convs.filter((c) => c.contact?.channel_contact_handle === asHandle);
2034
2189
  if (convs.length === 0) {
@@ -2426,29 +2581,32 @@ async function cmdChat(flags) {
2426
2581
  }
2427
2582
  console.log(`\n(same --as '${from}' continues this conversation — timeline: octwin logs --as ${from})`);
2428
2583
  }
2429
- /** `octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json]`
2584
+ /** `octwin media generate "<prompt>" [--out <file.png>] [--json]`
2430
2585
  * — AI-generate an image on the platform (needs a `media:generate`-scoped token),
2431
2586
  * store it as a public asset, and return its `MEDIA-` handle + serve URL. `--out`
2432
2587
  * downloads the bytes to a file (WhatsApp renders only `.png`/`.jpg`); the paired
2433
- * `octwin chat --media <file|id>` feeds it into a running media-collect flow. */
2588
+ * `octwin chat --media <file|id>` feeds it into a running media-collect flow.
2589
+ *
2590
+ * A `--size` flag was accepted until 2026-08-10. The platform never forwarded it
2591
+ * to the provider, and stored resolution is decided at ingest regardless, so it
2592
+ * only ever changed the (fabricated) width/height the command printed back. */
2434
2593
  async function cmdMedia(flags) {
2435
2594
  const sub = flags._[0];
2436
2595
  if (sub !== 'generate')
2437
- die('usage: octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json]');
2596
+ die('usage: octwin media generate "<prompt>" [--out <file.png>] [--json]');
2438
2597
  const t = resolveTarget(flags);
2439
2598
  const { url } = t;
2440
2599
  const prompt = flags._[1];
2441
2600
  if (!prompt)
2442
- die('usage: octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json]');
2601
+ die('usage: octwin media generate "<prompt>" [--out <file.png>] [--json]');
2443
2602
  const asJson = flags.json === true;
2444
- const size = typeof flags.size === 'string' ? flags.size : undefined;
2445
2603
  const out = typeof flags.out === 'string' ? flags.out : undefined;
2446
2604
  if (!asJson)
2447
2605
  console.log(`→ Generating an image on ${targetLabel(t)} @ ${url} …`);
2448
2606
  const res = await fetchOrDie(`${url}/api/self/p/media/generate`, {
2449
2607
  method: 'POST',
2450
2608
  headers: { 'content-type': 'application/json', ...authHeaders(t) },
2451
- body: JSON.stringify({ prompt, ...(size ? { size } : {}) }),
2609
+ body: JSON.stringify({ prompt }),
2452
2610
  }, 'media generate');
2453
2611
  const text = await res.text();
2454
2612
  if (!res.ok) {
@@ -2477,10 +2635,10 @@ async function cmdMedia(flags) {
2477
2635
  }
2478
2636
  }
2479
2637
  if (asJson) {
2480
- console.log(JSON.stringify({ media_id: r.media_id, url: absUrl, mime: r.mime, width: r.width, height: r.height, bytes: r.bytes }));
2638
+ console.log(JSON.stringify({ media_id: r.media_id, url: absUrl, mime: r.mime, bytes: r.bytes }));
2481
2639
  return;
2482
2640
  }
2483
- console.log(`✓ Generated ${r.media_ref} (${r.width}×${r.height}, ${r.mime}, ${r.bytes} bytes)`);
2641
+ console.log(`✓ Generated ${r.media_ref} (${r.mime}, ${r.bytes} bytes)`);
2484
2642
  console.log(` id: ${r.media_id}`);
2485
2643
  console.log(` url: ${absUrl}`);
2486
2644
  if (out)
@@ -3514,7 +3672,7 @@ async function cmdSchedulingWrite(flags) {
3514
3672
  console.log(JSON.stringify(json, null, 2));
3515
3673
  return;
3516
3674
  }
3517
- if (json?.has_scheduling === false) {
3675
+ if (json?.has_data === false) {
3518
3676
  console.log('This pack declares no scheduling.');
3519
3677
  return;
3520
3678
  }
@@ -3589,7 +3747,7 @@ async function cmdSchedulingWrite(flags) {
3589
3747
  const { status, json } = await apiSend('POST', `${base}/availability/${path}`, body, t);
3590
3748
  if (status !== 201 && status !== 200)
3591
3749
  writeFail(`add the ${noun}`, status, json, url);
3592
- if (json?.has_scheduling === false)
3750
+ if (json?.has_data === false)
3593
3751
  die('this pack declares no scheduling');
3594
3752
  const created = json?.rule ?? json?.exception ?? {};
3595
3753
  console.log(`✓ ${noun[0].toUpperCase()}${noun.slice(1)} added — ${created.id ?? '(no id returned)'}`);
@@ -3610,30 +3768,42 @@ async function cmdScheduling(flags) {
3610
3768
  if (flags.slots === true)
3611
3769
  die('usage: octwin scheduling --slots <resourceRecordId> (the record id of a bookable resource)');
3612
3770
  if (resourceId) {
3613
- const q = new URLSearchParams({ include_booked: '1' }); // full occupancy, as the operator preview does
3614
- if (typeof flags.from === 'string')
3615
- q.set('from', flags.from);
3616
- if (typeof flags.days === 'string')
3617
- q.set('days', flags.days); // server-clamped to 1–31
3771
+ // The range is `from`/`to`; `--days` is kept as the ergonomic flag and converted
3772
+ // here, since a CLI user thinks in "the next 7 days".
3773
+ const from = typeof flags.from === 'string' ? flags.from : new Date().toISOString().slice(0, 10);
3774
+ const days = typeof flags.days === 'string' ? Math.max(Number(flags.days) || 1, 1) : 7;
3775
+ const to = new Date(Date.parse(`${from}T00:00:00Z`) + (days - 1) * 86_400_000).toISOString().slice(0, 10);
3776
+ const q = new URLSearchParams({ include_booked: '1', from, to }); // full occupancy, as the operator preview does
3777
+ q.append('resource_id', resourceId);
3618
3778
  if (!asJson)
3619
3779
  console.log(`→ Computing slots for resource ${resourceId} in ${targetLabel(t)} …`);
3620
- const { status, json } = await apiGet(`${base}/resources/${encodeURIComponent(resourceId)}/slots?${q.toString()}`, t);
3621
- if (status === 404)
3622
- die(`resource '${resourceId}' not found (pass an XRM record id — \`octwin records <entity>\` lists them)`);
3780
+ const { status, json } = await apiGet(`${base}/slots?${q.toString()}`, t);
3623
3781
  if (status === 400)
3624
- die(`that record isn't a bookable resource${errDetail(json)}`);
3782
+ die(`could not compute slots${errDetail(json)}`);
3625
3783
  if (status !== 200)
3626
3784
  die(`could not compute slots (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
3627
3785
  if (asJson) {
3628
3786
  console.log(JSON.stringify(json, null, 2));
3629
3787
  return;
3630
3788
  }
3631
- if (json?.has_scheduling === false) {
3789
+ if (json?.has_data === false) {
3632
3790
  console.log('This pack declares no `scheduling.yaml` — nothing to schedule.');
3633
3791
  return;
3634
3792
  }
3635
- const slots = (json?.slots ?? []);
3636
- console.log(`Slots for ${resourceId} (timezone ${json?.timezone ?? '?'}): ${slots.length}`);
3793
+ // A resource the server could not use comes back named, with a reason, rather
3794
+ // than as an HTTP status one call may carry several resources.
3795
+ const bad = (json?.unresolved ?? []).find((u) => u.resource_id === resourceId);
3796
+ if (bad?.reason === 'not_found') {
3797
+ die(`resource '${resourceId}' not found (pass an XRM record id — \`octwin records <entity>\` lists them)`);
3798
+ }
3799
+ if (bad?.reason === 'not_bookable')
3800
+ die(`that record isn't a bookable resource`);
3801
+ const res = (json?.resources ?? [])[0];
3802
+ const slots = (res?.slots ?? []);
3803
+ console.log(`Slots for ${resourceId} (timezone ${res?.timezone ?? '?'} — from ${res?.timezone_source ?? '?'}): ${slots.length}`);
3804
+ if (res?.timezone_source === 'pack_default' && json?.pack_timezone_declared === false) {
3805
+ console.log(' ⚠ The pack declares no `timezone:`, so these are UTC — a clock nobody chose.');
3806
+ }
3637
3807
  if (slots.length === 0)
3638
3808
  console.log(' (none — no availability rules cover this window; `octwin deploy --seed` seeds the demo rules)');
3639
3809
  for (const s of slots) {
@@ -3650,7 +3820,7 @@ async function cmdScheduling(flags) {
3650
3820
  console.log(JSON.stringify(json, null, 2));
3651
3821
  return;
3652
3822
  }
3653
- if (json?.has_scheduling === false) {
3823
+ if (json?.has_data === false) {
3654
3824
  console.log('This pack declares no `scheduling.yaml` — nothing to schedule.');
3655
3825
  return;
3656
3826
  }
@@ -3662,260 +3832,275 @@ async function cmdScheduling(flags) {
3662
3832
  console.log('\nSlots for one resource: octwin scheduling --slots <resourceRecordId> (ids: octwin records <entity>)');
3663
3833
  }
3664
3834
  function help() {
3665
- console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
3666
-
3667
- octwin --version # print the CLI version (+ any upgrade notice)
3668
- octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
3669
- octwin validate [--dir .] [--remote] [--require-kb] # --remote runs the platform's FULL schema check + lint (all errors at once)
3670
- octwin login --url <platformUrl> --token oct_… # a deploy token from the console
3671
- octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
3672
- octwin projects [--archived] [--json] # the --project slugs this token can name
3673
- octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
3674
- octwin status [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
3675
- octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
3676
- octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
3677
- octwin work [recordId] [--queues] [--json] # inspect the work inbox (worked records) + timelines
3678
- octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
3679
- octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
3680
- octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
3681
- octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
3682
- octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
3683
- octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
3684
- octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
3685
- octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
3686
- octwin platform-kb [pull] [--if-stale|--check] [--dir .] [--url <url>] # no token needed
3687
- octwin test [--dir .] # = validate --remote (the full platform check)
3688
- octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
3689
-
3690
- Writes — exercise the state your pack creates (each needs the matching :write scope):
3691
- octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…"
3692
- octwin records tasks | task complete <taskId> [--outcome done|cancelled]
3693
- octwin work assign <id> --to user:<uuid>|none | note <id> "…" | stage <id> --to <stage>
3694
- octwin work decide <id> --action <a> [--param k=v] [--dry-run] # --dry-run previews, commits nothing
3695
- octwin orders transition <ref> --to <status> | refund <ref> --force
3696
- octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
3697
- octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
3698
- octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
3699
-
3700
- Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
3701
- \`octwin chat --as <h>\` calls continue the same conversation; press a rendered
3702
- button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
3703
- Get a deploy token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
3704
- octwin platform-kb pullwrites the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
3705
- Config (deploy): flags > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login (\`octwin login\` sets the default target).
3835
+ console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
3836
+
3837
+ octwin --version # print the CLI version (+ any upgrade notice)
3838
+ octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
3839
+ octwin validate [--dir .] [--remote] [--require-kb] # --remote runs the platform's FULL schema check + lint (all errors at once)
3840
+ octwin login --url <platformUrl> --token oct_… # a deploy token from the console
3841
+ octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
3842
+ octwin projects [--archived] [--json] # the --project slugs this token can name
3843
+ octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
3844
+ octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
3845
+ octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
3846
+ octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
3847
+ octwin work [recordId] [--queues] [--json] # inspect the work inbox (worked records) + timelines
3848
+ octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
3849
+ octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
3850
+ octwin media generate "<prompt>" [--out <file.png>] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
3851
+ octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
3852
+ octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
3853
+ octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
3854
+ octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
3855
+ octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
3856
+ octwin platform-kb [pull] [--if-stale|--check] [--dir .] [--url <url>] # no token needed
3857
+ octwin test [--dir .] # = validate --remote (the full platform check)
3858
+ octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
3859
+ octwin memos [--all] [--json] # read the platform's replies + notices (a reply to your feedback lands here)
3860
+
3861
+ Writes exercise the state your pack creates (each needs the matching :write scope):
3862
+ octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…"
3863
+ octwin records tasks | task complete <taskId> [--outcome done|cancelled]
3864
+ octwin work assign <id> --to user:<uuid>|none | note <id> "…" | stage <id> --to <stage>
3865
+ octwin work decide <id> --action <a> [--param k=v] [--dry-run] # --dry-run previews, commits nothing
3866
+ octwin orders transition <ref> --to <status> | refund <ref> --force
3867
+ octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
3868
+ octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
3869
+ octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
3870
+
3871
+ Multi-turn: the platform keeps ONE open conversation per --as handle consecutive
3872
+ \`octwin chat --as <h>\` calls continue the same conversation; press a rendered
3873
+ button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
3874
+ Get a deploy token: console your workspace Settings API tokens → Generate (tick records:read to inspect data).
3875
+ octwin platform-kb pull writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
3876
+ Config (deploy): flags > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login (\`octwin login\` sets the default target).
3706
3877
  Per-command usage: octwin <command> --help`);
3707
3878
  }
3708
3879
  /** Per-subcommand usage — printed for `octwin <cmd> --help|-h` BEFORE any
3709
3880
  * network/auth work (a --help that 401s is worse than no help at all). */
3710
3881
  const COMMAND_HELP = {
3711
- init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
3882
+ init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
3712
3883
  Scaffold a pure-YAML starter pack into <dir>.`,
3713
- validate: `octwin validate [--dir .] [--remote] [--require-kb]
3714
- Offline structural check, plus two checks driven by the pulled capability
3715
- reference (render-intent fields, primitive arguments). Those two SKIP when the
3716
- reference is missing — the run says so, and --require-kb turns the skip into a
3717
- failure for CI. --remote additionally runs the platform's FULL manifest +
3718
- flow-DSL validation and its flow lint (all errors at once) — same check as deploy.`,
3719
- login: `octwin login --url <platformUrl> --token oct_…
3720
- Save a deploy token (console Settings API tokens) for that platform url,
3721
- make that url the DEFAULT deploy target for every later command, and echo the
3884
+ validate: `octwin validate [--dir .] [--remote] [--require-kb] [--strict-primitives]
3885
+ Offline structural check, plus two checks driven by the pulled capability
3886
+ reference (render-intent fields, primitive arguments). Those two SKIP when the
3887
+ reference is missing — the run says so, and --require-kb turns the skip into a
3888
+ failure for CI. --remote additionally runs the platform's FULL manifest +
3889
+ flow-DSL validation and its flow lint (all errors at once) — same check as deploy.
3890
+ --strict-primitives (with --remote) additionally type-checks LITERAL args:
3891
+ values against each primitive's declared input schema; expression strings
3892
+ ('$found.id', '{$t(…)}') are always exempt.`,
3893
+ login: `octwin login --url <platformUrl> --token oct_…
3894
+ Save a deploy token (console → Settings → API tokens) for that platform url,
3895
+ make that url the DEFAULT deploy target for every later command, and echo the
3722
3896
  workspace + project pin + scopes the token reaches.`,
3723
- whoami: `octwin whoami [--url <url>] [--tenant <slug>]
3897
+ whoami: `octwin whoami [--url <url>] [--tenant <slug>]
3724
3898
  Verify the resolved token authenticates against the tenant.`,
3725
- projects: `octwin projects [--archived] [--json]
3726
- List the workspace's projects — the slugs every --project flag takes, with the
3727
- plan's project cap. --archived includes archived ones. A pack:deploy token
3728
- reaches this (it names a project in every other command).
3729
-
3730
- octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
3731
- Create a project. The URL slug is derived from the name unless --slug pins one.
3732
- --pack installs an ALREADY-published pack; the usual next step is instead
3733
- \`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
3734
-
3735
- octwin projects rm <slug> [--yes]
3736
- HARD delete — the project and everything cascading from it (conversations,
3737
- contacts, records, installs). No undo, and not the same as archiving.
3738
- WITHOUT --yes it only previews what would be destroyed, so the dry run is the
3739
- default. Together these make a disposable end-to-end environment:
3740
- octwin projects create "Scratch" && octwin deploy --project scratch --seed
3741
- octwin chat "hi" --project scratch
3742
- octwin projects rm scratch --yes
3899
+ projects: `octwin projects [--archived] [--json]
3900
+ List the workspace's projects — the slugs every --project flag takes, with the
3901
+ plan's project cap. --archived includes archived ones. A pack:deploy token
3902
+ reaches this (it names a project in every other command).
3903
+
3904
+ octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
3905
+ Create a project. The URL slug is derived from the name unless --slug pins one.
3906
+ --pack installs an ALREADY-published pack; the usual next step is instead
3907
+ \`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
3908
+
3909
+ octwin projects rm <slug> [--yes]
3910
+ HARD delete — the project and everything cascading from it (conversations,
3911
+ contacts, records, installs). No undo, and not the same as archiving.
3912
+ WITHOUT --yes it only previews what would be destroyed, so the dry run is the
3913
+ default. Together these make a disposable end-to-end environment:
3914
+ octwin projects create "Scratch" && octwin deploy --project scratch --seed
3915
+ octwin chat "hi" --project scratch
3916
+ octwin projects rm scratch --yes
3743
3917
  Both verbs need the \`projects:write\` scope — a pack:deploy token does NOT confer it.`,
3744
- deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
3745
- Upload the pack bundle, validate server-side, install onto the project.
3918
+ deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
3919
+ Upload the pack bundle, validate server-side, install onto the project.
3746
3920
  --seed additionally applies the pack's demo seed (streams progress).`,
3747
- seed: `octwin seed [--pack <packId>]
3748
- Apply the pack's demo/reference data to the project it is installed on, without
3749
- redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
3750
- and the demo operator topology. Reports what each kind produced.
3751
- Idempotent and safe to re-run — records upsert, and existing media is REUSED rather
3752
- than regenerated, so a second pass costs nothing. --pack is only needed when a
3921
+ seed: `octwin seed [--pack <packId>]
3922
+ Apply the pack's demo/reference data to the project it is installed on, without
3923
+ redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
3924
+ and the demo operator topology. Reports what each kind produced.
3925
+ Idempotent and safe to re-run — records upsert, and existing media is REUSED rather
3926
+ than regenerated, so a second pass costs nothing. --pack is only needed when a
3753
3927
  project somehow runs more than one.`,
3754
- status: `octwin status [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
3755
- Show installed vs live version + the flow list for this pack.`,
3756
- records: `octwin records [entity] [id] [--limit 50] [--offset n]
3757
- Inspect the pack's XRM data. No args = list entities. Worked records (cases,
3758
- tickets, anything routed to a queue) read best through \`octwin work\`.
3759
-
3760
- WRITES (need \`records:write\`; every one is re-checked by RBAC on the record):
3761
- octwin records create <entity> --set field=value [--set …] [--stage s] [--contact <id>]
3762
- octwin records patch <recordId> --entity <entity> --set field=value
3763
- octwin records stage <recordId> --to <stage> [--note "..."]
3764
- octwin records note <recordId> "the note text"
3765
- octwin records tasks # open follow-up tasks (\`tasks\` plan feature)
3766
- octwin records task complete <taskId> [--outcome done|cancelled] [--note "..."]
3767
-
3768
- --set coerces JSON scalars: \`--set rating=4.5\` sends a number, \`--set x=null\`
3769
- sends null. Use --fields-json '{"a":{"b":1}}' for anything nested.
3770
- \`patch\` needs --entity even though it has an id: the route resolves the field
3771
- validator from it. A leading \`create/patch/stage/note/tasks/task\` is read as a
3928
+ status: `octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
3929
+ Show installed vs live version + the flow list for this pack.
3930
+ The pack id is read from manifest.yaml and QUALIFIED with your workspace slug
3931
+ (a manifest declares a bare name; the owner is attached when you publish). Pass
3932
+ <packId> explicitly to skip that lookup \`octwin agents\` and \`octwin projects\`
3933
+ both print the qualified form.`,
3934
+ records: `octwin records [entity] [id] [--limit 50] [--offset n]
3935
+ Inspect the pack's XRM data. No args = list entities. Worked records (cases,
3936
+ tickets, anything routed to a queue) read best through \`octwin work\`.
3937
+
3938
+ WRITES (need \`records:write\`; every one is re-checked by RBAC on the record):
3939
+ octwin records create <entity> --set field=value [--set …] [--stage s] [--contact <id>]
3940
+ octwin records patch <recordId> --entity <entity> --set field=value
3941
+ octwin records stage <recordId> --to <stage> [--note "..."]
3942
+ octwin records note <recordId> "the note text"
3943
+ octwin records tasks # open follow-up tasks (\`tasks\` plan feature)
3944
+ octwin records task complete <taskId> [--outcome done|cancelled] [--note "..."]
3945
+
3946
+ --set coerces JSON scalars: \`--set rating=4.5\` sends a number, \`--set x=null\`
3947
+ sends null. Use --fields-json '{"a":{"b":1}}' for anything nested.
3948
+ \`patch\` needs --entity even though it has an id: the route resolves the field
3949
+ validator from it. A leading \`create/patch/stage/note/tasks/task\` is read as a
3772
3950
  VERB — to list an entity actually named one of those, use \`--entity <name>\`.`,
3773
- work: `octwin work [recordId] [--queues] [--limit 50] [--offset n] [--json]
3774
- Inspect the work inbox — every entity the pack declares worked (cases, orders
3775
- needing review, applications, …): the inbox, one item + its timeline
3776
- (+ applicable actions), or --queues for queue keys + open counts.
3777
-
3778
- WRITES (need \`work:write\`; \`stage\` needs \`records:write\`):
3779
- octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none
3780
- octwin work note <recordId> "the note text"
3781
- octwin work stage <recordId> --to <stage> [--note "..."]
3782
- octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]
3783
-
3784
- \`decide\` applies one of the entity's declared operator actions — \`octwin work <id>\`
3785
- lists them with their params. --dry-run previews the customer-facing copy and the
3786
- resulting stage WITHOUT committing (that route needs only \`work:read\`).
3951
+ work: `octwin work [recordId] [--queues] [--limit 50] [--offset n] [--json]
3952
+ Inspect the work inbox — every entity the pack declares worked (cases, orders
3953
+ needing review, applications, …): the inbox, one item + its timeline
3954
+ (+ applicable actions), or --queues for queue keys + open counts.
3955
+
3956
+ WRITES (need \`work:write\`; \`stage\` needs \`records:write\`):
3957
+ octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none
3958
+ octwin work note <recordId> "the note text"
3959
+ octwin work stage <recordId> --to <stage> [--note "..."]
3960
+ octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]
3961
+
3962
+ \`decide\` applies one of the entity's declared operator actions — \`octwin work <id>\`
3963
+ lists them with their params. --dry-run previews the customer-facing copy and the
3964
+ resulting stage WITHOUT committing (that route needs only \`work:read\`).
3787
3965
  \`stage\` is the XRM records verb (one transition spelling platform-wide).`,
3788
- logs: `octwin logs [conversationId] [--as <handle>] [--json]
3789
- No id = recent conversations (handle, status, last activity; --as filters).
3790
- With id = the full event timeline including what each turn rendered.
3966
+ logs: `octwin logs [conversationId] [--as <handle>] [--json]
3967
+ No id = recent conversations (handle, status, last activity; --as filters).
3968
+ With id = the full event timeline including what each turn rendered.
3791
3969
  --json = raw events (verbatim payloads).`,
3792
- pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
3793
- Write a DEPLOYED pack's source back to disk — the inverse of deploy.
3794
- A pack pushed with 'octwin deploy' lives on the platform as an artifact the
3795
- runtime serves but nothing hands back, so its only source copy is the machine
3796
- that pushed it. Pull it, fix it, redeploy it.
3797
- Defaults to the version installed on the target project; --version overrides.
3798
- --dir defaults to ./<packId>; a non-empty dir needs --force.
3799
- The pulled dir redeploys where it came from — the target is your saved login.
3970
+ pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
3971
+ Write a DEPLOYED pack's source back to disk — the inverse of deploy.
3972
+ A pack pushed with 'octwin deploy' lives on the platform as an artifact the
3973
+ runtime serves but nothing hands back, so its only source copy is the machine
3974
+ that pushed it. Pull it, fix it, redeploy it.
3975
+ Defaults to the version installed on the target project; --version overrides.
3976
+ --dir defaults to ./<packId>; a non-empty dir needs --force.
3977
+ The pulled dir redeploys where it came from — the target is your saved login.
3800
3978
  You may pull a pack your tenant OWNS (deployed); an operator token pulls any.`,
3801
- chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
3802
- octwin chat --script <file> [--as <handle>] [--json]
3803
- Drive ONE turn through the dev web channel and print every render with its
3804
- tap ids. Same --as handle = same conversation (multi-turn works).
3805
- --tap presses a rendered button/list row instead of sending text.
3806
- --media uploads a local file (or a media id from 'media generate --json') as
3807
- an image/document/audio inbound — any "message" rides as its caption; feeds a
3808
- running media-collect flow (e.g. activate-app).
3809
- --json dumps the raw SSE envelopes for the turn.
3810
-
3811
- --script drives a WHOLE conversation from a file, ONE TURN PER LINE, in one
3812
- process over one connection — waiting for each turn to settle before sending
3813
- the next. Use this for any multi-step flow: chaining shell invocations races
3814
- the agent loop, because a turn ends on a quiet gap that can arrive while the
3815
- server is still working (the symptom is placeholder-filled fields or a second
3816
- workflow run). Blank lines and # comments are skipped:
3817
-
3818
- # book an appointment end to end
3819
- احجز موعد
3820
- tap:t:invoke:book-appointment:doctor_id=D1
3821
- media:./licence.jpg | here is my licence
3979
+ chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
3980
+ octwin chat --script <file> [--as <handle>] [--json]
3981
+ Drive ONE turn through the dev web channel and print every render with its
3982
+ tap ids. Same --as handle = same conversation (multi-turn works).
3983
+ --tap presses a rendered button/list row instead of sending text.
3984
+ --media uploads a local file (or a media id from 'media generate --json') as
3985
+ an image/document/audio inbound — any "message" rides as its caption; feeds a
3986
+ running media-collect flow (e.g. activate-app).
3987
+ --json dumps the raw SSE envelopes for the turn.
3988
+
3989
+ --script drives a WHOLE conversation from a file, ONE TURN PER LINE, in one
3990
+ process over one connection — waiting for each turn to settle before sending
3991
+ the next. Use this for any multi-step flow: chaining shell invocations races
3992
+ the agent loop, because a turn ends on a quiet gap that can arrive while the
3993
+ server is still working (the symptom is placeholder-filled fields or a second
3994
+ workflow run). Blank lines and # comments are skipped:
3995
+
3996
+ # book an appointment end to end
3997
+ احجز موعد
3998
+ tap:t:invoke:book-appointment:doctor_id=D1
3999
+ media:./licence.jpg | here is my licence
3822
4000
  tap:t:resume:book-appointment:run_id=R1;_ctl_approved=true`,
3823
- media: `octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json]
3824
- AI-generate an image (needs a media:generate-scoped token), store it as a
3825
- public asset, and print its MEDIA- handle + serve URL. --out downloads the
3826
- bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
3827
- width, height, bytes }. Pair with 'octwin chat --media' to drive media flows.`,
3828
- agents: `octwin agents [packId::agentId] [--prompt] [--json]
3829
- No args = the roster with each agent's EFFECTIVE model and which layer set it.
3830
- With an agent = every governed setting (model / memory.last_messages /
3831
- working_memory) plus the layer that won — an operator PLATFORM default can
3832
- override what your manifest declares, and this is where you see that.
3833
- --prompt = the exact system prompt the LLM sees for this project (pack
3834
- instructions + platform protocol + any project overlay). Needs agents:read.
3835
- The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
3836
-
3837
- WRITES (need \`agents:write\`):
3838
- octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
3839
- [--enable-tool <toolId>] [--disable-tool <toolId>]
3840
-
3841
- Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
3842
- so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
4001
+ media: `octwin media generate "<prompt>" [--out <file.png>] [--json]
4002
+ AI-generate an image (needs a media:generate-scoped token), store it as a
4003
+ public asset, and print its MEDIA- handle + serve URL. --out downloads the
4004
+ bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
4005
+ bytes }. Pair with 'octwin chat --media' to drive media flows.`,
4006
+ agents: `octwin agents [packId::agentId] [--prompt] [--json]
4007
+ No args = the roster with each agent's EFFECTIVE model and which layer set it.
4008
+ With an agent = every governed setting (model / memory.last_messages /
4009
+ working_memory) plus the layer that won — an operator PLATFORM default can
4010
+ override what your manifest declares, and this is where you see that.
4011
+ --prompt = the exact system prompt the LLM sees for this project (pack
4012
+ instructions + platform protocol + any project overlay). Needs agents:read.
4013
+ The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
4014
+
4015
+ WRITES (need \`agents:write\`):
4016
+ octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
4017
+ [--enable-tool <toolId>] [--disable-tool <toolId>]
4018
+
4019
+ Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
4020
+ so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
3843
4021
  ids refuses --model with a 403 — the platform default governs there.`,
3844
- orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
3845
- No args = the order list (#number, status/payment, total, contact). With a
3846
- reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
3847
- payment_ref, and the allowed status transitions. Needs orders:read + the
3848
- \`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
3849
- so \`pending\` on a gateway-less workspace is expected, not a bug.
3850
-
3851
- WRITES (need \`orders:write\`):
3852
- octwin orders transition <reference_id> --to <status>
3853
- octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
3854
-
3855
- Refund is irreversible and moves money, hence --force. The route answers 200 even
3856
- when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
3857
- on a refusal rather than reporting a refund that never happened. Only a payment in
4022
+ orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
4023
+ No args = the order list (#number, status/payment, total, contact). With a
4024
+ reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
4025
+ payment_ref, and the allowed status transitions. Needs orders:read + the
4026
+ \`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
4027
+ so \`pending\` on a gateway-less workspace is expected, not a bug.
4028
+
4029
+ WRITES (need \`orders:write\`):
4030
+ octwin orders transition <reference_id> --to <status>
4031
+ octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
4032
+
4033
+ Refund is irreversible and moves money, hence --force. The route answers 200 even
4034
+ when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
4035
+ on a refusal rather than reporting a refund that never happened. Only a payment in
3858
4036
  \`captured\` state can be refunded; \`payment_status\` is never settable directly.`,
3859
- analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
3860
- No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
3861
- With an entity = stage-by-stage conversion (default --funnel) over the last 30
3862
- days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
4037
+ analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
4038
+ No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
4039
+ With an entity = stage-by-stage conversion (default --funnel) over the last 30
4040
+ days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
3863
4041
  range-filtered). Needs records:read + a \`view\` grant on \`record.<entity>\`.`,
3864
- catalog: `octwin catalog [--readiness] [--json]
3865
- The commerce \`product\` records + price, availability, stock (null = not
3866
- inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
3867
- Graph checklist (LIVE Graph calls; needs a bound access token). Needs
3868
- catalog:read + the \`catalog\` plan feature.
3869
-
3870
- WRITES (need \`catalog:write\`):
3871
- octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
3872
- octwin catalog stock <retailerId> [--set-on-hand <n>]
3873
-
3874
- \`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
3875
- inventory-tracked (always sellable), which is different from 0. Lowering on_hand
3876
- below the units already reserved for open carts is refused. Creating/deleting
4042
+ catalog: `octwin catalog [--readiness] [--json]
4043
+ The commerce \`product\` records + price, availability, stock (null = not
4044
+ inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
4045
+ Graph checklist (LIVE Graph calls; needs a bound access token). Needs
4046
+ catalog:read + the \`catalog\` plan feature.
4047
+
4048
+ WRITES (need \`catalog:write\`):
4049
+ octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
4050
+ octwin catalog stock <retailerId> [--set-on-hand <n>]
4051
+
4052
+ \`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
4053
+ inventory-tracked (always sellable), which is different from 0. Lowering on_hand
4054
+ below the units already reserved for open carts is refused. Creating/deleting
3877
4055
  products and the Meta catalog binding/sync stay in the console.`,
3878
- scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
3879
- No args = the engine state (bookable resource types, upcoming slots, booked
3880
- seats). --slots <recordId> computes the slots for one bookable resource
3881
- (occupancy included; --days is clamped to 1-31 server-side) — the way to verify
3882
- the availability rules a \`deploy --seed\` created. Needs scheduling:read.
3883
-
3884
- RULES (list needs scheduling:read; add/rm need scheduling:write):
3885
- octwin scheduling rules --resource <resourceRecordId>
3886
- octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
3887
- [--slot-minutes 30] [--capacity 1]
3888
- octwin scheduling rule rm <ruleId>
3889
- octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
3890
- [--start 09:00 --end 13:00]
3891
- octwin scheduling exception rm <exceptionId>
3892
-
3893
- --dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
4056
+ scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
4057
+ No args = the engine state (bookable resource types, upcoming slots, booked
4058
+ seats). --slots <recordId> computes the slots for one bookable resource
4059
+ (occupancy included; --days is clamped to 1-31 server-side) — the way to verify
4060
+ the availability rules a \`deploy --seed\` created. Needs scheduling:read.
4061
+
4062
+ RULES (list needs scheduling:read; add/rm need scheduling:write):
4063
+ octwin scheduling rules --resource <resourceRecordId>
4064
+ octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
4065
+ [--slot-minutes 30] [--capacity 1]
4066
+ octwin scheduling rule rm <ruleId>
4067
+ octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
4068
+ [--start 09:00 --end 13:00]
4069
+ octwin scheduling exception rm <exceptionId>
4070
+
4071
+ --dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
3894
4072
  \`--slots\` is how you check what a rule actually produces.`,
3895
- 'platform-kb': `octwin platform-kb [pull] [--if-stale] [--check] [--dir .] [--url <url>] [--token <t>]
3896
- Pull the platform capability reference (markdown + JSON catalogs) into
3897
- .octwin/platform-kb/ for the octwin-pack authoring skill, plus three maps:
3898
- INDEX.md (the corpus) · SYMBOLS.md (every name -> its file; grep this) ·
3899
- OUTLINE.md (every heading with its line number).
3900
-
3901
- NO TOKEN NEEDED — the reference is platform stdlib and is served anonymously.
3902
- A token is used when you have one (it also works against older platforms).
3903
-
3904
- --if-stale poll the platform's content_hash first and skip the download when
3905
- nothing changed. Cheap enough to run at the start of every session.
3906
- --check report only, write nothing. Exit 0 = current, 2 = stale or never
3907
- pulled, 1 = could not tell (offline / refused). For scripts and
4073
+ 'platform-kb': `octwin platform-kb [pull] [--if-stale] [--check] [--dir .] [--url <url>] [--token <t>]
4074
+ Pull the platform capability reference (markdown + JSON catalogs) into
4075
+ .octwin/platform-kb/ for the octwin-pack authoring skill, plus three maps:
4076
+ INDEX.md (the corpus) · SYMBOLS.md (every name -> its file; grep this) ·
4077
+ OUTLINE.md (every heading with its line number).
4078
+
4079
+ NO TOKEN NEEDED — the reference is platform stdlib and is served anonymously.
4080
+ A token is used when you have one (it also works against older platforms).
4081
+
4082
+ --if-stale poll the platform's content_hash first and skip the download when
4083
+ nothing changed. Cheap enough to run at the start of every session.
4084
+ --check report only, write nothing. Exit 0 = current, 2 = stale or never
4085
+ pulled, 1 = could not tell (offline / refused). For scripts and
3908
4086
  agent loops that want to branch without parsing prose.`,
3909
- test: `octwin test [--dir .]
4087
+ test: `octwin test [--dir .]
3910
4088
  Alias for \`octwin validate --remote\` — the full platform check.`,
3911
- feedback: `octwin feedback [--dir .]
3912
- Submit this pack's FEEDBACK.md to the platform team.
3913
- The octwin-pack skill writes that file in its last step findings grouped by
3914
- owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
3915
- you to paste it into a chat.
3916
- Attaches the pack id + version from manifest.yaml, this CLI's version, and the
3917
- content_hash of the capability reference in .octwin/platform-kb/ — triage needs
3918
- the last two to tell "the platform is wrong" from "that was already fixed" or
4089
+ memos: `octwin memos [--all] [--json]
4090
+ Read what the platform has told you: a REPLY to a report you sent with
4091
+ \`octwin feedback\`, or a NOTICE published to every author (a new capability,
4092
+ a deprecation, a breaking change). Bodies are printed in full.
4093
+ Reading marks them read, so the reminder stops. --all re-reads history and
4094
+ acks nothing. --json to branch on \`severity\`
4095
+ (info | action_required | breaking).`,
4096
+ feedback: `octwin feedback [--dir .]
4097
+ Submit this pack's FEEDBACK.md to the platform team.
4098
+ The octwin-pack skill writes that file in its last step — findings grouped by
4099
+ owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
4100
+ you to paste it into a chat.
4101
+ Attaches the pack id + version from manifest.yaml, this CLI's version, and the
4102
+ content_hash of the capability reference in .octwin/platform-kb/ — triage needs
4103
+ the last two to tell "the platform is wrong" from "that was already fixed" or
3919
4104
  "you were reading a stale reference". Needs the \`pack:deploy\` scope.`,
3920
4105
  };
3921
4106
  async function main() {
@@ -3998,6 +4183,9 @@ async function main() {
3998
4183
  case 'feedback':
3999
4184
  await cmdFeedback(flags);
4000
4185
  break;
4186
+ case 'memos':
4187
+ await cmdMemos(flags);
4188
+ break;
4001
4189
  case 'test':
4002
4190
  await cmdValidate({ ...flags, remote: true });
4003
4191
  break; // A6: `test` = the full remote validate, not a validate-clone
@@ -4019,6 +4207,8 @@ async function main() {
4019
4207
  // offline paths); CLI-upgrade always.
4020
4208
  if (commandTouchesPlatform(command, flags))
4021
4209
  await notifyIfKbStale(flags);
4210
+ if (commandTouchesPlatform(command, flags))
4211
+ await notifyIfMemosWaiting(flags);
4022
4212
  await notifyIfOutdated();
4023
4213
  }
4024
4214
  main().catch((err) => die(err?.message ?? String(err)));