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 +31 -0
- package/README.md +8 -4
- package/dist/index.js +59 -5
- package/dist/lib/rename.js +12 -41
- package/package.json +1 -1
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
|
|
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`
|
|
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
|
-
|
|
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
|
|
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
|
package/dist/lib/rename.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* rename.ts — rename map + safe substitution over a copied starter tree.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
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
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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