@saga-ai/cli 0.4.0 → 0.5.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 (2) hide show
  1. package/dist/cli.cjs +164 -31
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -5022,7 +5022,114 @@ function spawnWorker(prompt, model, settings, workingDir) {
5022
5022
  }
5023
5023
  return result.stdout || "";
5024
5024
  }
5025
- function runLoop(epicSlug, storySlug, maxCycles, maxTime, model, projectDir, pluginRoot) {
5025
+ function formatStreamLine(line) {
5026
+ try {
5027
+ const data = JSON.parse(line);
5028
+ if (data.type === "assistant" && data.message?.content) {
5029
+ for (const block of data.message.content) {
5030
+ if (block.type === "text" && block.text) {
5031
+ return block.text;
5032
+ }
5033
+ if (block.type === "tool_use") {
5034
+ return `[Tool: ${block.name}]`;
5035
+ }
5036
+ }
5037
+ }
5038
+ if (data.type === "system" && data.subtype === "init") {
5039
+ return `[Session started: ${data.session_id}]`;
5040
+ }
5041
+ if (data.type === "result") {
5042
+ const status = data.subtype === "success" ? "completed" : "failed";
5043
+ return `
5044
+ [Worker ${status} in ${Math.round(data.duration_ms / 1e3)}s]`;
5045
+ }
5046
+ return null;
5047
+ } catch {
5048
+ return null;
5049
+ }
5050
+ }
5051
+ function parseStreamingResult(buffer) {
5052
+ const lines = buffer.split("\n").filter((line) => line.trim());
5053
+ for (let i = lines.length - 1; i >= 0; i--) {
5054
+ try {
5055
+ const data = JSON.parse(lines[i]);
5056
+ if (data.type === "result") {
5057
+ if (data.is_error) {
5058
+ throw new Error(`Worker failed: ${data.result || "Unknown error"}`);
5059
+ }
5060
+ if (!data.structured_output) {
5061
+ throw new Error("Worker result missing structured_output");
5062
+ }
5063
+ const output = data.structured_output;
5064
+ if (!VALID_STATUSES.has(output.status)) {
5065
+ throw new Error(`Invalid status: ${output.status}`);
5066
+ }
5067
+ return {
5068
+ status: output.status,
5069
+ summary: output.summary || "",
5070
+ blocker: output.blocker ?? null
5071
+ };
5072
+ }
5073
+ } catch (e) {
5074
+ if (e instanceof Error && e.message.startsWith("Worker")) {
5075
+ throw e;
5076
+ }
5077
+ }
5078
+ }
5079
+ throw new Error("No result found in worker output");
5080
+ }
5081
+ function spawnWorkerAsync(prompt, model, settings, workingDir) {
5082
+ return new Promise((resolve2, reject) => {
5083
+ let buffer = "";
5084
+ const args = [
5085
+ "-p",
5086
+ prompt,
5087
+ "--model",
5088
+ model,
5089
+ "--output-format",
5090
+ "stream-json",
5091
+ "--verbose",
5092
+ "--json-schema",
5093
+ JSON.stringify(WORKER_OUTPUT_SCHEMA),
5094
+ "--settings",
5095
+ JSON.stringify(settings),
5096
+ "--dangerously-skip-permissions"
5097
+ ];
5098
+ const child = (0, import_node_child_process.spawn)("claude", args, {
5099
+ cwd: workingDir,
5100
+ stdio: ["ignore", "pipe", "pipe"]
5101
+ });
5102
+ child.stdout.on("data", (chunk) => {
5103
+ const text = chunk.toString();
5104
+ buffer += text;
5105
+ const lines = text.split("\n");
5106
+ for (const line of lines) {
5107
+ if (line.trim()) {
5108
+ const formatted = formatStreamLine(line);
5109
+ if (formatted) {
5110
+ process.stdout.write(formatted);
5111
+ }
5112
+ }
5113
+ }
5114
+ });
5115
+ child.stderr.on("data", (chunk) => {
5116
+ process.stderr.write(chunk);
5117
+ });
5118
+ child.on("error", (err) => {
5119
+ reject(new Error(`Failed to spawn worker: ${err.message}`));
5120
+ });
5121
+ child.on("close", (code) => {
5122
+ process.stdout.write("\n");
5123
+ try {
5124
+ const result = parseStreamingResult(buffer);
5125
+ resolve2(result);
5126
+ } catch (e) {
5127
+ reject(e);
5128
+ }
5129
+ });
5130
+ });
5131
+ }
5132
+ async function runLoop(epicSlug, storySlug, maxCycles, maxTime, model, projectDir, pluginRoot, stream = false) {
5026
5133
  const worktree = (0, import_node_path4.join)(projectDir, ".saga", "worktrees", epicSlug, storySlug);
5027
5134
  const validation = validateStoryFiles(worktree, epicSlug, storySlug);
5028
5135
  if (!validation.valid) {
@@ -5064,33 +5171,55 @@ function runLoop(epicSlug, storySlug, maxCycles, maxTime, model, projectDir, plu
5064
5171
  break;
5065
5172
  }
5066
5173
  cycles += 1;
5067
- let output;
5068
- try {
5069
- output = spawnWorker(workerPrompt, model, settings, worktree);
5070
- } catch (e) {
5071
- return {
5072
- status: "ERROR",
5073
- summary: e.message,
5074
- cycles,
5075
- elapsedMinutes: (Date.now() - startTime) / 6e4,
5076
- blocker: null,
5077
- epicSlug,
5078
- storySlug
5079
- };
5080
- }
5081
5174
  let parsed;
5082
- try {
5083
- parsed = parseWorkerOutput(output);
5084
- } catch (e) {
5085
- return {
5086
- status: "ERROR",
5087
- summary: e.message,
5088
- cycles,
5089
- elapsedMinutes: (Date.now() - startTime) / 6e4,
5090
- blocker: null,
5091
- epicSlug,
5092
- storySlug
5093
- };
5175
+ if (stream) {
5176
+ console.log(`
5177
+ --- Worker ${cycles} started ---
5178
+ `);
5179
+ try {
5180
+ parsed = await spawnWorkerAsync(workerPrompt, model, settings, worktree);
5181
+ } catch (e) {
5182
+ return {
5183
+ status: "ERROR",
5184
+ summary: e.message,
5185
+ cycles,
5186
+ elapsedMinutes: (Date.now() - startTime) / 6e4,
5187
+ blocker: null,
5188
+ epicSlug,
5189
+ storySlug
5190
+ };
5191
+ }
5192
+ console.log(`
5193
+ --- Worker ${cycles} result: ${parsed.status} ---
5194
+ `);
5195
+ } else {
5196
+ let output;
5197
+ try {
5198
+ output = spawnWorker(workerPrompt, model, settings, worktree);
5199
+ } catch (e) {
5200
+ return {
5201
+ status: "ERROR",
5202
+ summary: e.message,
5203
+ cycles,
5204
+ elapsedMinutes: (Date.now() - startTime) / 6e4,
5205
+ blocker: null,
5206
+ epicSlug,
5207
+ storySlug
5208
+ };
5209
+ }
5210
+ try {
5211
+ parsed = parseWorkerOutput(output);
5212
+ } catch (e) {
5213
+ return {
5214
+ status: "ERROR",
5215
+ summary: e.message,
5216
+ cycles,
5217
+ elapsedMinutes: (Date.now() - startTime) / 6e4,
5218
+ blocker: null,
5219
+ epicSlug,
5220
+ storySlug
5221
+ };
5222
+ }
5094
5223
  }
5095
5224
  summaries.push(parsed.summary);
5096
5225
  if (parsed.status === "FINISH") {
@@ -5157,19 +5286,22 @@ Searched in: ${(0, import_node_path4.join)(projectPath, ".saga", "worktrees")}`)
5157
5286
  const maxCycles = options.maxCycles ?? DEFAULT_MAX_CYCLES;
5158
5287
  const maxTime = options.maxTime ?? DEFAULT_MAX_TIME;
5159
5288
  const model = options.model ?? DEFAULT_MODEL;
5289
+ const stream = options.stream ?? false;
5160
5290
  console.log("Starting story implementation...");
5161
5291
  console.log(` Epic: ${storyInfo.epicSlug}`);
5162
5292
  console.log(` Story: ${storyInfo.storySlug}`);
5163
5293
  console.log(` Worktree: ${storyInfo.worktreePath}`);
5294
+ console.log(` Streaming: ${stream ? "enabled" : "disabled"}`);
5164
5295
  console.log("");
5165
- const result = runLoop(
5296
+ const result = await runLoop(
5166
5297
  storyInfo.epicSlug,
5167
5298
  storyInfo.storySlug,
5168
5299
  maxCycles,
5169
5300
  maxTime,
5170
5301
  model,
5171
5302
  projectPath,
5172
- pluginRoot
5303
+ pluginRoot,
5304
+ stream
5173
5305
  );
5174
5306
  console.log(JSON.stringify(result, null, 2));
5175
5307
  if (result.status === "ERROR") {
@@ -5441,14 +5573,15 @@ program2.command("init").description("Initialize .saga/ directory structure").op
5441
5573
  const globalOpts = program2.opts();
5442
5574
  await initCommand({ path: globalOpts.path, dryRun: options.dryRun });
5443
5575
  });
5444
- program2.command("implement <story-slug>").description("Run story implementation").option("--max-cycles <n>", "Maximum number of implementation cycles", parseInt).option("--max-time <n>", "Maximum time in minutes", parseInt).option("--model <name>", "Model to use for implementation").option("--dry-run", "Validate dependencies without running implementation").action(async (storySlug, options) => {
5576
+ program2.command("implement <story-slug>").description("Run story implementation").option("--max-cycles <n>", "Maximum number of implementation cycles", parseInt).option("--max-time <n>", "Maximum time in minutes", parseInt).option("--model <name>", "Model to use for implementation").option("--dry-run", "Validate dependencies without running implementation").option("--stream", "Stream worker output in real-time").action(async (storySlug, options) => {
5445
5577
  const globalOpts = program2.opts();
5446
5578
  await implementCommand(storySlug, {
5447
5579
  path: globalOpts.path,
5448
5580
  maxCycles: options.maxCycles,
5449
5581
  maxTime: options.maxTime,
5450
5582
  model: options.model,
5451
- dryRun: options.dryRun
5583
+ dryRun: options.dryRun,
5584
+ stream: options.stream
5452
5585
  });
5453
5586
  });
5454
5587
  program2.command("find <query>").description("Find an epic or story by slug/title").option("--type <type>", "Type to search for: epic or story (default: story)").action(async (query, options) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saga-ai/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "CLI for SAGA - Structured Autonomous Goal Achievement",
5
5
  "type": "module",
6
6
  "bin": {