@jupyternaut/persona 0.0.0 → 0.20.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 (49) hide show
  1. package/lib/chat-commands/mention.d.ts +9 -0
  2. package/lib/chat-commands/mention.js +30 -0
  3. package/lib/completion/completion-provider.d.ts +86 -0
  4. package/lib/completion/completion-provider.js +246 -0
  5. package/lib/completion/index.d.ts +2 -0
  6. package/lib/completion/index.js +1 -0
  7. package/lib/components/completion-status.d.ts +26 -0
  8. package/lib/components/completion-status.js +52 -0
  9. package/lib/components/index.d.ts +2 -0
  10. package/lib/components/index.js +1 -0
  11. package/lib/diff-manager.d.ts +25 -0
  12. package/lib/diff-manager.js +60 -0
  13. package/lib/index.d.ts +8 -0
  14. package/lib/index.js +522 -0
  15. package/lib/models/settings-model.d.ts +36 -0
  16. package/lib/models/settings-model.js +356 -0
  17. package/lib/persona-registry.d.ts +15 -0
  18. package/lib/persona-registry.js +29 -0
  19. package/lib/persona.d.ts +66 -0
  20. package/lib/persona.js +414 -0
  21. package/lib/process-attachments.d.ts +5 -0
  22. package/lib/process-attachments.js +287 -0
  23. package/lib/tokens.d.ts +101 -0
  24. package/lib/tokens.js +20 -0
  25. package/lib/widgets/ai-settings.d.ts +54 -0
  26. package/lib/widgets/ai-settings.js +572 -0
  27. package/lib/widgets/provider-config-dialog.d.ts +16 -0
  28. package/lib/widgets/provider-config-dialog.js +384 -0
  29. package/package.json +111 -7
  30. package/schema/settings-model.json +287 -0
  31. package/src/chat-commands/mention.tsx +46 -0
  32. package/src/completion/completion-provider.ts +350 -0
  33. package/src/completion/index.ts +1 -0
  34. package/src/components/completion-status.tsx +93 -0
  35. package/src/components/index.ts +1 -0
  36. package/src/diff-manager.ts +81 -0
  37. package/src/index.ts +710 -0
  38. package/src/models/settings-model.ts +415 -0
  39. package/src/persona-registry.ts +46 -0
  40. package/src/persona.ts +610 -0
  41. package/src/process-attachments.ts +369 -0
  42. package/src/tokens.ts +121 -0
  43. package/src/widgets/ai-settings.tsx +1308 -0
  44. package/src/widgets/provider-config-dialog.tsx +997 -0
  45. package/style/base.css +14 -0
  46. package/style/index.css +1 -0
  47. package/style/index.js +1 -0
  48. package/README.md +0 -3
  49. package/index.js +0 -1
package/src/index.ts ADDED
@@ -0,0 +1,710 @@
1
+ import {
2
+ anthropicProvider,
3
+ createBrowserFetchTool,
4
+ createDiscoverCommandsTool,
5
+ createDiscoverSkillsTool,
6
+ createExecuteCommandTool,
7
+ createLoadSkillTool,
8
+ genericProvider,
9
+ googleProvider,
10
+ loadSkillsFromPaths,
11
+ mistralProvider,
12
+ openaiProvider,
13
+ AgentManagerFactory,
14
+ IAgentManagerFactory,
15
+ IAISettingsModel,
16
+ IDiffManager,
17
+ IProviderRegistry,
18
+ IToolRegistry,
19
+ ISkillRegistry,
20
+ ProviderRegistry,
21
+ SECRETS_NAMESPACE,
22
+ SkillRegistry,
23
+ ToolRegistry
24
+ } from '@jupyternaut/agent';
25
+
26
+ import type { IAISecretsAccess } from '@jupyternaut/agent';
27
+
28
+ import {
29
+ ILayoutRestorer,
30
+ JupyterFrontEnd,
31
+ JupyterFrontEndPlugin
32
+ } from '@jupyterlab/application';
33
+
34
+ import { IChatTracker, IChatPanel, IChatCommandRegistry } from '@jupyter/chat';
35
+
36
+ import { ICommandPalette, IThemeManager } from '@jupyterlab/apputils';
37
+
38
+ import { ICompletionProviderManager } from '@jupyterlab/completer';
39
+
40
+ import { IDocumentManager } from '@jupyterlab/docmanager';
41
+
42
+ import { ISettingRegistry } from '@jupyterlab/settingregistry';
43
+
44
+ import { IStatusBar } from '@jupyterlab/statusbar';
45
+
46
+ import { PathExt } from '@jupyterlab/coreutils';
47
+
48
+ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
49
+
50
+ import { IFormRendererRegistry, settingsIcon } from '@jupyterlab/ui-components';
51
+
52
+ import { DisposableSet } from '@lumino/disposable';
53
+
54
+ import { IMcpManager } from 'jupyter-mcp-manager';
55
+
56
+ import { ISecretsManager, SecretsManager } from 'jupyter-secrets-manager';
57
+
58
+ import { MentionCommandProvider } from './chat-commands/mention';
59
+
60
+ import { AICompletionProvider } from './completion';
61
+
62
+ import { CompletionStatusWidget } from './components';
63
+
64
+ import { DiffManager } from './diff-manager';
65
+
66
+ import { AISettingsModel } from './models/settings-model';
67
+
68
+ import { PersonaRegistry } from './persona-registry';
69
+
70
+ import { CommandIds, IPersonaRegistry, DEFAULT_PERSONA } from './tokens';
71
+
72
+ import { AISettingsWidget } from './widgets/ai-settings';
73
+
74
+ namespace Private {
75
+ let aiSecretsToken: symbol | null = null;
76
+
77
+ export function setAISecretsToken(token: symbol | null): void {
78
+ aiSecretsToken = token;
79
+ }
80
+
81
+ export function createAISecretsAccess(
82
+ secretsManager?: ISecretsManager
83
+ ): IAISecretsAccess {
84
+ return {
85
+ get isAvailable() {
86
+ return !!(aiSecretsToken && secretsManager);
87
+ },
88
+ async get(id: string): Promise<string | undefined> {
89
+ if (!aiSecretsToken || !secretsManager) {
90
+ return;
91
+ }
92
+ const secret = await secretsManager.get(
93
+ aiSecretsToken,
94
+ SECRETS_NAMESPACE,
95
+ id
96
+ );
97
+ return secret?.value;
98
+ },
99
+ async set(id: string, value: string): Promise<void> {
100
+ if (!aiSecretsToken || !secretsManager) {
101
+ return;
102
+ }
103
+ await secretsManager.set(aiSecretsToken, SECRETS_NAMESPACE, id, {
104
+ namespace: SECRETS_NAMESPACE,
105
+ id,
106
+ value
107
+ });
108
+ },
109
+ async attach(
110
+ id: string,
111
+ input: HTMLInputElement,
112
+ callback?: (value: string) => void
113
+ ): Promise<void> {
114
+ if (!aiSecretsToken || !secretsManager) {
115
+ return;
116
+ }
117
+ await secretsManager.attach(
118
+ aiSecretsToken,
119
+ SECRETS_NAMESPACE,
120
+ id,
121
+ input,
122
+ callback
123
+ );
124
+ }
125
+ };
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Provider registry plugin
131
+ */
132
+ const providerRegistryPlugin: JupyterFrontEndPlugin<IProviderRegistry> = {
133
+ id: '@jupyternaut/persona:provider-registry',
134
+ description: 'AI provider registry',
135
+ autoStart: true,
136
+ provides: IProviderRegistry,
137
+ activate: () => {
138
+ return new ProviderRegistry();
139
+ }
140
+ };
141
+
142
+ /**
143
+ * Anthropic provider plugin
144
+ */
145
+ const anthropicProviderPlugin: JupyterFrontEndPlugin<void> = {
146
+ id: '@jupyternaut/persona:anthropic-provider',
147
+ description: 'Register Anthropic provider',
148
+ autoStart: true,
149
+ requires: [IProviderRegistry],
150
+ activate: (app: JupyterFrontEnd, providerRegistry: IProviderRegistry) => {
151
+ providerRegistry.registerProvider(anthropicProvider);
152
+ }
153
+ };
154
+
155
+ /**
156
+ * Google provider plugin
157
+ */
158
+ const googleProviderPlugin: JupyterFrontEndPlugin<void> = {
159
+ id: '@jupyternaut/persona:google-provider',
160
+ description: 'Register Google Generative AI provider',
161
+ autoStart: true,
162
+ requires: [IProviderRegistry],
163
+ activate: (app: JupyterFrontEnd, providerRegistry: IProviderRegistry) => {
164
+ providerRegistry.registerProvider(googleProvider);
165
+ }
166
+ };
167
+
168
+ /**
169
+ * Mistral provider plugin
170
+ */
171
+ const mistralProviderPlugin: JupyterFrontEndPlugin<void> = {
172
+ id: '@jupyternaut/persona:mistral-provider',
173
+ description: 'Register Mistral provider',
174
+ autoStart: true,
175
+ requires: [IProviderRegistry],
176
+ activate: (app: JupyterFrontEnd, providerRegistry: IProviderRegistry) => {
177
+ providerRegistry.registerProvider(mistralProvider);
178
+ }
179
+ };
180
+
181
+ /**
182
+ * OpenAI provider plugin
183
+ */
184
+ const openaiProviderPlugin: JupyterFrontEndPlugin<void> = {
185
+ id: '@jupyternaut/persona:openai-provider',
186
+ description: 'Register OpenAI provider',
187
+ autoStart: true,
188
+ requires: [IProviderRegistry],
189
+ activate: (app: JupyterFrontEnd, providerRegistry: IProviderRegistry) => {
190
+ providerRegistry.registerProvider(openaiProvider);
191
+ }
192
+ };
193
+
194
+ /**
195
+ * Generic provider plugin
196
+ */
197
+ const genericProviderPlugin: JupyterFrontEndPlugin<void> = {
198
+ id: '@jupyternaut/persona:generic-provider',
199
+ description: 'Register Generic OpenAI-compatible provider',
200
+ autoStart: true,
201
+ requires: [IProviderRegistry],
202
+ activate: (app: JupyterFrontEnd, providerRegistry: IProviderRegistry) => {
203
+ providerRegistry.registerProvider(genericProvider);
204
+ }
205
+ };
206
+
207
+ /**
208
+ * Provides the persona handler registry without any IChatTracker dependency,
209
+ * so it can be consumed by the toolbar factory without creating a plugin cycle.
210
+ */
211
+ const personaRegistry: JupyterFrontEndPlugin<IPersonaRegistry> = {
212
+ id: '@jupyternaut/persona:registry',
213
+ description: 'Registry mapping chat models to their persona handlers',
214
+ autoStart: true,
215
+ provides: IPersonaRegistry,
216
+ requires: [IAISettingsModel],
217
+ optional: [IProviderRegistry, IDocumentManager],
218
+ activate: (
219
+ app: JupyterFrontEnd,
220
+ settingsModel: IAISettingsModel,
221
+ providerRegistry?: IProviderRegistry,
222
+ documentManager?: IDocumentManager
223
+ ): IPersonaRegistry => {
224
+ return new PersonaRegistry({
225
+ persona: DEFAULT_PERSONA,
226
+ settingsModel,
227
+ providerRegistry,
228
+ documentManager
229
+ });
230
+ }
231
+ };
232
+
233
+ /**
234
+ * Connects the persona handler registry to the chat tracker, creating a
235
+ * PersonaHandler for each chat widget that is opened.
236
+ */
237
+ const persona: JupyterFrontEndPlugin<void> = {
238
+ id: '@jupyternaut/persona:plugin',
239
+ description: 'Attach persona handlers to chat widgets as they are opened',
240
+ autoStart: true,
241
+ requires: [IPersonaRegistry, IAgentManagerFactory, IAISettingsModel],
242
+ optional: [IChatTracker, IProviderRegistry, IToolRegistry],
243
+ activate: (
244
+ app: JupyterFrontEnd,
245
+ registry: IPersonaRegistry,
246
+ agentManagerFactory: IAgentManagerFactory,
247
+ settingsModel: IAISettingsModel,
248
+ chatTracker: IChatTracker | null,
249
+ providerRegistry?: IProviderRegistry,
250
+ toolRegistry?: IToolRegistry
251
+ ): void => {
252
+ const attachPersona = (widget: IChatPanel) => {
253
+ if (registry.get(widget.model)) {
254
+ return;
255
+ }
256
+
257
+ const agentManager = agentManagerFactory.createAgent({
258
+ settingsModel,
259
+ providerRegistry,
260
+ toolRegistry
261
+ });
262
+
263
+ registry.register(widget.model, agentManager);
264
+ widget.disposed.connect(() => {
265
+ registry.unregister(widget.model);
266
+ });
267
+ };
268
+
269
+ chatTracker?.forEach(widget => attachPersona(widget));
270
+ chatTracker?.widgetAdded.connect((_, widget) => attachPersona(widget));
271
+ }
272
+ };
273
+
274
+ /**
275
+ * Clear chat command plugin.
276
+ */
277
+ const mentionCommandPlugin: JupyterFrontEndPlugin<void> = {
278
+ id: '@jupyternaut/persona:mention',
279
+ description: 'Register the Jupyternaut mention chat command.',
280
+ autoStart: true,
281
+ requires: [IChatCommandRegistry],
282
+ activate: (app, registry: IChatCommandRegistry) => {
283
+ registry.addProvider(new MentionCommandProvider());
284
+ }
285
+ };
286
+
287
+ /**
288
+ * A plugin to provide the agent manager factory and completion provider.
289
+ * These objects require the secrets manager token with the same namespace.
290
+ */
291
+ const agentManagerFactory: JupyterFrontEndPlugin<IAgentManagerFactory> =
292
+ SecretsManager.sign(SECRETS_NAMESPACE, token => {
293
+ Private.setAISecretsToken(token);
294
+
295
+ return {
296
+ id: SECRETS_NAMESPACE,
297
+ description: 'Provide the AI agent manager',
298
+ autoStart: true,
299
+ provides: IAgentManagerFactory,
300
+ requires: [IAISettingsModel, IProviderRegistry],
301
+ optional: [
302
+ ISkillRegistry,
303
+ ICompletionProviderManager,
304
+ ISecretsManager,
305
+ IMcpManager
306
+ ],
307
+ activate: (
308
+ app: JupyterFrontEnd,
309
+ settingsModel: IAISettingsModel,
310
+ providerRegistry: IProviderRegistry,
311
+ skillRegistry?: ISkillRegistry,
312
+ completionManager?: ICompletionProviderManager,
313
+ secretsManager?: ISecretsManager,
314
+ mcpManager?: IMcpManager
315
+ ): IAgentManagerFactory => {
316
+ const agentManagerFactory = new AgentManagerFactory({
317
+ settingsModel,
318
+ skillRegistry,
319
+ mcpManager,
320
+ secretsManager,
321
+ token
322
+ });
323
+
324
+ // Build the completion provider
325
+ if (completionManager) {
326
+ const completionProvider = new AICompletionProvider({
327
+ settingsModel,
328
+ providerRegistry,
329
+ secretsManager,
330
+ token
331
+ });
332
+
333
+ completionManager.registerInlineProvider(completionProvider);
334
+ } else {
335
+ console.info(
336
+ 'Completion provider manager not available, skipping AI completion setup'
337
+ );
338
+ }
339
+
340
+ return agentManagerFactory;
341
+ }
342
+ };
343
+ });
344
+
345
+ /**
346
+ * AI settings panel plugin.
347
+ */
348
+ const settingsPanelPlugin: JupyterFrontEndPlugin<void> = {
349
+ id: '@jupyternaut/persona:settings-panel',
350
+ description: 'Provide the AI settings panel',
351
+ autoStart: true,
352
+ requires: [IAISettingsModel, IAgentManagerFactory, IProviderRegistry],
353
+ optional: [
354
+ ICommandPalette,
355
+ ILayoutRestorer,
356
+ ISecretsManager,
357
+ IThemeManager,
358
+ ITranslator,
359
+ IFormRendererRegistry,
360
+ // This token is not used, but depending on it ensures that the renderer has been
361
+ // added to the form renderer registry.
362
+ IMcpManager
363
+ ],
364
+ activate: (
365
+ app: JupyterFrontEnd,
366
+ settingsModel: IAISettingsModel,
367
+ agentManagerFactory: IAgentManagerFactory,
368
+ providerRegistry: IProviderRegistry,
369
+ palette?: ICommandPalette,
370
+ restorer?: ILayoutRestorer,
371
+ secretsManager?: ISecretsManager,
372
+ themeManager?: IThemeManager,
373
+ translator?: ITranslator,
374
+ formRenderer?: IFormRendererRegistry
375
+ ): void => {
376
+ const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
377
+ const secretsAccess = Private.createAISecretsAccess(secretsManager);
378
+
379
+ // Get the renderer for MCP servers settings
380
+ const mcpServerRenderer = formRenderer?.getRenderer(
381
+ 'jupyter-mcp-manager:manager.mcpSettings'
382
+ ).fieldRenderer;
383
+
384
+ const settingsWidget = new AISettingsWidget({
385
+ settingsModel,
386
+ agentManagerFactory,
387
+ themeManager,
388
+ providerRegistry,
389
+ secretsAccess,
390
+ trans,
391
+ mcpServerRenderer
392
+ });
393
+ settingsWidget.title.icon = settingsIcon;
394
+ settingsWidget.title.iconClass = 'jp-ai-settings-icon';
395
+
396
+ const open = () => {
397
+ let widget = Array.from(app.shell.widgets('main')).find(
398
+ w => w.id === 'jupyternaut-persona-settings'
399
+ ) as AISettingsWidget | undefined;
400
+
401
+ if (!widget) {
402
+ widget = settingsWidget;
403
+ app.shell.add(widget, 'main');
404
+ }
405
+
406
+ app.shell.activateById(widget.id);
407
+ };
408
+
409
+ if (restorer) {
410
+ restorer.add(settingsWidget, settingsWidget.id);
411
+ }
412
+
413
+ app.commands.addCommand(CommandIds.openSettings, {
414
+ label: trans.__('Jupyternaut Settings'),
415
+ caption: trans.__('Configure AI providers and behavior'),
416
+ icon: settingsIcon,
417
+ iconClass: 'jp-ai-settings-icon',
418
+ execute: () => {
419
+ open();
420
+ },
421
+ describedBy: {
422
+ args: {}
423
+ }
424
+ });
425
+
426
+ if (palette) {
427
+ palette.addItem({
428
+ command: CommandIds.openSettings,
429
+ category: trans.__('AI Assistant')
430
+ });
431
+ }
432
+ }
433
+ };
434
+
435
+ /**
436
+ * Built-in completion providers plugin
437
+ */
438
+ const settingsModel: JupyterFrontEndPlugin<IAISettingsModel> = {
439
+ id: '@jupyternaut/persona:settings-model',
440
+ description: 'Provide the AI settings model',
441
+ autoStart: true,
442
+ provides: IAISettingsModel,
443
+ requires: [ISettingRegistry],
444
+ activate: (
445
+ app: JupyterFrontEnd,
446
+ settingRegistry: ISettingRegistry
447
+ ): IAISettingsModel => {
448
+ return new AISettingsModel({ settingRegistry });
449
+ }
450
+ };
451
+
452
+ /**
453
+ * Diff manager plugin
454
+ */
455
+ const diffManager: JupyterFrontEndPlugin<IDiffManager> = {
456
+ id: '@jupyternaut/persona:diff-manager',
457
+ description: 'Provide the diff manager for notebook cell diffs',
458
+ autoStart: true,
459
+ provides: IDiffManager,
460
+ requires: [IAISettingsModel],
461
+ activate: (
462
+ app: JupyterFrontEnd,
463
+ settingsModel: IAISettingsModel
464
+ ): IDiffManager => {
465
+ return new DiffManager({
466
+ commands: app.commands,
467
+ settingsModel
468
+ });
469
+ }
470
+ };
471
+
472
+ /**
473
+ * Skill registry plugin
474
+ */
475
+ const skillRegistryPlugin: JupyterFrontEndPlugin<ISkillRegistry> = {
476
+ id: '@jupyternaut/persona:skill-registry',
477
+ description: 'Provide the skill registry',
478
+ autoStart: true,
479
+ provides: ISkillRegistry,
480
+ activate: () => {
481
+ return new SkillRegistry();
482
+ }
483
+ };
484
+
485
+ const toolRegistry: JupyterFrontEndPlugin<IToolRegistry> = {
486
+ id: '@jupyternaut/persona:tool-registry',
487
+ description: 'Provide the AI tool registry',
488
+ autoStart: true,
489
+ optional: [ISkillRegistry],
490
+ provides: IToolRegistry,
491
+ activate: (app: JupyterFrontEnd, skillRegistry?: ISkillRegistry) => {
492
+ const toolRegistry = new ToolRegistry();
493
+
494
+ // Add command operation tools
495
+ const discoverCommandsTool = createDiscoverCommandsTool(app.commands);
496
+ const executeCommandTool = createExecuteCommandTool(app.commands);
497
+
498
+ toolRegistry.add('discover_commands', discoverCommandsTool);
499
+ toolRegistry.add('execute_command', executeCommandTool);
500
+ toolRegistry.add('browser_fetch', createBrowserFetchTool());
501
+ if (skillRegistry) {
502
+ toolRegistry.add(
503
+ 'discover_skills',
504
+ createDiscoverSkillsTool(skillRegistry)
505
+ );
506
+ toolRegistry.add('load_skill', createLoadSkillTool(skillRegistry));
507
+ }
508
+
509
+ return toolRegistry;
510
+ }
511
+ };
512
+
513
+ const completionStatus: JupyterFrontEndPlugin<void> = {
514
+ id: '@jupyternaut/persona:completion-status',
515
+ description: 'The completion status displayed in the status bar',
516
+ autoStart: true,
517
+ requires: [IAISettingsModel],
518
+ optional: [IStatusBar, ITranslator],
519
+ activate: (
520
+ app: JupyterFrontEnd,
521
+ settingsModel: IAISettingsModel,
522
+ statusBar: IStatusBar | null,
523
+ translator?: ITranslator
524
+ ) => {
525
+ if (!statusBar) {
526
+ return;
527
+ }
528
+ const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
529
+ const item = new CompletionStatusWidget({
530
+ settingsModel,
531
+ translator: trans
532
+ });
533
+ statusBar?.registerStatusItem('completionState', {
534
+ item,
535
+ align: 'right',
536
+ rank: 10
537
+ });
538
+ }
539
+ };
540
+
541
+ /**
542
+ * Skills plugin: discovers and registers agent skills from the filesystem.
543
+ */
544
+ const skillsPlugin: JupyterFrontEndPlugin<void> = {
545
+ id: '@jupyternaut/persona:skills',
546
+ description: 'Discover and register agent skills',
547
+ autoStart: true,
548
+ requires: [IAISettingsModel, IDocumentManager, ISkillRegistry],
549
+ optional: [ICommandPalette, ITranslator],
550
+ activate: async (
551
+ app: JupyterFrontEnd,
552
+ settingsModel: IAISettingsModel,
553
+ docManager: IDocumentManager,
554
+ skillRegistry: ISkillRegistry,
555
+ palette?: ICommandPalette,
556
+ translator?: ITranslator
557
+ ) => {
558
+ const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
559
+ const validateResourcePath = (resourcePath: string): string | null => {
560
+ if (resourcePath.startsWith('/')) {
561
+ return null;
562
+ }
563
+
564
+ const normalized = PathExt.normalize(resourcePath);
565
+ if (normalized.startsWith('..') || normalized === '') {
566
+ return null;
567
+ }
568
+
569
+ return normalized;
570
+ };
571
+
572
+ let currentSkillsPaths = settingsModel.config.skillsPaths;
573
+ let currentSkillDisposables = new DisposableSet();
574
+
575
+ const loadAndRegister = async () => {
576
+ const skillsPaths = settingsModel.config.skillsPaths;
577
+ const skills = await loadSkillsFromPaths(
578
+ docManager.services.contents,
579
+ skillsPaths
580
+ );
581
+
582
+ const registrations = skills.map(skill => ({
583
+ name: skill.name,
584
+ description: skill.description,
585
+ instructions: skill.instructions,
586
+ resources: skill.resources,
587
+ loadResource: async (resource: string) => {
588
+ const validatedPath = validateResourcePath(resource);
589
+ if (validatedPath === null) {
590
+ return {
591
+ name: skill.name,
592
+ resource,
593
+ error: 'Invalid resource path: path traversal not allowed'
594
+ };
595
+ }
596
+
597
+ if (!skill.resources.includes(validatedPath)) {
598
+ return {
599
+ name: skill.name,
600
+ resource,
601
+ error: `Resource not found: ${resource}`
602
+ };
603
+ }
604
+
605
+ const resourcePath = `${skill.path}/${validatedPath}`;
606
+ try {
607
+ const fileModel = await docManager.services.contents.get(
608
+ resourcePath,
609
+ {
610
+ content: true
611
+ }
612
+ );
613
+ if (typeof fileModel.content !== 'string') {
614
+ return {
615
+ name: skill.name,
616
+ resource,
617
+ error: 'Resource content is not a string'
618
+ };
619
+ }
620
+ return {
621
+ name: skill.name,
622
+ resource,
623
+ content: fileModel.content
624
+ };
625
+ } catch (error) {
626
+ return {
627
+ name: skill.name,
628
+ resource,
629
+ error: `Failed to read resource: ${error}`
630
+ };
631
+ }
632
+ }
633
+ }));
634
+
635
+ currentSkillDisposables.dispose();
636
+ currentSkillDisposables = new DisposableSet();
637
+ for (const registration of registrations) {
638
+ currentSkillDisposables.add(skillRegistry.registerSkill(registration));
639
+ }
640
+ };
641
+
642
+ app.commands.addCommand(CommandIds.refreshSkills, {
643
+ label: trans.__('Refresh Agents Skills'),
644
+ caption: trans.__(
645
+ 'Re-scan the agents skills directory and update the registry'
646
+ ),
647
+ execute: async () => {
648
+ await loadAndRegister();
649
+ }
650
+ });
651
+
652
+ if (palette) {
653
+ palette.addItem({
654
+ command: CommandIds.refreshSkills,
655
+ category: trans.__('AI Assistant')
656
+ });
657
+ }
658
+
659
+ loadAndRegister().catch(error =>
660
+ console.warn('Failed to load skills on activation:', error)
661
+ );
662
+
663
+ settingsModel.stateChanged.connect(() => {
664
+ const newPaths = settingsModel.config.skillsPaths;
665
+ if (
666
+ newPaths.length === currentSkillsPaths.length &&
667
+ newPaths.every((p, i) => p === currentSkillsPaths[i])
668
+ ) {
669
+ return;
670
+ }
671
+ currentSkillsPaths = newPaths;
672
+ loadAndRegister().catch(error =>
673
+ console.warn('Failed to reload skills:', error)
674
+ );
675
+ });
676
+ }
677
+ };
678
+
679
+ export default [
680
+ // Provider registry and builtin providers
681
+ providerRegistryPlugin,
682
+ anthropicProviderPlugin,
683
+ googleProviderPlugin,
684
+ mistralProviderPlugin,
685
+ openaiProviderPlugin,
686
+ genericProviderPlugin,
687
+ // Agent
688
+ agentManagerFactory,
689
+ completionStatus,
690
+ // Skills
691
+ skillRegistryPlugin,
692
+ skillsPlugin,
693
+ // Tools
694
+ toolRegistry,
695
+ // Persona
696
+ personaRegistry,
697
+ persona,
698
+ mentionCommandPlugin,
699
+ // Settings
700
+ settingsModel,
701
+ settingsPanelPlugin,
702
+ // Diff manager (to be removed ?)
703
+ diffManager
704
+ ];
705
+
706
+ // Export extension points for other extensions to use
707
+ export * from './tokens';
708
+
709
+ // Export helper functions
710
+ export { processAttachments } from './process-attachments';