neoagent 2.3.1-beta.93 → 2.3.1-beta.94

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/lib/manager.js CHANGED
@@ -119,28 +119,45 @@ function readEnvFileRaw() {
119
119
  return fs.readFileSync(ENV_FILE, 'utf8');
120
120
  }
121
121
 
122
+ function sanitizeEnvKey(key) {
123
+ return String(key).replace(/[\r\n]/g, '');
124
+ }
125
+
126
+ function sanitizeEnvValue(value) {
127
+ return String(value).replace(/[\r\n]/g, '');
128
+ }
129
+
130
+ function validateEnvKey(key) {
131
+ if (!/^[A-Z][A-Z0-9_]*$/.test(key)) {
132
+ throw new Error(`Invalid env key "${key}". Keys must be uppercase letters, digits, and underscores (e.g. PORT, ANTHROPIC_API_KEY).`);
133
+ }
134
+ }
135
+
122
136
  function upsertEnvValue(key, value) {
137
+ const safeKey = sanitizeEnvKey(key);
138
+ const safeValue = sanitizeEnvValue(value);
123
139
  const raw = readEnvFileRaw();
124
140
  const lines = raw ? raw.split('\n') : [];
125
141
  let replaced = false;
126
142
 
127
143
  for (let i = 0; i < lines.length; i++) {
128
- if (lines[i].startsWith(`${key}=`)) {
129
- lines[i] = `${key}=${value}`;
144
+ if (lines[i].startsWith(`${safeKey}=`)) {
145
+ lines[i] = `${safeKey}=${safeValue}`;
130
146
  replaced = true;
131
147
  break;
132
148
  }
133
149
  }
134
150
 
135
- if (!replaced) lines.push(`${key}=${value}`);
151
+ if (!replaced) lines.push(`${safeKey}=${safeValue}`);
136
152
  const output = lines.filter((_, idx, arr) => idx !== arr.length - 1 || arr[idx] !== '').join('\n') + '\n';
137
153
  fs.writeFileSync(ENV_FILE, output, { mode: 0o600 });
138
154
  }
139
155
 
140
156
  function removeEnvValue(key) {
157
+ const safeKey = sanitizeEnvKey(key);
141
158
  const raw = readEnvFileRaw();
142
159
  if (!raw) return false;
143
- const lines = raw.split('\n').filter((line) => !line.startsWith(`${key}=`));
160
+ const lines = raw.split('\n').filter((line) => !line.startsWith(`${safeKey}=`));
144
161
  const output = lines.filter((_, idx, arr) => idx !== arr.length - 1 || arr[idx] !== '').join('\n') + '\n';
145
162
  fs.writeFileSync(ENV_FILE, output, { mode: 0o600 });
146
163
  return true;
@@ -381,15 +398,19 @@ function listNeoAgentServerProcesses() {
381
398
  };
382
399
  })
383
400
  .filter(Boolean)
384
- .filter((entry) =>
385
- entry.pid !== process.pid &&
386
- /(^|\s)node(\s|$)/.test(entry.command) &&
387
- (
388
- appIndexPattern.test(String(entry.command || '').replace(/\\/g, '/')) ||
389
- genericNeoAgentPattern.test(String(entry.command || '').replace(/\\/g, '/')) ||
390
- repoNamePattern.test(String(entry.command || ''))
391
- )
392
- );
401
+ .filter((entry) => {
402
+ if (entry.pid === process.pid) return false;
403
+ const cmd = String(entry.command || '');
404
+ const cmdNormalized = cmd.replace(/\\/g, '/');
405
+ const executablePart = cmd.split(/\s+/)[0] || '';
406
+ const executableBase = path.basename(executablePart);
407
+ const isNode = /^node\d*$/.test(executableBase) || /(^|\s)node\d*(\s|$)/.test(cmd);
408
+ return isNode && (
409
+ appIndexPattern.test(cmdNormalized) ||
410
+ genericNeoAgentPattern.test(cmdNormalized) ||
411
+ repoNamePattern.test(cmd)
412
+ );
413
+ });
393
414
  }
394
415
 
395
416
  function killNeoAgentServerProcesses() {
@@ -464,7 +485,12 @@ async function cmdSetup() {
464
485
  logInfo('Press Enter to keep the current value shown in brackets.');
465
486
 
466
487
  heading('Core');
467
- const port = await ask('Server port', current.PORT || '3333');
488
+ const portRaw = await ask('Server port', current.PORT || '3333');
489
+ const portNum = Number(portRaw);
490
+ if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
491
+ throw new Error(`Invalid port "${portRaw}". Must be an integer between 1 and 65535.`);
492
+ }
493
+ const port = String(portNum);
468
494
  const publicUrl = await ask('Public base URL', current.PUBLIC_URL || '');
469
495
  const secureCookiesDefault = current.SECURE_COOKIES ||
470
496
  (String(publicUrl || '').trim().startsWith('https://') ? 'true' : 'false');
@@ -735,6 +761,31 @@ async function cmdMigrate(args = []) {
735
761
  });
736
762
  }
737
763
 
764
+ async function pollDeviceCode({ pollUrl, pollBody, pollHeaders = {}, intervalMs, timeoutMs, onToken }) {
765
+ const start = Date.now();
766
+ let currentInterval = intervalMs;
767
+ while (Date.now() - start < timeoutMs) {
768
+ await new Promise((r) => setTimeout(r, currentInterval));
769
+ const res = await fetch(pollUrl, {
770
+ method: 'POST',
771
+ headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', ...pollHeaders },
772
+ body: JSON.stringify(pollBody()),
773
+ });
774
+ if (res.status === 403 || res.status === 404) continue;
775
+ if (!res.ok) {
776
+ const text = await res.text().catch(() => 'Unknown error');
777
+ throw new Error(`Token poll failed: HTTP ${res.status} — ${text}`);
778
+ }
779
+ const data = await res.json();
780
+ const done = await onToken(data);
781
+ if (done) return;
782
+ if (data.error === 'authorization_pending') continue;
783
+ if (data.error === 'slow_down') { currentInterval += 5000; continue; }
784
+ if (data.error) throw new Error(`Authentication failed: ${data.error_description || data.error}`);
785
+ }
786
+ throw new Error('Authentication timed out after 15 minutes.');
787
+ }
788
+
738
789
  async function cmdLogin(args = []) {
739
790
  const provider = args[0];
740
791
  if (provider !== 'github-copilot' && provider !== 'openai-codex') {
@@ -751,54 +802,28 @@ async function cmdLogin(args = []) {
751
802
  headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
752
803
  body: JSON.stringify({ client_id: clientId, scope: 'user:email' })
753
804
  });
754
-
755
- if (!reqRes.ok) {
756
- throw new Error(`Failed to request device code: HTTP ${reqRes.status}`);
757
- }
805
+ if (!reqRes.ok) throw new Error(`Failed to request device code: HTTP ${reqRes.status}`);
758
806
 
759
807
  const { device_code, user_code, verification_uri, interval } = await reqRes.json();
760
-
761
808
  console.log(`\n ${COLORS.cyan}Please visit:${COLORS.reset} ${verification_uri}`);
762
- console.log(` ${COLORS.cyan}And enter the code:${COLORS.reset} ${COLORS.bold}${user_code}${COLORS.reset}\n`);
763
-
809
+ console.log(` ${COLORS.cyan}Enter code:${COLORS.reset} ${COLORS.bold}${user_code}${COLORS.reset}\n`);
764
810
  logInfo('Waiting for authorization (timeout in 15m)...');
765
- const startTime = Date.now();
766
- const timeoutMs = 15 * 60 * 1000;
767
- let currentPollInterval = (interval || 5) * 1000;
768
-
769
- while (Date.now() - startTime < timeoutMs) {
770
- await new Promise((r) => setTimeout(r, currentPollInterval));
771
- const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
772
- method: 'POST',
773
- headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
774
- body: JSON.stringify({
775
- client_id: clientId,
776
- device_code,
777
- grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
778
- })
779
- });
780
-
781
- if (!tokenRes.ok) {
782
- const errorText = await tokenRes.text().catch(() => 'Unknown error');
783
- throw new Error(`GitHub token request failed: HTTP ${tokenRes.status} - ${errorText}`);
784
- }
785
811
 
786
- const data = await tokenRes.json();
787
- if (data.access_token) {
812
+ await pollDeviceCode({
813
+ pollUrl: 'https://github.com/login/oauth/access_token',
814
+ pollBody: () => ({ client_id: clientId, device_code, grant_type: 'urn:ietf:params:oauth:grant-type:device_code' }),
815
+ intervalMs: (interval || 5) * 1000,
816
+ timeoutMs: 15 * 60 * 1000,
817
+ onToken: async (data) => {
818
+ if (!data.access_token) return false;
788
819
  upsertEnvValue('GITHUB_COPILOT_ACCESS_TOKEN', data.access_token);
789
- logOk('Successfully authenticated and saved GitHub Copilot access token to .env');
790
- logInfo('Applying updated provider credentials by restarting NeoAgent...');
820
+ logOk('Saved GitHub Copilot access token to .env');
821
+ logInfo('Restarting NeoAgent to apply credentials...');
791
822
  cmdRestart();
792
- return;
793
- } else if (data.error === 'authorization_pending') {
794
- // Continue polling
795
- } else if (data.error === 'slow_down') {
796
- currentPollInterval += 5000;
797
- } else if (data.error) {
798
- throw new Error(`Authentication failed: ${data.error_description || data.error}`);
799
- }
800
- }
801
- throw new Error('GitHub authentication timed out after 15 minutes.');
823
+ return true;
824
+ },
825
+ });
826
+ return;
802
827
  } else if (provider === 'openai-codex') {
803
828
  heading('OpenAI Codex Login');
804
829
  const clientId = 'app_EMoamEEZ73f0CkXaXp7hrann';
@@ -820,53 +845,24 @@ async function cmdLogin(args = []) {
820
845
  const verification_uri = 'https://auth.openai.com/codex/device';
821
846
 
822
847
  console.log(`\n ${COLORS.cyan}Please visit:${COLORS.reset} ${verification_uri}`);
823
- console.log(` ${COLORS.cyan}And enter the code:${COLORS.reset} ${COLORS.bold}${user_code}${COLORS.reset}\n`);
824
-
848
+ console.log(` ${COLORS.cyan}Enter code:${COLORS.reset} ${COLORS.bold}${user_code}${COLORS.reset}\n`);
825
849
  logInfo('Waiting for authorization (timeout in 15m)...');
826
- const startTime = Date.now();
827
- const timeoutMs = 15 * 60 * 1000;
828
- let currentPollInterval = (interval || 5) * 1000;
850
+
829
851
  let authorizationCode = null;
830
852
  let codeVerifier = null;
831
853
 
832
- while (Date.now() - startTime < timeoutMs) {
833
- await new Promise((r) => setTimeout(r, currentPollInterval));
834
- const tokenRes = await fetch('https://auth.openai.com/api/accounts/deviceauth/token', {
835
- method: 'POST',
836
- headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
837
- body: JSON.stringify({
838
- device_auth_id: device_auth_id,
839
- user_code: user_code
840
- })
841
- });
842
-
843
- if (tokenRes.status === 403 || tokenRes.status === 404) {
844
- // These statuses are returned by OpenAI while authorization is pending
845
- continue;
846
- }
847
-
848
- if (!tokenRes.ok) {
849
- const errorText = await tokenRes.text().catch(() => 'Unknown error');
850
- throw new Error(`OpenAI token request failed: HTTP ${tokenRes.status} - ${errorText}`);
851
- }
852
-
853
- const pollData = await tokenRes.json();
854
- if (pollData.authorization_code && pollData.code_verifier) {
855
- authorizationCode = pollData.authorization_code;
856
- codeVerifier = pollData.code_verifier;
857
- break;
858
- } else if (pollData.error === 'authorization_pending') {
859
- // Continue polling
860
- } else if (pollData.error === 'slow_down') {
861
- currentPollInterval += 5000;
862
- } else if (pollData.error) {
863
- throw new Error(`Authentication failed: ${pollData.error_description || pollData.error}`);
864
- }
865
- }
866
-
867
- if (!authorizationCode || !codeVerifier) {
868
- throw new Error('OpenAI authentication timed out after 15 minutes.');
869
- }
854
+ await pollDeviceCode({
855
+ pollUrl: 'https://auth.openai.com/api/accounts/deviceauth/token',
856
+ pollBody: () => ({ device_auth_id, user_code }),
857
+ intervalMs: (interval || 5) * 1000,
858
+ timeoutMs: 15 * 60 * 1000,
859
+ onToken: async (data) => {
860
+ if (!data.authorization_code || !data.code_verifier) return false;
861
+ authorizationCode = data.authorization_code;
862
+ codeVerifier = data.code_verifier;
863
+ return true;
864
+ },
865
+ });
870
866
 
871
867
  logInfo('Exchanging authorization code for access token...');
872
868
  const exchangeRes = await fetch('https://auth.openai.com/oauth/token', {
@@ -877,27 +873,26 @@ async function cmdLogin(args = []) {
877
873
  code: authorizationCode,
878
874
  redirect_uri: 'https://auth.openai.com/deviceauth/callback',
879
875
  client_id: clientId,
880
- code_verifier: codeVerifier
881
- })
876
+ code_verifier: codeVerifier,
877
+ }),
882
878
  });
883
879
 
884
880
  if (!exchangeRes.ok) {
885
881
  const errorText = await exchangeRes.text().catch(() => 'Unknown error');
886
- throw new Error(`OpenAI token exchange failed: HTTP ${exchangeRes.status} - ${errorText}`);
882
+ throw new Error(`OpenAI token exchange failed: HTTP ${exchangeRes.status} ${errorText}`);
887
883
  }
888
884
 
889
885
  const exchangeData = await exchangeRes.json();
890
- if (exchangeData.access_token) {
891
- upsertEnvValue('OPENAI_CODEX_ACCESS_TOKEN', exchangeData.access_token);
892
- if (exchangeData.refresh_token) {
893
- upsertEnvValue('OPENAI_CODEX_REFRESH_TOKEN', exchangeData.refresh_token);
894
- }
895
- logOk('Successfully authenticated and saved OpenAI Codex tokens to .env');
896
- logInfo('Applying updated provider credentials by restarting NeoAgent...');
897
- cmdRestart();
898
- } else {
886
+ if (!exchangeData.access_token) {
899
887
  throw new Error('OpenAI token exchange succeeded but did not return an access token.');
900
888
  }
889
+ upsertEnvValue('OPENAI_CODEX_ACCESS_TOKEN', exchangeData.access_token);
890
+ if (exchangeData.refresh_token) {
891
+ upsertEnvValue('OPENAI_CODEX_REFRESH_TOKEN', exchangeData.refresh_token);
892
+ }
893
+ logOk('Saved OpenAI Codex tokens to .env');
894
+ logInfo('Restarting NeoAgent to apply credentials...');
895
+ cmdRestart();
901
896
  }
902
897
  }
903
898
 
@@ -1052,6 +1047,54 @@ async function ensureQemuInstalled() {
1052
1047
  }
1053
1048
  }
1054
1049
 
1050
+ function ensureYtDlpInstalled() {
1051
+ heading('Ensure yt-dlp Installed');
1052
+ if (commandExists('yt-dlp')) {
1053
+ const ver = runQuiet('yt-dlp', ['--version']);
1054
+ logOk(`yt-dlp ${ver.status === 0 ? ver.stdout.trim() : '(version unknown)'}`);
1055
+ return;
1056
+ }
1057
+
1058
+ logInfo('yt-dlp not found. Attempting to install...');
1059
+ const platform = detectPlatform();
1060
+
1061
+ if (platform === 'macos') {
1062
+ if (!commandExists('brew')) {
1063
+ logWarn('Homebrew not found — skipping yt-dlp install. Install manually: brew install yt-dlp');
1064
+ return;
1065
+ }
1066
+ try {
1067
+ runOrThrow('brew', ['install', 'yt-dlp']);
1068
+ logOk('yt-dlp installed via Homebrew');
1069
+ } catch {
1070
+ logWarn('yt-dlp install failed. Install manually: brew install yt-dlp');
1071
+ }
1072
+ return;
1073
+ }
1074
+
1075
+ if (platform === 'linux') {
1076
+ if (commandExists('pipx')) {
1077
+ try {
1078
+ runOrThrow('pipx', ['install', 'yt-dlp']);
1079
+ logOk('yt-dlp installed via pipx');
1080
+ return;
1081
+ } catch {
1082
+ // fall through to pip3
1083
+ }
1084
+ }
1085
+ if (commandExists('pip3')) {
1086
+ try {
1087
+ runOrThrow('pip3', ['install', '--user', 'yt-dlp']);
1088
+ logOk('yt-dlp installed via pip3');
1089
+ return;
1090
+ } catch {
1091
+ // fall through to warn
1092
+ }
1093
+ }
1094
+ logWarn('Could not install yt-dlp automatically. Install manually: pipx install yt-dlp');
1095
+ }
1096
+ }
1097
+
1055
1098
  async function cmdInstall() {
1056
1099
  heading(`Install ${APP_NAME}`);
1057
1100
  if (!fs.existsSync(ENV_FILE)) {
@@ -1061,6 +1104,7 @@ async function cmdInstall() {
1061
1104
 
1062
1105
  installDependencies();
1063
1106
  await ensureQemuInstalled();
1107
+ ensureYtDlpInstalled();
1064
1108
  buildBundledWebClientIfPossible({ required: true });
1065
1109
 
1066
1110
  const platform = detectPlatform();
@@ -1122,7 +1166,7 @@ function cmdStop() {
1122
1166
  logOk(`Stopped pid ${pid}`);
1123
1167
  stopped = true;
1124
1168
  } catch {
1125
- logWarn(`pid ${pid} not running`);
1169
+ logWarn(`pid ${pid} was not running (stale PID file)`);
1126
1170
  }
1127
1171
  }
1128
1172
  fs.rmSync(pidPath, { force: true });
@@ -1183,22 +1227,52 @@ async function cmdStatus() {
1183
1227
  const port = loadEnvPort();
1184
1228
  const running = await isPortOpen(port);
1185
1229
  const releaseChannel = currentReleaseChannel();
1230
+ const platform = detectPlatform();
1186
1231
 
1187
1232
  if (running) {
1188
- logOk(`running on http://localhost:${port}`);
1233
+ logOk(`server http://localhost:${port}`);
1189
1234
  } else {
1190
- logWarn(`not reachable on port ${port}`);
1235
+ logWarn(`server not reachable on port ${port}`);
1191
1236
  }
1192
1237
 
1193
- console.log(` install root ${APP_DIR}`);
1194
- console.log(` version ${currentInstalledVersionLabel()}`);
1195
- console.log(` release channel ${releaseChannelSummary(releaseChannel)}`);
1238
+ if (platform === 'macos' && fs.existsSync(PLIST_DST)) {
1239
+ const svcRes = runQuiet('launchctl', ['list', SERVICE_LABEL]);
1240
+ if (svcRes.status === 0 && svcRes.stdout.trim()) {
1241
+ logOk(`service launchd (${SERVICE_LABEL})`);
1242
+ } else {
1243
+ logWarn(`service launchd unit not loaded — run: neoagent install`);
1244
+ }
1245
+ } else if (platform === 'linux' && fs.existsSync(SYSTEMD_UNIT)) {
1246
+ const svcRes = runQuiet('systemctl', ['--user', 'is-active', 'neoagent']);
1247
+ if (svcRes.status === 0 && svcRes.stdout.trim() === 'active') {
1248
+ logOk('service systemd (neoagent)');
1249
+ } else {
1250
+ logWarn('service systemd unit not active — run: neoagent install');
1251
+ }
1252
+ }
1253
+
1254
+ if (fs.existsSync(ENV_FILE)) {
1255
+ logOk(`config ${ENV_FILE}`);
1256
+ } else {
1257
+ logWarn(`config .env not found — run: neoagent setup`);
1258
+ }
1259
+
1260
+ if (hasBundledWebClient(WEB_CLIENT_DIR)) {
1261
+ logOk('web bundled Flutter client present');
1262
+ } else {
1263
+ logWarn('web no bundled client — run: neoagent rebuild-web');
1264
+ }
1265
+
1266
+ console.log('');
1267
+ console.log(` install ${APP_DIR}`);
1268
+ console.log(` version ${currentInstalledVersionLabel()}`);
1269
+ console.log(` channel ${releaseChannelSummary(releaseChannel)}`);
1196
1270
 
1197
1271
  const processes = listNeoAgentServerProcesses();
1198
1272
  if (processes.length > 0) {
1199
- console.log(` neoagent pids ${processes.map((proc) => proc.pid).join(', ')}`);
1273
+ console.log(` pids ${processes.map((proc) => proc.pid).join(', ')}`);
1200
1274
  if (processes.length > 1) {
1201
- logWarn(`multiple NeoAgent server processes detected (${processes.length})`);
1275
+ logWarn(`multiple NeoAgent processes detected (${processes.length})`);
1202
1276
  }
1203
1277
  }
1204
1278
  }
@@ -1260,6 +1334,7 @@ function cmdUpdate(args = []) {
1260
1334
  const targetBranch = resolvePreferredGitBranch(releaseChannel);
1261
1335
  logInfo(`Using git branch ${targetBranch} for the ${releaseChannel} channel.`);
1262
1336
  ensureGitBranchForReleaseChannel(targetBranch);
1337
+ backupRuntimeData();
1263
1338
  runOrThrow('git', ['pull', '--rebase', '--autostash', 'origin', targetBranch]);
1264
1339
 
1265
1340
  const next = runQuiet('git', ['rev-parse', '--short', 'HEAD']);
@@ -1289,6 +1364,7 @@ function cmdUpdate(args = []) {
1289
1364
  }
1290
1365
 
1291
1366
  versionAfter = currentInstalledVersionLabel();
1367
+ ensureYtDlpInstalled();
1292
1368
 
1293
1369
  if (!hasBundledWebClient(WEB_CLIENT_DIR)) {
1294
1370
  throw new Error('No bundled Flutter web client found after update.');
@@ -1300,11 +1376,16 @@ function cmdUpdate(args = []) {
1300
1376
 
1301
1377
  async function cmdEnv(args = []) {
1302
1378
  heading('Environment Variables');
1303
- let action = args[0];
1379
+ const action = (args[0] || '').trim().toLowerCase();
1304
1380
 
1305
1381
  if (!action) {
1306
- const picked = await ask('Action (list/get/set/unset)', 'list');
1307
- action = (picked || 'list').trim().toLowerCase();
1382
+ console.log('Usage: neoagent env <subcommand>');
1383
+ console.log('');
1384
+ console.log(' neoagent env list List all variables (secrets masked)');
1385
+ console.log(' neoagent env get KEY Print a single variable');
1386
+ console.log(' neoagent env set KEY VALUE Set a variable');
1387
+ console.log(' neoagent env unset KEY Remove a variable');
1388
+ return;
1308
1389
  }
1309
1390
 
1310
1391
  if (action === 'list') {
@@ -1329,16 +1410,17 @@ async function cmdEnv(args = []) {
1329
1410
  }
1330
1411
 
1331
1412
  if (action === 'set') {
1332
- const key = args[1] || await ask('Key', 'PORT');
1333
- const value = args.slice(2).join(' ') || await ask('Value', key === 'PORT' ? '3333' : '');
1413
+ const key = args[1];
1414
+ const value = args.slice(2).join(' ');
1334
1415
  if (!key || !value) throw new Error('Usage: neoagent env set <KEY> <VALUE>');
1416
+ validateEnvKey(key);
1335
1417
  upsertEnvValue(key, value);
1336
1418
  logOk(`Set ${key} in ${ENV_FILE}`);
1337
1419
  return;
1338
1420
  }
1339
1421
 
1340
1422
  if (action === 'unset') {
1341
- const key = args[1] || await ask('Key', 'PORT');
1423
+ const key = args[1];
1342
1424
  if (!key) throw new Error('Usage: neoagent env unset <KEY>');
1343
1425
  removeEnvValue(key);
1344
1426
  logOk(`Removed ${key} from ${ENV_FILE}`);
@@ -1348,16 +1430,55 @@ async function cmdEnv(args = []) {
1348
1430
  throw new Error('Usage: neoagent env [list|get|set|unset] ...');
1349
1431
  }
1350
1432
 
1433
+ function cmdVersion() {
1434
+ console.log(currentInstalledVersionLabel());
1435
+ }
1436
+
1351
1437
  function printHelp() {
1352
- console.log(`${APP_NAME} manager`);
1353
- console.log('Usage: neoagent <command>');
1354
- console.log('Commands: install | setup | env | channel | update | restart | rebuild-web | start | stop | status | logs | uninstall | migrate | login');
1355
- console.log('Login usage: neoagent login github-copilot | neoagent login openai-codex');
1356
- console.log('Channel usage: neoagent channel | neoagent channel stable | neoagent channel beta');
1357
- console.log('Update usage: neoagent update | neoagent update stable | neoagent update beta');
1358
- console.log('Env usage: neoagent env list | neoagent env get PORT | neoagent env set PORT 3333 | neoagent env unset PORT');
1359
- console.log('Migrate usage: neoagent migrate | neoagent migrate dry-run | neoagent migrate status');
1360
- console.log(' neoagent migrate openclaw-only | neoagent migrate hermes-only');
1438
+ const c = COLORS;
1439
+ const W = 38;
1440
+
1441
+ function row(cmd, desc) {
1442
+ const padded = ` neoagent ${cmd}`.padEnd(W);
1443
+ console.log(`${padded}${c.dim}${desc}${c.reset}`);
1444
+ }
1445
+
1446
+ console.log(`\n${c.bold}neoagent${c.reset} — manage your NeoAgent server\n`);
1447
+ console.log(`${c.bold}Usage${c.reset} neoagent <command> [args]\n`);
1448
+
1449
+ console.log(`${c.bold}Lifecycle${c.reset}`);
1450
+ row('install', 'First-time install and service setup');
1451
+ row('start', 'Start the server');
1452
+ row('stop', 'Stop the server');
1453
+ row('restart', 'Stop, then start');
1454
+ row('status', 'Health overview (server, service, config)');
1455
+ row('logs', 'Tail server logs');
1456
+ row('uninstall', 'Remove the system service');
1457
+ console.log('');
1458
+
1459
+ console.log(`${c.bold}Configuration${c.reset}`);
1460
+ row('setup', 'Interactive configuration wizard');
1461
+ row('env list', 'List all variables (secrets masked)');
1462
+ row('env get KEY', 'Print a single variable');
1463
+ row('env set KEY VALUE', 'Set a variable');
1464
+ row('env unset KEY', 'Remove a variable');
1465
+ row('channel', 'Show current release channel');
1466
+ row('channel stable|beta', 'Switch release channel');
1467
+ console.log('');
1468
+
1469
+ console.log(`${c.bold}Updates & Auth${c.reset}`);
1470
+ row('update', 'Update to latest on current channel');
1471
+ row('update stable|beta', 'Update and switch channel');
1472
+ row('login github-copilot','Authenticate GitHub Copilot');
1473
+ row('login openai-codex', 'Authenticate OpenAI Codex');
1474
+ console.log('');
1475
+
1476
+ console.log(`${c.bold}Maintenance${c.reset}`);
1477
+ row('migrate', 'Migrate from another agent installation');
1478
+ row('migrate dry-run', 'Preview what would be migrated');
1479
+ row('rebuild-web', 'Rebuild the bundled Flutter web client');
1480
+ row('version', 'Print installed version');
1481
+ console.log('');
1361
1482
  }
1362
1483
 
1363
1484
  async function runCLI(argv) {
@@ -1408,13 +1529,18 @@ async function runCLI(argv) {
1408
1529
  case 'login':
1409
1530
  await cmdLogin(argv.slice(1));
1410
1531
  break;
1532
+ case 'version':
1533
+ case '--version':
1534
+ case '-V':
1535
+ cmdVersion();
1536
+ break;
1411
1537
  case 'help':
1412
1538
  case '--help':
1413
1539
  case '-h':
1414
1540
  printHelp();
1415
1541
  break;
1416
1542
  default:
1417
- throw new Error(`Unknown command: ${command}`);
1543
+ throw new Error(`Unknown command: ${command}. Run "neoagent --help" for usage.`);
1418
1544
  }
1419
1545
  }
1420
1546
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.3.1-beta.93",
3
+ "version": "2.3.1-beta.94",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -1 +1 @@
1
- 3034f723b7ff72b29c3df48d7cab6fe2
1
+ 2700d443d51328af53dfc4e4cb2cec1f
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"42d3d75a56efe1a2e9902f52dc8006099c45d9
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "3770663104" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "3348826037" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });