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.
Files changed (104) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +710 -97
  3. package/bin/loreli.js +89 -0
  4. package/package.json +77 -14
  5. package/packages/README.md +101 -0
  6. package/packages/action/README.md +98 -0
  7. package/packages/action/prompts/action.md +172 -0
  8. package/packages/action/src/index.js +684 -0
  9. package/packages/agent/README.md +606 -0
  10. package/packages/agent/src/backends/claude.js +387 -0
  11. package/packages/agent/src/backends/codex.js +351 -0
  12. package/packages/agent/src/backends/cursor.js +371 -0
  13. package/packages/agent/src/backends/index.js +486 -0
  14. package/packages/agent/src/base.js +138 -0
  15. package/packages/agent/src/cli.js +275 -0
  16. package/packages/agent/src/discover.js +396 -0
  17. package/packages/agent/src/factory.js +124 -0
  18. package/packages/agent/src/index.js +12 -0
  19. package/packages/agent/src/models.js +159 -0
  20. package/packages/agent/src/output.js +62 -0
  21. package/packages/agent/src/session.js +162 -0
  22. package/packages/agent/src/trace.js +186 -0
  23. package/packages/classify/README.md +136 -0
  24. package/packages/classify/prompts/blocker.md +12 -0
  25. package/packages/classify/prompts/feedback.md +14 -0
  26. package/packages/classify/prompts/pane-state.md +20 -0
  27. package/packages/classify/src/index.js +81 -0
  28. package/packages/config/README.md +898 -0
  29. package/packages/config/src/defaults.js +145 -0
  30. package/packages/config/src/index.js +223 -0
  31. package/packages/config/src/schema.js +291 -0
  32. package/packages/config/src/validate.js +160 -0
  33. package/packages/context/README.md +165 -0
  34. package/packages/context/src/index.js +198 -0
  35. package/packages/hub/README.md +338 -0
  36. package/packages/hub/src/base.js +154 -0
  37. package/packages/hub/src/github.js +1597 -0
  38. package/packages/hub/src/index.js +79 -0
  39. package/packages/hub/src/labels.js +48 -0
  40. package/packages/identity/README.md +288 -0
  41. package/packages/identity/src/index.js +620 -0
  42. package/packages/identity/src/themes/avatar.js +217 -0
  43. package/packages/identity/src/themes/digimon.js +217 -0
  44. package/packages/identity/src/themes/dragonball.js +217 -0
  45. package/packages/identity/src/themes/lotr.js +217 -0
  46. package/packages/identity/src/themes/marvel.js +217 -0
  47. package/packages/identity/src/themes/pokemon.js +217 -0
  48. package/packages/identity/src/themes/starwars.js +217 -0
  49. package/packages/identity/src/themes/transformers.js +217 -0
  50. package/packages/identity/src/themes/zelda.js +217 -0
  51. package/packages/knowledge/README.md +217 -0
  52. package/packages/knowledge/src/index.js +243 -0
  53. package/packages/log/README.md +93 -0
  54. package/packages/log/src/index.js +252 -0
  55. package/packages/marker/README.md +200 -0
  56. package/packages/marker/src/index.js +184 -0
  57. package/packages/mcp/README.md +323 -0
  58. package/packages/mcp/instructions.md +126 -0
  59. package/packages/mcp/scaffolding/.agents/skills/loreli-context/SKILL.md +89 -0
  60. package/packages/mcp/scaffolding/ISSUE_TEMPLATE/config.yml +2 -0
  61. package/packages/mcp/scaffolding/ISSUE_TEMPLATE/loreli.yml +83 -0
  62. package/packages/mcp/scaffolding/loreli.yml +491 -0
  63. package/packages/mcp/scaffolding/mcp-configs/.codex/config.toml +4 -0
  64. package/packages/mcp/scaffolding/mcp-configs/.cursor/mcp.json +14 -0
  65. package/packages/mcp/scaffolding/mcp-configs/.mcp.json +14 -0
  66. package/packages/mcp/scaffolding/pull-request.md +23 -0
  67. package/packages/mcp/src/index.js +600 -0
  68. package/packages/mcp/src/tools/agent-context.js +44 -0
  69. package/packages/mcp/src/tools/agents.js +450 -0
  70. package/packages/mcp/src/tools/context.js +200 -0
  71. package/packages/mcp/src/tools/github.js +1163 -0
  72. package/packages/mcp/src/tools/hitl.js +162 -0
  73. package/packages/mcp/src/tools/index.js +18 -0
  74. package/packages/mcp/src/tools/refactor.js +227 -0
  75. package/packages/mcp/src/tools/repo.js +44 -0
  76. package/packages/mcp/src/tools/start.js +904 -0
  77. package/packages/mcp/src/tools/status.js +149 -0
  78. package/packages/mcp/src/tools/work.js +134 -0
  79. package/packages/orchestrator/README.md +192 -0
  80. package/packages/orchestrator/src/index.js +1492 -0
  81. package/packages/planner/README.md +251 -0
  82. package/packages/planner/prompts/plan-reviewer.md +109 -0
  83. package/packages/planner/prompts/planner.md +191 -0
  84. package/packages/planner/prompts/tiebreaker-reviewer.md +71 -0
  85. package/packages/planner/src/index.js +1381 -0
  86. package/packages/review/README.md +129 -0
  87. package/packages/review/prompts/reviewer.md +158 -0
  88. package/packages/review/src/index.js +1403 -0
  89. package/packages/risk/README.md +178 -0
  90. package/packages/risk/prompts/risk.md +272 -0
  91. package/packages/risk/src/index.js +439 -0
  92. package/packages/session/README.md +165 -0
  93. package/packages/session/src/index.js +215 -0
  94. package/packages/test-utils/README.md +96 -0
  95. package/packages/test-utils/src/index.js +354 -0
  96. package/packages/tmux/README.md +261 -0
  97. package/packages/tmux/src/index.js +501 -0
  98. package/packages/workflow/README.md +317 -0
  99. package/packages/workflow/prompts/preamble.md +14 -0
  100. package/packages/workflow/src/index.js +660 -0
  101. package/packages/workflow/src/proof-of-life.js +74 -0
  102. package/packages/workspace/README.md +143 -0
  103. package/packages/workspace/src/index.js +1127 -0
  104. 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
+ }