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.
@@ -0,0 +1,56 @@
1
+ /**
2
+ * A deploy the platform's checks refused — told the way the console tells it
3
+ * (`docs/design/pack-findings/`, capture 04): first what did NOT change (the live version keeps
4
+ * running), then what to fix, grouped by what a customer feels, then the author's own messages under
5
+ * each. It used to print the raw JSON body, which read like a crash.
6
+ *
7
+ * The body's `details` carry each finding's `kind` — the owner's sentence and whether customers notice it
8
+ * now — when the platform has one (`DryRunValidationError`); a detail without one is listed under
9
+ * "Also fix", never dropped. Pure, so the shape is tested without a network.
10
+ */
11
+ /** Messages shown per group — `octwin validate` lists every one. */
12
+ const PER_GROUP = 10;
13
+ export function formatChecksRefusal(body) {
14
+ const b = body;
15
+ if (!b || !Array.isArray(b.details) || b.details.length === 0)
16
+ return null;
17
+ const details = b.details.filter(d => d && typeof d.message === 'string');
18
+ const live = typeof b.live_version === 'string' ? b.live_version : null;
19
+ const groups = new Map();
20
+ const other = [];
21
+ for (const d of details) {
22
+ if (!d.kind) {
23
+ other.push(d);
24
+ continue;
25
+ }
26
+ const g = groups.get(d.kind.key);
27
+ if (g)
28
+ g.items.push(d);
29
+ else
30
+ groups.set(d.kind.key, { owner: d.kind.owner, now: d.kind.impact === 'now', items: [d] });
31
+ }
32
+ const ordered = [...groups.values()].sort((a, b2) => Number(b2.now) - Number(a.now) || b2.items.length - a.items.length);
33
+ const things = ordered.length + (other.length > 0 ? 1 : 0);
34
+ const lines = [
35
+ live
36
+ ? `✗ Not deployed — your live version ${live} keeps running, unchanged.`
37
+ : '✗ Not deployed — nothing on the platform changed.',
38
+ ` Fix ${things === 1 ? 'this' : `these ${things} things`}, then deploy again:`,
39
+ '',
40
+ ];
41
+ const messages = (items) => {
42
+ for (const d of items.slice(0, PER_GROUP))
43
+ lines.push(` [${d.where}] ${d.message}`);
44
+ if (items.length > PER_GROUP)
45
+ lines.push(` … and ${items.length - PER_GROUP} more — \`octwin validate\` lists them all`);
46
+ };
47
+ for (const g of ordered) {
48
+ lines.push(` ${g.now ? '!' : '·'} ${g.owner}${g.items.length > 1 ? ` (${g.items.length} places)` : ''}${g.now ? '' : ' (tidy-up)'}`);
49
+ messages(g.items);
50
+ }
51
+ if (other.length > 0) {
52
+ lines.push(ordered.length > 0 ? ' · Also fix:' : ' · Fix:');
53
+ messages(other);
54
+ }
55
+ return lines;
56
+ }
@@ -5,7 +5,7 @@
5
5
  * vendored copy of ~~`scripts/pack-scaffold/rename.ts`~~, deleted with the in-repo packs;
6
6
  * this is the only copy now.) `octwin init` copies the in-package
7
7
  * `templates/starter/` tree (packId `starter-kit`, agent `assistant`, flows
8
- * `home` + `browse`) then this rewrites the copy into a fresh pack:
8
+ * `home` + `request`) then this rewrites the copy into a fresh pack:
9
9
  * • `starter-kit` → <pack-id> (manifest id, comments, README)
10
10
  * • agent display name (when --display-name given)
11
11
  * • manifest description (when --description given)
@@ -0,0 +1,36 @@
1
+ /**
2
+ * What `octwin automation` needs to speak RULES — the project's rules as the platform's Automatic tab reads them
3
+ * (`/api/self/p/automation/…`). Pure, so the two decisions it makes are tested on their own:
4
+ *
5
+ * - **which rule an author means** (`resolveRuleKey`) — the key `octwin automation` prints, or the key of the older
6
+ * `automation.yaml` job it was compiled from: a `segment` job `cart_nudge` runs as the rule `segment:cart_nudge`,
7
+ * and the job key is what the author has in their own YAML;
8
+ * - **the server's words** (`phraseText`) — a rule's sentence and a line's headline arrive as `Phrase`s (text, a
9
+ * strong part, a quote with named slots), the same the console draws; the terminal prints them flat.
10
+ */
11
+ /** A sentence of `Phrase`s as one line of text — a quote in quotes, a slot as `{name}`. */
12
+ export function phraseText(phrases) {
13
+ return (phrases ?? []).map(p => p.text ?? p.strong
14
+ ?? (p.quote ? `"${p.quote.map(q => q.text ?? `{${q.slot ?? ''}}`).join('')}"` : '')).join('');
15
+ }
16
+ /**
17
+ * The rule an author typed: its key exactly, else the one rule whose key ends in `:<typed>` (an older job's key, or a
18
+ * hook's moment). Two such rules, or none, is an answer that names what does exist.
19
+ */
20
+ export function resolveRuleKey(typed, keys) {
21
+ if (keys.includes(typed))
22
+ return { key: typed };
23
+ const hits = keys.filter(k => k.endsWith(`:${typed}`));
24
+ if (hits.length === 1)
25
+ return { key: hits[0] };
26
+ if (hits.length > 1)
27
+ return { error: `'${typed}' could be ${hits.length} rules — ${hits.join(', ')}. Use the full key.` };
28
+ return { error: `no rule '${typed}' in this project.` + (keys.length ? ` Rules: ${keys.join(', ')}` : ' It runs none.') };
29
+ }
30
+ /** A time as the terminal shows it — `2026-10-22 09:00Z`; a missing one is a dash. */
31
+ export function shortTime(iso) {
32
+ if (!iso)
33
+ return '—';
34
+ const d = new Date(iso);
35
+ return Number.isNaN(d.getTime()) ? iso : `${d.toISOString().slice(0, 16).replace('T', ' ')}Z`;
36
+ }
package/package.json CHANGED
@@ -1,46 +1,46 @@
1
- {
2
- "name": "octwin-cli",
3
- "version": "0.8.16",
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.9.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": ">=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
+ }
@@ -0,0 +1,10 @@
1
+ # What the pack does by itself — rules: WHEN something changes (`on:`) or a time arrives (`at:`), FOR a
2
+ # record, DO actions. Guide: `craft-automation`; every change, time and action: `rules-guide`.
3
+ rules:
4
+ # The journey's goal, reached by the RECORD — so a request an operator creates counts too, not
5
+ # only chat ones. Guide: `craft-measure`.
6
+ - key: request_made
7
+ for: request
8
+ on: created
9
+ do:
10
+ - track: { journey: request, goal_reached: requested }
@@ -1,14 +1,13 @@
1
- # Pack-level slash-command declarations (OPTIONAL but recommended).
1
+ # Slash commands — matched BEFORE the AI Agent sees the message. Without this file a pack has none,
2
+ # and `/clear` reaches the agent as ordinary text. Guide: `craft-ux` §7 (the escape hatch).
2
3
  #
3
- # The platform's default `systemCommand` matches inbound text against these
4
- # BEFORE normal agent dispatch. Without a `commands.yaml`, a pack has NO slash
5
- # commands — e.g. `/clear` silently does nothing (it just goes to the agent as
6
- # text). Ship this file if you want `/clear` (and friends) to work like the
7
- # other packs.
8
- #
9
- # action: `close_conversation` (reset the conversation) | `noop` (reply only).
4
+ # action: `close_conversation` (start over) | `noop` (reply only) | `cancel_handoff` (a customer
5
+ # waiting for a person goes back to the AI Agent; takes no `reply:`).
6
+ # `reply:` is one text per language this pack speaks; each customer reads theirs.
10
7
 
11
- - match: '/clear'
12
- action: close_conversation
13
- reply: 'تم مسح المحادثة ✅ تقدر تبدأ من جديد.'
14
- source: '/clear'
8
+ - match: '/clear'
9
+ action: close_conversation
10
+ reply:
11
+ ar: 'تم مسح المحادثة ✅ ماذا تريد أن تفعل؟'
12
+ en: 'Cleared ✅ — what would you like to do?'
13
+ source: '/clear'
@@ -1,32 +1,34 @@
1
- # The HOME hub — your pack's front door / menu, and the FIRST tool the agent calls
2
- # on a greeting or an unclear message. A pure-render `list_picker`: one row per
3
- # primary capability, each routing a tap STRAIGHT to that tool via `on_select.invoke`
4
- # (no LLM turn). Add a row as you add each journey. Every shipped reference pack
5
- # ships a home flow like this — see references/ux-patterns.md in the octwin-pack skill.
6
- flow_id: home
7
- version: 1
8
- description: Main menu / welcome. Call on a greeting, an unclear or off-topic message, or "what can you do?".
9
-
10
- input:
11
- message:
12
- type: string
13
- optional: true
14
- describe: 'Optional ONE short greeting line YOU compose for this user (e.g. greet them by name).'
15
-
16
- entry:
17
- - else: true
18
- goto: show
19
-
20
- steps:
21
- show:
22
- - render:
23
- render_intent: list_picker # the universal chooser (WhatsApp list / web menu)
24
- header: '{$t("home.header")}'
25
- body: '$coalesce($input.message, $t("home.greeting"))' # agent line if given, else the default
26
- cta: '{$t("home.cta")}' # the WhatsApp list-button label (required for list_picker)
27
- items: # ONE row per capability — the tap routes via on_select
28
- - { title: '{$t("home.row_browse")}', on_select: { invoke: 'browse' } }
29
- # Add a row per tool as you build them, e.g.:
30
- # - { title: '{$t("home.row_x")}', on_select: { invoke: 'some-tool', with: { mode: x } } }
31
- memory_note: 'Showed the home menu.' # English breadcrumb — surfaced to the agent next turn
32
- - end: applied
1
+ # The HOME hub — the pack's front door, and the FIRST tool the agent calls on a greeting or an
2
+ # unclear message. A pure-render list: one row per capability, each tap routed STRAIGHT to its tool
3
+ # (no AI Agent turn). Add a row as you add each journey. Guide: `craft-ux` §1.
4
+ flow_id: home
5
+ version: 1
6
+ description: Main menu / welcome. Call on a greeting, an unclear or off-topic message, or "what can you do?".
7
+
8
+ input:
9
+ message:
10
+ type: string
11
+ optional: true
12
+ describe: 'Optional ONE short greeting line YOU compose for this customer (e.g. greet them by name).'
13
+
14
+ entry:
15
+ - else: true
16
+ goto: show
17
+
18
+ steps:
19
+ show:
20
+ # Marks the funnel's first stage — what the customer DID, not what the flow did. The goal is
21
+ # reached by the request record's own hook (xrm.yaml). Guide: `craft-measure`.
22
+ - track: { journey: request, stage_enter: menu }
23
+ - render:
24
+ render_intent: list_picker
25
+ header: '{$t("home.header")}'
26
+ body: '$coalesce($input.message, $t("home.greeting"))'
27
+ footer: '{$t("home.footer")}'
28
+ cta: '{$t("home.cta")}'
29
+ items:
30
+ - { title: '{$t("home.row_request")}', description: '{$t("home.row_request_about")}', on_select: { invoke: request } }
31
+ # A row per journey you add, e.g.:
32
+ # - { title: '{$t("home.row_x")}', on_select: { invoke: some-tool } }
33
+ memory_note: 'Showed the home menu.'
34
+ - end: applied
@@ -1,9 +1,8 @@
1
- # Every user-visible string for the home flow, keyed by `$t("home.<key>")`.
2
- # Lead labels with an emoji — it makes the menu scannable (all reference packs do this).
3
- flow_id: home
4
-
5
- strings:
6
- header: 'القائمة الرئيسية'
7
- greeting: 'أهلاً بك 👋 اختر من القائمة:'
8
- cta: 'اختر'
9
- row_browse: '📦 تصفّح'
1
+ flow_id: home
2
+ strings:
3
+ header: '👋 أهلاً بك'
4
+ greeting: 'كيف نقدر نساعدك اليوم؟'
5
+ footer: 'اكتب /clear في أي وقت لتبدأ من جديد'
6
+ cta: '📋 القائمة'
7
+ row_request: '🔧 طلب زيارة'
8
+ row_request_about: 'فني يأتي إليك'
@@ -0,0 +1,8 @@
1
+ flow_id: home
2
+ strings:
3
+ header: '👋 Welcome'
4
+ greeting: 'How can we help you today?'
5
+ footer: 'Type /clear any time to start over'
6
+ cta: '📋 Menu'
7
+ row_request: '🔧 Request a visit'
8
+ row_request_about: 'A technician comes to you'
@@ -0,0 +1,95 @@
1
+ flow_id: request
2
+ version: 1
3
+ description: Ask for a technician's visit. Call when the customer reports a problem at home. Pass `issue` only when they named it.
4
+
5
+ input:
6
+ area: { type: string, agent_fillable: false } # an area record id — only a tapped row sets it
7
+ issue: { type: string, optional: true, enum: [leak, power, ac, other] }
8
+
9
+ entry:
10
+ - else: true
11
+ goto: intake
12
+
13
+ steps:
14
+ intake:
15
+ - collect:
16
+ fields:
17
+ # A fetched picker that holds an ID: `bind:` named, `rows:` read off it, `page: true`
18
+ # (free when the list fits), `on_empty:` because a typed area name is not an area id,
19
+ # and `selection:` so the label travels with the id for the summary.
20
+ - field: area
21
+ label: '$t("request.ask_area")'
22
+ options:
23
+ body: '{$t("request.ask_area")}'
24
+ fetch: { do: record_list, args: { entity: area, all: true, limit: 50 }, bind: areas }
25
+ rows: '$areas.rows'
26
+ map: { value: '$item.id', label: '$item.fields.name', selection: { area_name: '$item.fields.name' } }
27
+ page: true
28
+ on_empty: no_areas
29
+ # A fixed set: `type: choice` with an `enum:` — the labels are `<pack_id>.issue.<value>` in
30
+ # the pack-level locale file, and `$enum_label` reads them back. A typed answer matches too.
31
+ - field: issue
32
+ type: choice
33
+ label: '$t("request.ask_issue")'
34
+ options:
35
+ body: '{$t("request.ask_issue")}'
36
+ enum: issue
37
+ values: [leak, power, ac, other]
38
+ # A phone: the native share on WhatsApp, a typed local number everywhere; `dial_code` reads
39
+ # 01005551111 as +20…
40
+ - field: phone
41
+ type: phone
42
+ dial_code: '20'
43
+ label: '$t("request.ask_phone")'
44
+ # A number with a reject rule. `when` is TRUE when the value is WRONG, and it runs only once
45
+ # the field has a value — so it needs no null guard.
46
+ - field: floor
47
+ type: number
48
+ label: '$t("request.ask_floor")'
49
+ validate:
50
+ when: '$state.floor < 0 || $state.floor > 60'
51
+ error: '{$t("request.floor_error")}'
52
+ # Optional: never blocks `complete`, never asked on its own — filled when the customer
53
+ # volunteers it or the agent passes it. (An optional `type: media` IS offered, with Skip.)
54
+ - field: notes
55
+ required: false
56
+ label: '$t("request.ask_notes")'
57
+ complete: [{ goto: confirm }]
58
+
59
+ confirm:
60
+ - use: approve_apply
61
+ with:
62
+ header: '{$t("request.confirm_header")}'
63
+ preview: '{$t("request.preview", { area: $state.area_name, issue: $enum_label($state.issue, "issue"), phone: $state.phone, floor: $state.floor, notes: $coalesce($state.notes, "—") })}'
64
+ apply: save
65
+ abort_render: { render_intent: text_card, body: '{$t("request.not_sent")}' }
66
+ on_other: intake # a typed correction re-enters the collect; filled fields are skipped
67
+ memory_note: 'Asked the customer to confirm the visit request.'
68
+
69
+ save:
70
+ - do: record_save
71
+ args:
72
+ entity: request
73
+ fields:
74
+ area: '$state.area'
75
+ issue: '$state.issue'
76
+ phone: '$state.phone'
77
+ floor: '$state.floor'
78
+ notes: '$coalesce($state.notes, "")'
79
+ bind: saved
80
+ outputs:
81
+ ok:
82
+ - render:
83
+ render_intent: detail_card
84
+ body: '{$t("request.saved", { number: $saved.record_number })}'
85
+ buttons:
86
+ - { title: '{$t("request.btn_home")}', on_select: { invoke: home } }
87
+ memory_note: 'Saved visit request #{$saved.record_number}.'
88
+ - end: applied
89
+ invalid:
90
+ - assign: { state.phone: null }
91
+ - goto: intake
92
+ failed: [{ end: failed }]
93
+
94
+ no_areas:
95
+ - end: { status: aborted, body: '{$t("request.no_areas")}' }
@@ -0,0 +1,14 @@
1
+ flow_id: request
2
+ strings:
3
+ ask_area: '📍 في أي منطقة أنت؟'
4
+ ask_issue: '🔧 ما المشكلة؟'
5
+ ask_phone: '📱 رقم موبايلك، ليتصل بك الفني؟'
6
+ ask_floor: '🏢 في أي دور؟'
7
+ floor_error: 'من فضلك اكتب دورًا بين 0 و60.'
8
+ ask_notes: 'هل هناك شيء آخر يجب أن نعرفه؟'
9
+ confirm_header: '📋 من فضلك راجع طلبك'
10
+ preview: "📍 {area}\n🔧 {issue}\n📱 {phone}\n🏢 الدور {floor}\n📝 {notes}"
11
+ not_sent: 'حسنًا، لم نرسل أي طلب.'
12
+ saved: "✅ رقم الطلب {number}. سيتصل بك الفني لتحديد موعد."
13
+ btn_home: '🏠 القائمة'
14
+ no_areas: 'لا نستقبل طلبات في أي منطقة حتى الآن. من فضلك تحدث مع الاستقبال من القائمة.'
@@ -0,0 +1,14 @@
1
+ flow_id: request
2
+ strings:
3
+ ask_area: '📍 Which area are you in?'
4
+ ask_issue: '🔧 What is the problem?'
5
+ ask_phone: '📱 Your mobile number, so the technician can call you?'
6
+ ask_floor: '🏢 Which floor?'
7
+ floor_error: 'A floor between 0 and 60, please.'
8
+ ask_notes: 'Anything else we should know?'
9
+ confirm_header: '📋 Please check your request'
10
+ preview: "📍 {area}\n🔧 {issue}\n📱 {phone}\n🏢 Floor {floor}\n📝 {notes}"
11
+ not_sent: 'OK, nothing was sent.'
12
+ saved: "✅ Request number {number}. A technician will call you to agree a time."
13
+ btn_home: '🏠 Menu'
14
+ no_areas: 'We are not taking requests in any area yet. Please talk to reception from the menu.'
@@ -0,0 +1,12 @@
1
+ # A journey is the funnel the owner reads on the Analytics and Performance pages: stages are what
2
+ # the customer did; the goal is the outcome. Guide: `craft-measure` (the journey recipe).
3
+ id: request
4
+ label: { en: 'Asking for a visit', ar: 'طلب زيارة' }
5
+ lifecycle:
6
+ reentry: after_exit # a customer who asks again later is a second run
7
+ terminal: { goal: requested } # what completes a run
8
+ stages:
9
+ - { id: menu, order: 1, label: { en: 'Opened the menu', ar: 'فتح القائمة' } }
10
+ - { id: requested, order: 2, label: { en: 'Requested a visit', ar: 'طلب زيارة' } }
11
+ goals:
12
+ - { id: requested, stage: requested, label: { en: 'Visit requested', ar: 'تم طلب الزيارة' } }
@@ -1,13 +1,9 @@
1
- # starter-kit — pack-wide Arabic strings.
2
- #
3
- # Entries here are shared across ALL flows in this pack (cross-flow labels,
4
- # common error messages, status verbs). Flow-specific strings live in
5
- # `flows/tools/<flow>.locale.ar.yaml`. Reference these via `$t("starter-kit.<key>")`
6
- # — every key is namespaced (pack-level by `pack_id`; flow-level by `flow_id`).
7
- # A bare `$t("<key>")` does NOT resolve (returns the raw key); the flow linter's
8
- # `locale-key-not-namespaced` rule fails the build on a bare key.
1
+ # Pack-wide Arabic strings, read as `$t("starter-kit.<key>")` — or by `$enum_label` for a `choice`
2
+ # field's labels (`issue.<value>`). A flow's own strings live beside the flow. Guide: `craft-language`.
9
3
 
10
4
  pack_id: starter-kit
11
5
  strings:
12
- # Example pack-level string — reference as `$t("starter-kit.greeting")`.
13
- greeting: 'أهلاً! إزاي أقدر أساعدك؟'
6
+ issue.leak: '💧 تسريب مياه'
7
+ issue.power: '⚡ الكهرباء'
8
+ issue.ac: '❄️ التكييف'
9
+ issue.other: '🔧 شيء آخر'
@@ -0,0 +1,9 @@
1
+ # Pack-wide English strings, read as `$t("starter-kit.<key>")` — or by `$enum_label` for a `choice`
2
+ # field's labels (`issue.<value>`). A flow's own strings live beside the flow. Guide: `craft-language`.
3
+
4
+ pack_id: starter-kit
5
+ strings:
6
+ issue.leak: '💧 Water leak'
7
+ issue.power: '⚡ Power'
8
+ issue.ac: '❄️ Air conditioning'
9
+ issue.other: '🔧 Something else'
@@ -1,65 +1,64 @@
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`.
1
+ # manifest.yaml — the one file that declares your pack. Required: id, version (a STRING,
2
+ # e.g. '1.0.0'), description. Everything else grows with your domain. `octwin validate` runs the
3
+ # platform's full check. Guide: `octwin platform-kb pull`, then read `craft-manifest`.
5
4
 
6
5
  id: starter-kit
7
- version: 1.0.0
8
- description: 'A hello bot — edit this to build your pack.'
6
+ version: '1.0.0'
7
+ description: 'A starter pack — a home menu and a visit request. Edit it into your business.'
9
8
 
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
9
  supported_channels: [whatsapp, web]
15
10
  required_adapters: [messaging]
16
11
 
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.
12
+ # The languages this pack speaks — held to the root locale.<lang>.yaml files, and every customer
13
+ # string must exist in each (a missing key refuses the deploy). Guide: `craft-language`.
14
+ supported_languages: [ar, en]
15
+
16
+ default_settings:
17
+ locale: ar # the language a customer reads before the platform knows theirs
18
+
19
+ # A VOICE NOTE cannot be blocked on WhatsApp, and an absent block means `passthrough` — no
20
+ # speech-to-text, so the AI Agent gets "a voice message arrived" and has to improvise.
21
+ # `transcribe` is right for a conversational pack. Guide: `craft-ux` §5.
23
22
  inbound_preprocessing:
24
23
  voicenote:
25
24
  mode: transcribe
26
25
 
27
- # Default locale for platform-emitted copy + this pack's `$t()` lookups.
28
- default_settings:
29
- locale: ar
26
+ # How the pack looks in a catalog a business browses. Optional; without it the catalog shows your
27
+ # id and description. Guide: `craft-manifest` (listing).
28
+ # listing:
29
+ # title: 'My business'
30
+ # tagline: 'The one line a buyer reads first'
31
+ # category: retail
32
+
33
+ # Your chat face — the name, colour and welcome the web widget shows. It SEEDS the project's settings
34
+ # at install only, so get it right before the first install. Guide: `craft-manifest` (widget).
35
+ # widget:
36
+ # name: 'My business'
37
+ # accent: '#0369a1'
38
+ # welcome:
39
+ # en: 'Hi! How can we help?'
40
+ # ar: 'أهلاً! كيف نقدر نساعدك؟'
30
41
 
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.
42
+ # The flow tools. Each id is `flows/tools/<id>.flow.yaml` with a locale file per language. The HOME
43
+ # hub comes first — it is the pack's front door. Guide: `craft-ux` §1.
34
44
  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.*
45
+ - home # the menu
46
+ - request # a visit request — the `craft-collect` recipe; replace it with your own journey
37
47
 
38
- # One agent. The platform's universal agent protocol is auto-appended by
39
- # `createPackAgents` — `instructions:` carries only pack-supplied parts.
40
48
  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).
49
+ - id: assistant
50
+ display_name: 'Assistant'
51
+ # A model id names its provider: `openrouter/<vendor>/<model>`. A bare id is refused.
52
+ # Operators can override the model per project in the console.
53
+ default_model: 'openrouter/google/gemini-3.1-flash-lite-preview'
54
+ # The flows this agent may call — its whole world. List the hub first. Guide: `craft-manifest`.
47
55
  tools:
48
56
  - home
49
- - browse
57
+ - request
50
58
  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
59
  instructions:
54
60
  - 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.
61
+ # No `working_memory:` — it adds a tool call on most turns and small models echo it to the
62
+ # customer. The platform already gives the agent the customer's name, language and phone.
64
63
  default_memory:
65
- lastMessages: 10 # size of the recalled message window
64
+ lastMessages: 10