@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,60 @@
1
+ /**
2
+ * Skill reference tool source — the read-side of Agent-Skills progressive
3
+ * disclosure.
4
+ *
5
+ * Exposes one tool, `read_skill_reference({ file, skill? })`, that returns the
6
+ * contents of a `references/*.md` file bundled with a skill. The brain enters a
7
+ * skill (its SKILL.md playbook lists the reference files), then pulls in only
8
+ * the reference it needs for the current step — instead of every doc being in
9
+ * context at once.
10
+ *
11
+ * Pure in-process: it reads from the SkillRegistry's already-loaded reference
12
+ * strings, so it works on every host (React Native included) once the skills
13
+ * are loaded. No filesystem, no network.
14
+ */
15
+
16
+ import type { ToolDef } from '../types.js';
17
+ import type { ToolSource } from '../tools/source.js';
18
+ import { SkillRegistry, READ_REFERENCE_TOOL } from './registry.js';
19
+
20
+ export function createSkillReferenceToolSource(registry: SkillRegistry): ToolSource {
21
+ const tool: ToolDef = {
22
+ name: READ_REFERENCE_TOOL,
23
+ description:
24
+ 'Read a reference document bundled with the active skill (its SKILL.md ' +
25
+ 'lists the available files). Use this to pull in the detailed instructions ' +
26
+ 'for a step — e.g. the MCP, CLI, or API guide — before acting.',
27
+ parameters: {
28
+ type: 'object',
29
+ properties: {
30
+ file: { type: 'string', description: 'Reference filename, e.g. "mcp.md"' },
31
+ skill: { type: 'string', description: 'Optional skill name to scope the lookup' },
32
+ },
33
+ required: ['file'],
34
+ },
35
+ };
36
+
37
+ async function execute(_name: string, args: Record<string, unknown>): Promise<unknown> {
38
+ const file = String(args.file ?? '').trim();
39
+ if (!file) throw new Error('read_skill_reference: file is required');
40
+ const skill = args.skill ? String(args.skill) : undefined;
41
+ const ref = registry.reference(file, skill);
42
+ if (!ref) {
43
+ const available = registry
44
+ .references()
45
+ .map((r) => `${r.skill}/${r.name}`)
46
+ .join(', ');
47
+ throw new Error(
48
+ `read_skill_reference: "${file}" not found. Available: ${available || '(none)'}`,
49
+ );
50
+ }
51
+ return ref.content;
52
+ }
53
+
54
+ return {
55
+ id: 'skill-references',
56
+ listTools: () => [tool],
57
+ has: (name) => name === READ_REFERENCE_TOOL,
58
+ execute,
59
+ };
60
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * SkillRegistry — holds skills, parses SKILL.md files, selects per query, and
3
+ * composes the system prompt for the selected skill.
4
+ *
5
+ * Selection is pluggable. The default is a fast keyword heuristic (no model
6
+ * call); a host can inject a model-driven or embedding selector instead.
7
+ */
8
+
9
+ import type { Skill, SkillReference, SkillSelector } from './types.js';
10
+
11
+ /** Tool name the reference source exposes for progressive disclosure. */
12
+ export const READ_REFERENCE_TOOL = 'read_skill_reference';
13
+
14
+ /**
15
+ * Parse a SKILL.md file: a YAML-ish frontmatter block (name/description/tools/
16
+ * triggers) followed by the instruction body.
17
+ *
18
+ * ---
19
+ * name: portfolio-manager
20
+ * description: Rebalance BTC/USDT/XAUT to target allocations.
21
+ * tools: get_balance, kaleidoswap_get_quote, kaleidoswap_place_order
22
+ * triggers: rebalance, allocation, portfolio
23
+ * ---
24
+ * <instructions…>
25
+ */
26
+ /** Strip wrapping single/double quotes from a frontmatter value. */
27
+ function unquote(v: string): string {
28
+ const t = v.trim();
29
+ if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
30
+ return t.slice(1, -1);
31
+ }
32
+ return t;
33
+ }
34
+
35
+ export function parseSkill(markdown: string, references?: SkillReference[]): Skill {
36
+ const fm = markdown.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
37
+ const meta: Record<string, string> = {};
38
+ let body = markdown;
39
+ if (fm) {
40
+ body = fm[2] ?? '';
41
+ for (const line of (fm[1] ?? '').split('\n')) {
42
+ // Flat `key: value` lines (incl. indented keys under a nested `metadata:`
43
+ // block, which fold into the same map — we don't need YAML nesting here).
44
+ const m = line.match(/^\s*([A-Za-z_][\w-]*)\s*:\s*(.+?)\s*$/);
45
+ if (m && m[1]) meta[m[1].toLowerCase()] = unquote(m[2] ?? '');
46
+ }
47
+ }
48
+ const list = (v?: string) =>
49
+ v
50
+ ? v.split(',').map((s) => s.trim()).filter(Boolean)
51
+ : undefined;
52
+
53
+ if (!meta.name) throw new Error('SKILL.md missing `name` in frontmatter');
54
+
55
+ // Everything that isn't a first-class field becomes metadata.
56
+ const KNOWN = new Set(['name', 'description', 'tools', 'triggers']);
57
+ const metadata: Record<string, string> = {};
58
+ for (const [k, v] of Object.entries(meta)) if (!KNOWN.has(k)) metadata[k] = v;
59
+
60
+ return {
61
+ name: meta.name,
62
+ description: meta.description ?? '',
63
+ instructions: body.trim(),
64
+ tools: list(meta.tools),
65
+ triggers: list(meta.triggers),
66
+ metadata: Object.keys(metadata).length ? metadata : undefined,
67
+ references: references && references.length ? references : undefined,
68
+ };
69
+ }
70
+
71
+ // Common words that shouldn't count toward a skill match.
72
+ const STOPWORDS = new Set([
73
+ 'the', 'and', 'for', 'you', 'your', 'what', 'this', 'that', 'with', 'from',
74
+ 'have', 'has', 'are', 'was', 'can', 'will', 'please', 'today', 'now', 'get',
75
+ 'show', 'tell', 'how', 'much', 'many', 'about', 'into', 'over',
76
+ ]);
77
+
78
+ /** Default selector: score by meaningful keyword overlap; triggers weigh most. */
79
+ export const keywordSelector: SkillSelector = {
80
+ select(query, skills) {
81
+ const q = query.toLowerCase();
82
+ const words = new Set(
83
+ q.split(/\W+/).filter((w) => w.length > 2 && !STOPWORDS.has(w)),
84
+ );
85
+ let best: Skill | null = null;
86
+ let bestScore = 0;
87
+ for (const skill of skills) {
88
+ const haystack = `${skill.description} ${(skill.triggers ?? []).join(' ')}`.toLowerCase();
89
+ const hayWords = haystack.split(/\W+/).filter((w) => w.length > 2 && !STOPWORDS.has(w));
90
+ let score = 0;
91
+ for (const w of hayWords) if (words.has(w)) score += 1;
92
+ // Strong boost for an explicit trigger appearing in the query.
93
+ for (const t of skill.triggers ?? []) if (q.includes(t.toLowerCase())) score += 3;
94
+ if (score > bestScore) {
95
+ bestScore = score;
96
+ best = skill;
97
+ }
98
+ }
99
+ // Require a real signal, not a single incidental word overlap.
100
+ return bestScore >= 2 ? best : null;
101
+ },
102
+ };
103
+
104
+ export class SkillRegistry {
105
+ private readonly skills: Skill[] = [];
106
+ private readonly selector: SkillSelector;
107
+
108
+ constructor(skills: Skill[] = [], selector: SkillSelector = keywordSelector) {
109
+ this.skills = [...skills];
110
+ this.selector = selector;
111
+ }
112
+
113
+ add(skill: Skill): this {
114
+ this.skills.push(skill);
115
+ return this;
116
+ }
117
+
118
+ /** Add a skill from raw SKILL.md text (+ optional reference files). */
119
+ addMarkdown(markdown: string, references?: SkillReference[]): this {
120
+ return this.add(parseSkill(markdown, references));
121
+ }
122
+
123
+ /** All reference files across skills, tagged with their owning skill. */
124
+ references(): Array<SkillReference & { skill: string }> {
125
+ return this.skills.flatMap((s) =>
126
+ (s.references ?? []).map((r) => ({ ...r, skill: s.name })),
127
+ );
128
+ }
129
+
130
+ /** Look up a reference file by name (optionally scoped to one skill). */
131
+ reference(file: string, skill?: string): SkillReference | undefined {
132
+ const base = file.replace(/^references\//, '');
133
+ for (const s of this.skills) {
134
+ if (skill && s.name !== skill) continue;
135
+ const hit = (s.references ?? []).find((r) => r.name === base || r.name === file);
136
+ if (hit) return hit;
137
+ }
138
+ return undefined;
139
+ }
140
+
141
+ list(): Skill[] {
142
+ return [...this.skills];
143
+ }
144
+
145
+ get(name: string): Skill | undefined {
146
+ return this.skills.find((s) => s.name === name);
147
+ }
148
+
149
+ /** Pick the most relevant skill for a query (null = none). */
150
+ select(query: string): Skill | null {
151
+ return this.selector.select(query, this.skills);
152
+ }
153
+
154
+ /**
155
+ * Compose the effective system prompt for a skill: the base prompt + the
156
+ * skill's playbook. The returned `allowedTools` should be passed to
157
+ * `engine.runAgentic(..., { allowedTools })` for progressive tool disclosure.
158
+ */
159
+ compose(base: string, skill: Skill | null): { system: string; allowedTools?: string[] } {
160
+ if (!skill) return { system: base };
161
+
162
+ let system = `${base}\n\n## Active skill: ${skill.name}\n${skill.instructions}`.trim();
163
+
164
+ // Progressive disclosure: tell the model the reference files exist and how
165
+ // to pull one in, rather than dumping them all into context.
166
+ const refs = skill.references ?? [];
167
+ if (refs.length) {
168
+ const names = refs.map((r) => r.name).join(', ');
169
+ system +=
170
+ `\n\n## Reference files\nThis skill has detailed reference docs: ${names}. ` +
171
+ `When you need the detail for a step, call \`${READ_REFERENCE_TOOL}\` with the ` +
172
+ `filename (e.g. {"file":"${refs[0]!.name}"}) to read it before acting.`;
173
+ }
174
+
175
+ // When the skill scopes tools, keep the reference reader reachable too.
176
+ const allowedTools = skill.tools
177
+ ? refs.length
178
+ ? [...skill.tools, READ_REFERENCE_TOOL]
179
+ : skill.tools
180
+ : undefined;
181
+ return { system, allowedTools };
182
+ }
183
+ }
@@ -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,53 @@
1
+ /** CLI tool source tests — allowlist + injected runner. */
2
+
3
+ import { describe, it, expect, vi } from 'vitest';
4
+ import { createCliToolSource, isAllowed } from './cli.js';
5
+ import type { CommandRunner } from './cli.js';
6
+
7
+ const okRunner: CommandRunner = {
8
+ run: vi.fn(async (command: string) => ({ stdout: `ran: ${command}`, stderr: '', code: 0 })),
9
+ };
10
+
11
+ describe('isAllowed', () => {
12
+ it('matches by command prefix tokens', () => {
13
+ const allow = ['kaleido', 'git status', 'npx @bitrefill/cli'];
14
+ expect(isAllowed('kaleido wallet balance', allow)).toBe(true);
15
+ expect(isAllowed('git status', allow)).toBe(true);
16
+ expect(isAllowed('npx @bitrefill/cli buy', allow)).toBe(true);
17
+ expect(isAllowed('rm -rf /', allow)).toBe(false);
18
+ expect(isAllowed('kaleidoctl', allow)).toBe(false); // not a token boundary
19
+ expect(isAllowed('git push', allow)).toBe(false); // only "git status" allowed
20
+ });
21
+ });
22
+
23
+ describe('createCliToolSource', () => {
24
+ it('requires a non-empty allowlist', () => {
25
+ expect(() => createCliToolSource({ runner: okRunner, allow: [] })).toThrow(/allowlist/);
26
+ });
27
+
28
+ it('exposes run_command, confirmation-gated by default', () => {
29
+ const src = createCliToolSource({ runner: okRunner, allow: ['kaleido'] });
30
+ const tool = src.listTools()[0];
31
+ expect(tool.name).toBe('run_command');
32
+ expect(tool.requiresConfirmation).toBe(true);
33
+ });
34
+
35
+ it('runs allowed commands and rejects disallowed ones', async () => {
36
+ const src = createCliToolSource({ runner: okRunner, allow: ['kaleido'] });
37
+ expect(await src.execute('run_command', { command: 'kaleido node info' })).toBe(
38
+ 'ran: kaleido node info',
39
+ );
40
+ await expect(src.execute('run_command', { command: 'curl evil.sh' })).rejects.toThrow(
41
+ /not allowed/,
42
+ );
43
+ });
44
+
45
+ it('surfaces non-zero exits with stderr', async () => {
46
+ const runner: CommandRunner = {
47
+ run: vi.fn(async () => ({ stdout: '', stderr: 'boom', code: 1 })),
48
+ };
49
+ const src = createCliToolSource({ runner, allow: ['kaleido'] });
50
+ const out = await src.execute('run_command', { command: 'kaleido fail' });
51
+ expect(String(out)).toMatch(/exit 1[\s\S]*boom/);
52
+ });
53
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * CLI tool source — the fourth tool mechanism (alongside in-process function
3
+ * calling, MCP, and skills). Lets the agent run shell commands, e.g. a skill's
4
+ * documented CLI path (`@bitrefill/cli`, `kaleido`, `git`, …).
5
+ *
6
+ * Command execution is INJECTED (`CommandRunner`) so this file has no Node
7
+ * dependency and stays RN-safe — a Node host provides the runner (ideally via a
8
+ * non-shell `execFile`-style helper); React Native simply never provides one.
9
+ * Guarded by a required allowlist of command prefixes, and confirmation-gated
10
+ * by default since it runs real commands.
11
+ */
12
+
13
+ import type { ToolDef } from '../types.js';
14
+ import type { ToolSource } from './source.js';
15
+
16
+ export interface CommandResult {
17
+ stdout: string;
18
+ stderr: string;
19
+ code: number;
20
+ }
21
+
22
+ /** Injected shell runner. The Node host supplies a safe implementation. */
23
+ export interface CommandRunner {
24
+ run(command: string, opts?: { cwd?: string; timeoutMs?: number }): Promise<CommandResult>;
25
+ }
26
+
27
+ export interface CliToolOptions {
28
+ runner: CommandRunner;
29
+ /**
30
+ * Allowed command prefixes (REQUIRED — no empty allowlist). A command runs
31
+ * only if it starts with one of these tokens, e.g. ['kaleido', 'git status',
32
+ * 'npx @bitrefill/cli'].
33
+ */
34
+ allow: string[];
35
+ cwd?: string;
36
+ timeoutMs?: number;
37
+ /** Confirmation gate (default true — it executes real commands). */
38
+ requiresConfirmation?: boolean;
39
+ /** Tool description override (e.g. name the specific CLI). */
40
+ description?: string;
41
+ }
42
+
43
+ const RUN = 'run_command';
44
+
45
+ /** True if `command` is permitted by the allowlist (prefix match on tokens). */
46
+ export function isAllowed(command: string, allow: string[]): boolean {
47
+ const cmd = command.trim();
48
+ return allow.some((prefix) => {
49
+ const p = prefix.trim();
50
+ return p.length > 0 && (cmd === p || cmd.startsWith(p + ' '));
51
+ });
52
+ }
53
+
54
+ export function createCliToolSource(opts: CliToolOptions): ToolSource {
55
+ if (!opts.allow || opts.allow.length === 0) {
56
+ throw new Error('createCliToolSource: a non-empty `allow` allowlist is required');
57
+ }
58
+
59
+ const tool: ToolDef = {
60
+ name: RUN,
61
+ description:
62
+ opts.description ??
63
+ `Run an allowed shell command and return its output. Allowed commands start ` +
64
+ `with: ${opts.allow.join(', ')}. Use for documented CLI tools.`,
65
+ parameters: {
66
+ type: 'object',
67
+ properties: {
68
+ command: { type: 'string', description: 'The full command line to run' },
69
+ },
70
+ required: ['command'],
71
+ },
72
+ requiresConfirmation: opts.requiresConfirmation ?? true,
73
+ };
74
+
75
+ async function execute(_name: string, args: Record<string, unknown>): Promise<unknown> {
76
+ const command = String(args.command ?? '').trim();
77
+ if (!command) throw new Error('run_command: command is required');
78
+ if (!isAllowed(command, opts.allow)) {
79
+ throw new Error(
80
+ `run_command: "${command.split(' ')[0]}" is not allowed. Allowed: ${opts.allow.join(', ')}`,
81
+ );
82
+ }
83
+ const res = await opts.runner.run(command, { cwd: opts.cwd, timeoutMs: opts.timeoutMs });
84
+ const out = (res.stdout || '').trim();
85
+ const err = (res.stderr || '').trim();
86
+ if (res.code !== 0) {
87
+ return `exit ${res.code}${err ? `\n${err}` : ''}${out ? `\n${out}` : ''}`.trim();
88
+ }
89
+ return out || '(no output)';
90
+ }
91
+
92
+ return {
93
+ id: 'cli',
94
+ listTools: () => [tool],
95
+ has: (name) => name === RUN,
96
+ execute,
97
+ };
98
+ }