octwin-cli 0.1.20 → 0.1.21

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,37 @@ 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.21] - 2026-07-30
9
+
10
+ ### Added
11
+ - **`deploy` and `status` report the marketplace-listing verdict.** A manifest carrying
12
+ `listing.public: true` put the pack into an operator review queue, and the state existed only in
13
+ the console — so an author working from the CLI got no acknowledgement that the request had
14
+ registered, and never saw a rejection note (which the platform *requires* precisely because it is
15
+ their only feedback). Both commands now print it via `printPublicListing`, and stay silent for a
16
+ pack that never asked. `status` also distinguishes *approved* from *actually public*: an approval
17
+ pins the content sha it reviewed, so any edit returns the pack to the queue on its own.
18
+ - **`octwin pull` is discoverable.** It was implemented and answered `--help`, but appeared in
19
+ neither `octwin help` nor the README — the one command that recovers a deployed pack's only source
20
+ copy was invisible unless you already knew its name. Also added to the scope-requirement table, so
21
+ a 403 names `pack:deploy` instead of printing the generic hint.
22
+
23
+ ### Fixed
24
+ - **The capability-reference drift check no longer fails silently on a narrow token.** The `?meta=1`
25
+ poll needs `pack:deploy`, but it rides on every networked command — so an author inspecting data
26
+ with a `records:read`-only token got no drift signal at all, and a stale reference is exactly what
27
+ leads to inventing a primitive from memory. A 401/403 there now says so once.
28
+ - **README: casework is declared in `worklist.yaml`, not a `cases.yaml`.** That grammar was retired;
29
+ following the README produced a pack whose casework silently never loaded. `octwin media generate`
30
+ was also missing from the command table.
31
+ - **`records` no longer blames the plan for a route the plan does not gate.** `GET /xrm/entities`
32
+ carries only the scope guard, so a 403 on the entity list wrongly cited the `records` plan feature.
33
+
34
+ ### Removed
35
+ - **Unreachable rename branches in `init`.** The agent-id and `main`-flow renames sat behind
36
+ `--agent` / `--flow` options the CLI never parsed, against a template that ships no `main` flow —
37
+ both branches could never fire, and their patterns would have matched nothing if they had.
38
+
8
39
  ## [0.1.20] - 2026-07-29
9
40
 
10
41
  ### Added
package/README.md CHANGED
@@ -64,8 +64,9 @@ octwin status # "✓ live and current" once it's warm
64
64
  | `octwin validate` | Check the pack locally (structure + pure-YAML rules). The platform re-validates the full manifest/flow schema on deploy. |
65
65
  | `octwin login` | Save a deploy token for a platform URL **and make that URL the default target** (both stored in `~/.octwin/credentials.json`). `--url`, `--token`. |
66
66
  | `octwin whoami` | Verify the saved/passed token is valid for a tenant. `--url`, `--tenant`. |
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 version, the **content sha** the instance loaded vs. the one the catalog holds (a redeploy of the *same* version changes it), and its flows. |
67
+ | `octwin deploy` | Upload + install the pack onto your tenant's project. `--seed` also runs the pack's demo seed. Reports the **marketplace-listing verdict** when the manifest carries `listing.public: true`. |
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), its flows, and whether it is live on the public marketplace. |
69
+ | `octwin pull <packId>` | Write a **deployed** pack's source back to disk — the inverse of `deploy`, and how a pack pushed from one machine is recovered. Defaults to the version installed on the target project; `--version` overrides, `--dir` defaults to `./<packId>`, a non-empty dir needs `--force`. You may pull a pack your tenant **owns**. |
69
70
  | `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
71
  | `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
72
  | `octwin records` | Inspect the pack's XRM data (needs a `records:read` token). No args = list entities. |
@@ -75,6 +76,7 @@ octwin status # "✓ live and current" once it's warm
75
76
  | `octwin analytics` | Stage-by-stage conversion for **any** entity declared with a `pipeline:` (`--overview` / `--milestones` / `--trends` / `--cost`; `--stage <id>` lists the records currently at a stage). Needs `records:read`. |
76
77
  | `octwin catalog` | Commerce products with price / availability / stock, plus the WhatsApp catalog binding. `--readiness` runs the Meta Graph checklist. Needs `catalog:read` + the `catalog` plan feature. |
77
78
  | `octwin scheduling` | The scheduling engine's state, or `--slots <resourceRecordId>` for the slots one bookable resource actually computes — how you verify the availability rules `deploy --seed` created. Needs `scheduling:read`. |
79
+ | `octwin media generate "<prompt>"` | AI-generate an image, store it as a public asset, and print its `MEDIA-` handle + serve URL. `--out` downloads the bytes (WhatsApp renders only `.png`/`.jpg`); `--size`; `--json`. Pairs with `octwin chat --media` to drive media-collect flows. Needs `media:generate`. |
78
80
  | `octwin platform-kb pull` | Pull the platform's capability reference into `.octwin/platform-kb/` for the **`octwin-pack`** Claude Code authoring plugin: guides as markdown, plus **one JSON file per capability** (`primitives/record_list.json`, `render-intents/carousel.json`, `declarations/xrm.json`, …) and an **`INDEX.md`** mapping every entry to its file — so a lookup is a small targeted read, not a whole catalog. |
79
81
  | `octwin test` | Alias for `octwin validate --remote` — the platform's full manifest + flow-DSL check. |
80
82
  | `octwin help` | Show usage. Every subcommand also answers `--help`. |
@@ -170,8 +172,10 @@ A pack is **pure declarative data** — `.yaml` / `.yml` / `.md` / `.sql` / `.js
170
172
  code (`.ts`/`.js`), HTTP routes, DB clients, and custom primitives are **not** allowed (this is what
171
173
  makes an external pack safe to run on a shared platform; the server enforces it on deploy). For
172
174
  domain records, use Octwin's first-class storage modules — **XRM** (records with stage pipelines),
173
- **catalog** (products), or **casework** (tickets) — declared in `xrm.yaml` / `cases.yaml`, so a pack
174
- needs **no database of its own**.
175
+ **catalog** (products), or **casework** (tickets) — declared in `xrm.yaml` and `worklist.yaml`, so a
176
+ pack needs **no database of its own**. (Casework rides `worklist.yaml`'s `work.<entity>` block;
177
+ there is no ~~`cases.yaml`~~ grammar. `octwin platform-kb pull` ships the authoritative list of
178
+ declaration files — read its `INDEX.md` rather than this paragraph.)
175
179
 
176
180
  ## Links
177
181
 
package/dist/index.js CHANGED
@@ -135,9 +135,16 @@ const COMMAND_REQUIREMENTS = {
135
135
  validate: { scope: 'pack:deploy' },
136
136
  status: { scope: 'pack:deploy' },
137
137
  test: { scope: 'pack:deploy' },
138
+ // `pull` reads a stored artifact through the deploy route's sibling, so it needs the
139
+ // same scope. Omitting it here meant a 403 on the one command that recovers a pack's
140
+ // only source copy printed the generic hint WITHOUT naming the scope to grant.
141
+ pull: { scope: 'pack:deploy' },
138
142
  'platform-kb': { scope: 'pack:deploy' },
139
143
  media: { scope: 'media:generate' },
140
- records: { scope: 'records:read', feature: 'records' },
144
+ // The plan feature gates RECORD reads, not the entity list (`/xrm/entities` carries only
145
+ // the scope guard) — so the hint says which half it applies to rather than blaming the
146
+ // plan for a 403 the plan did not cause.
147
+ records: { scope: 'records:read', feature: 'records', featureAppliesTo: 'reading records (listing entities needs only the scope)' },
141
148
  analytics: { scope: 'records:read', feature: 'records' },
142
149
  cases: { scope: 'cases:read', feature: 'cases' },
143
150
  logs: { scope: 'conversations:read' },
@@ -158,7 +165,10 @@ function scopeRequirementHint() {
158
165
  const special = req.scope === 'pack:deploy' || req.scope === 'media:generate';
159
166
  return `needs the \`${req.scope}\` scope`
160
167
  + (special ? ' (granted DIRECTLY only — a `tenant:admin` token does not confer it)' : '')
161
- + (req.feature ? `, and the \`${req.feature}\` plan feature on this workspace` : '');
168
+ + (req.feature
169
+ ? `, and the \`${req.feature}\` plan feature on this workspace`
170
+ + (req.featureAppliesTo ? ` for ${req.featureAppliesTo}` : '')
171
+ : '');
162
172
  }
163
173
  /** Print the auth hints below an HTTP-failure line when it's a 401/403 — so every
164
174
  * command explains a token problem, not just the inspect family (author-feedback A7):
@@ -442,8 +452,18 @@ async function notifyIfKbStale(flags) {
442
452
  headers: authHeaders(t), signal: ctrl.signal,
443
453
  });
444
454
  clearTimeout(timer);
445
- if (!res.ok)
455
+ if (!res.ok) {
456
+ // The meta poll needs `pack:deploy`, but this nudge rides on every networked
457
+ // command — so an author inspecting data with a narrow (`records:read`-only) token
458
+ // got NO drift signal at all, silently, and a stale reference is exactly what makes
459
+ // an author invent a primitive from memory. Say so once; stay silent for every other
460
+ // failure (offline, timeout, a platform without the route).
461
+ if (res.status === 401 || res.status === 403) {
462
+ console.error('\nⓘ can\'t check whether the platform capability reference drifted — that needs a `pack:deploy` token.');
463
+ console.error(' Refresh it directly with a deploy token: octwin platform-kb --token oct_…');
464
+ }
446
465
  return;
466
+ }
447
467
  const meta = await res.json();
448
468
  if (meta.content_hash && meta.content_hash !== local.content_hash) {
449
469
  // Per-entry summary (now that the index carries per-entry hashes) — the
@@ -505,8 +525,6 @@ function cmdInit(flags) {
505
525
  cpSync(TEMPLATE_DIR, dir, { recursive: true });
506
526
  applyRenames(dir, {
507
527
  packId: id,
508
- flowId: 'main',
509
- agentId: 'assistant',
510
528
  description: flags.description ?? undefined,
511
529
  displayName: flags['display-name'] ?? undefined,
512
530
  });
@@ -842,6 +860,36 @@ async function readDeployProgress(body) {
842
860
  }
843
861
  return { terminal, stepErrors };
844
862
  }
863
+ /**
864
+ * The anonymous-marketplace verdict, for `deploy` and `status` alike.
865
+ *
866
+ * `listing.public: true` in the manifest puts a pack into an operator review queue, and the
867
+ * state lived only in the console — so an author working from the CLI got no acknowledgement
868
+ * that the request had registered, and no sight of a rejection note (which the platform
869
+ * REQUIRES precisely because it is their only feedback). Silent for `none`/absent, so a pack
870
+ * that never asked prints nothing.
871
+ *
872
+ * `pending` after a redeploy is normal rather than a regression: an approval pins the sha it
873
+ * reviewed, so any content edit returns the pack to the queue on its own.
874
+ */
875
+ function printPublicListing(state, note, live) {
876
+ const n = typeof note === 'string' && note ? ` — operator note: ${note}` : '';
877
+ switch (state) {
878
+ case 'pending':
879
+ console.log(' ⓘ marketplace listing: PENDING operator review (any content edit re-queues it — the approval pins a content sha).');
880
+ break;
881
+ case 'approved':
882
+ console.log(live === false
883
+ ? ' ⓘ marketplace listing: approved, but NOT currently public — the approved content sha no longer matches what you publish. It is back in the review queue.'
884
+ : ' ✓ marketplace listing: live on the public marketplace.');
885
+ break;
886
+ case 'rejected':
887
+ console.log(` ⚠ marketplace listing: REJECTED${n}`);
888
+ break;
889
+ default: // 'none' | null | absent → never asked
890
+ break;
891
+ }
892
+ }
845
893
  function printDeploySuccess(id, version, t, r) {
846
894
  console.log(`✓ Deployed ${id}@${version} and installed onto ${targetLabel(t)}`);
847
895
  if (r?.warning)
@@ -867,6 +915,7 @@ function printDeploySuccess(id, version, t, r) {
867
915
  if (dropped > 0) {
868
916
  console.log(` ⓘ ${dropped} suspended run(s) invalidated — a tap on any card rendered before this deploy will report a stale run. Start those conversations again.`);
869
917
  }
918
+ printPublicListing(r?.public_listing, r?.public_review_note);
870
919
  console.log(`\nChat with it: octwin chat "hi" --as tester (or the web widget / console test page).`);
871
920
  }
872
921
  async function cmdDeploy(flags) {
@@ -974,6 +1023,10 @@ async function cmdStatus(flags) {
974
1023
  else {
975
1024
  console.log('\n✓ live and current.');
976
1025
  }
1026
+ // Independent of the runtime verdict above: a listing decision controls anonymous
1027
+ // visibility only and never touches a running install, so it is reported alongside
1028
+ // rather than folded into that ladder.
1029
+ printPublicListing(json.public_listing, json.public_review_note, json.publicly_listed);
977
1030
  if (localVersion !== '?' && localVersion !== json.installed_version) {
978
1031
  console.log(` (local manifest is ${localVersion}; deployed is ${json.installed_version} — \`octwin deploy\` to push local edits.)`);
979
1032
  }
@@ -2320,6 +2373,7 @@ function help() {
2320
2373
  octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
2321
2374
  octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
2322
2375
  octwin status [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
2376
+ octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
2323
2377
  octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
2324
2378
  octwin cases [caseId] [--queues] [--json] # inspect casework (support tickets) + timelines
2325
2379
  octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
@@ -1,20 +1,21 @@
1
1
  /**
2
2
  * rename.ts — rename map + safe substitution over a copied starter tree.
3
3
  *
4
- * A VENDORED, platform-free copy of the platform scaffolder's rename module
5
- * (`scripts/pack-scaffold/rename.ts`). `octwin init` copies the in-package
6
- * `templates/starter/` tree (packId `starter-kit`, flow `main`, agent
7
- * `assistant`) then this rewrites the copy into a fresh pack:
4
+ * Platform-free by construction the CLI ships with zero platform imports. (It began as a
5
+ * vendored copy of ~~`scripts/pack-scaffold/rename.ts`~~, deleted with the in-repo packs;
6
+ * this is the only copy now.) `octwin init` copies the in-package
7
+ * `templates/starter/` tree (packId `starter-kit`, agent `assistant`, flows
8
+ * `home` + `browse`) then this rewrites the copy into a fresh pack:
8
9
  * • `starter-kit` → <pack-id> (manifest id, comments, README)
9
- * • agent display name + id (when --display-name / --agent given)
10
+ * • agent display name (when --display-name given)
10
11
  * • manifest description (when --description given)
11
- * • the `main` flow → <flow-id> (when --flow given): file renames + scoped edits
12
12
  *
13
- * Flow renaming is scoped to the exact syntactic forms the template uses
14
- * (flow_id, manifest flows/tools entries, `$t("main.…")` / `'main.…'` locale
15
- * namespaces) never a blind `main` substring replace.
13
+ * ~~It also renamed the agent id and a `main` flow~~ behind `--agent` / `--flow` options
14
+ * the CLI never parsed: `octwin init` passes the template's own ids, and the template ships
15
+ * no `main` flow at all, so both branches were unreachable and their patterns would have
16
+ * matched nothing. Renaming a flow is an EDIT after scaffolding, not an init flag.
16
17
  */
17
- import { readdirSync, statSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
18
+ import { readdirSync, statSync, readFileSync, writeFileSync } from 'node:fs';
18
19
  import { join } from 'node:path';
19
20
  const TEXT_EXT = /\.(ya?ml|md|sql|json)$/;
20
21
  function walkFiles(dir) {
@@ -49,30 +50,9 @@ function contentReplacements(opts) {
49
50
  const safe = opts.displayName.replace(/'/g, "''");
50
51
  edits.push([/^(\s*display_name:\s*).*$/m, `$1'${safe}'`]);
51
52
  }
52
- // 4. Agent id — the manifest agents[] list item and the messages.<lang>.yaml key.
53
- if (opts.agentId !== 'assistant') {
54
- edits.push([/^(\s*-\s*id:\s+)assistant\s*$/m, `$1${opts.agentId}`]); // manifest agents[].id
55
- edits.push([/^(\s+)assistant:(\s*)$/m, `$1${opts.agentId}:$2`]); // messages.<lang>.yaml agents.<id>
56
- }
57
- // 5. Flow id — scoped forms only (never a blind `main` substring replace).
58
- if (opts.flowId !== 'main') {
59
- const f = opts.flowId;
60
- edits.push([/^flow_id:(\s*)main\s*$/m, `flow_id:$1${f}`]); // flow + locale headers
61
- edits.push([/^(\s*-\s*)main\s*$/gm, `$1${f}`]); // manifest flows: + tools: entries
62
- edits.push([new RegExp(`(\\$t\\(["'])main\\.`, 'g'), `$1${f}.`]); // $t("main.…")
63
- }
64
53
  return { global, manifestOnly };
65
54
  }
66
- /** File-basename prefix renames (e.g. `main.flow.yaml` `<flow>.flow.yaml`). */
67
- function fileRenames(opts) {
68
- if (opts.flowId === 'main')
69
- return [];
70
- return [['main.', `${opts.flowId}.`]];
71
- }
72
- /**
73
- * Apply all renames in-place to a freshly-copied pack tree at `destDir`.
74
- * Content edits run first, then file renames (so edits see original names).
75
- */
55
+ /** Apply all renames in-place to a freshly-copied pack tree at `destDir`. */
76
56
  export function applyRenames(destDir, opts) {
77
57
  const { global, manifestOnly } = contentReplacements(opts);
78
58
  for (const file of walkFiles(destDir)) {
@@ -91,15 +71,6 @@ export function applyRenames(destDir, opts) {
91
71
  if (changed)
92
72
  writeFileSync(file, text, 'utf8');
93
73
  }
94
- for (const [fromPrefix, toPrefix] of fileRenames(opts)) {
95
- for (const file of walkFiles(destDir)) {
96
- const dir = file.slice(0, file.length - basename(file).length);
97
- const base = basename(file);
98
- if (base.startsWith(fromPrefix)) {
99
- renameSync(file, join(dir, toPrefix + base.slice(fromPrefix.length)));
100
- }
101
- }
102
- }
103
74
  }
104
75
  function basename(p) {
105
76
  const i = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octwin-cli",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
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": {