matimo-examples 0.1.0-alpha.12 → 0.1.0-alpha.13

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.
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ============================================================================
4
+ * PER-EXECUTION CREDENTIAL OVERRIDE — MULTI-TENANT EXAMPLE
5
+ * ============================================================================
6
+ *
7
+ * PATTERN: Per-call credentials (options.credentials)
8
+ * ─────────────────────────────────────────────────────────────────────────
9
+ * Demonstrates how to supply credentials per `execute()` call instead of
10
+ * relying on environment variables. This is the right pattern for
11
+ * multi-tenant platforms where each user/tenant has their own API keys.
12
+ *
13
+ * Use this pattern when:
14
+ * ✅ Serving multiple tenants from a single process
15
+ * ✅ Credentials come from a database / secrets manager / vault
16
+ * ✅ You must NOT store per-tenant tokens in process.env
17
+ * ✅ You want strict per-call credential isolation
18
+ *
19
+ * Contrast with single-tenant pattern (env vars):
20
+ * SLACK_BOT_TOKEN=xoxb-xxx matimo execute slack-send-message ...
21
+ * → works fine for one account, breaks for ten tenants
22
+ *
23
+ * SETUP:
24
+ * ─────────────────────────────────────────────────────────────────────────
25
+ * No .env token needed — this example uses placeholder tenant tokens to
26
+ * show the API shape. Real requests will fail (expected). To see real calls
27
+ * succeed, replace the placeholder tokens with real Slack bot tokens.
28
+ *
29
+ * USAGE:
30
+ * ─────────────────────────────────────────────────────────────────────────
31
+ * pnpm credentials:example
32
+ *
33
+ * KEY CONCEPTS DEMONSTRATED:
34
+ * ─────────────────────────────────────────────────────────────────────────
35
+ * 1. getRequiredCredentials(toolName) — discover what keys a tool needs
36
+ * 2. execute(name, params, { credentials }) — per-call credential injection
37
+ * 3. Tenant isolation — two tenants, same process, different tokens
38
+ * 4. Graceful partial credentials — credential + env-var fallback strategy
39
+ * 5. Credential manifest — build a map of all tools → required keys at startup
40
+ *
41
+ * ============================================================================
42
+ */
43
+
44
+ import 'dotenv/config';
45
+ import { MatimoInstance } from 'matimo';
46
+
47
+ // ─── Simulated tenant "database" ─────────────────────────────────────────────
48
+ // In a real platform these would come from your DB / vault / secrets manager.
49
+ const TENANTS = {
50
+ 'tenant-acme': {
51
+ name: 'Acme Corp',
52
+ secrets: {
53
+ // Replace with a real token to see live Slack calls
54
+ SLACK_BOT_TOKEN: process.env.ACME_SLACK_BOT_TOKEN ?? 'xoxb-acme-placeholder-token',
55
+ },
56
+ },
57
+ 'tenant-globex': {
58
+ name: 'Globex Inc',
59
+ secrets: {
60
+ SLACK_BOT_TOKEN: process.env.GLOBEX_SLACK_BOT_TOKEN ?? 'xoxb-globex-placeholder-token',
61
+ },
62
+ },
63
+ };
64
+
65
+ type TenantId = keyof typeof TENANTS;
66
+ type Tenant = (typeof TENANTS)[TenantId];
67
+
68
+ // ─── Main ─────────────────────────────────────────────────────────────────────
69
+
70
+ async function main() {
71
+ console.info('\n╔════════════════════════════════════════════════════════╗');
72
+ console.info('║ Per-Execution Credential Override — Multi-Tenant ║');
73
+ console.info('╚════════════════════════════════════════════════════════╝\n');
74
+
75
+ // ── 1. Initialize once — no per-tenant init needed ──────────────────────
76
+ console.info('🚀 Initializing Matimo (once for all tenants)…');
77
+ const matimo = await MatimoInstance.init({ autoDiscover: true });
78
+ console.info(`✅ Loaded ${matimo.listTools().length} tools\n`);
79
+
80
+ // ── 2. Discover required credential keys at startup ──────────────────────
81
+ // getRequiredCredentials() tells you EXACTLY what keys to put in `credentials`
82
+ // for a given tool — no need to read the YAML.
83
+ console.info('🔑 Building credential manifest for all tools…');
84
+ const credentialManifest: Record<string, string[]> = {};
85
+ for (const tool of matimo.listTools()) {
86
+ const keys = matimo.getRequiredCredentials(tool.name);
87
+ if (keys.length > 0) {
88
+ credentialManifest[tool.name] = keys;
89
+ }
90
+ }
91
+
92
+ const toolsWithAuth = Object.keys(credentialManifest).length;
93
+ const toolsNoAuth = matimo.listTools().length - toolsWithAuth;
94
+ console.info(` ${toolsWithAuth} tools need credentials, ${toolsNoAuth} are public`);
95
+ console.info(' Sample manifest entries:');
96
+ for (const [tool, keys] of Object.entries(credentialManifest).slice(0, 5)) {
97
+ console.info(` ${tool}: [${keys.join(', ')}]`);
98
+ }
99
+ console.info();
100
+
101
+ // ── 3. Per-tenant execution helper ───────────────────────────────────────
102
+ // Collect only the keys the tool needs from the tenant's secrets store.
103
+ async function executeForTenant(
104
+ tenantId: TenantId,
105
+ toolName: string,
106
+ params: Record<string, unknown>
107
+ ) {
108
+ const tenant: Tenant = TENANTS[tenantId];
109
+ const requiredKeys = matimo.getRequiredCredentials(toolName);
110
+
111
+ // Build credentials map — only the keys this specific tool needs
112
+ const credentials: Record<string, string> = {};
113
+ const missing: string[] = [];
114
+
115
+ for (const key of requiredKeys) {
116
+ const value = tenant.secrets[key as keyof typeof tenant.secrets];
117
+ if (value) {
118
+ credentials[key] = value;
119
+ } else {
120
+ missing.push(key);
121
+ }
122
+ }
123
+
124
+ if (missing.length > 0) {
125
+ console.warn(
126
+ ` ⚠️ [${tenant.name}] Missing ${missing.length} credential key(s) for '${toolName}'.`
127
+ );
128
+ }
129
+
130
+ console.info(
131
+ ` 🏢 [${tenant.name}] Executing '${toolName}' with ${Object.keys(credentials).length} credential(s)…`
132
+ );
133
+
134
+ try {
135
+ const result = await matimo.execute(toolName, params, { credentials });
136
+ return { tenantId, toolName, success: true, result };
137
+ } catch (err) {
138
+ // Expected for placeholder tokens — real tokens would succeed
139
+ const message = err instanceof Error ? err.message : String(err);
140
+ return { tenantId, toolName, success: false, error: message };
141
+ }
142
+ }
143
+
144
+ // ── 4. Demo: same tool, two tenants, fully isolated ──────────────────────
145
+ console.info('════════════════════════════════════════════════════════════');
146
+ console.info('Demo 1: Same tool, two tenants, isolated credentials');
147
+ console.info('════════════════════════════════════════════════════════════\n');
148
+
149
+ const channel = process.env.SLACK_CHANNEL_ID ?? 'C0000000000';
150
+ const params = { channel, text: `Hello from multi-tenant demo at ${new Date().toISOString()}` };
151
+
152
+ const [acmeResult, globexResult] = await Promise.all([
153
+ executeForTenant('tenant-acme', 'slack-send-message', params),
154
+ executeForTenant('tenant-globex', 'slack-send-message', params),
155
+ ]);
156
+
157
+ for (const r of [acmeResult, globexResult]) {
158
+ const icon = r.success ? '✅' : '⚠️ ';
159
+ const tenant = TENANTS[r.tenantId as TenantId].name;
160
+ console.info(
161
+ ` ${icon} [${tenant}] ${r.success ? 'Succeeded' : `Failed (expected with placeholder token): ${r.error?.slice(0, 80)}`}`
162
+ );
163
+ }
164
+
165
+ // ── 5. Verify process.env was NOT modified ────────────────────────────────
166
+ console.info('\n════════════════════════════════════════════════════════════');
167
+ console.info('Demo 2: process.env isolation check');
168
+ console.info('════════════════════════════════════════════════════════════\n');
169
+
170
+ const envBefore = process.env.SLACK_BOT_TOKEN;
171
+ await executeForTenant('tenant-acme', 'slack-send-message', params);
172
+ const envAfter = process.env.SLACK_BOT_TOKEN;
173
+
174
+ if (envBefore === envAfter) {
175
+ console.info(' ✅ process.env.SLACK_BOT_TOKEN unchanged — credentials are call-scoped');
176
+ } else {
177
+ console.error(' ❌ UNEXPECTED: process.env was mutated!');
178
+ }
179
+
180
+ // ── 6. Fallback to env vars when credentials not provided ─────────────────
181
+ console.info('\n════════════════════════════════════════════════════════════');
182
+ console.info('Demo 3: Backward compatibility — no credentials → env var fallback');
183
+ console.info('════════════════════════════════════════════════════════════\n');
184
+
185
+ // Temporarily set env var to simulate single-tenant / legacy usage
186
+ const wasSet = !!process.env.SLACK_BOT_TOKEN;
187
+ process.env.SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN ?? 'xoxb-env-fallback-token';
188
+
189
+ console.info(' Calling execute() without credentials — falls back to process.env…');
190
+ try {
191
+ await matimo.execute('slack-send-message', params);
192
+ console.info(' ✅ Succeeded (env var token was valid)');
193
+ } catch {
194
+ console.info(' ⚠️ Failed at API level (env token is a placeholder — expected)');
195
+ }
196
+
197
+ if (!wasSet) delete process.env.SLACK_BOT_TOKEN;
198
+
199
+ // ── 7. Credential key lookup reference ───────────────────────────────────
200
+ console.info('\n════════════════════════════════════════════════════════════');
201
+ console.info('Reference: credential keys for Slack tools');
202
+ console.info('════════════════════════════════════════════════════════════\n');
203
+
204
+ const slackTools = matimo.listTools().filter((t) => t.name.startsWith('slack'));
205
+ for (const tool of slackTools.slice(0, 5)) {
206
+ const keys = matimo.getRequiredCredentials(tool.name);
207
+ console.info(` ${tool.name}`);
208
+ console.info(
209
+ ` credentials required: ${keys.length ? `${keys.length} key(s)` : '(none required)'}`
210
+ );
211
+ }
212
+ if (slackTools.length > 5) {
213
+ console.info(` … and ${slackTools.length - 5} more Slack tools`);
214
+ }
215
+
216
+ console.info('\n✅ Example complete.\n');
217
+ console.info('To use real credentials, set per-tenant env vars:');
218
+ console.info(' ACME_SLACK_BOT_TOKEN=xoxb-acme-real-token');
219
+ console.info(' GLOBEX_SLACK_BOT_TOKEN=xoxb-globex-real-token\n');
220
+ }
221
+
222
+ main().catch((err) => {
223
+ console.error('Fatal error:', err);
224
+ process.exit(1);
225
+ });
@@ -0,0 +1,319 @@
1
+ # Meta-Tools Integration Flow
2
+
3
+ **The most comprehensive example showing tool creation → policy validation → human approval → execution.**
4
+
5
+ A real LangChain ReAct agent demonstrates the complete tool lifecycle without being told which metadata tools to use.
6
+
7
+ ## What It Shows
8
+
9
+ ```
10
+ Agent receives high-level goals (not tool names):
11
+ ├─ "Create a safe HTTP GET tool"
12
+ ├─ "Attempt a shell command tool" (will be blocked)
13
+ ├─ "Attempt a file reader tool" (will be blocked)
14
+ ├─ "Create safe tools that pass policy"
15
+ └─ "List and use the tools we created"
16
+
17
+ Agent autonomously discovers and uses:
18
+ ├─ matimo_doctor → Validate YAML against policies
19
+ ├─ matimo_create_tool → Create draft tools on disk
20
+ ├─ matimo_review → Get human approval (terminal prompt)
21
+ ├─ matimo_reload_tools → Reload registry after approval
22
+ ├─ matimo_list_user_tools → List created tools
23
+ └─ [Tool execution] → Use the approved tools
24
+
25
+ Policy engine enforces:
26
+ ├─ ✅ Only allowed domains (safe)
27
+ ├─ ✅ Only HTTP methods GET/POST
28
+ ├─ ❌ No shell commands
29
+ ├─ ❌ No SSRF attacks
30
+ └─ ❌ No namespace hijacking
31
+ ```
32
+
33
+ ## Running It
34
+
35
+ ```bash
36
+ # From examples/tools/
37
+ pnpm meta:flow
38
+
39
+ # Or with auto-approval (for CI/testing):
40
+ printf "y\ny\ny\ny\ny\ny\n" | npx tsx meta-flow/meta-tools-integration.ts
41
+
42
+ # Run just meta-tools validation from anywhere:
43
+ cd examples/tools
44
+ pnpm validate:meta
45
+ ```
46
+
47
+ When prompted: type `y` to approve tools, `n` to reject them. Agent learns from rejections.
48
+
49
+ ## Key Differences from Other Examples
50
+
51
+ | Aspect | Policy Demo | Skills Demo | **Meta-Tools** |
52
+ |--------|-------------|-------------|----------------|
53
+ | Focus | Policy engine | Skills system | **Complete lifecycle** |
54
+ | Agent task | Validates tools | Creates skills | **Creates tools → approves → uses** |
55
+ | Policy demo | ✓ | - | ✓ |
56
+ | Skills demo | - | ✓ | - |
57
+ | Human approval | ✓ | ✓ | **✓ (most interactive)** |
58
+ | Tool creation | - | ✓ | **✓** |
59
+ | Tool execution | - | - | **✓** |
60
+ | Missions | 10 | 6 | **5 progressive** |
61
+ | Duration | ~90s | ~60s | **~120s** |
62
+
63
+ ## Mission Breakdown
64
+
65
+ ### Mission 1: Safe HTTP Tool
66
+ ```
67
+ Agent: "Create a weather tool that calls a safe API"
68
+
69
+ Agent thinks: "I need to validate, create, get approval, reload, then test"
70
+
71
+ Agent: matimo_doctor(weather_fetch yaml)
72
+ → ✅ "Valid: api.weatherapi.com approved, GET method allowed"
73
+
74
+ Agent: matimo_create_tool('weather_fetch', yaml, toolsDir)
75
+ → ✅ "Created draft: weather_fetch"
76
+
77
+ Agent: matimo_review('weather_fetch', toolsDir)
78
+ → Terminal: "Approve weather_fetch? (y/n): "
79
+ → Human types: y
80
+ → ✅ "Approved by human operator"
81
+
82
+ Agent: matimo_reload_tools(toolsDir)
83
+ → ✅ "Reloaded: weather_fetch available"
84
+
85
+ Agent: weather_fetch({city: 'London'})
86
+ → ✅ "API response: temp, condition, etc."
87
+ ```
88
+
89
+ **Result**: ✅ Agent successfully created, approved, and executed a safe tool
90
+
91
+ ---
92
+
93
+ ### Mission 2: Attempt Shell Command (Will Fail)
94
+ ```
95
+ Agent: "Create a tool that executes shell commands"
96
+
97
+ Agent generates: name: shell_exec, type: command, command: bash
98
+
99
+ Agent: matimo_doctor(yaml)
100
+ → ❌ "Command tools are blocked by policy (allowCommandTools=false)"
101
+
102
+ Agent learns: "I cannot create command-type tools"
103
+ ```
104
+
105
+ **Result**: ⚠️ Policy enforced, Agent learns constraints
106
+
107
+ ---
108
+
109
+ ### Mission 3: Attempt File Reader (Will Fail)
110
+ ```
111
+ Agent: "Create a tool to read files"
112
+
113
+ Agent generates: name: file_reader, command: cat {path}
114
+
115
+ Agent: matimo_doctor(yaml)
116
+ → ❌ "Command tool type is blocked"
117
+
118
+ Agent learns: "Command execution is not allowed"
119
+ ```
120
+
121
+ **Result**: ⚠️ Policy blocks dangerous operation type
122
+
123
+ ---
124
+
125
+ ### Mission 4: Create Safe Tools (Learning from Failures)
126
+ ```
127
+ Agent notices previous commands were blocked.
128
+ Agent now creates ONLY HTTP tools:
129
+ ├─ Tool 1: user_lookup (HTTP GET from jsonplaceholder)
130
+ │ ├─ doctor: ✅ Valid
131
+ │ ├─ create: ✅ Created
132
+ │ ├─ review: 🛡️ Human approves
133
+ │ └─ reload: ✅ Available
134
+
135
+ └─ Tool 2: github_stars (HTTP GET from api.github.com)
136
+ ├─ doctor: ✅ Valid
137
+ ├─ create: ✅ Created
138
+ ├─ review: 🛡️ Human approves
139
+ └─ reload: ✅ Available
140
+ ```
141
+
142
+ **Result**: ✅ Multiple tools created, approved, and reloaded
143
+
144
+ ---
145
+
146
+ ### Mission 5: List and Execute Tools
147
+ ```
148
+ Agent: "Show all tools we created and test one"
149
+
150
+ Agent: matimo_list_user_tools(toolsDir)
151
+ → Returns: [user_lookup, github_stars]
152
+
153
+ Agent picks user_lookup and executes:
154
+ Agent: user_lookup({id: 5})
155
+ → Returns: {name: 'Chelsey', city: 'Roscoe', ...}
156
+
157
+ Agent reports: "Both tools work correctly"
158
+ ```
159
+
160
+ **Result**: ✅ Tools are listed and executable
161
+
162
+ ---
163
+
164
+ ## Output You'll See
165
+
166
+ ```
167
+ ╔════════════════════════════════════════════════════════════════════╗
168
+ ║ Matimo Meta-Tools Integration Flow ║
169
+ ║ Tool Creation → Policy Validation → Human Approval → Usage ║
170
+ ╚════════════════════════════════════════════════════════════════════╝
171
+
172
+ ═══════════════════════════════════════════════════════════════════════
173
+ PHASE 1: Setup
174
+ ═══════════════════════════════════════════════════════════════════════
175
+
176
+ ℹ Matimo meta-tools loaded: 9 tools
177
+ ✓ PASS Meta-tools available: matimo_doctor, matimo_create_tool, ...
178
+ ✓ PASS LangChain agent initialized: gpt-4o-mini with meta-tools
179
+
180
+ ℹ Tools directory: /tmp/matimo-meta-flow-xxx/tools
181
+ ℹ When prompted, type 'y' to approve tools
182
+
183
+ ═══════════════════════════════════════════════════════════════════════
184
+ PHASE 2: Missions (Agent-Driven Tool Lifecycle)
185
+ ═══════════════════════════════════════════════════════════════════════
186
+
187
+ ── Mission 1: Create a safe HTTP GET tool ─────────────────────────
188
+ 🎯 Agent Goal: "Create a weather tool that calls a safe API"
189
+
190
+ 🔧 Agent calls: matimo_doctor("name: weather_fetch,...")
191
+ 📋 Result: Valid: safe domain (api.weatherapi.com), GET method allowed
192
+
193
+ 🔧 Agent calls: matimo_create_tool("weather_fetch", yaml, ...)
194
+ 📋 Result: Created draft: weather_fetch
195
+
196
+ 🔧 Agent calls: matimo_review("weather_fetch", ...)
197
+
198
+ ╔══════════════════════════════════════════════════════╗
199
+ ║ 🛡️ HUMAN APPROVAL REQUIRED ║
200
+ ║ Tool: weather_fetch ║
201
+ ║ Desc: Fetch current weather for a city ║
202
+ ╚══════════════════════════════════════════════════════╝
203
+ ❓ Approve? (y/n): y
204
+ ✓ PASS Approved by human operator.
205
+
206
+ 🔧 Agent calls: matimo_reload_tools(...)
207
+ 📋 Result: Reloaded: weather_fetch now available
208
+
209
+ 💬 Agent: I have successfully created and approved a weather tool.
210
+
211
+ ── Mission 2: Attempt to create a shell command tool ──────────────
212
+ 🎯 Agent Goal: "Create a tool that executes shell commands"
213
+
214
+ 🔧 Agent calls: matimo_doctor("name: shell_exec, type: command,...")
215
+ ❌ Command tools are blocked (allowCommandTools=false)
216
+
217
+ 💬 Agent: I understand. Command execution tools are not allowed.
218
+
219
+ [... more missions ...]
220
+
221
+ ═══════════════════════════════════════════════════════════════════════
222
+ PHASE 3: Verification & Summary
223
+ ═══════════════════════════════════════════════════════════════════════
224
+
225
+ ✓ PASS Tools created on disk: 4 tools
226
+ ✓ PASS weather_fetch/definition.yaml
227
+ ✓ PASS user_lookup/definition.yaml
228
+ ✓ PASS github_stars/definition.yaml
229
+ ✓ PASS city_lookup/definition.yaml
230
+
231
+ Mission Results:
232
+ ✓ PASS Safe HTTP Tool
233
+ Created: weather_fetch
234
+ ⚠ WARN Shell Command (blocked)
235
+ ⚠ WARN File Reader (blocked)
236
+ ✓ PASS Safe Tool Creation
237
+ Created: user_lookup, github_stars
238
+ ✓ PASS List & Execute Tools
239
+
240
+ Summary:
241
+ ℹ Missions: 5
242
+ ℹ Successful: 4
243
+ ℹ Tools created: 4
244
+ ℹ Policy blocks enforced: 2
245
+ ℹ Human approval invoked: 4 times
246
+
247
+ ✓ PASS Real LangChain agent making autonomous decisions
248
+ ✓ PASS Policy engine validating tool definitions
249
+ ✓ PASS Agent learning from policy rejections
250
+ ✓ PASS Human-in-the-loop approval workflow
251
+ ✓ PASS Tool registry reloading after approval
252
+ ✓ PASS Tool execution after approval
253
+ ```
254
+
255
+ ## Code Structure
256
+
257
+ ```
258
+ meta-flow/
259
+ ├── meta-tools-integration.ts
260
+ │ ├─ Header: Policy config, system prompt
261
+ │ ├─ Phase 1: Initialize matimo with meta-tools
262
+ │ ├─ Phase 2: Run 5 autonomous missions
263
+ │ └─ Phase 3: Verification and summary
264
+ ├─ (No mock data in toolspaths)
265
+ └─ Temp directory created for tool artifacts
266
+ ```
267
+
268
+ ## Environment Setup
269
+
270
+ ```bash
271
+ # .env file in examples/tools/
272
+ OPENAI_API_KEY=sk-... # Required for LLM
273
+
274
+ # Optional:
275
+ MATIMO_LOG_LEVEL=debug # See internal logging
276
+ ```
277
+
278
+ ## What to Look For
279
+
280
+ ✅ **Success markers**:
281
+ - Agent successfully creates tools step-by-step
282
+ - Policy engine actually blocks dangerous patterns (not just demo)
283
+ - Human approval prompts work and agent waits for input
284
+ - Tools are created on disk and can be listed
285
+ - Approved tools execute and return real data
286
+
287
+ ⚠️ **Learning moments**:
288
+ - Watch agent adjust strategy after policy rejection
289
+ - See how agent learns allowedDomains/allowedMethods constraints
290
+ - Notice agent doesn't attempt blocked patterns after first failure
291
+
292
+ 🛡️ **Human-in-the-loop in action**:
293
+ - Every tool creation prompts for human approval
294
+ - Type 'y' to simulate approval
295
+ - Type 'n' to simulate rejection, watch agent adapt
296
+
297
+ ## Common Issues
298
+
299
+ | Issue | Solution |
300
+ |-------|----------|
301
+ | "OpenAI API timeout" | Increase `timeout` in ChatOpenAI config (default: 30s) |
302
+ | "Agent doesn't conclude" | MAX_ITERATIONS may be too low (default: 12) |
303
+ | "Tools don't execute" | Check reload was called after review approval |
304
+ | "No terminal prompt" | Verify approval handler is set: `approvalHandler.setApprovalCallback(...)` |
305
+ | "Policy doesn't block" | Check PolicyConfig is passed to MatimoInstance.init() |
306
+
307
+ ## Next: Using in Production
308
+
309
+ 1. **Deploy policy config**: Define your domain allowlist
310
+ 2. **Integrate storage**: Replace temp dir with persistent tool registry
311
+ 3. **Integrate approval DB**: Log all human decisions
312
+ 4. **Monitor violations**: Alert when policy blocks attempts
313
+ 5. **Fine-tune prompts**: Customize for your domain
314
+
315
+ ---
316
+
317
+ **See also**:
318
+ - [policy-demo.ts](../policy/policy-demo.ts) — Policy engine focused example
319
+ - [skills-demo.ts](../skills/skills-demo.ts) — Skills system focused example