reslop 0.3.1 → 0.3.2
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/CHANGELOG.md +16 -1
- package/README.md +21 -3
- package/lib/agents/acp.js +309 -0
- package/lib/agents/agents.js +163 -436
- package/lib/agents/allow.js +141 -52
- package/lib/agents/allow.json +16 -14
- package/lib/agents/models.js +250 -383
- package/lib/agents/registry.js +437 -0
- package/lib/agents/run.js +234 -0
- package/lib/agents/transcript.js +143 -0
- package/lib/cli.js +2 -1
- package/lib/common/process.js +26 -1
- package/lib/dashboard/git.js +11 -1
- package/lib/dashboard/reports.js +80 -0
- package/lib/dashboard/watch.js +43 -8
- package/lib/dashboard/workspace.js +19 -20
- package/lib/dashboard/worktrees.js +145 -0
- package/lib/git/dir.js +50 -0
- package/lib/git/linked.js +152 -0
- package/lib/git/local.js +179 -0
- package/lib/git/status.js +126 -0
- package/lib/git/worktree.js +113 -36
- package/lib/input/actions.js +83 -5
- package/lib/input/keys.js +30 -1
- package/lib/input/line-edit.js +5 -1
- package/lib/render/agents.js +50 -24
- package/lib/render/branches.js +55 -27
- package/lib/render/field.js +19 -5
- package/lib/render/footer.js +14 -6
- package/lib/render/frame.js +16 -2
- package/lib/render/menu.js +103 -6
- package/lib/render/name-edit.js +31 -0
- package/lib/render/repos.js +52 -8
- package/lib/render/status.js +22 -4
- package/lib/report/run.js +16 -23
- package/lib/review/agent-instructions.md +2 -2
- package/lib/runs/process.js +3 -3
- package/lib/runs/runs.js +27 -4
- package/lib/session/agents/agents.js +220 -205
- package/lib/session/agents/away.js +48 -0
- package/lib/session/agents/job.js +23 -86
- package/lib/session/agents/jobs.js +14 -27
- package/lib/session/agents/options.js +45 -101
- package/lib/session/agents/plans.js +53 -0
- package/lib/session/agents/view.js +34 -0
- package/lib/session/branches.js +18 -1
- package/lib/session/compose.js +13 -5
- package/lib/session/dispatch.js +43 -4
- package/lib/session/editor-pointer.js +98 -0
- package/lib/session/files-pane.js +3 -0
- package/lib/session/lifecycle.js +67 -10
- package/lib/session/navigation.js +5 -0
- package/lib/session/npm/history.js +2 -1
- package/lib/session/npm/npm.js +89 -12
- package/lib/session/ops.js +3 -0
- package/lib/session/pointer.js +304 -46
- package/lib/session/session.js +15 -2
- package/lib/session/terminal.js +2 -2
- package/lib/session/text-menu.js +50 -0
- package/lib/session/view.js +19 -2
- package/lib/session/workspace.js +179 -38
- package/lib/session/worktrees.js +280 -0
- package/lib/term/themes/dark.json +4 -0
- package/lib/term/themes/light.json +4 -0
- package/package.json +2 -2
package/lib/agents/agents.js
CHANGED
|
@@ -2,164 +2,109 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
|
-
const {
|
|
6
|
-
const { IS_WIN, runProc, spawnBase } = require('../common/process.js');
|
|
7
|
-
const { trimText } = require('../common/utilities.js');
|
|
5
|
+
const { IS_WIN, npmBin, npmOpts, spawnBase } = require('../common/process.js');
|
|
8
6
|
const { commandEnv, PipeProcess } = require('../runs/process.js');
|
|
9
|
-
const {
|
|
7
|
+
const { trimText } = require('../common/utilities.js');
|
|
8
|
+
const { COMMANDS } = require('./allow.js');
|
|
10
9
|
const models = require('./models.js');
|
|
11
|
-
const {
|
|
12
|
-
const {
|
|
13
|
-
const
|
|
10
|
+
const { choiceOf } = models;
|
|
11
|
+
const { startRun, probeSession } = require('./run.js');
|
|
12
|
+
const registry = require('./registry.js');
|
|
13
|
+
const { installedRegistryAgent } = registry;
|
|
14
|
+
const { resolveGitDirs } = require('../git/dir.js');
|
|
14
15
|
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
const GITDIR_PREFIX = 'gitdir:';
|
|
16
|
+
const CODEX_SANDBOX = 'sandbox_workspace_write';
|
|
17
|
+
const CODEX_ROOTS = 'writable_roots';
|
|
18
|
+
const AGY_SERVER = IS_WIN ? 'agy_acp_server.exe' : 'agy_acp_server.par';
|
|
19
|
+
const AGY_ARGS = process.platform === 'linux' ? ['--uid='] : [];
|
|
20
20
|
|
|
21
|
-
const
|
|
22
|
-
|
|
21
|
+
const realPath = (file) => {
|
|
22
|
+
try {
|
|
23
|
+
return fs.realpathSync.native(file);
|
|
24
|
+
} catch {
|
|
25
|
+
return path.resolve(file);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
// workspace-write remounts .git read-only unless that path is itself writable.
|
|
30
|
+
const gitWriteRoots = (cwd) => {
|
|
31
|
+
if (!cwd) return [];
|
|
32
|
+
const dirs = resolveGitDirs(cwd);
|
|
33
|
+
if (!dirs) return [];
|
|
34
|
+
const roots = [realPath(dirs.gitDir), realPath(dirs.commonDir)];
|
|
35
|
+
return [...new Set(roots)];
|
|
29
36
|
};
|
|
30
37
|
|
|
31
|
-
|
|
38
|
+
// The adapter merges this into the session config of codex
|
|
39
|
+
const codexEnv = (cwd, env = process.env) => {
|
|
40
|
+
const roots = gitWriteRoots(cwd);
|
|
41
|
+
if (!roots.length || env.CODEX_CONFIG) return {};
|
|
42
|
+
const config = { [CODEX_SANDBOX]: { [CODEX_ROOTS]: roots } };
|
|
43
|
+
return { CODEX_CONFIG: JSON.stringify(config) };
|
|
44
|
+
};
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
'
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
'none',
|
|
40
|
-
'--allowedTools',
|
|
41
|
-
...BASH_ALLOWS,
|
|
42
|
-
];
|
|
46
|
+
const antigravityEnv = (cwd, row) => {
|
|
47
|
+
if (!row || !row.bin) return {};
|
|
48
|
+
const name = IS_WIN ? 'localharness_external.exe' : 'localharness_external';
|
|
49
|
+
const helper = path.join(path.dirname(row.bin), name);
|
|
50
|
+
return fs.existsSync(helper) ? { ANTIGRAVITY_HARNESS_PATH: helper } : {};
|
|
51
|
+
};
|
|
43
52
|
|
|
53
|
+
// Each agent is reached through the command that serves ACP on its stdio:
|
|
54
|
+
// its own subcommand, or the adapter that wraps its SDK
|
|
44
55
|
const defineAgent = (spec) => ({
|
|
45
56
|
id: spec.id,
|
|
46
57
|
name: spec.name ?? spec.id,
|
|
47
58
|
title: spec.title,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
prefix: spec.prefix ?? [],
|
|
56
|
-
promptFlag: spec.promptFlag ?? '',
|
|
57
|
-
login: spec.login ?? [],
|
|
58
|
-
status: spec.status ?? [],
|
|
59
|
-
contexts: spec.contexts ?? [],
|
|
60
|
-
resumeWith: spec.resumeWith ?? null,
|
|
61
|
-
listCmds: spec.listCmds,
|
|
62
|
-
parseModels: spec.parseModels ?? parseNameList,
|
|
63
|
-
wideModels: spec.wideModels ?? null,
|
|
64
|
-
partialList: spec.partialList === true,
|
|
65
|
-
modelEfforts: spec.modelEfforts ?? null,
|
|
66
|
-
effortCmds: spec.effortCmds ?? [],
|
|
67
|
-
allowlist: spec.allowlist === true,
|
|
68
|
-
denied: spec.denied ?? null,
|
|
69
|
-
savesExtra: spec.savesExtra === true,
|
|
59
|
+
commands: spec.commands,
|
|
60
|
+
package: spec.package ?? '',
|
|
61
|
+
registry: spec.registry ?? '',
|
|
62
|
+
installLabel: spec.installLabel ?? '',
|
|
63
|
+
hint: spec.hint ?? '',
|
|
64
|
+
mode: spec.mode ?? '',
|
|
65
|
+
env: spec.env ?? (() => ({})),
|
|
70
66
|
});
|
|
71
67
|
|
|
72
68
|
const AGENTS = [
|
|
73
69
|
defineAgent({
|
|
74
70
|
id: 'claude',
|
|
75
71
|
title: 'Claude Code',
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
prefix: CLAUDE_PREFIX,
|
|
81
|
-
resumeWith: (id) => ['--resume', id],
|
|
82
|
-
// No command lists models: the help names the aliases and the efforts
|
|
83
|
-
listCmds: [['--help']],
|
|
84
|
-
parseModels: parseHelpModels,
|
|
85
|
-
partialList: true,
|
|
86
|
-
effortCmds: [['--help']],
|
|
72
|
+
commands: [['claude-agent-acp'], ['claude-code-acp']],
|
|
73
|
+
package: '@agentclientprotocol/claude-agent-acp',
|
|
74
|
+
// Edits are accepted by the agent, the rest is asked of the run
|
|
75
|
+
mode: 'acceptEdits',
|
|
87
76
|
}),
|
|
88
77
|
defineAgent({
|
|
89
78
|
id: 'opencode',
|
|
90
79
|
title: 'OpenCode',
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
resumeWith: (id) => ['--session', id],
|
|
94
|
-
listCmds: [['models']],
|
|
80
|
+
commands: [['opencode', 'acp']],
|
|
81
|
+
package: 'opencode-ai',
|
|
95
82
|
}),
|
|
96
83
|
defineAgent({
|
|
97
84
|
id: 'cursor',
|
|
98
85
|
title: 'Cursor',
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
allowlist: true,
|
|
105
|
-
login: ['login'],
|
|
106
|
-
status: ['status', '--format', 'json'],
|
|
107
|
-
contexts: ['256k', '1m'],
|
|
108
|
-
resumeWith: (id) => [`--resume=${id}`],
|
|
109
|
-
listCmds: [['--list-models'], ['models']],
|
|
110
|
-
parseModels: parseCursorModels,
|
|
111
|
-
wideModels: parseCursorWide,
|
|
86
|
+
commands: [
|
|
87
|
+
['cursor-agent', 'acp'],
|
|
88
|
+
['agent', 'acp'],
|
|
89
|
+
],
|
|
90
|
+
hint: 'needs the Cursor CLI from cursor.com/cli',
|
|
112
91
|
}),
|
|
113
92
|
defineAgent({
|
|
114
93
|
id: 'codex',
|
|
115
94
|
title: 'Codex',
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
efforts: ['default', 'low', 'medium', 'high', 'xhigh'],
|
|
121
|
-
// The TUI rejects a closed stdin; exec is the non-interactive entry
|
|
122
|
-
prefix: ['exec'],
|
|
123
|
-
resumeWith: (id) => ['resume', id],
|
|
124
|
-
listCmds: [
|
|
125
|
-
['debug', 'models', '--bundled'],
|
|
126
|
-
['debug', 'models'],
|
|
127
|
-
['models'],
|
|
128
|
-
],
|
|
129
|
-
parseModels: parseJsonModels,
|
|
130
|
-
modelEfforts: parseJsonEfforts,
|
|
95
|
+
commands: [['codex-acp']],
|
|
96
|
+
package: '@agentclientprotocol/codex-acp',
|
|
97
|
+
mode: 'workspace-write',
|
|
98
|
+
env: codexEnv,
|
|
131
99
|
}),
|
|
132
100
|
defineAgent({
|
|
133
101
|
id: 'agy',
|
|
134
102
|
title: 'Antigravity',
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
'gemini-3.8-flash-low',
|
|
141
|
-
'gemini-3.7-flash-high',
|
|
142
|
-
'gemini-3.7-flash-medium',
|
|
143
|
-
'gemini-3.7-flash-low',
|
|
144
|
-
'gemini-3.6-flash-high',
|
|
145
|
-
'gemini-3.6-flash-medium',
|
|
146
|
-
'gemini-3.6-flash-low',
|
|
147
|
-
'gemini-3.1-pro-high',
|
|
148
|
-
'gemini-3.1-pro-low',
|
|
149
|
-
'claude-sonnet-4-6',
|
|
150
|
-
'claude-opus-4-6-thinking',
|
|
151
|
-
'gpt-oss-120b-medium',
|
|
152
|
-
],
|
|
153
|
-
effortFlag: '--effort',
|
|
154
|
-
efforts: ['default', 'low', 'medium', 'high'],
|
|
155
|
-
promptFlag: '-p',
|
|
156
|
-
// Exits 0 after a refused tool call, so the output tells the outcome
|
|
157
|
-
denied: PERMISSION_FAIL,
|
|
158
|
-
// Extra flags in .reslop are the opt-in to run without confirmations
|
|
159
|
-
savesExtra: true,
|
|
160
|
-
resumeWith: (id) => ['--conversation', id],
|
|
161
|
-
listCmds: [['models']],
|
|
162
|
-
effortCmds: [['--help']],
|
|
103
|
+
commands: [[AGY_SERVER, ...AGY_ARGS], ['agy-acp']],
|
|
104
|
+
registry: 'antigravity-acp',
|
|
105
|
+
installLabel: 'Google Antigravity ACP',
|
|
106
|
+
hint: 'install Google Antigravity ACP',
|
|
107
|
+
env: antigravityEnv,
|
|
163
108
|
}),
|
|
164
109
|
];
|
|
165
110
|
|
|
@@ -204,12 +149,25 @@ const findBin = (name, env = process.env) => {
|
|
|
204
149
|
return '';
|
|
205
150
|
};
|
|
206
151
|
|
|
207
|
-
const
|
|
208
|
-
for (const name of spec.
|
|
209
|
-
const
|
|
210
|
-
if (
|
|
152
|
+
const findCommand = (spec, env) => {
|
|
153
|
+
for (const [name, ...args] of spec.commands) {
|
|
154
|
+
const bin = findBin(name, env);
|
|
155
|
+
if (bin) return { bin, args };
|
|
211
156
|
}
|
|
212
|
-
|
|
157
|
+
if (spec.registry) {
|
|
158
|
+
const installed = installedRegistryAgent(spec.registry, { env });
|
|
159
|
+
if (installed) return installed;
|
|
160
|
+
}
|
|
161
|
+
return { bin: '', args: [] };
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// What a missing agent takes: an npm package or an official registry binary
|
|
165
|
+
const installHint = (spec, env) => {
|
|
166
|
+
if (spec.package) return `npm i -g ${spec.package}`;
|
|
167
|
+
if (spec.registry && findBin('agy', env)) {
|
|
168
|
+
return 'agy installed; install Google Antigravity ACP';
|
|
169
|
+
}
|
|
170
|
+
return spec.hint;
|
|
213
171
|
};
|
|
214
172
|
|
|
215
173
|
const detectAgents = (env = process.env) =>
|
|
@@ -217,339 +175,115 @@ const detectAgents = (env = process.env) =>
|
|
|
217
175
|
id: spec.id,
|
|
218
176
|
name: spec.name,
|
|
219
177
|
spec,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
178
|
+
package: spec.package,
|
|
179
|
+
installable: Boolean(spec.package || spec.registry),
|
|
180
|
+
installLabel: spec.installLabel,
|
|
181
|
+
hint: installHint(spec, env),
|
|
182
|
+
catalog: null,
|
|
183
|
+
...findCommand(spec, env),
|
|
225
184
|
}));
|
|
226
185
|
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const tryList = async (run, bin, args, options) => {
|
|
231
|
-
try {
|
|
232
|
-
return await run(bin, args, options);
|
|
233
|
-
} catch {
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
const listedOf = (listed) => {
|
|
239
|
-
const bare = Array.isArray(listed);
|
|
240
|
-
const given = bare ? { names: listed, wide: listed.wide } : listed;
|
|
241
|
-
const from = given && typeof given === 'object' ? given : {};
|
|
242
|
-
const efforts = Array.isArray(from.efforts) ? from.efforts : [];
|
|
243
|
-
const perModel = from.modelEfforts instanceof Map ? from.modelEfforts : null;
|
|
244
|
-
return {
|
|
245
|
-
names: Array.isArray(from.names) ? from.names : [],
|
|
246
|
-
wide: Array.isArray(from.wide) ? from.wide : null,
|
|
247
|
-
efforts: efforts.length ? efforts : null,
|
|
248
|
-
modelEfforts: perModel && perModel.size ? perModel : null,
|
|
249
|
-
};
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
// One answer per command line, so a command that serves two lists runs once
|
|
253
|
-
class CliProbe {
|
|
254
|
-
constructor(bin, options) {
|
|
255
|
-
this.bin = bin;
|
|
256
|
-
this.run = options.run ?? runProc;
|
|
257
|
-
this.env = { ...process.env, ...options.env };
|
|
258
|
-
this.timeout = options.timeout ?? LIST_MS;
|
|
259
|
-
this.texts = new Map();
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async read(args) {
|
|
263
|
-
const key = args.join(' ');
|
|
264
|
-
if (this.texts.has(key)) return this.texts.get(key);
|
|
265
|
-
const { env, timeout } = this;
|
|
266
|
-
const result = await tryList(this.run, this.bin, args, { env, timeout });
|
|
267
|
-
const { stdout = '', stderr = '' } = result ?? {};
|
|
268
|
-
const text = result ? `${stdout}\n${stderr}` : '';
|
|
269
|
-
this.texts.set(key, text);
|
|
270
|
-
return text;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
async first(cmds, parse) {
|
|
274
|
-
for (const args of cmds) {
|
|
275
|
-
const text = await this.read(args);
|
|
276
|
-
const found = text ? parse(text) : [];
|
|
277
|
-
if (found.length) return { text, found };
|
|
278
|
-
}
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const listModels = async (bin, spec, options = {}) => {
|
|
284
|
-
const listed = { names: [], wide: null, efforts: null, modelEfforts: null };
|
|
285
|
-
if (!bin || !spec) return listed;
|
|
286
|
-
const probe = new CliProbe(bin, options);
|
|
287
|
-
const models = await probe.first(spec.listCmds, spec.parseModels);
|
|
288
|
-
if (models) {
|
|
289
|
-
const { text } = models;
|
|
290
|
-
listed.names = models.found;
|
|
291
|
-
listed.wide = spec.wideModels ? spec.wideModels(text) : null;
|
|
292
|
-
listed.modelEfforts = spec.modelEfforts ? spec.modelEfforts(text) : null;
|
|
293
|
-
}
|
|
294
|
-
const levels = (text) => optionValues(text, spec.effortFlag);
|
|
295
|
-
const efforts = await probe.first(spec.effortCmds, levels);
|
|
296
|
-
if (efforts) listed.efforts = efforts.found;
|
|
297
|
-
return listed;
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
const splitArgs = (text) => trimText(text).split(/\s+/).filter(Boolean);
|
|
301
|
-
|
|
302
|
-
const quoteArg = (text) => {
|
|
303
|
-
const arg = `${text ?? ''}`;
|
|
304
|
-
if (!arg) return '""';
|
|
305
|
-
if (!/[\s"'\\]/.test(arg)) return arg;
|
|
306
|
-
return `"${arg.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
const commandLine = (launch) => {
|
|
310
|
-
if (!launch || !launch.ok || !launch.cmd) return '';
|
|
311
|
-
const args = launch.args ?? [];
|
|
312
|
-
const hasPrompt = launch.prompt && args.at(-1) === launch.prompt;
|
|
313
|
-
const shown = hasPrompt ? args.slice(0, -1) : args;
|
|
314
|
-
const plan = launch.plan ? quoteArg(launch.plan) : '';
|
|
315
|
-
const bin = path.basename(launch.cmd);
|
|
316
|
-
return [bin, ...shown.map(quoteArg), plan].filter(Boolean).join(' ');
|
|
186
|
+
const listModels = (row, options = {}) => {
|
|
187
|
+
if (!row || !row.bin) return Promise.resolve(null);
|
|
188
|
+
return probeSession(row, options);
|
|
317
189
|
};
|
|
318
190
|
|
|
319
191
|
const planRef = (reviewPath, cwd) => {
|
|
320
192
|
const file = `${reviewPath ?? ''}`;
|
|
321
193
|
if (!file || !cwd) return file;
|
|
322
|
-
const rel = path.relative(cwd, file);
|
|
194
|
+
const rel = path.relative(cwd, path.resolve(cwd, file));
|
|
323
195
|
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return file;
|
|
324
196
|
return rel.split(path.sep).join('/');
|
|
325
197
|
};
|
|
326
198
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (start < 0 || end <= start) return null;
|
|
336
|
-
const body = raw.slice(start, end + 1);
|
|
337
|
-
const data = jsonParse(body);
|
|
338
|
-
if (!data || typeof data !== 'object') return null;
|
|
339
|
-
if (typeof data.isAuthenticated === 'boolean') return data.isAuthenticated;
|
|
340
|
-
if (data.status === 'authenticated') return true;
|
|
341
|
-
if (data.status === 'unauthenticated') return false;
|
|
342
|
-
if (data.status === 'partially-authenticated') return false;
|
|
343
|
-
return null;
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
const authState = (text) => {
|
|
347
|
-
const raw = trimText(text);
|
|
348
|
-
if (!raw) return null;
|
|
349
|
-
const parsed = parsedAuth(raw);
|
|
350
|
-
if (parsed !== null) return parsed;
|
|
351
|
-
if (/not logged in/i.test(raw)) return false;
|
|
352
|
-
if (/logged in/i.test(raw)) return true;
|
|
353
|
-
return null;
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
const probeAuth = async (row) => {
|
|
357
|
-
const args = row && row.spec ? row.spec.status : [];
|
|
358
|
-
if (!row || !row.bin || !args.length) return null;
|
|
359
|
-
const result = await runProc(row.bin, args, {
|
|
360
|
-
env: commandEnv(),
|
|
361
|
-
timeout: LIST_MS,
|
|
362
|
-
});
|
|
363
|
-
if (!result || result.error) return null;
|
|
364
|
-
const text = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
|
|
365
|
-
return authState(text);
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
const effortArgs = (spec, effort) => {
|
|
369
|
-
const level = trimText(effort);
|
|
370
|
-
if (!spec.effortFlag || !level || level === 'default') return [];
|
|
371
|
-
return [spec.effortFlag, spec.effortValue(level)];
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
const statOf = (file) => {
|
|
375
|
-
try {
|
|
376
|
-
return fs.statSync(file);
|
|
377
|
-
} catch {
|
|
378
|
-
return null;
|
|
379
|
-
}
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
const readFirstLine = (file) => {
|
|
383
|
-
try {
|
|
384
|
-
return fs.readFileSync(file, 'utf8').split('\n')[0] ?? '';
|
|
385
|
-
} catch {
|
|
386
|
-
return '';
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
const realPath = (file) => {
|
|
391
|
-
try {
|
|
392
|
-
return fs.realpathSync.native(file);
|
|
393
|
-
} catch {
|
|
394
|
-
return path.resolve(file);
|
|
395
|
-
}
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
const gitDirOf = (dot) => {
|
|
399
|
-
const line = readFirstLine(dot).trim();
|
|
400
|
-
if (!line.startsWith(GITDIR_PREFIX)) return '';
|
|
401
|
-
const raw = line.slice(GITDIR_PREFIX.length).trim();
|
|
402
|
-
if (!raw) return '';
|
|
403
|
-
return path.resolve(path.dirname(dot), raw);
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
const commonGitDir = (gitDir) => {
|
|
407
|
-
const raw = readFirstLine(path.join(gitDir, 'commondir')).trim();
|
|
408
|
-
if (!raw) return gitDir;
|
|
409
|
-
return path.resolve(gitDir, raw);
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
// workspace-write remounts .git read-only unless that path is itself writable.
|
|
413
|
-
const gitWriteRoots = (cwd) => {
|
|
414
|
-
if (!cwd) return [];
|
|
415
|
-
const dot = path.join(path.resolve(cwd), '.git');
|
|
416
|
-
const stat = statOf(dot);
|
|
417
|
-
if (!stat) return [];
|
|
418
|
-
if (stat.isDirectory()) return [realPath(dot)];
|
|
419
|
-
if (!stat.isFile()) return [];
|
|
420
|
-
const gitDir = gitDirOf(dot);
|
|
421
|
-
if (!gitDir) return [];
|
|
422
|
-
const roots = [realPath(gitDir), realPath(commonGitDir(gitDir))];
|
|
423
|
-
return [...new Set(roots)];
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
const tomlQuote = (text) => {
|
|
427
|
-
const escaped = text.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
|
|
428
|
-
return `"${escaped}"`;
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
const codexGitArgs = (cwd) => {
|
|
432
|
-
const roots = gitWriteRoots(cwd);
|
|
433
|
-
if (!roots.length) return [];
|
|
434
|
-
const listed = roots.map(tomlQuote).join(',');
|
|
435
|
-
const value = `sandbox_workspace_write.writable_roots=[${listed}]`;
|
|
436
|
-
return ['--sandbox', 'workspace-write', '-c', value];
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
const paramPart = (key, value) => {
|
|
440
|
-
const text = trimText(value);
|
|
441
|
-
if (!text) return '';
|
|
442
|
-
return `${key}=${text}`;
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
const withParams = (model, parts) => {
|
|
446
|
-
const body = parts.filter(Boolean).join(',');
|
|
447
|
-
if (!body || !model || model === 'default') return model;
|
|
448
|
-
return `${model}[${body}]`;
|
|
449
|
-
};
|
|
199
|
+
// A refusal reads to an agent as a person saying no, and it stops to ask
|
|
200
|
+
const UNATTENDED = [
|
|
201
|
+
'You are running unattended: nobody can answer questions or approve',
|
|
202
|
+
'commands. Change files with your edit tools. The shell is limited to',
|
|
203
|
+
`${COMMANDS.join(', ')} and read-only filters such as cat, grep,`,
|
|
204
|
+
'head and tail. When a command is refused, do the step another way',
|
|
205
|
+
'and continue with the plan.',
|
|
206
|
+
].join(' ');
|
|
450
207
|
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
if (!level || level === 'default') return '';
|
|
454
|
-
if (`${model ?? ''}`.endsWith(`-${level}`)) return '';
|
|
455
|
-
return level;
|
|
456
|
-
};
|
|
457
|
-
|
|
458
|
-
const fastParam = (model, fast) => {
|
|
459
|
-
if (fast !== true) return '';
|
|
460
|
-
if (`${model ?? ''}`.endsWith('-fast')) return '';
|
|
461
|
-
return 'true';
|
|
462
|
-
};
|
|
208
|
+
const planPrompt = (reviewPath) =>
|
|
209
|
+
`Read and execute repair plan at ${reviewPath}\n\n${UNATTENDED}`;
|
|
463
210
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const level = effortParam(resolved.model, resolved.effort);
|
|
471
|
-
parts.push(paramPart('effort', level));
|
|
472
|
-
parts.push(paramPart('fast', fastParam(resolved.model, fast)));
|
|
473
|
-
return withParams(resolved.model, parts);
|
|
211
|
+
// What a run is shown as: the settings it was started with and its plan
|
|
212
|
+
const launchLabel = (launch) => {
|
|
213
|
+
const { choice } = launch;
|
|
214
|
+
const fast = choice.fast ? 'fast' : '';
|
|
215
|
+
const parts = [launch.name, choice.model, choice.effort, fast];
|
|
216
|
+
return [...parts, choice.context, launch.plan].filter(Boolean).join(' ');
|
|
474
217
|
};
|
|
475
218
|
|
|
476
|
-
const
|
|
219
|
+
const buildLaunch = (row, picked, reviewPath, cwd, sessionId = '') => {
|
|
220
|
+
if (!row || !row.bin) return { ok: false, error: 'not installed' };
|
|
221
|
+
if (!reviewPath) return { ok: false, error: 'no plan' };
|
|
222
|
+
const plan = planRef(reviewPath, cwd) || reviewPath;
|
|
477
223
|
const launch = {
|
|
478
224
|
ok: true,
|
|
479
|
-
cmd: row.bin,
|
|
480
|
-
args,
|
|
481
|
-
prompt,
|
|
482
|
-
plan,
|
|
483
225
|
id: row.id,
|
|
484
226
|
name: row.name,
|
|
485
|
-
kind,
|
|
227
|
+
kind: 'run',
|
|
228
|
+
cmd: row.bin,
|
|
229
|
+
args: (row.args ?? []).slice(),
|
|
230
|
+
mode: row.spec.mode,
|
|
231
|
+
env: row.spec.env(cwd, row),
|
|
232
|
+
choice: choiceOf(picked),
|
|
233
|
+
plan,
|
|
234
|
+
prompt: planPrompt(plan),
|
|
235
|
+
session: trimText(sessionId),
|
|
486
236
|
command: '',
|
|
487
237
|
};
|
|
488
|
-
launch.command =
|
|
238
|
+
launch.command = launchLabel(launch);
|
|
489
239
|
return launch;
|
|
490
240
|
};
|
|
491
241
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if (!
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
if (!reviewPath) return { ok: false, error: 'no plan' };
|
|
501
|
-
const { spec } = row;
|
|
502
|
-
const picked = choice ?? emptyChoice(row);
|
|
503
|
-
const catalog = catalogOf(row.models);
|
|
504
|
-
const resolved = catalog.withFast(catalog.resolve(picked), picked.fast);
|
|
505
|
-
const asked = trimText(picked.context);
|
|
506
|
-
const menu = contextMenu(resolved.model, row.wide, catalog, spec.contexts);
|
|
507
|
-
const pass = menu.includes(asked) && asked !== menu[0] ? asked : '';
|
|
508
|
-
const model = modelArg(spec, resolved, pass, picked.fast);
|
|
509
|
-
const plan = planRef(reviewPath, cwd) || reviewPath;
|
|
510
|
-
const prompt = planPrompt(plan);
|
|
511
|
-
const continued = resumeArgs(spec, sessionId);
|
|
512
|
-
const args = [...spec.prefix, ...continued];
|
|
513
|
-
if (model && model !== 'default') args.push(spec.modelFlag, model);
|
|
514
|
-
if (!spec.modelParams && !resolved.encoded) {
|
|
515
|
-
args.push(...effortArgs(spec, resolved.effort));
|
|
242
|
+
// The package lands in the global prefix of npm, where PATH finds its command
|
|
243
|
+
const buildInstall = (row) => {
|
|
244
|
+
if (!row) return { ok: false, error: 'no agent' };
|
|
245
|
+
if (row.bin) return { ok: false, error: 'installed' };
|
|
246
|
+
const name = row.spec.package;
|
|
247
|
+
const registryId = row.spec.registry;
|
|
248
|
+
if (!name && !registryId) {
|
|
249
|
+
return { ok: false, error: row.hint || 'no installer' };
|
|
516
250
|
}
|
|
517
|
-
|
|
518
|
-
args
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
return env;
|
|
251
|
+
const npmArgs = name ? ['install', '--global', name] : [];
|
|
252
|
+
const args = registryId
|
|
253
|
+
? [path.join(__dirname, 'registry.js'), registryId]
|
|
254
|
+
: npmArgs;
|
|
255
|
+
const cmd = registryId ? process.execPath : npmBin();
|
|
256
|
+
const command = registryId
|
|
257
|
+
? `install ${row.spec.installLabel || row.name}`
|
|
258
|
+
: `npm ${npmArgs.join(' ')}`;
|
|
259
|
+
return {
|
|
260
|
+
ok: true,
|
|
261
|
+
id: row.id,
|
|
262
|
+
name: row.name,
|
|
263
|
+
kind: 'install',
|
|
264
|
+
installer: registryId ? 'registry' : 'npm',
|
|
265
|
+
cmd,
|
|
266
|
+
args,
|
|
267
|
+
plan: '',
|
|
268
|
+
session: '',
|
|
269
|
+
command,
|
|
270
|
+
};
|
|
538
271
|
};
|
|
539
272
|
|
|
540
|
-
const
|
|
541
|
-
const
|
|
273
|
+
const startInstall = (cwd, launch, onData, onClose) => {
|
|
274
|
+
const extra = { cwd, env: commandEnv(), detached: !IS_WIN };
|
|
275
|
+
const options =
|
|
276
|
+
launch.installer === 'npm' ? npmOpts(extra) : spawnBase(extra);
|
|
542
277
|
const proc = new PipeProcess(onData, onClose);
|
|
543
278
|
proc.start(launch.cmd, launch.args, options).closeInput();
|
|
544
279
|
return proc;
|
|
545
280
|
};
|
|
546
281
|
|
|
547
|
-
// An agent that reads an allow list gets the plan entries before it runs
|
|
548
282
|
const startAgent = (cwd, launch, onData, onClose) => {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
return
|
|
283
|
+
if (launch.kind === 'install') {
|
|
284
|
+
return startInstall(cwd, launch, onData, onClose);
|
|
285
|
+
}
|
|
286
|
+
return startRun(cwd, launch, onData, onClose, launch.env);
|
|
553
287
|
};
|
|
554
288
|
|
|
555
289
|
module.exports = {
|
|
@@ -558,16 +292,9 @@ module.exports = {
|
|
|
558
292
|
agentSpec,
|
|
559
293
|
findBin,
|
|
560
294
|
detectAgents,
|
|
561
|
-
canLogin,
|
|
562
|
-
listedOf,
|
|
563
295
|
listModels,
|
|
564
|
-
splitArgs,
|
|
565
296
|
planPrompt,
|
|
566
|
-
needsAuth,
|
|
567
|
-
authState,
|
|
568
|
-
probeAuth,
|
|
569
|
-
commandLine,
|
|
570
297
|
buildLaunch,
|
|
571
|
-
|
|
298
|
+
buildInstall,
|
|
572
299
|
startAgent,
|
|
573
300
|
};
|