@supen-ai/cli 0.1.10 → 0.1.12

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 (81) hide show
  1. package/README.md +15 -7
  2. package/daemon/dist/agent-sdk/driver-output-ui.js +1 -1
  3. package/daemon/dist/agent-sdk/drivers/acpx-driver.js +1 -1
  4. package/daemon/dist/agent-sdk/drivers/acpx-driver.js.map +1 -1
  5. package/daemon/dist/agent-sdk/drivers/registry.js +1 -1
  6. package/daemon/dist/agent-sdk/drivers/registry.js.map +1 -1
  7. package/daemon/dist/channels/http-routes.js +59 -59
  8. package/daemon/dist/channels/http-routes.js.map +1 -1
  9. package/daemon/dist/channels/http.js +1 -1
  10. package/daemon/dist/channels/http.js.map +1 -1
  11. package/daemon/dist/core/coding-cli-status-cache.d.ts +19 -0
  12. package/daemon/dist/core/coding-cli-status-cache.d.ts.map +1 -0
  13. package/daemon/dist/core/coding-cli-status-cache.js +122 -0
  14. package/daemon/dist/core/coding-cli-status-cache.js.map +1 -0
  15. package/daemon/dist/core/config.js +1 -1
  16. package/daemon/dist/core/config.js.map +1 -1
  17. package/daemon/dist/core/control-commands.js +1 -1
  18. package/daemon/dist/core/cortex.js +4 -4
  19. package/daemon/dist/core/gateway-config.d.ts +1 -1
  20. package/daemon/dist/core/gateway-config.d.ts.map +1 -1
  21. package/daemon/dist/core/gateway-config.js +4 -1
  22. package/daemon/dist/core/gateway-config.js.map +1 -1
  23. package/daemon/dist/core/gateway.d.ts.map +1 -1
  24. package/daemon/dist/core/gateway.js +8 -34
  25. package/daemon/dist/core/gateway.js.map +1 -1
  26. package/daemon/dist/core/os-info.d.ts +65 -0
  27. package/daemon/dist/core/os-info.d.ts.map +1 -0
  28. package/daemon/dist/core/os-info.js +225 -0
  29. package/daemon/dist/core/os-info.js.map +1 -0
  30. package/daemon/dist/core/protocol-adapter.js +2 -2
  31. package/daemon/dist/core/thread-runtime-state.js +1 -1
  32. package/daemon/dist/http/context.js +2 -2
  33. package/daemon/dist/http/context.js.map +1 -1
  34. package/daemon/dist/http/router.d.ts.map +1 -1
  35. package/daemon/dist/http/router.js +4 -4
  36. package/daemon/dist/http/router.js.map +1 -1
  37. package/daemon/dist/http/routes/agents.d.ts.map +1 -1
  38. package/daemon/dist/http/routes/agents.js +13 -16
  39. package/daemon/dist/http/routes/agents.js.map +1 -1
  40. package/daemon/dist/http/routes/automations.d.ts.map +1 -1
  41. package/daemon/dist/http/routes/automations.js +27 -13
  42. package/daemon/dist/http/routes/automations.js.map +1 -1
  43. package/daemon/dist/http/routes/autonomy.js +4 -4
  44. package/daemon/dist/http/routes/autonomy.js.map +1 -1
  45. package/daemon/dist/http/routes/chat-input.d.ts +0 -1
  46. package/daemon/dist/http/routes/chat-input.d.ts.map +1 -1
  47. package/daemon/dist/http/routes/chat-input.js +0 -1
  48. package/daemon/dist/http/routes/chat-input.js.map +1 -1
  49. package/daemon/dist/http/routes/plugins.js +6 -6
  50. package/daemon/dist/http/routes/plugins.js.map +1 -1
  51. package/daemon/dist/http/routes/rpc.d.ts.map +1 -1
  52. package/daemon/dist/http/routes/rpc.js +43 -10
  53. package/daemon/dist/http/routes/rpc.js.map +1 -1
  54. package/daemon/dist/http/routes/sessions.js +15 -15
  55. package/daemon/dist/http/routes/sessions.js.map +1 -1
  56. package/daemon/dist/http/routes/skills.js +11 -11
  57. package/daemon/dist/http/routes/skills.js.map +1 -1
  58. package/daemon/dist/http/routes/system.d.ts +1 -0
  59. package/daemon/dist/http/routes/system.d.ts.map +1 -1
  60. package/daemon/dist/http/routes/system.js +540 -98
  61. package/daemon/dist/http/routes/system.js.map +1 -1
  62. package/daemon/dist/index.js +1 -1
  63. package/daemon/package.json +83 -0
  64. package/daemon/scripts/supen-daemon.js +1 -1
  65. package/dist/auth/login.js +1 -1
  66. package/dist/auth/login.js.map +1 -1
  67. package/dist/bootstrap.d.ts +1 -0
  68. package/dist/bootstrap.js +5 -4
  69. package/dist/bootstrap.js.map +1 -1
  70. package/dist/computer.js +44 -14
  71. package/dist/computer.js.map +1 -1
  72. package/dist/daemon-manage.js +9 -2
  73. package/dist/daemon-manage.js.map +1 -1
  74. package/dist/doctor.js +261 -14
  75. package/dist/doctor.js.map +1 -1
  76. package/dist/index.js +2 -1
  77. package/dist/index.js.map +1 -1
  78. package/dist/os-identity.d.ts +6 -0
  79. package/dist/os-identity.js +68 -0
  80. package/dist/os-identity.js.map +1 -0
  81. package/package.json +2 -2
@@ -10,12 +10,15 @@ import { readSpaceEnvMap, updateSpaceEnvMap, withSupenLlmEnv } from '../../core/
10
10
  import { readCodexSubscription } from '../../core/codex-subscription.js';
11
11
  import { buildHubSnapshotForSpace } from '../../core/hub-snapshot.js';
12
12
  import { readEnrollmentState, createEnrollmentToken, verifyEnrollmentToken, setTrustState } from '../../core/enrollment.js';
13
+ import { collectOsTelemetryFields } from '../../core/os-info.js';
14
+ import { getCodingCliStatusResponse, startCodingCliStatusCache, } from '../../core/coding-cli-status-cache.js';
13
15
  import { getGatewayInstance, getLlmToken } from '../../core/gateway.js';
14
16
  import { normalizeGatewayUplinkUrl, readGatewayConfig, writeGatewayConfig, } from '../../core/gateway-config.js';
15
17
  import { ensureSession, getAllAgents, getDailyUsage, getGlobalUsage, getSessionsForAgent, getSessionUiEvents, updateSessionBackendDriverId, updateSessionSdkId, } from '../../core/store.js';
16
18
  import { listMcpEnvKeys, listMcpServers } from '../../mcp/default-servers.js';
17
19
  import { getMcpEnvOverrides, updateMcpEnvOverrides } from '../../mcp/settings.js';
18
20
  import { getMcpManager } from '../../mcp/index.js';
21
+ import { logger } from '../../core/logger.js';
19
22
  import { buildDaemonOpenApiSpec } from '../../channels/http-routes.js';
20
23
  import { writeJson, writeProtocolError, readJsonBody } from '../response.js';
21
24
  import { listRecentThreadEventsAfter, readThreadEventLogHead, } from '../../core/thread-event-log.js';
@@ -38,6 +41,10 @@ const MIRRORED_THREAD_STREAM_REPLAY_MAX_BYTES = 8 * 1024 * 1024;
38
41
  const MIRRORED_THREAD_RUNNING_LEASE_MS = 30 * 60 * 1000;
39
42
  const MIRRORED_THREAD_HISTORY_CHUNK_BYTES = 1024 * 1024;
40
43
  const require = createRequire(import.meta.url);
44
+ const HOST_DAEMON_INSTALL_DIR = path.join(SUPEN_HOME, 'daemon');
45
+ const HOST_DAEMON_CLI_PACKAGE_ROOT = path.join(HOST_DAEMON_INSTALL_DIR, 'node_modules', '@supen-ai', 'cli');
46
+ 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';
41
48
  function readSqliteRows(dbPath, query) {
42
49
  try {
43
50
  const { DatabaseSync } = require('node:sqlite');
@@ -53,6 +60,33 @@ function readSqliteRows(dbPath, query) {
53
60
  return null;
54
61
  }
55
62
  }
63
+ function quoteSqliteString(value) {
64
+ return `'${value.replace(/'/g, "''")}'`;
65
+ }
66
+ function runSqliteStatement(dbPath, query, params = []) {
67
+ try {
68
+ const { DatabaseSync } = require('node:sqlite');
69
+ const db = new DatabaseSync(dbPath);
70
+ try {
71
+ db.prepare(query).run(...params);
72
+ return true;
73
+ }
74
+ finally {
75
+ db.close();
76
+ }
77
+ }
78
+ catch {
79
+ const expandedQuery = params.reduce((sql, param) => {
80
+ const value = typeof param === 'string' ? quoteSqliteString(param) : String(param);
81
+ return sql.replace('?', value);
82
+ }, query);
83
+ const result = spawnSync('sqlite3', [dbPath, expandedQuery], {
84
+ encoding: 'utf-8',
85
+ maxBuffer: 1024 * 1024,
86
+ });
87
+ return result.status === 0;
88
+ }
89
+ }
56
90
  function gatewayUplinkStatus(config = readGatewayConfig()) {
57
91
  const gatewayUrl = (config.gateway_url || '').trim();
58
92
  return {
@@ -218,7 +252,7 @@ function summarizeUiChunk(chunk) {
218
252
  : 'Unknown error';
219
253
  return { category: 'error', summary: truncateText(errorText) };
220
254
  }
221
- if (type === 'data-supen-event') {
255
+ if (type === 'data-codex-event') {
222
256
  const data = chunk.data && typeof chunk.data === 'object'
223
257
  ? chunk.data
224
258
  : {};
@@ -476,6 +510,15 @@ function readThreadIndexEntry(threadId) {
476
510
  function readThreadStateEntry(threadId) {
477
511
  return readThreadStateEntries(1000).find((entry) => entry.id === threadId) || null;
478
512
  }
513
+ function archiveMirroredThread(threadId) {
514
+ const dbPath = path.join(localAgentHome(), 'state_5.sqlite');
515
+ if (!threadId || !fs.existsSync(dbPath))
516
+ return false;
517
+ const existing = readThreadStateEntry(threadId);
518
+ if (!existing)
519
+ return false;
520
+ return runSqliteStatement(dbPath, 'update threads set archived = 1 where id = ?', [threadId]);
521
+ }
479
522
  function isoFromMillis(value) {
480
523
  const ms = typeof value === 'number' ? value : Number(value);
481
524
  if (!Number.isFinite(ms) || ms <= 0)
@@ -757,6 +800,66 @@ function isMirrorableCodexWorkspace(projectPath) {
757
800
  }
758
801
  return !isPathWithin(path.join(SUPEN_HOME, 'tasks'), projectPath);
759
802
  }
803
+ function mimeTypeForWorkspaceFile(filePath) {
804
+ const ext = path.extname(filePath).toLowerCase();
805
+ if (ext === '.md' || ext === '.markdown' || ext === '.mdx')
806
+ return 'text/markdown; charset=utf-8';
807
+ if (ext === '.txt' || ext === '.log' || ext === '.env')
808
+ return 'text/plain; charset=utf-8';
809
+ if (ext === '.json')
810
+ return 'application/json; charset=utf-8';
811
+ if (ext === '.html' || ext === '.htm')
812
+ return 'text/html; charset=utf-8';
813
+ if (ext === '.css')
814
+ return 'text/css; charset=utf-8';
815
+ if (ext === '.js' || ext === '.mjs' || ext === '.cjs')
816
+ return 'text/javascript; charset=utf-8';
817
+ if (ext === '.ts' || ext === '.tsx' || ext === '.jsx')
818
+ return 'text/plain; charset=utf-8';
819
+ if (ext === '.png')
820
+ return 'image/png';
821
+ if (ext === '.jpg' || ext === '.jpeg')
822
+ return 'image/jpeg';
823
+ if (ext === '.gif')
824
+ return 'image/gif';
825
+ if (ext === '.webp')
826
+ return 'image/webp';
827
+ if (ext === '.svg')
828
+ return 'image/svg+xml';
829
+ if (ext === '.pdf')
830
+ return 'application/pdf';
831
+ return 'application/octet-stream';
832
+ }
833
+ function serveRemoteFile(req, res, filePath) {
834
+ const stat = fs.statSync(filePath);
835
+ const basename = path.basename(filePath);
836
+ res.writeHead(200, {
837
+ 'Content-Type': mimeTypeForWorkspaceFile(filePath),
838
+ 'Content-Length': stat.size,
839
+ 'Last-Modified': stat.mtime.toUTCString(),
840
+ 'Cache-Control': 'no-store',
841
+ 'Content-Disposition': `inline; filename="${basename.replace(/"/g, '')}"`,
842
+ });
843
+ if (req.method === 'HEAD') {
844
+ res.end();
845
+ return;
846
+ }
847
+ fs.createReadStream(filePath).pipe(res);
848
+ }
849
+ function resolveRemoteFilePath(rawPath) {
850
+ const trimmed = rawPath.trim();
851
+ if (!trimmed)
852
+ return null;
853
+ const expanded = trimmed === '~'
854
+ ? os.homedir()
855
+ : trimmed.startsWith('~/')
856
+ ? path.join(os.homedir(), trimmed.slice(2))
857
+ : trimmed;
858
+ const resolved = path.resolve(expanded);
859
+ if (!path.isAbsolute(resolved))
860
+ return null;
861
+ return resolved;
862
+ }
760
863
  function isAutomationRunThreadTitle(title) {
761
864
  if (!title)
762
865
  return false;
@@ -880,7 +983,9 @@ function sanitizeMirroredHistoryPayload(value, depth = 0) {
880
983
  }
881
984
  function safeMirroredHistoryAttachmentUrl(url) {
882
985
  const trimmed = url.trim();
883
- if (trimmed.startsWith('data:') && trimmed.length > MIRRORED_THREAD_INLINE_DATA_URL_LIMIT) {
986
+ if (trimmed.startsWith('data:') &&
987
+ !trimmed.startsWith('data:image/') &&
988
+ trimmed.length > MIRRORED_THREAD_INLINE_DATA_URL_LIMIT) {
884
989
  return '';
885
990
  }
886
991
  return trimmed;
@@ -902,12 +1007,15 @@ function imageAttachmentFromUrl(url, index) {
902
1007
  const trimmed = url.trim();
903
1008
  if (!trimmed)
904
1009
  return null;
1010
+ const safeUrl = safeMirroredHistoryAttachmentUrl(trimmed);
1011
+ if (!safeUrl)
1012
+ return null;
905
1013
  const mimeType = mimeTypeFromImageUrl(trimmed);
906
1014
  const filename = `pasted-image-${index + 1}.${imageExtensionFromMimeType(mimeType)}`;
907
1015
  return {
908
1016
  name: filename,
909
1017
  filename,
910
- url: safeMirroredHistoryAttachmentUrl(trimmed),
1018
+ url: safeUrl,
911
1019
  mimeType,
912
1020
  mime_type: mimeType,
913
1021
  };
@@ -1079,7 +1187,7 @@ function codexReplayEvent(eventId, timestamp, taskId, raw) {
1079
1187
  timestamp,
1080
1188
  task_id: taskId,
1081
1189
  chunk: {
1082
- type: 'data-supen-event',
1190
+ type: 'data-codex-event',
1083
1191
  id: eventId,
1084
1192
  data: { raw },
1085
1193
  },
@@ -1158,6 +1266,49 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1158
1266
  ...(attachments.length > 0 ? { attachments } : {}),
1159
1267
  });
1160
1268
  };
1269
+ const pushWebUserMessage = (event) => {
1270
+ if (event.source !== 'web' || event.event_type !== 'user_message')
1271
+ return;
1272
+ const payload = event.raw_payload && typeof event.raw_payload === 'object'
1273
+ ? event.raw_payload
1274
+ : {};
1275
+ const text = typeof payload.content === 'string' ? payload.content : '';
1276
+ const attachments = Array.isArray(payload.attachments)
1277
+ ? payload.attachments
1278
+ .map((attachment, index) => {
1279
+ if (!attachment || typeof attachment !== 'object')
1280
+ return null;
1281
+ const record = attachment;
1282
+ const url = typeof record.url === 'string' ? record.url :
1283
+ typeof record.supenUrl === 'string' ? record.supenUrl :
1284
+ typeof record.path === 'string' ? record.path :
1285
+ typeof record.taskPath === 'string' ? record.taskPath :
1286
+ '';
1287
+ if (!url.trim())
1288
+ return null;
1289
+ const filename = typeof record.filename === 'string' ? record.filename :
1290
+ typeof record.name === 'string' ? record.name :
1291
+ `attachment-${index + 1}`;
1292
+ const mimeType = typeof record.mimeType === 'string' ? record.mimeType :
1293
+ typeof record.mime_type === 'string' ? record.mime_type :
1294
+ 'application/octet-stream';
1295
+ const safeUrl = safeMirroredHistoryAttachmentUrl(url);
1296
+ if (!safeUrl)
1297
+ return null;
1298
+ return {
1299
+ name: filename,
1300
+ filename,
1301
+ url: safeUrl,
1302
+ mimeType,
1303
+ mime_type: mimeType,
1304
+ };
1305
+ })
1306
+ .filter((attachment) => Boolean(attachment))
1307
+ : [];
1308
+ const timestamp = typeof payload.timestamp === 'string' ? payload.timestamp : event.received_at;
1309
+ const messageId = typeof payload.id === 'string' ? payload.id : `${threadId}-web-user-${event.sequence}`;
1310
+ pushUserMessage(text, timestamp, messageId, attachments);
1311
+ };
1161
1312
  const pushEvent = (timestamp, chunk) => {
1162
1313
  eventIndex += 1;
1163
1314
  events.push({
@@ -1368,15 +1519,20 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1368
1519
  task_id: threadId,
1369
1520
  });
1370
1521
  }
1371
- for (const event of listRecentThreadEventsAfter(eventLogThreadId, 0, {
1522
+ const eventLogEvents = listRecentThreadEventsAfter(eventLogThreadId, 0, {
1372
1523
  limit: MIRRORED_THREAD_STREAM_REPLAY_LIMIT,
1373
1524
  maxBytes: MIRRORED_THREAD_STREAM_REPLAY_MAX_BYTES,
1374
- })) {
1525
+ });
1526
+ for (const event of eventLogEvents) {
1527
+ pushWebUserMessage(event);
1528
+ }
1529
+ for (const event of eventLogEvents) {
1375
1530
  const chunk = threadStreamChunkForEvent(event);
1376
1531
  if (!chunk)
1377
1532
  continue;
1378
1533
  events.push({
1379
1534
  id: `${threadId}-event-log-${event.sequence}`,
1535
+ sequence: event.sequence,
1380
1536
  timestamp: event.received_at,
1381
1537
  task_id: threadId,
1382
1538
  chunk,
@@ -1539,17 +1695,41 @@ function threadStreamChunkForEvent(event) {
1539
1695
  return null;
1540
1696
  if (event.raw_payload &&
1541
1697
  typeof event.raw_payload === 'object' &&
1542
- !Array.isArray(event.raw_payload)) {
1543
- return event.raw_payload;
1698
+ !Array.isArray(event.raw_payload) &&
1699
+ typeof event.raw_payload.type === 'string' &&
1700
+ String(event.raw_payload.type).trim()) {
1701
+ return normalizeStoredCodexThreadChunk(event.raw_payload);
1544
1702
  }
1545
1703
  return {
1546
- type: 'data-supen-event',
1704
+ type: 'data-codex-event',
1547
1705
  data: {
1548
1706
  eventType: event.event_type,
1549
1707
  raw: event.raw_payload,
1550
1708
  },
1551
1709
  };
1552
1710
  }
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
+ }
1553
1733
  function buildMcpSettingsResponse() {
1554
1734
  const overrides = getMcpEnvOverrides();
1555
1735
  const runtimeEnv = { ...process.env };
@@ -1653,19 +1833,206 @@ function codexConnectSnapshot() {
1653
1833
  error: codexConnectRuntime.error,
1654
1834
  };
1655
1835
  }
1836
+ function detectGlobalNpmRoot() {
1837
+ const result = spawnSync('npm', ['root', '-g'], {
1838
+ encoding: 'utf8',
1839
+ timeout: 3000,
1840
+ });
1841
+ if (result.status !== 0)
1842
+ return null;
1843
+ const root = String(result.stdout || '').trim();
1844
+ if (!root)
1845
+ return null;
1846
+ try {
1847
+ return fs.realpathSync(root);
1848
+ }
1849
+ catch {
1850
+ return root;
1851
+ }
1852
+ }
1853
+ function findPackageRootFrom(startPath, expectedName) {
1854
+ if (!startPath)
1855
+ return null;
1856
+ let current = fs.existsSync(startPath) && fs.statSync(startPath).isDirectory()
1857
+ ? startPath
1858
+ : path.dirname(startPath);
1859
+ for (let i = 0; i < 8; i += 1) {
1860
+ const packageJsonPath = path.join(current, 'package.json');
1861
+ if (fs.existsSync(packageJsonPath)) {
1862
+ try {
1863
+ const parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
1864
+ if (parsed.name === expectedName)
1865
+ return current;
1866
+ }
1867
+ catch {
1868
+ return null;
1869
+ }
1870
+ }
1871
+ const next = path.dirname(current);
1872
+ if (next === current)
1873
+ break;
1874
+ current = next;
1875
+ }
1876
+ return null;
1877
+ }
1878
+ function readPackageVersion(packageRoot) {
1879
+ if (!packageRoot)
1880
+ return null;
1881
+ try {
1882
+ const parsed = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
1883
+ return typeof parsed.version === 'string' ? parsed.version : null;
1884
+ }
1885
+ catch {
1886
+ return null;
1887
+ }
1888
+ }
1889
+ function detectDaemonPackageStatus() {
1890
+ const envVersion = process.env.SUPEN_DAEMON_VERSION || process.env.npm_package_version || null;
1891
+ let packageRoot = null;
1892
+ try {
1893
+ packageRoot = findPackageRootFrom(require.resolve('@supen-ai/daemon'), '@supen-ai/daemon');
1894
+ }
1895
+ catch {
1896
+ packageRoot = null;
1897
+ }
1898
+ if (!packageRoot) {
1899
+ for (const candidate of [process.argv[1], process.cwd(), path.resolve(process.cwd(), 'apps/daemon')]) {
1900
+ packageRoot = findPackageRootFrom(candidate || null, '@supen-ai/daemon');
1901
+ if (packageRoot)
1902
+ break;
1903
+ }
1904
+ }
1905
+ let cliPackageRoot = null;
1906
+ try {
1907
+ if (fs.existsSync(path.join(HOST_DAEMON_CLI_PACKAGE_ROOT, 'package.json'))) {
1908
+ cliPackageRoot = fs.realpathSync(HOST_DAEMON_CLI_PACKAGE_ROOT);
1909
+ }
1910
+ }
1911
+ catch {
1912
+ cliPackageRoot = null;
1913
+ }
1914
+ if (!cliPackageRoot) {
1915
+ cliPackageRoot = findPackageRootFrom(process.argv[1] || null, '@supen-ai/cli');
1916
+ }
1917
+ const bundledDaemonRoot = cliPackageRoot
1918
+ ? path.join(cliPackageRoot, 'daemon')
1919
+ : null;
1920
+ const bundledDaemonVersion = readPackageVersion(bundledDaemonRoot);
1921
+ const packageVersion = readPackageVersion(packageRoot);
1922
+ const npmRoot = detectGlobalNpmRoot();
1923
+ const realPackageRoot = packageRoot ? fs.realpathSync(packageRoot) : null;
1924
+ const realCliPackageRoot = cliPackageRoot ? fs.realpathSync(cliPackageRoot) : null;
1925
+ const installSource = realCliPackageRoot && realCliPackageRoot.startsWith(`${HOST_DAEMON_INSTALL_DIR}${path.sep}`)
1926
+ ? 'supen-cli-bundle'
1927
+ : realPackageRoot && npmRoot && realPackageRoot.startsWith(`${npmRoot}${path.sep}@supen-ai${path.sep}daemon`)
1928
+ ? 'npm-global'
1929
+ : packageRoot
1930
+ ? 'local-package'
1931
+ : null;
1932
+ return {
1933
+ version: envVersion || bundledDaemonVersion || packageVersion,
1934
+ package_root: packageRoot || bundledDaemonRoot,
1935
+ install_source: installSource,
1936
+ update_supported: installSource === 'supen-cli-bundle' || installSource === 'npm-global',
1937
+ };
1938
+ }
1939
+ function restartManagedHostDaemon() {
1940
+ if (process.platform === 'linux') {
1941
+ const result = spawnSync('systemctl', ['--user', 'restart', 'supen-daemon.service'], {
1942
+ encoding: 'utf8',
1943
+ timeout: 10_000,
1944
+ });
1945
+ if (result.status === 0)
1946
+ return { method: 'systemd', attempted: true };
1947
+ }
1948
+ if (process.platform === 'darwin') {
1949
+ const uid = typeof process.getuid === 'function' ? process.getuid() : Number(process.env.UID || 0);
1950
+ const result = spawnSync('launchctl', ['kickstart', '-k', `gui/${uid}/ai.supen.daemon`], {
1951
+ encoding: 'utf8',
1952
+ timeout: 10_000,
1953
+ });
1954
+ if (result.status === 0)
1955
+ return { method: 'launchd', attempted: true };
1956
+ }
1957
+ if (fs.existsSync(HOST_DAEMON_BIN_PATH)) {
1958
+ const child = spawn(process.execPath, [HOST_DAEMON_BIN_PATH], {
1959
+ detached: true,
1960
+ stdio: 'ignore',
1961
+ env: process.env,
1962
+ });
1963
+ child.unref();
1964
+ setTimeout(() => process.exit(0), 250);
1965
+ return { method: 'detached-node', attempted: true };
1966
+ }
1967
+ return { method: 'none', attempted: false };
1968
+ }
1969
+ function inferCliInstallSource(name, executablePath, resolvedPath) {
1970
+ if (!executablePath && !resolvedPath)
1971
+ return { install_source: null, update_supported: false };
1972
+ const candidates = [executablePath || '', resolvedPath || ''].map((value) => value.toLowerCase());
1973
+ const normalized = candidates.join('\n');
1974
+ if (normalized.includes('/.local/share/pnpm/') || normalized.includes('/pnpm/')) {
1975
+ return { install_source: 'pnpm', update_supported: false };
1976
+ }
1977
+ if (normalized.includes('/homebrew/') || normalized.includes('/opt/homebrew/') || normalized.includes('/cellar/')) {
1978
+ return { install_source: 'homebrew', update_supported: false };
1979
+ }
1980
+ if (name === 'codex') {
1981
+ const npmRoot = detectGlobalNpmRoot();
1982
+ const codexPackagePath = resolvedPath || executablePath || '';
1983
+ if (npmRoot && codexPackagePath.startsWith(`${npmRoot}${path.sep}@openai${path.sep}codex${path.sep}`)) {
1984
+ return { install_source: 'npm-global', update_supported: true };
1985
+ }
1986
+ if (normalized.includes('/node_modules/@openai/codex/')) {
1987
+ return { install_source: 'node-package', update_supported: false };
1988
+ }
1989
+ }
1990
+ if (name === 'gemini') {
1991
+ const npmRoot = detectGlobalNpmRoot();
1992
+ const geminiPackagePath = resolvedPath || executablePath || '';
1993
+ if (npmRoot && geminiPackagePath.startsWith(`${npmRoot}${path.sep}@google${path.sep}gemini-cli${path.sep}`)) {
1994
+ return { install_source: 'npm-global', update_supported: true };
1995
+ }
1996
+ if (normalized.includes('/node_modules/@google/gemini-cli/')) {
1997
+ return { install_source: 'node-package', update_supported: false };
1998
+ }
1999
+ }
2000
+ return { install_source: null, update_supported: false };
2001
+ }
1656
2002
  function detectCliInstalled(name) {
1657
- const cmd = spawnSync('sh', ['-lc', `command -v ${name}`], {
2003
+ const cmd = spawnSync('sh', ['-c', `command -v ${name}`], {
1658
2004
  encoding: 'utf8',
1659
2005
  timeout: 3000,
1660
2006
  });
1661
- if (cmd.status !== 0)
1662
- return { installed: false, version: null };
2007
+ if (cmd.status !== 0) {
2008
+ return { installed: false, version: null, path: null, resolved_path: null, install_source: null, update_supported: false };
2009
+ }
2010
+ const executablePath = String(cmd.stdout || '').split(/\r?\n/g).find(Boolean)?.trim() || null;
2011
+ let resolvedPath = null;
2012
+ if (executablePath) {
2013
+ try {
2014
+ resolvedPath = fs.realpathSync(executablePath);
2015
+ }
2016
+ catch {
2017
+ resolvedPath = executablePath;
2018
+ }
2019
+ }
1663
2020
  const versionCmd = spawnSync(name, ['--version'], {
1664
2021
  encoding: 'utf8',
1665
2022
  timeout: 5000,
1666
2023
  });
1667
- const output = trimLogNoise(`${versionCmd.stdout || ''}\n${versionCmd.stderr || ''}`) || '';
1668
- return { installed: true, version: output || null };
2024
+ const output = versionCmd.status === 0
2025
+ ? trimLogNoise(`${versionCmd.stdout || ''}\n${versionCmd.stderr || ''}`) || ''
2026
+ : '';
2027
+ const semver = output.match(/\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\b/)?.[0] || null;
2028
+ const source = inferCliInstallSource(name, executablePath, resolvedPath);
2029
+ return {
2030
+ installed: true,
2031
+ version: semver || (output && !output.includes('\n') ? output : null),
2032
+ path: executablePath,
2033
+ resolved_path: resolvedPath,
2034
+ ...source,
2035
+ };
1669
2036
  }
1670
2037
  function detectCodexAuthStatus(installed) {
1671
2038
  if (!installed)
@@ -1674,14 +2041,27 @@ function detectCodexAuthStatus(installed) {
1674
2041
  encoding: 'utf8',
1675
2042
  timeout: 8000,
1676
2043
  });
1677
- const text = `${result.stdout || ''}\n${result.stderr || ''}`
2044
+ const lines = `${result.stdout || ''}\n${result.stderr || ''}`
1678
2045
  .split(/\r?\n/g)
1679
2046
  .map((line) => trimLogNoise(line))
1680
- .filter(Boolean)
1681
- .join('\n');
2047
+ .filter(Boolean);
2048
+ const text = lines.join('\n');
1682
2049
  const authenticated = /logged in/i.test(text) && !/not logged in/i.test(text);
1683
- const summary = text.split(/\r?\n/g).find((line) => line.trim().length > 0) || null;
1684
- return { authenticated, summary };
2050
+ const email = text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i)?.[0] || null;
2051
+ const accountLine = lines.find((line) => /logged in as|account|email|user/i.test(line) && !line.startsWith('file://')) || null;
2052
+ const summary = authenticated
2053
+ ? email || accountLine || 'Authenticated'
2054
+ : null;
2055
+ let accountId = null;
2056
+ try {
2057
+ const parsed = JSON.parse(fs.readFileSync(path.join(resolveCodexHome(), 'auth.json'), 'utf8'));
2058
+ const tokens = parsed.tokens && typeof parsed.tokens === 'object' ? parsed.tokens : {};
2059
+ accountId = typeof tokens.account_id === 'string' && tokens.account_id.trim() ? tokens.account_id.trim() : null;
2060
+ }
2061
+ catch {
2062
+ accountId = null;
2063
+ }
2064
+ return { authenticated, summary, account_id: accountId };
1685
2065
  }
1686
2066
  function readCodexDefaultModel() {
1687
2067
  const codexHome = resolveCodexHome();
@@ -1799,6 +2179,10 @@ function readCodingCliStatusPayload() {
1799
2179
  name,
1800
2180
  installed: detected.installed,
1801
2181
  version: detected.version,
2182
+ path: detected.path,
2183
+ resolved_path: detected.resolved_path,
2184
+ install_source: detected.install_source,
2185
+ update_supported: detected.update_supported,
1802
2186
  };
1803
2187
  });
1804
2188
  const codexInstalled = clis.find((cli) => cli.name === 'codex')?.installed ?? false;
@@ -1819,6 +2203,10 @@ function readCodingCliStatusPayload() {
1819
2203
  installed: detected.installed,
1820
2204
  version: detected.version,
1821
2205
  managed: app.managed,
2206
+ path: detected.path,
2207
+ resolved_path: detected.resolved_path,
2208
+ install_source: detected.install_source,
2209
+ update_supported: detected.update_supported,
1822
2210
  };
1823
2211
  });
1824
2212
  return {
@@ -1889,6 +2277,15 @@ function readCodingCliStatusPayload() {
1889
2277
  codex_connect: codexConnectSnapshot(),
1890
2278
  };
1891
2279
  }
2280
+ function readCachedCodingCliStatusPayload(options) {
2281
+ startCodingCliStatusCache({
2282
+ build: () => ({
2283
+ ...readCodingCliStatusPayload(),
2284
+ daemon: detectDaemonPackageStatus(),
2285
+ }),
2286
+ });
2287
+ return getCodingCliStatusResponse(options);
2288
+ }
1892
2289
  function readRuntimeModelStatusPayload() {
1893
2290
  const selected_cli = process.env.SUPEN_CODING_CLI || readConfigSummary().coding_cli || 'codex';
1894
2291
  const codexTransport = normalizeCodexTransport(process.env.SUPEN_CODEX_TRANSPORT) ||
@@ -2017,15 +2414,15 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2017
2414
  writeJson(res, 200, { status: 'ok' });
2018
2415
  return true;
2019
2416
  }
2020
- if (pathname === '/api/computer/openapi.json' && method === 'GET') {
2417
+ if (pathname === '/api/computers/{computer_id}/openapi.json' && method === 'GET') {
2021
2418
  writeJson(res, 200, buildDaemonOpenApiSpec());
2022
2419
  return true;
2023
2420
  }
2024
- if (pathname === '/api/computer/models' && method === 'GET') {
2421
+ if (pathname === '/api/computers/{computer_id}/models' && method === 'GET') {
2025
2422
  writeJson(res, 200, { models: MODELS_REGISTRY });
2026
2423
  return true;
2027
2424
  }
2028
- if (pathname === '/api/computer/config/reload' && method === 'POST') {
2425
+ if (pathname === '/api/computers/{computer_id}/config/reload' && method === 'POST') {
2029
2426
  const before = {
2030
2427
  total_models: MODELS_REGISTRY.length,
2031
2428
  default_model: DEFAULT_MODEL?.id ?? null,
@@ -2041,11 +2438,11 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2041
2438
  });
2042
2439
  return true;
2043
2440
  }
2044
- if (pathname === '/api/computer/config-summary' && method === 'GET') {
2441
+ if (pathname === '/api/computers/{computer_id}/config-summary' && method === 'GET') {
2045
2442
  writeJson(res, 200, readConfigSummary());
2046
2443
  return true;
2047
2444
  }
2048
- if (pathname === '/api/computer/logs' && method === 'GET') {
2445
+ if (pathname === '/api/computers/{computer_id}/logs' && method === 'GET') {
2049
2446
  const agentId = coerceSingleQueryParam(url.searchParams.get('agent_id'));
2050
2447
  const sessionId = coerceSingleQueryParam(url.searchParams.get('session_id'));
2051
2448
  const limitRaw = coerceSingleQueryParam(url.searchParams.get('limit'));
@@ -2057,7 +2454,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2057
2454
  }));
2058
2455
  return true;
2059
2456
  }
2060
- if (pathname === '/api/computer/logs/stream' && method === 'GET') {
2457
+ if (pathname === '/api/computers/{computer_id}/logs/stream' && method === 'GET') {
2061
2458
  const agentId = coerceSingleQueryParam(url.searchParams.get('agent_id'));
2062
2459
  const sessionId = coerceSingleQueryParam(url.searchParams.get('session_id'));
2063
2460
  const limitRaw = coerceSingleQueryParam(url.searchParams.get('limit'));
@@ -2100,7 +2497,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2100
2497
  writeSse(res);
2101
2498
  return true;
2102
2499
  }
2103
- if (pathname === '/api/computer/config-yaml' && method === 'GET') {
2500
+ if (pathname === '/api/computers/{computer_id}/config-yaml' && method === 'GET') {
2104
2501
  try {
2105
2502
  const yaml = readConfigYamlFile();
2106
2503
  writeJson(res, 200, { yaml });
@@ -2110,7 +2507,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2110
2507
  }
2111
2508
  return true;
2112
2509
  }
2113
- if (pathname === '/api/computer/config-yaml' && method === 'PUT') {
2510
+ if (pathname === '/api/computers/{computer_id}/config-yaml' && method === 'PUT') {
2114
2511
  try {
2115
2512
  const parsed = await readJsonBody(req);
2116
2513
  const yamlText = typeof parsed === 'object' && parsed && typeof parsed.yaml === 'string'
@@ -2137,11 +2534,11 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2137
2534
  }
2138
2535
  return true;
2139
2536
  }
2140
- if (pathname === '/api/computer/gateway-uplink' && method === 'GET') {
2537
+ if (pathname === '/api/computers/{computer_id}/gateway-uplink' && method === 'GET') {
2141
2538
  writeJson(res, 200, gatewayUplinkStatus());
2142
2539
  return true;
2143
2540
  }
2144
- if (pathname === '/api/computer/gateway-uplink' && (method === 'PUT' || method === 'PATCH')) {
2541
+ if (pathname === '/api/computers/{computer_id}/gateway-uplink' && (method === 'PUT' || method === 'PATCH')) {
2145
2542
  if (process.env.SUPEN_GATEWAY_URL) {
2146
2543
  writeProtocolError(res, 409, 'config_error', 'env_override', 'SUPEN_GATEWAY_URL is set; update the daemon environment to change the gateway uplink.');
2147
2544
  return true;
@@ -2184,11 +2581,11 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2184
2581
  }
2185
2582
  return true;
2186
2583
  }
2187
- if (pathname === '/api/computer/env' && method === 'GET') {
2584
+ if (pathname === '/api/computers/{computer_id}/env' && method === 'GET') {
2188
2585
  writeJson(res, 200, { env: readSpaceEnvMap() });
2189
2586
  return true;
2190
2587
  }
2191
- if (pathname === '/api/computer/llm-env' && method === 'GET') {
2588
+ if (pathname === '/api/computers/{computer_id}/llm-env' && method === 'GET') {
2192
2589
  const token = getLlmToken() || process.env.SUPEN_LLM_TOKEN || process.env.SUPEN_LLM_API_KEY || '';
2193
2590
  const env = withSupenLlmEnv({
2194
2591
  ...process.env,
@@ -2208,15 +2605,15 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2208
2605
  });
2209
2606
  return true;
2210
2607
  }
2211
- if (pathname === '/api/computer/apps' && method === 'GET') {
2212
- writeJson(res, 200, readCodingCliStatusPayload());
2608
+ if (pathname === '/api/computers/{computer_id}/apps' && method === 'GET') {
2609
+ writeJson(res, 200, readCachedCodingCliStatusPayload());
2213
2610
  return true;
2214
2611
  }
2215
- if (pathname === '/api/computer/runtime-models' && method === 'GET') {
2612
+ if (pathname === '/api/computers/{computer_id}/runtime-models' && method === 'GET') {
2216
2613
  writeJson(res, 200, readRuntimeModelStatusPayload());
2217
2614
  return true;
2218
2615
  }
2219
- if (pathname === '/api/computer/runtime-models/default' && method === 'PUT') {
2616
+ if (pathname === '/api/computers/{computer_id}/runtime-models/default' && method === 'PUT') {
2220
2617
  try {
2221
2618
  const parsed = await readJsonBody(req);
2222
2619
  const model = typeof parsed === 'object' && parsed && typeof parsed.model === 'string'
@@ -2237,7 +2634,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2237
2634
  }
2238
2635
  return true;
2239
2636
  }
2240
- const spaceCodexTransportDefaultMatch = pathname.match(/^\/api\/computer\/apps\/codex\/transports\/([^/]+)\/default$/);
2637
+ const spaceCodexTransportDefaultMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/apps\/codex\/transports\/([^/]+)\/default$/);
2241
2638
  if (spaceCodexTransportDefaultMatch && method === 'PUT') {
2242
2639
  try {
2243
2640
  const transport = normalizeCodexTransport(decodeURIComponent(spaceCodexTransportDefaultMatch[1] || '').trim());
@@ -2249,7 +2646,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2249
2646
  writeJson(res, 200, {
2250
2647
  ok: true,
2251
2648
  ...result,
2252
- status: readCodingCliStatusPayload(),
2649
+ status: readCachedCodingCliStatusPayload({ force: true }),
2253
2650
  });
2254
2651
  }
2255
2652
  catch (err) {
@@ -2257,7 +2654,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2257
2654
  }
2258
2655
  return true;
2259
2656
  }
2260
- const spaceAppDefaultMatch = pathname.match(/^\/api\/computer\/apps\/([^/]+)\/default$/);
2657
+ const spaceAppDefaultMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/apps\/([^/]+)\/default$/);
2261
2658
  if (spaceAppDefaultMatch && method === 'PUT') {
2262
2659
  try {
2263
2660
  const cli = normalizeCliName(decodeURIComponent(spaceAppDefaultMatch[1] || '').trim());
@@ -2269,7 +2666,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2269
2666
  writeJson(res, 200, {
2270
2667
  ok: true,
2271
2668
  ...result,
2272
- status: readCodingCliStatusPayload(),
2669
+ status: readCachedCodingCliStatusPayload({ force: true }),
2273
2670
  });
2274
2671
  }
2275
2672
  catch (err) {
@@ -2277,7 +2674,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2277
2674
  }
2278
2675
  return true;
2279
2676
  }
2280
- const spaceAppInstallMatch = pathname.match(/^\/api\/computer\/apps\/([^/]+)\/install$/);
2677
+ const spaceAppInstallMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/apps\/([^/]+)\/install$/);
2281
2678
  if (spaceAppInstallMatch && method === 'POST') {
2282
2679
  try {
2283
2680
  const cli = normalizeCliName(decodeURIComponent(spaceAppInstallMatch[1] || '').trim());
@@ -2289,6 +2686,11 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2289
2686
  writeProtocolError(res, 400, 'validation_error', 'install_not_supported', 'Only codex and gemini support managed install at the moment');
2290
2687
  return true;
2291
2688
  }
2689
+ const current = detectCliInstalled(cli);
2690
+ if (current.installed && !current.update_supported) {
2691
+ 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.`);
2692
+ return true;
2693
+ }
2292
2694
  const installSpec = MANAGED_CODING_CLI_INSTALL_COMMANDS[cli];
2293
2695
  const result = spawnSync(installSpec.command, installSpec.args, {
2294
2696
  encoding: 'utf8',
@@ -2302,7 +2704,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2302
2704
  writeJson(res, 200, {
2303
2705
  ok: true,
2304
2706
  cli,
2305
- status: readCodingCliStatusPayload(),
2707
+ status: readCachedCodingCliStatusPayload({ force: true }),
2306
2708
  });
2307
2709
  }
2308
2710
  catch (err) {
@@ -2310,14 +2712,51 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2310
2712
  }
2311
2713
  return true;
2312
2714
  }
2313
- const spaceAppConnectMatch = pathname.match(/^\/api\/computer\/apps\/([^/]+)\/connect$/);
2715
+ if (pathname === '/api/computers/{computer_id}/daemon/update' && method === 'POST') {
2716
+ try {
2717
+ const current = detectDaemonPackageStatus();
2718
+ if (!current.update_supported) {
2719
+ writeProtocolError(res, 400, 'validation_error', 'daemon_update_not_supported', `Supen daemon is installed via ${current.install_source || current.package_root || 'an unknown source'}; update it with that installer on this computer.`);
2720
+ return true;
2721
+ }
2722
+ fs.mkdirSync(HOST_DAEMON_INSTALL_DIR, { recursive: true });
2723
+ const result = spawnSync('npm', ['install', '--prefix', HOST_DAEMON_INSTALL_DIR, HOST_DAEMON_PACKAGE_SPEC], {
2724
+ encoding: 'utf8',
2725
+ timeout: 120_000,
2726
+ });
2727
+ if (result.status !== 0) {
2728
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`.trim() || 'Failed to update Supen daemon';
2729
+ writeProtocolError(res, 500, 'install_error', 'daemon_update_failed', output);
2730
+ return true;
2731
+ }
2732
+ writeJson(res, 200, {
2733
+ ok: true,
2734
+ daemon: detectDaemonPackageStatus(),
2735
+ status: readCachedCodingCliStatusPayload({ force: true }),
2736
+ restart: { scheduled: true },
2737
+ });
2738
+ setTimeout(() => {
2739
+ try {
2740
+ restartManagedHostDaemon();
2741
+ }
2742
+ catch (err) {
2743
+ logger.error({ err }, 'Failed to restart Supen daemon after update');
2744
+ }
2745
+ }, 250);
2746
+ }
2747
+ catch (err) {
2748
+ writeProtocolError(res, 500, 'install_error', 'daemon_update_failed', err?.message || 'Failed to update Supen daemon');
2749
+ }
2750
+ return true;
2751
+ }
2752
+ const spaceAppConnectMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/apps\/([^/]+)\/connect$/);
2314
2753
  if (spaceAppConnectMatch && method === 'POST') {
2315
2754
  const cli = normalizeCliName(decodeURIComponent(spaceAppConnectMatch[1] || '').trim());
2316
2755
  if (cli !== 'codex') {
2317
2756
  writeProtocolError(res, 400, 'validation_error', 'connect_not_supported', 'Only codex supports connect flow at the moment');
2318
2757
  return true;
2319
2758
  }
2320
- const status = readCodingCliStatusPayload();
2759
+ const status = readCachedCodingCliStatusPayload();
2321
2760
  const codex = status.clis.find((entry) => entry.name === 'codex');
2322
2761
  if (!codex?.installed) {
2323
2762
  writeProtocolError(res, 400, 'validation_error', 'codex_not_installed', 'codex CLI is not installed');
@@ -2326,7 +2765,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2326
2765
  writeJson(res, 200, {
2327
2766
  ok: true,
2328
2767
  codex_connect: startCodexConnectFlow(),
2329
- status: readCodingCliStatusPayload(),
2768
+ status: readCachedCodingCliStatusPayload({ force: true }),
2330
2769
  });
2331
2770
  return true;
2332
2771
  }
@@ -2339,11 +2778,11 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2339
2778
  writeJson(res, 200, {
2340
2779
  ok: true,
2341
2780
  codex_connect: cancelCodexConnectFlow(),
2342
- status: readCodingCliStatusPayload(),
2781
+ status: readCachedCodingCliStatusPayload({ force: true }),
2343
2782
  });
2344
2783
  return true;
2345
2784
  }
2346
- const spaceAppSessionMatch = pathname.match(/^\/api\/computer\/apps\/([^/]+)\/session$/);
2785
+ const spaceAppSessionMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/apps\/([^/]+)\/session$/);
2347
2786
  if (spaceAppSessionMatch && method === 'DELETE') {
2348
2787
  const cli = normalizeCliName(decodeURIComponent(spaceAppSessionMatch[1] || '').trim());
2349
2788
  if (cli !== 'codex') {
@@ -2359,10 +2798,10 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2359
2798
  writeProtocolError(res, 500, 'auth_error', 'codex_logout_failed', output);
2360
2799
  return true;
2361
2800
  }
2362
- writeJson(res, 200, { ok: true, status: readCodingCliStatusPayload() });
2801
+ writeJson(res, 200, { ok: true, status: readCachedCodingCliStatusPayload({ force: true }) });
2363
2802
  return true;
2364
2803
  }
2365
- if (pathname === '/api/computer/env' && method === 'PUT') {
2804
+ if (pathname === '/api/computers/{computer_id}/env' && method === 'PUT') {
2366
2805
  try {
2367
2806
  const parsed = await readJsonBody(req);
2368
2807
  const updates = parsed && typeof parsed === 'object' && parsed.env && typeof parsed.env === 'object' && !Array.isArray(parsed.env)
@@ -2380,20 +2819,20 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2380
2819
  }
2381
2820
  return true;
2382
2821
  }
2383
- if (pathname === '/api/computer/hub-snapshot' && method === 'GET') {
2822
+ if (pathname === '/api/computers/{computer_id}/hub-snapshot' && method === 'GET') {
2384
2823
  const spaceId = (url.searchParams.get('space_id') || process.env.SUPEN_SPACE_ID || 'local').trim() || 'local';
2385
2824
  writeJson(res, 200, buildHubSnapshotForSpace(spaceId));
2386
2825
  return true;
2387
2826
  }
2388
- if (pathname === '/api/computer/usage' && method === 'GET') {
2827
+ if (pathname === '/api/computers/{computer_id}/usage' && method === 'GET') {
2389
2828
  writeJson(res, 200, getGlobalUsage());
2390
2829
  return true;
2391
2830
  }
2392
- if (pathname === '/api/computer/usage/daily' && method === 'GET') {
2831
+ if (pathname === '/api/computers/{computer_id}/usage/daily' && method === 'GET') {
2393
2832
  writeJson(res, 200, { daily: getDailyUsage() });
2394
2833
  return true;
2395
2834
  }
2396
- if (pathname === '/api/computer/codex/subscription' && method === 'GET') {
2835
+ if (pathname === '/api/computers/{computer_id}/codex/subscription' && method === 'GET') {
2397
2836
  try {
2398
2837
  writeJson(res, 200, await readCodexSubscription());
2399
2838
  }
@@ -2402,17 +2841,45 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2402
2841
  }
2403
2842
  return true;
2404
2843
  }
2405
- if (pathname === '/api/computer/quota-status' && method === 'GET') {
2844
+ if (pathname === '/api/computers/{computer_id}/quota-status' && method === 'GET') {
2406
2845
  writeJson(res, 200, readLatestSpaceQuotaStatus());
2407
2846
  return true;
2408
2847
  }
2409
- if (pathname === '/api/computer/codex/threads' && method === 'GET') {
2848
+ if (pathname === '/api/computers/{computer_id}/codex/threads' && method === 'GET') {
2410
2849
  const limitRaw = coerceSingleQueryParam(url.searchParams.get('limit'));
2411
2850
  const limit = limitRaw ? Number.parseInt(limitRaw, 10) : MIRRORED_THREAD_LIMIT;
2412
2851
  writeJson(res, 200, readMirroredTaskProjects(Number.isFinite(limit) ? limit : MIRRORED_THREAD_LIMIT));
2413
2852
  return true;
2414
2853
  }
2415
- const mirroredThreadStreamMatch = pathname.match(/^\/api\/computer\/codex\/threads\/([^/]+)\/stream$/);
2854
+ if (pathname === '/api/computers/{computer_id}/files/preview' && (method === 'GET' || method === 'HEAD')) {
2855
+ const requestedPath = coerceSingleQueryParam(url.searchParams.get('path')) || '';
2856
+ const filePath = resolveRemoteFilePath(requestedPath);
2857
+ if (!filePath) {
2858
+ writeProtocolError(res, 400, 'validation_error', 'invalid_file_path', 'A valid file path is required.');
2859
+ return true;
2860
+ }
2861
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
2862
+ writeProtocolError(res, 404, 'not_found', 'remote_file_not_found', 'File not found.');
2863
+ return true;
2864
+ }
2865
+ serveRemoteFile(req, res, filePath);
2866
+ return true;
2867
+ }
2868
+ const mirroredThreadArchiveMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/codex\/threads\/([^/]+)\/archive$/);
2869
+ if (mirroredThreadArchiveMatch && method === 'POST') {
2870
+ const threadId = decodeURIComponent(mirroredThreadArchiveMatch[1] || '').trim();
2871
+ if (!threadId) {
2872
+ writeProtocolError(res, 400, 'validation_error', 'missing_thread_id', 'Thread id is required.');
2873
+ return true;
2874
+ }
2875
+ if (!archiveMirroredThread(threadId)) {
2876
+ writeProtocolError(res, 404, 'not_found', 'mirrored_thread_not_found', 'Mirrored task was not found.');
2877
+ return true;
2878
+ }
2879
+ writeJson(res, 200, { ok: true, archived: true });
2880
+ return true;
2881
+ }
2882
+ const mirroredThreadStreamMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/codex\/threads\/([^/]+)\/stream$/);
2416
2883
  if (mirroredThreadStreamMatch && method === 'GET') {
2417
2884
  const threadId = decodeURIComponent(mirroredThreadStreamMatch[1] || '').trim();
2418
2885
  if (!threadId) {
@@ -2455,7 +2922,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2455
2922
  res.flush?.();
2456
2923
  return true;
2457
2924
  }
2458
- const mirroredThreadHistoryMatch = pathname.match(/^\/api\/computer\/codex\/threads\/([^/]+)\/messages$/);
2925
+ const mirroredThreadHistoryMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/codex\/threads\/([^/]+)\/messages$/);
2459
2926
  if (mirroredThreadHistoryMatch && method === 'GET') {
2460
2927
  const threadId = decodeURIComponent(mirroredThreadHistoryMatch[1] || '').trim();
2461
2928
  const limitRaw = coerceSingleQueryParam(url.searchParams.get('limit'));
@@ -2470,7 +2937,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2470
2937
  writeJson(res, 200, history);
2471
2938
  return true;
2472
2939
  }
2473
- const mirroredThreadAdoptMatch = pathname.match(/^\/api\/computer\/codex\/threads\/([^/]+)\/adopt$/);
2940
+ const mirroredThreadAdoptMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/codex\/threads\/([^/]+)\/adopt$/);
2474
2941
  if (mirroredThreadAdoptMatch && method === 'POST') {
2475
2942
  const threadId = decodeURIComponent(mirroredThreadAdoptMatch[1] || '').trim();
2476
2943
  const parsed = await readJsonBody(req);
@@ -2487,7 +2954,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2487
2954
  writeJson(res, 200, { thread: serializeAdoptedMirroredThread(session) });
2488
2955
  return true;
2489
2956
  }
2490
- if (pathname === '/api/computer/codex/projects/open' && method === 'POST') {
2957
+ if (pathname === '/api/computers/{computer_id}/codex/projects/open' && method === 'POST') {
2491
2958
  try {
2492
2959
  const parsed = await readJsonBody(req);
2493
2960
  const projectPath = parsed && typeof parsed === 'object' && typeof parsed.path === 'string'
@@ -2514,12 +2981,12 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2514
2981
  }
2515
2982
  return true;
2516
2983
  }
2517
- if (pathname === '/api/computer/mcp/servers' && method === 'GET') {
2984
+ if (pathname === '/api/computers/{computer_id}/mcp/servers' && method === 'GET') {
2518
2985
  writeJson(res, 200, { servers: buildMcpSettingsResponse().servers });
2519
2986
  return true;
2520
2987
  }
2521
2988
  /** Tools discovered via MCP `tools/list` for each connected server (in-memory; empty if servers not connected). */
2522
- if (pathname === '/api/computer/mcp/tool-catalog' && method === 'GET') {
2989
+ if (pathname === '/api/computers/{computer_id}/mcp/tool-catalog' && method === 'GET') {
2523
2990
  try {
2524
2991
  const force = url.searchParams.get('refresh') === '1' || url.searchParams.get('force') === '1';
2525
2992
  const mgr = await getMcpManager({ forceConfigRefresh: force });
@@ -2538,11 +3005,11 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2538
3005
  }
2539
3006
  return true;
2540
3007
  }
2541
- if (pathname === '/api/computer/mcp/settings' && method === 'GET') {
3008
+ if (pathname === '/api/computers/{computer_id}/mcp/settings' && method === 'GET') {
2542
3009
  writeJson(res, 200, buildMcpSettingsResponse());
2543
3010
  return true;
2544
3011
  }
2545
- if (pathname === '/api/computer/mcp/settings' && method === 'PUT') {
3012
+ if (pathname === '/api/computers/{computer_id}/mcp/settings' && method === 'PUT') {
2546
3013
  const parsed = await readJsonBody(req);
2547
3014
  const allowedKeys = new Set(listMcpEnvKeys());
2548
3015
  const payloadEnv = parsed && typeof parsed === 'object' && parsed.env && typeof parsed.env === 'object'
@@ -2563,25 +3030,14 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2563
3030
  writeJson(res, 200, buildMcpSettingsResponse());
2564
3031
  return true;
2565
3032
  }
2566
- if (pathname === '/api/computer' && method === 'GET') {
2567
- const cpus = os.cpus();
2568
- const memTotal = os.totalmem();
2569
- const memFree = os.freemem();
3033
+ if (pathname === '/api/computers/{computer_id}' && method === 'GET') {
2570
3034
  const enroll = readEnrollmentState(DAEMON_HOSTNAME || undefined);
2571
- let disk_total;
2572
- let disk_free;
2573
- let disk_used;
2574
- try {
2575
- const stats = fs.statfsSync(SUPEN_HOME);
2576
- disk_total = stats.bsize * stats.blocks;
2577
- disk_free = stats.bsize * stats.bavail;
2578
- disk_used = disk_total - disk_free;
2579
- }
2580
- catch { /* ignore */ }
3035
+ const daemon = detectDaemonPackageStatus();
2581
3036
  writeJson(res, 200, {
2582
3037
  daemon_id: enroll.daemon_id,
2583
3038
  hostname: os.hostname(),
2584
- version: process.env.SUPEN_DAEMON_VERSION || process.env.npm_package_version || null,
3039
+ version: daemon.version,
3040
+ daemon,
2585
3041
  runtime: {
2586
3042
  node: process.version,
2587
3043
  platform: process.platform,
@@ -2602,31 +3058,17 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2602
3058
  queued_tasks: 0,
2603
3059
  total_slots: 0,
2604
3060
  },
2605
- os: {
2606
- hostname: os.hostname(),
2607
- platform: os.platform(),
2608
- arch: os.arch(),
2609
- cpus: cpus.length,
2610
- cpu_model: cpus[0]?.model || 'Unknown',
2611
- load_avg: os.loadavg(),
2612
- mem_total: memTotal,
2613
- mem_free: memFree,
2614
- mem_used: memTotal - memFree,
2615
- uptime: os.uptime(),
2616
- disk_total,
2617
- disk_free,
2618
- disk_used,
2619
- },
3061
+ os: collectOsTelemetryFields(),
2620
3062
  });
2621
3063
  return true;
2622
3064
  }
2623
3065
  // ── Enrollment ──
2624
- if (pathname === '/api/computer/enroll/status' && method === 'GET') {
3066
+ if (pathname === '/api/computers/{computer_id}/enroll/status' && method === 'GET') {
2625
3067
  const state = readEnrollmentState(DAEMON_HOSTNAME || undefined);
2626
3068
  writeJson(res, 200, state);
2627
3069
  return true;
2628
3070
  }
2629
- if (pathname === '/api/computer/enroll/verify' && method === 'POST') {
3071
+ if (pathname === '/api/computers/{computer_id}/enroll/verify' && method === 'POST') {
2630
3072
  const parsed = await readJsonBody(req);
2631
3073
  const result = verifyEnrollmentToken({
2632
3074
  token: parsed.token,
@@ -2636,12 +3078,12 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2636
3078
  writeJson(res, result.ok ? 200 : 401, result);
2637
3079
  return true;
2638
3080
  }
2639
- if (pathname === '/api/computer/trust' && method === 'POST') {
2640
- writeProtocolError(res, 410, 'auth_error', 'trust_endpoint_removed', 'Direct trust elevation is disabled. Use the enrollment flow (/api/computer/enroll/token + /api/computer/enroll/verify) to transition a daemon to trusted state.');
3081
+ if (pathname === '/api/computers/{computer_id}/trust' && method === 'POST') {
3082
+ writeProtocolError(res, 410, 'auth_error', 'trust_endpoint_removed', 'Direct trust elevation is disabled. Use the enrollment flow (/api/computers/{computer_id}/enroll/token + /api/computers/{computer_id}/enroll/verify) to transition a daemon to trusted state.');
2641
3083
  return true;
2642
3084
  }
2643
3085
  // Create enrollment token
2644
- if (pathname === '/api/computer/enroll/token' && method === 'POST') {
3086
+ if (pathname === '/api/computers/{computer_id}/enroll/token' && method === 'POST') {
2645
3087
  try {
2646
3088
  const parsed = await readJsonBody(req);
2647
3089
  const ttlMinutes = parsed?.ttl_minutes || DEFAULT_TOKEN_TTL_MINUTES;
@@ -2661,7 +3103,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2661
3103
  return true;
2662
3104
  }
2663
3105
  // Revoke enrollment
2664
- if (pathname === '/api/computer/enroll/revoke' && method === 'POST') {
3106
+ if (pathname === '/api/computers/{computer_id}/enroll/revoke' && method === 'POST') {
2665
3107
  try {
2666
3108
  setTrustState('revoked', { daemonId: DAEMON_HOSTNAME || undefined });
2667
3109
  writeJson(res, 200, { ok: true, trust_state: 'revoked' });