@mag.ni/process 1.0.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.
package/dist/client.js ADDED
@@ -0,0 +1,701 @@
1
+ /**
2
+ * MCP Processes - API Client
3
+ * HTTP client for the Process Workboard API
4
+ */
5
+ import { OAuthManager } from './auth.js';
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import * as os from 'os';
9
+ // =============================================================================
10
+ // Persistent Session ID
11
+ // =============================================================================
12
+ /**
13
+ * Load or create a persistent server session ID.
14
+ * This ensures the same MCP server instance always uses the same session ID
15
+ * across restarts, preventing force-claim churn on the backend.
16
+ */
17
+ function getOrCreateSessionId() {
18
+ const configDir = path.join(os.homedir(), '.config', 'mcp-processes');
19
+ const sessionFile = path.join(configDir, 'session.json');
20
+ try {
21
+ const data = JSON.parse(fs.readFileSync(sessionFile, 'utf-8'));
22
+ if (data?.sessionId && typeof data.sessionId === 'string') {
23
+ return data.sessionId;
24
+ }
25
+ }
26
+ catch {
27
+ // File doesn't exist or is corrupted — generate a new one
28
+ }
29
+ const sessionId = crypto.randomUUID();
30
+ try {
31
+ fs.mkdirSync(configDir, { recursive: true });
32
+ fs.writeFileSync(sessionFile, JSON.stringify({ sessionId }, null, 2), 'utf-8');
33
+ }
34
+ catch {
35
+ // If we can't persist, the in-memory ID will still work for this session
36
+ }
37
+ return sessionId;
38
+ }
39
+ // =============================================================================
40
+ // Error Classes
41
+ // =============================================================================
42
+ export class ApiClientError extends Error {
43
+ statusCode;
44
+ apiError;
45
+ constructor(message, statusCode, apiError) {
46
+ super(message);
47
+ this.statusCode = statusCode;
48
+ this.apiError = apiError;
49
+ this.name = 'ApiClientError';
50
+ }
51
+ }
52
+ export class SessionNotClaimedError extends Error {
53
+ constructor() {
54
+ super('No user claimed. Call claimUser() first.');
55
+ this.name = 'SessionNotClaimedError';
56
+ }
57
+ }
58
+ // =============================================================================
59
+ // API Client
60
+ // =============================================================================
61
+ export class ProcessesApiClient {
62
+ baseUrl;
63
+ sessionId = null;
64
+ claimedUserId = null;
65
+ // OAuth2 authorization code + PKCE
66
+ oauthManager = null;
67
+ // BFF session cookies (set by /api/auth/session)
68
+ cookies = [];
69
+ sessionEstablished = false;
70
+ constructor(baseUrl, oauth) {
71
+ // Remove trailing slash if present
72
+ this.baseUrl = baseUrl.replace(/\/$/, '');
73
+ if (oauth) {
74
+ this.oauthManager = new OAuthManager(oauth);
75
+ }
76
+ }
77
+ // ===========================================================================
78
+ // Session State
79
+ // ===========================================================================
80
+ /**
81
+ * Get the current session ID (if claimed)
82
+ */
83
+ getSessionId() {
84
+ return this.sessionId;
85
+ }
86
+ /**
87
+ * Get the currently claimed user ID
88
+ */
89
+ getClaimedUserId() {
90
+ return this.claimedUserId;
91
+ }
92
+ /**
93
+ * Check if a user is currently claimed
94
+ */
95
+ hasClaimedUser() {
96
+ return this.sessionId !== null && this.claimedUserId !== null;
97
+ }
98
+ /**
99
+ * Set the session ID (for restoring sessions)
100
+ */
101
+ setSessionId(sessionId, userId) {
102
+ this.sessionId = sessionId;
103
+ this.claimedUserId = userId;
104
+ }
105
+ /**
106
+ * Clear the session state (used when lease expires)
107
+ */
108
+ clearSession() {
109
+ this.sessionId = null;
110
+ this.claimedUserId = null;
111
+ }
112
+ // ===========================================================================
113
+ // HTTP Helpers
114
+ // ===========================================================================
115
+ /**
116
+ * Establish a BFF session by exchanging the OAuth access token for a session cookie.
117
+ * This calls POST /api/auth/session with the Bearer token, and the backend validates
118
+ * it via the auth server's userinfo endpoint, then returns a session cookie.
119
+ */
120
+ async ensureBffSession() {
121
+ if (this.sessionEstablished && this.cookies.length > 0)
122
+ return;
123
+ if (!this.oauthManager)
124
+ return;
125
+ const token = await this.oauthManager.ensureAccessToken();
126
+ const response = await fetch(`${this.baseUrl}/api/auth/session`, {
127
+ method: 'POST',
128
+ headers: {
129
+ 'Authorization': `Bearer ${token}`,
130
+ 'User-Agent': 'MCP-Processes/1.0',
131
+ },
132
+ redirect: 'manual',
133
+ });
134
+ if (!response.ok) {
135
+ const text = await response.text().catch(() => '');
136
+ throw new Error(`BFF session creation failed (${response.status}): ${text}`);
137
+ }
138
+ // Capture set-cookie headers
139
+ const setCookies = response.headers.getSetCookie?.() ?? [];
140
+ if (setCookies.length > 0) {
141
+ this.cookies = setCookies.map(c => c.split(';')[0]);
142
+ }
143
+ this.sessionEstablished = true;
144
+ console.error('BFF session established');
145
+ }
146
+ async getHeaders(includeSession = false) {
147
+ // Ensure we have a BFF session cookie
148
+ await this.ensureBffSession();
149
+ const headers = {
150
+ 'Content-Type': 'application/json',
151
+ };
152
+ // Send session cookies instead of Bearer token
153
+ if (this.cookies.length > 0) {
154
+ headers['Cookie'] = this.cookies.join('; ');
155
+ }
156
+ if (includeSession && this.sessionId) {
157
+ headers['X-Session-Id'] = this.sessionId;
158
+ }
159
+ return headers;
160
+ }
161
+ async request(method, path, options = {}) {
162
+ const url = `${this.baseUrl}${path}`;
163
+ const { body, includeSession = false } = options;
164
+ const headers = await this.getHeaders(includeSession);
165
+ let response = await fetch(url, {
166
+ method,
167
+ headers,
168
+ body: body ? JSON.stringify(body) : undefined,
169
+ redirect: 'manual',
170
+ });
171
+ // If 401, session may have expired — re-establish and retry once
172
+ if (response.status === 401 && this.oauthManager) {
173
+ this.sessionEstablished = false;
174
+ this.cookies = [];
175
+ const retryHeaders = await this.getHeaders(includeSession);
176
+ response = await fetch(url, {
177
+ method,
178
+ headers: retryHeaders,
179
+ body: body ? JSON.stringify(body) : undefined,
180
+ redirect: 'manual',
181
+ });
182
+ }
183
+ // Capture any new cookies from the response (session sliding)
184
+ const setCookies = response.headers.getSetCookie?.() ?? [];
185
+ if (setCookies.length > 0) {
186
+ this.cookies = setCookies.map(c => c.split(';')[0]);
187
+ }
188
+ if (!response.ok) {
189
+ let apiError;
190
+ try {
191
+ const errorBody = await response.json();
192
+ apiError = errorBody;
193
+ }
194
+ catch {
195
+ // Response body is not JSON
196
+ }
197
+ throw new ApiClientError(apiError?.message || `HTTP ${response.status}: ${response.statusText}`, response.status, apiError);
198
+ }
199
+ // Handle 204 No Content
200
+ if (response.status === 204) {
201
+ return undefined;
202
+ }
203
+ return response.json();
204
+ }
205
+ requireSession() {
206
+ if (!this.sessionId) {
207
+ throw new SessionNotClaimedError();
208
+ }
209
+ }
210
+ // ===========================================================================
211
+ // Authentication
212
+ // ===========================================================================
213
+ /**
214
+ * Logout: destroy the server-side BFF session and clear local OAuth credentials.
215
+ * After this, the next API call will trigger a fresh interactive login.
216
+ */
217
+ async logout() {
218
+ // Destroy the server-side session
219
+ if (this.sessionEstablished && this.cookies.length > 0) {
220
+ try {
221
+ await fetch(`${this.baseUrl}/api/auth/session`, {
222
+ method: 'DELETE',
223
+ headers: {
224
+ 'Cookie': this.cookies.join('; '),
225
+ 'User-Agent': 'MCP-Processes/1.0',
226
+ },
227
+ redirect: 'manual',
228
+ });
229
+ }
230
+ catch {
231
+ // Best-effort — session will expire server-side anyway
232
+ }
233
+ }
234
+ // Clear local BFF session state
235
+ this.cookies = [];
236
+ this.sessionEstablished = false;
237
+ // Clear stored OAuth tokens so next call triggers fresh login
238
+ if (this.oauthManager) {
239
+ this.oauthManager.clearCredentials();
240
+ }
241
+ }
242
+ // ===========================================================================
243
+ // Agent User Management
244
+ // ===========================================================================
245
+ /**
246
+ * List all agent users with their claim status
247
+ */
248
+ async listAgentUsers() {
249
+ return this.request('GET', '/api/agents');
250
+ }
251
+ /**
252
+ * Create a new agent user
253
+ */
254
+ async createAgentUser(request) {
255
+ return this.request('POST', '/api/agents', {
256
+ body: request,
257
+ });
258
+ }
259
+ /**
260
+ * Delete an agent user (must be unclaimed)
261
+ */
262
+ async deleteAgentUser(userId) {
263
+ return this.request('DELETE', `/api/agents/${encodeURIComponent(userId)}`);
264
+ }
265
+ // ===========================================================================
266
+ // Session Management
267
+ // ===========================================================================
268
+ /**
269
+ * Claim a user identity to start working
270
+ * Returns a lease that must be kept alive with heartbeat()
271
+ */
272
+ async claimUser(userId) {
273
+ // Use a persistent session ID so the same server always reuses its identity
274
+ this.sessionId = getOrCreateSessionId();
275
+ const response = await this.request('POST', `/api/agents/${encodeURIComponent(userId)}/claim`, { includeSession: true });
276
+ this.claimedUserId = response.userId;
277
+ return response;
278
+ }
279
+ /**
280
+ * Send heartbeat to keep the lease alive
281
+ * Must be called periodically (recommended: every minute for 5-minute TTL)
282
+ */
283
+ async heartbeat() {
284
+ this.requireSession();
285
+ return this.request('POST', '/api/agents/heartbeat', {
286
+ includeSession: true,
287
+ });
288
+ }
289
+ /**
290
+ * Release the claimed user identity
291
+ * Should be called when done working
292
+ */
293
+ async releaseUser() {
294
+ this.requireSession();
295
+ const response = await this.request('POST', '/api/agents/release', {
296
+ includeSession: true,
297
+ });
298
+ // Clear session state
299
+ this.sessionId = null;
300
+ this.claimedUserId = null;
301
+ return response;
302
+ }
303
+ // ===========================================================================
304
+ // Instance Management
305
+ // ===========================================================================
306
+ /**
307
+ * Get instances assigned to the claimed user
308
+ * Returns WorkflowInstanceDto[] from the backend
309
+ */
310
+ async getMyInstances(status) {
311
+ this.requireSession();
312
+ let path = '/api/instances/my';
313
+ if (status) {
314
+ path += `?status=${encodeURIComponent(status)}`;
315
+ }
316
+ return this.request('GET', path, { includeSession: true });
317
+ }
318
+ /**
319
+ * Get full details for a specific instance
320
+ * Returns the raw WorkflowInstanceDto from the backend
321
+ */
322
+ async getInstanceDetails(instanceId) {
323
+ this.requireSession();
324
+ return this.request('GET', `/api/instances/${encodeURIComponent(instanceId)}`, { includeSession: true });
325
+ }
326
+ /**
327
+ * Get available actions for an instance at its current step
328
+ */
329
+ async getAvailableActions(instanceId) {
330
+ this.requireSession();
331
+ return this.request('GET', `/api/instances/${encodeURIComponent(instanceId)}/actions`, { includeSession: true });
332
+ }
333
+ /**
334
+ * Execute an action on an instance
335
+ */
336
+ async takeAction(instanceId, request) {
337
+ this.requireSession();
338
+ return this.request('POST', `/api/instances/${encodeURIComponent(instanceId)}/actions`, {
339
+ body: request,
340
+ includeSession: true,
341
+ });
342
+ }
343
+ // ===========================================================================
344
+ // Diagram Management
345
+ // ===========================================================================
346
+ /**
347
+ * Get a diagram by ID
348
+ * Maps the raw API response (arrows/from/to/shape) to the MCP type (edges/sourceNodeId/targetNodeId/type)
349
+ */
350
+ async getDiagram(diagramId) {
351
+ const raw = await this.request('GET', `/api/diagrams/${encodeURIComponent(diagramId)}`);
352
+ return this.mapRawDiagram(raw);
353
+ }
354
+ /**
355
+ * Map raw API diagram response to MCP Diagram type.
356
+ * The backend returns ArrowDto[] as "arrows" with "from"/"to" properties,
357
+ * while MCP types expect DiagramEdge[] as "edges" with "sourceNodeId"/"targetNodeId".
358
+ * Similarly, nodes use "shape"/"isStart"/"isEnd" instead of "type".
359
+ * Raw properties are preserved via spread for handlers that access them directly.
360
+ */
361
+ mapRawDiagram(raw) {
362
+ // Map nodes: API returns shape/isStart/isEnd, we derive NodeType
363
+ const rawNodes = raw.nodes || [];
364
+ const nodes = rawNodes.map((n) => {
365
+ let type = 'task';
366
+ if (n.isStart)
367
+ type = 'start';
368
+ else if (n.isEnd)
369
+ type = 'end';
370
+ else if (n.shape === 'diamond')
371
+ type = 'decision';
372
+ else if (n.childDiagramId)
373
+ type = 'subprocess';
374
+ return {
375
+ ...n,
376
+ id: n.id,
377
+ type,
378
+ label: n.label || '',
379
+ position: { x: n.x || 0, y: n.y || 0 },
380
+ data: n,
381
+ };
382
+ });
383
+ // Map arrows to edges: API uses from/to, MCP uses sourceNodeId/targetNodeId
384
+ const rawArrows = raw.arrows || [];
385
+ const edges = rawArrows.map((a) => ({
386
+ ...a,
387
+ id: a.id,
388
+ sourceNodeId: a.from,
389
+ targetNodeId: a.to,
390
+ label: a.label || null,
391
+ condition: null,
392
+ }));
393
+ return {
394
+ ...raw,
395
+ id: raw.id,
396
+ name: raw.title || '',
397
+ description: null,
398
+ nodes,
399
+ edges,
400
+ createdAt: raw.createdAt || '',
401
+ updatedAt: raw.updatedAt || '',
402
+ };
403
+ }
404
+ /**
405
+ * Get full context for a diagram including node details
406
+ */
407
+ async getDiagramContext(diagramId) {
408
+ const diagram = await this.getDiagram(diagramId);
409
+ const nodes = diagram.nodes ?? [];
410
+ const edges = diagram.edges ?? [];
411
+ // Build node context map
412
+ const nodeDetails = new Map();
413
+ for (const node of nodes) {
414
+ const incomingEdges = edges.filter((e) => e.targetNodeId === node.id);
415
+ const outgoingEdges = edges.filter((e) => e.sourceNodeId === node.id);
416
+ // Build available actions from outgoing edges
417
+ const availableActions = outgoingEdges.map((edge) => ({
418
+ id: edge.id,
419
+ name: edge.label || 'proceed',
420
+ description: edge.condition ? `Condition: ${edge.condition}` : null,
421
+ targetNodeId: edge.targetNodeId,
422
+ requiresData: false,
423
+ dataSchema: null,
424
+ }));
425
+ nodeDetails.set(node.id, {
426
+ node,
427
+ incomingEdges,
428
+ outgoingEdges,
429
+ availableActions,
430
+ });
431
+ }
432
+ return {
433
+ diagram,
434
+ nodeDetails,
435
+ };
436
+ }
437
+ // ===========================================================================
438
+ // Utility Methods
439
+ // ===========================================================================
440
+ /**
441
+ * Assign an instance to a user
442
+ */
443
+ async assignInstance(instanceId, userId) {
444
+ return this.request('POST', `/api/instances/${encodeURIComponent(instanceId)}/assign`, {
445
+ body: { userId },
446
+ includeSession: true,
447
+ });
448
+ }
449
+ /**
450
+ * Get a user by ID
451
+ */
452
+ async getUser(userId) {
453
+ return this.request('GET', `/api/users/${encodeURIComponent(userId)}`);
454
+ }
455
+ // ===========================================================================
456
+ // Instance Notes
457
+ // ===========================================================================
458
+ /**
459
+ * Get all notes for an instance
460
+ */
461
+ async getInstanceNotes(instanceId) {
462
+ this.requireSession();
463
+ return this.request('GET', `/api/instances/${encodeURIComponent(instanceId)}/notes`, { includeSession: true });
464
+ }
465
+ /**
466
+ * Add a note to an instance
467
+ */
468
+ async addInstanceNote(instanceId, text) {
469
+ this.requireSession();
470
+ return this.request('POST', `/api/instances/${encodeURIComponent(instanceId)}/notes`, {
471
+ body: {
472
+ userId: this.claimedUserId,
473
+ text,
474
+ },
475
+ includeSession: true,
476
+ });
477
+ }
478
+ // ===========================================================================
479
+ // Design Mode - Diagram Management
480
+ // ===========================================================================
481
+ /**
482
+ * List all diagrams with optional filtering
483
+ */
484
+ async listDiagrams(filter) {
485
+ const params = new URLSearchParams();
486
+ if (filter?.search)
487
+ params.append('search', filter.search);
488
+ if (filter?.diagramType)
489
+ params.append('diagramType', filter.diagramType);
490
+ if (filter?.sortBy)
491
+ params.append('sortBy', filter.sortBy);
492
+ if (filter?.sortOrder)
493
+ params.append('sortOrder', filter.sortOrder);
494
+ if (filter?.topLevelOnly)
495
+ params.append('topLevelOnly', 'true');
496
+ const queryString = params.toString();
497
+ const path = `/api/diagrams${queryString ? `?${queryString}` : ''}`;
498
+ return this.request('GET', path);
499
+ }
500
+ /**
501
+ * Get a diagram by ID (raw API response without mapping)
502
+ */
503
+ async getDiagramRaw(diagramId) {
504
+ return this.request('GET', `/api/diagrams/${encodeURIComponent(diagramId)}`);
505
+ }
506
+ /**
507
+ * Create a new diagram
508
+ */
509
+ async createDiagram(data) {
510
+ return this.request('POST', '/api/diagrams', {
511
+ body: {
512
+ title: data.title,
513
+ token: data.token,
514
+ diagramType: data.diagramType || 'process',
515
+ mode: data.mode || 'reactive',
516
+ },
517
+ });
518
+ }
519
+ /**
520
+ * Update a diagram's properties
521
+ */
522
+ async updateDiagram(diagramId, data) {
523
+ return this.request('PUT', `/api/diagrams/${encodeURIComponent(diagramId)}`, {
524
+ body: data,
525
+ });
526
+ }
527
+ /**
528
+ * Delete a diagram
529
+ */
530
+ async deleteDiagram(diagramId, confirm = false) {
531
+ const path = `/api/diagrams/${encodeURIComponent(diagramId)}${confirm ? '?confirm=true' : ''}`;
532
+ try {
533
+ await this.request('DELETE', path);
534
+ return { success: true };
535
+ }
536
+ catch (error) {
537
+ if (error instanceof ApiClientError && error.statusCode === 409) {
538
+ const apiError = error.apiError;
539
+ if (apiError?.requiresConfirmation) {
540
+ return {
541
+ requiresConfirmation: true,
542
+ message: apiError.message,
543
+ count: apiError.count,
544
+ };
545
+ }
546
+ }
547
+ throw error;
548
+ }
549
+ }
550
+ /**
551
+ * Duplicate a diagram
552
+ */
553
+ async duplicateDiagram(diagramId) {
554
+ return this.request('POST', `/api/diagrams/${encodeURIComponent(diagramId)}/duplicate`);
555
+ }
556
+ // ===========================================================================
557
+ // Design Mode - Node Management
558
+ // ===========================================================================
559
+ /**
560
+ * Add a node to a diagram
561
+ * Fetches the current diagram, adds the node, and saves
562
+ */
563
+ async addNode(diagramId, nodeData) {
564
+ // Get current diagram
565
+ const diagram = await this.getDiagramRaw(diagramId);
566
+ const nodes = diagram.nodes || [];
567
+ const arrows = diagram.arrows || [];
568
+ // Add the new node
569
+ nodes.push(nodeData);
570
+ // Update diagram with new nodes array
571
+ await this.updateDiagram(diagramId, { nodes, arrows });
572
+ return nodeData;
573
+ }
574
+ /**
575
+ * Update a node in a diagram
576
+ */
577
+ async updateNode(diagramId, nodeId, updateData) {
578
+ // Get current diagram
579
+ const diagram = await this.getDiagramRaw(diagramId);
580
+ const nodes = diagram.nodes || [];
581
+ const arrows = diagram.arrows || [];
582
+ // Find and update the node
583
+ const nodeIndex = nodes.findIndex((n) => n.id === nodeId);
584
+ if (nodeIndex === -1) {
585
+ throw new ApiClientError(`Node '${nodeId}' not found in diagram`, 404);
586
+ }
587
+ // Merge update data with existing node
588
+ nodes[nodeIndex] = { ...nodes[nodeIndex], ...updateData };
589
+ // Update diagram
590
+ await this.updateDiagram(diagramId, { nodes, arrows });
591
+ return nodes[nodeIndex];
592
+ }
593
+ /**
594
+ * Delete a node from a diagram
595
+ * Also removes any arrows connected to this node
596
+ */
597
+ async deleteNode(diagramId, nodeId) {
598
+ // Get current diagram
599
+ const diagram = await this.getDiagramRaw(diagramId);
600
+ const nodes = diagram.nodes || [];
601
+ const arrows = diagram.arrows || [];
602
+ // Find node
603
+ const nodeIndex = nodes.findIndex((n) => n.id === nodeId);
604
+ if (nodeIndex === -1) {
605
+ throw new ApiClientError(`Node '${nodeId}' not found in diagram`, 404);
606
+ }
607
+ // Remove node
608
+ nodes.splice(nodeIndex, 1);
609
+ // Remove arrows connected to this node
610
+ const originalArrowCount = arrows.length;
611
+ const filteredArrows = arrows.filter((a) => a.from !== nodeId && a.to !== nodeId);
612
+ const removedArrowCount = originalArrowCount - filteredArrows.length;
613
+ // Update diagram
614
+ await this.updateDiagram(diagramId, { nodes, arrows: filteredArrows });
615
+ return { removedArrowCount };
616
+ }
617
+ // ===========================================================================
618
+ // Design Mode - Arrow Management
619
+ // ===========================================================================
620
+ /**
621
+ * Add an arrow to a diagram
622
+ */
623
+ async addArrow(diagramId, arrowData) {
624
+ // Get current diagram
625
+ const diagram = await this.getDiagramRaw(diagramId);
626
+ const nodes = diagram.nodes || [];
627
+ const arrows = diagram.arrows || [];
628
+ // Validate that source and target nodes exist
629
+ const fromExists = nodes.some((n) => n.id === arrowData.from);
630
+ const toExists = nodes.some((n) => n.id === arrowData.to);
631
+ if (!fromExists) {
632
+ throw new ApiClientError(`Source node '${arrowData.from}' not found in diagram`, 404);
633
+ }
634
+ if (!toExists) {
635
+ throw new ApiClientError(`Target node '${arrowData.to}' not found in diagram`, 404);
636
+ }
637
+ // Add the new arrow
638
+ arrows.push(arrowData);
639
+ // Update diagram
640
+ await this.updateDiagram(diagramId, { nodes, arrows });
641
+ return arrowData;
642
+ }
643
+ /**
644
+ * Update an arrow in a diagram
645
+ */
646
+ async updateArrow(diagramId, arrowId, updateData) {
647
+ // Get current diagram
648
+ const diagram = await this.getDiagramRaw(diagramId);
649
+ const nodes = diagram.nodes || [];
650
+ const arrows = diagram.arrows || [];
651
+ // Find and update the arrow
652
+ const arrowIndex = arrows.findIndex((a) => a.id === arrowId);
653
+ if (arrowIndex === -1) {
654
+ throw new ApiClientError(`Arrow '${arrowId}' not found in diagram`, 404);
655
+ }
656
+ // Merge update data with existing arrow
657
+ arrows[arrowIndex] = { ...arrows[arrowIndex], ...updateData };
658
+ // Update diagram
659
+ await this.updateDiagram(diagramId, { nodes, arrows });
660
+ return arrows[arrowIndex];
661
+ }
662
+ /**
663
+ * Delete an arrow from a diagram
664
+ */
665
+ async deleteArrow(diagramId, arrowId) {
666
+ // Get current diagram
667
+ const diagram = await this.getDiagramRaw(diagramId);
668
+ const nodes = diagram.nodes || [];
669
+ const arrows = diagram.arrows || [];
670
+ // Find arrow
671
+ const arrowIndex = arrows.findIndex((a) => a.id === arrowId);
672
+ if (arrowIndex === -1) {
673
+ throw new ApiClientError(`Arrow '${arrowId}' not found in diagram`, 404);
674
+ }
675
+ // Remove arrow
676
+ arrows.splice(arrowIndex, 1);
677
+ // Update diagram
678
+ await this.updateDiagram(diagramId, { nodes, arrows });
679
+ }
680
+ }
681
+ // =============================================================================
682
+ // Factory Function
683
+ // =============================================================================
684
+ /**
685
+ * Create a new API client instance from environment variables
686
+ */
687
+ export function createClient() {
688
+ const baseUrl = (process.env.PROCESSES_API_URL || 'https://process.mag.ni').replace(/\/$/, '');
689
+ const authUrl = (process.env.PROCESSES_AUTH_URL || 'https://auth.mag.ni').replace(/\/$/, '');
690
+ const oauthClientId = process.env.PROCESSES_OAUTH_CLIENT_ID || 'process-mcp';
691
+ const oauthClientSecret = process.env.PROCESSES_OAUTH_CLIENT_SECRET;
692
+ const oauth = {
693
+ authUrl,
694
+ clientId: oauthClientId,
695
+ clientSecret: oauthClientSecret,
696
+ callbackPort: parseInt(process.env.PROCESSES_OAUTH_CALLBACK_PORT || '19823', 10),
697
+ scopes: ['openid', 'profile', 'email', 'offline_access', 'user.clientId_currently_logged'],
698
+ };
699
+ return new ProcessesApiClient(baseUrl, oauth);
700
+ }
701
+ //# sourceMappingURL=client.js.map