@supen-ai/cli 1.3.2 → 1.3.4

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.
Files changed (33) hide show
  1. package/README.md +1 -1
  2. package/daemon/dist/agent-sdk/driver-output-ui.d.ts +1 -1
  3. package/daemon/dist/agent-sdk/driver-output-ui.d.ts.map +1 -1
  4. package/daemon/dist/agent-sdk/driver-output-ui.js +10 -275
  5. package/daemon/dist/agent-sdk/driver-output-ui.js.map +1 -1
  6. package/daemon/dist/channels/http-routes.d.ts.map +1 -1
  7. package/daemon/dist/channels/http-routes.js +394 -26
  8. package/daemon/dist/channels/http-routes.js.map +1 -1
  9. package/daemon/dist/core/cortex.d.ts.map +1 -1
  10. package/daemon/dist/core/cortex.js +0 -17
  11. package/daemon/dist/core/cortex.js.map +1 -1
  12. package/daemon/dist/core/thread-runtime-state.d.ts.map +1 -1
  13. package/daemon/dist/core/thread-runtime-state.js +1 -37
  14. package/daemon/dist/core/thread-runtime-state.js.map +1 -1
  15. package/daemon/dist/http/routes/rpc.d.ts.map +1 -1
  16. package/daemon/dist/http/routes/rpc.js +20 -3
  17. package/daemon/dist/http/routes/rpc.js.map +1 -1
  18. package/daemon/dist/http/routes/sessions.d.ts.map +1 -1
  19. package/daemon/dist/http/routes/sessions.js +2 -225
  20. package/daemon/dist/http/routes/sessions.js.map +1 -1
  21. package/daemon/dist/http/routes/system.d.ts.map +1 -1
  22. package/daemon/dist/http/routes/system.js +101 -390
  23. package/daemon/dist/http/routes/system.js.map +1 -1
  24. package/daemon/dist/http/websocket.js +1 -1
  25. package/daemon/dist/http/websocket.js.map +1 -1
  26. package/daemon/dist/index.d.ts.map +1 -1
  27. package/daemon/dist/index.js +4 -2
  28. package/daemon/dist/index.js.map +1 -1
  29. package/daemon/package.json +1 -1
  30. package/dist/computer.js +1 -1
  31. package/dist/index.js +1 -1
  32. package/dist/index.js.map +1 -1
  33. package/package.json +1 -1
@@ -31,10 +31,6 @@ const MIRRORED_THREAD_LIMIT = 80;
31
31
  const MIRRORED_THREAD_HISTORY_LIMIT = 100;
32
32
  const MIRRORED_THREAD_HISTORY_MAX_BYTES = 64 * 1024 * 1024;
33
33
  const MIRRORED_THREAD_TEXT_LIMIT = 48_000;
34
- const MIRRORED_THREAD_TOOL_STRING_LIMIT = 24_000;
35
- const MIRRORED_THREAD_OBJECT_DEPTH_LIMIT = 4;
36
- const MIRRORED_THREAD_OBJECT_KEY_LIMIT = 80;
37
- const MIRRORED_THREAD_OBJECT_ARRAY_LIMIT = 80;
38
34
  const MIRRORED_THREAD_INLINE_DATA_URL_LIMIT = 120_000;
39
35
  const MIRRORED_THREAD_STREAM_REPLAY_LIMIT = 500;
40
36
  const MIRRORED_THREAD_STREAM_REPLAY_MAX_BYTES = 8 * 1024 * 1024;
@@ -44,7 +40,7 @@ const require = createRequire(import.meta.url);
44
40
  const HOST_DAEMON_INSTALL_DIR = path.join(SUPEN_HOME, 'daemon');
45
41
  const HOST_DAEMON_CLI_PACKAGE_ROOT = path.join(HOST_DAEMON_INSTALL_DIR, 'node_modules', '@supen-ai', 'cli');
46
42
  const HOST_DAEMON_BIN_PATH = path.join(HOST_DAEMON_CLI_PACKAGE_ROOT, 'daemon', 'scripts', 'supen-daemon.js');
47
- const HOST_DAEMON_PACKAGE_SPEC = '@supen-ai/cli';
43
+ const HOST_DAEMON_PACKAGE_SPEC = '@supen-ai/cli@latest';
48
44
  function readSqliteRows(dbPath, query) {
49
45
  try {
50
46
  const { DatabaseSync } = require('node:sqlite');
@@ -953,34 +949,6 @@ function truncateMirroredHistoryString(value, limit, label) {
953
949
  const omitted = value.length - limit;
954
950
  return `${value.slice(0, limit)}\n\n[${label} truncated: ${omitted.toLocaleString()} more characters omitted]`;
955
951
  }
956
- function sanitizeMirroredHistoryPayload(value, depth = 0) {
957
- if (typeof value === 'string') {
958
- return truncateMirroredHistoryString(value, MIRRORED_THREAD_TOOL_STRING_LIMIT, 'Tool output');
959
- }
960
- if (value === null || typeof value !== 'object')
961
- return value;
962
- if (depth >= MIRRORED_THREAD_OBJECT_DEPTH_LIMIT) {
963
- return Array.isArray(value) ? '[Nested array omitted]' : '[Nested object omitted]';
964
- }
965
- if (Array.isArray(value)) {
966
- const visibleItems = value
967
- .slice(0, MIRRORED_THREAD_OBJECT_ARRAY_LIMIT)
968
- .map((item) => sanitizeMirroredHistoryPayload(item, depth + 1));
969
- if (value.length > MIRRORED_THREAD_OBJECT_ARRAY_LIMIT) {
970
- visibleItems.push(`[${value.length - MIRRORED_THREAD_OBJECT_ARRAY_LIMIT} more items omitted]`);
971
- }
972
- return visibleItems;
973
- }
974
- const result = {};
975
- const entries = Object.entries(value);
976
- for (const [key, entryValue] of entries.slice(0, MIRRORED_THREAD_OBJECT_KEY_LIMIT)) {
977
- result[key] = sanitizeMirroredHistoryPayload(entryValue, depth + 1);
978
- }
979
- if (entries.length > MIRRORED_THREAD_OBJECT_KEY_LIMIT) {
980
- result.__truncated_keys = `${entries.length - MIRRORED_THREAD_OBJECT_KEY_LIMIT} more keys omitted`;
981
- }
982
- return result;
983
- }
984
952
  function safeMirroredHistoryAttachmentUrl(url) {
985
953
  const trimmed = url.trim();
986
954
  if (trimmed.startsWith('data:') &&
@@ -1088,111 +1056,6 @@ function isMirroredGoalContextMessage(text) {
1088
1056
  const trimmed = text.trim();
1089
1057
  return trimmed.startsWith('<goal_context>') && trimmed.endsWith('</goal_context>');
1090
1058
  }
1091
- function parseJsonObject(value) {
1092
- if (typeof value !== 'string')
1093
- return value;
1094
- try {
1095
- return JSON.parse(value);
1096
- }
1097
- catch {
1098
- return value;
1099
- }
1100
- }
1101
- function readableOutputFromMcpResult(result) {
1102
- if (!result || typeof result !== 'object')
1103
- return result;
1104
- const ok = result.Ok;
1105
- const error = result.Err;
1106
- const payload = ok && typeof ok === 'object' ? ok : error;
1107
- if (!payload || typeof payload !== 'object')
1108
- return result;
1109
- const content = payload.content;
1110
- if (!Array.isArray(content))
1111
- return payload;
1112
- const text = content
1113
- .map((entry) => {
1114
- if (typeof entry === 'string')
1115
- return entry;
1116
- if (entry && typeof entry === 'object' && typeof entry.text === 'string') {
1117
- return entry.text;
1118
- }
1119
- return '';
1120
- })
1121
- .filter(Boolean)
1122
- .join('\n');
1123
- return text || payload;
1124
- }
1125
- function fileNameFromPath(value) {
1126
- const trimmed = value.trim();
1127
- if (!trimmed)
1128
- return '';
1129
- return path.basename(trimmed);
1130
- }
1131
- function displayPatchPath(filePath, workspacePath) {
1132
- const trimmed = filePath.trim();
1133
- if (!trimmed || !workspacePath || !path.isAbsolute(trimmed))
1134
- return trimmed;
1135
- const relativePath = path.relative(workspacePath, trimmed);
1136
- if (!relativePath || relativePath.startsWith('..') || path.isAbsolute(relativePath))
1137
- return trimmed;
1138
- return relativePath;
1139
- }
1140
- function diffStatsFromText(value) {
1141
- let additions = 0;
1142
- let deletions = 0;
1143
- for (const line of value.split(/\r?\n/)) {
1144
- if (line.startsWith('+++') || line.startsWith('---'))
1145
- continue;
1146
- if (line.startsWith('+'))
1147
- additions += 1;
1148
- if (line.startsWith('-'))
1149
- deletions += 1;
1150
- }
1151
- return { additions, deletions };
1152
- }
1153
- function extractPatchFiles(input) {
1154
- if (typeof input !== 'string')
1155
- return [];
1156
- const files = [];
1157
- const seen = new Set();
1158
- for (const line of input.split(/\r?\n/)) {
1159
- const match = line.match(/^\*\*\* (?:Update|Add|Delete) File:\s+(.+?)\s*$/);
1160
- if (!match)
1161
- continue;
1162
- const filePath = match[1]?.trim();
1163
- if (!filePath || seen.has(filePath))
1164
- continue;
1165
- seen.add(filePath);
1166
- files.push({ path: filePath, name: fileNameFromPath(filePath) || filePath });
1167
- }
1168
- return files;
1169
- }
1170
- function extractPatchEndFiles(changes, workspacePath) {
1171
- if (!changes || typeof changes !== 'object')
1172
- return [];
1173
- return Object.entries(changes).map(([filePath, change]) => {
1174
- const diff = typeof change?.unified_diff === 'string' ? change.unified_diff : '';
1175
- const displayPath = displayPatchPath(filePath, workspacePath);
1176
- return {
1177
- path: displayPath,
1178
- name: fileNameFromPath(displayPath) || displayPath,
1179
- ...(diff ? { diff } : {}),
1180
- ...diffStatsFromText(diff),
1181
- };
1182
- });
1183
- }
1184
- function codexReplayEvent(eventId, timestamp, taskId, raw) {
1185
- return {
1186
- id: eventId,
1187
- timestamp,
1188
- task_id: taskId,
1189
- chunk: {
1190
- type: 'data-codex-event',
1191
- id: eventId,
1192
- data: { raw },
1193
- },
1194
- };
1195
- }
1196
1059
  function mirroredThreadWorkspacePath(sessionPath, stateEntry) {
1197
1060
  const stateCwd = typeof stateEntry?.cwd === 'string' ? stateEntry.cwd : null;
1198
1061
  const sessionWorkspace = readThreadWorkspacePath(sessionPath);
@@ -1216,10 +1079,8 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1216
1079
  const messages = [];
1217
1080
  const events = [];
1218
1081
  let messageIndex = 0;
1219
- let eventIndex = 0;
1220
1082
  let latestTimestamp = indexEntry?.updated_at || '';
1221
1083
  let recentGoalContext = null;
1222
- const toolCalls = new Map();
1223
1084
  const mirroredUserMessageKey = (text) => sanitizeMirroredUserText(text)
1224
1085
  .replace(/\s+/g, ' ')
1225
1086
  .trim();
@@ -1309,39 +1170,6 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1309
1170
  const messageId = typeof payload.id === 'string' ? payload.id : `${threadId}-web-user-${event.sequence}`;
1310
1171
  pushUserMessage(text, timestamp, messageId, attachments);
1311
1172
  };
1312
- const pushEvent = (timestamp, chunk) => {
1313
- eventIndex += 1;
1314
- events.push({
1315
- id: `${threadId}-event-${eventIndex}`,
1316
- timestamp,
1317
- task_id: threadId,
1318
- chunk,
1319
- });
1320
- };
1321
- const pushToolInput = (timestamp, toolCallId, toolName, input) => {
1322
- const sanitizedInput = sanitizeMirroredHistoryPayload(input);
1323
- toolCalls.set(toolCallId, { toolName, input: sanitizedInput });
1324
- pushEvent(timestamp, {
1325
- type: 'tool-input-available',
1326
- toolCallId,
1327
- toolName,
1328
- input: sanitizedInput,
1329
- });
1330
- };
1331
- const pushToolOutput = (timestamp, toolCallId, output, fallbackToolName, fallbackInput, failed = false) => {
1332
- const toolCall = toolCalls.get(toolCallId);
1333
- const toolName = toolCall?.toolName || fallbackToolName;
1334
- const input = toolCall?.input ?? (fallbackInput === undefined ? undefined : sanitizeMirroredHistoryPayload(fallbackInput));
1335
- pushEvent(timestamp, {
1336
- type: failed ? 'tool-output-error' : 'tool-output-available',
1337
- toolCallId,
1338
- ...(toolName ? { toolName } : {}),
1339
- ...(input !== undefined ? { input } : {}),
1340
- ...(failed
1341
- ? { errorText: typeof output === 'string' ? output : 'Tool failed' }
1342
- : { output: sanitizeMirroredHistoryPayload(parseJsonObject(output)) }),
1343
- });
1344
- };
1345
1173
  for (const line of lines) {
1346
1174
  const parsed = parseJsonLine(line);
1347
1175
  if (typeof parsed?.timestamp === 'string')
@@ -1356,146 +1184,11 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1356
1184
  if (payload.type === 'user_message') {
1357
1185
  const text = typeof payload.message === 'string' ? payload.message.trim() : '';
1358
1186
  pushUserMessage(text, timestamp, undefined, imageAttachmentsFromUnknown(payload.images));
1359
- continue;
1360
- }
1361
- if (payload.type === 'turn_aborted') {
1362
- if (eventLogThreadId !== threadId) {
1363
- events.push(codexReplayEvent(`${threadId}-turn-aborted-${eventIndex + 1}`, timestamp, threadId, {
1364
- method: 'turn/completed',
1365
- params: {
1366
- turn: {
1367
- id: payload.turn_id || payload.turnId || 'turn',
1368
- status: 'interrupted',
1369
- },
1370
- },
1371
- }));
1372
- eventIndex += 1;
1373
- }
1374
- continue;
1375
- }
1376
- if (payload.type === 'context_compacted') {
1377
- events.push(codexReplayEvent(`${threadId}-context-compacted-${eventIndex + 1}`, timestamp, threadId, {
1378
- method: 'contextCompaction/completed',
1379
- params: {
1380
- threadId,
1381
- messageCount: payload.message_count || payload.messageCount,
1382
- tokensSaved: payload.tokens_saved || payload.tokensSaved,
1383
- },
1384
- }));
1385
- eventIndex += 1;
1386
- continue;
1387
- }
1388
- if (payload.type === 'web_search_end') {
1389
- const toolCallId = typeof payload.call_id === 'string' ? payload.call_id : `${threadId}-web-search-${eventIndex + 1}`;
1390
- const input = {
1391
- query: payload.query,
1392
- action: payload.action,
1393
- };
1394
- pushToolInput(timestamp, toolCallId, 'web_search', input);
1395
- pushToolOutput(timestamp, toolCallId, payload.action || payload.query || 'completed');
1396
- continue;
1397
- }
1398
- if (payload.type === 'mcp_tool_call_end') {
1399
- const toolCallId = typeof payload.call_id === 'string' ? payload.call_id : `${threadId}-mcp-tool-${eventIndex + 1}`;
1400
- const invocation = payload.invocation && typeof payload.invocation === 'object'
1401
- ? payload.invocation
1402
- : {};
1403
- const server = typeof invocation.server === 'string' ? invocation.server : '';
1404
- const tool = typeof invocation.tool === 'string' ? invocation.tool : '';
1405
- const toolName = [server, tool].filter(Boolean).join('.') || 'mcpToolCall';
1406
- const input = invocation.arguments;
1407
- const result = payload.result && typeof payload.result === 'object'
1408
- ? payload.result
1409
- : payload.result;
1410
- const failed = Boolean(result && typeof result === 'object' && 'Err' in result);
1411
- pushToolInput(timestamp, toolCallId, toolName, input);
1412
- pushToolOutput(timestamp, toolCallId, readableOutputFromMcpResult(result), toolName, input, failed);
1413
- continue;
1414
- }
1415
- if (payload.type === 'patch_apply_end') {
1416
- const files = extractPatchEndFiles(payload.changes, workspacePath);
1417
- const additions = files.reduce((sum, file) => sum + file.additions, 0);
1418
- const deletions = files.reduce((sum, file) => sum + file.deletions, 0);
1419
- events.push(codexReplayEvent(`${threadId}-patch-end-${eventIndex + 1}`, timestamp, threadId, {
1420
- method: 'codex/patchApply/completed',
1421
- params: {
1422
- callId: payload.call_id,
1423
- success: payload.success !== false,
1424
- files,
1425
- fileCount: files.length,
1426
- additions,
1427
- deletions,
1428
- status: payload.status,
1429
- },
1430
- }));
1431
- eventIndex += 1;
1432
- continue;
1433
1187
  }
1434
- }
1435
- if (parsed?.type === 'compacted') {
1436
- events.push(codexReplayEvent(`${threadId}-context-compacted-${eventIndex + 1}`, timestamp, threadId, {
1437
- method: 'contextCompaction/completed',
1438
- params: { threadId },
1439
- }));
1440
- eventIndex += 1;
1441
1188
  continue;
1442
1189
  }
1443
1190
  if (parsed?.type !== 'response_item')
1444
1191
  continue;
1445
- if (payload.type === 'function_call') {
1446
- const toolCallId = typeof payload.call_id === 'string' ? payload.call_id : `${threadId}-tool-${eventIndex + 1}`;
1447
- const toolName = typeof payload.name === 'string' ? payload.name : 'tool';
1448
- const input = sanitizeMirroredHistoryPayload(parseJsonObject(payload.arguments));
1449
- pushToolInput(timestamp, toolCallId, toolName, input);
1450
- continue;
1451
- }
1452
- if (payload.type === 'function_call_output') {
1453
- const toolCallId = typeof payload.call_id === 'string' ? payload.call_id : `${threadId}-tool-${eventIndex + 1}`;
1454
- pushToolOutput(timestamp, toolCallId, payload.output);
1455
- continue;
1456
- }
1457
- if (payload.type === 'tool_search_call') {
1458
- const toolCallId = typeof payload.call_id === 'string' ? payload.call_id : `${threadId}-tool-search-${eventIndex + 1}`;
1459
- pushToolInput(timestamp, toolCallId, 'tool_search', payload.arguments);
1460
- continue;
1461
- }
1462
- if (payload.type === 'tool_search_output') {
1463
- const toolCallId = typeof payload.call_id === 'string' ? payload.call_id : `${threadId}-tool-search-${eventIndex + 1}`;
1464
- pushToolOutput(timestamp, toolCallId, {
1465
- status: payload.status,
1466
- tools: payload.tools,
1467
- }, 'tool_search');
1468
- continue;
1469
- }
1470
- if (payload.type === 'custom_tool_call') {
1471
- const toolName = typeof payload.name === 'string' ? payload.name : 'custom_tool';
1472
- const toolCallId = typeof payload.call_id === 'string' ? payload.call_id : `${threadId}-tool-${eventIndex + 1}`;
1473
- if (toolName === 'apply_patch') {
1474
- const input = typeof payload.input === 'string' ? payload.input : '';
1475
- const files = extractPatchFiles(input);
1476
- events.push(codexReplayEvent(`${threadId}-patch-start-${eventIndex + 1}`, timestamp, threadId, {
1477
- method: 'codex/patchApply/started',
1478
- params: {
1479
- callId: payload.call_id,
1480
- files,
1481
- fileCount: files.length,
1482
- ...diffStatsFromText(input),
1483
- },
1484
- }));
1485
- eventIndex += 1;
1486
- }
1487
- else {
1488
- pushToolInput(timestamp, toolCallId, toolName, parseJsonObject(payload.input));
1489
- }
1490
- continue;
1491
- }
1492
- if (payload.type === 'custom_tool_call_output') {
1493
- const toolCallId = typeof payload.call_id === 'string' ? payload.call_id : `${threadId}-tool-${eventIndex + 1}`;
1494
- if (toolCalls.has(toolCallId)) {
1495
- pushToolOutput(timestamp, toolCallId, payload.output);
1496
- }
1497
- continue;
1498
- }
1499
1192
  if (payload.type !== 'message')
1500
1193
  continue;
1501
1194
  messageIndex += 1;
@@ -1615,6 +1308,16 @@ function coerceSingleQueryParam(value) {
1615
1308
  return value[0] || null;
1616
1309
  return value || null;
1617
1310
  }
1311
+ function codexThreadsListRoute(pathname) {
1312
+ return /^\/api\/computers\/\{computer_id\}\/agents\/[^/]+\/codex\/threads$/.test(pathname);
1313
+ }
1314
+ function codexThreadActionRoute(pathname, action) {
1315
+ const suffix = action === 'messages' ? 'messages' : action;
1316
+ return pathname.match(new RegExp(`^/api/computers/\\{computer_id\\}/agents/[^/]+/codex/threads/([^/]+)/${suffix}$`));
1317
+ }
1318
+ function codexProjectsOpenRoute(pathname) {
1319
+ return /^\/api\/computers\/\{computer_id\}\/agents\/[^/]+\/codex\/projects\/open$/.test(pathname);
1320
+ }
1618
1321
  function collectSpaceLogEntries(filters = {}) {
1619
1322
  const spaceId = currentSpaceId();
1620
1323
  const limit = Math.max(1, Math.min(filters.limit || SPACE_LOG_EVENT_LIMIT, 500));
@@ -1698,7 +1401,7 @@ function threadStreamChunkForEvent(event) {
1698
1401
  !Array.isArray(event.raw_payload) &&
1699
1402
  typeof event.raw_payload.type === 'string' &&
1700
1403
  String(event.raw_payload.type).trim()) {
1701
- return normalizeStoredCodexThreadChunk(event.raw_payload);
1404
+ return event.raw_payload;
1702
1405
  }
1703
1406
  return {
1704
1407
  type: 'data-codex-event',
@@ -1708,28 +1411,6 @@ function threadStreamChunkForEvent(event) {
1708
1411
  },
1709
1412
  };
1710
1413
  }
1711
- function normalizeStoredCodexThreadChunk(chunk) {
1712
- if (chunk.type !== 'data-supen-event')
1713
- return chunk;
1714
- const data = chunk.data && typeof chunk.data === 'object' && !Array.isArray(chunk.data)
1715
- ? chunk.data
1716
- : {};
1717
- const raw = data.raw && typeof data.raw === 'object' && !Array.isArray(data.raw)
1718
- ? data.raw
1719
- : {};
1720
- return {
1721
- ...chunk,
1722
- type: 'data-codex-event',
1723
- data: {
1724
- ...data,
1725
- eventType: typeof data.eventType === 'string' && data.eventType.trim()
1726
- ? data.eventType
1727
- : typeof raw.method === 'string' && raw.method.trim()
1728
- ? raw.method
1729
- : 'codex-event',
1730
- },
1731
- };
1732
- }
1733
1414
  function buildMcpSettingsResponse() {
1734
1415
  const overrides = getMcpEnvOverrides();
1735
1416
  const runtimeEnv = { ...process.env };
@@ -1764,6 +1445,15 @@ const MANAGED_CODING_CLI_INSTALL_COMMANDS = {
1764
1445
  codex: CODING_CLI_INSTALL_COMMANDS.codex,
1765
1446
  gemini: CODING_CLI_INSTALL_COMMANDS.gemini,
1766
1447
  };
1448
+ function resolveManagedCodingCliInstallCommand(cli, current) {
1449
+ if (cli === 'codex' && current.install_source === 'homebrew') {
1450
+ return {
1451
+ command: 'sh',
1452
+ args: ['-lc', 'brew upgrade codex || brew upgrade --cask codex || brew reinstall --cask codex'],
1453
+ };
1454
+ }
1455
+ return MANAGED_CODING_CLI_INSTALL_COMMANDS[cli];
1456
+ }
1767
1457
  const CODING_CLI_NAMES = ['codex', 'gemini', 'claude'];
1768
1458
  const DETECTABLE_SPACE_APPS = [
1769
1459
  { id: 'libreoffice', command: 'libreoffice', managed: false },
@@ -1883,7 +1573,6 @@ function readPackageVersion(packageRoot) {
1883
1573
  }
1884
1574
  }
1885
1575
  function detectDaemonPackageStatus() {
1886
- const envVersion = process.env.SUPEN_DAEMON_VERSION || process.env.npm_package_version || null;
1887
1576
  let packageRoot = null;
1888
1577
  try {
1889
1578
  packageRoot = findPackageRootFrom(require.resolve('@supen-ai/daemon'), '@supen-ai/daemon');
@@ -1926,7 +1615,7 @@ function detectDaemonPackageStatus() {
1926
1615
  ? 'local-package'
1927
1616
  : null;
1928
1617
  return {
1929
- version: envVersion || bundledDaemonVersion || packageVersion,
1618
+ version: bundledDaemonVersion || packageVersion,
1930
1619
  package_root: packageRoot || bundledDaemonRoot,
1931
1620
  install_source: installSource,
1932
1621
  update_supported: installSource === 'supen-cli-bundle' || installSource === 'npm-global',
@@ -1971,7 +1660,7 @@ function inferCliInstallSource(name, executablePath, resolvedPath) {
1971
1660
  return { install_source: 'pnpm', update_supported: false };
1972
1661
  }
1973
1662
  if (normalized.includes('/homebrew/') || normalized.includes('/opt/homebrew/') || normalized.includes('/cellar/')) {
1974
- return { install_source: 'homebrew', update_supported: false };
1663
+ return { install_source: 'homebrew', update_supported: name === 'codex' };
1975
1664
  }
1976
1665
  if (name === 'codex') {
1977
1666
  const npmRoot = detectGlobalNpmRoot();
@@ -2322,6 +2011,47 @@ function readInstalledAppsPayload() {
2322
2011
  }),
2323
2012
  };
2324
2013
  }
2014
+ const CODEX_SUBSCRIPTION_STATUS_WAIT_MS = 1800;
2015
+ const CODEX_SUBSCRIPTION_CACHE_TTL_MS = 5 * 60 * 1000;
2016
+ let cachedCodexSubscription = null;
2017
+ let codexSubscriptionRefreshInFlight = null;
2018
+ function refreshCodexSubscriptionStatus() {
2019
+ if (codexSubscriptionRefreshInFlight)
2020
+ return codexSubscriptionRefreshInFlight;
2021
+ codexSubscriptionRefreshInFlight = readCodexSubscription()
2022
+ .then((payload) => {
2023
+ cachedCodexSubscription = { payload, cachedAt: Date.now() };
2024
+ return payload;
2025
+ })
2026
+ .finally(() => {
2027
+ codexSubscriptionRefreshInFlight = null;
2028
+ });
2029
+ return codexSubscriptionRefreshInFlight;
2030
+ }
2031
+ async function readCodexSubscriptionForStatus() {
2032
+ if (cachedCodexSubscription &&
2033
+ Date.now() - cachedCodexSubscription.cachedAt < CODEX_SUBSCRIPTION_CACHE_TTL_MS) {
2034
+ return { payload: cachedCodexSubscription.payload, error: null };
2035
+ }
2036
+ const refresh = refreshCodexSubscriptionStatus();
2037
+ const timeout = new Promise((resolve) => {
2038
+ setTimeout(() => resolve(null), CODEX_SUBSCRIPTION_STATUS_WAIT_MS).unref?.();
2039
+ });
2040
+ try {
2041
+ const payload = await Promise.race([refresh, timeout]);
2042
+ if (payload)
2043
+ return { payload, error: null };
2044
+ }
2045
+ catch (err) {
2046
+ if (cachedCodexSubscription)
2047
+ return { payload: cachedCodexSubscription.payload, error: null };
2048
+ return { payload: null, error: err?.message || 'Unable to read Codex subscription details.' };
2049
+ }
2050
+ if (cachedCodexSubscription)
2051
+ return { payload: cachedCodexSubscription.payload, error: null };
2052
+ refresh.catch(() => undefined);
2053
+ return { payload: null, error: null };
2054
+ }
2325
2055
  function readCachedCodingCliStatusPayload(options) {
2326
2056
  startCodingCliStatusCache({
2327
2057
  build: () => ({
@@ -2396,14 +2126,22 @@ function codexSubscriptionSummary(subscription) {
2396
2126
  : {};
2397
2127
  return {
2398
2128
  plan_type: typeof rateLimits.planType === 'string' && rateLimits.planType.trim() ? rateLimits.planType.trim() : null,
2399
- credits_balance: typeof credits.balance === 'string' && credits.balance.trim() ? credits.balance.trim() : null,
2129
+ credits_balance: typeof credits.balance === 'string' && credits.balance.trim()
2130
+ ? credits.balance.trim()
2131
+ : typeof credits.balance === 'number' && Number.isFinite(credits.balance)
2132
+ ? String(credits.balance)
2133
+ : null,
2400
2134
  };
2401
2135
  }
2402
2136
  async function readCodexAgentStatusPayload() {
2403
- const status = readCachedCodingCliStatusPayload();
2137
+ const status = {
2138
+ ...readCachedCodingCliStatusPayload(),
2139
+ daemon: detectDaemonPackageStatus(),
2140
+ };
2404
2141
  const quotaStatus = readLatestSpaceQuotaStatus();
2405
- try {
2406
- const subscription = await readCodexSubscription();
2142
+ const subscriptionResult = await readCodexSubscriptionForStatus();
2143
+ const subscription = subscriptionResult.payload;
2144
+ if (subscription) {
2407
2145
  const subscriptionWindows = mergeQuotaWindows(normalizeCodexSubscriptionQuotaWindows(subscription), normalizeQuotaWindows(subscription.rate_limits), normalizeQuotaWindows(subscription.rate_limits_by_limit_id));
2408
2146
  return {
2409
2147
  ...status,
@@ -2418,23 +2156,21 @@ async function readCodexAgentStatusPayload() {
2418
2156
  quota_windows: mergeQuotaWindows(quotaStatus.windows, subscriptionWindows),
2419
2157
  };
2420
2158
  }
2421
- catch (err) {
2422
- return {
2423
- ...status,
2424
- subscription: {
2425
- ok: false,
2426
- fetched_at: null,
2427
- payload: null,
2428
- error: err?.message || 'Unable to read Codex subscription details.',
2429
- },
2430
- subscription_summary: {
2431
- plan_type: null,
2432
- credits_balance: null,
2433
- },
2434
- quota_status: quotaStatus,
2435
- quota_windows: quotaStatus.windows,
2436
- };
2437
- }
2159
+ return {
2160
+ ...status,
2161
+ subscription: {
2162
+ ok: false,
2163
+ fetched_at: null,
2164
+ payload: null,
2165
+ error: subscriptionResult.error,
2166
+ },
2167
+ subscription_summary: {
2168
+ plan_type: null,
2169
+ credits_balance: null,
2170
+ },
2171
+ quota_status: quotaStatus,
2172
+ quota_windows: quotaStatus.windows,
2173
+ };
2438
2174
  }
2439
2175
  function readRuntimeModelStatusPayload() {
2440
2176
  const selected_cli = process.env.SUPEN_CODING_CLI || readConfigSummary().coding_cli || 'codex';
@@ -2763,31 +2499,6 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2763
2499
  writeJson(res, 200, await readCodexAgentStatusPayload());
2764
2500
  return true;
2765
2501
  }
2766
- if (pathname === '/api/computers/{computer_id}/runtime-models' && method === 'GET') {
2767
- writeJson(res, 200, readRuntimeModelStatusPayload());
2768
- return true;
2769
- }
2770
- if (pathname === '/api/computers/{computer_id}/runtime-models/default' && method === 'PUT') {
2771
- try {
2772
- const parsed = await readJsonBody(req);
2773
- const model = typeof parsed === 'object' && parsed && typeof parsed.model === 'string'
2774
- ? String(parsed.model).trim()
2775
- : '';
2776
- if (!model) {
2777
- writeProtocolError(res, 400, 'validation_error', 'missing_model', 'Request body must include a non-empty "model" string.');
2778
- return true;
2779
- }
2780
- writeCodexDefaultModel(model);
2781
- writeJson(res, 200, {
2782
- ok: true,
2783
- status: readRuntimeModelStatusPayload(),
2784
- });
2785
- }
2786
- catch (err) {
2787
- writeProtocolError(res, 500, 'config_error', 'codex_model_default_failed', err?.message || 'Failed to set Codex default model');
2788
- }
2789
- return true;
2790
- }
2791
2502
  const codexTransportDefaultMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/agents\/codex\/transports\/([^/]+)\/default$/);
2792
2503
  if (codexTransportDefaultMatch && method === 'PUT') {
2793
2504
  try {
@@ -2822,7 +2533,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2822
2533
  }
2823
2534
  return true;
2824
2535
  }
2825
- if (pathname === '/api/computers/{computer_id}/agents/codex/install' && method === 'POST') {
2536
+ if (pathname === '/api/computers/{computer_id}/agents/codex/update' && method === 'POST') {
2826
2537
  try {
2827
2538
  const cli = 'codex';
2828
2539
  const current = detectCliInstalled(cli);
@@ -2830,7 +2541,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2830
2541
  writeProtocolError(res, 400, 'validation_error', 'coding_cli_update_not_supported', `${cli} is installed via ${current.install_source || current.resolved_path || current.path || 'an unknown source'}; update it with that installer on this computer.`);
2831
2542
  return true;
2832
2543
  }
2833
- const installSpec = MANAGED_CODING_CLI_INSTALL_COMMANDS.codex;
2544
+ const installSpec = resolveManagedCodingCliInstallCommand(cli, current);
2834
2545
  const result = spawnSync(installSpec.command, installSpec.args, {
2835
2546
  encoding: 'utf8',
2836
2547
  timeout: 120_000,
@@ -2946,15 +2657,15 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2946
2657
  writeJson(res, 200, buildHubSnapshotForSpace(spaceId));
2947
2658
  return true;
2948
2659
  }
2949
- if (pathname === '/api/computers/{computer_id}/usage' && method === 'GET') {
2660
+ if (pathname === '/api/computers/{computer_id}/agents/codex/usage' && method === 'GET') {
2950
2661
  writeJson(res, 200, getGlobalUsage());
2951
2662
  return true;
2952
2663
  }
2953
- if (pathname === '/api/computers/{computer_id}/usage/daily' && method === 'GET') {
2664
+ if (pathname === '/api/computers/{computer_id}/agents/codex/usage/daily' && method === 'GET') {
2954
2665
  writeJson(res, 200, { daily: getDailyUsage() });
2955
2666
  return true;
2956
2667
  }
2957
- if (pathname === '/api/computers/{computer_id}/codex/threads' && method === 'GET') {
2668
+ if (codexThreadsListRoute(pathname) && method === 'GET') {
2958
2669
  const limitRaw = coerceSingleQueryParam(url.searchParams.get('limit'));
2959
2670
  const limit = limitRaw ? Number.parseInt(limitRaw, 10) : MIRRORED_THREAD_LIMIT;
2960
2671
  writeJson(res, 200, readMirroredTaskProjects(Number.isFinite(limit) ? limit : MIRRORED_THREAD_LIMIT));
@@ -2974,7 +2685,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2974
2685
  serveRemoteFile(req, res, filePath);
2975
2686
  return true;
2976
2687
  }
2977
- const mirroredThreadArchiveMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/codex\/threads\/([^/]+)\/archive$/);
2688
+ const mirroredThreadArchiveMatch = codexThreadActionRoute(pathname, 'archive');
2978
2689
  if (mirroredThreadArchiveMatch && method === 'POST') {
2979
2690
  const threadId = decodeURIComponent(mirroredThreadArchiveMatch[1] || '').trim();
2980
2691
  if (!threadId) {
@@ -2988,7 +2699,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2988
2699
  writeJson(res, 200, { ok: true, archived: true });
2989
2700
  return true;
2990
2701
  }
2991
- const mirroredThreadStreamMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/codex\/threads\/([^/]+)\/stream$/);
2702
+ const mirroredThreadStreamMatch = codexThreadActionRoute(pathname, 'stream');
2992
2703
  if (mirroredThreadStreamMatch && method === 'GET') {
2993
2704
  const threadId = decodeURIComponent(mirroredThreadStreamMatch[1] || '').trim();
2994
2705
  if (!threadId) {
@@ -3031,7 +2742,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
3031
2742
  res.flush?.();
3032
2743
  return true;
3033
2744
  }
3034
- const mirroredThreadHistoryMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/codex\/threads\/([^/]+)\/messages$/);
2745
+ const mirroredThreadHistoryMatch = codexThreadActionRoute(pathname, 'messages');
3035
2746
  if (mirroredThreadHistoryMatch && method === 'GET') {
3036
2747
  const threadId = decodeURIComponent(mirroredThreadHistoryMatch[1] || '').trim();
3037
2748
  const limitRaw = coerceSingleQueryParam(url.searchParams.get('limit'));
@@ -3046,7 +2757,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
3046
2757
  writeJson(res, 200, history);
3047
2758
  return true;
3048
2759
  }
3049
- const mirroredThreadAdoptMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/codex\/threads\/([^/]+)\/adopt$/);
2760
+ const mirroredThreadAdoptMatch = codexThreadActionRoute(pathname, 'adopt');
3050
2761
  if (mirroredThreadAdoptMatch && method === 'POST') {
3051
2762
  const threadId = decodeURIComponent(mirroredThreadAdoptMatch[1] || '').trim();
3052
2763
  const parsed = await readJsonBody(req);
@@ -3063,7 +2774,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
3063
2774
  writeJson(res, 200, { thread: serializeAdoptedMirroredThread(session) });
3064
2775
  return true;
3065
2776
  }
3066
- if (pathname === '/api/computers/{computer_id}/codex/projects/open' && method === 'POST') {
2777
+ if (codexProjectsOpenRoute(pathname) && method === 'POST') {
3067
2778
  try {
3068
2779
  const parsed = await readJsonBody(req);
3069
2780
  const projectPath = parsed && typeof parsed === 'object' && typeof parsed.path === 'string'