FN-8309: add report screenshot and activity capture
Add opt-in local screenshot attachments and scrubbed activity traces to reports. - Capture, preview, confirm, and locally retain report screenshots without GitHub egress. - Include bounded activity traces in draft and filing context with server-side validation. - Add report pipeline coverage, documentation, and release metadata. Files changed: .changeset/fn-8309-report-capture.md | 7 ++ docs/dashboard-guide.md | 2 + packages/dashboard/app/App.tsx | 10 ++ packages/dashboard/app/api/report.ts | 16 ++- packages/dashboard/app/components/ReportModal.css | 3 + packages/dashboard/app/components/ReportModal.tsx | 45 ++++++- .../app/utils/__tests__/activity-trace.test.ts | 19 +++ packages/dashboard/app/utils/activity-trace.ts | 37 ++++++ packages/dashboard/app/utils/capture-screenshot.ts | 22 ++++ packages/dashboard/package.json | 5 +- .../src/__tests__/report-pipeline.test.ts | 13 +++ packages/dashboard/src/report-pipeline.ts | 29 ++++- packages/dashboard/src/routes.ts | 2 +- .../dashboard/src/routes/register-report-routes.ts | 85 +++++++++++++- packages/dashboard/src/routes/types.ts | 4 +- pnpm-lock.yaml | 129 ++++++++++----------- 16 files changed, 341 insertions(+), 87 deletions(-) Fusion-Task-Id: FN-8309 Fusion-Task-Lineage: db9c14a6-1721-4e81-8891-cfd301473cd0 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8309-report-capture.md
Normal file
7
.changeset/fn-8309-report-capture.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Add scrubbed activity context and optional local report screenshots.
|
||||
category: feature
|
||||
dev: Screenshots require preview confirmation and never egress to GitHub.
|
||||
@@ -2090,3 +2090,5 @@ The Header **Report** menu is available on desktop and mobile and offers **Bug**
|
||||
Fusion gathers available task/agent context, structures the prompt into a report, scrubs secrets, local paths, project names, home-directory identities, email addresses, and likely personal names, then checks **open** GitHub issues or Discussions for duplicates. Scrubbing is mandatory for every route and is repeated on the server when a reviewed draft is edited before filing. A strong duplicate receives a visible 👍 reaction and one scrubbed data-point comment instead of a new issue or Discussion. Bug and Idea reports use issues; Feedback and unresolved Help reports use repository Discussions. If preparation or filing cannot reach GitHub, Fusion preserves the draft and shows a retryable error.
|
||||
|
||||
In **Settings → General**, choose **Review draft before filing** (the default) or **File automatically**. Both paths show the resulting issue, Discussion, or endorsement link. Help checks Fusion's local knowledge index on every server report path first and only escalates when it cannot find an answer.
|
||||
|
||||
Reports include a default-on activity trace of up to 20 recent views, actions, and client errors (at most 4,000 characters). It is ordinary report text and receives the same server-side scrub as every other report field. You can optionally attach a screenshot of the current Fusion view. Fusion captures only the dashboard DOM, shows the exact preview, and requires confirmation before it uploads the PNG/JPEG (up to 2MB) as a local image artifact. In automatic mode this confirmation and local artifact upload happen before filing begins. The server validates the resulting artifact reference (format, image type, MIME type, and report-upload provenance) before it can appear as a text-only local-retention note in a report. Screenshot pixels are never sent to GitHub.
|
||||
|
||||
@@ -48,6 +48,7 @@ import { useAuthOnboarding } from "./hooks/useAuthOnboarding";
|
||||
import { useMobileKeyboard } from "./hooks/useMobileKeyboard";
|
||||
import { isIOS, useMobileKeyboardViewportLock, useMobileViewportRestoreReset } from "./hooks/useMobileScrollLock";
|
||||
import { computeMobileBarKeyboardFlags } from "./utils/mobileBarKeyboardFlags";
|
||||
import { recordActivity } from "./utils/activity-trace";
|
||||
import { closeViewShortcut, retainViewNavRevert } from "./utils/dashboardShortcutToggles";
|
||||
import { useSetupReadiness } from "./hooks/useSetupReadiness";
|
||||
import { useGithubSetupWarningDelay } from "./hooks/useGithubSetupWarningDelay";
|
||||
@@ -421,6 +422,15 @@ function AppInner() {
|
||||
setThemeMode,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
recordActivity({ kind: "view", label: String(viewMode) });
|
||||
}, [viewMode]);
|
||||
useEffect(() => {
|
||||
const recordError = (event: ErrorEvent) => recordActivity({ kind: "client-error", label: event.message });
|
||||
window.addEventListener("error", recordError);
|
||||
return () => window.removeEventListener("error", recordError);
|
||||
}, []);
|
||||
|
||||
const { views: rawPluginDashboardViews } = usePluginDashboardViews(currentProject?.id);
|
||||
const graphPluginTaskView = useMemo(() => {
|
||||
// Prefer API response for the graph view (supports dynamic plugin discovery)
|
||||
|
||||
@@ -6,6 +6,18 @@ async function post(path: string, body: unknown) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export function reportDraft(input: { actionType: ReportActionType; userPrompt: string; contextRefs?: { taskId?: string; agentId?: string } }) { return post("/api/report/draft", input); }
|
||||
export function reportFile(input: { actionType: ReportActionType; report: unknown; endorseIssueNumber?: number; endorseDiscussionId?: string }) { return post("/api/report/file", input); }
|
||||
export interface ReportActivityTraceEntry { ts: string; kind: string; label: string; }
|
||||
export interface ReportContextInput { actionType: ReportActionType; userPrompt: string; contextRefs?: { taskId?: string; agentId?: string }; activityTrace?: ReportActivityTraceEntry[]; screenshotArtifactId?: string; }
|
||||
|
||||
export async function uploadReportScreenshot(blob: Blob, contextRefs?: { taskId?: string; agentId?: string }): Promise<{ artifactId: string; uri?: string }> {
|
||||
const form = new FormData();
|
||||
form.append("screenshot", blob, "report-screenshot.jpg");
|
||||
if (contextRefs) form.append("contextRefs", JSON.stringify(contextRefs));
|
||||
const response = await fetch("/api/report/attachment", { method: "POST", body: form });
|
||||
if (!response.ok) throw new Error((await response.json().catch(() => ({ error: response.statusText }))).error ?? response.statusText);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export function reportDraft(input: ReportContextInput) { return post("/api/report/draft", input); }
|
||||
export function reportFile(input: { actionType: ReportActionType; report: unknown; endorseIssueNumber?: number; endorseDiscussionId?: string; activityTrace?: ReportActivityTraceEntry[]; screenshotArtifactId?: string }) { return post("/api/report/file", input); }
|
||||
export function reportHelp(question: string) { return post("/api/report/help", { question }); }
|
||||
|
||||
@@ -3,4 +3,7 @@
|
||||
.report-modal textarea { min-block-size: var(--space-32); resize: vertical; }
|
||||
.report-modal__close { position: absolute; inset-block-start: var(--space-2); inset-inline-end: var(--space-2); }
|
||||
.report-modal__error { color: var(--color-error); }
|
||||
.report-modal__screenshot-option { display: flex; gap: var(--space-2); align-items: center; }
|
||||
.report-modal__screenshot-preview { display: grid; gap: var(--space-2); padding: var(--space-3); border-radius: var(--radius-md); background: color-mix(in srgb, var(--bg-raised) 85%, transparent); }
|
||||
.report-modal__screenshot-preview img { max-inline-size: 100%; max-block-size: var(--space-96); object-fit: contain; }
|
||||
@media (max-width: 768px) { .report-modal { inline-size: 100%; padding: var(--space-4); } }
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { ReportActionType } from "@fusion/core";
|
||||
import { reportDraft, reportFile, reportHelp } from "../api";
|
||||
import { reportDraft, reportFile, reportHelp, uploadReportScreenshot } from "../api";
|
||||
import { captureAppScreenshot, type CapturedScreenshot } from "../utils/capture-screenshot";
|
||||
import { recordActivity, snapshotActivityTrace } from "../utils/activity-trace";
|
||||
import "./ReportModal.css";
|
||||
|
||||
const prompts: Record<ReportActionType, string> = { bug: "What went wrong?", feedback: "What would you like to share?", idea: "What would you like Fusion to do?", help: "What would you like help with?" };
|
||||
@@ -16,16 +18,43 @@ export function ReportModal({ actionType, onClose, contextRefs }: { actionType:
|
||||
const [result, setResult] = useState<ModalResult>();
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
const submit = async () => {
|
||||
if (!prompt.trim()) return;
|
||||
const [screenshotEnabled, setScreenshotEnabled] = useState(false);
|
||||
const [capturedScreenshot, setCapturedScreenshot] = useState<CapturedScreenshot>();
|
||||
const [screenshotConfirmed, setScreenshotConfirmed] = useState(false);
|
||||
const [screenshotArtifactId, setScreenshotArtifactId] = useState<string>();
|
||||
useEffect(() => () => { if (capturedScreenshot) URL.revokeObjectURL(capturedScreenshot.previewUrl); }, [capturedScreenshot]);
|
||||
const captureScreenshot = async () => {
|
||||
setBusy(true);
|
||||
setError(undefined);
|
||||
try {
|
||||
const captured = await captureAppScreenshot();
|
||||
setCapturedScreenshot(captured);
|
||||
setScreenshotConfirmed(false);
|
||||
} catch (captureError) {
|
||||
setScreenshotEnabled(false);
|
||||
setError(captureError instanceof Error ? captureError.message : "We could not capture the current screen.");
|
||||
} finally { setBusy(false); }
|
||||
};
|
||||
const submit = async () => {
|
||||
if (!prompt.trim()) return;
|
||||
if (screenshotEnabled && (!capturedScreenshot || !screenshotConfirmed)) {
|
||||
setError("Preview and confirm the screenshot before continuing.");
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setError(undefined);
|
||||
try {
|
||||
recordActivity({ kind: "report", label: `${actionType} report submitted` });
|
||||
if (actionType === "help") {
|
||||
const help = await reportHelp(prompt);
|
||||
if (help.answered) { setResult({ kind: "help", answer: help.answer }); return; }
|
||||
}
|
||||
setResult(await reportDraft({ actionType, userPrompt: prompt, contextRefs }));
|
||||
const attachment = screenshotEnabled && capturedScreenshot && !screenshotArtifactId
|
||||
? await uploadReportScreenshot(capturedScreenshot.blob, contextRefs)
|
||||
: undefined;
|
||||
const artifactId = attachment?.artifactId ?? screenshotArtifactId;
|
||||
if (artifactId) setScreenshotArtifactId(artifactId);
|
||||
setResult(await reportDraft({ actionType, userPrompt: prompt, contextRefs, activityTrace: snapshotActivityTrace(), screenshotArtifactId: artifactId }));
|
||||
} catch {
|
||||
setError("We could not prepare your report. Check your connection and try again.");
|
||||
} finally { setBusy(false); }
|
||||
@@ -35,7 +64,7 @@ export function ReportModal({ actionType, onClose, contextRefs }: { actionType:
|
||||
setBusy(true);
|
||||
setError(undefined);
|
||||
try {
|
||||
setResult(await reportFile({ actionType, report: result.report, endorseIssueNumber, endorseDiscussionId }));
|
||||
setResult(await reportFile({ actionType, report: result.report, endorseIssueNumber, endorseDiscussionId, activityTrace: snapshotActivityTrace(), screenshotArtifactId }));
|
||||
} catch {
|
||||
setError("We could not send your report. Your draft is still here; try again.");
|
||||
} finally { setBusy(false); }
|
||||
@@ -43,7 +72,13 @@ export function ReportModal({ actionType, onClose, contextRefs }: { actionType:
|
||||
return <div className="report-modal-backdrop" role="presentation"><section className="card report-modal" role="dialog" aria-modal="true" aria-label={`${actionType} report`}>
|
||||
<button className="btn-icon report-modal__close" type="button" aria-label="Close report" onClick={onClose}>×</button>
|
||||
{error && <p className="report-modal__error" role="alert">{error}</p>}
|
||||
{!result && <><h2>{actionType[0].toUpperCase() + actionType.slice(1)}</h2><label htmlFor="report-prompt">{prompts[actionType]}</label><textarea id="report-prompt" className="input" value={prompt} onChange={(event) => setPrompt(event.target.value)} maxLength={4000} /><button className="btn btn-primary" type="button" disabled={!prompt.trim() || busy} onClick={() => void submit()}>{error ? "Retry" : "Continue"}</button></>}
|
||||
{!result && <><h2>{actionType[0].toUpperCase() + actionType.slice(1)}</h2><label htmlFor="report-prompt">{prompts[actionType]}</label><textarea id="report-prompt" className="input" value={prompt} onChange={(event) => setPrompt(event.target.value)} maxLength={4000} />
|
||||
{/* FNXC:ReportPipeline 2026-07-16-11:00: Screenshot capture is opt-in and always previewed and explicitly confirmed before its local upload or any report filing, including auto-file mode. */}
|
||||
<label className="report-modal__screenshot-option"><input type="checkbox" checked={screenshotEnabled} onChange={(event) => { setScreenshotEnabled(event.target.checked); if (event.target.checked) void captureScreenshot(); else { setScreenshotConfirmed(false); setScreenshotArtifactId(undefined); } }} /> Attach a screenshot of the current screen</label>
|
||||
{screenshotEnabled && <div className="report-modal__screenshot-preview">
|
||||
{capturedScreenshot ? <><img src={capturedScreenshot.previewUrl} alt="Preview of the screenshot that will be retained locally" /><label><input type="checkbox" checked={screenshotConfirmed} onChange={(event) => setScreenshotConfirmed(event.target.checked)} /> I confirm this preview may be retained locally with my report.</label></> : <p>Capturing a preview…</p>}
|
||||
</div>}
|
||||
<button className="btn btn-primary" type="button" disabled={!prompt.trim() || busy} onClick={() => void submit()}>{error ? "Retry" : "Continue"}</button></>}
|
||||
{result?.kind === "draft-ready" && result.report && <><h2>Review your report</h2><label htmlFor="report-review-prompt">Report summary</label><textarea id="report-review-prompt" className="input" value={result.report.userPrompt} onChange={(event) => {
|
||||
const userPrompt = event.target.value;
|
||||
// FNXC:ReportPipeline 2026-07-16-18:45:
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { clearActivityTraceForTests, recordActivity, snapshotActivityTrace } from "../activity-trace.js";
|
||||
|
||||
afterEach(clearActivityTraceForTests);
|
||||
|
||||
describe("activity trace", () => {
|
||||
it("keeps the newest twenty entries", () => {
|
||||
for (let index = 0; index < 25; index++) recordActivity({ ts: String(index), kind: "view", label: `view ${index}` });
|
||||
const trace = snapshotActivityTrace();
|
||||
expect(trace).toHaveLength(20);
|
||||
expect(trace[0]?.ts).toBe("5");
|
||||
});
|
||||
|
||||
it("evicts old entries when the text budget is exceeded", () => {
|
||||
for (let index = 0; index < 10; index++) recordActivity({ kind: "action", label: String(index).repeat(900) });
|
||||
const trace = snapshotActivityTrace();
|
||||
expect(trace.reduce((total, entry) => total + entry.ts.length + entry.kind.length + entry.label.length, 0)).toBeLessThanOrEqual(4000);
|
||||
});
|
||||
});
|
||||
37
packages/dashboard/app/utils/activity-trace.ts
Normal file
37
packages/dashboard/app/utils/activity-trace.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export interface ActivityTraceEntry {
|
||||
ts: string;
|
||||
kind: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const MAX_ENTRIES = 20;
|
||||
const MAX_CHARS = 4_000;
|
||||
const entries: ActivityTraceEntry[] = [];
|
||||
|
||||
function size(entry: ActivityTraceEntry): number {
|
||||
return entry.ts.length + entry.kind.length + entry.label.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* FNXC:ReportPipeline 2026-07-16-09:00:
|
||||
* Reports may include a small user-facing activity trace by default. Keep it
|
||||
* local, bounded text so it receives the same server-side scrub as all report
|
||||
* context before any GitHub egress.
|
||||
*/
|
||||
export function recordActivity(entry: Omit<ActivityTraceEntry, "ts"> & { ts?: string }): void {
|
||||
const next: ActivityTraceEntry = {
|
||||
ts: typeof entry.ts === "string" ? entry.ts.slice(0, 64) : new Date().toISOString(),
|
||||
kind: String(entry.kind).slice(0, 80),
|
||||
label: String(entry.label).slice(0, 1_000),
|
||||
};
|
||||
entries.push(next);
|
||||
while (entries.length > MAX_ENTRIES || entries.reduce((total, item) => total + size(item), 0) > MAX_CHARS) entries.shift();
|
||||
}
|
||||
|
||||
export function snapshotActivityTrace(): ActivityTraceEntry[] {
|
||||
return entries.map((entry) => ({ ...entry }));
|
||||
}
|
||||
|
||||
export function clearActivityTraceForTests(): void {
|
||||
entries.length = 0;
|
||||
}
|
||||
22
packages/dashboard/app/utils/capture-screenshot.ts
Normal file
22
packages/dashboard/app/utils/capture-screenshot.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import html2canvas from "html2canvas";
|
||||
|
||||
const MAX_SCREENSHOT_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
export interface CapturedScreenshot {
|
||||
blob: Blob;
|
||||
previewUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* FNXC:ReportPipeline 2026-07-16-10:00:
|
||||
* Screenshot capture deliberately snapshots only Fusion's app root, never the
|
||||
* display. This avoids collecting other tabs or operating-system windows.
|
||||
*/
|
||||
export async function captureAppScreenshot(): Promise<CapturedScreenshot> {
|
||||
const root = document.querySelector("#root");
|
||||
if (!root) throw new Error("The dashboard view is not available for capture.");
|
||||
const canvas = await html2canvas(root as HTMLElement, { scale: 0.75, useCORS: true, logging: false });
|
||||
const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, "image/jpeg", 0.8));
|
||||
if (!blob || blob.size > MAX_SCREENSHOT_BYTES) throw new Error("The screenshot is too large. Try a smaller dashboard view.");
|
||||
return { blob, previewUrl: URL.createObjectURL(blob) };
|
||||
}
|
||||
@@ -107,6 +107,7 @@
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@codemirror/view": "^6.36.4",
|
||||
"@earendil-works/pi-coding-agent": "0.80.10",
|
||||
"@fusion-plugin-examples/claude-runtime": "workspace:*",
|
||||
"@fusion-plugin-examples/cli-printing-press": "workspace:*",
|
||||
"@fusion-plugin-examples/compound-engineering": "workspace:*",
|
||||
"@fusion-plugin-examples/cursor-runtime": "workspace:*",
|
||||
@@ -132,6 +133,7 @@
|
||||
"@xyflow/react": "^12.11.0",
|
||||
"archiver": "^7.0.1",
|
||||
"express": "^5.1.0",
|
||||
"html2canvas": "1.4.1",
|
||||
"i18next": "^26.3.1",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
@@ -151,8 +153,7 @@
|
||||
"remark-gfm": "^4.0.1",
|
||||
"unified": "^11.0.5",
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.25.76",
|
||||
"@fusion-plugin-examples/claude-runtime": "workspace:*"
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
|
||||
@@ -26,6 +26,19 @@ describe("report pipeline", () => {
|
||||
expect(context.client!.createIssue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("scrubs activity trace text along with all report context", async () => {
|
||||
const result = await runReportPipeline({
|
||||
actionType: "bug",
|
||||
userPrompt: "report failure",
|
||||
activityTrace: [{ ts: "2026-07-16T00:00:00Z", kind: "error", label: "Jane Doe at /Users/alice/private-project/a.ts emailed alice@example.com with ghp_abcdefghijk1234567890 and sk-abcdefghijklmnopqrstuvwxyz" }],
|
||||
}, deps());
|
||||
expect(result.kind).toBe("draft-ready");
|
||||
if (result.kind === "draft-ready") {
|
||||
expect(result.report.body).toContain("activityTrace");
|
||||
expect(result.report.body).not.toMatch(/private-project|alice@example\.com|ghp_|sk-|Jane Doe/);
|
||||
}
|
||||
});
|
||||
|
||||
it("only accepts open duplicate matches", async () => {
|
||||
const context = deps({ client: { createIssue: vi.fn(), addIssueReaction: vi.fn(), commentOnIssue: vi.fn(), searchIssues: vi.fn().mockResolvedValue([{ number: 1, title: "dashboard rendering failed issue", body: "dashboard rendering failed", html_url: "url", state: "closed" }]) } });
|
||||
const result = await runReportPipeline({ actionType: "bug", userPrompt: "dashboard rendering failed" }, context);
|
||||
|
||||
@@ -7,10 +7,18 @@ import { scrubReportPayload, type ReportScrubContext } from "./report-scrub.js";
|
||||
|
||||
export type { ReportActionType, ReportMode };
|
||||
|
||||
export interface ReportActivityTraceEntry {
|
||||
ts: string;
|
||||
kind: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface ReportInput {
|
||||
actionType: ReportActionType;
|
||||
userPrompt: string;
|
||||
contextRefs?: { taskId?: string; agentId?: string };
|
||||
activityTrace?: ReportActivityTraceEntry[];
|
||||
screenshotArtifactId?: string;
|
||||
}
|
||||
|
||||
export interface StructuredReport {
|
||||
@@ -39,6 +47,12 @@ export interface ReportPipelineDeps {
|
||||
}
|
||||
|
||||
const MAX_PROMPT_LENGTH = 4_000;
|
||||
/*
|
||||
FNXC:ReportPipeline 2026-07-16-10:45:
|
||||
Screenshot capture remains a per-report, off-by-default user choice rather than
|
||||
project policy. Activity trace is default-on client context because it is bounded
|
||||
and scrubbed; no persisted settings are needed for either behavior.
|
||||
*/
|
||||
const endorsedSessions = new Map<string, { url: string; issueNumber: number }>();
|
||||
|
||||
export function resolveReportMode(actionType: ReportActionType, settings: ReportPipelineDeps["projectSettings"]): ReportMode {
|
||||
@@ -69,7 +83,10 @@ function expectedBehavior(actionType: ReportActionType): string {
|
||||
|
||||
function structureReport(input: ReportInput, gathered: Record<string, unknown>): StructuredReport {
|
||||
const prompt = requirePrompt(input);
|
||||
const context = { actionType: input.actionType, ...gathered, ...input.contextRefs };
|
||||
// FNXC:ReportPipeline 2026-07-16-09:00:
|
||||
// Activity trace is ordinary text context. It must continue through
|
||||
// scrubReportPayload with every other report field before GitHub egress.
|
||||
const context = { actionType: input.actionType, ...gathered, ...input.contextRefs, ...(input.activityTrace?.length ? { activityTrace: input.activityTrace } : {}) };
|
||||
const formattedContext = formatContext(context);
|
||||
return {
|
||||
userPrompt: prompt,
|
||||
@@ -175,7 +192,15 @@ function normalizeSubmittedReport(input: ReportInput, gathered: Record<string, u
|
||||
|
||||
export async function runReportPipeline(input: ReportInput, deps: ReportPipelineDeps, options: { file?: boolean; endorseIssueNumber?: number; endorseDiscussionId?: string; report?: StructuredReport } = {}): Promise<ReportResult> {
|
||||
const gathered = await deps.gatherContext?.(input) ?? { taskId: input.contextRefs?.taskId, agentId: input.contextRefs?.agentId };
|
||||
const report = scrubReportPayload(normalizeSubmittedReport(input, gathered, options.report), deps.scrubContext);
|
||||
let report = scrubReportPayload(normalizeSubmittedReport(input, gathered, options.report), deps.scrubContext);
|
||||
if (input.screenshotArtifactId) {
|
||||
// FNXC:ReportPipeline 2026-07-16-10:30:
|
||||
// The route has validated this reference before pipeline entry. Rebuild the
|
||||
// text-only local-retention note here so edited drafts cannot omit it and
|
||||
// image bytes never cross the GitHub boundary.
|
||||
const note = `Screenshot captured and retained locally (artifact ${input.screenshotArtifactId}).`;
|
||||
if (!report.body.includes(note)) report = { ...report, body: `${report.body}\n\n${note}` };
|
||||
}
|
||||
const mode = resolveReportMode(input.actionType, deps.projectSettings);
|
||||
const clientResult = createClient(deps);
|
||||
if (clientResult.unavailable) return clientResult.unavailable;
|
||||
|
||||
@@ -2257,7 +2257,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
// index holds sensitive repo/PR content so no endpoint is unauthenticated or
|
||||
// cross-project readable.
|
||||
registerKnowledgeRoutes(routeContext);
|
||||
registerReportRoutes(routeContext);
|
||||
registerReportRoutes({ ...routeContext, reportUpload: upload });
|
||||
// U11 — inbound external signal webhooks (Sentry/Datadog/PagerDuty/generic).
|
||||
// Each route HMAC-verifies against a per-provider secret; never an
|
||||
// unauthenticated task-creation endpoint.
|
||||
|
||||
@@ -4,9 +4,53 @@ import { requireAsyncLayer } from "../require-async-layer.js";
|
||||
import { runReportPipeline, type ReportInput, type StructuredReport } from "../report-pipeline.js";
|
||||
import { scrubReportPayload } from "../report-scrub.js";
|
||||
import { selfCheckHelp } from "../report-help-selfcheck.js";
|
||||
import type { Request, Response } from "express";
|
||||
import type { ApiRouteRegistrar } from "./types.js";
|
||||
|
||||
const ACTION_TYPES = new Set(["bug", "feedback", "idea", "help"]);
|
||||
const MAX_ACTIVITY_TRACE_ENTRIES = 20;
|
||||
const MAX_ACTIVITY_TRACE_CHARS = 4_000;
|
||||
const MAX_SCREENSHOT_BYTES = 2 * 1024 * 1024;
|
||||
const ARTIFACT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
const REPORT_ATTACHMENT_SOURCE = "report-attachment";
|
||||
|
||||
type ScopedStore = Awaited<ReturnType<Parameters<ApiRouteRegistrar>[0]["getScopedStore"]>>;
|
||||
|
||||
function isImagePayload(mimeType: string | undefined, bytes: Buffer): boolean {
|
||||
const png = bytes.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
|
||||
const jpeg = bytes.subarray(0, 3).equals(Buffer.from([0xff, 0xd8, 0xff]));
|
||||
return (mimeType === "image/png" && png) || (mimeType === "image/jpeg" && jpeg);
|
||||
}
|
||||
|
||||
async function validateScreenshotArtifact(store: ScopedStore, input: ReportInput): Promise<ReportInput> {
|
||||
if (!input.screenshotArtifactId) return input;
|
||||
// FNXC:ReportPipeline 2026-07-16-10:30:
|
||||
// The client reference is untrusted. Validate UUID, scoped artifact type,
|
||||
// MIME, and report-upload provenance before it can become egressed text;
|
||||
// this blocks data-URI and arbitrary-text smuggling into GitHub reports.
|
||||
if (!ARTIFACT_ID_PATTERN.test(input.screenshotArtifactId)) throw new ApiError(400, "Invalid report screenshot reference.");
|
||||
const artifact = await store.getArtifact(input.screenshotArtifactId);
|
||||
if (artifact?.type !== "image" || !["image/png", "image/jpeg"].includes(artifact.mimeType ?? "") || artifact.metadata?.source !== REPORT_ATTACHMENT_SOURCE) {
|
||||
throw new ApiError(400, "Invalid report screenshot reference.");
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
function runUpload(upload: NonNullable<Parameters<ApiRouteRegistrar>[0]["reportUpload"]>, req: Request, res: Response): Promise<void> {
|
||||
return new Promise((resolve, reject) => upload.single("screenshot")(req, res, (error?: unknown) => error ? reject(error) : resolve()));
|
||||
}
|
||||
|
||||
function parseActivityTrace(value: unknown): ReportInput["activityTrace"] {
|
||||
if (!Array.isArray(value)) return undefined;
|
||||
const entries = value.flatMap((entry) => {
|
||||
if (!entry || typeof entry !== "object") return [];
|
||||
const candidate = entry as Record<string, unknown>;
|
||||
if (typeof candidate.ts !== "string" || typeof candidate.kind !== "string" || typeof candidate.label !== "string") return [];
|
||||
return [{ ts: candidate.ts.slice(0, 64), kind: candidate.kind.slice(0, 80), label: candidate.label.slice(0, 1_000) }];
|
||||
});
|
||||
while (entries.length > MAX_ACTIVITY_TRACE_ENTRIES || entries.reduce((total, entry) => total + entry.ts.length + entry.kind.length + entry.label.length, 0) > MAX_ACTIVITY_TRACE_CHARS) entries.shift();
|
||||
return entries;
|
||||
}
|
||||
|
||||
async function gatherReportContext(store: Awaited<ReturnType<Parameters<ApiRouteRegistrar>[0]["getScopedStore"]>>, input: ReportInput, settings: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||||
const context: Record<string, unknown> = {
|
||||
@@ -14,6 +58,8 @@ async function gatherReportContext(store: Awaited<ReturnType<Parameters<ApiRoute
|
||||
githubAuthMode: settings.githubAuthMode,
|
||||
taskId: input.contextRefs?.taskId,
|
||||
agentId: input.contextRefs?.agentId,
|
||||
activityTrace: input.activityTrace,
|
||||
...(input.screenshotArtifactId ? { screenshot: `Screenshot captured and retained locally (artifact ${input.screenshotArtifactId}).` } : {}),
|
||||
};
|
||||
if (!input.contextRefs?.taskId) return context;
|
||||
|
||||
@@ -36,7 +82,13 @@ function parseInput(body: unknown): ReportInput {
|
||||
const actionType = typeof value.actionType === "string" ? value.actionType : "";
|
||||
const userPrompt = typeof value.userPrompt === "string" ? value.userPrompt : "";
|
||||
if (!ACTION_TYPES.has(actionType) || !userPrompt.trim()) throw new ApiError(400, "A report type and description are required.");
|
||||
return { actionType: actionType as ReportInput["actionType"], userPrompt, contextRefs: typeof value.contextRefs === "object" && value.contextRefs ? value.contextRefs as ReportInput["contextRefs"] : undefined };
|
||||
return {
|
||||
actionType: actionType as ReportInput["actionType"],
|
||||
userPrompt,
|
||||
contextRefs: typeof value.contextRefs === "object" && value.contextRefs ? value.contextRefs as ReportInput["contextRefs"] : undefined,
|
||||
activityTrace: parseActivityTrace(value.activityTrace),
|
||||
screenshotArtifactId: typeof value.screenshotArtifactId === "string" ? value.screenshotArtifactId : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,12 +97,32 @@ function parseInput(body: unknown): ReportInput {
|
||||
* route treats edited drafts as untrusted and re-scrubs server-side immediately
|
||||
* before the pipeline may call GitHub.
|
||||
*/
|
||||
export const registerReportRoutes: ApiRouteRegistrar = ({ router, getScopedStore, rethrowAsApiError }) => {
|
||||
export const registerReportRoutes: ApiRouteRegistrar = ({ router, getScopedStore, rethrowAsApiError, reportUpload }) => {
|
||||
router.post("/report/attachment", async (req, res) => {
|
||||
try {
|
||||
if (!reportUpload) throw new ApiError(500, "Report attachment upload is unavailable.");
|
||||
await runUpload(reportUpload, req, res);
|
||||
const file = req.file;
|
||||
if (!file || file.size > MAX_SCREENSHOT_BYTES || !isImagePayload(file.mimetype, file.buffer)) throw new ApiError(400, "Report screenshots must be PNG or JPEG files up to 2MB.");
|
||||
const store = await getScopedStore(req);
|
||||
const contextRefs = typeof req.body?.contextRefs === "string" ? JSON.parse(req.body.contextRefs) : req.body?.contextRefs;
|
||||
const taskId = contextRefs && typeof contextRefs.taskId === "string" ? contextRefs.taskId : undefined;
|
||||
const artifact = await store.registerArtifact({ type: "image", title: "Report screenshot", mimeType: file.mimetype, data: Buffer.from(file.buffer), taskId, authorType: "user", authorId: "dashboard-user", metadata: { source: REPORT_ATTACHMENT_SOURCE } });
|
||||
// FNXC:ReportPipeline 2026-07-16-10:30:
|
||||
// Pixels are unscrubbable. Persist this optional, confirmed screenshot
|
||||
// locally only; no GitHub transport receives its bytes or a data URI.
|
||||
res.status(201).json({ artifactId: artifact.id, uri: artifact.uri });
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) throw error;
|
||||
rethrowAsApiError(error, "Failed to retain report screenshot");
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/report/draft", async (req, res) => {
|
||||
try {
|
||||
const store = await getScopedStore(req);
|
||||
const scopes = await store.getSettingsByScopeFast();
|
||||
const input = parseInput(req.body);
|
||||
const input = await validateScreenshotArtifact(store, parseInput(req.body));
|
||||
const help = await selfCheckHelpBeforePipeline(store, input);
|
||||
if (help?.answered) {
|
||||
res.json({ kind: "help", answer: help.answer });
|
||||
@@ -82,15 +154,18 @@ export const registerReportRoutes: ApiRouteRegistrar = ({ router, getScopedStore
|
||||
taskId: typeof (untrusted.context as Record<string, unknown>).taskId === "string" ? (untrusted.context as Record<string, unknown>).taskId : undefined,
|
||||
agentId: typeof (untrusted.context as Record<string, unknown>).agentId === "string" ? (untrusted.context as Record<string, unknown>).agentId : undefined,
|
||||
},
|
||||
activityTrace: raw.activityTrace ?? (untrusted.context as Record<string, unknown> | undefined)?.activityTrace,
|
||||
screenshotArtifactId: raw.screenshotArtifactId ?? (untrusted.context as Record<string, unknown> | undefined)?.screenshotArtifactId,
|
||||
});
|
||||
const validatedInput = await validateScreenshotArtifact(store, input);
|
||||
const endorseIssueNumber = typeof raw.endorseIssueNumber === "number" ? raw.endorseIssueNumber : undefined;
|
||||
const endorseDiscussionId = typeof raw.endorseDiscussionId === "string" ? raw.endorseDiscussionId : undefined;
|
||||
const help = await selfCheckHelpBeforePipeline(store, input);
|
||||
const help = await selfCheckHelpBeforePipeline(store, validatedInput);
|
||||
if (help?.answered) {
|
||||
res.json({ kind: "help", answer: help.answer });
|
||||
return;
|
||||
}
|
||||
const result = await runReportPipeline(input, {
|
||||
const result = await runReportPipeline(validatedInput, {
|
||||
projectSettings: scopes.project,
|
||||
globalSettings: scopes.global,
|
||||
scrubContext: { rootDir: store.getRootDir(), projectName: store.getRootDir().split(/[\\/]/).pop() },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Request, Router } from "express";
|
||||
import type { Request, RequestHandler, Router } from "express";
|
||||
import type { AutomationStore, RoutineStore, TaskStore } from "@fusion/core";
|
||||
import type { ServerOptions } from "../server.js";
|
||||
import type { RuntimeLogger } from "../runtime-logger.js";
|
||||
@@ -42,6 +42,8 @@ export type ScopeValue = "global" | "project";
|
||||
export interface ApiRoutesContext {
|
||||
router: Router;
|
||||
store: TaskStore;
|
||||
/** Narrow multipart seam for routes that must accept local binary artifacts. */
|
||||
reportUpload?: { single(fieldName: string): RequestHandler };
|
||||
options?: ServerOptions;
|
||||
runtimeLogger: RuntimeLogger;
|
||||
planningLogger: RuntimeLogger;
|
||||
|
||||
129
pnpm-lock.yaml
generated
129
pnpm-lock.yaml
generated
@@ -52,10 +52,10 @@ importers:
|
||||
dependencies:
|
||||
'@earendil-works/pi-ai':
|
||||
specifier: 0.80.10
|
||||
version: 0.80.10(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
|
||||
version: 0.80.10(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-coding-agent':
|
||||
specifier: 0.80.10
|
||||
version: 0.80.10(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
|
||||
version: 0.80.10(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
|
||||
claude-code-cli-acp:
|
||||
specifier: 0.1.1
|
||||
version: 0.1.1
|
||||
@@ -329,6 +329,9 @@ importers:
|
||||
express:
|
||||
specifier: ^5.1.0
|
||||
version: 5.2.1
|
||||
html2canvas:
|
||||
specifier: 1.4.1
|
||||
version: 1.4.1
|
||||
i18next:
|
||||
specifier: ^26.3.1
|
||||
version: 26.3.1(typescript@5.9.3)
|
||||
@@ -3948,6 +3951,10 @@ packages:
|
||||
bare-url@2.4.0:
|
||||
resolution: {integrity: sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==}
|
||||
|
||||
base64-arraybuffer@1.0.2:
|
||||
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
@@ -4430,6 +4437,9 @@ packages:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
css-line-break@2.1.0:
|
||||
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
|
||||
|
||||
css-tree@3.2.1:
|
||||
resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
|
||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
|
||||
@@ -5468,6 +5478,10 @@ packages:
|
||||
html-void-elements@3.0.0:
|
||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||
|
||||
html2canvas@1.4.1:
|
||||
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
http-cache-semantics@4.2.0:
|
||||
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
|
||||
|
||||
@@ -7485,6 +7499,9 @@ packages:
|
||||
text-decoder@1.2.7:
|
||||
resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==}
|
||||
|
||||
text-segmentation@1.0.3:
|
||||
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
|
||||
|
||||
thenify-all@1.6.0:
|
||||
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
||||
engines: {node: '>=0.8'}
|
||||
@@ -7760,6 +7777,9 @@ packages:
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
utrie@1.0.2:
|
||||
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
|
||||
|
||||
uuid@10.0.0:
|
||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
@@ -8123,10 +8143,6 @@ snapshots:
|
||||
package-manager-detector: 1.6.0
|
||||
tinyexec: 1.2.4
|
||||
|
||||
'@anthropic-ai/sdk@0.91.1':
|
||||
dependencies:
|
||||
json-schema-to-ts: 3.1.1
|
||||
|
||||
'@anthropic-ai/sdk@0.91.1(zod@3.25.76)':
|
||||
dependencies:
|
||||
json-schema-to-ts: 3.1.1
|
||||
@@ -8869,9 +8885,9 @@ snapshots:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@earendil-works/pi-agent-core@0.80.10(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)':
|
||||
'@earendil-works/pi-agent-core@0.80.10(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
|
||||
'@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
|
||||
ignore: 7.0.5
|
||||
typebox: 1.1.38
|
||||
yaml: 2.9.0
|
||||
@@ -8913,15 +8929,15 @@ snapshots:
|
||||
|
||||
'@earendil-works/pi-ai@0.80.10':
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk': 0.91.1
|
||||
'@anthropic-ai/sdk': 0.91.1(zod@4.3.6)
|
||||
'@aws-sdk/client-bedrock-runtime': 3.1048.0
|
||||
'@google/genai': 1.52.0
|
||||
'@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))
|
||||
'@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@smithy/node-http-handler': 4.7.3
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
openai: 6.26.0
|
||||
openai: 6.26.0(ws@8.20.0)(zod@4.3.6)
|
||||
partial-json: 0.1.7
|
||||
typebox: 1.1.38
|
||||
transitivePeerDependencies:
|
||||
@@ -8932,17 +8948,17 @@ snapshots:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@earendil-works/pi-ai@0.80.10(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)':
|
||||
'@earendil-works/pi-ai@0.80.10(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk': 0.91.1(zod@3.25.76)
|
||||
'@anthropic-ai/sdk': 0.91.1(zod@4.3.6)
|
||||
'@aws-sdk/client-bedrock-runtime': 3.1048.0
|
||||
'@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))
|
||||
'@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))
|
||||
'@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@smithy/node-http-handler': 4.7.3
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
openai: 6.26.0(ws@8.20.0)(zod@3.25.76)
|
||||
openai: 6.26.0(ws@8.20.0)(zod@4.3.6)
|
||||
partial-json: 0.1.7
|
||||
typebox: 1.1.38
|
||||
transitivePeerDependencies:
|
||||
@@ -8978,7 +8994,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk': 0.91.1(zod@3.25.76)
|
||||
'@aws-sdk/client-bedrock-runtime': 3.1048.0
|
||||
'@google/genai': 1.52.0
|
||||
'@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))
|
||||
'@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@smithy/node-http-handler': 4.7.3
|
||||
@@ -9025,10 +9041,10 @@ snapshots:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@earendil-works/pi-coding-agent@0.80.10(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)':
|
||||
'@earendil-works/pi-coding-agent@0.80.10(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@earendil-works/pi-agent-core': 0.80.10(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
|
||||
'@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.20.0)(zod@3.25.76)
|
||||
'@earendil-works/pi-agent-core': 0.80.10(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-tui': 0.80.10
|
||||
'@silvia-odwyer/photon-node': 0.3.4
|
||||
chalk: 5.6.2
|
||||
@@ -9455,30 +9471,6 @@ snapshots:
|
||||
|
||||
'@exodus/bytes@1.15.0': {}
|
||||
|
||||
'@google/genai@1.52.0':
|
||||
dependencies:
|
||||
google-auth-library: 10.6.2
|
||||
p-retry: 4.6.2
|
||||
protobufjs: 7.5.8
|
||||
ws: 8.21.1
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@google/genai@1.52.0(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))':
|
||||
dependencies:
|
||||
google-auth-library: 10.6.2
|
||||
p-retry: 4.6.2
|
||||
protobufjs: 7.5.8
|
||||
ws: 8.21.1
|
||||
optionalDependencies:
|
||||
'@modelcontextprotocol/sdk': 1.28.0(zod@3.25.76)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@google/genai@1.52.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))':
|
||||
dependencies:
|
||||
google-auth-library: 10.6.2
|
||||
@@ -9998,29 +9990,6 @@ snapshots:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@modelcontextprotocol/sdk@1.28.0(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@hono/node-server': 1.19.12(hono@4.12.9)
|
||||
ajv: 8.18.0
|
||||
ajv-formats: 3.0.1(ajv@8.18.0)
|
||||
content-type: 1.0.5
|
||||
cors: 2.8.6
|
||||
cross-spawn: 7.0.6
|
||||
eventsource: 3.0.7
|
||||
eventsource-parser: 3.0.6
|
||||
express: 5.2.1
|
||||
express-rate-limit: 8.3.1(express@5.2.1)
|
||||
hono: 4.12.9
|
||||
jose: 6.2.2
|
||||
json-schema-typed: 8.0.2
|
||||
pkce-challenge: 5.0.1
|
||||
raw-body: 3.0.2
|
||||
zod: 3.25.76
|
||||
zod-to-json-schema: 3.25.1(zod@3.25.76)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
'@modelcontextprotocol/sdk@1.28.0(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@hono/node-server': 1.19.12(hono@4.12.9)
|
||||
@@ -10894,7 +10863,7 @@ snapshots:
|
||||
obug: 2.1.2
|
||||
std-env: 4.1.0
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@25.5.2)(@vitest/coverage-v8@4.1.8)(happy-dom@20.10.1)(jsdom@29.0.1)(vite@6.4.1(@types/node@25.5.2)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@25.5.2)(@vitest/coverage-v8@4.1.8)(happy-dom@20.10.1)(jsdom@29.0.1)(vite@6.4.1(@types/node@25.5.2)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))
|
||||
|
||||
'@vitest/expect@4.1.8':
|
||||
dependencies:
|
||||
@@ -11297,6 +11266,8 @@ snapshots:
|
||||
dependencies:
|
||||
bare-path: 3.0.0
|
||||
|
||||
base64-arraybuffer@1.0.2: {}
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
baseline-browser-mapping@2.10.10: {}
|
||||
@@ -11768,6 +11739,10 @@ snapshots:
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
css-line-break@2.1.0:
|
||||
dependencies:
|
||||
utrie: 1.0.2
|
||||
|
||||
css-tree@3.2.1:
|
||||
dependencies:
|
||||
mdn-data: 2.27.1
|
||||
@@ -13018,6 +12993,11 @@ snapshots:
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
|
||||
html2canvas@1.4.1:
|
||||
dependencies:
|
||||
css-line-break: 2.1.0
|
||||
text-segmentation: 1.0.3
|
||||
|
||||
http-cache-semantics@4.2.0: {}
|
||||
|
||||
http-errors@2.0.1:
|
||||
@@ -14237,13 +14217,16 @@ snapshots:
|
||||
is-docker: 2.2.1
|
||||
is-wsl: 2.2.0
|
||||
|
||||
openai@6.26.0: {}
|
||||
|
||||
openai@6.26.0(ws@8.20.0)(zod@3.25.76):
|
||||
optionalDependencies:
|
||||
ws: 8.20.0
|
||||
zod: 3.25.76
|
||||
|
||||
openai@6.26.0(ws@8.20.0)(zod@4.3.6):
|
||||
optionalDependencies:
|
||||
ws: 8.20.0
|
||||
zod: 4.3.6
|
||||
|
||||
openai@6.26.0(ws@8.21.1)(zod@4.3.6):
|
||||
optionalDependencies:
|
||||
ws: 8.21.1
|
||||
@@ -15391,6 +15374,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- react-native-b4a
|
||||
|
||||
text-segmentation@1.0.3:
|
||||
dependencies:
|
||||
utrie: 1.0.2
|
||||
|
||||
thenify-all@1.6.0:
|
||||
dependencies:
|
||||
thenify: 3.3.1
|
||||
@@ -15658,6 +15645,10 @@ snapshots:
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
utrie@1.0.2:
|
||||
dependencies:
|
||||
base64-arraybuffer: 1.0.2
|
||||
|
||||
uuid@10.0.0: {}
|
||||
|
||||
uuid@14.0.1: {}
|
||||
|
||||
Reference in New Issue
Block a user