@saga-ai/dashboard 4.2.0 → 4.2.1

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 +137 -104
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -28,8 +28,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  ));
29
29
 
30
30
  // src/cli.ts
31
- var import_node_fs7 = require("node:fs");
32
- var import_node_path8 = require("node:path");
31
+ var import_node_fs6 = require("node:fs");
32
+ var import_node_path7 = require("node:path");
33
33
  var import_node_process6 = __toESM(require("node:process"), 1);
34
34
  var import_commander = require("commander");
35
35
 
@@ -38,14 +38,14 @@ var import_node_process4 = __toESM(require("node:process"), 1);
38
38
 
39
39
  // src/server/index.ts
40
40
  var import_node_http = require("node:http");
41
- var import_node_path6 = require("node:path");
41
+ var import_node_path5 = require("node:path");
42
42
  var import_express3 = __toESM(require("express"), 1);
43
43
 
44
44
  // src/server/routes.ts
45
45
  var import_express2 = require("express");
46
46
 
47
47
  // src/server/parser.ts
48
- var import_node_fs3 = require("node:fs");
48
+ var import_node_fs2 = require("node:fs");
49
49
  var import_promises = require("node:fs/promises");
50
50
 
51
51
  // ../saga-utils/src/directory.ts
@@ -74,6 +74,13 @@ function createStoryPaths(projectRoot, storyId) {
74
74
  journalMd: `${storyDir}/journal.md`
75
75
  };
76
76
  }
77
+ function createWorktreePaths(projectRoot, storyId) {
78
+ const { worktrees } = createSagaPaths(projectRoot);
79
+ return {
80
+ storyId,
81
+ worktreeDir: `${worktrees}/${storyId}`
82
+ };
83
+ }
77
84
 
78
85
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
79
86
  var external_exports = {};
@@ -4195,6 +4202,79 @@ var StorySchema = external_exports.object({
4195
4202
  // ../saga-utils/src/storage.ts
4196
4203
  var import_node_fs = require("node:fs");
4197
4204
  var import_node_path = require("node:path");
4205
+ function buildScannedStory(storyDir, storyJsonPath, worktreePath) {
4206
+ if (!(0, import_node_fs.existsSync)(storyJsonPath)) {
4207
+ return null;
4208
+ }
4209
+ let storyData;
4210
+ try {
4211
+ const raw = (0, import_node_fs.readFileSync)(storyJsonPath, "utf-8");
4212
+ storyData = StorySchema.parse(JSON.parse(raw));
4213
+ } catch {
4214
+ return null;
4215
+ }
4216
+ let tasks = [];
4217
+ try {
4218
+ const files = (0, import_node_fs.readdirSync)(storyDir);
4219
+ tasks = files.filter((f) => f.endsWith(".json") && f !== "story.json").map((f) => {
4220
+ const raw = (0, import_node_fs.readFileSync)((0, import_node_path.join)(storyDir, f), "utf-8");
4221
+ return TaskSchema.parse(JSON.parse(raw));
4222
+ });
4223
+ } catch {
4224
+ }
4225
+ const status = deriveStoryStatus(tasks);
4226
+ const journalMdPath = (0, import_node_path.join)(storyDir, "journal.md");
4227
+ return {
4228
+ ...storyData,
4229
+ status,
4230
+ storyPath: storyJsonPath,
4231
+ worktreePath,
4232
+ journalPath: (0, import_node_fs.existsSync)(journalMdPath) ? journalMdPath : void 0,
4233
+ tasks
4234
+ };
4235
+ }
4236
+ function scanMasterStories(projectRoot, storyMap) {
4237
+ const { stories } = createSagaPaths(projectRoot);
4238
+ if (!(0, import_node_fs.existsSync)(stories)) {
4239
+ return;
4240
+ }
4241
+ const entries = (0, import_node_fs.readdirSync)(stories, { withFileTypes: true });
4242
+ for (const entry of entries) {
4243
+ if (!entry.isDirectory()) {
4244
+ continue;
4245
+ }
4246
+ const storyPaths = createStoryPaths(projectRoot, entry.name);
4247
+ const scanned = buildScannedStory(storyPaths.storyDir, storyPaths.storyJson);
4248
+ if (!scanned) {
4249
+ continue;
4250
+ }
4251
+ const wtPaths = createWorktreePaths(projectRoot, entry.name);
4252
+ if ((0, import_node_fs.existsSync)(wtPaths.worktreeDir)) {
4253
+ scanned.worktreePath = wtPaths.worktreeDir;
4254
+ }
4255
+ storyMap.set(scanned.id, scanned);
4256
+ }
4257
+ }
4258
+ function scanWorktreeStories(projectRoot, storyMap) {
4259
+ const { worktrees } = createSagaPaths(projectRoot);
4260
+ if (!(0, import_node_fs.existsSync)(worktrees)) {
4261
+ return;
4262
+ }
4263
+ const entries = (0, import_node_fs.readdirSync)(worktrees, { withFileTypes: true });
4264
+ for (const wtEntry of entries) {
4265
+ if (!wtEntry.isDirectory()) {
4266
+ continue;
4267
+ }
4268
+ const storyId = wtEntry.name;
4269
+ const wtStoryDir = (0, import_node_path.join)(worktrees, storyId, ".saga", "stories", storyId);
4270
+ const wtStoryJson = (0, import_node_path.join)(wtStoryDir, "story.json");
4271
+ const wtPath = (0, import_node_path.join)(worktrees, storyId);
4272
+ const scanned = buildScannedStory(wtStoryDir, wtStoryJson, wtPath);
4273
+ if (scanned) {
4274
+ storyMap.set(scanned.id, scanned);
4275
+ }
4276
+ }
4277
+ }
4198
4278
  function readStory(projectRoot, storyId) {
4199
4279
  const { storyJson } = createStoryPaths(projectRoot, storyId);
4200
4280
  if (!(0, import_node_fs.existsSync)(storyJson)) {
@@ -4245,27 +4325,6 @@ function listEpics(projectRoot) {
4245
4325
  return EpicSchema.parse(parsed);
4246
4326
  });
4247
4327
  }
4248
- function listStories(projectRoot) {
4249
- const { stories } = createSagaPaths(projectRoot);
4250
- if (!(0, import_node_fs.existsSync)(stories)) {
4251
- throw new Error(`Stories directory not found: ${stories}`);
4252
- }
4253
- const entries = (0, import_node_fs.readdirSync)(stories, { withFileTypes: true });
4254
- return entries.filter((entry) => entry.isDirectory()).map((entry) => {
4255
- const storyJsonPath = (0, import_node_path.join)(stories, entry.name, "story.json");
4256
- if (!(0, import_node_fs.existsSync)(storyJsonPath)) {
4257
- return null;
4258
- }
4259
- const raw = (0, import_node_fs.readFileSync)(storyJsonPath, "utf-8");
4260
- let parsed;
4261
- try {
4262
- parsed = JSON.parse(raw);
4263
- } catch {
4264
- throw new Error(`Malformed JSON in story file: ${storyJsonPath}`);
4265
- }
4266
- return StorySchema.parse(parsed);
4267
- }).filter((story) => story !== null);
4268
- }
4269
4328
  function deriveStoryStatus(tasks) {
4270
4329
  if (tasks.length === 0) {
4271
4330
  return "pending";
@@ -4290,46 +4349,11 @@ function deriveEpicStatus(storyStatuses) {
4290
4349
  }
4291
4350
  return "pending";
4292
4351
  }
4293
-
4294
- // src/utils/saga-scanner.ts
4295
- var import_node_fs2 = require("node:fs");
4296
- var import_node_path2 = require("node:path");
4297
- function scanStories(sagaRoot) {
4298
- const storiesDir = (0, import_node_path2.join)(sagaRoot, ".saga", "stories");
4299
- if (!(0, import_node_fs2.existsSync)(storiesDir)) {
4300
- return [];
4301
- }
4302
- const stories = listStories(sagaRoot);
4303
- return stories.map((story) => {
4304
- let tasks = [];
4305
- try {
4306
- tasks = listTasks(sagaRoot, story.id);
4307
- } catch {
4308
- }
4309
- const { journalMd } = createStoryPaths(sagaRoot, story.id);
4310
- const hasJournal = (0, import_node_fs2.existsSync)(journalMd);
4311
- return {
4312
- id: story.id,
4313
- title: story.title,
4314
- description: story.description,
4315
- epicId: story.epic,
4316
- guidance: story.guidance,
4317
- doneWhen: story.doneWhen,
4318
- avoid: story.avoid,
4319
- branch: story.branch,
4320
- pr: story.pr,
4321
- worktree: story.worktree,
4322
- journalPath: hasJournal ? journalMd : void 0,
4323
- tasks
4324
- };
4325
- });
4326
- }
4327
- function scanEpics(sagaRoot) {
4328
- const epicsDir = (0, import_node_path2.join)(sagaRoot, ".saga", "epics");
4329
- if (!(0, import_node_fs2.existsSync)(epicsDir)) {
4330
- return [];
4331
- }
4332
- return listEpics(sagaRoot);
4352
+ function scanStories(projectRoot) {
4353
+ const storyMap = /* @__PURE__ */ new Map();
4354
+ scanMasterStories(projectRoot, storyMap);
4355
+ scanWorktreeStories(projectRoot, storyMap);
4356
+ return Array.from(storyMap.values());
4333
4357
  }
4334
4358
 
4335
4359
  // src/server/parser.ts
@@ -4353,7 +4377,7 @@ function toStoryDetail(story) {
4353
4377
  id: story.id,
4354
4378
  title: story.title,
4355
4379
  description: story.description,
4356
- epic: story.epicId,
4380
+ epic: story.epic,
4357
4381
  status: toApiStatus(derivedStatus),
4358
4382
  tasks,
4359
4383
  guidance: story.guidance,
@@ -4391,14 +4415,19 @@ function buildEpic(scannedEpic, epicStories) {
4391
4415
  }
4392
4416
  function parseStory(sagaRoot, storyId) {
4393
4417
  try {
4394
- const story = readStory(sagaRoot, storyId);
4418
+ const wtPaths = createWorktreePaths(sagaRoot, storyId);
4419
+ const wtStoryDir = `${wtPaths.worktreeDir}/.saga/stories/${storyId}`;
4420
+ const wtStoryJson = `${wtStoryDir}/story.json`;
4421
+ const useWorktree = (0, import_node_fs2.existsSync)(wtStoryJson);
4422
+ const storyRoot = useWorktree ? wtPaths.worktreeDir : sagaRoot;
4423
+ const story = readStory(storyRoot, storyId);
4395
4424
  let tasks = [];
4396
4425
  try {
4397
- tasks = listTasks(sagaRoot, storyId);
4426
+ tasks = listTasks(storyRoot, storyId);
4398
4427
  } catch {
4399
4428
  }
4400
- const { journalMd } = createStoryPaths(sagaRoot, storyId);
4401
- const hasJournal = (0, import_node_fs3.existsSync)(journalMd);
4429
+ const { journalMd } = createStoryPaths(storyRoot, storyId);
4430
+ const hasJournal = (0, import_node_fs2.existsSync)(journalMd);
4402
4431
  const derivedStatus = deriveStoryStatus(tasks);
4403
4432
  return {
4404
4433
  id: story.id,
@@ -4472,7 +4501,11 @@ async function parseJournal(journalPath) {
4472
4501
  }
4473
4502
  function scanSagaDirectory(sagaRoot) {
4474
4503
  const scannedStories = scanStories(sagaRoot);
4475
- const scannedEpics = scanEpics(sagaRoot);
4504
+ let scannedEpics = [];
4505
+ try {
4506
+ scannedEpics = listEpics(sagaRoot);
4507
+ } catch {
4508
+ }
4476
4509
  const allStories = scannedStories.map(toStoryDetail);
4477
4510
  const storiesByEpic = /* @__PURE__ */ new Map();
4478
4511
  const standaloneStories = [];
@@ -4497,9 +4530,9 @@ var import_express = require("express");
4497
4530
 
4498
4531
  // src/lib/sessions.ts
4499
4532
  var import_node_child_process = require("node:child_process");
4500
- var import_node_fs4 = require("node:fs");
4533
+ var import_node_fs3 = require("node:fs");
4501
4534
  var import_promises2 = require("node:fs/promises");
4502
- var import_node_path3 = require("node:path");
4535
+ var import_node_path2 = require("node:path");
4503
4536
  var import_node_process = __toESM(require("node:process"), 1);
4504
4537
  var PREVIEW_LINES_COUNT = 5;
4505
4538
  var PREVIEW_MAX_LENGTH = 500;
@@ -4569,7 +4602,7 @@ function listSessions() {
4569
4602
  name,
4570
4603
  status: "running",
4571
4604
  // If it shows up in tmux ls, it's running
4572
- outputFile: (0, import_node_path3.join)(OUTPUT_DIR, `${name}.jsonl`)
4605
+ outputFile: (0, import_node_path2.join)(OUTPUT_DIR, `${name}.jsonl`)
4573
4606
  });
4574
4607
  }
4575
4608
  }
@@ -4584,8 +4617,8 @@ function getSessionStatus(sessionName) {
4584
4617
  };
4585
4618
  }
4586
4619
  function streamLogs(sessionName) {
4587
- const outputFile = (0, import_node_path3.join)(OUTPUT_DIR, `${sessionName}.jsonl`);
4588
- if (!(0, import_node_fs4.existsSync)(outputFile)) {
4620
+ const outputFile = (0, import_node_path2.join)(OUTPUT_DIR, `${sessionName}.jsonl`);
4621
+ if (!(0, import_node_fs3.existsSync)(outputFile)) {
4589
4622
  throw new Error(`Output file not found: ${outputFile}`);
4590
4623
  }
4591
4624
  return new Promise((resolve, reject) => {
@@ -4638,8 +4671,8 @@ async function buildSessionInfo(name, status) {
4638
4671
  if (!parsed) {
4639
4672
  return null;
4640
4673
  }
4641
- const outputFile = (0, import_node_path3.join)(OUTPUT_DIR, `${name}.jsonl`);
4642
- const outputAvailable = (0, import_node_fs4.existsSync)(outputFile);
4674
+ const outputFile = (0, import_node_path2.join)(OUTPUT_DIR, `${name}.jsonl`);
4675
+ const outputAvailable = (0, import_node_fs3.existsSync)(outputFile);
4643
4676
  let startTime = /* @__PURE__ */ new Date();
4644
4677
  let endTime;
4645
4678
  let outputPreview;
@@ -4885,9 +4918,9 @@ function createApiRouter(sagaRoot) {
4885
4918
  var import_ws = require("ws");
4886
4919
 
4887
4920
  // src/lib/log-stream-manager.ts
4888
- var import_node_fs5 = require("node:fs");
4921
+ var import_node_fs4 = require("node:fs");
4889
4922
  var import_promises3 = require("node:fs/promises");
4890
- var import_node_path4 = require("node:path");
4923
+ var import_node_path3 = require("node:path");
4891
4924
  var import_chokidar = __toESM(require("chokidar"), 1);
4892
4925
  function parseJsonlLines(content) {
4893
4926
  const messages = [];
@@ -4969,8 +5002,8 @@ var LogStreamManager = class {
4969
5002
  * Creates a file watcher if this is the first subscriber.
4970
5003
  */
4971
5004
  async subscribe(sessionName, ws) {
4972
- const outputFile = (0, import_node_path4.join)(OUTPUT_DIR, `${sessionName}.jsonl`);
4973
- if (!(0, import_node_fs5.existsSync)(outputFile)) {
5005
+ const outputFile = (0, import_node_path3.join)(OUTPUT_DIR, `${sessionName}.jsonl`);
5006
+ if (!(0, import_node_fs4.existsSync)(outputFile)) {
4974
5007
  this.sendToClient(ws, {
4975
5008
  type: "logs:error",
4976
5009
  sessionName,
@@ -5099,10 +5132,10 @@ var LogStreamManager = class {
5099
5132
  if (!subs || subs.size === 0) {
5100
5133
  return;
5101
5134
  }
5102
- const outputFile = (0, import_node_path4.join)(OUTPUT_DIR, `${sessionName}.jsonl`);
5135
+ const outputFile = (0, import_node_path3.join)(OUTPUT_DIR, `${sessionName}.jsonl`);
5103
5136
  const finalMessages = [];
5104
5137
  try {
5105
- if ((0, import_node_fs5.existsSync)(outputFile)) {
5138
+ if ((0, import_node_fs4.existsSync)(outputFile)) {
5106
5139
  const content = await (0, import_promises3.readFile)(outputFile, "utf-8");
5107
5140
  const lastLineCount = this.lineCounts.get(sessionName) ?? 0;
5108
5141
  const nonEmptyLines = content.split("\n").filter((l) => l.trim());
@@ -5148,7 +5181,7 @@ var LogStreamManager = class {
5148
5181
 
5149
5182
  // src/server/watcher.ts
5150
5183
  var import_node_events = require("node:events");
5151
- var import_node_path5 = require("node:path");
5184
+ var import_node_path4 = require("node:path");
5152
5185
  var import_node_process2 = __toESM(require("node:process"), 1);
5153
5186
  var import_chokidar2 = __toESM(require("chokidar"), 1);
5154
5187
  var EPIC_PATH_PARTS = 3;
@@ -5161,8 +5194,8 @@ function shouldUsePolling() {
5161
5194
  function parseEpicsPath(parts) {
5162
5195
  if (parts.length === EPIC_PATH_PARTS) {
5163
5196
  const fileName = parts[2];
5164
- if ((0, import_node_path5.extname)(fileName) === ".json") {
5165
- const epicId = (0, import_node_path5.basename)(fileName, ".json");
5197
+ if ((0, import_node_path4.extname)(fileName) === ".json") {
5198
+ const epicId = (0, import_node_path4.basename)(fileName, ".json");
5166
5199
  return {
5167
5200
  epicId,
5168
5201
  isEpicFile: true,
@@ -5193,7 +5226,7 @@ function parseStoriesPath(parts) {
5193
5226
  isMainStoryFile: false
5194
5227
  };
5195
5228
  }
5196
- if ((0, import_node_path5.extname)(fileName) === ".json") {
5229
+ if ((0, import_node_path4.extname)(fileName) === ".json") {
5197
5230
  return {
5198
5231
  storyId,
5199
5232
  isEpicFile: false,
@@ -5205,8 +5238,8 @@ function parseStoriesPath(parts) {
5205
5238
  return null;
5206
5239
  }
5207
5240
  function parseFilePath(filePath, sagaRoot) {
5208
- const relativePath = (0, import_node_path5.relative)(sagaRoot, filePath);
5209
- const parts = relativePath.split(import_node_path5.sep);
5241
+ const relativePath = (0, import_node_path4.relative)(sagaRoot, filePath);
5242
+ const parts = relativePath.split(import_node_path4.sep);
5210
5243
  if (parts[0] !== ".saga" || parts.length < MIN_PATH_PARTS) {
5211
5244
  return null;
5212
5245
  }
@@ -5278,8 +5311,8 @@ function createDebounceKey(parsed) {
5278
5311
  return `epic:${parsed.epicId}`;
5279
5312
  }
5280
5313
  function createChokidarWatcher(sagaRoot) {
5281
- const storiesDir = (0, import_node_path5.join)(sagaRoot, ".saga", "stories");
5282
- const epicsDir = (0, import_node_path5.join)(sagaRoot, ".saga", "epics");
5314
+ const storiesDir = (0, import_node_path4.join)(sagaRoot, ".saga", "stories");
5315
+ const epicsDir = (0, import_node_path4.join)(sagaRoot, ".saga", "epics");
5283
5316
  const usePolling = shouldUsePolling();
5284
5317
  return import_chokidar2.default.watch([storiesDir, epicsDir], {
5285
5318
  persistent: true,
@@ -5311,7 +5344,7 @@ function createFileEventHandler(sagaRoot, debouncer, emitter, state) {
5311
5344
  type: watcherEventType,
5312
5345
  epicId: parsed.epicId,
5313
5346
  storyId: parsed.storyId,
5314
- path: (0, import_node_path5.relative)(sagaRoot, filePath)
5347
+ path: (0, import_node_path4.relative)(sagaRoot, filePath)
5315
5348
  };
5316
5349
  debouncer.schedule(createDebounceKey(parsed), event, (e) => {
5317
5350
  if (!state.closed) {
@@ -5633,8 +5666,8 @@ function createApp(sagaRoot) {
5633
5666
  res.json({ status: "ok" });
5634
5667
  });
5635
5668
  app.use("/api", createApiRouter(sagaRoot));
5636
- const clientDistPath = (0, import_node_path6.join)(__dirname, "client");
5637
- const _indexHtmlPath = (0, import_node_path6.join)(clientDistPath, "index.html");
5669
+ const clientDistPath = (0, import_node_path5.join)(__dirname, "client");
5670
+ const _indexHtmlPath = (0, import_node_path5.join)(clientDistPath, "index.html");
5638
5671
  app.use(import_express3.default.static(clientDistPath));
5639
5672
  app.get("/{*splat}", (_req, res) => {
5640
5673
  res.sendFile("index.html", { root: clientDistPath });
@@ -5672,17 +5705,17 @@ async function startServer(config) {
5672
5705
  }
5673
5706
 
5674
5707
  // src/utils/project-discovery.ts
5675
- var import_node_fs6 = require("node:fs");
5676
- var import_node_path7 = require("node:path");
5708
+ var import_node_fs5 = require("node:fs");
5709
+ var import_node_path6 = require("node:path");
5677
5710
  var import_node_process3 = __toESM(require("node:process"), 1);
5678
5711
  function findProjectRoot(startDir) {
5679
5712
  let currentDir = startDir ?? import_node_process3.default.cwd();
5680
5713
  while (true) {
5681
- const sagaDir = (0, import_node_path7.join)(currentDir, ".saga");
5682
- if ((0, import_node_fs6.existsSync)(sagaDir)) {
5714
+ const sagaDir = (0, import_node_path6.join)(currentDir, ".saga");
5715
+ if ((0, import_node_fs5.existsSync)(sagaDir)) {
5683
5716
  return currentDir;
5684
5717
  }
5685
- const parentDir = (0, import_node_path7.dirname)(currentDir);
5718
+ const parentDir = (0, import_node_path6.dirname)(currentDir);
5686
5719
  if (parentDir === currentDir) {
5687
5720
  return null;
5688
5721
  }
@@ -5691,8 +5724,8 @@ function findProjectRoot(startDir) {
5691
5724
  }
5692
5725
  function resolveProjectPath(explicitPath) {
5693
5726
  if (explicitPath) {
5694
- const sagaDir = (0, import_node_path7.join)(explicitPath, ".saga");
5695
- if (!(0, import_node_fs6.existsSync)(sagaDir)) {
5727
+ const sagaDir = (0, import_node_path6.join)(explicitPath, ".saga");
5728
+ if (!(0, import_node_fs5.existsSync)(sagaDir)) {
5696
5729
  throw new Error(
5697
5730
  `No .saga/ directory found at specified path: ${explicitPath}
5698
5731
  Make sure the path points to a SAGA project root.`
@@ -5758,8 +5791,8 @@ async function sessionsLogsCommand(sessionName) {
5758
5791
  }
5759
5792
 
5760
5793
  // src/cli.ts
5761
- var packageJsonPath = (0, import_node_path8.join)(__dirname, "..", "package.json");
5762
- var packageJson = JSON.parse((0, import_node_fs7.readFileSync)(packageJsonPath, "utf-8"));
5794
+ var packageJsonPath = (0, import_node_path7.join)(__dirname, "..", "package.json");
5795
+ var packageJson = JSON.parse((0, import_node_fs6.readFileSync)(packageJsonPath, "utf-8"));
5763
5796
  var program = new import_commander.Command();
5764
5797
  program.name("saga").description("Dashboard and session monitoring for SAGA - Structured Autonomous Goal Achievement").version(packageJson.version).addHelpCommand("help [command]", "Display help for a command");
5765
5798
  program.option("-p, --path <dir>", "Path to SAGA project directory (overrides auto-discovery)");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saga-ai/dashboard",
3
- "version": "4.2.0",
3
+ "version": "4.2.1",
4
4
  "description": "Dashboard and session monitoring for SAGA - Structured Autonomous Goal Achievement",
5
5
  "type": "module",
6
6
  "bin": {