@saga-ai/cli 0.7.1 → 0.8.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.
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") {
@@ -1374,6 +1278,7 @@ Searched in: ${(0, import_node_path5.join)(projectPath, ".saga", "worktrees")}`)
1374
1278
  // src/server/index.ts
1375
1279
  var import_express2 = __toESM(require("express"), 1);
1376
1280
  var import_http = require("http");
1281
+ var import_path6 = require("path");
1377
1282
 
1378
1283
  // src/server/routes.ts
1379
1284
  var import_express = require("express");
@@ -1430,7 +1335,7 @@ function validateTaskStatus(status) {
1430
1335
  return "pending";
1431
1336
  }
1432
1337
  async function parseStory(storyPath, epicSlug) {
1433
- const { join: join12 } = await import("path");
1338
+ const { join: join13 } = await import("path");
1434
1339
  const { stat: stat2 } = await import("fs/promises");
1435
1340
  let content;
1436
1341
  try {
@@ -1450,7 +1355,7 @@ async function parseStory(storyPath, epicSlug) {
1450
1355
  const title = frontmatter.title || dirName;
1451
1356
  const status = validateStatus(frontmatter.status);
1452
1357
  const tasks = parseTasks(frontmatter.tasks);
1453
- const journalPath = join12(storyDir, "journal.md");
1358
+ const journalPath = join13(storyDir, "journal.md");
1454
1359
  let hasJournal = false;
1455
1360
  try {
1456
1361
  await stat2(journalPath);
@@ -1976,6 +1881,11 @@ function createApp(sagaRoot) {
1976
1881
  res.json({ status: "ok" });
1977
1882
  });
1978
1883
  app.use("/api", createApiRouter(sagaRoot));
1884
+ const clientDistPath = (0, import_path6.join)(__dirname, "client");
1885
+ app.use(import_express2.default.static(clientDistPath));
1886
+ app.get("/{*splat}", (_req, res) => {
1887
+ res.sendFile((0, import_path6.join)(clientDistPath, "index.html"));
1888
+ });
1979
1889
  return app;
1980
1890
  }
1981
1891
  async function startServer(config) {
@@ -2307,16 +2217,14 @@ program.command("init").description("Initialize .saga/ directory structure").opt
2307
2217
  const globalOpts = program.opts();
2308
2218
  await initCommand({ path: globalOpts.path, dryRun: options.dryRun });
2309
2219
  });
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) => {
2220
+ 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
2221
  const globalOpts = program.opts();
2312
2222
  await implementCommand(storySlug, {
2313
2223
  path: globalOpts.path,
2314
2224
  maxCycles: options.maxCycles,
2315
2225
  maxTime: options.maxTime,
2316
2226
  model: options.model,
2317
- dryRun: options.dryRun,
2318
- stream: options.stream,
2319
- attached: options.attached
2227
+ dryRun: options.dryRun
2320
2228
  });
2321
2229
  });
2322
2230
  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) => {