livedesk 0.1.204 → 0.1.205

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/hub/package.json CHANGED
@@ -15,6 +15,7 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@ffmpeg-installer/ffmpeg": "^1.1.0",
18
+ "@openai/codex-sdk": "0.144.5",
18
19
  "cors": "^2.8.5",
19
20
  "express": "^4.21.2",
20
21
  "ffmpeg-static": "^5.3.0",
@@ -1,44 +1,84 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
- import { AgentSettingsStore } from './agent-settings.js';
3
+ import { AgentSettingsStore, AGENT_PROVIDER_CODEX, AGENT_PROVIDER_OPENCODE_GO } from './agent-settings.js';
4
4
  import { createOsSecretStore } from './secret-store.js';
5
5
  import { createOpenCodeGoProvider } from './opencode-go-provider.js';
6
+ import { AgentProviderError } from './provider-errors.js';
7
+ import { createCodexAgentRuntime } from './codex-agent-runtime.js';
6
8
 
7
9
  function maskApiKey(value) {
8
10
  const key = String(value || '');
9
11
  return key.length >= 4 ? `••••${key.slice(-4)}` : '';
10
12
  }
11
13
 
12
- export function createAgentManager({ dataDir = path.join(os.homedir(), '.livedesk'), settingsStore, secretStore, provider } = {}) {
14
+ function publicCodexStatus(status = {}) {
15
+ return {
16
+ installed: status.installed === true,
17
+ authenticated: ['signed-in', 'not-signed-in'].includes(status.authenticated) ? status.authenticated : 'unknown',
18
+ status: String(status.status || 'unknown').slice(0, 40),
19
+ codexPath: String(status.codexPath || '').slice(0, 500),
20
+ detail: String(status.detail || '').slice(0, 300)
21
+ };
22
+ }
23
+
24
+ export function createAgentManager({
25
+ dataDir = path.join(os.homedir(), '.livedesk'),
26
+ settingsStore,
27
+ secretStore,
28
+ provider,
29
+ codexRuntime
30
+ } = {}) {
13
31
  const settings = settingsStore || new AgentSettingsStore({ dataDir });
14
32
  const secrets = secretStore || createOsSecretStore({ dataDir });
15
- const agentProvider = provider || createOpenCodeGoProvider();
33
+ const legacyProvider = provider || createOpenCodeGoProvider();
34
+ const runtime = codexRuntime;
35
+ let statusCache = { expiresAt: 0, value: null };
36
+
37
+ async function getCodexStatus() {
38
+ if (!runtime) return { installed: false, authenticated: 'unknown', status: 'unavailable', codexPath: '' };
39
+ if (statusCache.value && statusCache.expiresAt > Date.now()) return statusCache.value;
40
+ const value = publicCodexStatus(await runtime.getStatus());
41
+ statusCache = { value, expiresAt: Date.now() + 10000 };
42
+ return value;
43
+ }
16
44
 
17
45
  async function publicSettings() {
18
46
  const current = await settings.get();
19
47
  let hasApiKey = false;
20
48
  let apiKeyPreview = '';
21
49
  let secretStoreAvailable = true;
22
- try {
23
- const key = await secrets.getApiKey(current.provider);
24
- hasApiKey = Boolean(key);
25
- apiKeyPreview = maskApiKey(key);
26
- } catch (error) {
27
- secretStoreAvailable = false;
50
+ if (current.provider === AGENT_PROVIDER_OPENCODE_GO) {
51
+ try {
52
+ const key = await secrets.getApiKey(AGENT_PROVIDER_OPENCODE_GO);
53
+ hasApiKey = Boolean(key);
54
+ apiKeyPreview = maskApiKey(key);
55
+ } catch {
56
+ secretStoreAvailable = false;
57
+ }
28
58
  }
29
- return { ...current, hasApiKey, apiKeyPreview, secretStoreAvailable };
59
+ const codex = await getCodexStatus();
60
+ return {
61
+ ...current,
62
+ hasApiKey,
63
+ apiKeyPreview,
64
+ secretStoreAvailable,
65
+ codexInstallation: codex.installed ? 'installed' : 'not-installed',
66
+ codexAuth: codex.authenticated,
67
+ codexStatus: codex.status,
68
+ codexPath: codex.codexPath,
69
+ agentWorkspacePath: current.codexWorkingDirectory
70
+ };
71
+ }
72
+
73
+ async function legacyApiKey() {
74
+ const key = await secrets.getApiKey(AGENT_PROVIDER_OPENCODE_GO);
75
+ if (!key) throw new AgentProviderError('agent-api-key-missing', 'OpenCode Go API key is not configured.', { status: 401 });
76
+ return key;
30
77
  }
31
78
 
32
- async function apiKeyForCurrentSettings() {
79
+ async function currentProvider() {
33
80
  const current = await settings.get();
34
- const apiKey = await secrets.getApiKey(current.provider);
35
- if (!apiKey) {
36
- const error = new Error('OpenCode Go API key is not configured.');
37
- error.code = 'agent-api-key-missing';
38
- error.status = 401;
39
- throw error;
40
- }
41
- return { current, apiKey };
81
+ return { current, active: current.provider === AGENT_PROVIDER_CODEX ? runtime : legacyProvider };
42
82
  }
43
83
 
44
84
  return {
@@ -48,84 +88,69 @@ export function createAgentManager({ dataDir = path.join(os.homedir(), '.livedes
48
88
  const apiKey = typeof nextPatch.apiKey === 'string' ? nextPatch.apiKey.trim() : '';
49
89
  delete nextPatch.apiKey;
50
90
  const current = await settings.get();
51
- if (apiKey) await secrets.setApiKey(current.provider, apiKey);
91
+ const requestedProvider = String(nextPatch.provider || current.provider);
92
+ if (apiKey) {
93
+ if (requestedProvider !== AGENT_PROVIDER_OPENCODE_GO) {
94
+ throw new AgentProviderError('agent-codex-does-not-use-api-key', 'Codex authentication is owned by the installed Codex CLI.', { status: 400 });
95
+ }
96
+ await secrets.setApiKey(AGENT_PROVIDER_OPENCODE_GO, apiKey);
97
+ }
52
98
  await settings.update(nextPatch);
99
+ statusCache = { expiresAt: 0, value: null };
53
100
  return publicSettings();
54
101
  },
55
102
  async resetSettings() {
56
103
  await settings.reset();
104
+ statusCache = { expiresAt: 0, value: null };
57
105
  return publicSettings();
58
106
  },
59
107
  async deleteApiKey() {
60
- const current = await settings.get();
61
- await secrets.deleteApiKey(current.provider);
108
+ await secrets.deleteApiKey(AGENT_PROVIDER_OPENCODE_GO);
62
109
  return publicSettings();
63
110
  },
64
111
  async getModels() {
65
- const { current, apiKey } = await apiKeyForCurrentSettings();
66
- return agentProvider.getModels(current, apiKey);
112
+ const { current, active } = await currentProvider();
113
+ if (current.provider === AGENT_PROVIDER_CODEX) {
114
+ return [{ id: '', name: 'Codex default', protocol: 'codex-sdk', available: true, capabilities: { supportsReasoningLevel: true, supportsStreaming: true, supportsTools: true } }];
115
+ }
116
+ return active.getModels(current, await legacyApiKey());
67
117
  },
68
118
  async testConnection() {
69
- const { current, apiKey } = await apiKeyForCurrentSettings();
70
- return agentProvider.testConnection(current, apiKey);
119
+ const { current, active } = await currentProvider();
120
+ if (current.provider === AGENT_PROVIDER_CODEX) {
121
+ if (!active) throw new AgentProviderError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
122
+ statusCache = { expiresAt: 0, value: null };
123
+ return active.testConnection();
124
+ }
125
+ return active.testConnection(current, await legacyApiKey());
71
126
  },
72
127
  async createPlan(input) {
73
- const current = await settings.get();
74
- if (!current.enabled) {
75
- const error = new Error('Agent AI is disabled in Settings.');
76
- error.code = 'agent-ai-disabled';
77
- error.status = 409;
78
- throw error;
79
- }
80
- const apiKey = await secrets.getApiKey(current.provider);
81
- if (!apiKey) {
82
- const error = new Error('OpenCode Go API key is not configured.');
83
- error.code = 'agent-api-key-missing';
84
- error.status = 401;
85
- throw error;
128
+ const { current, active } = await currentProvider();
129
+ if (!current.enabled) throw new AgentProviderError('agent-ai-disabled', 'Agent AI is disabled in Settings.', { status: 409 });
130
+ if (current.provider === AGENT_PROVIDER_CODEX) {
131
+ throw new AgentProviderError('agent-codex-run-required', 'Codex runs use the LiveDesk tool loop.', { status: 409 });
86
132
  }
87
- return agentProvider.createPlan(input, current, apiKey);
133
+ return active.createPlan(input, current, await legacyApiKey());
88
134
  },
89
135
  async createSummary(input) {
90
- const current = await settings.get();
91
- if (!current.enabled || !current.generateSummary) {
92
- const error = new Error('Agent summaries are disabled in Settings.');
93
- error.code = 'agent-summary-disabled';
94
- error.status = 409;
95
- throw error;
96
- }
97
- const apiKey = await secrets.getApiKey(current.provider);
98
- if (!apiKey) {
99
- const error = new Error('OpenCode Go API key is not configured.');
100
- error.code = 'agent-api-key-missing';
101
- error.status = 401;
102
- throw error;
136
+ const { current, active } = await currentProvider();
137
+ if (!current.enabled || !current.generateSummary) throw new AgentProviderError('agent-summary-disabled', 'Agent summaries are disabled in Settings.', { status: 409 });
138
+ if (current.provider === AGENT_PROVIDER_CODEX) {
139
+ return { summary: String(input?.summary?.results?.length ? `${input.summary.completed || 0} completed, ${input.summary.failed || 0} failed.` : 'LiveDesk Agent run completed.').slice(0, 1200), modelId: current.codexModelId || 'Codex default' };
103
140
  }
104
141
  const source = input && typeof input === 'object' ? input : {};
105
142
  const results = Array.isArray(source.results) ? source.results : [];
106
143
  const safeResults = current.includeRawDeviceDetails
107
- ? results.map(result => ({
108
- deviceName: String(result?.deviceName || '').slice(0, 120),
109
- status: String(result?.status || '').slice(0, 40),
110
- result: String(result?.result || '').slice(0, 1200),
111
- error: String(result?.error || '').slice(0, 500)
112
- }))
113
- : results.map(result => ({
114
- deviceName: String(result?.deviceName || '').slice(0, 120),
115
- status: String(result?.status || '').slice(0, 40),
116
- hasResult: Boolean(result?.result),
117
- hasError: Boolean(result?.error)
118
- }));
119
- return agentProvider.createSummary({
120
- instruction: String(source.instruction || '').slice(0, 500),
121
- operation: String(source.operation || '').slice(0, 80),
122
- summary: {
123
- total: Number(source.total || results.length),
124
- completed: Number(source.completed || 0),
125
- failed: Number(source.failed || 0),
126
- results: safeResults
127
- }
128
- }, current, apiKey);
129
- }
144
+ ? results.map(result => ({ deviceName: String(result?.deviceName || '').slice(0, 120), status: String(result?.status || '').slice(0, 40), result: String(result?.result || '').slice(0, 1200), error: String(result?.error || '').slice(0, 500) }))
145
+ : results.map(result => ({ deviceName: String(result?.deviceName || '').slice(0, 120), status: String(result?.status || '').slice(0, 40), hasResult: Boolean(result?.result), hasError: Boolean(result?.error) }));
146
+ return active.createSummary({ instruction: String(source.instruction || '').slice(0, 500), operation: String(source.operation || '').slice(0, 80), summary: { total: Number(source.total || results.length), completed: Number(source.completed || 0), failed: Number(source.failed || 0), results: safeResults } }, current, await legacyApiKey());
147
+ },
148
+ async startRun(input) {
149
+ const current = await settings.get();
150
+ if (current.provider !== AGENT_PROVIDER_CODEX || !runtime) throw new AgentProviderError('agent-codex-not-active', 'Codex SDK is not the active Agent provider.', { status: 409 });
151
+ return runtime.start(input);
152
+ },
153
+ getRun(runId) { return runtime?.get(runId) || null; },
154
+ cancelRun(runId) { return runtime?.cancel(runId) || null; }
130
155
  };
131
156
  }
@@ -2,12 +2,33 @@ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
 
5
- export const AGENT_PROVIDER = 'opencode-go';
5
+ export const AGENT_PROVIDER_CODEX = 'codex';
6
+ export const AGENT_PROVIDER_OPENCODE_GO = 'opencode-go';
7
+ export const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_CODEX;
6
8
  export const DEFAULT_AGENT_BASE_URL = 'https://opencode.ai/zen/go/v1';
7
-
9
+ export const DEFAULT_CODEX_MODEL_ID = '';
10
+ export const DEFAULT_AGENT_WORKSPACE = path.join(os.homedir(), '.livedesk', 'agent-workspace');
8
11
  export const DEFAULT_AGENT_SETTINGS = Object.freeze({
9
- enabled: false,
10
- provider: AGENT_PROVIDER,
12
+ settingsVersion: 2,
13
+ enabled: true,
14
+ provider: DEFAULT_AGENT_PROVIDER,
15
+ codexModelId: DEFAULT_CODEX_MODEL_ID,
16
+ codexReasoningEffort: 'medium',
17
+ codexSandboxMode: 'read-only',
18
+ codexApprovalPolicy: 'never',
19
+ codexWorkingDirectory: DEFAULT_AGENT_WORKSPACE,
20
+ codexTaskTimeoutMs: 600000,
21
+ codexMaxTurns: 12,
22
+ codexMaxToolCalls: 20,
23
+ codexResumeSessions: true,
24
+ codexShowDetailedEvents: true,
25
+ codexNetworkAccessEnabled: false,
26
+ codexWebSearchMode: 'disabled',
27
+ deterministicParserFirst: true,
28
+ generateSummary: true,
29
+ fallbackSummary: true,
30
+ includeRawDeviceDetails: false,
31
+ maxConcurrentRequests: 2,
11
32
  baseUrl: DEFAULT_AGENT_BASE_URL,
12
33
  defaultModelId: 'deepseek-v4-flash',
13
34
  planModelId: '',
@@ -22,15 +43,15 @@ export const DEFAULT_AGENT_SETTINGS = Object.freeze({
22
43
  summaryTimeoutMs: 60000,
23
44
  retryCount: 1,
24
45
  retryDelayMs: 1000,
25
- maxConcurrentRequests: 2,
26
- streamSummary: false,
27
- deterministicParserFirst: true,
28
- generateSummary: true,
29
- fallbackSummary: true,
30
- includeRawDeviceDetails: false
46
+ streamSummary: false
31
47
  });
32
48
 
33
- const REASONING_LEVELS = new Set(['auto', 'low', 'medium', 'high']);
49
+ const PROVIDERS = new Set([AGENT_PROVIDER_CODEX, AGENT_PROVIDER_OPENCODE_GO]);
50
+ const REASONING_EFFORTS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh']);
51
+ const SANDBOX_MODES = new Set(['read-only', 'workspace-write', 'danger-full-access']);
52
+ const APPROVAL_POLICIES = new Set(['never', 'on-request', 'on-failure', 'untrusted']);
53
+ const WEB_SEARCH_MODES = new Set(['disabled', 'cached', 'live']);
54
+ const LEGACY_REASONING_LEVELS = new Set(['auto', 'low', 'medium', 'high']);
34
55
 
35
56
  export class AgentSettingsError extends Error {
36
57
  constructor(code, message) {
@@ -57,13 +78,17 @@ function stringValue(value, fallback, maxLength = 240) {
57
78
  return value.trim().slice(0, maxLength);
58
79
  }
59
80
 
81
+ function enumValue(value, allowed, fallback, code) {
82
+ const normalized = stringValue(value, fallback, 80).toLowerCase();
83
+ if (!allowed.has(normalized)) throw new AgentSettingsError(code, `Invalid agent setting: ${normalized}.`);
84
+ return normalized;
85
+ }
86
+
60
87
  function normalizeBaseUrl(value) {
61
- const candidate = stringValue(value, DEFAULT_AGENT_BASE_URL, 500).replace(/\/+$/, '');
88
+ const candidate = stringValue(value, DEFAULT_AGENT_SETTINGS.baseUrl, 500).replace(/\/+$/, '');
62
89
  try {
63
90
  const parsed = new URL(candidate);
64
- if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname) {
65
- throw new Error('invalid protocol');
66
- }
91
+ if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname) throw new Error('invalid protocol');
67
92
  parsed.hash = '';
68
93
  parsed.search = '';
69
94
  return parsed.toString().replace(/\/+$/, '');
@@ -72,19 +97,47 @@ function normalizeBaseUrl(value) {
72
97
  }
73
98
  }
74
99
 
100
+ function normalizeWorkingDirectory(value) {
101
+ const candidate = stringValue(value, DEFAULT_AGENT_WORKSPACE, 500);
102
+ if (!candidate || candidate.includes('\0')) {
103
+ throw new AgentSettingsError('agent-invalid-working-directory', 'Codex working directory is invalid.');
104
+ }
105
+ return path.resolve(candidate);
106
+ }
107
+
75
108
  export function normalizeAgentSettings(value = {}) {
76
109
  const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
77
- const provider = stringValue(source.provider, DEFAULT_AGENT_SETTINGS.provider, 40);
78
- if (provider !== AGENT_PROVIDER) {
79
- throw new AgentSettingsError('agent-unsupported-provider', 'Only OpenCode Go is supported by this LiveDesk build.');
80
- }
81
- const reasoningLevel = stringValue(source.reasoningLevel, DEFAULT_AGENT_SETTINGS.reasoningLevel, 20).toLowerCase();
82
- if (!REASONING_LEVELS.has(reasoningLevel)) {
83
- throw new AgentSettingsError('agent-invalid-reasoning-level', 'Reasoning level is invalid.');
84
- }
110
+ // The pre-Codex settings file was written by LiveDesk itself and had no
111
+ // migration marker. Keep the legacy values but move that first-run config
112
+ // to the new Codex default.
113
+ const migratingLegacy = Number(source.settingsVersion || 0) < 2 && source.provider === AGENT_PROVIDER_OPENCODE_GO;
114
+ const provider = migratingLegacy
115
+ ? DEFAULT_AGENT_PROVIDER
116
+ : enumValue(source.provider, PROVIDERS, DEFAULT_AGENT_PROVIDER, 'agent-unsupported-provider');
117
+ const reasoningLevel = enumValue(source.reasoningLevel, LEGACY_REASONING_LEVELS, DEFAULT_AGENT_SETTINGS.reasoningLevel, 'agent-invalid-reasoning-level');
85
118
  return {
119
+ settingsVersion: 2,
86
120
  enabled: booleanValue(source.enabled, DEFAULT_AGENT_SETTINGS.enabled),
87
- provider: AGENT_PROVIDER,
121
+ provider,
122
+ codexModelId: stringValue(source.codexModelId, DEFAULT_AGENT_SETTINGS.codexModelId, 160),
123
+ codexReasoningEffort: enumValue(source.codexReasoningEffort, REASONING_EFFORTS, DEFAULT_AGENT_SETTINGS.codexReasoningEffort, 'agent-invalid-reasoning-effort'),
124
+ // These are safety settings, not user-controlled execution toggles. They
125
+ // are accepted for compatibility but always normalized to the safe floor.
126
+ codexSandboxMode: 'read-only',
127
+ codexApprovalPolicy: 'never',
128
+ codexWorkingDirectory: normalizeWorkingDirectory(source.codexWorkingDirectory),
129
+ codexTaskTimeoutMs: numberValue(source.codexTaskTimeoutMs, 30000, 1800000, DEFAULT_AGENT_SETTINGS.codexTaskTimeoutMs, true),
130
+ codexMaxTurns: numberValue(source.codexMaxTurns, 1, 40, DEFAULT_AGENT_SETTINGS.codexMaxTurns, true),
131
+ codexMaxToolCalls: numberValue(source.codexMaxToolCalls, 1, 100, DEFAULT_AGENT_SETTINGS.codexMaxToolCalls, true),
132
+ codexResumeSessions: booleanValue(source.codexResumeSessions, DEFAULT_AGENT_SETTINGS.codexResumeSessions),
133
+ codexShowDetailedEvents: booleanValue(source.codexShowDetailedEvents, DEFAULT_AGENT_SETTINGS.codexShowDetailedEvents),
134
+ codexNetworkAccessEnabled: false,
135
+ codexWebSearchMode: 'disabled',
136
+ deterministicParserFirst: booleanValue(source.deterministicParserFirst, DEFAULT_AGENT_SETTINGS.deterministicParserFirst),
137
+ generateSummary: booleanValue(source.generateSummary, DEFAULT_AGENT_SETTINGS.generateSummary),
138
+ fallbackSummary: booleanValue(source.fallbackSummary, DEFAULT_AGENT_SETTINGS.fallbackSummary),
139
+ includeRawDeviceDetails: booleanValue(source.includeRawDeviceDetails, DEFAULT_AGENT_SETTINGS.includeRawDeviceDetails),
140
+ maxConcurrentRequests: numberValue(source.maxConcurrentRequests, 1, 8, DEFAULT_AGENT_SETTINGS.maxConcurrentRequests, true),
88
141
  baseUrl: normalizeBaseUrl(source.baseUrl ?? DEFAULT_AGENT_SETTINGS.baseUrl),
89
142
  defaultModelId: stringValue(source.defaultModelId, DEFAULT_AGENT_SETTINGS.defaultModelId, 160),
90
143
  planModelId: stringValue(source.planModelId, DEFAULT_AGENT_SETTINGS.planModelId, 160),
@@ -99,12 +152,7 @@ export function normalizeAgentSettings(value = {}) {
99
152
  summaryTimeoutMs: numberValue(source.summaryTimeoutMs, 5000, 180000, DEFAULT_AGENT_SETTINGS.summaryTimeoutMs, true),
100
153
  retryCount: numberValue(source.retryCount, 0, 4, DEFAULT_AGENT_SETTINGS.retryCount, true),
101
154
  retryDelayMs: numberValue(source.retryDelayMs, 100, 10000, DEFAULT_AGENT_SETTINGS.retryDelayMs, true),
102
- maxConcurrentRequests: numberValue(source.maxConcurrentRequests, 1, 8, DEFAULT_AGENT_SETTINGS.maxConcurrentRequests, true),
103
- streamSummary: booleanValue(source.streamSummary, DEFAULT_AGENT_SETTINGS.streamSummary),
104
- deterministicParserFirst: booleanValue(source.deterministicParserFirst, DEFAULT_AGENT_SETTINGS.deterministicParserFirst),
105
- generateSummary: booleanValue(source.generateSummary, DEFAULT_AGENT_SETTINGS.generateSummary),
106
- fallbackSummary: booleanValue(source.fallbackSummary, DEFAULT_AGENT_SETTINGS.fallbackSummary),
107
- includeRawDeviceDetails: booleanValue(source.includeRawDeviceDetails, DEFAULT_AGENT_SETTINGS.includeRawDeviceDetails)
155
+ streamSummary: booleanValue(source.streamSummary, DEFAULT_AGENT_SETTINGS.streamSummary)
108
156
  };
109
157
  }
110
158
 
@@ -126,12 +174,8 @@ export class AgentSettingsStore {
126
174
  try {
127
175
  const raw = await readFile(this.filePath, 'utf8');
128
176
  this.settings = normalizeAgentSettings(JSON.parse(raw));
129
- } catch (error) {
130
- if (error?.code !== 'ENOENT') {
131
- this.settings = normalizeAgentSettings(DEFAULT_AGENT_SETTINGS);
132
- } else {
133
- this.settings = normalizeAgentSettings(DEFAULT_AGENT_SETTINGS);
134
- }
177
+ } catch {
178
+ this.settings = normalizeAgentSettings(DEFAULT_AGENT_SETTINGS);
135
179
  }
136
180
  return { ...this.settings };
137
181
  }
@@ -151,4 +195,3 @@ export class AgentSettingsStore {
151
195
  return { ...next };
152
196
  }
153
197
  }
154
-