tide-commander 0.84.2 → 0.85.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/README.md +4 -1
- package/dist/assets/main-BB4dFHqm.css +1 -0
- package/dist/assets/{main-BSaZHBzs.js → main-hADDQllW.js} +97 -97
- package/dist/assets/{web-GtGat3e7.js → web-C9K1Th3w.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/locales/de/config.json +4 -1
- package/dist/locales/en/config.json +4 -1
- package/dist/locales/es/config.json +4 -1
- package/dist/locales/fr/config.json +4 -1
- package/dist/locales/hi/config.json +4 -1
- package/dist/locales/it/config.json +4 -1
- package/dist/locales/ja/config.json +4 -1
- package/dist/locales/pt/config.json +4 -1
- package/dist/locales/ru/config.json +4 -1
- package/dist/locales/zh-CN/config.json +4 -1
- package/dist/src/packages/server/claude/runner/process-lifecycle.js +2 -0
- package/dist/src/packages/server/claude/session-loader.js +158 -0
- package/dist/src/packages/server/cli.js +29 -9
- package/dist/src/packages/server/codex/backend.js +26 -2
- package/dist/src/packages/server/codex/json-event-parser.js +224 -4
- package/dist/src/packages/server/data/builtin-skills/full-notifications.js +6 -12
- package/dist/src/packages/server/routes/agents.js +29 -1
- package/dist/src/packages/server/routes/config.js +1 -1
- package/dist/src/packages/server/services/system-prompt-service.js +55 -0
- package/package.json +1 -1
- package/dist/assets/main-C6IAMrFB.css +0 -1
|
@@ -5,6 +5,7 @@ import { createLogger } from '../utils/logger.js';
|
|
|
5
5
|
const log = createLogger('CodexParser');
|
|
6
6
|
let hasLoggedTurnAbortedLiveWarning = false;
|
|
7
7
|
const MAX_DIFF_FILE_BYTES = 256 * 1024;
|
|
8
|
+
const MAX_FALLBACK_EVENT_TEXT = 4000;
|
|
8
9
|
function sanitizeCodexMessageText(text) {
|
|
9
10
|
const hadTurnAborted = /<turn_aborted>[\s\S]*?<\/turn_aborted>/.test(text);
|
|
10
11
|
const withoutTurnAborted = text.replace(/<turn_aborted>[\s\S]*?<\/turn_aborted>/g, '').trim();
|
|
@@ -71,10 +72,37 @@ function parseEnvelope(value) {
|
|
|
71
72
|
type: asString(value.type),
|
|
72
73
|
item: parseItem(value.item),
|
|
73
74
|
usage: parseUsage(value.usage),
|
|
75
|
+
payload: parseResponsePayload(value.payload),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function parseSummaryArray(value) {
|
|
79
|
+
if (!Array.isArray(value))
|
|
80
|
+
return undefined;
|
|
81
|
+
return value
|
|
82
|
+
.filter(isObject)
|
|
83
|
+
.map((entry) => ({
|
|
84
|
+
type: asString(entry.type),
|
|
85
|
+
text: asString(entry.text),
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
function parseResponsePayload(payload) {
|
|
89
|
+
if (!isObject(payload))
|
|
90
|
+
return undefined;
|
|
91
|
+
return {
|
|
92
|
+
type: asString(payload.type),
|
|
93
|
+
status: asString(payload.status),
|
|
94
|
+
action: parseAction(payload.action),
|
|
95
|
+
item: parseItem(payload.item),
|
|
96
|
+
usage: parseUsage(payload.usage),
|
|
97
|
+
call_id: asString(payload.call_id),
|
|
98
|
+
name: asString(payload.name),
|
|
99
|
+
input: asString(payload.input),
|
|
100
|
+
output: asString(payload.output),
|
|
101
|
+
summary: parseSummaryArray(payload.summary),
|
|
74
102
|
};
|
|
75
103
|
}
|
|
76
104
|
/**
|
|
77
|
-
* Parses line-delimited JSON events from `codex exec --json` and maps them to
|
|
105
|
+
* Parses line-delimited JSON events from `codex exec --experimental-json` and maps them to
|
|
78
106
|
* Tide runtime events.
|
|
79
107
|
*/
|
|
80
108
|
export class CodexJsonEventParser {
|
|
@@ -108,6 +136,12 @@ export class CodexJsonEventParser {
|
|
|
108
136
|
const event = parseEnvelope(rawEvent);
|
|
109
137
|
if (!event?.type)
|
|
110
138
|
return [];
|
|
139
|
+
if (event.type === 'event_msg') {
|
|
140
|
+
return this.parseEventMsg(rawEvent);
|
|
141
|
+
}
|
|
142
|
+
if (event.type === 'response_item') {
|
|
143
|
+
return this.parseResponseItem(event.payload);
|
|
144
|
+
}
|
|
111
145
|
if (event.type === 'item.started') {
|
|
112
146
|
return this.parseItemStarted(event.item);
|
|
113
147
|
}
|
|
@@ -121,7 +155,171 @@ export class CodexJsonEventParser {
|
|
|
121
155
|
if (event.type === 'turn.completed') {
|
|
122
156
|
return this.parseTurnCompleted(event.usage);
|
|
123
157
|
}
|
|
124
|
-
|
|
158
|
+
// Informational envelope events that don't need terminal display
|
|
159
|
+
if (event.type === 'turn.started' || event.type === 'thread.started') {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
return [this.buildUnknownEventFallback(`Unhandled Codex event type: ${event.type}`, rawEvent)];
|
|
163
|
+
}
|
|
164
|
+
parseEventMsg(rawEvent) {
|
|
165
|
+
if (!isObject(rawEvent))
|
|
166
|
+
return [];
|
|
167
|
+
const payload = rawEvent.payload;
|
|
168
|
+
if (!isObject(payload))
|
|
169
|
+
return [];
|
|
170
|
+
const payloadType = asString(payload.type);
|
|
171
|
+
// token_count: Silent. Token accounting is handled by turn.completed.
|
|
172
|
+
if (payloadType === 'token_count') {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
// agent_reasoning: Map to thinking event
|
|
176
|
+
if (payloadType === 'agent_reasoning') {
|
|
177
|
+
const text = asString(payload.text);
|
|
178
|
+
if (!text)
|
|
179
|
+
return [];
|
|
180
|
+
return [{ type: 'thinking', text, isStreaming: false }];
|
|
181
|
+
}
|
|
182
|
+
// turn_aborted: Silent. The interruption is already visible from the agent stopping.
|
|
183
|
+
if (payloadType === 'turn_aborted') {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
// user_message / agent_message are handled via item.completed, skip if seen here
|
|
187
|
+
if (payloadType === 'user_message' || payloadType === 'agent_message') {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
// task_complete: Display the final agent message as formatted text
|
|
191
|
+
if (payloadType === 'task_complete') {
|
|
192
|
+
const lastMsg = asString(payload.last_agent_message);
|
|
193
|
+
if (lastMsg) {
|
|
194
|
+
return [{ type: 'text', text: lastMsg, isStreaming: false }];
|
|
195
|
+
}
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
return [this.buildUnknownEventFallback(`Unhandled Codex event_msg type: ${payloadType}`, payload)];
|
|
199
|
+
}
|
|
200
|
+
parseResponseItem(payload) {
|
|
201
|
+
if (!payload?.type)
|
|
202
|
+
return [];
|
|
203
|
+
// Codex can emit web search activity wrapped in response_item payloads.
|
|
204
|
+
// Emit synthetic start/result so terminal streaming and history stay consistent.
|
|
205
|
+
if (payload.type === 'web_search_call') {
|
|
206
|
+
const toolName = 'web_search';
|
|
207
|
+
const toolInput = {
|
|
208
|
+
actionType: payload.action?.type,
|
|
209
|
+
actionQuery: payload.action?.query,
|
|
210
|
+
actionQueries: payload.action?.queries,
|
|
211
|
+
actionUrl: payload.action?.url,
|
|
212
|
+
status: payload.status,
|
|
213
|
+
};
|
|
214
|
+
if (payload.status === 'completed') {
|
|
215
|
+
return [
|
|
216
|
+
{ type: 'tool_start', toolName, toolInput },
|
|
217
|
+
{ type: 'tool_result', toolName, toolOutput: JSON.stringify(toolInput) },
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
if (payload.status === 'in_progress' || payload.status === 'started') {
|
|
221
|
+
return [{ type: 'tool_start', toolName, toolInput }];
|
|
222
|
+
}
|
|
223
|
+
return [{ type: 'tool_result', toolName, toolOutput: JSON.stringify(toolInput) }];
|
|
224
|
+
}
|
|
225
|
+
// reasoning: Map summary text to thinking event
|
|
226
|
+
if (payload.type === 'reasoning') {
|
|
227
|
+
const text = this.extractReasoningSummaryText(payload);
|
|
228
|
+
if (!text)
|
|
229
|
+
return [];
|
|
230
|
+
return [{ type: 'thinking', text, isStreaming: false }];
|
|
231
|
+
}
|
|
232
|
+
// custom_tool_call: Map to tool_start (with special handling for apply_patch)
|
|
233
|
+
if (payload.type === 'custom_tool_call') {
|
|
234
|
+
return this.parseCustomToolCall(payload);
|
|
235
|
+
}
|
|
236
|
+
// custom_tool_call_output: Map to tool_result
|
|
237
|
+
if (payload.type === 'custom_tool_call_output') {
|
|
238
|
+
return this.parseCustomToolCallOutput(payload);
|
|
239
|
+
}
|
|
240
|
+
return [this.buildUnknownEventFallback(`Unhandled Codex response_item payload type: ${payload.type}`, payload)];
|
|
241
|
+
}
|
|
242
|
+
extractReasoningSummaryText(payload) {
|
|
243
|
+
if (!payload.summary || !Array.isArray(payload.summary))
|
|
244
|
+
return undefined;
|
|
245
|
+
const texts = payload.summary
|
|
246
|
+
.map((s) => s.text)
|
|
247
|
+
.filter((t) => Boolean(t));
|
|
248
|
+
return texts.length > 0 ? texts.join('\n') : undefined;
|
|
249
|
+
}
|
|
250
|
+
parseCustomToolCall(payload) {
|
|
251
|
+
const toolName = payload.name || 'unknown_tool';
|
|
252
|
+
const callId = payload.call_id;
|
|
253
|
+
const input = payload.input || '';
|
|
254
|
+
// Track tool name by call_id for correlating with custom_tool_call_output
|
|
255
|
+
if (callId) {
|
|
256
|
+
this.activeToolByItemId.set(callId, toolName);
|
|
257
|
+
}
|
|
258
|
+
// Special handling for apply_patch: generate synthetic Edit/Write events
|
|
259
|
+
if (toolName === 'apply_patch' && input) {
|
|
260
|
+
const inferredCalls = this.extractApplyPatchOperations(input);
|
|
261
|
+
if (inferredCalls.length > 0) {
|
|
262
|
+
const events = [];
|
|
263
|
+
for (const call of inferredCalls) {
|
|
264
|
+
const filePath = this.stringField(call.toolInput.file_path);
|
|
265
|
+
if (filePath) {
|
|
266
|
+
const uiPath = this.normalizePathForUi(filePath);
|
|
267
|
+
if (uiPath) {
|
|
268
|
+
call.toolInput.file_path = uiPath;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
events.push({
|
|
272
|
+
type: 'tool_start',
|
|
273
|
+
toolName: call.toolName,
|
|
274
|
+
toolInput: call.toolInput,
|
|
275
|
+
});
|
|
276
|
+
if (call.toolOutput) {
|
|
277
|
+
events.push({
|
|
278
|
+
type: 'tool_result',
|
|
279
|
+
toolName: call.toolName,
|
|
280
|
+
toolOutput: call.toolOutput,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return events;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Generic tool_start for non-apply_patch or when patch parsing yields nothing
|
|
288
|
+
return [{
|
|
289
|
+
type: 'tool_start',
|
|
290
|
+
toolName,
|
|
291
|
+
toolInput: { input, call_id: callId },
|
|
292
|
+
}];
|
|
293
|
+
}
|
|
294
|
+
parseCustomToolCallOutput(payload) {
|
|
295
|
+
const callId = payload.call_id;
|
|
296
|
+
const toolName = callId
|
|
297
|
+
? (this.activeToolByItemId.get(callId) ?? 'unknown_tool')
|
|
298
|
+
: 'unknown_tool';
|
|
299
|
+
if (callId) {
|
|
300
|
+
this.activeToolByItemId.delete(callId);
|
|
301
|
+
}
|
|
302
|
+
let toolOutput = payload.output || '';
|
|
303
|
+
// For apply_patch, extract the meaningful output from the JSON wrapper
|
|
304
|
+
if (toolName === 'apply_patch' && toolOutput) {
|
|
305
|
+
try {
|
|
306
|
+
const parsed = JSON.parse(toolOutput);
|
|
307
|
+
if (isObject(parsed)) {
|
|
308
|
+
const outputText = asString(parsed.output);
|
|
309
|
+
if (outputText) {
|
|
310
|
+
toolOutput = outputText;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
// Use raw output string as-is
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return [{
|
|
319
|
+
type: 'tool_result',
|
|
320
|
+
toolName: toolName === 'apply_patch' ? 'Edit' : toolName,
|
|
321
|
+
toolOutput,
|
|
322
|
+
}];
|
|
125
323
|
}
|
|
126
324
|
parseItemStarted(item) {
|
|
127
325
|
if (!item?.type)
|
|
@@ -152,7 +350,7 @@ export class CodexJsonEventParser {
|
|
|
152
350
|
},
|
|
153
351
|
];
|
|
154
352
|
}
|
|
155
|
-
return [];
|
|
353
|
+
return [this.buildUnknownEventFallback(`Unhandled Codex item.started type: ${item.type}`, item)];
|
|
156
354
|
}
|
|
157
355
|
parseItemCompleted(item) {
|
|
158
356
|
if (!item?.type)
|
|
@@ -203,7 +401,12 @@ export class CodexJsonEventParser {
|
|
|
203
401
|
},
|
|
204
402
|
];
|
|
205
403
|
}
|
|
206
|
-
|
|
404
|
+
// file_change: Silent. File change info is already captured from the
|
|
405
|
+
// command_execution or custom_tool_call that caused it.
|
|
406
|
+
if (item.type === 'file_change') {
|
|
407
|
+
return [];
|
|
408
|
+
}
|
|
409
|
+
return [this.buildUnknownEventFallback(`Unhandled Codex item.completed type: ${item.type}`, item)];
|
|
207
410
|
}
|
|
208
411
|
parseTurnCompleted(usage) {
|
|
209
412
|
if (!usage)
|
|
@@ -223,6 +426,23 @@ export class CodexJsonEventParser {
|
|
|
223
426
|
}
|
|
224
427
|
return [event];
|
|
225
428
|
}
|
|
429
|
+
buildUnknownEventFallback(prefix, payload) {
|
|
430
|
+
let serialized = '';
|
|
431
|
+
try {
|
|
432
|
+
serialized = JSON.stringify(payload);
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
serialized = String(payload);
|
|
436
|
+
}
|
|
437
|
+
if (serialized.length > MAX_FALLBACK_EVENT_TEXT) {
|
|
438
|
+
serialized = `${serialized.slice(0, MAX_FALLBACK_EVENT_TEXT)}...`;
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
type: 'text',
|
|
442
|
+
text: `[codex-event] ${prefix}\n${serialized}`,
|
|
443
|
+
isStreaming: false,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
226
446
|
buildWebSearchToolInput(item) {
|
|
227
447
|
return {
|
|
228
448
|
query: item.query,
|
|
@@ -2,7 +2,7 @@ export const fullNotifications = {
|
|
|
2
2
|
slug: 'full-notifications',
|
|
3
3
|
name: 'Full Notifications',
|
|
4
4
|
description: 'Send notification via browser, android or in-app',
|
|
5
|
-
allowedTools: ['Bash(curl:*)'
|
|
5
|
+
allowedTools: ['Bash(curl:*)'],
|
|
6
6
|
content: `# Task Completion Notifications (MANDATORY)
|
|
7
7
|
|
|
8
8
|
**IMPORTANT: You MUST send a notification automatically whenever you complete a task. This is not optional.**
|
|
@@ -14,31 +14,26 @@ export const fullNotifications = {
|
|
|
14
14
|
|
|
15
15
|
## Command Template
|
|
16
16
|
\`\`\`bash
|
|
17
|
-
curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/json" -d '{"agentId":"YOUR_AGENT_ID","title":"TITLE","message":"MESSAGE"}'
|
|
17
|
+
curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/json" -d '{"agentId":"YOUR_AGENT_ID","title":"TITLE","message":"MESSAGE"}'
|
|
18
18
|
\`\`\`
|
|
19
19
|
|
|
20
20
|
## Examples by Type
|
|
21
21
|
|
|
22
22
|
**Task Complete:**
|
|
23
23
|
\`\`\`bash
|
|
24
|
-
curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/json" -d '{"agentId":"YOUR_AGENT_ID","title":"Task Complete","message":"Build succeeded"}'
|
|
24
|
+
curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/json" -d '{"agentId":"YOUR_AGENT_ID","title":"Task Complete","message":"Build succeeded"}'
|
|
25
25
|
\`\`\`
|
|
26
26
|
|
|
27
27
|
**Error/Attention Needed:**
|
|
28
28
|
\`\`\`bash
|
|
29
|
-
curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/json" -d '{"agentId":"YOUR_AGENT_ID","title":"Error","message":"Build failed"}'
|
|
29
|
+
curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/json" -d '{"agentId":"YOUR_AGENT_ID","title":"Error","message":"Build failed"}'
|
|
30
30
|
\`\`\`
|
|
31
31
|
|
|
32
32
|
**Input Required:**
|
|
33
33
|
\`\`\`bash
|
|
34
|
-
curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/json" -d '{"agentId":"YOUR_AGENT_ID","title":"Input Needed","message":"Which database?"}'
|
|
34
|
+
curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/json" -d '{"agentId":"YOUR_AGENT_ID","title":"Input Needed","message":"Which database?"}'
|
|
35
35
|
\`\`\`
|
|
36
36
|
|
|
37
|
-
## Icons (gdbus)
|
|
38
|
-
- \`dialog-information\` - Task complete
|
|
39
|
-
- \`dialog-warning\` - Error/attention needed
|
|
40
|
-
- \`dialog-question\` - Input required
|
|
41
|
-
|
|
42
37
|
## Rules
|
|
43
38
|
- Replace \`YOUR_AGENT_ID\` with your actual agent ID from the system prompt
|
|
44
39
|
- Keep messages under 50 characters
|
|
@@ -49,6 +44,5 @@ curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/j
|
|
|
49
44
|
- If task involves waiting for other agents to finish, do NOT notify until they confirm completion
|
|
50
45
|
- Only notify when YOU have nothing more to do on this task
|
|
51
46
|
- Send notification as your FINAL action after completing work
|
|
52
|
-
- Do NOT skip this step - the user relies on notifications
|
|
53
|
-
- The \`&\` runs both commands in parallel (curl for mobile/browser, gdbus for Linux desktop)`,
|
|
47
|
+
- Do NOT skip this step - the user relies on notifications`,
|
|
54
48
|
};
|
|
@@ -12,7 +12,7 @@ import { getClaudeProjectDir } from '../data/index.js';
|
|
|
12
12
|
// Session listing is done inline for performance
|
|
13
13
|
import { createLogger } from '../utils/logger.js';
|
|
14
14
|
import { buildCustomAgentConfig } from '../websocket/handlers/command-handler.js';
|
|
15
|
-
import { getSystemPrompt, setSystemPrompt, clearSystemPrompt, isEchoPromptEnabled, setEchoPromptEnabled } from '../services/system-prompt-service.js';
|
|
15
|
+
import { getSystemPrompt, setSystemPrompt, clearSystemPrompt, isEchoPromptEnabled, setEchoPromptEnabled, getCodexBinaryPath, setCodexBinaryPath } from '../services/system-prompt-service.js';
|
|
16
16
|
const log = createLogger('Routes');
|
|
17
17
|
const router = Router();
|
|
18
18
|
function runCommandWithTimeout(command, args, timeoutMs, cwd) {
|
|
@@ -482,4 +482,32 @@ router.post('/system-settings/echo-prompt', (req, res) => {
|
|
|
482
482
|
res.status(500).json({ error: err.message });
|
|
483
483
|
}
|
|
484
484
|
});
|
|
485
|
+
// GET /api/system-settings/codex-binary - Get the codex binary path
|
|
486
|
+
router.get('/system-settings/codex-binary', (_req, res) => {
|
|
487
|
+
try {
|
|
488
|
+
const binaryPath = getCodexBinaryPath();
|
|
489
|
+
res.json({ path: binaryPath });
|
|
490
|
+
}
|
|
491
|
+
catch (err) {
|
|
492
|
+
log.error(' Failed to get codex binary path:', err);
|
|
493
|
+
res.status(500).json({ error: err.message });
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
// POST /api/system-settings/codex-binary - Set the codex binary path
|
|
497
|
+
router.post('/system-settings/codex-binary', (req, res) => {
|
|
498
|
+
try {
|
|
499
|
+
const { path: binaryPath } = req.body;
|
|
500
|
+
if (typeof binaryPath !== 'string') {
|
|
501
|
+
res.status(400).json({ error: 'path must be a string' });
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
setCodexBinaryPath(binaryPath);
|
|
505
|
+
log.log(` Codex binary path updated: ${binaryPath || '(cleared)'}`);
|
|
506
|
+
res.json({ success: true, path: binaryPath });
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
log.error(' Failed to set codex binary path:', err);
|
|
510
|
+
res.status(500).json({ error: err.message });
|
|
511
|
+
}
|
|
512
|
+
});
|
|
485
513
|
export default router;
|
|
@@ -97,7 +97,7 @@ const CONFIG_CATEGORIES = [
|
|
|
97
97
|
id: 'system-settings',
|
|
98
98
|
name: 'System Settings',
|
|
99
99
|
description: 'Global system prompt and settings',
|
|
100
|
-
files: ['system-prompt.json', 'echo-prompt-setting.json'],
|
|
100
|
+
files: ['system-prompt.json', 'echo-prompt-setting.json', 'codex-binary-path.json'],
|
|
101
101
|
sourceDir: 'data',
|
|
102
102
|
},
|
|
103
103
|
];
|
|
@@ -115,3 +115,58 @@ export function setEchoPromptEnabled(enabled) {
|
|
|
115
115
|
throw error;
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Codex Binary Path Setting
|
|
120
|
+
// ============================================================================
|
|
121
|
+
const CODEX_BINARY_FILE = path.join(DATA_DIR, 'codex-binary-path.json');
|
|
122
|
+
/**
|
|
123
|
+
* Get the configured codex binary path (empty string if not set)
|
|
124
|
+
*/
|
|
125
|
+
export function getCodexBinaryPath() {
|
|
126
|
+
ensureDataDir();
|
|
127
|
+
try {
|
|
128
|
+
if (fs.existsSync(CODEX_BINARY_FILE)) {
|
|
129
|
+
const data = JSON.parse(fs.readFileSync(CODEX_BINARY_FILE, 'utf-8'));
|
|
130
|
+
return data.path;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
log.error(` Failed to load codex binary path: ${error.message}`);
|
|
135
|
+
}
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Set the codex binary path
|
|
140
|
+
*/
|
|
141
|
+
export function setCodexBinaryPath(binaryPath) {
|
|
142
|
+
ensureDataDir();
|
|
143
|
+
const trimmed = binaryPath.trim();
|
|
144
|
+
if (trimmed) {
|
|
145
|
+
const data = {
|
|
146
|
+
path: trimmed,
|
|
147
|
+
updatedAt: Date.now(),
|
|
148
|
+
};
|
|
149
|
+
fs.writeFileSync(CODEX_BINARY_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
150
|
+
log.log(` Codex binary path set: ${trimmed}`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// Empty means clear
|
|
154
|
+
clearCodexBinaryPath();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Clear the codex binary path (revert to auto-detect)
|
|
159
|
+
*/
|
|
160
|
+
export function clearCodexBinaryPath() {
|
|
161
|
+
ensureDataDir();
|
|
162
|
+
try {
|
|
163
|
+
if (fs.existsSync(CODEX_BINARY_FILE)) {
|
|
164
|
+
fs.unlinkSync(CODEX_BINARY_FILE);
|
|
165
|
+
log.log(` Codex binary path cleared (will auto-detect)`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
log.error(` Failed to clear codex binary path: ${error.message}`);
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|