@miller-tech/uap 1.2.0 → 1.3.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.
@@ -0,0 +1,740 @@
1
+ # OpenCode Integration Guide
2
+
3
+ This guide explains how to add new integrations to OpenCode based on analysis of the Universal Agent Protocol (UAP) codebase.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [OpenCode Plugin Architecture](#opencode-plugin-architecture)
8
+ 2. [Plugin Structure and Registration](#plugin-structure-and-registration)
9
+ 3. [Defining Custom Tools](#defining-custom-tools)
10
+ 4. [Hook System](#hook-system)
11
+ 5. [Integration Patterns](#integration-patterns)
12
+ 6. [Example: Creating a New Integration](#example-creating-a-new-integration)
13
+ 7. [Best Practices](#best-practices)
14
+
15
+ ---
16
+
17
+ ## OpenCode Plugin Architecture
18
+
19
+ OpenCode uses a **TypeScript plugin system** via the `@opencode-ai/plugin` package (v1.2.x). Plugins are TypeScript modules that extend agent capabilities through:
20
+
21
+ - **Custom Tools**: Define new tools that the LLM can call
22
+ - **Event Hooks**: Intercept and modify agent behavior at specific points
23
+ - **Middleware**: Transform messages and context before/after processing
24
+
25
+ ### Plugin Location
26
+
27
+ Plugins are stored in:
28
+
29
+ ```
30
+ .opencode/plugin/
31
+ ├── uap-commands.ts # UAP CLI commands as tools
32
+ ├── uap-skills.ts # Skill loading system
33
+ ├── uap-droids.ts # Specialized agent droids
34
+ ├── uap-pattern-rag.ts # Pattern retrieval via RAG
35
+ ├── uap-task-completion.ts # Task completion tracking
36
+ ├── uap-session-hooks.ts # Session lifecycle hooks
37
+ └── uap-enforce.ts # Loop detection and enforcement
38
+ ```
39
+
40
+ ### Dependencies
41
+
42
+ The plugin system requires:
43
+
44
+ ```json
45
+ {
46
+ "dependencies": {
47
+ "@opencode-ai/plugin": "1.2.16"
48
+ }
49
+ }
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Plugin Structure and Registration
55
+
56
+ ### Basic Plugin Template
57
+
58
+ ```typescript
59
+ import type { Plugin } from '@opencode-ai/plugin';
60
+ import { tool } from '@opencode-ai/plugin';
61
+
62
+ export const MyPlugin: Plugin = async ({ $, directory, client }) => {
63
+ return {
64
+ // Tool definitions
65
+ tool: {
66
+ my_custom_tool: tool({
67
+ description: 'What this tool does',
68
+ args: {
69
+ param1: tool.schema.string().describe('First parameter'),
70
+ },
71
+ async execute({ param1 }) {
72
+ // Implementation using shell commands or other tools
73
+ const result = await $`command ${param1}`.quiet();
74
+ return result.stdout.toString().trim();
75
+ },
76
+ }),
77
+ },
78
+
79
+ // Event hooks
80
+ event: async ({ event }) => {
81
+ if (event.type === 'session.created') {
82
+ console.log('Session started');
83
+ }
84
+ },
85
+
86
+ // Middleware for message transformation
87
+ middleware: async (input, next) => {
88
+ // Modify input before processing
89
+ const result = await next(input);
90
+ // Optionally modify output
91
+ return result;
92
+ },
93
+ };
94
+ };
95
+ ```
96
+
97
+ ### Available Plugin Context
98
+
99
+ | Parameter | Type | Description |
100
+ | ----------- | ------------------- | ----------------------------------------------- |
101
+ | `$` | Template string tag | Shell command execution (similar to $ in shell) |
102
+ | `directory` | string | Project directory path |
103
+ | `client` | OpenCode client | Direct access to OpenCode client API |
104
+
105
+ ---
106
+
107
+ ## Defining Custom Tools
108
+
109
+ ### Tool Schema Definition
110
+
111
+ Tools are defined using the `tool()` function with a schema:
112
+
113
+ ```typescript
114
+ import { tool } from '@opencode-ai/plugin';
115
+
116
+ const myTool = tool({
117
+ description: 'Description visible to the LLM',
118
+ args: {
119
+ // Required string parameter
120
+ name: tool.schema.string().describe('User name'),
121
+
122
+ // Optional number with constraints
123
+ age: tool.schema.number().min(0).max(150).default(18).describe('User age'),
124
+
125
+ // Enum parameter
126
+ mode: tool.schema.enum(['read', 'write', 'execute']).default('read').describe('Operation mode'),
127
+
128
+ // Array parameter
129
+ items: tool.schema.array().of(tool.schema.string()).describe('List of items'),
130
+ },
131
+ async execute(args) {
132
+ // Tool implementation
133
+ const { name, age = 18, mode = 'read', items = [] } = args;
134
+
135
+ // Use shell commands via $ template tag
136
+ const result = await $`echo "Processing ${name} in ${mode} mode"`;
137
+
138
+ return result.stdout.toString().trim();
139
+ },
140
+ });
141
+ ```
142
+
143
+ ### Tool Registration
144
+
145
+ Tools are registered in the `tool` property of the plugin return value:
146
+
147
+ ```typescript
148
+ export const MyPlugin: Plugin = async ({ $ }) => {
149
+ return {
150
+ tool: {
151
+ // Single tool
152
+ my_tool: tool({...}),
153
+
154
+ // Multiple tools
155
+ another_tool: tool({...}),
156
+ },
157
+ };
158
+ };
159
+ ```
160
+
161
+ ### Tool Naming Convention
162
+
163
+ - Use **snake_case** for tool names (e.g., `my_custom_tool`)
164
+ - Prefix with domain when relevant (e.g., `uap_memory_query`, `git_worktree_create`)
165
+ - Tools become accessible to the LLM as `/tool_name` commands
166
+
167
+ ---
168
+
169
+ ## Hook System
170
+
171
+ OpenCode provides several hook points for customizing agent behavior:
172
+
173
+ ### Event Hooks
174
+
175
+ ```typescript
176
+ export const MyPlugin: Plugin = async ({ $ }) => {
177
+ return {
178
+ event: async ({ event }) => {
179
+ if (event.type === 'session.created') {
180
+ // Session initialization
181
+ console.log('New session started');
182
+ }
183
+
184
+ if (event.type === 'session.compacting') {
185
+ // Before context compression
186
+ await $`echo "Saving critical state before compaction"`;
187
+ }
188
+ },
189
+ };
190
+ };
191
+ ```
192
+
193
+ #### Available Events
194
+
195
+ | Event Type | When Fired | Use Case |
196
+ | -------------------- | -------------------------- | --------------------------------- |
197
+ | `session.created` | New session starts | Initialize state, load context |
198
+ | `session.compacting` | Before context compression | Preserve important information |
199
+ | `message.created` | User message received | Pre-process input, inject context |
200
+
201
+ ### Tool Execution Hooks
202
+
203
+ ```typescript
204
+ export const MyPlugin: Plugin = async ({ $ }) => {
205
+ return {
206
+ 'tool.execute.before': async (input, output) => {
207
+ // Before tool execution - can modify args or block
208
+ if (input.tool === 'bash') {
209
+ console.log(`Executing: ${output.args.command}`);
210
+ }
211
+ },
212
+
213
+ 'tool.execute.after': async (input, _output) => {
214
+ // After tool execution - can log, record, or modify output
215
+ const result = _output.output?.toString();
216
+ await $`echo "Tool ${input.tool} completed" >> /tmp/tool_log.txt`;
217
+ },
218
+ };
219
+ };
220
+ ```
221
+
222
+ ### Tool Definition Hooks
223
+
224
+ ```typescript
225
+ export const MyPlugin: Plugin = async ({ $ }) => {
226
+ return {
227
+ 'tool.definition': async (_input, output) => {
228
+ // Modify tool descriptions before they reach the LLM
229
+ if (output.description) {
230
+ output.description += '\n\n[Note: This tool requires admin privileges]';
231
+ }
232
+ },
233
+ };
234
+ };
235
+ ```
236
+
237
+ ### System Transform Hooks
238
+
239
+ ```typescript
240
+ export const MyPlugin: Plugin = async ({ $ }) => {
241
+ return {
242
+ 'experimental.chat.system.transform': async (_input, output) => {
243
+ // Inject system context into the conversation
244
+ const context = await getRelevantContext();
245
+ output.system.push(context);
246
+ },
247
+ };
248
+ };
249
+ ```
250
+
251
+ ### Middleware
252
+
253
+ ```typescript
254
+ export const MyPlugin: Plugin = async ({ $ }) => {
255
+ return {
256
+ middleware: async (input, next) => {
257
+ // Transform input messages before processing
258
+ const lastMessage = input.messages?.[input.messages.length - 1];
259
+
260
+ if (lastMessage?.role === 'user') {
261
+ // Add pre-processing context
262
+ const taskContext = await extractTaskContext(lastMessage.content);
263
+ input.messages.splice(input.messages.length - 1, 0, {
264
+ role: 'system',
265
+ content: `<task-context>${taskContext}</task-context>`,
266
+ });
267
+ }
268
+
269
+ // Call next middleware
270
+ const result = await next(input);
271
+
272
+ // Post-process output if needed
273
+ return result;
274
+ },
275
+ };
276
+ };
277
+ ```
278
+
279
+ ---
280
+
281
+ ## Integration Patterns
282
+
283
+ ### Pattern 1: CLI Command Wrapper
284
+
285
+ Wrap existing CLI tools as agent tools:
286
+
287
+ ```typescript
288
+ export const MyPlugin: Plugin = async ({ $ }) => {
289
+ return {
290
+ tool: {
291
+ my_cli_wrapper: tool({
292
+ description: 'Execute my-cli command with automatic error handling',
293
+ args: {
294
+ command: tool.schema.string().describe('CLI subcommand'),
295
+ args: tool.schema.array().of(tool.schema.string()).optional(),
296
+ },
297
+ async execute({ command, args = [] }) {
298
+ const result = await $`my-cli ${command} ${args.join(' ')}`.nothrow();
299
+
300
+ if (result.exitCode !== 0) {
301
+ return `Error: ${result.stderr.toString()}`;
302
+ }
303
+
304
+ return result.stdout.toString().trim();
305
+ },
306
+ }),
307
+ },
308
+ };
309
+ };
310
+ ```
311
+
312
+ ### Pattern 2: File System Operations
313
+
314
+ Create file-based tools with validation:
315
+
316
+ ```typescript
317
+ import { readFile, writeFile, readdir } from 'fs/promises';
318
+
319
+ export const MyPlugin: Plugin = async ({ directory }) => {
320
+ const projectDir = directory || '.';
321
+
322
+ return {
323
+ tool: {
324
+ project_file_read: tool({
325
+ description: 'Read a file from the project with path validation',
326
+ args: {
327
+ path: tool.schema.string().describe('Relative file path'),
328
+ },
329
+ async execute({ path }) {
330
+ const fullPath = path.startsWith('/') ? path : join(projectDir, path);
331
+
332
+ // Security: prevent directory traversal
333
+ if (path.includes('..') || !fullPath.startsWith(projectDir)) {
334
+ return 'Error: Access denied';
335
+ }
336
+
337
+ try {
338
+ const content = await readFile(fullPath, 'utf-8');
339
+ return content;
340
+ } catch (err) {
341
+ return `File not found: ${path}`;
342
+ }
343
+ },
344
+ }),
345
+ },
346
+ };
347
+ };
348
+ ```
349
+
350
+ ### Pattern 3: Memory Integration
351
+
352
+ Integrate with persistent memory systems:
353
+
354
+ ```typescript
355
+ import { exec } from 'child_process';
356
+ import { promisify } from 'util';
357
+
358
+ const execAsync = promisify(exec);
359
+
360
+ export const MyPlugin: Plugin = async ({ $ }) => {
361
+ return {
362
+ tool: {
363
+ memory_query: tool({
364
+ description: 'Query persistent memory for relevant context',
365
+ args: {
366
+ query: tool.schema.string().describe('Search query'),
367
+ limit: tool.schema.number().default(5).describe('Max results'),
368
+ },
369
+ async execute({ query, limit }) {
370
+ const result =
371
+ await $`python3 ./agents/scripts/query_memory.py "${query}" --limit ${limit}`.quiet();
372
+ return result.stdout.toString().trim() || 'No memories found.';
373
+ },
374
+ }),
375
+ },
376
+ };
377
+ };
378
+ ```
379
+
380
+ ### Pattern 4: External API Integration
381
+
382
+ Connect to external services:
383
+
384
+ ```typescript
385
+ export const MyPlugin: Plugin = async ({ client }) => {
386
+ return {
387
+ tool: {
388
+ github_issue_create: tool({
389
+ description: 'Create a GitHub issue via the API',
390
+ args: {
391
+ title: tool.schema.string().describe('Issue title'),
392
+ body: tool.schema.string().describe('Issue description'),
393
+ labels: tool.schema.array().of(tool.schema.string()).optional(),
394
+ },
395
+ async execute({ title, body, labels = [] }) {
396
+ const response = await client.fetch(
397
+ 'https://api.github.com/repos/{owner}/{repo}/issues',
398
+ {
399
+ method: 'POST',
400
+ headers: { Authorization: `token ${process.env.GITHUB_TOKEN}` },
401
+ body: JSON.stringify({ title, body, labels }),
402
+ }
403
+ );
404
+
405
+ const data = await response.json();
406
+ return `Issue created: ${data.html_url}`;
407
+ },
408
+ }),
409
+ },
410
+ };
411
+ };
412
+ ```
413
+
414
+ ### Pattern 5: Context Injection (RAG)
415
+
416
+ Inject relevant context on-demand:
417
+
418
+ ```typescript
419
+ export const MyPlugin: Plugin = async ({ $ }) => {
420
+ return {
421
+ middleware: async (input, next) => {
422
+ const lastMessage = input.messages?.[input.messages.length - 1];
423
+
424
+ if (lastMessage?.role === 'user') {
425
+ const taskText =
426
+ typeof lastMessage.content === 'string'
427
+ ? lastMessage.content
428
+ : JSON.stringify(lastMessage.content);
429
+
430
+ // Query for relevant patterns/docs
431
+ if (taskText.length > 50) {
432
+ const result =
433
+ await $`python3 ./scripts/query_patterns.py "${taskText.slice(0, 200)}" --top 3`.quiet();
434
+ const context = result.stdout.toString().trim();
435
+
436
+ if (context) {
437
+ input.messages.splice(input.messages.length - 1, 0, {
438
+ role: 'system',
439
+ content: `<relevant-context>\n${context}\n</relevant-context>`,
440
+ });
441
+ }
442
+ }
443
+ }
444
+
445
+ return next(input);
446
+ },
447
+ };
448
+ };
449
+ ```
450
+
451
+ ---
452
+
453
+ ## Example: Creating a New Integration
454
+
455
+ Let's create a complete integration example: a **Database Migration Tool** plugin.
456
+
457
+ ### Step 1: Create the Plugin File
458
+
459
+ Create `.opencode/plugin/db-migrations.ts`:
460
+
461
+ ```typescript
462
+ import type { Plugin } from '@opencode-ai/plugin';
463
+ import { tool } from '@opencode-ai/plugin';
464
+ import { readFile, writeFile, readdir } from 'fs/promises';
465
+ import { join } from 'path';
466
+
467
+ /**
468
+ * Database Migration Plugin
469
+ *
470
+ * Provides tools for managing database migrations:
471
+ * - db_migration_create: Create new migration files
472
+ * - db_migration_status: Check migration status
473
+ * - db_migration_apply: Apply pending migrations
474
+ * - db_migration_history: View migration history
475
+ */
476
+
477
+ export const DBMigrationsPlugin: Plugin = async ({ $, directory }) => {
478
+ const projectDir = directory || '.';
479
+ const migrationsDir = join(projectDir, 'migrations');
480
+
481
+ // Track applied migrations
482
+ let migrationCache: string[] = [];
483
+
484
+ async function loadMigrationStatus() {
485
+ if (migrationCache.length === 0) {
486
+ try {
487
+ const result =
488
+ await $`sqlite3 ${projectDir}/db.sqlite3 "SELECT name FROM django_migrations ORDER BY id;"`.quiet();
489
+ migrationCache = result.stdout.toString().trim().split('\n').filter(Boolean);
490
+ } catch {
491
+ migrationCache = [];
492
+ }
493
+ }
494
+ return migrationCache;
495
+ }
496
+
497
+ async function getMigrationFiles() {
498
+ try {
499
+ const files = await readdir(migrationsDir);
500
+ return files.filter((f) => f.endsWith('.sql') || f.endsWith('.py'));
501
+ } catch {
502
+ return [];
503
+ }
504
+ }
505
+
506
+ return {
507
+ tool: {
508
+ db_migration_create: tool({
509
+ description: 'Create a new database migration file with timestamp prefix',
510
+ args: {
511
+ name: tool.schema.string().describe('Migration name (will be slugified)'),
512
+ },
513
+ async execute({ name }) {
514
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
515
+ const slug = name
516
+ .toLowerCase()
517
+ .replace(/[^a-z0-9]+/g, '-')
518
+ .replace(/(^-|-$)/g, '');
519
+ const filename = `${timestamp}_${slug}.sql`;
520
+
521
+ const migrationContent = `-- Migration: ${name}\n-- Created: ${new Date().toISOString()}\n\n-- Add your SQL here\n`;
522
+
523
+ await writeFile(join(migrationsDir, filename), migrationContent);
524
+
525
+ return `Created migration: ${filename}`;
526
+ },
527
+ }),
528
+
529
+ db_migration_status: tool({
530
+ description: 'Check which migrations have been applied and which are pending',
531
+ args: {},
532
+ async execute() {
533
+ const [applied, pending] = await Promise.all([
534
+ loadMigrationStatus(),
535
+ getMigrationFiles(),
536
+ ]);
537
+
538
+ const pendingMigrations = pending
539
+ .filter((f) => !f.replace('.sql', '').split('_')[0].includes('initial'))
540
+ .filter((f) => {
541
+ const timestamp = f.split('_')[0];
542
+ return !applied.some((m) => m.includes(timestamp));
543
+ });
544
+
545
+ let output = '### Applied Migrations\n';
546
+ applied.forEach((m) => (output += `- ${m}\n`));
547
+
548
+ if (pendingMigrations.length > 0) {
549
+ output += '\n### Pending Migrations\n';
550
+ pendingMigrations.forEach((f) => (output += `- ${f}\n`));
551
+ } else {
552
+ output += '\n✅ All migrations applied!';
553
+ }
554
+
555
+ return output;
556
+ },
557
+ }),
558
+
559
+ db_migration_apply: tool({
560
+ description: 'Apply all pending database migrations',
561
+ args: {
562
+ migration: tool.schema.string().optional().describe('Specific migration to apply'),
563
+ },
564
+ async execute({ migration }) {
565
+ if (migration) {
566
+ // Apply specific migration
567
+ const result = await $`python3 manage.py migrate app_name ${migration}`.nothrow();
568
+ return result.stdout.toString() + result.stderr.toString();
569
+ }
570
+
571
+ // Apply all pending
572
+ const result = await $`python3 manage.py migrate`.nothrow();
573
+ return result.stdout.toString() + result.stderr.toString();
574
+ },
575
+ }),
576
+
577
+ db_migration_history: tool({
578
+ description: 'View migration history with timestamps',
579
+ args: {
580
+ limit: tool.schema.number().default(10).describe('Number of recent migrations'),
581
+ },
582
+ async execute({ limit }) {
583
+ const result =
584
+ await $`sqlite3 ${projectDir}/db.sqlite3 "SELECT name, datetime(first_applied) FROM django_migrations ORDER BY id DESC LIMIT ${limit};"`.quiet();
585
+ return result.stdout.toString().trim() || 'No migration history found.';
586
+ },
587
+ }),
588
+ },
589
+ };
590
+ };
591
+ ```
592
+
593
+ ### Step 2: Install Dependencies
594
+
595
+ Ensure `@opencode-ai/plugin` is in your dependencies:
596
+
597
+ ```bash
598
+ npm install @opencode-ai/plugin
599
+ ```
600
+
601
+ ### Step 3: Use the Plugin
602
+
603
+ The plugin will be automatically loaded by OpenCode when placed in `.opencode/plugin/`. The LLM can now use:
604
+
605
+ ```bash
606
+ /db_migration_create --name add_user_email_index
607
+ /db_migration_status
608
+ /db_migration_apply
609
+ /db_migration_history --limit 20
610
+ ```
611
+
612
+ ---
613
+
614
+ ## Best Practices
615
+
616
+ ### 1. Error Handling
617
+
618
+ Always handle errors gracefully and provide helpful messages:
619
+
620
+ ```typescript
621
+ async execute({ param }) {
622
+ try {
623
+ const result = await $`command ${param}`.nothrow();
624
+ if (result.exitCode !== 0) {
625
+ return `Error: ${result.stderr.toString() || 'Command failed'}`;
626
+ }
627
+ return result.stdout.toString().trim();
628
+ } catch (error) {
629
+ return `Failed to execute command: ${error.message}`;
630
+ }
631
+ }
632
+ ```
633
+
634
+ ### 2. Security Considerations
635
+
636
+ - Validate all inputs to prevent command injection
637
+ - Use parameterized commands when possible
638
+ - Implement path traversal protection for file operations
639
+ - Sanitize output before returning to the LLM
640
+
641
+ ```typescript
642
+ // Secure file path handling
643
+ const safePath = path.normalize(userPath).replace(/^\.\./, '');
644
+ if (!safePath.startsWith(projectDir)) {
645
+ return 'Error: Access denied';
646
+ }
647
+ ```
648
+
649
+ ### 3. Performance Optimization
650
+
651
+ - Cache expensive operations when possible
652
+ - Use `--quiet` flag to reduce shell output
653
+ - Implement lazy loading for large datasets
654
+
655
+ ```typescript
656
+ let cache: string | null = null;
657
+
658
+ async getCachedData() {
659
+ if (!cache) {
660
+ cache = await fetchData();
661
+ }
662
+ return cache;
663
+ }
664
+ ```
665
+
666
+ ### 4. Context Management
667
+
668
+ - Use `session.created` to initialize state
669
+ - Use `session.compacting` to preserve critical information
670
+ - Clear cache on session reset
671
+
672
+ ```typescript
673
+ export const MyPlugin: Plugin = async ({ $ }) => {
674
+ let sessionState: any = null;
675
+
676
+ return {
677
+ event: async ({ event }) => {
678
+ if (event.type === 'session.created') {
679
+ sessionState = await initializeState();
680
+ }
681
+ },
682
+ 'experimental.session.compacting': async (_input, output) => {
683
+ // Save important state before compaction
684
+ output.context.push(`<saved-state>${JSON.stringify(sessionState)}</saved-state>`);
685
+ },
686
+ };
687
+ };
688
+ ```
689
+
690
+ ### 5. Testing Your Plugin
691
+
692
+ ```bash
693
+ # Test plugin loading
694
+ opencode --help
695
+
696
+ # Check if tools are registered
697
+ opencode run "List available tools" # Should show your new tools
698
+
699
+ # Manual testing
700
+ cd .opencode/plugin && npx tsc --noEmit # Type check
701
+ ```
702
+
703
+ ---
704
+
705
+ ## Troubleshooting
706
+
707
+ ### Plugin Not Loading
708
+
709
+ 1. Check file location: must be in `.opencode/plugin/`
710
+ 2. Verify TypeScript compilation: `npm run build`
711
+ 3. Check plugin syntax: `node -c .opencode/plugin/your-plugin.ts`
712
+ 4. Review OpenCode logs for errors
713
+
714
+ ### Tool Not Appearing to LLM
715
+
716
+ 1. Ensure tool description is clear and comprehensive
717
+ 2. Check tool name follows snake_case convention
718
+ 3. Verify tool schema has all required fields
719
+ 4. Restart OpenCode session after plugin changes
720
+
721
+ ### Performance Issues
722
+
723
+ 1. Add caching for expensive operations
724
+ 2. Use `--quiet` flag on shell commands
725
+ 3. Implement pagination for large results
726
+ 4. Consider async loading for heavy computations
727
+
728
+ ---
729
+
730
+ ## References
731
+
732
+ - **OpenCode Plugin API**: `@opencode-ai/plugin` package documentation
733
+ - **UAP Implementation**: See `.opencode/plugin/` in this repository for examples
734
+ - **Tool Schema Reference**: Check `uap-commands.ts` for tool definition patterns
735
+ - **Hook Examples**: See `uap-session-hooks.ts` and `uap-pattern-rag.ts`
736
+
737
+ ---
738
+
739
+ **Last Updated:** 2026-03-17
740
+ **Version:** 1.0.0