@m8i-51/shoal 0.1.16 → 0.1.17

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m8i-51/shoal",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
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": {
package/server/index.ts CHANGED
@@ -3,11 +3,12 @@ import express from "express";
3
3
  import { rateLimit } from "express-rate-limit";
4
4
  import { fileURLToPath } from "url";
5
5
  import { dirname, join, resolve } from "path";
6
- import { existsSync, readFileSync, writeFileSync } from "fs";
6
+ import { existsSync, readFileSync, writeFileSync, readdirSync } from "fs";
7
7
  import { listRuns, getReportPath } from "./runs.js";
8
8
  import { activeSessions, spawnRun, cancelSession } from "./runner.js";
9
9
  import { loadSchedule, saveSchedule, startScheduler, type ScheduleConfig } from "./scheduler.js";
10
10
  import { generateDiary, getDiaryPath } from "../framework/diary.js";
11
+ import type { Finding } from "../framework/types.js";
11
12
 
12
13
  function specFilePath(baseUrl: string): string {
13
14
  try {
@@ -128,6 +129,69 @@ app.post("/api/runs/:runId/diary", async (req, res) => {
128
129
  }
129
130
  });
130
131
 
132
+ // ----------------------------------------------------------------
133
+ // API: Hall of Issues — 全 run の findings を横断取得
134
+ // ----------------------------------------------------------------
135
+ function loadAllFindings(): (Finding & { runId: string })[] {
136
+ const base = resolve(process.cwd(), "findings");
137
+ if (!existsSync(base)) return [];
138
+ const all: (Finding & { runId: string })[] = [];
139
+ for (const runDir of readdirSync(base)) {
140
+ if (!/^run_\d+$/.test(runDir)) continue;
141
+ const dir = join(base, runDir);
142
+ try {
143
+ for (const file of readdirSync(dir)) {
144
+ if (!file.endsWith(".json")) continue;
145
+ try {
146
+ const f: Finding = JSON.parse(readFileSync(join(dir, file), "utf-8"));
147
+ all.push({ ...f, runId: runDir });
148
+ } catch { /* skip */ }
149
+ }
150
+ } catch { /* skip */ }
151
+ }
152
+ return all.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
153
+ }
154
+
155
+ app.get("/api/findings", (_req, res) => {
156
+ res.json(loadAllFindings());
157
+ });
158
+
159
+ app.get("/api/findings/export", (_req, res) => {
160
+ const findings = loadAllFindings().map(({ id, title, body, category, agentName, role, timestamp, runId }) => ({
161
+ id, title, body, category, agentName, role, timestamp, runId,
162
+ }));
163
+ const bundle = { version: "1", exportedAt: new Date().toISOString(), source: "shoal", findings };
164
+ res.setHeader("Content-Type", "application/json");
165
+ res.setHeader("Content-Disposition", `attachment; filename="shoal-findings-${Date.now()}.json"`);
166
+ res.json(bundle);
167
+ });
168
+
169
+ app.post("/api/findings/proxy-url", async (req, res) => {
170
+ const { url } = req.body as { url?: string };
171
+ if (!url || typeof url !== "string") { res.status(400).json({ error: "url required" }); return; }
172
+ let parsed: URL;
173
+ try {
174
+ parsed = new URL(url);
175
+ if (!["http:", "https:"].includes(parsed.protocol)) throw new Error("invalid protocol");
176
+ const h = parsed.hostname;
177
+ if (h === "localhost" || h === "127.0.0.1" || h === "::1" || h.startsWith("192.168.") || h.startsWith("10.") || h.endsWith(".local")) {
178
+ res.status(400).json({ error: "private urls not allowed" });
179
+ return;
180
+ }
181
+ } catch {
182
+ res.status(400).json({ error: "invalid url" });
183
+ return;
184
+ }
185
+ try {
186
+ const upstream = await fetch(url, { signal: AbortSignal.timeout(10_000) });
187
+ if (!upstream.ok) { res.status(502).json({ error: "upstream error" }); return; }
188
+ const data = await upstream.json();
189
+ res.json(data);
190
+ } catch {
191
+ res.status(502).json({ error: "failed to fetch url" });
192
+ }
193
+ });
194
+
131
195
  // ----------------------------------------------------------------
132
196
  // API: serve HTML report for a run
133
197
  // ----------------------------------------------------------------