octwin-cli 0.6.0 → 0.6.1
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 +22 -0
- package/README.md +1 -1
- package/dist/index.js +337 -265
- package/dist/lib/net-hint.js +57 -0
- package/dist/lib/render-check.js +44 -6
- package/package.json +37 -37
- package/templates/starter/manifest.yaml +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ 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.6.1] - 2026-08-18
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **`octwin status` worked for no pack at all, and had not since 2026-08-01.** It sent the manifest's
|
|
12
|
+
**bare** id (`clinic`) to a route that requires the qualified `<owner>.<name>` — and a manifest
|
|
13
|
+
cannot legally declare the qualified form, because the owner segment is attached at publish time
|
|
14
|
+
from the authenticated publisher (both the platform and this CLI reject a `.` in it). So the route
|
|
15
|
+
answered 404 for every pack, every time. It now resolves the owner from `whoami`'s `tenant_slug`,
|
|
16
|
+
and takes an explicit `octwin status <packId>` when you already know it (from `octwin agents` or
|
|
17
|
+
`octwin projects`, both of which print qualified ids). Reported by two pack authors, who lost time
|
|
18
|
+
to a command that said their pack was missing seconds after a deploy the platform had accepted.
|
|
19
|
+
- **A 404 from `octwin status` no longer always reads "not installed".** That one branch covered four
|
|
20
|
+
different failures — a malformed pack id, an unknown tenant, an unknown/pinned-elsewhere project,
|
|
21
|
+
and the genuine no-active-install — so the message named the *least* likely cause and sent an author
|
|
22
|
+
chasing a deploy that had already succeeded. The route says which; the CLI now relays it, and adds
|
|
23
|
+
the "run `octwin deploy` first" hint only to the case it fits.
|
|
24
|
+
- **`octwin logs` always reported "No conversations"**, including immediately after an `octwin chat`
|
|
25
|
+
that had plainly worked. It read `conversations` off the response; the platform moved that endpoint
|
|
26
|
+
to the shared page envelope on 2026-08-09 and answers `rows`, which every other list command here
|
|
27
|
+
already reads. Both authors filed this as replica lag or a cache gap — nothing lagged, the payload
|
|
28
|
+
was simply never read. `octwin logs <conversationId>` was unaffected.
|
|
29
|
+
|
|
8
30
|
## [0.6.0] - 2026-08-09
|
|
9
31
|
|
|
10
32
|
### Changed
|
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ octwin status # "✓ live and current" once it's warm
|
|
|
80
80
|
| `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`. |
|
|
81
81
|
| `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. |
|
|
82
82
|
| `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`. |
|
|
83
|
-
| `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`); `--
|
|
83
|
+
| `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`); `--json`. Pairs with `octwin chat --media` to drive media-collect flows. Needs `media:generate`. |
|
|
84
84
|
| `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 **three maps** — `INDEX.md` (the corpus, by family and size), `SYMBOLS.md` (every name → its exact file; grep this), `OUTLINE.md` (every heading with its line number). **No token needed** — the reference is served anonymously. `--if-stale` skips the download when your copy is current (cheap enough for every session); `--check` writes nothing and exits 0 current / 2 stale / 1 could-not-tell. |
|
|
85
85
|
| `octwin test` | Alias for `octwin validate --remote` — the platform's full manifest + flow-DSL check. |
|
|
86
86
|
| `octwin feedback` | Submit this pack's `FEEDBACK.md` to the platform team, with the pack version, your CLI version and the `content_hash` of the capability reference you pulled — the two facts that separate a real platform gap from something already fixed or a stale KB. |
|
package/dist/index.js
CHANGED
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
* octwin projects [--archived] # the --project slugs this token can name
|
|
16
16
|
* octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
17
17
|
* octwin pull <packId> [--dir <out>] [--version v] [--force] # write a DEPLOYED pack's source back to disk
|
|
18
|
-
* octwin status [
|
|
18
|
+
* octwin status [<packId>] # did my deploy land? which version is live?
|
|
19
19
|
* octwin records [entity] [id] # inspect the pack's XRM data (records:read token)
|
|
20
20
|
* octwin work [recordId] [--queues] # inspect the work inbox (worked records) — list / one item + timeline
|
|
21
21
|
* octwin logs [conversationId] [--as h] [--json] # list conversations / show one's timeline
|
|
22
22
|
* octwin chat "msg" [--as h] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn via the web channel (+ send media)
|
|
23
23
|
* octwin chat --script <file> [--as h] # drive a WHOLE conversation, one turn per line (the reliable way to test a flow)
|
|
24
|
-
* octwin media generate "<prompt>" [--out <file.png>] [--
|
|
24
|
+
* octwin media generate "<prompt>" [--out <file.png>] [--json] # AI-generate an image → MEDIA- handle (media:generate scope)
|
|
25
25
|
* octwin agents [packId::agentId] [--prompt] # effective model/memory + which layer won; --prompt = the resolved system prompt
|
|
26
26
|
* octwin orders [reference_id] # the orders a conversation produced — money breakdown + payment state (orders:read)
|
|
27
27
|
* octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion, any pipelined entity
|
|
@@ -62,6 +62,7 @@ import { validatePackBundle, describePackNameProblem, asPackName } from './lib/v
|
|
|
62
62
|
import { loadAllowedRenderKeys, findRenderKeyViolations, describeRenderFinding } from './lib/render-check.js';
|
|
63
63
|
import { loadPrimitiveArgSpecs, findArgViolations, describeArgFinding } from './lib/args-check.js';
|
|
64
64
|
import { yamlLineOf } from './lib/yaml-pos.js';
|
|
65
|
+
import { localhostFamilyHint } from './lib/net-hint.js';
|
|
65
66
|
import { loadBuiltinNames, findBuiltinViolations, describeBuiltinFinding } from './lib/builtin-check.js';
|
|
66
67
|
import { loadTemplateSpecs, findTemplateViolations, describeTemplateFinding } from './lib/template-check.js';
|
|
67
68
|
import { loadSystemEntities, findEntityViolations, describeEntityFinding } from './lib/entity-check.js';
|
|
@@ -164,7 +165,8 @@ async function fetchOrDie(url, init, what) {
|
|
|
164
165
|
return await fetch(url, init);
|
|
165
166
|
}
|
|
166
167
|
catch (err) {
|
|
167
|
-
|
|
168
|
+
const hint = localhostFamilyHint(url, err);
|
|
169
|
+
die(`${what} — platform unreachable at ${url} (${err?.message ?? err})${hint ? `\n ${hint}` : ''}`);
|
|
168
170
|
}
|
|
169
171
|
}
|
|
170
172
|
/** One consistent explanation for auth failures on admin reads. A 401 can also
|
|
@@ -748,7 +750,7 @@ async function cmdValidate(flags) {
|
|
|
748
750
|
const withLines = (fs) => fs.map(f => ({ ...f, line: files[f.file] ? yamlLineOf(files[f.file], f.path) : null }));
|
|
749
751
|
const render = loadAllowedRenderKeys(packDir);
|
|
750
752
|
if (render.keys) {
|
|
751
|
-
const findings = withLines(yamlDocs().flatMap(([p, doc]) => findRenderKeyViolations(doc, p, render.keys)));
|
|
753
|
+
const findings = withLines(yamlDocs().flatMap(([p, doc]) => findRenderKeyViolations(doc, p, render.keys, render.nested)));
|
|
752
754
|
if (findings.length) {
|
|
753
755
|
console.error(`✗ ${findings.length} render-intent field error${findings.length === 1 ? '' : 's'}:`);
|
|
754
756
|
for (const f of findings)
|
|
@@ -896,10 +898,16 @@ async function cmdValidate(flags) {
|
|
|
896
898
|
const t = resolveTarget(flags);
|
|
897
899
|
const { url } = t;
|
|
898
900
|
console.log(`→ Validating against ${targetLabel(t)} @ ${url} …`);
|
|
901
|
+
// `--strict-primitives` (opt-in): the server additionally type-checks LITERAL
|
|
902
|
+
// `args:` values against each primitive's per-key input schema. Expression
|
|
903
|
+
// strings are always exempt — this judges only plain YAML scalars.
|
|
899
904
|
const res = await fetchOrDie(`${url}/api/self/p/packs/validate`, {
|
|
900
905
|
method: 'POST',
|
|
901
906
|
headers: { 'content-type': 'application/json', ...authHeaders(t) },
|
|
902
|
-
body: JSON.stringify({
|
|
907
|
+
body: JSON.stringify({
|
|
908
|
+
files, blobs,
|
|
909
|
+
...(flags['strict-primitives'] === true ? { strict_primitives: true } : {}),
|
|
910
|
+
}),
|
|
903
911
|
}, 'remote validate');
|
|
904
912
|
const text = await res.text();
|
|
905
913
|
let json;
|
|
@@ -1257,13 +1265,13 @@ async function cmdSeed(flags) {
|
|
|
1257
1265
|
const { terminal: final, stepErrors } = await readDeployProgress(res.body);
|
|
1258
1266
|
if (!final || final.stage === 'error')
|
|
1259
1267
|
die(`seed failed${final?.message ? `: ${final.message}` : ' (stream ended early)'}`);
|
|
1260
|
-
console.log(`
|
|
1268
|
+
console.log(`
|
|
1261
1269
|
✓ ${final.message ?? 'seed complete'}`);
|
|
1262
1270
|
printSeedCounts(final.result?.seeded);
|
|
1263
1271
|
if (stepErrors.length) {
|
|
1264
1272
|
// A kind failed but the rest ran — the reconcile softens each step. Say which,
|
|
1265
1273
|
// and exit non-zero so a scripted `seed && chat` doesn't read as clean.
|
|
1266
|
-
console.error(`
|
|
1274
|
+
console.error(`
|
|
1267
1275
|
⚠ ${stepErrors.length} step${stepErrors.length === 1 ? '' : 's'} failed — data may be incomplete:`);
|
|
1268
1276
|
for (const e of stepErrors)
|
|
1269
1277
|
console.error(` • ${e}`);
|
|
@@ -1345,6 +1353,33 @@ async function cmdDeploy(flags) {
|
|
|
1345
1353
|
}
|
|
1346
1354
|
printDeploySuccess(id, version, t, json);
|
|
1347
1355
|
}
|
|
1356
|
+
/**
|
|
1357
|
+
* The QUALIFIED pack id (`<owner>.<name>`) for a manifest's bare name.
|
|
1358
|
+
*
|
|
1359
|
+
* A manifest declares a bare name and cannot legally declare anything else — the platform
|
|
1360
|
+
* and this CLI both reject a `.` in it, because the owner segment is attached at publish
|
|
1361
|
+
* time from the authenticated publisher. Every route that takes a pack id in its PATH
|
|
1362
|
+
* requires the qualified form, so a command holding only a manifest has to ask who it is.
|
|
1363
|
+
*
|
|
1364
|
+
* `whoami`'s `tenant_slug` IS the owner segment. Without this, `octwin status` sent the bare
|
|
1365
|
+
* name, `asPackId` rejected it, and the route's 404 was reported as "not installed" — so the
|
|
1366
|
+
* command was structurally broken for every pack from the day ids gained owners
|
|
1367
|
+
* (2026-08-01) until this was fixed, while the KB still taught it as step 4 of the loop.
|
|
1368
|
+
*/
|
|
1369
|
+
async function qualifiedPackId(t, bareName) {
|
|
1370
|
+
if (bareName.includes('.'))
|
|
1371
|
+
return bareName; // already qualified (explicit override)
|
|
1372
|
+
const res = await fetchOrDie(`${t.url}/api/self/t/whoami`, { headers: authHeaders(t) }, 'workspace lookup');
|
|
1373
|
+
if (!res.ok) {
|
|
1374
|
+
printAuthHint(res.status, t.url);
|
|
1375
|
+
die(`could not resolve your workspace to qualify the pack id (HTTP ${res.status}) — pass the full '<owner>.${bareName}' instead`);
|
|
1376
|
+
}
|
|
1377
|
+
const j = await res.json();
|
|
1378
|
+
if (typeof j.tenant_slug !== 'string' || !j.tenant_slug) {
|
|
1379
|
+
die(`the platform did not report a workspace slug — pass the full '<owner>.${bareName}' instead`);
|
|
1380
|
+
}
|
|
1381
|
+
return `${j.tenant_slug}.${bareName}`;
|
|
1382
|
+
}
|
|
1348
1383
|
async function cmdStatus(flags) {
|
|
1349
1384
|
const packDir = resolve(flags.dir ?? '.');
|
|
1350
1385
|
const t = resolveTarget(flags);
|
|
@@ -1355,8 +1390,11 @@ async function cmdStatus(flags) {
|
|
|
1355
1390
|
const doc = parseYaml(readFileSync(manifestPath, 'utf8'));
|
|
1356
1391
|
if (typeof doc?.id !== 'string')
|
|
1357
1392
|
die('manifest.yaml must declare a string `id`');
|
|
1358
|
-
const
|
|
1393
|
+
const bare = doc.id;
|
|
1359
1394
|
const localVersion = typeof doc?.version === 'string' ? doc.version : '?';
|
|
1395
|
+
// An explicit `octwin status <packId>` wins, so an author who already knows the qualified
|
|
1396
|
+
// id (from `octwin agents` / `octwin projects`) can skip the lookup.
|
|
1397
|
+
const id = await qualifiedPackId(t, flags._[1] ?? bare);
|
|
1360
1398
|
console.log(`→ Checking ${id}@${localVersion} on ${targetLabel(t)} @ ${url} …`);
|
|
1361
1399
|
const res = await fetchOrDie(`${url}/api/self/p/packs/${id}/runtime`, {
|
|
1362
1400
|
headers: authHeaders(t),
|
|
@@ -1370,8 +1408,16 @@ async function cmdStatus(flags) {
|
|
|
1370
1408
|
json = text;
|
|
1371
1409
|
}
|
|
1372
1410
|
if (!res.ok) {
|
|
1373
|
-
|
|
1374
|
-
|
|
1411
|
+
// A 404 here means one of four different things, and collapsing them into "not
|
|
1412
|
+
// installed" sent one author chasing a deploy that had already succeeded. The route
|
|
1413
|
+
// says which; relay it and only add the deploy hint to the case it fits.
|
|
1414
|
+
if (res.status === 404) {
|
|
1415
|
+
const why = typeof json === 'object' && json !== null && typeof json.error === 'string' ? json.error : text;
|
|
1416
|
+
const notInstalled = /no active install|not installed/i.test(why);
|
|
1417
|
+
die(notInstalled
|
|
1418
|
+
? `${why}\n Run \`octwin deploy\` first.`
|
|
1419
|
+
: `status check failed (HTTP 404) — ${why}`);
|
|
1420
|
+
}
|
|
1375
1421
|
console.error(`✗ status check failed (HTTP ${res.status})`);
|
|
1376
1422
|
printAuthHint(res.status, url);
|
|
1377
1423
|
console.error(typeof json === 'string' ? json : JSON.stringify(json, null, 2));
|
|
@@ -2028,7 +2074,11 @@ async function cmdLogs(flags) {
|
|
|
2028
2074
|
const { status, json } = await apiGet(`${base}/conversations?limit=50`, t);
|
|
2029
2075
|
if (status !== 200)
|
|
2030
2076
|
die(`could not read conversations (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
2031
|
-
|
|
2077
|
+
// `rows` — the shared `makePage` envelope every other list command here reads. This one
|
|
2078
|
+
// read `json.conversations`, a noun the route stopped emitting on 2026-08-09, so `convs`
|
|
2079
|
+
// was ALWAYS empty and the command reported "No conversations yet" seconds after a chat
|
|
2080
|
+
// that had plainly worked. Two pack authors filed it as a replica-lag bug; nothing lagged.
|
|
2081
|
+
let convs = (json?.rows ?? []);
|
|
2032
2082
|
if (asHandle)
|
|
2033
2083
|
convs = convs.filter((c) => c.contact?.channel_contact_handle === asHandle);
|
|
2034
2084
|
if (convs.length === 0) {
|
|
@@ -2426,29 +2476,32 @@ async function cmdChat(flags) {
|
|
|
2426
2476
|
}
|
|
2427
2477
|
console.log(`\n(same --as '${from}' continues this conversation — timeline: octwin logs --as ${from})`);
|
|
2428
2478
|
}
|
|
2429
|
-
/** `octwin media generate "<prompt>" [--out <file.png>] [--
|
|
2479
|
+
/** `octwin media generate "<prompt>" [--out <file.png>] [--json]`
|
|
2430
2480
|
* — AI-generate an image on the platform (needs a `media:generate`-scoped token),
|
|
2431
2481
|
* store it as a public asset, and return its `MEDIA-` handle + serve URL. `--out`
|
|
2432
2482
|
* downloads the bytes to a file (WhatsApp renders only `.png`/`.jpg`); the paired
|
|
2433
|
-
* `octwin chat --media <file|id>` feeds it into a running media-collect flow.
|
|
2483
|
+
* `octwin chat --media <file|id>` feeds it into a running media-collect flow.
|
|
2484
|
+
*
|
|
2485
|
+
* A `--size` flag was accepted until 2026-08-10. The platform never forwarded it
|
|
2486
|
+
* to the provider, and stored resolution is decided at ingest regardless, so it
|
|
2487
|
+
* only ever changed the (fabricated) width/height the command printed back. */
|
|
2434
2488
|
async function cmdMedia(flags) {
|
|
2435
2489
|
const sub = flags._[0];
|
|
2436
2490
|
if (sub !== 'generate')
|
|
2437
|
-
die('usage: octwin media generate "<prompt>" [--out <file.png>] [--
|
|
2491
|
+
die('usage: octwin media generate "<prompt>" [--out <file.png>] [--json]');
|
|
2438
2492
|
const t = resolveTarget(flags);
|
|
2439
2493
|
const { url } = t;
|
|
2440
2494
|
const prompt = flags._[1];
|
|
2441
2495
|
if (!prompt)
|
|
2442
|
-
die('usage: octwin media generate "<prompt>" [--out <file.png>] [--
|
|
2496
|
+
die('usage: octwin media generate "<prompt>" [--out <file.png>] [--json]');
|
|
2443
2497
|
const asJson = flags.json === true;
|
|
2444
|
-
const size = typeof flags.size === 'string' ? flags.size : undefined;
|
|
2445
2498
|
const out = typeof flags.out === 'string' ? flags.out : undefined;
|
|
2446
2499
|
if (!asJson)
|
|
2447
2500
|
console.log(`→ Generating an image on ${targetLabel(t)} @ ${url} …`);
|
|
2448
2501
|
const res = await fetchOrDie(`${url}/api/self/p/media/generate`, {
|
|
2449
2502
|
method: 'POST',
|
|
2450
2503
|
headers: { 'content-type': 'application/json', ...authHeaders(t) },
|
|
2451
|
-
body: JSON.stringify({ prompt
|
|
2504
|
+
body: JSON.stringify({ prompt }),
|
|
2452
2505
|
}, 'media generate');
|
|
2453
2506
|
const text = await res.text();
|
|
2454
2507
|
if (!res.ok) {
|
|
@@ -2477,10 +2530,10 @@ async function cmdMedia(flags) {
|
|
|
2477
2530
|
}
|
|
2478
2531
|
}
|
|
2479
2532
|
if (asJson) {
|
|
2480
|
-
console.log(JSON.stringify({ media_id: r.media_id, url: absUrl, mime: r.mime,
|
|
2533
|
+
console.log(JSON.stringify({ media_id: r.media_id, url: absUrl, mime: r.mime, bytes: r.bytes }));
|
|
2481
2534
|
return;
|
|
2482
2535
|
}
|
|
2483
|
-
console.log(`✓ Generated ${r.media_ref} (${r.
|
|
2536
|
+
console.log(`✓ Generated ${r.media_ref} (${r.mime}, ${r.bytes} bytes)`);
|
|
2484
2537
|
console.log(` id: ${r.media_id}`);
|
|
2485
2538
|
console.log(` url: ${absUrl}`);
|
|
2486
2539
|
if (out)
|
|
@@ -3514,7 +3567,7 @@ async function cmdSchedulingWrite(flags) {
|
|
|
3514
3567
|
console.log(JSON.stringify(json, null, 2));
|
|
3515
3568
|
return;
|
|
3516
3569
|
}
|
|
3517
|
-
if (json?.
|
|
3570
|
+
if (json?.has_data === false) {
|
|
3518
3571
|
console.log('This pack declares no scheduling.');
|
|
3519
3572
|
return;
|
|
3520
3573
|
}
|
|
@@ -3589,7 +3642,7 @@ async function cmdSchedulingWrite(flags) {
|
|
|
3589
3642
|
const { status, json } = await apiSend('POST', `${base}/availability/${path}`, body, t);
|
|
3590
3643
|
if (status !== 201 && status !== 200)
|
|
3591
3644
|
writeFail(`add the ${noun}`, status, json, url);
|
|
3592
|
-
if (json?.
|
|
3645
|
+
if (json?.has_data === false)
|
|
3593
3646
|
die('this pack declares no scheduling');
|
|
3594
3647
|
const created = json?.rule ?? json?.exception ?? {};
|
|
3595
3648
|
console.log(`✓ ${noun[0].toUpperCase()}${noun.slice(1)} added — ${created.id ?? '(no id returned)'}`);
|
|
@@ -3610,30 +3663,42 @@ async function cmdScheduling(flags) {
|
|
|
3610
3663
|
if (flags.slots === true)
|
|
3611
3664
|
die('usage: octwin scheduling --slots <resourceRecordId> (the record id of a bookable resource)');
|
|
3612
3665
|
if (resourceId) {
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3666
|
+
// The range is `from`/`to`; `--days` is kept as the ergonomic flag and converted
|
|
3667
|
+
// here, since a CLI user thinks in "the next 7 days".
|
|
3668
|
+
const from = typeof flags.from === 'string' ? flags.from : new Date().toISOString().slice(0, 10);
|
|
3669
|
+
const days = typeof flags.days === 'string' ? Math.max(Number(flags.days) || 1, 1) : 7;
|
|
3670
|
+
const to = new Date(Date.parse(`${from}T00:00:00Z`) + (days - 1) * 86_400_000).toISOString().slice(0, 10);
|
|
3671
|
+
const q = new URLSearchParams({ include_booked: '1', from, to }); // full occupancy, as the operator preview does
|
|
3672
|
+
q.append('resource_id', resourceId);
|
|
3618
3673
|
if (!asJson)
|
|
3619
3674
|
console.log(`→ Computing slots for resource ${resourceId} in ${targetLabel(t)} …`);
|
|
3620
|
-
const { status, json } = await apiGet(`${base}/
|
|
3621
|
-
if (status === 404)
|
|
3622
|
-
die(`resource '${resourceId}' not found (pass an XRM record id — \`octwin records <entity>\` lists them)`);
|
|
3675
|
+
const { status, json } = await apiGet(`${base}/slots?${q.toString()}`, t);
|
|
3623
3676
|
if (status === 400)
|
|
3624
|
-
die(`
|
|
3677
|
+
die(`could not compute slots${errDetail(json)}`);
|
|
3625
3678
|
if (status !== 200)
|
|
3626
3679
|
die(`could not compute slots (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
3627
3680
|
if (asJson) {
|
|
3628
3681
|
console.log(JSON.stringify(json, null, 2));
|
|
3629
3682
|
return;
|
|
3630
3683
|
}
|
|
3631
|
-
if (json?.
|
|
3684
|
+
if (json?.has_data === false) {
|
|
3632
3685
|
console.log('This pack declares no `scheduling.yaml` — nothing to schedule.');
|
|
3633
3686
|
return;
|
|
3634
3687
|
}
|
|
3635
|
-
|
|
3636
|
-
|
|
3688
|
+
// A resource the server could not use comes back named, with a reason, rather
|
|
3689
|
+
// than as an HTTP status — one call may carry several resources.
|
|
3690
|
+
const bad = (json?.unresolved ?? []).find((u) => u.resource_id === resourceId);
|
|
3691
|
+
if (bad?.reason === 'not_found') {
|
|
3692
|
+
die(`resource '${resourceId}' not found (pass an XRM record id — \`octwin records <entity>\` lists them)`);
|
|
3693
|
+
}
|
|
3694
|
+
if (bad?.reason === 'not_bookable')
|
|
3695
|
+
die(`that record isn't a bookable resource`);
|
|
3696
|
+
const res = (json?.resources ?? [])[0];
|
|
3697
|
+
const slots = (res?.slots ?? []);
|
|
3698
|
+
console.log(`Slots for ${resourceId} (timezone ${res?.timezone ?? '?'} — from ${res?.timezone_source ?? '?'}): ${slots.length}`);
|
|
3699
|
+
if (res?.timezone_source === 'pack_default' && json?.pack_timezone_declared === false) {
|
|
3700
|
+
console.log(' ⚠ The pack declares no `timezone:`, so these are UTC — a clock nobody chose.');
|
|
3701
|
+
}
|
|
3637
3702
|
if (slots.length === 0)
|
|
3638
3703
|
console.log(' (none — no availability rules cover this window; `octwin deploy --seed` seeds the demo rules)');
|
|
3639
3704
|
for (const s of slots) {
|
|
@@ -3650,7 +3715,7 @@ async function cmdScheduling(flags) {
|
|
|
3650
3715
|
console.log(JSON.stringify(json, null, 2));
|
|
3651
3716
|
return;
|
|
3652
3717
|
}
|
|
3653
|
-
if (json?.
|
|
3718
|
+
if (json?.has_data === false) {
|
|
3654
3719
|
console.log('This pack declares no `scheduling.yaml` — nothing to schedule.');
|
|
3655
3720
|
return;
|
|
3656
3721
|
}
|
|
@@ -3662,260 +3727,267 @@ async function cmdScheduling(flags) {
|
|
|
3662
3727
|
console.log('\nSlots for one resource: octwin scheduling --slots <resourceRecordId> (ids: octwin records <entity>)');
|
|
3663
3728
|
}
|
|
3664
3729
|
function help() {
|
|
3665
|
-
console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
|
|
3666
|
-
|
|
3667
|
-
octwin --version # print the CLI version (+ any upgrade notice)
|
|
3668
|
-
octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
3669
|
-
octwin validate [--dir .] [--remote] [--require-kb] # --remote runs the platform's FULL schema check + lint (all errors at once)
|
|
3670
|
-
octwin login --url <platformUrl> --token oct_… # a deploy token from the console
|
|
3671
|
-
octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
|
|
3672
|
-
octwin projects [--archived] [--json] # the --project slugs this token can name
|
|
3673
|
-
octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
3674
|
-
octwin status [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
|
|
3675
|
-
octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
|
|
3676
|
-
octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
|
|
3677
|
-
octwin work [recordId] [--queues] [--json] # inspect the work inbox (worked records) + timelines
|
|
3678
|
-
octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
|
|
3679
|
-
octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
|
|
3680
|
-
octwin media generate "<prompt>" [--out <file.png>] [--
|
|
3681
|
-
octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
|
|
3682
|
-
octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
|
|
3683
|
-
octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
|
|
3684
|
-
octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
|
|
3685
|
-
octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
|
|
3686
|
-
octwin platform-kb [pull] [--if-stale|--check] [--dir .] [--url <url>] # no token needed
|
|
3687
|
-
octwin test [--dir .] # = validate --remote (the full platform check)
|
|
3688
|
-
octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
|
|
3689
|
-
|
|
3690
|
-
Writes — exercise the state your pack creates (each needs the matching :write scope):
|
|
3691
|
-
octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…"
|
|
3692
|
-
octwin records tasks | task complete <taskId> [--outcome done|cancelled]
|
|
3693
|
-
octwin work assign <id> --to user:<uuid>|none | note <id> "…" | stage <id> --to <stage>
|
|
3694
|
-
octwin work decide <id> --action <a> [--param k=v] [--dry-run] # --dry-run previews, commits nothing
|
|
3695
|
-
octwin orders transition <ref> --to <status> | refund <ref> --force
|
|
3696
|
-
octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
|
|
3697
|
-
octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
|
|
3698
|
-
octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
|
|
3699
|
-
|
|
3700
|
-
Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
|
|
3701
|
-
\`octwin chat --as <h>\` calls continue the same conversation; press a rendered
|
|
3702
|
-
button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
|
|
3703
|
-
Get a deploy token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
|
|
3704
|
-
octwin platform-kb pull → writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
|
|
3705
|
-
Config (deploy): flags > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login (\`octwin login\` sets the default target).
|
|
3730
|
+
console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
|
|
3731
|
+
|
|
3732
|
+
octwin --version # print the CLI version (+ any upgrade notice)
|
|
3733
|
+
octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
3734
|
+
octwin validate [--dir .] [--remote] [--require-kb] # --remote runs the platform's FULL schema check + lint (all errors at once)
|
|
3735
|
+
octwin login --url <platformUrl> --token oct_… # a deploy token from the console
|
|
3736
|
+
octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
|
|
3737
|
+
octwin projects [--archived] [--json] # the --project slugs this token can name
|
|
3738
|
+
octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
3739
|
+
octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
|
|
3740
|
+
octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
|
|
3741
|
+
octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
|
|
3742
|
+
octwin work [recordId] [--queues] [--json] # inspect the work inbox (worked records) + timelines
|
|
3743
|
+
octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
|
|
3744
|
+
octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
|
|
3745
|
+
octwin media generate "<prompt>" [--out <file.png>] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
|
|
3746
|
+
octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
|
|
3747
|
+
octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
|
|
3748
|
+
octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
|
|
3749
|
+
octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
|
|
3750
|
+
octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
|
|
3751
|
+
octwin platform-kb [pull] [--if-stale|--check] [--dir .] [--url <url>] # no token needed
|
|
3752
|
+
octwin test [--dir .] # = validate --remote (the full platform check)
|
|
3753
|
+
octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
|
|
3754
|
+
|
|
3755
|
+
Writes — exercise the state your pack creates (each needs the matching :write scope):
|
|
3756
|
+
octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…"
|
|
3757
|
+
octwin records tasks | task complete <taskId> [--outcome done|cancelled]
|
|
3758
|
+
octwin work assign <id> --to user:<uuid>|none | note <id> "…" | stage <id> --to <stage>
|
|
3759
|
+
octwin work decide <id> --action <a> [--param k=v] [--dry-run] # --dry-run previews, commits nothing
|
|
3760
|
+
octwin orders transition <ref> --to <status> | refund <ref> --force
|
|
3761
|
+
octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
|
|
3762
|
+
octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
|
|
3763
|
+
octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
|
|
3764
|
+
|
|
3765
|
+
Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
|
|
3766
|
+
\`octwin chat --as <h>\` calls continue the same conversation; press a rendered
|
|
3767
|
+
button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
|
|
3768
|
+
Get a deploy token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
|
|
3769
|
+
octwin platform-kb pull → writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
|
|
3770
|
+
Config (deploy): flags > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login (\`octwin login\` sets the default target).
|
|
3706
3771
|
Per-command usage: octwin <command> --help`);
|
|
3707
3772
|
}
|
|
3708
3773
|
/** Per-subcommand usage — printed for `octwin <cmd> --help|-h` BEFORE any
|
|
3709
3774
|
* network/auth work (a --help that 401s is worse than no help at all). */
|
|
3710
3775
|
const COMMAND_HELP = {
|
|
3711
|
-
init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
3776
|
+
init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
3712
3777
|
Scaffold a pure-YAML starter pack into <dir>.`,
|
|
3713
|
-
validate: `octwin validate [--dir .] [--remote] [--require-kb]
|
|
3714
|
-
Offline structural check, plus two checks driven by the pulled capability
|
|
3715
|
-
reference (render-intent fields, primitive arguments). Those two SKIP when the
|
|
3716
|
-
reference is missing — the run says so, and --require-kb turns the skip into a
|
|
3717
|
-
failure for CI. --remote additionally runs the platform's FULL manifest +
|
|
3718
|
-
flow-DSL validation and its flow lint (all errors at once) — same check as deploy
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3778
|
+
validate: `octwin validate [--dir .] [--remote] [--require-kb] [--strict-primitives]
|
|
3779
|
+
Offline structural check, plus two checks driven by the pulled capability
|
|
3780
|
+
reference (render-intent fields, primitive arguments). Those two SKIP when the
|
|
3781
|
+
reference is missing — the run says so, and --require-kb turns the skip into a
|
|
3782
|
+
failure for CI. --remote additionally runs the platform's FULL manifest +
|
|
3783
|
+
flow-DSL validation and its flow lint (all errors at once) — same check as deploy.
|
|
3784
|
+
--strict-primitives (with --remote) additionally type-checks LITERAL args:
|
|
3785
|
+
values against each primitive's declared input schema; expression strings
|
|
3786
|
+
('$found.id', '{$t(…)}') are always exempt.`,
|
|
3787
|
+
login: `octwin login --url <platformUrl> --token oct_…
|
|
3788
|
+
Save a deploy token (console → Settings → API tokens) for that platform url,
|
|
3789
|
+
make that url the DEFAULT deploy target for every later command, and echo the
|
|
3722
3790
|
workspace + project pin + scopes the token reaches.`,
|
|
3723
|
-
whoami: `octwin whoami [--url <url>] [--tenant <slug>]
|
|
3791
|
+
whoami: `octwin whoami [--url <url>] [--tenant <slug>]
|
|
3724
3792
|
Verify the resolved token authenticates against the tenant.`,
|
|
3725
|
-
projects: `octwin projects [--archived] [--json]
|
|
3726
|
-
List the workspace's projects — the slugs every --project flag takes, with the
|
|
3727
|
-
plan's project cap. --archived includes archived ones. A pack:deploy token
|
|
3728
|
-
reaches this (it names a project in every other command).
|
|
3729
|
-
|
|
3730
|
-
octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
|
|
3731
|
-
Create a project. The URL slug is derived from the name unless --slug pins one.
|
|
3732
|
-
--pack installs an ALREADY-published pack; the usual next step is instead
|
|
3733
|
-
\`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
|
|
3734
|
-
|
|
3735
|
-
octwin projects rm <slug> [--yes]
|
|
3736
|
-
HARD delete — the project and everything cascading from it (conversations,
|
|
3737
|
-
contacts, records, installs). No undo, and not the same as archiving.
|
|
3738
|
-
WITHOUT --yes it only previews what would be destroyed, so the dry run is the
|
|
3739
|
-
default. Together these make a disposable end-to-end environment:
|
|
3740
|
-
octwin projects create "Scratch" && octwin deploy --project scratch --seed
|
|
3741
|
-
octwin chat "hi" --project scratch
|
|
3742
|
-
octwin projects rm scratch --yes
|
|
3793
|
+
projects: `octwin projects [--archived] [--json]
|
|
3794
|
+
List the workspace's projects — the slugs every --project flag takes, with the
|
|
3795
|
+
plan's project cap. --archived includes archived ones. A pack:deploy token
|
|
3796
|
+
reaches this (it names a project in every other command).
|
|
3797
|
+
|
|
3798
|
+
octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
|
|
3799
|
+
Create a project. The URL slug is derived from the name unless --slug pins one.
|
|
3800
|
+
--pack installs an ALREADY-published pack; the usual next step is instead
|
|
3801
|
+
\`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
|
|
3802
|
+
|
|
3803
|
+
octwin projects rm <slug> [--yes]
|
|
3804
|
+
HARD delete — the project and everything cascading from it (conversations,
|
|
3805
|
+
contacts, records, installs). No undo, and not the same as archiving.
|
|
3806
|
+
WITHOUT --yes it only previews what would be destroyed, so the dry run is the
|
|
3807
|
+
default. Together these make a disposable end-to-end environment:
|
|
3808
|
+
octwin projects create "Scratch" && octwin deploy --project scratch --seed
|
|
3809
|
+
octwin chat "hi" --project scratch
|
|
3810
|
+
octwin projects rm scratch --yes
|
|
3743
3811
|
Both verbs need the \`projects:write\` scope — a pack:deploy token does NOT confer it.`,
|
|
3744
|
-
deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
3745
|
-
Upload the pack bundle, validate server-side, install onto the project.
|
|
3812
|
+
deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
3813
|
+
Upload the pack bundle, validate server-side, install onto the project.
|
|
3746
3814
|
--seed additionally applies the pack's demo seed (streams progress).`,
|
|
3747
|
-
seed: `octwin seed [--pack <packId>]
|
|
3748
|
-
Apply the pack's demo/reference data to the project it is installed on, without
|
|
3749
|
-
redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
|
|
3750
|
-
and the demo operator topology. Reports what each kind produced.
|
|
3751
|
-
Idempotent and safe to re-run — records upsert, and existing media is REUSED rather
|
|
3752
|
-
than regenerated, so a second pass costs nothing. --pack is only needed when a
|
|
3815
|
+
seed: `octwin seed [--pack <packId>]
|
|
3816
|
+
Apply the pack's demo/reference data to the project it is installed on, without
|
|
3817
|
+
redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
|
|
3818
|
+
and the demo operator topology. Reports what each kind produced.
|
|
3819
|
+
Idempotent and safe to re-run — records upsert, and existing media is REUSED rather
|
|
3820
|
+
than regenerated, so a second pass costs nothing. --pack is only needed when a
|
|
3753
3821
|
project somehow runs more than one.`,
|
|
3754
|
-
status: `octwin status [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
|
|
3755
|
-
Show installed vs live version + the flow list for this pack
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
octwin records
|
|
3766
|
-
octwin records
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3822
|
+
status: `octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
|
|
3823
|
+
Show installed vs live version + the flow list for this pack.
|
|
3824
|
+
The pack id is read from manifest.yaml and QUALIFIED with your workspace slug
|
|
3825
|
+
(a manifest declares a bare name; the owner is attached when you publish). Pass
|
|
3826
|
+
<packId> explicitly to skip that lookup — \`octwin agents\` and \`octwin projects\`
|
|
3827
|
+
both print the qualified form.`,
|
|
3828
|
+
records: `octwin records [entity] [id] [--limit 50] [--offset n]
|
|
3829
|
+
Inspect the pack's XRM data. No args = list entities. Worked records (cases,
|
|
3830
|
+
tickets, anything routed to a queue) read best through \`octwin work\`.
|
|
3831
|
+
|
|
3832
|
+
WRITES (need \`records:write\`; every one is re-checked by RBAC on the record):
|
|
3833
|
+
octwin records create <entity> --set field=value [--set …] [--stage s] [--contact <id>]
|
|
3834
|
+
octwin records patch <recordId> --entity <entity> --set field=value
|
|
3835
|
+
octwin records stage <recordId> --to <stage> [--note "..."]
|
|
3836
|
+
octwin records note <recordId> "the note text"
|
|
3837
|
+
octwin records tasks # open follow-up tasks (\`tasks\` plan feature)
|
|
3838
|
+
octwin records task complete <taskId> [--outcome done|cancelled] [--note "..."]
|
|
3839
|
+
|
|
3840
|
+
--set coerces JSON scalars: \`--set rating=4.5\` sends a number, \`--set x=null\`
|
|
3841
|
+
sends null. Use --fields-json '{"a":{"b":1}}' for anything nested.
|
|
3842
|
+
\`patch\` needs --entity even though it has an id: the route resolves the field
|
|
3843
|
+
validator from it. A leading \`create/patch/stage/note/tasks/task\` is read as a
|
|
3772
3844
|
VERB — to list an entity actually named one of those, use \`--entity <name>\`.`,
|
|
3773
|
-
work: `octwin work [recordId] [--queues] [--limit 50] [--offset n] [--json]
|
|
3774
|
-
Inspect the work inbox — every entity the pack declares worked (cases, orders
|
|
3775
|
-
needing review, applications, …): the inbox, one item + its timeline
|
|
3776
|
-
(+ applicable actions), or --queues for queue keys + open counts.
|
|
3777
|
-
|
|
3778
|
-
WRITES (need \`work:write\`; \`stage\` needs \`records:write\`):
|
|
3779
|
-
octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none
|
|
3780
|
-
octwin work note <recordId> "the note text"
|
|
3781
|
-
octwin work stage <recordId> --to <stage> [--note "..."]
|
|
3782
|
-
octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]
|
|
3783
|
-
|
|
3784
|
-
\`decide\` applies one of the entity's declared operator actions — \`octwin work <id>\`
|
|
3785
|
-
lists them with their params. --dry-run previews the customer-facing copy and the
|
|
3786
|
-
resulting stage WITHOUT committing (that route needs only \`work:read\`).
|
|
3845
|
+
work: `octwin work [recordId] [--queues] [--limit 50] [--offset n] [--json]
|
|
3846
|
+
Inspect the work inbox — every entity the pack declares worked (cases, orders
|
|
3847
|
+
needing review, applications, …): the inbox, one item + its timeline
|
|
3848
|
+
(+ applicable actions), or --queues for queue keys + open counts.
|
|
3849
|
+
|
|
3850
|
+
WRITES (need \`work:write\`; \`stage\` needs \`records:write\`):
|
|
3851
|
+
octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none
|
|
3852
|
+
octwin work note <recordId> "the note text"
|
|
3853
|
+
octwin work stage <recordId> --to <stage> [--note "..."]
|
|
3854
|
+
octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]
|
|
3855
|
+
|
|
3856
|
+
\`decide\` applies one of the entity's declared operator actions — \`octwin work <id>\`
|
|
3857
|
+
lists them with their params. --dry-run previews the customer-facing copy and the
|
|
3858
|
+
resulting stage WITHOUT committing (that route needs only \`work:read\`).
|
|
3787
3859
|
\`stage\` is the XRM records verb (one transition spelling platform-wide).`,
|
|
3788
|
-
logs: `octwin logs [conversationId] [--as <handle>] [--json]
|
|
3789
|
-
No id = recent conversations (handle, status, last activity; --as filters).
|
|
3790
|
-
With id = the full event timeline including what each turn rendered.
|
|
3860
|
+
logs: `octwin logs [conversationId] [--as <handle>] [--json]
|
|
3861
|
+
No id = recent conversations (handle, status, last activity; --as filters).
|
|
3862
|
+
With id = the full event timeline including what each turn rendered.
|
|
3791
3863
|
--json = raw events (verbatim payloads).`,
|
|
3792
|
-
pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
|
|
3793
|
-
Write a DEPLOYED pack's source back to disk — the inverse of deploy.
|
|
3794
|
-
A pack pushed with 'octwin deploy' lives on the platform as an artifact the
|
|
3795
|
-
runtime serves but nothing hands back, so its only source copy is the machine
|
|
3796
|
-
that pushed it. Pull it, fix it, redeploy it.
|
|
3797
|
-
Defaults to the version installed on the target project; --version overrides.
|
|
3798
|
-
--dir defaults to ./<packId>; a non-empty dir needs --force.
|
|
3799
|
-
The pulled dir redeploys where it came from — the target is your saved login.
|
|
3864
|
+
pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
|
|
3865
|
+
Write a DEPLOYED pack's source back to disk — the inverse of deploy.
|
|
3866
|
+
A pack pushed with 'octwin deploy' lives on the platform as an artifact the
|
|
3867
|
+
runtime serves but nothing hands back, so its only source copy is the machine
|
|
3868
|
+
that pushed it. Pull it, fix it, redeploy it.
|
|
3869
|
+
Defaults to the version installed on the target project; --version overrides.
|
|
3870
|
+
--dir defaults to ./<packId>; a non-empty dir needs --force.
|
|
3871
|
+
The pulled dir redeploys where it came from — the target is your saved login.
|
|
3800
3872
|
You may pull a pack your tenant OWNS (deployed); an operator token pulls any.`,
|
|
3801
|
-
chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
|
|
3802
|
-
octwin chat --script <file> [--as <handle>] [--json]
|
|
3803
|
-
Drive ONE turn through the dev web channel and print every render with its
|
|
3804
|
-
tap ids. Same --as handle = same conversation (multi-turn works).
|
|
3805
|
-
--tap presses a rendered button/list row instead of sending text.
|
|
3806
|
-
--media uploads a local file (or a media id from 'media generate --json') as
|
|
3807
|
-
an image/document/audio inbound — any "message" rides as its caption; feeds a
|
|
3808
|
-
running media-collect flow (e.g. activate-app).
|
|
3809
|
-
--json dumps the raw SSE envelopes for the turn.
|
|
3810
|
-
|
|
3811
|
-
--script drives a WHOLE conversation from a file, ONE TURN PER LINE, in one
|
|
3812
|
-
process over one connection — waiting for each turn to settle before sending
|
|
3813
|
-
the next. Use this for any multi-step flow: chaining shell invocations races
|
|
3814
|
-
the agent loop, because a turn ends on a quiet gap that can arrive while the
|
|
3815
|
-
server is still working (the symptom is placeholder-filled fields or a second
|
|
3816
|
-
workflow run). Blank lines and # comments are skipped:
|
|
3817
|
-
|
|
3818
|
-
# book an appointment end to end
|
|
3819
|
-
احجز موعد
|
|
3820
|
-
tap:t:invoke:book-appointment:doctor_id=D1
|
|
3821
|
-
media:./licence.jpg | here is my licence
|
|
3873
|
+
chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
|
|
3874
|
+
octwin chat --script <file> [--as <handle>] [--json]
|
|
3875
|
+
Drive ONE turn through the dev web channel and print every render with its
|
|
3876
|
+
tap ids. Same --as handle = same conversation (multi-turn works).
|
|
3877
|
+
--tap presses a rendered button/list row instead of sending text.
|
|
3878
|
+
--media uploads a local file (or a media id from 'media generate --json') as
|
|
3879
|
+
an image/document/audio inbound — any "message" rides as its caption; feeds a
|
|
3880
|
+
running media-collect flow (e.g. activate-app).
|
|
3881
|
+
--json dumps the raw SSE envelopes for the turn.
|
|
3882
|
+
|
|
3883
|
+
--script drives a WHOLE conversation from a file, ONE TURN PER LINE, in one
|
|
3884
|
+
process over one connection — waiting for each turn to settle before sending
|
|
3885
|
+
the next. Use this for any multi-step flow: chaining shell invocations races
|
|
3886
|
+
the agent loop, because a turn ends on a quiet gap that can arrive while the
|
|
3887
|
+
server is still working (the symptom is placeholder-filled fields or a second
|
|
3888
|
+
workflow run). Blank lines and # comments are skipped:
|
|
3889
|
+
|
|
3890
|
+
# book an appointment end to end
|
|
3891
|
+
احجز موعد
|
|
3892
|
+
tap:t:invoke:book-appointment:doctor_id=D1
|
|
3893
|
+
media:./licence.jpg | here is my licence
|
|
3822
3894
|
tap:t:resume:book-appointment:run_id=R1;_ctl_approved=true`,
|
|
3823
|
-
media: `octwin media generate "<prompt>" [--out <file.png>] [--
|
|
3824
|
-
AI-generate an image (needs a media:generate-scoped token), store it as a
|
|
3825
|
-
public asset, and print its MEDIA- handle + serve URL. --out downloads the
|
|
3826
|
-
bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
|
|
3827
|
-
|
|
3828
|
-
agents: `octwin agents [packId::agentId] [--prompt] [--json]
|
|
3829
|
-
No args = the roster with each agent's EFFECTIVE model and which layer set it.
|
|
3830
|
-
With an agent = every governed setting (model / memory.last_messages /
|
|
3831
|
-
working_memory) plus the layer that won — an operator PLATFORM default can
|
|
3832
|
-
override what your manifest declares, and this is where you see that.
|
|
3833
|
-
--prompt = the exact system prompt the LLM sees for this project (pack
|
|
3834
|
-
instructions + platform protocol + any project overlay). Needs agents:read.
|
|
3835
|
-
The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
|
|
3836
|
-
|
|
3837
|
-
WRITES (need \`agents:write\`):
|
|
3838
|
-
octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
|
|
3839
|
-
[--enable-tool <toolId>] [--disable-tool <toolId>]
|
|
3840
|
-
|
|
3841
|
-
Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
|
|
3842
|
-
so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
|
|
3895
|
+
media: `octwin media generate "<prompt>" [--out <file.png>] [--json]
|
|
3896
|
+
AI-generate an image (needs a media:generate-scoped token), store it as a
|
|
3897
|
+
public asset, and print its MEDIA- handle + serve URL. --out downloads the
|
|
3898
|
+
bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
|
|
3899
|
+
bytes }. Pair with 'octwin chat --media' to drive media flows.`,
|
|
3900
|
+
agents: `octwin agents [packId::agentId] [--prompt] [--json]
|
|
3901
|
+
No args = the roster with each agent's EFFECTIVE model and which layer set it.
|
|
3902
|
+
With an agent = every governed setting (model / memory.last_messages /
|
|
3903
|
+
working_memory) plus the layer that won — an operator PLATFORM default can
|
|
3904
|
+
override what your manifest declares, and this is where you see that.
|
|
3905
|
+
--prompt = the exact system prompt the LLM sees for this project (pack
|
|
3906
|
+
instructions + platform protocol + any project overlay). Needs agents:read.
|
|
3907
|
+
The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
|
|
3908
|
+
|
|
3909
|
+
WRITES (need \`agents:write\`):
|
|
3910
|
+
octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
|
|
3911
|
+
[--enable-tool <toolId>] [--disable-tool <toolId>]
|
|
3912
|
+
|
|
3913
|
+
Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
|
|
3914
|
+
so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
|
|
3843
3915
|
ids refuses --model with a 403 — the platform default governs there.`,
|
|
3844
|
-
orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
|
|
3845
|
-
No args = the order list (#number, status/payment, total, contact). With a
|
|
3846
|
-
reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
|
|
3847
|
-
payment_ref, and the allowed status transitions. Needs orders:read + the
|
|
3848
|
-
\`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
|
|
3849
|
-
so \`pending\` on a gateway-less workspace is expected, not a bug.
|
|
3850
|
-
|
|
3851
|
-
WRITES (need \`orders:write\`):
|
|
3852
|
-
octwin orders transition <reference_id> --to <status>
|
|
3853
|
-
octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
|
|
3854
|
-
|
|
3855
|
-
Refund is irreversible and moves money, hence --force. The route answers 200 even
|
|
3856
|
-
when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
|
|
3857
|
-
on a refusal rather than reporting a refund that never happened. Only a payment in
|
|
3916
|
+
orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
|
|
3917
|
+
No args = the order list (#number, status/payment, total, contact). With a
|
|
3918
|
+
reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
|
|
3919
|
+
payment_ref, and the allowed status transitions. Needs orders:read + the
|
|
3920
|
+
\`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
|
|
3921
|
+
so \`pending\` on a gateway-less workspace is expected, not a bug.
|
|
3922
|
+
|
|
3923
|
+
WRITES (need \`orders:write\`):
|
|
3924
|
+
octwin orders transition <reference_id> --to <status>
|
|
3925
|
+
octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
|
|
3926
|
+
|
|
3927
|
+
Refund is irreversible and moves money, hence --force. The route answers 200 even
|
|
3928
|
+
when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
|
|
3929
|
+
on a refusal rather than reporting a refund that never happened. Only a payment in
|
|
3858
3930
|
\`captured\` state can be refunded; \`payment_status\` is never settable directly.`,
|
|
3859
|
-
analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
|
|
3860
|
-
No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
|
|
3861
|
-
With an entity = stage-by-stage conversion (default --funnel) over the last 30
|
|
3862
|
-
days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
|
|
3931
|
+
analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
|
|
3932
|
+
No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
|
|
3933
|
+
With an entity = stage-by-stage conversion (default --funnel) over the last 30
|
|
3934
|
+
days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
|
|
3863
3935
|
range-filtered). Needs records:read + a \`view\` grant on \`record.<entity>\`.`,
|
|
3864
|
-
catalog: `octwin catalog [--readiness] [--json]
|
|
3865
|
-
The commerce \`product\` records + price, availability, stock (null = not
|
|
3866
|
-
inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
|
|
3867
|
-
Graph checklist (LIVE Graph calls; needs a bound access token). Needs
|
|
3868
|
-
catalog:read + the \`catalog\` plan feature.
|
|
3869
|
-
|
|
3870
|
-
WRITES (need \`catalog:write\`):
|
|
3871
|
-
octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
|
|
3872
|
-
octwin catalog stock <retailerId> [--set-on-hand <n>]
|
|
3873
|
-
|
|
3874
|
-
\`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
|
|
3875
|
-
inventory-tracked (always sellable), which is different from 0. Lowering on_hand
|
|
3876
|
-
below the units already reserved for open carts is refused. Creating/deleting
|
|
3936
|
+
catalog: `octwin catalog [--readiness] [--json]
|
|
3937
|
+
The commerce \`product\` records + price, availability, stock (null = not
|
|
3938
|
+
inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
|
|
3939
|
+
Graph checklist (LIVE Graph calls; needs a bound access token). Needs
|
|
3940
|
+
catalog:read + the \`catalog\` plan feature.
|
|
3941
|
+
|
|
3942
|
+
WRITES (need \`catalog:write\`):
|
|
3943
|
+
octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
|
|
3944
|
+
octwin catalog stock <retailerId> [--set-on-hand <n>]
|
|
3945
|
+
|
|
3946
|
+
\`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
|
|
3947
|
+
inventory-tracked (always sellable), which is different from 0. Lowering on_hand
|
|
3948
|
+
below the units already reserved for open carts is refused. Creating/deleting
|
|
3877
3949
|
products and the Meta catalog binding/sync stay in the console.`,
|
|
3878
|
-
scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
|
|
3879
|
-
No args = the engine state (bookable resource types, upcoming slots, booked
|
|
3880
|
-
seats). --slots <recordId> computes the slots for one bookable resource
|
|
3881
|
-
(occupancy included; --days is clamped to 1-31 server-side) — the way to verify
|
|
3882
|
-
the availability rules a \`deploy --seed\` created. Needs scheduling:read.
|
|
3883
|
-
|
|
3884
|
-
RULES (list needs scheduling:read; add/rm need scheduling:write):
|
|
3885
|
-
octwin scheduling rules --resource <resourceRecordId>
|
|
3886
|
-
octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
|
|
3887
|
-
[--slot-minutes 30] [--capacity 1]
|
|
3888
|
-
octwin scheduling rule rm <ruleId>
|
|
3889
|
-
octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
|
|
3890
|
-
[--start 09:00 --end 13:00]
|
|
3891
|
-
octwin scheduling exception rm <exceptionId>
|
|
3892
|
-
|
|
3893
|
-
--dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
|
|
3950
|
+
scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
|
|
3951
|
+
No args = the engine state (bookable resource types, upcoming slots, booked
|
|
3952
|
+
seats). --slots <recordId> computes the slots for one bookable resource
|
|
3953
|
+
(occupancy included; --days is clamped to 1-31 server-side) — the way to verify
|
|
3954
|
+
the availability rules a \`deploy --seed\` created. Needs scheduling:read.
|
|
3955
|
+
|
|
3956
|
+
RULES (list needs scheduling:read; add/rm need scheduling:write):
|
|
3957
|
+
octwin scheduling rules --resource <resourceRecordId>
|
|
3958
|
+
octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
|
|
3959
|
+
[--slot-minutes 30] [--capacity 1]
|
|
3960
|
+
octwin scheduling rule rm <ruleId>
|
|
3961
|
+
octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
|
|
3962
|
+
[--start 09:00 --end 13:00]
|
|
3963
|
+
octwin scheduling exception rm <exceptionId>
|
|
3964
|
+
|
|
3965
|
+
--dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
|
|
3894
3966
|
\`--slots\` is how you check what a rule actually produces.`,
|
|
3895
|
-
'platform-kb': `octwin platform-kb [pull] [--if-stale] [--check] [--dir .] [--url <url>] [--token <t>]
|
|
3896
|
-
Pull the platform capability reference (markdown + JSON catalogs) into
|
|
3897
|
-
.octwin/platform-kb/ for the octwin-pack authoring skill, plus three maps:
|
|
3898
|
-
INDEX.md (the corpus) · SYMBOLS.md (every name -> its file; grep this) ·
|
|
3899
|
-
OUTLINE.md (every heading with its line number).
|
|
3900
|
-
|
|
3901
|
-
NO TOKEN NEEDED — the reference is platform stdlib and is served anonymously.
|
|
3902
|
-
A token is used when you have one (it also works against older platforms).
|
|
3903
|
-
|
|
3904
|
-
--if-stale poll the platform's content_hash first and skip the download when
|
|
3905
|
-
nothing changed. Cheap enough to run at the start of every session.
|
|
3906
|
-
--check report only, write nothing. Exit 0 = current, 2 = stale or never
|
|
3907
|
-
pulled, 1 = could not tell (offline / refused). For scripts and
|
|
3967
|
+
'platform-kb': `octwin platform-kb [pull] [--if-stale] [--check] [--dir .] [--url <url>] [--token <t>]
|
|
3968
|
+
Pull the platform capability reference (markdown + JSON catalogs) into
|
|
3969
|
+
.octwin/platform-kb/ for the octwin-pack authoring skill, plus three maps:
|
|
3970
|
+
INDEX.md (the corpus) · SYMBOLS.md (every name -> its file; grep this) ·
|
|
3971
|
+
OUTLINE.md (every heading with its line number).
|
|
3972
|
+
|
|
3973
|
+
NO TOKEN NEEDED — the reference is platform stdlib and is served anonymously.
|
|
3974
|
+
A token is used when you have one (it also works against older platforms).
|
|
3975
|
+
|
|
3976
|
+
--if-stale poll the platform's content_hash first and skip the download when
|
|
3977
|
+
nothing changed. Cheap enough to run at the start of every session.
|
|
3978
|
+
--check report only, write nothing. Exit 0 = current, 2 = stale or never
|
|
3979
|
+
pulled, 1 = could not tell (offline / refused). For scripts and
|
|
3908
3980
|
agent loops that want to branch without parsing prose.`,
|
|
3909
|
-
test: `octwin test [--dir .]
|
|
3981
|
+
test: `octwin test [--dir .]
|
|
3910
3982
|
Alias for \`octwin validate --remote\` — the full platform check.`,
|
|
3911
|
-
feedback: `octwin feedback [--dir .]
|
|
3912
|
-
Submit this pack's FEEDBACK.md to the platform team.
|
|
3913
|
-
The octwin-pack skill writes that file in its last step — findings grouped by
|
|
3914
|
-
owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
|
|
3915
|
-
you to paste it into a chat.
|
|
3916
|
-
Attaches the pack id + version from manifest.yaml, this CLI's version, and the
|
|
3917
|
-
content_hash of the capability reference in .octwin/platform-kb/ — triage needs
|
|
3918
|
-
the last two to tell "the platform is wrong" from "that was already fixed" or
|
|
3983
|
+
feedback: `octwin feedback [--dir .]
|
|
3984
|
+
Submit this pack's FEEDBACK.md to the platform team.
|
|
3985
|
+
The octwin-pack skill writes that file in its last step — findings grouped by
|
|
3986
|
+
owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
|
|
3987
|
+
you to paste it into a chat.
|
|
3988
|
+
Attaches the pack id + version from manifest.yaml, this CLI's version, and the
|
|
3989
|
+
content_hash of the capability reference in .octwin/platform-kb/ — triage needs
|
|
3990
|
+
the last two to tell "the platform is wrong" from "that was already fixed" or
|
|
3919
3991
|
"you were reading a stale reference". Needs the \`pack:deploy\` scope.`,
|
|
3920
3992
|
};
|
|
3921
3993
|
async function main() {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn "the platform is unreachable" into a hint the author can act on.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* `fetch('http://localhost:3000')` can be refused while the platform is running
|
|
7
|
+
* and answering. The server binds IPv4-only (`host: '0.0.0.0'` in `src/server.ts`),
|
|
8
|
+
* Node resolves `localhost` to `::1` first, and whether the connection then falls
|
|
9
|
+
* back to `127.0.0.1` is decided by Node's `autoSelectFamily`, NOT by our code.
|
|
10
|
+
*
|
|
11
|
+
* Measured on node v24.12.0, fetching an IPv4-only bind over `localhost`:
|
|
12
|
+
*
|
|
13
|
+
* autoSelectFamily=true (the default here) -> 200
|
|
14
|
+
* autoSelectFamily=false -> fetch failed / ECONNREFUSED
|
|
15
|
+
*
|
|
16
|
+
* So on a runtime where that default is off — or with
|
|
17
|
+
* `--no-network-family-autoselection` / that flag in `NODE_OPTIONS` — every command
|
|
18
|
+
* reports the platform as down while `--url http://127.0.0.1:3000` and `curl` both
|
|
19
|
+
* work. That reading is what costs the time: a pack author's first act is to run the
|
|
20
|
+
* platform locally, and "unreachable" sends them to look at the server.
|
|
21
|
+
*
|
|
22
|
+
* This module only explains; it changes no addresses. Rewriting the host inside
|
|
23
|
+
* `readTarget` would also change the key saved credentials are stored under, and
|
|
24
|
+
* moving the server to `host: '::'` can fail to bind where IPv6 is disabled — so
|
|
25
|
+
* neither belongs in a diagnostic path.
|
|
26
|
+
*/
|
|
27
|
+
/** A `localhost` HTTP(S) origin — the only case where the IPv4/IPv6 split applies. */
|
|
28
|
+
function isLocalhostUrl(url) {
|
|
29
|
+
try {
|
|
30
|
+
return new URL(url).hostname.toLowerCase() === 'localhost';
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false; // not a URL we can reason about; no hint to give
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Did the connection get refused, as opposed to timing out / DNS / TLS? */
|
|
37
|
+
function isConnectionRefused(err) {
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
for (let e = err; e && !seen.has(e); e = e.cause) {
|
|
40
|
+
seen.add(e);
|
|
41
|
+
if (e.code === 'ECONNREFUSED' || e.code === 'ECONNRESET')
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The extra sentence to append when a failure looks like the IPv6-`localhost`
|
|
48
|
+
* trap, or `null` when it does not — a hint printed on unrelated failures (a
|
|
49
|
+
* genuinely stopped server, a wrong port) is worse than none, because it sends
|
|
50
|
+
* the author to fix an address that was never the problem.
|
|
51
|
+
*/
|
|
52
|
+
export function localhostFamilyHint(url, err) {
|
|
53
|
+
if (!isLocalhostUrl(url) || !isConnectionRefused(err))
|
|
54
|
+
return null;
|
|
55
|
+
const ipv4 = url.replace(/(^https?:\/\/)localhost\b/i, '$1127.0.0.1');
|
|
56
|
+
return `if the platform IS running, this is the IPv6 'localhost' trap — Node tried ::1 and the server binds IPv4 only. Retry with --url ${ipv4}`;
|
|
57
|
+
}
|
package/dist/lib/render-check.js
CHANGED
|
@@ -30,9 +30,10 @@ import { lookupKbSubdir, isEntryFile } from './kb-path.js';
|
|
|
30
30
|
* "checked, nothing wrong" from "could not check". See `kb-path.ts`.
|
|
31
31
|
*/
|
|
32
32
|
export function loadAllowedRenderKeys(packDir) {
|
|
33
|
+
const nested = new Map();
|
|
33
34
|
const lookup = lookupKbSubdir(packDir, 'render-intents', dir => readdirSync(dir).some(isEntryFile));
|
|
34
35
|
if (lookup.state !== 'ok')
|
|
35
|
-
return { lookup, keys: null };
|
|
36
|
+
return { lookup, keys: null, nested };
|
|
36
37
|
const dir = lookup.dir;
|
|
37
38
|
const out = new Map();
|
|
38
39
|
try {
|
|
@@ -42,16 +43,20 @@ export function loadAllowedRenderKeys(packDir) {
|
|
|
42
43
|
const entry = JSON.parse(readFileSync(join(dir, file), 'utf8'));
|
|
43
44
|
if (entry.render_intent && Array.isArray(entry.allowed_keys)) {
|
|
44
45
|
out.set(entry.render_intent, entry.allowed_keys);
|
|
46
|
+
// Optional: a KB pulled before 2026-08-09 has no nested sets, and the nested check simply
|
|
47
|
+
// does not run for that intent. Absent is skipped, never treated as "nothing is allowed".
|
|
48
|
+
if (entry.nested_allowed_keys)
|
|
49
|
+
nested.set(entry.render_intent, entry.nested_allowed_keys);
|
|
45
50
|
}
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
53
|
catch (err) {
|
|
49
|
-
return { lookup: { state: 'malformed', dir, reason: String(err?.message ?? err) }, keys: null };
|
|
54
|
+
return { lookup: { state: 'malformed', dir, reason: String(err?.message ?? err) }, keys: null, nested };
|
|
50
55
|
}
|
|
51
56
|
if (out.size === 0) {
|
|
52
|
-
return { lookup: { state: 'malformed', dir, reason: 'no render-intent entries parsed' }, keys: null };
|
|
57
|
+
return { lookup: { state: 'malformed', dir, reason: 'no render-intent entries parsed' }, keys: null, nested };
|
|
53
58
|
}
|
|
54
|
-
return { lookup, keys: out };
|
|
59
|
+
return { lookup, keys: out, nested };
|
|
55
60
|
}
|
|
56
61
|
/**
|
|
57
62
|
* Walk parsed YAML for objects carrying `render_intent` and report keys outside
|
|
@@ -59,8 +64,32 @@ export function loadAllowedRenderKeys(packDir) {
|
|
|
59
64
|
* nodes: intents nest (an `auto_collection` carries `empty`/`single`/`multi`
|
|
60
65
|
* sub-intents), and a stray key is just as invisible there.
|
|
61
66
|
*/
|
|
62
|
-
export function findRenderKeyViolations(doc, file, allowedByIntent) {
|
|
67
|
+
export function findRenderKeyViolations(doc, file, allowedByIntent, nestedByIntent = new Map()) {
|
|
63
68
|
const findings = [];
|
|
69
|
+
/**
|
|
70
|
+
* Descend a declared nested path, mirroring the platform's `checkNested`.
|
|
71
|
+
*
|
|
72
|
+
* Descends only where the KB DECLARES a shape, and skips non-objects — so an expression string
|
|
73
|
+
* (`item_template: '$tpl'`) and an undeclared bag (`items:`, pack-supplied records) are both left
|
|
74
|
+
* alone, exactly as at load time.
|
|
75
|
+
*/
|
|
76
|
+
const walkNested = (value, intent, at, sets, path) => {
|
|
77
|
+
if (Array.isArray(value)) {
|
|
78
|
+
value.forEach((v, i) => walkNested(v, intent, `${at}[]`, sets, [...path, i]));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!value || typeof value !== 'object')
|
|
82
|
+
return;
|
|
83
|
+
const obj = value;
|
|
84
|
+
const allowed = sets[at];
|
|
85
|
+
if (allowed) {
|
|
86
|
+
const bad = Object.keys(obj).filter(k => !allowed.includes(k));
|
|
87
|
+
if (bad.length)
|
|
88
|
+
findings.push({ file, path: [...path], intent, keys: bad, allowed, nestedAt: at });
|
|
89
|
+
}
|
|
90
|
+
for (const [k, v] of Object.entries(obj))
|
|
91
|
+
walkNested(v, intent, `${at}.${k}`, sets, [...path, k]);
|
|
92
|
+
};
|
|
64
93
|
const walk = (node, path) => {
|
|
65
94
|
if (Array.isArray(node)) {
|
|
66
95
|
node.forEach((v, i) => walk(v, [...path, i]));
|
|
@@ -78,6 +107,14 @@ export function findRenderKeyViolations(doc, file, allowedByIntent) {
|
|
|
78
107
|
const bad = Object.keys(obj).filter(k => !allowed.includes(k));
|
|
79
108
|
if (bad.length)
|
|
80
109
|
findings.push({ file, path: [...path], intent: obj.render_intent, keys: bad, allowed });
|
|
110
|
+
const sets = nestedByIntent.get(obj.render_intent);
|
|
111
|
+
if (sets) {
|
|
112
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
113
|
+
if (k === 'render_intent' || !allowed.includes(k))
|
|
114
|
+
continue;
|
|
115
|
+
walkNested(v, obj.render_intent, k, sets, [...path, k]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
81
118
|
}
|
|
82
119
|
}
|
|
83
120
|
for (const [k, v] of Object.entries(obj))
|
|
@@ -93,6 +130,7 @@ export function describeRenderFinding(f) {
|
|
|
93
130
|
return `${f.file}${at}: unknown render_intent '${f.intent}' — known intents: ${f.allowed.join(', ')}`;
|
|
94
131
|
}
|
|
95
132
|
const plural = f.keys.length === 1 ? 'field' : 'fields';
|
|
96
|
-
|
|
133
|
+
const where = f.nestedAt ? `'${f.intent}' at '${f.nestedAt}'` : `'${f.intent}'`;
|
|
134
|
+
return `${f.file}${at}: render_intent ${where} has unknown ${plural} ${f.keys.map(k => `'${k}'`).join(', ')} ` +
|
|
97
135
|
`— silently dropped at render. Allowed: ${f.allowed.join(', ')}`;
|
|
98
136
|
}
|
package/package.json
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "octwin-cli",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "Octwin external-pack developer CLI (by CEQUENS) — scaffold, validate, deploy, and check pure-YAML packs on your tenant.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"octwin": "dist/index.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist",
|
|
11
|
-
"templates",
|
|
12
|
-
"README.md",
|
|
13
|
-
"CHANGELOG.md",
|
|
14
|
-
"LICENSE"
|
|
15
|
-
],
|
|
16
|
-
"engines": {
|
|
17
|
-
"node": ">=
|
|
18
|
-
},
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "tsc -p tsconfig.json",
|
|
21
|
-
"prepublishOnly": "npm run build"
|
|
22
|
-
},
|
|
23
|
-
"dependencies": {
|
|
24
|
-
"yaml": "^2.6.0"
|
|
25
|
-
},
|
|
26
|
-
"devDependencies": {
|
|
27
|
-
"@types/node": "^22.0.0",
|
|
28
|
-
"typescript": "^5.7.0"
|
|
29
|
-
},
|
|
30
|
-
"publishConfig": {
|
|
31
|
-
"access": "public"
|
|
32
|
-
},
|
|
33
|
-
"keywords": ["octwin", "cequens", "cli", "whatsapp", "chatbot", "pack", "conversational-ai", "yaml"],
|
|
34
|
-
"author": "CEQUENS",
|
|
35
|
-
"homepage": "https://www.npmjs.com/package/octwin-cli",
|
|
36
|
-
"license": "MIT"
|
|
37
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "octwin-cli",
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "Octwin external-pack developer CLI (by CEQUENS) — scaffold, validate, deploy, and check pure-YAML packs on your tenant.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"octwin": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"templates",
|
|
12
|
+
"README.md",
|
|
13
|
+
"CHANGELOG.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"yaml": "^2.6.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^22.0.0",
|
|
28
|
+
"typescript": "^5.7.0"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"keywords": ["octwin", "cequens", "cli", "whatsapp", "chatbot", "pack", "conversational-ai", "yaml"],
|
|
34
|
+
"author": "CEQUENS",
|
|
35
|
+
"homepage": "https://www.npmjs.com/package/octwin-cli",
|
|
36
|
+
"license": "MIT"
|
|
37
|
+
}
|
|
@@ -14,6 +14,16 @@ description: 'A hello bot — edit this to build your pack.'
|
|
|
14
14
|
supported_channels: [whatsapp, web]
|
|
15
15
|
required_adapters: [messaging]
|
|
16
16
|
|
|
17
|
+
# What to do with a VOICE NOTE. WhatsApp cannot stop customers sending them, and an
|
|
18
|
+
# ABSENT block is not "nothing happens" — it means `passthrough`, i.e. no
|
|
19
|
+
# speech-to-text, so the agent gets "a voice message arrived, no transcript" and has
|
|
20
|
+
# to improvise. `transcribe` is the right default for a conversational pack; use
|
|
21
|
+
# `passthrough` when you handle the audio ref yourself, or `decline` (with a
|
|
22
|
+
# `notice:`) to ask for text instead.
|
|
23
|
+
inbound_preprocessing:
|
|
24
|
+
voicenote:
|
|
25
|
+
mode: transcribe
|
|
26
|
+
|
|
17
27
|
# Default locale for platform-emitted copy + this pack's `$t()` lookups.
|
|
18
28
|
default_settings:
|
|
19
29
|
locale: ar
|