@m8i-51/shoal 0.1.2 → 0.1.4
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/package.json +2 -1
- package/server/index.ts +32 -6
- package/server/runs.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m8i-51/shoal",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Multi-agent web exploration framework — finds bugs, UX issues, and missing features by running AI agents against your app",
|
|
6
6
|
"repository": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"@clack/prompts": "^1.3.0",
|
|
39
39
|
"dotenv": "^17.3.1",
|
|
40
40
|
"express": "^5.2.1",
|
|
41
|
+
"express-rate-limit": "^8.5.0",
|
|
41
42
|
"openai": "^6.33.0",
|
|
42
43
|
"playwright": "^1.59.1",
|
|
43
44
|
"tsx": "^4.21.0"
|
package/server/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import "dotenv/config";
|
|
2
2
|
import express from "express";
|
|
3
|
+
import { rateLimit } from "express-rate-limit";
|
|
3
4
|
import { fileURLToPath } from "url";
|
|
4
|
-
import { dirname, join } from "path";
|
|
5
|
+
import { dirname, join, resolve } from "path";
|
|
5
6
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
6
7
|
import { listRuns, getReportPath } from "./runs.js";
|
|
7
8
|
import { activeSessions, spawnRun, cancelSession } from "./runner.js";
|
|
@@ -15,11 +16,23 @@ function specFilePath(baseUrl: string): string {
|
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
const RUN_ID_RE = /^run_\d+$/;
|
|
20
|
+
function isValidRunId(id: string): boolean {
|
|
21
|
+
return RUN_ID_RE.test(id);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const logsBase = resolve(process.cwd(), "logs");
|
|
25
|
+
function safeLogPath(filename: string): string | null {
|
|
26
|
+
const p = resolve(logsBase, filename);
|
|
27
|
+
return p.startsWith(logsBase + "/") ? p : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
31
|
const app = express();
|
|
20
32
|
const PORT = parseInt(process.env.PORT ?? "4000", 10);
|
|
21
33
|
|
|
22
34
|
app.use(express.json());
|
|
35
|
+
app.use(rateLimit({ windowMs: 60_000, limit: 120 }));
|
|
23
36
|
|
|
24
37
|
// ----------------------------------------------------------------
|
|
25
38
|
// API: product spec (goals)
|
|
@@ -80,7 +93,12 @@ app.get("/api/runs", (_req, res) => {
|
|
|
80
93
|
// API: serve HTML report for a run
|
|
81
94
|
// ----------------------------------------------------------------
|
|
82
95
|
app.get("/api/runs/:runId/report", (req, res) => {
|
|
83
|
-
const
|
|
96
|
+
const { runId } = req.params;
|
|
97
|
+
if (!isValidRunId(runId)) {
|
|
98
|
+
res.status(400).json({ error: "invalid run id" });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const reportPath = getReportPath(runId);
|
|
84
102
|
if (!reportPath) {
|
|
85
103
|
res.status(404).json({ error: "report not found" });
|
|
86
104
|
return;
|
|
@@ -157,14 +175,18 @@ function sseStream(req: express.Request, res: express.Response, sessionId: strin
|
|
|
157
175
|
// API: SSE — /api/sessions/:sessionId/events(後方互換)
|
|
158
176
|
// ----------------------------------------------------------------
|
|
159
177
|
app.get("/api/sessions/:sessionId/events", (req, res) => {
|
|
160
|
-
|
|
178
|
+
const { sessionId } = req.params;
|
|
179
|
+
if (!isValidRunId(sessionId)) { res.status(400).json({ error: "invalid session id" }); return; }
|
|
180
|
+
sseStream(req, res, sessionId);
|
|
161
181
|
});
|
|
162
182
|
|
|
163
183
|
// ----------------------------------------------------------------
|
|
164
184
|
// API: SSE — /api/runs/:runId/events(詳細ページ用)
|
|
165
185
|
// ----------------------------------------------------------------
|
|
166
186
|
app.get("/api/runs/:runId/events", (req, res) => {
|
|
167
|
-
|
|
187
|
+
const { runId } = req.params;
|
|
188
|
+
if (!isValidRunId(runId)) { res.status(400).json({ error: "invalid run id" }); return; }
|
|
189
|
+
sseStream(req, res, runId);
|
|
168
190
|
});
|
|
169
191
|
|
|
170
192
|
// ----------------------------------------------------------------
|
|
@@ -172,6 +194,10 @@ app.get("/api/runs/:runId/events", (req, res) => {
|
|
|
172
194
|
// ----------------------------------------------------------------
|
|
173
195
|
app.get("/api/runs/:runId/log", (req, res) => {
|
|
174
196
|
const { runId } = req.params;
|
|
197
|
+
if (!isValidRunId(runId)) {
|
|
198
|
+
res.status(400).json({ error: "invalid run id" });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
175
201
|
|
|
176
202
|
// 1. アクティブセッション(インメモリ)を優先
|
|
177
203
|
const session = activeSessions.get(runId);
|
|
@@ -181,8 +207,8 @@ app.get("/api/runs/:runId/log", (req, res) => {
|
|
|
181
207
|
}
|
|
182
208
|
|
|
183
209
|
// 2. 保存済みログファイルにフォールバック
|
|
184
|
-
const logFilePath =
|
|
185
|
-
if (existsSync(logFilePath)) {
|
|
210
|
+
const logFilePath = safeLogPath(`log_${runId}.txt`);
|
|
211
|
+
if (logFilePath && existsSync(logFilePath)) {
|
|
186
212
|
const lines = readFileSync(logFilePath, "utf-8").split("\n").filter((l) => l !== "");
|
|
187
213
|
res.json({ lines, done: true, exitCode: null });
|
|
188
214
|
return;
|
package/server/runs.ts
CHANGED
|
@@ -98,6 +98,7 @@ export function listRuns(): RunSummary[] {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
export function getReportPath(runId: string): string | null {
|
|
101
|
+
if (!/^run_\d+$/.test(runId)) return null;
|
|
101
102
|
const p = path.join(process.cwd(), "logs", `report_${runId}.html`);
|
|
102
103
|
return fs.existsSync(p) ? p : null;
|
|
103
104
|
}
|