@aiderdesk/aiderdesk 0.61.0 → 0.61.1

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 (22) hide show
  1. package/out/resources/skills/extension-creator/SKILL.md +320 -0
  2. package/out/resources/skills/extension-creator/assets/templates/Component.jsx.template +76 -0
  3. package/out/resources/skills/extension-creator/assets/templates/ConfigComponent.jsx.template +38 -0
  4. package/out/resources/skills/extension-creator/assets/templates/folder-extension/config.ts.template +29 -0
  5. package/out/resources/skills/extension-creator/assets/templates/folder-extension/index.ts.template +42 -0
  6. package/out/resources/skills/extension-creator/assets/templates/folder-extension/package.json +12 -0
  7. package/out/resources/skills/extension-creator/assets/templates/folder-extension/tsconfig.json +23 -0
  8. package/out/resources/skills/extension-creator/assets/templates/folder-extension-with-config/index.ts.template +80 -0
  9. package/out/resources/skills/extension-creator/assets/templates/single-file.ts.template +30 -0
  10. package/out/resources/skills/extension-creator/assets/templates/ui-component-external.ts.template +91 -0
  11. package/out/resources/skills/extension-creator/assets/templates/ui-component.ts.template +79 -0
  12. package/out/resources/skills/extension-creator/references/command-definition.md +170 -0
  13. package/out/resources/skills/extension-creator/references/config-components.md +427 -0
  14. package/out/resources/skills/extension-creator/references/event-types.md +156 -0
  15. package/out/resources/skills/extension-creator/references/examples-gallery.md +583 -0
  16. package/out/resources/skills/extension-creator/references/extension-interface.md +240 -0
  17. package/out/resources/skills/extension-creator/references/extension-types.md +186 -0
  18. package/out/resources/skills/extension-creator/references/in-repo-flow.md +132 -0
  19. package/out/resources/skills/extension-creator/references/install-targets.md +96 -0
  20. package/out/resources/skills/extension-creator/references/project-global-flow.md +76 -0
  21. package/out/resources/skills/extension-creator/references/ui-components.md +663 -0
  22. package/package.json +1 -1
@@ -0,0 +1,583 @@
1
+ # Examples Gallery
2
+
3
+ Real extension examples from the AiderDesk repository.
4
+
5
+ ## Single-File Examples
6
+
7
+ ### Theme Switcher (Commands)
8
+
9
+ ```typescript
10
+ // theme.ts - Adds /theme command
11
+ import type { Extension, ExtensionContext, CommandDefinition } from '@aiderdesk/extensions';
12
+
13
+ const AVAILABLE_THEMES = ['dark', 'light', 'ocean', 'forest'];
14
+
15
+ const THEME_COMMAND: CommandDefinition = {
16
+ name: 'theme',
17
+ description: 'Switch the AiderDesk theme',
18
+ arguments: [
19
+ { description: 'Theme name', required: true, options: AVAILABLE_THEMES }
20
+ ],
21
+ async execute(args: string[], context: ExtensionContext): Promise<void> {
22
+ const themeName = args[0];
23
+ await context.updateSettings({ theme: themeName });
24
+ context.getTaskContext()?.addLogMessage('info', `Theme: ${themeName}`);
25
+ }
26
+ };
27
+
28
+ export default class ThemeExtension implements Extension {
29
+ static metadata = {
30
+ name: 'Theme Extension',
31
+ version: '1.0.0',
32
+ description: 'Adds /theme command',
33
+ author: 'my_git_name',
34
+ capabilities: ['commands'],
35
+ };
36
+
37
+ async onLoad(context: ExtensionContext): Promise<void> {
38
+ // context.log() outputs to backend console only; for user-visible messages use context.getTaskContext()?.addLogMessage()
39
+ context.log('Theme Extension loaded', 'info');
40
+ }
41
+
42
+ getCommands(_context: ExtensionContext): CommandDefinition[] {
43
+ return [THEME_COMMAND];
44
+ }
45
+ }
46
+ ```
47
+
48
+ ### Plan Mode (Modes + Events)
49
+
50
+ ```typescript
51
+ // plan-mode.ts - Adds Plan mode with prepended instructions
52
+ import type { Extension, ExtensionContext, ModeDefinition, AgentStartedEvent } from '@aiderdesk/extensions';
53
+
54
+ const PLAN_USER_MESSAGE = 'Create a plan before coding...';
55
+
56
+ export default class PlanModeExtension implements Extension {
57
+ static metadata = {
58
+ name: 'Plan Mode',
59
+ version: '1.0.0',
60
+ description: 'Adds Plan mode',
61
+ author: 'my_git_name',
62
+ capabilities: ['modes', 'events'],
63
+ };
64
+
65
+ getModes(): ModeDefinition[] {
66
+ return [{ name: 'plan', label: 'Plan', description: 'Plan first', icon: 'GoProjectRoadmap' }];
67
+ }
68
+
69
+ async onAgentStarted(event: AgentStartedEvent): Promise<Partial<AgentStartedEvent>> {
70
+ if (event.mode !== 'plan') return {};
71
+
72
+ return {
73
+ contextMessages: [
74
+ { id: 'plan-user', role: 'user', content: PLAN_USER_MESSAGE },
75
+ { id: 'plan-assistant', role: 'assistant', content: 'OK, I will plan first.' },
76
+ ...event.contextMessages
77
+ ]
78
+ };
79
+ }
80
+ }
81
+ ```
82
+
83
+ ### Permission Gate (Blocking)
84
+
85
+ ```typescript
86
+ // permission-gate.ts - Blocks dangerous commands
87
+ import type { Extension, ExtensionContext, ToolCalledEvent } from '@aiderdesk/extensions';
88
+
89
+ const DANGEROUS_PATTERNS = ['rm -rf', 'sudo', 'chmod 777'];
90
+
91
+ export default class PermissionGateExtension implements Extension {
92
+ static metadata = {
93
+ name: 'Permission Gate',
94
+ version: '1.0.0',
95
+ description: 'Prompts for dangerous commands',
96
+ author: 'my_git_name',
97
+ capabilities: ['events'],
98
+ };
99
+
100
+ async onToolCalled(event: ToolCalledEvent, context: ExtensionContext): Promise<Partial<ToolCalledEvent>> {
101
+ if (event.tool !== 'power---bash') return {};
102
+
103
+ const command = event.toolInput.command as string;
104
+ if (DANGEROUS_PATTERNS.some(p => command.includes(p))) {
105
+ const taskContext = context.getTaskContext();
106
+ const answer = await taskContext?.askQuestion(
107
+ `Allow dangerous command: ${command}?`,
108
+ { options: ['Yes', 'No'] }
109
+ );
110
+ if (answer !== 'Yes') {
111
+ return { blocked: true };
112
+ }
113
+ }
114
+ return {};
115
+ }
116
+ }
117
+ ```
118
+
119
+ ## Folder Examples
120
+
121
+ ### Tree-Sitter Repo Map (Folder + Dependencies)
122
+
123
+ ```
124
+ tree-sitter-repo-map/
125
+ ├── index.ts # Main extension
126
+ ├── config.ts # Config management
127
+ ├── logger.ts # Local logger
128
+ ├── constants.ts # Extension constants
129
+ ├── repo-map-manager.ts # Core functionality
130
+ ├── tree-sitter-parser.ts # Parser wrapper
131
+ ├── package.json # Dependencies
132
+ ├── tsconfig.json # TS config
133
+ ├── README.md # Docs
134
+ └── resources/
135
+ ├── wasm/ # Tree-sitter WASM files
136
+ └── queries/ # Language queries
137
+ ```
138
+
139
+ Key patterns:
140
+ - Local imports only (no `@/` paths)
141
+ - Config stored in extension directory
142
+ - Resources bundled with extension
143
+ - Singleton manager pattern
144
+
145
+ ### Sandbox (External Dependencies)
146
+
147
+ ```
148
+ sandbox/
149
+ ├── index.ts # Extension implementation
150
+ └── package.json # @anthropic-ai/sandbox-runtime
151
+ ```
152
+
153
+ ```json
154
+ // package.json
155
+ {
156
+ "dependencies": {
157
+ "@anthropic-ai/sandbox-runtime": "^0.0.38"
158
+ }
159
+ }
160
+ ```
161
+
162
+ ### TPS Counter (UI Components + Data)
163
+
164
+ ```typescript
165
+ // tps-counter/index.ts - Display tokens per second metrics
166
+ import { readFileSync } from 'fs';
167
+ import { join } from 'path';
168
+ import type {
169
+ Extension,
170
+ ExtensionContext,
171
+ UIComponentDefinition,
172
+ ResponseChunkEvent,
173
+ ResponseCompletedEvent
174
+ } from '@aiderdesk/extensions';
175
+
176
+ export default class TPSCounterExtension implements Extension {
177
+ static metadata = {
178
+ name: 'TPS Counter',
179
+ version: '1.0.0',
180
+ description: 'Displays tokens per second for responses',
181
+ author: 'my_git_name',
182
+ capabilities: ['metrics', 'ui'],
183
+ };
184
+
185
+ private messageStartTimes = new Map<string, number>();
186
+ private messageTpsData = new Map<string, { tps: number; tokens: number; duration: number }>();
187
+ private currentData = { averageTps: 0, messageCount: 0 };
188
+
189
+ async onResponseChunk(event: ResponseChunkEvent): Promise<void> {
190
+ const messageId = event.chunk.messageId;
191
+ if (!this.messageStartTimes.has(messageId)) {
192
+ this.messageStartTimes.set(messageId, Date.now());
193
+ }
194
+ }
195
+
196
+ async onResponseCompleted(event: ResponseCompletedEvent, context: ExtensionContext): Promise<void> {
197
+ const messageId = event.response.messageId;
198
+ const startTime = this.messageStartTimes.get(messageId);
199
+ if (!startTime) return;
200
+
201
+ const duration = (Date.now() - startTime) / 1000;
202
+ const tokens = event.response.usageReport?.receivedTokens || 0;
203
+ const tps = duration > 0 ? tokens / duration : 0;
204
+
205
+ this.messageTpsData.set(messageId, { tps, tokens, duration });
206
+ this.currentData.averageTps = this.calculateAverage();
207
+ this.currentData.messageCount++;
208
+
209
+ // Trigger UI refresh
210
+ context.triggerUIDataRefresh('tps-counter');
211
+ context.triggerUIDataRefresh('tps-counter-message-bar');
212
+ }
213
+
214
+ getUIComponents(): UIComponentDefinition[] {
215
+ return [
216
+ {
217
+ id: 'tps-counter',
218
+ placement: 'task-usage-info-bottom',
219
+ jsx: readFileSync(join(__dirname, './TPSCounter.jsx'), 'utf-8'),
220
+ loadData: true,
221
+ },
222
+ {
223
+ id: 'tps-counter-message-bar',
224
+ placement: 'task-message-bar',
225
+ jsx: readFileSync(join(__dirname, './TPSMessageBar.jsx'), 'utf-8'),
226
+ loadData: true,
227
+ },
228
+ ];
229
+ }
230
+
231
+ async getUIExtensionData(componentId: string): Promise<unknown> {
232
+ if (componentId === 'tps-counter') {
233
+ return this.currentData;
234
+ }
235
+ if (componentId === 'tps-counter-message-bar') {
236
+ return Object.fromEntries(this.messageTpsData);
237
+ }
238
+ return undefined;
239
+ }
240
+
241
+ private calculateAverage(): number {
242
+ // Calculate average TPS from all messages
243
+ const values = Array.from(this.messageTpsData.values());
244
+ return values.reduce((sum, v) => sum + v.tps, 0) / values.length;
245
+ }
246
+ }
247
+ ```
248
+
249
+ **TPSCounter.jsx:**
250
+ ```jsx
251
+ ({ data }) => {
252
+ if (!data || data.messageCount === 0) return null;
253
+
254
+ return (
255
+ <div className="flex items-center gap-1 text-2xs mt-1 w-full justify-between">
256
+ <span>Avg. tokens/s:</span>
257
+ <span>{Math.round(data.averageTps)}</span>
258
+ </div>
259
+ );
260
+ }
261
+ ```
262
+
263
+ **TPSMessageBar.jsx:**
264
+ ```jsx
265
+ ({ data, message }) => {
266
+ if (!data || !message?.id) return null;
267
+
268
+ const messageTps = data[message.id];
269
+ if (!messageTps) return null;
270
+
271
+ return (
272
+ <span
273
+ className="text-2xs mt-[4px] text-text-muted"
274
+ title={`${messageTps.tokens} tokens in ${messageTps.duration.toFixed(2)}s`}
275
+ >
276
+ {Math.round(messageTps.tps)} TPS
277
+ </span>
278
+ );
279
+ }
280
+ ```
281
+
282
+ ### Multi-Model Run (Interactive UI)
283
+
284
+ ```typescript
285
+ // multi-model-run/index.ts - Run prompts across multiple models
286
+ import { readFileSync } from 'fs';
287
+ import { join } from 'path';
288
+ import type {
289
+ Extension,
290
+ ExtensionContext,
291
+ UIComponentDefinition,
292
+ PromptStartedEvent
293
+ } from '@aiderdesk/extensions';
294
+
295
+ interface MultiModelState {
296
+ models: Array<{ index: number; modelId: string }>;
297
+ useWorktrees: boolean;
298
+ }
299
+
300
+ export default class MultiModelRunExtension implements Extension {
301
+ static metadata = {
302
+ name: 'Multi-Model Run',
303
+ version: '1.0.0',
304
+ description: 'Run prompts across multiple models',
305
+ author: 'my_git_name',
306
+ capabilities: ['ui', 'task-manipulation'],
307
+ };
308
+
309
+ private state: MultiModelState = { models: [], useWorktrees: true };
310
+
311
+ getUIComponents(): UIComponentDefinition[] {
312
+ return [{
313
+ id: 'multi-model-selector',
314
+ placement: 'task-input-above',
315
+ jsx: readFileSync(join(__dirname, './MultiModelSelector.jsx'), 'utf-8'),
316
+ loadData: true,
317
+ }];
318
+ }
319
+
320
+ async getUIExtensionData(componentId: string): Promise<unknown> {
321
+ if (componentId === 'multi-model-selector') {
322
+ return this.state;
323
+ }
324
+ return undefined;
325
+ }
326
+
327
+ async executeUIExtensionAction(
328
+ componentId: string,
329
+ action: string,
330
+ args: unknown[],
331
+ context: ExtensionContext
332
+ ): Promise<unknown> {
333
+ if (componentId !== 'multi-model-selector') return;
334
+
335
+ if (action === 'add-model') {
336
+ this.state.models.push({
337
+ index: this.state.models.length,
338
+ modelId: ''
339
+ });
340
+ context.triggerUIDataRefresh('multi-model-selector');
341
+ }
342
+
343
+ if (action === 'remove-model') {
344
+ const index = args[0] as number;
345
+ this.state.models = this.state.models.filter(m => m.index !== index);
346
+ context.triggerUIDataRefresh('multi-model-selector');
347
+ }
348
+
349
+ if (action === 'update-model') {
350
+ const [index, modelId] = args as [number, string];
351
+ const model = this.state.models.find(m => m.index === index);
352
+ if (model) {
353
+ model.modelId = modelId;
354
+ context.triggerUIDataRefresh('multi-model-selector');
355
+ }
356
+ }
357
+
358
+ if (action === 'set-use-worktrees') {
359
+ this.state.useWorktrees = args[0] as boolean;
360
+ context.triggerUIDataRefresh('multi-model-selector');
361
+ }
362
+
363
+ return { success: true };
364
+ }
365
+
366
+ async onPromptStarted(event: PromptStartedEvent, context: ExtensionContext): Promise<void> {
367
+ const selectedModels = this.state.models.filter(m => m.modelId);
368
+ if (selectedModels.length === 0) return;
369
+
370
+ // Create duplicate tasks for each additional model
371
+ const projectContext = context.getProjectContext();
372
+ const taskContext = context.getTaskContext();
373
+
374
+ for (const model of selectedModels.slice(1)) {
375
+ await projectContext.createTask({
376
+ name: `${taskContext?.data.name} (${model.modelId})`,
377
+ initialPrompt: event.prompt,
378
+ modelId: model.modelId,
379
+ });
380
+ }
381
+ }
382
+ }
383
+ ```
384
+
385
+ **MultiModelSelector.jsx:**
386
+ ```jsx
387
+ ({ data, models, providers, ui, executeExtensionAction }) => {
388
+ const { useCallback } = React;
389
+ const { Button, ModelSelector, Checkbox } = ui;
390
+
391
+ const modelSelections = data?.models ?? [];
392
+ const useWorktrees = data?.useWorktrees ?? true;
393
+
394
+ const handleAddModel = useCallback(async () => {
395
+ await executeExtensionAction('add-model');
396
+ }, [executeExtensionAction]);
397
+
398
+ const handleRemoveModel = useCallback((index) => async () => {
399
+ await executeExtensionAction('remove-model', index);
400
+ }, [executeExtensionAction]);
401
+
402
+ const handleModelChange = useCallback((index) => async (model) => {
403
+ const modelId = model ? `${model.providerId}/${model.id}` : '';
404
+ await executeExtensionAction('update-model', index, modelId);
405
+ }, [executeExtensionAction]);
406
+
407
+ const handleWorktreesChange = useCallback(async (checked) => {
408
+ await executeExtensionAction('set-use-worktrees', checked);
409
+ }, [executeExtensionAction]);
410
+
411
+ return (
412
+ <div className="flex flex-col gap-2 pb-2 w-full">
413
+ <div className="flex gap-4 w-full justify-between items-center">
414
+ <div className="flex items-center gap-2 flex-wrap">
415
+ {modelSelections.map((selection) => (
416
+ <div key={selection.index} className="flex items-center gap-1">
417
+ <ModelSelector
418
+ models={models}
419
+ providers={providers}
420
+ selectedModelId={selection.modelId}
421
+ onChange={handleModelChange(selection.index)}
422
+ />
423
+ <button
424
+ onClick={handleRemoveModel(selection.index)}
425
+ className="text-text-muted hover:text-text-primary"
426
+ >
427
+ ×
428
+ </button>
429
+ </div>
430
+ ))}
431
+ </div>
432
+ <div className="flex gap-2 items-center">
433
+ <Checkbox
434
+ label="Use worktrees"
435
+ checked={useWorktrees}
436
+ onChange={handleWorktreesChange}
437
+ size="xs"
438
+ />
439
+ <Button
440
+ variant="outline"
441
+ size="xs"
442
+ onClick={handleAddModel}
443
+ >
444
+ + Add model
445
+ </Button>
446
+ </div>
447
+ </div>
448
+ </div>
449
+ );
450
+ }
451
+ ```
452
+
453
+ ### External Rules (Config Component + Events)
454
+
455
+ Extension with a **config component** (settings dialog) that lets users configure additional rule folders, plus an event handler that uses the loaded config.
456
+
457
+ ```
458
+ external-rules/
459
+ ├── index.ts # Main extension with 3 config methods + event handler
460
+ ├── ConfigComponent.jsx # Settings UI (text input for comma-separated folders)
461
+ └── config.json # Persisted config: { "ruleFolders": "" }
462
+ ```
463
+
464
+ **index.ts:**
465
+ ```typescript
466
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
467
+ import { join, relative } from 'node:path';
468
+ import type { Extension, ExtensionContext, RuleFilesRetrievedEvent } from '@aiderdesk/extensions';
469
+
470
+ const configComponentJsx = readFileSync(join(__dirname, './ConfigComponent.jsx'), 'utf-8');
471
+
472
+ const DEFAULT_RULE_DIRECTORIES = ['.cursor/rules', '.roo/rules'];
473
+ const DEFAULT_ROOT_RULE_FILES = ['CLAUDE.md'];
474
+
475
+ interface ExternalRulesConfig {
476
+ ruleFolders: string;
477
+ }
478
+
479
+ const DEFAULT_CONFIG: ExternalRulesConfig = { ruleFolders: '' };
480
+
481
+ export default class ExternalRulesExtension implements Extension {
482
+ static metadata = {
483
+ name: 'External Rules',
484
+ version: '1.0.0',
485
+ description: 'Includes rule files from Cursor, Claude Code, and Roo Code',
486
+ author: 'author-name',
487
+ capabilities: ['context'],
488
+ };
489
+
490
+ private configPath: string;
491
+
492
+ constructor() {
493
+ this.configPath = join(__dirname, 'config.json');
494
+ }
495
+
496
+ async onLoad(context: ExtensionContext): Promise<void> {
497
+ // context.log() outputs to backend console only; for user-visible messages use context.getTaskContext()?.addLogMessage()
498
+ context.log('External Rules Extension loaded', 'info');
499
+ }
500
+
501
+ // --- Config Component API ---
502
+
503
+ getConfigComponent(_context: ExtensionContext): string {
504
+ return configComponentJsx;
505
+ }
506
+
507
+ async getConfigData(_context: ExtensionContext): Promise<ExternalRulesConfig> {
508
+ try {
509
+ if (existsSync(this.configPath)) {
510
+ const data = readFileSync(this.configPath, 'utf-8');
511
+ return { ...DEFAULT_CONFIG, ...JSON.parse(data) };
512
+ }
513
+ } catch { /* ignore */ }
514
+ return { ...DEFAULT_CONFIG };
515
+ }
516
+
517
+ async saveConfigData(configData: unknown, _context: ExtensionContext): Promise<unknown> {
518
+ const merged: ExternalRulesConfig = { ...DEFAULT_CONFIG, ...configData as Partial<ExternalRulesConfig> };
519
+ writeFileSync(this.configPath, JSON.stringify(merged, null, 2), 'utf-8');
520
+ return merged;
521
+ }
522
+
523
+ // --- Event Handler using loaded config ---
524
+
525
+ async onRuleFilesRetrieved(event: RuleFilesRetrievedEvent, context: ExtensionContext) {
526
+ const projectDir = context.getProjectDir();
527
+ if (!projectDir) return undefined;
528
+
529
+ // Load user-configured additional folders
530
+ const config = await this.getConfigData(context);
531
+ const additionalFolders = config.ruleFolders
532
+ ? config.ruleFolders.split(',').map((f) => f.trim()).filter(Boolean)
533
+ : [];
534
+
535
+ // Merge defaults with user folders
536
+ const allDirectories = [...DEFAULT_RULE_DIRECTORIES, ...additionalFolders];
537
+ // ... scan directories and return augmented files ...
538
+ }
539
+ }
540
+ ```
541
+
542
+ **ConfigComponent.jsx:**
543
+ ```jsx
544
+ ({ config, updateConfig, ui }) => {
545
+ const { Input } = ui;
546
+
547
+ return (
548
+ <div className="flex flex-col gap-4">
549
+ <Input
550
+ label="Additional Rule Folders"
551
+ value={config?.ruleFolders || ''}
552
+ onChange={(value) => updateConfig({ ...config, ruleFolders: value })}
553
+ placeholder=".custom-rules, .ai/rules"
554
+ />
555
+ <p className="text-xs text-text-secondary -mt-2">
556
+ Comma-separated folder paths relative to project root. Scanned in addition to built-in sources.
557
+ </p>
558
+ </div>
559
+ );
560
+ };
561
+ ```
562
+
563
+ **Key patterns shown:**
564
+ - JSX loaded from external file via `readFileSync` at module level
565
+ - Three-method config API: `getConfigComponent`, `getConfigData`, `saveConfigData`
566
+ - **No inner state**: reads directly from `config` prop, uses `ui.Input` instead of raw HTML
567
+ - Config used at runtime by event handlers (`onRuleFilesRetrieved` calls `this.getConfigData`)
568
+ - Default merging ensures missing fields are handled gracefully
569
+
570
+ ## Common Patterns Summary
571
+
572
+ | Pattern | Extension | Use Case |
573
+ |---------|-----------|----------|
574
+ | Commands | theme.ts | Add /commands |
575
+ | Modes | plan-mode.ts | Custom conversation modes |
576
+ | Blocking | permission-gate.ts | Prevent operations |
577
+ | Modifying | tree-sitter-repo-map | Change event data |
578
+ | Config | tree-sitter-repo-map | Persistent settings (internal) |
579
+ | **Config Component** | **external-rules** | **Settings UI dialog with Save/Cancel** |
580
+ | Agents | pirate.ts | Custom agent profiles |
581
+ | Tools | chunkhound-search | Custom AI tools |
582
+ | UI Display | tps-counter | Show information in UI |
583
+ | UI Interactive | multi-model-run | User input in UI |