@kaleidorg/mind 0.5.1 → 0.6.1

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 (174) hide show
  1. package/dist/autonomy/index.d.ts +21 -0
  2. package/dist/autonomy/index.d.ts.map +1 -0
  3. package/dist/autonomy/index.js +16 -0
  4. package/dist/autonomy/index.js.map +1 -0
  5. package/dist/autonomy/prompt.d.ts +21 -0
  6. package/dist/autonomy/prompt.d.ts.map +1 -0
  7. package/dist/autonomy/prompt.js +37 -0
  8. package/dist/autonomy/prompt.js.map +1 -0
  9. package/dist/autonomy/risk.d.ts +53 -0
  10. package/dist/autonomy/risk.d.ts.map +1 -0
  11. package/dist/autonomy/risk.js +74 -0
  12. package/dist/autonomy/risk.js.map +1 -0
  13. package/dist/autonomy/run-state.d.ts +39 -0
  14. package/dist/autonomy/run-state.d.ts.map +1 -0
  15. package/dist/autonomy/run-state.js +118 -0
  16. package/dist/autonomy/run-state.js.map +1 -0
  17. package/dist/autonomy/scheduler.d.ts +18 -0
  18. package/dist/autonomy/scheduler.d.ts.map +1 -0
  19. package/dist/autonomy/scheduler.js +113 -0
  20. package/dist/autonomy/scheduler.js.map +1 -0
  21. package/dist/autonomy/task-store.d.ts +44 -0
  22. package/dist/autonomy/task-store.d.ts.map +1 -0
  23. package/dist/autonomy/task-store.js +139 -0
  24. package/dist/autonomy/task-store.js.map +1 -0
  25. package/dist/autonomy/types.d.ts +164 -0
  26. package/dist/autonomy/types.d.ts.map +1 -0
  27. package/dist/autonomy/types.js +20 -0
  28. package/dist/autonomy/types.js.map +1 -0
  29. package/dist/bitrefill/contract.d.ts +60 -0
  30. package/dist/bitrefill/contract.d.ts.map +1 -0
  31. package/dist/bitrefill/contract.js +119 -0
  32. package/dist/bitrefill/contract.js.map +1 -0
  33. package/dist/context/compress.d.ts +65 -0
  34. package/dist/context/compress.d.ts.map +1 -0
  35. package/dist/context/compress.js +181 -0
  36. package/dist/context/compress.js.map +1 -0
  37. package/dist/engine.d.ts +20 -0
  38. package/dist/engine.d.ts.map +1 -1
  39. package/dist/engine.js +23 -4
  40. package/dist/engine.js.map +1 -1
  41. package/dist/evidence.d.ts +62 -0
  42. package/dist/evidence.d.ts.map +1 -0
  43. package/dist/evidence.js +47 -0
  44. package/dist/evidence.js.map +1 -0
  45. package/dist/flashnet/contract.d.ts +56 -0
  46. package/dist/flashnet/contract.d.ts.map +1 -0
  47. package/dist/flashnet/contract.js +100 -0
  48. package/dist/flashnet/contract.js.map +1 -0
  49. package/dist/funnel.d.ts +11 -0
  50. package/dist/funnel.d.ts.map +1 -1
  51. package/dist/funnel.js +62 -7
  52. package/dist/funnel.js.map +1 -1
  53. package/dist/index.d.ts +12 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +11 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/kaleidoswap/contract.js +1 -1
  58. package/dist/kaleidoswap/contract.js.map +1 -1
  59. package/dist/knowledge/bitcoin-copilot.d.ts.map +1 -1
  60. package/dist/knowledge/bitcoin-copilot.js +85 -2
  61. package/dist/knowledge/bitcoin-copilot.js.map +1 -1
  62. package/dist/providers/types.d.ts +17 -0
  63. package/dist/providers/types.d.ts.map +1 -1
  64. package/dist/qvac/index.d.ts +1 -1
  65. package/dist/qvac/index.d.ts.map +1 -1
  66. package/dist/qvac/index.js.map +1 -1
  67. package/dist/qvac/parse.d.ts +18 -0
  68. package/dist/qvac/parse.d.ts.map +1 -1
  69. package/dist/qvac/parse.js +1 -0
  70. package/dist/qvac/parse.js.map +1 -1
  71. package/dist/qvac/provider.d.ts +16 -0
  72. package/dist/qvac/provider.d.ts.map +1 -1
  73. package/dist/qvac/provider.js +40 -1
  74. package/dist/qvac/provider.js.map +1 -1
  75. package/dist/qvac/stream.d.ts +22 -0
  76. package/dist/qvac/stream.d.ts.map +1 -1
  77. package/dist/qvac/stream.js +33 -1
  78. package/dist/qvac/stream.js.map +1 -1
  79. package/dist/recipe/buy-asset-channel.d.ts +1 -1
  80. package/dist/recipe/buy-asset-channel.d.ts.map +1 -1
  81. package/dist/recipe/buy-asset-channel.js +4 -3
  82. package/dist/recipe/buy-asset-channel.js.map +1 -1
  83. package/dist/recipe/flashnet-swap.d.ts +35 -0
  84. package/dist/recipe/flashnet-swap.d.ts.map +1 -0
  85. package/dist/recipe/flashnet-swap.js +239 -0
  86. package/dist/recipe/flashnet-swap.js.map +1 -0
  87. package/dist/recipe/kaleidoswap-atomic.d.ts +1 -1
  88. package/dist/recipe/kaleidoswap-atomic.d.ts.map +1 -1
  89. package/dist/recipe/kaleidoswap-atomic.js +42 -20
  90. package/dist/recipe/kaleidoswap-atomic.js.map +1 -1
  91. package/dist/recipe/kaleidoswap-channel-order.d.ts.map +1 -1
  92. package/dist/recipe/kaleidoswap-channel-order.js +31 -10
  93. package/dist/recipe/kaleidoswap-channel-order.js.map +1 -1
  94. package/dist/recipe/kaleidoswap-price.d.ts.map +1 -1
  95. package/dist/recipe/kaleidoswap-price.js +7 -1
  96. package/dist/recipe/kaleidoswap-price.js.map +1 -1
  97. package/dist/recipe/runner.d.ts.map +1 -1
  98. package/dist/recipe/runner.js +43 -3
  99. package/dist/recipe/runner.js.map +1 -1
  100. package/dist/recipe/swap.d.ts.map +1 -1
  101. package/dist/recipe/swap.js +14 -1
  102. package/dist/recipe/swap.js.map +1 -1
  103. package/dist/tools/mcp.d.ts +19 -0
  104. package/dist/tools/mcp.d.ts.map +1 -1
  105. package/dist/tools/mcp.js +51 -9
  106. package/dist/tools/mcp.js.map +1 -1
  107. package/dist/wallet/confirm.d.ts.map +1 -1
  108. package/dist/wallet/confirm.js +1 -0
  109. package/dist/wallet/confirm.js.map +1 -1
  110. package/dist/wallet/contract.d.ts.map +1 -1
  111. package/dist/wallet/contract.js +20 -4
  112. package/dist/wallet/contract.js.map +1 -1
  113. package/package.json +5 -4
  114. package/skills/bitrefill/SKILL.md +152 -52
  115. package/skills/channel-manager/SKILL.md +59 -0
  116. package/skills/dca/SKILL.md +48 -0
  117. package/skills/flashnet-swaps/SKILL.md +158 -0
  118. package/skills/kaleido-lsps/SKILL.md +34 -17
  119. package/skills/kaleido-trading/SKILL.md +37 -13
  120. package/skills/liquidity-optimizer/SKILL.md +91 -0
  121. package/skills/merchant-finder/SKILL.md +2 -2
  122. package/skills/portfolio-manager/SKILL.md +67 -0
  123. package/skills/rgb-lightning-node/SKILL.md +38 -11
  124. package/skills/spark-wallet/SKILL.md +235 -0
  125. package/skills/wallet-assistant/SKILL.md +2 -2
  126. package/src/autonomy/autonomy.test.ts +348 -0
  127. package/src/autonomy/index.ts +50 -0
  128. package/src/autonomy/prompt.ts +48 -0
  129. package/src/autonomy/risk.ts +139 -0
  130. package/src/autonomy/run-state.ts +144 -0
  131. package/src/autonomy/scheduler.ts +120 -0
  132. package/src/autonomy/task-store.ts +167 -0
  133. package/src/autonomy/types.ts +186 -0
  134. package/src/bitrefill/contract.test.ts +89 -0
  135. package/src/bitrefill/contract.ts +190 -0
  136. package/src/context/compress.test.ts +120 -0
  137. package/src/context/compress.ts +230 -0
  138. package/src/engine.test.ts +34 -0
  139. package/src/engine.ts +35 -4
  140. package/src/evidence.test.ts +80 -0
  141. package/src/evidence.ts +114 -0
  142. package/src/flashnet/contract.test.ts +101 -0
  143. package/src/flashnet/contract.ts +164 -0
  144. package/src/funnel.mind.test.ts +390 -0
  145. package/src/funnel.ts +73 -8
  146. package/src/index.ts +92 -1
  147. package/src/kaleidoswap/contract.ts +1 -1
  148. package/src/knowledge/bitcoin-copilot.ts +96 -2
  149. package/src/providers/types.ts +18 -0
  150. package/src/qvac/index.ts +1 -0
  151. package/src/qvac/parse.ts +20 -0
  152. package/src/qvac/provider.test.ts +17 -0
  153. package/src/qvac/provider.ts +62 -2
  154. package/src/qvac/stream.test.ts +36 -0
  155. package/src/qvac/stream.ts +54 -1
  156. package/src/recipe/buy-asset-channel.test.ts +5 -0
  157. package/src/recipe/buy-asset-channel.ts +6 -3
  158. package/src/recipe/flashnet-swap.test.ts +114 -0
  159. package/src/recipe/flashnet-swap.ts +266 -0
  160. package/src/recipe/kaleidoswap-atomic.test.ts +24 -3
  161. package/src/recipe/kaleidoswap-atomic.ts +39 -20
  162. package/src/recipe/kaleidoswap-channel-order.test.ts +38 -0
  163. package/src/recipe/kaleidoswap-channel-order.ts +27 -9
  164. package/src/recipe/kaleidoswap-price.ts +7 -1
  165. package/src/recipe/recipe.test.ts +21 -0
  166. package/src/recipe/runner.ts +46 -3
  167. package/src/recipe/swap.ts +16 -1
  168. package/src/tools/mcp.live.test.ts +116 -0
  169. package/src/tools/mcp.parse.test.ts +37 -0
  170. package/src/tools/mcp.ts +55 -9
  171. package/src/wallet/confirm.test.ts +8 -0
  172. package/src/wallet/confirm.ts +1 -0
  173. package/src/wallet/contract.test.ts +10 -0
  174. package/src/wallet/contract.ts +26 -4
@@ -0,0 +1,235 @@
1
+ ---
2
+ name: spark-wallet
3
+ description: "Operate the user's Spark BTC wallet on this device — check Spark balance, get a Spark deposit address, create a Spark Lightning invoice to receive, pay any BOLT11 Lightning invoice with Spark, or send BTC on-chain from Spark. Use this when the user names Spark explicitly OR when paying a Lightning invoice on a phone where Spark is the connected layer. Pairs with the bitrefill skill: a Bitrefill purchase that returns a Lightning invoice is paid with `spark_pay_invoice`."
4
+ tools: spark_get_balance, spark_get_address, spark_get_onchain_address, spark_create_invoice, spark_pay_invoice, spark_send, get_price, fiat_to_sats, bitrefill_search, bitrefill_get_product, bitrefill_get_balance, bitrefill_create_invoice, bitrefill_get_invoice, bitrefill_get_order
5
+ triggers: spark, sprak, spakr, spark wallet, pay with spark, send with spark, spark balance, spark address, spark invoice, lightning invoice, pay invoice, bolt11, ln invoice, pay this invoice, on-chain address, onchain address, deposit address, deposit btc, fund spark
6
+ metadata:
7
+ author: kaleidoswap
8
+ version: "1.0.0"
9
+ layer: spark
10
+ ---
11
+
12
+ # Spark wallet
13
+
14
+ Spark is one of the user's connected BTC layers (alongside RLN/RGB and Arkade
15
+ on some hosts). It speaks Lightning natively — receive via `spark_create_invoice`,
16
+ send via `spark_pay_invoice` (BOLT11) or `spark_send` (on-chain). All numbers
17
+ are in **satoshis** unless stated otherwise.
18
+
19
+ ## Three "addresses", three different tools (read this carefully)
20
+
21
+ The word "address" can mean three completely different things on Spark.
22
+ Each has a DIFFERENT tool, a DIFFERENT shape, and a DIFFERENT use. If you
23
+ return the wrong one, the user loses money on a bad deposit. Pick by what
24
+ the user is trying to DO, not just by the word "address":
25
+
26
+ | User intent | Tool | What you get | Looks like |
27
+ |---|---|---|---|
28
+ | Receive a **Spark-to-Spark** transfer (off-chain, within Spark) | `spark_get_address` | Spark identity / pubkey | `sparkrt1…` / `spark1…` |
29
+ | Deposit **L1 Bitcoin** into Spark from the on-chain world | `spark_get_onchain_address` | Real Bitcoin on-chain address | `bc1…` / `tb1…` / `bcrt1…` |
30
+ | Receive over **Lightning** (BOLT11) | `spark_create_invoice` | A Lightning invoice string | `lnbc…` / `lntb…` / `lnbcrt…` |
31
+
32
+ **Disambiguation by phrasing — examples:**
33
+
34
+ - "give me my **on-chain address**" / "**bitcoin address** to fund Spark"
35
+ / "**deposit address**" / "where do I send BTC from my hardware wallet
36
+ / mainnet" → `spark_get_onchain_address`. Result starts with bc1/tb1/
37
+ bcrt1 — verify before replying.
38
+ - "my **Spark address**" / "give me a **Spark address**" / "where do I
39
+ receive a **Spark transfer**" → `spark_get_address`. Result starts with
40
+ spark1/sparkrt1. **DO NOT label this as an on-chain address — it is
41
+ off-chain.**
42
+ - "give me an **invoice for N sats**" / "an **LN invoice**" / "**pay me**
43
+ N sats" → `spark_create_invoice({amount_sats: N})`. Result is a `lnbc…`
44
+ string.
45
+
46
+ **Critical: when the user says "on-chain", they mean L1 Bitcoin.** Never
47
+ return a `sparkrt1…` and call it "on-chain". If you return a
48
+ `spark_get_address` result, you MUST describe it as a Spark (off-chain)
49
+ address — never as an on-chain or Bitcoin address. If you return a
50
+ `spark_get_onchain_address` result, you can call it an on-chain Bitcoin
51
+ deposit address.
52
+
53
+ A useful sanity check before you send the reply: glance at the
54
+ `address` string's prefix. If it begins with `spark`, it's the off-chain
55
+ Spark identity. If it begins with `bc1`/`tb1`/`bcrt1`, it's an on-chain
56
+ BTC address. If it begins with `lnbc`/`lntb`/`lnbcrt`, it's a Lightning
57
+ invoice. Whatever you call it in your reply MUST match its actual prefix.
58
+
59
+ ## What Spark holds (and what it does NOT)
60
+
61
+ This is the single most important thing to get right when the user asks
62
+ about "assets on Spark" or "what can I trade on Spark".
63
+
64
+ **Spark holds:**
65
+ - **BTC** (sats) — Spark's native on-chain-pegged BTC.
66
+ - **Spark-native tokens** — e.g. **USDB**. These are tokens issued on the
67
+ Spark protocol itself, traded on **Flashnet** (Spark-native AMM).
68
+
69
+ **Spark does NOT hold:**
70
+ - **RGB assets** (USDT, XAUT, …). RGB assets are a DIFFERENT protocol on
71
+ Bitcoin/Lightning — they live on the user's **RLN** (RGB Lightning Node),
72
+ NOT Spark. A USDT balance, if the user has one, is on RLN — never on
73
+ Spark.
74
+ - Tokens from any other chain (Ethereum USDT, Tron USDT, Solana, …). The
75
+ wallet does not custody those at all.
76
+
77
+ **Asset → which skill / venue:**
78
+
79
+ | Asset | Layer | Swap venue | Skill |
80
+ |---|---|---|---|
81
+ | BTC / sats | Spark / RLN / on-chain | Either (depends on direction) | spark-wallet (Spark side), wallet-assistant |
82
+ | USDB (and other Spark tokens) | Spark | **Flashnet** (AMM) | **flashnet-swaps** |
83
+ | USDT, XAUT (RGB assets) | RLN/RGB | **KaleidoSwap maker** | **kaleido-trading** |
84
+
85
+ If the user asks "what can I trade on Spark?", the correct answer lists
86
+ Spark-native tokens (BTC + USDB and anything else
87
+ `flashnet_list_pools` shows). **Never** answer USDT/XAUT for Spark.
88
+ Conversely, if asked "what can I trade on KaleidoSwap?", that's RGB
89
+ assets (USDT, XAUT) — **not** USDB.
90
+
91
+ When in doubt about what's actually tradeable, the source of truth is the
92
+ TOOL, not your training data — call `flashnet_list_pools` (Spark side) or
93
+ `kaleidoswap_get_assets` (RGB side) and report what comes back.
94
+
95
+ ## Critical rules (read first)
96
+
97
+ 1. **Always re-fetch volatile state — every turn, every time.** Balance,
98
+ address, invoice status, and any number that can change MUST come from a
99
+ tool call THIS turn. Do NOT reuse a value from a previous turn, even if
100
+ the user asked the exact same question 30 seconds ago. The user wouldn't
101
+ ask twice if they didn't want a fresh check.
102
+
103
+ - "what's my balance?" → ALWAYS call `spark_get_balance`. Yes, even if
104
+ you just called it. The whole point of asking again is to get a new
105
+ reading.
106
+ - "give me my address" → ALWAYS call `spark_get_address`. Spark may
107
+ rotate or surface a fresh address.
108
+ - "did my invoice settle?" → ALWAYS re-fetch the invoice/order status.
109
+
110
+ The ONLY thing you can reuse from history is the user's own input
111
+ (e.g. "the invoice I just made" → look up its id in history and call
112
+ the status tool on it).
113
+
114
+ 2. **Never invent a balance, invoice or address.** Every BTC number, BOLT11
115
+ string, and address in your reply MUST come from a Spark tool result
116
+ returned in the CURRENT turn — never guessed, never quoted from memory.
117
+
118
+ 3. **Read the tool result exactly.** `spark_get_balance` returns
119
+ `{ total, layer, network, connected }`. `connected: true` means the
120
+ Spark wallet IS active and reachable; `total: 0` with `connected: true`
121
+ simply means the user has no sats yet (perfectly normal for a fresh
122
+ wallet on regtest). Do NOT say "your wallet isn't connected" unless
123
+ `connected: false` or the tool threw an error. Say "your Spark wallet
124
+ is connected but empty — fund it with `spark_get_address`" when
125
+ `total: 0, connected: true`.
126
+
127
+ 4. **Choose the right send tool by destination shape.**
128
+ - Starts with `lnbc…` / `lntb…` / `lnbcrt…` → BOLT11 Lightning invoice → use
129
+ **`spark_pay_invoice`**.
130
+ - Starts with `bc1…` / `tb1…` / `bcrt1…` → on-chain Bitcoin address → use
131
+ **`spark_send`**.
132
+ - Looks like `name@domain` (a Lightning address) → not a Spark target;
133
+ either ask the host to resolve it first or use the cross-cutting
134
+ `send_payment` router. Spark itself doesn't dereference LNURL.
135
+
136
+ 5. **BOLT11 invoices encode their amount.** Don't pass `amount_sats` to
137
+ `spark_pay_invoice` unless the invoice is amount-less. Re-stating the
138
+ amount can produce silently-wrong sends on amount-less invoices.
139
+
140
+ 6. **Confirm before spending.** `spark_pay_invoice` and `spark_send` are
141
+ confirmation-gated by the contract — the host fires the gate
142
+ automatically. Before the call, summarize in one line:
143
+ `Paying 12,540 sats to lnbc12540n… from Spark. Confirm?`
144
+
145
+ 7. **Spark genuinely unavailable = stop, don't guess.** If the tool
146
+ THROWS with "Your SPARK wallet isn't connected yet" (an actual error,
147
+ not a 0 balance), say so plainly and stop — don't substitute RLN or
148
+ Arkade silently. The user may genuinely want Spark.
149
+
150
+ 8. **Never refuse an action a listed tool performs.** If a tool in your
151
+ set does what the user asked, CALL IT — do not reason about whether
152
+ "get" means "create", whether the wording is an exact match, or
153
+ whether some other tool would be "more correct". The user asking to
154
+ "create an address" when you have `spark_get_address` means: call
155
+ `spark_get_address`. Refusing to use an available tool is always wrong.
156
+
157
+ ## How to call the tools
158
+
159
+ ### Reads
160
+
161
+ - **`spark_get_balance({})`** — current Spark balances. Returns the BTC
162
+ balance (`total` in sats) AND every Spark-native **token** the wallet
163
+ holds (`tokens[]` — each with `address`, `balance`, optional `symbol`
164
+ and `decimals`). When the user asks "what do I have on Spark" or
165
+ "what's my Spark balance", surface BOTH the sats AND any non-zero
166
+ token balances; an answer that only mentions BTC when tokens are
167
+ present is incomplete.
168
+ - `flashnet_get_balance` returns the same numbers (it's the AMM-client
169
+ view of the same wallet) — there's no need to call it just to learn
170
+ the token balance.
171
+ - **`spark_get_address({})`** — the user's **Spark identity**
172
+ (`sparkrt1…`/`spark1…`), an OFF-CHAIN Spark-to-Spark receive target.
173
+ Reusable — getting and creating are the same operation, so the right
174
+ response to "create a Spark address" is ALSO this tool. NEVER call its
175
+ result an on-chain address or a Bitcoin address; it is neither. NEVER
176
+ reply "I cannot create an address" — this tool IS how you create one.
177
+ - **`spark_get_onchain_address({})`** — a real Bitcoin **on-chain
178
+ deposit** address (`bc1…`/`tb1…`/`bcrt1…`) for funding Spark from L1.
179
+ Use whenever the user says "on-chain", "bitcoin address", "deposit
180
+ address", "fund Spark", or otherwise indicates they want to send L1
181
+ BTC. NEVER substitute `spark_get_address` here.
182
+
183
+ ### Receive
184
+
185
+ - **`spark_create_invoice({ amount_sats? })`** — Spark Lightning invoice.
186
+ - Omit `amount_sats` for an "any amount" invoice.
187
+ - Returns `{ invoice: "lnbc…", … }`.
188
+ - This is the tool for "give me an invoice for N sats", NOT for any
189
+ "address" ask.
190
+
191
+ ### Send — pick by destination
192
+
193
+ - **`spark_pay_invoice({ invoice, amount_sats? })`** — pay a BOLT11 invoice.
194
+ - The model's default Lightning spend tool. Use this for invoices from
195
+ Bitrefill, a contact's invoice paste, or any `ln…` string.
196
+ - Pass `amount_sats` ONLY when the invoice is amount-less (a 0-amount
197
+ invoice). For ordinary amount-bound invoices, OMIT it.
198
+ - **`spark_send({ amount_sats, to })`** — on-chain Bitcoin send.
199
+ - Use only when `to` is an on-chain address (`bc1…`). Never pass a BOLT11
200
+ invoice here.
201
+
202
+ ### Helpers
203
+
204
+ - **`get_price({ fiat? })`** / **`fiat_to_sats({ amount, currency })`** —
205
+ for "how many sats is €10" style sub-questions before a Spark spend.
206
+
207
+ ## Cross-skill flow with Bitrefill
208
+
209
+ When the user wants to buy something from Bitrefill and pay with Spark, the
210
+ typical chain is:
211
+
212
+ 1. Call `bitrefill_search` / `bitrefill_get_product` to confirm the product +
213
+ the right `package_id` (see the `bitrefill` skill).
214
+ 2. Call `bitrefill_create_invoice({ products, payment_method: "lightning",
215
+ refund_address: <a Spark or on-chain address> })` — Bitrefill returns a
216
+ BOLT11 invoice on the response under `payment.lightning_invoice` (or
217
+ similar — relay whatever the host surfaces).
218
+ 3. **Pay with Spark**: `spark_pay_invoice({ invoice: <that BOLT11> })`. One
219
+ confirmation gate; Spark settles the invoice in seconds.
220
+ 4. Poll `bitrefill_get_invoice` until `status:"complete"`, then
221
+ `bitrefill_get_order` for the redemption code.
222
+
223
+ If the user pre-funded a Bitrefill account, prefer
224
+ `payment_method:"balance"` instead — no Spark spend, instant settlement. Use
225
+ the Lightning path when the user explicitly says "pay with Spark/Lightning"
226
+ or has no Bitrefill balance.
227
+
228
+ ## Reply style
229
+
230
+ - One short sentence per fact ("Spark holds 124,500 sats.").
231
+ - For invoices: show the invoice on its own line so the user can copy it.
232
+ - For sends: one-line pre-spend summary (amount + destination + "from
233
+ Spark"), then the result.
234
+ - When `spark_get_balance` says zero and the user asked to spend, stop and
235
+ say so — don't try to source funds from another layer silently.
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: wallet-assistant
3
3
  description: Everyday wallet tasks on this phone — check the BTC/asset balance, create an invoice to receive, send a payment, look up a contact, or quote a swap (e.g. "how many sats is 10 USDT?"). Triggers when the user asks about their balance, wants to receive or send money, pay an invoice, pay a contact, or convert between BTC and supported assets.
4
- tools: get_balances, resolve_contact, send_payment, rln_pay_invoice, rln_create_ln_invoice, spark_create_invoice, kaleidoswap_get_quote
5
- triggers: balance, pay, send, receive, address, invoice, transactions, contact, funds, money, sats
4
+ tools: get_balances, rln_get_balances, wdk_get_balances, spark_get_balance, rln_get_asset_balance, wdk_get_asset_balance, rln_list_assets, wdk_list_assets, rln_get_address, wdk_get_address, spark_get_address, spark_get_onchain_address, resolve_contact, send_payment, rln_send_btc, wdk_send_btc, rln_send_asset, wdk_send_asset, spark_send, spark_send_sats, rln_pay_invoice, wdk_pay_invoice, spark_pay_invoice, spark_pay_lightning_invoice, rln_create_ln_invoice, wdk_create_ln_invoice, spark_create_invoice, spark_create_lightning_invoice, rln_create_rgb_invoice, wdk_create_rgb_invoice, rln_list_payments, wdk_list_payments, kaleidoswap_get_quote
5
+ triggers: balance, pay, send, receive, address, invoice, transactions, contact, funds, money
6
6
  ---
7
7
 
8
8
  # Wallet assistant
@@ -0,0 +1,348 @@
1
+ /** Autonomy tests — deterministic: injected clock + timer, no real wallet/LLM. */
2
+
3
+ import { describe, it, expect, vi } from 'vitest';
4
+ import { InMemoryTaskStore, defaultTaskSeeds } from './task-store.js';
5
+ import { createTaskScheduler } from './scheduler.js';
6
+ import { TaskRunLog } from './run-state.js';
7
+ import { evaluateSpend, DEFAULT_RISK_LIMITS } from './risk.js';
8
+ import { buildTaskPrompt } from './prompt.js';
9
+ import type { AgentTask, RunLogSnapshot, RunLogIO, TaskStoreIO } from './types.js';
10
+
11
+ const SEC = 1000;
12
+
13
+ /** Drain pending microtasks (real timer — these tests don't fake setTimeout). */
14
+ const flush = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 0));
15
+
16
+ describe('InMemoryTaskStore', () => {
17
+ it('creates with defaults and lists', async () => {
18
+ const store = new InMemoryTaskStore({ now: () => 1 });
19
+ const t = await store.create({
20
+ name: 'Rebalance',
21
+ description: 'd',
22
+ skill: 'portfolio-manager',
23
+ scheduleSec: 3600,
24
+ enabled: true,
25
+ });
26
+ expect(t.id).toBe('task_1_1');
27
+ expect(t.runOnStartup).toBe(false);
28
+ expect(t.allocation).toEqual({ btcSat: 0, usdt: 0, xaut: 0 });
29
+ expect(t.lastRunAt).toBeNull();
30
+ expect(await store.list()).toHaveLength(1);
31
+ });
32
+
33
+ it('update cannot mutate id/createdAt; remove works', async () => {
34
+ const store = new InMemoryTaskStore({ now: () => 5 });
35
+ const t = await store.create({ name: 'x', description: '', skill: 's', scheduleSec: 0, enabled: false });
36
+ const patched = await store.update(t.id, {
37
+ enabled: true,
38
+ lastRunAt: 99,
39
+ // @ts-expect-error — id is not patchable, proving the type guard
40
+ id: 'evil',
41
+ } as Partial<AgentTask>);
42
+ expect(patched?.id).toBe(t.id);
43
+ expect(patched?.createdAt).toBe(5);
44
+ expect(patched?.enabled).toBe(true);
45
+ expect(patched?.lastRunAt).toBe(99);
46
+ expect(await store.remove(t.id)).toBe(true);
47
+ expect(await store.remove(t.id)).toBe(false);
48
+ });
49
+
50
+ it('seedDefaults is idempotent by id', async () => {
51
+ const store = new InMemoryTaskStore({ now: () => 1 });
52
+ const first = await store.seedDefaults(defaultTaskSeeds());
53
+ expect(first.map((t) => t.id)).toEqual(['heartbeat', 'rebalance', 'daily_summary']);
54
+ const second = await store.seedDefaults(defaultTaskSeeds());
55
+ expect(second).toHaveLength(0); // already present
56
+ expect(await store.list()).toHaveLength(3);
57
+ // seeds disabled by default — an agent never auto-arms itself
58
+ expect((await store.list()).every((t) => !t.enabled)).toBe(true);
59
+ });
60
+
61
+ it('persists through injected IO and a fresh store hydrates', async () => {
62
+ let saved: AgentTask[] = [];
63
+ const io: TaskStoreIO = {
64
+ load: vi.fn(async () => [...saved]),
65
+ save: vi.fn(async (tasks) => {
66
+ saved = [...tasks];
67
+ }),
68
+ };
69
+ const store = new InMemoryTaskStore({ io, now: () => 1 });
70
+ await store.create({ name: 'a', description: '', skill: 's', scheduleSec: 60, enabled: true });
71
+ expect(io.save).toHaveBeenCalled();
72
+
73
+ const store2 = new InMemoryTaskStore({ io, now: () => 2 });
74
+ expect(await store2.list()).toHaveLength(1);
75
+ });
76
+ });
77
+
78
+ describe('createTaskScheduler', () => {
79
+ function fixtureTask(over: Partial<AgentTask> = {}): AgentTask {
80
+ return {
81
+ id: 't1',
82
+ name: 'Heartbeat',
83
+ description: '',
84
+ skill: 'channel-manager',
85
+ scheduleSec: 300,
86
+ runOnStartup: false,
87
+ allocation: { btcSat: 0, usdt: 0, xaut: 0 },
88
+ enabled: true,
89
+ createdAt: 0,
90
+ lastRunAt: null,
91
+ ...over,
92
+ };
93
+ }
94
+
95
+ it('runs a task only once its interval has elapsed since creation', async () => {
96
+ let t = 0;
97
+ const store = new InMemoryTaskStore({ now: () => t });
98
+ await store.create(fixtureTask());
99
+ const run = vi.fn(async () => ({ ok: true }));
100
+ const sched = createTaskScheduler({ store, run, now: () => t });
101
+
102
+ t = 200 * SEC; // < 300s since createdAt=0 → not due
103
+ await sched.tick();
104
+ expect(run).not.toHaveBeenCalled();
105
+
106
+ t = 300 * SEC; // exactly due
107
+ await sched.tick();
108
+ expect(run).toHaveBeenCalledTimes(1);
109
+ // lastRunAt stamped at run start
110
+ expect((await store.get('t1'))?.lastRunAt).toBe(300 * SEC);
111
+
112
+ t = 400 * SEC; // < 300s since last run → not due again
113
+ await sched.tick();
114
+ expect(run).toHaveBeenCalledTimes(1);
115
+
116
+ t = 600 * SEC; // 300s elapsed → due
117
+ await sched.tick();
118
+ expect(run).toHaveBeenCalledTimes(2);
119
+ });
120
+
121
+ it('skips disabled tasks and scheduleSec=0 (manual-only)', async () => {
122
+ const store = new InMemoryTaskStore({ now: () => 0 });
123
+ await store.create(fixtureTask({ id: 'off', enabled: false, createdAt: 0 }));
124
+ await store.create(fixtureTask({ id: 'manual', scheduleSec: 0, createdAt: 0 }));
125
+ const run = vi.fn(async () => ({ ok: true }));
126
+ const sched = createTaskScheduler({ store, run, now: () => 10 ** 9 });
127
+ await sched.tick();
128
+ expect(run).not.toHaveBeenCalled();
129
+ });
130
+
131
+ it('start() runs runOnStartup tasks immediately via injected timer', async () => {
132
+ const store = new InMemoryTaskStore({ now: () => 0 });
133
+ await store.create(fixtureTask({ id: 'boot', runOnStartup: true, createdAt: 0 }));
134
+ const run = vi.fn(async () => ({ ok: true }));
135
+ const setTimer = vi.fn(() => 'h');
136
+ const clearTimer = vi.fn();
137
+ const sched = createTaskScheduler({ store, run, now: () => 0, setTimer, clearTimer });
138
+
139
+ sched.start();
140
+ expect(setTimer).toHaveBeenCalledTimes(1);
141
+ await flush(); // let the async startup pass settle
142
+ expect(run).toHaveBeenCalledTimes(1);
143
+ expect(sched.isRunning()).toBe(true);
144
+
145
+ sched.stop();
146
+ expect(clearTimer).toHaveBeenCalledWith('h');
147
+ expect(sched.isRunning()).toBe(false);
148
+ });
149
+
150
+ it('runNow forces a run regardless of schedule/enabled and reports outcome', async () => {
151
+ const store = new InMemoryTaskStore({ now: () => 0 });
152
+ await store.create(fixtureTask({ id: 'x', enabled: false, scheduleSec: 0 }));
153
+ const run = vi.fn(async () => ({ ok: true, text: 'done', toolCalls: 2 }));
154
+ const sched = createTaskScheduler({ store, run, now: () => 42 });
155
+ const outcome = await sched.runNow('x');
156
+ expect(outcome).toEqual({ ok: true, text: 'done', toolCalls: 2 });
157
+ expect((await store.get('x'))?.lastRunAt).toBe(42);
158
+ expect(await sched.runNow('nope')).toBeNull();
159
+ });
160
+
161
+ it('advances lastRunAt even when the run throws (no hot-loop)', async () => {
162
+ let t = 10 ** 6;
163
+ const store = new InMemoryTaskStore({ now: () => t });
164
+ await store.create(fixtureTask({ id: 'boom', createdAt: 0 }));
165
+ const onOutcome = vi.fn();
166
+ const run = vi.fn(async () => {
167
+ throw new Error('rpc down');
168
+ });
169
+ const sched = createTaskScheduler({ store, run, now: () => t, onOutcome });
170
+ await sched.tick();
171
+ expect((await store.get('boom'))?.lastRunAt).toBe(t);
172
+ expect(onOutcome).toHaveBeenCalledWith(
173
+ expect.objectContaining({ id: 'boom' }),
174
+ expect.objectContaining({ ok: false, error: 'rpc down' }),
175
+ expect.any(Number),
176
+ );
177
+ });
178
+
179
+ it('honors concurrency=1 — a slow task does not overlap itself', async () => {
180
+ let t = 10 ** 6;
181
+ const store = new InMemoryTaskStore({ now: () => t });
182
+ await store.create(fixtureTask({ id: 'a', createdAt: 0 }));
183
+ await store.create(fixtureTask({ id: 'b', createdAt: 0 }));
184
+ let resolveA: (() => void) | null = null;
185
+ const run = vi.fn((task: AgentTask) =>
186
+ task.id === 'a'
187
+ ? new Promise<{ ok: boolean }>((res) => {
188
+ resolveA = () => res({ ok: true });
189
+ })
190
+ : Promise.resolve({ ok: true }),
191
+ );
192
+ const sched = createTaskScheduler({ store, run, now: () => t, concurrency: 1 });
193
+ void sched.tick();
194
+ await flush();
195
+ // a is in-flight (its run never resolves); concurrency 1 holds b off this tick
196
+ expect(sched.active()).toEqual(['a']);
197
+ expect(run).toHaveBeenCalledTimes(1);
198
+ resolveA?.();
199
+ });
200
+ });
201
+
202
+ describe('TaskRunLog', () => {
203
+ it('aggregates stats, recent runs, and cumulative cost', async () => {
204
+ const log = new TaskRunLog({ now: () => 1, maxRecent: 2 });
205
+ await log.record({
206
+ taskId: 'r', taskName: 'Rebalance', startedAt: 100, durationMs: 5, toolCalls: 3,
207
+ ok: true, error: null, text: 'ok', cost: { usd: 0.01, inputTokens: 10, outputTokens: 5 },
208
+ });
209
+ await log.record({
210
+ taskId: 'r', taskName: 'Rebalance', startedAt: 200, durationMs: 7, toolCalls: 1,
211
+ ok: false, error: 'boom', text: '', cost: { usd: 0.02, inputTokens: 4, outputTokens: 2 },
212
+ });
213
+ const s = await log.statsFor('r');
214
+ expect(s?.runs).toBe(2);
215
+ expect(s?.errors).toBe(1);
216
+ expect(s?.lastError).toBe('boom');
217
+ expect(await log.totalCost()).toEqual({ usd: 0.03, inputTokens: 14, outputTokens: 7 });
218
+ expect((await log.recent()).map((r) => r.startedAt)).toEqual([200, 100]); // newest first
219
+ });
220
+
221
+ it('caps the recent ring buffer at maxRecent', async () => {
222
+ const log = new TaskRunLog({ now: () => 1, maxRecent: 2 });
223
+ for (let i = 0; i < 5; i++) {
224
+ await log.record({
225
+ taskId: 't', taskName: 'T', startedAt: i, durationMs: 1, toolCalls: 0,
226
+ ok: true, error: null, text: '', cost: { usd: 0, inputTokens: 0, outputTokens: 0 },
227
+ });
228
+ }
229
+ expect(await log.recent()).toHaveLength(2);
230
+ });
231
+
232
+ it('persists + hydrates through injected IO', async () => {
233
+ let snap: RunLogSnapshot | null = null;
234
+ const io: RunLogIO = {
235
+ load: vi.fn(async () => snap),
236
+ save: vi.fn(async (s) => {
237
+ snap = s;
238
+ }),
239
+ };
240
+ const log = new TaskRunLog({ io, now: () => 1 });
241
+ await log.record({
242
+ taskId: 't', taskName: 'T', startedAt: 1, durationMs: 1, toolCalls: 0,
243
+ ok: true, error: null, text: 'hello', cost: { usd: 1, inputTokens: 0, outputTokens: 0 },
244
+ });
245
+ expect(io.save).toHaveBeenCalled();
246
+ const log2 = new TaskRunLog({ io, now: () => 2 });
247
+ expect((await log2.totalCost()).usd).toBe(1);
248
+ });
249
+ });
250
+
251
+ describe('evaluateSpend (risk guardrails)', () => {
252
+ const live = { ...DEFAULT_RISK_LIMITS, dryRun: false };
253
+
254
+ it('blocks every spend when dry-run is on', () => {
255
+ const v = evaluateSpend({ kind: 'swap', amountUsd: 1 }, DEFAULT_RISK_LIMITS);
256
+ expect(v.outcome).toBe('block');
257
+ expect(v.reason).toMatch(/dry-run/);
258
+ });
259
+
260
+ it('blocks at/below the stop-loss floor', () => {
261
+ const v = evaluateSpend(
262
+ { kind: 'pay', amountSat: 1 },
263
+ { ...live, stopLossBtcSat: 50_000 },
264
+ { btcBalanceSat: 50_000 },
265
+ );
266
+ expect(v.outcome).toBe('block');
267
+ expect(v.reason).toMatch(/stop-loss/);
268
+ });
269
+
270
+ it('blocks a spend that would dip below the reserve', () => {
271
+ const v = evaluateSpend(
272
+ { kind: 'send', amountSat: 60_000 },
273
+ { ...live, minBtcReserveSat: 50_000, stopLossBtcSat: 0 },
274
+ { btcBalanceSat: 100_000 },
275
+ );
276
+ expect(v.outcome).toBe('block');
277
+ expect(v.reason).toMatch(/reserve/);
278
+ });
279
+
280
+ it('blocks above the max single spend', () => {
281
+ const v = evaluateSpend({ kind: 'swap', amountUsd: 100 }, { ...live, maxSpendUsd: 50 });
282
+ expect(v.outcome).toBe('block');
283
+ expect(v.reason).toMatch(/exceeds/);
284
+ });
285
+
286
+ it('blocks a new swap when the open-order cap is reached', () => {
287
+ const v = evaluateSpend(
288
+ { kind: 'swap', amountUsd: 1 },
289
+ { ...live, maxOpenOrders: 3, autoApproveUnderUsd: 100 },
290
+ { openOrders: 3 },
291
+ );
292
+ expect(v.outcome).toBe('block');
293
+ expect(v.reason).toMatch(/open orders/);
294
+ });
295
+
296
+ it('auto-approves a small spend under the threshold', () => {
297
+ const v = evaluateSpend(
298
+ { kind: 'pay', amountUsd: 2, amountSat: 3000 },
299
+ { ...live, autoApproveUnderUsd: 5, maxSpendUsd: 50, minBtcReserveSat: 0, stopLossBtcSat: 0 },
300
+ { btcBalanceSat: 1_000_000 },
301
+ );
302
+ expect(v.outcome).toBe('allow');
303
+ expect(v.requiresConfirmation).toBe(false);
304
+ });
305
+
306
+ it('requires confirmation above the auto-approve threshold', () => {
307
+ const v = evaluateSpend(
308
+ { kind: 'swap', amountUsd: 20 },
309
+ { ...live, autoApproveUnderUsd: 5, maxSpendUsd: 50 },
310
+ );
311
+ expect(v.outcome).toBe('confirm');
312
+ expect(v.requiresConfirmation).toBe(true);
313
+ });
314
+
315
+ it('requires confirmation when the USD value is unknown (never spends blind)', () => {
316
+ const v = evaluateSpend({ kind: 'channel' }, { ...live, autoApproveUnderUsd: 100 });
317
+ expect(v.outcome).toBe('confirm');
318
+ });
319
+ });
320
+
321
+ describe('buildTaskPrompt', () => {
322
+ const task: AgentTask = {
323
+ id: 'rebalance',
324
+ name: 'Portfolio Rebalance',
325
+ description: 'detect drift',
326
+ skill: 'portfolio-manager',
327
+ scheduleSec: 3600,
328
+ runOnStartup: false,
329
+ allocation: { btcSat: 100, usdt: 5, xaut: 0 },
330
+ enabled: true,
331
+ createdAt: 0,
332
+ lastRunAt: null,
333
+ };
334
+
335
+ it('embeds skill, dry-run flag, allocation, and the strict-JSON contract', () => {
336
+ const p = buildTaskPrompt(task, { dryRun: true, nowIso: '2026-06-19T00:00:00.000Z' });
337
+ expect(p).toMatch(/portfolio-manager/);
338
+ expect(p).toMatch(/dry_run: true/);
339
+ expect(p).toMatch(/Do NOT pay, send, swap/);
340
+ expect(p).toMatch(/"task":"rebalance"/);
341
+ expect(p).toMatch(/"btcSat":100/);
342
+ });
343
+
344
+ it('switches guidance to fund-safety language when live', () => {
345
+ const p = buildTaskPrompt(task, { dryRun: false, nowIso: '2026-06-19T00:00:00.000Z' });
346
+ expect(p).toMatch(/reserve or stop-loss/);
347
+ });
348
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Autonomy — the agent's task brain: a registry of scheduled tasks (TaskStore),
3
+ * the record of what they did (TaskRunLog), an interval engine that fires them
4
+ * (createTaskScheduler), and enforced spend guardrails (evaluateSpend).
5
+ *
6
+ * This is the half of the agent's memory the MemoryStore (soul + facts) doesn't
7
+ * cover — the operational state nanobot kept in tasks.json + cron + run history.
8
+ * Storage and timers are injected; the logic is pure TS.
9
+ */
10
+
11
+ export type {
12
+ TaskAllocation,
13
+ AgentTask,
14
+ NewTask,
15
+ TaskSeed,
16
+ TaskStore,
17
+ TaskStoreIO,
18
+ TaskRunCost,
19
+ TaskStats,
20
+ TaskRunRecord,
21
+ RunLogSnapshot,
22
+ RunLogIO,
23
+ TaskRunOutcome,
24
+ RunTask,
25
+ TimerHandle,
26
+ SchedulerOptions,
27
+ TaskScheduler,
28
+ } from './types.js';
29
+ export { ZERO_ALLOCATION } from './types.js';
30
+
31
+ export { InMemoryTaskStore, defaultTaskSeeds } from './task-store.js';
32
+ export type { TaskStoreOptions } from './task-store.js';
33
+
34
+ export { TaskRunLog } from './run-state.js';
35
+ export type { RunLogOptions } from './run-state.js';
36
+
37
+ export { createTaskScheduler } from './scheduler.js';
38
+
39
+ export { evaluateSpend, DEFAULT_RISK_LIMITS } from './risk.js';
40
+ export type {
41
+ SpendKind,
42
+ RiskLimits,
43
+ SpendAction,
44
+ RiskContext,
45
+ RiskOutcome,
46
+ RiskVerdict,
47
+ } from './risk.js';
48
+
49
+ export { buildTaskPrompt } from './prompt.js';
50
+ export type { TaskPromptOptions } from './prompt.js';