overlord-cli 0.2605101225.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.
Files changed (42) hide show
  1. package/README.md +128 -0
  2. package/bin/_cli/attach.mjs +357 -0
  3. package/bin/_cli/auth.mjs +696 -0
  4. package/bin/_cli/cli-update.mjs +119 -0
  5. package/bin/_cli/credentials.mjs +723 -0
  6. package/bin/_cli/index.mjs +170 -0
  7. package/bin/_cli/launcher.mjs +474 -0
  8. package/bin/_cli/new-ticket.mjs +306 -0
  9. package/bin/_cli/postinstall.mjs +43 -0
  10. package/bin/_cli/protocol.mjs +1799 -0
  11. package/bin/_cli/setup.mjs +1634 -0
  12. package/bin/_cli/ticket.mjs +55 -0
  13. package/bin/_cli/tickets.mjs +121 -0
  14. package/bin/_cli/version.mjs +7 -0
  15. package/bin/ovld.mjs +13 -0
  16. package/package.json +36 -0
  17. package/plugins/claude/.claude-plugin/plugin.json +34 -0
  18. package/plugins/claude/README.md +42 -0
  19. package/plugins/claude/commands/connect.md +19 -0
  20. package/plugins/claude/commands/create.md +17 -0
  21. package/plugins/claude/commands/load.md +19 -0
  22. package/plugins/claude/commands/spawn.md +17 -0
  23. package/plugins/claude/hooks/hooks.json +15 -0
  24. package/plugins/claude/scripts/permission-hook.sh +21 -0
  25. package/plugins/claude/skills/overlord-ticket/SKILL.md +164 -0
  26. package/plugins/cursor/.cursor-plugin/plugin.json +14 -0
  27. package/plugins/cursor/commands/connect.md +3 -0
  28. package/plugins/cursor/commands/create.md +11 -0
  29. package/plugins/cursor/commands/load.md +3 -0
  30. package/plugins/cursor/commands/spawn.md +3 -0
  31. package/plugins/cursor/mcp.json +8 -0
  32. package/plugins/cursor/rules/overlord-local.mdc +10 -0
  33. package/plugins/cursor/scripts/overlord-mcp.mjs +150 -0
  34. package/plugins/cursor/skills/overlord-ticket/SKILL.md +49 -0
  35. package/plugins/overlord/.codex-plugin/plugin.json +35 -0
  36. package/plugins/overlord/.mcp.json +11 -0
  37. package/plugins/overlord/README.md +42 -0
  38. package/plugins/overlord/assets/icon.png +0 -0
  39. package/plugins/overlord/assets/logo.png +0 -0
  40. package/plugins/overlord/assets/screenshot.svg +28 -0
  41. package/plugins/overlord/scripts/overlord-mcp.mjs +688 -0
  42. package/plugins/overlord/skills/overlord-ticket/SKILL.md +146 -0
@@ -0,0 +1,688 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+
6
+ const execFileAsync = promisify(execFile);
7
+ const OVLD_BIN = process.env.OVLD_BIN?.trim() || 'ovld';
8
+ const PROTOCOL_VERSION = '2025-06-18';
9
+
10
+ function execFileWithOptionalInput(file, args, options, input) {
11
+ if (input === undefined) {
12
+ return execFileAsync(file, args, options);
13
+ }
14
+
15
+ return new Promise((resolve, reject) => {
16
+ const child = execFile(file, args, options, (error, stdout, stderr) => {
17
+ if (error) {
18
+ error.stdout = stdout;
19
+ error.stderr = stderr;
20
+ reject(error);
21
+ return;
22
+ }
23
+
24
+ resolve({ stdout, stderr });
25
+ });
26
+
27
+ child.stdin?.end(input);
28
+ });
29
+ }
30
+
31
+ const tools = [
32
+ {
33
+ name: 'discover_project',
34
+ description: 'Resolve the Overlord project that matches a working directory.',
35
+ inputSchema: {
36
+ type: 'object',
37
+ properties: {
38
+ working_directory: { type: 'string', description: 'Directory to match. Defaults to the current workspace.' }
39
+ }
40
+ },
41
+ toCliFlags: args => ({
42
+ 'working-directory': args.working_directory
43
+ }),
44
+ subcommand: 'discover-project'
45
+ },
46
+ {
47
+ name: 'attach_ticket',
48
+ description: 'Attach an agent session to an existing Overlord ticket and return the working context.',
49
+ inputSchema: {
50
+ type: 'object',
51
+ properties: {
52
+ ticket_id: { type: 'string', description: 'Target ticket ID' },
53
+ agent: { type: 'string' },
54
+ model: { type: 'string' },
55
+ method: { type: 'string' },
56
+ external_session_id: { type: ['string', 'null'] },
57
+ metadata: { type: 'object' }
58
+ },
59
+ required: ['ticket_id']
60
+ },
61
+ toCliFlags: args => ({
62
+ 'ticket-id': args.ticket_id,
63
+ agent: args.agent,
64
+ model: args.model,
65
+ method: args.method,
66
+ 'external-session-id': args.external_session_id,
67
+ 'metadata-json': args.metadata
68
+ }),
69
+ subcommand: 'attach'
70
+ },
71
+ {
72
+ name: 'connect_ticket',
73
+ description: 'Create a lightweight Overlord session without loading the full ticket context.',
74
+ inputSchema: {
75
+ type: 'object',
76
+ properties: {
77
+ ticket_id: { type: 'string' },
78
+ agent: { type: 'string' },
79
+ method: { type: 'string' }
80
+ },
81
+ required: ['ticket_id']
82
+ },
83
+ toCliFlags: args => ({
84
+ 'ticket-id': args.ticket_id,
85
+ agent: args.agent,
86
+ method: args.method
87
+ }),
88
+ subcommand: 'connect'
89
+ },
90
+ {
91
+ name: 'load_ticket_context',
92
+ description: 'Fetch Overlord ticket context without creating a session.',
93
+ inputSchema: {
94
+ type: 'object',
95
+ properties: {
96
+ ticket_id: { type: 'string' }
97
+ },
98
+ required: ['ticket_id']
99
+ },
100
+ toCliFlags: args => ({
101
+ 'ticket-id': args.ticket_id
102
+ }),
103
+ subcommand: 'load-context'
104
+ },
105
+ {
106
+ name: 'spawn_ticket',
107
+ description: 'Create a follow-up ticket and attach to it immediately.',
108
+ inputSchema: {
109
+ type: 'object',
110
+ properties: {
111
+ objective: { type: 'string' },
112
+ title: { type: 'string' },
113
+ priority: { type: 'string', enum: ['low', 'medium', 'high', 'urgent'] },
114
+ project_id: { type: 'string' },
115
+ working_directory: { type: 'string' },
116
+ acceptance_criteria: { type: 'string' },
117
+ available_tools: { type: 'string' },
118
+ execution_target: { type: 'string', enum: ['agent', 'human'] },
119
+ delegate: { type: 'string' },
120
+ parent_session_key: { type: 'string' },
121
+ parent_ticket_id: { type: 'string' },
122
+ agent: { type: 'string' },
123
+ model: { type: 'string' },
124
+ metadata: { type: 'object' },
125
+ method: { type: 'string' }
126
+ },
127
+ required: ['objective']
128
+ },
129
+ toCliFlags: args => ({
130
+ objective: args.objective,
131
+ title: args.title,
132
+ priority: args.priority,
133
+ 'project-id': args.project_id,
134
+ 'working-directory': args.working_directory,
135
+ 'acceptance-criteria': args.acceptance_criteria,
136
+ 'available-tools': args.available_tools,
137
+ 'execution-target': args.execution_target,
138
+ delegate: args.delegate,
139
+ 'parent-session-key': args.parent_session_key,
140
+ 'parent-ticket-id': args.parent_ticket_id,
141
+ agent: args.agent,
142
+ model: args.model,
143
+ 'metadata-json': args.metadata,
144
+ method: args.method
145
+ }),
146
+ subcommand: 'spawn'
147
+ },
148
+ {
149
+ name: 'post_update',
150
+ description: 'Post an Overlord progress update or activity event.',
151
+ inputSchema: {
152
+ type: 'object',
153
+ properties: {
154
+ session_key: { type: 'string' },
155
+ ticket_id: { type: 'string' },
156
+ summary: { type: 'string' },
157
+ phase: { type: 'string', enum: ['draft', 'execute', 'review', 'deliver', 'complete', 'blocked', 'cancelled'] },
158
+ event_type: { type: 'string', enum: ['update', 'user_follow_up', 'alert'] },
159
+ external_url: { type: ['string', 'null'] },
160
+ external_session_id: { type: ['string', 'null'] },
161
+ payload: { type: 'object' },
162
+ change_rationales: { type: 'array' }
163
+ },
164
+ required: ['session_key', 'ticket_id', 'summary']
165
+ },
166
+ toCliFlags: args => ({
167
+ 'session-key': args.session_key,
168
+ 'ticket-id': args.ticket_id,
169
+ summary: args.summary,
170
+ phase: args.phase,
171
+ 'event-type': args.event_type,
172
+ 'external-url': args.external_url,
173
+ 'external-session-id': args.external_session_id,
174
+ 'payload-json': args.payload,
175
+ 'change-rationales-json': args.change_rationales
176
+ }),
177
+ subcommand: 'update'
178
+ },
179
+ {
180
+ name: 'record_change_rationales',
181
+ description: 'Persist structured change rationale rows without posting a separate update.',
182
+ inputSchema: {
183
+ type: 'object',
184
+ properties: {
185
+ session_key: { type: 'string' },
186
+ ticket_id: { type: 'string' },
187
+ summary: { type: 'string' },
188
+ phase: { type: 'string', enum: ['draft', 'execute', 'review', 'deliver', 'complete', 'blocked', 'cancelled'] },
189
+ change_rationales: { type: 'array' }
190
+ },
191
+ required: ['session_key', 'ticket_id', 'change_rationales']
192
+ },
193
+ toCliFlags: args => ({
194
+ 'session-key': args.session_key,
195
+ 'ticket-id': args.ticket_id,
196
+ summary: args.summary,
197
+ phase: args.phase,
198
+ 'change-rationales-json': args.change_rationales
199
+ }),
200
+ subcommand: 'record-change-rationales'
201
+ },
202
+ {
203
+ name: 'ask_blocking_question',
204
+ description: 'Send a blocking question to the human reviewer or PM.',
205
+ inputSchema: {
206
+ type: 'object',
207
+ properties: {
208
+ session_key: { type: 'string' },
209
+ ticket_id: { type: 'string' },
210
+ question: { type: 'string' },
211
+ phase: { type: 'string', enum: ['draft', 'execute', 'review', 'deliver', 'complete', 'blocked', 'cancelled'] },
212
+ payload: { type: 'object' }
213
+ },
214
+ required: ['session_key', 'ticket_id', 'question']
215
+ },
216
+ toCliFlags: args => ({
217
+ 'session-key': args.session_key,
218
+ 'ticket-id': args.ticket_id,
219
+ question: args.question,
220
+ phase: args.phase,
221
+ 'payload-json': args.payload
222
+ }),
223
+ subcommand: 'ask'
224
+ },
225
+ {
226
+ name: 'read_shared_context',
227
+ description: 'Read persistent shared context entries for a ticket.',
228
+ inputSchema: {
229
+ type: 'object',
230
+ properties: {
231
+ session_key: { type: 'string' },
232
+ ticket_id: { type: 'string' },
233
+ query: { type: 'string' },
234
+ limit: { type: 'number' }
235
+ },
236
+ required: ['session_key', 'ticket_id']
237
+ },
238
+ toCliFlags: args => ({
239
+ 'session-key': args.session_key,
240
+ 'ticket-id': args.ticket_id,
241
+ query: args.query,
242
+ limit: args.limit
243
+ }),
244
+ subcommand: 'read-context'
245
+ },
246
+ {
247
+ name: 'write_shared_context',
248
+ description: 'Write a persistent shared context entry for future Overlord sessions.',
249
+ inputSchema: {
250
+ type: 'object',
251
+ properties: {
252
+ session_key: { type: 'string' },
253
+ ticket_id: { type: 'string' },
254
+ key: { type: 'string' },
255
+ value: {},
256
+ tags: { type: 'array', items: { type: 'string' } }
257
+ },
258
+ required: ['session_key', 'ticket_id', 'key', 'value']
259
+ },
260
+ toCliFlags: args => ({
261
+ 'session-key': args.session_key,
262
+ 'ticket-id': args.ticket_id,
263
+ key: args.key,
264
+ value: typeof args.value === 'string' ? args.value : JSON.stringify(args.value),
265
+ tags: Array.isArray(args.tags) ? args.tags.join(',') : args.tags
266
+ }),
267
+ subcommand: 'write-context'
268
+ },
269
+ {
270
+ name: 'deliver_ticket',
271
+ description:
272
+ 'Deliver final work back into Overlord with summary, artifacts, and change rationales. Large payloads are streamed to the CLI through stdin, so this tool does not create delivery scratch files.',
273
+ inputSchema: {
274
+ type: 'object',
275
+ properties: {
276
+ session_key: { type: 'string' },
277
+ ticket_id: { type: 'string' },
278
+ summary: { type: 'string' },
279
+ artifacts: { type: 'array' },
280
+ change_rationales: { type: 'array' },
281
+ skip_file_change_check: { type: 'boolean' }
282
+ },
283
+ required: ['session_key', 'ticket_id', 'summary']
284
+ },
285
+ toCliFlags: args => ({
286
+ 'session-key': args.session_key,
287
+ 'ticket-id': args.ticket_id,
288
+ 'payload-file': '-',
289
+ 'skip-file-change-check': args.skip_file_change_check
290
+ }),
291
+ toCliStdin: args => JSON.stringify({
292
+ summary: args.summary,
293
+ ...(Array.isArray(args.artifacts) ? { artifacts: args.artifacts } : {}),
294
+ ...(Array.isArray(args.change_rationales) ? { changeRationales: args.change_rationales } : {})
295
+ }),
296
+ subcommand: 'deliver'
297
+ },
298
+ {
299
+ name: 'list_attachments',
300
+ description:
301
+ 'List objective attachments visible to the current ticket session. Returns attachment IDs needed by get_attachment_download_url, plus their objective_id, label, content_type, file_size, and storage_path.',
302
+ inputSchema: {
303
+ type: 'object',
304
+ properties: {
305
+ session_key: { type: 'string' },
306
+ ticket_id: { type: 'string' },
307
+ objective_id: { type: 'string' }
308
+ },
309
+ required: ['session_key', 'ticket_id']
310
+ },
311
+ toCliFlags: args => ({
312
+ 'session-key': args.session_key,
313
+ 'ticket-id': args.ticket_id,
314
+ 'objective-id': args.objective_id
315
+ }),
316
+ subcommand: 'attachment-list'
317
+ },
318
+ {
319
+ name: 'prepare_attachment_upload',
320
+ description: 'Prepare an objective attachment upload and return a signed upload URL.',
321
+ inputSchema: {
322
+ type: 'object',
323
+ properties: {
324
+ session_key: { type: 'string' },
325
+ ticket_id: { type: 'string' },
326
+ objective_id: { type: 'string' },
327
+ file_name: { type: 'string' },
328
+ label: { type: 'string' },
329
+ content_type: { type: 'string' },
330
+ file_size: { type: 'number' },
331
+ metadata: { type: 'object' }
332
+ },
333
+ required: ['session_key', 'ticket_id', 'objective_id', 'file_name']
334
+ },
335
+ toCliFlags: args => ({
336
+ 'session-key': args.session_key,
337
+ 'ticket-id': args.ticket_id,
338
+ 'objective-id': args.objective_id,
339
+ 'file-name': args.file_name,
340
+ label: args.label,
341
+ 'content-type': args.content_type,
342
+ 'file-size': args.file_size,
343
+ 'metadata-json': args.metadata
344
+ }),
345
+ subcommand: 'attachment-prepare-upload'
346
+ },
347
+ {
348
+ name: 'finalize_attachment_upload',
349
+ description: 'Finalize an objective attachment after uploading bytes to the signed storage URL.',
350
+ inputSchema: {
351
+ type: 'object',
352
+ properties: {
353
+ session_key: { type: 'string' },
354
+ ticket_id: { type: 'string' },
355
+ objective_id: { type: 'string' },
356
+ storage_path: { type: 'string' },
357
+ label: { type: 'string' },
358
+ content_type: { type: 'string' },
359
+ file_size: { type: 'number' },
360
+ metadata: { type: 'object' }
361
+ },
362
+ required: ['session_key', 'ticket_id', 'objective_id', 'storage_path', 'label']
363
+ },
364
+ toCliFlags: args => ({
365
+ 'session-key': args.session_key,
366
+ 'ticket-id': args.ticket_id,
367
+ 'objective-id': args.objective_id,
368
+ 'storage-path': args.storage_path,
369
+ label: args.label,
370
+ 'content-type': args.content_type,
371
+ 'file-size': args.file_size,
372
+ 'metadata-json': args.metadata
373
+ }),
374
+ subcommand: 'attachment-finalize-upload'
375
+ },
376
+ {
377
+ name: 'get_attachment_download_url',
378
+ description: 'Create a signed download URL for an uploaded objective attachment.',
379
+ inputSchema: {
380
+ type: 'object',
381
+ properties: {
382
+ session_key: { type: 'string' },
383
+ ticket_id: { type: 'string' },
384
+ objective_id: { type: 'string' },
385
+ attachment_id: { type: 'string' },
386
+ storage_path: { type: 'string' },
387
+ expires_in: { type: 'number' }
388
+ },
389
+ required: ['session_key', 'ticket_id']
390
+ },
391
+ toCliFlags: args => ({
392
+ 'session-key': args.session_key,
393
+ 'ticket-id': args.ticket_id,
394
+ 'objective-id': args.objective_id,
395
+ 'attachment-id': args.attachment_id,
396
+ 'storage-path': args.storage_path,
397
+ 'expires-in': args.expires_in
398
+ }),
399
+ subcommand: 'attachment-download-url'
400
+ },
401
+ {
402
+ name: 'upload_attachment_file',
403
+ description: 'Prepare, upload, and finalize a local file as an objective attachment in one step.',
404
+ inputSchema: {
405
+ type: 'object',
406
+ properties: {
407
+ session_key: { type: 'string' },
408
+ ticket_id: { type: 'string' },
409
+ objective_id: { type: 'string' },
410
+ file: { type: 'string' },
411
+ file_name: { type: 'string' },
412
+ label: { type: 'string' },
413
+ content_type: { type: 'string' },
414
+ metadata: { type: 'object' }
415
+ },
416
+ required: ['session_key', 'ticket_id', 'objective_id', 'file']
417
+ },
418
+ toCliFlags: args => ({
419
+ 'session-key': args.session_key,
420
+ 'ticket-id': args.ticket_id,
421
+ 'objective-id': args.objective_id,
422
+ file: args.file,
423
+ 'file-name': args.file_name,
424
+ label: args.label,
425
+ 'content-type': args.content_type,
426
+ 'metadata-json': args.metadata
427
+ }),
428
+ subcommand: 'attachment-upload-file'
429
+ }
430
+ ];
431
+
432
+ const searchTicketsTool = {
433
+ name: 'search_tickets',
434
+ description:
435
+ 'Search Overlord tickets by keyword and/or filter by status. Leave query empty to list all tickets matching the status filter. Useful when the user asks to find tickets related to a subject or in a specific workflow state.',
436
+ inputSchema: {
437
+ type: 'object',
438
+ properties: {
439
+ query: {
440
+ type: 'string',
441
+ description:
442
+ 'Keyword or phrase to search for in ticket titles and objectives. Leave empty to list without text filtering.'
443
+ },
444
+ statuses: {
445
+ type: 'array',
446
+ items: { type: 'string' },
447
+ description:
448
+ 'Filter by one or more ticket statuses (e.g. ["next-up", "execute", "review"]). Omit to include all non-completed statuses.'
449
+ },
450
+ include_completed: {
451
+ type: 'boolean',
452
+ description: 'Whether to include completed tickets in results. Defaults to false.'
453
+ },
454
+ limit: {
455
+ type: 'number',
456
+ description: 'Maximum number of results to return (1–20, default 8).'
457
+ }
458
+ }
459
+ },
460
+ toCliFlags: args => ({
461
+ query: args.query,
462
+ statuses: Array.isArray(args.statuses) ? args.statuses.join(',') : args.statuses,
463
+ 'include-completed': args.include_completed,
464
+ limit: args.limit
465
+ }),
466
+ subcommand: 'search-tickets'
467
+ };
468
+
469
+ const toolMap = new Map([
470
+ ...tools.map(tool => [tool.name, tool]),
471
+ [searchTicketsTool.name, searchTicketsTool]
472
+ ]);
473
+ let buffer = Buffer.alloc(0);
474
+
475
+ function serializeMessage(message) {
476
+ const json = JSON.stringify(message);
477
+ const body = Buffer.from(json, 'utf8');
478
+ const header = Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, 'utf8');
479
+ return Buffer.concat([header, body]);
480
+ }
481
+
482
+ function send(message) {
483
+ process.stdout.write(serializeMessage(message));
484
+ }
485
+
486
+ function parseMessages(chunk) {
487
+ buffer = Buffer.concat([buffer, chunk]);
488
+ const messages = [];
489
+
490
+ while (true) {
491
+ const headerEnd = buffer.indexOf('\r\n\r\n');
492
+ if (headerEnd === -1) break;
493
+
494
+ const headerText = buffer.subarray(0, headerEnd).toString('utf8');
495
+ const headers = Object.fromEntries(
496
+ headerText
497
+ .split('\r\n')
498
+ .map(line => {
499
+ const separatorIndex = line.indexOf(':');
500
+ return [line.slice(0, separatorIndex).trim().toLowerCase(), line.slice(separatorIndex + 1).trim()];
501
+ })
502
+ );
503
+
504
+ const contentLength = Number(headers['content-length']);
505
+ if (!Number.isFinite(contentLength)) {
506
+ throw new Error('Missing Content-Length header');
507
+ }
508
+
509
+ const totalLength = headerEnd + 4 + contentLength;
510
+ if (buffer.length < totalLength) break;
511
+
512
+ const body = buffer.subarray(headerEnd + 4, totalLength).toString('utf8');
513
+ buffer = buffer.subarray(totalLength);
514
+ messages.push(JSON.parse(body));
515
+ }
516
+
517
+ return messages;
518
+ }
519
+
520
+ function success(id, result) {
521
+ send({ jsonrpc: '2.0', id, result });
522
+ }
523
+
524
+ function failure(id, code, message) {
525
+ send({ jsonrpc: '2.0', id, error: { code, message } });
526
+ }
527
+
528
+ function cliArgsFromFlags(flags) {
529
+ const args = [];
530
+
531
+ for (const [key, value] of Object.entries(flags)) {
532
+ if (value === undefined || value === null) continue;
533
+
534
+ if (typeof value === 'boolean') {
535
+ if (value) args.push(`--${key}`);
536
+ continue;
537
+ }
538
+
539
+ const serialized =
540
+ typeof value === 'string' || typeof value === 'number' ? String(value) : JSON.stringify(value);
541
+ args.push(`--${key}`, serialized);
542
+ }
543
+
544
+ return args;
545
+ }
546
+
547
+ async function runProtocol(tool, args) {
548
+ const toolArgs = args ?? {};
549
+ const cliArgs = ['protocol', tool.subcommand, ...cliArgsFromFlags(tool.toCliFlags(toolArgs))];
550
+ const stdin = typeof tool.toCliStdin === 'function' ? tool.toCliStdin(toolArgs) : undefined;
551
+
552
+ try {
553
+ const { stdout, stderr } = await execFileWithOptionalInput(
554
+ OVLD_BIN,
555
+ cliArgs,
556
+ {
557
+ cwd: process.cwd(),
558
+ env: {
559
+ ...process.env,
560
+ AGENT_IDENTIFIER: process.env.AGENT_IDENTIFIER ?? 'codex-overlord-plugin'
561
+ },
562
+ maxBuffer: 20 * 1024 * 1024
563
+ },
564
+ stdin
565
+ );
566
+
567
+ const trimmed = stdout.trim();
568
+ const data = trimmed ? JSON.parse(trimmed) : {};
569
+
570
+ return {
571
+ content: [
572
+ {
573
+ type: 'text',
574
+ text: JSON.stringify(
575
+ {
576
+ subcommand: tool.subcommand,
577
+ data,
578
+ stderr: stderr.trim() || undefined
579
+ },
580
+ null,
581
+ 2
582
+ )
583
+ }
584
+ ],
585
+ structuredContent: data
586
+ };
587
+ } catch (error) {
588
+ const stdout = typeof error?.stdout === 'string' ? error.stdout.trim() : '';
589
+ const stderr = typeof error?.stderr === 'string' ? error.stderr.trim() : '';
590
+ const message = error instanceof Error ? error.message : String(error);
591
+
592
+ let parsedStdout = stdout;
593
+ if (stdout) {
594
+ try {
595
+ parsedStdout = JSON.parse(stdout);
596
+ } catch {
597
+ // keep raw stdout
598
+ }
599
+ }
600
+
601
+ return {
602
+ content: [
603
+ {
604
+ type: 'text',
605
+ text: JSON.stringify(
606
+ {
607
+ subcommand: tool.subcommand,
608
+ error: message,
609
+ stdout: parsedStdout || undefined,
610
+ stderr: stderr || undefined
611
+ },
612
+ null,
613
+ 2
614
+ )
615
+ }
616
+ ],
617
+ isError: true
618
+ };
619
+ }
620
+ }
621
+
622
+ async function handleRequest(message) {
623
+ const { id, method, params } = message;
624
+
625
+ if (method === 'initialize') {
626
+ success(id, {
627
+ protocolVersion: PROTOCOL_VERSION,
628
+ capabilities: {
629
+ tools: {
630
+ listChanged: false
631
+ }
632
+ },
633
+ serverInfo: {
634
+ name: 'overlord',
635
+ version: '0.2.1'
636
+ },
637
+ instructions:
638
+ 'Use these tools to drive Overlord ticket workflows through the installed ovld CLI. Most operations expect a session key from attach or connect. If the CLI reports that OVERLORD_URL is unreachable, request permission escalation or network access before retrying.'
639
+ });
640
+ return;
641
+ }
642
+
643
+ if (method === 'ping') {
644
+ success(id, {});
645
+ return;
646
+ }
647
+
648
+ if (method === 'tools/list') {
649
+ success(id, {
650
+ tools: [...tools, searchTicketsTool].map(tool => ({
651
+ name: tool.name,
652
+ description: tool.description,
653
+ inputSchema: tool.inputSchema
654
+ }))
655
+ });
656
+ return;
657
+ }
658
+
659
+ if (method === 'tools/call') {
660
+ const tool = toolMap.get(params?.name);
661
+ if (!tool) {
662
+ failure(id, -32602, `Unknown tool: ${params?.name ?? 'undefined'}`);
663
+ return;
664
+ }
665
+
666
+ success(id, await runProtocol(tool, params?.arguments ?? {}));
667
+ return;
668
+ }
669
+
670
+ failure(id, -32601, `Method not found: ${method}`);
671
+ }
672
+
673
+ process.stdin.on('data', async chunk => {
674
+ try {
675
+ for (const message of parseMessages(chunk)) {
676
+ if (!message || typeof message !== 'object') continue;
677
+ if ('method' in message && 'id' in message) {
678
+ await handleRequest(message);
679
+ }
680
+ }
681
+ } catch (error) {
682
+ const message = error instanceof Error ? error.message : String(error);
683
+ process.stderr.write(`Overlord MCP server failed: ${message}\n`);
684
+ process.exit(1);
685
+ }
686
+ });
687
+
688
+ process.stdin.resume();