baro-ai 0.1.0 → 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.
@@ -0,0 +1,61 @@
1
+ // src/core/cli-task.ts
2
+ import { spawn } from "child_process";
3
+ var CliTask = class {
4
+ id;
5
+ opts;
6
+ constructor(opts) {
7
+ this.id = opts.id;
8
+ this.opts = opts;
9
+ }
10
+ execute() {
11
+ return new Promise((resolve, reject) => {
12
+ const start = Date.now();
13
+ let stdout = "";
14
+ let stderr = "";
15
+ const proc = spawn(this.opts.command, this.opts.args, {
16
+ cwd: this.opts.cwd,
17
+ stdio: ["ignore", "pipe", "pipe"],
18
+ env: { ...process.env }
19
+ });
20
+ proc.stdout.on("data", (chunk) => {
21
+ const text = chunk.toString();
22
+ stdout += text;
23
+ if (this.opts.onStdout) {
24
+ for (const line of text.split("\n").filter(Boolean)) {
25
+ this.opts.onStdout(line);
26
+ }
27
+ }
28
+ });
29
+ proc.stderr.on("data", (chunk) => {
30
+ const text = chunk.toString();
31
+ stderr += text;
32
+ if (this.opts.onStderr) {
33
+ for (const line of text.split("\n").filter(Boolean)) {
34
+ this.opts.onStderr(line);
35
+ }
36
+ }
37
+ });
38
+ proc.on("error", (err) => {
39
+ reject(new Error(`Failed to spawn ${this.opts.command}: ${err.message}`));
40
+ });
41
+ proc.on("close", (code) => {
42
+ const result = {
43
+ stdout,
44
+ stderr,
45
+ exitCode: code ?? 1,
46
+ durationMs: Date.now() - start
47
+ };
48
+ if (code === 0) resolve(result);
49
+ else {
50
+ const err = new Error(`${this.opts.command} exited with code ${code}`);
51
+ err.result = result;
52
+ reject(err);
53
+ }
54
+ });
55
+ });
56
+ }
57
+ };
58
+
59
+ export {
60
+ CliTask
61
+ };
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- __require
4
- } from "./chunk-3RG5ZIWI.js";
3
+ CliTask
4
+ } from "./chunk-HWC47EK2.js";
5
5
 
6
6
  // src/cli.tsx
7
7
  import { render, Box as Box6 } from "ink";
@@ -9,6 +9,7 @@ import { render, Box as Box6 } from "ink";
9
9
  // src/App.tsx
10
10
  import { useState as useState3 } from "react";
11
11
  import { useApp as useApp2, useInput as useInput4 } from "ink";
12
+ import * as fs5 from "fs";
12
13
 
13
14
  // src/screens/ApiKeyScreen.tsx
14
15
  import { useState } from "react";
@@ -222,10 +223,10 @@ function createCodebaseTools(cwd) {
222
223
  required: ["path"],
223
224
  additionalProperties: false
224
225
  },
225
- async invoke(args) {
226
- const target = safePath(cwd, args.path || ".");
227
- if (!target || !fs2.existsSync(target)) return `Directory not found: ${args.path}`;
228
- if (!fs2.statSync(target).isDirectory()) return `Not a directory: ${args.path}`;
226
+ async invoke(args2) {
227
+ const target = safePath(cwd, args2.path || ".");
228
+ if (!target || !fs2.existsSync(target)) return `Directory not found: ${args2.path}`;
229
+ if (!fs2.statSync(target).isDirectory()) return `Not a directory: ${args2.path}`;
229
230
  const results = [];
230
231
  function walk(dir, prefix, depth) {
231
232
  if (results.length >= 200 || depth > 4) return;
@@ -234,7 +235,7 @@ function createCodebaseTools(cwd) {
234
235
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
235
236
  if (entry.isDirectory()) {
236
237
  results.push(rel + "/");
237
- if (args.recursive) walk(path2.join(dir, entry.name), rel, depth + 1);
238
+ if (args2.recursive) walk(path2.join(dir, entry.name), rel, depth + 1);
238
239
  } else {
239
240
  results.push(rel);
240
241
  }
@@ -255,10 +256,10 @@ function createCodebaseTools(cwd) {
255
256
  required: ["path"],
256
257
  additionalProperties: false
257
258
  },
258
- async invoke(args) {
259
- const target = safePath(cwd, args.path);
260
- if (!target || !fs2.existsSync(target)) return `File not found: ${args.path}`;
261
- if (fs2.statSync(target).isDirectory()) return `${args.path} is a directory. Use list_files.`;
259
+ async invoke(args2) {
260
+ const target = safePath(cwd, args2.path);
261
+ if (!target || !fs2.existsSync(target)) return `File not found: ${args2.path}`;
262
+ if (fs2.statSync(target).isDirectory()) return `${args2.path} is a directory. Use list_files.`;
262
263
  if (fs2.statSync(target).size > 5e5) return `File too large (${(fs2.statSync(target).size / 1024).toFixed(0)}KB)`;
263
264
  let content = fs2.readFileSync(target, "utf-8");
264
265
  if (content.length > MAX_FILE_SIZE) {
@@ -280,13 +281,13 @@ function createCodebaseTools(cwd) {
280
281
  required: ["pattern"],
281
282
  additionalProperties: false
282
283
  },
283
- async invoke(args) {
284
- const searchDir = safePath(cwd, args.path || ".");
285
- if (!searchDir || !fs2.existsSync(searchDir)) return `Directory not found: ${args.path}`;
284
+ async invoke(args2) {
285
+ const searchDir = safePath(cwd, args2.path || ".");
286
+ if (!searchDir || !fs2.existsSync(searchDir)) return `Directory not found: ${args2.path}`;
286
287
  try {
287
288
  const excludes = Array.from(IGNORE).map((d) => `--exclude-dir=${d}`).join(" ");
288
- const include = args.file_pattern ? `--include='${args.file_pattern}'` : "";
289
- const cmd = `grep -rn -i ${excludes} ${include} --max-count=50 -- ${JSON.stringify(args.pattern)} ${JSON.stringify(searchDir)} 2>/dev/null || true`;
289
+ const include = args2.file_pattern ? `--include='${args2.file_pattern}'` : "";
290
+ const cmd = `grep -rn -i ${excludes} ${include} --max-count=50 -- ${JSON.stringify(args2.pattern)} ${JSON.stringify(searchDir)} 2>/dev/null || true`;
290
291
  const output = execSync(cmd, { encoding: "utf-8", maxBuffer: 1024 * 1024 });
291
292
  const lines = output.split("\n").filter(Boolean).map(
292
293
  (line) => line.startsWith(cwd) ? line.slice(cwd.length + 1) : line
@@ -446,26 +447,136 @@ var Planner = class {
446
447
  }
447
448
  };
448
449
 
450
+ // src/core/claude-planner.ts
451
+ var SYSTEM_PROMPT2 = `You are an expert software architect. Break down the user's project goal into concrete user stories that form a dependency DAG.
452
+
453
+ You MUST explore the existing codebase first using your tools (read files, list directories, etc.) before generating the plan.
454
+
455
+ Output ONLY valid JSON matching this exact schema (no markdown, no explanation, just JSON):
456
+ {
457
+ "project": "short project name",
458
+ "branchName": "kebab-case-branch-name",
459
+ "description": "one-line description",
460
+ "userStories": [
461
+ {
462
+ "id": "S1",
463
+ "priority": 1,
464
+ "title": "short title",
465
+ "description": "what to implement",
466
+ "dependsOn": [],
467
+ "retries": 2,
468
+ "acceptance": ["testable criterion"],
469
+ "tests": ["npm test"]
470
+ }
471
+ ]
472
+ }
473
+
474
+ Rules:
475
+ - Each story: single focused unit of work for one AI agent
476
+ - Use dependsOn for dependencies; same-priority stories with no deps run IN PARALLEL
477
+ - Keep stories small (15-60 min of work each)
478
+ - Include testable acceptance criteria and test commands
479
+ - No circular dependencies
480
+ - Start with foundational stories, build up
481
+ - IDs: S1, S2, S3...
482
+ - Build on existing code, don't recreate what exists
483
+ - Output ONLY the JSON, nothing else`;
484
+ var ClaudePlanner = class {
485
+ cwd;
486
+ onLog;
487
+ constructor(options = {}) {
488
+ this.cwd = options.cwd ?? process.cwd();
489
+ this.onLog = options.onLog;
490
+ }
491
+ async send(userMessage) {
492
+ const prompt = `${SYSTEM_PROMPT2}
493
+
494
+ User goal: ${userMessage}`;
495
+ const task = new CliTask({
496
+ id: "planner",
497
+ command: "claude",
498
+ args: [
499
+ "--dangerously-skip-permissions",
500
+ "--output-format",
501
+ "json",
502
+ "-p",
503
+ prompt
504
+ ],
505
+ cwd: this.cwd,
506
+ onStdout: (line) => this.onLog?.(line),
507
+ onStderr: (line) => this.onLog?.(line)
508
+ });
509
+ const result = await task.execute();
510
+ let jsonText = result.stdout.trim();
511
+ try {
512
+ const wrapper = JSON.parse(jsonText);
513
+ if (wrapper.result) {
514
+ jsonText = wrapper.result;
515
+ }
516
+ } catch {
517
+ }
518
+ const jsonMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)```/);
519
+ if (jsonMatch) {
520
+ jsonText = jsonMatch[1].trim();
521
+ }
522
+ let prd;
523
+ try {
524
+ prd = JSON.parse(jsonText);
525
+ } catch {
526
+ throw new Error("Claude didn't return valid JSON. Try again with a clearer goal.");
527
+ }
528
+ if (!prd.project || !prd.userStories) {
529
+ throw new Error("Invalid plan format. Missing project or userStories.");
530
+ }
531
+ return {
532
+ ...prd,
533
+ userStories: prd.userStories.map((s) => ({
534
+ id: s.id ?? "S?",
535
+ priority: s.priority ?? 0,
536
+ title: s.title ?? "",
537
+ description: s.description ?? "",
538
+ dependsOn: s.dependsOn ?? [],
539
+ retries: s.retries ?? 2,
540
+ acceptance: s.acceptance ?? [],
541
+ tests: s.tests ?? [],
542
+ passes: false,
543
+ completedAt: null,
544
+ durationSecs: null
545
+ }))
546
+ };
547
+ }
548
+ };
549
+
449
550
  // src/screens/PlanScreen.tsx
450
551
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
451
- function PlanScreen({ onPlanReady, onQuit }) {
552
+ function PlanScreen({ plannerMode: plannerMode2, onPlanReady, onQuit }) {
452
553
  const [input, setInput] = useState2("");
453
554
  const [loading, setLoading] = useState2(false);
454
555
  const [tokenCount, setTokenCount] = useState2(0);
455
556
  const [toolCalls, setToolCalls] = useState2([]);
456
557
  const [error, setError] = useState2("");
457
- const [planner] = useState2(() => new Planner({
458
- cwd: process.cwd(),
459
- onToken: () => setTokenCount((c) => c + 1),
460
- onToolCall: (name, args) => {
461
- let label = name;
462
- if (name === "read_file") label = `Reading ${args?.path ?? "..."}`;
463
- else if (name === "grep") label = `Searching for "${args?.pattern ?? "..."}"`;
464
- else if (name === "list_files") label = `Listing ${args?.path || "root"}`;
465
- else if (name === "file_tree") label = "Scanning project structure";
466
- setToolCalls((prev) => [...prev.slice(-8), label]);
558
+ const [planner] = useState2(() => {
559
+ if (plannerMode2 === "openai") {
560
+ return new Planner({
561
+ cwd: process.cwd(),
562
+ onToken: () => setTokenCount((c) => c + 1),
563
+ onToolCall: (name, args2) => {
564
+ let label = name;
565
+ if (name === "read_file") label = `Reading ${args2?.path ?? "..."}`;
566
+ else if (name === "grep") label = `Searching for "${args2?.pattern ?? "..."}"`;
567
+ else if (name === "list_files") label = `Listing ${args2?.path || "root"}`;
568
+ else if (name === "file_tree") label = "Scanning project structure";
569
+ setToolCalls((prev) => [...prev.slice(-8), label]);
570
+ }
571
+ });
467
572
  }
468
- }));
573
+ return new ClaudePlanner({
574
+ cwd: process.cwd(),
575
+ onLog: (line) => {
576
+ setToolCalls((prev) => [...prev.slice(-8), line.slice(0, 80)]);
577
+ }
578
+ });
579
+ });
469
580
  const submit = useCallback(async () => {
470
581
  const goal = input.trim();
471
582
  if (!goal) return;
@@ -665,14 +776,12 @@ function ExecuteScreen({ prd, onDone }) {
665
776
 
666
777
  // src/App.tsx
667
778
  import { jsx as jsx5 } from "react/jsx-runtime";
668
- function App() {
779
+ function App({ plannerMode: plannerMode2 }) {
669
780
  const app = useApp2();
670
781
  const [screen, setScreen] = useState3(() => {
671
- if (process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY) {
672
- return "plan";
673
- }
782
+ if (plannerMode2 === "claude") return "plan";
783
+ if (process.env.OPENAI_API_KEY) return "plan";
674
784
  try {
675
- const fs5 = __require("fs");
676
785
  const home = process.env.HOME ?? "";
677
786
  const envPath = `${home}/.baro/.env`;
678
787
  if (fs5.existsSync(envPath)) {
@@ -681,33 +790,24 @@ function App() {
681
790
  const [key, val] = line.split("=");
682
791
  if (key && val) process.env[key.trim()] = val.trim();
683
792
  }
684
- if (process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY) {
685
- return "plan";
686
- }
793
+ if (process.env.OPENAI_API_KEY) return "plan";
687
794
  }
688
795
  } catch {
689
796
  }
690
797
  return "apikey";
691
798
  });
692
799
  const [prd, setPrd] = useState3(null);
693
- useInput4((input, key) => {
694
- if (key.escape && screen === "execute") {
695
- app.exit();
696
- }
800
+ useInput4((_input, key) => {
801
+ if (key.escape && screen === "execute") app.exit();
697
802
  });
698
803
  if (screen === "apikey") {
699
- return /* @__PURE__ */ jsx5(
700
- ApiKeyScreen,
701
- {
702
- onComplete: () => setScreen("plan"),
703
- onQuit: () => app.exit()
704
- }
705
- );
804
+ return /* @__PURE__ */ jsx5(ApiKeyScreen, { onComplete: () => setScreen("plan"), onQuit: () => app.exit() });
706
805
  }
707
806
  if (screen === "plan") {
708
807
  return /* @__PURE__ */ jsx5(
709
808
  PlanScreen,
710
809
  {
810
+ plannerMode: plannerMode2,
711
811
  onPlanReady: (plan) => {
712
812
  setPrd(plan);
713
813
  setScreen("review");
@@ -727,18 +827,36 @@ function App() {
727
827
  }
728
828
  );
729
829
  }
730
- return /* @__PURE__ */ jsx5(
731
- ExecuteScreen,
732
- {
733
- prd,
734
- onDone: () => app.exit()
735
- }
736
- );
830
+ return /* @__PURE__ */ jsx5(ExecuteScreen, { prd, onDone: () => app.exit() });
737
831
  }
738
832
 
739
833
  // src/cli.tsx
740
834
  import { jsx as jsx6 } from "react/jsx-runtime";
835
+ var args = process.argv.slice(2);
836
+ var plannerMode = "claude";
837
+ for (let i = 0; i < args.length; i++) {
838
+ if (args[i] === "--planner" && args[i + 1]) {
839
+ const val = args[i + 1].toLowerCase();
840
+ if (val === "openai" || val === "gpt" || val.startsWith("gpt-")) {
841
+ plannerMode = "openai";
842
+ }
843
+ i++;
844
+ }
845
+ if (args[i] === "--help" || args[i] === "-h") {
846
+ console.log(`
847
+ baro - autonomous parallel coding
848
+
849
+ Usage:
850
+ baro Plan with Claude Code, execute with Claude Code
851
+ baro --planner openai Plan with GPT-5.4 (needs OPENAI_API_KEY)
852
+
853
+ The default mode requires only Claude Code CLI installed.
854
+ No API keys needed.
855
+ `);
856
+ process.exit(0);
857
+ }
858
+ }
741
859
  process.stdout.write("\x1B[2J\x1B[H");
742
860
  render(
743
- /* @__PURE__ */ jsx6(Box6, { width: "100%", flexDirection: "column", alignItems: "flex-start", children: /* @__PURE__ */ jsx6(App, {}) })
861
+ /* @__PURE__ */ jsx6(Box6, { width: "100%", flexDirection: "column", alignItems: "flex-start", children: /* @__PURE__ */ jsx6(App, { plannerMode }) })
744
862
  );
@@ -1,68 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import "../chunk-3RG5ZIWI.js";
2
+ import {
3
+ CliTask
4
+ } from "../chunk-HWC47EK2.js";
3
5
 
4
6
  // src/core/executor.ts
5
7
  import * as path from "path";
6
8
  import * as fs from "fs";
7
9
 
8
- // src/core/cli-task.ts
9
- import { spawn } from "child_process";
10
- var CliTask = class {
11
- id;
12
- opts;
13
- constructor(opts) {
14
- this.id = opts.id;
15
- this.opts = opts;
16
- }
17
- execute() {
18
- return new Promise((resolve, reject) => {
19
- const start = Date.now();
20
- let stdout = "";
21
- let stderr = "";
22
- const proc = spawn(this.opts.command, this.opts.args, {
23
- cwd: this.opts.cwd,
24
- stdio: ["ignore", "pipe", "pipe"],
25
- env: { ...process.env }
26
- });
27
- proc.stdout.on("data", (chunk) => {
28
- const text = chunk.toString();
29
- stdout += text;
30
- if (this.opts.onStdout) {
31
- for (const line of text.split("\n").filter(Boolean)) {
32
- this.opts.onStdout(line);
33
- }
34
- }
35
- });
36
- proc.stderr.on("data", (chunk) => {
37
- const text = chunk.toString();
38
- stderr += text;
39
- if (this.opts.onStderr) {
40
- for (const line of text.split("\n").filter(Boolean)) {
41
- this.opts.onStderr(line);
42
- }
43
- }
44
- });
45
- proc.on("error", (err) => {
46
- reject(new Error(`Failed to spawn ${this.opts.command}: ${err.message}`));
47
- });
48
- proc.on("close", (code) => {
49
- const result = {
50
- stdout,
51
- stderr,
52
- exitCode: code ?? 1,
53
- durationMs: Date.now() - start
54
- };
55
- if (code === 0) resolve(result);
56
- else {
57
- const err = new Error(`${this.opts.command} exited with code ${code}`);
58
- err.result = result;
59
- reject(err);
60
- }
61
- });
62
- });
63
- }
64
- };
65
-
66
10
  // src/core/dag.ts
67
11
  function buildDag(stories) {
68
12
  const incomplete = stories.filter((s) => !s.passes);
@@ -137,7 +81,7 @@ async function main() {
137
81
  const task = new CliTask({
138
82
  id: story.id,
139
83
  command: "claude",
140
- args: ["--dangerously-skip-permissions", "--output-format", "stream-json", "-p", prompt],
84
+ args: ["--dangerously-skip-permissions", "--output-format", "stream-json", "--verbose", "-p", prompt],
141
85
  cwd,
142
86
  onStdout: (line) => {
143
87
  try {
@@ -149,23 +93,27 @@ async function main() {
149
93
  emit({ type: "story_log", id: story.id, line: l });
150
94
  }
151
95
  } else if (block.type === "tool_use") {
152
- emit({ type: "story_log", id: story.id, line: `\u2699 ${block.name}: ${JSON.stringify(block.input).slice(0, 100)}` });
153
- } else if (block.type === "tool_result") {
154
- const text = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
155
- if (text.length > 0) {
156
- emit({ type: "story_log", id: story.id, line: text.slice(0, 150) });
157
- }
96
+ const input = JSON.stringify(block.input ?? {});
97
+ const preview = input.length > 80 ? input.slice(0, 80) + "..." : input;
98
+ emit({ type: "story_log", id: story.id, line: `\u2699 ${block.name} ${preview}` });
158
99
  }
159
100
  }
101
+ } else if (ev.type === "system" && ev.subtype === "init") {
102
+ emit({ type: "story_log", id: story.id, line: `Model: ${ev.model ?? "unknown"}` });
160
103
  } else if (ev.type === "result") {
104
+ if (ev.result) {
105
+ for (const l of String(ev.result).split("\n").slice(0, 3)) {
106
+ if (l.trim()) emit({ type: "story_log", id: story.id, line: l });
107
+ }
108
+ }
161
109
  }
162
110
  } catch {
163
- if (line.trim()) {
164
- emit({ type: "story_log", id: story.id, line });
165
- }
111
+ if (line.trim()) emit({ type: "story_log", id: story.id, line });
166
112
  }
167
113
  },
168
- onStderr: (line) => emit({ type: "story_log", id: story.id, line })
114
+ onStderr: (line) => {
115
+ if (line.trim()) emit({ type: "story_log", id: story.id, line });
116
+ }
169
117
  });
170
118
  const result = await task.execute();
171
119
  const dur = Math.round(result.durationMs / 1e3);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baro-ai",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Autonomous parallel coding - plan and execute with AI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,10 +0,0 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
7
-
8
- export {
9
- __require
10
- };