@saga-ai/cli 0.6.0 → 0.7.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 +301 -47
  2. package/package.json +14 -3
package/dist/cli.cjs CHANGED
@@ -25,8 +25,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
 
26
26
  // src/cli.ts
27
27
  var import_commander = require("commander");
28
- var import_node_path7 = require("node:path");
29
- var import_node_fs6 = require("node:fs");
28
+ var import_node_path8 = require("node:path");
29
+ var import_node_fs7 = require("node:fs");
30
30
 
31
31
  // src/commands/init.ts
32
32
  var import_node_path2 = require("node:path");
@@ -193,9 +193,9 @@ async function initCommand(options) {
193
193
  }
194
194
 
195
195
  // src/commands/implement.ts
196
- var import_node_child_process = require("node:child_process");
197
- var import_node_path4 = require("node:path");
198
- var import_node_fs4 = require("node:fs");
196
+ var import_node_child_process2 = require("node:child_process");
197
+ var import_node_path5 = require("node:path");
198
+ var import_node_fs5 = require("node:fs");
199
199
 
200
200
  // src/utils/finder.ts
201
201
  var import_node_fs3 = require("node:fs");
@@ -503,7 +503,7 @@ function findEpic(projectPath, query) {
503
503
  matches: results.map((r) => r.item)
504
504
  };
505
505
  }
506
- async function findStory(projectPath, query) {
506
+ async function findStory(projectPath, query, options = {}) {
507
507
  if (!worktreesDirectoryExists(projectPath) && !epicsDirectoryExists(projectPath)) {
508
508
  return {
509
509
  found: false,
@@ -517,7 +517,16 @@ async function findStory(projectPath, query) {
517
517
  error: `No story found matching '${query}'`
518
518
  };
519
519
  }
520
- const allStories = scannedStories.map(toStoryInfo);
520
+ let allStories = scannedStories.map(toStoryInfo);
521
+ if (options.status) {
522
+ allStories = allStories.filter((story) => story.status === options.status);
523
+ if (allStories.length === 0) {
524
+ return {
525
+ found: false,
526
+ error: `No story found matching '${query}' with status '${options.status}'`
527
+ };
528
+ }
529
+ }
521
530
  const queryNormalized = normalize(query);
522
531
  for (const story of allStories) {
523
532
  if (normalize(story.slug) === queryNormalized) {
@@ -571,6 +580,165 @@ async function findStory(projectPath, query) {
571
580
  };
572
581
  }
573
582
 
583
+ // src/lib/sessions.ts
584
+ var import_node_child_process = require("node:child_process");
585
+ var import_node_fs4 = require("node:fs");
586
+ var import_node_path4 = require("node:path");
587
+ var OUTPUT_DIR = "/tmp/saga-sessions";
588
+ function shellEscape(str) {
589
+ return "'" + str.replace(/'/g, "'\\''") + "'";
590
+ }
591
+ function shellEscapeArgs(args) {
592
+ return args.map(shellEscape).join(" ");
593
+ }
594
+ function validateSlug(slug) {
595
+ if (!slug || slug.length === 0) {
596
+ return false;
597
+ }
598
+ if (!/^[a-z0-9-]+$/.test(slug)) {
599
+ return false;
600
+ }
601
+ if (slug.startsWith("-") || slug.endsWith("-")) {
602
+ return false;
603
+ }
604
+ return true;
605
+ }
606
+ function checkTmuxAvailable() {
607
+ const result = (0, import_node_child_process.spawnSync)("which", ["tmux"], { encoding: "utf-8" });
608
+ if (result.status !== 0) {
609
+ throw new Error("tmux is not installed or not found in PATH");
610
+ }
611
+ }
612
+ async function createSession(epicSlug, storySlug, command) {
613
+ if (!validateSlug(epicSlug)) {
614
+ throw new Error(`Invalid epic slug: '${epicSlug}'. Must contain only [a-z0-9-] and not start/end with hyphen.`);
615
+ }
616
+ if (!validateSlug(storySlug)) {
617
+ throw new Error(`Invalid story slug: '${storySlug}'. Must contain only [a-z0-9-] and not start/end with hyphen.`);
618
+ }
619
+ checkTmuxAvailable();
620
+ if (!(0, import_node_fs4.existsSync)(OUTPUT_DIR)) {
621
+ (0, import_node_fs4.mkdirSync)(OUTPUT_DIR, { recursive: true });
622
+ }
623
+ const timestamp = Date.now();
624
+ const sessionName = `saga-${epicSlug}-${storySlug}-${timestamp}`;
625
+ const outputFile = (0, import_node_path4.join)(OUTPUT_DIR, `${sessionName}.out`);
626
+ const commandFilePath = (0, import_node_path4.join)(OUTPUT_DIR, `${sessionName}.cmd`);
627
+ const wrapperScriptPath = (0, import_node_path4.join)(OUTPUT_DIR, `${sessionName}.sh`);
628
+ (0, import_node_fs4.writeFileSync)(commandFilePath, command, { mode: 384 });
629
+ const wrapperScriptContent = `#!/bin/bash
630
+ # Auto-generated wrapper script for SAGA session
631
+ set -e
632
+
633
+ COMMAND_FILE="${commandFilePath}"
634
+ OUTPUT_FILE="${outputFile}"
635
+ SCRIPT_FILE="${wrapperScriptPath}"
636
+
637
+ # Read the command from file
638
+ COMMAND="$(cat "$COMMAND_FILE")"
639
+
640
+ # Cleanup temporary files on exit
641
+ cleanup() {
642
+ rm -f "$COMMAND_FILE" "$SCRIPT_FILE"
643
+ }
644
+ trap cleanup EXIT
645
+
646
+ # Execute the command with output capture
647
+ # script syntax differs between macOS and Linux:
648
+ # macOS (Darwin): script -q <file> <shell> -c <command>
649
+ # Linux: script -q -c <command> <file>
650
+ if [[ "$(uname)" == "Darwin" ]]; then
651
+ # -F: flush output after each write (ensures immediate visibility)
652
+ exec script -qF "$OUTPUT_FILE" /bin/bash -c "$COMMAND"
653
+ else
654
+ exec script -q -c "$COMMAND" "$OUTPUT_FILE"
655
+ fi
656
+ `;
657
+ (0, import_node_fs4.writeFileSync)(wrapperScriptPath, wrapperScriptContent, { mode: 448 });
658
+ const createResult = (0, import_node_child_process.spawnSync)("tmux", [
659
+ "new-session",
660
+ "-d",
661
+ // detached
662
+ "-s",
663
+ sessionName,
664
+ // session name
665
+ wrapperScriptPath
666
+ // run the wrapper script
667
+ ], { encoding: "utf-8" });
668
+ if (createResult.status !== 0) {
669
+ throw new Error(`Failed to create tmux session: ${createResult.stderr || "unknown error"}`);
670
+ }
671
+ return {
672
+ sessionName,
673
+ outputFile
674
+ };
675
+ }
676
+ async function listSessions() {
677
+ const result = (0, import_node_child_process.spawnSync)("tmux", ["ls"], { encoding: "utf-8" });
678
+ if (result.status !== 0) {
679
+ return [];
680
+ }
681
+ const sessions = [];
682
+ const lines = result.stdout.trim().split("\n");
683
+ for (const line of lines) {
684
+ const match = line.match(/^(saga-[a-z0-9]+(?:-[a-z0-9]+)*-\d+):/);
685
+ if (match) {
686
+ const name = match[1];
687
+ sessions.push({
688
+ name,
689
+ status: "running",
690
+ // If it shows up in tmux ls, it's running
691
+ outputFile: (0, import_node_path4.join)(OUTPUT_DIR, `${name}.out`)
692
+ });
693
+ }
694
+ }
695
+ return sessions;
696
+ }
697
+ async function getSessionStatus(sessionName) {
698
+ const result = (0, import_node_child_process.spawnSync)("tmux", ["has-session", "-t", sessionName], {
699
+ encoding: "utf-8"
700
+ });
701
+ return {
702
+ running: result.status === 0
703
+ };
704
+ }
705
+ async function streamLogs(sessionName) {
706
+ const outputFile = (0, import_node_path4.join)(OUTPUT_DIR, `${sessionName}.out`);
707
+ if (!(0, import_node_fs4.existsSync)(outputFile)) {
708
+ throw new Error(`Output file not found: ${outputFile}`);
709
+ }
710
+ return new Promise((resolve2, reject) => {
711
+ const child = (0, import_node_child_process.spawn)("tail", ["-f", outputFile], {
712
+ stdio: ["ignore", "pipe", "pipe"]
713
+ });
714
+ child.stdout?.on("data", (chunk) => {
715
+ process.stdout.write(chunk);
716
+ });
717
+ child.stderr?.on("data", (chunk) => {
718
+ process.stderr.write(chunk);
719
+ });
720
+ child.on("error", (err) => {
721
+ reject(new Error(`Failed to stream logs: ${err.message}`));
722
+ });
723
+ child.on("close", (code) => {
724
+ resolve2();
725
+ });
726
+ const sigintHandler = () => {
727
+ child.kill("SIGTERM");
728
+ process.removeListener("SIGINT", sigintHandler);
729
+ };
730
+ process.on("SIGINT", sigintHandler);
731
+ });
732
+ }
733
+ async function killSession(sessionName) {
734
+ const result = (0, import_node_child_process.spawnSync)("tmux", ["kill-session", "-t", sessionName], {
735
+ encoding: "utf-8"
736
+ });
737
+ return {
738
+ killed: result.status === 0
739
+ };
740
+ }
741
+
574
742
  // src/commands/implement.ts
575
743
  var DEFAULT_MAX_CYCLES = 10;
576
744
  var DEFAULT_MAX_TIME = 60;
@@ -608,10 +776,10 @@ async function findStory2(projectPath, storySlug) {
608
776
  };
609
777
  }
610
778
  function computeStoryPath(worktree, epicSlug, storySlug) {
611
- return (0, import_node_path4.join)(worktree, ".saga", "epics", epicSlug, "stories", storySlug, "story.md");
779
+ return (0, import_node_path5.join)(worktree, ".saga", "epics", epicSlug, "stories", storySlug, "story.md");
612
780
  }
613
781
  function validateStoryFiles(worktree, epicSlug, storySlug) {
614
- if (!(0, import_node_fs4.existsSync)(worktree)) {
782
+ if (!(0, import_node_fs5.existsSync)(worktree)) {
615
783
  return {
616
784
  valid: false,
617
785
  error: `Worktree not found at ${worktree}
@@ -624,7 +792,7 @@ To create the worktree, use: /task-resume ${storySlug}`
624
792
  };
625
793
  }
626
794
  const storyPath = computeStoryPath(worktree, epicSlug, storySlug);
627
- if (!(0, import_node_fs4.existsSync)(storyPath)) {
795
+ if (!(0, import_node_fs5.existsSync)(storyPath)) {
628
796
  return {
629
797
  valid: false,
630
798
  error: `story.md not found in worktree.
@@ -638,11 +806,11 @@ This may indicate an incomplete story setup.`
638
806
  return { valid: true };
639
807
  }
640
808
  function getSkillRoot(pluginRoot) {
641
- return (0, import_node_path4.join)(pluginRoot, "skills", "execute-story");
809
+ return (0, import_node_path5.join)(pluginRoot, "skills", "execute-story");
642
810
  }
643
811
  function checkCommandExists(command) {
644
812
  try {
645
- const result = (0, import_node_child_process.spawnSync)("which", [command], { encoding: "utf-8" });
813
+ const result = (0, import_node_child_process2.spawnSync)("which", [command], { encoding: "utf-8" });
646
814
  if (result.status === 0 && result.stdout.trim()) {
647
815
  return { exists: true, path: result.stdout.trim() };
648
816
  }
@@ -678,8 +846,8 @@ function runDryRun(storyInfo, projectPath, pluginRoot) {
678
846
  if (!claudeCheck.exists) allPassed = false;
679
847
  if (pluginRoot) {
680
848
  const skillRoot = getSkillRoot(pluginRoot);
681
- const workerPromptPath = (0, import_node_path4.join)(skillRoot, WORKER_PROMPT_RELATIVE);
682
- if ((0, import_node_fs4.existsSync)(workerPromptPath)) {
849
+ const workerPromptPath = (0, import_node_path5.join)(skillRoot, WORKER_PROMPT_RELATIVE);
850
+ if ((0, import_node_fs5.existsSync)(workerPromptPath)) {
683
851
  checks.push({
684
852
  name: "Worker prompt",
685
853
  path: workerPromptPath,
@@ -700,7 +868,7 @@ function runDryRun(storyInfo, projectPath, pluginRoot) {
700
868
  path: `${storyInfo.storySlug} (epic: ${storyInfo.epicSlug})`,
701
869
  passed: true
702
870
  });
703
- if ((0, import_node_fs4.existsSync)(storyInfo.worktreePath)) {
871
+ if ((0, import_node_fs5.existsSync)(storyInfo.worktreePath)) {
704
872
  checks.push({
705
873
  name: "Worktree exists",
706
874
  path: storyInfo.worktreePath,
@@ -715,13 +883,13 @@ function runDryRun(storyInfo, projectPath, pluginRoot) {
715
883
  });
716
884
  allPassed = false;
717
885
  }
718
- if ((0, import_node_fs4.existsSync)(storyInfo.worktreePath)) {
886
+ if ((0, import_node_fs5.existsSync)(storyInfo.worktreePath)) {
719
887
  const storyMdPath = computeStoryPath(
720
888
  storyInfo.worktreePath,
721
889
  storyInfo.epicSlug,
722
890
  storyInfo.storySlug
723
891
  );
724
- if ((0, import_node_fs4.existsSync)(storyMdPath)) {
892
+ if ((0, import_node_fs5.existsSync)(storyMdPath)) {
725
893
  checks.push({
726
894
  name: "story.md in worktree",
727
895
  path: storyMdPath,
@@ -771,11 +939,11 @@ function printDryRunResults(result) {
771
939
  }
772
940
  function loadWorkerPrompt(pluginRoot) {
773
941
  const skillRoot = getSkillRoot(pluginRoot);
774
- const promptPath = (0, import_node_path4.join)(skillRoot, WORKER_PROMPT_RELATIVE);
775
- if (!(0, import_node_fs4.existsSync)(promptPath)) {
942
+ const promptPath = (0, import_node_path5.join)(skillRoot, WORKER_PROMPT_RELATIVE);
943
+ if (!(0, import_node_fs5.existsSync)(promptPath)) {
776
944
  throw new Error(`Worker prompt not found at ${promptPath}`);
777
945
  }
778
- return (0, import_node_fs4.readFileSync)(promptPath, "utf-8");
946
+ return (0, import_node_fs5.readFileSync)(promptPath, "utf-8");
779
947
  }
780
948
  function buildScopeSettings() {
781
949
  const hookCommand = "npx @saga-ai/cli scope-validator";
@@ -837,7 +1005,7 @@ function spawnWorker(prompt, model, settings, workingDir) {
837
1005
  JSON.stringify(settings),
838
1006
  "--dangerously-skip-permissions"
839
1007
  ];
840
- const result = (0, import_node_child_process.spawnSync)("claude", args, {
1008
+ const result = (0, import_node_child_process2.spawnSync)("claude", args, {
841
1009
  cwd: workingDir,
842
1010
  encoding: "utf-8",
843
1011
  maxBuffer: 50 * 1024 * 1024
@@ -921,7 +1089,7 @@ function spawnWorkerAsync(prompt, model, settings, workingDir) {
921
1089
  JSON.stringify(settings),
922
1090
  "--dangerously-skip-permissions"
923
1091
  ];
924
- const child = (0, import_node_child_process.spawn)("claude", args, {
1092
+ const child = (0, import_node_child_process2.spawn)("claude", args, {
925
1093
  cwd: workingDir,
926
1094
  stdio: ["ignore", "pipe", "pipe"]
927
1095
  });
@@ -956,7 +1124,7 @@ function spawnWorkerAsync(prompt, model, settings, workingDir) {
956
1124
  });
957
1125
  }
958
1126
  async function runLoop(epicSlug, storySlug, maxCycles, maxTime, model, projectDir, pluginRoot, stream = false) {
959
- const worktree = (0, import_node_path4.join)(projectDir, ".saga", "worktrees", epicSlug, storySlug);
1127
+ const worktree = (0, import_node_path5.join)(projectDir, ".saga", "worktrees", epicSlug, storySlug);
960
1128
  const validation = validateStoryFiles(worktree, epicSlug, storySlug);
961
1129
  if (!validation.valid) {
962
1130
  return {
@@ -1072,6 +1240,23 @@ async function runLoop(epicSlug, storySlug, maxCycles, maxTime, model, projectDi
1072
1240
  storySlug
1073
1241
  };
1074
1242
  }
1243
+ function buildDetachedCommand(storySlug, projectPath, options) {
1244
+ const parts = ["saga", "implement", storySlug, "--attached"];
1245
+ parts.push("--path", projectPath);
1246
+ if (options.maxCycles !== void 0) {
1247
+ parts.push("--max-cycles", String(options.maxCycles));
1248
+ }
1249
+ if (options.maxTime !== void 0) {
1250
+ parts.push("--max-time", String(options.maxTime));
1251
+ }
1252
+ if (options.model !== void 0) {
1253
+ parts.push("--model", options.model);
1254
+ }
1255
+ if (options.stream) {
1256
+ parts.push("--stream");
1257
+ }
1258
+ return shellEscapeArgs(parts);
1259
+ }
1075
1260
  async function implementCommand(storySlug, options) {
1076
1261
  let projectPath;
1077
1262
  try {
@@ -1084,7 +1269,7 @@ async function implementCommand(storySlug, options) {
1084
1269
  if (!storyInfo) {
1085
1270
  console.error(`Error: Story '${storySlug}' not found in SAGA project.`);
1086
1271
  console.error(`
1087
- Searched in: ${(0, import_node_path4.join)(projectPath, ".saga", "worktrees")}`);
1272
+ Searched in: ${(0, import_node_path5.join)(projectPath, ".saga", "worktrees")}`);
1088
1273
  console.error("\nMake sure the story worktree exists and has a story.md file.");
1089
1274
  console.error("Run /generate-stories to create story worktrees.");
1090
1275
  process.exit(1);
@@ -1095,7 +1280,7 @@ Searched in: ${(0, import_node_path4.join)(projectPath, ".saga", "worktrees")}`)
1095
1280
  printDryRunResults(dryRunResult);
1096
1281
  process.exit(dryRunResult.success ? 0 : 1);
1097
1282
  }
1098
- if (!(0, import_node_fs4.existsSync)(storyInfo.worktreePath)) {
1283
+ if (!(0, import_node_fs5.existsSync)(storyInfo.worktreePath)) {
1099
1284
  console.error(`Error: Worktree not found at ${storyInfo.worktreePath}`);
1100
1285
  console.error("\nThe story worktree has not been created yet.");
1101
1286
  console.error("Make sure the story was properly generated with /generate-stories.");
@@ -1113,6 +1298,38 @@ Searched in: ${(0, import_node_path4.join)(projectPath, ".saga", "worktrees")}`)
1113
1298
  const maxTime = options.maxTime ?? DEFAULT_MAX_TIME;
1114
1299
  const model = options.model ?? DEFAULT_MODEL;
1115
1300
  const stream = options.stream ?? false;
1301
+ const attached = options.attached ?? false;
1302
+ if (!attached) {
1303
+ if (stream) {
1304
+ console.error("Warning: --stream is ignored in detached mode. Use --attached --stream for streaming output.");
1305
+ }
1306
+ const detachedCommand = buildDetachedCommand(storySlug, projectPath, {
1307
+ maxCycles: options.maxCycles,
1308
+ maxTime: options.maxTime,
1309
+ model: options.model,
1310
+ stream: true
1311
+ // Always use streaming in detached mode so output is captured
1312
+ });
1313
+ try {
1314
+ const sessionResult = await createSession(
1315
+ storyInfo.epicSlug,
1316
+ storyInfo.storySlug,
1317
+ detachedCommand
1318
+ );
1319
+ console.log(JSON.stringify({
1320
+ mode: "detached",
1321
+ sessionName: sessionResult.sessionName,
1322
+ outputFile: sessionResult.outputFile,
1323
+ epicSlug: storyInfo.epicSlug,
1324
+ storySlug: storyInfo.storySlug,
1325
+ worktreePath: storyInfo.worktreePath
1326
+ }, null, 2));
1327
+ return;
1328
+ } catch (error) {
1329
+ console.error(`Error: Failed to create detached session: ${error.message}`);
1330
+ process.exit(1);
1331
+ }
1332
+ }
1116
1333
  console.log("Starting story implementation...");
1117
1334
  console.log(` Epic: ${storyInfo.epicSlug}`);
1118
1335
  console.log(` Story: ${storyInfo.storySlug}`);
@@ -1194,7 +1411,7 @@ function validateTaskStatus(status) {
1194
1411
  return "pending";
1195
1412
  }
1196
1413
  async function parseStory(storyPath, epicSlug) {
1197
- const { join: join11 } = await import("path");
1414
+ const { join: join12 } = await import("path");
1198
1415
  const { stat: stat2 } = await import("fs/promises");
1199
1416
  let content;
1200
1417
  try {
@@ -1214,7 +1431,7 @@ async function parseStory(storyPath, epicSlug) {
1214
1431
  const title = frontmatter.title || dirName;
1215
1432
  const status = validateStatus(frontmatter.status);
1216
1433
  const tasks = parseTasks(frontmatter.tasks);
1217
- const journalPath = join11(storyDir, "journal.md");
1434
+ const journalPath = join12(storyDir, "journal.md");
1218
1435
  let hasJournal = false;
1219
1436
  try {
1220
1437
  await stat2(journalPath);
@@ -1804,7 +2021,7 @@ async function dashboardCommand(options) {
1804
2021
  }
1805
2022
 
1806
2023
  // src/commands/scope-validator.ts
1807
- var import_node_path5 = require("node:path");
2024
+ var import_node_path6 = require("node:path");
1808
2025
  function getFilePathFromInput(hookInput) {
1809
2026
  try {
1810
2027
  const data = JSON.parse(hookInput);
@@ -1824,10 +2041,10 @@ function isArchiveAccess(path) {
1824
2041
  return path.includes(".saga/archive");
1825
2042
  }
1826
2043
  function isWithinWorktree(filePath, worktreePath) {
1827
- const absoluteFilePath = (0, import_node_path5.resolve)(filePath);
1828
- const absoluteWorktree = (0, import_node_path5.resolve)(worktreePath);
1829
- const relativePath = (0, import_node_path5.relative)(absoluteWorktree, absoluteFilePath);
1830
- if (relativePath.startsWith("..") || (0, import_node_path5.resolve)(relativePath) === relativePath) {
2044
+ const absoluteFilePath = (0, import_node_path6.resolve)(filePath);
2045
+ const absoluteWorktree = (0, import_node_path6.resolve)(worktreePath);
2046
+ const relativePath = (0, import_node_path6.relative)(absoluteWorktree, absoluteFilePath);
2047
+ if (relativePath.startsWith("..") || (0, import_node_path6.resolve)(relativePath) === relativePath) {
1831
2048
  return false;
1832
2049
  }
1833
2050
  return true;
@@ -1933,7 +2150,7 @@ async function findCommand(query, options) {
1933
2150
  if (type === "epic") {
1934
2151
  result = findEpic(projectPath, query);
1935
2152
  } else {
1936
- result = await findStory(projectPath, query);
2153
+ result = await findStory(projectPath, query, { status: options.status });
1937
2154
  }
1938
2155
  console.log(JSON.stringify(result, null, 2));
1939
2156
  if (!result.found) {
@@ -1942,12 +2159,12 @@ async function findCommand(query, options) {
1942
2159
  }
1943
2160
 
1944
2161
  // src/commands/worktree.ts
1945
- var import_node_path6 = require("node:path");
1946
- var import_node_fs5 = require("node:fs");
1947
- var import_node_child_process2 = require("node:child_process");
2162
+ var import_node_path7 = require("node:path");
2163
+ var import_node_fs6 = require("node:fs");
2164
+ var import_node_child_process3 = require("node:child_process");
1948
2165
  function runGitCommand(args, cwd) {
1949
2166
  try {
1950
- const output = (0, import_node_child_process2.execSync)(`git ${args.join(" ")}`, {
2167
+ const output = (0, import_node_child_process3.execSync)(`git ${args.join(" ")}`, {
1951
2168
  cwd,
1952
2169
  encoding: "utf-8",
1953
2170
  stdio: ["pipe", "pipe", "pipe"]
@@ -1974,7 +2191,7 @@ function getMainBranch(cwd) {
1974
2191
  }
1975
2192
  function createWorktree(projectPath, epicSlug, storySlug) {
1976
2193
  const branchName = `story-${storySlug}-epic-${epicSlug}`;
1977
- const worktreePath = (0, import_node_path6.join)(
2194
+ const worktreePath = (0, import_node_path7.join)(
1978
2195
  projectPath,
1979
2196
  ".saga",
1980
2197
  "worktrees",
@@ -1987,7 +2204,7 @@ function createWorktree(projectPath, epicSlug, storySlug) {
1987
2204
  error: `Branch already exists: ${branchName}`
1988
2205
  };
1989
2206
  }
1990
- if ((0, import_node_fs5.existsSync)(worktreePath)) {
2207
+ if ((0, import_node_fs6.existsSync)(worktreePath)) {
1991
2208
  return {
1992
2209
  success: false,
1993
2210
  error: `Worktree directory already exists: ${worktreePath}`
@@ -2005,8 +2222,8 @@ function createWorktree(projectPath, epicSlug, storySlug) {
2005
2222
  error: `Failed to create branch: ${createBranchResult.output}`
2006
2223
  };
2007
2224
  }
2008
- const worktreeParent = (0, import_node_path6.join)(projectPath, ".saga", "worktrees", epicSlug);
2009
- (0, import_node_fs5.mkdirSync)(worktreeParent, { recursive: true });
2225
+ const worktreeParent = (0, import_node_path7.join)(projectPath, ".saga", "worktrees", epicSlug);
2226
+ (0, import_node_fs6.mkdirSync)(worktreeParent, { recursive: true });
2010
2227
  const createWorktreeResult = runGitCommand(
2011
2228
  ["worktree", "add", worktreePath, branchName],
2012
2229
  projectPath
@@ -2039,9 +2256,31 @@ async function worktreeCommand(epicSlug, storySlug, options) {
2039
2256
  }
2040
2257
  }
2041
2258
 
2259
+ // src/commands/sessions/index.ts
2260
+ async function sessionsListCommand() {
2261
+ const sessions = await listSessions();
2262
+ console.log(JSON.stringify(sessions, null, 2));
2263
+ }
2264
+ async function sessionsStatusCommand(sessionName) {
2265
+ const status = await getSessionStatus(sessionName);
2266
+ console.log(JSON.stringify(status, null, 2));
2267
+ }
2268
+ async function sessionsLogsCommand(sessionName) {
2269
+ try {
2270
+ await streamLogs(sessionName);
2271
+ } catch (error) {
2272
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
2273
+ process.exit(1);
2274
+ }
2275
+ }
2276
+ async function sessionsKillCommand(sessionName) {
2277
+ const result = await killSession(sessionName);
2278
+ console.log(JSON.stringify(result, null, 2));
2279
+ }
2280
+
2042
2281
  // src/cli.ts
2043
- var packageJsonPath = (0, import_node_path7.join)(__dirname, "..", "package.json");
2044
- var packageJson = JSON.parse((0, import_node_fs6.readFileSync)(packageJsonPath, "utf-8"));
2282
+ var packageJsonPath = (0, import_node_path8.join)(__dirname, "..", "package.json");
2283
+ var packageJson = JSON.parse((0, import_node_fs7.readFileSync)(packageJsonPath, "utf-8"));
2045
2284
  var program = new import_commander.Command();
2046
2285
  program.name("saga").description("CLI for SAGA - Structured Autonomous Goal Achievement").version(packageJson.version).addHelpCommand("help [command]", "Display help for a command");
2047
2286
  program.option("-p, --path <dir>", "Path to SAGA project directory (overrides auto-discovery)");
@@ -2049,7 +2288,7 @@ program.command("init").description("Initialize .saga/ directory structure").opt
2049
2288
  const globalOpts = program.opts();
2050
2289
  await initCommand({ path: globalOpts.path, dryRun: options.dryRun });
2051
2290
  });
2052
- 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").action(async (storySlug, options) => {
2291
+ 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) => {
2053
2292
  const globalOpts = program.opts();
2054
2293
  await implementCommand(storySlug, {
2055
2294
  path: globalOpts.path,
@@ -2057,14 +2296,16 @@ program.command("implement <story-slug>").description("Run story implementation"
2057
2296
  maxTime: options.maxTime,
2058
2297
  model: options.model,
2059
2298
  dryRun: options.dryRun,
2060
- stream: options.stream
2299
+ stream: options.stream,
2300
+ attached: options.attached
2061
2301
  });
2062
2302
  });
2063
- 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)").action(async (query, options) => {
2303
+ 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) => {
2064
2304
  const globalOpts = program.opts();
2065
2305
  await findCommand(query, {
2066
2306
  path: globalOpts.path,
2067
- type: options.type
2307
+ type: options.type,
2308
+ status: options.status
2068
2309
  });
2069
2310
  });
2070
2311
  program.command("worktree <epic-slug> <story-slug>").description("Create git worktree for a story").action(async (epicSlug, storySlug) => {
@@ -2081,6 +2322,19 @@ program.command("dashboard").description("Start the dashboard server").option("-
2081
2322
  program.command("scope-validator").description("Validate file operations against story scope (internal)").action(async () => {
2082
2323
  await scopeValidatorCommand();
2083
2324
  });
2325
+ var sessionsCommand = program.command("sessions").description("Manage SAGA tmux sessions");
2326
+ sessionsCommand.command("list").description("List all SAGA sessions").action(async () => {
2327
+ await sessionsListCommand();
2328
+ });
2329
+ sessionsCommand.command("status <name>").description("Show session status").action(async (name) => {
2330
+ await sessionsStatusCommand(name);
2331
+ });
2332
+ sessionsCommand.command("logs <name>").description("Stream session output").action(async (name) => {
2333
+ await sessionsLogsCommand(name);
2334
+ });
2335
+ sessionsCommand.command("kill <name>").description("Terminate a session").action(async (name) => {
2336
+ await sessionsKillCommand(name);
2337
+ });
2084
2338
  program.on("command:*", (operands) => {
2085
2339
  console.error(`error: unknown command '${operands[0]}'`);
2086
2340
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saga-ai/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "CLI for SAGA - Structured Autonomous Goal Achievement",
5
5
  "type": "module",
6
6
  "bin": {
@@ -60,7 +60,16 @@
60
60
  "typescript": "^5.7.0",
61
61
  "vite": "^6.0.5",
62
62
  "vitest": "^3.2.4",
63
- "xstate": "^5.26.0"
63
+ "xstate": "^5.26.0",
64
+ "storybook": "^10.2.1",
65
+ "@storybook/react-vite": "^10.2.1",
66
+ "@chromatic-com/storybook": "^5.0.0",
67
+ "@storybook/addon-vitest": "^10.2.1",
68
+ "@storybook/addon-a11y": "^10.2.1",
69
+ "@storybook/addon-docs": "^10.2.1",
70
+ "@storybook/addon-onboarding": "^10.2.1",
71
+ "playwright": "^1.58.0",
72
+ "@vitest/browser": "^3.2.4"
64
73
  },
65
74
  "engines": {
66
75
  "node": ">=18.0.0"
@@ -77,6 +86,8 @@
77
86
  "dev:client": "vite --config src/client/vite.config.ts",
78
87
  "test": "vitest run",
79
88
  "test:watch": "vitest",
80
- "publish:npm": "pnpm build && pnpm test && pnpm publish --access public"
89
+ "publish:npm": "pnpm build && pnpm test && pnpm publish --access public",
90
+ "storybook": "storybook dev -p 6006 -c src/client/.storybook",
91
+ "build-storybook": "storybook build -c src/client/.storybook -o storybook-static"
81
92
  }
82
93
  }