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.
- package/bin/_cli/setup.mjs +35 -12
- package/package.json +1 -1
- package/plugins/claude/README.md +2 -2
- package/plugins/claude/skills/{overlord-ticket-workflow → overlord-ticket}/SKILL.md +2 -2
- 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/SKILL.md +8 -0
- package/plugins/overlord/README.md +1 -1
- package/plugins/overlord/skills/{overlord-ticket-workflow → overlord-ticket}/SKILL.md +1 -1
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
package/plugins/claude/README.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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,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
|
|
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
|