@kaleidorg/mind 0.0.1 → 0.2.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 (191) hide show
  1. package/dist/capabilities.d.ts +34 -0
  2. package/dist/capabilities.d.ts.map +1 -0
  3. package/dist/capabilities.js +34 -0
  4. package/dist/capabilities.js.map +1 -0
  5. package/dist/context/budget.d.ts +29 -0
  6. package/dist/context/budget.d.ts.map +1 -0
  7. package/dist/context/budget.js +36 -0
  8. package/dist/context/budget.js.map +1 -0
  9. package/dist/context/builder.d.ts +39 -0
  10. package/dist/context/builder.d.ts.map +1 -0
  11. package/dist/context/builder.js +77 -0
  12. package/dist/context/builder.js.map +1 -0
  13. package/dist/engine.d.ts +9 -0
  14. package/dist/engine.d.ts.map +1 -1
  15. package/dist/engine.js +18 -2
  16. package/dist/engine.js.map +1 -1
  17. package/dist/fastpath/fastpath.d.ts +38 -0
  18. package/dist/fastpath/fastpath.d.ts.map +1 -0
  19. package/dist/fastpath/fastpath.js +52 -0
  20. package/dist/fastpath/fastpath.js.map +1 -0
  21. package/dist/funnel.d.ts +111 -0
  22. package/dist/funnel.d.ts.map +1 -0
  23. package/dist/funnel.js +175 -0
  24. package/dist/funnel.js.map +1 -0
  25. package/dist/index.d.ts +43 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +32 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/knowledge/bitcoin-copilot.d.ts +11 -0
  30. package/dist/knowledge/bitcoin-copilot.d.ts.map +1 -0
  31. package/dist/knowledge/bitcoin-copilot.js +155 -0
  32. package/dist/knowledge/bitcoin-copilot.js.map +1 -0
  33. package/dist/knowledge/merchants.d.ts +24 -0
  34. package/dist/knowledge/merchants.d.ts.map +1 -0
  35. package/dist/knowledge/merchants.js +34 -0
  36. package/dist/knowledge/merchants.js.map +1 -0
  37. package/dist/knowledge/wallet.d.ts +34 -0
  38. package/dist/knowledge/wallet.d.ts.map +1 -0
  39. package/dist/knowledge/wallet.js +63 -0
  40. package/dist/knowledge/wallet.js.map +1 -0
  41. package/dist/memory/store.d.ts +34 -0
  42. package/dist/memory/store.d.ts.map +1 -0
  43. package/dist/memory/store.js +103 -0
  44. package/dist/memory/store.js.map +1 -0
  45. package/dist/memory/tool.d.ts +9 -0
  46. package/dist/memory/tool.d.ts.map +1 -0
  47. package/dist/memory/tool.js +70 -0
  48. package/dist/memory/tool.js.map +1 -0
  49. package/dist/memory/types.d.ts +56 -0
  50. package/dist/memory/types.d.ts.map +1 -0
  51. package/dist/memory/types.js +14 -0
  52. package/dist/memory/types.js.map +1 -0
  53. package/dist/rag/retriever.d.ts +30 -0
  54. package/dist/rag/retriever.d.ts.map +1 -0
  55. package/dist/rag/retriever.js +72 -0
  56. package/dist/rag/retriever.js.map +1 -0
  57. package/dist/rag/tool.d.ts +15 -0
  58. package/dist/rag/tool.d.ts.map +1 -0
  59. package/dist/rag/tool.js +42 -0
  60. package/dist/rag/tool.js.map +1 -0
  61. package/dist/rag/types.d.ts +44 -0
  62. package/dist/rag/types.d.ts.map +1 -0
  63. package/dist/rag/types.js +11 -0
  64. package/dist/rag/types.js.map +1 -0
  65. package/dist/rag/vector-store.d.ts +23 -0
  66. package/dist/rag/vector-store.d.ts.map +1 -0
  67. package/dist/rag/vector-store.js +72 -0
  68. package/dist/rag/vector-store.js.map +1 -0
  69. package/dist/recipe/asset-send.d.ts +15 -0
  70. package/dist/recipe/asset-send.d.ts.map +1 -0
  71. package/dist/recipe/asset-send.js +83 -0
  72. package/dist/recipe/asset-send.js.map +1 -0
  73. package/dist/recipe/payments.d.ts +15 -0
  74. package/dist/recipe/payments.d.ts.map +1 -0
  75. package/dist/recipe/payments.js +119 -0
  76. package/dist/recipe/payments.js.map +1 -0
  77. package/dist/recipe/receive.d.ts +14 -0
  78. package/dist/recipe/receive.d.ts.map +1 -0
  79. package/dist/recipe/receive.js +109 -0
  80. package/dist/recipe/receive.js.map +1 -0
  81. package/dist/recipe/runner.d.ts +42 -0
  82. package/dist/recipe/runner.d.ts.map +1 -0
  83. package/dist/recipe/runner.js +94 -0
  84. package/dist/recipe/runner.js.map +1 -0
  85. package/dist/recipe/swap.d.ts +16 -0
  86. package/dist/recipe/swap.d.ts.map +1 -0
  87. package/dist/recipe/swap.js +73 -0
  88. package/dist/recipe/swap.js.map +1 -0
  89. package/dist/recipe/types.d.ts +71 -0
  90. package/dist/recipe/types.d.ts.map +1 -0
  91. package/dist/recipe/types.js +13 -0
  92. package/dist/recipe/types.js.map +1 -0
  93. package/dist/skills/bundle.d.ts +30 -0
  94. package/dist/skills/bundle.d.ts.map +1 -0
  95. package/dist/skills/bundle.js +24 -0
  96. package/dist/skills/bundle.js.map +1 -0
  97. package/dist/skills/loader.d.ts +33 -0
  98. package/dist/skills/loader.d.ts.map +1 -0
  99. package/dist/skills/loader.js +59 -0
  100. package/dist/skills/loader.js.map +1 -0
  101. package/dist/skills/reference-source.d.ts +18 -0
  102. package/dist/skills/reference-source.d.ts.map +1 -0
  103. package/dist/skills/reference-source.js +53 -0
  104. package/dist/skills/reference-source.js.map +1 -0
  105. package/dist/skills/registry.d.ts +41 -0
  106. package/dist/skills/registry.d.ts.map +1 -0
  107. package/dist/skills/registry.js +167 -0
  108. package/dist/skills/registry.js.map +1 -0
  109. package/dist/skills/types.d.ts +53 -0
  110. package/dist/skills/types.d.ts.map +1 -0
  111. package/dist/skills/types.js +18 -0
  112. package/dist/skills/types.js.map +1 -0
  113. package/dist/tools/cli.d.ts +43 -0
  114. package/dist/tools/cli.d.ts.map +1 -0
  115. package/dist/tools/cli.js +61 -0
  116. package/dist/tools/cli.js.map +1 -0
  117. package/dist/tools/l402.d.ts +47 -0
  118. package/dist/tools/l402.d.ts.map +1 -0
  119. package/dist/tools/l402.js +84 -0
  120. package/dist/tools/l402.js.map +1 -0
  121. package/dist/tools/mcp.d.ts +3 -2
  122. package/dist/tools/mcp.d.ts.map +1 -1
  123. package/dist/tools/mcp.js +3 -2
  124. package/dist/tools/mcp.js.map +1 -1
  125. package/dist/wallet/contract.d.ts +57 -0
  126. package/dist/wallet/contract.d.ts.map +1 -0
  127. package/dist/wallet/contract.js +113 -0
  128. package/dist/wallet/contract.js.map +1 -0
  129. package/package.json +16 -5
  130. package/scripts/bundle-skills.mjs +84 -0
  131. package/skills/README.md +74 -0
  132. package/skills/bitrefill/SKILL.md +66 -0
  133. package/skills/bitrefill/references/api.md +99 -0
  134. package/skills/bitrefill/references/browse.md +71 -0
  135. package/skills/bitrefill/references/capability-matrix.md +115 -0
  136. package/skills/bitrefill/references/cli-headless-auth.md +133 -0
  137. package/skills/bitrefill/references/cli.md +237 -0
  138. package/skills/bitrefill/references/host-openclaw.md +167 -0
  139. package/skills/bitrefill/references/mcp.md +150 -0
  140. package/skills/bitrefill/references/safeguards.md +138 -0
  141. package/skills/bitrefill/references/troubleshooting.md +182 -0
  142. package/skills/kaleido-trading/SKILL.md +31 -0
  143. package/skills/kaleido-wallet/SKILL.md +28 -0
  144. package/src/capabilities.ts +67 -0
  145. package/src/context/budget.ts +46 -0
  146. package/src/context/builder.ts +100 -0
  147. package/src/context/context.test.ts +83 -0
  148. package/src/engine.test.ts +204 -0
  149. package/src/engine.ts +27 -2
  150. package/src/fastpath/fastpath.test.ts +34 -0
  151. package/src/fastpath/fastpath.ts +70 -0
  152. package/src/funnel.test.ts +207 -0
  153. package/src/funnel.ts +260 -0
  154. package/src/index.ts +102 -0
  155. package/src/knowledge/bitcoin-copilot.ts +177 -0
  156. package/src/knowledge/knowledge.test.ts +63 -0
  157. package/src/knowledge/merchants.ts +49 -0
  158. package/src/knowledge/wallet.ts +84 -0
  159. package/src/memory/memory.test.ts +85 -0
  160. package/src/memory/store.ts +129 -0
  161. package/src/memory/tool.ts +76 -0
  162. package/src/memory/types.ts +63 -0
  163. package/src/rag/rag.test.ts +85 -0
  164. package/src/rag/retriever.ts +94 -0
  165. package/src/rag/tool.ts +55 -0
  166. package/src/rag/types.ts +49 -0
  167. package/src/rag/vector-store.ts +78 -0
  168. package/src/recipe/asset-send.ts +79 -0
  169. package/src/recipe/payments.ts +116 -0
  170. package/src/recipe/receive.ts +98 -0
  171. package/src/recipe/recipe.test.ts +193 -0
  172. package/src/recipe/runner.ts +122 -0
  173. package/src/recipe/swap.ts +74 -0
  174. package/src/recipe/types.ts +76 -0
  175. package/src/skills/bundle.ts +42 -0
  176. package/src/skills/loader.ts +63 -0
  177. package/src/skills/reference-source.ts +60 -0
  178. package/src/skills/registry.ts +183 -0
  179. package/src/skills/skills.test.ts +191 -0
  180. package/src/skills/types.ts +55 -0
  181. package/src/tools/cli.test.ts +53 -0
  182. package/src/tools/cli.ts +98 -0
  183. package/src/tools/l402.test.ts +113 -0
  184. package/src/tools/l402.ts +122 -0
  185. package/src/tools/mcp.ts +3 -2
  186. package/src/wallet/contract.test.ts +89 -0
  187. package/src/wallet/contract.ts +157 -0
  188. package/dist/providers/qvac.d.ts +0 -89
  189. package/dist/providers/qvac.d.ts.map +0 -1
  190. package/dist/providers/qvac.js +0 -150
  191. package/dist/providers/qvac.js.map +0 -1
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Memory — the agent's persistent identity + what it has learned.
3
+ *
4
+ * Two layers, mirroring how nanobot splits SOUL.md / AGENTS.md / memory:
5
+ * - AgentProfile — static identity ("who am I, how do I behave"). Injected.
6
+ * - MemoryStore — durable, growing facts/preferences/events the agent
7
+ * remembers across sessions. Pluggable storage.
8
+ *
9
+ * Pure data + interfaces — no storage or embedding deps. The host injects
10
+ * persistence (AsyncStorage on RN, fs/SQLite on Node) and, optionally, an
11
+ * EmbeddingProvider for semantic recall.
12
+ */
13
+
14
+ /** Static agent identity, composed into the system prompt every turn. */
15
+ export interface AgentProfile {
16
+ /** Display name, e.g. "KaleidoMind". */
17
+ name: string;
18
+ /** Persona / identity — the "soul". Who the agent is, its voice, its values. */
19
+ soul: string;
20
+ /** Operating instructions / house rules (optional). */
21
+ instructions?: string;
22
+ }
23
+
24
+ export type MemoryKind = 'fact' | 'preference' | 'event' | 'note';
25
+
26
+ export interface MemoryItem {
27
+ id: string;
28
+ text: string;
29
+ kind: MemoryKind;
30
+ /** Epoch ms. */
31
+ createdAt: number;
32
+ tags?: string[];
33
+ /** Optional embedding for semantic recall (set when an embedder is wired). */
34
+ embedding?: number[];
35
+ }
36
+
37
+ /** What to add — id/createdAt/embedding are filled in by the store. */
38
+ export type NewMemory = Omit<MemoryItem, 'id' | 'createdAt' | 'embedding'> &
39
+ Partial<Pick<MemoryItem, 'id' | 'createdAt' | 'embedding'>>;
40
+
41
+ export interface MemoryQuery {
42
+ /** Free text to match (semantic if embeddings are available, else substring). */
43
+ text?: string;
44
+ kind?: MemoryKind;
45
+ tags?: string[];
46
+ /** Max items to return (default 5). */
47
+ limit?: number;
48
+ }
49
+
50
+ export interface MemoryStore {
51
+ add(item: NewMemory): Promise<MemoryItem>;
52
+ all(): Promise<MemoryItem[]>;
53
+ /** Best-matching items for the query (recency-ranked, or semantic if embedded). */
54
+ search(query: MemoryQuery): Promise<MemoryItem[]>;
55
+ remove(id: string): Promise<void>;
56
+ clear(): Promise<void>;
57
+ }
58
+
59
+ /** Injected persistence — load once, save on every mutation. RN/Node provide it. */
60
+ export interface MemoryIO {
61
+ load(): Promise<MemoryItem[]>;
62
+ save(items: MemoryItem[]): Promise<void>;
63
+ }
@@ -0,0 +1,85 @@
1
+ /** RAG tests — vector store, chunking, retriever + tool with a fake embedder. */
2
+
3
+ import { describe, it, expect } from 'vitest';
4
+ import { cosineSimilarity, InMemoryVectorStore } from './vector-store.js';
5
+ import { Retriever, chunkText } from './retriever.js';
6
+ import { createRagToolSource } from './tool.js';
7
+ import type { EmbeddingProvider } from './types.js';
8
+
9
+ // Deterministic bag-of-words embedder over a tiny vocab.
10
+ const VOCAB = ['btc', 'lightning', 'channel', 'swap', 'balance', 'rgb', 'price', 'esim'];
11
+ const fakeEmbeddings: EmbeddingProvider = {
12
+ dimension: VOCAB.length,
13
+ async embed(texts) {
14
+ return texts.map((t) => {
15
+ const lower = t.toLowerCase();
16
+ return VOCAB.map((w) => (lower.match(new RegExp(w, 'g'))?.length ?? 0));
17
+ });
18
+ },
19
+ };
20
+
21
+ describe('cosineSimilarity', () => {
22
+ it('is 1 for identical, 0 for orthogonal / degenerate', () => {
23
+ expect(cosineSimilarity([1, 2, 3], [1, 2, 3])).toBeCloseTo(1);
24
+ expect(cosineSimilarity([1, 0], [0, 1])).toBe(0);
25
+ expect(cosineSimilarity([0, 0], [1, 1])).toBe(0);
26
+ });
27
+ });
28
+
29
+ describe('chunkText', () => {
30
+ it('returns one chunk when short', () => {
31
+ expect(chunkText('hello world', 800)).toEqual(['hello world']);
32
+ });
33
+ it('splits long text with overlap on boundaries', () => {
34
+ const text = Array.from({ length: 50 }, (_, i) => `Sentence number ${i}.`).join(' ');
35
+ const chunks = chunkText(text, 120, 20);
36
+ expect(chunks.length).toBeGreaterThan(1);
37
+ expect(chunks.every((c) => c.length <= 140)).toBe(true);
38
+ });
39
+ });
40
+
41
+ describe('InMemoryVectorStore', () => {
42
+ it('upserts (dedup by id) and queries by similarity', async () => {
43
+ const store = new InMemoryVectorStore();
44
+ await store.upsert([
45
+ { id: 'a', text: 'about balance', embedding: [0, 0, 0, 0, 1, 0, 0, 0] },
46
+ { id: 'b', text: 'about price', embedding: [0, 0, 0, 0, 0, 0, 1, 0] },
47
+ ]);
48
+ await store.upsert([{ id: 'a', text: 'about balance v2', embedding: [0, 0, 0, 0, 1, 0, 0, 0] }]);
49
+ expect(await store.size()).toBe(2); // 'a' replaced, not duplicated
50
+
51
+ const hits = await store.query([0, 0, 0, 0, 1, 0, 0, 0], 1);
52
+ expect(hits[0].text).toBe('about balance v2');
53
+ expect(hits[0].score).toBeCloseTo(1);
54
+ });
55
+ });
56
+
57
+ describe('Retriever + RAG tool', () => {
58
+ it('ingests, then retrieves the most relevant chunk', async () => {
59
+ const retriever = new Retriever({ embeddings: fakeEmbeddings });
60
+ const n = await retriever.ingest([
61
+ { id: 'd1', text: 'How to open a lightning channel and manage balance.' },
62
+ { id: 'd2', text: 'eSIM data plans and price comparison.' },
63
+ ]);
64
+ expect(n).toBeGreaterThanOrEqual(2);
65
+
66
+ const hits = await retriever.search('what is my balance on lightning', 1);
67
+ expect(hits[0].text).toMatch(/lightning channel/i);
68
+ });
69
+
70
+ it('search_knowledge tool returns formatted snippets', async () => {
71
+ const retriever = new Retriever({ embeddings: fakeEmbeddings });
72
+ await retriever.ingest([{ id: 'd', text: 'BTC swap and channel fees explained.' }]);
73
+ const src = createRagToolSource(retriever, { k: 2 });
74
+ expect(src.has('search_knowledge')).toBe(true);
75
+ const out = await src.execute('search_knowledge', { query: 'swap channel' });
76
+ expect(String(out)).toMatch(/swap and channel/i);
77
+ });
78
+
79
+ it('returns a friendly message when nothing matches', async () => {
80
+ const retriever = new Retriever({ embeddings: fakeEmbeddings });
81
+ const src = createRagToolSource(retriever);
82
+ const out = await src.execute('search_knowledge', { query: 'anything' });
83
+ expect(String(out)).toMatch(/No relevant passages/);
84
+ });
85
+ });
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Retriever — ties an injected EmbeddingProvider to a VectorStore: ingest
3
+ * documents (chunk → embed → upsert) and search (embed query → top-k). Pure
4
+ * TS; the embedding model is the host's (QVAC `embed()` on device).
5
+ */
6
+
7
+ import { InMemoryVectorStore } from './vector-store.js';
8
+ import type {
9
+ EmbeddingProvider,
10
+ RagDocument,
11
+ RetrievedChunk,
12
+ VectorStore,
13
+ } from './types.js';
14
+
15
+ export interface RetrieverOptions {
16
+ embeddings: EmbeddingProvider;
17
+ /** Vector index (defaults to a fresh in-memory cosine store). */
18
+ store?: VectorStore;
19
+ /** Approx chars per chunk (default 800 ≈ 200 tokens). */
20
+ chunkSize?: number;
21
+ /** Chars of overlap between chunks (default 100). */
22
+ chunkOverlap?: number;
23
+ }
24
+
25
+ /** Split text into overlapping chunks, preferring paragraph/sentence breaks. */
26
+ export function chunkText(text: string, size = 800, overlap = 100): string[] {
27
+ const clean = text.replace(/\r\n/g, '\n').trim();
28
+ if (clean.length <= size) return clean ? [clean] : [];
29
+ const chunks: string[] = [];
30
+ let start = 0;
31
+ while (start < clean.length) {
32
+ let end = Math.min(start + size, clean.length);
33
+ if (end < clean.length) {
34
+ // Back up to the nearest paragraph/sentence/space boundary.
35
+ const slice = clean.slice(start, end);
36
+ const brk = Math.max(
37
+ slice.lastIndexOf('\n\n'),
38
+ slice.lastIndexOf('\n'),
39
+ slice.lastIndexOf('. '),
40
+ slice.lastIndexOf(' '),
41
+ );
42
+ if (brk > size * 0.5) end = start + brk + 1;
43
+ }
44
+ const piece = clean.slice(start, end).trim();
45
+ if (piece) chunks.push(piece);
46
+ if (end >= clean.length) break;
47
+ start = Math.max(end - overlap, start + 1);
48
+ }
49
+ return chunks;
50
+ }
51
+
52
+ export class Retriever {
53
+ private readonly embeddings: EmbeddingProvider;
54
+ private readonly store: VectorStore;
55
+ private readonly chunkSize: number;
56
+ private readonly chunkOverlap: number;
57
+
58
+ constructor(opts: RetrieverOptions) {
59
+ this.embeddings = opts.embeddings;
60
+ this.store = opts.store ?? new InMemoryVectorStore();
61
+ this.chunkSize = opts.chunkSize ?? 800;
62
+ this.chunkOverlap = opts.chunkOverlap ?? 100;
63
+ }
64
+
65
+ /** Chunk, embed, and index documents. Returns the number of chunks stored. */
66
+ async ingest(docs: RagDocument[]): Promise<number> {
67
+ const pending: { id: string; text: string; metadata?: Record<string, unknown> }[] = [];
68
+ for (const doc of docs) {
69
+ const pieces = chunkText(doc.text, this.chunkSize, this.chunkOverlap);
70
+ pieces.forEach((text, i) => {
71
+ const baseId = doc.id ?? `doc_${pending.length}`;
72
+ pending.push({ id: `${baseId}#${i}`, text, metadata: doc.metadata });
73
+ });
74
+ }
75
+ if (pending.length === 0) return 0;
76
+ const vectors = await this.embeddings.embed(pending.map((p) => p.text));
77
+ await this.store.upsert(
78
+ pending.map((p, i) => ({ ...p, embedding: vectors[i] })),
79
+ );
80
+ return pending.length;
81
+ }
82
+
83
+ /** Embed the query and return the top-k most similar chunks. */
84
+ async search(query: string, k = 4): Promise<RetrievedChunk[]> {
85
+ if (!query.trim()) return [];
86
+ const [qv] = await this.embeddings.embed([query]);
87
+ if (!qv) return [];
88
+ return this.store.query(qv, k);
89
+ }
90
+
91
+ vectorStore(): VectorStore {
92
+ return this.store;
93
+ }
94
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * RAG tool source — exposes `search_knowledge` so the model can pull in
3
+ * relevant context on demand (agentic RAG). Preferred over always-injecting
4
+ * retrieved text, which burns the small-model context window.
5
+ */
6
+
7
+ import type { ToolDef } from '../types.js';
8
+ import type { ToolSource } from '../tools/source.js';
9
+ import type { Retriever } from './retriever.js';
10
+
11
+ const SEARCH = 'search_knowledge';
12
+
13
+ export interface RagToolOptions {
14
+ /** Chunks to return (default 4). */
15
+ k?: number;
16
+ /** Override the tool description for your corpus, e.g. "Search the docs." */
17
+ description?: string;
18
+ }
19
+
20
+ export function createRagToolSource(retriever: Retriever, opts: RagToolOptions = {}): ToolSource {
21
+ const tool: ToolDef = {
22
+ name: SEARCH,
23
+ description:
24
+ opts.description ??
25
+ 'Search the knowledge base for passages relevant to a question and return ' +
26
+ 'the best matches. Use this before answering when the answer might be in ' +
27
+ 'ingested documents.',
28
+ parameters: {
29
+ type: 'object',
30
+ properties: {
31
+ query: { type: 'string', description: 'What to look up' },
32
+ k: { type: 'number', description: 'How many passages (default 4)' },
33
+ },
34
+ required: ['query'],
35
+ },
36
+ };
37
+
38
+ async function execute(_name: string, args: Record<string, unknown>): Promise<unknown> {
39
+ const query = String(args.query ?? '').trim();
40
+ if (!query) throw new Error('search_knowledge: query is required');
41
+ const k = Number(args.k) > 0 ? Number(args.k) : (opts.k ?? 4);
42
+ const hits = await retriever.search(query, k);
43
+ if (hits.length === 0) return 'No relevant passages found.';
44
+ return hits
45
+ .map((h, i) => `[${i + 1}] (score ${h.score.toFixed(2)}) ${h.text}`)
46
+ .join('\n\n');
47
+ }
48
+
49
+ return {
50
+ id: 'rag',
51
+ listTools: () => [tool],
52
+ has: (name) => name === SEARCH,
53
+ execute,
54
+ };
55
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * RAG types — retrieval-augmented generation primitives.
3
+ *
4
+ * The heavy parts (the embedding model, the vector index) are interfaces the
5
+ * host injects. On QVAC the EmbeddingProvider wraps the SDK `embed()` API
6
+ * (on-device); a server host could inject a remote embedder. The default
7
+ * VectorStore is a pure-JS in-memory cosine index (good for thousands of
8
+ * chunks); a host can swap in SQLite/native for more.
9
+ */
10
+
11
+ /** Turns text into vectors. Injected — QVAC `embed()` on device, etc. */
12
+ export interface EmbeddingProvider {
13
+ embed(texts: string[]): Promise<number[][]>;
14
+ /** Vector dimension, when known (informational). */
15
+ dimension?: number;
16
+ }
17
+
18
+ export interface Chunk {
19
+ id: string;
20
+ text: string;
21
+ metadata?: Record<string, unknown>;
22
+ embedding?: number[];
23
+ }
24
+
25
+ export interface RetrievedChunk extends Chunk {
26
+ /** Similarity score in [-1, 1] (cosine). */
27
+ score: number;
28
+ }
29
+
30
+ /** A document to ingest; chunked + embedded by the Retriever. */
31
+ export interface RagDocument {
32
+ id?: string;
33
+ text: string;
34
+ metadata?: Record<string, unknown>;
35
+ }
36
+
37
+ export interface VectorStore {
38
+ upsert(chunks: Chunk[]): Promise<void>;
39
+ /** Top-k by cosine similarity to `embedding`. */
40
+ query(embedding: number[], k: number): Promise<RetrievedChunk[]>;
41
+ size(): Promise<number>;
42
+ clear(): Promise<void>;
43
+ }
44
+
45
+ /** Injected persistence for the vector store (optional). */
46
+ export interface VectorStoreIO {
47
+ load(): Promise<Chunk[]>;
48
+ save(chunks: Chunk[]): Promise<void>;
49
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * In-memory vector store — pure-JS cosine similarity index. Zero deps, so it
3
+ * bundles in Bare/RN. Good for thousands of chunks; swap in a native/SQLite
4
+ * store via the VectorStore interface for larger corpora.
5
+ */
6
+
7
+ import type { Chunk, RetrievedChunk, VectorStore, VectorStoreIO } from './types.js';
8
+
9
+ /** Cosine similarity of two equal-length vectors. Returns 0 on degenerate input. */
10
+ export function cosineSimilarity(a: number[], b: number[]): number {
11
+ const n = Math.min(a.length, b.length);
12
+ let dot = 0;
13
+ let na = 0;
14
+ let nb = 0;
15
+ for (let i = 0; i < n; i++) {
16
+ dot += a[i]! * b[i]!;
17
+ na += a[i]! * a[i]!;
18
+ nb += b[i]! * b[i]!;
19
+ }
20
+ if (na === 0 || nb === 0) return 0;
21
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
22
+ }
23
+
24
+ export interface InMemoryVectorStoreOptions {
25
+ io?: VectorStoreIO;
26
+ }
27
+
28
+ export class InMemoryVectorStore implements VectorStore {
29
+ private chunks: Chunk[] = [];
30
+ private hydrated = false;
31
+ private readonly io?: VectorStoreIO;
32
+
33
+ constructor(opts: InMemoryVectorStoreOptions = {}) {
34
+ this.io = opts.io;
35
+ }
36
+
37
+ private async hydrate(): Promise<void> {
38
+ if (this.hydrated) return;
39
+ this.hydrated = true;
40
+ if (this.io) {
41
+ try {
42
+ this.chunks = await this.io.load();
43
+ } catch {
44
+ this.chunks = [];
45
+ }
46
+ }
47
+ }
48
+
49
+ async upsert(chunks: Chunk[]): Promise<void> {
50
+ await this.hydrate();
51
+ for (const c of chunks) {
52
+ const i = this.chunks.findIndex((x) => x.id === c.id);
53
+ if (i >= 0) this.chunks[i] = c;
54
+ else this.chunks.push(c);
55
+ }
56
+ if (this.io) await this.io.save(this.chunks);
57
+ }
58
+
59
+ async query(embedding: number[], k: number): Promise<RetrievedChunk[]> {
60
+ await this.hydrate();
61
+ return this.chunks
62
+ .filter((c) => c.embedding && c.embedding.length > 0)
63
+ .map((c) => ({ ...c, score: cosineSimilarity(embedding, c.embedding!) }))
64
+ .sort((a, b) => b.score - a.score)
65
+ .slice(0, k);
66
+ }
67
+
68
+ async size(): Promise<number> {
69
+ await this.hydrate();
70
+ return this.chunks.length;
71
+ }
72
+
73
+ async clear(): Promise<void> {
74
+ await this.hydrate();
75
+ this.chunks = [];
76
+ if (this.io) await this.io.save(this.chunks);
77
+ }
78
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Built-in "send an RGB asset" recipe — distinct from the BTC payments recipe
3
+ * because an asset amount must NOT be treated as fiat.
4
+ *
5
+ * "send 10 USDT to bob" → resolve_contact → rln_send_asset 🔒
6
+ * "pay alice 5 XAUT" → resolve_contact → rln_send_asset 🔒
7
+ *
8
+ * Fixes the bug where the payments recipe parsed "USDT" as a fiat currency and
9
+ * ran fiat_to_sats. Payments now excludes RGB assets; this recipe owns them.
10
+ */
11
+
12
+ import type { Recipe, RecipeContext } from './types.js';
13
+
14
+ const RGB_ASSET = /\b(usdt|tether|xaut|gold)\b/i;
15
+ const isOnchainOrInvoice = (s: string) => /^(ln(bc|tb|bcrt)|bc1|tb1|lnurl|rgb:|[a-z0-9._-]+@)/i.test(s.trim());
16
+
17
+ function normAsset(a?: string): string | undefined {
18
+ if (!a) return undefined;
19
+ const x = a.toLowerCase();
20
+ if (/usdt|tether/.test(x)) return 'USDT';
21
+ if (/xaut|gold/.test(x)) return 'XAUT';
22
+ return a.toUpperCase();
23
+ }
24
+ function parseAmount(t: string): number | undefined {
25
+ const m = t.match(/(\d[\d.,]*)\s*([km])?\b/i);
26
+ if (!m) return undefined;
27
+ let n = Number(m[1]!.replace(/,/g, ''));
28
+ if (m[2]) n *= m[2].toLowerCase() === 'k' ? 1_000 : 1_000_000;
29
+ return Number.isNaN(n) ? undefined : n;
30
+ }
31
+
32
+ /** "send 10 USDT to bob" / "pay alice 5 xaut". */
33
+ export function extractAssetSend(text: string): Record<string, unknown> | null {
34
+ const t = text.trim();
35
+ if (!/\b(send|pay|transfer)\b/i.test(t)) return null;
36
+ const asset = normAsset(t.match(RGB_ASSET)?.[1]);
37
+ if (!asset) return null; // not an asset send → let the payments recipe handle it
38
+ const amount = parseAmount(t);
39
+ let recipient = t.match(/\bto\s+([^\s,]+)/i)?.[1];
40
+ if (!recipient) {
41
+ const after = t.match(/\b(?:pay|send|transfer)\s+([^\s,]+)/i)?.[1];
42
+ if (after && !/^\d/.test(after) && !RGB_ASSET.test(after)) recipient = after;
43
+ }
44
+ if (!recipient && amount == null) return null;
45
+ return { recipient, asset, amount };
46
+ }
47
+
48
+ export const assetSendRecipe: Recipe = {
49
+ name: 'pay-asset',
50
+ description: 'Send an RGB asset (USDT, XAUT) to a contact or address — resolves the contact, then sends (with confirmation).',
51
+ match: (t) => /\b(send|pay|transfer)\b/i.test(t) && RGB_ASSET.test(t) && !/\b(invoice|receive|swap|buy|sell|for)\b/i.test(t),
52
+ triggers: ['send', 'pay', 'transfer'],
53
+ slots: [
54
+ { name: 'recipient', type: 'string', description: 'Contact name, address, or RGB invoice', required: true },
55
+ { name: 'asset', type: 'string', description: 'RGB asset: USDT or XAUT', required: true },
56
+ { name: 'amount', type: 'number', description: 'Asset amount' },
57
+ ],
58
+ extract: extractAssetSend,
59
+ confident: (s) => !!s.recipient && !!s.asset,
60
+ steps: [
61
+ {
62
+ tool: 'resolve_contact',
63
+ as: 'contact',
64
+ args: (ctx) => ({ name: ctx.slots.recipient }),
65
+ skipIf: (ctx) => !ctx.slots.recipient || isOnchainOrInvoice(String(ctx.slots.recipient)),
66
+ },
67
+ ],
68
+ final: {
69
+ tool: 'rln_send_asset',
70
+ args: (ctx: RecipeContext) => {
71
+ const contact = ctx.results.contact as { ln_address?: string } | undefined;
72
+ return { asset: ctx.slots.asset, amount: ctx.slots.amount, to: contact?.ln_address ?? ctx.slots.recipient };
73
+ },
74
+ },
75
+ summary: (ctx) => {
76
+ const to = (ctx.results.contact as { name?: string } | undefined)?.name ?? ctx.slots.recipient;
77
+ return `Sent ${ctx.slots.amount} ${ctx.slots.asset} to ${to}.`;
78
+ },
79
+ };
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Built-in "pay a contact" recipe — the flagship mobile multi-step flow.
3
+ *
4
+ * "pay bob 3 EUR" → resolve_contact → fiat_to_sats → send_payment 🔒
5
+ * "send 5000 sats to alice" → resolve_contact → send_payment 🔒
6
+ *
7
+ * Uses the canonical contract tools. The deterministic extractor handles most
8
+ * phrasings with no LLM at all (Tier-0); the runner falls back to one LLM
9
+ * extraction otherwise.
10
+ */
11
+
12
+ import type { Recipe, RecipeContext } from './types.js';
13
+
14
+ const CURRENCIES = /\b(sats?|sat|btc|usdt|xaut|eur|usd|gbp|dollars?|euros?|pounds?)\b/i;
15
+ const isOnchainOrInvoice = (s: string) => /^(ln(bc|tb|bcrt)|bc1|tb1|lnurl|[a-z0-9._-]+@)/i.test(s.trim());
16
+
17
+ function normCurrency(c?: string): string | undefined {
18
+ if (!c) return undefined;
19
+ const x = c.toLowerCase();
20
+ if (/^sat/.test(x)) return 'sats';
21
+ if (x === 'btc') return 'btc';
22
+ if (/dollar|^usd/.test(x)) return 'usd';
23
+ if (/euro|^eur/.test(x)) return 'eur';
24
+ if (/pound|^gbp/.test(x)) return 'gbp';
25
+ return x.toUpperCase();
26
+ }
27
+
28
+ /** "pay bob 3 eur", "send 5,000 sats to alice", "pay lnbc... ", "send 0.001 btc to bob" */
29
+ export function extractPayment(text: string): Record<string, unknown> | null {
30
+ const t = text.trim();
31
+ if (!/\b(pay|send|transfer)\b/i.test(t)) return null;
32
+
33
+ // Amount with optional k/m shorthand: "5k" → 5000, "2m" → 2_000_000.
34
+ const amtMatch = t.match(/(\d[\d.,]*)\s*([km])?\b/i);
35
+ let amountNum = amtMatch ? Number(amtMatch[1]!.replace(/,/g, '')) : undefined;
36
+ if (amountNum != null && amtMatch?.[2]) amountNum *= amtMatch[2].toLowerCase() === 'k' ? 1_000 : 1_000_000;
37
+ const amount = amountNum != null && !Number.isNaN(amountNum) ? String(amountNum) : undefined;
38
+ const currency = normCurrency(t.match(CURRENCIES)?.[1]);
39
+
40
+ // recipient: prefer "to <x>", else the token right after pay/send that isn't a number/currency
41
+ let recipient = t.match(/\bto\s+([^\s,]+)/i)?.[1];
42
+ if (!recipient) {
43
+ const after = t.match(/\b(?:pay|send|transfer)\s+([^\s,]+)/i)?.[1];
44
+ if (after && !/^\d/.test(after) && !CURRENCIES.test(after)) recipient = after;
45
+ }
46
+ if (!recipient && !amount) return null;
47
+ return {
48
+ recipient,
49
+ amount: amount ? Number(amount) : undefined,
50
+ currency,
51
+ };
52
+ }
53
+
54
+ /** Compute the sats amount when no fiat conversion step is needed. */
55
+ function directSats(ctx: RecipeContext): number | undefined {
56
+ const amount = Number(ctx.slots.amount);
57
+ if (!amount || Number.isNaN(amount)) return undefined;
58
+ const cur = String(ctx.slots.currency ?? 'sats').toLowerCase();
59
+ if (cur === 'btc') return Math.round(amount * 1e8);
60
+ return Math.round(amount); // sats (default)
61
+ }
62
+
63
+ const isFiat = (ctx: RecipeContext) => {
64
+ const c = String(ctx.slots.currency ?? '').toLowerCase();
65
+ return c !== '' && c !== 'sats' && c !== 'btc';
66
+ };
67
+
68
+ export const paymentsRecipe: Recipe = {
69
+ name: 'pay-contact',
70
+ description: 'Pay a contact or address — resolves the contact, converts fiat to sats, then sends (with confirmation).',
71
+ // A BTC/fiat spend intent — NOT a receive/invoice, and NOT an RGB-asset send
72
+ // ("send 10 USDT to bob" is handled by the asset-send recipe, so USDT/XAUT
73
+ // amounts are never mis-parsed as fiat).
74
+ match: (t) => /\b(pay|send|transfer)\b/i.test(t) && !/\b(invoice|receive|request|address|qr|deposit|usdt|tether|xaut|gold)\b/i.test(t),
75
+ triggers: ['pay', 'send', 'transfer'],
76
+ slots: [
77
+ { name: 'recipient', type: 'string', description: 'Who to pay: a contact name, Lightning address, or invoice', required: true },
78
+ { name: 'amount', type: 'number', description: 'The amount to send' },
79
+ { name: 'currency', type: 'string', description: 'Unit of the amount: sats, btc, or a fiat code like eur/usd' },
80
+ ],
81
+ extract: extractPayment,
82
+ confident: (s) => !!s.recipient,
83
+ steps: [
84
+ {
85
+ // Resolve a contact name → payable address (skip if already an address/invoice).
86
+ tool: 'resolve_contact',
87
+ as: 'contact',
88
+ args: (ctx) => ({ name: ctx.slots.recipient }),
89
+ skipIf: (ctx) => !ctx.slots.recipient || isOnchainOrInvoice(String(ctx.slots.recipient)),
90
+ },
91
+ {
92
+ // Convert fiat → sats (skip when the amount is already sats/btc or absent).
93
+ tool: 'fiat_to_sats',
94
+ as: 'conv',
95
+ args: (ctx) => ({ amount: ctx.slots.amount, currency: ctx.slots.currency }),
96
+ skipIf: (ctx) => !ctx.slots.amount || !isFiat(ctx),
97
+ },
98
+ ],
99
+ final: {
100
+ tool: 'send_payment',
101
+ args: (ctx) => {
102
+ const contact = ctx.results.contact as { ln_address?: string } | undefined;
103
+ const conv = ctx.results.conv as { sats?: number } | undefined;
104
+ return {
105
+ to: contact?.ln_address ?? ctx.slots.recipient,
106
+ amount_sats: conv?.sats ?? directSats(ctx),
107
+ };
108
+ },
109
+ },
110
+ summary: (ctx) => {
111
+ const conv = ctx.results.conv as { sats?: number } | undefined;
112
+ const sats = conv?.sats ?? directSats(ctx);
113
+ const to = (ctx.results.contact as { name?: string } | undefined)?.name ?? ctx.slots.recipient;
114
+ return sats ? `Sent ${sats.toLocaleString()} sats to ${to}.` : `Payment sent to ${to}.`;
115
+ },
116
+ };