octwin-cli 0.5.1 → 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 +43 -1
- package/README.md +6 -3
- package/dist/index.js +794 -485
- package/dist/lib/args-check.js +11 -10
- package/dist/lib/builtin-check.js +130 -0
- package/dist/lib/declaration-check.js +192 -0
- package/dist/lib/entity-check.js +150 -0
- package/dist/lib/kb-index.js +214 -0
- package/dist/lib/kb-path.js +17 -0
- package/dist/lib/kb-symbols.js +271 -0
- package/dist/lib/net-hint.js +57 -0
- package/dist/lib/render-check.js +56 -17
- package/dist/lib/template-check.js +107 -0
- package/dist/lib/validate.js +33 -4
- package/dist/lib/yaml-pos.js +29 -0
- package/package.json +37 -37
- package/templates/starter/manifest.yaml +10 -0
package/dist/index.js
CHANGED
|
@@ -15,19 +15,19 @@
|
|
|
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
|
-
* octwin
|
|
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
|
|
28
28
|
* octwin catalog [--readiness] # commerce products + stock + the WhatsApp catalog binding (catalog:read)
|
|
29
29
|
* octwin scheduling [--slots <resourceRecordId>] # engine state / computed slots (scheduling:read)
|
|
30
|
-
* octwin platform-kb [pull] [--dir .] #
|
|
30
|
+
* octwin platform-kb [pull] [--if-stale|--check] [--dir .] # the platform capability reference (no token needed)
|
|
31
31
|
* octwin test [--dir .] # = validate --remote (the full platform check)
|
|
32
32
|
*
|
|
33
33
|
* Multi-turn testing: the platform keeps ONE open conversation per `--as` handle,
|
|
@@ -58,12 +58,20 @@ import { homedir } from 'node:os';
|
|
|
58
58
|
import { fileURLToPath } from 'node:url';
|
|
59
59
|
import { parse as parseYaml } from 'yaml';
|
|
60
60
|
import { applyRenames } from './lib/rename.js';
|
|
61
|
-
import { validatePackBundle } from './lib/validate.js';
|
|
61
|
+
import { validatePackBundle, describePackNameProblem, asPackName } from './lib/validate.js';
|
|
62
62
|
import { loadAllowedRenderKeys, findRenderKeyViolations, describeRenderFinding } from './lib/render-check.js';
|
|
63
63
|
import { loadPrimitiveArgSpecs, findArgViolations, describeArgFinding } from './lib/args-check.js';
|
|
64
|
+
import { yamlLineOf } from './lib/yaml-pos.js';
|
|
65
|
+
import { localhostFamilyHint } from './lib/net-hint.js';
|
|
66
|
+
import { loadBuiltinNames, findBuiltinViolations, describeBuiltinFinding } from './lib/builtin-check.js';
|
|
67
|
+
import { loadTemplateSpecs, findTemplateViolations, describeTemplateFinding } from './lib/template-check.js';
|
|
68
|
+
import { loadSystemEntities, findEntityViolations, describeEntityFinding } from './lib/entity-check.js';
|
|
69
|
+
import { loadDeclarationSpecs, findDeclarationViolations, describeDeclarationFinding } from './lib/declaration-check.js';
|
|
64
70
|
import { describeKbLookup, findPlatformKbDir } from './lib/kb-path.js';
|
|
65
71
|
import { classifyPackPath, isSkippedDir } from './lib/pack-source.js';
|
|
66
72
|
import { readPage, morePageHint } from './lib/page.js';
|
|
73
|
+
import { kbOneLiner, buildKbIndexMarkdown, buildKbOutlineMarkdown, } from './lib/kb-index.js';
|
|
74
|
+
import { buildSymbols, linkExplainers, renderSymbolsMarkdown } from './lib/kb-symbols.js';
|
|
67
75
|
// The in-package starter template ships alongside `dist/` and `src/` (both one
|
|
68
76
|
// level under the package root), so `../templates/starter` resolves for the
|
|
69
77
|
// built CLI and `tsx` dev alike.
|
|
@@ -157,7 +165,8 @@ async function fetchOrDie(url, init, what) {
|
|
|
157
165
|
return await fetch(url, init);
|
|
158
166
|
}
|
|
159
167
|
catch (err) {
|
|
160
|
-
|
|
168
|
+
const hint = localhostFamilyHint(url, err);
|
|
169
|
+
die(`${what} — platform unreachable at ${url} (${err?.message ?? err})${hint ? `\n ${hint}` : ''}`);
|
|
161
170
|
}
|
|
162
171
|
}
|
|
163
172
|
/** One consistent explanation for auth failures on admin reads. A 401 can also
|
|
@@ -170,7 +179,7 @@ function authFailureHint(status, url) {
|
|
|
170
179
|
}
|
|
171
180
|
/**
|
|
172
181
|
* Write VERBS need a different scope than the read they share a command with —
|
|
173
|
-
* `octwin
|
|
182
|
+
* `octwin work` is `work:read`, `octwin work note` is `work:write` — so the
|
|
174
183
|
* requirement is resolved by `<command> <verb>` first, then by command.
|
|
175
184
|
*
|
|
176
185
|
* Keyed on the verb rather than duplicating whole commands, because the hint's
|
|
@@ -183,13 +192,17 @@ const VERB_REQUIREMENTS = {
|
|
|
183
192
|
'records note': { scope: 'records:write', feature: 'records' },
|
|
184
193
|
'records tasks': { scope: 'records:read', feature: 'tasks' },
|
|
185
194
|
'records task': { scope: 'records:write', feature: 'tasks' },
|
|
186
|
-
'
|
|
187
|
-
'
|
|
188
|
-
|
|
189
|
-
//
|
|
195
|
+
'work assign': { scope: 'work:write', feature: 'work' },
|
|
196
|
+
'work note': { scope: 'work:write', feature: 'work' },
|
|
197
|
+
// The stage move rides the XRM records verb (`POST …/xrm/records/:id/stage`) —
|
|
198
|
+
// the work surface deliberately has no second transition spelling (P-02).
|
|
199
|
+
'work stage': { scope: 'records:write', feature: 'records' },
|
|
200
|
+
// `decide --dry-run` hits the PREVIEW route, which is `work:read`. Naming the
|
|
190
201
|
// write scope is still the right hint: the committing form is the default.
|
|
191
|
-
'
|
|
192
|
-
|
|
202
|
+
'work decide': { scope: 'work:write', feature: 'work' },
|
|
203
|
+
// The stage move rides the XRM records verb (P-02), so the scope is records:write —
|
|
204
|
+
// the read that resolves the reference still needs orders:read.
|
|
205
|
+
'orders transition': { scope: 'records:write', feature: 'records' },
|
|
193
206
|
'orders refund': { scope: 'orders:write', feature: 'orders' },
|
|
194
207
|
'catalog availability': { scope: 'catalog:write', feature: 'catalog' },
|
|
195
208
|
'catalog stock': { scope: 'catalog:write', feature: 'catalog' },
|
|
@@ -213,7 +226,12 @@ const COMMAND_REQUIREMENTS = {
|
|
|
213
226
|
// same scope. Omitting it here meant a 403 on the one command that recovers a pack's
|
|
214
227
|
// only source copy printed the generic hint WITHOUT naming the scope to grant.
|
|
215
228
|
pull: { scope: 'pack:deploy' },
|
|
216
|
-
|
|
229
|
+
// `platform-kb` deliberately has NO requirement: the reference is platform stdlib,
|
|
230
|
+
// and the platform serves it anonymously at `/api/public/octwin-platform-kb` so a
|
|
231
|
+
// cold start needs no login. A token is still USED when present (the tenant-scoped
|
|
232
|
+
// route also works against platforms predating the public one) — but declaring a
|
|
233
|
+
// requirement here would print "needs pack:deploy" on a failure whose real cause is
|
|
234
|
+
// an unreachable instance.
|
|
217
235
|
feedback: { scope: 'pack:deploy' },
|
|
218
236
|
media: { scope: 'media:generate' },
|
|
219
237
|
// The plan feature gates RECORD reads, not the entity list (`/xrm/entities` carries only
|
|
@@ -221,7 +239,7 @@ const COMMAND_REQUIREMENTS = {
|
|
|
221
239
|
// plan for a 403 the plan did not cause.
|
|
222
240
|
records: { scope: 'records:read', feature: 'records', featureAppliesTo: 'reading records (listing entities needs only the scope)' },
|
|
223
241
|
analytics: { scope: 'records:read', feature: 'records' },
|
|
224
|
-
|
|
242
|
+
work: { scope: 'work:read', feature: 'work' },
|
|
225
243
|
logs: { scope: 'conversations:read' },
|
|
226
244
|
orders: { scope: 'orders:read', feature: 'orders' },
|
|
227
245
|
catalog: { scope: 'catalog:read', feature: 'catalog' },
|
|
@@ -234,7 +252,7 @@ const COMMAND_REQUIREMENTS = {
|
|
|
234
252
|
};
|
|
235
253
|
/** The command currently running — set once in `main()` so any failure printer can
|
|
236
254
|
* name the scope that command needs without threading it through every call.
|
|
237
|
-
* Carries the write VERB too (`
|
|
255
|
+
* Carries the write VERB too (`work note`), since that is what decides the scope. */
|
|
238
256
|
let CURRENT_COMMAND;
|
|
239
257
|
/** `→ needs the \`orders:read\` scope …` — the requirement line for the running
|
|
240
258
|
* command, or '' when the command has no declared requirement. */
|
|
@@ -484,6 +502,13 @@ async function notifyIfOutdated() {
|
|
|
484
502
|
}
|
|
485
503
|
catch { /* a version check must never break the CLI */ }
|
|
486
504
|
}
|
|
505
|
+
// ── platform-KB drift check (observe the pulled reference, fail-silent) ──
|
|
506
|
+
//
|
|
507
|
+
// `KbIndexEntry` / `KbEntriesDescriptor` mirror the platform's own types and live
|
|
508
|
+
// in `lib/kb-index.js` beside the renderers that consume them. `entries` (list
|
|
509
|
+
// catalogs only) describes how to enumerate a catalog so we can EXPLODE it into
|
|
510
|
+
// one file per entry; the PLATFORM supplies it rather than this CLI hardcoding
|
|
511
|
+
// per-catalog knowledge, so a platform that adds a catalog needs no CLI release.
|
|
487
512
|
/** A previously-pulled KB's identity in `<packDir>/.octwin/platform-kb/index.json`
|
|
488
513
|
* (content hash + per-entry index), or null if nothing has been pulled yet. */
|
|
489
514
|
function readLocalKb(packDir) {
|
|
@@ -515,15 +540,44 @@ function diffKbIndex(prev, next) {
|
|
|
515
540
|
const removed = prev.filter(e => !nextKeys.has(e.key)).map(e => e.key);
|
|
516
541
|
return { added, removed, changed };
|
|
517
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* Fetch the platform's KB identity (`?meta=1`) — the cheap poll behind both the
|
|
545
|
+
* drift nudge and `--if-stale`. Returns null on anything that is not a clean
|
|
546
|
+
* answer; the caller decides whether that is worth a word.
|
|
547
|
+
*
|
|
548
|
+
* `notAuthorized` is reported separately because it is the one failure with a
|
|
549
|
+
* fix the author can act on.
|
|
550
|
+
*/
|
|
551
|
+
async function fetchKbMeta(t, timeoutMs = 2_000) {
|
|
552
|
+
const ep = kbEndpoint(t);
|
|
553
|
+
const ctrl = new AbortController();
|
|
554
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
555
|
+
try {
|
|
556
|
+
const res = await fetch(`${ep.url}?meta=1`, { headers: ep.headers, signal: ctrl.signal });
|
|
557
|
+
if (!res.ok)
|
|
558
|
+
return { ok: false, notAuthorized: res.status === 401 || res.status === 403 };
|
|
559
|
+
return { ok: true, meta: await res.json() };
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
return { ok: false, notAuthorized: false };
|
|
563
|
+
}
|
|
564
|
+
finally {
|
|
565
|
+
clearTimeout(timer);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
518
568
|
/** Nudge (to stderr) when the platform's capability KB has changed since the last
|
|
519
569
|
* `octwin platform-kb pull`. The sibling of `notifyIfOutdated`, for the KB instead
|
|
520
570
|
* of the CLI: run only after commands that already hit the platform, so this adds
|
|
521
571
|
* a single tiny `?meta=1` GET on top of work that was networked anyway. Never
|
|
522
572
|
* throws — observing must never break a command. No-op until the author has pulled
|
|
523
|
-
* once (nothing to compare) or if the target
|
|
573
|
+
* once (nothing to compare) or if the target can't be resolved.
|
|
574
|
+
*
|
|
575
|
+
* NOT TTY-gated, deliberately. It was, on the reasoning that a nudge is for a
|
|
576
|
+
* human watching a terminal — but the primary reader of this CLI is now an
|
|
577
|
+
* authoring agent whose output is piped, and gating on `isTTY` meant the one
|
|
578
|
+
* reader that CANNOT notice a stale reference on its own was the only one never
|
|
579
|
+
* told. It is a single line on stderr, so piped stdout stays clean either way. */
|
|
524
580
|
async function notifyIfKbStale(flags) {
|
|
525
|
-
if (!process.stdout.isTTY)
|
|
526
|
-
return;
|
|
527
581
|
try {
|
|
528
582
|
const packDir = resolve(flags.dir ?? '.');
|
|
529
583
|
const local = readLocalKb(packDir);
|
|
@@ -532,25 +586,20 @@ async function notifyIfKbStale(flags) {
|
|
|
532
586
|
const t = resolveTargetOrNull(flags);
|
|
533
587
|
if (!t)
|
|
534
588
|
return;
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
// an author invent a primitive from memory. Say so once; stay silent for every other
|
|
546
|
-
// failure (offline, timeout, a platform without the route).
|
|
547
|
-
if (res.status === 401 || res.status === 403) {
|
|
548
|
-
console.error('\nⓘ can\'t check whether the platform capability reference drifted — that needs a `pack:deploy` token.');
|
|
549
|
-
console.error(' Refresh it directly with a deploy token: octwin platform-kb --token oct_…');
|
|
589
|
+
const polled = await fetchKbMeta(t);
|
|
590
|
+
if (!polled.ok) {
|
|
591
|
+
// The tenant-scoped meta poll needs `pack:deploy`, but this nudge rides on every
|
|
592
|
+
// networked command — so an author inspecting data with a narrow (`records:read`-only)
|
|
593
|
+
// token got NO drift signal at all, silently, and a stale reference is exactly what
|
|
594
|
+
// makes an author invent a primitive from memory. Say so once; stay silent for every
|
|
595
|
+
// other failure (offline, timeout, a platform without the route).
|
|
596
|
+
if (polled.notAuthorized) {
|
|
597
|
+
console.error('\nⓘ can\'t check whether the platform capability reference drifted — that token lacks `pack:deploy`.');
|
|
598
|
+
console.error(' Check it without a token: octwin platform-kb --check (or refresh: octwin platform-kb --token oct_…)');
|
|
550
599
|
}
|
|
551
600
|
return;
|
|
552
601
|
}
|
|
553
|
-
const meta =
|
|
602
|
+
const meta = polled.meta;
|
|
554
603
|
if (meta.content_hash && meta.content_hash !== local.content_hash) {
|
|
555
604
|
// Per-entry summary (now that the index carries per-entry hashes) — the
|
|
556
605
|
// exact list of what changed is one `octwin platform-kb` away.
|
|
@@ -566,7 +615,7 @@ async function notifyIfKbStale(flags) {
|
|
|
566
615
|
summary = ` (${parts.join(' · ')})`;
|
|
567
616
|
}
|
|
568
617
|
console.error(`\n⬆ the platform capability reference changed since you last pulled it${summary}.`);
|
|
569
|
-
console.error(' Refresh it: octwin platform-kb');
|
|
618
|
+
console.error(' Refresh it: octwin platform-kb --if-stale');
|
|
570
619
|
}
|
|
571
620
|
}
|
|
572
621
|
catch { /* a KB check must never break the CLI */ }
|
|
@@ -584,7 +633,7 @@ function commandTouchesPlatform(command, flags) {
|
|
|
584
633
|
case 'media':
|
|
585
634
|
case 'pull':
|
|
586
635
|
case 'records':
|
|
587
|
-
case '
|
|
636
|
+
case 'work':
|
|
588
637
|
case 'logs':
|
|
589
638
|
case 'whoami':
|
|
590
639
|
case 'feedback':
|
|
@@ -602,9 +651,14 @@ function commandTouchesPlatform(command, flags) {
|
|
|
602
651
|
function cmdInit(flags) {
|
|
603
652
|
const target = flags._[0] ?? die('usage: octwin init <dir> [--id my-pack]');
|
|
604
653
|
const dir = resolve(target);
|
|
654
|
+
// A BARE name, and it stays bare: the platform prefixes the workspace slug at publish,
|
|
655
|
+
// which is why `init` can stay offline (it has no idea which workspace this will deploy
|
|
656
|
+
// to, and no longer needs one) and why the same scaffold deploys into two workspaces as
|
|
657
|
+
// two packs with nothing to edit in between.
|
|
605
658
|
const id = flags.id ?? target.replace(/[/\\]/g, '').replace(/[^a-z0-9-]/gi, '-').toLowerCase();
|
|
606
|
-
|
|
607
|
-
|
|
659
|
+
const idProblem = describePackNameProblem(id);
|
|
660
|
+
if (idProblem)
|
|
661
|
+
die(`${idProblem}\n → pass --id <pack-name>`);
|
|
608
662
|
if (existsSync(dir) && readdirSync(dir).length > 0)
|
|
609
663
|
die(`target '${dir}' is not empty`);
|
|
610
664
|
if (!existsSync(TEMPLATE_DIR))
|
|
@@ -636,7 +690,14 @@ function cmdInit(flags) {
|
|
|
636
690
|
}
|
|
637
691
|
function localValidate(packDir) {
|
|
638
692
|
const { files, blobs } = collectBundleFiles(packDir);
|
|
639
|
-
const { id, version } = readManifestIdVersion(files);
|
|
693
|
+
const { id: rawId, version } = readManifestIdVersion(files);
|
|
694
|
+
// The manifest declares a BARE name; the platform prefixes your workspace at publish.
|
|
695
|
+
// Minted here so an unusable one is reported with the rest of the bundle's errors.
|
|
696
|
+
const id = asPackName(rawId);
|
|
697
|
+
if (!id) {
|
|
698
|
+
console.error(` ✗ ${describePackNameProblem(rawId)}`);
|
|
699
|
+
die('bundle validation failed (1 error)');
|
|
700
|
+
}
|
|
640
701
|
const r = validatePackBundle(id, files, blobs);
|
|
641
702
|
if (!r.ok) {
|
|
642
703
|
for (const e of r.errors)
|
|
@@ -660,17 +721,36 @@ async function cmdValidate(flags) {
|
|
|
660
721
|
return [];
|
|
661
722
|
}
|
|
662
723
|
});
|
|
663
|
-
// Checks that need the pulled KB.
|
|
664
|
-
// gitignored cache wiped by every pull,
|
|
665
|
-
//
|
|
666
|
-
// clone before the author could act. But a skip is now ANNOUNCED, and remembered:
|
|
724
|
+
// Checks that need the pulled KB. All of them DEGRADE when it is absent — the KB
|
|
725
|
+
// is a gitignored cache wiped by every pull, so failing hard would break a fresh
|
|
726
|
+
// clone before the author could act. But a skip is ANNOUNCED, and remembered:
|
|
667
727
|
// the ✓ used to print above these blocks unconditionally while the per-check ✓s
|
|
668
728
|
// lived inside the `if`s, so a KB-less run read as "one check, passed". An entire
|
|
669
729
|
// backlog batch reached production that way. The defect is the silence, not the skip.
|
|
730
|
+
//
|
|
731
|
+
// Reasons are COLLECTED rather than printed inline. When the KB is missing, every
|
|
732
|
+
// check skips for the identical reason, and six copies of one sentence is how a
|
|
733
|
+
// reader learns to scroll past the ⚠ block — which is the same failure as not
|
|
734
|
+
// printing it. One line, naming all six.
|
|
670
735
|
const skipped = [];
|
|
736
|
+
const skipReasons = new Map();
|
|
737
|
+
const noteSkip = (label, lookup) => {
|
|
738
|
+
skipped.push(label);
|
|
739
|
+
// Group by the lookup's IDENTITY, then let `describeKbLookup` phrase the one
|
|
740
|
+
// line at print time — so the wording stays in the module that owns it and
|
|
741
|
+
// cannot drift into a doubled "SKIPPED — SKIPPED —".
|
|
742
|
+
const key = lookup.state === 'ok' ? 'ok'
|
|
743
|
+
: `${lookup.state}|${'dir' in lookup ? lookup.dir : ''}|${'reason' in lookup ? lookup.reason : ''}`;
|
|
744
|
+
const bucket = skipReasons.get(key) ?? { lookup, labels: [] };
|
|
745
|
+
bucket.labels.push(label);
|
|
746
|
+
skipReasons.set(key, bucket);
|
|
747
|
+
};
|
|
748
|
+
/** Stamp each finding with its source line — the walkers carry the node
|
|
749
|
+
* path; `files` holds the raw text the locator needs (E-08). */
|
|
750
|
+
const withLines = (fs) => fs.map(f => ({ ...f, line: files[f.file] ? yamlLineOf(files[f.file], f.path) : null }));
|
|
671
751
|
const render = loadAllowedRenderKeys(packDir);
|
|
672
752
|
if (render.keys) {
|
|
673
|
-
const findings = yamlDocs().flatMap(([p, doc]) => findRenderKeyViolations(doc, p, render.keys));
|
|
753
|
+
const findings = withLines(yamlDocs().flatMap(([p, doc]) => findRenderKeyViolations(doc, p, render.keys, render.nested)));
|
|
674
754
|
if (findings.length) {
|
|
675
755
|
console.error(`✗ ${findings.length} render-intent field error${findings.length === 1 ? '' : 's'}:`);
|
|
676
756
|
for (const f of findings)
|
|
@@ -680,14 +760,13 @@ async function cmdValidate(flags) {
|
|
|
680
760
|
console.log('✓ render intents use only fields the platform renders');
|
|
681
761
|
}
|
|
682
762
|
else {
|
|
683
|
-
|
|
684
|
-
skipped.push('render-intent fields');
|
|
763
|
+
noteSkip('render-intent fields', render.lookup);
|
|
685
764
|
}
|
|
686
765
|
// Primitive `args:` keys, same source and same contract. Cannot see inside a
|
|
687
766
|
// `use:` template body (expansion is the platform's job); `--remote` covers that.
|
|
688
767
|
const args = loadPrimitiveArgSpecs(packDir);
|
|
689
768
|
if (args.specs) {
|
|
690
|
-
const findings = yamlDocs().flatMap(([p, doc]) => findArgViolations(doc, p, args.specs));
|
|
769
|
+
const findings = withLines(yamlDocs().flatMap(([p, doc]) => findArgViolations(doc, p, args.specs)));
|
|
691
770
|
if (findings.length) {
|
|
692
771
|
console.error(`✗ ${findings.length} primitive-argument error${findings.length === 1 ? '' : 's'}:`);
|
|
693
772
|
for (const f of findings)
|
|
@@ -697,8 +776,104 @@ async function cmdValidate(flags) {
|
|
|
697
776
|
console.log('✓ primitive arguments match their declared inputs');
|
|
698
777
|
}
|
|
699
778
|
else {
|
|
700
|
-
|
|
701
|
-
|
|
779
|
+
noteSkip('primitive arguments', args.lookup);
|
|
780
|
+
}
|
|
781
|
+
// Expression builtins — the function set is CLOSED and generated from the
|
|
782
|
+
// runtime, so an invented `$fn(` is checkable here and nowhere else offline.
|
|
783
|
+
const builtins = loadBuiltinNames(packDir);
|
|
784
|
+
if (builtins.names) {
|
|
785
|
+
const findings = withLines(yamlDocs().flatMap(([p, doc]) => findBuiltinViolations(doc, p, builtins.names)));
|
|
786
|
+
if (findings.length) {
|
|
787
|
+
console.error(`✗ ${findings.length} unknown expression function${findings.length === 1 ? '' : 's'}:`);
|
|
788
|
+
for (const f of findings)
|
|
789
|
+
console.error(` ✗ ${describeBuiltinFinding(f)}`);
|
|
790
|
+
die('fix these before deploying — the evaluator cannot resolve them, and it fails mid-conversation');
|
|
791
|
+
}
|
|
792
|
+
console.log('✓ every $function() in an expression exists');
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
noteSkip('expression functions', builtins.lookup);
|
|
796
|
+
}
|
|
797
|
+
// `use:` templates. A pack's OWN templates shadow the platform's, so they are
|
|
798
|
+
// named here and skipped — this check has no schema for them.
|
|
799
|
+
const templates = loadTemplateSpecs(packDir);
|
|
800
|
+
if (templates.specs) {
|
|
801
|
+
// A pack template is `templates/<name>.template.yaml` — the `.template`
|
|
802
|
+
// segment is part of the convention the expander scans for, NOT part of the
|
|
803
|
+
// name a `use:` writes. Capturing it would leave every pack that shadows a
|
|
804
|
+
// platform template (kaiian shadows `field_prompt_render`) reported as using
|
|
805
|
+
// one that does not exist.
|
|
806
|
+
const packTemplates = new Set(Object.keys(files)
|
|
807
|
+
.map(p => /^templates\/(.+)\.template\.ya?ml$/i.exec(p.replace(/\\/g, '/'))?.[1])
|
|
808
|
+
.filter((n) => !!n));
|
|
809
|
+
const findings = yamlDocs().flatMap(([p, doc]) => findTemplateViolations(doc, p, templates.specs, packTemplates));
|
|
810
|
+
if (findings.length) {
|
|
811
|
+
console.error(`✗ ${findings.length} template error${findings.length === 1 ? '' : 's'}:`);
|
|
812
|
+
for (const f of findings)
|
|
813
|
+
console.error(` ✗ ${describeTemplateFinding(f)}`);
|
|
814
|
+
die('fix these before deploying — a template param that does not exist arrives as undefined, and renders as a blank');
|
|
815
|
+
}
|
|
816
|
+
console.log('✓ `use:` templates and their params exist');
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
noteSkip('`use:` templates', templates.lookup);
|
|
820
|
+
}
|
|
821
|
+
// Reserved XRM entity keys — a boot error, which means the pack deploys clean
|
|
822
|
+
// and then fails to load on the first inbound message.
|
|
823
|
+
const system = loadSystemEntities(packDir);
|
|
824
|
+
if (system.entities) {
|
|
825
|
+
const findings = yamlDocs()
|
|
826
|
+
.filter(([p]) => /(^|[/\\])xrm\.ya?ml$/i.test(p))
|
|
827
|
+
.flatMap(([p, doc]) => findEntityViolations(doc, p, system.entities, system.rules));
|
|
828
|
+
if (findings.length) {
|
|
829
|
+
console.error(`✗ ${findings.length} reserved-entity error${findings.length === 1 ? '' : 's'}:`);
|
|
830
|
+
for (const f of findings)
|
|
831
|
+
console.error(` ✗ ${describeEntityFinding(f)}`);
|
|
832
|
+
die('fix these before deploying — these fail at BOOT, after a deploy that reported success');
|
|
833
|
+
}
|
|
834
|
+
// Name the narrower promise when the catalog-level rules are absent (a KB
|
|
835
|
+
// pulled before they were published) — a ✓ that reads wider than what ran is
|
|
836
|
+
// the failure this file's skip contract exists to prevent.
|
|
837
|
+
console.log(system.rules
|
|
838
|
+
? '✓ no entity collides with a reserved platform key, and no extension overrides a platform-owned one'
|
|
839
|
+
: '✓ no entity collides with a reserved platform key (re-pull for the `contact`/extension-override rules)');
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
noteSkip('reserved entity keys', system.lookup);
|
|
843
|
+
}
|
|
844
|
+
// The declaration files themselves, against the published JSON Schemas.
|
|
845
|
+
// Deliberately narrow (see declaration-check.ts) — it walks away from anything
|
|
846
|
+
// it cannot read rather than guessing.
|
|
847
|
+
const decls = loadDeclarationSpecs(packDir);
|
|
848
|
+
if (decls.specs) {
|
|
849
|
+
const findings = yamlDocs().flatMap(([p, doc]) => {
|
|
850
|
+
const base = p.replace(/\\/g, '/').split('/').pop() ?? p;
|
|
851
|
+
const spec = decls.specs.get(base);
|
|
852
|
+
// Only a file at the PACK ROOT is a declaration — `flows/tools/xrm.yaml`
|
|
853
|
+
// would be a flow that happens to share a name.
|
|
854
|
+
if (!spec || p.replace(/\\/g, '/').includes('/'))
|
|
855
|
+
return [];
|
|
856
|
+
return findDeclarationViolations(doc, spec).map(f => ({ ...f, file: p }));
|
|
857
|
+
});
|
|
858
|
+
if (findings.length) {
|
|
859
|
+
console.error(`✗ ${findings.length} declaration error${findings.length === 1 ? '' : 's'}:`);
|
|
860
|
+
for (const f of findings)
|
|
861
|
+
console.error(` ✗ ${describeDeclarationFinding(f)}`);
|
|
862
|
+
die('fix these before deploying — a declaration file is parsed strictly, and an unknown key is rejected');
|
|
863
|
+
}
|
|
864
|
+
// Deliberately narrow wording. Most of `xrm.yaml`'s field shapes are a Zod
|
|
865
|
+
// union, which renders as `anyOf` and which this check walks away from by
|
|
866
|
+
// design — so "matches its schema" would be a promise it does not keep, and
|
|
867
|
+
// an over-claimed ✓ is how an author stops reading `--remote` output.
|
|
868
|
+
console.log('✓ declaration files carry no unknown or missing keys (unions are left to --remote)');
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
noteSkip('declaration schemas', decls.lookup);
|
|
872
|
+
}
|
|
873
|
+
// One ⚠ per distinct reason, naming every check it cost.
|
|
874
|
+
for (const { lookup, labels } of skipReasons.values()) {
|
|
875
|
+
const what = labels.length === 1 ? labels[0] : `${labels.length} checks (${labels.join(', ')})`;
|
|
876
|
+
console.log(`⚠ ${describeKbLookup(lookup, what)}`);
|
|
702
877
|
}
|
|
703
878
|
// `--require-kb` is for CI, where a skip nobody reads is worse than a red build.
|
|
704
879
|
if (skipped.length && flags['require-kb'] === true) {
|
|
@@ -708,7 +883,7 @@ async function cmdValidate(flags) {
|
|
|
708
883
|
// The LAST line carries the skip. A reader who sees a ✓ and stops there is the
|
|
709
884
|
// failure mode; a caveat printed ABOVE the ✓ does not fix it.
|
|
710
885
|
if (skipped.length) {
|
|
711
|
-
console.log(`\n⚠ ${id}@${version} passed the checks that RAN — ${skipped.
|
|
886
|
+
console.log(`\n⚠ ${id}@${version} passed the checks that RAN — ${skipped.length} skipped: ${skipped.join(', ')}.`);
|
|
712
887
|
console.log(' Run `octwin platform-kb pull` (once, at your repo root — it covers every pack under it),');
|
|
713
888
|
console.log(' or `octwin validate --remote` to have the platform run everything server-side.');
|
|
714
889
|
}
|
|
@@ -723,10 +898,16 @@ async function cmdValidate(flags) {
|
|
|
723
898
|
const t = resolveTarget(flags);
|
|
724
899
|
const { url } = t;
|
|
725
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.
|
|
726
904
|
const res = await fetchOrDie(`${url}/api/self/p/packs/validate`, {
|
|
727
905
|
method: 'POST',
|
|
728
906
|
headers: { 'content-type': 'application/json', ...authHeaders(t) },
|
|
729
|
-
body: JSON.stringify({
|
|
907
|
+
body: JSON.stringify({
|
|
908
|
+
files, blobs,
|
|
909
|
+
...(flags['strict-primitives'] === true ? { strict_primitives: true } : {}),
|
|
910
|
+
}),
|
|
730
911
|
}, 'remote validate');
|
|
731
912
|
const text = await res.text();
|
|
732
913
|
let json;
|
|
@@ -838,6 +1019,24 @@ function resolveTargetOrNull(flags) {
|
|
|
838
1019
|
const t = readTarget(flags);
|
|
839
1020
|
return t.url && t.token ? t : null;
|
|
840
1021
|
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Where to read the platform capability reference from, and how.
|
|
1024
|
+
*
|
|
1025
|
+
* The KB is tenant-independent platform stdlib, and the platform now serves it
|
|
1026
|
+
* anonymously at `/api/public/octwin-platform-kb` precisely so a COLD START does
|
|
1027
|
+
* not need a login: `.octwin/` is gitignored, so a fresh clone — or a fresh
|
|
1028
|
+
* Claude Code session — has no reference at all, and an authoring agent's first
|
|
1029
|
+
* useful question came after a credential round-trip it had no way to satisfy.
|
|
1030
|
+
*
|
|
1031
|
+
* A token still WINS when one is available: the tenant-scoped route is the one
|
|
1032
|
+
* that has always existed, it works against platforms that predate the public
|
|
1033
|
+
* rung, and using it keeps the author's own instance the source of truth.
|
|
1034
|
+
*/
|
|
1035
|
+
function kbEndpoint(t) {
|
|
1036
|
+
return t.token
|
|
1037
|
+
? { url: `${t.url}/api/self/t/octwin-platform-kb`, headers: authHeaders(t), anonymous: false }
|
|
1038
|
+
: { url: `${t.url}/api/public/octwin-platform-kb`, headers: {}, anonymous: true };
|
|
1039
|
+
}
|
|
841
1040
|
/** The raw resolution both wrappers share — may return empty url/token. */
|
|
842
1041
|
function readTarget(flags) {
|
|
843
1042
|
const url = (flags.url ?? process.env.PACK_PLATFORM_URL ?? savedDefaultUrl()).replace(/\/$/, '');
|
|
@@ -1066,13 +1265,13 @@ async function cmdSeed(flags) {
|
|
|
1066
1265
|
const { terminal: final, stepErrors } = await readDeployProgress(res.body);
|
|
1067
1266
|
if (!final || final.stage === 'error')
|
|
1068
1267
|
die(`seed failed${final?.message ? `: ${final.message}` : ' (stream ended early)'}`);
|
|
1069
|
-
console.log(`
|
|
1268
|
+
console.log(`
|
|
1070
1269
|
✓ ${final.message ?? 'seed complete'}`);
|
|
1071
1270
|
printSeedCounts(final.result?.seeded);
|
|
1072
1271
|
if (stepErrors.length) {
|
|
1073
1272
|
// A kind failed but the rest ran — the reconcile softens each step. Say which,
|
|
1074
1273
|
// and exit non-zero so a scripted `seed && chat` doesn't read as clean.
|
|
1075
|
-
console.error(`
|
|
1274
|
+
console.error(`
|
|
1076
1275
|
⚠ ${stepErrors.length} step${stepErrors.length === 1 ? '' : 's'} failed — data may be incomplete:`);
|
|
1077
1276
|
for (const e of stepErrors)
|
|
1078
1277
|
console.error(` • ${e}`);
|
|
@@ -1154,6 +1353,33 @@ async function cmdDeploy(flags) {
|
|
|
1154
1353
|
}
|
|
1155
1354
|
printDeploySuccess(id, version, t, json);
|
|
1156
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
|
+
}
|
|
1157
1383
|
async function cmdStatus(flags) {
|
|
1158
1384
|
const packDir = resolve(flags.dir ?? '.');
|
|
1159
1385
|
const t = resolveTarget(flags);
|
|
@@ -1164,8 +1390,11 @@ async function cmdStatus(flags) {
|
|
|
1164
1390
|
const doc = parseYaml(readFileSync(manifestPath, 'utf8'));
|
|
1165
1391
|
if (typeof doc?.id !== 'string')
|
|
1166
1392
|
die('manifest.yaml must declare a string `id`');
|
|
1167
|
-
const
|
|
1393
|
+
const bare = doc.id;
|
|
1168
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);
|
|
1169
1398
|
console.log(`→ Checking ${id}@${localVersion} on ${targetLabel(t)} @ ${url} …`);
|
|
1170
1399
|
const res = await fetchOrDie(`${url}/api/self/p/packs/${id}/runtime`, {
|
|
1171
1400
|
headers: authHeaders(t),
|
|
@@ -1179,8 +1408,16 @@ async function cmdStatus(flags) {
|
|
|
1179
1408
|
json = text;
|
|
1180
1409
|
}
|
|
1181
1410
|
if (!res.ok) {
|
|
1182
|
-
|
|
1183
|
-
|
|
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
|
+
}
|
|
1184
1421
|
console.error(`✗ status check failed (HTTP ${res.status})`);
|
|
1185
1422
|
printAuthHint(res.status, url);
|
|
1186
1423
|
console.error(typeof json === 'string' ? json : JSON.stringify(json, null, 2));
|
|
@@ -1231,35 +1468,10 @@ async function cmdStatus(flags) {
|
|
|
1231
1468
|
function kbEntryFileName(name) {
|
|
1232
1469
|
return name.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
1233
1470
|
}
|
|
1234
|
-
/**
|
|
1235
|
-
*
|
|
1236
|
-
*
|
|
1237
|
-
|
|
1238
|
-
* text routinely inlines an envelope shape (`… { rows, total, …, refs? } …`) whose
|
|
1239
|
-
* `?` would otherwise cut the summary off mid-brace. */
|
|
1240
|
-
function kbOneLiner(text, max = 160) {
|
|
1241
|
-
if (typeof text !== 'string' || !text.trim())
|
|
1242
|
-
return '';
|
|
1243
|
-
const flat = text.replace(/\s+/g, ' ').trim();
|
|
1244
|
-
let depth = 0;
|
|
1245
|
-
let end = -1;
|
|
1246
|
-
for (let i = 0; i < flat.length; i++) {
|
|
1247
|
-
const ch = flat[i];
|
|
1248
|
-
if (ch === '{' || ch === '(' || ch === '[')
|
|
1249
|
-
depth++;
|
|
1250
|
-
else if (ch === '}' || ch === ')' || ch === ']')
|
|
1251
|
-
depth = Math.max(0, depth - 1);
|
|
1252
|
-
else if (depth === 0 && (ch === '.' || ch === '!' || ch === '?')) {
|
|
1253
|
-
const next = flat[i + 1];
|
|
1254
|
-
if (next === undefined || next === ' ') {
|
|
1255
|
-
end = i + 1;
|
|
1256
|
-
break;
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
const line = end >= 40 ? flat.slice(0, end) : flat;
|
|
1261
|
-
return line.length > max ? line.slice(0, max - 1).trimEnd() + '…' : line;
|
|
1262
|
-
}
|
|
1471
|
+
/** Envelope keys that are bookkeeping, not catalog-level content — never worth a
|
|
1472
|
+
* `_catalog.json` of their own. Anything else in the envelope is a real rule that
|
|
1473
|
+
* would otherwise be dropped by the explode. */
|
|
1474
|
+
const CATALOG_ENVELOPE_NOISE = new Set(['version', 'description', 'source']);
|
|
1263
1475
|
/** Enumerate a catalog's entries per the platform-supplied descriptor. Handles both
|
|
1264
1476
|
* collection shapes in use: an ARRAY of named objects (`primitives`, keyed by
|
|
1265
1477
|
* `name`) and an OBJECT MAP keyed by entry name (`declarations`, `system-entities`).
|
|
@@ -1288,73 +1500,56 @@ function enumerateKbEntries(catalog, d) {
|
|
|
1288
1500
|
}
|
|
1289
1501
|
return [];
|
|
1290
1502
|
}
|
|
1291
|
-
/**
|
|
1292
|
-
* Build `INDEX.md` — the map an authoring agent reads FIRST.
|
|
1293
|
-
*
|
|
1294
|
-
* The KB is ~800 KB across three dozen files; reading it whole costs more context
|
|
1295
|
-
* than the pack being authored. This index is one ~7k-token read that names every
|
|
1296
|
-
* doc and every catalog entry with a one-line summary and its exact path, so the
|
|
1297
|
-
* agent can jump straight to the ~600-token file it actually needs.
|
|
1298
|
-
*/
|
|
1299
|
-
function buildKbIndexMarkdown(bundle, exploded) {
|
|
1300
|
-
const index = bundle.index ?? [];
|
|
1301
|
-
const docs = index.filter(e => e.kind === 'doc');
|
|
1302
|
-
const catalogs = index.filter(e => e.kind === 'catalog');
|
|
1303
|
-
const L = [];
|
|
1304
|
-
L.push('# Octwin platform capability reference — INDEX');
|
|
1305
|
-
L.push('');
|
|
1306
|
-
L.push(`Reference version ${bundle.version ?? '?'} · content_hash \`${bundle.content_hash ?? '?'}\` · pulled ${bundle.generated_at ?? '?'}`);
|
|
1307
|
-
L.push('');
|
|
1308
|
-
L.push('**This is the map. Read it, then open only the specific file you need — never a whole catalog.**');
|
|
1309
|
-
L.push('Everything the platform supports is here; if a step, function, field, or render intent is NOT in');
|
|
1310
|
-
L.push('this index, it does not exist for a pure-YAML pack. Do not fill a gap from memory.');
|
|
1311
|
-
L.push('');
|
|
1312
|
-
L.push('## Start here');
|
|
1313
|
-
L.push('');
|
|
1314
|
-
L.push('1. `craft-capabilities.md` — how this reference fits together.');
|
|
1315
|
-
L.push('2. `craft-ux.md` — what a *good* pack looks like (home hub, rich cards, confirm-before-commit).');
|
|
1316
|
-
L.push('3. `craft-flows.md` — the flow DSL in practice.');
|
|
1317
|
-
L.push('4. Then the tables below, on demand.');
|
|
1318
|
-
L.push('');
|
|
1319
|
-
L.push('## Guides & reference docs');
|
|
1320
|
-
L.push('');
|
|
1321
|
-
L.push('| Doc | Read it for | File |');
|
|
1322
|
-
L.push('|---|---|---|');
|
|
1323
|
-
for (const d of docs)
|
|
1324
|
-
L.push(`| ${d.title ?? d.key} | ${kbOneLiner(d.summary)} | \`${d.key}.md\` |`);
|
|
1325
|
-
L.push('');
|
|
1326
|
-
L.push('## Catalogs — exact machine-readable schemas');
|
|
1327
|
-
L.push('');
|
|
1328
|
-
for (const c of catalogs) {
|
|
1329
|
-
const entries = exploded.get(c.key);
|
|
1330
|
-
L.push(`### ${c.title ?? c.key}`);
|
|
1331
|
-
L.push('');
|
|
1332
|
-
L.push(kbOneLiner(c.summary, 400));
|
|
1333
|
-
L.push('');
|
|
1334
|
-
if (!entries || entries.length === 0) {
|
|
1335
|
-
L.push(`Single document: \`${c.key}.json\``);
|
|
1336
|
-
L.push('');
|
|
1337
|
-
continue;
|
|
1338
|
-
}
|
|
1339
|
-
L.push(`${entries.length} entries in \`${c.key}/\` — one file each.`);
|
|
1340
|
-
L.push('');
|
|
1341
|
-
L.push('| Entry | What it does | File |');
|
|
1342
|
-
L.push('|---|---|---|');
|
|
1343
|
-
for (const e of entries) {
|
|
1344
|
-
L.push(`| \`${e.name}\` | ${e.summary.replace(/\|/g, '\\|')} | \`${c.key}/${kbEntryFileName(e.name)}.json\` |`);
|
|
1345
|
-
}
|
|
1346
|
-
L.push('');
|
|
1347
|
-
}
|
|
1348
|
-
return L.join('\n') + '\n';
|
|
1349
|
-
}
|
|
1350
1503
|
async function cmdPlatformKb(flags) {
|
|
1351
1504
|
const packDir = resolve(flags.dir ?? '.');
|
|
1352
|
-
const t =
|
|
1505
|
+
const t = readTarget(flags);
|
|
1506
|
+
if (!t.url)
|
|
1507
|
+
die('no platform url — pass --url <url>, set PACK_PLATFORM_URL, or run `octwin login`');
|
|
1353
1508
|
const { url } = t;
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1509
|
+
const ep = kbEndpoint(t);
|
|
1510
|
+
// ── --check: report staleness as an EXIT CODE, write nothing ──────────
|
|
1511
|
+
//
|
|
1512
|
+
// For a loop that wants to branch on "is my reference current?" without
|
|
1513
|
+
// parsing prose. 0 = current · 2 = stale (or never pulled) · 1 = could not
|
|
1514
|
+
// tell. Three codes, not two: an agent that treats "unreachable" as "stale"
|
|
1515
|
+
// re-pulls forever against an instance that is down.
|
|
1516
|
+
if (flags.check === true) {
|
|
1517
|
+
const local = readLocalKb(packDir);
|
|
1518
|
+
const polled = await fetchKbMeta(t, 10_000);
|
|
1519
|
+
if (!polled.ok) {
|
|
1520
|
+
console.error(polled.notAuthorized
|
|
1521
|
+
? '✗ cannot check — the platform refused the token, and this instance serves no anonymous reference.'
|
|
1522
|
+
: `✗ cannot check — ${url} did not answer.`);
|
|
1523
|
+
process.exit(1);
|
|
1524
|
+
}
|
|
1525
|
+
const remote = polled.meta.content_hash;
|
|
1526
|
+
if (!local?.content_hash) {
|
|
1527
|
+
console.log(`⬆ no reference pulled yet (platform is at ${remote ?? '?'}) — run \`octwin platform-kb pull\`.`);
|
|
1528
|
+
process.exit(2);
|
|
1529
|
+
}
|
|
1530
|
+
if (remote && remote !== local.content_hash) {
|
|
1531
|
+
console.log(`⬆ stale: local ${local.content_hash} → platform ${remote}. Run \`octwin platform-kb pull\`.`);
|
|
1532
|
+
process.exit(2);
|
|
1533
|
+
}
|
|
1534
|
+
console.log(`✓ current (${local.content_hash}).`);
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
// ── --if-stale: make "pull at the start of every session" free ────────
|
|
1538
|
+
//
|
|
1539
|
+
// One small `?meta=1` GET instead of ~1 MB, and it is what lets the skill say
|
|
1540
|
+
// "pull every session" without that costing a megabyte per session. An
|
|
1541
|
+
// unreachable platform is NOT treated as current: it falls through to the real
|
|
1542
|
+
// pull, which fails loudly with the actual error.
|
|
1543
|
+
if (flags['if-stale'] === true) {
|
|
1544
|
+
const local = readLocalKb(packDir);
|
|
1545
|
+
const polled = await fetchKbMeta(t, 10_000);
|
|
1546
|
+
if (polled.ok && local?.content_hash && polled.meta.content_hash === local.content_hash) {
|
|
1547
|
+
console.log(`✓ capability reference already current (${local.content_hash}) — nothing to pull.`);
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
console.log(`→ Pulling the platform capability reference from ${url}${ep.anonymous ? ' (anonymous — no token needed for the reference)' : ''} …`);
|
|
1552
|
+
const res = await fetchOrDie(ep.url, { headers: ep.headers }, 'platform-kb pull');
|
|
1358
1553
|
const text = await res.text();
|
|
1359
1554
|
if (!res.ok) {
|
|
1360
1555
|
let j;
|
|
@@ -1405,6 +1600,9 @@ async function cmdPlatformKb(flags) {
|
|
|
1405
1600
|
// (or an unexpected payload shape) fall back to the flat file.
|
|
1406
1601
|
const byKey = new Map((bundle.index ?? []).map(e => [e.key, e]));
|
|
1407
1602
|
const exploded = new Map();
|
|
1603
|
+
/** Catalogs written as ONE file — the flow schema is the only one today, and the
|
|
1604
|
+
* symbol router still mines it for node op-keys. */
|
|
1605
|
+
const flatCatalogs = {};
|
|
1408
1606
|
let catalogCount = 0;
|
|
1409
1607
|
let entryCount = 0;
|
|
1410
1608
|
for (const [key, val] of Object.entries(bundle.sources ?? {})) {
|
|
@@ -1415,6 +1613,7 @@ async function cmdPlatformKb(flags) {
|
|
|
1415
1613
|
const entries = descriptor ? enumerateKbEntries(val, descriptor) : [];
|
|
1416
1614
|
if (entries.length === 0) {
|
|
1417
1615
|
writeFileSync(join(outDir, `${key}.json`), JSON.stringify(val, null, 2) + '\n', 'utf8');
|
|
1616
|
+
flatCatalogs[key] = val;
|
|
1418
1617
|
continue;
|
|
1419
1618
|
}
|
|
1420
1619
|
const dir = join(outDir, key);
|
|
@@ -1422,17 +1621,57 @@ async function cmdPlatformKb(flags) {
|
|
|
1422
1621
|
for (const entry of entries) {
|
|
1423
1622
|
writeFileSync(join(dir, `${kbEntryFileName(entry.name)}.json`), JSON.stringify(entry.value, null, 2) + '\n', 'utf8');
|
|
1424
1623
|
}
|
|
1624
|
+
// The ENVELOPE, when it carries anything beyond the collection.
|
|
1625
|
+
//
|
|
1626
|
+
// Exploding a catalog drops everything that is not an entry — which was fine
|
|
1627
|
+
// while the envelope held only `version`/`description`/`source`, and silently
|
|
1628
|
+
// wrong the moment a catalog published a rule that belongs to the whole set
|
|
1629
|
+
// rather than to one member. `system-entities` does exactly that: the reserved
|
|
1630
|
+
// entity keys, the reserved prefix, and the keys an `extends: system` entity
|
|
1631
|
+
// may not override are properties of the CATALOG. Written as `_catalog.json`
|
|
1632
|
+
// so the offline checks can read them.
|
|
1633
|
+
//
|
|
1634
|
+
// The `_` prefix is load-bearing: every loader that reads one of these
|
|
1635
|
+
// directories iterates `*.json`, so a sibling that is not an entry must be
|
|
1636
|
+
// skippable by name. See the `isEntryFile` guard the checks share.
|
|
1637
|
+
const envelope = Object.fromEntries(Object.entries(val)
|
|
1638
|
+
.filter(([k]) => k !== descriptor.at && !CATALOG_ENVELOPE_NOISE.has(k)));
|
|
1639
|
+
if (Object.keys(envelope).length > 0) {
|
|
1640
|
+
writeFileSync(join(dir, '_catalog.json'), JSON.stringify(envelope, null, 2) + '\n', 'utf8');
|
|
1641
|
+
}
|
|
1425
1642
|
exploded.set(key, entries);
|
|
1426
1643
|
entryCount += entries.length;
|
|
1427
1644
|
}
|
|
1428
|
-
//
|
|
1429
|
-
|
|
1645
|
+
// ── the three maps ────────────────────────────────────────────────────
|
|
1646
|
+
//
|
|
1647
|
+
// Three, not one, because an author arrives with three different things in
|
|
1648
|
+
// hand: a QUESTION (INDEX), a NAME (SYMBOLS), or a doc too heavy to read whole
|
|
1649
|
+
// (OUTLINE). One file answering all three is the file nobody can afford to
|
|
1650
|
+
// read — which is what INDEX.md had become at ~30 KB, most of it entry rows
|
|
1651
|
+
// that belong in a grep target.
|
|
1652
|
+
writeFileSync(join(outDir, 'INDEX.md'), buildKbIndexMarkdown(bundle, new Map([...exploded].map(([k, v]) => [k, v.length]))), 'utf8');
|
|
1653
|
+
// SYMBOLS.md — every addressable name → the file that defines it. Derived from
|
|
1654
|
+
// the catalogs just written, so it needs nothing extra over the wire.
|
|
1655
|
+
const symbols = buildSymbols(exploded, (catalogKey, entryName) => `${catalogKey}/${kbEntryFileName(entryName)}.json`, flatCatalogs);
|
|
1656
|
+
linkExplainers(symbols, new Map((bundle.index ?? [])
|
|
1657
|
+
.filter(e => e.kind === 'doc' && e.sections?.length)
|
|
1658
|
+
.map(e => [e.key, e.sections])));
|
|
1659
|
+
writeFileSync(join(outDir, 'SYMBOLS.md'), renderSymbolsMarkdown(symbols, bundle.content_hash), 'utf8');
|
|
1660
|
+
// OUTLINE.md — only when the platform published section data. An empty outline
|
|
1661
|
+
// would read as "these docs have no sections", which is worse than its absence.
|
|
1662
|
+
const outline = buildKbOutlineMarkdown(bundle);
|
|
1663
|
+
if (outline)
|
|
1664
|
+
writeFileSync(join(outDir, 'OUTLINE.md'), outline, 'utf8');
|
|
1430
1665
|
// Persist `content_hash` too — the staleness observer (`notifyIfKbStale`) reads
|
|
1431
1666
|
// it back and compares against the platform's current hash to nudge a re-pull.
|
|
1432
1667
|
writeFileSync(join(outDir, 'index.json'), JSON.stringify({ version: bundle.version, content_hash: bundle.content_hash, generated_at: bundle.generated_at, index: bundle.index }, null, 2) + '\n', 'utf8');
|
|
1433
1668
|
console.log(`✓ Pulled the Octwin platform KB → ${outDir}`);
|
|
1434
1669
|
console.log(` ${mdCount} markdown docs + ${catalogCount} catalogs (${entryCount} entries, one file each) — reference version ${bundle.version ?? '?'}`);
|
|
1435
|
-
console.log(
|
|
1670
|
+
console.log(` Three maps: INDEX.md (the corpus) · SYMBOLS.md (${symbols.length} names → their file, grep it)`
|
|
1671
|
+
+ `${outline ? ' · OUTLINE.md (every heading, with line numbers)' : ''}`);
|
|
1672
|
+
if (!outline) {
|
|
1673
|
+
console.log(' (no OUTLINE.md — this platform publishes no doc section data; upgrade it for line-addressable docs.)');
|
|
1674
|
+
}
|
|
1436
1675
|
console.log(' Every pack UNDER this directory finds it — `octwin validate` walks up to locate it,');
|
|
1437
1676
|
console.log(' so one pull at a repo root covers a whole monorepo of packs.');
|
|
1438
1677
|
// Changelog since the last pull — per-entry hashes tell us WHICH docs/catalogs
|
|
@@ -1454,7 +1693,7 @@ async function cmdPlatformKb(flags) {
|
|
|
1454
1693
|
}
|
|
1455
1694
|
console.log(' The octwin-pack authoring skill reads these as the source of truth for what the platform supports.');
|
|
1456
1695
|
}
|
|
1457
|
-
// ── records /
|
|
1696
|
+
// ── records / work / logs / chat — headless inspect + test with the deploy token ────
|
|
1458
1697
|
/** GET an admin endpoint with the deploy token; returns `{ status, json }`.
|
|
1459
1698
|
* Dies (with the URL) on a network failure; auth failures return so the
|
|
1460
1699
|
* caller can add command-specific context on top of `authFailureHint`. */
|
|
@@ -1616,9 +1855,9 @@ async function cmdRecords(flags) {
|
|
|
1616
1855
|
const { status, json } = await apiGet(`${base}/xrm/records?entity=${encodeURIComponent(entity)}&${pagingQs(flags)}`, t);
|
|
1617
1856
|
if (status !== 200) {
|
|
1618
1857
|
// Always show the server's reason (it names the unknown entity). Cases are
|
|
1619
|
-
//
|
|
1858
|
+
// worked records (worklist), not pack-declared XRM — point at the right command.
|
|
1620
1859
|
if (entity === 'case' || entity === 'cases') {
|
|
1621
|
-
console.error(` '${entity}' is
|
|
1860
|
+
console.error(` '${entity}' is a worked record (worklist), not a pack-declared XRM entity — inspect it with: octwin work`);
|
|
1622
1861
|
}
|
|
1623
1862
|
die(`could not read records (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
1624
1863
|
}
|
|
@@ -1835,7 +2074,11 @@ async function cmdLogs(flags) {
|
|
|
1835
2074
|
const { status, json } = await apiGet(`${base}/conversations?limit=50`, t);
|
|
1836
2075
|
if (status !== 200)
|
|
1837
2076
|
die(`could not read conversations (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
1838
|
-
|
|
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 ?? []);
|
|
1839
2082
|
if (asHandle)
|
|
1840
2083
|
convs = convs.filter((c) => c.contact?.channel_contact_handle === asHandle);
|
|
1841
2084
|
if (convs.length === 0) {
|
|
@@ -2233,29 +2476,32 @@ async function cmdChat(flags) {
|
|
|
2233
2476
|
}
|
|
2234
2477
|
console.log(`\n(same --as '${from}' continues this conversation — timeline: octwin logs --as ${from})`);
|
|
2235
2478
|
}
|
|
2236
|
-
/** `octwin media generate "<prompt>" [--out <file.png>] [--
|
|
2479
|
+
/** `octwin media generate "<prompt>" [--out <file.png>] [--json]`
|
|
2237
2480
|
* — AI-generate an image on the platform (needs a `media:generate`-scoped token),
|
|
2238
2481
|
* store it as a public asset, and return its `MEDIA-` handle + serve URL. `--out`
|
|
2239
2482
|
* downloads the bytes to a file (WhatsApp renders only `.png`/`.jpg`); the paired
|
|
2240
|
-
* `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. */
|
|
2241
2488
|
async function cmdMedia(flags) {
|
|
2242
2489
|
const sub = flags._[0];
|
|
2243
2490
|
if (sub !== 'generate')
|
|
2244
|
-
die('usage: octwin media generate "<prompt>" [--out <file.png>] [--
|
|
2491
|
+
die('usage: octwin media generate "<prompt>" [--out <file.png>] [--json]');
|
|
2245
2492
|
const t = resolveTarget(flags);
|
|
2246
2493
|
const { url } = t;
|
|
2247
2494
|
const prompt = flags._[1];
|
|
2248
2495
|
if (!prompt)
|
|
2249
|
-
die('usage: octwin media generate "<prompt>" [--out <file.png>] [--
|
|
2496
|
+
die('usage: octwin media generate "<prompt>" [--out <file.png>] [--json]');
|
|
2250
2497
|
const asJson = flags.json === true;
|
|
2251
|
-
const size = typeof flags.size === 'string' ? flags.size : undefined;
|
|
2252
2498
|
const out = typeof flags.out === 'string' ? flags.out : undefined;
|
|
2253
2499
|
if (!asJson)
|
|
2254
2500
|
console.log(`→ Generating an image on ${targetLabel(t)} @ ${url} …`);
|
|
2255
2501
|
const res = await fetchOrDie(`${url}/api/self/p/media/generate`, {
|
|
2256
2502
|
method: 'POST',
|
|
2257
2503
|
headers: { 'content-type': 'application/json', ...authHeaders(t) },
|
|
2258
|
-
body: JSON.stringify({ prompt
|
|
2504
|
+
body: JSON.stringify({ prompt }),
|
|
2259
2505
|
}, 'media generate');
|
|
2260
2506
|
const text = await res.text();
|
|
2261
2507
|
if (!res.ok) {
|
|
@@ -2284,80 +2530,96 @@ async function cmdMedia(flags) {
|
|
|
2284
2530
|
}
|
|
2285
2531
|
}
|
|
2286
2532
|
if (asJson) {
|
|
2287
|
-
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 }));
|
|
2288
2534
|
return;
|
|
2289
2535
|
}
|
|
2290
|
-
console.log(`✓ Generated ${r.media_ref} (${r.
|
|
2536
|
+
console.log(`✓ Generated ${r.media_ref} (${r.mime}, ${r.bytes} bytes)`);
|
|
2291
2537
|
console.log(` id: ${r.media_id}`);
|
|
2292
2538
|
console.log(` url: ${absUrl}`);
|
|
2293
2539
|
if (out)
|
|
2294
2540
|
console.log(` saved → ${out}`);
|
|
2295
2541
|
console.log(` Send it into a chat: octwin chat "here you go" --media ${out ?? r.media_id} --as <handle>`);
|
|
2296
2542
|
}
|
|
2297
|
-
/** Reserved leading words on `octwin
|
|
2298
|
-
const
|
|
2543
|
+
/** Reserved leading words on `octwin work` — see `RECORD_VERBS` for the rule. */
|
|
2544
|
+
const WORK_VERBS = new Set(['assign', 'note', 'stage', 'decide']);
|
|
2545
|
+
/** A localized label bag (or legacy bare string) → one printable string.
|
|
2546
|
+
* The work routes ship labels as full bags so each consumer picks; the CLI
|
|
2547
|
+
* prefers English and falls back to whatever the bag has. */
|
|
2548
|
+
function pickLabel(v) {
|
|
2549
|
+
if (typeof v === 'string')
|
|
2550
|
+
return v;
|
|
2551
|
+
if (v && typeof v === 'object') {
|
|
2552
|
+
const bag = v;
|
|
2553
|
+
return bag.en ?? Object.values(bag)[0] ?? null;
|
|
2554
|
+
}
|
|
2555
|
+
return null;
|
|
2556
|
+
}
|
|
2299
2557
|
/**
|
|
2300
|
-
* The write half of `octwin
|
|
2558
|
+
* The write half of `octwin work` — assign / note / stage / decide.
|
|
2301
2559
|
*
|
|
2302
|
-
* `
|
|
2303
|
-
*
|
|
2304
|
-
*
|
|
2560
|
+
* `stage` is deliberately the XRM records verb (`POST …/xrm/records/:id/stage`):
|
|
2561
|
+
* the work surface carries no second transition spelling — a stage move has one
|
|
2562
|
+
* verb platform-wide (consolidation P-02).
|
|
2563
|
+
*
|
|
2564
|
+
* `decide --dry-run` routes to the PREVIEW endpoint, which sits behind `work:read`
|
|
2565
|
+
* rather than `work:write`: it renders the customer-facing copy and the resulting
|
|
2566
|
+
* stage without committing. That makes "show me what this disposition would do"
|
|
2305
2567
|
* safe to run with a read-only token, which is exactly when an author wants it.
|
|
2306
2568
|
*/
|
|
2307
|
-
async function
|
|
2569
|
+
async function cmdWorkWrite(flags) {
|
|
2308
2570
|
const t = resolveTarget(flags);
|
|
2309
2571
|
const { url } = t;
|
|
2310
2572
|
const base = `${url}/api/self/p`;
|
|
2311
2573
|
const verb = flags._[0];
|
|
2312
|
-
const id = flags._[1] ?? die(`usage: octwin
|
|
2313
|
-
const readBack = () => console.log(`\nRead it back: octwin
|
|
2574
|
+
const id = flags._[1] ?? die(`usage: octwin work ${verb} <recordId> …`);
|
|
2575
|
+
const readBack = () => console.log(`\nRead it back: octwin work ${id}`);
|
|
2314
2576
|
if (verb === 'assign') {
|
|
2315
2577
|
// `--to none` unassigns (the route takes null); anything else must carry the
|
|
2316
2578
|
// principal kind, because a bare uuid cannot say user-or-team.
|
|
2317
2579
|
const to = typeof flags.to === 'string' ? flags.to
|
|
2318
|
-
: die('usage: octwin
|
|
2580
|
+
: die('usage: octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none');
|
|
2319
2581
|
const assignee = to === 'none' ? null : to;
|
|
2320
2582
|
if (assignee !== null && !/^(user|team):/.test(assignee)) {
|
|
2321
2583
|
die(`--to must be 'user:<uuid>', 'team:<uuid>' or 'none' (got '${to}')`);
|
|
2322
2584
|
}
|
|
2323
|
-
console.log(`→ ${assignee === null ? 'Unassigning' : `Assigning to ${assignee}`}
|
|
2324
|
-
const { status, json } = await apiSend('PATCH', `${base}/
|
|
2585
|
+
console.log(`→ ${assignee === null ? 'Unassigning' : `Assigning to ${assignee}`} work item ${id} …`);
|
|
2586
|
+
const { status, json } = await apiSend('PATCH', `${base}/work/${encodeURIComponent(id)}/assign`, { assignee }, t);
|
|
2325
2587
|
if (status !== 200)
|
|
2326
|
-
writeFail(`assign
|
|
2588
|
+
writeFail(`assign work item ${id}`, status, json, url, true);
|
|
2327
2589
|
console.log(assignee === null ? '✓ Unassigned.' : `✓ Assigned to ${json?.assignee ?? assignee}.`);
|
|
2328
2590
|
return;
|
|
2329
2591
|
}
|
|
2330
2592
|
if (verb === 'note') {
|
|
2331
|
-
const note = flags._[2] ?? die('usage: octwin
|
|
2332
|
-
console.log(`→ Adding a note to
|
|
2333
|
-
const { status, json } = await apiSend('POST', `${base}/
|
|
2593
|
+
const note = flags._[2] ?? die('usage: octwin work note <recordId> "the note text"');
|
|
2594
|
+
console.log(`→ Adding a note to work item ${id} …`);
|
|
2595
|
+
const { status, json } = await apiSend('POST', `${base}/work/${encodeURIComponent(id)}/note`, { note }, t);
|
|
2334
2596
|
if (status !== 200)
|
|
2335
|
-
writeFail(`note
|
|
2336
|
-
console.log('✓ Note added to the
|
|
2597
|
+
writeFail(`note work item ${id}`, status, json, url, true);
|
|
2598
|
+
console.log('✓ Note added to the record timeline.');
|
|
2337
2599
|
readBack();
|
|
2338
2600
|
return;
|
|
2339
2601
|
}
|
|
2340
|
-
if (verb === '
|
|
2602
|
+
if (verb === 'stage') {
|
|
2341
2603
|
const to = typeof flags.to === 'string' ? flags.to
|
|
2342
|
-
: die('usage: octwin
|
|
2343
|
-
const body = {
|
|
2604
|
+
: die('usage: octwin work stage <recordId> --to <stage> [--note "..."]');
|
|
2605
|
+
const body = { to_stage: to };
|
|
2344
2606
|
if (typeof flags.note === 'string')
|
|
2345
2607
|
body.note = flags.note;
|
|
2346
|
-
console.log(`→ Moving
|
|
2347
|
-
const { status, json } = await apiSend('POST', `${base}/
|
|
2608
|
+
console.log(`→ Moving record ${id} to '${to}' …`);
|
|
2609
|
+
const { status, json } = await apiSend('POST', `${base}/xrm/records/${encodeURIComponent(id)}/stage`, body, t);
|
|
2348
2610
|
if (status !== 200) {
|
|
2349
|
-
// The
|
|
2611
|
+
// The work detail read carries the legal targets; point at it rather than
|
|
2350
2612
|
// leaving the author to guess the vocabulary.
|
|
2351
2613
|
if (status === 400)
|
|
2352
|
-
console.error(` → legal targets for this
|
|
2353
|
-
writeFail(`move
|
|
2614
|
+
console.error(` → legal targets for this record: octwin work ${id} (see its workflow)`);
|
|
2615
|
+
writeFail(`move record ${id} to '${to}'`, status, json, url, true);
|
|
2354
2616
|
}
|
|
2355
|
-
console.log(`✓
|
|
2617
|
+
console.log(`✓ Record is now '${json?.stage ?? to}'.`);
|
|
2356
2618
|
return;
|
|
2357
2619
|
}
|
|
2358
|
-
// decide
|
|
2620
|
+
// decide — apply one of the entity's declared operator actions
|
|
2359
2621
|
const action = typeof flags.action === 'string' ? flags.action
|
|
2360
|
-
: die('usage: octwin
|
|
2622
|
+
: die('usage: octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]');
|
|
2361
2623
|
const params = {};
|
|
2362
2624
|
for (const pair of flagList(flags, 'param')) {
|
|
2363
2625
|
const eq = pair.indexOf('=');
|
|
@@ -2369,39 +2631,41 @@ async function cmdCasesWrite(flags) {
|
|
|
2369
2631
|
const body = { action, ...(Object.keys(params).length ? { params } : {}) };
|
|
2370
2632
|
if (!dryRun && typeof flags.note === 'string')
|
|
2371
2633
|
body.internal_note = flags.note;
|
|
2372
|
-
console.log(`→ ${dryRun ? 'Previewing' : 'Applying'} '${action}' on
|
|
2373
|
-
const endpoint = `${base}/
|
|
2634
|
+
console.log(`→ ${dryRun ? 'Previewing' : 'Applying'} '${action}' on work item ${id} …`);
|
|
2635
|
+
const endpoint = `${base}/work/${encodeURIComponent(id)}/action${dryRun ? '/preview' : ''}`;
|
|
2374
2636
|
const { status, json } = await apiSend('POST', endpoint, body, t);
|
|
2375
2637
|
if (status === 404)
|
|
2376
|
-
die(`
|
|
2638
|
+
die(`work item '${id}' not found`);
|
|
2377
2639
|
if (status !== 200) {
|
|
2378
|
-
console.error(` → the
|
|
2379
|
-
writeFail(`${dryRun ? 'preview' : 'apply'} '${action}' on
|
|
2640
|
+
console.error(` → the item's applicable actions are listed by: octwin work ${id}`);
|
|
2641
|
+
writeFail(`${dryRun ? 'preview' : 'apply'} '${action}' on work item ${id}`, status, json, url, true);
|
|
2380
2642
|
}
|
|
2381
2643
|
if (dryRun) {
|
|
2382
2644
|
console.log('Preview (nothing was committed):');
|
|
2383
2645
|
console.log(JSON.stringify(json, null, 2));
|
|
2384
2646
|
return;
|
|
2385
2647
|
}
|
|
2386
|
-
console.log(`✓ Applied '${action}' —
|
|
2387
|
-
// `notified`
|
|
2388
|
-
//
|
|
2389
|
-
|
|
2648
|
+
console.log(`✓ Applied '${action}'${json?.to_stage ? ` — record is now '${json.to_stage}'` : ''}.`);
|
|
2649
|
+
// `relayed`/`notified` are the customer-facing half; silence here usually means
|
|
2650
|
+
// the action had no message template, which is easy to mistake for a failure.
|
|
2651
|
+
const reached = json?.relayed || json?.notified;
|
|
2652
|
+
console.log(reached ? ' ✓ the customer was notified.' : ' ⓘ no customer notification was sent by this action.');
|
|
2390
2653
|
readBack();
|
|
2391
2654
|
}
|
|
2392
|
-
/** `octwin
|
|
2393
|
-
* the aggregate inbox, one
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2655
|
+
/** `octwin work [recordId] [--queues]` — inspect the work inbox (every entity the
|
|
2656
|
+
* pack declares worked): the aggregate inbox, one item + its timeline, or the
|
|
2657
|
+
* queue list. */
|
|
2658
|
+
async function cmdWork(flags) {
|
|
2659
|
+
if (typeof flags._[0] === 'string' && WORK_VERBS.has(flags._[0]))
|
|
2660
|
+
return cmdWorkWrite(flags);
|
|
2397
2661
|
const t = resolveTarget(flags);
|
|
2398
2662
|
const { url } = t;
|
|
2399
2663
|
const base = `${url}/api/self/p`;
|
|
2400
|
-
const
|
|
2664
|
+
const recordId = flags._[0];
|
|
2401
2665
|
const asJson = flags.json === true;
|
|
2402
2666
|
if (!asJson)
|
|
2403
|
-
console.log(`→ Reading ${flags.queues === true ? '
|
|
2404
|
-
const
|
|
2667
|
+
console.log(`→ Reading ${flags.queues === true ? 'work queues' : recordId ? `work item ${recordId}` : 'the work inbox'} from ${targetLabel(t)} …`);
|
|
2668
|
+
const workFail = (what, status, json) => {
|
|
2405
2669
|
// A 403 here can also be an RBAC gap the scope hint can't see — a role whose
|
|
2406
2670
|
// grants don't reach the queue passes the scope gate and still gets nothing.
|
|
2407
2671
|
if (status === 403)
|
|
@@ -2409,72 +2673,74 @@ async function cmdCases(flags) {
|
|
|
2409
2673
|
die(`could not read ${what} (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
2410
2674
|
};
|
|
2411
2675
|
if (flags.queues === true) {
|
|
2412
|
-
const { status, json } = await apiGet(`${base}/
|
|
2676
|
+
const { status, json } = await apiGet(`${base}/work/queues`, t);
|
|
2413
2677
|
if (status !== 200)
|
|
2414
|
-
|
|
2678
|
+
workFail('work queues', status, json);
|
|
2415
2679
|
if (asJson) {
|
|
2416
2680
|
console.log(JSON.stringify(json, null, 2));
|
|
2417
2681
|
return;
|
|
2418
2682
|
}
|
|
2419
2683
|
const queues = (json?.queues ?? []);
|
|
2420
|
-
console.log(`
|
|
2421
|
-
for (const q of queues)
|
|
2422
|
-
|
|
2684
|
+
console.log(`Work queues in ${targetLabel(t)}:`);
|
|
2685
|
+
for (const q of queues) {
|
|
2686
|
+
const name = pickLabel(q.name);
|
|
2687
|
+
console.log(` ${q.key}${name ? ` (${name})` : ''} ${q.open_count} open`);
|
|
2688
|
+
}
|
|
2423
2689
|
if (json?.unrouted_open_count)
|
|
2424
2690
|
console.log(` (unrouted: ${json.unrouted_open_count} open)`);
|
|
2425
2691
|
return;
|
|
2426
2692
|
}
|
|
2427
|
-
if (!
|
|
2428
|
-
const { status, json } = await apiGet(`${base}/
|
|
2693
|
+
if (!recordId) {
|
|
2694
|
+
const { status, json } = await apiGet(`${base}/work?${pagingQs(flags)}`, t);
|
|
2429
2695
|
if (status !== 200)
|
|
2430
|
-
|
|
2696
|
+
workFail('the work inbox', status, json);
|
|
2431
2697
|
if (asJson) {
|
|
2432
2698
|
console.log(JSON.stringify(json, null, 2));
|
|
2433
2699
|
return;
|
|
2434
2700
|
}
|
|
2435
2701
|
const page = readPage(json);
|
|
2436
|
-
console.log(`
|
|
2702
|
+
console.log(`Work items in ${targetLabel(t)}: ${page.total ?? page.rows.length} total`);
|
|
2437
2703
|
if (page.rows.length === 0)
|
|
2438
2704
|
console.log(' (none)');
|
|
2439
|
-
for (const
|
|
2440
|
-
const sla =
|
|
2441
|
-
console.log(` #${
|
|
2705
|
+
for (const w of page.rows) {
|
|
2706
|
+
const sla = w.sla_due_at ? ` sla:${w.sla_due_at}` : '';
|
|
2707
|
+
console.log(` #${w.record_number ?? '?'} ${w.entity}${w.type ? `/${w.type}` : ''} [${w.stage ?? '?'}] ${w.priority}${w.queue_key ? ` q:${w.queue_key}` : ''}${sla} ${w.record_id}`);
|
|
2442
2708
|
}
|
|
2443
|
-
const more = morePageHint(page, 'octwin
|
|
2709
|
+
const more = morePageHint(page, 'octwin work');
|
|
2444
2710
|
if (more)
|
|
2445
2711
|
console.log(more);
|
|
2446
|
-
console.log('\nOne
|
|
2712
|
+
console.log('\nOne item + timeline: octwin work <recordId> queues: octwin work --queues');
|
|
2447
2713
|
return;
|
|
2448
2714
|
}
|
|
2449
|
-
const { status, json } = await apiGet(`${base}/
|
|
2715
|
+
const { status, json } = await apiGet(`${base}/work/${encodeURIComponent(recordId)}`, t);
|
|
2450
2716
|
if (status === 404)
|
|
2451
|
-
die(`
|
|
2717
|
+
die(`work item '${recordId}' not found`);
|
|
2452
2718
|
if (status !== 200)
|
|
2453
|
-
|
|
2719
|
+
workFail('work item', status, json);
|
|
2454
2720
|
if (asJson) {
|
|
2455
2721
|
console.log(JSON.stringify(json, null, 2));
|
|
2456
2722
|
return;
|
|
2457
2723
|
}
|
|
2458
|
-
const
|
|
2459
|
-
console.log(
|
|
2460
|
-
console.log(` id: ${
|
|
2724
|
+
const w = json?.item ?? {};
|
|
2725
|
+
console.log(`${pickLabel(json?.entity_label) ?? w.entity} #${w.record_number ?? '?'}${w.type ? ` ${w.type}` : ''} [${w.stage ?? '?'}] ${w.priority}`);
|
|
2726
|
+
console.log(` id: ${w.record_id} queue: ${w.queue_key ?? '(unrouted)'} assignee: ${w.assignee_principal ?? '(none)'}`);
|
|
2461
2727
|
if (json?.contact)
|
|
2462
2728
|
console.log(` contact: ${json.contact.display_name ?? json.contact.channel_contact_handle ?? json.contact.id}`);
|
|
2463
|
-
if (
|
|
2464
|
-
console.log(` conversation: ${
|
|
2465
|
-
if (
|
|
2466
|
-
console.log(` sla due: ${
|
|
2467
|
-
if (
|
|
2468
|
-
console.log(` fields: ${JSON.stringify(
|
|
2469
|
-
const events = (json?.
|
|
2729
|
+
if (w.conversation_id)
|
|
2730
|
+
console.log(` conversation: ${w.conversation_id} (octwin logs ${w.conversation_id})`);
|
|
2731
|
+
if (w.sla_due_at)
|
|
2732
|
+
console.log(` sla due: ${w.sla_due_at}`);
|
|
2733
|
+
if (w.fields && Object.keys(w.fields).length > 0)
|
|
2734
|
+
console.log(` fields: ${JSON.stringify(w.fields)}`);
|
|
2735
|
+
const events = (json?.timeline ?? []);
|
|
2470
2736
|
console.log(` Timeline (${events.length}):`);
|
|
2471
2737
|
for (const e of events) {
|
|
2472
2738
|
const payload = e.payload && Object.keys(e.payload).length > 0 ? ` ${JSON.stringify(e.payload)}` : '';
|
|
2473
2739
|
console.log(` ${e.ts ?? ''} ${e.kind}${e.actor ? ` (${e.actor})` : ''}${payload}`);
|
|
2474
2740
|
}
|
|
2475
|
-
const
|
|
2476
|
-
if (
|
|
2477
|
-
console.log(`
|
|
2741
|
+
const actions = (json?.actions ?? []);
|
|
2742
|
+
if (actions.length > 0) {
|
|
2743
|
+
console.log(` Actions: ${actions.map((a) => `${a.action}${a.to_stage ? `→${a.to_stage}` : ''}`).join(', ')}`);
|
|
2478
2744
|
}
|
|
2479
2745
|
}
|
|
2480
2746
|
// ── money formatting (orders / catalog) ─────────────────────────────────────
|
|
@@ -2891,21 +3157,32 @@ async function cmdOrdersWrite(flags) {
|
|
|
2891
3157
|
if (verb === 'transition') {
|
|
2892
3158
|
const to = typeof flags.to === 'string' ? flags.to
|
|
2893
3159
|
: die('usage: octwin orders transition <reference_id> --to <status>');
|
|
3160
|
+
// A stage move has ONE verb platform-wide — `POST …/xrm/records/:id/stage`
|
|
3161
|
+
// (`POST …/orders/:ref/transition` was retired, consolidation P-02). The order
|
|
3162
|
+
// detail read resolves the opaque reference to the record id and carries the
|
|
3163
|
+
// legal targets for the failure hint.
|
|
2894
3164
|
console.log(`→ Moving order ${ref} to '${to}' …`);
|
|
2895
|
-
const
|
|
2896
|
-
if (status === 404)
|
|
3165
|
+
const detail = await apiGet(`${base}/${encodeURIComponent(ref)}`, t);
|
|
3166
|
+
if (detail.status === 404)
|
|
2897
3167
|
die(`order '${ref}' not found (pass the opaque reference_id, not the #number)`);
|
|
2898
|
-
if (status
|
|
3168
|
+
if (detail.status !== 200)
|
|
3169
|
+
writeFail(`read order ${ref}`, detail.status, detail.json, url);
|
|
3170
|
+
const recordId = detail.json?.order?.id;
|
|
3171
|
+
if (!recordId)
|
|
3172
|
+
die(`order '${ref}' carries no record id — cannot move its stage`);
|
|
3173
|
+
const { status, json } = await apiSend('POST', `${url}/api/self/p/xrm/records/${encodeURIComponent(recordId)}/stage`, { to_stage: to }, t);
|
|
3174
|
+
if (status === 400 || status === 409) {
|
|
2899
3175
|
console.error(`✗ '${to}' is not a legal move for this order.`);
|
|
2900
|
-
|
|
2901
|
-
|
|
3176
|
+
const allowed = (detail.json?.transitions ?? []);
|
|
3177
|
+
if (allowed.length > 0)
|
|
3178
|
+
console.error(` → allowed: ${allowed.join(', ')}`);
|
|
2902
3179
|
else
|
|
2903
3180
|
console.error(` → see the allowed set: octwin orders ${ref}`);
|
|
2904
3181
|
process.exit(1);
|
|
2905
3182
|
}
|
|
2906
3183
|
if (status !== 200)
|
|
2907
3184
|
writeFail(`move order ${ref} to '${to}'`, status, json, url);
|
|
2908
|
-
console.log(`✓ Order is now '${json?.
|
|
3185
|
+
console.log(`✓ Order is now '${json?.stage ?? to}'.`);
|
|
2909
3186
|
return;
|
|
2910
3187
|
}
|
|
2911
3188
|
// refund — irreversible, and money. `--force` rather than a prompt: the CLI is
|
|
@@ -3290,7 +3567,7 @@ async function cmdSchedulingWrite(flags) {
|
|
|
3290
3567
|
console.log(JSON.stringify(json, null, 2));
|
|
3291
3568
|
return;
|
|
3292
3569
|
}
|
|
3293
|
-
if (json?.
|
|
3570
|
+
if (json?.has_data === false) {
|
|
3294
3571
|
console.log('This pack declares no scheduling.');
|
|
3295
3572
|
return;
|
|
3296
3573
|
}
|
|
@@ -3365,7 +3642,7 @@ async function cmdSchedulingWrite(flags) {
|
|
|
3365
3642
|
const { status, json } = await apiSend('POST', `${base}/availability/${path}`, body, t);
|
|
3366
3643
|
if (status !== 201 && status !== 200)
|
|
3367
3644
|
writeFail(`add the ${noun}`, status, json, url);
|
|
3368
|
-
if (json?.
|
|
3645
|
+
if (json?.has_data === false)
|
|
3369
3646
|
die('this pack declares no scheduling');
|
|
3370
3647
|
const created = json?.rule ?? json?.exception ?? {};
|
|
3371
3648
|
console.log(`✓ ${noun[0].toUpperCase()}${noun.slice(1)} added — ${created.id ?? '(no id returned)'}`);
|
|
@@ -3386,30 +3663,42 @@ async function cmdScheduling(flags) {
|
|
|
3386
3663
|
if (flags.slots === true)
|
|
3387
3664
|
die('usage: octwin scheduling --slots <resourceRecordId> (the record id of a bookable resource)');
|
|
3388
3665
|
if (resourceId) {
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
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);
|
|
3394
3673
|
if (!asJson)
|
|
3395
3674
|
console.log(`→ Computing slots for resource ${resourceId} in ${targetLabel(t)} …`);
|
|
3396
|
-
const { status, json } = await apiGet(`${base}/
|
|
3397
|
-
if (status === 404)
|
|
3398
|
-
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);
|
|
3399
3676
|
if (status === 400)
|
|
3400
|
-
die(`
|
|
3677
|
+
die(`could not compute slots${errDetail(json)}`);
|
|
3401
3678
|
if (status !== 200)
|
|
3402
3679
|
die(`could not compute slots (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
3403
3680
|
if (asJson) {
|
|
3404
3681
|
console.log(JSON.stringify(json, null, 2));
|
|
3405
3682
|
return;
|
|
3406
3683
|
}
|
|
3407
|
-
if (json?.
|
|
3684
|
+
if (json?.has_data === false) {
|
|
3408
3685
|
console.log('This pack declares no `scheduling.yaml` — nothing to schedule.');
|
|
3409
3686
|
return;
|
|
3410
3687
|
}
|
|
3411
|
-
|
|
3412
|
-
|
|
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
|
+
}
|
|
3413
3702
|
if (slots.length === 0)
|
|
3414
3703
|
console.log(' (none — no availability rules cover this window; `octwin deploy --seed` seeds the demo rules)');
|
|
3415
3704
|
for (const s of slots) {
|
|
@@ -3426,7 +3715,7 @@ async function cmdScheduling(flags) {
|
|
|
3426
3715
|
console.log(JSON.stringify(json, null, 2));
|
|
3427
3716
|
return;
|
|
3428
3717
|
}
|
|
3429
|
-
if (json?.
|
|
3718
|
+
if (json?.has_data === false) {
|
|
3430
3719
|
console.log('This pack declares no `scheduling.yaml` — nothing to schedule.');
|
|
3431
3720
|
return;
|
|
3432
3721
|
}
|
|
@@ -3438,254 +3727,274 @@ async function cmdScheduling(flags) {
|
|
|
3438
3727
|
console.log('\nSlots for one resource: octwin scheduling --slots <resourceRecordId> (ids: octwin records <entity>)');
|
|
3439
3728
|
}
|
|
3440
3729
|
function help() {
|
|
3441
|
-
console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
|
|
3442
|
-
|
|
3443
|
-
octwin --version # print the CLI version (+ any upgrade notice)
|
|
3444
|
-
octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
3445
|
-
octwin validate [--dir .] [--remote] [--require-kb] # --remote runs the platform's FULL schema check + lint (all errors at once)
|
|
3446
|
-
octwin login --url <platformUrl> --token oct_… # a deploy token from the console
|
|
3447
|
-
octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
|
|
3448
|
-
octwin projects [--archived] [--json] # the --project slugs this token can name
|
|
3449
|
-
octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
3450
|
-
octwin status [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
|
|
3451
|
-
octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
|
|
3452
|
-
octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
|
|
3453
|
-
octwin
|
|
3454
|
-
octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
|
|
3455
|
-
octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
|
|
3456
|
-
octwin media generate "<prompt>" [--out <file.png>] [--
|
|
3457
|
-
octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
|
|
3458
|
-
octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
|
|
3459
|
-
octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
|
|
3460
|
-
octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
|
|
3461
|
-
octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
|
|
3462
|
-
octwin platform-kb [pull] [--dir .] [--url <url>]
|
|
3463
|
-
octwin test [--dir .] # = validate --remote (the full platform check)
|
|
3464
|
-
octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
|
|
3465
|
-
|
|
3466
|
-
Writes — exercise the state your pack creates (each needs the matching :write scope):
|
|
3467
|
-
octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…"
|
|
3468
|
-
octwin records tasks | task complete <taskId> [--outcome done|cancelled]
|
|
3469
|
-
octwin
|
|
3470
|
-
octwin
|
|
3471
|
-
octwin orders transition <ref> --to <status> | refund <ref> --force
|
|
3472
|
-
octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
|
|
3473
|
-
octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
|
|
3474
|
-
octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
|
|
3475
|
-
|
|
3476
|
-
Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
|
|
3477
|
-
\`octwin chat --as <h>\` calls continue the same conversation; press a rendered
|
|
3478
|
-
button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
|
|
3479
|
-
Get a deploy token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
|
|
3480
|
-
octwin platform-kb pull → writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
|
|
3481
|
-
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).
|
|
3482
3771
|
Per-command usage: octwin <command> --help`);
|
|
3483
3772
|
}
|
|
3484
3773
|
/** Per-subcommand usage — printed for `octwin <cmd> --help|-h` BEFORE any
|
|
3485
3774
|
* network/auth work (a --help that 401s is worse than no help at all). */
|
|
3486
3775
|
const COMMAND_HELP = {
|
|
3487
|
-
init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
3776
|
+
init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
3488
3777
|
Scaffold a pure-YAML starter pack into <dir>.`,
|
|
3489
|
-
validate: `octwin validate [--dir .] [--remote] [--require-kb]
|
|
3490
|
-
Offline structural check, plus two checks driven by the pulled capability
|
|
3491
|
-
reference (render-intent fields, primitive arguments). Those two SKIP when the
|
|
3492
|
-
reference is missing — the run says so, and --require-kb turns the skip into a
|
|
3493
|
-
failure for CI. --remote additionally runs the platform's FULL manifest +
|
|
3494
|
-
flow-DSL validation and its flow lint (all errors at once) — same check as deploy
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
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
|
|
3498
3790
|
workspace + project pin + scopes the token reaches.`,
|
|
3499
|
-
whoami: `octwin whoami [--url <url>] [--tenant <slug>]
|
|
3791
|
+
whoami: `octwin whoami [--url <url>] [--tenant <slug>]
|
|
3500
3792
|
Verify the resolved token authenticates against the tenant.`,
|
|
3501
|
-
projects: `octwin projects [--archived] [--json]
|
|
3502
|
-
List the workspace's projects — the slugs every --project flag takes, with the
|
|
3503
|
-
plan's project cap. --archived includes archived ones. A pack:deploy token
|
|
3504
|
-
reaches this (it names a project in every other command).
|
|
3505
|
-
|
|
3506
|
-
octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
|
|
3507
|
-
Create a project. The URL slug is derived from the name unless --slug pins one.
|
|
3508
|
-
--pack installs an ALREADY-published pack; the usual next step is instead
|
|
3509
|
-
\`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
|
|
3510
|
-
|
|
3511
|
-
octwin projects rm <slug> [--yes]
|
|
3512
|
-
HARD delete — the project and everything cascading from it (conversations,
|
|
3513
|
-
contacts, records, installs). No undo, and not the same as archiving.
|
|
3514
|
-
WITHOUT --yes it only previews what would be destroyed, so the dry run is the
|
|
3515
|
-
default. Together these make a disposable end-to-end environment:
|
|
3516
|
-
octwin projects create "Scratch" && octwin deploy --project scratch --seed
|
|
3517
|
-
octwin chat "hi" --project scratch
|
|
3518
|
-
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
|
|
3519
3811
|
Both verbs need the \`projects:write\` scope — a pack:deploy token does NOT confer it.`,
|
|
3520
|
-
deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
3521
|
-
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.
|
|
3522
3814
|
--seed additionally applies the pack's demo seed (streams progress).`,
|
|
3523
|
-
seed: `octwin seed [--pack <packId>]
|
|
3524
|
-
Apply the pack's demo/reference data to the project it is installed on, without
|
|
3525
|
-
redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
|
|
3526
|
-
and the demo operator topology. Reports what each kind produced.
|
|
3527
|
-
Idempotent and safe to re-run — records upsert, and existing media is REUSED rather
|
|
3528
|
-
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
|
|
3529
3821
|
project somehow runs more than one.`,
|
|
3530
|
-
status: `octwin status [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
|
|
3531
|
-
Show installed vs live version + the flow list for this pack
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
octwin records
|
|
3542
|
-
octwin records
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
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
|
|
3548
3844
|
VERB — to list an entity actually named one of those, use \`--entity <name>\`.`,
|
|
3549
|
-
|
|
3550
|
-
Inspect
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
octwin
|
|
3556
|
-
octwin
|
|
3557
|
-
octwin
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
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\`).
|
|
3859
|
+
\`stage\` is the XRM records verb (one transition spelling platform-wide).`,
|
|
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.
|
|
3565
3863
|
--json = raw events (verbatim payloads).`,
|
|
3566
|
-
pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
|
|
3567
|
-
Write a DEPLOYED pack's source back to disk — the inverse of deploy.
|
|
3568
|
-
A pack pushed with 'octwin deploy' lives on the platform as an artifact the
|
|
3569
|
-
runtime serves but nothing hands back, so its only source copy is the machine
|
|
3570
|
-
that pushed it. Pull it, fix it, redeploy it.
|
|
3571
|
-
Defaults to the version installed on the target project; --version overrides.
|
|
3572
|
-
--dir defaults to ./<packId>; a non-empty dir needs --force.
|
|
3573
|
-
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.
|
|
3574
3872
|
You may pull a pack your tenant OWNS (deployed); an operator token pulls any.`,
|
|
3575
|
-
chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
|
|
3576
|
-
octwin chat --script <file> [--as <handle>] [--json]
|
|
3577
|
-
Drive ONE turn through the dev web channel and print every render with its
|
|
3578
|
-
tap ids. Same --as handle = same conversation (multi-turn works).
|
|
3579
|
-
--tap presses a rendered button/list row instead of sending text.
|
|
3580
|
-
--media uploads a local file (or a media id from 'media generate --json') as
|
|
3581
|
-
an image/document/audio inbound — any "message" rides as its caption; feeds a
|
|
3582
|
-
running media-collect flow (e.g. activate-app).
|
|
3583
|
-
--json dumps the raw SSE envelopes for the turn.
|
|
3584
|
-
|
|
3585
|
-
--script drives a WHOLE conversation from a file, ONE TURN PER LINE, in one
|
|
3586
|
-
process over one connection — waiting for each turn to settle before sending
|
|
3587
|
-
the next. Use this for any multi-step flow: chaining shell invocations races
|
|
3588
|
-
the agent loop, because a turn ends on a quiet gap that can arrive while the
|
|
3589
|
-
server is still working (the symptom is placeholder-filled fields or a second
|
|
3590
|
-
workflow run). Blank lines and # comments are skipped:
|
|
3591
|
-
|
|
3592
|
-
# book an appointment end to end
|
|
3593
|
-
احجز موعد
|
|
3594
|
-
tap:t:invoke:book-appointment:doctor_id=D1
|
|
3595
|
-
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
|
|
3596
3894
|
tap:t:resume:book-appointment:run_id=R1;_ctl_approved=true`,
|
|
3597
|
-
media: `octwin media generate "<prompt>" [--out <file.png>] [--
|
|
3598
|
-
AI-generate an image (needs a media:generate-scoped token), store it as a
|
|
3599
|
-
public asset, and print its MEDIA- handle + serve URL. --out downloads the
|
|
3600
|
-
bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
|
|
3601
|
-
|
|
3602
|
-
agents: `octwin agents [packId::agentId] [--prompt] [--json]
|
|
3603
|
-
No args = the roster with each agent's EFFECTIVE model and which layer set it.
|
|
3604
|
-
With an agent = every governed setting (model / memory.last_messages /
|
|
3605
|
-
working_memory) plus the layer that won — an operator PLATFORM default can
|
|
3606
|
-
override what your manifest declares, and this is where you see that.
|
|
3607
|
-
--prompt = the exact system prompt the LLM sees for this project (pack
|
|
3608
|
-
instructions + platform protocol + any project overlay). Needs agents:read.
|
|
3609
|
-
The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
|
|
3610
|
-
|
|
3611
|
-
WRITES (need \`agents:write\`):
|
|
3612
|
-
octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
|
|
3613
|
-
[--enable-tool <toolId>] [--disable-tool <toolId>]
|
|
3614
|
-
|
|
3615
|
-
Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
|
|
3616
|
-
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
|
|
3617
3915
|
ids refuses --model with a 403 — the platform default governs there.`,
|
|
3618
|
-
orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
|
|
3619
|
-
No args = the order list (#number, status/payment, total, contact). With a
|
|
3620
|
-
reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
|
|
3621
|
-
payment_ref, and the allowed status transitions. Needs orders:read + the
|
|
3622
|
-
\`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
|
|
3623
|
-
so \`pending\` on a gateway-less workspace is expected, not a bug.
|
|
3624
|
-
|
|
3625
|
-
WRITES (need \`orders:write\`):
|
|
3626
|
-
octwin orders transition <reference_id> --to <status>
|
|
3627
|
-
octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
|
|
3628
|
-
|
|
3629
|
-
Refund is irreversible and moves money, hence --force. The route answers 200 even
|
|
3630
|
-
when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
|
|
3631
|
-
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
|
|
3632
3930
|
\`captured\` state can be refunded; \`payment_status\` is never settable directly.`,
|
|
3633
|
-
analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
|
|
3634
|
-
No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
|
|
3635
|
-
With an entity = stage-by-stage conversion (default --funnel) over the last 30
|
|
3636
|
-
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
|
|
3637
3935
|
range-filtered). Needs records:read + a \`view\` grant on \`record.<entity>\`.`,
|
|
3638
|
-
catalog: `octwin catalog [--readiness] [--json]
|
|
3639
|
-
The commerce \`product\` records + price, availability, stock (null = not
|
|
3640
|
-
inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
|
|
3641
|
-
Graph checklist (LIVE Graph calls; needs a bound access token). Needs
|
|
3642
|
-
catalog:read + the \`catalog\` plan feature.
|
|
3643
|
-
|
|
3644
|
-
WRITES (need \`catalog:write\`):
|
|
3645
|
-
octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
|
|
3646
|
-
octwin catalog stock <retailerId> [--set-on-hand <n>]
|
|
3647
|
-
|
|
3648
|
-
\`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
|
|
3649
|
-
inventory-tracked (always sellable), which is different from 0. Lowering on_hand
|
|
3650
|
-
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
|
|
3651
3949
|
products and the Meta catalog binding/sync stay in the console.`,
|
|
3652
|
-
scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
|
|
3653
|
-
No args = the engine state (bookable resource types, upcoming slots, booked
|
|
3654
|
-
seats). --slots <recordId> computes the slots for one bookable resource
|
|
3655
|
-
(occupancy included; --days is clamped to 1-31 server-side) — the way to verify
|
|
3656
|
-
the availability rules a \`deploy --seed\` created. Needs scheduling:read.
|
|
3657
|
-
|
|
3658
|
-
RULES (list needs scheduling:read; add/rm need scheduling:write):
|
|
3659
|
-
octwin scheduling rules --resource <resourceRecordId>
|
|
3660
|
-
octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
|
|
3661
|
-
[--slot-minutes 30] [--capacity 1]
|
|
3662
|
-
octwin scheduling rule rm <ruleId>
|
|
3663
|
-
octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
|
|
3664
|
-
[--start 09:00 --end 13:00]
|
|
3665
|
-
octwin scheduling exception rm <exceptionId>
|
|
3666
|
-
|
|
3667
|
-
--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
|
|
3668
3966
|
\`--slots\` is how you check what a rule actually produces.`,
|
|
3669
|
-
'platform-kb': `octwin platform-kb [pull] [--
|
|
3670
|
-
Pull the platform capability reference (markdown + JSON catalogs) into
|
|
3671
|
-
.octwin/platform-kb/ for the octwin-pack authoring skill
|
|
3672
|
-
|
|
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
|
|
3980
|
+
agent loops that want to branch without parsing prose.`,
|
|
3981
|
+
test: `octwin test [--dir .]
|
|
3673
3982
|
Alias for \`octwin validate --remote\` — the full platform check.`,
|
|
3674
|
-
feedback: `octwin feedback [--dir .]
|
|
3675
|
-
Submit this pack's FEEDBACK.md to the platform team.
|
|
3676
|
-
The octwin-pack skill writes that file in its last step — findings grouped by
|
|
3677
|
-
owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
|
|
3678
|
-
you to paste it into a chat.
|
|
3679
|
-
Attaches the pack id + version from manifest.yaml, this CLI's version, and the
|
|
3680
|
-
content_hash of the capability reference in .octwin/platform-kb/ — triage needs
|
|
3681
|
-
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
|
|
3682
3991
|
"you were reading a stale reference". Needs the \`pack:deploy\` scope.`,
|
|
3683
3992
|
};
|
|
3684
3993
|
async function main() {
|
|
3685
3994
|
const [command, ...rest] = process.argv.slice(2);
|
|
3686
3995
|
const flags = parseFlags(rest);
|
|
3687
3996
|
// So an auth failure can name the scope THIS invocation needs. A leading write
|
|
3688
|
-
// verb changes the answer (`
|
|
3997
|
+
// verb changes the answer (`work` reads, `work note` writes), so it rides along
|
|
3689
3998
|
// when the first positional is one — `VERB_REQUIREMENTS` is keyed that way.
|
|
3690
3999
|
const leadingVerb = flags._[0];
|
|
3691
4000
|
CURRENT_COMMAND = (typeof leadingVerb === 'string' && command && `${command} ${leadingVerb}` in VERB_REQUIREMENTS)
|
|
@@ -3722,8 +4031,8 @@ async function main() {
|
|
|
3722
4031
|
case 'records':
|
|
3723
4032
|
await cmdRecords(flags);
|
|
3724
4033
|
break;
|
|
3725
|
-
case '
|
|
3726
|
-
await
|
|
4034
|
+
case 'work':
|
|
4035
|
+
await cmdWork(flags);
|
|
3727
4036
|
break;
|
|
3728
4037
|
case 'logs':
|
|
3729
4038
|
await cmdLogs(flags);
|