loreli 0.0.0 → 2.0.0
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/LICENSE +1 -1
- package/README.md +710 -97
- package/bin/loreli.js +89 -0
- package/package.json +77 -14
- package/packages/README.md +101 -0
- package/packages/action/README.md +98 -0
- package/packages/action/prompts/action.md +172 -0
- package/packages/action/src/index.js +684 -0
- package/packages/agent/README.md +606 -0
- package/packages/agent/src/backends/claude.js +387 -0
- package/packages/agent/src/backends/codex.js +351 -0
- package/packages/agent/src/backends/cursor.js +371 -0
- package/packages/agent/src/backends/index.js +486 -0
- package/packages/agent/src/base.js +138 -0
- package/packages/agent/src/cli.js +275 -0
- package/packages/agent/src/discover.js +396 -0
- package/packages/agent/src/factory.js +124 -0
- package/packages/agent/src/index.js +12 -0
- package/packages/agent/src/models.js +159 -0
- package/packages/agent/src/output.js +62 -0
- package/packages/agent/src/session.js +162 -0
- package/packages/agent/src/trace.js +186 -0
- package/packages/classify/README.md +136 -0
- package/packages/classify/prompts/blocker.md +12 -0
- package/packages/classify/prompts/feedback.md +14 -0
- package/packages/classify/prompts/pane-state.md +20 -0
- package/packages/classify/src/index.js +81 -0
- package/packages/config/README.md +898 -0
- package/packages/config/src/defaults.js +145 -0
- package/packages/config/src/index.js +223 -0
- package/packages/config/src/schema.js +291 -0
- package/packages/config/src/validate.js +160 -0
- package/packages/context/README.md +165 -0
- package/packages/context/src/index.js +198 -0
- package/packages/hub/README.md +338 -0
- package/packages/hub/src/base.js +154 -0
- package/packages/hub/src/github.js +1597 -0
- package/packages/hub/src/index.js +79 -0
- package/packages/hub/src/labels.js +48 -0
- package/packages/identity/README.md +288 -0
- package/packages/identity/src/index.js +620 -0
- package/packages/identity/src/themes/avatar.js +217 -0
- package/packages/identity/src/themes/digimon.js +217 -0
- package/packages/identity/src/themes/dragonball.js +217 -0
- package/packages/identity/src/themes/lotr.js +217 -0
- package/packages/identity/src/themes/marvel.js +217 -0
- package/packages/identity/src/themes/pokemon.js +217 -0
- package/packages/identity/src/themes/starwars.js +217 -0
- package/packages/identity/src/themes/transformers.js +217 -0
- package/packages/identity/src/themes/zelda.js +217 -0
- package/packages/knowledge/README.md +217 -0
- package/packages/knowledge/src/index.js +243 -0
- package/packages/log/README.md +93 -0
- package/packages/log/src/index.js +252 -0
- package/packages/marker/README.md +200 -0
- package/packages/marker/src/index.js +184 -0
- package/packages/mcp/README.md +323 -0
- package/packages/mcp/instructions.md +126 -0
- package/packages/mcp/scaffolding/.agents/skills/loreli-context/SKILL.md +89 -0
- package/packages/mcp/scaffolding/ISSUE_TEMPLATE/config.yml +2 -0
- package/packages/mcp/scaffolding/ISSUE_TEMPLATE/loreli.yml +83 -0
- package/packages/mcp/scaffolding/loreli.yml +491 -0
- package/packages/mcp/scaffolding/mcp-configs/.codex/config.toml +4 -0
- package/packages/mcp/scaffolding/mcp-configs/.cursor/mcp.json +14 -0
- package/packages/mcp/scaffolding/mcp-configs/.mcp.json +14 -0
- package/packages/mcp/scaffolding/pull-request.md +23 -0
- package/packages/mcp/src/index.js +600 -0
- package/packages/mcp/src/tools/agent-context.js +44 -0
- package/packages/mcp/src/tools/agents.js +450 -0
- package/packages/mcp/src/tools/context.js +200 -0
- package/packages/mcp/src/tools/github.js +1163 -0
- package/packages/mcp/src/tools/hitl.js +162 -0
- package/packages/mcp/src/tools/index.js +18 -0
- package/packages/mcp/src/tools/refactor.js +227 -0
- package/packages/mcp/src/tools/repo.js +44 -0
- package/packages/mcp/src/tools/start.js +904 -0
- package/packages/mcp/src/tools/status.js +149 -0
- package/packages/mcp/src/tools/work.js +134 -0
- package/packages/orchestrator/README.md +192 -0
- package/packages/orchestrator/src/index.js +1492 -0
- package/packages/planner/README.md +251 -0
- package/packages/planner/prompts/plan-reviewer.md +109 -0
- package/packages/planner/prompts/planner.md +191 -0
- package/packages/planner/prompts/tiebreaker-reviewer.md +71 -0
- package/packages/planner/src/index.js +1381 -0
- package/packages/review/README.md +129 -0
- package/packages/review/prompts/reviewer.md +158 -0
- package/packages/review/src/index.js +1403 -0
- package/packages/risk/README.md +178 -0
- package/packages/risk/prompts/risk.md +272 -0
- package/packages/risk/src/index.js +439 -0
- package/packages/session/README.md +165 -0
- package/packages/session/src/index.js +215 -0
- package/packages/test-utils/README.md +96 -0
- package/packages/test-utils/src/index.js +354 -0
- package/packages/tmux/README.md +261 -0
- package/packages/tmux/src/index.js +501 -0
- package/packages/workflow/README.md +317 -0
- package/packages/workflow/prompts/preamble.md +14 -0
- package/packages/workflow/src/index.js +660 -0
- package/packages/workflow/src/proof-of-life.js +74 -0
- package/packages/workspace/README.md +143 -0
- package/packages/workspace/src/index.js +1127 -0
- package/index.js +0 -8
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { CliAgent } from '../cli.js';
|
|
4
|
+
import { resolve, env } from '../models.js';
|
|
5
|
+
import { mcpJson, cursorMatcher } from 'loreli/workspace';
|
|
6
|
+
import { vendor } from 'loreli/identity';
|
|
7
|
+
import { logger } from 'loreli/log';
|
|
8
|
+
|
|
9
|
+
const log = logger('cursor');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Interval for polling pane content during startup.
|
|
13
|
+
* @type {number}
|
|
14
|
+
*/
|
|
15
|
+
const READY_POLL = 1000;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Maximum time to wait for cursor-agent to become ready.
|
|
19
|
+
* @type {number}
|
|
20
|
+
*/
|
|
21
|
+
const READY_TIMEOUT = 60000;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Maximum time (ms) to wait for the TUI input prompt after the banner
|
|
25
|
+
* appears. This is NOT a fixed sleep — the readiness loop polls for
|
|
26
|
+
* a concrete signal (workspace path line rendered in the pane).
|
|
27
|
+
*
|
|
28
|
+
* @type {number}
|
|
29
|
+
*/
|
|
30
|
+
const INPUT_READY_TIMEOUT = 15000;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Settle delay (ms) after Phase 1 before sending the trust-approval
|
|
34
|
+
* keystroke. Gives cursor-agent time to render the trust dialog.
|
|
35
|
+
* @type {number}
|
|
36
|
+
*/
|
|
37
|
+
const TRUST_SETTLE = 2000;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Cursor Agent CLI backend. Interactive: stays running in a tmux pane.
|
|
41
|
+
*
|
|
42
|
+
* Cursor Agent is the multi-provider workhorse — it can run models from
|
|
43
|
+
* Anthropic, OpenAI, Google, and others through a single CLI. This makes
|
|
44
|
+
* it the natural fallback when provider-specific CLIs (claude, codex) are
|
|
45
|
+
* unavailable or their API endpoints are unreachable.
|
|
46
|
+
*
|
|
47
|
+
* The yin/yang adversarial pairing is preserved because each agent's
|
|
48
|
+
* identity carries its provider. The model resolver maps `balanced` to
|
|
49
|
+
* `claude-sonnet-4-20250514` for anthropic and `gpt-4o` for openai.
|
|
50
|
+
* The CURSOR_MODELS table then translates those API identifiers into
|
|
51
|
+
* cursor-agent short names.
|
|
52
|
+
*
|
|
53
|
+
* Flags:
|
|
54
|
+
* - `--force`: auto-approve tool usage (file writes, bash commands)
|
|
55
|
+
* - `--sandbox disabled`: no sandbox isolation prompts
|
|
56
|
+
* - `--approve-mcps`: auto-approve scaffolded Loreli MCP server
|
|
57
|
+
*
|
|
58
|
+
* @extends CliAgent
|
|
59
|
+
*/
|
|
60
|
+
export class CursorBackend extends CliAgent {
|
|
61
|
+
/**
|
|
62
|
+
* Regex patterns that identify known Cursor Agent blocking dialogs.
|
|
63
|
+
*
|
|
64
|
+
* @type {Array<{pattern: RegExp, category: string, reasoning: string, remedy?: string[]}>}
|
|
65
|
+
*/
|
|
66
|
+
static patterns = [
|
|
67
|
+
{
|
|
68
|
+
pattern: /ready to build\?[\s\S]*no,\s*propose changes\s*\(p\s*or\s*esc\)/i,
|
|
69
|
+
category: 'option_dialog',
|
|
70
|
+
reasoning: 'Cursor plan-mode summary dialog blocks planner execution until a choice is made',
|
|
71
|
+
remedy: ['p']
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
pattern: /read the complete task instructions from[\s\S]*follow every instruction precisely\./i,
|
|
75
|
+
category: 'option_dialog',
|
|
76
|
+
reasoning: 'Cursor can remain on the initial injected prompt until an explicit continuation Enter key',
|
|
77
|
+
remedy: ['Enter']
|
|
78
|
+
}
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Detect known Cursor Agent CLI blocking patterns in pane output.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} output - Captured pane content.
|
|
85
|
+
* @returns {{category: string, reasoning: string, remedy?: string[]}|null} Diagnosis, or null if unrecognized.
|
|
86
|
+
*/
|
|
87
|
+
static diagnose(output) {
|
|
88
|
+
if (!output) return null;
|
|
89
|
+
for (const { pattern, category, reasoning, remedy } of CursorBackend.patterns) {
|
|
90
|
+
if (pattern.test(output)) return { category, reasoning, ...(remedy && { remedy }) };
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Return the scaffold descriptor for Cursor Agent workspaces.
|
|
97
|
+
*
|
|
98
|
+
* Token propagation relies on process env inheritance and the
|
|
99
|
+
* envFile fallback. cursor-agent CLI does not expand
|
|
100
|
+
* `${env:NAME}` interpolation — it passes the literal string
|
|
101
|
+
* to MCP subprocesses, poisoning `_hydrateHub()`.
|
|
102
|
+
* Hooks use beforeShellExecution.
|
|
103
|
+
*
|
|
104
|
+
* @param {object} [context] - Agent context for env/token injection.
|
|
105
|
+
* @returns {object} Scaffold descriptor.
|
|
106
|
+
*/
|
|
107
|
+
static scaffold(context) {
|
|
108
|
+
const denied = context?.denied ?? [];
|
|
109
|
+
|
|
110
|
+
const dangerousCommands = [
|
|
111
|
+
...denied,
|
|
112
|
+
'git', 'rm', 'mv', 'chmod', 'sed', 'perl', 'tee', 'truncate',
|
|
113
|
+
'node', 'python', 'python3', 'ruby'
|
|
114
|
+
];
|
|
115
|
+
const unique = [...new Set(dangerousCommands)];
|
|
116
|
+
|
|
117
|
+
const descriptor = {
|
|
118
|
+
configs: [
|
|
119
|
+
{
|
|
120
|
+
path: '.mcp.json',
|
|
121
|
+
content: mcpJson(context, context ? { tokenRef: '${GITHUB_TOKEN}' } : {}),
|
|
122
|
+
marker: 'loreli',
|
|
123
|
+
format: 'json'
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
path: '.cursor/mcp.json',
|
|
127
|
+
content: mcpJson(context, context ? {
|
|
128
|
+
envFile: '.git/loreli.env'
|
|
129
|
+
} : {}),
|
|
130
|
+
marker: 'loreli',
|
|
131
|
+
format: 'json'
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
hooks: [
|
|
135
|
+
{
|
|
136
|
+
path: '.cursor/hooks.json',
|
|
137
|
+
key: 'beforeShellExecution',
|
|
138
|
+
entry: {
|
|
139
|
+
command: '.loreli/deny.sh',
|
|
140
|
+
matcher: cursorMatcher(unique)
|
|
141
|
+
},
|
|
142
|
+
marker: 'deny.sh',
|
|
143
|
+
defaults: { version: 1 }
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
files: []
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Declare .git/loreli.env for token propagation. Written by
|
|
150
|
+
// workspace.create() after clone when .git/ exists.
|
|
151
|
+
if (context?.token) {
|
|
152
|
+
descriptor.files.push({
|
|
153
|
+
path: '.git/loreli.env',
|
|
154
|
+
content: `GITHUB_TOKEN=${context.token}\n`,
|
|
155
|
+
mode: 0o600
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return descriptor;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* One-shot LLM call via `cursor-agent -p` (print mode).
|
|
163
|
+
*
|
|
164
|
+
* @param {string} prompt - Text prompt to send.
|
|
165
|
+
* @param {object} [opts] - Options.
|
|
166
|
+
* @param {string} [opts.model='fast'] - Model alias or exact string.
|
|
167
|
+
* @param {string} [opts.provider='anthropic'] - Provider for model resolution.
|
|
168
|
+
* @param {object} [opts.config] - Config instance for model resolution.
|
|
169
|
+
* @param {number} [opts.timeout=60000] - Max execution time in ms.
|
|
170
|
+
* @returns {Promise<string>} LLM response text.
|
|
171
|
+
*/
|
|
172
|
+
static async oneshot(prompt, { model, provider, config, timeout, discovered } = {}) {
|
|
173
|
+
const v = vendor(provider ?? 'anthropic');
|
|
174
|
+
const resolved = resolve(model ?? 'fast', 'cursor', v, config, discovered);
|
|
175
|
+
const vars = env('cursor', config) ?? {};
|
|
176
|
+
return CliAgent._exec('cursor-agent', [
|
|
177
|
+
'-p', '--trust', '--model', resolved, '--output-format', 'text'
|
|
178
|
+
], vars, timeout, prompt);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @param {object} opts
|
|
183
|
+
* @param {object} opts.identity - Agent identity.
|
|
184
|
+
* @param {string} opts.role - Agent role.
|
|
185
|
+
* @param {string} opts.cwd - Working directory.
|
|
186
|
+
* @param {string} [opts.model='balanced'] - Model alias or exact string.
|
|
187
|
+
* @param {object} [opts.config] - Config instance for model resolution.
|
|
188
|
+
* @param {string} [opts.session='loreli'] - Tmux session name.
|
|
189
|
+
* @param {number} [opts.readyTimeout=60000] - Max ms to wait for CLI readiness.
|
|
190
|
+
*/
|
|
191
|
+
constructor(opts) {
|
|
192
|
+
const v = vendor(opts.identity?.provider ?? 'anthropic');
|
|
193
|
+
const model = resolve(opts.model ?? 'balanced', 'cursor', v, opts.config, opts.discovered);
|
|
194
|
+
const mode = opts.role === 'planner' ? 'plan' : undefined;
|
|
195
|
+
|
|
196
|
+
super({
|
|
197
|
+
...opts,
|
|
198
|
+
command: buildCommand(model, opts.cwd, mode)
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
/** @type {string} Resolved model identifier. */
|
|
202
|
+
this.model = model;
|
|
203
|
+
|
|
204
|
+
/** @type {object|undefined} Backend-specific environment variables. */
|
|
205
|
+
this._env = env('cursor', opts.config) ?? {};
|
|
206
|
+
|
|
207
|
+
if (opts.context?.token) this._env.GITHUB_TOKEN = opts.context.token;
|
|
208
|
+
|
|
209
|
+
/** @type {number} Max ms to wait for CLI readiness. */
|
|
210
|
+
this.readyTimeout = opts.readyTimeout ?? READY_TIMEOUT;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Spawn cursor-agent and wait for readiness.
|
|
215
|
+
*
|
|
216
|
+
* Builds an inline shell command with env exports and passes it
|
|
217
|
+
* directly to tmux — no launcher scripts on disk.
|
|
218
|
+
*
|
|
219
|
+
* @returns {Promise<void>}
|
|
220
|
+
*/
|
|
221
|
+
async spawn() {
|
|
222
|
+
const cmd = this._shell(`exec ${this.command}`, this._env);
|
|
223
|
+
this.paneId = await this._launch(cmd);
|
|
224
|
+
|
|
225
|
+
log.info(`spawning ${this.identity?.name ?? '?'} in pane ${this.paneId}: ${this.command}`);
|
|
226
|
+
this.transition('spawned');
|
|
227
|
+
|
|
228
|
+
await this._navigate();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Wait for cursor-agent to finish initializing.
|
|
233
|
+
*
|
|
234
|
+
* Readiness is detected reactively through three phases:
|
|
235
|
+
*
|
|
236
|
+
* **Phase 1 — Process detection** (up to `readyTimeout`):
|
|
237
|
+
* Polls until the cursor-agent Node process is running. Two signals:
|
|
238
|
+
* - Banner: `Cursor Agent v<version>` appears in pane output.
|
|
239
|
+
* - Process: tmux reports `node` or a version string as the
|
|
240
|
+
* foreground command.
|
|
241
|
+
*
|
|
242
|
+
* **Phase 1.5 — Trust approval**:
|
|
243
|
+
* cursor-agent shows a "Workspace Trust" dialog for directories
|
|
244
|
+
* not previously trusted. The dialog blocks all TUI rendering.
|
|
245
|
+
* `--force` only auto-approves command execution, not workspace
|
|
246
|
+
* trust, and `--trust` only works in `--print` mode. Sending
|
|
247
|
+
* Enter dismisses the dialog (default action = approve). When
|
|
248
|
+
* no dialog is showing, the keystroke enters the empty input
|
|
249
|
+
* field harmlessly.
|
|
250
|
+
*
|
|
251
|
+
* **Phase 2 — Input readiness** (up to `INPUT_READY_TIMEOUT`):
|
|
252
|
+
* After the process starts, continues polling pane output for the
|
|
253
|
+
* TUI's workspace line (e.g. `~/path · branch`). This line renders
|
|
254
|
+
* only after MCP connections are established and the input field is
|
|
255
|
+
* active.
|
|
256
|
+
*
|
|
257
|
+
* @returns {Promise<void>}
|
|
258
|
+
*/
|
|
259
|
+
async _navigate() {
|
|
260
|
+
if (this.readyTimeout <= 0) return;
|
|
261
|
+
|
|
262
|
+
const deadline = Date.now() + this.readyTimeout;
|
|
263
|
+
const name = this.identity?.name ?? '?';
|
|
264
|
+
|
|
265
|
+
// Phase 1: detect cursor-agent process startup
|
|
266
|
+
while (Date.now() < deadline) {
|
|
267
|
+
const output = await this.tmux.capture(this.paneId);
|
|
268
|
+
|
|
269
|
+
if (output.includes('Cursor Agent v')) {
|
|
270
|
+
log.info(`${name} phase 1 — cursor-agent banner detected`);
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const cmd = await this.tmux.command(this.paneId);
|
|
275
|
+
if (cmd === 'node' || /^\d+\.\d+\.\d+/.test(cmd)) {
|
|
276
|
+
log.info(`${name} phase 1 — cursor-agent process detected (${cmd})`);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await new Promise(function wait(r) { setTimeout(r, READY_POLL); });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Phase 1.5: dismiss "Workspace Trust" dialog if present.
|
|
284
|
+
// cursor-agent blocks on an interactive trust prompt for
|
|
285
|
+
// programmatically-created workspaces. Enter approves it.
|
|
286
|
+
await new Promise(function settle(r) { setTimeout(r, TRUST_SETTLE); });
|
|
287
|
+
await this.tmux.keys(this.paneId, 'Enter');
|
|
288
|
+
log.info(`${name} phase 1.5 — sent trust approval (Enter)`);
|
|
289
|
+
|
|
290
|
+
// Phase 2: wait for TUI input readiness (reactive, not fixed delay).
|
|
291
|
+
const inputDeadline = Date.now() + INPUT_READY_TIMEOUT;
|
|
292
|
+
while (Date.now() < inputDeadline) {
|
|
293
|
+
const output = await this.tmux.capture(this.paneId);
|
|
294
|
+
|
|
295
|
+
if (output.includes('\u00b7')) {
|
|
296
|
+
log.info(`${name} phase 2 — TUI input ready (workspace line detected)`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
await new Promise(function wait(r) { setTimeout(r, READY_POLL); });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
log.warn(`${name} input readiness timeout after ${INPUT_READY_TIMEOUT}ms — proceeding anyway`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Send a prompt to cursor-agent's interactive session.
|
|
308
|
+
*
|
|
309
|
+
* Multi-line prompts are written to a Markdown file and a single-line
|
|
310
|
+
* reference is sent, matching the ClaudeBackend pattern. This avoids
|
|
311
|
+
* tmux send-keys fragmenting the message at newlines.
|
|
312
|
+
*
|
|
313
|
+
* @param {string} message - The prompt text.
|
|
314
|
+
* @returns {Promise<void>}
|
|
315
|
+
*/
|
|
316
|
+
async send(message) {
|
|
317
|
+
if (!this.paneId) throw new Error('Agent not spawned — call spawn() first');
|
|
318
|
+
if (this.canTransition('working')) this.transition('working');
|
|
319
|
+
|
|
320
|
+
if (!message.includes('\n')) {
|
|
321
|
+
await this.tmux.send(this.paneId, message);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const dir = join(this.cwd, '.loreli');
|
|
326
|
+
await mkdir(dir, { recursive: true });
|
|
327
|
+
const file = join(dir, `task-${Date.now()}.md`);
|
|
328
|
+
await writeFile(file, message, 'utf8');
|
|
329
|
+
log.info(`${this.identity?.name ?? '?'} prompt written to ${file}`);
|
|
330
|
+
|
|
331
|
+
await this.tmux.send(this.paneId,
|
|
332
|
+
`Read the complete task instructions from ${file} and execute them. Follow every instruction precisely.`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Build the cursor-agent CLI command string.
|
|
339
|
+
*
|
|
340
|
+
* Model names are resolved directly from config — no translation table.
|
|
341
|
+
* The `backends.cursor.models` config maps tiers to cursor-agent native
|
|
342
|
+
* names per provider (e.g. `sonnet-4.5-thinking`, `gpt-5.3-codex`).
|
|
343
|
+
*
|
|
344
|
+
* Flags:
|
|
345
|
+
* - `--model`: resolved cursor-agent model name
|
|
346
|
+
* - `--plan`: read-only plan mode (planner)
|
|
347
|
+
* - `--force`: auto-approve tool prompts (all roles; required for unattended planner MCP calls)
|
|
348
|
+
* - `--sandbox disabled`: disable sandbox isolation prompts (action/reviewer)
|
|
349
|
+
* - `--approve-mcps`: auto-approve the scaffolded Loreli MCP server
|
|
350
|
+
* - `--workspace`: set the working directory
|
|
351
|
+
*
|
|
352
|
+
* @param {string} model - Resolved cursor-agent model name from config.
|
|
353
|
+
* @param {string} [cwd] - Working directory for --workspace.
|
|
354
|
+
* @param {string} [mode] - Execution mode ('plan' for planner read-only mode).
|
|
355
|
+
* @returns {string} CLI command string.
|
|
356
|
+
*/
|
|
357
|
+
function buildCommand(model, cwd, mode) {
|
|
358
|
+
const parts = ['cursor-agent', `--model ${model}`];
|
|
359
|
+
|
|
360
|
+
if (mode === 'plan') {
|
|
361
|
+
parts.push('--plan', '--force');
|
|
362
|
+
} else {
|
|
363
|
+
parts.push('--force', '--sandbox disabled');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
parts.push('--approve-mcps');
|
|
367
|
+
|
|
368
|
+
if (cwd) parts.push(`--workspace '${cwd}'`);
|
|
369
|
+
|
|
370
|
+
return parts.join(' ');
|
|
371
|
+
}
|