@bramblex/codex-workbench 0.1.13 → 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 +101 -108
- 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
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// pi 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, PI_CODING_AGENT_DIR } = require('../config');
|
|
11
|
+
const { updateMetadata } = require('../model/metadata');
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Paths
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const PI_SESSIONS_DIR = process.env.PI_CODING_AGENT_SESSION_DIR ||
|
|
18
|
+
path.join(PI_CODING_AGENT_DIR, 'sessions');
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Helpers
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
function shellQuote(value) {
|
|
25
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isExecutable(file) {
|
|
29
|
+
try { fs.accessSync(file, fs.constants.X_OK); return true; } catch { return false; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findOnPath(command, pathValue) {
|
|
33
|
+
for (const dir of (pathValue || process.env.PATH || '').split(path.delimiter)) {
|
|
34
|
+
if (!dir) continue;
|
|
35
|
+
const candidate = path.join(dir, command);
|
|
36
|
+
if (isExecutable(candidate)) return candidate;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function usableCwd(dir) {
|
|
42
|
+
for (const candidate of [dir, process.cwd(), HOME]) {
|
|
43
|
+
if (!candidate || candidate === '(unknown)') continue;
|
|
44
|
+
try { if (fs.statSync(candidate).isDirectory()) return candidate; } catch { /* skip */ }
|
|
45
|
+
}
|
|
46
|
+
return HOME;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function commandShell() {
|
|
50
|
+
const shell = process.env.SHELL || '/bin/sh';
|
|
51
|
+
try { fs.accessSync(shell, fs.constants.X_OK); return shell; } catch { return '/bin/sh'; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Binary discovery
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
function resolvePiBin() {
|
|
59
|
+
const env = process.env;
|
|
60
|
+
|
|
61
|
+
// PI_BIN env var takes priority
|
|
62
|
+
if (env.PI_BIN) {
|
|
63
|
+
if (isExecutable(env.PI_BIN)) return env.PI_BIN;
|
|
64
|
+
throw new Error(`PI_BIN is not executable: ${env.PI_BIN}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check PATH
|
|
68
|
+
const fromPath = findOnPath('pi', env.PATH);
|
|
69
|
+
if (fromPath) return fromPath;
|
|
70
|
+
|
|
71
|
+
// npm global bin (common locations)
|
|
72
|
+
const npmPrefix = (() => {
|
|
73
|
+
try {
|
|
74
|
+
const result = spawnSync('npm', ['prefix', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
75
|
+
return (result.stdout || '').trim();
|
|
76
|
+
} catch { return ''; }
|
|
77
|
+
})();
|
|
78
|
+
|
|
79
|
+
if (npmPrefix) {
|
|
80
|
+
const candidate = path.join(npmPrefix, 'bin', 'pi');
|
|
81
|
+
if (isExecutable(candidate)) return candidate;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw new Error('Could not find the pi executable. Install with: npm install -g @earendil-works/pi-coding-agent');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Session listing
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
function walk(dir, out) {
|
|
92
|
+
out = out || [];
|
|
93
|
+
let entries;
|
|
94
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return out; }
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const full = path.join(dir, entry.name);
|
|
97
|
+
if (entry.isDirectory()) walk(full, out);
|
|
98
|
+
else if (entry.isFile() && entry.name.endsWith('.jsonl')) out.push(full);
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Session parsing – pi v3 JSONL format
|
|
105
|
+
//
|
|
106
|
+
// Header: {"type":"session","version":3,"id":"uuid","timestamp":"...","cwd":"/path"}
|
|
107
|
+
// Message: {"type":"message","id":"...","parentId":"...","message":{"role":"user|assistant|toolResult","content":[...]}}
|
|
108
|
+
// Model: {"type":"model_change", ...}
|
|
109
|
+
// Thinking:{"type":"thinking_level_change", ...}
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
function piTextFromContent(content) {
|
|
113
|
+
if (typeof content === 'string') return content;
|
|
114
|
+
if (!Array.isArray(content)) return '';
|
|
115
|
+
return content
|
|
116
|
+
.filter((item) => item && item.type === 'text')
|
|
117
|
+
.map((item) => item.text || '')
|
|
118
|
+
.join(' ')
|
|
119
|
+
.replace(/\s+/g, ' ')
|
|
120
|
+
.trim();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseSession(file) {
|
|
124
|
+
const stat = fs.statSync(file);
|
|
125
|
+
const raw = fs.readFileSync(file, 'utf8').trim();
|
|
126
|
+
if (!raw) {
|
|
127
|
+
return emptySession(file, stat, path.basename(file, '.jsonl'));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const lines = raw.split(/\n/);
|
|
131
|
+
let header = {};
|
|
132
|
+
const messages = [];
|
|
133
|
+
let turns = 0;
|
|
134
|
+
let provider = '';
|
|
135
|
+
let cliVersion = '';
|
|
136
|
+
|
|
137
|
+
for (const line of lines) {
|
|
138
|
+
let row;
|
|
139
|
+
try { row = JSON.parse(line); } catch { continue; }
|
|
140
|
+
|
|
141
|
+
if (row.type === 'session') {
|
|
142
|
+
header = row;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (row.type === 'model_change') {
|
|
147
|
+
provider = row.provider || '';
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (row.type === 'message' && row.message) {
|
|
152
|
+
const msg = row.message;
|
|
153
|
+
if (msg.role === 'toolResult') continue; // skip tool results for display purposes
|
|
154
|
+
|
|
155
|
+
const text = piTextFromContent(msg.content);
|
|
156
|
+
// Skip empty messages (e.g. tool calls with no text)
|
|
157
|
+
if (!text && msg.role !== 'user') continue;
|
|
158
|
+
|
|
159
|
+
messages.push({
|
|
160
|
+
role: msg.role,
|
|
161
|
+
text: text || '',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (msg.role === 'user') turns += 1;
|
|
165
|
+
|
|
166
|
+
// Extract provider from first assistant message
|
|
167
|
+
if (!provider && msg.role === 'assistant' && msg.provider) {
|
|
168
|
+
provider = msg.provider;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const id = header.id || extractIdFromFilename(file);
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
id,
|
|
177
|
+
file,
|
|
178
|
+
cwd: header.cwd || '(unknown)',
|
|
179
|
+
startedAt: header.timestamp || stat.birthtime?.toISOString() || null,
|
|
180
|
+
updatedAt: stat.mtime.toISOString(),
|
|
181
|
+
cliVersion,
|
|
182
|
+
provider,
|
|
183
|
+
turns,
|
|
184
|
+
first: firstUserText(messages),
|
|
185
|
+
last: lastUserText(messages),
|
|
186
|
+
lastAssistant: lastAssistantText(messages),
|
|
187
|
+
messages,
|
|
188
|
+
backend: 'pi',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function extractIdFromFilename(file) {
|
|
193
|
+
const base = path.basename(file, '.jsonl');
|
|
194
|
+
// pi filenames: <timestamp>_<uuid>.jsonl
|
|
195
|
+
const parts = base.split('_');
|
|
196
|
+
if (parts.length >= 2) return parts[parts.length - 1];
|
|
197
|
+
return base;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function emptySession(file, stat, fallbackId) {
|
|
201
|
+
return {
|
|
202
|
+
id: extractIdFromFilename(file) || fallbackId,
|
|
203
|
+
file,
|
|
204
|
+
cwd: '(unknown)',
|
|
205
|
+
startedAt: stat.birthtime?.toISOString() || null,
|
|
206
|
+
updatedAt: stat.mtime.toISOString(),
|
|
207
|
+
cliVersion: '',
|
|
208
|
+
provider: '',
|
|
209
|
+
turns: 0,
|
|
210
|
+
first: '',
|
|
211
|
+
last: '',
|
|
212
|
+
lastAssistant: '',
|
|
213
|
+
messages: [],
|
|
214
|
+
backend: 'pi',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function firstUserText(messages) {
|
|
219
|
+
const m = messages.find((msg) => msg.role === 'user');
|
|
220
|
+
return m ? m.text : '';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function lastUserText(messages) {
|
|
224
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
225
|
+
if (messages[i].role === 'user') return messages[i].text;
|
|
226
|
+
}
|
|
227
|
+
return '';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function lastAssistantText(messages) {
|
|
231
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
232
|
+
if (messages[i].role === 'assistant') return messages[i].text;
|
|
233
|
+
}
|
|
234
|
+
return '';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// CLI execution
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
function runArgv(argv, cwd, inherit) {
|
|
242
|
+
const shellCommand = `exec ${argv.map(shellQuote).join(' ')}`;
|
|
243
|
+
const shell = commandShell();
|
|
244
|
+
if (inherit) {
|
|
245
|
+
const child = spawn(shell, ['-lc', shellCommand], { stdio: 'inherit', cwd, env: process.env });
|
|
246
|
+
child.on('error', (err) => { console.error(`error: failed to start pi: ${err.message}`); process.exit(1); });
|
|
247
|
+
child.on('exit', (code, signal) => { if (signal) process.kill(process.pid, signal); process.exit(code || 0); });
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
const result = spawnSync(shell, ['-lc', shellCommand], { stdio: 'inherit', cwd, env: process.env });
|
|
251
|
+
if (result.error) throw new Error(`failed to start pi: ${result.error.message}`);
|
|
252
|
+
const status = typeof result.status === 'number' ? result.status : 1;
|
|
253
|
+
process.exitCode = status;
|
|
254
|
+
return status;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// pi CLI commands mapping
|
|
258
|
+
function runSessionCommand(command, session, args, inherit) {
|
|
259
|
+
const executable = resolvePiBin();
|
|
260
|
+
const cwd = usableCwd(session.cwd);
|
|
261
|
+
|
|
262
|
+
switch (command) {
|
|
263
|
+
case 'resume': {
|
|
264
|
+
// pi --session <file> [args...]
|
|
265
|
+
let argv = [executable, '--session', session.file];
|
|
266
|
+
if (args && args.length) argv.push('-p', args.join(' '));
|
|
267
|
+
return runArgv(argv, cwd, inherit);
|
|
268
|
+
}
|
|
269
|
+
case 'fork': {
|
|
270
|
+
// pi --fork <file>
|
|
271
|
+
const argv = [executable, '--fork', session.file];
|
|
272
|
+
return runArgv(argv, cwd, inherit);
|
|
273
|
+
}
|
|
274
|
+
case 'delete': {
|
|
275
|
+
// pi has no delete CLI – just remove the file
|
|
276
|
+
// A force flag is handled by session-sources calling deleteSessionFile
|
|
277
|
+
return -1; // signal that file-based deletion should be used
|
|
278
|
+
}
|
|
279
|
+
case 'archive':
|
|
280
|
+
case 'unarchive': {
|
|
281
|
+
updateMetadata(session, { archived: command === 'archive' });
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
default:
|
|
285
|
+
throw new Error(`Unknown command for pi backend: ${command}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function runNew(cwd, args, inherit) {
|
|
290
|
+
const executable = resolvePiBin();
|
|
291
|
+
let argv = [executable];
|
|
292
|
+
if (args && args.length) argv.push('-p', args.join(' '));
|
|
293
|
+
return runArgv(argv, usableCwd(cwd), inherit);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// Provider interface
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
function isAvailable() {
|
|
301
|
+
try { return fs.statSync(PI_SESSIONS_DIR).isDirectory(); } catch { return false; }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function getSessionFiles() {
|
|
305
|
+
return walk(PI_SESSIONS_DIR);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function resolveBin() {
|
|
309
|
+
try { return resolvePiBin(); } catch { return null; }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
module.exports = {
|
|
313
|
+
id: 'pi',
|
|
314
|
+
label: 'pi',
|
|
315
|
+
isAvailable,
|
|
316
|
+
getSessionFiles,
|
|
317
|
+
parseSession,
|
|
318
|
+
resolveBin,
|
|
319
|
+
runCommand: runSessionCommand,
|
|
320
|
+
runNew,
|
|
321
|
+
usableCwd,
|
|
322
|
+
// Re-exports
|
|
323
|
+
shellQuote,
|
|
324
|
+
commandShell,
|
|
325
|
+
resolvePiBin,
|
|
326
|
+
};
|
|
@@ -1,69 +1,34 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function commandShell() {
|
|
26
|
-
const shell = process.env.SHELL || '/bin/sh';
|
|
27
|
-
try {
|
|
28
|
-
fs.accessSync(shell, fs.constants.X_OK);
|
|
29
|
-
return shell;
|
|
30
|
-
} catch {
|
|
31
|
-
return '/bin/sh';
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function runCodexArgv(argv, cwd, inherit = false) {
|
|
36
|
-
const shellCommand = `exec ${argv.map(shellQuote).join(' ')}`;
|
|
37
|
-
const shell = commandShell();
|
|
38
|
-
if (inherit) {
|
|
39
|
-
const child = spawn(shell, ['-lc', shellCommand], { stdio: 'inherit', cwd, env: process.env });
|
|
40
|
-
child.on('error', (err) => {
|
|
41
|
-
console.error(`error: failed to start codex: ${err.message}`);
|
|
42
|
-
process.exit(1);
|
|
43
|
-
});
|
|
44
|
-
child.on('exit', (code, signal) => {
|
|
45
|
-
if (signal) process.kill(process.pid, signal);
|
|
46
|
-
process.exit(code || 0);
|
|
47
|
-
});
|
|
48
|
-
return undefined;
|
|
49
|
-
}
|
|
50
|
-
const result = spawnSync(shell, ['-lc', shellCommand], { stdio: 'inherit', cwd, env: process.env });
|
|
51
|
-
if (result.error) throw new Error(`failed to start codex: ${result.error.message}`);
|
|
52
|
-
const status = typeof result.status === 'number' ? result.status : 1;
|
|
53
|
-
process.exitCode = status;
|
|
54
|
-
return status;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function runCodexCommand(command, session, args = [], inherit = false) {
|
|
58
|
-
const executable = resolveCodexBin();
|
|
59
|
-
const argv = [executable, command, session.id, ...args];
|
|
60
|
-
return runCodexArgv(argv, usableCwd(session.cwd), inherit);
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Backward-compatible re-exports – delegates to the provider layer.
|
|
5
|
+
// New code should import directly from src/providers/ and use
|
|
6
|
+
// providerForSession() to route by session.backend.
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
const codex = require('../providers/codex');
|
|
10
|
+
const { providerForSession } = require('../providers');
|
|
11
|
+
|
|
12
|
+
// Re-export low-level helpers
|
|
13
|
+
const shellQuote = codex.shellQuote;
|
|
14
|
+
const commandShell = codex.commandShell;
|
|
15
|
+
const usableCwd = codex.usableCwd;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Run a CLI command against a session, routing to the correct provider backend.
|
|
19
|
+
*/
|
|
20
|
+
function runCodexCommand(command, session, args, inherit) {
|
|
21
|
+
const provider = providerForSession(session);
|
|
22
|
+
return provider.runCommand(command, session, args, inherit);
|
|
61
23
|
}
|
|
62
24
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Start a new session, routing to the correct provider backend.
|
|
27
|
+
* Defaults to codex if no backend is specified.
|
|
28
|
+
*/
|
|
29
|
+
function runNewCodexSession(cwd, args, inherit, backend) {
|
|
30
|
+
const provider = backend ? require('../providers').getProvider(backend) : codex;
|
|
31
|
+
return provider.runNew(cwd, args, inherit);
|
|
67
32
|
}
|
|
68
33
|
|
|
69
34
|
module.exports = {
|
|
@@ -6,6 +6,7 @@ const { listSessions, updateMetadata } = require('../model/session-store');
|
|
|
6
6
|
const { listServers } = require('../model/workbench-config');
|
|
7
7
|
const { runCodexCommand, runNewCodexSession, usableCwd } = require('./codex-runner');
|
|
8
8
|
const { runRemoteCwb, runRemoteCwbJson, runRemoteCwbJsonAsync } = require('./ssh-runner');
|
|
9
|
+
const { getAvailableProviders } = require('../providers');
|
|
9
10
|
|
|
10
11
|
const LOCAL_SOURCE = {
|
|
11
12
|
id: 'local',
|
|
@@ -46,7 +47,8 @@ function sortSessions(sessions) {
|
|
|
46
47
|
return sessions;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
function loadLocalWorkbenchSessions(sources
|
|
50
|
+
function loadLocalWorkbenchSessions(sources) {
|
|
51
|
+
if (!sources) sources = configuredSources();
|
|
50
52
|
const sessions = listSessions().map((session) => attachSource(session, LOCAL_SOURCE));
|
|
51
53
|
return { errors: [], sessions, sources };
|
|
52
54
|
}
|
|
@@ -91,8 +93,48 @@ function resultStatus(result) {
|
|
|
91
93
|
return typeof result.status === 'number' ? result.status : 1;
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
+
function defaultBackend() {
|
|
97
|
+
const available = getAvailableProviders();
|
|
98
|
+
if (available.length === 0) return 'codex'; // fallback
|
|
99
|
+
// Prefer codex for backward compat; use the only one if just one is available
|
|
100
|
+
const codexAvail = available.find((p) => p.id === 'codex');
|
|
101
|
+
return codexAvail ? 'codex' : available[0].id;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function providerSummary(provider) {
|
|
105
|
+
return {
|
|
106
|
+
id: provider.id,
|
|
107
|
+
label: provider.label || provider.id,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function listLocalBackends() {
|
|
112
|
+
return getAvailableProviders().map(providerSummary);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function listSourceBackends(source) {
|
|
116
|
+
if (!source || !source.remote) return listLocalBackends();
|
|
117
|
+
const payload = runRemoteCwbJson(source, ['backends', '--json']);
|
|
118
|
+
if (!Array.isArray(payload)) throw new Error('remote backends did not return an array');
|
|
119
|
+
return payload
|
|
120
|
+
.filter((backend) => backend && backend.id)
|
|
121
|
+
.map((backend) => ({
|
|
122
|
+
id: String(backend.id),
|
|
123
|
+
label: backend.label ? String(backend.label) : String(backend.id),
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function runSourceSessionCommand(session, command, args) {
|
|
128
|
+
if (!session.sourceRemote) {
|
|
129
|
+
const status = runCodexCommand(command, session, args);
|
|
130
|
+
// If the provider returns -1 (e.g. pi delete = file-based), fall through to file deletion
|
|
131
|
+
if (status === -1) {
|
|
132
|
+
const { deleteSessionFile } = require('../model/session-store');
|
|
133
|
+
deleteSessionFile(session);
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
return status;
|
|
137
|
+
}
|
|
96
138
|
const source = configuredSourceOrThrow(session.sourceId);
|
|
97
139
|
const tty = command === 'resume' || command === 'fork';
|
|
98
140
|
const result = runRemoteCwb(source, [command, session.id, ...args], { tty });
|
|
@@ -102,9 +144,10 @@ function runSourceSessionCommand(session, command, args = []) {
|
|
|
102
144
|
return status;
|
|
103
145
|
}
|
|
104
146
|
|
|
105
|
-
function runSourceNewSession(source, cwd, args
|
|
106
|
-
if (!source || !source.remote) return runNewCodexSession(cwd, args);
|
|
107
|
-
const
|
|
147
|
+
function runSourceNewSession(source, cwd, args, backend) {
|
|
148
|
+
if (!source || !source.remote) return runNewCodexSession(cwd, args, true, backend || defaultBackend());
|
|
149
|
+
const backendArgs = backend ? ['--backend', backend] : [];
|
|
150
|
+
const result = runRemoteCwb(source, ['new', '--cwd', cwd, ...backendArgs, ...args], { tty: true });
|
|
108
151
|
if (result.error) throw result.error;
|
|
109
152
|
const status = resultStatus(result);
|
|
110
153
|
process.exitCode = status;
|
|
@@ -122,8 +165,6 @@ function updateSourceMetadata(session, patch) {
|
|
|
122
165
|
result = runRemoteCwb(source, ['rename', session.id, patch.name || '']);
|
|
123
166
|
} else if (Object.prototype.hasOwnProperty.call(patch, 'note')) {
|
|
124
167
|
result = runRemoteCwb(source, ['note', session.id, patch.note || '']);
|
|
125
|
-
} else if (Object.prototype.hasOwnProperty.call(patch, 'hidden')) {
|
|
126
|
-
result = runRemoteCwb(source, [patch.hidden ? 'hide' : 'unhide', session.id]);
|
|
127
168
|
}
|
|
128
169
|
if (!result) return 0;
|
|
129
170
|
if (result.error) throw result.error;
|
|
@@ -156,6 +197,8 @@ module.exports = {
|
|
|
156
197
|
configuredSources,
|
|
157
198
|
createSourceDirectory,
|
|
158
199
|
listSourceDirectories,
|
|
200
|
+
listLocalBackends,
|
|
201
|
+
listSourceBackends,
|
|
159
202
|
loadLocalWorkbenchSessions,
|
|
160
203
|
loadRemoteSourceSessions,
|
|
161
204
|
loadWorkbenchSessions,
|
|
@@ -164,4 +207,5 @@ module.exports = {
|
|
|
164
207
|
sourceById,
|
|
165
208
|
sourceKey,
|
|
166
209
|
updateSourceMetadata,
|
|
210
|
+
defaultBackend,
|
|
167
211
|
};
|