@kaleidorg/mind 0.9.0 → 0.10.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/engine/answer.d.ts +3 -0
- package/dist/engine/answer.d.ts.map +1 -1
- package/dist/engine/answer.js +19 -2
- package/dist/engine/answer.js.map +1 -1
- package/dist/engine/rgb-ticker.d.ts +17 -0
- package/dist/engine/rgb-ticker.d.ts.map +1 -0
- package/dist/engine/rgb-ticker.js +47 -0
- package/dist/engine/rgb-ticker.js.map +1 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +8 -2
- package/dist/engine.js.map +1 -1
- package/dist/guards.d.ts +12 -0
- package/dist/guards.d.ts.map +1 -1
- package/dist/guards.js +60 -0
- package/dist/guards.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/kaleidoswap/contract.d.ts +7 -0
- package/dist/kaleidoswap/contract.d.ts.map +1 -1
- package/dist/kaleidoswap/contract.js +71 -17
- package/dist/kaleidoswap/contract.js.map +1 -1
- package/dist/kaleidoswap/index.d.ts +1 -1
- package/dist/kaleidoswap/index.d.ts.map +1 -1
- package/dist/kaleidoswap/index.js +1 -1
- package/dist/kaleidoswap/index.js.map +1 -1
- package/dist/qvac/parse.d.ts.map +1 -1
- package/dist/qvac/parse.js +13 -0
- package/dist/qvac/parse.js.map +1 -1
- package/dist/qvac/provider.d.ts.map +1 -1
- package/dist/qvac/provider.js +127 -95
- package/dist/qvac/provider.js.map +1 -1
- package/dist/recipe/asset-send.js +1 -1
- package/dist/recipe/asset-send.js.map +1 -1
- package/dist/testing/mock-wallet.d.ts.map +1 -1
- package/dist/testing/mock-wallet.js +6 -0
- package/dist/testing/mock-wallet.js.map +1 -1
- package/dist/wallet/contract.d.ts +5 -0
- package/dist/wallet/contract.d.ts.map +1 -1
- package/dist/wallet/contract.js +39 -6
- package/dist/wallet/contract.js.map +1 -1
- package/package.json +1 -1
- package/scripts/snapshot-mcp-tools.mjs +38 -0
- package/skills/README.md +98 -64
- package/skills/bitrefill/SKILL.md +30 -157
- package/skills/channel-manager/SKILL.md +31 -52
- package/skills/flashnet-swaps/SKILL.md +24 -150
- package/skills/kaleido-node/SKILL.md +25 -55
- package/skills/kaleido-trading/SKILL.md +28 -172
- package/skills/kaleido-trading/references/assets.md +4 -4
- package/skills/kaleido-trading/references/atomic.md +5 -7
- package/skills/merchant-finder/SKILL.md +25 -108
- package/skills/paid-data/SKILL.md +25 -58
- package/skills/portfolio-manager/SKILL.md +26 -60
- package/skills/rgb-lightning-node/SKILL.md +37 -255
- package/skills/rgb-lightning-node/references/channels.md +34 -0
- package/skills/spark-wallet/SKILL.md +26 -228
- package/skills/submarine-swaps/SKILL.md +19 -37
- package/skills/wallet-assistant/SKILL.md +26 -44
- package/src/engine/answer.ts +26 -2
- package/src/engine/rgb-ticker.test.ts +29 -0
- package/src/engine/rgb-ticker.ts +54 -0
- package/src/engine.ts +16 -2
- package/src/funnel.mind.test.ts +6 -5
- package/src/guards.test.ts +39 -0
- package/src/guards.ts +61 -0
- package/src/index.ts +4 -0
- package/src/kaleidoswap/contract.test.ts +32 -3
- package/src/kaleidoswap/contract.ts +65 -17
- package/src/kaleidoswap/index.ts +1 -0
- package/src/qvac/parse.test.ts +14 -0
- package/src/qvac/parse.ts +11 -0
- package/src/qvac/provider.test.ts +33 -0
- package/src/qvac/provider.ts +37 -4
- package/src/recipe/asset-send.ts +1 -1
- package/src/recipe/recipe.test.ts +1 -1
- package/src/skills/catalog.test.ts +215 -0
- package/src/skills/mcp-tools.snapshot.json +1938 -0
- package/src/testing/mock-wallet.ts +6 -0
- package/src/wallet/contract.test.ts +20 -1
- package/src/wallet/contract.ts +36 -6
- package/skills/dca/SKILL.md +0 -48
- package/skills/kaleido-lsps/SKILL.md +0 -131
- package/skills/liquidity-optimizer/SKILL.md +0 -91
|
@@ -1,48 +1,30 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: submarine-swaps
|
|
3
|
-
description: "Pay a Lightning invoice from Liquid funds (L-USDT or L-BTC) through a KaleidoSwap submarine swap
|
|
3
|
+
description: "Pay a Lightning invoice from Liquid funds (L-USDT or L-BTC) through a KaleidoSwap submarine swap, and follow its status."
|
|
4
4
|
tools: kaleidoswap_submarine_pairs, kaleidoswap_submarine_create, kaleidoswap_submarine_fund, kaleidoswap_submarine_status
|
|
5
|
-
|
|
5
|
+
requires-tools: kaleidoswap_submarine_create
|
|
6
|
+
triggers: l-usdt, liquid usdt, usdt on liquid, l-btc, liquid bitcoin, submarine, submarine swap, pay invoice with liquid
|
|
6
7
|
metadata:
|
|
7
8
|
author: kaleidoswap
|
|
8
|
-
version: "0.
|
|
9
|
+
version: "0.2.0"
|
|
9
10
|
---
|
|
11
|
+
# Submarine swaps
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
The user locks Liquid funds; the maker pays the Lightning invoice, then claims
|
|
14
|
+
the lockup. If the maker can't pay, the lockup is refundable. Plain "USDT" is
|
|
15
|
+
RGB USDT (`kaleido-trading`), not this skill.
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
## Critical rules
|
|
18
|
-
|
|
19
|
-
- Every amount, fee and status in your reply MUST come from a tool result in the
|
|
20
|
-
CURRENT turn. Never guess the amount to lock — `kaleidoswap_submarine_create`
|
|
21
|
-
returns it.
|
|
22
|
-
- `kaleidoswap_submarine_fund` moves money. Call it ONLY after the user has
|
|
23
|
-
confirmed the exact `expected_amount` returned by `kaleidoswap_submarine_create`.
|
|
24
|
-
- `kaleidoswap_submarine_fund` takes only `swap_id`. Never try to pass an amount
|
|
25
|
-
or an address.
|
|
26
|
-
- A bare "USDT" means RGB USDT on the Lightning node, which uses the atomic swap
|
|
27
|
-
tools, not this skill. Use this skill only for Liquid assets (L-USDT, L-BTC).
|
|
28
|
-
|
|
29
|
-
## Flow
|
|
30
|
-
|
|
31
|
-
1. `kaleidoswap_submarine_create { invoice, from_asset }` — from_asset is
|
|
32
|
-
`L-USDT` (default) or `L-BTC`. Returns `swap_id`, `expected_amount` (smallest
|
|
33
|
-
unit: L-USDT has 8 decimals, L-BTC is sats) and the fees.
|
|
17
|
+
## Do
|
|
18
|
+
1. `kaleidoswap_submarine_create` (`invoice`, `from_asset`) — `from_asset` is
|
|
19
|
+
`L-USDT` (default) or `L-BTC`. Returns `swap_id`, `expected_amount`
|
|
20
|
+
(smallest unit, 8 decimals for both) and fees. Nothing moves yet.
|
|
34
21
|
2. Tell the user the amount and ask to confirm.
|
|
35
|
-
3. `kaleidoswap_submarine_fund
|
|
36
|
-
4. `kaleidoswap_submarine_status
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
Use `kaleidoswap_submarine_pairs` when the user asks what can pay a Lightning
|
|
41
|
-
invoice, or for the limits and fees.
|
|
22
|
+
3. `kaleidoswap_submarine_fund` (`swap_id` only; confirm-gated).
|
|
23
|
+
4. `kaleidoswap_submarine_status` (`swap_id`) — `transaction.claimed` means paid;
|
|
24
|
+
`failed: true` on a funded swap needs a refund: quote the `refund` field.
|
|
25
|
+
- Limits and fees: `kaleidoswap_submarine_pairs {}`.
|
|
42
26
|
|
|
43
27
|
## Examples
|
|
44
|
-
|
|
45
|
-
- "
|
|
46
|
-
|
|
47
|
-
- "did my Liquid payment go through? swap sub1" →
|
|
48
|
-
`kaleidoswap_submarine_status { swap_id: "sub1" }`
|
|
28
|
+
- "Pay lntbs10u1p… with L-USDT" → `kaleidoswap_submarine_create {"invoice":"lntbs10u1p…","from_asset":"L-USDT"}`
|
|
29
|
+
- "Did swap sub1 go through?" → `kaleidoswap_submarine_status {"swap_id":"sub1"}`
|
|
30
|
+
- "What can pay Lightning from Liquid?" → `kaleidoswap_submarine_pairs {}`
|
|
@@ -1,49 +1,31 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wallet-assistant
|
|
3
|
-
description: Everyday wallet tasks
|
|
4
|
-
tools: get_balances,
|
|
5
|
-
|
|
3
|
+
description: "Everyday wallet tasks in the app across all layers: balance, receive (invoice or address), send or pay, contacts, BTC price and fiat conversion."
|
|
4
|
+
tools: get_balances, create_invoice, send_payment, resolve_contact, get_price, fiat_to_sats, rln_list_assets, rln_pay_invoice, spark_pay_invoice
|
|
5
|
+
requires-tools: get_balances
|
|
6
|
+
triggers: balance, balances, pay, send, receive, address, invoice, contact, funds, money, price, how much
|
|
7
|
+
metadata:
|
|
8
|
+
author: kaleidoswap
|
|
9
|
+
version: "0.3.0"
|
|
6
10
|
---
|
|
7
|
-
|
|
8
11
|
# Wallet assistant
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
yourself.
|
|
30
|
-
|
|
31
|
-
## Rules
|
|
32
|
-
|
|
33
|
-
- **Balance / "how much do I have"** → use the balance tool, then state the
|
|
34
|
-
number.
|
|
35
|
-
- **Receive / "an invoice for N sats"** → use the invoice creation tool (for
|
|
36
|
-
the appropriate layer) with the amount.
|
|
37
|
-
- **"How many sats is N USDT?" / "What's 0.1 BTC in USDT?" / "convert N X
|
|
38
|
-
to Y"** → use the quote/conversion tool and
|
|
39
|
-
read the display fields from the result. Supported pairs: any of
|
|
40
|
-
BTC/USDT/XAUT against each other. Fiat (EUR/USD/GBP) is NOT quoted by
|
|
41
|
-
the maker — if the user asks "how much sats is 10 EUR", say so plainly
|
|
42
|
-
and offer USDT as the closest analogue (USDT is a USD-pegged stablecoin).
|
|
43
|
-
- **Pay a Lightning invoice** → use the lightning payment tool.
|
|
44
|
-
- **Send to a person/amount** → first resolve the contact, then use the send
|
|
45
|
-
payment tool with the amount in sats and the recipient. State the amount and
|
|
46
|
-
destination; the app asks the user to confirm before it sends.
|
|
47
|
-
|
|
48
|
-
Keep replies short, but never drop a balance component, a fee, or the
|
|
49
|
-
quote's `rfq_id` when present.
|
|
13
|
+
The app routes these tools to the right layer (Spark, RLN, Arkade, Liquid).
|
|
14
|
+
`amount_sats` is satoshis; other amounts are display units of the asset.
|
|
15
|
+
|
|
16
|
+
## Do
|
|
17
|
+
- Balance: `get_balances` (add `layer` for one layer). Report every component;
|
|
18
|
+
`pending` is not spendable yet.
|
|
19
|
+
- Receive: `create_invoice` with `asset` and optional `amount`; reply with the
|
|
20
|
+
full invoice or address returned.
|
|
21
|
+
- Send to a name: `resolve_contact` first, then `send_payment`. A BOLT11
|
|
22
|
+
invoice the user pasted: `rln_pay_invoice` or `spark_pay_invoice`. Every
|
|
23
|
+
send is confirm-gated; state amount and destination.
|
|
24
|
+
- Fiat: `fiat_to_sats` converts; `get_price` gives the BTC price.
|
|
25
|
+
- Swaps go to `kaleido-trading` (RGB) or `flashnet-swaps` (Spark tokens).
|
|
26
|
+
|
|
27
|
+
## Examples
|
|
28
|
+
- "What's my balance?" → `get_balances {}`
|
|
29
|
+
- "Invoice for 2000 sats" → `create_invoice {"asset":"BTC","amount":2000}`
|
|
30
|
+
- "Send 5000 sats to bob" → `resolve_contact {"name":"bob"}` then `send_payment {"to":"<bob's address>","amount_sats":5000}`
|
|
31
|
+
- "How many sats is 10 EUR?" → `fiat_to_sats {"amount":10,"currency":"EUR"}`
|
package/src/engine/answer.ts
CHANGED
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
* quotes a readback from being mistaken for an invented invoice.
|
|
9
9
|
*/
|
|
10
10
|
import type { ToolCallError } from '../providers/types.js';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
findUngroundedPaymentData,
|
|
13
|
+
fixSatsBtcConversions,
|
|
14
|
+
paymentStrings,
|
|
15
|
+
producedPaymentReply,
|
|
16
|
+
ungroundedReply,
|
|
17
|
+
type UngroundedItem,
|
|
18
|
+
} from '../guards.js';
|
|
12
19
|
import { fixRgbBalanceUnits } from '../context/rgb-units.js';
|
|
13
20
|
|
|
14
21
|
export interface Answer {
|
|
@@ -49,6 +56,8 @@ export interface FinalizeOptions {
|
|
|
49
56
|
sources: unknown[];
|
|
50
57
|
/** Tool results, for relabelling RGB balances. */
|
|
51
58
|
toolResults: unknown[];
|
|
59
|
+
/** Invoices and addresses receive tools returned this run (`producedPaymentData`). */
|
|
60
|
+
produced?: UngroundedItem[];
|
|
52
61
|
aborted: boolean;
|
|
53
62
|
}
|
|
54
63
|
|
|
@@ -59,8 +68,23 @@ export function finalizeAnswer(answer: Answer, opts: FinalizeOptions): string {
|
|
|
59
68
|
let text = answer.text;
|
|
60
69
|
if (opts.fixAmounts) text = fixRgbBalanceUnits(fixSatsBtcConversions(text), opts.toolResults);
|
|
61
70
|
if (opts.guardPaymentData) {
|
|
71
|
+
const produced = opts.produced ?? [];
|
|
72
|
+
const lower = text.toLowerCase();
|
|
73
|
+
const missing = produced.filter((p) => !lower.includes(p.value.toLowerCase()));
|
|
74
|
+
// A partial or garbled copy of a value a tool produced (it shares the
|
|
75
|
+
// value's start, or is cut out of it): show the tool's value instead.
|
|
76
|
+
const mangled = paymentStrings(text).some((f) => {
|
|
77
|
+
const v = f.value.toLowerCase();
|
|
78
|
+
return missing.some((p) => {
|
|
79
|
+
const want = p.value.toLowerCase();
|
|
80
|
+
return want.includes(v) || v.slice(0, 12) === want.slice(0, 12);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
if (mangled) return producedPaymentReply(produced);
|
|
62
84
|
const ungrounded = findUngroundedPaymentData(text, opts.sources);
|
|
63
|
-
if (ungrounded.length)
|
|
85
|
+
if (ungrounded.length) return produced.length ? producedPaymentReply(produced) : ungroundedReply(ungrounded);
|
|
86
|
+
// The model left out the invoice/address it was asked for: add it verbatim.
|
|
87
|
+
if (missing.length) text = `${text}\n\n${producedPaymentReply(missing)}`;
|
|
64
88
|
}
|
|
65
89
|
return text;
|
|
66
90
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { resolveRgbTicker } from './rgb-ticker.js';
|
|
3
|
+
|
|
4
|
+
const assets = [
|
|
5
|
+
{ asset_id: 'rgb:usdt-1', ticker: 'USDT', name: 'Tether USD' },
|
|
6
|
+
{ asset_id: 'rgb:xaut-1', ticker: 'XAUT', name: 'Tether Gold' },
|
|
7
|
+
];
|
|
8
|
+
const host = (result: unknown, tools = ['rln_list_assets']) => ({
|
|
9
|
+
hasTool: async (n: string) => tools.includes(n),
|
|
10
|
+
run: vi.fn(async () => result),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('resolveRgbTicker', () => {
|
|
14
|
+
it('replaces a ticker with the asset id from the same host', async () => {
|
|
15
|
+
const h = host(JSON.stringify(assets));
|
|
16
|
+
expect(await resolveRgbTicker('rln_send_asset', { asset_id: 'usdt', amount: 1 }, h)).toEqual({ asset_id: 'rgb:usdt-1', amount: 1 });
|
|
17
|
+
expect(h.run).toHaveBeenCalledWith('rln_list_assets', {});
|
|
18
|
+
expect(await resolveRgbTicker('rln_create_rgb_invoice', { asset: 'XAUT' }, host({ assets }))).toEqual({ asset: 'rgb:xaut-1' });
|
|
19
|
+
expect(await resolveRgbTicker('wdk_send_asset', { asset_id: 'USDT' }, host({ nia: assets }, ['wdk_list_assets']))).toEqual({ asset_id: 'rgb:usdt-1' });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('leaves ids, unknown tickers, other tools and hosts without a list tool alone', async () => {
|
|
23
|
+
const h = host(assets);
|
|
24
|
+
expect(await resolveRgbTicker('rln_send_asset', { asset_id: 'rgb:usdt-1' }, h)).toEqual({ asset_id: 'rgb:usdt-1' });
|
|
25
|
+
expect(await resolveRgbTicker('rln_send_asset', { asset_id: 'FOO' }, h)).toEqual({ asset_id: 'FOO' });
|
|
26
|
+
expect(await resolveRgbTicker('rln_issue_asset', { ticker: 'USDT' }, h)).toEqual({ ticker: 'USDT' });
|
|
27
|
+
expect(await resolveRgbTicker('rln_send_asset', { asset_id: 'USDT' }, host(assets, []))).toEqual({ asset_id: 'USDT' });
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RGB tools take an `asset_id` (`rgb:…`), but users say "USDT". Small models
|
|
3
|
+
* either look the id up first (an extra turn, and some stop after it) or pass
|
|
4
|
+
* the ticker. When a send / RGB-invoice call carries a ticker, resolve it here
|
|
5
|
+
* with the same host's `*_list_assets`, so one call does the job on every
|
|
6
|
+
* host (in-app contract or kaleido-mcp).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
type Obj = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
const NEEDS_ASSET_ID = /(^|_)(send_asset|create_rgb_invoice)$/;
|
|
12
|
+
const ASSET_KEYS = ['asset_id', 'asset'] as const;
|
|
13
|
+
|
|
14
|
+
function listedAssets(result: unknown): Obj[] {
|
|
15
|
+
let r = result;
|
|
16
|
+
if (typeof r === 'string') {
|
|
17
|
+
try {
|
|
18
|
+
r = JSON.parse(r);
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(r)) return r as Obj[];
|
|
24
|
+
if (!r || typeof r !== 'object') return [];
|
|
25
|
+
const o = r as Obj;
|
|
26
|
+
if (Array.isArray(o.assets)) return o.assets as Obj[];
|
|
27
|
+
return ['nia', 'cfa', 'uda', 'ifa'].flatMap((k) => (Array.isArray(o[k]) ? (o[k] as Obj[]) : []));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* `args` with a ticker in `asset_id` / `asset` replaced by the asset's id, or
|
|
32
|
+
* `args` unchanged (not an RGB call, already an id, no list tool, no single
|
|
33
|
+
* match).
|
|
34
|
+
*/
|
|
35
|
+
export async function resolveRgbTicker(
|
|
36
|
+
toolName: string,
|
|
37
|
+
args: Record<string, unknown>,
|
|
38
|
+
host: {
|
|
39
|
+
hasTool: (name: string) => Promise<boolean>;
|
|
40
|
+
run: (name: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
41
|
+
},
|
|
42
|
+
): Promise<Record<string, unknown>> {
|
|
43
|
+
if (!NEEDS_ASSET_ID.test(toolName)) return args;
|
|
44
|
+
const key = ASSET_KEYS.find((k) => typeof args[k] === 'string');
|
|
45
|
+
const ticker = key ? String(args[key]).trim() : '';
|
|
46
|
+
if (!key || !ticker || ticker.toLowerCase().startsWith('rgb:') || !/^[A-Za-z0-9]{1,12}$/.test(ticker)) return args;
|
|
47
|
+
|
|
48
|
+
const listTool = toolName.replace(NEEDS_ASSET_ID, '$1list_assets');
|
|
49
|
+
if (!(await host.hasTool(listTool))) return args;
|
|
50
|
+
const matches = listedAssets(await host.run(listTool, {})).filter(
|
|
51
|
+
(a) => typeof a.ticker === 'string' && a.ticker.toUpperCase() === ticker.toUpperCase() && typeof a.asset_id === 'string',
|
|
52
|
+
);
|
|
53
|
+
return matches.length === 1 ? { ...args, [key]: matches[0]!.asset_id } : args;
|
|
54
|
+
}
|
package/src/engine.ts
CHANGED
|
@@ -18,9 +18,18 @@ import type { ConfirmDecision, Message, ToolCall, ToolDef, ToolResult } from './
|
|
|
18
18
|
import type { InferenceMetrics, LLMProvider, ToolChoice } from './providers/types.js';
|
|
19
19
|
import type { ToolRegistry } from './tools/registry.js';
|
|
20
20
|
import { compressToolResult, type ToolCrushOptions } from './context/compress.js';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
callKey,
|
|
23
|
+
declinedToolResult,
|
|
24
|
+
detectWalletAction,
|
|
25
|
+
hasCapableTool,
|
|
26
|
+
noToolReply,
|
|
27
|
+
producedPaymentData,
|
|
28
|
+
validateToolArgs,
|
|
29
|
+
} from './guards.js';
|
|
22
30
|
import { confirmReadback } from './wallet/confirm.js';
|
|
23
31
|
import { annotateRgbBalances } from './context/rgb-units.js';
|
|
32
|
+
import { resolveRgbTicker } from './engine/rgb-ticker.js';
|
|
24
33
|
import {
|
|
25
34
|
REPEATED_CALL_REPLY,
|
|
26
35
|
TOOL_CALL_FAILED_REPLY,
|
|
@@ -279,6 +288,7 @@ export class Engine {
|
|
|
279
288
|
guardPaymentData: this.guardPaymentData,
|
|
280
289
|
sources: [...messages.map((m) => m.content), ...state.executed.map((e) => e.result)],
|
|
281
290
|
toolResults: state.executed.map((e) => e.result),
|
|
291
|
+
produced: producedPaymentData(state.executed),
|
|
282
292
|
aborted: !!opts.signal?.aborted,
|
|
283
293
|
});
|
|
284
294
|
// Append the final answer so the returned conversation is complete (the
|
|
@@ -346,7 +356,11 @@ export class Engine {
|
|
|
346
356
|
} else if (!def) {
|
|
347
357
|
result = { error: `Unknown tool "${call.name}".` };
|
|
348
358
|
} else {
|
|
349
|
-
const
|
|
359
|
+
const callArgs = await resolveRgbTicker(call.name, call.arguments, {
|
|
360
|
+
hasTool: async (name) => !!(await this.registry.getDef(name)),
|
|
361
|
+
run: (name, a) => this.safeExecute(name, a),
|
|
362
|
+
});
|
|
363
|
+
const check = validateToolArgs(def, callArgs);
|
|
350
364
|
if (!check.ok) {
|
|
351
365
|
result = {
|
|
352
366
|
error: `Invalid arguments for ${call.name}: ${check.errors.join('; ')}. Fix them or ask the user for the missing values.`,
|
package/src/funnel.mind.test.ts
CHANGED
|
@@ -318,10 +318,11 @@ describe('desktop mind — skill scoping (real skills)', () => {
|
|
|
318
318
|
);
|
|
319
319
|
});
|
|
320
320
|
|
|
321
|
-
it('wallet-assistant
|
|
321
|
+
it('wallet-assistant is the in-app router; the node skill carries the rln_* balance tools', () => {
|
|
322
322
|
const wallet = SKILLS.find((s) => s.name === 'wallet-assistant')!;
|
|
323
|
-
expect(wallet.tools).
|
|
324
|
-
|
|
323
|
+
expect(wallet.metadata?.['requires-tools']).toBe('get_balances');
|
|
324
|
+
const node = SKILLS.find((s) => s.name === 'rgb-lightning-node')!;
|
|
325
|
+
expect(node.tools).toEqual(expect.arrayContaining(['rln_get_balances', 'rln_get_address', 'rln_send_btc', 'rln_create_ln_invoice']));
|
|
325
326
|
});
|
|
326
327
|
|
|
327
328
|
it('rgb-lightning-node (triggers on "channels") exposes only canonical rln_* tools', () => {
|
|
@@ -361,9 +362,9 @@ describe('desktop mind — skill scoping (real skills)', () => {
|
|
|
361
362
|
const res = await funnel.runTurn("what's my balance?");
|
|
362
363
|
|
|
363
364
|
expect(res.tier).toBe('agentic');
|
|
364
|
-
//
|
|
365
|
+
// Node-only host: the RGB node skill is selected AND rln_get_balances survives its scoping…
|
|
365
366
|
const agenticLine = logs.find((l) => l.startsWith('tier=agentic'));
|
|
366
|
-
expect(agenticLine).toMatch(/skill=
|
|
367
|
+
expect(agenticLine).toMatch(/skill=rgb-lightning-node/);
|
|
367
368
|
expect(agenticLine).toMatch(/rln_get_balances/);
|
|
368
369
|
// …and the tool actually executes (not narrated).
|
|
369
370
|
expect(calls.map((c) => c.name)).toContain('rln_get_balances');
|
package/src/guards.test.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { InProcessToolSource } from './tools/in-process.js';
|
|
|
6
6
|
import { parseSkill, SkillRegistry } from './skills/registry.js';
|
|
7
7
|
import { scriptedProvider } from './testing/scripted-provider.js';
|
|
8
8
|
import { confirmReadback } from './wallet/confirm.js';
|
|
9
|
+
import { getKaleidoswapTool } from './kaleidoswap/contract.js';
|
|
9
10
|
import {
|
|
10
11
|
DECLINED_TOOL_MESSAGE,
|
|
11
12
|
detectWalletAction,
|
|
@@ -31,7 +32,18 @@ const ISSUE_SCHEMA = {
|
|
|
31
32
|
|
|
32
33
|
const INVOICE = 'lnbcrt50u1pn9xyzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypq';
|
|
33
34
|
|
|
35
|
+
const QUOTE = { name: 'kaleidoswap_get_quote', parameters: getKaleidoswapTool('kaleidoswap_get_quote')!.parameters };
|
|
36
|
+
|
|
34
37
|
describe('validateToolArgs', () => {
|
|
38
|
+
it('rejects sats passed as a BTC display amount and names the BTC value', () => {
|
|
39
|
+
const r = validateToolArgs(QUOTE, { from_asset_id: 'BTC', to_asset_id: 'USDT', from_amount: 50_000 });
|
|
40
|
+
expect(r.ok).toBe(false);
|
|
41
|
+
expect(r.errors.join(' ')).toMatch(/from_amount: 0\.0005/);
|
|
42
|
+
expect(validateToolArgs(QUOTE, { from_asset_id: 'BTC', to_asset_id: 'USDT', from_amount: 0.0005 }).ok).toBe(true);
|
|
43
|
+
expect(validateToolArgs(QUOTE, { from_asset_id: 'USDT', to_asset_id: 'BTC', from_amount: 5000 }).ok).toBe(true);
|
|
44
|
+
expect(validateToolArgs(QUOTE, { from_asset_id: 'BTC', to_asset_id: 'USDT', from_amount: 1, to_amount: 2 }).ok).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
35
47
|
it('accepts valid args and coerces numeric strings', () => {
|
|
36
48
|
const r = validateToolArgs({ name: 'rln_issue_asset', parameters: ISSUE_SCHEMA }, { name: 'Hack', ticker: 'HCK', amount: '1000' });
|
|
37
49
|
expect(r.ok).toBe(true);
|
|
@@ -397,3 +409,30 @@ describe('engine replies skip the answer guards', () => {
|
|
|
397
409
|
expect(res.text).not.toMatch(/won't make one up/);
|
|
398
410
|
});
|
|
399
411
|
});
|
|
412
|
+
|
|
413
|
+
describe('invoices and addresses come from the tool, not the model', () => {
|
|
414
|
+
const invoice = 'rgb:~/~/~/sig/rgb:usdt/10/utxob:abcdefghijklmnop';
|
|
415
|
+
const tools = [{ name: 'rln_create_rgb_invoice', description: '', parameters: {}, handler: async () => ({ invoice }) }];
|
|
416
|
+
|
|
417
|
+
it('appends the invoice when the model leaves it out', async () => {
|
|
418
|
+
const engine = engineWith(tools, [{ tool: 'rln_create_rgb_invoice' }, { text: 'Here is the full invoice string for receiving 10 USDT.' }]);
|
|
419
|
+
const res = await engine.runAgentic([{ role: 'user', content: 'Create an RGB invoice for 10 USDT' }]);
|
|
420
|
+
expect(res.text).toBe(`Here is the full invoice string for receiving 10 USDT.\n\nHere is your RGB invoice:\n\n\`${invoice}\``);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("replaces a mangled copy with the tool's value instead of refusing", async () => {
|
|
424
|
+
const engine = engineWith(tools, [{ tool: 'rln_create_rgb_invoice' }, { text: 'Invoice: rgb:~/~/~/sig/rgb:usdt/10/utxob:abcdefgh' }]);
|
|
425
|
+
const res = await engine.runAgentic([{ role: 'user', content: 'Create an RGB invoice for 10 USDT' }]);
|
|
426
|
+
expect(res.text).toBe(`Here is your RGB invoice:\n\n\`${invoice}\``);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('leaves a correct answer alone and ignores payment data from other tools', async () => {
|
|
430
|
+
const ok = engineWith(tools, [{ tool: 'rln_create_rgb_invoice' }, { text: `Your invoice: ${invoice}` }]);
|
|
431
|
+
expect((await ok.runAgentic([{ role: 'user', content: 'invoice me 10 USDT' }])).text).toBe(`Your invoice: ${invoice}`);
|
|
432
|
+
const reads = engineWith(
|
|
433
|
+
[{ name: 'rln_list_transfers', description: '', parameters: {}, handler: async () => ({ transfers: [{ invoice }] }) }],
|
|
434
|
+
[{ tool: 'rln_list_transfers' }, { text: 'You have one transfer.' }],
|
|
435
|
+
);
|
|
436
|
+
expect((await reads.runAgentic([{ role: 'user', content: 'list my transfers' }])).text).toBe('You have one transfer.');
|
|
437
|
+
});
|
|
438
|
+
});
|
package/src/guards.ts
CHANGED
|
@@ -78,6 +78,26 @@ const SEMANTIC_RULES: Record<string, (a: Record<string, unknown>) => string[]> =
|
|
|
78
78
|
};
|
|
79
79
|
SEMANTIC_RULES.wdk_issue_asset = SEMANTIC_RULES.rln_issue_asset!;
|
|
80
80
|
|
|
81
|
+
/** Above this, a BTC amount in display units is almost certainly sats. */
|
|
82
|
+
const MAX_PLAUSIBLE_BTC = 1000;
|
|
83
|
+
|
|
84
|
+
/** KaleidoSwap quote amounts are display units; reject sats passed as BTC. */
|
|
85
|
+
function btcDisplayAmounts(a: Record<string, unknown>): string[] {
|
|
86
|
+
const errors: string[] = [];
|
|
87
|
+
const legs: Array<[string, unknown]> = [['from_amount', a.from_asset_id], ['to_amount', a.to_asset_id]];
|
|
88
|
+
if (a.from_amount != null && a.to_amount != null) errors.push('give only one of "from_amount" or "to_amount"');
|
|
89
|
+
for (const [key, asset] of legs) {
|
|
90
|
+
const v = a[key];
|
|
91
|
+
if (typeof v !== 'number' || !/^btc$/i.test(String(asset ?? '').trim())) continue;
|
|
92
|
+
if (v >= MAX_PLAUSIBLE_BTC) {
|
|
93
|
+
errors.push(`"${key}" is in BTC, not sats: ${v} sats is ${+(v / 1e8).toFixed(8)} BTC — pass ${key}: ${+(v / 1e8).toFixed(8)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return errors;
|
|
97
|
+
}
|
|
98
|
+
SEMANTIC_RULES.kaleidoswap_get_quote = btcDisplayAmounts;
|
|
99
|
+
SEMANTIC_RULES.kaleidoswap_get_spreads = btcDisplayAmounts;
|
|
100
|
+
|
|
81
101
|
/**
|
|
82
102
|
* Validate (and lightly coerce) a tool call's arguments against its schema.
|
|
83
103
|
* Unknown schema shapes pass through unchanged.
|
|
@@ -169,6 +189,47 @@ export function findUngroundedPaymentData(text: string, sources: unknown[]): Ung
|
|
|
169
189
|
return out;
|
|
170
190
|
}
|
|
171
191
|
|
|
192
|
+
// Tools whose job is to hand the user a payment request or an address.
|
|
193
|
+
const RECEIVE_TOOL = /(^|_)create_(ln_|lightning_|rgb_)?invoice$|(^|_)get_address$|(^|_)receive(_|$)|_invoice_create$/i;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Invoices and addresses that receive tools returned in this run: what the
|
|
197
|
+
* user asked for, to be shown verbatim rather than retyped by the model.
|
|
198
|
+
*/
|
|
199
|
+
export function producedPaymentData(executed: Array<{ name: string; result: unknown }>): UngroundedItem[] {
|
|
200
|
+
const found: UngroundedItem[] = [];
|
|
201
|
+
for (const { name, result } of executed) {
|
|
202
|
+
if (!RECEIVE_TOOL.test(name) || (result && typeof result === 'object' && 'error' in result)) continue;
|
|
203
|
+
let text: string;
|
|
204
|
+
try {
|
|
205
|
+
text = typeof result === 'string' ? result : JSON.stringify(result);
|
|
206
|
+
} catch {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
found.push(...paymentStrings(text));
|
|
210
|
+
}
|
|
211
|
+
// Keep whole values only: the utxob: inside an RGB invoice is not a second item.
|
|
212
|
+
const lower = found.map((f) => f.value.toLowerCase());
|
|
213
|
+
return found.filter((f, i) => {
|
|
214
|
+
const v = lower[i]!;
|
|
215
|
+
return lower.indexOf(v) === i && !lower.some((o, j) => j !== i && o !== v && o.includes(v));
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Payment strings (invoices, addresses) in a piece of text. */
|
|
220
|
+
export function paymentStrings(text: string): UngroundedItem[] {
|
|
221
|
+
const out: UngroundedItem[] = [];
|
|
222
|
+
for (const { kind, re } of PAYMENT_DATA) {
|
|
223
|
+
for (const m of text.matchAll(re)) out.push({ kind, value: m[0].replace(/[.,;:!?*\\]+$/, '') });
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** The answer built from the tool's own values, when the model's copy is unusable. */
|
|
229
|
+
export function producedPaymentReply(items: UngroundedItem[]): string {
|
|
230
|
+
return items.map((i) => `Here is your ${i.kind}:\n\n\`${i.value}\``).join('\n\n');
|
|
231
|
+
}
|
|
232
|
+
|
|
172
233
|
export function ungroundedReply(items: UngroundedItem[]): string {
|
|
173
234
|
const kinds = [...new Set(items.map((i) => i.kind))].join(' / ');
|
|
174
235
|
return `I can't give you that ${kinds}: no tool produced it in this conversation, and I won't make one up. ` +
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,7 @@ export {
|
|
|
49
49
|
walletTools,
|
|
50
50
|
toToolDefs,
|
|
51
51
|
bindWalletTools,
|
|
52
|
+
normalizeWalletArgs,
|
|
52
53
|
} from './wallet/contract.js';
|
|
53
54
|
export type {
|
|
54
55
|
WalletLayer,
|
|
@@ -63,6 +64,9 @@ export {
|
|
|
63
64
|
detectWalletAction,
|
|
64
65
|
hasCapableTool,
|
|
65
66
|
wantsToolCall,
|
|
67
|
+
producedPaymentData,
|
|
68
|
+
paymentStrings,
|
|
69
|
+
producedPaymentReply,
|
|
66
70
|
fixSatsBtcConversions,
|
|
67
71
|
formatSatsAsBtc,
|
|
68
72
|
DECLINED_TOOL_MESSAGE,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getKaleidoswapTool,
|
|
7
7
|
kaleidoswapTools,
|
|
8
8
|
bindKaleidoswapTools,
|
|
9
|
+
normalizeKaleidoswapArgs,
|
|
9
10
|
type KaleidoswapHandler,
|
|
10
11
|
} from './contract.js';
|
|
11
12
|
|
|
@@ -109,10 +110,14 @@ describe('bindKaleidoswapTools', () => {
|
|
|
109
110
|
expect(pairs?.requiresConfirmation).toBeFalsy();
|
|
110
111
|
});
|
|
111
112
|
|
|
112
|
-
it('dispatches execute()
|
|
113
|
+
it('dispatches execute() with kaleido-mcp args plus the legacy aliases', async () => {
|
|
113
114
|
const src = bindKaleidoswapTools(echoHandlers());
|
|
114
|
-
const r = await src.execute('kaleidoswap_get_quote', {
|
|
115
|
-
expect(r).toMatchObject({
|
|
115
|
+
const r = await src.execute('kaleidoswap_get_quote', { from_asset_id: 'BTC', to_asset_id: 'USDT', from_amount: 0.001 });
|
|
116
|
+
expect(r).toMatchObject({
|
|
117
|
+
ok: true,
|
|
118
|
+
tool: 'get_quote',
|
|
119
|
+
args: { from_asset_id: 'BTC', to_asset_id: 'USDT', from_amount: 0.001, from_asset: 'BTC', to_asset: 'USDT', amount: 100_000 },
|
|
120
|
+
});
|
|
116
121
|
});
|
|
117
122
|
|
|
118
123
|
it('throws when a handler is missing and allowMissing is false', () => {
|
|
@@ -142,3 +147,27 @@ describe('bindKaleidoswapTools', () => {
|
|
|
142
147
|
expect(src.id).toBe('maker-prod');
|
|
143
148
|
});
|
|
144
149
|
});
|
|
150
|
+
|
|
151
|
+
describe('normalizeKaleidoswapArgs', () => {
|
|
152
|
+
it('maps a display-unit quote to the legacy sats amount', () => {
|
|
153
|
+
expect(normalizeKaleidoswapArgs('kaleidoswap_get_quote', { from_asset_id: 'BTC', to_asset_id: 'USDT', from_amount: 0.0005 }))
|
|
154
|
+
.toMatchObject({ from_asset: 'BTC', to_asset: 'USDT', amount: 50_000 });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('keeps RGB amounts in display units and marks a buy-side amount', () => {
|
|
158
|
+
expect(normalizeKaleidoswapArgs('kaleidoswap_get_quote', { from_asset_id: 'BTC', to_asset_id: 'USDT', to_amount: 10 }))
|
|
159
|
+
.toMatchObject({ amount: 10, amount_side: 'to' });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('maps a legacy sats quote to display units', () => {
|
|
163
|
+
expect(normalizeKaleidoswapArgs('kaleidoswap_get_quote', { from_asset: 'BTC', to_asset: 'USDT', amount: 100_000 }))
|
|
164
|
+
.toMatchObject({ from_asset_id: 'BTC', to_asset_id: 'USDT', from_amount: 0.001 });
|
|
165
|
+
expect(normalizeKaleidoswapArgs('kaleidoswap_get_quote', { from_asset: 'USDT', to_asset: 'BTC', amount: 25 }))
|
|
166
|
+
.toMatchObject({ from_amount: 25 });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('aliases rfq_id/quote_id and payment_hash/atomic_id', () => {
|
|
170
|
+
expect(normalizeKaleidoswapArgs('kaleidoswap_atomic_init', { rfq_id: 'r1' })).toMatchObject({ quote_id: 'r1' });
|
|
171
|
+
expect(normalizeKaleidoswapArgs('kaleidoswap_atomic_status', { atomic_id: 'h1' })).toMatchObject({ payment_hash: 'h1' });
|
|
172
|
+
});
|
|
173
|
+
});
|