@kaleidorg/mind 0.0.1 → 0.1.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 (62) hide show
  1. package/dist/engine.d.ts +9 -0
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +18 -2
  4. package/dist/engine.js.map +1 -1
  5. package/dist/index.d.ts +7 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +4 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/skills/bundle.d.ts +30 -0
  10. package/dist/skills/bundle.d.ts.map +1 -0
  11. package/dist/skills/bundle.js +24 -0
  12. package/dist/skills/bundle.js.map +1 -0
  13. package/dist/skills/loader.d.ts +33 -0
  14. package/dist/skills/loader.d.ts.map +1 -0
  15. package/dist/skills/loader.js +59 -0
  16. package/dist/skills/loader.js.map +1 -0
  17. package/dist/skills/reference-source.d.ts +18 -0
  18. package/dist/skills/reference-source.d.ts.map +1 -0
  19. package/dist/skills/reference-source.js +53 -0
  20. package/dist/skills/reference-source.js.map +1 -0
  21. package/dist/skills/registry.d.ts +41 -0
  22. package/dist/skills/registry.d.ts.map +1 -0
  23. package/dist/skills/registry.js +167 -0
  24. package/dist/skills/registry.js.map +1 -0
  25. package/dist/skills/types.d.ts +53 -0
  26. package/dist/skills/types.d.ts.map +1 -0
  27. package/dist/skills/types.js +18 -0
  28. package/dist/skills/types.js.map +1 -0
  29. package/dist/tools/l402.d.ts +47 -0
  30. package/dist/tools/l402.d.ts.map +1 -0
  31. package/dist/tools/l402.js +84 -0
  32. package/dist/tools/l402.js.map +1 -0
  33. package/package.json +9 -2
  34. package/scripts/bundle-skills.mjs +84 -0
  35. package/skills/README.md +74 -0
  36. package/skills/bitrefill/SKILL.md +66 -0
  37. package/skills/bitrefill/references/api.md +99 -0
  38. package/skills/bitrefill/references/browse.md +71 -0
  39. package/skills/bitrefill/references/capability-matrix.md +115 -0
  40. package/skills/bitrefill/references/cli-headless-auth.md +133 -0
  41. package/skills/bitrefill/references/cli.md +237 -0
  42. package/skills/bitrefill/references/host-openclaw.md +167 -0
  43. package/skills/bitrefill/references/mcp.md +150 -0
  44. package/skills/bitrefill/references/safeguards.md +138 -0
  45. package/skills/bitrefill/references/troubleshooting.md +182 -0
  46. package/skills/kaleido-trading/SKILL.md +31 -0
  47. package/skills/kaleido-wallet/SKILL.md +28 -0
  48. package/src/engine.test.ts +204 -0
  49. package/src/engine.ts +27 -2
  50. package/src/index.ts +17 -0
  51. package/src/skills/bundle.ts +42 -0
  52. package/src/skills/loader.ts +63 -0
  53. package/src/skills/reference-source.ts +60 -0
  54. package/src/skills/registry.ts +183 -0
  55. package/src/skills/skills.test.ts +191 -0
  56. package/src/skills/types.ts +55 -0
  57. package/src/tools/l402.test.ts +113 -0
  58. package/src/tools/l402.ts +122 -0
  59. package/dist/providers/qvac.d.ts +0 -89
  60. package/dist/providers/qvac.d.ts.map +0 -1
  61. package/dist/providers/qvac.js +0 -150
  62. package/dist/providers/qvac.js.map +0 -1
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Skills tests — parse SKILL.md, select per query, compose system + tool
3
+ * filter, and verify the engine honours allowedTools (progressive disclosure).
4
+ */
5
+
6
+ import { describe, it, expect, vi } from 'vitest';
7
+ import { SkillRegistry, parseSkill, READ_REFERENCE_TOOL } from './registry.js';
8
+ import { createSkillReferenceToolSource } from './reference-source.js';
9
+ import { skillsFromBundle } from './bundle.js';
10
+ import { Engine } from '../engine.js';
11
+ import { ToolRegistry } from '../tools/registry.js';
12
+ import { InProcessToolSource } from '../tools/in-process.js';
13
+ import type { LLMProvider } from '../providers/types.js';
14
+ import type { ToolCall } from '../types.js';
15
+
16
+ // A real-spec SKILL.md: quoted multi-line description, nested metadata, no tools.
17
+ const BITREFILL_SKILL = `---
18
+ name: bitrefill
19
+ description: "Buy or browse Bitrefill — gift cards, mobile top-ups, and eSIMs. Triggers when the user mentions Bitrefill, gift cards, mobile top-up, or eSIM."
20
+ compatibility: "Detects host capabilities at runtime."
21
+ metadata:
22
+ author: bitrefill
23
+ version: "2.1.5"
24
+ ---
25
+
26
+ # Bitrefill
27
+
28
+ Routes by capability. See the references for each path.`;
29
+
30
+ const PORTFOLIO_SKILL = `---
31
+ name: portfolio-manager
32
+ description: Rebalance the BTC, USDT and XAUT portfolio to target allocations.
33
+ tools: get_balance, place_order
34
+ triggers: rebalance, allocation, portfolio
35
+ ---
36
+ You manage a Bitcoin L2 portfolio. Check balances first, then place orders to
37
+ hit the target allocation. Never exceed the user's risk band.`;
38
+
39
+ describe('parseSkill', () => {
40
+ it('parses frontmatter + body', () => {
41
+ const s = parseSkill(PORTFOLIO_SKILL);
42
+ expect(s.name).toBe('portfolio-manager');
43
+ expect(s.description).toMatch(/Rebalance/);
44
+ expect(s.tools).toEqual(['get_balance', 'place_order']);
45
+ expect(s.triggers).toEqual(['rebalance', 'allocation', 'portfolio']);
46
+ expect(s.instructions).toMatch(/manage a Bitcoin L2 portfolio/);
47
+ });
48
+ });
49
+
50
+ describe('SkillRegistry selection', () => {
51
+ const reg = new SkillRegistry();
52
+ reg.addMarkdown(PORTFOLIO_SKILL);
53
+ reg.addMarkdown(`---
54
+ name: channel-manager
55
+ description: Open and manage Lightning channels.
56
+ triggers: channel, liquidity, lsp
57
+ ---
58
+ Manage Lightning channels via LSPS1.`);
59
+
60
+ it('routes a query to the right skill by trigger/description', () => {
61
+ expect(reg.select('please rebalance my portfolio')?.name).toBe('portfolio-manager');
62
+ expect(reg.select('open a new lightning channel')?.name).toBe('channel-manager');
63
+ });
64
+
65
+ it('returns null when nothing matches', () => {
66
+ expect(reg.select('what is the weather today')).toBeNull();
67
+ });
68
+
69
+ it('composes the system prompt + exposes the skill tool list', () => {
70
+ const skill = reg.select('rebalance my allocation')!;
71
+ const { system, allowedTools } = reg.compose('You are KaleidoMind.', skill);
72
+ expect(system).toMatch(/You are KaleidoMind\./);
73
+ expect(system).toMatch(/Active skill: portfolio-manager/);
74
+ expect(allowedTools).toEqual(['get_balance', 'place_order']);
75
+ });
76
+ });
77
+
78
+ describe('parseSkill — real Agent-Skills spec', () => {
79
+ it('unquotes the description, captures metadata, tolerates no tools', () => {
80
+ const s = parseSkill(BITREFILL_SKILL);
81
+ expect(s.name).toBe('bitrefill');
82
+ expect(s.description.startsWith('"')).toBe(false);
83
+ expect(s.description).toMatch(/gift cards/);
84
+ expect(s.tools).toBeUndefined();
85
+ // nested metadata keys fold into the flat metadata map
86
+ expect(s.metadata?.author).toBe('bitrefill');
87
+ expect(s.metadata?.version).toBe('2.1.5');
88
+ expect(s.metadata?.compatibility).toMatch(/host capabilities/);
89
+ });
90
+
91
+ it('selects on the long description embedding trigger phrases', () => {
92
+ const reg = new SkillRegistry();
93
+ reg.addMarkdown(BITREFILL_SKILL);
94
+ expect(reg.select('can you buy me an amazon gift card')?.name).toBe('bitrefill');
95
+ expect(reg.select('I want to buy an eSIM data plan')?.name).toBe('bitrefill');
96
+ });
97
+ });
98
+
99
+ describe('progressive disclosure — references', () => {
100
+ const refs = [
101
+ { name: 'mcp.md', content: '# MCP\nUse the remote MCP at api.bitrefill.com/mcp.' },
102
+ { name: 'cli.md', content: '# CLI\nGuest checkout via @bitrefill/cli.' },
103
+ ];
104
+
105
+ it('compose() advertises the reference files + keeps the reader tool reachable', () => {
106
+ const reg = new SkillRegistry();
107
+ reg.addMarkdown(
108
+ `---\nname: bitrefill\ndescription: shop with bitcoin\ntools: buy_product\ntriggers: bitrefill\n---\nbody`,
109
+ refs,
110
+ );
111
+ const skill = reg.get('bitrefill')!;
112
+ const { system, allowedTools } = reg.compose('base', skill);
113
+ expect(system).toMatch(/Reference files/);
114
+ expect(system).toMatch(/mcp\.md, cli\.md/);
115
+ expect(system).toMatch(READ_REFERENCE_TOOL);
116
+ // scoped tools, plus the reference reader so refs stay readable
117
+ expect(allowedTools).toEqual(['buy_product', READ_REFERENCE_TOOL]);
118
+ });
119
+
120
+ it('the reference tool source returns file contents and lists on miss', async () => {
121
+ const reg = new SkillRegistry();
122
+ reg.addMarkdown(`---\nname: bitrefill\ndescription: d\n---\nbody`, refs);
123
+ const src = createSkillReferenceToolSource(reg);
124
+ expect(src.has(READ_REFERENCE_TOOL)).toBe(true);
125
+
126
+ const out = await src.execute(READ_REFERENCE_TOOL, { file: 'mcp.md' });
127
+ expect(out).toMatch(/remote MCP/);
128
+
129
+ // scoping by skill + path-prefixed filename both resolve
130
+ const out2 = await src.execute(READ_REFERENCE_TOOL, { file: 'references/cli.md', skill: 'bitrefill' });
131
+ expect(out2).toMatch(/Guest checkout/);
132
+
133
+ await expect(src.execute(READ_REFERENCE_TOOL, { file: 'nope.md' })).rejects.toThrow(
134
+ /not found.*bitrefill\/mcp\.md/,
135
+ );
136
+ });
137
+ });
138
+
139
+ describe('skillsFromBundle — RN-safe loading', () => {
140
+ it('rehydrates skills (incl. references) from a v1 bundle', () => {
141
+ const skills = skillsFromBundle({
142
+ version: 1,
143
+ skills: [
144
+ {
145
+ dir: 'bitrefill',
146
+ markdown: '---\nname: bitrefill\ndescription: shop with bitcoin\ntriggers: bitrefill, gift card\n---\nbody',
147
+ references: [{ name: 'mcp.md', content: '# MCP' }],
148
+ },
149
+ ],
150
+ });
151
+ expect(skills).toHaveLength(1);
152
+ expect(skills[0].name).toBe('bitrefill');
153
+ expect(skills[0].references?.[0]?.name).toBe('mcp.md');
154
+ expect(skills[0].dir).toBe('bitrefill');
155
+ // and it's selectable through a registry built from the bundle
156
+ expect(new SkillRegistry(skills).select('buy a gift card')?.name).toBe('bitrefill');
157
+ });
158
+
159
+ it('rejects a malformed bundle', () => {
160
+ expect(() => skillsFromBundle({ version: 2 as 1, skills: [] })).toThrow(/valid v1/);
161
+ });
162
+ });
163
+
164
+ describe('engine honours allowedTools (progressive disclosure)', () => {
165
+ it('only exposes the skill’s tools to the model', async () => {
166
+ const seenToolNames: string[][] = [];
167
+ const provider: LLMProvider = {
168
+ name: 'spy',
169
+ async runTurn(input) {
170
+ seenToolNames.push(input.tools.map((t) => t.name));
171
+ return { text: 'done', rawContent: 'done', toolCalls: [] as ToolCall[] };
172
+ },
173
+ };
174
+ const tools = new ToolRegistry([
175
+ new InProcessToolSource('wallet', [
176
+ { name: 'get_balance', description: '', parameters: {}, handler: async () => ({}) },
177
+ { name: 'place_order', description: '', parameters: {}, handler: async () => ({}) },
178
+ { name: 'open_channel', description: '', parameters: {}, handler: async () => ({}) },
179
+ { name: 'delete_everything', description: '', parameters: {}, handler: async () => ({}) },
180
+ ]),
181
+ ]);
182
+ const engine = new Engine({ provider, tools });
183
+
184
+ await engine.runAgentic([{ role: 'user', content: 'rebalance' }], {
185
+ allowedTools: ['get_balance', 'place_order'],
186
+ });
187
+
188
+ // The model only saw the two allowed tools — not open_channel / delete_everything.
189
+ expect(seenToolNames[0].sort()).toEqual(['get_balance', 'place_order']);
190
+ });
191
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Skills — Claude-style Agent Skills the brain can "enter".
3
+ *
4
+ * A skill is the top layer of tool use: a folder with a `SKILL.md` (YAML
5
+ * frontmatter + a markdown playbook) and optional `references/*.md` files that
6
+ * are loaded on demand. Entering a skill injects its playbook into the system
7
+ * prompt and (optionally) scopes the agent to a subset of tools. The tools are
8
+ * still invoked via function calling and may be backed by in-process handlers
9
+ * or MCP servers — skills don't replace those, they *direct* them.
10
+ *
11
+ * This is progressive disclosure, the core idea behind Anthropic's Agent Skills:
12
+ * a small local model never sees every tool or instruction at once — only the
13
+ * selected skill's playbook, and it can pull a reference file in when it needs
14
+ * the detail. The format is compatible with skills published for Claude (e.g.
15
+ * `bitrefill/agents`), so the same SKILL.md runs the QVAC brain unchanged.
16
+ */
17
+
18
+ /** A reference file (references/*.md) the agent can read on demand. */
19
+ export interface SkillReference {
20
+ /** Filename, e.g. "mcp.md". */
21
+ name: string;
22
+ /** Markdown contents. */
23
+ content: string;
24
+ }
25
+
26
+ export interface Skill {
27
+ /** Stable id, e.g. "bitrefill" / "portfolio-manager". */
28
+ name: string;
29
+ /**
30
+ * "When to use this" — the spec's selection signal. May be long and embed the
31
+ * trigger phrases ("…Triggers when the user mentions gift cards, eSIM…").
32
+ */
33
+ description: string;
34
+ /** The playbook: markdown instructions injected into the system prompt. */
35
+ instructions: string;
36
+ /**
37
+ * Tool names this skill is allowed to use. When set, the engine exposes only
38
+ * these tools while the skill is active (progressive disclosure). Omit to
39
+ * allow all registered tools (the default for capability-routing skills).
40
+ */
41
+ tools?: string[];
42
+ /** Optional trigger keywords to boost selection (in addition to description). */
43
+ triggers?: string[];
44
+ /** Remaining frontmatter (compatibility, author, version, homepage, …). */
45
+ metadata?: Record<string, string>;
46
+ /** Reference files (references/*.md) for progressive disclosure. */
47
+ references?: SkillReference[];
48
+ /** Source folder, when loaded from disk (Node). */
49
+ dir?: string;
50
+ }
51
+
52
+ /** Picks the most relevant skill for a query (or null for none). */
53
+ export interface SkillSelector {
54
+ select(query: string, skills: Skill[]): Skill | null;
55
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * L402 tool tests — the "agent pays for a tool in sats" flow, deterministic
3
+ * with a stubbed fetch. No network, no model.
4
+ */
5
+
6
+ import { describe, it, expect, vi } from 'vitest';
7
+ import { createL402ToolSource, parseL402Challenge, bolt11AmountSats } from './l402.js';
8
+
9
+ describe('L402 helpers', () => {
10
+ it('parses an L402 challenge header', () => {
11
+ const h = 'L402 macaroon="AbCdEf==", invoice="lnbc100n1xyz"';
12
+ expect(parseL402Challenge(h)).toEqual({ macaroon: 'AbCdEf==', invoice: 'lnbc100n1xyz' });
13
+ });
14
+
15
+ it('returns null when the header is not an L402 challenge', () => {
16
+ expect(parseL402Challenge('Bearer xyz')).toBeNull();
17
+ });
18
+
19
+ it('parses bolt11 amounts to sats', () => {
20
+ expect(bolt11AmountSats('lnbc100n1xyz')).toBe(10); // 100 nano-BTC = 10 sats
21
+ expect(bolt11AmountSats('lnbc1u1xyz')).toBe(100); // 1 micro-BTC = 100 sats
22
+ expect(bolt11AmountSats('lnbc2500u1xyz')).toBe(250_000);
23
+ });
24
+ });
25
+
26
+ describe('createL402ToolSource — pay-and-fetch flow', () => {
27
+ it('pays the invoice on 402 then re-fetches with the L402 token', async () => {
28
+ const payInvoice = vi.fn(async () => ({ preimage: 'deadbeefpreimage' }));
29
+
30
+ // First call → 402 with a challenge; second (authed) call → 200 with data.
31
+ const fetchImpl = vi
32
+ .fn()
33
+ .mockResolvedValueOnce({
34
+ status: 402,
35
+ ok: false,
36
+ headers: new Map([
37
+ ['www-authenticate', 'L402 macaroon="MAC123", invoice="lnbc100n1demo"'],
38
+ ]) as any,
39
+ })
40
+ .mockResolvedValueOnce({
41
+ status: 200,
42
+ ok: true,
43
+ headers: new Map() as any,
44
+ async text() {
45
+ return JSON.stringify({ btc_usd: 73000 });
46
+ },
47
+ });
48
+
49
+ const src = createL402ToolSource({ payInvoice, fetchImpl: fetchImpl as unknown as typeof fetch });
50
+
51
+ const result = await src.execute('fetch_paid_resource', { url: 'https://api.example/premium/price' });
52
+
53
+ // paid the right invoice + amount (100n = 10 sats)
54
+ expect(payInvoice).toHaveBeenCalledWith('lnbc100n1demo', 10);
55
+ // second fetch carried the L402 Authorization header
56
+ const secondCallArgs = fetchImpl.mock.calls[1];
57
+ expect(secondCallArgs[1].headers.Authorization).toBe('L402 MAC123:deadbeefpreimage');
58
+ // returned the parsed JSON resource
59
+ expect(result).toEqual({ btc_usd: 73000 });
60
+ });
61
+
62
+ it('returns the resource directly when no payment is required (200)', async () => {
63
+ const payInvoice = vi.fn();
64
+ const fetchImpl = vi.fn().mockResolvedValueOnce({
65
+ status: 200,
66
+ ok: true,
67
+ headers: new Map() as any,
68
+ async text() {
69
+ return 'plain text resource';
70
+ },
71
+ });
72
+ const src = createL402ToolSource({ payInvoice, fetchImpl: fetchImpl as unknown as typeof fetch });
73
+
74
+ const result = await src.execute('fetch_paid_resource', { url: 'https://api.example/free' });
75
+
76
+ expect(payInvoice).not.toHaveBeenCalled();
77
+ expect(result).toBe('plain text resource');
78
+ });
79
+
80
+ it('exposes the tool with confirmation required (money tool)', () => {
81
+ const src = createL402ToolSource({ payInvoice: vi.fn() });
82
+ const tools = src.listTools();
83
+ expect(tools).toHaveLength(1);
84
+ expect(tools[0].name).toBe('fetch_paid_resource');
85
+ expect(tools[0].requiresConfirmation).toBe(true);
86
+ });
87
+
88
+ it('honours requiresConfirmation: false (auto-pay hosts)', () => {
89
+ const src = createL402ToolSource({ payInvoice: vi.fn(), requiresConfirmation: false });
90
+ expect(src.listTools()[0].requiresConfirmation).toBe(false);
91
+ });
92
+
93
+ it('declines invoices above maxAutoPaySats', async () => {
94
+ const payInvoice = vi.fn(async () => ({ preimage: 'x' }));
95
+ const fetchImpl = vi.fn().mockResolvedValueOnce({
96
+ status: 402,
97
+ ok: false,
98
+ headers: new Map([
99
+ // 2500u = 250,000 sats — above the 1000 cap
100
+ ['www-authenticate', 'L402 macaroon="M", invoice="lnbc2500u1big"'],
101
+ ]) as any,
102
+ });
103
+ const src = createL402ToolSource({
104
+ payInvoice,
105
+ maxAutoPaySats: 1000,
106
+ fetchImpl: fetchImpl as unknown as typeof fetch,
107
+ });
108
+ await expect(src.execute('fetch_paid_resource', { url: 'https://x/y' })).rejects.toThrow(
109
+ /above the 1000 sat auto-pay cap/,
110
+ );
111
+ expect(payInvoice).not.toHaveBeenCalled();
112
+ });
113
+ });
@@ -0,0 +1,122 @@
1
+ /**
2
+ * L402 tool source — lets the agent pay for paywalled HTTP resources in sats.
3
+ *
4
+ * Exposes one tool, `fetch_paid_resource(url)`, that runs the L402 flow:
5
+ * GET url → 402 with an L402 challenge (macaroon + Lightning invoice)
6
+ * → pay the invoice (via the injected `payInvoice`, e.g. the on-device wallet)
7
+ * → re-GET with `Authorization: L402 <macaroon>:<preimage>` → return the body.
8
+ *
9
+ * This is the "agent pays for a tool in sats" capability: any KaleidoMind agent
10
+ * (mobile or desktop) can buy premium data / inference autonomously. Payment
11
+ * runs through the host's wallet, so it stays on-device; the engine's
12
+ * confirmation gate can wrap the spend if desired.
13
+ *
14
+ * No dependencies — uses global fetch (Node ≥18, React Native). The fetch impl
15
+ * is injectable for testing.
16
+ */
17
+
18
+ import type { ToolDef } from '../types.js';
19
+ import type { ToolSource } from './source.js';
20
+
21
+ export interface L402PayResult {
22
+ /** Payment preimage (hex) — proves the invoice was paid. */
23
+ preimage: string;
24
+ }
25
+
26
+ export interface L402Options {
27
+ /** Pay a BOLT11 invoice, resolve with the preimage. Wallet on device; mock in tests. */
28
+ payInvoice: (invoice: string, amountSats: number) => Promise<L402PayResult>;
29
+ /** Override fetch (tests). Defaults to global fetch. */
30
+ fetchImpl?: typeof fetch;
31
+ /** Optional progress logging. */
32
+ log?: (msg: string) => void;
33
+ /**
34
+ * Gate the tool behind the engine's confirmation (default true). The catch:
35
+ * the invoice amount is only known DURING execution (the 402 challenge), so a
36
+ * pre-execution confirmation can't show it. On hosts without a dedicated L402
37
+ * confirmation UX, set this false and bound spending with `maxAutoPaySats`.
38
+ */
39
+ requiresConfirmation?: boolean;
40
+ /** Auto-pay only invoices up to this many sats; reject larger ones. */
41
+ maxAutoPaySats?: number;
42
+ }
43
+
44
+ /** Parse an L402 (or legacy LSAT) WWW-Authenticate challenge. */
45
+ export function parseL402Challenge(header: string): { macaroon: string; invoice: string } | null {
46
+ const macaroon = header.match(/macaroon="([^"]+)"/i)?.[1];
47
+ const invoice = header.match(/invoice="([^"]+)"/i)?.[1];
48
+ return macaroon && invoice ? { macaroon, invoice } : null;
49
+ }
50
+
51
+ /** Rough BOLT11 amount → sats (for logging / spend caps). 0 if unparseable. */
52
+ export function bolt11AmountSats(invoice: string): number {
53
+ const m = invoice.match(/^ln(?:bc|tb|bcrt)(\d+)([munp]?)/i);
54
+ if (!m) return 0;
55
+ const n = Number(m[1]);
56
+ const mult = (m[2] || '').toLowerCase();
57
+ const btc = mult === 'm' ? n / 1e3 : mult === 'u' ? n / 1e6 : mult === 'n' ? n / 1e9 : mult === 'p' ? n / 1e12 : n;
58
+ return Math.round(btc * 1e8);
59
+ }
60
+
61
+ export function createL402ToolSource(opts: L402Options): ToolSource {
62
+ const doFetch = opts.fetchImpl ?? fetch;
63
+
64
+ const tool: ToolDef = {
65
+ name: 'fetch_paid_resource',
66
+ description:
67
+ 'Fetch a paywalled (L402) HTTP resource, automatically paying the required ' +
68
+ 'Lightning invoice in sats. Use this for premium or paid APIs (market data, ' +
69
+ 'inference, etc.). Pass the resource URL.',
70
+ parameters: {
71
+ type: 'object',
72
+ properties: { url: { type: 'string', description: 'The resource URL to fetch' } },
73
+ required: ['url'],
74
+ },
75
+ requiresConfirmation: opts.requiresConfirmation ?? true,
76
+ };
77
+
78
+ async function execute(_name: string, args: Record<string, unknown>): Promise<unknown> {
79
+ const url = String(args.url ?? '');
80
+ if (!url) throw new Error('fetch_paid_resource: url is required');
81
+
82
+ let res = await doFetch(url);
83
+
84
+ if (res.status === 402) {
85
+ const challenge = parseL402Challenge(res.headers.get('www-authenticate') ?? '');
86
+ if (!challenge) throw new Error('402 Payment Required but no L402 challenge present');
87
+
88
+ const amountSats =
89
+ bolt11AmountSats(challenge.invoice) || Number(res.headers.get('x-amount-sats') ?? 0);
90
+
91
+ if (opts.maxAutoPaySats != null && amountSats > opts.maxAutoPaySats) {
92
+ throw new Error(
93
+ `L402 invoice is ${amountSats} sats, above the ${opts.maxAutoPaySats} sat auto-pay cap — declined`,
94
+ );
95
+ }
96
+
97
+ opts.log?.(`L402: ${url} requires ${amountSats} sats — paying…`);
98
+
99
+ const { preimage } = await opts.payInvoice(challenge.invoice, amountSats);
100
+
101
+ res = await doFetch(url, {
102
+ headers: { Authorization: `L402 ${challenge.macaroon}:${preimage}` },
103
+ });
104
+ opts.log?.(`L402: paid ${amountSats} sats → ${res.status}`);
105
+ }
106
+
107
+ if (!res.ok) throw new Error(`fetch_paid_resource: ${res.status} ${res.statusText}`);
108
+ const body = await res.text();
109
+ try {
110
+ return JSON.parse(body);
111
+ } catch {
112
+ return body;
113
+ }
114
+ }
115
+
116
+ return {
117
+ id: 'l402',
118
+ listTools: () => [tool],
119
+ has: (name) => name === tool.name,
120
+ execute,
121
+ };
122
+ }
@@ -1,89 +0,0 @@
1
- /**
2
- * QVAC provider — primary, on-device.
3
- *
4
- * Wraps the QVAC SDK's llamacpp-completion plugin. The actual binding
5
- * differs slightly per host (RN vs Node vs browser worker), so the heavy
6
- * lifting is delegated to a `QvacBinding` injected by the adapter packages.
7
- *
8
- * Node (kaleido-agent): binding = await import('@qvac/sdk/node')
9
- * React Native (rate): binding from @kaleido/mind-rn → wraps QVACService
10
- * Browser (rate-extension): binding from @kaleido/mind-browser → talks to worker
11
- */
12
- import type { LLMProvider, ProviderConfig, ProviderRunOptions, ProviderRunResult } from './types.js';
13
- import type { Message, ToolDef, TurnEvent } from '../types.js';
14
- /**
15
- * Host-injected binding. Each adapter package supplies its own.
16
- * We keep this duck-typed (not an abstract class) so adapters
17
- * don't have to extend anything heavy.
18
- */
19
- export interface QvacBinding {
20
- load(modelPath: string, opts: {
21
- contextSize: number;
22
- backend?: 'metal' | 'cpu' | 'auto';
23
- }): Promise<void>;
24
- unload(): Promise<void>;
25
- isReady(): boolean;
26
- /** Synchronous completion. The adapter handles the QVAC SDK plumbing. */
27
- complete(prompt: string, opts: {
28
- maxTokens?: number;
29
- temperature?: number;
30
- topP?: number;
31
- stop?: string[];
32
- signal?: AbortSignal;
33
- }): Promise<{
34
- text: string;
35
- usage?: {
36
- prompt: number;
37
- completion: number;
38
- };
39
- }>;
40
- /** Streaming completion — yields token strings. */
41
- stream(prompt: string, opts: {
42
- maxTokens?: number;
43
- temperature?: number;
44
- topP?: number;
45
- stop?: string[];
46
- signal?: AbortSignal;
47
- }): AsyncIterable<string>;
48
- }
49
- export interface QvacProviderOpts extends ProviderConfig {
50
- binding: QvacBinding;
51
- /**
52
- * Chat template family. Auto-detect from model name when omitted.
53
- * 'qwen3' — Qwen 3 family (thinking-mode aware)
54
- * 'hermes' — Hermes 3/4 family
55
- * 'llama3' — Llama 3.x Instruct
56
- * 'chatml' — generic ChatML
57
- */
58
- template?: 'qwen3' | 'hermes' | 'llama3' | 'chatml';
59
- backend?: 'metal' | 'cpu' | 'auto';
60
- }
61
- export declare class QvacProvider implements LLMProvider {
62
- readonly name = "qvac";
63
- readonly modelId: string;
64
- private readonly binding;
65
- private readonly contextSize;
66
- private readonly temperature;
67
- private readonly topP;
68
- private readonly template;
69
- private readonly backend;
70
- constructor(opts: QvacProviderOpts);
71
- load(): Promise<void>;
72
- unload(): Promise<void>;
73
- isReady(): boolean;
74
- run(messages: Message[], tools: ToolDef[], opts?: ProviderRunOptions): Promise<ProviderRunResult>;
75
- stream(messages: Message[], tools: ToolDef[], opts?: ProviderRunOptions): AsyncIterable<TurnEvent>;
76
- private detectTemplate;
77
- /**
78
- * Render messages + tools into the model's chat template.
79
- * TODO: full implementations per family. This stub uses a minimal
80
- * tool-aware ChatML rendering that works for Qwen3 / Hermes / Llama 3.
81
- */
82
- private formatPrompt;
83
- /**
84
- * Parse the model's text output into (text, tool_calls).
85
- * Recognises <tool_call>{...}</tool_call> blocks.
86
- */
87
- private parseCompletion;
88
- }
89
- //# sourceMappingURL=qvac.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"qvac.d.ts","sourceRoot":"","sources":["../../src/providers/qvac.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,cAAc,EACd,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,OAAO,EAAY,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEzE;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1G,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,OAAO,IAAI,OAAO,CAAC;IACnB,yEAAyE;IACzE,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE;QAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;KACtB,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC,CAAC;IAC9E,mDAAmD;IACnD,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE;QAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;KACtB,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD,OAAO,EAAE,WAAW,CAAC;IACrB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACpD,OAAO,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;CACpC;AAED,qBAAa,YAAa,YAAW,WAAW;IAC9C,SAAgB,IAAI,UAAU;IAC9B,SAAgB,OAAO,EAAE,MAAM,CAAC;IAEhC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4C;IACrE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2C;gBAEvD,IAAI,EAAE,gBAAgB;IAU5B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAQrB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAI7B,OAAO,IAAI,OAAO;IAIZ,GAAG,CACP,QAAQ,EAAE,OAAO,EAAE,EACnB,KAAK,EAAE,OAAO,EAAE,EAChB,IAAI,GAAE,kBAAuB,GAC5B,OAAO,CAAC,iBAAiB,CAAC;IAYtB,MAAM,CACX,QAAQ,EAAE,OAAO,EAAE,EACnB,KAAK,EAAE,OAAO,EAAE,EAChB,IAAI,GAAE,kBAAuB,GAC5B,aAAa,CAAC,SAAS,CAAC;IAoB3B,OAAO,CAAC,cAAc;IAQtB;;;;OAIG;IACH,OAAO,CAAC,YAAY;IA+BpB;;;OAGG;IACH,OAAO,CAAC,eAAe;CAwBxB"}