@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,278 @@
1
+ /**
2
+ * A stateful mock wallet — balances per layer, contacts (incl. ambiguous +
3
+ * injectable), a price, validation (insufficient funds, no route, unknown
4
+ * contact, no colored UTXOs), RGB issuance, and a record of what actually got
5
+ * "sent". The canonical contract tools bind to it, so the agent sees real
6
+ * schemas and real failure modes — not trivial canned stubs.
7
+ *
8
+ * Used by the eval harness and exported as `@kaleidorg/mind/testing` so hosts
9
+ * can build and demo an agent with no node, no funds and no network.
10
+ */
11
+
12
+ import { bindWalletTools, type WalletHandler } from '../wallet/contract.js';
13
+ import { ToolRegistry } from '../tools/registry.js';
14
+ import { bindSubmarineTools, type SubmarineHandler } from '../submarine/contract.js';
15
+
16
+ export interface SendRecord {
17
+ tool: string;
18
+ to: string;
19
+ amount_sats?: number;
20
+ asset?: string;
21
+ amount?: number;
22
+ }
23
+ export interface MockContact { name: string; ln_address: string; note?: string }
24
+ export interface MockWalletOptions {
25
+ priceUsd?: number;
26
+ failRoute?: boolean;
27
+ contacts?: MockContact[];
28
+ balances?: { spark: number; rln: number; arkade: number };
29
+ assets?: Record<string, number>;
30
+ /** Free colored UTXOs on the RLN node (issuance / RGB receive consume one). */
31
+ utxos?: number;
32
+ /** Liquid balances in smallest units (L-USDT: 8 decimals, L-BTC: sats) for submarine swaps. */
33
+ liquid?: { 'L-USDT': number; 'L-BTC': number };
34
+ }
35
+ export interface MockRgbAsset {
36
+ asset_id: string;
37
+ ticker: string;
38
+ name: string;
39
+ precision: number;
40
+ schema: 'NIA' | 'CFA' | 'UDA';
41
+ issued_supply: number;
42
+ }
43
+ export interface MockTransfer {
44
+ idx: number;
45
+ asset_id: string;
46
+ kind: 'Issuance' | 'Send' | 'ReceiveBlind';
47
+ status: 'Settled' | 'WaitingCounterparty';
48
+ amount: number;
49
+ }
50
+
51
+ export class MockWallet {
52
+ priceUsd: number;
53
+ failRoute: boolean;
54
+ balances: { spark: number; rln: number; arkade: number };
55
+ assets: Record<string, number>;
56
+ contacts: MockContact[];
57
+ utxos: number;
58
+ liquid: { 'L-USDT': number; 'L-BTC': number };
59
+ /** Submarine swaps opened against the mock /v2 maker, by id. */
60
+ submarineSwaps: Record<string, { from: 'L-USDT' | 'L-BTC'; invoice: string; expected: number; funded: boolean }> = {};
61
+ /** RGB assets the node knows about (seeded USDT/XAUT + anything issued). */
62
+ rgbAssets: MockRgbAsset[];
63
+ transfers: MockTransfer[] = [];
64
+ sends: SendRecord[] = [];
65
+
66
+ private readonly opts: MockWalletOptions;
67
+
68
+ constructor(o: MockWalletOptions = {}) {
69
+ this.opts = o;
70
+ this.priceUsd = o.priceUsd ?? 65_000;
71
+ this.failRoute = o.failRoute ?? false;
72
+ // Copies: the wallet mutates its state, and reset() rebuilds from `o`.
73
+ this.balances = { ...(o.balances ?? { spark: 500_000, rln: 300_000, arkade: 200_000 }) };
74
+ this.assets = { ...(o.assets ?? { USDT: 25_000_000, XAUT: 0 }) };
75
+ this.contacts = o.contacts ? [...o.contacts] : [
76
+ { name: 'bob', ln_address: 'bob@kaleidoswap.com' },
77
+ { name: 'alice', ln_address: 'alice@kaleidoswap.com' },
78
+ { name: 'john', ln_address: 'john.smith@kaleidoswap.com' },
79
+ { name: 'john', ln_address: 'john.doe@kaleidoswap.com' }, // ambiguous on purpose
80
+ ];
81
+ this.utxos = o.utxos ?? 5;
82
+ this.liquid = { ...(o.liquid ?? { 'L-USDT': 100 * 1e8, 'L-BTC': 200_000 }) };
83
+ this.rgbAssets = Object.keys(this.assets).map((ticker) => ({
84
+ asset_id: mockAssetId(ticker),
85
+ ticker,
86
+ name: ticker === 'USDT' ? 'Tether USD' : ticker === 'XAUT' ? 'Tether Gold' : ticker,
87
+ precision: 0,
88
+ schema: 'NIA' as const,
89
+ issued_supply: this.assets[ticker]!,
90
+ }));
91
+ }
92
+
93
+ private findAsset(ref: unknown): MockRgbAsset | undefined {
94
+ const q = String(ref ?? '');
95
+ return this.rgbAssets.find((a) => a.asset_id === q || a.ticker.toUpperCase() === q.toUpperCase());
96
+ }
97
+
98
+ /** Issue a new RGB asset — mirrors the node: needs a free colored UTXO. */
99
+ issueAsset(args: Record<string, unknown>): MockRgbAsset {
100
+ if (this.utxos < 1) throw new Error('No available colored UTXOs — call rln_create_utxos first.');
101
+ const schema = (['NIA', 'CFA', 'UDA'].includes(String(args.schema)) ? args.schema : 'NIA') as MockRgbAsset['schema'];
102
+ const ticker = String(args.ticker ?? '').toUpperCase();
103
+ if (!/^[A-Z0-9]{1,8}$/.test(ticker)) throw new Error('ticker must be 1-8 uppercase letters/digits.');
104
+ if (this.findAsset(ticker)) throw new Error(`An asset with ticker ${ticker} already exists.`);
105
+ const supply = schema === 'UDA' ? 1 : Number(args.amount);
106
+ if (!(supply > 0)) throw new Error('amount must be positive.');
107
+ const asset: MockRgbAsset = {
108
+ asset_id: mockAssetId(ticker),
109
+ ticker,
110
+ name: String(args.name ?? ticker),
111
+ precision: Number(args.precision ?? 0),
112
+ schema,
113
+ issued_supply: supply,
114
+ };
115
+ this.utxos -= 1;
116
+ this.rgbAssets.push(asset);
117
+ this.assets[ticker] = supply;
118
+ this.transfers.push({ idx: this.transfers.length + 1, asset_id: asset.asset_id, kind: 'Issuance', status: 'Settled', amount: supply });
119
+ this.sends.push({ tool: 'rln_issue_asset', to: 'self', asset: ticker, amount: supply });
120
+ return asset;
121
+ }
122
+
123
+ totalSats(): number {
124
+ return this.balances.spark + this.balances.rln + this.balances.arkade;
125
+ }
126
+ /** Back to the constructor state: balances, contacts, UTXOs, issued assets, history. */
127
+ reset(): void {
128
+ Object.assign(this, new MockWallet(this.opts));
129
+ }
130
+
131
+ private send(tool: string, rec: { to: string; amount_sats?: number; asset?: string; amount?: number }) {
132
+ if (this.failRoute) throw new Error('No route to destination.');
133
+ if (rec.amount_sats != null && rec.amount_sats > this.totalSats()) {
134
+ throw new Error(`Insufficient funds: have ${this.totalSats()} sats, need ${rec.amount_sats}.`);
135
+ }
136
+ this.sends.push({ tool, ...rec });
137
+ return { status: 'SUCCESS', payment_hash: 'mock' + this.sends.length };
138
+ }
139
+
140
+ handlers(): Record<string, WalletHandler> {
141
+ return {
142
+ get_balances: async () => ({
143
+ total_sats: this.totalSats(),
144
+ layers: [
145
+ { layer: 'spark', btc_sats: this.balances.spark, assets: [] },
146
+ { layer: 'rln', btc_sats: this.balances.rln, assets: Object.entries(this.assets).filter(([, v]) => v > 0).map(([ticker, amount]) => ({ ticker, amount })) },
147
+ { layer: 'arkade', btc_sats: this.balances.arkade, assets: [] },
148
+ ],
149
+ }),
150
+ spark_get_balance: async () => ({ btc_sats: this.balances.spark }),
151
+ rln_get_balances: async () => ({ btc_sats: this.balances.rln, assets: this.assets }),
152
+ arkade_get_balance: async () => ({ btc_sats: this.balances.arkade }),
153
+ spark_get_address: async () => ({ address: 'bc1qspark0mockreceiveaddr' }),
154
+ arkade_get_address: async () => ({ address: 'ark1q0mockreceiveaddr' }),
155
+ get_price: async ({ fiat }) => ({ asset: 'BTC', price_usd: this.priceUsd, fiat: (fiat as string) ?? 'USD' }),
156
+ fiat_to_sats: async ({ amount }) => ({ sats: Math.round((Number(amount) / this.priceUsd) * 1e8) }),
157
+ resolve_contact: async ({ name }) => {
158
+ const q = String(name).toLowerCase();
159
+ const matches = this.contacts.filter((c) => c.name.toLowerCase() === q);
160
+ if (matches.length === 0) throw new Error(`No contact named "${name}".`);
161
+ if (matches.length > 1) throw new Error(`Ambiguous: ${matches.length} contacts named "${name}" — ask the user which one.`);
162
+ return matches[0]!;
163
+ },
164
+ rln_create_ln_invoice: async ({ amount_sats }) => ({ invoice: `lnbcmock${amount_sats ?? ''}` }),
165
+ rln_create_rgb_invoice: async ({ asset, amount }) => ({ invoice: 'rgb:mockinvoice', asset, amount }),
166
+ send_payment: async ({ to, amount_sats }) => this.send('send_payment', { to: String(to), amount_sats: amount_sats != null ? Number(amount_sats) : undefined }),
167
+ rln_pay_invoice: async ({ invoice }) => this.send('rln_pay_invoice', { to: String(invoice) }),
168
+ rln_send_asset: async ({ asset, amount, to }) => {
169
+ const a = String(asset);
170
+ if ((this.assets[a] ?? 0) < Number(amount)) throw new Error(`Insufficient ${a} balance.`);
171
+ const res = this.send('rln_send_asset', { to: String(to), asset: a, amount: Number(amount) });
172
+ const found = this.findAsset(a);
173
+ if (found) this.transfers.push({ idx: this.transfers.length + 1, asset_id: found.asset_id, kind: 'Send', status: 'Settled', amount: Number(amount) });
174
+ return res;
175
+ },
176
+ rln_list_assets: async () => ({
177
+ assets: this.rgbAssets.map((a) => ({ ...a, balance: { spendable: this.assets[a.ticker] ?? 0 } })),
178
+ }),
179
+ rln_list_transfers: async ({ asset_id, asset }) => {
180
+ const ref = asset_id ?? asset;
181
+ const found = this.findAsset(ref);
182
+ if (!found) throw new Error(`Unknown asset "${String(ref)}".`);
183
+ return { transfers: this.transfers.filter((x) => x.asset_id === found.asset_id) };
184
+ },
185
+ rln_create_utxos: async ({ num }) => {
186
+ const n = Number(num ?? 5);
187
+ this.utxos += n;
188
+ this.sends.push({ tool: 'rln_create_utxos', to: 'self', amount: n });
189
+ return { created: true, num: n, free_utxos: this.utxos };
190
+ },
191
+ rln_issue_asset: async (args) => {
192
+ const a = this.issueAsset(args);
193
+ return { issued: true, asset_id: a.asset_id, ticker: a.ticker, name: a.name, schema: a.schema, issued_supply: a.issued_supply };
194
+ },
195
+ get_swap_quote: async ({ from_asset, to_asset, amount }) => ({
196
+ quote_id: 'quote-mock',
197
+ from_asset,
198
+ to_asset,
199
+ amount: Number(amount),
200
+ // toy rate: 1 USDT ≈ 1538 sats at $65k; otherwise echo.
201
+ receive_amount: String(from_asset).toUpperCase() === 'USDT' ? Math.round(Number(amount) * (1e8 / this.priceUsd)) : Number(amount),
202
+ }),
203
+ execute_swap: async ({ from_asset, to_asset, amount }) => {
204
+ this.sends.push({ tool: 'execute_swap', to: `${from_asset}->${to_asset}`, amount: Number(amount) });
205
+ return { status: 'SUCCESS', swap_id: 'swap' + this.sends.length };
206
+ },
207
+ };
208
+ }
209
+
210
+ /**
211
+ * The KaleidoSwap /v2 submarine contract against a toy maker: 0.5% fee, the
212
+ * invoice amount read from its BOLT11 human-readable part, L-USDT at `priceUsd`.
213
+ */
214
+ submarineHandlers(): Record<string, SubmarineHandler> {
215
+ return {
216
+ kaleidoswap_submarine_pairs: async () => ({
217
+ pairs: [
218
+ { from: 'L-USDT', to: 'BTC', fees: { percentage: 0.5 }, limits: { minimal: 10 * 1e8, maximal: 1_000 * 1e8 } },
219
+ { from: 'L-BTC', to: 'BTC', fees: { percentage: 0.5 }, limits: { minimal: 10_000, maximal: 1_000_000 } },
220
+ ],
221
+ }),
222
+ kaleidoswap_submarine_create: async ({ invoice, from_asset }) => {
223
+ const from = (from_asset ?? 'L-USDT') as 'L-USDT' | 'L-BTC';
224
+ if (from !== 'L-USDT' && from !== 'L-BTC') throw new Error(`Unsupported from_asset "${String(from_asset)}".`);
225
+ const sats = bolt11Sats(String(invoice));
226
+ if (sats == null) throw new Error('invalid_invoice: the invoice must be BOLT11 with an amount.');
227
+ const withFee = sats * 1.005;
228
+ const expected = Math.ceil(from === 'L-USDT' ? (withFee / 1e8) * this.priceUsd * 1e8 : withFee);
229
+ const id = `sub${Object.keys(this.submarineSwaps).length + 1}`;
230
+ this.submarineSwaps[id] = { from, invoice: String(invoice), expected, funded: false };
231
+ return { swap_id: id, from_asset: from, expected_amount: String(expected), lockup_address: `tlq1qmock${id}` };
232
+ },
233
+ kaleidoswap_submarine_fund: async ({ swap_id }) => {
234
+ const swap = this.submarineSwaps[String(swap_id)];
235
+ if (!swap) throw new Error(`No submarine swap "${String(swap_id)}" was created by this server.`);
236
+ if (swap.funded) throw new Error(`Swap ${String(swap_id)} is already funded.`);
237
+ if (this.liquid[swap.from] < swap.expected) throw new Error(`Insufficient ${swap.from} balance.`);
238
+ this.liquid[swap.from] -= swap.expected;
239
+ swap.funded = true;
240
+ this.sends.push({ tool: 'kaleidoswap_submarine_fund', to: swap.invoice, asset: swap.from, amount: swap.expected });
241
+ return { funded: true, swap_id, asset: swap.from, amount: String(swap.expected), txid: `mocktx${String(swap_id)}` };
242
+ },
243
+ kaleidoswap_submarine_status: async ({ swap_id }) => {
244
+ const swap = this.submarineSwaps[String(swap_id)];
245
+ if (!swap) throw new Error(`invalid_swap_id: "${String(swap_id)}" is not a swap id`);
246
+ const status = swap.funded ? 'transaction.claimed' : 'swap.created';
247
+ return { swap_id, status, done: swap.funded, failed: false, funded: swap.funded };
248
+ },
249
+ };
250
+ }
251
+
252
+ /** Bind the contract tools to this wallet (optionally overriding some — e.g. to inject). */
253
+ registry(overrides?: Partial<Record<string, WalletHandler>>): ToolRegistry {
254
+ const h = { ...this.handlers(), ...(overrides ?? {}) } as Record<string, WalletHandler>;
255
+ const sub = { ...this.submarineHandlers(), ...(overrides ?? {}) } as Record<string, SubmarineHandler>;
256
+ return new ToolRegistry([
257
+ bindWalletTools(h, { layers: ['spark', 'rln', 'arkade', 'core'], allowMissing: true }),
258
+ bindSubmarineTools(sub, { allowMissing: true }),
259
+ ]);
260
+ }
261
+ }
262
+
263
+ /** Deterministic fake RGB contract id per ticker (stable across runs). */
264
+ function mockAssetId(ticker: string): string {
265
+ let h = 0;
266
+ for (const c of ticker) h = (h * 31 + c.charCodeAt(0)) >>> 0;
267
+ return `rgb:mock-${ticker.toLowerCase()}-${h.toString(36)}`;
268
+ }
269
+
270
+ /** Amount in sats from a BOLT11 human-readable part ("lntbs10u…" → 1000), or null. */
271
+ function bolt11Sats(invoice: string): number | null {
272
+ const m = invoice.toLowerCase().match(/^ln(?:bcrt|tbs|bc|tb)(\d+)([munp])?1/);
273
+ if (!m) return null;
274
+ const n = Number(m[1]);
275
+ const mult = { m: 1e5, u: 100, n: 0.1, p: 0.0001 }[m[2] ?? ''] ?? 1e8;
276
+ const sats = n * mult;
277
+ return sats >= 1 ? Math.round(sats) : null;
278
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * A scripted LLMProvider — replays planned turns instead of running a model.
3
+ *
4
+ * Lets hosts wire the full Engine / Funnel (skills, recipes, confirm gate) in
5
+ * unit tests, CI and demos without loading a QVAC model. Deterministic tiers
6
+ * (fast-path, recipes with regex extraction) never call it at all.
7
+ */
8
+
9
+ import type { ToolCall } from '../types.js';
10
+ import type { LLMProvider } from '../providers/types.js';
11
+
12
+ export interface ScriptedTurn {
13
+ /** Assistant text for this turn. */
14
+ text?: string;
15
+ /** Tool to call — ignored when the tool is not in the turn's scoped tool list. */
16
+ tool?: string;
17
+ args?: Record<string, unknown>;
18
+ }
19
+
20
+ export function scriptedProvider(turns: ScriptedTurn[] = [], fallback = 'I need more information.'): LLMProvider {
21
+ let index = 0;
22
+ return {
23
+ name: 'scripted',
24
+ async runTurn(input) {
25
+ const planned = turns[index++] ?? { text: fallback };
26
+ const call: ToolCall | undefined = planned.tool && input.tools.some((t) => t.name === planned.tool)
27
+ ? { id: `scripted-${index}`, name: planned.tool, arguments: planned.args ?? {} }
28
+ : undefined;
29
+ return {
30
+ text: planned.text ?? '',
31
+ rawContent: planned.text ?? '',
32
+ toolCalls: call ? [call] : [],
33
+ inference: { durationMs: 0, status: 'completed' },
34
+ };
35
+ },
36
+ };
37
+ }
@@ -48,6 +48,22 @@ describe('confirmReadback', () => {
48
48
  expect(line).toBe('Pay Lightning invoice lnbc1p…abcd over Spark. Confirm?');
49
49
  });
50
50
 
51
+ it('rln_send_asset: kaleido-mcp argument names', () => {
52
+ expect(confirmReadback({ name: 'rln_send_asset', arguments: { asset_id: 'rgb:abcdefgh-1234567890-xyz', amount: 5, recipient_id: 'utxob:ab12cd34ef56gh78ij90' } }))
53
+ .toBe('Send 5 rgb:ab…-xyz to utxob:…ij90 over RLN. Confirm?');
54
+ });
55
+
56
+ it('rln_send_btc / channel / atomic taker readbacks', () => {
57
+ expect(confirmReadback({ name: 'rln_send_btc', arguments: { address: 'tb1qexampleaddress0000000000xyz', amount_sat: 25000 } }))
58
+ .toBe('Send 25,000 sats on-chain to tb1qex…0xyz over RLN. Confirm?');
59
+ expect(confirmReadback({ name: 'rln_open_channel', arguments: { peer_pubkey_and_addr: '03abcdef0123456789abcdef0123456789@1.2.3.4:9735', capacity_sat: 100000 } }))
60
+ .toBe('Open a 100,000 sats channel to 03abcd…6789. Confirm?');
61
+ expect(confirmReadback({ name: 'rln_close_channel', arguments: { channel_id: 'chan0123456789abcdef0123', peer_pubkey: 'x', force: true } }))
62
+ .toBe('Force-close channel chan01…0123. Confirm?');
63
+ expect(confirmReadback({ name: 'rln_atomic_taker', arguments: { swapstring: '30/rgb:asset/10/btc/3600/abcdef0123456789' } }))
64
+ .toBe('Accept atomic swap 30/rgb…6789 on your node. Confirm?');
65
+ });
66
+
51
67
  it('execute_swap: from → to with amount', () => {
52
68
  expect(confirmReadback({ name: 'execute_swap', arguments: { from_asset: 'BTC', to_asset: 'USDT', amount: 0.01 } }))
53
69
  .toBe('Swap 0.01 BTC for USDT. Confirm?');
@@ -31,7 +31,7 @@ function isRef(s: string): boolean {
31
31
  /** Shorten an address/invoice for speech; leave contact names intact. */
32
32
  function shortRef(s: string): string {
33
33
  const v = s.trim();
34
- return isRef(v) ? `${v.slice(0, 6)}…${v.slice(-4)}` : v;
34
+ return isRef(v) && v.length > 12 ? `${v.slice(0, 6)}…${v.slice(-4)}` : v;
35
35
  }
36
36
 
37
37
  /** " over Spark" suffix for the call's layer (explicit arg wins, else the tool's). */
@@ -41,8 +41,26 @@ function over(name: string, args: Record<string, unknown>): string {
41
41
  return label ? ` over ${label}` : '';
42
42
  }
43
43
 
44
- const sats = (v: unknown) => `${fmtNum(Number(v))} sats`;
45
- const asset = (amount: unknown, ticker: unknown) => `${fmtNum(Number(amount))} ${String(ticker)}`;
44
+ /** A finite number from a tool arg, else undefined (never NaN in a readback). */
45
+ function num(v: unknown): number | undefined {
46
+ if (typeof v === 'number') return Number.isFinite(v) ? v : undefined;
47
+ if (typeof v === 'string' && v.trim() !== '' && Number.isFinite(Number(v))) return Number(v);
48
+ return undefined;
49
+ }
50
+
51
+ /** A display string from a tool arg, stripped of stray quotes/commas a model may leave in. */
52
+ function label(v: unknown): string {
53
+ return String(v ?? '').trim().replace(/^[\s'"`,]+|[\s'"`,]+$/g, '') || '?';
54
+ }
55
+
56
+ const sats = (v: unknown) => {
57
+ const n = num(v);
58
+ return n === undefined ? 'an unspecified amount of sats' : `${fmtNum(n)} sats`;
59
+ };
60
+ const asset = (amount: unknown, ticker: unknown) => {
61
+ const n = num(amount);
62
+ return n === undefined ? `an unspecified amount of ${label(ticker)}` : `${fmtNum(n)} ${label(ticker)}`;
63
+ };
46
64
 
47
65
  /** A spoken confirmation ending in "Confirm?", or null for non-spend tools. */
48
66
  export function confirmReadback(call: { name: string; arguments: Record<string, unknown> }): string | null {
@@ -62,12 +80,32 @@ export function confirmReadback(call: { name: string; arguments: Record<string,
62
80
  return ask(`Send ${sats(a.amount_sats)} to ${to()}${over(name, a)}`);
63
81
  case 'rln_send_asset':
64
82
  case 'liquid_send':
65
- return ask(`Send ${asset(a.amount, a.asset)} to ${to()}${over(name, a)}`);
83
+ // kaleido-mcp names these asset_id / recipient_id.
84
+ return ask(`Send ${asset(a.amount, a.asset ?? shortRef(String(a.asset_id ?? '')))} to ${to(a.to != null ? 'to' : 'recipient_id')}${over(name, a)}`);
66
85
  case 'rln_pay_invoice':
67
86
  case 'spark_pay_invoice':
68
87
  return ask(`Pay Lightning invoice ${shortRef(String(a.invoice ?? ''))}${over(name, a)}`);
88
+ case 'rln_issue_asset': {
89
+ const named = a.name != null && a.ticker != null && label(a.name) !== label(a.ticker) ? ` (${label(a.name)})` : '';
90
+ return a.schema === 'UDA'
91
+ ? ask(`Issue unique asset ${label(a.ticker ?? a.name)}${named} on RGB`)
92
+ : ask(`Issue ${asset(a.amount, a.ticker ?? a.name)}${named}, a new RGB asset`);
93
+ }
94
+ case 'rln_create_utxos':
95
+ return ask(`Create ${fmtNum(num(a.num) ?? 5)} RGB UTXOs on-chain${over(name, a)}`);
96
+ case 'rln_send_btc':
97
+ return ask(`Send ${sats(a.amount_sat)} on-chain to ${to('address')}${over(name, a)}`);
98
+ case 'rln_open_channel': {
99
+ const peer = shortRef(String(a.peer_pubkey_and_addr ?? '').split('@')[0] ?? '');
100
+ const withAsset = a.asset_id != null && num(a.asset_amount) !== undefined ? ` with ${fmtNum(num(a.asset_amount)!)} of ${shortRef(String(a.asset_id))}` : '';
101
+ return ask(`Open a ${sats(a.capacity_sat)} channel to ${peer}${withAsset}`);
102
+ }
103
+ case 'rln_close_channel':
104
+ return ask(`${a.force ? 'Force-close' : 'Close'} channel ${shortRef(String(a.channel_id ?? ''))}`);
105
+ case 'rln_atomic_taker':
106
+ return ask(`Accept atomic swap ${shortRef(String(a.swapstring ?? ''))} on your node`);
69
107
  case 'execute_swap':
70
- return ask(`Swap ${fmtNum(Number(a.amount))} ${String(a.from_asset)} for ${String(a.to_asset)}`);
108
+ return ask(`Swap ${asset(a.amount, a.from_asset)} for ${label(a.to_asset)}`);
71
109
  default:
72
110
  // Unknown but spend-flagged tool → a generic, still-honest readback.
73
111
  return getWalletTool(name)?.spend ? ask(`Confirm ${name.replace(/_/g, ' ')}`) : null;
@@ -1,6 +1,8 @@
1
1
  /** Wallet contract tests — integrity of the single source of truth. */
2
2
 
3
3
  import { describe, it, expect, vi } from 'vitest';
4
+ import { readFileSync } from 'node:fs';
5
+ import { parseSkill } from '../skills/registry.js';
4
6
  import {
5
7
  WALLET_TOOLS,
6
8
  SPEND_TOOLS,
@@ -40,9 +42,13 @@ describe('WALLET_TOOLS contract', () => {
40
42
  expect(isSpendTool('execute_swap')).toBe(true);
41
43
  expect(isSpendTool('spark_send')).toBe(true);
42
44
  expect(isSpendTool('spark_pay_invoice')).toBe(true);
45
+ expect(isSpendTool('rln_issue_asset')).toBe(true);
46
+ expect(isSpendTool('rln_create_utxos')).toBe(true);
43
47
  // reads are not
44
48
  expect(isSpendTool('get_balances')).toBe(false);
45
49
  expect(isSpendTool('get_price')).toBe(false);
50
+ expect(isSpendTool('rln_list_assets')).toBe(false);
51
+ expect(isSpendTool('rln_list_transfers')).toBe(false);
46
52
  expect([...SPEND_TOOLS].length).toBeGreaterThanOrEqual(5);
47
53
  });
48
54
 
@@ -97,3 +103,35 @@ describe('bindWalletTools', () => {
97
103
  expect(src.listTools().map((t) => t.name)).toEqual(['spark_get_balance']);
98
104
  });
99
105
  });
106
+
107
+ describe('RLN tools vs kaleido-mcp and the rgb-lightning-node skill', () => {
108
+ // The rln_* tools kaleido-mcp registers (rln_mpp_pay belongs to paid-data).
109
+ const MCP_RLN = [
110
+ 'rln_get_node_info', 'rln_get_balances', 'rln_get_asset_balance', 'rln_list_assets',
111
+ 'rln_get_address', 'rln_create_rgb_invoice', 'rln_create_ln_invoice', 'rln_pay_invoice',
112
+ 'rln_send_btc', 'rln_send_asset', 'rln_list_channels', 'rln_connect_peer', 'rln_open_channel',
113
+ 'rln_close_channel', 'rln_get_channel_id', 'rln_list_payments', 'rln_refresh_transfers',
114
+ 'rln_atomic_taker', 'rln_list_swaps', 'rln_get_swap',
115
+ ];
116
+
117
+ it('covers every rln_* tool kaleido-mcp exposes', () => {
118
+ const missing = MCP_RLN.filter((n) => !getWalletTool(n));
119
+ expect(missing).toEqual([]);
120
+ });
121
+
122
+ it('gates every fund-moving or committing RLN tool', () => {
123
+ for (const n of ['rln_send_btc', 'rln_open_channel', 'rln_close_channel', 'rln_atomic_taker']) {
124
+ expect(isSpendTool(n)).toBe(true);
125
+ }
126
+ for (const n of ['rln_get_asset_balance', 'rln_refresh_transfers', 'rln_list_swaps', 'rln_get_swap']) {
127
+ expect(isSpendTool(n)).toBe(false);
128
+ }
129
+ });
130
+
131
+ it('the skill only declares tools that exist in the contract', () => {
132
+ const md = readFileSync(new URL('../../skills/rgb-lightning-node/SKILL.md', import.meta.url), 'utf8');
133
+ const skill = parseSkill(md);
134
+ const unknown = (skill.tools ?? []).filter((n) => !getWalletTool(n));
135
+ expect(unknown).toEqual([]);
136
+ });
137
+ });
@@ -91,6 +91,42 @@ export const WALLET_TOOLS: WalletToolDef[] = [
91
91
  t('rln', 'rln_create_rgb_invoice', 'Create an RGB asset invoice to receive an asset (e.g. USDT).', { asset, amount: { type: 'number', description: 'Asset amount' } }, ['asset', 'amount']),
92
92
  t('rln', 'rln_pay_invoice', 'Pay a Lightning invoice from the RLN node.', { invoice: { type: 'string' } }, ['invoice'], true),
93
93
  t('rln', 'rln_send_asset', 'Send an RGB asset (e.g. USDT) to a recipient.', { asset, amount: { type: 'number' }, to: { type: 'string' } }, ['asset', 'amount', 'to'], true),
94
+ t('rln', 'rln_list_assets', 'List RGB assets held by the RLN node (asset_id, ticker, name, precision, balances).'),
95
+ t('rln', 'rln_get_asset_balance', 'Get the balance of one RGB asset by asset_id (settled, future, spendable, off-chain).', { asset_id: { type: 'string', description: "RGB asset id, e.g. 'rgb:2JEUOrsc-…'" } }, ['asset_id']),
96
+ t('rln', 'rln_refresh_transfers', 'Refresh pending RGB transfers so balances and transfer status are up to date.'),
97
+ t('rln', 'rln_get_address', 'Get an on-chain BTC address of the RLN node for deposits.'),
98
+ t('rln', 'rln_send_btc', 'Send on-chain BTC from the RLN node.', { address: { type: 'string', description: 'Destination Bitcoin address' }, amount_sat: { type: 'number', description: 'Amount in satoshis' }, fee_rate: { type: 'number', description: 'sat/vbyte (default 3)' } }, ['address', 'amount_sat'], true),
99
+ t('rln', 'rln_list_payments', 'List recent Lightning payments (sent and received) on the RLN node.', { limit: { type: 'number', description: 'Max payments (default 20)' } }),
100
+ t('rln', 'rln_connect_peer', 'Connect the RLN node to a Lightning peer.', { peer_pubkey_and_addr: { type: 'string', description: 'pubkey@host:port' } }, ['peer_pubkey_and_addr']),
101
+ t('rln', 'rln_open_channel', 'Open a Lightning channel, optionally allocating an RGB asset to it.', {
102
+ peer_pubkey_and_addr: { type: 'string', description: 'pubkey@host:port' },
103
+ capacity_sat: { type: 'number', description: 'Channel capacity in satoshis' },
104
+ push_msat: { type: 'number', description: 'Millisatoshis pushed to the peer (default 0)' },
105
+ asset_id: { type: 'string', description: 'RGB asset id to allocate' },
106
+ asset_amount: { type: 'number', description: 'RGB asset amount to allocate' },
107
+ is_public: { type: 'boolean', description: 'Announce the channel (default false)' },
108
+ }, ['peer_pubkey_and_addr', 'capacity_sat'], true),
109
+ t('rln', 'rln_close_channel', 'Close a Lightning channel (force only for an unresponsive peer).', { channel_id: { type: 'string' }, peer_pubkey: { type: 'string' }, force: { type: 'boolean', description: 'Unilateral close (default false)' } }, ['channel_id', 'peer_pubkey'], true),
110
+ t('rln', 'rln_get_channel_id', 'Resolve a temporary_channel_id from rln_open_channel to the final channel_id.', { temporary_channel_id: { type: 'string' } }, ['temporary_channel_id']),
111
+ t('rln', 'rln_atomic_taker', 'Whitelist a maker swap on the node (taker side). Pass the swapstring from kaleidoswap_atomic_init, before kaleidoswap_atomic_execute.', { swapstring: { type: 'string' } }, ['swapstring'], true),
112
+ t('rln', 'rln_list_swaps', 'List atomic swaps on the RLN node (maker and taker sides).'),
113
+ t('rln', 'rln_get_swap', 'Get an atomic swap by payment_hash.', { payment_hash: { type: 'string' }, taker: { type: 'boolean', description: 'Taker-side swap' } }, ['payment_hash']),
114
+ t('rln', 'rln_list_transfers', 'List RGB transfers for one asset (issuance, sends, receives) with their status — use to check if an RGB invoice was paid.', { asset_id: { type: 'string', description: 'RGB asset id (in-app wallets also accept a ticker)' } }, ['asset_id']),
115
+ // Issuance + UTXO prep spend on-chain BTC and are irreversible → gated like any spend.
116
+ t('rln', 'rln_create_utxos', 'Create colorable UTXOs on the RLN node — needed before issuing or receiving RGB assets on a fresh node.', {
117
+ num: { type: 'number', description: 'How many UTXOs (default 5)' },
118
+ size: { type: 'number', description: 'Size of each UTXO in sats (default: node decides)' },
119
+ up_to: { type: 'boolean', description: 'Only top up to `num` free UTXOs (default false)' },
120
+ fee_rate: { type: 'number', description: 'sat/vbyte (default 1)' },
121
+ }, [], true),
122
+ t('rln', 'rln_issue_asset', 'Issue a NEW RGB asset owned by this node (e.g. an event ticket or loyalty token).', {
123
+ schema: { type: 'string', enum: ['NIA', 'CFA', 'UDA'], description: 'NIA = token (default), CFA = collectible, UDA = unique/NFT' },
124
+ name: { type: 'string', description: 'Asset name, e.g. "Hackathon Ticket"' },
125
+ ticker: { type: 'string', description: 'Uppercase ticker, e.g. "TICKET". Required for NIA and UDA' },
126
+ amount: { type: 'number', description: 'Total supply in display units. Required for NIA and CFA; UDA is always 1' },
127
+ precision: { type: 'number', description: 'Decimal places (default 0)' },
128
+ details: { type: 'string', description: 'Optional description (CFA and UDA)' },
129
+ }, ['name'], true),
94
130
 
95
131
  // ── Arkade ─────────────────────────────────────────────────────────────
96
132
  t('arkade', 'arkade_get_balance', 'Get the Arkade wallet balance.'),