octwin-cli 0.8.11 → 0.8.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/README.md +8 -5
- package/dist/index.js +616 -416
- package/dist/lib/args-check.js +55 -0
- package/dist/lib/chat-line.js +44 -0
- package/dist/lib/outcome.js +11 -0
- package/dist/lib/template-check.js +4 -2
- package/package.json +2 -2
- package/templates/starter/flows/tools/browse.flow.yaml +7 -0
- package/templates/starter/flows/tools/browse.locale.ar.yaml +1 -0
- package/templates/starter/manifest.yaml +1 -1
package/dist/lib/args-check.js
CHANGED
|
@@ -58,6 +58,11 @@ export function loadPrimitiveArgSpecs(packDir) {
|
|
|
58
58
|
required: Array.isArray(schema.required) ? schema.required : [],
|
|
59
59
|
open: ap !== undefined && ap !== false,
|
|
60
60
|
requiresOneOf: Array.isArray(entry.requiresOneOf) ? entry.requiresOneOf : [],
|
|
61
|
+
// Kept only when BOTH halves are present. A pair with no message would print a warning
|
|
62
|
+
// that names two argument keys and says nothing about why they are a trap — which is the
|
|
63
|
+
// sort of warning authors learn to scroll past.
|
|
64
|
+
warnsTogether: (entry.warnsTogether ?? [])
|
|
65
|
+
.filter((w) => Array.isArray(w?.args) && w.args.length > 0 && typeof w?.message === 'string' && w.message.length > 0),
|
|
61
66
|
});
|
|
62
67
|
}
|
|
63
68
|
}
|
|
@@ -74,6 +79,10 @@ export function loadPrimitiveArgSpecs(packDir) {
|
|
|
74
79
|
* primitive's declared set. Walks the whole document rather than just `steps:`
|
|
75
80
|
* — a `do:` is legal in a `foreach` body, a `collect` hook, an `outputs:` port
|
|
76
81
|
* body and `require:`, and a dropped argument is just as invisible there.
|
|
82
|
+
*
|
|
83
|
+
* Warnings come back on their own list rather than mixed into `ArgFinding`. They have a different
|
|
84
|
+
* severity, a different exit code, and a different sentence — folding them together would have
|
|
85
|
+
* meant either failing a build over a warning or hiding an error inside a warning count.
|
|
77
86
|
*/
|
|
78
87
|
export function findArgViolations(doc, file, specs) {
|
|
79
88
|
const findings = [];
|
|
@@ -107,6 +116,52 @@ export function findArgViolations(doc, file, specs) {
|
|
|
107
116
|
walk(doc, []);
|
|
108
117
|
return findings;
|
|
109
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Walk for `do:` nodes whose `args:` supply every member of a declared `warnsTogether` pair.
|
|
121
|
+
*
|
|
122
|
+
* A separate walk rather than a second output of `findArgViolations`, on purpose: that function
|
|
123
|
+
* skips a primitive whose schema is OPEN (`!spec.open`), because an open schema makes "unknown
|
|
124
|
+
* key" unanswerable. A `warnsTogether` pair is answerable either way — it names two keys that ARE
|
|
125
|
+
* declared — so gating it on closedness would silently drop the check for every passthrough
|
|
126
|
+
* primitive, which is exactly the shape of hole a scan grows without anyone noticing.
|
|
127
|
+
*/
|
|
128
|
+
export function findArgWarnings(doc, file, specs) {
|
|
129
|
+
const warnings = [];
|
|
130
|
+
const walk = (node, path) => {
|
|
131
|
+
if (Array.isArray(node)) {
|
|
132
|
+
node.forEach((v, i) => walk(v, [...path, i]));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!node || typeof node !== 'object')
|
|
136
|
+
return;
|
|
137
|
+
const obj = node;
|
|
138
|
+
if (typeof obj.do === 'string') {
|
|
139
|
+
const spec = specs.get(obj.do);
|
|
140
|
+
if (spec?.warnsTogether.length) {
|
|
141
|
+
const args = (obj.args && typeof obj.args === 'object' && !Array.isArray(obj.args))
|
|
142
|
+
? obj.args
|
|
143
|
+
: {};
|
|
144
|
+
for (const pair of spec.warnsTogether) {
|
|
145
|
+
// KEY PRESENCE only, matching the platform's own `checkPrimitiveArgs`. The values are
|
|
146
|
+
// almost always expressions (`record_id: '$found.record_id'`), so judging them would
|
|
147
|
+
// mean judging a string — and the trap is about the combination, not the values.
|
|
148
|
+
if (pair.args.every(k => k in args)) {
|
|
149
|
+
warnings.push({ file, primitive: obj.do, path: [...path], args: [...pair.args], message: pair.message });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const [k, v] of Object.entries(obj))
|
|
155
|
+
walk(v, [...path, k]);
|
|
156
|
+
};
|
|
157
|
+
walk(doc, []);
|
|
158
|
+
return warnings;
|
|
159
|
+
}
|
|
160
|
+
/** One line per warning: where, which pair, and the primitive's own reason. */
|
|
161
|
+
export function describeArgWarning(w) {
|
|
162
|
+
const at = w.line ? `${w.file}:${w.line}` : w.file;
|
|
163
|
+
return `${at}: \`${w.primitive}\` passes ${w.args.map(a => `'${a}'`).join(' + ')} together — ${w.message}`;
|
|
164
|
+
}
|
|
110
165
|
/** One-line human message per finding. */
|
|
111
166
|
export function describeArgFinding(f) {
|
|
112
167
|
const parts = [];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who sent a render on the chat stream — and whether it answers the turn.
|
|
3
|
+
*
|
|
4
|
+
* `octwin chat` reads ONE stream for a conversation, and more than the AI Agent writes to it:
|
|
5
|
+
*
|
|
6
|
+
* - a `notify` send — a `schedule_notify` reminder, a reaction's notification, a queued
|
|
7
|
+
* briefing — which answers nothing the author just sent (`source: 'notify'` on the web
|
|
8
|
+
* envelope);
|
|
9
|
+
* - a person holding the conversation in Live chat (`author.kind: 'human'`, with a name);
|
|
10
|
+
* - a platform line such as a handoff's join / return copy (`author.kind: 'system'`).
|
|
11
|
+
*
|
|
12
|
+
* Every one of them used to print as `← <body>`, exactly like the AI Agent's reply. The case
|
|
13
|
+
* that cost an author real time (chughtai, 2026-09-23): a reminder fired mid `--script`, the CLI
|
|
14
|
+
* took it for the turn's reply and ended the turn on it, and the real reply — which `octwin logs`
|
|
15
|
+
* showed the flow had produced correctly — looked lost.
|
|
16
|
+
*
|
|
17
|
+
* Kept free of I/O so the rules can be tested without a stream.
|
|
18
|
+
*/
|
|
19
|
+
export function chatSender(ev) {
|
|
20
|
+
if (ev.source === 'notify')
|
|
21
|
+
return 'notify';
|
|
22
|
+
if (ev.author?.kind === 'human')
|
|
23
|
+
return 'operator';
|
|
24
|
+
if (ev.author?.kind === 'system')
|
|
25
|
+
return 'platform';
|
|
26
|
+
return 'agent';
|
|
27
|
+
}
|
|
28
|
+
/** The label printed before the body — empty for the AI Agent, whose replies stay `← …`. */
|
|
29
|
+
export function chatLabel(ev) {
|
|
30
|
+
switch (chatSender(ev)) {
|
|
31
|
+
case 'notify': return '[notify] ';
|
|
32
|
+
case 'operator': return ev.author?.name ? `[operator: ${ev.author.name}] ` : '[operator] ';
|
|
33
|
+
case 'platform': return '[platform] ';
|
|
34
|
+
default: return '';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether this render is an ANSWER to the message the author just sent — what ends a turn's wait.
|
|
39
|
+
* A notify is not: it would have been sent whether or not the author wrote anything. An operator's
|
|
40
|
+
* line and a platform line are: in a held conversation they ARE the reply.
|
|
41
|
+
*/
|
|
42
|
+
export function answersTheTurn(ev) {
|
|
43
|
+
return chatSender(ev) !== 'notify';
|
|
44
|
+
}
|
package/dist/lib/outcome.js
CHANGED
|
@@ -56,6 +56,17 @@ export function reportChecks(checks, io) {
|
|
|
56
56
|
io.die(outcome.hint);
|
|
57
57
|
}
|
|
58
58
|
// eslint-disable-next-line no-fallthrough -- `die` returns never; the case above cannot exit.
|
|
59
|
+
case 'warnings': {
|
|
60
|
+
// On `log`, not `err`, and the run CONTINUES. A warning that stopped the run, or that
|
|
61
|
+
// arrived on the error stream, would be an error wearing a different word — and the next
|
|
62
|
+
// check is the one the author was actually waiting for.
|
|
63
|
+
const n = outcome.lines.length;
|
|
64
|
+
io.log(`⚠ ${n} ${outcome.noun}${n === 1 ? '' : 's'} — not fatal:`);
|
|
65
|
+
for (const line of outcome.lines)
|
|
66
|
+
io.log(` ⚠ ${line}`);
|
|
67
|
+
io.log(` ${outcome.hint}`);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
59
70
|
case 'not-run':
|
|
60
71
|
notRun.push({ label: check.label, lookup: outcome.lookup });
|
|
61
72
|
break;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* platform's own `approve_apply` is the common case: `confirm_title` misspelled
|
|
10
10
|
* silently falls back to the hardcoded Arabic default.
|
|
11
11
|
*
|
|
12
|
-
* PACK TEMPLATES SHADOW PLATFORM ONES. A pack may ship `templates/<name>.yaml`
|
|
12
|
+
* PACK TEMPLATES SHADOW PLATFORM ONES. A pack may ship `templates/<name>.template.yaml`
|
|
13
13
|
* of its own, and a pack template of the same name wins. So the caller passes
|
|
14
14
|
* the pack's own template names and this check stays silent about them entirely
|
|
15
15
|
* — it has no schema for a template it did not publish, and inventing one would
|
|
@@ -96,7 +96,9 @@ export function describeTemplateFinding(f) {
|
|
|
96
96
|
switch (f.kind) {
|
|
97
97
|
case 'unknown':
|
|
98
98
|
return `${f.file}: \`use: ${f.template}\` — no such template. Platform templates: ${f.known.join(', ')}`
|
|
99
|
-
|
|
99
|
+
// `.template.yaml`, not `.yaml`: the platform loads only that suffix, so a template saved
|
|
100
|
+
// as this hint used to say was silently ignored and the `use:` stayed unresolved.
|
|
101
|
+
+ `. (A template of your own must live at templates/${f.template}.template.yaml in the pack.)`;
|
|
100
102
|
case 'unknown-param':
|
|
101
103
|
return `${f.file}: \`use: ${f.template}\` passes ${f.keys.map(k => `'${k}'`).join(', ')}, which the template declares no param for `
|
|
102
104
|
+ `— dropped in silence, and the body sees undefined. Params: ${f.params.join(', ')}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "octwin-cli",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.13",
|
|
4
4
|
"description": "Octwin external-pack developer CLI (by CEQUENS) — scaffold, validate, deploy, and check pure-YAML packs on your tenant.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,6 +41,6 @@
|
|
|
41
41
|
"yaml"
|
|
42
42
|
],
|
|
43
43
|
"author": "CEQUENS",
|
|
44
|
-
"homepage": "https://
|
|
44
|
+
"homepage": "https://octwin.ai",
|
|
45
45
|
"license": "MIT"
|
|
46
46
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
# EXAMPLE — replace it or delete it; do not leave it behind.
|
|
1
2
|
# An example capability the home hub invokes. Replace this with your real tool
|
|
2
3
|
# (record_list + a carousel, a collect: intake, a case_open, …). Kept trivial here
|
|
3
4
|
# so the scaffold is chattable end-to-end: home menu → tap "browse" → this renders.
|
|
5
|
+
#
|
|
6
|
+
# If you do not keep it, delete THREE things together: this file, every
|
|
7
|
+
# `browse.locale.<lang>.yaml` beside it, and the `browse` line in manifest.yaml
|
|
8
|
+
# `flows:` (plus the home menu row that invokes it). A file left here with its
|
|
9
|
+
# manifest line removed never loads — but its copy is still checked, so a pack that
|
|
10
|
+
# adds a second language fails validation over a flow nobody uses.
|
|
4
11
|
flow_id: browse
|
|
5
12
|
version: 1
|
|
6
13
|
description: An example capability, reached from the home menu. Replace with your real tool.
|
|
@@ -33,7 +33,7 @@ default_settings:
|
|
|
33
33
|
# front door; add each new tool here as you build it.
|
|
34
34
|
flows:
|
|
35
35
|
- home # the menu / navigation hub (see references/ux-patterns.md)
|
|
36
|
-
- browse # an
|
|
36
|
+
- browse # an EXAMPLE tool the hub invokes — replace it, or delete this line AND flows/tools/browse.*
|
|
37
37
|
|
|
38
38
|
# One agent. The platform's universal agent protocol is auto-appended by
|
|
39
39
|
# `createPackAgents` — `instructions:` carries only pack-supplied parts.
|