@livedesk/hub 0.1.40 → 0.1.42

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/hub",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
4
4
  "description": "LiveDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@ffmpeg-installer/ffmpeg": "^1.1.0",
19
- "@livedesk/runtime-core": "0.1.5",
19
+ "@livedesk/runtime-core": "0.1.5",
20
20
  "@openai/codex-sdk": "0.145.0",
21
21
  "cors": "^2.8.5",
22
22
  "express": "^4.21.2",
@@ -1,29 +1,29 @@
1
- function normalizeIds(value) {
2
- const source = Array.isArray(value) ? value : [];
3
- return [...new Set(source.map(item => String(item || '').trim()).filter(Boolean))].slice(0, 500);
4
- }
5
-
6
- export function createAgentDeviceScope(allowedDeviceIds, fallbackDeviceIds = []) {
7
- const requested = normalizeIds(allowedDeviceIds);
8
- const fallback = normalizeIds(fallbackDeviceIds);
9
- return new Set(requested.length > 0 ? requested : fallback);
10
- }
11
-
12
- export function selectAgentDeviceIds({ allowedDeviceIds, requestedDeviceIds, connectedDeviceIds }) {
13
- const allowed = allowedDeviceIds instanceof Set ? allowedDeviceIds : new Set(normalizeIds(allowedDeviceIds));
14
- const requested = normalizeIds(requestedDeviceIds);
15
- const connected = new Set(normalizeIds(connectedDeviceIds));
16
- const candidates = requested.length > 0 ? requested : [...allowed];
17
- return candidates.filter(deviceId => allowed.has(deviceId) && connected.has(deviceId));
18
- }
19
-
20
- export function resolveAgentTargetIds({ allowedDeviceIds, requestedDeviceIds, connectedDeviceIds }) {
21
- const requested = normalizeIds(requestedDeviceIds);
22
- const allowed = allowedDeviceIds instanceof Set ? allowedDeviceIds : new Set(normalizeIds(allowedDeviceIds));
23
- const deviceIds = selectAgentDeviceIds({ allowedDeviceIds: allowed, requestedDeviceIds: requested, connectedDeviceIds });
24
- if (deviceIds.length > 0) return { deviceIds, error: '' };
25
- if (requested.length > 0 && requested.every(deviceId => !allowed.has(deviceId))) {
26
- return { deviceIds: [], error: 'agent-target-outside-selection' };
27
- }
28
- return { deviceIds: [], error: 'agent-no-target-devices' };
29
- }
1
+ function normalizeIds(value) {
2
+ const source = Array.isArray(value) ? value : [];
3
+ return [...new Set(source.map(item => String(item || '').trim()).filter(Boolean))].slice(0, 500);
4
+ }
5
+
6
+ export function createAgentDeviceScope(allowedDeviceIds, fallbackDeviceIds = []) {
7
+ const requested = normalizeIds(allowedDeviceIds);
8
+ const fallback = normalizeIds(fallbackDeviceIds);
9
+ return new Set(requested.length > 0 ? requested : fallback);
10
+ }
11
+
12
+ export function selectAgentDeviceIds({ allowedDeviceIds, requestedDeviceIds, connectedDeviceIds }) {
13
+ const allowed = allowedDeviceIds instanceof Set ? allowedDeviceIds : new Set(normalizeIds(allowedDeviceIds));
14
+ const requested = normalizeIds(requestedDeviceIds);
15
+ const connected = new Set(normalizeIds(connectedDeviceIds));
16
+ const candidates = requested.length > 0 ? requested : [...allowed];
17
+ return candidates.filter(deviceId => allowed.has(deviceId) && connected.has(deviceId));
18
+ }
19
+
20
+ export function resolveAgentTargetIds({ allowedDeviceIds, requestedDeviceIds, connectedDeviceIds }) {
21
+ const requested = normalizeIds(requestedDeviceIds);
22
+ const allowed = allowedDeviceIds instanceof Set ? allowedDeviceIds : new Set(normalizeIds(allowedDeviceIds));
23
+ const deviceIds = selectAgentDeviceIds({ allowedDeviceIds: allowed, requestedDeviceIds: requested, connectedDeviceIds });
24
+ if (deviceIds.length > 0) return { deviceIds, error: '' };
25
+ if (requested.length > 0 && requested.every(deviceId => !allowed.has(deviceId))) {
26
+ return { deviceIds: [], error: 'agent-target-outside-selection' };
27
+ }
28
+ return { deviceIds: [], error: 'agent-no-target-devices' };
29
+ }
@@ -1,103 +1,103 @@
1
- import os from 'node:os';
2
- import path from 'node:path';
3
- import { AgentSettingsStore } from './agent-settings.js';
4
- import { AgentRuntimeError } from './agent-runtime-error.js';
5
-
6
- function publicCodexStatus(status = {}) {
7
- return {
8
- installed: status.installed === true,
9
- authenticated: ['signed-in', 'not-signed-in'].includes(status.authenticated) ? status.authenticated : 'unknown',
10
- status: String(status.status || 'unknown').slice(0, 40),
11
- codexPath: String(status.codexPath || '').slice(0, 500),
12
- detail: String(status.detail || '').slice(0, 300)
13
- };
14
- }
15
-
16
- function publicCodexConnection(result = {}) {
17
- const latencyMs = Number(result.latencyMs);
18
- return {
19
- ok: result.ok === true,
20
- latencyMs: Number.isFinite(latencyMs) ? Math.max(0, Math.round(latencyMs)) : 0,
21
- threadId: String(result.threadId || '').slice(0, 200)
22
- };
23
- }
24
-
25
- export function createAgentManager({
26
- dataDir = path.join(os.homedir(), '.livedesk'),
27
- settingsStore,
28
- codexRuntime
29
- } = {}) {
30
- const settings = settingsStore || new AgentSettingsStore({ dataDir });
31
- const runtime = codexRuntime;
32
- let statusCache = { expiresAt: 0, value: null };
33
- let statusRefreshPromise = null;
34
- let statusRefreshGeneration = 0;
35
-
36
- async function getCodexStatus() {
37
- if (!runtime) return { installed: false, authenticated: 'unknown', status: 'unavailable', codexPath: '' };
38
- if (statusCache.value && statusCache.expiresAt > Date.now()) return statusCache.value;
39
- if (statusRefreshPromise) return statusRefreshPromise;
40
- const generation = statusRefreshGeneration;
41
- const refresh = (async () => {
42
- let value;
43
- try {
44
- value = publicCodexStatus(await runtime.getStatus());
45
- } catch (error) {
46
- value = publicCodexStatus({
47
- installed: true,
48
- authenticated: 'unknown',
49
- status: error?.code || 'security-unavailable',
50
- detail: error?.message || 'Codex security isolation is unavailable.'
51
- });
52
- }
53
- if (generation === statusRefreshGeneration) {
54
- statusCache = { value, expiresAt: Date.now() + 10000 };
55
- }
56
- return value;
57
- })();
58
- statusRefreshPromise = refresh;
59
- try {
60
- return await refresh;
61
- } finally {
62
- if (statusRefreshPromise === refresh) statusRefreshPromise = null;
63
- }
64
- }
65
-
66
- async function publicSettings() {
67
- const current = await settings.get();
68
- const codex = await getCodexStatus();
69
- return {
70
- enabled: current.enabled === true,
71
- codexInstallation: codex.installed ? 'installed' : 'not-installed',
72
- codexAuth: codex.authenticated,
73
- codexStatus: codex.status
74
- };
75
- }
76
-
77
- return {
78
- getSettings: publicSettings,
79
- async testConnection() {
80
- if (!runtime) throw new AgentRuntimeError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
81
- statusCache = { expiresAt: 0, value: null };
82
- statusRefreshGeneration += 1;
83
- statusRefreshPromise = null;
84
- return publicCodexConnection(await runtime.testConnection());
85
- },
86
- async createSummary(input) {
87
- const source = input && typeof input === 'object' ? input : {};
88
- const results = Array.isArray(source.results) ? source.results : [];
89
- return {
90
- summary: String(results.length ? `${source.completed || 0} completed, ${source.failed || 0} failed.` : 'LiveDesk Agent run completed.').slice(0, 1200)
91
- };
92
- },
93
- async startRun(input) {
94
- if (!runtime) {
95
- throw new AgentRuntimeError('agent-codex-not-active', 'Codex SDK is not available for LiveDesk Agent commands.', { status: 409 });
96
- }
97
- return runtime.start(input);
98
- },
99
- getRun(runId) { return runtime?.get(runId) || null; },
100
- cancelRun(runId) { return runtime?.cancel(runId) || null; },
101
- cancelAllRuns() { return runtime?.cancelAll?.() || 0; }
102
- };
103
- }
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { AgentSettingsStore } from './agent-settings.js';
4
+ import { AgentRuntimeError } from './agent-runtime-error.js';
5
+
6
+ function publicCodexStatus(status = {}) {
7
+ return {
8
+ installed: status.installed === true,
9
+ authenticated: ['signed-in', 'not-signed-in'].includes(status.authenticated) ? status.authenticated : 'unknown',
10
+ status: String(status.status || 'unknown').slice(0, 40),
11
+ codexPath: String(status.codexPath || '').slice(0, 500),
12
+ detail: String(status.detail || '').slice(0, 300)
13
+ };
14
+ }
15
+
16
+ function publicCodexConnection(result = {}) {
17
+ const latencyMs = Number(result.latencyMs);
18
+ return {
19
+ ok: result.ok === true,
20
+ latencyMs: Number.isFinite(latencyMs) ? Math.max(0, Math.round(latencyMs)) : 0,
21
+ threadId: String(result.threadId || '').slice(0, 200)
22
+ };
23
+ }
24
+
25
+ export function createAgentManager({
26
+ dataDir = path.join(os.homedir(), '.livedesk'),
27
+ settingsStore,
28
+ codexRuntime
29
+ } = {}) {
30
+ const settings = settingsStore || new AgentSettingsStore({ dataDir });
31
+ const runtime = codexRuntime;
32
+ let statusCache = { expiresAt: 0, value: null };
33
+ let statusRefreshPromise = null;
34
+ let statusRefreshGeneration = 0;
35
+
36
+ async function getCodexStatus() {
37
+ if (!runtime) return { installed: false, authenticated: 'unknown', status: 'unavailable', codexPath: '' };
38
+ if (statusCache.value && statusCache.expiresAt > Date.now()) return statusCache.value;
39
+ if (statusRefreshPromise) return statusRefreshPromise;
40
+ const generation = statusRefreshGeneration;
41
+ const refresh = (async () => {
42
+ let value;
43
+ try {
44
+ value = publicCodexStatus(await runtime.getStatus());
45
+ } catch (error) {
46
+ value = publicCodexStatus({
47
+ installed: true,
48
+ authenticated: 'unknown',
49
+ status: error?.code || 'security-unavailable',
50
+ detail: error?.message || 'Codex security isolation is unavailable.'
51
+ });
52
+ }
53
+ if (generation === statusRefreshGeneration) {
54
+ statusCache = { value, expiresAt: Date.now() + 10000 };
55
+ }
56
+ return value;
57
+ })();
58
+ statusRefreshPromise = refresh;
59
+ try {
60
+ return await refresh;
61
+ } finally {
62
+ if (statusRefreshPromise === refresh) statusRefreshPromise = null;
63
+ }
64
+ }
65
+
66
+ async function publicSettings() {
67
+ const current = await settings.get();
68
+ const codex = await getCodexStatus();
69
+ return {
70
+ enabled: current.enabled === true,
71
+ codexInstallation: codex.installed ? 'installed' : 'not-installed',
72
+ codexAuth: codex.authenticated,
73
+ codexStatus: codex.status
74
+ };
75
+ }
76
+
77
+ return {
78
+ getSettings: publicSettings,
79
+ async testConnection() {
80
+ if (!runtime) throw new AgentRuntimeError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
81
+ statusCache = { expiresAt: 0, value: null };
82
+ statusRefreshGeneration += 1;
83
+ statusRefreshPromise = null;
84
+ return publicCodexConnection(await runtime.testConnection());
85
+ },
86
+ async createSummary(input) {
87
+ const source = input && typeof input === 'object' ? input : {};
88
+ const results = Array.isArray(source.results) ? source.results : [];
89
+ return {
90
+ summary: String(results.length ? `${source.completed || 0} completed, ${source.failed || 0} failed.` : 'LiveDesk Agent run completed.').slice(0, 1200)
91
+ };
92
+ },
93
+ async startRun(input) {
94
+ if (!runtime) {
95
+ throw new AgentRuntimeError('agent-codex-not-active', 'Codex SDK is not available for LiveDesk Agent commands.', { status: 409 });
96
+ }
97
+ return runtime.start(input);
98
+ },
99
+ getRun(runId) { return runtime?.get(runId) || null; },
100
+ cancelRun(runId) { return runtime?.cancel(runId) || null; },
101
+ cancelAllRuns() { return runtime?.cancelAll?.() || 0; }
102
+ };
103
+ }
@@ -1,78 +1,78 @@
1
- import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import crypto from 'node:crypto';
5
- import { DEFAULT_CUSTOM_AGENT_PERMISSION_CATEGORIES, normalizeAgentPermissionCategories } from './agent-permissions.js';
6
-
7
- function safePolicyId(value) {
8
- const id = String(value || '').trim().replace(/[^a-zA-Z0-9_.:-]/g, '-').slice(0, 80);
9
- return id || 'custom-default';
10
- }
11
-
12
- function publicPolicy(policy) {
13
- return {
14
- id: policy.id,
15
- name: policy.name,
16
- categories: { ...policy.categories },
17
- createdAt: policy.createdAt,
18
- updatedAt: policy.updatedAt
19
- };
20
- }
21
-
22
- export function createAgentPermissionStore({ dataDir = path.join(os.homedir(), '.livedesk') } = {}) {
23
- const filePath = path.join(dataDir, 'agent-permission-policies.json');
24
- let policies;
25
-
26
- async function load() {
27
- if (policies) return policies;
28
- try {
29
- const parsed = JSON.parse(await readFile(filePath, 'utf8'));
30
- policies = new Map(Object.entries(parsed && typeof parsed === 'object' ? parsed : {}).map(([id, value]) => [safePolicyId(id), {
31
- id: safePolicyId(id),
32
- name: String(value?.name || id).slice(0, 120),
33
- categories: normalizeAgentPermissionCategories(value?.categories, DEFAULT_CUSTOM_AGENT_PERMISSION_CATEGORIES),
34
- createdAt: String(value?.createdAt || new Date().toISOString()),
35
- updatedAt: String(value?.updatedAt || new Date().toISOString())
36
- }]));
37
- } catch {
38
- policies = new Map();
39
- }
40
- if (!policies.has('custom-default')) {
41
- const now = new Date().toISOString();
42
- policies.set('custom-default', { id: 'custom-default', name: 'Custom default', categories: { ...DEFAULT_CUSTOM_AGENT_PERMISSION_CATEGORIES }, createdAt: now, updatedAt: now });
43
- }
44
- return policies;
45
- }
46
-
47
- async function save() {
48
- const current = await load();
49
- const value = Object.fromEntries([...current].map(([id, policy]) => [id, policy]));
50
- await mkdir(path.dirname(filePath), { recursive: true });
51
- const temporaryPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
52
- await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
53
- await rename(temporaryPath, filePath);
54
- }
55
-
56
- return {
57
- async list() { return [...await load()].map(([, policy]) => publicPolicy(policy)); },
58
- async get(id = 'custom-default') {
59
- const current = await load();
60
- return publicPolicy(current.get(safePolicyId(id)) || current.get('custom-default'));
61
- },
62
- async upsert({ id = 'custom-default', name = 'Custom default', categories = {} } = {}) {
63
- const current = await load();
64
- const policyId = safePolicyId(id);
65
- const previous = current.get(policyId);
66
- const now = new Date().toISOString();
67
- current.set(policyId, {
68
- id: policyId,
69
- name: String(name || policyId).slice(0, 120),
70
- categories: normalizeAgentPermissionCategories(categories, previous?.categories || DEFAULT_CUSTOM_AGENT_PERMISSION_CATEGORIES),
71
- createdAt: previous?.createdAt || now,
72
- updatedAt: now
73
- });
74
- await save();
75
- return publicPolicy(current.get(policyId));
76
- }
77
- };
78
- }
1
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import crypto from 'node:crypto';
5
+ import { DEFAULT_CUSTOM_AGENT_PERMISSION_CATEGORIES, normalizeAgentPermissionCategories } from './agent-permissions.js';
6
+
7
+ function safePolicyId(value) {
8
+ const id = String(value || '').trim().replace(/[^a-zA-Z0-9_.:-]/g, '-').slice(0, 80);
9
+ return id || 'custom-default';
10
+ }
11
+
12
+ function publicPolicy(policy) {
13
+ return {
14
+ id: policy.id,
15
+ name: policy.name,
16
+ categories: { ...policy.categories },
17
+ createdAt: policy.createdAt,
18
+ updatedAt: policy.updatedAt
19
+ };
20
+ }
21
+
22
+ export function createAgentPermissionStore({ dataDir = path.join(os.homedir(), '.livedesk') } = {}) {
23
+ const filePath = path.join(dataDir, 'agent-permission-policies.json');
24
+ let policies;
25
+
26
+ async function load() {
27
+ if (policies) return policies;
28
+ try {
29
+ const parsed = JSON.parse(await readFile(filePath, 'utf8'));
30
+ policies = new Map(Object.entries(parsed && typeof parsed === 'object' ? parsed : {}).map(([id, value]) => [safePolicyId(id), {
31
+ id: safePolicyId(id),
32
+ name: String(value?.name || id).slice(0, 120),
33
+ categories: normalizeAgentPermissionCategories(value?.categories, DEFAULT_CUSTOM_AGENT_PERMISSION_CATEGORIES),
34
+ createdAt: String(value?.createdAt || new Date().toISOString()),
35
+ updatedAt: String(value?.updatedAt || new Date().toISOString())
36
+ }]));
37
+ } catch {
38
+ policies = new Map();
39
+ }
40
+ if (!policies.has('custom-default')) {
41
+ const now = new Date().toISOString();
42
+ policies.set('custom-default', { id: 'custom-default', name: 'Custom default', categories: { ...DEFAULT_CUSTOM_AGENT_PERMISSION_CATEGORIES }, createdAt: now, updatedAt: now });
43
+ }
44
+ return policies;
45
+ }
46
+
47
+ async function save() {
48
+ const current = await load();
49
+ const value = Object.fromEntries([...current].map(([id, policy]) => [id, policy]));
50
+ await mkdir(path.dirname(filePath), { recursive: true });
51
+ const temporaryPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
52
+ await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
53
+ await rename(temporaryPath, filePath);
54
+ }
55
+
56
+ return {
57
+ async list() { return [...await load()].map(([, policy]) => publicPolicy(policy)); },
58
+ async get(id = 'custom-default') {
59
+ const current = await load();
60
+ return publicPolicy(current.get(safePolicyId(id)) || current.get('custom-default'));
61
+ },
62
+ async upsert({ id = 'custom-default', name = 'Custom default', categories = {} } = {}) {
63
+ const current = await load();
64
+ const policyId = safePolicyId(id);
65
+ const previous = current.get(policyId);
66
+ const now = new Date().toISOString();
67
+ current.set(policyId, {
68
+ id: policyId,
69
+ name: String(name || policyId).slice(0, 120),
70
+ categories: normalizeAgentPermissionCategories(categories, previous?.categories || DEFAULT_CUSTOM_AGENT_PERMISSION_CATEGORIES),
71
+ createdAt: previous?.createdAt || now,
72
+ updatedAt: now
73
+ });
74
+ await save();
75
+ return publicPolicy(current.get(policyId));
76
+ }
77
+ };
78
+ }
@@ -1,9 +1,9 @@
1
- export class AgentRuntimeError extends Error {
2
- constructor(code, message, { status = 502, retryable = false } = {}) {
3
- super(message);
4
- this.name = 'AgentRuntimeError';
5
- this.code = code;
6
- this.status = status;
7
- this.retryable = retryable;
8
- }
9
- }
1
+ export class AgentRuntimeError extends Error {
2
+ constructor(code, message, { status = 502, retryable = false } = {}) {
3
+ super(message);
4
+ this.name = 'AgentRuntimeError';
5
+ this.code = code;
6
+ this.status = status;
7
+ this.retryable = retryable;
8
+ }
9
+ }
@@ -1,71 +1,71 @@
1
- import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
-
5
- export const DEFAULT_AGENT_SETTINGS = Object.freeze({
6
- settingsVersion: 5,
7
- enabled: true,
8
- codexTaskTimeoutMs: 600000,
9
- codexMaxTurns: 12,
10
- codexMaxToolCalls: 20,
11
- codexResumeSessions: true,
12
- maxConcurrentRequests: 2
13
- });
14
-
15
- function booleanValue(value, fallback) {
16
- return typeof value === 'boolean' ? value : fallback;
17
- }
18
-
19
- function numberValue(value, min, max, fallback, integer = false) {
20
- const number = Number(value);
21
- if (!Number.isFinite(number)) return fallback;
22
- const clamped = Math.max(min, Math.min(max, number));
23
- return integer ? Math.round(clamped) : clamped;
24
- }
25
-
26
- export function normalizeAgentSettings(value = {}) {
27
- const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
28
- return {
29
- settingsVersion: 5,
30
- enabled: booleanValue(source.enabled, DEFAULT_AGENT_SETTINGS.enabled),
31
- codexTaskTimeoutMs: numberValue(source.codexTaskTimeoutMs, 30000, 1800000, DEFAULT_AGENT_SETTINGS.codexTaskTimeoutMs, true),
32
- codexMaxTurns: numberValue(source.codexMaxTurns, 1, 40, DEFAULT_AGENT_SETTINGS.codexMaxTurns, true),
33
- codexMaxToolCalls: numberValue(source.codexMaxToolCalls, 1, 100, DEFAULT_AGENT_SETTINGS.codexMaxToolCalls, true),
34
- codexResumeSessions: booleanValue(source.codexResumeSessions, DEFAULT_AGENT_SETTINGS.codexResumeSessions),
35
- maxConcurrentRequests: numberValue(source.maxConcurrentRequests, 1, 8, DEFAULT_AGENT_SETTINGS.maxConcurrentRequests, true)
36
- };
37
- }
38
-
39
- async function writeJsonAtomic(filePath, value) {
40
- await mkdir(path.dirname(filePath), { recursive: true });
41
- const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
42
- await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
43
- await rename(temporaryPath, filePath);
44
- }
45
-
46
- export class AgentSettingsStore {
47
- constructor({ dataDir = path.join(os.homedir(), '.livedesk') } = {}) {
48
- this.filePath = path.join(dataDir, 'agent-settings.json');
49
- this.settings = null;
50
- }
51
-
52
- async get() {
53
- if (this.settings) return { ...this.settings };
54
- try {
55
- const raw = await readFile(this.filePath, 'utf8');
56
- this.settings = normalizeAgentSettings(JSON.parse(raw));
57
- } catch {
58
- this.settings = normalizeAgentSettings(DEFAULT_AGENT_SETTINGS);
59
- }
60
- return { ...this.settings };
61
- }
62
-
63
- async update(patch = {}) {
64
- const current = await this.get();
65
- const next = normalizeAgentSettings({ ...current, ...patch });
66
- await writeJsonAtomic(this.filePath, next);
67
- this.settings = next;
68
- return { ...next };
69
- }
70
-
71
- }
1
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ export const DEFAULT_AGENT_SETTINGS = Object.freeze({
6
+ settingsVersion: 5,
7
+ enabled: true,
8
+ codexTaskTimeoutMs: 600000,
9
+ codexMaxTurns: 12,
10
+ codexMaxToolCalls: 20,
11
+ codexResumeSessions: true,
12
+ maxConcurrentRequests: 2
13
+ });
14
+
15
+ function booleanValue(value, fallback) {
16
+ return typeof value === 'boolean' ? value : fallback;
17
+ }
18
+
19
+ function numberValue(value, min, max, fallback, integer = false) {
20
+ const number = Number(value);
21
+ if (!Number.isFinite(number)) return fallback;
22
+ const clamped = Math.max(min, Math.min(max, number));
23
+ return integer ? Math.round(clamped) : clamped;
24
+ }
25
+
26
+ export function normalizeAgentSettings(value = {}) {
27
+ const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
28
+ return {
29
+ settingsVersion: 5,
30
+ enabled: booleanValue(source.enabled, DEFAULT_AGENT_SETTINGS.enabled),
31
+ codexTaskTimeoutMs: numberValue(source.codexTaskTimeoutMs, 30000, 1800000, DEFAULT_AGENT_SETTINGS.codexTaskTimeoutMs, true),
32
+ codexMaxTurns: numberValue(source.codexMaxTurns, 1, 40, DEFAULT_AGENT_SETTINGS.codexMaxTurns, true),
33
+ codexMaxToolCalls: numberValue(source.codexMaxToolCalls, 1, 100, DEFAULT_AGENT_SETTINGS.codexMaxToolCalls, true),
34
+ codexResumeSessions: booleanValue(source.codexResumeSessions, DEFAULT_AGENT_SETTINGS.codexResumeSessions),
35
+ maxConcurrentRequests: numberValue(source.maxConcurrentRequests, 1, 8, DEFAULT_AGENT_SETTINGS.maxConcurrentRequests, true)
36
+ };
37
+ }
38
+
39
+ async function writeJsonAtomic(filePath, value) {
40
+ await mkdir(path.dirname(filePath), { recursive: true });
41
+ const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
42
+ await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
43
+ await rename(temporaryPath, filePath);
44
+ }
45
+
46
+ export class AgentSettingsStore {
47
+ constructor({ dataDir = path.join(os.homedir(), '.livedesk') } = {}) {
48
+ this.filePath = path.join(dataDir, 'agent-settings.json');
49
+ this.settings = null;
50
+ }
51
+
52
+ async get() {
53
+ if (this.settings) return { ...this.settings };
54
+ try {
55
+ const raw = await readFile(this.filePath, 'utf8');
56
+ this.settings = normalizeAgentSettings(JSON.parse(raw));
57
+ } catch {
58
+ this.settings = normalizeAgentSettings(DEFAULT_AGENT_SETTINGS);
59
+ }
60
+ return { ...this.settings };
61
+ }
62
+
63
+ async update(patch = {}) {
64
+ const current = await this.get();
65
+ const next = normalizeAgentSettings({ ...current, ...patch });
66
+ await writeJsonAtomic(this.filePath, next);
67
+ this.settings = next;
68
+ return { ...next };
69
+ }
70
+
71
+ }