@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 +0 -3
- package/dist/cli.cjs +34 -126
- package/dist/client/assets/index-Ct5VoJmK.js +165 -0
- package/dist/client/assets/index-DVpp5008.css +1 -0
- package/dist/client/index.html +14 -0
- package/package.json +18 -13
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
|
|
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
|
-
|
|
1189
|
-
console.log(`
|
|
1133
|
+
console.log(`
|
|
1190
1134
|
--- Worker ${cycles} started ---
|
|
1191
1135
|
`);
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
|
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
|
|
1320
|
-
|
|
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:
|
|
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 =
|
|
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").
|
|
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) => {
|