@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/LICENSE +21 -0
- package/README.md +122 -0
- package/dist/auth.d.ts +50 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +421 -0
- package/dist/auth.js.map +1 -0
- package/dist/client.d.ts +208 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +701 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/agents.d.ts +39 -0
- package/dist/tools/agents.d.ts.map +1 -0
- package/dist/tools/agents.js +141 -0
- package/dist/tools/agents.js.map +1 -0
- package/dist/tools/design.d.ts +175 -0
- package/dist/tools/design.d.ts.map +1 -0
- package/dist/tools/design.js +1172 -0
- package/dist/tools/design.js.map +1 -0
- package/dist/tools/index.d.ts +27 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +131 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/session.d.ts +77 -0
- package/dist/tools/session.d.ts.map +1 -0
- package/dist/tools/session.js +285 -0
- package/dist/tools/session.js.map +1 -0
- package/dist/tools/work.d.ts +99 -0
- package/dist/tools/work.d.ts.map +1 -0
- package/dist/tools/work.js +702 -0
- package/dist/tools/work.js.map +1 -0
- package/dist/types.d.ts +264 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/package.json +47 -0
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
|