reslop 0.2.7 → 0.3.1

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 (158) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +573 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +17 -0
  7. package/lib/agents/models.js +467 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +43 -68
  10. package/lib/{files.js → common/files.js} +48 -55
  11. package/lib/{find.js → common/find.js} +1 -1
  12. package/lib/common/format.js +101 -0
  13. package/lib/{utilities.js → common/process.js} +7 -95
  14. package/lib/common/tasks.json +27 -0
  15. package/lib/common/utilities.js +112 -0
  16. package/lib/dashboard/git.js +9 -16
  17. package/lib/dashboard/model.js +90 -49
  18. package/lib/dashboard/npm.js +17 -13
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +36 -31
  21. package/lib/{workspace.js → dashboard/workspace.js} +23 -38
  22. package/lib/{deps.js → deps/deps.js} +31 -45
  23. package/lib/{deps-diff.js → deps/diff.js} +35 -25
  24. package/lib/{manifest.js → deps/manifest.js} +47 -30
  25. package/lib/diff/align.js +1 -2
  26. package/lib/diff/diff.js +2 -2
  27. package/lib/diff/display.js +29 -5
  28. package/lib/diff/inline.js +10 -14
  29. package/lib/diff/items.js +6 -4
  30. package/lib/diff/patch.js +39 -3
  31. package/lib/diff/unit.js +45 -16
  32. package/lib/{git-branches.js → git/branches.js} +53 -48
  33. package/lib/{git-deps.js → git/deps.js} +151 -95
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +504 -0
  36. package/lib/highlight/bash.js +1 -2
  37. package/lib/highlight/csv.js +1 -2
  38. package/lib/highlight/dot.js +1 -2
  39. package/lib/{highlight.js → highlight/highlight.js} +42 -11
  40. package/lib/highlight/html.js +20 -18
  41. package/lib/highlight/json.js +1 -2
  42. package/lib/highlight/log.js +1 -2
  43. package/lib/highlight/txt.js +1 -2
  44. package/lib/{session → input}/actions.js +114 -48
  45. package/lib/{editor.js → input/editor.js} +16 -8
  46. package/lib/{keys.js → input/keys.js} +24 -45
  47. package/lib/input/line-edit.js +78 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +642 -0
  50. package/lib/render/branches.js +4 -5
  51. package/lib/render/commits.js +54 -64
  52. package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
  53. package/lib/render/{dash-blocks.js → dashboard/blocks.js} +131 -137
  54. package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
  55. package/lib/render/diff.js +166 -45
  56. package/lib/render/field.js +131 -0
  57. package/lib/render/files.js +9 -14
  58. package/lib/render/footer.js +385 -0
  59. package/lib/render/frame.js +123 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +320 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +16 -11
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +9 -13
  66. package/lib/render/primitives.js +101 -15
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +13 -25
  69. package/lib/render/status.js +370 -0
  70. package/lib/render/status.json +85 -0
  71. package/lib/render/{dash-table.js → table.js} +13 -60
  72. package/lib/render/tiles.js +109 -48
  73. package/lib/render/unit.js +32 -13
  74. package/lib/{report-model.js → report/model.js} +4 -6
  75. package/lib/{report-parse.js → report/parse.js} +9 -14
  76. package/lib/{report-render.js → report/render.js} +48 -19
  77. package/lib/{report-run.js → report/run.js} +42 -30
  78. package/lib/{report-test.js → report/test.js} +56 -55
  79. package/lib/{report-tools.js → report/tools.js} +24 -33
  80. package/lib/review/agent-instructions.md +14 -0
  81. package/lib/review/format.js +420 -0
  82. package/lib/{review-merge.js → review/merge.js} +9 -8
  83. package/lib/review/review.js +190 -0
  84. package/lib/review/store.js +281 -0
  85. package/lib/review/templates.js +83 -0
  86. package/lib/runs/commands.js +14 -0
  87. package/lib/runs/logs.js +299 -0
  88. package/lib/runs/output.js +174 -0
  89. package/lib/runs/process.js +113 -0
  90. package/lib/{runs.js → runs/runs.js} +84 -36
  91. package/lib/runs/scripts.js +150 -0
  92. package/lib/session/agents/agents.js +976 -0
  93. package/lib/session/agents/job.js +320 -0
  94. package/lib/session/agents/jobs.js +355 -0
  95. package/lib/session/agents/options.js +153 -0
  96. package/lib/session/agents/plans.js +83 -0
  97. package/lib/session/branches.js +33 -40
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +91 -52
  100. package/lib/session/compose.js +189 -147
  101. package/lib/session/dashboard.js +23 -22
  102. package/lib/session/dispatch.js +86 -21
  103. package/lib/session/files-pane.js +47 -26
  104. package/lib/session/find.js +16 -29
  105. package/lib/session/import.js +93 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +78 -72
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +6 -3
  110. package/lib/session/npm/history.js +250 -0
  111. package/lib/session/npm/npm.js +836 -0
  112. package/lib/session/{packages.js → npm/packages.js} +21 -14
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +202 -10
  115. package/lib/session/prefs.js +112 -0
  116. package/lib/session/reads.js +109 -0
  117. package/lib/session/review.js +28 -10
  118. package/lib/session/run-panel.js +40 -0
  119. package/lib/{session.js → session/session.js} +158 -63
  120. package/lib/session/tasks.js +321 -66
  121. package/lib/session/templates.js +1 -1
  122. package/lib/session/terminal.js +94 -12
  123. package/lib/session/unit.js +35 -30
  124. package/lib/session/update.js +194 -6
  125. package/lib/session/view.js +65 -65
  126. package/lib/session/workspace.js +39 -78
  127. package/lib/{capabilities.js → source/capabilities.js} +3 -12
  128. package/lib/{github.js → source/github.js} +102 -32
  129. package/lib/{gitlab.js → source/gitlab.js} +87 -10
  130. package/lib/{remote.js → source/remote.js} +144 -40
  131. package/lib/source/review-threads.graphql +20 -0
  132. package/lib/source/source.js +74 -0
  133. package/lib/term/ansi.js +396 -0
  134. package/lib/{clipboard.js → term/clipboard.js} +3 -6
  135. package/lib/{select.js → term/select.js} +4 -3
  136. package/lib/term/themes/dark.json +122 -0
  137. package/lib/term/themes/light.json +122 -0
  138. package/lib/term/wide.txt +21 -0
  139. package/lib/{wrap.js → term/wrap.js} +4 -4
  140. package/package.json +2 -2
  141. package/lib/ansi.js +0 -545
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -455
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -806
  148. package/lib/render/present.js +0 -19
  149. package/lib/render/report.js +0 -43
  150. package/lib/render/search.js +0 -25
  151. package/lib/review.js +0 -745
  152. package/lib/session/accessors.js +0 -26
  153. package/lib/session/edit-click.js +0 -57
  154. package/lib/session/npm.js +0 -452
  155. package/lib/session/progress.js +0 -41
  156. package/lib/source.js +0 -36
  157. package/lib/update.js +0 -192
  158. /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
@@ -0,0 +1,573 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
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');
8
+ const { commandEnv, PipeProcess } = require('../runs/process.js');
9
+ const { ensurePlanAllows, PLAN_ALLOWS } = require('./allow.js');
10
+ 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
+ const GITDIR_PREFIX = 'gitdir:';
20
+
21
+ const plainEffort = (level) => level;
22
+ const codexEffort = (level) => `model_reasoning_effort=${level}`;
23
+
24
+ const bashAllow = (entry) => {
25
+ if (!entry.startsWith('Shell(') || !entry.endsWith(')')) return '';
26
+ const bin = entry.slice('Shell('.length, -1).split(/\s+/)[0];
27
+ if (!bin) return '';
28
+ return `Bash(${bin} *)`;
29
+ };
30
+
31
+ const BASH_ALLOWS = [...new Set(PLAN_ALLOWS.map(bashAllow).filter(Boolean))];
32
+
33
+ // A piped session has no approval host. Preapproved shells can still run tests.
34
+ const CLAUDE_PREFIX = [
35
+ '--print',
36
+ '--permission-mode',
37
+ 'acceptEdits',
38
+ '--permission-prompts',
39
+ 'none',
40
+ '--allowedTools',
41
+ ...BASH_ALLOWS,
42
+ ];
43
+
44
+ const defineAgent = (spec) => ({
45
+ id: spec.id,
46
+ name: spec.name ?? spec.id,
47
+ title: spec.title,
48
+ bins: spec.bins,
49
+ models: spec.models,
50
+ modelFlag: spec.modelFlag ?? '--model',
51
+ effortFlag: spec.effortFlag ?? '',
52
+ effortValue: spec.effortValue ?? plainEffort,
53
+ efforts: spec.efforts ?? [],
54
+ modelParams: spec.modelParams === true,
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,
70
+ });
71
+
72
+ const AGENTS = [
73
+ defineAgent({
74
+ id: 'claude',
75
+ title: 'Claude Code',
76
+ bins: ['claude'],
77
+ models: ['default', 'sonnet', 'opus', 'haiku', 'fable'],
78
+ effortFlag: '--effort',
79
+ efforts: ['default', 'low', 'medium', 'high', 'max'],
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']],
87
+ }),
88
+ defineAgent({
89
+ id: 'opencode',
90
+ title: 'OpenCode',
91
+ bins: ['opencode'],
92
+ models: ['default', 'sonnet', 'opus', 'gpt-5'],
93
+ resumeWith: (id) => ['--session', id],
94
+ listCmds: [['models']],
95
+ }),
96
+ defineAgent({
97
+ id: 'cursor',
98
+ title: 'Cursor',
99
+ bins: ['cursor-agent', 'agent'],
100
+ models: ['default', 'auto', 'grok-4.6', 'composer-2', 'gpt-5', 'sonnet'],
101
+ modelParams: true,
102
+ efforts: ['default', 'low', 'medium', 'high', 'xhigh'],
103
+ prefix: ['--print', '--trust'],
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,
112
+ }),
113
+ defineAgent({
114
+ id: 'codex',
115
+ title: 'Codex',
116
+ bins: ['codex'],
117
+ models: ['default', 'gpt-5.1-codex', 'gpt-5', 'o3'],
118
+ effortFlag: '-c',
119
+ effortValue: codexEffort,
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,
131
+ }),
132
+ defineAgent({
133
+ id: 'agy',
134
+ title: 'Antigravity',
135
+ bins: ['agy'],
136
+ models: [
137
+ 'default',
138
+ 'gemini-3.8-flash-high',
139
+ 'gemini-3.8-flash-medium',
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']],
163
+ }),
164
+ ];
165
+
166
+ const agentSpec = (id) => AGENTS.find((spec) => spec.id === id) ?? null;
167
+
168
+ const pathDirs = (env) => {
169
+ const raw = env.PATH ?? env.Path ?? '';
170
+ return raw.split(path.delimiter).filter(Boolean);
171
+ };
172
+
173
+ const winNames = (name, env) => {
174
+ const ext = env.PATHEXT || '.EXE;.CMD;.BAT;.COM';
175
+ const upper = name.toUpperCase();
176
+ const names = [name];
177
+ for (const item of ext.split(';').filter(Boolean)) {
178
+ const suffix = item.startsWith('.') ? item : `.${item}`;
179
+ if (upper.endsWith(suffix.toUpperCase())) return [name];
180
+ names.push(`${name}${suffix}`);
181
+ }
182
+ return names;
183
+ };
184
+
185
+ const binNames = (name, env) => (IS_WIN ? winNames(name, env) : [name]);
186
+
187
+ const isBin = (file) => {
188
+ try {
189
+ if (!fs.statSync(file).isFile()) return false;
190
+ if (!IS_WIN) fs.accessSync(file, fs.constants.X_OK);
191
+ return true;
192
+ } catch {
193
+ return false;
194
+ }
195
+ };
196
+
197
+ const findBin = (name, env = process.env) => {
198
+ for (const dir of pathDirs(env)) {
199
+ for (const file of binNames(name, env)) {
200
+ const full = path.join(dir, file);
201
+ if (isBin(full)) return full;
202
+ }
203
+ }
204
+ return '';
205
+ };
206
+
207
+ const findAgentBin = (spec, env) => {
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);
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(' ');
317
+ };
318
+
319
+ const planRef = (reviewPath, cwd) => {
320
+ const file = `${reviewPath ?? ''}`;
321
+ if (!file || !cwd) return file;
322
+ const rel = path.relative(cwd, file);
323
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return file;
324
+ return rel.split(path.sep).join('/');
325
+ };
326
+
327
+ const planPrompt = (reviewPath) =>
328
+ `Read and execute repair plan at ${reviewPath}`;
329
+
330
+ const needsAuth = (text) => AUTH_NEED.test(`${text ?? ''}`);
331
+
332
+ const parsedAuth = (raw) => {
333
+ const start = raw.indexOf('{');
334
+ const end = raw.lastIndexOf('}');
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
+ };
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
+ };
463
+
464
+ const modelArg = (spec, resolved, context, fast) => {
465
+ const parts = [paramPart('context', context)];
466
+ const brackets = spec.modelParams && !resolved.encoded;
467
+ if (!brackets || resolved.params === false) {
468
+ return withParams(resolved.model, parts);
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);
474
+ };
475
+
476
+ const createLaunch = (row, kind, args, plan = '', prompt = '') => {
477
+ const launch = {
478
+ ok: true,
479
+ cmd: row.bin,
480
+ args,
481
+ prompt,
482
+ plan,
483
+ id: row.id,
484
+ name: row.name,
485
+ kind,
486
+ command: '',
487
+ };
488
+ launch.command = commandLine(launch);
489
+ return launch;
490
+ };
491
+
492
+ const resumeArgs = (spec, sessionId) => {
493
+ const id = `${sessionId ?? ''}`;
494
+ if (!id || !spec.resumeWith) return [];
495
+ return spec.resumeWith(id);
496
+ };
497
+
498
+ const buildLaunch = (row, choice, reviewPath, cwd, sessionId = '') => {
499
+ if (!row || !row.bin) return { ok: false, error: 'not installed' };
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));
516
+ }
517
+ if (spec.id === 'codex') args.push(...codexGitArgs(cwd));
518
+ args.push(...splitArgs(picked.extra));
519
+ if (spec.promptFlag) args.push(spec.promptFlag);
520
+ args.push(prompt);
521
+ const launch = createLaunch(row, 'run', args, plan, prompt);
522
+ const id = `${sessionId ?? ''}`;
523
+ if (id) launch.session = id;
524
+ return launch;
525
+ };
526
+
527
+ const buildLogin = (row) => {
528
+ if (!row || !row.bin) return { ok: false, error: 'not installed' };
529
+ if (!canLogin(row)) return { ok: false, error: 'no login' };
530
+ return createLaunch(row, 'login', row.spec.login.slice());
531
+ };
532
+
533
+ // The review screen forces raw output. An agent needs the reduced test report.
534
+ const agentEnv = () => {
535
+ const env = commandEnv();
536
+ delete env.RESLOP_OUTPUT;
537
+ return env;
538
+ };
539
+
540
+ const startPipe = (cwd, launch, onData, onClose) => {
541
+ const options = spawnBase({ cwd, env: agentEnv(), detached: !IS_WIN });
542
+ const proc = new PipeProcess(onData, onClose);
543
+ proc.start(launch.cmd, launch.args, options).closeInput();
544
+ return proc;
545
+ };
546
+
547
+ // An agent that reads an allow list gets the plan entries before it runs
548
+ const startAgent = (cwd, launch, onData, onClose) => {
549
+ const spec = agentSpec(launch.id);
550
+ const listed = launch.kind === 'run' && spec && spec.allowlist;
551
+ if (listed) ensurePlanAllows();
552
+ return startPipe(cwd, launch, onData, onClose);
553
+ };
554
+
555
+ module.exports = {
556
+ ...models,
557
+ AGENTS,
558
+ agentSpec,
559
+ findBin,
560
+ detectAgents,
561
+ canLogin,
562
+ listedOf,
563
+ listModels,
564
+ splitArgs,
565
+ planPrompt,
566
+ needsAuth,
567
+ authState,
568
+ probeAuth,
569
+ commandLine,
570
+ buildLaunch,
571
+ buildLogin,
572
+ startAgent,
573
+ };
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { jsonParse, isHashObject } = require('metautil');
7
+ const { writeJson, trimText } = require('../common/utilities.js');
8
+
9
+ const { planAllows: PLAN_ALLOWS } = require('./allow.json');
10
+
11
+ const cursorConfigPath = (env = process.env) => {
12
+ const override = trimText(env.RESLOP_CURSOR_CONFIG);
13
+ if (override) return override;
14
+ const root = trimText(env.CURSOR_CONFIG_DIR);
15
+ const dir = root || path.join(os.homedir(), '.cursor');
16
+ return path.join(dir, 'cli-config.json');
17
+ };
18
+
19
+ const readConfig = (file) => {
20
+ let text = null;
21
+ try {
22
+ text = fs.readFileSync(file, 'utf8');
23
+ } catch (error) {
24
+ if (!error || error.code !== 'ENOENT') return null;
25
+ }
26
+ if (text === null) return {};
27
+ const data = jsonParse(text);
28
+ if (!isHashObject(data)) return null;
29
+ return data;
30
+ };
31
+
32
+ const allowList = (data) => {
33
+ if (!data.permissions || typeof data.permissions !== 'object') {
34
+ data.permissions = { allow: [], deny: [] };
35
+ }
36
+ if (!Array.isArray(data.permissions.allow)) data.permissions.allow = [];
37
+ if (!Array.isArray(data.permissions.deny)) data.permissions.deny = [];
38
+ return data.permissions.allow;
39
+ };
40
+
41
+ const ensureAllows = (file, entries) => {
42
+ const data = readConfig(file);
43
+ if (!data) return false;
44
+ const allow = allowList(data);
45
+ let changed = false;
46
+ for (const entry of entries) {
47
+ const token = trimText(entry);
48
+ if (!token || allow.includes(token)) continue;
49
+ allow.push(token);
50
+ changed = true;
51
+ }
52
+ if (!changed) return true;
53
+ if (!data.version) data.version = 1;
54
+ if (!data.approvalMode) data.approvalMode = 'allowlist';
55
+ try {
56
+ writeJson(file, data);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ };
62
+
63
+ const ensurePlanAllows = (env = process.env) =>
64
+ ensureAllows(cursorConfigPath(env), PLAN_ALLOWS);
65
+
66
+ module.exports = {
67
+ PLAN_ALLOWS,
68
+ cursorConfigPath,
69
+ ensureAllows,
70
+ ensurePlanAllows,
71
+ };
@@ -0,0 +1,17 @@
1
+ {
2
+ "planAllows": [
3
+ "Write(.plan/**)",
4
+ "Write(.plan/**/*.md)",
5
+ "Write(**/.plan/**)",
6
+ "Write(**/.plan/**/*.md)",
7
+ "Shell(npm)",
8
+ "Shell(npm test)",
9
+ "Shell(npm t)",
10
+ "Shell(npx)",
11
+ "Shell(node)",
12
+ "Shell(node --test)",
13
+ "Shell(reslop)",
14
+ "Shell(reslop t)",
15
+ "Shell(git)"
16
+ ]
17
+ }