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.
- package/bin/_cli/setup.mjs +35 -12
- package/package.json +1 -1
- package/plugins/cursor/.cursor-plugin/plugin.json +14 -0
- package/plugins/cursor/commands/connect.md +1 -0
- package/plugins/cursor/commands/load.md +1 -0
- package/plugins/cursor/commands/spawn.md +1 -0
- package/plugins/cursor/mcp.json +8 -0
- package/plugins/cursor/rules/overlord-local.mdc +8 -0
- package/plugins/cursor/scripts/overlord-mcp.mjs +150 -0
- package/plugins/cursor/skills/overlord-ticket-workflow/SKILL.md +8 -0
package/bin/_cli/setup.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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:
|
|
922
|
+
version: pluginVersion(paths.pluginManifest) ?? '0.0.0',
|
|
902
923
|
contentHash: currentContentHashForAgent('cursor'),
|
|
903
924
|
installedAt: new Date().toISOString(),
|
|
904
|
-
files: [paths.
|
|
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
|
-
|
|
962
|
-
|
|
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
|
|
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
|
@@ -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,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.
|