octwin-cli 0.8.13 β 0.8.14
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 +1008 -965
- package/README.md +6 -1
- package/dist/cli.js +6007 -0
- package/dist/index.js +4 -5919
- package/dist/lib/media-lines.js +36 -0
- package/dist/lib/output.js +99 -0
- package/dist/lib/press.js +72 -0
- package/package.json +46 -46
- package/templates/starter/flows/tools/browse.flow.yaml +31 -31
- package/templates/starter/flows/tools/browse.locale.ar.yaml +6 -6
- package/templates/starter/manifest.yaml +65 -65
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pictures, files, voice notes and map pins a render carries, as lines a reader can see.
|
|
3
|
+
*
|
|
4
|
+
* A render's body and buttons were printed, its media were not β so a reply that SENT the full menu
|
|
5
|
+
* as a picture read as "π Our full menu β save the picture" with nothing under it, and a tester
|
|
6
|
+
* (the AI Author's reviewer, 2026-09-28) reported "no picture appeared" for a picture that was there.
|
|
7
|
+
* A carousel card gets a short `(image)` mark rather than its URL: a menu of seven dishes is shown
|
|
8
|
+
* many times in a test, and the mark is what a reader needs.
|
|
9
|
+
*
|
|
10
|
+
* Twin of the AI Author's test-chat printer (`src/routes/ai-author/_media-lines.ts` in the platform
|
|
11
|
+
* repo) β the same lines in `octwin chat` and in the AI Author's `chat`; change both.
|
|
12
|
+
*/
|
|
13
|
+
const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});
|
|
14
|
+
const str = (v) => (typeof v === 'string' ? v : v == null ? '' : String(v));
|
|
15
|
+
/** A media URL without its signed query (`?exp=β¦&token=β¦`) β the part that names the file. */
|
|
16
|
+
const where = (url) => str(url).split('?')[0];
|
|
17
|
+
/** The lines for a render's own media β a header picture, a file, a voice note, a pin. */
|
|
18
|
+
export function mediaLines(hint) {
|
|
19
|
+
const h = obj(hint);
|
|
20
|
+
const out = [];
|
|
21
|
+
if (h.header_image)
|
|
22
|
+
out.push(`[image] ${where(h.header_image)}`);
|
|
23
|
+
if (h.type === 'document')
|
|
24
|
+
out.push(`[file] ${str(h.filename) || 'a file'} β ${where(h.url)}`);
|
|
25
|
+
if (h.type === 'audio')
|
|
26
|
+
out.push(`[voice note]${typeof h.duration_sec === 'number' ? ` ${h.duration_sec}s` : ''} β ${where(h.url)}`);
|
|
27
|
+
if (h.type === 'location') {
|
|
28
|
+
const label = [str(h.name), str(h.address)].filter(Boolean).join(' β ');
|
|
29
|
+
out.push(`[location] ${label ? `${label} ` : ''}(${str(h.latitude)}, ${str(h.longitude)})`);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
/** A carousel card's mark: `(image) ` when it carries a picture. */
|
|
34
|
+
export function cardImageMark(card) {
|
|
35
|
+
return obj(card).image_url ? '(image) ' : '';
|
|
36
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where everything this CLI prints goes β the ONE module allowed to write to stdout or stderr
|
|
3
|
+
* (eslint `no-console` holds the rest of `src/` to it).
|
|
4
|
+
*
|
|
5
|
+
* ## The contract `--json` makes
|
|
6
|
+
*
|
|
7
|
+
* Under `--json`, **stdout carries the command's result and nothing else**, so a script can
|
|
8
|
+
* `JSON.parse` it. Everything a person reads β the `β Reading β¦` progress line, a table, a hint, a
|
|
9
|
+
* warning β goes to stderr instead. That is decided HERE, by `print`, not by each command remembering
|
|
10
|
+
* an `if (!asJson)` guard: a 5,600-line CLI with 486 `console.log` calls forgot it often enough that
|
|
11
|
+
* three commands were reported unparseable in one round (`records --json`, `projects create --json`,
|
|
12
|
+
* `logs --json` with nothing to list β 2026-10-01).
|
|
13
|
+
*
|
|
14
|
+
* - `print` β prose. stdout normally; stderr under `--json`.
|
|
15
|
+
* - `err` β a warning or an error. Always stderr.
|
|
16
|
+
* - `result` β the command's DATA. Under `--json`, the one thing on stdout (one pretty document, or
|
|
17
|
+
* one compact line per call for a streaming command); otherwise `human` renders it, or
|
|
18
|
+
* it prints as pretty JSON.
|
|
19
|
+
*
|
|
20
|
+
* A command declares which `--json` shape it has in the `COMMANDS` table (`cli.ts`); `runCli` then
|
|
21
|
+
* refuses `--json` on a command with none, and fails loudly when a `document` command ends without
|
|
22
|
+
* its result β the second half of the reported bug: `logs --json` with no conversations printed a
|
|
23
|
+
* sentence and no JSON at all.
|
|
24
|
+
*/
|
|
25
|
+
import { format } from 'node:util';
|
|
26
|
+
import { writeSync } from 'node:fs';
|
|
27
|
+
/**
|
|
28
|
+
* The process's streams. stderr is written synchronously (`writeSync(2, β¦)`) so a notice printed just
|
|
29
|
+
* before the process ends is never lost β the reason `flushNotices` always wrote that way.
|
|
30
|
+
*/
|
|
31
|
+
export const processIo = {
|
|
32
|
+
stdout: text => { process.stdout.write(text); },
|
|
33
|
+
stderr: text => {
|
|
34
|
+
try {
|
|
35
|
+
writeSync(2, text);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
process.stderr.write(text);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
export class Out {
|
|
43
|
+
io;
|
|
44
|
+
json;
|
|
45
|
+
results = 0;
|
|
46
|
+
code = 0;
|
|
47
|
+
constructor(io, json) {
|
|
48
|
+
this.io = io;
|
|
49
|
+
this.json = json;
|
|
50
|
+
}
|
|
51
|
+
/** `--json` was asked for, and this command has a JSON shape. */
|
|
52
|
+
get asJson() { return this.json !== false; }
|
|
53
|
+
/** Prose for a person. stdout normally; stderr under `--json`, so stdout carries only the result. */
|
|
54
|
+
print(...args) {
|
|
55
|
+
const line = format(...args) + '\n';
|
|
56
|
+
if (this.asJson)
|
|
57
|
+
this.io.stderr(line);
|
|
58
|
+
else
|
|
59
|
+
this.io.stdout(line);
|
|
60
|
+
}
|
|
61
|
+
/** A warning or an error β always stderr. */
|
|
62
|
+
err(...args) {
|
|
63
|
+
this.io.stderr(format(...args) + '\n');
|
|
64
|
+
}
|
|
65
|
+
/** Raw text to stderr, as given (no newline added) β the notice block writes its own. */
|
|
66
|
+
errRaw(text) {
|
|
67
|
+
this.io.stderr(text);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The command's data. Under `--json` the one thing on stdout: a `document` command emits it once
|
|
71
|
+
* (a second call is a bug in the command, and throws); a `lines` command emits one compact line per
|
|
72
|
+
* call. Otherwise `human` renders it β or, with none, it prints as pretty JSON, which is how a
|
|
73
|
+
* command with no table of its own shows a record.
|
|
74
|
+
*/
|
|
75
|
+
result(value, human) {
|
|
76
|
+
if (this.json === 'document') {
|
|
77
|
+
if (this.results > 0)
|
|
78
|
+
throw new Error('this command emitted a second --json result; a document has exactly one');
|
|
79
|
+
this.results++;
|
|
80
|
+
this.io.stdout(JSON.stringify(value ?? null, null, 2) + '\n');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (this.json === 'lines') {
|
|
84
|
+
this.results++;
|
|
85
|
+
this.io.stdout(JSON.stringify(value ?? null) + '\n');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (human)
|
|
89
|
+
human();
|
|
90
|
+
else
|
|
91
|
+
this.io.stdout(JSON.stringify(value ?? null, null, 2) + '\n');
|
|
92
|
+
}
|
|
93
|
+
/** How many results were emitted β `runCli` checks a `document` command emitted its one. */
|
|
94
|
+
get emitted() { return this.results; }
|
|
95
|
+
/** End this run non-zero without unwinding β a command that printed a full report of a failure. */
|
|
96
|
+
fail(code = 1) { this.code = code; }
|
|
97
|
+
/** The exit code a command chose with `fail`, else 0. */
|
|
98
|
+
get exitCode() { return this.code; }
|
|
99
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `press:` β tap a button by the LABEL the customer sees, not by its tap id.
|
|
3
|
+
*
|
|
4
|
+
* A form or checkout button carries its run's id (`t:resume:basket:run_id=R1;area=maadi`), which exists
|
|
5
|
+
* only once the reply arrives β so a `--script` could not reach past the first form step: `tap:` needs
|
|
6
|
+
* an id nobody can write in advance. `press:<label>` reads the reply the script just got and taps the
|
|
7
|
+
* button with that label, so a whole checkout is one script.
|
|
8
|
+
*
|
|
9
|
+
* Deterministic on purpose: a test must FAIL when the button it expects is gone, not have a guess work
|
|
10
|
+
* around it. Twin of the AI Author's `press` (`src/platform/core/ai-author/press.ts` in the platform
|
|
11
|
+
* repo) β the same rules, so a script means the same thing in both; change both.
|
|
12
|
+
*/
|
|
13
|
+
const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});
|
|
14
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
15
|
+
/** Every tappable button and list row in a render β never a link, which opens a URL instead. */
|
|
16
|
+
export function tapsOfHint(hint) {
|
|
17
|
+
const h = obj(hint);
|
|
18
|
+
const out = [];
|
|
19
|
+
const add = (b, kind) => {
|
|
20
|
+
const x = obj(b);
|
|
21
|
+
if (x.type === 'url' || !str(x.id) || !str(x.title))
|
|
22
|
+
return;
|
|
23
|
+
out.push({ id: str(x.id), title: str(x.title), kind });
|
|
24
|
+
};
|
|
25
|
+
for (const b of h.buttons ?? [])
|
|
26
|
+
add(b, 'button');
|
|
27
|
+
for (const s of h.sections ?? [])
|
|
28
|
+
for (const r of obj(s).rows ?? [])
|
|
29
|
+
add(r, 'row');
|
|
30
|
+
for (const c of h.cards ?? [])
|
|
31
|
+
for (const b of obj(c).buttons ?? [])
|
|
32
|
+
add(b, 'button');
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/** A label as a customer would say it: no emoji or symbols, no case, single spaces β any script. */
|
|
36
|
+
export function pressKey(s) {
|
|
37
|
+
return s.normalize('NFKC').toLowerCase()
|
|
38
|
+
.replace(/[\p{Extended_Pictographic}βοΈ]/gu, '')
|
|
39
|
+
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
|
40
|
+
.trim();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The one button a label names, trying in order: the exact label; the same words without emoji or
|
|
44
|
+
* case; the one label that contains those words. None, or several different buttons, is a stop.
|
|
45
|
+
*/
|
|
46
|
+
export function pickTap(taps, label) {
|
|
47
|
+
const buttons = [...new Set(taps.map(t => t.title))];
|
|
48
|
+
const one = (found, how) => {
|
|
49
|
+
const ids = [...new Set(found.map(t => t.id))];
|
|
50
|
+
if (ids.length === 1)
|
|
51
|
+
return { ok: true, tap: found[0], how };
|
|
52
|
+
if (ids.length > 1)
|
|
53
|
+
return { ok: false, reason: 'several', buttons };
|
|
54
|
+
return null;
|
|
55
|
+
};
|
|
56
|
+
const exact = one(taps.filter(t => t.title.trim() === label.trim()), 'exact');
|
|
57
|
+
if (exact)
|
|
58
|
+
return exact;
|
|
59
|
+
const key = pressKey(label);
|
|
60
|
+
if (!key)
|
|
61
|
+
return { ok: false, reason: 'none', buttons };
|
|
62
|
+
return one(taps.filter(t => pressKey(t.title) === key), 'same words')
|
|
63
|
+
?? one(taps.filter(t => pressKey(t.title).includes(key)), 'contains')
|
|
64
|
+
?? { ok: false, reason: 'none', buttons };
|
|
65
|
+
}
|
|
66
|
+
/** Why a script stopped at a `press:`, and what the last reply offered instead. */
|
|
67
|
+
export function pressStopLine(label, picked) {
|
|
68
|
+
const shown = picked.buttons.length ? `The last reply's buttons: ${picked.buttons.map(b => `"${b}"`).join(' Β· ')}.` : 'The last reply had no buttons.';
|
|
69
|
+
return picked.reason === 'several'
|
|
70
|
+
? `press "${label}" matches more than one button. ${shown} Use the exact label, or tap its id.`
|
|
71
|
+
: `press "${label}": no button with that label. ${shown}`;
|
|
72
|
+
}
|
package/package.json
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "octwin-cli",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "Octwin external-pack developer CLI (by CEQUENS) β scaffold, validate, deploy, and check pure-YAML packs on your tenant.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"octwin": "dist/index.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist",
|
|
11
|
-
"templates",
|
|
12
|
-
"README.md",
|
|
13
|
-
"CHANGELOG.md",
|
|
14
|
-
"LICENSE"
|
|
15
|
-
],
|
|
16
|
-
"engines": {
|
|
17
|
-
"node": ">=22"
|
|
18
|
-
},
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "tsc -p tsconfig.json",
|
|
21
|
-
"prepublishOnly": "npm run build"
|
|
22
|
-
},
|
|
23
|
-
"dependencies": {
|
|
24
|
-
"yaml": "^2.6.0"
|
|
25
|
-
},
|
|
26
|
-
"devDependencies": {
|
|
27
|
-
"@types/node": "^22.0.0",
|
|
28
|
-
"typescript": "^5.7.0"
|
|
29
|
-
},
|
|
30
|
-
"publishConfig": {
|
|
31
|
-
"access": "public"
|
|
32
|
-
},
|
|
33
|
-
"keywords": [
|
|
34
|
-
"octwin",
|
|
35
|
-
"cequens",
|
|
36
|
-
"cli",
|
|
37
|
-
"whatsapp",
|
|
38
|
-
"chatbot",
|
|
39
|
-
"pack",
|
|
40
|
-
"conversational-ai",
|
|
41
|
-
"yaml"
|
|
42
|
-
],
|
|
43
|
-
"author": "CEQUENS",
|
|
44
|
-
"homepage": "https://octwin.ai",
|
|
45
|
-
"license": "MIT"
|
|
46
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "octwin-cli",
|
|
3
|
+
"version": "0.8.14",
|
|
4
|
+
"description": "Octwin external-pack developer CLI (by CEQUENS) β scaffold, validate, deploy, and check pure-YAML packs on your tenant.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"octwin": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"templates",
|
|
12
|
+
"README.md",
|
|
13
|
+
"CHANGELOG.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"yaml": "^2.6.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^22.0.0",
|
|
28
|
+
"typescript": "^5.7.0"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"octwin",
|
|
35
|
+
"cequens",
|
|
36
|
+
"cli",
|
|
37
|
+
"whatsapp",
|
|
38
|
+
"chatbot",
|
|
39
|
+
"pack",
|
|
40
|
+
"conversational-ai",
|
|
41
|
+
"yaml"
|
|
42
|
+
],
|
|
43
|
+
"author": "CEQUENS",
|
|
44
|
+
"homepage": "https://octwin.ai",
|
|
45
|
+
"license": "MIT"
|
|
46
|
+
}
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
# EXAMPLE β replace it or delete it; do not leave it behind.
|
|
2
|
-
# An example capability the home hub invokes. Replace this with your real tool
|
|
3
|
-
# (record_list + a carousel, a collect: intake, a case_open, β¦). Kept trivial here
|
|
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.
|
|
11
|
-
flow_id: browse
|
|
12
|
-
version: 1
|
|
13
|
-
description: An example capability, reached from the home menu. Replace with your real tool.
|
|
14
|
-
|
|
15
|
-
input:
|
|
16
|
-
message:
|
|
17
|
-
type: string
|
|
18
|
-
optional: true
|
|
19
|
-
describe: 'Optional note from the user.'
|
|
20
|
-
|
|
21
|
-
entry:
|
|
22
|
-
- else: true
|
|
23
|
-
goto: show
|
|
24
|
-
|
|
25
|
-
steps:
|
|
26
|
-
show:
|
|
27
|
-
- render:
|
|
28
|
-
render_intent: text_card
|
|
29
|
-
body: '{$t("browse.body")}'
|
|
30
|
-
memory_note: '{$t("browse.memory_note")}'
|
|
31
|
-
- end: applied
|
|
1
|
+
# EXAMPLE β replace it or delete it; do not leave it behind.
|
|
2
|
+
# An example capability the home hub invokes. Replace this with your real tool
|
|
3
|
+
# (record_list + a carousel, a collect: intake, a case_open, β¦). Kept trivial here
|
|
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.
|
|
11
|
+
flow_id: browse
|
|
12
|
+
version: 1
|
|
13
|
+
description: An example capability, reached from the home menu. Replace with your real tool.
|
|
14
|
+
|
|
15
|
+
input:
|
|
16
|
+
message:
|
|
17
|
+
type: string
|
|
18
|
+
optional: true
|
|
19
|
+
describe: 'Optional note from the user.'
|
|
20
|
+
|
|
21
|
+
entry:
|
|
22
|
+
- else: true
|
|
23
|
+
goto: show
|
|
24
|
+
|
|
25
|
+
steps:
|
|
26
|
+
show:
|
|
27
|
+
- render:
|
|
28
|
+
render_intent: text_card
|
|
29
|
+
body: '{$t("browse.body")}'
|
|
30
|
+
memory_note: '{$t("browse.memory_note")}'
|
|
31
|
+
- end: applied
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# EXAMPLE copy for the example `browse` flow β delete it together with browse.flow.yaml.
|
|
2
|
-
flow_id: browse
|
|
3
|
-
|
|
4
|
-
strings:
|
|
5
|
-
body: 'ΩΩΨ§ ΩΨΈΩΨ± Ψ§ΩΩ
ΨΨͺΩΩ β¨ ΨΉΨ―ΩΩ Ψ£Ψ―Ψ§Ψ© "browse" ΩΨͺΨ¨ΩΩ Ω
ΩΨ²ΨͺΩ Ψ§ΩΨΩΩΩΩΨ©.'
|
|
6
|
-
memory_note: 'Showed the browse example.'
|
|
1
|
+
# EXAMPLE copy for the example `browse` flow β delete it together with browse.flow.yaml.
|
|
2
|
+
flow_id: browse
|
|
3
|
+
|
|
4
|
+
strings:
|
|
5
|
+
body: 'ΩΩΨ§ ΩΨΈΩΨ± Ψ§ΩΩ
ΨΨͺΩΩ β¨ ΨΉΨ―ΩΩ Ψ£Ψ―Ψ§Ψ© "browse" ΩΨͺΨ¨ΩΩ Ω
ΩΨ²ΨͺΩ Ψ§ΩΨΩΩΩΩΨ©.'
|
|
6
|
+
memory_note: 'Showed the browse example.'
|
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
# manifest.yaml β your pack is declared entirely by this one file. Required:
|
|
2
|
-
# id, version (a STRING, e.g. '1.0.0'), description. Everything else is optional
|
|
3
|
-
# and grows with your domain. `octwin validate` checks it locally; the platform
|
|
4
|
-
# re-validates the full schema on `octwin deploy`.
|
|
5
|
-
|
|
6
|
-
id: starter-kit
|
|
7
|
-
version: 1.0.0
|
|
8
|
-
description: 'A hello bot β edit this to build your pack.'
|
|
9
|
-
|
|
10
|
-
# Channels this pack supports + platform capabilities it needs. A pack with
|
|
11
|
-
# no media or search only needs `messaging`. Add embedding / vision / storage
|
|
12
|
-
# as your domain grows. (Your data model goes in `xrm.yaml` β packs have no
|
|
13
|
-
# database of their own.)
|
|
14
|
-
supported_channels: [whatsapp, web]
|
|
15
|
-
required_adapters: [messaging]
|
|
16
|
-
|
|
17
|
-
# What to do with a VOICE NOTE. WhatsApp cannot stop customers sending them, and an
|
|
18
|
-
# ABSENT block is not "nothing happens" β it means `passthrough`, i.e. no
|
|
19
|
-
# speech-to-text, so the agent gets "a voice message arrived, no transcript" and has
|
|
20
|
-
# to improvise. `transcribe` is the right default for a conversational pack; use
|
|
21
|
-
# `passthrough` when you handle the audio ref yourself, or `decline` (with a
|
|
22
|
-
# `notice:`) to ask for text instead.
|
|
23
|
-
inbound_preprocessing:
|
|
24
|
-
voicenote:
|
|
25
|
-
mode: transcribe
|
|
26
|
-
|
|
27
|
-
# Default locale for platform-emitted copy + this pack's `$t()` lookups.
|
|
28
|
-
default_settings:
|
|
29
|
-
locale: ar
|
|
30
|
-
|
|
31
|
-
# Agent-callable flow tools. Each id maps to `flows/tools/<id>.flow.yaml`
|
|
32
|
-
# (+ its `<id>.locale.<lang>.yaml`). List the HOME hub first β it's the pack's
|
|
33
|
-
# front door; add each new tool here as you build it.
|
|
34
|
-
flows:
|
|
35
|
-
- home # the menu / navigation hub (see references/ux-patterns.md)
|
|
36
|
-
- browse # an EXAMPLE tool the hub invokes β replace it, or delete this line AND flows/tools/browse.*
|
|
37
|
-
|
|
38
|
-
# One agent. The platform's universal agent protocol is auto-appended by
|
|
39
|
-
# `createPackAgents` β `instructions:` carries only pack-supplied parts.
|
|
40
|
-
agents:
|
|
41
|
-
- id: assistant
|
|
42
|
-
display_name: 'Starter Assistant'
|
|
43
|
-
# `openrouter/` prefix routes via OpenRouter. Operators override the
|
|
44
|
-
# model per-project from the console.
|
|
45
|
-
default_model: 'openrouter/google/gemini-3.1-flash-lite-preview'
|
|
46
|
-
# Flow tools this agent can call (subset of `flows:` above; hub first).
|
|
47
|
-
tools:
|
|
48
|
-
- home
|
|
49
|
-
- browse
|
|
50
|
-
include_platform_protocol: true
|
|
51
|
-
# Instruction parts, joined with '\n\n'. `file:` paths are pack-root
|
|
52
|
-
# relative; `.md` files may use built-in placeholders like {{pack.id}}.
|
|
53
|
-
instructions:
|
|
54
|
-
- file: prompts/identity.md
|
|
55
|
-
# No working_memory block β deliberate, and you should keep it that way.
|
|
56
|
-
# Declaring one makes the platform add Mastra's updateWorkingMemory tool +
|
|
57
|
-
# a "call it every turn" instruction: an extra LLM generation on most turns,
|
|
58
|
-
# and small models (like the lite default above) parrot the
|
|
59
|
-
# <working_memory_data> envelope straight into user-visible replies. Session
|
|
60
|
-
# identity (name/lang/phone) already reaches the model via the platform's
|
|
61
|
-
# first-turn [ctx: β¦] header, and tools read it from the request context β
|
|
62
|
-
# so you gain nothing here. Leave it off unless you're on a large model and
|
|
63
|
-
# have a concrete slot schema to maintain.
|
|
64
|
-
default_memory:
|
|
65
|
-
lastMessages: 10 # size of the recalled message window
|
|
1
|
+
# manifest.yaml β your pack is declared entirely by this one file. Required:
|
|
2
|
+
# id, version (a STRING, e.g. '1.0.0'), description. Everything else is optional
|
|
3
|
+
# and grows with your domain. `octwin validate` checks it locally; the platform
|
|
4
|
+
# re-validates the full schema on `octwin deploy`.
|
|
5
|
+
|
|
6
|
+
id: starter-kit
|
|
7
|
+
version: 1.0.0
|
|
8
|
+
description: 'A hello bot β edit this to build your pack.'
|
|
9
|
+
|
|
10
|
+
# Channels this pack supports + platform capabilities it needs. A pack with
|
|
11
|
+
# no media or search only needs `messaging`. Add embedding / vision / storage
|
|
12
|
+
# as your domain grows. (Your data model goes in `xrm.yaml` β packs have no
|
|
13
|
+
# database of their own.)
|
|
14
|
+
supported_channels: [whatsapp, web]
|
|
15
|
+
required_adapters: [messaging]
|
|
16
|
+
|
|
17
|
+
# What to do with a VOICE NOTE. WhatsApp cannot stop customers sending them, and an
|
|
18
|
+
# ABSENT block is not "nothing happens" β it means `passthrough`, i.e. no
|
|
19
|
+
# speech-to-text, so the agent gets "a voice message arrived, no transcript" and has
|
|
20
|
+
# to improvise. `transcribe` is the right default for a conversational pack; use
|
|
21
|
+
# `passthrough` when you handle the audio ref yourself, or `decline` (with a
|
|
22
|
+
# `notice:`) to ask for text instead.
|
|
23
|
+
inbound_preprocessing:
|
|
24
|
+
voicenote:
|
|
25
|
+
mode: transcribe
|
|
26
|
+
|
|
27
|
+
# Default locale for platform-emitted copy + this pack's `$t()` lookups.
|
|
28
|
+
default_settings:
|
|
29
|
+
locale: ar
|
|
30
|
+
|
|
31
|
+
# Agent-callable flow tools. Each id maps to `flows/tools/<id>.flow.yaml`
|
|
32
|
+
# (+ its `<id>.locale.<lang>.yaml`). List the HOME hub first β it's the pack's
|
|
33
|
+
# front door; add each new tool here as you build it.
|
|
34
|
+
flows:
|
|
35
|
+
- home # the menu / navigation hub (see references/ux-patterns.md)
|
|
36
|
+
- browse # an EXAMPLE tool the hub invokes β replace it, or delete this line AND flows/tools/browse.*
|
|
37
|
+
|
|
38
|
+
# One agent. The platform's universal agent protocol is auto-appended by
|
|
39
|
+
# `createPackAgents` β `instructions:` carries only pack-supplied parts.
|
|
40
|
+
agents:
|
|
41
|
+
- id: assistant
|
|
42
|
+
display_name: 'Starter Assistant'
|
|
43
|
+
# `openrouter/` prefix routes via OpenRouter. Operators override the
|
|
44
|
+
# model per-project from the console.
|
|
45
|
+
default_model: 'openrouter/google/gemini-3.1-flash-lite-preview'
|
|
46
|
+
# Flow tools this agent can call (subset of `flows:` above; hub first).
|
|
47
|
+
tools:
|
|
48
|
+
- home
|
|
49
|
+
- browse
|
|
50
|
+
include_platform_protocol: true
|
|
51
|
+
# Instruction parts, joined with '\n\n'. `file:` paths are pack-root
|
|
52
|
+
# relative; `.md` files may use built-in placeholders like {{pack.id}}.
|
|
53
|
+
instructions:
|
|
54
|
+
- file: prompts/identity.md
|
|
55
|
+
# No working_memory block β deliberate, and you should keep it that way.
|
|
56
|
+
# Declaring one makes the platform add Mastra's updateWorkingMemory tool +
|
|
57
|
+
# a "call it every turn" instruction: an extra LLM generation on most turns,
|
|
58
|
+
# and small models (like the lite default above) parrot the
|
|
59
|
+
# <working_memory_data> envelope straight into user-visible replies. Session
|
|
60
|
+
# identity (name/lang/phone) already reaches the model via the platform's
|
|
61
|
+
# first-turn [ctx: β¦] header, and tools read it from the request context β
|
|
62
|
+
# so you gain nothing here. Leave it off unless you're on a large model and
|
|
63
|
+
# have a concrete slot schema to maintain.
|
|
64
|
+
default_memory:
|
|
65
|
+
lastMessages: 10 # size of the recalled message window
|