neoagent 2.1.14 → 2.1.15

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
@@ -21,13 +21,13 @@ neoagent install
21
21
  Manage the service:
22
22
  ```bash
23
23
  neoagent status
24
+ neoagent channel beta
24
25
  neoagent update
25
26
  neoagent fix
26
27
  neoagent logs
27
28
  ```
28
29
 
29
30
  Use `neoagent fix` if a self-edit or broken local install leaves NeoAgent in a bad state. On git installs it backs up runtime data, saves local tracked changes, resets tracked source files, reinstalls dependencies, and restarts the service.
30
-
31
31
  ---
32
32
 
33
33
  [⚙️ Configuration](docs/configuration.md) · [🧰 Skills](docs/skills.md) · [🐛 Issues](https://github.com/NeoLabs-Systems/NeoAgent/issues)
package/lib/manager.js CHANGED
@@ -21,6 +21,14 @@ const {
21
21
  ensureRuntimeDirs,
22
22
  migrateLegacyRuntime
23
23
  } = require('../runtime/paths');
24
+ const {
25
+ parseReleaseChannel,
26
+ getReleaseChannelBranch,
27
+ getReleaseChannelDistTag,
28
+ getReleaseChannelLabel,
29
+ readConfiguredReleaseChannel,
30
+ writeReleaseChannelToEnvFile,
31
+ } = require('../runtime/release_channel');
24
32
 
25
33
  const APP_NAME = 'NeoAgent';
26
34
  const SERVICE_LABEL = 'com.neoagent';
@@ -184,6 +192,59 @@ function commandExists(cmd) {
184
192
  return sharedCommandExists((command, args) => runQuiet(command, args), cmd);
185
193
  }
186
194
 
195
+ function currentReleaseChannel() {
196
+ return readConfiguredReleaseChannel({ envFile: ENV_FILE });
197
+ }
198
+
199
+ function releaseChannelSummary(channel) {
200
+ const normalized = parseReleaseChannel(channel) || currentReleaseChannel();
201
+ return `${getReleaseChannelLabel(normalized)} (branch ${getReleaseChannelBranch(normalized)}, npm ${getReleaseChannelDistTag(normalized)})`;
202
+ }
203
+
204
+ function gitWorkingTreeDirty() {
205
+ const res = runQuiet('git', ['status', '--porcelain']);
206
+ return res.status === 0 && Boolean(res.stdout.trim());
207
+ }
208
+
209
+ function gitLocalBranchExists(branch) {
210
+ return runQuiet('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`]).status === 0;
211
+ }
212
+
213
+ function gitRemoteBranchExists(branch) {
214
+ return runQuiet('git', ['ls-remote', '--exit-code', '--heads', 'origin', branch]).status === 0;
215
+ }
216
+
217
+ function ensureGitBranchForReleaseChannel(targetBranch) {
218
+ const branchRes = runQuiet('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
219
+ const currentBranch = branchRes.status === 0 ? branchRes.stdout.trim() : '';
220
+ if (currentBranch === targetBranch) {
221
+ return currentBranch;
222
+ }
223
+
224
+ if (!gitRemoteBranchExists(targetBranch)) {
225
+ throw new Error(`Release channel branch "${targetBranch}" was not found on origin.`);
226
+ }
227
+
228
+ if (gitWorkingTreeDirty()) {
229
+ throw new Error(
230
+ `Cannot switch to ${targetBranch} while the git worktree has local changes. Commit or stash them first, then rerun the update.`,
231
+ );
232
+ }
233
+
234
+ if (gitLocalBranchExists(targetBranch)) {
235
+ runOrThrow('git', ['checkout', targetBranch]);
236
+ } else {
237
+ runOrThrow('git', ['checkout', '-b', targetBranch, '--track', `origin/${targetBranch}`]);
238
+ }
239
+
240
+ if (currentBranch) {
241
+ logOk(`Switched git branch ${currentBranch} -> ${targetBranch}`);
242
+ } else {
243
+ logOk(`Checked out git branch ${targetBranch}`);
244
+ }
245
+ return targetBranch;
246
+ }
247
+
187
248
  function ensureLogDir() {
188
249
  ensureRuntimeDirs();
189
250
  }
@@ -565,6 +626,7 @@ async function cmdStatus() {
565
626
  heading(`${APP_NAME} Status`);
566
627
  const port = loadEnvPort();
567
628
  const running = await isPortOpen(port);
629
+ const releaseChannel = currentReleaseChannel();
568
630
 
569
631
  if (running) {
570
632
  logOk(`running on http://localhost:${port}`);
@@ -574,6 +636,7 @@ async function cmdStatus() {
574
636
 
575
637
  console.log(` install root ${APP_DIR}`);
576
638
  console.log(` version ${currentInstalledVersionLabel()}`);
639
+ console.log(` release channel ${releaseChannelSummary(releaseChannel)}`);
577
640
 
578
641
  const processes = listNeoAgentServerProcesses();
579
642
  if (processes.length > 0) {
@@ -595,23 +658,51 @@ function cmdLogs() {
595
658
  runOrThrow('tail', ['-f', log, err], { cwd: APP_DIR });
596
659
  }
597
660
 
598
- function cmdUpdate() {
661
+ function cmdChannel(args = []) {
662
+ heading('Release Channel');
663
+ const requested = args[0];
664
+
665
+ if (!requested) {
666
+ const channel = currentReleaseChannel();
667
+ console.log(` configured ${releaseChannelSummary(channel)}`);
668
+ return;
669
+ }
670
+
671
+ const nextChannel = parseReleaseChannel(requested);
672
+ if (!nextChannel) {
673
+ throw new Error('Usage: neoagent channel [stable|beta]');
674
+ }
675
+
676
+ writeReleaseChannelToEnvFile(nextChannel, ENV_FILE);
677
+ process.env.NEOAGENT_RELEASE_CHANNEL = nextChannel;
678
+ logOk(`Release channel set to ${releaseChannelSummary(nextChannel)}`);
679
+ }
680
+
681
+ function cmdUpdate(args = []) {
599
682
  heading(`Update ${APP_NAME}`);
600
683
  migrateLegacyRuntime((msg) => logInfo(msg));
601
684
  ensureRuntimeDirs();
685
+ const requestedChannel = args[0] ? parseReleaseChannel(args[0]) : null;
686
+ if (args[0] && !requestedChannel) {
687
+ throw new Error('Usage: neoagent update [stable|beta]');
688
+ }
689
+ const releaseChannel = requestedChannel || currentReleaseChannel();
690
+ if (requestedChannel) {
691
+ writeReleaseChannelToEnvFile(releaseChannel, ENV_FILE);
692
+ process.env.NEOAGENT_RELEASE_CHANNEL = releaseChannel;
693
+ logOk(`Release channel set to ${releaseChannelSummary(releaseChannel)}`);
694
+ }
695
+ const targetBranch = getReleaseChannelBranch(releaseChannel);
696
+ const npmTag = getReleaseChannelDistTag(releaseChannel);
602
697
  const versionBefore = currentInstalledVersionLabel();
603
698
  let versionAfter = versionBefore;
604
699
 
605
700
  if (fs.existsSync(path.join(APP_DIR, '.git')) && commandExists('git')) {
606
- const branch = runQuiet('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
607
701
  const current = runQuiet('git', ['rev-parse', '--short', 'HEAD']);
608
702
 
609
- runOrThrow('git', ['fetch', 'origin']);
610
- if (branch.status === 0) {
611
- runOrThrow('git', ['pull', '--rebase', '--autostash', 'origin', branch.stdout.trim()]);
612
- } else {
613
- runOrThrow('git', ['pull', '--rebase', '--autostash']);
614
- }
703
+ runOrThrow('git', ['fetch', 'origin', targetBranch]);
704
+ ensureGitBranchForReleaseChannel(targetBranch);
705
+ runOrThrow('git', ['pull', '--rebase', '--autostash', 'origin', targetBranch]);
615
706
 
616
707
  const next = runQuiet('git', ['rev-parse', '--short', 'HEAD']);
617
708
  if (current.status === 0 && next.status === 0 && current.stdout.trim() !== next.stdout.trim()) {
@@ -623,16 +714,16 @@ function cmdUpdate() {
623
714
  buildBundledWebClientIfPossible();
624
715
  }
625
716
  } else {
626
- logWarn('No git repo detected; attempting npm global update.');
717
+ logWarn(`No git repo detected; attempting npm global update from ${npmTag}.`);
627
718
  if (commandExists('npm')) {
628
719
  try {
629
720
  backupRuntimeData();
630
- runOrThrow('npm', ['install', '-g', 'neoagent@latest', '--force'], {
721
+ runOrThrow('npm', ['install', '-g', `neoagent@${npmTag}`, '--force'], {
631
722
  env: withInstallEnv()
632
723
  });
633
724
  logOk('npm global update completed (forced reinstall)');
634
725
  } catch {
635
- logWarn('npm global update failed. Run: npm install -g neoagent@latest --force');
726
+ logWarn(`npm global update failed. Run: npm install -g neoagent@${npmTag} --force`);
636
727
  }
637
728
  } else {
638
729
  logWarn('npm not found. Cannot perform global update.');
@@ -702,7 +793,9 @@ async function cmdEnv(args = []) {
702
793
  function printHelp() {
703
794
  console.log(`${APP_NAME} manager`);
704
795
  console.log('Usage: neoagent <command>');
705
- console.log('Commands: install | setup | env | update | restart | start | stop | status | logs | uninstall');
796
+ console.log('Commands: install | setup | env | channel | update | restart | start | stop | status | logs | uninstall');
797
+ console.log('Channel usage: neoagent channel | neoagent channel stable | neoagent channel beta');
798
+ console.log('Update usage: neoagent update | neoagent update stable | neoagent update beta');
706
799
  console.log('Env usage: neoagent env list | neoagent env get PORT | neoagent env set PORT 3333 | neoagent env unset PORT');
707
800
  }
708
801
 
@@ -721,8 +814,11 @@ async function runCLI(argv) {
721
814
  case 'env':
722
815
  await cmdEnv(argv.slice(1));
723
816
  break;
817
+ case 'channel':
818
+ cmdChannel(argv.slice(1));
819
+ break;
724
820
  case 'update':
725
- cmdUpdate();
821
+ cmdUpdate(argv.slice(1));
726
822
  break;
727
823
  case 'restart':
728
824
  cmdRestart();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.1.14",
3
+ "version": "2.1.15",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { ENV_FILE } = require('./paths');
6
+
7
+ const DEFAULT_RELEASE_CHANNEL = 'stable';
8
+ const RELEASE_CHANNEL_ENV_KEY = 'NEOAGENT_RELEASE_CHANNEL';
9
+ const RELEASE_CHANNEL_BRANCHES = Object.freeze({
10
+ stable: 'main',
11
+ beta: 'beta',
12
+ });
13
+ const RELEASE_CHANNEL_DIST_TAGS = Object.freeze({
14
+ stable: 'latest',
15
+ beta: 'beta',
16
+ });
17
+
18
+ function parseEnv(raw) {
19
+ const map = new Map();
20
+ for (const line of String(raw || '').split('\n')) {
21
+ if (!line || line.startsWith('#') || !line.includes('=')) continue;
22
+ const idx = line.indexOf('=');
23
+ const key = line.slice(0, idx).trim();
24
+ const value = line.slice(idx + 1);
25
+ if (key) map.set(key, value);
26
+ }
27
+ return map;
28
+ }
29
+
30
+ function parseReleaseChannel(value) {
31
+ const normalized = String(value || '').trim().toLowerCase();
32
+ switch (normalized) {
33
+ case 'stable':
34
+ case 'normal':
35
+ case 'default':
36
+ case 'latest':
37
+ case 'main':
38
+ return 'stable';
39
+ case 'beta':
40
+ case 'preview':
41
+ case 'prerelease':
42
+ case 'pre-release':
43
+ return 'beta';
44
+ default:
45
+ return null;
46
+ }
47
+ }
48
+
49
+ function normalizeReleaseChannel(value) {
50
+ return parseReleaseChannel(value) || DEFAULT_RELEASE_CHANNEL;
51
+ }
52
+
53
+ function getReleaseChannelBranch(channel) {
54
+ return RELEASE_CHANNEL_BRANCHES[normalizeReleaseChannel(channel)];
55
+ }
56
+
57
+ function getReleaseChannelDistTag(channel) {
58
+ return RELEASE_CHANNEL_DIST_TAGS[normalizeReleaseChannel(channel)];
59
+ }
60
+
61
+ function getReleaseChannelLabel(channel) {
62
+ return normalizeReleaseChannel(channel) === 'beta' ? 'Beta' : 'Stable';
63
+ }
64
+
65
+ function readReleaseChannelFromRaw(raw) {
66
+ const env = parseEnv(raw);
67
+ return normalizeReleaseChannel(env.get(RELEASE_CHANNEL_ENV_KEY));
68
+ }
69
+
70
+ function readReleaseChannelFromEnvFile(envFile = ENV_FILE) {
71
+ try {
72
+ return readReleaseChannelFromRaw(fs.readFileSync(envFile, 'utf8'));
73
+ } catch {
74
+ return DEFAULT_RELEASE_CHANNEL;
75
+ }
76
+ }
77
+
78
+ function readConfiguredReleaseChannel({ env = process.env, envFile = ENV_FILE } = {}) {
79
+ return normalizeReleaseChannel(env[RELEASE_CHANNEL_ENV_KEY] || readReleaseChannelFromEnvFile(envFile));
80
+ }
81
+
82
+ function writeReleaseChannelToEnvFile(channel, envFile = ENV_FILE) {
83
+ const normalized = parseReleaseChannel(channel);
84
+ if (!normalized) {
85
+ throw new Error('Release channel must be "stable" or "beta".');
86
+ }
87
+
88
+ const raw = fs.existsSync(envFile) ? fs.readFileSync(envFile, 'utf8') : '';
89
+ const lines = raw ? raw.split('\n') : [];
90
+ let replaced = false;
91
+
92
+ for (let i = 0; i < lines.length; i++) {
93
+ if (lines[i].startsWith(`${RELEASE_CHANNEL_ENV_KEY}=`)) {
94
+ lines[i] = `${RELEASE_CHANNEL_ENV_KEY}=${normalized}`;
95
+ replaced = true;
96
+ break;
97
+ }
98
+ }
99
+
100
+ if (!replaced) {
101
+ lines.push(`${RELEASE_CHANNEL_ENV_KEY}=${normalized}`);
102
+ }
103
+
104
+ const output =
105
+ lines.filter((_, idx, arr) => idx !== arr.length - 1 || arr[idx] !== '').join('\n') + '\n';
106
+ fs.mkdirSync(path.dirname(envFile), { recursive: true });
107
+ fs.writeFileSync(envFile, output, { mode: 0o600 });
108
+ return normalized;
109
+ }
110
+
111
+ module.exports = {
112
+ DEFAULT_RELEASE_CHANNEL,
113
+ RELEASE_CHANNEL_ENV_KEY,
114
+ parseReleaseChannel,
115
+ normalizeReleaseChannel,
116
+ getReleaseChannelBranch,
117
+ getReleaseChannelDistTag,
118
+ getReleaseChannelLabel,
119
+ readReleaseChannelFromRaw,
120
+ readReleaseChannelFromEnvFile,
121
+ readConfiguredReleaseChannel,
122
+ writeReleaseChannelToEnvFile,
123
+ };
package/server/index.js CHANGED
@@ -53,6 +53,7 @@ registerStaticRoutes(app);
53
53
  registerErrorHandler(app);
54
54
 
55
55
  let shuttingDown = false;
56
+ let shutdownExitCode = 0;
56
57
 
57
58
  httpServer.on('connection', (socket) => {
58
59
  activeSockets.add(socket);
@@ -146,7 +147,8 @@ function closeHttpServer(server, sockets, timeoutMs = 5000) {
146
147
  });
147
148
  }
148
149
 
149
- async function shutdown() {
150
+ async function shutdown(exitCode = 0) {
151
+ shutdownExitCode = Math.max(shutdownExitCode, exitCode);
150
152
  if (shuttingDown) return;
151
153
  shuttingDown = true;
152
154
 
@@ -159,12 +161,15 @@ async function shutdown() {
159
161
  ]);
160
162
 
161
163
  db.close();
162
- process.exit(0);
164
+ process.exit(shutdownExitCode);
163
165
  }
164
166
 
165
- httpServer.listen(PORT, async () => {
167
+ httpServer.listen(PORT, () => {
166
168
  console.log(`NeoAgent running on http://localhost:${PORT}`);
167
- await startServices(app, io);
169
+ startServices(app, io).catch(async (err) => {
170
+ console.error('[Startup] Service initialization failed:', err);
171
+ await shutdown(1);
172
+ });
168
173
  });
169
174
 
170
175
  process.on('SIGINT', shutdown);
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"052f31d115eceda8cbff1b3481fcde4330c4ae
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "1633758143" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "1964138140" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });