octwin-cli 0.8.6 → 0.8.8

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
@@ -10,7 +10,7 @@
10
10
  * octwin --version | -v # print the CLI version (+ any upgrade notice)
11
11
  * octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
12
12
  * octwin validate [--dir .] [--remote] [--require-kb] # --remote → the platform's FULL schema check + lint, all errors at once
13
- * octwin login --url <platformUrl> --token oct_…
13
+ * octwin login --url <platformUrl> [--token oct_…] # no token → approve in a browser
14
14
  * octwin whoami [--url <url>] [--tenant <slug>]
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]
@@ -60,7 +60,7 @@
60
60
  */
61
61
  import { readFileSync, writeFileSync, writeSync, mkdirSync, existsSync, readdirSync, statSync, cpSync, rmSync } from 'node:fs';
62
62
  import { join, resolve, dirname, basename } from 'node:path';
63
- import { homedir } from 'node:os';
63
+ import { homedir, hostname, platform } from 'node:os';
64
64
  import { fileURLToPath } from 'node:url';
65
65
  import { parse as parseYaml } from 'yaml';
66
66
  import { applyRenames } from './lib/rename.js';
@@ -79,6 +79,7 @@ import { classifyPackPath, isSkippedDir } from './lib/pack-source.js';
79
79
  import { readPage, morePageHint } from './lib/page.js';
80
80
  import { kbOneLiner, buildKbIndexMarkdown, buildKbOutlineMarkdown, } from './lib/kb-index.js';
81
81
  import { buildSymbols, linkExplainers, renderSymbolsMarkdown } from './lib/kb-symbols.js';
82
+ import { readPending, writePending, clearPending, isPendingExpired, secondsLeft, interpretPoll, clientLabel, EXIT_STILL_PENDING, } from './lib/device-login.js';
82
83
  // The in-package starter template ships alongside `dist/` and `src/` (both one
83
84
  // level under the package root), so `../templates/starter` resolves for the
84
85
  // built CLI and `tsx` dev alike.
@@ -181,7 +182,7 @@ async function fetchOrDie(url, init, what) {
181
182
  * unauthenticated), so say so instead of sending the author on a re-login hunt. */
182
183
  function authFailureHint(status, url) {
183
184
  return status === 401
184
- ? `the token was rejected — invalid / expired / revoked. If it JUST worked, this can be a one-off platform hiccup: retry once before re-logging in (octwin login --url ${url} --token oct_…)`
185
+ ? `the token was rejected — invalid / expired / revoked. If it JUST worked, this can be a one-off platform hiccup: retry once, then sign in again with \`octwin login --url ${url}\` (a browser-issued token lasts 30 days, so renewal is that one command)`
185
186
  : `the token is valid but not authorized here (missing scope, plan feature, or role)`;
186
187
  }
187
188
  /**
@@ -1224,10 +1225,34 @@ async function cmdValidate(flags) {
1224
1225
  }
1225
1226
  exitNow(1);
1226
1227
  }
1228
+ /**
1229
+ * Two ways in, one saved result.
1230
+ *
1231
+ * octwin login --url <u> --token oct_… the token you minted in the console
1232
+ * octwin login --url <u> approve it in a browser instead
1233
+ *
1234
+ * The token form is unchanged. The browser form is the one an agent can drive:
1235
+ * see `cmdLoginBrowser`.
1236
+ */
1227
1237
  async function cmdLogin(flags) {
1228
- const rawUrl = flags.url ?? process.env.PACK_PLATFORM_URL ?? die('usage: octwin login --url <platformUrl> --token <t>');
1238
+ const rawUrl = flags.url ?? process.env.PACK_PLATFORM_URL ?? savedDefaultUrl();
1239
+ if (!rawUrl) {
1240
+ die('usage: octwin login --url <platformUrl> (add --token oct_… to use a token from the console)');
1241
+ }
1229
1242
  const url = rawUrl.replace(/\/$/, '');
1230
- const token = flags.token ?? process.env.PACK_TOKEN ?? die('missing --token');
1243
+ const token = flags.token ?? process.env.PACK_TOKEN;
1244
+ if (token)
1245
+ return saveLogin(url, token);
1246
+ return cmdLoginBrowser(url, flags);
1247
+ }
1248
+ /**
1249
+ * Persist a token as the default deploy target, then say what it reaches.
1250
+ *
1251
+ * Shared by both login paths deliberately: a browser sign-in and a pasted token
1252
+ * must produce the same saved state and the same confirmation, or one of them
1253
+ * becomes the odd one people distrust.
1254
+ */
1255
+ async function saveLogin(url, token) {
1231
1256
  const creds = readCreds();
1232
1257
  creds[url] = token;
1233
1258
  creds[DEFAULT_URL_KEY] = url; // login sets the default deploy target
@@ -1249,6 +1274,150 @@ async function cmdLogin(flags) {
1249
1274
  }
1250
1275
  catch { /* platform unreachable — the token is saved regardless */ }
1251
1276
  }
1277
+ /** `~/.octwin/pending-login.json` — a handshake in flight, per platform url. */
1278
+ function pendingLoginPath() { return join(homedir(), '.octwin', 'pending-login.json'); }
1279
+ /**
1280
+ * The gap between polls.
1281
+ *
1282
+ * **Deliberately NOT `.unref()`ed.** The timer this returns is the only pending work
1283
+ * during a wait, so an unref'd one lets Node decide the event loop is empty and exit
1284
+ * mid-handshake — silently, with code 0, printing neither the ✓ nor the "not approved
1285
+ * yet" line. Measured 2026-09-06: the first resume happened to win the race and every
1286
+ * later one exited quietly. `unref` belongs on a timer RACING something else (as in
1287
+ * `SseFrameReader`, where a pending read holds the loop open); here it is the thing
1288
+ * being waited for.
1289
+ */
1290
+ function sleep(ms) {
1291
+ return new Promise(res => { setTimeout(res, ms); });
1292
+ }
1293
+ /**
1294
+ * How long one `octwin login` waits before handing the terminal back.
1295
+ *
1296
+ * 90 seconds, because THE CLI IS MOSTLY DRIVEN BY AN AGENT and an agent's shell
1297
+ * call is killed around two minutes. A wait that outlives its caller is worse than
1298
+ * a short one: the process dies mid-handshake and the code the human is still
1299
+ * typing belongs to nothing. So the wait is bounded, the handshake is written to
1300
+ * disk, and running the command again resumes the SAME code — which the platform
1301
+ * keeps alive for its full ten minutes.
1302
+ */
1303
+ const LOGIN_WAIT_SECONDS = 90;
1304
+ async function cmdLoginBrowser(url, flags) {
1305
+ const asJson = flags.json === true;
1306
+ const path = pendingLoginPath();
1307
+ let pending = readPending(path, url);
1308
+ let resuming = pending != null;
1309
+ if (pending && isPendingExpired(pending)) {
1310
+ clearPending(path, url);
1311
+ pending = null;
1312
+ resuming = false;
1313
+ if (!asJson)
1314
+ console.log('⚠ The previous code expired. Here is a new one.');
1315
+ }
1316
+ if (!pending) {
1317
+ const res = await fetchOrDie(`${url}/api/public/cli-login/start`, {
1318
+ method: 'POST',
1319
+ headers: { 'content-type': 'application/json' },
1320
+ body: JSON.stringify({ client_label: clientLabel(hostname(), platform()) }),
1321
+ }, 'starting sign-in');
1322
+ const started = await res.json().catch(() => null);
1323
+ if (!res.ok || !started?.device_code || !started.user_code) {
1324
+ if (res.status === 404) {
1325
+ die(`this platform has no browser sign-in yet (older version) — mint a token in the console and use \`octwin login --url ${url} --token oct_…\``);
1326
+ }
1327
+ die(`could not start sign-in (HTTP ${res.status})${started?.error ? ` — ${started.error}` : ''}`);
1328
+ }
1329
+ pending = {
1330
+ device_code: started.device_code,
1331
+ user_code: started.user_code,
1332
+ verify_url: started.verify_url ?? `${url}/cli-login`,
1333
+ expires_at: started.expires_at ?? new Date(Date.now() + 600_000).toISOString(),
1334
+ interval_seconds: started.interval_seconds ?? 5,
1335
+ };
1336
+ writePending(path, url, pending);
1337
+ }
1338
+ const waitSeconds = flags.wait !== undefined ? Math.max(0, Number(flags.wait) || 0) : LOGIN_WAIT_SECONDS;
1339
+ if (asJson) {
1340
+ console.log(JSON.stringify({
1341
+ status: 'pending',
1342
+ verify_url: pending.verify_url,
1343
+ user_code: pending.user_code,
1344
+ expires_at: pending.expires_at,
1345
+ }, null, 2));
1346
+ }
1347
+ else {
1348
+ // The link and the code go on their own labelled lines because an AGENT reads
1349
+ // this and relays it into a conversation verbatim. The format is an interface.
1350
+ //
1351
+ // The code is printed even though the link already carries it: it is what the
1352
+ // approver checks the page against, and the only thing they can compare if the
1353
+ // link reached them some other way.
1354
+ console.log('');
1355
+ console.log(` Open this link to approve: ${pending.verify_url}`);
1356
+ console.log(` Code shown on that page: ${pending.user_code}`);
1357
+ console.log('');
1358
+ console.log(` Valid for ${Math.max(1, Math.round(secondsLeft(pending) / 60))} more minute(s). Approving gives this machine access to your workspace,`);
1359
+ console.log(' so only approve a link you opened because YOU ran this command.');
1360
+ if (waitSeconds > 0)
1361
+ console.log(` Waiting ${waitSeconds}s… (run \`octwin login\` again to keep waiting)`);
1362
+ }
1363
+ const intervalMs = Math.max(1, pending.interval_seconds) * 1000;
1364
+ const deadline = Date.now() + waitSeconds * 1000;
1365
+ // A resumed handshake may ALREADY be approved, so ask before waiting; a fresh one
1366
+ // cannot be, so give the human one interval first.
1367
+ let delay = resuming ? 0 : intervalMs;
1368
+ while (Date.now() + delay <= deadline) {
1369
+ await sleep(delay);
1370
+ delay = intervalMs;
1371
+ const res = await fetchOrDie(`${url}/api/public/cli-login/poll`, {
1372
+ method: 'POST',
1373
+ headers: { 'content-type': 'application/json' },
1374
+ body: JSON.stringify({ device_code: pending.device_code }),
1375
+ }, 'checking sign-in');
1376
+ const body = await res.json().catch(() => null);
1377
+ const outcome = interpretPoll(res.status, body, res.headers.get('retry-after'), intervalMs);
1378
+ switch (outcome.kind) {
1379
+ case 'approved': {
1380
+ clearPending(path, url);
1381
+ if (asJson) {
1382
+ console.log(JSON.stringify({
1383
+ status: 'approved', tenant_slug: outcome.tenantSlug,
1384
+ project_slug: outcome.projectSlug, token_expires: outcome.tokenExpires,
1385
+ }, null, 2));
1386
+ const creds = readCreds();
1387
+ creds[url] = outcome.token;
1388
+ creds[DEFAULT_URL_KEY] = url;
1389
+ writeCreds(creds);
1390
+ return;
1391
+ }
1392
+ await saveLogin(url, outcome.token);
1393
+ if (outcome.tokenExpires) {
1394
+ console.log(` → Expires ${outcome.tokenExpires.slice(0, 10)} — run \`octwin login\` again to renew.`);
1395
+ }
1396
+ return;
1397
+ }
1398
+ case 'gone':
1399
+ clearPending(path, url);
1400
+ die(outcome.reason === 'expired'
1401
+ ? 'that code expired before it was approved — run `octwin login` again for a new one'
1402
+ : 'that sign-in request is no longer on the platform — run `octwin login` again');
1403
+ break;
1404
+ case 'failed':
1405
+ die(`sign-in failed — ${outcome.detail}`);
1406
+ break;
1407
+ case 'retry':
1408
+ delay = outcome.afterMs;
1409
+ break;
1410
+ case 'pending':
1411
+ break;
1412
+ }
1413
+ }
1414
+ if (!asJson) {
1415
+ console.log('');
1416
+ console.log('⏳ Not approved yet. Run `octwin login` again to keep waiting — the same code stays valid.');
1417
+ console.log(' Running in CI? Skip login and set PACK_PLATFORM_URL + PACK_TOKEN instead.');
1418
+ }
1419
+ exitNow(EXIT_STILL_PENDING);
1420
+ }
1252
1421
  /** Bearer auth + the optional self-surface overrides, as request headers. */
1253
1422
  function authHeaders(t) {
1254
1423
  const h = { authorization: `Bearer ${t.token}` };
@@ -1557,25 +1726,11 @@ function printDeploySuccess(id, version, t, r, listing, problems = 0) {
1557
1726
  : `✓ Deployed ${id}@${version} and installed onto ${targetLabel(t)}`);
1558
1727
  if (r?.warning)
1559
1728
  console.log(` ⚠ ${r.warning}`);
1560
- const s = r?.summary;
1561
- if (s) {
1562
- const parts = [];
1563
- if (s.records != null)
1564
- parts.push(`${s.records} record(s) created`);
1565
- if (s.updated)
1566
- parts.push(`${s.updated} updated`);
1567
- if (s.images)
1568
- parts.push(`${s.images} image(s) generated`);
1569
- if (s.rules)
1570
- parts.push(`${s.rules} availability rule(s)`);
1571
- // A count of rows that threw. The seed keeps going past a bad row now, so a
1572
- // partial seed is a real outcome and has to be said out loud — the alternative
1573
- // reads as a complete one with fewer records than the author wrote.
1574
- if (s.failed)
1575
- parts.push(`${s.failed} row(s) FAILED`);
1576
- if (parts.length)
1577
- console.log(` Seeded: ${parts.join(', ')}`);
1578
- }
1729
+ // This read `r.summary.records` / `.rules` / `.failed` and had been DEAD: the deploy route's
1730
+ // `summary` is a run-log STRING (`"clinic v1.2.0 — {…}"`), so every field was undefined and the
1731
+ // block printed nothing. The counts live on `r.seeded`, keyed by seed kind, and `printSeedCounts`
1732
+ // already renders them — filtering zeros, which is what makes a partial seed legible.
1733
+ printSeedCounts(r?.seeded);
1579
1734
  // A redeploy rebuilds the pack's tools, and suspended flow runs live with them.
1580
1735
  // Say so: otherwise the next tap on a card rendered before the deploy comes back
1581
1736
  // stale and reads like a flow bug.
@@ -1626,7 +1781,7 @@ async function cmdSeed(flags) {
1626
1781
  if (stepErrors.length) {
1627
1782
  // A kind failed but the rest ran — the reconcile softens each step. Say which,
1628
1783
  // and exit non-zero so a scripted `seed && chat` doesn't read as clean.
1629
- console.error(`
1784
+ console.error(`
1630
1785
  ⚠ ${stepErrors.length} step${stepErrors.length === 1 ? '' : 's'} failed — data may be incomplete:`);
1631
1786
  for (const e of stepErrors)
1632
1787
  console.error(` • ${e}`);
@@ -4984,372 +5139,388 @@ async function cmdUsage(flags) {
4984
5139
  console.log('\nThis is MODEL spend. WhatsApp/Meta message billing is operator-only — not reachable by an API token.');
4985
5140
  }
4986
5141
  function help() {
4987
- console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
4988
-
4989
- octwin --version # print the CLI version (+ any upgrade notice)
4990
- octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
4991
- octwin validate [--dir .] [--remote] [--require-kb] # --remote runs the platform's FULL schema check + lint (all errors at once)
4992
- octwin login --url <platformUrl> --token oct_… # a deploy token from the console
4993
- octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
4994
- octwin projects [--archived] [--json] # the --project slugs this token can name
4995
- octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
4996
- [--request-listing | --withdraw-listing] # public marketplace opt-in, see: octwin help deploy
4997
- octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
4998
- octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
4999
- octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
5000
- octwin work [recordId] [--queues] [--unrouted] [--json] # inspect the work inbox (worked records) + timelines
5001
- # --queues: per-queue open counts + an UNROUTED warning · --unrouted: only the items in no queue
5002
- octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
5003
- octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
5004
- octwin media generate "<prompt>" [--out <file.png>] [--json] # AI-generate an image MEDIA- handle (needs media:generate scope)
5005
- octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
5006
- octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
5007
- octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
5008
- octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
5009
- octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
5010
- octwin automation [campaigns] [--json] # the jobs your declarations produced + health, last result each
5011
- octwin integrations [--json] # declared connections BESIDE what is configured (the silent-never-fires check)
5012
- octwin integrations deliveries [<id>] | events # the outbound delivery log / inbound events
5013
- octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]
5014
- octwin performance [--detail] [--json] # the project's business indicators (value/conversion/duration)
5015
- octwin usage [--json] # model calls, tokens and cost (project if pinned, else workspace)
5016
- octwin platform-kb [pull] [--if-stale|--check] [--dir .] [--url <url>] # no token needed
5017
- octwin test [--dir .] # = validate --remote (the full platform check)
5018
- octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
5019
- octwin memos [--all] [--json] # read the platform's replies + notices (a reply to your feedback lands here)
5020
-
5021
- Writes — exercise the state your pack creates (each needs the matching :write scope):
5022
- octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…"
5023
- octwin records tasks | task complete <taskId> [--outcome done|cancelled]
5024
- octwin work assign <id> --to user:<uuid>|none | note <id> "…" | stage <id> --to <stage>
5025
- octwin work decide <id> --action <a> [--param k=v] [--dry-run] # --dry-run previews, commits nothing
5026
- octwin orders transition <ref> --to <status> | refund <ref> --force
5027
- octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
5028
- octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
5029
- octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
5030
- octwin automation run <jobId> | pause <jobId> | resume <jobId> | send <campaignId>
5031
- octwin integrations test <key> # a LIVE call to the connection's health: operation
5032
- octwin integrations retry|cancel|send-now <deliveryId>
5033
- (octwin integrations preflight <key> needs only integrations:read — it makes no call)
5034
-
5035
- Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
5036
- \`octwin chat --as <h>\` calls continue the same conversation; press a rendered
5037
- button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
5038
- Get a deploy token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
5039
- octwin platform-kb pull writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
5040
- Config (deploy): flags > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login (\`octwin login\` sets the default target).
5142
+ console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
5143
+
5144
+ octwin --version # print the CLI version (+ any upgrade notice)
5145
+ octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
5146
+ octwin validate [--dir .] [--remote] [--require-kb] # --remote runs the platform's FULL schema check + lint (all errors at once)
5147
+ octwin login --url <platformUrl> # approve in a browser; the CLI collects the token
5148
+ octwin login --url <platformUrl> --token oct_… # or paste a deploy token from the console
5149
+ octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
5150
+ octwin projects [--archived] [--json] # the --project slugs this token can name
5151
+ octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
5152
+ [--request-listing | --withdraw-listing] # public marketplace opt-in, see: octwin help deploy
5153
+ octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
5154
+ octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
5155
+ octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
5156
+ octwin work [recordId] [--queues] [--unrouted] [--json] # inspect the work inbox (worked records) + timelines
5157
+ # --queues: per-queue open counts + an UNROUTED warning · --unrouted: only the items in no queue
5158
+ octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
5159
+ octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
5160
+ octwin media generate "<prompt>" [--out <file.png>] [--json] # AI-generate an image MEDIA- handle (needs media:generate scope)
5161
+ octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
5162
+ octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
5163
+ octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
5164
+ octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
5165
+ octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
5166
+ octwin automation [campaigns] [--json] # the jobs your declarations produced + health, last result each
5167
+ octwin integrations [--json] # declared connections BESIDE what is configured (the silent-never-fires check)
5168
+ octwin integrations deliveries [<id>] | events # the outbound delivery log / inbound events
5169
+ octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]
5170
+ octwin performance [--detail] [--json] # the project's business indicators (value/conversion/duration)
5171
+ octwin usage [--json] # model calls, tokens and cost (project if pinned, else workspace)
5172
+ octwin platform-kb [pull] [--if-stale|--check] [--dir .] [--url <url>] # no token needed
5173
+ octwin test [--dir .] # = validate --remote (the full platform check)
5174
+ octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
5175
+ octwin memos [--all] [--json] # read the platform's replies + notices (a reply to your feedback lands here)
5176
+
5177
+ Writes exercise the state your pack creates (each needs the matching :write scope):
5178
+ octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…"
5179
+ octwin records tasks | task complete <taskId> [--outcome done|cancelled]
5180
+ octwin work assign <id> --to user:<uuid>|none | note <id> "…" | stage <id> --to <stage>
5181
+ octwin work decide <id> --action <a> [--param k=v] [--dry-run] # --dry-run previews, commits nothing
5182
+ octwin orders transition <ref> --to <status> | refund <ref> --force
5183
+ octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
5184
+ octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
5185
+ octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
5186
+ octwin automation run <jobId> | pause <jobId> | resume <jobId> | send <campaignId>
5187
+ octwin integrations test <key> # a LIVE call to the connection's health: operation
5188
+ octwin integrations retry|cancel|send-now <deliveryId>
5189
+ (octwin integrations preflight <key> needs only integrations:read — it makes no call)
5190
+
5191
+ Multi-turn: the platform keeps ONE open conversation per --as handle consecutive
5192
+ \`octwin chat --as <h>\` calls continue the same conversation; press a rendered
5193
+ button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
5194
+ Signing in: \`octwin login --url <u>\` prints a link + code to approve in a browser. To pick your own
5195
+ scopes instead, mint a token: console your workspace Settings API tokens → Generate (tick records:read to inspect data).
5196
+ octwin platform-kb pull → writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
5197
+ Config (deploy): flags > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login (\`octwin login\` sets the default target).
5041
5198
  Per-command usage: octwin <command> --help`);
5042
5199
  }
5043
5200
  /** Per-subcommand usage — printed for `octwin <cmd> --help|-h` BEFORE any
5044
5201
  * network/auth work (a --help that 401s is worse than no help at all). */
5045
5202
  const COMMAND_HELP = {
5046
- init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
5203
+ init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
5047
5204
  Scaffold a pure-YAML starter pack into <dir>.`,
5048
- validate: `octwin validate [--dir .] [--remote] [--require-kb] [--strict-primitives]
5049
- Offline structural check, plus two checks driven by the pulled capability
5050
- reference (render-intent fields, primitive arguments). Those two SKIP when the
5051
- reference is missing — the run says so, and --require-kb turns the skip into a
5052
- failure for CI. --remote additionally runs the platform's FULL manifest +
5053
- flow-DSL validation and its flow lint (all errors at once) — same check as deploy.
5054
- --strict-primitives (with --remote) additionally type-checks LITERAL args:
5055
- values against each primitive's declared input schema; expression strings
5205
+ validate: `octwin validate [--dir .] [--remote] [--require-kb] [--strict-primitives]
5206
+ Offline structural check, plus two checks driven by the pulled capability
5207
+ reference (render-intent fields, primitive arguments). Those two SKIP when the
5208
+ reference is missing — the run says so, and --require-kb turns the skip into a
5209
+ failure for CI. --remote additionally runs the platform's FULL manifest +
5210
+ flow-DSL validation and its flow lint (all errors at once) — same check as deploy.
5211
+ --strict-primitives (with --remote) additionally type-checks LITERAL args:
5212
+ values against each primitive's declared input schema; expression strings
5056
5213
  ('$found.id', '{$t(…)}') are always exempt.`,
5057
- login: `octwin login --url <platformUrl> --token oct_…
5058
- Save a deploy token (console Settings API tokens) for that platform url,
5059
- make that url the DEFAULT deploy target for every later command, and echo the
5060
- workspace + project pin + scopes the token reaches.`,
5061
- whoami: `octwin whoami [--url <url>] [--tenant <slug>]
5214
+ login: `octwin login --url <platformUrl> [--token oct_…] [--wait <seconds>] [--json]
5215
+ Sign in to a platform and make that url the DEFAULT deploy target for every
5216
+ later command.
5217
+
5218
+ WITH --token: saves a token you minted yourself (console → Settings → API
5219
+ tokens). Unchanged, and still the right answer for CI — there, skip login and
5220
+ set PACK_PLATFORM_URL + PACK_TOKEN instead.
5221
+
5222
+ WITHOUT --token: prints a link to approve in a browser. A workspace admin opens
5223
+ it, checks which machine is asking, and clicks Approve; the CLI then collects a
5224
+ 30-day token by itself, so no token is ever pasted between people. The code is
5225
+ printed too — it is what the page shows, so you can tell it is your own request.
5226
+ Approve only a link you opened because YOU ran this command: a link someone
5227
+ sends you would connect THEIR machine to your workspace.
5228
+
5229
+ The wait is bounded (90s by default) and RESUMABLE: if nobody has approved yet
5230
+ it exits 75 and keeps the request, so running \`octwin login\` again picks up the
5231
+ same code. --wait 0 prints the code and returns at once.`,
5232
+ whoami: `octwin whoami [--url <url>] [--tenant <slug>]
5062
5233
  Verify the resolved token authenticates against the tenant.`,
5063
- projects: `octwin projects [--archived] [--json]
5064
- List the workspace's projects — the slugs every --project flag takes, with the
5065
- plan's project cap. --archived includes archived ones. A pack:deploy token
5066
- reaches this (it names a project in every other command).
5067
-
5068
- octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
5069
- Create a project. The URL slug is derived from the name unless --slug pins one.
5070
- --pack installs an ALREADY-published pack; the usual next step is instead
5071
- \`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
5072
-
5073
- octwin projects rm <slug> [--yes]
5074
- HARD delete — the project and everything cascading from it (conversations,
5075
- contacts, records, installs). No undo, and not the same as archiving.
5076
- WITHOUT --yes it only previews what would be destroyed, so the dry run is the
5077
- default. Together these make a disposable end-to-end environment:
5078
- octwin projects create "Scratch" && octwin deploy --project scratch --seed
5079
- octwin chat "hi" --project scratch
5080
- octwin projects rm scratch --yes
5234
+ projects: `octwin projects [--archived] [--json]
5235
+ List the workspace's projects — the slugs every --project flag takes, with the
5236
+ plan's project cap. --archived includes archived ones. A pack:deploy token
5237
+ reaches this (it names a project in every other command).
5238
+
5239
+ octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
5240
+ Create a project. The URL slug is derived from the name unless --slug pins one.
5241
+ --pack installs an ALREADY-published pack; the usual next step is instead
5242
+ \`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
5243
+
5244
+ octwin projects rm <slug> [--yes]
5245
+ HARD delete — the project and everything cascading from it (conversations,
5246
+ contacts, records, installs). No undo, and not the same as archiving.
5247
+ WITHOUT --yes it only previews what would be destroyed, so the dry run is the
5248
+ default. Together these make a disposable end-to-end environment:
5249
+ octwin projects create "Scratch" && octwin deploy --project scratch --seed
5250
+ octwin chat "hi" --project scratch
5251
+ octwin projects rm scratch --yes
5081
5252
  Both verbs need the \`projects:write\` scope — a pack:deploy token does NOT confer it.`,
5082
- deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
5083
- [--request-listing | --withdraw-listing]
5084
- Upload the pack bundle, validate server-side, install onto the project.
5085
- --seed additionally applies the pack's demo seed (streams progress).
5086
-
5087
- A plain deploy says NOTHING about the public marketplace — it is a test loop, so it
5088
- neither asks for a listing nor gives one up. The marketplace flags are opt-in:
5089
-
5090
- --request-listing ask an operator to review this pack for the public marketplace
5091
- (the pre-signup storefront at /packs). Requires 'public: true'
5092
- under 'listing:' in manifest.yaml — the manifest states that the
5093
- pack is a product, the flag is you choosing to ask.
5094
- --withdraw-listing retract the request, including an approved listing.
5095
-
5096
- An approval covers the CONTENT it was made against, so a later deploy that changes the
5253
+ deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
5254
+ [--request-listing | --withdraw-listing]
5255
+ Upload the pack bundle, validate server-side, install onto the project.
5256
+ --seed additionally applies the pack's demo seed (streams progress).
5257
+
5258
+ A plain deploy says NOTHING about the public marketplace — it is a test loop, so it
5259
+ neither asks for a listing nor gives one up. The marketplace flags are opt-in:
5260
+
5261
+ --request-listing ask an operator to review this pack for the public marketplace
5262
+ (the pre-signup storefront at /packs). Requires 'public: true'
5263
+ under 'listing:' in manifest.yaml — the manifest states that the
5264
+ pack is a product, the flag is you choosing to ask.
5265
+ --withdraw-listing retract the request, including an approved listing.
5266
+
5267
+ An approval covers the CONTENT it was made against, so a later deploy that changes the
5097
5268
  pack returns it to the review queue on its own — no flag needed, and the CLI says so.`,
5098
- seed: `octwin seed [--pack <packId>]
5099
- Apply the pack's demo/reference data to the project it is installed on, without
5100
- redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
5101
- and the demo operator topology. Reports what each kind produced.
5102
- Idempotent and safe to re-run — records upsert, and existing media is REUSED rather
5103
- than regenerated, so a second pass costs nothing. --pack is only needed when a
5269
+ seed: `octwin seed [--pack <packId>]
5270
+ Apply the pack's demo/reference data to the project it is installed on, without
5271
+ redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
5272
+ and the demo operator topology. Reports what each kind produced.
5273
+ Idempotent and safe to re-run — records upsert, and existing media is REUSED rather
5274
+ than regenerated, so a second pass costs nothing. --pack is only needed when a
5104
5275
  project somehow runs more than one.`,
5105
- status: `octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
5106
- Show installed vs live version + the flow list for this pack.
5107
- The pack id is read from manifest.yaml and QUALIFIED with your workspace slug
5108
- (a manifest declares a bare name; the owner is attached when you publish). Pass
5109
- <packId> explicitly to skip that lookup — \`octwin agents\` and \`octwin projects\`
5276
+ status: `octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
5277
+ Show installed vs live version + the flow list for this pack.
5278
+ The pack id is read from manifest.yaml and QUALIFIED with your workspace slug
5279
+ (a manifest declares a bare name; the owner is attached when you publish). Pass
5280
+ <packId> explicitly to skip that lookup — \`octwin agents\` and \`octwin projects\`
5110
5281
  both print the qualified form.`,
5111
- records: `octwin records [entity] [id] [--limit 50] [--offset n]
5112
- Inspect the pack's XRM data. No args = list entities. Worked records (cases,
5113
- tickets, anything routed to a queue) read best through \`octwin work\`.
5114
-
5115
- WRITES (need \`records:write\`; every one is re-checked by RBAC on the record):
5116
- octwin records create <entity> --set field=value [--set …] [--stage s] [--contact <id>]
5117
- octwin records patch <recordId> --entity <entity> --set field=value
5118
- octwin records stage <recordId> --to <stage> [--note "..."]
5119
- octwin records note <recordId> "the note text"
5120
- octwin records tasks # open follow-up tasks (\`tasks\` plan feature)
5121
- octwin records task complete <taskId> [--outcome done|cancelled] [--note "..."]
5122
-
5123
- --set coerces JSON scalars: \`--set rating=4.5\` sends a number, \`--set x=null\`
5124
- sends null. Use --fields-json '{"a":{"b":1}}' for anything nested.
5125
- \`patch\` needs --entity even though it has an id: the route resolves the field
5126
- validator from it. A leading \`create/patch/stage/note/tasks/task\` is read as a
5282
+ records: `octwin records [entity] [id] [--limit 50] [--offset n]
5283
+ Inspect the pack's XRM data. No args = list entities. Worked records (cases,
5284
+ tickets, anything routed to a queue) read best through \`octwin work\`.
5285
+
5286
+ WRITES (need \`records:write\`; every one is re-checked by RBAC on the record):
5287
+ octwin records create <entity> --set field=value [--set …] [--stage s] [--contact <id>]
5288
+ octwin records patch <recordId> --entity <entity> --set field=value
5289
+ octwin records stage <recordId> --to <stage> [--note "..."]
5290
+ octwin records note <recordId> "the note text"
5291
+ octwin records tasks # open follow-up tasks (\`tasks\` plan feature)
5292
+ octwin records task complete <taskId> [--outcome done|cancelled] [--note "..."]
5293
+
5294
+ --set coerces JSON scalars: \`--set rating=4.5\` sends a number, \`--set x=null\`
5295
+ sends null. Use --fields-json '{"a":{"b":1}}' for anything nested.
5296
+ \`patch\` needs --entity even though it has an id: the route resolves the field
5297
+ validator from it. A leading \`create/patch/stage/note/tasks/task\` is read as a
5127
5298
  VERB — to list an entity actually named one of those, use \`--entity <name>\`.`,
5128
- work: `octwin work [recordId] [--queues] [--unrouted] [--limit 50] [--offset n] [--json]
5129
- Inspect the work inbox — every entity the pack declares worked (cases, orders
5130
- needing review, applications, …): the inbox, one item + its timeline
5131
- (+ applicable actions), or --queues for queue keys + open counts.
5132
-
5133
- WRITES (need \`work:write\`; \`stage\` needs \`records:write\`):
5134
- octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none
5135
- octwin work note <recordId> "the note text"
5136
- octwin work stage <recordId> --to <stage> [--note "..."]
5137
- octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]
5138
-
5139
- \`decide\` applies one of the entity's declared operator actions — \`octwin work <id>\`
5140
- lists them with their params. --dry-run previews the customer-facing copy and the
5141
- resulting stage WITHOUT committing (that route needs only \`work:read\`).
5299
+ work: `octwin work [recordId] [--queues] [--unrouted] [--limit 50] [--offset n] [--json]
5300
+ Inspect the work inbox — every entity the pack declares worked (cases, orders
5301
+ needing review, applications, …): the inbox, one item + its timeline
5302
+ (+ applicable actions), or --queues for queue keys + open counts.
5303
+
5304
+ WRITES (need \`work:write\`; \`stage\` needs \`records:write\`):
5305
+ octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none
5306
+ octwin work note <recordId> "the note text"
5307
+ octwin work stage <recordId> --to <stage> [--note "..."]
5308
+ octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]
5309
+
5310
+ \`decide\` applies one of the entity's declared operator actions — \`octwin work <id>\`
5311
+ lists them with their params. --dry-run previews the customer-facing copy and the
5312
+ resulting stage WITHOUT committing (that route needs only \`work:read\`).
5142
5313
  \`stage\` is the XRM records verb (one transition spelling platform-wide).`,
5143
- logs: `octwin logs [conversationId] [--as <handle>] [--json]
5144
- No id = recent conversations (handle, status, last activity; --as filters).
5145
- With id = the full event timeline including what each turn rendered.
5314
+ logs: `octwin logs [conversationId] [--as <handle>] [--json]
5315
+ No id = recent conversations (handle, status, last activity; --as filters).
5316
+ With id = the full event timeline including what each turn rendered.
5146
5317
  --json = raw events (verbatim payloads).`,
5147
- pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
5148
- Write a DEPLOYED pack's source back to disk — the inverse of deploy.
5149
- A pack pushed with 'octwin deploy' lives on the platform as an artifact the
5150
- runtime serves but nothing hands back, so its only source copy is the machine
5151
- that pushed it. Pull it, fix it, redeploy it.
5152
- Defaults to the version installed on the target project; --version overrides.
5153
- --dir defaults to ./<packId>; a non-empty dir needs --force.
5154
- The pulled dir redeploys where it came from — the target is your saved login.
5318
+ pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
5319
+ Write a DEPLOYED pack's source back to disk — the inverse of deploy.
5320
+ A pack pushed with 'octwin deploy' lives on the platform as an artifact the
5321
+ runtime serves but nothing hands back, so its only source copy is the machine
5322
+ that pushed it. Pull it, fix it, redeploy it.
5323
+ Defaults to the version installed on the target project; --version overrides.
5324
+ --dir defaults to ./<packId>; a non-empty dir needs --force.
5325
+ The pulled dir redeploys where it came from — the target is your saved login.
5155
5326
  You may pull a pack your tenant OWNS (deployed); an operator token pulls any.`,
5156
- chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
5157
- octwin chat --script <file> [--as <handle>] [--json]
5158
- Drive ONE turn through the dev web channel and print every render with its
5159
- tap ids. Same --as handle = same conversation (multi-turn works).
5160
- --tap presses a rendered button/list row instead of sending text.
5161
- --media uploads a local file (or a media id from 'media generate --json') as
5162
- an image/document/audio inbound — any "message" rides as its caption; feeds a
5163
- running media-collect flow (e.g. activate-app).
5164
- --json dumps the raw SSE envelopes for the turn.
5165
-
5166
- --script drives a WHOLE conversation from a file, ONE TURN PER LINE, in one
5167
- process over one connection — waiting for each turn to settle before sending
5168
- the next. Use this for any multi-step flow: chaining shell invocations races
5169
- the agent loop, because a turn ends on a quiet gap that can arrive while the
5170
- server is still working (the symptom is placeholder-filled fields or a second
5171
- workflow run). Blank lines and # comments are skipped:
5172
-
5173
- # book an appointment end to end
5174
- احجز موعد
5175
- tap:t:invoke:book-appointment:doctor_id=D1
5176
- media:./licence.jpg | here is my licence
5327
+ chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
5328
+ octwin chat --script <file> [--as <handle>] [--json]
5329
+ Drive ONE turn through the dev web channel and print every render with its
5330
+ tap ids. Same --as handle = same conversation (multi-turn works).
5331
+ --tap presses a rendered button/list row instead of sending text.
5332
+ --media uploads a local file (or a media id from 'media generate --json') as
5333
+ an image/document/audio inbound — any "message" rides as its caption; feeds a
5334
+ running media-collect flow (e.g. activate-app).
5335
+ --json dumps the raw SSE envelopes for the turn.
5336
+
5337
+ --script drives a WHOLE conversation from a file, ONE TURN PER LINE, in one
5338
+ process over one connection — waiting for each turn to settle before sending
5339
+ the next. Use this for any multi-step flow: chaining shell invocations races
5340
+ the agent loop, because a turn ends on a quiet gap that can arrive while the
5341
+ server is still working (the symptom is placeholder-filled fields or a second
5342
+ workflow run). Blank lines and # comments are skipped:
5343
+
5344
+ # book an appointment end to end
5345
+ احجز موعد
5346
+ tap:t:invoke:book-appointment:doctor_id=D1
5347
+ media:./licence.jpg | here is my licence
5177
5348
  tap:t:resume:book-appointment:run_id=R1;_ctl_approved=true`,
5178
- media: `octwin media generate "<prompt>" [--out <file.png>] [--json]
5179
- AI-generate an image (needs a media:generate-scoped token), store it as a
5180
- public asset, and print its MEDIA- handle + serve URL. --out downloads the
5181
- bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
5349
+ media: `octwin media generate "<prompt>" [--out <file.png>] [--json]
5350
+ AI-generate an image (needs a media:generate-scoped token), store it as a
5351
+ public asset, and print its MEDIA- handle + serve URL. --out downloads the
5352
+ bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
5182
5353
  bytes }. Pair with 'octwin chat --media' to drive media flows.`,
5183
- agents: `octwin agents [packId::agentId] [--prompt] [--json]
5184
- No args = the roster with each agent's EFFECTIVE model and which layer set it.
5185
- With an agent = every governed setting (model / memory.last_messages /
5186
- working_memory) plus the layer that won — an operator PLATFORM default can
5187
- override what your manifest declares, and this is where you see that.
5188
- --prompt = the exact system prompt the LLM sees for this project (pack
5189
- instructions + platform protocol + any project overlay). Needs agents:read.
5190
- The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
5191
-
5192
- WRITES (need \`agents:write\`):
5193
- octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
5194
- [--enable-tool <toolId>] [--disable-tool <toolId>]
5195
-
5196
- Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
5197
- so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
5354
+ agents: `octwin agents [packId::agentId] [--prompt] [--json]
5355
+ No args = the roster with each agent's EFFECTIVE model and which layer set it.
5356
+ With an agent = every governed setting (model / memory.last_messages /
5357
+ working_memory) plus the layer that won — an operator PLATFORM default can
5358
+ override what your manifest declares, and this is where you see that.
5359
+ --prompt = the exact system prompt the LLM sees for this project (pack
5360
+ instructions + platform protocol + any project overlay). Needs agents:read.
5361
+ The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
5362
+
5363
+ WRITES (need \`agents:write\`):
5364
+ octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
5365
+ [--enable-tool <toolId>] [--disable-tool <toolId>]
5366
+
5367
+ Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
5368
+ so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
5198
5369
  ids refuses --model with a 403 — the platform default governs there.`,
5199
- orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
5200
- No args = the order list (#number, status/payment, total, contact). With a
5201
- reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
5202
- payment_ref, and the allowed status transitions. Needs orders:read + the
5203
- \`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
5204
- so \`pending\` on a gateway-less workspace is expected, not a bug.
5205
-
5206
- WRITES (need \`orders:write\`):
5207
- octwin orders transition <reference_id> --to <status>
5208
- octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
5209
-
5210
- Refund is irreversible and moves money, hence --force. The route answers 200 even
5211
- when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
5212
- on a refusal rather than reporting a refund that never happened. Only a payment in
5370
+ orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
5371
+ No args = the order list (#number, status/payment, total, contact). With a
5372
+ reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
5373
+ payment_ref, and the allowed status transitions. Needs orders:read + the
5374
+ \`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
5375
+ so \`pending\` on a gateway-less workspace is expected, not a bug.
5376
+
5377
+ WRITES (need \`orders:write\`):
5378
+ octwin orders transition <reference_id> --to <status>
5379
+ octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
5380
+
5381
+ Refund is irreversible and moves money, hence --force. The route answers 200 even
5382
+ when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
5383
+ on a refusal rather than reporting a refund that never happened. Only a payment in
5213
5384
  \`captured\` state can be refunded; \`payment_status\` is never settable directly.`,
5214
- analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
5215
- No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
5216
- With an entity = stage-by-stage conversion (default --funnel) over the last 30
5217
- days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
5385
+ analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
5386
+ No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
5387
+ With an entity = stage-by-stage conversion (default --funnel) over the last 30
5388
+ days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
5218
5389
  range-filtered). Needs records:read + a \`view\` grant on \`record.<entity>\`.`,
5219
- catalog: `octwin catalog [--readiness] [--json]
5220
- The commerce \`product\` records + price, availability, stock (null = not
5221
- inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
5222
- Graph checklist (LIVE Graph calls; needs a bound access token). Needs
5223
- catalog:read + the \`catalog\` plan feature.
5224
-
5225
- WRITES (need \`catalog:write\`):
5226
- octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
5227
- octwin catalog stock <retailerId> [--set-on-hand <n>]
5228
-
5229
- \`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
5230
- inventory-tracked (always sellable), which is different from 0. Lowering on_hand
5231
- below the units already reserved for open carts is refused. Creating/deleting
5390
+ catalog: `octwin catalog [--readiness] [--json]
5391
+ The commerce \`product\` records + price, availability, stock (null = not
5392
+ inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
5393
+ Graph checklist (LIVE Graph calls; needs a bound access token). Needs
5394
+ catalog:read + the \`catalog\` plan feature.
5395
+
5396
+ WRITES (need \`catalog:write\`):
5397
+ octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
5398
+ octwin catalog stock <retailerId> [--set-on-hand <n>]
5399
+
5400
+ \`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
5401
+ inventory-tracked (always sellable), which is different from 0. Lowering on_hand
5402
+ below the units already reserved for open carts is refused. Creating/deleting
5232
5403
  products and the Meta catalog binding/sync stay in the console.`,
5233
- scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
5234
- No args = the engine state (bookable resource types, upcoming slots, booked
5235
- seats). --slots <recordId> computes the slots for one bookable resource
5236
- (occupancy included; --days is clamped to 1-31 server-side) — the way to verify
5237
- the availability rules a \`deploy --seed\` created. Needs scheduling:read.
5238
-
5239
- RULES (list needs scheduling:read; add/rm need scheduling:write):
5240
- octwin scheduling rules --resource <resourceRecordId>
5241
- octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
5242
- [--slot-minutes 30] [--capacity 1]
5243
- octwin scheduling rule rm <ruleId>
5244
- octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
5245
- [--start 09:00 --end 13:00]
5246
- octwin scheduling exception rm <exceptionId>
5247
-
5248
- --dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
5404
+ scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
5405
+ No args = the engine state (bookable resource types, upcoming slots, booked
5406
+ seats). --slots <recordId> computes the slots for one bookable resource
5407
+ (occupancy included; --days is clamped to 1-31 server-side) — the way to verify
5408
+ the availability rules a \`deploy --seed\` created. Needs scheduling:read.
5409
+
5410
+ RULES (list needs scheduling:read; add/rm need scheduling:write):
5411
+ octwin scheduling rules --resource <resourceRecordId>
5412
+ octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
5413
+ [--slot-minutes 30] [--capacity 1]
5414
+ octwin scheduling rule rm <ruleId>
5415
+ octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
5416
+ [--start 09:00 --end 13:00]
5417
+ octwin scheduling exception rm <exceptionId>
5418
+
5419
+ --dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
5249
5420
  \`--slots\` is how you check what a rule actually produces.`,
5250
- automation: `octwin automation [campaigns] [--limit n] [--offset n] [--json]
5251
- No args = every job the pack's automation declaration produced, with its status,
5252
- interval and LAST RESULT (matched / acted / errors), under a health line whose
5253
- counts come from SQL rather than from filtering the page — the job list is capped
5254
- server-side, so a client-side count would depend on the cap. Needs automation:read.
5255
-
5256
- Jobs are DERIVED from declarations. There is no \`create\`: no automation block in
5257
- the pack means no jobs, and \`octwin deploy\` is what installs them.
5258
-
5259
- WRITES (automation:write):
5260
- octwin automation run <jobId> # run once, now — prints matched/acted/errors
5261
- octwin automation pause|resume <jobId>
5262
- octwin automation send <campaignId> # enqueue a campaign; enqueued != delivered
5263
-
5264
- <jobId> is the \`key\` the list shows (its uuid works too). The routes themselves
5265
- accept only a uuid — the CLI resolves the key for you, and names the keys that do
5266
- exist when it cannot. A 403 on a write can be an RBAC grant gap rather than a
5421
+ automation: `octwin automation [campaigns] [--limit n] [--offset n] [--json]
5422
+ No args = every job the pack's automation declaration produced, with its status,
5423
+ interval and LAST RESULT (matched / acted / errors), under a health line whose
5424
+ counts come from SQL rather than from filtering the page — the job list is capped
5425
+ server-side, so a client-side count would depend on the cap. Needs automation:read.
5426
+
5427
+ Jobs are DERIVED from declarations. There is no \`create\`: no automation block in
5428
+ the pack means no jobs, and \`octwin deploy\` is what installs them.
5429
+
5430
+ WRITES (automation:write):
5431
+ octwin automation run <jobId> # run once, now — prints matched/acted/errors
5432
+ octwin automation pause|resume <jobId>
5433
+ octwin automation send <campaignId> # enqueue a campaign; enqueued != delivered
5434
+
5435
+ <jobId> is the \`key\` the list shows (its uuid works too). The routes themselves
5436
+ accept only a uuid — the CLI resolves the key for you, and names the keys that do
5437
+ exist when it cannot. A 403 on a write can be an RBAC grant gap rather than a
5267
5438
  missing scope: the action is re-checked against the job.`,
5268
- integrations: `octwin integrations [--json]
5269
- What the pack DECLARES beside what is actually CONFIGURED, in one view — because a
5270
- connection that is declared and never configured is the commonest reason an
5271
- integration silently never fires, and neither list alone can show it. Flags the
5272
- gap explicitly. Needs integrations:read.
5273
-
5274
- DIAGNOSE ONE CONNECTION:
5275
- octwin integrations preflight <key> # every check, with a fix hint. Makes NO
5276
- # outbound call — needs only integrations:read
5277
- octwin integrations test <key> # a LIVE call to its health: operation
5278
- # (integrations:write). Exits 1 when it fails.
5279
-
5280
- THE DELIVERY LOG:
5281
- octwin integrations deliveries [--status s] [--operation id] [--limit n]
5282
- octwin integrations deliveries <id> # + the redacted request/response snapshots
5283
- octwin integrations retry|cancel|send-now <id> # integrations:write
5284
- octwin integrations events # INBOUND events (what arrived at your webhook)
5285
-
5286
- retry/cancel answer 409 when the delivery is in the wrong state; the message
5439
+ integrations: `octwin integrations [--json]
5440
+ What the pack DECLARES beside what is actually CONFIGURED, in one view — because a
5441
+ connection that is declared and never configured is the commonest reason an
5442
+ integration silently never fires, and neither list alone can show it. Flags the
5443
+ gap explicitly. Needs integrations:read.
5444
+
5445
+ DIAGNOSE ONE CONNECTION:
5446
+ octwin integrations preflight <key> # every check, with a fix hint. Makes NO
5447
+ # outbound call — needs only integrations:read
5448
+ octwin integrations test <key> # a LIVE call to its health: operation
5449
+ # (integrations:write). Exits 1 when it fails.
5450
+
5451
+ THE DELIVERY LOG:
5452
+ octwin integrations deliveries [--status s] [--operation id] [--limit n]
5453
+ octwin integrations deliveries <id> # + the redacted request/response snapshots
5454
+ octwin integrations retry|cancel|send-now <id> # integrations:write
5455
+ octwin integrations events # INBOUND events (what arrived at your webhook)
5456
+
5457
+ retry/cancel answer 409 when the delivery is in the wrong state; the message
5287
5458
  carries the rule.`,
5288
- journeys: `octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition]
5289
- [--stage <stageId>] [--limit n] [--json]
5290
- No args = the journeys the pack declares. With an id, one of six views —
5291
- --funnel (default) stage-by-stage reach and drop-off · --overview entered vs
5292
- converted plus the biggest drop-off · --goals completions, contacts, value and
5293
- p50 time · --trends per-bucket activity · --cost tokens and dollars per goal ·
5294
- --definition what was DECLARED, unmeasured (the one view that works with no
5295
- traffic). Needs journeys:read.
5296
-
5297
- --stage <stageId> lists the runs sitting at a stage right now (a live snapshot,
5298
- not the funnel's cumulative reached counts).
5299
-
5300
- Same flag grammar as \`octwin analytics\` on purpose: a journey funnel and an
5301
- entity funnel are the same question about different subjects. Journeys carry RBAC
5302
- on top of the scope, so an empty answer can be a missing \`view\` grant rather
5459
+ journeys: `octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition]
5460
+ [--stage <stageId>] [--limit n] [--json]
5461
+ No args = the journeys the pack declares. With an id, one of six views —
5462
+ --funnel (default) stage-by-stage reach and drop-off · --overview entered vs
5463
+ converted plus the biggest drop-off · --goals completions, contacts, value and
5464
+ p50 time · --trends per-bucket activity · --cost tokens and dollars per goal ·
5465
+ --definition what was DECLARED, unmeasured (the one view that works with no
5466
+ traffic). Needs journeys:read.
5467
+
5468
+ --stage <stageId> lists the runs sitting at a stage right now (a live snapshot,
5469
+ not the funnel's cumulative reached counts).
5470
+
5471
+ Same flag grammar as \`octwin analytics\` on purpose: a journey funnel and an
5472
+ entity funnel are the same question about different subjects. Journeys carry RBAC
5473
+ on top of the scope, so an empty answer can be a missing \`view\` grant rather
5303
5474
  than missing data — the output says which causes are possible.`,
5304
- performance: `octwin performance [--detail] [--json]
5305
- The project's business indicators — value produced, conversion, duration — each
5306
- with its delta against the previous window and a \`why\` naming the declaration it
5307
- came from. --detail adds the per-indicator breakdown.
5308
-
5309
- Needs records:read, NOT a performance scope (there is none), so a read-only token
5310
- already reaches it. Indicators are DERIVED: a pack that declares no journey goal
5475
+ performance: `octwin performance [--detail] [--json]
5476
+ The project's business indicators — value produced, conversion, duration — each
5477
+ with its delta against the previous window and a \`why\` naming the declaration it
5478
+ came from. --detail adds the per-indicator breakdown.
5479
+
5480
+ Needs records:read, NOT a performance scope (there is none), so a read-only token
5481
+ already reaches it. Indicators are DERIVED: a pack that declares no journey goal
5311
5482
  value and no pipelined entity produces none, which is a different thing from zero.`,
5312
- usage: `octwin usage [--json]
5313
- Model calls, tokens and cost for the resolved scope — project when one is pinned
5314
- or passed with --project, otherwise the whole workspace. Broken down by model,
5315
- kind, agent and channel.
5316
-
5317
- Needs no particular scope: any valid token reaches it.
5318
-
5319
- This is MODEL spend only. WhatsApp/Meta message billing is operator-only and
5483
+ usage: `octwin usage [--json]
5484
+ Model calls, tokens and cost for the resolved scope — project when one is pinned
5485
+ or passed with --project, otherwise the whole workspace. Broken down by model,
5486
+ kind, agent and channel.
5487
+
5488
+ Needs no particular scope: any valid token reaches it.
5489
+
5490
+ This is MODEL spend only. WhatsApp/Meta message billing is operator-only and
5320
5491
  deliberately outside the token scope registry — no API token can read it.`,
5321
- 'platform-kb': `octwin platform-kb [pull] [--if-stale] [--check] [--dir .] [--url <url>] [--token <t>]
5322
- Pull the platform capability reference (markdown + JSON catalogs) into
5323
- .octwin/platform-kb/ for the octwin-pack authoring skill, plus three maps:
5324
- INDEX.md (the corpus) · SYMBOLS.md (every name -> its file; grep this) ·
5325
- OUTLINE.md (every heading with its line number).
5326
-
5327
- NO TOKEN NEEDED — the reference is platform stdlib and is served anonymously,
5328
- and this command never sends one. --token is accepted and ignored, so an older
5329
- script that passes it keeps working.
5330
-
5331
- --if-stale poll the platform's content_hash first and skip the download when
5332
- nothing changed. Cheap enough to run at the start of every session.
5333
- --check report only, write nothing. Exit 0 = current, 2 = stale or never
5334
- pulled, 1 = could not tell (offline / no reference served). For
5492
+ 'platform-kb': `octwin platform-kb [pull] [--if-stale] [--check] [--dir .] [--url <url>] [--token <t>]
5493
+ Pull the platform capability reference (markdown + JSON catalogs) into
5494
+ .octwin/platform-kb/ for the octwin-pack authoring skill, plus three maps:
5495
+ INDEX.md (the corpus) · SYMBOLS.md (every name -> its file; grep this) ·
5496
+ OUTLINE.md (every heading with its line number).
5497
+
5498
+ NO TOKEN NEEDED — the reference is platform stdlib and is served anonymously,
5499
+ and this command never sends one. --token is accepted and ignored, so an older
5500
+ script that passes it keeps working.
5501
+
5502
+ --if-stale poll the platform's content_hash first and skip the download when
5503
+ nothing changed. Cheap enough to run at the start of every session.
5504
+ --check report only, write nothing. Exit 0 = current, 2 = stale or never
5505
+ pulled, 1 = could not tell (offline / no reference served). For
5335
5506
  scripts and agent loops that want to branch without parsing prose.`,
5336
- test: `octwin test [--dir .]
5507
+ test: `octwin test [--dir .]
5337
5508
  Alias for \`octwin validate --remote\` — the full platform check.`,
5338
- memos: `octwin memos [--all] [--json]
5339
- Read what the platform has told you: a REPLY to a report you sent with
5340
- \`octwin feedback\`, or a NOTICE published to every author (a new capability,
5341
- a deprecation, a breaking change). Bodies are printed in full.
5342
- Reading marks them read, so the reminder stops. --all re-reads history and
5343
- acks nothing. --json to branch on \`severity\`
5509
+ memos: `octwin memos [--all] [--json]
5510
+ Read what the platform has told you: a REPLY to a report you sent with
5511
+ \`octwin feedback\`, or a NOTICE published to every author (a new capability,
5512
+ a deprecation, a breaking change). Bodies are printed in full.
5513
+ Reading marks them read, so the reminder stops. --all re-reads history and
5514
+ acks nothing. --json to branch on \`severity\`
5344
5515
  (info | action_required | breaking).`,
5345
- feedback: `octwin feedback [--dir .]
5346
- Submit this pack's FEEDBACK.md to the platform team.
5347
- The octwin-pack skill writes that file in its last step — findings grouped by
5348
- owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
5349
- you to paste it into a chat.
5350
- Attaches the pack id + version from manifest.yaml, this CLI's version, and the
5351
- content_hash of the capability reference in .octwin/platform-kb/ — triage needs
5352
- the last two to tell "the platform is wrong" from "that was already fixed" or
5516
+ feedback: `octwin feedback [--dir .]
5517
+ Submit this pack's FEEDBACK.md to the platform team.
5518
+ The octwin-pack skill writes that file in its last step — findings grouped by
5519
+ owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
5520
+ you to paste it into a chat.
5521
+ Attaches the pack id + version from manifest.yaml, this CLI's version, and the
5522
+ content_hash of the capability reference in .octwin/platform-kb/ — triage needs
5523
+ the last two to tell "the platform is wrong" from "that was already fixed" or
5353
5524
  "you were reading a stale reference". Needs the \`pack:deploy\` scope.`,
5354
5525
  };
5355
5526
  async function main() {