overlord-cli 3.22.0 → 3.24.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/README.md CHANGED
@@ -33,6 +33,9 @@ ovld attach
33
33
  ovld create "Investigate the failing build"
34
34
  ovld prompt "Draft a fix for the onboarding flow"
35
35
  ovld update
36
+ ovld protocol discover-project
37
+ ovld protocol attach --ticket-id <ticket-id>
38
+ ovld protocol update --session-key <session-key> --ticket-id <ticket-id> --summary "Working on it" --phase execute
36
39
  ovld setup codex
37
40
  ovld setup cursor
38
41
  ovld setup gemini
@@ -53,8 +56,9 @@ ovld doctor
53
56
  - `auth` - log in, log out, or check auth status
54
57
  - `tickets` - list or create tickets
55
58
  - `ticket` - work with a single ticket
56
- - `protocol` - run ticket lifecycle commands
57
- - `connect`, `restart`, `run`, `resume`, `context` - launch or resume an agent session
59
+ - `protocol` - run ticket lifecycle commands such as `discover-project`, `attach`, `connect`, `load-context`, `spawn`, `update`, `record-change-rationales`, `ask`, `read-context`, `write-context`, `deliver`, and artifact upload/download helpers
60
+ - `connect`, `restart`, `context` - launch or resume an agent session or print ticket context
61
+ - `run`, `resume` - legacy aliases for `connect` and `restart`
58
62
  - `setup` - install the Overlord connector or plugin bundle for a supported agent
59
63
  - `update` - install the latest CLI release from npm
60
64
  - `doctor` - verify installed agent connectors and check whether a newer CLI version is available
@@ -38,7 +38,7 @@ Usage:
38
38
  ${primaryCommand} connect <agent> Launch an agent on a ticket
39
39
  ${primaryCommand} restart <agent> Resume an agent session
40
40
  ${primaryCommand} context Print ticket context (requires TICKET_ID)
41
- ${primaryCommand} setup <agent|all> Install Overlord agent connector
41
+ ${primaryCommand} setup [agent|all] Install Overlord agent connector (interactive if no args)
42
42
  ${primaryCommand} update Install the latest CLI version from npm
43
43
  ${primaryCommand} doctor Validate installed agent connectors and check for CLI updates
44
44
  ${primaryCommand} version Show the installed CLI version
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Post-install message for Overlord CLI
5
+ * Shown after `npm install -g overlord-cli` or `yarn global add overlord-cli`
6
+ */
7
+
8
+ import { fileURLToPath } from 'node:url';
9
+ import { dirname } from 'node:path';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+
14
+ // Only show message if this is a global install (not local dev)
15
+ const isGlobalInstall = process.env.npm_config_global === 'true' ||
16
+ process.env.npm_execpath?.includes('yarn') ||
17
+ !__dirname.includes('node_modules');
18
+
19
+ if (!isGlobalInstall) {
20
+ process.exit(0);
21
+ }
22
+
23
+ const green = s => `\x1b[32m${s}\x1b[0m`;
24
+ const cyan = s => `\x1b[36m${s}\x1b[0m`;
25
+ const bold = s => `\x1b[1m${s}\x1b[0m`;
26
+
27
+ console.log(`
28
+ ${green('✓')} Overlord CLI installed successfully!
29
+
30
+ ${bold('Next step:')} Configure agent connectors
31
+
32
+ ${cyan('ovld setup')}
33
+
34
+ This will guide you through:
35
+ • Selecting which agent connectors to install (Claude, Cursor, etc.)
36
+ • Configuring agent permissions for Overlord protocol access
37
+
38
+ You can also run ${cyan('ovld setup <agent>')} to install a specific agent connector,
39
+ or ${cyan('ovld doctor')} to check your installation status.
40
+
41
+ Run ${cyan('ovld help')} to see all available commands.
42
+ `);
@@ -189,6 +189,37 @@ function readJsonFile(filePath, label) {
189
189
  }
190
190
  }
191
191
 
192
+ async function readTextFromStdin(label) {
193
+ const chunks = [];
194
+ try {
195
+ for await (const chunk of process.stdin) {
196
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
197
+ }
198
+ } catch (err) {
199
+ throw new Error(
200
+ `${label}: could not read stdin: ${err instanceof Error ? err.message : String(err)}`
201
+ );
202
+ }
203
+ return Buffer.concat(chunks).toString('utf8');
204
+ }
205
+
206
+ async function readJsonFileOrStdin(filePath, label) {
207
+ if (filePath !== '-') {
208
+ return readJsonFile(filePath, label);
209
+ }
210
+
211
+ try {
212
+ return JSON.parse(await readTextFromStdin(label));
213
+ } catch (err) {
214
+ if (err instanceof Error && err.message.startsWith(`${label}: could not read stdin`)) {
215
+ throw err;
216
+ }
217
+ throw new Error(
218
+ `${label}: could not parse stdin: ${err instanceof Error ? err.message : String(err)}`
219
+ );
220
+ }
221
+ }
222
+
192
223
  // ---------------------------------------------------------------------------
193
224
  // changeRationales helper
194
225
  // ---------------------------------------------------------------------------
@@ -200,7 +231,10 @@ function readJsonFile(filePath, label) {
200
231
  */
201
232
  async function resolveChangeRationales(flags) {
202
233
  if (flags['change-rationales-file']) {
203
- return readJsonFile(String(flags['change-rationales-file']), '--change-rationales-file');
234
+ return await readJsonFileOrStdin(
235
+ String(flags['change-rationales-file']),
236
+ '--change-rationales-file'
237
+ );
204
238
  }
205
239
  if (flags['change-rationales-json']) {
206
240
  try {
@@ -624,7 +658,7 @@ async function protocolDeliver(args) {
624
658
  if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
625
659
  if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
626
660
  const deliverPayload = flags['payload-file']
627
- ? readJsonFile(String(flags['payload-file']), '--payload-file')
661
+ ? await readJsonFileOrStdin(String(flags['payload-file']), '--payload-file')
628
662
  : null;
629
663
  const summary = deliverPayload?.summary ??
630
664
  (flags['summary-file']
@@ -642,7 +676,7 @@ async function protocolDeliver(args) {
642
676
  throw new Error('Use either --payload-file or --artifacts-json, not both');
643
677
  }
644
678
  if (flags['artifacts-file']) {
645
- artifacts = readJsonFile(String(flags['artifacts-file']), '--artifacts-file');
679
+ artifacts = await readJsonFileOrStdin(String(flags['artifacts-file']), '--artifacts-file');
646
680
  } else if (flags['artifacts-json']) {
647
681
  try {
648
682
  artifacts = JSON.parse(String(flags['artifacts-json']));
@@ -1144,14 +1178,15 @@ deliver:
1144
1178
  --session-key <key>
1145
1179
  --ticket-id <id>
1146
1180
  --summary <text> or --summary-file <path>
1147
- or: --payload-file <path> containing { summary, artifacts, changeRationales }
1181
+ or: --payload-file <path|-> containing { summary, artifacts, changeRationales }
1148
1182
  Optional:
1149
1183
  --artifacts-json <json>
1150
- --artifacts-file <path>
1184
+ --artifacts-file <path|->
1151
1185
  --change-rationales-json <json>
1152
- --change-rationales-file <path>
1186
+ --change-rationales-file <path|->
1153
1187
  --skip-file-change-check Bypass local git vs changeRationales validation
1154
1188
  Notes:
1189
+ Use --payload-file - to read the full delivery JSON from stdin without creating a scratch file.
1155
1190
  Do not combine --payload-file with --artifacts-json/--artifacts-file or change-rationale flags.
1156
1191
  In a git workspace, deliver validates that changed files are represented by changeRationales unless skipped.
1157
1192
 
@@ -1243,6 +1278,7 @@ Examples:
1243
1278
  ovld protocol deliver --session-key <key> --ticket-id <id> --summary "Done"
1244
1279
  ovld protocol deliver --session-key <key> --ticket-id <id> --summary "Done" --artifacts-file ./artifacts.json
1245
1280
  ovld protocol deliver --session-key <key> --ticket-id <id> --payload-file ./deliver.json
1281
+ ovld protocol deliver --session-key <key> --ticket-id <id> --payload-file -
1246
1282
  ovld protocol deliver --session-key <key> --ticket-id <id> --summary "Done" --skip-file-change-check
1247
1283
  ovld protocol deliver --session-key <key> --ticket-id <id> --summary "Done" --timeout 60000
1248
1284
  `);
@@ -84,11 +84,11 @@ If you receive a prompt with a specified ticket ID, adhere to the following. If
84
84
  --change-rationales-json '[{"label":"Short reviewer title","file_path":"path/to/file.ts","summary":"What changed.","why":"Why it changed.","impact":"Behavioral impact.","hunks":[{"header":"@@ -10,6 +10,14 @@"}]}]'
85
85
  \`\`\`
86
86
 
87
- For larger or quote-sensitive deliveries, prefer a single JSON file:
87
+ For larger or quote-sensitive deliveries, prefer a single JSON payload on stdin:
88
88
  \`\`\`bash
89
- ovld protocol deliver --session-key <sessionKey> --ticket-id $TICKET_ID --payload-file ./deliver.json
89
+ ovld protocol deliver --session-key <sessionKey> --ticket-id $TICKET_ID --payload-file -
90
90
  \`\`\`
91
- Treat \`deliver.json\` as ephemeral scratch data only. Create it outside the repository when practical, never commit it, and remove it after delivery.
91
+ This avoids creating a scratch delivery file that needs cleanup. If your runtime cannot provide stdin directly, \`--payload-file ./deliver.json\` remains supported; treat that file as ephemeral scratch data, never commit it, and remove it after delivery.
92
92
 
93
93
  ## Change Rationales
94
94
 
@@ -96,7 +96,7 @@ Always include \`changeRationales\` when delivering. Optionally include them on
96
96
 
97
97
  Before delivering, make sure every meaningful git-tracked file change is represented in \`changeRationales\`; do not send \`file_changes\` as an artifact.
98
98
 
99
- These are structured protocol payloads that Overlord stores as first-class rows in the \`file_changes\` table. Prefer inline JSON or the dedicated command below. For quote-sensitive deliveries, prefer \`--payload-file\` so summary, artifacts, and change rationales stay in one JSON document, but treat that JSON as ephemeral scratch data rather than a repository artifact. Ordinary deliver artifacts should use \`next_steps\`, \`test_results\`, \`migration\`, \`note\`, \`url\`, or \`decision\`.
99
+ These are structured protocol payloads that Overlord stores as first-class rows in the \`file_changes\` table. Prefer inline JSON or the dedicated command below. For larger full delivery payloads, prefer \`--payload-file -\` so summary, artifacts, and change rationales stay in one JSON document without creating a temporary file. Ordinary deliver artifacts should use \`next_steps\`, \`test_results\`, \`migration\`, \`note\`, \`url\`, or \`decision\`.
100
100
 
101
101
  \`\`\`bash
102
102
  ovld protocol record-change-rationales --session-key <sessionKey> --ticket-id $TICKET_ID \\\\
@@ -158,11 +158,11 @@ If you receive a prompt with a specified ticket ID, adhere to the following. If
158
158
  --artifacts-json '[{"type":"next_steps","label":"Next steps","content":"..."}]' \\\\
159
159
  --change-rationales-json '[{"label":"Short reviewer title","file_path":"path/to/file.ts","summary":"What changed.","why":"Why it changed.","impact":"Behavioral impact.","hunks":[{"header":"@@ -10,6 +10,14 @@"}]}]'
160
160
  \`\`\`
161
- If you use \`--payload-file\`, \`--artifacts-file\`, or \`--change-rationales-file\` for larger JSON, treat that file as ephemeral scratch data outside the repository and remove it after delivery. Do not leave delivery JSON checked into the worktree.
161
+ For larger delivery JSON, prefer \`--payload-file -\` and stream the full payload on stdin so no scratch file needs to be created or removed. If you use \`--payload-file\`, \`--artifacts-file\`, or \`--change-rationales-file\` with a real path, treat that file as ephemeral scratch data outside the repository and remove it after delivery. Do not leave delivery JSON checked into the worktree.
162
162
 
163
163
  ## Change Rationales
164
164
 
165
- Always include \`changeRationales\` when delivering. Before delivering, make sure every meaningful git-tracked file change is represented in \`changeRationales\`; do not send \`file_changes\` as an artifact. Record only meaningful behavioral changes. Overlord stores these as structured rows in the \`file_changes\` table. If you need a JSON file for transport, keep it ephemeral and out of the repository.
165
+ Always include \`changeRationales\` when delivering. Before delivering, make sure every meaningful git-tracked file change is represented in \`changeRationales\`; do not send \`file_changes\` as an artifact. Record only meaningful behavioral changes. Overlord stores these as structured rows in the \`file_changes\` table. For larger delivery payloads, prefer \`--payload-file -\` with stdin. If you need a JSON file for transport, keep it ephemeral and out of the repository.
166
166
 
167
167
  \`\`\`bash
168
168
  ovld protocol record-change-rationales --session-key <sessionKey> --ticket-id $TICKET_ID \\\\
@@ -231,7 +231,7 @@ When done, deliver with artifacts and change rationales:
231
231
  \`\`\`bash
232
232
  ovld protocol deliver --session-key <sessionKey> --ticket-id $TICKET_ID --summary "Narrative: what you did, next steps." --artifacts-json '[{"type":"next_steps","label":"Next steps","content":"..."}]' --change-rationales-json '[{"label":"Short reviewer title","file_path":"path/to/file.ts","summary":"What changed.","why":"Why it changed.","impact":"Behavioral impact.","hunks":[{"header":"@@ -10,6 +10,14 @@"}]}]'
233
233
  \`\`\`
234
- If you use a JSON file for delivery transport, keep it ephemeral scratch data outside the repository and remove it after the protocol call.
234
+ For larger delivery JSON, prefer \`--payload-file -\` with stdin so no scratch file needs to be created or removed. If you use a JSON file for delivery transport, keep it ephemeral scratch data outside the repository and remove it after the protocol call.
235
235
 
236
236
  Rules:
237
237
  - Always attach first and deliver last.
@@ -950,6 +950,392 @@ function currentNodeMajor() {
950
950
  return Number.parseInt(process.versions.node.split('.')[0] ?? '', 10);
951
951
  }
952
952
 
953
+ // ---------------------------------------------------------------------------
954
+ // Interactive checkbox prompt
955
+ // ---------------------------------------------------------------------------
956
+
957
+ /**
958
+ * Run an interactive checkbox list (multiselect with spacebar).
959
+ *
960
+ * @param {object} opts
961
+ * @param {string} opts.message - Prompt message shown above the list
962
+ * @param {string[]} opts.choices - List of choice labels
963
+ * @param {string[]} [opts.defaults] - Initially selected choices
964
+ * @returns {Promise<string[]>} - Array of selected choice labels
965
+ */
966
+ function runCheckboxPrompt({ message, choices, defaults = [] }) {
967
+ return new Promise(resolve => {
968
+ const hide = '\x1b[?25l';
969
+ const show = '\x1b[?25h';
970
+ const saveCursor = '\x1b7';
971
+ const restoreCursor = '\x1b8';
972
+ const eraseBelow = '\x1b[J';
973
+ const cyan = s => `\x1b[36m${s}\x1b[0m`;
974
+ const bold = s => `\x1b[1m${s}\x1b[0m`;
975
+ const dim = s => `\x1b[2m${s}\x1b[0m`;
976
+
977
+ let cursorIdx = 0;
978
+ let selected = new Set(defaults);
979
+ let hasRendered = false;
980
+
981
+ function render() {
982
+ const lines = [];
983
+ lines.push(bold(message));
984
+ lines.push(dim(' ↑↓ navigate · Space toggle · Enter confirm · Esc cancel'));
985
+ lines.push('');
986
+
987
+ for (let i = 0; i < choices.length; i++) {
988
+ const choice = choices[i];
989
+ const isSelected = selected.has(choice);
990
+ const isCursor = i === cursorIdx;
991
+ const checkbox = isSelected ? '[✓]' : '[ ]';
992
+ const marker = isCursor ? cyan('▶') : ' ';
993
+ const label = isCursor ? bold(choice) : choice;
994
+ lines.push(` ${marker} ${checkbox} ${label}`);
995
+ }
996
+
997
+ if (hasRendered) {
998
+ process.stdout.write(restoreCursor + eraseBelow);
999
+ }
1000
+ process.stdout.write(saveCursor + lines.join('\n'));
1001
+ hasRendered = true;
1002
+ }
1003
+
1004
+ function cleanup() {
1005
+ if (hasRendered) {
1006
+ process.stdout.write(restoreCursor + eraseBelow);
1007
+ }
1008
+ process.stdin.setRawMode(false);
1009
+ process.stdin.removeAllListeners('data');
1010
+ process.stdout.write(show);
1011
+ }
1012
+
1013
+ process.stdin.setRawMode(true);
1014
+ process.stdin.resume();
1015
+ process.stdin.setEncoding('utf8');
1016
+ process.stdout.write(hide);
1017
+ render();
1018
+
1019
+ process.stdin.on('data', key => {
1020
+ // Ctrl-C / Ctrl-D → exit
1021
+ if (key === '\x03' || key === '\x04') {
1022
+ cleanup();
1023
+ process.exit(0);
1024
+ }
1025
+
1026
+ // Escape → cancel
1027
+ if (key === '\x1b') {
1028
+ cleanup();
1029
+ resolve([]);
1030
+ return;
1031
+ }
1032
+
1033
+ // Enter → confirm selection
1034
+ if (key === '\r' || key === '\n') {
1035
+ cleanup();
1036
+ resolve(Array.from(selected));
1037
+ return;
1038
+ }
1039
+
1040
+ // Arrow up
1041
+ if (key === '\x1b[A') {
1042
+ cursorIdx = (cursorIdx - 1 + choices.length) % choices.length;
1043
+ render();
1044
+ return;
1045
+ }
1046
+
1047
+ // Arrow down
1048
+ if (key === '\x1b[B') {
1049
+ cursorIdx = (cursorIdx + 1) % choices.length;
1050
+ render();
1051
+ return;
1052
+ }
1053
+
1054
+ // Spacebar → toggle selection
1055
+ if (key === ' ') {
1056
+ const choice = choices[cursorIdx];
1057
+ if (selected.has(choice)) {
1058
+ selected.delete(choice);
1059
+ } else {
1060
+ selected.add(choice);
1061
+ }
1062
+ render();
1063
+ return;
1064
+ }
1065
+ });
1066
+ });
1067
+ }
1068
+
1069
+ /**
1070
+ * Ask a yes/no question interactively.
1071
+ *
1072
+ * @param {string} question - The question to ask
1073
+ * @param {boolean} defaultYes - Default answer if user just presses Enter
1074
+ * @returns {Promise<boolean>} - true if yes, false if no
1075
+ */
1076
+ function askYesNo(question, defaultYes = true) {
1077
+ return new Promise(resolve => {
1078
+ const hide = '\x1b[?25l';
1079
+ const show = '\x1b[?25h';
1080
+ const saveCursor = '\x1b7';
1081
+ const restoreCursor = '\x1b8';
1082
+ const eraseBelow = '\x1b[J';
1083
+ const cyan = s => `\x1b[36m${s}\x1b[0m`;
1084
+ const bold = s => `\x1b[1m${s}\x1b[0m`;
1085
+ const dim = s => `\x1b[2m${s}\x1b[0m`;
1086
+
1087
+ const choices = ['Yes', 'No'];
1088
+ let cursorIdx = defaultYes ? 0 : 1;
1089
+ let hasRendered = false;
1090
+
1091
+ function render() {
1092
+ const lines = [];
1093
+ lines.push(bold(question));
1094
+ lines.push('');
1095
+
1096
+ for (let i = 0; i < choices.length; i++) {
1097
+ const choice = choices[i];
1098
+ const isCursor = i === cursorIdx;
1099
+ const marker = isCursor ? cyan('▶') : ' ';
1100
+ const label = isCursor ? bold(choice) : choice;
1101
+ lines.push(` ${marker} ${label}`);
1102
+ }
1103
+
1104
+ lines.push('');
1105
+ lines.push(dim(' ↑↓ navigate · Enter confirm · Esc cancel'));
1106
+
1107
+ if (hasRendered) {
1108
+ process.stdout.write(restoreCursor + eraseBelow);
1109
+ }
1110
+ process.stdout.write(saveCursor + lines.join('\n'));
1111
+ hasRendered = true;
1112
+ }
1113
+
1114
+ function cleanup() {
1115
+ if (hasRendered) {
1116
+ process.stdout.write(restoreCursor + eraseBelow);
1117
+ }
1118
+ process.stdin.setRawMode(false);
1119
+ process.stdin.removeAllListeners('data');
1120
+ process.stdout.write(show);
1121
+ }
1122
+
1123
+ process.stdin.setRawMode(true);
1124
+ process.stdin.resume();
1125
+ process.stdin.setEncoding('utf8');
1126
+ process.stdout.write(hide);
1127
+ render();
1128
+
1129
+ process.stdin.on('data', key => {
1130
+ // Ctrl-C / Ctrl-D → exit
1131
+ if (key === '\x03' || key === '\x04') {
1132
+ cleanup();
1133
+ process.exit(0);
1134
+ }
1135
+
1136
+ // Escape → cancel (default to No)
1137
+ if (key === '\x1b') {
1138
+ cleanup();
1139
+ resolve(false);
1140
+ return;
1141
+ }
1142
+
1143
+ // Enter → confirm selection
1144
+ if (key === '\r' || key === '\n') {
1145
+ cleanup();
1146
+ resolve(cursorIdx === 0);
1147
+ return;
1148
+ }
1149
+
1150
+ // Arrow up
1151
+ if (key === '\x1b[A') {
1152
+ cursorIdx = (cursorIdx - 1 + choices.length) % choices.length;
1153
+ render();
1154
+ return;
1155
+ }
1156
+
1157
+ // Arrow down
1158
+ if (key === '\x1b[B') {
1159
+ cursorIdx = (cursorIdx + 1) % choices.length;
1160
+ render();
1161
+ return;
1162
+ }
1163
+
1164
+ // y/Y → yes
1165
+ if (key === 'y' || key === 'Y') {
1166
+ cleanup();
1167
+ resolve(true);
1168
+ return;
1169
+ }
1170
+
1171
+ // n/N → no
1172
+ if (key === 'n' || key === 'N') {
1173
+ cleanup();
1174
+ resolve(false);
1175
+ return;
1176
+ }
1177
+ });
1178
+ });
1179
+ }
1180
+
1181
+ // ---------------------------------------------------------------------------
1182
+ // Agent permissions installation
1183
+ // ---------------------------------------------------------------------------
1184
+
1185
+ function getPlatformUrl() {
1186
+ // Check for OVERLORD_URL env var first, otherwise default to localhost
1187
+ return process.env.OVERLORD_URL || 'http://localhost:3000';
1188
+ }
1189
+
1190
+ function installAgentPermissions(agents, platformUrl) {
1191
+ console.log(`\nInstalling agent permissions for: ${agents.join(', ')}`);
1192
+ console.log(`Platform URL: ${platformUrl}\n`);
1193
+
1194
+ for (const agent of agents) {
1195
+ if (agent === 'claude') {
1196
+ installClaudePermissions(platformUrl);
1197
+ } else if (agent === 'opencode') {
1198
+ installOpenCodePermissions(platformUrl);
1199
+ } else if (agent === 'codex') {
1200
+ installCodexPermissions(platformUrl);
1201
+ }
1202
+ // cursor and gemini don't have permission configuration
1203
+ }
1204
+ }
1205
+
1206
+ function installClaudePermissions(platformUrl) {
1207
+ const settingsPath = path.join(process.cwd(), '.claude', 'settings.local.json');
1208
+ console.log(`--- Claude Code ---`);
1209
+ console.log(`Settings file: ${settingsPath}`);
1210
+
1211
+ let settings = { permissions: { allow: [] } };
1212
+ if (fs.existsSync(settingsPath)) {
1213
+ try {
1214
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
1215
+ } catch (e) {
1216
+ console.error(` ERROR: Could not parse ${settingsPath}: ${e.message}`);
1217
+ return false;
1218
+ }
1219
+ if (!settings.permissions) settings.permissions = {};
1220
+ if (!Array.isArray(settings.permissions.allow)) settings.permissions.allow = [];
1221
+ }
1222
+
1223
+ const PROTOCOL_ENDPOINTS = [
1224
+ 'attach', 'update', 'ask', 'read-context', 'write-context', 'deliver',
1225
+ 'create-ticket', 'list-tickets', 'record-change-rationales', 'spawn',
1226
+ 'discover-project', 'load-context', 'artifact-upload-file', 'artifact-download-url'
1227
+ ];
1228
+
1229
+ const entries = [];
1230
+ for (const endpoint of PROTOCOL_ENDPOINTS) {
1231
+ entries.push(`Bash(curl -s -X POST "${platformUrl}/api/protocol/${endpoint}":*)`);
1232
+ }
1233
+ entries.push(`Bash(curl -s -H 'Authorization::*)`);
1234
+ for (const endpoint of PROTOCOL_ENDPOINTS) {
1235
+ entries.push(`Bash(curl -s -X POST "$OVERLORD_URL/api/protocol/${endpoint}":*)`);
1236
+ }
1237
+ entries.push(`Bash(curl -s -H "Authorization::*)`);
1238
+
1239
+ const existing = new Set(settings.permissions.allow);
1240
+ const toAdd = entries.filter((e) => !existing.has(e));
1241
+
1242
+ if (toAdd.length === 0) {
1243
+ console.log(' All required permissions already present. Nothing to do.\n');
1244
+ return true;
1245
+ }
1246
+
1247
+ console.log(` Adding ${toAdd.length} permission entries:`);
1248
+ for (const entry of toAdd) {
1249
+ console.log(` + ${entry}`);
1250
+ }
1251
+
1252
+ // Backup
1253
+ if (fs.existsSync(settingsPath)) {
1254
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
1255
+ const backupPath = `${settingsPath}.backup-${ts}`;
1256
+ fs.copyFileSync(settingsPath, backupPath);
1257
+ console.log(` Backup: ${backupPath}`);
1258
+ }
1259
+
1260
+ settings.permissions.allow = [...settings.permissions.allow, ...toAdd];
1261
+
1262
+ const dir = path.dirname(settingsPath);
1263
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1264
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
1265
+ console.log(' Settings updated.\n');
1266
+ return true;
1267
+ }
1268
+
1269
+ function installOpenCodePermissions(_platformUrl) {
1270
+ console.log(`--- OpenCode ---`);
1271
+ const configPath = path.join(os.homedir(), '.config', 'opencode', 'opencode.json');
1272
+ console.log(`Config file: ${configPath}`);
1273
+
1274
+ let config = {};
1275
+ if (fs.existsSync(configPath)) {
1276
+ try {
1277
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1278
+ } catch (e) {
1279
+ console.error(` ERROR: Could not parse ${configPath}: ${e.message}`);
1280
+ return false;
1281
+ }
1282
+ }
1283
+
1284
+ const existingPermission =
1285
+ config.permission && typeof config.permission === 'object' ? config.permission : {};
1286
+ const existingBash =
1287
+ existingPermission.bash && typeof existingPermission.bash === 'object'
1288
+ ? existingPermission.bash
1289
+ : {};
1290
+
1291
+ const next = {
1292
+ ...config,
1293
+ $schema: 'https://opencode.ai/config.json',
1294
+ permission: {
1295
+ ...existingPermission,
1296
+ bash: {
1297
+ '*': 'ask',
1298
+ ...existingBash,
1299
+ 'ovld protocol *': 'allow',
1300
+ 'curl -sS -X POST *': 'allow',
1301
+ 'curl -s -X POST *': 'allow'
1302
+ }
1303
+ }
1304
+ };
1305
+
1306
+ if (fs.existsSync(configPath)) {
1307
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
1308
+ const backupPath = `${configPath}.backup-${ts}`;
1309
+ fs.copyFileSync(configPath, backupPath);
1310
+ console.log(` Backup: ${backupPath}`);
1311
+ }
1312
+
1313
+ const dir = path.dirname(configPath);
1314
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1315
+ fs.writeFileSync(configPath, JSON.stringify(next, null, 2) + '\n');
1316
+ console.log(' Config updated.\n');
1317
+ return true;
1318
+ }
1319
+
1320
+ function installCodexPermissions(platformUrl) {
1321
+ console.log(`--- Codex ---`);
1322
+ console.log(' Codex does not support file-based permission configuration.');
1323
+ console.log(' To warm up permissions, run the following commands once inside a Codex session:');
1324
+ console.log(' (Codex will prompt for approval; approve each one to persist the prefix.)\n');
1325
+
1326
+ const PROTOCOL_ENDPOINTS = [
1327
+ 'attach', 'update', 'ask', 'read-context', 'write-context', 'deliver',
1328
+ 'create-ticket', 'list-tickets'
1329
+ ];
1330
+
1331
+ for (const endpoint of PROTOCOL_ENDPOINTS) {
1332
+ console.log(` curl -s -X POST "${platformUrl}/api/protocol/${endpoint}" -H "Content-Type: application/json" -H "Authorization: Bearer \\$AGENT_TOKEN" -d '{}'`);
1333
+ }
1334
+ console.log(` curl -s -H "Authorization: Bearer \\$AGENT_TOKEN" "${platformUrl}/api/protocol/context/test"`);
1335
+ console.log();
1336
+ return true;
1337
+ }
1338
+
953
1339
  // ---------------------------------------------------------------------------
954
1340
  // Public API
955
1341
  // ---------------------------------------------------------------------------
@@ -959,18 +1345,103 @@ export async function runSetupCommand(args) {
959
1345
 
960
1346
  if (agent === '--help' || agent === '-h' || agent === 'help') {
961
1347
  console.log(`Usage:
1348
+ ovld setup Interactive setup (select agents and configure permissions)
962
1349
  ovld setup claude Install Overlord bundle for Claude Code
963
1350
  ovld setup codex Install Overlord Codex plugin bundle
964
1351
  ovld setup cursor Install Overlord rules, slash commands, and permissions for Cursor
965
1352
  ovld setup gemini Install Overlord slash commands and policy rules for Gemini CLI
966
1353
  ovld setup opencode Install Overlord connector for OpenCode
967
1354
  ovld setup all Install for all supported agents
968
- ovld doctor Validate installed connectors`);
1355
+ ovld doctor Validate installed connectors and check for CLI updates`);
1356
+ return;
1357
+ }
1358
+
1359
+ // Interactive mode when called without arguments
1360
+ if (!agent) {
1361
+ console.log('Welcome to Overlord agent setup!\n');
1362
+
1363
+ // Step 1: Select agents to install
1364
+ const agentLabels = supportedAgents.map(a => {
1365
+ const descriptions = {
1366
+ claude: 'Claude Code',
1367
+ codex: 'Codex',
1368
+ cursor: 'Cursor',
1369
+ gemini: 'Gemini CLI',
1370
+ opencode: 'OpenCode'
1371
+ };
1372
+ return `${a.padEnd(10)} - ${descriptions[a] || a}`;
1373
+ });
1374
+
1375
+ const selectedLabels = await runCheckboxPrompt({
1376
+ message: 'Select agent connectors to install (Space to toggle, Enter to confirm):',
1377
+ choices: agentLabels,
1378
+ defaults: []
1379
+ });
1380
+
1381
+ if (selectedLabels.length === 0) {
1382
+ console.log('\nNo agents selected. Setup cancelled.');
1383
+ return;
1384
+ }
1385
+
1386
+ // Extract agent names from selected labels
1387
+ const selectedAgents = selectedLabels.map(label => label.split('-')[0].trim());
1388
+
1389
+ // Step 2: Install selected agents
1390
+ console.log(`\nInstalling Overlord agent bundles for: ${selectedAgents.join(', ')}...\n`);
1391
+
1392
+ const installedAgents = [];
1393
+ for (const a of selectedAgents) {
1394
+ console.log(`[${a}]`);
1395
+ try {
1396
+ if (a === 'claude') installClaude();
1397
+ else if (a === 'codex') installCodex();
1398
+ else if (a === 'cursor') installCursor();
1399
+ else if (a === 'gemini') installGemini();
1400
+ else installOpenCode();
1401
+ installedAgents.push(a);
1402
+ } catch (err) {
1403
+ console.error(` ✗ Failed: ${err.message}`);
1404
+ }
1405
+ console.log();
1406
+ }
1407
+
1408
+ if (installedAgents.length === 0) {
1409
+ console.log('No agents were successfully installed.');
1410
+ return;
1411
+ }
1412
+
1413
+ // Step 3: Offer to configure agent permissions
1414
+ const agentsThatNeedPermissions = installedAgents.filter(a =>
1415
+ ['claude', 'codex', 'opencode'].includes(a)
1416
+ );
1417
+
1418
+ if (agentsThatNeedPermissions.length > 0) {
1419
+ console.log('Agent connectors installed successfully!\n');
1420
+
1421
+ const shouldInstallPermissions = await askYesNo(
1422
+ 'Would you like to configure agent permissions for Overlord protocol access?',
1423
+ true
1424
+ );
1425
+
1426
+ if (shouldInstallPermissions) {
1427
+ const platformUrl = getPlatformUrl();
1428
+ installAgentPermissions(agentsThatNeedPermissions, platformUrl);
1429
+ console.log('✓ Agent permissions configured.\n');
1430
+ } else {
1431
+ console.log('\nSkipped agent permissions configuration.');
1432
+ console.log('You can run the permission installer later with:');
1433
+ console.log(' node scripts/install-agent-permissions.mjs\n');
1434
+ }
1435
+ }
1436
+
1437
+ console.log('Setup complete! Run `ovld doctor` to verify your installation.');
969
1438
  return;
970
1439
  }
971
1440
 
972
1441
  if (agent === 'all') {
973
1442
  console.log('Installing Overlord agent bundle for all supported agents...\n');
1443
+ const installedAgents = [];
1444
+
974
1445
  for (const a of supportedAgents) {
975
1446
  console.log(`[${a}]`);
976
1447
  try {
@@ -979,11 +1450,30 @@ export async function runSetupCommand(args) {
979
1450
  else if (a === 'cursor') installCursor();
980
1451
  else if (a === 'gemini') installGemini();
981
1452
  else installOpenCode();
1453
+ installedAgents.push(a);
982
1454
  } catch (err) {
983
1455
  console.error(` ✗ Failed: ${err.message}`);
984
1456
  }
985
1457
  console.log();
986
1458
  }
1459
+
1460
+ // Offer permissions setup for 'all' command too
1461
+ const agentsThatNeedPermissions = installedAgents.filter(a =>
1462
+ ['claude', 'codex', 'opencode'].includes(a)
1463
+ );
1464
+
1465
+ if (agentsThatNeedPermissions.length > 0) {
1466
+ const shouldInstallPermissions = await askYesNo(
1467
+ '\nWould you like to configure agent permissions for Overlord protocol access?',
1468
+ true
1469
+ );
1470
+
1471
+ if (shouldInstallPermissions) {
1472
+ const platformUrl = getPlatformUrl();
1473
+ installAgentPermissions(agentsThatNeedPermissions, platformUrl);
1474
+ }
1475
+ }
1476
+
987
1477
  console.log('Done.');
988
1478
  return;
989
1479
  }
@@ -1003,6 +1493,19 @@ export async function runSetupCommand(args) {
1003
1493
  else if (agent === 'gemini') installGemini();
1004
1494
  else installOpenCode();
1005
1495
  console.log('\nDone.');
1496
+
1497
+ // Offer permissions setup for single agent install too
1498
+ if (['claude', 'codex', 'opencode'].includes(agent)) {
1499
+ const shouldInstallPermissions = await askYesNo(
1500
+ '\nWould you like to configure agent permissions for Overlord protocol access?',
1501
+ true
1502
+ );
1503
+
1504
+ if (shouldInstallPermissions) {
1505
+ const platformUrl = getPlatformUrl();
1506
+ installAgentPermissions([agent], platformUrl);
1507
+ }
1508
+ }
1006
1509
  } catch (err) {
1007
1510
  console.error(`\nFailed: ${err.message}`);
1008
1511
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "3.22.0",
3
+ "version": "3.24.0",
4
4
  "description": "Overlord CLI — launch AI agents on tickets from anywhere",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,9 @@
14
14
  "bin/",
15
15
  "plugins/"
16
16
  ],
17
+ "scripts": {
18
+ "postinstall": "node bin/_cli/postinstall.mjs || true"
19
+ },
17
20
  "engines": {
18
21
  "node": ">=20"
19
22
  },
@@ -7,6 +7,27 @@ const execFileAsync = promisify(execFile);
7
7
  const OVLD_BIN = process.env.OVLD_BIN?.trim() || 'ovld';
8
8
  const PROTOCOL_VERSION = '2025-06-18';
9
9
 
10
+ function execFileWithOptionalInput(file, args, options, input) {
11
+ if (input === undefined) {
12
+ return execFileAsync(file, args, options);
13
+ }
14
+
15
+ return new Promise((resolve, reject) => {
16
+ const child = execFile(file, args, options, (error, stdout, stderr) => {
17
+ if (error) {
18
+ error.stdout = stdout;
19
+ error.stderr = stderr;
20
+ reject(error);
21
+ return;
22
+ }
23
+
24
+ resolve({ stdout, stderr });
25
+ });
26
+
27
+ child.stdin?.end(input);
28
+ });
29
+ }
30
+
10
31
  const tools = [
11
32
  {
12
33
  name: 'discover_project',
@@ -239,7 +260,8 @@ const tools = [
239
260
  },
240
261
  {
241
262
  name: 'deliver_ticket',
242
- description: 'Deliver final work back into Overlord with summary, artifacts, and change rationales.',
263
+ description:
264
+ 'Deliver final work back into Overlord with summary, artifacts, and change rationales. Large payloads are streamed to the CLI through stdin, so this tool does not create delivery scratch files.',
243
265
  inputSchema: {
244
266
  type: 'object',
245
267
  properties: {
@@ -255,11 +277,14 @@ const tools = [
255
277
  toCliFlags: args => ({
256
278
  'session-key': args.session_key,
257
279
  'ticket-id': args.ticket_id,
258
- summary: args.summary,
259
- 'artifacts-json': args.artifacts,
260
- 'change-rationales-json': args.change_rationales,
280
+ 'payload-file': '-',
261
281
  'skip-file-change-check': args.skip_file_change_check
262
282
  }),
283
+ toCliStdin: args => JSON.stringify({
284
+ summary: args.summary,
285
+ ...(Array.isArray(args.artifacts) ? { artifacts: args.artifacts } : {}),
286
+ ...(Array.isArray(args.change_rationales) ? { changeRationales: args.change_rationales } : {})
287
+ }),
263
288
  subcommand: 'deliver'
264
289
  },
265
290
  {
@@ -490,17 +515,24 @@ function cliArgsFromFlags(flags) {
490
515
  }
491
516
 
492
517
  async function runProtocol(tool, args) {
493
- const cliArgs = ['protocol', tool.subcommand, ...cliArgsFromFlags(tool.toCliFlags(args ?? {}))];
518
+ const toolArgs = args ?? {};
519
+ const cliArgs = ['protocol', tool.subcommand, ...cliArgsFromFlags(tool.toCliFlags(toolArgs))];
520
+ const stdin = typeof tool.toCliStdin === 'function' ? tool.toCliStdin(toolArgs) : undefined;
494
521
 
495
522
  try {
496
- const { stdout, stderr } = await execFileAsync(OVLD_BIN, cliArgs, {
497
- cwd: process.cwd(),
498
- env: {
499
- ...process.env,
500
- AGENT_IDENTIFIER: process.env.AGENT_IDENTIFIER ?? 'codex-overlord-plugin'
523
+ const { stdout, stderr } = await execFileWithOptionalInput(
524
+ OVLD_BIN,
525
+ cliArgs,
526
+ {
527
+ cwd: process.cwd(),
528
+ env: {
529
+ ...process.env,
530
+ AGENT_IDENTIFIER: process.env.AGENT_IDENTIFIER ?? 'codex-overlord-plugin'
531
+ },
532
+ maxBuffer: 20 * 1024 * 1024
501
533
  },
502
- maxBuffer: 20 * 1024 * 1024
503
- });
534
+ stdin
535
+ );
504
536
 
505
537
  const trimmed = stdout.trim();
506
538
  const data = trimmed ? JSON.parse(trimmed) : {};
@@ -15,7 +15,7 @@ Overlord plugin.
15
15
  and stop.
16
16
  6. Deliver last with `ovld protocol deliver`, including meaningful `changeRationales` for every
17
17
  behavioral git-tracked change.
18
- If you need `--payload-file`, `--artifacts-file`, or `--change-rationales-file`, treat that JSON as ephemeral scratch data, not as a repository file. Remove it after delivery and never commit it.
18
+ For larger delivery JSON, prefer `--payload-file -` and stream the full payload on stdin so no scratch file needs to be created or removed. If you need `--payload-file`, `--artifacts-file`, or `--change-rationales-file` with a real path, treat that JSON as ephemeral scratch data, not as a repository file. Remove it after delivery and never commit it.
19
19
 
20
20
  ## Rules
21
21