@kaleidorg/mind 0.6.0 → 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.
- package/dist/bitrefill/contract.d.ts +60 -0
- package/dist/bitrefill/contract.d.ts.map +1 -0
- package/dist/bitrefill/contract.js +119 -0
- package/dist/bitrefill/contract.js.map +1 -0
- package/dist/context/compress.d.ts +65 -0
- package/dist/context/compress.d.ts.map +1 -0
- package/dist/context/compress.js +181 -0
- package/dist/context/compress.js.map +1 -0
- package/dist/engine.d.ts +20 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +23 -4
- package/dist/engine.js.map +1 -1
- package/dist/evidence.d.ts +62 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +47 -0
- package/dist/evidence.js.map +1 -0
- package/dist/flashnet/contract.d.ts +56 -0
- package/dist/flashnet/contract.d.ts.map +1 -0
- package/dist/flashnet/contract.js +100 -0
- package/dist/flashnet/contract.js.map +1 -0
- package/dist/funnel.d.ts +11 -0
- package/dist/funnel.d.ts.map +1 -1
- package/dist/funnel.js +50 -7
- package/dist/funnel.js.map +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/kaleidoswap/contract.js +1 -1
- package/dist/kaleidoswap/contract.js.map +1 -1
- package/dist/knowledge/bitcoin-copilot.d.ts.map +1 -1
- package/dist/knowledge/bitcoin-copilot.js +83 -0
- package/dist/knowledge/bitcoin-copilot.js.map +1 -1
- package/dist/providers/types.d.ts +17 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/qvac/provider.d.ts.map +1 -1
- package/dist/qvac/provider.js +23 -0
- package/dist/qvac/provider.js.map +1 -1
- package/dist/qvac/stream.d.ts +6 -0
- package/dist/qvac/stream.d.ts.map +1 -1
- package/dist/qvac/stream.js +12 -0
- package/dist/qvac/stream.js.map +1 -1
- package/dist/recipe/flashnet-swap.d.ts +35 -0
- package/dist/recipe/flashnet-swap.d.ts.map +1 -0
- package/dist/recipe/flashnet-swap.js +239 -0
- package/dist/recipe/flashnet-swap.js.map +1 -0
- package/dist/recipe/kaleidoswap-atomic.d.ts.map +1 -1
- package/dist/recipe/kaleidoswap-atomic.js +37 -16
- package/dist/recipe/kaleidoswap-atomic.js.map +1 -1
- package/dist/recipe/kaleidoswap-channel-order.d.ts.map +1 -1
- package/dist/recipe/kaleidoswap-channel-order.js +31 -10
- package/dist/recipe/kaleidoswap-channel-order.js.map +1 -1
- package/dist/recipe/kaleidoswap-price.d.ts.map +1 -1
- package/dist/recipe/kaleidoswap-price.js +7 -1
- package/dist/recipe/kaleidoswap-price.js.map +1 -1
- package/dist/recipe/runner.d.ts.map +1 -1
- package/dist/recipe/runner.js +5 -3
- package/dist/recipe/runner.js.map +1 -1
- package/dist/recipe/swap.d.ts.map +1 -1
- package/dist/recipe/swap.js +14 -1
- package/dist/recipe/swap.js.map +1 -1
- package/dist/wallet/confirm.d.ts.map +1 -1
- package/dist/wallet/confirm.js +1 -0
- package/dist/wallet/confirm.js.map +1 -1
- package/dist/wallet/contract.d.ts.map +1 -1
- package/dist/wallet/contract.js +20 -4
- package/dist/wallet/contract.js.map +1 -1
- package/package.json +4 -4
- package/skills/bitrefill/SKILL.md +152 -52
- package/skills/flashnet-swaps/SKILL.md +158 -0
- package/skills/kaleido-lsps/SKILL.md +25 -8
- package/skills/kaleido-trading/SKILL.md +36 -12
- package/skills/merchant-finder/SKILL.md +1 -1
- package/skills/rgb-lightning-node/SKILL.md +35 -8
- package/skills/spark-wallet/SKILL.md +235 -0
- package/skills/wallet-assistant/SKILL.md +2 -2
- package/src/bitrefill/contract.test.ts +89 -0
- package/src/bitrefill/contract.ts +190 -0
- package/src/context/compress.test.ts +120 -0
- package/src/context/compress.ts +230 -0
- package/src/engine.test.ts +34 -0
- package/src/engine.ts +35 -4
- package/src/evidence.test.ts +80 -0
- package/src/evidence.ts +114 -0
- package/src/flashnet/contract.test.ts +101 -0
- package/src/flashnet/contract.ts +164 -0
- package/src/funnel.ts +59 -8
- package/src/index.ts +51 -1
- package/src/kaleidoswap/contract.ts +1 -1
- package/src/knowledge/bitcoin-copilot.ts +94 -0
- package/src/providers/types.ts +18 -0
- package/src/qvac/provider.ts +25 -1
- package/src/qvac/stream.test.ts +11 -0
- package/src/qvac/stream.ts +16 -0
- package/src/recipe/flashnet-swap.test.ts +114 -0
- package/src/recipe/flashnet-swap.ts +266 -0
- package/src/recipe/kaleidoswap-atomic.test.ts +21 -0
- package/src/recipe/kaleidoswap-atomic.ts +34 -16
- package/src/recipe/kaleidoswap-channel-order.test.ts +38 -0
- package/src/recipe/kaleidoswap-channel-order.ts +27 -9
- package/src/recipe/kaleidoswap-price.ts +7 -1
- package/src/recipe/recipe.test.ts +5 -0
- package/src/recipe/runner.ts +5 -3
- package/src/recipe/swap.ts +16 -1
- package/src/wallet/confirm.test.ts +8 -0
- package/src/wallet/confirm.ts +1 -0
- package/src/wallet/contract.test.ts +10 -0
- 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, 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, resolve_contact, send_payment, rln_send_btc, wdk_send_btc, rln_send_asset, wdk_send_asset, spark_send_sats, rln_pay_invoice, wdk_pay_invoice, spark_pay_lightning_invoice, rln_create_ln_invoice, wdk_create_ln_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
|
|
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,89 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
BITREFILL_TOOLS,
|
|
4
|
+
BITREFILL_SPEND_TOOLS,
|
|
5
|
+
isBitrefillSpendTool,
|
|
6
|
+
getBitrefillTool,
|
|
7
|
+
bindBitrefillTools,
|
|
8
|
+
type BitrefillHandler,
|
|
9
|
+
} from './contract.js';
|
|
10
|
+
|
|
11
|
+
describe('BITREFILL_TOOLS — shape invariants', () => {
|
|
12
|
+
it('exposes the expected tool names in order', () => {
|
|
13
|
+
expect(BITREFILL_TOOLS.map((t) => t.name)).toEqual([
|
|
14
|
+
'bitrefill_search',
|
|
15
|
+
'bitrefill_get_product',
|
|
16
|
+
'bitrefill_get_balance',
|
|
17
|
+
'bitrefill_create_invoice',
|
|
18
|
+
'bitrefill_get_invoice',
|
|
19
|
+
'bitrefill_get_order',
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('every tool has an object parameters schema', () => {
|
|
24
|
+
for (const t of BITREFILL_TOOLS) {
|
|
25
|
+
expect((t.parameters as any)?.type).toBe('object');
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('aligns spend ↔ requiresConfirmation', () => {
|
|
30
|
+
for (const t of BITREFILL_TOOLS) {
|
|
31
|
+
expect(!!t.spend).toBe(!!t.requiresConfirmation);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('marks only bitrefill_create_invoice as spend', () => {
|
|
36
|
+
expect([...BITREFILL_SPEND_TOOLS]).toEqual(['bitrefill_create_invoice']);
|
|
37
|
+
expect(isBitrefillSpendTool('bitrefill_create_invoice')).toBe(true);
|
|
38
|
+
expect(isBitrefillSpendTool('bitrefill_search')).toBe(false);
|
|
39
|
+
expect(isBitrefillSpendTool('bitrefill_get_balance')).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('getBitrefillTool returns by name', () => {
|
|
43
|
+
expect(getBitrefillTool('bitrefill_get_product')?.name).toBe('bitrefill_get_product');
|
|
44
|
+
expect(getBitrefillTool('nope')).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('create_invoice requires products + payment_method', () => {
|
|
48
|
+
const def = getBitrefillTool('bitrefill_create_invoice')!;
|
|
49
|
+
expect((def.parameters as any).required).toEqual(['products', 'payment_method']);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('bindBitrefillTools', () => {
|
|
54
|
+
const echoHandlers = (): Record<string, BitrefillHandler> => ({
|
|
55
|
+
bitrefill_search: async (a) => ({ ok: true, t: 'search', args: a }),
|
|
56
|
+
bitrefill_get_product: async (a) => ({ ok: true, t: 'get_product', args: a }),
|
|
57
|
+
bitrefill_get_balance: async () => ({ balance: 100, currency: 'USD' }),
|
|
58
|
+
bitrefill_create_invoice: async (a) => ({ ok: true, t: 'create_invoice', args: a }),
|
|
59
|
+
bitrefill_get_invoice: async (a) => ({ ok: true, t: 'get_invoice', args: a }),
|
|
60
|
+
bitrefill_get_order: async (a) => ({ ok: true, t: 'get_order', args: a }),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('binds every tool and preserves the spend gate', () => {
|
|
64
|
+
const src = bindBitrefillTools(echoHandlers());
|
|
65
|
+
expect(src.listTools().length).toBe(6);
|
|
66
|
+
const create = src.listTools().find((t) => t.name === 'bitrefill_create_invoice');
|
|
67
|
+
expect(create?.requiresConfirmation).toBe(true);
|
|
68
|
+
const search = src.listTools().find((t) => t.name === 'bitrefill_search');
|
|
69
|
+
expect(search?.requiresConfirmation).toBeFalsy();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('dispatches with args', async () => {
|
|
73
|
+
const src = bindBitrefillTools(echoHandlers());
|
|
74
|
+
const r = await src.execute('bitrefill_search', { query: 'amazon', country: 'US' });
|
|
75
|
+
expect(r).toMatchObject({ ok: true, t: 'search', args: { query: 'amazon', country: 'US' } });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('throws on a missing handler unless allowMissing', () => {
|
|
79
|
+
const partial = { bitrefill_search: echoHandlers().bitrefill_search };
|
|
80
|
+
expect(() => bindBitrefillTools(partial)).toThrow(/no handler/);
|
|
81
|
+
const src = bindBitrefillTools(partial, { allowMissing: true });
|
|
82
|
+
expect(src.listTools().map((t) => t.name)).toEqual(['bitrefill_search']);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('uses opts.id for the ToolSource id', () => {
|
|
86
|
+
const src = bindBitrefillTools(echoHandlers(), { id: 'bitrefill-personal' });
|
|
87
|
+
expect(src.id).toBe('bitrefill-personal');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Bitrefill tool contract — gift cards, mobile top-ups, eSIMs.
|
|
3
|
+
*
|
|
4
|
+
* Same pattern as the LSPS1 contract: the tool *names + schemas* live here so
|
|
5
|
+
* every host (CLI REST adapter, desktop MCP, mobile WDK adapter) exposes the
|
|
6
|
+
* exact same surface to the agent. Only the transport differs.
|
|
7
|
+
*
|
|
8
|
+
* - CLI / desktop server → REST against `https://api.bitrefill.com/v2`
|
|
9
|
+
* (see `apps/cli/src/bitrefillTools.ts`).
|
|
10
|
+
* - Desktop sidecar → the remote MCP at `api.bitrefill.com/mcp` already
|
|
11
|
+
* exposes equivalent tools under different names; a binder there can
|
|
12
|
+
* rename them to this contract for parity.
|
|
13
|
+
*
|
|
14
|
+
* `bitrefill_create_invoice` is the spend — confirmation-gated by the contract.
|
|
15
|
+
* Everything else is read-only (search, product details, balance, invoice/order
|
|
16
|
+
* status). Invoice creation supports `payment_method:"balance"` (instant, pulls
|
|
17
|
+
* from pre-funded account) or `lightning|bitcoin|usdc_base|...` (the response
|
|
18
|
+
* carries a payment URI/invoice; the user pays out-of-band, then the order is
|
|
19
|
+
* fulfilled).
|
|
20
|
+
*
|
|
21
|
+
* Pure data — no deps, no fetch, RN-safe.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { ToolDef } from '../types.js';
|
|
25
|
+
import { InProcessToolSource } from '../tools/in-process.js';
|
|
26
|
+
import type { InProcessTool } from '../tools/in-process.js';
|
|
27
|
+
|
|
28
|
+
export interface BitrefillToolDef extends ToolDef {
|
|
29
|
+
/** Moves real money → confirmation-gated. */
|
|
30
|
+
spend?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Props = Record<
|
|
34
|
+
string,
|
|
35
|
+
{ type: string; description?: string; enum?: string[]; items?: unknown }
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
function t(
|
|
39
|
+
name: string,
|
|
40
|
+
description: string,
|
|
41
|
+
properties: Props = {},
|
|
42
|
+
required: string[] = [],
|
|
43
|
+
spend = false,
|
|
44
|
+
): BitrefillToolDef {
|
|
45
|
+
return {
|
|
46
|
+
name,
|
|
47
|
+
description,
|
|
48
|
+
spend,
|
|
49
|
+
requiresConfirmation: spend,
|
|
50
|
+
parameters: { type: 'object', properties, required },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The canonical Bitrefill tool list. Each host's binder translates these
|
|
56
|
+
* args into the Bitrefill REST body (CLI) or MCP/CLI call (other hosts).
|
|
57
|
+
*/
|
|
58
|
+
export const BITREFILL_TOOLS: BitrefillToolDef[] = [
|
|
59
|
+
t(
|
|
60
|
+
'bitrefill_search',
|
|
61
|
+
"Search Bitrefill's product catalog by keyword (brand, country, type). Returns up to ~20 matches with `id`, `name`, `country`, `category` and `denominations`. The model picks the right product id and then calls `bitrefill_get_product` for the package list.",
|
|
62
|
+
{
|
|
63
|
+
query: { type: 'string', description: 'Search keyword. e.g. "amazon", "steam", "vodafone uk", "esim europe".' },
|
|
64
|
+
country: { type: 'string', description: 'OPTIONAL — ISO country code to scope results (e.g. "US", "GB", "DE"). Many brands are country-specific.' },
|
|
65
|
+
limit: { type: 'number', description: 'OPTIONAL — max results (1–25, default 10).' },
|
|
66
|
+
},
|
|
67
|
+
['query'],
|
|
68
|
+
),
|
|
69
|
+
|
|
70
|
+
t(
|
|
71
|
+
'bitrefill_get_product',
|
|
72
|
+
"Get full details for one product, including its `packages` array (each package = a denomination with `id`, `value`, `price`, `currency`). Use the package `id` (NOT the bare value) when creating an invoice.",
|
|
73
|
+
{
|
|
74
|
+
product_id: { type: 'string', description: 'Product slug from bitrefill_search, e.g. "amazon-us", "steam-us".' },
|
|
75
|
+
},
|
|
76
|
+
['product_id'],
|
|
77
|
+
),
|
|
78
|
+
|
|
79
|
+
t(
|
|
80
|
+
'bitrefill_get_balance',
|
|
81
|
+
"Get the user's Bitrefill account balance (the pre-funded pool used by `payment_method:\"balance\"`). Returns `{ balance, currency }`. No args.",
|
|
82
|
+
),
|
|
83
|
+
|
|
84
|
+
t(
|
|
85
|
+
'bitrefill_create_invoice',
|
|
86
|
+
"SPEND: confirmation-gated. Create an invoice for one or more products. Pass `payment_method:\"balance\"` + `auto_pay:true` for instant fulfillment from the account balance (lowest blast radius). For Lightning/on-chain, omit `auto_pay`, set `payment_method:\"lightning\"` (etc.) and `refund_address` — the response carries the payment URI; poll `bitrefill_get_invoice` until status=\"complete\" and then read the order. Up to 20 line items per invoice.",
|
|
87
|
+
{
|
|
88
|
+
products: {
|
|
89
|
+
type: 'array',
|
|
90
|
+
description: 'Line items. Each: { product_id, package_id, quantity }. Get `package_id` from bitrefill_get_product (NOT the bare denomination value).',
|
|
91
|
+
items: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
product_id: { type: 'string' },
|
|
95
|
+
package_id: { type: 'string' },
|
|
96
|
+
quantity: { type: 'number' },
|
|
97
|
+
},
|
|
98
|
+
required: ['product_id', 'package_id', 'quantity'],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
payment_method: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
description: 'How to pay: "balance" (account balance, instant), "lightning", "bitcoin", "usdc_base" (x402), "usdc_polygon", "usdt_tron", etc.',
|
|
104
|
+
enum: ['balance', 'lightning', 'bitcoin', 'usdc_base', 'usdc_polygon', 'usdc_ethereum', 'usdt_tron', 'usdt_ethereum'],
|
|
105
|
+
},
|
|
106
|
+
auto_pay: { type: 'boolean', description: 'Required true with `payment_method:"balance"` for instant settlement. Omit for crypto methods.' },
|
|
107
|
+
refund_address: { type: 'string', description: 'REQUIRED for non-balance crypto methods — refund destination if the invoice expires or partially pays.' },
|
|
108
|
+
email: { type: 'string', description: 'OPTIONAL — delivery / receipt email. Defaults to the account email when authenticated.' },
|
|
109
|
+
webhook_url: { type: 'string', description: 'OPTIONAL — URL Bitrefill calls when the order is delivered.' },
|
|
110
|
+
},
|
|
111
|
+
['products', 'payment_method'],
|
|
112
|
+
/* spend */ true,
|
|
113
|
+
),
|
|
114
|
+
|
|
115
|
+
t(
|
|
116
|
+
'bitrefill_get_invoice',
|
|
117
|
+
"Get the invoice's current status: `unpaid`, `pending`, `paid`, `complete`, `expired`, `failed`. For crypto payment methods, poll this until `complete`; then call `bitrefill_get_order` for redemption details.",
|
|
118
|
+
{
|
|
119
|
+
invoice_id: { type: 'string', description: 'Invoice id returned by bitrefill_create_invoice.' },
|
|
120
|
+
},
|
|
121
|
+
['invoice_id'],
|
|
122
|
+
),
|
|
123
|
+
|
|
124
|
+
t(
|
|
125
|
+
'bitrefill_get_order',
|
|
126
|
+
"Get an order's redemption details once delivered. Returns `redemption_info` containing the code, PIN (for prepaid cards), redemption link, instructions. ONLY call after the corresponding invoice status is `complete`. Treat the returned code as cash — never paste it in shared chats.",
|
|
127
|
+
{
|
|
128
|
+
order_id: { type: 'string', description: 'Order id from a completed invoice (`order_id` on the invoice or in its `orders[]`).' },
|
|
129
|
+
},
|
|
130
|
+
['order_id'],
|
|
131
|
+
),
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
/** All Bitrefill tool names that move money (confirmation-gated). */
|
|
135
|
+
export const BITREFILL_SPEND_TOOLS: Set<string> = new Set(
|
|
136
|
+
BITREFILL_TOOLS.filter((t) => t.spend).map((t) => t.name),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
export function isBitrefillSpendTool(name: string): boolean {
|
|
140
|
+
return BITREFILL_SPEND_TOOLS.has(name);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getBitrefillTool(name: string): BitrefillToolDef | undefined {
|
|
144
|
+
return BITREFILL_TOOLS.find((t) => t.name === name);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A handler bound to one Bitrefill tool. */
|
|
148
|
+
export type BitrefillHandler = (args: Record<string, unknown>) => Promise<unknown>;
|
|
149
|
+
|
|
150
|
+
export interface BindBitrefillOptions {
|
|
151
|
+
/** Skip tools without a handler instead of throwing (default false). */
|
|
152
|
+
allowMissing?: boolean;
|
|
153
|
+
/** ToolSource id for the registry (default 'bitrefill'). */
|
|
154
|
+
id?: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Bind Bitrefill contract tools to in-process handlers → an InProcessToolSource.
|
|
159
|
+
*
|
|
160
|
+
* const source = bindBitrefillTools({
|
|
161
|
+
* bitrefill_search: async (args) => api.search(args),
|
|
162
|
+
* bitrefill_get_product: async ({ product_id }) => api.product(product_id),
|
|
163
|
+
* bitrefill_get_balance: async () => api.balance(),
|
|
164
|
+
* bitrefill_create_invoice: async (args) => api.createInvoice(args),
|
|
165
|
+
* bitrefill_get_invoice: async ({ invoice_id }) => api.invoice(invoice_id),
|
|
166
|
+
* bitrefill_get_order: async ({ order_id }) => api.order(order_id),
|
|
167
|
+
* });
|
|
168
|
+
* tools.register(source);
|
|
169
|
+
*/
|
|
170
|
+
export function bindBitrefillTools(
|
|
171
|
+
handlers: Record<string, BitrefillHandler>,
|
|
172
|
+
opts: BindBitrefillOptions = {},
|
|
173
|
+
): InProcessToolSource {
|
|
174
|
+
const bound: InProcessTool[] = [];
|
|
175
|
+
for (const def of BITREFILL_TOOLS) {
|
|
176
|
+
const handler = handlers[def.name];
|
|
177
|
+
if (!handler) {
|
|
178
|
+
if (opts.allowMissing) continue;
|
|
179
|
+
throw new Error(`bindBitrefillTools: no handler for "${def.name}"`);
|
|
180
|
+
}
|
|
181
|
+
bound.push({
|
|
182
|
+
name: def.name,
|
|
183
|
+
description: def.description,
|
|
184
|
+
parameters: def.parameters,
|
|
185
|
+
requiresConfirmation: def.requiresConfirmation,
|
|
186
|
+
handler,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return new InProcessToolSource(opts.id ?? 'bitrefill', bound);
|
|
190
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** Tool-output compression tests — savings + the safety guarantees. */
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
import { compressToolResult } from './compress.js';
|
|
5
|
+
import { estimateTokens } from './budget.js';
|
|
6
|
+
|
|
7
|
+
/** Build a verbose merchant-list-like result the agentic loop would crush. */
|
|
8
|
+
function merchants(n: number): { results: Array<Record<string, unknown>> } {
|
|
9
|
+
return {
|
|
10
|
+
results: Array.from({ length: n }, (_, i) => ({
|
|
11
|
+
name: `Coffee Shop ${i}`,
|
|
12
|
+
category: 'cafe',
|
|
13
|
+
description:
|
|
14
|
+
'Accepts Bitcoin on-chain and Lightning. Open daily. ' +
|
|
15
|
+
'A cozy spot with reliable wifi and great espresso for digital nomads.',
|
|
16
|
+
lat: 41.0 + i / 1000,
|
|
17
|
+
lng: 12.0 + i / 1000,
|
|
18
|
+
tags: ['bitcoin', 'lightning', 'cafe'],
|
|
19
|
+
})),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('compressToolResult', () => {
|
|
24
|
+
it('passes small results through untouched', () => {
|
|
25
|
+
const small = { total_sats: 123_456, layers: 2 };
|
|
26
|
+
const r = compressToolResult(small);
|
|
27
|
+
expect(r.changed).toBe(false);
|
|
28
|
+
expect(r.content).toBe(JSON.stringify(small));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('elides the middle of a long array and reports the omitted count', () => {
|
|
32
|
+
const r = compressToolResult(merchants(40), { maxArrayItems: 6 });
|
|
33
|
+
expect(r.changed).toBe(true);
|
|
34
|
+
expect(r.compressedTokens).toBeLessThan(r.originalTokens);
|
|
35
|
+
expect(r.elided).toBeGreaterThan(0);
|
|
36
|
+
|
|
37
|
+
const parsed = JSON.parse(r.content) as { results: Array<Record<string, unknown>> };
|
|
38
|
+
const marker = parsed.results.find((x) => '__elided__' in x);
|
|
39
|
+
expect(marker).toBeDefined();
|
|
40
|
+
expect(marker!.__elided__).toBe(r.elided);
|
|
41
|
+
// Kept first/last anchors → fewer items than the original 40.
|
|
42
|
+
expect(parsed.results.length).toBeLessThan(40);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('dedupes identical array items before eliding', () => {
|
|
46
|
+
const dup = { rows: Array.from({ length: 30 }, () => ({ status: 'ok', code: 200 })) };
|
|
47
|
+
const r = compressToolResult(dup, { maxArrayItems: 4 });
|
|
48
|
+
const parsed = JSON.parse(r.content) as { rows: Array<Record<string, unknown>> };
|
|
49
|
+
const real = parsed.rows.filter((x) => !('__elided__' in x));
|
|
50
|
+
// 30 identical rows collapse to a single unique row (≤ maxArrayItems).
|
|
51
|
+
expect(real.length).toBe(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('never regresses: returns the original when crushing would not save tokens', () => {
|
|
55
|
+
// A flat array of unique short numbers compresses to roughly itself; the
|
|
56
|
+
// elision marker can cost more than it saves — must fall back to original.
|
|
57
|
+
const flat = { xs: Array.from({ length: 60 }, (_, i) => i) };
|
|
58
|
+
const r = compressToolResult(flat, { maxArrayItems: 50, dedupe: false });
|
|
59
|
+
expect(r.compressedTokens).toBeLessThanOrEqual(r.originalTokens);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('SAFETY: never truncates whitespace-free identifiers (invoices/addresses)', () => {
|
|
63
|
+
const invoice = 'lnbc' + '1'.repeat(1500); // long BOLT11-like, no spaces
|
|
64
|
+
const addr = 'bc1q' + 'a'.repeat(800);
|
|
65
|
+
const payload = {
|
|
66
|
+
filler: Array.from({ length: 20 }, (_, i) => ({ note: 'x'.repeat(50), i })),
|
|
67
|
+
invoice,
|
|
68
|
+
address: addr,
|
|
69
|
+
};
|
|
70
|
+
const r = compressToolResult(payload, { maxArrayItems: 4, maxStringLength: 80 });
|
|
71
|
+
expect(r.content).toContain(invoice); // intact, not truncated
|
|
72
|
+
expect(r.content).toContain(addr);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('SAFETY: never elides/truncates values under preserved money keys', () => {
|
|
76
|
+
const payload = {
|
|
77
|
+
// A long prose string under a preserve key stays intact.
|
|
78
|
+
balance: 'x '.repeat(1000),
|
|
79
|
+
// Numbers are never touched regardless.
|
|
80
|
+
total_sats: 4_800_123,
|
|
81
|
+
history: Array.from({ length: 40 }, (_, i) => ({ memo: 'spent on coffee number ' + i, i })),
|
|
82
|
+
};
|
|
83
|
+
const r = compressToolResult(payload, { maxArrayItems: 4, maxStringLength: 40 });
|
|
84
|
+
const parsed = JSON.parse(r.content) as Record<string, unknown>;
|
|
85
|
+
expect(parsed.balance).toBe(payload.balance); // preserved verbatim
|
|
86
|
+
expect(parsed.total_sats).toBe(4_800_123);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('SAFETY: numbers are never altered', () => {
|
|
90
|
+
const payload = {
|
|
91
|
+
quotes: Array.from({ length: 30 }, (_, i) => ({
|
|
92
|
+
amount_sats: 1000 + i,
|
|
93
|
+
rate: 0.00012345,
|
|
94
|
+
fee: 7,
|
|
95
|
+
})),
|
|
96
|
+
};
|
|
97
|
+
const r = compressToolResult(payload, { maxArrayItems: 5 });
|
|
98
|
+
const parsed = JSON.parse(r.content) as { quotes: Array<Record<string, number>> };
|
|
99
|
+
for (const q of parsed.quotes) {
|
|
100
|
+
if ('__elided__' in q) continue;
|
|
101
|
+
expect(Number.isInteger(q.amount_sats)).toBe(true);
|
|
102
|
+
expect(q.rate).toBe(0.00012345);
|
|
103
|
+
expect(q.fee).toBe(7);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('truncates long prose strings (with whitespace) when over the limit', () => {
|
|
108
|
+
const payload = { log: ('error happened at step ').repeat(200) };
|
|
109
|
+
const r = compressToolResult(payload, { maxStringLength: 100, minTokens: 1 });
|
|
110
|
+
expect(r.changed).toBe(true);
|
|
111
|
+
expect(r.content).toContain('… (+');
|
|
112
|
+
expect(estimateTokens(r.content)).toBeLessThan(r.originalTokens);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('collapses nesting beyond maxDepth to a shape summary', () => {
|
|
116
|
+
const deep = { a: { b: { c: { d: { e: { f: { g: 'too deep' } } } } } }, pad: 'p'.repeat(900) };
|
|
117
|
+
const r = compressToolResult(deep, { maxDepth: 3, minTokens: 1 });
|
|
118
|
+
expect(r.content).toMatch(/\[object: \d+ keys\]/);
|
|
119
|
+
});
|
|
120
|
+
});
|