octwin-cli 0.1.15 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@ Format: [Keep a Changelog](https://keepachangelog.com/) — newest first, bucket
5
5
  **Added · Changed · Deprecated · Removed · Fixed · Security**. The platform-wide view lives in the
6
6
  repo root [`CHANGELOG.md`](../../CHANGELOG.md); this file is the CLI-only cut that ships with the package.
7
7
 
8
+ ## [0.1.16] - 2026-07-26
9
+
10
+ ### Added
11
+ - **A pack can ship committed images.** `jpg`/`jpeg`/`png`/`webp`/`gif`/`pdf` files in your pack
12
+ directory now travel as a separate `blobs` half of the bundle (base64 on the wire, `bytea` in
13
+ storage) instead of being rejected as "not an allowed pack file type". Declare each one under the
14
+ manifest's `static_assets: [{ file, key }]` and reference it from `config:` with the
15
+ `$pack_asset:<key>` sentinel — the platform uploads it to the media system at install and the
16
+ sentinel resolves to the served URL. `octwin validate` reports the image count alongside the file
17
+ count.
18
+ - **The size ceilings fail locally, before upload** — 2 MB per file, 32 MB of binary per pack,
19
+ mirroring the server. `svg` stays rejected: it is script-capable and these assets are served to
20
+ browsers.
21
+
22
+ ### Fixed
23
+ - **Committed images were silently corrupted.** `collectBundleFiles` read *every* file with
24
+ `readFileSync(full, 'utf8')`, so a JPEG went through a lossy UTF-8 decode and arrived mangled
25
+ server-side — a deploy that "succeeded" and rendered a broken image. Binary files now split off
26
+ into `blobs` and keep their bytes.
27
+ - **`octwin status` reported `loaded=(none)` for every pack, always.** It printed a `loaded_version`
28
+ field that stopped existing when reload moved to content-sha keying, which also meant the
29
+ version-drift warning underneath it could never fire. It now prints the **content sha** the
30
+ instance has loaded and the one the catalog holds (with the artifact's `origin`), and warns off the
31
+ platform's own `up_to_date` flag. The sha is the more useful fact anyway: re-publishing the *same*
32
+ version changes it, which is exactly the author's inner loop. A pack that is live and current but
33
+ **withdrawn** from the catalog now says so, rather than reporting a clean bill of health.
34
+
8
35
  ## [0.1.15] - 2026-07-25
9
36
 
10
37
  ### Added
package/README.md CHANGED
@@ -65,7 +65,7 @@ octwin status # "✓ live and current" once it's warm
65
65
  | `octwin login` | Save a deploy token for a platform URL (stored in `~/.octwin/credentials.json`). `--url`, `--token`. |
66
66
  | `octwin whoami` | Verify the saved/passed token is valid for a tenant. `--url`, `--tenant`. |
67
67
  | `octwin deploy` | Upload + install the pack onto your tenant's project. `--seed` also runs the pack's demo seed. |
68
- | `octwin status` | Report what the platform has live for this pack — installed vs. loaded version, and its flows. |
68
+ | `octwin status` | Report what the platform has live for this pack — installed version, the **content sha** the instance loaded vs. the one the catalog holds (a redeploy of the *same* version changes it), and its flows. |
69
69
  | `octwin chat "msg"` | Drive a turn through the dev web channel and print **every render with its tap ids**. `--as <handle>` picks the test user; `--tap "<tap-id>"` presses a rendered button/list row; `--json` dumps the raw envelopes. |
70
70
  | `octwin logs` | List recent conversations (handle, status, last activity; `--as` filters), or show one conversation's full event timeline — including what each turn rendered. `--json` for raw payloads. |
71
71
  | `octwin records` | Inspect the pack's XRM data (needs a `records:read` token). No args = list entities. |
package/dist/index.js CHANGED
@@ -218,9 +218,27 @@ async function resolveMediaPart(url, arg) {
218
218
  }
219
219
  // ── bundle collection ───────────────────────────────────────────────────────
220
220
  const SKIP_DIRS = new Set(['.git', 'node_modules', '.pack-bundles', 'dist', '.mastra']);
221
- /** Collect every text file under `packDir` into a `{ relPath: content }` map. */
221
+ /**
222
+ * Binary extensions the platform accepts as artifact BLOBS (mirrors
223
+ * `ALLOWED_BINARY_EXT` server-side). Deliberately no `svg` — script-capable.
224
+ */
225
+ const BINARY_EXT = new Set(['jpg', 'jpeg', 'png', 'webp', 'gif', 'pdf']);
226
+ /** Per-blob / total ceilings, mirroring the server so oversize fails LOCALLY. */
227
+ const MAX_BLOB_BYTES = 2 * 1024 * 1024;
228
+ const MAX_ARTIFACT_BYTES = 32 * 1024 * 1024;
229
+ /**
230
+ * Collect a pack directory into its two halves: `files` (`{ relPath: utf8 }`) and
231
+ * `blobs` (`{ relPath: base64 }`).
232
+ *
233
+ * Every file used to be read with `readFileSync(full, 'utf8')`, which silently
234
+ * MANGLED any committed image — the bytes went through a lossy UTF-8 decode and
235
+ * arrived corrupt. Binary files now split off into `blobs`, transported as base64
236
+ * (transport only; they land in `bytea` server-side).
237
+ */
222
238
  function collectBundleFiles(packDir) {
223
239
  const files = {};
240
+ const blobs = {};
241
+ let totalBlobBytes = 0;
224
242
  const walk = (dir, prefix) => {
225
243
  for (const name of readdirSync(dir)) {
226
244
  const full = join(dir, name);
@@ -235,11 +253,24 @@ function collectBundleFiles(packDir) {
235
253
  continue; // deploy config, not part of the pack
236
254
  if (name.startsWith('.'))
237
255
  continue; // .gitignore etc. — not pack content
256
+ const ext = name.slice(name.lastIndexOf('.') + 1).toLowerCase();
257
+ if (BINARY_EXT.has(ext)) {
258
+ const buf = readFileSync(full);
259
+ if (buf.byteLength > MAX_BLOB_BYTES) {
260
+ die(`'${rel}' is ${(buf.byteLength / 1024 / 1024).toFixed(1)} MB — the per-file limit is ${MAX_BLOB_BYTES / 1024 / 1024} MB`);
261
+ }
262
+ totalBlobBytes += buf.byteLength;
263
+ blobs[rel] = buf.toString('base64');
264
+ continue;
265
+ }
238
266
  files[rel] = readFileSync(full, 'utf8');
239
267
  }
240
268
  };
241
269
  walk(packDir, '');
242
- return files;
270
+ if (totalBlobBytes > MAX_ARTIFACT_BYTES) {
271
+ die(`binary payload is ${(totalBlobBytes / 1024 / 1024).toFixed(1)} MB — the per-pack limit is ${MAX_ARTIFACT_BYTES / 1024 / 1024} MB`);
272
+ }
273
+ return { files, blobs };
243
274
  }
244
275
  function readManifestIdVersion(files) {
245
276
  const raw = files['manifest.yaml'];
@@ -487,20 +518,20 @@ function cmdInit(flags) {
487
518
  console.log(' octwin deploy');
488
519
  }
489
520
  function localValidate(packDir) {
490
- const files = collectBundleFiles(packDir);
521
+ const { files, blobs } = collectBundleFiles(packDir);
491
522
  const { id, version } = readManifestIdVersion(files);
492
- const r = validatePackBundle(id, files);
523
+ const r = validatePackBundle(id, files, blobs);
493
524
  if (!r.ok) {
494
525
  for (const e of r.errors)
495
526
  console.error(` ✗ ${e}`);
496
527
  die(`bundle validation failed (${r.errors.length} error${r.errors.length === 1 ? '' : 's'})`);
497
528
  }
498
- return { id, version, files };
529
+ return { id, version, files, blobs };
499
530
  }
500
531
  async function cmdValidate(flags) {
501
532
  const packDir = resolve(flags.dir ?? '.');
502
- const { id, version, files } = localValidate(packDir); // offline structural gate first (fast, no server/token)
503
- console.log(`✓ ${id}@${version} passes the offline structural check (${Object.keys(files).length} files)`);
533
+ const { id, version, files, blobs } = localValidate(packDir); // offline structural gate first (fast, no server/token)
534
+ console.log(`✓ ${id}@${version} passes the offline structural check (${Object.keys(files).length} files, ${Object.keys(blobs).length} image(s))`);
504
535
  if (flags.remote !== true) {
505
536
  console.log(' Run `octwin validate --remote` to run the platform\'s FULL manifest + flow-DSL validation');
506
537
  console.log(' (all errors at once) before you deploy.');
@@ -514,7 +545,7 @@ async function cmdValidate(flags) {
514
545
  const res = await fetchOrDie(`${url}/api/self/p/packs/validate`, {
515
546
  method: 'POST',
516
547
  headers: { 'content-type': 'application/json', ...authHeaders(t) },
517
- body: JSON.stringify({ files }),
548
+ body: JSON.stringify({ files, blobs }),
518
549
  }, 'remote validate');
519
550
  const text = await res.text();
520
551
  let json;
@@ -709,7 +740,7 @@ async function cmdDeploy(flags) {
709
740
  const packDir = resolve(flags.dir ?? '.');
710
741
  const t = resolveTarget(flags, packDir);
711
742
  const { url } = t;
712
- const { id, version, files } = localValidate(packDir);
743
+ const { id, version, files, blobs } = localValidate(packDir);
713
744
  const endpoint = `${url}/api/self/p/packs/deploy`;
714
745
  const seed = flags.seed === true;
715
746
  console.log(`→ Deploying ${id}@${version} (${Object.keys(files).length} files) to ${targetLabel(t)}${seed ? ' — with demo seed' : ''} …`);
@@ -718,7 +749,7 @@ async function cmdDeploy(flags) {
718
749
  // Ask for a progress stream; the platform falls back to plain JSON if it
719
750
  // (or an error before any progress) can't stream — handled below.
720
751
  headers: { 'content-type': 'application/json', accept: 'text/event-stream', ...authHeaders(t) },
721
- body: JSON.stringify({ files, seed }),
752
+ body: JSON.stringify({ files, blobs, seed }),
722
753
  }, 'deploy');
723
754
  // Streaming path — live install + seed progress (image generation can take a
724
755
  // while, so `--seed` prints per-record / per-image lines as they happen).
@@ -790,13 +821,22 @@ async function cmdStatus(flags) {
790
821
  }
791
822
  console.log(`${id} on ${targetLabel(t)} @ ${url}`);
792
823
  console.log(` installed version : ${json.installed_version}`);
793
- console.log(` live on instance : registered=${json.registered} source=${json.source} loaded=${json.loaded_version ?? '(none)'}`);
824
+ // Reads the CONTENT SHA, not a version string. It printed `loaded=${json.loaded_version}` a
825
+ // field that stopped existing when reload moved to sha keying, so this line always said
826
+ // `(none)` and the drift warning below could never fire. The sha is also the more useful fact:
827
+ // re-publishing the SAME version changes it, which is exactly the author's inner loop.
828
+ const shortSha = (s) => (typeof s === 'string' && s ? s.slice(0, 12) + '…' : '(none)');
829
+ console.log(` live on instance : registered=${json.registered} loaded=${shortSha(json.loaded_content_sha)}`);
830
+ console.log(` catalog artifact : ${shortSha(json.catalog_content_sha)}${json.origin ? ` origin=${json.origin}` : ''}`);
794
831
  console.log(` flows : ${(json.flows ?? []).join(', ') || '(none)'}`);
795
832
  if (!json.registered) {
796
833
  console.log('\n… not warm on the instance you hit yet — it loads on the next inbound (chat once, then re-check).');
797
834
  }
798
- else if (json.loaded_version && json.installed_version && json.loaded_version !== json.installed_version) {
799
- console.log(`\n⚠ instance has ${json.loaded_version} but the project is bound to ${json.installed_version}a redeploy lands on the next turn.`);
835
+ else if (json.up_to_date === false) {
836
+ console.log('\n⚠ this instance is running an OLDER artifact than the catalog holdsit picks up the current one on the next inbound turn (chat once, then re-check).');
837
+ }
838
+ else if (json.catalog_status === 'withdrawn') {
839
+ console.log('\n⚠ live and current, but the pack is WITHDRAWN from the catalog — existing installs keep running; new installs are refused.');
800
840
  }
801
841
  else {
802
842
  console.log('\n✓ live and current.');
@@ -1894,110 +1934,110 @@ async function cmdScheduling(flags) {
1894
1934
  console.log('\nSlots for one resource: octwin scheduling --slots <resourceRecordId> (ids: octwin records <entity>)');
1895
1935
  }
1896
1936
  function help() {
1897
- console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
1898
-
1899
- octwin --version # print the CLI version (+ any upgrade notice)
1900
- octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
1901
- octwin validate [--dir .] [--remote] # --remote runs the platform's FULL schema check (all errors at once)
1902
- octwin login --url <platformUrl> --token oct_… # a deploy token from the console
1903
- octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
1904
- octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
1905
- octwin status [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
1906
- octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
1907
- octwin cases [caseId] [--queues] [--json] # inspect casework (support tickets) + timelines
1908
- octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
1909
- octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
1910
- octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
1911
- octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
1912
- octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
1913
- octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
1914
- octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
1915
- octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
1916
- octwin platform-kb [pull] [--dir .] [--url <url>] [--tenant <slug>] [--token <t>]
1917
- octwin test [--dir .] # = validate --remote (the full platform check)
1918
-
1919
- Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
1920
- \`octwin chat --as <h>\` calls continue the same conversation; press a rendered
1921
- button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
1922
- Get a deploy token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
1923
- octwin platform-kb pull → writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
1924
- Config (deploy): flags > pack.json > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login.
1937
+ console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
1938
+
1939
+ octwin --version # print the CLI version (+ any upgrade notice)
1940
+ octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
1941
+ octwin validate [--dir .] [--remote] # --remote runs the platform's FULL schema check (all errors at once)
1942
+ octwin login --url <platformUrl> --token oct_… # a deploy token from the console
1943
+ octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
1944
+ octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
1945
+ octwin status [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
1946
+ octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
1947
+ octwin cases [caseId] [--queues] [--json] # inspect casework (support tickets) + timelines
1948
+ octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
1949
+ octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
1950
+ octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
1951
+ octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
1952
+ octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
1953
+ octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
1954
+ octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
1955
+ octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
1956
+ octwin platform-kb [pull] [--dir .] [--url <url>] [--tenant <slug>] [--token <t>]
1957
+ octwin test [--dir .] # = validate --remote (the full platform check)
1958
+
1959
+ Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
1960
+ \`octwin chat --as <h>\` calls continue the same conversation; press a rendered
1961
+ button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
1962
+ Get a deploy token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
1963
+ octwin platform-kb pull → writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
1964
+ Config (deploy): flags > pack.json > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login.
1925
1965
  Per-command usage: octwin <command> --help`);
1926
1966
  }
1927
1967
  /** Per-subcommand usage — printed for `octwin <cmd> --help|-h` BEFORE any
1928
1968
  * network/auth work (a --help that 401s is worse than no help at all). */
1929
1969
  const COMMAND_HELP = {
1930
- init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
1970
+ init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
1931
1971
  Scaffold a pure-YAML starter pack into <dir>.`,
1932
- validate: `octwin validate [--dir .] [--remote]
1933
- Offline structural check; --remote additionally runs the platform's FULL
1972
+ validate: `octwin validate [--dir .] [--remote]
1973
+ Offline structural check; --remote additionally runs the platform's FULL
1934
1974
  manifest + flow-DSL validation (all errors at once) — same check as deploy.`,
1935
- login: `octwin login --url <platformUrl> --token oct_…
1936
- Save a deploy token (console → Settings → API tokens) for that platform url,
1975
+ login: `octwin login --url <platformUrl> --token oct_…
1976
+ Save a deploy token (console → Settings → API tokens) for that platform url,
1937
1977
  and echo the workspace + project pin + scopes the token reaches.`,
1938
- whoami: `octwin whoami [--url <url>] [--tenant <slug>]
1978
+ whoami: `octwin whoami [--url <url>] [--tenant <slug>]
1939
1979
  Verify the resolved token authenticates against the tenant.`,
1940
- deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
1941
- Upload the pack bundle, validate server-side, install onto the project.
1980
+ deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
1981
+ Upload the pack bundle, validate server-side, install onto the project.
1942
1982
  --seed additionally applies the pack's demo seed (streams progress).`,
1943
- status: `octwin status [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
1983
+ status: `octwin status [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
1944
1984
  Show installed vs live version + the flow list for this pack.`,
1945
- records: `octwin records [entity] [id] [--limit 50]
1946
- Inspect the pack's XRM data. No args = list entities. Cases/tickets are
1985
+ records: `octwin records [entity] [id] [--limit 50]
1986
+ Inspect the pack's XRM data. No args = list entities. Cases/tickets are
1947
1987
  casework, not XRM — use \`octwin cases\` for those.`,
1948
- cases: `octwin cases [caseId] [--queues] [--limit 50] [--json]
1949
- Inspect casework (support tickets): the inbox, one case + its timeline
1988
+ cases: `octwin cases [caseId] [--queues] [--limit 50] [--json]
1989
+ Inspect casework (support tickets): the inbox, one case + its timeline
1950
1990
  (+ applicable decisions), or --queues for queue keys + open counts.`,
1951
- logs: `octwin logs [conversationId] [--as <handle>] [--json]
1952
- No id = recent conversations (handle, status, last activity; --as filters).
1953
- With id = the full event timeline including what each turn rendered.
1991
+ logs: `octwin logs [conversationId] [--as <handle>] [--json]
1992
+ No id = recent conversations (handle, status, last activity; --as filters).
1993
+ With id = the full event timeline including what each turn rendered.
1954
1994
  --json = raw events (verbatim payloads).`,
1955
- chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
1956
- Drive one turn through the dev web channel and print every render with its
1957
- tap ids. Same --as handle = same conversation (multi-turn works).
1958
- --tap presses a rendered button/list row instead of sending text.
1959
- --media uploads a local file (or a media id from 'media generate --json') as
1960
- an image/document/audio inbound — any "message" rides as its caption; feeds a
1961
- running media-collect flow (e.g. activate-app).
1995
+ chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
1996
+ Drive one turn through the dev web channel and print every render with its
1997
+ tap ids. Same --as handle = same conversation (multi-turn works).
1998
+ --tap presses a rendered button/list row instead of sending text.
1999
+ --media uploads a local file (or a media id from 'media generate --json') as
2000
+ an image/document/audio inbound — any "message" rides as its caption; feeds a
2001
+ running media-collect flow (e.g. activate-app).
1962
2002
  --json dumps the raw SSE envelopes for the turn.`,
1963
- media: `octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json]
1964
- AI-generate an image (needs a media:generate-scoped token), store it as a
1965
- public asset, and print its MEDIA- handle + serve URL. --out downloads the
1966
- bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
2003
+ media: `octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json]
2004
+ AI-generate an image (needs a media:generate-scoped token), store it as a
2005
+ public asset, and print its MEDIA- handle + serve URL. --out downloads the
2006
+ bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
1967
2007
  width, height, bytes }. Pair with 'octwin chat --media' to drive media flows.`,
1968
- agents: `octwin agents [packId::agentId] [--prompt] [--json]
1969
- No args = the roster with each agent's EFFECTIVE model and which layer set it.
1970
- With an agent = every governed setting (model / memory.last_messages /
1971
- working_memory) plus the layer that won — an operator PLATFORM default can
1972
- override what your manifest declares, and this is where you see that.
1973
- --prompt = the exact system prompt the LLM sees for this project (pack
1974
- instructions + platform protocol + any project overlay). Needs agents:read.
2008
+ agents: `octwin agents [packId::agentId] [--prompt] [--json]
2009
+ No args = the roster with each agent's EFFECTIVE model and which layer set it.
2010
+ With an agent = every governed setting (model / memory.last_messages /
2011
+ working_memory) plus the layer that won — an operator PLATFORM default can
2012
+ override what your manifest declares, and this is where you see that.
2013
+ --prompt = the exact system prompt the LLM sees for this project (pack
2014
+ instructions + platform protocol + any project overlay). Needs agents:read.
1975
2015
  The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.`,
1976
- orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
1977
- No args = the order list (#number, status/payment, total, contact). With a
1978
- reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
1979
- payment_ref, and the allowed status transitions. Needs orders:read + the
1980
- \`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
2016
+ orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
2017
+ No args = the order list (#number, status/payment, total, contact). With a
2018
+ reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
2019
+ payment_ref, and the allowed status transitions. Needs orders:read + the
2020
+ \`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
1981
2021
  so \`pending\` on a gateway-less workspace is expected, not a bug.`,
1982
- analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
1983
- No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
1984
- With an entity = stage-by-stage conversion (default --funnel) over the last 30
1985
- days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
2022
+ analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
2023
+ No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
2024
+ With an entity = stage-by-stage conversion (default --funnel) over the last 30
2025
+ days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
1986
2026
  range-filtered). Needs records:read + a \`view\` grant on \`record.<entity>\`.`,
1987
- catalog: `octwin catalog [--readiness] [--json]
1988
- The commerce \`product\` records + price, availability, stock (null = not
1989
- inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
1990
- Graph checklist (LIVE Graph calls; needs a bound access token). Needs
2027
+ catalog: `octwin catalog [--readiness] [--json]
2028
+ The commerce \`product\` records + price, availability, stock (null = not
2029
+ inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
2030
+ Graph checklist (LIVE Graph calls; needs a bound access token). Needs
1991
2031
  catalog:read + the \`catalog\` plan feature.`,
1992
- scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
1993
- No args = the engine state (bookable resource types, upcoming slots, booked
1994
- seats). --slots <recordId> computes the slots for one bookable resource
1995
- (occupancy included; --days is clamped to 1-31 server-side) — the way to verify
2032
+ scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
2033
+ No args = the engine state (bookable resource types, upcoming slots, booked
2034
+ seats). --slots <recordId> computes the slots for one bookable resource
2035
+ (occupancy included; --days is clamped to 1-31 server-side) — the way to verify
1996
2036
  the availability rules a \`deploy --seed\` created. Needs scheduling:read.`,
1997
- 'platform-kb': `octwin platform-kb [pull] [--dir .] [--url <url>] [--tenant <slug>] [--token <t>]
1998
- Pull the platform capability reference (markdown + JSON catalogs) into
2037
+ 'platform-kb': `octwin platform-kb [pull] [--dir .] [--url <url>] [--tenant <slug>] [--token <t>]
2038
+ Pull the platform capability reference (markdown + JSON catalogs) into
1999
2039
  .octwin/platform-kb/ for the octwin-pack authoring skill.`,
2000
- test: `octwin test [--dir .]
2040
+ test: `octwin test [--dir .]
2001
2041
  Alias for \`octwin validate --remote\` — the full platform check.`,
2002
2042
  };
2003
2043
  async function main() {
@@ -8,9 +8,20 @@
8
8
  * uploading. The **server re-validates authoritatively** on deploy — this local
9
9
  * copy is a fast pre-check, not the source of truth — and it omits the server's
10
10
  * in-repo shadow check (there is no `src/packs` in a developer's own repo).
11
+ *
12
+ * The duplication is deliberate: `octwin-cli` is published to npm and must not
13
+ * import the platform. `validate-parity.test.ts` asserts the two copies agree on
14
+ * every rule, so a divergence fails the build rather than surfacing as "it passed
15
+ * locally but the deploy rejected it".
11
16
  */
12
- /** Declarative file extensions a pure-YAML pack may contain. */
17
+ /** Declarative TEXT extensions a pure-YAML pack may contain. */
13
18
  const ALLOWED_EXT = new Set(['yaml', 'yml', 'md', 'sql', 'json']);
19
+ /** Binary extensions that travel as artifact BLOBS (base64 on the wire, bytea in
20
+ * storage). No `svg` — it is script-capable and these are served to browsers. */
21
+ const ALLOWED_BINARY_EXT = new Set(['jpg', 'jpeg', 'png', 'webp', 'gif', 'pdf']);
22
+ /** Ceilings, mirrored from the server so oversize fails LOCALLY before upload. */
23
+ const MAX_BLOB_BYTES = 2 * 1024 * 1024;
24
+ const MAX_ARTIFACT_BYTES = 32 * 1024 * 1024;
14
25
  /** Extensions/paths that mean executable code or a non-YAML capability — rejected. */
15
26
  const CODE_EXT = new Set(['ts', 'js', 'mjs', 'cjs', 'jsx', 'tsx', 'node', 'wasm', 'sh', 'bash', 'exe', 'py', 'rb']);
16
27
  /** Normalize to forward slashes + strip a leading `./`. */
@@ -28,13 +39,15 @@ function ext(p) {
28
39
  * no path traversal or absolute paths, declarative extensions only. Returns all
29
40
  * violations at once.
30
41
  */
31
- export function validatePackBundle(packId, files) {
42
+ export function validatePackBundle(packId, files,
43
+ /** `{ relPath: base64 }` — the binary half, as the CLI collects it. */
44
+ blobs = {}) {
32
45
  const errors = [];
33
46
  if (!/^[a-z][a-z0-9-]*$/.test(packId)) {
34
47
  errors.push(`pack id '${packId}' must be lowercase ASCII with hyphens (e.g. 'my-pack')`);
35
48
  }
36
49
  const paths = Object.keys(files);
37
- if (paths.length === 0)
50
+ if (paths.length === 0 && Object.keys(blobs).length === 0)
38
51
  errors.push('bundle is empty');
39
52
  if (!paths.some(p => norm(p) === 'manifest.yaml')) {
40
53
  errors.push('bundle is missing manifest.yaml at its root');
@@ -62,10 +75,43 @@ export function validatePackBundle(packId, files) {
62
75
  errors.push(`'${p}': executable code is not allowed (pure-YAML packs only)`);
63
76
  continue;
64
77
  }
78
+ if (ALLOWED_BINARY_EXT.has(e)) {
79
+ errors.push(`'${p}': binary files travel as blobs, not in the text file map`);
80
+ continue;
81
+ }
65
82
  if (!ALLOWED_EXT.has(e)) {
66
- errors.push(`'${p}': not an allowed pack file type (.yaml/.yml/.md/.sql only)`);
83
+ errors.push(`'${p}': not an allowed pack file type (.yaml/.yml/.md/.sql/.json, or an image: ${[...ALLOWED_BINARY_EXT].join('/')})`);
84
+ continue;
85
+ }
86
+ }
87
+ // Blobs — same rules the server applies to the decoded bytes. Sizes are derived
88
+ // from the base64 length (3 bytes per 4 chars, minus padding) so this needs no
89
+ // Buffer and stays platform-free.
90
+ let totalBlobBytes = 0;
91
+ for (const raw of Object.keys(blobs)) {
92
+ const p = norm(raw);
93
+ if (p.startsWith('/') || /^[a-zA-Z]:/.test(p) || p.split('/').includes('..')) {
94
+ errors.push(`unsafe blob path '${raw}' (absolute or traversal)`);
95
+ continue;
96
+ }
97
+ const e = ext(p);
98
+ if (!ALLOWED_BINARY_EXT.has(e)) {
99
+ errors.push(`'${p}': not an allowed image type (${[...ALLOWED_BINARY_EXT].join('/')}; SVG is rejected — it is script-capable)`);
67
100
  continue;
68
101
  }
102
+ const b64 = blobs[raw];
103
+ const size = Math.floor(b64.replace(/=+$/, '').length * 3 / 4);
104
+ totalBlobBytes += size;
105
+ if (size > MAX_BLOB_BYTES) {
106
+ errors.push(`'${p}': ${(size / 1024 / 1024).toFixed(1)} MB exceeds the ${MAX_BLOB_BYTES / 1024 / 1024} MB per-file limit`);
107
+ }
108
+ if (files[raw] !== undefined) {
109
+ errors.push(`'${p}': present as BOTH a text file and a blob — pick one`);
110
+ }
111
+ }
112
+ if (totalBlobBytes > MAX_ARTIFACT_BYTES) {
113
+ errors.push(`binary payload ${(totalBlobBytes / 1024 / 1024).toFixed(1)} MB exceeds the ` +
114
+ `${MAX_ARTIFACT_BYTES / 1024 / 1024} MB per-pack limit`);
69
115
  }
70
116
  return { ok: errors.length === 0, errors };
71
117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octwin-cli",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "Octwin external-pack developer CLI (by CEQUENS) — scaffold, validate, deploy, and check pure-YAML packs on your tenant.",
5
5
  "type": "module",
6
6
  "bin": {