builder-assistant-engineer 0.2.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 (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +250 -0
  3. package/dist/analyst/prompt.js +19 -0
  4. package/dist/analyst/retry.js +38 -0
  5. package/dist/artifacts/diff.js +47 -0
  6. package/dist/artifacts/lessons.js +32 -0
  7. package/dist/artifacts/merge.js +70 -0
  8. package/dist/artifacts/write.js +44 -0
  9. package/dist/backends/agent-cli.js +174 -0
  10. package/dist/backends/api.js +171 -0
  11. package/dist/backends/index.js +10 -0
  12. package/dist/backends/manual.js +52 -0
  13. package/dist/backends/sse.js +24 -0
  14. package/dist/backends/types.js +1 -0
  15. package/dist/bin.js +3 -0
  16. package/dist/cli.js +104 -0
  17. package/dist/commands/context.js +12 -0
  18. package/dist/commands/init.js +199 -0
  19. package/dist/commands/next.js +103 -0
  20. package/dist/commands/plan.js +71 -0
  21. package/dist/commands/replan.js +37 -0
  22. package/dist/commands/review.js +37 -0
  23. package/dist/commands/shared.js +40 -0
  24. package/dist/commands/status.js +95 -0
  25. package/dist/config/commands.js +85 -0
  26. package/dist/config/schema.js +43 -0
  27. package/dist/config/settings.js +8 -0
  28. package/dist/config/store.js +38 -0
  29. package/dist/core/bash.js +105 -0
  30. package/dist/core/errors.js +14 -0
  31. package/dist/core/fs.js +35 -0
  32. package/dist/core/git.js +104 -0
  33. package/dist/core/gitignore.js +14 -0
  34. package/dist/core/json.js +13 -0
  35. package/dist/core/paths.js +14 -0
  36. package/dist/core/process.js +56 -0
  37. package/dist/core/prompt-loader.js +27 -0
  38. package/dist/core/state.js +27 -0
  39. package/dist/core/which.js +24 -0
  40. package/dist/detect/agents.js +17 -0
  41. package/dist/detect/mode.js +11 -0
  42. package/dist/digest/baseline.js +192 -0
  43. package/dist/digest/budget.js +31 -0
  44. package/dist/digest/files.js +167 -0
  45. package/dist/digest/format.js +18 -0
  46. package/dist/digest/git.js +19 -0
  47. package/dist/digest/index.js +100 -0
  48. package/dist/digest/manifests.js +133 -0
  49. package/dist/digest/read.js +28 -0
  50. package/dist/digest/redact.js +37 -0
  51. package/dist/digest/render.js +138 -0
  52. package/dist/digest/select.js +116 -0
  53. package/dist/digest/todos.js +16 -0
  54. package/dist/digest/tree.js +55 -0
  55. package/dist/digest/walk.js +72 -0
  56. package/dist/gates/capture.js +145 -0
  57. package/dist/gates/contract.js +339 -0
  58. package/dist/gates/enforce.js +143 -0
  59. package/dist/gates/findings.js +17 -0
  60. package/dist/gates/gate.js +315 -0
  61. package/dist/gates/ignore-rules.js +104 -0
  62. package/dist/gates/protected-files.js +311 -0
  63. package/dist/gates/regression.js +219 -0
  64. package/dist/gates/reports.js +349 -0
  65. package/dist/gates/results.js +295 -0
  66. package/dist/gates/runners.js +189 -0
  67. package/dist/gates/state-guard.js +56 -0
  68. package/dist/i18n/en.js +288 -0
  69. package/dist/i18n/es.js +288 -0
  70. package/dist/i18n/index.js +18 -0
  71. package/dist/interview/adaptive.js +69 -0
  72. package/dist/interview/brief.js +27 -0
  73. package/dist/interview/questions.js +18 -0
  74. package/dist/interview/render.js +19 -0
  75. package/dist/interview/reply.js +34 -0
  76. package/dist/next/attempts.js +151 -0
  77. package/dist/next/start.js +179 -0
  78. package/dist/plan/continuation.js +40 -0
  79. package/dist/plan/evidence.js +239 -0
  80. package/dist/plan/filter.js +20 -0
  81. package/dist/plan/parser.js +283 -0
  82. package/dist/plan/prior.js +32 -0
  83. package/dist/plan/questions.js +48 -0
  84. package/dist/plan/repair.js +33 -0
  85. package/dist/plan/report.js +37 -0
  86. package/dist/plan/run.js +191 -0
  87. package/dist/prompts/analyst.md +101 -0
  88. package/dist/prompts/continue.md +11 -0
  89. package/dist/prompts/fix-format.md +13 -0
  90. package/dist/prompts/fix-paths.md +23 -0
  91. package/dist/prompts/lesson.md +18 -0
  92. package/dist/prompts/retry.md +9 -0
  93. package/dist/prompts/review.md +36 -0
  94. package/dist/prompts/task.md +9 -0
  95. package/dist/review/changes.js +177 -0
  96. package/dist/review/diff.js +115 -0
  97. package/dist/review/integrity.js +150 -0
  98. package/dist/review/mechanical.js +62 -0
  99. package/dist/review/parse.js +40 -0
  100. package/dist/review/reviewer.js +23 -0
  101. package/dist/review/run.js +134 -0
  102. package/dist/review/scope.js +55 -0
  103. package/dist/review/secrets.js +163 -0
  104. package/dist/review/snapshot.js +38 -0
  105. package/dist/review/tests.js +140 -0
  106. package/dist/tasks/attempts.js +71 -0
  107. package/dist/tasks/checks.js +181 -0
  108. package/dist/tasks/frontmatter.js +58 -0
  109. package/dist/tasks/graph.js +27 -0
  110. package/dist/tasks/handoff.js +62 -0
  111. package/dist/tasks/learn.js +128 -0
  112. package/dist/tasks/load.js +37 -0
  113. package/dist/tasks/metrics.js +40 -0
  114. package/dist/tasks/runs.js +12 -0
  115. package/dist/tasks/schema.js +96 -0
  116. package/dist/tasks/select.js +15 -0
  117. package/dist/tasks/shell-words.js +176 -0
  118. package/dist/tasks/status.js +13 -0
  119. package/dist/tasks/verify.js +181 -0
  120. package/dist/ui/clack.js +46 -0
  121. package/dist/ui/prompter.js +1 -0
  122. package/package.json +64 -0
@@ -0,0 +1,174 @@
1
+ import { mkdir, rm } from "node:fs/promises";
2
+ import { join, relative } from "node:path";
3
+ import { stripVTControlCharacters } from "node:util";
4
+ import { UserError } from "../core/errors.js";
5
+ import { readTextIfExists, writeText } from "../core/fs.js";
6
+ import { asRecord, parseObject } from "../core/json.js";
7
+ import { baePaths } from "../core/paths.js";
8
+ import { runCommand, runInteractive } from "../core/process.js";
9
+ import { t } from "../i18n/index.js";
10
+ const STDIN_INSTRUCTION = "Follow the instructions above exactly.";
11
+ const ERROR_TAIL = 2_000;
12
+ export const AGENT_SPECS = {
13
+ claude: {
14
+ name: "claude",
15
+ command: "claude",
16
+ headless: (access) => [
17
+ "-p",
18
+ "--output-format",
19
+ "stream-json",
20
+ "--verbose",
21
+ "--no-session-persistence",
22
+ "--permission-prompts",
23
+ "none",
24
+ ...(access === "read"
25
+ ? ["--tools", "Read,Grep,Glob", "--setting-sources", "user"]
26
+ : ["--permission-mode", "acceptEdits", "--setting-sources", "user,project"]),
27
+ ],
28
+ interactive: (instruction) => [instruction],
29
+ jsonOutput: true,
30
+ parse: parseClaudeJson,
31
+ },
32
+ opencode: {
33
+ name: "opencode",
34
+ command: "opencode",
35
+ headless: (access) => ["run", ...(access === "read" ? ["--agent", "plan"] : [])],
36
+ interactive: (instruction) => ["--prompt", instruction],
37
+ parse: (result) => ({
38
+ text: result.stdout,
39
+ info: { model: stderrModel(result, /^> \S+ · (.+)$/m) },
40
+ }),
41
+ },
42
+ codex: {
43
+ name: "codex",
44
+ command: "codex",
45
+ headless: (access, outputFile) => [
46
+ "exec",
47
+ "--color",
48
+ "never",
49
+ "--sandbox",
50
+ access === "read" ? "read-only" : "workspace-write",
51
+ ...(access === "read" ? ["--skip-git-repo-check"] : []),
52
+ "--output-last-message",
53
+ outputFile,
54
+ "-",
55
+ ],
56
+ interactive: (instruction) => [instruction],
57
+ readsOutputFile: true,
58
+ parse: (result) => ({
59
+ text: result.stdout,
60
+ info: { model: stderrModel(result, /^model:\s*(.+)$/m) },
61
+ }),
62
+ },
63
+ gemini: {
64
+ name: "gemini",
65
+ command: "gemini",
66
+ headless: (access) => [
67
+ "--output-format",
68
+ "text",
69
+ "--approval-mode",
70
+ access === "read" ? "plan" : "auto_edit",
71
+ "--prompt",
72
+ STDIN_INSTRUCTION,
73
+ ],
74
+ interactive: (instruction) => ["--prompt-interactive", instruction],
75
+ },
76
+ };
77
+ export const defaultRunner = { run: runCommand, interactive: runInteractive };
78
+ export function createAgentBackend(spec, runner = defaultRunner) {
79
+ return {
80
+ name: spec.name,
81
+ run: (prompt, options) => options.interactive
82
+ ? runSession(spec, runner, prompt, options)
83
+ : runHeadless(spec, runner, prompt, options),
84
+ };
85
+ }
86
+ async function runHeadless(spec, runner, prompt, options) {
87
+ const tmp = baePaths(options.cwd).tmp;
88
+ const outputFile = join(tmp, `${spec.name}-output.md`);
89
+ if (spec.readsOutputFile) {
90
+ await mkdir(tmp, { recursive: true });
91
+ await rm(outputFile, { force: true });
92
+ }
93
+ const args = spec.headless(options.access ?? "read", outputFile);
94
+ const result = await runner.run(spec.command, args, {
95
+ cwd: options.cwd,
96
+ input: prompt,
97
+ timeoutMs: options.timeoutMs,
98
+ onStdout: spec.jsonOutput ? undefined : options.stream,
99
+ });
100
+ assertSucceeded(spec, result, options.timeoutMs);
101
+ const parsed = spec.parse?.(result) ?? { text: result.stdout, info: {} };
102
+ options.onInfo?.(parsed.info);
103
+ if (spec.jsonOutput)
104
+ options.stream?.(parsed.text);
105
+ if (!spec.readsOutputFile)
106
+ return parsed.text;
107
+ return (await readTextIfExists(outputFile)) ?? parsed.text;
108
+ }
109
+ function parseClaudeJson(result) {
110
+ const events = result.stdout.split(/\r?\n/).flatMap((line) => {
111
+ const event = line.trim().startsWith("{") ? parseObject(line.trim()) : undefined;
112
+ return event ? [event] : [];
113
+ });
114
+ const data = events.filter((event) => event.type === "result").at(-1);
115
+ if (!data || typeof data.result !== "string")
116
+ return { text: result.stdout, info: {} };
117
+ const models = Object.keys(asRecord(data.modelUsage));
118
+ const cost = typeof data.total_cost_usd === "number" ? data.total_cost_usd : undefined;
119
+ const truncated = typeof data.stop_reason === "string" ? data.stop_reason === "max_tokens" : undefined;
120
+ const answer = finalAnswer(events);
121
+ return {
122
+ text: answer || data.result,
123
+ info: { model: models.join(", ") || undefined, costUsd: cost, truncated },
124
+ };
125
+ }
126
+ function finalAnswer(events) {
127
+ let tail = [];
128
+ const seen = new Set();
129
+ for (const event of events) {
130
+ if (event.type !== "assistant")
131
+ continue;
132
+ const message = asRecord(event.message);
133
+ const blocks = Array.isArray(message.content) ? message.content : [];
134
+ for (const block of blocks.map(asRecord)) {
135
+ if (block.type === "tool_use")
136
+ tail = [];
137
+ if (block.type !== "text" || typeof block.text !== "string")
138
+ continue;
139
+ const key = `${String(message.id ?? "")}:${block.text}`;
140
+ if (seen.has(key))
141
+ continue;
142
+ seen.add(key);
143
+ tail.push(block.text);
144
+ }
145
+ }
146
+ return tail.join("");
147
+ }
148
+ function stderrModel(result, pattern) {
149
+ return pattern.exec(stripVTControlCharacters(result.stderr))?.[1]?.trim() || undefined;
150
+ }
151
+ async function runSession(spec, runner, prompt, options) {
152
+ const promptFile = join(baePaths(options.cwd).tmp, "prompt.md");
153
+ await writeText(promptFile, prompt);
154
+ const path = relative(options.cwd, promptFile).split("\\").join("/");
155
+ const instruction = `Read ${path} and carry out the task it describes. It is your complete task prompt.`;
156
+ const result = await runner.interactive(spec.command, spec.interactive(instruction), {
157
+ cwd: options.cwd,
158
+ });
159
+ if (result.notFound)
160
+ throw new UserError(t("backend.notInstalled", { command: spec.command }));
161
+ return "";
162
+ }
163
+ function assertSucceeded(spec, result, timeoutMs) {
164
+ if (result.notFound)
165
+ throw new UserError(t("backend.notInstalled", { command: spec.command }));
166
+ if (result.timedOut) {
167
+ const minutes = Math.round((timeoutMs ?? 0) / 60_000);
168
+ throw new UserError(t("backend.timedOut", { command: spec.command, minutes }));
169
+ }
170
+ if (result.exitCode === 0)
171
+ return;
172
+ const details = (result.stderr.trim() || result.stdout.trim()).slice(-ERROR_TAIL);
173
+ throw new UserError(t("backend.failed", { command: spec.command, code: result.exitCode, details }));
174
+ }
@@ -0,0 +1,171 @@
1
+ import { UserError } from "../core/errors.js";
2
+ import { asRecord, parseObject } from "../core/json.js";
3
+ import { t } from "../i18n/index.js";
4
+ import { readSse } from "./sse.js";
5
+ export const DEFAULT_ANTHROPIC_MODEL = "claude-opus-5";
6
+ export const ANTHROPIC_MAX_TOKENS = 64_000;
7
+ const ANTHROPIC_VERSION = "2023-06-01";
8
+ const FALLBACK_BETA = "server-side-fallback-2026-07-01";
9
+ const RETRYABLE = new Set([408, 429, 500, 502, 503, 504, 529]);
10
+ const MAX_RETRIES = 2;
11
+ const MAX_RETRY_DELAY_MS = 30_000;
12
+ const ERROR_TAIL = 1_000;
13
+ const defaultDeps = {
14
+ env: process.env,
15
+ fetch: (...args) => fetch(...args),
16
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
17
+ };
18
+ export function createApiBackend(overrides = {}) {
19
+ const deps = { ...defaultDeps, ...overrides };
20
+ return {
21
+ name: "api",
22
+ run: async (prompt, options) => {
23
+ if (options.interactive)
24
+ throw new UserError(t("backend.apiInteractive"));
25
+ const target = resolveApiTarget(deps.env);
26
+ options.onInfo?.({ model: target.model });
27
+ return target.provider === "anthropic"
28
+ ? callAnthropic(target, prompt, options, deps)
29
+ : callOpenAi(target, prompt, options, deps);
30
+ },
31
+ };
32
+ }
33
+ export function resolveApiTarget(env) {
34
+ const provider = env.BAE_API_PROVIDER ?? inferProvider(env);
35
+ if (provider === "anthropic") {
36
+ return {
37
+ provider,
38
+ baseUrl: trimSlash(env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com"),
39
+ apiKey: env.ANTHROPIC_API_KEY,
40
+ model: env.BAE_MODEL ?? DEFAULT_ANTHROPIC_MODEL,
41
+ };
42
+ }
43
+ if (provider === "openai") {
44
+ const baseUrl = trimSlash(env.OPENAI_BASE_URL ?? "https://api.openai.com/v1");
45
+ if (!env.BAE_MODEL)
46
+ throw new UserError(t("backend.apiModelRequired", { baseUrl }));
47
+ return { provider, baseUrl, apiKey: env.OPENAI_API_KEY, model: env.BAE_MODEL };
48
+ }
49
+ if (provider === undefined)
50
+ throw new UserError(t("backend.apiNotConfigured"));
51
+ throw new UserError(t("backend.apiProviderInvalid", { provider }));
52
+ }
53
+ export function hasApiCredentials(env) {
54
+ return inferProvider(env) !== undefined;
55
+ }
56
+ function inferProvider(env) {
57
+ if (env.ANTHROPIC_API_KEY)
58
+ return "anthropic";
59
+ if (env.OPENAI_API_KEY || env.OPENAI_BASE_URL)
60
+ return "openai";
61
+ return undefined;
62
+ }
63
+ async function callAnthropic(target, prompt, options, deps) {
64
+ const useFallbacks = target.model === DEFAULT_ANTHROPIC_MODEL;
65
+ const headers = {
66
+ "content-type": "application/json",
67
+ "x-api-key": target.apiKey ?? "",
68
+ "anthropic-version": ANTHROPIC_VERSION,
69
+ ...(useFallbacks ? { "anthropic-beta": FALLBACK_BETA } : {}),
70
+ };
71
+ const body = {
72
+ model: target.model,
73
+ max_tokens: ANTHROPIC_MAX_TOKENS,
74
+ stream: true,
75
+ messages: [{ role: "user", content: prompt }],
76
+ ...(useFallbacks ? { fallbacks: "default" } : {}),
77
+ };
78
+ const response = await post(`${target.baseUrl}/v1/messages`, headers, body, "Anthropic", deps);
79
+ let text = "";
80
+ let stopReason;
81
+ for await (const { event, data } of readSse(bodyOf(response))) {
82
+ const payload = parseObject(data) ?? {};
83
+ if (event === "error")
84
+ throw new UserError(apiError("Anthropic", payload));
85
+ const delta = asRecord(payload.delta);
86
+ if (payload.type === "content_block_delta" && delta.type === "text_delta") {
87
+ const chunk = String(delta.text ?? "");
88
+ text += chunk;
89
+ options.stream?.(chunk);
90
+ }
91
+ if (payload.type === "message_delta")
92
+ stopReason = delta.stop_reason;
93
+ }
94
+ if (stopReason === "refusal")
95
+ throw new UserError(t("backend.refusal"));
96
+ if (stopReason === "max_tokens")
97
+ throw new UserError(t("backend.truncated"));
98
+ options.onInfo?.({ truncated: false });
99
+ return text;
100
+ }
101
+ async function callOpenAi(target, prompt, options, deps) {
102
+ const headers = {
103
+ "content-type": "application/json",
104
+ ...(target.apiKey ? { authorization: `Bearer ${target.apiKey}` } : {}),
105
+ };
106
+ const body = {
107
+ model: target.model,
108
+ stream: true,
109
+ messages: [{ role: "user", content: prompt }],
110
+ };
111
+ const response = await post(`${target.baseUrl}/chat/completions`, headers, body, "OpenAI", deps);
112
+ let text = "";
113
+ let finishReason;
114
+ for await (const { data } of readSse(bodyOf(response))) {
115
+ if (data.trim() === "[DONE]")
116
+ break;
117
+ const payload = parseObject(data) ?? {};
118
+ if (payload.error)
119
+ throw new UserError(apiError("OpenAI", payload));
120
+ const choice = asRecord(Array.isArray(payload.choices) ? payload.choices[0] : undefined);
121
+ const content = asRecord(choice.delta).content;
122
+ if (typeof content === "string") {
123
+ text += content;
124
+ options.stream?.(content);
125
+ }
126
+ finishReason = choice.finish_reason ?? finishReason;
127
+ }
128
+ if (finishReason === "length")
129
+ throw new UserError(t("backend.truncated"));
130
+ options.onInfo?.({ truncated: false });
131
+ return text;
132
+ }
133
+ async function post(url, headers, body, provider, deps) {
134
+ for (let attempt = 0;; attempt++) {
135
+ const response = await send(url, headers, body, deps);
136
+ if (response.ok)
137
+ return response;
138
+ if (!RETRYABLE.has(response.status) || attempt >= MAX_RETRIES) {
139
+ const details = (await response.text()).slice(0, ERROR_TAIL);
140
+ throw new UserError(t("backend.apiHttp", { provider, status: response.status, details }));
141
+ }
142
+ await deps.sleep(retryDelay(response, attempt));
143
+ }
144
+ }
145
+ async function send(url, headers, body, deps) {
146
+ try {
147
+ return await deps.fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
148
+ }
149
+ catch (error) {
150
+ const details = error instanceof Error ? error.message : String(error);
151
+ throw new UserError(t("backend.apiNetwork", { url, details }));
152
+ }
153
+ }
154
+ function retryDelay(response, attempt) {
155
+ const header = Number(response.headers.get("retry-after"));
156
+ const seconds = Number.isFinite(header) && header > 0 ? header : 2 ** (attempt + 1);
157
+ return Math.min(seconds * 1_000, MAX_RETRY_DELAY_MS);
158
+ }
159
+ function bodyOf(response) {
160
+ if (!response.body)
161
+ return (async function* () { })();
162
+ return response.body;
163
+ }
164
+ function apiError(provider, payload) {
165
+ const error = asRecord(payload.error);
166
+ const details = String(error.message ?? JSON.stringify(payload));
167
+ return t("backend.apiHttp", { provider, status: String(error.type ?? "stream"), details });
168
+ }
169
+ function trimSlash(url) {
170
+ return url.replace(/\/+$/, "");
171
+ }
@@ -0,0 +1,10 @@
1
+ import { AGENT_SPECS, createAgentBackend, defaultRunner } from "./agent-cli.js";
2
+ import { createApiBackend } from "./api.js";
3
+ import { createManualBackend } from "./manual.js";
4
+ export function createBackend(name, deps = {}) {
5
+ if (name === "api")
6
+ return createApiBackend(deps.api);
7
+ if (name === "manual")
8
+ return createManualBackend(deps.manual);
9
+ return createAgentBackend(AGENT_SPECS[name], deps.runner ?? defaultRunner);
10
+ }
@@ -0,0 +1,52 @@
1
+ import { rm } from "node:fs/promises";
2
+ import { join, relative } from "node:path";
3
+ import { createInterface } from "node:readline";
4
+ import clipboard from "clipboardy";
5
+ import { UserError } from "../core/errors.js";
6
+ import { readTextIfExists, writeText } from "../core/fs.js";
7
+ import { baePaths } from "../core/paths.js";
8
+ import { t } from "../i18n/index.js";
9
+ const defaultIo = {
10
+ input: process.stdin,
11
+ output: process.stdout,
12
+ copy: (text) => clipboard.write(text),
13
+ };
14
+ export function createManualBackend(overrides = {}) {
15
+ const io = { ...defaultIo, ...overrides };
16
+ return {
17
+ name: "manual",
18
+ run: async (prompt, options) => {
19
+ const tmp = baePaths(options.cwd).tmp;
20
+ const promptFile = join(tmp, "prompt.md");
21
+ const responseFile = join(tmp, "response.md");
22
+ await writeText(promptFile, prompt);
23
+ await rm(responseFile, { force: true });
24
+ const copied = await io.copy(prompt).then(() => true, () => false);
25
+ const show = (path) => relative(options.cwd, path).split("\\").join("/");
26
+ io.output.write(`${prompt}\n\n`);
27
+ io.output.write(`${t(copied ? "manual.copied" : "manual.notCopied", { path: show(promptFile) })}\n`);
28
+ if (options.interactive) {
29
+ io.output.write(`${t("manual.awaitingDone")}\n`);
30
+ await readPasted(io.input);
31
+ return "";
32
+ }
33
+ io.output.write(`${t("manual.awaitingResponse", { path: show(responseFile) })}\n`);
34
+ const pasted = await readPasted(io.input);
35
+ const answer = pasted || ((await readTextIfExists(responseFile)) ?? "");
36
+ if (!answer.trim())
37
+ throw new UserError(t("manual.emptyResponse", { path: show(responseFile) }));
38
+ return answer;
39
+ },
40
+ };
41
+ }
42
+ async function readPasted(input) {
43
+ const reader = createInterface({ input, terminal: false });
44
+ const lines = [];
45
+ for await (const line of reader) {
46
+ if (lines.length === 0 && line.trim() === "")
47
+ break;
48
+ lines.push(line);
49
+ }
50
+ reader.close();
51
+ return lines.join("\n");
52
+ }
@@ -0,0 +1,24 @@
1
+ export async function* readSse(body) {
2
+ const decoder = new TextDecoder();
3
+ let buffer = "";
4
+ for await (const chunk of body) {
5
+ buffer += decoder.decode(chunk, { stream: true }).replace(/\r\n?/g, "\n");
6
+ const parts = buffer.split("\n\n");
7
+ buffer = parts.pop() ?? "";
8
+ yield* parts.map(parseEvent).filter((event) => event !== undefined);
9
+ }
10
+ const last = parseEvent(buffer + decoder.decode());
11
+ if (last)
12
+ yield last;
13
+ }
14
+ function parseEvent(block) {
15
+ let event = "message";
16
+ const data = [];
17
+ for (const line of block.split("\n")) {
18
+ if (line.startsWith("event:"))
19
+ event = line.slice(6).trim();
20
+ else if (line.startsWith("data:"))
21
+ data.push(line.slice(5).replace(/^ /, ""));
22
+ }
23
+ return data.length > 0 ? { event, data: data.join("\n") } : undefined;
24
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "./cli.js";
3
+ process.exitCode = await main(process.argv, process.cwd());
package/dist/cli.js ADDED
@@ -0,0 +1,104 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { Command, CommanderError, Option } from "commander";
3
+ import pc from "picocolors";
4
+ import { defaultDeps } from "./commands/context.js";
5
+ import { runInit } from "./commands/init.js";
6
+ import { runNext } from "./commands/next.js";
7
+ import { runPlan } from "./commands/plan.js";
8
+ import { runReplan } from "./commands/replan.js";
9
+ import { runReview } from "./commands/review.js";
10
+ import { runStatus } from "./commands/status.js";
11
+ import { BACKENDS } from "./config/schema.js";
12
+ import { resolveSettings } from "./config/settings.js";
13
+ import { readConfig } from "./config/store.js";
14
+ import { ExitCode, UserError } from "./core/errors.js";
15
+ import { isLang, LANGS, setLang, t } from "./i18n/index.js";
16
+ import { ONLY_GROUPS } from "./plan/filter.js";
17
+ export async function main(argv, cwd, deps = {}) {
18
+ const config = await readConfig(cwd).catch(() => undefined);
19
+ setLang(resolveSettings({ lang: scanLang(argv) }, config).lang);
20
+ try {
21
+ await buildProgram({ ...defaultDeps(), ...deps }, cwd).parseAsync(argv);
22
+ return 0;
23
+ }
24
+ catch (error) {
25
+ return report(error);
26
+ }
27
+ }
28
+ export function buildProgram(deps, cwd) {
29
+ const context = (command) => ({
30
+ ...deps,
31
+ cwd,
32
+ flags: command.optsWithGlobals(),
33
+ });
34
+ const program = new Command("builder-assistant-engineer")
35
+ .description(t("program.description"))
36
+ .exitOverride()
37
+ .version(readVersion(), "-v, --version", t("option.version"))
38
+ .helpOption("-h, --help", t("option.help"))
39
+ .helpCommand("help [command]", t("command.help"))
40
+ .addOption(new Option("--backend <name>", t("option.backend")).choices(BACKENDS))
41
+ .addOption(new Option("--lang <code>", t("option.lang")).choices(LANGS))
42
+ .option("--dry-run", t("option.dryRun"))
43
+ .option("-y, --yes", t("option.yes"));
44
+ program
45
+ .command("init")
46
+ .description(t("command.init"))
47
+ .option("--brief <paths>", t("option.brief"))
48
+ .action((options, command) => runInit(context(command), options));
49
+ program
50
+ .command("plan")
51
+ .description(t("command.plan"))
52
+ .addOption(new Option("--only <group>", t("option.only")).choices(ONLY_GROUPS))
53
+ .action((options, command) => runPlan(context(command), options));
54
+ program
55
+ .command("next")
56
+ .description(t("command.next"))
57
+ .option("--headless", t("option.headless"))
58
+ .option("--accept-finding <id>", t("option.acceptFinding"), collect, [])
59
+ .option("--allow-skip", t("option.allowSkip"))
60
+ .action((options, command) => runNext(context(command), options));
61
+ program
62
+ .command("status")
63
+ .description(t("command.status"))
64
+ .action((_options, command) => runStatus(context(command)));
65
+ program
66
+ .command("replan")
67
+ .description(t("command.replan"))
68
+ .action((_options, command) => runReplan(context(command)));
69
+ program
70
+ .command("review")
71
+ .description(t("command.review"))
72
+ .argument("[task]", t("argument.task"))
73
+ .option("--accept-finding <id>", t("option.acceptFinding"), collect, [])
74
+ .action((task, options, command) => runReview(context(command), task, options));
75
+ return program;
76
+ }
77
+ function collect(value, previous) {
78
+ return [...previous, value];
79
+ }
80
+ export function scanLang(argv) {
81
+ const index = argv.findIndex((arg) => arg === "--lang" || arg.startsWith("--lang="));
82
+ if (index === -1)
83
+ return undefined;
84
+ const arg = argv[index] ?? "";
85
+ const value = arg.includes("=") ? arg.slice(arg.indexOf("=") + 1) : argv[index + 1];
86
+ return isLang(value) ? value : undefined;
87
+ }
88
+ function report(error) {
89
+ if (error instanceof CommanderError)
90
+ return error.exitCode;
91
+ if (error instanceof ExitCode)
92
+ return error.code;
93
+ if (error instanceof UserError) {
94
+ process.stderr.write(`${pc.red(error.message)}\n`);
95
+ return 1;
96
+ }
97
+ process.stderr.write(`${pc.red(t("error.unexpected"))}\n`);
98
+ process.stderr.write(`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
99
+ return 1;
100
+ }
101
+ function readVersion() {
102
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
103
+ return pkg.version;
104
+ }
@@ -0,0 +1,12 @@
1
+ import { createBackend } from "../backends/index.js";
2
+ import { createClackPrompter } from "../ui/clack.js";
3
+ export function defaultDeps() {
4
+ return {
5
+ prompter: createClackPrompter(),
6
+ createBackend: (name) => createBackend(name),
7
+ env: process.env,
8
+ print: (text) => {
9
+ process.stdout.write(text);
10
+ },
11
+ };
12
+ }