@saga-ai/cli 0.7.1 → 0.7.2

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 (3) hide show
  1. package/README.md +0 -3
  2. package/dist/cli.cjs +26 -124
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -85,15 +85,12 @@ saga implement my-story
85
85
  saga implement my-story --max-cycles 5
86
86
  saga implement my-story --max-time 30
87
87
  saga implement my-story --model sonnet
88
- saga implement my-story --attached --stream
89
88
  ```
90
89
 
91
90
  Options:
92
91
  - `--max-cycles <n>` - Maximum number of worker cycles (default: 10)
93
92
  - `--max-time <n>` - Maximum time in minutes (default: 60)
94
93
  - `--model <name>` - Model to use (default: opus)
95
- - `--attached` - Run in attached mode (synchronous). Default is detached (runs in tmux session)
96
- - `--stream` - Stream worker output to stdout (only works with `--attached`)
97
94
 
98
95
  By default, `implement` runs in **detached mode** using a tmux session. This allows the worker to continue running even if your terminal disconnects. Use `saga sessions` to monitor detached sessions.
99
96
 
package/dist/cli.cjs CHANGED
@@ -630,6 +630,9 @@ async function createSession(epicSlug, storySlug, command) {
630
630
  # Auto-generated wrapper script for SAGA session
631
631
  set -e
632
632
 
633
+ # Mark this as an internal SAGA session (used by CLI to detect it's running inside tmux)
634
+ export SAGA_INTERNAL_SESSION=1
635
+
633
636
  COMMAND_FILE="${commandFilePath}"
634
637
  OUTPUT_FILE="${outputFile}"
635
638
  SCRIPT_FILE="${wrapperScriptPath}"
@@ -958,64 +961,6 @@ function buildScopeSettings() {
958
961
  }
959
962
  };
960
963
  }
961
- function parseWorkerOutput(output) {
962
- if (!output || !output.trim()) {
963
- throw new Error("Worker output is empty");
964
- }
965
- let cliResponse;
966
- try {
967
- cliResponse = JSON.parse(output.trim());
968
- } catch (e) {
969
- throw new Error(`Invalid JSON in worker output: ${e}`);
970
- }
971
- if (!("structured_output" in cliResponse)) {
972
- if (cliResponse.is_error) {
973
- const errorMsg = cliResponse.result || "Unknown error";
974
- throw new Error(`Worker failed: ${errorMsg}`);
975
- }
976
- throw new Error(`Worker output missing structured_output field. Got keys: ${Object.keys(cliResponse).join(", ")}`);
977
- }
978
- const parsed = cliResponse.structured_output;
979
- if (!("status" in parsed)) {
980
- throw new Error("Worker output missing required field: status");
981
- }
982
- if (!("summary" in parsed)) {
983
- throw new Error("Worker output missing required field: summary");
984
- }
985
- if (!VALID_STATUSES.has(parsed.status)) {
986
- throw new Error(`Invalid status value: ${parsed.status}. Must be one of: ${[...VALID_STATUSES].join(", ")}`);
987
- }
988
- return {
989
- status: parsed.status,
990
- summary: parsed.summary,
991
- blocker: parsed.blocker ?? null
992
- };
993
- }
994
- function spawnWorker(prompt, model, settings, workingDir) {
995
- const args = [
996
- "-p",
997
- prompt,
998
- "--model",
999
- model,
1000
- "--output-format",
1001
- "json",
1002
- "--json-schema",
1003
- JSON.stringify(WORKER_OUTPUT_SCHEMA),
1004
- "--settings",
1005
- JSON.stringify(settings),
1006
- "--dangerously-skip-permissions"
1007
- ];
1008
- const result = (0, import_node_child_process2.spawnSync)("claude", args, {
1009
- cwd: workingDir,
1010
- encoding: "utf-8",
1011
- maxBuffer: 50 * 1024 * 1024
1012
- // 50MB buffer for large outputs
1013
- });
1014
- if (result.error) {
1015
- throw new Error(`Failed to spawn worker: ${result.error.message}`);
1016
- }
1017
- return result.stdout || "";
1018
- }
1019
964
  function formatStreamLine(line) {
1020
965
  try {
1021
966
  const data = JSON.parse(line);
@@ -1142,7 +1087,7 @@ function spawnWorkerAsync(prompt, model, settings, workingDir) {
1142
1087
  });
1143
1088
  });
1144
1089
  }
1145
- async function runLoop(epicSlug, storySlug, maxCycles, maxTime, model, projectDir, pluginRoot, stream = false) {
1090
+ async function runLoop(epicSlug, storySlug, maxCycles, maxTime, model, projectDir, pluginRoot) {
1146
1091
  const worktree = (0, import_node_path5.join)(projectDir, ".saga", "worktrees", epicSlug, storySlug);
1147
1092
  const validation = validateStoryFiles(worktree, epicSlug, storySlug);
1148
1093
  if (!validation.valid) {
@@ -1185,55 +1130,25 @@ async function runLoop(epicSlug, storySlug, maxCycles, maxTime, model, projectDi
1185
1130
  }
1186
1131
  cycles += 1;
1187
1132
  let parsed;
1188
- if (stream) {
1189
- console.log(`
1133
+ console.log(`
1190
1134
  --- Worker ${cycles} started ---
1191
1135
  `);
1192
- try {
1193
- parsed = await spawnWorkerAsync(workerPrompt, model, settings, worktree);
1194
- } catch (e) {
1195
- return {
1196
- status: "ERROR",
1197
- summary: e.message,
1198
- cycles,
1199
- elapsedMinutes: (Date.now() - startTime) / 6e4,
1200
- blocker: null,
1201
- epicSlug,
1202
- storySlug
1203
- };
1204
- }
1205
- console.log(`
1136
+ try {
1137
+ parsed = await spawnWorkerAsync(workerPrompt, model, settings, worktree);
1138
+ } catch (e) {
1139
+ return {
1140
+ status: "ERROR",
1141
+ summary: e.message,
1142
+ cycles,
1143
+ elapsedMinutes: (Date.now() - startTime) / 6e4,
1144
+ blocker: null,
1145
+ epicSlug,
1146
+ storySlug
1147
+ };
1148
+ }
1149
+ console.log(`
1206
1150
  --- Worker ${cycles} result: ${parsed.status} ---
1207
1151
  `);
1208
- } else {
1209
- let output;
1210
- try {
1211
- output = spawnWorker(workerPrompt, model, settings, worktree);
1212
- } catch (e) {
1213
- return {
1214
- status: "ERROR",
1215
- summary: e.message,
1216
- cycles,
1217
- elapsedMinutes: (Date.now() - startTime) / 6e4,
1218
- blocker: null,
1219
- epicSlug,
1220
- storySlug
1221
- };
1222
- }
1223
- try {
1224
- parsed = parseWorkerOutput(output);
1225
- } catch (e) {
1226
- return {
1227
- status: "ERROR",
1228
- summary: e.message,
1229
- cycles,
1230
- elapsedMinutes: (Date.now() - startTime) / 6e4,
1231
- blocker: null,
1232
- epicSlug,
1233
- storySlug
1234
- };
1235
- }
1236
- }
1237
1152
  summaries.push(parsed.summary);
1238
1153
  if (parsed.status === "FINISH") {
1239
1154
  finalStatus = "FINISH";
@@ -1260,7 +1175,7 @@ async function runLoop(epicSlug, storySlug, maxCycles, maxTime, model, projectDi
1260
1175
  };
1261
1176
  }
1262
1177
  function buildDetachedCommand(storySlug, projectPath, options) {
1263
- const parts = ["saga", "implement", storySlug, "--attached"];
1178
+ const parts = ["saga", "implement", storySlug];
1264
1179
  parts.push("--path", projectPath);
1265
1180
  if (options.maxCycles !== void 0) {
1266
1181
  parts.push("--max-cycles", String(options.maxCycles));
@@ -1271,9 +1186,6 @@ function buildDetachedCommand(storySlug, projectPath, options) {
1271
1186
  if (options.model !== void 0) {
1272
1187
  parts.push("--model", options.model);
1273
1188
  }
1274
- if (options.stream) {
1275
- parts.push("--stream");
1276
- }
1277
1189
  return shellEscapeArgs(parts);
1278
1190
  }
1279
1191
  async function implementCommand(storySlug, options) {
@@ -1316,18 +1228,12 @@ Searched in: ${(0, import_node_path5.join)(projectPath, ".saga", "worktrees")}`)
1316
1228
  const maxCycles = options.maxCycles ?? DEFAULT_MAX_CYCLES;
1317
1229
  const maxTime = options.maxTime ?? DEFAULT_MAX_TIME;
1318
1230
  const model = options.model ?? DEFAULT_MODEL;
1319
- const stream = options.stream ?? false;
1320
- const attached = options.attached ?? false;
1321
- if (!attached) {
1322
- if (stream) {
1323
- console.error("Warning: --stream is ignored in detached mode. Use --attached --stream for streaming output.");
1324
- }
1231
+ const isInternalSession = process.env.SAGA_INTERNAL_SESSION === "1";
1232
+ if (!isInternalSession) {
1325
1233
  const detachedCommand = buildDetachedCommand(storySlug, projectPath, {
1326
1234
  maxCycles: options.maxCycles,
1327
1235
  maxTime: options.maxTime,
1328
- model: options.model,
1329
- stream: true
1330
- // Always use streaming in detached mode so output is captured
1236
+ model: options.model
1331
1237
  });
1332
1238
  try {
1333
1239
  const sessionResult = await createSession(
@@ -1353,7 +1259,6 @@ Searched in: ${(0, import_node_path5.join)(projectPath, ".saga", "worktrees")}`)
1353
1259
  console.log(` Epic: ${storyInfo.epicSlug}`);
1354
1260
  console.log(` Story: ${storyInfo.storySlug}`);
1355
1261
  console.log(` Worktree: ${storyInfo.worktreePath}`);
1356
- console.log(` Streaming: ${stream ? "enabled" : "disabled"}`);
1357
1262
  console.log("");
1358
1263
  const result = await runLoop(
1359
1264
  storyInfo.epicSlug,
@@ -1362,8 +1267,7 @@ Searched in: ${(0, import_node_path5.join)(projectPath, ".saga", "worktrees")}`)
1362
1267
  maxTime,
1363
1268
  model,
1364
1269
  projectPath,
1365
- pluginRoot,
1366
- stream
1270
+ pluginRoot
1367
1271
  );
1368
1272
  console.log(JSON.stringify(result, null, 2));
1369
1273
  if (result.status === "ERROR") {
@@ -2307,16 +2211,14 @@ program.command("init").description("Initialize .saga/ directory structure").opt
2307
2211
  const globalOpts = program.opts();
2308
2212
  await initCommand({ path: globalOpts.path, dryRun: options.dryRun });
2309
2213
  });
2310
- program.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").option("--attached", "Run in attached mode (synchronous, tied to terminal)").action(async (storySlug, options) => {
2214
+ program.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) => {
2311
2215
  const globalOpts = program.opts();
2312
2216
  await implementCommand(storySlug, {
2313
2217
  path: globalOpts.path,
2314
2218
  maxCycles: options.maxCycles,
2315
2219
  maxTime: options.maxTime,
2316
2220
  model: options.model,
2317
- dryRun: options.dryRun,
2318
- stream: options.stream,
2319
- attached: options.attached
2221
+ dryRun: options.dryRun
2320
2222
  });
2321
2223
  });
2322
2224
  program.command("find <query>").description("Find an epic or story by slug/title").option("--type <type>", "Type to search for: epic or story (default: story)").option("--status <status>", "Filter stories by status (e.g., ready, in-progress, completed)").action(async (query, options) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saga-ai/cli",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "CLI for SAGA - Structured Autonomous Goal Achievement",
5
5
  "type": "module",
6
6
  "bin": {