ralph-cli-sandboxed 0.6.6 → 0.7.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 (36) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +1 -1
  3. package/dist/commands/ask.d.ts +6 -0
  4. package/dist/commands/ask.js +140 -0
  5. package/dist/commands/branch.js +8 -4
  6. package/dist/commands/chat.js +11 -8
  7. package/dist/commands/docker.js +19 -4
  8. package/dist/commands/fix-config.js +0 -41
  9. package/dist/commands/help.js +10 -0
  10. package/dist/commands/run.js +9 -9
  11. package/dist/index.js +2 -0
  12. package/dist/providers/telegram.js +1 -1
  13. package/dist/responders/claude-code-responder.js +1 -0
  14. package/dist/responders/cli-responder.js +1 -0
  15. package/dist/responders/llm-responder.js +1 -1
  16. package/dist/templates/macos-scripts.js +18 -18
  17. package/dist/tui/components/JsonSnippetEditor.js +7 -7
  18. package/dist/tui/components/KeyValueEditor.js +5 -1
  19. package/dist/tui/components/LLMProvidersEditor.js +7 -9
  20. package/dist/tui/components/Preview.js +1 -1
  21. package/dist/tui/components/SectionNav.js +18 -2
  22. package/dist/utils/chat-client.js +1 -0
  23. package/dist/utils/config.d.ts +1 -0
  24. package/dist/utils/config.js +3 -1
  25. package/dist/utils/config.test.d.ts +1 -0
  26. package/dist/utils/config.test.js +424 -0
  27. package/dist/utils/notification.js +1 -1
  28. package/dist/utils/prd-validator.js +16 -4
  29. package/dist/utils/prd-validator.test.d.ts +1 -0
  30. package/dist/utils/prd-validator.test.js +1095 -0
  31. package/dist/utils/responder.js +4 -1
  32. package/dist/utils/stream-json.test.d.ts +1 -0
  33. package/dist/utils/stream-json.test.js +1007 -0
  34. package/docs/DOCKER.md +14 -0
  35. package/docs/PRD-GENERATOR.md +15 -0
  36. package/package.json +16 -13
@@ -0,0 +1,424 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { getCliConfig, getLLMProviders, getLLMProviderApiKey, getLLMProviderBaseUrl, DEFAULT_LLM_PROVIDERS, DEFAULT_CLI_CONFIG, } from "./config.js";
3
+ // ─── getCliConfig ───────────────────────────────────────────────────
4
+ describe("getCliConfig", () => {
5
+ it("returns default CLI config when none specified", () => {
6
+ const config = {
7
+ language: "typescript",
8
+ checkCommand: "npm run build",
9
+ testCommand: "npm test",
10
+ };
11
+ const result = getCliConfig(config);
12
+ expect(result.command).toBe("claude");
13
+ expect(result.promptArgs).toEqual(["-p"]);
14
+ });
15
+ it("returns custom CLI config from config", () => {
16
+ const config = {
17
+ language: "typescript",
18
+ checkCommand: "npm run build",
19
+ testCommand: "npm test",
20
+ cli: {
21
+ command: "aider",
22
+ args: ["--yes"],
23
+ promptArgs: ["--message"],
24
+ },
25
+ };
26
+ const result = getCliConfig(config);
27
+ expect(result.command).toBe("aider");
28
+ expect(result.promptArgs).toEqual(["--message"]);
29
+ });
30
+ it("defaults promptArgs to ['-p'] when not set", () => {
31
+ const config = {
32
+ language: "typescript",
33
+ checkCommand: "npm run build",
34
+ testCommand: "npm test",
35
+ cli: { command: "custom-cli" },
36
+ };
37
+ const result = getCliConfig(config);
38
+ expect(result.promptArgs).toEqual(["-p"]);
39
+ });
40
+ it("preserves args and yoloArgs from custom cli config", () => {
41
+ const config = {
42
+ language: "typescript",
43
+ checkCommand: "npm run build",
44
+ testCommand: "npm test",
45
+ cli: {
46
+ command: "aider",
47
+ args: ["--yes", "--no-auto-commits"],
48
+ yoloArgs: ["--auto-accept"],
49
+ promptArgs: ["--message"],
50
+ },
51
+ };
52
+ const result = getCliConfig(config);
53
+ expect(result.args).toEqual(["--yes", "--no-auto-commits"]);
54
+ expect(result.yoloArgs).toEqual(["--auto-accept"]);
55
+ });
56
+ it("preserves model and modelArgs from custom cli config", () => {
57
+ const config = {
58
+ language: "typescript",
59
+ checkCommand: "npm run build",
60
+ testCommand: "npm test",
61
+ cli: {
62
+ command: "claude",
63
+ model: "claude-opus-4-20250514",
64
+ modelArgs: ["--model"],
65
+ promptArgs: ["-p"],
66
+ },
67
+ };
68
+ const result = getCliConfig(config);
69
+ expect(result.model).toBe("claude-opus-4-20250514");
70
+ expect(result.modelArgs).toEqual(["--model"]);
71
+ });
72
+ it("preserves fileArgs from custom cli config", () => {
73
+ const config = {
74
+ language: "typescript",
75
+ checkCommand: "npm run build",
76
+ testCommand: "npm test",
77
+ cli: {
78
+ command: "aider",
79
+ fileArgs: ["--read"],
80
+ promptArgs: ["--message"],
81
+ },
82
+ };
83
+ const result = getCliConfig(config);
84
+ expect(result.fileArgs).toEqual(["--read"]);
85
+ });
86
+ it("uses cliProvider to fill in promptArgs when cli.promptArgs is undefined", () => {
87
+ const config = {
88
+ language: "typescript",
89
+ checkCommand: "npm run build",
90
+ testCommand: "npm test",
91
+ cliProvider: "claude",
92
+ cli: { command: "claude" },
93
+ };
94
+ const result = getCliConfig(config);
95
+ // Should pick up promptArgs from the claude provider
96
+ expect(result.promptArgs).toBeDefined();
97
+ expect(Array.isArray(result.promptArgs)).toBe(true);
98
+ });
99
+ it("does not override explicit cli.promptArgs with cliProvider", () => {
100
+ const config = {
101
+ language: "typescript",
102
+ checkCommand: "npm run build",
103
+ testCommand: "npm test",
104
+ cliProvider: "claude",
105
+ cli: {
106
+ command: "claude",
107
+ promptArgs: ["--custom-prompt"],
108
+ },
109
+ };
110
+ const result = getCliConfig(config);
111
+ expect(result.promptArgs).toEqual(["--custom-prompt"]);
112
+ });
113
+ it("falls back to ['-p'] when cliProvider is set but provider not found", () => {
114
+ const config = {
115
+ language: "typescript",
116
+ checkCommand: "npm run build",
117
+ testCommand: "npm test",
118
+ cliProvider: "nonexistent-provider",
119
+ cli: { command: "custom-cli" },
120
+ };
121
+ const result = getCliConfig(config);
122
+ expect(result.promptArgs).toEqual(["-p"]);
123
+ });
124
+ it("returns a new object, not the same reference as DEFAULT_CLI_CONFIG", () => {
125
+ const config = {
126
+ language: "typescript",
127
+ checkCommand: "npm run build",
128
+ testCommand: "npm test",
129
+ };
130
+ const result = getCliConfig(config);
131
+ expect(result).not.toBe(DEFAULT_CLI_CONFIG);
132
+ });
133
+ it("handles empty cli object", () => {
134
+ const config = {
135
+ language: "typescript",
136
+ checkCommand: "npm run build",
137
+ testCommand: "npm test",
138
+ cli: {},
139
+ };
140
+ const result = getCliConfig(config);
141
+ expect(result.promptArgs).toEqual(["-p"]);
142
+ expect(result.command).toBeUndefined();
143
+ });
144
+ });
145
+ // ─── getLLMProviders ────────────────────────────────────────────────
146
+ describe("getLLMProviders", () => {
147
+ it("returns default providers when none configured", () => {
148
+ const config = {
149
+ language: "typescript",
150
+ checkCommand: "npm run build",
151
+ testCommand: "npm test",
152
+ };
153
+ const result = getLLMProviders(config);
154
+ expect(result.anthropic).toBeDefined();
155
+ expect(result.openai).toBeDefined();
156
+ expect(result.ollama).toBeDefined();
157
+ });
158
+ it("merges custom providers with defaults", () => {
159
+ const config = {
160
+ language: "typescript",
161
+ checkCommand: "npm run build",
162
+ testCommand: "npm test",
163
+ llmProviders: {
164
+ custom: {
165
+ type: "openai",
166
+ model: "gpt-4-turbo",
167
+ apiKey: "sk-test",
168
+ },
169
+ },
170
+ };
171
+ const result = getLLMProviders(config);
172
+ expect(result.custom).toBeDefined();
173
+ expect(result.custom.model).toBe("gpt-4-turbo");
174
+ // Defaults still present
175
+ expect(result.anthropic).toBeDefined();
176
+ });
177
+ it("allows overriding default providers", () => {
178
+ const config = {
179
+ language: "typescript",
180
+ checkCommand: "npm run build",
181
+ testCommand: "npm test",
182
+ llmProviders: {
183
+ anthropic: {
184
+ type: "anthropic",
185
+ model: "claude-opus-4-20250514",
186
+ apiKey: "sk-custom",
187
+ },
188
+ },
189
+ };
190
+ const result = getLLMProviders(config);
191
+ expect(result.anthropic.model).toBe("claude-opus-4-20250514");
192
+ expect(result.anthropic.apiKey).toBe("sk-custom");
193
+ });
194
+ it("returns all three default providers with empty llmProviders", () => {
195
+ const config = {
196
+ language: "typescript",
197
+ checkCommand: "npm run build",
198
+ testCommand: "npm test",
199
+ llmProviders: {},
200
+ };
201
+ const result = getLLMProviders(config);
202
+ expect(Object.keys(result)).toContain("anthropic");
203
+ expect(Object.keys(result)).toContain("openai");
204
+ expect(Object.keys(result)).toContain("ollama");
205
+ });
206
+ it("can add multiple custom providers at once", () => {
207
+ const config = {
208
+ language: "typescript",
209
+ checkCommand: "npm run build",
210
+ testCommand: "npm test",
211
+ llmProviders: {
212
+ "my-gpt": { type: "openai", model: "gpt-4o-mini" },
213
+ "my-claude": { type: "anthropic", model: "claude-haiku-4-5-20251001" },
214
+ "local-llm": { type: "ollama", model: "codellama", baseUrl: "http://gpu-server:11434" },
215
+ },
216
+ };
217
+ const result = getLLMProviders(config);
218
+ expect(result["my-gpt"].model).toBe("gpt-4o-mini");
219
+ expect(result["my-claude"].model).toBe("claude-haiku-4-5-20251001");
220
+ expect(result["local-llm"].baseUrl).toBe("http://gpu-server:11434");
221
+ // defaults still present
222
+ expect(result.anthropic).toBeDefined();
223
+ });
224
+ it("override completely replaces a default provider (no deep merge)", () => {
225
+ const config = {
226
+ language: "typescript",
227
+ checkCommand: "npm run build",
228
+ testCommand: "npm test",
229
+ llmProviders: {
230
+ ollama: {
231
+ type: "ollama",
232
+ model: "mistral",
233
+ // no baseUrl set
234
+ },
235
+ },
236
+ };
237
+ const result = getLLMProviders(config);
238
+ // The override replaces the whole object, so baseUrl from default is gone
239
+ expect(result.ollama.model).toBe("mistral");
240
+ expect(result.ollama.baseUrl).toBeUndefined();
241
+ });
242
+ });
243
+ // ─── getLLMProviderApiKey ───────────────────────────────────────────
244
+ describe("getLLMProviderApiKey", () => {
245
+ const originalEnv = process.env;
246
+ beforeEach(() => {
247
+ process.env = { ...originalEnv };
248
+ });
249
+ afterEach(() => {
250
+ process.env = originalEnv;
251
+ });
252
+ it("returns explicit API key when set", () => {
253
+ const provider = {
254
+ type: "anthropic",
255
+ model: "claude-sonnet-4-20250514",
256
+ apiKey: "sk-explicit",
257
+ };
258
+ expect(getLLMProviderApiKey(provider)).toBe("sk-explicit");
259
+ });
260
+ it("falls back to ANTHROPIC_API_KEY env var", () => {
261
+ process.env.ANTHROPIC_API_KEY = "sk-env-anthropic";
262
+ const provider = {
263
+ type: "anthropic",
264
+ model: "claude-sonnet-4-20250514",
265
+ };
266
+ expect(getLLMProviderApiKey(provider)).toBe("sk-env-anthropic");
267
+ });
268
+ it("falls back to OPENAI_API_KEY env var", () => {
269
+ process.env.OPENAI_API_KEY = "sk-env-openai";
270
+ const provider = {
271
+ type: "openai",
272
+ model: "gpt-4o",
273
+ };
274
+ expect(getLLMProviderApiKey(provider)).toBe("sk-env-openai");
275
+ });
276
+ it("returns undefined for ollama", () => {
277
+ const provider = {
278
+ type: "ollama",
279
+ model: "llama3",
280
+ };
281
+ expect(getLLMProviderApiKey(provider)).toBeUndefined();
282
+ });
283
+ it("returns undefined for anthropic when no env var set", () => {
284
+ delete process.env.ANTHROPIC_API_KEY;
285
+ const provider = {
286
+ type: "anthropic",
287
+ model: "claude-sonnet-4-20250514",
288
+ };
289
+ expect(getLLMProviderApiKey(provider)).toBeUndefined();
290
+ });
291
+ it("returns undefined for openai when no env var set", () => {
292
+ delete process.env.OPENAI_API_KEY;
293
+ const provider = {
294
+ type: "openai",
295
+ model: "gpt-4o",
296
+ };
297
+ expect(getLLMProviderApiKey(provider)).toBeUndefined();
298
+ });
299
+ it("returns undefined for unknown provider type", () => {
300
+ const provider = {
301
+ type: "unknown-provider",
302
+ model: "some-model",
303
+ };
304
+ expect(getLLMProviderApiKey(provider)).toBeUndefined();
305
+ });
306
+ it("explicit key takes precedence over env var", () => {
307
+ process.env.ANTHROPIC_API_KEY = "sk-env";
308
+ const provider = {
309
+ type: "anthropic",
310
+ model: "claude-sonnet-4-20250514",
311
+ apiKey: "sk-explicit-wins",
312
+ };
313
+ expect(getLLMProviderApiKey(provider)).toBe("sk-explicit-wins");
314
+ });
315
+ it("returns undefined for ollama even with explicit empty string", () => {
316
+ const provider = {
317
+ type: "ollama",
318
+ model: "llama3",
319
+ apiKey: "",
320
+ };
321
+ // empty string is falsy, so falls through to switch which returns undefined
322
+ expect(getLLMProviderApiKey(provider)).toBeUndefined();
323
+ });
324
+ });
325
+ // ─── getLLMProviderBaseUrl ──────────────────────────────────────────
326
+ describe("getLLMProviderBaseUrl", () => {
327
+ it("returns explicit baseUrl when set", () => {
328
+ const provider = {
329
+ type: "anthropic",
330
+ model: "claude-sonnet-4-20250514",
331
+ baseUrl: "https://custom.api.com",
332
+ };
333
+ expect(getLLMProviderBaseUrl(provider)).toBe("https://custom.api.com");
334
+ });
335
+ it("returns default URL for anthropic", () => {
336
+ const provider = {
337
+ type: "anthropic",
338
+ model: "claude-sonnet-4-20250514",
339
+ };
340
+ expect(getLLMProviderBaseUrl(provider)).toBe("https://api.anthropic.com");
341
+ });
342
+ it("returns default URL for openai", () => {
343
+ const provider = {
344
+ type: "openai",
345
+ model: "gpt-4o",
346
+ };
347
+ expect(getLLMProviderBaseUrl(provider)).toBe("https://api.openai.com/v1");
348
+ });
349
+ it("returns default URL for ollama", () => {
350
+ const provider = {
351
+ type: "ollama",
352
+ model: "llama3",
353
+ };
354
+ expect(getLLMProviderBaseUrl(provider)).toBe("http://localhost:11434");
355
+ });
356
+ it("returns empty string for unknown provider type", () => {
357
+ const provider = {
358
+ type: "unknown",
359
+ model: "some-model",
360
+ };
361
+ expect(getLLMProviderBaseUrl(provider)).toBe("");
362
+ });
363
+ it("explicit baseUrl overrides default for openai", () => {
364
+ const provider = {
365
+ type: "openai",
366
+ model: "gpt-4o",
367
+ baseUrl: "https://my-proxy.example.com/v1",
368
+ };
369
+ expect(getLLMProviderBaseUrl(provider)).toBe("https://my-proxy.example.com/v1");
370
+ });
371
+ it("explicit baseUrl overrides default for ollama", () => {
372
+ const provider = {
373
+ type: "ollama",
374
+ model: "llama3",
375
+ baseUrl: "http://remote-gpu:11434",
376
+ };
377
+ expect(getLLMProviderBaseUrl(provider)).toBe("http://remote-gpu:11434");
378
+ });
379
+ });
380
+ // ─── DEFAULT_LLM_PROVIDERS ─────────────────────────────────────────
381
+ describe("DEFAULT_LLM_PROVIDERS", () => {
382
+ it("has anthropic, openai, and ollama providers", () => {
383
+ expect(DEFAULT_LLM_PROVIDERS.anthropic.type).toBe("anthropic");
384
+ expect(DEFAULT_LLM_PROVIDERS.openai.type).toBe("openai");
385
+ expect(DEFAULT_LLM_PROVIDERS.ollama.type).toBe("ollama");
386
+ });
387
+ it("ollama has localhost baseUrl", () => {
388
+ expect(DEFAULT_LLM_PROVIDERS.ollama.baseUrl).toBe("http://localhost:11434");
389
+ });
390
+ it("anthropic has a valid model name", () => {
391
+ expect(DEFAULT_LLM_PROVIDERS.anthropic.model).toMatch(/^claude-/);
392
+ });
393
+ it("openai has a valid model name", () => {
394
+ expect(DEFAULT_LLM_PROVIDERS.openai.model).toMatch(/^gpt-/);
395
+ });
396
+ it("no default providers have explicit API keys", () => {
397
+ expect(DEFAULT_LLM_PROVIDERS.anthropic.apiKey).toBeUndefined();
398
+ expect(DEFAULT_LLM_PROVIDERS.openai.apiKey).toBeUndefined();
399
+ expect(DEFAULT_LLM_PROVIDERS.ollama.apiKey).toBeUndefined();
400
+ });
401
+ it("anthropic and openai have no explicit baseUrl (use runtime defaults)", () => {
402
+ expect(DEFAULT_LLM_PROVIDERS.anthropic.baseUrl).toBeUndefined();
403
+ expect(DEFAULT_LLM_PROVIDERS.openai.baseUrl).toBeUndefined();
404
+ });
405
+ it("has exactly three default providers", () => {
406
+ expect(Object.keys(DEFAULT_LLM_PROVIDERS)).toHaveLength(3);
407
+ });
408
+ });
409
+ // ─── DEFAULT_CLI_CONFIG ─────────────────────────────────────────────
410
+ describe("DEFAULT_CLI_CONFIG", () => {
411
+ it("uses claude as default command", () => {
412
+ expect(DEFAULT_CLI_CONFIG.command).toBe("claude");
413
+ expect(DEFAULT_CLI_CONFIG.promptArgs).toEqual(["-p"]);
414
+ });
415
+ it("has empty args array", () => {
416
+ expect(DEFAULT_CLI_CONFIG.args).toEqual([]);
417
+ });
418
+ it("has no yoloArgs by default", () => {
419
+ expect(DEFAULT_CLI_CONFIG.yoloArgs).toBeUndefined();
420
+ });
421
+ it("has no model by default", () => {
422
+ expect(DEFAULT_CLI_CONFIG.model).toBeUndefined();
423
+ });
424
+ });
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "child_process";
2
- import { isRunningInContainer, } from "./config.js";
2
+ import { isRunningInContainer } from "./config.js";
3
3
  import { isDaemonAvailable, sendDaemonNotification, sendDaemonRequest, sendSlackNotification, sendTelegramNotification, sendDiscordNotification, } from "./daemon-client.js";
4
4
  /**
5
5
  * Send a notification using the configured notify command.
@@ -435,7 +435,7 @@ export function createTemplatePrd(backupPath) {
435
435
  */
436
436
  function hasProblematicEmbeddedQuotes(value) {
437
437
  // Look for "...special chars..." followed by more text
438
- const regex = /"[^"]*[:{}\[\]][^"]*"/g;
438
+ const regex = /"[^"]*[:{}[\]][^"]*"/g;
439
439
  let match;
440
440
  while ((match = regex.exec(value)) !== null) {
441
441
  const afterQuote = value.substring(match.index + match[0].length);
@@ -468,7 +468,10 @@ function fixYamlEmbeddedQuotes(yaml) {
468
468
  const prefix = match[1];
469
469
  const value = match[2];
470
470
  // Skip if already quoted
471
- if (value.startsWith('"') || value.startsWith("'") || value.startsWith("|") || value.startsWith(">")) {
471
+ if (value.startsWith('"') ||
472
+ value.startsWith("'") ||
473
+ value.startsWith("|") ||
474
+ value.startsWith(">")) {
472
475
  result.push(line);
473
476
  continue;
474
477
  }
@@ -568,7 +571,16 @@ export function robustYamlParse(content) {
568
571
  * If parsed content is an object with one of these keys containing an array,
569
572
  * we unwrap it automatically.
570
573
  */
571
- const PRD_WRAPPER_KEYS = ["features", "items", "entries", "prd", "tasks", "requirements", "todo", "checklist"];
574
+ const PRD_WRAPPER_KEYS = [
575
+ "features",
576
+ "items",
577
+ "entries",
578
+ "prd",
579
+ "tasks",
580
+ "requirements",
581
+ "todo",
582
+ "checklist",
583
+ ];
572
584
  /**
573
585
  * Unwraps PRD content if it's wrapped in a common object structure.
574
586
  * LLMs often generate structures like:
@@ -682,7 +694,7 @@ export function writePrdAuto(prdPath, entries) {
682
694
  export function expandFileReferences(text, baseDir) {
683
695
  // Handle null/undefined text
684
696
  if (typeof text !== "string") {
685
- return text ?? "";
697
+ return String(text ?? "");
686
698
  }
687
699
  // Match @{filepath} patterns
688
700
  const pattern = /@\{([^}]+)\}/g;
@@ -0,0 +1 @@
1
+ export {};