octwin-cli 0.8.16 → 0.8.17
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 +1109 -1028
- package/README.md +37 -29
- package/dist/cli.js +975 -463
- package/dist/lib/base.js +85 -0
- package/dist/lib/init-id.js +13 -0
- package/dist/lib/journey-check.js +119 -0
- package/dist/lib/kb-index.js +142 -18
- package/dist/lib/recipe.js +190 -0
- package/dist/lib/refusal.js +56 -0
- package/dist/lib/rename.js +1 -1
- package/package.json +46 -46
- package/templates/starter/commands.yaml +11 -12
- package/templates/starter/flows/tools/home.flow.yaml +34 -32
- package/templates/starter/flows/tools/home.locale.ar.yaml +8 -9
- package/templates/starter/flows/tools/home.locale.en.yaml +8 -0
- package/templates/starter/flows/tools/request.flow.yaml +95 -0
- package/templates/starter/flows/tools/request.locale.ar.yaml +14 -0
- package/templates/starter/flows/tools/request.locale.en.yaml +14 -0
- package/templates/starter/journeys/request.journey.yaml +12 -0
- package/templates/starter/locale.ar.yaml +6 -10
- package/templates/starter/locale.en.yaml +9 -0
- package/templates/starter/manifest.yaml +44 -45
- package/templates/starter/messages.ar.yaml +3 -10
- package/templates/starter/messages.en.yaml +19 -0
- package/templates/starter/prompts/identity.md +7 -5
- package/templates/starter/xrm.yaml +32 -0
- package/templates/starter/flows/tools/browse.flow.yaml +0 -31
- package/templates/starter/flows/tools/browse.locale.ar.yaml +0 -6
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
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
|
|
16
|
-
* octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
16
|
+
* octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed] [--force]
|
|
17
17
|
* [--request-listing | --withdraw-listing] # marketplace listing — opt-in, see `deploy` help
|
|
18
18
|
* octwin pull <packId> [--dir <out>] [--version v] [--force] # write a DEPLOYED pack's source back to disk
|
|
19
19
|
* octwin status [<packId>] # did my deploy land? which version is live?
|
|
@@ -64,6 +64,8 @@ import { homedir, hostname, platform } from 'node:os';
|
|
|
64
64
|
import { fileURLToPath } from 'node:url';
|
|
65
65
|
import { parse as parseYaml } from 'yaml';
|
|
66
66
|
import { applyRenames } from './lib/rename.js';
|
|
67
|
+
import { packIdFromDir } from './lib/init-id.js';
|
|
68
|
+
import { recipeModules, recipeStarters, planRecipe, recipeNeeds } from './lib/recipe.js';
|
|
67
69
|
import { validatePackBundle, describePackNameProblem, asPackName } from './lib/validate.js';
|
|
68
70
|
import { loadAllowedRenderKeys, findRenderKeyViolations, describeRenderFinding } from './lib/render-check.js';
|
|
69
71
|
import { loadPrimitiveArgSpecs, findArgViolations, describeArgFinding, findArgWarnings, describeArgWarning, } from './lib/args-check.js';
|
|
@@ -76,9 +78,12 @@ import { loadBuiltinNames, findBuiltinViolations, describeBuiltinFinding } from
|
|
|
76
78
|
import { loadTemplateSpecs, findTemplateViolations, describeTemplateFinding } from './lib/template-check.js';
|
|
77
79
|
import { loadSystemEntities, findEntityViolations, describeEntityFinding } from './lib/entity-check.js';
|
|
78
80
|
import { loadDeclarationSpecs, findDeclarationViolations, describeDeclarationFinding } from './lib/declaration-check.js';
|
|
81
|
+
import { collectJourneyIds, findJourneyViolations, describeJourneyFinding, isFlowFile } from './lib/journey-check.js';
|
|
79
82
|
import { findPlatformKbDir } from './lib/kb-path.js';
|
|
80
83
|
import { reportChecks } from './lib/outcome.js';
|
|
81
84
|
import { classifyPackPath, isSkippedDir } from './lib/pack-source.js';
|
|
85
|
+
import { BASE_FILE, deployBaseOf, hashBundle, planPull, readBase, writeBase } from './lib/base.js';
|
|
86
|
+
import { formatChecksRefusal } from './lib/refusal.js';
|
|
82
87
|
import { readPage, morePageHint } from './lib/page.js';
|
|
83
88
|
import { chatLabel, answersTheTurn } from './lib/chat-line.js';
|
|
84
89
|
import { kbOneLiner, buildKbIndexMarkdown, buildKbOutlineMarkdown, } from './lib/kb-index.js';
|
|
@@ -291,6 +296,9 @@ const COMMAND_REQUIREMENTS = {
|
|
|
291
296
|
// derived from record + journey data, and the route is gated accordingly, so the
|
|
292
297
|
// Read-only token preset already reaches this.
|
|
293
298
|
performance: { scope: 'records:read' },
|
|
299
|
+
charts: { scope: 'records:read', feature: 'records' },
|
|
300
|
+
handoff: { scope: 'work:read', feature: 'live_chat' },
|
|
301
|
+
surveys: { scope: 'records:read', feature: 'records' },
|
|
294
302
|
// `usage` is deliberately absent: its route is `requireTenantAccess` only, so any
|
|
295
303
|
// valid token reaches it. Declaring a requirement would print "needs the X scope"
|
|
296
304
|
// on a failure whose real cause is an unreachable instance — the same reasoning as
|
|
@@ -914,6 +922,9 @@ function commandTouchesPlatform(command, flags) {
|
|
|
914
922
|
case 'journeys':
|
|
915
923
|
case 'performance':
|
|
916
924
|
case 'usage':
|
|
925
|
+
case 'charts':
|
|
926
|
+
case 'surveys':
|
|
927
|
+
case 'handoff':
|
|
917
928
|
case 'projects':
|
|
918
929
|
case 'seed': return true;
|
|
919
930
|
default: return false;
|
|
@@ -927,7 +938,7 @@ function cmdInit(flags) {
|
|
|
927
938
|
// which is why `init` can stay offline (it has no idea which workspace this will deploy
|
|
928
939
|
// to, and no longer needs one) and why the same scaffold deploys into two workspaces as
|
|
929
940
|
// two packs with nothing to edit in between.
|
|
930
|
-
const id = flags.id ?? target
|
|
941
|
+
const id = flags.id ?? packIdFromDir(target);
|
|
931
942
|
const idProblem = describePackNameProblem(id);
|
|
932
943
|
if (idProblem)
|
|
933
944
|
die(`${idProblem}\n → pass --id <pack-name>`);
|
|
@@ -948,17 +959,127 @@ function cmdInit(flags) {
|
|
|
948
959
|
// the token carries its own tenant + optional project pin.
|
|
949
960
|
writeFileSync(join(dir, '.gitignore'), 'node_modules/\n.pack-bundles/\n.octwin/\n', 'utf8');
|
|
950
961
|
if (!existsSync(join(dir, 'README.md'))) {
|
|
951
|
-
writeFileSync(join(dir, 'README.md'),
|
|
962
|
+
writeFileSync(join(dir, 'README.md'), [
|
|
963
|
+
`# ${id}`, '',
|
|
964
|
+
'A pure-YAML pack for the Octwin platform, in Arabic and English.', '',
|
|
965
|
+
'- `flows/tools/home.flow.yaml` — the menu (your front door); add a row per journey',
|
|
966
|
+
'- `flows/tools/request.flow.yaml` + `xrm.yaml` — a visit request, saved as a record; replace it with your own',
|
|
967
|
+
'- `journeys/request.journey.yaml` — the funnel the owner reads on the Performance page',
|
|
968
|
+
'- `octwin platform-kb pull` — the authoring guides and recipes; start with `.octwin/platform-kb/INDEX.md`',
|
|
969
|
+
'- `octwin recipe` — list the recipes; `octwin recipe <module>` adds one to this pack',
|
|
970
|
+
'- `octwin validate` — the platform\'s full check, before every deploy',
|
|
971
|
+
'- `octwin deploy --seed` — deploy, install, and add the demo rows',
|
|
972
|
+
'',
|
|
973
|
+
].join('\n'), 'utf8');
|
|
952
974
|
}
|
|
953
975
|
out.print(`✓ Scaffolded pure-YAML pack '${id}' at ${dir}`);
|
|
954
976
|
out.print('\nNext:');
|
|
955
977
|
out.print(` cd ${target}`);
|
|
956
978
|
out.print(' git init && git add -A && git commit -m "init pack"');
|
|
957
|
-
out.print(' #
|
|
958
|
-
out.print(' octwin
|
|
959
|
-
out.print(' #
|
|
960
|
-
out.print('
|
|
961
|
-
out.print(' octwin
|
|
979
|
+
out.print(' octwin platform-kb pull # the authoring guides + recipes — read .octwin/platform-kb/INDEX.md first');
|
|
980
|
+
out.print(' octwin recipe # the recipes you can add; `octwin recipe <module>` writes one in');
|
|
981
|
+
out.print(' octwin validate # the platform\'s FULL check — no login needed');
|
|
982
|
+
out.print(' # point the CLI at your platform (the token carries the tenant), then deploy with the demo rows:');
|
|
983
|
+
out.print(' octwin login --url <platformUrl>');
|
|
984
|
+
out.print(' octwin deploy --seed');
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* `octwin recipe [<module>] [--dir .] [--dry-run]` — a KB recipe's starter files into this pack.
|
|
988
|
+
*
|
|
989
|
+
* Read from the PULLED guide (`lib/recipe.ts`), so a recipe has one copy — the one the platform holds
|
|
990
|
+
* to its full deploy check together with every other recipe, on the `init` starter. Nothing is
|
|
991
|
+
* overwritten: a file the pack already has is named, and a declaration it already has (an `xrm.yaml`
|
|
992
|
+
* holding the author's own entities) gets the recipe's part printed to merge. With no module it lists
|
|
993
|
+
* the recipes the pulled KB carries.
|
|
994
|
+
*/
|
|
995
|
+
function cmdRecipe(flags) {
|
|
996
|
+
const packDir = resolve(flags.dir ?? '.');
|
|
997
|
+
const kbDir = findPlatformKbDir(packDir)
|
|
998
|
+
?? die('no pulled KB here or above — run `octwin platform-kb pull` first (a recipe is read from it)');
|
|
999
|
+
let index = [];
|
|
1000
|
+
try {
|
|
1001
|
+
index = JSON.parse(readFileSync(join(kbDir, 'index.json'), 'utf8')).index ?? [];
|
|
1002
|
+
}
|
|
1003
|
+
catch {
|
|
1004
|
+
die(`${join(kbDir, 'index.json')} could not be read — re-run \`octwin platform-kb pull\``);
|
|
1005
|
+
}
|
|
1006
|
+
const modules = recipeModules(index);
|
|
1007
|
+
const module = flags._[0]?.toLowerCase();
|
|
1008
|
+
if (!module) {
|
|
1009
|
+
if (modules.size === 0)
|
|
1010
|
+
die('the pulled KB names no recipe — re-run `octwin platform-kb pull` (an older platform published none)');
|
|
1011
|
+
out.print('Recipes in the pulled KB — `octwin recipe <module>` writes one into this pack:');
|
|
1012
|
+
for (const [m, key] of modules)
|
|
1013
|
+
out.print(` ${m.padEnd(14)} guide:${key}#${m}-recipe`);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
const key = modules.get(module) ?? die(`no recipe '${module}' in the pulled KB — known: ${[...modules.keys()].join(', ')}`);
|
|
1017
|
+
const starters = recipeStarters(readFileSync(join(kbDir, `${key}.md`), 'utf8'), module)
|
|
1018
|
+
?? die(`${key}.md has no '## ${module} recipe' section — re-run \`octwin platform-kb pull\``);
|
|
1019
|
+
const manifestPath = join(packDir, 'manifest.yaml');
|
|
1020
|
+
if (!existsSync(manifestPath))
|
|
1021
|
+
die(`no manifest.yaml in ${packDir} — run this inside a pack, or pass --dir`);
|
|
1022
|
+
const manifest = parseYaml(readFileSync(manifestPath, 'utf8'));
|
|
1023
|
+
const { actions, flows } = planRecipe(starters, p => existsSync(join(packDir, p)), String(manifest.id ?? ''));
|
|
1024
|
+
const dry = flags['dry-run'] === true;
|
|
1025
|
+
out.print(`${dry ? 'Would apply' : 'Applying'} the ${module} recipe (guide:${key}#${module}-recipe) to ${packDir}`);
|
|
1026
|
+
for (const a of actions) {
|
|
1027
|
+
if (a.act === 'write') {
|
|
1028
|
+
if (!dry) {
|
|
1029
|
+
mkdirSync(dirname(join(packDir, a.path)), { recursive: true });
|
|
1030
|
+
writeFileSync(join(packDir, a.path), a.body, 'utf8');
|
|
1031
|
+
}
|
|
1032
|
+
out.print(` ✓ ${dry ? 'would write' : 'wrote'} ${a.path}`);
|
|
1033
|
+
}
|
|
1034
|
+
else if (a.act === 'exists') {
|
|
1035
|
+
out.print(` • ${a.path} already exists — left as it is (compare it with guide:${key}#${module}-starter)`);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
for (const a of actions) {
|
|
1039
|
+
if (a.act === 'merge') {
|
|
1040
|
+
out.print(`\n${a.path} already exists — merge this part into it${a.note ? ` (${a.note})` : ''}:`);
|
|
1041
|
+
out.print(a.body.trimEnd());
|
|
1042
|
+
}
|
|
1043
|
+
else if (a.act === 'manifest') {
|
|
1044
|
+
out.print('\nAdd to manifest.yaml:');
|
|
1045
|
+
out.print(a.body.trimEnd());
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
const listed = (v) => (Array.isArray(v) ? v.map(String) : []);
|
|
1049
|
+
const newFlows = flows.filter(f => !listed(manifest.flows).includes(f));
|
|
1050
|
+
const newTools = flows.filter(f => !listed(manifest.agents?.[0]?.tools).includes(f));
|
|
1051
|
+
if (newFlows.length || newTools.length) {
|
|
1052
|
+
out.print('\nList the new flows in manifest.yaml:');
|
|
1053
|
+
if (newFlows.length)
|
|
1054
|
+
out.print(` flows: ${newFlows.map(f => `- ${f}`).join(' ')}`);
|
|
1055
|
+
if (newTools.length)
|
|
1056
|
+
out.print(` agents[0].tools: ${newTools.map(f => `- ${f}`).join(' ')}`);
|
|
1057
|
+
}
|
|
1058
|
+
// What this recipe's buttons and `track:` ids need that the pack does not ship yet — other recipes,
|
|
1059
|
+
// derived from their files (`recipeNeeds`). Said now, so the author does not meet the chain as one
|
|
1060
|
+
// validate error after another.
|
|
1061
|
+
const all = new Map();
|
|
1062
|
+
for (const [m, k] of modules) {
|
|
1063
|
+
const s = existsSync(join(kbDir, `${k}.md`)) ? recipeStarters(readFileSync(join(kbDir, `${k}.md`), 'utf8'), m) : null;
|
|
1064
|
+
if (s)
|
|
1065
|
+
all.set(m, s);
|
|
1066
|
+
}
|
|
1067
|
+
const journeyDir = join(packDir, 'journeys');
|
|
1068
|
+
const journeys = existsSync(journeyDir)
|
|
1069
|
+
? readdirSync(journeyDir).filter(f => f.endsWith('.journey.yaml'))
|
|
1070
|
+
.map(f => [`journeys/${f}`, parseYaml(readFileSync(join(journeyDir, f), 'utf8'))])
|
|
1071
|
+
: [];
|
|
1072
|
+
const { needs, unresolved } = recipeNeeds(module, all, {
|
|
1073
|
+
flows: new Set([...listed(manifest.flows), ...flows]), journeys,
|
|
1074
|
+
});
|
|
1075
|
+
if (needs.length) {
|
|
1076
|
+
out.print(`\nThis recipe also needs ${needs.length === 1 ? 'another recipe' : `${needs.length} other recipes`} — its buttons and \`track:\` ids name what they ship:`);
|
|
1077
|
+
for (const n of needs)
|
|
1078
|
+
out.print(` octwin recipe ${n}`);
|
|
1079
|
+
}
|
|
1080
|
+
for (const u of unresolved)
|
|
1081
|
+
out.print(` ! ${u}`);
|
|
1082
|
+
out.print(`\nThen read guide:${key}#${module}-ingredients and #${module}-rules, add the home row it names, and run \`octwin validate\`.`);
|
|
962
1083
|
}
|
|
963
1084
|
function localValidate(packDir) {
|
|
964
1085
|
const { files, blobs } = collectBundleFiles(packDir);
|
|
@@ -1194,6 +1315,25 @@ async function cmdValidate(flags) {
|
|
|
1194
1315
|
: { kind: 'passed', line: 'declaration files carry no unknown or missing keys (unions are left to --remote)' };
|
|
1195
1316
|
},
|
|
1196
1317
|
},
|
|
1318
|
+
{
|
|
1319
|
+
// Journey ids named by `track:` / `do: track_event`, against the pack's OWN `journeys/`.
|
|
1320
|
+
// Needs no KB — the declared set is in the bundle — so it never skips. Last, because a
|
|
1321
|
+
// malformed journey file is the declaration check's finding, not a wall of these.
|
|
1322
|
+
label: 'journey ids',
|
|
1323
|
+
run: () => {
|
|
1324
|
+
const ids = collectJourneyIds(yamlDocs());
|
|
1325
|
+
const findings = withLines(yamlDocs()
|
|
1326
|
+
.filter(([p]) => isFlowFile(p))
|
|
1327
|
+
.flatMap(([p, doc]) => findJourneyViolations(doc, p, ids)));
|
|
1328
|
+
return findings.length
|
|
1329
|
+
? {
|
|
1330
|
+
kind: 'findings', noun: 'undeclared journey id',
|
|
1331
|
+
lines: findings.map(describeJourneyFinding),
|
|
1332
|
+
hint: 'fix these before deploying — the platform refuses them too, and at runtime a `track:` throws mid-conversation while `track_event` takes its `failed` port',
|
|
1333
|
+
}
|
|
1334
|
+
: { kind: 'passed', line: 'every literal journey id in `track:` / `track_event` is declared in journeys/' };
|
|
1335
|
+
},
|
|
1336
|
+
},
|
|
1197
1337
|
];
|
|
1198
1338
|
const skipped = reportChecks(checks, {
|
|
1199
1339
|
log: (l) => out.print(l),
|
|
@@ -1639,16 +1779,20 @@ async function cmdWhoami(flags) {
|
|
|
1639
1779
|
* You may pull a pack YOUR tenant owns (an operator token may pull any). A pack
|
|
1640
1780
|
* you merely installed from the marketplace is not yours to read.
|
|
1641
1781
|
*
|
|
1642
|
-
* Writes into
|
|
1643
|
-
*
|
|
1644
|
-
*
|
|
1645
|
-
*
|
|
1782
|
+
* Writes into the pack folder you are standing in when it IS this pack (its
|
|
1783
|
+
* `.octwin-base.json` or its manifest says so), else into `./<packId>` — `--dir`
|
|
1784
|
+
* overrides. It replaces only files that still match the folder's base: a file you
|
|
1785
|
+
* changed since your last pull or deploy stops it (`planPull`), unless `--force`. It
|
|
1786
|
+
* then writes the new base, so the next `octwin deploy` is judged against what you
|
|
1787
|
+
* pulled. The pulled directory is immediately `octwin deploy`-able back to where it
|
|
1788
|
+
* came from — the target is the saved login, so nothing machine-specific needs to
|
|
1789
|
+
* land in the pack dir.
|
|
1646
1790
|
*/
|
|
1647
1791
|
async function cmdPull(flags) {
|
|
1648
1792
|
const packId = flags._[0] ?? '';
|
|
1649
1793
|
if (!packId)
|
|
1650
1794
|
die('usage: octwin pull <packId> [--dir <out>] [--version <v>] [--force]');
|
|
1651
|
-
const outDir =
|
|
1795
|
+
const outDir = pullTarget(flags, packId);
|
|
1652
1796
|
const t = resolveTarget(flags);
|
|
1653
1797
|
const { url } = t;
|
|
1654
1798
|
const qs = typeof flags.version === 'string' ? `?version=${encodeURIComponent(flags.version)}` : '';
|
|
@@ -1676,8 +1820,23 @@ async function cmdPull(flags) {
|
|
|
1676
1820
|
const total = Object.keys(files).length + Object.keys(blobs).length;
|
|
1677
1821
|
if (total === 0)
|
|
1678
1822
|
die(`${packId}@${json.version} has no files — nothing to write`);
|
|
1679
|
-
|
|
1680
|
-
|
|
1823
|
+
// Replace only what still matches the base — never the author's own edits (`planPull`).
|
|
1824
|
+
const incoming = hashBundle(files, blobs);
|
|
1825
|
+
const prior = readBase(outDir);
|
|
1826
|
+
const base = prior?.pack_id === json.pack_id ? prior : null;
|
|
1827
|
+
const here = existsSync(outDir) ? collectBundleFiles(outDir) : { files: {}, blobs: {} };
|
|
1828
|
+
const { atRisk, remove } = planPull({ local: hashBundle(here.files, here.blobs), incoming, base });
|
|
1829
|
+
if (atRisk.length > 0 && flags.force !== true) {
|
|
1830
|
+
out.err(`✗ ${outDir} has changes the live ${json.version} does not — pulling would overwrite them:`);
|
|
1831
|
+
for (const rel of atRisk.slice(0, 15))
|
|
1832
|
+
out.err(` • ${rel}`);
|
|
1833
|
+
if (atRisk.length > 15)
|
|
1834
|
+
out.err(` … and ${atRisk.length - 15} more`);
|
|
1835
|
+
out.err(base
|
|
1836
|
+
? ' Save them first (git commit, or copy the folder), then pull with --force and apply them again.'
|
|
1837
|
+
: ` This folder has no ${BASE_FILE} (the record of its last pull or deploy), so your edits cannot be told `
|
|
1838
|
+
+ 'apart from an old version. Save them first (git commit, or copy the folder), then pull with --force.');
|
|
1839
|
+
exitNow(1);
|
|
1681
1840
|
}
|
|
1682
1841
|
for (const [rel, body] of Object.entries(files)) {
|
|
1683
1842
|
const p = join(outDir, rel);
|
|
@@ -1689,9 +1848,35 @@ async function cmdPull(flags) {
|
|
|
1689
1848
|
mkdirSync(dirname(p), { recursive: true });
|
|
1690
1849
|
writeFileSync(p, Buffer.from(b64, 'base64'));
|
|
1691
1850
|
}
|
|
1851
|
+
// The live version no longer has these — left behind, the next deploy would bring them back.
|
|
1852
|
+
for (const rel of remove)
|
|
1853
|
+
rmSync(join(outDir, rel), { force: true });
|
|
1854
|
+
writeBase(outDir, { pack_id: json.pack_id, version: json.version, content_sha: json.content_sha, files: incoming });
|
|
1692
1855
|
out.print(`✓ Pulled ${json.pack_id}@${json.version} → ${outDir}`);
|
|
1693
|
-
out.print(` ${Object.keys(files).length} file(s)${Object.keys(blobs).length ? `, ${Object.keys(blobs).length} blob(s)` : ''}
|
|
1694
|
-
|
|
1856
|
+
out.print(` ${Object.keys(files).length} file(s)${Object.keys(blobs).length ? `, ${Object.keys(blobs).length} blob(s)` : ''}`
|
|
1857
|
+
+ `${remove.length ? `, ${remove.length} removed` : ''} content_sha ${String(json.content_sha).slice(0, 12)}…`);
|
|
1858
|
+
out.print(`\nFix it, then: octwin deploy${outDir === resolve('.') ? '' : ` --dir ${outDir}`}`);
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Where `pull` writes: `--dir`, else the folder you are in when it is this pack (its base names it, or
|
|
1862
|
+
* its manifest declares the pack's name — the folder a refused deploy was run from), else `./<packId>`.
|
|
1863
|
+
*/
|
|
1864
|
+
function pullTarget(flags, packId) {
|
|
1865
|
+
if (typeof flags.dir === 'string')
|
|
1866
|
+
return resolve(flags.dir);
|
|
1867
|
+
const here = resolve('.');
|
|
1868
|
+
if (readBase(here)?.pack_id === packId)
|
|
1869
|
+
return here;
|
|
1870
|
+
const manifest = join(here, 'manifest.yaml');
|
|
1871
|
+
if (existsSync(manifest)) {
|
|
1872
|
+
try {
|
|
1873
|
+
const id = parseYaml(readFileSync(manifest, 'utf8'))?.id;
|
|
1874
|
+
if (typeof id === 'string' && id === packId.slice(packId.indexOf('.') + 1))
|
|
1875
|
+
return here;
|
|
1876
|
+
}
|
|
1877
|
+
catch { /* not a manifest we can read — not this pack */ }
|
|
1878
|
+
}
|
|
1879
|
+
return resolve(packId);
|
|
1695
1880
|
}
|
|
1696
1881
|
/**
|
|
1697
1882
|
* Read the deploy SSE stream, printing each progress frame's message live, and
|
|
@@ -1849,6 +2034,9 @@ function printDeploySuccess(id, version, t, r, listing, problems = 0) {
|
|
|
1849
2034
|
out.print(problems > 0
|
|
1850
2035
|
? `⚠ Deployed ${id}@${version} onto ${targetLabel(t)} WITH ${problems} failure(s) — data may be incomplete`
|
|
1851
2036
|
: `✓ Deployed ${id}@${version} and installed onto ${targetLabel(t)}`);
|
|
2037
|
+
if (r?.forced_over) {
|
|
2038
|
+
out.print(` ⚠ --force replaced ${r.forced_over === 'downgrade' ? 'a higher live version' : 'a version someone else published after your copy'}.`);
|
|
2039
|
+
}
|
|
1852
2040
|
if (r?.warning)
|
|
1853
2041
|
out.print(` ⚠ ${r.warning}`);
|
|
1854
2042
|
// This read `r.summary.records` / `.rules` / `.failed` and had been DEAD: the deploy route's
|
|
@@ -1908,7 +2096,7 @@ async function cmdSeed(flags) {
|
|
|
1908
2096
|
if (stepErrors.length) {
|
|
1909
2097
|
// A kind failed but the rest ran — the reconcile softens each step. Say which,
|
|
1910
2098
|
// and exit non-zero so a scripted `seed && chat` doesn't read as clean.
|
|
1911
|
-
out.err(`
|
|
2099
|
+
out.err(`
|
|
1912
2100
|
⚠ ${stepErrors.length} step${stepErrors.length === 1 ? '' : 's'} failed — data may be incomplete:`);
|
|
1913
2101
|
for (const e of stepErrors)
|
|
1914
2102
|
out.err(` • ${e}`);
|
|
@@ -1954,7 +2142,12 @@ async function cmdDeploy(flags) {
|
|
|
1954
2142
|
const endpoint = `${url}/api/self/p/packs/deploy`;
|
|
1955
2143
|
const seed = flags.seed === true;
|
|
1956
2144
|
const listing = resolveListingFlags(flags);
|
|
1957
|
-
|
|
2145
|
+
// What this copy was made from: the platform refuses when someone published since (`live_moved`).
|
|
2146
|
+
const base = deployBaseOf(readBase(packDir));
|
|
2147
|
+
const force = flags.force === true;
|
|
2148
|
+
const extras = [
|
|
2149
|
+
seed ? 'with demo seed' : null, listing ? LISTING_LABEL[listing] : null, force ? 'forced over any newer live version' : null,
|
|
2150
|
+
].filter(Boolean);
|
|
1958
2151
|
out.print(`→ Deploying ${id}@${version} (${Object.keys(files).length} files) to ${targetLabel(t)}${extras.length ? ` — ${extras.join(', ')}` : ''} …`);
|
|
1959
2152
|
const res = await fetchOrDie(endpoint, {
|
|
1960
2153
|
method: 'POST',
|
|
@@ -1963,7 +2156,9 @@ async function cmdDeploy(flags) {
|
|
|
1963
2156
|
headers: { 'content-type': 'application/json', accept: 'text/event-stream', ...authHeaders(t) },
|
|
1964
2157
|
// `listing` is omitted entirely unless asked for: an ordinary deploy says NOTHING about the
|
|
1965
2158
|
// marketplace, which is what keeps a test loop out of the operator review queue.
|
|
1966
|
-
body: JSON.stringify({
|
|
2159
|
+
body: JSON.stringify({
|
|
2160
|
+
files, blobs, seed, ...(listing ? { listing } : {}), ...(base ? { base } : {}), ...(force ? { force: true } : {}),
|
|
2161
|
+
}),
|
|
1967
2162
|
}, 'deploy');
|
|
1968
2163
|
// Streaming path — live install + seed progress (image generation can take a
|
|
1969
2164
|
// while, so `--seed` prints per-record / per-image lines as they happen).
|
|
@@ -1971,6 +2166,7 @@ async function cmdDeploy(flags) {
|
|
|
1971
2166
|
const { terminal: final, stepErrors } = await readDeployProgress(res.body);
|
|
1972
2167
|
if (!final || final.stage === 'error')
|
|
1973
2168
|
die(`deploy failed${final?.message ? `: ${final.message}` : ' (stream ended early)'}`);
|
|
2169
|
+
rememberDeploy(packDir, final, files, blobs);
|
|
1974
2170
|
// The count goes IN, so the headline itself carries the verdict. It used to be
|
|
1975
2171
|
// printed underneath a `✓ Deployed` line, which is what a reporting author read.
|
|
1976
2172
|
printDeploySuccess(id, version, t, final, { intent: listing, packDir }, stepErrors.length);
|
|
@@ -1996,11 +2192,28 @@ async function cmdDeploy(flags) {
|
|
|
1996
2192
|
json = text;
|
|
1997
2193
|
}
|
|
1998
2194
|
if (!res.ok) {
|
|
2195
|
+
// Someone published since this copy was made, or it is an older version: nothing changed on the
|
|
2196
|
+
// platform. The message says what happened and the way out (pull, or --force) — print it as words.
|
|
2197
|
+
if (res.status === 409 && (json?.code === 'live_moved' || json?.code === 'downgrade')) {
|
|
2198
|
+
out.err(`✗ ${json.error}`);
|
|
2199
|
+
if (json.code === 'live_moved' && typeof flags.dir === 'string') {
|
|
2200
|
+
out.err(` This copy is in ${flags.dir} — add \`--dir ${flags.dir}\` to the pull.`);
|
|
2201
|
+
}
|
|
2202
|
+
exitNow(1);
|
|
2203
|
+
}
|
|
2204
|
+
// The checks refused it: say what keeps running, then what to fix (`formatChecksRefusal`).
|
|
2205
|
+
const refusal = res.status === 400 ? formatChecksRefusal(json) : null;
|
|
2206
|
+
if (refusal) {
|
|
2207
|
+
for (const line of refusal)
|
|
2208
|
+
out.err(line);
|
|
2209
|
+
exitNow(1);
|
|
2210
|
+
}
|
|
1999
2211
|
out.err(`✗ deploy failed (HTTP ${res.status})`);
|
|
2000
2212
|
printAuthHint(res.status, url);
|
|
2001
2213
|
out.err(typeof json === 'string' ? json : JSON.stringify(json, null, 2));
|
|
2002
2214
|
exitNow(1);
|
|
2003
2215
|
}
|
|
2216
|
+
rememberDeploy(packDir, json, files, blobs);
|
|
2004
2217
|
// This path had NO step-error check at all. With no SSE frames to read, the only
|
|
2005
2218
|
// evidence a seed kind threw is `seed_failed` in the body — so a broken seed printed
|
|
2006
2219
|
// a clean `✓ Deployed` here even after the streaming path was fixed in 0.1.14.
|
|
@@ -2013,6 +2226,15 @@ async function cmdDeploy(flags) {
|
|
|
2013
2226
|
exitNow(1);
|
|
2014
2227
|
}
|
|
2015
2228
|
}
|
|
2229
|
+
/**
|
|
2230
|
+
* After a deploy: this folder's new base is what the project now runs. Written even when a seed step
|
|
2231
|
+
* failed — the pack itself IS installed. A platform too old to report `content_sha` leaves the base alone.
|
|
2232
|
+
*/
|
|
2233
|
+
function rememberDeploy(packDir, r, files, blobs) {
|
|
2234
|
+
if (typeof r?.content_sha !== 'string' || typeof r?.pack_id !== 'string' || typeof r?.version !== 'string')
|
|
2235
|
+
return;
|
|
2236
|
+
writeBase(packDir, { pack_id: r.pack_id, version: r.version, content_sha: r.content_sha, files: hashBundle(files, blobs) });
|
|
2237
|
+
}
|
|
2016
2238
|
/**
|
|
2017
2239
|
* The QUALIFIED pack id (`<owner>.<name>`) for a manifest's bare name.
|
|
2018
2240
|
*
|
|
@@ -2092,6 +2314,13 @@ async function cmdStatus(flags) {
|
|
|
2092
2314
|
const shortSha = (s) => (typeof s === 'string' && s ? s.slice(0, 12) + '…' : '(none)');
|
|
2093
2315
|
out.print(` live on instance : registered=${json.registered} loaded=${shortSha(json.loaded_content_sha)}`);
|
|
2094
2316
|
out.print(` catalog artifact : ${shortSha(json.catalog_content_sha)}${json.origin ? ` origin=${json.origin}` : ''}`);
|
|
2317
|
+
// The copy on disk against what runs: the question a deploy is about to be judged on (`live_moved`).
|
|
2318
|
+
const base = readBase(packDir);
|
|
2319
|
+
out.print(` your copy : ${!base || base.pack_id !== id
|
|
2320
|
+
? `no ${BASE_FILE} — run \`octwin pull ${id}\` once, so a deploy can tell when someone else published`
|
|
2321
|
+
: base.content_sha === json.catalog_content_sha
|
|
2322
|
+
? `from ${base.version} (${shortSha(base.content_sha)}) — the live content`
|
|
2323
|
+
: `from ${base.version} (${shortSha(base.content_sha)}) — the live content MOVED since; run \`octwin pull ${id}\` before you deploy`}`);
|
|
2095
2324
|
out.print(` flows : ${(json.flows ?? []).join(', ') || '(none)'}`);
|
|
2096
2325
|
// Every line above is process-global — `registered` is true for a pack ANY
|
|
2097
2326
|
// project on this instance loaded. `dispatches` is the project-scoped answer to
|
|
@@ -2322,9 +2551,10 @@ async function cmdPlatformKb(flags) {
|
|
|
2322
2551
|
writeFileSync(join(outDir, 'OUTLINE.md'), outline, 'utf8');
|
|
2323
2552
|
// Persist `content_hash` too — the staleness observer (`notifyIfKbStale`) reads
|
|
2324
2553
|
// it back and compares against the platform's current hash to nudge a re-pull.
|
|
2325
|
-
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');
|
|
2554
|
+
writeFileSync(join(outDir, 'index.json'), JSON.stringify({ version: bundle.version, content_hash: bundle.content_hash, generated_at: bundle.generated_at, platform_version: bundle.platform_version, index: bundle.index }, null, 2) + '\n', 'utf8');
|
|
2326
2555
|
out.print(`✓ Pulled the Octwin platform KB → ${outDir}`);
|
|
2327
|
-
out.print(` ${mdCount} markdown docs + ${catalogCount} catalogs (${entryCount} entries, one file each) — reference version ${bundle.version ?? '?'}`
|
|
2556
|
+
out.print(` ${mdCount} markdown docs + ${catalogCount} catalogs (${entryCount} entries, one file each) — reference version ${bundle.version ?? '?'}`
|
|
2557
|
+
+ `, published by platform ${bundle.platform_version ?? 'unknown'}`);
|
|
2328
2558
|
out.print(` Three maps: INDEX.md (the corpus) · SYMBOLS.md (${symbols.length} names → their file, grep it)`
|
|
2329
2559
|
+ `${outline ? ' · OUTLINE.md (every heading, with line numbers)' : ''}`);
|
|
2330
2560
|
if (!outline) {
|
|
@@ -4092,12 +4322,15 @@ async function cmdAgents(flags) {
|
|
|
4092
4322
|
* docs/BACKLOG.md → "Headless payment settle" (a `--settle` flag lands here once
|
|
4093
4323
|
* the platform grows a manual-driver-only reconcile endpoint). */
|
|
4094
4324
|
function printPaymentNote(paymentStatus) {
|
|
4095
|
-
if (paymentStatus
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4325
|
+
if (paymentStatus === 'none') {
|
|
4326
|
+
out.print(" → payment_status 'none': no payment was opened. Without a hosted-checkout gateway");
|
|
4327
|
+
out.print(' `payment_request` takes its `empty` port and leaves the order alone — the flow should offer');
|
|
4328
|
+
out.print(' pay-on-delivery. Refund is the one operator-driven move.');
|
|
4329
|
+
}
|
|
4330
|
+
else if (paymentStatus === 'pending') {
|
|
4331
|
+
out.print(" → payment_status 'pending': a checkout was opened and the gateway's webhook has not");
|
|
4332
|
+
out.print(' confirmed it yet. The forward payment lifecycle is WEBHOOK-owned (not patchable).');
|
|
4333
|
+
}
|
|
4101
4334
|
}
|
|
4102
4335
|
/** `octwin orders [referenceId] [--status s] [--payment p] [--limit n] [--json]` —
|
|
4103
4336
|
* the orders a conversation created: money breakdown, payment state, allowed
|
|
@@ -4108,8 +4341,8 @@ const ORDER_VERBS = new Set(['transition', 'refund']);
|
|
|
4108
4341
|
* The write half of `octwin orders` — fulfilment transitions and refunds.
|
|
4109
4342
|
*
|
|
4110
4343
|
* `payment_status` is deliberately NOT settable: the forward payment lifecycle is
|
|
4111
|
-
* webhook-owned
|
|
4112
|
-
*
|
|
4344
|
+
* webhook-owned: `none` is an order no payment was opened for (no gateway), `pending` one whose
|
|
4345
|
+
* checkout the gateway has not confirmed yet (`printPaymentNote`).
|
|
4113
4346
|
*/
|
|
4114
4347
|
async function cmdOrdersWrite(flags) {
|
|
4115
4348
|
const t = resolveTarget(flags);
|
|
@@ -4700,7 +4933,7 @@ async function cmdScheduling(flags) {
|
|
|
4700
4933
|
*
|
|
4701
4934
|
* `pause` / `resume` rather than a literal `status active|paused`: the route body
|
|
4702
4935
|
* takes the status, but the RBAC layer it calls checks the ACTION
|
|
4703
|
-
* (`
|
|
4936
|
+
* (`assertMay(…, action: 'pause' | 'resume')`), and `VERB_REQUIREMENTS` is keyed
|
|
4704
4937
|
* per verb — so one verb per intent makes both the permission hint and the 403
|
|
4705
4938
|
* say the right thing.
|
|
4706
4939
|
*/
|
|
@@ -4800,11 +5033,26 @@ async function cmdAutomationWrite(flags) {
|
|
|
4800
5033
|
return;
|
|
4801
5034
|
}
|
|
4802
5035
|
const r = json?.result ?? {};
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
5036
|
+
const already = Number(r.already_queued ?? 0);
|
|
5037
|
+
out.print(`✓ campaign '${typed}': matched ${r.matched ?? 0}, enqueued ${r.enqueued ?? 0}${already ? `, ${already} already had it` : ''} [${r.stage ?? '?'}]`);
|
|
5038
|
+
// A campaign that stopped half-way is FINISHED by the same command: it queues only the contacts
|
|
5039
|
+
// with no delivery yet, so `already` is the earlier send's work, not a skip.
|
|
5040
|
+
if (already)
|
|
5041
|
+
out.print(' finished an earlier send — each contact gets this campaign once');
|
|
5042
|
+
if ((r.enqueued ?? 0) + already < (r.matched ?? 0))
|
|
5043
|
+
out.print(' partial: some matched records were not enqueued (records sharing a contact, or a record with none)');
|
|
4806
5044
|
out.print(' enqueued ≠ delivered — watch the sends land with `octwin logs`');
|
|
4807
5045
|
}
|
|
5046
|
+
/**
|
|
5047
|
+
* A campaign's delivery progress — the per-status counts of the reminders it queued. Not sent yet
|
|
5048
|
+
* (a draft) is all zeros, and reads as a dash rather than "sent 0".
|
|
5049
|
+
*/
|
|
5050
|
+
function campaignProgressLine(p) {
|
|
5051
|
+
const n = (k) => Number(p?.[k] ?? 0);
|
|
5052
|
+
if (n('sent') + n('failed') + n('pending') + n('cancelled') === 0)
|
|
5053
|
+
return '—'.padEnd(28);
|
|
5054
|
+
return `sent ${n('sent')} · failed ${n('failed')} · pending ${n('pending')}${n('cancelled') ? ` · cancelled ${n('cancelled')}` : ''}`.padEnd(28);
|
|
5055
|
+
}
|
|
4808
5056
|
/**
|
|
4809
5057
|
* `octwin automation [--campaigns] [--json]` — the jobs a pack's declarations
|
|
4810
5058
|
* produced, with their last result, plus the health counts.
|
|
@@ -4838,15 +5086,17 @@ async function cmdAutomation(flags) {
|
|
|
4838
5086
|
const page = readPage(json);
|
|
4839
5087
|
out.print(`Campaigns in ${targetLabel(t)}: ${page.total ?? page.rows.length}`);
|
|
4840
5088
|
if (page.rows.length === 0)
|
|
4841
|
-
out.print(' (none — a
|
|
5089
|
+
out.print(' (none — a flow drafts one with the `campaign_create` step)');
|
|
5090
|
+
// What the route returns, field for field: `{ id, record_number, name, entity, stage, progress }`.
|
|
5091
|
+
// It read `label` / `key` / `audience_size` / `matched` until 2026-10-11 — none of which a campaign
|
|
5092
|
+
// row carries — so every row printed its uuid twice and "—".
|
|
4842
5093
|
for (const c of page.rows) {
|
|
4843
|
-
|
|
4844
|
-
out.print(` ${String(label).padEnd(28)} ${String(c.audience_size ?? c.matched ?? '—').padStart(6)} contact(s) ${c.key ?? c.id}`);
|
|
5094
|
+
out.print(` ${`#${c.record_number ?? '?'}`.padEnd(6)} ${String(c.name ?? '(no name)').padEnd(28)} ${String(c.stage ?? '—').padEnd(9)} ${campaignProgressLine(c.progress)} ${c.id}`);
|
|
4845
5095
|
}
|
|
4846
5096
|
const more = morePageHint(page, 'octwin automation campaigns');
|
|
4847
5097
|
if (more)
|
|
4848
5098
|
out.print(more);
|
|
4849
|
-
out.print('\nSend one
|
|
5099
|
+
out.print('\nSend a draft, or finish one stuck at `sending`: octwin automation send <campaignId> (needs automation:write; each contact gets it once)');
|
|
4850
5100
|
return;
|
|
4851
5101
|
}
|
|
4852
5102
|
if (!asJson)
|
|
@@ -4864,7 +5114,7 @@ async function cmdAutomation(flags) {
|
|
|
4864
5114
|
const page = readPage(jobs.json);
|
|
4865
5115
|
const h = health.status === 200 ? (health.json ?? {}) : {};
|
|
4866
5116
|
out.print(`Automation in ${targetLabel(t)}: ${h.total ?? page.rows.length} job(s)`
|
|
4867
|
-
+ ` — ${h.active ?? '?'} active, ${h.paused ?? '?'} paused, ${h.failing ?? '?'} failing, ${h.
|
|
5117
|
+
+ ` — ${h.active ?? '?'} active, ${h.paused ?? '?'} paused, ${h.failing ?? '?'} failing, ${h.never_checked ?? '?'} never checked`);
|
|
4868
5118
|
if (page.rows.length === 0) {
|
|
4869
5119
|
out.print(' (none — jobs are DERIVED from the pack\'s automation declaration, not created here.');
|
|
4870
5120
|
out.print(' No `automation.yaml` block → no jobs. `octwin deploy` installs them.)');
|
|
@@ -4872,7 +5122,9 @@ async function cmdAutomation(flags) {
|
|
|
4872
5122
|
}
|
|
4873
5123
|
for (const j of page.rows) {
|
|
4874
5124
|
const r = j.last_result ?? {};
|
|
4875
|
-
|
|
5125
|
+
// A job runs only when its look finds work, so "not run yet" is normal; "never checked" is the dead one.
|
|
5126
|
+
const ran = (j.last_run_at ? `last run ${j.last_run_at}` : 'not run yet')
|
|
5127
|
+
+ (j.last_checked_at ? `, checked ${j.last_checked_at}` : ', never checked');
|
|
4876
5128
|
const result = j.last_result
|
|
4877
5129
|
? ` matched ${r.matched ?? 0}/acted ${r.acted ?? 0}${(r.errors ?? 0) > 0 ? `/ERRORS ${r.errors}` : ''}`
|
|
4878
5130
|
: '';
|
|
@@ -5334,8 +5586,214 @@ async function cmdJourneys(flags) {
|
|
|
5334
5586
|
}
|
|
5335
5587
|
// ── performance: the project's business indicators ──────────────────────────
|
|
5336
5588
|
/**
|
|
5337
|
-
* `octwin
|
|
5338
|
-
*
|
|
5589
|
+
* `octwin charts [<key>] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--json]` — the pack's own `charts:`
|
|
5590
|
+
* (xrm.yaml), as the owner's dashboard draws them: the list, or one chart's figure, parts or points.
|
|
5591
|
+
* Needs `records:read`; a `view` grant on each chart's entity decides which ones the token sees.
|
|
5592
|
+
*/
|
|
5593
|
+
async function cmdCharts(flags) {
|
|
5594
|
+
const t = resolveTarget(flags);
|
|
5595
|
+
const { url } = t;
|
|
5596
|
+
const base = `${url}/api/self/p/xrm/charts`;
|
|
5597
|
+
const key = flags._[0];
|
|
5598
|
+
const asJson = flags.json === true;
|
|
5599
|
+
if (!key) {
|
|
5600
|
+
if (!asJson)
|
|
5601
|
+
out.print(`→ Reading the pack's charts from ${targetLabel(t)} …`);
|
|
5602
|
+
const { status, json } = await apiGet(`${base}`, t);
|
|
5603
|
+
if (status !== 200)
|
|
5604
|
+
die(`could not read charts (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
5605
|
+
if (asJson) {
|
|
5606
|
+
out.result(json);
|
|
5607
|
+
return;
|
|
5608
|
+
}
|
|
5609
|
+
const charts = (json?.charts ?? []);
|
|
5610
|
+
if (charts.length === 0) {
|
|
5611
|
+
out.print('No charts — a pack declares them under `charts:` in xrm.yaml (craft-measure, "Your own charts"), or this token may not view their entities.');
|
|
5612
|
+
return;
|
|
5613
|
+
}
|
|
5614
|
+
for (const c of charts)
|
|
5615
|
+
out.print(` ${c.key.padEnd(24)} ${(pickLabel(c.label) ?? c.key).padEnd(32)} ${c.show} · ${c.entity}`);
|
|
5616
|
+
out.print('\n`octwin charts <key>` draws one.');
|
|
5617
|
+
return;
|
|
5618
|
+
}
|
|
5619
|
+
const q = new URLSearchParams();
|
|
5620
|
+
if (typeof flags.from === 'string')
|
|
5621
|
+
q.set('from', flags.from);
|
|
5622
|
+
if (typeof flags.to === 'string')
|
|
5623
|
+
q.set('to', flags.to);
|
|
5624
|
+
const qs = q.toString() ? `?${q}` : '';
|
|
5625
|
+
if (!asJson)
|
|
5626
|
+
out.print(`→ Reading chart '${key}' from ${targetLabel(t)} …`);
|
|
5627
|
+
const { status, json } = await apiGet(`${base}/${encodeURIComponent(key)}${qs}`, t);
|
|
5628
|
+
if (status !== 200)
|
|
5629
|
+
die(`could not read chart '${key}' (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
5630
|
+
if (asJson) {
|
|
5631
|
+
out.result(json);
|
|
5632
|
+
return;
|
|
5633
|
+
}
|
|
5634
|
+
if (json?.has_data !== true) {
|
|
5635
|
+
out.print(`No figure for '${key}' — its entity has no records in this window, or this token may not view it.`);
|
|
5636
|
+
return;
|
|
5637
|
+
}
|
|
5638
|
+
const c = json.chart;
|
|
5639
|
+
out.print(`${pickLabel(c.label) ?? c.key} ${json.range?.from ?? '?'} → ${json.range?.to ?? '?'}`);
|
|
5640
|
+
if (c.value !== undefined)
|
|
5641
|
+
out.print(` ${c.value ?? '—'}${c.ratio ? ` (${c.ratio.of ?? '—'} ÷ ${c.ratio.per ?? '—'})` : ''}`);
|
|
5642
|
+
for (const p of c.parts ?? [])
|
|
5643
|
+
out.print(` ${String(pickLabel(p.label) ?? p.key).padEnd(28)} ${String(p.value ?? '—').padStart(10)} n=${p.count}`);
|
|
5644
|
+
if (c.cut)
|
|
5645
|
+
out.print(` … top ${c.cut.kept} of ${c.cut.groups}${c.cut.other ? `; the rest: ${c.cut.other.value ?? '—'} (n=${c.cut.other.count})` : ''}`);
|
|
5646
|
+
for (const p of c.points ?? [])
|
|
5647
|
+
out.print(` ${String(p.bucket).padEnd(12)} ${String(p.value ?? '—').padStart(10)} n=${p.count}`);
|
|
5648
|
+
}
|
|
5649
|
+
/**
|
|
5650
|
+
* `octwin handoff [<id>] [--bucket waiting|mine|all|ended] [--limit n] [--offset n] [--json]` — the
|
|
5651
|
+
* Live chat desk, READ-ONLY: the handoffs in a bucket, or one with its transcript (who said what —
|
|
5652
|
+
* customer, AI Agent, operator, platform). Needs `work:read` and the `live_chat` plan feature.
|
|
5653
|
+
*
|
|
5654
|
+
* Accepting, answering and returning a handoff stay in the console: those routes need a signed-in
|
|
5655
|
+
* PERSON to hold the conversation (`accepting needs a signed-in person`), and a token is not one.
|
|
5656
|
+
*/
|
|
5657
|
+
async function cmdHandoff(flags) {
|
|
5658
|
+
const t = resolveTarget(flags);
|
|
5659
|
+
const { url } = t;
|
|
5660
|
+
const base = `${url}/api/self/p/handoff`;
|
|
5661
|
+
const id = flags._[0];
|
|
5662
|
+
const asJson = flags.json === true;
|
|
5663
|
+
if (!id) {
|
|
5664
|
+
const q = new URLSearchParams();
|
|
5665
|
+
if (typeof flags.bucket === 'string')
|
|
5666
|
+
q.set('bucket', flags.bucket);
|
|
5667
|
+
if (flags.limit !== undefined)
|
|
5668
|
+
q.set('limit', String(flags.limit));
|
|
5669
|
+
if (flags.offset !== undefined)
|
|
5670
|
+
q.set('offset', String(flags.offset));
|
|
5671
|
+
const qs = q.toString() ? `?${q}` : '';
|
|
5672
|
+
if (!asJson)
|
|
5673
|
+
out.print(`→ Reading the Live chat desk from ${targetLabel(t)} …`);
|
|
5674
|
+
const { status, json } = await apiGet(`${base}${qs}`, t);
|
|
5675
|
+
if (status !== 200)
|
|
5676
|
+
die(`could not read the Live chat desk (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
5677
|
+
if (asJson) {
|
|
5678
|
+
out.result(json);
|
|
5679
|
+
return;
|
|
5680
|
+
}
|
|
5681
|
+
if (json?.has_data === false) {
|
|
5682
|
+
out.print(`No Live chat on this project — ${json?.reason ?? 'the pack works no handoff'} (craft-escalation, the Handoff recipe).`);
|
|
5683
|
+
return;
|
|
5684
|
+
}
|
|
5685
|
+
const rows = (json?.rows ?? []);
|
|
5686
|
+
if (rows.length === 0) {
|
|
5687
|
+
out.print('No handoffs in this view (--bucket waiting|mine|all|ended).');
|
|
5688
|
+
return;
|
|
5689
|
+
}
|
|
5690
|
+
for (const r of rows) {
|
|
5691
|
+
const who = r.contact?.display_name ?? r.contact?.channel_contact_handle ?? '—';
|
|
5692
|
+
out.print(` #${String(r.handoff_number).padEnd(5)} ${r.stage.padEnd(10)} ${String(r.channel ?? '—').padEnd(9)} ${(pickLabel(r.queue?.name) ?? r.queue_key ?? '—').padEnd(18)} ${who} ${r.id}`);
|
|
5693
|
+
}
|
|
5694
|
+
out.print('\n`octwin handoff <id>` shows one with its transcript.');
|
|
5695
|
+
return;
|
|
5696
|
+
}
|
|
5697
|
+
if (!asJson)
|
|
5698
|
+
out.print(`→ Reading handoff ${id} from ${targetLabel(t)} …`);
|
|
5699
|
+
const { status, json } = await apiGet(`${base}/${encodeURIComponent(id)}`, t);
|
|
5700
|
+
if (status !== 200)
|
|
5701
|
+
die(`could not read handoff ${id} (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
5702
|
+
if (asJson) {
|
|
5703
|
+
out.result(json);
|
|
5704
|
+
return;
|
|
5705
|
+
}
|
|
5706
|
+
const h = json?.handoff ?? {};
|
|
5707
|
+
out.print(`Handoff #${h.handoff_number ?? '?'} — ${h.stage ?? '?'}${h.queue_key ? ` · queue ${h.queue_key}` : ''}${h.reason ? ` · ${h.reason}` : ''}`);
|
|
5708
|
+
if (h.summary)
|
|
5709
|
+
out.print(` ${h.summary}`);
|
|
5710
|
+
for (const line of (json?.transcript ?? [])) {
|
|
5711
|
+
out.print(` ${String(line.ts).slice(11, 16)} ${line.role === 'operator' && line.author ? `operator: ${line.author}` : line.role} ${line.body}`);
|
|
5712
|
+
}
|
|
5713
|
+
if (json?.has_more)
|
|
5714
|
+
out.print(' … earlier lines in the console (Live chat).');
|
|
5715
|
+
}
|
|
5716
|
+
/**
|
|
5717
|
+
* `octwin surveys [<key>] [--limit n] [--offset n] [--json]` — the declared surveys with their
|
|
5718
|
+
* standing (responses, the headline score under its model), or one survey: its questions, the
|
|
5719
|
+
* rollup and the recent responses. The same reads as the console's Surveys page. Needs `records:read`.
|
|
5720
|
+
*/
|
|
5721
|
+
async function cmdSurveys(flags) {
|
|
5722
|
+
const t = resolveTarget(flags);
|
|
5723
|
+
const { url } = t;
|
|
5724
|
+
const base = `${url}/api/self/p/surveys`;
|
|
5725
|
+
const key = flags._[0];
|
|
5726
|
+
const asJson = flags.json === true;
|
|
5727
|
+
const score = (s) => s?.value == null ? '—' : `${s.value}${s.unit === 'pct' ? '%' : ''} (${s.model})`;
|
|
5728
|
+
if (!key) {
|
|
5729
|
+
if (!asJson)
|
|
5730
|
+
out.print(`→ Reading surveys from ${targetLabel(t)} …`);
|
|
5731
|
+
const { status, json } = await apiGet(`${base}`, t);
|
|
5732
|
+
if (status !== 200)
|
|
5733
|
+
die(`could not read surveys (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
5734
|
+
if (asJson) {
|
|
5735
|
+
out.result(json);
|
|
5736
|
+
return;
|
|
5737
|
+
}
|
|
5738
|
+
const rows = (json?.rows ?? []);
|
|
5739
|
+
if (rows.length === 0) {
|
|
5740
|
+
out.print('No surveys — a pack declares them in surveys.yaml (craft-surveys).');
|
|
5741
|
+
return;
|
|
5742
|
+
}
|
|
5743
|
+
for (const r of rows)
|
|
5744
|
+
out.print(` ${r.key.padEnd(22)} ${(pickLabel(r.label) ?? r.key).padEnd(30)} responses ${String(r.responses ?? '—').padStart(5)} score ${score(r.score)}`);
|
|
5745
|
+
out.print('\n`octwin surveys <key>` shows one — its questions, rollup and recent answers.');
|
|
5746
|
+
return;
|
|
5747
|
+
}
|
|
5748
|
+
const q = new URLSearchParams();
|
|
5749
|
+
if (flags.limit !== undefined)
|
|
5750
|
+
q.set('limit', String(flags.limit));
|
|
5751
|
+
if (flags.offset !== undefined)
|
|
5752
|
+
q.set('offset', String(flags.offset));
|
|
5753
|
+
const qs = q.toString() ? `?${q}` : '';
|
|
5754
|
+
if (!asJson)
|
|
5755
|
+
out.print(`→ Reading survey '${key}' from ${targetLabel(t)} …`);
|
|
5756
|
+
const { status, json } = await apiGet(`${base}/${encodeURIComponent(key)}${qs}`, t);
|
|
5757
|
+
if (status !== 200)
|
|
5758
|
+
die(`could not read survey '${key}' (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
5759
|
+
if (asJson) {
|
|
5760
|
+
out.result(json);
|
|
5761
|
+
return;
|
|
5762
|
+
}
|
|
5763
|
+
const sv = json?.survey ?? {};
|
|
5764
|
+
const a = json?.analytics;
|
|
5765
|
+
out.print(`${pickLabel(sv.label) ?? key}${sv.subject ? ` about: ${sv.subject}` : ''}`);
|
|
5766
|
+
for (const qq of sv.questions ?? [])
|
|
5767
|
+
out.print(` • ${qq.key} (${qq.type}${qq.required ? ', required' : ''})`);
|
|
5768
|
+
out.print(a ? ` responses ${a.responses} · scored ${a.scored} · score ${score(a.score)}` : ' rollup unavailable — the pack has not loaded on this project');
|
|
5769
|
+
const rows = (json?.responses?.rows ?? []);
|
|
5770
|
+
for (const r of rows)
|
|
5771
|
+
out.print(` ${String(r.created_at).slice(0, 16)} score ${r.score ?? '—'} ${JSON.stringify(r.answers)}`);
|
|
5772
|
+
}
|
|
5773
|
+
/**
|
|
5774
|
+
* `--range <days>` and `--channel <whatsapp|web>` as the route's own `?range=` / `?channel=` — the
|
|
5775
|
+
* console's two controls. Until 2026-10-11 the CLI read only `--detail`, so `--channel web` printed
|
|
5776
|
+
* the all-channel band as if it were the web's (an unknown flag was ignored then, not refused).
|
|
5777
|
+
*/
|
|
5778
|
+
function performanceQuery(flags) {
|
|
5779
|
+
const p = new URLSearchParams();
|
|
5780
|
+
if (flags.range !== undefined) {
|
|
5781
|
+
const days = Number(flags.range);
|
|
5782
|
+
if (!Number.isInteger(days) || days <= 0)
|
|
5783
|
+
die('--range takes a whole number of days, e.g. --range 30');
|
|
5784
|
+
p.set('range', String(days));
|
|
5785
|
+
}
|
|
5786
|
+
if (flags.channel !== undefined) {
|
|
5787
|
+
if (typeof flags.channel !== 'string')
|
|
5788
|
+
die('--channel takes a channel name, e.g. --channel whatsapp');
|
|
5789
|
+
p.set('channel', flags.channel);
|
|
5790
|
+
}
|
|
5791
|
+
const s = p.toString();
|
|
5792
|
+
return s ? `?${s}` : '';
|
|
5793
|
+
}
|
|
5794
|
+
/**
|
|
5795
|
+
* `octwin performance [--detail] [--range <days>] [--channel <ch>] [--json]` — the indicators the
|
|
5796
|
+
* project's own declarations produce: value, conversion, duration, per journey.
|
|
5339
5797
|
*
|
|
5340
5798
|
* Needs `records:read` — **not** a `performance:*` scope, which does not exist.
|
|
5341
5799
|
* That means the Read-only token preset already reaches this.
|
|
@@ -5351,9 +5809,10 @@ async function cmdPerformance(flags) {
|
|
|
5351
5809
|
// Two explicit calls rather than `apiGet(detail ? … : base)`: the route guard's
|
|
5352
5810
|
// extractor only reads a template literal in the FIRST argument position, so a
|
|
5353
5811
|
// ternary hides both URLs from it.
|
|
5812
|
+
const qs = performanceQuery(flags);
|
|
5354
5813
|
const { status, json } = detail
|
|
5355
|
-
? await apiGet(`${base}/detail`, t)
|
|
5356
|
-
: await apiGet(`${base}`, t);
|
|
5814
|
+
? await apiGet(`${base}/detail${qs}`, t)
|
|
5815
|
+
: await apiGet(`${base}${qs}`, t);
|
|
5357
5816
|
if (status !== 200)
|
|
5358
5817
|
die(`could not read performance (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
5359
5818
|
if (asJson) {
|
|
@@ -5466,427 +5925,461 @@ async function cmdUsage(flags) {
|
|
|
5466
5925
|
out.print('\nThis is MODEL spend. WhatsApp/Meta message billing is operator-only — not reachable by an API token.');
|
|
5467
5926
|
}
|
|
5468
5927
|
function help() {
|
|
5469
|
-
out.print(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
|
|
5470
|
-
|
|
5471
|
-
octwin --version # print the CLI version (+ any upgrade notice)
|
|
5472
|
-
octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
5473
|
-
octwin
|
|
5474
|
-
octwin
|
|
5475
|
-
octwin login --url <platformUrl>
|
|
5476
|
-
octwin
|
|
5477
|
-
octwin
|
|
5478
|
-
octwin
|
|
5479
|
-
|
|
5480
|
-
octwin
|
|
5481
|
-
octwin
|
|
5482
|
-
|
|
5483
|
-
octwin
|
|
5484
|
-
|
|
5485
|
-
octwin
|
|
5486
|
-
octwin
|
|
5487
|
-
|
|
5488
|
-
octwin
|
|
5489
|
-
octwin
|
|
5490
|
-
octwin
|
|
5491
|
-
octwin
|
|
5492
|
-
octwin
|
|
5493
|
-
octwin
|
|
5494
|
-
octwin
|
|
5495
|
-
octwin
|
|
5496
|
-
octwin
|
|
5497
|
-
octwin
|
|
5498
|
-
octwin
|
|
5499
|
-
octwin
|
|
5500
|
-
octwin
|
|
5501
|
-
octwin
|
|
5502
|
-
octwin
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
octwin
|
|
5506
|
-
octwin
|
|
5507
|
-
octwin
|
|
5508
|
-
octwin
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
octwin
|
|
5512
|
-
octwin
|
|
5513
|
-
octwin
|
|
5514
|
-
octwin
|
|
5515
|
-
octwin
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5928
|
+
out.print(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
|
|
5929
|
+
|
|
5930
|
+
octwin --version # print the CLI version (+ any upgrade notice)
|
|
5931
|
+
octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
5932
|
+
octwin recipe [<module>] [--dir .] [--dry-run] # a KB recipe's starter files into this pack (no module = the list)
|
|
5933
|
+
octwin validate [--dir .] [--remote | --offline] [--require-kb] [--strict-primitives] # the platform's FULL check by default — no login needed
|
|
5934
|
+
octwin login --url <platformUrl> # approve in a browser; the CLI collects the token
|
|
5935
|
+
octwin login --url <platformUrl> --token oct_… # or paste a deploy token from the console
|
|
5936
|
+
octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
|
|
5937
|
+
octwin projects [--archived] [--json] # the --project slugs this token can name
|
|
5938
|
+
octwin projects create "<name>" [--slug s] [--pack p] | rm <slug> [--yes] # a disposable project (needs projects:write)
|
|
5939
|
+
octwin seed [--reset] [--pack <packId>] # apply the pack's demo data without redeploying
|
|
5940
|
+
octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed] [--force]
|
|
5941
|
+
[--request-listing | --withdraw-listing] # public marketplace — opt-in, see: octwin help deploy
|
|
5942
|
+
octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
|
|
5943
|
+
octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
|
|
5944
|
+
octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
|
|
5945
|
+
octwin work [recordId] [--queues] [--unrouted] [--json] # inspect the work inbox (worked records) + timelines
|
|
5946
|
+
# --queues: per-queue open counts + an UNROUTED warning · --unrouted: only the items in no queue
|
|
5947
|
+
octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
|
|
5948
|
+
octwin chat "message" [--as <handle>] [--tap <tap-id>] [--press <label>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
|
|
5949
|
+
octwin media generate "<prompt>" [--out <file.png>] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
|
|
5950
|
+
octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
|
|
5951
|
+
octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
|
|
5952
|
+
octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
|
|
5953
|
+
octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
|
|
5954
|
+
octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
|
|
5955
|
+
octwin automation [campaigns] [--json] # the jobs your declarations produced + health, last result each
|
|
5956
|
+
octwin integrations [--json] # declared connections BESIDE what is configured (the silent-never-fires check)
|
|
5957
|
+
octwin integrations deliveries [<id>] | events # the outbound delivery log / inbound events
|
|
5958
|
+
octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]
|
|
5959
|
+
octwin performance [--detail] [--range <days>] [--channel <ch>] [--json] # the project's business indicators (value/conversion/duration)
|
|
5960
|
+
octwin usage [--json] # model calls, tokens and cost (project if pinned, else workspace)
|
|
5961
|
+
octwin charts [<key>] [--from d] [--to d] [--json] # the pack's own charts: — the list, or one chart's figure
|
|
5962
|
+
octwin handoff [<id>] [--bucket waiting|mine|all|ended] [--limit n] [--offset n] [--json] # the Live chat desk, read-only
|
|
5963
|
+
octwin surveys [<key>] [--limit n] [--offset n] [--json] # surveys with their score; one survey's questions, rollup, answers
|
|
5964
|
+
octwin platform-kb [pull] [--if-stale|--check] [--dir .] [--url <url>] # no token needed
|
|
5965
|
+
octwin test [--dir .] [--require-kb] [--strict-primitives] # = validate --remote (the full platform check)
|
|
5966
|
+
octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
|
|
5967
|
+
octwin memos [--all] [--json] # read the platform's replies + notices (a reply to your feedback lands here)
|
|
5968
|
+
|
|
5969
|
+
Writes — exercise the state your pack creates (each needs the matching :write scope):
|
|
5970
|
+
octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…", archive|restore <id>
|
|
5971
|
+
octwin records tasks | task complete <taskId> [--outcome done|cancelled]
|
|
5972
|
+
octwin work assign <id> --to user:<uuid>|none | note <id> "…" | stage <id> --to <stage>
|
|
5973
|
+
octwin work decide <id> --action <a> [--param k=v] [--dry-run] # --dry-run previews, commits nothing
|
|
5974
|
+
octwin orders transition <ref> --to <status> | refund <ref> --force
|
|
5975
|
+
octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
|
|
5976
|
+
octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
|
|
5977
|
+
octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
|
|
5978
|
+
octwin automation run <jobId> | pause <jobId> | resume <jobId> | send <campaignId>
|
|
5979
|
+
octwin integrations test <key> # a LIVE call to the connection's health: operation
|
|
5980
|
+
octwin integrations retry|cancel|send-now <deliveryId>
|
|
5981
|
+
(octwin integrations preflight <key> needs only integrations:read — it makes no call)
|
|
5982
|
+
|
|
5983
|
+
Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
|
|
5984
|
+
\`octwin chat --as <h>\` calls continue the same conversation; press a rendered
|
|
5985
|
+
button/row with \`--tap "<tap-id>"\` (chat prints every tap id), or by its label with
|
|
5986
|
+
\`--press "<label>"\` — the way through a form or checkout, whose ids change.
|
|
5987
|
+
Signing in: \`octwin login --url <u>\` prints a link + code to approve in a browser. To pick your own
|
|
5988
|
+
scopes instead, mint a token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
|
|
5989
|
+
octwin platform-kb pull → writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
|
|
5990
|
+
Config (deploy): flags > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login (\`octwin login\` sets the default target).
|
|
5526
5991
|
Per-command usage: octwin <command> --help`);
|
|
5527
5992
|
}
|
|
5528
5993
|
/** Per-subcommand usage — printed for `octwin <cmd> --help|-h` BEFORE any
|
|
5529
5994
|
* network/auth work (a --help that 401s is worse than no help at all). */
|
|
5530
5995
|
const COMMAND_HELP = {
|
|
5531
|
-
init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
5996
|
+
init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
5532
5997
|
Scaffold a pure-YAML starter pack into <dir>.`,
|
|
5533
|
-
validate: `octwin validate [--dir .] [--remote | --offline] [--require-kb] [--strict-primitives]
|
|
5534
|
-
First the fast OFFLINE checks (structure, YAML, and the checks driven by the
|
|
5535
|
-
pulled capability reference — those SKIP when the reference is missing, and
|
|
5536
|
-
--require-kb turns the skip into a failure for CI). Then, BY DEFAULT, the
|
|
5537
|
-
platform's FULL validation — the same check deploy runs, all errors at once.
|
|
5538
|
-
|
|
5539
|
-
The full check needs NO login: without a token + --project it goes to the
|
|
5540
|
-
platform's public validate route. With a token AND --project (or PACK_PROJECT)
|
|
5541
|
-
it goes through your workspace, which also confirms that project exists.
|
|
5542
|
-
|
|
5543
|
-
When the platform cannot be reached (no url, offline, rate-limited) the run
|
|
5544
|
-
ends with "only the OFFLINE check ran — a PARTIAL check" and exits 0.
|
|
5545
|
-
--remote REQUIRES the full check: not reaching it is an error. --offline runs
|
|
5546
|
-
only the partial subset. --strict-primitives additionally type-checks LITERAL
|
|
5547
|
-
args: values against each primitive's declared input schema; expression
|
|
5998
|
+
validate: `octwin validate [--dir .] [--remote | --offline] [--require-kb] [--strict-primitives]
|
|
5999
|
+
First the fast OFFLINE checks (structure, YAML, and the checks driven by the
|
|
6000
|
+
pulled capability reference — those SKIP when the reference is missing, and
|
|
6001
|
+
--require-kb turns the skip into a failure for CI). Then, BY DEFAULT, the
|
|
6002
|
+
platform's FULL validation — the same check deploy runs, all errors at once.
|
|
6003
|
+
|
|
6004
|
+
The full check needs NO login: without a token + --project it goes to the
|
|
6005
|
+
platform's public validate route. With a token AND --project (or PACK_PROJECT)
|
|
6006
|
+
it goes through your workspace, which also confirms that project exists.
|
|
6007
|
+
|
|
6008
|
+
When the platform cannot be reached (no url, offline, rate-limited) the run
|
|
6009
|
+
ends with "only the OFFLINE check ran — a PARTIAL check" and exits 0.
|
|
6010
|
+
--remote REQUIRES the full check: not reaching it is an error. --offline runs
|
|
6011
|
+
only the partial subset. --strict-primitives additionally type-checks LITERAL
|
|
6012
|
+
args: values against each primitive's declared input schema; expression
|
|
5548
6013
|
strings ('$found.id', '{$t(…)}') are always exempt.`,
|
|
5549
|
-
login: `octwin login --url <platformUrl> [--token oct_…] [--wait <seconds>] [--json]
|
|
5550
|
-
Sign in to a platform and make that url the DEFAULT deploy target for every
|
|
5551
|
-
later command.
|
|
5552
|
-
|
|
5553
|
-
WITH --token: saves a token you minted yourself (console → Settings → API
|
|
5554
|
-
tokens). Unchanged, and still the right answer for CI — there, skip login and
|
|
5555
|
-
set PACK_PLATFORM_URL + PACK_TOKEN instead.
|
|
5556
|
-
|
|
5557
|
-
WITHOUT --token: prints a link to approve in a browser. A workspace admin opens
|
|
5558
|
-
it, checks which machine is asking, and clicks Approve; the CLI then collects a
|
|
5559
|
-
30-day token by itself, so no token is ever pasted between people. The code is
|
|
5560
|
-
printed too — it is what the page shows, so you can tell it is your own request.
|
|
5561
|
-
Approve only a link you opened because YOU ran this command: a link someone
|
|
5562
|
-
sends you would connect THEIR machine to your workspace.
|
|
5563
|
-
|
|
5564
|
-
The wait is bounded (90s by default) and RESUMABLE: if nobody has approved yet
|
|
5565
|
-
it exits 75 and keeps the request, so running \`octwin login\` again picks up the
|
|
6014
|
+
login: `octwin login --url <platformUrl> [--token oct_…] [--wait <seconds>] [--json]
|
|
6015
|
+
Sign in to a platform and make that url the DEFAULT deploy target for every
|
|
6016
|
+
later command.
|
|
6017
|
+
|
|
6018
|
+
WITH --token: saves a token you minted yourself (console → Settings → API
|
|
6019
|
+
tokens). Unchanged, and still the right answer for CI — there, skip login and
|
|
6020
|
+
set PACK_PLATFORM_URL + PACK_TOKEN instead.
|
|
6021
|
+
|
|
6022
|
+
WITHOUT --token: prints a link to approve in a browser. A workspace admin opens
|
|
6023
|
+
it, checks which machine is asking, and clicks Approve; the CLI then collects a
|
|
6024
|
+
30-day token by itself, so no token is ever pasted between people. The code is
|
|
6025
|
+
printed too — it is what the page shows, so you can tell it is your own request.
|
|
6026
|
+
Approve only a link you opened because YOU ran this command: a link someone
|
|
6027
|
+
sends you would connect THEIR machine to your workspace.
|
|
6028
|
+
|
|
6029
|
+
The wait is bounded (90s by default) and RESUMABLE: if nobody has approved yet
|
|
6030
|
+
it exits 75 and keeps the request, so running \`octwin login\` again picks up the
|
|
5566
6031
|
same code. --wait 0 prints the code and returns at once.`,
|
|
5567
|
-
whoami: `octwin whoami [--url <url>] [--tenant <slug>]
|
|
6032
|
+
whoami: `octwin whoami [--url <url>] [--tenant <slug>]
|
|
5568
6033
|
Verify the resolved token authenticates against the tenant.`,
|
|
5569
|
-
projects: `octwin projects [--archived] [--json]
|
|
5570
|
-
List the workspace's projects — the slugs every --project flag takes, with the
|
|
5571
|
-
plan's project cap. --archived includes archived ones. A pack:deploy token
|
|
5572
|
-
reaches this (it names a project in every other command).
|
|
5573
|
-
|
|
5574
|
-
octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
|
|
5575
|
-
Create a project. The URL slug is derived from the name unless --slug pins one.
|
|
5576
|
-
--pack installs an ALREADY-published pack; the usual next step is instead
|
|
5577
|
-
\`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
|
|
5578
|
-
|
|
5579
|
-
octwin projects rm <slug> [--yes]
|
|
5580
|
-
HARD delete — the project and everything cascading from it (conversations,
|
|
5581
|
-
contacts, records, installs). No undo, and not the same as archiving.
|
|
5582
|
-
WITHOUT --yes it only previews what would be destroyed, so the dry run is the
|
|
5583
|
-
default. Together these make a disposable end-to-end environment:
|
|
5584
|
-
octwin projects create "Scratch" && octwin deploy --project scratch --seed
|
|
5585
|
-
octwin chat "hi" --project scratch
|
|
5586
|
-
octwin projects rm scratch --yes
|
|
6034
|
+
projects: `octwin projects [--archived] [--json]
|
|
6035
|
+
List the workspace's projects — the slugs every --project flag takes, with the
|
|
6036
|
+
plan's project cap. --archived includes archived ones. A pack:deploy token
|
|
6037
|
+
reaches this (it names a project in every other command).
|
|
6038
|
+
|
|
6039
|
+
octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
|
|
6040
|
+
Create a project. The URL slug is derived from the name unless --slug pins one.
|
|
6041
|
+
--pack installs an ALREADY-published pack; the usual next step is instead
|
|
6042
|
+
\`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
|
|
6043
|
+
|
|
6044
|
+
octwin projects rm <slug> [--yes]
|
|
6045
|
+
HARD delete — the project and everything cascading from it (conversations,
|
|
6046
|
+
contacts, records, installs). No undo, and not the same as archiving.
|
|
6047
|
+
WITHOUT --yes it only previews what would be destroyed, so the dry run is the
|
|
6048
|
+
default. Together these make a disposable end-to-end environment:
|
|
6049
|
+
octwin projects create "Scratch" && octwin deploy --project scratch --seed
|
|
6050
|
+
octwin chat "hi" --project scratch
|
|
6051
|
+
octwin projects rm scratch --yes
|
|
5587
6052
|
Both verbs need the \`projects:write\` scope — a pack:deploy token does NOT confer it.`,
|
|
5588
|
-
deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
5589
|
-
[--request-listing | --withdraw-listing]
|
|
5590
|
-
Upload the pack bundle, validate server-side, install onto the project.
|
|
5591
|
-
--seed additionally applies the pack's demo seed (streams progress).
|
|
5592
|
-
|
|
5593
|
-
A
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
6053
|
+
deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed] [--force]
|
|
6054
|
+
[--request-listing | --withdraw-listing]
|
|
6055
|
+
Upload the pack bundle, validate server-side, install onto the project.
|
|
6056
|
+
--seed additionally applies the pack's demo seed (streams progress).
|
|
6057
|
+
|
|
6058
|
+
A deploy sends what this copy was made from (${BASE_FILE}, written by every pull and
|
|
6059
|
+
deploy). When someone published since — Tavi in the console, another deploy, a repo
|
|
6060
|
+
import — it is refused and NOTHING changes: run 'octwin pull <packId>', which updates
|
|
6061
|
+
this folder (it stops if you changed a file since, so you can save your edit first),
|
|
6062
|
+
then deploy again. A version lower than the live one is refused the same way.
|
|
6063
|
+
--force deploy anyway, replacing the newer or higher live version.
|
|
6064
|
+
|
|
6065
|
+
A plain deploy says NOTHING about the public marketplace — it is a test loop, so it
|
|
6066
|
+
neither asks for a listing nor gives one up. The marketplace flags are opt-in:
|
|
6067
|
+
|
|
6068
|
+
--request-listing ask an operator to review this pack for the public marketplace
|
|
6069
|
+
(the pre-signup storefront at /packs). Requires 'public: true'
|
|
6070
|
+
under 'listing:' in manifest.yaml — the manifest states that the
|
|
6071
|
+
pack is a product, the flag is you choosing to ask.
|
|
6072
|
+
--withdraw-listing retract the request, including an approved listing.
|
|
6073
|
+
|
|
6074
|
+
An approval covers the CONTENT it was made against, so a later deploy that changes the
|
|
5603
6075
|
pack returns it to the review queue on its own — no flag needed, and the CLI says so.`,
|
|
5604
|
-
seed: `octwin seed [--pack <packId>] [--reset]
|
|
5605
|
-
Apply the pack's demo/reference data to the project it is installed on, without
|
|
5606
|
-
redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
|
|
5607
|
-
and the demo operator topology. Reports what each kind produced.
|
|
5608
|
-
Idempotent and safe to re-run — each demo row is found again by its handle (its
|
|
5609
|
-
\`as:\`, natural key or title) and updated, and existing media is REUSED rather
|
|
5610
|
-
than regenerated, so a second pass costs nothing. Demo rows are DATA: no \`on:\`
|
|
5611
|
-
reaction fires, and a row takes its \`stage:\` as written.
|
|
5612
|
-
--reset archive every row the pack's demo loaded, then load it again. Needed once
|
|
5613
|
-
for demo rows seeded before 2026-10-05 (the seed says so), and a clean
|
|
5614
|
-
slate whenever you want one. Records your customers made are not touched.
|
|
6076
|
+
seed: `octwin seed [--pack <packId>] [--reset]
|
|
6077
|
+
Apply the pack's demo/reference data to the project it is installed on, without
|
|
6078
|
+
redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
|
|
6079
|
+
and the demo operator topology. Reports what each kind produced.
|
|
6080
|
+
Idempotent and safe to re-run — each demo row is found again by its handle (its
|
|
6081
|
+
\`as:\`, natural key or title) and updated, and existing media is REUSED rather
|
|
6082
|
+
than regenerated, so a second pass costs nothing. Demo rows are DATA: no \`on:\`
|
|
6083
|
+
reaction fires, and a row takes its \`stage:\` as written.
|
|
6084
|
+
--reset archive every row the pack's demo loaded, then load it again. Needed once
|
|
6085
|
+
for demo rows seeded before 2026-10-05 (the seed says so), and a clean
|
|
6086
|
+
slate whenever you want one. Records your customers made are not touched.
|
|
5615
6087
|
--pack is only needed when a project somehow runs more than one.`,
|
|
5616
|
-
status: `octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
|
|
5617
|
-
Show installed vs live version + the flow list for this pack
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
6088
|
+
status: `octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
|
|
6089
|
+
Show installed vs live version + the flow list for this pack, and whether your copy
|
|
6090
|
+
(${BASE_FILE}) is still the live content — if not, pull before you deploy.
|
|
6091
|
+
The pack id is read from manifest.yaml and QUALIFIED with your workspace slug
|
|
6092
|
+
(a manifest declares a bare name; the owner is attached when you publish). Pass
|
|
6093
|
+
<packId> explicitly to skip that lookup — \`octwin agents\` and \`octwin projects\`
|
|
5621
6094
|
both print the qualified form.`,
|
|
5622
|
-
records: `octwin records [entity] [id] [--limit 50] [--offset n] [--json]
|
|
5623
|
-
Inspect the pack's XRM data. No args = list entities. Worked records (cases,
|
|
5624
|
-
tickets, anything routed to a queue) read best through \`octwin work\`.
|
|
5625
|
-
|
|
5626
|
-
SEARCH:
|
|
5627
|
-
octwin records <entity> --search "<words>" # ranked, best match first, each row with its score (0–1)
|
|
5628
|
-
Works for an entity that declares \`search:\` in xrm.yaml (fuzzy, plus semantic when declared) —
|
|
5629
|
-
the same search the console's search box runs. Any other entity answers "not searchable".
|
|
5630
|
-
|
|
5631
|
-
WRITES (need \`records:write\`; every one is re-checked by RBAC on the record):
|
|
5632
|
-
octwin records create <entity> --set field=value [--set …] [--stage s] [--contact <id>]
|
|
5633
|
-
octwin records patch <recordId> --entity <entity> --set field=value
|
|
5634
|
-
octwin records stage <recordId> --to <stage> [--note "..."]
|
|
5635
|
-
octwin records note <recordId> "the note text"
|
|
5636
|
-
octwin records archive <recordId> # out of every list and count (test data too)
|
|
5637
|
-
octwin records restore <recordId> # undo an archive
|
|
5638
|
-
octwin records tasks # open follow-up tasks (\`tasks\` plan feature)
|
|
5639
|
-
octwin records task complete <taskId> [--outcome done|cancelled] [--note "..."]
|
|
5640
|
-
|
|
5641
|
-
There is NO delete — on the platform or here. \`archive\` is how a record leaves use:
|
|
5642
|
-
it fires the entity's \`on.archived\` reactions, exactly like the console's Archive,
|
|
5643
|
-
and \`restore\` brings it back.
|
|
5644
|
-
|
|
5645
|
-
--set coerces JSON scalars: \`--set rating=4.5\` sends a number, \`--set x=null\`
|
|
5646
|
-
sends null. Use --fields-json '{"a":{"b":1}}' for anything nested.
|
|
5647
|
-
\`patch\` needs --entity even though it has an id: the route resolves the field
|
|
5648
|
-
validator from it. A leading \`create/patch/stage/note/archive/restore/tasks/task\` is
|
|
6095
|
+
records: `octwin records [entity] [id] [--limit 50] [--offset n] [--json]
|
|
6096
|
+
Inspect the pack's XRM data. No args = list entities. Worked records (cases,
|
|
6097
|
+
tickets, anything routed to a queue) read best through \`octwin work\`.
|
|
6098
|
+
|
|
6099
|
+
SEARCH:
|
|
6100
|
+
octwin records <entity> --search "<words>" # ranked, best match first, each row with its score (0–1)
|
|
6101
|
+
Works for an entity that declares \`search:\` in xrm.yaml (fuzzy, plus semantic when declared) —
|
|
6102
|
+
the same search the console's search box runs. Any other entity answers "not searchable".
|
|
6103
|
+
|
|
6104
|
+
WRITES (need \`records:write\`; every one is re-checked by RBAC on the record):
|
|
6105
|
+
octwin records create <entity> --set field=value [--set …] [--stage s] [--contact <id>]
|
|
6106
|
+
octwin records patch <recordId> --entity <entity> --set field=value
|
|
6107
|
+
octwin records stage <recordId> --to <stage> [--note "..."]
|
|
6108
|
+
octwin records note <recordId> "the note text"
|
|
6109
|
+
octwin records archive <recordId> # out of every list and count (test data too)
|
|
6110
|
+
octwin records restore <recordId> # undo an archive
|
|
6111
|
+
octwin records tasks # open follow-up tasks (\`tasks\` plan feature)
|
|
6112
|
+
octwin records task complete <taskId> [--outcome done|cancelled] [--note "..."]
|
|
6113
|
+
|
|
6114
|
+
There is NO delete — on the platform or here. \`archive\` is how a record leaves use:
|
|
6115
|
+
it fires the entity's \`on.archived\` reactions, exactly like the console's Archive,
|
|
6116
|
+
and \`restore\` brings it back.
|
|
6117
|
+
|
|
6118
|
+
--set coerces JSON scalars: \`--set rating=4.5\` sends a number, \`--set x=null\`
|
|
6119
|
+
sends null. Use --fields-json '{"a":{"b":1}}' for anything nested.
|
|
6120
|
+
\`patch\` needs --entity even though it has an id: the route resolves the field
|
|
6121
|
+
validator from it. A leading \`create/patch/stage/note/archive/restore/tasks/task\` is
|
|
5649
6122
|
read as a VERB — to list an entity actually named one of those, use \`--entity <name>\`.`,
|
|
5650
|
-
work: `octwin work [recordId] [--queues] [--unrouted] [--limit 50] [--offset n] [--json]
|
|
5651
|
-
Inspect the work inbox — every entity the pack declares worked (cases, orders
|
|
5652
|
-
needing review, applications, …): the inbox, one item + its timeline
|
|
5653
|
-
(+ applicable actions), or --queues for queue keys + open counts.
|
|
5654
|
-
|
|
5655
|
-
WRITES (need \`work:write\`; \`stage\` needs \`records:write\`):
|
|
5656
|
-
octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none
|
|
5657
|
-
octwin work note <recordId> "the note text"
|
|
5658
|
-
octwin work stage <recordId> --to <stage> [--note "..."]
|
|
5659
|
-
octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]
|
|
5660
|
-
|
|
5661
|
-
\`decide\` applies one of the entity's declared operator actions — \`octwin work <id>\`
|
|
5662
|
-
lists them with their params. --dry-run previews the customer-facing copy and the
|
|
5663
|
-
resulting stage WITHOUT committing (that route needs only \`work:read\`).
|
|
6123
|
+
work: `octwin work [recordId] [--queues] [--unrouted] [--limit 50] [--offset n] [--json]
|
|
6124
|
+
Inspect the work inbox — every entity the pack declares worked (cases, orders
|
|
6125
|
+
needing review, applications, …): the inbox, one item + its timeline
|
|
6126
|
+
(+ applicable actions), or --queues for queue keys + open counts.
|
|
6127
|
+
|
|
6128
|
+
WRITES (need \`work:write\`; \`stage\` needs \`records:write\`):
|
|
6129
|
+
octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none
|
|
6130
|
+
octwin work note <recordId> "the note text"
|
|
6131
|
+
octwin work stage <recordId> --to <stage> [--note "..."]
|
|
6132
|
+
octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]
|
|
6133
|
+
|
|
6134
|
+
\`decide\` applies one of the entity's declared operator actions — \`octwin work <id>\`
|
|
6135
|
+
lists them with their params. --dry-run previews the customer-facing copy and the
|
|
6136
|
+
resulting stage WITHOUT committing (that route needs only \`work:read\`).
|
|
5664
6137
|
\`stage\` is the XRM records verb (one transition spelling platform-wide).`,
|
|
5665
|
-
logs: `octwin logs [conversationId] [--as <handle>] [--json]
|
|
5666
|
-
No id = recent conversations (handle, status, last activity; --as filters).
|
|
5667
|
-
With id = the full event timeline including what each turn rendered.
|
|
6138
|
+
logs: `octwin logs [conversationId] [--as <handle>] [--json]
|
|
6139
|
+
No id = recent conversations (handle, status, last activity; --as filters).
|
|
6140
|
+
With id = the full event timeline including what each turn rendered.
|
|
5668
6141
|
--json = raw events (verbatim payloads).`,
|
|
5669
|
-
pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
|
|
5670
|
-
Write a DEPLOYED pack's source back to disk — the inverse of deploy.
|
|
5671
|
-
A pack pushed with 'octwin deploy' lives on the platform as an artifact the
|
|
5672
|
-
runtime serves but nothing hands back, so its only source copy is the machine
|
|
5673
|
-
that pushed it. Pull it, fix it, redeploy it.
|
|
5674
|
-
Defaults to the version installed on the target project; --version overrides.
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
--
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
--
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
6142
|
+
pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
|
|
6143
|
+
Write a DEPLOYED pack's source back to disk — the inverse of deploy.
|
|
6144
|
+
A pack pushed with 'octwin deploy' lives on the platform as an artifact the
|
|
6145
|
+
runtime serves but nothing hands back, so its only source copy is the machine
|
|
6146
|
+
that pushed it. Pull it, fix it, redeploy it.
|
|
6147
|
+
Defaults to the version installed on the target project; --version overrides.
|
|
6148
|
+
Writes into the folder you are in when it is this pack, else ./<packId>; --dir overrides.
|
|
6149
|
+
Replaces only files that still match your last pull or deploy (${BASE_FILE}); a file
|
|
6150
|
+
you changed since stops it — save your edit first, or --force overwrites it.
|
|
6151
|
+
The pulled dir redeploys where it came from — the target is your saved login.
|
|
6152
|
+
You may pull a pack your workspace OWNS, or any first-party (Octwin) pack — those are
|
|
6153
|
+
free to copy as a starting point; an operator token pulls any.`,
|
|
6154
|
+
chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--press <label>] [--media <file|id>] [--json]
|
|
6155
|
+
octwin chat --script <file> [--as <handle>] [--json]
|
|
6156
|
+
Drive ONE turn through the dev web channel and print every render with its
|
|
6157
|
+
tap ids. Same --as handle = same conversation (multi-turn works).
|
|
6158
|
+
--tap presses a rendered button/list row instead of sending text.
|
|
6159
|
+
--press does the same BY ITS LABEL, in the reply the conversation is waiting on
|
|
6160
|
+
— for a form or checkout step, whose tap id carries a run id that changes.
|
|
6161
|
+
--media uploads a local file (or a media id from 'media generate --json') as
|
|
6162
|
+
an image/document/audio inbound — any "message" rides as its caption; feeds a
|
|
6163
|
+
running media-collect flow (e.g. activate-app).
|
|
6164
|
+
--json dumps the raw SSE envelopes for the turn.
|
|
6165
|
+
|
|
6166
|
+
Each reply line says who sent it. Plain \`← …\` is the AI Agent. \`← [notify] …\` is a
|
|
6167
|
+
reminder or notification (a \`schedule_notify\`, a reaction) — NOT the turn's answer,
|
|
6168
|
+
so the wait for the real reply goes on. \`← [operator: Name] …\` is a person in
|
|
6169
|
+
Live chat; \`← [platform] …\` is a platform line (a handoff joining or returning).
|
|
6170
|
+
After the last turn the stream stays open ${LATE_DRAIN_MS / 1000}s more, so a late message
|
|
6171
|
+
still prints (marked "after the last turn") instead of reaching a closed connection.
|
|
6172
|
+
|
|
6173
|
+
--script drives a WHOLE conversation from a file, ONE TURN PER LINE, in one
|
|
6174
|
+
process over one connection — waiting for each turn to settle before sending
|
|
6175
|
+
the next. Use this for any multi-step flow: chaining shell invocations races
|
|
6176
|
+
the agent loop, because a turn ends on a quiet gap that can arrive while the
|
|
6177
|
+
server is still working (the symptom is placeholder-filled fields or a second
|
|
6178
|
+
workflow run). Blank lines and # comments are skipped:
|
|
6179
|
+
|
|
6180
|
+
# book an appointment end to end
|
|
6181
|
+
احجز موعد
|
|
6182
|
+
tap:t:invoke:book-appointment:doctor_id=D1
|
|
6183
|
+
media:./licence.jpg | here is my licence
|
|
6184
|
+
press:Confirm
|
|
6185
|
+
|
|
6186
|
+
press:<label> taps the button of the reply just received whose label matches —
|
|
6187
|
+
exactly, or the same words without emoji or case, or the one label containing
|
|
6188
|
+
them. None, or several buttons, stops the script and lists the buttons. A form
|
|
6189
|
+
step's tap id carries its run id (t:resume:…:run_id=…), which exists only once
|
|
5714
6190
|
the reply arrives, so a script reaches past the first form step only by press.`,
|
|
5715
|
-
media: `octwin media generate "<prompt>" [--out <file.png>] [--json]
|
|
5716
|
-
AI-generate an image (needs a media:generate-scoped token), store it as a
|
|
5717
|
-
public asset, and print its MEDIA- handle + serve URL. --out downloads the
|
|
5718
|
-
bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
|
|
6191
|
+
media: `octwin media generate "<prompt>" [--out <file.png>] [--json]
|
|
6192
|
+
AI-generate an image (needs a media:generate-scoped token), store it as a
|
|
6193
|
+
public asset, and print its MEDIA- handle + serve URL. --out downloads the
|
|
6194
|
+
bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
|
|
5719
6195
|
bytes }. Pair with 'octwin chat --media' to drive media flows.`,
|
|
5720
|
-
agents: `octwin agents [packId::agentId] [--prompt] [--json]
|
|
5721
|
-
No args = the roster with each agent's EFFECTIVE model and which layer set it.
|
|
5722
|
-
With an agent = every governed setting (model / memory.last_messages /
|
|
5723
|
-
working_memory) plus the layer that won — an operator PLATFORM default can
|
|
5724
|
-
override what your manifest declares, and this is where you see that.
|
|
5725
|
-
--prompt = the exact system prompt the LLM sees for this project (pack
|
|
5726
|
-
instructions + platform protocol + any project overlay). Needs agents:read.
|
|
5727
|
-
The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
|
|
5728
|
-
|
|
5729
|
-
WRITES (need \`agents:write\`):
|
|
5730
|
-
octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
|
|
5731
|
-
[--enable-tool <toolId>] [--disable-tool <toolId>]
|
|
5732
|
-
|
|
5733
|
-
Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
|
|
5734
|
-
so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
|
|
6196
|
+
agents: `octwin agents [packId::agentId] [--prompt] [--json]
|
|
6197
|
+
No args = the roster with each agent's EFFECTIVE model and which layer set it.
|
|
6198
|
+
With an agent = every governed setting (model / memory.last_messages /
|
|
6199
|
+
working_memory) plus the layer that won — an operator PLATFORM default can
|
|
6200
|
+
override what your manifest declares, and this is where you see that.
|
|
6201
|
+
--prompt = the exact system prompt the LLM sees for this project (pack
|
|
6202
|
+
instructions + platform protocol + any project overlay). Needs agents:read.
|
|
6203
|
+
The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
|
|
6204
|
+
|
|
6205
|
+
WRITES (need \`agents:write\`):
|
|
6206
|
+
octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
|
|
6207
|
+
[--enable-tool <toolId>] [--disable-tool <toolId>]
|
|
6208
|
+
|
|
6209
|
+
Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
|
|
6210
|
+
so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
|
|
5735
6211
|
ids refuses --model with a 403 — the platform default governs there.`,
|
|
5736
|
-
orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--json]
|
|
5737
|
-
No args = the order list (#number, status/payment, total, contact). With a
|
|
5738
|
-
reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
|
|
5739
|
-
payment_ref, and the allowed status transitions. Needs orders:read + the
|
|
5740
|
-
\`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
WRITES (need \`orders:write\`):
|
|
5744
|
-
octwin orders transition <reference_id> --to <status>
|
|
5745
|
-
octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
|
|
5746
|
-
|
|
5747
|
-
Refund is irreversible and moves money, hence --force. The route answers 200 even
|
|
5748
|
-
when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
|
|
5749
|
-
on a refusal rather than reporting a refund that never happened. Only a payment in
|
|
6212
|
+
orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--offset n] [--json]
|
|
6213
|
+
No args = the order list (#number, status/payment, total, contact). With a
|
|
6214
|
+
reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
|
|
6215
|
+
payment_ref, and the allowed status transitions. Needs orders:read + the
|
|
6216
|
+
\`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned —
|
|
6217
|
+
\`none\` = no payment opened (no gateway), \`pending\` = checkout not confirmed yet.
|
|
6218
|
+
|
|
6219
|
+
WRITES (need \`orders:write\`):
|
|
6220
|
+
octwin orders transition <reference_id> --to <status>
|
|
6221
|
+
octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
|
|
6222
|
+
|
|
6223
|
+
Refund is irreversible and moves money, hence --force. The route answers 200 even
|
|
6224
|
+
when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
|
|
6225
|
+
on a refusal rather than reporting a refund that never happened. Only a payment in
|
|
5750
6226
|
\`captured\` state can be refunded; \`payment_status\` is never settable directly.`,
|
|
5751
|
-
analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--json]
|
|
5752
|
-
No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
|
|
5753
|
-
With an entity = stage-by-stage conversion (default --funnel) over the last 30
|
|
5754
|
-
days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
|
|
6227
|
+
analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--limit n] [--offset n] [--json]
|
|
6228
|
+
No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
|
|
6229
|
+
With an entity = stage-by-stage conversion (default --funnel) over the last 30
|
|
6230
|
+
days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
|
|
5755
6231
|
range-filtered). Needs records:read + a \`view\` grant on \`record.<entity>\`.`,
|
|
5756
|
-
catalog: `octwin catalog [--readiness] [--json]
|
|
5757
|
-
The commerce \`product\` records + price, availability, stock (null = not
|
|
5758
|
-
inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
|
|
5759
|
-
Graph checklist (LIVE Graph calls; needs a bound access token). Needs
|
|
5760
|
-
catalog:read + the \`catalog\` plan feature.
|
|
5761
|
-
|
|
5762
|
-
WRITES (need \`catalog:write\`):
|
|
5763
|
-
octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
|
|
5764
|
-
octwin catalog stock <retailerId> [--set-on-hand <n>]
|
|
5765
|
-
|
|
5766
|
-
\`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
|
|
5767
|
-
inventory-tracked (always sellable), which is different from 0. Lowering on_hand
|
|
5768
|
-
below the units already reserved for open carts is refused. Creating/deleting
|
|
6232
|
+
catalog: `octwin catalog [--readiness] [--limit n] [--offset n] [--json]
|
|
6233
|
+
The commerce \`product\` records + price, availability, stock (null = not
|
|
6234
|
+
inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
|
|
6235
|
+
Graph checklist (LIVE Graph calls; needs a bound access token). Needs
|
|
6236
|
+
catalog:read + the \`catalog\` plan feature.
|
|
6237
|
+
|
|
6238
|
+
WRITES (need \`catalog:write\`):
|
|
6239
|
+
octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
|
|
6240
|
+
octwin catalog stock <retailerId> [--set-on-hand <n>]
|
|
6241
|
+
|
|
6242
|
+
\`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
|
|
6243
|
+
inventory-tracked (always sellable), which is different from 0. Lowering on_hand
|
|
6244
|
+
below the units already reserved for open carts is refused. Creating/deleting
|
|
5769
6245
|
products and the Meta catalog binding/sync stay in the console.`,
|
|
5770
|
-
scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
|
|
5771
|
-
No args = the engine state (bookable resource types, upcoming slots, booked
|
|
5772
|
-
seats). --slots <recordId> computes the slots for one bookable resource
|
|
5773
|
-
(occupancy included; --days is clamped to 1-31 server-side) — the way to verify
|
|
5774
|
-
the availability rules a \`deploy --seed\` created. Needs scheduling:read.
|
|
5775
|
-
|
|
5776
|
-
RULES (list needs scheduling:read; add/rm need scheduling:write):
|
|
5777
|
-
octwin scheduling rules --resource <resourceRecordId>
|
|
5778
|
-
octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
|
|
5779
|
-
[--slot-minutes 30] [--capacity 1]
|
|
5780
|
-
octwin scheduling rule rm <ruleId>
|
|
5781
|
-
octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
|
|
5782
|
-
[--start 09:00 --end 13:00]
|
|
5783
|
-
octwin scheduling exception rm <exceptionId>
|
|
5784
|
-
|
|
5785
|
-
--dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
|
|
6246
|
+
scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
|
|
6247
|
+
No args = the engine state (bookable resource types, upcoming slots, booked
|
|
6248
|
+
seats). --slots <recordId> computes the slots for one bookable resource
|
|
6249
|
+
(occupancy included; --days is clamped to 1-31 server-side) — the way to verify
|
|
6250
|
+
the availability rules a \`deploy --seed\` created. Needs scheduling:read.
|
|
6251
|
+
|
|
6252
|
+
RULES (list needs scheduling:read; add/rm need scheduling:write):
|
|
6253
|
+
octwin scheduling rules --resource <resourceRecordId>
|
|
6254
|
+
octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
|
|
6255
|
+
[--slot-minutes 30] [--capacity 1]
|
|
6256
|
+
octwin scheduling rule rm <ruleId>
|
|
6257
|
+
octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
|
|
6258
|
+
[--start 09:00 --end 13:00]
|
|
6259
|
+
octwin scheduling exception rm <exceptionId>
|
|
6260
|
+
|
|
6261
|
+
--dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
|
|
5786
6262
|
\`--slots\` is how you check what a rule actually produces.`,
|
|
5787
|
-
automation: `octwin automation [campaigns] [--limit n] [--offset n] [--json]
|
|
5788
|
-
No args = every job the pack's automation declaration produced, with its status,
|
|
5789
|
-
interval and LAST RESULT (matched / acted / errors), under a health line whose
|
|
5790
|
-
counts come from SQL rather than from filtering the page — the job list is capped
|
|
5791
|
-
server-side, so a client-side count would depend on the cap. Needs automation:read.
|
|
5792
|
-
|
|
5793
|
-
Jobs are DERIVED from declarations. There is no \`create\`: no automation block in
|
|
5794
|
-
the pack means no jobs, and \`octwin deploy\` is what installs them.
|
|
5795
|
-
|
|
5796
|
-
WRITES (automation:write):
|
|
5797
|
-
octwin automation run <jobId> # run once, now — prints matched/acted/errors
|
|
5798
|
-
octwin automation pause|resume <jobId>
|
|
5799
|
-
octwin automation send <campaignId> # enqueue a campaign; enqueued != delivered
|
|
5800
|
-
|
|
5801
|
-
<jobId> is the \`key\` the list shows (its uuid works too). The routes themselves
|
|
5802
|
-
accept only a uuid — the CLI resolves the key for you, and names the keys that do
|
|
5803
|
-
exist when it cannot. A 403 on a write can be an RBAC grant gap rather than a
|
|
6263
|
+
automation: `octwin automation [campaigns | --campaigns] [--limit n] [--offset n] [--json]
|
|
6264
|
+
No args = every job the pack's automation declaration produced, with its status,
|
|
6265
|
+
interval and LAST RESULT (matched / acted / errors), under a health line whose
|
|
6266
|
+
counts come from SQL rather than from filtering the page — the job list is capped
|
|
6267
|
+
server-side, so a client-side count would depend on the cap. Needs automation:read.
|
|
6268
|
+
|
|
6269
|
+
Jobs are DERIVED from declarations. There is no \`create\`: no automation block in
|
|
6270
|
+
the pack means no jobs, and \`octwin deploy\` is what installs them.
|
|
6271
|
+
|
|
6272
|
+
WRITES (automation:write):
|
|
6273
|
+
octwin automation run <jobId> # run once, now — prints matched/acted/errors
|
|
6274
|
+
octwin automation pause|resume <jobId>
|
|
6275
|
+
octwin automation send <campaignId> # enqueue a campaign; enqueued != delivered
|
|
6276
|
+
|
|
6277
|
+
<jobId> is the \`key\` the list shows (its uuid works too). The routes themselves
|
|
6278
|
+
accept only a uuid — the CLI resolves the key for you, and names the keys that do
|
|
6279
|
+
exist when it cannot. A 403 on a write can be an RBAC grant gap rather than a
|
|
5804
6280
|
missing scope: the action is re-checked against the job.`,
|
|
5805
|
-
integrations: `octwin integrations [--json]
|
|
5806
|
-
What the pack DECLARES beside what is actually CONFIGURED, in one view — because a
|
|
5807
|
-
connection that is declared and never configured is the commonest reason an
|
|
5808
|
-
integration silently never fires, and neither list alone can show it. Flags the
|
|
5809
|
-
gap explicitly. Needs integrations:read.
|
|
5810
|
-
|
|
5811
|
-
DIAGNOSE ONE CONNECTION:
|
|
5812
|
-
octwin integrations preflight <key> # every check, with a fix hint. Makes NO
|
|
5813
|
-
# outbound call — needs only integrations:read
|
|
5814
|
-
octwin integrations test <key> # a LIVE call to its health: operation
|
|
5815
|
-
# (integrations:write). Exits 1 when it fails.
|
|
5816
|
-
|
|
5817
|
-
THE DELIVERY LOG:
|
|
5818
|
-
octwin integrations deliveries [--status s] [--operation id] [--limit n]
|
|
5819
|
-
octwin integrations deliveries <id> # + the redacted request/response snapshots
|
|
5820
|
-
octwin integrations retry|cancel|send-now <id> # integrations:write
|
|
5821
|
-
octwin integrations events # INBOUND events (what arrived at your webhook)
|
|
5822
|
-
|
|
5823
|
-
retry/cancel answer 409 when the delivery is in the wrong state; the message
|
|
6281
|
+
integrations: `octwin integrations [--json]
|
|
6282
|
+
What the pack DECLARES beside what is actually CONFIGURED, in one view — because a
|
|
6283
|
+
connection that is declared and never configured is the commonest reason an
|
|
6284
|
+
integration silently never fires, and neither list alone can show it. Flags the
|
|
6285
|
+
gap explicitly. Needs integrations:read.
|
|
6286
|
+
|
|
6287
|
+
DIAGNOSE ONE CONNECTION:
|
|
6288
|
+
octwin integrations preflight <key> # every check, with a fix hint. Makes NO
|
|
6289
|
+
# outbound call — needs only integrations:read
|
|
6290
|
+
octwin integrations test <key> # a LIVE call to its health: operation
|
|
6291
|
+
# (integrations:write). Exits 1 when it fails.
|
|
6292
|
+
|
|
6293
|
+
THE DELIVERY LOG:
|
|
6294
|
+
octwin integrations deliveries [--status s] [--operation id] [--limit n] [--offset n]
|
|
6295
|
+
octwin integrations deliveries <id> # + the redacted request/response snapshots
|
|
6296
|
+
octwin integrations retry|cancel|send-now <id> # integrations:write
|
|
6297
|
+
octwin integrations events # INBOUND events (what arrived at your webhook)
|
|
6298
|
+
|
|
6299
|
+
retry/cancel answer 409 when the delivery is in the wrong state; the message
|
|
5824
6300
|
carries the rule.`,
|
|
5825
|
-
journeys: `octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition]
|
|
5826
|
-
[--stage <stageId>] [--limit n] [--json]
|
|
5827
|
-
No args = the journeys the pack declares. With an id, one of six views —
|
|
5828
|
-
--funnel (default) stage-by-stage reach and drop-off · --overview entered vs
|
|
5829
|
-
converted plus the biggest drop-off · --goals completions, contacts, value and
|
|
5830
|
-
p50 time · --trends per-bucket activity · --cost tokens and dollars per goal ·
|
|
5831
|
-
--definition what was DECLARED, unmeasured (the one view that works with no
|
|
5832
|
-
traffic). Needs journeys:read.
|
|
5833
|
-
|
|
5834
|
-
--stage <stageId> lists the runs sitting at a stage right now (a live snapshot,
|
|
5835
|
-
not the funnel's cumulative reached counts).
|
|
5836
|
-
|
|
5837
|
-
Same flag grammar as \`octwin analytics\` on purpose: a journey funnel and an
|
|
5838
|
-
entity funnel are the same question about different subjects. Journeys carry RBAC
|
|
5839
|
-
on top of the scope, so an empty answer can be a missing \`view\` grant rather
|
|
6301
|
+
journeys: `octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition]
|
|
6302
|
+
[--stage <stageId>] [--limit n] [--offset n] [--json]
|
|
6303
|
+
No args = the journeys the pack declares. With an id, one of six views —
|
|
6304
|
+
--funnel (default) stage-by-stage reach and drop-off · --overview entered vs
|
|
6305
|
+
converted plus the biggest drop-off · --goals completions, contacts, value and
|
|
6306
|
+
p50 time · --trends per-bucket activity · --cost tokens and dollars per goal ·
|
|
6307
|
+
--definition what was DECLARED, unmeasured (the one view that works with no
|
|
6308
|
+
traffic). Needs journeys:read.
|
|
6309
|
+
|
|
6310
|
+
--stage <stageId> lists the runs sitting at a stage right now (a live snapshot,
|
|
6311
|
+
not the funnel's cumulative reached counts).
|
|
6312
|
+
|
|
6313
|
+
Same flag grammar as \`octwin analytics\` on purpose: a journey funnel and an
|
|
6314
|
+
entity funnel are the same question about different subjects. Journeys carry RBAC
|
|
6315
|
+
on top of the scope, so an empty answer can be a missing \`view\` grant rather
|
|
5840
6316
|
than missing data — the output says which causes are possible.`,
|
|
5841
|
-
performance: `octwin performance [--detail] [--json]
|
|
5842
|
-
The project's business indicators — value produced, conversion, duration — each
|
|
5843
|
-
with its delta against the previous window and a \`why\` naming the declaration it
|
|
5844
|
-
came from. --detail adds the per-indicator breakdown.
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
6317
|
+
performance: `octwin performance [--detail] [--range <days>] [--channel <ch>] [--json]
|
|
6318
|
+
The project's business indicators — value produced, conversion, duration — each
|
|
6319
|
+
with its delta against the previous window and a \`why\` naming the declaration it
|
|
6320
|
+
came from. --detail adds the per-indicator breakdown. --range <days> sets the window
|
|
6321
|
+
and --channel <whatsapp|web> narrows every figure to one channel — the console's two
|
|
6322
|
+
controls on the same page.
|
|
6323
|
+
|
|
6324
|
+
Needs records:read, NOT a performance scope (there is none), so a read-only token
|
|
6325
|
+
already reaches it. Indicators are DERIVED: a pack that declares no journey goal
|
|
5848
6326
|
value and no pipelined entity produces none, which is a different thing from zero.`,
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
6327
|
+
charts: `octwin charts [<key>] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--json]
|
|
6328
|
+
The pack's own \`charts:\` (xrm.yaml), as the owner's dashboard draws them. No key = the
|
|
6329
|
+
list; with a key = that chart's figure, its parts (a \`by:\` chart) or its points (an
|
|
6330
|
+
\`over:\` chart) for the window. A token sees the charts whose entity it may view.
|
|
6331
|
+
Needs records:read.`,
|
|
6332
|
+
handoff: `octwin handoff [<id>] [--bucket waiting|mine|all|ended] [--limit n] [--offset n] [--json]
|
|
6333
|
+
The Live chat desk, read-only: the handoffs in a bucket (default all), or one with
|
|
6334
|
+
its transcript — who said what: customer, AI Agent, operator, platform. Needs work:read
|
|
6335
|
+
and the live_chat plan feature. Accepting, answering and returning one stay in the
|
|
6336
|
+
console: those need a signed-in PERSON to hold the conversation, and a token is not one.`,
|
|
6337
|
+
surveys: `octwin surveys [<key>] [--limit n] [--offset n] [--json]
|
|
6338
|
+
No key = the declared surveys with their responses and headline score (under the
|
|
6339
|
+
survey's own model — average, top box or NPS). With a key = its questions, the rollup,
|
|
6340
|
+
and the recent responses (--limit / --offset page them). Needs records:read.`,
|
|
6341
|
+
usage: `octwin usage [--json]
|
|
6342
|
+
Model calls, tokens and cost for the resolved scope — project when one is pinned
|
|
6343
|
+
or passed with --project, otherwise the whole workspace. Broken down by model,
|
|
6344
|
+
kind, agent and channel.
|
|
6345
|
+
|
|
6346
|
+
Needs no particular scope: any valid token reaches it.
|
|
6347
|
+
|
|
6348
|
+
This is MODEL spend only. WhatsApp/Meta message billing is operator-only and
|
|
5857
6349
|
deliberately outside the token scope registry — no API token can read it.`,
|
|
5858
|
-
'platform-kb': `octwin platform-kb [pull] [--if-stale] [--check] [--dir .] [--url <url>] [--token <t>]
|
|
5859
|
-
Pull the platform capability reference (markdown + JSON catalogs) into
|
|
5860
|
-
.octwin/platform-kb/ for the octwin-pack authoring skill, plus three maps:
|
|
5861
|
-
INDEX.md (the corpus) · SYMBOLS.md (every name -> its file; grep this) ·
|
|
5862
|
-
OUTLINE.md (every heading with its line number).
|
|
5863
|
-
|
|
5864
|
-
NO TOKEN NEEDED — the reference is platform stdlib and is served anonymously,
|
|
5865
|
-
and this command never sends one. --token is accepted and ignored, so an older
|
|
5866
|
-
script that passes it keeps working.
|
|
5867
|
-
|
|
5868
|
-
--if-stale poll the platform's content_hash first and skip the download when
|
|
5869
|
-
nothing changed. Cheap enough to run at the start of every session.
|
|
5870
|
-
--check report only, write nothing. Exit 0 = current, 2 = stale or never
|
|
5871
|
-
pulled, 1 = could not tell (offline / no reference served). For
|
|
6350
|
+
'platform-kb': `octwin platform-kb [pull] [--if-stale] [--check] [--dir .] [--url <url>] [--token <t>]
|
|
6351
|
+
Pull the platform capability reference (markdown + JSON catalogs) into
|
|
6352
|
+
.octwin/platform-kb/ for the octwin-pack authoring skill, plus three maps:
|
|
6353
|
+
INDEX.md (the corpus) · SYMBOLS.md (every name -> its file; grep this) ·
|
|
6354
|
+
OUTLINE.md (every heading with its line number).
|
|
6355
|
+
|
|
6356
|
+
NO TOKEN NEEDED — the reference is platform stdlib and is served anonymously,
|
|
6357
|
+
and this command never sends one. --token is accepted and ignored, so an older
|
|
6358
|
+
script that passes it keeps working.
|
|
6359
|
+
|
|
6360
|
+
--if-stale poll the platform's content_hash first and skip the download when
|
|
6361
|
+
nothing changed. Cheap enough to run at the start of every session.
|
|
6362
|
+
--check report only, write nothing. Exit 0 = current, 2 = stale or never
|
|
6363
|
+
pulled, 1 = could not tell (offline / no reference served). For
|
|
5872
6364
|
scripts and agent loops that want to branch without parsing prose.`,
|
|
5873
|
-
test: `octwin test [--dir .]
|
|
5874
|
-
Alias for \`octwin validate --remote\` — the full platform check
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
6365
|
+
test: `octwin test [--dir .] [--require-kb] [--strict-primitives]
|
|
6366
|
+
Alias for \`octwin validate --remote\` — the full platform check, REQUIRED (a skipped full
|
|
6367
|
+
check is an error). --offline contradicts it; the other validate flags mean what they mean there.`,
|
|
6368
|
+
memos: `octwin memos [--all] [--json]
|
|
6369
|
+
Read what the platform has told you: a REPLY to a report you sent with
|
|
6370
|
+
\`octwin feedback\`, or a NOTICE published to every author (a new capability,
|
|
6371
|
+
a deprecation, a breaking change). Bodies are printed in full.
|
|
6372
|
+
Reading marks them read, so the reminder stops. --all re-reads history and
|
|
6373
|
+
acks nothing. --json to branch on \`severity\`
|
|
5881
6374
|
(info | action_required | breaking).`,
|
|
5882
|
-
feedback: `octwin feedback [--dir .]
|
|
5883
|
-
Submit this pack's FEEDBACK.md to the platform team.
|
|
5884
|
-
The octwin-pack skill writes that file in its last step — findings grouped by
|
|
5885
|
-
owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
|
|
5886
|
-
you to paste it into a chat.
|
|
5887
|
-
Attaches the pack id + version from manifest.yaml, this CLI's version, and the
|
|
5888
|
-
content_hash of the capability reference in .octwin/platform-kb/ — triage needs
|
|
5889
|
-
the last two to tell "the platform is wrong" from "that was already fixed" or
|
|
6375
|
+
feedback: `octwin feedback [--dir .]
|
|
6376
|
+
Submit this pack's FEEDBACK.md to the platform team.
|
|
6377
|
+
The octwin-pack skill writes that file in its last step — findings grouped by
|
|
6378
|
+
owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
|
|
6379
|
+
you to paste it into a chat.
|
|
6380
|
+
Attaches the pack id + version from manifest.yaml, this CLI's version, and the
|
|
6381
|
+
content_hash of the capability reference in .octwin/platform-kb/ — triage needs
|
|
6382
|
+
the last two to tell "the platform is wrong" from "that was already fixed" or
|
|
5890
6383
|
"you were reading a stale reference". Needs the \`pack:deploy\` scope.`,
|
|
5891
6384
|
};
|
|
5892
6385
|
/**
|
|
@@ -5897,36 +6390,48 @@ octwin projects rm <slug> [--yes]
|
|
|
5897
6390
|
* (`cli.json-contract.test.ts`).
|
|
5898
6391
|
*/
|
|
5899
6392
|
export const COMMANDS = {
|
|
5900
|
-
init: { run: cmdInit, json: false },
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
6393
|
+
init: { run: cmdInit, json: false, flags: ['id', 'description', 'display-name'] },
|
|
6394
|
+
recipe: { run: cmdRecipe, json: false, flags: ['dry-run'] },
|
|
6395
|
+
validate: { run: cmdValidate, json: false, flags: ['offline', 'remote', 'require-kb', 'strict-primitives'] },
|
|
6396
|
+
login: { run: cmdLogin, json: 'lines', flags: ['wait'] }, // pending, then approved — two objects
|
|
6397
|
+
whoami: { run: cmdWhoami, json: false, flags: [] },
|
|
6398
|
+
deploy: { run: cmdDeploy, json: false, flags: ['force', 'seed', 'request-listing', 'withdraw-listing'] },
|
|
6399
|
+
pull: { run: cmdPull, json: false, flags: ['force', 'version'] },
|
|
6400
|
+
status: { run: cmdStatus, json: false, flags: [] },
|
|
6401
|
+
records: { run: cmdRecords, json: 'document', flags: ['contact', 'entity', 'fields-json', 'limit', 'note', 'offset', 'outcome', 'search', 'set', 'stage', 'to'] },
|
|
6402
|
+
work: { run: cmdWork, json: 'document', flags: ['action', 'dry-run', 'limit', 'note', 'offset', 'param', 'queues', 'to', 'unrouted'] },
|
|
6403
|
+
logs: { run: cmdLogs, json: 'document', flags: ['as'] },
|
|
6404
|
+
chat: { run: cmdChat, json: 'lines', flags: ['as', 'media', 'press', 'script', 'tap'] }, // one line per event the turn produced
|
|
6405
|
+
media: { run: cmdMedia, json: 'document', flags: ['out'] },
|
|
6406
|
+
seed: { run: cmdSeed, json: false, flags: ['pack', 'reset'] },
|
|
6407
|
+
projects: { run: cmdProjects, json: 'document', flags: ['archived', 'pack', 'slug', 'yes'] },
|
|
6408
|
+
agents: { run: cmdAgents, json: 'document', flags: ['disable-tool', 'enable-tool', 'enabled', 'model', 'overlay', 'prompt'] },
|
|
6409
|
+
orders: { run: cmdOrders, json: 'document', flags: ['force', 'limit', 'mark-returned', 'offset', 'payment', 'reason', 'status', 'to'] },
|
|
6410
|
+
analytics: { run: cmdAnalytics, json: 'document', flags: ['funnel', 'overview', 'milestones', 'trends', 'cost', 'stage', 'limit', 'offset'] },
|
|
6411
|
+
catalog: { run: cmdCatalog, json: 'document', flags: ['limit', 'offset', 'readiness', 'set-on-hand', 'to'] },
|
|
6412
|
+
scheduling: { run: cmdScheduling, json: 'document', flags: ['capacity', 'date', 'days', 'dow', 'end', 'from', 'kind', 'resource', 'slot-minutes', 'slots', 'start'] },
|
|
6413
|
+
automation: { run: cmdAutomation, json: 'document', flags: ['campaigns', 'limit', 'offset'] },
|
|
6414
|
+
integrations: { run: cmdIntegrations, json: 'document', flags: ['limit', 'offset', 'operation', 'status'] },
|
|
6415
|
+
journeys: { run: cmdJourneys, json: 'document', flags: ['funnel', 'overview', 'goals', 'trends', 'cost', 'definition', 'stage', 'limit', 'offset'] },
|
|
6416
|
+
performance: { run: cmdPerformance, json: 'document', flags: ['detail', 'range', 'channel'] },
|
|
6417
|
+
usage: { run: cmdUsage, json: 'document', flags: [] },
|
|
6418
|
+
charts: { run: cmdCharts, json: 'document', flags: ['from', 'to'] },
|
|
6419
|
+
handoff: { run: cmdHandoff, json: 'document', flags: ['bucket', 'limit', 'offset'] },
|
|
6420
|
+
surveys: { run: cmdSurveys, json: 'document', flags: ['limit', 'offset'] },
|
|
6421
|
+
'platform-kb': { run: cmdPlatformKb, json: false, flags: ['check', 'if-stale'] },
|
|
6422
|
+
feedback: { run: cmdFeedback, json: false, flags: [] },
|
|
6423
|
+
memos: { run: cmdMemos, json: 'document', flags: ['all'] },
|
|
5927
6424
|
// A6: `test` = the full remote validate, not a validate-clone.
|
|
5928
|
-
test: { run: flags => cmdValidate({ ...flags, remote: true }), json: false },
|
|
6425
|
+
test: { run: flags => cmdValidate({ ...flags, remote: true }), json: false, flags: ['offline', 'remote', 'require-kb', 'strict-primitives'] },
|
|
5929
6426
|
};
|
|
6427
|
+
/**
|
|
6428
|
+
* The flags every command takes: the target (`--url` / `--tenant` / `--project` / `--token`, read by
|
|
6429
|
+
* `resolveTarget`), `--json` (held to `json:` above), `--help`, and `--dir` — the pack directory, which
|
|
6430
|
+
* `runCli` itself reads on every run to find the pulled KB (`readLocalKb`). Each command's OWN flags are its
|
|
6431
|
+
* `flags:` row — the one list `runCli` refuses against, and `cli.flags.test.ts` holds to the code that
|
|
6432
|
+
* reads them, to `help()` / `COMMAND_HELP`, and to the README.
|
|
6433
|
+
*/
|
|
6434
|
+
export const GLOBAL_FLAGS = ['url', 'tenant', 'project', 'token', 'json', 'help', 'dir'];
|
|
5930
6435
|
/** Exit code when a `--json` command ended without the result it promised — a bug in this CLI. */
|
|
5931
6436
|
export const EXIT_NO_RESULT = 70;
|
|
5932
6437
|
/**
|
|
@@ -5977,6 +6482,13 @@ export async function runCli(argv, io) {
|
|
|
5977
6482
|
if (flags.json === true && spec.json === false) {
|
|
5978
6483
|
die(`\`octwin ${command}\` has no --json output — the commands that do are: ${Object.entries(COMMANDS).filter(([, c]) => c.json !== false).map(([n]) => n).join(', ')}`);
|
|
5979
6484
|
}
|
|
6485
|
+
// A flag the command does not read is REFUSED, before it runs. It used to be ignored, so a
|
|
6486
|
+
// typo — or a flag the command never learned, like `performance --channel` until 2026-10-11 —
|
|
6487
|
+
// ran the command with its default and printed an answer to a question nobody asked.
|
|
6488
|
+
const unknownFlags = Object.keys(flags).filter(k => k !== '_' && !GLOBAL_FLAGS.includes(k) && !spec.flags.includes(k));
|
|
6489
|
+
if (unknownFlags.length) {
|
|
6490
|
+
die(`\`octwin ${command}\` has no ${unknownFlags.map(f => `--${f}`).join(', ')} — its flags: ${spec.flags.length ? spec.flags.map(f => `--${f}`).join(' ') : '(none of its own)'}; on every command: ${GLOBAL_FLAGS.map(f => `--${f}`).join(' ')}. Usage: octwin ${command} --help`);
|
|
6491
|
+
}
|
|
5980
6492
|
// Compute the trailing nudges BEFORE the command runs, so they survive a `die()` — see the
|
|
5981
6493
|
// notice-channel block above. They are PRINTED after the command, by `flushNotices` here on the
|
|
5982
6494
|
// success path and by `exitNow` on every failure path. Fail-silent by construction (each helper
|