neoctl-web 0.1.0 → 0.1.2

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/favicon.svg CHANGED
@@ -1,3 +1,3 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
- <path d="M32 4 50 32 32 60 14 32Z" fill="#38bdf8"/>
3
- </svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <path d="M32 4 50 32 32 60 14 32Z" fill="#38bdf8"/>
3
+ </svg>
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-H7num-0s.js"></script>
23
- <link rel="stylesheet" crossorigin href="/assets/index-BSFq6wfd.css">
22
+ <script type="module" crossorigin src="/assets/index-B6NPbDJ9.js"></script>
23
+ <link rel="stylesheet" crossorigin href="/assets/index-DG8NYKAP.css">
24
24
  </head>
25
25
  <body>
26
26
  <div id="app"></div>
@@ -1,150 +1,150 @@
1
- import fsp from 'node:fs/promises';
2
- import path from 'node:path';
3
- import v8 from 'node:v8';
4
-
5
- const DEFAULT_SAMPLE_MS = 60_000;
6
- const DEFAULT_RETENTION_MS = 24 * 60 * 60_000;
7
- const DEFAULT_PUBLIC_SAMPLES = 60;
8
- const MAX_PUBLIC_SAMPLES = 60;
9
- const DEFAULT_MAX_PERSISTED_SAMPLES = 1_440;
10
- const DEFAULT_MAX_PERSISTED_BYTES = 256 * 1024;
11
-
12
- export function createMemoryMonitor({
13
- storageFile,
14
- sampleMs = DEFAULT_SAMPLE_MS,
15
- retentionMs = DEFAULT_RETENTION_MS,
16
- publicSamples = DEFAULT_PUBLIC_SAMPLES,
17
- maxPersistedSamples = DEFAULT_MAX_PERSISTED_SAMPLES,
18
- maxPersistedBytes = DEFAULT_MAX_PERSISTED_BYTES,
19
- memoryUsage = () => process.memoryUsage(),
20
- heapStatistics = () => v8.getHeapStatistics(),
21
- now = () => Date.now(),
22
- } = {}) {
23
- const normalizedSampleMs = positiveInteger(sampleMs, DEFAULT_SAMPLE_MS);
24
- const normalizedRetentionMs = Math.max(normalizedSampleMs, positiveInteger(retentionMs, DEFAULT_RETENTION_MS));
25
- const persistedSampleLimit = Math.max(2, positiveInteger(maxPersistedSamples, DEFAULT_MAX_PERSISTED_SAMPLES));
26
- const persistedByteLimit = Math.max(1_024, positiveInteger(maxPersistedBytes, DEFAULT_MAX_PERSISTED_BYTES));
27
- const maxSamples = Math.min(persistedSampleLimit, Math.max(2, Math.ceil(normalizedRetentionMs / normalizedSampleMs) + 1));
28
- const publicLimit = Math.min(MAX_PUBLIC_SAMPLES, Math.max(2, positiveInteger(publicSamples, DEFAULT_PUBLIC_SAMPLES)));
29
- let samples = [];
30
- let timer;
31
- let writeQueue = Promise.resolve();
32
-
33
- async function start() {
34
- samples = (await readSamples(storageFile, persistedByteLimit)).slice(-maxSamples);
35
- await sample();
36
- timer = setInterval(() => { void sample(); }, normalizedSampleMs);
37
- timer.unref?.();
38
- }
39
-
40
- function stop() {
41
- if (timer) clearInterval(timer);
42
- timer = undefined;
43
- }
44
-
45
- async function sample() {
46
- const timestamp = now();
47
- const usage = memoryUsage();
48
- const heap = heapStatistics();
49
- const next = {
50
- at: new Date(timestamp).toISOString(),
51
- rss: byteValue(usage.rss),
52
- heapUsed: byteValue(usage.heapUsed),
53
- heapTotal: byteValue(usage.heapTotal),
54
- heapLimit: byteValue(heap.heap_size_limit),
55
- external: byteValue(usage.external),
56
- arrayBuffers: byteValue(usage.arrayBuffers),
57
- };
58
- const cutoff = timestamp - normalizedRetentionMs;
59
- samples = [...samples, next]
60
- .filter((entry) => Date.parse(entry.at) >= cutoff)
61
- .slice(-maxSamples);
62
- const persisted = serializePersistedState({
63
- version: 1,
64
- sampleMs: normalizedSampleMs,
65
- retentionMs: normalizedRetentionMs,
66
- maxPersistedSamples: maxSamples,
67
- maxPersistedBytes: persistedByteLimit,
68
- samples,
69
- }, persistedByteLimit);
70
- samples = persisted.samples;
71
- writeQueue = writeQueue
72
- .catch(() => undefined)
73
- .then(() => writeSamples(storageFile, persisted.text));
74
- await writeQueue;
75
- return next;
76
- }
77
-
78
- function getPublicState() {
79
- return {
80
- sampleMs: normalizedSampleMs,
81
- retentionMs: normalizedRetentionMs,
82
- maxPersistedSamples: maxSamples,
83
- maxPersistedBytes: persistedByteLimit,
84
- current: samples.at(-1) || null,
85
- history: samples.slice(-publicLimit),
86
- };
87
- }
88
-
89
- return { start, stop, sample, getPublicState };
90
- }
91
-
92
- function positiveInteger(value, fallback) {
93
- const number = Number(value);
94
- return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
95
- }
96
-
97
- function byteValue(value) {
98
- const number = Number(value);
99
- return Number.isFinite(number) && number >= 0 ? Math.round(number) : 0;
100
- }
101
-
102
- async function readSamples(storageFile, maxBytes) {
103
- if (!storageFile) return [];
104
- try {
105
- const info = await fsp.stat(storageFile);
106
- if (info.size > maxBytes) {
107
- console.warn(`memory monitor data exceeds ${maxBytes} bytes; resetting history`);
108
- return [];
109
- }
110
- const parsed = JSON.parse(await fsp.readFile(storageFile, 'utf8'));
111
- return Array.isArray(parsed?.samples) ? parsed.samples.map(normalizeSample).filter(Boolean) : [];
112
- } catch (error) {
113
- if (error?.code !== 'ENOENT') console.warn(`failed to read memory monitor data: ${error.message || error}`);
114
- return [];
115
- }
116
- }
117
-
118
- function normalizeSample(value) {
119
- if (!value || typeof value !== 'object' || !Number.isFinite(Date.parse(value.at))) return null;
120
- return {
121
- at: new Date(Date.parse(value.at)).toISOString(),
122
- rss: byteValue(value.rss),
123
- heapUsed: byteValue(value.heapUsed),
124
- heapTotal: byteValue(value.heapTotal),
125
- heapLimit: byteValue(value.heapLimit),
126
- external: byteValue(value.external),
127
- arrayBuffers: byteValue(value.arrayBuffers),
128
- };
129
- }
130
-
131
- function serializePersistedState(value, maxBytes) {
132
- let samples = value.samples;
133
- let text = `${JSON.stringify({ ...value, samples })}\n`;
134
- while (Buffer.byteLength(text, 'utf8') > maxBytes && samples.length > 1) {
135
- const excessRatio = maxBytes / Buffer.byteLength(text, 'utf8');
136
- const keep = Math.max(1, Math.min(samples.length - 1, Math.floor(samples.length * excessRatio * 0.95)));
137
- samples = samples.slice(-keep);
138
- text = `${JSON.stringify({ ...value, samples })}\n`;
139
- }
140
- if (Buffer.byteLength(text, 'utf8') > maxBytes) throw new Error(`memory monitor state exceeds ${maxBytes} bytes`);
141
- return { samples, text };
142
- }
143
-
144
- async function writeSamples(storageFile, text) {
145
- if (!storageFile) return;
146
- await fsp.mkdir(path.dirname(storageFile), { recursive: true });
147
- const temporary = `${storageFile}.${process.pid}.tmp`;
148
- await fsp.writeFile(temporary, text, 'utf8');
149
- await fsp.rename(temporary, storageFile);
150
- }
1
+ import fsp from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import v8 from 'node:v8';
4
+
5
+ const DEFAULT_SAMPLE_MS = 60_000;
6
+ const DEFAULT_RETENTION_MS = 24 * 60 * 60_000;
7
+ const DEFAULT_PUBLIC_SAMPLES = 60;
8
+ const MAX_PUBLIC_SAMPLES = 60;
9
+ const DEFAULT_MAX_PERSISTED_SAMPLES = 1_440;
10
+ const DEFAULT_MAX_PERSISTED_BYTES = 256 * 1024;
11
+
12
+ export function createMemoryMonitor({
13
+ storageFile,
14
+ sampleMs = DEFAULT_SAMPLE_MS,
15
+ retentionMs = DEFAULT_RETENTION_MS,
16
+ publicSamples = DEFAULT_PUBLIC_SAMPLES,
17
+ maxPersistedSamples = DEFAULT_MAX_PERSISTED_SAMPLES,
18
+ maxPersistedBytes = DEFAULT_MAX_PERSISTED_BYTES,
19
+ memoryUsage = () => process.memoryUsage(),
20
+ heapStatistics = () => v8.getHeapStatistics(),
21
+ now = () => Date.now(),
22
+ } = {}) {
23
+ const normalizedSampleMs = positiveInteger(sampleMs, DEFAULT_SAMPLE_MS);
24
+ const normalizedRetentionMs = Math.max(normalizedSampleMs, positiveInteger(retentionMs, DEFAULT_RETENTION_MS));
25
+ const persistedSampleLimit = Math.max(2, positiveInteger(maxPersistedSamples, DEFAULT_MAX_PERSISTED_SAMPLES));
26
+ const persistedByteLimit = Math.max(1_024, positiveInteger(maxPersistedBytes, DEFAULT_MAX_PERSISTED_BYTES));
27
+ const maxSamples = Math.min(persistedSampleLimit, Math.max(2, Math.ceil(normalizedRetentionMs / normalizedSampleMs) + 1));
28
+ const publicLimit = Math.min(MAX_PUBLIC_SAMPLES, Math.max(2, positiveInteger(publicSamples, DEFAULT_PUBLIC_SAMPLES)));
29
+ let samples = [];
30
+ let timer;
31
+ let writeQueue = Promise.resolve();
32
+
33
+ async function start() {
34
+ samples = (await readSamples(storageFile, persistedByteLimit)).slice(-maxSamples);
35
+ await sample();
36
+ timer = setInterval(() => { void sample(); }, normalizedSampleMs);
37
+ timer.unref?.();
38
+ }
39
+
40
+ function stop() {
41
+ if (timer) clearInterval(timer);
42
+ timer = undefined;
43
+ }
44
+
45
+ async function sample() {
46
+ const timestamp = now();
47
+ const usage = memoryUsage();
48
+ const heap = heapStatistics();
49
+ const next = {
50
+ at: new Date(timestamp).toISOString(),
51
+ rss: byteValue(usage.rss),
52
+ heapUsed: byteValue(usage.heapUsed),
53
+ heapTotal: byteValue(usage.heapTotal),
54
+ heapLimit: byteValue(heap.heap_size_limit),
55
+ external: byteValue(usage.external),
56
+ arrayBuffers: byteValue(usage.arrayBuffers),
57
+ };
58
+ const cutoff = timestamp - normalizedRetentionMs;
59
+ samples = [...samples, next]
60
+ .filter((entry) => Date.parse(entry.at) >= cutoff)
61
+ .slice(-maxSamples);
62
+ const persisted = serializePersistedState({
63
+ version: 1,
64
+ sampleMs: normalizedSampleMs,
65
+ retentionMs: normalizedRetentionMs,
66
+ maxPersistedSamples: maxSamples,
67
+ maxPersistedBytes: persistedByteLimit,
68
+ samples,
69
+ }, persistedByteLimit);
70
+ samples = persisted.samples;
71
+ writeQueue = writeQueue
72
+ .catch(() => undefined)
73
+ .then(() => writeSamples(storageFile, persisted.text));
74
+ await writeQueue;
75
+ return next;
76
+ }
77
+
78
+ function getPublicState() {
79
+ return {
80
+ sampleMs: normalizedSampleMs,
81
+ retentionMs: normalizedRetentionMs,
82
+ maxPersistedSamples: maxSamples,
83
+ maxPersistedBytes: persistedByteLimit,
84
+ current: samples.at(-1) || null,
85
+ history: samples.slice(-publicLimit),
86
+ };
87
+ }
88
+
89
+ return { start, stop, sample, getPublicState };
90
+ }
91
+
92
+ function positiveInteger(value, fallback) {
93
+ const number = Number(value);
94
+ return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
95
+ }
96
+
97
+ function byteValue(value) {
98
+ const number = Number(value);
99
+ return Number.isFinite(number) && number >= 0 ? Math.round(number) : 0;
100
+ }
101
+
102
+ async function readSamples(storageFile, maxBytes) {
103
+ if (!storageFile) return [];
104
+ try {
105
+ const info = await fsp.stat(storageFile);
106
+ if (info.size > maxBytes) {
107
+ console.warn(`memory monitor data exceeds ${maxBytes} bytes; resetting history`);
108
+ return [];
109
+ }
110
+ const parsed = JSON.parse(await fsp.readFile(storageFile, 'utf8'));
111
+ return Array.isArray(parsed?.samples) ? parsed.samples.map(normalizeSample).filter(Boolean) : [];
112
+ } catch (error) {
113
+ if (error?.code !== 'ENOENT') console.warn(`failed to read memory monitor data: ${error.message || error}`);
114
+ return [];
115
+ }
116
+ }
117
+
118
+ function normalizeSample(value) {
119
+ if (!value || typeof value !== 'object' || !Number.isFinite(Date.parse(value.at))) return null;
120
+ return {
121
+ at: new Date(Date.parse(value.at)).toISOString(),
122
+ rss: byteValue(value.rss),
123
+ heapUsed: byteValue(value.heapUsed),
124
+ heapTotal: byteValue(value.heapTotal),
125
+ heapLimit: byteValue(value.heapLimit),
126
+ external: byteValue(value.external),
127
+ arrayBuffers: byteValue(value.arrayBuffers),
128
+ };
129
+ }
130
+
131
+ function serializePersistedState(value, maxBytes) {
132
+ let samples = value.samples;
133
+ let text = `${JSON.stringify({ ...value, samples })}\n`;
134
+ while (Buffer.byteLength(text, 'utf8') > maxBytes && samples.length > 1) {
135
+ const excessRatio = maxBytes / Buffer.byteLength(text, 'utf8');
136
+ const keep = Math.max(1, Math.min(samples.length - 1, Math.floor(samples.length * excessRatio * 0.95)));
137
+ samples = samples.slice(-keep);
138
+ text = `${JSON.stringify({ ...value, samples })}\n`;
139
+ }
140
+ if (Buffer.byteLength(text, 'utf8') > maxBytes) throw new Error(`memory monitor state exceeds ${maxBytes} bytes`);
141
+ return { samples, text };
142
+ }
143
+
144
+ async function writeSamples(storageFile, text) {
145
+ if (!storageFile) return;
146
+ await fsp.mkdir(path.dirname(storageFile), { recursive: true });
147
+ const temporary = `${storageFile}.${process.pid}.tmp`;
148
+ await fsp.writeFile(temporary, text, 'utf8');
149
+ await fsp.rename(temporary, storageFile);
150
+ }
package/package.json CHANGED
@@ -1,64 +1,65 @@
1
- {
2
- "name": "neoctl-web",
3
- "version": "0.1.0",
4
- "description": "Neo browser workspace with an embedded agent runtime.",
5
- "type": "module",
6
- "engines": {
7
- "node": ">=20"
8
- },
9
- "scripts": {
10
- "predev": "npm --prefix ../engine run build",
11
- "dev": "node scripts/dev.mjs --core local",
12
- "dev:package": "node scripts/dev.mjs --core package",
13
- "dev:ui": "vite",
14
- "test:xhs": "node --test artifacts.test.mjs",
15
- "test:runtime": "node --test runtime-router-cleanup.test.mjs runtime-workspaces.test.mjs",
16
- "test:plugins": "npm --prefix ../engine run build && node --test plugins.test.mjs plugin-settings.test.mjs tool-settings.test.mjs",
17
- "test:monitoring": "node --test cpa-quota.test.mjs memory-monitor.test.mjs",
18
- "build": "vite build",
19
- "test:cli": "node --test neow.test.mjs",
20
- "prestart": "npm run build",
21
- "start": "node server.mjs",
22
- "preview": "vite preview",
23
- "neo": "neo",
24
- "neo:web": "neo -web",
25
- "neo:login": "neo -login",
26
- "neo:help": "neo -help",
27
- "prepack": "npm run build && npm run test:cli"
28
- },
29
- "dependencies": {
30
- "@tanstack/vue-virtual": "^3.13.36",
31
- "highlight.js": "^11.11.1",
32
- "marked": "^18.0.3",
33
- "neoctl": "0.2.31",
34
- "streaming-markdown": "^0.2.15",
35
- "vue": "^3.5.13"
36
- },
37
- "devDependencies": {
38
- "@vitejs/plugin-vue": "^5.2.4",
39
- "vite": "^5.4.21"
40
- },
41
- "bin": {
42
- "neow": "bin/neow.mjs"
43
- },
44
- "files": [
45
- "bin/neow.mjs",
46
- "dist",
47
- "plugins",
48
- "server.mjs",
49
- "core-runtime.mjs",
50
- "plugins.mjs",
1
+ {
2
+ "name": "neoctl-web",
3
+ "version": "0.1.2",
4
+ "description": "Neo browser workspace with an embedded agent runtime.",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "scripts": {
10
+ "predev": "npm --prefix ../engine run build",
11
+ "dev": "node scripts/dev.mjs --core local",
12
+ "dev:package": "node scripts/dev.mjs --core package",
13
+ "dev:ui": "vite",
14
+ "test:xhs": "node --test artifacts.test.mjs",
15
+ "test:runtime": "node --test runtime-router-cleanup.test.mjs runtime-workspaces.test.mjs",
16
+ "test:plugins": "npm --prefix ../engine run build && node --test plugins.test.mjs plugin-settings.test.mjs tool-settings.test.mjs",
17
+ "test:monitoring": "node --test cpa-quota.test.mjs memory-monitor.test.mjs",
18
+ "build": "vite build",
19
+ "test:cli": "node --test neow.test.mjs",
20
+ "prestart": "npm run build",
21
+ "start": "node server.mjs",
22
+ "preview": "vite preview",
23
+ "neo": "neo",
24
+ "neo:web": "neo -web",
25
+ "neo:login": "neo -login",
26
+ "neo:help": "neo -help",
27
+ "prepack": "npm run build && npm run test:cli"
28
+ },
29
+ "dependencies": {
30
+ "@tanstack/vue-virtual": "^3.13.36",
31
+ "highlight.js": "^11.11.1",
32
+ "marked": "^18.0.3",
33
+ "neoctl": "0.2.31",
34
+ "streaming-markdown": "^0.2.15",
35
+ "vue": "^3.5.13"
36
+ },
37
+ "devDependencies": {
38
+ "@vitejs/plugin-vue": "^5.2.4",
39
+ "vite": "^5.4.21"
40
+ },
41
+ "bin": {
42
+ "neow": "bin/neow.mjs"
43
+ },
44
+ "files": [
45
+ "bin/neow.mjs",
46
+ "dist",
47
+ "plugins",
48
+ "server.mjs",
49
+ "core-runtime.mjs",
50
+ "plugins.mjs",
51
51
  "plugin-settings.mjs",
52
+ "platform-paths.mjs",
52
53
  "tool-settings.mjs",
53
- "runtime-workspaces.mjs",
54
- "runtime-router-cleanup.mjs",
55
- "cpa-quota.mjs",
56
- "memory-monitor.mjs",
57
- "README.md"
58
- ],
59
- "publishConfig": {
60
- "access": "public"
61
- },
62
- "keywords": ["neoctl", "agent", "vue", "web"],
63
- "license": "Apache-2.0"
64
- }
54
+ "runtime-workspaces.mjs",
55
+ "runtime-router-cleanup.mjs",
56
+ "cpa-quota.mjs",
57
+ "memory-monitor.mjs",
58
+ "README.md"
59
+ ],
60
+ "publishConfig": {
61
+ "access": "public"
62
+ },
63
+ "keywords": ["neoctl", "agent", "vue", "web"],
64
+ "license": "Apache-2.0"
65
+ }
@@ -0,0 +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,67 +1,67 @@
1
- import fsp from 'node:fs/promises';
2
- import path from 'node:path';
3
-
4
- export async function createWebPluginSettings(storageFile) {
5
- let state = await readState(storageFile);
6
- let writeQueue = Promise.resolve();
7
-
8
- function snapshot() {
9
- return structuredClone(state);
10
- }
11
-
12
- async function update(next) {
13
- state = next;
14
- writeQueue = writeQueue.catch(() => undefined).then(() => writeState(storageFile, state));
15
- await writeQueue;
16
- }
17
-
18
- return {
19
- snapshot,
20
- globalEnabledIds() {
21
- return Array.isArray(state.globalEnabled) ? [...state.globalEnabled] : undefined;
22
- },
23
- sessionOverrides(sessionId) {
24
- const value = state.sessions[String(sessionId || '')];
25
- return value && typeof value === 'object' ? { ...value } : {};
26
- },
27
- async setGlobalEnabled(ids) {
28
- await update({ ...state, globalEnabled: [...new Set(ids)].sort() });
29
- },
30
- async setSessionOverrides(sessionId, overrides) {
31
- const id = String(sessionId || '').trim();
32
- if (!id) throw new Error('session id is required');
33
- const sessions = { ...state.sessions };
34
- const normalized = Object.fromEntries(Object.entries(overrides).filter(([, value]) => typeof value === 'boolean'));
35
- if (Object.keys(normalized).length) sessions[id] = normalized;
36
- else delete sessions[id];
37
- await update({ ...state, sessions });
38
- },
39
- };
40
- }
41
-
42
- async function readState(storageFile) {
43
- if (!storageFile) return emptyState();
44
- try {
45
- const parsed = JSON.parse(await fsp.readFile(storageFile, 'utf8'));
46
- return {
47
- version: 1,
48
- globalEnabled: Array.isArray(parsed?.globalEnabled) ? parsed.globalEnabled.map(String) : undefined,
49
- sessions: parsed?.sessions && typeof parsed.sessions === 'object' && !Array.isArray(parsed.sessions) ? parsed.sessions : {},
50
- };
51
- } catch (error) {
52
- if (error?.code !== 'ENOENT') console.warn(`failed to read web plugin settings: ${error.message || error}`);
53
- return emptyState();
54
- }
55
- }
56
-
57
- function emptyState() {
58
- return { version: 1, globalEnabled: undefined, sessions: {} };
59
- }
60
-
61
- async function writeState(storageFile, state) {
62
- if (!storageFile) return;
63
- await fsp.mkdir(path.dirname(storageFile), { recursive: true });
64
- const temporary = `${storageFile}.${process.pid}.tmp`;
65
- await fsp.writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
66
- await fsp.rename(temporary, storageFile);
67
- }
1
+ import fsp from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ export async function createWebPluginSettings(storageFile) {
5
+ let state = await readState(storageFile);
6
+ let writeQueue = Promise.resolve();
7
+
8
+ function snapshot() {
9
+ return structuredClone(state);
10
+ }
11
+
12
+ async function update(next) {
13
+ state = next;
14
+ writeQueue = writeQueue.catch(() => undefined).then(() => writeState(storageFile, state));
15
+ await writeQueue;
16
+ }
17
+
18
+ return {
19
+ snapshot,
20
+ globalEnabledIds() {
21
+ return Array.isArray(state.globalEnabled) ? [...state.globalEnabled] : undefined;
22
+ },
23
+ sessionOverrides(sessionId) {
24
+ const value = state.sessions[String(sessionId || '')];
25
+ return value && typeof value === 'object' ? { ...value } : {};
26
+ },
27
+ async setGlobalEnabled(ids) {
28
+ await update({ ...state, globalEnabled: [...new Set(ids)].sort() });
29
+ },
30
+ async setSessionOverrides(sessionId, overrides) {
31
+ const id = String(sessionId || '').trim();
32
+ if (!id) throw new Error('session id is required');
33
+ const sessions = { ...state.sessions };
34
+ const normalized = Object.fromEntries(Object.entries(overrides).filter(([, value]) => typeof value === 'boolean'));
35
+ if (Object.keys(normalized).length) sessions[id] = normalized;
36
+ else delete sessions[id];
37
+ await update({ ...state, sessions });
38
+ },
39
+ };
40
+ }
41
+
42
+ async function readState(storageFile) {
43
+ if (!storageFile) return emptyState();
44
+ try {
45
+ const parsed = JSON.parse(await fsp.readFile(storageFile, 'utf8'));
46
+ return {
47
+ version: 1,
48
+ globalEnabled: Array.isArray(parsed?.globalEnabled) ? parsed.globalEnabled.map(String) : undefined,
49
+ sessions: parsed?.sessions && typeof parsed.sessions === 'object' && !Array.isArray(parsed.sessions) ? parsed.sessions : {},
50
+ };
51
+ } catch (error) {
52
+ if (error?.code !== 'ENOENT') console.warn(`failed to read web plugin settings: ${error.message || error}`);
53
+ return emptyState();
54
+ }
55
+ }
56
+
57
+ function emptyState() {
58
+ return { version: 1, globalEnabled: undefined, sessions: {} };
59
+ }
60
+
61
+ async function writeState(storageFile, state) {
62
+ if (!storageFile) return;
63
+ await fsp.mkdir(path.dirname(storageFile), { recursive: true });
64
+ const temporary = `${storageFile}.${process.pid}.tmp`;
65
+ await fsp.writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
66
+ await fsp.rename(temporary, storageFile);
67
+ }