@axhub/genie 0.2.10 → 0.2.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 (96) hide show
  1. package/dist/api-docs.html +2 -2
  2. package/dist/assets/App-Clb2COtW.js +274 -0
  3. package/dist/assets/ImagePlaygroundPage-DqhMSbM8.js +106 -0
  4. package/dist/assets/ImagePlaygroundPage-MEn3NN80.css +1 -0
  5. package/dist/assets/ReviewApp-CDcLYe-u.js +1 -0
  6. package/dist/assets/{_basePickBy-DVVb07UV.js → _basePickBy-jUZsM51q.js} +1 -1
  7. package/dist/assets/{_baseUniq-BtbziL5G.js → _baseUniq-BXglE6_v.js} +1 -1
  8. package/dist/assets/{arc-BsCC8yBD.js → arc-D-oFCFBv.js} +1 -1
  9. package/dist/assets/{architectureDiagram-2XIMDMQ5-woFp6eNI.js → architectureDiagram-2XIMDMQ5-DC8bAnQt.js} +1 -1
  10. package/dist/assets/{blockDiagram-WCTKOSBZ-ya8VAc2k.js → blockDiagram-WCTKOSBZ-C4semIRc.js} +1 -1
  11. package/dist/assets/{c4Diagram-IC4MRINW-CY1dZmIZ.js → c4Diagram-IC4MRINW-FHj1QO3y.js} +1 -1
  12. package/dist/assets/channel-BF4woPXX.js +1 -0
  13. package/dist/assets/{chunk-4BX2VUAB-CR1lAd74.js → chunk-4BX2VUAB-D-LjsQ_s.js} +1 -1
  14. package/dist/assets/{chunk-55IACEB6-CP98WcFC.js → chunk-55IACEB6-DI3j_d7A.js} +1 -1
  15. package/dist/assets/{chunk-FMBD7UC4-D9c7ijAB.js → chunk-FMBD7UC4-BEVnaLFN.js} +1 -1
  16. package/dist/assets/{chunk-JSJVCQXG-DQAGYOn-.js → chunk-JSJVCQXG-CSxpcErk.js} +1 -1
  17. package/dist/assets/{chunk-KX2RTZJC-BbTXiDq7.js → chunk-KX2RTZJC-BbuhDN4h.js} +1 -1
  18. package/dist/assets/{chunk-NQ4KR5QH-BI6AX0dr.js → chunk-NQ4KR5QH-C3x61XQa.js} +1 -1
  19. package/dist/assets/{chunk-QZHKN3VN-DB3V2Ifo.js → chunk-QZHKN3VN-DxWOFtPh.js} +1 -1
  20. package/dist/assets/{chunk-WL4C6EOR-DhzTthv6.js → chunk-WL4C6EOR-Bt2OauD2.js} +1 -1
  21. package/dist/assets/classDiagram-VBA2DB6C-D2kHlnQ7.js +1 -0
  22. package/dist/assets/classDiagram-v2-RAHNMMFH-D2kHlnQ7.js +1 -0
  23. package/dist/assets/clone-CqBvwCJW.js +1 -0
  24. package/dist/assets/{cose-bilkent-S5V4N54A-BQ09ZE2j.js → cose-bilkent-S5V4N54A-Dexadrue.js} +1 -1
  25. package/dist/assets/{dagre-KLK3FWXG-Dc2ueD_R.js → dagre-KLK3FWXG-F9U4X2xC.js} +1 -1
  26. package/dist/assets/{diagram-E7M64L7V-DP-LsQoL.js → diagram-E7M64L7V-B3V17aH3.js} +1 -1
  27. package/dist/assets/{diagram-IFDJBPK2-Cg6r42cB.js → diagram-IFDJBPK2-CdHAmLL1.js} +1 -1
  28. package/dist/assets/{diagram-P4PSJMXO-aHsfoUZE.js → diagram-P4PSJMXO-CrTNfk8K.js} +1 -1
  29. package/dist/assets/{erDiagram-INFDFZHY-qBXJ4aAz.js → erDiagram-INFDFZHY-vDh9SWK9.js} +1 -1
  30. package/dist/assets/{flowDiagram-PKNHOUZH-D_13emJM.js → flowDiagram-PKNHOUZH-DpltMg7L.js} +1 -1
  31. package/dist/assets/{ganttDiagram-A5KZAMGK-BvIcOLwz.js → ganttDiagram-A5KZAMGK-COTk2xur.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-ad0vvNcU.js → gitGraphDiagram-K3NZZRJ6-BNV7bvvj.js} +1 -1
  33. package/dist/assets/{graph-CeJCMjan.js → graph-Dkeg9oys.js} +1 -1
  34. package/dist/assets/{highlighted-body-TPN3WLV5-B_novwSz.js → highlighted-body-TPN3WLV5-DaiQEBwR.js} +1 -1
  35. package/dist/assets/index-DgGmiqsP.css +1 -0
  36. package/dist/assets/index-DvA901Vs.js +2 -0
  37. package/dist/assets/{infoDiagram-LFFYTUFH-lOxAqb3m.js → infoDiagram-LFFYTUFH-CZioW3Gt.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-PHBUUO56-DIr-51gj.js → ishikawaDiagram-PHBUUO56-BbqR3i1B.js} +1 -1
  39. package/dist/assets/{journeyDiagram-4ABVD52K-CYcIW0ZU.js → journeyDiagram-4ABVD52K-wfb-WHzl.js} +1 -1
  40. package/dist/assets/{kanban-definition-K7BYSVSG-C1ZK616a.js → kanban-definition-K7BYSVSG-B3c4y3VN.js} +1 -1
  41. package/dist/assets/{layout-CI2RM-v6.js → layout-Xr9Z2VGF.js} +1 -1
  42. package/dist/assets/{linear-DE7bISck.js → linear-JBmzAJtl.js} +1 -1
  43. package/dist/assets/{mermaid-O7DHMXV3-XxAJo8EK.js → mermaid-O7DHMXV3-fDuyNLKe.js} +238 -220
  44. package/dist/assets/{mindmap-definition-YRQLILUH-Dz6EFjmn.js → mindmap-definition-YRQLILUH-B5NTN_jD.js} +1 -1
  45. package/dist/assets/{pieDiagram-SKSYHLDU-DPpEzUed.js → pieDiagram-SKSYHLDU-CuO98GVu.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-337W2JSQ-xdoXNet7.js → quadrantDiagram-337W2JSQ-LL3f4vLf.js} +1 -1
  47. package/dist/assets/{requirementDiagram-Z7DCOOCP-DUq8H3CL.js → requirementDiagram-Z7DCOOCP-Di-2O6LH.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-WA2Y5GQK-CmqEUxRu.js → sankeyDiagram-WA2Y5GQK-9lHqrXqR.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-2WXFIKYE-DhtXRNiH.js → sequenceDiagram-2WXFIKYE-BQu-SoGr.js} +1 -1
  50. package/dist/assets/{stateDiagram-RAJIS63D-Dj0HOlbN.js → stateDiagram-RAJIS63D-BUxvd2BC.js} +1 -1
  51. package/dist/assets/stateDiagram-v2-FVOUBMTO-CDVexTiR.js +1 -0
  52. package/dist/assets/{timeline-definition-YZTLITO2-DUuJzZB5.js → timeline-definition-YZTLITO2-oP47UEU6.js} +1 -1
  53. package/dist/assets/{treemap-KZPCXAKY-DpYBQ0qr.js → treemap-KZPCXAKY-BRjDo2aE.js} +1 -1
  54. package/dist/assets/{vendor-codemirror-CMHSJ_9p.js → vendor-codemirror-BiCeS-y4.js} +1 -1
  55. package/dist/assets/{vendor-react-xmA_f8ig.js → vendor-react-DVlYPmi3.js} +1 -1
  56. package/dist/assets/{vennDiagram-LZ73GAT5-DpePUyOd.js → vennDiagram-LZ73GAT5-DrRqcDqo.js} +1 -1
  57. package/dist/assets/{xychartDiagram-JWTSCODW-Cfp1I4_U.js → xychartDiagram-JWTSCODW-DUXrymAi.js} +1 -1
  58. package/dist/index.html +4 -4
  59. package/package.json +25 -6
  60. package/scripts/refresh-acp-default-capabilities.mjs +160 -0
  61. package/server/acp-runtime/client.js +1137 -181
  62. package/server/acp-runtime/command-overrides.js +48 -0
  63. package/server/acp-runtime/index.js +576 -16
  64. package/server/acp-runtime/registry.js +6 -4
  65. package/server/acp-runtime/session-store.js +235 -92
  66. package/server/database/db.js +12 -3
  67. package/server/external-agent/ws.js +212 -11
  68. package/server/index.js +156 -53
  69. package/server/projects-watcher-config.js +4 -0
  70. package/server/projects.js +485 -125
  71. package/server/routes/cc-connect.js +5 -4
  72. package/server/routes/codex.js +24 -0
  73. package/server/routes/commands.js +166 -10
  74. package/server/routes/runs.js +641 -0
  75. package/server/routes/session-core.js +357 -109
  76. package/server/session-core/eventStore.js +0 -121
  77. package/server/session-core/providerAdapters.js +644 -163
  78. package/server/session-core/providerDiscovery.js +66 -38
  79. package/server/session-core/runRegistry.js +244 -0
  80. package/server/session-core/runtimeState.js +75 -3
  81. package/server/session-core/runtimeWriter.js +132 -10
  82. package/server/utils/codexImagePlayground.js +479 -0
  83. package/server/utils/localTerminal.js +56 -0
  84. package/server/utils/shellCommand.js +70 -0
  85. package/shared/acpCapabilities.js +393 -0
  86. package/shared/acpDefaultCapabilities.generated.json +141 -0
  87. package/shared/conversationEvents.js +425 -121
  88. package/dist/assets/App-CYCCsgwf.js +0 -264
  89. package/dist/assets/ReviewApp-0srHIXwb.js +0 -1
  90. package/dist/assets/channel-BMhScXFe.js +0 -1
  91. package/dist/assets/classDiagram-VBA2DB6C-CMIxlWcT.js +0 -1
  92. package/dist/assets/classDiagram-v2-RAHNMMFH-CMIxlWcT.js +0 -1
  93. package/dist/assets/clone-BPqOt4r3.js +0 -1
  94. package/dist/assets/index-C514cLyb.js +0 -2
  95. package/dist/assets/index-h1DBl_g3.css +0 -1
  96. package/dist/assets/stateDiagram-v2-FVOUBMTO-C9utf5gv.js +0 -1
@@ -2,16 +2,41 @@ import {
2
2
  isConversationEvent,
3
3
  normalizeRealtimePayloadToConversationEvents
4
4
  } from '../../shared/conversationEvents.js';
5
- import { appendMirroredConversationEvents } from './eventStore.js';
6
5
  import { publishSessionRuntimeStateChanges } from './runtimeState.js';
7
6
 
7
+ function shouldSendNormalizedEventsBeforeRawPayload(data) {
8
+ if (!data || data.type === 'conversation-event') {
9
+ return false;
10
+ }
11
+
12
+ if (data.type === 'claude-response') {
13
+ const dataType = data.data?.type;
14
+ return dataType === 'content_block_delta' || dataType === 'content_block_stop';
15
+ }
16
+
17
+ if (data.type === 'codex-response') {
18
+ const dataType = data.data?.type;
19
+ return dataType === 'item_delta' || dataType === 'item_done';
20
+ }
21
+
22
+ return false;
23
+ }
24
+
25
+ function isRuntimeReplayConversationEvent(event) {
26
+ const sourceType = String(event?.rawRef?.sourceType || '').trim().toLowerCase();
27
+ return Boolean(event?.extensions?.runtimeReplay) || sourceType === 'acp-session-loaded-replay';
28
+ }
29
+
8
30
  export class SessionEventMirrorWriter {
9
- constructor(writer, provider = 'claude') {
31
+ constructor(writer, provider = 'claude', options = {}) {
10
32
  this.writer = writer;
11
33
  this.provider = provider;
12
34
  this.sessionId = null;
13
35
  this.isWebSocketWriter = writer?.isWebSocketWriter;
14
36
  this.isSSEStreamWriter = writer?.isSSEStreamWriter;
37
+ this.runRegistry = options.runRegistry || null;
38
+ this.runId = options.runId || null;
39
+ this.clientRequestId = options.clientRequestId || null;
15
40
  }
16
41
 
17
42
  send(data) {
@@ -19,8 +44,6 @@ export class SessionEventMirrorWriter {
19
44
  this.sessionId = data.sessionId;
20
45
  }
21
46
 
22
- this.writer.send(data);
23
-
24
47
  const normalizedEvents = data?.type === 'conversation-event' && isConversationEvent(data?.event)
25
48
  ? [data.event]
26
49
  : normalizeRealtimePayloadToConversationEvents({
@@ -29,21 +52,28 @@ export class SessionEventMirrorWriter {
29
52
  sessionId: data?.sessionId || this.sessionId
30
53
  }, this.provider);
31
54
 
32
- normalizedEvents.forEach((event) => {
55
+ const sendNormalizedEvents = () => normalizedEvents.forEach((event) => {
33
56
  if (!(data?.type === 'conversation-event' && data?.event?.eventId === event.eventId)) {
34
- this.writer.send({
57
+ this.writer.send(this.annotateConversationEventPayload({
35
58
  type: 'conversation-event',
36
59
  provider: event.provider,
37
60
  sessionId: event.sessionId,
38
61
  event
39
- });
62
+ }, event));
40
63
  }
41
64
  });
42
65
 
66
+ if (data?.type === 'conversation-event' && isConversationEvent(data?.event)) {
67
+ this.writer.send(this.annotateConversationEventPayload(data, data.event));
68
+ } else if (shouldSendNormalizedEventsBeforeRawPayload(data)) {
69
+ sendNormalizedEvents();
70
+ this.writer.send(data);
71
+ } else {
72
+ this.writer.send(data);
73
+ sendNormalizedEvents();
74
+ }
75
+
43
76
  if (normalizedEvents.length > 0) {
44
- void appendMirroredConversationEvents(normalizedEvents).catch((error) => {
45
- console.warn('[WARN] Failed to persist mirrored conversation events:', error.message);
46
- });
47
77
  void publishSessionRuntimeStateChanges(normalizedEvents).catch((error) => {
48
78
  console.warn('[WARN] Failed to publish runtime state changes:', error.message);
49
79
  });
@@ -63,4 +93,96 @@ export class SessionEventMirrorWriter {
63
93
  }
64
94
  return this.sessionId;
65
95
  }
96
+
97
+ setRunContext({ runId, clientRequestId, runRegistry } = {}) {
98
+ if (runId !== undefined) {
99
+ this.runId = runId;
100
+ }
101
+ if (clientRequestId !== undefined) {
102
+ this.clientRequestId = clientRequestId;
103
+ }
104
+ if (runRegistry !== undefined) {
105
+ this.runRegistry = runRegistry;
106
+ }
107
+ }
108
+
109
+ annotateConversationEventPayload(payload, event) {
110
+ if (!this.runRegistry || !this.runId || !isConversationEvent(event)) {
111
+ return payload;
112
+ }
113
+
114
+ const eventSessionId = event.sessionId || payload?.sessionId || this.sessionId || null;
115
+ if (eventSessionId) {
116
+ this.sessionId = eventSessionId;
117
+ }
118
+
119
+ if (isRuntimeReplayConversationEvent(event)) {
120
+ return payload;
121
+ }
122
+
123
+ const eventClientRequestId = event.extensions?.clientRequestId || payload?.clientRequestId || this.clientRequestId || null;
124
+ const runEvent = this.runRegistry.appendRunEvent(this.runId, {
125
+ type: 'conversation-event',
126
+ provider: event.provider || payload?.provider || this.provider,
127
+ sessionId: eventSessionId,
128
+ clientRequestId: eventClientRequestId,
129
+ event,
130
+ });
131
+
132
+ this.updateRunStatusFromConversationEvent(event, {
133
+ sessionId: eventSessionId,
134
+ clientRequestId: eventClientRequestId,
135
+ });
136
+
137
+ if (!runEvent) {
138
+ return payload;
139
+ }
140
+
141
+ return {
142
+ ...payload,
143
+ provider: payload?.provider || event.provider || this.provider,
144
+ sessionId: payload?.sessionId || eventSessionId,
145
+ clientRequestId: payload?.clientRequestId || eventClientRequestId,
146
+ runId: this.runId,
147
+ runEventSeq: runEvent.seq,
148
+ };
149
+ }
150
+
151
+ updateRunStatusFromConversationEvent(event, { sessionId = null, clientRequestId = null } = {}) {
152
+ if (!this.runRegistry || !this.runId || !isConversationEvent(event)) {
153
+ return;
154
+ }
155
+
156
+ const state = event.kind === 'session_state_changed'
157
+ ? String(event.payload?.state || '').trim().toLowerCase()
158
+ : '';
159
+ let status = null;
160
+
161
+ if (event.kind === 'approval_request' || state === 'awaiting_approval') {
162
+ status = 'awaiting_approval';
163
+ } else if (event.kind === 'elicitation_request' || state === 'awaiting_input') {
164
+ status = 'awaiting_input';
165
+ } else if (event.kind === 'error' || state === 'errored') {
166
+ status = 'failed';
167
+ } else if (state === 'completed') {
168
+ status = 'succeeded';
169
+ } else if (state === 'aborted') {
170
+ status = 'canceled';
171
+ } else if (state === 'streaming' || event.kind === 'approval_resolved' || event.kind === 'elicitation_resolved') {
172
+ status = 'running';
173
+ }
174
+
175
+ if (status) {
176
+ this.runRegistry.updateRun(this.runId, {
177
+ status,
178
+ sessionId,
179
+ clientRequestId,
180
+ });
181
+ } else if (sessionId || clientRequestId) {
182
+ this.runRegistry.updateRun(this.runId, {
183
+ sessionId,
184
+ clientRequestId,
185
+ });
186
+ }
187
+ }
66
188
  }
@@ -0,0 +1,479 @@
1
+ import { promises as fs } from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import TOML from '@iarna/toml';
5
+
6
+ export const CODEX_IMAGE_MODEL = 'gpt-image-2';
7
+ const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';
8
+ const PROJECT_IMAGE_CONFIG_FILENAMES = [
9
+ path.join('.codex', 'imagegen.config.local.json'),
10
+ path.join('.codex', 'imagegen.config.json')
11
+ ];
12
+
13
+ function dedupePaths(paths) {
14
+ const seen = new Set();
15
+ const result = [];
16
+
17
+ for (const item of paths) {
18
+ if (typeof item !== 'string' || !item.trim()) {
19
+ continue;
20
+ }
21
+
22
+ const key = item.trim();
23
+ if (seen.has(key)) {
24
+ continue;
25
+ }
26
+
27
+ seen.add(key);
28
+ result.push(key);
29
+ }
30
+
31
+ return result;
32
+ }
33
+
34
+ function splitPathList(value, delimiter = path.delimiter) {
35
+ if (typeof value !== 'string' || !value.trim()) {
36
+ return [];
37
+ }
38
+
39
+ return value
40
+ .split(delimiter)
41
+ .map((item) => item.trim())
42
+ .filter(Boolean);
43
+ }
44
+
45
+ function getUnixConfigDirs(env = {}) {
46
+ const dirs = [];
47
+ const configuredDirs = splitPathList(env.XDG_CONFIG_DIRS || '', ':');
48
+
49
+ if (configuredDirs.length) {
50
+ dirs.push(...configuredDirs);
51
+ } else {
52
+ dirs.push('/etc/xdg');
53
+ }
54
+
55
+ dirs.push('/etc');
56
+ return dedupePaths(dirs);
57
+ }
58
+
59
+ function getWindowsConfigDirs(env = {}, homeDir = os.homedir()) {
60
+ return dedupePaths([
61
+ env.APPDATA ? path.win32.join(env.APPDATA, 'Codex') : null,
62
+ env.LOCALAPPDATA ? path.win32.join(env.LOCALAPPDATA, 'Codex') : null,
63
+ env.PROGRAMDATA ? path.win32.join(env.PROGRAMDATA, 'Codex') : 'C:\\ProgramData\\Codex',
64
+ path.win32.join(homeDir, 'AppData', 'Roaming', 'Codex'),
65
+ path.win32.join(homeDir, 'AppData', 'Local', 'Codex')
66
+ ]);
67
+ }
68
+
69
+ export function createCodexConfigPaths({
70
+ platform = process.platform,
71
+ homeDir = os.homedir(),
72
+ env = process.env
73
+ } = {}) {
74
+ const useWin32 = platform === 'win32';
75
+ const pathApi = useWin32 ? path.win32 : path.posix;
76
+ const codexHome = pathApi.join(homeDir, '.codex');
77
+ const configuredCodexHome = typeof env.CODEX_HOME === 'string' && env.CODEX_HOME.trim()
78
+ ? env.CODEX_HOME.trim()
79
+ : null;
80
+ const configDirs = [];
81
+
82
+ if (useWin32) {
83
+ configDirs.push(...getWindowsConfigDirs(env, homeDir));
84
+ } else {
85
+ const xdgConfigHome = env.XDG_CONFIG_HOME
86
+ ? pathApi.join(env.XDG_CONFIG_HOME, 'codex')
87
+ : pathApi.join(homeDir, '.config', 'codex');
88
+ configDirs.push(xdgConfigHome);
89
+ configDirs.push(...getUnixConfigDirs(env).map((dir) => pathApi.join(dir, 'codex')));
90
+
91
+ if (platform === 'darwin') {
92
+ configDirs.push('/Library/Application Support/Codex');
93
+ }
94
+ }
95
+
96
+ const roots = dedupePaths([configuredCodexHome, codexHome, ...configDirs]);
97
+ return {
98
+ authPaths: roots.map((root) => pathApi.join(root, 'auth.json')),
99
+ configPaths: roots.map((root) => pathApi.join(root, 'config.toml'))
100
+ };
101
+ }
102
+
103
+ function createProjectImageConfigPaths(projectPath) {
104
+ if (typeof projectPath !== 'string' || !projectPath.trim()) {
105
+ return [];
106
+ }
107
+
108
+ const root = path.resolve(projectPath.trim());
109
+ return PROJECT_IMAGE_CONFIG_FILENAMES.map((filename) => path.join(root, filename));
110
+ }
111
+
112
+ async function readExistingTextFiles(paths) {
113
+ const files = [];
114
+
115
+ for (const filePath of paths) {
116
+ try {
117
+ files.push({
118
+ path: filePath,
119
+ content: await fs.readFile(filePath, 'utf8')
120
+ });
121
+ } catch (error) {
122
+ if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') {
123
+ throw error;
124
+ }
125
+ }
126
+ }
127
+
128
+ return files;
129
+ }
130
+
131
+ function mergePlainObjects(base, override) {
132
+ const next = { ...(base || {}) };
133
+ for (const [key, value] of Object.entries(override || {})) {
134
+ if (
135
+ value &&
136
+ typeof value === 'object' &&
137
+ !Array.isArray(value) &&
138
+ next[key] &&
139
+ typeof next[key] === 'object' &&
140
+ !Array.isArray(next[key])
141
+ ) {
142
+ next[key] = mergePlainObjects(next[key], value);
143
+ } else {
144
+ next[key] = value;
145
+ }
146
+ }
147
+ return next;
148
+ }
149
+
150
+ function parseConfigFiles(files) {
151
+ const warnings = [];
152
+ let config = {};
153
+
154
+ for (const file of files) {
155
+ try {
156
+ config = mergePlainObjects(config, TOML.parse(file.content));
157
+ } catch (error) {
158
+ warnings.push({
159
+ path: file.path,
160
+ message: `Failed to parse Codex config: ${error.message}`
161
+ });
162
+ }
163
+ }
164
+
165
+ return { config, warnings };
166
+ }
167
+
168
+ function normalizeBaseUrl(value) {
169
+ const trimmed = String(value || '').trim();
170
+ if (!trimmed) {
171
+ return DEFAULT_OPENAI_BASE_URL;
172
+ }
173
+
174
+ return trimmed.replace(/\/+$/, '');
175
+ }
176
+
177
+ function getActiveProvider(config) {
178
+ const providerId = typeof config?.model_provider === 'string'
179
+ ? config.model_provider.trim()
180
+ : '';
181
+ const providers = config?.model_providers && typeof config.model_providers === 'object'
182
+ ? config.model_providers
183
+ : {};
184
+ const provider = providerId ? providers[providerId] : null;
185
+
186
+ return {
187
+ providerId: providerId || null,
188
+ provider: provider && typeof provider === 'object' ? provider : null
189
+ };
190
+ }
191
+
192
+ function getApiMode(provider) {
193
+ const wireApi = typeof provider?.wire_api === 'string'
194
+ ? provider.wire_api.trim().toLowerCase()
195
+ : '';
196
+
197
+ return wireApi === 'responses' ? 'images' : 'images';
198
+ }
199
+
200
+ function getProjectApiMode(value) {
201
+ return value === 'responses' ? 'responses' : 'images';
202
+ }
203
+
204
+ function getBooleanLaunchParam(value, fallback = false) {
205
+ if (typeof value === 'boolean') {
206
+ return value ? 'true' : 'false';
207
+ }
208
+
209
+ if (typeof value === 'string') {
210
+ const normalized = value.trim().toLowerCase();
211
+ if (['true', '1', 'yes', 'on'].includes(normalized)) {
212
+ return 'true';
213
+ }
214
+ if (['false', '0', 'no', 'off'].includes(normalized)) {
215
+ return 'false';
216
+ }
217
+ }
218
+
219
+ return fallback ? 'true' : 'false';
220
+ }
221
+
222
+ function resolveEnvReference(value, env = process.env) {
223
+ if (typeof value !== 'string') {
224
+ return '';
225
+ }
226
+
227
+ const trimmed = value.trim();
228
+ if (!trimmed) {
229
+ return '';
230
+ }
231
+
232
+ if (trimmed.startsWith('env:')) {
233
+ const envName = trimmed.slice('env:'.length).trim();
234
+ return envName ? String(env[envName] || '').trim() : '';
235
+ }
236
+
237
+ const envMatch = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(trimmed);
238
+ if (envMatch) {
239
+ return String(env[envMatch[1]] || '').trim();
240
+ }
241
+
242
+ return trimmed;
243
+ }
244
+
245
+ function getProjectApiKey(config, env = process.env) {
246
+ if (typeof config?.apiKeyEnv === 'string' && config.apiKeyEnv.trim()) {
247
+ return String(env[config.apiKeyEnv.trim()] || '').trim();
248
+ }
249
+
250
+ return resolveEnvReference(config?.apiKey || config?.api_key || '', env);
251
+ }
252
+
253
+ function normalizeProjectConfigObject(value) {
254
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
255
+ return null;
256
+ }
257
+
258
+ const record = value;
259
+ if (record.profile && typeof record.profile === 'object' && !Array.isArray(record.profile)) {
260
+ return record.profile;
261
+ }
262
+
263
+ return record;
264
+ }
265
+
266
+ async function readProjectImageConfig(projectPath, env = process.env) {
267
+ const paths = createProjectImageConfigPaths(projectPath);
268
+ const files = await readExistingTextFiles(paths);
269
+ const warnings = [];
270
+
271
+ for (const file of files) {
272
+ try {
273
+ const parsed = JSON.parse(file.content);
274
+ const config = normalizeProjectConfigObject(parsed);
275
+ if (!config) {
276
+ warnings.push({
277
+ path: file.path,
278
+ message: 'Project image config must be a JSON object.'
279
+ });
280
+ continue;
281
+ }
282
+
283
+ const source = typeof config.source === 'string' ? config.source.trim().toLowerCase() : '';
284
+ if (source === 'codex') {
285
+ return {
286
+ config: null,
287
+ file: file.path,
288
+ paths,
289
+ warnings,
290
+ useCodexConfig: true
291
+ };
292
+ }
293
+
294
+ const apiKey = getProjectApiKey(config, env);
295
+ const launchParams = {
296
+ apiUrl: normalizeBaseUrl(config.apiUrl || config.baseUrl || config.base_url || DEFAULT_OPENAI_BASE_URL),
297
+ apiKey,
298
+ apiMode: getProjectApiMode(config.apiMode || config.api_mode),
299
+ model: typeof config.model === 'string' && config.model.trim()
300
+ ? config.model.trim()
301
+ : CODEX_IMAGE_MODEL,
302
+ codexCli: getBooleanLaunchParam(config.codexCli ?? config.codex_cli, false)
303
+ };
304
+
305
+ if (config.apiProxy !== undefined || config.api_proxy !== undefined) {
306
+ launchParams.apiProxy = getBooleanLaunchParam(config.apiProxy ?? config.api_proxy, false);
307
+ }
308
+
309
+ const configWarnings = [];
310
+ if (!apiKey) {
311
+ const keySource = typeof config.apiKey === 'string' ? config.apiKey.trim() : '';
312
+ const keyEnv = typeof config.apiKeyEnv === 'string' ? config.apiKeyEnv.trim() : '';
313
+ configWarnings.push({
314
+ path: file.path,
315
+ message: keySource.startsWith('env:') || keyEnv
316
+ ? 'Project image config references an environment variable, but no API key value was found.'
317
+ : 'Project image config does not include an API key.'
318
+ });
319
+ }
320
+
321
+ return {
322
+ config,
323
+ file: file.path,
324
+ paths,
325
+ launchParams,
326
+ warnings: [...warnings, ...configWarnings],
327
+ useCodexConfig: false
328
+ };
329
+ } catch (error) {
330
+ warnings.push({
331
+ path: file.path,
332
+ message: `Failed to parse project image config: ${error.message}`
333
+ });
334
+ }
335
+ }
336
+
337
+ return {
338
+ config: null,
339
+ file: null,
340
+ paths,
341
+ warnings,
342
+ useCodexConfig: false
343
+ };
344
+ }
345
+
346
+ function readApiKeyFromAuth(auth) {
347
+ if (!auth || typeof auth !== 'object') {
348
+ return '';
349
+ }
350
+
351
+ if (typeof auth.OPENAI_API_KEY === 'string' && auth.OPENAI_API_KEY.trim()) {
352
+ return auth.OPENAI_API_KEY.trim();
353
+ }
354
+
355
+ if (typeof auth.openaiApiKey === 'string' && auth.openaiApiKey.trim()) {
356
+ return auth.openaiApiKey.trim();
357
+ }
358
+
359
+ if (typeof auth.api_key === 'string' && auth.api_key.trim()) {
360
+ return auth.api_key.trim();
361
+ }
362
+
363
+ return '';
364
+ }
365
+
366
+ async function readFirstAuth(authPaths) {
367
+ const files = await readExistingTextFiles(authPaths);
368
+ for (const file of files) {
369
+ try {
370
+ const parsed = JSON.parse(file.content);
371
+ const apiKey = readApiKeyFromAuth(parsed);
372
+ if (apiKey) {
373
+ return {
374
+ apiKey,
375
+ authFile: file.path
376
+ };
377
+ }
378
+ } catch {
379
+ // Ignore malformed auth candidates and continue scanning.
380
+ }
381
+ }
382
+
383
+ return {
384
+ apiKey: '',
385
+ authFile: files[0]?.path || null
386
+ };
387
+ }
388
+
389
+ export async function resolveCodexImagePlaygroundConfig(options = {}) {
390
+ const projectConfig = await readProjectImageConfig(options.projectPath, options.env || process.env);
391
+ if (projectConfig.launchParams && !projectConfig.useCodexConfig) {
392
+ const ready = Boolean(projectConfig.launchParams.apiKey);
393
+
394
+ return {
395
+ ready,
396
+ launchParams: projectConfig.launchParams,
397
+ discovery: {
398
+ projectConfigFile: projectConfig.file,
399
+ projectConfigPaths: projectConfig.paths,
400
+ configSource: 'project'
401
+ },
402
+ warnings: projectConfig.warnings
403
+ };
404
+ }
405
+
406
+ const paths = options.configPaths && options.authPaths
407
+ ? {
408
+ configPaths: options.configPaths,
409
+ authPaths: options.authPaths
410
+ }
411
+ : createCodexConfigPaths(options);
412
+ const configFiles = await readExistingTextFiles(paths.configPaths);
413
+ const { config, warnings } = parseConfigFiles(configFiles);
414
+ const { provider } = getActiveProvider(config);
415
+ const { apiKey, authFile } = await readFirstAuth(paths.authPaths);
416
+ const apiUrl = normalizeBaseUrl(provider?.base_url || config?.base_url || DEFAULT_OPENAI_BASE_URL);
417
+ const ready = Boolean(apiKey);
418
+
419
+ return {
420
+ ready,
421
+ launchParams: {
422
+ apiUrl,
423
+ apiKey,
424
+ apiMode: getApiMode(provider),
425
+ model: CODEX_IMAGE_MODEL,
426
+ codexCli: 'true'
427
+ },
428
+ discovery: {
429
+ configFiles: configFiles.map((file) => file.path),
430
+ authFile,
431
+ scannedConfigPaths: paths.configPaths,
432
+ scannedAuthPaths: paths.authPaths,
433
+ projectConfigFile: projectConfig.file,
434
+ projectConfigPaths: projectConfig.paths,
435
+ configSource: 'codex'
436
+ },
437
+ warnings: ready ? [...projectConfig.warnings, ...warnings] : [
438
+ ...projectConfig.warnings,
439
+ ...warnings,
440
+ { message: 'No OpenAI API key found in Codex auth files.' }
441
+ ]
442
+ };
443
+ }
444
+
445
+ function normalizeRoutePath(routePath) {
446
+ const value = typeof routePath === 'string' ? routePath.trim() : '';
447
+ if (!value.startsWith('/') || value.startsWith('//')) {
448
+ return '/image-playground';
449
+ }
450
+
451
+ return value;
452
+ }
453
+
454
+ export function buildCodexImagePlaygroundLaunch({
455
+ routePath = '/image-playground',
456
+ config
457
+ } = {}) {
458
+ const launchParams = config?.launchParams || {};
459
+ const urlSearchParams = new URLSearchParams();
460
+
461
+ for (const [key, value] of Object.entries(launchParams)) {
462
+ if (typeof value === 'string' && value) {
463
+ urlSearchParams.set(key, value);
464
+ }
465
+ }
466
+
467
+ const queryString = urlSearchParams.toString();
468
+ const safeRoutePath = normalizeRoutePath(routePath);
469
+ const url = queryString ? `${safeRoutePath}?${queryString}` : safeRoutePath;
470
+ const { apiKey: _apiKey, ...safeParams } = launchParams;
471
+
472
+ return {
473
+ ready: Boolean(config?.ready),
474
+ url,
475
+ params: safeParams,
476
+ discovery: config?.discovery || {},
477
+ warnings: config?.warnings || []
478
+ };
479
+ }
@@ -0,0 +1,56 @@
1
+ import os from 'os';
2
+ import { spawn } from 'child_process';
3
+
4
+ function escapeAppleScriptString(value) {
5
+ return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
6
+ }
7
+
8
+ export function buildLocalTerminalSpawn({ command, platform = os.platform() }) {
9
+ const terminalCommand = String(command || '').trim();
10
+
11
+ if (!terminalCommand) {
12
+ throw new Error('Command is required');
13
+ }
14
+
15
+ if (platform === 'darwin') {
16
+ return {
17
+ command: 'osascript',
18
+ args: [
19
+ '-e',
20
+ 'tell application "Terminal" to activate',
21
+ '-e',
22
+ `tell application "Terminal" to do script "${escapeAppleScriptString(terminalCommand)}"`
23
+ ]
24
+ };
25
+ }
26
+
27
+ if (platform === 'win32') {
28
+ return {
29
+ command: 'cmd.exe',
30
+ args: [
31
+ '/c',
32
+ 'start',
33
+ '',
34
+ 'powershell.exe',
35
+ '-NoExit',
36
+ '-Command',
37
+ terminalCommand
38
+ ]
39
+ };
40
+ }
41
+
42
+ return {
43
+ command: 'x-terminal-emulator',
44
+ args: ['-e', 'bash', '-lc', terminalCommand]
45
+ };
46
+ }
47
+
48
+ export function openLocalTerminal({ command, platform = os.platform() }) {
49
+ const spawnConfig = buildLocalTerminalSpawn({ command, platform });
50
+ const child = spawn(spawnConfig.command, spawnConfig.args, {
51
+ detached: true,
52
+ stdio: 'ignore'
53
+ });
54
+ child.unref();
55
+ return spawnConfig;
56
+ }