reslop 0.3.0 → 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 +29 -1
- package/README.md +21 -3
- package/lib/agents/acp.js +309 -0
- package/lib/agents/agents.js +166 -370
- package/lib/agents/allow.js +141 -52
- package/lib/agents/allow.json +16 -13
- package/lib/agents/models.js +250 -376
- 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/deps.js +29 -0
- package/lib/git/dir.js +50 -0
- package/lib/git/git.js +1 -1
- 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 +137 -31
- package/lib/input/actions.js +83 -2
- package/lib/input/editor.js +9 -0
- package/lib/input/keys.js +39 -3
- package/lib/input/line-edit.js +8 -3
- package/lib/render/agents.js +64 -36
- package/lib/render/branches.js +55 -27
- package/lib/render/field.js +19 -5
- package/lib/render/footer.js +14 -4
- package/lib/render/frame.js +16 -2
- package/lib/render/log.js +5 -4
- 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/render.js +4 -7
- package/lib/report/run.js +16 -23
- package/lib/review/agent-instructions.md +13 -8
- package/lib/review/format.js +0 -6
- package/lib/runs/process.js +3 -3
- package/lib/runs/runs.js +27 -4
- package/lib/session/agents/agents.js +227 -194
- 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 -2
- package/lib/session/compose.js +20 -6
- package/lib/session/dispatch.js +43 -3
- package/lib/session/editor-pointer.js +98 -0
- package/lib/session/files-pane.js +3 -0
- package/lib/session/import.js +12 -1
- package/lib/session/lifecycle.js +70 -10
- package/lib/session/navigation.js +5 -0
- package/lib/session/npm/history.js +3 -1
- package/lib/session/npm/npm.js +111 -13
- package/lib/session/ops.js +3 -0
- package/lib/session/pointer.js +304 -46
- package/lib/session/session.js +17 -2
- package/lib/session/tasks.js +1 -1
- 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/select.js +2 -1
- 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,163 +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
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
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');
|
|
15
|
+
|
|
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
|
+
|
|
21
|
+
const realPath = (file) => {
|
|
22
|
+
try {
|
|
23
|
+
return fs.realpathSync.native(file);
|
|
24
|
+
} catch {
|
|
25
|
+
return path.resolve(file);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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)];
|
|
28
36
|
};
|
|
29
37
|
|
|
30
|
-
|
|
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
|
+
};
|
|
31
45
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
'
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
'none',
|
|
39
|
-
'--allowedTools',
|
|
40
|
-
...BASH_ALLOWS,
|
|
41
|
-
];
|
|
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
|
+
};
|
|
42
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
|
|
43
55
|
const defineAgent = (spec) => ({
|
|
44
56
|
id: spec.id,
|
|
45
57
|
name: spec.name ?? spec.id,
|
|
46
58
|
title: spec.title,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
prefix: spec.prefix ?? [],
|
|
55
|
-
promptFlag: spec.promptFlag ?? '',
|
|
56
|
-
login: spec.login ?? [],
|
|
57
|
-
status: spec.status ?? [],
|
|
58
|
-
contexts: spec.contexts ?? [],
|
|
59
|
-
resumeWith: spec.resumeWith ?? null,
|
|
60
|
-
listCmds: spec.listCmds,
|
|
61
|
-
parseModels: spec.parseModels ?? parseNameList,
|
|
62
|
-
wideModels: spec.wideModels ?? null,
|
|
63
|
-
partialList: spec.partialList === true,
|
|
64
|
-
modelEfforts: spec.modelEfforts ?? null,
|
|
65
|
-
effortCmds: spec.effortCmds ?? [],
|
|
66
|
-
allowlist: spec.allowlist === true,
|
|
67
|
-
denied: spec.denied ?? null,
|
|
68
|
-
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 ?? (() => ({})),
|
|
69
66
|
});
|
|
70
67
|
|
|
71
68
|
const AGENTS = [
|
|
72
69
|
defineAgent({
|
|
73
70
|
id: 'claude',
|
|
74
71
|
title: 'Claude Code',
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
prefix: CLAUDE_PREFIX,
|
|
80
|
-
resumeWith: (id) => ['--resume', id],
|
|
81
|
-
// No command lists models: the help names the aliases and the efforts
|
|
82
|
-
listCmds: [['--help']],
|
|
83
|
-
parseModels: parseHelpModels,
|
|
84
|
-
partialList: true,
|
|
85
|
-
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',
|
|
86
76
|
}),
|
|
87
77
|
defineAgent({
|
|
88
78
|
id: 'opencode',
|
|
89
79
|
title: 'OpenCode',
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
resumeWith: (id) => ['--session', id],
|
|
93
|
-
listCmds: [['models']],
|
|
80
|
+
commands: [['opencode', 'acp']],
|
|
81
|
+
package: 'opencode-ai',
|
|
94
82
|
}),
|
|
95
83
|
defineAgent({
|
|
96
84
|
id: 'cursor',
|
|
97
85
|
title: 'Cursor',
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
allowlist: true,
|
|
104
|
-
login: ['login'],
|
|
105
|
-
status: ['status', '--format', 'json'],
|
|
106
|
-
contexts: ['256k', '1m'],
|
|
107
|
-
resumeWith: (id) => [`--resume=${id}`],
|
|
108
|
-
listCmds: [['--list-models'], ['models']],
|
|
109
|
-
parseModels: parseCursorModels,
|
|
110
|
-
wideModels: parseCursorWide,
|
|
86
|
+
commands: [
|
|
87
|
+
['cursor-agent', 'acp'],
|
|
88
|
+
['agent', 'acp'],
|
|
89
|
+
],
|
|
90
|
+
hint: 'needs the Cursor CLI from cursor.com/cli',
|
|
111
91
|
}),
|
|
112
92
|
defineAgent({
|
|
113
93
|
id: 'codex',
|
|
114
94
|
title: 'Codex',
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
efforts: ['default', 'low', 'medium', 'high', 'xhigh'],
|
|
120
|
-
// The TUI rejects a closed stdin; exec is the non-interactive entry
|
|
121
|
-
prefix: ['exec'],
|
|
122
|
-
resumeWith: (id) => ['resume', id],
|
|
123
|
-
listCmds: [
|
|
124
|
-
['debug', 'models', '--bundled'],
|
|
125
|
-
['debug', 'models'],
|
|
126
|
-
['models'],
|
|
127
|
-
],
|
|
128
|
-
parseModels: parseJsonModels,
|
|
129
|
-
modelEfforts: parseJsonEfforts,
|
|
95
|
+
commands: [['codex-acp']],
|
|
96
|
+
package: '@agentclientprotocol/codex-acp',
|
|
97
|
+
mode: 'workspace-write',
|
|
98
|
+
env: codexEnv,
|
|
130
99
|
}),
|
|
131
100
|
defineAgent({
|
|
132
101
|
id: 'agy',
|
|
133
102
|
title: 'Antigravity',
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
'gemini-3.8-flash-low',
|
|
140
|
-
'gemini-3.7-flash-high',
|
|
141
|
-
'gemini-3.7-flash-medium',
|
|
142
|
-
'gemini-3.7-flash-low',
|
|
143
|
-
'gemini-3.6-flash-high',
|
|
144
|
-
'gemini-3.6-flash-medium',
|
|
145
|
-
'gemini-3.6-flash-low',
|
|
146
|
-
'gemini-3.1-pro-high',
|
|
147
|
-
'gemini-3.1-pro-low',
|
|
148
|
-
'claude-sonnet-4-6',
|
|
149
|
-
'claude-opus-4-6-thinking',
|
|
150
|
-
'gpt-oss-120b-medium',
|
|
151
|
-
],
|
|
152
|
-
effortFlag: '--effort',
|
|
153
|
-
efforts: ['default', 'low', 'medium', 'high'],
|
|
154
|
-
promptFlag: '-p',
|
|
155
|
-
// Exits 0 after a refused tool call, so the output tells the outcome
|
|
156
|
-
denied: PERMISSION_FAIL,
|
|
157
|
-
// Extra flags in .reslop are the opt-in to run without confirmations
|
|
158
|
-
savesExtra: true,
|
|
159
|
-
resumeWith: (id) => ['--conversation', id],
|
|
160
|
-
listCmds: [['models']],
|
|
161
|
-
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,
|
|
162
108
|
}),
|
|
163
109
|
];
|
|
164
110
|
|
|
@@ -203,12 +149,25 @@ const findBin = (name, env = process.env) => {
|
|
|
203
149
|
return '';
|
|
204
150
|
};
|
|
205
151
|
|
|
206
|
-
const
|
|
207
|
-
for (const name of spec.
|
|
208
|
-
const
|
|
209
|
-
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 };
|
|
210
156
|
}
|
|
211
|
-
|
|
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;
|
|
212
171
|
};
|
|
213
172
|
|
|
214
173
|
const detectAgents = (env = process.env) =>
|
|
@@ -216,271 +175,115 @@ const detectAgents = (env = process.env) =>
|
|
|
216
175
|
id: spec.id,
|
|
217
176
|
name: spec.name,
|
|
218
177
|
spec,
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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),
|
|
224
184
|
}));
|
|
225
185
|
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const tryList = async (run, bin, args, options) => {
|
|
230
|
-
try {
|
|
231
|
-
return await run(bin, args, options);
|
|
232
|
-
} catch {
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
const listedOf = (listed) => {
|
|
238
|
-
const bare = Array.isArray(listed);
|
|
239
|
-
const given = bare ? { names: listed, wide: listed.wide } : listed;
|
|
240
|
-
const from = given && typeof given === 'object' ? given : {};
|
|
241
|
-
const efforts = Array.isArray(from.efforts) ? from.efforts : [];
|
|
242
|
-
const perModel = from.modelEfforts instanceof Map ? from.modelEfforts : null;
|
|
243
|
-
return {
|
|
244
|
-
names: Array.isArray(from.names) ? from.names : [],
|
|
245
|
-
wide: Array.isArray(from.wide) ? from.wide : null,
|
|
246
|
-
efforts: efforts.length ? efforts : null,
|
|
247
|
-
modelEfforts: perModel && perModel.size ? perModel : null,
|
|
248
|
-
};
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
// One answer per command line, so a command that serves two lists runs once
|
|
252
|
-
class CliProbe {
|
|
253
|
-
constructor(bin, options) {
|
|
254
|
-
this.bin = bin;
|
|
255
|
-
this.run = options.run ?? runProc;
|
|
256
|
-
this.env = { ...process.env, ...options.env };
|
|
257
|
-
this.timeout = options.timeout ?? LIST_MS;
|
|
258
|
-
this.texts = new Map();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async read(args) {
|
|
262
|
-
const key = args.join(' ');
|
|
263
|
-
if (this.texts.has(key)) return this.texts.get(key);
|
|
264
|
-
const { env, timeout } = this;
|
|
265
|
-
const result = await tryList(this.run, this.bin, args, { env, timeout });
|
|
266
|
-
const { stdout = '', stderr = '' } = result ?? {};
|
|
267
|
-
const text = result ? `${stdout}\n${stderr}` : '';
|
|
268
|
-
this.texts.set(key, text);
|
|
269
|
-
return text;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
async first(cmds, parse) {
|
|
273
|
-
for (const args of cmds) {
|
|
274
|
-
const text = await this.read(args);
|
|
275
|
-
const found = text ? parse(text) : [];
|
|
276
|
-
if (found.length) return { text, found };
|
|
277
|
-
}
|
|
278
|
-
return null;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const listModels = async (bin, spec, options = {}) => {
|
|
283
|
-
const listed = { names: [], wide: null, efforts: null, modelEfforts: null };
|
|
284
|
-
if (!bin || !spec) return listed;
|
|
285
|
-
const probe = new CliProbe(bin, options);
|
|
286
|
-
const models = await probe.first(spec.listCmds, spec.parseModels);
|
|
287
|
-
if (models) {
|
|
288
|
-
const { text } = models;
|
|
289
|
-
listed.names = models.found;
|
|
290
|
-
listed.wide = spec.wideModels ? spec.wideModels(text) : null;
|
|
291
|
-
listed.modelEfforts = spec.modelEfforts ? spec.modelEfforts(text) : null;
|
|
292
|
-
}
|
|
293
|
-
const levels = (text) => optionValues(text, spec.effortFlag);
|
|
294
|
-
const efforts = await probe.first(spec.effortCmds, levels);
|
|
295
|
-
if (efforts) listed.efforts = efforts.found;
|
|
296
|
-
return listed;
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
const splitArgs = (text) => trimText(text).split(/\s+/).filter(Boolean);
|
|
300
|
-
|
|
301
|
-
const quoteArg = (text) => {
|
|
302
|
-
const arg = `${text ?? ''}`;
|
|
303
|
-
if (!arg) return '""';
|
|
304
|
-
if (!/[\s"'\\]/.test(arg)) return arg;
|
|
305
|
-
return `"${arg.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
const commandLine = (launch) => {
|
|
309
|
-
if (!launch || !launch.ok || !launch.cmd) return '';
|
|
310
|
-
const args = launch.args ?? [];
|
|
311
|
-
const hasPrompt = launch.prompt && args.at(-1) === launch.prompt;
|
|
312
|
-
const shown = hasPrompt ? args.slice(0, -1) : args;
|
|
313
|
-
const plan = launch.plan ? quoteArg(launch.plan) : '';
|
|
314
|
-
const bin = path.basename(launch.cmd);
|
|
315
|
-
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);
|
|
316
189
|
};
|
|
317
190
|
|
|
318
191
|
const planRef = (reviewPath, cwd) => {
|
|
319
192
|
const file = `${reviewPath ?? ''}`;
|
|
320
193
|
if (!file || !cwd) return file;
|
|
321
|
-
const rel = path.relative(cwd, file);
|
|
194
|
+
const rel = path.relative(cwd, path.resolve(cwd, file));
|
|
322
195
|
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return file;
|
|
323
196
|
return rel.split(path.sep).join('/');
|
|
324
197
|
};
|
|
325
198
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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(' ');
|
|
330
207
|
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (data.status === 'unauthenticated') return false;
|
|
341
|
-
if (data.status === 'partially-authenticated') return false;
|
|
342
|
-
return null;
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
const authState = (text) => {
|
|
346
|
-
const raw = trimText(text);
|
|
347
|
-
if (!raw) return null;
|
|
348
|
-
const parsed = parsedAuth(raw);
|
|
349
|
-
if (parsed !== null) return parsed;
|
|
350
|
-
if (/not logged in/i.test(raw)) return false;
|
|
351
|
-
if (/logged in/i.test(raw)) return true;
|
|
352
|
-
return null;
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
const probeAuth = async (row) => {
|
|
356
|
-
const args = row && row.spec ? row.spec.status : [];
|
|
357
|
-
if (!row || !row.bin || !args.length) return null;
|
|
358
|
-
const result = await runProc(row.bin, args, {
|
|
359
|
-
env: commandEnv(),
|
|
360
|
-
timeout: LIST_MS,
|
|
361
|
-
});
|
|
362
|
-
if (!result || result.error) return null;
|
|
363
|
-
const text = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
|
|
364
|
-
return authState(text);
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
const effortArgs = (spec, effort) => {
|
|
368
|
-
const level = trimText(effort);
|
|
369
|
-
if (!spec.effortFlag || !level || level === 'default') return [];
|
|
370
|
-
return [spec.effortFlag, spec.effortValue(level)];
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
const paramPart = (key, value) => {
|
|
374
|
-
const text = trimText(value);
|
|
375
|
-
if (!text) return '';
|
|
376
|
-
return `${key}=${text}`;
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
const withParams = (model, parts) => {
|
|
380
|
-
const body = parts.filter(Boolean).join(',');
|
|
381
|
-
if (!body || !model || model === 'default') return model;
|
|
382
|
-
return `${model}[${body}]`;
|
|
383
|
-
};
|
|
384
|
-
|
|
385
|
-
const effortParam = (model, effort) => {
|
|
386
|
-
const level = trimText(effort);
|
|
387
|
-
if (!level || level === 'default') return '';
|
|
388
|
-
if (`${model ?? ''}`.endsWith(`-${level}`)) return '';
|
|
389
|
-
return level;
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
const fastParam = (model, fast) => {
|
|
393
|
-
if (fast !== true) return '';
|
|
394
|
-
if (`${model ?? ''}`.endsWith('-fast')) return '';
|
|
395
|
-
return 'true';
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
const modelArg = (spec, resolved, context, fast) => {
|
|
399
|
-
const parts = [paramPart('context', context)];
|
|
400
|
-
const plain = spec.modelParams && !resolved.encoded;
|
|
401
|
-
if (!plain) return withParams(resolved.model, parts);
|
|
402
|
-
const level = effortParam(resolved.model, resolved.effort);
|
|
403
|
-
parts.push(paramPart('effort', level));
|
|
404
|
-
parts.push(paramPart('fast', fastParam(resolved.model, fast)));
|
|
405
|
-
return withParams(resolved.model, parts);
|
|
208
|
+
const planPrompt = (reviewPath) =>
|
|
209
|
+
`Read and execute repair plan at ${reviewPath}\n\n${UNATTENDED}`;
|
|
210
|
+
|
|
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(' ');
|
|
406
217
|
};
|
|
407
218
|
|
|
408
|
-
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;
|
|
409
223
|
const launch = {
|
|
410
224
|
ok: true,
|
|
411
|
-
cmd: row.bin,
|
|
412
|
-
args,
|
|
413
|
-
prompt,
|
|
414
|
-
plan,
|
|
415
225
|
id: row.id,
|
|
416
226
|
name: row.name,
|
|
417
|
-
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),
|
|
418
236
|
command: '',
|
|
419
237
|
};
|
|
420
|
-
launch.command =
|
|
238
|
+
launch.command = launchLabel(launch);
|
|
421
239
|
return launch;
|
|
422
240
|
};
|
|
423
241
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
if (!
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (!reviewPath) return { ok: false, error: 'no plan' };
|
|
433
|
-
const { spec } = row;
|
|
434
|
-
const picked = choice ?? emptyChoice(row);
|
|
435
|
-
const catalog = catalogOf(row.models);
|
|
436
|
-
const resolved = catalog.withFast(catalog.resolve(picked), picked.fast);
|
|
437
|
-
const asked = trimText(picked.context);
|
|
438
|
-
const menu = contextMenu(resolved.model, row.wide, catalog, spec.contexts);
|
|
439
|
-
const pass = menu.includes(asked) && asked !== menu[0] ? asked : '';
|
|
440
|
-
const model = modelArg(spec, resolved, pass, picked.fast);
|
|
441
|
-
const plan = planRef(reviewPath, cwd) || reviewPath;
|
|
442
|
-
const prompt = planPrompt(plan);
|
|
443
|
-
const continued = resumeArgs(spec, sessionId);
|
|
444
|
-
const args = [...spec.prefix, ...continued];
|
|
445
|
-
if (model && model !== 'default') args.push(spec.modelFlag, model);
|
|
446
|
-
if (!spec.modelParams && !resolved.encoded) {
|
|
447
|
-
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' };
|
|
448
250
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
+
};
|
|
469
271
|
};
|
|
470
272
|
|
|
471
|
-
const
|
|
472
|
-
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);
|
|
473
277
|
const proc = new PipeProcess(onData, onClose);
|
|
474
278
|
proc.start(launch.cmd, launch.args, options).closeInput();
|
|
475
279
|
return proc;
|
|
476
280
|
};
|
|
477
281
|
|
|
478
|
-
// An agent that reads an allow list gets the plan entries before it runs
|
|
479
282
|
const startAgent = (cwd, launch, onData, onClose) => {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
return
|
|
283
|
+
if (launch.kind === 'install') {
|
|
284
|
+
return startInstall(cwd, launch, onData, onClose);
|
|
285
|
+
}
|
|
286
|
+
return startRun(cwd, launch, onData, onClose, launch.env);
|
|
484
287
|
};
|
|
485
288
|
|
|
486
289
|
module.exports = {
|
|
@@ -489,16 +292,9 @@ module.exports = {
|
|
|
489
292
|
agentSpec,
|
|
490
293
|
findBin,
|
|
491
294
|
detectAgents,
|
|
492
|
-
canLogin,
|
|
493
|
-
listedOf,
|
|
494
295
|
listModels,
|
|
495
|
-
splitArgs,
|
|
496
296
|
planPrompt,
|
|
497
|
-
needsAuth,
|
|
498
|
-
authState,
|
|
499
|
-
probeAuth,
|
|
500
|
-
commandLine,
|
|
501
297
|
buildLaunch,
|
|
502
|
-
|
|
298
|
+
buildInstall,
|
|
503
299
|
startAgent,
|
|
504
300
|
};
|