overlord-cli 4.0.0 → 4.3.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.
@@ -6,7 +6,14 @@ import { stdin as input, stdout as output } from 'node:process';
6
6
  import { buildAuthHeaders, resolveAuth } from './credentials.mjs';
7
7
  import { runLauncherCommand } from './launcher.mjs';
8
8
 
9
- const PROMPT_AGENTS = ['claude', 'codex', 'cursor', 'gemini', 'opencode'];
9
+ const PROMPT_AGENT_IDENTIFIERS = {
10
+ claude: 'claude-code',
11
+ codex: 'codex',
12
+ cursor: 'cursor',
13
+ gemini: 'gemini',
14
+ opencode: 'opencode'
15
+ };
16
+ const PROMPT_AGENTS = Object.keys(PROMPT_AGENT_IDENTIFIERS);
10
17
 
11
18
  function parseFlags(args) {
12
19
  const flags = {};
@@ -40,10 +47,10 @@ function parseFlags(args) {
40
47
 
41
48
  function buildUsage(commandName) {
42
49
  if (commandName === 'prompt') {
43
- return 'Usage: ovld prompt "<objective>" [--title "..."] [--acceptance-criteria "..."] [--available-tools "..."] [--execution-target agent|human] [--priority low|medium|high|urgent] [--project-id <id>] [--agent <agent>]';
50
+ return 'Usage: ovld prompt "<objective>" [--title "..."] [--acceptance-criteria "..."] [--available-tools "..."] [--execution-target agent|human] [--priority low|medium|high|urgent] [--project-id <id>] [--agent <agent>] [--delegate <agent>]';
44
51
  }
45
52
 
46
- return 'Usage: ovld create "<objective>" [--title "..."] [--acceptance-criteria "..."] [--available-tools "..."] [--execution-target agent|human] [--priority low|medium|high|urgent] [--project-id <id>]';
53
+ return 'Usage: ovld create "<objective>" [--title "..."] [--acceptance-criteria "..."] [--available-tools "..."] [--execution-target agent|human] [--priority low|medium|high|urgent] [--project-id <id>] [--delegate <agent>]';
47
54
  }
48
55
 
49
56
  function ensureObjective(commandName, objective) {
@@ -151,7 +158,9 @@ async function createTicket(platformUrl, agentToken, localSecret, body) {
151
158
 
152
159
  const data = await res.json().catch(() => ({}));
153
160
  if (!res.ok) {
154
- throw new Error(`Failed to create ticket (${res.status}): ${data.error ?? JSON.stringify(data)}`);
161
+ throw new Error(
162
+ `Failed to create ticket (${res.status}): ${data.error ?? JSON.stringify(data)}`
163
+ );
155
164
  }
156
165
 
157
166
  return data.ticket;
@@ -179,6 +188,20 @@ function resolveAgent(agent) {
179
188
  return normalizedAgent;
180
189
  }
181
190
 
191
+ export function resolvePromptAgentIdentifier(agent) {
192
+ return PROMPT_AGENT_IDENTIFIERS[agent] ?? agent;
193
+ }
194
+
195
+ export function resolveTicketCreationDelegate(flags = {}, selectedAgent = null) {
196
+ const explicitDelegate = typeof flags.delegate === 'string' ? flags.delegate.trim() : '';
197
+ if (explicitDelegate) return explicitDelegate;
198
+
199
+ if (selectedAgent) return resolvePromptAgentIdentifier(selectedAgent);
200
+
201
+ const envAgent = process.env.AGENT_IDENTIFIER?.trim();
202
+ return envAgent || null;
203
+ }
204
+
182
205
  async function runTicketCreationFlow(args, { commandName, launchAgent }) {
183
206
  const { flags, positionals } = parseFlags(args);
184
207
  const objective = String(flags.objective ?? positionals.join(' ')).trim();
@@ -200,6 +223,18 @@ async function runTicketCreationFlow(args, { commandName, launchAgent }) {
200
223
  renderItem: project => projectLabel(project)
201
224
  }));
202
225
 
226
+ const selectedAgent = launchAgent
227
+ ? (resolveAgent(typeof flags.agent === 'string' ? flags.agent : '') ??
228
+ (await promptForSelection({
229
+ items: PROMPT_AGENTS,
230
+ label: 'Agents',
231
+ prompt: 'Select an agent by number:',
232
+ renderItem: agent => agent
233
+ })))
234
+ : null;
235
+
236
+ const ticketDelegate = resolveTicketCreationDelegate(flags, selectedAgent);
237
+
203
238
  const ticket = await createTicket(platformUrl, agentToken, localSecret, {
204
239
  objective,
205
240
  title: String(flags.title ?? ''),
@@ -207,7 +242,8 @@ async function runTicketCreationFlow(args, { commandName, launchAgent }) {
207
242
  availableTools: String(flags['available-tools'] ?? ''),
208
243
  executionTarget: String(flags['execution-target'] ?? 'agent'),
209
244
  priority: String(flags.priority ?? 'medium'),
210
- projectId: selectedProject.id
245
+ projectId: selectedProject.id,
246
+ ...(ticketDelegate ? { delegate: ticketDelegate } : {})
211
247
  });
212
248
 
213
249
  if (!launchAgent) {
@@ -215,15 +251,6 @@ async function runTicketCreationFlow(args, { commandName, launchAgent }) {
215
251
  return;
216
252
  }
217
253
 
218
- const selectedAgent =
219
- resolveAgent(typeof flags.agent === 'string' ? flags.agent : '') ??
220
- (await promptForSelection({
221
- items: PROMPT_AGENTS,
222
- label: 'Agents',
223
- prompt: 'Select an agent by number:',
224
- renderItem: agent => agent
225
- }));
226
-
227
254
  process.env.TICKET_ID = ticket.id;
228
255
  await runLauncherCommand('run', [selectedAgent, '--ticket-id', ticket.id]);
229
256
  }
@@ -35,6 +35,22 @@ function parseFlags(args) {
35
35
  return result;
36
36
  }
37
37
 
38
+ export function resolveProtocolAgentIdentifier(flags = {}) {
39
+ const explicitAgent = typeof flags.agent === 'string' ? flags.agent.trim() : '';
40
+ if (explicitAgent) return explicitAgent;
41
+
42
+ const envAgent = process.env.AGENT_IDENTIFIER?.trim();
43
+ return envAgent || 'claude-code';
44
+ }
45
+
46
+ export function resolveProtocolTicketDelegate(flags = {}, agentIdentifier = '') {
47
+ const explicitDelegate = typeof flags.delegate === 'string' ? flags.delegate.trim() : '';
48
+ if (explicitDelegate) return explicitDelegate;
49
+
50
+ const resolvedAgent = String(agentIdentifier).trim();
51
+ return resolvedAgent || null;
52
+ }
53
+
38
54
  /**
39
55
  * Default request timeout in milliseconds. Overridable via --timeout flag or
40
56
  * OVERLORD_TIMEOUT env var. A bounded timeout prevents indefinite spinner hangs
@@ -427,7 +443,7 @@ async function protocolAttach(args) {
427
443
 
428
444
  const body = {
429
445
  ticketId,
430
- agentIdentifier: String(flags.agent ?? process.env.AGENT_IDENTIFIER ?? 'claude-code'),
446
+ agentIdentifier: resolveProtocolAgentIdentifier(flags),
431
447
  connectionMethod: String(flags.method ?? 'cli'),
432
448
  ...(externalSessionId !== undefined ? { externalSessionId } : {}),
433
449
  metadata: {
@@ -915,7 +931,7 @@ async function protocolConnect(args) {
915
931
 
916
932
  const body = {
917
933
  ticketId,
918
- agentIdentifier: String(flags.agent ?? process.env.AGENT_IDENTIFIER ?? 'claude-code'),
934
+ agentIdentifier: resolveProtocolAgentIdentifier(flags),
919
935
  connectionMethod: String(flags.method ?? 'cli'),
920
936
  metadata: {}
921
937
  };
@@ -969,6 +985,7 @@ async function protocolSpawn(args) {
969
985
  const objective = requireFlag(flags, 'objective', undefined);
970
986
  const { platformUrl, agentToken, localSecret } = resolveAuth();
971
987
  const timeoutMs = resolveTimeout(flags);
988
+ const agentIdentifier = resolveProtocolAgentIdentifier(flags);
972
989
 
973
990
  // When --project-id is not provided, auto-send cwd as workingDirectory
974
991
  // so the server can resolve the project from the local_working_directory setting.
@@ -976,7 +993,7 @@ async function protocolSpawn(args) {
976
993
 
977
994
  const body = {
978
995
  objective,
979
- agentIdentifier: String(flags.agent ?? process.env.AGENT_IDENTIFIER ?? 'claude-code'),
996
+ agentIdentifier,
980
997
  connectionMethod: String(flags.method ?? 'cli'),
981
998
  metadata: {},
982
999
  ...(flags.title ? { title: String(flags.title) } : {}),
@@ -986,7 +1003,7 @@ async function protocolSpawn(args) {
986
1003
  ...(flags['acceptance-criteria'] ? { acceptanceCriteria: String(flags['acceptance-criteria']) } : {}),
987
1004
  ...(flags['available-tools'] ? { availableTools: String(flags['available-tools']) } : {}),
988
1005
  ...(flags['execution-target'] ? { executionTarget: String(flags['execution-target']) } : {}),
989
- ...(flags.delegate ? { delegate: String(flags.delegate) } : {}),
1006
+ delegate: resolveProtocolTicketDelegate(flags, agentIdentifier),
990
1007
  ...(flags['parent-session-key'] ? { parentSessionKey: String(flags['parent-session-key']) } : {}),
991
1008
  ...(flags['parent-ticket-id'] ? { parentTicketId: String(flags['parent-ticket-id'] ?? process.env.TICKET_ID ?? '') } : {})
992
1009
  };
@@ -1260,12 +1277,12 @@ artifact-upload-file:
1260
1277
  Examples:
1261
1278
  ovld protocol discover-project
1262
1279
  ovld protocol discover-project --working-directory /path/to/repo
1263
- ovld protocol spawn --objective "Implement feature X" # auto-resolves project from cwd
1280
+ ovld protocol spawn --agent codex --objective "Implement feature X" # auto-resolves project from cwd
1264
1281
  ovld protocol attach --ticket-id abc-123
1265
1282
  ovld protocol attach --ticket-id abc-123 --external-session-id null
1266
1283
  ovld protocol connect --ticket-id abc-123
1267
1284
  ovld protocol load-context --ticket-id abc-123
1268
- ovld protocol spawn --objective "Implement user auth" --priority high
1285
+ ovld protocol spawn --agent codex --objective "Implement user auth" --priority high
1269
1286
  ovld protocol update --session-key <key> --ticket-id <id> --summary "Did X" --phase execute
1270
1287
  ovld protocol update --session-key <key> --ticket-id <id> --summary-file ./update.txt --event-type user_follow_up
1271
1288
  ovld protocol record-change-rationales --session-key <key> --ticket-id <id> --change-rationales-json '[{"label":"...","file_path":"...","summary":"...","why":"...","impact":"...","hunks":[{"header":"@@ ... @@"}]}]'
@@ -24,6 +24,8 @@ const PACKAGE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', 'plugins', 'overl
24
24
  const REPO_PLUGIN_DIR = path.resolve(__dirname, '..', '..', '..', '..', 'plugins', 'overlord');
25
25
  const PACKAGE_CLAUDE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', 'plugins', 'claude');
26
26
  const REPO_CLAUDE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', '..', '..', 'plugins', 'claude');
27
+ const PACKAGE_CURSOR_PLUGIN_DIR = path.resolve(__dirname, '..', '..', 'plugins', 'cursor');
28
+ const REPO_CURSOR_PLUGIN_DIR = path.resolve(__dirname, '..', '..', '..', '..', 'plugins', 'cursor');
27
29
  const CODEX_TARGET_PLUGIN_DIR = path.join(os.homedir(), '.codex', 'plugins', 'overlord');
28
30
  const CODEX_TARGET_PLUGIN_MANIFEST = path.join(
29
31
  CODEX_TARGET_PLUGIN_DIR,
@@ -367,7 +369,7 @@ argument-hint: <objective or raw flags>
367
369
  disable-model-invocation: true
368
370
  ---
369
371
 
370
- Run \`ovld protocol spawn\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol spawn --objective "<objective>"\`.`
372
+ Run \`ovld protocol spawn --agent claude-code\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol spawn --agent claude-code --objective "<objective>"\`.`
371
373
  }
372
374
  ];
373
375
  }
@@ -388,7 +390,7 @@ Run \`ovld protocol spawn\` with \`$ARGUMENTS\`. If no flags are present, treat
388
390
  {
389
391
  path: path.join(base, 'spawn.md'),
390
392
  content:
391
- 'Create a new Overlord ticket.\n\nRun `ovld protocol spawn --objective "<objective>"` using the text after `/spawn` unless raw flags were provided.\n'
393
+ 'Create a new Overlord ticket.\n\nRun `ovld protocol spawn --agent cursor --objective "<objective>"` using the text after `/spawn` unless raw flags were provided. If raw flags were provided, pass them after `ovld protocol spawn --agent cursor`.\n'
392
394
  }
393
395
  ];
394
396
  }
@@ -409,7 +411,7 @@ Run \`ovld protocol spawn\` with \`$ARGUMENTS\`. If no flags are present, treat
409
411
  {
410
412
  path: path.join(base, 'spawn.toml'),
411
413
  content:
412
- 'description = "Create a new Overlord ticket from the current conversation."\nprompt = """\nRun `ovld protocol spawn --objective "<objective>"` using `{{args}}` as the objective unless raw flags were provided.\n"""\n'
414
+ 'description = "Create a new Overlord ticket from the current conversation."\nprompt = """\nRun `ovld protocol spawn --agent gemini --objective "<objective>"` using `{{args}}` as the objective unless raw flags were provided. If raw flags were provided, pass them after `ovld protocol spawn --agent gemini`.\n"""\n'
413
415
  }
414
416
  ];
415
417
  }
@@ -441,7 +443,7 @@ description: Create a new Overlord ticket from the current conversation
441
443
  agent: build
442
444
  ---
443
445
 
444
- Run \`ovld protocol spawn\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol spawn --objective "<objective>"\`.`
446
+ Run \`ovld protocol spawn --agent opencode\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol spawn --agent opencode --objective "<objective>"\`.`
445
447
  }
446
448
  ];
447
449
  }
@@ -479,9 +481,7 @@ function currentContentHashForAgent(agent) {
479
481
  return claudeContentHash();
480
482
  }
481
483
  if (agent === 'cursor') {
482
- return contentHash(
483
- [CURSOR_RULES_CONTENT, ...slashCommandFiles('cursor').map(file => file.content)].join('\n')
484
- );
484
+ return contentHashForDirectory(cursorSourcePluginDir());
485
485
  }
486
486
  if (agent === 'gemini') {
487
487
  return contentHash(slashCommandFiles('gemini').map(file => file.content).join('\n'));
@@ -508,6 +508,14 @@ function claudeSourcePluginDir() {
508
508
  );
509
509
  }
510
510
 
511
+ function cursorSourcePluginDir() {
512
+ if (fs.existsSync(PACKAGE_CURSOR_PLUGIN_DIR)) return PACKAGE_CURSOR_PLUGIN_DIR;
513
+ if (fs.existsSync(REPO_CURSOR_PLUGIN_DIR)) return REPO_CURSOR_PLUGIN_DIR;
514
+ throw new Error(
515
+ `Cursor plugin bundle not found. Checked ${PACKAGE_CURSOR_PLUGIN_DIR} and ${REPO_CURSOR_PLUGIN_DIR}.`
516
+ );
517
+ }
518
+
511
519
  function listFilesRecursive(dir) {
512
520
  if (!fs.existsSync(dir)) return [];
513
521
  return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
@@ -731,6 +739,8 @@ function openCodePaths() {
731
739
  function cursorPaths() {
732
740
  const base = path.join(os.homedir(), '.cursor');
733
741
  return {
742
+ pluginDir: path.join(base, 'plugins', 'local', 'overlord'),
743
+ pluginManifest: path.join(base, 'plugins', 'local', 'overlord', '.cursor-plugin', 'plugin.json'),
734
744
  rulesFile: path.join(base, 'rules', 'overlord-local.mdc'),
735
745
  settingsFile: path.join(base, 'settings.json')
736
746
  };
@@ -870,10 +880,21 @@ function installCodex() {
870
880
 
871
881
  function installCursor() {
872
882
  const paths = cursorPaths();
873
- writeTextFile(paths.rulesFile, CURSOR_RULES_CONTENT);
874
- console.log(` ✓ Installed rules: ${paths.rulesFile}`);
875
-
876
- const slashResult = installSlashCommands('cursor');
883
+ const sourceDir = cursorSourcePluginDir();
884
+ fs.mkdirSync(path.dirname(paths.pluginDir), { recursive: true });
885
+ fs.rmSync(paths.pluginDir, { recursive: true, force: true });
886
+ fs.cpSync(sourceDir, paths.pluginDir, { recursive: true });
887
+ console.log(` ✓ Installed plugin: ${paths.pluginDir}`);
888
+
889
+ if (fs.existsSync(paths.rulesFile)) {
890
+ fs.rmSync(paths.rulesFile, { force: true });
891
+ console.log(` ✓ Removed legacy rules file: ${paths.rulesFile}`);
892
+ }
893
+ const removedLegacySlash = uninstallSlashCommands('cursor');
894
+ if (removedLegacySlash.removedFiles.length > 0) {
895
+ console.log(' ✓ Removed legacy slash commands:');
896
+ for (const filePath of removedLegacySlash.removedFiles) console.log(` ${filePath}`);
897
+ }
877
898
 
878
899
  const existingSettings = readJsonFile(paths.settingsFile);
879
900
  const permissions =
@@ -898,10 +919,10 @@ function installCursor() {
898
919
 
899
920
  const manifest = readManifest();
900
921
  manifest.cursor = {
901
- version: BUNDLE_VERSION,
922
+ version: pluginVersion(paths.pluginManifest) ?? '0.0.0',
902
923
  contentHash: currentContentHashForAgent('cursor'),
903
924
  installedAt: new Date().toISOString(),
904
- files: [paths.rulesFile, paths.settingsFile, ...slashResult.managedFiles]
925
+ files: [...listFilesRecursive(paths.pluginDir), paths.settingsFile]
905
926
  };
906
927
  writeManifest(manifest);
907
928
 
@@ -958,8 +979,10 @@ function doctorAgent(agent) {
958
979
  agent === 'claude'
959
980
  ? pluginVersion(path.join(claudeSourcePluginDir(), '.claude-plugin', 'plugin.json'))
960
981
  : agent === 'codex'
961
- ? pluginVersion(path.join(codexSourcePluginDir(), '.codex-plugin', 'plugin.json'))
962
- : BUNDLE_VERSION;
982
+ ? pluginVersion(path.join(codexSourcePluginDir(), '.codex-plugin', 'plugin.json'))
983
+ : agent === 'cursor'
984
+ ? pluginVersion(path.join(cursorSourcePluginDir(), '.cursor-plugin', 'plugin.json'))
985
+ : BUNDLE_VERSION;
963
986
  const currentHash = currentContentHashForAgent(agent);
964
987
 
965
988
  if (entry.version !== currentVersion || entry.contentHash !== currentHash) {
@@ -1382,7 +1405,7 @@ export async function runSetupCommand(args) {
1382
1405
  ovld setup Interactive setup (select agents and configure permissions)
1383
1406
  ovld setup claude Prepare the Overlord Claude plugin and migrate v3.25 connector files
1384
1407
  ovld setup codex Install Overlord Codex plugin bundle
1385
- ovld setup cursor Install Overlord rules, slash commands, and permissions for Cursor
1408
+ ovld setup cursor Install Overlord Cursor local plugin and permissions
1386
1409
  ovld setup gemini Install Overlord slash commands and policy rules for Gemini CLI
1387
1410
  ovld setup opencode Install Overlord connector for OpenCode
1388
1411
  ovld setup all Prepare all supported agents
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "4.0.0",
3
+ "version": "4.3.0",
4
4
  "description": "Overlord CLI — launch AI agents on tickets from anywhere",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,9 +7,9 @@ disable-model-invocation: true
7
7
  Create a new Overlord ticket from the user's request.
8
8
 
9
9
  Use `$ARGUMENTS` as the input.
10
- If it already contains flags such as `--title`, `--priority`, `--project-id`, or `--execution-target`, pass those flags through after `ovld protocol spawn`.
10
+ If it already contains flags such as `--title`, `--priority`, `--project-id`, or `--execution-target`, pass those flags through after `ovld protocol spawn --agent claude-code`.
11
11
  Otherwise, treat `$ARGUMENTS` as the objective text and run:
12
- `ovld protocol spawn --objective "<objective>"`
12
+ `ovld protocol spawn --agent claude-code --objective "<objective>"`
13
13
 
14
14
  If no objective was provided, ask the user for one and stop.
15
15
 
@@ -71,7 +71,7 @@ correct project by matching your current working directory against each project'
71
71
  configured "Local working directory". No `--project-id` flag is needed:
72
72
 
73
73
  ```bash
74
- ovld protocol spawn --objective "Implement feature X" --priority medium
74
+ ovld protocol spawn --agent claude-code --objective "Implement feature X" --priority medium
75
75
  ```
76
76
 
77
77
  To discover which project maps to the current directory:
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "overlord",
3
+ "version": "0.1.0",
4
+ "description": "Local Overlord workflow plugin for Cursor terminal agents.",
5
+ "author": {
6
+ "name": "Cooperativ",
7
+ "url": "https://github.com/cooperativ"
8
+ },
9
+ "homepage": "https://github.com/cooperativ/overlord",
10
+ "repository": "https://github.com/cooperativ/overlord",
11
+ "license": "UNLICENSED",
12
+ "skills": "./skills/",
13
+ "mcpServers": "./mcp.json"
14
+ }
@@ -0,0 +1 @@
1
+ Run `ovld protocol connect --ticket-id <ticketId>` using the text after `/connect`.
@@ -0,0 +1 @@
1
+ Run `ovld protocol load-context --ticket-id <ticketId>` using the text after `/load`.
@@ -0,0 +1 @@
1
+ Run `ovld protocol spawn --agent cursor --objective "<objective>"` using text after `/spawn` unless raw flags were provided.
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "overlord": {
4
+ "command": "node",
5
+ "args": ["./scripts/overlord-mcp.mjs"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ ---
2
+ description: Overlord local workflow protocol for Cursor.
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # Overlord Local Workflow
7
+
8
+ Attach first, update during work, ask when blocked, and deliver last using `ovld protocol` commands.
@@ -0,0 +1,150 @@
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
+ let buffer = Buffer.alloc(0);
10
+
11
+ function send(message) {
12
+ const json = JSON.stringify(message);
13
+ const body = Buffer.from(json, 'utf8');
14
+ const header = Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, 'utf8');
15
+ process.stdout.write(Buffer.concat([header, body]));
16
+ }
17
+
18
+ function parseMessages(chunk) {
19
+ buffer = Buffer.concat([buffer, chunk]);
20
+ const messages = [];
21
+ while (true) {
22
+ const headerEnd = buffer.indexOf('\r\n\r\n');
23
+ if (headerEnd === -1) break;
24
+ const headerText = buffer.subarray(0, headerEnd).toString('utf8');
25
+ const lengthMatch = headerText.match(/Content-Length:\s*(\d+)/i);
26
+ if (!lengthMatch) throw new Error('Missing Content-Length header');
27
+ const contentLength = Number(lengthMatch[1]);
28
+ const totalLength = headerEnd + 4 + contentLength;
29
+ if (buffer.length < totalLength) break;
30
+ const body = buffer.subarray(headerEnd + 4, totalLength).toString('utf8');
31
+ buffer = buffer.subarray(totalLength);
32
+ messages.push(JSON.parse(body));
33
+ }
34
+ return messages;
35
+ }
36
+
37
+ async function runProtocol(subcommand, args = {}) {
38
+ const flags = Object.entries(args).flatMap(([key, value]) => {
39
+ if (value === undefined || value === null) return [];
40
+ if (typeof value === 'boolean') return value ? [`--${key}`] : [];
41
+ if (Array.isArray(value)) return [`--${key}`, JSON.stringify(value)];
42
+ if (typeof value === 'object') return [`--${key}-json`, JSON.stringify(value)];
43
+ return [`--${key}`, String(value)];
44
+ });
45
+
46
+ try {
47
+ const { stdout } = await execFileAsync(OVLD_BIN, ['protocol', subcommand, ...flags], {
48
+ env: {
49
+ ...process.env,
50
+ AGENT_IDENTIFIER: process.env.AGENT_IDENTIFIER ?? 'cursor-overlord-plugin'
51
+ },
52
+ maxBuffer: 20 * 1024 * 1024
53
+ });
54
+ const data = stdout.trim() ? JSON.parse(stdout) : {};
55
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], structuredContent: data };
56
+ } catch (error) {
57
+ const message = error instanceof Error ? error.message : String(error);
58
+ return { content: [{ type: 'text', text: message }], isError: true };
59
+ }
60
+ }
61
+
62
+ process.stdin.on('data', async chunk => {
63
+ for (const message of parseMessages(chunk)) {
64
+ if (!message || typeof message !== 'object' || !('id' in message)) continue;
65
+ if (message.method === 'initialize') {
66
+ send({
67
+ jsonrpc: '2.0',
68
+ id: message.id,
69
+ result: {
70
+ protocolVersion: PROTOCOL_VERSION,
71
+ capabilities: { tools: { listChanged: false } },
72
+ serverInfo: { name: 'overlord-cursor', version: '0.1.0' }
73
+ }
74
+ });
75
+ continue;
76
+ }
77
+ if (message.method === 'tools/list') {
78
+ send({
79
+ jsonrpc: '2.0',
80
+ id: message.id,
81
+ result: {
82
+ tools: [
83
+ {
84
+ name: 'attach_ticket',
85
+ description: 'Attach to an Overlord ticket.',
86
+ inputSchema: { type: 'object', properties: { ticket_id: { type: 'string' } }, required: ['ticket_id'] }
87
+ },
88
+ {
89
+ name: 'post_update',
90
+ description: 'Post a progress update.',
91
+ inputSchema: {
92
+ type: 'object',
93
+ properties: { session_key: { type: 'string' }, ticket_id: { type: 'string' }, summary: { type: 'string' } },
94
+ required: ['session_key', 'ticket_id', 'summary']
95
+ }
96
+ },
97
+ {
98
+ name: 'deliver_ticket',
99
+ description: 'Deliver completed work.',
100
+ inputSchema: {
101
+ type: 'object',
102
+ properties: { session_key: { type: 'string' }, ticket_id: { type: 'string' }, summary: { type: 'string' } },
103
+ required: ['session_key', 'ticket_id', 'summary']
104
+ }
105
+ }
106
+ ]
107
+ }
108
+ });
109
+ continue;
110
+ }
111
+ if (message.method === 'tools/call') {
112
+ const toolName = message.params?.name;
113
+ const args = message.params?.arguments ?? {};
114
+ if (toolName === 'attach_ticket') {
115
+ send({ jsonrpc: '2.0', id: message.id, result: await runProtocol('attach', { 'ticket-id': args.ticket_id }) });
116
+ } else if (toolName === 'post_update') {
117
+ send({
118
+ jsonrpc: '2.0',
119
+ id: message.id,
120
+ result: await runProtocol('update', {
121
+ 'session-key': args.session_key,
122
+ 'ticket-id': args.ticket_id,
123
+ summary: args.summary,
124
+ phase: 'execute'
125
+ })
126
+ });
127
+ } else if (toolName === 'deliver_ticket') {
128
+ send({
129
+ jsonrpc: '2.0',
130
+ id: message.id,
131
+ result: await runProtocol('deliver', {
132
+ 'session-key': args.session_key,
133
+ 'ticket-id': args.ticket_id,
134
+ summary: args.summary
135
+ })
136
+ });
137
+ } else {
138
+ send({ jsonrpc: '2.0', id: message.id, error: { code: -32602, message: `Unknown tool: ${toolName}` } });
139
+ }
140
+ continue;
141
+ }
142
+ if (message.method === 'ping') {
143
+ send({ jsonrpc: '2.0', id: message.id, result: {} });
144
+ continue;
145
+ }
146
+ send({ jsonrpc: '2.0', id: message.id, error: { code: -32601, message: `Method not found: ${message.method}` } });
147
+ }
148
+ });
149
+
150
+ process.stdin.resume();
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: overlord-ticket-workflow
3
+ description: Durable local workflow for Overlord tickets from Cursor.
4
+ ---
5
+
6
+ # Overlord Ticket Workflow
7
+
8
+ Attach first with `ovld protocol attach --ticket-id <ticket-id>`, keep the `sessionKey`, post updates during implementation, and deliver last with change rationales.