@foxden-app/foxclaw 0.2.0
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.
- package/.env.example +36 -0
- package/LICENSE +22 -0
- package/README.md +244 -0
- package/README_EN.md +244 -0
- package/dist/channels/bridge_messaging_router.d.ts +27 -0
- package/dist/channels/bridge_messaging_router.js +85 -0
- package/dist/channels/telegram/telegram_channel_adapter.d.ts +12 -0
- package/dist/channels/telegram/telegram_channel_adapter.js +21 -0
- package/dist/channels/telegram/telegram_messaging_port.d.ts +25 -0
- package/dist/channels/telegram/telegram_messaging_port.js +51 -0
- package/dist/channels/weixin/account_store.d.ts +15 -0
- package/dist/channels/weixin/account_store.js +54 -0
- package/dist/channels/weixin/ilink/aes_ecb.d.ts +3 -0
- package/dist/channels/weixin/ilink/aes_ecb.js +12 -0
- package/dist/channels/weixin/ilink/api.d.ts +44 -0
- package/dist/channels/weixin/ilink/api.js +187 -0
- package/dist/channels/weixin/ilink/cdn_upload.d.ts +11 -0
- package/dist/channels/weixin/ilink/cdn_upload.js +60 -0
- package/dist/channels/weixin/ilink/cdn_url.d.ts +7 -0
- package/dist/channels/weixin/ilink/cdn_url.js +7 -0
- package/dist/channels/weixin/ilink/constants.d.ts +7 -0
- package/dist/channels/weixin/ilink/constants.js +27 -0
- package/dist/channels/weixin/ilink/context.d.ts +13 -0
- package/dist/channels/weixin/ilink/context.js +13 -0
- package/dist/channels/weixin/ilink/login_qr.d.ts +34 -0
- package/dist/channels/weixin/ilink/login_qr.js +233 -0
- package/dist/channels/weixin/ilink/media_image.d.ts +11 -0
- package/dist/channels/weixin/ilink/media_image.js +44 -0
- package/dist/channels/weixin/ilink/mime.d.ts +3 -0
- package/dist/channels/weixin/ilink/mime.js +36 -0
- package/dist/channels/weixin/ilink/pic_decrypt.d.ts +2 -0
- package/dist/channels/weixin/ilink/pic_decrypt.js +56 -0
- package/dist/channels/weixin/ilink/random.d.ts +2 -0
- package/dist/channels/weixin/ilink/random.js +7 -0
- package/dist/channels/weixin/ilink/redact.d.ts +4 -0
- package/dist/channels/weixin/ilink/redact.js +34 -0
- package/dist/channels/weixin/ilink/runtime_attach.d.ts +3 -0
- package/dist/channels/weixin/ilink/runtime_attach.js +13 -0
- package/dist/channels/weixin/ilink/send.d.ts +21 -0
- package/dist/channels/weixin/ilink/send.js +108 -0
- package/dist/channels/weixin/ilink/session_guard.d.ts +6 -0
- package/dist/channels/weixin/ilink/session_guard.js +39 -0
- package/dist/channels/weixin/ilink/types.d.ts +155 -0
- package/dist/channels/weixin/ilink/types.js +10 -0
- package/dist/channels/weixin/ilink/upload.d.ts +15 -0
- package/dist/channels/weixin/ilink/upload.js +75 -0
- package/dist/channels/weixin/sync_buf_store.d.ts +3 -0
- package/dist/channels/weixin/sync_buf_store.js +19 -0
- package/dist/channels/weixin/weixin_channel_adapter.d.ts +18 -0
- package/dist/channels/weixin/weixin_channel_adapter.js +273 -0
- package/dist/channels/weixin/weixin_messaging_port.d.ts +29 -0
- package/dist/channels/weixin/weixin_messaging_port.js +113 -0
- package/dist/codex_app/client.d.ts +176 -0
- package/dist/codex_app/client.js +1230 -0
- package/dist/codex_app/deeplink.d.ts +7 -0
- package/dist/codex_app/deeplink.js +29 -0
- package/dist/codex_app/local_usage.d.ts +16 -0
- package/dist/codex_app/local_usage.js +123 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.js +131 -0
- package/dist/controller/access.d.ts +11 -0
- package/dist/controller/access.js +33 -0
- package/dist/controller/activity.d.ts +62 -0
- package/dist/controller/activity.js +330 -0
- package/dist/controller/commands.d.ts +6 -0
- package/dist/controller/commands.js +17 -0
- package/dist/controller/controller.d.ts +326 -0
- package/dist/controller/controller.js +7503 -0
- package/dist/controller/observer.d.ts +16 -0
- package/dist/controller/observer.js +98 -0
- package/dist/controller/presentation.d.ts +80 -0
- package/dist/controller/presentation.js +568 -0
- package/dist/controller/service_tier.d.ts +9 -0
- package/dist/controller/service_tier.js +32 -0
- package/dist/controller/session_observer.d.ts +22 -0
- package/dist/controller/session_observer.js +259 -0
- package/dist/controller/status.d.ts +10 -0
- package/dist/controller/status.js +28 -0
- package/dist/core/bridge_scope.d.ts +18 -0
- package/dist/core/bridge_scope.js +46 -0
- package/dist/core/channel_port.d.ts +15 -0
- package/dist/core/channel_port.js +1 -0
- package/dist/i18n.d.ts +1108 -0
- package/dist/i18n.js +1154 -0
- package/dist/lock.d.ts +7 -0
- package/dist/lock.js +80 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +57 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +236 -0
- package/dist/runtime.d.ts +3 -0
- package/dist/runtime.js +14 -0
- package/dist/store/database.d.ts +79 -0
- package/dist/store/database.js +489 -0
- package/dist/store/migrate_bridge_scope.d.ts +6 -0
- package/dist/store/migrate_bridge_scope.js +59 -0
- package/dist/telegram/addressing.d.ts +33 -0
- package/dist/telegram/addressing.js +57 -0
- package/dist/telegram/api.d.ts +14 -0
- package/dist/telegram/api.js +89 -0
- package/dist/telegram/gateway.d.ts +76 -0
- package/dist/telegram/gateway.js +383 -0
- package/dist/telegram/media.d.ts +34 -0
- package/dist/telegram/media.js +180 -0
- package/dist/telegram/rendering.d.ts +10 -0
- package/dist/telegram/rendering.js +21 -0
- package/dist/telegram/scope.d.ts +6 -0
- package/dist/telegram/scope.js +24 -0
- package/dist/telegram/text.d.ts +7 -0
- package/dist/telegram/text.js +47 -0
- package/dist/types.d.ts +343 -0
- package/dist/types.js +1 -0
- package/docs/agent-assisted-install.md +84 -0
- package/docs/install-for-beginners.md +287 -0
- package/docs/troubleshooting.md +239 -0
- package/package.json +62 -0
- package/scripts/doctor.sh +3 -0
- package/scripts/launchd/install.sh +54 -0
- package/scripts/status.sh +3 -0
- package/scripts/systemd/install.sh +83 -0
- package/scripts/systemd/uninstall.sh +15 -0
- package/skills/foxclaw/SKILL.md +167 -0
- package/skills/foxclaw/agents/openai.yaml +4 -0
- package/skills/foxclaw/references/telegram-setup.md +93 -0
- package/skills/foxclaw/scripts/bootstrap_host.py +350 -0
- package/skills/foxclaw/scripts/bootstrap_remote.py +67 -0
|
@@ -0,0 +1,1230 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { buildThreadDeepLink, openUrl } from './deeplink.js';
|
|
7
|
+
export class CodexAppClient extends EventEmitter {
|
|
8
|
+
codexCliBin;
|
|
9
|
+
launchCommand;
|
|
10
|
+
autolaunch;
|
|
11
|
+
serverStatePath;
|
|
12
|
+
serverLogPath;
|
|
13
|
+
logger;
|
|
14
|
+
child = null;
|
|
15
|
+
socket = null;
|
|
16
|
+
requestId = 0;
|
|
17
|
+
pending = new Map();
|
|
18
|
+
desiredRunning = false;
|
|
19
|
+
reconnectTimer = null;
|
|
20
|
+
starting = null;
|
|
21
|
+
port = null;
|
|
22
|
+
connected = false;
|
|
23
|
+
userAgent = null;
|
|
24
|
+
constructor(codexCliBin, launchCommand, autolaunch, serverStatePath, serverLogPath, logger) {
|
|
25
|
+
super();
|
|
26
|
+
this.codexCliBin = codexCliBin;
|
|
27
|
+
this.launchCommand = launchCommand;
|
|
28
|
+
this.autolaunch = autolaunch;
|
|
29
|
+
this.serverStatePath = serverStatePath;
|
|
30
|
+
this.serverLogPath = serverLogPath;
|
|
31
|
+
this.logger = logger;
|
|
32
|
+
}
|
|
33
|
+
isConnected() {
|
|
34
|
+
return this.connected;
|
|
35
|
+
}
|
|
36
|
+
getUserAgent() {
|
|
37
|
+
return this.userAgent;
|
|
38
|
+
}
|
|
39
|
+
getServerStatus() {
|
|
40
|
+
const state = this.readServerState();
|
|
41
|
+
const pid = this.child?.pid ?? state?.pid ?? null;
|
|
42
|
+
const port = this.port ?? state?.port ?? null;
|
|
43
|
+
return {
|
|
44
|
+
pid,
|
|
45
|
+
port,
|
|
46
|
+
running: pid !== null && isProcessAlive(pid),
|
|
47
|
+
managed: state !== null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async start() {
|
|
51
|
+
this.desiredRunning = true;
|
|
52
|
+
if (this.connected)
|
|
53
|
+
return;
|
|
54
|
+
if (!this.starting) {
|
|
55
|
+
this.starting = this.startServer().finally(() => {
|
|
56
|
+
this.starting = null;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
await this.starting;
|
|
60
|
+
}
|
|
61
|
+
async stop(options = {}) {
|
|
62
|
+
this.desiredRunning = false;
|
|
63
|
+
if (this.reconnectTimer) {
|
|
64
|
+
clearTimeout(this.reconnectTimer);
|
|
65
|
+
this.reconnectTimer = null;
|
|
66
|
+
}
|
|
67
|
+
const socket = this.socket;
|
|
68
|
+
this.socket = null;
|
|
69
|
+
this.connected = false;
|
|
70
|
+
socket?.close();
|
|
71
|
+
if (options.terminateServer ?? true) {
|
|
72
|
+
await this.terminateServer();
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
this.child = null;
|
|
76
|
+
}
|
|
77
|
+
this.rejectPending(new Error('Codex app bridge stopped'));
|
|
78
|
+
}
|
|
79
|
+
async restart() {
|
|
80
|
+
await this.stop({ terminateServer: true });
|
|
81
|
+
await this.start();
|
|
82
|
+
}
|
|
83
|
+
async listThreads(options) {
|
|
84
|
+
const result = await this.request('thread/list', {
|
|
85
|
+
limit: options.limit,
|
|
86
|
+
sortKey: 'updated_at',
|
|
87
|
+
searchTerm: options.searchTerm ?? null,
|
|
88
|
+
archived: options.archived ?? false,
|
|
89
|
+
});
|
|
90
|
+
const rows = Array.isArray(result.data) ? result.data : [];
|
|
91
|
+
return rows.map(mapThread);
|
|
92
|
+
}
|
|
93
|
+
async listLoadedThreads() {
|
|
94
|
+
const threadIds = [];
|
|
95
|
+
let cursor = null;
|
|
96
|
+
do {
|
|
97
|
+
const result = await this.request('thread/loaded/list', { cursor, limit: 100 });
|
|
98
|
+
const rows = Array.isArray(result?.data) ? result.data : [];
|
|
99
|
+
threadIds.push(...rows.filter((value) => typeof value === 'string'));
|
|
100
|
+
cursor = typeof result?.nextCursor === 'string' ? result.nextCursor : null;
|
|
101
|
+
} while (cursor);
|
|
102
|
+
return threadIds;
|
|
103
|
+
}
|
|
104
|
+
async readThread(threadId, includeTurns = false) {
|
|
105
|
+
const result = await this.request('thread/read', { threadId, includeTurns });
|
|
106
|
+
const thread = result.thread;
|
|
107
|
+
return thread ? mapThread(thread) : null;
|
|
108
|
+
}
|
|
109
|
+
async readThreadSnapshot(threadId) {
|
|
110
|
+
const result = await this.request('thread/read', { threadId, includeTurns: true });
|
|
111
|
+
const thread = result.thread;
|
|
112
|
+
return thread ? mapThreadSnapshot(thread) : null;
|
|
113
|
+
}
|
|
114
|
+
async startThread(options) {
|
|
115
|
+
const result = await this.request('thread/start', {
|
|
116
|
+
cwd: options.cwd,
|
|
117
|
+
approvalPolicy: options.approvalPolicy,
|
|
118
|
+
model: options.model,
|
|
119
|
+
modelProvider: null,
|
|
120
|
+
sandbox: options.sandboxMode,
|
|
121
|
+
config: null,
|
|
122
|
+
serviceName: null,
|
|
123
|
+
baseInstructions: null,
|
|
124
|
+
developerInstructions: null,
|
|
125
|
+
personality: null,
|
|
126
|
+
ephemeral: null,
|
|
127
|
+
experimentalRawEvents: true,
|
|
128
|
+
persistExtendedHistory: false,
|
|
129
|
+
});
|
|
130
|
+
return mapThreadSessionState(result);
|
|
131
|
+
}
|
|
132
|
+
async resumeThread(options) {
|
|
133
|
+
const result = await this.request('thread/resume', {
|
|
134
|
+
threadId: options.threadId,
|
|
135
|
+
cwd: options.cwd ?? null,
|
|
136
|
+
approvalPolicy: options.approvalPolicy ?? null,
|
|
137
|
+
baseInstructions: null,
|
|
138
|
+
developerInstructions: null,
|
|
139
|
+
config: null,
|
|
140
|
+
sandbox: options.sandboxMode ?? null,
|
|
141
|
+
model: options.model ?? null,
|
|
142
|
+
modelProvider: null,
|
|
143
|
+
personality: null,
|
|
144
|
+
experimentalRawEvents: true,
|
|
145
|
+
persistExtendedHistory: false,
|
|
146
|
+
});
|
|
147
|
+
return mapThreadSessionState(result);
|
|
148
|
+
}
|
|
149
|
+
async startTurn(options) {
|
|
150
|
+
const params = {
|
|
151
|
+
threadId: options.threadId,
|
|
152
|
+
input: options.input,
|
|
153
|
+
cwd: options.cwd,
|
|
154
|
+
approvalPolicy: options.approvalPolicy,
|
|
155
|
+
sandboxPolicy: mapSandboxPolicy(options.sandboxMode),
|
|
156
|
+
model: options.model,
|
|
157
|
+
effort: options.effort,
|
|
158
|
+
summary: null,
|
|
159
|
+
personality: null,
|
|
160
|
+
outputSchema: null,
|
|
161
|
+
};
|
|
162
|
+
if (options.serviceTier !== undefined) {
|
|
163
|
+
params.serviceTier = options.serviceTier;
|
|
164
|
+
}
|
|
165
|
+
if (options.collaborationMode) {
|
|
166
|
+
params.collaborationMode = options.collaborationMode;
|
|
167
|
+
}
|
|
168
|
+
const result = await this.request('turn/start', params);
|
|
169
|
+
return result.turn;
|
|
170
|
+
}
|
|
171
|
+
async steerTurn(threadId, expectedTurnId, input) {
|
|
172
|
+
const result = await this.request('turn/steer', { threadId, expectedTurnId, input });
|
|
173
|
+
return { turnId: String(result?.turnId ?? expectedTurnId) };
|
|
174
|
+
}
|
|
175
|
+
async forkThread(options) {
|
|
176
|
+
const params = {
|
|
177
|
+
threadId: options.threadId,
|
|
178
|
+
cwd: options.cwd,
|
|
179
|
+
approvalPolicy: options.approvalPolicy,
|
|
180
|
+
sandbox: options.sandboxMode,
|
|
181
|
+
model: options.model,
|
|
182
|
+
modelProvider: null,
|
|
183
|
+
threadSource: null,
|
|
184
|
+
ephemeral: false,
|
|
185
|
+
};
|
|
186
|
+
if (options.serviceTier !== undefined) {
|
|
187
|
+
params.serviceTier = options.serviceTier;
|
|
188
|
+
}
|
|
189
|
+
const result = await this.request('thread/fork', params);
|
|
190
|
+
return mapThreadSessionState(result);
|
|
191
|
+
}
|
|
192
|
+
async rollbackThread(threadId, numTurns) {
|
|
193
|
+
const result = await this.request('thread/rollback', { threadId, numTurns });
|
|
194
|
+
const thread = result?.thread;
|
|
195
|
+
return thread ? mapThreadSnapshot(thread) : null;
|
|
196
|
+
}
|
|
197
|
+
async setThreadName(threadId, name) {
|
|
198
|
+
await this.request('thread/name/set', { threadId, name });
|
|
199
|
+
}
|
|
200
|
+
async compactThread(threadId) {
|
|
201
|
+
await this.request('thread/compact/start', { threadId });
|
|
202
|
+
}
|
|
203
|
+
async getThreadGoal(threadId) {
|
|
204
|
+
const result = await this.request('thread/goal/get', { threadId });
|
|
205
|
+
return mapThreadGoal(result?.goal);
|
|
206
|
+
}
|
|
207
|
+
async setThreadGoal(options) {
|
|
208
|
+
const params = { threadId: options.threadId };
|
|
209
|
+
if (options.objective !== undefined) {
|
|
210
|
+
params.objective = options.objective;
|
|
211
|
+
}
|
|
212
|
+
if (options.status !== undefined) {
|
|
213
|
+
params.status = options.status;
|
|
214
|
+
}
|
|
215
|
+
if (options.tokenBudget !== undefined) {
|
|
216
|
+
params.tokenBudget = options.tokenBudget;
|
|
217
|
+
}
|
|
218
|
+
const result = await this.request('thread/goal/set', params);
|
|
219
|
+
const goal = mapThreadGoal(result?.goal);
|
|
220
|
+
if (!goal) {
|
|
221
|
+
throw new Error('thread/goal/set returned no goal');
|
|
222
|
+
}
|
|
223
|
+
return goal;
|
|
224
|
+
}
|
|
225
|
+
async clearThreadGoal(threadId) {
|
|
226
|
+
const result = await this.request('thread/goal/clear', { threadId });
|
|
227
|
+
return Boolean(result?.cleared);
|
|
228
|
+
}
|
|
229
|
+
async listThreadTurns(threadId, limit = 10) {
|
|
230
|
+
const result = await this.request('thread/turns/list', {
|
|
231
|
+
threadId,
|
|
232
|
+
cursor: null,
|
|
233
|
+
limit,
|
|
234
|
+
sortDirection: 'desc',
|
|
235
|
+
itemsView: 'summary',
|
|
236
|
+
});
|
|
237
|
+
const turns = Array.isArray(result?.data) ? result.data : [];
|
|
238
|
+
return turns.map(mapTurnSnapshot);
|
|
239
|
+
}
|
|
240
|
+
async archiveThread(threadId) {
|
|
241
|
+
await this.request('thread/archive', { threadId });
|
|
242
|
+
}
|
|
243
|
+
async unarchiveThread(threadId) {
|
|
244
|
+
const result = await this.request('thread/unarchive', { threadId });
|
|
245
|
+
const thread = result?.thread;
|
|
246
|
+
return thread ? mapThread(thread) : null;
|
|
247
|
+
}
|
|
248
|
+
async startReview(threadId, target, delivery = 'inline') {
|
|
249
|
+
const result = await this.request('review/start', { threadId, target, delivery });
|
|
250
|
+
return {
|
|
251
|
+
turnId: String(result?.turn?.id ?? ''),
|
|
252
|
+
reviewThreadId: String(result?.reviewThreadId ?? threadId),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
async listModels() {
|
|
256
|
+
const models = [];
|
|
257
|
+
let cursor = null;
|
|
258
|
+
do {
|
|
259
|
+
const result = await this.request('model/list', { cursor, limit: 100, includeHidden: false });
|
|
260
|
+
const rows = Array.isArray(result.data) ? result.data : [];
|
|
261
|
+
models.push(...rows.map(mapModel));
|
|
262
|
+
cursor = typeof result.nextCursor === 'string' ? result.nextCursor : null;
|
|
263
|
+
} while (cursor);
|
|
264
|
+
return models;
|
|
265
|
+
}
|
|
266
|
+
async readAccount() {
|
|
267
|
+
const result = await this.request('account/read', { refreshToken: false });
|
|
268
|
+
const account = result?.account;
|
|
269
|
+
if (!account || typeof account !== 'object') {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
type: typeof account.type === 'string' ? account.type : 'unknown',
|
|
274
|
+
email: typeof account.email === 'string' ? account.email : null,
|
|
275
|
+
planType: typeof account.planType === 'string' ? account.planType : null,
|
|
276
|
+
requiresOpenaiAuth: Boolean(result?.requiresOpenaiAuth),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
async startDeviceLogin() {
|
|
280
|
+
const result = await this.request('account/login/start', { type: 'chatgptDeviceCode' });
|
|
281
|
+
if (result?.type !== 'chatgptDeviceCode') {
|
|
282
|
+
throw new Error(`Unexpected login response: ${JSON.stringify(result)}`);
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
type: 'chatgptDeviceCode',
|
|
286
|
+
loginId: String(result.loginId),
|
|
287
|
+
verificationUrl: String(result.verificationUrl),
|
|
288
|
+
userCode: String(result.userCode),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
async cancelLogin(loginId) {
|
|
292
|
+
await this.request('account/login/cancel', { loginId });
|
|
293
|
+
}
|
|
294
|
+
async logoutAccount() {
|
|
295
|
+
await this.request('account/logout', undefined);
|
|
296
|
+
}
|
|
297
|
+
async sendAddCreditsNudgeEmail(creditType) {
|
|
298
|
+
await this.request('account/sendAddCreditsNudgeEmail', { creditType });
|
|
299
|
+
}
|
|
300
|
+
async readAccountRateLimits() {
|
|
301
|
+
const result = await this.request('account/rateLimits/read', undefined);
|
|
302
|
+
if (!result || typeof result !== 'object') {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
rateLimits: mapRateLimitSnapshot(result.rateLimits),
|
|
307
|
+
rateLimitsByLimitId: mapRateLimitsByLimitId(result.rateLimitsByLimitId),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
async readEffectiveConfig(cwd) {
|
|
311
|
+
const result = await this.request('config/read', {
|
|
312
|
+
includeLayers: false,
|
|
313
|
+
cwd,
|
|
314
|
+
});
|
|
315
|
+
const config = result?.config ?? {};
|
|
316
|
+
return {
|
|
317
|
+
model: typeof config.model === 'string' ? config.model : null,
|
|
318
|
+
modelReasoningEffort: normalizeReasoningEffort(config.model_reasoning_effort),
|
|
319
|
+
planModeReasoningEffort: normalizeReasoningEffort(config.plan_mode_reasoning_effort),
|
|
320
|
+
developerInstructions: typeof config.developer_instructions === 'string' && config.developer_instructions.trim()
|
|
321
|
+
? config.developer_instructions
|
|
322
|
+
: null,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
async listCollaborationModes() {
|
|
326
|
+
const result = await this.request('collaborationMode/list', {});
|
|
327
|
+
const rows = Array.isArray(result?.data) ? result.data : [];
|
|
328
|
+
return rows.map(mapCollaborationModePreset);
|
|
329
|
+
}
|
|
330
|
+
async listSkills(cwd, forceReload = false) {
|
|
331
|
+
const result = await this.request('skills/list', {
|
|
332
|
+
cwds: cwd ? [cwd] : [],
|
|
333
|
+
forceReload,
|
|
334
|
+
});
|
|
335
|
+
const rows = Array.isArray(result?.data) ? result.data : [];
|
|
336
|
+
return rows.map(mapSkillsListEntry);
|
|
337
|
+
}
|
|
338
|
+
async writeSkillConfig(selector, enabled) {
|
|
339
|
+
await this.request('skills/config/write', {
|
|
340
|
+
name: selector.name ?? null,
|
|
341
|
+
path: selector.path ?? null,
|
|
342
|
+
enabled,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
async listHooks(cwd) {
|
|
346
|
+
const result = await this.request('hooks/list', { cwds: cwd ? [cwd] : [] });
|
|
347
|
+
const rows = Array.isArray(result?.data) ? result.data : [];
|
|
348
|
+
return rows.map(mapHooksListEntry);
|
|
349
|
+
}
|
|
350
|
+
async listPlugins(cwd) {
|
|
351
|
+
const result = await this.request('plugin/list', {
|
|
352
|
+
cwds: cwd ? [cwd] : null,
|
|
353
|
+
marketplaceKinds: null,
|
|
354
|
+
});
|
|
355
|
+
const rows = Array.isArray(result?.marketplaces) ? result.marketplaces : [];
|
|
356
|
+
return rows.map(mapPluginMarketplace);
|
|
357
|
+
}
|
|
358
|
+
async readPlugin(pluginName, options = {}) {
|
|
359
|
+
const result = await this.request('plugin/read', {
|
|
360
|
+
pluginName,
|
|
361
|
+
marketplacePath: options.marketplacePath ?? null,
|
|
362
|
+
remoteMarketplaceName: options.remoteMarketplaceName ?? null,
|
|
363
|
+
});
|
|
364
|
+
const plugin = result?.plugin;
|
|
365
|
+
return plugin && typeof plugin === 'object' ? mapPluginDetail(plugin) : null;
|
|
366
|
+
}
|
|
367
|
+
async readPluginSkill(remoteMarketplaceName, remotePluginId, skillName) {
|
|
368
|
+
const result = await this.request('plugin/skill/read', { remoteMarketplaceName, remotePluginId, skillName });
|
|
369
|
+
return typeof result?.contents === 'string' ? result.contents : null;
|
|
370
|
+
}
|
|
371
|
+
async listApps(threadId, forceRefetch = false) {
|
|
372
|
+
const apps = [];
|
|
373
|
+
let cursor = null;
|
|
374
|
+
do {
|
|
375
|
+
const result = await this.request('app/list', { cursor, limit: 100, threadId: threadId ?? null, forceRefetch });
|
|
376
|
+
const rows = Array.isArray(result?.data) ? result.data : [];
|
|
377
|
+
apps.push(...rows.map(mapAppInfo));
|
|
378
|
+
cursor = typeof result?.nextCursor === 'string' ? result.nextCursor : null;
|
|
379
|
+
} while (cursor);
|
|
380
|
+
return apps;
|
|
381
|
+
}
|
|
382
|
+
async readConfig(cwd, includeLayers = true) {
|
|
383
|
+
const result = await this.request('config/read', { cwd, includeLayers });
|
|
384
|
+
return result && typeof result === 'object' ? result : {};
|
|
385
|
+
}
|
|
386
|
+
async readConfigRequirements() {
|
|
387
|
+
const result = await this.request('configRequirements/read', undefined);
|
|
388
|
+
const requirements = result?.requirements;
|
|
389
|
+
return requirements && typeof requirements === 'object' ? mapConfigRequirements(requirements) : null;
|
|
390
|
+
}
|
|
391
|
+
async listExperimentalFeatures() {
|
|
392
|
+
const features = [];
|
|
393
|
+
let cursor = null;
|
|
394
|
+
do {
|
|
395
|
+
const result = await this.request('experimentalFeature/list', { cursor, limit: 100 });
|
|
396
|
+
const rows = Array.isArray(result?.data) ? result.data : [];
|
|
397
|
+
features.push(...rows.map(mapExperimentalFeature));
|
|
398
|
+
cursor = typeof result?.nextCursor === 'string' ? result.nextCursor : null;
|
|
399
|
+
} while (cursor);
|
|
400
|
+
return features;
|
|
401
|
+
}
|
|
402
|
+
async readModelProviderCapabilities() {
|
|
403
|
+
const result = await this.request('modelProvider/capabilities/read', {});
|
|
404
|
+
return {
|
|
405
|
+
webSearch: Boolean(result?.webSearch),
|
|
406
|
+
imageGeneration: Boolean(result?.imageGeneration),
|
|
407
|
+
namespaceTools: Boolean(result?.namespaceTools),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
async fuzzyFileSearch(query, roots) {
|
|
411
|
+
const result = await this.request('fuzzyFileSearch', {
|
|
412
|
+
query,
|
|
413
|
+
roots,
|
|
414
|
+
cancellationToken: null,
|
|
415
|
+
});
|
|
416
|
+
const files = Array.isArray(result?.files) ? result.files : [];
|
|
417
|
+
return files.map(mapFuzzyFileResult);
|
|
418
|
+
}
|
|
419
|
+
async listMcpServerStatus(detail = 'full') {
|
|
420
|
+
const rows = [];
|
|
421
|
+
let cursor = null;
|
|
422
|
+
do {
|
|
423
|
+
const result = await this.request('mcpServerStatus/list', { cursor, limit: 100, detail });
|
|
424
|
+
const data = Array.isArray(result?.data) ? result.data : [];
|
|
425
|
+
rows.push(...data.map(mapMcpServerStatus));
|
|
426
|
+
cursor = typeof result?.nextCursor === 'string' ? result.nextCursor : null;
|
|
427
|
+
} while (cursor);
|
|
428
|
+
return rows;
|
|
429
|
+
}
|
|
430
|
+
async reloadMcpServers() {
|
|
431
|
+
await this.request('config/mcpServer/reload', undefined);
|
|
432
|
+
}
|
|
433
|
+
async loginMcpServer(name) {
|
|
434
|
+
const result = await this.request('mcpServer/oauth/login', { name });
|
|
435
|
+
return String(result?.authorizationUrl ?? result?.authorization_url ?? '');
|
|
436
|
+
}
|
|
437
|
+
async readMcpResource(server, uri, threadId) {
|
|
438
|
+
const result = await this.request('mcpServer/resource/read', {
|
|
439
|
+
server,
|
|
440
|
+
uri,
|
|
441
|
+
threadId: threadId ?? null,
|
|
442
|
+
});
|
|
443
|
+
const contents = Array.isArray(result?.contents) ? result.contents : [];
|
|
444
|
+
return contents.map(mapMcpResourceContent);
|
|
445
|
+
}
|
|
446
|
+
async interruptTurn(threadId, turnId) {
|
|
447
|
+
await this.request('turn/interrupt', { threadId, turnId });
|
|
448
|
+
}
|
|
449
|
+
async revealThread(threadId) {
|
|
450
|
+
const url = buildThreadDeepLink(threadId);
|
|
451
|
+
await openUrl(url);
|
|
452
|
+
}
|
|
453
|
+
async respond(requestId, result) {
|
|
454
|
+
this.send({ jsonrpc: '2.0', id: requestId, result });
|
|
455
|
+
}
|
|
456
|
+
async respondError(requestId, message) {
|
|
457
|
+
this.send({ jsonrpc: '2.0', id: requestId, error: { code: -32000, message } });
|
|
458
|
+
}
|
|
459
|
+
async startServer() {
|
|
460
|
+
if (await this.attachPersistedServer()) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (this.autolaunch) {
|
|
464
|
+
const launcher = spawn(this.launchCommand, { shell: true, detached: true, stdio: 'ignore' });
|
|
465
|
+
launcher.unref();
|
|
466
|
+
}
|
|
467
|
+
this.port = await reservePort();
|
|
468
|
+
const [stdoutFd, stderrFd] = this.openServerLogFiles();
|
|
469
|
+
let child;
|
|
470
|
+
try {
|
|
471
|
+
child = spawn(this.codexCliBin, ['app-server', '--listen', `ws://127.0.0.1:${this.port}`], {
|
|
472
|
+
detached: true,
|
|
473
|
+
stdio: ['ignore', stdoutFd, stderrFd],
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
finally {
|
|
477
|
+
fs.closeSync(stdoutFd);
|
|
478
|
+
fs.closeSync(stderrFd);
|
|
479
|
+
}
|
|
480
|
+
child.unref();
|
|
481
|
+
this.child = child;
|
|
482
|
+
if (!child.pid) {
|
|
483
|
+
this.child = null;
|
|
484
|
+
throw new Error('Failed to start codex app-server: child pid is unavailable');
|
|
485
|
+
}
|
|
486
|
+
this.writeServerState({
|
|
487
|
+
pid: child.pid,
|
|
488
|
+
port: this.port,
|
|
489
|
+
command: `${this.codexCliBin} app-server --listen ws://127.0.0.1:${this.port}`,
|
|
490
|
+
logPath: this.serverLogPath,
|
|
491
|
+
bridgePid: process.pid,
|
|
492
|
+
startedAt: new Date().toISOString(),
|
|
493
|
+
});
|
|
494
|
+
child.on('exit', (code, signal) => {
|
|
495
|
+
if (this.child !== child) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
this.child = null;
|
|
499
|
+
if (child.pid) {
|
|
500
|
+
this.clearServerStateForPid(child.pid);
|
|
501
|
+
}
|
|
502
|
+
this.handleDisconnect({ code, signal, source: 'process-exit' });
|
|
503
|
+
});
|
|
504
|
+
child.on('error', (error) => {
|
|
505
|
+
if (this.child !== child) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
this.child = null;
|
|
509
|
+
if (child.pid) {
|
|
510
|
+
this.clearServerStateForPid(child.pid);
|
|
511
|
+
}
|
|
512
|
+
this.handleDisconnect({ error: error.message, source: 'process-error' });
|
|
513
|
+
});
|
|
514
|
+
const spawnFailed = new Promise((_, reject) => {
|
|
515
|
+
child.once('error', reject);
|
|
516
|
+
child.once('exit', (code, signal) => {
|
|
517
|
+
reject(new Error(`codex app-server exited before WebSocket connection: code=${code ?? 'null'} signal=${signal ?? 'null'}`));
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
await Promise.race([this.connectWebSocket(), spawnFailed]);
|
|
521
|
+
await this.initialize();
|
|
522
|
+
}
|
|
523
|
+
async attachPersistedServer() {
|
|
524
|
+
const state = this.readServerState();
|
|
525
|
+
if (!state) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
if (!isProcessAlive(state.pid)) {
|
|
529
|
+
this.clearServerState();
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
this.port = state.port;
|
|
533
|
+
try {
|
|
534
|
+
await this.connectWebSocket();
|
|
535
|
+
await this.initialize();
|
|
536
|
+
this.logger.info('codex.app-server.attached', { pid: state.pid, port: state.port });
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
const socket = this.socket;
|
|
541
|
+
this.socket = null;
|
|
542
|
+
this.connected = false;
|
|
543
|
+
socket?.close();
|
|
544
|
+
this.logger.warn('codex.app-server.attach_failed', {
|
|
545
|
+
pid: state.pid,
|
|
546
|
+
port: state.port,
|
|
547
|
+
error: error instanceof Error ? error.message : String(error),
|
|
548
|
+
});
|
|
549
|
+
this.clearServerStateForPid(state.pid);
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
async connectWebSocket() {
|
|
554
|
+
const url = `ws://127.0.0.1:${this.port}`;
|
|
555
|
+
const started = Date.now();
|
|
556
|
+
while (Date.now() - started < 10_000) {
|
|
557
|
+
try {
|
|
558
|
+
await new Promise((resolve, reject) => {
|
|
559
|
+
const ws = new WebSocket(url);
|
|
560
|
+
const onError = (event) => {
|
|
561
|
+
ws.close();
|
|
562
|
+
reject(new Error(`WebSocket connect failed: ${String(event.type)}`));
|
|
563
|
+
};
|
|
564
|
+
ws.addEventListener('open', () => {
|
|
565
|
+
this.socket = ws;
|
|
566
|
+
this.connected = true;
|
|
567
|
+
ws.addEventListener('message', message => this.handleMessage(String(message.data)));
|
|
568
|
+
ws.addEventListener('close', () => {
|
|
569
|
+
if (this.socket !== ws) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
this.socket = null;
|
|
573
|
+
this.handleDisconnect({ code: 'ws-close', source: 'websocket-close' });
|
|
574
|
+
});
|
|
575
|
+
ws.addEventListener('error', err => {
|
|
576
|
+
this.logger.warn('codex.ws.error', String(err.message ?? 'unknown'));
|
|
577
|
+
});
|
|
578
|
+
resolve();
|
|
579
|
+
}, { once: true });
|
|
580
|
+
ws.addEventListener('error', onError, { once: true });
|
|
581
|
+
});
|
|
582
|
+
this.emit('connected');
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
await sleep(250);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
throw new Error(`Timed out connecting to ${url}`);
|
|
590
|
+
}
|
|
591
|
+
async initialize() {
|
|
592
|
+
const result = await this.request('initialize', {
|
|
593
|
+
clientInfo: {
|
|
594
|
+
name: 'foxclaw',
|
|
595
|
+
title: 'FoxClaw',
|
|
596
|
+
version: '0.2.0',
|
|
597
|
+
},
|
|
598
|
+
capabilities: {
|
|
599
|
+
experimentalApi: true,
|
|
600
|
+
optOutNotificationMethods: [
|
|
601
|
+
'codex/event/agent_reasoning_delta',
|
|
602
|
+
'codex/event/reasoning_content_delta',
|
|
603
|
+
'codex/event/reasoning_raw_content_delta',
|
|
604
|
+
'codex/event/exec_command_output_delta',
|
|
605
|
+
]
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
this.userAgent = result.userAgent ?? null;
|
|
609
|
+
this.send({ jsonrpc: '2.0', method: 'initialized' });
|
|
610
|
+
}
|
|
611
|
+
async request(method, params) {
|
|
612
|
+
if (!this.socket || !this.connected) {
|
|
613
|
+
await this.start();
|
|
614
|
+
}
|
|
615
|
+
const id = String(++this.requestId);
|
|
616
|
+
return new Promise((resolve, reject) => {
|
|
617
|
+
this.pending.set(id, { resolve, reject });
|
|
618
|
+
this.send({ jsonrpc: '2.0', id, method, params });
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
send(payload) {
|
|
622
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
623
|
+
throw new Error('codex app-server socket is not open');
|
|
624
|
+
}
|
|
625
|
+
this.socket.send(JSON.stringify(payload));
|
|
626
|
+
}
|
|
627
|
+
handleMessage(raw) {
|
|
628
|
+
let message;
|
|
629
|
+
try {
|
|
630
|
+
message = JSON.parse(raw);
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
this.logger.warn('codex.message.parse_failed', { raw, error: String(error) });
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if ('id' in message && !('method' in message)) {
|
|
637
|
+
const pending = this.pending.get(String(message.id));
|
|
638
|
+
if (!pending)
|
|
639
|
+
return;
|
|
640
|
+
this.pending.delete(String(message.id));
|
|
641
|
+
if (message.error) {
|
|
642
|
+
pending.reject(new Error(message.error.message || 'JSON-RPC error'));
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
pending.resolve(message.result);
|
|
646
|
+
}
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if ('id' in message && 'method' in message) {
|
|
650
|
+
this.emit('serverRequest', message);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if ('method' in message) {
|
|
654
|
+
this.emit('notification', message);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
handleDisconnect(meta) {
|
|
658
|
+
if (this.connected) {
|
|
659
|
+
this.connected = false;
|
|
660
|
+
}
|
|
661
|
+
this.rejectPending(new Error(`codex app-server disconnected: ${JSON.stringify(meta)}`));
|
|
662
|
+
this.emit('disconnected', meta);
|
|
663
|
+
if (this.desiredRunning) {
|
|
664
|
+
this.scheduleReconnect();
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
rejectPending(error) {
|
|
668
|
+
for (const { reject } of this.pending.values()) {
|
|
669
|
+
reject(error);
|
|
670
|
+
}
|
|
671
|
+
this.pending.clear();
|
|
672
|
+
}
|
|
673
|
+
scheduleReconnect() {
|
|
674
|
+
if (this.reconnectTimer || !this.desiredRunning)
|
|
675
|
+
return;
|
|
676
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
677
|
+
this.reconnectTimer = null;
|
|
678
|
+
try {
|
|
679
|
+
await this.startServer();
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
this.logger.error('codex.reconnect_failed', { error: String(error) });
|
|
683
|
+
this.scheduleReconnect();
|
|
684
|
+
}
|
|
685
|
+
}, 1500);
|
|
686
|
+
}
|
|
687
|
+
async terminateServer() {
|
|
688
|
+
const child = this.child;
|
|
689
|
+
const state = this.readServerState();
|
|
690
|
+
const pid = child?.pid ?? state?.pid ?? null;
|
|
691
|
+
this.child = null;
|
|
692
|
+
this.clearServerState();
|
|
693
|
+
if (pid === null || !isProcessAlive(pid)) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
try {
|
|
697
|
+
process.kill(pid, 'SIGTERM');
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
this.logger.warn('codex.app-server.kill_failed', {
|
|
701
|
+
pid,
|
|
702
|
+
error: error instanceof Error ? error.message : String(error),
|
|
703
|
+
});
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const exited = await waitForProcessExit(pid, 3000);
|
|
707
|
+
if (!exited) {
|
|
708
|
+
this.logger.warn('codex.app-server.kill_timeout', { pid });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
openServerLogFiles() {
|
|
712
|
+
fs.mkdirSync(path.dirname(this.serverLogPath), { recursive: true });
|
|
713
|
+
const stdoutFd = fs.openSync(this.serverLogPath, 'a');
|
|
714
|
+
try {
|
|
715
|
+
return [stdoutFd, fs.openSync(this.serverLogPath, 'a')];
|
|
716
|
+
}
|
|
717
|
+
catch (error) {
|
|
718
|
+
fs.closeSync(stdoutFd);
|
|
719
|
+
throw error;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
readServerState() {
|
|
723
|
+
try {
|
|
724
|
+
const parsed = JSON.parse(fs.readFileSync(this.serverStatePath, 'utf8'));
|
|
725
|
+
if (typeof parsed.pid === 'number'
|
|
726
|
+
&& Number.isInteger(parsed.pid)
|
|
727
|
+
&& parsed.pid > 0
|
|
728
|
+
&& typeof parsed.port === 'number'
|
|
729
|
+
&& Number.isInteger(parsed.port)
|
|
730
|
+
&& parsed.port > 0
|
|
731
|
+
&& parsed.port <= 65535) {
|
|
732
|
+
return {
|
|
733
|
+
pid: parsed.pid,
|
|
734
|
+
port: parsed.port,
|
|
735
|
+
command: typeof parsed.command === 'string' ? parsed.command : '',
|
|
736
|
+
logPath: typeof parsed.logPath === 'string' ? parsed.logPath : this.serverLogPath,
|
|
737
|
+
bridgePid: typeof parsed.bridgePid === 'number' ? parsed.bridgePid : 0,
|
|
738
|
+
startedAt: typeof parsed.startedAt === 'string' ? parsed.startedAt : '',
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
catch {
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
writeServerState(state) {
|
|
748
|
+
fs.mkdirSync(path.dirname(this.serverStatePath), { recursive: true });
|
|
749
|
+
const tmp = `${this.serverStatePath}.tmp`;
|
|
750
|
+
fs.writeFileSync(tmp, JSON.stringify(state, null, 2), 'utf8');
|
|
751
|
+
fs.renameSync(tmp, this.serverStatePath);
|
|
752
|
+
}
|
|
753
|
+
clearServerState() {
|
|
754
|
+
try {
|
|
755
|
+
fs.unlinkSync(this.serverStatePath);
|
|
756
|
+
}
|
|
757
|
+
catch (error) {
|
|
758
|
+
if (error.code !== 'ENOENT') {
|
|
759
|
+
this.logger.warn('codex.app-server.state_clear_failed', {
|
|
760
|
+
error: error instanceof Error ? error.message : String(error),
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
clearServerStateForPid(pid) {
|
|
766
|
+
const state = this.readServerState();
|
|
767
|
+
if (!state || state.pid !== pid) {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
this.clearServerState();
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
async function reservePort() {
|
|
774
|
+
return new Promise((resolve, reject) => {
|
|
775
|
+
const server = net.createServer();
|
|
776
|
+
server.listen(0, '127.0.0.1', () => {
|
|
777
|
+
const address = server.address();
|
|
778
|
+
if (!address || typeof address === 'string') {
|
|
779
|
+
reject(new Error('Failed to reserve TCP port'));
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const port = address.port;
|
|
783
|
+
server.close(() => resolve(port));
|
|
784
|
+
});
|
|
785
|
+
server.on('error', reject);
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
function sleep(ms) {
|
|
789
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
790
|
+
}
|
|
791
|
+
function isProcessAlive(pid) {
|
|
792
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
try {
|
|
796
|
+
process.kill(pid, 0);
|
|
797
|
+
return true;
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
return error.code === 'EPERM';
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
804
|
+
const started = Date.now();
|
|
805
|
+
while (Date.now() - started < timeoutMs) {
|
|
806
|
+
if (!isProcessAlive(pid)) {
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
await sleep(100);
|
|
810
|
+
}
|
|
811
|
+
return !isProcessAlive(pid);
|
|
812
|
+
}
|
|
813
|
+
function mapThread(raw) {
|
|
814
|
+
return {
|
|
815
|
+
threadId: String(raw.id),
|
|
816
|
+
name: raw.name ? String(raw.name) : null,
|
|
817
|
+
preview: String(raw.preview || '(empty)'),
|
|
818
|
+
cwd: raw.cwd ? String(raw.cwd) : null,
|
|
819
|
+
modelProvider: raw.modelProvider ? String(raw.modelProvider) : null,
|
|
820
|
+
source: raw.source ? String(raw.source) : null,
|
|
821
|
+
path: raw.path ? String(raw.path) : null,
|
|
822
|
+
status: mapThreadStatus(raw.status),
|
|
823
|
+
updatedAt: Number(raw.updatedAt || 0),
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function mapThreadSnapshot(raw) {
|
|
827
|
+
const base = mapThread(raw);
|
|
828
|
+
const activeFlags = Array.isArray(raw?.status?.activeFlags)
|
|
829
|
+
? raw.status.activeFlags
|
|
830
|
+
.filter((entry) => typeof entry === 'string')
|
|
831
|
+
: [];
|
|
832
|
+
const turns = Array.isArray(raw?.turns)
|
|
833
|
+
? raw.turns.map(mapTurnSnapshot)
|
|
834
|
+
: [];
|
|
835
|
+
return {
|
|
836
|
+
...base,
|
|
837
|
+
activeFlags,
|
|
838
|
+
turns,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
function mapThreadStatus(raw) {
|
|
842
|
+
const type = raw?.type;
|
|
843
|
+
if (type === 'active' || type === 'idle' || type === 'notLoaded' || type === 'systemError') {
|
|
844
|
+
return type;
|
|
845
|
+
}
|
|
846
|
+
return 'idle';
|
|
847
|
+
}
|
|
848
|
+
function mapThreadSessionState(raw) {
|
|
849
|
+
return {
|
|
850
|
+
thread: mapThread(raw.thread),
|
|
851
|
+
model: String(raw.model ?? ''),
|
|
852
|
+
modelProvider: String(raw.modelProvider ?? ''),
|
|
853
|
+
reasoningEffort: raw.reasoningEffort === null ? null : String(raw.reasoningEffort),
|
|
854
|
+
cwd: String(raw.cwd ?? ''),
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
function mapTurnSnapshot(raw) {
|
|
858
|
+
return {
|
|
859
|
+
turnId: String(raw?.id || ''),
|
|
860
|
+
status: String(raw?.status || 'unknown'),
|
|
861
|
+
error: raw?.error ? String(raw.error) : null,
|
|
862
|
+
items: Array.isArray(raw?.items) ? raw.items.map(mapTurnItemSnapshot) : [],
|
|
863
|
+
itemsView: typeof raw?.itemsView === 'string' ? raw.itemsView : null,
|
|
864
|
+
startedAt: numberOrNull(raw?.startedAt),
|
|
865
|
+
completedAt: numberOrNull(raw?.completedAt),
|
|
866
|
+
durationMs: numberOrNull(raw?.durationMs),
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
function mapTurnItemSnapshot(raw) {
|
|
870
|
+
return {
|
|
871
|
+
itemId: String(raw?.id || ''),
|
|
872
|
+
type: String(raw?.type || ''),
|
|
873
|
+
phase: raw?.phase ? String(raw.phase) : null,
|
|
874
|
+
text: raw?.text ? String(raw.text) : null,
|
|
875
|
+
command: raw?.command ? String(raw.command) : null,
|
|
876
|
+
status: raw?.status ? String(raw.status) : null,
|
|
877
|
+
aggregatedOutput: raw?.aggregatedOutput ? String(raw.aggregatedOutput) : null,
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
function mapModel(raw) {
|
|
881
|
+
const efforts = Array.isArray(raw.supportedReasoningEfforts)
|
|
882
|
+
? raw.supportedReasoningEfforts
|
|
883
|
+
.map((entry) => entry?.reasoningEffort)
|
|
884
|
+
.filter((value) => typeof value === 'string')
|
|
885
|
+
: [];
|
|
886
|
+
const rawServiceTiers = Array.isArray(raw.serviceTiers)
|
|
887
|
+
? raw.serviceTiers
|
|
888
|
+
: Array.isArray(raw.service_tiers)
|
|
889
|
+
? raw.service_tiers
|
|
890
|
+
: [];
|
|
891
|
+
const serviceTiers = rawServiceTiers.length > 0
|
|
892
|
+
? rawServiceTiers
|
|
893
|
+
.map((entry) => ({
|
|
894
|
+
id: typeof entry?.id === 'string' ? entry.id : '',
|
|
895
|
+
name: typeof entry?.name === 'string' ? entry.name : '',
|
|
896
|
+
description: typeof entry?.description === 'string' ? entry.description : '',
|
|
897
|
+
}))
|
|
898
|
+
.filter((entry) => entry.id.length > 0)
|
|
899
|
+
: [];
|
|
900
|
+
return {
|
|
901
|
+
id: String(raw.id),
|
|
902
|
+
model: String(raw.model),
|
|
903
|
+
displayName: String(raw.displayName || raw.model),
|
|
904
|
+
description: String(raw.description || ''),
|
|
905
|
+
isDefault: Boolean(raw.isDefault),
|
|
906
|
+
supportedReasoningEfforts: efforts,
|
|
907
|
+
defaultReasoningEffort: String(raw.defaultReasoningEffort),
|
|
908
|
+
serviceTiers,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
function mapThreadGoal(raw) {
|
|
912
|
+
if (!raw || typeof raw !== 'object') {
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
const status = normalizeThreadGoalStatus(raw.status);
|
|
916
|
+
return {
|
|
917
|
+
threadId: String(raw.threadId ?? raw.thread_id ?? ''),
|
|
918
|
+
objective: String(raw.objective ?? ''),
|
|
919
|
+
status,
|
|
920
|
+
tokenBudget: numberOrNull(raw.tokenBudget ?? raw.token_budget),
|
|
921
|
+
tokensUsed: numberOrNull(raw.tokensUsed ?? raw.tokens_used) ?? 0,
|
|
922
|
+
timeUsedSeconds: numberOrNull(raw.timeUsedSeconds ?? raw.time_used_seconds) ?? 0,
|
|
923
|
+
createdAt: numberOrNull(raw.createdAt ?? raw.created_at) ?? 0,
|
|
924
|
+
updatedAt: numberOrNull(raw.updatedAt ?? raw.updated_at) ?? 0,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
function mapFuzzyFileResult(raw) {
|
|
928
|
+
return {
|
|
929
|
+
root: String(raw?.root ?? ''),
|
|
930
|
+
path: String(raw?.path ?? ''),
|
|
931
|
+
matchType: formatRawLabel(raw?.match_type ?? raw?.matchType),
|
|
932
|
+
fileName: String(raw?.file_name ?? raw?.fileName ?? ''),
|
|
933
|
+
score: numberOrNull(raw?.score) ?? 0,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function mapRateLimitsByLimitId(raw) {
|
|
937
|
+
if (!raw || typeof raw !== 'object') {
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
const entries = Object.entries(raw)
|
|
941
|
+
.map(([key, value]) => [key, mapRateLimitSnapshot(value)])
|
|
942
|
+
.filter((entry) => entry[1] !== null);
|
|
943
|
+
return entries.length > 0 ? Object.fromEntries(entries) : null;
|
|
944
|
+
}
|
|
945
|
+
function mapCollaborationModePreset(raw) {
|
|
946
|
+
return {
|
|
947
|
+
name: String(raw?.name ?? ''),
|
|
948
|
+
mode: normalizeCollaborationMode(raw?.mode),
|
|
949
|
+
model: typeof raw?.model === 'string' ? raw.model : null,
|
|
950
|
+
reasoningEffort: normalizeReasoningEffort(raw?.reasoning_effort),
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
function mapSkillsListEntry(raw) {
|
|
954
|
+
const skills = Array.isArray(raw?.skills) ? raw.skills : [];
|
|
955
|
+
const errors = Array.isArray(raw?.errors) ? raw.errors : [];
|
|
956
|
+
return {
|
|
957
|
+
cwd: String(raw?.cwd ?? ''),
|
|
958
|
+
skills: skills.map(mapSkillMetadata),
|
|
959
|
+
errors: errors.map((entry) => formatRawError(entry)),
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
function mapSkillMetadata(raw) {
|
|
963
|
+
const iface = raw?.interface && typeof raw.interface === 'object' ? raw.interface : {};
|
|
964
|
+
return {
|
|
965
|
+
name: String(raw?.name ?? ''),
|
|
966
|
+
description: String(raw?.description ?? ''),
|
|
967
|
+
shortDescription: typeof raw?.shortDescription === 'string' ? raw.shortDescription : null,
|
|
968
|
+
path: String(raw?.path ?? ''),
|
|
969
|
+
scope: String(raw?.scope ?? ''),
|
|
970
|
+
enabled: Boolean(raw?.enabled),
|
|
971
|
+
displayName: typeof iface.displayName === 'string' ? iface.displayName : null,
|
|
972
|
+
defaultPrompt: typeof iface.defaultPrompt === 'string' ? iface.defaultPrompt : null,
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
function mapHooksListEntry(raw) {
|
|
976
|
+
const hooks = Array.isArray(raw?.hooks) ? raw.hooks : [];
|
|
977
|
+
const errors = Array.isArray(raw?.errors) ? raw.errors : [];
|
|
978
|
+
const warnings = Array.isArray(raw?.warnings) ? raw.warnings : [];
|
|
979
|
+
return {
|
|
980
|
+
cwd: String(raw?.cwd ?? ''),
|
|
981
|
+
hooks: hooks.map(mapHookMetadata),
|
|
982
|
+
errors: errors.map((entry) => ({
|
|
983
|
+
path: String(entry?.path ?? ''),
|
|
984
|
+
message: String(entry?.message ?? formatRawError(entry)),
|
|
985
|
+
})),
|
|
986
|
+
warnings: warnings.map((entry) => String(entry)),
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
function mapHookMetadata(raw) {
|
|
990
|
+
return {
|
|
991
|
+
key: String(raw?.key ?? ''),
|
|
992
|
+
eventName: formatRawLabel(raw?.eventName),
|
|
993
|
+
handlerType: formatRawLabel(raw?.handlerType),
|
|
994
|
+
enabled: Boolean(raw?.enabled),
|
|
995
|
+
trustStatus: formatRawLabel(raw?.trustStatus),
|
|
996
|
+
sourcePath: String(raw?.sourcePath ?? ''),
|
|
997
|
+
pluginId: typeof raw?.pluginId === 'string' ? raw.pluginId : null,
|
|
998
|
+
command: typeof raw?.command === 'string' ? raw.command : null,
|
|
999
|
+
statusMessage: typeof raw?.statusMessage === 'string' ? raw.statusMessage : null,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
function mapPluginMarketplace(raw) {
|
|
1003
|
+
const iface = raw?.interface && typeof raw.interface === 'object' ? raw.interface : {};
|
|
1004
|
+
const plugins = Array.isArray(raw?.plugins) ? raw.plugins : [];
|
|
1005
|
+
return {
|
|
1006
|
+
name: String(raw?.name ?? ''),
|
|
1007
|
+
displayName: typeof iface.displayName === 'string' ? iface.displayName : null,
|
|
1008
|
+
path: typeof raw?.path === 'string' ? raw.path : null,
|
|
1009
|
+
plugins: plugins.map(mapPluginSummary),
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
function mapPluginSummary(raw) {
|
|
1013
|
+
const keywords = Array.isArray(raw?.keywords) ? raw.keywords : [];
|
|
1014
|
+
return {
|
|
1015
|
+
id: String(raw?.id ?? ''),
|
|
1016
|
+
name: String(raw?.name ?? raw?.id ?? ''),
|
|
1017
|
+
enabled: Boolean(raw?.enabled),
|
|
1018
|
+
installed: Boolean(raw?.installed),
|
|
1019
|
+
source: formatRawLabel(raw?.source),
|
|
1020
|
+
availability: formatRawLabel(raw?.availability),
|
|
1021
|
+
authPolicy: formatRawLabel(raw?.authPolicy),
|
|
1022
|
+
installPolicy: formatRawLabel(raw?.installPolicy),
|
|
1023
|
+
keywords: keywords.map((entry) => String(entry)),
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
function mapPluginSkillSummary(raw) {
|
|
1027
|
+
return {
|
|
1028
|
+
name: String(raw?.name ?? ''),
|
|
1029
|
+
description: String(raw?.description ?? ''),
|
|
1030
|
+
shortDescription: typeof raw?.shortDescription === 'string' ? raw.shortDescription : null,
|
|
1031
|
+
enabled: Boolean(raw?.enabled),
|
|
1032
|
+
path: typeof raw?.path === 'string' ? raw.path : null,
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
function mapPluginDetail(raw) {
|
|
1036
|
+
const skills = Array.isArray(raw?.skills) ? raw.skills : [];
|
|
1037
|
+
const hooks = Array.isArray(raw?.hooks) ? raw.hooks : [];
|
|
1038
|
+
const apps = Array.isArray(raw?.apps) ? raw.apps : [];
|
|
1039
|
+
const mcpServers = Array.isArray(raw?.mcpServers) ? raw.mcpServers : [];
|
|
1040
|
+
return {
|
|
1041
|
+
marketplaceName: String(raw?.marketplaceName ?? ''),
|
|
1042
|
+
marketplacePath: typeof raw?.marketplacePath === 'string' ? raw.marketplacePath : null,
|
|
1043
|
+
summary: mapPluginSummary(raw?.summary),
|
|
1044
|
+
description: typeof raw?.description === 'string' ? raw.description : null,
|
|
1045
|
+
skills: skills.map(mapPluginSkillSummary),
|
|
1046
|
+
hooks: hooks.map((entry) => ({ key: String(entry?.key ?? ''), eventName: formatRawLabel(entry?.eventName) })),
|
|
1047
|
+
apps: apps.map((entry) => ({
|
|
1048
|
+
id: String(entry?.id ?? ''),
|
|
1049
|
+
name: String(entry?.name ?? ''),
|
|
1050
|
+
description: typeof entry?.description === 'string' ? entry.description : null,
|
|
1051
|
+
needsAuth: Boolean(entry?.needsAuth),
|
|
1052
|
+
})),
|
|
1053
|
+
mcpServers: mcpServers.map((entry) => String(entry)),
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
function mapAppInfo(raw) {
|
|
1057
|
+
const plugins = Array.isArray(raw?.pluginDisplayNames) ? raw.pluginDisplayNames : [];
|
|
1058
|
+
return {
|
|
1059
|
+
id: String(raw?.id ?? ''),
|
|
1060
|
+
name: String(raw?.name ?? raw?.id ?? ''),
|
|
1061
|
+
description: typeof raw?.description === 'string' ? raw.description : null,
|
|
1062
|
+
isEnabled: raw?.isEnabled !== false,
|
|
1063
|
+
isAccessible: Boolean(raw?.isAccessible),
|
|
1064
|
+
installUrl: typeof raw?.installUrl === 'string' ? raw.installUrl : null,
|
|
1065
|
+
distributionChannel: typeof raw?.distributionChannel === 'string' ? raw.distributionChannel : null,
|
|
1066
|
+
pluginDisplayNames: plugins.map((entry) => String(entry)),
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
function mapExperimentalFeature(raw) {
|
|
1070
|
+
return {
|
|
1071
|
+
name: String(raw?.name ?? ''),
|
|
1072
|
+
displayName: typeof raw?.displayName === 'string' ? raw.displayName : null,
|
|
1073
|
+
enabled: Boolean(raw?.enabled),
|
|
1074
|
+
defaultEnabled: Boolean(raw?.defaultEnabled),
|
|
1075
|
+
stage: formatRawLabel(raw?.stage),
|
|
1076
|
+
description: typeof raw?.description === 'string' ? raw.description : null,
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
function mapConfigRequirements(raw) {
|
|
1080
|
+
return {
|
|
1081
|
+
allowedApprovalPolicies: stringArrayOrNull(raw?.allowedApprovalPolicies),
|
|
1082
|
+
allowedSandboxModes: stringArrayOrNull(raw?.allowedSandboxModes),
|
|
1083
|
+
allowedWebSearchModes: stringArrayOrNull(raw?.allowedWebSearchModes),
|
|
1084
|
+
enforceResidency: raw?.enforceResidency === null || raw?.enforceResidency === undefined ? null : formatRawLabel(raw.enforceResidency),
|
|
1085
|
+
featureRequirements: raw?.featureRequirements && typeof raw.featureRequirements === 'object'
|
|
1086
|
+
? Object.fromEntries(Object.entries(raw.featureRequirements).map(([key, value]) => [key, Boolean(value)]))
|
|
1087
|
+
: null,
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
function mapMcpServerStatus(raw) {
|
|
1091
|
+
const tools = raw?.tools && typeof raw.tools === 'object' ? Object.keys(raw.tools) : [];
|
|
1092
|
+
const resources = Array.isArray(raw?.resources) ? raw.resources : [];
|
|
1093
|
+
const templates = Array.isArray(raw?.resourceTemplates) ? raw.resourceTemplates : [];
|
|
1094
|
+
return {
|
|
1095
|
+
name: String(raw?.name ?? ''),
|
|
1096
|
+
authStatus: formatMcpAuthStatus(raw?.authStatus),
|
|
1097
|
+
toolNames: tools.sort((left, right) => left.localeCompare(right)),
|
|
1098
|
+
resourceUris: resources
|
|
1099
|
+
.map((entry) => typeof entry?.uri === 'string' ? entry.uri : null)
|
|
1100
|
+
.filter((entry) => entry !== null),
|
|
1101
|
+
resourceTemplateUris: templates
|
|
1102
|
+
.map((entry) => typeof entry?.uriTemplate === 'string'
|
|
1103
|
+
? entry.uriTemplate
|
|
1104
|
+
: typeof entry?.uri_template === 'string'
|
|
1105
|
+
? entry.uri_template
|
|
1106
|
+
: null)
|
|
1107
|
+
.filter((entry) => entry !== null),
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
function mapMcpResourceContent(raw) {
|
|
1111
|
+
return {
|
|
1112
|
+
type: typeof raw?.text === 'string' ? 'text' : typeof raw?.blob === 'string' ? 'blob' : 'unknown',
|
|
1113
|
+
text: typeof raw?.text === 'string' ? raw.text : null,
|
|
1114
|
+
blob: typeof raw?.blob === 'string' ? raw.blob : null,
|
|
1115
|
+
mimeType: typeof raw?.mimeType === 'string' ? raw.mimeType : null,
|
|
1116
|
+
uri: typeof raw?.uri === 'string' ? raw.uri : null,
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
function formatMcpAuthStatus(raw) {
|
|
1120
|
+
if (raw === null || raw === undefined) {
|
|
1121
|
+
return 'unknown';
|
|
1122
|
+
}
|
|
1123
|
+
if (typeof raw === 'string') {
|
|
1124
|
+
return raw;
|
|
1125
|
+
}
|
|
1126
|
+
if (typeof raw?.status === 'string') {
|
|
1127
|
+
return raw.status;
|
|
1128
|
+
}
|
|
1129
|
+
if (typeof raw?.type === 'string') {
|
|
1130
|
+
return raw.type;
|
|
1131
|
+
}
|
|
1132
|
+
return JSON.stringify(raw);
|
|
1133
|
+
}
|
|
1134
|
+
function formatRawError(raw) {
|
|
1135
|
+
if (raw instanceof Error) {
|
|
1136
|
+
return raw.message;
|
|
1137
|
+
}
|
|
1138
|
+
if (typeof raw === 'string') {
|
|
1139
|
+
return raw;
|
|
1140
|
+
}
|
|
1141
|
+
if (raw && typeof raw === 'object' && 'message' in raw && typeof raw.message === 'string') {
|
|
1142
|
+
return raw.message;
|
|
1143
|
+
}
|
|
1144
|
+
return JSON.stringify(raw);
|
|
1145
|
+
}
|
|
1146
|
+
function stringArrayOrNull(raw) {
|
|
1147
|
+
return Array.isArray(raw) ? raw.map((entry) => formatRawLabel(entry)) : null;
|
|
1148
|
+
}
|
|
1149
|
+
function formatRawLabel(raw) {
|
|
1150
|
+
if (raw === null || raw === undefined) {
|
|
1151
|
+
return 'unknown';
|
|
1152
|
+
}
|
|
1153
|
+
if (typeof raw === 'string') {
|
|
1154
|
+
return raw;
|
|
1155
|
+
}
|
|
1156
|
+
if (typeof raw === 'number' || typeof raw === 'boolean') {
|
|
1157
|
+
return String(raw);
|
|
1158
|
+
}
|
|
1159
|
+
if (typeof raw === 'object') {
|
|
1160
|
+
const keys = Object.keys(raw);
|
|
1161
|
+
if (keys.length === 1) {
|
|
1162
|
+
return keys[0];
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return JSON.stringify(raw);
|
|
1166
|
+
}
|
|
1167
|
+
function normalizeCollaborationMode(value) {
|
|
1168
|
+
return value === 'default' || value === 'plan' ? value : null;
|
|
1169
|
+
}
|
|
1170
|
+
function normalizeReasoningEffort(value) {
|
|
1171
|
+
return typeof value === 'string' && ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'].includes(value)
|
|
1172
|
+
? value
|
|
1173
|
+
: null;
|
|
1174
|
+
}
|
|
1175
|
+
function normalizeThreadGoalStatus(value) {
|
|
1176
|
+
return value === 'paused' || value === 'budgetLimited' || value === 'complete'
|
|
1177
|
+
? value
|
|
1178
|
+
: 'active';
|
|
1179
|
+
}
|
|
1180
|
+
function mapRateLimitSnapshot(raw) {
|
|
1181
|
+
if (!raw || typeof raw !== 'object') {
|
|
1182
|
+
return null;
|
|
1183
|
+
}
|
|
1184
|
+
return {
|
|
1185
|
+
limitId: typeof raw.limitId === 'string' ? raw.limitId : null,
|
|
1186
|
+
limitName: typeof raw.limitName === 'string' ? raw.limitName : null,
|
|
1187
|
+
primary: mapRateLimitWindow(raw.primary),
|
|
1188
|
+
secondary: mapRateLimitWindow(raw.secondary),
|
|
1189
|
+
credits: mapCreditsSnapshot(raw.credits),
|
|
1190
|
+
planType: typeof raw.planType === 'string' ? raw.planType : null,
|
|
1191
|
+
rateLimitReachedType: typeof raw.rateLimitReachedType === 'string' ? raw.rateLimitReachedType : null,
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
function mapRateLimitWindow(raw) {
|
|
1195
|
+
if (!raw || typeof raw !== 'object') {
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
const usedPercent = Number(raw.usedPercent);
|
|
1199
|
+
return {
|
|
1200
|
+
usedPercent: Number.isFinite(usedPercent) ? usedPercent : 0,
|
|
1201
|
+
windowDurationMins: numberOrNull(raw.windowDurationMins),
|
|
1202
|
+
resetsAt: numberOrNull(raw.resetsAt),
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
function mapCreditsSnapshot(raw) {
|
|
1206
|
+
if (!raw || typeof raw !== 'object') {
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
return {
|
|
1210
|
+
hasCredits: Boolean(raw.hasCredits),
|
|
1211
|
+
unlimited: Boolean(raw.unlimited),
|
|
1212
|
+
balance: raw.balance === null || raw.balance === undefined ? null : String(raw.balance),
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
function numberOrNull(value) {
|
|
1216
|
+
if (value === null || value === undefined) {
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
const numberValue = Number(value);
|
|
1220
|
+
return Number.isFinite(numberValue) ? numberValue : null;
|
|
1221
|
+
}
|
|
1222
|
+
function mapSandboxPolicy(mode) {
|
|
1223
|
+
if (mode === 'read-only') {
|
|
1224
|
+
return { type: 'readOnly' };
|
|
1225
|
+
}
|
|
1226
|
+
if (mode === 'danger-full-access') {
|
|
1227
|
+
return { type: 'dangerFullAccess' };
|
|
1228
|
+
}
|
|
1229
|
+
return { type: 'workspaceWrite' };
|
|
1230
|
+
}
|