@critical-path/svelte 0.10.2 → 0.12.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,24 @@
1
+ import type { CriticalPathClient } from '@critical-path/client';
2
+ import type { Task, TaskStatus, StatusDefinition } from '@critical-path/core';
3
+ export interface KanbanStateOptions {
4
+ groupBy?: 'workflow' | 'semantic';
5
+ customDefinitions?: StatusDefinition[];
6
+ }
7
+ export declare class KanbanState {
8
+ projectId?: string | undefined;
9
+ options: KanbanStateOptions;
10
+ private taskState;
11
+ get data(): Task[];
12
+ get tasks(): Task[];
13
+ get loading(): boolean;
14
+ get error(): Error | null;
15
+ columns: Record<string, Task[]>;
16
+ constructor(client: CriticalPathClient, projectId?: string | undefined, options?: KanbanStateOptions);
17
+ fetch(): Promise<void>;
18
+ createTask(input: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task>;
19
+ moveTask(taskId: string, targetStatus: TaskStatus): Promise<Task>;
20
+ updateTaskStatus(taskId: string, status: TaskStatus): Promise<Task>;
21
+ deleteTask(taskId: string): Promise<boolean | undefined>;
22
+ }
23
+ export declare function createKanbanState(client: CriticalPathClient, projectId?: string, options?: KanbanStateOptions): KanbanState;
24
+ //# sourceMappingURL=kanban-state.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kanban-state.svelte.d.ts","sourceRoot":"","sources":["../src/kanban-state.svelte.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,gBAAgB,EAAkB,MAAM,qBAAqB,CAAC;AAI9F,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;IAClC,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,CAAC;CACxC;AAED,qBAAa,WAAW;IAmEb,SAAS,CAAC,EAAE,MAAM;IAClB,OAAO,EAAE,kBAAkB;IAnEpC,OAAO,CAAC,SAAS,CAAY;IAE7B,IAAI,IAAI,IAAI,IAAI,EAAE,CAEjB;IAED,IAAI,KAAK,IAAI,IAAI,EAAE,CAElB;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,KAAK,IAAI,KAAK,GAAG,IAAI,CAExB;IAED,OAAO,yBA4CJ;gBAGD,MAAM,EAAE,kBAAkB,EACnB,SAAS,CAAC,EAAE,MAAM,YAAA,EAClB,OAAO,GAAE,kBAAuB;IAKnC,KAAK;IAIL,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC;IAI9D,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,UAAU;IAIjD,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU;IAInD,UAAU,CAAC,MAAM,EAAE,MAAM;CAGhC;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,kBAAkB,EAC1B,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,kBAAkB,GAC3B,WAAW,CAEb"}
@@ -0,0 +1,12 @@
1
+ import type { CriticalPathClient } from '@critical-path/client';
2
+ export declare class TaskTransitionsState {
3
+ private client;
4
+ taskId?: string | undefined;
5
+ allowedTransitions: string[];
6
+ loading: boolean;
7
+ error: Error | null;
8
+ constructor(client: CriticalPathClient, taskId?: string | undefined);
9
+ fetch(taskId?: string): Promise<void>;
10
+ }
11
+ export declare function createTaskTransitionsState(client: CriticalPathClient, taskId?: string): TaskTransitionsState;
12
+ //# sourceMappingURL=task-transitions-state.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-transitions-state.svelte.d.ts","sourceRoot":"","sources":["../src/task-transitions-state.svelte.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,qBAAa,oBAAoB;IAKnB,OAAO,CAAC,MAAM;IAA6B,MAAM,CAAC,EAAE,MAAM;IAJtE,kBAAkB,WAAwB;IAC1C,OAAO,UAA0B;IACjC,KAAK,eAA8B;gBAEf,MAAM,EAAE,kBAAkB,EAAS,MAAM,CAAC,EAAE,MAAM,YAAA;IAEhE,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM;CAkB5B;AAED,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,kBAAkB,EAC1B,MAAM,CAAC,EAAE,MAAM,GACd,oBAAoB,CAEtB"}
@@ -0,0 +1,26 @@
1
+ import type { CriticalPathClient } from '@critical-path/client';
2
+ export interface WebMcpStateOptions {
3
+ projectId?: string;
4
+ tools?: string[];
5
+ autoRegister?: boolean;
6
+ document?: any;
7
+ navigator?: any;
8
+ window?: any;
9
+ onToolExecuted?: (toolName: string, input: any, result: any) => void;
10
+ }
11
+ export declare class WebMcpState {
12
+ private client;
13
+ private options;
14
+ registered: boolean;
15
+ activeProjectId: string | undefined;
16
+ tools: string[];
17
+ error: Error | null;
18
+ private handle;
19
+ private abortController;
20
+ constructor(client: CriticalPathClient, options?: WebMcpStateOptions);
21
+ register(): void;
22
+ unregister(): void;
23
+ setProjectId(projectId?: string): void;
24
+ }
25
+ export declare function createWebMcpState(client: CriticalPathClient, options?: WebMcpStateOptions): WebMcpState;
26
+ //# sourceMappingURL=webmcp-state.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webmcp-state.svelte.d.ts","sourceRoot":"","sources":["../src/webmcp-state.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAGhE,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,SAAS,CAAC,EAAE,GAAG,CAAC;IAChB,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;CACtE;AAED,qBAAa,WAAW;IASV,OAAO,CAAC,MAAM;IAAsB,OAAO,CAAC,OAAO;IAR/D,UAAU,UAA0B;IACpC,eAAe,qBAAyC;IACxD,KAAK,WAAwB;IAC7B,KAAK,eAA8B;IAEnC,OAAO,CAAC,MAAM,CAAqC;IACnD,OAAO,CAAC,eAAe,CAAgC;gBAEnC,MAAM,EAAE,kBAAkB,EAAU,OAAO,GAAE,kBAAuB;IAOxF,QAAQ,IAAI,IAAI;IA0BhB,UAAU,IAAI,IAAI;IAUlB,YAAY,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;CAQvC;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,WAAW,CAEvG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@critical-path/svelte",
3
- "version": "0.10.2",
3
+ "version": "0.12.0",
4
4
  "description": "Svelte 5 Runes state and reactive integrations for Critical Path headless project management framework",
5
5
  "author": "Jack James",
6
6
  "license": "MIT",
@@ -19,7 +19,8 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@critical-path/client": "0.9.2",
22
- "@critical-path/core": "0.15.2"
22
+ "@critical-path/core": "0.15.2",
23
+ "@critical-path/mcp": "0.2.0"
23
24
  },
24
25
  "peerDependencies": {
25
26
  "svelte": "^5.0.0"
@@ -0,0 +1,41 @@
1
+ /// <reference types="svelte" />
2
+ import type { CriticalPathClient } from '@critical-path/client';
3
+ import type { DeliverableSummary } from '@critical-path/core';
4
+
5
+ export class DeliverableSummaryState {
6
+ summary = $state<DeliverableSummary | null>(null);
7
+ loading = $state<boolean>(false);
8
+ error = $state<Error | null>(null);
9
+
10
+ get data(): DeliverableSummary | null {
11
+ return this.summary;
12
+ }
13
+
14
+ constructor(private client: CriticalPathClient, public deliverableId?: string) {}
15
+
16
+ async fetch(deliverableId?: string) {
17
+ const targetId = deliverableId || this.deliverableId;
18
+ if (!targetId) {
19
+ this.summary = null;
20
+ this.loading = false;
21
+ return;
22
+ }
23
+ this.deliverableId = targetId;
24
+ this.loading = true;
25
+ this.error = null;
26
+ try {
27
+ this.summary = await this.client.getDeliverableSummary(targetId);
28
+ } catch (err) {
29
+ this.error = err instanceof Error ? err : new Error(String(err));
30
+ } finally {
31
+ this.loading = false;
32
+ }
33
+ }
34
+ }
35
+
36
+ export function createDeliverableSummaryState(
37
+ client: CriticalPathClient,
38
+ deliverableId?: string
39
+ ): DeliverableSummaryState {
40
+ return new DeliverableSummaryState(client, deliverableId);
41
+ }
package/src/index.test.ts CHANGED
@@ -14,7 +14,15 @@ import {
14
14
  TaskActivityState,
15
15
  createTaskActivityState,
16
16
  DeliverableState,
17
- createDeliverableState
17
+ createDeliverableState,
18
+ DeliverableSummaryState,
19
+ createDeliverableSummaryState,
20
+ KanbanState,
21
+ createKanbanState,
22
+ TaskTransitionsState,
23
+ createTaskTransitionsState,
24
+ WebMcpState,
25
+ createWebMcpState
18
26
  } from './index.js';
19
27
  import type { CriticalPathClient } from '@critical-path/client';
20
28
  import type { Project, Task, Workflow, Comment, Attachment, Deliverable, DeliverableSummary } from '@critical-path/core';
@@ -29,6 +37,9 @@ describe('@critical-path/svelte Svelte 5 Runes Test Suite', () => {
29
37
  expect(createAttachmentState).toBeDefined();
30
38
  expect(createTaskActivityState).toBeDefined();
31
39
  expect(createDeliverableState).toBeDefined();
40
+ expect(createDeliverableSummaryState).toBeDefined();
41
+ expect(createKanbanState).toBeDefined();
42
+ expect(createTaskTransitionsState).toBeDefined();
32
43
  expect(ProjectState).toBeDefined();
33
44
  expect(TaskState).toBeDefined();
34
45
  expect(WorkflowState).toBeDefined();
@@ -36,6 +47,11 @@ describe('@critical-path/svelte Svelte 5 Runes Test Suite', () => {
36
47
  expect(AttachmentState).toBeDefined();
37
48
  expect(TaskActivityState).toBeDefined();
38
49
  expect(DeliverableState).toBeDefined();
50
+ expect(DeliverableSummaryState).toBeDefined();
51
+ expect(KanbanState).toBeDefined();
52
+ expect(TaskTransitionsState).toBeDefined();
53
+ expect(WebMcpState).toBeDefined();
54
+ expect(createWebMcpState).toBeDefined();
39
55
  });
40
56
 
41
57
  describe('WorkflowState', () => {
@@ -408,5 +424,151 @@ describe('@critical-path/svelte Svelte 5 Runes Test Suite', () => {
408
424
  expect(state.data).toEqual([]);
409
425
  });
410
426
  });
427
+
428
+ describe('WebMcpState', () => {
429
+ it('manages WebMCP registration, project scoping, and unregistration', async () => {
430
+ const mockDoc: any = {};
431
+ const mockWin: any = {};
432
+
433
+ const mockClient = {
434
+ getTasks: vi.fn().mockResolvedValue([]),
435
+ createTask: vi.fn().mockImplementation((input) => Promise.resolve({ id: 't1', ...input }))
436
+ } as unknown as CriticalPathClient;
437
+
438
+ const mcpState = createWebMcpState(mockClient, {
439
+ projectId: 'proj_svelte',
440
+ document: mockDoc,
441
+ window: mockWin
442
+ });
443
+
444
+ expect(mcpState.registered).toBe(true);
445
+ expect(mcpState.activeProjectId).toBe('proj_svelte');
446
+ expect(mcpState.tools.length).toBeGreaterThan(0);
447
+ expect(mockDoc.modelContext).toBeDefined();
448
+
449
+ // Execute tool via modelContext
450
+ const created = await mockDoc.modelContext.executeTool('create_task', {
451
+ title: 'Svelte 5 Task'
452
+ });
453
+ expect(created.projectId).toBe('proj_svelte');
454
+ expect(created.title).toBe('Svelte 5 Task');
455
+
456
+ // Update project ID dynamically
457
+ mcpState.setProjectId('proj_svelte_2');
458
+ expect(mcpState.activeProjectId).toBe('proj_svelte_2');
459
+ expect(mcpState.registered).toBe(true);
460
+
461
+ const created2 = await mockDoc.modelContext.executeTool('create_task', {
462
+ title: 'Svelte 5 Task 2'
463
+ });
464
+ expect(created2.projectId).toBe('proj_svelte_2');
465
+
466
+ // Unregister
467
+ mcpState.unregister();
468
+ expect(mcpState.registered).toBe(false);
469
+ expect(mcpState.tools).toEqual([]);
470
+ });
471
+ });
472
+
473
+ describe('KanbanState', () => {
474
+ it('initializes and buckets tasks into workflow and semantic columns', async () => {
475
+ const mockTasks: Task[] = [
476
+ {
477
+ id: 't_todo',
478
+ projectId: 'p1',
479
+ title: 'To Do Task',
480
+ status: 'todo',
481
+ priority: 'high',
482
+ createdAt: '2026-01-01',
483
+ updatedAt: '2026-01-01'
484
+ },
485
+ {
486
+ id: 't_done',
487
+ projectId: 'p1',
488
+ title: 'Done Task',
489
+ status: 'done',
490
+ priority: 'low',
491
+ createdAt: '2026-01-01',
492
+ updatedAt: '2026-01-01'
493
+ }
494
+ ];
495
+
496
+ const mockClient = {
497
+ getTasks: vi.fn().mockResolvedValue(mockTasks),
498
+ updateTask: vi.fn().mockImplementation((id, updates) => Promise.resolve({ ...mockTasks.find((t) => t.id === id), ...updates }))
499
+ } as unknown as CriticalPathClient;
500
+
501
+ // Workflow grouping
502
+ const kanban = createKanbanState(mockClient, 'p1');
503
+ await kanban.fetch();
504
+
505
+ expect(kanban.columns.todo).toHaveLength(1);
506
+ expect(kanban.columns.done).toHaveLength(1);
507
+ expect(kanban.columns.in_progress).toHaveLength(0);
508
+
509
+ // Move task
510
+ const updated = await kanban.moveTask('t_todo', 'in_progress');
511
+ expect(updated.status).toBe('in_progress');
512
+
513
+ // Semantic grouping
514
+ const semanticKanban = createKanbanState(mockClient, 'p1', { groupBy: 'semantic' });
515
+ await semanticKanban.fetch();
516
+ expect(semanticKanban.columns.not_started).toHaveLength(1);
517
+ expect(semanticKanban.columns.completed).toHaveLength(1);
518
+ });
519
+ });
520
+
521
+ describe('TaskTransitionsState', () => {
522
+ it('fetches allowed transitions for a task', async () => {
523
+ const mockClient = {
524
+ getAllowedTaskTransitions: vi.fn().mockResolvedValue(['in_progress', 'canceled'])
525
+ } as unknown as CriticalPathClient;
526
+
527
+ const transitionsState = createTaskTransitionsState(mockClient, 'task_123');
528
+ expect(transitionsState.allowedTransitions).toEqual([]);
529
+
530
+ await transitionsState.fetch();
531
+ expect(mockClient.getAllowedTaskTransitions).toHaveBeenCalledWith('task_123');
532
+ expect(transitionsState.allowedTransitions).toEqual(['in_progress', 'canceled']);
533
+ });
534
+ });
535
+
536
+ describe('DeliverableSummaryState', () => {
537
+ it('fetches deliverable summary reactively', async () => {
538
+ const mockDeliverable: Deliverable = {
539
+ id: 'deliv_1',
540
+ projectId: 'proj_1',
541
+ title: 'MVP Launch',
542
+ status: 'planned',
543
+ createdAt: '2026-01-01',
544
+ updatedAt: '2026-01-01'
545
+ };
546
+
547
+ const mockSummary: DeliverableSummary = {
548
+ deliverable: mockDeliverable,
549
+ totalTasks: 4,
550
+ completedTasks: 3,
551
+ activeTasks: 1,
552
+ progressPercentage: 75,
553
+ estimatedHours: 40,
554
+ loggedHours: 30
555
+ };
556
+
557
+ const mockClient = {
558
+ getDeliverableSummary: vi.fn().mockResolvedValue(mockSummary)
559
+ } as unknown as CriticalPathClient;
560
+
561
+ const summaryState = createDeliverableSummaryState(mockClient, 'deliv_1');
562
+ expect(summaryState.summary).toBeNull();
563
+ expect(summaryState.data).toBeNull();
564
+
565
+ await summaryState.fetch();
566
+ expect(mockClient.getDeliverableSummary).toHaveBeenCalledWith('deliv_1');
567
+ expect(summaryState.summary).toEqual(mockSummary);
568
+ expect(summaryState.data).toEqual(mockSummary);
569
+ });
570
+ });
411
571
  });
412
572
 
573
+
574
+
package/src/index.ts CHANGED
@@ -11,4 +11,9 @@ export * from './comment-state.svelte.js';
11
11
  export * from './attachment-state.svelte.js';
12
12
  export * from './activity-state.svelte.js';
13
13
  export * from './deliverable-state.svelte.js';
14
+ export * from './deliverable-summary-state.svelte.js';
15
+ export * from './kanban-state.svelte.js';
16
+ export * from './task-transitions-state.svelte.js';
17
+ export * from './webmcp-state.svelte.js';
18
+
14
19
 
@@ -0,0 +1,112 @@
1
+ /// <reference types="svelte" />
2
+ import type { CriticalPathClient } from '@critical-path/client';
3
+ import type { Task, TaskStatus, StatusDefinition, SemanticStatus } from '@critical-path/core';
4
+ import { resolveStatusDefinition } from '@critical-path/core';
5
+ import { TaskState } from './task-state.svelte.js';
6
+
7
+ export interface KanbanStateOptions {
8
+ groupBy?: 'workflow' | 'semantic';
9
+ customDefinitions?: StatusDefinition[];
10
+ }
11
+
12
+ export class KanbanState {
13
+ private taskState: TaskState;
14
+
15
+ get data(): Task[] {
16
+ return this.taskState.data;
17
+ }
18
+
19
+ get tasks(): Task[] {
20
+ return this.taskState.data;
21
+ }
22
+
23
+ get loading(): boolean {
24
+ return this.taskState.loading;
25
+ }
26
+
27
+ get error(): Error | null {
28
+ return this.taskState.error;
29
+ }
30
+
31
+ columns = $derived.by<Record<string, Task[]>>(() => {
32
+ const tasks = this.taskState.data;
33
+ if (this.options?.groupBy === 'semantic') {
34
+ const semanticCols: Record<SemanticStatus, Task[]> = {
35
+ not_started: [],
36
+ in_progress: [],
37
+ completed: [],
38
+ canceled: []
39
+ };
40
+ for (const task of tasks) {
41
+ const category =
42
+ task.semanticStatus ||
43
+ resolveStatusDefinition(task.status, this.options?.customDefinitions).category;
44
+ if (semanticCols[category]) {
45
+ semanticCols[category].push(task);
46
+ } else {
47
+ semanticCols.not_started.push(task);
48
+ }
49
+ }
50
+ return semanticCols;
51
+ }
52
+
53
+ const workflowCols: Record<string, Task[]> = {};
54
+ if (this.options?.customDefinitions && this.options.customDefinitions.length > 0) {
55
+ for (const def of this.options.customDefinitions) {
56
+ workflowCols[def.key] = [];
57
+ }
58
+ } else {
59
+ workflowCols.backlog = [];
60
+ workflowCols.todo = [];
61
+ workflowCols.in_progress = [];
62
+ workflowCols.in_review = [];
63
+ workflowCols.done = [];
64
+ workflowCols.canceled = [];
65
+ }
66
+
67
+ for (const task of tasks) {
68
+ if (!workflowCols[task.status]) {
69
+ workflowCols[task.status] = [];
70
+ }
71
+ workflowCols[task.status].push(task);
72
+ }
73
+
74
+ return workflowCols;
75
+ });
76
+
77
+ constructor(
78
+ client: CriticalPathClient,
79
+ public projectId?: string,
80
+ public options: KanbanStateOptions = {}
81
+ ) {
82
+ this.taskState = new TaskState(client, projectId);
83
+ }
84
+
85
+ async fetch() {
86
+ return this.taskState.fetch();
87
+ }
88
+
89
+ async createTask(input: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>) {
90
+ return this.taskState.createTask(input);
91
+ }
92
+
93
+ async moveTask(taskId: string, targetStatus: TaskStatus) {
94
+ return this.taskState.updateTaskStatus(taskId, targetStatus);
95
+ }
96
+
97
+ async updateTaskStatus(taskId: string, status: TaskStatus) {
98
+ return this.taskState.updateTaskStatus(taskId, status);
99
+ }
100
+
101
+ async deleteTask(taskId: string) {
102
+ return this.taskState.deleteTask(taskId);
103
+ }
104
+ }
105
+
106
+ export function createKanbanState(
107
+ client: CriticalPathClient,
108
+ projectId?: string,
109
+ options?: KanbanStateOptions
110
+ ): KanbanState {
111
+ return new KanbanState(client, projectId, options);
112
+ }
@@ -0,0 +1,36 @@
1
+ /// <reference types="svelte" />
2
+ import type { CriticalPathClient } from '@critical-path/client';
3
+
4
+ export class TaskTransitionsState {
5
+ allowedTransitions = $state<string[]>([]);
6
+ loading = $state<boolean>(false);
7
+ error = $state<Error | null>(null);
8
+
9
+ constructor(private client: CriticalPathClient, public taskId?: string) {}
10
+
11
+ async fetch(taskId?: string) {
12
+ const targetTaskId = taskId || this.taskId;
13
+ if (!targetTaskId) {
14
+ this.allowedTransitions = [];
15
+ this.loading = false;
16
+ return;
17
+ }
18
+ this.taskId = targetTaskId;
19
+ this.loading = true;
20
+ this.error = null;
21
+ try {
22
+ this.allowedTransitions = await this.client.getAllowedTaskTransitions(targetTaskId);
23
+ } catch (err) {
24
+ this.error = err instanceof Error ? err : new Error(String(err));
25
+ } finally {
26
+ this.loading = false;
27
+ }
28
+ }
29
+ }
30
+
31
+ export function createTaskTransitionsState(
32
+ client: CriticalPathClient,
33
+ taskId?: string
34
+ ): TaskTransitionsState {
35
+ return new TaskTransitionsState(client, taskId);
36
+ }
@@ -0,0 +1,78 @@
1
+ import type { CriticalPathClient } from '@critical-path/client';
2
+ import { registerWebMcpTools, type WebMcpRegistryHandle } from '@critical-path/mcp/web';
3
+
4
+ export interface WebMcpStateOptions {
5
+ projectId?: string;
6
+ tools?: string[];
7
+ autoRegister?: boolean;
8
+ document?: any;
9
+ navigator?: any;
10
+ window?: any;
11
+ onToolExecuted?: (toolName: string, input: any, result: any) => void;
12
+ }
13
+
14
+ export class WebMcpState {
15
+ registered = $state<boolean>(false);
16
+ activeProjectId = $state<string | undefined>(undefined);
17
+ tools = $state<string[]>([]);
18
+ error = $state<Error | null>(null);
19
+
20
+ private handle: WebMcpRegistryHandle | null = null;
21
+ private abortController: AbortController | null = null;
22
+
23
+ constructor(private client: CriticalPathClient, private options: WebMcpStateOptions = {}) {
24
+ this.activeProjectId = options.projectId;
25
+ if (options.autoRegister ?? true) {
26
+ this.register();
27
+ }
28
+ }
29
+
30
+ register(): void {
31
+ if (this.registered) return;
32
+ this.error = null;
33
+
34
+ try {
35
+ this.abortController = new AbortController();
36
+ this.handle = registerWebMcpTools({
37
+ client: this.client,
38
+ projectId: this.activeProjectId,
39
+ tools: this.options.tools,
40
+ signal: this.abortController.signal,
41
+ document: this.options.document,
42
+ navigator: this.options.navigator,
43
+ window: this.options.window,
44
+ onToolExecuted: (name, input, result) => {
45
+ this.options.onToolExecuted?.(name, input, result);
46
+ }
47
+ });
48
+ this.tools = this.handle.getRegisteredTools().map((t) => t.name);
49
+ this.registered = true;
50
+ } catch (err) {
51
+ this.error = err instanceof Error ? err : new Error(String(err));
52
+ this.registered = false;
53
+ }
54
+ }
55
+
56
+ unregister(): void {
57
+ if (!this.registered) return;
58
+ this.abortController?.abort();
59
+ this.handle?.unregister();
60
+ this.handle = null;
61
+ this.abortController = null;
62
+ this.registered = false;
63
+ this.tools = [];
64
+ }
65
+
66
+ setProjectId(projectId?: string): void {
67
+ if (this.activeProjectId === projectId) return;
68
+ this.activeProjectId = projectId;
69
+ if (this.registered) {
70
+ this.unregister();
71
+ this.register();
72
+ }
73
+ }
74
+ }
75
+
76
+ export function createWebMcpState(client: CriticalPathClient, options?: WebMcpStateOptions): WebMcpState {
77
+ return new WebMcpState(client, options);
78
+ }