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