@supen-ai/cli 1.4.7 → 1.4.9

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.
@@ -6,12 +6,13 @@ import { createRequire } from 'module';
6
6
  import { spawn, spawnSync } from 'child_process';
7
7
  import { DAEMON_HOSTNAME, DEFAULT_MODEL, MODELS_REGISTRY, SUPEN_HOME, reloadModelsRegistry, } from '../../core/config.js';
8
8
  import { readConfigSummary, readConfigYamlFile, writeConfigYamlFile, } from '../../core/env.js';
9
- import { readSpaceEnvMap, updateSpaceEnvMap, withRuntimeSpaceEnv, withSupenLlmEnv } from '../../core/space-env.js';
9
+ import { readSpaceEnvMap, updateSpaceEnvMap, withSupenLlmEnv } from '../../core/space-env.js';
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
13
  import { collectOsTelemetryFields } from '../../core/os-info.js';
14
14
  import { getCodingCliStatusResponse, startCodingCliStatusCache, } from '../../core/coding-cli-status-cache.js';
15
+ import { buildToolLookupEnv as buildSharedToolLookupEnv, resolveToolExecutablePath, } from '../../core/tool-lookup.js';
15
16
  import { getGatewayInstance, getLlmToken } from '../../core/gateway.js';
16
17
  import { normalizeGatewayUplinkUrl, readGatewayConfig, writeGatewayConfig, } from '../../core/gateway-config.js';
17
18
  import { ensureThread, getAllAgents, getDailyUsage, getGlobalUsage, getThreadsForAgent, getThreadUiEvents, updateThreadBackendDriverId, updateThreadSdkSessionId, } from '../../core/store.js';
@@ -21,10 +22,10 @@ import { getMcpManager } from '../../mcp/index.js';
21
22
  import { logger } from '../../core/logger.js';
22
23
  import { buildDaemonOpenApiSpec } from '../../channels/http-routes.js';
23
24
  import { writeJson, writeProtocolError, readJsonBody } from '../response.js';
24
- import { listRecentThreadEventsAfter, readThreadEventLogHead, } from '../../core/thread-event-log.js';
25
+ import { appendThreadEvent, listRecentThreadEventsAfter, readThreadEventLogHead, } from '../../core/thread-event-log.js';
25
26
  import { projectThreadRuntimeState } from '../../core/thread-runtime-state.js';
26
27
  import { attachThreadContextToChunk, readThreadContext, } from '../../core/thread-context.js';
27
- import { addThreadStreamClient, removeThreadStreamClient, writeThreadStreamEvent } from '../thread-stream.js';
28
+ import { addThreadStreamClient, broadcastToThread, removeThreadStreamClient, writeThreadStreamEvent } from '../thread-stream.js';
28
29
  const DEFAULT_TOKEN_TTL_MINUTES = 20;
29
30
  const SPACE_LOG_EVENT_LIMIT = 200;
30
31
  const SPACE_LOG_STREAM_POLL_MS = 1000;
@@ -881,141 +882,6 @@ function readThreadWorkspacePath(filePath) {
881
882
  }
882
883
  return null;
883
884
  }
884
- const CODEX_THREAD_OBSERVER_STDOUT_MAX_BYTES = 4 * 1024 * 1024;
885
- function startCodexThreadObserver(input) {
886
- const codexPath = resolveCliExecutablePath('codex');
887
- if (!codexPath) {
888
- input.write({
889
- type: 'error',
890
- message: 'codex executable was not found in the daemon tool lookup PATH',
891
- });
892
- return () => { };
893
- }
894
- const child = spawn(codexPath, ['app-server', '--listen', 'stdio://'], {
895
- cwd: input.cwd,
896
- stdio: ['pipe', 'pipe', 'pipe'],
897
- env: withRuntimeSpaceEnv(buildToolLookupEnv()),
898
- });
899
- let buffer = '';
900
- let nextId = 1;
901
- let observerEventIndex = 0;
902
- let initialized = false;
903
- let closed = false;
904
- child.on('error', (err) => {
905
- if (closed)
906
- return;
907
- input.write({
908
- type: 'error',
909
- message: err instanceof Error ? err.message : String(err),
910
- });
911
- });
912
- child.on('close', () => {
913
- closed = true;
914
- });
915
- child.stdin.on('error', (error) => {
916
- closed = true;
917
- if (error?.code === 'EPIPE' || error?.code === 'ECONNRESET')
918
- return;
919
- logger.debug({ err: error, threadId: input.threadId }, 'Codex thread observer stdin failed');
920
- });
921
- const closeObserver = (signal = 'SIGTERM') => {
922
- if (closed)
923
- return;
924
- closed = true;
925
- try {
926
- child.stdin.destroy();
927
- child.kill(signal);
928
- }
929
- catch {
930
- // Observer cleanup is best-effort; the HTTP stream is already closing.
931
- }
932
- };
933
- const send = (message) => {
934
- if (closed || child.stdin.destroyed || !child.stdin.writable)
935
- return;
936
- try {
937
- child.stdin.write(`${JSON.stringify(message)}\n`);
938
- }
939
- catch (error) {
940
- closed = true;
941
- const code = error?.code;
942
- if (code === 'EPIPE' || code === 'ECONNRESET')
943
- return;
944
- logger.debug({ err: error, threadId: input.threadId }, 'Codex thread observer write failed');
945
- }
946
- };
947
- const request = (method, params) => {
948
- send({ id: nextId++, method, ...(params ? { params } : {}) });
949
- };
950
- child.stdout.on('data', (chunk) => {
951
- if (closed)
952
- return;
953
- buffer += String(chunk);
954
- if (buffer.length > CODEX_THREAD_OBSERVER_STDOUT_MAX_BYTES) {
955
- logger.debug({ threadId: input.threadId, bytes: buffer.length }, 'Codex thread observer stdout exceeded line buffer limit');
956
- closeObserver('SIGKILL');
957
- return;
958
- }
959
- while (true) {
960
- const newlineIndex = buffer.indexOf('\n');
961
- if (newlineIndex < 0)
962
- break;
963
- const line = buffer.slice(0, newlineIndex).trim();
964
- buffer = buffer.slice(newlineIndex + 1);
965
- if (!line.startsWith('{'))
966
- continue;
967
- let parsed;
968
- try {
969
- parsed = JSON.parse(line);
970
- }
971
- catch {
972
- continue;
973
- }
974
- if (parsed.id !== undefined && typeof parsed.method === 'string') {
975
- send({ id: parsed.id, result: {} });
976
- continue;
977
- }
978
- if (parsed.id !== undefined) {
979
- if (!initialized && parsed.id === 1) {
980
- initialized = true;
981
- send({ method: 'initialized' });
982
- request('thread/resume', {
983
- threadId: input.threadId,
984
- cwd: input.cwd,
985
- experimentalRawEvents: true,
986
- });
987
- }
988
- continue;
989
- }
990
- const method = typeof parsed.method === 'string' ? parsed.method : '';
991
- if (!method)
992
- continue;
993
- const params = parsed.params && typeof parsed.params === 'object'
994
- ? parsed.params
995
- : {};
996
- const eventThreadId = typeof params.threadId === 'string' ? params.threadId : '';
997
- if (eventThreadId !== input.threadId)
998
- continue;
999
- observerEventIndex += 1;
1000
- input.write(codexAppServerEventChunk(method, params), `observer-${Date.now()}-${observerEventIndex}`);
1001
- }
1002
- });
1003
- child.stderr.on('data', () => { });
1004
- child.once('error', (error) => {
1005
- logger.debug({ err: error, threadId: input.threadId }, 'Codex thread observer failed');
1006
- });
1007
- request('initialize', {
1008
- clientInfo: {
1009
- name: 'supen-daemon-thread-observer',
1010
- title: 'Supen',
1011
- version: '0.1.0',
1012
- },
1013
- capabilities: { experimentalApi: true },
1014
- });
1015
- return () => {
1016
- closeObserver();
1017
- };
1018
- }
1019
885
  function normalizeThreadStatusToken(value) {
1020
886
  return typeof value === 'string'
1021
887
  ? value.trim().toLowerCase().replace(/[-_\s]/g, '')
@@ -1464,6 +1330,23 @@ function isMirroredGoalContextMessage(text) {
1464
1330
  const trimmed = text.trim();
1465
1331
  return trimmed.startsWith('<goal_context>') && trimmed.endsWith('</goal_context>');
1466
1332
  }
1333
+ function threadGoalEventAction(event) {
1334
+ const raw = event.raw_payload && typeof event.raw_payload === 'object'
1335
+ ? event.raw_payload
1336
+ : {};
1337
+ const method = typeof raw.method === 'string' ? raw.method : '';
1338
+ const params = raw.params && typeof raw.params === 'object' ? raw.params : {};
1339
+ if (method === 'thread/goal/cleared')
1340
+ return 'cleared';
1341
+ if (method !== 'thread/goal/updated')
1342
+ return null;
1343
+ const status = typeof params.status === 'string' ? params.status.trim().toLowerCase() : '';
1344
+ if (status === 'paused')
1345
+ return 'paused';
1346
+ if (status === 'active' || status === 'resumed')
1347
+ return 'active';
1348
+ return null;
1349
+ }
1467
1350
  function mirroredThreadWorkspacePath(threadPath, stateEntry) {
1468
1351
  const stateCwd = typeof stateEntry?.cwd === 'string' ? stateEntry.cwd : null;
1469
1352
  const threadWorkspace = readThreadWorkspacePath(threadPath);
@@ -1736,6 +1619,22 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1736
1619
  chunk,
1737
1620
  });
1738
1621
  }
1622
+ for (const event of usableEventLogEvents) {
1623
+ const action = threadGoalEventAction(event);
1624
+ if (!action)
1625
+ continue;
1626
+ const eventTimestampMs = Date.parse(event.received_at);
1627
+ const goalTimestampMs = recentGoalContext ? Date.parse(recentGoalContext.timestamp) : 0;
1628
+ if (recentGoalContext && Number.isFinite(eventTimestampMs) && Number.isFinite(goalTimestampMs) && eventTimestampMs < goalTimestampMs) {
1629
+ continue;
1630
+ }
1631
+ if (action === 'cleared') {
1632
+ recentGoalContext = null;
1633
+ }
1634
+ else if (recentGoalContext) {
1635
+ recentGoalContext = { ...recentGoalContext, status: action };
1636
+ }
1637
+ }
1739
1638
  const collapsedMessages = collapseCompletedAssistantProgressMessages(messages);
1740
1639
  const slicedMessages = collapsedMessages.slice(-safeLimit);
1741
1640
  const slicedEvents = events.slice(-safeLimit * 8);
@@ -1990,13 +1889,6 @@ const DETECTABLE_SPACE_APPS = [
1990
1889
  { id: 'gh', command: 'gh', managed: false },
1991
1890
  { id: 'git', command: 'git', managed: false },
1992
1891
  ];
1993
- const TOOL_LOOKUP_PATH_ENTRIES = [
1994
- '/opt/homebrew/bin',
1995
- '/usr/local/bin',
1996
- '/opt/homebrew/sbin',
1997
- '/usr/local/sbin',
1998
- '/Applications/Codex.app/Contents/Resources',
1999
- ];
2000
1892
  const codexConnectRuntime = {
2001
1893
  state: 'idle',
2002
1894
  updatedAt: null,
@@ -2227,25 +2119,10 @@ function inferNpmPackagePrefix(packageName, candidatePath) {
2227
2119
  : beforeNodeModules;
2228
2120
  }
2229
2121
  function buildToolLookupEnv() {
2230
- const existingPath = process.env.PATH || '';
2231
- const entries = [
2232
- ...TOOL_LOOKUP_PATH_ENTRIES,
2233
- ...existingPath.split(path.delimiter),
2234
- ].filter((entry) => entry.trim().length > 0);
2235
- return {
2236
- ...process.env,
2237
- PATH: Array.from(new Set(entries)).join(path.delimiter),
2238
- };
2122
+ return buildSharedToolLookupEnv();
2239
2123
  }
2240
2124
  function resolveCliExecutablePath(name) {
2241
- const detected = spawnSync('sh', ['-c', `command -v ${name}`], {
2242
- encoding: 'utf8',
2243
- timeout: 3000,
2244
- env: buildToolLookupEnv(),
2245
- });
2246
- if (detected.status !== 0)
2247
- return null;
2248
- return String(detected.stdout || '').split(/\r?\n/g).find(Boolean)?.trim() || null;
2125
+ return resolveToolExecutablePath(name);
2249
2126
  }
2250
2127
  function detectCliInstalled(name) {
2251
2128
  const executablePath = resolveCliExecutablePath(name);
@@ -3223,7 +3100,6 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
3223
3100
  const workspacePath = readThreadWorkspacePath(threadPath) || process.cwd();
3224
3101
  const withThreadContext = (chunk) => attachThreadContextToChunk(chunk, readThreadContext({ threadId, workspace: workspacePath }));
3225
3102
  let streamClosed = false;
3226
- let stopCodexObserver = () => { };
3227
3103
  let pingTimer = null;
3228
3104
  let jsonlPollTimer = null;
3229
3105
  const cleanup = () => {
@@ -3234,22 +3110,8 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
3234
3110
  clearInterval(pingTimer);
3235
3111
  if (jsonlPollTimer)
3236
3112
  clearInterval(jsonlPollTimer);
3237
- stopCodexObserver();
3238
3113
  removeThreadStreamClient(threadId, res);
3239
3114
  };
3240
- stopCodexObserver = process.env.NODE_ENV === 'test'
3241
- ? () => { }
3242
- : startCodexThreadObserver({
3243
- threadId,
3244
- cwd: workspacePath,
3245
- write: (chunk, id) => {
3246
- if (streamClosed)
3247
- return;
3248
- const wrote = writeThreadStreamEvent(res, withThreadContext(chunk), id ? { id } : {});
3249
- if (!wrote)
3250
- cleanup();
3251
- },
3252
- });
3253
3115
  pingTimer = setInterval(() => {
3254
3116
  if (!writeSse(res)) {
3255
3117
  cleanup();
@@ -3344,6 +3206,52 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
3344
3206
  writeJson(res, 200, { thread: serializeAdoptedMirroredThread(thread) });
3345
3207
  return true;
3346
3208
  }
3209
+ const mirroredThreadGoalMatch = codexThreadActionRoute(pathname, 'goal');
3210
+ if (mirroredThreadGoalMatch && (method === 'PATCH' || method === 'DELETE')) {
3211
+ const threadId = decodeURIComponent(mirroredThreadGoalMatch[1] || '').trim();
3212
+ if (!threadId || !codexThreadFiles().has(threadId)) {
3213
+ writeProtocolError(res, 404, 'not_found', 'mirrored_thread_not_found', 'Mirrored task history was not found.');
3214
+ return true;
3215
+ }
3216
+ let status = null;
3217
+ if (method === 'PATCH') {
3218
+ const parsed = await readJsonBody(req);
3219
+ const rawStatus = parsed && typeof parsed === 'object' && typeof parsed.status === 'string'
3220
+ ? String(parsed.status).trim().toLowerCase()
3221
+ : '';
3222
+ if (rawStatus === 'active' || rawStatus === 'resumed')
3223
+ status = 'active';
3224
+ if (rawStatus === 'paused')
3225
+ status = 'paused';
3226
+ if (!status) {
3227
+ writeProtocolError(res, 400, 'validation_error', 'invalid_goal_status', 'Expected goal status "active" or "paused".');
3228
+ return true;
3229
+ }
3230
+ }
3231
+ const now = new Date().toISOString();
3232
+ const rawPayload = method === 'DELETE'
3233
+ ? { method: 'thread/goal/cleared', params: { threadId, source: 'supen', timestamp: now } }
3234
+ : { method: 'thread/goal/updated', params: { threadId, status, source: 'supen', timestamp: now } };
3235
+ const event = appendThreadEvent({
3236
+ threadId,
3237
+ runtimeThreadId: threadId,
3238
+ source: 'web',
3239
+ eventType: method === 'DELETE' ? 'thread_goal_cleared' : 'thread_goal_updated',
3240
+ rawPayload,
3241
+ receivedAt: now,
3242
+ });
3243
+ const chunk = threadStreamChunkForEvent(event);
3244
+ if (chunk) {
3245
+ broadcastToThread(threadId, attachThreadContextToChunk(chunk, readThreadContext({ threadId })), { id: event.sequence });
3246
+ }
3247
+ writeJson(res, 200, {
3248
+ ok: true,
3249
+ thread_id: threadId,
3250
+ goal: method === 'DELETE' ? null : { status },
3251
+ event_log_head: event.sequence,
3252
+ });
3253
+ return true;
3254
+ }
3347
3255
  if (codexProjectsOpenRoute(pathname) && method === 'POST') {
3348
3256
  try {
3349
3257
  const parsed = await readJsonBody(req);