@supen-ai/cli 0.1.14 → 1.3.3

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.
@@ -1 +1 @@
1
- {"version":3,"file":"system.d.ts","sourceRoot":"","sources":["../../../src/http/routes/system.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AA+M1B,KAAK,sBAAsB,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,yBAAyB,EAAE,CAAC;CAC3C,CAAC;AAEF,KAAK,yBAAyB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,KAAK,oBAAoB,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAAC;AAEF,KAAK,mBAAmB,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAuVF,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,EAChB,QAAQ,SAAoC,GAC3C,MAAM,EAAE,CAuBV;AAuxBD,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,KAAK,SAAgC,EACrC,OAAO,GAAE;IAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0anD;AA0+BD,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CA+8BlB"}
1
+ {"version":3,"file":"system.d.ts","sourceRoot":"","sources":["../../../src/http/routes/system.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AA+M1B,KAAK,sBAAsB,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,yBAAyB,EAAE,CAAC;CAC3C,CAAC;AAEF,KAAK,yBAAyB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,KAAK,oBAAoB,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAAC;AAEF,KAAK,mBAAmB,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAuVF,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,EAChB,QAAQ,SAAoC,GAC3C,MAAM,EAAE,CAuBV;AAuxBD,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,KAAK,SAAgC,EACrC,OAAO,GAAE;IAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0anD;AAkrCD,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CA60BlB"}
@@ -1615,6 +1615,16 @@ function coerceSingleQueryParam(value) {
1615
1615
  return value[0] || null;
1616
1616
  return value || null;
1617
1617
  }
1618
+ function codexThreadsListRoute(pathname) {
1619
+ return /^\/api\/computers\/\{computer_id\}\/agents\/[^/]+\/codex\/threads$/.test(pathname);
1620
+ }
1621
+ function codexThreadActionRoute(pathname, action) {
1622
+ const suffix = action === 'messages' ? 'messages' : action;
1623
+ return pathname.match(new RegExp(`^/api/computers/\\{computer_id\\}/agents/[^/]+/codex/threads/([^/]+)/${suffix}$`));
1624
+ }
1625
+ function codexProjectsOpenRoute(pathname) {
1626
+ return /^\/api\/computers\/\{computer_id\}\/agents\/[^/]+\/codex\/projects\/open$/.test(pathname);
1627
+ }
1618
1628
  function collectSpaceLogEntries(filters = {}) {
1619
1629
  const spaceId = currentSpaceId();
1620
1630
  const limit = Math.max(1, Math.min(filters.limit || SPACE_LOG_EVENT_LIMIT, 500));
@@ -1764,12 +1774,17 @@ const MANAGED_CODING_CLI_INSTALL_COMMANDS = {
1764
1774
  codex: CODING_CLI_INSTALL_COMMANDS.codex,
1765
1775
  gemini: CODING_CLI_INSTALL_COMMANDS.gemini,
1766
1776
  };
1777
+ function resolveManagedCodingCliInstallCommand(cli, current) {
1778
+ if (cli === 'codex' && current.install_source === 'homebrew') {
1779
+ return {
1780
+ command: 'sh',
1781
+ args: ['-lc', 'brew upgrade codex || brew upgrade --cask codex || brew reinstall --cask codex'],
1782
+ };
1783
+ }
1784
+ return MANAGED_CODING_CLI_INSTALL_COMMANDS[cli];
1785
+ }
1767
1786
  const CODING_CLI_NAMES = ['codex', 'gemini', 'claude'];
1768
1787
  const DETECTABLE_SPACE_APPS = [
1769
- { id: 'codex', command: 'codex', managed: true },
1770
- { id: 'gemini', command: 'gemini', managed: true },
1771
- { id: 'claude', command: 'claude', managed: false },
1772
- { id: 'acpx', command: 'acpx', managed: false },
1773
1788
  { id: 'libreoffice', command: 'libreoffice', managed: false },
1774
1789
  { id: 'dotnet', command: 'dotnet', managed: false },
1775
1790
  { id: 'tiwater-docx', command: 'tiwater-docx', managed: false },
@@ -1975,7 +1990,7 @@ function inferCliInstallSource(name, executablePath, resolvedPath) {
1975
1990
  return { install_source: 'pnpm', update_supported: false };
1976
1991
  }
1977
1992
  if (normalized.includes('/homebrew/') || normalized.includes('/opt/homebrew/') || normalized.includes('/cellar/')) {
1978
- return { install_source: 'homebrew', update_supported: false };
1993
+ return { install_source: 'homebrew', update_supported: name === 'codex' };
1979
1994
  }
1980
1995
  if (name === 'codex') {
1981
1996
  const npmRoot = detectGlobalNpmRoot();
@@ -2053,15 +2068,47 @@ function detectCodexAuthStatus(installed) {
2053
2068
  ? email || accountLine || 'Authenticated'
2054
2069
  : null;
2055
2070
  let accountId = null;
2071
+ let tokenEmail = null;
2072
+ let tokenName = null;
2056
2073
  try {
2057
2074
  const parsed = JSON.parse(fs.readFileSync(path.join(resolveCodexHome(), 'auth.json'), 'utf8'));
2058
2075
  const tokens = parsed.tokens && typeof parsed.tokens === 'object' ? parsed.tokens : {};
2059
2076
  accountId = typeof tokens.account_id === 'string' && tokens.account_id.trim() ? tokens.account_id.trim() : null;
2077
+ const identity = codexIdentityClaimsFromIdToken(tokens.id_token);
2078
+ tokenEmail = identity.email;
2079
+ tokenName = identity.name;
2060
2080
  }
2061
2081
  catch {
2062
2082
  accountId = null;
2063
2083
  }
2064
- return { authenticated, summary, account_id: accountId };
2084
+ return {
2085
+ authenticated,
2086
+ summary: authenticated ? tokenEmail || email || tokenName || accountLine || 'Authenticated' : null,
2087
+ account_id: accountId,
2088
+ email: tokenEmail || email || null,
2089
+ name: tokenName,
2090
+ };
2091
+ }
2092
+ function codexIdentityClaimsFromIdToken(idToken) {
2093
+ if (typeof idToken !== 'string' || !idToken.trim())
2094
+ return { email: null, name: null };
2095
+ const payload = idToken.split('.')[1];
2096
+ if (!payload)
2097
+ return { email: null, name: null };
2098
+ try {
2099
+ const decoded = Buffer.from(payload.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8');
2100
+ const claims = JSON.parse(decoded);
2101
+ const email = typeof claims.email === 'string' && claims.email.trim()
2102
+ ? claims.email.trim()
2103
+ : null;
2104
+ const name = typeof claims.name === 'string' && claims.name.trim()
2105
+ ? claims.name.trim()
2106
+ : null;
2107
+ return { email, name };
2108
+ }
2109
+ catch {
2110
+ return { email: null, name: null };
2111
+ }
2065
2112
  }
2066
2113
  function readCodexDefaultModel() {
2067
2114
  const codexHome = resolveCodexHome();
@@ -2277,6 +2324,23 @@ function readCodingCliStatusPayload() {
2277
2324
  codex_connect: codexConnectSnapshot(),
2278
2325
  };
2279
2326
  }
2327
+ function readInstalledAppsPayload() {
2328
+ return {
2329
+ installed_apps: DETECTABLE_SPACE_APPS.map((app) => {
2330
+ const detected = detectCliInstalled(app.command);
2331
+ return {
2332
+ id: app.id,
2333
+ installed: detected.installed,
2334
+ version: detected.version,
2335
+ managed: app.managed,
2336
+ path: detected.path,
2337
+ resolved_path: detected.resolved_path,
2338
+ install_source: detected.install_source,
2339
+ update_supported: detected.update_supported,
2340
+ };
2341
+ }),
2342
+ };
2343
+ }
2280
2344
  function readCachedCodingCliStatusPayload(options) {
2281
2345
  startCodingCliStatusCache({
2282
2346
  build: () => ({
@@ -2286,6 +2350,111 @@ function readCachedCodingCliStatusPayload(options) {
2286
2350
  });
2287
2351
  return getCodingCliStatusResponse(options);
2288
2352
  }
2353
+ function mergeQuotaWindows(...groups) {
2354
+ const byLabel = new Map();
2355
+ for (const group of groups) {
2356
+ for (const window of group) {
2357
+ const key = window.label.trim().toLowerCase();
2358
+ if (!key)
2359
+ continue;
2360
+ byLabel.set(key, window);
2361
+ }
2362
+ }
2363
+ return Array.from(byLabel.values());
2364
+ }
2365
+ function resetAtFromCodexValue(value) {
2366
+ if (typeof value === 'string' && value.trim())
2367
+ return value.trim();
2368
+ if (typeof value === 'number' && Number.isFinite(value)) {
2369
+ const millis = value > 10_000_000_000 ? value : value * 1000;
2370
+ return new Date(millis).toISOString();
2371
+ }
2372
+ return null;
2373
+ }
2374
+ function quotaLabelForCodexWindow(key, record) {
2375
+ const duration = numberValue(record.windowDurationMins);
2376
+ if (duration === 300)
2377
+ return '5-hour limit';
2378
+ if (duration === 10080)
2379
+ return 'Weekly limit';
2380
+ return key === 'primary' ? 'Primary limit' : key === 'secondary' ? 'Secondary limit' : labelForQuotaWindow(key, record);
2381
+ }
2382
+ function quotaWindowFromCodexNestedRecord(key, value) {
2383
+ if (!value || typeof value !== 'object')
2384
+ return null;
2385
+ const record = value;
2386
+ const percent = numberValue(record.usedPercent ?? record.used_percent ?? record.percent);
2387
+ const reset_at = resetAtFromCodexValue(record.resetsAt ?? record.resetAt ?? record.reset_at ?? record.reset);
2388
+ if (percent === null && !reset_at)
2389
+ return null;
2390
+ return {
2391
+ label: quotaLabelForCodexWindow(key, record),
2392
+ used: null,
2393
+ limit: null,
2394
+ remaining: null,
2395
+ percent,
2396
+ reset_at,
2397
+ };
2398
+ }
2399
+ function normalizeCodexSubscriptionQuotaWindows(subscription) {
2400
+ const rateLimits = subscription.rate_limits && typeof subscription.rate_limits === 'object'
2401
+ ? subscription.rate_limits
2402
+ : null;
2403
+ if (!rateLimits)
2404
+ return [];
2405
+ return ['primary', 'secondary']
2406
+ .map((key) => quotaWindowFromCodexNestedRecord(key, rateLimits[key]))
2407
+ .filter((entry) => Boolean(entry));
2408
+ }
2409
+ function codexSubscriptionSummary(subscription) {
2410
+ const rateLimits = subscription.rate_limits && typeof subscription.rate_limits === 'object'
2411
+ ? subscription.rate_limits
2412
+ : {};
2413
+ const credits = rateLimits.credits && typeof rateLimits.credits === 'object'
2414
+ ? rateLimits.credits
2415
+ : {};
2416
+ return {
2417
+ plan_type: typeof rateLimits.planType === 'string' && rateLimits.planType.trim() ? rateLimits.planType.trim() : null,
2418
+ credits_balance: typeof credits.balance === 'string' && credits.balance.trim() ? credits.balance.trim() : null,
2419
+ };
2420
+ }
2421
+ async function readCodexAgentStatusPayload() {
2422
+ const status = readCachedCodingCliStatusPayload();
2423
+ const quotaStatus = readLatestSpaceQuotaStatus();
2424
+ try {
2425
+ const subscription = await readCodexSubscription();
2426
+ const subscriptionWindows = mergeQuotaWindows(normalizeCodexSubscriptionQuotaWindows(subscription), normalizeQuotaWindows(subscription.rate_limits), normalizeQuotaWindows(subscription.rate_limits_by_limit_id));
2427
+ return {
2428
+ ...status,
2429
+ subscription: {
2430
+ ok: true,
2431
+ fetched_at: subscription.fetched_at,
2432
+ payload: subscription,
2433
+ error: null,
2434
+ },
2435
+ subscription_summary: codexSubscriptionSummary(subscription),
2436
+ quota_status: quotaStatus,
2437
+ quota_windows: mergeQuotaWindows(quotaStatus.windows, subscriptionWindows),
2438
+ };
2439
+ }
2440
+ catch (err) {
2441
+ return {
2442
+ ...status,
2443
+ subscription: {
2444
+ ok: false,
2445
+ fetched_at: null,
2446
+ payload: null,
2447
+ error: err?.message || 'Unable to read Codex subscription details.',
2448
+ },
2449
+ subscription_summary: {
2450
+ plan_type: null,
2451
+ credits_balance: null,
2452
+ },
2453
+ quota_status: quotaStatus,
2454
+ quota_windows: quotaStatus.windows,
2455
+ };
2456
+ }
2457
+ }
2289
2458
  function readRuntimeModelStatusPayload() {
2290
2459
  const selected_cli = process.env.SUPEN_CODING_CLI || readConfigSummary().coding_cli || 'codex';
2291
2460
  const codexTransport = normalizeCodexTransport(process.env.SUPEN_CODEX_TRANSPORT) ||
@@ -2606,38 +2775,17 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2606
2775
  return true;
2607
2776
  }
2608
2777
  if (pathname === '/api/computers/{computer_id}/apps' && method === 'GET') {
2609
- writeJson(res, 200, readCachedCodingCliStatusPayload());
2610
- return true;
2611
- }
2612
- if (pathname === '/api/computers/{computer_id}/runtime-models' && method === 'GET') {
2613
- writeJson(res, 200, readRuntimeModelStatusPayload());
2778
+ writeJson(res, 200, readInstalledAppsPayload());
2614
2779
  return true;
2615
2780
  }
2616
- if (pathname === '/api/computers/{computer_id}/runtime-models/default' && method === 'PUT') {
2617
- try {
2618
- const parsed = await readJsonBody(req);
2619
- const model = typeof parsed === 'object' && parsed && typeof parsed.model === 'string'
2620
- ? String(parsed.model).trim()
2621
- : '';
2622
- if (!model) {
2623
- writeProtocolError(res, 400, 'validation_error', 'missing_model', 'Request body must include a non-empty "model" string.');
2624
- return true;
2625
- }
2626
- writeCodexDefaultModel(model);
2627
- writeJson(res, 200, {
2628
- ok: true,
2629
- status: readRuntimeModelStatusPayload(),
2630
- });
2631
- }
2632
- catch (err) {
2633
- writeProtocolError(res, 500, 'config_error', 'codex_model_default_failed', err?.message || 'Failed to set Codex default model');
2634
- }
2781
+ if (pathname === '/api/computers/{computer_id}/agents/codex' && method === 'GET') {
2782
+ writeJson(res, 200, await readCodexAgentStatusPayload());
2635
2783
  return true;
2636
2784
  }
2637
- const spaceCodexTransportDefaultMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/apps\/codex\/transports\/([^/]+)\/default$/);
2638
- if (spaceCodexTransportDefaultMatch && method === 'PUT') {
2785
+ const codexTransportDefaultMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/agents\/codex\/transports\/([^/]+)\/default$/);
2786
+ if (codexTransportDefaultMatch && method === 'PUT') {
2639
2787
  try {
2640
- const transport = normalizeCodexTransport(decodeURIComponent(spaceCodexTransportDefaultMatch[1] || '').trim());
2788
+ const transport = normalizeCodexTransport(decodeURIComponent(codexTransportDefaultMatch[1] || '').trim());
2641
2789
  if (!transport) {
2642
2790
  writeProtocolError(res, 400, 'validation_error', 'invalid_codex_transport', 'transport must be one of: app-server, exec, acpx');
2643
2791
  return true;
@@ -2654,15 +2802,9 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2654
2802
  }
2655
2803
  return true;
2656
2804
  }
2657
- const spaceAppDefaultMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/apps\/([^/]+)\/default$/);
2658
- if (spaceAppDefaultMatch && method === 'PUT') {
2805
+ if (pathname === '/api/computers/{computer_id}/agents/codex/default' && method === 'PUT') {
2659
2806
  try {
2660
- const cli = normalizeCliName(decodeURIComponent(spaceAppDefaultMatch[1] || '').trim());
2661
- if (!cli) {
2662
- writeProtocolError(res, 400, 'validation_error', 'invalid_cli', 'cli must be one of: codex, gemini, claude');
2663
- return true;
2664
- }
2665
- const result = setDefaultCodingCli(cli);
2807
+ const result = setDefaultCodingCli('codex');
2666
2808
  writeJson(res, 200, {
2667
2809
  ok: true,
2668
2810
  ...result,
@@ -2670,28 +2812,19 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2670
2812
  });
2671
2813
  }
2672
2814
  catch (err) {
2673
- writeProtocolError(res, 500, 'config_error', 'coding_cli_default_failed', err?.message || 'Failed to set default coding CLI');
2815
+ writeProtocolError(res, 500, 'config_error', 'coding_agent_default_failed', err?.message || 'Failed to set default coding agent');
2674
2816
  }
2675
2817
  return true;
2676
2818
  }
2677
- const spaceAppInstallMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/apps\/([^/]+)\/install$/);
2678
- if (spaceAppInstallMatch && method === 'POST') {
2819
+ if (pathname === '/api/computers/{computer_id}/agents/codex/update' && method === 'POST') {
2679
2820
  try {
2680
- const cli = normalizeCliName(decodeURIComponent(spaceAppInstallMatch[1] || '').trim());
2681
- if (!cli) {
2682
- writeProtocolError(res, 400, 'validation_error', 'invalid_cli', 'cli must be one of: codex, gemini, claude');
2683
- return true;
2684
- }
2685
- if (!isManagedCliName(cli)) {
2686
- writeProtocolError(res, 400, 'validation_error', 'install_not_supported', 'Only codex and gemini support managed install at the moment');
2687
- return true;
2688
- }
2821
+ const cli = 'codex';
2689
2822
  const current = detectCliInstalled(cli);
2690
2823
  if (current.installed && !current.update_supported) {
2691
2824
  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
2825
  return true;
2693
2826
  }
2694
- const installSpec = MANAGED_CODING_CLI_INSTALL_COMMANDS[cli];
2827
+ const installSpec = resolveManagedCodingCliInstallCommand(cli, current);
2695
2828
  const result = spawnSync(installSpec.command, installSpec.args, {
2696
2829
  encoding: 'utf8',
2697
2830
  timeout: 120_000,
@@ -2749,13 +2882,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2749
2882
  }
2750
2883
  return true;
2751
2884
  }
2752
- const spaceAppConnectMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/apps\/([^/]+)\/connect$/);
2753
- if (spaceAppConnectMatch && method === 'POST') {
2754
- const cli = normalizeCliName(decodeURIComponent(spaceAppConnectMatch[1] || '').trim());
2755
- if (cli !== 'codex') {
2756
- writeProtocolError(res, 400, 'validation_error', 'connect_not_supported', 'Only codex supports connect flow at the moment');
2757
- return true;
2758
- }
2885
+ if (pathname === '/api/computers/{computer_id}/agents/codex/connect' && method === 'POST') {
2759
2886
  const status = readCachedCodingCliStatusPayload();
2760
2887
  const codex = status.clis.find((entry) => entry.name === 'codex');
2761
2888
  if (!codex?.installed) {
@@ -2769,12 +2896,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2769
2896
  });
2770
2897
  return true;
2771
2898
  }
2772
- if (spaceAppConnectMatch && method === 'DELETE') {
2773
- const cli = normalizeCliName(decodeURIComponent(spaceAppConnectMatch[1] || '').trim());
2774
- if (cli !== 'codex') {
2775
- writeProtocolError(res, 400, 'validation_error', 'connect_not_supported', 'Only codex supports connect flow at the moment');
2776
- return true;
2777
- }
2899
+ if (pathname === '/api/computers/{computer_id}/agents/codex/connect' && method === 'DELETE') {
2778
2900
  writeJson(res, 200, {
2779
2901
  ok: true,
2780
2902
  codex_connect: cancelCodexConnectFlow(),
@@ -2782,13 +2904,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2782
2904
  });
2783
2905
  return true;
2784
2906
  }
2785
- const spaceAppSessionMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/apps\/([^/]+)\/session$/);
2786
- if (spaceAppSessionMatch && method === 'DELETE') {
2787
- const cli = normalizeCliName(decodeURIComponent(spaceAppSessionMatch[1] || '').trim());
2788
- if (cli !== 'codex') {
2789
- writeProtocolError(res, 400, 'validation_error', 'session_not_supported', 'Only codex supports session connect/disconnect at the moment');
2790
- return true;
2791
- }
2907
+ if (pathname === '/api/computers/{computer_id}/agents/codex/session' && method === 'DELETE') {
2792
2908
  const cmd = spawnSync('codex', ['logout'], {
2793
2909
  encoding: 'utf8',
2794
2910
  timeout: 15_000,
@@ -2824,28 +2940,15 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2824
2940
  writeJson(res, 200, buildHubSnapshotForSpace(spaceId));
2825
2941
  return true;
2826
2942
  }
2827
- if (pathname === '/api/computers/{computer_id}/usage' && method === 'GET') {
2943
+ if (pathname === '/api/computers/{computer_id}/agents/codex/usage' && method === 'GET') {
2828
2944
  writeJson(res, 200, getGlobalUsage());
2829
2945
  return true;
2830
2946
  }
2831
- if (pathname === '/api/computers/{computer_id}/usage/daily' && method === 'GET') {
2947
+ if (pathname === '/api/computers/{computer_id}/agents/codex/usage/daily' && method === 'GET') {
2832
2948
  writeJson(res, 200, { daily: getDailyUsage() });
2833
2949
  return true;
2834
2950
  }
2835
- if (pathname === '/api/computers/{computer_id}/codex/subscription' && method === 'GET') {
2836
- try {
2837
- writeJson(res, 200, await readCodexSubscription());
2838
- }
2839
- catch (err) {
2840
- writeProtocolError(res, 503, 'service_unavailable', 'codex_subscription_unavailable', err?.message || 'Unable to read Codex subscription details.');
2841
- }
2842
- return true;
2843
- }
2844
- if (pathname === '/api/computers/{computer_id}/quota-status' && method === 'GET') {
2845
- writeJson(res, 200, readLatestSpaceQuotaStatus());
2846
- return true;
2847
- }
2848
- if (pathname === '/api/computers/{computer_id}/codex/threads' && method === 'GET') {
2951
+ if (codexThreadsListRoute(pathname) && method === 'GET') {
2849
2952
  const limitRaw = coerceSingleQueryParam(url.searchParams.get('limit'));
2850
2953
  const limit = limitRaw ? Number.parseInt(limitRaw, 10) : MIRRORED_THREAD_LIMIT;
2851
2954
  writeJson(res, 200, readMirroredTaskProjects(Number.isFinite(limit) ? limit : MIRRORED_THREAD_LIMIT));
@@ -2865,7 +2968,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2865
2968
  serveRemoteFile(req, res, filePath);
2866
2969
  return true;
2867
2970
  }
2868
- const mirroredThreadArchiveMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/codex\/threads\/([^/]+)\/archive$/);
2971
+ const mirroredThreadArchiveMatch = codexThreadActionRoute(pathname, 'archive');
2869
2972
  if (mirroredThreadArchiveMatch && method === 'POST') {
2870
2973
  const threadId = decodeURIComponent(mirroredThreadArchiveMatch[1] || '').trim();
2871
2974
  if (!threadId) {
@@ -2879,7 +2982,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2879
2982
  writeJson(res, 200, { ok: true, archived: true });
2880
2983
  return true;
2881
2984
  }
2882
- const mirroredThreadStreamMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/codex\/threads\/([^/]+)\/stream$/);
2985
+ const mirroredThreadStreamMatch = codexThreadActionRoute(pathname, 'stream');
2883
2986
  if (mirroredThreadStreamMatch && method === 'GET') {
2884
2987
  const threadId = decodeURIComponent(mirroredThreadStreamMatch[1] || '').trim();
2885
2988
  if (!threadId) {
@@ -2922,7 +3025,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2922
3025
  res.flush?.();
2923
3026
  return true;
2924
3027
  }
2925
- const mirroredThreadHistoryMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/codex\/threads\/([^/]+)\/messages$/);
3028
+ const mirroredThreadHistoryMatch = codexThreadActionRoute(pathname, 'messages');
2926
3029
  if (mirroredThreadHistoryMatch && method === 'GET') {
2927
3030
  const threadId = decodeURIComponent(mirroredThreadHistoryMatch[1] || '').trim();
2928
3031
  const limitRaw = coerceSingleQueryParam(url.searchParams.get('limit'));
@@ -2937,7 +3040,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2937
3040
  writeJson(res, 200, history);
2938
3041
  return true;
2939
3042
  }
2940
- const mirroredThreadAdoptMatch = pathname.match(/^\/api\/computers\/\{computer_id\}\/codex\/threads\/([^/]+)\/adopt$/);
3043
+ const mirroredThreadAdoptMatch = codexThreadActionRoute(pathname, 'adopt');
2941
3044
  if (mirroredThreadAdoptMatch && method === 'POST') {
2942
3045
  const threadId = decodeURIComponent(mirroredThreadAdoptMatch[1] || '').trim();
2943
3046
  const parsed = await readJsonBody(req);
@@ -2954,7 +3057,7 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
2954
3057
  writeJson(res, 200, { thread: serializeAdoptedMirroredThread(session) });
2955
3058
  return true;
2956
3059
  }
2957
- if (pathname === '/api/computers/{computer_id}/codex/projects/open' && method === 'POST') {
3060
+ if (codexProjectsOpenRoute(pathname) && method === 'POST') {
2958
3061
  try {
2959
3062
  const parsed = await readJsonBody(req);
2960
3063
  const projectPath = parsed && typeof parsed === 'object' && typeof parsed.path === 'string'