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.
- package/README.md +128 -0
- package/bin/_cli/attach.mjs +357 -0
- package/bin/_cli/auth.mjs +696 -0
- package/bin/_cli/cli-update.mjs +119 -0
- package/bin/_cli/credentials.mjs +723 -0
- package/bin/_cli/index.mjs +170 -0
- package/bin/_cli/launcher.mjs +474 -0
- package/bin/_cli/new-ticket.mjs +306 -0
- package/bin/_cli/postinstall.mjs +43 -0
- package/bin/_cli/protocol.mjs +1799 -0
- package/bin/_cli/setup.mjs +1634 -0
- package/bin/_cli/ticket.mjs +55 -0
- package/bin/_cli/tickets.mjs +121 -0
- package/bin/_cli/version.mjs +7 -0
- package/bin/ovld.mjs +13 -0
- package/package.json +36 -0
- package/plugins/claude/.claude-plugin/plugin.json +34 -0
- package/plugins/claude/README.md +42 -0
- package/plugins/claude/commands/connect.md +19 -0
- package/plugins/claude/commands/create.md +17 -0
- package/plugins/claude/commands/load.md +19 -0
- package/plugins/claude/commands/spawn.md +17 -0
- package/plugins/claude/hooks/hooks.json +15 -0
- package/plugins/claude/scripts/permission-hook.sh +21 -0
- package/plugins/claude/skills/overlord-ticket/SKILL.md +164 -0
- package/plugins/cursor/.cursor-plugin/plugin.json +14 -0
- package/plugins/cursor/commands/connect.md +3 -0
- package/plugins/cursor/commands/create.md +11 -0
- package/plugins/cursor/commands/load.md +3 -0
- package/plugins/cursor/commands/spawn.md +3 -0
- package/plugins/cursor/mcp.json +8 -0
- package/plugins/cursor/rules/overlord-local.mdc +10 -0
- package/plugins/cursor/scripts/overlord-mcp.mjs +150 -0
- package/plugins/cursor/skills/overlord-ticket/SKILL.md +49 -0
- package/plugins/overlord/.codex-plugin/plugin.json +35 -0
- package/plugins/overlord/.mcp.json +11 -0
- package/plugins/overlord/README.md +42 -0
- package/plugins/overlord/assets/icon.png +0 -0
- package/plugins/overlord/assets/logo.png +0 -0
- package/plugins/overlord/assets/screenshot.svg +28 -0
- package/plugins/overlord/scripts/overlord-mcp.mjs +688 -0
- 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();
|