@kaleidorg/mind 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/dist/capabilities.d.ts +4 -0
  2. package/dist/capabilities.d.ts.map +1 -1
  3. package/dist/capabilities.js +7 -0
  4. package/dist/capabilities.js.map +1 -1
  5. package/dist/engine.d.ts +9 -0
  6. package/dist/engine.d.ts.map +1 -1
  7. package/dist/engine.js +1 -0
  8. package/dist/engine.js.map +1 -1
  9. package/dist/funnel.d.ts +6 -0
  10. package/dist/funnel.d.ts.map +1 -1
  11. package/dist/funnel.js +26 -6
  12. package/dist/funnel.js.map +1 -1
  13. package/dist/index.d.ts +9 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +8 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/kaleidoswap/contract.d.ts +72 -0
  18. package/dist/kaleidoswap/contract.d.ts.map +1 -0
  19. package/dist/kaleidoswap/contract.js +125 -0
  20. package/dist/kaleidoswap/contract.js.map +1 -0
  21. package/dist/knowledge/btc-map.d.ts +87 -0
  22. package/dist/knowledge/btc-map.d.ts.map +1 -0
  23. package/dist/knowledge/btc-map.js +365 -0
  24. package/dist/knowledge/btc-map.js.map +1 -0
  25. package/dist/lsps1/contract.d.ts +55 -0
  26. package/dist/lsps1/contract.d.ts.map +1 -0
  27. package/dist/lsps1/contract.js +91 -0
  28. package/dist/lsps1/contract.js.map +1 -0
  29. package/dist/memory/store.d.ts +7 -1
  30. package/dist/memory/store.d.ts.map +1 -1
  31. package/dist/memory/store.js +43 -3
  32. package/dist/memory/store.js.map +1 -1
  33. package/dist/memory/types.d.ts +12 -0
  34. package/dist/memory/types.d.ts.map +1 -1
  35. package/dist/qvac/assistant.d.ts +73 -0
  36. package/dist/qvac/assistant.d.ts.map +1 -0
  37. package/dist/qvac/assistant.js +97 -0
  38. package/dist/qvac/assistant.js.map +1 -0
  39. package/dist/qvac/config.d.ts +64 -0
  40. package/dist/qvac/config.d.ts.map +1 -0
  41. package/dist/qvac/config.js +71 -0
  42. package/dist/qvac/config.js.map +1 -0
  43. package/dist/qvac/delegate.d.ts +48 -0
  44. package/dist/qvac/delegate.d.ts.map +1 -0
  45. package/dist/qvac/delegate.js +51 -0
  46. package/dist/qvac/delegate.js.map +1 -0
  47. package/dist/qvac/index.d.ts +19 -0
  48. package/dist/qvac/index.d.ts.map +1 -0
  49. package/dist/qvac/index.js +19 -0
  50. package/dist/qvac/index.js.map +1 -0
  51. package/dist/qvac/parse.d.ts +44 -0
  52. package/dist/qvac/parse.d.ts.map +1 -0
  53. package/dist/qvac/parse.js +28 -0
  54. package/dist/qvac/parse.js.map +1 -0
  55. package/dist/qvac/provider.d.ts +49 -0
  56. package/dist/qvac/provider.d.ts.map +1 -0
  57. package/dist/qvac/provider.js +68 -0
  58. package/dist/qvac/provider.js.map +1 -0
  59. package/dist/qvac/stream.d.ts +37 -0
  60. package/dist/qvac/stream.d.ts.map +1 -0
  61. package/dist/qvac/stream.js +29 -0
  62. package/dist/qvac/stream.js.map +1 -0
  63. package/dist/qvac/text.d.ts +19 -0
  64. package/dist/qvac/text.d.ts.map +1 -0
  65. package/dist/qvac/text.js +56 -0
  66. package/dist/qvac/text.js.map +1 -0
  67. package/dist/qvac/voice.d.ts +69 -0
  68. package/dist/qvac/voice.d.ts.map +1 -0
  69. package/dist/qvac/voice.js +51 -0
  70. package/dist/qvac/voice.js.map +1 -0
  71. package/dist/recipe/kaleidoswap-atomic.d.ts +27 -0
  72. package/dist/recipe/kaleidoswap-atomic.d.ts.map +1 -0
  73. package/dist/recipe/kaleidoswap-atomic.js +111 -0
  74. package/dist/recipe/kaleidoswap-atomic.js.map +1 -0
  75. package/dist/recipe/runner.d.ts.map +1 -1
  76. package/dist/recipe/runner.js +13 -1
  77. package/dist/recipe/runner.js.map +1 -1
  78. package/dist/skills/registry.d.ts.map +1 -1
  79. package/dist/skills/registry.js +20 -2
  80. package/dist/skills/registry.js.map +1 -1
  81. package/dist/wallet/confirm.d.ts +12 -0
  82. package/dist/wallet/confirm.d.ts.map +1 -0
  83. package/dist/wallet/confirm.js +67 -0
  84. package/dist/wallet/confirm.js.map +1 -0
  85. package/package.json +16 -1
  86. package/skills/README.md +6 -1
  87. package/skills/kaleido-lsps/SKILL.md +56 -0
  88. package/skills/kaleido-trading/SKILL.md +85 -18
  89. package/skills/merchant-finder/SKILL.md +87 -0
  90. package/skills/paid-data/SKILL.md +12 -0
  91. package/skills/wallet-assistant/SKILL.md +38 -0
  92. package/src/capabilities.ts +12 -0
  93. package/src/context/context.test.ts +6 -2
  94. package/src/engine.ts +6 -0
  95. package/src/funnel.ts +32 -7
  96. package/src/index.ts +43 -0
  97. package/src/kaleidoswap/contract.test.ts +147 -0
  98. package/src/kaleidoswap/contract.ts +212 -0
  99. package/src/knowledge/btc-map.test.ts +188 -0
  100. package/src/knowledge/btc-map.ts +446 -0
  101. package/src/lsps1/contract.test.ts +81 -0
  102. package/src/lsps1/contract.ts +132 -0
  103. package/src/memory/memory.test.ts +55 -0
  104. package/src/memory/store.ts +49 -4
  105. package/src/memory/types.ts +13 -0
  106. package/src/qvac/assistant.test.ts +132 -0
  107. package/src/qvac/assistant.ts +146 -0
  108. package/src/qvac/config.test.ts +44 -0
  109. package/src/qvac/config.ts +76 -0
  110. package/src/qvac/delegate.test.ts +68 -0
  111. package/src/qvac/delegate.ts +71 -0
  112. package/src/qvac/index.ts +72 -0
  113. package/src/qvac/parse.test.ts +52 -0
  114. package/src/qvac/parse.ts +57 -0
  115. package/src/qvac/provider.test.ts +107 -0
  116. package/src/qvac/provider.ts +124 -0
  117. package/src/qvac/stream.test.ts +79 -0
  118. package/src/qvac/stream.ts +56 -0
  119. package/src/qvac/text.test.ts +70 -0
  120. package/src/qvac/text.ts +60 -0
  121. package/src/qvac/voice.test.ts +151 -0
  122. package/src/qvac/voice.ts +122 -0
  123. package/src/recipe/kaleidoswap-atomic.test.ts +138 -0
  124. package/src/recipe/kaleidoswap-atomic.ts +117 -0
  125. package/src/recipe/runner.ts +13 -1
  126. package/src/skills/registry.ts +21 -2
  127. package/src/skills/skills.test.ts +42 -0
  128. package/src/wallet/confirm.test.ts +57 -0
  129. package/src/wallet/confirm.ts +74 -0
  130. package/skills/kaleido-wallet/SKILL.md +0 -28
package/src/funnel.ts CHANGED
@@ -34,13 +34,31 @@ import type { LLMProvider } from './providers/types.js';
34
34
  import type { ConfirmDecision, Message, ToolResult } from './types.js';
35
35
 
36
36
  /** Base system prompt for the wallet assistant. Hosts may override. */
37
- export const DEFAULT_WALLET_SYSTEM =
38
- 'You are KaleidoSwap, a concise, privacy-first assistant running inside a ' +
39
- 'non-custodial Bitcoin, Lightning and RGB wallet. Use the provided tools to ' +
40
- 'take actions: pay invoices and contacts, create invoices, check balances. ' +
41
- 'Never invent a balance, address, amount or result — always call the ' +
42
- 'relevant tool and report what it returns. All BTC amounts are in satoshis. ' +
43
- 'Keep replies short and friendly.';
37
+ export const DEFAULT_WALLET_SYSTEM = [
38
+ 'You are KaleidoSwap, a concise, privacy-first assistant running inside a',
39
+ 'non-custodial Bitcoin, Lightning and RGB wallet.',
40
+ '',
41
+ 'CORE RULES (these override every skill instruction):',
42
+ "1. If a tool can answer the user's question, CALL IT. Never describe how a",
43
+ " tool works (\"the pairs are listed using kaleidoswap_get_pairs\") — calling",
44
+ ' the tool IS the answer.',
45
+ '2. Never invent a balance, address, amount, price, quote, fee, pair, or any',
46
+ " other value. Every number or identifier in your reply MUST come from a tool",
47
+ ' result returned in the CURRENT turn.',
48
+ '3. Never reuse a number, name, or detail from a previous turn unless the user',
49
+ ' is explicitly asking about that earlier result. Each new question gets a',
50
+ ' fresh tool call.',
51
+ '4. If a tool needs a required argument the user did not give (e.g. an amount',
52
+ " for a quote), ASK for it. Do not invent values. Do not call the tool with",
53
+ ' the required field missing.',
54
+ '5. All BTC amounts are in satoshis. Asset codes are case-insensitive but the',
55
+ ' canonical forms are BTC, USDT, XAUT — do not silently shorten to USD, XAU.',
56
+ '',
57
+ 'Keep replies short and friendly. When a tool returns multiple fields, surface',
58
+ "the ones that matter — never collapse a structured result to a single number",
59
+ 'when other fields are non-zero or safety-relevant (e.g. pending balances,',
60
+ 'fees, slippage).',
61
+ ].join('\n');
44
62
 
45
63
  /** Tools that stay available even when a skill narrows the set. */
46
64
  const AMBIENT_MEMORY = ['remember', 'recall'];
@@ -74,6 +92,12 @@ export interface FunnelCallbacks {
74
92
  call: { name: string; arguments: Record<string, unknown> },
75
93
  info: { requiresConfirmation: boolean },
76
94
  ) => void;
95
+ /** A tool returned a result (agentic tier). Errors arrive as `{error}`. */
96
+ onToolResult?: (event: {
97
+ name: string;
98
+ arguments: Record<string, unknown>;
99
+ result: unknown;
100
+ }) => void;
77
101
  onConfirm?: (call: { name: string; arguments: Record<string, unknown> }) => Promise<ConfirmDecision>;
78
102
  }
79
103
 
@@ -253,6 +277,7 @@ export class Funnel {
253
277
  .then((def) => cbs.onToolCall?.(call, { requiresConfirmation: !!def?.requiresConfirmation }))
254
278
  .catch(() => cbs.onToolCall?.(call, { requiresConfirmation: false }));
255
279
  },
280
+ onToolResult: cbs.onToolResult,
256
281
  onConfirm: cbs.onConfirm,
257
282
  });
258
283
  return { text: res.text ?? '', tier: 'agentic', toolCalls: res.toolCalls, turns: res.turns };
package/src/index.ts CHANGED
@@ -49,6 +49,40 @@ export type {
49
49
  WalletHandler,
50
50
  BindWalletOptions,
51
51
  } from './wallet/contract.js';
52
+ export { confirmReadback } from './wallet/confirm.js';
53
+
54
+ // ── KaleidoSwap maker tool contract (single source of truth) ────────────────
55
+ export {
56
+ KALEIDOSWAP_TOOLS,
57
+ KALEIDOSWAP_SPEND_TOOLS,
58
+ isKaleidoswapSpendTool,
59
+ getKaleidoswapTool,
60
+ kaleidoswapTools,
61
+ bindKaleidoswapTools,
62
+ } from './kaleidoswap/contract.js';
63
+ export type {
64
+ KaleidoswapGroup,
65
+ KaleidoswapToolDef,
66
+ KaleidoswapHandler,
67
+ BindKaleidoswapOptions,
68
+ } from './kaleidoswap/contract.js';
69
+
70
+ // ── LSPS1 (Lightning Service Provider channel orders) ───────────────────────
71
+ export {
72
+ LSPS1_TOOLS,
73
+ LSPS1_SPEND_TOOLS,
74
+ isLsps1SpendTool,
75
+ getLsps1Tool,
76
+ bindLsps1Tools,
77
+ } from './lsps1/contract.js';
78
+ export type {
79
+ Lsps1ToolDef,
80
+ Lsps1Handler,
81
+ BindLsps1Options,
82
+ } from './lsps1/contract.js';
83
+
84
+ // ── KaleidoSwap atomic-swap recipe (opt-in — register via Funnel.recipes) ──
85
+ export { kaleidoswapAtomicRecipe } from './recipe/kaleidoswap-atomic.js';
52
86
 
53
87
  // ── Recipes (mobile multi-step: "recipes, not planning") ───────────────────
54
88
  export { runRecipe, extractSlots, RecipeRegistry } from './recipe/runner.js';
@@ -69,6 +103,7 @@ export type { MemoryStoreOptions } from './memory/store.js';
69
103
  export { createMemoryToolSource } from './memory/tool.js';
70
104
  export type {
71
105
  AgentProfile,
106
+ MemoryConsolidation,
72
107
  MemoryItem,
73
108
  MemoryKind,
74
109
  MemoryQuery,
@@ -110,6 +145,14 @@ export { walletHistoryToDocuments, contactsToDocuments } from './knowledge/walle
110
145
  export type { WalletTx, Contact } from './knowledge/wallet.js';
111
146
  export { merchantsToDocuments } from './knowledge/merchants.js';
112
147
  export type { Merchant } from './knowledge/merchants.js';
148
+ export { createBtcMapToolSource, BTC_MAP_SAMPLE } from './knowledge/btc-map.js';
149
+ export type {
150
+ BtcMapToolOptions,
151
+ BtcMapMerchant,
152
+ BtcMapFetch,
153
+ LocationProvider,
154
+ LatLng,
155
+ } from './knowledge/btc-map.js';
113
156
 
114
157
  export { Engine } from './engine.js';
115
158
  export type { EngineOptions, AgenticOptions, AgenticResult } from './engine.js';
@@ -0,0 +1,147 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ KALEIDOSWAP_TOOLS,
4
+ KALEIDOSWAP_SPEND_TOOLS,
5
+ isKaleidoswapSpendTool,
6
+ getKaleidoswapTool,
7
+ kaleidoswapTools,
8
+ bindKaleidoswapTools,
9
+ type KaleidoswapHandler,
10
+ } from './contract.js';
11
+
12
+ describe('KALEIDOSWAP_TOOLS — shape invariants', () => {
13
+ it('exposes the expected tool names', () => {
14
+ const names = KALEIDOSWAP_TOOLS.map((t) => t.name);
15
+ expect(names).toEqual([
16
+ 'kaleidoswap_get_assets',
17
+ 'kaleidoswap_get_pairs',
18
+ 'kaleidoswap_get_quote',
19
+ 'kaleidoswap_get_nodeinfo',
20
+ 'kaleidoswap_place_order',
21
+ 'kaleidoswap_get_order_status',
22
+ 'kaleidoswap_get_order_history',
23
+ 'kaleidoswap_atomic_init',
24
+ 'kaleidoswap_atomic_execute',
25
+ 'kaleidoswap_atomic_status',
26
+ ]);
27
+ });
28
+
29
+ it('every tool has a group and a parameters object', () => {
30
+ for (const t of KALEIDOSWAP_TOOLS) {
31
+ expect(['market', 'orders', 'atomic']).toContain(t.group);
32
+ expect(t.parameters).toBeDefined();
33
+ expect((t.parameters as any).type).toBe('object');
34
+ }
35
+ });
36
+
37
+ it('marks every spend tool as requiresConfirmation', () => {
38
+ for (const t of KALEIDOSWAP_TOOLS) {
39
+ expect(!!t.spend).toBe(!!t.requiresConfirmation);
40
+ }
41
+ });
42
+
43
+ it('lists every spend tool exactly once in KALEIDOSWAP_SPEND_TOOLS', () => {
44
+ const expected = KALEIDOSWAP_TOOLS.filter((t) => t.spend).map((t) => t.name).sort();
45
+ expect([...KALEIDOSWAP_SPEND_TOOLS].sort()).toEqual(expected);
46
+ // Sanity: place_order, atomic_init, atomic_execute are spend; the rest aren't.
47
+ expect(expected).toEqual([
48
+ 'kaleidoswap_atomic_execute',
49
+ 'kaleidoswap_atomic_init',
50
+ 'kaleidoswap_place_order',
51
+ ]);
52
+ });
53
+
54
+ it('isKaleidoswapSpendTool agrees with the set', () => {
55
+ expect(isKaleidoswapSpendTool('kaleidoswap_place_order')).toBe(true);
56
+ expect(isKaleidoswapSpendTool('kaleidoswap_get_pairs')).toBe(false);
57
+ expect(isKaleidoswapSpendTool('not_a_tool')).toBe(false);
58
+ });
59
+
60
+ it('getKaleidoswapTool returns by name', () => {
61
+ expect(getKaleidoswapTool('kaleidoswap_get_quote')?.group).toBe('market');
62
+ expect(getKaleidoswapTool('nope')).toBeUndefined();
63
+ });
64
+ });
65
+
66
+ describe('kaleidoswapTools(groups)', () => {
67
+ it('returns all tools when no group filter', () => {
68
+ expect(kaleidoswapTools().length).toBe(KALEIDOSWAP_TOOLS.length);
69
+ });
70
+
71
+ it('filters by group', () => {
72
+ const market = kaleidoswapTools({ groups: ['market'] });
73
+ expect(market.every((t) => t.group === 'market')).toBe(true);
74
+ expect(market.map((t) => t.name)).toContain('kaleidoswap_get_quote');
75
+ expect(market.map((t) => t.name)).not.toContain('kaleidoswap_place_order');
76
+ });
77
+
78
+ it('combines multiple groups', () => {
79
+ const readPlusOrders = kaleidoswapTools({ groups: ['market', 'orders'] });
80
+ expect(readPlusOrders.some((t) => t.name === 'kaleidoswap_atomic_init')).toBe(false);
81
+ expect(readPlusOrders.some((t) => t.name === 'kaleidoswap_place_order')).toBe(true);
82
+ });
83
+ });
84
+
85
+ describe('bindKaleidoswapTools', () => {
86
+ // Handlers that just echo their args so we can verify wiring.
87
+ const echoHandlers = (): Record<string, KaleidoswapHandler> => ({
88
+ kaleidoswap_get_assets: async () => ({ ok: true, tool: 'get_assets' }),
89
+ kaleidoswap_get_pairs: async () => ({ ok: true, tool: 'get_pairs' }),
90
+ kaleidoswap_get_quote: async (a) => ({ ok: true, tool: 'get_quote', args: a }),
91
+ kaleidoswap_get_nodeinfo: async () => ({ ok: true, tool: 'get_nodeinfo' }),
92
+ kaleidoswap_place_order: async (a) => ({ ok: true, tool: 'place_order', args: a }),
93
+ kaleidoswap_get_order_status: async (a) => ({ ok: true, tool: 'get_order_status', args: a }),
94
+ kaleidoswap_get_order_history: async (a) => ({ ok: true, tool: 'get_order_history', args: a }),
95
+ kaleidoswap_atomic_init: async (a) => ({ ok: true, tool: 'atomic_init', args: a }),
96
+ kaleidoswap_atomic_execute: async (a) => ({ ok: true, tool: 'atomic_execute', args: a }),
97
+ kaleidoswap_atomic_status: async (a) => ({ ok: true, tool: 'atomic_status', args: a }),
98
+ });
99
+
100
+ it('binds every tool when all handlers are present', () => {
101
+ const src = bindKaleidoswapTools(echoHandlers());
102
+ const tools = src.listTools();
103
+ expect(tools.length).toBe(KALEIDOSWAP_TOOLS.length);
104
+ });
105
+
106
+ it('preserves descriptions and the spend gate (requiresConfirmation)', () => {
107
+ const src = bindKaleidoswapTools(echoHandlers());
108
+ const place = src.listTools().find((t) => t.name === 'kaleidoswap_place_order');
109
+ const pairs = src.listTools().find((t) => t.name === 'kaleidoswap_get_pairs');
110
+ expect(place?.requiresConfirmation).toBe(true);
111
+ expect(pairs?.requiresConfirmation).toBeFalsy();
112
+ });
113
+
114
+ it('dispatches execute() to the right handler with args', async () => {
115
+ const src = bindKaleidoswapTools(echoHandlers());
116
+ const r = await src.execute('kaleidoswap_get_quote', { from_asset: 'BTC', to_asset: 'USDT', amount: 100_000 });
117
+ expect(r).toMatchObject({ ok: true, tool: 'get_quote', args: { from_asset: 'BTC', amount: 100_000 } });
118
+ });
119
+
120
+ it('throws when a handler is missing and allowMissing is false', () => {
121
+ const handlers = { kaleidoswap_get_pairs: echoHandlers().kaleidoswap_get_pairs };
122
+ expect(() => bindKaleidoswapTools(handlers)).toThrow(/no handler/);
123
+ });
124
+
125
+ it('skips missing handlers when allowMissing is true', () => {
126
+ const handlers: Record<string, KaleidoswapHandler> = {
127
+ kaleidoswap_get_pairs: async () => ({ ok: true }),
128
+ kaleidoswap_get_quote: async () => ({ ok: true }),
129
+ };
130
+ const src = bindKaleidoswapTools(handlers, { allowMissing: true });
131
+ const names = src.listTools().map((t) => t.name);
132
+ expect(names).toEqual(['kaleidoswap_get_pairs', 'kaleidoswap_get_quote']);
133
+ });
134
+
135
+ it('respects the groups filter when binding', () => {
136
+ const src = bindKaleidoswapTools(echoHandlers(), { groups: ['market'] });
137
+ const names = src.listTools().map((t) => t.name);
138
+ expect(names).toContain('kaleidoswap_get_quote');
139
+ expect(names).not.toContain('kaleidoswap_place_order');
140
+ expect(names).not.toContain('kaleidoswap_atomic_init');
141
+ });
142
+
143
+ it('uses opts.id for the ToolSource id', () => {
144
+ const src = bindKaleidoswapTools(echoHandlers(), { id: 'maker-prod' });
145
+ expect(src.id).toBe('maker-prod');
146
+ });
147
+ });
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Canonical KaleidoSwap tool contract — the single source of truth for the
3
+ * agent-facing tools that drive the KaleidoSwap maker.
4
+ *
5
+ * Every surface implements THESE EXACT tools, only the transport differs:
6
+ * - mobile → in-process handlers over the WDK protocol package
7
+ * (`@kaleidorg/wdk-protocol-swap-kaleidoswap`) via `bindKaleidoswapTools`
8
+ * - desktop → HTTP / kaleido-mcp / kaleido-cli, also via `bindKaleidoswapTools`
9
+ * - eval → stub handlers, also via `bindKaleidoswapTools`
10
+ *
11
+ * Because the schemas are identical everywhere, skills are portable and the
12
+ * model comparison is honest. Tools are grouped (`market`, `orders`, `atomic`)
13
+ * so a host can expose a read-only subset for sandbox/eval modes.
14
+ *
15
+ * Spend tools (place an order, init/execute an atomic swap) carry
16
+ * `spend: true` → `requiresConfirmation: true`, so the Engine always pauses
17
+ * for the host's confirm gate before executing.
18
+ *
19
+ * Pure data — no deps, no fetch, RN-safe.
20
+ */
21
+
22
+ import type { ToolDef } from '../types.js';
23
+ import { InProcessToolSource } from '../tools/in-process.js';
24
+ import type { InProcessTool } from '../tools/in-process.js';
25
+
26
+ /** Functional grouping for selective binding (e.g. read-only sandbox). */
27
+ export type KaleidoswapGroup = 'market' | 'orders' | 'atomic';
28
+
29
+ export interface KaleidoswapToolDef extends ToolDef {
30
+ /** Functional group — lets a host expose a subset. */
31
+ group: KaleidoswapGroup;
32
+ /** Moves funds → confirmation-gated. */
33
+ spend?: boolean;
34
+ }
35
+
36
+ type Props = Record<string, { type: string; description?: string; enum?: string[] }>;
37
+
38
+ function t(
39
+ group: KaleidoswapGroup,
40
+ name: string,
41
+ description: string,
42
+ properties: Props = {},
43
+ required: string[] = [],
44
+ spend = false,
45
+ ): KaleidoswapToolDef {
46
+ return {
47
+ group,
48
+ name,
49
+ description,
50
+ spend,
51
+ requiresConfirmation: spend,
52
+ parameters: { type: 'object', properties, required },
53
+ };
54
+ }
55
+
56
+ /**
57
+ * The canonical KaleidoSwap tool list. Schema is intentionally agent-facing —
58
+ * each host's adapter translates these args into the underlying transport's
59
+ * request body (maker REST JSON, WDK protocol calls, MCP, etc.).
60
+ */
61
+ export const KALEIDOSWAP_TOOLS: KaleidoswapToolDef[] = [
62
+ // ─── market (read) ─────────────────────────────────────────────────────
63
+ t('market',
64
+ 'kaleidoswap_get_assets',
65
+ 'List the assets KaleidoSwap supports — BTC plus the RGB assets the maker has inventory for (e.g. USDT, XAUT). Returns symbol, precision, and issuer/contract id. No args.'),
66
+
67
+ t('market',
68
+ 'kaleidoswap_get_pairs',
69
+ 'List the trading pairs currently quoted by the maker, with the latest bid/ask and the minimum/maximum executable size on each side. Use this before quoting to pick a valid pair. No args.'),
70
+
71
+ t('market',
72
+ 'kaleidoswap_get_quote',
73
+ 'Get an executable quote for swapping a specific amount on one pair. Returns a quote id (use it with place_order or atomic_init), the expected receive amount, fees, slippage, and how long the quote is valid for. Re-quote rather than reusing a stale id.',
74
+ {
75
+ from_asset: { type: 'string', description: 'Asset to spend, e.g. "BTC" or "USDT".' },
76
+ to_asset: { type: 'string', description: 'Asset to receive, e.g. "USDT" or "BTC".' },
77
+ amount: { type: 'number', description: 'Amount of from_asset to swap. BTC is in satoshis; RGB assets use their asset-defined precision.' },
78
+ side: { type: 'string', enum: ['buy', 'sell'], description: 'Default "sell" (you sell from_asset). Use "buy" only when from_asset is the quote currency you spend to acquire to_asset.' },
79
+ },
80
+ ['from_asset', 'to_asset', 'amount']),
81
+
82
+ t('market',
83
+ 'kaleidoswap_get_nodeinfo',
84
+ "Get info about the maker's Lightning node — pubkey, host, port, connect URI. Useful before opening a channel or when the user wants to see the counterparty. No args."),
85
+
86
+ // ─── orders (orderbook / market-order flow) ─────────────────────────────
87
+ t('orders',
88
+ 'kaleidoswap_place_order',
89
+ 'Place an order using an executable quote. Returns an order id. SPEND: the host pauses for user confirmation before the maker is called. Use only after kaleidoswap_get_quote and only when the user has explicitly approved the amount + destination.',
90
+ {
91
+ quote_id: { type: 'string', description: 'The quote id returned by kaleidoswap_get_quote (must still be valid).' },
92
+ },
93
+ ['quote_id'],
94
+ /* spend */ true),
95
+
96
+ t('orders',
97
+ 'kaleidoswap_get_order_status',
98
+ 'Check the status of an order by id — pending / settling / completed / failed. Poll this after place_order until the order settles.',
99
+ {
100
+ order_id: { type: 'string', description: 'The order id returned by kaleidoswap_place_order.' },
101
+ },
102
+ ['order_id']),
103
+
104
+ t('orders',
105
+ 'kaleidoswap_get_order_history',
106
+ "Get the user's recent KaleidoSwap orders for context (last N, paginated). Read-only.",
107
+ {
108
+ limit: { type: 'number', description: 'Max rows (default 20, max 100).' },
109
+ cursor: { type: 'string', description: 'Pagination cursor from a previous call.' },
110
+ }),
111
+
112
+ // ─── atomic (the trust-minimised swap chain — used by the recipe) ───────
113
+ t('atomic',
114
+ 'kaleidoswap_atomic_init',
115
+ "Initialise an atomic swap from a quote. Requires the receiver's RGB/LN invoice so the maker can lock the outgoing leg. SPEND: confirmation-gated. Returns the maker's invoice for the user to pay and an atomic id to track.",
116
+ {
117
+ quote_id: { type: 'string', description: 'The quote id from kaleidoswap_get_quote.' },
118
+ receive_invoice: { type: 'string', description: "The user's RGB or Lightning invoice for to_asset, created on the user's own node." },
119
+ },
120
+ ['quote_id', 'receive_invoice'],
121
+ /* spend */ true),
122
+
123
+ t('atomic',
124
+ 'kaleidoswap_atomic_execute',
125
+ "Tell the maker to release the receive leg now that the user has paid the maker's invoice. SPEND: confirmation-gated (committing the swap). Returns an updated atomic status.",
126
+ {
127
+ atomic_id: { type: 'string', description: 'The atomic id from kaleidoswap_atomic_init.' },
128
+ },
129
+ ['atomic_id'],
130
+ /* spend */ true),
131
+
132
+ t('atomic',
133
+ 'kaleidoswap_atomic_status',
134
+ 'Poll the status of an atomic swap — pending_payment / paid / settling / completed / failed / expired. Use this in a loop after execute until it terminates.',
135
+ {
136
+ atomic_id: { type: 'string', description: 'The atomic id from kaleidoswap_atomic_init.' },
137
+ },
138
+ ['atomic_id']),
139
+ ];
140
+
141
+ /** All tool names that move funds (confirmation-gated). */
142
+ export const KALEIDOSWAP_SPEND_TOOLS: Set<string> = new Set(
143
+ KALEIDOSWAP_TOOLS.filter((t) => t.spend).map((t) => t.name),
144
+ );
145
+
146
+ /** Quick lookup. */
147
+ export function isKaleidoswapSpendTool(name: string): boolean {
148
+ return KALEIDOSWAP_SPEND_TOOLS.has(name);
149
+ }
150
+
151
+ /** Quick lookup. */
152
+ export function getKaleidoswapTool(name: string): KaleidoswapToolDef | undefined {
153
+ return KALEIDOSWAP_TOOLS.find((t) => t.name === name);
154
+ }
155
+
156
+ /** Pick the contract tools for the given groups (all by default). */
157
+ export function kaleidoswapTools(
158
+ opts: { groups?: KaleidoswapGroup[] } = {},
159
+ ): KaleidoswapToolDef[] {
160
+ if (!opts.groups) return [...KALEIDOSWAP_TOOLS];
161
+ const groups = new Set(opts.groups);
162
+ return KALEIDOSWAP_TOOLS.filter((x) => groups.has(x.group));
163
+ }
164
+
165
+ /** A handler bound to one contract tool. Args validated by JSON schema upstream. */
166
+ export type KaleidoswapHandler = (args: Record<string, unknown>) => Promise<unknown>;
167
+
168
+ export interface BindKaleidoswapOptions {
169
+ /** Restrict the surface to a subset (e.g. read-only on eval). */
170
+ groups?: KaleidoswapGroup[];
171
+ /** Skip tools that have no handler instead of throwing (default false). */
172
+ allowMissing?: boolean;
173
+ /** ToolSource id for the registry (default 'kaleidoswap'). */
174
+ id?: string;
175
+ }
176
+
177
+ /**
178
+ * Bind contract tools to in-process handlers → an InProcessToolSource.
179
+ *
180
+ * The host is responsible for the actual transport (HTTP/WDK/CLI/MCP) — this
181
+ * function is a pure shape adapter that preserves names, descriptions,
182
+ * parameter schemas, and the spend gate.
183
+ *
184
+ * const source = bindKaleidoswapTools({
185
+ * kaleidoswap_get_quote: async (args) => makerSdk.quote(args),
186
+ * kaleidoswap_place_order: async ({ quote_id }) => makerSdk.placeOrder({ quoteId: quote_id }),
187
+ * // …
188
+ * });
189
+ * tools.register(source);
190
+ */
191
+ export function bindKaleidoswapTools(
192
+ handlers: Record<string, KaleidoswapHandler>,
193
+ opts: BindKaleidoswapOptions = {},
194
+ ): InProcessToolSource {
195
+ const defs = kaleidoswapTools(opts);
196
+ const bound: InProcessTool[] = [];
197
+ for (const def of defs) {
198
+ const handler = handlers[def.name];
199
+ if (!handler) {
200
+ if (opts.allowMissing) continue;
201
+ throw new Error(`bindKaleidoswapTools: no handler for "${def.name}"`);
202
+ }
203
+ bound.push({
204
+ name: def.name,
205
+ description: def.description,
206
+ parameters: def.parameters,
207
+ requiresConfirmation: def.requiresConfirmation,
208
+ handler,
209
+ });
210
+ }
211
+ return new InProcessToolSource(opts.id ?? 'kaleidoswap', bound);
212
+ }
@@ -0,0 +1,188 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ createBtcMapToolSource,
4
+ BTC_MAP_SAMPLE,
5
+ type BtcMapFetch,
6
+ type LocationProvider,
7
+ type BtcMapMerchant,
8
+ } from './btc-map.js';
9
+ import type { Merchant } from './merchants.js';
10
+
11
+ const exec = async (
12
+ src: ReturnType<typeof createBtcMapToolSource>,
13
+ name: string,
14
+ args: Record<string, unknown> = {},
15
+ ): Promise<any> => src.execute(name, args);
16
+
17
+ describe('createBtcMapToolSource — tool surface', () => {
18
+ it('exposes find_merchant_locations and get_merchant_info', () => {
19
+ const src = createBtcMapToolSource();
20
+ const names = src.listTools().map((t) => t.name);
21
+ expect(names).toEqual(['find_merchant_locations', 'get_merchant_info']);
22
+ expect(src.has('find_merchant_locations')).toBe(true);
23
+ expect(src.has('get_merchant_info')).toBe(true);
24
+ expect(src.has('find_merchants')).toBe(false); // legacy name is gone
25
+ });
26
+
27
+ it('throws on unknown tool', async () => {
28
+ const src = createBtcMapToolSource();
29
+ await expect(exec(src, 'no_such_tool')).rejects.toThrow(/unknown tool/);
30
+ });
31
+ });
32
+
33
+ describe('find_merchant_locations — offline fallback', () => {
34
+ it('returns offline rows when no location and no fetch are injected', async () => {
35
+ const src = createBtcMapToolSource();
36
+ const r = await exec(src, 'find_merchant_locations', {});
37
+ expect(r.success).toBe(true);
38
+ expect(r.source).toBe('offline');
39
+ expect(r.precise_location).toBe(false);
40
+ expect(r.merchants.length).toBeGreaterThan(0);
41
+ expect(r.merchants.length).toBeLessThanOrEqual(10); // default limit
42
+ });
43
+
44
+ it('honours category filter on offline data', async () => {
45
+ const src = createBtcMapToolSource();
46
+ const r = await exec(src, 'find_merchant_locations', { category: 'bar' });
47
+ expect(r.merchants.every((m: any) => m.category === 'bar')).toBe(true);
48
+ // Sample has at least one bar (PubKey).
49
+ expect(r.merchants.length).toBeGreaterThan(0);
50
+ });
51
+
52
+ it('honours query filter on offline data', async () => {
53
+ const src = createBtcMapToolSource();
54
+ const r = await exec(src, 'find_merchant_locations', { query: 'PubKey' });
55
+ expect(r.merchants.some((m: any) => m.name === 'PubKey')).toBe(true);
56
+ });
57
+
58
+ it('clamps limit to 1–20 with a default of 10', async () => {
59
+ const src = createBtcMapToolSource();
60
+ const big = await exec(src, 'find_merchant_locations', { limit: 9999 });
61
+ expect(big.merchants.length).toBeLessThanOrEqual(20);
62
+ const tiny = await exec(src, 'find_merchant_locations', { limit: -5 });
63
+ expect(tiny.merchants.length).toBeGreaterThanOrEqual(1); // clamped to ≥1
64
+ });
65
+
66
+ it('caller can override the offline dataset', async () => {
67
+ const custom: Merchant[] = [
68
+ { id: 'only', name: 'Only Café', category: 'cafe', acceptedAssets: ['lightning'] },
69
+ ];
70
+ const src = createBtcMapToolSource({ offlineMerchants: custom });
71
+ const r = await exec(src, 'find_merchant_locations', {});
72
+ expect(r.merchants).toHaveLength(1);
73
+ expect(r.merchants[0].name).toBe('Only Café');
74
+ });
75
+ });
76
+
77
+ describe('find_merchant_locations — live path (host-injected fetch)', () => {
78
+ const point = { lat: 46.0, lng: 8.95 };
79
+ const locationOnly: LocationProvider = {
80
+ getCurrent: async () => ({ ...point, label: 'Lugano', precise: true }),
81
+ };
82
+
83
+ it('uses live fetch when location resolves', async () => {
84
+ const captured: any[] = [];
85
+ const fetchImpl: BtcMapFetch = async (q) => {
86
+ captured.push(q);
87
+ return [
88
+ { id: 1, name: 'Live Café', category: 'cafe', lat: 46.0, lng: 8.95, distance_m: 42,
89
+ acceptedAssets: ['lightning'] } satisfies BtcMapMerchant,
90
+ ];
91
+ };
92
+ const src = createBtcMapToolSource({ location: locationOnly, fetch: fetchImpl });
93
+ const r = await exec(src, 'find_merchant_locations', { query: 'café', radius_km: 3 });
94
+ expect(r.source).toBe('btcmap');
95
+ expect(r.precise_location).toBe(true);
96
+ expect(r.merchants[0].name).toBe('Live Café');
97
+ expect(r.merchants[0].distance_m).toBe(42);
98
+ expect(captured[0]).toMatchObject({
99
+ center: { lat: 46.0, lng: 8.95 },
100
+ radiusMeters: 3000,
101
+ query: 'café',
102
+ limit: 10,
103
+ });
104
+ });
105
+
106
+ it('geocodes near_address when provided', async () => {
107
+ const geocoded = { lat: 38.71, lng: -9.143 };
108
+ const seen: string[] = [];
109
+ const location: LocationProvider = {
110
+ getCurrent: async () => ({ lat: 46.0, lng: 8.95, precise: true }), // should NOT be used
111
+ geocode: async (addr) => {
112
+ seen.push(addr);
113
+ return geocoded;
114
+ },
115
+ };
116
+ const fetchImpl: BtcMapFetch = async (q) => [
117
+ { id: 'lis', name: 'Lisbon Place', lat: q.center.lat, lng: q.center.lng,
118
+ acceptedAssets: ['lightning'] } satisfies BtcMapMerchant,
119
+ ];
120
+ const src = createBtcMapToolSource({ location, fetch: fetchImpl });
121
+ const r = await exec(src, 'find_merchant_locations', { near_address: 'Lisbon' });
122
+ expect(seen).toEqual(['Lisbon']);
123
+ expect(r.merchants[0].lat).toBeCloseTo(geocoded.lat);
124
+ expect(r.merchants[0].lng).toBeCloseTo(geocoded.lng);
125
+ expect(r.precise_location).toBe(false); // came from geocode, not GPS
126
+ });
127
+
128
+ it('falls back to offline when live fetch throws', async () => {
129
+ const src = createBtcMapToolSource({
130
+ location: locationOnly,
131
+ fetch: async () => { throw new Error('btcmap is down'); },
132
+ });
133
+ const r = await exec(src, 'find_merchant_locations', {});
134
+ expect(r.source).toBe('offline');
135
+ expect(r.merchants.length).toBeGreaterThan(0);
136
+ });
137
+
138
+ it('falls back to offline when location resolves but fetch is missing', async () => {
139
+ const src = createBtcMapToolSource({ location: locationOnly });
140
+ const r = await exec(src, 'find_merchant_locations', {});
141
+ expect(r.source).toBe('offline'); // no fetch injected → no live path
142
+ });
143
+ });
144
+
145
+ describe('get_merchant_info', () => {
146
+ it('finds a merchant by id', async () => {
147
+ const src = createBtcMapToolSource();
148
+ const r = await exec(src, 'get_merchant_info', { merchant_id: 'nyc-pubkey' });
149
+ expect(r.success).toBe(true);
150
+ expect(r.merchant.name).toBe('PubKey');
151
+ expect(r.merchant.accepts_lightning).toBe(true);
152
+ });
153
+
154
+ it('finds a merchant by exact name', async () => {
155
+ const src = createBtcMapToolSource();
156
+ const r = await exec(src, 'get_merchant_info', { merchant_name: 'Bistro Libertine' });
157
+ expect(r.success).toBe(true);
158
+ expect(r.merchant.city).toBe('Lugano');
159
+ });
160
+
161
+ it('returns an error and possible suggestions when the name does not match', async () => {
162
+ const src = createBtcMapToolSource();
163
+ const r = await exec(src, 'get_merchant_info', { merchant_name: 'Nonexistent Merchant Name' });
164
+ expect(r.success).toBe(false);
165
+ expect(typeof r.error).toBe('string');
166
+ });
167
+
168
+ it('treats a fuzzy-close name as a hit (not an error)', async () => {
169
+ const src = createBtcMapToolSource();
170
+ const r = await exec(src, 'get_merchant_info', { merchant_name: 'Pubkey' }); // PubKey
171
+ expect(r.success).toBe(true);
172
+ expect(r.merchant.name).toBe('PubKey');
173
+ });
174
+
175
+ it('returns an error (no throw) when given nothing', async () => {
176
+ const src = createBtcMapToolSource();
177
+ const r = await exec(src, 'get_merchant_info', {});
178
+ expect(r.success).toBe(false);
179
+ });
180
+ });
181
+
182
+ describe('BTC_MAP_SAMPLE', () => {
183
+ it('has a row in Lugano with Lightning support (sanity)', () => {
184
+ const lugano = BTC_MAP_SAMPLE.filter((m) => m.city === 'Lugano');
185
+ expect(lugano.length).toBeGreaterThan(0);
186
+ expect(lugano.every((m) => m.acceptedAssets?.includes('lightning'))).toBe(true);
187
+ });
188
+ });