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 +27 -0
- package/README.md +1 -1
- package/dist/index.js +135 -95
- package/dist/lib/validate.js +50 -4
- package/package.json +1 -1
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.
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
799
|
-
console.log(
|
|
835
|
+
else if (json.up_to_date === false) {
|
|
836
|
+
console.log('\n⚠ this instance is running an OLDER artifact than the catalog holds — it 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() {
|
package/dist/lib/validate.js
CHANGED
|
@@ -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
|
|
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
|
|
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