@kaleidorg/mind 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capabilities.d.ts +38 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +41 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/context/budget.d.ts +29 -0
- package/dist/context/budget.d.ts.map +1 -0
- package/dist/context/budget.js +36 -0
- package/dist/context/budget.js.map +1 -0
- package/dist/context/builder.d.ts +39 -0
- package/dist/context/builder.d.ts.map +1 -0
- package/dist/context/builder.js +77 -0
- package/dist/context/builder.js.map +1 -0
- package/dist/engine.d.ts +9 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1 -0
- package/dist/engine.js.map +1 -1
- package/dist/fastpath/fastpath.d.ts +38 -0
- package/dist/fastpath/fastpath.d.ts.map +1 -0
- package/dist/fastpath/fastpath.js +52 -0
- package/dist/fastpath/fastpath.js.map +1 -0
- package/dist/funnel.d.ts +117 -0
- package/dist/funnel.d.ts.map +1 -0
- package/dist/funnel.js +195 -0
- package/dist/funnel.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -1
- package/dist/kaleidoswap/contract.d.ts +72 -0
- package/dist/kaleidoswap/contract.d.ts.map +1 -0
- package/dist/kaleidoswap/contract.js +125 -0
- package/dist/kaleidoswap/contract.js.map +1 -0
- package/dist/knowledge/bitcoin-copilot.d.ts +11 -0
- package/dist/knowledge/bitcoin-copilot.d.ts.map +1 -0
- package/dist/knowledge/bitcoin-copilot.js +155 -0
- package/dist/knowledge/bitcoin-copilot.js.map +1 -0
- package/dist/knowledge/btc-map.d.ts +87 -0
- package/dist/knowledge/btc-map.d.ts.map +1 -0
- package/dist/knowledge/btc-map.js +365 -0
- package/dist/knowledge/btc-map.js.map +1 -0
- package/dist/knowledge/merchants.d.ts +24 -0
- package/dist/knowledge/merchants.d.ts.map +1 -0
- package/dist/knowledge/merchants.js +34 -0
- package/dist/knowledge/merchants.js.map +1 -0
- package/dist/knowledge/wallet.d.ts +34 -0
- package/dist/knowledge/wallet.d.ts.map +1 -0
- package/dist/knowledge/wallet.js +63 -0
- package/dist/knowledge/wallet.js.map +1 -0
- package/dist/lsps1/contract.d.ts +55 -0
- package/dist/lsps1/contract.d.ts.map +1 -0
- package/dist/lsps1/contract.js +91 -0
- package/dist/lsps1/contract.js.map +1 -0
- package/dist/memory/store.d.ts +40 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +143 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/memory/tool.d.ts +9 -0
- package/dist/memory/tool.d.ts.map +1 -0
- package/dist/memory/tool.js +70 -0
- package/dist/memory/tool.js.map +1 -0
- package/dist/memory/types.d.ts +68 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +14 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/rag/retriever.d.ts +30 -0
- package/dist/rag/retriever.d.ts.map +1 -0
- package/dist/rag/retriever.js +72 -0
- package/dist/rag/retriever.js.map +1 -0
- package/dist/rag/tool.d.ts +15 -0
- package/dist/rag/tool.d.ts.map +1 -0
- package/dist/rag/tool.js +42 -0
- package/dist/rag/tool.js.map +1 -0
- package/dist/rag/types.d.ts +44 -0
- package/dist/rag/types.d.ts.map +1 -0
- package/dist/rag/types.js +11 -0
- package/dist/rag/types.js.map +1 -0
- package/dist/rag/vector-store.d.ts +23 -0
- package/dist/rag/vector-store.d.ts.map +1 -0
- package/dist/rag/vector-store.js +72 -0
- package/dist/rag/vector-store.js.map +1 -0
- package/dist/recipe/asset-send.d.ts +15 -0
- package/dist/recipe/asset-send.d.ts.map +1 -0
- package/dist/recipe/asset-send.js +83 -0
- package/dist/recipe/asset-send.js.map +1 -0
- package/dist/recipe/kaleidoswap-atomic.d.ts +27 -0
- package/dist/recipe/kaleidoswap-atomic.d.ts.map +1 -0
- package/dist/recipe/kaleidoswap-atomic.js +111 -0
- package/dist/recipe/kaleidoswap-atomic.js.map +1 -0
- package/dist/recipe/payments.d.ts +15 -0
- package/dist/recipe/payments.d.ts.map +1 -0
- package/dist/recipe/payments.js +119 -0
- package/dist/recipe/payments.js.map +1 -0
- package/dist/recipe/receive.d.ts +14 -0
- package/dist/recipe/receive.d.ts.map +1 -0
- package/dist/recipe/receive.js +109 -0
- package/dist/recipe/receive.js.map +1 -0
- package/dist/recipe/runner.d.ts +42 -0
- package/dist/recipe/runner.d.ts.map +1 -0
- package/dist/recipe/runner.js +106 -0
- package/dist/recipe/runner.js.map +1 -0
- package/dist/recipe/swap.d.ts +16 -0
- package/dist/recipe/swap.d.ts.map +1 -0
- package/dist/recipe/swap.js +73 -0
- package/dist/recipe/swap.js.map +1 -0
- package/dist/recipe/types.d.ts +71 -0
- package/dist/recipe/types.d.ts.map +1 -0
- package/dist/recipe/types.js +13 -0
- package/dist/recipe/types.js.map +1 -0
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +20 -2
- package/dist/skills/registry.js.map +1 -1
- package/dist/tools/cli.d.ts +43 -0
- package/dist/tools/cli.d.ts.map +1 -0
- package/dist/tools/cli.js +61 -0
- package/dist/tools/cli.js.map +1 -0
- package/dist/tools/mcp.d.ts +3 -2
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mcp.js +3 -2
- package/dist/tools/mcp.js.map +1 -1
- package/dist/wallet/confirm.d.ts +12 -0
- package/dist/wallet/confirm.d.ts.map +1 -0
- package/dist/wallet/confirm.js +67 -0
- package/dist/wallet/confirm.js.map +1 -0
- package/dist/wallet/contract.d.ts +57 -0
- package/dist/wallet/contract.d.ts.map +1 -0
- package/dist/wallet/contract.js +113 -0
- package/dist/wallet/contract.js.map +1 -0
- package/package.json +10 -5
- package/skills/README.md +6 -1
- package/skills/kaleido-lsps/SKILL.md +56 -0
- package/skills/kaleido-trading/SKILL.md +85 -18
- package/skills/merchant-finder/SKILL.md +87 -0
- package/skills/paid-data/SKILL.md +12 -0
- package/skills/wallet-assistant/SKILL.md +38 -0
- package/src/capabilities.ts +79 -0
- package/src/context/budget.ts +46 -0
- package/src/context/builder.ts +100 -0
- package/src/context/context.test.ts +87 -0
- package/src/engine.ts +6 -0
- package/src/fastpath/fastpath.test.ts +34 -0
- package/src/fastpath/fastpath.ts +70 -0
- package/src/funnel.test.ts +207 -0
- package/src/funnel.ts +285 -0
- package/src/index.ts +128 -0
- package/src/kaleidoswap/contract.test.ts +147 -0
- package/src/kaleidoswap/contract.ts +212 -0
- package/src/knowledge/bitcoin-copilot.ts +177 -0
- package/src/knowledge/btc-map.test.ts +188 -0
- package/src/knowledge/btc-map.ts +446 -0
- package/src/knowledge/knowledge.test.ts +63 -0
- package/src/knowledge/merchants.ts +49 -0
- package/src/knowledge/wallet.ts +84 -0
- package/src/lsps1/contract.test.ts +81 -0
- package/src/lsps1/contract.ts +132 -0
- package/src/memory/memory.test.ts +140 -0
- package/src/memory/store.ts +174 -0
- package/src/memory/tool.ts +76 -0
- package/src/memory/types.ts +76 -0
- package/src/rag/rag.test.ts +85 -0
- package/src/rag/retriever.ts +94 -0
- package/src/rag/tool.ts +55 -0
- package/src/rag/types.ts +49 -0
- package/src/rag/vector-store.ts +78 -0
- package/src/recipe/asset-send.ts +79 -0
- package/src/recipe/kaleidoswap-atomic.test.ts +138 -0
- package/src/recipe/kaleidoswap-atomic.ts +117 -0
- package/src/recipe/payments.ts +116 -0
- package/src/recipe/receive.ts +98 -0
- package/src/recipe/recipe.test.ts +193 -0
- package/src/recipe/runner.ts +134 -0
- package/src/recipe/swap.ts +74 -0
- package/src/recipe/types.ts +76 -0
- package/src/skills/registry.ts +21 -2
- package/src/skills/skills.test.ts +42 -0
- package/src/tools/cli.test.ts +53 -0
- package/src/tools/cli.ts +98 -0
- package/src/tools/mcp.ts +3 -2
- package/src/wallet/confirm.test.ts +57 -0
- package/src/wallet/confirm.ts +74 -0
- package/src/wallet/contract.test.ts +89 -0
- package/src/wallet/contract.ts +157 -0
- package/skills/kaleido-wallet/SKILL.md +0 -28
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BTC Map tool source — exposes `find_merchant_locations` and
|
|
3
|
+
* `get_merchant_info` so the agent can answer "where can I spend Bitcoin
|
|
4
|
+
* near me?" using the SAME tool names on every surface.
|
|
5
|
+
*
|
|
6
|
+
* - mobile → injects device GPS + a live BTC Map fetch
|
|
7
|
+
* - desktop → injects a live fetch (or a server-side cache)
|
|
8
|
+
* - eval / playground → no injection, falls back to the bundled offline list
|
|
9
|
+
*
|
|
10
|
+
* Pure data + orchestration — NO network in core. The host injects the
|
|
11
|
+
* `location` resolver and the `fetch` adapter; without them the source still
|
|
12
|
+
* runs against an offline `Merchant[]` so the skill is never dead on arrival.
|
|
13
|
+
*
|
|
14
|
+
* The result shape mirrors what rate's host returns today, so a mobile host
|
|
15
|
+
* can swap its bespoke merchant tools for this factory verbatim.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { ToolDef } from '../types.js';
|
|
19
|
+
import type { ToolSource } from '../tools/source.js';
|
|
20
|
+
import type { Merchant } from './merchants.js';
|
|
21
|
+
|
|
22
|
+
const FIND = 'find_merchant_locations';
|
|
23
|
+
const INFO = 'get_merchant_info';
|
|
24
|
+
|
|
25
|
+
/** A geographic point + optional human label. */
|
|
26
|
+
export interface LatLng {
|
|
27
|
+
lat: number;
|
|
28
|
+
lng: number;
|
|
29
|
+
/** Optional "Lugano, Switzerland" style label for messages. */
|
|
30
|
+
label?: string;
|
|
31
|
+
/** True when the location came from the device GPS, false for a default/fallback. */
|
|
32
|
+
precise?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Host-injected location resolver. Core is platform-agnostic — RN has GPS,
|
|
37
|
+
* Node typically doesn't. When omitted, `find_merchant_locations` falls back
|
|
38
|
+
* to the offline merchant list.
|
|
39
|
+
*/
|
|
40
|
+
export interface LocationProvider {
|
|
41
|
+
/** Resolve "near me" — the device location, or null if unavailable. */
|
|
42
|
+
getCurrent(): Promise<LatLng | null>;
|
|
43
|
+
/** Optional: geocode a free-text address ("Via Pessina 12, Lugano"). */
|
|
44
|
+
geocode?(address: string): Promise<LatLng | null>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A merchant returned by a live BTC Map fetch. */
|
|
48
|
+
export interface BtcMapMerchant extends Merchant {
|
|
49
|
+
/** Distance from the search centre, metres. */
|
|
50
|
+
distance_m?: number;
|
|
51
|
+
/** Free-text contact extras the model can surface to the user. */
|
|
52
|
+
phone?: string;
|
|
53
|
+
website?: string;
|
|
54
|
+
opening_hours?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Host-injected live-fetch adapter. Receives a normalized query and returns
|
|
59
|
+
* the matching merchants. The host is responsible for the HTTP call, caching,
|
|
60
|
+
* and any rate limits — core never reaches the network.
|
|
61
|
+
*/
|
|
62
|
+
export type BtcMapFetch = (q: {
|
|
63
|
+
center: { lat: number; lng: number };
|
|
64
|
+
radiusMeters: number;
|
|
65
|
+
query?: string;
|
|
66
|
+
category?: string;
|
|
67
|
+
limit: number;
|
|
68
|
+
}) => Promise<BtcMapMerchant[]>;
|
|
69
|
+
|
|
70
|
+
export interface BtcMapToolOptions {
|
|
71
|
+
/** Resolve "near me" + geocode an address. RN hosts inject Expo Location. */
|
|
72
|
+
location?: LocationProvider;
|
|
73
|
+
/** Hit a live BTC Map (or your own cache). Host-injected — no network in core. */
|
|
74
|
+
fetch?: BtcMapFetch;
|
|
75
|
+
/** Offline list used when no location/fetch is available. Defaults to BTC_MAP_SAMPLE. */
|
|
76
|
+
offlineMerchants?: Merchant[];
|
|
77
|
+
/** Default for `limit` when the caller doesn't set one. Default 10. */
|
|
78
|
+
k?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A tiny, hand-curated sample so the skill works offline out of the box. */
|
|
82
|
+
export const BTC_MAP_SAMPLE: Merchant[] = [
|
|
83
|
+
{
|
|
84
|
+
id: 'lugano-bitcoinpeople-cafe',
|
|
85
|
+
name: 'Bitcoin People Café',
|
|
86
|
+
category: 'cafe',
|
|
87
|
+
address: 'Via Pessina 12',
|
|
88
|
+
city: 'Lugano',
|
|
89
|
+
lat: 46.0037,
|
|
90
|
+
lng: 8.9511,
|
|
91
|
+
acceptedAssets: ['lightning', 'onchain'],
|
|
92
|
+
description: 'Specialty espresso bar; Plan ₿ Lugano partner.',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'lugano-bistro-libertine',
|
|
96
|
+
name: 'Bistro Libertine',
|
|
97
|
+
category: 'restaurant',
|
|
98
|
+
address: 'Piazza della Riforma 3',
|
|
99
|
+
city: 'Lugano',
|
|
100
|
+
lat: 46.004,
|
|
101
|
+
lng: 8.952,
|
|
102
|
+
acceptedAssets: ['lightning', 'usdt', 'onchain'],
|
|
103
|
+
description: 'Italian-Swiss bistro, lunch + dinner. Accepts Tether on Liquid.',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'lugano-bookshop-volta',
|
|
107
|
+
name: 'Libreria Volta',
|
|
108
|
+
category: 'shop',
|
|
109
|
+
address: 'Via Cattedrale 8',
|
|
110
|
+
city: 'Lugano',
|
|
111
|
+
lat: 46.0055,
|
|
112
|
+
lng: 8.9499,
|
|
113
|
+
acceptedAssets: ['lightning'],
|
|
114
|
+
description: 'Independent bookshop; Italian, English and German titles.',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'lisbon-meson-andaluz',
|
|
118
|
+
name: 'Mesón Andaluz',
|
|
119
|
+
category: 'restaurant',
|
|
120
|
+
address: 'Rua das Flores 42',
|
|
121
|
+
city: 'Lisbon',
|
|
122
|
+
lat: 38.71,
|
|
123
|
+
lng: -9.143,
|
|
124
|
+
acceptedAssets: ['lightning', 'onchain'],
|
|
125
|
+
description: 'Andalusian tapas in Chiado. Bitcoin accepted since 2022.',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'lisbon-surf-bitcoin',
|
|
129
|
+
name: 'Surf & Sats',
|
|
130
|
+
category: 'shop',
|
|
131
|
+
address: 'Av. da Liberdade 180',
|
|
132
|
+
city: 'Lisbon',
|
|
133
|
+
lat: 38.7211,
|
|
134
|
+
lng: -9.1466,
|
|
135
|
+
acceptedAssets: ['lightning'],
|
|
136
|
+
description: 'Surfboard rental and lessons in Costa da Caparica.',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'sansalvador-elzonte-hope',
|
|
140
|
+
name: 'Hope House El Zonte',
|
|
141
|
+
category: 'cafe',
|
|
142
|
+
address: 'Calle Principal',
|
|
143
|
+
city: 'El Zonte',
|
|
144
|
+
lat: 13.492,
|
|
145
|
+
lng: -89.4395,
|
|
146
|
+
acceptedAssets: ['lightning', 'onchain'],
|
|
147
|
+
description: 'Bitcoin Beach hub. Coffee, community, and a Lightning ATM.',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'sansalvador-elzonte-garten',
|
|
151
|
+
name: 'Garten Restaurante',
|
|
152
|
+
category: 'restaurant',
|
|
153
|
+
address: 'Bitcoin Beach',
|
|
154
|
+
city: 'El Zonte',
|
|
155
|
+
lat: 13.4925,
|
|
156
|
+
lng: -89.438,
|
|
157
|
+
acceptedAssets: ['lightning'],
|
|
158
|
+
description: 'Beachfront restaurant, full Bitcoin payments since 2021.',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'nyc-pubkey',
|
|
162
|
+
name: 'PubKey',
|
|
163
|
+
category: 'bar',
|
|
164
|
+
address: '85 Washington Pl',
|
|
165
|
+
city: 'New York',
|
|
166
|
+
lat: 40.732,
|
|
167
|
+
lng: -73.999,
|
|
168
|
+
acceptedAssets: ['lightning', 'onchain'],
|
|
169
|
+
description: 'Bitcoin bar in Greenwich Village — meetups, Lightning tap.',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'prague-paralelni-polis',
|
|
173
|
+
name: 'Paralelní Polis',
|
|
174
|
+
category: 'cafe',
|
|
175
|
+
address: 'Dělnická 43',
|
|
176
|
+
city: 'Prague',
|
|
177
|
+
lat: 50.105,
|
|
178
|
+
lng: 14.448,
|
|
179
|
+
acceptedAssets: ['lightning', 'onchain', 'monero'],
|
|
180
|
+
description: 'Crypto-only café and hackerspace — no fiat accepted, ever.',
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: 'amsterdam-bitcoin-embassy',
|
|
184
|
+
name: 'Bitcoin Embassy Amsterdam',
|
|
185
|
+
category: 'cafe',
|
|
186
|
+
address: 'Nieuwezijds Voorburgwal 162',
|
|
187
|
+
city: 'Amsterdam',
|
|
188
|
+
lat: 52.374,
|
|
189
|
+
lng: 4.893,
|
|
190
|
+
acceptedAssets: ['lightning', 'onchain'],
|
|
191
|
+
description: 'Co-working café and meetup hub, Lightning tap on draft beer.',
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
/** Clamp a value to a numeric range, returning the default when input is bad. */
|
|
196
|
+
function clamp(n: unknown, lo: number, hi: number, dflt: number): number {
|
|
197
|
+
const v = Number(n);
|
|
198
|
+
if (!Number.isFinite(v)) return dflt;
|
|
199
|
+
return Math.min(hi, Math.max(lo, v));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Substring scoring used by the offline fallback (matches the old behavior). */
|
|
203
|
+
function fuzzyScore(query: string, text: string): number {
|
|
204
|
+
const q = query.toLowerCase();
|
|
205
|
+
const t = text.toLowerCase();
|
|
206
|
+
if (!q) return 1;
|
|
207
|
+
if (t.includes(q)) return 1;
|
|
208
|
+
let hits = 0;
|
|
209
|
+
let qi = 0;
|
|
210
|
+
for (let i = 0; i < t.length && qi < q.length; i++) {
|
|
211
|
+
if (t[i] === q[qi]) {
|
|
212
|
+
hits++;
|
|
213
|
+
qi++;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return hits / q.length;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Map a Merchant → the response row the model sees (stable for rate parity). */
|
|
220
|
+
function row(m: BtcMapMerchant) {
|
|
221
|
+
return {
|
|
222
|
+
id: m.id,
|
|
223
|
+
name: m.name,
|
|
224
|
+
address: m.address,
|
|
225
|
+
category: m.category,
|
|
226
|
+
lat: m.lat,
|
|
227
|
+
lng: m.lng,
|
|
228
|
+
distance_m: m.distance_m,
|
|
229
|
+
phone: m.phone,
|
|
230
|
+
website: m.website,
|
|
231
|
+
opening_hours: m.opening_hours,
|
|
232
|
+
accepts_bitcoin: m.acceptedAssets?.includes('onchain') ?? true,
|
|
233
|
+
accepts_lightning: m.acceptedAssets?.includes('lightning') ?? true,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Offline search: substring + optional category over the bundled list. */
|
|
238
|
+
function searchOffline(
|
|
239
|
+
merchants: Merchant[],
|
|
240
|
+
query: string | undefined,
|
|
241
|
+
category: string | undefined,
|
|
242
|
+
limit: number,
|
|
243
|
+
): Merchant[] {
|
|
244
|
+
let filtered = merchants;
|
|
245
|
+
if (category) {
|
|
246
|
+
const c = category.toLowerCase().trim();
|
|
247
|
+
filtered = filtered.filter((m) => (m.category ?? '').toLowerCase() === c);
|
|
248
|
+
}
|
|
249
|
+
const q = (query ?? '').trim();
|
|
250
|
+
if (q.length >= 2) {
|
|
251
|
+
filtered = filtered
|
|
252
|
+
.map((m) => {
|
|
253
|
+
const hay = `${m.name ?? ''} ${m.description ?? ''} ${m.address ?? ''} ${m.city ?? ''}`;
|
|
254
|
+
const score =
|
|
255
|
+
fuzzyScore(q, m.name ?? '') * 3 +
|
|
256
|
+
fuzzyScore(q, m.address ?? '') * 2 +
|
|
257
|
+
fuzzyScore(q, hay) * 1;
|
|
258
|
+
return { m, score };
|
|
259
|
+
})
|
|
260
|
+
.filter((x) => x.score > 0.3)
|
|
261
|
+
.sort((a, b) => b.score - a.score)
|
|
262
|
+
.map((x) => x.m);
|
|
263
|
+
}
|
|
264
|
+
return filtered.slice(0, limit);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Build a ToolSource exposing `find_merchant_locations` + `get_merchant_info`.
|
|
269
|
+
*
|
|
270
|
+
* Resolution order for `find_merchant_locations`:
|
|
271
|
+
* 1. `near_address` provided → opts.location.geocode → opts.fetch (live)
|
|
272
|
+
* 2. opts.location.getCurrent → opts.fetch (live)
|
|
273
|
+
* 3. fall through to offline substring search over opts.offlineMerchants
|
|
274
|
+
*
|
|
275
|
+
* Any step that fails silently falls through to the next, so the skill is
|
|
276
|
+
* always answerable.
|
|
277
|
+
*/
|
|
278
|
+
export function createBtcMapToolSource(opts: BtcMapToolOptions = {}): ToolSource {
|
|
279
|
+
const offline = opts.offlineMerchants ?? BTC_MAP_SAMPLE;
|
|
280
|
+
const defaultLimit = opts.k ?? 10;
|
|
281
|
+
|
|
282
|
+
const find: ToolDef = {
|
|
283
|
+
name: FIND,
|
|
284
|
+
description:
|
|
285
|
+
"Find Bitcoin-accepting merchants near the user using live BTC Map data " +
|
|
286
|
+
"and the device's real location when available. Use when the user wants " +
|
|
287
|
+
'merchants, shops, restaurants, cafes, bars, ATMs, or places to spend ' +
|
|
288
|
+
'Bitcoin nearby. Pass ONLY the fields the user actually named — do not ' +
|
|
289
|
+
'invent constraints (e.g. omit `query` when they just say "near me").',
|
|
290
|
+
parameters: {
|
|
291
|
+
type: 'object',
|
|
292
|
+
properties: {
|
|
293
|
+
query: { type: 'string', description: 'Optional filter for merchant name or type, e.g. "coffee"' },
|
|
294
|
+
category: { type: 'string', description: 'restaurant | cafe | bar | shop | grocery | lodging | atm' },
|
|
295
|
+
near_address: { type: 'string', description: 'Address/city to search around instead of the current location' },
|
|
296
|
+
radius_km: { type: 'number', description: 'Search radius in km (0.25–50, default 5)' },
|
|
297
|
+
limit: { type: 'number', description: 'Max number of results (1–20, default 10)' },
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const info: ToolDef = {
|
|
303
|
+
name: INFO,
|
|
304
|
+
description:
|
|
305
|
+
'Get detailed information about one specific merchant by id or name — ' +
|
|
306
|
+
'full address, accepted assets, contact details. Use after ' +
|
|
307
|
+
'find_merchant_locations when the user asks for more on a specific result.',
|
|
308
|
+
parameters: {
|
|
309
|
+
type: 'object',
|
|
310
|
+
properties: {
|
|
311
|
+
merchant_id: { type: 'string', description: 'Merchant id from find_merchant_locations (string or number)' },
|
|
312
|
+
merchant_name: { type: 'string', description: 'Merchant name (used when id is unknown)' },
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
async function tryLive(
|
|
318
|
+
center: LatLng,
|
|
319
|
+
radiusMeters: number,
|
|
320
|
+
query: string | undefined,
|
|
321
|
+
category: string | undefined,
|
|
322
|
+
limit: number,
|
|
323
|
+
): Promise<BtcMapMerchant[] | null> {
|
|
324
|
+
if (!opts.fetch) return null;
|
|
325
|
+
try {
|
|
326
|
+
return await opts.fetch({
|
|
327
|
+
center: { lat: center.lat, lng: center.lng },
|
|
328
|
+
radiusMeters,
|
|
329
|
+
query,
|
|
330
|
+
category,
|
|
331
|
+
limit,
|
|
332
|
+
});
|
|
333
|
+
} catch {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function resolveCenter(near_address: string | undefined): Promise<LatLng | null> {
|
|
339
|
+
if (!opts.location) return null;
|
|
340
|
+
if (near_address && near_address.trim().length >= 2 && opts.location.geocode) {
|
|
341
|
+
try {
|
|
342
|
+
const pt = await opts.location.geocode(near_address);
|
|
343
|
+
if (pt) return { ...pt, label: near_address, precise: false };
|
|
344
|
+
} catch {
|
|
345
|
+
/* fall through */
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
return await opts.location.getCurrent();
|
|
350
|
+
} catch {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function findLocations(args: Record<string, unknown>): Promise<unknown> {
|
|
356
|
+
const query = args.query ? String(args.query) : undefined;
|
|
357
|
+
const category = args.category ? String(args.category) : undefined;
|
|
358
|
+
const near_address = args.near_address ? String(args.near_address) : undefined;
|
|
359
|
+
const radius_km = clamp(args.radius_km, 0.25, 50, 5);
|
|
360
|
+
const limit = clamp(args.limit, 1, 20, defaultLimit);
|
|
361
|
+
const radiusMeters = radius_km * 1000;
|
|
362
|
+
|
|
363
|
+
// 1 + 2. Try the live path when we can.
|
|
364
|
+
const center = await resolveCenter(near_address);
|
|
365
|
+
if (center) {
|
|
366
|
+
const live = await tryLive(center, radiusMeters, query, category, limit);
|
|
367
|
+
if (live) {
|
|
368
|
+
const where = center.label || (center.precise ? 'your location' : 'the default location');
|
|
369
|
+
return {
|
|
370
|
+
success: true,
|
|
371
|
+
source: 'btcmap',
|
|
372
|
+
precise_location: !!center.precise,
|
|
373
|
+
center: { lat: center.lat, lng: center.lng },
|
|
374
|
+
merchants: live.map(row),
|
|
375
|
+
total_found: live.length,
|
|
376
|
+
message:
|
|
377
|
+
live.length > 0
|
|
378
|
+
? `Found ${live.length} Bitcoin merchant${live.length === 1 ? '' : 's'} near ${where}${query ? ` matching "${query}"` : ''}.`
|
|
379
|
+
: `No Bitcoin merchants found within ${radius_km} km of ${where}. Try widening the radius.`,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 3. Offline fallback.
|
|
385
|
+
const found = searchOffline(offline, query, category, limit);
|
|
386
|
+
return {
|
|
387
|
+
success: true,
|
|
388
|
+
source: 'offline',
|
|
389
|
+
precise_location: false,
|
|
390
|
+
merchants: found.map(row),
|
|
391
|
+
total_found: found.length,
|
|
392
|
+
message:
|
|
393
|
+
found.length > 0
|
|
394
|
+
? `Showing ${found.length} merchant${found.length === 1 ? '' : 's'} from the offline list${query ? ` matching "${query}"` : ''}.`
|
|
395
|
+
: `No merchants in the offline list matched${query ? ` "${query}"` : ''}. The host hasn't injected a live BTC Map fetch.`,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function getInfo(args: Record<string, unknown>): Promise<unknown> {
|
|
400
|
+
const idArg = args.merchant_id;
|
|
401
|
+
const nameArg = args.merchant_name ? String(args.merchant_name).trim() : '';
|
|
402
|
+
let m: Merchant | undefined;
|
|
403
|
+
if (idArg !== undefined && idArg !== null && idArg !== '') {
|
|
404
|
+
const want = String(idArg);
|
|
405
|
+
m = offline.find((x) => String(x.id) === want);
|
|
406
|
+
}
|
|
407
|
+
if (!m && nameArg.length >= 2) {
|
|
408
|
+
const q = nameArg.toLowerCase();
|
|
409
|
+
m =
|
|
410
|
+
offline.find((x) => (x.name ?? '').toLowerCase() === q) ??
|
|
411
|
+
offline
|
|
412
|
+
.map((x) => ({ x, score: fuzzyScore(q, (x.name ?? '').toLowerCase()) }))
|
|
413
|
+
.filter((r) => r.score > 0.5)
|
|
414
|
+
.sort((a, b) => b.score - a.score)[0]?.x;
|
|
415
|
+
}
|
|
416
|
+
if (!m) {
|
|
417
|
+
const suggestions = nameArg
|
|
418
|
+
? offline
|
|
419
|
+
.map((x) => ({ name: x.name ?? '', score: fuzzyScore(nameArg.toLowerCase(), (x.name ?? '').toLowerCase()) }))
|
|
420
|
+
.filter((r) => r.score > 0.3)
|
|
421
|
+
.sort((a, b) => b.score - a.score)
|
|
422
|
+
.slice(0, 3)
|
|
423
|
+
.map((r) => r.name)
|
|
424
|
+
: [];
|
|
425
|
+
return {
|
|
426
|
+
success: false,
|
|
427
|
+
error: `Could not find merchant${nameArg ? ` "${nameArg}"` : idArg !== undefined ? ` with id ${idArg}` : ''}.`,
|
|
428
|
+
suggestions: suggestions.length ? suggestions : undefined,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return { success: true, merchant: { ...row(m as BtcMapMerchant), city: m.city } };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function execute(name: string, args: Record<string, unknown>): Promise<unknown> {
|
|
435
|
+
if (name === FIND) return findLocations(args);
|
|
436
|
+
if (name === INFO) return getInfo(args);
|
|
437
|
+
throw new Error(`btc-map: unknown tool "${name}"`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
id: 'btc-map',
|
|
442
|
+
listTools: () => [find, info],
|
|
443
|
+
has: (name) => name === FIND || name === INFO,
|
|
444
|
+
execute,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Knowledge pack + corpus adapter tests — pure transforms. */
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
import { BITCOIN_COPILOT_DOCS } from './bitcoin-copilot.js';
|
|
5
|
+
import { walletHistoryToDocuments, contactsToDocuments } from './wallet.js';
|
|
6
|
+
import { merchantsToDocuments } from './merchants.js';
|
|
7
|
+
|
|
8
|
+
describe('BITCOIN_COPILOT_DOCS', () => {
|
|
9
|
+
it('is a non-trivial corpus with unique ids and real text', () => {
|
|
10
|
+
expect(BITCOIN_COPILOT_DOCS.length).toBeGreaterThanOrEqual(15);
|
|
11
|
+
const ids = BITCOIN_COPILOT_DOCS.map((d) => d.id);
|
|
12
|
+
expect(new Set(ids).size).toBe(ids.length); // unique
|
|
13
|
+
expect(BITCOIN_COPILOT_DOCS.every((d) => (d.text?.length ?? 0) > 40)).toBe(true);
|
|
14
|
+
// covers the key concept the hero demo asks about
|
|
15
|
+
expect(BITCOIN_COPILOT_DOCS.some((d) => /inbound liquidity/i.test(d.text))).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('walletHistoryToDocuments', () => {
|
|
20
|
+
it('renders sent/received sentences with date, amount, counterparty', () => {
|
|
21
|
+
const docs = walletHistoryToDocuments([
|
|
22
|
+
{ id: 't1', type: 'send', amountSats: 5000, counterparty: 'Bob', memo: 'lunch', timestamp: 1700000000000 },
|
|
23
|
+
{ id: 't2', type: 'receive', amountSats: 21000, counterparty: 'Alice', timestamp: 1700100000000 },
|
|
24
|
+
{ type: 'swap', amountSats: 100000, asset: 'USDT' },
|
|
25
|
+
]);
|
|
26
|
+
expect(docs[0].text).toMatch(/you sent 5000 sats to Bob — "lunch"/);
|
|
27
|
+
expect(docs[1].text).toMatch(/you received 21000 sats from Alice/);
|
|
28
|
+
expect(docs[2].text).toMatch(/you swapped 100000 USDT/);
|
|
29
|
+
expect(docs[2].id).toBe('tx_2'); // id defaulted
|
|
30
|
+
expect(docs[0].metadata?.kind).toBe('transaction');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('contactsToDocuments', () => {
|
|
35
|
+
it('includes address/nostr/note and skips empty contacts', () => {
|
|
36
|
+
const docs = contactsToDocuments([
|
|
37
|
+
{ name: 'Bob', lightningAddress: 'bob@getalby.com', note: 'coffee buddy' },
|
|
38
|
+
{}, // skipped
|
|
39
|
+
]);
|
|
40
|
+
expect(docs).toHaveLength(1);
|
|
41
|
+
expect(docs[0].text).toMatch(/Contact: Bob — Lightning address bob@getalby.com, coffee buddy/);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('merchantsToDocuments', () => {
|
|
46
|
+
it('renders place + acceptance and preserves coordinates in metadata', () => {
|
|
47
|
+
const docs = merchantsToDocuments([
|
|
48
|
+
{
|
|
49
|
+
name: 'Bitcoin Café',
|
|
50
|
+
category: 'cafe',
|
|
51
|
+
address: 'Via Nassa 1',
|
|
52
|
+
city: 'Lugano',
|
|
53
|
+
lat: 46.0,
|
|
54
|
+
lng: 8.95,
|
|
55
|
+
acceptedAssets: ['lightning', 'onchain'],
|
|
56
|
+
},
|
|
57
|
+
{ city: 'nowhere' }, // no name → skipped
|
|
58
|
+
]);
|
|
59
|
+
expect(docs).toHaveLength(1);
|
|
60
|
+
expect(docs[0].text).toMatch(/Bitcoin Café \(cafe\) at Via Nassa 1, Lugano\. Accepts lightning, onchain\./);
|
|
61
|
+
expect(docs[0].metadata).toMatchObject({ kind: 'merchant', lat: 46.0, lng: 8.95, city: 'Lugano' });
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BTC map discovery — turn a merchant directory (e.g. BTCMap / a local dataset)
|
|
3
|
+
* into `RagDocument[]` so the agent can answer "where can I spend Bitcoin for
|
|
4
|
+
* coffee near me?" with on-device semantic search over places.
|
|
5
|
+
*
|
|
6
|
+
* Pure transform over a generic Merchant shape. Coordinates are kept in
|
|
7
|
+
* metadata so the host can still pin results on a map after retrieval.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { RagDocument } from '../rag/types.js';
|
|
11
|
+
|
|
12
|
+
export interface Merchant {
|
|
13
|
+
id?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
category?: string; // 'cafe' | 'restaurant' | 'shop' | …
|
|
16
|
+
address?: string;
|
|
17
|
+
city?: string;
|
|
18
|
+
lat?: number;
|
|
19
|
+
lng?: number;
|
|
20
|
+
/** e.g. ['onchain', 'lightning', 'rgb']. */
|
|
21
|
+
acceptedAssets?: string[];
|
|
22
|
+
description?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One searchable doc per merchant; lat/lng preserved in metadata for mapping. */
|
|
26
|
+
export function merchantsToDocuments(merchants: Merchant[]): RagDocument[] {
|
|
27
|
+
return merchants
|
|
28
|
+
.filter((m) => m.name)
|
|
29
|
+
.map((m, i) => {
|
|
30
|
+
const where = [m.address, m.city].filter(Boolean).join(', ');
|
|
31
|
+
const pays = m.acceptedAssets?.length
|
|
32
|
+
? `Accepts ${m.acceptedAssets.join(', ')}.`
|
|
33
|
+
: 'Accepts Bitcoin.';
|
|
34
|
+
const cat = m.category ? ` (${m.category})` : '';
|
|
35
|
+
const desc = m.description ? ` ${m.description}` : '';
|
|
36
|
+
return {
|
|
37
|
+
id: m.id ?? `merchant_${m.name ?? i}`,
|
|
38
|
+
text: `${m.name}${cat}${where ? ` at ${where}` : ''}. ${pays}${desc}`.trim(),
|
|
39
|
+
metadata: {
|
|
40
|
+
kind: 'merchant',
|
|
41
|
+
name: m.name,
|
|
42
|
+
category: m.category,
|
|
43
|
+
city: m.city,
|
|
44
|
+
lat: m.lat,
|
|
45
|
+
lng: m.lng,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Personal wallet knowledge — turn a user's transaction history + contacts into
|
|
3
|
+
* `RagDocument[]` so the agent can answer "what did I spend on coffee last
|
|
4
|
+
* month?", "who did I pay 50k sats to?", "summarise my swaps" — all on-device,
|
|
5
|
+
* nothing leaving the phone.
|
|
6
|
+
*
|
|
7
|
+
* Pure transforms over minimal, generic shapes (hosts map their own types in).
|
|
8
|
+
* No deps, no PII leaves: the host decides what to ingest.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { RagDocument } from '../rag/types.js';
|
|
12
|
+
|
|
13
|
+
export interface WalletTx {
|
|
14
|
+
id?: string;
|
|
15
|
+
/** 'send' | 'receive' | 'swap' | 'deposit' | 'withdraw' | … */
|
|
16
|
+
type?: string;
|
|
17
|
+
amountSats?: number;
|
|
18
|
+
asset?: string; // 'BTC' | 'USDT' | 'XAUT' | …
|
|
19
|
+
/** Who/where — a name, contact, address, or merchant. */
|
|
20
|
+
counterparty?: string;
|
|
21
|
+
memo?: string;
|
|
22
|
+
status?: string;
|
|
23
|
+
/** Epoch ms. */
|
|
24
|
+
timestamp?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Contact {
|
|
28
|
+
name?: string;
|
|
29
|
+
lightningAddress?: string;
|
|
30
|
+
npub?: string;
|
|
31
|
+
note?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isoDate(ts?: number): string {
|
|
35
|
+
if (!ts) return 'an unknown date';
|
|
36
|
+
// Avoid Date formatting differences — just YYYY-MM-DD from the ISO string.
|
|
37
|
+
try {
|
|
38
|
+
return new Date(ts).toISOString().slice(0, 10);
|
|
39
|
+
} catch {
|
|
40
|
+
return 'an unknown date';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One short, searchable sentence per transaction. */
|
|
45
|
+
export function walletHistoryToDocuments(txs: WalletTx[]): RagDocument[] {
|
|
46
|
+
return txs.map((tx, i) => {
|
|
47
|
+
const date = isoDate(tx.timestamp);
|
|
48
|
+
const asset = tx.asset ?? 'sats';
|
|
49
|
+
const amount = tx.amountSats != null ? `${tx.amountSats} ${asset === 'BTC' ? 'sats' : asset}` : 'an amount';
|
|
50
|
+
const verb =
|
|
51
|
+
tx.type === 'receive' || tx.type === 'deposit'
|
|
52
|
+
? 'received'
|
|
53
|
+
: tx.type === 'swap'
|
|
54
|
+
? 'swapped'
|
|
55
|
+
: tx.type === 'withdraw'
|
|
56
|
+
? 'withdrew'
|
|
57
|
+
: 'sent';
|
|
58
|
+
const who = tx.counterparty ? ` ${verb === 'received' ? 'from' : 'to'} ${tx.counterparty}` : '';
|
|
59
|
+
const memo = tx.memo ? ` — "${tx.memo}"` : '';
|
|
60
|
+
const status = tx.status && tx.status !== 'complete' ? ` (${tx.status})` : '';
|
|
61
|
+
return {
|
|
62
|
+
id: tx.id ?? `tx_${i}`,
|
|
63
|
+
text: `On ${date} you ${verb} ${amount}${who}${memo}${status}.`,
|
|
64
|
+
metadata: { kind: 'transaction', type: tx.type, asset: tx.asset, timestamp: tx.timestamp },
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** One doc per contact, so "who is Bob?" / "pay my friend" can resolve. */
|
|
70
|
+
export function contactsToDocuments(contacts: Contact[]): RagDocument[] {
|
|
71
|
+
return contacts
|
|
72
|
+
.filter((c) => c.name || c.lightningAddress || c.npub)
|
|
73
|
+
.map((c, i) => {
|
|
74
|
+
const parts: string[] = [];
|
|
75
|
+
if (c.lightningAddress) parts.push(`Lightning address ${c.lightningAddress}`);
|
|
76
|
+
if (c.npub) parts.push(`Nostr ${c.npub}`);
|
|
77
|
+
if (c.note) parts.push(c.note);
|
|
78
|
+
return {
|
|
79
|
+
id: `contact_${c.name ?? c.lightningAddress ?? i}`,
|
|
80
|
+
text: `Contact: ${c.name ?? 'unnamed'}${parts.length ? ` — ${parts.join(', ')}` : ''}.`,
|
|
81
|
+
metadata: { kind: 'contact', name: c.name },
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|