overlord-cli 4.1.0 → 4.5.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.
@@ -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,
@@ -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.1.0",
3
+ "version": "4.5.0",
4
4
  "description": "Overlord CLI — launch AI agents on tickets from anywhere",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,7 +4,7 @@ Claude Code plugin that exposes the Overlord local ticket workflow to any Claude
4
4
 
5
5
  ## What ships
6
6
 
7
- - `skills/overlord-ticket-workflow/SKILL.md` — durable attach → update → ask → deliver workflow.
7
+ - `skills/overlord-ticket/SKILL.md` — durable attach → update → ask → deliver workflow.
8
8
  - `commands/{connect,load,spawn}.md` — slash commands for session routing and ticket creation.
9
9
  - `hooks/hooks.json` + `scripts/permission-hook.sh` — PermissionRequest notifier that posts to `/api/protocol/permission-request` on the Overlord platform.
10
10
  - `userConfig` for `overlord_url` (non-sensitive) and `agent_token` (sensitive → OS keychain) so the hook and CLI know where to talk.
@@ -35,7 +35,7 @@ The plugin prompts for `overlord_url` and `agent_token` at install time. The tok
35
35
 
36
36
  Inside Claude Code the components are surfaced with the plugin prefix:
37
37
 
38
- - skill → `overlord:overlord-ticket-workflow`
38
+ - skill → `overlord:overlord-ticket`
39
39
  - commands → `/overlord:connect`, `/overlord:load`, `/overlord:spawn`
40
40
 
41
41
  Prompts generated by Overlord (see `lib/overlord/ticket-prompt.ts`) already reference these names in `bundle` instruction mode.
@@ -1,9 +1,9 @@
1
1
  ---
2
- name: overlord-ticket-workflow
2
+ name: overlord-ticket
3
3
  description: Overlord local workflow protocol — attach, update, deliver lifecycle for ticket-driven work from Claude Code.
4
4
  ---
5
5
 
6
- # Overlord Local Workflow
6
+ # Overlord Ticket
7
7
 
8
8
  If you receive a prompt with a specified ticket ID, adhere to the following. If the prompt does not have a ticket ID, the user may choose to add one later, but otherwise, proceed without it.
9
9
 
@@ -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
3
+ description: Durable local workflow for Overlord tickets from Cursor.
4
+ ---
5
+
6
+ # Overlord Ticket
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.
@@ -30,7 +30,7 @@ The MCP server shells into the installed `ovld` binary so the plugin stays align
30
30
 
31
31
  ## Skill coverage
32
32
 
33
- - `skills/overlord-ticket-workflow/SKILL.md` teaches Codex the durable local workflow:
33
+ - `skills/overlord-ticket/SKILL.md` teaches Codex the durable local workflow:
34
34
  attach first, update during work, ask when blocked, and deliver last.
35
35
 
36
36
  ## App surface status
@@ -1,4 +1,4 @@
1
- # Overlord Ticket Workflow
1
+ # Overlord Ticket
2
2
 
3
3
  Use this skill when the user wants to work on an Overlord ticket from Codex through the local
4
4
  Overlord plugin.