tide-commander 1.152.0 → 1.154.0
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/CHANGELOG.md +20 -0
- package/dist/assets/{BossLogsModal-DZWUfQXs.js → BossLogsModal-Bo0pUCWU.js} +1 -1
- package/dist/assets/{BossSpawnModal-7IwzcqE7.js → BossSpawnModal-BOOHaqaD.js} +1 -1
- package/dist/assets/{ControlsModal-D8L1xD-H.js → ControlsModal-DItGV-CW.js} +1 -1
- package/dist/assets/{DockerLogsModal-_zLe5UFl.js → DockerLogsModal-DjAZ3GL7.js} +1 -1
- package/dist/assets/{EmbeddedEditor-gbiFXvt_.js → EmbeddedEditor-Cihkr8Xb.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-CcXTV3NP.js → GmailOAuthSetup-G1XqiM44.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-wbETidgv.js → GoogleOAuthSetup-Ce6gojdm.js} +1 -1
- package/dist/assets/{IframeModal-a9E5UDBk.js → IframeModal-CSSZJzPv.js} +1 -1
- package/dist/assets/{IntegrationsPanel-BbGyluL0.js → IntegrationsPanel-EspPH09n.js} +2 -2
- package/dist/assets/LogViewerModal-Bn-HDysM.js +1 -0
- package/dist/assets/{MonitoringModal-DymqjenY.js → MonitoringModal-DuSLTKVR.js} +1 -1
- package/dist/assets/{PM2LogsModal-CEaCk-dt.js → PM2LogsModal-Bo2OtaQd.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-q_FFv7L6.js → RestoreArchivedAreaModal-BOx-V6tl.js} +1 -1
- package/dist/assets/{Scene2DCanvas-DOvVtiYO.js → Scene2DCanvas-BIUbi14G.js} +1 -1
- package/dist/assets/{SceneManager-D0NdrG8K.js → SceneManager-CIWrGc-K.js} +4 -4
- package/dist/assets/{SkillsPanel-DpqD60Hy.js → SkillsPanel-_KHtTDhC.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-BdoALd_g.js → SlackMultiInstanceSetup-DbQ3rUtd.js} +1 -1
- package/dist/assets/{SpawnModal-DFFFH2kc.js → SpawnModal-CYEoOMPW.js} +1 -1
- package/dist/assets/{StatisticsModal-Dv_rYZDt.js → StatisticsModal-BJ-coeis.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-CRCCI686.js → SubordinateAssignmentModal-5Z3jvbM9.js} +1 -1
- package/dist/assets/SymbolsNerdFontMono-Regular-BM2yZ5nr.woff2 +0 -0
- package/dist/assets/{TriggerManagerPanel-DBN8IRa-.js → TriggerManagerPanel-DvC0xP9r.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-uQEvNTs2.js → WorkflowEditorPanel-6ym1_csr.js} +1 -1
- package/dist/assets/{area-logos-CuVxToGM.js → area-logos-mWPCtCnf.js} +1 -1
- package/dist/assets/{index-Dwll5Eyj.js → index-8VVbTmtb.js} +1 -1
- package/dist/assets/{index-BNzYPyBm.css → index-BPjQZacc.css} +1 -1
- package/dist/assets/index-C34l72MW.js +1 -0
- package/dist/assets/{index-DKnfLe7S.js → index-CS8OutmQ.js} +1 -1
- package/dist/assets/{index-ipuWrg1S.js → index-CT2vtbed.js} +3 -3
- package/dist/assets/{index-Blc4MwKs.js → index-CY_NzVPm.js} +1 -1
- package/dist/assets/index-CiKbhMfN.js +1 -0
- package/dist/assets/{index-HvyFhszc.js → index-CnESn83U.js} +2 -2
- package/dist/assets/{index-8Ghh3AQ-.js → index-DZiNmiyC.js} +1 -1
- package/dist/assets/{index-D4iie3Vd.js → index-DxyZ-rYT.js} +1 -1
- package/dist/assets/main-BHBs2E1L.css +1 -0
- package/dist/assets/main-D5j8Ul0m.js +289 -0
- package/dist/assets/{web-D0JSbYeo.js → web-BvAQVbIF.js} +1 -1
- package/dist/assets/{web-1pfPwxBS.js → web-Cd0-XOkz.js} +1 -1
- package/dist/assets/{web-DjgiLxvi.js → web-DMxl9AVF.js} +1 -1
- package/dist/build-info.json +1 -0
- package/dist/index.html +2 -2
- package/dist/locales/de/config.json +1 -0
- package/dist/locales/en/config.json +6 -1
- package/dist/locales/en/terminal.json +36 -0
- package/dist/locales/es/config.json +4 -1
- package/dist/locales/es/terminal.json +8 -0
- package/dist/locales/fr/config.json +1 -0
- package/dist/locales/hi/config.json +1 -0
- package/dist/locales/it/config.json +1 -0
- package/dist/locales/ja/config.json +1 -0
- package/dist/locales/pt/config.json +1 -0
- package/dist/locales/ru/config.json +1 -0
- package/dist/locales/zh-CN/config.json +1 -0
- package/dist/src/packages/server/codex/app-server/app-server-event-adapter.js +222 -0
- package/dist/src/packages/server/codex/app-server/app-server-process.js +304 -0
- package/dist/src/packages/server/codex/app-server/app-server-recovery-store.js +76 -0
- package/dist/src/packages/server/codex/app-server/codex-app-server-runner.js +453 -0
- package/dist/src/packages/server/routes/agents.js +29 -1
- package/dist/src/packages/server/routes/system.js +216 -1
- package/dist/src/packages/server/runtime/codex-runner-router.js +99 -0
- package/dist/src/packages/server/runtime/codex-runtime-provider.js +5 -3
- package/dist/src/packages/server/services/claude-credentials-service.js +536 -0
- package/dist/src/packages/server/services/claude-usage-service.js +35 -28
- package/dist/src/packages/server/services/provider-credentials-service.js +338 -0
- package/dist/src/packages/server/services/system-prompt-service.js +46 -0
- package/package.json +1 -1
- package/dist/assets/LogViewerModal-cnMAbNvY.js +0 -1
- package/dist/assets/index-BHM8V4i-.js +0 -1
- package/dist/assets/index-OkenGYC6.js +0 -1
- package/dist/assets/main-DpNJ8pqp.css +0 -1
- package/dist/assets/main-aIpEYDDR.js +0 -289
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodexAppServerProcess
|
|
3
|
+
*
|
|
4
|
+
* Manages a DETACHED, restart-surviving `codex app-server` listening on a
|
|
5
|
+
* localhost WebSocket (`--listen ws://127.0.0.1:PORT`) and speaks its
|
|
6
|
+
* "JSON-RPC lite" protocol (standard JSON-RPC 2.0 shape, `"jsonrpc":"2.0"`
|
|
7
|
+
* omitted on the wire) over that WebSocket.
|
|
8
|
+
*
|
|
9
|
+
* Why WebSocket + detached:
|
|
10
|
+
* - `codex exec` never streams; the app-server emits `item/agentMessage/delta`
|
|
11
|
+
* tokens (word-by-word).
|
|
12
|
+
* - A stdio child dies with the commander. A detached process bound to a TCP
|
|
13
|
+
* port survives commander restarts, and the app-server keeps running any
|
|
14
|
+
* in-flight turn independently. On restart the commander reconnects the
|
|
15
|
+
* WebSocket and `thread/resume` re-subscribes — the server even replays the
|
|
16
|
+
* turn's remaining deltas (verified empirically).
|
|
17
|
+
* - The raw `unix://` transport uses an undocumented framing; the `ws://`
|
|
18
|
+
* transport uses standard WebSocket framing (the commander already depends on
|
|
19
|
+
* `ws`) and exposes `/readyz` for health probes.
|
|
20
|
+
*
|
|
21
|
+
* One process multiplexes every Codex agent: each agent owns a `threadId`, and
|
|
22
|
+
* notifications carry that id so the runner can fan them back out.
|
|
23
|
+
*/
|
|
24
|
+
import { spawn } from 'child_process';
|
|
25
|
+
import * as http from 'http';
|
|
26
|
+
import * as net from 'net';
|
|
27
|
+
import * as fs from 'fs';
|
|
28
|
+
import { WebSocket } from 'ws';
|
|
29
|
+
import { createLogger } from '../../utils/logger.js';
|
|
30
|
+
import { loadDaemonInfo, saveDaemonInfo, clearDaemonInfo, APP_SERVER_LOG_FILE, } from './app-server-recovery-store.js';
|
|
31
|
+
const log = createLogger('CodexAppServer');
|
|
32
|
+
const INITIALIZE_TIMEOUT_MS = 15000;
|
|
33
|
+
const READYZ_TIMEOUT_MS = 20000;
|
|
34
|
+
const READYZ_POLL_MS = 250;
|
|
35
|
+
export class CodexAppServerProcess {
|
|
36
|
+
opts;
|
|
37
|
+
ws = null;
|
|
38
|
+
port = null;
|
|
39
|
+
nextId = 1;
|
|
40
|
+
pending = new Map();
|
|
41
|
+
readyPromise = null;
|
|
42
|
+
stopped = false;
|
|
43
|
+
/** True when the last ensureStarted() rejoined an already-running daemon. */
|
|
44
|
+
reconnectedToExisting = false;
|
|
45
|
+
constructor(opts) {
|
|
46
|
+
this.opts = opts;
|
|
47
|
+
}
|
|
48
|
+
isAlive() {
|
|
49
|
+
return this.ws !== null && this.ws.readyState === WebSocket.OPEN && !this.stopped;
|
|
50
|
+
}
|
|
51
|
+
/** Did the current connection rejoin a daemon that survived a prior commander? */
|
|
52
|
+
didReconnect() {
|
|
53
|
+
return this.reconnectedToExisting;
|
|
54
|
+
}
|
|
55
|
+
ensureStarted() {
|
|
56
|
+
if (this.readyPromise)
|
|
57
|
+
return this.readyPromise;
|
|
58
|
+
this.readyPromise = this.boot();
|
|
59
|
+
return this.readyPromise;
|
|
60
|
+
}
|
|
61
|
+
async boot() {
|
|
62
|
+
this.stopped = false;
|
|
63
|
+
// 1. Rejoin an already-running daemon if one is healthy.
|
|
64
|
+
const existing = loadDaemonInfo();
|
|
65
|
+
if (existing && (await this.isDaemonHealthy(existing))) {
|
|
66
|
+
log.log(`🔗 Rejoining existing app-server daemon pid=${existing.pid} port=${existing.port}`);
|
|
67
|
+
this.port = existing.port;
|
|
68
|
+
this.reconnectedToExisting = true;
|
|
69
|
+
await this.connectAndInitialize(existing.port);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// 2. Spawn a fresh detached daemon.
|
|
73
|
+
this.reconnectedToExisting = false;
|
|
74
|
+
const port = await findFreePort();
|
|
75
|
+
const pid = this.spawnDaemon(port);
|
|
76
|
+
saveDaemonInfo({ port, pid, startedAt: Date.now() });
|
|
77
|
+
this.port = port;
|
|
78
|
+
await this.waitForReadyz(port);
|
|
79
|
+
await this.connectAndInitialize(port);
|
|
80
|
+
}
|
|
81
|
+
spawnDaemon(port) {
|
|
82
|
+
const { executablePath, cwd, extraEnv } = this.opts;
|
|
83
|
+
const logFd = (() => {
|
|
84
|
+
try {
|
|
85
|
+
return fs.openSync(APP_SERVER_LOG_FILE, 'a');
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return 'ignore';
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
log.log(`🚀 Spawning detached app-server: ${executablePath} app-server --listen ws://127.0.0.1:${port}`);
|
|
92
|
+
const child = spawn(executablePath, ['app-server', '--listen', `ws://127.0.0.1:${port}`], {
|
|
93
|
+
cwd,
|
|
94
|
+
env: { ...process.env, ...extraEnv },
|
|
95
|
+
detached: true,
|
|
96
|
+
stdio: ['ignore', logFd, logFd],
|
|
97
|
+
});
|
|
98
|
+
child.unref();
|
|
99
|
+
child.on('error', (err) => log.error(`app-server spawn error: ${err.message}`));
|
|
100
|
+
return child.pid ?? -1;
|
|
101
|
+
}
|
|
102
|
+
async isDaemonHealthy(info) {
|
|
103
|
+
if (!isPidAlive(info.pid))
|
|
104
|
+
return false;
|
|
105
|
+
return probeReadyz(info.port, 1500);
|
|
106
|
+
}
|
|
107
|
+
async waitForReadyz(port) {
|
|
108
|
+
const deadline = Date.now() + READYZ_TIMEOUT_MS;
|
|
109
|
+
while (Date.now() < deadline) {
|
|
110
|
+
if (await probeReadyz(port, 1000))
|
|
111
|
+
return;
|
|
112
|
+
await sleep(READYZ_POLL_MS);
|
|
113
|
+
}
|
|
114
|
+
throw new Error('app-server /readyz never became healthy');
|
|
115
|
+
}
|
|
116
|
+
connectAndInitialize(port) {
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
|
|
119
|
+
this.ws = ws;
|
|
120
|
+
let settled = false;
|
|
121
|
+
const initTimer = setTimeout(() => {
|
|
122
|
+
if (!settled) {
|
|
123
|
+
settled = true;
|
|
124
|
+
reject(new Error('app-server initialize timed out'));
|
|
125
|
+
}
|
|
126
|
+
}, INITIALIZE_TIMEOUT_MS);
|
|
127
|
+
ws.on('open', () => {
|
|
128
|
+
this.request('initialize', {
|
|
129
|
+
clientInfo: { name: 'tide-commander', title: 'Tide Commander', version: '1.0.0' },
|
|
130
|
+
capabilities: { experimentalApi: true },
|
|
131
|
+
})
|
|
132
|
+
.then(() => {
|
|
133
|
+
this.notify('initialized', {});
|
|
134
|
+
clearTimeout(initTimer);
|
|
135
|
+
if (!settled) {
|
|
136
|
+
settled = true;
|
|
137
|
+
log.log('✅ app-server WebSocket initialized');
|
|
138
|
+
resolve();
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
.catch((err) => {
|
|
142
|
+
clearTimeout(initTimer);
|
|
143
|
+
if (!settled) {
|
|
144
|
+
settled = true;
|
|
145
|
+
reject(err);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
ws.on('message', (data) => this.handleLine(data.toString()));
|
|
150
|
+
ws.on('error', (err) => {
|
|
151
|
+
log.warn(`app-server WebSocket error: ${err.message}`);
|
|
152
|
+
if (!settled) {
|
|
153
|
+
settled = true;
|
|
154
|
+
clearTimeout(initTimer);
|
|
155
|
+
reject(err);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
ws.on('close', () => this.handleDisconnect());
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
handleDisconnect() {
|
|
162
|
+
const wasConnected = this.ws !== null;
|
|
163
|
+
this.ws = null;
|
|
164
|
+
this.readyPromise = null;
|
|
165
|
+
for (const [, p] of this.pending)
|
|
166
|
+
p.reject(new Error('app-server connection closed'));
|
|
167
|
+
this.pending.clear();
|
|
168
|
+
if (wasConnected && !this.stopped) {
|
|
169
|
+
log.warn('app-server connection closed unexpectedly');
|
|
170
|
+
this.opts.onExit();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
handleLine(line) {
|
|
174
|
+
const trimmed = line.trim();
|
|
175
|
+
if (!trimmed)
|
|
176
|
+
return;
|
|
177
|
+
let msg;
|
|
178
|
+
try {
|
|
179
|
+
msg = JSON.parse(trimmed);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
log.warn(`Non-JSON message from app-server: ${trimmed.slice(0, 200)}`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (msg.id !== undefined && msg.method === undefined) {
|
|
186
|
+
const id = typeof msg.id === 'number' ? msg.id : Number(msg.id);
|
|
187
|
+
const pending = this.pending.get(id);
|
|
188
|
+
if (!pending)
|
|
189
|
+
return;
|
|
190
|
+
this.pending.delete(id);
|
|
191
|
+
if (msg.error)
|
|
192
|
+
pending.reject(new Error(msg.error.message || `app-server error on ${pending.method}`));
|
|
193
|
+
else
|
|
194
|
+
pending.resolve(msg.result);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (msg.method !== undefined && msg.id !== undefined) {
|
|
198
|
+
this.handleServerRequest(msg.id, msg.method);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (msg.method !== undefined) {
|
|
202
|
+
this.opts.onNotification(msg.method, msg.params || {});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Auto-answer server-initiated requests. Agents run approvalPolicy:'never' +
|
|
207
|
+
* danger-full-access, so approvals shouldn't fire — but approve rather than
|
|
208
|
+
* hang if one does.
|
|
209
|
+
*/
|
|
210
|
+
handleServerRequest(id, method) {
|
|
211
|
+
if (/approval/i.test(method)) {
|
|
212
|
+
this.respond(id, { decision: 'approved' });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
log.warn(`Unhandled app-server request '${method}' — responding empty`);
|
|
216
|
+
this.respond(id, {});
|
|
217
|
+
}
|
|
218
|
+
respond(id, result) {
|
|
219
|
+
this.sendRaw({ id, result });
|
|
220
|
+
}
|
|
221
|
+
request(method, params) {
|
|
222
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
223
|
+
return Promise.reject(new Error('app-server not connected'));
|
|
224
|
+
}
|
|
225
|
+
const id = this.nextId++;
|
|
226
|
+
const promise = new Promise((resolve, reject) => {
|
|
227
|
+
this.pending.set(id, { resolve, reject, method });
|
|
228
|
+
});
|
|
229
|
+
this.sendRaw({ id, method, params });
|
|
230
|
+
return promise;
|
|
231
|
+
}
|
|
232
|
+
notify(method, params) {
|
|
233
|
+
this.sendRaw({ method, params });
|
|
234
|
+
}
|
|
235
|
+
sendRaw(obj) {
|
|
236
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
237
|
+
log.warn('Cannot send to app-server: WebSocket not open');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
this.ws.send(JSON.stringify(obj));
|
|
241
|
+
}
|
|
242
|
+
/** Disconnect our client but LEAVE the daemon running (survives restart). */
|
|
243
|
+
disconnect() {
|
|
244
|
+
this.stopped = true;
|
|
245
|
+
if (this.ws) {
|
|
246
|
+
try {
|
|
247
|
+
this.ws.close();
|
|
248
|
+
}
|
|
249
|
+
catch { /* ignore */ }
|
|
250
|
+
this.ws = null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Disconnect AND kill the daemon (used on explicit full shutdown). */
|
|
254
|
+
killDaemon() {
|
|
255
|
+
this.disconnect();
|
|
256
|
+
const info = loadDaemonInfo();
|
|
257
|
+
if (info && isPidAlive(info.pid)) {
|
|
258
|
+
try {
|
|
259
|
+
process.kill(info.pid, 'SIGTERM');
|
|
260
|
+
}
|
|
261
|
+
catch { /* ignore */ }
|
|
262
|
+
}
|
|
263
|
+
clearDaemonInfo();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function isPidAlive(pid) {
|
|
267
|
+
if (!pid || pid < 1)
|
|
268
|
+
return false;
|
|
269
|
+
try {
|
|
270
|
+
process.kill(pid, 0);
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function probeReadyz(port, timeoutMs) {
|
|
278
|
+
return new Promise((resolve) => {
|
|
279
|
+
const req = http.get({ host: '127.0.0.1', port, path: '/readyz', timeout: timeoutMs }, (res) => {
|
|
280
|
+
res.resume();
|
|
281
|
+
resolve(res.statusCode !== undefined && res.statusCode >= 200 && res.statusCode < 500);
|
|
282
|
+
});
|
|
283
|
+
req.on('error', () => resolve(false));
|
|
284
|
+
req.on('timeout', () => {
|
|
285
|
+
req.destroy();
|
|
286
|
+
resolve(false);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
function findFreePort() {
|
|
291
|
+
return new Promise((resolve, reject) => {
|
|
292
|
+
const srv = net.createServer();
|
|
293
|
+
srv.unref();
|
|
294
|
+
srv.on('error', reject);
|
|
295
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
296
|
+
const addr = srv.address();
|
|
297
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
298
|
+
srv.close(() => (port ? resolve(port) : reject(new Error('could not allocate a free port'))));
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function sleep(ms) {
|
|
303
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
304
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for the restart-surviving Codex app-server.
|
|
3
|
+
*
|
|
4
|
+
* Two things are persisted, in a dedicated file (never the shared
|
|
5
|
+
* running-processes.json), so a restarted commander can rejoin a still-running
|
|
6
|
+
* app-server and its in-flight turns:
|
|
7
|
+
*
|
|
8
|
+
* 1. Daemon info — the `ws://` port + pid of the detached `codex app-server`
|
|
9
|
+
* process. On boot we probe it; if alive we reconnect instead of spawning.
|
|
10
|
+
* 2. Per-agent thread state — agentId ↔ threadId (== sessionId) + turn state, so
|
|
11
|
+
* we can re-subscribe (thread/resume) to each agent's thread and finalize any
|
|
12
|
+
* turn that was mid-flight during the restart.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import * as os from 'os';
|
|
17
|
+
import { createLogger } from '../../utils/logger.js';
|
|
18
|
+
const log = createLogger('CodexAppServerStore');
|
|
19
|
+
const DATA_DIR = path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'tide-commander');
|
|
20
|
+
const DAEMON_FILE = path.join(DATA_DIR, 'codex-app-server-daemon.json');
|
|
21
|
+
const AGENTS_FILE = path.join(DATA_DIR, 'codex-app-server-agents.json');
|
|
22
|
+
export const APP_SERVER_LOG_FILE = path.join(DATA_DIR, 'codex-app-server.log');
|
|
23
|
+
function ensureDataDir() {
|
|
24
|
+
if (!fs.existsSync(DATA_DIR))
|
|
25
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
export function loadDaemonInfo() {
|
|
28
|
+
try {
|
|
29
|
+
if (!fs.existsSync(DAEMON_FILE))
|
|
30
|
+
return null;
|
|
31
|
+
const data = JSON.parse(fs.readFileSync(DAEMON_FILE, 'utf-8'));
|
|
32
|
+
if (typeof data.port === 'number' && typeof data.pid === 'number')
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
log.warn(`Failed to load daemon info: ${err.message}`);
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
export function saveDaemonInfo(info) {
|
|
41
|
+
ensureDataDir();
|
|
42
|
+
try {
|
|
43
|
+
fs.writeFileSync(DAEMON_FILE, JSON.stringify(info, null, 2), 'utf-8');
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
log.error(`Failed to save daemon info: ${err.message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function clearDaemonInfo() {
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync(DAEMON_FILE))
|
|
52
|
+
fs.unlinkSync(DAEMON_FILE);
|
|
53
|
+
}
|
|
54
|
+
catch { /* ignore */ }
|
|
55
|
+
}
|
|
56
|
+
export function loadAppServerAgents() {
|
|
57
|
+
try {
|
|
58
|
+
if (!fs.existsSync(AGENTS_FILE))
|
|
59
|
+
return [];
|
|
60
|
+
const data = JSON.parse(fs.readFileSync(AGENTS_FILE, 'utf-8'));
|
|
61
|
+
return Array.isArray(data) ? data : [];
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
log.warn(`Failed to load app-server agents: ${err.message}`);
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function saveAppServerAgents(agents) {
|
|
69
|
+
ensureDataDir();
|
|
70
|
+
try {
|
|
71
|
+
fs.writeFileSync(AGENTS_FILE, JSON.stringify(agents, null, 2), 'utf-8');
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
log.error(`Failed to save app-server agents: ${err.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|