@supen-ai/cli 0.1.9 → 0.1.11

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 (74) hide show
  1. package/README.md +9 -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/config.js +1 -1
  12. package/daemon/dist/core/config.js.map +1 -1
  13. package/daemon/dist/core/control-commands.js +1 -1
  14. package/daemon/dist/core/cortex.js +4 -4
  15. package/daemon/dist/core/gateway-config.d.ts +1 -1
  16. package/daemon/dist/core/gateway-config.d.ts.map +1 -1
  17. package/daemon/dist/core/gateway-config.js +1 -1
  18. package/daemon/dist/core/gateway-config.js.map +1 -1
  19. package/daemon/dist/core/gateway.d.ts.map +1 -1
  20. package/daemon/dist/core/gateway.js +8 -34
  21. package/daemon/dist/core/gateway.js.map +1 -1
  22. package/daemon/dist/core/os-info.d.ts +65 -0
  23. package/daemon/dist/core/os-info.d.ts.map +1 -0
  24. package/daemon/dist/core/os-info.js +225 -0
  25. package/daemon/dist/core/os-info.js.map +1 -0
  26. package/daemon/dist/core/protocol-adapter.js +2 -2
  27. package/daemon/dist/core/thread-runtime-state.js +1 -1
  28. package/daemon/dist/http/context.js +2 -2
  29. package/daemon/dist/http/context.js.map +1 -1
  30. package/daemon/dist/http/router.d.ts.map +1 -1
  31. package/daemon/dist/http/router.js +4 -4
  32. package/daemon/dist/http/router.js.map +1 -1
  33. package/daemon/dist/http/routes/agents.d.ts.map +1 -1
  34. package/daemon/dist/http/routes/agents.js +13 -16
  35. package/daemon/dist/http/routes/agents.js.map +1 -1
  36. package/daemon/dist/http/routes/automations.d.ts.map +1 -1
  37. package/daemon/dist/http/routes/automations.js +27 -13
  38. package/daemon/dist/http/routes/automations.js.map +1 -1
  39. package/daemon/dist/http/routes/autonomy.js +4 -4
  40. package/daemon/dist/http/routes/autonomy.js.map +1 -1
  41. package/daemon/dist/http/routes/chat-input.d.ts +0 -1
  42. package/daemon/dist/http/routes/chat-input.d.ts.map +1 -1
  43. package/daemon/dist/http/routes/chat-input.js +0 -1
  44. package/daemon/dist/http/routes/chat-input.js.map +1 -1
  45. package/daemon/dist/http/routes/plugins.js +6 -6
  46. package/daemon/dist/http/routes/plugins.js.map +1 -1
  47. package/daemon/dist/http/routes/rpc.d.ts.map +1 -1
  48. package/daemon/dist/http/routes/rpc.js +43 -10
  49. package/daemon/dist/http/routes/rpc.js.map +1 -1
  50. package/daemon/dist/http/routes/sessions.js +15 -15
  51. package/daemon/dist/http/routes/sessions.js.map +1 -1
  52. package/daemon/dist/http/routes/skills.js +11 -11
  53. package/daemon/dist/http/routes/skills.js.map +1 -1
  54. package/daemon/dist/http/routes/system.d.ts +1 -0
  55. package/daemon/dist/http/routes/system.d.ts.map +1 -1
  56. package/daemon/dist/http/routes/system.js +316 -82
  57. package/daemon/dist/http/routes/system.js.map +1 -1
  58. package/daemon/dist/index.js +1 -1
  59. package/daemon/scripts/supen-daemon.js +1 -1
  60. package/dist/auth/login.js +1 -1
  61. package/dist/auth/login.js.map +1 -1
  62. package/dist/bootstrap.d.ts +1 -0
  63. package/dist/bootstrap.js +5 -4
  64. package/dist/bootstrap.js.map +1 -1
  65. package/dist/computer.js +25 -4
  66. package/dist/computer.js.map +1 -1
  67. package/dist/daemon-manage.js +9 -2
  68. package/dist/daemon-manage.js.map +1 -1
  69. package/dist/index.js +2 -1
  70. package/dist/index.js.map +1 -1
  71. package/dist/os-identity.d.ts +6 -0
  72. package/dist/os-identity.js +68 -0
  73. package/dist/os-identity.js.map +1 -0
  74. package/package.json +2 -2
@@ -10,6 +10,7 @@ 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';
13
14
  import { getGatewayInstance, getLlmToken } from '../../core/gateway.js';
14
15
  import { normalizeGatewayUplinkUrl, readGatewayConfig, writeGatewayConfig, } from '../../core/gateway-config.js';
15
16
  import { ensureSession, getAllAgents, getDailyUsage, getGlobalUsage, getSessionsForAgent, getSessionUiEvents, updateSessionBackendDriverId, updateSessionSdkId, } from '../../core/store.js';
@@ -53,6 +54,33 @@ function readSqliteRows(dbPath, query) {
53
54
  return null;
54
55
  }
55
56
  }
57
+ function quoteSqliteString(value) {
58
+ return `'${value.replace(/'/g, "''")}'`;
59
+ }
60
+ function runSqliteStatement(dbPath, query, params = []) {
61
+ try {
62
+ const { DatabaseSync } = require('node:sqlite');
63
+ const db = new DatabaseSync(dbPath);
64
+ try {
65
+ db.prepare(query).run(...params);
66
+ return true;
67
+ }
68
+ finally {
69
+ db.close();
70
+ }
71
+ }
72
+ catch {
73
+ const expandedQuery = params.reduce((sql, param) => {
74
+ const value = typeof param === 'string' ? quoteSqliteString(param) : String(param);
75
+ return sql.replace('?', value);
76
+ }, query);
77
+ const result = spawnSync('sqlite3', [dbPath, expandedQuery], {
78
+ encoding: 'utf-8',
79
+ maxBuffer: 1024 * 1024,
80
+ });
81
+ return result.status === 0;
82
+ }
83
+ }
56
84
  function gatewayUplinkStatus(config = readGatewayConfig()) {
57
85
  const gatewayUrl = (config.gateway_url || '').trim();
58
86
  return {
@@ -218,7 +246,7 @@ function summarizeUiChunk(chunk) {
218
246
  : 'Unknown error';
219
247
  return { category: 'error', summary: truncateText(errorText) };
220
248
  }
221
- if (type === 'data-supen-event') {
249
+ if (type === 'data-codex-event') {
222
250
  const data = chunk.data && typeof chunk.data === 'object'
223
251
  ? chunk.data
224
252
  : {};
@@ -476,6 +504,15 @@ function readThreadIndexEntry(threadId) {
476
504
  function readThreadStateEntry(threadId) {
477
505
  return readThreadStateEntries(1000).find((entry) => entry.id === threadId) || null;
478
506
  }
507
+ function archiveMirroredThread(threadId) {
508
+ const dbPath = path.join(localAgentHome(), 'state_5.sqlite');
509
+ if (!threadId || !fs.existsSync(dbPath))
510
+ return false;
511
+ const existing = readThreadStateEntry(threadId);
512
+ if (!existing)
513
+ return false;
514
+ return runSqliteStatement(dbPath, 'update threads set archived = 1 where id = ?', [threadId]);
515
+ }
479
516
  function isoFromMillis(value) {
480
517
  const ms = typeof value === 'number' ? value : Number(value);
481
518
  if (!Number.isFinite(ms) || ms <= 0)
@@ -757,6 +794,66 @@ function isMirrorableCodexWorkspace(projectPath) {
757
794
  }
758
795
  return !isPathWithin(path.join(SUPEN_HOME, 'tasks'), projectPath);
759
796
  }
797
+ function mimeTypeForWorkspaceFile(filePath) {
798
+ const ext = path.extname(filePath).toLowerCase();
799
+ if (ext === '.md' || ext === '.markdown' || ext === '.mdx')
800
+ return 'text/markdown; charset=utf-8';
801
+ if (ext === '.txt' || ext === '.log' || ext === '.env')
802
+ return 'text/plain; charset=utf-8';
803
+ if (ext === '.json')
804
+ return 'application/json; charset=utf-8';
805
+ if (ext === '.html' || ext === '.htm')
806
+ return 'text/html; charset=utf-8';
807
+ if (ext === '.css')
808
+ return 'text/css; charset=utf-8';
809
+ if (ext === '.js' || ext === '.mjs' || ext === '.cjs')
810
+ return 'text/javascript; charset=utf-8';
811
+ if (ext === '.ts' || ext === '.tsx' || ext === '.jsx')
812
+ return 'text/plain; charset=utf-8';
813
+ if (ext === '.png')
814
+ return 'image/png';
815
+ if (ext === '.jpg' || ext === '.jpeg')
816
+ return 'image/jpeg';
817
+ if (ext === '.gif')
818
+ return 'image/gif';
819
+ if (ext === '.webp')
820
+ return 'image/webp';
821
+ if (ext === '.svg')
822
+ return 'image/svg+xml';
823
+ if (ext === '.pdf')
824
+ return 'application/pdf';
825
+ return 'application/octet-stream';
826
+ }
827
+ function serveRemoteFile(req, res, filePath) {
828
+ const stat = fs.statSync(filePath);
829
+ const basename = path.basename(filePath);
830
+ res.writeHead(200, {
831
+ 'Content-Type': mimeTypeForWorkspaceFile(filePath),
832
+ 'Content-Length': stat.size,
833
+ 'Last-Modified': stat.mtime.toUTCString(),
834
+ 'Cache-Control': 'no-store',
835
+ 'Content-Disposition': `inline; filename="${basename.replace(/"/g, '')}"`,
836
+ });
837
+ if (req.method === 'HEAD') {
838
+ res.end();
839
+ return;
840
+ }
841
+ fs.createReadStream(filePath).pipe(res);
842
+ }
843
+ function resolveRemoteFilePath(rawPath) {
844
+ const trimmed = rawPath.trim();
845
+ if (!trimmed)
846
+ return null;
847
+ const expanded = trimmed === '~'
848
+ ? os.homedir()
849
+ : trimmed.startsWith('~/')
850
+ ? path.join(os.homedir(), trimmed.slice(2))
851
+ : trimmed;
852
+ const resolved = path.resolve(expanded);
853
+ if (!path.isAbsolute(resolved))
854
+ return null;
855
+ return resolved;
856
+ }
760
857
  function isAutomationRunThreadTitle(title) {
761
858
  if (!title)
762
859
  return false;
@@ -880,7 +977,9 @@ function sanitizeMirroredHistoryPayload(value, depth = 0) {
880
977
  }
881
978
  function safeMirroredHistoryAttachmentUrl(url) {
882
979
  const trimmed = url.trim();
883
- if (trimmed.startsWith('data:') && trimmed.length > MIRRORED_THREAD_INLINE_DATA_URL_LIMIT) {
980
+ if (trimmed.startsWith('data:') &&
981
+ !trimmed.startsWith('data:image/') &&
982
+ trimmed.length > MIRRORED_THREAD_INLINE_DATA_URL_LIMIT) {
884
983
  return '';
885
984
  }
886
985
  return trimmed;
@@ -902,12 +1001,15 @@ function imageAttachmentFromUrl(url, index) {
902
1001
  const trimmed = url.trim();
903
1002
  if (!trimmed)
904
1003
  return null;
1004
+ const safeUrl = safeMirroredHistoryAttachmentUrl(trimmed);
1005
+ if (!safeUrl)
1006
+ return null;
905
1007
  const mimeType = mimeTypeFromImageUrl(trimmed);
906
1008
  const filename = `pasted-image-${index + 1}.${imageExtensionFromMimeType(mimeType)}`;
907
1009
  return {
908
1010
  name: filename,
909
1011
  filename,
910
- url: safeMirroredHistoryAttachmentUrl(trimmed),
1012
+ url: safeUrl,
911
1013
  mimeType,
912
1014
  mime_type: mimeType,
913
1015
  };
@@ -1079,7 +1181,7 @@ function codexReplayEvent(eventId, timestamp, taskId, raw) {
1079
1181
  timestamp,
1080
1182
  task_id: taskId,
1081
1183
  chunk: {
1082
- type: 'data-supen-event',
1184
+ type: 'data-codex-event',
1083
1185
  id: eventId,
1084
1186
  data: { raw },
1085
1187
  },
@@ -1158,6 +1260,49 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1158
1260
  ...(attachments.length > 0 ? { attachments } : {}),
1159
1261
  });
1160
1262
  };
1263
+ const pushWebUserMessage = (event) => {
1264
+ if (event.source !== 'web' || event.event_type !== 'user_message')
1265
+ return;
1266
+ const payload = event.raw_payload && typeof event.raw_payload === 'object'
1267
+ ? event.raw_payload
1268
+ : {};
1269
+ const text = typeof payload.content === 'string' ? payload.content : '';
1270
+ const attachments = Array.isArray(payload.attachments)
1271
+ ? payload.attachments
1272
+ .map((attachment, index) => {
1273
+ if (!attachment || typeof attachment !== 'object')
1274
+ return null;
1275
+ const record = attachment;
1276
+ const url = typeof record.url === 'string' ? record.url :
1277
+ typeof record.supenUrl === 'string' ? record.supenUrl :
1278
+ typeof record.path === 'string' ? record.path :
1279
+ typeof record.taskPath === 'string' ? record.taskPath :
1280
+ '';
1281
+ if (!url.trim())
1282
+ return null;
1283
+ const filename = typeof record.filename === 'string' ? record.filename :
1284
+ typeof record.name === 'string' ? record.name :
1285
+ `attachment-${index + 1}`;
1286
+ const mimeType = typeof record.mimeType === 'string' ? record.mimeType :
1287
+ typeof record.mime_type === 'string' ? record.mime_type :
1288
+ 'application/octet-stream';
1289
+ const safeUrl = safeMirroredHistoryAttachmentUrl(url);
1290
+ if (!safeUrl)
1291
+ return null;
1292
+ return {
1293
+ name: filename,
1294
+ filename,
1295
+ url: safeUrl,
1296
+ mimeType,
1297
+ mime_type: mimeType,
1298
+ };
1299
+ })
1300
+ .filter((attachment) => Boolean(attachment))
1301
+ : [];
1302
+ const timestamp = typeof payload.timestamp === 'string' ? payload.timestamp : event.received_at;
1303
+ const messageId = typeof payload.id === 'string' ? payload.id : `${threadId}-web-user-${event.sequence}`;
1304
+ pushUserMessage(text, timestamp, messageId, attachments);
1305
+ };
1161
1306
  const pushEvent = (timestamp, chunk) => {
1162
1307
  eventIndex += 1;
1163
1308
  events.push({
@@ -1368,15 +1513,20 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1368
1513
  task_id: threadId,
1369
1514
  });
1370
1515
  }
1371
- for (const event of listRecentThreadEventsAfter(eventLogThreadId, 0, {
1516
+ const eventLogEvents = listRecentThreadEventsAfter(eventLogThreadId, 0, {
1372
1517
  limit: MIRRORED_THREAD_STREAM_REPLAY_LIMIT,
1373
1518
  maxBytes: MIRRORED_THREAD_STREAM_REPLAY_MAX_BYTES,
1374
- })) {
1519
+ });
1520
+ for (const event of eventLogEvents) {
1521
+ pushWebUserMessage(event);
1522
+ }
1523
+ for (const event of eventLogEvents) {
1375
1524
  const chunk = threadStreamChunkForEvent(event);
1376
1525
  if (!chunk)
1377
1526
  continue;
1378
1527
  events.push({
1379
1528
  id: `${threadId}-event-log-${event.sequence}`,
1529
+ sequence: event.sequence,
1380
1530
  timestamp: event.received_at,
1381
1531
  task_id: threadId,
1382
1532
  chunk,
@@ -1539,11 +1689,13 @@ function threadStreamChunkForEvent(event) {
1539
1689
  return null;
1540
1690
  if (event.raw_payload &&
1541
1691
  typeof event.raw_payload === 'object' &&
1542
- !Array.isArray(event.raw_payload)) {
1692
+ !Array.isArray(event.raw_payload) &&
1693
+ typeof event.raw_payload.type === 'string' &&
1694
+ String(event.raw_payload.type).trim()) {
1543
1695
  return event.raw_payload;
1544
1696
  }
1545
1697
  return {
1546
- type: 'data-supen-event',
1698
+ type: 'data-codex-event',
1547
1699
  data: {
1548
1700
  eventType: event.event_type,
1549
1701
  raw: event.raw_payload,
@@ -1653,19 +1805,87 @@ function codexConnectSnapshot() {
1653
1805
  error: codexConnectRuntime.error,
1654
1806
  };
1655
1807
  }
1808
+ function detectGlobalNpmRoot() {
1809
+ const result = spawnSync('npm', ['root', '-g'], {
1810
+ encoding: 'utf8',
1811
+ timeout: 3000,
1812
+ });
1813
+ if (result.status !== 0)
1814
+ return null;
1815
+ const root = String(result.stdout || '').trim();
1816
+ if (!root)
1817
+ return null;
1818
+ try {
1819
+ return fs.realpathSync(root);
1820
+ }
1821
+ catch {
1822
+ return root;
1823
+ }
1824
+ }
1825
+ function inferCliInstallSource(name, executablePath, resolvedPath) {
1826
+ if (!executablePath && !resolvedPath)
1827
+ return { install_source: null, update_supported: false };
1828
+ const candidates = [executablePath || '', resolvedPath || ''].map((value) => value.toLowerCase());
1829
+ const normalized = candidates.join('\n');
1830
+ if (normalized.includes('/.local/share/pnpm/') || normalized.includes('/pnpm/')) {
1831
+ return { install_source: 'pnpm', update_supported: false };
1832
+ }
1833
+ if (normalized.includes('/homebrew/') || normalized.includes('/opt/homebrew/') || normalized.includes('/cellar/')) {
1834
+ return { install_source: 'homebrew', update_supported: false };
1835
+ }
1836
+ if (name === 'codex') {
1837
+ const npmRoot = detectGlobalNpmRoot();
1838
+ const codexPackagePath = resolvedPath || executablePath || '';
1839
+ if (npmRoot && codexPackagePath.startsWith(`${npmRoot}${path.sep}@openai${path.sep}codex${path.sep}`)) {
1840
+ return { install_source: 'npm-global', update_supported: true };
1841
+ }
1842
+ if (normalized.includes('/node_modules/@openai/codex/')) {
1843
+ return { install_source: 'node-package', update_supported: false };
1844
+ }
1845
+ }
1846
+ if (name === 'gemini') {
1847
+ const npmRoot = detectGlobalNpmRoot();
1848
+ const geminiPackagePath = resolvedPath || executablePath || '';
1849
+ if (npmRoot && geminiPackagePath.startsWith(`${npmRoot}${path.sep}@google${path.sep}gemini-cli${path.sep}`)) {
1850
+ return { install_source: 'npm-global', update_supported: true };
1851
+ }
1852
+ if (normalized.includes('/node_modules/@google/gemini-cli/')) {
1853
+ return { install_source: 'node-package', update_supported: false };
1854
+ }
1855
+ }
1856
+ return { install_source: null, update_supported: false };
1857
+ }
1656
1858
  function detectCliInstalled(name) {
1657
- const cmd = spawnSync('sh', ['-lc', `command -v ${name}`], {
1859
+ const cmd = spawnSync('sh', ['-c', `command -v ${name}`], {
1658
1860
  encoding: 'utf8',
1659
1861
  timeout: 3000,
1660
1862
  });
1661
- if (cmd.status !== 0)
1662
- return { installed: false, version: null };
1863
+ if (cmd.status !== 0) {
1864
+ return { installed: false, version: null, path: null, resolved_path: null, install_source: null, update_supported: false };
1865
+ }
1866
+ const executablePath = String(cmd.stdout || '').split(/\r?\n/g).find(Boolean)?.trim() || null;
1867
+ let resolvedPath = null;
1868
+ if (executablePath) {
1869
+ try {
1870
+ resolvedPath = fs.realpathSync(executablePath);
1871
+ }
1872
+ catch {
1873
+ resolvedPath = executablePath;
1874
+ }
1875
+ }
1663
1876
  const versionCmd = spawnSync(name, ['--version'], {
1664
1877
  encoding: 'utf8',
1665
1878
  timeout: 5000,
1666
1879
  });
1667
1880
  const output = trimLogNoise(`${versionCmd.stdout || ''}\n${versionCmd.stderr || ''}`) || '';
1668
- return { installed: true, version: output || null };
1881
+ const source = inferCliInstallSource(name, executablePath, resolvedPath);
1882
+ return {
1883
+ installed: true,
1884
+ version: output || null,
1885
+ path: executablePath,
1886
+ resolved_path: resolvedPath,
1887
+ ...source,
1888
+ };
1669
1889
  }
1670
1890
  function detectCodexAuthStatus(installed) {
1671
1891
  if (!installed)
@@ -1799,6 +2019,10 @@ function readCodingCliStatusPayload() {
1799
2019
  name,
1800
2020
  installed: detected.installed,
1801
2021
  version: detected.version,
2022
+ path: detected.path,
2023
+ resolved_path: detected.resolved_path,
2024
+ install_source: detected.install_source,
2025
+ update_supported: detected.update_supported,
1802
2026
  };
1803
2027
  });
1804
2028
  const codexInstalled = clis.find((cli) => cli.name === 'codex')?.installed ?? false;
@@ -1819,6 +2043,10 @@ function readCodingCliStatusPayload() {
1819
2043
  installed: detected.installed,
1820
2044
  version: detected.version,
1821
2045
  managed: app.managed,
2046
+ path: detected.path,
2047
+ resolved_path: detected.resolved_path,
2048
+ install_source: detected.install_source,
2049
+ update_supported: detected.update_supported,
1822
2050
  };
1823
2051
  });
1824
2052
  return {
@@ -2017,15 +2245,15 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2017
2245
  writeJson(res, 200, { status: 'ok' });
2018
2246
  return true;
2019
2247
  }
2020
- if (pathname === '/api/computer/openapi.json' && method === 'GET') {
2248
+ if (pathname === '/api/computers/{computer_id}/openapi.json' && method === 'GET') {
2021
2249
  writeJson(res, 200, buildDaemonOpenApiSpec());
2022
2250
  return true;
2023
2251
  }
2024
- if (pathname === '/api/computer/models' && method === 'GET') {
2252
+ if (pathname === '/api/computers/{computer_id}/models' && method === 'GET') {
2025
2253
  writeJson(res, 200, { models: MODELS_REGISTRY });
2026
2254
  return true;
2027
2255
  }
2028
- if (pathname === '/api/computer/config/reload' && method === 'POST') {
2256
+ if (pathname === '/api/computers/{computer_id}/config/reload' && method === 'POST') {
2029
2257
  const before = {
2030
2258
  total_models: MODELS_REGISTRY.length,
2031
2259
  default_model: DEFAULT_MODEL?.id ?? null,
@@ -2041,11 +2269,11 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2041
2269
  });
2042
2270
  return true;
2043
2271
  }
2044
- if (pathname === '/api/computer/config-summary' && method === 'GET') {
2272
+ if (pathname === '/api/computers/{computer_id}/config-summary' && method === 'GET') {
2045
2273
  writeJson(res, 200, readConfigSummary());
2046
2274
  return true;
2047
2275
  }
2048
- if (pathname === '/api/computer/logs' && method === 'GET') {
2276
+ if (pathname === '/api/computers/{computer_id}/logs' && method === 'GET') {
2049
2277
  const agentId = coerceSingleQueryParam(url.searchParams.get('agent_id'));
2050
2278
  const sessionId = coerceSingleQueryParam(url.searchParams.get('session_id'));
2051
2279
  const limitRaw = coerceSingleQueryParam(url.searchParams.get('limit'));
@@ -2057,7 +2285,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2057
2285
  }));
2058
2286
  return true;
2059
2287
  }
2060
- if (pathname === '/api/computer/logs/stream' && method === 'GET') {
2288
+ if (pathname === '/api/computers/{computer_id}/logs/stream' && method === 'GET') {
2061
2289
  const agentId = coerceSingleQueryParam(url.searchParams.get('agent_id'));
2062
2290
  const sessionId = coerceSingleQueryParam(url.searchParams.get('session_id'));
2063
2291
  const limitRaw = coerceSingleQueryParam(url.searchParams.get('limit'));
@@ -2100,7 +2328,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2100
2328
  writeSse(res);
2101
2329
  return true;
2102
2330
  }
2103
- if (pathname === '/api/computer/config-yaml' && method === 'GET') {
2331
+ if (pathname === '/api/computers/{computer_id}/config-yaml' && method === 'GET') {
2104
2332
  try {
2105
2333
  const yaml = readConfigYamlFile();
2106
2334
  writeJson(res, 200, { yaml });
@@ -2110,7 +2338,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2110
2338
  }
2111
2339
  return true;
2112
2340
  }
2113
- if (pathname === '/api/computer/config-yaml' && method === 'PUT') {
2341
+ if (pathname === '/api/computers/{computer_id}/config-yaml' && method === 'PUT') {
2114
2342
  try {
2115
2343
  const parsed = await readJsonBody(req);
2116
2344
  const yamlText = typeof parsed === 'object' && parsed && typeof parsed.yaml === 'string'
@@ -2137,11 +2365,11 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2137
2365
  }
2138
2366
  return true;
2139
2367
  }
2140
- if (pathname === '/api/computer/gateway-uplink' && method === 'GET') {
2368
+ if (pathname === '/api/computers/{computer_id}/gateway-uplink' && method === 'GET') {
2141
2369
  writeJson(res, 200, gatewayUplinkStatus());
2142
2370
  return true;
2143
2371
  }
2144
- if (pathname === '/api/computer/gateway-uplink' && (method === 'PUT' || method === 'PATCH')) {
2372
+ if (pathname === '/api/computers/{computer_id}/gateway-uplink' && (method === 'PUT' || method === 'PATCH')) {
2145
2373
  if (process.env.SUPEN_GATEWAY_URL) {
2146
2374
  writeProtocolError(res, 409, 'config_error', 'env_override', 'SUPEN_GATEWAY_URL is set; update the daemon environment to change the gateway uplink.');
2147
2375
  return true;
@@ -2184,11 +2412,11 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2184
2412
  }
2185
2413
  return true;
2186
2414
  }
2187
- if (pathname === '/api/computer/env' && method === 'GET') {
2415
+ if (pathname === '/api/computers/{computer_id}/env' && method === 'GET') {
2188
2416
  writeJson(res, 200, { env: readSpaceEnvMap() });
2189
2417
  return true;
2190
2418
  }
2191
- if (pathname === '/api/computer/llm-env' && method === 'GET') {
2419
+ if (pathname === '/api/computers/{computer_id}/llm-env' && method === 'GET') {
2192
2420
  const token = getLlmToken() || process.env.SUPEN_LLM_TOKEN || process.env.SUPEN_LLM_API_KEY || '';
2193
2421
  const env = withSupenLlmEnv({
2194
2422
  ...process.env,
@@ -2208,15 +2436,15 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2208
2436
  });
2209
2437
  return true;
2210
2438
  }
2211
- if (pathname === '/api/computer/apps' && method === 'GET') {
2439
+ if (pathname === '/api/computers/{computer_id}/apps' && method === 'GET') {
2212
2440
  writeJson(res, 200, readCodingCliStatusPayload());
2213
2441
  return true;
2214
2442
  }
2215
- if (pathname === '/api/computer/runtime-models' && method === 'GET') {
2443
+ if (pathname === '/api/computers/{computer_id}/runtime-models' && method === 'GET') {
2216
2444
  writeJson(res, 200, readRuntimeModelStatusPayload());
2217
2445
  return true;
2218
2446
  }
2219
- if (pathname === '/api/computer/runtime-models/default' && method === 'PUT') {
2447
+ if (pathname === '/api/computers/{computer_id}/runtime-models/default' && method === 'PUT') {
2220
2448
  try {
2221
2449
  const parsed = await readJsonBody(req);
2222
2450
  const model = typeof parsed === 'object' && parsed && typeof parsed.model === 'string'
@@ -2237,7 +2465,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2237
2465
  }
2238
2466
  return true;
2239
2467
  }
2240
- const spaceCodexTransportDefaultMatch = pathname.match(/^\/api\/computer\/apps\/codex\/transports\/([^/]+)\/default$/);
2468
+ const spaceCodexTransportDefaultMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/apps\/codex\/transports\/([^/]+)\/default$/);
2241
2469
  if (spaceCodexTransportDefaultMatch && method === 'PUT') {
2242
2470
  try {
2243
2471
  const transport = normalizeCodexTransport(decodeURIComponent(spaceCodexTransportDefaultMatch[1] || '').trim());
@@ -2257,7 +2485,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2257
2485
  }
2258
2486
  return true;
2259
2487
  }
2260
- const spaceAppDefaultMatch = pathname.match(/^\/api\/computer\/apps\/([^/]+)\/default$/);
2488
+ const spaceAppDefaultMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/apps\/([^/]+)\/default$/);
2261
2489
  if (spaceAppDefaultMatch && method === 'PUT') {
2262
2490
  try {
2263
2491
  const cli = normalizeCliName(decodeURIComponent(spaceAppDefaultMatch[1] || '').trim());
@@ -2277,7 +2505,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2277
2505
  }
2278
2506
  return true;
2279
2507
  }
2280
- const spaceAppInstallMatch = pathname.match(/^\/api\/computer\/apps\/([^/]+)\/install$/);
2508
+ const spaceAppInstallMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/apps\/([^/]+)\/install$/);
2281
2509
  if (spaceAppInstallMatch && method === 'POST') {
2282
2510
  try {
2283
2511
  const cli = normalizeCliName(decodeURIComponent(spaceAppInstallMatch[1] || '').trim());
@@ -2289,6 +2517,11 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2289
2517
  writeProtocolError(res, 400, 'validation_error', 'install_not_supported', 'Only codex and gemini support managed install at the moment');
2290
2518
  return true;
2291
2519
  }
2520
+ const current = detectCliInstalled(cli);
2521
+ if (current.installed && !current.update_supported) {
2522
+ 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.`);
2523
+ return true;
2524
+ }
2292
2525
  const installSpec = MANAGED_CODING_CLI_INSTALL_COMMANDS[cli];
2293
2526
  const result = spawnSync(installSpec.command, installSpec.args, {
2294
2527
  encoding: 'utf8',
@@ -2310,7 +2543,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2310
2543
  }
2311
2544
  return true;
2312
2545
  }
2313
- const spaceAppConnectMatch = pathname.match(/^\/api\/computer\/apps\/([^/]+)\/connect$/);
2546
+ const spaceAppConnectMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/apps\/([^/]+)\/connect$/);
2314
2547
  if (spaceAppConnectMatch && method === 'POST') {
2315
2548
  const cli = normalizeCliName(decodeURIComponent(spaceAppConnectMatch[1] || '').trim());
2316
2549
  if (cli !== 'codex') {
@@ -2343,7 +2576,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2343
2576
  });
2344
2577
  return true;
2345
2578
  }
2346
- const spaceAppSessionMatch = pathname.match(/^\/api\/computer\/apps\/([^/]+)\/session$/);
2579
+ const spaceAppSessionMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/apps\/([^/]+)\/session$/);
2347
2580
  if (spaceAppSessionMatch && method === 'DELETE') {
2348
2581
  const cli = normalizeCliName(decodeURIComponent(spaceAppSessionMatch[1] || '').trim());
2349
2582
  if (cli !== 'codex') {
@@ -2362,7 +2595,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2362
2595
  writeJson(res, 200, { ok: true, status: readCodingCliStatusPayload() });
2363
2596
  return true;
2364
2597
  }
2365
- if (pathname === '/api/computer/env' && method === 'PUT') {
2598
+ if (pathname === '/api/computers/{computer_id}/env' && method === 'PUT') {
2366
2599
  try {
2367
2600
  const parsed = await readJsonBody(req);
2368
2601
  const updates = parsed && typeof parsed === 'object' && parsed.env && typeof parsed.env === 'object' && !Array.isArray(parsed.env)
@@ -2380,20 +2613,20 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2380
2613
  }
2381
2614
  return true;
2382
2615
  }
2383
- if (pathname === '/api/computer/hub-snapshot' && method === 'GET') {
2616
+ if (pathname === '/api/computers/{computer_id}/hub-snapshot' && method === 'GET') {
2384
2617
  const spaceId = (url.searchParams.get('space_id') || process.env.SUPEN_SPACE_ID || 'local').trim() || 'local';
2385
2618
  writeJson(res, 200, buildHubSnapshotForSpace(spaceId));
2386
2619
  return true;
2387
2620
  }
2388
- if (pathname === '/api/computer/usage' && method === 'GET') {
2621
+ if (pathname === '/api/computers/{computer_id}/usage' && method === 'GET') {
2389
2622
  writeJson(res, 200, getGlobalUsage());
2390
2623
  return true;
2391
2624
  }
2392
- if (pathname === '/api/computer/usage/daily' && method === 'GET') {
2625
+ if (pathname === '/api/computers/{computer_id}/usage/daily' && method === 'GET') {
2393
2626
  writeJson(res, 200, { daily: getDailyUsage() });
2394
2627
  return true;
2395
2628
  }
2396
- if (pathname === '/api/computer/codex/subscription' && method === 'GET') {
2629
+ if (pathname === '/api/computers/{computer_id}/codex/subscription' && method === 'GET') {
2397
2630
  try {
2398
2631
  writeJson(res, 200, await readCodexSubscription());
2399
2632
  }
@@ -2402,17 +2635,45 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2402
2635
  }
2403
2636
  return true;
2404
2637
  }
2405
- if (pathname === '/api/computer/quota-status' && method === 'GET') {
2638
+ if (pathname === '/api/computers/{computer_id}/quota-status' && method === 'GET') {
2406
2639
  writeJson(res, 200, readLatestSpaceQuotaStatus());
2407
2640
  return true;
2408
2641
  }
2409
- if (pathname === '/api/computer/codex/threads' && method === 'GET') {
2642
+ if (pathname === '/api/computers/{computer_id}/codex/threads' && method === 'GET') {
2410
2643
  const limitRaw = coerceSingleQueryParam(url.searchParams.get('limit'));
2411
2644
  const limit = limitRaw ? Number.parseInt(limitRaw, 10) : MIRRORED_THREAD_LIMIT;
2412
2645
  writeJson(res, 200, readMirroredTaskProjects(Number.isFinite(limit) ? limit : MIRRORED_THREAD_LIMIT));
2413
2646
  return true;
2414
2647
  }
2415
- const mirroredThreadStreamMatch = pathname.match(/^\/api\/computer\/codex\/threads\/([^/]+)\/stream$/);
2648
+ if (pathname === '/api/computers/{computer_id}/files/preview' && (method === 'GET' || method === 'HEAD')) {
2649
+ const requestedPath = coerceSingleQueryParam(url.searchParams.get('path')) || '';
2650
+ const filePath = resolveRemoteFilePath(requestedPath);
2651
+ if (!filePath) {
2652
+ writeProtocolError(res, 400, 'validation_error', 'invalid_file_path', 'A valid file path is required.');
2653
+ return true;
2654
+ }
2655
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
2656
+ writeProtocolError(res, 404, 'not_found', 'remote_file_not_found', 'File not found.');
2657
+ return true;
2658
+ }
2659
+ serveRemoteFile(req, res, filePath);
2660
+ return true;
2661
+ }
2662
+ const mirroredThreadArchiveMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/codex\/threads\/([^/]+)\/archive$/);
2663
+ if (mirroredThreadArchiveMatch && method === 'POST') {
2664
+ const threadId = decodeURIComponent(mirroredThreadArchiveMatch[1] || '').trim();
2665
+ if (!threadId) {
2666
+ writeProtocolError(res, 400, 'validation_error', 'missing_thread_id', 'Thread id is required.');
2667
+ return true;
2668
+ }
2669
+ if (!archiveMirroredThread(threadId)) {
2670
+ writeProtocolError(res, 404, 'not_found', 'mirrored_thread_not_found', 'Mirrored task was not found.');
2671
+ return true;
2672
+ }
2673
+ writeJson(res, 200, { ok: true, archived: true });
2674
+ return true;
2675
+ }
2676
+ const mirroredThreadStreamMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/codex\/threads\/([^/]+)\/stream$/);
2416
2677
  if (mirroredThreadStreamMatch && method === 'GET') {
2417
2678
  const threadId = decodeURIComponent(mirroredThreadStreamMatch[1] || '').trim();
2418
2679
  if (!threadId) {
@@ -2455,7 +2716,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2455
2716
  res.flush?.();
2456
2717
  return true;
2457
2718
  }
2458
- const mirroredThreadHistoryMatch = pathname.match(/^\/api\/computer\/codex\/threads\/([^/]+)\/messages$/);
2719
+ const mirroredThreadHistoryMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/codex\/threads\/([^/]+)\/messages$/);
2459
2720
  if (mirroredThreadHistoryMatch && method === 'GET') {
2460
2721
  const threadId = decodeURIComponent(mirroredThreadHistoryMatch[1] || '').trim();
2461
2722
  const limitRaw = coerceSingleQueryParam(url.searchParams.get('limit'));
@@ -2470,7 +2731,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2470
2731
  writeJson(res, 200, history);
2471
2732
  return true;
2472
2733
  }
2473
- const mirroredThreadAdoptMatch = pathname.match(/^\/api\/computer\/codex\/threads\/([^/]+)\/adopt$/);
2734
+ const mirroredThreadAdoptMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/codex\/threads\/([^/]+)\/adopt$/);
2474
2735
  if (mirroredThreadAdoptMatch && method === 'POST') {
2475
2736
  const threadId = decodeURIComponent(mirroredThreadAdoptMatch[1] || '').trim();
2476
2737
  const parsed = await readJsonBody(req);
@@ -2487,7 +2748,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2487
2748
  writeJson(res, 200, { thread: serializeAdoptedMirroredThread(session) });
2488
2749
  return true;
2489
2750
  }
2490
- if (pathname === '/api/computer/codex/projects/open' && method === 'POST') {
2751
+ if (pathname === '/api/computers/{computer_id}/codex/projects/open' && method === 'POST') {
2491
2752
  try {
2492
2753
  const parsed = await readJsonBody(req);
2493
2754
  const projectPath = parsed && typeof parsed === 'object' && typeof parsed.path === 'string'
@@ -2514,12 +2775,12 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2514
2775
  }
2515
2776
  return true;
2516
2777
  }
2517
- if (pathname === '/api/computer/mcp/servers' && method === 'GET') {
2778
+ if (pathname === '/api/computers/{computer_id}/mcp/servers' && method === 'GET') {
2518
2779
  writeJson(res, 200, { servers: buildMcpSettingsResponse().servers });
2519
2780
  return true;
2520
2781
  }
2521
2782
  /** 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') {
2783
+ if (pathname === '/api/computers/{computer_id}/mcp/tool-catalog' && method === 'GET') {
2523
2784
  try {
2524
2785
  const force = url.searchParams.get('refresh') === '1' || url.searchParams.get('force') === '1';
2525
2786
  const mgr = await getMcpManager({ forceConfigRefresh: force });
@@ -2538,11 +2799,11 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2538
2799
  }
2539
2800
  return true;
2540
2801
  }
2541
- if (pathname === '/api/computer/mcp/settings' && method === 'GET') {
2802
+ if (pathname === '/api/computers/{computer_id}/mcp/settings' && method === 'GET') {
2542
2803
  writeJson(res, 200, buildMcpSettingsResponse());
2543
2804
  return true;
2544
2805
  }
2545
- if (pathname === '/api/computer/mcp/settings' && method === 'PUT') {
2806
+ if (pathname === '/api/computers/{computer_id}/mcp/settings' && method === 'PUT') {
2546
2807
  const parsed = await readJsonBody(req);
2547
2808
  const allowedKeys = new Set(listMcpEnvKeys());
2548
2809
  const payloadEnv = parsed && typeof parsed === 'object' && parsed.env && typeof parsed.env === 'object'
@@ -2563,21 +2824,8 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2563
2824
  writeJson(res, 200, buildMcpSettingsResponse());
2564
2825
  return true;
2565
2826
  }
2566
- if (pathname === '/api/computer' && method === 'GET') {
2567
- const cpus = os.cpus();
2568
- const memTotal = os.totalmem();
2569
- const memFree = os.freemem();
2827
+ if (pathname === '/api/computers/{computer_id}' && method === 'GET') {
2570
2828
  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 */ }
2581
2829
  writeJson(res, 200, {
2582
2830
  daemon_id: enroll.daemon_id,
2583
2831
  hostname: os.hostname(),
@@ -2602,31 +2850,17 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2602
2850
  queued_tasks: 0,
2603
2851
  total_slots: 0,
2604
2852
  },
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
- },
2853
+ os: collectOsTelemetryFields(),
2620
2854
  });
2621
2855
  return true;
2622
2856
  }
2623
2857
  // ── Enrollment ──
2624
- if (pathname === '/api/computer/enroll/status' && method === 'GET') {
2858
+ if (pathname === '/api/computers/{computer_id}/enroll/status' && method === 'GET') {
2625
2859
  const state = readEnrollmentState(DAEMON_HOSTNAME || undefined);
2626
2860
  writeJson(res, 200, state);
2627
2861
  return true;
2628
2862
  }
2629
- if (pathname === '/api/computer/enroll/verify' && method === 'POST') {
2863
+ if (pathname === '/api/computers/{computer_id}/enroll/verify' && method === 'POST') {
2630
2864
  const parsed = await readJsonBody(req);
2631
2865
  const result = verifyEnrollmentToken({
2632
2866
  token: parsed.token,
@@ -2636,12 +2870,12 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2636
2870
  writeJson(res, result.ok ? 200 : 401, result);
2637
2871
  return true;
2638
2872
  }
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.');
2873
+ if (pathname === '/api/computers/{computer_id}/trust' && method === 'POST') {
2874
+ 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
2875
  return true;
2642
2876
  }
2643
2877
  // Create enrollment token
2644
- if (pathname === '/api/computer/enroll/token' && method === 'POST') {
2878
+ if (pathname === '/api/computers/{computer_id}/enroll/token' && method === 'POST') {
2645
2879
  try {
2646
2880
  const parsed = await readJsonBody(req);
2647
2881
  const ttlMinutes = parsed?.ttl_minutes || DEFAULT_TOKEN_TTL_MINUTES;
@@ -2661,7 +2895,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2661
2895
  return true;
2662
2896
  }
2663
2897
  // Revoke enrollment
2664
- if (pathname === '/api/computer/enroll/revoke' && method === 'POST') {
2898
+ if (pathname === '/api/computers/{computer_id}/enroll/revoke' && method === 'POST') {
2665
2899
  try {
2666
2900
  setTrustState('revoked', { daemonId: DAEMON_HOSTNAME || undefined });
2667
2901
  writeJson(res, 200, { ok: true, trust_state: 'revoked' });