@kaleidorg/mind 0.1.0 → 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.
Files changed (182) hide show
  1. package/dist/capabilities.d.ts +38 -0
  2. package/dist/capabilities.d.ts.map +1 -0
  3. package/dist/capabilities.js +41 -0
  4. package/dist/capabilities.js.map +1 -0
  5. package/dist/context/budget.d.ts +29 -0
  6. package/dist/context/budget.d.ts.map +1 -0
  7. package/dist/context/budget.js +36 -0
  8. package/dist/context/budget.js.map +1 -0
  9. package/dist/context/builder.d.ts +39 -0
  10. package/dist/context/builder.d.ts.map +1 -0
  11. package/dist/context/builder.js +77 -0
  12. package/dist/context/builder.js.map +1 -0
  13. package/dist/engine.d.ts +9 -0
  14. package/dist/engine.d.ts.map +1 -1
  15. package/dist/engine.js +1 -0
  16. package/dist/engine.js.map +1 -1
  17. package/dist/fastpath/fastpath.d.ts +38 -0
  18. package/dist/fastpath/fastpath.d.ts.map +1 -0
  19. package/dist/fastpath/fastpath.js +52 -0
  20. package/dist/fastpath/fastpath.js.map +1 -0
  21. package/dist/funnel.d.ts +117 -0
  22. package/dist/funnel.d.ts.map +1 -0
  23. package/dist/funnel.js +195 -0
  24. package/dist/funnel.js.map +1 -0
  25. package/dist/index.d.ts +44 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +36 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/kaleidoswap/contract.d.ts +72 -0
  30. package/dist/kaleidoswap/contract.d.ts.map +1 -0
  31. package/dist/kaleidoswap/contract.js +125 -0
  32. package/dist/kaleidoswap/contract.js.map +1 -0
  33. package/dist/knowledge/bitcoin-copilot.d.ts +11 -0
  34. package/dist/knowledge/bitcoin-copilot.d.ts.map +1 -0
  35. package/dist/knowledge/bitcoin-copilot.js +155 -0
  36. package/dist/knowledge/bitcoin-copilot.js.map +1 -0
  37. package/dist/knowledge/btc-map.d.ts +87 -0
  38. package/dist/knowledge/btc-map.d.ts.map +1 -0
  39. package/dist/knowledge/btc-map.js +365 -0
  40. package/dist/knowledge/btc-map.js.map +1 -0
  41. package/dist/knowledge/merchants.d.ts +24 -0
  42. package/dist/knowledge/merchants.d.ts.map +1 -0
  43. package/dist/knowledge/merchants.js +34 -0
  44. package/dist/knowledge/merchants.js.map +1 -0
  45. package/dist/knowledge/wallet.d.ts +34 -0
  46. package/dist/knowledge/wallet.d.ts.map +1 -0
  47. package/dist/knowledge/wallet.js +63 -0
  48. package/dist/knowledge/wallet.js.map +1 -0
  49. package/dist/lsps1/contract.d.ts +55 -0
  50. package/dist/lsps1/contract.d.ts.map +1 -0
  51. package/dist/lsps1/contract.js +91 -0
  52. package/dist/lsps1/contract.js.map +1 -0
  53. package/dist/memory/store.d.ts +40 -0
  54. package/dist/memory/store.d.ts.map +1 -0
  55. package/dist/memory/store.js +143 -0
  56. package/dist/memory/store.js.map +1 -0
  57. package/dist/memory/tool.d.ts +9 -0
  58. package/dist/memory/tool.d.ts.map +1 -0
  59. package/dist/memory/tool.js +70 -0
  60. package/dist/memory/tool.js.map +1 -0
  61. package/dist/memory/types.d.ts +68 -0
  62. package/dist/memory/types.d.ts.map +1 -0
  63. package/dist/memory/types.js +14 -0
  64. package/dist/memory/types.js.map +1 -0
  65. package/dist/rag/retriever.d.ts +30 -0
  66. package/dist/rag/retriever.d.ts.map +1 -0
  67. package/dist/rag/retriever.js +72 -0
  68. package/dist/rag/retriever.js.map +1 -0
  69. package/dist/rag/tool.d.ts +15 -0
  70. package/dist/rag/tool.d.ts.map +1 -0
  71. package/dist/rag/tool.js +42 -0
  72. package/dist/rag/tool.js.map +1 -0
  73. package/dist/rag/types.d.ts +44 -0
  74. package/dist/rag/types.d.ts.map +1 -0
  75. package/dist/rag/types.js +11 -0
  76. package/dist/rag/types.js.map +1 -0
  77. package/dist/rag/vector-store.d.ts +23 -0
  78. package/dist/rag/vector-store.d.ts.map +1 -0
  79. package/dist/rag/vector-store.js +72 -0
  80. package/dist/rag/vector-store.js.map +1 -0
  81. package/dist/recipe/asset-send.d.ts +15 -0
  82. package/dist/recipe/asset-send.d.ts.map +1 -0
  83. package/dist/recipe/asset-send.js +83 -0
  84. package/dist/recipe/asset-send.js.map +1 -0
  85. package/dist/recipe/kaleidoswap-atomic.d.ts +27 -0
  86. package/dist/recipe/kaleidoswap-atomic.d.ts.map +1 -0
  87. package/dist/recipe/kaleidoswap-atomic.js +111 -0
  88. package/dist/recipe/kaleidoswap-atomic.js.map +1 -0
  89. package/dist/recipe/payments.d.ts +15 -0
  90. package/dist/recipe/payments.d.ts.map +1 -0
  91. package/dist/recipe/payments.js +119 -0
  92. package/dist/recipe/payments.js.map +1 -0
  93. package/dist/recipe/receive.d.ts +14 -0
  94. package/dist/recipe/receive.d.ts.map +1 -0
  95. package/dist/recipe/receive.js +109 -0
  96. package/dist/recipe/receive.js.map +1 -0
  97. package/dist/recipe/runner.d.ts +42 -0
  98. package/dist/recipe/runner.d.ts.map +1 -0
  99. package/dist/recipe/runner.js +106 -0
  100. package/dist/recipe/runner.js.map +1 -0
  101. package/dist/recipe/swap.d.ts +16 -0
  102. package/dist/recipe/swap.d.ts.map +1 -0
  103. package/dist/recipe/swap.js +73 -0
  104. package/dist/recipe/swap.js.map +1 -0
  105. package/dist/recipe/types.d.ts +71 -0
  106. package/dist/recipe/types.d.ts.map +1 -0
  107. package/dist/recipe/types.js +13 -0
  108. package/dist/recipe/types.js.map +1 -0
  109. package/dist/skills/registry.d.ts.map +1 -1
  110. package/dist/skills/registry.js +20 -2
  111. package/dist/skills/registry.js.map +1 -1
  112. package/dist/tools/cli.d.ts +43 -0
  113. package/dist/tools/cli.d.ts.map +1 -0
  114. package/dist/tools/cli.js +61 -0
  115. package/dist/tools/cli.js.map +1 -0
  116. package/dist/tools/mcp.d.ts +3 -2
  117. package/dist/tools/mcp.d.ts.map +1 -1
  118. package/dist/tools/mcp.js +3 -2
  119. package/dist/tools/mcp.js.map +1 -1
  120. package/dist/wallet/confirm.d.ts +12 -0
  121. package/dist/wallet/confirm.d.ts.map +1 -0
  122. package/dist/wallet/confirm.js +67 -0
  123. package/dist/wallet/confirm.js.map +1 -0
  124. package/dist/wallet/contract.d.ts +57 -0
  125. package/dist/wallet/contract.d.ts.map +1 -0
  126. package/dist/wallet/contract.js +113 -0
  127. package/dist/wallet/contract.js.map +1 -0
  128. package/package.json +10 -5
  129. package/skills/README.md +6 -1
  130. package/skills/kaleido-lsps/SKILL.md +56 -0
  131. package/skills/kaleido-trading/SKILL.md +85 -18
  132. package/skills/merchant-finder/SKILL.md +87 -0
  133. package/skills/paid-data/SKILL.md +12 -0
  134. package/skills/wallet-assistant/SKILL.md +38 -0
  135. package/src/capabilities.ts +79 -0
  136. package/src/context/budget.ts +46 -0
  137. package/src/context/builder.ts +100 -0
  138. package/src/context/context.test.ts +87 -0
  139. package/src/engine.ts +6 -0
  140. package/src/fastpath/fastpath.test.ts +34 -0
  141. package/src/fastpath/fastpath.ts +70 -0
  142. package/src/funnel.test.ts +207 -0
  143. package/src/funnel.ts +285 -0
  144. package/src/index.ts +128 -0
  145. package/src/kaleidoswap/contract.test.ts +147 -0
  146. package/src/kaleidoswap/contract.ts +212 -0
  147. package/src/knowledge/bitcoin-copilot.ts +177 -0
  148. package/src/knowledge/btc-map.test.ts +188 -0
  149. package/src/knowledge/btc-map.ts +446 -0
  150. package/src/knowledge/knowledge.test.ts +63 -0
  151. package/src/knowledge/merchants.ts +49 -0
  152. package/src/knowledge/wallet.ts +84 -0
  153. package/src/lsps1/contract.test.ts +81 -0
  154. package/src/lsps1/contract.ts +132 -0
  155. package/src/memory/memory.test.ts +140 -0
  156. package/src/memory/store.ts +174 -0
  157. package/src/memory/tool.ts +76 -0
  158. package/src/memory/types.ts +76 -0
  159. package/src/rag/rag.test.ts +85 -0
  160. package/src/rag/retriever.ts +94 -0
  161. package/src/rag/tool.ts +55 -0
  162. package/src/rag/types.ts +49 -0
  163. package/src/rag/vector-store.ts +78 -0
  164. package/src/recipe/asset-send.ts +79 -0
  165. package/src/recipe/kaleidoswap-atomic.test.ts +138 -0
  166. package/src/recipe/kaleidoswap-atomic.ts +117 -0
  167. package/src/recipe/payments.ts +116 -0
  168. package/src/recipe/receive.ts +98 -0
  169. package/src/recipe/recipe.test.ts +193 -0
  170. package/src/recipe/runner.ts +134 -0
  171. package/src/recipe/swap.ts +74 -0
  172. package/src/recipe/types.ts +76 -0
  173. package/src/skills/registry.ts +21 -2
  174. package/src/skills/skills.test.ts +42 -0
  175. package/src/tools/cli.test.ts +53 -0
  176. package/src/tools/cli.ts +98 -0
  177. package/src/tools/mcp.ts +3 -2
  178. package/src/wallet/confirm.test.ts +57 -0
  179. package/src/wallet/confirm.ts +74 -0
  180. package/src/wallet/contract.test.ts +89 -0
  181. package/src/wallet/contract.ts +157 -0
  182. package/skills/kaleido-wallet/SKILL.md +0 -28
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Built-in "atomic swap on KaleidoSwap" recipe — trust-minimised chain.
3
+ *
4
+ * Most users want the simple market-order swap (`swapRecipe` over generic
5
+ * `get_swap_quote` / `execute_swap`). This recipe is the EXPLICIT atomic path:
6
+ * the user creates an RGB/LN receive invoice, the maker locks the swap, the
7
+ * user pays the maker's Lightning invoice, and the maker releases.
8
+ *
9
+ * Triggered only by explicit atomic-swap intent ("atomic swap", "trustless
10
+ * swap", "htlc swap") so it never preempts the simpler swap path for vague
11
+ * phrasings.
12
+ *
13
+ * "atomic swap 100000 sats for usdt"
14
+ * ↓ 1 model inference (slot extraction)
15
+ * kaleidoswap_get_quote ← maker prices the swap
16
+ * rln_create_rgb_invoice ← user's node prepares receive (if to_asset is RGB)
17
+ * rln_create_ln_invoice ← (alt) if to_asset is BTC
18
+ * kaleidoswap_atomic_init 🔒 ← maker locks the swap, returns its invoice
19
+ * rln_pay_invoice 🔒 ← user pays the maker
20
+ * kaleidoswap_atomic_execute 🔒 ← (final) maker releases the asset
21
+ *
22
+ * Two-or-three confirmation gates are intentional: each represents a distinct
23
+ * decision point. The host's confirm UI describes what's about to happen.
24
+ */
25
+
26
+ import type { Recipe } from './types.js';
27
+ import { extractSwap } from './swap.js';
28
+
29
+ const ATOMIC_INTENT =
30
+ /\b(atomic|trustless|htlc)\b.*\b(swap|exchange|convert|trade)\b|\b(swap|exchange|convert|trade)\b.*\b(atomic|trustless|htlc)\b/i;
31
+
32
+ function isBtc(asset: unknown): boolean {
33
+ return String(asset ?? '').toUpperCase() === 'BTC';
34
+ }
35
+
36
+ export const kaleidoswapAtomicRecipe: Recipe = {
37
+ name: 'kaleidoswap-atomic',
38
+ description:
39
+ 'Trust-minimised atomic swap on KaleidoSwap: quote, prepare a receive invoice on the user\'s node, lock the swap with the maker, pay, and execute.',
40
+ match: (t) => ATOMIC_INTENT.test(t),
41
+ triggers: ['atomic swap', 'trustless swap', 'htlc swap'],
42
+ slots: [
43
+ { name: 'from_asset', type: 'string', description: 'Asset to spend (BTC / USDT / XAUT)', required: true },
44
+ { name: 'to_asset', type: 'string', description: 'Asset to receive (BTC / USDT / XAUT)', required: true },
45
+ { name: 'amount', type: 'number', description: 'Amount of from_asset to swap' },
46
+ ],
47
+ extract: extractSwap,
48
+ confident: (s) => !!s.from_asset && !!s.to_asset && !!s.amount,
49
+ steps: [
50
+ // 1. Maker quotes the swap. Returns { quote_id, receive_amount, fees, ttl_ms, ... }.
51
+ {
52
+ tool: 'kaleidoswap_get_quote',
53
+ as: 'quote',
54
+ args: (ctx) => ({
55
+ from_asset: ctx.slots.from_asset,
56
+ to_asset: ctx.slots.to_asset,
57
+ amount: ctx.slots.amount,
58
+ }),
59
+ },
60
+ // 2a. User's node creates an RGB receive invoice (when to_asset is an RGB asset).
61
+ {
62
+ tool: 'rln_create_rgb_invoice',
63
+ as: 'receive_rgb',
64
+ args: (ctx) => {
65
+ const q = ctx.results.quote as { receive_amount?: number } | undefined;
66
+ return { asset: ctx.slots.to_asset, amount: q?.receive_amount };
67
+ },
68
+ skipIf: (ctx) => isBtc(ctx.slots.to_asset),
69
+ },
70
+ // 2b. User's node creates an LN receive invoice (when to_asset is BTC).
71
+ {
72
+ tool: 'rln_create_ln_invoice',
73
+ as: 'receive_ln',
74
+ args: (ctx) => {
75
+ const q = ctx.results.quote as { receive_amount?: number } | undefined;
76
+ return { amount_sats: q?.receive_amount };
77
+ },
78
+ skipIf: (ctx) => !isBtc(ctx.slots.to_asset),
79
+ },
80
+ // 3. Maker locks the swap. Returns { atomic_id, maker_invoice }. Spend-gated.
81
+ {
82
+ tool: 'kaleidoswap_atomic_init',
83
+ as: 'atomic',
84
+ args: (ctx) => {
85
+ const rgb = ctx.results.receive_rgb as { invoice?: string } | undefined;
86
+ const ln = ctx.results.receive_ln as { invoice?: string } | undefined;
87
+ const q = ctx.results.quote as { quote_id?: string } | undefined;
88
+ return {
89
+ quote_id: q?.quote_id,
90
+ receive_invoice: rgb?.invoice ?? ln?.invoice,
91
+ };
92
+ },
93
+ },
94
+ // 4. User pays the maker's Lightning invoice. Spend-gated by the wallet contract.
95
+ {
96
+ tool: 'rln_pay_invoice',
97
+ as: 'paid',
98
+ args: (ctx) => {
99
+ const a = ctx.results.atomic as { maker_invoice?: string } | undefined;
100
+ return { invoice: a?.maker_invoice };
101
+ },
102
+ },
103
+ ],
104
+ // 5. Maker releases the receive asset → swap completes. Spend-gated.
105
+ final: {
106
+ tool: 'kaleidoswap_atomic_execute',
107
+ args: (ctx) => {
108
+ const a = ctx.results.atomic as { atomic_id?: string } | undefined;
109
+ return { atomic_id: a?.atomic_id };
110
+ },
111
+ },
112
+ summary: (ctx) => {
113
+ const q = ctx.results.quote as { receive_amount?: number } | undefined;
114
+ const tail = q?.receive_amount ? ` ≈ ${q.receive_amount} ${ctx.slots.to_asset}` : '';
115
+ return `Atomic swap: ${ctx.slots.amount} ${ctx.slots.from_asset} → ${ctx.slots.to_asset}${tail}.`;
116
+ },
117
+ };
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Built-in "pay a contact" recipe — the flagship mobile multi-step flow.
3
+ *
4
+ * "pay bob 3 EUR" → resolve_contact → fiat_to_sats → send_payment 🔒
5
+ * "send 5000 sats to alice" → resolve_contact → send_payment 🔒
6
+ *
7
+ * Uses the canonical contract tools. The deterministic extractor handles most
8
+ * phrasings with no LLM at all (Tier-0); the runner falls back to one LLM
9
+ * extraction otherwise.
10
+ */
11
+
12
+ import type { Recipe, RecipeContext } from './types.js';
13
+
14
+ const CURRENCIES = /\b(sats?|sat|btc|usdt|xaut|eur|usd|gbp|dollars?|euros?|pounds?)\b/i;
15
+ const isOnchainOrInvoice = (s: string) => /^(ln(bc|tb|bcrt)|bc1|tb1|lnurl|[a-z0-9._-]+@)/i.test(s.trim());
16
+
17
+ function normCurrency(c?: string): string | undefined {
18
+ if (!c) return undefined;
19
+ const x = c.toLowerCase();
20
+ if (/^sat/.test(x)) return 'sats';
21
+ if (x === 'btc') return 'btc';
22
+ if (/dollar|^usd/.test(x)) return 'usd';
23
+ if (/euro|^eur/.test(x)) return 'eur';
24
+ if (/pound|^gbp/.test(x)) return 'gbp';
25
+ return x.toUpperCase();
26
+ }
27
+
28
+ /** "pay bob 3 eur", "send 5,000 sats to alice", "pay lnbc... ", "send 0.001 btc to bob" */
29
+ export function extractPayment(text: string): Record<string, unknown> | null {
30
+ const t = text.trim();
31
+ if (!/\b(pay|send|transfer)\b/i.test(t)) return null;
32
+
33
+ // Amount with optional k/m shorthand: "5k" → 5000, "2m" → 2_000_000.
34
+ const amtMatch = t.match(/(\d[\d.,]*)\s*([km])?\b/i);
35
+ let amountNum = amtMatch ? Number(amtMatch[1]!.replace(/,/g, '')) : undefined;
36
+ if (amountNum != null && amtMatch?.[2]) amountNum *= amtMatch[2].toLowerCase() === 'k' ? 1_000 : 1_000_000;
37
+ const amount = amountNum != null && !Number.isNaN(amountNum) ? String(amountNum) : undefined;
38
+ const currency = normCurrency(t.match(CURRENCIES)?.[1]);
39
+
40
+ // recipient: prefer "to <x>", else the token right after pay/send that isn't a number/currency
41
+ let recipient = t.match(/\bto\s+([^\s,]+)/i)?.[1];
42
+ if (!recipient) {
43
+ const after = t.match(/\b(?:pay|send|transfer)\s+([^\s,]+)/i)?.[1];
44
+ if (after && !/^\d/.test(after) && !CURRENCIES.test(after)) recipient = after;
45
+ }
46
+ if (!recipient && !amount) return null;
47
+ return {
48
+ recipient,
49
+ amount: amount ? Number(amount) : undefined,
50
+ currency,
51
+ };
52
+ }
53
+
54
+ /** Compute the sats amount when no fiat conversion step is needed. */
55
+ function directSats(ctx: RecipeContext): number | undefined {
56
+ const amount = Number(ctx.slots.amount);
57
+ if (!amount || Number.isNaN(amount)) return undefined;
58
+ const cur = String(ctx.slots.currency ?? 'sats').toLowerCase();
59
+ if (cur === 'btc') return Math.round(amount * 1e8);
60
+ return Math.round(amount); // sats (default)
61
+ }
62
+
63
+ const isFiat = (ctx: RecipeContext) => {
64
+ const c = String(ctx.slots.currency ?? '').toLowerCase();
65
+ return c !== '' && c !== 'sats' && c !== 'btc';
66
+ };
67
+
68
+ export const paymentsRecipe: Recipe = {
69
+ name: 'pay-contact',
70
+ description: 'Pay a contact or address — resolves the contact, converts fiat to sats, then sends (with confirmation).',
71
+ // A BTC/fiat spend intent — NOT a receive/invoice, and NOT an RGB-asset send
72
+ // ("send 10 USDT to bob" is handled by the asset-send recipe, so USDT/XAUT
73
+ // amounts are never mis-parsed as fiat).
74
+ match: (t) => /\b(pay|send|transfer)\b/i.test(t) && !/\b(invoice|receive|request|address|qr|deposit|usdt|tether|xaut|gold)\b/i.test(t),
75
+ triggers: ['pay', 'send', 'transfer'],
76
+ slots: [
77
+ { name: 'recipient', type: 'string', description: 'Who to pay: a contact name, Lightning address, or invoice', required: true },
78
+ { name: 'amount', type: 'number', description: 'The amount to send' },
79
+ { name: 'currency', type: 'string', description: 'Unit of the amount: sats, btc, or a fiat code like eur/usd' },
80
+ ],
81
+ extract: extractPayment,
82
+ confident: (s) => !!s.recipient,
83
+ steps: [
84
+ {
85
+ // Resolve a contact name → payable address (skip if already an address/invoice).
86
+ tool: 'resolve_contact',
87
+ as: 'contact',
88
+ args: (ctx) => ({ name: ctx.slots.recipient }),
89
+ skipIf: (ctx) => !ctx.slots.recipient || isOnchainOrInvoice(String(ctx.slots.recipient)),
90
+ },
91
+ {
92
+ // Convert fiat → sats (skip when the amount is already sats/btc or absent).
93
+ tool: 'fiat_to_sats',
94
+ as: 'conv',
95
+ args: (ctx) => ({ amount: ctx.slots.amount, currency: ctx.slots.currency }),
96
+ skipIf: (ctx) => !ctx.slots.amount || !isFiat(ctx),
97
+ },
98
+ ],
99
+ final: {
100
+ tool: 'send_payment',
101
+ args: (ctx) => {
102
+ const contact = ctx.results.contact as { ln_address?: string } | undefined;
103
+ const conv = ctx.results.conv as { sats?: number } | undefined;
104
+ return {
105
+ to: contact?.ln_address ?? ctx.slots.recipient,
106
+ amount_sats: conv?.sats ?? directSats(ctx),
107
+ };
108
+ },
109
+ },
110
+ summary: (ctx) => {
111
+ const conv = ctx.results.conv as { sats?: number } | undefined;
112
+ const sats = conv?.sats ?? directSats(ctx);
113
+ const to = (ctx.results.contact as { name?: string } | undefined)?.name ?? ctx.slots.recipient;
114
+ return sats ? `Sent ${sats.toLocaleString()} sats to ${to}.` : `Payment sent to ${to}.`;
115
+ },
116
+ };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Built-in "receive" recipe — create an invoice/address to get paid, in sats,
3
+ * BTC, an RGB asset, OR a fiat amount (converted to sats deterministically so a
4
+ * small model never mis-parses "$2.00").
5
+ *
6
+ * "invoice for 5000 sats" → BTC invoice, 5000 sats
7
+ * "create a payment request of $2" → fiat_to_sats(2, USD) → BTC invoice
8
+ * "invoice for 25 USDT on Liquid" → RGB asset invoice
9
+ * "an invoice" → BTC invoice, any amount
10
+ */
11
+
12
+ import type { Recipe } from './types.js';
13
+
14
+ const LAYER = /\b(spark|arkade|liquid|rln|lightning|rgb)\b/i;
15
+ const RECEIVE_INTENT = /\b(invoice|receive|deposit|get paid|pay me|payment request|request (a )?payment)\b/i;
16
+
17
+ type Kind = 'sats' | 'btc' | 'fiat' | 'asset';
18
+
19
+ function classify(text: string): { kind: Kind; code: string } {
20
+ if (/\$|\busd\b|dollar/i.test(text)) return { kind: 'fiat', code: 'USD' };
21
+ if (/€|\beur\b|euro/i.test(text)) return { kind: 'fiat', code: 'EUR' };
22
+ if (/£|\bgbp\b|pound/i.test(text)) return { kind: 'fiat', code: 'GBP' };
23
+ if (/usdt|tether/i.test(text)) return { kind: 'asset', code: 'USDT' };
24
+ if (/xaut|gold/i.test(text)) return { kind: 'asset', code: 'XAUT' };
25
+ if (/\bbtc\b|bitcoin/i.test(text)) return { kind: 'btc', code: 'BTC' };
26
+ return { kind: 'sats', code: 'BTC' }; // default: sats
27
+ }
28
+ function normLayer(l?: string): string | undefined {
29
+ if (!l) return undefined;
30
+ const x = l.toLowerCase();
31
+ if (x === 'lightning' || x === 'rgb') return 'rln';
32
+ if (['spark', 'arkade', 'liquid', 'rln'].includes(x)) return x;
33
+ return undefined;
34
+ }
35
+ function parseAmount(t: string): number | undefined {
36
+ const m = t.match(/(\d[\d.,]*)\s*([km])?\b/i);
37
+ if (!m) return undefined;
38
+ let n = Number(m[1]!.replace(/,/g, ''));
39
+ if (m[2]) n *= m[2].toLowerCase() === 'k' ? 1_000 : 1_000_000;
40
+ return Number.isNaN(n) ? undefined : n;
41
+ }
42
+
43
+ export function extractReceive(text: string): Record<string, unknown> | null {
44
+ const t = text.trim();
45
+ if (!RECEIVE_INTENT.test(t)) return null;
46
+ const amount = parseAmount(t);
47
+ const { kind, code } = classify(t);
48
+ const layer = normLayer(t.match(LAYER)?.[1]);
49
+ return { ...(amount != null ? { amount } : {}), kind, currency: code, ...(layer ? { layer } : {}) };
50
+ }
51
+
52
+ export const receiveRecipe: Recipe = {
53
+ name: 'receive',
54
+ description: 'Create an invoice/address to receive funds — sats, BTC, a fiat amount (converted), or an RGB asset.',
55
+ match: (t) => RECEIVE_INTENT.test(t) && !/\b(send|transfer|swap|buy|sell)\b/i.test(t),
56
+ triggers: ['invoice', 'receive', 'deposit', 'payment request'],
57
+ slots: [
58
+ { name: 'amount', type: 'number', description: 'Amount to receive (optional)' },
59
+ { name: 'currency', type: 'string', description: 'sats | BTC | USD | EUR | USDT | XAUT' },
60
+ { name: 'layer', type: 'string', description: 'Rail: spark, rln, liquid (optional)' },
61
+ ],
62
+ extract: extractReceive,
63
+ confident: () => true,
64
+ steps: [
65
+ {
66
+ // Fiat → sats, only when the amount is fiat-denominated.
67
+ tool: 'fiat_to_sats',
68
+ as: 'conv',
69
+ args: (ctx) => ({ amount: ctx.slots.amount, currency: ctx.slots.currency }),
70
+ skipIf: (ctx) => ctx.slots.kind !== 'fiat' || ctx.slots.amount == null,
71
+ },
72
+ ],
73
+ final: {
74
+ tool: 'create_invoice',
75
+ args: (ctx) => {
76
+ const kind = ctx.slots.kind as Kind;
77
+ const conv = ctx.results.conv as { sats?: number } | undefined;
78
+ let asset = 'BTC';
79
+ let amount = ctx.slots.amount as number | undefined;
80
+ if (kind === 'asset') asset = String(ctx.slots.currency);
81
+ else if (kind === 'fiat') amount = conv?.sats;
82
+ else if (kind === 'btc' && amount != null) amount = Math.round(amount * 1e8); // sats
83
+ return { asset, amount, layer: ctx.slots.layer };
84
+ },
85
+ },
86
+ summary: (ctx, result) => {
87
+ const r = result as { invoice?: string; address?: string } | undefined;
88
+ const dest = r?.invoice ?? r?.address ?? '';
89
+ const kind = ctx.slots.kind as Kind;
90
+ const conv = ctx.results.conv as { sats?: number } | undefined;
91
+ let amt = 'any amount';
92
+ if (kind === 'fiat' && ctx.slots.amount != null) amt = `${ctx.slots.currency} ${ctx.slots.amount} (${(conv?.sats ?? 0).toLocaleString()} sats)`;
93
+ else if (kind === 'asset' && ctx.slots.amount != null) amt = `${Number(ctx.slots.amount).toLocaleString()} ${ctx.slots.currency}`;
94
+ else if (ctx.slots.amount != null) amt = `${Number(ctx.slots.amount).toLocaleString()} ${kind === 'btc' ? 'BTC' : 'sats'}`;
95
+ const label = kind === 'asset' ? String(ctx.slots.currency) : 'BTC';
96
+ return dest ? `Here's your ${label} invoice for ${amt}:\n\n${dest}` : `Created an invoice for ${amt}.`;
97
+ },
98
+ };
@@ -0,0 +1,193 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { ToolRegistry } from '../tools/registry.js';
3
+ import { InProcessToolSource } from '../tools/in-process.js';
4
+ import type { LLMProvider } from '../providers/types.js';
5
+ import { runRecipe, RecipeRegistry } from './runner.js';
6
+ import { paymentsRecipe, extractPayment } from './payments.js';
7
+ import { swapRecipe, extractSwap } from './swap.js';
8
+ import { receiveRecipe, extractReceive } from './receive.js';
9
+ import { assetSendRecipe, extractAssetSend } from './asset-send.js';
10
+ import { paymentsRecipe as _pay } from './payments.js';
11
+
12
+ // Stub contract tools: resolve_contact, fiat_to_sats, send_payment (spend).
13
+ function stubTools(spy?: { send?: (a: any) => void }) {
14
+ const src = new InProcessToolSource('wallet', [
15
+ { name: 'resolve_contact', description: '', parameters: { type: 'object', properties: {} }, handler: async ({ name }) => ({ name, ln_address: `${name}@kaleidoswap.com` }) },
16
+ { name: 'fiat_to_sats', description: '', parameters: { type: 'object', properties: {} }, handler: async ({ amount }) => ({ sats: Math.round(Number(amount) * 1000) }) },
17
+ { name: 'send_payment', description: '', parameters: { type: 'object', properties: {} }, requiresConfirmation: true, handler: async (a) => { spy?.send?.(a); return { status: 'SUCCESS', payment_hash: 'h' }; } },
18
+ ]);
19
+ return new ToolRegistry([src]);
20
+ }
21
+
22
+ const approve: LLMProvider = { name: 'x', runTurn: async () => ({ text: '', rawContent: '', toolCalls: [] }) };
23
+
24
+ describe('extractPayment (deterministic Tier-0)', () => {
25
+ it('parses contact + fiat', () => {
26
+ expect(extractPayment('pay bob 3 eur')).toEqual({ recipient: 'bob', amount: 3, currency: 'eur' });
27
+ });
28
+ it('parses "send N sats to X"', () => {
29
+ expect(extractPayment('send 5,000 sats to alice')).toEqual({ recipient: 'alice', amount: 5000, currency: 'sats' });
30
+ });
31
+ it('parses btc + onchain-ish recipient', () => {
32
+ expect(extractPayment('send 0.001 btc to bob')).toEqual({ recipient: 'bob', amount: 0.001, currency: 'btc' });
33
+ });
34
+ it('expands k/m shorthand (no 1000x under-send)', () => {
35
+ expect(extractPayment('send 5k sats to bob')).toEqual({ recipient: 'bob', amount: 5000, currency: 'sats' });
36
+ expect(extractPayment('send 2m sats to alice')).toEqual({ recipient: 'alice', amount: 2_000_000, currency: 'sats' });
37
+ });
38
+ it('returns null for non-payment text', () => {
39
+ expect(extractPayment('what is my balance')).toBeNull();
40
+ });
41
+ });
42
+
43
+ describe('runRecipe — pay a contact', () => {
44
+ it('fiat path: resolve → fiat_to_sats → confirm → send', async () => {
45
+ const sent: any[] = [];
46
+ const tools = stubTools({ send: (a) => sent.push(a) });
47
+ const onConfirm = vi.fn(async () => ({ approved: true }));
48
+ const res = await runRecipe(paymentsRecipe, 'pay bob 3 eur', { provider: approve, tools, onConfirm });
49
+
50
+ expect(res.status).toBe('done');
51
+ expect(res.inferences).toBe(0); // deterministic extraction, no LLM
52
+ expect(onConfirm).toHaveBeenCalledOnce();
53
+ expect(res.results.contact).toMatchObject({ ln_address: 'bob@kaleidoswap.com' });
54
+ expect(sent[0]).toEqual({ to: 'bob@kaleidoswap.com', amount_sats: 3000 }); // 3 * 1000
55
+ expect(res.text).toContain('3,000 sats');
56
+ });
57
+
58
+ it('sats path skips fiat_to_sats', async () => {
59
+ const sent: any[] = [];
60
+ const tools = stubTools({ send: (a) => sent.push(a) });
61
+ const res = await runRecipe(paymentsRecipe, 'send 5000 sats to alice', { provider: approve, tools, onConfirm: async () => ({ approved: true }) });
62
+ expect(res.status).toBe('done');
63
+ expect(res.results.conv).toBeUndefined(); // fiat step skipped
64
+ expect(sent[0]).toEqual({ to: 'alice@kaleidoswap.com', amount_sats: 5000 });
65
+ });
66
+
67
+ it('denied confirmation → cancelled, nothing sent', async () => {
68
+ const sent: any[] = [];
69
+ const tools = stubTools({ send: (a) => sent.push(a) });
70
+ const res = await runRecipe(paymentsRecipe, 'pay bob 3 eur', { provider: approve, tools, onConfirm: async () => ({ approved: false }) });
71
+ expect(res.status).toBe('cancelled');
72
+ expect(sent).toHaveLength(0);
73
+ });
74
+
75
+ it('falls back to ONE LLM extraction when regex misses', async () => {
76
+ const sent: any[] = [];
77
+ const tools = stubTools({ send: (a) => sent.push(a) });
78
+ // A recipe with no deterministic extractor → must use the provider.
79
+ const llmOnly = { ...paymentsRecipe, extract: undefined };
80
+ const provider: LLMProvider = {
81
+ name: 'mock',
82
+ runTurn: vi.fn(async () => ({ text: '', rawContent: '', toolCalls: [{ id: '1', name: 'extract_request', arguments: { recipient: 'bob', amount: 2, currency: 'usd' } }] })),
83
+ };
84
+ const res = await runRecipe(llmOnly, 'could you move a couple bucks to bob', { provider, tools, onConfirm: async () => ({ approved: true }) });
85
+ expect(res.inferences).toBe(1);
86
+ expect(provider.runTurn).toHaveBeenCalledOnce();
87
+ expect(sent[0]).toEqual({ to: 'bob@kaleidoswap.com', amount_sats: 2000 });
88
+ });
89
+ });
90
+
91
+ describe('extractSwap', () => {
92
+ it('parses "buy X <to> with <from>"', () => {
93
+ expect(extractSwap('buy 0.001 btc with usdt')).toEqual({ amount: 0.001, to_asset: 'BTC', from_asset: 'USDT' });
94
+ });
95
+ it('parses "swap X <from> for <to>"', () => {
96
+ expect(extractSwap('swap 10 usdt for btc')).toEqual({ amount: 10, from_asset: 'USDT', to_asset: 'BTC' });
97
+ });
98
+ it('returns null for non-swap text', () => {
99
+ expect(extractSwap('what is my balance')).toBeNull();
100
+ });
101
+ });
102
+
103
+ describe('runRecipe — swap', () => {
104
+ it('quote → confirm → execute', async () => {
105
+ const exec: any[] = [];
106
+ const tools = new ToolRegistry([new InProcessToolSource('w', [
107
+ { name: 'get_swap_quote', description: '', parameters: { type: 'object', properties: {} }, handler: async (a) => ({ quote_id: 'q1', receive_amount: 1500, ...a }) },
108
+ { name: 'execute_swap', description: '', parameters: { type: 'object', properties: {} }, requiresConfirmation: true, handler: async (a) => { exec.push(a); return { status: 'SUCCESS' }; } },
109
+ ])]);
110
+ const onConfirm = vi.fn(async () => ({ approved: true }));
111
+ const res = await runRecipe(swapRecipe, 'buy 0.001 btc with usdt', { provider: approve, tools, onConfirm });
112
+ expect(res.status).toBe('done');
113
+ expect(res.inferences).toBe(0);
114
+ expect(onConfirm).toHaveBeenCalledOnce();
115
+ expect(res.results.quote).toMatchObject({ quote_id: 'q1' });
116
+ expect(exec[0]).toMatchObject({ quote_id: 'q1', from_asset: 'USDT', to_asset: 'BTC', amount: 0.001 });
117
+ });
118
+ });
119
+
120
+ describe('extractReceive', () => {
121
+ it('parses asset + layer', () => {
122
+ expect(extractReceive('create an invoice for 25 usdt on liquid')).toEqual({ amount: 25, kind: 'asset', currency: 'USDT', layer: 'liquid' });
123
+ });
124
+ it('amountless BTC invoice', () => {
125
+ expect(extractReceive('give me an invoice')).toEqual({ kind: 'sats', currency: 'BTC' });
126
+ });
127
+ it('sats with k shorthand', () => {
128
+ expect(extractReceive('invoice for 5k sats')).toEqual({ amount: 5000, kind: 'sats', currency: 'BTC' });
129
+ });
130
+ it('fiat: "$2.00" → 2 USD (not 2000)', () => {
131
+ expect(extractReceive('create a payment request of $2.00')).toEqual({ amount: 2, kind: 'fiat', currency: 'USD' });
132
+ });
133
+ it('null for non-receive text', () => {
134
+ expect(extractReceive('pay bob 3 eur')).toBeNull();
135
+ });
136
+ });
137
+
138
+ describe('runRecipe — receive', () => {
139
+ const tools = () => new ToolRegistry([new InProcessToolSource('w', [
140
+ { name: 'fiat_to_sats', description: '', parameters: { type: 'object', properties: {} }, handler: async ({ amount }) => ({ sats: Math.round(Number(amount) * 1500) }) },
141
+ { name: 'create_invoice', description: '', parameters: { type: 'object', properties: {} }, handler: async (a) => ({ invoice: `lnbc-${a.asset}-${a.amount ?? 'any'}` }) },
142
+ ])]);
143
+ it('sats → create_invoice (no confirmation)', async () => {
144
+ const onConfirm = vi.fn(async () => ({ approved: true }));
145
+ const res = await runRecipe(receiveRecipe, 'invoice for 5000 sats', { provider: approve, tools: tools(), onConfirm });
146
+ expect(res.status).toBe('done');
147
+ expect(onConfirm).not.toHaveBeenCalled();
148
+ expect(res.text).toContain('lnbc-BTC-5000');
149
+ });
150
+ it('fiat "$2" → fiat_to_sats → invoice (3000 sats), never 2000', async () => {
151
+ const res = await runRecipe(receiveRecipe, 'create a payment request of $2', { provider: approve, tools: tools() });
152
+ expect(res.status).toBe('done');
153
+ expect(res.text).toContain('lnbc-BTC-3000'); // 2 * 1500
154
+ expect(res.text).toContain('USD 2');
155
+ });
156
+ });
157
+
158
+ describe('extractAssetSend + USDT bug fix', () => {
159
+ it('parses "send N USDT to contact"', () => {
160
+ expect(extractAssetSend('send 10 usdt to bob')).toEqual({ recipient: 'bob', asset: 'USDT', amount: 10 });
161
+ });
162
+ it('null for BTC/sats sends (payments owns those)', () => {
163
+ expect(extractAssetSend('send 5000 sats to bob')).toBeNull();
164
+ });
165
+ it('payments recipe NO LONGER matches an asset send (was the bug)', () => {
166
+ expect(_pay.match!('send 10 usdt to bob')).toBe(false);
167
+ expect(_pay.match!('send 5000 sats to bob')).toBe(true);
168
+ expect(assetSendRecipe.match!('send 10 usdt to bob')).toBe(true);
169
+ });
170
+ });
171
+
172
+ describe('runRecipe — asset send', () => {
173
+ it('resolve → rln_send_asset (confirmation-gated), no fiat conversion', async () => {
174
+ const sent: any[] = [];
175
+ const tools = new ToolRegistry([new InProcessToolSource('w', [
176
+ { name: 'resolve_contact', description: '', parameters: { type: 'object', properties: {} }, handler: async ({ name }) => ({ name, ln_address: `${name}@x.com` }) },
177
+ { name: 'rln_send_asset', description: '', parameters: { type: 'object', properties: {} }, requiresConfirmation: true, handler: async (a) => { sent.push(a); return { status: 'SUCCESS' }; } },
178
+ ])]);
179
+ const onConfirm = vi.fn(async () => ({ approved: true }));
180
+ const res = await runRecipe(assetSendRecipe, 'send 10 usdt to bob', { provider: approve, tools, onConfirm });
181
+ expect(res.status).toBe('done');
182
+ expect(onConfirm).toHaveBeenCalledOnce();
183
+ expect(sent[0]).toEqual({ asset: 'USDT', amount: 10, to: 'bob@x.com' });
184
+ });
185
+ });
186
+
187
+ describe('RecipeRegistry', () => {
188
+ it('selects by trigger', () => {
189
+ const reg = new RecipeRegistry([paymentsRecipe]);
190
+ expect(reg.select('pay bob 3 eur')?.name).toBe('pay-contact');
191
+ expect(reg.select('what is my balance')).toBeNull();
192
+ });
193
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Recipe runner — executes a Recipe with ONE structured extraction (or a
3
+ * deterministic regex), then runs the deterministic step chain and the
4
+ * confirmation-gated final action. This is mobile multi-step: a tiny model
5
+ * fills slots; the engine does the planning.
6
+ */
7
+
8
+ import type { LLMProvider } from '../providers/types.js';
9
+ import type { ToolRegistry } from '../tools/registry.js';
10
+ import type { ConfirmDecision } from '../types.js';
11
+ import type { Recipe, RecipeContext, RecipeResult } from './types.js';
12
+
13
+ const EXTRACT_TOOL = 'extract_request';
14
+
15
+ export interface RunRecipeOptions {
16
+ provider: LLMProvider;
17
+ tools: ToolRegistry;
18
+ /** Called before the (spend) final action when its tool is confirmation-gated. */
19
+ onConfirm?: (call: { name: string; arguments: Record<string, unknown> }) => Promise<ConfirmDecision>;
20
+ /** Progress hook per completed step. */
21
+ onStep?: (name: string, args: Record<string, unknown>, result: unknown) => void;
22
+ /** Skip extraction and use these slots (deterministic Tier-0 / tests). */
23
+ slots?: Record<string, unknown>;
24
+ signal?: AbortSignal;
25
+ }
26
+
27
+ /** Extract the recipe's slots — deterministic regex first, else ONE LLM call. */
28
+ export async function extractSlots(
29
+ provider: LLMProvider,
30
+ recipe: Recipe,
31
+ text: string,
32
+ ): Promise<{ slots: Record<string, unknown>; inferences: number }> {
33
+ const det = recipe.extract?.(text);
34
+ if (det && Object.values(det).some((v) => v !== undefined && v !== null && v !== '')) {
35
+ return { slots: det, inferences: 0 };
36
+ }
37
+ const properties: Record<string, { type: string; description: string }> = {};
38
+ for (const s of recipe.slots) properties[s.name] = { type: s.type ?? 'string', description: s.description };
39
+ const extractTool = {
40
+ name: EXTRACT_TOOL,
41
+ description: `Extract the fields from the user's request.`,
42
+ parameters: { type: 'object', properties, required: recipe.slots.filter((s) => s.required).map((s) => s.name) },
43
+ };
44
+ const out = await provider.runTurn({
45
+ system: `Call ${EXTRACT_TOOL} with the fields from the user's message. Do not call any other tool and do not add commentary.`,
46
+ messages: [{ role: 'user', content: text }],
47
+ tools: [extractTool],
48
+ });
49
+ const call = out.toolCalls?.find((c) => c.name === EXTRACT_TOOL) ?? out.toolCalls?.[0];
50
+ return { slots: (call?.arguments as Record<string, unknown>) ?? {}, inferences: 1 };
51
+ }
52
+
53
+ /** Run a recipe end to end. Never throws — failures come back as status:'error'. */
54
+ export async function runRecipe(recipe: Recipe, text: string, opts: RunRecipeOptions): Promise<RecipeResult> {
55
+ const ctx: RecipeContext = { text, slots: opts.slots ?? {}, results: {} };
56
+ let inferences = 0;
57
+ try {
58
+ if (!opts.slots) {
59
+ const ex = await extractSlots(opts.provider, recipe, text);
60
+ ctx.slots = ex.slots;
61
+ inferences = ex.inferences;
62
+ }
63
+
64
+ // Deterministic steps. Intermediate spend tools fire the same confirmation
65
+ // gate as the final step — recipes with multi-spend chains (e.g. atomic
66
+ // swaps) MUST have every money-moving call gated, never just the last one.
67
+ // Missing onConfirm fails closed, matching the Engine.
68
+ for (const step of recipe.steps) {
69
+ if (step.skipIf?.(ctx)) continue;
70
+ const args = step.args(ctx);
71
+ const def = await opts.tools.getDef(step.tool);
72
+ if (def?.requiresConfirmation) {
73
+ const decision = opts.onConfirm
74
+ ? await opts.onConfirm({ name: step.tool, arguments: args })
75
+ : { approved: false, reason: 'no confirmation handler available' };
76
+ if (!decision.approved) {
77
+ return { recipe: recipe.name, slots: ctx.slots, results: ctx.results, text: 'Cancelled — nothing was sent.', status: 'cancelled', inferences };
78
+ }
79
+ }
80
+ const result = await opts.tools.execute(step.tool, args);
81
+ ctx.results[step.as ?? step.tool] = result;
82
+ opts.onStep?.(step.tool, args, result);
83
+ }
84
+
85
+ // Final action — confirmation-gated if the tool requires it. Like the
86
+ // Engine, a missing onConfirm FAILS CLOSED: the spend is declined, never
87
+ // silently executed.
88
+ const finalArgs = recipe.final.args(ctx);
89
+ const def = await opts.tools.getDef(recipe.final.tool);
90
+ if (def?.requiresConfirmation) {
91
+ const decision = opts.onConfirm
92
+ ? await opts.onConfirm({ name: recipe.final.tool, arguments: finalArgs })
93
+ : { approved: false, reason: 'no confirmation handler available' };
94
+ if (!decision.approved) {
95
+ return { recipe: recipe.name, slots: ctx.slots, results: ctx.results, text: 'Cancelled — nothing was sent.', status: 'cancelled', inferences };
96
+ }
97
+ }
98
+ const finalResult = await opts.tools.execute(recipe.final.tool, finalArgs);
99
+ ctx.results[recipe.final.as ?? recipe.final.tool] = finalResult;
100
+ opts.onStep?.(recipe.final.tool, finalArgs, finalResult);
101
+
102
+ const out = recipe.summary?.(ctx, finalResult) ?? 'Done.';
103
+ return { recipe: recipe.name, slots: ctx.slots, results: ctx.results, final: finalResult, text: out, status: 'done', inferences };
104
+ } catch (e) {
105
+ const msg = (e as Error)?.message ?? String(e);
106
+ return { recipe: recipe.name, slots: ctx.slots, results: ctx.results, text: `Couldn't complete that: ${msg}`, status: 'error', error: msg, inferences };
107
+ }
108
+ }
109
+
110
+ /** Selects a recipe for a request. Use before falling back to the free agentic loop. */
111
+ export class RecipeRegistry {
112
+ private recipes: Recipe[];
113
+ constructor(recipes: Recipe[] = []) {
114
+ this.recipes = [...recipes];
115
+ }
116
+ add(recipe: Recipe): void {
117
+ this.recipes.push(recipe);
118
+ }
119
+ list(): Recipe[] {
120
+ return [...this.recipes];
121
+ }
122
+ get(name: string): Recipe | undefined {
123
+ return this.recipes.find((r) => r.name === name);
124
+ }
125
+ /** First recipe whose match()/triggers fit the text, else null. */
126
+ select(text: string): Recipe | null {
127
+ const lc = text.toLowerCase();
128
+ return (
129
+ this.recipes.find((r) =>
130
+ r.match ? r.match(text) : (r.triggers ?? []).some((t) => lc.includes(t.toLowerCase())),
131
+ ) ?? null
132
+ );
133
+ }
134
+ }