codex-to-poke 0.1.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/LICENSE +21 -0
- package/README.md +24 -0
- package/dist/agent.cjs +23425 -0
- package/dist/relay.cjs +47535 -0
- package/package.json +36 -0
- package/src/index.js +825 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
import { dirname, join, resolve } from 'node:path';
|
|
8
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const command = ['help', '--help', '-h'].includes(args[0] ?? '')
|
|
13
|
+
? 'help'
|
|
14
|
+
: args[0]?.startsWith('-')
|
|
15
|
+
? 'local'
|
|
16
|
+
: (args[0] ?? 'local');
|
|
17
|
+
const invocationCwd = process.env.INIT_CWD ?? process.cwd();
|
|
18
|
+
const managedChildren = new Map();
|
|
19
|
+
const serviceLogs = new Map();
|
|
20
|
+
const statuses = { relay: 'idle', agent: 'idle', poke: 'idle' };
|
|
21
|
+
const bannerText = [
|
|
22
|
+
'▄▄▄▄ ▄▄▄ ▄▄ ▄▄ ▄▄▄▄▄ ▄▄▄▄ ▄▄▄▄▄ ▄▄ ▄▄ ',
|
|
23
|
+
'██▄█▀ ██▀██ ██▄█▀ ██▄▄ ██▀██ ██▄▄ ▀█▄█▀ ',
|
|
24
|
+
'██ ▀███▀ ██ ██ ██▄▄▄ ████▀ ██▄▄▄ ██ ██ ',
|
|
25
|
+
].join('\n');
|
|
26
|
+
let configPath = '';
|
|
27
|
+
let config = {};
|
|
28
|
+
let readline = null;
|
|
29
|
+
let stopping = false;
|
|
30
|
+
let restarting = false;
|
|
31
|
+
|
|
32
|
+
if (command === 'help') help();
|
|
33
|
+
else if (command === 'local') await local();
|
|
34
|
+
else die(`unknown command: ${command}`);
|
|
35
|
+
|
|
36
|
+
async function local() {
|
|
37
|
+
configPath = defaultConfigPath();
|
|
38
|
+
config = createConfig(loadSavedConfig());
|
|
39
|
+
saveConfig();
|
|
40
|
+
|
|
41
|
+
printBanner();
|
|
42
|
+
|
|
43
|
+
registerSignals();
|
|
44
|
+
try {
|
|
45
|
+
await startStack();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(formatError(error));
|
|
48
|
+
await stopStack(false);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
startConsole();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createConfig(saved) {
|
|
55
|
+
const first = firstWorkspace(saved);
|
|
56
|
+
const readOnly = has('--read-only');
|
|
57
|
+
const writeEnabled = readOnly
|
|
58
|
+
? false
|
|
59
|
+
: has('--write') || has('--full-access')
|
|
60
|
+
? true
|
|
61
|
+
: (saved.writeTasksEnabled ?? first.allowWrite ?? false);
|
|
62
|
+
const fullAccess = readOnly
|
|
63
|
+
? false
|
|
64
|
+
: has('--full-access')
|
|
65
|
+
? true
|
|
66
|
+
: has('--write')
|
|
67
|
+
? false
|
|
68
|
+
: (saved.fullAccessEnabled ?? first.allowFullAccess ?? false);
|
|
69
|
+
const alias = value('--alias') ?? first.alias ?? 'main';
|
|
70
|
+
const root = value('--workspace')
|
|
71
|
+
? resolveUserPath(value('--workspace'))
|
|
72
|
+
: (first.root ?? resolveUserPath('.'));
|
|
73
|
+
const workspaces = normalizeWorkspaces(saved.workspaces, {
|
|
74
|
+
alias,
|
|
75
|
+
root,
|
|
76
|
+
writeEnabled,
|
|
77
|
+
fullAccess,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
upsertWorkspace(workspaces, {
|
|
81
|
+
alias,
|
|
82
|
+
root,
|
|
83
|
+
description: first.description ?? `${alias} workspace`,
|
|
84
|
+
allowWrite: writeEnabled,
|
|
85
|
+
allowFullAccess: fullAccess,
|
|
86
|
+
defaultSandbox: sandboxFor(writeEnabled, fullAccess),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
port: String(value('--port') ?? saved.port ?? '3000'),
|
|
91
|
+
userId: value('--user-id') ?? saved.userId ?? 'local',
|
|
92
|
+
relayUrl: `ws://127.0.0.1:${value('--port') ?? saved.port ?? '3000'}/agent`,
|
|
93
|
+
relayToken: value('--token') ?? saved.relayToken ?? randomHex(),
|
|
94
|
+
appServerCommand: value('--codex') ?? saved.appServerCommand ?? 'codex',
|
|
95
|
+
appServerArgs: Array.isArray(saved.appServerArgs)
|
|
96
|
+
? saved.appServerArgs
|
|
97
|
+
: ['app-server', '--listen', 'stdio://'],
|
|
98
|
+
defaultModel: value('--model') ?? saved.defaultModel ?? 'gpt-5.5',
|
|
99
|
+
defaultReasoning: value('--reasoning') ?? saved.defaultReasoning ?? 'medium',
|
|
100
|
+
defaultVerbosity: value('--verbosity') ?? saved.defaultVerbosity ?? 'medium',
|
|
101
|
+
defaultApprovalPolicy: value('--approval') ?? saved.defaultApprovalPolicy ?? 'on-request',
|
|
102
|
+
writeTasksEnabled: writeEnabled,
|
|
103
|
+
fullAccessEnabled: fullAccess,
|
|
104
|
+
workspaces,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeWorkspaces(workspaces, defaults) {
|
|
109
|
+
const items = Array.isArray(workspaces) ? workspaces : [];
|
|
110
|
+
const normalized = items
|
|
111
|
+
.filter(
|
|
112
|
+
(workspace) => workspace && typeof workspace === 'object' && workspace.alias && workspace.root
|
|
113
|
+
)
|
|
114
|
+
.map((workspace) => ({
|
|
115
|
+
alias: String(workspace.alias),
|
|
116
|
+
root: String(workspace.root),
|
|
117
|
+
description: workspace.description
|
|
118
|
+
? String(workspace.description)
|
|
119
|
+
: `${workspace.alias} workspace`,
|
|
120
|
+
allowWrite: Boolean(workspace.allowWrite),
|
|
121
|
+
allowFullAccess: Boolean(workspace.allowFullAccess),
|
|
122
|
+
defaultSandbox:
|
|
123
|
+
workspace.defaultSandbox ?? sandboxFor(workspace.allowWrite, workspace.allowFullAccess),
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
if (normalized.length) return normalized;
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
alias: defaults.alias,
|
|
130
|
+
root: defaults.root,
|
|
131
|
+
description: `${defaults.alias} workspace`,
|
|
132
|
+
allowWrite: defaults.writeEnabled,
|
|
133
|
+
allowFullAccess: defaults.fullAccess,
|
|
134
|
+
defaultSandbox: sandboxFor(defaults.writeEnabled, defaults.fullAccess),
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function startStack() {
|
|
140
|
+
await stopStack(false);
|
|
141
|
+
|
|
142
|
+
statuses.relay = 'starting';
|
|
143
|
+
spawnManaged('relay', 'pokedex-relay', ['--config', configPath]);
|
|
144
|
+
await waitForRelay();
|
|
145
|
+
statuses.relay = 'ok';
|
|
146
|
+
|
|
147
|
+
statuses.agent = 'starting';
|
|
148
|
+
spawnManaged('agent', 'pokedex-agent', ['--config', configPath]);
|
|
149
|
+
await waitForAgent();
|
|
150
|
+
statuses.agent = 'ok';
|
|
151
|
+
|
|
152
|
+
statuses.poke = 'starting';
|
|
153
|
+
spawnManaged('poke', npxBin(), ['poke@latest', 'tunnel', mcpHttpUrlWithToken(), '-n', 'pokedex']);
|
|
154
|
+
await waitForPoke();
|
|
155
|
+
statuses.poke = 'ok';
|
|
156
|
+
console.log("✅ Everything's fine. pokedex is ready.");
|
|
157
|
+
console.log('Type "help" for commands. Keep this terminal open while you use Poke.\n');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function waitForRelay() {
|
|
161
|
+
await waitFor(
|
|
162
|
+
async () => {
|
|
163
|
+
const health = await fetchJson(`http://127.0.0.1:${config.port}/health`);
|
|
164
|
+
return health?.ok === true;
|
|
165
|
+
},
|
|
166
|
+
10_000,
|
|
167
|
+
'relay did not become healthy',
|
|
168
|
+
'relay'
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function waitForAgent() {
|
|
173
|
+
await waitFor(
|
|
174
|
+
async () => {
|
|
175
|
+
const health = await fetchJson(`http://127.0.0.1:${config.port}/health`);
|
|
176
|
+
return Number(health?.agents ?? 0) > 0;
|
|
177
|
+
},
|
|
178
|
+
15_000,
|
|
179
|
+
'agent did not connect to relay',
|
|
180
|
+
'agent'
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function waitForPoke() {
|
|
185
|
+
await sleep(1400);
|
|
186
|
+
const child = managedChildren.get('poke')?.child;
|
|
187
|
+
if (!child || child.exitCode !== null || child.killed)
|
|
188
|
+
throw serviceFailure('poke', 'Poke stopped before the tunnel was ready.');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function waitFor(check, timeoutMs, message, serviceName) {
|
|
192
|
+
const deadline = Date.now() + timeoutMs;
|
|
193
|
+
while (Date.now() < deadline) {
|
|
194
|
+
if (serviceName) failIfServiceExited(serviceName, message);
|
|
195
|
+
if (await check()) return;
|
|
196
|
+
await sleep(250);
|
|
197
|
+
}
|
|
198
|
+
throw serviceFailure(serviceName, message);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function startConsole() {
|
|
202
|
+
readline = createInterface({
|
|
203
|
+
input: process.stdin,
|
|
204
|
+
output: process.stdout,
|
|
205
|
+
prompt: 'pokedex> ',
|
|
206
|
+
});
|
|
207
|
+
readline.on('line', (line) => void runConsoleCommand(line));
|
|
208
|
+
readline.on('close', () => void stopManaged(0));
|
|
209
|
+
readline.prompt();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function runConsoleCommand(line) {
|
|
213
|
+
readline?.pause();
|
|
214
|
+
try {
|
|
215
|
+
await handleCommand(splitCommand(line.trim()));
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error(formatError(error));
|
|
218
|
+
} finally {
|
|
219
|
+
if (!stopping) {
|
|
220
|
+
readline?.resume();
|
|
221
|
+
readline?.prompt();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function handleCommand(parts) {
|
|
227
|
+
const [name, subcommand, ...rest] = parts;
|
|
228
|
+
if (!name) return printStatus();
|
|
229
|
+
if (['q', 'quit', 'exit'].includes(name)) return await stopManaged(0);
|
|
230
|
+
if (name === 'help') return printInteractiveHelp();
|
|
231
|
+
if (name === 'status') return printStatus();
|
|
232
|
+
if (name === 'config') return printConfig();
|
|
233
|
+
if (name === 'output') return printServiceOutput(subcommand);
|
|
234
|
+
if (name === 'restart') return await saveAndRestart('restarting stack');
|
|
235
|
+
if (name === 'write') return await setWrite(subcommand);
|
|
236
|
+
if (name === 'full-access') return await setFullAccess(subcommand);
|
|
237
|
+
if (name === 'workspace') return await handleWorkspaceCommand(subcommand, rest);
|
|
238
|
+
if (name === 'port') return await setPort(subcommand);
|
|
239
|
+
if (name === 'token' && subcommand === 'rotate') return await rotateToken();
|
|
240
|
+
if (name === 'user-id') return await setScalar('userId', subcommand, 'user id');
|
|
241
|
+
if (name === 'model') return await setScalar('defaultModel', subcommand, 'model');
|
|
242
|
+
if (name === 'reasoning')
|
|
243
|
+
return await setEnum('defaultReasoning', subcommand, [
|
|
244
|
+
'minimal',
|
|
245
|
+
'low',
|
|
246
|
+
'medium',
|
|
247
|
+
'high',
|
|
248
|
+
'xhigh',
|
|
249
|
+
]);
|
|
250
|
+
if (name === 'verbosity')
|
|
251
|
+
return await setEnum('defaultVerbosity', subcommand, ['low', 'medium', 'high']);
|
|
252
|
+
if (name === 'approval')
|
|
253
|
+
return await setEnum('defaultApprovalPolicy', subcommand, ['untrusted', 'on-request', 'never']);
|
|
254
|
+
if (name === 'codex') return await setCodex([subcommand, ...rest].filter(Boolean));
|
|
255
|
+
throw new Error(`Unknown command: ${name}. Type "help" for commands.`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function setWrite(raw) {
|
|
259
|
+
const enabled = parseOnOff(raw, !config.writeTasksEnabled);
|
|
260
|
+
config.writeTasksEnabled = enabled;
|
|
261
|
+
activeWorkspace().allowWrite = enabled;
|
|
262
|
+
if (!enabled) {
|
|
263
|
+
config.fullAccessEnabled = false;
|
|
264
|
+
activeWorkspace().allowFullAccess = false;
|
|
265
|
+
}
|
|
266
|
+
syncWorkspaceSandbox(activeWorkspace());
|
|
267
|
+
await saveAndRestart(`write ${enabled ? 'on' : 'off'}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function setFullAccess(raw) {
|
|
271
|
+
const enabled = parseOnOff(raw, !config.fullAccessEnabled);
|
|
272
|
+
config.fullAccessEnabled = enabled;
|
|
273
|
+
config.writeTasksEnabled = enabled || config.writeTasksEnabled;
|
|
274
|
+
activeWorkspace().allowFullAccess = enabled;
|
|
275
|
+
activeWorkspace().allowWrite = enabled || activeWorkspace().allowWrite;
|
|
276
|
+
syncWorkspaceSandbox(activeWorkspace());
|
|
277
|
+
await saveAndRestart(`full-access ${enabled ? 'on' : 'off'}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function handleWorkspaceCommand(subcommand, rest) {
|
|
281
|
+
if (subcommand === 'list' || !subcommand) return printWorkspaces();
|
|
282
|
+
if (subcommand === 'add') return await addWorkspace(rest);
|
|
283
|
+
if (subcommand === 'remove') return await removeWorkspace(rest[0]);
|
|
284
|
+
if (subcommand === 'use') return await useWorkspace(rest[0]);
|
|
285
|
+
if (subcommand === 'describe') return await describeWorkspace(rest[0], rest.slice(1).join(' '));
|
|
286
|
+
if (subcommand === 'write') return await setWorkspaceAccess(rest[0], 'allowWrite', rest[1]);
|
|
287
|
+
if (subcommand === 'full-access')
|
|
288
|
+
return await setWorkspaceAccess(rest[0], 'allowFullAccess', rest[1]);
|
|
289
|
+
throw new Error('workspace commands: list, add, remove, use, describe, write, full-access');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function addWorkspace(parts) {
|
|
293
|
+
const [alias, root, ...description] = parts;
|
|
294
|
+
assertAlias(alias);
|
|
295
|
+
if (!root) throw new Error('usage: workspace add <alias> <path> [description]');
|
|
296
|
+
upsertWorkspace(config.workspaces, {
|
|
297
|
+
alias,
|
|
298
|
+
root: resolveUserPath(root),
|
|
299
|
+
description: description.join(' ') || `${alias} workspace`,
|
|
300
|
+
allowWrite: config.writeTasksEnabled,
|
|
301
|
+
allowFullAccess: config.fullAccessEnabled,
|
|
302
|
+
defaultSandbox: sandboxFor(config.writeTasksEnabled, config.fullAccessEnabled),
|
|
303
|
+
});
|
|
304
|
+
await saveAndRestart(`workspace ${alias} saved`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function removeWorkspace(alias) {
|
|
308
|
+
assertAlias(alias);
|
|
309
|
+
if (config.workspaces.length === 1) throw new Error('cannot remove the last workspace');
|
|
310
|
+
findWorkspace(alias);
|
|
311
|
+
config.workspaces = config.workspaces.filter((workspace) => workspace.alias !== alias);
|
|
312
|
+
await saveAndRestart(`workspace ${alias} removed`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function useWorkspace(alias) {
|
|
316
|
+
assertAlias(alias);
|
|
317
|
+
const workspace = findWorkspace(alias);
|
|
318
|
+
config.workspaces = [workspace, ...config.workspaces.filter((item) => item.alias !== alias)];
|
|
319
|
+
await saveAndRestart(`active workspace ${alias}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function describeWorkspace(alias, description) {
|
|
323
|
+
assertAlias(alias);
|
|
324
|
+
if (!description) throw new Error('usage: workspace describe <alias> <description>');
|
|
325
|
+
findWorkspace(alias).description = description;
|
|
326
|
+
await saveAndRestart(`workspace ${alias} described`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function setWorkspaceAccess(alias, key, raw) {
|
|
330
|
+
assertAlias(alias);
|
|
331
|
+
const workspace = findWorkspace(alias);
|
|
332
|
+
workspace[key] = parseOnOff(raw, !workspace[key]);
|
|
333
|
+
if (key === 'allowFullAccess' && workspace[key]) {
|
|
334
|
+
workspace.allowWrite = true;
|
|
335
|
+
config.writeTasksEnabled = true;
|
|
336
|
+
config.fullAccessEnabled = true;
|
|
337
|
+
}
|
|
338
|
+
if (key === 'allowWrite' && !workspace[key]) workspace.allowFullAccess = false;
|
|
339
|
+
syncWorkspaceSandbox(workspace);
|
|
340
|
+
await saveAndRestart(`workspace ${alias} updated`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function setPort(raw) {
|
|
344
|
+
if (!/^\d+$/.test(raw ?? '')) throw new Error('usage: port <number>');
|
|
345
|
+
config.port = String(Number(raw));
|
|
346
|
+
config.relayUrl = `ws://127.0.0.1:${config.port}/agent`;
|
|
347
|
+
await saveAndRestart(`port ${config.port}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function rotateToken() {
|
|
351
|
+
config.relayToken = randomHex();
|
|
352
|
+
await saveAndRestart('token rotated');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function setScalar(key, raw, label) {
|
|
356
|
+
if (!raw) throw new Error(`usage: ${label} <value>`);
|
|
357
|
+
config[key] = raw;
|
|
358
|
+
await saveAndRestart(`${label} ${raw}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function setEnum(key, raw, allowed) {
|
|
362
|
+
if (!allowed.includes(raw)) throw new Error(`allowed values: ${allowed.join(', ')}`);
|
|
363
|
+
config[key] = raw;
|
|
364
|
+
await saveAndRestart(`${key} ${raw}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function setCodex(parts) {
|
|
368
|
+
if (!parts.length) throw new Error('usage: codex <command> [app-server args...]');
|
|
369
|
+
config.appServerCommand = parts[0];
|
|
370
|
+
config.appServerArgs = parts.slice(1).length
|
|
371
|
+
? parts.slice(1)
|
|
372
|
+
: ['app-server', '--listen', 'stdio://'];
|
|
373
|
+
await saveAndRestart(`codex command ${config.appServerCommand}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function saveAndRestart(message) {
|
|
377
|
+
saveConfig();
|
|
378
|
+
console.log(`✅ ${message}. Saved.`);
|
|
379
|
+
await startStack();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function printStatus() {
|
|
383
|
+
console.log(`relay ${statusIcon(statuses.relay)} ${statuses.relay}`);
|
|
384
|
+
console.log(`agent ${statusIcon(statuses.agent)} ${statuses.agent}`);
|
|
385
|
+
console.log(`poke ${statusIcon(statuses.poke)} ${statuses.poke}`);
|
|
386
|
+
console.log(`mcp ${mcpHttpUrl()}`);
|
|
387
|
+
console.log(`mode ${modeLabel(activeWorkspace())}`);
|
|
388
|
+
console.log(`space ${activeWorkspace().alias} -> ${activeWorkspace().root}`);
|
|
389
|
+
console.log('tip type "help" for commands');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function printConfig() {
|
|
393
|
+
console.log(JSON.stringify(redactConfig(config), null, 2));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function printWorkspaces() {
|
|
397
|
+
for (const workspace of config.workspaces) {
|
|
398
|
+
const marker = workspace === activeWorkspace() ? '*' : ' ';
|
|
399
|
+
console.log(`${marker} ${workspace.alias} ${modeLabel(workspace)} ${workspace.root}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function printServiceOutput(name) {
|
|
404
|
+
const names = name ? [name] : ['relay', 'agent', 'poke'];
|
|
405
|
+
for (const key of names) {
|
|
406
|
+
const lines = serviceLogs.get(key);
|
|
407
|
+
if (!lines) {
|
|
408
|
+
console.log(`\n${serviceTitle(key)} output\nNo output yet.`);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
console.log(`\n${serviceTitle(key)} output`);
|
|
412
|
+
console.log(lines.slice(-30).join('\n') || 'no output');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function printInteractiveHelp() {
|
|
417
|
+
console.log(`pokedex commands
|
|
418
|
+
status
|
|
419
|
+
config
|
|
420
|
+
output [relay|agent|poke]
|
|
421
|
+
write [on|off]
|
|
422
|
+
full-access [on|off]
|
|
423
|
+
workspace list
|
|
424
|
+
workspace add <alias> <path> [description]
|
|
425
|
+
workspace remove <alias>
|
|
426
|
+
workspace use <alias>
|
|
427
|
+
workspace describe <alias> <description>
|
|
428
|
+
workspace write <alias> [on|off]
|
|
429
|
+
workspace full-access <alias> [on|off]
|
|
430
|
+
model <name>
|
|
431
|
+
reasoning minimal|low|medium|high|xhigh
|
|
432
|
+
verbosity low|medium|high
|
|
433
|
+
approval untrusted|on-request|never
|
|
434
|
+
codex <command> [app-server args...]
|
|
435
|
+
port <number>
|
|
436
|
+
token rotate
|
|
437
|
+
restart
|
|
438
|
+
quit
|
|
439
|
+
|
|
440
|
+
setup
|
|
441
|
+
codex login
|
|
442
|
+
npx poke@latest login`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function spawnManaged(name, bin, binArgs) {
|
|
446
|
+
const commandInfo = commandFor(bin);
|
|
447
|
+
const child = spawn(commandInfo.command, [...commandInfo.args, ...binArgs], {
|
|
448
|
+
cwd: invocationCwd,
|
|
449
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
450
|
+
env: process.env,
|
|
451
|
+
});
|
|
452
|
+
const entry = { child, exitCode: null, signal: null };
|
|
453
|
+
managedChildren.set(name, entry);
|
|
454
|
+
serviceLogs.set(name, []);
|
|
455
|
+
child.stdout?.setEncoding('utf8');
|
|
456
|
+
child.stderr?.setEncoding('utf8');
|
|
457
|
+
child.stdout?.on('data', (chunk) => appendLog(name, chunk));
|
|
458
|
+
child.stderr?.on('data', (chunk) => appendLog(name, chunk));
|
|
459
|
+
child.on('error', (error) => {
|
|
460
|
+
statuses[name] = 'error';
|
|
461
|
+
appendLog(name, error.message);
|
|
462
|
+
});
|
|
463
|
+
child.on('exit', (code, signal) => {
|
|
464
|
+
entry.exitCode = code ?? 0;
|
|
465
|
+
entry.signal = signal;
|
|
466
|
+
if (stopping || restarting) return;
|
|
467
|
+
statuses[name] = 'down';
|
|
468
|
+
if (!readline) return;
|
|
469
|
+
console.error(
|
|
470
|
+
`\n⚠️ ${serviceTitle(name)} stopped. Type "status" or "restart"; type "help" for commands.`
|
|
471
|
+
);
|
|
472
|
+
readline?.prompt();
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function failIfServiceExited(name, detail) {
|
|
477
|
+
const entry = managedChildren.get(name);
|
|
478
|
+
if (!entry) throw serviceFailure(name, detail);
|
|
479
|
+
if (entry.child.exitCode !== null || entry.child.killed || statuses[name] === 'error') {
|
|
480
|
+
throw serviceFailure(name, detail);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function serviceFailure(name, detail) {
|
|
485
|
+
if (!name) return new Error(detail);
|
|
486
|
+
const entry = managedChildren.get(name);
|
|
487
|
+
const output = formatServiceOutput(name);
|
|
488
|
+
const lines = [`⚠️ ${serviceTitle(name)} needs attention.`, serviceHint(name, detail)];
|
|
489
|
+
if (
|
|
490
|
+
!entry ||
|
|
491
|
+
(entry.child.exitCode === null &&
|
|
492
|
+
!entry.child.killed &&
|
|
493
|
+
entry.exitCode === null &&
|
|
494
|
+
!entry.signal &&
|
|
495
|
+
statuses[name] !== 'error')
|
|
496
|
+
) {
|
|
497
|
+
if (output) lines.push('', output);
|
|
498
|
+
lines.push('', 'Tip: type "help" after Pokedex starts to see commands.');
|
|
499
|
+
return new Error(lines.join('\n'));
|
|
500
|
+
}
|
|
501
|
+
if (output) lines.push('', output);
|
|
502
|
+
lines.push('', 'Tip: type "help" after Pokedex starts to see commands.');
|
|
503
|
+
return new Error(lines.join('\n'));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function formatServiceOutput(name) {
|
|
507
|
+
const lines = serviceLogs.get(name) ?? [];
|
|
508
|
+
return lines.length ? `${serviceTitle(name)} output:\n${lines.slice(-20).join('\n')}` : '';
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function appendLog(name, chunk) {
|
|
512
|
+
const lines = serviceLogs.get(name) ?? [];
|
|
513
|
+
lines.push(
|
|
514
|
+
...String(chunk)
|
|
515
|
+
.split(/\r?\n/)
|
|
516
|
+
.map((line) => line.trim())
|
|
517
|
+
.map((line) => cleanServiceLine(name, line))
|
|
518
|
+
.filter(Boolean)
|
|
519
|
+
);
|
|
520
|
+
serviceLogs.set(name, lines.slice(-100));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function stopStack(final) {
|
|
524
|
+
restarting = !final;
|
|
525
|
+
for (const entry of managedChildren.values()) entry.child.kill('SIGTERM');
|
|
526
|
+
const children = [...managedChildren.values()].map((entry) => waitForExit(entry.child));
|
|
527
|
+
await Promise.all(children);
|
|
528
|
+
managedChildren.clear();
|
|
529
|
+
if (!final) {
|
|
530
|
+
restarting = false;
|
|
531
|
+
statuses.relay = 'idle';
|
|
532
|
+
statuses.agent = 'idle';
|
|
533
|
+
statuses.poke = 'idle';
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function stopManaged(code) {
|
|
538
|
+
if (stopping) return;
|
|
539
|
+
stopping = true;
|
|
540
|
+
readline?.close();
|
|
541
|
+
await stopStack(true);
|
|
542
|
+
process.exit(code);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function waitForExit(child) {
|
|
546
|
+
if (child.exitCode !== null) return Promise.resolve();
|
|
547
|
+
return new Promise((resolveExit) => child.once('exit', resolveExit));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function fetchJson(url) {
|
|
551
|
+
try {
|
|
552
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(1000) });
|
|
553
|
+
return response.ok ? await response.json() : null;
|
|
554
|
+
} catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function saveConfig() {
|
|
560
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
561
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function loadSavedConfig() {
|
|
565
|
+
if (existsSync(configPath)) return readJson(configPath);
|
|
566
|
+
return {};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function readJson(path) {
|
|
570
|
+
if (!existsSync(path)) return {};
|
|
571
|
+
try {
|
|
572
|
+
const state = JSON.parse(readFileSync(path, 'utf8'));
|
|
573
|
+
return state && typeof state === 'object' ? state : {};
|
|
574
|
+
} catch (error) {
|
|
575
|
+
die(`invalid json file ${path}: ${error.message}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function upsertWorkspace(workspaces, workspace) {
|
|
580
|
+
const existing = workspaces.findIndex((item) => item.alias === workspace.alias);
|
|
581
|
+
if (existing === -1) workspaces.unshift(workspace);
|
|
582
|
+
else workspaces[existing] = { ...workspaces[existing], ...workspace };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function firstWorkspace(raw) {
|
|
586
|
+
return Array.isArray(raw.workspaces) && raw.workspaces[0] ? raw.workspaces[0] : {};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function activeWorkspace() {
|
|
590
|
+
return config.workspaces[0];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function findWorkspace(alias) {
|
|
594
|
+
const workspace = config.workspaces.find((item) => item.alias === alias);
|
|
595
|
+
if (!workspace) throw new Error(`unknown workspace: ${alias}`);
|
|
596
|
+
return workspace;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function syncWorkspaceSandbox(workspace) {
|
|
600
|
+
workspace.defaultSandbox = sandboxFor(workspace.allowWrite, workspace.allowFullAccess);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function sandboxFor(writeEnabled, fullAccess) {
|
|
604
|
+
if (fullAccess) return 'danger_full_access';
|
|
605
|
+
if (writeEnabled) return 'workspace_write';
|
|
606
|
+
return 'read_only';
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function modeLabel(workspace) {
|
|
610
|
+
if (workspace.allowFullAccess) return 'full-access';
|
|
611
|
+
if (workspace.allowWrite) return 'write';
|
|
612
|
+
return 'read-only';
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function statusIcon(status) {
|
|
616
|
+
if (status === 'ok') return '✅';
|
|
617
|
+
if (status === 'starting') return '⏳';
|
|
618
|
+
if (status === 'error' || status === 'down') return '⚠️';
|
|
619
|
+
return '•';
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function serviceTitle(name) {
|
|
623
|
+
if (name === 'relay') return 'pokedex relay';
|
|
624
|
+
if (name === 'agent') return 'Codex agent';
|
|
625
|
+
if (name === 'poke') return 'Poke tunnel';
|
|
626
|
+
return 'pokedex';
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function serviceHint(name, detail) {
|
|
630
|
+
if (name === 'poke') return 'Run `npx poke@latest login`, then start Pokedex again.';
|
|
631
|
+
if (name === 'agent') return 'Run `codex login` and `codex doctor`, then start Pokedex again.';
|
|
632
|
+
if (name === 'relay')
|
|
633
|
+
return 'Check the port with `pokedex --port <number>`, then start Pokedex again.';
|
|
634
|
+
return detail;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function cleanServiceLine(name, line) {
|
|
638
|
+
if (name !== 'poke') return line;
|
|
639
|
+
return line.replace(/Run ['"]?poke login['"]?\.?/gi, 'Run npx poke@latest login.');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function formatError(error) {
|
|
643
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
644
|
+
if (message.startsWith('⚠️') || message.startsWith('✅')) return message;
|
|
645
|
+
return `⚠️ ${message}\nType "help" for commands.`;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function parseOnOff(raw, defaultValue) {
|
|
649
|
+
if (!raw) return defaultValue;
|
|
650
|
+
if (['on', 'true', 'yes', '1'].includes(raw)) return true;
|
|
651
|
+
if (['off', 'false', 'no', '0'].includes(raw)) return false;
|
|
652
|
+
throw new Error('use on or off');
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function assertAlias(alias) {
|
|
656
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(alias ?? ''))
|
|
657
|
+
throw new Error('alias must match /^[a-z0-9][a-z0-9_-]{0,63}$/i');
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function splitCommand(line) {
|
|
661
|
+
const parts = [];
|
|
662
|
+
let current = '';
|
|
663
|
+
let quote = '';
|
|
664
|
+
for (const char of line) {
|
|
665
|
+
if (quote) {
|
|
666
|
+
if (char === quote) quote = '';
|
|
667
|
+
else current += char;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (char === "'" || char === '"') {
|
|
671
|
+
quote = char;
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
if (/\s/.test(char)) {
|
|
675
|
+
if (current) parts.push(current);
|
|
676
|
+
current = '';
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
current += char;
|
|
680
|
+
}
|
|
681
|
+
if (current) parts.push(current);
|
|
682
|
+
return parts;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function redactConfig(raw) {
|
|
686
|
+
return { ...raw, relayToken: raw.relayToken ? `${raw.relayToken.slice(0, 6)}...` : '' };
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function mcpHttpUrl() {
|
|
690
|
+
return `http://127.0.0.1:${config.port}/mcp`;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function mcpHttpUrlWithToken() {
|
|
694
|
+
return `${mcpHttpUrl()}?token=${encodeURIComponent(config.relayToken)}`;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function commandFor(bin) {
|
|
698
|
+
return resolveManagedCommand(bin) ?? { command: bin, args: [] };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function resolveManagedCommand(bin) {
|
|
702
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
703
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
704
|
+
const packagedPaths = {
|
|
705
|
+
'pokedex-agent': join(packageRoot, 'dist/agent.cjs'),
|
|
706
|
+
'pokedex-relay': join(packageRoot, 'dist/relay.cjs'),
|
|
707
|
+
};
|
|
708
|
+
const paths = {
|
|
709
|
+
'pokedex-agent': join(root, 'apps/agent/dist/index.js'),
|
|
710
|
+
'pokedex-relay': join(root, 'apps/relay/dist/index.js'),
|
|
711
|
+
};
|
|
712
|
+
if (existsSync(join(root, 'package.json'))) ensureLocalBuild(root, packagedPaths);
|
|
713
|
+
if (packagedPaths[bin] && existsSync(packagedPaths[bin]))
|
|
714
|
+
return { command: process.execPath, args: [packagedPaths[bin]] };
|
|
715
|
+
if (paths[bin] && existsSync(paths[bin]))
|
|
716
|
+
return { command: process.execPath, args: [paths[bin]] };
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function ensureLocalBuild(root, packagedPaths) {
|
|
721
|
+
if (Object.values(packagedPaths).every((path) => existsSync(path))) return;
|
|
722
|
+
const result = spawnSync(npmBin(), ['run', 'build'], {
|
|
723
|
+
cwd: root,
|
|
724
|
+
encoding: 'utf8',
|
|
725
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
726
|
+
env: { ...process.env, npm_config_update_notifier: 'false' },
|
|
727
|
+
});
|
|
728
|
+
if (result.status !== 0)
|
|
729
|
+
throw new Error(`local build failed. run npm run build and retry.${formatBuildOutput(result)}`);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function formatBuildOutput(result) {
|
|
733
|
+
const output = [result.stdout, result.stderr]
|
|
734
|
+
.filter(Boolean)
|
|
735
|
+
.join('\n')
|
|
736
|
+
.split(/\r?\n/)
|
|
737
|
+
.map((line) => line.trim())
|
|
738
|
+
.filter(Boolean)
|
|
739
|
+
.slice(-30)
|
|
740
|
+
.join('\n');
|
|
741
|
+
return output ? `\n${output}` : '';
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function npmBin() {
|
|
745
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function npxBin() {
|
|
749
|
+
return process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function defaultConfigPath() {
|
|
753
|
+
return join(homedir(), '.pokedex', 'config.json');
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function resolveUserPath(path) {
|
|
757
|
+
if (path === '~') return homedir();
|
|
758
|
+
if (path.startsWith('~/') || path.startsWith('~\\')) return join(homedir(), path.slice(2));
|
|
759
|
+
return resolve(invocationCwd, path);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function value(flag) {
|
|
763
|
+
const index = args.indexOf(flag);
|
|
764
|
+
return index === -1 ? undefined : args[index + 1];
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function has(flag) {
|
|
768
|
+
return args.includes(flag);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function randomHex() {
|
|
772
|
+
return randomBytes(32).toString('hex');
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function registerSignals() {
|
|
776
|
+
process.once('SIGINT', () => void stopManaged(0));
|
|
777
|
+
process.once('SIGTERM', () => void stopManaged(0));
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function printBanner() {
|
|
781
|
+
console.log(color(bannerText, '33;1'));
|
|
782
|
+
console.log('');
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function color(text, code) {
|
|
786
|
+
if (!process.stdout.isTTY || process.env.NO_COLOR) return text;
|
|
787
|
+
return `\u001b[${code}m${text}\u001b[0m`;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function die(message) {
|
|
791
|
+
console.error(formatError(message));
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function help() {
|
|
796
|
+
console.log(`pokedex
|
|
797
|
+
Local Poke to Codex bridge.
|
|
798
|
+
|
|
799
|
+
usage
|
|
800
|
+
pokedex [--workspace .] [--port 3000] [--write] [--read-only]
|
|
801
|
+
pokedex help
|
|
802
|
+
|
|
803
|
+
setup
|
|
804
|
+
codex login
|
|
805
|
+
npx poke@latest login
|
|
806
|
+
|
|
807
|
+
config
|
|
808
|
+
~/.pokedex/config.json
|
|
809
|
+
|
|
810
|
+
interactive commands
|
|
811
|
+
status
|
|
812
|
+
output [relay|agent|poke]
|
|
813
|
+
write on
|
|
814
|
+
workspace add repo ./repo
|
|
815
|
+
model gpt-5.5
|
|
816
|
+
restart
|
|
817
|
+
help
|
|
818
|
+
quit
|
|
819
|
+
|
|
820
|
+
common
|
|
821
|
+
npx codex-to-poke
|
|
822
|
+
npx codex-to-poke --write
|
|
823
|
+
npx codex-to-poke --read-only
|
|
824
|
+
`);
|
|
825
|
+
}
|