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.
- package/bin/_cli/new-ticket.mjs +41 -14
- package/bin/_cli/protocol.mjs +23 -6
- package/bin/_cli/setup.mjs +39 -16
- package/package.json +1 -1
- package/plugins/claude/commands/spawn.md +2 -2
- package/plugins/claude/skills/overlord-ticket-workflow/SKILL.md +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/new-ticket.mjs
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
}
|
package/bin/_cli/protocol.mjs
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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":"@@ ... @@"}]}]'
|
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,
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
@@ -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,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.
|