overlord-cli 4.1.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.
@@ -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.3.0",
4
4
  "description": "Overlord CLI — launch AI agents on tickets from anywhere",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.