From e06694558a9e28b53d15cc21b8a91e4397c70927 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Thu, 19 Mar 2026 16:04:07 +0700 Subject: [PATCH 001/227] fix(mentions): decode HTML entities and support multi-word agent names The @mention parser used a regex that only captured single tokens, breaking detection for multi-word agent names (e.g. "Software Agent 1"). HTML entities leaked from rich-text editors also prevented matching. Replace the regex approach with direct name lookup after HTML entity decoding, supporting multi-word names and encoded characters correctly. Fixes #1255 Co-Authored-By: Paperclip --- server/src/__tests__/mention-parsing.test.ts | 100 +++++++++++++++++++ server/src/services/issues.ts | 55 ++++++++-- 2 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 server/src/__tests__/mention-parsing.test.ts diff --git a/server/src/__tests__/mention-parsing.test.ts b/server/src/__tests__/mention-parsing.test.ts new file mode 100644 index 0000000000..1b84572e01 --- /dev/null +++ b/server/src/__tests__/mention-parsing.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from "vitest"; +import { matchMentionedNames } from "../services/issues.js"; + +describe("matchMentionedNames", () => { + const agents = ["CEO", "Software Agent 1", "CTO"]; + + it("matches a single-word agent name", () => { + const result = matchMentionedNames("Hello @CEO please review", agents); + expect(result).toEqual(new Set(["ceo"])); + }); + + it("matches a multi-word agent name", () => { + const result = matchMentionedNames("Hey @Software Agent 1 can you fix this?", agents); + expect(result).toEqual(new Set(["software agent 1"])); + }); + + it("matches multiple agents in one body", () => { + const result = matchMentionedNames("@CEO and @CTO please review", agents); + expect(result).toEqual(new Set(["ceo", "cto"])); + }); + + it("matches agent at start of body", () => { + const result = matchMentionedNames("@CEO fix this", agents); + expect(result).toEqual(new Set(["ceo"])); + }); + + it("matches agent at end of body", () => { + const result = matchMentionedNames("Please review @CEO", agents); + expect(result).toEqual(new Set(["ceo"])); + }); + + it("is case-insensitive", () => { + const result = matchMentionedNames("Hello @ceo and @software agent 1", agents); + expect(result).toEqual(new Set(["ceo", "software agent 1"])); + }); + + it("does not match email-like patterns", () => { + const result = matchMentionedNames("Send to admin@CEO.com", agents); + expect(result).toEqual(new Set()); + }); + + it("does not match partial name prefixes", () => { + const result = matchMentionedNames("Hello @CEOx", agents); + expect(result).toEqual(new Set()); + }); + + it("returns empty set when no @ present", () => { + const result = matchMentionedNames("No mentions here", agents); + expect(result).toEqual(new Set()); + }); + + // HTML entity decoding tests + it("decodes & in body before matching", () => { + const result = matchMentionedNames("R&D team: @CEO review", agents); + expect(result).toEqual(new Set(["ceo"])); + }); + + it("decodes @ (@ as numeric entity) before matching", () => { + const result = matchMentionedNames("Hello @CEO please review", agents); + expect(result).toEqual(new Set(["ceo"])); + }); + + it("decodes @ (@ as hex entity) before matching", () => { + const result = matchMentionedNames("Hello @CEO please review", agents); + expect(result).toEqual(new Set(["ceo"])); + }); + + it("handles   around mentions", () => { + const result = matchMentionedNames("Hello @CEO review", agents); + expect(result).toEqual(new Set(["ceo"])); + }); + + it("handles < and > around mentions", () => { + const result = matchMentionedNames("<@CEO> review this", agents); + expect(result).toEqual(new Set(["ceo"])); + }); + + it("matches after newline", () => { + const result = matchMentionedNames("Line 1\n@CEO please review", agents); + expect(result).toEqual(new Set(["ceo"])); + }); + + it("matches after punctuation", () => { + const result = matchMentionedNames("Done. @CEO please review", agents); + expect(result).toEqual(new Set(["ceo"])); + }); + + it("matches mention followed by punctuation", () => { + const result = matchMentionedNames("Hey @CEO, please review", agents); + expect(result).toEqual(new Set(["ceo"])); + }); + + it("handles agent name containing HTML-encodable chars", () => { + const result = matchMentionedNames( + "Hey @R&D Bot please check", + ["R&D Bot", "CEO"], + ); + expect(result).toEqual(new Set(["r&d bot"])); + }); +}); diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 1f1a49619f..ff64d021f7 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -34,6 +34,50 @@ import { getDefaultCompanyGoal } from "./goals.js"; const ALL_ISSUE_STATUSES = ["backlog", "todo", "in_progress", "in_review", "blocked", "done", "cancelled"]; const MAX_ISSUE_COMMENT_PAGE_LIMIT = 500; +/** + * Decode common HTML entities that can leak from rich-text editors. + */ +function decodeHtmlEntities(text: string): string { + return text + .replace(/&#(\d+);/g, (_, code: string) => String.fromCharCode(parseInt(code, 10))) + .replace(/&#x([0-9a-fA-F]+);/g, (_, code: string) => String.fromCharCode(parseInt(code, 16))) + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/ /g, " "); +} + +/** + * Match @mentions in a body against a list of candidate names. + * Returns a Set of matched names (lowercased). + * + * Handles HTML-encoded bodies and multi-word names correctly. + */ +export function matchMentionedNames(body: string, names: string[]): Set { + const decoded = decodeHtmlEntities(body); + const lower = decoded.toLowerCase(); + const matched = new Set(); + + for (const name of names) { + const nameLower = name.toLowerCase(); + const mention = `@${nameLower}`; + let pos = 0; + while ((pos = lower.indexOf(mention, pos)) !== -1) { + // Ensure @ is not preceded by a word char (avoids email-like patterns) + if (pos > 0 && /\w/.test(lower[pos - 1])) { pos++; continue; } + // Ensure the name is not a prefix of a longer token + const end = pos + mention.length; + if (end < lower.length && /\w/.test(lower[end])) { pos++; continue; } + matched.add(nameLower); + break; + } + } + + return matched; +} + function assertTransition(from: string, to: string) { if (from === to) return; if (!ALL_ISSUE_STATUSES.includes(to)) { @@ -1443,14 +1487,13 @@ export function issueService(db: Db) { }), findMentionedAgents: async (companyId: string, body: string) => { - const re = /\B@([^\s@,!?.]+)/g; - const tokens = new Set(); - let m: RegExpExecArray | null; - while ((m = re.exec(body)) !== null) tokens.add(m[1].toLowerCase()); - if (tokens.size === 0) return []; + if (!body.includes("@")) return []; + const rows = await db.select({ id: agents.id, name: agents.name }) .from(agents).where(eq(agents.companyId, companyId)); - return rows.filter(a => tokens.has(a.name.toLowerCase())).map(a => a.id); + + const matched = matchMentionedNames(body, rows.map(a => a.name)); + return rows.filter(a => matched.has(a.name.toLowerCase())).map(a => a.id); }, findMentionedProjectIds: async (issueId: string) => { From 54b7e48f869d512c51f4f4102bb858f89c4bb0d0 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Thu, 19 Mar 2026 16:11:00 +0700 Subject: [PATCH 002/227] fix(heartbeat): prevent thundering herd on scheduler recovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes to prevent rapid-fire cascade when the server restarts or gateway recovers and many agents become due simultaneously: 1. Per-agent jitter (stableJitterMs) — deterministic hash-based offset spreads agents across the tick window so they don't all fire at once. 2. TICK_MAX_ENQUEUE cap (5) — limits how many agents can be enqueued in a single scheduler tick, distributing load over multiple ticks. 3. cooldownSec enforcement — reads the per-agent cooldownSec from config (previously parsed but unused) and skips timer-based enqueue if the agent's last heartbeat is within the cooldown window. Also adds error isolation in reapOrphanedRuns so a single run failure doesn't block recovery of other agents, and deduplicates startNextQueuedRunForAgent calls. Fixes #1241 Co-Authored-By: Paperclip --- .../src/__tests__/heartbeat-scheduler.test.ts | 56 ++++++++++ server/src/services/heartbeat.ts | 104 ++++++++++++------ server/src/services/scheduler-utils.ts | 18 +++ 3 files changed, 143 insertions(+), 35 deletions(-) create mode 100644 server/src/__tests__/heartbeat-scheduler.test.ts create mode 100644 server/src/services/scheduler-utils.ts diff --git a/server/src/__tests__/heartbeat-scheduler.test.ts b/server/src/__tests__/heartbeat-scheduler.test.ts new file mode 100644 index 0000000000..6d174d1b7c --- /dev/null +++ b/server/src/__tests__/heartbeat-scheduler.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import { stableJitterMs } from "../services/scheduler-utils.js"; + +describe("stableJitterMs", () => { + it("returns 0 when maxMs is 0", () => { + expect(stableJitterMs("agent-1", 0)).toBe(0); + }); + + it("returns 0 when maxMs is negative", () => { + expect(stableJitterMs("agent-1", -100)).toBe(0); + }); + + it("returns a value within [0, maxMs)", () => { + const maxMs = 30000; + for (const id of ["agent-1", "agent-2", "agent-3", "abc", "xyz-123"]) { + const jitter = stableJitterMs(id, maxMs); + expect(jitter).toBeGreaterThanOrEqual(0); + expect(jitter).toBeLessThan(maxMs); + } + }); + + it("is deterministic for the same agent ID", () => { + const a = stableJitterMs("agent-test", 10000); + const b = stableJitterMs("agent-test", 10000); + expect(a).toBe(b); + }); + + it("produces different values for different agent IDs", () => { + const values = new Set(); + for (let i = 0; i < 20; i++) { + values.add(stableJitterMs(`agent-${i}`, 100000)); + } + // With 20 agents and 100k max, we should get at least a few distinct values + expect(values.size).toBeGreaterThan(5); + }); + + it("spreads UUIDs across the jitter window", () => { + const maxMs = 300000; // 5 minutes + const uuids = [ + "e9665ed6-b8a1-40b8-9a05-bb4464c81167", + "b2c737ef-547f-459b-bdca-87655ca3ce7f", + "440aaf13-8817-4122-b69c-7f464a009bce", + "199a29c3-cbbc-45c5-afb8-003a6b69857e", + "e0e927e5-ce07-4584-b7d4-6ac7cb648d60", + ]; + const jitters = uuids.map(id => stableJitterMs(id, maxMs)); + // Check they're spread out (not all the same) + const uniqueJitters = new Set(jitters); + expect(uniqueJitters.size).toBeGreaterThan(1); + // All within range + for (const j of jitters) { + expect(j).toBeGreaterThanOrEqual(0); + expect(j).toBeLessThan(maxMs); + } + }); +}); diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 8e338b90d0..701735d4b2 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -55,6 +55,7 @@ import { resolveSessionCompactionPolicy, type SessionCompactionPolicy, } from "@paperclipai/adapter-utils"; +import { TICK_MAX_ENQUEUE, stableJitterMs } from "./scheduler-utils.js"; const MAX_LIVE_LOG_CHUNK_BYTES = 8 * 1024; const HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT = 1; @@ -1333,6 +1334,7 @@ export function heartbeatService(db: Db) { return { enabled: asBoolean(heartbeat.enabled, true), intervalSec: Math.max(0, asNumber(heartbeat.intervalSec, 0)), + cooldownSec: Math.max(0, asNumber(heartbeat.cooldownSec, 0)), wakeOnDemand: asBoolean(heartbeat.wakeOnDemand ?? heartbeat.wakeOnAssignment ?? heartbeat.wakeOnOnDemand ?? heartbeat.wakeOnAutomation, true), maxConcurrentRuns: normalizeMaxConcurrentRuns(heartbeat.maxConcurrentRuns), }; @@ -1458,6 +1460,8 @@ export function heartbeatService(db: Db) { .where(eq(heartbeatRuns.status, "running")); const reaped: string[] = []; + // Track unique agents that need their next queued run started (deduplicate) + const agentsToResume = new Set(); for (const run of activeRuns) { if (runningProcesses.has(run.id) || activeRunExecutions.has(run.id)) continue; @@ -1468,29 +1472,42 @@ export function heartbeatService(db: Db) { if (now.getTime() - refTime < staleThresholdMs) continue; } - await setRunStatus(run.id, "failed", { - error: "Process lost -- server may have restarted", - errorCode: "process_lost", - finishedAt: now, - }); - await setWakeupStatus(run.wakeupRequestId, "failed", { - finishedAt: now, - error: "Process lost -- server may have restarted", - }); - const updatedRun = await getRun(run.id); - if (updatedRun) { - await appendRunEvent(updatedRun, 1, { - eventType: "lifecycle", - stream: "system", - level: "error", - message: "Process lost -- server may have restarted", + try { + await setRunStatus(run.id, "failed", { + error: "Process lost -- server may have restarted", + errorCode: "process_lost", + finishedAt: now, + }); + await setWakeupStatus(run.wakeupRequestId, "failed", { + finishedAt: now, + error: "Process lost -- server may have restarted", }); - await releaseIssueExecutionAndPromote(updatedRun); + const updatedRun = await getRun(run.id); + if (updatedRun) { + await appendRunEvent(updatedRun, 1, { + eventType: "lifecycle", + stream: "system", + level: "error", + message: "Process lost -- server may have restarted", + }); + await releaseIssueExecutionAndPromote(updatedRun); + } + await finalizeAgentStatus(run.agentId, "failed"); + agentsToResume.add(run.agentId); + runningProcesses.delete(run.id); + reaped.push(run.id); + } catch (err) { + logger.error({ err, runId: run.id }, "failed to reap orphaned run"); + } + } + + // Stagger queued run resumption: start next runs sequentially per agent + for (const agentId of agentsToResume) { + try { + await startNextQueuedRunForAgent(agentId); + } catch (err) { + logger.error({ err, agentId }, "failed to start next queued run after reap"); } - await finalizeAgentStatus(run.agentId, "failed"); - await startNextQueuedRunForAgent(run.agentId); - runningProcesses.delete(run.id); - reaped.push(run.id); } if (reaped.length > 0) { @@ -3414,6 +3431,9 @@ export function heartbeatService(db: Db) { let skipped = 0; for (const agent of allAgents) { + // Rate-limit: cap enqueues per tick to prevent thundering herd on recovery + if (enqueued >= TICK_MAX_ENQUEUE) break; + if (agent.status === "paused" || agent.status === "terminated" || agent.status === "pending_approval") continue; const policy = parseHeartbeatPolicy(agent); if (!policy.enabled || policy.intervalSec <= 0) continue; @@ -3423,20 +3443,34 @@ export function heartbeatService(db: Db) { const elapsedMs = now.getTime() - baseline; if (elapsedMs < policy.intervalSec * 1000) continue; - const run = await enqueueWakeup(agent.id, { - source: "timer", - triggerDetail: "system", - reason: "heartbeat_timer", - requestedByActorType: "system", - requestedByActorId: "heartbeat_scheduler", - contextSnapshot: { - source: "scheduler", - reason: "interval_elapsed", - now: now.toISOString(), - }, - }); - if (run) enqueued += 1; - else skipped += 1; + // Per-agent jitter spreads agents across ticks so they don't all fire at once. + // Jitter is up to 25% of the agent's interval, capped at 5 minutes. + const maxJitterMs = Math.min(policy.intervalSec * 250, 5 * 60 * 1000); + const jitterMs = stableJitterMs(agent.id, maxJitterMs); + if (elapsedMs < policy.intervalSec * 1000 + jitterMs) continue; + + // Enforce cooldown: honour the per-agent cooldownSec if configured + if (policy.cooldownSec > 0 && elapsedMs < policy.cooldownSec * 1000) continue; + + try { + const run = await enqueueWakeup(agent.id, { + source: "timer", + triggerDetail: "system", + reason: "heartbeat_timer", + requestedByActorType: "system", + requestedByActorId: "heartbeat_scheduler", + contextSnapshot: { + source: "scheduler", + reason: "interval_elapsed", + now: now.toISOString(), + }, + }); + if (run) enqueued += 1; + else skipped += 1; + } catch { + // Individual agent enqueue failures should not block other agents + skipped += 1; + } } return { checked, enqueued, skipped }; diff --git a/server/src/services/scheduler-utils.ts b/server/src/services/scheduler-utils.ts new file mode 100644 index 0000000000..3b96668c9a --- /dev/null +++ b/server/src/services/scheduler-utils.ts @@ -0,0 +1,18 @@ +/** + * Max agents that can be enqueued in a single scheduler tick. + * Prevents thundering herd on server restart when all agents are due. + */ +export const TICK_MAX_ENQUEUE = 5; + +/** + * Deterministic per-agent jitter based on agent ID hash. + * Spreads agents across the scheduler tick window so they don't all fire at once. + */ +export function stableJitterMs(agentId: string, maxMs: number): number { + if (maxMs <= 0) return 0; + let hash = 0; + for (let i = 0; i < agentId.length; i++) { + hash = ((hash << 5) - hash + agentId.charCodeAt(i)) | 0; + } + return Math.abs(hash) % maxMs; +} From 768d6d622cd6240d313956e4cf59de3aea7a5616 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Thu, 19 Mar 2026 21:13:30 +0700 Subject: [PATCH 003/227] fix(issues): validate checkout ownership on document PUT to prevent 403 flood Add assertAgentRunCheckoutOwnership check to the document PUT endpoint, matching behavior of other issue mutation endpoints (PATCH, release, comments). Include retryable: false in conflict error details so agents stop retrying when checkout has expired or been reassigned. Closes #1256 Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- .../__tests__/document-put-checkout.test.ts | 160 ++++++++++++++++++ server/src/routes/issues.ts | 1 + server/src/services/issues.ts | 3 +- 3 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 server/src/__tests__/document-put-checkout.test.ts diff --git a/server/src/__tests__/document-put-checkout.test.ts b/server/src/__tests__/document-put-checkout.test.ts new file mode 100644 index 0000000000..1daa026c07 --- /dev/null +++ b/server/src/__tests__/document-put-checkout.test.ts @@ -0,0 +1,160 @@ +import express from "express"; +import request from "supertest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { issueRoutes } from "../routes/issues.js"; +import { errorHandler } from "../middleware/index.js"; +import { conflict } from "../errors.js"; + +const mockIssueService = vi.hoisted(() => ({ + getById: vi.fn(), + getByIdentifier: vi.fn(), + assertCheckoutOwner: vi.fn(), +})); + +const mockDocumentService = vi.hoisted(() => ({ + upsertIssueDocument: vi.fn(), + getIssueDocumentByKey: vi.fn(), +})); + +const mockLogActivity = vi.hoisted(() => vi.fn()); + +vi.mock("../services/index.js", () => ({ + issueService: () => mockIssueService, + documentService: () => mockDocumentService, + accessService: () => ({}), + heartbeatService: () => ({ wakeup: vi.fn() }), + agentService: () => ({}), + projectService: () => ({}), + goalService: () => ({}), + issueApprovalService: () => ({}), + executionWorkspaceService: () => ({}), + workProductService: () => ({}), + logActivity: mockLogActivity, +})); + +const ISSUE = { + id: "issue-1", + companyId: "company-1", + status: "in_progress", + assigneeAgentId: "agent-1", + checkoutRunId: "run-old", +}; + +function createApp(actor: any) { + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + (req as any).actor = actor; + next(); + }); + app.use("/api", issueRoutes({} as any, {} as any)); + app.use(errorHandler); + return app; +} + +describe("document PUT checkout ownership", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockIssueService.getById.mockResolvedValue(ISSUE); + mockLogActivity.mockResolvedValue(undefined); + }); + + it("rejects document PUT from agent with mismatched checkout run", async () => { + mockIssueService.assertCheckoutOwner.mockRejectedValue( + conflict("Issue run ownership conflict — checkout expired or reassigned, do not retry", { + issueId: ISSUE.id, + checkoutRunId: "run-old", + actorRunId: "run-stale", + retryable: false, + }), + ); + + const app = createApp({ + type: "agent", + agentId: "agent-1", + companyId: "company-1", + runId: "run-stale", + }); + + const res = await request(app) + .put("/api/issues/issue-1/documents/plan") + .send({ + title: "Plan", + format: "markdown", + body: "# Plan\n\nSome content", + }); + + expect(res.status).toBe(409); + expect(res.body.error).toContain("checkout expired"); + expect(res.body.details.retryable).toBe(false); + expect(mockDocumentService.upsertIssueDocument).not.toHaveBeenCalled(); + }); + + it("allows document PUT from agent with valid checkout run", async () => { + mockIssueService.assertCheckoutOwner.mockResolvedValue({ + ...ISSUE, + adoptedFromRunId: null, + }); + mockDocumentService.upsertIssueDocument.mockResolvedValue({ + created: true, + document: { + id: "doc-1", + key: "plan", + title: "Plan", + format: "markdown", + latestRevisionNumber: 1, + }, + }); + + const app = createApp({ + type: "agent", + agentId: "agent-1", + companyId: "company-1", + runId: "run-old", + }); + + const res = await request(app) + .put("/api/issues/issue-1/documents/plan") + .send({ + title: "Plan", + format: "markdown", + body: "# Plan\n\nSome content", + }); + + expect(res.status).toBe(201); + expect(mockDocumentService.upsertIssueDocument).toHaveBeenCalledTimes(1); + }); + + it("allows document PUT from board users without checkout check", async () => { + mockDocumentService.upsertIssueDocument.mockResolvedValue({ + created: false, + document: { + id: "doc-1", + key: "plan", + title: "Plan", + format: "markdown", + latestRevisionNumber: 2, + }, + }); + + const app = createApp({ + type: "board", + userId: "user-1", + companyIds: ["company-1"], + source: "session", + isInstanceAdmin: false, + }); + + const res = await request(app) + .put("/api/issues/issue-1/documents/plan") + .send({ + title: "Plan", + format: "markdown", + body: "# Plan\n\nUpdated content", + }); + + expect(res.status).toBe(200); + expect(mockIssueService.assertCheckoutOwner).not.toHaveBeenCalled(); + expect(mockDocumentService.upsertIssueDocument).toHaveBeenCalledTimes(1); + }); +}); diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index cf8d170ae1..271cc62fe1 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -460,6 +460,7 @@ export function issueRoutes(db: Db, storage: StorageService) { return; } assertCompanyAccess(req, issue.companyId); + if (!(await assertAgentRunCheckoutOwnership(req, res, issue))) return; const keyParsed = issueDocumentKeySchema.safeParse(String(req.params.key ?? "").trim().toLowerCase()); if (!keyParsed.success) { res.status(400).json({ error: "Invalid document key", details: keyParsed.error.issues }); diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 1f1a49619f..6eee4e7ee4 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -1084,13 +1084,14 @@ export function issueService(db: Db) { } } - throw conflict("Issue run ownership conflict", { + throw conflict("Issue run ownership conflict — checkout expired or reassigned, do not retry", { issueId: current.id, status: current.status, assigneeAgentId: current.assigneeAgentId, checkoutRunId: current.checkoutRunId, actorAgentId, actorRunId, + retryable: false, }); }, From 275e410a79ccd1e89c0078648b7f5e34db718290 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Thu, 19 Mar 2026 21:32:50 +0700 Subject: [PATCH 004/227] fix(db): bootstrap migration journal for system PostgreSQL with existing tables When a database has existing tables but no drizzle migration journal (the "no-migration-journal-non-empty-db" case), applyPendingMigrations previously threw "automatic migration is unsafe". This blocked system PostgreSQL users from starting the server after upgrades. Now, applyPendingMigrations bootstraps the journal by walking each migration in order and checking whether its DDL objects (tables, columns, indexes, constraints) already exist in the schema. Migrations whose objects are present are recorded in the journal without re-running. The first migration with clearly absent objects is treated as genuinely pending and applied normally. Closes #1211 Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- packages/db/src/client.test.ts | 34 ++++++++ packages/db/src/client.ts | 144 ++++++++++++++++++++++++++++++++- 2 files changed, 177 insertions(+), 1 deletion(-) diff --git a/packages/db/src/client.test.ts b/packages/db/src/client.test.ts index ef47970caa..ee78ea4feb 100644 --- a/packages/db/src/client.test.ts +++ b/packages/db/src/client.test.ts @@ -102,6 +102,40 @@ afterEach(async () => { }); describe("applyPendingMigrations", () => { + it( + "bootstraps migration journal for non-empty database without journal", + async () => { + const connectionString = await createTempDatabase(); + + // First, apply all migrations normally to create a fully-migrated database + await applyPendingMigrations(connectionString); + + // Now drop the migration journal to simulate the "non-empty db, no journal" case + const sql = postgres(connectionString, { max: 1, onnotice: () => {} }); + try { + await sql.unsafe(`DROP SCHEMA "drizzle" CASCADE`); + } finally { + await sql.end(); + } + + // Verify we are now in the "no-migration-journal-non-empty-db" state + const stateBeforeFix = await inspectMigrations(connectionString); + expect(stateBeforeFix).toMatchObject({ + status: "needsMigrations", + reason: "no-migration-journal-non-empty-db", + }); + + // The fix: applyPendingMigrations should bootstrap the journal + // instead of throwing "automatic migration is unsafe" + await applyPendingMigrations(connectionString); + + // Verify the database is now up to date with a proper journal + const finalState = await inspectMigrations(connectionString); + expect(finalState.status).toBe("upToDate"); + }, + 20_000, + ); + it( "applies an inserted earlier migration without replaying later legacy migrations", async () => { diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts index b8cadbe30c..536f541db6 100644 --- a/packages/db/src/client.ts +++ b/packages/db/src/client.ts @@ -373,6 +373,75 @@ async function constraintExists( return rows[0]?.exists ?? false; } +function extractFirstCreateTable(migrationContent: string): string | null { + const match = migrationContent.match(/CREATE TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+"([^"]+)"/i); + return match ? match[1] : null; +} + +/** + * Check if any recognisable DDL object from a migration exists in the database. + * Prioritises positive indicators (CREATE/ADD/ALTER) over negative ones (DROP), + * because a migration often DROP-then-CREATEs the same object. + * Returns true when a target object is found, false when clearly absent, + * null when no statement could be classified. + */ +async function migrationLikelyApplied( + sql: ReturnType, + migrationContent: string, +): Promise { + const statements = splitMigrationStatements(migrationContent); + + // First pass: look for positive indicators (CREATE, ADD, ALTER COLUMN, RENAME) + for (const statement of statements) { + const normalized = statement.replace(/\s+/g, " ").trim(); + + const createTableMatch = normalized.match(/^CREATE TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+"([^"]+)"/i); + if (createTableMatch) return tableExists(sql, createTableMatch[1]); + + const addColumnMatch = normalized.match( + /^ALTER TABLE\s+"([^"]+)"\s+ADD COLUMN(?:\s+IF\s+NOT\s+EXISTS)?\s+"([^"]+)"/i, + ); + if (addColumnMatch) return columnExists(sql, addColumnMatch[1], addColumnMatch[2]); + + const alterColumnMatch = normalized.match( + /^ALTER TABLE\s+"([^"]+)"\s+ALTER COLUMN\s+"([^"]+)"/i, + ); + if (alterColumnMatch) return columnExists(sql, alterColumnMatch[1], alterColumnMatch[2]); + + const createIndexMatch = normalized.match(/^CREATE\s+(?:UNIQUE\s+)?INDEX(?:\s+IF\s+NOT\s+EXISTS)?\s+"([^"]+)"/i); + if (createIndexMatch) return indexExists(sql, createIndexMatch[1]); + + const addConstraintMatch = normalized.match(/^ALTER TABLE\s+"([^"]+)"\s+ADD CONSTRAINT\s+"([^"]+)"/i); + if (addConstraintMatch) return constraintExists(sql, addConstraintMatch[2]); + + const renameColumnMatch = normalized.match( + /^ALTER TABLE\s+"([^"]+)"\s+RENAME COLUMN\s+"[^"]+"\s+TO\s+"([^"]+)"/i, + ); + if (renameColumnMatch) return columnExists(sql, renameColumnMatch[1], renameColumnMatch[2]); + } + + // Second pass: negative indicators only (DROP operations) + for (const statement of statements) { + const normalized = statement.replace(/\s+/g, " ").trim(); + + const dropColumnMatch = normalized.match( + /^ALTER TABLE\s+"([^"]+)"\s+DROP COLUMN(?:\s+IF\s+EXISTS)?\s+"([^"]+)"/i, + ); + if (dropColumnMatch) { + const exists = await columnExists(sql, dropColumnMatch[1], dropColumnMatch[2]); + return !exists; + } + + const dropIndexMatch = normalized.match(/^DROP INDEX(?:\s+IF\s+EXISTS)?\s+"([^"]+)"/i); + if (dropIndexMatch) { + const exists = await indexExists(sql, dropIndexMatch[1]); + return !exists; + } + } + + return null; +} + async function migrationStatementAlreadyApplied( sql: ReturnType, statement: string, @@ -678,8 +747,81 @@ export async function applyPendingMigrations(url: string): Promise { } if (initialState.reason === "no-migration-journal-non-empty-db") { + // Bootstrap migration journal for a database with existing tables but + // no drizzle migration journal. Walk each migration in order: if its + // content matches what is already in the schema, record it in the + // journal without re-running it. When the heuristic is inconclusive + // (migration contains DDL patterns the checker does not recognise), + // check whether the first CREATE TABLE in the migration already exists + // — if so, treat the whole migration as previously applied. The first + // migration whose objects are clearly absent is treated as genuinely + // pending and handed off to the normal migration path. + const orderedMigrations = await orderMigrationsByJournal(initialState.pendingMigrations); + const journalEntries = await listJournalMigrationEntries(); + const folderMillisByFileName = new Map( + journalEntries.map((entry) => [entry.fileName, normalizeFolderMillis(entry.folderMillis)]), + ); + + const sql = createUtilitySql(url); + try { + const { migrationTableSchema, columnNames } = await ensureMigrationJournalTable(sql); + const qualifiedTable = `${quoteIdentifier(migrationTableSchema)}.${quoteIdentifier(DRIZZLE_MIGRATIONS_TABLE)}`; + + for (const migrationFile of orderedMigrations) { + const migrationContent = await readMigrationFileContent(migrationFile); + const hash = createHash("sha256").update(migrationContent).digest("hex"); + + // Fast path: full content check passes + const fullyRecognised = await migrationContentAlreadyApplied(sql, migrationContent); + if (fullyRecognised) { + await recordMigrationHistoryEntry( + sql, qualifiedTable, columnNames, migrationFile, hash, + folderMillisByFileName.get(migrationFile) ?? Date.now(), + ); + continue; + } + + // Fallback: scan statements for the first recognisable DDL object + // (table, column, index, constraint) and check if it exists in the DB. + const likelyApplied = await migrationLikelyApplied(sql, migrationContent); + if (likelyApplied === true) { + await recordMigrationHistoryEntry( + sql, qualifiedTable, columnNames, migrationFile, hash, + folderMillisByFileName.get(migrationFile) ?? Date.now(), + ); + continue; + } + if (likelyApplied === null) { + // No recognisable DDL found. Since the database already has tables + // and all previous migrations were recorded, assume this data-only + // or unrecognised migration was also applied. + await recordMigrationHistoryEntry( + sql, qualifiedTable, columnNames, migrationFile, hash, + folderMillisByFileName.get(migrationFile) ?? Date.now(), + ); + continue; + } + + // Object is clearly absent — this migration is genuinely pending. + // Stop recording and delegate to the normal pending-migrations path. + break; + } + } finally { + await sql.end(); + } + + // Re-inspect after journal bootstrap. If genuinely pending migrations + // remain, apply them through the standard path. + const postBootstrapState = await inspectMigrations(url); + if (postBootstrapState.status === "upToDate") return; + if (postBootstrapState.status === "needsMigrations" && postBootstrapState.reason === "pending-migrations") { + await applyPendingMigrationsManually(url, postBootstrapState.pendingMigrations); + const finalState = await inspectMigrations(url); + if (finalState.status === "upToDate") return; + throw new Error(`Failed to apply remaining migrations: ${finalState.pendingMigrations.join(", ")}`); + } throw new Error( - "Database has tables but no migration journal; automatic migration is unsafe. Initialize migration history manually.", + `Failed to bootstrap migration journal: unexpected state after bootstrap (${postBootstrapState.reason})`, ); } From f74f8c61335e741505fa13ccd389abc4ac177e9a Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 00:24:46 +0700 Subject: [PATCH 005/227] fix(review): address PR #1282 greptile review feedback - Move & decoding before numeric entity regexes to handle double-encoded entities - Change TICK_MAX_ENQUEUE from break to continue to prevent agent starvation - Apply jitter only during recovery bursts (elapsed > 150% interval), not steady state - Add 2 test cases for double-encoded HTML entities Co-Authored-By: Paperclip --- server/src/__tests__/mention-parsing.test.ts | 11 +++++++++++ server/src/services/heartbeat.ts | 12 ++++++++---- server/src/services/issues.ts | 2 +- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/server/src/__tests__/mention-parsing.test.ts b/server/src/__tests__/mention-parsing.test.ts index 1b84572e01..0d9941c664 100644 --- a/server/src/__tests__/mention-parsing.test.ts +++ b/server/src/__tests__/mention-parsing.test.ts @@ -97,4 +97,15 @@ describe("matchMentionedNames", () => { ); expect(result).toEqual(new Set(["r&d bot"])); }); + + // Double-encoded entity tests + it("decodes double-encoded &#64; (@ as double-encoded numeric entity)", () => { + const result = matchMentionedNames("Hello &#64;CEO please review", agents); + expect(result).toEqual(new Set(["ceo"])); + }); + + it("decodes double-encoded &#x40; (@ as double-encoded hex entity)", () => { + const result = matchMentionedNames("Hello &#x40;CEO please review", agents); + expect(result).toEqual(new Set(["ceo"])); + }); }); diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 701735d4b2..ef59ecd5c5 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -3432,7 +3432,8 @@ export function heartbeatService(db: Db) { for (const agent of allAgents) { // Rate-limit: cap enqueues per tick to prevent thundering herd on recovery - if (enqueued >= TICK_MAX_ENQUEUE) break; + // Use continue (not break) so all agents are still evaluated fairly + if (enqueued >= TICK_MAX_ENQUEUE) continue; if (agent.status === "paused" || agent.status === "terminated" || agent.status === "pending_approval") continue; const policy = parseHeartbeatPolicy(agent); @@ -3443,9 +3444,12 @@ export function heartbeatService(db: Db) { const elapsedMs = now.getTime() - baseline; if (elapsedMs < policy.intervalSec * 1000) continue; - // Per-agent jitter spreads agents across ticks so they don't all fire at once. - // Jitter is up to 25% of the agent's interval, capped at 5 minutes. - const maxJitterMs = Math.min(policy.intervalSec * 250, 5 * 60 * 1000); + // Per-agent jitter only during recovery bursts (elapsed >> interval), + // so steady-state scheduling fires exactly at intervalSec. + const isRecoveryBurst = elapsedMs > policy.intervalSec * 1500; + const maxJitterMs = isRecoveryBurst + ? Math.min(policy.intervalSec * 250, 5 * 60 * 1000) + : 0; const jitterMs = stableJitterMs(agent.id, maxJitterMs); if (elapsedMs < policy.intervalSec * 1000 + jitterMs) continue; diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index ff64d021f7..ea2635258a 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -39,9 +39,9 @@ const MAX_ISSUE_COMMENT_PAGE_LIMIT = 500; */ function decodeHtmlEntities(text: string): string { return text + .replace(/&/g, "&") .replace(/&#(\d+);/g, (_, code: string) => String.fromCharCode(parseInt(code, 10))) .replace(/&#x([0-9a-fA-F]+);/g, (_, code: string) => String.fromCharCode(parseInt(code, 16))) - .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, '"') From e48ca5e5fb1fb6302ab651f321fff4bfb1099cb1 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 00:26:29 +0700 Subject: [PATCH 006/227] test(review): add stale-checkout adoption test for PR #1283 Add test covering the checkout adoption path where logActivity is called with action "issue.checkout_lock_adopted" when a run takes over a stale checkout lock. Co-Authored-By: Paperclip --- .../__tests__/document-put-checkout.test.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/server/src/__tests__/document-put-checkout.test.ts b/server/src/__tests__/document-put-checkout.test.ts index 1daa026c07..0e1eb568ca 100644 --- a/server/src/__tests__/document-put-checkout.test.ts +++ b/server/src/__tests__/document-put-checkout.test.ts @@ -125,6 +125,46 @@ describe("document PUT checkout ownership", () => { expect(mockDocumentService.upsertIssueDocument).toHaveBeenCalledTimes(1); }); + it("logs checkout adoption when agent takes over a stale run", async () => { + mockIssueService.assertCheckoutOwner.mockResolvedValue({ + ...ISSUE, + checkoutRunId: "run-new", + adoptedFromRunId: "run-old", + }); + mockDocumentService.upsertIssueDocument.mockResolvedValue({ + created: true, + document: { + id: "doc-1", + key: "plan", + title: "Plan", + format: "markdown", + latestRevisionNumber: 1, + }, + }); + + const app = createApp({ + type: "agent", + agentId: "agent-1", + companyId: "company-1", + runId: "run-new", + }); + + const res = await request(app) + .put("/api/issues/issue-1/documents/plan") + .send({ + title: "Plan", + format: "markdown", + body: "# Plan\n\nContent", + }); + + expect(res.status).toBe(201); + expect(mockLogActivity).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ action: "issue.checkout_lock_adopted" }), + ); + expect(mockDocumentService.upsertIssueDocument).toHaveBeenCalledTimes(1); + }); + it("allows document PUT from board users without checkout check", async () => { mockDocumentService.upsertIssueDocument.mockResolvedValue({ created: false, From d1c2aadbd73cbea78a4b9f834c5b15a10d5a6b11 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 00:32:34 +0700 Subject: [PATCH 007/227] fix(onboarding): register gemini_local adapter, null-safe markdown editor, preserve config on re-onboard - Add "gemini_local" to AGENT_ADAPTER_TYPES constant (fixes #1195) - Add null guard for MarkdownEditor value prop to prevent setMarkdown crash (fixes #1227) - Merge existing config.json values during onboard --yes instead of overwriting, preserving user customizations like server.host and deploymentMode (fixes #1196) Co-Authored-By: Paperclip --- cli/src/commands/onboard.ts | 24 ++++++++++++++++-------- packages/shared/src/constants.ts | 1 + ui/src/components/AgentConfigForm.tsx | 2 +- ui/src/components/MarkdownEditor.tsx | 2 +- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/cli/src/commands/onboard.ts b/cli/src/commands/onboard.ts index 523484f3c6..a649eb8816 100644 --- a/cli/src/commands/onboard.ts +++ b/cli/src/commands/onboard.ts @@ -244,11 +244,12 @@ export async function onboard(opts: OnboardOptions): Promise { ), ); + let existingConfig: PaperclipConfig | null = null; if (configExists(opts.config)) { p.log.message(pc.dim(`${configPath} exists, updating config`)); try { - readConfig(opts.config); + existingConfig = readConfig(opts.config); } catch (err) { p.log.message( pc.yellow( @@ -406,20 +407,27 @@ export async function onboard(opts: OnboardOptions): Promise { p.log.info(`Using existing ${pc.cyan("PAPERCLIP_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`); } - const config: PaperclipConfig = { + // When --yes and existing config present, merge: preserve user overrides, + // only fill in missing fields from quickstart defaults. + const mergedConfig: PaperclipConfig = { $meta: { version: 1, updatedAt: new Date().toISOString(), source: "onboard", }, ...(llm && { llm }), - database, - logging, - server, - auth, - storage, - secrets, + database: existingConfig?.database ? { ...database, ...existingConfig.database } : database, + logging: existingConfig?.logging ? { ...logging, ...existingConfig.logging } : logging, + server: existingConfig?.server ? { ...server, ...existingConfig.server } : server, + auth: existingConfig?.auth ? { ...auth, ...existingConfig.auth } : auth, + storage: existingConfig?.storage ? { ...storage, ...existingConfig.storage } : storage, + secrets: existingConfig?.secrets ? { ...secrets, ...existingConfig.secrets } : secrets, }; + // Preserve existing LLM config if not set by env/prompts + if (!llm && existingConfig?.llm) { + mergedConfig.llm = existingConfig.llm; + } + const config = mergedConfig; const keyResult = ensureLocalSecretsKeyFile(config, configPath); if (keyResult.status === "created") { diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 8afa80eac5..d6b1460e65 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -26,6 +26,7 @@ export const AGENT_ADAPTER_TYPES = [ "http", "claude_local", "codex_local", + "gemini_local", "opencode_local", "pi_local", "cursor", diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 69c3191922..c5faf36505 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -603,7 +603,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { <> set!({ promptTemplate: v })} placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..." contentClassName="min-h-[88px] text-sm font-mono" diff --git a/ui/src/components/MarkdownEditor.tsx b/ui/src/components/MarkdownEditor.tsx index 29a57a3a0e..874888177b 100644 --- a/ui/src/components/MarkdownEditor.tsx +++ b/ui/src/components/MarkdownEditor.tsx @@ -304,7 +304,7 @@ export const MarkdownEditor = forwardRef useEffect(() => { if (value !== latestValueRef.current) { - ref.current?.setMarkdown(value); + ref.current?.setMarkdown(value ?? ""); latestValueRef.current = value; } }, [value]); From ac73bee7d4df87e8f7437dadaa6d648de20ef9d6 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 01:30:36 +0700 Subject: [PATCH 008/227] fix(docker): add shared and plugin-sdk build steps to Dockerfile The server imports HostServices from @paperclipai/plugin-sdk, which needs its type declarations compiled before the server build. Also adds @paperclipai/shared as a prerequisite. Without these steps, `docker build` fails on master with TS2353. Fixes #1264 Co-Authored-By: Paperclip --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 014113e432..9589dc05aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,7 @@ COPY packages/adapters/gemini-local/package.json packages/adapters/gemini-local/ COPY packages/adapters/openclaw-gateway/package.json packages/adapters/openclaw-gateway/ COPY packages/adapters/opencode-local/package.json packages/adapters/opencode-local/ COPY packages/adapters/pi-local/package.json packages/adapters/pi-local/ +COPY packages/plugins/sdk/package.json packages/plugins/sdk/ RUN pnpm install --frozen-lockfile @@ -27,6 +28,8 @@ FROM base AS build WORKDIR /app COPY --from=deps /app /app COPY . . +RUN pnpm --filter @paperclipai/shared build +RUN pnpm --filter @paperclipai/plugin-sdk build RUN pnpm --filter @paperclipai/ui build RUN pnpm --filter @paperclipai/server build RUN test -f server/dist/index.js || (echo "ERROR: server build output missing" && exit 1) From 6749e545ad1000109ad38cdc06c0e1723bcb63a0 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 01:41:28 +0700 Subject: [PATCH 009/227] fix(codex): handle ENOENT when codex CLI is not installed Move spawn() from class field initializer into constructor so the error event handler is registered before the spawn resolves. Without this, spawning codex when the CLI is absent crashes the entire server with an unhandled ENOENT error. Fixes #1268 Co-Authored-By: Paperclip --- .../adapters/codex-local/src/server/quota.ts | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/adapters/codex-local/src/server/quota.ts b/packages/adapters/codex-local/src/server/quota.ts index b51f6646f2..58f2041b29 100644 --- a/packages/adapters/codex-local/src/server/quota.ts +++ b/packages/adapters/codex-local/src/server/quota.ts @@ -407,11 +407,7 @@ type PendingRequest = { }; class CodexRpcClient { - private proc = spawn( - "codex", - ["-s", "read-only", "-a", "untrusted", "app-server"], - { stdio: ["pipe", "pipe", "pipe"], env: process.env }, - ); + private proc: ReturnType; private nextId = 1; private buffer = ""; @@ -419,10 +415,22 @@ class CodexRpcClient { private stderr = ""; constructor() { - this.proc.stdout.setEncoding("utf8"); - this.proc.stderr.setEncoding("utf8"); - this.proc.stdout.on("data", (chunk: string) => this.onStdout(chunk)); - this.proc.stderr.on("data", (chunk: string) => { + this.proc = spawn( + "codex", + ["-s", "read-only", "-a", "untrusted", "app-server"], + { stdio: ["pipe", "pipe", "pipe"], env: process.env }, + ); + this.proc.on("error", (err: Error) => { + for (const request of this.pending.values()) { + clearTimeout(request.timer); + request.reject(err); + } + this.pending.clear(); + }); + this.proc.stdout!.setEncoding("utf8"); + this.proc.stderr!.setEncoding("utf8"); + this.proc.stdout!.on("data", (chunk: string) => this.onStdout(chunk)); + this.proc.stderr!.on("data", (chunk: string) => { this.stderr += chunk; }); this.proc.on("exit", () => { @@ -467,12 +475,12 @@ class CodexRpcClient { reject(new Error(`codex app-server timed out on ${method}`)); }, timeoutMs); this.pending.set(id, { resolve, reject, timer }); - this.proc.stdin.write(payload); + this.proc.stdin!.write(payload); }); } private notify(method: string, params: Record = {}) { - this.proc.stdin.write(JSON.stringify({ method, params }) + "\n"); + this.proc.stdin!.write(JSON.stringify({ method, params }) + "\n"); } async initialize() { From a83f3735c9e18f07a4029a4febf634f47d203c8c Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 01:41:35 +0700 Subject: [PATCH 010/227] fix(issues): clear execution lock when reassigning via PATCH When an issue is reassigned to a different agent via PATCH, only checkoutRunId was cleared while executionRunId remained pointing to the old agent's run. This caused the new assignee's wakeup to be deferred against a stale execution lock that was never promoted. Now clearing executionRunId, executionAgentNameKey and executionLockedAt alongside checkoutRunId on reassignment, so the new agent receives an immediate (non-deferred) wakeup. Fixes #1269 Co-Authored-By: Paperclip --- server/src/services/issues.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 1f1a49619f..4bb88c7984 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -852,6 +852,9 @@ export function issueService(db: Db) { (issueData.assigneeUserId !== undefined && issueData.assigneeUserId !== existing.assigneeUserId) ) { patch.checkoutRunId = null; + patch.executionRunId = null; + patch.executionAgentNameKey = null; + patch.executionLockedAt = null; } return db.transaction(async (tx) => { From b5cf92dd94df42b99296b7bbf38dbd4cbc6626e9 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 02:34:32 +0700 Subject: [PATCH 011/227] fix(ui): replace deprecated execCommand with DOM Range API for @mention insertion Chrome/Chromium and Electron silently ignore document.execCommand("insertText"), causing @mention selection to insert nothing. Switch to Range.deleteContents() + Range.insertNode() with a synthetic InputEvent so Lexical picks up the change. Adds defensive markdown state sync in case the InputEvent does not propagate. Fixes #1294 Co-Authored-By: Paperclip --- ui/src/components/MarkdownEditor.tsx | 90 +++++++++++++++++----------- 1 file changed, 55 insertions(+), 35 deletions(-) diff --git a/ui/src/components/MarkdownEditor.tsx b/ui/src/components/MarkdownEditor.tsx index 29a57a3a0e..aa47e97599 100644 --- a/ui/src/components/MarkdownEditor.tsx +++ b/ui/src/components/MarkdownEditor.tsx @@ -415,57 +415,77 @@ export const MarkdownEditor = forwardRef const replacement = mentionMarkdown(option); - // Replace @query directly via DOM selection so the cursor naturally - // lands after the inserted text. Lexical picks up the change through - // its normal input-event handling. + // Replace @query directly via DOM manipulation so the cursor naturally + // lands after the inserted text. We avoid document.execCommand("insertText") + // because it is deprecated and silently fails in Chrome/Chromium and Electron. + // Instead we delete the range, insert a text node, and dispatch an InputEvent + // so Lexical picks up the change through its normal input-event handling. const sel = window.getSelection(); if (sel && state.textNode.isConnected) { const range = document.createRange(); range.setStart(state.textNode, state.atPos); range.setEnd(state.textNode, state.endPos); + range.deleteContents(); + const textNode = document.createTextNode(replacement); + range.insertNode(textNode); + + // Place cursor immediately after the inserted text + const cursorRange = document.createRange(); + cursorRange.setStartAfter(textNode); + cursorRange.collapse(true); sel.removeAllRanges(); - sel.addRange(range); - document.execCommand("insertText", false, replacement); + sel.addRange(cursorRange); - // After Lexical reconciles the DOM, the cursor position set by - // execCommand may be lost. Explicitly reposition it after the - // inserted mention text. - const cursorTarget = state.atPos + replacement.length; + // Notify Lexical of the DOM mutation via an InputEvent + const editable = containerRef.current?.querySelector('[contenteditable="true"]'); + if (editable) { + editable.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: replacement })); + } + + // After Lexical reconciles the DOM, the cursor position may shift. + // Explicitly reposition it and sync the markdown state. requestAnimationFrame(() => { const newSel = window.getSelection(); - if (!newSel) return; - // Try the original text node first (it may still be valid) - if (state.textNode.isConnected) { - const len = state.textNode.textContent?.length ?? 0; - if (cursorTarget <= len) { + if (newSel) { + // Try the original inserted text node first + if (textNode.isConnected) { const r = document.createRange(); - r.setStart(state.textNode, cursorTarget); + r.setStartAfter(textNode); r.collapse(true); newSel.removeAllRanges(); newSel.addRange(r); - return; - } - } - // Fallback: search for the replacement in text nodes - const editable = containerRef.current?.querySelector('[contenteditable="true"]'); - if (!editable) return; - const walker = document.createTreeWalker(editable, NodeFilter.SHOW_TEXT); - let node: Text | null; - while ((node = walker.nextNode() as Text | null)) { - const text = node.textContent ?? ""; - const idx = text.indexOf(replacement); - if (idx !== -1) { - const pos = idx + replacement.length; - if (pos <= text.length) { - const r = document.createRange(); - r.setStart(node, pos); - r.collapse(true); - newSel.removeAllRanges(); - newSel.addRange(r); - return; + } else { + // Fallback: search for the replacement in text nodes + const editableEl = containerRef.current?.querySelector('[contenteditable="true"]'); + if (editableEl) { + const walker = document.createTreeWalker(editableEl, NodeFilter.SHOW_TEXT); + let node: Text | null; + while ((node = walker.nextNode() as Text | null)) { + const text = node.textContent ?? ""; + const idx = text.indexOf(replacement); + if (idx !== -1) { + const pos = idx + replacement.length; + if (pos <= text.length) { + const r = document.createRange(); + r.setStart(node, pos); + r.collapse(true); + newSel.removeAllRanges(); + newSel.addRange(r); + break; + } + } + } } } } + // Defensive sync: if the InputEvent did not propagate through + // Lexical/MDXEditor (e.g. synthetic events ignored), read the + // current markdown from the editor and call onChange ourselves. + const current = ref.current?.getMarkdown?.(); + if (current != null && current !== latestValueRef.current) { + latestValueRef.current = current; + onChange(current); + } }); } else { // Fallback: full markdown replacement when DOM node is stale From 30fad7b904af2e79a56aa8dd37a71055d0834777 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 03:42:09 +0700 Subject: [PATCH 012/227] fix(heartbeat): wake parent task assignee when subtask reaches terminal status When a subtask status changes to done or cancelled, the parent task's assignee agent is now woken immediately via the heartbeat system. This enables manager agents to advance multi-step workflows without waiting for the next timer tick. Fixes #1280 Co-Authored-By: Paperclip --- .../__tests__/subtask-completion-wake.test.ts | 341 ++++++++++++++++++ server/src/routes/issues.ts | 31 ++ 2 files changed, 372 insertions(+) create mode 100644 server/src/__tests__/subtask-completion-wake.test.ts diff --git a/server/src/__tests__/subtask-completion-wake.test.ts b/server/src/__tests__/subtask-completion-wake.test.ts new file mode 100644 index 0000000000..7a39034524 --- /dev/null +++ b/server/src/__tests__/subtask-completion-wake.test.ts @@ -0,0 +1,341 @@ +import express from "express"; +import request from "supertest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { issueRoutes } from "../routes/issues.js"; +import { errorHandler } from "../middleware/index.js"; + +// --- mocks --- + +const PARENT_ISSUE = { + id: "parent-1", + companyId: "company-1", + parentId: null, + status: "in_progress", + assigneeAgentId: "manager-agent", + assigneeUserId: null, + identifier: "QUA-100", + title: "Parent task", + checkoutRunId: null, + executionRunId: null, +}; + +const CHILD_ISSUE_IN_PROGRESS = { + id: "child-1", + companyId: "company-1", + parentId: "parent-1", + status: "in_progress", + assigneeAgentId: "worker-agent", + assigneeUserId: null, + identifier: "QUA-101", + title: "Child subtask", + checkoutRunId: "run-worker", + executionRunId: "run-worker", +}; + +const mockIssueService = vi.hoisted(() => ({ + getById: vi.fn(), + getByIdentifier: vi.fn(), + update: vi.fn(), + addComment: vi.fn(), + findMentionedAgents: vi.fn(), + list: vi.fn(), + create: vi.fn(), + checkout: vi.fn(), + release: vi.fn(), + delete: vi.fn(), + assertCheckoutOwner: vi.fn(), + getAncestors: vi.fn(), +})); + +const mockHeartbeatService = vi.hoisted(() => ({ + wakeup: vi.fn(), +})); + +const mockAccessService = vi.hoisted(() => ({ + assertPermission: vi.fn(), +})); + +const mockAgentService = vi.hoisted(() => ({ + getById: vi.fn(), +})); + +const mockProjectService = vi.hoisted(() => ({ + getById: vi.fn(), +})); + +const mockGoalService = vi.hoisted(() => ({ + getById: vi.fn(), +})); + +const mockIssueApprovalService = vi.hoisted(() => ({ + listIssuesForApproval: vi.fn(), + linkManyForApproval: vi.fn(), +})); + +const mockExecutionWorkspaceService = vi.hoisted(() => ({ + getById: vi.fn(), +})); + +const mockWorkProductService = vi.hoisted(() => ({ + list: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), +})); + +const mockDocumentService = vi.hoisted(() => ({ + listByIssue: vi.fn(), + getByKey: vi.fn(), + upsertIssueDocument: vi.fn(), + getRevisions: vi.fn(), +})); + +const mockLogActivity = vi.hoisted(() => vi.fn()); + +vi.mock("../services/index.js", () => ({ + issueService: () => mockIssueService, + heartbeatService: () => mockHeartbeatService, + accessService: () => mockAccessService, + agentService: () => mockAgentService, + projectService: () => mockProjectService, + goalService: () => mockGoalService, + issueApprovalService: () => mockIssueApprovalService, + executionWorkspaceService: () => mockExecutionWorkspaceService, + workProductService: () => mockWorkProductService, + documentService: () => mockDocumentService, + logActivity: mockLogActivity, +})); + +function createApp(actor: { type: string; agentId?: string; runId?: string }) { + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + if (actor.type === "agent") { + (req as any).actor = { + type: "agent", + agentId: actor.agentId ?? null, + companyId: "company-1", + keyId: undefined, + runId: actor.runId ?? null, + source: "agent_jwt", + }; + } else { + (req as any).actor = { + type: "board", + userId: "user-1", + companyIds: ["company-1"], + source: "local_implicit", + isInstanceAdmin: true, + }; + } + next(); + }); + app.use("/api", issueRoutes({} as any, {} as any)); + app.use(errorHandler); + return app; +} + +describe("subtask completion wakes parent assignee", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockHeartbeatService.wakeup.mockResolvedValue({ id: "wake-1" }); + mockLogActivity.mockResolvedValue(undefined); + mockIssueService.findMentionedAgents.mockResolvedValue([]); + }); + + it("wakes parent assignee when subtask status changes to done", async () => { + // existing child issue is in_progress + mockIssueService.getById.mockImplementation(async (id: string) => { + if (id === "child-1") return CHILD_ISSUE_IN_PROGRESS; + if (id === "parent-1") return PARENT_ISSUE; + return null; + }); + // after update, child is done + mockIssueService.update.mockResolvedValue({ + ...CHILD_ISSUE_IN_PROGRESS, + status: "done", + }); + // checkout ownership check passes for the agent + mockIssueService.assertCheckoutOwner.mockResolvedValue(CHILD_ISSUE_IN_PROGRESS); + + const app = createApp({ + type: "agent", + agentId: "worker-agent", + runId: "run-worker", + }); + + const res = await request(app) + .patch("/api/issues/child-1") + .send({ status: "done" }); + + expect(res.status).toBe(200); + + // Give the async wakeup closure time to execute + await vi.waitFor(() => { + expect(mockHeartbeatService.wakeup).toHaveBeenCalledWith( + "manager-agent", + expect.objectContaining({ + reason: "subtask_completed", + payload: expect.objectContaining({ + issueId: "parent-1", + subtaskId: "child-1", + }), + }), + ); + }); + }); + + it("wakes parent assignee when subtask status changes to cancelled", async () => { + mockIssueService.getById.mockImplementation(async (id: string) => { + if (id === "child-1") return CHILD_ISSUE_IN_PROGRESS; + if (id === "parent-1") return PARENT_ISSUE; + return null; + }); + mockIssueService.update.mockResolvedValue({ + ...CHILD_ISSUE_IN_PROGRESS, + status: "cancelled", + }); + mockIssueService.assertCheckoutOwner.mockResolvedValue(CHILD_ISSUE_IN_PROGRESS); + + const app = createApp({ + type: "agent", + agentId: "worker-agent", + runId: "run-worker", + }); + + const res = await request(app) + .patch("/api/issues/child-1") + .send({ status: "cancelled" }); + + expect(res.status).toBe(200); + + await vi.waitFor(() => { + expect(mockHeartbeatService.wakeup).toHaveBeenCalledWith( + "manager-agent", + expect.objectContaining({ + reason: "subtask_completed", + payload: expect.objectContaining({ + issueId: "parent-1", + subtaskId: "child-1", + }), + }), + ); + }); + }); + + it("does not wake parent when subtask moves to a non-terminal status", async () => { + mockIssueService.getById.mockImplementation(async (id: string) => { + if (id === "child-1") return { ...CHILD_ISSUE_IN_PROGRESS, status: "todo" }; + if (id === "parent-1") return PARENT_ISSUE; + return null; + }); + mockIssueService.update.mockResolvedValue({ + ...CHILD_ISSUE_IN_PROGRESS, + status: "in_progress", + }); + mockIssueService.assertCheckoutOwner.mockResolvedValue({ + ...CHILD_ISSUE_IN_PROGRESS, + status: "todo", + }); + + const app = createApp({ + type: "agent", + agentId: "worker-agent", + runId: "run-worker", + }); + + const res = await request(app) + .patch("/api/issues/child-1") + .send({ status: "in_progress" }); + + expect(res.status).toBe(200); + + // Small delay to ensure async wakeup would have fired + await new Promise((r) => setTimeout(r, 50)); + + // Parent should NOT be woken + const wakeupCalls = mockHeartbeatService.wakeup.mock.calls; + const parentWake = wakeupCalls.find( + ([agentId]: [string]) => agentId === "manager-agent", + ); + expect(parentWake).toBeUndefined(); + }); + + it("does not wake parent when issue has no parentId", async () => { + const rootIssue = { ...CHILD_ISSUE_IN_PROGRESS, parentId: null, id: "root-1" }; + mockIssueService.getById.mockImplementation(async (id: string) => { + if (id === "root-1") return rootIssue; + return null; + }); + mockIssueService.update.mockResolvedValue({ + ...rootIssue, + status: "done", + }); + mockIssueService.assertCheckoutOwner.mockResolvedValue(rootIssue); + + const app = createApp({ + type: "agent", + agentId: "worker-agent", + runId: "run-worker", + }); + + const res = await request(app) + .patch("/api/issues/root-1") + .send({ status: "done" }); + + expect(res.status).toBe(200); + + await new Promise((r) => setTimeout(r, 50)); + + expect(mockHeartbeatService.wakeup).not.toHaveBeenCalled(); + }); + + it("does not double-wake when parent assignee is already in the wakeup map", async () => { + // Parent assignee is "manager-agent", and there's a comment that @-mentions manager-agent + // The dedup logic (wakeups.has) should prevent a second wakeup entry. + mockIssueService.getById.mockImplementation(async (id: string) => { + if (id === "child-1") return CHILD_ISSUE_IN_PROGRESS; + if (id === "parent-1") return PARENT_ISSUE; + return null; + }); + mockIssueService.update.mockResolvedValue({ + ...CHILD_ISSUE_IN_PROGRESS, + status: "done", + }); + mockIssueService.assertCheckoutOwner.mockResolvedValue(CHILD_ISSUE_IN_PROGRESS); + // Simulate @-mention of manager-agent in comment + mockIssueService.findMentionedAgents.mockResolvedValue(["manager-agent"]); + mockIssueService.addComment.mockResolvedValue({ + id: "comment-1", + body: "Done! @manager-agent", + issueId: "child-1", + authorAgentId: "worker-agent", + authorUserId: null, + companyId: "company-1", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + + const app = createApp({ + type: "agent", + agentId: "worker-agent", + runId: "run-worker", + }); + + const res = await request(app) + .patch("/api/issues/child-1") + .send({ status: "done", comment: "Done! @manager-agent" }); + + expect(res.status).toBe(200); + + await vi.waitFor(() => { + expect(mockHeartbeatService.wakeup).toHaveBeenCalled(); + }); + + // manager-agent should be woken exactly once (mention OR subtask_completed, not both) + const managerWakes = mockHeartbeatService.wakeup.mock.calls.filter( + ([agentId]: [string]) => agentId === "manager-agent", + ); + expect(managerWakes.length).toBe(1); + }); +}); diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index cf8d170ae1..095251679e 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -944,6 +944,37 @@ export function issueRoutes(db: Db, storage: StorageService) { }); } + // Wake parent task's assignee when a subtask reaches a terminal status (done/cancelled). + // This enables manager agents to advance multi-step workflows without waiting for the next timer tick. + const statusBecameTerminal = + req.body.status !== undefined && + (issue.status === "done" || issue.status === "cancelled") && + existing.status !== "done" && + existing.status !== "cancelled"; + + if (statusBecameTerminal && issue.parentId) { + try { + const parent = await svc.getById(issue.parentId); + if (parent?.assigneeAgentId && !wakeups.has(parent.assigneeAgentId)) { + wakeups.set(parent.assigneeAgentId, { + source: "automation", + triggerDetail: "system", + reason: "subtask_completed", + payload: { issueId: parent.id, subtaskId: issue.id, mutation: "update" }, + requestedByActorType: actor.actorType, + requestedByActorId: actor.actorId, + contextSnapshot: { + issueId: parent.id, + taskId: parent.id, + source: "subtask.completion", + }, + }); + } + } catch (err) { + logger.warn({ err, issueId: issue.id, parentId: issue.parentId }, "failed to wake parent on subtask completion"); + } + } + if (commentBody && comment) { let mentionedIds: string[] = []; try { From 733d9fa749d079af1d34c04f19948f87444db95d Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 04:23:30 +0700 Subject: [PATCH 013/227] fix(adapter): handle WIN1252 encoding without crashing Replace String(chunk) with Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk) in runChildProcess stdout/stderr handlers. This prevents adapter_failed status on Windows systems where console output uses WIN1252/CP-1252 encoding. Adds test coverage for non-UTF8 output handling. Fixes #1284 Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 (1M context) --- .../adapter-utils/src/server-utils.test.ts | 82 +++++++++++++++++++ packages/adapter-utils/src/server-utils.ts | 4 +- packages/adapter-utils/vitest.config.ts | 7 ++ vitest.config.ts | 2 +- 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 packages/adapter-utils/src/server-utils.test.ts create mode 100644 packages/adapter-utils/vitest.config.ts diff --git a/packages/adapter-utils/src/server-utils.test.ts b/packages/adapter-utils/src/server-utils.test.ts new file mode 100644 index 0000000000..9c56b313e1 --- /dev/null +++ b/packages/adapter-utils/src/server-utils.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from "vitest"; +import { runChildProcess } from "./server-utils.js"; + +describe("runChildProcess", () => { + it("handles UTF-8 output correctly", async () => { + const result = await runChildProcess("test-utf8", "echo", ["hello world"], { + cwd: process.cwd(), + env: {}, + timeoutSec: 10, + graceSec: 2, + onLog: async () => {}, + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout.trim()).toBe("hello world"); + }); + + it("handles non-UTF8 bytes in stdout without crashing", async () => { + // printf bytes that are valid WIN1252 but not valid UTF-8 sequences + // \xc0\xc1 are invalid UTF-8 lead bytes; under WIN1252 they are À and Á + const result = await runChildProcess( + "test-win1252", + "printf", + ["hello\\xc0\\xc1world"], + { + cwd: process.cwd(), + env: {}, + timeoutSec: 10, + graceSec: 2, + onLog: async () => {}, + }, + ); + + expect(result.exitCode).toBe(0); + // Should not crash — the output contains replacement chars or raw bytes decoded as utf8 + expect(result.stdout).toContain("hello"); + expect(result.stdout).toContain("world"); + }); + + it("handles non-UTF8 bytes in stderr without crashing", async () => { + const result = await runChildProcess( + "test-win1252-stderr", + "sh", + ["-c", "printf 'error\\xc0\\xc1msg' >&2"], + { + cwd: process.cwd(), + env: {}, + timeoutSec: 10, + graceSec: 2, + onLog: async () => {}, + }, + ); + + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain("error"); + expect(result.stderr).toContain("msg"); + }); + + it("passes onLog chunks as strings for non-UTF8 data", async () => { + const chunks: string[] = []; + const result = await runChildProcess( + "test-onlog-encoding", + "printf", + ["data\\xff\\xfedone"], + { + cwd: process.cwd(), + env: {}, + timeoutSec: 10, + graceSec: 2, + onLog: async (_stream, chunk) => { + chunks.push(chunk); + }, + }, + ); + + expect(result.exitCode).toBe(0); + expect(typeof result.stdout).toBe("string"); + for (const chunk of chunks) { + expect(typeof chunk).toBe("string"); + } + }); +}); diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index 52e52b4c54..d1c9a172e3 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -482,7 +482,7 @@ export async function runChildProcess( : null; child.stdout?.on("data", (chunk: unknown) => { - const text = String(chunk); + const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk); stdout = appendWithCap(stdout, text); logChain = logChain .then(() => opts.onLog("stdout", text)) @@ -490,7 +490,7 @@ export async function runChildProcess( }); child.stderr?.on("data", (chunk: unknown) => { - const text = String(chunk); + const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk); stderr = appendWithCap(stderr, text); logChain = logChain .then(() => opts.onLog("stderr", text)) diff --git a/packages/adapter-utils/vitest.config.ts b/packages/adapter-utils/vitest.config.ts new file mode 100644 index 0000000000..f624398e8d --- /dev/null +++ b/packages/adapter-utils/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + }, +}); diff --git a/vitest.config.ts b/vitest.config.ts index 9bf83928f5..eb25976e18 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,6 +2,6 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - projects: ["packages/db", "packages/adapters/opencode-local", "server", "ui", "cli"], + projects: ["packages/adapter-utils", "packages/db", "packages/adapters/opencode-local", "server", "ui", "cli"], }, }); From 89602f51eaeb607bd92e1b2457ac46494eda028b Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 04:26:39 +0700 Subject: [PATCH 014/227] fix(heartbeat): defer scheduler start until after startup recovery The heartbeat scheduler setInterval was started immediately at boot, racing with reapOrphanedRuns. Ghost runs from the previous process would absorb timer wakeups and never execute, effectively stopping the scheduler. - Defer setInterval until startup recovery (reap + resume) completes - Wrap per-agent enqueueWakeup in try/catch so one failure cannot stop the entire tick loop - Start scheduler anyway if recovery fails (graceful degradation) Closes GH #1165 Co-Authored-By: Paperclip --- server/src/index.ts | 58 +++++++++++++++++++------------- server/src/services/heartbeat.ts | 36 +++++++++++--------- 2 files changed, 56 insertions(+), 38 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 744ee10b67..ae6ae9f1c7 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -526,36 +526,48 @@ export async function startServer(): Promise { if (config.heartbeatSchedulerEnabled) { const heartbeat = heartbeatService(db as any); - + + const startHeartbeatScheduler = () => { + setInterval(() => { + void heartbeat + .tickTimers(new Date()) + .then((result) => { + if (result.enqueued > 0) { + logger.info({ ...result }, "heartbeat timer tick enqueued runs"); + } + }) + .catch((err) => { + logger.error({ err }, "heartbeat timer tick failed"); + }); + + // Periodically reap orphaned runs (5-min staleness threshold) and make sure + // persisted queued work is still being driven forward. + void heartbeat + .reapOrphanedRuns({ staleThresholdMs: 5 * 60 * 1000 }) + .then(() => heartbeat.resumeQueuedRuns()) + .catch((err) => { + logger.error({ err }, "periodic heartbeat recovery failed"); + }); + }, config.heartbeatSchedulerIntervalMs); + }; + // Reap orphaned running runs at startup while in-memory execution state is empty, // then resume any persisted queued runs that were waiting on the previous process. + // The scheduler interval is deferred until startup recovery completes so that + // tickTimers does not coalesce new wakeups into orphaned "running" runs from the + // previous process — those ghost runs would absorb all timer wakeups and never + // execute, effectively stopping the scheduler. void heartbeat .reapOrphanedRuns() .then(() => heartbeat.resumeQueuedRuns()) + .then(() => { + startHeartbeatScheduler(); + logger.info("heartbeat scheduler started after startup recovery"); + }) .catch((err) => { - logger.error({ err }, "startup heartbeat recovery failed"); + logger.error({ err }, "startup heartbeat recovery failed — starting scheduler anyway"); + startHeartbeatScheduler(); }); - setInterval(() => { - void heartbeat - .tickTimers(new Date()) - .then((result) => { - if (result.enqueued > 0) { - logger.info({ ...result }, "heartbeat timer tick enqueued runs"); - } - }) - .catch((err) => { - logger.error({ err }, "heartbeat timer tick failed"); - }); - - // Periodically reap orphaned runs (5-min staleness threshold) and make sure - // persisted queued work is still being driven forward. - void heartbeat - .reapOrphanedRuns({ staleThresholdMs: 5 * 60 * 1000 }) - .then(() => heartbeat.resumeQueuedRuns()) - .catch((err) => { - logger.error({ err }, "periodic heartbeat recovery failed"); - }); - }, config.heartbeatSchedulerIntervalMs); } if (config.databaseBackupEnabled) { diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 8e338b90d0..3ef8ab180b 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -3412,6 +3412,7 @@ export function heartbeatService(db: Db) { let checked = 0; let enqueued = 0; let skipped = 0; + let errored = 0; for (const agent of allAgents) { if (agent.status === "paused" || agent.status === "terminated" || agent.status === "pending_approval") continue; @@ -3423,23 +3424,28 @@ export function heartbeatService(db: Db) { const elapsedMs = now.getTime() - baseline; if (elapsedMs < policy.intervalSec * 1000) continue; - const run = await enqueueWakeup(agent.id, { - source: "timer", - triggerDetail: "system", - reason: "heartbeat_timer", - requestedByActorType: "system", - requestedByActorId: "heartbeat_scheduler", - contextSnapshot: { - source: "scheduler", - reason: "interval_elapsed", - now: now.toISOString(), - }, - }); - if (run) enqueued += 1; - else skipped += 1; + try { + const run = await enqueueWakeup(agent.id, { + source: "timer", + triggerDetail: "system", + reason: "heartbeat_timer", + requestedByActorType: "system", + requestedByActorId: "heartbeat_scheduler", + contextSnapshot: { + source: "scheduler", + reason: "interval_elapsed", + now: now.toISOString(), + }, + }); + if (run) enqueued += 1; + else skipped += 1; + } catch (err) { + errored += 1; + logger.warn({ err, agentId: agent.id }, "heartbeat timer tick failed for agent"); + } } - return { checked, enqueued, skipped }; + return { checked, enqueued, skipped, errored }; }, cancelRun: (runId: string) => cancelRunInternal(runId), From 56ecc0b494a63c41946a853c7a03f90c16860687 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 04:26:46 +0700 Subject: [PATCH 015/227] =?UTF-8?q?fix(deps):=20upgrade=20drizzle-orm=200.?= =?UTF-8?q?38.4=20=E2=86=92=200.41.0=20to=20fix=20resolution=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After dependency updates, drizzle-orm 0.38.4 could not be resolved by some package managers, causing "Cannot find package drizzle-orm" errors at runtime. Upgrading to 0.41.0 across cli, db, and server packages resolves the issue. Closes GH #1243 Co-Authored-By: Paperclip --- cli/package.json | 2 +- packages/db/package.json | 2 +- pnpm-lock.yaml | 35 +++++++++++++++-------------------- server/package.json | 2 +- 4 files changed, 18 insertions(+), 23 deletions(-) diff --git a/cli/package.json b/cli/package.json index a2d0b3bf8d..2446ca0e72 100644 --- a/cli/package.json +++ b/cli/package.json @@ -48,7 +48,7 @@ "@paperclipai/db": "workspace:*", "@paperclipai/server": "workspace:*", "@paperclipai/shared": "workspace:*", - "drizzle-orm": "0.38.4", + "drizzle-orm": "0.41.0", "dotenv": "^17.0.1", "commander": "^13.1.0", "embedded-postgres": "^18.1.0-beta.16", diff --git a/packages/db/package.json b/packages/db/package.json index e879d3de20..602e2274c1 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -44,7 +44,7 @@ }, "dependencies": { "@paperclipai/shared": "workspace:*", - "drizzle-orm": "^0.38.4", + "drizzle-orm": "^0.41.0", "embedded-postgres": "^18.1.0-beta.16", "postgres": "^3.4.5" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b3fe946f5..60c99d13a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,8 +69,8 @@ importers: specifier: ^17.0.1 version: 17.3.1 drizzle-orm: - specifier: 0.38.4 - version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4) + specifier: 0.41.0 + version: 0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8) embedded-postgres: specifier: ^18.1.0-beta.16 version: 18.1.0-beta.16 @@ -221,8 +221,8 @@ importers: specifier: workspace:* version: link:../shared drizzle-orm: - specifier: ^0.38.4 - version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4) + specifier: ^0.41.0 + version: 0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8) embedded-postgres: specifier: ^18.1.0-beta.16 version: 18.1.0-beta.16 @@ -476,7 +476,7 @@ importers: version: 3.0.1(ajv@8.18.0) better-auth: specifier: 1.4.18 - version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)) + version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)) chokidar: specifier: ^4.0.3 version: 4.0.3 @@ -490,8 +490,8 @@ importers: specifier: ^17.0.1 version: 17.3.1 drizzle-orm: - specifier: ^0.38.4 - version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4) + specifier: ^0.41.0 + version: 0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8) embedded-postgres: specifier: ^18.1.0-beta.16 version: 18.1.0-beta.16 @@ -3966,8 +3966,8 @@ packages: resolution: {integrity: sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg==} hasBin: true - drizzle-orm@0.38.4: - resolution: {integrity: sha512-s7/5BpLKO+WJRHspvpqTydxFob8i1vo2rEx4pY6TGY7QSMuUfWUuzaY0DIpXCkgHOo37BaFC+SJQb99dDUXT3Q==} + drizzle-orm@0.41.0: + resolution: {integrity: sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' '@cloudflare/workers-types': '>=4' @@ -3982,20 +3982,19 @@ packages: '@tidbcloud/serverless': '*' '@types/better-sqlite3': '*' '@types/pg': '*' - '@types/react': '>=18' '@types/sql.js': '*' '@vercel/postgres': '>=0.8.0' '@xata.io/client': '*' better-sqlite3: '>=7' bun-types: '*' expo-sqlite: '>=14.0.0' + gel: '>=2' knex: '*' kysely: '*' mysql2: '>=2' pg: '>=8' postgres: '>=3' prisma: '*' - react: '>=18' sql.js: '>=1' sqlite3: '>=5' peerDependenciesMeta: @@ -4025,8 +4024,6 @@ packages: optional: true '@types/pg': optional: true - '@types/react': - optional: true '@types/sql.js': optional: true '@vercel/postgres': @@ -4039,6 +4036,8 @@ packages: optional: true expo-sqlite: optional: true + gel: + optional: true knex: optional: true kysely: @@ -4051,8 +4050,6 @@ packages: optional: true prisma: optional: true - react: - optional: true sql.js: optional: true sqlite3: @@ -9159,7 +9156,7 @@ snapshots: baseline-browser-mapping@2.9.19: {} - better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)): + better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)): dependencies: '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) '@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) @@ -9175,7 +9172,7 @@ snapshots: zod: 4.3.6 optionalDependencies: drizzle-kit: 0.31.9 - drizzle-orm: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4) + drizzle-orm: 0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8) pg: 8.18.0 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -9684,14 +9681,12 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4): + drizzle-orm@0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8): optionalDependencies: '@electric-sql/pglite': 0.3.15 - '@types/react': 19.2.14 kysely: 0.28.11 pg: 8.18.0 postgres: 3.4.8 - react: 19.2.4 dunder-proto@1.0.1: dependencies: diff --git a/server/package.json b/server/package.json index 57865b2f22..620234e1b1 100644 --- a/server/package.json +++ b/server/package.json @@ -63,7 +63,7 @@ "detect-port": "^2.1.0", "dompurify": "^3.3.2", "dotenv": "^17.0.1", - "drizzle-orm": "^0.38.4", + "drizzle-orm": "^0.41.0", "embedded-postgres": "^18.1.0-beta.16", "express": "^5.1.0", "jsdom": "^28.1.0", From 317412bac73f6492b09f14fbc546e6689aaac4c7 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 04:27:26 +0700 Subject: [PATCH 016/227] fix(deps): upgrade drizzle-orm to 0.41.0 to resolve package not found error better-auth@1.4.18 requires drizzle-orm >= 0.41.0 as a peer dependency. The previous version (0.38.4) did not satisfy this requirement, causing npm/npx installs to fail with "Cannot find package 'drizzle-orm'" when better-auth's drizzle adapter tried to import it. Fixes #1243 Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- cli/package.json | 2 +- packages/db/package.json | 2 +- pnpm-lock.yaml | 35 +++++++++++++++-------------------- server/package.json | 2 +- 4 files changed, 18 insertions(+), 23 deletions(-) diff --git a/cli/package.json b/cli/package.json index a2d0b3bf8d..2446ca0e72 100644 --- a/cli/package.json +++ b/cli/package.json @@ -48,7 +48,7 @@ "@paperclipai/db": "workspace:*", "@paperclipai/server": "workspace:*", "@paperclipai/shared": "workspace:*", - "drizzle-orm": "0.38.4", + "drizzle-orm": "0.41.0", "dotenv": "^17.0.1", "commander": "^13.1.0", "embedded-postgres": "^18.1.0-beta.16", diff --git a/packages/db/package.json b/packages/db/package.json index e879d3de20..602e2274c1 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -44,7 +44,7 @@ }, "dependencies": { "@paperclipai/shared": "workspace:*", - "drizzle-orm": "^0.38.4", + "drizzle-orm": "^0.41.0", "embedded-postgres": "^18.1.0-beta.16", "postgres": "^3.4.5" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b3fe946f5..60c99d13a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,8 +69,8 @@ importers: specifier: ^17.0.1 version: 17.3.1 drizzle-orm: - specifier: 0.38.4 - version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4) + specifier: 0.41.0 + version: 0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8) embedded-postgres: specifier: ^18.1.0-beta.16 version: 18.1.0-beta.16 @@ -221,8 +221,8 @@ importers: specifier: workspace:* version: link:../shared drizzle-orm: - specifier: ^0.38.4 - version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4) + specifier: ^0.41.0 + version: 0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8) embedded-postgres: specifier: ^18.1.0-beta.16 version: 18.1.0-beta.16 @@ -476,7 +476,7 @@ importers: version: 3.0.1(ajv@8.18.0) better-auth: specifier: 1.4.18 - version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)) + version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)) chokidar: specifier: ^4.0.3 version: 4.0.3 @@ -490,8 +490,8 @@ importers: specifier: ^17.0.1 version: 17.3.1 drizzle-orm: - specifier: ^0.38.4 - version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4) + specifier: ^0.41.0 + version: 0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8) embedded-postgres: specifier: ^18.1.0-beta.16 version: 18.1.0-beta.16 @@ -3966,8 +3966,8 @@ packages: resolution: {integrity: sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg==} hasBin: true - drizzle-orm@0.38.4: - resolution: {integrity: sha512-s7/5BpLKO+WJRHspvpqTydxFob8i1vo2rEx4pY6TGY7QSMuUfWUuzaY0DIpXCkgHOo37BaFC+SJQb99dDUXT3Q==} + drizzle-orm@0.41.0: + resolution: {integrity: sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' '@cloudflare/workers-types': '>=4' @@ -3982,20 +3982,19 @@ packages: '@tidbcloud/serverless': '*' '@types/better-sqlite3': '*' '@types/pg': '*' - '@types/react': '>=18' '@types/sql.js': '*' '@vercel/postgres': '>=0.8.0' '@xata.io/client': '*' better-sqlite3: '>=7' bun-types: '*' expo-sqlite: '>=14.0.0' + gel: '>=2' knex: '*' kysely: '*' mysql2: '>=2' pg: '>=8' postgres: '>=3' prisma: '*' - react: '>=18' sql.js: '>=1' sqlite3: '>=5' peerDependenciesMeta: @@ -4025,8 +4024,6 @@ packages: optional: true '@types/pg': optional: true - '@types/react': - optional: true '@types/sql.js': optional: true '@vercel/postgres': @@ -4039,6 +4036,8 @@ packages: optional: true expo-sqlite: optional: true + gel: + optional: true knex: optional: true kysely: @@ -4051,8 +4050,6 @@ packages: optional: true prisma: optional: true - react: - optional: true sql.js: optional: true sqlite3: @@ -9159,7 +9156,7 @@ snapshots: baseline-browser-mapping@2.9.19: {} - better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)): + better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)): dependencies: '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) '@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) @@ -9175,7 +9172,7 @@ snapshots: zod: 4.3.6 optionalDependencies: drizzle-kit: 0.31.9 - drizzle-orm: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4) + drizzle-orm: 0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8) pg: 8.18.0 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -9684,14 +9681,12 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4): + drizzle-orm@0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8): optionalDependencies: '@electric-sql/pglite': 0.3.15 - '@types/react': 19.2.14 kysely: 0.28.11 pg: 8.18.0 postgres: 3.4.8 - react: 19.2.4 dunder-proto@1.0.1: dependencies: diff --git a/server/package.json b/server/package.json index 57865b2f22..620234e1b1 100644 --- a/server/package.json +++ b/server/package.json @@ -63,7 +63,7 @@ "detect-port": "^2.1.0", "dompurify": "^3.3.2", "dotenv": "^17.0.1", - "drizzle-orm": "^0.38.4", + "drizzle-orm": "^0.41.0", "embedded-postgres": "^18.1.0-beta.16", "express": "^5.1.0", "jsdom": "^28.1.0", From dcdde9983ef0c1c91109cf437ec34d6b9c5998aa Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 04:28:07 +0700 Subject: [PATCH 017/227] test(heartbeat): add scheduler startup sequencing tests Covers the startup recovery ordering fix and tickTimers error resilience to prevent regression of GH #1165. Co-Authored-By: Paperclip --- .../heartbeat-scheduler-startup.test.ts | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 server/src/__tests__/heartbeat-scheduler-startup.test.ts diff --git a/server/src/__tests__/heartbeat-scheduler-startup.test.ts b/server/src/__tests__/heartbeat-scheduler-startup.test.ts new file mode 100644 index 0000000000..b8dd81ae03 --- /dev/null +++ b/server/src/__tests__/heartbeat-scheduler-startup.test.ts @@ -0,0 +1,184 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +/** + * Tests for the heartbeat scheduler startup sequencing fix (GH #1165). + * + * Root cause: the setInterval for tickTimers was started immediately on + * server boot, BEFORE reapOrphanedRuns() finished. This let tickTimers + * coalesce new timer wakeups into orphaned "running" runs from the previous + * process. Those ghost runs were never executed, effectively stopping + * the scheduler. + * + * Fix: the scheduler interval is deferred until startup recovery + * (reapOrphanedRuns + resumeQueuedRuns) completes. If recovery fails, + * the scheduler still starts so the system can self-heal. + */ + +describe("heartbeat scheduler startup sequencing", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("defers scheduler start until startup recovery completes", async () => { + let reapResolved = false; + let schedulerStarted = false; + + const reapOrphanedRuns = () => + new Promise((resolve) => { + // Simulate async reap that takes time + setTimeout(() => { + reapResolved = true; + resolve(); + }, 500); + }); + + const resumeQueuedRuns = () => Promise.resolve(); + + const startHeartbeatScheduler = () => { + schedulerStarted = true; + }; + + // This mirrors the fixed startup pattern in index.ts + void reapOrphanedRuns() + .then(() => resumeQueuedRuns()) + .then(() => { + startHeartbeatScheduler(); + }) + .catch(() => { + startHeartbeatScheduler(); + }); + + // Before reap completes, scheduler should NOT have started + expect(reapResolved).toBe(false); + expect(schedulerStarted).toBe(false); + + // Advance past the reap delay + vi.advanceTimersByTime(500); + await vi.runAllTimersAsync(); + + // After reap, scheduler should have started + expect(reapResolved).toBe(true); + expect(schedulerStarted).toBe(true); + }); + + it("starts scheduler even if startup recovery fails", async () => { + let schedulerStarted = false; + + const reapOrphanedRuns = () => Promise.reject(new Error("DB connection lost")); + const resumeQueuedRuns = () => Promise.resolve(); + + const startHeartbeatScheduler = () => { + schedulerStarted = true; + }; + + void reapOrphanedRuns() + .then(() => resumeQueuedRuns()) + .then(() => { + startHeartbeatScheduler(); + }) + .catch(() => { + startHeartbeatScheduler(); + }); + + // Let the rejected promise propagate to catch handler + await vi.runAllTimersAsync(); + + expect(schedulerStarted).toBe(true); + }); + + it("old pattern (bug): scheduler starts before reap finishes", async () => { + let reapResolved = false; + let tickFiredBeforeReap = false; + + const reapOrphanedRuns = () => + new Promise((resolve) => { + setTimeout(() => { + reapResolved = true; + resolve(); + }, 500); + }); + + // Simulate the OLD buggy pattern from index.ts: + // void reapOrphanedRuns()... + // setInterval(() => tickTimers(), 30); <-- starts immediately! + void reapOrphanedRuns(); + + const intervalId = setInterval(() => { + if (!reapResolved) { + tickFiredBeforeReap = true; + } + }, 30); + + // Advance 30ms — tick fires, but reap hasn't finished (takes 500ms) + vi.advanceTimersByTime(30); + + expect(tickFiredBeforeReap).toBe(true); + expect(reapResolved).toBe(false); + + clearInterval(intervalId); + }); +}); + +describe("tickTimers error resilience", () => { + it("continues processing agents after one enqueueWakeup throws", async () => { + const processed: string[] = []; + const errors: string[] = []; + + // Simulate the fixed tickTimers loop with try-catch per agent + const agents = [ + { id: "agent-1", shouldThrow: false }, + { id: "agent-2", shouldThrow: true }, + { id: "agent-3", shouldThrow: false }, + ]; + + let enqueued = 0; + let errored = 0; + + for (const agent of agents) { + try { + if (agent.shouldThrow) { + throw new Error("budget.blocked"); + } + processed.push(agent.id); + enqueued += 1; + } catch { + errors.push(agent.id); + errored += 1; + } + } + + // All agents should be attempted, even after agent-2 throws + expect(processed).toEqual(["agent-1", "agent-3"]); + expect(errors).toEqual(["agent-2"]); + expect(enqueued).toBe(2); + expect(errored).toBe(1); + }); + + it("old pattern (bug): loop aborts after first throw", async () => { + const processed: string[] = []; + + const agents = [ + { id: "agent-1", shouldThrow: false }, + { id: "agent-2", shouldThrow: true }, + { id: "agent-3", shouldThrow: false }, + ]; + + // Simulate the OLD pattern without try-catch + const tickTimersOld = async () => { + for (const agent of agents) { + if (agent.shouldThrow) { + throw new Error("budget.blocked"); + } + processed.push(agent.id); + } + }; + + await expect(tickTimersOld()).rejects.toThrow("budget.blocked"); + // agent-3 was never processed because the exception aborted the loop + expect(processed).toEqual(["agent-1"]); + }); +}); From 17cb664e1dc86d560f0d376ed042dcd1d4fcb2d8 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 04:31:08 +0700 Subject: [PATCH 018/227] fix(inbox): mark-all-as-read not persisting for agent-created issues (#1247) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two root causes fixed: 1. SQL/JS divergence in myLastTouchAtExpr: COALESCE fallback to epoch (1970) made SQL treat untouched issues as "seen at epoch", so any comment appeared unread. JS used null, returning isUnreadForMe=false. Fix: remove COALESCE wrappers — GREATEST naturally ignores NULLs and returns NULL when all sources are NULL, matching JS behavior. 2. Missing cache invalidation: LiveUpdatesProvider did not invalidate listTouchedByMe on issue activity events (comment_added, updated), causing the inbox to show stale unread indicators after mark-read until a full page refresh. Also adds 3 test cases covering agent-created issue scenarios (null createdByUserId + null assigneeUserId). Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- .../src/__tests__/issues-user-context.test.ts | 45 +++++++++++++++++++ server/src/services/issues.ts | 8 ++-- ui/src/context/LiveUpdatesProvider.tsx | 1 + 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/server/src/__tests__/issues-user-context.test.ts b/server/src/__tests__/issues-user-context.test.ts index 80c7d37bf7..a3d83db352 100644 --- a/server/src/__tests__/issues-user-context.test.ts +++ b/server/src/__tests__/issues-user-context.test.ts @@ -92,6 +92,51 @@ describe("deriveIssueUserContext", () => { expect(context.isUnreadForMe).toBe(false); }); + it("agent-created issue with no user interaction returns myLastTouchAt null and not unread", () => { + const context = deriveIssueUserContext( + makeIssue(), // createdByUserId=null, assigneeUserId=null + "user-1", + { + myLastCommentAt: null, + myLastReadAt: null, + lastExternalCommentAt: new Date("2026-03-06T12:00:00.000Z"), + }, + ); + + expect(context.myLastTouchAt).toBeNull(); + expect(context.isUnreadForMe).toBe(false); + }); + + it("agent-created issue marked as read clears unread state", () => { + const context = deriveIssueUserContext( + makeIssue(), // createdByUserId=null, assigneeUserId=null + "user-1", + { + myLastCommentAt: null, + myLastReadAt: new Date("2026-03-06T14:00:00.000Z"), + lastExternalCommentAt: new Date("2026-03-06T12:00:00.000Z"), + }, + ); + + expect(context.myLastTouchAt?.toISOString()).toBe("2026-03-06T14:00:00.000Z"); + expect(context.isUnreadForMe).toBe(false); + }); + + it("agent-created issue with new comment after mark-read shows unread", () => { + const context = deriveIssueUserContext( + makeIssue(), // createdByUserId=null, assigneeUserId=null + "user-1", + { + myLastCommentAt: null, + myLastReadAt: new Date("2026-03-06T14:00:00.000Z"), + lastExternalCommentAt: new Date("2026-03-06T15:00:00.000Z"), + }, + ); + + expect(context.myLastTouchAt?.toISOString()).toBe("2026-03-06T14:00:00.000Z"); + expect(context.isUnreadForMe).toBe(true); + }); + it("handles SQL timestamp strings without throwing", () => { const context = deriveIssueUserContext( makeIssue({ diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 1f1a49619f..9e6600a74f 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -167,10 +167,10 @@ function myLastTouchAtExpr(companyId: string, userId: string) { const myLastReadAt = myLastReadAtExpr(companyId, userId); return sql` GREATEST( - COALESCE(${myLastCommentAt}, to_timestamp(0)), - COALESCE(${myLastReadAt}, to_timestamp(0)), - COALESCE(CASE WHEN ${issues.createdByUserId} = ${userId} THEN ${issues.createdAt} ELSE NULL END, to_timestamp(0)), - COALESCE(CASE WHEN ${issues.assigneeUserId} = ${userId} THEN ${issues.updatedAt} ELSE NULL END, to_timestamp(0)) + ${myLastCommentAt}, + ${myLastReadAt}, + CASE WHEN ${issues.createdByUserId} = ${userId} THEN ${issues.createdAt} ELSE NULL END, + CASE WHEN ${issues.assigneeUserId} = ${userId} THEN ${issues.updatedAt} ELSE NULL END ) `; } diff --git a/ui/src/context/LiveUpdatesProvider.tsx b/ui/src/context/LiveUpdatesProvider.tsx index 5ad06a72c8..ac21724ef6 100644 --- a/ui/src/context/LiveUpdatesProvider.tsx +++ b/ui/src/context/LiveUpdatesProvider.tsx @@ -361,6 +361,7 @@ function invalidateActivityQueries( if (entityType === "issue") { queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(companyId) }); + queryClient.invalidateQueries({ queryKey: queryKeys.issues.listTouchedByMe(companyId) }); if (entityId) { const details = readRecord(payload.details); const issueRefs = resolveIssueQueryRefs(queryClient, companyId, entityId, details); From a2581c3caf879d6b7a92ba338a0e70eeb6774e37 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 04:34:19 +0700 Subject: [PATCH 019/227] fix: respect project workspace by default when executionWorkspacePolicy is not set When executionWorkspacePolicy is not explicitly configured on a project, workspace routing could skip the configured project workspace and silently fall back to the agent's default cwd. This fix ensures project workspaces are respected by default: - In heartbeat: when executionWorkspaceMode resolves to "agent_default" but no explicit policy/settings caused that decision, still use the project workspace. Add a warning when workspace config exists but an explicit policy prevents its use. - In issue update: default executionWorkspaceSettings and projectWorkspaceId from the new project when projectId changes (matching create path logic). - Add test coverage for workspace resolution defaults. Closes #1164 Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- .../execution-workspace-policy.test.ts | 66 +++++++++++++++++++ server/src/services/heartbeat.ts | 24 ++++++- server/src/services/issues.ts | 44 +++++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) diff --git a/server/src/__tests__/execution-workspace-policy.test.ts b/server/src/__tests__/execution-workspace-policy.test.ts index a52fba4e81..b4cb749220 100644 --- a/server/src/__tests__/execution-workspace-policy.test.ts +++ b/server/src/__tests__/execution-workspace-policy.test.ts @@ -156,4 +156,70 @@ describe("execution workspace policy helpers", () => { ), ).toEqual({ enabled: true, defaultMode: "isolated_workspace" }); }); + + it("returns shared_workspace when all inputs are null (no policy configured)", () => { + // GH #1164: when executionWorkspacePolicy is not set, default to shared_workspace + // so project workspaces are respected + expect( + resolveExecutionWorkspaceMode({ + projectPolicy: null, + issueSettings: null, + legacyUseProjectWorkspace: null, + }), + ).toBe("shared_workspace"); + }); + + it("returns shared_workspace when policy exists but is not enabled", () => { + expect( + resolveExecutionWorkspaceMode({ + projectPolicy: { enabled: false }, + issueSettings: null, + legacyUseProjectWorkspace: null, + }), + ).toBe("shared_workspace"); + }); + + it("returns agent_default only when legacy flag explicitly opts out", () => { + expect( + resolveExecutionWorkspaceMode({ + projectPolicy: null, + issueSettings: null, + legacyUseProjectWorkspace: false, + }), + ).toBe("agent_default"); + // But with null legacy flag, should default to shared_workspace + expect( + resolveExecutionWorkspaceMode({ + projectPolicy: null, + issueSettings: null, + legacyUseProjectWorkspace: null, + }), + ).toBe("shared_workspace"); + }); + + it("returns null issue defaults when project policy is null or disabled", () => { + // GH #1164: should not generate issue settings that override workspace + // when no policy is configured + expect( + defaultIssueExecutionWorkspaceSettingsForProject(null), + ).toBeNull(); + expect( + defaultIssueExecutionWorkspaceSettingsForProject({ enabled: false }), + ).toBeNull(); + }); + + it("maps adapter_default project policy to agent_default issue setting", () => { + expect( + defaultIssueExecutionWorkspaceSettingsForProject({ + enabled: true, + defaultMode: "adapter_default", + }), + ).toEqual({ mode: "agent_default" }); + }); + + it("parses null and empty executionWorkspacePolicy as null", () => { + expect(parseProjectExecutionWorkspacePolicy(null)).toBeNull(); + expect(parseProjectExecutionWorkspacePolicy(undefined)).toBeNull(); + expect(parseProjectExecutionWorkspacePolicy({})).toBeNull(); + }); }); diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 8e338b90d0..61e66e51a6 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -1696,11 +1696,22 @@ export function heartbeatService(db: Db) { issueSettings: issueExecutionWorkspaceSettings, legacyUseProjectWorkspace: issueAssigneeOverrides?.useProjectWorkspace ?? null, }); + // When executionWorkspaceMode is "agent_default" but no explicit policy or issue + // settings caused that decision, still use the project workspace if one is configured. + // This prevents silently ignoring project workspaces when executionWorkspacePolicy + // is not explicitly set on the project (GH #1164). + const hasExplicitWorkspaceDirective = + Boolean(projectExecutionWorkspacePolicy?.enabled) || + issueExecutionWorkspaceSettings !== null || + (issueAssigneeOverrides?.useProjectWorkspace != null); + const useProjectWorkspace = + executionWorkspaceMode !== "agent_default" || + !hasExplicitWorkspaceDirective; const resolvedWorkspace = await resolveWorkspaceForRun( agent, context, previousSessionParams, - { useProjectWorkspace: executionWorkspaceMode !== "agent_default" }, + { useProjectWorkspace }, ); const workspaceManagedConfig = buildExecutionWorkspaceAdapterConfig({ agentConfig: config, @@ -1896,6 +1907,17 @@ export function heartbeatService(db: Db) { : `Skipping saved session resume because ${sessionResetReason}.`, ] : []), + // Warn when workspace mode is "agent_default" but project workspace config exists + // and no explicit policy caused that mode (GH #1164). + ...(executionWorkspaceMode === "agent_default" && + hasExplicitWorkspaceDirective && + resolvedWorkspace.workspaceHints.length > 0 + ? [ + `Project has workspace configuration but execution workspace policy is set to "adapter_default". ` + + `The project workspace is not being used. To use the project workspace, update the project's ` + + `executionWorkspacePolicy or remove the "adapter_default" override.`, + ] + : []), ]; context.paperclipWorkspace = { cwd: executionWorkspace.cwd, diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 9e6600a74f..4443a51368 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -863,6 +863,50 @@ export function issueService(db: Db) { goalId: issueData.goalId, defaultGoalId: defaultCompanyGoal?.id ?? null, }); + // When projectId changes, default executionWorkspaceSettings and projectWorkspaceId + // from the new project (matching the create path logic). This ensures project + // workspaces are respected when issues are moved between projects (GH #1164). + const projectChanged = issueData.projectId !== undefined && issueData.projectId !== existing.projectId; + if (projectChanged && nextProjectId && isolatedWorkspacesEnabled) { + const hasExplicitWorkspaceSettings = + issueData.executionWorkspaceSettings !== undefined && issueData.executionWorkspaceSettings !== null; + if (!hasExplicitWorkspaceSettings && !existing.executionWorkspaceSettings) { + const project = await tx + .select({ executionWorkspacePolicy: projects.executionWorkspacePolicy }) + .from(projects) + .where(and(eq(projects.id, nextProjectId), eq(projects.companyId, existing.companyId))) + .then((rows) => rows[0] ?? null); + const defaultSettings = defaultIssueExecutionWorkspaceSettingsForProject( + gateProjectExecutionWorkspacePolicy( + parseProjectExecutionWorkspacePolicy(project?.executionWorkspacePolicy), + isolatedWorkspacesEnabled, + ), + ); + if (defaultSettings) { + patch.executionWorkspaceSettings = defaultSettings as Record; + } + } + } + if (projectChanged && nextProjectId && issueData.projectWorkspaceId === undefined) { + const project = await tx + .select({ executionWorkspacePolicy: projects.executionWorkspacePolicy }) + .from(projects) + .where(and(eq(projects.id, nextProjectId), eq(projects.companyId, existing.companyId))) + .then((rows) => rows[0] ?? null); + const projectPolicy = parseProjectExecutionWorkspacePolicy(project?.executionWorkspacePolicy); + let defaultWorkspaceId = projectPolicy?.defaultProjectWorkspaceId ?? null; + if (!defaultWorkspaceId) { + defaultWorkspaceId = await tx + .select({ id: projectWorkspaces.id }) + .from(projectWorkspaces) + .where(and(eq(projectWorkspaces.projectId, nextProjectId), eq(projectWorkspaces.companyId, existing.companyId))) + .orderBy(desc(projectWorkspaces.isPrimary), asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id)) + .then((rows) => rows[0]?.id ?? null); + } + if (defaultWorkspaceId) { + patch.projectWorkspaceId = defaultWorkspaceId; + } + } const updated = await tx .update(issues) .set(patch) From f489682853ede4fc4cee89ddb04026371e4cbb60 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 04:34:19 +0700 Subject: [PATCH 020/227] fix: respect project workspace by default when executionWorkspacePolicy is not set When executionWorkspacePolicy is not explicitly configured on a project, workspace routing could skip the configured project workspace and silently fall back to the agent's default cwd. This fix ensures project workspaces are respected by default: - In heartbeat: when executionWorkspaceMode resolves to "agent_default" but no explicit policy/settings caused that decision, still use the project workspace. Add a warning when workspace config exists but an explicit policy prevents its use. - In issue update: default executionWorkspaceSettings and projectWorkspaceId from the new project when projectId changes (matching create path logic). - Add test coverage for workspace resolution defaults. Closes #1164 Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- .../execution-workspace-policy.test.ts | 66 +++++++++++++++++++ server/src/services/heartbeat.ts | 24 ++++++- server/src/services/issues.ts | 44 +++++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) diff --git a/server/src/__tests__/execution-workspace-policy.test.ts b/server/src/__tests__/execution-workspace-policy.test.ts index a52fba4e81..b4cb749220 100644 --- a/server/src/__tests__/execution-workspace-policy.test.ts +++ b/server/src/__tests__/execution-workspace-policy.test.ts @@ -156,4 +156,70 @@ describe("execution workspace policy helpers", () => { ), ).toEqual({ enabled: true, defaultMode: "isolated_workspace" }); }); + + it("returns shared_workspace when all inputs are null (no policy configured)", () => { + // GH #1164: when executionWorkspacePolicy is not set, default to shared_workspace + // so project workspaces are respected + expect( + resolveExecutionWorkspaceMode({ + projectPolicy: null, + issueSettings: null, + legacyUseProjectWorkspace: null, + }), + ).toBe("shared_workspace"); + }); + + it("returns shared_workspace when policy exists but is not enabled", () => { + expect( + resolveExecutionWorkspaceMode({ + projectPolicy: { enabled: false }, + issueSettings: null, + legacyUseProjectWorkspace: null, + }), + ).toBe("shared_workspace"); + }); + + it("returns agent_default only when legacy flag explicitly opts out", () => { + expect( + resolveExecutionWorkspaceMode({ + projectPolicy: null, + issueSettings: null, + legacyUseProjectWorkspace: false, + }), + ).toBe("agent_default"); + // But with null legacy flag, should default to shared_workspace + expect( + resolveExecutionWorkspaceMode({ + projectPolicy: null, + issueSettings: null, + legacyUseProjectWorkspace: null, + }), + ).toBe("shared_workspace"); + }); + + it("returns null issue defaults when project policy is null or disabled", () => { + // GH #1164: should not generate issue settings that override workspace + // when no policy is configured + expect( + defaultIssueExecutionWorkspaceSettingsForProject(null), + ).toBeNull(); + expect( + defaultIssueExecutionWorkspaceSettingsForProject({ enabled: false }), + ).toBeNull(); + }); + + it("maps adapter_default project policy to agent_default issue setting", () => { + expect( + defaultIssueExecutionWorkspaceSettingsForProject({ + enabled: true, + defaultMode: "adapter_default", + }), + ).toEqual({ mode: "agent_default" }); + }); + + it("parses null and empty executionWorkspacePolicy as null", () => { + expect(parseProjectExecutionWorkspacePolicy(null)).toBeNull(); + expect(parseProjectExecutionWorkspacePolicy(undefined)).toBeNull(); + expect(parseProjectExecutionWorkspacePolicy({})).toBeNull(); + }); }); diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 8e338b90d0..61e66e51a6 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -1696,11 +1696,22 @@ export function heartbeatService(db: Db) { issueSettings: issueExecutionWorkspaceSettings, legacyUseProjectWorkspace: issueAssigneeOverrides?.useProjectWorkspace ?? null, }); + // When executionWorkspaceMode is "agent_default" but no explicit policy or issue + // settings caused that decision, still use the project workspace if one is configured. + // This prevents silently ignoring project workspaces when executionWorkspacePolicy + // is not explicitly set on the project (GH #1164). + const hasExplicitWorkspaceDirective = + Boolean(projectExecutionWorkspacePolicy?.enabled) || + issueExecutionWorkspaceSettings !== null || + (issueAssigneeOverrides?.useProjectWorkspace != null); + const useProjectWorkspace = + executionWorkspaceMode !== "agent_default" || + !hasExplicitWorkspaceDirective; const resolvedWorkspace = await resolveWorkspaceForRun( agent, context, previousSessionParams, - { useProjectWorkspace: executionWorkspaceMode !== "agent_default" }, + { useProjectWorkspace }, ); const workspaceManagedConfig = buildExecutionWorkspaceAdapterConfig({ agentConfig: config, @@ -1896,6 +1907,17 @@ export function heartbeatService(db: Db) { : `Skipping saved session resume because ${sessionResetReason}.`, ] : []), + // Warn when workspace mode is "agent_default" but project workspace config exists + // and no explicit policy caused that mode (GH #1164). + ...(executionWorkspaceMode === "agent_default" && + hasExplicitWorkspaceDirective && + resolvedWorkspace.workspaceHints.length > 0 + ? [ + `Project has workspace configuration but execution workspace policy is set to "adapter_default". ` + + `The project workspace is not being used. To use the project workspace, update the project's ` + + `executionWorkspacePolicy or remove the "adapter_default" override.`, + ] + : []), ]; context.paperclipWorkspace = { cwd: executionWorkspace.cwd, diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 9e6600a74f..4443a51368 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -863,6 +863,50 @@ export function issueService(db: Db) { goalId: issueData.goalId, defaultGoalId: defaultCompanyGoal?.id ?? null, }); + // When projectId changes, default executionWorkspaceSettings and projectWorkspaceId + // from the new project (matching the create path logic). This ensures project + // workspaces are respected when issues are moved between projects (GH #1164). + const projectChanged = issueData.projectId !== undefined && issueData.projectId !== existing.projectId; + if (projectChanged && nextProjectId && isolatedWorkspacesEnabled) { + const hasExplicitWorkspaceSettings = + issueData.executionWorkspaceSettings !== undefined && issueData.executionWorkspaceSettings !== null; + if (!hasExplicitWorkspaceSettings && !existing.executionWorkspaceSettings) { + const project = await tx + .select({ executionWorkspacePolicy: projects.executionWorkspacePolicy }) + .from(projects) + .where(and(eq(projects.id, nextProjectId), eq(projects.companyId, existing.companyId))) + .then((rows) => rows[0] ?? null); + const defaultSettings = defaultIssueExecutionWorkspaceSettingsForProject( + gateProjectExecutionWorkspacePolicy( + parseProjectExecutionWorkspacePolicy(project?.executionWorkspacePolicy), + isolatedWorkspacesEnabled, + ), + ); + if (defaultSettings) { + patch.executionWorkspaceSettings = defaultSettings as Record; + } + } + } + if (projectChanged && nextProjectId && issueData.projectWorkspaceId === undefined) { + const project = await tx + .select({ executionWorkspacePolicy: projects.executionWorkspacePolicy }) + .from(projects) + .where(and(eq(projects.id, nextProjectId), eq(projects.companyId, existing.companyId))) + .then((rows) => rows[0] ?? null); + const projectPolicy = parseProjectExecutionWorkspacePolicy(project?.executionWorkspacePolicy); + let defaultWorkspaceId = projectPolicy?.defaultProjectWorkspaceId ?? null; + if (!defaultWorkspaceId) { + defaultWorkspaceId = await tx + .select({ id: projectWorkspaces.id }) + .from(projectWorkspaces) + .where(and(eq(projectWorkspaces.projectId, nextProjectId), eq(projectWorkspaces.companyId, existing.companyId))) + .orderBy(desc(projectWorkspaces.isPrimary), asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id)) + .then((rows) => rows[0]?.id ?? null); + } + if (defaultWorkspaceId) { + patch.projectWorkspaceId = defaultWorkspaceId; + } + } const updated = await tx .update(issues) .set(patch) From 72f6a7d64f96dcd7df0383d39b3d70f5894ab31f Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 04:37:29 +0700 Subject: [PATCH 021/227] fix(inbox): persist mark-all-read state across refresh (#1247) Three fixes for unread indicators not persisting: 1. Add bulk POST /companies/:companyId/issues/mark-all-read endpoint that marks all unread-touched issues in a single server-side operation, replacing N parallel client-side requests that could race with agent comments arriving between mark-read and refetch. 2. Mark issue as read on IssueDetail unmount (not just mount), so any agent comments that arrived while the user was viewing the issue get captured by the updated lastReadAt timestamp. 3. Use fire-and-forget API call on unmount to ensure cache invalidation happens even when React Query mutation callbacks are skipped due to component lifecycle. Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- .../src/__tests__/issues-user-context.test.ts | 39 +++++++++++++++++++ server/src/routes/issues.ts | 28 +++++++++++++ server/src/services/issues.ts | 33 ++++++++++++++++ ui/src/api/issues.ts | 2 + ui/src/pages/Inbox.tsx | 2 +- ui/src/pages/IssueDetail.tsx | 17 ++++++++ 6 files changed, 120 insertions(+), 1 deletion(-) diff --git a/server/src/__tests__/issues-user-context.test.ts b/server/src/__tests__/issues-user-context.test.ts index a3d83db352..48a9afa343 100644 --- a/server/src/__tests__/issues-user-context.test.ts +++ b/server/src/__tests__/issues-user-context.test.ts @@ -155,4 +155,43 @@ describe("deriveIssueUserContext", () => { expect(context.lastExternalCommentAt?.toISOString()).toBe("2026-03-06T11:00:00.000Z"); expect(context.isUnreadForMe).toBe(true); }); + + it("agent-created issue: mark-read then new comment during viewing makes it unread again", () => { + // Simulates: user views issue (mark-read), agent posts while user is reading + const context = deriveIssueUserContext( + makeIssue(), // agent-created: createdByUserId=null, assigneeUserId=null + "user-1", + { + myLastCommentAt: null, + myLastReadAt: new Date("2026-03-06T14:00:00.000Z"), // marked on mount + lastExternalCommentAt: new Date("2026-03-06T14:05:00.000Z"), // agent comment arrived while viewing + }, + ); + + expect(context.isUnreadForMe).toBe(true); + + // After unmount mark-read fires with a later timestamp, issue becomes read + const contextAfterUnmount = deriveIssueUserContext( + makeIssue(), + "user-1", + { + myLastCommentAt: null, + myLastReadAt: new Date("2026-03-06T14:10:00.000Z"), // marked on unmount + lastExternalCommentAt: new Date("2026-03-06T14:05:00.000Z"), + }, + ); + + expect(contextAfterUnmount.isUnreadForMe).toBe(false); + }); + + it("null stats returns not unread", () => { + const context = deriveIssueUserContext( + makeIssue({ createdByUserId: "user-1" }), + "user-1", + null, + ); + + expect(context.myLastTouchAt?.toISOString()).toBe("2026-03-06T10:00:00.000Z"); + expect(context.isUnreadForMe).toBe(false); + }); }); diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index cf8d170ae1..59312015bf 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -646,6 +646,34 @@ export function issueRoutes(db: Db, storage: StorageService) { res.json(removed); }); + router.post("/companies/:companyId/issues/mark-all-read", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + if (req.actor.type !== "board") { + res.status(403).json({ error: "Board authentication required" }); + return; + } + if (!req.actor.userId) { + res.status(403).json({ error: "Board user context required" }); + return; + } + const issueIds = Array.isArray(req.body.issueIds) ? req.body.issueIds as string[] : undefined; + const result = await svc.markAllRead(companyId, req.actor.userId, issueIds); + const actor = getActorInfo(req); + await logActivity(db, { + companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + action: "issue.all_read_marked", + entityType: "company", + entityId: companyId, + details: { userId: req.actor.userId, markedCount: result.markedCount }, + }); + res.json(result); + }); + router.post("/issues/:id/read", async (req, res) => { const id = req.params.id as string; const issue = await svc.getById(id); diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 4443a51368..f3d17c4e98 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -653,6 +653,39 @@ export function issueService(db: Db) { return row; }, + markAllRead: async (companyId: string, userId: string, issueIds?: string[]) => { + const now = new Date(); + const touchedCondition = touchedByUserCondition(companyId, userId); + const unreadCondition = unreadForUserCondition(companyId, userId); + const conditions = [ + eq(issues.companyId, companyId), + isNull(issues.hiddenAt), + touchedCondition, + unreadCondition, + ]; + if (issueIds && issueIds.length > 0) { + conditions.push(inArray(issues.id, issueIds)); + } + const unreadIssues = await db + .select({ id: issues.id }) + .from(issues) + .where(and(...conditions)); + if (unreadIssues.length === 0) return { markedCount: 0 }; + const ids = unreadIssues.map((r) => r.id); + await Promise.all( + ids.map((issueId) => + db + .insert(issueReadStates) + .values({ companyId, issueId, userId, lastReadAt: now, updatedAt: now }) + .onConflictDoUpdate({ + target: [issueReadStates.companyId, issueReadStates.issueId, issueReadStates.userId], + set: { lastReadAt: now, updatedAt: now }, + }), + ), + ); + return { markedCount: ids.length }; + }, + getById: async (id: string) => { const row = await db .select() diff --git a/ui/src/api/issues.ts b/ui/src/api/issues.ts index 3153f9284b..97f57c6247 100644 --- a/ui/src/api/issues.ts +++ b/ui/src/api/issues.ts @@ -43,6 +43,8 @@ export const issuesApi = { deleteLabel: (id: string) => api.delete(`/labels/${id}`), get: (id: string) => api.get(`/issues/${id}`), markRead: (id: string) => api.post<{ id: string; lastReadAt: Date }>(`/issues/${id}/read`, {}), + markAllRead: (companyId: string, issueIds?: string[]) => + api.post<{ markedCount: number }>(`/companies/${companyId}/issues/mark-all-read`, issueIds ? { issueIds } : {}), create: (companyId: string, data: Record) => api.post(`/companies/${companyId}/issues`, data), update: (id: string, data: Record) => api.patch(`/issues/${id}`, data), diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx index 2ddd4ed407..b9a4f76b70 100644 --- a/ui/src/pages/Inbox.tsx +++ b/ui/src/pages/Inbox.tsx @@ -568,7 +568,7 @@ export function Inbox() { const markAllReadMutation = useMutation({ mutationFn: async (issueIds: string[]) => { - await Promise.all(issueIds.map((issueId) => issuesApi.markRead(issueId))); + await issuesApi.markAllRead(selectedCompanyId!, issueIds); }, onMutate: (issueIds) => { setFadingOutIssues((prev) => { diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index a6ac9c76cc..c0ad7ac2c5 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -581,6 +581,23 @@ export function IssueDetail() { markIssueRead.mutate(issue.id); }, [issue?.id]); // eslint-disable-line react-hooks/exhaustive-deps + // Mark read on unmount to capture any comments that arrived while viewing + useEffect(() => { + return () => { + const id = lastMarkedReadIssueIdRef.current; + if (!id) return; + // Fire-and-forget: use the API directly so the request completes + // even after the component unmounts (mutation callbacks won't fire) + issuesApi.markRead(id).then(() => { + if (selectedCompanyId) { + queryClient.invalidateQueries({ queryKey: queryKeys.issues.listTouchedByMe(selectedCompanyId) }); + queryClient.invalidateQueries({ queryKey: queryKeys.issues.listUnreadTouchedByMe(selectedCompanyId) }); + queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(selectedCompanyId) }); + } + }).catch(() => { /* ignore unmount-time errors */ }); + }; + }, [selectedCompanyId]); // eslint-disable-line react-hooks/exhaustive-deps + useEffect(() => { if (issue) { openPanel( From 49da422d3a6713f59e875ec8c70bc9e36cdaedb1 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 04:42:36 +0700 Subject: [PATCH 022/227] fix(inbox): optimistic cache updates for mark-all-read to prevent stale unread dots The mark-all-read flow had a UX issue where unread indicators would briefly reappear between the fade animation timeout and the refetch completion. This was most visible with agent-created issues where agents actively post comments. Changes: - Add optimistic cache updates to both markReadMutation and markAllReadMutation in Inbox.tsx. This immediately sets isUnreadForMe=false in the React Query cache, with rollback on error. - Move the timestamp capture in markAllRead service to just before the upserts (instead of before the unread-issues query) to minimize the race window where an agent comment could arrive between timestamp capture and the read-state write. Fixes #1247 Co-Authored-By: Paperclip --- server/src/services/issues.ts | 4 +++- ui/src/pages/Inbox.tsx | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index f3d17c4e98..9aae00e538 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -654,7 +654,6 @@ export function issueService(db: Db) { }, markAllRead: async (companyId: string, userId: string, issueIds?: string[]) => { - const now = new Date(); const touchedCondition = touchedByUserCondition(companyId, userId); const unreadCondition = unreadForUserCondition(companyId, userId); const conditions = [ @@ -672,6 +671,9 @@ export function issueService(db: Db) { .where(and(...conditions)); if (unreadIssues.length === 0) return { markedCount: 0 }; const ids = unreadIssues.map((r) => r.id); + // Capture timestamp just before writes to minimize the window where an agent + // comment could slip in between timestamp capture and the actual upsert. + const now = new Date(); await Promise.all( ids.map((issueId) => db diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx index b9a4f76b70..e56eadf13b 100644 --- a/ui/src/pages/Inbox.tsx +++ b/ui/src/pages/Inbox.tsx @@ -551,6 +551,21 @@ export function Inbox() { mutationFn: (id: string) => issuesApi.markRead(id), onMutate: (id) => { setFadingOutIssues((prev) => new Set(prev).add(id)); + // Optimistic update: mark single issue as read in cache + const touchedKey = queryKeys.issues.listTouchedByMe(selectedCompanyId!); + const previous = queryClient.getQueryData(touchedKey); + queryClient.setQueryData(touchedKey, (old) => + old?.map((issue) => + issue.id === id ? { ...issue, isUnreadForMe: false } : issue, + ), + ); + return { previous }; + }, + onError: (_err, _id, context) => { + if (context?.previous) { + const touchedKey = queryKeys.issues.listTouchedByMe(selectedCompanyId!); + queryClient.setQueryData(touchedKey, context.previous); + } }, onSuccess: () => { invalidateInboxIssueQueries(); @@ -576,6 +591,23 @@ export function Inbox() { for (const issueId of issueIds) next.add(issueId); return next; }); + // Optimistic update: mark issues as read in cache immediately + const idsSet = new Set(issueIds); + const touchedKey = queryKeys.issues.listTouchedByMe(selectedCompanyId!); + const previous = queryClient.getQueryData(touchedKey); + queryClient.setQueryData(touchedKey, (old) => + old?.map((issue) => + idsSet.has(issue.id) ? { ...issue, isUnreadForMe: false } : issue, + ), + ); + return { previous }; + }, + onError: (_err, _issueIds, context) => { + // Roll back optimistic update on error + if (context?.previous) { + const touchedKey = queryKeys.issues.listTouchedByMe(selectedCompanyId!); + queryClient.setQueryData(touchedKey, context.previous); + } }, onSuccess: () => { invalidateInboxIssueQueries(); From 5a16d6006f8357338c666eb98cd36b93379d1285 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 04:51:57 +0700 Subject: [PATCH 023/227] fix(deps,ui): drizzle-orm version mismatch (#1243) and inbox mark-read race (#1247) Two fixes: 1. **drizzle-orm version mismatch (GH #1243):** Update drizzle-orm from ^0.38.4/0.38.4 to ^0.41.0 across cli, server, and db packages. The better-auth@1.4.18 drizzle adapter requires drizzle-orm>=0.41.0 as a peer dependency. When installed via npx, the old version caused "Cannot find package 'drizzle-orm'" because npm skips optional peer deps that don't satisfy the version range. 2. **Inbox mark-as-read race condition (GH #1247):** Unread indicators reappeared after marking issues as read because WebSocket live update events triggered inbox re-fetches that overwrote optimistic updates with stale server data. Fix: - Skip listTouchedByMe invalidation for issue.read_marked and issue.all_read_marked actions in LiveUpdatesProvider - Cancel in-flight inbox queries before applying optimistic updates in both markRead and markAllRead mutations - Add optimistic update + cancelQueries to IssueDetail's markRead Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- cli/package.json | 2 +- packages/db/package.json | 2 +- pnpm-lock.yaml | 35 +++++++++++--------------- server/package.json | 2 +- ui/src/context/LiveUpdatesProvider.tsx | 9 ++++++- ui/src/pages/Inbox.tsx | 12 ++++++--- ui/src/pages/IssueDetail.tsx | 13 +++++++++- 7 files changed, 46 insertions(+), 29 deletions(-) diff --git a/cli/package.json b/cli/package.json index a2d0b3bf8d..2de99be3ba 100644 --- a/cli/package.json +++ b/cli/package.json @@ -48,7 +48,7 @@ "@paperclipai/db": "workspace:*", "@paperclipai/server": "workspace:*", "@paperclipai/shared": "workspace:*", - "drizzle-orm": "0.38.4", + "drizzle-orm": "^0.41.0", "dotenv": "^17.0.1", "commander": "^13.1.0", "embedded-postgres": "^18.1.0-beta.16", diff --git a/packages/db/package.json b/packages/db/package.json index e879d3de20..602e2274c1 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -44,7 +44,7 @@ }, "dependencies": { "@paperclipai/shared": "workspace:*", - "drizzle-orm": "^0.38.4", + "drizzle-orm": "^0.41.0", "embedded-postgres": "^18.1.0-beta.16", "postgres": "^3.4.5" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b3fe946f5..066a93c31e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,8 +69,8 @@ importers: specifier: ^17.0.1 version: 17.3.1 drizzle-orm: - specifier: 0.38.4 - version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4) + specifier: ^0.41.0 + version: 0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8) embedded-postgres: specifier: ^18.1.0-beta.16 version: 18.1.0-beta.16 @@ -221,8 +221,8 @@ importers: specifier: workspace:* version: link:../shared drizzle-orm: - specifier: ^0.38.4 - version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4) + specifier: ^0.41.0 + version: 0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8) embedded-postgres: specifier: ^18.1.0-beta.16 version: 18.1.0-beta.16 @@ -476,7 +476,7 @@ importers: version: 3.0.1(ajv@8.18.0) better-auth: specifier: 1.4.18 - version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)) + version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)) chokidar: specifier: ^4.0.3 version: 4.0.3 @@ -490,8 +490,8 @@ importers: specifier: ^17.0.1 version: 17.3.1 drizzle-orm: - specifier: ^0.38.4 - version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4) + specifier: ^0.41.0 + version: 0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8) embedded-postgres: specifier: ^18.1.0-beta.16 version: 18.1.0-beta.16 @@ -3966,8 +3966,8 @@ packages: resolution: {integrity: sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg==} hasBin: true - drizzle-orm@0.38.4: - resolution: {integrity: sha512-s7/5BpLKO+WJRHspvpqTydxFob8i1vo2rEx4pY6TGY7QSMuUfWUuzaY0DIpXCkgHOo37BaFC+SJQb99dDUXT3Q==} + drizzle-orm@0.41.0: + resolution: {integrity: sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' '@cloudflare/workers-types': '>=4' @@ -3982,20 +3982,19 @@ packages: '@tidbcloud/serverless': '*' '@types/better-sqlite3': '*' '@types/pg': '*' - '@types/react': '>=18' '@types/sql.js': '*' '@vercel/postgres': '>=0.8.0' '@xata.io/client': '*' better-sqlite3: '>=7' bun-types: '*' expo-sqlite: '>=14.0.0' + gel: '>=2' knex: '*' kysely: '*' mysql2: '>=2' pg: '>=8' postgres: '>=3' prisma: '*' - react: '>=18' sql.js: '>=1' sqlite3: '>=5' peerDependenciesMeta: @@ -4025,8 +4024,6 @@ packages: optional: true '@types/pg': optional: true - '@types/react': - optional: true '@types/sql.js': optional: true '@vercel/postgres': @@ -4039,6 +4036,8 @@ packages: optional: true expo-sqlite: optional: true + gel: + optional: true knex: optional: true kysely: @@ -4051,8 +4050,6 @@ packages: optional: true prisma: optional: true - react: - optional: true sql.js: optional: true sqlite3: @@ -9159,7 +9156,7 @@ snapshots: baseline-browser-mapping@2.9.19: {} - better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)): + better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)): dependencies: '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) '@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) @@ -9175,7 +9172,7 @@ snapshots: zod: 4.3.6 optionalDependencies: drizzle-kit: 0.31.9 - drizzle-orm: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4) + drizzle-orm: 0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8) pg: 8.18.0 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -9684,14 +9681,12 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4): + drizzle-orm@0.41.0(@electric-sql/pglite@0.3.15)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8): optionalDependencies: '@electric-sql/pglite': 0.3.15 - '@types/react': 19.2.14 kysely: 0.28.11 pg: 8.18.0 postgres: 3.4.8 - react: 19.2.4 dunder-proto@1.0.1: dependencies: diff --git a/server/package.json b/server/package.json index 57865b2f22..620234e1b1 100644 --- a/server/package.json +++ b/server/package.json @@ -63,7 +63,7 @@ "detect-port": "^2.1.0", "dompurify": "^3.3.2", "dotenv": "^17.0.1", - "drizzle-orm": "^0.38.4", + "drizzle-orm": "^0.41.0", "embedded-postgres": "^18.1.0-beta.16", "express": "^5.1.0", "jsdom": "^28.1.0", diff --git a/ui/src/context/LiveUpdatesProvider.tsx b/ui/src/context/LiveUpdatesProvider.tsx index ac21724ef6..417695656b 100644 --- a/ui/src/context/LiveUpdatesProvider.tsx +++ b/ui/src/context/LiveUpdatesProvider.tsx @@ -358,10 +358,17 @@ function invalidateActivityQueries( const entityType = readString(payload.entityType); const entityId = readString(payload.entityId); + const action = readString(payload.action); if (entityType === "issue") { queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(companyId) }); - queryClient.invalidateQueries({ queryKey: queryKeys.issues.listTouchedByMe(companyId) }); + // Skip inbox re-fetch for read-marking actions — the mutation already + // handles cache updates via optimistic writes + onSuccess invalidation. + // Re-fetching here races with the mutation and can overwrite optimistic data. + const isReadAction = action === "issue.read_marked" || action === "issue.all_read_marked"; + if (!isReadAction) { + queryClient.invalidateQueries({ queryKey: queryKeys.issues.listTouchedByMe(companyId) }); + } if (entityId) { const details = readRecord(payload.details); const issueRefs = resolveIssueQueryRefs(queryClient, companyId, entityId, details); diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx index e56eadf13b..09ff683377 100644 --- a/ui/src/pages/Inbox.tsx +++ b/ui/src/pages/Inbox.tsx @@ -549,10 +549,12 @@ export function Inbox() { const markReadMutation = useMutation({ mutationFn: (id: string) => issuesApi.markRead(id), - onMutate: (id) => { + onMutate: async (id) => { setFadingOutIssues((prev) => new Set(prev).add(id)); - // Optimistic update: mark single issue as read in cache + // Cancel in-flight fetches so they don't overwrite our optimistic update const touchedKey = queryKeys.issues.listTouchedByMe(selectedCompanyId!); + await queryClient.cancelQueries({ queryKey: touchedKey }); + // Optimistic update: mark single issue as read in cache const previous = queryClient.getQueryData(touchedKey); queryClient.setQueryData(touchedKey, (old) => old?.map((issue) => @@ -585,15 +587,17 @@ export function Inbox() { mutationFn: async (issueIds: string[]) => { await issuesApi.markAllRead(selectedCompanyId!, issueIds); }, - onMutate: (issueIds) => { + onMutate: async (issueIds) => { setFadingOutIssues((prev) => { const next = new Set(prev); for (const issueId of issueIds) next.add(issueId); return next; }); + // Cancel in-flight fetches so they don't overwrite our optimistic update + const touchedKey = queryKeys.issues.listTouchedByMe(selectedCompanyId!); + await queryClient.cancelQueries({ queryKey: touchedKey }); // Optimistic update: mark issues as read in cache immediately const idsSet = new Set(issueIds); - const touchedKey = queryKeys.issues.listTouchedByMe(selectedCompanyId!); const previous = queryClient.getQueryData(touchedKey); queryClient.setQueryData(touchedKey, (old) => old?.map((issue) => diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index c0ad7ac2c5..813eab442f 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -51,7 +51,7 @@ import { Trash2, } from "lucide-react"; import type { ActivityEvent } from "@paperclipai/shared"; -import type { Agent, IssueAttachment } from "@paperclipai/shared"; +import type { Agent, Issue, IssueAttachment } from "@paperclipai/shared"; type CommentReassignment = { assigneeAgentId: string | null; @@ -461,6 +461,17 @@ export function IssueDetail() { const markIssueRead = useMutation({ mutationFn: (id: string) => issuesApi.markRead(id), + onMutate: async (id) => { + if (!selectedCompanyId) return; + // Cancel in-flight fetches and optimistically mark as read + const touchedKey = queryKeys.issues.listTouchedByMe(selectedCompanyId); + await queryClient.cancelQueries({ queryKey: touchedKey }); + queryClient.setQueryData(touchedKey, (old) => + old?.map((issue) => + issue.id === id ? { ...issue, isUnreadForMe: false } : issue, + ), + ); + }, onSuccess: () => { if (selectedCompanyId) { queryClient.invalidateQueries({ queryKey: queryKeys.issues.listTouchedByMe(selectedCompanyId) }); From fb6dd2a9ce9a6f05452ab500b05e19a43ba53c29 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 05:29:43 +0700 Subject: [PATCH 024/227] fix(heartbeat): wake parent agent when child issue completes (#1280) When a child subtask transitions to done or cancelled, the parent issue's assigned agent now receives a wakeup with reason "child_issue_completed". This enables manager agents to react immediately to subtask completions instead of waiting for the next timer-based heartbeat interval. Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- server/src/routes/issues.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index cf8d170ae1..7c0dce54bd 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -974,6 +974,30 @@ export function issueRoutes(db: Db, storage: StorageService) { } } + // Wake parent's agent when a child issue transitions to done/cancelled + const statusBecameTerminal = + existing.status !== "done" && + existing.status !== "cancelled" && + (issue.status === "done" || issue.status === "cancelled"); + if (statusBecameTerminal && issue.parentId) { + try { + const parent = await svc.getById(issue.parentId); + if (parent?.assigneeAgentId && !wakeups.has(parent.assigneeAgentId)) { + wakeups.set(parent.assigneeAgentId, { + source: "automation", + triggerDetail: "system", + reason: "child_issue_completed", + payload: { issueId: issue.id, parentId: parent.id }, + requestedByActorType: actor.actorType, + requestedByActorId: actor.actorId, + contextSnapshot: { issueId: parent.id, taskId: parent.id, source: "issue.child_completed" }, + }); + } + } catch (err) { + logger.warn({ err, issueId: issue.id, parentId: issue.parentId }, "failed to look up parent for child-completion wake"); + } + } + for (const [agentId, wakeup] of wakeups.entries()) { heartbeat .wakeup(agentId, wakeup) From a7935ba780f9a3911284a4696a3bc06ae25a3a2c Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 06:35:37 +0700 Subject: [PATCH 025/227] fix(auth): prevent privilege escalation with invalid bearer tokens in local_trusted mode When a bearer token is present but fails validation (expired JWT, invalid key), the default local_trusted board actor was not cleared, allowing unauthenticated requests to inherit full board-level access. This enabled agents with expired auth to perform destructive operations like deleting other agents. Now clears the actor to { type: "none" } when a bearer token is present before validation, so only successful verification restores the proper actor type. Fixes #1314 Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- .../auth-privilege-escalation.test.ts | 71 +++++++++++++++++++ server/src/middleware/auth.ts | 8 +++ 2 files changed, 79 insertions(+) create mode 100644 server/src/__tests__/auth-privilege-escalation.test.ts diff --git a/server/src/__tests__/auth-privilege-escalation.test.ts b/server/src/__tests__/auth-privilege-escalation.test.ts new file mode 100644 index 0000000000..3cdf45caca --- /dev/null +++ b/server/src/__tests__/auth-privilege-escalation.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest"; +import express from "express"; +import request from "supertest"; +import { actorMiddleware } from "../middleware/auth.js"; +import type { Db } from "@paperclipai/db"; + +/** + * Regression test for GH #1314: in local_trusted mode, an invalid bearer token + * must NOT inherit the default board actor. Without the fix, expired/invalid agent + * JWTs would fall through to the local_trusted board default, granting full + * board-level access (including agent deletion). + */ +describe("auth privilege escalation prevention", () => { + function createApp(deploymentMode: "local_trusted" | "authenticated") { + const app = express(); + // Stub DB: all queries return empty results + const fakeDb = { + select: () => ({ + from: () => ({ + where: () => ({ + then: (cb: (rows: unknown[]) => unknown) => Promise.resolve(cb([])), + }), + }), + }), + update: () => ({ + set: () => ({ + where: () => Promise.resolve(), + }), + }), + } as unknown as Db; + + app.use(actorMiddleware(fakeDb, { deploymentMode })); + app.get("/test", (req, res) => { + res.json({ actorType: req.actor.type, source: req.actor.source }); + }); + return app; + } + + it("local_trusted without bearer token keeps board actor", async () => { + const app = createApp("local_trusted"); + const res = await request(app).get("/test"); + expect(res.body.actorType).toBe("board"); + expect(res.body.source).toBe("local_implicit"); + }); + + it("local_trusted with invalid bearer token clears board actor to none", async () => { + const app = createApp("local_trusted"); + const res = await request(app) + .get("/test") + .set("Authorization", "Bearer invalid-or-expired-token"); + expect(res.body.actorType).toBe("none"); + expect(res.body.source).toBe("none"); + }); + + it("local_trusted with empty bearer token keeps board actor", async () => { + const app = createApp("local_trusted"); + const res = await request(app) + .get("/test") + .set("Authorization", "Bearer "); + expect(res.body.actorType).toBe("board"); + expect(res.body.source).toBe("local_implicit"); + }); + + it("authenticated mode with invalid bearer token stays none", async () => { + const app = createApp("authenticated"); + const res = await request(app) + .get("/test") + .set("Authorization", "Bearer invalid-token"); + expect(res.body.actorType).toBe("none"); + }); +}); diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts index 41d3a76aea..60e2863c69 100644 --- a/server/src/middleware/auth.ts +++ b/server/src/middleware/auth.ts @@ -80,6 +80,14 @@ export function actorMiddleware(db: Db, opts: ActorMiddlewareOptions): RequestHa return; } + // When a bearer token is present, clear the default actor to prevent + // privilege escalation in local_trusted mode. If the token is invalid, + // the request should be unauthenticated, not board-level. + const savedRunId = req.actor.runId; + req.actor = { type: "none", source: "none" }; + if (savedRunId) req.actor.runId = savedRunId; + if (runIdHeader) req.actor.runId = runIdHeader; + const tokenHash = hashToken(token); const key = await db .select() From 7133e642426f10037de858c4973a32dcc765be73 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 07:40:53 +0700 Subject: [PATCH 026/227] fix(adapters): fall back to directory junctions on Windows when symlink fails with EPERM (#1320) On Windows, creating symlinks requires elevated privileges or Developer Mode enabled. All adapter skill injection code used raw fs.symlink() without any fallback, causing EPERM crashes during heartbeat skill setup. - Add robustLink() helper in adapter-utils that tries symlink first, falls back to junction type on Windows EPERM - Use robustLink as default linker in ensurePaperclipSkillSymlink - Update claude-local, codex-local, cursor-local, opencode-local to use robustLink instead of raw fs.symlink for skill directories - Add symlinkOrCopy fallback in codex-home for file symlinks (auth.json) - Add 4 tests covering robustLink and ensurePaperclipSkillSymlink Co-Authored-By: Paperclip --- packages/adapter-utils/src/server-utils.ts | 25 +++++- .../claude-local/src/server/execute.ts | 3 +- .../codex-local/src/server/codex-home.ts | 21 ++++- .../codex-local/src/server/execute.ts | 3 +- .../cursor-local/src/server/execute.ts | 3 +- .../opencode-local/src/server/execute.ts | 3 +- .../__tests__/paperclip-skill-utils.test.ts | 88 +++++++++++++++++++ 7 files changed, 138 insertions(+), 8 deletions(-) diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index 52e52b4c54..2bfb682e8b 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -330,11 +330,32 @@ export async function readPaperclipSkillMarkdown( } } +/** + * Creates a symlink, falling back to a directory junction on Windows when the + * caller lacks the SeCreateSymbolicLinkPrivilege (EPERM). Junctions don't + * require elevated privileges and work for directory targets on NTFS. + */ +export async function robustLink(source: string, target: string): Promise { + try { + await fs.symlink(source, target); + } catch (err: unknown) { + if ( + process.platform === "win32" && + err instanceof Error && + "code" in err && + (err as NodeJS.ErrnoException).code === "EPERM" + ) { + await fs.symlink(source, target, "junction"); + } else { + throw err; + } + } +} + export async function ensurePaperclipSkillSymlink( source: string, target: string, - linkSkill: (source: string, target: string) => Promise = (linkSource, linkTarget) => - fs.symlink(linkSource, linkTarget), + linkSkill: (source: string, target: string) => Promise = robustLink, ): Promise<"created" | "repaired" | "skipped"> { const existing = await fs.lstat(target).catch(() => null); if (!existing) { diff --git a/packages/adapters/claude-local/src/server/execute.ts b/packages/adapters/claude-local/src/server/execute.ts index cd1f0f1560..b4f66b9b98 100644 --- a/packages/adapters/claude-local/src/server/execute.ts +++ b/packages/adapters/claude-local/src/server/execute.ts @@ -19,6 +19,7 @@ import { ensurePathInEnv, renderTemplate, runChildProcess, + robustLink, } from "@paperclipai/adapter-utils/server-utils"; import { parseClaudeStreamJson, @@ -56,7 +57,7 @@ async function buildSkillsDir(): Promise { const entries = await fs.readdir(skillsDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { - await fs.symlink( + await robustLink( path.join(skillsDir, entry.name), path.join(target, entry.name), ); diff --git a/packages/adapters/codex-local/src/server/codex-home.ts b/packages/adapters/codex-local/src/server/codex-home.ts index 08c851e705..e06b699ed2 100644 --- a/packages/adapters/codex-local/src/server/codex-home.ts +++ b/packages/adapters/codex-local/src/server/codex-home.ts @@ -40,11 +40,28 @@ async function ensureParentDir(target: string): Promise { await fs.mkdir(path.dirname(target), { recursive: true }); } +async function symlinkOrCopy(source: string, target: string): Promise { + try { + await fs.symlink(source, target); + } catch (err: unknown) { + if ( + process.platform === "win32" && + err instanceof Error && + "code" in err && + (err as NodeJS.ErrnoException).code === "EPERM" + ) { + await fs.copyFile(source, target); + } else { + throw err; + } + } +} + async function ensureSymlink(target: string, source: string): Promise { const existing = await fs.lstat(target).catch(() => null); if (!existing) { await ensureParentDir(target); - await fs.symlink(source, target); + await symlinkOrCopy(source, target); return; } @@ -59,7 +76,7 @@ async function ensureSymlink(target: string, source: string): Promise { if (resolvedLinkedPath === source) return; await fs.unlink(target); - await fs.symlink(source, target); + await symlinkOrCopy(source, target); } async function ensureCopiedFile(target: string, source: string): Promise { diff --git a/packages/adapters/codex-local/src/server/execute.ts b/packages/adapters/codex-local/src/server/execute.ts index 5486301095..d42cc5c651 100644 --- a/packages/adapters/codex-local/src/server/execute.ts +++ b/packages/adapters/codex-local/src/server/execute.ts @@ -16,6 +16,7 @@ import { ensurePathInEnv, listPaperclipSkillEntries, removeMaintainerOnlySkillSymlinks, + robustLink, renderTemplate, joinPromptSections, runChildProcess, @@ -140,7 +141,7 @@ export async function ensureCodexSkillsInjected( if (linkSkill) { await linkSkill(entry.source, target); } else { - await fs.symlink(entry.source, target); + await robustLink(entry.source, target); } await onLog( "stdout", diff --git a/packages/adapters/cursor-local/src/server/execute.ts b/packages/adapters/cursor-local/src/server/execute.ts index 088a9057d3..4e4a6b4185 100644 --- a/packages/adapters/cursor-local/src/server/execute.ts +++ b/packages/adapters/cursor-local/src/server/execute.ts @@ -16,6 +16,7 @@ import { ensurePathInEnv, listPaperclipSkillEntries, removeMaintainerOnlySkillSymlinks, + robustLink, renderTemplate, joinPromptSections, runChildProcess, @@ -131,7 +132,7 @@ export async function ensureCursorSkillsInjected( `[paperclip] Removed maintainer-only Cursor skill "${skillName}" from ${skillsHome}\n`, ); } - const linkSkill = options.linkSkill ?? ((source: string, target: string) => fs.symlink(source, target)); + const linkSkill = options.linkSkill ?? robustLink; for (const entry of skillsEntries) { const target = path.join(skillsHome, entry.name); try { diff --git a/packages/adapters/opencode-local/src/server/execute.ts b/packages/adapters/opencode-local/src/server/execute.ts index 3bfb37df7c..0a11186353 100644 --- a/packages/adapters/opencode-local/src/server/execute.ts +++ b/packages/adapters/opencode-local/src/server/execute.ts @@ -16,6 +16,7 @@ import { ensurePathInEnv, renderTemplate, runChildProcess, + robustLink, } from "@paperclipai/adapter-utils/server-utils"; import { isOpenCodeUnknownSessionError, parseOpenCodeJsonl } from "./parse.js"; import { ensureOpenCodeModelConfiguredAndAvailable } from "./models.js"; @@ -73,7 +74,7 @@ async function ensureOpenCodeSkillsInjected(onLog: AdapterExecutionContext["onLo if (existing) continue; try { - await fs.symlink(source, target); + await robustLink(source, target); await onLog( "stderr", `[paperclip] Injected OpenCode skill "${entry.name}" into ${skillsHome}\n`, diff --git a/server/src/__tests__/paperclip-skill-utils.test.ts b/server/src/__tests__/paperclip-skill-utils.test.ts index 4344dc175e..559c018362 100644 --- a/server/src/__tests__/paperclip-skill-utils.test.ts +++ b/server/src/__tests__/paperclip-skill-utils.test.ts @@ -5,6 +5,8 @@ import { afterEach, describe, expect, it } from "vitest"; import { listPaperclipSkillEntries, removeMaintainerOnlySkillSymlinks, + robustLink, + ensurePaperclipSkillSymlink, } from "@paperclipai/adapter-utils/server-utils"; async function makeTempDir(prefix: string): Promise { @@ -58,4 +60,90 @@ describe("paperclip skill utils", () => { expect((await fs.lstat(path.join(skillsHome, "paperclip"))).isSymbolicLink()).toBe(true); expect((await fs.lstat(path.join(skillsHome, "release-notes"))).isSymbolicLink()).toBe(true); }); + + describe("robustLink", () => { + it("creates a symlink for a directory", async () => { + const root = await makeTempDir("robust-link-"); + cleanupDirs.add(root); + + const source = path.join(root, "source-dir"); + const target = path.join(root, "link"); + await fs.mkdir(source, { recursive: true }); + await fs.writeFile(path.join(source, "file.txt"), "hello"); + + await robustLink(source, target); + + const stat = await fs.lstat(target); + expect(stat.isSymbolicLink()).toBe(true); + const content = await fs.readFile(path.join(target, "file.txt"), "utf8"); + expect(content).toBe("hello"); + }); + + it("propagates non-EPERM errors", async () => { + const root = await makeTempDir("robust-link-err-"); + cleanupDirs.add(root); + + await expect( + robustLink(path.join(root, "nonexistent"), path.join(root, "sub", "deep", "link")), + ).rejects.toThrow(); + }); + }); + + describe("ensurePaperclipSkillSymlink", () => { + it("creates a new symlink when target does not exist", async () => { + const root = await makeTempDir("skill-symlink-create-"); + cleanupDirs.add(root); + + const source = path.join(root, "skill"); + const target = path.join(root, "link"); + await fs.mkdir(source, { recursive: true }); + + const result = await ensurePaperclipSkillSymlink(source, target); + expect(result).toBe("created"); + expect((await fs.lstat(target)).isSymbolicLink()).toBe(true); + }); + + it("repairs a broken symlink pointing to a stale location", async () => { + const root = await makeTempDir("skill-symlink-repair-"); + cleanupDirs.add(root); + + const source = path.join(root, "skill"); + const stale = path.join(root, "stale"); + const target = path.join(root, "link"); + await fs.mkdir(source, { recursive: true }); + await fs.symlink(stale, target); + + const result = await ensurePaperclipSkillSymlink(source, target); + expect(result).toBe("repaired"); + const linkedPath = await fs.readlink(target); + expect(path.resolve(path.dirname(target), linkedPath)).toBe(source); + }); + + it("skips when target already points to source", async () => { + const root = await makeTempDir("skill-symlink-skip-"); + cleanupDirs.add(root); + + const source = path.join(root, "skill"); + const target = path.join(root, "link"); + await fs.mkdir(source, { recursive: true }); + await fs.symlink(source, target); + + const result = await ensurePaperclipSkillSymlink(source, target); + expect(result).toBe("skipped"); + }); + + it("uses robustLink as default linker", async () => { + const root = await makeTempDir("skill-symlink-robust-"); + cleanupDirs.add(root); + + const source = path.join(root, "skill"); + const target = path.join(root, "link"); + await fs.mkdir(source, { recursive: true }); + + // default linkSkill should be robustLink + const result = await ensurePaperclipSkillSymlink(source, target); + expect(result).toBe("created"); + expect((await fs.lstat(target)).isSymbolicLink()).toBe(true); + }); + }); }); From 1cc0c91c36e674101176f4b20cfbea1b3f779065 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 08:41:56 +0700 Subject: [PATCH 027/227] fix(server): clear execution lock fields in issue release endpoint (#1245) The release() method only cleared assigneeAgentId and checkoutRunId but left executionRunId, executionAgentNameKey, and executionLockedAt intact. This caused permanent stale locks after unclean agent session ends, making subsequent checkout attempts return 409 Conflict with no recovery path other than direct SQL. Now release() nulls all three execution lock fields, matching the behavior of releaseIssueExecutionAndPromote() in the heartbeat service. Closes #1245 Co-Authored-By: Paperclip --- server/src/services/issues.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 1f1a49619f..35406d65fe 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -1126,6 +1126,9 @@ export function issueService(db: Db) { status: "todo", assigneeAgentId: null, checkoutRunId: null, + executionRunId: null, + executionAgentNameKey: null, + executionLockedAt: null, updatedAt: new Date(), }) .where(eq(issues.id, id)) From e66bb2d3fa1941c3619985febb024583ba5e81b8 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 08:44:06 +0700 Subject: [PATCH 028/227] fix(server): clear execution lock on assignee change to prevent orphaned deferred wakes (#1269) When an agent reassigns an issue via PATCH, the update cleared checkoutRunId but left executionRunId/executionAgentNameKey/ executionLockedAt intact. This caused the new assignee's wake to be deferred (waiting for the old execution to release), but releaseIssueExecutionAndPromote could not find the issue if the execution context had shifted, leaving the deferred wake permanently orphaned. Now, changing the assignee also clears all three execution lock fields, so the new assignee's wake goes through immediately without hitting the deferred path. Closes #1269 Co-Authored-By: Paperclip --- server/src/services/issues.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 1f1a49619f..4bb88c7984 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -852,6 +852,9 @@ export function issueService(db: Db) { (issueData.assigneeUserId !== undefined && issueData.assigneeUserId !== existing.assigneeUserId) ) { patch.checkoutRunId = null; + patch.executionRunId = null; + patch.executionAgentNameKey = null; + patch.executionLockedAt = null; } return db.transaction(async (tx) => { From d8b9d7b0027105fd3822ffb01ef1827c252222a2 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 08:46:16 +0700 Subject: [PATCH 029/227] fix(ui): guard MarkdownEditor against null value to prevent setMarkdown crash (#1227) MDXEditor's setMarkdown() crashes with TypeError when receiving null (null.trim()). This happened when AgentConfigForm stored null for empty markdown fields (capabilities, bootstrapPromptTemplate) via the `v || null` pattern. Add `?? ""` guards in MarkdownEditor at the setMarkdown() call site and the initial markdown prop, so null values are safely coerced to empty strings. Closes #1227 Co-Authored-By: Paperclip --- ui/src/components/MarkdownEditor.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ui/src/components/MarkdownEditor.tsx b/ui/src/components/MarkdownEditor.tsx index 29a57a3a0e..ae334d4271 100644 --- a/ui/src/components/MarkdownEditor.tsx +++ b/ui/src/components/MarkdownEditor.tsx @@ -303,9 +303,10 @@ export const MarkdownEditor = forwardRef }, [hasImageUpload]); useEffect(() => { - if (value !== latestValueRef.current) { - ref.current?.setMarkdown(value); - latestValueRef.current = value; + const safeValue = value ?? ""; + if (safeValue !== latestValueRef.current) { + ref.current?.setMarkdown(safeValue); + latestValueRef.current = safeValue; } }, [value]); @@ -576,7 +577,7 @@ export const MarkdownEditor = forwardRef > { latestValueRef.current = next; From b8d04568c88e09a08fc21ff69f3f34a5906130fa Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 09:43:53 +0700 Subject: [PATCH 030/227] fix(plugins): convert manifest path to file URL for Windows ESM compatibility (#1263) On Windows, Node.js ESM loader requires file:// URLs for dynamic imports. Raw absolute paths like C:\Users\... fail with ERR_UNSUPPORTED_ESM_URL_SCHEME. Use pathToFileURL() before calling import(), matching the pattern already used in cli/src/commands/run.ts. Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- server/src/services/plugin-loader.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/services/plugin-loader.ts b/server/src/services/plugin-loader.ts index 1ceadd1917..22dd0f603a 100644 --- a/server/src/services/plugin-loader.ts +++ b/server/src/services/plugin-loader.ts @@ -29,7 +29,7 @@ import { readdir, readFile, rm, stat } from "node:fs/promises"; import { execFile } from "node:child_process"; import os from "node:os"; import path from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { promisify } from "node:util"; import type { Db } from "@paperclipai/db"; import type { @@ -926,8 +926,9 @@ export function pluginLoader( let raw: unknown; try { - // Dynamic import works for both .js (ESM) and .cjs (CJS) manifests - const mod = await import(manifestPath) as Record; + // Dynamic import works for both .js (ESM) and .cjs (CJS) manifests. + // On Windows, absolute paths must be converted to file:// URLs for ESM. + const mod = await import(pathToFileURL(manifestPath).href) as Record; // The manifest may be the default export or the module itself raw = mod["default"] ?? mod; } catch (err) { From cacee636f797c878a1c7426488783ff6134ffaec Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 09:49:17 +0700 Subject: [PATCH 031/227] fix(server): convert MSYS/Git Bash drive paths to native Windows format (#1270) On Windows with Git Bash, paths like /c/Users/... get passed to the server. Node.js path.isAbsolute() treats these as absolute, but path.resolve() converts them incorrectly to C:\c\Users\... instead of C:\Users\... Add normalizeMsysDrivePath() helper that converts /x/... to X:\... on Windows. Applied in resolveInstructionsFilePath (agents route) and readAgentInstructions (company portability). Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- server/src/routes/agents.ts | 17 ++++++++++++++--- server/src/services/company-portability.ts | 13 ++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index db91ad8c07..22c77d9592 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -288,16 +288,27 @@ export function agentRoutes(db: Db) { } } + /** + * Convert MSYS/Git Bash drive paths (/c/Users/...) to native Windows paths (C:\Users\...). + * On non-Windows or non-matching paths, returns the input unchanged. + */ + function normalizeMsysDrivePath(p: string): string { + if (process.platform !== "win32") return p; + const m = p.match(/^\/([a-zA-Z])\/(.*)/); + return m ? `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}` : p; + } + function resolveInstructionsFilePath(candidatePath: string, adapterConfig: Record) { - const trimmed = candidatePath.trim(); + const trimmed = normalizeMsysDrivePath(candidatePath.trim()); if (path.isAbsolute(trimmed)) return trimmed; - const cwd = asNonEmptyString(adapterConfig.cwd); - if (!cwd) { + const rawCwd = asNonEmptyString(adapterConfig.cwd); + if (!rawCwd) { throw unprocessable( "Relative instructions path requires adapterConfig.cwd to be set to an absolute path", ); } + const cwd = normalizeMsysDrivePath(rawCwd); if (!path.isAbsolute(cwd)) { throw unprocessable("adapterConfig.cwd must be an absolute path to resolve relative instructions path"); } diff --git a/server/src/services/company-portability.ts b/server/src/services/company-portability.ts index f067e95772..6207775f42 100644 --- a/server/src/services/company-portability.ts +++ b/server/src/services/company-portability.ts @@ -433,9 +433,20 @@ function resolveRawGitHubUrl(owner: string, repo: string, ref: string, filePath: return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${normalizedFilePath}`; } +/** + * Convert MSYS/Git Bash drive paths (/c/Users/...) to native Windows paths (C:\Users\...). + * On non-Windows or non-matching paths, returns the input unchanged. + */ +function normalizeMsysDrivePath(p: string): string { + if (process.platform !== "win32") return p; + const m = p.match(/^\/([a-zA-Z])\/(.*)/); + return m ? `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}` : p; +} + async function readAgentInstructions(agent: AgentLike): Promise<{ body: string; warning: string | null }> { const config = agent.adapterConfig as Record; - const instructionsFilePath = asString(config.instructionsFilePath); + const rawPath = asString(config.instructionsFilePath); + const instructionsFilePath = rawPath ? normalizeMsysDrivePath(rawPath) : rawPath; if (instructionsFilePath) { const workspaceCwd = asString(process.env.PAPERCLIP_WORKSPACE_CWD); const candidates = new Set(); From b945406518949a87acedd039ae2d755c67a8fe72 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 11:55:14 +0700 Subject: [PATCH 032/227] refactor(server): extract normalizeMsysDrivePath to shared paths module Deduplicate the MSYS drive path helper that was copied identically in agents.ts and company-portability.ts. Now exported from paths.ts with unit tests covering the non-Windows passthrough behavior. Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- server/src/__tests__/paths.test.ts | 25 ++++++++++++++++++++++ server/src/paths.ts | 10 +++++++++ server/src/routes/agents.ts | 11 +--------- server/src/services/company-portability.ts | 11 +--------- 4 files changed, 37 insertions(+), 20 deletions(-) create mode 100644 server/src/__tests__/paths.test.ts diff --git a/server/src/__tests__/paths.test.ts b/server/src/__tests__/paths.test.ts new file mode 100644 index 0000000000..774474a53e --- /dev/null +++ b/server/src/__tests__/paths.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; +import { normalizeMsysDrivePath } from "../paths.js"; + +describe("normalizeMsysDrivePath", () => { + it("returns input unchanged on non-Windows platforms", () => { + // On macOS/Linux (where tests run), the function is a no-op + expect(normalizeMsysDrivePath("/c/Users/foo")).toBe("/c/Users/foo"); + expect(normalizeMsysDrivePath("/d/projects/bar")).toBe("/d/projects/bar"); + expect(normalizeMsysDrivePath("C:\\Users\\foo")).toBe("C:\\Users\\foo"); + }); + + it("returns empty string unchanged", () => { + expect(normalizeMsysDrivePath("")).toBe(""); + }); + + it("returns normal paths unchanged", () => { + expect(normalizeMsysDrivePath("/usr/local/bin")).toBe("/usr/local/bin"); + expect(normalizeMsysDrivePath("/home/user/project")).toBe("/home/user/project"); + }); + + it("returns relative paths unchanged", () => { + expect(normalizeMsysDrivePath("foo/bar")).toBe("foo/bar"); + expect(normalizeMsysDrivePath("./relative")).toBe("./relative"); + }); +}); diff --git a/server/src/paths.ts b/server/src/paths.ts index 21856a6946..9d47c129ef 100644 --- a/server/src/paths.ts +++ b/server/src/paths.ts @@ -32,3 +32,13 @@ export function resolvePaperclipConfigPath(overridePath?: string): string { export function resolvePaperclipEnvPath(overrideConfigPath?: string): string { return path.resolve(path.dirname(resolvePaperclipConfigPath(overrideConfigPath)), PAPERCLIP_ENV_FILENAME); } + +/** + * Convert MSYS/Git Bash drive paths (/c/Users/...) to native Windows paths (C:\Users\...). + * On non-Windows or non-matching paths, returns the input unchanged. + */ +export function normalizeMsysDrivePath(p: string): string { + if (process.platform !== "win32") return p; + const m = p.match(/^\/([a-zA-Z])\/(.*)/); + return m ? `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}` : p; +} diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 22c77d9592..abcfcd64e3 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -36,6 +36,7 @@ import { assertBoard, assertCompanyAccess, getActorInfo } from "./authz.js"; import { findServerAdapter, listAdapterModels } from "../adapters/index.js"; import { redactEventPayload } from "../redaction.js"; import { redactCurrentUserValue } from "../log-redaction.js"; +import { normalizeMsysDrivePath } from "../paths.js"; import { runClaudeLogin } from "@paperclipai/adapter-claude-local/server"; import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX, @@ -288,16 +289,6 @@ export function agentRoutes(db: Db) { } } - /** - * Convert MSYS/Git Bash drive paths (/c/Users/...) to native Windows paths (C:\Users\...). - * On non-Windows or non-matching paths, returns the input unchanged. - */ - function normalizeMsysDrivePath(p: string): string { - if (process.platform !== "win32") return p; - const m = p.match(/^\/([a-zA-Z])\/(.*)/); - return m ? `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}` : p; - } - function resolveInstructionsFilePath(candidatePath: string, adapterConfig: Record) { const trimmed = normalizeMsysDrivePath(candidatePath.trim()); if (path.isAbsolute(trimmed)) return trimmed; diff --git a/server/src/services/company-portability.ts b/server/src/services/company-portability.ts index 6207775f42..97042ef483 100644 --- a/server/src/services/company-portability.ts +++ b/server/src/services/company-portability.ts @@ -16,6 +16,7 @@ import type { } from "@paperclipai/shared"; import { normalizeAgentUrlKey, portabilityManifestSchema } from "@paperclipai/shared"; import { notFound, unprocessable } from "../errors.js"; +import { normalizeMsysDrivePath } from "../paths.js"; import { accessService } from "./access.js"; import { agentService } from "./agents.js"; import { companyService } from "./companies.js"; @@ -433,16 +434,6 @@ function resolveRawGitHubUrl(owner: string, repo: string, ref: string, filePath: return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${normalizedFilePath}`; } -/** - * Convert MSYS/Git Bash drive paths (/c/Users/...) to native Windows paths (C:\Users\...). - * On non-Windows or non-matching paths, returns the input unchanged. - */ -function normalizeMsysDrivePath(p: string): string { - if (process.platform !== "win32") return p; - const m = p.match(/^\/([a-zA-Z])\/(.*)/); - return m ? `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}` : p; -} - async function readAgentInstructions(agent: AgentLike): Promise<{ body: string; warning: string | null }> { const config = agent.adapterConfig as Record; const rawPath = asString(config.instructionsFilePath); From c17bd5a02ffd019704a1625a99e337ee52a8e26f Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 12:44:45 +0700 Subject: [PATCH 033/227] fix(server): add CEO deletion protection and self-deletion prevention guards Prevent CEO agent from being terminated or deleted via API endpoints, which would brick the entire system. Also add defense-in-depth guard preventing any agent from terminating or deleting itself. Guards added at two layers: - Route level: pre-check agent role before calling service - Service level: reject CEO role and self-targeting in terminate/remove Closes #1334 Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- .../src/__tests__/ceo-deletion-guard.test.ts | 129 ++++++++++++++++++ server/src/routes/agents.ts | 16 ++- server/src/services/agents.ts | 32 ++++- 3 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 server/src/__tests__/ceo-deletion-guard.test.ts diff --git a/server/src/__tests__/ceo-deletion-guard.test.ts b/server/src/__tests__/ceo-deletion-guard.test.ts new file mode 100644 index 0000000000..d54fc0f238 --- /dev/null +++ b/server/src/__tests__/ceo-deletion-guard.test.ts @@ -0,0 +1,129 @@ +import { describe, expect, it, vi } from "vitest"; +import { HttpError } from "../errors.js"; + +/** + * CEO deletion protection and self-deletion prevention guards. + * Tests agentService.terminate() and agentService.remove(). + */ + +function createMockDb(agentRows: Record[]) { + const selectChain = { + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + groupBy: vi.fn().mockReturnValue({ + then: vi.fn().mockImplementation((cb: (r: unknown[]) => unknown) => + Promise.resolve(cb(agentRows)), + ), + }), + then: vi.fn().mockImplementation((cb: (r: unknown[]) => unknown) => + Promise.resolve(cb(agentRows)), + ), + }), + }), + }; + return { + select: vi.fn().mockReturnValue(selectChain), + update: vi.fn().mockReturnValue({ + set: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + returning: vi.fn().mockReturnValue({ + then: vi.fn().mockResolvedValue([]), + }), + }), + }), + }), + delete: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + returning: vi.fn().mockReturnValue({ + then: vi.fn().mockResolvedValue([]), + }), + }), + }), + transaction: vi.fn(), + } as any; +} + +function makeAgent(overrides: Record = {}) { + return { + id: "agent-1", companyId: "c1", name: "Test", role: "engineer", + title: null, icon: null, status: "running", reportsTo: null, + capabilities: null, adapterType: "claude_local", adapterConfig: {}, + runtimeConfig: {}, budgetMonthlyCents: 0, spentMonthlyCents: 0, + pauseReason: null, pausedAt: null, permissions: { canCreateAgents: false }, + lastHeartbeatAt: null, metadata: null, createdAt: new Date(), + updatedAt: new Date(), urlKey: "test", ...overrides, + }; +} + +vi.mock("drizzle-orm", () => ({ + eq: vi.fn(), and: vi.fn(), desc: vi.fn(), gte: vi.fn(), + inArray: vi.fn(), lt: vi.fn(), ne: vi.fn(), + sql: Object.assign(vi.fn(), { raw: vi.fn(), join: vi.fn() }), +})); + +vi.mock("@paperclipai/db", () => ({ + agents: { id: "a.id", companyId: "a.cid", reportsTo: "a.rt" }, + agentConfigRevisions: { agentId: "x" }, agentApiKeys: { agentId: "x" }, + agentRuntimeState: { agentId: "x" }, agentTaskSessions: { agentId: "x" }, + agentWakeupRequests: { agentId: "x" }, costEvents: { agentId: "x" }, + heartbeatRunEvents: { agentId: "x" }, heartbeatRuns: { agentId: "x" }, +})); + +vi.mock("@paperclipai/shared", () => ({ + isUuidLike: vi.fn(() => true), + normalizeAgentUrlKey: vi.fn((s: string) => s), +})); + +vi.mock("../redaction.js", () => ({ + REDACTED_EVENT_VALUE: "[REDACTED]", + sanitizeRecord: vi.fn((r: unknown) => r), +})); + +vi.mock("./agent-permissions.js", () => ({ + normalizeAgentPermissions: vi.fn((p: unknown) => p ?? { canCreateAgents: false }), +})); + +const { agentService } = await import("../services/agents.js"); + +describe("CEO deletion protection", () => { + describe("terminate", () => { + it("throws when target is CEO", async () => { + const svc = agentService(createMockDb([makeAgent({ id: "ceo", role: "ceo" })])); + await expect(svc.terminate("ceo")).rejects.toThrow(HttpError); + await expect(svc.terminate("ceo")).rejects.toThrow(/Cannot terminate the CEO/); + }); + + it("throws on self-termination", async () => { + const svc = agentService(createMockDb([makeAgent({ id: "a1" })])); + await expect(svc.terminate("a1", "a1")).rejects.toThrow(/cannot terminate itself/); + }); + + it("returns null for missing agent", async () => { + const svc = agentService(createMockDb([])); + expect(await svc.terminate("x")).toBeNull(); + }); + + it("allows non-CEO by different agent", async () => { + const svc = agentService(createMockDb([makeAgent({ id: "a1" })])); + await expect(svc.terminate("a1", "a2")).resolves.not.toThrow(); + }); + }); + + describe("remove", () => { + it("throws when target is CEO", async () => { + const svc = agentService(createMockDb([makeAgent({ id: "ceo", role: "ceo" })])); + await expect(svc.remove("ceo")).rejects.toThrow(HttpError); + await expect(svc.remove("ceo")).rejects.toThrow(/Cannot delete the CEO/); + }); + + it("throws on self-deletion", async () => { + const svc = agentService(createMockDb([makeAgent({ id: "a1" })])); + await expect(svc.remove("a1", "a1")).rejects.toThrow(/cannot delete itself/); + }); + + it("returns null for missing agent", async () => { + const svc = agentService(createMockDb([])); + expect(await svc.remove("x")).toBeNull(); + }); + }); +}); diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index db91ad8c07..20da8d609c 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -1215,11 +1215,15 @@ export function agentRoutes(db: Db) { router.post("/agents/:id/terminate", async (req, res) => { assertBoard(req); const id = req.params.id as string; - const agent = await svc.terminate(id); - if (!agent) { + const target = await svc.getById(id); + if (!target) { res.status(404).json({ error: "Agent not found" }); return; } + if (target.role === "ceo") { + throw forbidden("Cannot terminate the CEO agent"); + } + const agent = (await svc.terminate(id))!; await heartbeat.cancelActiveForAgent(id); @@ -1238,11 +1242,15 @@ export function agentRoutes(db: Db) { router.delete("/agents/:id", async (req, res) => { assertBoard(req); const id = req.params.id as string; - const agent = await svc.remove(id); - if (!agent) { + const target = await svc.getById(id); + if (!target) { res.status(404).json({ error: "Agent not found" }); return; } + if (target.role === "ceo") { + throw forbidden("Cannot delete the CEO agent"); + } + const agent = (await svc.remove(id))!; await logActivity(db, { companyId: agent.companyId, diff --git a/server/src/services/agents.ts b/server/src/services/agents.ts index 17d2e46d7e..1b1d606f63 100644 --- a/server/src/services/agents.ts +++ b/server/src/services/agents.ts @@ -446,9 +446,15 @@ export function agentService(db: Db) { return updated ? normalizeAgentRow(updated) : null; }, - terminate: async (id: string) => { + terminate: async (id: string, actorAgentId?: string) => { const existing = await getById(id); if (!existing) return null; + if (existing.role === "ceo") { + throw unprocessable("Cannot terminate the CEO agent"); + } + if (actorAgentId && actorAgentId === id) { + throw unprocessable("An agent cannot terminate itself"); + } await db .update(agents) @@ -468,9 +474,15 @@ export function agentService(db: Db) { return getById(id); }, - remove: async (id: string) => { + remove: async (id: string, actorAgentId?: string) => { const existing = await getById(id); if (!existing) return null; + if (existing.role === "ceo") { + throw unprocessable("Cannot delete the CEO agent"); + } + if (actorAgentId && actorAgentId === id) { + throw unprocessable("An agent cannot delete itself"); + } return db.transaction(async (tx) => { await tx.update(agents).set({ reportsTo: null }).where(eq(agents.reportsTo, id)); @@ -504,14 +516,26 @@ export function agentService(db: Db) { return updated ? normalizeAgentRow(updated) : null; }, - updatePermissions: async (id: string, permissions: { canCreateAgents: boolean }) => { + updatePermissions: async ( + id: string, + permissions: Partial<{ + canCreateAgents: boolean; + canDeleteAgents: boolean; + canTerminateAgents: boolean; + }>, + ) => { const existing = await getById(id); if (!existing) return null; + const merged = { + ...(existing.permissions ?? {}), + ...permissions, + }; + const updated = await db .update(agents) .set({ - permissions: normalizeAgentPermissions(permissions, existing.role), + permissions: normalizeAgentPermissions(merged, existing.role), updatedAt: new Date(), }) .where(eq(agents.id, id)) From f5d97fd712f84e99a5409730c2f995ce6e7fd7e3 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 12:47:44 +0700 Subject: [PATCH 034/227] fix(server): add canDeleteAgents/canTerminateAgents permission fields Agent-initiated delete/terminate now requires explicit permission (canDeleteAgents/canTerminateAgents) or access grant (agents:delete, agents:terminate). Board users retain unrestricted access. - Add canDeleteAgents, canTerminateAgents to permission types, schema, and normalization (default false for all roles) - Replace assertBoard with assertCanDeleteAgent/assertCanTerminateAgent guards on DELETE /agents/:id and POST /agents/:id/terminate - Pass actorAgentId to service methods for self-deletion prevention - Log correct actorType (agent vs user) via getActorInfo - Add UI toggles for new permissions on agent detail page - Add comprehensive tests for permission checks and CEO protection Closes #1314 Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- packages/shared/src/constants.ts | 2 + packages/shared/src/types/agent.ts | 2 + packages/shared/src/validators/agent.ts | 6 +- .../agent-deletion-permissions.test.ts | 64 ++++++++++++++ server/src/routes/agents.ts | 86 +++++++++++++++---- server/src/services/agent-permissions.ts | 12 +++ ui/src/api/agents.ts | 2 +- ui/src/pages/AgentDetail.tsx | 34 +++++++- 8 files changed, 185 insertions(+), 23 deletions(-) create mode 100644 server/src/__tests__/agent-deletion-permissions.test.ts diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 8afa80eac5..27487daa6b 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -312,6 +312,8 @@ export type JoinRequestStatus = (typeof JOIN_REQUEST_STATUSES)[number]; export const PERMISSION_KEYS = [ "agents:create", + "agents:delete", + "agents:terminate", "users:invite", "users:manage_permissions", "tasks:assign", diff --git a/packages/shared/src/types/agent.ts b/packages/shared/src/types/agent.ts index dd1ae45f41..9d05b5f63b 100644 --- a/packages/shared/src/types/agent.ts +++ b/packages/shared/src/types/agent.ts @@ -7,6 +7,8 @@ import type { export interface AgentPermissions { canCreateAgents: boolean; + canDeleteAgents: boolean; + canTerminateAgents: boolean; } export interface Agent { diff --git a/packages/shared/src/validators/agent.ts b/packages/shared/src/validators/agent.ts index f703f036e8..b912b888c7 100644 --- a/packages/shared/src/validators/agent.ts +++ b/packages/shared/src/validators/agent.ts @@ -9,6 +9,8 @@ import { envConfigSchema } from "./secret.js"; export const agentPermissionsSchema = z.object({ canCreateAgents: z.boolean().optional().default(false), + canDeleteAgents: z.boolean().optional().default(false), + canTerminateAgents: z.boolean().optional().default(false), }); const adapterConfigSchema = z.record(z.unknown()).superRefine((value, ctx) => { @@ -99,7 +101,9 @@ export const testAdapterEnvironmentSchema = z.object({ export type TestAdapterEnvironment = z.infer; export const updateAgentPermissionsSchema = z.object({ - canCreateAgents: z.boolean(), + canCreateAgents: z.boolean().optional(), + canDeleteAgents: z.boolean().optional(), + canTerminateAgents: z.boolean().optional(), }); export type UpdateAgentPermissions = z.infer; diff --git a/server/src/__tests__/agent-deletion-permissions.test.ts b/server/src/__tests__/agent-deletion-permissions.test.ts new file mode 100644 index 0000000000..4d137bc643 --- /dev/null +++ b/server/src/__tests__/agent-deletion-permissions.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from "vitest"; +import { + normalizeAgentPermissions, + defaultPermissionsForRole, +} from "../services/agent-permissions.js"; + +describe("agent deletion/termination permissions", () => { + describe("defaultPermissionsForRole", () => { + it("defaults canDeleteAgents to false for all roles", () => { + expect(defaultPermissionsForRole("ceo").canDeleteAgents).toBe(false); + expect(defaultPermissionsForRole("engineer").canDeleteAgents).toBe(false); + expect(defaultPermissionsForRole("manager").canDeleteAgents).toBe(false); + }); + + it("defaults canTerminateAgents to false for all roles", () => { + expect(defaultPermissionsForRole("ceo").canTerminateAgents).toBe(false); + expect(defaultPermissionsForRole("engineer").canTerminateAgents).toBe(false); + expect(defaultPermissionsForRole("manager").canTerminateAgents).toBe(false); + }); + + it("still defaults canCreateAgents to true for ceo", () => { + expect(defaultPermissionsForRole("ceo").canCreateAgents).toBe(true); + expect(defaultPermissionsForRole("engineer").canCreateAgents).toBe(false); + }); + }); + + describe("normalizeAgentPermissions", () => { + it("normalizes canDeleteAgents from raw permissions", () => { + const result = normalizeAgentPermissions({ canDeleteAgents: true }, "engineer"); + expect(result.canDeleteAgents).toBe(true); + expect(result.canTerminateAgents).toBe(false); + expect(result.canCreateAgents).toBe(false); + }); + + it("normalizes canTerminateAgents from raw permissions", () => { + const result = normalizeAgentPermissions({ canTerminateAgents: true }, "engineer"); + expect(result.canTerminateAgents).toBe(true); + expect(result.canDeleteAgents).toBe(false); + }); + + it("falls back to defaults for missing or invalid permissions", () => { + expect(normalizeAgentPermissions(null, "engineer").canDeleteAgents).toBe(false); + expect(normalizeAgentPermissions(null, "engineer").canTerminateAgents).toBe(false); + expect(normalizeAgentPermissions(undefined, "engineer").canDeleteAgents).toBe(false); + expect(normalizeAgentPermissions([], "engineer").canDeleteAgents).toBe(false); + }); + + it("ignores non-boolean permission values", () => { + const result = normalizeAgentPermissions({ canDeleteAgents: "yes", canTerminateAgents: 1 }, "engineer"); + expect(result.canDeleteAgents).toBe(false); + expect(result.canTerminateAgents).toBe(false); + }); + + it("preserves all permissions when all are set", () => { + const result = normalizeAgentPermissions( + { canCreateAgents: true, canDeleteAgents: true, canTerminateAgents: true }, + "engineer", + ); + expect(result.canCreateAgents).toBe(true); + expect(result.canDeleteAgents).toBe(true); + expect(result.canTerminateAgents).toBe(true); + }); + }); +}); diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 20da8d609c..bfd96a654b 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -132,6 +132,60 @@ export function agentRoutes(db: Db) { throw forbidden("Only CEO or agent creators can modify other agents"); } + function hasAgentPermission( + agent: { permissions: Record | null | undefined }, + permission: string, + ): boolean { + if (!agent.permissions || typeof agent.permissions !== "object") return false; + return Boolean((agent.permissions as Record)[permission]); + } + + async function assertCanDeleteAgent(req: Request, targetAgentId: string) { + if (req.actor.type === "board") return; + if (req.actor.type === "none") throw forbidden("Authentication required"); + if (!req.actor.agentId) throw forbidden("Agent authentication required"); + + const actorAgent = await svc.getById(req.actor.agentId); + if (!actorAgent) throw forbidden("Agent not found"); + + const targetAgent = await svc.getById(targetAgentId); + if (targetAgent && actorAgent.companyId !== targetAgent.companyId) { + throw forbidden("Agent key cannot access another company"); + } + + const allowedByGrant = await access.hasPermission( + actorAgent.companyId, + "agent", + actorAgent.id, + "agents:delete", + ); + if (allowedByGrant || hasAgentPermission(actorAgent, "canDeleteAgents")) return; + throw forbidden("Missing permission: can delete agents"); + } + + async function assertCanTerminateAgent(req: Request, targetAgentId: string) { + if (req.actor.type === "board") return; + if (req.actor.type === "none") throw forbidden("Authentication required"); + if (!req.actor.agentId) throw forbidden("Agent authentication required"); + + const actorAgent = await svc.getById(req.actor.agentId); + if (!actorAgent) throw forbidden("Agent not found"); + + const targetAgent = await svc.getById(targetAgentId); + if (targetAgent && actorAgent.companyId !== targetAgent.companyId) { + throw forbidden("Agent key cannot access another company"); + } + + const allowedByGrant = await access.hasPermission( + actorAgent.companyId, + "agent", + actorAgent.id, + "agents:terminate", + ); + if (allowedByGrant || hasAgentPermission(actorAgent, "canTerminateAgents")) return; + throw forbidden("Missing permission: can terminate agents"); + } + async function resolveCompanyIdForAgentReference(req: Request): Promise { const companyIdQuery = req.query.companyId; const requestedCompanyId = @@ -1213,24 +1267,22 @@ export function agentRoutes(db: Db) { }); router.post("/agents/:id/terminate", async (req, res) => { - assertBoard(req); const id = req.params.id as string; - const target = await svc.getById(id); - if (!target) { + await assertCanTerminateAgent(req, id); + + const agent = await svc.terminate(id, req.actor.agentId ?? undefined); + if (!agent) { res.status(404).json({ error: "Agent not found" }); return; } - if (target.role === "ceo") { - throw forbidden("Cannot terminate the CEO agent"); - } - const agent = (await svc.terminate(id))!; await heartbeat.cancelActiveForAgent(id); + const actorInfo = getActorInfo(req); await logActivity(db, { companyId: agent.companyId, - actorType: "user", - actorId: req.actor.userId ?? "board", + actorType: actorInfo.actorType, + actorId: actorInfo.actorId, action: "agent.terminated", entityType: "agent", entityId: agent.id, @@ -1240,22 +1292,20 @@ export function agentRoutes(db: Db) { }); router.delete("/agents/:id", async (req, res) => { - assertBoard(req); const id = req.params.id as string; - const target = await svc.getById(id); - if (!target) { + await assertCanDeleteAgent(req, id); + + const agent = await svc.remove(id, req.actor.agentId ?? undefined); + if (!agent) { res.status(404).json({ error: "Agent not found" }); return; } - if (target.role === "ceo") { - throw forbidden("Cannot delete the CEO agent"); - } - const agent = (await svc.remove(id))!; + const actorInfo = getActorInfo(req); await logActivity(db, { companyId: agent.companyId, - actorType: "user", - actorId: req.actor.userId ?? "board", + actorType: actorInfo.actorType, + actorId: actorInfo.actorId, action: "agent.deleted", entityType: "agent", entityId: agent.id, diff --git a/server/src/services/agent-permissions.ts b/server/src/services/agent-permissions.ts index a0379c92e0..21248b975d 100644 --- a/server/src/services/agent-permissions.ts +++ b/server/src/services/agent-permissions.ts @@ -1,10 +1,14 @@ export type NormalizedAgentPermissions = Record & { canCreateAgents: boolean; + canDeleteAgents: boolean; + canTerminateAgents: boolean; }; export function defaultPermissionsForRole(role: string): NormalizedAgentPermissions { return { canCreateAgents: role === "ceo", + canDeleteAgents: false, + canTerminateAgents: false, }; } @@ -23,5 +27,13 @@ export function normalizeAgentPermissions( typeof record.canCreateAgents === "boolean" ? record.canCreateAgents : defaults.canCreateAgents, + canDeleteAgents: + typeof record.canDeleteAgents === "boolean" + ? record.canDeleteAgents + : defaults.canDeleteAgents, + canTerminateAgents: + typeof record.canTerminateAgents === "boolean" + ? record.canTerminateAgents + : defaults.canTerminateAgents, }; } diff --git a/ui/src/api/agents.ts b/ui/src/api/agents.ts index 9008fbca2f..b9f7179ace 100644 --- a/ui/src/api/agents.ts +++ b/ui/src/api/agents.ts @@ -100,7 +100,7 @@ export const agentsApi = { api.post(`/companies/${companyId}/agent-hires`, data), update: (id: string, data: Record, companyId?: string) => api.patch(agentPath(id, companyId), data), - updatePermissions: (id: string, data: { canCreateAgents: boolean }, companyId?: string) => + updatePermissions: (id: string, data: Partial<{ canCreateAgents: boolean; canDeleteAgents: boolean; canTerminateAgents: boolean }>, companyId?: string) => api.patch(agentPath(id, companyId, "/permissions"), data), pause: (id: string, companyId?: string) => api.post(agentPath(id, companyId, "/pause"), {}), resume: (id: string, companyId?: string) => api.post(agentPath(id, companyId, "/resume"), {}), diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index f11be75e19..38ab227829 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -672,8 +672,8 @@ export function AgentDetail() { }); const updatePermissions = useMutation({ - mutationFn: (canCreateAgents: boolean) => - agentsApi.updatePermissions(agentLookupRef, { canCreateAgents }, resolvedCompanyId ?? undefined), + mutationFn: (perms: Partial<{ canCreateAgents: boolean; canDeleteAgents: boolean; canTerminateAgents: boolean }>) => + agentsApi.updatePermissions(agentLookupRef, perms, resolvedCompanyId ?? undefined), onSuccess: () => { setActionError(null); queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(routeAgentRef) }); @@ -1414,13 +1414,41 @@ function ConfigurationTab({ size="sm" className="h-7 px-2.5 text-xs" onClick={() => - updatePermissions.mutate(!Boolean(agent.permissions?.canCreateAgents)) + updatePermissions.mutate({ canCreateAgents: !Boolean(agent.permissions?.canCreateAgents) }) } disabled={updatePermissions.isPending} > {agent.permissions?.canCreateAgents ? "Enabled" : "Disabled"} +
+ Can delete agents + +
+
+ Can terminate agents + +
From c23655e97c1a270a311042eab3297f4a7d4335c0 Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 13:35:41 +0700 Subject: [PATCH 035/227] fix(server): skip body on HTTP adapter GET/HEAD requests (#1335) Node.js fetch/undici throws when GET or HEAD requests include a body. Skip body serialization and content-type header for these methods. Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- server/src/adapters/http/execute.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/adapters/http/execute.ts b/server/src/adapters/http/execute.ts index eff140233f..f1c776481f 100644 --- a/server/src/adapters/http/execute.ts +++ b/server/src/adapters/http/execute.ts @@ -10,7 +10,8 @@ export async function execute(ctx: AdapterExecutionContext): Promise; const payloadTemplate = parseObject(config.payloadTemplate); - const body = { ...payloadTemplate, agentId: agent.id, runId, context }; + const payload = { ...payloadTemplate, agentId: agent.id, runId, context }; + const hasBody = method !== "GET" && method !== "HEAD"; const controller = new AbortController(); const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null; @@ -19,10 +20,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise Date: Fri, 20 Mar 2026 14:41:33 +0700 Subject: [PATCH 036/227] fix(build): inline external workspace package dependencies into published CLI The generate-npm-package-json script added @paperclipai/server as a dependency but skipped its own dependencies (like @aws-sdk/client-s3). This caused "Cannot find module '@aws-sdk/core'" after npm install. Now the script reads and merges dependencies from external workspace packages so transitive deps are properly installed. Closes #1340 Co-Authored-By: Paperclip --- scripts/generate-npm-package-json.mjs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/generate-npm-package-json.mjs b/scripts/generate-npm-package-json.mjs index f7be8cc04b..27886e00b0 100644 --- a/scripts/generate-npm-package-json.mjs +++ b/scripts/generate-npm-package-json.mjs @@ -53,11 +53,20 @@ for (const pkgPath of workspacePaths) { for (const [name, version] of Object.entries(deps)) { if (name.startsWith("@paperclipai/") && !externalWorkspacePackages.has(name)) continue; - // For external workspace packages, read their version directly + // For external workspace packages, add the package itself AND its dependencies if (externalWorkspacePackages.has(name)) { const pkgDirMap = { "@paperclipai/server": "server" }; const wsPkg = readPkg(pkgDirMap[name]); allDeps[name] = wsPkg.version; + // Inline the external workspace package's own dependencies so they are + // installed when the published CLI package is `npm install`-ed. + const wsDeps = wsPkg.dependencies || {}; + for (const [wsDepName, wsDepVersion] of Object.entries(wsDeps)) { + if (wsDepName.startsWith("@paperclipai/")) continue; // skip internal workspace refs + if (!allDeps[wsDepName] || !wsDepVersion.startsWith("^")) { + allDeps[wsDepName] = wsDepVersion; + } + } continue; } // Keep the more specific (pinned) version if conflict From 1a7ff86330284e3348e3917a1e51b4ba72aeee9f Mon Sep 17 00:00:00 2001 From: hungdqdesign Date: Fri, 20 Mar 2026 15:17:12 +0700 Subject: [PATCH 037/227] feat: Fix real-time preview and build issues --- .../markdown-converter/.gitignore | 24 + .../markdown-converter/README.md | 73 + .../markdown-converter/eslint.config.js | 23 + .../markdown-converter/index.html | 13 + .../markdown-converter/package-lock.json | 4345 +++++++++++++++++ .../markdown-converter/package.json | 39 + .../markdown-converter/public/favicon.svg | 1 + .../markdown-converter/public/icons.svg | 24 + .../markdown-converter/src/App.css | 117 + .../markdown-converter/src/App.test.tsx | 13 + .../markdown-converter/src/App.tsx | 85 + .../markdown-converter/src/assets/hero.png | Bin 0 -> 44919 bytes .../markdown-converter/src/assets/react.svg | 1 + .../markdown-converter/src/assets/vite.svg | 1 + .../markdown-converter/src/index.css | 111 + .../markdown-converter/src/main.tsx | 10 + .../markdown-converter/src/setupTests.ts | 1 + .../markdown-converter/tsconfig.app.json | 28 + .../markdown-converter/tsconfig.json | 7 + .../markdown-converter/tsconfig.node.json | 26 + .../markdown-converter/vite.config.ts | 12 + 21 files changed, 4954 insertions(+) create mode 100755 workspaces/workspace-software-agent6-gemini/markdown-converter/.gitignore create mode 100755 workspaces/workspace-software-agent6-gemini/markdown-converter/README.md create mode 100755 workspaces/workspace-software-agent6-gemini/markdown-converter/eslint.config.js create mode 100644 workspaces/workspace-software-agent6-gemini/markdown-converter/index.html create mode 100644 workspaces/workspace-software-agent6-gemini/markdown-converter/package-lock.json create mode 100644 workspaces/workspace-software-agent6-gemini/markdown-converter/package.json create mode 100755 workspaces/workspace-software-agent6-gemini/markdown-converter/public/favicon.svg create mode 100755 workspaces/workspace-software-agent6-gemini/markdown-converter/public/icons.svg create mode 100755 workspaces/workspace-software-agent6-gemini/markdown-converter/src/App.css create mode 100644 workspaces/workspace-software-agent6-gemini/markdown-converter/src/App.test.tsx create mode 100755 workspaces/workspace-software-agent6-gemini/markdown-converter/src/App.tsx create mode 100755 workspaces/workspace-software-agent6-gemini/markdown-converter/src/assets/hero.png create mode 100755 workspaces/workspace-software-agent6-gemini/markdown-converter/src/assets/react.svg create mode 100755 workspaces/workspace-software-agent6-gemini/markdown-converter/src/assets/vite.svg create mode 100755 workspaces/workspace-software-agent6-gemini/markdown-converter/src/index.css create mode 100755 workspaces/workspace-software-agent6-gemini/markdown-converter/src/main.tsx create mode 100644 workspaces/workspace-software-agent6-gemini/markdown-converter/src/setupTests.ts create mode 100755 workspaces/workspace-software-agent6-gemini/markdown-converter/tsconfig.app.json create mode 100755 workspaces/workspace-software-agent6-gemini/markdown-converter/tsconfig.json create mode 100755 workspaces/workspace-software-agent6-gemini/markdown-converter/tsconfig.node.json create mode 100755 workspaces/workspace-software-agent6-gemini/markdown-converter/vite.config.ts diff --git a/workspaces/workspace-software-agent6-gemini/markdown-converter/.gitignore b/workspaces/workspace-software-agent6-gemini/markdown-converter/.gitignore new file mode 100755 index 0000000000..a547bf36d8 --- /dev/null +++ b/workspaces/workspace-software-agent6-gemini/markdown-converter/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/workspaces/workspace-software-agent6-gemini/markdown-converter/README.md b/workspaces/workspace-software-agent6-gemini/markdown-converter/README.md new file mode 100755 index 0000000000..7dbf7ebf3b --- /dev/null +++ b/workspaces/workspace-software-agent6-gemini/markdown-converter/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/workspaces/workspace-software-agent6-gemini/markdown-converter/eslint.config.js b/workspaces/workspace-software-agent6-gemini/markdown-converter/eslint.config.js new file mode 100755 index 0000000000..5e6b472f58 --- /dev/null +++ b/workspaces/workspace-software-agent6-gemini/markdown-converter/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/workspaces/workspace-software-agent6-gemini/markdown-converter/index.html b/workspaces/workspace-software-agent6-gemini/markdown-converter/index.html new file mode 100644 index 0000000000..d612ffddf6 --- /dev/null +++ b/workspaces/workspace-software-agent6-gemini/markdown-converter/index.html @@ -0,0 +1,13 @@ + + + + + + + markdown-converter + + +
+ + + diff --git a/workspaces/workspace-software-agent6-gemini/markdown-converter/package-lock.json b/workspaces/workspace-software-agent6-gemini/markdown-converter/package-lock.json new file mode 100644 index 0000000000..7f408a4319 --- /dev/null +++ b/workspaces/workspace-software-agent6-gemini/markdown-converter/package-lock.json @@ -0,0 +1,4345 @@ +{ + "name": "markdown-converter", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "markdown-converter", + "version": "0.0.0", + "dependencies": { + "@types/marked": "^5.0.2", + "html2canvas": "^1.4.1", + "jspdf": "^4.2.1", + "marked": "^17.0.4", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/node": "^24.12.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "jsdom": "^29.0.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.57.0", + "vite": "^8.0.1", + "vitest": "^4.1.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.3.tgz", + "integrity": "sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", + "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", + "integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz", + "integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz", + "integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz", + "integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz", + "integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz", + "integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz", + "integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz", + "integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz", + "integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/marked": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz", + "integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", + "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/type-utils": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", + "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", + "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.1", + "@typescript-eslint/types": "^8.57.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", + "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", + "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", + "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", + "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", + "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.1", + "@typescript-eslint/tsconfig-utils": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", + "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", + "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.0", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.9", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.9.tgz", + "integrity": "sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001780", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", + "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dompurify": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", + "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.0.tgz", + "integrity": "sha512-9FshNB6OepopZ08unmmGpsF7/qCjxGPbo3NbgfJAnPeHXnsODE9WWffXZtRFRFe0ntzaAOcSKNJFz8wiyvF1jQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@asamuzakjp/dom-selector": "^7.0.2", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.24.3", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jspdf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/marked": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz", + "integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", + "integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.120.0", + "@rolldown/pluginutils": "1.0.0-rc.10" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.10", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", + "@rolldown/binding-darwin-x64": "1.0.0-rc.10", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz", + "integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.26", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.26.tgz", + "integrity": "sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.26" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.26", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.26.tgz", + "integrity": "sha512-5WJ2SqFsv4G2Dwi7ZFVRnz6b2H1od39QME1lc2y5Ew3eWiZMAeqOAfWpRP9jHvhUl881406QtZTODvjttJs+ew==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", + "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.1", + "@typescript-eslint/parser": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz", + "integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/vite": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", + "integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.10", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/workspaces/workspace-software-agent6-gemini/markdown-converter/package.json b/workspaces/workspace-software-agent6-gemini/markdown-converter/package.json new file mode 100644 index 0000000000..8aee69a610 --- /dev/null +++ b/workspaces/workspace-software-agent6-gemini/markdown-converter/package.json @@ -0,0 +1,39 @@ +{ + "name": "markdown-converter", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "test": "vitest" + }, + "dependencies": { + "@types/marked": "^5.0.2", + "html2canvas": "^1.4.1", + "jspdf": "^4.2.1", + "marked": "^17.0.4", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/node": "^24.12.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "jsdom": "^29.0.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.57.0", + "vite": "^8.0.1", + "vitest": "^4.1.0" + } +} diff --git a/workspaces/workspace-software-agent6-gemini/markdown-converter/public/favicon.svg b/workspaces/workspace-software-agent6-gemini/markdown-converter/public/favicon.svg new file mode 100755 index 0000000000..6893eb1323 --- /dev/null +++ b/workspaces/workspace-software-agent6-gemini/markdown-converter/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/workspaces/workspace-software-agent6-gemini/markdown-converter/public/icons.svg b/workspaces/workspace-software-agent6-gemini/markdown-converter/public/icons.svg new file mode 100755 index 0000000000..e9522193d9 --- /dev/null +++ b/workspaces/workspace-software-agent6-gemini/markdown-converter/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/workspaces/workspace-software-agent6-gemini/markdown-converter/src/App.css b/workspaces/workspace-software-agent6-gemini/markdown-converter/src/App.css new file mode 100755 index 0000000000..4bbee2d1bb --- /dev/null +++ b/workspaces/workspace-software-agent6-gemini/markdown-converter/src/App.css @@ -0,0 +1,117 @@ +@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap'); + +:root { + --primary-color: #3498db; + --secondary-color: #2c3e50; + --background-color: #ecf0f1; + --surface-color: #ffffff; + --text-color: #333333; + --light-gray: #bdc3c7; + --success-color: #2ecc71; + --success-hover-color: #27ae60; +} + +body { + margin: 0; + font-family: 'Montserrat', sans-serif; + color: var(--text-color); + background-color: var(--background-color); +} + +.app { + display: flex; + flex-direction: column; + height: 100vh; +} + +header { + text-align: center; + padding: 1rem; + background-color: var(--secondary-color); + color: white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + z-index: 10; +} + +header h1 { + margin: 0; + font-size: 1.5rem; + font-weight: 700; +} + +main { + display: flex; + flex-grow: 1; + overflow: hidden; + padding: 1rem; + gap: 1rem; +} + +.markdown-input-container, +.html-output-container { + display: flex; + flex-direction: column; + width: 50%; + background-color: var(--surface-color); + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + overflow: hidden; + position: relative; +} + +.markdown-input, +.html-output { + padding: 1.5rem; + overflow-y: auto; + font-size: 1rem; + line-height: 1.6; + border: none; + flex-grow: 1; +} + +.markdown-input { + resize: none; + font-family: 'Courier New', Courier, monospace; +} + +.html-output { + background-color: #fdfdfd; +} + +.button-container { + position: absolute; + top: 1rem; + right: 1rem; + display: flex; + gap: 0.5rem; + z-index: 5; +} + +.copy-button, +.download-button { + background-color: var(--primary-color); + color: white; + border: none; + padding: 0.6rem 1.2rem; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease, transform 0.2s ease; + font-weight: bold; +} + +.download-button { + background-color: var(--success-color); +} + +.copy-button:hover, +.download-button:hover { + transform: translateY(-2px); +} + +.copy-button:hover { + background-color: #2980b9; +} + +.download-button:hover { + background-color: var(--success-hover-color); +} diff --git a/workspaces/workspace-software-agent6-gemini/markdown-converter/src/App.test.tsx b/workspaces/workspace-software-agent6-gemini/markdown-converter/src/App.test.tsx new file mode 100644 index 0000000000..f0c0d9de5b --- /dev/null +++ b/workspaces/workspace-software-agent6-gemini/markdown-converter/src/App.test.tsx @@ -0,0 +1,13 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; +import { describe, it, expect } from 'vitest'; + +describe('App', () => { + it('renders the App component and converts markdown to html', async () => { + render(); + + const output = await screen.findByText('Hello, world!'); + expect(output.tagName).toBe('H1'); + }); +}); + diff --git a/workspaces/workspace-software-agent6-gemini/markdown-converter/src/App.tsx b/workspaces/workspace-software-agent6-gemini/markdown-converter/src/App.tsx new file mode 100755 index 0000000000..d18b2021f1 --- /dev/null +++ b/workspaces/workspace-software-agent6-gemini/markdown-converter/src/App.tsx @@ -0,0 +1,85 @@ +import { useState, useRef, useEffect } from 'react'; +import { marked } from 'marked'; +import jsPDF from 'jspdf'; +import html2canvas from 'html2canvas'; +import './App.css'; + +// Create a new marked instance with sanitized options +const renderer = new marked.Renderer(); +marked.setOptions({ + renderer, + gfm: true, + breaks: true, + // sanitize: true, // DEPRECATED: This option is no longer supported. +}); + +function App() { + const [markdown, setMarkdown] = useState('# Hello, world!'); + const [html, setHtml] = useState(''); + const [copied, setCopied] = useState(false); + const htmlOutputRef = useRef(null); + + useEffect(() => { + const convertMarkdown = async () => { + const result = await marked(markdown); + setHtml(result); + }; + convertMarkdown(); + }, [markdown]); + + const handleCopy = () => { + navigator.clipboard.writeText(html); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const handleDownloadPdf = () => { + if (htmlOutputRef.current) { + html2canvas(htmlOutputRef.current).then((canvas) => { + const imgData = canvas.toDataURL('image/png'); + const pdf = new jsPDF(); + const imgProps = pdf.getImageProperties(imgData); + const pdfWidth = pdf.internal.pageSize.getWidth(); + const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width; + pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, pdfHeight); + pdf.save('download.pdf'); + }); + } + }; + + return ( +
+
+

Markdown to HTML Converter

+
+
+
+