@getmarrow/mcp 3.0.7 → 3.0.8
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/cli.js +226 -254
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,9 @@ function parseArgs() {
|
|
|
25
25
|
}
|
|
26
26
|
const cliArgs = parseArgs();
|
|
27
27
|
const API_KEY = cliArgs.apiKey || process.env.MARROW_API_KEY || '';
|
|
28
|
-
|
|
28
|
+
// [SECURITY #3] Validate BASE_URL — require HTTPS to prevent SSRF / credential leakage
|
|
29
|
+
const rawBaseUrl = process.env.MARROW_BASE_URL || 'https://api.getmarrow.ai';
|
|
30
|
+
const BASE_URL = (0, index_1.validateBaseUrl)(rawBaseUrl);
|
|
29
31
|
const SESSION_ID = process.env.MARROW_SESSION_ID || undefined;
|
|
30
32
|
const AUTO_ENROLL = process.env.MARROW_AUTO_ENROLL === 'true';
|
|
31
33
|
const AGENT_ID = process.env.MARROW_AGENT_ID || `${require('os').hostname()}-${Date.now().toString(36)}`;
|
|
@@ -35,16 +37,19 @@ if (!API_KEY) {
|
|
|
35
37
|
process.stderr.write(' or: npx @getmarrow/mcp --key mrw_yourkey\n');
|
|
36
38
|
process.exit(1);
|
|
37
39
|
}
|
|
40
|
+
// [SECURITY #12] Warn if API key is visible in process args
|
|
41
|
+
if (cliArgs.apiKey) {
|
|
42
|
+
process.stderr.write('[marrow] Warning: --key flag exposes API key in process list. Use MARROW_API_KEY env var for production.\n');
|
|
43
|
+
}
|
|
38
44
|
// Auto-orient on startup — cache warnings, inject into EVERY marrow_think response
|
|
39
45
|
let cachedOrientWarnings = [];
|
|
40
46
|
let thinkCallCount = 0;
|
|
41
|
-
let orientCallCount = 0;
|
|
42
|
-
let initialized = false;
|
|
47
|
+
let orientCallCount = 0;
|
|
48
|
+
let initialized = false;
|
|
43
49
|
const pendingDecisions = new Map();
|
|
44
50
|
const PENDING_TTL_MS = 30 * 60 * 1000; // 30 min TTL
|
|
45
51
|
function actionHash(action) {
|
|
46
52
|
const normalized = action.toLowerCase().trim().replace(/\s+/g, ' ');
|
|
47
|
-
// djb2 hash to prevent decision_id mismatches from normalization-only collisions
|
|
48
53
|
let h = 5381;
|
|
49
54
|
for (let i = 0; i < normalized.length; i++) {
|
|
50
55
|
h = ((h << 5) + h) ^ normalized.charCodeAt(i);
|
|
@@ -52,6 +57,7 @@ function actionHash(action) {
|
|
|
52
57
|
}
|
|
53
58
|
return h.toString(36) + '_' + normalized.slice(0, 32);
|
|
54
59
|
}
|
|
60
|
+
// [FIX #11] Actually call cleanupPending to prevent unbounded map growth
|
|
55
61
|
function cleanupPending() {
|
|
56
62
|
const now = Date.now();
|
|
57
63
|
for (const [key, val] of pendingDecisions) {
|
|
@@ -64,13 +70,15 @@ function formatWarningActionably(w) {
|
|
|
64
70
|
const pct = Math.round(w.failureRate * 100);
|
|
65
71
|
return `⚠️ ${w.type} has ${pct}% failure rate — check what went wrong last time before proceeding`;
|
|
66
72
|
}
|
|
73
|
+
// [FIX #4] Log orient refresh failures instead of silently ignoring
|
|
67
74
|
async function refreshOrientWarnings() {
|
|
68
75
|
try {
|
|
69
76
|
const r = await (0, index_1.marrowOrient)(API_KEY, BASE_URL, undefined, SESSION_ID);
|
|
70
77
|
cachedOrientWarnings = r.warnings;
|
|
71
78
|
}
|
|
72
|
-
catch {
|
|
73
|
-
|
|
79
|
+
catch (err) {
|
|
80
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
81
|
+
process.stderr.write(`[marrow] Warning: failed to refresh orient warnings: ${msg}\n`);
|
|
74
82
|
}
|
|
75
83
|
}
|
|
76
84
|
// Initial orient
|
|
@@ -82,29 +90,31 @@ refreshOrientWarnings().then(() => {
|
|
|
82
90
|
// Auto-commit tracking for session close
|
|
83
91
|
let lastDecisionId = null;
|
|
84
92
|
let lastCommitted = false;
|
|
93
|
+
// [FIX #5] Log auto-commit failures instead of silently ignoring; remove broken AbortController
|
|
85
94
|
async function autoCommitOnClose() {
|
|
86
95
|
if (lastDecisionId && !lastCommitted) {
|
|
87
96
|
try {
|
|
88
|
-
const controller = new AbortController();
|
|
89
|
-
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
90
97
|
await (0, index_1.marrowCommit)(API_KEY, BASE_URL, {
|
|
91
98
|
decision_id: lastDecisionId,
|
|
92
99
|
success: false,
|
|
93
100
|
outcome: 'Session ended without explicit commit',
|
|
94
101
|
}, SESSION_ID);
|
|
95
|
-
clearTimeout(timeout);
|
|
96
102
|
}
|
|
97
|
-
catch {
|
|
98
|
-
|
|
103
|
+
catch (err) {
|
|
104
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
105
|
+
process.stderr.write(`[marrow] Warning: auto-commit on close failed: ${msg}\n`);
|
|
99
106
|
}
|
|
100
107
|
}
|
|
101
108
|
}
|
|
102
|
-
|
|
109
|
+
// [FIX #10] Handle both SIGTERM and SIGINT for clean shutdown
|
|
110
|
+
async function gracefulShutdown() {
|
|
103
111
|
const forceExit = setTimeout(() => process.exit(0), 5000);
|
|
104
112
|
forceExit.unref();
|
|
105
113
|
await autoCommitOnClose();
|
|
106
114
|
process.exit(0);
|
|
107
|
-
}
|
|
115
|
+
}
|
|
116
|
+
process.on('SIGTERM', gracefulShutdown);
|
|
117
|
+
process.on('SIGINT', gracefulShutdown);
|
|
108
118
|
function send(response) {
|
|
109
119
|
process.stdout.write(JSON.stringify(response) + '\n');
|
|
110
120
|
}
|
|
@@ -114,7 +124,31 @@ function success(id, result) {
|
|
|
114
124
|
function error(id, code, message) {
|
|
115
125
|
send({ jsonrpc: '2.0', id, error: { code, message } });
|
|
116
126
|
}
|
|
117
|
-
//
|
|
127
|
+
// [FIX #9] Runtime validation helper for required string params
|
|
128
|
+
function requireString(args, name) {
|
|
129
|
+
const val = args[name];
|
|
130
|
+
if (typeof val !== 'string' || !val.trim()) {
|
|
131
|
+
throw new Error(`"${name}" is required and must be a non-empty string`);
|
|
132
|
+
}
|
|
133
|
+
return val;
|
|
134
|
+
}
|
|
135
|
+
// [FIX #6 & #7] Safe JSON response helper for memory API functions
|
|
136
|
+
async function safeMemoryResponse(res) {
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
let detail = '';
|
|
139
|
+
try {
|
|
140
|
+
detail = await res.text();
|
|
141
|
+
}
|
|
142
|
+
catch { /* ignore */ }
|
|
143
|
+
throw new Error(`API error ${res.status}: ${detail.slice(0, 200)}`);
|
|
144
|
+
}
|
|
145
|
+
const json = await res.json();
|
|
146
|
+
if (json.error) {
|
|
147
|
+
throw new Error(json.error);
|
|
148
|
+
}
|
|
149
|
+
return json;
|
|
150
|
+
}
|
|
151
|
+
// Memory API functions — all patched with safeMemoryResponse and validatePathParam
|
|
118
152
|
async function marrowListMemories(apiKey, baseUrl, params, sessionId) {
|
|
119
153
|
const qs = new URLSearchParams();
|
|
120
154
|
if (params?.status)
|
|
@@ -131,21 +165,23 @@ async function marrowListMemories(apiKey, baseUrl, params, sessionId) {
|
|
|
131
165
|
...(sessionId ? { 'X-Marrow-Session-Id': sessionId } : {}),
|
|
132
166
|
},
|
|
133
167
|
});
|
|
134
|
-
const json = await res
|
|
168
|
+
const json = await safeMemoryResponse(res);
|
|
135
169
|
return json.data?.memories || [];
|
|
136
170
|
}
|
|
137
171
|
async function marrowGetMemory(apiKey, baseUrl, id, sessionId) {
|
|
138
|
-
const
|
|
172
|
+
const safeId = (0, index_1.validatePathParam)(id, 'id');
|
|
173
|
+
const res = await fetch(`${baseUrl}/v1/memories/${safeId}`, {
|
|
139
174
|
headers: {
|
|
140
175
|
Authorization: `Bearer ${apiKey}`,
|
|
141
176
|
...(sessionId ? { 'X-Marrow-Session-Id': sessionId } : {}),
|
|
142
177
|
},
|
|
143
178
|
});
|
|
144
|
-
const json = await res
|
|
179
|
+
const json = await safeMemoryResponse(res);
|
|
145
180
|
return json.data?.memory || null;
|
|
146
181
|
}
|
|
147
182
|
async function marrowUpdateMemory(apiKey, baseUrl, id, patch, sessionId) {
|
|
148
|
-
const
|
|
183
|
+
const safeId = (0, index_1.validatePathParam)(id, 'id');
|
|
184
|
+
const res = await fetch(`${baseUrl}/v1/memories/${safeId}`, {
|
|
149
185
|
method: 'PATCH',
|
|
150
186
|
headers: {
|
|
151
187
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -154,11 +190,12 @@ async function marrowUpdateMemory(apiKey, baseUrl, id, patch, sessionId) {
|
|
|
154
190
|
},
|
|
155
191
|
body: JSON.stringify(patch),
|
|
156
192
|
});
|
|
157
|
-
const json = await res
|
|
158
|
-
return json.data
|
|
193
|
+
const json = await safeMemoryResponse(res);
|
|
194
|
+
return json.data?.memory;
|
|
159
195
|
}
|
|
160
196
|
async function marrowDeleteMemory(apiKey, baseUrl, id, meta, sessionId) {
|
|
161
|
-
const
|
|
197
|
+
const safeId = (0, index_1.validatePathParam)(id, 'id');
|
|
198
|
+
const res = await fetch(`${baseUrl}/v1/memories/${safeId}`, {
|
|
162
199
|
method: 'DELETE',
|
|
163
200
|
headers: {
|
|
164
201
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -167,11 +204,12 @@ async function marrowDeleteMemory(apiKey, baseUrl, id, meta, sessionId) {
|
|
|
167
204
|
},
|
|
168
205
|
body: JSON.stringify(meta || {}),
|
|
169
206
|
});
|
|
170
|
-
const json = await res
|
|
171
|
-
return json.data
|
|
207
|
+
const json = await safeMemoryResponse(res);
|
|
208
|
+
return json.data?.memory;
|
|
172
209
|
}
|
|
173
210
|
async function marrowMarkOutdated(apiKey, baseUrl, id, meta, sessionId) {
|
|
174
|
-
const
|
|
211
|
+
const safeId = (0, index_1.validatePathParam)(id, 'id');
|
|
212
|
+
const res = await fetch(`${baseUrl}/v1/memories/${safeId}/outdated`, {
|
|
175
213
|
method: 'POST',
|
|
176
214
|
headers: {
|
|
177
215
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -180,11 +218,12 @@ async function marrowMarkOutdated(apiKey, baseUrl, id, meta, sessionId) {
|
|
|
180
218
|
},
|
|
181
219
|
body: JSON.stringify(meta || {}),
|
|
182
220
|
});
|
|
183
|
-
const json = await res
|
|
184
|
-
return json.data
|
|
221
|
+
const json = await safeMemoryResponse(res);
|
|
222
|
+
return json.data?.memory;
|
|
185
223
|
}
|
|
186
224
|
async function marrowSupersedeMemory(apiKey, baseUrl, id, replacement, sessionId) {
|
|
187
|
-
const
|
|
225
|
+
const safeId = (0, index_1.validatePathParam)(id, 'id');
|
|
226
|
+
const res = await fetch(`${baseUrl}/v1/memories/${safeId}/supersede`, {
|
|
188
227
|
method: 'POST',
|
|
189
228
|
headers: {
|
|
190
229
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -193,11 +232,12 @@ async function marrowSupersedeMemory(apiKey, baseUrl, id, replacement, sessionId
|
|
|
193
232
|
},
|
|
194
233
|
body: JSON.stringify(replacement),
|
|
195
234
|
});
|
|
196
|
-
const json = await res
|
|
235
|
+
const json = await safeMemoryResponse(res);
|
|
197
236
|
return json.data;
|
|
198
237
|
}
|
|
199
238
|
async function marrowShareMemory(apiKey, baseUrl, id, agentIds, actor, sessionId) {
|
|
200
|
-
const
|
|
239
|
+
const safeId = (0, index_1.validatePathParam)(id, 'id');
|
|
240
|
+
const res = await fetch(`${baseUrl}/v1/memories/${safeId}/share`, {
|
|
201
241
|
method: 'POST',
|
|
202
242
|
headers: {
|
|
203
243
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -206,8 +246,8 @@ async function marrowShareMemory(apiKey, baseUrl, id, agentIds, actor, sessionId
|
|
|
206
246
|
},
|
|
207
247
|
body: JSON.stringify({ agent_ids: agentIds, actor }),
|
|
208
248
|
});
|
|
209
|
-
const json = await res
|
|
210
|
-
return json.data
|
|
249
|
+
const json = await safeMemoryResponse(res);
|
|
250
|
+
return json.data?.memory;
|
|
211
251
|
}
|
|
212
252
|
async function marrowExportMemories(apiKey, baseUrl, params, sessionId) {
|
|
213
253
|
const qs = new URLSearchParams();
|
|
@@ -223,7 +263,7 @@ async function marrowExportMemories(apiKey, baseUrl, params, sessionId) {
|
|
|
223
263
|
...(sessionId ? { 'X-Marrow-Session-Id': sessionId } : {}),
|
|
224
264
|
},
|
|
225
265
|
});
|
|
226
|
-
const json = await res
|
|
266
|
+
const json = await safeMemoryResponse(res);
|
|
227
267
|
return json.data;
|
|
228
268
|
}
|
|
229
269
|
async function marrowImportMemories(apiKey, baseUrl, memories, mode, sessionId) {
|
|
@@ -236,7 +276,7 @@ async function marrowImportMemories(apiKey, baseUrl, memories, mode, sessionId)
|
|
|
236
276
|
},
|
|
237
277
|
body: JSON.stringify({ memories, mode }),
|
|
238
278
|
});
|
|
239
|
-
const json = await res
|
|
279
|
+
const json = await safeMemoryResponse(res);
|
|
240
280
|
return json.data;
|
|
241
281
|
}
|
|
242
282
|
async function marrowRetrieveMemories(apiKey, baseUrl, query, params, sessionId) {
|
|
@@ -262,10 +302,10 @@ async function marrowRetrieveMemories(apiKey, baseUrl, query, params, sessionId)
|
|
|
262
302
|
...(sessionId ? { 'X-Marrow-Session-Id': sessionId } : {}),
|
|
263
303
|
},
|
|
264
304
|
});
|
|
265
|
-
const json = await res
|
|
305
|
+
const json = await safeMemoryResponse(res);
|
|
266
306
|
return json.data;
|
|
267
307
|
}
|
|
268
|
-
// Tool definitions
|
|
308
|
+
// Tool definitions (unchanged)
|
|
269
309
|
const TOOLS = [
|
|
270
310
|
{
|
|
271
311
|
name: 'marrow_orient',
|
|
@@ -298,35 +338,17 @@ const TOOLS = [
|
|
|
298
338
|
inputSchema: {
|
|
299
339
|
type: 'object',
|
|
300
340
|
properties: {
|
|
301
|
-
action: {
|
|
302
|
-
type: 'string',
|
|
303
|
-
description: 'What the agent is about to do',
|
|
304
|
-
},
|
|
341
|
+
action: { type: 'string', description: 'What the agent is about to do' },
|
|
305
342
|
type: {
|
|
306
343
|
type: 'string',
|
|
307
344
|
enum: ['implementation', 'security', 'architecture', 'process', 'general'],
|
|
308
345
|
description: 'Type of action (default: general)',
|
|
309
346
|
},
|
|
310
|
-
context: {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
},
|
|
314
|
-
|
|
315
|
-
type: 'string',
|
|
316
|
-
description: 'decision_id from previous think() call — auto-commits that session',
|
|
317
|
-
},
|
|
318
|
-
previous_success: {
|
|
319
|
-
type: 'boolean',
|
|
320
|
-
description: 'Did the previous action succeed?',
|
|
321
|
-
},
|
|
322
|
-
previous_outcome: {
|
|
323
|
-
type: 'string',
|
|
324
|
-
description: 'What happened in the previous action (required if previous_decision_id provided)',
|
|
325
|
-
},
|
|
326
|
-
checkLoop: {
|
|
327
|
-
type: 'boolean',
|
|
328
|
-
description: 'Enable loop detection: warns if you are about to retry a failed approach. Recommended: true.',
|
|
329
|
-
},
|
|
347
|
+
context: { type: 'object', description: 'Optional metadata about the current situation' },
|
|
348
|
+
previous_decision_id: { type: 'string', description: 'decision_id from previous think() call — auto-commits that session' },
|
|
349
|
+
previous_success: { type: 'boolean', description: 'Did the previous action succeed?' },
|
|
350
|
+
previous_outcome: { type: 'string', description: 'What happened in the previous action (required if previous_decision_id provided)' },
|
|
351
|
+
checkLoop: { type: 'boolean', description: 'Enable loop detection: warns if you are about to retry a failed approach. Recommended: true.' },
|
|
330
352
|
},
|
|
331
353
|
required: ['action'],
|
|
332
354
|
},
|
|
@@ -339,22 +361,10 @@ const TOOLS = [
|
|
|
339
361
|
inputSchema: {
|
|
340
362
|
type: 'object',
|
|
341
363
|
properties: {
|
|
342
|
-
decision_id: {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
},
|
|
346
|
-
success: {
|
|
347
|
-
type: 'boolean',
|
|
348
|
-
description: 'Did the action succeed?',
|
|
349
|
-
},
|
|
350
|
-
outcome: {
|
|
351
|
-
type: 'string',
|
|
352
|
-
description: 'What happened — be specific, this trains the hive',
|
|
353
|
-
},
|
|
354
|
-
caused_by: {
|
|
355
|
-
type: 'string',
|
|
356
|
-
description: 'Optional: what caused this action',
|
|
357
|
-
},
|
|
364
|
+
decision_id: { type: 'string', description: 'decision_id from the marrow_think call' },
|
|
365
|
+
success: { type: 'boolean', description: 'Did the action succeed?' },
|
|
366
|
+
outcome: { type: 'string', description: 'What happened — be specific, this trains the hive' },
|
|
367
|
+
caused_by: { type: 'string', description: 'Optional: what caused this action' },
|
|
358
368
|
},
|
|
359
369
|
required: ['decision_id', 'success', 'outcome'],
|
|
360
370
|
},
|
|
@@ -366,18 +376,9 @@ const TOOLS = [
|
|
|
366
376
|
inputSchema: {
|
|
367
377
|
type: 'object',
|
|
368
378
|
properties: {
|
|
369
|
-
description: {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
},
|
|
373
|
-
success: {
|
|
374
|
-
type: 'boolean',
|
|
375
|
-
description: 'Whether it succeeded',
|
|
376
|
-
},
|
|
377
|
-
outcome: {
|
|
378
|
-
type: 'string',
|
|
379
|
-
description: 'One-line summary of what happened',
|
|
380
|
-
},
|
|
379
|
+
description: { type: 'string', description: 'What the agent did' },
|
|
380
|
+
success: { type: 'boolean', description: 'Whether it succeeded' },
|
|
381
|
+
outcome: { type: 'string', description: 'One-line summary of what happened' },
|
|
381
382
|
type: {
|
|
382
383
|
type: 'string',
|
|
383
384
|
enum: ['implementation', 'security', 'architecture', 'process', 'general'],
|
|
@@ -396,18 +397,9 @@ const TOOLS = [
|
|
|
396
397
|
inputSchema: {
|
|
397
398
|
type: 'object',
|
|
398
399
|
properties: {
|
|
399
|
-
action: {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
},
|
|
403
|
-
outcome: {
|
|
404
|
-
type: 'string',
|
|
405
|
-
description: 'What happened (if already done). Omit to log intent only.',
|
|
406
|
-
},
|
|
407
|
-
success: {
|
|
408
|
-
type: 'boolean',
|
|
409
|
-
description: 'Did it succeed (default: true)',
|
|
410
|
-
},
|
|
400
|
+
action: { type: 'string', description: 'What you are about to do or just did' },
|
|
401
|
+
outcome: { type: 'string', description: 'What happened (if already done). Omit to log intent only.' },
|
|
402
|
+
success: { type: 'boolean', description: 'Did it succeed (default: true)' },
|
|
411
403
|
type: {
|
|
412
404
|
type: 'string',
|
|
413
405
|
enum: ['implementation', 'security', 'architecture', 'process', 'general'],
|
|
@@ -425,10 +417,7 @@ const TOOLS = [
|
|
|
425
417
|
inputSchema: {
|
|
426
418
|
type: 'object',
|
|
427
419
|
properties: {
|
|
428
|
-
query: {
|
|
429
|
-
type: 'string',
|
|
430
|
-
description: 'Plain English question about your decision history',
|
|
431
|
-
},
|
|
420
|
+
query: { type: 'string', description: 'Plain English question about your decision history' },
|
|
432
421
|
},
|
|
433
422
|
required: ['query'],
|
|
434
423
|
},
|
|
@@ -436,11 +425,7 @@ const TOOLS = [
|
|
|
436
425
|
{
|
|
437
426
|
name: 'marrow_status',
|
|
438
427
|
description: 'Check Marrow platform health and status.',
|
|
439
|
-
inputSchema: {
|
|
440
|
-
type: 'object',
|
|
441
|
-
properties: {},
|
|
442
|
-
required: [],
|
|
443
|
-
},
|
|
428
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
444
429
|
},
|
|
445
430
|
{
|
|
446
431
|
name: 'marrow_list_memories',
|
|
@@ -459,13 +444,7 @@ const TOOLS = [
|
|
|
459
444
|
{
|
|
460
445
|
name: 'marrow_get_memory',
|
|
461
446
|
description: 'Get a single memory by ID.',
|
|
462
|
-
inputSchema: {
|
|
463
|
-
type: 'object',
|
|
464
|
-
properties: {
|
|
465
|
-
id: { type: 'string', description: 'Memory ID' },
|
|
466
|
-
},
|
|
467
|
-
required: ['id'],
|
|
468
|
-
},
|
|
447
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Memory ID' } }, required: ['id'] },
|
|
469
448
|
},
|
|
470
449
|
{
|
|
471
450
|
name: 'marrow_update_memory',
|
|
@@ -589,29 +568,12 @@ const TOOLS = [
|
|
|
589
568
|
inputSchema: {
|
|
590
569
|
type: 'object',
|
|
591
570
|
properties: {
|
|
592
|
-
action: {
|
|
593
|
-
type: 'string',
|
|
594
|
-
enum: ['register', 'list', 'get', 'update', 'start', 'advance', 'instances'],
|
|
595
|
-
description: 'Workflow action to perform',
|
|
596
|
-
},
|
|
571
|
+
action: { type: 'string', enum: ['register', 'list', 'get', 'update', 'start', 'advance', 'instances'], description: 'Workflow action to perform' },
|
|
597
572
|
workflowId: { type: 'string', description: 'Workflow ID (required for get/start/advance/instances)' },
|
|
598
573
|
instanceId: { type: 'string', description: 'Instance ID (required for advance)' },
|
|
599
574
|
name: { type: 'string', description: 'Workflow name (for register)' },
|
|
600
575
|
description: { type: 'string', description: 'Workflow description (for register/update)' },
|
|
601
|
-
steps: {
|
|
602
|
-
type: 'array',
|
|
603
|
-
description: 'Step definitions (for register)',
|
|
604
|
-
items: {
|
|
605
|
-
type: 'object',
|
|
606
|
-
properties: {
|
|
607
|
-
step: { type: 'number', description: 'Step order (1, 2, 3...)' },
|
|
608
|
-
agent_role: { type: 'string', description: 'Expected agent role (e.g., "builder", "auditor")' },
|
|
609
|
-
action_type: { type: 'string', description: 'Action type (e.g., "build", "audit", "patch")' },
|
|
610
|
-
description: { type: 'string', description: 'Step description' },
|
|
611
|
-
},
|
|
612
|
-
required: ['step', 'description'],
|
|
613
|
-
},
|
|
614
|
-
},
|
|
576
|
+
steps: { type: 'array', description: 'Step definitions (for register)', items: { type: 'object', properties: { step: { type: 'number', description: 'Step order (1, 2, 3...)' }, agent_role: { type: 'string', description: 'Expected agent role (e.g., "builder", "auditor")' }, action_type: { type: 'string', description: 'Action type (e.g., "build", "audit", "patch")' }, description: { type: 'string', description: 'Step description' } }, required: ['step', 'description'] } },
|
|
615
577
|
tags: { type: 'array', items: { type: 'string' }, description: 'Tags (for register)' },
|
|
616
578
|
agentId: { type: 'string', description: 'Agent ID starting the workflow (for start)' },
|
|
617
579
|
context: { type: 'object', description: 'Workflow context (for start)' },
|
|
@@ -629,13 +591,18 @@ const TOOLS = [
|
|
|
629
591
|
// Request handler
|
|
630
592
|
async function handleRequest(req) {
|
|
631
593
|
const { id, method, params } = req;
|
|
594
|
+
// [FIX #15] Enforce initialize-first per MCP spec
|
|
595
|
+
if (!initialized && method !== 'initialize') {
|
|
596
|
+
error(id, -32002, 'Server not initialized. Send initialize first.');
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
632
599
|
try {
|
|
633
600
|
if (method === 'initialize') {
|
|
634
601
|
initialized = true;
|
|
635
602
|
success(id, {
|
|
636
603
|
protocolVersion: '2024-11-05',
|
|
637
604
|
capabilities: { tools: {}, prompts: {} },
|
|
638
|
-
serverInfo: { name: 'marrow', version: '
|
|
605
|
+
serverInfo: { name: 'marrow', version: '3.0.8' },
|
|
639
606
|
});
|
|
640
607
|
// Auto-enroll: emit enrollment notification on connection
|
|
641
608
|
if (AUTO_ENROLL) {
|
|
@@ -656,7 +623,6 @@ async function handleRequest(req) {
|
|
|
656
623
|
return;
|
|
657
624
|
}
|
|
658
625
|
if (method === 'prompts/list') {
|
|
659
|
-
// Auto-enroll: only expose marrow-always-on prompt when MARROW_AUTO_ENROLL is set
|
|
660
626
|
if (AUTO_ENROLL) {
|
|
661
627
|
success(id, {
|
|
662
628
|
prompts: [
|
|
@@ -675,7 +641,6 @@ async function handleRequest(req) {
|
|
|
675
641
|
}
|
|
676
642
|
if (method === 'prompts/get') {
|
|
677
643
|
const promptName = params?.name;
|
|
678
|
-
// Auto-enroll gate: only serve marrow-always-on when enabled
|
|
679
644
|
if (promptName !== 'marrow-always-on' || !AUTO_ENROLL) {
|
|
680
645
|
error(id, -32602, 'Unknown prompt');
|
|
681
646
|
return;
|
|
@@ -744,7 +709,6 @@ This is not optional overhead — it's how you stop repeating the same failures.
|
|
|
744
709
|
if (toolName === 'marrow_orient') {
|
|
745
710
|
orientCallCount++;
|
|
746
711
|
const result = await (0, index_1.marrowOrient)(API_KEY, BASE_URL, { taskType: args.taskType, autoWarn: args.autoWarn ?? true }, SESSION_ID);
|
|
747
|
-
// Auto-enroll: on first orient call, prepend enrollment instructions
|
|
748
712
|
if (AUTO_ENROLL && orientCallCount === 1) {
|
|
749
713
|
const enrollmentText = `\n\n📋 **Marrow Auto-Enroll Active**\n\n` +
|
|
750
714
|
`You have Marrow — a collective memory that tells you what went wrong last time before you do it again.\n\n` +
|
|
@@ -771,8 +735,10 @@ This is not optional overhead — it's how you stop repeating the same failures.
|
|
|
771
735
|
return;
|
|
772
736
|
}
|
|
773
737
|
if (toolName === 'marrow_think') {
|
|
738
|
+
// [FIX #9] Validate required param
|
|
739
|
+
const action = requireString(args, 'action');
|
|
774
740
|
const result = await (0, index_1.marrowThink)(API_KEY, BASE_URL, {
|
|
775
|
-
action
|
|
741
|
+
action,
|
|
776
742
|
type: args.type,
|
|
777
743
|
context: args.context,
|
|
778
744
|
previous_decision_id: args.previous_decision_id,
|
|
@@ -799,7 +765,6 @@ This is not optional overhead — it's how you stop repeating the same failures.
|
|
|
799
765
|
...existingInsights,
|
|
800
766
|
];
|
|
801
767
|
}
|
|
802
|
-
// Track for auto-commit
|
|
803
768
|
lastDecisionId = result.decision_id;
|
|
804
769
|
lastCommitted = false;
|
|
805
770
|
success(id, {
|
|
@@ -808,10 +773,16 @@ This is not optional overhead — it's how you stop repeating the same failures.
|
|
|
808
773
|
return;
|
|
809
774
|
}
|
|
810
775
|
if (toolName === 'marrow_commit') {
|
|
776
|
+
// [FIX #9] Validate required params
|
|
777
|
+
const decision_id = requireString(args, 'decision_id');
|
|
778
|
+
const outcome = requireString(args, 'outcome');
|
|
779
|
+
if (typeof args.success !== 'boolean') {
|
|
780
|
+
throw new Error('"success" is required and must be a boolean');
|
|
781
|
+
}
|
|
811
782
|
const result = await (0, index_1.marrowCommit)(API_KEY, BASE_URL, {
|
|
812
|
-
decision_id
|
|
783
|
+
decision_id,
|
|
813
784
|
success: args.success,
|
|
814
|
-
outcome
|
|
785
|
+
outcome,
|
|
815
786
|
caused_by: args.caused_by,
|
|
816
787
|
}, SESSION_ID);
|
|
817
788
|
lastCommitted = true;
|
|
@@ -822,61 +793,83 @@ This is not optional overhead — it's how you stop repeating the same failures.
|
|
|
822
793
|
return;
|
|
823
794
|
}
|
|
824
795
|
if (toolName === 'marrow_run') {
|
|
825
|
-
//
|
|
826
|
-
|
|
827
|
-
const
|
|
828
|
-
|
|
796
|
+
// [FIX #9] Validate required params
|
|
797
|
+
const description = requireString(args, 'description');
|
|
798
|
+
const outcome = requireString(args, 'outcome');
|
|
799
|
+
// [FIX #16] Handle partial failures — return think result even if commit fails
|
|
800
|
+
let thinkResult = null;
|
|
801
|
+
try {
|
|
802
|
+
await (0, index_1.marrowOrient)(API_KEY, BASE_URL, undefined, SESSION_ID);
|
|
803
|
+
}
|
|
804
|
+
catch (err) {
|
|
805
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
806
|
+
process.stderr.write(`[marrow] marrow_run orient failed (continuing): ${msg}\n`);
|
|
807
|
+
}
|
|
808
|
+
thinkResult = await (0, index_1.marrowThink)(API_KEY, BASE_URL, {
|
|
809
|
+
action: description,
|
|
829
810
|
type: args.type || 'general',
|
|
830
811
|
}, SESSION_ID);
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
812
|
+
let commitResult = null;
|
|
813
|
+
try {
|
|
814
|
+
commitResult = await (0, index_1.marrowCommit)(API_KEY, BASE_URL, {
|
|
815
|
+
decision_id: thinkResult.decision_id,
|
|
816
|
+
success: args.success ?? true,
|
|
817
|
+
outcome,
|
|
818
|
+
}, SESSION_ID);
|
|
819
|
+
}
|
|
820
|
+
catch (err) {
|
|
821
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
822
|
+
process.stderr.write(`[marrow] marrow_run commit failed: ${msg}\n`);
|
|
823
|
+
success(id, {
|
|
824
|
+
content: [{
|
|
825
|
+
type: 'text',
|
|
826
|
+
text: JSON.stringify({
|
|
827
|
+
think: thinkResult,
|
|
828
|
+
commit: null,
|
|
829
|
+
commit_error: msg,
|
|
830
|
+
decision_id: thinkResult.decision_id,
|
|
831
|
+
}, null, 2),
|
|
832
|
+
}],
|
|
833
|
+
});
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
836
|
success(id, {
|
|
837
|
-
content: [
|
|
838
|
-
{
|
|
837
|
+
content: [{
|
|
839
838
|
type: 'text',
|
|
840
839
|
text: JSON.stringify({ think: thinkResult, commit: commitResult }, null, 2),
|
|
841
|
-
},
|
|
842
|
-
],
|
|
840
|
+
}],
|
|
843
841
|
});
|
|
844
842
|
return;
|
|
845
843
|
}
|
|
846
844
|
if (toolName === 'marrow_auto') {
|
|
847
|
-
//
|
|
848
|
-
|
|
849
|
-
const action = args.action;
|
|
845
|
+
// [FIX #9] Validate required param
|
|
846
|
+
const action = requireString(args, 'action');
|
|
850
847
|
const outcome = args.outcome;
|
|
851
848
|
const outcomeSuccess = args.success ?? true;
|
|
852
849
|
const type = args.type || 'general';
|
|
853
|
-
//
|
|
850
|
+
// [FIX #11] Cleanup pending decisions on each auto call
|
|
851
|
+
cleanupPending();
|
|
852
|
+
// [FIX #8] Include pending flag so agent knows logging is deferred
|
|
854
853
|
const response = {
|
|
855
854
|
action,
|
|
856
855
|
outcome: outcome || 'pending',
|
|
857
856
|
warnings: cachedOrientWarnings.map(formatWarningActionably),
|
|
857
|
+
logging: 'deferred',
|
|
858
858
|
};
|
|
859
859
|
// Fire-and-forget the actual API calls
|
|
860
860
|
(async () => {
|
|
861
861
|
try {
|
|
862
862
|
if (!outcome) {
|
|
863
|
-
// Intent only
|
|
864
863
|
await (0, index_1.marrowThink)(API_KEY, BASE_URL, { action, type }, SESSION_ID);
|
|
865
864
|
}
|
|
866
865
|
else {
|
|
867
|
-
// Full loop
|
|
868
866
|
const thinkResult = await (0, index_1.marrowThink)(API_KEY, BASE_URL, { action, type }, SESSION_ID);
|
|
869
|
-
await (0, index_1.marrowCommit)(API_KEY, BASE_URL, {
|
|
870
|
-
decision_id: thinkResult.decision_id,
|
|
871
|
-
success: outcomeSuccess,
|
|
872
|
-
outcome,
|
|
873
|
-
}, SESSION_ID);
|
|
867
|
+
await (0, index_1.marrowCommit)(API_KEY, BASE_URL, { decision_id: thinkResult.decision_id, success: outcomeSuccess, outcome }, SESSION_ID);
|
|
874
868
|
}
|
|
875
869
|
}
|
|
876
870
|
catch (err) {
|
|
877
|
-
// Log to stderr so agent can see it in logs
|
|
878
871
|
const msg = err instanceof Error ? err.message : String(err);
|
|
879
|
-
process.stderr.write(`[marrow] marrow_auto failed: ${msg}\n`);
|
|
872
|
+
process.stderr.write(`[marrow] marrow_auto background logging failed: ${msg}\n`);
|
|
880
873
|
}
|
|
881
874
|
})();
|
|
882
875
|
success(id, {
|
|
@@ -885,7 +878,8 @@ This is not optional overhead — it's how you stop repeating the same failures.
|
|
|
885
878
|
return;
|
|
886
879
|
}
|
|
887
880
|
if (toolName === 'marrow_ask') {
|
|
888
|
-
const
|
|
881
|
+
const query = requireString(args, 'query');
|
|
882
|
+
const result = await (0, index_1.marrowAsk)(API_KEY, BASE_URL, { query }, SESSION_ID);
|
|
889
883
|
success(id, {
|
|
890
884
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
891
885
|
});
|
|
@@ -898,104 +892,63 @@ This is not optional overhead — it's how you stop repeating the same failures.
|
|
|
898
892
|
});
|
|
899
893
|
return;
|
|
900
894
|
}
|
|
901
|
-
// Memory control tools
|
|
895
|
+
// Memory control tools — all use requireString for id validation
|
|
902
896
|
if (toolName === 'marrow_list_memories') {
|
|
903
|
-
const result = await marrowListMemories(API_KEY, BASE_URL, {
|
|
904
|
-
|
|
905
|
-
query: args.query,
|
|
906
|
-
limit: args.limit,
|
|
907
|
-
agentId: args.agentId,
|
|
908
|
-
}, SESSION_ID);
|
|
909
|
-
success(id, {
|
|
910
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
911
|
-
});
|
|
897
|
+
const result = await marrowListMemories(API_KEY, BASE_URL, { status: args.status, query: args.query, limit: args.limit, agentId: args.agentId }, SESSION_ID);
|
|
898
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
912
899
|
return;
|
|
913
900
|
}
|
|
914
901
|
if (toolName === 'marrow_get_memory') {
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
});
|
|
902
|
+
const memId = requireString(args, 'id');
|
|
903
|
+
const result = await marrowGetMemory(API_KEY, BASE_URL, memId, SESSION_ID);
|
|
904
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
919
905
|
return;
|
|
920
906
|
}
|
|
921
907
|
if (toolName === 'marrow_update_memory') {
|
|
922
|
-
const
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
tags: args.tags,
|
|
926
|
-
actor: args.actor,
|
|
927
|
-
note: args.note,
|
|
928
|
-
}, SESSION_ID);
|
|
929
|
-
success(id, {
|
|
930
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
931
|
-
});
|
|
908
|
+
const memId = requireString(args, 'id');
|
|
909
|
+
const result = await marrowUpdateMemory(API_KEY, BASE_URL, memId, { text: args.text, source: args.source, tags: args.tags, actor: args.actor, note: args.note }, SESSION_ID);
|
|
910
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
932
911
|
return;
|
|
933
912
|
}
|
|
934
913
|
if (toolName === 'marrow_delete_memory') {
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
});
|
|
914
|
+
const memId = requireString(args, 'id');
|
|
915
|
+
const result = await marrowDeleteMemory(API_KEY, BASE_URL, memId, { actor: args.actor, note: args.note }, SESSION_ID);
|
|
916
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
939
917
|
return;
|
|
940
918
|
}
|
|
941
919
|
if (toolName === 'marrow_mark_outdated') {
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
});
|
|
920
|
+
const memId = requireString(args, 'id');
|
|
921
|
+
const result = await marrowMarkOutdated(API_KEY, BASE_URL, memId, { actor: args.actor, note: args.note }, SESSION_ID);
|
|
922
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
946
923
|
return;
|
|
947
924
|
}
|
|
948
925
|
if (toolName === 'marrow_supersede_memory') {
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
actor: args.actor,
|
|
954
|
-
note: args.note,
|
|
955
|
-
}, SESSION_ID);
|
|
956
|
-
success(id, {
|
|
957
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
958
|
-
});
|
|
926
|
+
const memId = requireString(args, 'id');
|
|
927
|
+
const newText = requireString(args, 'text');
|
|
928
|
+
const result = await marrowSupersedeMemory(API_KEY, BASE_URL, memId, { text: newText, source: args.source, tags: args.tags, actor: args.actor, note: args.note }, SESSION_ID);
|
|
929
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
959
930
|
return;
|
|
960
931
|
}
|
|
961
932
|
if (toolName === 'marrow_share_memory') {
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
});
|
|
933
|
+
const memId = requireString(args, 'id');
|
|
934
|
+
const result = await marrowShareMemory(API_KEY, BASE_URL, memId, args.agentIds || [], args.actor, SESSION_ID);
|
|
935
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
966
936
|
return;
|
|
967
937
|
}
|
|
968
938
|
if (toolName === 'marrow_export_memories') {
|
|
969
|
-
const result = await marrowExportMemories(API_KEY, BASE_URL, {
|
|
970
|
-
|
|
971
|
-
status: args.status,
|
|
972
|
-
tags: args.tags,
|
|
973
|
-
}, SESSION_ID);
|
|
974
|
-
success(id, {
|
|
975
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
976
|
-
});
|
|
939
|
+
const result = await marrowExportMemories(API_KEY, BASE_URL, { format: args.format, status: args.status, tags: args.tags }, SESSION_ID);
|
|
940
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
977
941
|
return;
|
|
978
942
|
}
|
|
979
943
|
if (toolName === 'marrow_import_memories') {
|
|
980
944
|
const result = await marrowImportMemories(API_KEY, BASE_URL, args.memories || [], args.mode || 'merge', SESSION_ID);
|
|
981
|
-
success(id, {
|
|
982
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
983
|
-
});
|
|
945
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
984
946
|
return;
|
|
985
947
|
}
|
|
986
948
|
if (toolName === 'marrow_retrieve_memories') {
|
|
987
|
-
const
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
to: args.to,
|
|
991
|
-
tags: args.tags,
|
|
992
|
-
source: args.source,
|
|
993
|
-
status: args.status,
|
|
994
|
-
shared: args.shared,
|
|
995
|
-
}, SESSION_ID);
|
|
996
|
-
success(id, {
|
|
997
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
998
|
-
});
|
|
949
|
+
const query = requireString(args, 'query');
|
|
950
|
+
const result = await marrowRetrieveMemories(API_KEY, BASE_URL, query, { limit: args.limit, from: args.from, to: args.to, tags: args.tags, source: args.source, status: args.status, shared: args.shared }, SESSION_ID);
|
|
951
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
999
952
|
return;
|
|
1000
953
|
}
|
|
1001
954
|
if (toolName === 'marrow_workflow') {
|
|
@@ -1016,9 +969,7 @@ This is not optional overhead — it's how you stop repeating the same failures.
|
|
|
1016
969
|
contextUpdate: args.contextUpdate,
|
|
1017
970
|
status: args.status,
|
|
1018
971
|
}, SESSION_ID);
|
|
1019
|
-
success(id, {
|
|
1020
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
1021
|
-
});
|
|
972
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
1022
973
|
return;
|
|
1023
974
|
}
|
|
1024
975
|
error(id, -32601, `Method not found: ${toolName}`);
|
|
@@ -1034,8 +985,9 @@ This is not optional overhead — it's how you stop repeating the same failures.
|
|
|
1034
985
|
// MCP stdio loop — raw stdin, no readline (readline writes prompts to stdout which breaks MCP)
|
|
1035
986
|
let buffer = '';
|
|
1036
987
|
let pendingRequests = 0;
|
|
988
|
+
let stdinEnded = false;
|
|
1037
989
|
function checkExit() {
|
|
1038
|
-
if (pendingRequests === 0) {
|
|
990
|
+
if (stdinEnded && pendingRequests === 0) {
|
|
1039
991
|
autoCommitOnClose().then(() => process.exit(0));
|
|
1040
992
|
}
|
|
1041
993
|
}
|
|
@@ -1048,8 +1000,21 @@ process.stdin.on('data', (chunk) => {
|
|
|
1048
1000
|
const trimmed = line.trim();
|
|
1049
1001
|
if (!trimmed)
|
|
1050
1002
|
continue;
|
|
1003
|
+
// [FIX #1] Wrap JSON.parse in try-catch to prevent crash on malformed input
|
|
1004
|
+
let msg;
|
|
1005
|
+
try {
|
|
1006
|
+
msg = JSON.parse(trimmed);
|
|
1007
|
+
}
|
|
1008
|
+
catch (parseErr) {
|
|
1009
|
+
process.stderr.write(`[marrow] JSON parse error: ${parseErr}\n`);
|
|
1010
|
+
send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
// MCP notifications (no id) must be silently ignored per spec
|
|
1014
|
+
if (msg.id === undefined || msg.id === null)
|
|
1015
|
+
continue;
|
|
1051
1016
|
pendingRequests++;
|
|
1052
|
-
handleRequest(
|
|
1017
|
+
handleRequest(msg)
|
|
1053
1018
|
.catch((err) => {
|
|
1054
1019
|
process.stderr.write(`[marrow] Handler error: ${err}\n`);
|
|
1055
1020
|
})
|
|
@@ -1060,23 +1025,30 @@ process.stdin.on('data', (chunk) => {
|
|
|
1060
1025
|
}
|
|
1061
1026
|
});
|
|
1062
1027
|
process.stdin.on('end', () => {
|
|
1063
|
-
|
|
1028
|
+
stdinEnded = true;
|
|
1064
1029
|
if (buffer.trim()) {
|
|
1030
|
+
let msg;
|
|
1065
1031
|
try {
|
|
1066
|
-
|
|
1067
|
-
handleRequest(JSON.parse(buffer.trim()))
|
|
1068
|
-
.catch((err) => {
|
|
1069
|
-
process.stderr.write(`[marrow] Parse error on remaining: ${err}\n`);
|
|
1070
|
-
})
|
|
1071
|
-
.finally(() => {
|
|
1072
|
-
pendingRequests--;
|
|
1073
|
-
checkExit();
|
|
1074
|
-
});
|
|
1032
|
+
msg = JSON.parse(buffer.trim());
|
|
1075
1033
|
}
|
|
1076
1034
|
catch (err) {
|
|
1077
|
-
process.stderr.write(`[marrow]
|
|
1035
|
+
process.stderr.write(`[marrow] JSON parse error on remaining buffer: ${err}\n`);
|
|
1036
|
+
checkExit();
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (msg.id === undefined || msg.id === null) {
|
|
1078
1040
|
checkExit();
|
|
1041
|
+
return;
|
|
1079
1042
|
}
|
|
1043
|
+
pendingRequests++;
|
|
1044
|
+
handleRequest(msg)
|
|
1045
|
+
.catch((err) => {
|
|
1046
|
+
process.stderr.write(`[marrow] Handler error on remaining: ${err}\n`);
|
|
1047
|
+
})
|
|
1048
|
+
.finally(() => {
|
|
1049
|
+
pendingRequests--;
|
|
1050
|
+
checkExit();
|
|
1051
|
+
});
|
|
1080
1052
|
}
|
|
1081
1053
|
else {
|
|
1082
1054
|
checkExit();
|