octwin-cli 0.8.11 → 0.8.13
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 +60 -0
- package/README.md +8 -5
- package/dist/index.js +616 -416
- package/dist/lib/args-check.js +55 -0
- package/dist/lib/chat-line.js +44 -0
- package/dist/lib/outcome.js +11 -0
- package/dist/lib/template-check.js +4 -2
- package/package.json +2 -2
- package/templates/starter/flows/tools/browse.flow.yaml +7 -0
- package/templates/starter/flows/tools/browse.locale.ar.yaml +1 -0
- package/templates/starter/manifest.yaml +1 -1
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* octwin --version | -v # print the CLI version (+ any upgrade notice)
|
|
11
11
|
* octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
12
|
-
* octwin validate [--dir .] [--remote] [--require-kb] #
|
|
12
|
+
* octwin validate [--dir .] [--remote | --offline] [--require-kb] # the platform's FULL check by default (no login needed); --offline = the partial subset
|
|
13
13
|
* octwin login --url <platformUrl> [--token oct_…] # no token → approve in a browser
|
|
14
14
|
* octwin whoami [--url <url>] [--tenant <slug>]
|
|
15
15
|
* octwin projects [--archived] # the --project slugs this token can name
|
|
@@ -66,10 +66,10 @@ import { parse as parseYaml } from 'yaml';
|
|
|
66
66
|
import { applyRenames } from './lib/rename.js';
|
|
67
67
|
import { validatePackBundle, describePackNameProblem, asPackName } from './lib/validate.js';
|
|
68
68
|
import { loadAllowedRenderKeys, findRenderKeyViolations, describeRenderFinding } from './lib/render-check.js';
|
|
69
|
-
import { loadPrimitiveArgSpecs, findArgViolations, describeArgFinding } from './lib/args-check.js';
|
|
69
|
+
import { loadPrimitiveArgSpecs, findArgViolations, describeArgFinding, findArgWarnings, describeArgWarning, } from './lib/args-check.js';
|
|
70
70
|
import { yamlLineOf } from './lib/yaml-pos.js';
|
|
71
71
|
import { localhostFamilyHint } from './lib/net-hint.js';
|
|
72
|
-
import { authHeaders as buildAuthHeaders } from './lib/client-headers.js';
|
|
72
|
+
import { authHeaders as buildAuthHeaders, clientReportHeaders } from './lib/client-headers.js';
|
|
73
73
|
import { loadBuiltinNames, findBuiltinViolations, describeBuiltinFinding } from './lib/builtin-check.js';
|
|
74
74
|
import { loadTemplateSpecs, findTemplateViolations, describeTemplateFinding } from './lib/template-check.js';
|
|
75
75
|
import { loadSystemEntities, findEntityViolations, describeEntityFinding } from './lib/entity-check.js';
|
|
@@ -78,6 +78,7 @@ import { findPlatformKbDir } from './lib/kb-path.js';
|
|
|
78
78
|
import { reportChecks } from './lib/outcome.js';
|
|
79
79
|
import { classifyPackPath, isSkippedDir } from './lib/pack-source.js';
|
|
80
80
|
import { readPage, morePageHint } from './lib/page.js';
|
|
81
|
+
import { chatLabel, answersTheTurn } from './lib/chat-line.js';
|
|
81
82
|
import { kbOneLiner, buildKbIndexMarkdown, buildKbOutlineMarkdown, } from './lib/kb-index.js';
|
|
82
83
|
import { buildSymbols, linkExplainers, renderSymbolsMarkdown } from './lib/kb-symbols.js';
|
|
83
84
|
import { readPending, writePending, clearPending, isPendingExpired, secondsLeft, interpretPoll, clientLabel, EXIT_STILL_PENDING, } from './lib/device-login.js';
|
|
@@ -199,6 +200,8 @@ const VERB_REQUIREMENTS = {
|
|
|
199
200
|
'records patch': { scope: 'records:write', feature: 'records' },
|
|
200
201
|
'records stage': { scope: 'records:write', feature: 'records' },
|
|
201
202
|
'records note': { scope: 'records:write', feature: 'records' },
|
|
203
|
+
'records archive': { scope: 'records:write', feature: 'records' },
|
|
204
|
+
'records restore': { scope: 'records:write', feature: 'records' },
|
|
202
205
|
'records tasks': { scope: 'records:read', feature: 'tasks' },
|
|
203
206
|
'records task': { scope: 'records:write', feature: 'tasks' },
|
|
204
207
|
'work assign': { scope: 'work:write', feature: 'work' },
|
|
@@ -872,7 +875,8 @@ function exitNow(code) {
|
|
|
872
875
|
* `platform-kb` refreshes the reference itself, so it needs no nudge). */
|
|
873
876
|
function commandTouchesPlatform(command, flags) {
|
|
874
877
|
switch (command) {
|
|
875
|
-
|
|
878
|
+
// `validate` runs the FULL check against the platform by default now; only `--offline` stays offline.
|
|
879
|
+
case 'validate': return flags.offline !== true;
|
|
876
880
|
case 'deploy':
|
|
877
881
|
case 'status':
|
|
878
882
|
case 'test':
|
|
@@ -1049,13 +1053,33 @@ async function cmdValidate(flags) {
|
|
|
1049
1053
|
if (!args.specs)
|
|
1050
1054
|
return { kind: 'not-run', lookup: args.lookup };
|
|
1051
1055
|
const findings = withLines(yamlDocs().flatMap(([p, doc]) => findArgViolations(doc, p, args.specs)));
|
|
1052
|
-
|
|
1053
|
-
|
|
1056
|
+
if (findings.length) {
|
|
1057
|
+
return {
|
|
1054
1058
|
kind: 'findings', noun: 'primitive-argument error',
|
|
1055
1059
|
lines: findings.map(describeArgFinding),
|
|
1056
1060
|
hint: 'fix these before deploying — an undeclared argument is dropped with no error at runtime',
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Errors first, and only then the warnings — a `findings` outcome exits, so a warning
|
|
1065
|
+
* printed above it would never be read, and one printed below it would never be reached.
|
|
1066
|
+
*
|
|
1067
|
+
* `warnsTogether` is the platform's own declaration (`record_save` with `record_id:` +
|
|
1068
|
+
* `stage:` refuses the whole write on an existing record). It has been enforced server-side
|
|
1069
|
+
* since 2026-09-06 and was reachable only through `--remote`; the author who lost a write
|
|
1070
|
+
* to it was on CLI 0.6.0, offline. Measured before shipping: **0 findings across all 48
|
|
1071
|
+
* `record_save` call sites in the 23 marketplace packs** — the check is silent on a clean
|
|
1072
|
+
* corpus, which is the bar a warning has to clear before it is worth printing at all.
|
|
1073
|
+
*/
|
|
1074
|
+
const warnings = withLines(yamlDocs().flatMap(([p, doc]) => findArgWarnings(doc, p, args.specs)));
|
|
1075
|
+
if (warnings.length) {
|
|
1076
|
+
return {
|
|
1077
|
+
kind: 'warnings', noun: 'argument-combination warning',
|
|
1078
|
+
lines: warnings.map(describeArgWarning),
|
|
1079
|
+
hint: 'legal YAML, and usually not what you meant — read each one and decide.',
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
return { kind: 'passed', line: 'primitive arguments match their declared inputs' };
|
|
1059
1083
|
},
|
|
1060
1084
|
},
|
|
1061
1085
|
{
|
|
@@ -1172,36 +1196,62 @@ async function cmdValidate(flags) {
|
|
|
1172
1196
|
if (skipped.length && flags['require-kb'] === true) {
|
|
1173
1197
|
die(`--require-kb: ${skipped.length} check${skipped.length === 1 ? '' : 's'} could not run (${skipped.join(', ')})`);
|
|
1174
1198
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1199
|
+
/**
|
|
1200
|
+
* ── The FULL check is the DEFAULT — the one deploy runs ─────────────────────────
|
|
1201
|
+
*
|
|
1202
|
+
* The checks above are a SUBSET: they replay what the KB publishes, file by file. On
|
|
1203
|
+
* 2026-09-25 they passed a pack the platform refused — a starter flow left behind with
|
|
1204
|
+
* Arabic copy only, in a pack that ships English — and "the offline check passed" had read
|
|
1205
|
+
* as "deploy will pass". So `validate` now asks the platform unless told not to, and says
|
|
1206
|
+
* PARTIAL, on its last line, whenever it could not.
|
|
1207
|
+
*
|
|
1208
|
+
* Two doors, one check (`routes/packs/_validate-bundle.ts` on the platform):
|
|
1209
|
+
* • a token AND a named project (`--project` / `PACK_PROJECT`) → the logged-in door,
|
|
1210
|
+
* which also confirms the project exists — the right pre-flight right before a deploy;
|
|
1211
|
+
* • anything else → `POST /api/public/packs/validate`, which needs no login and no
|
|
1212
|
+
* project. An unpinned token with no project would get a 400 on the other door, and a
|
|
1213
|
+
* default must not fail for a config gap the check itself does not care about.
|
|
1214
|
+
*
|
|
1215
|
+
* `--remote` REQUIRES the full check (a failure to reach it is an error); `--offline` skips it.
|
|
1216
|
+
*/
|
|
1217
|
+
if (flags.offline === true)
|
|
1218
|
+
return printPartialTrailer(id, version, skipped, 'you passed --offline');
|
|
1219
|
+
const t = readTarget(flags);
|
|
1220
|
+
if (!t.url) {
|
|
1221
|
+
if (flags.remote === true)
|
|
1222
|
+
die('--remote needs a platform url — pass --url, set PACK_PLATFORM_URL, or `octwin login --url <url>`');
|
|
1223
|
+
return printPartialTrailer(id, version, skipped, 'no platform url is set — pass --url, set PACK_PLATFORM_URL, or `octwin login --url <url>`');
|
|
1224
|
+
}
|
|
1225
|
+
const loggedInDoor = !!t.token && !!t.project;
|
|
1226
|
+
const endpoint = loggedInDoor ? `${t.url}/api/self/p/packs/validate` : `${t.url}/api/public/packs/validate`;
|
|
1227
|
+
const headers = loggedInDoor
|
|
1228
|
+
? authHeaders(t)
|
|
1229
|
+
// The public door takes no credential — but it still gets this run's client report, so a
|
|
1230
|
+
// stale reference is flagged exactly as it is for a logged-in author.
|
|
1231
|
+
: clientReportHeaders({ cliVersion: VERSION, kbContentHash: LOCAL_KB?.content_hash ?? null });
|
|
1232
|
+
console.log(`→ Running the platform's full check${loggedInDoor ? ` against ${targetLabel(t)}` : ' (no login needed)'} @ ${t.url} …`);
|
|
1194
1233
|
// `--strict-primitives` (opt-in): the server additionally type-checks LITERAL
|
|
1195
1234
|
// `args:` values against each primitive's per-key input schema. Expression
|
|
1196
1235
|
// strings are always exempt — this judges only plain YAML scalars.
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1236
|
+
let res;
|
|
1237
|
+
try {
|
|
1238
|
+
res = await fetch(endpoint, {
|
|
1239
|
+
method: 'POST',
|
|
1240
|
+
headers: { 'content-type': 'application/json', ...headers },
|
|
1241
|
+
body: JSON.stringify({
|
|
1242
|
+
files, blobs,
|
|
1243
|
+
...(flags['strict-primitives'] === true ? { strict_primitives: true } : {}),
|
|
1244
|
+
}),
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
catch (err) {
|
|
1248
|
+
const why = `platform unreachable at ${t.url} (${err?.message ?? err})`;
|
|
1249
|
+
if (flags.remote === true) {
|
|
1250
|
+
const hint = localhostFamilyHint(t.url, err);
|
|
1251
|
+
die(`full check — ${why}${hint ? `\n ${hint}` : ''}`);
|
|
1252
|
+
}
|
|
1253
|
+
return printPartialTrailer(id, version, skipped, why);
|
|
1254
|
+
}
|
|
1205
1255
|
const text = await res.text();
|
|
1206
1256
|
let json;
|
|
1207
1257
|
try {
|
|
@@ -1210,6 +1260,14 @@ async function cmdValidate(flags) {
|
|
|
1210
1260
|
catch {
|
|
1211
1261
|
json = text;
|
|
1212
1262
|
}
|
|
1263
|
+
// A platform older than the public door answers Fastify's default 404 — degrade to the
|
|
1264
|
+
// subset (loudly), or fail when the full check was REQUIRED.
|
|
1265
|
+
if (!loggedInDoor && res.status === 404 && (typeof json !== 'object' || json === null || json.error === 'Not Found')) {
|
|
1266
|
+
const why = `the platform at ${t.url} has no public validate yet — log in and pass --project for the full check`;
|
|
1267
|
+
if (flags.remote === true)
|
|
1268
|
+
die(why);
|
|
1269
|
+
return printPartialTrailer(id, version, skipped, why);
|
|
1270
|
+
}
|
|
1213
1271
|
if (!res.ok) {
|
|
1214
1272
|
// A 404 here is AMBIGUOUS and must not be collapsed. The route resolves the
|
|
1215
1273
|
// tenant and the project BEFORE it validates anything, so a 404 is usually an
|
|
@@ -1230,8 +1288,13 @@ async function cmdValidate(flags) {
|
|
|
1230
1288
|
die('this platform has no /packs/validate endpoint yet (older version) — deploy runs the full check');
|
|
1231
1289
|
die(`remote validate${errDetail(json)} — check --tenant/--project (or PACK_TENANT/PACK_PROJECT); \`octwin projects\` lists what this token can reach`);
|
|
1232
1290
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1291
|
+
// The public door is rate-limited per caller. Hitting it is not a verdict on the pack.
|
|
1292
|
+
if (res.status === 429 && flags.remote !== true) {
|
|
1293
|
+
return printPartialTrailer(id, version, skipped, `the platform's no-login check is rate-limited right now — retry shortly, or log in and pass --project`);
|
|
1294
|
+
}
|
|
1295
|
+
console.error(`✗ full check failed (HTTP ${res.status})`);
|
|
1296
|
+
if (loggedInDoor)
|
|
1297
|
+
printAuthHint(res.status, t.url);
|
|
1235
1298
|
console.error(typeof json === 'string' ? json : JSON.stringify(json, null, 2));
|
|
1236
1299
|
exitNow(1);
|
|
1237
1300
|
}
|
|
@@ -1260,6 +1323,19 @@ async function cmdValidate(flags) {
|
|
|
1260
1323
|
}
|
|
1261
1324
|
exitNow(1);
|
|
1262
1325
|
}
|
|
1326
|
+
/**
|
|
1327
|
+
* The LAST lines when only the offline SUBSET ran. A reader who sees a ✓ and stops there is the
|
|
1328
|
+
* failure mode, and a caveat printed ABOVE the ✓ does not fix it — so the word is PARTIAL, it is
|
|
1329
|
+
* last, and it says why. Exit 0, as offline `validate` always has: the subset found nothing, and
|
|
1330
|
+
* a config gap is not a finding (`--remote` is for when the full check must not be skipped).
|
|
1331
|
+
*/
|
|
1332
|
+
function printPartialTrailer(id, version, skipped, why) {
|
|
1333
|
+
console.log(`\n⚠ ${id}@${version}: only the OFFLINE check ran — a PARTIAL check. Deploy runs more, and can still refuse this pack.`);
|
|
1334
|
+
console.log(` Why: ${why}.`);
|
|
1335
|
+
if (skipped.length) {
|
|
1336
|
+
console.log(` Also skipped offline (${skipped.length}): ${skipped.join(', ')} — \`octwin platform-kb pull\` once at your repo root restores them.`);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1263
1339
|
/**
|
|
1264
1340
|
* Two ways in, one saved result.
|
|
1265
1341
|
*
|
|
@@ -1822,7 +1898,7 @@ async function cmdSeed(flags) {
|
|
|
1822
1898
|
if (stepErrors.length) {
|
|
1823
1899
|
// A kind failed but the rest ran — the reconcile softens each step. Say which,
|
|
1824
1900
|
// and exit non-zero so a scripted `seed && chat` doesn't read as clean.
|
|
1825
|
-
console.error(`
|
|
1901
|
+
console.error(`
|
|
1826
1902
|
⚠ ${stepErrors.length} step${stepErrors.length === 1 ? '' : 's'} failed — data may be incomplete:`);
|
|
1827
1903
|
for (const e of stepErrors)
|
|
1828
1904
|
console.error(` • ${e}`);
|
|
@@ -2281,6 +2357,24 @@ async function apiGet(endpoint, t) {
|
|
|
2281
2357
|
}
|
|
2282
2358
|
return { status: res.status, json };
|
|
2283
2359
|
}
|
|
2360
|
+
/**
|
|
2361
|
+
* The project this token is pinned to, or null (unpinned, or the lookup failed). A pinned token
|
|
2362
|
+
* cannot reach workspace-wide routes, so a command with a workspace view and a project view uses
|
|
2363
|
+
* this to pick the one the token can actually read. `authHeaders`, never a hand-built header — an
|
|
2364
|
+
* authenticating call without it stamps NULL over the token's client report.
|
|
2365
|
+
*/
|
|
2366
|
+
async function pinnedProjectOf(t) {
|
|
2367
|
+
try {
|
|
2368
|
+
const res = await fetch(`${t.url}/api/self/t/whoami`, { headers: authHeaders(t) });
|
|
2369
|
+
if (!res.ok)
|
|
2370
|
+
return null;
|
|
2371
|
+
const j = await res.json();
|
|
2372
|
+
return typeof j.project_slug === 'string' && j.project_slug ? j.project_slug : null;
|
|
2373
|
+
}
|
|
2374
|
+
catch {
|
|
2375
|
+
return null;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2284
2378
|
/**
|
|
2285
2379
|
* Send a WRITE to an admin endpoint with the deploy token; returns `{ status, json }`.
|
|
2286
2380
|
*
|
|
@@ -2399,7 +2493,7 @@ function pagingQs(flags, defaultLimit = 50) {
|
|
|
2399
2493
|
* guessed) so the ambiguity is documented rather than emergent — a pack that
|
|
2400
2494
|
* declares an entity actually called `note` reaches it via `--entity note`.
|
|
2401
2495
|
*/
|
|
2402
|
-
const RECORD_VERBS = new Set(['create', 'patch', 'stage', 'note', 'tasks', 'task']);
|
|
2496
|
+
const RECORD_VERBS = new Set(['create', 'patch', 'stage', 'note', 'tasks', 'task', 'archive', 'restore']);
|
|
2403
2497
|
/** `octwin records [entity] [id]` — inspect the pack's XRM data (needs a `records:read` token). */
|
|
2404
2498
|
async function cmdRecords(flags) {
|
|
2405
2499
|
// A leading reserved word is a write. To READ an entity whose name collides
|
|
@@ -2536,6 +2630,24 @@ async function cmdRecordsWrite(flags) {
|
|
|
2536
2630
|
console.log(`✓ #${json?.record?.record_number ?? '?'} is now at '${json?.record?.stage ?? to}'`);
|
|
2537
2631
|
return;
|
|
2538
2632
|
}
|
|
2633
|
+
// There is no delete — on the platform or here. Archive is how a record leaves use (test data
|
|
2634
|
+
// included): it drops out of every list, count and report, fires the entity's `on.archived`
|
|
2635
|
+
// reactions exactly as the console's Archive does, and `restore` brings it back.
|
|
2636
|
+
if (verb === 'archive' || verb === 'restore') {
|
|
2637
|
+
const archived = verb === 'archive';
|
|
2638
|
+
// `||`, not `??`: an EMPTY id (a failed `$(…)` in a script) must not reach the route.
|
|
2639
|
+
const id = arg || die(`usage: octwin records ${verb} <recordId>`);
|
|
2640
|
+
console.log(`→ ${archived ? 'Archiving' : 'Restoring'} ${id} in ${targetLabel(t)} …`);
|
|
2641
|
+
const { status, json } = await apiSend('POST', `${base}/xrm/records/${encodeURIComponent(id)}/archive`, { archived }, t);
|
|
2642
|
+
if (status === 404)
|
|
2643
|
+
die(`record '${id}' not found`);
|
|
2644
|
+
if (status !== 200)
|
|
2645
|
+
writeFail(`${verb} record ${id}`, status, json, url, true);
|
|
2646
|
+
console.log(archived
|
|
2647
|
+
? `✓ Archived #${json?.record?.record_number ?? '?'} — out of every list and count. Undo: octwin records restore ${id}`
|
|
2648
|
+
: `✓ Restored #${json?.record?.record_number ?? '?'} — back in use.`);
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2539
2651
|
if (verb === 'note') {
|
|
2540
2652
|
const id = arg ?? die('usage: octwin records note <recordId> "the note text"');
|
|
2541
2653
|
const note = flags._[2] ?? die('octwin records note needs the note text as the last argument');
|
|
@@ -2776,9 +2888,25 @@ async function cmdLogs(flags) {
|
|
|
2776
2888
|
console.log(`Timeline for ${convId} (${events.length} events):`);
|
|
2777
2889
|
for (const e of events) {
|
|
2778
2890
|
const isErr = e.type === 'tool' && e.subtype === 'platform_error';
|
|
2779
|
-
|
|
2891
|
+
/**
|
|
2892
|
+
* An ADVISORY is a turn that worked and that you probably did not mean.
|
|
2893
|
+
*
|
|
2894
|
+
* It reads differently from an error on purpose: nothing failed, so `⚠ ERROR` would be a lie
|
|
2895
|
+
* and the plain `tool/platform_advisory` tag would be scrolled past. The platform raises these
|
|
2896
|
+
* exactly where a silent-but-wrong result used to be invisible — a `record_list` scoped to a
|
|
2897
|
+
* customer who owns none of a shared entity returns zero rows and the `empty` port, which
|
|
2898
|
+
* prints identically to a real no-match. Three separate authors lost debugging sessions to
|
|
2899
|
+
* that one before it had anywhere to appear.
|
|
2900
|
+
*/
|
|
2901
|
+
const isAdvisory = e.type === 'tool' && e.subtype === 'platform_advisory';
|
|
2902
|
+
const tag = isErr ? '⚠ ERROR' : isAdvisory ? '! CHECK' : `${e.type}${e.subtype ? `/${e.subtype}` : ''}`;
|
|
2780
2903
|
const body = typeof e.content === 'string' ? e.content : JSON.stringify(e.content ?? '');
|
|
2781
2904
|
console.log(` ${e.ts ?? ''} ${tag}: ${body ?? ''}`);
|
|
2905
|
+
// The remedy, indented under its advisory. Printing the observation without the fix is what
|
|
2906
|
+
// the envelope field and the server log already did, and neither reached an author.
|
|
2907
|
+
if (isAdvisory && typeof e.metadata?.remedy === 'string' && e.metadata.remedy) {
|
|
2908
|
+
console.log(` → ${e.metadata.remedy}`);
|
|
2909
|
+
}
|
|
2782
2910
|
// The renders this turn actually sent (captured outbound payloads) — the
|
|
2783
2911
|
// other half of the story next to the structured tap lines.
|
|
2784
2912
|
const captured = Array.isArray(e.metadata?.raw) ? e.metadata.raw : [];
|
|
@@ -2904,6 +3032,7 @@ class SseFrameReader {
|
|
|
2904
3032
|
const REPLAY_SETTLE_MS = 400; // quiet gap that marks the end of the connect replay burst
|
|
2905
3033
|
const TURN_SETTLE_MS = 2_000; // quiet gap after a render = the turn finished sending
|
|
2906
3034
|
const REPLY_TIMEOUT_MS = 45_000; // hard cap waiting for the first render of the turn
|
|
3035
|
+
const LATE_DRAIN_MS = 3_000; // after the last turn: stay connected for late pushes (`drainLateRenders`)
|
|
2907
3036
|
/**
|
|
2908
3037
|
* Parse a `--script` file — ONE TURN PER LINE, in order:
|
|
2909
3038
|
*
|
|
@@ -2938,19 +3067,12 @@ function parseChatScript(body) {
|
|
|
2938
3067
|
}
|
|
2939
3068
|
return turns;
|
|
2940
3069
|
}
|
|
2941
|
-
/**
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
* by chaining shell invocations (`chat A && chat B`) races the agent loop: this
|
|
2948
|
-
* command ends a turn on a QUIET GAP, and the server-side loop can still be
|
|
2949
|
-
* running when the process exits, so the next invocation's inbound lands
|
|
2950
|
-
* mid-turn. The agent then fills required fields with placeholder text, or starts
|
|
2951
|
-
* a second workflow run. Inside one process the loop simply waits for the settle
|
|
2952
|
-
* before sending the next turn, over the same SSE connection.
|
|
2953
|
-
*/
|
|
3070
|
+
/** The line printed for a held turn — what the author needs to know, not a failure. */
|
|
3071
|
+
function heldLine(voice) {
|
|
3072
|
+
return voice === 'human'
|
|
3073
|
+
? '⏸ a person is holding this conversation — the AI Agent will not reply. The operator answers from Live chat in the console; nothing more comes back here for this turn.'
|
|
3074
|
+
: '⏸ waiting for a person to join — the AI Agent will not reply until someone accepts in Live chat, or the wait runs out and the conversation returns to it.';
|
|
3075
|
+
}
|
|
2954
3076
|
async function runChatTurn(args) {
|
|
2955
3077
|
const { url, tenant, project, from, turn, frames, asJson } = args;
|
|
2956
3078
|
let boundary = args.boundary;
|
|
@@ -2990,15 +3112,23 @@ async function runChatTurn(args) {
|
|
|
2990
3112
|
console.log(` … delivered — waiting for the reply (up to ${Math.round(REPLY_TIMEOUT_MS / 1000)}s)`);
|
|
2991
3113
|
// Collect THIS turn's renders (id > boundary). A turn can send several
|
|
2992
3114
|
// messages, so keep reading until a quiet gap after the last render.
|
|
3115
|
+
//
|
|
3116
|
+
// A HELD turn ends the wait too. While a person holds the conversation (or one has been
|
|
3117
|
+
// asked for) the platform deliberately sends no AI Agent reply, and says so with a `held`
|
|
3118
|
+
// envelope; before it did, this loop sat out the whole REPLY_TIMEOUT_MS on every held turn
|
|
3119
|
+
// and the author read it as a hang. After `held` the loop still settles for TURN_SETTLE_MS,
|
|
3120
|
+
// because the same customer message replays any operator line queued while they were away.
|
|
2993
3121
|
const deadline = Date.now() + REPLY_TIMEOUT_MS;
|
|
2994
3122
|
let rendersSeen = 0;
|
|
3123
|
+
let held = null;
|
|
2995
3124
|
for (;;) {
|
|
2996
3125
|
const remaining = deadline - Date.now();
|
|
2997
3126
|
if (remaining <= 0)
|
|
2998
3127
|
break;
|
|
2999
|
-
const
|
|
3128
|
+
const settling = rendersSeen > 0 || held !== null;
|
|
3129
|
+
const f = await frames.next(settling ? TURN_SETTLE_MS : Math.min(remaining, REPLY_TIMEOUT_MS));
|
|
3000
3130
|
if (f === 'timeout') {
|
|
3001
|
-
if (
|
|
3131
|
+
if (settling)
|
|
3002
3132
|
break;
|
|
3003
3133
|
else
|
|
3004
3134
|
continue;
|
|
@@ -3009,19 +3139,59 @@ async function runChatTurn(args) {
|
|
|
3009
3139
|
continue; // late replay stragglers
|
|
3010
3140
|
if (f.id != null)
|
|
3011
3141
|
boundary = f.id;
|
|
3142
|
+
if (f.ev?.kind === 'held' && held === null) {
|
|
3143
|
+
held = f.ev.voice === 'human' ? 'human' : 'pending';
|
|
3144
|
+
if (!asJson)
|
|
3145
|
+
console.log(heldLine(held));
|
|
3146
|
+
}
|
|
3147
|
+
// Only an ANSWER ends the wait. A notify (a reminder firing mid-script) is printed, labelled,
|
|
3148
|
+
// and the wait goes on — it used to count as the reply, end the turn on the reminder, and leave
|
|
3149
|
+
// the real reply looking lost (`lib/chat-line.ts`).
|
|
3012
3150
|
if (asJson) {
|
|
3013
3151
|
console.log(JSON.stringify(f.ev));
|
|
3014
|
-
if (f.ev?.kind === 'render')
|
|
3152
|
+
if (f.ev?.kind === 'render' && answersTheTurn(f.ev))
|
|
3015
3153
|
rendersSeen++;
|
|
3016
3154
|
continue;
|
|
3017
3155
|
}
|
|
3018
3156
|
if (f.ev?.kind !== 'render')
|
|
3019
3157
|
continue; // status/typing noise
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3158
|
+
if (answersTheTurn(f.ev))
|
|
3159
|
+
rendersSeen++;
|
|
3160
|
+
printRender(f.ev);
|
|
3161
|
+
}
|
|
3162
|
+
return { boundary, rendersSeen, held };
|
|
3163
|
+
}
|
|
3164
|
+
/** One render line — `← <label><body>` — and its interactive content. */
|
|
3165
|
+
function printRender(ev, note = '') {
|
|
3166
|
+
console.log(`← ${chatLabel(ev)}${ev.body ?? '(no text body)'}${ev.hint?.type && ev.hint.type !== 'text' ? ` (render: ${ev.hint.type})` : ''}${note}`);
|
|
3167
|
+
printHint(ev.hint);
|
|
3168
|
+
}
|
|
3169
|
+
/**
|
|
3170
|
+
* After the LAST turn, keep reading for `LATE_DRAIN_MS` and print what arrives — a notify, a
|
|
3171
|
+
* slow post-turn line — before closing the stream. A render the platform pushes to a stream the
|
|
3172
|
+
* CLI has already closed reaches no one and is recorded as undelivered ("the visitor has no open
|
|
3173
|
+
* chat connection"), which reads like a platform fault when it is only the CLI leaving early.
|
|
3174
|
+
*/
|
|
3175
|
+
async function drainLateRenders(frames, boundary, asJson) {
|
|
3176
|
+
const deadline = Date.now() + LATE_DRAIN_MS;
|
|
3177
|
+
for (;;) {
|
|
3178
|
+
const remaining = deadline - Date.now();
|
|
3179
|
+
if (remaining <= 0)
|
|
3180
|
+
return;
|
|
3181
|
+
const f = await frames.next(remaining);
|
|
3182
|
+
if (f === 'timeout' || f === 'done')
|
|
3183
|
+
return;
|
|
3184
|
+
if (f.id != null && f.id <= boundary)
|
|
3185
|
+
continue;
|
|
3186
|
+
if (f.id != null)
|
|
3187
|
+
boundary = f.id;
|
|
3188
|
+
if (asJson) {
|
|
3189
|
+
console.log(JSON.stringify(f.ev));
|
|
3190
|
+
continue;
|
|
3191
|
+
}
|
|
3192
|
+
if (f.ev?.kind === 'render')
|
|
3193
|
+
printRender(f.ev, ' (after the last turn)');
|
|
3023
3194
|
}
|
|
3024
|
-
return { boundary, rendersSeen };
|
|
3025
3195
|
}
|
|
3026
3196
|
/** `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--script <file>] [--json]`
|
|
3027
3197
|
* — drive a turn (or a whole scripted conversation) through the dev web channel
|
|
@@ -3122,13 +3292,18 @@ async function cmdChat(flags) {
|
|
|
3122
3292
|
// Phase 2+3 — run each turn in order, over the SAME connection. One turn is
|
|
3123
3293
|
// the normal case; `--script` drives a whole conversation.
|
|
3124
3294
|
let totalRenders = 0;
|
|
3295
|
+
let anyHeld = false;
|
|
3125
3296
|
for (const [i, turn] of turns.entries()) {
|
|
3126
3297
|
if (turns.length > 1)
|
|
3127
3298
|
console.log(`\n── turn ${i + 1}/${turns.length} ──`);
|
|
3128
3299
|
const r = await runChatTurn({ url, tenant, project, from, turn, frames, boundary, asJson });
|
|
3129
3300
|
boundary = r.boundary;
|
|
3130
3301
|
totalRenders += r.rendersSeen;
|
|
3131
|
-
if (r.
|
|
3302
|
+
if (r.held)
|
|
3303
|
+
anyHeld = true;
|
|
3304
|
+
// A held turn answered nothing BY DESIGN — the script goes on, so an author can script the
|
|
3305
|
+
// customer's side of a handoff (their lines reach the operator's pane) without it stopping.
|
|
3306
|
+
if (r.rendersSeen === 0 && !r.held && turns.length > 1) {
|
|
3132
3307
|
// Stop rather than fire the rest of the script into a conversation that
|
|
3133
3308
|
// isn't answering — the remaining turns would land out of context.
|
|
3134
3309
|
await cancel();
|
|
@@ -3136,8 +3311,9 @@ async function cmdChat(flags) {
|
|
|
3136
3311
|
exitNow(1);
|
|
3137
3312
|
}
|
|
3138
3313
|
}
|
|
3314
|
+
await drainLateRenders(frames, boundary, asJson);
|
|
3139
3315
|
await cancel();
|
|
3140
|
-
if (totalRenders === 0) {
|
|
3316
|
+
if (totalRenders === 0 && !anyHeld) {
|
|
3141
3317
|
console.error(` no reply after ${Math.round(REPLY_TIMEOUT_MS / 1000)}s — the pack may not be warm yet, or the turn produced no render.`);
|
|
3142
3318
|
exitNow(1);
|
|
3143
3319
|
}
|
|
@@ -5150,9 +5326,13 @@ async function cmdUsage(flags) {
|
|
|
5150
5326
|
const t = resolveTarget(flags);
|
|
5151
5327
|
const { url } = t;
|
|
5152
5328
|
const asJson = flags.json === true;
|
|
5153
|
-
|
|
5329
|
+
// A token pinned to one project is refused on the workspace rollup (the pin is a ceiling on
|
|
5330
|
+
// every route), so for a pinned token the project view IS the answer — ask whoami rather
|
|
5331
|
+
// than print a 403 at someone whose token can only ever see one project.
|
|
5332
|
+
const pinned = t.project ? null : await pinnedProjectOf(t);
|
|
5333
|
+
const scoped = Boolean(t.project || pinned);
|
|
5154
5334
|
if (!asJson)
|
|
5155
|
-
console.log(`→ Reading model usage for ${scoped ? targetLabel(t) : 'the whole workspace'} …`);
|
|
5335
|
+
console.log(`→ Reading model usage for ${scoped ? (pinned ? `project '${pinned}'` : targetLabel(t)) : 'the whole workspace'} …`);
|
|
5156
5336
|
// Both URLs written out in place, for the same reason as `performance` above: an
|
|
5157
5337
|
// `endpoint` variable would leave BOTH invisible to `cli-routes.test.ts`.
|
|
5158
5338
|
const { status, json } = scoped
|
|
@@ -5185,388 +5365,408 @@ async function cmdUsage(flags) {
|
|
|
5185
5365
|
console.log('\nThis is MODEL spend. WhatsApp/Meta message billing is operator-only — not reachable by an API token.');
|
|
5186
5366
|
}
|
|
5187
5367
|
function help() {
|
|
5188
|
-
console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
|
|
5189
|
-
|
|
5190
|
-
octwin --version # print the CLI version (+ any upgrade notice)
|
|
5191
|
-
octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
5192
|
-
octwin validate [--dir .] [--remote] [--require-kb] #
|
|
5193
|
-
octwin login --url <platformUrl> # approve in a browser; the CLI collects the token
|
|
5194
|
-
octwin login --url <platformUrl> --token oct_… # or paste a deploy token from the console
|
|
5195
|
-
octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
|
|
5196
|
-
octwin projects [--archived] [--json] # the --project slugs this token can name
|
|
5197
|
-
octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
5198
|
-
[--request-listing | --withdraw-listing] # public marketplace — opt-in, see: octwin help deploy
|
|
5199
|
-
octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
|
|
5200
|
-
octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
|
|
5201
|
-
octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
|
|
5202
|
-
octwin work [recordId] [--queues] [--unrouted] [--json] # inspect the work inbox (worked records) + timelines
|
|
5203
|
-
# --queues: per-queue open counts + an UNROUTED warning · --unrouted: only the items in no queue
|
|
5204
|
-
octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
|
|
5205
|
-
octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
|
|
5206
|
-
octwin media generate "<prompt>" [--out <file.png>] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
|
|
5207
|
-
octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
|
|
5208
|
-
octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
|
|
5209
|
-
octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
|
|
5210
|
-
octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
|
|
5211
|
-
octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
|
|
5212
|
-
octwin automation [campaigns] [--json] # the jobs your declarations produced + health, last result each
|
|
5213
|
-
octwin integrations [--json] # declared connections BESIDE what is configured (the silent-never-fires check)
|
|
5214
|
-
octwin integrations deliveries [<id>] | events # the outbound delivery log / inbound events
|
|
5215
|
-
octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]
|
|
5216
|
-
octwin performance [--detail] [--json] # the project's business indicators (value/conversion/duration)
|
|
5217
|
-
octwin usage [--json] # model calls, tokens and cost (project if pinned, else workspace)
|
|
5218
|
-
octwin platform-kb [pull] [--if-stale|--check] [--dir .] [--url <url>] # no token needed
|
|
5219
|
-
octwin test [--dir .] # = validate --remote (the full platform check)
|
|
5220
|
-
octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
|
|
5221
|
-
octwin memos [--all] [--json] # read the platform's replies + notices (a reply to your feedback lands here)
|
|
5222
|
-
|
|
5223
|
-
Writes — exercise the state your pack creates (each needs the matching :write scope):
|
|
5224
|
-
octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…"
|
|
5225
|
-
octwin records tasks | task complete <taskId> [--outcome done|cancelled]
|
|
5226
|
-
octwin work assign <id> --to user:<uuid>|none | note <id> "…" | stage <id> --to <stage>
|
|
5227
|
-
octwin work decide <id> --action <a> [--param k=v] [--dry-run] # --dry-run previews, commits nothing
|
|
5228
|
-
octwin orders transition <ref> --to <status> | refund <ref> --force
|
|
5229
|
-
octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
|
|
5230
|
-
octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
|
|
5231
|
-
octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
|
|
5232
|
-
octwin automation run <jobId> | pause <jobId> | resume <jobId> | send <campaignId>
|
|
5233
|
-
octwin integrations test <key> # a LIVE call to the connection's health: operation
|
|
5234
|
-
octwin integrations retry|cancel|send-now <deliveryId>
|
|
5235
|
-
(octwin integrations preflight <key> needs only integrations:read — it makes no call)
|
|
5236
|
-
|
|
5237
|
-
Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
|
|
5238
|
-
\`octwin chat --as <h>\` calls continue the same conversation; press a rendered
|
|
5239
|
-
button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
|
|
5240
|
-
Signing in: \`octwin login --url <u>\` prints a link + code to approve in a browser. To pick your own
|
|
5241
|
-
scopes instead, mint a token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
|
|
5242
|
-
octwin platform-kb pull → writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
|
|
5243
|
-
Config (deploy): flags > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login (\`octwin login\` sets the default target).
|
|
5368
|
+
console.log(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
|
|
5369
|
+
|
|
5370
|
+
octwin --version # print the CLI version (+ any upgrade notice)
|
|
5371
|
+
octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
5372
|
+
octwin validate [--dir .] [--remote | --offline] [--require-kb] # the platform's FULL check by default — no login needed
|
|
5373
|
+
octwin login --url <platformUrl> # approve in a browser; the CLI collects the token
|
|
5374
|
+
octwin login --url <platformUrl> --token oct_… # or paste a deploy token from the console
|
|
5375
|
+
octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
|
|
5376
|
+
octwin projects [--archived] [--json] # the --project slugs this token can name
|
|
5377
|
+
octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
5378
|
+
[--request-listing | --withdraw-listing] # public marketplace — opt-in, see: octwin help deploy
|
|
5379
|
+
octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
|
|
5380
|
+
octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
|
|
5381
|
+
octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
|
|
5382
|
+
octwin work [recordId] [--queues] [--unrouted] [--json] # inspect the work inbox (worked records) + timelines
|
|
5383
|
+
# --queues: per-queue open counts + an UNROUTED warning · --unrouted: only the items in no queue
|
|
5384
|
+
octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
|
|
5385
|
+
octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
|
|
5386
|
+
octwin media generate "<prompt>" [--out <file.png>] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
|
|
5387
|
+
octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
|
|
5388
|
+
octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
|
|
5389
|
+
octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
|
|
5390
|
+
octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
|
|
5391
|
+
octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
|
|
5392
|
+
octwin automation [campaigns] [--json] # the jobs your declarations produced + health, last result each
|
|
5393
|
+
octwin integrations [--json] # declared connections BESIDE what is configured (the silent-never-fires check)
|
|
5394
|
+
octwin integrations deliveries [<id>] | events # the outbound delivery log / inbound events
|
|
5395
|
+
octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]
|
|
5396
|
+
octwin performance [--detail] [--json] # the project's business indicators (value/conversion/duration)
|
|
5397
|
+
octwin usage [--json] # model calls, tokens and cost (project if pinned, else workspace)
|
|
5398
|
+
octwin platform-kb [pull] [--if-stale|--check] [--dir .] [--url <url>] # no token needed
|
|
5399
|
+
octwin test [--dir .] # = validate --remote (the full platform check)
|
|
5400
|
+
octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
|
|
5401
|
+
octwin memos [--all] [--json] # read the platform's replies + notices (a reply to your feedback lands here)
|
|
5402
|
+
|
|
5403
|
+
Writes — exercise the state your pack creates (each needs the matching :write scope):
|
|
5404
|
+
octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…", archive|restore <id>
|
|
5405
|
+
octwin records tasks | task complete <taskId> [--outcome done|cancelled]
|
|
5406
|
+
octwin work assign <id> --to user:<uuid>|none | note <id> "…" | stage <id> --to <stage>
|
|
5407
|
+
octwin work decide <id> --action <a> [--param k=v] [--dry-run] # --dry-run previews, commits nothing
|
|
5408
|
+
octwin orders transition <ref> --to <status> | refund <ref> --force
|
|
5409
|
+
octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
|
|
5410
|
+
octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
|
|
5411
|
+
octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
|
|
5412
|
+
octwin automation run <jobId> | pause <jobId> | resume <jobId> | send <campaignId>
|
|
5413
|
+
octwin integrations test <key> # a LIVE call to the connection's health: operation
|
|
5414
|
+
octwin integrations retry|cancel|send-now <deliveryId>
|
|
5415
|
+
(octwin integrations preflight <key> needs only integrations:read — it makes no call)
|
|
5416
|
+
|
|
5417
|
+
Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
|
|
5418
|
+
\`octwin chat --as <h>\` calls continue the same conversation; press a rendered
|
|
5419
|
+
button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
|
|
5420
|
+
Signing in: \`octwin login --url <u>\` prints a link + code to approve in a browser. To pick your own
|
|
5421
|
+
scopes instead, mint a token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
|
|
5422
|
+
octwin platform-kb pull → writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
|
|
5423
|
+
Config (deploy): flags > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login (\`octwin login\` sets the default target).
|
|
5244
5424
|
Per-command usage: octwin <command> --help`);
|
|
5245
5425
|
}
|
|
5246
5426
|
/** Per-subcommand usage — printed for `octwin <cmd> --help|-h` BEFORE any
|
|
5247
5427
|
* network/auth work (a --help that 401s is worse than no help at all). */
|
|
5248
5428
|
const COMMAND_HELP = {
|
|
5249
|
-
init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
5429
|
+
init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
5250
5430
|
Scaffold a pure-YAML starter pack into <dir>.`,
|
|
5251
|
-
validate: `octwin validate [--dir .] [--remote] [--require-kb] [--strict-primitives]
|
|
5252
|
-
|
|
5253
|
-
reference
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
it
|
|
5431
|
+
validate: `octwin validate [--dir .] [--remote | --offline] [--require-kb] [--strict-primitives]
|
|
5432
|
+
First the fast OFFLINE checks (structure, YAML, and the checks driven by the
|
|
5433
|
+
pulled capability reference — those SKIP when the reference is missing, and
|
|
5434
|
+
--require-kb turns the skip into a failure for CI). Then, BY DEFAULT, the
|
|
5435
|
+
platform's FULL validation — the same check deploy runs, all errors at once.
|
|
5436
|
+
|
|
5437
|
+
The full check needs NO login: without a token + --project it goes to the
|
|
5438
|
+
platform's public validate route. With a token AND --project (or PACK_PROJECT)
|
|
5439
|
+
it goes through your workspace, which also confirms that project exists.
|
|
5440
|
+
|
|
5441
|
+
When the platform cannot be reached (no url, offline, rate-limited) the run
|
|
5442
|
+
ends with "only the OFFLINE check ran — a PARTIAL check" and exits 0.
|
|
5443
|
+
--remote REQUIRES the full check: not reaching it is an error. --offline runs
|
|
5444
|
+
only the partial subset. --strict-primitives additionally type-checks LITERAL
|
|
5445
|
+
args: values against each primitive's declared input schema; expression
|
|
5446
|
+
strings ('$found.id', '{$t(…)}') are always exempt.`,
|
|
5447
|
+
login: `octwin login --url <platformUrl> [--token oct_…] [--wait <seconds>] [--json]
|
|
5448
|
+
Sign in to a platform and make that url the DEFAULT deploy target for every
|
|
5449
|
+
later command.
|
|
5450
|
+
|
|
5451
|
+
WITH --token: saves a token you minted yourself (console → Settings → API
|
|
5452
|
+
tokens). Unchanged, and still the right answer for CI — there, skip login and
|
|
5453
|
+
set PACK_PLATFORM_URL + PACK_TOKEN instead.
|
|
5454
|
+
|
|
5455
|
+
WITHOUT --token: prints a link to approve in a browser. A workspace admin opens
|
|
5456
|
+
it, checks which machine is asking, and clicks Approve; the CLI then collects a
|
|
5457
|
+
30-day token by itself, so no token is ever pasted between people. The code is
|
|
5458
|
+
printed too — it is what the page shows, so you can tell it is your own request.
|
|
5459
|
+
Approve only a link you opened because YOU ran this command: a link someone
|
|
5460
|
+
sends you would connect THEIR machine to your workspace.
|
|
5461
|
+
|
|
5462
|
+
The wait is bounded (90s by default) and RESUMABLE: if nobody has approved yet
|
|
5463
|
+
it exits 75 and keeps the request, so running \`octwin login\` again picks up the
|
|
5277
5464
|
same code. --wait 0 prints the code and returns at once.`,
|
|
5278
|
-
whoami: `octwin whoami [--url <url>] [--tenant <slug>]
|
|
5465
|
+
whoami: `octwin whoami [--url <url>] [--tenant <slug>]
|
|
5279
5466
|
Verify the resolved token authenticates against the tenant.`,
|
|
5280
|
-
projects: `octwin projects [--archived] [--json]
|
|
5281
|
-
List the workspace's projects — the slugs every --project flag takes, with the
|
|
5282
|
-
plan's project cap. --archived includes archived ones. A pack:deploy token
|
|
5283
|
-
reaches this (it names a project in every other command).
|
|
5284
|
-
|
|
5285
|
-
octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
|
|
5286
|
-
Create a project. The URL slug is derived from the name unless --slug pins one.
|
|
5287
|
-
--pack installs an ALREADY-published pack; the usual next step is instead
|
|
5288
|
-
\`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
|
|
5289
|
-
|
|
5290
|
-
octwin projects rm <slug> [--yes]
|
|
5291
|
-
HARD delete — the project and everything cascading from it (conversations,
|
|
5292
|
-
contacts, records, installs). No undo, and not the same as archiving.
|
|
5293
|
-
WITHOUT --yes it only previews what would be destroyed, so the dry run is the
|
|
5294
|
-
default. Together these make a disposable end-to-end environment:
|
|
5295
|
-
octwin projects create "Scratch" && octwin deploy --project scratch --seed
|
|
5296
|
-
octwin chat "hi" --project scratch
|
|
5297
|
-
octwin projects rm scratch --yes
|
|
5467
|
+
projects: `octwin projects [--archived] [--json]
|
|
5468
|
+
List the workspace's projects — the slugs every --project flag takes, with the
|
|
5469
|
+
plan's project cap. --archived includes archived ones. A pack:deploy token
|
|
5470
|
+
reaches this (it names a project in every other command).
|
|
5471
|
+
|
|
5472
|
+
octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
|
|
5473
|
+
Create a project. The URL slug is derived from the name unless --slug pins one.
|
|
5474
|
+
--pack installs an ALREADY-published pack; the usual next step is instead
|
|
5475
|
+
\`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
|
|
5476
|
+
|
|
5477
|
+
octwin projects rm <slug> [--yes]
|
|
5478
|
+
HARD delete — the project and everything cascading from it (conversations,
|
|
5479
|
+
contacts, records, installs). No undo, and not the same as archiving.
|
|
5480
|
+
WITHOUT --yes it only previews what would be destroyed, so the dry run is the
|
|
5481
|
+
default. Together these make a disposable end-to-end environment:
|
|
5482
|
+
octwin projects create "Scratch" && octwin deploy --project scratch --seed
|
|
5483
|
+
octwin chat "hi" --project scratch
|
|
5484
|
+
octwin projects rm scratch --yes
|
|
5298
5485
|
Both verbs need the \`projects:write\` scope — a pack:deploy token does NOT confer it.`,
|
|
5299
|
-
deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
5300
|
-
[--request-listing | --withdraw-listing]
|
|
5301
|
-
Upload the pack bundle, validate server-side, install onto the project.
|
|
5302
|
-
--seed additionally applies the pack's demo seed (streams progress).
|
|
5303
|
-
|
|
5304
|
-
A plain deploy says NOTHING about the public marketplace — it is a test loop, so it
|
|
5305
|
-
neither asks for a listing nor gives one up. The marketplace flags are opt-in:
|
|
5306
|
-
|
|
5307
|
-
--request-listing ask an operator to review this pack for the public marketplace
|
|
5308
|
-
(the pre-signup storefront at /packs). Requires 'public: true'
|
|
5309
|
-
under 'listing:' in manifest.yaml — the manifest states that the
|
|
5310
|
-
pack is a product, the flag is you choosing to ask.
|
|
5311
|
-
--withdraw-listing retract the request, including an approved listing.
|
|
5312
|
-
|
|
5313
|
-
An approval covers the CONTENT it was made against, so a later deploy that changes the
|
|
5486
|
+
deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
5487
|
+
[--request-listing | --withdraw-listing]
|
|
5488
|
+
Upload the pack bundle, validate server-side, install onto the project.
|
|
5489
|
+
--seed additionally applies the pack's demo seed (streams progress).
|
|
5490
|
+
|
|
5491
|
+
A plain deploy says NOTHING about the public marketplace — it is a test loop, so it
|
|
5492
|
+
neither asks for a listing nor gives one up. The marketplace flags are opt-in:
|
|
5493
|
+
|
|
5494
|
+
--request-listing ask an operator to review this pack for the public marketplace
|
|
5495
|
+
(the pre-signup storefront at /packs). Requires 'public: true'
|
|
5496
|
+
under 'listing:' in manifest.yaml — the manifest states that the
|
|
5497
|
+
pack is a product, the flag is you choosing to ask.
|
|
5498
|
+
--withdraw-listing retract the request, including an approved listing.
|
|
5499
|
+
|
|
5500
|
+
An approval covers the CONTENT it was made against, so a later deploy that changes the
|
|
5314
5501
|
pack returns it to the review queue on its own — no flag needed, and the CLI says so.`,
|
|
5315
|
-
seed: `octwin seed [--pack <packId>]
|
|
5316
|
-
Apply the pack's demo/reference data to the project it is installed on, without
|
|
5317
|
-
redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
|
|
5318
|
-
and the demo operator topology. Reports what each kind produced.
|
|
5319
|
-
Idempotent and safe to re-run — records upsert, and existing media is REUSED rather
|
|
5320
|
-
than regenerated, so a second pass costs nothing. --pack is only needed when a
|
|
5502
|
+
seed: `octwin seed [--pack <packId>]
|
|
5503
|
+
Apply the pack's demo/reference data to the project it is installed on, without
|
|
5504
|
+
redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
|
|
5505
|
+
and the demo operator topology. Reports what each kind produced.
|
|
5506
|
+
Idempotent and safe to re-run — records upsert, and existing media is REUSED rather
|
|
5507
|
+
than regenerated, so a second pass costs nothing. --pack is only needed when a
|
|
5321
5508
|
project somehow runs more than one.`,
|
|
5322
|
-
status: `octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
|
|
5323
|
-
Show installed vs live version + the flow list for this pack.
|
|
5324
|
-
The pack id is read from manifest.yaml and QUALIFIED with your workspace slug
|
|
5325
|
-
(a manifest declares a bare name; the owner is attached when you publish). Pass
|
|
5326
|
-
<packId> explicitly to skip that lookup — \`octwin agents\` and \`octwin projects\`
|
|
5509
|
+
status: `octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
|
|
5510
|
+
Show installed vs live version + the flow list for this pack.
|
|
5511
|
+
The pack id is read from manifest.yaml and QUALIFIED with your workspace slug
|
|
5512
|
+
(a manifest declares a bare name; the owner is attached when you publish). Pass
|
|
5513
|
+
<packId> explicitly to skip that lookup — \`octwin agents\` and \`octwin projects\`
|
|
5327
5514
|
both print the qualified form.`,
|
|
5328
|
-
records: `octwin records [entity] [id] [--limit 50] [--offset n]
|
|
5329
|
-
Inspect the pack's XRM data. No args = list entities. Worked records (cases,
|
|
5330
|
-
tickets, anything routed to a queue) read best through \`octwin work\`.
|
|
5331
|
-
|
|
5332
|
-
WRITES (need \`records:write\`; every one is re-checked by RBAC on the record):
|
|
5333
|
-
octwin records create <entity> --set field=value [--set …] [--stage s] [--contact <id>]
|
|
5334
|
-
octwin records patch <recordId> --entity <entity> --set field=value
|
|
5335
|
-
octwin records stage <recordId> --to <stage> [--note "..."]
|
|
5336
|
-
octwin records note <recordId> "the note text"
|
|
5337
|
-
octwin records
|
|
5338
|
-
octwin records
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
octwin work
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5515
|
+
records: `octwin records [entity] [id] [--limit 50] [--offset n]
|
|
5516
|
+
Inspect the pack's XRM data. No args = list entities. Worked records (cases,
|
|
5517
|
+
tickets, anything routed to a queue) read best through \`octwin work\`.
|
|
5518
|
+
|
|
5519
|
+
WRITES (need \`records:write\`; every one is re-checked by RBAC on the record):
|
|
5520
|
+
octwin records create <entity> --set field=value [--set …] [--stage s] [--contact <id>]
|
|
5521
|
+
octwin records patch <recordId> --entity <entity> --set field=value
|
|
5522
|
+
octwin records stage <recordId> --to <stage> [--note "..."]
|
|
5523
|
+
octwin records note <recordId> "the note text"
|
|
5524
|
+
octwin records archive <recordId> # out of every list and count (test data too)
|
|
5525
|
+
octwin records restore <recordId> # undo an archive
|
|
5526
|
+
octwin records tasks # open follow-up tasks (\`tasks\` plan feature)
|
|
5527
|
+
octwin records task complete <taskId> [--outcome done|cancelled] [--note "..."]
|
|
5528
|
+
|
|
5529
|
+
There is NO delete — on the platform or here. \`archive\` is how a record leaves use:
|
|
5530
|
+
it fires the entity's \`on.archived\` reactions, exactly like the console's Archive,
|
|
5531
|
+
and \`restore\` brings it back.
|
|
5532
|
+
|
|
5533
|
+
--set coerces JSON scalars: \`--set rating=4.5\` sends a number, \`--set x=null\`
|
|
5534
|
+
sends null. Use --fields-json '{"a":{"b":1}}' for anything nested.
|
|
5535
|
+
\`patch\` needs --entity even though it has an id: the route resolves the field
|
|
5536
|
+
validator from it. A leading \`create/patch/stage/note/archive/restore/tasks/task\` is
|
|
5537
|
+
read as a VERB — to list an entity actually named one of those, use \`--entity <name>\`.`,
|
|
5538
|
+
work: `octwin work [recordId] [--queues] [--unrouted] [--limit 50] [--offset n] [--json]
|
|
5539
|
+
Inspect the work inbox — every entity the pack declares worked (cases, orders
|
|
5540
|
+
needing review, applications, …): the inbox, one item + its timeline
|
|
5541
|
+
(+ applicable actions), or --queues for queue keys + open counts.
|
|
5542
|
+
|
|
5543
|
+
WRITES (need \`work:write\`; \`stage\` needs \`records:write\`):
|
|
5544
|
+
octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none
|
|
5545
|
+
octwin work note <recordId> "the note text"
|
|
5546
|
+
octwin work stage <recordId> --to <stage> [--note "..."]
|
|
5547
|
+
octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]
|
|
5548
|
+
|
|
5549
|
+
\`decide\` applies one of the entity's declared operator actions — \`octwin work <id>\`
|
|
5550
|
+
lists them with their params. --dry-run previews the customer-facing copy and the
|
|
5551
|
+
resulting stage WITHOUT committing (that route needs only \`work:read\`).
|
|
5359
5552
|
\`stage\` is the XRM records verb (one transition spelling platform-wide).`,
|
|
5360
|
-
logs: `octwin logs [conversationId] [--as <handle>] [--json]
|
|
5361
|
-
No id = recent conversations (handle, status, last activity; --as filters).
|
|
5362
|
-
With id = the full event timeline including what each turn rendered.
|
|
5553
|
+
logs: `octwin logs [conversationId] [--as <handle>] [--json]
|
|
5554
|
+
No id = recent conversations (handle, status, last activity; --as filters).
|
|
5555
|
+
With id = the full event timeline including what each turn rendered.
|
|
5363
5556
|
--json = raw events (verbatim payloads).`,
|
|
5364
|
-
pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
|
|
5365
|
-
Write a DEPLOYED pack's source back to disk — the inverse of deploy.
|
|
5366
|
-
A pack pushed with 'octwin deploy' lives on the platform as an artifact the
|
|
5367
|
-
runtime serves but nothing hands back, so its only source copy is the machine
|
|
5368
|
-
that pushed it. Pull it, fix it, redeploy it.
|
|
5369
|
-
Defaults to the version installed on the target project; --version overrides.
|
|
5370
|
-
--dir defaults to ./<packId>; a non-empty dir needs --force.
|
|
5371
|
-
The pulled dir redeploys where it came from — the target is your saved login.
|
|
5557
|
+
pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
|
|
5558
|
+
Write a DEPLOYED pack's source back to disk — the inverse of deploy.
|
|
5559
|
+
A pack pushed with 'octwin deploy' lives on the platform as an artifact the
|
|
5560
|
+
runtime serves but nothing hands back, so its only source copy is the machine
|
|
5561
|
+
that pushed it. Pull it, fix it, redeploy it.
|
|
5562
|
+
Defaults to the version installed on the target project; --version overrides.
|
|
5563
|
+
--dir defaults to ./<packId>; a non-empty dir needs --force.
|
|
5564
|
+
The pulled dir redeploys where it came from — the target is your saved login.
|
|
5372
5565
|
You may pull a pack your tenant OWNS (deployed); an operator token pulls any.`,
|
|
5373
|
-
chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
|
|
5374
|
-
octwin chat --script <file> [--as <handle>] [--json]
|
|
5375
|
-
Drive ONE turn through the dev web channel and print every render with its
|
|
5376
|
-
tap ids. Same --as handle = same conversation (multi-turn works).
|
|
5377
|
-
--tap presses a rendered button/list row instead of sending text.
|
|
5378
|
-
--media uploads a local file (or a media id from 'media generate --json') as
|
|
5379
|
-
an image/document/audio inbound — any "message" rides as its caption; feeds a
|
|
5380
|
-
running media-collect flow (e.g. activate-app).
|
|
5381
|
-
--json dumps the raw SSE envelopes for the turn.
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
the
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5566
|
+
chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
|
|
5567
|
+
octwin chat --script <file> [--as <handle>] [--json]
|
|
5568
|
+
Drive ONE turn through the dev web channel and print every render with its
|
|
5569
|
+
tap ids. Same --as handle = same conversation (multi-turn works).
|
|
5570
|
+
--tap presses a rendered button/list row instead of sending text.
|
|
5571
|
+
--media uploads a local file (or a media id from 'media generate --json') as
|
|
5572
|
+
an image/document/audio inbound — any "message" rides as its caption; feeds a
|
|
5573
|
+
running media-collect flow (e.g. activate-app).
|
|
5574
|
+
--json dumps the raw SSE envelopes for the turn.
|
|
5575
|
+
|
|
5576
|
+
Each reply line says who sent it. Plain \`← …\` is the AI Agent. \`← [notify] …\` is a
|
|
5577
|
+
reminder or notification (a \`schedule_notify\`, a reaction) — NOT the turn's answer,
|
|
5578
|
+
so the wait for the real reply goes on. \`← [operator: Name] …\` is a person in
|
|
5579
|
+
Live chat; \`← [platform] …\` is a platform line (a handoff joining or returning).
|
|
5580
|
+
After the last turn the stream stays open ${LATE_DRAIN_MS / 1000}s more, so a late message
|
|
5581
|
+
still prints (marked "after the last turn") instead of reaching a closed connection.
|
|
5582
|
+
|
|
5583
|
+
--script drives a WHOLE conversation from a file, ONE TURN PER LINE, in one
|
|
5584
|
+
process over one connection — waiting for each turn to settle before sending
|
|
5585
|
+
the next. Use this for any multi-step flow: chaining shell invocations races
|
|
5586
|
+
the agent loop, because a turn ends on a quiet gap that can arrive while the
|
|
5587
|
+
server is still working (the symptom is placeholder-filled fields or a second
|
|
5588
|
+
workflow run). Blank lines and # comments are skipped:
|
|
5589
|
+
|
|
5590
|
+
# book an appointment end to end
|
|
5591
|
+
احجز موعد
|
|
5592
|
+
tap:t:invoke:book-appointment:doctor_id=D1
|
|
5593
|
+
media:./licence.jpg | here is my licence
|
|
5394
5594
|
tap:t:resume:book-appointment:run_id=R1;_ctl_approved=true`,
|
|
5395
|
-
media: `octwin media generate "<prompt>" [--out <file.png>] [--json]
|
|
5396
|
-
AI-generate an image (needs a media:generate-scoped token), store it as a
|
|
5397
|
-
public asset, and print its MEDIA- handle + serve URL. --out downloads the
|
|
5398
|
-
bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
|
|
5595
|
+
media: `octwin media generate "<prompt>" [--out <file.png>] [--json]
|
|
5596
|
+
AI-generate an image (needs a media:generate-scoped token), store it as a
|
|
5597
|
+
public asset, and print its MEDIA- handle + serve URL. --out downloads the
|
|
5598
|
+
bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
|
|
5399
5599
|
bytes }. Pair with 'octwin chat --media' to drive media flows.`,
|
|
5400
|
-
agents: `octwin agents [packId::agentId] [--prompt] [--json]
|
|
5401
|
-
No args = the roster with each agent's EFFECTIVE model and which layer set it.
|
|
5402
|
-
With an agent = every governed setting (model / memory.last_messages /
|
|
5403
|
-
working_memory) plus the layer that won — an operator PLATFORM default can
|
|
5404
|
-
override what your manifest declares, and this is where you see that.
|
|
5405
|
-
--prompt = the exact system prompt the LLM sees for this project (pack
|
|
5406
|
-
instructions + platform protocol + any project overlay). Needs agents:read.
|
|
5407
|
-
The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
|
|
5408
|
-
|
|
5409
|
-
WRITES (need \`agents:write\`):
|
|
5410
|
-
octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
|
|
5411
|
-
[--enable-tool <toolId>] [--disable-tool <toolId>]
|
|
5412
|
-
|
|
5413
|
-
Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
|
|
5414
|
-
so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
|
|
5600
|
+
agents: `octwin agents [packId::agentId] [--prompt] [--json]
|
|
5601
|
+
No args = the roster with each agent's EFFECTIVE model and which layer set it.
|
|
5602
|
+
With an agent = every governed setting (model / memory.last_messages /
|
|
5603
|
+
working_memory) plus the layer that won — an operator PLATFORM default can
|
|
5604
|
+
override what your manifest declares, and this is where you see that.
|
|
5605
|
+
--prompt = the exact system prompt the LLM sees for this project (pack
|
|
5606
|
+
instructions + platform protocol + any project overlay). Needs agents:read.
|
|
5607
|
+
The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
|
|
5608
|
+
|
|
5609
|
+
WRITES (need \`agents:write\`):
|
|
5610
|
+
octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
|
|
5611
|
+
[--enable-tool <toolId>] [--disable-tool <toolId>]
|
|
5612
|
+
|
|
5613
|
+
Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
|
|
5614
|
+
so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
|
|
5415
5615
|
ids refuses --model with a 403 — the platform default governs there.`,
|
|
5416
|
-
orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
|
|
5417
|
-
No args = the order list (#number, status/payment, total, contact). With a
|
|
5418
|
-
reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
|
|
5419
|
-
payment_ref, and the allowed status transitions. Needs orders:read + the
|
|
5420
|
-
\`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
|
|
5421
|
-
so \`pending\` on a gateway-less workspace is expected, not a bug.
|
|
5422
|
-
|
|
5423
|
-
WRITES (need \`orders:write\`):
|
|
5424
|
-
octwin orders transition <reference_id> --to <status>
|
|
5425
|
-
octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
|
|
5426
|
-
|
|
5427
|
-
Refund is irreversible and moves money, hence --force. The route answers 200 even
|
|
5428
|
-
when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
|
|
5429
|
-
on a refusal rather than reporting a refund that never happened. Only a payment in
|
|
5616
|
+
orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
|
|
5617
|
+
No args = the order list (#number, status/payment, total, contact). With a
|
|
5618
|
+
reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
|
|
5619
|
+
payment_ref, and the allowed status transitions. Needs orders:read + the
|
|
5620
|
+
\`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned,
|
|
5621
|
+
so \`pending\` on a gateway-less workspace is expected, not a bug.
|
|
5622
|
+
|
|
5623
|
+
WRITES (need \`orders:write\`):
|
|
5624
|
+
octwin orders transition <reference_id> --to <status>
|
|
5625
|
+
octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
|
|
5626
|
+
|
|
5627
|
+
Refund is irreversible and moves money, hence --force. The route answers 200 even
|
|
5628
|
+
when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
|
|
5629
|
+
on a refusal rather than reporting a refund that never happened. Only a payment in
|
|
5430
5630
|
\`captured\` state can be refunded; \`payment_status\` is never settable directly.`,
|
|
5431
|
-
analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
|
|
5432
|
-
No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
|
|
5433
|
-
With an entity = stage-by-stage conversion (default --funnel) over the last 30
|
|
5434
|
-
days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
|
|
5631
|
+
analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
|
|
5632
|
+
No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
|
|
5633
|
+
With an entity = stage-by-stage conversion (default --funnel) over the last 30
|
|
5634
|
+
days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
|
|
5435
5635
|
range-filtered). Needs records:read + a \`view\` grant on \`record.<entity>\`.`,
|
|
5436
|
-
catalog: `octwin catalog [--readiness] [--json]
|
|
5437
|
-
The commerce \`product\` records + price, availability, stock (null = not
|
|
5438
|
-
inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
|
|
5439
|
-
Graph checklist (LIVE Graph calls; needs a bound access token). Needs
|
|
5440
|
-
catalog:read + the \`catalog\` plan feature.
|
|
5441
|
-
|
|
5442
|
-
WRITES (need \`catalog:write\`):
|
|
5443
|
-
octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
|
|
5444
|
-
octwin catalog stock <retailerId> [--set-on-hand <n>]
|
|
5445
|
-
|
|
5446
|
-
\`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
|
|
5447
|
-
inventory-tracked (always sellable), which is different from 0. Lowering on_hand
|
|
5448
|
-
below the units already reserved for open carts is refused. Creating/deleting
|
|
5636
|
+
catalog: `octwin catalog [--readiness] [--json]
|
|
5637
|
+
The commerce \`product\` records + price, availability, stock (null = not
|
|
5638
|
+
inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
|
|
5639
|
+
Graph checklist (LIVE Graph calls; needs a bound access token). Needs
|
|
5640
|
+
catalog:read + the \`catalog\` plan feature.
|
|
5641
|
+
|
|
5642
|
+
WRITES (need \`catalog:write\`):
|
|
5643
|
+
octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
|
|
5644
|
+
octwin catalog stock <retailerId> [--set-on-hand <n>]
|
|
5645
|
+
|
|
5646
|
+
\`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
|
|
5647
|
+
inventory-tracked (always sellable), which is different from 0. Lowering on_hand
|
|
5648
|
+
below the units already reserved for open carts is refused. Creating/deleting
|
|
5449
5649
|
products and the Meta catalog binding/sync stay in the console.`,
|
|
5450
|
-
scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
|
|
5451
|
-
No args = the engine state (bookable resource types, upcoming slots, booked
|
|
5452
|
-
seats). --slots <recordId> computes the slots for one bookable resource
|
|
5453
|
-
(occupancy included; --days is clamped to 1-31 server-side) — the way to verify
|
|
5454
|
-
the availability rules a \`deploy --seed\` created. Needs scheduling:read.
|
|
5455
|
-
|
|
5456
|
-
RULES (list needs scheduling:read; add/rm need scheduling:write):
|
|
5457
|
-
octwin scheduling rules --resource <resourceRecordId>
|
|
5458
|
-
octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
|
|
5459
|
-
[--slot-minutes 30] [--capacity 1]
|
|
5460
|
-
octwin scheduling rule rm <ruleId>
|
|
5461
|
-
octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
|
|
5462
|
-
[--start 09:00 --end 13:00]
|
|
5463
|
-
octwin scheduling exception rm <exceptionId>
|
|
5464
|
-
|
|
5465
|
-
--dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
|
|
5650
|
+
scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
|
|
5651
|
+
No args = the engine state (bookable resource types, upcoming slots, booked
|
|
5652
|
+
seats). --slots <recordId> computes the slots for one bookable resource
|
|
5653
|
+
(occupancy included; --days is clamped to 1-31 server-side) — the way to verify
|
|
5654
|
+
the availability rules a \`deploy --seed\` created. Needs scheduling:read.
|
|
5655
|
+
|
|
5656
|
+
RULES (list needs scheduling:read; add/rm need scheduling:write):
|
|
5657
|
+
octwin scheduling rules --resource <resourceRecordId>
|
|
5658
|
+
octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
|
|
5659
|
+
[--slot-minutes 30] [--capacity 1]
|
|
5660
|
+
octwin scheduling rule rm <ruleId>
|
|
5661
|
+
octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
|
|
5662
|
+
[--start 09:00 --end 13:00]
|
|
5663
|
+
octwin scheduling exception rm <exceptionId>
|
|
5664
|
+
|
|
5665
|
+
--dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
|
|
5466
5666
|
\`--slots\` is how you check what a rule actually produces.`,
|
|
5467
|
-
automation: `octwin automation [campaigns] [--limit n] [--offset n] [--json]
|
|
5468
|
-
No args = every job the pack's automation declaration produced, with its status,
|
|
5469
|
-
interval and LAST RESULT (matched / acted / errors), under a health line whose
|
|
5470
|
-
counts come from SQL rather than from filtering the page — the job list is capped
|
|
5471
|
-
server-side, so a client-side count would depend on the cap. Needs automation:read.
|
|
5472
|
-
|
|
5473
|
-
Jobs are DERIVED from declarations. There is no \`create\`: no automation block in
|
|
5474
|
-
the pack means no jobs, and \`octwin deploy\` is what installs them.
|
|
5475
|
-
|
|
5476
|
-
WRITES (automation:write):
|
|
5477
|
-
octwin automation run <jobId> # run once, now — prints matched/acted/errors
|
|
5478
|
-
octwin automation pause|resume <jobId>
|
|
5479
|
-
octwin automation send <campaignId> # enqueue a campaign; enqueued != delivered
|
|
5480
|
-
|
|
5481
|
-
<jobId> is the \`key\` the list shows (its uuid works too). The routes themselves
|
|
5482
|
-
accept only a uuid — the CLI resolves the key for you, and names the keys that do
|
|
5483
|
-
exist when it cannot. A 403 on a write can be an RBAC grant gap rather than a
|
|
5667
|
+
automation: `octwin automation [campaigns] [--limit n] [--offset n] [--json]
|
|
5668
|
+
No args = every job the pack's automation declaration produced, with its status,
|
|
5669
|
+
interval and LAST RESULT (matched / acted / errors), under a health line whose
|
|
5670
|
+
counts come from SQL rather than from filtering the page — the job list is capped
|
|
5671
|
+
server-side, so a client-side count would depend on the cap. Needs automation:read.
|
|
5672
|
+
|
|
5673
|
+
Jobs are DERIVED from declarations. There is no \`create\`: no automation block in
|
|
5674
|
+
the pack means no jobs, and \`octwin deploy\` is what installs them.
|
|
5675
|
+
|
|
5676
|
+
WRITES (automation:write):
|
|
5677
|
+
octwin automation run <jobId> # run once, now — prints matched/acted/errors
|
|
5678
|
+
octwin automation pause|resume <jobId>
|
|
5679
|
+
octwin automation send <campaignId> # enqueue a campaign; enqueued != delivered
|
|
5680
|
+
|
|
5681
|
+
<jobId> is the \`key\` the list shows (its uuid works too). The routes themselves
|
|
5682
|
+
accept only a uuid — the CLI resolves the key for you, and names the keys that do
|
|
5683
|
+
exist when it cannot. A 403 on a write can be an RBAC grant gap rather than a
|
|
5484
5684
|
missing scope: the action is re-checked against the job.`,
|
|
5485
|
-
integrations: `octwin integrations [--json]
|
|
5486
|
-
What the pack DECLARES beside what is actually CONFIGURED, in one view — because a
|
|
5487
|
-
connection that is declared and never configured is the commonest reason an
|
|
5488
|
-
integration silently never fires, and neither list alone can show it. Flags the
|
|
5489
|
-
gap explicitly. Needs integrations:read.
|
|
5490
|
-
|
|
5491
|
-
DIAGNOSE ONE CONNECTION:
|
|
5492
|
-
octwin integrations preflight <key> # every check, with a fix hint. Makes NO
|
|
5493
|
-
# outbound call — needs only integrations:read
|
|
5494
|
-
octwin integrations test <key> # a LIVE call to its health: operation
|
|
5495
|
-
# (integrations:write). Exits 1 when it fails.
|
|
5496
|
-
|
|
5497
|
-
THE DELIVERY LOG:
|
|
5498
|
-
octwin integrations deliveries [--status s] [--operation id] [--limit n]
|
|
5499
|
-
octwin integrations deliveries <id> # + the redacted request/response snapshots
|
|
5500
|
-
octwin integrations retry|cancel|send-now <id> # integrations:write
|
|
5501
|
-
octwin integrations events # INBOUND events (what arrived at your webhook)
|
|
5502
|
-
|
|
5503
|
-
retry/cancel answer 409 when the delivery is in the wrong state; the message
|
|
5685
|
+
integrations: `octwin integrations [--json]
|
|
5686
|
+
What the pack DECLARES beside what is actually CONFIGURED, in one view — because a
|
|
5687
|
+
connection that is declared and never configured is the commonest reason an
|
|
5688
|
+
integration silently never fires, and neither list alone can show it. Flags the
|
|
5689
|
+
gap explicitly. Needs integrations:read.
|
|
5690
|
+
|
|
5691
|
+
DIAGNOSE ONE CONNECTION:
|
|
5692
|
+
octwin integrations preflight <key> # every check, with a fix hint. Makes NO
|
|
5693
|
+
# outbound call — needs only integrations:read
|
|
5694
|
+
octwin integrations test <key> # a LIVE call to its health: operation
|
|
5695
|
+
# (integrations:write). Exits 1 when it fails.
|
|
5696
|
+
|
|
5697
|
+
THE DELIVERY LOG:
|
|
5698
|
+
octwin integrations deliveries [--status s] [--operation id] [--limit n]
|
|
5699
|
+
octwin integrations deliveries <id> # + the redacted request/response snapshots
|
|
5700
|
+
octwin integrations retry|cancel|send-now <id> # integrations:write
|
|
5701
|
+
octwin integrations events # INBOUND events (what arrived at your webhook)
|
|
5702
|
+
|
|
5703
|
+
retry/cancel answer 409 when the delivery is in the wrong state; the message
|
|
5504
5704
|
carries the rule.`,
|
|
5505
|
-
journeys: `octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition]
|
|
5506
|
-
[--stage <stageId>] [--limit n] [--json]
|
|
5507
|
-
No args = the journeys the pack declares. With an id, one of six views —
|
|
5508
|
-
--funnel (default) stage-by-stage reach and drop-off · --overview entered vs
|
|
5509
|
-
converted plus the biggest drop-off · --goals completions, contacts, value and
|
|
5510
|
-
p50 time · --trends per-bucket activity · --cost tokens and dollars per goal ·
|
|
5511
|
-
--definition what was DECLARED, unmeasured (the one view that works with no
|
|
5512
|
-
traffic). Needs journeys:read.
|
|
5513
|
-
|
|
5514
|
-
--stage <stageId> lists the runs sitting at a stage right now (a live snapshot,
|
|
5515
|
-
not the funnel's cumulative reached counts).
|
|
5516
|
-
|
|
5517
|
-
Same flag grammar as \`octwin analytics\` on purpose: a journey funnel and an
|
|
5518
|
-
entity funnel are the same question about different subjects. Journeys carry RBAC
|
|
5519
|
-
on top of the scope, so an empty answer can be a missing \`view\` grant rather
|
|
5705
|
+
journeys: `octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition]
|
|
5706
|
+
[--stage <stageId>] [--limit n] [--json]
|
|
5707
|
+
No args = the journeys the pack declares. With an id, one of six views —
|
|
5708
|
+
--funnel (default) stage-by-stage reach and drop-off · --overview entered vs
|
|
5709
|
+
converted plus the biggest drop-off · --goals completions, contacts, value and
|
|
5710
|
+
p50 time · --trends per-bucket activity · --cost tokens and dollars per goal ·
|
|
5711
|
+
--definition what was DECLARED, unmeasured (the one view that works with no
|
|
5712
|
+
traffic). Needs journeys:read.
|
|
5713
|
+
|
|
5714
|
+
--stage <stageId> lists the runs sitting at a stage right now (a live snapshot,
|
|
5715
|
+
not the funnel's cumulative reached counts).
|
|
5716
|
+
|
|
5717
|
+
Same flag grammar as \`octwin analytics\` on purpose: a journey funnel and an
|
|
5718
|
+
entity funnel are the same question about different subjects. Journeys carry RBAC
|
|
5719
|
+
on top of the scope, so an empty answer can be a missing \`view\` grant rather
|
|
5520
5720
|
than missing data — the output says which causes are possible.`,
|
|
5521
|
-
performance: `octwin performance [--detail] [--json]
|
|
5522
|
-
The project's business indicators — value produced, conversion, duration — each
|
|
5523
|
-
with its delta against the previous window and a \`why\` naming the declaration it
|
|
5524
|
-
came from. --detail adds the per-indicator breakdown.
|
|
5525
|
-
|
|
5526
|
-
Needs records:read, NOT a performance scope (there is none), so a read-only token
|
|
5527
|
-
already reaches it. Indicators are DERIVED: a pack that declares no journey goal
|
|
5721
|
+
performance: `octwin performance [--detail] [--json]
|
|
5722
|
+
The project's business indicators — value produced, conversion, duration — each
|
|
5723
|
+
with its delta against the previous window and a \`why\` naming the declaration it
|
|
5724
|
+
came from. --detail adds the per-indicator breakdown.
|
|
5725
|
+
|
|
5726
|
+
Needs records:read, NOT a performance scope (there is none), so a read-only token
|
|
5727
|
+
already reaches it. Indicators are DERIVED: a pack that declares no journey goal
|
|
5528
5728
|
value and no pipelined entity produces none, which is a different thing from zero.`,
|
|
5529
|
-
usage: `octwin usage [--json]
|
|
5530
|
-
Model calls, tokens and cost for the resolved scope — project when one is pinned
|
|
5531
|
-
or passed with --project, otherwise the whole workspace. Broken down by model,
|
|
5532
|
-
kind, agent and channel.
|
|
5533
|
-
|
|
5534
|
-
Needs no particular scope: any valid token reaches it.
|
|
5535
|
-
|
|
5536
|
-
This is MODEL spend only. WhatsApp/Meta message billing is operator-only and
|
|
5729
|
+
usage: `octwin usage [--json]
|
|
5730
|
+
Model calls, tokens and cost for the resolved scope — project when one is pinned
|
|
5731
|
+
or passed with --project, otherwise the whole workspace. Broken down by model,
|
|
5732
|
+
kind, agent and channel.
|
|
5733
|
+
|
|
5734
|
+
Needs no particular scope: any valid token reaches it.
|
|
5735
|
+
|
|
5736
|
+
This is MODEL spend only. WhatsApp/Meta message billing is operator-only and
|
|
5537
5737
|
deliberately outside the token scope registry — no API token can read it.`,
|
|
5538
|
-
'platform-kb': `octwin platform-kb [pull] [--if-stale] [--check] [--dir .] [--url <url>] [--token <t>]
|
|
5539
|
-
Pull the platform capability reference (markdown + JSON catalogs) into
|
|
5540
|
-
.octwin/platform-kb/ for the octwin-pack authoring skill, plus three maps:
|
|
5541
|
-
INDEX.md (the corpus) · SYMBOLS.md (every name -> its file; grep this) ·
|
|
5542
|
-
OUTLINE.md (every heading with its line number).
|
|
5543
|
-
|
|
5544
|
-
NO TOKEN NEEDED — the reference is platform stdlib and is served anonymously,
|
|
5545
|
-
and this command never sends one. --token is accepted and ignored, so an older
|
|
5546
|
-
script that passes it keeps working.
|
|
5547
|
-
|
|
5548
|
-
--if-stale poll the platform's content_hash first and skip the download when
|
|
5549
|
-
nothing changed. Cheap enough to run at the start of every session.
|
|
5550
|
-
--check report only, write nothing. Exit 0 = current, 2 = stale or never
|
|
5551
|
-
pulled, 1 = could not tell (offline / no reference served). For
|
|
5738
|
+
'platform-kb': `octwin platform-kb [pull] [--if-stale] [--check] [--dir .] [--url <url>] [--token <t>]
|
|
5739
|
+
Pull the platform capability reference (markdown + JSON catalogs) into
|
|
5740
|
+
.octwin/platform-kb/ for the octwin-pack authoring skill, plus three maps:
|
|
5741
|
+
INDEX.md (the corpus) · SYMBOLS.md (every name -> its file; grep this) ·
|
|
5742
|
+
OUTLINE.md (every heading with its line number).
|
|
5743
|
+
|
|
5744
|
+
NO TOKEN NEEDED — the reference is platform stdlib and is served anonymously,
|
|
5745
|
+
and this command never sends one. --token is accepted and ignored, so an older
|
|
5746
|
+
script that passes it keeps working.
|
|
5747
|
+
|
|
5748
|
+
--if-stale poll the platform's content_hash first and skip the download when
|
|
5749
|
+
nothing changed. Cheap enough to run at the start of every session.
|
|
5750
|
+
--check report only, write nothing. Exit 0 = current, 2 = stale or never
|
|
5751
|
+
pulled, 1 = could not tell (offline / no reference served). For
|
|
5552
5752
|
scripts and agent loops that want to branch without parsing prose.`,
|
|
5553
|
-
test: `octwin test [--dir .]
|
|
5753
|
+
test: `octwin test [--dir .]
|
|
5554
5754
|
Alias for \`octwin validate --remote\` — the full platform check.`,
|
|
5555
|
-
memos: `octwin memos [--all] [--json]
|
|
5556
|
-
Read what the platform has told you: a REPLY to a report you sent with
|
|
5557
|
-
\`octwin feedback\`, or a NOTICE published to every author (a new capability,
|
|
5558
|
-
a deprecation, a breaking change). Bodies are printed in full.
|
|
5559
|
-
Reading marks them read, so the reminder stops. --all re-reads history and
|
|
5560
|
-
acks nothing. --json to branch on \`severity\`
|
|
5755
|
+
memos: `octwin memos [--all] [--json]
|
|
5756
|
+
Read what the platform has told you: a REPLY to a report you sent with
|
|
5757
|
+
\`octwin feedback\`, or a NOTICE published to every author (a new capability,
|
|
5758
|
+
a deprecation, a breaking change). Bodies are printed in full.
|
|
5759
|
+
Reading marks them read, so the reminder stops. --all re-reads history and
|
|
5760
|
+
acks nothing. --json to branch on \`severity\`
|
|
5561
5761
|
(info | action_required | breaking).`,
|
|
5562
|
-
feedback: `octwin feedback [--dir .]
|
|
5563
|
-
Submit this pack's FEEDBACK.md to the platform team.
|
|
5564
|
-
The octwin-pack skill writes that file in its last step — findings grouped by
|
|
5565
|
-
owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
|
|
5566
|
-
you to paste it into a chat.
|
|
5567
|
-
Attaches the pack id + version from manifest.yaml, this CLI's version, and the
|
|
5568
|
-
content_hash of the capability reference in .octwin/platform-kb/ — triage needs
|
|
5569
|
-
the last two to tell "the platform is wrong" from "that was already fixed" or
|
|
5762
|
+
feedback: `octwin feedback [--dir .]
|
|
5763
|
+
Submit this pack's FEEDBACK.md to the platform team.
|
|
5764
|
+
The octwin-pack skill writes that file in its last step — findings grouped by
|
|
5765
|
+
owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
|
|
5766
|
+
you to paste it into a chat.
|
|
5767
|
+
Attaches the pack id + version from manifest.yaml, this CLI's version, and the
|
|
5768
|
+
content_hash of the capability reference in .octwin/platform-kb/ — triage needs
|
|
5769
|
+
the last two to tell "the platform is wrong" from "that was already fixed" or
|
|
5570
5770
|
"you were reading a stale reference". Needs the \`pack:deploy\` scope.`,
|
|
5571
5771
|
};
|
|
5572
5772
|
async function main() {
|