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.
Files changed (74) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/README.md +21 -3
  3. package/lib/agents/acp.js +309 -0
  4. package/lib/agents/agents.js +166 -370
  5. package/lib/agents/allow.js +141 -52
  6. package/lib/agents/allow.json +16 -13
  7. package/lib/agents/models.js +250 -376
  8. package/lib/agents/registry.js +437 -0
  9. package/lib/agents/run.js +234 -0
  10. package/lib/agents/transcript.js +143 -0
  11. package/lib/cli.js +2 -1
  12. package/lib/common/process.js +26 -1
  13. package/lib/dashboard/git.js +11 -1
  14. package/lib/dashboard/reports.js +80 -0
  15. package/lib/dashboard/watch.js +43 -8
  16. package/lib/dashboard/workspace.js +19 -20
  17. package/lib/dashboard/worktrees.js +145 -0
  18. package/lib/git/deps.js +29 -0
  19. package/lib/git/dir.js +50 -0
  20. package/lib/git/git.js +1 -1
  21. package/lib/git/linked.js +152 -0
  22. package/lib/git/local.js +179 -0
  23. package/lib/git/status.js +126 -0
  24. package/lib/git/worktree.js +137 -31
  25. package/lib/input/actions.js +83 -2
  26. package/lib/input/editor.js +9 -0
  27. package/lib/input/keys.js +39 -3
  28. package/lib/input/line-edit.js +8 -3
  29. package/lib/render/agents.js +64 -36
  30. package/lib/render/branches.js +55 -27
  31. package/lib/render/field.js +19 -5
  32. package/lib/render/footer.js +14 -4
  33. package/lib/render/frame.js +16 -2
  34. package/lib/render/log.js +5 -4
  35. package/lib/render/menu.js +103 -6
  36. package/lib/render/name-edit.js +31 -0
  37. package/lib/render/repos.js +52 -8
  38. package/lib/render/status.js +22 -4
  39. package/lib/report/render.js +4 -7
  40. package/lib/report/run.js +16 -23
  41. package/lib/review/agent-instructions.md +13 -8
  42. package/lib/review/format.js +0 -6
  43. package/lib/runs/process.js +3 -3
  44. package/lib/runs/runs.js +27 -4
  45. package/lib/session/agents/agents.js +227 -194
  46. package/lib/session/agents/away.js +48 -0
  47. package/lib/session/agents/job.js +23 -86
  48. package/lib/session/agents/jobs.js +14 -27
  49. package/lib/session/agents/options.js +45 -101
  50. package/lib/session/agents/plans.js +53 -0
  51. package/lib/session/agents/view.js +34 -0
  52. package/lib/session/branches.js +18 -2
  53. package/lib/session/compose.js +20 -6
  54. package/lib/session/dispatch.js +43 -3
  55. package/lib/session/editor-pointer.js +98 -0
  56. package/lib/session/files-pane.js +3 -0
  57. package/lib/session/import.js +12 -1
  58. package/lib/session/lifecycle.js +70 -10
  59. package/lib/session/navigation.js +5 -0
  60. package/lib/session/npm/history.js +3 -1
  61. package/lib/session/npm/npm.js +111 -13
  62. package/lib/session/ops.js +3 -0
  63. package/lib/session/pointer.js +304 -46
  64. package/lib/session/session.js +17 -2
  65. package/lib/session/tasks.js +1 -1
  66. package/lib/session/terminal.js +2 -2
  67. package/lib/session/text-menu.js +50 -0
  68. package/lib/session/view.js +19 -2
  69. package/lib/session/workspace.js +179 -38
  70. package/lib/session/worktrees.js +280 -0
  71. package/lib/term/select.js +2 -1
  72. package/lib/term/themes/dark.json +4 -0
  73. package/lib/term/themes/light.json +4 -0
  74. package/package.json +2 -2
@@ -2,163 +2,109 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
- const { jsonParse } = require('metautil');
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 { ensurePlanAllows, PLAN_ALLOWS } = require('./allow.js');
7
+ const { trimText } = require('../common/utilities.js');
8
+ const { COMMANDS } = require('./allow.js');
10
9
  const models = require('./models.js');
11
- const { parseNameList, parseCursorModels, parseCursorWide } = models;
12
- const { parseJsonModels, catalogOf, emptyChoice, contextMenu } = models;
13
- const { parseJsonEfforts, parseHelpModels, optionValues } = models;
14
-
15
- const LIST_MS = 5000;
16
- const AUTH_NEED = /authentication required/i;
17
- const PERMISSION_FAIL =
18
- /permissions?[ -]?(?:denied|denial)|denied permissions?/i;
19
-
20
- const plainEffort = (level) => level;
21
- const codexEffort = (level) => `model_reasoning_effort=${level}`;
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
- const bashAllow = (entry) => {
24
- if (!entry.startsWith('Shell(') || !entry.endsWith(')')) return '';
25
- const bin = entry.slice('Shell('.length, -1).split(/\s+/)[0];
26
- if (!bin) return '';
27
- return `Bash(${bin} *)`;
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
- const BASH_ALLOWS = [...new Set(PLAN_ALLOWS.map(bashAllow).filter(Boolean))];
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
- // A piped session has no approval host. Preapproved shells can still run tests.
33
- const CLAUDE_PREFIX = [
34
- '--print',
35
- '--permission-mode',
36
- 'acceptEdits',
37
- '--permission-prompts',
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
- bins: spec.bins,
48
- models: spec.models,
49
- modelFlag: spec.modelFlag ?? '--model',
50
- effortFlag: spec.effortFlag ?? '',
51
- effortValue: spec.effortValue ?? plainEffort,
52
- efforts: spec.efforts ?? [],
53
- modelParams: spec.modelParams === true,
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
- bins: ['claude'],
76
- models: ['default', 'sonnet', 'opus', 'haiku', 'fable'],
77
- effortFlag: '--effort',
78
- efforts: ['default', 'low', 'medium', 'high', 'max'],
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
- bins: ['opencode'],
91
- models: ['default', 'sonnet', 'opus', 'gpt-5'],
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
- bins: ['cursor-agent', 'agent'],
99
- models: ['default', 'auto', 'grok-4.6', 'composer-2', 'gpt-5', 'sonnet'],
100
- modelParams: true,
101
- efforts: ['default', 'low', 'medium', 'high', 'xhigh'],
102
- prefix: ['--print', '--trust'],
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
- bins: ['codex'],
116
- models: ['default', 'gpt-5.1-codex', 'gpt-5', 'o3'],
117
- effortFlag: '-c',
118
- effortValue: codexEffort,
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
- bins: ['agy'],
135
- models: [
136
- 'default',
137
- 'gemini-3.8-flash-high',
138
- 'gemini-3.8-flash-medium',
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 findAgentBin = (spec, env) => {
207
- for (const name of spec.bins) {
208
- const found = findBin(name, env);
209
- if (found) return found;
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
- return '';
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
- models: spec.models.slice(),
220
- wide: null,
221
- listedEfforts: null,
222
- modelEfforts: null,
223
- bin: findAgentBin(spec, env),
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 canLogin = (row) =>
227
- Boolean(row && row.bin && row.spec && row.spec.login.length);
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
- const planPrompt = (reviewPath) =>
327
- `Read and execute repair plan at ${reviewPath}`;
328
-
329
- const needsAuth = (text) => AUTH_NEED.test(`${text ?? ''}`);
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 parsedAuth = (raw) => {
332
- const start = raw.indexOf('{');
333
- const end = raw.lastIndexOf('}');
334
- if (start < 0 || end <= start) return null;
335
- const body = raw.slice(start, end + 1);
336
- const data = jsonParse(body);
337
- if (!data || typeof data !== 'object') return null;
338
- if (typeof data.isAuthenticated === 'boolean') return data.isAuthenticated;
339
- if (data.status === 'authenticated') return true;
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 createLaunch = (row, kind, args, plan = '', prompt = '') => {
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 = commandLine(launch);
238
+ launch.command = launchLabel(launch);
421
239
  return launch;
422
240
  };
423
241
 
424
- const resumeArgs = (spec, sessionId) => {
425
- const id = `${sessionId ?? ''}`;
426
- if (!id || !spec.resumeWith) return [];
427
- return spec.resumeWith(id);
428
- };
429
-
430
- const buildLaunch = (row, choice, reviewPath, cwd, sessionId = '') => {
431
- if (!row || !row.bin) return { ok: false, error: 'not installed' };
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
- args.push(...splitArgs(picked.extra));
450
- if (spec.promptFlag) args.push(spec.promptFlag);
451
- args.push(prompt);
452
- const launch = createLaunch(row, 'run', args, plan, prompt);
453
- const id = `${sessionId ?? ''}`;
454
- if (id) launch.session = id;
455
- return launch;
456
- };
457
-
458
- const buildLogin = (row) => {
459
- if (!row || !row.bin) return { ok: false, error: 'not installed' };
460
- if (!canLogin(row)) return { ok: false, error: 'no login' };
461
- return createLaunch(row, 'login', row.spec.login.slice());
462
- };
463
-
464
- // The review screen forces raw output. An agent needs the reduced test report.
465
- const agentEnv = () => {
466
- const env = commandEnv();
467
- delete env.RESLOP_OUTPUT;
468
- 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
+ };
469
271
  };
470
272
 
471
- const startPipe = (cwd, launch, onData, onClose) => {
472
- const options = spawnBase({ cwd, env: agentEnv(), detached: !IS_WIN });
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
- const spec = agentSpec(launch.id);
481
- const listed = launch.kind === 'run' && spec && spec.allowlist;
482
- if (listed) ensurePlanAllows();
483
- return startPipe(cwd, launch, onData, onClose);
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
- buildLogin,
298
+ buildInstall,
503
299
  startAgent,
504
300
  };