neoctl-web 0.1.6 → 0.1.8

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/dist/index.html CHANGED
@@ -19,8 +19,8 @@
19
19
  })()
20
20
  </script>
21
21
  <title>neo runtime</title>
22
- <script type="module" crossorigin src="/assets/index-CwSgoMSk.js"></script>
23
- <link rel="stylesheet" crossorigin href="/assets/index-MgmD3vMN.css">
22
+ <script type="module" crossorigin src="/assets/index-BihhJN9D.js"></script>
23
+ <link rel="stylesheet" crossorigin href="/assets/index-CIm8rv9k.css">
24
24
  </head>
25
25
  <body>
26
26
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoctl-web",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Neo browser workspace with an embedded agent runtime.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -15,6 +15,7 @@
15
15
  "test:runtime": "node --test runtime-router-cleanup.test.mjs runtime-workspaces.test.mjs",
16
16
  "test:plugins": "npm --prefix ../engine run build && node --test plugins.test.mjs plugin-settings.test.mjs tool-settings.test.mjs",
17
17
  "test:monitoring": "node --test cpa-quota.test.mjs memory-monitor.test.mjs",
18
+ "test:control": "node --test control-sync.test.mjs control-transcript.test.mjs",
18
19
  "build": "vite build",
19
20
  "test:cli": "node --test neow.test.mjs",
20
21
  "prestart": "npm run build",
@@ -46,6 +47,8 @@
46
47
  "dist",
47
48
  "plugins",
48
49
  "server.mjs",
50
+ "control-sync.mjs",
51
+ "control-protocol.mjs",
49
52
  "core-runtime.mjs",
50
53
  "plugins.mjs",
51
54
  "plugin-settings.mjs",
@@ -1,40 +1,40 @@
1
- import os from 'node:os';
2
- import path from 'node:path';
3
-
4
- export function defaultWebDataRoot(options = {}) {
5
- const platform = options.platform || process.platform;
6
- const env = options.env || process.env;
7
- const homeDir = options.homeDir || os.homedir();
8
- const pathApi = platform === 'win32' ? path.win32 : path.posix;
9
-
10
- if (platform === 'win32') {
11
- const localAppData = absoluteEnvPath(env.LOCALAPPDATA, pathApi);
12
- return pathApi.join(localAppData || pathApi.join(homeDir, 'AppData', 'Local'), 'neoctl-web');
13
- }
14
- if (platform === 'darwin') {
15
- return pathApi.join(homeDir, 'Library', 'Application Support', 'neoctl-web');
16
- }
17
- const xdgDataHome = absoluteEnvPath(env.XDG_DATA_HOME, pathApi);
18
- return pathApi.join(xdgDataHome || pathApi.join(homeDir, '.local', 'share'), 'neoctl-web');
19
- }
20
-
21
- export function resolveWebStorage(options = {}) {
22
- const platform = options.platform || process.platform;
23
- const env = options.env || process.env;
24
- const pathApi = platform === 'win32' ? path.win32 : path.posix;
25
- const cwd = options.cwd || process.cwd();
26
- const dataRoot = pathApi.resolve(
27
- cwd,
28
- String(env.NEO_WEB_DATA_DIR || '').trim() || defaultWebDataRoot({ ...options, platform, env }),
29
- );
30
- const workspaceRoot = pathApi.resolve(
31
- cwd,
32
- String(env.NEO_WORKSPACE_ROOT || '').trim() || pathApi.join(dataRoot, 'workspaces'),
33
- );
34
- return { dataRoot, workspaceRoot };
35
- }
36
-
37
- function absoluteEnvPath(value, pathApi) {
38
- const candidate = String(value || '').trim();
39
- return candidate && pathApi.isAbsolute(candidate) ? candidate : '';
40
- }
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+
4
+ export function defaultWebDataRoot(options = {}) {
5
+ const platform = options.platform || process.platform;
6
+ const env = options.env || process.env;
7
+ const homeDir = options.homeDir || os.homedir();
8
+ const pathApi = platform === 'win32' ? path.win32 : path.posix;
9
+
10
+ if (platform === 'win32') {
11
+ const localAppData = absoluteEnvPath(env.LOCALAPPDATA, pathApi);
12
+ return pathApi.join(localAppData || pathApi.join(homeDir, 'AppData', 'Local'), 'neoctl-web');
13
+ }
14
+ if (platform === 'darwin') {
15
+ return pathApi.join(homeDir, 'Library', 'Application Support', 'neoctl-web');
16
+ }
17
+ const xdgDataHome = absoluteEnvPath(env.XDG_DATA_HOME, pathApi);
18
+ return pathApi.join(xdgDataHome || pathApi.join(homeDir, '.local', 'share'), 'neoctl-web');
19
+ }
20
+
21
+ export function resolveWebStorage(options = {}) {
22
+ const platform = options.platform || process.platform;
23
+ const env = options.env || process.env;
24
+ const pathApi = platform === 'win32' ? path.win32 : path.posix;
25
+ const cwd = options.cwd || process.cwd();
26
+ const dataRoot = pathApi.resolve(
27
+ cwd,
28
+ String(env.NEO_WEB_DATA_DIR || '').trim() || defaultWebDataRoot({ ...options, platform, env }),
29
+ );
30
+ const workspaceRoot = pathApi.resolve(
31
+ cwd,
32
+ String(env.NEO_WORKSPACE_ROOT || '').trim() || pathApi.join(dataRoot, 'workspaces'),
33
+ );
34
+ return { dataRoot, workspaceRoot };
35
+ }
36
+
37
+ function absoluteEnvPath(value, pathApi) {
38
+ const candidate = String(value || '').trim();
39
+ return candidate && pathApi.isAbsolute(candidate) ? candidate : '';
40
+ }
package/server.mjs CHANGED
@@ -3,16 +3,30 @@ import fs from 'node:fs';
3
3
  import fsp from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { coreRuntimeInfo, createWebRuntime, loadNeoPlugins, runWebServer } from './core-runtime.mjs';
7
- import { createWebPluginHost } from './plugins.mjs';
8
- import { createWebPluginSettings } from './plugin-settings.mjs';
9
- import { createWebToolSettings } from './tool-settings.mjs';
10
- import { createWorkspaceRuntimeManager } from './runtime-workspaces.mjs';
11
- import { installRuntimeRouterIdleCleanup } from './runtime-router-cleanup.mjs';
12
- import { createCpaQuotaMonitor } from './cpa-quota.mjs';
13
- import { createMemoryMonitor } from './memory-monitor.mjs';
14
- import { resolveWebStorage } from './platform-paths.mjs';
6
+ // Consume the private launcher secret before importing application/runtime modules.
7
+ // Imports can initialize plugins or spawn children; none may inherit this value.
8
+ let desktopControlConfig;
9
+ {
10
+ const raw = process.env.NEO_DESKTOP_CONTROL_CONFIG;
11
+ delete process.env.NEO_DESKTOP_CONTROL_CONFIG;
12
+ try { desktopControlConfig = raw ? JSON.parse(raw) : undefined; } catch {}
13
+ }
14
+
15
+ const { coreRuntimeInfo, createWebRuntime, loadNeoPlugins, runWebServer } = await import('./core-runtime.mjs');
16
+ const { createWebPluginHost } = await import('./plugins.mjs');
17
+ const { createWebPluginSettings } = await import('./plugin-settings.mjs');
18
+ const { createWebToolSettings } = await import('./tool-settings.mjs');
19
+ const { createWorkspaceRuntimeManager } = await import('./runtime-workspaces.mjs');
20
+ const { installRuntimeRouterIdleCleanup } = await import('./runtime-router-cleanup.mjs');
21
+ const { createCpaQuotaMonitor } = await import('./cpa-quota.mjs');
22
+ const { createMemoryMonitor } = await import('./memory-monitor.mjs');
23
+ const { resolveWebStorage } = await import('./platform-paths.mjs');
24
+ const { createControlSync, createLoginApplier, validateControlConfig } = await import('./control-sync.mjs');
15
25
 
26
+ const controlConfig = validateControlConfig(desktopControlConfig);
27
+ desktopControlConfig = undefined;
28
+ const controlEnabled = Boolean(controlConfig);
29
+
16
30
  installRuntimeRouterIdleCleanup();
17
31
  console.log(`neo core: ${coreRuntimeInfo.source} ${coreRuntimeInfo.version} (${coreRuntimeInfo.location})`);
18
32
  process.env.NEO_CORE_VERSION = coreRuntimeInfo.version;
@@ -21,9 +35,9 @@ process.env.NEO_CLIENT_REVISION ||= `${coreRuntimeInfo.version}-${Date.now().toS
21
35
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
36
  const root = path.resolve(process.env.DIST_DIR || path.join(__dirname, 'dist'));
23
37
  const host = process.env.APP_HOST || '0.0.0.0';
24
- const port = Number(process.env.APP_PORT || process.env.PORT || 5173);
25
- const runtimeTarget = new URL(process.env.NEO_RUNTIME_TARGET || 'http://127.0.0.1:3101');
26
- const { dataRoot, workspaceRoot } = resolveWebStorage();
38
+ const port = Number(process.env.APP_PORT || process.env.PORT || 5173);
39
+ const runtimeTarget = new URL(process.env.NEO_RUNTIME_TARGET || 'http://127.0.0.1:3101');
40
+ const { dataRoot, workspaceRoot } = resolveWebStorage();
27
41
  const promptLibraryFile = path.resolve(process.env.NEO_PROMPT_LIBRARY_FILE || path.join(dataRoot, 'prompt-library.json'));
28
42
  const uploadsDir = path.resolve(process.env.NEO_UPLOADS_DIR || path.join(dataRoot, 'uploads'));
29
43
  const pluginDir = path.resolve(process.env.NEO_WEB_PLUGIN_DIR || path.join(__dirname, 'plugins'));
@@ -53,10 +67,21 @@ const memoryMonitor = createMemoryMonitor({
53
67
  maxPersistedSamples: process.env.NEO_MEMORY_MAX_PERSISTED_SAMPLES,
54
68
  maxPersistedBytes: process.env.NEO_MEMORY_MAX_PERSISTED_BYTES,
55
69
  });
56
- const workspaceRuntime = createWorkspaceRuntimeManager({
57
- projectRoot: process.cwd(),
58
- workspaceRoot,
59
- registryFile: path.join(dataRoot, 'session-workspaces.json'),
70
+ // Weak references do not defeat the existing router's idle-session cleanup.
71
+ const controlRepls = new Set();
72
+ function activeControlRepls() {
73
+ const active = [];
74
+ for (const reference of controlRepls) {
75
+ const repl = reference.deref();
76
+ if (repl) active.push(repl);
77
+ else controlRepls.delete(reference);
78
+ }
79
+ return active;
80
+ }
81
+ const workspaceRuntime = createWorkspaceRuntimeManager({
82
+ projectRoot: process.cwd(),
83
+ workspaceRoot,
84
+ registryFile: path.join(dataRoot, 'session-workspaces.json'),
60
85
  createRuntime: (runtimeOptions) => createWebRuntime({
61
86
  ...runtimeOptions,
62
87
  ...pluginHost.runtimePlugins(runtimeOptions.sessionId),
@@ -106,12 +131,26 @@ await new Promise((resolve, reject) => {
106
131
  });
107
132
  });
108
133
 
134
+ // Optional Desktop-only background work starts after HTTP listen and never delays UI.
135
+ if (controlEnabled) {
136
+ const controlSync = createControlSync({
137
+ config: controlConfig,
138
+ dataDir: dataRoot,
139
+ applyProfile: embedRuntime ? createLoginApplier({ runtimeUrl: runtimeTarget, getActiveRepls: activeControlRepls }) : undefined,
140
+ }).start();
141
+ server.once('close', () => { void controlSync.stop(); });
142
+ }
143
+
109
144
  async function startEmbeddedRuntime() {
110
145
  const runtimeHost = runtimeTarget.hostname || '127.0.0.1';
111
146
  const runtimePort = runtimeTarget.port || '3101';
112
147
  await runWebServer(['--host', runtimeHost, '--port', runtimePort], {
113
148
  createRuntime: workspaceRuntime.createRuntime,
114
- createRepl: workspaceRuntime.createRepl,
149
+ createRepl(runtime) {
150
+ const repl = workspaceRuntime.createRepl(runtime);
151
+ if (controlEnabled) controlRepls.add(new WeakRef(repl));
152
+ return repl;
153
+ },
115
154
  });
116
155
  }
117
156