@saga-ai/cli 2.12.0 → 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 +223 -15
- package/dist/client/assets/index-CQYbaS_0.js +198 -0
- package/dist/client/assets/index-DjgOmj3i.css +1 -0
- package/dist/client/index.html +2 -2
- package/package.json +8 -1
- package/dist/client/assets/index-Ct5VoJmK.js +0 -165
- package/dist/client/assets/index-DVpp5008.css +0 -1
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
|
|
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
|
|
1349
|
+
var import_express2 = require("express");
|
|
1285
1350
|
|
|
1286
1351
|
// src/server/parser.ts
|
|
1287
|
-
var
|
|
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,
|
|
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:
|
|
1404
|
+
const { stat: stat3 } = await import("fs/promises");
|
|
1340
1405
|
let content;
|
|
1341
1406
|
try {
|
|
1342
|
-
content = await (0,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
});
|
|
@@ -1612,12 +1814,14 @@ function createDebouncer(delayMs) {
|
|
|
1612
1814
|
async function createSagaWatcher(sagaRoot) {
|
|
1613
1815
|
const emitter = new import_events.EventEmitter();
|
|
1614
1816
|
const debouncer = createDebouncer(100);
|
|
1615
|
-
const
|
|
1616
|
-
const
|
|
1817
|
+
const epicsDir = (0, import_path4.join)(sagaRoot, ".saga", "epics");
|
|
1818
|
+
const archiveDir = (0, import_path4.join)(sagaRoot, ".saga", "archive");
|
|
1819
|
+
const watcher = import_chokidar.default.watch([epicsDir, archiveDir], {
|
|
1617
1820
|
persistent: true,
|
|
1618
1821
|
ignoreInitial: true,
|
|
1619
1822
|
// Don't emit events for existing files
|
|
1620
|
-
// Use polling for reliable cross-platform behavior
|
|
1823
|
+
// Use polling for reliable cross-platform behavior in tests
|
|
1824
|
+
// This is fine since we only watch epics/archive (~20 files), not entire .saga/ (79K+ files)
|
|
1621
1825
|
usePolling: true,
|
|
1622
1826
|
interval: 100
|
|
1623
1827
|
});
|
|
@@ -1724,6 +1928,9 @@ async function createWebSocketServer(httpServer, sagaRoot) {
|
|
|
1724
1928
|
sendToClient(ws, message);
|
|
1725
1929
|
}
|
|
1726
1930
|
}
|
|
1931
|
+
startSessionPolling((msg) => {
|
|
1932
|
+
broadcast({ event: msg.type, data: msg.sessions });
|
|
1933
|
+
});
|
|
1727
1934
|
function broadcastToSubscribers(storyKey, message) {
|
|
1728
1935
|
for (const [ws, state] of clients) {
|
|
1729
1936
|
if (state.subscribedStories.has(storyKey)) {
|
|
@@ -1846,6 +2053,7 @@ async function createWebSocketServer(httpServer, sagaRoot) {
|
|
|
1846
2053
|
},
|
|
1847
2054
|
async close() {
|
|
1848
2055
|
clearInterval(heartbeatInterval);
|
|
2056
|
+
stopSessionPolling();
|
|
1849
2057
|
for (const [ws] of clients) {
|
|
1850
2058
|
ws.close();
|
|
1851
2059
|
}
|
|
@@ -1869,8 +2077,8 @@ async function createWebSocketServer(httpServer, sagaRoot) {
|
|
|
1869
2077
|
// src/server/index.ts
|
|
1870
2078
|
var DEFAULT_PORT = 3847;
|
|
1871
2079
|
function createApp(sagaRoot) {
|
|
1872
|
-
const app = (0,
|
|
1873
|
-
app.use(
|
|
2080
|
+
const app = (0, import_express3.default)();
|
|
2081
|
+
app.use(import_express3.default.json());
|
|
1874
2082
|
app.use((_req, res, next) => {
|
|
1875
2083
|
res.header("Access-Control-Allow-Origin", "*");
|
|
1876
2084
|
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
@@ -1883,7 +2091,7 @@ function createApp(sagaRoot) {
|
|
|
1883
2091
|
app.use("/api", createApiRouter(sagaRoot));
|
|
1884
2092
|
const clientDistPath = (0, import_path6.join)(__dirname, "client");
|
|
1885
2093
|
const indexHtmlPath = (0, import_path6.join)(clientDistPath, "index.html");
|
|
1886
|
-
app.use(
|
|
2094
|
+
app.use(import_express3.default.static(clientDistPath));
|
|
1887
2095
|
app.get("/{*splat}", (_req, res) => {
|
|
1888
2096
|
res.sendFile("index.html", { root: clientDistPath });
|
|
1889
2097
|
});
|