octwin-cli 0.8.16 → 0.9.0
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 +1129 -1028
- package/README.md +39 -31
- package/dist/cli.js +1209 -573
- 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/dist/lib/rules.js +36 -0
- package/package.json +46 -46
- package/templates/starter/automation.yaml +10 -0
- 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 +28 -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?
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion, any pipelined entity
|
|
29
29
|
* octwin catalog [--readiness] # commerce products + stock + the WhatsApp catalog binding (catalog:read)
|
|
30
30
|
* octwin scheduling [--slots <resourceRecordId>] # engine state / computed slots (scheduling:read)
|
|
31
|
-
* octwin automation [campaigns]
|
|
31
|
+
* octwin automation [coming|happened|why <id>|campaigns] # the project's rules in words, what is coming up, what happened (automation:read)
|
|
32
32
|
* octwin integrations [deliveries|events|preflight|test …] # declared vs configured, and the delivery log (integrations:read)
|
|
33
33
|
* octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] # declared journeys, measured (journeys:read)
|
|
34
34
|
* octwin performance [--detail] # the project's business indicators (records:read — there is no performance scope)
|
|
@@ -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,10 +78,14 @@ 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';
|
|
88
|
+
import { phraseText, resolveRuleKey, shortTime } from './lib/rules.js';
|
|
83
89
|
import { chatLabel, answersTheTurn } from './lib/chat-line.js';
|
|
84
90
|
import { kbOneLiner, buildKbIndexMarkdown, buildKbOutlineMarkdown, } from './lib/kb-index.js';
|
|
85
91
|
import { buildSymbols, linkExplainers, renderSymbolsMarkdown } from './lib/kb-symbols.js';
|
|
@@ -235,9 +241,9 @@ const VERB_REQUIREMENTS = {
|
|
|
235
241
|
// which does NOT confer this — that 403 is otherwise baffling.
|
|
236
242
|
'projects create': { scope: 'projects:write' },
|
|
237
243
|
'projects rm': { scope: 'projects:write' },
|
|
238
|
-
//
|
|
239
|
-
// `campaigns`
|
|
240
|
-
// entry here would print "needs automation:write" on a read failure.
|
|
244
|
+
// Rules are declared, so there is no `create` — only acting on one (or a campaign).
|
|
245
|
+
// `coming` / `happened` / `why` / `campaigns` are absent on purpose: they are READS sharing
|
|
246
|
+
// the verb slot, and an entry here would print "needs automation:write" on a read failure.
|
|
241
247
|
'automation run': { scope: 'automation:write' },
|
|
242
248
|
'automation pause': { scope: 'automation:write' },
|
|
243
249
|
'automation resume': { scope: 'automation:write' },
|
|
@@ -291,6 +297,9 @@ const COMMAND_REQUIREMENTS = {
|
|
|
291
297
|
// derived from record + journey data, and the route is gated accordingly, so the
|
|
292
298
|
// Read-only token preset already reaches this.
|
|
293
299
|
performance: { scope: 'records:read' },
|
|
300
|
+
charts: { scope: 'records:read', feature: 'records' },
|
|
301
|
+
handoff: { scope: 'work:read', feature: 'live_chat' },
|
|
302
|
+
surveys: { scope: 'records:read', feature: 'records' },
|
|
294
303
|
// `usage` is deliberately absent: its route is `requireTenantAccess` only, so any
|
|
295
304
|
// valid token reaches it. Declaring a requirement would print "needs the X scope"
|
|
296
305
|
// on a failure whose real cause is an unreachable instance — the same reasoning as
|
|
@@ -914,6 +923,9 @@ function commandTouchesPlatform(command, flags) {
|
|
|
914
923
|
case 'journeys':
|
|
915
924
|
case 'performance':
|
|
916
925
|
case 'usage':
|
|
926
|
+
case 'charts':
|
|
927
|
+
case 'surveys':
|
|
928
|
+
case 'handoff':
|
|
917
929
|
case 'projects':
|
|
918
930
|
case 'seed': return true;
|
|
919
931
|
default: return false;
|
|
@@ -927,7 +939,7 @@ function cmdInit(flags) {
|
|
|
927
939
|
// which is why `init` can stay offline (it has no idea which workspace this will deploy
|
|
928
940
|
// to, and no longer needs one) and why the same scaffold deploys into two workspaces as
|
|
929
941
|
// two packs with nothing to edit in between.
|
|
930
|
-
const id = flags.id ?? target
|
|
942
|
+
const id = flags.id ?? packIdFromDir(target);
|
|
931
943
|
const idProblem = describePackNameProblem(id);
|
|
932
944
|
if (idProblem)
|
|
933
945
|
die(`${idProblem}\n → pass --id <pack-name>`);
|
|
@@ -948,17 +960,127 @@ function cmdInit(flags) {
|
|
|
948
960
|
// the token carries its own tenant + optional project pin.
|
|
949
961
|
writeFileSync(join(dir, '.gitignore'), 'node_modules/\n.pack-bundles/\n.octwin/\n', 'utf8');
|
|
950
962
|
if (!existsSync(join(dir, 'README.md'))) {
|
|
951
|
-
writeFileSync(join(dir, 'README.md'),
|
|
963
|
+
writeFileSync(join(dir, 'README.md'), [
|
|
964
|
+
`# ${id}`, '',
|
|
965
|
+
'A pure-YAML pack for the Octwin platform, in Arabic and English.', '',
|
|
966
|
+
'- `flows/tools/home.flow.yaml` — the menu (your front door); add a row per journey',
|
|
967
|
+
'- `flows/tools/request.flow.yaml` + `xrm.yaml` — a visit request, saved as a record; replace it with your own',
|
|
968
|
+
'- `journeys/request.journey.yaml` — the funnel the owner reads on the Performance page',
|
|
969
|
+
'- `octwin platform-kb pull` — the authoring guides and recipes; start with `.octwin/platform-kb/INDEX.md`',
|
|
970
|
+
'- `octwin recipe` — list the recipes; `octwin recipe <module>` adds one to this pack',
|
|
971
|
+
'- `octwin validate` — the platform\'s full check, before every deploy',
|
|
972
|
+
'- `octwin deploy --seed` — deploy, install, and add the demo rows',
|
|
973
|
+
'',
|
|
974
|
+
].join('\n'), 'utf8');
|
|
952
975
|
}
|
|
953
976
|
out.print(`✓ Scaffolded pure-YAML pack '${id}' at ${dir}`);
|
|
954
977
|
out.print('\nNext:');
|
|
955
978
|
out.print(` cd ${target}`);
|
|
956
979
|
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
|
|
980
|
+
out.print(' octwin platform-kb pull # the authoring guides + recipes — read .octwin/platform-kb/INDEX.md first');
|
|
981
|
+
out.print(' octwin recipe # the recipes you can add; `octwin recipe <module>` writes one in');
|
|
982
|
+
out.print(' octwin validate # the platform\'s FULL check — no login needed');
|
|
983
|
+
out.print(' # point the CLI at your platform (the token carries the tenant), then deploy with the demo rows:');
|
|
984
|
+
out.print(' octwin login --url <platformUrl>');
|
|
985
|
+
out.print(' octwin deploy --seed');
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* `octwin recipe [<module>] [--dir .] [--dry-run]` — a KB recipe's starter files into this pack.
|
|
989
|
+
*
|
|
990
|
+
* Read from the PULLED guide (`lib/recipe.ts`), so a recipe has one copy — the one the platform holds
|
|
991
|
+
* to its full deploy check together with every other recipe, on the `init` starter. Nothing is
|
|
992
|
+
* overwritten: a file the pack already has is named, and a declaration it already has (an `xrm.yaml`
|
|
993
|
+
* holding the author's own entities) gets the recipe's part printed to merge. With no module it lists
|
|
994
|
+
* the recipes the pulled KB carries.
|
|
995
|
+
*/
|
|
996
|
+
function cmdRecipe(flags) {
|
|
997
|
+
const packDir = resolve(flags.dir ?? '.');
|
|
998
|
+
const kbDir = findPlatformKbDir(packDir)
|
|
999
|
+
?? die('no pulled KB here or above — run `octwin platform-kb pull` first (a recipe is read from it)');
|
|
1000
|
+
let index = [];
|
|
1001
|
+
try {
|
|
1002
|
+
index = JSON.parse(readFileSync(join(kbDir, 'index.json'), 'utf8')).index ?? [];
|
|
1003
|
+
}
|
|
1004
|
+
catch {
|
|
1005
|
+
die(`${join(kbDir, 'index.json')} could not be read — re-run \`octwin platform-kb pull\``);
|
|
1006
|
+
}
|
|
1007
|
+
const modules = recipeModules(index);
|
|
1008
|
+
const module = flags._[0]?.toLowerCase();
|
|
1009
|
+
if (!module) {
|
|
1010
|
+
if (modules.size === 0)
|
|
1011
|
+
die('the pulled KB names no recipe — re-run `octwin platform-kb pull` (an older platform published none)');
|
|
1012
|
+
out.print('Recipes in the pulled KB — `octwin recipe <module>` writes one into this pack:');
|
|
1013
|
+
for (const [m, key] of modules)
|
|
1014
|
+
out.print(` ${m.padEnd(14)} guide:${key}#${m}-recipe`);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const key = modules.get(module) ?? die(`no recipe '${module}' in the pulled KB — known: ${[...modules.keys()].join(', ')}`);
|
|
1018
|
+
const starters = recipeStarters(readFileSync(join(kbDir, `${key}.md`), 'utf8'), module)
|
|
1019
|
+
?? die(`${key}.md has no '## ${module} recipe' section — re-run \`octwin platform-kb pull\``);
|
|
1020
|
+
const manifestPath = join(packDir, 'manifest.yaml');
|
|
1021
|
+
if (!existsSync(manifestPath))
|
|
1022
|
+
die(`no manifest.yaml in ${packDir} — run this inside a pack, or pass --dir`);
|
|
1023
|
+
const manifest = parseYaml(readFileSync(manifestPath, 'utf8'));
|
|
1024
|
+
const { actions, flows } = planRecipe(starters, p => existsSync(join(packDir, p)), String(manifest.id ?? ''));
|
|
1025
|
+
const dry = flags['dry-run'] === true;
|
|
1026
|
+
out.print(`${dry ? 'Would apply' : 'Applying'} the ${module} recipe (guide:${key}#${module}-recipe) to ${packDir}`);
|
|
1027
|
+
for (const a of actions) {
|
|
1028
|
+
if (a.act === 'write') {
|
|
1029
|
+
if (!dry) {
|
|
1030
|
+
mkdirSync(dirname(join(packDir, a.path)), { recursive: true });
|
|
1031
|
+
writeFileSync(join(packDir, a.path), a.body, 'utf8');
|
|
1032
|
+
}
|
|
1033
|
+
out.print(` ✓ ${dry ? 'would write' : 'wrote'} ${a.path}`);
|
|
1034
|
+
}
|
|
1035
|
+
else if (a.act === 'exists') {
|
|
1036
|
+
out.print(` • ${a.path} already exists — left as it is (compare it with guide:${key}#${module}-starter)`);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
for (const a of actions) {
|
|
1040
|
+
if (a.act === 'merge') {
|
|
1041
|
+
out.print(`\n${a.path} already exists — merge this part into it${a.note ? ` (${a.note})` : ''}:`);
|
|
1042
|
+
out.print(a.body.trimEnd());
|
|
1043
|
+
}
|
|
1044
|
+
else if (a.act === 'manifest') {
|
|
1045
|
+
out.print('\nAdd to manifest.yaml:');
|
|
1046
|
+
out.print(a.body.trimEnd());
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
const listed = (v) => (Array.isArray(v) ? v.map(String) : []);
|
|
1050
|
+
const newFlows = flows.filter(f => !listed(manifest.flows).includes(f));
|
|
1051
|
+
const newTools = flows.filter(f => !listed(manifest.agents?.[0]?.tools).includes(f));
|
|
1052
|
+
if (newFlows.length || newTools.length) {
|
|
1053
|
+
out.print('\nList the new flows in manifest.yaml:');
|
|
1054
|
+
if (newFlows.length)
|
|
1055
|
+
out.print(` flows: ${newFlows.map(f => `- ${f}`).join(' ')}`);
|
|
1056
|
+
if (newTools.length)
|
|
1057
|
+
out.print(` agents[0].tools: ${newTools.map(f => `- ${f}`).join(' ')}`);
|
|
1058
|
+
}
|
|
1059
|
+
// What this recipe's buttons and `track:` ids need that the pack does not ship yet — other recipes,
|
|
1060
|
+
// derived from their files (`recipeNeeds`). Said now, so the author does not meet the chain as one
|
|
1061
|
+
// validate error after another.
|
|
1062
|
+
const all = new Map();
|
|
1063
|
+
for (const [m, k] of modules) {
|
|
1064
|
+
const s = existsSync(join(kbDir, `${k}.md`)) ? recipeStarters(readFileSync(join(kbDir, `${k}.md`), 'utf8'), m) : null;
|
|
1065
|
+
if (s)
|
|
1066
|
+
all.set(m, s);
|
|
1067
|
+
}
|
|
1068
|
+
const journeyDir = join(packDir, 'journeys');
|
|
1069
|
+
const journeys = existsSync(journeyDir)
|
|
1070
|
+
? readdirSync(journeyDir).filter(f => f.endsWith('.journey.yaml'))
|
|
1071
|
+
.map(f => [`journeys/${f}`, parseYaml(readFileSync(join(journeyDir, f), 'utf8'))])
|
|
1072
|
+
: [];
|
|
1073
|
+
const { needs, unresolved } = recipeNeeds(module, all, {
|
|
1074
|
+
flows: new Set([...listed(manifest.flows), ...flows]), journeys,
|
|
1075
|
+
});
|
|
1076
|
+
if (needs.length) {
|
|
1077
|
+
out.print(`\nThis recipe also needs ${needs.length === 1 ? 'another recipe' : `${needs.length} other recipes`} — its buttons and \`track:\` ids name what they ship:`);
|
|
1078
|
+
for (const n of needs)
|
|
1079
|
+
out.print(` octwin recipe ${n}`);
|
|
1080
|
+
}
|
|
1081
|
+
for (const u of unresolved)
|
|
1082
|
+
out.print(` ! ${u}`);
|
|
1083
|
+
out.print(`\nThen read guide:${key}#${module}-ingredients and #${module}-rules, add the home row it names, and run \`octwin validate\`.`);
|
|
962
1084
|
}
|
|
963
1085
|
function localValidate(packDir) {
|
|
964
1086
|
const { files, blobs } = collectBundleFiles(packDir);
|
|
@@ -1194,6 +1316,25 @@ async function cmdValidate(flags) {
|
|
|
1194
1316
|
: { kind: 'passed', line: 'declaration files carry no unknown or missing keys (unions are left to --remote)' };
|
|
1195
1317
|
},
|
|
1196
1318
|
},
|
|
1319
|
+
{
|
|
1320
|
+
// Journey ids named by `track:` / `do: track_event`, against the pack's OWN `journeys/`.
|
|
1321
|
+
// Needs no KB — the declared set is in the bundle — so it never skips. Last, because a
|
|
1322
|
+
// malformed journey file is the declaration check's finding, not a wall of these.
|
|
1323
|
+
label: 'journey ids',
|
|
1324
|
+
run: () => {
|
|
1325
|
+
const ids = collectJourneyIds(yamlDocs());
|
|
1326
|
+
const findings = withLines(yamlDocs()
|
|
1327
|
+
.filter(([p]) => isFlowFile(p))
|
|
1328
|
+
.flatMap(([p, doc]) => findJourneyViolations(doc, p, ids)));
|
|
1329
|
+
return findings.length
|
|
1330
|
+
? {
|
|
1331
|
+
kind: 'findings', noun: 'undeclared journey id',
|
|
1332
|
+
lines: findings.map(describeJourneyFinding),
|
|
1333
|
+
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',
|
|
1334
|
+
}
|
|
1335
|
+
: { kind: 'passed', line: 'every literal journey id in `track:` / `track_event` is declared in journeys/' };
|
|
1336
|
+
},
|
|
1337
|
+
},
|
|
1197
1338
|
];
|
|
1198
1339
|
const skipped = reportChecks(checks, {
|
|
1199
1340
|
log: (l) => out.print(l),
|
|
@@ -1639,16 +1780,20 @@ async function cmdWhoami(flags) {
|
|
|
1639
1780
|
* You may pull a pack YOUR tenant owns (an operator token may pull any). A pack
|
|
1640
1781
|
* you merely installed from the marketplace is not yours to read.
|
|
1641
1782
|
*
|
|
1642
|
-
* Writes into
|
|
1643
|
-
*
|
|
1644
|
-
*
|
|
1645
|
-
*
|
|
1783
|
+
* Writes into the pack folder you are standing in when it IS this pack (its
|
|
1784
|
+
* `.octwin-base.json` or its manifest says so), else into `./<packId>` — `--dir`
|
|
1785
|
+
* overrides. It replaces only files that still match the folder's base: a file you
|
|
1786
|
+
* changed since your last pull or deploy stops it (`planPull`), unless `--force`. It
|
|
1787
|
+
* then writes the new base, so the next `octwin deploy` is judged against what you
|
|
1788
|
+
* pulled. The pulled directory is immediately `octwin deploy`-able back to where it
|
|
1789
|
+
* came from — the target is the saved login, so nothing machine-specific needs to
|
|
1790
|
+
* land in the pack dir.
|
|
1646
1791
|
*/
|
|
1647
1792
|
async function cmdPull(flags) {
|
|
1648
1793
|
const packId = flags._[0] ?? '';
|
|
1649
1794
|
if (!packId)
|
|
1650
1795
|
die('usage: octwin pull <packId> [--dir <out>] [--version <v>] [--force]');
|
|
1651
|
-
const outDir =
|
|
1796
|
+
const outDir = pullTarget(flags, packId);
|
|
1652
1797
|
const t = resolveTarget(flags);
|
|
1653
1798
|
const { url } = t;
|
|
1654
1799
|
const qs = typeof flags.version === 'string' ? `?version=${encodeURIComponent(flags.version)}` : '';
|
|
@@ -1676,8 +1821,23 @@ async function cmdPull(flags) {
|
|
|
1676
1821
|
const total = Object.keys(files).length + Object.keys(blobs).length;
|
|
1677
1822
|
if (total === 0)
|
|
1678
1823
|
die(`${packId}@${json.version} has no files — nothing to write`);
|
|
1679
|
-
|
|
1680
|
-
|
|
1824
|
+
// Replace only what still matches the base — never the author's own edits (`planPull`).
|
|
1825
|
+
const incoming = hashBundle(files, blobs);
|
|
1826
|
+
const prior = readBase(outDir);
|
|
1827
|
+
const base = prior?.pack_id === json.pack_id ? prior : null;
|
|
1828
|
+
const here = existsSync(outDir) ? collectBundleFiles(outDir) : { files: {}, blobs: {} };
|
|
1829
|
+
const { atRisk, remove } = planPull({ local: hashBundle(here.files, here.blobs), incoming, base });
|
|
1830
|
+
if (atRisk.length > 0 && flags.force !== true) {
|
|
1831
|
+
out.err(`✗ ${outDir} has changes the live ${json.version} does not — pulling would overwrite them:`);
|
|
1832
|
+
for (const rel of atRisk.slice(0, 15))
|
|
1833
|
+
out.err(` • ${rel}`);
|
|
1834
|
+
if (atRisk.length > 15)
|
|
1835
|
+
out.err(` … and ${atRisk.length - 15} more`);
|
|
1836
|
+
out.err(base
|
|
1837
|
+
? ' Save them first (git commit, or copy the folder), then pull with --force and apply them again.'
|
|
1838
|
+
: ` This folder has no ${BASE_FILE} (the record of its last pull or deploy), so your edits cannot be told `
|
|
1839
|
+
+ 'apart from an old version. Save them first (git commit, or copy the folder), then pull with --force.');
|
|
1840
|
+
exitNow(1);
|
|
1681
1841
|
}
|
|
1682
1842
|
for (const [rel, body] of Object.entries(files)) {
|
|
1683
1843
|
const p = join(outDir, rel);
|
|
@@ -1689,9 +1849,35 @@ async function cmdPull(flags) {
|
|
|
1689
1849
|
mkdirSync(dirname(p), { recursive: true });
|
|
1690
1850
|
writeFileSync(p, Buffer.from(b64, 'base64'));
|
|
1691
1851
|
}
|
|
1852
|
+
// The live version no longer has these — left behind, the next deploy would bring them back.
|
|
1853
|
+
for (const rel of remove)
|
|
1854
|
+
rmSync(join(outDir, rel), { force: true });
|
|
1855
|
+
writeBase(outDir, { pack_id: json.pack_id, version: json.version, content_sha: json.content_sha, files: incoming });
|
|
1692
1856
|
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
|
-
|
|
1857
|
+
out.print(` ${Object.keys(files).length} file(s)${Object.keys(blobs).length ? `, ${Object.keys(blobs).length} blob(s)` : ''}`
|
|
1858
|
+
+ `${remove.length ? `, ${remove.length} removed` : ''} content_sha ${String(json.content_sha).slice(0, 12)}…`);
|
|
1859
|
+
out.print(`\nFix it, then: octwin deploy${outDir === resolve('.') ? '' : ` --dir ${outDir}`}`);
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Where `pull` writes: `--dir`, else the folder you are in when it is this pack (its base names it, or
|
|
1863
|
+
* its manifest declares the pack's name — the folder a refused deploy was run from), else `./<packId>`.
|
|
1864
|
+
*/
|
|
1865
|
+
function pullTarget(flags, packId) {
|
|
1866
|
+
if (typeof flags.dir === 'string')
|
|
1867
|
+
return resolve(flags.dir);
|
|
1868
|
+
const here = resolve('.');
|
|
1869
|
+
if (readBase(here)?.pack_id === packId)
|
|
1870
|
+
return here;
|
|
1871
|
+
const manifest = join(here, 'manifest.yaml');
|
|
1872
|
+
if (existsSync(manifest)) {
|
|
1873
|
+
try {
|
|
1874
|
+
const id = parseYaml(readFileSync(manifest, 'utf8'))?.id;
|
|
1875
|
+
if (typeof id === 'string' && id === packId.slice(packId.indexOf('.') + 1))
|
|
1876
|
+
return here;
|
|
1877
|
+
}
|
|
1878
|
+
catch { /* not a manifest we can read — not this pack */ }
|
|
1879
|
+
}
|
|
1880
|
+
return resolve(packId);
|
|
1695
1881
|
}
|
|
1696
1882
|
/**
|
|
1697
1883
|
* Read the deploy SSE stream, printing each progress frame's message live, and
|
|
@@ -1849,6 +2035,9 @@ function printDeploySuccess(id, version, t, r, listing, problems = 0) {
|
|
|
1849
2035
|
out.print(problems > 0
|
|
1850
2036
|
? `⚠ Deployed ${id}@${version} onto ${targetLabel(t)} WITH ${problems} failure(s) — data may be incomplete`
|
|
1851
2037
|
: `✓ Deployed ${id}@${version} and installed onto ${targetLabel(t)}`);
|
|
2038
|
+
if (r?.forced_over) {
|
|
2039
|
+
out.print(` ⚠ --force replaced ${r.forced_over === 'downgrade' ? 'a higher live version' : 'a version someone else published after your copy'}.`);
|
|
2040
|
+
}
|
|
1852
2041
|
if (r?.warning)
|
|
1853
2042
|
out.print(` ⚠ ${r.warning}`);
|
|
1854
2043
|
// This read `r.summary.records` / `.rules` / `.failed` and had been DEAD: the deploy route's
|
|
@@ -1908,7 +2097,7 @@ async function cmdSeed(flags) {
|
|
|
1908
2097
|
if (stepErrors.length) {
|
|
1909
2098
|
// A kind failed but the rest ran — the reconcile softens each step. Say which,
|
|
1910
2099
|
// and exit non-zero so a scripted `seed && chat` doesn't read as clean.
|
|
1911
|
-
out.err(`
|
|
2100
|
+
out.err(`
|
|
1912
2101
|
⚠ ${stepErrors.length} step${stepErrors.length === 1 ? '' : 's'} failed — data may be incomplete:`);
|
|
1913
2102
|
for (const e of stepErrors)
|
|
1914
2103
|
out.err(` • ${e}`);
|
|
@@ -1954,7 +2143,12 @@ async function cmdDeploy(flags) {
|
|
|
1954
2143
|
const endpoint = `${url}/api/self/p/packs/deploy`;
|
|
1955
2144
|
const seed = flags.seed === true;
|
|
1956
2145
|
const listing = resolveListingFlags(flags);
|
|
1957
|
-
|
|
2146
|
+
// What this copy was made from: the platform refuses when someone published since (`live_moved`).
|
|
2147
|
+
const base = deployBaseOf(readBase(packDir));
|
|
2148
|
+
const force = flags.force === true;
|
|
2149
|
+
const extras = [
|
|
2150
|
+
seed ? 'with demo seed' : null, listing ? LISTING_LABEL[listing] : null, force ? 'forced over any newer live version' : null,
|
|
2151
|
+
].filter(Boolean);
|
|
1958
2152
|
out.print(`→ Deploying ${id}@${version} (${Object.keys(files).length} files) to ${targetLabel(t)}${extras.length ? ` — ${extras.join(', ')}` : ''} …`);
|
|
1959
2153
|
const res = await fetchOrDie(endpoint, {
|
|
1960
2154
|
method: 'POST',
|
|
@@ -1963,7 +2157,9 @@ async function cmdDeploy(flags) {
|
|
|
1963
2157
|
headers: { 'content-type': 'application/json', accept: 'text/event-stream', ...authHeaders(t) },
|
|
1964
2158
|
// `listing` is omitted entirely unless asked for: an ordinary deploy says NOTHING about the
|
|
1965
2159
|
// marketplace, which is what keeps a test loop out of the operator review queue.
|
|
1966
|
-
body: JSON.stringify({
|
|
2160
|
+
body: JSON.stringify({
|
|
2161
|
+
files, blobs, seed, ...(listing ? { listing } : {}), ...(base ? { base } : {}), ...(force ? { force: true } : {}),
|
|
2162
|
+
}),
|
|
1967
2163
|
}, 'deploy');
|
|
1968
2164
|
// Streaming path — live install + seed progress (image generation can take a
|
|
1969
2165
|
// while, so `--seed` prints per-record / per-image lines as they happen).
|
|
@@ -1971,6 +2167,7 @@ async function cmdDeploy(flags) {
|
|
|
1971
2167
|
const { terminal: final, stepErrors } = await readDeployProgress(res.body);
|
|
1972
2168
|
if (!final || final.stage === 'error')
|
|
1973
2169
|
die(`deploy failed${final?.message ? `: ${final.message}` : ' (stream ended early)'}`);
|
|
2170
|
+
rememberDeploy(packDir, final, files, blobs);
|
|
1974
2171
|
// The count goes IN, so the headline itself carries the verdict. It used to be
|
|
1975
2172
|
// printed underneath a `✓ Deployed` line, which is what a reporting author read.
|
|
1976
2173
|
printDeploySuccess(id, version, t, final, { intent: listing, packDir }, stepErrors.length);
|
|
@@ -1996,11 +2193,28 @@ async function cmdDeploy(flags) {
|
|
|
1996
2193
|
json = text;
|
|
1997
2194
|
}
|
|
1998
2195
|
if (!res.ok) {
|
|
2196
|
+
// Someone published since this copy was made, or it is an older version: nothing changed on the
|
|
2197
|
+
// platform. The message says what happened and the way out (pull, or --force) — print it as words.
|
|
2198
|
+
if (res.status === 409 && (json?.code === 'live_moved' || json?.code === 'downgrade')) {
|
|
2199
|
+
out.err(`✗ ${json.error}`);
|
|
2200
|
+
if (json.code === 'live_moved' && typeof flags.dir === 'string') {
|
|
2201
|
+
out.err(` This copy is in ${flags.dir} — add \`--dir ${flags.dir}\` to the pull.`);
|
|
2202
|
+
}
|
|
2203
|
+
exitNow(1);
|
|
2204
|
+
}
|
|
2205
|
+
// The checks refused it: say what keeps running, then what to fix (`formatChecksRefusal`).
|
|
2206
|
+
const refusal = res.status === 400 ? formatChecksRefusal(json) : null;
|
|
2207
|
+
if (refusal) {
|
|
2208
|
+
for (const line of refusal)
|
|
2209
|
+
out.err(line);
|
|
2210
|
+
exitNow(1);
|
|
2211
|
+
}
|
|
1999
2212
|
out.err(`✗ deploy failed (HTTP ${res.status})`);
|
|
2000
2213
|
printAuthHint(res.status, url);
|
|
2001
2214
|
out.err(typeof json === 'string' ? json : JSON.stringify(json, null, 2));
|
|
2002
2215
|
exitNow(1);
|
|
2003
2216
|
}
|
|
2217
|
+
rememberDeploy(packDir, json, files, blobs);
|
|
2004
2218
|
// This path had NO step-error check at all. With no SSE frames to read, the only
|
|
2005
2219
|
// evidence a seed kind threw is `seed_failed` in the body — so a broken seed printed
|
|
2006
2220
|
// a clean `✓ Deployed` here even after the streaming path was fixed in 0.1.14.
|
|
@@ -2013,6 +2227,15 @@ async function cmdDeploy(flags) {
|
|
|
2013
2227
|
exitNow(1);
|
|
2014
2228
|
}
|
|
2015
2229
|
}
|
|
2230
|
+
/**
|
|
2231
|
+
* After a deploy: this folder's new base is what the project now runs. Written even when a seed step
|
|
2232
|
+
* failed — the pack itself IS installed. A platform too old to report `content_sha` leaves the base alone.
|
|
2233
|
+
*/
|
|
2234
|
+
function rememberDeploy(packDir, r, files, blobs) {
|
|
2235
|
+
if (typeof r?.content_sha !== 'string' || typeof r?.pack_id !== 'string' || typeof r?.version !== 'string')
|
|
2236
|
+
return;
|
|
2237
|
+
writeBase(packDir, { pack_id: r.pack_id, version: r.version, content_sha: r.content_sha, files: hashBundle(files, blobs) });
|
|
2238
|
+
}
|
|
2016
2239
|
/**
|
|
2017
2240
|
* The QUALIFIED pack id (`<owner>.<name>`) for a manifest's bare name.
|
|
2018
2241
|
*
|
|
@@ -2092,6 +2315,13 @@ async function cmdStatus(flags) {
|
|
|
2092
2315
|
const shortSha = (s) => (typeof s === 'string' && s ? s.slice(0, 12) + '…' : '(none)');
|
|
2093
2316
|
out.print(` live on instance : registered=${json.registered} loaded=${shortSha(json.loaded_content_sha)}`);
|
|
2094
2317
|
out.print(` catalog artifact : ${shortSha(json.catalog_content_sha)}${json.origin ? ` origin=${json.origin}` : ''}`);
|
|
2318
|
+
// The copy on disk against what runs: the question a deploy is about to be judged on (`live_moved`).
|
|
2319
|
+
const base = readBase(packDir);
|
|
2320
|
+
out.print(` your copy : ${!base || base.pack_id !== id
|
|
2321
|
+
? `no ${BASE_FILE} — run \`octwin pull ${id}\` once, so a deploy can tell when someone else published`
|
|
2322
|
+
: base.content_sha === json.catalog_content_sha
|
|
2323
|
+
? `from ${base.version} (${shortSha(base.content_sha)}) — the live content`
|
|
2324
|
+
: `from ${base.version} (${shortSha(base.content_sha)}) — the live content MOVED since; run \`octwin pull ${id}\` before you deploy`}`);
|
|
2095
2325
|
out.print(` flows : ${(json.flows ?? []).join(', ') || '(none)'}`);
|
|
2096
2326
|
// Every line above is process-global — `registered` is true for a pack ANY
|
|
2097
2327
|
// project on this instance loaded. `dispatches` is the project-scoped answer to
|
|
@@ -2322,9 +2552,10 @@ async function cmdPlatformKb(flags) {
|
|
|
2322
2552
|
writeFileSync(join(outDir, 'OUTLINE.md'), outline, 'utf8');
|
|
2323
2553
|
// Persist `content_hash` too — the staleness observer (`notifyIfKbStale`) reads
|
|
2324
2554
|
// 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');
|
|
2555
|
+
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
2556
|
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 ?? '?'}`
|
|
2557
|
+
out.print(` ${mdCount} markdown docs + ${catalogCount} catalogs (${entryCount} entries, one file each) — reference version ${bundle.version ?? '?'}`
|
|
2558
|
+
+ `, published by platform ${bundle.platform_version ?? 'unknown'}`);
|
|
2328
2559
|
out.print(` Three maps: INDEX.md (the corpus) · SYMBOLS.md (${symbols.length} names → their file, grep it)`
|
|
2329
2560
|
+ `${outline ? ' · OUTLINE.md (every heading, with line numbers)' : ''}`);
|
|
2330
2561
|
if (!outline) {
|
|
@@ -2533,12 +2764,16 @@ async function cmdRecords(flags) {
|
|
|
2533
2764
|
out.print('No AI Agent is loaded in this project.');
|
|
2534
2765
|
return;
|
|
2535
2766
|
}
|
|
2536
|
-
|
|
2537
|
-
|
|
2767
|
+
// The LISTED types, as the console's switcher shows them: the catalog carries every platform
|
|
2768
|
+
// entity for lookups by name. A server older than the `listed` field sent declared types only,
|
|
2769
|
+
// so a missing one reads as listed.
|
|
2770
|
+
const ents = (json?.entities ?? []).filter(e => e.listed !== false);
|
|
2771
|
+
if (json?.has_xrm === false && ents.length === 0) {
|
|
2772
|
+
// No record TYPES of its own and no platform records yet — the platform's (an order, a case)
|
|
2773
|
+
// still read by name.
|
|
2538
2774
|
out.print('This pack declares no record types of its own. Platform records still read by name: octwin records order | product | case | …');
|
|
2539
2775
|
return;
|
|
2540
2776
|
}
|
|
2541
|
-
const ents = (json?.entities ?? []);
|
|
2542
2777
|
if (ents.length === 0) {
|
|
2543
2778
|
out.print('No entities visible — mint a token with the `records:read` scope to inspect data.');
|
|
2544
2779
|
return;
|
|
@@ -4092,12 +4327,15 @@ async function cmdAgents(flags) {
|
|
|
4092
4327
|
* docs/BACKLOG.md → "Headless payment settle" (a `--settle` flag lands here once
|
|
4093
4328
|
* the platform grows a manual-driver-only reconcile endpoint). */
|
|
4094
4329
|
function printPaymentNote(paymentStatus) {
|
|
4095
|
-
if (paymentStatus
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4330
|
+
if (paymentStatus === 'none') {
|
|
4331
|
+
out.print(" → payment_status 'none': no payment was opened. Without a hosted-checkout gateway");
|
|
4332
|
+
out.print(' `payment_request` takes its `empty` port and leaves the order alone — the flow should offer');
|
|
4333
|
+
out.print(' pay-on-delivery. Refund is the one operator-driven move.');
|
|
4334
|
+
}
|
|
4335
|
+
else if (paymentStatus === 'pending') {
|
|
4336
|
+
out.print(" → payment_status 'pending': a checkout was opened and the gateway's webhook has not");
|
|
4337
|
+
out.print(' confirmed it yet. The forward payment lifecycle is WEBHOOK-owned (not patchable).');
|
|
4338
|
+
}
|
|
4101
4339
|
}
|
|
4102
4340
|
/** `octwin orders [referenceId] [--status s] [--payment p] [--limit n] [--json]` —
|
|
4103
4341
|
* the orders a conversation created: money breakdown, payment state, allowed
|
|
@@ -4108,8 +4346,8 @@ const ORDER_VERBS = new Set(['transition', 'refund']);
|
|
|
4108
4346
|
* The write half of `octwin orders` — fulfilment transitions and refunds.
|
|
4109
4347
|
*
|
|
4110
4348
|
* `payment_status` is deliberately NOT settable: the forward payment lifecycle is
|
|
4111
|
-
* webhook-owned
|
|
4112
|
-
*
|
|
4349
|
+
* webhook-owned: `none` is an order no payment was opened for (no gateway), `pending` one whose
|
|
4350
|
+
* checkout the gateway has not confirmed yet (`printPaymentNote`).
|
|
4113
4351
|
*/
|
|
4114
4352
|
async function cmdOrdersWrite(flags) {
|
|
4115
4353
|
const t = resolveTarget(flags);
|
|
@@ -4694,49 +4932,51 @@ async function cmdScheduling(flags) {
|
|
|
4694
4932
|
out.print(` upcoming slots: ${json?.upcoming_slots ?? 0} booked seats: ${json?.booked_seats ?? 0}`);
|
|
4695
4933
|
out.print('\nSlots for one resource: octwin scheduling --slots <resourceRecordId> (ids: octwin records <entity>)');
|
|
4696
4934
|
}
|
|
4697
|
-
// ── automation:
|
|
4935
|
+
// ── automation: the project's rules + campaigns ──────────────────────────────
|
|
4698
4936
|
/**
|
|
4699
|
-
* Verbs
|
|
4937
|
+
* Verbs of `octwin automation` — a view (`coming`, `happened`, `why`, `campaigns`) or an act (`run`, `pause`,
|
|
4938
|
+
* `resume`, `send`) — not a rule key.
|
|
4700
4939
|
*
|
|
4701
|
-
*
|
|
4702
|
-
*
|
|
4703
|
-
*
|
|
4704
|
-
*
|
|
4705
|
-
* say the right thing.
|
|
4940
|
+
* Until 0.9.0 the command read JOB ROWS (`…/automation/jobs`). Since the platform's migration 126 those are only its
|
|
4941
|
+
* own two sweeps: a pack's `rules:` — and its older `segment` / `stale_records` / `sla_sweep` jobs and `on:` hooks,
|
|
4942
|
+
* which run as compiled rules — were invisible in the terminal. It reads the rules routes the console's Outbound →
|
|
4943
|
+
* Automatic tab reads now, so the words are the server's.
|
|
4706
4944
|
*/
|
|
4707
|
-
const AUTOMATION_VERBS = new Set(['run', 'pause', 'resume', 'campaigns', 'send']);
|
|
4945
|
+
const AUTOMATION_VERBS = new Set(['coming', 'happened', 'why', 'run', 'pause', 'resume', 'campaigns', 'send']);
|
|
4946
|
+
const AUTOMATION_WRITES = new Set(['run', 'pause', 'resume', 'send']);
|
|
4708
4947
|
/**
|
|
4709
|
-
* Turn
|
|
4710
|
-
*
|
|
4711
|
-
* (`UUID_RE` is the one already declared for `--media`, deliberately reused rather
|
|
4712
|
-
* than a second copy of the same pattern.)
|
|
4713
|
-
*
|
|
4714
|
-
* The write routes take a UUID path param and reject anything else with a bare
|
|
4715
|
-
* *"Malformed identifier in the URL — expected a UUID"* 400. But the thing an author
|
|
4716
|
-
* has in front of them is the declaration KEY (`cart_recovery_nudge`) — that is what
|
|
4717
|
-
* the list prints, and it is the name in their own YAML. Measured: passing the key
|
|
4718
|
-
* 400s on all three write verbs.
|
|
4719
|
-
*
|
|
4720
|
-
* So the key is resolved here, against the list route, rather than documented as a
|
|
4721
|
-
* gotcha. A UUID passes straight through, and an unknown key fails naming the keys
|
|
4722
|
-
* that DO exist — which is the answer to the question the author is actually asking.
|
|
4948
|
+
* Turn the campaign an author typed into the UUID the send route demands — the id `octwin automation campaigns`
|
|
4949
|
+
* prints, or the campaign's key. An unknown one fails naming those that exist.
|
|
4723
4950
|
*/
|
|
4724
|
-
async function
|
|
4951
|
+
async function resolveCampaignId(typed, t) {
|
|
4725
4952
|
if (UUID_RE.test(typed))
|
|
4726
4953
|
return typed;
|
|
4727
|
-
const {
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4954
|
+
const { url } = t;
|
|
4955
|
+
const base = `${url}/api/self/p/automation`;
|
|
4956
|
+
const { status, json } = await apiGet(`${base}/campaigns`, t);
|
|
4957
|
+
if (status !== 200)
|
|
4958
|
+
die(`could not resolve '${typed}' — reading campaigns failed (HTTP ${status})${errDetail(json)}`);
|
|
4731
4959
|
const rows = readPage(json).rows;
|
|
4732
4960
|
const hit = rows.find(r => r.key === typed || r.id === typed);
|
|
4733
4961
|
if (hit?.id)
|
|
4734
4962
|
return hit.id;
|
|
4735
4963
|
const keys = rows.map(r => r.key ?? r.id).filter(Boolean);
|
|
4736
|
-
die(`no ${
|
|
4737
|
-
+ (keys.length ? ` Available: ${keys.join(', ')}` : ` This project declares none.`));
|
|
4964
|
+
die(`no campaign '${typed}' in this project.` + (keys.length ? ` Available: ${keys.join(', ')}` : ' This project has none.'));
|
|
4738
4965
|
}
|
|
4739
|
-
/** `
|
|
4966
|
+
/** The rule an author typed, against the project's rules (`resolveRuleKey`) — or the command dies naming them. */
|
|
4967
|
+
async function resolveRule(typed, t) {
|
|
4968
|
+
const { url } = t;
|
|
4969
|
+
const base = `${url}/api/self/p/automation`;
|
|
4970
|
+
const { status, json } = await apiGet(`${base}/rules`, t);
|
|
4971
|
+
if (status !== 200)
|
|
4972
|
+
die(`could not resolve '${typed}' — reading the rules failed (HTTP ${status})${errDetail(json)}`);
|
|
4973
|
+
const keys = (json?.rules ?? []).map(r => r.key).filter((k) => typeof k === 'string');
|
|
4974
|
+
const r = resolveRuleKey(typed, keys);
|
|
4975
|
+
if ('error' in r)
|
|
4976
|
+
die(r.error);
|
|
4977
|
+
return r.key;
|
|
4978
|
+
}
|
|
4979
|
+
/** `pause` / `resume` / `run` a rule, `send` a campaign — the writes behind `octwin automation`. */
|
|
4740
4980
|
async function cmdAutomationWrite(flags) {
|
|
4741
4981
|
const t = resolveTarget(flags);
|
|
4742
4982
|
const { url } = t;
|
|
@@ -4744,88 +4984,212 @@ async function cmdAutomationWrite(flags) {
|
|
|
4744
4984
|
const verb = flags._[0];
|
|
4745
4985
|
const typed = flags._[1];
|
|
4746
4986
|
const asJson = flags.json === true;
|
|
4747
|
-
if (
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4987
|
+
if (verb === 'send') {
|
|
4988
|
+
if (!typed)
|
|
4989
|
+
die('usage: octwin automation send <campaignId> (ids: octwin automation campaigns)');
|
|
4990
|
+
const id = await resolveCampaignId(typed, t);
|
|
4991
|
+
if (!asJson)
|
|
4992
|
+
out.print(`→ Sending campaign '${typed}' in ${targetLabel(t)} …`);
|
|
4993
|
+
const { status, json } = await apiSend('POST', `${base}/campaigns/${encodeURIComponent(id)}/send`, undefined, t);
|
|
4752
4994
|
if (status === 404)
|
|
4753
|
-
die(`
|
|
4754
|
-
// RBAC re-checks the ACTION on the job, so a 403 here can be a grant gap
|
|
4755
|
-
// rather than a missing scope — same caveat as a record write.
|
|
4995
|
+
die(`campaign '${typed}' not found in ${targetLabel(t)} (ids: octwin automation campaigns)`);
|
|
4756
4996
|
if (status !== 200)
|
|
4757
|
-
writeFail(
|
|
4997
|
+
writeFail(`send campaign '${typed}'`, status, json, url, true);
|
|
4758
4998
|
if (asJson) {
|
|
4759
4999
|
out.result(json);
|
|
4760
5000
|
return;
|
|
4761
5001
|
}
|
|
4762
|
-
const
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
5002
|
+
const r = json?.result ?? {};
|
|
5003
|
+
const already = Number(r.already_queued ?? 0);
|
|
5004
|
+
out.print(`✓ campaign '${typed}': matched ${r.matched ?? 0}, enqueued ${r.enqueued ?? 0}${already ? `, ${already} already had it` : ''} [${r.stage ?? '?'}]`);
|
|
5005
|
+
// A campaign that stopped half-way is FINISHED by the same command: it queues only the contacts
|
|
5006
|
+
// with no delivery yet, so `already` is the earlier send's work, not a skip.
|
|
5007
|
+
if (already)
|
|
5008
|
+
out.print(' finished an earlier send — each contact gets this campaign once');
|
|
5009
|
+
if ((r.enqueued ?? 0) + already < (r.matched ?? 0))
|
|
5010
|
+
out.print(' partial: some matched records were not enqueued (records sharing a contact, or a record with none)');
|
|
5011
|
+
out.print(' enqueued ≠ delivered — watch the sends land with `octwin automation happened`');
|
|
5012
|
+
return;
|
|
5013
|
+
}
|
|
5014
|
+
if (!typed)
|
|
5015
|
+
die(`usage: octwin automation ${verb} <ruleKey> (keys: octwin automation)`);
|
|
5016
|
+
const key = await resolveRule(typed, t);
|
|
5017
|
+
const path = `${base}/rules/${encodeURIComponent(key)}/${verb}`;
|
|
5018
|
+
if (!asJson && verb === 'run')
|
|
5019
|
+
out.print(`→ Running rule '${key}' in ${targetLabel(t)} …`);
|
|
5020
|
+
const { status, json } = await apiSend('POST', path, undefined, t);
|
|
5021
|
+
if (status === 404)
|
|
5022
|
+
die(`rule '${key}' not found in ${targetLabel(t)} (keys: octwin automation)`);
|
|
5023
|
+
// RBAC re-checks the ACTION on `automation.rule`, so a 403 can be a grant gap rather than a missing scope; a 409
|
|
5024
|
+
// is the rule saying why not (only a rule on a timetable can be run now) — the server's words.
|
|
5025
|
+
if (status !== 200)
|
|
5026
|
+
writeFail(`${verb} rule '${key}'`, status, json, url, true);
|
|
5027
|
+
if (asJson) {
|
|
5028
|
+
out.result(json);
|
|
4766
5029
|
return;
|
|
4767
5030
|
}
|
|
4768
5031
|
if (verb === 'run') {
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
5032
|
+
out.print(`✓ '${key}' runs now — it reaches ${json?.reaches ?? 0} record(s); next time after this run: ${shortTime(json?.next)}`);
|
|
5033
|
+
out.print(' see what it did: octwin automation happened');
|
|
5034
|
+
}
|
|
5035
|
+
else if (verb === 'pause') {
|
|
5036
|
+
out.print(`✓ '${key}' is paused — its times still arm, and nothing runs (nothing is caught up when it resumes)`);
|
|
5037
|
+
}
|
|
5038
|
+
else {
|
|
5039
|
+
out.print(json?.resumed === false ? ` '${key}' was not paused` : `✓ '${key}' runs again`);
|
|
5040
|
+
}
|
|
5041
|
+
}
|
|
5042
|
+
/**
|
|
5043
|
+
* A campaign's delivery progress — the per-status counts of its deliveries. Not sent yet (a draft) is all zeros, and
|
|
5044
|
+
* reads as a dash rather than "sent 0".
|
|
5045
|
+
*/
|
|
5046
|
+
function campaignProgressLine(p) {
|
|
5047
|
+
const n = (k) => Number(p?.[k] ?? 0);
|
|
5048
|
+
if (n('sent') + n('failed') + n('pending') + n('cancelled') === 0)
|
|
5049
|
+
return '—'.padEnd(28);
|
|
5050
|
+
return `sent ${n('sent')} · failed ${n('failed')} · pending ${n('pending')}${n('cancelled') ? ` · cancelled ${n('cancelled')}` : ''}`.padEnd(28);
|
|
5051
|
+
}
|
|
5052
|
+
/** A line's record and customer, as one tail — `· Booking #7 · Sara`. */
|
|
5053
|
+
function lineTail(l) {
|
|
5054
|
+
const rec = l?.record ? ` · ${l.record.noun ?? l.record.entity ?? 'record'} #${l.record.number ?? '?'}` : '';
|
|
5055
|
+
const who = l?.contact ? ` · ${l.contact.name ?? 'customer'}` : '';
|
|
5056
|
+
return `${rec}${who}`;
|
|
5057
|
+
}
|
|
5058
|
+
/** A query string of the values that are set — `range=week&q=cairo`. Each caller names the flags it passes. */
|
|
5059
|
+
function setQs(values) {
|
|
5060
|
+
const p = new URLSearchParams();
|
|
5061
|
+
for (const [k, v] of Object.entries(values))
|
|
5062
|
+
if (typeof v === 'string')
|
|
5063
|
+
p.set(k, v);
|
|
5064
|
+
return p.toString();
|
|
5065
|
+
}
|
|
5066
|
+
/** `octwin automation coming` — every time still to come: a rule's, or a flow's own message (a send). */
|
|
5067
|
+
async function automationComing(flags, t) {
|
|
5068
|
+
const { url } = t;
|
|
5069
|
+
const base = `${url}/api/self/p/automation`;
|
|
5070
|
+
const asJson = flags.json === true;
|
|
5071
|
+
if (!asJson)
|
|
5072
|
+
out.print(`→ Reading what is coming up in ${targetLabel(t)} …`);
|
|
5073
|
+
const qs = setQs({ range: flags.range, entity: flags.entity, q: flags.q, cursor: flags.cursor });
|
|
5074
|
+
const { status, json } = await apiGet(`${base}/upcoming?${qs || 'range=week'}`, t);
|
|
5075
|
+
if (status !== 200)
|
|
5076
|
+
die(`could not read what is coming up (HTTP ${status})${errDetail(json)}${authFailureDetail(status, t.url)}`);
|
|
5077
|
+
if (asJson) {
|
|
5078
|
+
out.result(json);
|
|
4788
5079
|
return;
|
|
4789
5080
|
}
|
|
4790
|
-
|
|
5081
|
+
const lines = (json?.lines ?? []);
|
|
5082
|
+
out.print(`Coming up in ${targetLabel(t)} (${typeof flags.range === 'string' ? flags.range : 'week'}): ${json?.total ?? lines.length}`);
|
|
5083
|
+
if (lines.length === 0)
|
|
5084
|
+
out.print(' (nothing — a rule\'s time arms when its record is made or changed)');
|
|
5085
|
+
for (const l of lines) {
|
|
5086
|
+
const mark = l.skipped ? ' [skipped]' : l.paused ? ' [paused]' : '';
|
|
5087
|
+
out.print(` ${shortTime(l.at)} ${phraseText(l.headline)}${lineTail(l)}${mark} ${l.kind === 'queued' ? 'message' : 'timer'} ${l.id}`);
|
|
5088
|
+
}
|
|
5089
|
+
if (json?.next)
|
|
5090
|
+
out.print(`\nMore: octwin automation coming --cursor ${json.next}`);
|
|
5091
|
+
if (lines.length)
|
|
5092
|
+
out.print('Why one goes, and what it will say: octwin automation why <id>');
|
|
5093
|
+
}
|
|
5094
|
+
/** `octwin automation happened` — every fire of the period: its final word, and why a message did not go. */
|
|
5095
|
+
async function automationHappened(flags, t) {
|
|
5096
|
+
const { url } = t;
|
|
5097
|
+
const base = `${url}/api/self/p/automation`;
|
|
5098
|
+
const asJson = flags.json === true;
|
|
4791
5099
|
if (!asJson)
|
|
4792
|
-
out.print(`→
|
|
4793
|
-
const
|
|
4794
|
-
|
|
4795
|
-
die(`campaign '${typed}' not found in ${targetLabel(t)} (ids: octwin automation campaigns)`);
|
|
5100
|
+
out.print(`→ Reading what happened in ${targetLabel(t)} …`);
|
|
5101
|
+
const qs = setQs({ chip: flags.chip, period: flags.period, q: flags.q, cursor: flags.cursor });
|
|
5102
|
+
const { status, json } = await apiGet(`${base}/history?${qs || 'period=7d'}`, t);
|
|
4796
5103
|
if (status !== 200)
|
|
4797
|
-
|
|
5104
|
+
die(`could not read what happened (HTTP ${status})${errDetail(json)}${authFailureDetail(status, t.url)}`);
|
|
4798
5105
|
if (asJson) {
|
|
4799
5106
|
out.result(json);
|
|
4800
5107
|
return;
|
|
4801
5108
|
}
|
|
4802
|
-
const
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
5109
|
+
const lines = (json?.lines ?? []);
|
|
5110
|
+
const c = json?.counts ?? {};
|
|
5111
|
+
out.print(`Happened in ${targetLabel(t)} (${typeof flags.period === 'string' ? flags.period : '7d'}): ${json?.total ?? lines.length}`
|
|
5112
|
+
+ ` — ${c.not_delivered ?? 0} not delivered, ${c.failed ?? 0} failed, ${c.skipped ?? 0} skipped`);
|
|
5113
|
+
if (lines.length === 0)
|
|
5114
|
+
out.print(' (nothing yet)');
|
|
5115
|
+
for (const l of lines) {
|
|
5116
|
+
const why = l.code || l.reason ? ` (${l.code ?? l.reason})` : '';
|
|
5117
|
+
out.print(` ${shortTime(l.at)} ${String(l.outcome ?? '—').padEnd(13)} ${phraseText(l.headline)}${lineTail(l)}${why} ${l.id}`);
|
|
5118
|
+
}
|
|
5119
|
+
if (json?.next)
|
|
5120
|
+
out.print(`\nMore: octwin automation happened --cursor ${json.next}`);
|
|
5121
|
+
if (lines.length)
|
|
5122
|
+
out.print('Each step and the words sent: octwin automation why <id>');
|
|
5123
|
+
}
|
|
5124
|
+
/**
|
|
5125
|
+
* `octwin automation why <id>` — one line's *Why?*, as the console's drawer: the rule in words, each step with its
|
|
5126
|
+
* outcome, the words the customer reads. The line's kind picks the route (`--kind timer|fire|queued`); without it
|
|
5127
|
+
* each is asked in turn — a coming time, a fire, a send.
|
|
5128
|
+
*/
|
|
5129
|
+
async function automationWhy(flags, t) {
|
|
5130
|
+
const { url } = t;
|
|
5131
|
+
const base = `${url}/api/self/p/automation`;
|
|
5132
|
+
const asJson = flags.json === true;
|
|
5133
|
+
const id = flags._[1];
|
|
5134
|
+
if (!id)
|
|
5135
|
+
die('usage: octwin automation why <id> [--kind timer|fire|queued] (ids: octwin automation coming | happened)');
|
|
5136
|
+
const kinds = typeof flags.kind === 'string' ? [flags.kind] : ['timer', 'fire', 'queued'];
|
|
5137
|
+
for (const kind of kinds) {
|
|
5138
|
+
if (!['timer', 'fire', 'queued'].includes(kind))
|
|
5139
|
+
die(`--kind is timer, fire or queued (got '${kind}')`);
|
|
5140
|
+
const { status, json } = await apiGet(`${base}/why/${kind}/${encodeURIComponent(id)}`, t);
|
|
5141
|
+
if (status === 404)
|
|
5142
|
+
continue;
|
|
5143
|
+
if (status !== 200)
|
|
5144
|
+
die(`could not read why (HTTP ${status})${errDetail(json)}${authFailureDetail(status, t.url)}`);
|
|
5145
|
+
if (asJson) {
|
|
5146
|
+
out.result(json);
|
|
5147
|
+
return;
|
|
5148
|
+
}
|
|
5149
|
+
const v = json ?? {};
|
|
5150
|
+
out.print(`${phraseText(v.line?.headline)}${lineTail(v.line)} — ${shortTime(v.line?.at)}`);
|
|
5151
|
+
if (v.rule)
|
|
5152
|
+
out.print(` rule ${v.rule.key}: ${phraseText(v.rule.sentence)}`);
|
|
5153
|
+
if (v.line?.outcome)
|
|
5154
|
+
out.print(` outcome: ${v.line.outcome}${v.line.code ? ` (${v.line.code})` : ''}${v.line.reason ? ` — ${v.line.reason}` : ''}`);
|
|
5155
|
+
for (const check of (v.checks ?? []))
|
|
5156
|
+
out.print(` checked again before it acts: ${phraseText(check)}`);
|
|
5157
|
+
if (v.words?.text)
|
|
5158
|
+
out.print(` the customer reads${v.words.tag ? ` (${v.words.tag})` : ''}: ${v.words.text}`);
|
|
5159
|
+
for (const s of (v.steps ?? [])) {
|
|
5160
|
+
const send = s.send ? ` · ${s.send.status}${s.send.reason ? ` (${s.send.reason})` : ''}` : '';
|
|
5161
|
+
out.print(` step ${s.verb}: ${s.skipped ? `held back (${s.skipped})` : s.state}${send}${s.detail ? ` — ${s.detail}` : ''}`);
|
|
5162
|
+
}
|
|
5163
|
+
if (v.queued)
|
|
5164
|
+
out.print(` a flow's own message: ${v.queued.status}${v.queued.error ? ` — ${v.queued.error}` : ''}`);
|
|
5165
|
+
return;
|
|
5166
|
+
}
|
|
5167
|
+
die(`nothing under '${id}' in ${targetLabel(t)} — a coming time may have just happened, and a fire is kept 30 days`);
|
|
4807
5168
|
}
|
|
4808
5169
|
/**
|
|
4809
|
-
* `octwin automation [--
|
|
4810
|
-
* produced, with their last result, plus the health counts.
|
|
5170
|
+
* `octwin automation [--json]` — the project's rules, each in the server's words, under the week's numbers.
|
|
4811
5171
|
*
|
|
4812
|
-
* Needs `automation:read`. The
|
|
4813
|
-
*
|
|
4814
|
-
* filtering the page — past the cap a client-side count would depend on the cap
|
|
4815
|
-
* instead of the data.
|
|
5172
|
+
* Needs `automation:read`. The words are the console's (`rules/describe.ts` writes each rule as a sentence), so the
|
|
5173
|
+
* terminal says what the Automatic tab says.
|
|
4816
5174
|
*/
|
|
4817
5175
|
async function cmdAutomation(flags) {
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
5176
|
+
const verb = typeof flags._[0] === 'string' ? flags._[0] : null;
|
|
5177
|
+
if (verb && AUTOMATION_WRITES.has(verb))
|
|
5178
|
+
return cmdAutomationWrite(flags);
|
|
5179
|
+
if (verb && !AUTOMATION_VERBS.has(verb)) {
|
|
5180
|
+
die(`unknown automation verb '${verb}' — octwin automation [coming | happened | why <id> | campaigns | run|pause|resume <ruleKey> | send <campaignId>]`);
|
|
4822
5181
|
}
|
|
4823
5182
|
const t = resolveTarget(flags);
|
|
4824
5183
|
const { url } = t;
|
|
4825
5184
|
const base = `${url}/api/self/p/automation`;
|
|
4826
5185
|
const asJson = flags.json === true;
|
|
4827
|
-
|
|
4828
|
-
|
|
5186
|
+
if (verb === 'coming')
|
|
5187
|
+
return automationComing(flags, t);
|
|
5188
|
+
if (verb === 'happened')
|
|
5189
|
+
return automationHappened(flags, t);
|
|
5190
|
+
if (verb === 'why')
|
|
5191
|
+
return automationWhy(flags, t);
|
|
5192
|
+
if (verb === 'campaigns' || flags.campaigns === true) {
|
|
4829
5193
|
if (!asJson)
|
|
4830
5194
|
out.print(`→ Reading campaigns from ${targetLabel(t)} …`);
|
|
4831
5195
|
const { status, json } = await apiGet(`${base}/campaigns?${pagingQs(flags)}`, t);
|
|
@@ -4838,50 +5202,51 @@ async function cmdAutomation(flags) {
|
|
|
4838
5202
|
const page = readPage(json);
|
|
4839
5203
|
out.print(`Campaigns in ${targetLabel(t)}: ${page.total ?? page.rows.length}`);
|
|
4840
5204
|
if (page.rows.length === 0)
|
|
4841
|
-
out.print(' (none — a
|
|
5205
|
+
out.print(' (none — a flow drafts one with the `campaign_create` step)');
|
|
5206
|
+
// What the route returns, field for field: `{ id, record_number, name, entity, stage, progress }`.
|
|
5207
|
+
// It read `label` / `key` / `audience_size` / `matched` until 2026-10-11 — none of which a campaign
|
|
5208
|
+
// row carries — so every row printed its uuid twice and "—".
|
|
4842
5209
|
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}`);
|
|
5210
|
+
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
5211
|
}
|
|
4846
5212
|
const more = morePageHint(page, 'octwin automation campaigns');
|
|
4847
5213
|
if (more)
|
|
4848
5214
|
out.print(more);
|
|
4849
|
-
out.print('\nSend one
|
|
5215
|
+
out.print('\nSend a draft, or finish one stuck at `sending`: octwin automation send <campaignId> (needs automation:write; each contact gets it once)');
|
|
4850
5216
|
return;
|
|
4851
5217
|
}
|
|
4852
5218
|
if (!asJson)
|
|
4853
|
-
out.print(`→ Reading
|
|
4854
|
-
const [
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
if (jobs.status !== 200)
|
|
4859
|
-
die(`could not read automation jobs (HTTP ${jobs.status})${errDetail(jobs.json)}${authFailureDetail(jobs.status, url)}`);
|
|
5219
|
+
out.print(`→ Reading the rules of ${targetLabel(t)} …`);
|
|
5220
|
+
const [rules, summary] = await Promise.all([apiGet(`${base}/rules`, t), apiGet(`${base}/summary`, t)]);
|
|
5221
|
+
if (rules.status !== 200)
|
|
5222
|
+
die(`could not read the rules (HTTP ${rules.status})${errDetail(rules.json)}${authFailureDetail(rules.status, url)}`);
|
|
5223
|
+
const s = summary.status === 200 ? summary.json : null;
|
|
4860
5224
|
if (asJson) {
|
|
4861
|
-
out.result({
|
|
5225
|
+
out.result({ summary: s, rules: rules.json?.rules ?? [] });
|
|
4862
5226
|
return;
|
|
4863
5227
|
}
|
|
4864
|
-
const
|
|
4865
|
-
const
|
|
4866
|
-
out.print(`Automation in ${targetLabel(t)}: ${
|
|
4867
|
-
+ ` — ${
|
|
4868
|
-
|
|
4869
|
-
out.print(
|
|
4870
|
-
out.print(' No `automation.yaml` block → no jobs. `octwin deploy` installs them.)');
|
|
4871
|
-
return;
|
|
5228
|
+
const list = (rules.json?.rules ?? []);
|
|
5229
|
+
const w = s?.week ?? {};
|
|
5230
|
+
out.print(`Automation in ${targetLabel(t)}: ${list.length} rule(s)`
|
|
5231
|
+
+ (s ? ` — this week ${w.sent ?? 0} sent, ${w.notDelivered ?? 0} not delivered, ${w.failed ?? 0} failed, ${s.counts?.upcoming ?? 0} coming up` : ''));
|
|
5232
|
+
for (const n of (s?.needsLook ?? [])) {
|
|
5233
|
+
out.print(` ⚠ ${n.rule?.key ?? '?'}: ${n.kind === 'paused' ? 'paused' : n.kind === 'not_delivered' ? `${n.n} not delivered${n.codes?.length ? ` (${n.codes.join(', ')})` : ''}` : `${n.n} failed`}`);
|
|
4872
5234
|
}
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
const result = j.last_result
|
|
4877
|
-
? ` matched ${r.matched ?? 0}/acted ${r.acted ?? 0}${(r.errors ?? 0) > 0 ? `/ERRORS ${r.errors}` : ''}`
|
|
4878
|
-
: '';
|
|
4879
|
-
out.print(` ${String(j.key ?? j.id).padEnd(26)} ${String(j.status).padEnd(7)} ${j.kind}/${j.entity ?? '—'}`
|
|
4880
|
-
+ ` every ${j.interval_seconds}s ${ran}${result}`);
|
|
5235
|
+
if (list.length === 0) {
|
|
5236
|
+
out.print(' (none — a pack writes rules in automation.yaml `rules:`; `octwin deploy` installs them)');
|
|
5237
|
+
return;
|
|
4881
5238
|
}
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
5239
|
+
for (const r of list) {
|
|
5240
|
+
const wk = r.week ?? {};
|
|
5241
|
+
out.print(` ${String(r.key)} [${r.paused ? 'paused' : 'on'}] next ${shortTime(r.next)}`
|
|
5242
|
+
+ ` · week ${wk.total ?? 0}: ${wk.sent ?? 0} sent, ${wk.notDelivered ?? 0} not delivered, ${wk.failed ?? 0} failed, ${wk.skipped ?? 0} skipped`);
|
|
5243
|
+
out.print(` ${phraseText(r.sentence)}`);
|
|
5244
|
+
}
|
|
5245
|
+
out.print('\nComing up: octwin automation coming [--range today|tomorrow|week|all]');
|
|
5246
|
+
out.print('Happened: octwin automation happened [--chip not_delivered|failed|skipped|moves|tasks] [--period 7d|30d]');
|
|
5247
|
+
out.print('Why one line: octwin automation why <id>');
|
|
5248
|
+
out.print('Act: octwin automation pause|resume|run <ruleKey> (needs automation:write)');
|
|
5249
|
+
out.print('Campaigns: octwin automation campaigns');
|
|
4885
5250
|
}
|
|
4886
5251
|
// ── integrations: declared connections, their credentials, and the delivery log ──
|
|
4887
5252
|
/** Verbs that act on a connection or a delivery, rather than naming one. */
|
|
@@ -5334,8 +5699,214 @@ async function cmdJourneys(flags) {
|
|
|
5334
5699
|
}
|
|
5335
5700
|
// ── performance: the project's business indicators ──────────────────────────
|
|
5336
5701
|
/**
|
|
5337
|
-
* `octwin
|
|
5338
|
-
*
|
|
5702
|
+
* `octwin charts [<key>] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--json]` — the pack's own `charts:`
|
|
5703
|
+
* (xrm.yaml), as the owner's dashboard draws them: the list, or one chart's figure, parts or points.
|
|
5704
|
+
* Needs `records:read`; a `view` grant on each chart's entity decides which ones the token sees.
|
|
5705
|
+
*/
|
|
5706
|
+
async function cmdCharts(flags) {
|
|
5707
|
+
const t = resolveTarget(flags);
|
|
5708
|
+
const { url } = t;
|
|
5709
|
+
const base = `${url}/api/self/p/xrm/charts`;
|
|
5710
|
+
const key = flags._[0];
|
|
5711
|
+
const asJson = flags.json === true;
|
|
5712
|
+
if (!key) {
|
|
5713
|
+
if (!asJson)
|
|
5714
|
+
out.print(`→ Reading the pack's charts from ${targetLabel(t)} …`);
|
|
5715
|
+
const { status, json } = await apiGet(`${base}`, t);
|
|
5716
|
+
if (status !== 200)
|
|
5717
|
+
die(`could not read charts (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
5718
|
+
if (asJson) {
|
|
5719
|
+
out.result(json);
|
|
5720
|
+
return;
|
|
5721
|
+
}
|
|
5722
|
+
const charts = (json?.charts ?? []);
|
|
5723
|
+
if (charts.length === 0) {
|
|
5724
|
+
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.');
|
|
5725
|
+
return;
|
|
5726
|
+
}
|
|
5727
|
+
for (const c of charts)
|
|
5728
|
+
out.print(` ${c.key.padEnd(24)} ${(pickLabel(c.label) ?? c.key).padEnd(32)} ${c.show} · ${c.entity}`);
|
|
5729
|
+
out.print('\n`octwin charts <key>` draws one.');
|
|
5730
|
+
return;
|
|
5731
|
+
}
|
|
5732
|
+
const q = new URLSearchParams();
|
|
5733
|
+
if (typeof flags.from === 'string')
|
|
5734
|
+
q.set('from', flags.from);
|
|
5735
|
+
if (typeof flags.to === 'string')
|
|
5736
|
+
q.set('to', flags.to);
|
|
5737
|
+
const qs = q.toString() ? `?${q}` : '';
|
|
5738
|
+
if (!asJson)
|
|
5739
|
+
out.print(`→ Reading chart '${key}' from ${targetLabel(t)} …`);
|
|
5740
|
+
const { status, json } = await apiGet(`${base}/${encodeURIComponent(key)}${qs}`, t);
|
|
5741
|
+
if (status !== 200)
|
|
5742
|
+
die(`could not read chart '${key}' (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
5743
|
+
if (asJson) {
|
|
5744
|
+
out.result(json);
|
|
5745
|
+
return;
|
|
5746
|
+
}
|
|
5747
|
+
if (json?.has_data !== true) {
|
|
5748
|
+
out.print(`No figure for '${key}' — its entity has no records in this window, or this token may not view it.`);
|
|
5749
|
+
return;
|
|
5750
|
+
}
|
|
5751
|
+
const c = json.chart;
|
|
5752
|
+
out.print(`${pickLabel(c.label) ?? c.key} ${json.range?.from ?? '?'} → ${json.range?.to ?? '?'}`);
|
|
5753
|
+
if (c.value !== undefined)
|
|
5754
|
+
out.print(` ${c.value ?? '—'}${c.ratio ? ` (${c.ratio.of ?? '—'} ÷ ${c.ratio.per ?? '—'})` : ''}`);
|
|
5755
|
+
for (const p of c.parts ?? [])
|
|
5756
|
+
out.print(` ${String(pickLabel(p.label) ?? p.key).padEnd(28)} ${String(p.value ?? '—').padStart(10)} n=${p.count}`);
|
|
5757
|
+
if (c.cut)
|
|
5758
|
+
out.print(` … top ${c.cut.kept} of ${c.cut.groups}${c.cut.other ? `; the rest: ${c.cut.other.value ?? '—'} (n=${c.cut.other.count})` : ''}`);
|
|
5759
|
+
for (const p of c.points ?? [])
|
|
5760
|
+
out.print(` ${String(p.bucket).padEnd(12)} ${String(p.value ?? '—').padStart(10)} n=${p.count}`);
|
|
5761
|
+
}
|
|
5762
|
+
/**
|
|
5763
|
+
* `octwin handoff [<id>] [--bucket waiting|mine|all|ended] [--limit n] [--offset n] [--json]` — the
|
|
5764
|
+
* Live chat desk, READ-ONLY: the handoffs in a bucket, or one with its transcript (who said what —
|
|
5765
|
+
* customer, AI Agent, operator, platform). Needs `work:read` and the `live_chat` plan feature.
|
|
5766
|
+
*
|
|
5767
|
+
* Accepting, answering and returning a handoff stay in the console: those routes need a signed-in
|
|
5768
|
+
* PERSON to hold the conversation (`accepting needs a signed-in person`), and a token is not one.
|
|
5769
|
+
*/
|
|
5770
|
+
async function cmdHandoff(flags) {
|
|
5771
|
+
const t = resolveTarget(flags);
|
|
5772
|
+
const { url } = t;
|
|
5773
|
+
const base = `${url}/api/self/p/handoff`;
|
|
5774
|
+
const id = flags._[0];
|
|
5775
|
+
const asJson = flags.json === true;
|
|
5776
|
+
if (!id) {
|
|
5777
|
+
const q = new URLSearchParams();
|
|
5778
|
+
if (typeof flags.bucket === 'string')
|
|
5779
|
+
q.set('bucket', flags.bucket);
|
|
5780
|
+
if (flags.limit !== undefined)
|
|
5781
|
+
q.set('limit', String(flags.limit));
|
|
5782
|
+
if (flags.offset !== undefined)
|
|
5783
|
+
q.set('offset', String(flags.offset));
|
|
5784
|
+
const qs = q.toString() ? `?${q}` : '';
|
|
5785
|
+
if (!asJson)
|
|
5786
|
+
out.print(`→ Reading the Live chat desk from ${targetLabel(t)} …`);
|
|
5787
|
+
const { status, json } = await apiGet(`${base}${qs}`, t);
|
|
5788
|
+
if (status !== 200)
|
|
5789
|
+
die(`could not read the Live chat desk (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
5790
|
+
if (asJson) {
|
|
5791
|
+
out.result(json);
|
|
5792
|
+
return;
|
|
5793
|
+
}
|
|
5794
|
+
if (json?.has_data === false) {
|
|
5795
|
+
out.print(`No Live chat on this project — ${json?.reason ?? 'the pack works no handoff'} (craft-escalation, the Handoff recipe).`);
|
|
5796
|
+
return;
|
|
5797
|
+
}
|
|
5798
|
+
const rows = (json?.rows ?? []);
|
|
5799
|
+
if (rows.length === 0) {
|
|
5800
|
+
out.print('No handoffs in this view (--bucket waiting|mine|all|ended).');
|
|
5801
|
+
return;
|
|
5802
|
+
}
|
|
5803
|
+
for (const r of rows) {
|
|
5804
|
+
const who = r.contact?.display_name ?? r.contact?.channel_contact_handle ?? '—';
|
|
5805
|
+
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}`);
|
|
5806
|
+
}
|
|
5807
|
+
out.print('\n`octwin handoff <id>` shows one with its transcript.');
|
|
5808
|
+
return;
|
|
5809
|
+
}
|
|
5810
|
+
if (!asJson)
|
|
5811
|
+
out.print(`→ Reading handoff ${id} from ${targetLabel(t)} …`);
|
|
5812
|
+
const { status, json } = await apiGet(`${base}/${encodeURIComponent(id)}`, t);
|
|
5813
|
+
if (status !== 200)
|
|
5814
|
+
die(`could not read handoff ${id} (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
5815
|
+
if (asJson) {
|
|
5816
|
+
out.result(json);
|
|
5817
|
+
return;
|
|
5818
|
+
}
|
|
5819
|
+
const h = json?.handoff ?? {};
|
|
5820
|
+
out.print(`Handoff #${h.handoff_number ?? '?'} — ${h.stage ?? '?'}${h.queue_key ? ` · queue ${h.queue_key}` : ''}${h.reason ? ` · ${h.reason}` : ''}`);
|
|
5821
|
+
if (h.summary)
|
|
5822
|
+
out.print(` ${h.summary}`);
|
|
5823
|
+
for (const line of (json?.transcript ?? [])) {
|
|
5824
|
+
out.print(` ${String(line.ts).slice(11, 16)} ${line.role === 'operator' && line.author ? `operator: ${line.author}` : line.role} ${line.body}`);
|
|
5825
|
+
}
|
|
5826
|
+
if (json?.has_more)
|
|
5827
|
+
out.print(' … earlier lines in the console (Live chat).');
|
|
5828
|
+
}
|
|
5829
|
+
/**
|
|
5830
|
+
* `octwin surveys [<key>] [--limit n] [--offset n] [--json]` — the declared surveys with their
|
|
5831
|
+
* standing (responses, the headline score under its model), or one survey: its questions, the
|
|
5832
|
+
* rollup and the recent responses. The same reads as the console's Surveys page. Needs `records:read`.
|
|
5833
|
+
*/
|
|
5834
|
+
async function cmdSurveys(flags) {
|
|
5835
|
+
const t = resolveTarget(flags);
|
|
5836
|
+
const { url } = t;
|
|
5837
|
+
const base = `${url}/api/self/p/surveys`;
|
|
5838
|
+
const key = flags._[0];
|
|
5839
|
+
const asJson = flags.json === true;
|
|
5840
|
+
const score = (s) => s?.value == null ? '—' : `${s.value}${s.unit === 'pct' ? '%' : ''} (${s.model})`;
|
|
5841
|
+
if (!key) {
|
|
5842
|
+
if (!asJson)
|
|
5843
|
+
out.print(`→ Reading surveys from ${targetLabel(t)} …`);
|
|
5844
|
+
const { status, json } = await apiGet(`${base}`, t);
|
|
5845
|
+
if (status !== 200)
|
|
5846
|
+
die(`could not read surveys (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
5847
|
+
if (asJson) {
|
|
5848
|
+
out.result(json);
|
|
5849
|
+
return;
|
|
5850
|
+
}
|
|
5851
|
+
const rows = (json?.rows ?? []);
|
|
5852
|
+
if (rows.length === 0) {
|
|
5853
|
+
out.print('No surveys — a pack declares them in surveys.yaml (craft-surveys).');
|
|
5854
|
+
return;
|
|
5855
|
+
}
|
|
5856
|
+
for (const r of rows)
|
|
5857
|
+
out.print(` ${r.key.padEnd(22)} ${(pickLabel(r.label) ?? r.key).padEnd(30)} responses ${String(r.responses ?? '—').padStart(5)} score ${score(r.score)}`);
|
|
5858
|
+
out.print('\n`octwin surveys <key>` shows one — its questions, rollup and recent answers.');
|
|
5859
|
+
return;
|
|
5860
|
+
}
|
|
5861
|
+
const q = new URLSearchParams();
|
|
5862
|
+
if (flags.limit !== undefined)
|
|
5863
|
+
q.set('limit', String(flags.limit));
|
|
5864
|
+
if (flags.offset !== undefined)
|
|
5865
|
+
q.set('offset', String(flags.offset));
|
|
5866
|
+
const qs = q.toString() ? `?${q}` : '';
|
|
5867
|
+
if (!asJson)
|
|
5868
|
+
out.print(`→ Reading survey '${key}' from ${targetLabel(t)} …`);
|
|
5869
|
+
const { status, json } = await apiGet(`${base}/${encodeURIComponent(key)}${qs}`, t);
|
|
5870
|
+
if (status !== 200)
|
|
5871
|
+
die(`could not read survey '${key}' (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
5872
|
+
if (asJson) {
|
|
5873
|
+
out.result(json);
|
|
5874
|
+
return;
|
|
5875
|
+
}
|
|
5876
|
+
const sv = json?.survey ?? {};
|
|
5877
|
+
const a = json?.analytics;
|
|
5878
|
+
out.print(`${pickLabel(sv.label) ?? key}${sv.subject ? ` about: ${sv.subject}` : ''}`);
|
|
5879
|
+
for (const qq of sv.questions ?? [])
|
|
5880
|
+
out.print(` • ${qq.key} (${qq.type}${qq.required ? ', required' : ''})`);
|
|
5881
|
+
out.print(a ? ` responses ${a.responses} · scored ${a.scored} · score ${score(a.score)}` : ' rollup unavailable — the pack has not loaded on this project');
|
|
5882
|
+
const rows = (json?.responses?.rows ?? []);
|
|
5883
|
+
for (const r of rows)
|
|
5884
|
+
out.print(` ${String(r.created_at).slice(0, 16)} score ${r.score ?? '—'} ${JSON.stringify(r.answers)}`);
|
|
5885
|
+
}
|
|
5886
|
+
/**
|
|
5887
|
+
* `--range <days>` and `--channel <whatsapp|web>` as the route's own `?range=` / `?channel=` — the
|
|
5888
|
+
* console's two controls. Until 2026-10-11 the CLI read only `--detail`, so `--channel web` printed
|
|
5889
|
+
* the all-channel band as if it were the web's (an unknown flag was ignored then, not refused).
|
|
5890
|
+
*/
|
|
5891
|
+
function performanceQuery(flags) {
|
|
5892
|
+
const p = new URLSearchParams();
|
|
5893
|
+
if (flags.range !== undefined) {
|
|
5894
|
+
const days = Number(flags.range);
|
|
5895
|
+
if (!Number.isInteger(days) || days <= 0)
|
|
5896
|
+
die('--range takes a whole number of days, e.g. --range 30');
|
|
5897
|
+
p.set('range', String(days));
|
|
5898
|
+
}
|
|
5899
|
+
if (flags.channel !== undefined) {
|
|
5900
|
+
if (typeof flags.channel !== 'string')
|
|
5901
|
+
die('--channel takes a channel name, e.g. --channel whatsapp');
|
|
5902
|
+
p.set('channel', flags.channel);
|
|
5903
|
+
}
|
|
5904
|
+
const s = p.toString();
|
|
5905
|
+
return s ? `?${s}` : '';
|
|
5906
|
+
}
|
|
5907
|
+
/**
|
|
5908
|
+
* `octwin performance [--detail] [--range <days>] [--channel <ch>] [--json]` — the indicators the
|
|
5909
|
+
* project's own declarations produce: value, conversion, duration, per journey.
|
|
5339
5910
|
*
|
|
5340
5911
|
* Needs `records:read` — **not** a `performance:*` scope, which does not exist.
|
|
5341
5912
|
* That means the Read-only token preset already reaches this.
|
|
@@ -5351,9 +5922,10 @@ async function cmdPerformance(flags) {
|
|
|
5351
5922
|
// Two explicit calls rather than `apiGet(detail ? … : base)`: the route guard's
|
|
5352
5923
|
// extractor only reads a template literal in the FIRST argument position, so a
|
|
5353
5924
|
// ternary hides both URLs from it.
|
|
5925
|
+
const qs = performanceQuery(flags);
|
|
5354
5926
|
const { status, json } = detail
|
|
5355
|
-
? await apiGet(`${base}/detail`, t)
|
|
5356
|
-
: await apiGet(`${base}`, t);
|
|
5927
|
+
? await apiGet(`${base}/detail${qs}`, t)
|
|
5928
|
+
: await apiGet(`${base}${qs}`, t);
|
|
5357
5929
|
if (status !== 200)
|
|
5358
5930
|
die(`could not read performance (HTTP ${status})${errDetail(json)}${authFailureDetail(status, url)}`);
|
|
5359
5931
|
if (asJson) {
|
|
@@ -5466,427 +6038,472 @@ async function cmdUsage(flags) {
|
|
|
5466
6038
|
out.print('\nThis is MODEL spend. WhatsApp/Meta message billing is operator-only — not reachable by an API token.');
|
|
5467
6039
|
}
|
|
5468
6040
|
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
|
-
|
|
6041
|
+
out.print(`octwin ${VERSION} — Octwin external-pack developer CLI (by CEQUENS)
|
|
6042
|
+
|
|
6043
|
+
octwin --version # print the CLI version (+ any upgrade notice)
|
|
6044
|
+
octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
6045
|
+
octwin recipe [<module>] [--dir .] [--dry-run] # a KB recipe's starter files into this pack (no module = the list)
|
|
6046
|
+
octwin validate [--dir .] [--remote | --offline] [--require-kb] [--strict-primitives] # the platform's FULL check by default — no login needed
|
|
6047
|
+
octwin login --url <platformUrl> # approve in a browser; the CLI collects the token
|
|
6048
|
+
octwin login --url <platformUrl> --token oct_… # or paste a deploy token from the console
|
|
6049
|
+
octwin whoami [--url <url>] [--tenant <slug>] # verify the token works
|
|
6050
|
+
octwin projects [--archived] [--json] # the --project slugs this token can name
|
|
6051
|
+
octwin projects create "<name>" [--slug s] [--pack p] | rm <slug> [--yes] # a disposable project (needs projects:write)
|
|
6052
|
+
octwin seed [--reset] [--pack <packId>] # apply the pack's demo data without redeploying
|
|
6053
|
+
octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed] [--force]
|
|
6054
|
+
[--request-listing | --withdraw-listing] # public marketplace — opt-in, see: octwin help deploy
|
|
6055
|
+
octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
|
|
6056
|
+
octwin pull <packId> [--dir <out>] [--version <v>] [--force] # write a DEPLOYED pack's source back to disk (the inverse of deploy)
|
|
6057
|
+
octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
|
|
6058
|
+
octwin work [recordId] [--queues] [--unrouted] [--json] # inspect the work inbox (worked records) + timelines
|
|
6059
|
+
# --queues: per-queue open counts + an UNROUTED warning · --unrouted: only the items in no queue
|
|
6060
|
+
octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
|
|
6061
|
+
octwin chat "message" [--as <handle>] [--tap <tap-id>] [--press <label>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
|
|
6062
|
+
octwin media generate "<prompt>" [--out <file.png>] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
|
|
6063
|
+
octwin agents [packId::agentId] [--prompt] [--json] # effective model/memory + WHICH layer won; --prompt = the resolved system prompt
|
|
6064
|
+
octwin orders [reference_id] [--status s] [--payment p] [--json] # the orders a conversation produced + money + payment state
|
|
6065
|
+
octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] # stage conversion for any pipelined entity
|
|
6066
|
+
octwin catalog [--readiness] [--json] # commerce products + stock + the WhatsApp catalog binding
|
|
6067
|
+
octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] # engine state / computed slots
|
|
6068
|
+
octwin automation [coming|happened|why <id>|campaigns] [--json] # the rules in words; coming up; happened, and why
|
|
6069
|
+
octwin integrations [--json] # declared connections BESIDE what is configured (the silent-never-fires check)
|
|
6070
|
+
octwin integrations deliveries [<id>] | events # the outbound delivery log / inbound events
|
|
6071
|
+
octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition] [--stage <id>]
|
|
6072
|
+
octwin performance [--detail] [--range <days>] [--channel <ch>] [--json] # the project's business indicators (value/conversion/duration)
|
|
6073
|
+
octwin usage [--json] # model calls, tokens and cost (project if pinned, else workspace)
|
|
6074
|
+
octwin charts [<key>] [--from d] [--to d] [--json] # the pack's own charts: — the list, or one chart's figure
|
|
6075
|
+
octwin handoff [<id>] [--bucket waiting|mine|all|ended] [--limit n] [--offset n] [--json] # the Live chat desk, read-only
|
|
6076
|
+
octwin surveys [<key>] [--limit n] [--offset n] [--json] # surveys with their score; one survey's questions, rollup, answers
|
|
6077
|
+
octwin platform-kb [pull] [--if-stale|--check] [--dir .] [--url <url>] # no token needed
|
|
6078
|
+
octwin test [--dir .] [--require-kb] [--strict-primitives] # = validate --remote (the full platform check)
|
|
6079
|
+
octwin feedback [--dir .] # submit this pack's FEEDBACK.md to the platform team
|
|
6080
|
+
octwin memos [--all] [--json] # read the platform's replies + notices (a reply to your feedback lands here)
|
|
6081
|
+
|
|
6082
|
+
Writes — exercise the state your pack creates (each needs the matching :write scope):
|
|
6083
|
+
octwin records create <entity> --set field=value … # also: patch <id> --entity <e>, stage <id> --to <s>, note <id> "…", archive|restore <id>
|
|
6084
|
+
octwin records tasks | task complete <taskId> [--outcome done|cancelled]
|
|
6085
|
+
octwin work assign <id> --to user:<uuid>|none | note <id> "…" | stage <id> --to <stage>
|
|
6086
|
+
octwin work decide <id> --action <a> [--param k=v] [--dry-run] # --dry-run previews, commits nothing
|
|
6087
|
+
octwin orders transition <ref> --to <status> | refund <ref> --force
|
|
6088
|
+
octwin catalog availability <sku> --to "in stock" | stock <sku> [--set-on-hand n]
|
|
6089
|
+
octwin scheduling rules --resource <id> | rule add|rm | exception add|rm
|
|
6090
|
+
octwin agents set <packId::agentId> [--model m] [--enable-tool t] [--disable-tool t]
|
|
6091
|
+
octwin automation pause|resume|run <ruleKey> | send <campaignId>
|
|
6092
|
+
octwin integrations test <key> # a LIVE call to the connection's health: operation
|
|
6093
|
+
octwin integrations retry|cancel|send-now <deliveryId>
|
|
6094
|
+
(octwin integrations preflight <key> needs only integrations:read — it makes no call)
|
|
6095
|
+
|
|
6096
|
+
Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
|
|
6097
|
+
\`octwin chat --as <h>\` calls continue the same conversation; press a rendered
|
|
6098
|
+
button/row with \`--tap "<tap-id>"\` (chat prints every tap id), or by its label with
|
|
6099
|
+
\`--press "<label>"\` — the way through a form or checkout, whose ids change.
|
|
6100
|
+
Signing in: \`octwin login --url <u>\` prints a link + code to approve in a browser. To pick your own
|
|
6101
|
+
scopes instead, mint a token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
|
|
6102
|
+
octwin platform-kb pull → writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
|
|
6103
|
+
Config (deploy): flags > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login (\`octwin login\` sets the default target).
|
|
5526
6104
|
Per-command usage: octwin <command> --help`);
|
|
5527
6105
|
}
|
|
5528
6106
|
/** Per-subcommand usage — printed for `octwin <cmd> --help|-h` BEFORE any
|
|
5529
6107
|
* network/auth work (a --help that 401s is worse than no help at all). */
|
|
5530
6108
|
const COMMAND_HELP = {
|
|
5531
|
-
init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
6109
|
+
init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
5532
6110
|
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
|
|
6111
|
+
validate: `octwin validate [--dir .] [--remote | --offline] [--require-kb] [--strict-primitives]
|
|
6112
|
+
First the fast OFFLINE checks (structure, YAML, and the checks driven by the
|
|
6113
|
+
pulled capability reference — those SKIP when the reference is missing, and
|
|
6114
|
+
--require-kb turns the skip into a failure for CI). Then, BY DEFAULT, the
|
|
6115
|
+
platform's FULL validation — the same check deploy runs, all errors at once.
|
|
6116
|
+
|
|
6117
|
+
The full check needs NO login: without a token + --project it goes to the
|
|
6118
|
+
platform's public validate route. With a token AND --project (or PACK_PROJECT)
|
|
6119
|
+
it goes through your workspace, which also confirms that project exists.
|
|
6120
|
+
|
|
6121
|
+
When the platform cannot be reached (no url, offline, rate-limited) the run
|
|
6122
|
+
ends with "only the OFFLINE check ran — a PARTIAL check" and exits 0.
|
|
6123
|
+
--remote REQUIRES the full check: not reaching it is an error. --offline runs
|
|
6124
|
+
only the partial subset. --strict-primitives additionally type-checks LITERAL
|
|
6125
|
+
args: values against each primitive's declared input schema; expression
|
|
5548
6126
|
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
|
|
6127
|
+
login: `octwin login --url <platformUrl> [--token oct_…] [--wait <seconds>] [--json]
|
|
6128
|
+
Sign in to a platform and make that url the DEFAULT deploy target for every
|
|
6129
|
+
later command.
|
|
6130
|
+
|
|
6131
|
+
WITH --token: saves a token you minted yourself (console → Settings → API
|
|
6132
|
+
tokens). Unchanged, and still the right answer for CI — there, skip login and
|
|
6133
|
+
set PACK_PLATFORM_URL + PACK_TOKEN instead.
|
|
6134
|
+
|
|
6135
|
+
WITHOUT --token: prints a link to approve in a browser. A workspace admin opens
|
|
6136
|
+
it, checks which machine is asking, and clicks Approve; the CLI then collects a
|
|
6137
|
+
30-day token by itself, so no token is ever pasted between people. The code is
|
|
6138
|
+
printed too — it is what the page shows, so you can tell it is your own request.
|
|
6139
|
+
Approve only a link you opened because YOU ran this command: a link someone
|
|
6140
|
+
sends you would connect THEIR machine to your workspace.
|
|
6141
|
+
|
|
6142
|
+
The wait is bounded (90s by default) and RESUMABLE: if nobody has approved yet
|
|
6143
|
+
it exits 75 and keeps the request, so running \`octwin login\` again picks up the
|
|
5566
6144
|
same code. --wait 0 prints the code and returns at once.`,
|
|
5567
|
-
whoami: `octwin whoami [--url <url>] [--tenant <slug>]
|
|
6145
|
+
whoami: `octwin whoami [--url <url>] [--tenant <slug>]
|
|
5568
6146
|
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
|
|
6147
|
+
projects: `octwin projects [--archived] [--json]
|
|
6148
|
+
List the workspace's projects — the slugs every --project flag takes, with the
|
|
6149
|
+
plan's project cap. --archived includes archived ones. A pack:deploy token
|
|
6150
|
+
reaches this (it names a project in every other command).
|
|
6151
|
+
|
|
6152
|
+
octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
|
|
6153
|
+
Create a project. The URL slug is derived from the name unless --slug pins one.
|
|
6154
|
+
--pack installs an ALREADY-published pack; the usual next step is instead
|
|
6155
|
+
\`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
|
|
6156
|
+
|
|
6157
|
+
octwin projects rm <slug> [--yes]
|
|
6158
|
+
HARD delete — the project and everything cascading from it (conversations,
|
|
6159
|
+
contacts, records, installs). No undo, and not the same as archiving.
|
|
6160
|
+
WITHOUT --yes it only previews what would be destroyed, so the dry run is the
|
|
6161
|
+
default. Together these make a disposable end-to-end environment:
|
|
6162
|
+
octwin projects create "Scratch" && octwin deploy --project scratch --seed
|
|
6163
|
+
octwin chat "hi" --project scratch
|
|
6164
|
+
octwin projects rm scratch --yes
|
|
5587
6165
|
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
|
-
|
|
6166
|
+
deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed] [--force]
|
|
6167
|
+
[--request-listing | --withdraw-listing]
|
|
6168
|
+
Upload the pack bundle, validate server-side, install onto the project.
|
|
6169
|
+
--seed additionally applies the pack's demo seed (streams progress).
|
|
6170
|
+
|
|
6171
|
+
A deploy sends what this copy was made from (${BASE_FILE}, written by every pull and
|
|
6172
|
+
deploy). When someone published since — Tavi in the console, another deploy, a repo
|
|
6173
|
+
import — it is refused and NOTHING changes: run 'octwin pull <packId>', which updates
|
|
6174
|
+
this folder (it stops if you changed a file since, so you can save your edit first),
|
|
6175
|
+
then deploy again. A version lower than the live one is refused the same way.
|
|
6176
|
+
--force deploy anyway, replacing the newer or higher live version.
|
|
6177
|
+
|
|
6178
|
+
A plain deploy says NOTHING about the public marketplace — it is a test loop, so it
|
|
6179
|
+
neither asks for a listing nor gives one up. The marketplace flags are opt-in:
|
|
6180
|
+
|
|
6181
|
+
--request-listing ask an operator to review this pack for the public marketplace
|
|
6182
|
+
(the pre-signup storefront at /packs). Requires 'public: true'
|
|
6183
|
+
under 'listing:' in manifest.yaml — the manifest states that the
|
|
6184
|
+
pack is a product, the flag is you choosing to ask.
|
|
6185
|
+
--withdraw-listing retract the request, including an approved listing.
|
|
6186
|
+
|
|
6187
|
+
An approval covers the CONTENT it was made against, so a later deploy that changes the
|
|
5603
6188
|
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.
|
|
6189
|
+
seed: `octwin seed [--pack <packId>] [--reset]
|
|
6190
|
+
Apply the pack's demo/reference data to the project it is installed on, without
|
|
6191
|
+
redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
|
|
6192
|
+
and the demo operator topology. Reports what each kind produced.
|
|
6193
|
+
Idempotent and safe to re-run — each demo row is found again by its handle (its
|
|
6194
|
+
\`as:\`, natural key or title) and updated, and existing media is REUSED rather
|
|
6195
|
+
than regenerated, so a second pass costs nothing. Demo rows are DATA: no \`on:\`
|
|
6196
|
+
reaction fires, and a row takes its \`stage:\` as written.
|
|
6197
|
+
--reset archive every row the pack's demo loaded, then load it again. Needed once
|
|
6198
|
+
for demo rows seeded before 2026-10-05 (the seed says so), and a clean
|
|
6199
|
+
slate whenever you want one. Records your customers made are not touched.
|
|
5615
6200
|
--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
|
-
|
|
6201
|
+
status: `octwin status [<packId>] [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
|
|
6202
|
+
Show installed vs live version + the flow list for this pack, and whether your copy
|
|
6203
|
+
(${BASE_FILE}) is still the live content — if not, pull before you deploy.
|
|
6204
|
+
The pack id is read from manifest.yaml and QUALIFIED with your workspace slug
|
|
6205
|
+
(a manifest declares a bare name; the owner is attached when you publish). Pass
|
|
6206
|
+
<packId> explicitly to skip that lookup — \`octwin agents\` and \`octwin projects\`
|
|
5621
6207
|
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
|
|
6208
|
+
records: `octwin records [entity] [id] [--limit 50] [--offset n] [--json]
|
|
6209
|
+
Inspect the pack's XRM data. No args = list entities. Worked records (cases,
|
|
6210
|
+
tickets, anything routed to a queue) read best through \`octwin work\`.
|
|
6211
|
+
|
|
6212
|
+
SEARCH:
|
|
6213
|
+
octwin records <entity> --search "<words>" # ranked, best match first, each row with its score (0–1)
|
|
6214
|
+
Works for an entity that declares \`search:\` in xrm.yaml (fuzzy, plus semantic when declared) —
|
|
6215
|
+
the same search the console's search box runs. Any other entity answers "not searchable".
|
|
6216
|
+
|
|
6217
|
+
WRITES (need \`records:write\`; every one is re-checked by RBAC on the record):
|
|
6218
|
+
octwin records create <entity> --set field=value [--set …] [--stage s] [--contact <id>]
|
|
6219
|
+
octwin records patch <recordId> --entity <entity> --set field=value
|
|
6220
|
+
octwin records stage <recordId> --to <stage> [--note "..."]
|
|
6221
|
+
octwin records note <recordId> "the note text"
|
|
6222
|
+
octwin records archive <recordId> # out of every list and count (test data too)
|
|
6223
|
+
octwin records restore <recordId> # undo an archive
|
|
6224
|
+
octwin records tasks # open follow-up tasks (\`tasks\` plan feature)
|
|
6225
|
+
octwin records task complete <taskId> [--outcome done|cancelled] [--note "..."]
|
|
6226
|
+
|
|
6227
|
+
There is NO delete — on the platform or here. \`archive\` is how a record leaves use:
|
|
6228
|
+
it fires the entity's \`on.archived\` reactions, exactly like the console's Archive,
|
|
6229
|
+
and \`restore\` brings it back.
|
|
6230
|
+
|
|
6231
|
+
--set coerces JSON scalars: \`--set rating=4.5\` sends a number, \`--set x=null\`
|
|
6232
|
+
sends null. Use --fields-json '{"a":{"b":1}}' for anything nested.
|
|
6233
|
+
\`patch\` needs --entity even though it has an id: the route resolves the field
|
|
6234
|
+
validator from it. A leading \`create/patch/stage/note/archive/restore/tasks/task\` is
|
|
5649
6235
|
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\`).
|
|
6236
|
+
work: `octwin work [recordId] [--queues] [--unrouted] [--limit 50] [--offset n] [--json]
|
|
6237
|
+
Inspect the work inbox — every entity the pack declares worked (cases, orders
|
|
6238
|
+
needing review, applications, …): the inbox, one item + its timeline
|
|
6239
|
+
(+ applicable actions), or --queues for queue keys + open counts.
|
|
6240
|
+
|
|
6241
|
+
WRITES (need \`work:write\`; \`stage\` needs \`records:write\`):
|
|
6242
|
+
octwin work assign <recordId> --to user:<uuid>|team:<uuid>|none
|
|
6243
|
+
octwin work note <recordId> "the note text"
|
|
6244
|
+
octwin work stage <recordId> --to <stage> [--note "..."]
|
|
6245
|
+
octwin work decide <recordId> --action <action> [--param k=v] [--note "..."] [--dry-run]
|
|
6246
|
+
|
|
6247
|
+
\`decide\` applies one of the entity's declared operator actions — \`octwin work <id>\`
|
|
6248
|
+
lists them with their params. --dry-run previews the customer-facing copy and the
|
|
6249
|
+
resulting stage WITHOUT committing (that route needs only \`work:read\`).
|
|
5664
6250
|
\`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.
|
|
6251
|
+
logs: `octwin logs [conversationId] [--as <handle>] [--json]
|
|
6252
|
+
No id = recent conversations (handle, status, last activity; --as filters).
|
|
6253
|
+
With id = the full event timeline including what each turn rendered.
|
|
5668
6254
|
--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
|
-
|
|
6255
|
+
pull: `octwin pull <packId> [--dir <out>] [--version <v>] [--force]
|
|
6256
|
+
Write a DEPLOYED pack's source back to disk — the inverse of deploy.
|
|
6257
|
+
A pack pushed with 'octwin deploy' lives on the platform as an artifact the
|
|
6258
|
+
runtime serves but nothing hands back, so its only source copy is the machine
|
|
6259
|
+
that pushed it. Pull it, fix it, redeploy it.
|
|
6260
|
+
Defaults to the version installed on the target project; --version overrides.
|
|
6261
|
+
Writes into the folder you are in when it is this pack, else ./<packId>; --dir overrides.
|
|
6262
|
+
Replaces only files that still match your last pull or deploy (${BASE_FILE}); a file
|
|
6263
|
+
you changed since stops it — save your edit first, or --force overwrites it.
|
|
6264
|
+
The pulled dir redeploys where it came from — the target is your saved login.
|
|
6265
|
+
You may pull a pack your workspace OWNS, or any first-party (Octwin) pack — those are
|
|
6266
|
+
free to copy as a starting point; an operator token pulls any.`,
|
|
6267
|
+
chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--press <label>] [--media <file|id>] [--json]
|
|
6268
|
+
octwin chat --script <file> [--as <handle>] [--json]
|
|
6269
|
+
Drive ONE turn through the dev web channel and print every render with its
|
|
6270
|
+
tap ids. Same --as handle = same conversation (multi-turn works).
|
|
6271
|
+
--tap presses a rendered button/list row instead of sending text.
|
|
6272
|
+
--press does the same BY ITS LABEL, in the reply the conversation is waiting on
|
|
6273
|
+
— for a form or checkout step, whose tap id carries a run id that changes.
|
|
6274
|
+
--media uploads a local file (or a media id from 'media generate --json') as
|
|
6275
|
+
an image/document/audio inbound — any "message" rides as its caption; feeds a
|
|
6276
|
+
running media-collect flow (e.g. activate-app).
|
|
6277
|
+
--json dumps the raw SSE envelopes for the turn.
|
|
6278
|
+
|
|
6279
|
+
Each reply line says who sent it. Plain \`← …\` is the AI Agent. \`← [notify] …\` is a
|
|
6280
|
+
reminder or notification (a \`schedule_notify\`, a reaction) — NOT the turn's answer,
|
|
6281
|
+
so the wait for the real reply goes on. \`← [operator: Name] …\` is a person in
|
|
6282
|
+
Live chat; \`← [platform] …\` is a platform line (a handoff joining or returning).
|
|
6283
|
+
After the last turn the stream stays open ${LATE_DRAIN_MS / 1000}s more, so a late message
|
|
6284
|
+
still prints (marked "after the last turn") instead of reaching a closed connection.
|
|
6285
|
+
|
|
6286
|
+
--script drives a WHOLE conversation from a file, ONE TURN PER LINE, in one
|
|
6287
|
+
process over one connection — waiting for each turn to settle before sending
|
|
6288
|
+
the next. Use this for any multi-step flow: chaining shell invocations races
|
|
6289
|
+
the agent loop, because a turn ends on a quiet gap that can arrive while the
|
|
6290
|
+
server is still working (the symptom is placeholder-filled fields or a second
|
|
6291
|
+
workflow run). Blank lines and # comments are skipped:
|
|
6292
|
+
|
|
6293
|
+
# book an appointment end to end
|
|
6294
|
+
احجز موعد
|
|
6295
|
+
tap:t:invoke:book-appointment:doctor_id=D1
|
|
6296
|
+
media:./licence.jpg | here is my licence
|
|
6297
|
+
press:Confirm
|
|
6298
|
+
|
|
6299
|
+
press:<label> taps the button of the reply just received whose label matches —
|
|
6300
|
+
exactly, or the same words without emoji or case, or the one label containing
|
|
6301
|
+
them. None, or several buttons, stops the script and lists the buttons. A form
|
|
6302
|
+
step's tap id carries its run id (t:resume:…:run_id=…), which exists only once
|
|
5714
6303
|
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,
|
|
6304
|
+
media: `octwin media generate "<prompt>" [--out <file.png>] [--json]
|
|
6305
|
+
AI-generate an image (needs a media:generate-scoped token), store it as a
|
|
6306
|
+
public asset, and print its MEDIA- handle + serve URL. --out downloads the
|
|
6307
|
+
bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
|
|
5719
6308
|
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
|
|
6309
|
+
agents: `octwin agents [packId::agentId] [--prompt] [--json]
|
|
6310
|
+
No args = the roster with each agent's EFFECTIVE model and which layer set it.
|
|
6311
|
+
With an agent = every governed setting (model / memory.last_messages /
|
|
6312
|
+
working_memory) plus the layer that won — an operator PLATFORM default can
|
|
6313
|
+
override what your manifest declares, and this is where you see that.
|
|
6314
|
+
--prompt = the exact system prompt the LLM sees for this project (pack
|
|
6315
|
+
instructions + platform protocol + any project overlay). Needs agents:read.
|
|
6316
|
+
The agent ref is the compound \`<packId>::<agentId>\` key or the override-row UUID.
|
|
6317
|
+
|
|
6318
|
+
WRITES (need \`agents:write\`):
|
|
6319
|
+
octwin agents set <ref> [--model <m>] [--enabled true|false] [--overlay "..."|none]
|
|
6320
|
+
[--enable-tool <toolId>] [--disable-tool <toolId>]
|
|
6321
|
+
|
|
6322
|
+
Only what you pass is changed. Tool flags read-modify-write \`config_json.tools\`
|
|
6323
|
+
so a sibling decision isn't dropped; absent = enabled. A workspace that hides model
|
|
5735
6324
|
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
|
|
6325
|
+
orders: `octwin orders [reference_id] [--status s] [--payment p] [--limit 50] [--offset n] [--json]
|
|
6326
|
+
No args = the order list (#number, status/payment, total, contact). With a
|
|
6327
|
+
reference_id = line items, the subtotal/tax/shipping/discount/total breakdown,
|
|
6328
|
+
payment_ref, and the allowed status transitions. Needs orders:read + the
|
|
6329
|
+
\`orders\` plan feature. Note: the forward payment lifecycle is webhook-owned —
|
|
6330
|
+
\`none\` = no payment opened (no gateway), \`pending\` = checkout not confirmed yet.
|
|
6331
|
+
|
|
6332
|
+
WRITES (need \`orders:write\`):
|
|
6333
|
+
octwin orders transition <reference_id> --to <status>
|
|
6334
|
+
octwin orders refund <reference_id> [--reason "..."] [--mark-returned] --force
|
|
6335
|
+
|
|
6336
|
+
Refund is irreversible and moves money, hence --force. The route answers 200 even
|
|
6337
|
+
when the GATEWAY refuses, so the CLI reads the gateway verdict and exits non-zero
|
|
6338
|
+
on a refusal rather than reporting a refund that never happened. Only a payment in
|
|
5750
6339
|
\`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
|
|
6340
|
+
analytics: `octwin analytics [entity] [--funnel|--overview|--milestones|--trends|--cost] [--stage <id>] [--limit n] [--offset n] [--json]
|
|
6341
|
+
No args = the entities that carry a \`pipeline:\` (a funnel needs stages).
|
|
6342
|
+
With an entity = stage-by-stage conversion (default --funnel) over the last 30
|
|
6343
|
+
days. --stage <id> lists the records CURRENTLY at a stage (a live snapshot, not
|
|
5755
6344
|
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
|
|
6345
|
+
catalog: `octwin catalog [--readiness] [--limit n] [--offset n] [--json]
|
|
6346
|
+
The commerce \`product\` records + price, availability, stock (null = not
|
|
6347
|
+
inventory-tracked) and the WhatsApp catalog binding. --readiness runs the Meta
|
|
6348
|
+
Graph checklist (LIVE Graph calls; needs a bound access token). Needs
|
|
6349
|
+
catalog:read + the \`catalog\` plan feature.
|
|
6350
|
+
|
|
6351
|
+
WRITES (need \`catalog:write\`):
|
|
6352
|
+
octwin catalog availability <retailerId> --to "in stock"|"out of stock"|…
|
|
6353
|
+
octwin catalog stock <retailerId> [--set-on-hand <n>]
|
|
6354
|
+
|
|
6355
|
+
\`stock\` with no --set-on-hand READS it; \`null\` means the SKU is not
|
|
6356
|
+
inventory-tracked (always sellable), which is different from 0. Lowering on_hand
|
|
6357
|
+
below the units already reserved for open carts is refused. Creating/deleting
|
|
5769
6358
|
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
|
|
6359
|
+
scheduling: `octwin scheduling [--slots <resourceRecordId>] [--from YYYY-MM-DD] [--days n] [--json]
|
|
6360
|
+
No args = the engine state (bookable resource types, upcoming slots, booked
|
|
6361
|
+
seats). --slots <recordId> computes the slots for one bookable resource
|
|
6362
|
+
(occupancy included; --days is clamped to 1-31 server-side) — the way to verify
|
|
6363
|
+
the availability rules a \`deploy --seed\` created. Needs scheduling:read.
|
|
6364
|
+
|
|
6365
|
+
RULES (list needs scheduling:read; add/rm need scheduling:write):
|
|
6366
|
+
octwin scheduling rules --resource <resourceRecordId>
|
|
6367
|
+
octwin scheduling rule add --resource <id> --dow 1 --start 09:00 --end 17:00
|
|
6368
|
+
[--slot-minutes 30] [--capacity 1]
|
|
6369
|
+
octwin scheduling rule rm <ruleId>
|
|
6370
|
+
octwin scheduling exception add --resource <id> --date YYYY-MM-DD --kind closed|extra
|
|
6371
|
+
[--start 09:00 --end 13:00]
|
|
6372
|
+
octwin scheduling exception rm <exceptionId>
|
|
6373
|
+
|
|
6374
|
+
--dow is 0-6, 0 = Sunday. \`rules\` is how you find an id to remove, and
|
|
5786
6375
|
\`--slots\` is how you check what a rule actually produces.`,
|
|
5787
|
-
automation: `octwin automation [campaigns
|
|
5788
|
-
No args = every
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
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
|
|
6376
|
+
automation: `octwin automation [coming | happened | why <id> | campaigns | --campaigns] [--json]
|
|
6377
|
+
No args = every RULE of the project — each in the platform's own words (the
|
|
6378
|
+
sentence the console's Outbound → Automatic tab shows), on or paused, its week
|
|
6379
|
+
(sent / not delivered / failed / skipped) and its next time — under the week's
|
|
6380
|
+
numbers and what needs a look. Needs automation:read.
|
|
5800
6381
|
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
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.
|
|
6382
|
+
A pack writes rules in automation.yaml \`rules:\`; its older \`segment\` /
|
|
6383
|
+
\`stale_records\` / \`sla_sweep\` jobs and xrm \`on:\` hooks run as rules too,
|
|
6384
|
+
listed under the key they compile to (\`segment:<job key>\`).
|
|
5810
6385
|
|
|
5811
|
-
|
|
5812
|
-
octwin
|
|
5813
|
-
|
|
5814
|
-
octwin
|
|
5815
|
-
|
|
6386
|
+
READ:
|
|
6387
|
+
octwin automation coming [--range today|tomorrow|week|all] [--entity e] [--q text] [--cursor c]
|
|
6388
|
+
# every time still to come — a rule's, or a flow's own message
|
|
6389
|
+
octwin automation happened [--chip all|not_delivered|failed|skipped|moves|tasks] [--period 7d|30d] [--cursor c]
|
|
6390
|
+
# every fire: its final word, and why a message did not go
|
|
6391
|
+
octwin automation why <id> [--kind timer|fire|queued]
|
|
6392
|
+
# one line: the rule in words, each step, the words sent
|
|
6393
|
+
octwin automation campaigns [--limit n] [--offset n]
|
|
5816
6394
|
|
|
5817
|
-
|
|
5818
|
-
octwin
|
|
5819
|
-
octwin
|
|
5820
|
-
octwin
|
|
5821
|
-
octwin integrations events # INBOUND events (what arrived at your webhook)
|
|
6395
|
+
WRITES (automation:write):
|
|
6396
|
+
octwin automation pause|resume <ruleKey> # paused: its times arm, nothing runs, nothing caught up
|
|
6397
|
+
octwin automation run <ruleKey> # a rule on a timetable, now
|
|
6398
|
+
octwin automation send <campaignId> # enqueue a campaign; enqueued != delivered
|
|
5822
6399
|
|
|
5823
|
-
|
|
6400
|
+
<ruleKey> is the key the list prints; the key of the older job in your own
|
|
6401
|
+
automation.yaml finds the rule it runs as. A 403 on a write can be an RBAC grant
|
|
6402
|
+
gap rather than a missing scope: the action is re-checked on automation.rule.
|
|
6403
|
+
(Until 0.9.0 this command listed job rows — since the platform ran every pack's
|
|
6404
|
+
automation as rules, those were only its own two sweeps.)`,
|
|
6405
|
+
integrations: `octwin integrations [--json]
|
|
6406
|
+
What the pack DECLARES beside what is actually CONFIGURED, in one view — because a
|
|
6407
|
+
connection that is declared and never configured is the commonest reason an
|
|
6408
|
+
integration silently never fires, and neither list alone can show it. Flags the
|
|
6409
|
+
gap explicitly. Needs integrations:read.
|
|
6410
|
+
|
|
6411
|
+
DIAGNOSE ONE CONNECTION:
|
|
6412
|
+
octwin integrations preflight <key> # every check, with a fix hint. Makes NO
|
|
6413
|
+
# outbound call — needs only integrations:read
|
|
6414
|
+
octwin integrations test <key> # a LIVE call to its health: operation
|
|
6415
|
+
# (integrations:write). Exits 1 when it fails.
|
|
6416
|
+
|
|
6417
|
+
THE DELIVERY LOG:
|
|
6418
|
+
octwin integrations deliveries [--status s] [--operation id] [--limit n] [--offset n]
|
|
6419
|
+
octwin integrations deliveries <id> # + the redacted request/response snapshots
|
|
6420
|
+
octwin integrations retry|cancel|send-now <id> # integrations:write
|
|
6421
|
+
octwin integrations events # INBOUND events (what arrived at your webhook)
|
|
6422
|
+
|
|
6423
|
+
retry/cancel answer 409 when the delivery is in the wrong state; the message
|
|
5824
6424
|
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
|
|
6425
|
+
journeys: `octwin journeys [journeyId] [--funnel|--overview|--goals|--trends|--cost|--definition]
|
|
6426
|
+
[--stage <stageId>] [--limit n] [--offset n] [--json]
|
|
6427
|
+
No args = the journeys the pack declares. With an id, one of six views —
|
|
6428
|
+
--funnel (default) stage-by-stage reach and drop-off · --overview entered vs
|
|
6429
|
+
converted plus the biggest drop-off · --goals completions, contacts, value and
|
|
6430
|
+
p50 time · --trends per-bucket activity · --cost tokens and dollars per goal ·
|
|
6431
|
+
--definition what was DECLARED, unmeasured (the one view that works with no
|
|
6432
|
+
traffic). Needs journeys:read.
|
|
6433
|
+
|
|
6434
|
+
--stage <stageId> lists the runs sitting at a stage right now (a live snapshot,
|
|
6435
|
+
not the funnel's cumulative reached counts).
|
|
6436
|
+
|
|
6437
|
+
Same flag grammar as \`octwin analytics\` on purpose: a journey funnel and an
|
|
6438
|
+
entity funnel are the same question about different subjects. Journeys carry RBAC
|
|
6439
|
+
on top of the scope, so an empty answer can be a missing \`view\` grant rather
|
|
5840
6440
|
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
|
-
|
|
6441
|
+
performance: `octwin performance [--detail] [--range <days>] [--channel <ch>] [--json]
|
|
6442
|
+
The project's business indicators — value produced, conversion, duration — each
|
|
6443
|
+
with its delta against the previous window and a \`why\` naming the declaration it
|
|
6444
|
+
came from. --detail adds the per-indicator breakdown. --range <days> sets the window
|
|
6445
|
+
and --channel <whatsapp|web> narrows every figure to one channel — the console's two
|
|
6446
|
+
controls on the same page.
|
|
6447
|
+
|
|
6448
|
+
Needs records:read, NOT a performance scope (there is none), so a read-only token
|
|
6449
|
+
already reaches it. Indicators are DERIVED: a pack that declares no journey goal
|
|
5848
6450
|
value and no pipelined entity produces none, which is a different thing from zero.`,
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
6451
|
+
charts: `octwin charts [<key>] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--json]
|
|
6452
|
+
The pack's own \`charts:\` (xrm.yaml), as the owner's dashboard draws them. No key = the
|
|
6453
|
+
list; with a key = that chart's figure, its parts (a \`by:\` chart) or its points (an
|
|
6454
|
+
\`over:\` chart) for the window. A token sees the charts whose entity it may view.
|
|
6455
|
+
Needs records:read.`,
|
|
6456
|
+
handoff: `octwin handoff [<id>] [--bucket waiting|mine|all|ended] [--limit n] [--offset n] [--json]
|
|
6457
|
+
The Live chat desk, read-only: the handoffs in a bucket (default all), or one with
|
|
6458
|
+
its transcript — who said what: customer, AI Agent, operator, platform. Needs work:read
|
|
6459
|
+
and the live_chat plan feature. Accepting, answering and returning one stay in the
|
|
6460
|
+
console: those need a signed-in PERSON to hold the conversation, and a token is not one.`,
|
|
6461
|
+
surveys: `octwin surveys [<key>] [--limit n] [--offset n] [--json]
|
|
6462
|
+
No key = the declared surveys with their responses and headline score (under the
|
|
6463
|
+
survey's own model — average, top box or NPS). With a key = its questions, the rollup,
|
|
6464
|
+
and the recent responses (--limit / --offset page them). Needs records:read.`,
|
|
6465
|
+
usage: `octwin usage [--json]
|
|
6466
|
+
Model calls, tokens and cost for the resolved scope — project when one is pinned
|
|
6467
|
+
or passed with --project, otherwise the whole workspace. Broken down by model,
|
|
6468
|
+
kind, agent and channel.
|
|
6469
|
+
|
|
6470
|
+
Needs no particular scope: any valid token reaches it.
|
|
6471
|
+
|
|
6472
|
+
This is MODEL spend only. WhatsApp/Meta message billing is operator-only and
|
|
5857
6473
|
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
|
|
6474
|
+
'platform-kb': `octwin platform-kb [pull] [--if-stale] [--check] [--dir .] [--url <url>] [--token <t>]
|
|
6475
|
+
Pull the platform capability reference (markdown + JSON catalogs) into
|
|
6476
|
+
.octwin/platform-kb/ for the octwin-pack authoring skill, plus three maps:
|
|
6477
|
+
INDEX.md (the corpus) · SYMBOLS.md (every name -> its file; grep this) ·
|
|
6478
|
+
OUTLINE.md (every heading with its line number).
|
|
6479
|
+
|
|
6480
|
+
NO TOKEN NEEDED — the reference is platform stdlib and is served anonymously,
|
|
6481
|
+
and this command never sends one. --token is accepted and ignored, so an older
|
|
6482
|
+
script that passes it keeps working.
|
|
6483
|
+
|
|
6484
|
+
--if-stale poll the platform's content_hash first and skip the download when
|
|
6485
|
+
nothing changed. Cheap enough to run at the start of every session.
|
|
6486
|
+
--check report only, write nothing. Exit 0 = current, 2 = stale or never
|
|
6487
|
+
pulled, 1 = could not tell (offline / no reference served). For
|
|
5872
6488
|
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
|
-
|
|
6489
|
+
test: `octwin test [--dir .] [--require-kb] [--strict-primitives]
|
|
6490
|
+
Alias for \`octwin validate --remote\` — the full platform check, REQUIRED (a skipped full
|
|
6491
|
+
check is an error). --offline contradicts it; the other validate flags mean what they mean there.`,
|
|
6492
|
+
memos: `octwin memos [--all] [--json]
|
|
6493
|
+
Read what the platform has told you: a REPLY to a report you sent with
|
|
6494
|
+
\`octwin feedback\`, or a NOTICE published to every author (a new capability,
|
|
6495
|
+
a deprecation, a breaking change). Bodies are printed in full.
|
|
6496
|
+
Reading marks them read, so the reminder stops. --all re-reads history and
|
|
6497
|
+
acks nothing. --json to branch on \`severity\`
|
|
5881
6498
|
(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
|
|
6499
|
+
feedback: `octwin feedback [--dir .]
|
|
6500
|
+
Submit this pack's FEEDBACK.md to the platform team.
|
|
6501
|
+
The octwin-pack skill writes that file in its last step — findings grouped by
|
|
6502
|
+
owner (A · CLI, B · Platform, C · Skill/KB). This delivers it instead of asking
|
|
6503
|
+
you to paste it into a chat.
|
|
6504
|
+
Attaches the pack id + version from manifest.yaml, this CLI's version, and the
|
|
6505
|
+
content_hash of the capability reference in .octwin/platform-kb/ — triage needs
|
|
6506
|
+
the last two to tell "the platform is wrong" from "that was already fixed" or
|
|
5890
6507
|
"you were reading a stale reference". Needs the \`pack:deploy\` scope.`,
|
|
5891
6508
|
};
|
|
5892
6509
|
/**
|
|
@@ -5897,36 +6514,48 @@ octwin projects rm <slug> [--yes]
|
|
|
5897
6514
|
* (`cli.json-contract.test.ts`).
|
|
5898
6515
|
*/
|
|
5899
6516
|
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
|
-
|
|
6517
|
+
init: { run: cmdInit, json: false, flags: ['id', 'description', 'display-name'] },
|
|
6518
|
+
recipe: { run: cmdRecipe, json: false, flags: ['dry-run'] },
|
|
6519
|
+
validate: { run: cmdValidate, json: false, flags: ['offline', 'remote', 'require-kb', 'strict-primitives'] },
|
|
6520
|
+
login: { run: cmdLogin, json: 'lines', flags: ['wait'] }, // pending, then approved — two objects
|
|
6521
|
+
whoami: { run: cmdWhoami, json: false, flags: [] },
|
|
6522
|
+
deploy: { run: cmdDeploy, json: false, flags: ['force', 'seed', 'request-listing', 'withdraw-listing'] },
|
|
6523
|
+
pull: { run: cmdPull, json: false, flags: ['force', 'version'] },
|
|
6524
|
+
status: { run: cmdStatus, json: false, flags: [] },
|
|
6525
|
+
records: { run: cmdRecords, json: 'document', flags: ['contact', 'entity', 'fields-json', 'limit', 'note', 'offset', 'outcome', 'search', 'set', 'stage', 'to'] },
|
|
6526
|
+
work: { run: cmdWork, json: 'document', flags: ['action', 'dry-run', 'limit', 'note', 'offset', 'param', 'queues', 'to', 'unrouted'] },
|
|
6527
|
+
logs: { run: cmdLogs, json: 'document', flags: ['as'] },
|
|
6528
|
+
chat: { run: cmdChat, json: 'lines', flags: ['as', 'media', 'press', 'script', 'tap'] }, // one line per event the turn produced
|
|
6529
|
+
media: { run: cmdMedia, json: 'document', flags: ['out'] },
|
|
6530
|
+
seed: { run: cmdSeed, json: false, flags: ['pack', 'reset'] },
|
|
6531
|
+
projects: { run: cmdProjects, json: 'document', flags: ['archived', 'pack', 'slug', 'yes'] },
|
|
6532
|
+
agents: { run: cmdAgents, json: 'document', flags: ['disable-tool', 'enable-tool', 'enabled', 'model', 'overlay', 'prompt'] },
|
|
6533
|
+
orders: { run: cmdOrders, json: 'document', flags: ['force', 'limit', 'mark-returned', 'offset', 'payment', 'reason', 'status', 'to'] },
|
|
6534
|
+
analytics: { run: cmdAnalytics, json: 'document', flags: ['funnel', 'overview', 'milestones', 'trends', 'cost', 'stage', 'limit', 'offset'] },
|
|
6535
|
+
catalog: { run: cmdCatalog, json: 'document', flags: ['limit', 'offset', 'readiness', 'set-on-hand', 'to'] },
|
|
6536
|
+
scheduling: { run: cmdScheduling, json: 'document', flags: ['capacity', 'date', 'days', 'dow', 'end', 'from', 'kind', 'resource', 'slot-minutes', 'slots', 'start'] },
|
|
6537
|
+
automation: { run: cmdAutomation, json: 'document', flags: ['campaigns', 'chip', 'cursor', 'entity', 'kind', 'limit', 'offset', 'period', 'q', 'range'] },
|
|
6538
|
+
integrations: { run: cmdIntegrations, json: 'document', flags: ['limit', 'offset', 'operation', 'status'] },
|
|
6539
|
+
journeys: { run: cmdJourneys, json: 'document', flags: ['funnel', 'overview', 'goals', 'trends', 'cost', 'definition', 'stage', 'limit', 'offset'] },
|
|
6540
|
+
performance: { run: cmdPerformance, json: 'document', flags: ['detail', 'range', 'channel'] },
|
|
6541
|
+
usage: { run: cmdUsage, json: 'document', flags: [] },
|
|
6542
|
+
charts: { run: cmdCharts, json: 'document', flags: ['from', 'to'] },
|
|
6543
|
+
handoff: { run: cmdHandoff, json: 'document', flags: ['bucket', 'limit', 'offset'] },
|
|
6544
|
+
surveys: { run: cmdSurveys, json: 'document', flags: ['limit', 'offset'] },
|
|
6545
|
+
'platform-kb': { run: cmdPlatformKb, json: false, flags: ['check', 'if-stale'] },
|
|
6546
|
+
feedback: { run: cmdFeedback, json: false, flags: [] },
|
|
6547
|
+
memos: { run: cmdMemos, json: 'document', flags: ['all'] },
|
|
5927
6548
|
// A6: `test` = the full remote validate, not a validate-clone.
|
|
5928
|
-
test: { run: flags => cmdValidate({ ...flags, remote: true }), json: false },
|
|
6549
|
+
test: { run: flags => cmdValidate({ ...flags, remote: true }), json: false, flags: ['offline', 'remote', 'require-kb', 'strict-primitives'] },
|
|
5929
6550
|
};
|
|
6551
|
+
/**
|
|
6552
|
+
* The flags every command takes: the target (`--url` / `--tenant` / `--project` / `--token`, read by
|
|
6553
|
+
* `resolveTarget`), `--json` (held to `json:` above), `--help`, and `--dir` — the pack directory, which
|
|
6554
|
+
* `runCli` itself reads on every run to find the pulled KB (`readLocalKb`). Each command's OWN flags are its
|
|
6555
|
+
* `flags:` row — the one list `runCli` refuses against, and `cli.flags.test.ts` holds to the code that
|
|
6556
|
+
* reads them, to `help()` / `COMMAND_HELP`, and to the README.
|
|
6557
|
+
*/
|
|
6558
|
+
export const GLOBAL_FLAGS = ['url', 'tenant', 'project', 'token', 'json', 'help', 'dir'];
|
|
5930
6559
|
/** Exit code when a `--json` command ended without the result it promised — a bug in this CLI. */
|
|
5931
6560
|
export const EXIT_NO_RESULT = 70;
|
|
5932
6561
|
/**
|
|
@@ -5977,6 +6606,13 @@ export async function runCli(argv, io) {
|
|
|
5977
6606
|
if (flags.json === true && spec.json === false) {
|
|
5978
6607
|
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
6608
|
}
|
|
6609
|
+
// A flag the command does not read is REFUSED, before it runs. It used to be ignored, so a
|
|
6610
|
+
// typo — or a flag the command never learned, like `performance --channel` until 2026-10-11 —
|
|
6611
|
+
// ran the command with its default and printed an answer to a question nobody asked.
|
|
6612
|
+
const unknownFlags = Object.keys(flags).filter(k => k !== '_' && !GLOBAL_FLAGS.includes(k) && !spec.flags.includes(k));
|
|
6613
|
+
if (unknownFlags.length) {
|
|
6614
|
+
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`);
|
|
6615
|
+
}
|
|
5980
6616
|
// Compute the trailing nudges BEFORE the command runs, so they survive a `die()` — see the
|
|
5981
6617
|
// notice-channel block above. They are PRINTED after the command, by `flushNotices` here on the
|
|
5982
6618
|
// success path and by `exitNow` on every failure path. Fail-silent by construction (each helper
|