@saga-ai/cli 2.12.1 → 2.13.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/dist/cli.cjs CHANGED
@@ -583,6 +583,7 @@ async function findStory(projectPath, query, options = {}) {
583
583
  // src/lib/sessions.ts
584
584
  var import_node_child_process = require("node:child_process");
585
585
  var import_node_fs4 = require("node:fs");
586
+ var import_promises2 = require("node:fs/promises");
586
587
  var import_node_path4 = require("node:path");
587
588
  var OUTPUT_DIR = "/tmp/saga-sessions";
588
589
  function shellEscape(str) {
@@ -741,6 +742,70 @@ async function killSession(sessionName) {
741
742
  killed: result.status === 0
742
743
  };
743
744
  }
745
+ function parseSessionName(name) {
746
+ if (!name || !name.startsWith("saga__")) {
747
+ return null;
748
+ }
749
+ const parts = name.split("__");
750
+ if (parts.length !== 4) {
751
+ return null;
752
+ }
753
+ const [, epicSlug, storySlug, pid] = parts;
754
+ if (!epicSlug || !storySlug || !pid) {
755
+ return null;
756
+ }
757
+ return {
758
+ epicSlug,
759
+ storySlug
760
+ };
761
+ }
762
+ async function buildSessionInfo(name, status) {
763
+ const parsed = parseSessionName(name);
764
+ if (!parsed) {
765
+ return null;
766
+ }
767
+ const outputFile = (0, import_node_path4.join)(OUTPUT_DIR, `${name}.out`);
768
+ const outputAvailable = (0, import_node_fs4.existsSync)(outputFile);
769
+ let startTime = /* @__PURE__ */ new Date();
770
+ let endTime;
771
+ let outputPreview;
772
+ if (outputAvailable) {
773
+ try {
774
+ const stats = await (0, import_promises2.stat)(outputFile);
775
+ startTime = stats.birthtime;
776
+ if (status === "completed") {
777
+ endTime = stats.mtime;
778
+ }
779
+ } catch {
780
+ }
781
+ try {
782
+ const content = await (0, import_promises2.readFile)(outputFile, "utf-8");
783
+ const lines = content.split("\n").filter((line) => line.length > 0);
784
+ if (lines.length > 0) {
785
+ const lastLines = lines.slice(-5);
786
+ let preview = lastLines.join("\n");
787
+ if (preview.length > 500) {
788
+ const truncated = preview.slice(0, 500);
789
+ const lastNewline = truncated.lastIndexOf("\n");
790
+ preview = lastNewline > 0 ? truncated.slice(0, lastNewline) : truncated;
791
+ }
792
+ outputPreview = preview;
793
+ }
794
+ } catch {
795
+ }
796
+ }
797
+ return {
798
+ name,
799
+ epicSlug: parsed.epicSlug,
800
+ storySlug: parsed.storySlug,
801
+ status,
802
+ outputFile,
803
+ outputAvailable,
804
+ startTime,
805
+ endTime,
806
+ outputPreview
807
+ };
808
+ }
744
809
 
745
810
  // src/commands/implement.ts
746
811
  var DEFAULT_MAX_CYCLES = 10;
@@ -1276,21 +1341,21 @@ Searched in: ${(0, import_node_path5.join)(projectPath, ".saga", "worktrees")}`)
1276
1341
  }
1277
1342
 
1278
1343
  // src/server/index.ts
1279
- var import_express2 = __toESM(require("express"), 1);
1344
+ var import_express3 = __toESM(require("express"), 1);
1280
1345
  var import_http = require("http");
1281
1346
  var import_path6 = require("path");
1282
1347
 
1283
1348
  // src/server/routes.ts
1284
- var import_express = require("express");
1349
+ var import_express2 = require("express");
1285
1350
 
1286
1351
  // src/server/parser.ts
1287
- var import_promises2 = require("fs/promises");
1352
+ var import_promises3 = require("fs/promises");
1288
1353
  var import_path2 = require("path");
1289
1354
  var import_gray_matter2 = __toESM(require("gray-matter"), 1);
1290
1355
  async function toStoryDetail(story, sagaRoot) {
1291
1356
  let tasks = [];
1292
1357
  try {
1293
- const content = await (0, import_promises2.readFile)(story.storyPath, "utf-8");
1358
+ const content = await (0, import_promises3.readFile)(story.storyPath, "utf-8");
1294
1359
  const parsed = (0, import_gray_matter2.default)(content);
1295
1360
  tasks = parseTasks(parsed.data.tasks);
1296
1361
  } catch {
@@ -1336,10 +1401,10 @@ function validateTaskStatus(status) {
1336
1401
  }
1337
1402
  async function parseStory(storyPath, epicSlug) {
1338
1403
  const { join: join13 } = await import("path");
1339
- const { stat: stat2 } = await import("fs/promises");
1404
+ const { stat: stat3 } = await import("fs/promises");
1340
1405
  let content;
1341
1406
  try {
1342
- content = await (0, import_promises2.readFile)(storyPath, "utf-8");
1407
+ content = await (0, import_promises3.readFile)(storyPath, "utf-8");
1343
1408
  } catch {
1344
1409
  return null;
1345
1410
  }
@@ -1358,7 +1423,7 @@ async function parseStory(storyPath, epicSlug) {
1358
1423
  const journalPath = join13(storyDir, "journal.md");
1359
1424
  let hasJournal = false;
1360
1425
  try {
1361
- await stat2(journalPath);
1426
+ await stat3(journalPath);
1362
1427
  hasJournal = true;
1363
1428
  } catch {
1364
1429
  }
@@ -1376,7 +1441,7 @@ async function parseStory(storyPath, epicSlug) {
1376
1441
  }
1377
1442
  async function parseJournal(journalPath) {
1378
1443
  try {
1379
- const content = await (0, import_promises2.readFile)(journalPath, "utf-8");
1444
+ const content = await (0, import_promises3.readFile)(journalPath, "utf-8");
1380
1445
  const entries = [];
1381
1446
  const sections = content.split(/^##\s+/m).slice(1);
1382
1447
  for (const section of sections) {
@@ -1455,6 +1520,142 @@ async function scanSagaDirectory(sagaRoot) {
1455
1520
 
1456
1521
  // src/server/routes.ts
1457
1522
  var import_path3 = require("path");
1523
+
1524
+ // src/server/session-routes.ts
1525
+ var import_express = require("express");
1526
+
1527
+ // src/lib/session-polling.ts
1528
+ var POLLING_INTERVAL_MS = 3e3;
1529
+ var pollingInterval = null;
1530
+ var currentSessions = [];
1531
+ var isFirstPoll = true;
1532
+ function getCurrentSessions() {
1533
+ return [...currentSessions];
1534
+ }
1535
+ function startSessionPolling(broadcast) {
1536
+ stopSessionPolling();
1537
+ pollSessions(broadcast);
1538
+ pollingInterval = setInterval(() => {
1539
+ pollSessions(broadcast);
1540
+ }, POLLING_INTERVAL_MS);
1541
+ }
1542
+ function stopSessionPolling() {
1543
+ if (pollingInterval) {
1544
+ clearInterval(pollingInterval);
1545
+ pollingInterval = null;
1546
+ }
1547
+ currentSessions = [];
1548
+ isFirstPoll = true;
1549
+ }
1550
+ async function pollSessions(broadcast) {
1551
+ try {
1552
+ const sessions = await discoverSessions();
1553
+ const hasChanges = detectChanges(sessions);
1554
+ if (hasChanges) {
1555
+ currentSessions = sessions;
1556
+ isFirstPoll = false;
1557
+ broadcast({
1558
+ type: "sessions:updated",
1559
+ sessions
1560
+ });
1561
+ }
1562
+ } catch (error) {
1563
+ console.error("Error polling sessions:", error);
1564
+ }
1565
+ }
1566
+ async function discoverSessions() {
1567
+ const rawSessions = await listSessions();
1568
+ const detailedSessions = [];
1569
+ for (const session of rawSessions) {
1570
+ try {
1571
+ const statusResult = await getSessionStatus(session.name);
1572
+ const status = statusResult.running ? "running" : "completed";
1573
+ const detailed = await buildSessionInfo(session.name, status);
1574
+ if (detailed) {
1575
+ detailedSessions.push(detailed);
1576
+ }
1577
+ } catch (error) {
1578
+ console.error(`Error building session info for ${session.name}:`, error);
1579
+ }
1580
+ }
1581
+ detailedSessions.sort((a, b) => b.startTime.getTime() - a.startTime.getTime());
1582
+ return detailedSessions;
1583
+ }
1584
+ function detectChanges(newSessions) {
1585
+ if (isFirstPoll) {
1586
+ return true;
1587
+ }
1588
+ if (newSessions.length !== currentSessions.length) {
1589
+ return true;
1590
+ }
1591
+ const newSessionMap = /* @__PURE__ */ new Map();
1592
+ for (const session of newSessions) {
1593
+ newSessionMap.set(session.name, session);
1594
+ }
1595
+ const currentSessionMap = /* @__PURE__ */ new Map();
1596
+ for (const session of currentSessions) {
1597
+ currentSessionMap.set(session.name, session);
1598
+ }
1599
+ for (const name of newSessionMap.keys()) {
1600
+ if (!currentSessionMap.has(name)) {
1601
+ return true;
1602
+ }
1603
+ }
1604
+ for (const name of currentSessionMap.keys()) {
1605
+ if (!newSessionMap.has(name)) {
1606
+ return true;
1607
+ }
1608
+ }
1609
+ for (const [name, newSession] of newSessionMap) {
1610
+ const currentSession = currentSessionMap.get(name);
1611
+ if (currentSession && currentSession.status !== newSession.status) {
1612
+ return true;
1613
+ }
1614
+ }
1615
+ return false;
1616
+ }
1617
+
1618
+ // src/server/session-routes.ts
1619
+ function createSessionApiRouter() {
1620
+ const router = (0, import_express.Router)();
1621
+ router.get("/sessions", (_req, res) => {
1622
+ try {
1623
+ let sessions = getCurrentSessions();
1624
+ const { epicSlug, storySlug, status } = _req.query;
1625
+ if (epicSlug && typeof epicSlug === "string") {
1626
+ sessions = sessions.filter((s) => s.epicSlug === epicSlug);
1627
+ if (storySlug && typeof storySlug === "string") {
1628
+ sessions = sessions.filter((s) => s.storySlug === storySlug);
1629
+ }
1630
+ }
1631
+ if (status && typeof status === "string" && (status === "running" || status === "completed")) {
1632
+ sessions = sessions.filter((s) => s.status === status);
1633
+ }
1634
+ res.json(sessions);
1635
+ } catch (error) {
1636
+ console.error("Error fetching sessions:", error);
1637
+ res.status(500).json({ error: "Failed to fetch sessions" });
1638
+ }
1639
+ });
1640
+ router.get("/sessions/:sessionName", (req, res) => {
1641
+ try {
1642
+ const { sessionName } = req.params;
1643
+ const sessions = getCurrentSessions();
1644
+ const session = sessions.find((s) => s.name === sessionName);
1645
+ if (!session) {
1646
+ res.status(404).json({ error: "Session not found" });
1647
+ return;
1648
+ }
1649
+ res.json(session);
1650
+ } catch (error) {
1651
+ console.error("Error fetching session:", error);
1652
+ res.status(500).json({ error: "Failed to fetch session" });
1653
+ }
1654
+ });
1655
+ return router;
1656
+ }
1657
+
1658
+ // src/server/routes.ts
1458
1659
  async function getEpics(sagaRoot) {
1459
1660
  return scanSagaDirectory(sagaRoot);
1460
1661
  }
@@ -1467,7 +1668,7 @@ function toEpicSummary(epic) {
1467
1668
  };
1468
1669
  }
1469
1670
  function createApiRouter(sagaRoot) {
1470
- const router = (0, import_express.Router)();
1671
+ const router = (0, import_express2.Router)();
1471
1672
  router.get("/epics", async (_req, res) => {
1472
1673
  try {
1473
1674
  const epics = await getEpics(sagaRoot);
@@ -1520,6 +1721,7 @@ function createApiRouter(sagaRoot) {
1520
1721
  res.status(500).json({ error: "Failed to fetch story" });
1521
1722
  }
1522
1723
  });
1724
+ router.use(createSessionApiRouter());
1523
1725
  router.use((_req, res) => {
1524
1726
  res.status(404).json({ error: "API endpoint not found" });
1525
1727
  });
@@ -1726,6 +1928,9 @@ async function createWebSocketServer(httpServer, sagaRoot) {
1726
1928
  sendToClient(ws, message);
1727
1929
  }
1728
1930
  }
1931
+ startSessionPolling((msg) => {
1932
+ broadcast({ event: msg.type, data: msg.sessions });
1933
+ });
1729
1934
  function broadcastToSubscribers(storyKey, message) {
1730
1935
  for (const [ws, state] of clients) {
1731
1936
  if (state.subscribedStories.has(storyKey)) {
@@ -1848,6 +2053,7 @@ async function createWebSocketServer(httpServer, sagaRoot) {
1848
2053
  },
1849
2054
  async close() {
1850
2055
  clearInterval(heartbeatInterval);
2056
+ stopSessionPolling();
1851
2057
  for (const [ws] of clients) {
1852
2058
  ws.close();
1853
2059
  }
@@ -1871,8 +2077,8 @@ async function createWebSocketServer(httpServer, sagaRoot) {
1871
2077
  // src/server/index.ts
1872
2078
  var DEFAULT_PORT = 3847;
1873
2079
  function createApp(sagaRoot) {
1874
- const app = (0, import_express2.default)();
1875
- app.use(import_express2.default.json());
2080
+ const app = (0, import_express3.default)();
2081
+ app.use(import_express3.default.json());
1876
2082
  app.use((_req, res, next) => {
1877
2083
  res.header("Access-Control-Allow-Origin", "*");
1878
2084
  res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
@@ -1885,7 +2091,7 @@ function createApp(sagaRoot) {
1885
2091
  app.use("/api", createApiRouter(sagaRoot));
1886
2092
  const clientDistPath = (0, import_path6.join)(__dirname, "client");
1887
2093
  const indexHtmlPath = (0, import_path6.join)(clientDistPath, "index.html");
1888
- app.use(import_express2.default.static(clientDistPath));
2094
+ app.use(import_express3.default.static(clientDistPath));
1889
2095
  app.get("/{*splat}", (_req, res) => {
1890
2096
  res.sendFile("index.html", { root: clientDistPath });
1891
2097
  });