overlord-cli 3.21.0 → 3.23.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
@@ -13,6 +13,7 @@ npm install -g overlord-cli
13
13
  ```
14
14
 
15
15
  Use Node.js 20 or newer for every CLI install or update.
16
+ Run `ovld update` any time you want to refresh the global npm install to the latest release.
16
17
 
17
18
  ## Usage
18
19
 
@@ -31,6 +32,10 @@ ovld auth login
31
32
  ovld attach
32
33
  ovld create "Investigate the failing build"
33
34
  ovld prompt "Draft a fix for the onboarding flow"
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
34
39
  ovld setup codex
35
40
  ovld setup cursor
36
41
  ovld setup gemini
@@ -51,9 +56,11 @@ ovld doctor
51
56
  - `auth` - log in, log out, or check auth status
52
57
  - `tickets` - list or create tickets
53
58
  - `ticket` - work with a single ticket
54
- - `protocol` - run ticket lifecycle commands
55
- - `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`
56
62
  - `setup` - install the Overlord connector or plugin bundle for a supported agent
63
+ - `update` - install the latest CLI release from npm
57
64
  - `doctor` - verify installed agent connectors and check whether a newer CLI version is available
58
65
 
59
66
  ## License
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from 'node:child_process';
4
+ import { createRequire } from 'node:module';
5
+
6
+ const require = createRequire(import.meta.url);
7
+ const cliPackage = require('../../package.json');
8
+
9
+ const CURRENT_CLI_VERSION = typeof cliPackage.version === 'string' ? cliPackage.version : '0.0.0';
10
+ const CLI_PACKAGE_NAME =
11
+ typeof cliPackage.name === 'string' && cliPackage.name ? cliPackage.name : 'overlord-cli';
12
+
13
+ const ORANGE = '\x1b[38;5;208m';
14
+ const RESET = '\x1b[0m';
15
+
16
+ function colorizeOrange(text) {
17
+ return `${ORANGE}${text}${RESET}`;
18
+ }
19
+
20
+ export function getCurrentCliVersion() {
21
+ return CURRENT_CLI_VERSION;
22
+ }
23
+
24
+ export function getCliPackageName() {
25
+ return CLI_PACKAGE_NAME;
26
+ }
27
+
28
+ export async function fetchLatestCliVersion({
29
+ fetchImpl = fetch,
30
+ packageName = CLI_PACKAGE_NAME,
31
+ timeoutMs = 2500
32
+ } = {}) {
33
+ const controller = new AbortController();
34
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
35
+
36
+ try {
37
+ const response = await fetchImpl(`https://registry.npmjs.org/${packageName}/latest`, {
38
+ signal: controller.signal,
39
+ headers: { Accept: 'application/json' }
40
+ });
41
+
42
+ if (!response.ok) return null;
43
+
44
+ const payload = await response.json();
45
+ return typeof payload?.version === 'string' ? payload.version : null;
46
+ } catch {
47
+ return null;
48
+ } finally {
49
+ clearTimeout(timeout);
50
+ }
51
+ }
52
+
53
+ export async function checkForCliUpdate(options = {}) {
54
+ const currentVersion = options.currentVersion ?? CURRENT_CLI_VERSION;
55
+ const latestVersion = await fetchLatestCliVersion(options);
56
+ if (!latestVersion || latestVersion === currentVersion) return null;
57
+ return latestVersion;
58
+ }
59
+
60
+ export function formatCliUpdateNotice(latestVersion, { currentVersion = CURRENT_CLI_VERSION } = {}) {
61
+ return `New Overlord CLI version available: v${latestVersion} (installed v${currentVersion}). Run \`ovld update\` to update via npm.`;
62
+ }
63
+
64
+ export function printCliUpdateNotice(
65
+ latestVersion,
66
+ { currentVersion = CURRENT_CLI_VERSION, stream = process.stderr } = {}
67
+ ) {
68
+ if (!latestVersion) return false;
69
+ stream.write(`${colorizeOrange(formatCliUpdateNotice(latestVersion, { currentVersion }))}\n`);
70
+ return true;
71
+ }
72
+
73
+ export async function runCliUpdateCommand({
74
+ currentVersion = CURRENT_CLI_VERSION,
75
+ fetchLatestVersionFn = fetchLatestCliVersion,
76
+ logger = console,
77
+ npmCommand = 'npm',
78
+ packageName = CLI_PACKAGE_NAME,
79
+ spawnSyncImpl = spawnSync
80
+ } = {}) {
81
+ const latestVersion = await fetchLatestVersionFn({ currentVersion, packageName });
82
+
83
+ if (latestVersion && latestVersion === currentVersion) {
84
+ logger.log(`Overlord CLI ${currentVersion} is already the latest version.`);
85
+ return { alreadyLatest: true, currentVersion, latestVersion };
86
+ }
87
+
88
+ const target = `${packageName}@latest`;
89
+ if (latestVersion) {
90
+ logger.log(`Updating Overlord CLI ${currentVersion} -> ${latestVersion} via npm...`);
91
+ } else {
92
+ logger.log(`Updating Overlord CLI via npm...`);
93
+ }
94
+
95
+ const result = spawnSyncImpl(npmCommand, ['install', '-g', target], {
96
+ env: process.env,
97
+ stdio: 'inherit'
98
+ });
99
+
100
+ if (result.error) {
101
+ throw result.error;
102
+ }
103
+
104
+ if (typeof result.status === 'number' && result.status !== 0) {
105
+ throw new Error(`\`${npmCommand} install -g ${target}\` exited with status ${result.status}.`);
106
+ }
107
+
108
+ if (typeof result.signal === 'string') {
109
+ throw new Error(`\`${npmCommand} install -g ${target}\` was terminated by ${result.signal}.`);
110
+ }
111
+
112
+ if (latestVersion) {
113
+ logger.log(`Overlord CLI updated to v${latestVersion}.`);
114
+ } else {
115
+ logger.log('Overlord CLI update complete. Run `ovld version` to confirm the installed version.');
116
+ }
117
+
118
+ return { alreadyLatest: false, currentVersion, latestVersion, result };
119
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { runAttachCommand } from './attach.mjs';
4
4
  import { runAuthCommand } from './auth.mjs';
5
+ import { checkForCliUpdate, printCliUpdateNotice, runCliUpdateCommand } from './cli-update.mjs';
5
6
  import { runLauncherCommand } from './launcher.mjs';
6
7
  import { runProtocolCommand } from './protocol.mjs';
7
8
  import { runDoctorCommand, runSetupCommand } from './setup.mjs';
@@ -37,7 +38,8 @@ Usage:
37
38
  ${primaryCommand} connect <agent> Launch an agent on a ticket
38
39
  ${primaryCommand} restart <agent> Resume an agent session
39
40
  ${primaryCommand} context Print ticket context (requires TICKET_ID)
40
- ${primaryCommand} setup <agent|all> Install Overlord agent connector
41
+ ${primaryCommand} setup [agent|all] Install Overlord agent connector (interactive if no args)
42
+ ${primaryCommand} update Install the latest CLI version from npm
41
43
  ${primaryCommand} doctor Validate installed agent connectors and check for CLI updates
42
44
  ${primaryCommand} version Show the installed CLI version
43
45
  ${primaryCommand} help Show this help message
@@ -66,6 +68,15 @@ Run a subcommand with --help for more detail.
66
68
  export async function runCli({ primaryCommand }) {
67
69
  assertSupportedNodeVersion();
68
70
  const [command, ...rest] = process.argv.slice(2);
71
+ const shouldCheckForUpdate =
72
+ Boolean(process.stdout.isTTY || process.stderr.isTTY) &&
73
+ command !== 'doctor' &&
74
+ command !== 'update';
75
+ const latestCliVersion = shouldCheckForUpdate ? await checkForCliUpdate() : null;
76
+
77
+ if (latestCliVersion && command !== 'doctor' && command !== 'update') {
78
+ printCliUpdateNotice(latestCliVersion);
79
+ }
69
80
 
70
81
  if (!command || command === 'help' || command === '--help' || command === '-h') {
71
82
  printHelp(primaryCommand);
@@ -119,8 +130,18 @@ export async function runCli({ primaryCommand }) {
119
130
  return;
120
131
  }
121
132
 
133
+ if (command === 'update') {
134
+ if (rest[0] === '--help' || rest[0] === '-h' || rest[0] === 'help') {
135
+ console.log(`Usage:
136
+ ${primaryCommand} update Install the latest CLI version from npm`);
137
+ return;
138
+ }
139
+ await runCliUpdateCommand();
140
+ return;
141
+ }
142
+
122
143
  if (command === 'doctor') {
123
- await runDoctorCommand();
144
+ await runDoctorCommand({ latestCliVersion });
124
145
  return;
125
146
  }
126
147
 
@@ -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
+ `);
@@ -12,6 +12,7 @@ import fs from 'node:fs';
12
12
  import os from 'node:os';
13
13
  import path from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
+ import { checkForCliUpdate, getCurrentCliVersion, printCliUpdateNotice } from './cli-update.mjs';
15
16
 
16
17
  const BUNDLE_VERSION = '1.8.0';
17
18
  const MD_MARKER_START = '<!-- overlord:managed:start -->';
@@ -280,11 +281,6 @@ function readJsonFileOrNull(filePath) {
280
281
  }
281
282
  }
282
283
 
283
- function localCliVersion() {
284
- const cliPackage = readJsonFileOrNull(path.resolve(__dirname, '..', '..', 'package.json'));
285
- return typeof cliPackage?.version === 'string' ? cliPackage.version : null;
286
- }
287
-
288
284
  function writeJsonFile(filePath, data) {
289
285
  const dir = path.dirname(filePath);
290
286
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
@@ -483,28 +479,6 @@ function currentContentHashForAgent(agent) {
483
479
  );
484
480
  }
485
481
 
486
- async function checkForCliUpdate() {
487
- const currentVersion = localCliVersion();
488
- if (!currentVersion) return null;
489
- const controller = new AbortController();
490
- const timeout = setTimeout(() => controller.abort(), 2500);
491
- try {
492
- const response = await fetch('https://registry.npmjs.org/overlord-cli/latest', {
493
- signal: controller.signal,
494
- headers: { Accept: 'application/json' }
495
- });
496
- if (!response.ok) return null;
497
- const payload = await response.json();
498
- const latestVersion = typeof payload?.version === 'string' ? payload.version : null;
499
- if (!latestVersion) return null;
500
- return latestVersion === currentVersion ? null : latestVersion;
501
- } catch {
502
- return null;
503
- } finally {
504
- clearTimeout(timeout);
505
- }
506
- }
507
-
508
482
  function codexSourcePluginDir() {
509
483
  if (fs.existsSync(PACKAGE_PLUGIN_DIR)) return PACKAGE_PLUGIN_DIR;
510
484
  if (fs.existsSync(REPO_PLUGIN_DIR)) return REPO_PLUGIN_DIR;
@@ -976,6 +950,392 @@ function currentNodeMajor() {
976
950
  return Number.parseInt(process.versions.node.split('.')[0] ?? '', 10);
977
951
  }
978
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
+
979
1339
  // ---------------------------------------------------------------------------
980
1340
  // Public API
981
1341
  // ---------------------------------------------------------------------------
@@ -985,18 +1345,103 @@ export async function runSetupCommand(args) {
985
1345
 
986
1346
  if (agent === '--help' || agent === '-h' || agent === 'help') {
987
1347
  console.log(`Usage:
1348
+ ovld setup Interactive setup (select agents and configure permissions)
988
1349
  ovld setup claude Install Overlord bundle for Claude Code
989
1350
  ovld setup codex Install Overlord Codex plugin bundle
990
1351
  ovld setup cursor Install Overlord rules, slash commands, and permissions for Cursor
991
1352
  ovld setup gemini Install Overlord slash commands and policy rules for Gemini CLI
992
1353
  ovld setup opencode Install Overlord connector for OpenCode
993
1354
  ovld setup all Install for all supported agents
994
- 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.');
995
1438
  return;
996
1439
  }
997
1440
 
998
1441
  if (agent === 'all') {
999
1442
  console.log('Installing Overlord agent bundle for all supported agents...\n');
1443
+ const installedAgents = [];
1444
+
1000
1445
  for (const a of supportedAgents) {
1001
1446
  console.log(`[${a}]`);
1002
1447
  try {
@@ -1005,11 +1450,30 @@ export async function runSetupCommand(args) {
1005
1450
  else if (a === 'cursor') installCursor();
1006
1451
  else if (a === 'gemini') installGemini();
1007
1452
  else installOpenCode();
1453
+ installedAgents.push(a);
1008
1454
  } catch (err) {
1009
1455
  console.error(` ✗ Failed: ${err.message}`);
1010
1456
  }
1011
1457
  console.log();
1012
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
+
1013
1477
  console.log('Done.');
1014
1478
  return;
1015
1479
  }
@@ -1029,13 +1493,26 @@ export async function runSetupCommand(args) {
1029
1493
  else if (agent === 'gemini') installGemini();
1030
1494
  else installOpenCode();
1031
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
+ }
1032
1509
  } catch (err) {
1033
1510
  console.error(`\nFailed: ${err.message}`);
1034
1511
  process.exit(1);
1035
1512
  }
1036
1513
  }
1037
1514
 
1038
- export async function runDoctorCommand() {
1515
+ export async function runDoctorCommand({ latestCliVersion = null } = {}) {
1039
1516
  console.log('Overlord agent bundle status:\n');
1040
1517
  let allOk = true;
1041
1518
  const nodeMajor = currentNodeMajor();
@@ -1051,18 +1528,15 @@ export async function runDoctorCommand() {
1051
1528
  for (const agent of supportedAgents) {
1052
1529
  if (!doctorAgent(agent)) allOk = false;
1053
1530
  }
1054
- const latestCliVersion = await checkForCliUpdate();
1531
+ const updateVersion = latestCliVersion ?? (await checkForCliUpdate());
1055
1532
  console.log();
1056
1533
  if (allOk) {
1057
1534
  console.log('All bundles are up to date.');
1058
1535
  } else {
1059
1536
  console.log('Run `ovld setup <agent>` or `ovld setup all` to install/repair.');
1060
1537
  }
1061
- if (latestCliVersion) {
1538
+ if (updateVersion) {
1062
1539
  console.log();
1063
- console.log(`CLI update available: ${latestCliVersion}`);
1064
- console.log(
1065
- `Update with Node.js ${REQUIRED_NODE_MAJOR}+ and reinstall the installed CLI wrapper if needed.`
1066
- );
1540
+ printCliUpdateNotice(updateVersion, { currentVersion: getCurrentCliVersion(), stream: process.stdout });
1067
1541
  }
1068
1542
  }
@@ -1,10 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { createRequire } from 'node:module';
4
-
5
- const require = createRequire(import.meta.url);
6
- const { version } = require('../../package.json');
3
+ import { getCurrentCliVersion } from './cli-update.mjs';
7
4
 
8
5
  export function runVersionCommand() {
9
- console.log(`Overlord CLI ${version}`);
6
+ console.log(`Overlord CLI ${getCurrentCliVersion()}`);
10
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "3.21.0",
3
+ "version": "3.23.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
  },