octwin-cli 0.1.21 → 0.3.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 +432 -350
- package/README.md +210 -188
- package/dist/index.js +1053 -91
- package/dist/lib/args-check.js +27 -10
- package/dist/lib/kb-path.js +76 -0
- package/dist/lib/page.js +61 -0
- package/dist/lib/render-check.js +20 -8
- package/dist/lib/validate.js +11 -2
- package/package.json +37 -37
- package/templates/starter/manifest.yaml +55 -54
package/dist/lib/args-check.js
CHANGED
|
@@ -27,13 +27,20 @@
|
|
|
27
27
|
* platform's expander. `octwin validate --remote` covers that case; this
|
|
28
28
|
* covers everything written directly in the flow.
|
|
29
29
|
*/
|
|
30
|
-
import {
|
|
30
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
31
31
|
import { join } from 'node:path';
|
|
32
|
-
|
|
32
|
+
import { lookupKbSubdir } from './kb-path.js';
|
|
33
|
+
/**
|
|
34
|
+
* Load `primitive -> ArgSpec` from the pulled KB.
|
|
35
|
+
*
|
|
36
|
+
* Three-state, like its sibling in `render-check.ts` — the caller must be able to
|
|
37
|
+
* distinguish "checked and clean" from "could not check". See `kb-path.ts`.
|
|
38
|
+
*/
|
|
33
39
|
export function loadPrimitiveArgSpecs(packDir) {
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
36
|
-
return null;
|
|
40
|
+
const lookup = lookupKbSubdir(packDir, 'primitives', dir => readdirSync(dir).some(f => f.endsWith('.json')));
|
|
41
|
+
if (lookup.state !== 'ok')
|
|
42
|
+
return { lookup, specs: null };
|
|
43
|
+
const dir = lookup.dir;
|
|
37
44
|
const out = new Map();
|
|
38
45
|
try {
|
|
39
46
|
for (const file of readdirSync(dir)) {
|
|
@@ -50,13 +57,17 @@ export function loadPrimitiveArgSpecs(packDir) {
|
|
|
50
57
|
keys: Object.keys(schema.properties),
|
|
51
58
|
required: Array.isArray(schema.required) ? schema.required : [],
|
|
52
59
|
open: ap !== undefined && ap !== false,
|
|
60
|
+
requiresOneOf: Array.isArray(entry.requiresOneOf) ? entry.requiresOneOf : [],
|
|
53
61
|
});
|
|
54
62
|
}
|
|
55
63
|
}
|
|
56
|
-
catch {
|
|
57
|
-
return null;
|
|
64
|
+
catch (err) {
|
|
65
|
+
return { lookup: { state: 'malformed', dir, reason: String(err?.message ?? err) }, specs: null };
|
|
58
66
|
}
|
|
59
|
-
|
|
67
|
+
if (out.size === 0) {
|
|
68
|
+
return { lookup: { state: 'malformed', dir, reason: 'no primitive entries parsed' }, specs: null };
|
|
69
|
+
}
|
|
70
|
+
return { lookup, specs: out };
|
|
60
71
|
}
|
|
61
72
|
/**
|
|
62
73
|
* Walk parsed YAML for `do:` nodes and report `args:` keys outside the
|
|
@@ -84,8 +95,9 @@ export function findArgViolations(doc, file, specs) {
|
|
|
84
95
|
: {};
|
|
85
96
|
const unknown = Object.keys(args).filter(k => !spec.keys.includes(k));
|
|
86
97
|
const missing = spec.required.filter(k => !(k in args));
|
|
87
|
-
|
|
88
|
-
|
|
98
|
+
const unscoped = spec.requiresOneOf.filter(g => !g.some(k => k in args));
|
|
99
|
+
if (unknown.length || missing.length || unscoped.length) {
|
|
100
|
+
findings.push({ file, primitive: obj.do, unknown, missing, unscoped, declared: [...spec.keys].sort() });
|
|
89
101
|
}
|
|
90
102
|
}
|
|
91
103
|
}
|
|
@@ -106,5 +118,10 @@ export function describeArgFinding(f) {
|
|
|
106
118
|
const plural = f.missing.length === 1 ? 'argument' : 'arguments';
|
|
107
119
|
parts.push(`missing required ${plural} ${f.missing.map(k => `'${k}'`).join(', ')}`);
|
|
108
120
|
}
|
|
121
|
+
for (const g of f.unscoped) {
|
|
122
|
+
// Worth its own sentence rather than a key list: the reader's question is
|
|
123
|
+
// "so what?", and for the scope groups the answer is a data leak.
|
|
124
|
+
parts.push(`no ${g.map(k => `'${k}'`).join(' or ')} — the read is project-wide and returns other contacts' records`);
|
|
125
|
+
}
|
|
109
126
|
return `${f.file}: \`${f.primitive}\` has ${parts.join('; ')}. It takes: ${f.declared.join(', ') || 'no arguments'}`;
|
|
110
127
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locate the pulled platform KB — the one resolver for all three readers.
|
|
3
|
+
*
|
|
4
|
+
* ## Why a walk-up
|
|
5
|
+
*
|
|
6
|
+
* `octwin platform-kb pull` writes `<cwd>/.octwin/platform-kb/`. Authors pull once
|
|
7
|
+
* at a repo root and then work inside `packs/<name>/`, which is how every other
|
|
8
|
+
* `.<tool>` directory behaves (`.git`, `node_modules`, `.env`). The readers used to
|
|
9
|
+
* join `.octwin` onto `packDir` exactly, so a repo-root pull covered no pack at
|
|
10
|
+
* all — and the failure was invisible, because a missing KB made each check
|
|
11
|
+
* silently return "nothing to check".
|
|
12
|
+
*
|
|
13
|
+
* ## Why three states and not `T | null`
|
|
14
|
+
*
|
|
15
|
+
* `catch { return null }` collapses *never pulled*, *corrupt* and *interrupted
|
|
16
|
+
* mid-pull* into one answer, and the advice attached to that answer ("run
|
|
17
|
+
* `octwin platform-kb pull`") is right for the first and only accidentally right
|
|
18
|
+
* for the others. A half-written pull that reads as "you haven't pulled yet" is
|
|
19
|
+
* how an author re-runs the same broken command twice. The reader tells the
|
|
20
|
+
* caller which of the three it found; the caller decides what to print.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, statSync } from 'node:fs';
|
|
23
|
+
import { dirname, join } from 'node:path';
|
|
24
|
+
/** How far up to look. Deep enough for a monorepo, shallow enough to stay local. */
|
|
25
|
+
const MAX_HOPS = 12;
|
|
26
|
+
/**
|
|
27
|
+
* Walk up from `packDir` looking for `.octwin/platform-kb/`.
|
|
28
|
+
*
|
|
29
|
+
* Stops at the filesystem root — `dirname(x) === x` is the fixed point on both
|
|
30
|
+
* POSIX (`/`) and Windows (`C:\`), and without that guard this loops forever.
|
|
31
|
+
* `MAX_HOPS` is the belt to that braces: a path that is neither absolute nor
|
|
32
|
+
* rooted the way we expect still terminates.
|
|
33
|
+
*/
|
|
34
|
+
export function findPlatformKbDir(packDir) {
|
|
35
|
+
let dir = packDir;
|
|
36
|
+
for (let hop = 0; hop < MAX_HOPS; hop++) {
|
|
37
|
+
const candidate = join(dir, '.octwin', 'platform-kb');
|
|
38
|
+
if (existsSync(candidate) && statSync(candidate).isDirectory())
|
|
39
|
+
return candidate;
|
|
40
|
+
const parent = dirname(dir);
|
|
41
|
+
if (parent === dir)
|
|
42
|
+
break;
|
|
43
|
+
dir = parent;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve one KB SUBDIRECTORY (`primitives`, `render-intents`) to a three-state
|
|
49
|
+
* answer. `readEntries` is the caller's parse; throwing from it means malformed,
|
|
50
|
+
* returning an empty result means the directory is there but has no usable
|
|
51
|
+
* entries — which is also malformed, not absent.
|
|
52
|
+
*/
|
|
53
|
+
export function lookupKbSubdir(packDir, subdir, isUsable) {
|
|
54
|
+
const kb = findPlatformKbDir(packDir);
|
|
55
|
+
if (!kb)
|
|
56
|
+
return { state: 'absent' };
|
|
57
|
+
const dir = join(kb, subdir);
|
|
58
|
+
if (!existsSync(dir)) {
|
|
59
|
+
return { state: 'malformed', dir: kb, reason: `'${subdir}/' is missing from the pulled KB` };
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
if (!isUsable(dir)) {
|
|
63
|
+
return { state: 'malformed', dir: kb, reason: `'${subdir}/' has no readable entries` };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return { state: 'malformed', dir: kb, reason: `'${subdir}/' could not be read — ${err?.message ?? err}` };
|
|
68
|
+
}
|
|
69
|
+
return { state: 'ok', dir };
|
|
70
|
+
}
|
|
71
|
+
/** The line to print when a check could not run. Same wording for all three. */
|
|
72
|
+
export function describeKbLookup(l, what) {
|
|
73
|
+
return l.state === 'absent'
|
|
74
|
+
? `${what} SKIPPED — no capability reference found. Run \`octwin platform-kb pull\` (a pull at your repo root covers every pack under it).`
|
|
75
|
+
: `${what} SKIPPED — the capability reference at ${l.dir} is unusable: ${l.reason}. Re-run \`octwin platform-kb pull\`.`;
|
|
76
|
+
}
|
package/dist/lib/page.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* page.ts — read the platform's ONE page envelope.
|
|
3
|
+
*
|
|
4
|
+
* Every list route returns `Page<T>` (`src/platform/core/utils/paging.ts`):
|
|
5
|
+
* `{ rows, total, offset, limit, has_more, next_offset }`. `rows` is deliberately
|
|
6
|
+
* the collection key for every domain, so a client needs exactly one reader.
|
|
7
|
+
*
|
|
8
|
+
* ## Why this file exists
|
|
9
|
+
*
|
|
10
|
+
* It did not, and the CLI paid for it. Four commands each hand-copied a
|
|
11
|
+
* per-domain collection noun — `json.records`, `json.cases`, `json.orders`,
|
|
12
|
+
* `json.products` — from the days when every route invented its own envelope.
|
|
13
|
+
* When the platform collapsed those into `Page`, the CLI kept reading keys that
|
|
14
|
+
* no longer existed, and `.records ?? []` degraded to an empty array instead of
|
|
15
|
+
* failing. The result shipped: `octwin records doctor` printed
|
|
16
|
+
* "5 record(s)" and then "(none — check your token)", blaming the user's
|
|
17
|
+
* credentials for a key rename.
|
|
18
|
+
*
|
|
19
|
+
* A `?? []` fallback on a response you do not control is a silent-wrong
|
|
20
|
+
* generator. Reading the envelope in ONE place, with a test pinned to the real
|
|
21
|
+
* shape, is the actual fix — a future rename breaks one function loudly rather
|
|
22
|
+
* than four commands quietly.
|
|
23
|
+
*
|
|
24
|
+
* `assets` (media) and `agents` are NOT `Page`-shaped — those routes predate it
|
|
25
|
+
* and return their own envelopes. Do not force them through here; they have no
|
|
26
|
+
* `total` semantics to honour.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Read a `Page` envelope defensively.
|
|
30
|
+
*
|
|
31
|
+
* Returns an empty view for a non-object or a body with no `rows` ARRAY, so a
|
|
32
|
+
* degraded read (`{ has_xrm: false, ...emptyPage() }`) and an error body both
|
|
33
|
+
* land on "no rows" without throwing. `total` stays **null** when absent rather
|
|
34
|
+
* than defaulting to 0 — a caller that prints "0 of 0" when the server said
|
|
35
|
+
* nothing is inventing a fact.
|
|
36
|
+
*/
|
|
37
|
+
export function readPage(json) {
|
|
38
|
+
const body = (json ?? {});
|
|
39
|
+
const rows = Array.isArray(body.rows) ? body.rows : [];
|
|
40
|
+
return {
|
|
41
|
+
rows,
|
|
42
|
+
total: typeof body.total === 'number' ? body.total : null,
|
|
43
|
+
hasMore: body.has_more === true,
|
|
44
|
+
nextOffset: typeof body.next_offset === 'number' ? body.next_offset : null,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* One line telling the author the list was cut short and how to see the rest —
|
|
49
|
+
* `has_more` existed on every response and no command surfaced it, so a
|
|
50
|
+
* truncated list was indistinguishable from a complete one.
|
|
51
|
+
*
|
|
52
|
+
* Empty string when there is nothing to say, so callers can print it
|
|
53
|
+
* unconditionally.
|
|
54
|
+
*/
|
|
55
|
+
export function morePageHint(page, command) {
|
|
56
|
+
if (!page.hasMore || page.nextOffset == null)
|
|
57
|
+
return '';
|
|
58
|
+
const shown = page.rows.length;
|
|
59
|
+
const of = page.total != null ? ` of ${page.total}` : '';
|
|
60
|
+
return ` … showing ${shown}${of} — next page: ${command} --offset ${page.nextOffset}`;
|
|
61
|
+
}
|
package/dist/lib/render-check.js
CHANGED
|
@@ -17,14 +17,23 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Degrades to a no-op when the KB has not been pulled — never invents a rule it
|
|
19
19
|
* cannot source, and never blocks `validate` for an author who hasn't pulled yet.
|
|
20
|
+
* It SAYS SO when it does: the caller gets a `KbLookup`, not a bare null, so a
|
|
21
|
+
* skipped check can never be reported as a passed one.
|
|
20
22
|
*/
|
|
21
|
-
import {
|
|
23
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
22
24
|
import { join } from 'node:path';
|
|
23
|
-
|
|
25
|
+
import { lookupKbSubdir } from './kb-path.js';
|
|
26
|
+
/**
|
|
27
|
+
* Load `render_intent -> allowed_keys` from the pulled KB.
|
|
28
|
+
*
|
|
29
|
+
* Returns the three-state lookup alongside the map so the caller can tell
|
|
30
|
+
* "checked, nothing wrong" from "could not check". See `kb-path.ts`.
|
|
31
|
+
*/
|
|
24
32
|
export function loadAllowedRenderKeys(packDir) {
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
27
|
-
return null;
|
|
33
|
+
const lookup = lookupKbSubdir(packDir, 'render-intents', dir => readdirSync(dir).some(f => f.endsWith('.json')));
|
|
34
|
+
if (lookup.state !== 'ok')
|
|
35
|
+
return { lookup, keys: null };
|
|
36
|
+
const dir = lookup.dir;
|
|
28
37
|
const out = new Map();
|
|
29
38
|
try {
|
|
30
39
|
for (const file of readdirSync(dir)) {
|
|
@@ -36,10 +45,13 @@ export function loadAllowedRenderKeys(packDir) {
|
|
|
36
45
|
}
|
|
37
46
|
}
|
|
38
47
|
}
|
|
39
|
-
catch {
|
|
40
|
-
return null;
|
|
48
|
+
catch (err) {
|
|
49
|
+
return { lookup: { state: 'malformed', dir, reason: String(err?.message ?? err) }, keys: null };
|
|
50
|
+
}
|
|
51
|
+
if (out.size === 0) {
|
|
52
|
+
return { lookup: { state: 'malformed', dir, reason: 'no render-intent entries parsed' }, keys: null };
|
|
41
53
|
}
|
|
42
|
-
return
|
|
54
|
+
return { lookup, keys: out };
|
|
43
55
|
}
|
|
44
56
|
/**
|
|
45
57
|
* Walk parsed YAML for objects carrying `render_intent` and report keys outside
|
package/dist/lib/validate.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* locally but the deploy rejected it".
|
|
16
16
|
*/
|
|
17
17
|
/** Declarative TEXT extensions a pure-YAML pack may contain. */
|
|
18
|
-
const ALLOWED_EXT = new Set(['yaml', 'yml', 'md', '
|
|
18
|
+
const ALLOWED_EXT = new Set(['yaml', 'yml', 'md', 'json']);
|
|
19
19
|
/** Binary extensions that travel as artifact BLOBS (base64 on the wire, bytea in
|
|
20
20
|
* storage). No `svg` — it is script-capable and these are served to browsers. */
|
|
21
21
|
const ALLOWED_BINARY_EXT = new Set(['jpg', 'jpeg', 'png', 'webp', 'gif', 'pdf']);
|
|
@@ -70,6 +70,15 @@ blobs = {}) {
|
|
|
70
70
|
errors.push(`'${p}': pack primitives are not allowed (pure-YAML packs only)`);
|
|
71
71
|
continue;
|
|
72
72
|
}
|
|
73
|
+
// `.sql` used to be allowed and was executed VERBATIM by the pack migration
|
|
74
|
+
// runner. Packs no longer own a database, so there is nothing for it to run
|
|
75
|
+
// against — and rejecting it here is finally enforceable, which is what this
|
|
76
|
+
// module's header has always claimed. Named explicitly so the author gets a
|
|
77
|
+
// reason instead of a bare "not an allowed pack file type".
|
|
78
|
+
if (ext(p) === 'sql') {
|
|
79
|
+
errors.push(`'${p}': packs have no database — SQL is not allowed. Model the data in \`xrm.yaml\`, which is first-class platform storage.`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
73
82
|
const e = ext(p);
|
|
74
83
|
if (CODE_EXT.has(e)) {
|
|
75
84
|
errors.push(`'${p}': executable code is not allowed (pure-YAML packs only)`);
|
|
@@ -80,7 +89,7 @@ blobs = {}) {
|
|
|
80
89
|
continue;
|
|
81
90
|
}
|
|
82
91
|
if (!ALLOWED_EXT.has(e)) {
|
|
83
|
-
errors.push(`'${p}': not an allowed pack file type (.yaml/.yml/.md/.
|
|
92
|
+
errors.push(`'${p}': not an allowed pack file type (.yaml/.yml/.md/.json, or an image: ${[...ALLOWED_BINARY_EXT].join('/')})`);
|
|
84
93
|
continue;
|
|
85
94
|
}
|
|
86
95
|
}
|
package/package.json
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "octwin-cli",
|
|
3
|
-
"version": "0.
|
|
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": ">=20"
|
|
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": ["octwin", "cequens", "cli", "whatsapp", "chatbot", "pack", "conversational-ai", "yaml"],
|
|
34
|
-
"author": "CEQUENS",
|
|
35
|
-
"homepage": "https://www.npmjs.com/package/octwin-cli",
|
|
36
|
-
"license": "MIT"
|
|
37
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "octwin-cli",
|
|
3
|
+
"version": "0.3.0",
|
|
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": ">=20"
|
|
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": ["octwin", "cequens", "cli", "whatsapp", "chatbot", "pack", "conversational-ai", "yaml"],
|
|
34
|
+
"author": "CEQUENS",
|
|
35
|
+
"homepage": "https://www.npmjs.com/package/octwin-cli",
|
|
36
|
+
"license": "MIT"
|
|
37
|
+
}
|
|
@@ -1,54 +1,55 @@
|
|
|
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
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
tools
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
# Default locale for platform-emitted copy + this pack's `$t()` lookups.
|
|
18
|
+
default_settings:
|
|
19
|
+
locale: ar
|
|
20
|
+
|
|
21
|
+
# Agent-callable flow tools. Each id maps to `flows/tools/<id>.flow.yaml`
|
|
22
|
+
# (+ its `<id>.locale.<lang>.yaml`). List the HOME hub first — it's the pack's
|
|
23
|
+
# front door; add each new tool here as you build it.
|
|
24
|
+
flows:
|
|
25
|
+
- home # the menu / navigation hub (see references/ux-patterns.md)
|
|
26
|
+
- browse # an example tool the hub invokes — replace with your real capability
|
|
27
|
+
|
|
28
|
+
# One agent. The platform's universal agent protocol is auto-appended by
|
|
29
|
+
# `createPackAgents` — `instructions:` carries only pack-supplied parts.
|
|
30
|
+
agents:
|
|
31
|
+
- id: assistant
|
|
32
|
+
display_name: 'Starter Assistant'
|
|
33
|
+
# `openrouter/` prefix routes via OpenRouter. Operators override the
|
|
34
|
+
# model per-project from the console.
|
|
35
|
+
default_model: 'openrouter/google/gemini-3.1-flash-lite-preview'
|
|
36
|
+
# Flow tools this agent can call (subset of `flows:` above; hub first).
|
|
37
|
+
tools:
|
|
38
|
+
- home
|
|
39
|
+
- browse
|
|
40
|
+
include_platform_protocol: true
|
|
41
|
+
# Instruction parts, joined with '\n\n'. `file:` paths are pack-root
|
|
42
|
+
# relative; `.md` files may use built-in placeholders like {{pack.id}}.
|
|
43
|
+
instructions:
|
|
44
|
+
- file: prompts/identity.md
|
|
45
|
+
# No working_memory block — deliberate, and you should keep it that way.
|
|
46
|
+
# Declaring one makes the platform add Mastra's updateWorkingMemory tool +
|
|
47
|
+
# a "call it every turn" instruction: an extra LLM generation on most turns,
|
|
48
|
+
# and small models (like the lite default above) parrot the
|
|
49
|
+
# <working_memory_data> envelope straight into user-visible replies. Session
|
|
50
|
+
# identity (name/lang/phone) already reaches the model via the platform's
|
|
51
|
+
# first-turn [ctx: …] header, and tools read it from the request context —
|
|
52
|
+
# so you gain nothing here. Leave it off unless you're on a large model and
|
|
53
|
+
# have a concrete slot schema to maintain.
|
|
54
|
+
default_memory:
|
|
55
|
+
lastMessages: 10 # size of the recalled message window
|