reslop 0.3.1 → 0.3.3
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 +28 -1
- package/README.md +30 -4
- package/lib/agents/acp.js +309 -0
- package/lib/agents/agents.js +274 -438
- 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/script.js +93 -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 +84 -34
- 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,181 @@
|
|
|
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
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
10
|
+
const { choiceOf } = models;
|
|
11
|
+
const { startRun, probeSession } = require('./run.js');
|
|
12
|
+
const registry = require('./registry.js');
|
|
13
|
+
const { installedRegistryAgent } = registry;
|
|
14
|
+
const { localBin, scriptUrl } = require('./script.js');
|
|
15
|
+
const { resolveGitDirs } = require('../git/dir.js');
|
|
16
|
+
|
|
17
|
+
const CODEX_SANDBOX = 'sandbox_workspace_write';
|
|
18
|
+
const CODEX_ROOTS = 'writable_roots';
|
|
19
|
+
const AGY_SERVER = IS_WIN ? 'agy_acp_server.exe' : 'agy_acp_server.par';
|
|
20
|
+
const AGY_ARGS = process.platform === 'linux' ? ['--uid='] : [];
|
|
21
|
+
|
|
22
|
+
const realPath = (file) => {
|
|
23
|
+
try {
|
|
24
|
+
return fs.realpathSync.native(file);
|
|
25
|
+
} catch {
|
|
26
|
+
return path.resolve(file);
|
|
27
|
+
}
|
|
29
28
|
};
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
// workspace-write remounts .git read-only unless that path is itself writable.
|
|
31
|
+
const gitWriteRoots = (cwd) => {
|
|
32
|
+
if (!cwd) return [];
|
|
33
|
+
const dirs = resolveGitDirs(cwd);
|
|
34
|
+
if (!dirs) return [];
|
|
35
|
+
const roots = [realPath(dirs.gitDir), realPath(dirs.commonDir)];
|
|
36
|
+
return [...new Set(roots)];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// The adapter merges this into the session config of codex
|
|
40
|
+
const codexEnv = (cwd, env = process.env) => {
|
|
41
|
+
const roots = gitWriteRoots(cwd);
|
|
42
|
+
if (!roots.length || env.CODEX_CONFIG) return {};
|
|
43
|
+
const config = { [CODEX_SANDBOX]: { [CODEX_ROOTS]: roots } };
|
|
44
|
+
return { CODEX_CONFIG: JSON.stringify(config) };
|
|
45
|
+
};
|
|
43
46
|
|
|
47
|
+
const antigravityEnv = (cwd, row) => {
|
|
48
|
+
if (!row || !row.bin) return {};
|
|
49
|
+
const name = IS_WIN ? 'localharness_external.exe' : 'localharness_external';
|
|
50
|
+
const helper = path.join(path.dirname(row.bin), name);
|
|
51
|
+
return fs.existsSync(helper) ? { ANTIGRAVITY_HARNESS_PATH: helper } : {};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Each agent is reached through the command that serves ACP on its stdio:
|
|
55
|
+
// its own subcommand, or the adapter that wraps its SDK
|
|
44
56
|
const defineAgent = (spec) => ({
|
|
45
57
|
id: spec.id,
|
|
46
58
|
name: spec.name ?? spec.id,
|
|
47
59
|
title: spec.title,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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,
|
|
60
|
+
commands: spec.commands,
|
|
61
|
+
package: spec.package ?? '',
|
|
62
|
+
// The cli of the agent that an adapter starts, as { bin, package }
|
|
63
|
+
requires: spec.requires ?? null,
|
|
64
|
+
registry: spec.registry ?? '',
|
|
65
|
+
// The install scripts of the vendor, as { unix, win } https urls
|
|
66
|
+
script: spec.script ?? null,
|
|
67
|
+
installLabel: spec.installLabel ?? '',
|
|
68
|
+
hint: spec.hint ?? '',
|
|
69
|
+
// A program of the vendor that does not serve ACP, as { bin, hint }
|
|
70
|
+
related: spec.related ?? null,
|
|
71
|
+
mode: spec.mode ?? '',
|
|
72
|
+
env: spec.env ?? (() => ({})),
|
|
70
73
|
});
|
|
71
74
|
|
|
72
75
|
const AGENTS = [
|
|
73
76
|
defineAgent({
|
|
74
77
|
id: 'claude',
|
|
75
78
|
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']],
|
|
79
|
+
commands: [['claude-agent-acp'], ['claude-code-acp']],
|
|
80
|
+
package: '@agentclientprotocol/claude-agent-acp',
|
|
81
|
+
// Edits are accepted by the agent, the rest is asked of the run
|
|
82
|
+
mode: 'acceptEdits',
|
|
87
83
|
}),
|
|
88
84
|
defineAgent({
|
|
89
85
|
id: 'opencode',
|
|
90
86
|
title: 'OpenCode',
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
resumeWith: (id) => ['--session', id],
|
|
94
|
-
listCmds: [['models']],
|
|
87
|
+
commands: [['opencode', 'acp']],
|
|
88
|
+
package: 'opencode-ai',
|
|
95
89
|
}),
|
|
96
90
|
defineAgent({
|
|
97
91
|
id: 'cursor',
|
|
98
92
|
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,
|
|
93
|
+
commands: [
|
|
94
|
+
['cursor-agent', 'acp'],
|
|
95
|
+
['agent', 'acp'],
|
|
96
|
+
],
|
|
97
|
+
hint: 'needs the Cursor CLI from cursor.com/cli',
|
|
112
98
|
}),
|
|
113
99
|
defineAgent({
|
|
114
100
|
id: 'codex',
|
|
115
101
|
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,
|
|
102
|
+
commands: [['codex-acp']],
|
|
103
|
+
package: '@agentclientprotocol/codex-acp',
|
|
104
|
+
mode: 'workspace-write',
|
|
105
|
+
env: codexEnv,
|
|
131
106
|
}),
|
|
132
107
|
defineAgent({
|
|
133
108
|
id: 'agy',
|
|
134
109
|
title: 'Antigravity',
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
'
|
|
141
|
-
'
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
110
|
+
commands: [[AGY_SERVER, ...AGY_ARGS], ['agy-acp']],
|
|
111
|
+
registry: 'antigravity-acp',
|
|
112
|
+
installLabel: 'Google Antigravity ACP',
|
|
113
|
+
hint: 'install Google Antigravity ACP',
|
|
114
|
+
related: {
|
|
115
|
+
bin: 'agy',
|
|
116
|
+
hint: 'agy installed; install Google Antigravity ACP',
|
|
117
|
+
},
|
|
118
|
+
env: antigravityEnv,
|
|
119
|
+
}),
|
|
120
|
+
defineAgent({
|
|
121
|
+
id: 'junie',
|
|
122
|
+
title: 'Junie',
|
|
123
|
+
commands: [['junie', '--acp=true']],
|
|
124
|
+
package: '@jetbrains/junie',
|
|
125
|
+
}),
|
|
126
|
+
defineAgent({
|
|
127
|
+
id: 'copilot',
|
|
128
|
+
title: 'Copilot',
|
|
129
|
+
commands: [['copilot', '--acp']],
|
|
130
|
+
package: '@github/copilot',
|
|
131
|
+
}),
|
|
132
|
+
defineAgent({
|
|
133
|
+
id: 'pi',
|
|
134
|
+
title: 'Pi',
|
|
135
|
+
commands: [['pi-acp']],
|
|
136
|
+
package: 'pi-acp',
|
|
137
|
+
requires: { bin: 'pi', package: '@earendil-works/pi-coding-agent' },
|
|
138
|
+
}),
|
|
139
|
+
defineAgent({
|
|
140
|
+
id: 'kiro',
|
|
141
|
+
title: 'Kiro',
|
|
142
|
+
commands: [['kiro-cli', 'acp', '--agent-engine=v3']],
|
|
143
|
+
script: {
|
|
144
|
+
unix: 'https://cli.kiro.dev/install',
|
|
145
|
+
win: 'https://cli.kiro.dev/install.ps1',
|
|
146
|
+
},
|
|
147
|
+
installLabel: 'Kiro CLI from cli.kiro.dev',
|
|
148
|
+
hint: 'install the Kiro CLI from cli.kiro.dev',
|
|
149
|
+
}),
|
|
150
|
+
defineAgent({
|
|
151
|
+
id: 'cline',
|
|
152
|
+
title: 'Cline',
|
|
153
|
+
commands: [['cline', '--acp']],
|
|
154
|
+
package: 'cline',
|
|
155
|
+
}),
|
|
156
|
+
// Windsurf's agent is now the Devin CLI of Cognition; the editor has no ACP
|
|
157
|
+
defineAgent({
|
|
158
|
+
id: 'devin',
|
|
159
|
+
title: 'Devin for Windsurf',
|
|
160
|
+
commands: [['devin', 'acp']],
|
|
161
|
+
script: {
|
|
162
|
+
unix: 'https://cli.devin.ai/install.sh',
|
|
163
|
+
win: 'https://static.devin.ai/cli/setup.ps1',
|
|
164
|
+
},
|
|
165
|
+
installLabel: 'Devin CLI from cli.devin.ai',
|
|
166
|
+
hint: 'install the Devin CLI, the agent of Windsurf',
|
|
167
|
+
related: {
|
|
168
|
+
bin: 'windsurf',
|
|
169
|
+
hint: 'windsurf installed; install the Devin CLI',
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
defineAgent({
|
|
173
|
+
id: 'kilo',
|
|
174
|
+
title: 'Kilo Code',
|
|
175
|
+
commands: [
|
|
176
|
+
['kilo', 'acp'],
|
|
177
|
+
['kilocode', 'acp'],
|
|
152
178
|
],
|
|
153
|
-
|
|
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']],
|
|
179
|
+
package: '@kilocode/cli',
|
|
163
180
|
}),
|
|
164
181
|
];
|
|
165
182
|
|
|
@@ -194,8 +211,8 @@ const isBin = (file) => {
|
|
|
194
211
|
}
|
|
195
212
|
};
|
|
196
213
|
|
|
197
|
-
const
|
|
198
|
-
for (const dir of
|
|
214
|
+
const findIn = (dirs, name, env) => {
|
|
215
|
+
for (const dir of dirs) {
|
|
199
216
|
for (const file of binNames(name, env)) {
|
|
200
217
|
const full = path.join(dir, file);
|
|
201
218
|
if (isBin(full)) return full;
|
|
@@ -204,352 +221,178 @@ const findBin = (name, env = process.env) => {
|
|
|
204
221
|
return '';
|
|
205
222
|
};
|
|
206
223
|
|
|
207
|
-
const
|
|
208
|
-
for (const name of spec.bins) {
|
|
209
|
-
const found = findBin(name, env);
|
|
210
|
-
if (found) return found;
|
|
211
|
-
}
|
|
212
|
-
return '';
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
const detectAgents = (env = process.env) =>
|
|
216
|
-
AGENTS.map((spec) => ({
|
|
217
|
-
id: spec.id,
|
|
218
|
-
name: spec.name,
|
|
219
|
-
spec,
|
|
220
|
-
models: spec.models.slice(),
|
|
221
|
-
wide: null,
|
|
222
|
-
listedEfforts: null,
|
|
223
|
-
modelEfforts: null,
|
|
224
|
-
bin: findAgentBin(spec, env),
|
|
225
|
-
}));
|
|
226
|
-
|
|
227
|
-
const canLogin = (row) =>
|
|
228
|
-
Boolean(row && row.bin && row.spec && row.spec.login.length);
|
|
224
|
+
const findBin = (name, env = process.env) => findIn(pathDirs(env), name, env);
|
|
229
225
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
226
|
+
// An install script may put its command in a directory missing from PATH
|
|
227
|
+
const commandDirs = (spec, env) => {
|
|
228
|
+
const dirs = pathDirs(env);
|
|
229
|
+
const local = spec.script ? localBin(env) : '';
|
|
230
|
+
return local && !dirs.includes(local) ? [...dirs, local] : dirs;
|
|
236
231
|
};
|
|
237
232
|
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
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();
|
|
233
|
+
const findCommand = (spec, env) => {
|
|
234
|
+
const dirs = commandDirs(spec, env);
|
|
235
|
+
for (const [name, ...args] of spec.commands) {
|
|
236
|
+
const bin = findIn(dirs, name, env);
|
|
237
|
+
if (bin) return { bin, args };
|
|
260
238
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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;
|
|
239
|
+
if (spec.registry) {
|
|
240
|
+
const installed = installedRegistryAgent(spec.registry, { env });
|
|
241
|
+
if (installed) return installed;
|
|
271
242
|
}
|
|
243
|
+
return { bin: '', args: [] };
|
|
244
|
+
};
|
|
272
245
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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;
|
|
246
|
+
// npm packages still missing: the adapter and the cli of the agent it starts
|
|
247
|
+
const missingPackages = (spec, env, found) => {
|
|
248
|
+
const names = [];
|
|
249
|
+
if (!found.bin && spec.package) names.push(spec.package);
|
|
250
|
+
const cli = spec.requires;
|
|
251
|
+
if (cli && !findBin(cli.bin, env)) names.push(cli.package);
|
|
252
|
+
return names;
|
|
298
253
|
};
|
|
299
254
|
|
|
300
|
-
|
|
255
|
+
// What a missing agent takes: npm packages, a registry binary, or a script
|
|
256
|
+
const installHint = (spec, env, packages) => {
|
|
257
|
+
if (packages.length) return `npm i -g ${packages.join(' ')}`;
|
|
258
|
+
if (spec.package) return `npm i -g ${spec.package}`;
|
|
259
|
+
const { related } = spec;
|
|
260
|
+
if (related && findBin(related.bin, env)) return related.hint;
|
|
261
|
+
return spec.hint;
|
|
262
|
+
};
|
|
301
263
|
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return
|
|
264
|
+
const detectAgent = (spec, env) => {
|
|
265
|
+
const found = findCommand(spec, env);
|
|
266
|
+
const packages = missingPackages(spec, env, found);
|
|
267
|
+
const command = packages.length ? { bin: '', args: [] } : found;
|
|
268
|
+
return {
|
|
269
|
+
id: spec.id,
|
|
270
|
+
name: spec.name,
|
|
271
|
+
spec,
|
|
272
|
+
package: packages.join(' ') || spec.package,
|
|
273
|
+
packages,
|
|
274
|
+
installable: Boolean(spec.package || spec.registry || spec.script),
|
|
275
|
+
installLabel: spec.installLabel,
|
|
276
|
+
hint: installHint(spec, env, packages),
|
|
277
|
+
catalog: null,
|
|
278
|
+
...command,
|
|
279
|
+
};
|
|
307
280
|
};
|
|
308
281
|
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const bin = path.basename(launch.cmd);
|
|
316
|
-
return [bin, ...shown.map(quoteArg), plan].filter(Boolean).join(' ');
|
|
282
|
+
const detectAgents = (env = process.env) =>
|
|
283
|
+
AGENTS.map((spec) => detectAgent(spec, env));
|
|
284
|
+
|
|
285
|
+
const listModels = (row, options = {}) => {
|
|
286
|
+
if (!row || !row.bin) return Promise.resolve(null);
|
|
287
|
+
return probeSession(row, options);
|
|
317
288
|
};
|
|
318
289
|
|
|
319
290
|
const planRef = (reviewPath, cwd) => {
|
|
320
291
|
const file = `${reviewPath ?? ''}`;
|
|
321
292
|
if (!file || !cwd) return file;
|
|
322
|
-
const rel = path.relative(cwd, file);
|
|
293
|
+
const rel = path.relative(cwd, path.resolve(cwd, file));
|
|
323
294
|
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return file;
|
|
324
295
|
return rel.split(path.sep).join('/');
|
|
325
296
|
};
|
|
326
297
|
|
|
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
|
-
};
|
|
298
|
+
// A refusal reads to an agent as a person saying no, and it stops to ask
|
|
299
|
+
const UNATTENDED = [
|
|
300
|
+
'You are running unattended: nobody can answer questions or approve',
|
|
301
|
+
'commands. Change files with your edit tools. The shell is limited to',
|
|
302
|
+
`${COMMANDS.join(', ')} and read-only filters such as cat, grep,`,
|
|
303
|
+
'head and tail. When a command is refused, do the step another way',
|
|
304
|
+
'and continue with the plan.',
|
|
305
|
+
].join(' ');
|
|
355
306
|
|
|
356
|
-
const
|
|
357
|
-
|
|
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
|
-
};
|
|
450
|
-
|
|
451
|
-
const effortParam = (model, effort) => {
|
|
452
|
-
const level = trimText(effort);
|
|
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
|
-
};
|
|
307
|
+
const planPrompt = (reviewPath) =>
|
|
308
|
+
`Read and execute repair plan at ${reviewPath}\n\n${UNATTENDED}`;
|
|
463
309
|
|
|
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);
|
|
310
|
+
// What a run is shown as: the settings it was started with and its plan
|
|
311
|
+
const launchLabel = (launch) => {
|
|
312
|
+
const { choice } = launch;
|
|
313
|
+
const fast = choice.fast ? 'fast' : '';
|
|
314
|
+
const parts = [launch.name, choice.model, choice.effort, fast];
|
|
315
|
+
return [...parts, choice.context, launch.plan].filter(Boolean).join(' ');
|
|
474
316
|
};
|
|
475
317
|
|
|
476
|
-
const
|
|
318
|
+
const buildLaunch = (row, picked, reviewPath, cwd, sessionId = '') => {
|
|
319
|
+
if (!row || !row.bin) return { ok: false, error: 'not installed' };
|
|
320
|
+
if (!reviewPath) return { ok: false, error: 'no plan' };
|
|
321
|
+
const plan = planRef(reviewPath, cwd) || reviewPath;
|
|
477
322
|
const launch = {
|
|
478
323
|
ok: true,
|
|
479
|
-
cmd: row.bin,
|
|
480
|
-
args,
|
|
481
|
-
prompt,
|
|
482
|
-
plan,
|
|
483
324
|
id: row.id,
|
|
484
325
|
name: row.name,
|
|
485
|
-
kind,
|
|
326
|
+
kind: 'run',
|
|
327
|
+
cmd: row.bin,
|
|
328
|
+
args: (row.args ?? []).slice(),
|
|
329
|
+
mode: row.spec.mode,
|
|
330
|
+
env: row.spec.env(cwd, row),
|
|
331
|
+
choice: choiceOf(picked),
|
|
332
|
+
plan,
|
|
333
|
+
prompt: planPrompt(plan),
|
|
334
|
+
session: trimText(sessionId),
|
|
486
335
|
command: '',
|
|
487
336
|
};
|
|
488
|
-
launch.command =
|
|
337
|
+
launch.command = launchLabel(launch);
|
|
489
338
|
return launch;
|
|
490
339
|
};
|
|
491
340
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
341
|
+
// A helper of reslop downloads a registry binary or runs a vendor script
|
|
342
|
+
const helperInstall = (row, installer, file, arg) => ({
|
|
343
|
+
installer,
|
|
344
|
+
cmd: process.execPath,
|
|
345
|
+
args: [path.join(__dirname, file), arg],
|
|
346
|
+
command: `install ${row.spec.installLabel || row.name}`,
|
|
347
|
+
});
|
|
497
348
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
if (!reviewPath) return { ok: false, error: 'no plan' };
|
|
349
|
+
// The package lands in the global prefix of npm, where PATH finds its command
|
|
350
|
+
const installCommand = (row) => {
|
|
501
351
|
const { spec } = row;
|
|
502
|
-
|
|
503
|
-
|
|
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));
|
|
352
|
+
if (spec.registry) {
|
|
353
|
+
return helperInstall(row, 'registry', 'registry.js', spec.registry);
|
|
516
354
|
}
|
|
517
|
-
if (spec.
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
};
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
if (!
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
355
|
+
if (spec.script) {
|
|
356
|
+
const url = scriptUrl(spec.script);
|
|
357
|
+
return helperInstall(row, 'script', 'script.js', url);
|
|
358
|
+
}
|
|
359
|
+
const names = row.packages ?? [spec.package].filter(Boolean);
|
|
360
|
+
if (!names.length) return null;
|
|
361
|
+
const args = ['install', '--global', ...names];
|
|
362
|
+
const command = `npm ${args.join(' ')}`;
|
|
363
|
+
return { installer: 'npm', cmd: npmBin(), args, command };
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const buildInstall = (row) => {
|
|
367
|
+
if (!row) return { ok: false, error: 'no agent' };
|
|
368
|
+
if (row.bin) return { ok: false, error: 'installed' };
|
|
369
|
+
const install = installCommand(row);
|
|
370
|
+
if (!install) return { ok: false, error: row.hint || 'no installer' };
|
|
371
|
+
return {
|
|
372
|
+
ok: true,
|
|
373
|
+
id: row.id,
|
|
374
|
+
name: row.name,
|
|
375
|
+
kind: 'install',
|
|
376
|
+
...install,
|
|
377
|
+
plan: '',
|
|
378
|
+
session: '',
|
|
379
|
+
};
|
|
538
380
|
};
|
|
539
381
|
|
|
540
|
-
const
|
|
541
|
-
const
|
|
382
|
+
const startInstall = (cwd, launch, onData, onClose) => {
|
|
383
|
+
const extra = { cwd, env: commandEnv(), detached: !IS_WIN };
|
|
384
|
+
const options =
|
|
385
|
+
launch.installer === 'npm' ? npmOpts(extra) : spawnBase(extra);
|
|
542
386
|
const proc = new PipeProcess(onData, onClose);
|
|
543
387
|
proc.start(launch.cmd, launch.args, options).closeInput();
|
|
544
388
|
return proc;
|
|
545
389
|
};
|
|
546
390
|
|
|
547
|
-
// An agent that reads an allow list gets the plan entries before it runs
|
|
548
391
|
const startAgent = (cwd, launch, onData, onClose) => {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
return
|
|
392
|
+
if (launch.kind === 'install') {
|
|
393
|
+
return startInstall(cwd, launch, onData, onClose);
|
|
394
|
+
}
|
|
395
|
+
return startRun(cwd, launch, onData, onClose, launch.env);
|
|
553
396
|
};
|
|
554
397
|
|
|
555
398
|
module.exports = {
|
|
@@ -558,16 +401,9 @@ module.exports = {
|
|
|
558
401
|
agentSpec,
|
|
559
402
|
findBin,
|
|
560
403
|
detectAgents,
|
|
561
|
-
canLogin,
|
|
562
|
-
listedOf,
|
|
563
404
|
listModels,
|
|
564
|
-
splitArgs,
|
|
565
405
|
planPrompt,
|
|
566
|
-
needsAuth,
|
|
567
|
-
authState,
|
|
568
|
-
probeAuth,
|
|
569
|
-
commandLine,
|
|
570
406
|
buildLaunch,
|
|
571
|
-
|
|
407
|
+
buildInstall,
|
|
572
408
|
startAgent,
|
|
573
409
|
};
|