@bramblex/codex-workbench 0.1.14 → 0.1.15
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/README.md +60 -20
- package/package.json +2 -2
- package/src/cli-output.js +38 -26
- package/src/cli.js +18 -3
- package/src/config.js +44 -4
- package/src/model/metadata.js +44 -0
- package/src/model/session-store.js +39 -116
- package/src/model/workbench-config.js +48 -12
- package/src/providers/codex.js +267 -0
- package/src/providers/index.js +59 -0
- package/src/providers/pi.js +326 -0
- package/src/services/codex-runner.js +27 -62
- package/src/services/session-sources.js +52 -8
- package/src/ui/workbench.js +84 -18
|
@@ -2,118 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const {
|
|
5
|
+
const { loadMeta, removeMetadata, updateMetadata } = require('./metadata');
|
|
6
|
+
const { getAllSessionFiles, getProvider } = require('../providers');
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} catch {
|
|
11
|
-
return fallback;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function writeJson(file, value) {
|
|
16
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
17
|
-
fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function walk(dir, out = []) {
|
|
21
|
-
let entries = [];
|
|
22
|
-
try {
|
|
23
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
24
|
-
} catch {
|
|
25
|
-
return out;
|
|
26
|
-
}
|
|
27
|
-
for (const entry of entries) {
|
|
28
|
-
const full = path.join(dir, entry.name);
|
|
29
|
-
if (entry.isDirectory()) walk(full, out);
|
|
30
|
-
else if (entry.isFile() && entry.name.endsWith('.jsonl')) out.push(full);
|
|
31
|
-
}
|
|
32
|
-
return out;
|
|
33
|
-
}
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Metadata persistence (provider-agnostic: keyed by session id)
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
34
11
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.filter((item) => item && (item.type === 'input_text' || item.type === 'output_text'))
|
|
39
|
-
.map((item) => item.text || '')
|
|
40
|
-
.join(' ')
|
|
41
|
-
.replace(/\s+/g, ' ')
|
|
42
|
-
.trim();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function isNoiseUserText(text) {
|
|
46
|
-
return text.includes('<environment_context>') || text.includes('<permissions instructions>');
|
|
47
|
-
}
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Session listing (aggregates across all providers)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
48
15
|
|
|
49
|
-
function
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const lines = raw ? raw.split(/\n/) : [];
|
|
53
|
-
let meta = {};
|
|
54
|
-
const messages = [];
|
|
55
|
-
let turns = 0;
|
|
16
|
+
function listSessions() {
|
|
17
|
+
const meta = loadMeta();
|
|
18
|
+
const fileEntries = getAllSessionFiles();
|
|
56
19
|
|
|
57
|
-
|
|
58
|
-
|
|
20
|
+
const sessions = [];
|
|
21
|
+
for (const { file, backend } of fileEntries) {
|
|
59
22
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (msg.role === 'developer') continue;
|
|
68
|
-
const text = textFromContent(msg.content);
|
|
69
|
-
if (!text) continue;
|
|
70
|
-
if (msg.role === 'user' && isNoiseUserText(text)) continue;
|
|
71
|
-
messages.push({
|
|
72
|
-
role: msg.role,
|
|
73
|
-
phase: msg.phase || '',
|
|
74
|
-
text,
|
|
75
|
-
});
|
|
76
|
-
if (msg.role === 'user') turns += 1;
|
|
23
|
+
const provider = getProvider(backend);
|
|
24
|
+
const session = provider.parseSession(file);
|
|
25
|
+
// Merge workbench metadata (name, note, archived)
|
|
26
|
+
const custom = meta.sessions[session.id] || {};
|
|
27
|
+
sessions.push({ ...session, ...custom });
|
|
28
|
+
} catch (_err) {
|
|
29
|
+
// Skip unparseable files
|
|
77
30
|
}
|
|
78
31
|
}
|
|
79
32
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const lastUser = [...messages].reverse().find((msg) => msg.role === 'user');
|
|
83
|
-
const lastAssistant = [...messages].reverse().find((msg) => msg.role === 'assistant');
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
id,
|
|
87
|
-
file,
|
|
88
|
-
cwd: meta.cwd || '(unknown)',
|
|
89
|
-
startedAt: meta.timestamp || null,
|
|
90
|
-
updatedAt: stat.mtime.toISOString(),
|
|
91
|
-
cliVersion: meta.cli_version || '',
|
|
92
|
-
source: meta.source || '',
|
|
93
|
-
provider: meta.model_provider || '',
|
|
94
|
-
turns,
|
|
95
|
-
first: firstUser ? firstUser.text : '',
|
|
96
|
-
last: lastUser ? lastUser.text : '',
|
|
97
|
-
lastAssistant: lastAssistant ? lastAssistant.text : '',
|
|
98
|
-
messages,
|
|
99
|
-
};
|
|
33
|
+
sessions.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
34
|
+
return sessions;
|
|
100
35
|
}
|
|
101
36
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return data;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function listSessions() {
|
|
109
|
-
const meta = loadMeta();
|
|
110
|
-
return walk(SESSIONS_DIR)
|
|
111
|
-
.map(parseSession)
|
|
112
|
-
.map((session) => ({ ...session, ...(meta.sessions[session.id] || {}) }))
|
|
113
|
-
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
114
|
-
}
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Session resolution
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
115
40
|
|
|
116
|
-
function resolveSession(query, sessions
|
|
41
|
+
function resolveSession(query, sessions) {
|
|
42
|
+
if (!sessions) sessions = listSessions();
|
|
117
43
|
if (!query) throw new Error('Missing session. Run `codex-workbench list` to find a session id.');
|
|
118
44
|
const matches = sessions.filter((session) => {
|
|
119
45
|
return session.id === query ||
|
|
@@ -123,21 +49,9 @@ function resolveSession(query, sessions = listSessions()) {
|
|
|
123
49
|
});
|
|
124
50
|
if (matches.length === 1) return matches[0];
|
|
125
51
|
if (matches.length === 0) throw new Error(`No session matched: ${query}`);
|
|
126
|
-
throw new Error(
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function updateMetadata(session, patch) {
|
|
130
|
-
const meta = loadMeta();
|
|
131
|
-
meta.sessions[session.id] = { ...(meta.sessions[session.id] || {}), ...patch };
|
|
132
|
-
meta.updatedAt = new Date().toISOString();
|
|
133
|
-
writeJson(META_PATH, meta);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function removeMetadata(session) {
|
|
137
|
-
const meta = loadMeta();
|
|
138
|
-
delete meta.sessions[session.id];
|
|
139
|
-
meta.updatedAt = new Date().toISOString();
|
|
140
|
-
writeJson(META_PATH, meta);
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Ambiguous session: ${query}\n${matches.map((s) => ` ${s.id} ${s.name || ''}`).join('\n')}`
|
|
54
|
+
);
|
|
141
55
|
}
|
|
142
56
|
|
|
143
57
|
function deleteSessionFile(session) {
|
|
@@ -145,6 +59,15 @@ function deleteSessionFile(session) {
|
|
|
145
59
|
removeMetadata(session);
|
|
146
60
|
}
|
|
147
61
|
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Re-export parseSession for backward compat (uses codex provider by default)
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
function parseSession(file) {
|
|
67
|
+
const codex = getProvider('codex');
|
|
68
|
+
return codex.parseSession(file);
|
|
69
|
+
}
|
|
70
|
+
|
|
148
71
|
module.exports = {
|
|
149
72
|
deleteSessionFile,
|
|
150
73
|
listSessions,
|
|
@@ -6,29 +6,65 @@ const { CONFIG_PATH } = require('../config');
|
|
|
6
6
|
function readWorkbenchConfig(file = CONFIG_PATH) {
|
|
7
7
|
try {
|
|
8
8
|
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
10
|
+
throw new Error(`Workbench config must be a JSON object: ${file}`);
|
|
11
|
+
}
|
|
12
|
+
return data;
|
|
13
|
+
} catch (err) {
|
|
14
|
+
if (err && err.code === 'ENOENT') return {};
|
|
15
|
+
if (err instanceof SyntaxError) throw new Error(`Invalid workbench config JSON: ${file}`);
|
|
16
|
+
throw err;
|
|
12
17
|
}
|
|
13
18
|
}
|
|
14
19
|
|
|
20
|
+
function configError(index, message) {
|
|
21
|
+
return new Error(`Invalid server config at servers[${index}]: ${message}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stringField(value, field, index, required = false) {
|
|
25
|
+
if (value === undefined || value === null || value === '') {
|
|
26
|
+
if (required) throw configError(index, `${field} is required`);
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
if (typeof value !== 'string') throw configError(index, `${field} must be a string`);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeSshArgs(value, index) {
|
|
34
|
+
if (value === undefined) return [];
|
|
35
|
+
if (!Array.isArray(value)) throw configError(index, 'sshArgs must be an array');
|
|
36
|
+
return value.map((item, argIndex) => {
|
|
37
|
+
if (typeof item !== 'string') {
|
|
38
|
+
throw configError(index, `sshArgs[${argIndex}] must be a string`);
|
|
39
|
+
}
|
|
40
|
+
return item;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
15
44
|
function normalizeServer(server, index) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
45
|
+
if (!server || typeof server !== 'object' || Array.isArray(server)) {
|
|
46
|
+
throw configError(index, 'server must be an object');
|
|
47
|
+
}
|
|
48
|
+
const target = stringField(server.target || server.host, 'target', index, true);
|
|
49
|
+
const id = stringField(server.id, 'id', index) || target.replace(/[^a-zA-Z0-9_.-]+/g, '-');
|
|
50
|
+
const label = stringField(server.label || server.name, 'label', index) || id || `server-${index + 1}`;
|
|
51
|
+
const command = stringField(server.command, 'command', index) || 'cwb';
|
|
52
|
+
const sshArgs = normalizeSshArgs(server.sshArgs, index);
|
|
19
53
|
return {
|
|
20
54
|
id,
|
|
21
|
-
label
|
|
55
|
+
label,
|
|
22
56
|
target,
|
|
23
|
-
command
|
|
24
|
-
sshArgs
|
|
57
|
+
command,
|
|
58
|
+
sshArgs,
|
|
25
59
|
};
|
|
26
60
|
}
|
|
27
61
|
|
|
28
62
|
function listServers(config = readWorkbenchConfig()) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
63
|
+
if (config.servers === undefined) return [];
|
|
64
|
+
if (!Array.isArray(config.servers)) {
|
|
65
|
+
throw new Error('Invalid workbench config: servers must be an array');
|
|
66
|
+
}
|
|
67
|
+
return config.servers.map(normalizeServer);
|
|
32
68
|
}
|
|
33
69
|
|
|
34
70
|
module.exports = {
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Codex provider – session parsing, binary discovery, and CLI operations
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { spawn, spawnSync } = require('child_process');
|
|
10
|
+
const { HOME } = require('../config');
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Binary discovery (extracted from codex-bin.js)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
const DEFAULT_CODEX_BIN = '/Applications/Codex.app/Contents/Resources/codex';
|
|
17
|
+
|
|
18
|
+
function isExecutable(file) {
|
|
19
|
+
try { fs.accessSync(file, fs.constants.X_OK); return true; } catch { return false; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function findOnPath(command, pathValue) {
|
|
23
|
+
for (const dir of (pathValue || process.env.PATH || '').split(path.delimiter)) {
|
|
24
|
+
if (!dir) continue;
|
|
25
|
+
const candidate = path.join(dir, command);
|
|
26
|
+
if (isExecutable(candidate)) return candidate;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function shellQuote(value) {
|
|
32
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function executableFromOutput(output) {
|
|
36
|
+
for (const line of String(output || '').split(/\r?\n/)) {
|
|
37
|
+
const candidate = line.trim();
|
|
38
|
+
if (candidate && path.isAbsolute(candidate) && isExecutable(candidate)) return candidate;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function runShellLookup(shell, shellArgs, command, env) {
|
|
44
|
+
const result = spawnSync(shell, [...shellArgs, `command -v ${shellQuote(command)}`], {
|
|
45
|
+
encoding: 'utf8', env, stdio: ['ignore', 'pipe', 'pipe'],
|
|
46
|
+
});
|
|
47
|
+
if (result.error || result.status !== 0) return null;
|
|
48
|
+
return executableFromOutput(result.stdout);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function findWithShell(command, env) {
|
|
52
|
+
const shell = (env || process.env).SHELL || '/bin/sh';
|
|
53
|
+
if (!isExecutable(shell)) return null;
|
|
54
|
+
return runShellLookup(shell, ['-lc'], command, env) ||
|
|
55
|
+
runShellLookup(shell, ['-ic'], command, env);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveCodexBin() {
|
|
59
|
+
const env = process.env;
|
|
60
|
+
|
|
61
|
+
if (env.CODEX_BIN) {
|
|
62
|
+
if (isExecutable(env.CODEX_BIN)) return env.CODEX_BIN;
|
|
63
|
+
throw new Error(`CODEX_BIN is not executable: ${env.CODEX_BIN}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const fromLogin = findWithShell('codex', env);
|
|
67
|
+
if (fromLogin) return fromLogin;
|
|
68
|
+
|
|
69
|
+
const fromPath = findOnPath('codex', env.PATH);
|
|
70
|
+
if (fromPath) return fromPath;
|
|
71
|
+
|
|
72
|
+
if (DEFAULT_CODEX_BIN && isExecutable(DEFAULT_CODEX_BIN)) return DEFAULT_CODEX_BIN;
|
|
73
|
+
|
|
74
|
+
throw new Error('Could not find the codex executable. Set CODEX_BIN or add codex to your shell PATH.');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// CLI execution (extracted from codex-runner.js)
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
function usableCwd(dir) {
|
|
82
|
+
for (const candidate of [dir, process.cwd(), HOME]) {
|
|
83
|
+
if (!candidate || candidate === '(unknown)') continue;
|
|
84
|
+
try { if (fs.statSync(candidate).isDirectory()) return candidate; } catch { /* skip */ }
|
|
85
|
+
}
|
|
86
|
+
return HOME;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function commandShell() {
|
|
90
|
+
const shell = process.env.SHELL || '/bin/sh';
|
|
91
|
+
try { fs.accessSync(shell, fs.constants.X_OK); return shell; } catch { return '/bin/sh'; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function runArgv(argv, cwd, inherit) {
|
|
95
|
+
const shellCommand = `exec ${argv.map(shellQuote).join(' ')}`;
|
|
96
|
+
const shell = commandShell();
|
|
97
|
+
if (inherit) {
|
|
98
|
+
const child = spawn(shell, ['-lc', shellCommand], { stdio: 'inherit', cwd, env: process.env });
|
|
99
|
+
child.on('error', (err) => { console.error(`error: failed to start codex: ${err.message}`); process.exit(1); });
|
|
100
|
+
child.on('exit', (code, signal) => { if (signal) process.kill(process.pid, signal); process.exit(code || 0); });
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
const result = spawnSync(shell, ['-lc', shellCommand], { stdio: 'inherit', cwd, env: process.env });
|
|
104
|
+
if (result.error) throw new Error(`failed to start codex: ${result.error.message}`);
|
|
105
|
+
const status = typeof result.status === 'number' ? result.status : 1;
|
|
106
|
+
process.exitCode = status;
|
|
107
|
+
return status;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function runCommand(command, session, args, inherit) {
|
|
111
|
+
const executable = resolveCodexBin();
|
|
112
|
+
const argv = [executable, command, session.id, ...(args || [])];
|
|
113
|
+
return runArgv(argv, usableCwd(session.cwd), inherit);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function runNew(cwd, args, inherit) {
|
|
117
|
+
const executable = resolveCodexBin();
|
|
118
|
+
const argv = [executable, ...(args || [])];
|
|
119
|
+
return runArgv(argv, usableCwd(cwd), inherit);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Session parsing (extracted from session-store.js)
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
const CODEX_SESSIONS_DIR = process.env.CODEX_SESSIONS_DIR || path.join(
|
|
127
|
+
process.env.CODEX_HOME || path.join(HOME, '.codex'),
|
|
128
|
+
'sessions'
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
function textFromContent(content) {
|
|
132
|
+
if (!Array.isArray(content)) return '';
|
|
133
|
+
return content
|
|
134
|
+
.filter((item) => item && (item.type === 'input_text' || item.type === 'output_text'))
|
|
135
|
+
.map((item) => item.text || '')
|
|
136
|
+
.join(' ')
|
|
137
|
+
.replace(/\s+/g, ' ')
|
|
138
|
+
.trim();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isNoiseUserText(text) {
|
|
142
|
+
return text.includes('<environment_context>') || text.includes('<permissions instructions>');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function walk(dir, out) {
|
|
146
|
+
out = out || [];
|
|
147
|
+
let entries;
|
|
148
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return out; }
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
const full = path.join(dir, entry.name);
|
|
151
|
+
if (entry.isDirectory()) walk(full, out);
|
|
152
|
+
else if (entry.isFile() && entry.name.endsWith('.jsonl')) out.push(full);
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseSession(file) {
|
|
158
|
+
const stat = fs.statSync(file);
|
|
159
|
+
const raw = fs.readFileSync(file, 'utf8').trim();
|
|
160
|
+
const lines = raw ? raw.split(/\n/) : [];
|
|
161
|
+
let meta = {};
|
|
162
|
+
const messages = [];
|
|
163
|
+
let turns = 0;
|
|
164
|
+
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
let row;
|
|
167
|
+
try { row = JSON.parse(line); } catch { continue; }
|
|
168
|
+
if (row.type === 'session_meta') meta = row.payload || {};
|
|
169
|
+
if (row.type === 'response_item' && row.payload && row.payload.type === 'message') {
|
|
170
|
+
const msg = row.payload;
|
|
171
|
+
if (msg.role === 'developer') continue;
|
|
172
|
+
const text = textFromContent(msg.content);
|
|
173
|
+
if (!text) continue;
|
|
174
|
+
if (msg.role === 'user' && isNoiseUserText(text)) continue;
|
|
175
|
+
messages.push({ role: msg.role, phase: msg.phase || '', text });
|
|
176
|
+
if (msg.role === 'user') turns += 1;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const id = meta.id || path.basename(file, '.jsonl').split('-').slice(-5).join('-');
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
id,
|
|
184
|
+
file,
|
|
185
|
+
cwd: meta.cwd || '(unknown)',
|
|
186
|
+
startedAt: meta.timestamp || null,
|
|
187
|
+
updatedAt: stat.mtime.toISOString(),
|
|
188
|
+
cliVersion: meta.cli_version || '',
|
|
189
|
+
provider: meta.model_provider || '',
|
|
190
|
+
turns,
|
|
191
|
+
first: firstUserText(messages),
|
|
192
|
+
last: lastUserText(messages),
|
|
193
|
+
lastAssistant: lastAssistantText(messages),
|
|
194
|
+
messages,
|
|
195
|
+
backend: 'codex',
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function firstUserText(messages) {
|
|
200
|
+
const m = messages.find((msg) => msg.role === 'user');
|
|
201
|
+
return m ? m.text : '';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function lastUserText(messages) {
|
|
205
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
206
|
+
if (messages[i].role === 'user') return messages[i].text;
|
|
207
|
+
}
|
|
208
|
+
return '';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function lastAssistantText(messages) {
|
|
212
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
213
|
+
if (messages[i].role === 'assistant') return messages[i].text;
|
|
214
|
+
}
|
|
215
|
+
return '';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Provider interface
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
function isAvailable() {
|
|
223
|
+
try { return fs.statSync(CODEX_SESSIONS_DIR).isDirectory(); } catch { return false; }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getSessionFiles() {
|
|
227
|
+
return walk(CODEX_SESSIONS_DIR);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function resolveBin() {
|
|
231
|
+
try { return resolveCodexBin(); } catch { return null; }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Map workbench command names → codex CLI commands
|
|
235
|
+
const COMMAND_MAP = {
|
|
236
|
+
resume: 'resume',
|
|
237
|
+
fork: 'fork',
|
|
238
|
+
delete: 'delete',
|
|
239
|
+
archive: 'archive',
|
|
240
|
+
unarchive: 'unarchive',
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
function runSessionCommand(command, session, args, inherit) {
|
|
244
|
+
const codexCmd = COMMAND_MAP[command];
|
|
245
|
+
if (!codexCmd) throw new Error(`Unknown command for codex backend: ${command}`);
|
|
246
|
+
return runCommand(codexCmd, session, args, inherit);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = {
|
|
250
|
+
id: 'codex',
|
|
251
|
+
label: 'Codex',
|
|
252
|
+
isAvailable,
|
|
253
|
+
getSessionFiles,
|
|
254
|
+
parseSession,
|
|
255
|
+
resolveBin,
|
|
256
|
+
runCommand: runSessionCommand,
|
|
257
|
+
runNew,
|
|
258
|
+
usableCwd,
|
|
259
|
+
// Re-export for backward compat
|
|
260
|
+
shellQuote,
|
|
261
|
+
commandShell,
|
|
262
|
+
resolveCodexBin,
|
|
263
|
+
DEFAULT_CODEX_BIN,
|
|
264
|
+
findOnPath,
|
|
265
|
+
findWithShell,
|
|
266
|
+
isExecutable,
|
|
267
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Provider registry – auto-detects available backends, routes operations
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
const codex = require('./codex');
|
|
8
|
+
const pi = require('./pi');
|
|
9
|
+
|
|
10
|
+
const ALL_PROVIDERS = [codex, pi];
|
|
11
|
+
const providerMap = new Map(ALL_PROVIDERS.map((p) => [p.id, p]));
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Auto-detect which providers are available (their session directories exist).
|
|
15
|
+
*/
|
|
16
|
+
function getAvailableProviders() {
|
|
17
|
+
return ALL_PROVIDERS.filter((p) => p.isAvailable());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get a specific provider by id. Throws if not registered.
|
|
22
|
+
*/
|
|
23
|
+
function getProvider(id) {
|
|
24
|
+
const p = providerMap.get(id);
|
|
25
|
+
if (!p) throw new Error(`Unknown backend: ${id}. Available: ${[...providerMap.keys()].join(', ')}`);
|
|
26
|
+
return p;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the provider for a session (reads session.backend).
|
|
31
|
+
*/
|
|
32
|
+
function providerForSession(session) {
|
|
33
|
+
const backend = session.backend || 'codex';
|
|
34
|
+
return getProvider(backend);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* List all session files across all available providers.
|
|
39
|
+
* Returns [{ file, backend, providerId }].
|
|
40
|
+
*/
|
|
41
|
+
function getAllSessionFiles() {
|
|
42
|
+
const files = [];
|
|
43
|
+
for (const provider of getAvailableProviders()) {
|
|
44
|
+
for (const file of provider.getSessionFiles()) {
|
|
45
|
+
files.push({ file, backend: provider.id });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return files;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
ALL_PROVIDERS,
|
|
53
|
+
getAvailableProviders,
|
|
54
|
+
getProvider,
|
|
55
|
+
providerForSession,
|
|
56
|
+
getAllSessionFiles,
|
|
57
|
+
codex,
|
|
58
|
+
pi,
|
|
59
|
+
};
|