@kaleidorg/mind 0.6.4 → 0.8.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 (189) hide show
  1. package/README.md +345 -0
  2. package/dist/autonomy/risk.js.map +1 -1
  3. package/dist/autonomy/run-state.d.ts.map +1 -1
  4. package/dist/autonomy/run-state.js.map +1 -1
  5. package/dist/autonomy/task-store.d.ts.map +1 -1
  6. package/dist/autonomy/task-store.js.map +1 -1
  7. package/dist/bitrefill/contract.js.map +1 -1
  8. package/dist/context/budget.js.map +1 -1
  9. package/dist/context/builder.d.ts.map +1 -1
  10. package/dist/context/builder.js.map +1 -1
  11. package/dist/context/compress.d.ts.map +1 -1
  12. package/dist/context/compress.js +1 -0
  13. package/dist/context/compress.js.map +1 -1
  14. package/dist/context/rgb-units.d.ts +18 -0
  15. package/dist/context/rgb-units.d.ts.map +1 -0
  16. package/dist/context/rgb-units.js +86 -0
  17. package/dist/context/rgb-units.js.map +1 -0
  18. package/dist/engine.d.ts +53 -1
  19. package/dist/engine.d.ts.map +1 -1
  20. package/dist/engine.js +179 -17
  21. package/dist/engine.js.map +1 -1
  22. package/dist/evidence.d.ts +1 -1
  23. package/dist/evidence.d.ts.map +1 -1
  24. package/dist/evidence.js.map +1 -1
  25. package/dist/fastpath/fastpath.d.ts.map +1 -1
  26. package/dist/fastpath/fastpath.js.map +1 -1
  27. package/dist/flashnet/contract.js.map +1 -1
  28. package/dist/funnel.d.ts.map +1 -1
  29. package/dist/funnel.js +23 -4
  30. package/dist/funnel.js.map +1 -1
  31. package/dist/guards.d.ts +63 -0
  32. package/dist/guards.d.ts.map +1 -0
  33. package/dist/guards.js +284 -0
  34. package/dist/guards.js.map +1 -0
  35. package/dist/index.d.ts +10 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +9 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/kaleidoswap/contract.d.ts +3 -4
  40. package/dist/kaleidoswap/contract.d.ts.map +1 -1
  41. package/dist/kaleidoswap/contract.js +3 -17
  42. package/dist/kaleidoswap/contract.js.map +1 -1
  43. package/dist/knowledge/btc-map.js.map +1 -1
  44. package/dist/logger.d.ts.map +1 -1
  45. package/dist/logger.js.map +1 -1
  46. package/dist/lsps1/contract.js.map +1 -1
  47. package/dist/memory/store.d.ts.map +1 -1
  48. package/dist/memory/store.js.map +1 -1
  49. package/dist/providers/openai.d.ts +64 -0
  50. package/dist/providers/openai.d.ts.map +1 -0
  51. package/dist/providers/openai.js +233 -0
  52. package/dist/providers/openai.js.map +1 -0
  53. package/dist/providers/types.d.ts +20 -0
  54. package/dist/providers/types.d.ts.map +1 -1
  55. package/dist/qvac/assistant.js.map +1 -1
  56. package/dist/qvac/config.d.ts +7 -7
  57. package/dist/qvac/config.d.ts.map +1 -1
  58. package/dist/qvac/config.js +1 -1
  59. package/dist/qvac/delegate.d.ts +2 -0
  60. package/dist/qvac/delegate.d.ts.map +1 -1
  61. package/dist/qvac/delegate.js +2 -0
  62. package/dist/qvac/delegate.js.map +1 -1
  63. package/dist/qvac/index.d.ts +2 -0
  64. package/dist/qvac/index.d.ts.map +1 -1
  65. package/dist/qvac/index.js +2 -0
  66. package/dist/qvac/index.js.map +1 -1
  67. package/dist/qvac/models.d.ts +31 -0
  68. package/dist/qvac/models.d.ts.map +1 -0
  69. package/dist/qvac/models.js +80 -0
  70. package/dist/qvac/models.js.map +1 -0
  71. package/dist/qvac/parse.d.ts +16 -2
  72. package/dist/qvac/parse.d.ts.map +1 -1
  73. package/dist/qvac/parse.js +29 -7
  74. package/dist/qvac/parse.js.map +1 -1
  75. package/dist/qvac/provider.d.ts +5 -4
  76. package/dist/qvac/provider.d.ts.map +1 -1
  77. package/dist/qvac/provider.js +30 -21
  78. package/dist/qvac/provider.js.map +1 -1
  79. package/dist/qvac/stream.d.ts +3 -5
  80. package/dist/qvac/stream.d.ts.map +1 -1
  81. package/dist/qvac/stream.js +29 -1
  82. package/dist/qvac/stream.js.map +1 -1
  83. package/dist/qvac/tools.d.ts +37 -0
  84. package/dist/qvac/tools.d.ts.map +1 -0
  85. package/dist/qvac/tools.js +94 -0
  86. package/dist/qvac/tools.js.map +1 -0
  87. package/dist/qvac/voice.js.map +1 -1
  88. package/dist/rag/retriever.d.ts.map +1 -1
  89. package/dist/rag/tool.js.map +1 -1
  90. package/dist/rag/vector-store.d.ts.map +1 -1
  91. package/dist/rag/vector-store.js.map +1 -1
  92. package/dist/recipe/issue-asset.d.ts +16 -0
  93. package/dist/recipe/issue-asset.d.ts.map +1 -0
  94. package/dist/recipe/issue-asset.js +120 -0
  95. package/dist/recipe/issue-asset.js.map +1 -0
  96. package/dist/recipe/runner.d.ts.map +1 -1
  97. package/dist/recipe/runner.js +1 -0
  98. package/dist/recipe/runner.js.map +1 -1
  99. package/dist/recipe/submarine-pay.d.ts +17 -0
  100. package/dist/recipe/submarine-pay.d.ts.map +1 -0
  101. package/dist/recipe/submarine-pay.js +79 -0
  102. package/dist/recipe/submarine-pay.js.map +1 -0
  103. package/dist/skills/loader.js.map +1 -1
  104. package/dist/skills/registry.d.ts.map +1 -1
  105. package/dist/skills/registry.js +1 -1
  106. package/dist/skills/registry.js.map +1 -1
  107. package/dist/skills/select.d.ts +20 -0
  108. package/dist/skills/select.d.ts.map +1 -0
  109. package/dist/skills/select.js +32 -0
  110. package/dist/skills/select.js.map +1 -0
  111. package/dist/submarine/contract.d.ts +42 -0
  112. package/dist/submarine/contract.d.ts.map +1 -0
  113. package/dist/submarine/contract.js +73 -0
  114. package/dist/submarine/contract.js.map +1 -0
  115. package/dist/testing/index.d.ts +14 -0
  116. package/dist/testing/index.d.ts.map +1 -0
  117. package/dist/testing/index.js +12 -0
  118. package/dist/testing/index.js.map +1 -0
  119. package/dist/testing/mock-wallet.d.ts +103 -0
  120. package/dist/testing/mock-wallet.d.ts.map +1 -0
  121. package/dist/testing/mock-wallet.js +249 -0
  122. package/dist/testing/mock-wallet.js.map +1 -0
  123. package/dist/testing/scripted-provider.d.ts +17 -0
  124. package/dist/testing/scripted-provider.d.ts.map +1 -0
  125. package/dist/testing/scripted-provider.js +26 -0
  126. package/dist/testing/scripted-provider.js.map +1 -0
  127. package/dist/tools/in-process.d.ts.map +1 -1
  128. package/dist/tools/mcp.d.ts.map +1 -1
  129. package/dist/tools/registry.d.ts.map +1 -1
  130. package/dist/tools/registry.js.map +1 -1
  131. package/dist/wallet/confirm.d.ts.map +1 -1
  132. package/dist/wallet/confirm.js +43 -5
  133. package/dist/wallet/confirm.js.map +1 -1
  134. package/dist/wallet/contract.d.ts.map +1 -1
  135. package/dist/wallet/contract.js +36 -0
  136. package/dist/wallet/contract.js.map +1 -1
  137. package/package.json +15 -5
  138. package/skills/README.md +2 -1
  139. package/skills/kaleido-node/SKILL.md +63 -0
  140. package/skills/kaleido-trading/SKILL.md +34 -27
  141. package/skills/kaleido-trading/references/api.md +61 -0
  142. package/skills/kaleido-trading/references/assets.md +57 -0
  143. package/skills/kaleido-trading/references/atomic.md +93 -0
  144. package/skills/paid-data/SKILL.md +59 -6
  145. package/skills/rgb-lightning-node/SKILL.md +142 -17
  146. package/skills/spark-wallet/SKILL.md +1 -0
  147. package/skills/submarine-swaps/SKILL.md +48 -0
  148. package/src/context/compress.ts +1 -0
  149. package/src/context/rgb-units.test.ts +42 -0
  150. package/src/context/rgb-units.ts +89 -0
  151. package/src/engine.test.ts +94 -3
  152. package/src/engine.ts +234 -19
  153. package/src/funnel.mind.test.ts +4 -2
  154. package/src/funnel.ts +23 -4
  155. package/src/guards.test.ts +399 -0
  156. package/src/guards.ts +299 -0
  157. package/src/index.ts +40 -1
  158. package/src/kaleidoswap/contract.test.ts +8 -16
  159. package/src/kaleidoswap/contract.ts +4 -32
  160. package/src/providers/openai.test.ts +127 -0
  161. package/src/providers/openai.ts +282 -0
  162. package/src/providers/types.ts +22 -0
  163. package/src/qvac/config.ts +1 -1
  164. package/src/qvac/delegate.ts +2 -0
  165. package/src/qvac/index.ts +12 -0
  166. package/src/qvac/models.ts +98 -0
  167. package/src/qvac/parse.test.ts +7 -0
  168. package/src/qvac/parse.ts +38 -3
  169. package/src/qvac/provider.test.ts +72 -1
  170. package/src/qvac/provider.ts +40 -27
  171. package/src/qvac/stream.test.ts +28 -0
  172. package/src/qvac/stream.ts +33 -6
  173. package/src/qvac/tools.test.ts +86 -0
  174. package/src/qvac/tools.ts +116 -0
  175. package/src/recipe/issue-asset.test.ts +131 -0
  176. package/src/recipe/issue-asset.ts +123 -0
  177. package/src/recipe/runner.ts +1 -0
  178. package/src/recipe/submarine-pay.test.ts +87 -0
  179. package/src/recipe/submarine-pay.ts +78 -0
  180. package/src/skills/registry.ts +1 -1
  181. package/src/skills/select.ts +41 -0
  182. package/src/submarine/contract.ts +112 -0
  183. package/src/testing/index.ts +14 -0
  184. package/src/testing/mock-wallet.ts +278 -0
  185. package/src/testing/scripted-provider.ts +37 -0
  186. package/src/wallet/confirm.test.ts +16 -0
  187. package/src/wallet/confirm.ts +43 -5
  188. package/src/wallet/contract.test.ts +38 -0
  189. package/src/wallet/contract.ts +36 -0
@@ -0,0 +1,123 @@
1
+ /**
2
+ * "Issue an RGB asset" recipe — the hackathon one-liner:
3
+ *
4
+ * "issue 1000 TICKET tokens called Hackathon Ticket" → rln_issue_asset 🔒
5
+ * "mint an NFT called Genesis Badge" → rln_issue_asset 🔒 (UDA)
6
+ * "crea 500 token CAFE chiamati Caffè Club" → rln_issue_asset 🔒
7
+ *
8
+ * No intermediate steps: the plan is a single gated call, so the recipe's job
9
+ * is the slot extraction (name / ticker / supply / schema) and a deterministic
10
+ * readback — exactly the part a 0.6B model gets wrong. Opt-in: register via
11
+ * `Funnel.recipes` on hosts that bind `rln_issue_asset`.
12
+ */
13
+
14
+ import type { Recipe, RecipeContext } from './types.js';
15
+
16
+ // The request must OPEN with the verb ("issue …", "please mint …", "puoi creare …"),
17
+ // so "I have an issue with my tokens" or "how do I create a token?" stay with the model.
18
+ const VERB = /^(?:(?:please|pls|per favore|can you|could you|puoi|potresti)\s+)?(issue|mint|create|launch|emetti|conia|crea|creare|emettere|coniare)\b/i;
19
+ const NOUN = /\b(tokens?|assets?|coins?|nfts?|tickets?|gettoni|badges?)\b/i;
20
+ const NOT_ISSUE = /\b(invoice|fattura|channel|canale|swap|buy|sell|send|pay|compra|vendi|invia|paga)\b/i;
21
+ const NFT = /\b(nfts?|unique|unico|unica)\b/i;
22
+ // Words that look like tickers but are part of the request, not the asset.
23
+ const STOPWORDS = new Set(['RGB', 'NFT', 'NFTS', 'NIA', 'CFA', 'UDA', 'BTC', 'LN', 'AN', 'A']);
24
+
25
+ // "1000", "1,000", "1.000" (it), "1.5", "1,5" (it), optionally followed by k/m.
26
+ const NUM = String.raw`(\d{1,3}(?:[.,]\d{3})+|\d+(?:[.,]\d+)?)\s*([km])?`;
27
+
28
+ function toNumber(digits: string, suffix?: string): number | undefined {
29
+ // Groups of exactly three digits are thousands separators in both "1,000" and "1.000";
30
+ // a single separator followed by another digit count is a decimal point.
31
+ const n = /^\d{1,3}(?:[.,]\d{3})+$/.test(digits)
32
+ ? Number(digits.replace(/[.,]/g, ''))
33
+ : Number(digits.replace(',', '.'));
34
+ const scaled = suffix ? n * (suffix.toLowerCase() === 'k' ? 1_000 : 1_000_000) : n;
35
+ return Number.isFinite(scaled) && scaled > 0 ? scaled : undefined;
36
+ }
37
+
38
+ /**
39
+ * The supply: an explicit "supply 300" / "1m supply" wins, then a number right before the token
40
+ * noun / ticker ("1000 TICKET tokens", "500 token"). Numbers inside the asset name
41
+ * ("Web3 Summit 2026") are never the supply.
42
+ */
43
+ function parseAmount(t: string, name?: string): number | undefined {
44
+ const rest = name ? t.replace(name, ' ') : t;
45
+ const labelled = rest.match(new RegExp(String.raw`\b(?:supply|amount|quantit[àa]|totale)\s*(?:of|di|[:=])?\s*` + NUM, 'i'));
46
+ if (labelled) return toNumber(labelled[1]!, labelled[2]);
47
+ const trailing = rest.match(new RegExp(String.raw`(?:^|\s)` + NUM + String.raw`\s+(?:supply|di supply)\b`, 'i'));
48
+ if (trailing) return toNumber(trailing[1]!, trailing[2]);
49
+ const beforeNoun = rest.match(new RegExp(String.raw`(?:^|\s)` + NUM + String.raw`\s+(?:[A-Z][A-Z0-9]{1,7}\s+)?(?:tokens?|coins?|tickets?|gettoni|badges?|assets?)\b`, 'i'));
50
+ if (beforeNoun) return toNumber(beforeNoun[1]!, beforeNoun[2]);
51
+ return undefined;
52
+ }
53
+
54
+ const unquote = (s: string) => s.replace(/^["“'‘]+|["”'’]+$/g, '').trim();
55
+
56
+ function parseName(t: string): string | undefined {
57
+ // "called Joe's Pizza with ticker 'JOE'" → Joe's Pizza (apostrophes inside a name are fine).
58
+ const m = t.match(/\b(?:called|named|chiamat[oaie])\s+(.+?)(?=\s+(?:with|ticker|con|supply)\b|[,;]|\.(?:\s|$)|$)/i);
59
+ if (m?.[1]) return unquote(m[1]) || undefined;
60
+ // Otherwise only a properly paired quote: double quotes, or single quotes that open
61
+ // after a space and close before a space/punctuation (so "Joe's" never opens one).
62
+ const quoted = t.match(/["“]([^"”]{1,40})["”]/)?.[1] ?? t.match(/(?:^|\s)['‘]([^'’]{1,40})['’](?=\s|$|[,.;!?])/)?.[1];
63
+ return quoted?.trim() || undefined;
64
+ }
65
+
66
+ function parseTicker(t: string, name?: string): string | undefined {
67
+ const explicit = t.match(/\bticker\s*[:=]?\s*["'“‘]?([A-Za-z0-9]{1,8})\b/i)?.[1];
68
+ if (explicit) return explicit.toUpperCase();
69
+ // An all-caps word that isn't part of the name ("1000 TICKET tokens").
70
+ const nameWords = new Set((name ?? '').split(/\s+/));
71
+ for (const w of t.match(/\b[A-Z][A-Z0-9]{1,7}\b/g) ?? []) {
72
+ if (!STOPWORDS.has(w) && !nameWords.has(w)) return w;
73
+ }
74
+ return undefined;
75
+ }
76
+
77
+ /** Ticker fallback from the name: "Hackathon Ticket" → "HACKATHO". */
78
+ function tickerFromName(name: string): string {
79
+ return name.replace(/[^A-Za-z0-9]/g, '').toUpperCase().slice(0, 8);
80
+ }
81
+
82
+ export function extractIssueAsset(text: string): Record<string, unknown> | null {
83
+ const t = text.trim();
84
+ if (!VERB.test(t) || !NOUN.test(t) || NOT_ISSUE.test(t)) return null;
85
+ const schema = NFT.test(t) ? 'UDA' : 'NIA';
86
+ const name = parseName(t);
87
+ const ticker = parseTicker(t, name) ?? (name ? tickerFromName(name) : undefined);
88
+ const amount = schema === 'UDA' ? 1 : parseAmount(t, name);
89
+ if (!ticker && !name) return null;
90
+ return { name: name ?? ticker, ticker, amount, schema };
91
+ }
92
+
93
+ export const issueAssetRecipe: Recipe = {
94
+ name: 'issue-asset',
95
+ description: 'Issue a new RGB asset (token, ticket, NFT) on the RLN node — one confirmation-gated call.',
96
+ match: (t) => VERB.test(t) && NOUN.test(t) && !NOT_ISSUE.test(t),
97
+ triggers: ['issue', 'mint', 'create token', 'launch token', 'nft'],
98
+ slots: [
99
+ { name: 'name', type: 'string', description: 'Asset name, e.g. "Hackathon Ticket"', required: true },
100
+ { name: 'ticker', type: 'string', description: 'Uppercase ticker, up to 8 chars, e.g. "TICKET"' },
101
+ { name: 'amount', type: 'number', description: 'Total supply in whole units (1 for an NFT)', required: true },
102
+ { name: 'schema', type: 'string', description: 'NIA for a fungible token, UDA for a unique NFT' },
103
+ ],
104
+ extract: extractIssueAsset,
105
+ confident: (s) => !!(s.ticker || s.name) && Number(s.amount) > 0,
106
+ steps: [],
107
+ final: {
108
+ tool: 'rln_issue_asset',
109
+ args: (ctx: RecipeContext) => {
110
+ const name = String(ctx.slots.name ?? ctx.slots.ticker);
111
+ const ticker = String(ctx.slots.ticker ?? tickerFromName(name)).toUpperCase();
112
+ const schema = ctx.slots.schema === 'UDA' ? 'UDA' : 'NIA';
113
+ return { name, ticker, amount: schema === 'UDA' ? 1 : Number(ctx.slots.amount), schema };
114
+ },
115
+ },
116
+ summary: (ctx, final) => {
117
+ // wdk/kaleido MCP return `asset_id` flat; the raw node API nests it under `asset`.
118
+ const r = final as { asset_id?: string; asset?: { asset_id?: string } } | undefined;
119
+ const id = r?.asset_id ?? r?.asset?.asset_id;
120
+ const what = ctx.slots.schema === 'UDA' ? `unique asset ${ctx.slots.ticker ?? ctx.slots.name}` : `${ctx.slots.amount} ${ctx.slots.ticker ?? ctx.slots.name}`;
121
+ return id ? `Issued ${what}. Asset id: ${id}` : `Issued ${what}.`;
122
+ },
123
+ };
@@ -111,6 +111,7 @@ export async function extractSlots(
111
111
  system,
112
112
  messages: [{ role: 'user', content: text }],
113
113
  tools: [extractTool],
114
+ toolChoice: EXTRACT_TOOL,
114
115
  signal,
115
116
  });
116
117
  } catch (err) {
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { extractSubmarinePay, submarinePayRecipe } from './submarine-pay.js';
3
+ import { runRecipe } from './runner.js';
4
+ import { Funnel } from '../funnel.js';
5
+ import { paymentsRecipe } from './payments.js';
6
+ import { MockWallet, scriptedProvider } from '../testing/index.js';
7
+ import { SUBMARINE_TOOLS, isSubmarineSpendTool, formatSubmarineAmount } from '../submarine/contract.js';
8
+
9
+ // 10u = 1,000 sats on signet.
10
+ const INVOICE = 'lntbs10u1pnmockinvoice0qqqqqqqqqqqqqqqqqqqqqqqqq';
11
+
12
+ describe('submarine contract', () => {
13
+ it('only fund moves money, and it takes nothing but the swap id', () => {
14
+ expect(SUBMARINE_TOOLS.map((t) => t.name)).toEqual([
15
+ 'kaleidoswap_submarine_pairs', 'kaleidoswap_submarine_create', 'kaleidoswap_submarine_fund', 'kaleidoswap_submarine_status',
16
+ ]);
17
+ expect(SUBMARINE_TOOLS.filter((t) => t.requiresConfirmation).map((t) => t.name)).toEqual(['kaleidoswap_submarine_fund']);
18
+ expect(isSubmarineSpendTool('kaleidoswap_submarine_create')).toBe(false);
19
+ const fund = SUBMARINE_TOOLS.find((t) => t.name === 'kaleidoswap_submarine_fund')!;
20
+ expect(Object.keys((fund.parameters as any).properties)).toEqual(['swap_id']);
21
+ });
22
+ it('formats source amounts for the readback', () => {
23
+ expect(formatSubmarineAmount('L-USDT', '87500000')).toBe('0.875 L-USDT');
24
+ expect(formatSubmarineAmount('L-BTC', 1005)).toBe('1,005 sats of L-BTC');
25
+ });
26
+ });
27
+
28
+ describe('extractSubmarinePay', () => {
29
+ it('needs an invoice and an explicit Liquid asset', () => {
30
+ expect(extractSubmarinePay(`pay ${INVOICE} with L-USDT`)).toEqual({ invoice: INVOICE, from_asset: 'L-USDT' });
31
+ expect(extractSubmarinePay(`paga ${INVOICE} con USDT su Liquid`)).toEqual({ invoice: INVOICE, from_asset: 'L-USDT' });
32
+ expect(extractSubmarinePay(`pay ${INVOICE} using liquid bitcoin`)).toEqual({ invoice: INVOICE, from_asset: 'L-BTC' });
33
+ // Bare USDT is RGB USDT — not this recipe.
34
+ expect(extractSubmarinePay(`pay ${INVOICE} with USDT`)).toBeNull();
35
+ expect(extractSubmarinePay('pay bob 10 L-USDT')).toBeNull();
36
+ });
37
+ });
38
+
39
+ describe('runRecipe — submarine pay against the mock /v2 maker', () => {
40
+ it('creates, confirms once with the maker amount, then funds', async () => {
41
+ const wallet = new MockWallet({ priceUsd: 80_000 });
42
+ const onConfirm = vi.fn(async () => ({ approved: true }));
43
+ const res = await runRecipe(submarinePayRecipe, `pay ${INVOICE} with L-USDT`, { provider: scriptedProvider(), tools: wallet.registry(), onConfirm });
44
+ expect(res.status).toBe('done');
45
+ expect(res.inferences).toBe(0);
46
+ expect(onConfirm).toHaveBeenCalledOnce();
47
+ // 1,000 sats + 0.5% at $80k = 0.804 USD → 80,400,000 units of L-USDT (8 dp).
48
+ expect((onConfirm.mock.calls[0] as any[])[0].summary).toBe(
49
+ 'Pay Lightning invoice lntbs10u1p…qqqqqq with 0.804 L-USDT (fees included) through a KaleidoSwap submarine swap. Confirm?');
50
+ expect(wallet.liquid['L-USDT']).toBe(100 * 1e8 - 80_400_000);
51
+ expect(wallet.sends.map((s) => s.tool)).toEqual(['kaleidoswap_submarine_fund']);
52
+ expect(res.text).toMatch(/^Locked 0\.804 L-USDT in swap sub1/);
53
+ });
54
+
55
+ it('declining funds nothing, but the swap was opened (no money moved)', async () => {
56
+ const wallet = new MockWallet();
57
+ const res = await runRecipe(submarinePayRecipe, `pay ${INVOICE} with L-USDT`, {
58
+ provider: scriptedProvider(), tools: wallet.registry(), onConfirm: async () => ({ approved: false }),
59
+ });
60
+ expect(res.status).toBe('cancelled');
61
+ expect(wallet.sends).toEqual([]);
62
+ expect(wallet.submarineSwaps.sub1?.funded).toBe(false);
63
+ });
64
+
65
+ it('surfaces insufficient Liquid balance without locking anything', async () => {
66
+ const wallet = new MockWallet({ liquid: { 'L-USDT': 1000, 'L-BTC': 0 } });
67
+ const res = await runRecipe(submarinePayRecipe, `pay ${INVOICE} with L-USDT`, {
68
+ provider: scriptedProvider(), tools: wallet.registry(), onConfirm: async () => ({ approved: true }),
69
+ });
70
+ expect(res.status).toBe('error');
71
+ expect(res.error ?? res.text).toMatch(/Insufficient L-USDT/);
72
+ expect(wallet.sends).toEqual([]);
73
+ });
74
+ });
75
+
76
+ describe('Funnel — submarine-pay ahead of payments', () => {
77
+ it('routes "pay <invoice> with L-USDT" to the submarine recipe, plain invoices elsewhere', async () => {
78
+ const wallet = new MockWallet();
79
+ const funnel = new Funnel({ provider: scriptedProvider(), tools: wallet.registry(), recipes: [submarinePayRecipe, paymentsRecipe] });
80
+ const out = await funnel.runTurn(`pay ${INVOICE} with L-USDT`, { onConfirm: async () => ({ approved: true }) });
81
+ expect(out.tier).toBe('recipe');
82
+ expect(out.route).toBe('submarine-pay');
83
+ const status = await wallet.registry().execute('kaleidoswap_submarine_status', { swap_id: 'sub1' }) as { status: string };
84
+ expect(status.status).toBe('transaction.claimed');
85
+ expect(submarinePayRecipe.match!(`pay ${INVOICE}`)).toBe(false);
86
+ });
87
+ });
@@ -0,0 +1,78 @@
1
+ /**
2
+ * "Pay this Lightning invoice with my Liquid USDT" — a submarine swap on the
3
+ * KaleidoSwap /v2 maker:
4
+ *
5
+ * "pay lntbs1… with L-USDT" → kaleidoswap_submarine_create → 🔒 kaleidoswap_submarine_fund
6
+ * "paga lntbs1… con USDT su Liquid" → same
7
+ *
8
+ * One confirmation, shown after `create` so the user sees the exact amount the
9
+ * maker asked for (fees included) before anything is locked. Keys never reach
10
+ * the model; `fund` takes only the swap id. Opt-in: register via
11
+ * `Funnel.recipes` BEFORE the generic payments recipe on hosts that bind the
12
+ * submarine tools.
13
+ */
14
+
15
+ import type { Recipe, RecipeContext } from './types.js';
16
+ import { formatSubmarineAmount } from '../submarine/contract.js';
17
+
18
+ const INVOICE = /\b(ln(?:bcrt|tbs|bc|tb)[0-9a-z]{20,})\b/i;
19
+ // Only an explicit Liquid asset: a bare "USDT" is RGB USDT (atomic recipe), not L-USDT.
20
+ const L_USDT = /\b(l-?usdt|usdt[- ]?l|liquid[- ]usdt|usdt (?:on|su|in) liquid|usdt liquid)\b/i;
21
+ const L_BTC = /\b(l-?btc|liquid[- ]btc|liquid bitcoin|btc (?:on|su|in) liquid|bitcoin (?:on|su|in) liquid)\b/i;
22
+
23
+ /** Tool results may arrive as JSON text (MCP) or objects (in-process). */
24
+ function obj(v: unknown): Record<string, unknown> {
25
+ if (typeof v === 'string') {
26
+ try { return JSON.parse(v) as Record<string, unknown>; } catch { return {}; }
27
+ }
28
+ return (v ?? {}) as Record<string, unknown>;
29
+ }
30
+
31
+ function shortInvoice(inv: string): string {
32
+ return `${inv.slice(0, 10)}…${inv.slice(-6)}`;
33
+ }
34
+
35
+ export function extractSubmarinePay(text: string): Record<string, unknown> | null {
36
+ const invoice = text.match(INVOICE)?.[1];
37
+ if (!invoice) return null;
38
+ const from_asset = L_USDT.test(text) ? 'L-USDT' : L_BTC.test(text) ? 'L-BTC' : undefined;
39
+ if (!from_asset) return null;
40
+ return { invoice, from_asset };
41
+ }
42
+
43
+ export const submarinePayRecipe: Recipe = {
44
+ name: 'submarine-pay',
45
+ description: 'Pay a Lightning invoice from Liquid funds (L-USDT or L-BTC) via a KaleidoSwap submarine swap — one confirmation.',
46
+ match: (t) => INVOICE.test(t) && (L_USDT.test(t) || L_BTC.test(t)),
47
+ triggers: ['l-usdt', 'liquid usdt', 'l-btc', 'submarine'],
48
+ slots: [
49
+ { name: 'invoice', type: 'string', description: 'BOLT11 Lightning invoice to pay', required: true },
50
+ { name: 'from_asset', type: 'string', description: 'L-USDT or L-BTC', required: true },
51
+ ],
52
+ extract: extractSubmarinePay,
53
+ confident: (s) => typeof s.invoice === 'string' && (s.from_asset === 'L-USDT' || s.from_asset === 'L-BTC'),
54
+ steps: [
55
+ {
56
+ tool: 'kaleidoswap_submarine_create',
57
+ as: 'swap',
58
+ args: (ctx) => ({ invoice: ctx.slots.invoice, from_asset: ctx.slots.from_asset }),
59
+ },
60
+ ],
61
+ confirm: (ctx: RecipeContext) => {
62
+ const swap = obj(ctx.results.swap);
63
+ const asset = String(swap.from_asset ?? ctx.slots.from_asset);
64
+ return `Pay Lightning invoice ${shortInvoice(String(ctx.slots.invoice))} with ${formatSubmarineAmount(asset, swap.expected_amount)} (fees included) through a KaleidoSwap submarine swap. Confirm?`;
65
+ },
66
+ final: {
67
+ tool: 'kaleidoswap_submarine_fund',
68
+ args: (ctx) => ({ swap_id: obj(ctx.results.swap).swap_id }),
69
+ },
70
+ summary: (ctx, final) => {
71
+ const swap = obj(ctx.results.swap);
72
+ const funded = obj(final);
73
+ const asset = String(swap.from_asset ?? ctx.slots.from_asset);
74
+ return `Locked ${formatSubmarineAmount(asset, swap.expected_amount)} in swap ${String(swap.swap_id)}` +
75
+ (funded.txid ? ` (tx ${String(funded.txid).slice(0, 12)}…)` : '') +
76
+ '. The maker pays the invoice once the lockup is seen — ask me for the swap status to follow it.';
77
+ },
78
+ };
@@ -20,7 +20,7 @@ export const READ_REFERENCE_TOOL = 'read_skill_reference';
20
20
  * ---
21
21
  * name: portfolio-manager
22
22
  * description: Rebalance BTC/USDT/XAUT to target allocations.
23
- * tools: get_balance, kaleidoswap_get_quote, kaleidoswap_place_order
23
+ * tools: get_balance, kaleidoswap_get_quote, kaleidoswap_atomic_init
24
24
  * triggers: rebalance, allocation, portfolio
25
25
  * ---
26
26
  * <instructions…>
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Tool-aware skill selection: pick a skill that can actually act with the tools
3
+ * a host has, before any inference runs.
4
+ */
5
+
6
+ import type { ToolDef } from '../types.js';
7
+ import type { Skill } from './types.js';
8
+ import { READ_REFERENCE_TOOL, SkillRegistry } from './registry.js';
9
+
10
+ /**
11
+ * Whether a skill can act given the live tool names. A `requires-tools`
12
+ * frontmatter list must be fully live; otherwise at least one of the skill's
13
+ * scoped tools must be.
14
+ */
15
+ export function skillAvailable(skill: Skill, present: ReadonlySet<string>): boolean {
16
+ const required = skill.metadata?.['requires-tools'];
17
+ if (required) {
18
+ return required.split(',').map((t) => t.trim()).filter(Boolean).every((t) => present.has(t));
19
+ }
20
+ if (!skill.tools?.length) return true;
21
+ return skill.tools.some((t) => t !== READ_REFERENCE_TOOL && present.has(t));
22
+ }
23
+
24
+ /**
25
+ * Select the best skill for `query` among those that can act with `liveTools`.
26
+ * Skills whose `requires-tools` are missing are never picked; a skill with zero
27
+ * live tools is only a last resort.
28
+ */
29
+ export function selectAvailableSkill(
30
+ skills: SkillRegistry | readonly Skill[],
31
+ query: string,
32
+ liveTools: Iterable<string | Pick<ToolDef, 'name'>>,
33
+ ): Skill | null {
34
+ const list = skills instanceof SkillRegistry ? skills.list() : [...skills];
35
+ const present = new Set([...liveTools].map((t) => (typeof t === 'string' ? t : t.name)));
36
+ const usable = list.filter((s) => skillAvailable(s, present));
37
+ return (
38
+ new SkillRegistry(usable).select(query) ??
39
+ new SkillRegistry(list.filter((s) => !s.metadata?.['requires-tools'] && !usable.includes(s))).select(query)
40
+ );
41
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Canonical submarine-swap tool contract — pay a Lightning invoice from Liquid
3
+ * funds through the KaleidoSwap Boltz /v2 maker (kaleidoswap-maker-rs).
4
+ *
5
+ * The names match kaleido-mcp, which implements them over @kaleidorg/swap-sdk.
6
+ * Hosts with the SDK in-process (desktop sidecar, React Native) bind the same
7
+ * names with `bindSubmarineTools`.
8
+ *
9
+ * Keys and preimages never cross this contract: the host derives the refund key
10
+ * from its wallet mnemonic and persists the swap. `kaleidoswap_submarine_fund`
11
+ * — the only spend — takes nothing but the swap id, so the model cannot alter
12
+ * the amount, asset or lockup address it pays.
13
+ *
14
+ * Pure data — no deps, no fetch, RN-safe.
15
+ */
16
+
17
+ import type { ToolDef } from '../types.js';
18
+ import { InProcessToolSource } from '../tools/in-process.js';
19
+ import type { InProcessTool } from '../tools/in-process.js';
20
+
21
+ export interface SubmarineToolDef extends ToolDef {
22
+ /** Moves funds → confirmation-gated. */
23
+ spend?: boolean;
24
+ }
25
+
26
+ type Props = Record<string, { type: string; description?: string; enum?: string[] }>;
27
+
28
+ function t(name: string, description: string, properties: Props = {}, required: string[] = [], spend = false): SubmarineToolDef {
29
+ return { name, description, spend, requiresConfirmation: spend, parameters: { type: 'object', properties, required } };
30
+ }
31
+
32
+ /** Assets a submarine swap can be paid from, as the maker names them. */
33
+ export const SUBMARINE_FROM_ASSETS = ['L-USDT', 'L-BTC'] as const;
34
+ export type SubmarineFromAsset = typeof SUBMARINE_FROM_ASSETS[number];
35
+
36
+ /** Smallest-unit decimals of each source asset (L-USDT: 8, L-BTC: sats). */
37
+ export const SUBMARINE_ASSET_DECIMALS: Record<SubmarineFromAsset, number> = { 'L-USDT': 8, 'L-BTC': 8 };
38
+
39
+ const swapId = { type: 'string', description: 'Swap id returned by kaleidoswap_submarine_create' } as const;
40
+
41
+ export const SUBMARINE_TOOLS: SubmarineToolDef[] = [
42
+ t('kaleidoswap_submarine_pairs',
43
+ 'List what can pay a Lightning invoice through a submarine swap (e.g. L-USDT on Liquid → Lightning BTC): rate, min/max, fees. No args.'),
44
+
45
+ t('kaleidoswap_submarine_create',
46
+ 'Open a submarine swap that pays a Lightning (BOLT11) invoice from Liquid funds. No funds move: returns swap_id and expected_amount (source asset smallest unit). Then confirm with the user and call kaleidoswap_submarine_fund.',
47
+ {
48
+ invoice: { type: 'string', description: 'BOLT11 Lightning invoice to pay (must carry an amount)' },
49
+ from_asset: { type: 'string', enum: [...SUBMARINE_FROM_ASSETS], description: 'Asset to pay with (default L-USDT)' },
50
+ },
51
+ ['invoice']),
52
+
53
+ t('kaleidoswap_submarine_fund',
54
+ 'SPEND: lock the funds for a swap from kaleidoswap_submarine_create. Takes only the swap id — amount, asset and address come from the stored swap. The maker then pays the invoice.',
55
+ { swap_id: swapId },
56
+ ['swap_id'],
57
+ /* spend */ true),
58
+
59
+ t('kaleidoswap_submarine_status',
60
+ 'Status of a submarine swap. "transaction.claimed" = invoice paid, done. A failed funded swap needs a refund.',
61
+ { swap_id: swapId },
62
+ ['swap_id']),
63
+ ];
64
+
65
+ export const SUBMARINE_SPEND_TOOLS: Set<string> = new Set(SUBMARINE_TOOLS.filter((d) => d.spend).map((d) => d.name));
66
+
67
+ export function isSubmarineSpendTool(name: string): boolean {
68
+ return SUBMARINE_SPEND_TOOLS.has(name);
69
+ }
70
+
71
+ export function getSubmarineTool(name: string): SubmarineToolDef | undefined {
72
+ return SUBMARINE_TOOLS.find((d) => d.name === name);
73
+ }
74
+
75
+ /** Format a smallest-unit amount of a source asset for display ("52.3 L-USDT", "1,200 sats"). */
76
+ export function formatSubmarineAmount(asset: string, smallestUnits: unknown): string {
77
+ const n = Number(smallestUnits);
78
+ if (!Number.isFinite(n)) return `${String(smallestUnits)} ${asset}`;
79
+ if (asset === 'L-BTC') return `${n.toLocaleString('en-US')} sats of L-BTC`;
80
+ const decimals = SUBMARINE_ASSET_DECIMALS[asset as SubmarineFromAsset] ?? 0;
81
+ const value = n / 10 ** decimals;
82
+ return `${value.toLocaleString('en-US', { maximumFractionDigits: decimals })} ${asset}`;
83
+ }
84
+
85
+ export type SubmarineHandler = (args: Record<string, unknown>) => Promise<unknown>;
86
+
87
+ export interface BindSubmarineOptions {
88
+ /** Skip tools without a handler instead of throwing (default false). */
89
+ allowMissing?: boolean;
90
+ /** ToolSource id for the registry (default 'submarine'). */
91
+ id?: string;
92
+ }
93
+
94
+ /** Bind the submarine contract to in-process handlers → an InProcessToolSource. */
95
+ export function bindSubmarineTools(handlers: Record<string, SubmarineHandler>, opts: BindSubmarineOptions = {}): InProcessToolSource {
96
+ const bound: InProcessTool[] = [];
97
+ for (const def of SUBMARINE_TOOLS) {
98
+ const handler = handlers[def.name];
99
+ if (!handler) {
100
+ if (opts.allowMissing) continue;
101
+ throw new Error(`bindSubmarineTools: no handler for "${def.name}"`);
102
+ }
103
+ bound.push({
104
+ name: def.name,
105
+ description: def.description,
106
+ parameters: def.parameters,
107
+ requiresConfirmation: def.requiresConfirmation,
108
+ handler,
109
+ });
110
+ }
111
+ return new InProcessToolSource(opts.id ?? 'submarine', bound);
112
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @kaleidorg/mind/testing — build and demo an agent with no node, no funds and
3
+ * no model: a stateful mock wallet bound to the canonical contract, plus a
4
+ * scripted provider.
5
+ *
6
+ * import { MockWallet, scriptedProvider } from '@kaleidorg/mind/testing';
7
+ * const wallet = new MockWallet();
8
+ * const funnel = new Funnel({ provider: scriptedProvider(), tools: wallet.registry(), … });
9
+ */
10
+
11
+ export { MockWallet } from './mock-wallet.js';
12
+ export type { MockWalletOptions, MockContact, MockRgbAsset, MockTransfer, SendRecord } from './mock-wallet.js';
13
+ export { scriptedProvider } from './scripted-provider.js';
14
+ export type { ScriptedTurn } from './scripted-provider.js';