From b4696e736b1188bfd06819a088621a0f846439d8 Mon Sep 17 00:00:00 2001 From: bluewater_cg Date: Sun, 2 Aug 2026 21:56:50 -0700 Subject: [PATCH 01/23] feat: add Pi memory integration --- App/backend/README.md | 3 +- .../tests/agent-sources-route.test.ts | 1 + .../src/adapters/outbound/agent-paths.ts | 18 + .../outbound/agent-source/pi/adapter.ts | 106 ++++ .../outbound/agent-source/pi/index.ts | 2 + .../agent-source/pi/session-discovery.ts | 87 ++++ .../agent-source/pi/session-reader.ts | 172 +++++++ .../agent-source/pi/tests/adapter.test.ts | 109 +++++ .../agent-source/tests/agent-paths.test.ts | 16 +- .../outbound/skill-writer/pi/index.ts | 2 + .../outbound/skill-writer/pi/target.ts | 141 ++++++ .../skill-writer/pi/tests/target.test.ts | 249 ++++++++++ .../templates/memmy-pi-extension.ts | 461 ++++++++++++++++++ .../src/analytics/agent-source-analytics.ts | 2 +- .../agent-source-auto-inject-service.ts | 4 +- .../services/builtin-agent-source-registry.ts | 2 + App/backend/src/services/index.ts | 2 + .../agent-source-auto-inject-service.test.ts | 4 +- .../builtin-agent-source-registry.test.ts | 1 + App/frontend/desktop/src/i18n/messages.ts | 8 +- .../desktop/src/pages/agent-source-logos.ts | 2 + .../pages/first-encounter-relay-challenge.tsx | 2 +- .../desktop/src/pages/memory-sources-page.tsx | 2 +- .../memory/tests/memory-runtime-fixtures.ts | 1 + .../memory/tests/sources-sub-page.test.tsx | 4 +- docs/cn/memory/sources.mdx | 27 +- docs/en/memory/sources.mdx | 27 +- 27 files changed, 1431 insertions(+), 24 deletions(-) create mode 100644 App/backend/src/adapters/outbound/agent-source/pi/adapter.ts create mode 100644 App/backend/src/adapters/outbound/agent-source/pi/index.ts create mode 100644 App/backend/src/adapters/outbound/agent-source/pi/session-discovery.ts create mode 100644 App/backend/src/adapters/outbound/agent-source/pi/session-reader.ts create mode 100644 App/backend/src/adapters/outbound/agent-source/pi/tests/adapter.test.ts create mode 100644 App/backend/src/adapters/outbound/skill-writer/pi/index.ts create mode 100644 App/backend/src/adapters/outbound/skill-writer/pi/target.ts create mode 100644 App/backend/src/adapters/outbound/skill-writer/pi/tests/target.test.ts create mode 100644 App/backend/src/adapters/outbound/skill-writer/templates/memmy-pi-extension.ts diff --git a/App/backend/README.md b/App/backend/README.md index f4d9fe2e7..39ae84bf1 100644 --- a/App/backend/README.md +++ b/App/backend/README.md @@ -26,7 +26,7 @@ npm run db:migrate - `adapters/inbound/local-api`: Fastify routes, runtime-token authentication, CORS, SSE, and the Composio MCP bridge. - `adapters/outbound/agent-source`: built-in history readers for Cursor, Claude - Code, Codex, OpenCode, OpenClaw, Hermes, and WorkBuddy. + Code, Codex, Pi, OpenCode, OpenClaw, Hermes, and WorkBuddy. - `adapters/outbound/skill-writer`: Memory skill, hook, command, and plugin installation for the supported agents. - `adapters/outbound/agent-adapter`: manifest, loader, and registry contracts @@ -114,6 +114,7 @@ Every route in this table requires the local runtime token. | Cursor | Windows: `%APPDATA%\Cursor\User`; macOS: `~/Library/Application Support/Cursor/User`; Linux: `${XDG_CONFIG_HOME:-~/.config}/Cursor/User` (`workspaceStorage/*/state.vscdb` and `globalStorage/state.vscdb`) | `~/.cursor/skills/memmy-memory/` and `~/.cursor/hooks.json` | | Claude Code | `~/.claude/projects/**/*.jsonl` | `~/.claude/CLAUDE.md`, `skills/memmy-memory/`, hooks, and the resume command | | Codex | `~/.codex/sessions/**/rollout-*.jsonl` | `~/.codex/AGENTS.md`, `skills/memmy-memory/`, and hooks | +| Pi | `${PI_CODING_AGENT_SESSION_DIR:-~/.pi/agent/sessions}/**/*.jsonl` | `~/.pi/agent/AGENTS.md`, `skills/memmy-memory/`, and native extension | | OpenCode | `${XDG_DATA_HOME:-~/.local/share}/opencode/opencode.db` | `${XDG_CONFIG_HOME:-~/.config}/opencode/AGENTS.md`, `skills/memmy-memory/`, plugin, and resume command | | OpenClaw | SQLite databases under `~/.openclaw/` | Workspace `AGENTS.md`, `~/.openclaw/skills/memmy-memory/`, and the Memory extension | | Hermes | `~/.hermes/sessions/**/*.jsonl` and `~/.hermes/state.db` | `~/.hermes/SOUL.md`, `skills/memmy-memory/`, and Memory/resume plugins | diff --git a/App/backend/src/adapters/inbound/local-api/tests/agent-sources-route.test.ts b/App/backend/src/adapters/inbound/local-api/tests/agent-sources-route.test.ts index b01b9f0b8..e6bae39ed 100644 --- a/App/backend/src/adapters/inbound/local-api/tests/agent-sources-route.test.ts +++ b/App/backend/src/adapters/inbound/local-api/tests/agent-sources-route.test.ts @@ -415,6 +415,7 @@ describe("agent sources local api routes", () => { "cursor", "claude_code", "codex", + "pi", "opencode", "openclaw", "hermes", diff --git a/App/backend/src/adapters/outbound/agent-paths.ts b/App/backend/src/adapters/outbound/agent-paths.ts index 6fd411752..6fe96cca0 100644 --- a/App/backend/src/adapters/outbound/agent-paths.ts +++ b/App/backend/src/adapters/outbound/agent-paths.ts @@ -52,6 +52,24 @@ export function resolveCodexSessionsDirectory(options: ResolveAgentPathOptions = return createAgentPathRuntime(options).pathApi.join(resolveCodexHomeDirectory(options), "sessions"); } +export function resolvePiHomeDirectory(options: ResolveAgentPathOptions = {}): string { + const runtime = createAgentPathRuntime(options); + return resolveConfiguredDirectory( + runtime.environment.PI_CODING_AGENT_DIR, + runtime.pathApi.join(runtime.homeDirectory, ".pi", "agent"), + runtime + ); +} + +export function resolvePiSessionsDirectory(options: ResolveAgentPathOptions = {}): string { + const runtime = createAgentPathRuntime(options); + return resolveConfiguredDirectory( + runtime.environment.PI_CODING_AGENT_SESSION_DIR, + runtime.pathApi.join(resolvePiHomeDirectory(options), "sessions"), + runtime + ); +} + export function resolveOpencodeConfigDirectory(options: ResolveAgentPathOptions = {}): string { const runtime = createAgentPathRuntime(options); const xdgConfigRoot = resolveConfiguredDirectory( diff --git a/App/backend/src/adapters/outbound/agent-source/pi/adapter.ts b/App/backend/src/adapters/outbound/agent-source/pi/adapter.ts new file mode 100644 index 000000000..d84bc43b4 --- /dev/null +++ b/App/backend/src/adapters/outbound/agent-source/pi/adapter.ts @@ -0,0 +1,106 @@ +/** Pi source adapter module. */ +import { access } from "node:fs/promises"; +import { resolvePiSessionsDirectory } from "../../agent-paths.js"; +import { collectConversationWindow, remainingMessageCapacity } from "../conversation-window.js"; +import { redactSecrets } from "../secret-redactor.js"; +import type { ConversationMessage, ScanOptions, SourceAdapter, SourceDescriptor } from "../types.js"; +import { discoverPiSessions } from "./session-discovery.js"; +import { readPiSession, type RawPiMessage } from "./session-reader.js"; + +const PI_SOURCE_ID = "pi"; + +export interface CreatePiSourceAdapterDeps { + sessionsRoot?: string; + descriptor?: SourceDescriptor; +} + +export function createPiSourceAdapter(deps: CreatePiSourceAdapterDeps = {}): SourceAdapter { + const sessionsRoot = deps.sessionsRoot ?? resolvePiSessionsDirectory(); + const descriptor = deps.descriptor ?? Object.freeze({ + sourceId: PI_SOURCE_ID, + displayName: "Pi", + builtin: true, + dataPath: sessionsRoot + }); + + return { + descriptor, + async detect() { + try { + await access(sessionsRoot); + return true; + } catch (error) { + if (isNodeError(error) && error.code === "ENOENT") { + return false; + } + throw error; + } + }, + async *scan(options: ScanOptions) { + throwIfAborted(options.signal); + options.onProgress?.({ sourceId: descriptor.sourceId, phase: "discover", current: 0, total: 1 }); + const sessions = await discoverPiSessions({ + root: sessionsRoot, + order: options.order === "recent_first" ? "recent_first" : "path_asc", + maxSessions: options.maxScanTargets + }); + options.onProgress?.({ sourceId: descriptor.sourceId, phase: "discover", current: sessions.length, total: sessions.length }); + + let emittedMessages = 0; + for (const [sessionIndex, session] of sessions.entries()) { + throwIfAborted(options.signal); + if (options.maxMessages !== undefined && emittedMessages >= options.maxMessages) { + break; + } + options.onProgress?.({ + sourceId: descriptor.sourceId, + phase: "read", + current: sessionIndex, + total: sessions.length, + message: session.sessionFilePath + }); + const messages = await collectConversationWindow( + readPiSession(session.sessionFilePath, options.signal), + options.since, + options.signal, + remainingMessageCapacity(options.maxMessages, emittedMessages) + ); + for (const rawMessage of messages) { + throwIfAborted(options.signal); + if (options.maxMessages !== undefined && emittedMessages >= options.maxMessages) { + break; + } + emittedMessages += 1; + yield toConversationMessage(descriptor.sourceId, rawMessage, session.workspacePath, session.gitRoot); + } + } + options.onProgress?.({ sourceId: descriptor.sourceId, phase: "done", current: emittedMessages, total: emittedMessages }); + } + }; +} + +function toConversationMessage( + sourceId: string, + rawMessage: RawPiMessage, + workspacePath: string | null, + gitRoot: string | null +): ConversationMessage { + return { + ...rawMessage, + sourceId, + content: redactSecrets(rawMessage.content), + workspacePath, + gitRoot, + rawMeta: Object.freeze({}) + }; +} + +function throwIfAborted(signal: AbortSignal | undefined): void { + if (signal?.aborted) { + throw new DOMException("Pi source scan aborted", "AbortError"); + } +} + +function isNodeError(error: unknown): error is NodeJS.ErrnoException { + return error instanceof Error && "code" in error; +} diff --git a/App/backend/src/adapters/outbound/agent-source/pi/index.ts b/App/backend/src/adapters/outbound/agent-source/pi/index.ts new file mode 100644 index 000000000..bba100fa2 --- /dev/null +++ b/App/backend/src/adapters/outbound/agent-source/pi/index.ts @@ -0,0 +1,2 @@ +/** Pi module. */ +export { createPiSourceAdapter } from "./adapter.js"; diff --git a/App/backend/src/adapters/outbound/agent-source/pi/session-discovery.ts b/App/backend/src/adapters/outbound/agent-source/pi/session-discovery.ts new file mode 100644 index 000000000..add146dfd --- /dev/null +++ b/App/backend/src/adapters/outbound/agent-source/pi/session-discovery.ts @@ -0,0 +1,87 @@ +/** Pi session discovery module. */ +import { existsSync } from "node:fs"; +import { stat } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { readJsonlObjects } from "../jsonl-lines.js"; +import { readDirectoryIfExists } from "../read-directory.js"; + +export interface PiSessionFile { + sessionFilePath: string; + workspacePath: string | null; + gitRoot: string | null; +} + +export interface DiscoverPiSessionsOptions { + root: string; + order?: "path_asc" | "recent_first"; + maxSessions?: number; +} + +export async function discoverPiSessions(options: DiscoverPiSessionsOptions): Promise { + const files = await listSessionFiles(options.root, options.order ?? "path_asc", options.maxSessions); + const sessions: PiSessionFile[] = []; + + for (const sessionFilePath of files) { + const workspacePath = await readSessionCwd(sessionFilePath); + sessions.push({ + sessionFilePath, + workspacePath, + gitRoot: workspacePath ? findGitRoot(workspacePath) : null + }); + } + + return sessions; +} + +async function listSessionFiles( + root: string, + order: "path_asc" | "recent_first", + maxSessions: number | undefined +): Promise { + const files: Array<{ path: string; mtimeMs: number }> = []; + const directories = [root]; + + for (let directoryIndex = 0; directoryIndex < directories.length; directoryIndex += 1) { + const currentDirectory = directories[directoryIndex]!; + for (const entry of await readDirectoryIfExists(currentDirectory)) { + const path = join(currentDirectory, entry.name); + if (entry.isDirectory()) { + directories.push(path); + } else if (entry.isFile() && entry.name.endsWith(".jsonl")) { + const fileStat = await stat(path); + files.push({ path, mtimeMs: fileStat.mtimeMs }); + } + } + } + + return files + .sort((left, right) => order === "recent_first" + ? right.mtimeMs - left.mtimeMs || right.path.localeCompare(left.path) + : left.path.localeCompare(right.path)) + .slice(0, maxSessions ?? files.length) + .map((file) => file.path); +} + +async function readSessionCwd(filePath: string): Promise { + try { + for await (const record of readJsonlObjects(filePath)) { + if (record.type === "session") { + return typeof record.cwd === "string" ? record.cwd : null; + } + } + } catch { + return null; + } + return null; +} + +function findGitRoot(workspacePath: string): string | null { + let current = workspacePath; + while (current !== dirname(current)) { + if (existsSync(join(current, ".git"))) { + return current; + } + current = dirname(current); + } + return existsSync(join(current, ".git")) ? current : null; +} diff --git a/App/backend/src/adapters/outbound/agent-source/pi/session-reader.ts b/App/backend/src/adapters/outbound/agent-source/pi/session-reader.ts new file mode 100644 index 000000000..2bfdddafd --- /dev/null +++ b/App/backend/src/adapters/outbound/agent-source/pi/session-reader.ts @@ -0,0 +1,172 @@ +/** Pi session reader module. */ +import { basename } from "node:path"; +import { readJsonlObjects, type JsonObject } from "../jsonl-lines.js"; + +export interface RawPiMessage { + messageId: string; + conversationId: string; + role: "user" | "assistant" | "tool" | "system"; + content: string; + createdAt: string; +} + +interface PiEntry { + id: string; + parentId: string | null; + record: JsonObject; +} + +export async function* readPiSession(filePath: string, signal?: AbortSignal): AsyncIterable { + const entries: PiEntry[] = []; + const handledEntryIds = new Set(); + let sessionId = basename(filePath, ".jsonl"); + + for await (const record of readJsonlObjects(filePath, signal)) { + if (record.type === "session" && typeof record.id === "string") { + sessionId = record.id; + } + if (typeof record.id === "string") { + entries.push({ + id: record.id, + parentId: typeof record.parentId === "string" ? record.parentId : null, + record + }); + } + collectHandledEntryIds(record, handledEntryIds); + } + + const activeEntryIds = collectActiveBranchIds(entries); + for (const entry of entries) { + if (!activeEntryIds.has(entry.id) || handledEntryIds.has(entry.id)) { + continue; + } + const message = toRawPiMessage(entry.record, sessionId, entry.id); + if (message) { + yield message; + } + } +} + +function collectHandledEntryIds(record: JsonObject, handledEntryIds: Set): void { + if (record.type !== "custom" || record.customType !== "memmy-memory-capture" || !isRecord(record.data)) { + return; + } + if (!Array.isArray(record.data.entryIds)) { + return; + } + for (const entryId of record.data.entryIds) { + if (typeof entryId === "string") handledEntryIds.add(entryId); + } +} + +function collectActiveBranchIds(entries: readonly PiEntry[]): Set { + const byId = new Map(entries.map((entry) => [entry.id, entry])); + const activeIds = new Set(); + let current = entries.at(-1); + while (current && !activeIds.has(current.id)) { + activeIds.add(current.id); + current = current.parentId ? byId.get(current.parentId) : undefined; + } + return activeIds; +} + +function toRawPiMessage(record: JsonObject, sessionId: string, entryId: string): RawPiMessage | null { + if (record.type !== "message" || !isRecord(record.message)) { + return null; + } + const message = record.message; + const role = message.role; + if (role !== "user" && role !== "assistant" && role !== "toolResult" && role !== "system") { + return null; + } + const content = renderContent(message.content, role, message); + if (!content) { + return null; + } + return { + messageId: `${sessionId}:${entryId}`, + conversationId: sessionId, + role: role === "toolResult" ? "tool" : role, + content, + createdAt: normalizeTimestamp(record.timestamp ?? message.timestamp) + }; +} + +function renderContent(content: unknown, role: string, message: Record): string | null { + if (typeof content === "string") { + return content.trim() || null; + } + if (!Array.isArray(content)) { + return null; + } + + const parts: string[] = []; + for (const item of content) { + if (!isRecord(item) || item.type === "thinking") { + continue; + } + if (item.type === "text" && typeof item.text === "string" && item.text.trim()) { + parts.push(item.text.trim()); + continue; + } + if (item.type === "toolCall") { + parts.push(renderToolCall(item)); + continue; + } + if (role === "toolResult") { + const text = typeof item.text === "string" ? item.text : formatValue(item); + if (text.trim()) { + parts.push(text.trim()); + } + } + } + const rendered = parts.filter(Boolean).join("\n\n"); + if (role !== "toolResult" || !rendered) { + return rendered || null; + } + return [ + `Tool: ${normalizeString(message.toolName) || "tool"}`, + normalizeString(message.toolCallId) ? `Call ID: ${normalizeString(message.toolCallId)}` : "", + message.isError === true ? "Status: error" : "", + `Output:\n${rendered}` + ].filter(Boolean).join("\n\n"); +} + +function renderToolCall(item: Record): string { + return [ + `Tool: ${normalizeString(item.name) || "tool"}`, + normalizeString(item.id) ? `Call ID: ${normalizeString(item.id)}` : "", + item.arguments !== undefined ? `Input:\n${formatValue(item.arguments)}` : "" + ].filter(Boolean).join("\n\n"); +} + +function formatValue(value: unknown): string { + if (typeof value === "string") { + return value; + } + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + +function normalizeTimestamp(value: unknown): string { + if (typeof value === "number") { + const date = new Date(value); + return Number.isNaN(date.getTime()) ? new Date(0).toISOString() : date.toISOString(); + } + if (typeof value === "string") { + const date = new Date(value); + return Number.isNaN(date.getTime()) ? new Date(0).toISOString() : date.toISOString(); + } + return new Date(0).toISOString(); +} + +function normalizeString(value: unknown): string { + return typeof value === "string" ? value.trim() : ""; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/App/backend/src/adapters/outbound/agent-source/pi/tests/adapter.test.ts b/App/backend/src/adapters/outbound/agent-source/pi/tests/adapter.test.ts new file mode 100644 index 000000000..866041cb3 --- /dev/null +++ b/App/backend/src/adapters/outbound/agent-source/pi/tests/adapter.test.ts @@ -0,0 +1,109 @@ +/** Pi source adapter tests. */ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { createPiSourceAdapter } from "../index.js"; +import { discoverPiSessions } from "../session-discovery.js"; +import { readPiSession } from "../session-reader.js"; + +let tempDirectory: string | undefined; + +afterEach(() => { + if (tempDirectory) { + rmSync(tempDirectory, { recursive: true, force: true }); + tempDirectory = undefined; + } +}); + +describe("Pi source adapter", () => { + it("reads the active branch with text and tool traces but excludes thinking", async () => { + const fixture = createFixture(); + const messages = await collect(readPiSession(fixture.sessionPath)); + + expect(messages).toEqual([ + expect.objectContaining({ messageId: "pi-session-1:user-1", role: "user", content: expect.stringContaining("sk-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN") }), + expect.objectContaining({ role: "assistant", content: expect.stringContaining("Tool: bash") }), + expect.objectContaining({ role: "tool", content: expect.stringContaining("Tool: bash") }), + expect.objectContaining({ role: "assistant", content: "Done" }) + ]); + expect(messages.map((message) => message.content).join("\n")).not.toContain("private reasoning"); + expect(messages.map((message) => message.content).join("\n")).not.toContain("abandoned answer"); + }); + + it("discovers nested sessions and streams redacted messages", async () => { + const fixture = createFixture(); + const adapter = createPiSourceAdapter({ sessionsRoot: fixture.sessionsRoot }); + + await expect(discoverPiSessions({ root: fixture.sessionsRoot })).resolves.toEqual([ + expect.objectContaining({ sessionFilePath: fixture.sessionPath, workspacePath: fixture.workspacePath }) + ]); + const messages = await collect(adapter.scan({})); + expect(messages[0]).toEqual(expect.objectContaining({ + sourceId: "pi", + conversationId: "pi-session-1", + content: "Use OPENAI_API_KEY=[REDACTED:openai_api_key]", + workspacePath: fixture.workspacePath + })); + }); + + it("treats a missing sessions directory as empty history", async () => { + const sessionsRoot = join(tmpdir(), `memmy-missing-pi-${crypto.randomUUID()}`); + await expect(discoverPiSessions({ root: sessionsRoot })).resolves.toEqual([]); + await expect(collect(createPiSourceAdapter({ sessionsRoot }).scan({}))).resolves.toEqual([]); + }); + + it("honors scan limits and aborts", async () => { + const fixture = createFixture(); + const adapter = createPiSourceAdapter({ sessionsRoot: fixture.sessionsRoot }); + await expect(collect(adapter.scan({ maxMessages: 2 }))).resolves.toHaveLength(2); + + const controller = new AbortController(); + controller.abort(); + await expect(collect(adapter.scan({ signal: controller.signal }))).rejects.toThrow("Pi source scan aborted"); + }); + + it("skips entries already handled by the live extension", async () => { + const fixture = createFixture([ + { + type: "custom", + id: "capture-1", + parentId: "assistant-2", + timestamp: "2026-08-01T00:00:06.000Z", + customType: "memmy-memory-capture", + data: { entryIds: ["user-1", "assistant-1", "tool-1", "assistant-2"], status: "succeeded" } + } + ]); + + await expect(collect(readPiSession(fixture.sessionPath))).resolves.toEqual([]); + }); +}); + +function createFixture(extraRows: Array> = []): { sessionsRoot: string; sessionPath: string; workspacePath: string } { + tempDirectory = mkdtempSync(join(tmpdir(), "memmy-pi-source-")); + const sessionsRoot = join(tempDirectory, "sessions"); + const workspacePath = join(tempDirectory, "workspace"); + const sessionDirectory = join(sessionsRoot, "--workspace--"); + const sessionPath = join(sessionDirectory, "2026-08-01T00-00-00-000Z_pi-session-1.jsonl"); + mkdirSync(sessionDirectory, { recursive: true }); + mkdirSync(workspacePath, { recursive: true }); + const rows = [ + { type: "session", version: 3, id: "pi-session-1", timestamp: "2026-08-01T00:00:00.000Z", cwd: workspacePath }, + { type: "message", id: "user-1", parentId: null, timestamp: "2026-08-01T00:00:01.000Z", message: { role: "user", content: [{ type: "text", text: "Use OPENAI_API_KEY=sk-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN" }] } }, + { type: "message", id: "abandoned", parentId: "user-1", timestamp: "2026-08-01T00:00:02.000Z", message: { role: "assistant", content: [{ type: "text", text: "abandoned answer" }] } }, + { type: "message", id: "assistant-1", parentId: "user-1", timestamp: "2026-08-01T00:00:03.000Z", message: { role: "assistant", content: [{ type: "thinking", thinking: "private reasoning" }, { type: "toolCall", id: "call-1", name: "bash", arguments: { command: "pwd" } }] } }, + { type: "message", id: "tool-1", parentId: "assistant-1", timestamp: "2026-08-01T00:00:04.000Z", message: { role: "toolResult", toolCallId: "call-1", toolName: "bash", content: [{ type: "text", text: "command output" }] } }, + { type: "message", id: "assistant-2", parentId: "tool-1", timestamp: "2026-08-01T00:00:05.000Z", message: { role: "assistant", content: [{ type: "text", text: "Done" }] } }, + ...extraRows + ]; + writeFileSync(sessionPath, `${rows.map((row) => JSON.stringify(row)).join("\n")}\n`, "utf8"); + return { sessionsRoot, sessionPath, workspacePath }; +} + +async function collect(iterable: AsyncIterable): Promise { + const items: T[] = []; + for await (const item of iterable) { + items.push(item); + } + return items; +} diff --git a/App/backend/src/adapters/outbound/agent-source/tests/agent-paths.test.ts b/App/backend/src/adapters/outbound/agent-source/tests/agent-paths.test.ts index 09abf57b5..dc5bb1990 100644 --- a/App/backend/src/adapters/outbound/agent-source/tests/agent-paths.test.ts +++ b/App/backend/src/adapters/outbound/agent-source/tests/agent-paths.test.ts @@ -12,6 +12,8 @@ import { resolveOpencodeDatabasePath, resolveOpenclawConfigPath, resolveOpenclawStateDirectory, + resolvePiHomeDirectory, + resolvePiSessionsDirectory, resolveWorkbuddyHomeDirectory, resolveWorkbuddyProjectsDirectory } from "../../agent-paths.js"; @@ -25,6 +27,8 @@ const ENVIRONMENT_VARIABLES = [ "OPENCODE_CONFIG_DIR", "OPENCLAW_CONFIG_PATH", "OPENCLAW_STATE_DIR", + "PI_CODING_AGENT_DIR", + "PI_CODING_AGENT_SESSION_DIR", "WORKBUDDY_CONFIG_DIR", "XDG_CONFIG_HOME", "XDG_DATA_HOME" @@ -48,6 +52,8 @@ describe("agent paths", () => { process.env.HERMES_HOME = "/tmp/hermes-home"; process.env.OPENCLAW_STATE_DIR = "/tmp/openclaw-state"; process.env.OPENCLAW_CONFIG_PATH = "/tmp/openclaw-config.json"; + process.env.PI_CODING_AGENT_DIR = "/tmp/pi-home"; + process.env.PI_CODING_AGENT_SESSION_DIR = "/tmp/pi-sessions"; process.env.WORKBUDDY_CONFIG_DIR = "/tmp/workbuddy-home"; expect(resolveClaudeCodeHomeDirectory()).toBe("/tmp/claude-home"); @@ -55,6 +61,8 @@ describe("agent paths", () => { expect(resolveHermesHomeDirectory()).toBe("/tmp/hermes-home"); expect(resolveOpenclawStateDirectory()).toBe("/tmp/openclaw-state"); expect(resolveOpenclawConfigPath()).toBe("/tmp/openclaw-config.json"); + expect(resolvePiHomeDirectory()).toBe("/tmp/pi-home"); + expect(resolvePiSessionsDirectory()).toBe("/tmp/pi-sessions"); expect(resolveWorkbuddyHomeDirectory()).toBe("/tmp/workbuddy-home"); }); @@ -80,7 +88,7 @@ describe("agent paths", () => { expect(resolveOpencodeConfigDirectory()).toBe("/tmp/custom-opencode"); }); - it("resolves all seven Agent source paths on macOS", () => { + it("resolves all eight Agent source paths on macOS", () => { const options = { platform: "darwin" as const, homeDirectory: "/Users/alice", @@ -93,6 +101,7 @@ describe("agent paths", () => { codex: resolveCodexSessionsDirectory(options), opencode: resolveOpencodeDatabasePath(options), openclaw: resolveOpenclawStateDirectory(options), + pi: resolvePiSessionsDirectory(options), hermes: resolveHermesHomeDirectory(options), workbuddy: resolveWorkbuddyProjectsDirectory(options) }).toEqual({ @@ -101,12 +110,13 @@ describe("agent paths", () => { codex: "/Users/alice/.codex/sessions", opencode: "/Users/alice/.local/share/opencode/opencode.db", openclaw: "/Users/alice/.openclaw", + pi: "/Users/alice/.pi/agent/sessions", hermes: "/Users/alice/.hermes", workbuddy: "/Users/alice/.workbuddy/projects" }); }); - it("resolves all seven Agent source paths on Windows", () => { + it("resolves all eight Agent source paths on Windows", () => { const options = { platform: "win32", homeDirectory: "C:\\Users\\alice", @@ -121,6 +131,7 @@ describe("agent paths", () => { codex: resolveCodexSessionsDirectory(options), opencode: resolveOpencodeDatabasePath(options), openclaw: resolveOpenclawStateDirectory(options), + pi: resolvePiSessionsDirectory(options), hermes: resolveHermesHomeDirectory(options), workbuddy: resolveWorkbuddyProjectsDirectory(options) }).toEqual({ @@ -129,6 +140,7 @@ describe("agent paths", () => { codex: "C:\\Users\\alice\\.codex\\sessions", opencode: "C:\\Users\\alice\\.local\\share\\opencode\\opencode.db", openclaw: "C:\\Users\\alice\\.openclaw", + pi: "C:\\Users\\alice\\.pi\\agent\\sessions", hermes: "C:\\Users\\alice\\.hermes", workbuddy: "C:\\Users\\alice\\.workbuddy\\projects" }); diff --git a/App/backend/src/adapters/outbound/skill-writer/pi/index.ts b/App/backend/src/adapters/outbound/skill-writer/pi/index.ts new file mode 100644 index 000000000..7887ec512 --- /dev/null +++ b/App/backend/src/adapters/outbound/skill-writer/pi/index.ts @@ -0,0 +1,2 @@ +/** Pi module. */ +export { createPiSkillTarget } from "./target.js"; diff --git a/App/backend/src/adapters/outbound/skill-writer/pi/target.ts b/App/backend/src/adapters/outbound/skill-writer/pi/target.ts new file mode 100644 index 000000000..425f781f4 --- /dev/null +++ b/App/backend/src/adapters/outbound/skill-writer/pi/target.ts @@ -0,0 +1,141 @@ +/** Pi skill target module. */ +import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import { basename, dirname, join } from "node:path"; +import { resolvePiHomeDirectory } from "../../agent-paths.js"; +import { readMemmyMemoryServiceConfig } from "../memmy-runtime-config.js"; +import { removeMemmySkillDirectory, replaceMemmySkillDirectory } from "../skill-directory.js"; +import { renderMemmyPiExtension } from "../templates/memmy-pi-extension.js"; +import { renderMemmyPluginSkillManifest } from "../templates/memmy-plugin.js"; +import { renderMemmySkillBootstrapManifest } from "../templates/memmy-skill-directory.js"; +import type { SkillManifest, SkillTarget } from "../types.js"; + +const PI_TARGET_ID = "pi"; +const START_MARKER = ""; +const END_MARKER = ""; +const TARGET_FILE_NAME = "AGENTS.md"; +const EXTENSION_DIRECTORY_NAME = "extensions"; +const EXTENSION_FILE_NAME = "memmy-memory.ts"; +const CONFIG_FILE_NAME = "memmy-memory-config.json"; + +export interface CreatePiSkillTargetDeps { + rootDirectory?: string; + memmyConfigPath?: string; +} + +export function createPiSkillTarget(deps: CreatePiSkillTargetDeps = {}): SkillTarget { + const rootDirectory = deps.rootDirectory ?? resolvePiHomeDirectory(); + const memmyConfigPath = deps.memmyConfigPath ?? join(homedir(), ".memmy", "config.yaml"); + + return { + targetId: PI_TARGET_ID, + displayName: "Pi", + async resolveRootDirectory() { + return resolveExistingDirectory(rootDirectory); + }, + async install(manifest) { + const root = await requirePiRoot(rootDirectory); + await installSkill(root, manifest); + }, + async uninstall() { + const root = await resolveExistingDirectory(rootDirectory); + if (!root) return; + await removeBootstrap(root); + await removeMemmySkillDirectory(root); + }, + async isInstalled() { + const root = await resolveExistingDirectory(rootDirectory); + if (!root) return false; + return (await readTextFile(join(root, TARGET_FILE_NAME))).includes(START_MARKER); + }, + async installPlugin() { + const root = await requirePiRoot(rootDirectory); + const extensionDirectory = join(root, EXTENSION_DIRECTORY_NAME); + await mkdir(extensionDirectory, { recursive: true }); + await writeFileAtomically(join(extensionDirectory, EXTENSION_FILE_NAME), renderMemmyPiExtension()); + await writeFileAtomically( + join(extensionDirectory, CONFIG_FILE_NAME), + `${JSON.stringify({ + memmy_config_path: memmyConfigPath, + ...(await readMemmyMemoryServiceConfig(memmyConfigPath)) + }, null, 2)}\n` + ); + await installSkill(root, renderMemmyPluginSkillManifest(PI_TARGET_ID)); + }, + async uninstallPlugin() { + const root = await resolveExistingDirectory(rootDirectory); + if (!root) return; + await rm(join(root, EXTENSION_DIRECTORY_NAME, EXTENSION_FILE_NAME), { force: true }); + await rm(join(root, EXTENSION_DIRECTORY_NAME, CONFIG_FILE_NAME), { force: true }); + await removeBootstrap(root); + await removeMemmySkillDirectory(root); + } + }; +} + +async function installSkill(root: string, manifest: SkillManifest): Promise { + const filePath = join(root, TARGET_FILE_NAME); + const existing = await readTextFile(filePath); + await writeFileAtomically(filePath, upsertMarkerBlock(existing, renderMemmySkillBootstrapManifest(manifest))); + await replaceMemmySkillDirectory(root, manifest); +} + +async function removeBootstrap(root: string): Promise { + const filePath = join(root, TARGET_FILE_NAME); + const existing = await readTextFile(filePath); + if (existing.includes(START_MARKER)) { + await writeFileAtomically(filePath, existing.replace(markerPattern(), "")); + } +} + +function upsertMarkerBlock(existing: string, manifest: SkillManifest): string { + const block = `${manifest.marker}\n${manifest.content.trimEnd()}\n${END_MARKER}\n`; + if (markerPattern().test(existing)) { + return existing.replace(markerPattern(), block); + } + const separator = existing && !existing.endsWith("\n") ? "\n" : ""; + return `${existing}${separator}${block}`; +} + +function markerPattern(): RegExp { + return new RegExp(`${escapeRegExp(START_MARKER)}\\n[\\s\\S]*?${escapeRegExp(END_MARKER)}\\n?`, "m"); +} + +async function requirePiRoot(directory: string): Promise { + const root = await resolveExistingDirectory(directory); + if (!root) throw new Error("Pi is not installed or its directory is unavailable"); + return root; +} + +async function resolveExistingDirectory(directory: string): Promise { + try { + return (await stat(directory)).isDirectory() ? directory : null; + } catch (error) { + if (isNodeError(error) && error.code === "ENOENT") return null; + throw error; + } +} + +async function readTextFile(filePath: string): Promise { + try { + return await readFile(filePath, "utf8"); + } catch (error) { + if (isNodeError(error) && error.code === "ENOENT") return ""; + throw error; + } +} + +async function writeFileAtomically(filePath: string, content: string): Promise { + await mkdir(dirname(filePath), { recursive: true }); + const tempPath = join(dirname(filePath), `.${basename(filePath)}.${process.pid}.${Date.now()}.tmp`); + await writeFile(tempPath, content, "utf8"); + await rename(tempPath, filePath); +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function isNodeError(error: unknown): error is NodeJS.ErrnoException { + return error instanceof Error && "code" in error; +} diff --git a/App/backend/src/adapters/outbound/skill-writer/pi/tests/target.test.ts b/App/backend/src/adapters/outbound/skill-writer/pi/tests/target.test.ts new file mode 100644 index 000000000..7ca772a13 --- /dev/null +++ b/App/backend/src/adapters/outbound/skill-writer/pi/tests/target.test.ts @@ -0,0 +1,249 @@ +/** Pi skill target tests. */ +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; +import { afterEach, describe, expect, it } from "vitest"; +import { createPiSkillTarget } from "../index.js"; +import { renderMemmyDefaultSkillManifest } from "../../templates/memmy-default.js"; + +let tempDirectory: string | undefined; + +afterEach(() => { + if (tempDirectory) { + rmSync(tempDirectory, { recursive: true, force: true }); + tempDirectory = undefined; + } +}); + +describe("Pi skill target", () => { + it("installs the Pi extension, config, bootstrap, and skill idempotently", async () => { + const fixture = createFixture(); + const target = createPiSkillTarget(fixture); + writeFileSync(join(fixture.rootDirectory, "AGENTS.md"), "manual instructions\n", "utf8"); + + await target.installPlugin?.("pi"); + await target.installPlugin?.("pi"); + + const extension = readFileSync(join(fixture.rootDirectory, "extensions", "memmy-memory.ts"), "utf8"); + expect(extension).toContain('pi.on("before_agent_start"'); + expect(extension).toContain('pi.on("agent_settled"'); + expect(extension).toContain('pi.on("input"'); + expect(extension).toContain('pi.registerCommand("memmy-resume"'); + expect(extension).not.toContain('pi.on("agent_end"'); + const config = JSON.parse(readFileSync(join(fixture.rootDirectory, "extensions", "memmy-memory-config.json"), "utf8")); + expect(config).toEqual({ + memmy_config_path: fixture.memmyConfigPath, + endpoint: "http://127.0.0.1:18960", + token: "test-token" + }); + const agents = readFileSync(join(fixture.rootDirectory, "AGENTS.md"), "utf8"); + expect(agents.match(//gu)).toHaveLength(1); + expect(agents).toContain("manual instructions"); + expect(readFileSync(join(fixture.rootDirectory, "skills", "memmy-memory", "SKILL.md"), "utf8")) + .toContain("A Memmy Memory Hook or plugin is installed for this agent."); + }); + + it("uninstalls only Memmy-owned files", async () => { + const fixture = createFixture(); + const target = createPiSkillTarget(fixture); + const unrelatedExtension = join(fixture.rootDirectory, "extensions", "unrelated.ts"); + writeFileSync(unrelatedExtension, "export default () => {};\n", "utf8"); + await target.installPlugin?.("pi"); + + await target.uninstallPlugin?.("pi"); + + expect(existsSync(unrelatedExtension)).toBe(true); + expect(existsSync(join(fixture.rootDirectory, "extensions", "memmy-memory.ts"))).toBe(false); + expect(existsSync(join(fixture.rootDirectory, "skills", "memmy-memory"))).toBe(false); + expect(readFileSync(join(fixture.rootDirectory, "AGENTS.md"), "utf8")).toBe(""); + }); + + it("does not create the Pi directory when Pi is unavailable", async () => { + tempDirectory = mkdtempSync(join(tmpdir(), "memmy-pi-missing-")); + const rootDirectory = join(tempDirectory, ".pi", "agent"); + const target = createPiSkillTarget({ rootDirectory }); + await expect(target.install(renderMemmyDefaultSkillManifest("pi"))).rejects.toThrow("Pi is not installed"); + expect(existsSync(rootDirectory)).toBe(false); + }); + + it("awaits settled capture, preserves status, redacts secrets, and marks handled entries", async () => { + const fixture = createFixture(); + const requests: Array<{ path: string; body: Record }> = []; + let releaseComplete: (() => void) | undefined; + const completeGate = new Promise((resolve) => { + releaseComplete = resolve; + }); + const originalFetch = globalThis.fetch; + globalThis.fetch = async (input, init) => { + const path = new URL(input instanceof Request ? input.url : String(input)).pathname; + const body = init?.body ? JSON.parse(String(init.body)) as Record : {}; + requests.push({ path, body }); + if (path === "/api/v1/sessions/open") return jsonResponse({ sessionId: "pi-memory-session" }); + if (path === "/api/v1/turns/start") return jsonResponse({ + turnId: "pi-live-turn", + episodeId: "pi-episode", + sourceMemoryIds: ["memory-1"], + injectedContext: { markdown: "prior context" } + }); + if (path === "/api/v1/turns/pi-live-turn/complete") { + await completeGate; + return jsonResponse({ turnId: "pi-live-turn" }); + } + return jsonResponse({}, 404); + }; + const target = createPiSkillTarget(fixture); + await target.installPlugin?.("pi"); + + try { + const extensionPath = join(fixture.rootDirectory, "extensions", "memmy-memory.ts"); + const extensionModule = await import(`${pathToFileURL(extensionPath).href}?test=${crypto.randomUUID()}`) as { + default: (pi: unknown) => void; + }; + const handlers = new Map unknown>(); + const markers: Array<{ customType: string; data: unknown }> = []; + extensionModule.default({ + on(event: string, handler: (...args: never[]) => unknown) { + handlers.set(event, handler); + }, + registerCommand() {}, + appendEntry(customType: string, data: unknown) { + markers.push({ customType, data }); + } + }); + const secret = "sk-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN"; + const branch = [ + sessionEntry("parent", null, "system", "setup"), + sessionEntry("user-1", "parent", "user", `First ${secret}`), + sessionEntry("assistant-1", "user-1", "assistant", "Partial answer"), + sessionEntry("user-2", "assistant-1", "user", "Follow-up password=hunter2"), + sessionEntry("assistant-2", "user-2", "assistant", "", "error", `Failed with ${secret}`) + ]; + const context = extensionContext(branch, "parent"); + await handlers.get("before_agent_start")?.({ prompt: `First ${secret}` }, context as never); + + let settled = false; + const settledPromise = Promise.resolve(handlers.get("agent_settled")?.({}, context as never)).then(() => { + settled = true; + }); + await waitFor(() => requests.some((item) => item.path.endsWith("/complete"))); + expect(settled).toBe(false); + releaseComplete?.(); + await settledPromise; + + expect(requests.find((item) => item.path.endsWith("/start"))?.body.query).toBe("First [REDACTED:openai_api_key]"); + expect(requests.find((item) => item.path.endsWith("/complete"))?.body).toMatchObject({ + query: "First [REDACTED:openai_api_key]\n\nFollow-up password=[REDACTED:password]", + answer: "Partial answer", + status: "failed" + }); + expect(markers).toEqual([expect.objectContaining({ + customType: "memmy-memory-capture", + data: expect.objectContaining({ entryIds: ["user-1", "assistant-1", "user-2", "assistant-2"], status: "failed" }) + })]); + } finally { + releaseComplete?.(); + globalThis.fetch = originalFetch; + } + }); + + it("does not complete aborted runs but marks their entries handled", async () => { + const fixture = createFixture(); + const originalFetch = globalThis.fetch; + globalThis.fetch = async (input) => { + const path = new URL(input instanceof Request ? input.url : String(input)).pathname; + if (path === "/api/v1/sessions/open") return jsonResponse({ sessionId: "pi-memory-session" }); + if (path === "/api/v1/turns/start") return jsonResponse({ + turnId: "pi-aborted-turn", + episodeId: "pi-episode", + injectedContext: { markdown: "" } + }); + throw new Error(`Unexpected request: ${path}`); + }; + const target = createPiSkillTarget(fixture); + await target.installPlugin?.("pi"); + const extensionPath = join(fixture.rootDirectory, "extensions", "memmy-memory.ts"); + const extensionModule = await import(`${pathToFileURL(extensionPath).href}?test=${crypto.randomUUID()}`) as { + default: (pi: unknown) => void; + }; + const handlers = new Map unknown>(); + const markers: Array<{ customType: string; data: unknown }> = []; + extensionModule.default({ + on(event: string, handler: (...args: never[]) => unknown) { + handlers.set(event, handler); + }, + registerCommand() {}, + appendEntry(customType: string, data: unknown) { + markers.push({ customType, data }); + } + }); + try { + const branch = [ + sessionEntry("parent", null, "system", "setup"), + sessionEntry("user-1", "parent", "user", "cancel me"), + sessionEntry("assistant-1", "user-1", "assistant", "partial", "aborted") + ]; + const context = extensionContext(branch, "parent"); + await handlers.get("before_agent_start")?.({ prompt: "cancel me" }, context as never); + await handlers.get("agent_settled")?.({}, context as never); + expect(markers).toEqual([expect.objectContaining({ data: expect.objectContaining({ status: "aborted" }) })]); + } finally { + globalThis.fetch = originalFetch; + } + }); +}); + +function createFixture(): { rootDirectory: string; memmyConfigPath: string } { + tempDirectory = mkdtempSync(join(tmpdir(), "memmy-pi-target-")); + const rootDirectory = join(tempDirectory, ".pi", "agent"); + const memmyConfigPath = join(tempDirectory, ".memmy", "config.yaml"); + mkdirSync(join(rootDirectory, "extensions"), { recursive: true }); + mkdirSync(join(tempDirectory, ".memmy"), { recursive: true }); + writeFileSync(memmyConfigPath, 'storage:\n endpoint: "http://127.0.0.1:18960"\n token: "test-token"\n', "utf8"); + return { rootDirectory, memmyConfigPath }; +} + +function sessionEntry( + id: string, + parentId: string | null, + role: string, + text: string, + stopReason = "stop", + errorMessage?: string +): Record { + return { + type: "message", + id, + parentId, + timestamp: new Date().toISOString(), + message: { + role, + content: [{ type: "text", text }], + ...(role === "assistant" ? { stopReason, errorMessage } : {}) + } + }; +} + +function extensionContext(branch: Array>, leafId: string): Record { + return { + cwd: "/tmp/pi-project", + ui: { notify() {} }, + sessionManager: { + getSessionId: () => "pi-session-1", + getLeafId: () => leafId, + getBranch: () => branch + } + }; +} + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }); +} + +async function waitFor(predicate: () => boolean): Promise { + for (let attempt = 0; attempt < 100; attempt += 1) { + if (predicate()) return; + await new Promise((resolve) => setTimeout(resolve, 5)); + } + throw new Error("Timed out waiting for condition"); +} diff --git a/App/backend/src/adapters/outbound/skill-writer/templates/memmy-pi-extension.ts b/App/backend/src/adapters/outbound/skill-writer/templates/memmy-pi-extension.ts new file mode 100644 index 000000000..4bc682651 --- /dev/null +++ b/App/backend/src/adapters/outbound/skill-writer/templates/memmy-pi-extension.ts @@ -0,0 +1,461 @@ +/** Pi Memmy extension template. */ + +export function renderMemmyPiExtension(): string { + return String.raw`import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; +import { readFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +const SOURCE = "pi"; +const CONFIG_URL = new URL("./memmy-memory-config.json", import.meta.url); +const DEFAULT_MEMMY_CONFIG_PATH = join(homedir(), ".memmy", "config.yaml"); +const FETCH_TIMEOUT_MS = 45000; +const SEARCH_LIMIT = 20; +const DISPLAY_LIMIT = 5; +const RESUME_STATE_TTL_MS = 10 * 60 * 1000; +const RESUME_CONTEXT_MAX_CHARS = 24000; + +export default function memmyPiExtension(pi: ExtensionAPI): void { + const pendingTurns = new Map(); + let pendingResume: PendingResume | null = null; + let selectedResumeContext = ""; + let captureQueue = Promise.resolve(); + let turnSequence = 0; + + pi.on("before_agent_start", async (event, ctx) => { + const query = sanitizeText(event.prompt); + if (!query || isResumeCommand(query)) { + return; + } + let injectedContext = selectedResumeContext; + selectedResumeContext = ""; + const startParentId = ctx.sessionManager.getLeafId(); + turnSequence += 1; + const requestedTurnId = "pi-turn-" + hashText([ + ctx.sessionManager.getSessionId(), + startParentId || "root", + query, + String(turnSequence) + ].join("\u0000")); + try { + const memmy = await createMemmyClient(); + const externalSessionId = "pi-memory-" + ctx.sessionManager.getSessionId(); + const opened = await memmy.post("/api/v1/sessions/open", { + sessionId: externalSessionId, + source: SOURCE, + workspacePath: ctx.cwd || undefined + }); + const sessionId = normalizeText(opened.sessionId) || externalSessionId; + const turn = await memmy.post("/api/v1/turns/start", { + adapterId: "memmy-pi-extension", + requestId: "pi-start:" + requestedTurnId, + sessionId, + turnId: requestedTurnId, + query: redactSecrets(query), + source: SOURCE + }); + pendingTurns.set(requestedTurnId, { + sessionId, + turnId: normalizeText(turn.turnId) || requestedTurnId, + episodeId: normalizeText(turn.episodeId) || undefined, + sourceMemoryIds: Array.isArray(turn.sourceMemoryIds) ? turn.sourceMemoryIds : undefined, + initialQuery: query, + startParentId + }); + const recalled = normalizeText(turn.injectedContext && turn.injectedContext.markdown); + injectedContext = [injectedContext, recalled].filter(Boolean).join("\n\n"); + } catch { + pendingTurns.delete(requestedTurnId); + } + + if (injectedContext) { + return { + message: { + customType: "memmy-memory-context", + content: renderMemoryContext(injectedContext, query), + display: true + } + }; + } + }); + + pi.on("agent_settled", async (_event, ctx) => { + const captures = settledCaptures(ctx, pendingTurns); + if (!captures.length) { + return; + } + for (const capture of captures) { + pendingTurns.delete(capture.pendingKey); + if (capture.stopReason === "aborted") { + markSessionEntriesHandled(pi, capture.entryIds, "aborted"); + continue; + } + if (!capture.answer) { + continue; + } + const job = captureQueue.then(async () => { + await completeTurn(capture.turn, capture.query, capture.answer, capture.status); + markSessionEntriesHandled(pi, capture.entryIds, capture.status); + }); + captureQueue = job.catch(() => undefined); + await job.catch(() => undefined); + } + }); + + pi.on("session_shutdown", async () => { + pendingTurns.clear(); + pendingResume = null; + selectedResumeContext = ""; + await captureQueue; + }); + + pi.on("input", async (event, ctx) => { + if (event.source === "extension" || !/^[1-5]$/u.test(event.text.trim())) { + return { action: "continue" }; + } + const selection = Number(event.text.trim()); + const state = pendingResume; + if (!state || Date.now() - state.createdAt > RESUME_STATE_TTL_MS) { + pendingResume = null; + return { action: "continue" }; + } + const candidate = state.candidates.find((item) => item.index === selection); + if (!candidate) { + return { action: "continue" }; + } + try { + const memmy = await createMemmyClient(); + const detail = await memmy.get("/api/v1/memory/" + encodeURIComponent(candidate.episodeId)); + pendingResume = null; + selectedResumeContext = buildResumeContext(candidate, detail); + ctx.ui.notify("Resuming Memmy episode " + candidate.episodeId, "info"); + return { + action: "transform", + text: "Continue Memmy episode " + candidate.episodeId + ": " + (candidate.title || candidate.episodeId), + images: event.images + }; + } catch (error) { + ctx.ui.notify("Memmy resume failed: " + formatError(error), "warning"); + return { action: "handled" }; + } + }); + + pi.registerCommand("memmy-resume", { + description: "Find and resume a prior Memmy episode", + handler: async (args, ctx) => { + const query = normalizeText(args); + if (!query) { + ctx.ui.notify("Usage: /memmy-resume ", "warning"); + return; + } + if (query === "cancel") { + pendingResume = null; + ctx.ui.notify("Memmy resume selection cancelled.", "info"); + return; + } + try { + const memmy = await createMemmyClient(); + const result = await memmy.post("/api/v1/memory/search", { + query, + layers: ["L1"], + limit: SEARCH_LIMIT, + verbose: true, + source: SOURCE + }); + const candidates = await buildEpisodeCandidates(memmy, result); + pendingResume = { createdAt: Date.now(), candidates }; + ctx.ui.notify(formatResumeCandidates(query, candidates), "info"); + } catch (error) { + ctx.ui.notify("Memmy resume search failed: " + formatError(error), "warning"); + } + } + }); +} + +interface PendingTurn { + sessionId: string; + turnId: string; + episodeId?: string; + sourceMemoryIds?: unknown[]; + initialQuery: string; + startParentId: string | null; +} + +interface SettledCapture { + pendingKey: string; + turn: PendingTurn; + query: string; + answer: string; + status: "succeeded" | "failed"; + stopReason: string; + entryIds: string[]; +} + +interface ResumeCandidate { + index: number; + episodeId: string; + title: string; + summary: string; +} + +interface PendingResume { + createdAt: number; + candidates: ResumeCandidate[]; +} + +async function completeTurn( + turn: PendingTurn, + query: string, + answer: string, + status: "succeeded" | "failed" +): Promise { + const memmy = await createMemmyClient(); + await memmy.post("/api/v1/turns/" + encodeURIComponent(turn.turnId) + "/complete", { + adapterId: "memmy-pi-extension", + requestId: "pi-complete:" + turn.turnId + ":" + hashText(answer), + sessionId: turn.sessionId, + episodeId: turn.episodeId, + query: redactSecrets(query), + answer: redactSecrets(answer), + status, + source: SOURCE, + sourceMemoryIds: turn.sourceMemoryIds + }); +} + +function settledCaptures(ctx: ExtensionContext, pendingTurns: Map): SettledCapture[] { + const branch = ctx.sessionManager.getBranch(); + const claimedUserEntryIds = new Set(); + const located = [...pendingTurns].flatMap(([pendingKey, turn]) => { + const parentIndex = turn.startParentId ? branch.findIndex((entry) => entry.id === turn.startParentId) : -1; + const firstUserIndex = branch.findIndex((entry, index) => + index > parentIndex && + entry.type === "message" && + entry.message.role === "user" && + !claimedUserEntryIds.has(entry.id) && + messageText(entry.message) === turn.initialQuery + ); + if (firstUserIndex < 0) return []; + claimedUserEntryIds.add(branch[firstUserIndex]!.id); + return [{ pendingKey, turn, firstUserIndex }]; + }).sort((left, right) => left.firstUserIndex - right.firstUserIndex); + const captures: SettledCapture[] = []; + for (const [index, current] of located.entries()) { + const nextStartIndex = located[index + 1]?.firstUserIndex ?? branch.length; + const runEntries = branch.slice(current.firstUserIndex, nextStartIndex).filter((entry) => entry.type === "message"); + const userTexts = runEntries + .filter((entry) => entry.type === "message" && entry.message.role === "user") + .map((entry) => entry.type === "message" ? messageText(entry.message) : "") + .filter(Boolean); + const assistantEntries = runEntries.filter((entry) => + entry.type === "message" && entry.message.role === "assistant" + ); + const lastAssistant = assistantEntries.at(-1); + if (!lastAssistant || lastAssistant.type !== "message" || lastAssistant.message.role !== "assistant") { + continue; + } + const stopReason = normalizeText(lastAssistant.message.stopReason); + const assistantTexts = assistantEntries + .map((entry) => entry.type === "message" ? messageText(entry.message) : "") + .filter(Boolean); + const errorMessage = sanitizeText(lastAssistant.message.errorMessage); + captures.push({ + pendingKey: current.pendingKey, + turn: current.turn, + query: userTexts.join("\n\n"), + answer: assistantTexts.join("\n\n") || (stopReason === "error" ? errorMessage : ""), + status: stopReason === "error" ? "failed" : "succeeded", + stopReason, + entryIds: runEntries.map((entry) => entry.id) + }); + } + return captures; +} + +function messageText(message: { content: unknown }): string { + if (typeof message.content === "string") return sanitizeText(message.content); + if (!Array.isArray(message.content)) return ""; + return sanitizeText(message.content + .filter((part): part is { type: "text"; text: string } => isRecord(part) && part.type === "text" && typeof part.text === "string") + .map((part) => part.text) + .join("\n")); +} + +function markSessionEntriesHandled( + pi: ExtensionAPI, + entryIds: string[], + status: "succeeded" | "failed" | "aborted" +): void { + if (!entryIds.length) return; + pi.appendEntry("memmy-memory-capture", { entryIds, status }); +} + +async function buildEpisodeCandidates(memmy: MemmyClient, result: Record): Promise { + const hits = extractHits(result).slice(0, SEARCH_LIMIT); + const candidates = new Map>(); + for (const hit of hits) { + const memoryId = normalizeText(hit.id || hit.memoryId || hit.refId); + if (!memoryId) continue; + const detail = await memmy.get("/api/v1/memory/" + encodeURIComponent(memoryId)).catch(() => ({})); + const refs = isRecord(detail.refs) ? detail.refs : {}; + const episode = isRecord(refs.episode) ? refs.episode : {}; + const episodeId = normalizeText(episode.id || detail.episodeId || hit.episodeId) || + (memoryId.startsWith("episode_") ? memoryId : ""); + if (!episodeId || candidates.has(episodeId)) continue; + candidates.set(episodeId, { + episodeId, + title: normalizeText(episode.title || detail.title || hit.title) || episodeId, + summary: normalizeText(episode.summary || detail.summary || hit.summary || hit.body) + }); + if (candidates.size >= DISPLAY_LIMIT) break; + } + return [...candidates.values()].map((candidate, index) => ({ ...candidate, index: index + 1 })); +} + +function extractHits(result: Record): Record[] { + const debug = isRecord(result.debug) ? result.debug : {}; + for (const value of [result.hits, debug.hits, result.results, debug.results, result.memories, debug.memories]) { + if (Array.isArray(value)) return value.filter(isRecord); + } + return []; +} + +function formatResumeCandidates(query: string, candidates: ResumeCandidate[]): string { + if (!candidates.length) return "No L1 Memmy memories found for: \"" + query + "\""; + return [ + "Memmy resume candidates for \"" + query + "\":", + "", + ...candidates.map((candidate) => candidate.index + ". " + candidate.episodeId + + "\n " + truncate(candidate.title, 160) + + (candidate.summary ? "\n " + truncate(candidate.summary, 260) : "")), + "", + "Enter 1-5 to resume, or /memmy-resume cancel." + ].join("\n"); +} + +function buildResumeContext(candidate: ResumeCandidate, detail: Record): string { + return truncate([ + "The user selected this prior Memmy episode and wants to continue it.", + "Episode id: " + candidate.episodeId, + "Episode title: " + candidate.title, + normalizeText(detail.body) ? "Episode detail:\n" + normalizeText(detail.body) : "", + JSON.stringify(detail, null, 2) + ].filter(Boolean).join("\n\n"), RESUME_CONTEXT_MAX_CHARS); +} + +function renderMemoryContext(markdown: string, query: string): string { + return [ + "", + markdown, + "", + "", + "", + query, + "" + ].join("\n"); +} + +interface MemmyClient { + get(path: string): Promise>; + post(path: string, body: Record): Promise>; +} + +async function createMemmyClient(): Promise { + const localConfig = await readJsonConfig(); + const configPath = normalizeText(process.env.MEMMY_CONFIG) || normalizeText(localConfig.memmy_config_path) || DEFAULT_MEMMY_CONFIG_PATH; + const runtimeConfig = await readYamlConfig(configPath).catch(() => ({})); + const baseUrl = normalizeText(runtimeConfig.endpoint || localConfig.endpoint || "http://127.0.0.1:18960").replace(/\/+$/u, ""); + const token = normalizeText(runtimeConfig.token || localConfig.token); + return { + async get(path) { + return request(new URL(path, baseUrl), { method: "GET", headers: token ? { authorization: "Bearer " + token } : {} }); + }, + async post(path, body) { + const headers: Record = { "content-type": "application/json" }; + if (token) headers.authorization = "Bearer " + token; + return request(new URL(path, baseUrl), { method: "POST", headers, body: JSON.stringify({ ...body, source: SOURCE }) }); + } + }; +} + +async function request(url: URL, init: RequestInit): Promise> { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const response = await fetch(url, { ...init, signal: controller.signal }); + const text = await response.text(); + const data = text ? JSON.parse(text) : {}; + if (!response.ok) throw new Error(normalizeText(data?.error?.message) || response.statusText || "Memmy HTTP " + response.status); + return isRecord(data) ? data : {}; + } finally { + clearTimeout(timeout); + } +} + +async function readJsonConfig(): Promise> { + try { + const value = JSON.parse(await readFile(CONFIG_URL, "utf8")); + return isRecord(value) ? value : {}; + } catch { + return {}; + } +} + +async function readYamlConfig(path: string): Promise> { + const content = await readFile(path, "utf8"); + const values: Record = {}; + for (const line of content.split(/\r?\n/u)) { + const match = line.match(/^\s+(endpoint|token):\s*(.*?)\s*$/u); + if (match && !values[match[1]]) values[match[1]] = match[2].replace(/^['"]|['"]$/gu, ""); + } + return values; +} + +function isResumeCommand(value: string): boolean { + return /^\/?memmy-resume(?:\s|$)/u.test(value.trim()); +} + +function sanitizeText(value: unknown): string { + return normalizeText(value).replace(/\u0000/gu, "").trim(); +} + +function redactSecrets(input: string): string { + return redactBase64Runs(input + .replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/gu, "[REDACTED:ssh_private_key]") + .replace(/\b(Authorization\s*:\s*Bearer\s+)[A-Za-z0-9._~+/=-]+/giu, "$1[REDACTED:authorization_bearer]") + .replace(/\bsk-ant-api\d{2}-[A-Za-z0-9_-]{40,}\b/gu, "[REDACTED:anthropic_api_key]") + .replace(/\bsk-(?:proj-)?[A-Za-z0-9_-]{40,}\b/gu, "[REDACTED:openai_api_key]") + .replace(/\bAIza[A-Za-z0-9_-]{32,}\b/gu, "[REDACTED:google_api_key]") + .replace(/\b([A-Za-z0-9_]*password[A-Za-z0-9_]*\s*[:=]\s*)(?:"[^"\n]+"|'[^'\n]+'|[^\s#&]+)/giu, "$1[REDACTED:password]")); +} + +function redactBase64Runs(input: string): string { + return input.replace(/(^|[^A-Za-z0-9_])([A-Za-z0-9+/]{32,}={0,2})(?=$|[^A-Za-z0-9_])/gu, "$1[REDACTED:base64_secret]"); +} + +function normalizeText(value: unknown): string { + return typeof value === "string" ? value.trim() : ""; +} + +function truncate(value: string, limit: number): string { + return value.length <= limit ? value : value.slice(0, limit - 1) + "…"; +} + +function hashText(value: string): string { + let hash = 2166136261; + for (let index = 0; index < value.length; index += 1) { + hash ^= value.charCodeAt(index); + hash = Math.imul(hash, 16777619); + } + return (hash >>> 0).toString(16); +} + +function formatError(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} +`; +} diff --git a/App/backend/src/analytics/agent-source-analytics.ts b/App/backend/src/analytics/agent-source-analytics.ts index d8b89b893..955c2856e 100644 --- a/App/backend/src/analytics/agent-source-analytics.ts +++ b/App/backend/src/analytics/agent-source-analytics.ts @@ -29,7 +29,7 @@ export type AgentSourceInstallType = export type AgentSourceKind = "hook" | "native_plugin" | "skill" | "managed_skill"; const HOOK_AGENT_SOURCE_IDS = new Set(["cursor", "claude_code", "codex"]); -const NATIVE_PLUGIN_AGENT_SOURCE_IDS = new Set(["opencode", "openclaw", "hermes"]); +const NATIVE_PLUGIN_AGENT_SOURCE_IDS = new Set(["pi", "opencode", "openclaw", "hermes"]); const AGENT_SOURCE_ANALYTICS_SOURCE = "memmy-backend"; export type AgentSourceLifecycleAnalytics = { diff --git a/App/backend/src/services/agent-source-auto-inject-service.ts b/App/backend/src/services/agent-source-auto-inject-service.ts index 1ae9ac98c..053097043 100644 --- a/App/backend/src/services/agent-source-auto-inject-service.ts +++ b/App/backend/src/services/agent-source-auto-inject-service.ts @@ -3,8 +3,8 @@ import type { AgentSourceAutoInjectResult, ScanPreferences } from "@memmy/local- import type { PermissionManager } from "../permission/index.js"; import type { AgentSourceService } from "./agent-source-service.js"; -const AUTO_INJECT_AGENT_SOURCE_IDS = new Set(["cursor", "claude_code", "codex", "opencode", "openclaw", "hermes", "workbuddy"]); -const HOOK_OR_PLUGIN_AGENT_SOURCE_IDS = new Set(["cursor", "claude_code", "codex", "opencode", "openclaw", "hermes"]); +const AUTO_INJECT_AGENT_SOURCE_IDS = new Set(["cursor", "claude_code", "codex", "pi", "opencode", "openclaw", "hermes", "workbuddy"]); +const HOOK_OR_PLUGIN_AGENT_SOURCE_IDS = new Set(["cursor", "claude_code", "codex", "pi", "opencode", "openclaw", "hermes"]); export interface AgentSourceAutoInjectService { runOnce(): Promise; diff --git a/App/backend/src/services/builtin-agent-source-registry.ts b/App/backend/src/services/builtin-agent-source-registry.ts index d88cc177b..f9c84c326 100644 --- a/App/backend/src/services/builtin-agent-source-registry.ts +++ b/App/backend/src/services/builtin-agent-source-registry.ts @@ -4,6 +4,7 @@ import { createCursorSourceAdapter } from "../adapters/outbound/agent-source/cur import { createHermesSourceAdapter } from "../adapters/outbound/agent-source/hermes/index.js"; import { createOpenclawSourceAdapter } from "../adapters/outbound/agent-source/openclaw/index.js"; import { createOpencodeSourceAdapter } from "../adapters/outbound/agent-source/opencode/index.js"; +import { createPiSourceAdapter } from "../adapters/outbound/agent-source/pi/index.js"; import { createSourceRegistry, type SourceRegistry } from "../adapters/outbound/agent-source/source-registry.js"; import { createWorkbuddySourceAdapter } from "../adapters/outbound/agent-source/workbuddy/index.js"; @@ -12,6 +13,7 @@ export function createBuiltinAgentSourceRegistry(): SourceRegistry { createCursorSourceAdapter(), createClaudeCodeSourceAdapter(), createCodexSourceAdapter(), + createPiSourceAdapter(), createOpencodeSourceAdapter(), createOpenclawSourceAdapter(), createHermesSourceAdapter(), diff --git a/App/backend/src/services/index.ts b/App/backend/src/services/index.ts index 901795569..325ad319b 100644 --- a/App/backend/src/services/index.ts +++ b/App/backend/src/services/index.ts @@ -15,6 +15,7 @@ import { createCursorSkillTarget } from "../adapters/outbound/skill-writer/curso import { createHermesSkillTarget } from "../adapters/outbound/skill-writer/hermes/index.js"; import { createOpenclawSkillTarget } from "../adapters/outbound/skill-writer/openclaw/index.js"; import { createOpencodeSkillTarget } from "../adapters/outbound/skill-writer/opencode/index.js"; +import { createPiSkillTarget } from "../adapters/outbound/skill-writer/pi/index.js"; import { createWorkbuddySkillTarget } from "../adapters/outbound/skill-writer/workbuddy/index.js"; import { createSkillTargetRegistry, type SkillTargetRegistry } from "../adapters/outbound/skill-writer/target-registry.js"; import type { CloudClient } from "../adapters/outbound/cloud-client/index.js"; @@ -126,6 +127,7 @@ export function createBackendServices(options: CreateBackendServicesOptions): Ba createCursorSkillTarget({ memmyConfigPath: options.memmyConfigPath }), createClaudeCodeSkillTarget({ memmyConfigPath: options.memmyConfigPath }), createCodexSkillTarget({ memmyConfigPath: options.memmyConfigPath }), + createPiSkillTarget({ memmyConfigPath: options.memmyConfigPath }), createOpencodeSkillTarget(), createOpenclawSkillTarget({ memmyConfigPath: options.memmyConfigPath }), createHermesSkillTarget({ memmyConfigPath: options.memmyConfigPath }), diff --git a/App/backend/src/services/tests/agent-source-auto-inject-service.test.ts b/App/backend/src/services/tests/agent-source-auto-inject-service.test.ts index 74892a835..a229a0113 100644 --- a/App/backend/src/services/tests/agent-source-auto-inject-service.test.ts +++ b/App/backend/src/services/tests/agent-source-auto-inject-service.test.ts @@ -39,11 +39,12 @@ describe("agent source auto inject service", () => { await expect(service.runOnce()).resolves.toEqual({ ok: true, skipped: false, - installed: ["cursor", "opencode", "openclaw", "workbuddy"], + installed: ["cursor", "pi", "opencode", "openclaw", "workbuddy"], failed: [] }); expect(calls).toEqual([ "plugin:cursor:auto_inject", + "plugin:pi:auto_inject", "plugin:opencode:auto_inject", "plugin:openclaw:auto_inject", "skill:workbuddy", @@ -113,6 +114,7 @@ function createAgentSources(calls: string[]) { return [ source("cursor", "not_connected", true), source("codex", "skill_installed", true), + source("pi", "not_connected", true), source("opencode", "not_connected", true), source("openclaw", "not_connected", true), source("workbuddy", "not_connected", true), diff --git a/App/backend/src/services/tests/builtin-agent-source-registry.test.ts b/App/backend/src/services/tests/builtin-agent-source-registry.test.ts index 6177ef323..7dcccec7f 100644 --- a/App/backend/src/services/tests/builtin-agent-source-registry.test.ts +++ b/App/backend/src/services/tests/builtin-agent-source-registry.test.ts @@ -9,6 +9,7 @@ describe("built-in agent source registry", () => { "cursor", "claude_code", "codex", + "pi", "opencode", "openclaw", "hermes", diff --git a/App/frontend/desktop/src/i18n/messages.ts b/App/frontend/desktop/src/i18n/messages.ts index d3c6284cf..22155c092 100644 --- a/App/frontend/desktop/src/i18n/messages.ts +++ b/App/frontend/desktop/src/i18n/messages.ts @@ -310,7 +310,7 @@ export const zhCNMessages = { "onboarding.permission.title": "Memmy 需要你的授权", "onboarding.permission.subtitle": "为了让所有 AI 都记住同一个你", "onboarding.permission.scanTitle": "扫描已有 Agent 对话", - "onboarding.permission.scanBody": "读取 Cursor / Codex / WorkBuddy 等本地历史对话,生成你的记忆", + "onboarding.permission.scanBody": "读取 Cursor / Codex / Pi / WorkBuddy 等本地历史对话,生成你的记忆", "onboarding.permission.writeTitle": "允许其他Agent使用Memmy的记忆", "onboarding.permission.writeBody": "将会通过插件 / CLI / 修改 AGENTS.md 等方式引导消费记忆", "onboarding.permission.notice": "你可以随时在「记忆管理 -> 接入源管理」中调整授权", @@ -678,7 +678,7 @@ export const zhCNMessages = { "memory.daemonStopped": "已停止", "memory.preferences": "扫描行为", "memory.autoScan": "自动扫描已知 Agent", - "memory.autoScanDescription": "启动时扫描 Cursor / Codex / Claude Code / WorkBuddy 等已安装 Agent 的新对话", + "memory.autoScanDescription": "启动时扫描 Cursor / Codex / Pi / Claude Code / WorkBuddy 等已安装 Agent 的新对话", "memory.watchFiles": "自动增量同步", "memory.watchFilesDescription": "自动跟进 Agent 会话文件的新增内容", "memory.autoInject": "新发现 Agent 自动安装 Hook/插件", @@ -1670,7 +1670,7 @@ export const enUSMessages: Record = { "onboarding.permission.title": "Memmy needs your authorization", "onboarding.permission.subtitle": "Let all AI remember the same you", "onboarding.permission.scanTitle": "Scan existing Agent conversations", - "onboarding.permission.scanBody": "Read local Cursor / Codex / WorkBuddy histories and generate your memory", + "onboarding.permission.scanBody": "Read local Cursor / Codex / Pi / WorkBuddy histories and generate your memory", "onboarding.permission.writeTitle": "Allow other Agents to use Memmy memory", "onboarding.permission.writeBody": "Guide memory consumption through plugins / CLI / modifying AGENTS.md, etc.", "onboarding.permission.notice": "You can change this later in Memory -> Sources", @@ -2038,7 +2038,7 @@ export const enUSMessages: Record = { "memory.daemonStopped": "Stopped", "memory.preferences": "Scan behavior", "memory.autoScan": "Auto-scan known Agents", - "memory.autoScanDescription": "Scan new conversations from installed Agents such as Cursor / Codex / Claude Code / WorkBuddy on startup", + "memory.autoScanDescription": "Scan new conversations from installed Agents such as Cursor / Codex / Pi / Claude Code / WorkBuddy on startup", "memory.watchFiles": "Auto incremental sync", "memory.watchFilesDescription": "Automatically follow newly written Agent conversation files", "memory.autoInject": "Auto-install Hooks/plugins for new Agents", diff --git a/App/frontend/desktop/src/pages/agent-source-logos.ts b/App/frontend/desktop/src/pages/agent-source-logos.ts index 527cde235..fff93c67d 100644 --- a/App/frontend/desktop/src/pages/agent-source-logos.ts +++ b/App/frontend/desktop/src/pages/agent-source-logos.ts @@ -12,6 +12,7 @@ export const MEMORY_AGENT_SOURCE_VALUES = [ "cursor", "claude_code", "codex", + "pi", "opencode", "openclaw", "hermes", @@ -24,6 +25,7 @@ const AGENT_SOURCE_DISPLAY_NAMES: Record = { cursor: "Cursor", claude_code: "Claude Code", codex: "Codex", + pi: "Pi", opencode: "OpenCode", openclaw: "OpenClaw", hermes: "Hermes", diff --git a/App/frontend/desktop/src/pages/first-encounter-relay-challenge.tsx b/App/frontend/desktop/src/pages/first-encounter-relay-challenge.tsx index 057362682..61b2e1990 100644 --- a/App/frontend/desktop/src/pages/first-encounter-relay-challenge.tsx +++ b/App/frontend/desktop/src/pages/first-encounter-relay-challenge.tsx @@ -22,7 +22,7 @@ export interface FirstEncounterRelayOptInProps { onOpenConnections: () => void; } -const RELAY_AGENT_IDS = new Set(["cursor", "claude_code", "codex", "opencode", "openclaw", "hermes", "workbuddy"]); +const RELAY_AGENT_IDS = new Set(["cursor", "claude_code", "codex", "pi", "opencode", "openclaw", "hermes", "workbuddy"]); type RelayFeedback = | { kind: "copied" } | { kind: "copy_failed" } diff --git a/App/frontend/desktop/src/pages/memory-sources-page.tsx b/App/frontend/desktop/src/pages/memory-sources-page.tsx index 1c2a24f93..e1f1740eb 100644 --- a/App/frontend/desktop/src/pages/memory-sources-page.tsx +++ b/App/frontend/desktop/src/pages/memory-sources-page.tsx @@ -1253,7 +1253,7 @@ function SourceStatusBadge(props: { source: Pick{t(labelKey)}; } -const NATIVE_PLUGIN_AGENT_SOURCE_IDS = new Set(["opencode", "openclaw", "hermes"]); +const NATIVE_PLUGIN_AGENT_SOURCE_IDS = new Set(["pi", "opencode", "openclaw", "hermes"]); const HOOK_AGENT_SOURCE_IDS = new Set(["codex", "claude_code", "cursor"]); export function resolveAgentSourceStatusLabelKey(source: Pick): MessageKey { diff --git a/App/frontend/desktop/src/pages/memory/tests/memory-runtime-fixtures.ts b/App/frontend/desktop/src/pages/memory/tests/memory-runtime-fixtures.ts index a3f19e090..9b65d30a6 100644 --- a/App/frontend/desktop/src/pages/memory/tests/memory-runtime-fixtures.ts +++ b/App/frontend/desktop/src/pages/memory/tests/memory-runtime-fixtures.ts @@ -559,6 +559,7 @@ function filterMemoryItems(input: PanelItemsInput): PanelItemsOutput { "cursor", "claude_code", "codex", + "pi", "opencode", "openclaw", "hermes" diff --git a/App/frontend/desktop/src/pages/memory/tests/sources-sub-page.test.tsx b/App/frontend/desktop/src/pages/memory/tests/sources-sub-page.test.tsx index cc1258e81..d594d00f0 100644 --- a/App/frontend/desktop/src/pages/memory/tests/sources-sub-page.test.tsx +++ b/App/frontend/desktop/src/pages/memory/tests/sources-sub-page.test.tsx @@ -38,7 +38,7 @@ describe("SourcesSubPage", () => { }); it("同步按钮在扫描中旋转,完成后进入不可重复点击的勾选状态", () => { - const sourceIds = ["cursor", "claude_code", "codex", "opencode", "openclaw", "hermes", "workbuddy"]; + const sourceIds = ["cursor", "claude_code", "codex", "pi", "opencode", "openclaw", "hermes", "workbuddy"]; for (const sourceId of sourceIds) { const otherSourceId = sourceIds.find((candidate) => candidate !== sourceId)!; expect(resolveAgentSourceScanButtonState(sourceId, true, sourceId, new Set())).toBe("running"); @@ -265,6 +265,8 @@ describe("SourcesSubPage", () => { expect(resolveAgentSourceConnectionAction(createSource("hermes", "skill_installed"))).toBe("install_plugin"); expect(resolveAgentSourceConnectionAction(createSource("hermes", "plugin_installed"))).toBe("remove_plugin"); expect(resolveAgentSourceConnectionAction(createSource("opencode", "plugin_installed"))).toBe("remove_plugin"); + expect(resolveAgentSourceConnectionAction(createSource("pi", "not_connected"))).toBe("install_plugin"); + expect(resolveAgentSourceConnectionAction(createSource("pi", "plugin_installed"))).toBe("remove_plugin"); expect(resolveAgentSourceConnectionAction(createSource("cursor", "not_connected"))).toBe("install_hook"); expect(resolveAgentSourceConnectionAction(createSource("codex", "skill_installed"))).toBe("install_hook"); expect(resolveAgentSourceConnectionAction(createSource("claude_code", "plugin_installed"))).toBe("remove_hook"); diff --git a/docs/cn/memory/sources.mdx b/docs/cn/memory/sources.mdx index bc3baec49..ff12ced9d 100644 --- a/docs/cn/memory/sources.mdx +++ b/docs/cn/memory/sources.mdx @@ -1,6 +1,6 @@ --- title: Agent 来源与扫描 -description: 了解 Memmy 如何扫描六种内置 Agent 的本地历史,以及安装的 Hook、插件和实时记忆效果。 +description: 了解 Memmy 如何扫描七种内置 Agent 的本地历史,以及安装的 Hook、插件和实时记忆效果。 icon: ScanSearch --- @@ -22,6 +22,7 @@ Agent Source 是 Memmy 读取外部 Agent 本地历史的适配器。来源扫 | Cursor | `~/Library/Application Support/Cursor/User/workspaceStorage/**/state.vscdb` 和 `globalStorage/state.vscdb` | Hook | | Claude Code | `~/.claude/projects/**/*.jsonl` | Hook | | Codex | `~/.codex/sessions///
/rollout-*.jsonl` | Hook | +| Pi | `${PI_CODING_AGENT_SESSION_DIR:-~/.pi/agent/sessions}/**/*.jsonl` | 原生 Extension | | OpenCode | `~/.local/share/opencode/opencode.db` | 原生插件 | | OpenClaw | `~/.openclaw` 下的 conversation / memory SQLite | Memory 插件 | | Hermes | `~/.hermes/sessions/**/*.jsonl` 和 `~/.hermes/state.db` | Memory Provider 插件 | @@ -89,6 +90,7 @@ Memmy 通过 Agent 原生支持的 **Hook** 或**插件**接入实时对话。 | Cursor | Hook | `beforeSubmitPrompt`、`afterAgentResponse`、`stop` | 建立回合、记录回复、在回合结束时自动采集,并支持 `/memmy-resume` | | Claude Code | Hook | `UserPromptSubmit`、`Stop` | 请求前召回并注入相关记忆,结束时自动采集,支持 `/memmy-resume` | | Codex | Hook | `UserPromptSubmit`、`Stop` | 请求前召回并注入相关记忆,结束时自动采集,支持 `/memmy-resume` | +| Pi | 原生 Extension | `before_agent_start`、`agent_settled`、`input` | 请求前召回,只采集完全稳定的回合,并支持 `/memmy-resume` | | OpenCode | 原生插件 | 消息、工具、文本完成和会话事件 | 自动召回、采集回复与工具轨迹,并提供原生记忆工具和 `/memmy-resume` | | OpenClaw | Memory 插件 | `before_prompt_build`、`agent_end` | 构建提示词前注入记忆,Agent 结束时采集完整回合,并提供原生记忆工具 | | Hermes | Memory Provider 插件 | `prefetch`、`sync_turn` 等 Provider 生命周期 | 自动召回和采集,镜像 Hermes 的显式记忆写入,并提供原生记忆工具 | @@ -108,11 +110,11 @@ flowchart LR 安装后的共同效果: -- **自动召回**:Claude Code、Codex、OpenCode、OpenClaw 和 Hermes 会在正常请求执行前检索相关记忆并注入上下文。 +- **自动召回**:Claude Code、Codex、Pi、OpenCode、OpenClaw 和 Hermes 会在正常请求执行前检索相关记忆并注入上下文。 - **自动采集**:Hook 或插件会在回合结束时提交用户请求、Agent 回答和成功/失败状态,不需要 Agent 手动执行 `memmy-memory add`。 - **任务接续**:输入 `/memmy-resume ` 会搜索最多 5 个候选 L1 episode;继续输入 `1`–`5` 可读取完整 episode 并注入接续上下文。 - **按需查询**:随接入安装的 Skill 保留 `memmy-memory search` 和 `memmy-memory get`,只在自动上下文不足时使用。 -- **来源标记**:采集结果会记录 `cursor`、`claude_code`、`codex`、`opencode`、`openclaw` 或 `hermes`,便于过滤和追踪来源。 +- **来源标记**:采集结果会记录 `cursor`、`claude_code`、`codex`、`pi`、`opencode`、`openclaw` 或 `hermes`,便于过滤和追踪来源。
@@ -120,7 +122,7 @@ flowchart LR Cursor 当前的 Hook 重点负责自动采集和 /memmy-resume
- 普通请求需要额外记忆时,由随 Hook 安装的 Skill 执行按需搜索;其他五个接入会在普通请求前自动注入召回结果。 + 普通请求需要额外记忆时,由随 Hook 安装的 Skill 执行按需搜索;其他六个接入会在普通请求前自动注入召回结果。
### Cursor:三个 Hook @@ -171,6 +173,21 @@ Memmy 会在 `~/.cursor/hooks.json` 中追加自己的 Hook 条目,不会覆 - `~/.codex/AGENTS.md` - `~/.codex/skills/memmy-memory/SKILL.md` +### Pi:原生 Extension + +| 事件 | 时机 | 行为 | +| --- | --- | --- | +| `before_agent_start` | Prompt 展开后、Agent 循环前 | 打开共享 Memmy 会话、启动回合并注入召回上下文;Memory 不可用时不阻断 Pi | +| `agent_settled` | 重试、压缩重试和队列续跑全部结束后 | 对一次用户运行只采集一次最终 Assistant 回答 | +| `input` | 正常 Prompt 展开前 | 将待选择的 `/memmy-resume` 候选转换为携带完整 Episode 上下文的续接请求 | + +默认写入或更新: + +- `~/.pi/agent/extensions/memmy-memory.ts` +- `~/.pi/agent/extensions/memmy-memory-config.json` +- `~/.pi/agent/AGENTS.md` +- `~/.pi/agent/skills/memmy-memory/SKILL.md` + ### OpenCode:原生插件 | 回调 | 效果 | @@ -242,7 +259,7 @@ Hermes 同样只允许一个活动的 Memory Provider;检测到其他 Provider ### 安装、验证与移除 1. 打开 **记忆管理 → 跨Agent接入**。 -2. Cursor、Claude Code、Codex 点击 **安装 Hook**;OpenCode、OpenClaw、Hermes 点击 **安装插件**。 +2. Cursor、Claude Code、Codex 点击 **安装 Hook**;Pi、OpenCode、OpenClaw、Hermes 点击 **安装插件**。 3. 如果 Agent 正在运行,重启 Agent 或新建会话,让它重新加载配置。 4. 完成一轮普通对话,再到 Memmy 的记忆或日志页面确认对应来源出现新记录。 5. 输入 `/memmy-resume <关键词>`,确认能看到候选并用 `1`–`5` 选择一个 episode。 diff --git a/docs/en/memory/sources.mdx b/docs/en/memory/sources.mdx index 526524463..24ba0fedb 100644 --- a/docs/en/memory/sources.mdx +++ b/docs/en/memory/sources.mdx @@ -1,6 +1,6 @@ --- title: Agent Sources & Scanning -description: Learn how Memmy scans local history from six built-in Agents and which Hooks, plugins, and live-memory behaviors it installs. +description: Learn how Memmy scans local history from seven built-in Agents and which Hooks, plugins, and live-memory behaviors it installs. icon: ScanSearch --- @@ -22,6 +22,7 @@ An Agent Source is Memmy's adapter for reading local history from an external Ag | Cursor | `~/Library/Application Support/Cursor/User/workspaceStorage/**/state.vscdb` and `globalStorage/state.vscdb` | Hook | | Claude Code | `~/.claude/projects/**/*.jsonl` | Hook | | Codex | `~/.codex/sessions///
/rollout-*.jsonl` | Hook | +| Pi | `${PI_CODING_AGENT_SESSION_DIR:-~/.pi/agent/sessions}/**/*.jsonl` | Native extension | | OpenCode | `~/.local/share/opencode/opencode.db` | Native plugin | | OpenClaw | Conversation and memory SQLite databases under `~/.openclaw` | Memory plugin | | Hermes | `~/.hermes/sessions/**/*.jsonl` and `~/.hermes/state.db` | Memory Provider plugin | @@ -89,6 +90,7 @@ Memmy connects to live Agent conversations through each host's native **Hooks** | Cursor | Hook | `beforeSubmitPrompt`, `afterAgentResponse`, `stop` | Starts turns, records responses, captures completed turns, and supports `/memmy-resume` | | Claude Code | Hook | `UserPromptSubmit`, `Stop` | Recalls and injects memory before a request, captures the completed turn, and supports `/memmy-resume` | | Codex | Hook | `UserPromptSubmit`, `Stop` | Recalls and injects memory before a request, captures the completed turn, and supports `/memmy-resume` | +| Pi | Native extension | `before_agent_start`, `agent_settled`, `input` | Recalls memory before a request, captures only fully settled turns, and supports `/memmy-resume` | | OpenCode | Native plugin | Message, tool, text-completion, and session events | Recalls memory, captures responses and tool traces, and exposes native memory tools and `/memmy-resume` | | OpenClaw | Memory plugin | `before_prompt_build`, `agent_end` | Injects memory while building the prompt, captures the completed turn, and exposes native memory tools | | Hermes | Memory Provider plugin | Provider lifecycle methods such as `prefetch` and `sync_turn` | Recalls and captures automatically, mirrors explicit Hermes memory writes, and exposes native memory tools | @@ -108,11 +110,11 @@ flowchart LR Common results after installation: -- **Automatic recall:** Claude Code, Codex, OpenCode, OpenClaw, and Hermes retrieve and inject relevant memory before normal requests run. +- **Automatic recall:** Claude Code, Codex, Pi, OpenCode, OpenClaw, and Hermes retrieve and inject relevant memory before normal requests run. - **Automatic capture:** the Hook or plugin submits the user request, Agent answer, and success/failure status at the end of a turn. The Agent does not need to run `memmy-memory add` manually. - **Task resumption:** `/memmy-resume ` returns up to five L1 episode candidates. Enter `1`–`5` to load the complete episode and inject continuation context. - **On-demand lookup:** the bundled Skill keeps `memmy-memory search` and `memmy-memory get` for cases where automatic context is insufficient. -- **Source attribution:** captured turns carry `cursor`, `claude_code`, `codex`, `opencode`, `openclaw`, or `hermes`, so you can filter and trace their origin. +- **Source attribution:** captured turns carry `cursor`, `claude_code`, `codex`, `pi`, `opencode`, `openclaw`, or `hermes`, so you can filter and trace their origin.
@@ -120,7 +122,7 @@ Common results after installation: Cursor's current Hook focuses on automatic capture and /memmy-resume
- When a normal Cursor request needs additional memory, the bundled Skill performs an on-demand lookup. The other five integrations inject recalled context before normal requests. + When a normal Cursor request needs additional memory, the bundled Skill performs an on-demand lookup. The other six integrations inject recalled context before normal requests.
### Cursor: three Hooks @@ -171,6 +173,21 @@ Written or updated by default: - `~/.codex/AGENTS.md` - `~/.codex/skills/memmy-memory/SKILL.md` +### Pi: native extension + +| Event | Timing | Behavior | +| --- | --- | --- | +| `before_agent_start` | After prompt expansion, before the agent loop | Opens the shared Memmy session, starts the turn, and injects recalled context without blocking Pi if Memory is unavailable | +| `agent_settled` | After retries, compaction retries, and queued continuations finish | Captures the final Assistant response exactly once for the user run | +| `input` | Before normal prompt expansion | Converts a pending `/memmy-resume` candidate selection into a continuation request with full episode context | + +Written or updated by default: + +- `~/.pi/agent/extensions/memmy-memory.ts` +- `~/.pi/agent/extensions/memmy-memory-config.json` +- `~/.pi/agent/AGENTS.md` +- `~/.pi/agent/skills/memmy-memory/SKILL.md` + ### OpenCode: native plugin | Callback | Effect | @@ -242,7 +259,7 @@ Hermes likewise allows one active Memory Provider. Memmy asks for confirmation b ### Install, verify, and remove 1. Open **Memory → Cross-Agent access**. -2. For Cursor, Claude Code, and Codex, click **Install Hook**. For OpenCode, OpenClaw, and Hermes, click **Install plugin**. +2. For Cursor, Claude Code, and Codex, click **Install Hook**. For Pi, OpenCode, OpenClaw, and Hermes, click **Install plugin**. 3. If the Agent is already running, restart it or open a new session so it reloads its configuration. 4. Complete a normal turn, then confirm that the corresponding source appears in Memmy's memory or log views. 5. Enter `/memmy-resume `, confirm that candidates appear, and select an episode with `1`–`5`. From 113e90af63d20eee503b397cb5c576913e861819 Mon Sep 17 00:00:00 2001 From: bluewater_cg Date: Mon, 3 Aug 2026 00:16:35 -0700 Subject: [PATCH 02/23] fix: support root Electron dev startup --- scripts/dev-start.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/dev-start.sh b/scripts/dev-start.sh index 1b970151b..02be673d2 100755 --- a/scripts/dev-start.sh +++ b/scripts/dev-start.sh @@ -553,6 +553,11 @@ run_main() { require_command node require_command npm require_command lsof + local desktop_dev_args="" + if [[ "$(id -u)" -eq 0 ]]; then + desktop_dev_args=" -- --no-sandbox" + log "running Electron with --no-sandbox because the development process is root" + fi export MEMMY_RUNTIME_NODE_PATH="${MEMMY_RUNTIME_NODE_PATH:-$(command -v node)}" local runtime_node_dir runtime_node_dir="$(cd "$(dirname "$MEMMY_RUNTIME_NODE_PATH")" && pwd)" @@ -610,7 +615,7 @@ run_main() { "bash -c 'set -o pipefail; bash scripts/dev-start.sh --agent-api 2>&1 | tee .tmp/dev-stack/agent-api.log'" \ "bash -c 'set -o pipefail; bash scripts/dev-start.sh --gateway 2>&1 | tee .tmp/dev-stack/gateway.log'" \ "bash -c 'set -o pipefail; npm run dev -w @memmy/frontend-desktop -- --host 127.0.0.1 2>&1 | tee .tmp/dev-stack/frontend.log'" \ - "bash -c 'set -o pipefail; ./node_modules/.bin/wait-on http://127.0.0.1:19000 && env -u ELECTRON_RUN_AS_NODE npm run dev -w @memmy/desktop 2>&1 | tee .tmp/dev-stack/backend.log'" + "bash -c 'set -o pipefail; ./node_modules/.bin/wait-on http://127.0.0.1:19000 && env -u ELECTRON_RUN_AS_NODE npm run dev -w @memmy/desktop$desktop_dev_args 2>&1 | tee .tmp/dev-stack/backend.log'" } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then From 0745210ec20549e8dcecc3c064476a5d31ae779f Mon Sep 17 00:00:00 2001 From: bluewater_cg Date: Mon, 3 Aug 2026 00:53:11 -0700 Subject: [PATCH 03/23] fix: disable streaming for model config probes --- App/backend/src/services/model-config-tester.ts | 1 + App/backend/src/services/tests/model-config-tester.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/App/backend/src/services/model-config-tester.ts b/App/backend/src/services/model-config-tester.ts index 5f864fcaf..1192f9552 100644 --- a/App/backend/src/services/model-config-tester.ts +++ b/App/backend/src/services/model-config-tester.ts @@ -355,6 +355,7 @@ function sendOpenAiCompatibleChatProbe( body: JSON.stringify({ model: input.modelId, messages: [{ role: "user", content: "ping" }], + stream: false, // Reasoning tokens consume the output budget first, so the fallback needs enough budget to emit content. [tokenLimitParam]: tokenLimitParam === "max_completion_tokens" ? 128 : input.provider === "baidu" ? 64 : 1 }), diff --git a/App/backend/src/services/tests/model-config-tester.test.ts b/App/backend/src/services/tests/model-config-tester.test.ts index e72f197cd..578bcbd57 100644 --- a/App/backend/src/services/tests/model-config-tester.test.ts +++ b/App/backend/src/services/tests/model-config-tester.test.ts @@ -42,6 +42,7 @@ describe("model config tester", () => { expect(JSON.parse(String(calls[0]?.init.body))).toMatchObject({ model: "gpt-5.5", max_tokens: 1, + stream: false, messages: [{ role: "user", content: "ping" }] }); expect(JSON.stringify(result)).not.toContain("sk-test-secret"); From 6dbfa84ac6344187824b249c22c00344a3cc2ead Mon Sep 17 00:00:00 2001 From: bluewater_cg Date: Mon, 3 Aug 2026 05:15:14 -0700 Subject: [PATCH 04/23] feat: support remote Docker memory runtime --- .dockerignore | 16 ++++ .env.example | 5 ++ .github/workflows/windows-package.yml | 55 ++++++++++++ .gitignore | 1 + App/backend/local-api-contracts/src/index.ts | 3 +- .../src/api/tests/client-selection.test.ts | 2 +- App/frontend/desktop/src/i18n/messages.ts | 12 +++ .../desktop/src/pages/memory-sources-page.tsx | 35 ++++++-- ...sources-sub-page-path.interaction.test.tsx | 4 +- .../memory/tests/sources-sub-page.test.tsx | 2 + App/shell/desktop/interface/src/index.ts | 1 + App/shell/desktop/src/main/main.ts | 32 +++++-- .../desktop/src/main/runtime-services.ts | 58 +++++++++--- .../tests/packaged-runtime-boundary.test.ts | 5 +- .../desktop/tests/runtime-services.test.ts | 57 +++++++++--- Memory/Dockerfile | 34 +++++++ Memory/readme.md | 50 +++++++++++ Memory/src/server/index.ts | 20 ++++- Memory/src/viewer/static.ts | 23 ++++- Memory/tests/server-lock.test.ts | 27 ++++++ Memory/tests/viewer-static.test.ts | 46 +++++++++- README.md | 14 +++ compose.yaml | 41 +++++++++ deploy/memory/00-configure-desktop.bat | 44 +++++++++ deploy/memory/01-start-memory.bat | 23 +++++ deploy/memory/02-status-memory.bat | 9 ++ deploy/memory/03-logs-memory.bat | 7 ++ deploy/memory/04-restart-memory.bat | 14 +++ deploy/memory/05-stop-memory.bat | 14 +++ deploy/memory/06-update-memory.bat | 30 +++++++ deploy/memory/Memmy-Memory-Menu.bat | 5 ++ deploy/memory/Memmy-Memory-Menu.ps1 | 89 +++++++++++++++++++ deploy/memory/README.md | 38 ++++++++ deploy/memory/docker-compose.yml | 40 +++++++++ package-lock.json | 1 + tests/windows-package-workflow.test.ts | 33 +++++++ 36 files changed, 843 insertions(+), 47 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/windows-package.yml create mode 100644 Memory/Dockerfile create mode 100644 compose.yaml create mode 100644 deploy/memory/00-configure-desktop.bat create mode 100644 deploy/memory/01-start-memory.bat create mode 100644 deploy/memory/02-status-memory.bat create mode 100644 deploy/memory/03-logs-memory.bat create mode 100644 deploy/memory/04-restart-memory.bat create mode 100644 deploy/memory/05-stop-memory.bat create mode 100644 deploy/memory/06-update-memory.bat create mode 100644 deploy/memory/Memmy-Memory-Menu.bat create mode 100644 deploy/memory/Memmy-Memory-Menu.ps1 create mode 100644 deploy/memory/README.md create mode 100644 deploy/memory/docker-compose.yml create mode 100644 tests/windows-package-workflow.test.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..dfff83e82 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.git +.github +node_modules +**/node_modules +**/dist +release +**/release +.env +.env.* +!.env.example +*.log +*.sqlite +*.sqlite-* +.memmy +.codex +.pi diff --git a/.env.example b/.env.example index 79c90efae..734631ecc 100644 --- a/.env.example +++ b/.env.example @@ -10,3 +10,8 @@ MEMMY_GA4_MEASUREMENT_ID= # Legal agreement page base URLs MEMMY_LEGAL_CN_BASE_URL=https://memmy.cn MEMMY_LEGAL_INTL_BASE_URL=https://memmy.bot + +# Docker Memory service. Generate a unique value with at least 32 random bytes. +MEMMY_MEMORY_TOKEN= +# Host-side port; keep the default for a same-machine Windows desktop client. +MEMMY_MEMORY_HOST_PORT=18960 diff --git a/.github/workflows/windows-package.yml b/.github/workflows/windows-package.yml new file mode 100644 index 000000000..9f9cc9120 --- /dev/null +++ b/.github/workflows/windows-package.yml @@ -0,0 +1,55 @@ +name: Windows Package + +on: + workflow_dispatch: {} + push: + branches: + - actions/windows-package/** + +permissions: + contents: read + +concurrency: + group: windows-package-${{ github.ref }} + cancel-in-progress: true + +jobs: + package: + runs-on: windows-latest + timeout-minutes: 120 + defaults: + run: + shell: bash + steps: + - name: Check out source + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 24.15.0 + cache: npm + cache-dependency-path: | + package-lock.json + App/memmy-agent/package-lock.json + + - name: Build unsigned Windows x64 China installer + run: npm run package:win:x64:cn:unsigned + + - name: Create SHA-256 checksum + run: | + installer="$(find App/shell/desktop/release -maxdepth 1 -type f -name 'Memmy-*-win32-x64-cn-unsigned.exe' -print -quit)" + test -n "$installer" + sha256sum "$installer" > "$installer.sha256" + ls -lh "$installer" "$installer.sha256" + + - name: Upload installer artifact + uses: actions/upload-artifact@v4 + with: + name: memmy-windows-x64-cn-unsigned-${{ github.sha }} + path: | + App/shell/desktop/release/Memmy-*-win32-x64-cn-unsigned.exe + App/shell/desktop/release/Memmy-*-win32-x64-cn-unsigned.exe.sha256 + if-no-files-found: error + compression-level: 0 + retention-days: 7 diff --git a/.gitignore b/.gitignore index 1475e2048..d061c81f0 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ sessions/ !.github/workflows/*.yml !.github/release-notes/ !.github/release-notes/*.md +!.dockerignore .env .env.* !.env.example diff --git a/App/backend/local-api-contracts/src/index.ts b/App/backend/local-api-contracts/src/index.ts index aefc25728..a8eb62b50 100644 --- a/App/backend/local-api-contracts/src/index.ts +++ b/App/backend/local-api-contracts/src/index.ts @@ -176,7 +176,8 @@ export const AgentGatewayRuntimeConfigSchema = z.object({ export type AgentGatewayRuntimeConfig = z.infer; export const MemoryServiceRuntimeConfigSchema = z.object({ - baseUrl: z.string().url() + baseUrl: z.string().url(), + ownership: z.enum(["managed", "remote"]).default("managed") }); export type MemoryServiceRuntimeConfig = z.infer; diff --git a/App/frontend/desktop/src/api/tests/client-selection.test.ts b/App/frontend/desktop/src/api/tests/client-selection.test.ts index 84044efe9..24b33f50e 100644 --- a/App/frontend/desktop/src/api/tests/client-selection.test.ts +++ b/App/frontend/desktop/src/api/tests/client-selection.test.ts @@ -5,7 +5,7 @@ import { createAppClients } from "../client-types.js"; const runtimeConfig: RuntimeConfig = { baseUrl: "http://127.0.0.1:18100", localToken: "token", - memory: { baseUrl: "http://127.0.0.1:18960" } + memory: { baseUrl: "http://127.0.0.1:18960", ownership: "managed" } }; afterEach(() => { diff --git a/App/frontend/desktop/src/i18n/messages.ts b/App/frontend/desktop/src/i18n/messages.ts index 22155c092..1b90ba3aa 100644 --- a/App/frontend/desktop/src/i18n/messages.ts +++ b/App/frontend/desktop/src/i18n/messages.ts @@ -1077,6 +1077,12 @@ export const zhCNMessages = { "memory.restartServiceStillUnavailable": "记忆服务已重启,但健康检查仍未通过", "memory.restartServiceStillUnavailableWithReason": "记忆服务已重启,但健康检查仍未通过:{reason}", "memory.restartServiceUnavailable": "当前运行环境不支持重启记忆服务", + "memory.reconnectService": "重新连接", + "memory.reconnectServiceBusy": "连接中...", + "memory.reconnectServiceDone": "已重新连接记忆服务", + "memory.reconnectServiceFailed": "重新连接记忆服务失败:{reason}", + "memory.reconnectServiceStillUnavailable": "记忆服务仍不可用", + "memory.reconnectServiceStillUnavailableWithReason": "记忆服务仍不可用:{reason}", "memory.installSkill": "安装 Skill", "memory.installHook": "安装 Hook", "memory.installPlugin": "安装插件", @@ -2436,6 +2442,12 @@ export const enUSMessages: Record = { "memory.restartServiceStillUnavailable": "The memory service restarted, but its health check still fails", "memory.restartServiceStillUnavailableWithReason": "The memory service restarted, but its health check still fails: {reason}", "memory.restartServiceUnavailable": "Restarting the memory service is unavailable in this runtime", + "memory.reconnectService": "Reconnect", + "memory.reconnectServiceBusy": "Connecting...", + "memory.reconnectServiceDone": "Reconnected to the memory service", + "memory.reconnectServiceFailed": "Failed to reconnect to the memory service: {reason}", + "memory.reconnectServiceStillUnavailable": "The memory service is still unavailable", + "memory.reconnectServiceStillUnavailableWithReason": "The memory service is still unavailable: {reason}", "memory.installSkill": "Install Skill", "memory.installHook": "Install Hook", "memory.installPlugin": "Install plugin", diff --git a/App/frontend/desktop/src/pages/memory-sources-page.tsx b/App/frontend/desktop/src/pages/memory-sources-page.tsx index e1f1740eb..cba64152a 100644 --- a/App/frontend/desktop/src/pages/memory-sources-page.tsx +++ b/App/frontend/desktop/src/pages/memory-sources-page.tsx @@ -91,6 +91,7 @@ export function MemorySourcesContent(props: MemorySourcesContentProps = {}) { const scanPercent = scanProgress && hasDeterminateScanProgress ? formatActiveScanPercent(scanProgress.current, scanProgress.total) : 0; const scannableSources = state.agentSources.items.filter((source) => source.available); const memoryServiceAddress = formatMemoryServiceAddress(clients?.runtimeConfig.memory?.baseUrl); + const remoteMemoryService = clients?.runtimeConfig.memory?.ownership === "remote"; useEffect(() => { setMemoryServiceStatus((current) => current === "checking" ? memoryServiceStatusFromBootstrap(state.bootstrap?.health.memory) : current); @@ -214,26 +215,44 @@ export function MemorySourcesContent(props: MemorySourcesContentProps = {}) { setMemoryServiceError(""); void (async () => { try { - await restart(); + const result = await restart(); clearMemoryPanelCache(); try { const health = await clients.memoryRuntime.health(); if (health.ok && health.storage.ready) { setMemoryServiceStatus("ok"); - setMemoryServiceMessage(t("memory.restartServiceDone")); + setMemoryServiceMessage(t( + result.action === "reconnected" + ? "memory.reconnectServiceDone" + : "memory.restartServiceDone" + )); return; } setMemoryServiceStatus("unavailable"); - setMemoryServiceError(t("memory.restartServiceStillUnavailable")); + setMemoryServiceError(t( + remoteMemoryService + ? "memory.reconnectServiceStillUnavailable" + : "memory.restartServiceStillUnavailable" + )); } catch (error) { setMemoryServiceStatus("unavailable"); - setMemoryServiceError(t("memory.restartServiceStillUnavailableWithReason", { reason: formatErrorMessage(error) })); + setMemoryServiceError(t( + remoteMemoryService + ? "memory.reconnectServiceStillUnavailableWithReason" + : "memory.restartServiceStillUnavailableWithReason", + { reason: formatErrorMessage(error) } + )); } } catch (error) { setMemoryServiceStatus("unavailable"); - setMemoryServiceError(t("memory.restartServiceFailed", { reason: formatErrorMessage(error) })); + setMemoryServiceError(t( + remoteMemoryService + ? "memory.reconnectServiceFailed" + : "memory.restartServiceFailed", + { reason: formatErrorMessage(error) } + )); } finally { setMemoryServiceBusy(false); } @@ -635,7 +654,11 @@ export function MemorySourcesContent(props: MemorySourcesContentProps = {}) { checkingLabel={t("common.loading")} value={memoryServiceAddress ?? t("memory.daemonAddressUnavailable")} description={t("memory.daemonDescription")} - actionLabel={t(memoryServiceBusy ? "memory.restartServiceBusy" : "memory.restartService")} + actionLabel={t( + remoteMemoryService + ? memoryServiceBusy ? "memory.reconnectServiceBusy" : "memory.reconnectService" + : memoryServiceBusy ? "memory.restartServiceBusy" : "memory.restartService" + )} actionTone="success" onAction={restartMemoryService} actionDisabled={!clients || memoryServiceBusy} diff --git a/App/frontend/desktop/src/pages/memory/tests/sources-sub-page-path.interaction.test.tsx b/App/frontend/desktop/src/pages/memory/tests/sources-sub-page-path.interaction.test.tsx index 9d30ab840..faff3048f 100644 --- a/App/frontend/desktop/src/pages/memory/tests/sources-sub-page-path.interaction.test.tsx +++ b/App/frontend/desktop/src/pages/memory/tests/sources-sub-page-path.interaction.test.tsx @@ -40,7 +40,7 @@ describe("SourcesSubPage local data path", () => { runtimeConfig: { baseUrl: "http://127.0.0.1:18100", localToken: "local-token", - memory: { baseUrl: "http://127.0.0.1:18960" } + memory: { baseUrl: "http://127.0.0.1:18960", ownership: "remote" } }, localData: { getPath, @@ -69,6 +69,8 @@ describe("SourcesSubPage local data path", () => { expect(getPath).toHaveBeenCalledTimes(1); expect(listSources).toHaveBeenCalledTimes(1); expect(reveal).not.toHaveBeenCalled(); + expect(container.textContent).toContain("重新连接"); + expect(container.textContent).not.toContain("重启服务"); expect(container.textContent).not.toContain("~/.memmy/memory-service"); expect(container.textContent).not.toContain(windowsDataPath); diff --git a/App/frontend/desktop/src/pages/memory/tests/sources-sub-page.test.tsx b/App/frontend/desktop/src/pages/memory/tests/sources-sub-page.test.tsx index d594d00f0..21a33e0a2 100644 --- a/App/frontend/desktop/src/pages/memory/tests/sources-sub-page.test.tsx +++ b/App/frontend/desktop/src/pages/memory/tests/sources-sub-page.test.tsx @@ -201,6 +201,8 @@ describe("SourcesSubPage", () => { expect(formatMemoryServiceAddress("http://localhost:18888/")).toBe("localhost:18888"); expect(formatMemoryServiceAddress(undefined)).toBeUndefined(); expect(zhCNMessages["memory.restartService"]).toBe("重启服务"); + expect(zhCNMessages["memory.reconnectService"]).toBe("重新连接"); + expect(enUSMessages["memory.reconnectService"]).toBe("Reconnect"); expect(zhCNMessages).not.toHaveProperty("memory.daemonAddress"); }); diff --git a/App/shell/desktop/interface/src/index.ts b/App/shell/desktop/interface/src/index.ts index 39af7ddd6..3cd2602dd 100644 --- a/App/shell/desktop/interface/src/index.ts +++ b/App/shell/desktop/interface/src/index.ts @@ -12,6 +12,7 @@ export interface DesktopMenuBarIconResult { export interface DesktopMemoryServiceRestartResult { ok: true; baseUrl: string; + action: "restarted" | "reconnected"; } export interface DesktopAppInfo { diff --git a/App/shell/desktop/src/main/main.ts b/App/shell/desktop/src/main/main.ts index 8af1b289f..549935d5a 100644 --- a/App/shell/desktop/src/main/main.ts +++ b/App/shell/desktop/src/main/main.ts @@ -41,7 +41,7 @@ import { } from "./window-mode.js"; import { preparePackagedRuntimeConfig, - restartExternalMemoryService, + reconnectExternalMemoryService, resolveAgentGatewayRuntimeConfig, startPackagedRuntimeServices, type PackagedRuntimeServices @@ -96,7 +96,11 @@ let menuBarTray: Tray | null = null; const MENU_BAR_TRAY_GUID = "8B2A0C33-45C0-4C43-8F1C-77F7D4FDF2D4"; let runtimeServices: PackagedRuntimeServices | null = null; let runtimeConfig: DesktopRuntimeConfig | null = null; -let memoryServiceControl: { baseUrl: string; token: string } | null = null; +let memoryServiceControl: { + baseUrl: string; + token: string; + ownership: "managed" | "remote"; +} | null = null; let memoryServiceRestart: Promise | null = null; let packagedRendererServer: PackagedRendererStaticServer | null = null; let packagedRendererBaseUrl: string | null = null; @@ -639,7 +643,11 @@ function showPackagedStartupError(error: unknown): void { */ async function startLocalApi(services: PackagedRuntimeServices | null): Promise { const databasePath = join(app.getPath("userData"), "app.sqlite"); - let memoryControl: { baseUrl: string; token: string }; + let memoryControl: { + baseUrl: string; + token: string; + ownership: "managed" | "remote"; + }; if (services) { process.env.MEMMY_CONFIG ??= services.memory.configPath; process.env.MEMMY_MEMORY_LAYER_URL = services.memory.baseUrl; @@ -647,7 +655,8 @@ async function startLocalApi(services: PackagedRuntimeServices | null): Promise< process.env.MEMMY_MEMORY_DB_PATH = services.memory.databasePath; memoryControl = { baseUrl: services.memory.baseUrl, - token: services.memory.token + token: services.memory.token, + ownership: services.memory.ownership }; } else { const memoryRuntime = await preparePackagedRuntimeConfig({ @@ -662,7 +671,8 @@ async function startLocalApi(services: PackagedRuntimeServices | null): Promise< process.env.MEMMY_MEMORY_DB_PATH ??= memoryRuntime.memoryDatabasePath; memoryControl = { baseUrl: memoryRuntime.memoryBaseUrl, - token: memoryRuntime.memoryToken + token: memoryRuntime.memoryToken, + ownership: memoryRuntime.memoryOwnership }; } memoryServiceControl = memoryControl; @@ -688,7 +698,8 @@ async function startLocalApi(services: PackagedRuntimeServices | null): Promise< return { ...localBackend.runtimeConfig, memory: { - baseUrl: memoryControl.baseUrl + baseUrl: memoryControl.baseUrl, + ownership: memoryControl.ownership }, agentGateway: agentGatewayConfig }; @@ -705,14 +716,17 @@ async function restartMemoryService(): Promise => { + let action: DesktopMemoryServiceRestartResult["action"]; if (runtimeServices) { - await runtimeServices.restartMemory(); + action = await runtimeServices.restartMemory(); } else { - await restartExternalMemoryService(control); + await reconnectExternalMemoryService(control); + action = "reconnected"; } return { ok: true, - baseUrl: control.baseUrl + baseUrl: control.baseUrl, + action }; })(); memoryServiceRestart = operation; diff --git a/App/shell/desktop/src/main/runtime-services.ts b/App/shell/desktop/src/main/runtime-services.ts index 3049e54e0..6900904e8 100644 --- a/App/shell/desktop/src/main/runtime-services.ts +++ b/App/shell/desktop/src/main/runtime-services.ts @@ -19,6 +19,8 @@ const STOP_MANAGED_CHILD_GRACE_MS = 1_000; type RuntimeEnv = Record; type ConfigRecord = Record; +export type MemoryServiceOwnership = "managed" | "remote"; +export type MemoryServiceRefreshAction = "restarted" | "reconnected"; export interface PackagedRuntimeServices { memory: { @@ -26,13 +28,14 @@ export interface PackagedRuntimeServices { token: string; databasePath: string; configPath: string; + ownership: MemoryServiceOwnership; }; agentGateway: { baseUrl: string; bootstrapSecret: string; configPath: string; }; - restartMemory(): Promise; + restartMemory(): Promise; close(): Promise; terminateSync(): void; } @@ -65,6 +68,7 @@ export interface PackagedRuntimeConfig { memoryToken: string; memoryListenHost: string; memoryListenPort: number; + memoryOwnership: MemoryServiceOwnership; agentGatewayBaseUrl: string; agentGatewayHealthHost: string; agentGatewayHealthPort: number; @@ -125,7 +129,7 @@ export async function startPackagedRuntimeServices( {}, browserPreparationAttemptId ); - let memoryRestart: Promise | null = null; + let memoryRestart: Promise | null = null; let browserPreparation: PackagedBrowserPreparation | null = null; let closing = false; @@ -149,7 +153,8 @@ export async function startPackagedRuntimeServices( baseUrl: runtimeConfig.memoryBaseUrl, token: runtimeConfig.memoryToken, databasePath: runtimeConfig.memoryDatabasePath, - configPath: runtimeConfig.configPath + configPath: runtimeConfig.configPath, + ownership: runtimeConfig.memoryOwnership }, agentGateway: { baseUrl: runtimeConfig.agentGatewayBaseUrl, @@ -161,12 +166,12 @@ export async function startPackagedRuntimeServices( throw new Error("Memmy is shutting down"); } if (!memoryRestart) { - memoryRestart = restartManagedMemoryService(entries, runtimeConfig, children, options) + memoryRestart = refreshMemoryService(entries, runtimeConfig, children, options) .finally(() => { memoryRestart = null; }); } - await memoryRestart; + return await memoryRestart; }, async close() { closing = true; @@ -209,6 +214,9 @@ export async function preparePackagedRuntimeConfig( const heartbeat = ensureRecord(gateway, "heartbeat"); const agents = ensureRecord(config, "agents"); const defaults = ensureRecord(agents, "defaults"); + const memoryOwnership = memoryServiceOwnership( + env.MEMMY_MEMORY_RUNTIME ?? stringValue(storage.runtime) ?? "managed" + ); let changed = false; if (!Object.prototype.hasOwnProperty.call(config, "fileMemory")) { @@ -236,6 +244,7 @@ export async function preparePackagedRuntimeConfig( changed = setMissing(storage, "backend", "sqlite") || changed; changed = setMissing(storage, "sqlitePath", memoryDatabasePath) || changed; changed = setMissing(storage, "endpoint", DEFAULT_MEMORY_URL) || changed; + changed = setMissing(storage, "runtime", memoryOwnership) || changed; changed = setMissing(websocket, "host", LOCAL_HOST) || changed; changed = setMissing(websocket, "port", DEFAULT_AGENT_WEBSOCKET_PORT) || changed; if (shouldFillMissingAgentSecret && !stringValue(websocket.tokenIssueSecret) && !stringValue(websocket.token)) { @@ -262,7 +271,9 @@ export async function preparePackagedRuntimeConfig( if (shouldEnsureDirectories) { await Promise.all([ mkdir(agentWorkspace, { recursive: true }), - mkdir(dirname(memoryDatabasePath), { recursive: true }) + ...(memoryOwnership === "managed" + ? [mkdir(dirname(memoryDatabasePath), { recursive: true })] + : []) ]); } @@ -289,6 +300,7 @@ export async function preparePackagedRuntimeConfig( memoryToken, memoryListenHost: listenHostFromUrl(memoryUrl), memoryListenPort: listenPortFromUrl(memoryUrl), + memoryOwnership, agentGatewayBaseUrl: `http://${clientHost(agentWebsocketHost)}:${agentWebsocketPort}`, agentGatewayHealthHost: gatewayHealthHost, agentGatewayHealthPort: gatewayHealthPort, @@ -533,6 +545,9 @@ async function ensureMemoryService( if (probe === "unexpected") { throw new Error(`Memory endpoint is occupied by an unexpected service: ${healthUrl}`); } + if (runtimeConfig.memoryOwnership === "remote") { + return; + } const memoryChild = spawnNodeService("memory", entries.memoryEntry, [ "--config", @@ -559,6 +574,24 @@ async function ensureMemoryService( await waitForHttpService("memory", healthUrl, memoryChild, healthHeaders); } +async function refreshMemoryService( + entries: RuntimeEntryPaths, + runtimeConfig: PackagedRuntimeConfig, + children: ManagedChild[], + options: StartPackagedRuntimeServicesOptions +): Promise { + if (runtimeConfig.memoryOwnership === "remote") { + await reconnectExternalMemoryService({ + baseUrl: runtimeConfig.memoryBaseUrl, + token: runtimeConfig.memoryToken + }); + return "reconnected"; + } + + await restartManagedMemoryService(entries, runtimeConfig, children, options); + return "restarted"; +} + async function restartManagedMemoryService( entries: RuntimeEntryPaths, runtimeConfig: PackagedRuntimeConfig, @@ -588,7 +621,7 @@ async function restartManagedMemoryService( await ensureMemoryService(entries, runtimeConfig, children, options); } -export async function restartExternalMemoryService(input: { +export async function reconnectExternalMemoryService(input: { baseUrl: string; token: string; }): Promise { @@ -601,10 +634,6 @@ export async function restartExternalMemoryService(input: { ? `Memory endpoint returned an unexpected response: ${healthUrl}` : `Memory service is not running: ${healthUrl}`); } - - await requestMemoryServiceShutdown(input); - await waitForHttpServiceStop(healthUrl, healthHeaders); - await waitForHttpServiceReady("memory", healthUrl, healthHeaders); } async function requestMemoryServiceShutdown(input: { baseUrl: string; token: string }): Promise { @@ -1189,6 +1218,13 @@ function memoryProfileName(value: unknown): "account" | "byok" | undefined { return value === "account" || value === "byok" ? value : undefined; } +function memoryServiceOwnership(value: unknown): MemoryServiceOwnership { + if (value === "managed" || value === "remote") { + return value; + } + throw new Error("memmyMemory.storage.runtime must be managed or remote"); +} + function isRecord(value: unknown): value is ConfigRecord { return typeof value === "object" && value !== null && !Array.isArray(value); } diff --git a/App/shell/desktop/tests/packaged-runtime-boundary.test.ts b/App/shell/desktop/tests/packaged-runtime-boundary.test.ts index 255d54b58..1f50d91d3 100644 --- a/App/shell/desktop/tests/packaged-runtime-boundary.test.ts +++ b/App/shell/desktop/tests/packaged-runtime-boundary.test.ts @@ -545,7 +545,7 @@ describe("desktop packaged runtime boundaries", () => { expect(packageSource).not.toContain("/usr/local/bin when writable"); }); - it("restarts the Memory process through the desktop bridge", () => { + it("restarts managed Memory and only reconnects external Memory through the desktop bridge", () => { const mainSource = readFileSync(mainSourcePath, "utf8"); const preloadSource = readFileSync(preloadSourcePath, "utf8"); const runtimeSource = readFileSync(runtimeServicesPath, "utf8"); @@ -555,7 +555,10 @@ describe("desktop packaged runtime boundaries", () => { expect(mainSource).toContain('ipcMain.handle("memmy:restart-memory-service"'); expect(mainSource).toContain('ipcMain.removeHandler("memmy:restart-memory-service")'); expect(mainSource).toContain("await runtimeServices.restartMemory()"); + expect(mainSource).toContain("await reconnectExternalMemoryService(control)"); expect(runtimeSource).toContain("restartManagedMemoryService"); + expect(runtimeSource).toContain("runtimeConfig.memoryOwnership === \"remote\""); + expect(runtimeSource).toContain("reconnectExternalMemoryService"); expect(runtimeSource).toContain("/api/v1/admin/shutdown"); }); diff --git a/App/shell/desktop/tests/runtime-services.test.ts b/App/shell/desktop/tests/runtime-services.test.ts index d5c15a88f..6829a0d4b 100644 --- a/App/shell/desktop/tests/runtime-services.test.ts +++ b/App/shell/desktop/tests/runtime-services.test.ts @@ -10,7 +10,7 @@ import { AgentGatewaySupervisor, preparePackagedBrowser, preparePackagedRuntimeConfig, - restartExternalMemoryService, + reconnectExternalMemoryService, spawnNodeService, startPackagedBrowserPreparation, syncBundledAgentSkills, @@ -54,7 +54,7 @@ describe("packaged desktop runtime config", () => { await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); }); - it("requests a supervised Memory shutdown and waits for the replacement service", async () => { + it("health-checks an externally owned Memory service without shutting it down", async () => { let shutdownRequests = 0; let activeServer: Server; let port = 0; @@ -71,11 +71,6 @@ describe("packaged desktop runtime config", () => { shutdownRequests += 1; response.writeHead(200, { "content-type": "application/json" }); response.end(JSON.stringify({ accepted: true })); - response.once("finish", () => { - activeServer.close(); - activeServer.closeAllConnections(); - setTimeout(() => void startServer(), 250); - }); return; } response.writeHead(404); @@ -98,12 +93,12 @@ describe("packaged desktop runtime config", () => { }; await startServer(); - await restartExternalMemoryService({ + await reconnectExternalMemoryService({ baseUrl: `http://127.0.0.1:${port}`, token: "memory-token" }); - expect(shutdownRequests).toBe(1); + expect(shutdownRequests).toBe(0); }); it("creates missing packaged runtime config under the shared ~/.memmy home", async () => { @@ -123,6 +118,7 @@ describe("packaged desktop runtime config", () => { memoryBaseUrl: "http://127.0.0.1:18960", memoryListenHost: "127.0.0.1", memoryListenPort: 18960, + memoryOwnership: "managed", agentGatewayBaseUrl: "http://127.0.0.1:18980", agentGatewayBootstrapSecret: "stable-secret" }); @@ -157,7 +153,8 @@ describe("packaged desktop runtime config", () => { mode: "local", backend: "sqlite", sqlitePath: join(memmyHome, "memory-service", "memory.sqlite"), - endpoint: "http://127.0.0.1:18960" + endpoint: "http://127.0.0.1:18960", + runtime: "managed" } } }); @@ -165,6 +162,46 @@ describe("packaged desktop runtime config", () => { await expect(stat(join(memmyHome, "memory-service"))).resolves.toBeTruthy(); }); + it("treats remote Memory as externally owned even when it uses loopback", async () => { + const memmyHome = await makeTempRoot(); + const configPath = join(memmyHome, "config.yaml"); + await writeFile(configPath, YAML.stringify({ + memmyMemory: { + storage: { + runtime: "remote", + endpoint: "http://127.0.0.1:18960", + token: "docker-token" + } + } + }), "utf8"); + + const runtime = await preparePackagedRuntimeConfig({ + env: { MEMMY_HOME: memmyHome }, + secretFactory: () => "stable-secret" + }); + + expect(runtime).toMatchObject({ + memoryBaseUrl: "http://127.0.0.1:18960", + memoryToken: "docker-token", + memoryOwnership: "remote" + }); + await expect(stat(join(memmyHome, "workspace"))).resolves.toBeTruthy(); + await expect(stat(join(memmyHome, "memory-service"))).rejects.toMatchObject({ code: "ENOENT" }); + }); + + it("rejects unknown Memory runtime ownership values", async () => { + const memmyHome = await makeTempRoot(); + const configPath = join(memmyHome, "config.yaml"); + await writeFile(configPath, YAML.stringify({ + memmyMemory: { storage: { runtime: "docker-ish" } } + }), "utf8"); + + await expect(preparePackagedRuntimeConfig({ + env: { MEMMY_HOME: memmyHome }, + secretFactory: () => "stable-secret" + })).rejects.toThrow("memmyMemory.storage.runtime must be managed or remote"); + }); + it("preserves existing user model, memory, and websocket settings", async () => { const memmyHome = await makeTempRoot(); const configPath = join(memmyHome, "config.yaml"); diff --git a/Memory/Dockerfile b/Memory/Dockerfile new file mode 100644 index 000000000..1f9b0500e --- /dev/null +++ b/Memory/Dockerfile @@ -0,0 +1,34 @@ +FROM node:24-bookworm-slim AS builder + +RUN apt-get update \ + && apt-get install -y --no-install-recommends python3 make g++ \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY . . + +RUN npm ci \ + && npm run memory:build \ + && mkdir -p /runtime \ + && cp Memory/package.json /runtime/package.json \ + && cp -R Memory/dist /runtime/dist \ + && cd /runtime \ + && npm install --omit=dev --no-package-lock + +FROM node:24-bookworm-slim AS runtime + +ENV NODE_ENV=production \ + HOME=/home/node + +WORKDIR /app + +COPY --from=builder --chown=node:node /runtime ./ + +RUN mkdir -p /data /home/node/.memmy/memory-service/model-cache \ + && chown -R node:node /data /home/node/.memmy + +USER node + +EXPOSE 18960 + +ENTRYPOINT ["node", "dist/src/server/index.js"] diff --git a/Memory/readme.md b/Memory/readme.md index 0770505c7..527f98910 100644 --- a/Memory/readme.md +++ b/Memory/readme.md @@ -41,6 +41,55 @@ npm run memory:serve:dev -- \ The built-in Memory panel is available at `/` and `/viewer`. +## Docker + +The repository root includes a dedicated Node 24 Debian image and Compose +configuration. The image intentionally does not use Alpine because Memory has +native SQLite, sqlite-vec, and ONNX dependencies. + +Create a strong, unique token in the root `.env` before starting the service: + +```bash +docker run --rm node:24-bookworm-slim node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))" +docker compose up -d --build +docker compose ps +``` + +Set the generated value as `MEMMY_MEMORY_TOKEN`. Compose refuses to start when +the token is empty. The container listens on `0.0.0.0:18960`, while the default +published host port is restricted to `127.0.0.1:18960`. SQLite data and the +Hugging Face model cache use the `memory-data` and `memory-model-cache` named +volumes. + +For Docker Desktop on the same Windows computer, configure the desktop app to +use loopback and mark the service as remotely owned: + +```yaml +memmyMemory: + storage: + runtime: remote + endpoint: http://127.0.0.1:18960 + token: +``` + +`runtime: remote` means the desktop app only connects to and health-checks the +service. It never starts, stops, or restarts the container. Agent processes, +project history scanning, and Skill installation continue to run on the +Windows host. + +When another computer needs access, keep port 18960 off the public Internet and +put Caddy or Nginx in front of it with HTTPS. Point desktop clients at the HTTPS +LAN hostname instead of the server IP. The direct `192.168.x.x` address is only +needed when the container actually runs on another host. + +For a host-installed Caddy, the minimal reverse proxy is: + +```caddyfile +memory.example.lan { + reverse_proxy 127.0.0.1:18960 +} +``` + ## Configuration Unless `--config` is provided, the service checks these locations in order: @@ -57,6 +106,7 @@ memmyMemory: version: 1 activeProfile: byok storage: + runtime: managed mode: local backend: sqlite sqlitePath: ~/.memmy/memory-service/memory.sqlite diff --git a/Memory/src/server/index.ts b/Memory/src/server/index.ts index 868814079..97d617269 100644 --- a/Memory/src/server/index.ts +++ b/Memory/src/server/index.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import { randomUUID } from "node:crypto"; import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, unlinkSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -11,6 +12,7 @@ import { listenMemoryHttpServer } from "./http.js"; import { loadCloudServiceEnv } from "../cli/load-env.js"; const logger = createMemoryLogger("server"); +const SERVER_INSTANCE_ID = randomUUID(); export async function main(argv = process.argv.slice(2)): Promise { loadCloudServiceEnv(); @@ -93,6 +95,7 @@ export function acquireSqliteServerLock(input: { mkdirSync(dirname(lockPath), { recursive: true }); return acquireLockFile(lockPath, { pid: process.pid, + instanceId: SERVER_INSTANCE_ID, host: input.host, port: input.port, sqlitePath, @@ -126,7 +129,8 @@ function acquireLockFile(lockPath: string, payload: Record): Sq throw error; } const existing = readServerLock(lockPath); - if (!existing || !isProcessAlive(existing.pid)) { + const staleReusedPid = existing?.pid === process.pid && existing.instanceId !== SERVER_INSTANCE_ID; + if (!existing || staleReusedPid || !isProcessAlive(existing.pid)) { try { unlinkSync(lockPath); } catch (unlinkError) { @@ -146,9 +150,19 @@ function acquireLockFile(lockPath: string, payload: Record): Sq throw new Error(`failed to acquire Memory sqlite server lock: ${lockPath}`); } -function readServerLock(lockPath: string): { pid?: unknown; host?: unknown; port?: unknown } | undefined { +function readServerLock(lockPath: string): { + pid?: unknown; + instanceId?: unknown; + host?: unknown; + port?: unknown; +} | undefined { try { - return JSON.parse(readFileSync(lockPath, "utf8")) as { pid?: unknown; host?: unknown; port?: unknown }; + return JSON.parse(readFileSync(lockPath, "utf8")) as { + pid?: unknown; + instanceId?: unknown; + host?: unknown; + port?: unknown; + }; } catch { return undefined; } diff --git a/Memory/src/viewer/static.ts b/Memory/src/viewer/static.ts index aeba7d54a..3da026f21 100644 --- a/Memory/src/viewer/static.ts +++ b/Memory/src/viewer/static.ts @@ -280,6 +280,21 @@ export function memoryPanelHtml(): string { `; diff --git a/Memory/tests/contract/rest-panel-events.test.ts b/Memory/tests/contract/rest-panel-events.test.ts index 6c2db7a56..e2ad9a26b 100644 --- a/Memory/tests/contract/rest-panel-events.test.ts +++ b/Memory/tests/contract/rest-panel-events.test.ts @@ -39,7 +39,7 @@ describe("REST panel contract", () => { const viewerHtml = await viewerResponse.text(); expect(viewerResponse.status).toBe(200); expect(viewerResponse.headers.get("content-type")).toContain("text/html"); - expect(viewerHtml).toContain("Memmy Memory Panel"); + expect(viewerHtml).toContain("Memmy Memory Console"); expect(viewerHtml).toContain("/api/v1/panel/items"); expect(viewerHtml).toContain("/api/v1/memory/"); expect(viewerHtml).not.toContain("EventSource"); @@ -77,6 +77,24 @@ describe("REST panel contract", () => { expect(search.injectedContext).toContain(completed.l1MemoryId); const overview = await client.panelOverview() as { counts: { memories: number } }; expect(overview.counts.memories).toBeGreaterThan(0); + const panelHeaders = { authorization: "Bearer panel-token" }; + const [analysisResponse, metricsResponse, statusResponse, configResponse, activityResponse] = await Promise.all([ + fetch(`${endpoint}/api/v1/panel/analysis`, { headers: panelHeaders }), + fetch(`${endpoint}/api/v1/panel/metrics`, { headers: panelHeaders }), + fetch(`${endpoint}/api/v1/panel/status`, { headers: panelHeaders }), + fetch(`${endpoint}/api/v1/panel/config`, { headers: panelHeaders }), + fetch(`${endpoint}/api/v1/panel/activity?limit=10`, { headers: panelHeaders }) + ]); + expect([ + analysisResponse.status, + metricsResponse.status, + statusResponse.status, + configResponse.status, + activityResponse.status + ]).toEqual([200, 200, 200, 200, 200]); + const configStatus = await configResponse.json() as { redacted: boolean; config: Record }; + expect(configStatus.redacted).toBe(true); + expect(configStatus.config).toBeTypeOf("object"); const items = await client.panelItems({ layer: "L1" }) as { items: Array<{ id: string; metadata?: { source?: string } }> }; expect(items.items.map((item) => item.id)).toContain(completed.l1MemoryId); expect(items.items.find((item) => item.id === completed.l1MemoryId)?.metadata?.source).toBe("openclaw"); diff --git a/Memory/tests/viewer-static.test.ts b/Memory/tests/viewer-static.test.ts index 4b6334dd3..99c7e0229 100644 --- a/Memory/tests/viewer-static.test.ts +++ b/Memory/tests/viewer-static.test.ts @@ -3,6 +3,10 @@ import { describe, expect, it } from "vitest"; import { memoryPanelHtml } from "../src/viewer/static.js"; describe("memoryPanelHtml", () => { + it("uses an inline favicon so the protected server does not receive browser favicon requests", () => { + expect(memoryPanelHtml()).toContain(''); + }); + it("strips generated Summary prefixes from displayed memory titles", async () => { const harness = createViewerHarness(); runViewerScript(harness); @@ -77,6 +81,27 @@ describe("memoryPanelHtml", () => { expect(harness.requests().every((request) => request.authorization === "Bearer panel-token")).toBe(true); expect(harness.requests().every((request) => !request.path.includes("panel-token"))).toBe(true); }); + + it("validates a manually entered token against a protected panel endpoint", async () => { + const harness = createViewerHarness(); + runViewerScript(harness, { + sessionStorage: { + getItem: () => null, + setItem: () => undefined, + removeItem: () => undefined + } + }); + + harness.element("tokenInput").value = "manual-panel-token"; + const connect = harness.element("connectToken").onclick as () => Promise; + await connect(); + await flushPromises(); + + expect(harness.requests()[0]).toEqual({ + path: "/api/v1/panel/status", + authorization: "Bearer manual-panel-token" + }); + }); }); type FakeRow = FakeElement & { @@ -100,6 +125,16 @@ function runViewerScript( document: harness.document, fetch: harness.fetch, navigator: { clipboard: { writeText: async () => undefined } }, + sessionStorage: { + getItem: () => "viewer-test-token", + setItem: () => undefined, + removeItem: () => undefined + }, + setTimeout: (callback: () => void) => { + callback(); + return 0; + }, + clearTimeout: () => undefined, URLSearchParams, ...browserContext }); @@ -111,21 +146,95 @@ function createViewerHarness() { const detailResolvers = new Map(); const requests: Array<{ path: string; authorization?: string }> = []; const ids = [ + "navDashboard", + "navMemories", + "navActivity", + "navTasks", + "navSystem", + "viewDashboard", + "viewMemories", + "viewActivity", + "viewTasks", + "viewSystem", + "pageTitle", + "pageSubtitle", + "themeToggle", + "lockConsole", + "sidebarStatusDot", + "sidebarStatusText", + "sidebarVersion", "errorMessage", "stats", + "analysisMetrics", + "activityChart", + "activityCaption", + "activityTotal", + "sourceDistribution", + "sourceTotal", + "queueSummary", + "queueState", + "recentActivity", + "openActivity", "query", "layer", "status", + "sourceAgent", "memoryRows", "emptyState", "listMeta", + "memoryResultSummary", "pageInput", "totalPagesText", "prevPage", "nextPage", "detailTitle", "detailId", + "detailContent", "detailJson", + "deleteMemory", + "activityQuery", + "activityTool", + "activitySource", + "activityRows", + "activityEmpty", + "activityMeta", + "activityDetailTitle", + "activityDetailId", + "activityDetailJson", + "loadActivity", + "clearActivity", + "copyActivity", + "taskQuery", + "searchTasks", + "clearTasks", + "taskRows", + "taskEmpty", + "taskMeta", + "taskResultSummary", + "taskPageText", + "prevTaskPage", + "nextTaskPage", + "taskDetailTitle", + "taskDetailId", + "taskDetailContent", + "taskDetailJson", + "copyTask", + "deleteTask", + "systemHealthBadge", + "systemHealth", + "systemSchema", + "systemStorage", + "systemModels", + "systemQueues", + "configJson", + "runWorker", + "reloadConfig", + "copyConfig", + "authScreen", + "tokenInput", + "authError", + "connectToken", + "toast", "refresh", "search", "clearFilters", @@ -162,6 +271,18 @@ function createViewerHarness() { if (path === "/api/v1/panel/overview") { return jsonResponse({ counts: { memories: 2, experiences: 0, worldModels: 0, skills: 0 } }); } + if (path === "/api/v1/panel/analysis") { + return jsonResponse({ metrics: { avgRecallScore: 0.8, recallEvents: 2, activeSkills: 0, recentlyUsedSkills: 0, avgToolLatencyMs: 12, p95ToolLatencyMs: 20 }, dailyMemoryWrites: [], dailySkillEvolutions: [], toolLatency: { tools: [], series: [] } }); + } + if (path === "/api/v1/panel/metrics") { + return jsonResponse({ storage: { backend: "sqlite" }, schema: { version: 1 }, changeSeq: 1, feedback: { recent: 0 }, jobs: { queued: 0, leased: 0, succeeded: 0, failed: 0, dead_letter: 0 }, embeddingRetries: { pending: 0, in_progress: 0, succeeded: 0, failed: 0 }, models: {} }); + } + if (path === "/api/v1/panel/status") { + return jsonResponse({ health: { ok: true, version: "1.0.4", mode: "dev", activeProfile: "byok", uptimeMs: 10, storage: { backend: "sqlite" } }, serverTime: new Date().toISOString() }); + } + if (path === "/api/v1/panel/activity?limit=20") { + return jsonResponse({ entries: [] }); + } if (path.startsWith("/api/v1/panel/items?")) { return jsonResponse({ items: [ @@ -193,7 +314,8 @@ function createViewerHarness() { throw new Error(`missing element: ${id}`); } return element; - } + }, + documentElement: new FakeElement() }, element(id: string) { return elements.get(id)!; @@ -229,6 +351,7 @@ class FakeElement { onchange: unknown; childRows: FakeRow[] = []; dataset: Record = {}; + className = ""; classList = { add: () => undefined, remove: () => undefined, @@ -242,6 +365,14 @@ class FakeElement { select(): void { return undefined; } + + focus(): void { + return undefined; + } + + setAttribute(): void { + return undefined; + } } function listItem(id: string, title: string) { From eece4b3154e3c503e25511d3adf1bfaf6890e711 Mon Sep 17 00:00:00 2001 From: bluewater_cg Date: Tue, 4 Aug 2026 17:38:38 -0700 Subject: [PATCH 09/23] feat: add scoped memory governance protocol Second-party modifications based on the upstream MemTensor/memmy-agent repository, committed to the bluewatercg/memmy-agent fork. Add memmy.agent.v1 provenance, project-scoped Memory governance, markdown audit, supersession, checkpoints, and scoped panel/retrieval behavior. Ignore generated output artifacts and raise Memory test timeout to reduce load-related flake. --- .gitignore | 1 + .../local-api-contracts/src/memory-runtime.ts | 32 +- .../skill-writer/pi/tests/target.test.ts | 55 ++ .../templates/memmy-agent-protocol.test.ts | 43 ++ .../templates/memmy-agent-protocol.ts | 18 + .../templates/memmy-pi-extension.ts | 157 +++++- .../templates/memmy-resume-hook.ts | 124 +++- Memory/package.json | 2 +- Memory/src/cli/commands.ts | 5 +- Memory/src/client/rest-client.ts | 14 + Memory/src/server/http.ts | 101 +++- .../service/feedback/feedback-experience.ts | 40 +- .../src/service/governance/markdown-audit.ts | 126 +++++ .../service/import/import-job-processor.ts | 19 +- Memory/src/service/memory-service.ts | 530 +++++++++++++++++- .../src/service/namespace/namespace-scope.ts | 112 +++- Memory/src/service/read-model/episode.ts | 15 +- Memory/src/service/read-model/memory.ts | 13 +- Memory/src/service/read-model/panel-read.ts | 294 ++++++++-- Memory/src/service/read-model/skill.ts | 3 + .../retrieval/indexed-candidate-pool.ts | 22 +- .../service/retrieval/retrieval-service.ts | 34 +- .../service/session/session-turn-service.ts | 52 +- .../service/trials/skill-trial-resolver.ts | 9 +- Memory/src/service/turn/turn-normalization.ts | 2 +- Memory/src/service/worker/job-handlers.ts | 9 +- Memory/src/storage/repositories.ts | 209 ++++++- Memory/src/storage/schema.ts | 22 +- Memory/src/types.ts | 79 +++ .../contract/memory-rest-service.test.ts | 122 +++- Memory/tests/service/bundle/bundle.test.ts | 14 +- .../service/evolution/orchestration.test.ts | 2 +- .../service/governance/markdown-audit.test.ts | 59 ++ .../provenance-supersession.test.ts | 177 ++++++ .../namespace/project-isolation.test.ts | 147 +++++ .../service/read-model/panel-read.test.ts | 26 +- .../service/read-model/skill-read.test.ts | 4 +- .../retrieval/query-and-filter.test.ts | 12 +- .../tests/service/session/checkpoint.test.ts | 55 ++ .../service/session/session-lifecycle.test.ts | 14 +- .../service/session/turn-capture.test.ts | 6 +- 41 files changed, 2560 insertions(+), 220 deletions(-) create mode 100644 App/backend/src/adapters/outbound/skill-writer/templates/memmy-agent-protocol.test.ts create mode 100644 App/backend/src/adapters/outbound/skill-writer/templates/memmy-agent-protocol.ts create mode 100644 Memory/src/service/governance/markdown-audit.ts create mode 100644 Memory/tests/service/governance/markdown-audit.test.ts create mode 100644 Memory/tests/service/governance/provenance-supersession.test.ts create mode 100644 Memory/tests/service/namespace/project-isolation.test.ts create mode 100644 Memory/tests/service/session/checkpoint.test.ts diff --git a/.gitignore b/.gitignore index d061c81f0..fc6b422be 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ memmy-memory-*.tgz .DS_Store App/memmy-agent/package-lock.json sessions/ +output/ .* !.github/ !.github/workflows/ diff --git a/App/backend/local-api-contracts/src/memory-runtime.ts b/App/backend/local-api-contracts/src/memory-runtime.ts index 9be0d84fa..53d389082 100644 --- a/App/backend/local-api-contracts/src/memory-runtime.ts +++ b/App/backend/local-api-contracts/src/memory-runtime.ts @@ -134,6 +134,27 @@ export const MemoryDetailItemSchema = MemoryListItemSchema.extend({ body: z.string(), createdAt: IsoTimeSchema, sourceMemoryIds: z.array(NonEmptyStringSchema), + provenance: z.object({ + sourceAgent: NonEmptyStringSchema, + profileId: z.string().optional(), + projectId: z.string().optional(), + workspaceId: z.string().optional(), + workspacePath: z.string().optional(), + sessionId: z.string().optional(), + turnId: z.string().optional(), + adapterId: z.string().optional(), + requestId: z.string().optional(), + sourceMemoryIds: z.array(NonEmptyStringSchema), + repository: z.string().optional(), + branch: z.string().optional(), + commit: z.string().optional(), + capturedAt: IsoTimeSchema + }).optional(), + supersession: z.object({ + supersedesMemoryIds: z.array(NonEmptyStringSchema), + supersededByMemoryId: NonEmptyStringSchema.optional(), + reason: z.string().optional() + }).optional(), metadata: UnknownRecordSchema }); export type MemoryDetailItem = z.infer; @@ -384,7 +405,16 @@ export const AddMemoryInputSchema = RuntimeRequestFieldsSchema.extend({ sessionId: z.string().optional(), turnId: z.string().optional(), createdAt: IsoTimeSchema.optional(), - deferProcessing: z.boolean().optional() + deferProcessing: z.boolean().optional(), + sourceMemoryIds: z.array(NonEmptyStringSchema).optional(), + provenance: z.object({ + repository: z.string().optional(), + branch: z.string().optional(), + commit: z.string().optional(), + workspacePath: z.string().optional() + }).optional(), + supersedesMemoryId: NonEmptyStringSchema.optional(), + supersessionReason: z.string().optional() }); export type AddMemoryInput = z.infer; diff --git a/App/backend/src/adapters/outbound/skill-writer/pi/tests/target.test.ts b/App/backend/src/adapters/outbound/skill-writer/pi/tests/target.test.ts index 7ca772a13..d47c0a7ed 100644 --- a/App/backend/src/adapters/outbound/skill-writer/pi/tests/target.test.ts +++ b/App/backend/src/adapters/outbound/skill-writer/pi/tests/target.test.ts @@ -44,6 +44,61 @@ describe("Pi skill target", () => { .toContain("A Memmy Memory Hook or plugin is installed for this agent."); }); + it("reads the Memory storage endpoint instead of an earlier model endpoint", async () => { + const fixture = createFixture(); + writeFileSync(fixture.memmyConfigPath, [ + "memmyMemory:", + " profiles:", + " byok:", + " summary:", + ' endpoint: "http://model-provider.example/v1"', + " storage:", + ' endpoint: "http://127.0.0.1:18960"', + ' token: "test-token"', + "" + ].join("\n"), "utf8"); + + const requestedOrigins: string[] = []; + const originalFetch = globalThis.fetch; + globalThis.fetch = async (input) => { + const url = new URL(input instanceof Request ? input.url : String(input)); + requestedOrigins.push(url.origin); + if (url.pathname === "/api/v1/sessions/open") return jsonResponse({ sessionId: "pi-memory-session" }); + if (url.pathname === "/api/v1/turns/start") return jsonResponse({ turnId: "pi-live-turn" }); + return jsonResponse({}, 404); + }; + + const target = createPiSkillTarget(fixture); + await target.installPlugin?.("pi"); + + try { + const extensionPath = join(fixture.rootDirectory, "extensions", "memmy-memory.ts"); + const extensionModule = await import(`${pathToFileURL(extensionPath).href}?test=${crypto.randomUUID()}`) as { + default: (pi: unknown) => void; + }; + const handlers = new Map unknown>(); + extensionModule.default({ + on(event: string, handler: (...args: never[]) => unknown) { + handlers.set(event, handler); + }, + registerCommand() {}, + appendEntry() {} + }); + + await handlers.get("before_agent_start")?.( + { prompt: "Use the configured Memory service" }, + extensionContext([], "root") as never + ); + + expect(requestedOrigins).toEqual([ + "http://127.0.0.1:18960", + "http://127.0.0.1:18960" + ]); + } finally { + globalThis.fetch = originalFetch; + } + }); + it("uninstalls only Memmy-owned files", async () => { const fixture = createFixture(); const target = createPiSkillTarget(fixture); diff --git a/App/backend/src/adapters/outbound/skill-writer/templates/memmy-agent-protocol.test.ts b/App/backend/src/adapters/outbound/skill-writer/templates/memmy-agent-protocol.test.ts new file mode 100644 index 000000000..750536444 --- /dev/null +++ b/App/backend/src/adapters/outbound/skill-writer/templates/memmy-agent-protocol.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; +import { + MEMMY_AGENT_PROTOCOL_FIELDS, + MEMMY_AGENT_PROTOCOL_VERSION +} from "./memmy-agent-protocol.js"; +import { renderMemmyPiExtension } from "./memmy-pi-extension.js"; +import { renderMemmyResumeHookScript } from "./memmy-resume-hook.js"; + +describe("Memmy agent protocol templates", () => { + it.each([ + ["pi", "memmy-pi-extension", renderMemmyPiExtension()], + ["codex", "memmy-codex-hook", renderMemmyResumeHookScript({ source: "codex", mode: "codex" })], + ["claude_code", "memmy-claude_code-hook", renderMemmyResumeHookScript({ source: "claude_code", mode: "claude-code" })] + ])("renders the shared lifecycle contract for %s", (source, adapterId, script) => { + expect(script).toContain(`const MEMMY_PROTOCOL_VERSION = "${MEMMY_AGENT_PROTOCOL_VERSION}"`); + expect(script).toContain(`const SOURCE = "${source}"`); + expect(script).toContain(source === "pi" ? `const ADAPTER_ID = "${adapterId}"` : 'const ADAPTER_ID = "memmy-" + SOURCE + "-hook"'); + expect(script).toContain("/api/v1/sessions/open"); + expect(script).toContain("/api/v1/turns/start"); + expect(script).toContain("/complete"); + expect(script).toContain("namespace: protocolNamespace"); + expect(script).toContain("provenance: buildProtocolProvenance"); + expect(script).toContain("sourceMemoryIds"); + expect(script).toContain("readGitProvenance"); + expect(script).toContain("capturedAt: new Date().toISOString()"); + }); + + it("publishes the stable v1 field inventory", () => { + expect(MEMMY_AGENT_PROTOCOL_FIELDS).toEqual([ + "protocolVersion", + "source", + "adapterId", + "requestId", + "sessionId", + "turnId", + "episodeId", + "workspacePath", + "projectId", + "sourceMemoryIds", + "provenance" + ]); + }); +}); diff --git a/App/backend/src/adapters/outbound/skill-writer/templates/memmy-agent-protocol.ts b/App/backend/src/adapters/outbound/skill-writer/templates/memmy-agent-protocol.ts new file mode 100644 index 000000000..15304b6c3 --- /dev/null +++ b/App/backend/src/adapters/outbound/skill-writer/templates/memmy-agent-protocol.ts @@ -0,0 +1,18 @@ +/** Shared contract rendered into standalone Memmy agent adapters. */ + +export const MEMMY_AGENT_PROTOCOL_VERSION = "memmy.agent.v1"; + +export const MEMMY_AGENT_PROTOCOL_FIELDS = [ + "protocolVersion", + "source", + "adapterId", + "requestId", + "sessionId", + "turnId", + "episodeId", + "workspacePath", + "projectId", + "sourceMemoryIds", + "provenance" +] as const; + diff --git a/App/backend/src/adapters/outbound/skill-writer/templates/memmy-pi-extension.ts b/App/backend/src/adapters/outbound/skill-writer/templates/memmy-pi-extension.ts index 4bc682651..f1caa81f9 100644 --- a/App/backend/src/adapters/outbound/skill-writer/templates/memmy-pi-extension.ts +++ b/App/backend/src/adapters/outbound/skill-writer/templates/memmy-pi-extension.ts @@ -1,12 +1,17 @@ /** Pi Memmy extension template. */ +import { MEMMY_AGENT_PROTOCOL_VERSION } from "./memmy-agent-protocol.js"; + export function renderMemmyPiExtension(): string { return String.raw`import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; +import { execFileSync } from "node:child_process"; import { readFile } from "node:fs/promises"; import { homedir } from "node:os"; import { join } from "node:path"; const SOURCE = "pi"; +const ADAPTER_ID = "memmy-pi-extension"; +const MEMMY_PROTOCOL_VERSION = ${JSON.stringify(MEMMY_AGENT_PROTOCOL_VERSION)}; const CONFIG_URL = new URL("./memmy-memory-config.json", import.meta.url); const DEFAULT_MEMMY_CONFIG_PATH = join(homedir(), ".memmy", "config.yaml"); const FETCH_TIMEOUT_MS = 45000; @@ -40,25 +45,55 @@ export default function memmyPiExtension(pi: ExtensionAPI): void { try { const memmy = await createMemmyClient(); const externalSessionId = "pi-memory-" + ctx.sessionManager.getSessionId(); + const workspacePath = ctx.cwd || undefined; + const openRequestId = "pi-open:" + externalSessionId; + const openProvenance = buildProtocolProvenance({ + requestId: openRequestId, + sessionId: externalSessionId, + workspacePath + }); const opened = await memmy.post("/api/v1/sessions/open", { + protocolVersion: MEMMY_PROTOCOL_VERSION, + adapterId: ADAPTER_ID, + requestId: openRequestId, sessionId: externalSessionId, source: SOURCE, - workspacePath: ctx.cwd || undefined + workspacePath, + provenance: openProvenance, + meta: { + memmyProtocolVersion: MEMMY_PROTOCOL_VERSION, + provenance: openProvenance + } }); const sessionId = normalizeText(opened.sessionId) || externalSessionId; + const projectId = normalizeText(opened.projectId) || undefined; + const startRequestId = "pi-start:" + requestedTurnId; + const provenance = buildProtocolProvenance({ + requestId: startRequestId, + sessionId, + turnId: requestedTurnId, + workspacePath, + projectId + }); const turn = await memmy.post("/api/v1/turns/start", { - adapterId: "memmy-pi-extension", - requestId: "pi-start:" + requestedTurnId, + protocolVersion: MEMMY_PROTOCOL_VERSION, + adapterId: ADAPTER_ID, + requestId: startRequestId, + namespace: protocolNamespace(workspacePath, projectId), sessionId, turnId: requestedTurnId, query: redactSecrets(query), - source: SOURCE + source: SOURCE, + provenance }); pendingTurns.set(requestedTurnId, { sessionId, turnId: normalizeText(turn.turnId) || requestedTurnId, episodeId: normalizeText(turn.episodeId) || undefined, sourceMemoryIds: Array.isArray(turn.sourceMemoryIds) ? turn.sourceMemoryIds : undefined, + workspacePath, + projectId, + provenance, initialQuery: query, startParentId }); @@ -177,6 +212,9 @@ interface PendingTurn { turnId: string; episodeId?: string; sourceMemoryIds?: unknown[]; + workspacePath?: string; + projectId?: string; + provenance?: Record; initialQuery: string; startParentId: string | null; } @@ -210,16 +248,28 @@ async function completeTurn( status: "succeeded" | "failed" ): Promise { const memmy = await createMemmyClient(); + const requestId = "pi-complete:" + turn.turnId + ":" + hashText(answer); await memmy.post("/api/v1/turns/" + encodeURIComponent(turn.turnId) + "/complete", { - adapterId: "memmy-pi-extension", - requestId: "pi-complete:" + turn.turnId + ":" + hashText(answer), + protocolVersion: MEMMY_PROTOCOL_VERSION, + adapterId: ADAPTER_ID, + requestId, + namespace: protocolNamespace(turn.workspacePath, turn.projectId), sessionId: turn.sessionId, episodeId: turn.episodeId, query: redactSecrets(query), answer: redactSecrets(answer), status, source: SOURCE, - sourceMemoryIds: turn.sourceMemoryIds + sourceMemoryIds: turn.sourceMemoryIds, + provenance: buildProtocolProvenance({ + ...turn.provenance, + requestId, + sessionId: turn.sessionId, + turnId: turn.turnId, + workspacePath: turn.workspacePath, + projectId: turn.projectId, + sourceMemoryIds: turn.sourceMemoryIds + }) }); } @@ -355,6 +405,60 @@ function renderMemoryContext(markdown: string, query: string): string { ].join("\n"); } +interface ProtocolProvenanceInput { + requestId?: string; + sessionId?: string; + turnId?: string; + workspacePath?: string; + projectId?: string; + sourceMemoryIds?: unknown[]; + [key: string]: unknown; +} + +function protocolNamespace(workspacePath?: string, projectId?: string): Record { + return { + source: SOURCE, + profileId: "default", + workspacePath, + projectId + }; +} + +function buildProtocolProvenance(input: ProtocolProvenanceInput): Record { + const workspacePath = normalizeText(input.workspacePath) || undefined; + return { + ...input, + ...readGitProvenance(workspacePath), + sourceAgent: SOURCE, + adapterId: ADAPTER_ID, + workspacePath, + sourceMemoryIds: Array.isArray(input.sourceMemoryIds) + ? input.sourceMemoryIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0) + : [], + capturedAt: new Date().toISOString() + }; +} + +function readGitProvenance(workspacePath?: string): Record { + if (!workspacePath) return {}; + try { + const git = (...args: string[]) => execFileSync("git", ["-C", workspacePath, ...args], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"] + }).trim(); + const repository = git("rev-parse", "--show-toplevel"); + const branch = git("rev-parse", "--abbrev-ref", "HEAD"); + const commit = git("rev-parse", "HEAD"); + return { + ...(repository ? { repository } : {}), + ...(branch && branch !== "HEAD" ? { branch } : {}), + ...(commit ? { commit } : {}) + }; + } catch { + return {}; + } +} + interface MemmyClient { get(path: string): Promise>; post(path: string, body: Record): Promise>; @@ -403,12 +507,43 @@ async function readJsonConfig(): Promise> { async function readYamlConfig(path: string): Promise> { const content = await readFile(path, "utf8"); - const values: Record = {}; - for (const line of content.split(/\r?\n/u)) { + return parseStorageBlock(content); +} + +function parseStorageBlock(content: string): Record { + const storages: Array> = []; + let activeStorage: Record | null = null; + let storageIndent = 0; + for (const rawLine of content.split(/\r?\n/u)) { + const line = rawLine.replace(/#.*$/u, "").replace(/\s+$/u, ""); + if (!line.trim()) continue; + const indent = line.match(/^\s*/u)?.[0].length ?? 0; + if (/^\s*storage:\s*$/u.test(line)) { + activeStorage = {}; + storageIndent = indent; + storages.push(activeStorage); + continue; + } + if (activeStorage && indent <= storageIndent) activeStorage = null; + if (!activeStorage) continue; const match = line.match(/^\s+(endpoint|token):\s*(.*?)\s*$/u); - if (match && !values[match[1]]) values[match[1]] = match[2].replace(/^['"]|['"]$/gu, ""); + if (match) activeStorage[match[1]] = parseYamlScalar(match[2]); + } + return storages.find((storage) => storage.endpoint) ?? storages[0] ?? {}; +} + +function parseYamlScalar(value: string): string { + const trimmed = value.trim(); + if (!trimmed) return ""; + if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || + (trimmed.startsWith("'") && trimmed.endsWith("'"))) { + try { + return JSON.parse(trimmed) as string; + } catch { + return trimmed.slice(1, -1); + } } - return values; + return trimmed; } function isResumeCommand(value: string): boolean { diff --git a/App/backend/src/adapters/outbound/skill-writer/templates/memmy-resume-hook.ts b/App/backend/src/adapters/outbound/skill-writer/templates/memmy-resume-hook.ts index e775f64c8..05d468339 100644 --- a/App/backend/src/adapters/outbound/skill-writer/templates/memmy-resume-hook.ts +++ b/App/backend/src/adapters/outbound/skill-writer/templates/memmy-resume-hook.ts @@ -1,5 +1,7 @@ /** Memmy resume hook template. */ +import { MEMMY_AGENT_PROTOCOL_VERSION } from "./memmy-agent-protocol.js"; + export type MemmyResumeHookMode = "claude-code" | "codex" | "cursor"; export interface RenderMemmyResumeHookScriptOptions { @@ -10,12 +12,15 @@ export interface RenderMemmyResumeHookScriptOptions { /** Renders the Node hook script used by prompt-submit hooks. */ export function renderMemmyResumeHookScript(options: RenderMemmyResumeHookScriptOptions): string { return String.raw`#!/usr/bin/env node +import { execFileSync } from "node:child_process"; import { readFile, unlink, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; import { join } from "node:path"; const SOURCE = ${JSON.stringify(options.source)}; const MODE = ${JSON.stringify(options.mode)}; +const ADAPTER_ID = "memmy-" + SOURCE + "-hook"; +const MEMMY_PROTOCOL_VERSION = ${JSON.stringify(MEMMY_AGENT_PROTOCOL_VERSION)}; const CONFIG_URL = new URL("./memmy-memory-config.json", import.meta.url); const STATE_URL = new URL("./memmy-resume-state.json", import.meta.url); const DEFAULT_MEMMY_CONFIG_PATH = join(homedir(), ".memmy", "config.yaml"); @@ -172,25 +177,53 @@ async function captureCompletedTurn(payload) { const client = await createMemmyClient(); const externalSessionId = memoryExternalSessionId(payload); + const currentWorkspacePath = workspacePath(payload) || undefined; + const openRequestId = SOURCE + "-open:" + externalSessionId; + const openProvenance = buildProtocolProvenance({ + requestId: openRequestId, + sessionId: externalSessionId, + workspacePath: currentWorkspacePath + }); const opened = await client.post("/api/v1/sessions/open", { + protocolVersion: MEMMY_PROTOCOL_VERSION, + adapterId: ADAPTER_ID, + requestId: openRequestId, sessionId: externalSessionId, source: SOURCE, - workspacePath: workspacePath(payload) || undefined + workspacePath: currentWorkspacePath, + provenance: openProvenance, + meta: { + memmyProtocolVersion: MEMMY_PROTOCOL_VERSION, + provenance: openProvenance + } }); const sessionId = normalizeText(opened.sessionId) || externalSessionId; const turnId = normalizeText(pending && pending.turnId) || platformTurnId(payload) || SOURCE + "-fallback-" + hashText([sessionId, query, answer].join("\\u0000")); + const completeRequestId = SOURCE + "-complete:" + turnId + ":" + hashText([status, query, answer].join("\\u0000")); + const projectId = normalizeText(opened.projectId) || normalizeText(pending && pending.projectId) || undefined; await client.post("/api/v1/turns/" + encodeURIComponent(turnId) + "/complete", { - adapterId: "memmy-" + SOURCE + "-hook", - requestId: SOURCE + "-complete:" + turnId + ":" + hashText([status, query, answer].join("\\u0000")), + protocolVersion: MEMMY_PROTOCOL_VERSION, + adapterId: ADAPTER_ID, + requestId: completeRequestId, + namespace: protocolNamespace(currentWorkspacePath, projectId), sessionId, episodeId: normalizeText(pending && pending.episodeId) || undefined, query, answer, status, source: SOURCE, - sourceMemoryIds: Array.isArray(pending && pending.sourceMemoryIds) ? pending.sourceMemoryIds : undefined + sourceMemoryIds: Array.isArray(pending && pending.sourceMemoryIds) ? pending.sourceMemoryIds : undefined, + provenance: buildProtocolProvenance({ + ...(pending && pending.provenance && typeof pending.provenance === "object" ? pending.provenance : {}), + requestId: completeRequestId, + sessionId, + turnId, + workspacePath: currentWorkspacePath, + projectId, + sourceMemoryIds: Array.isArray(pending && pending.sourceMemoryIds) ? pending.sourceMemoryIds : [] + }) }); await clearTurnState(payload); } @@ -202,20 +235,47 @@ async function startCapturedTurn(payload, prompt) { } const client = await createMemmyClient(); const externalSessionId = memoryExternalSessionId(payload); + const currentWorkspacePath = workspacePath(payload) || undefined; + const openRequestId = SOURCE + "-open:" + externalSessionId; + const openProvenance = buildProtocolProvenance({ + requestId: openRequestId, + sessionId: externalSessionId, + workspacePath: currentWorkspacePath + }); const opened = await client.post("/api/v1/sessions/open", { + protocolVersion: MEMMY_PROTOCOL_VERSION, + adapterId: ADAPTER_ID, + requestId: openRequestId, sessionId: externalSessionId, source: SOURCE, - workspacePath: workspacePath(payload) || undefined + workspacePath: currentWorkspacePath, + provenance: openProvenance, + meta: { + memmyProtocolVersion: MEMMY_PROTOCOL_VERSION, + provenance: openProvenance + } }); const sessionId = normalizeText(opened.sessionId) || externalSessionId; + const projectId = normalizeText(opened.projectId) || undefined; const requestedTurnId = platformTurnId(payload) || SOURCE + "-turn-" + hashText([sessionId, query, String(Date.now())].join("\\u0000")); + const startRequestId = SOURCE + "-start:" + requestedTurnId; + const provenance = buildProtocolProvenance({ + requestId: startRequestId, + sessionId, + turnId: requestedTurnId, + workspacePath: currentWorkspacePath, + projectId + }); const turn = await client.post("/api/v1/turns/start", { - adapterId: "memmy-" + SOURCE + "-hook", - requestId: SOURCE + "-start:" + requestedTurnId, + protocolVersion: MEMMY_PROTOCOL_VERSION, + adapterId: ADAPTER_ID, + requestId: startRequestId, + namespace: protocolNamespace(currentWorkspacePath, projectId), sessionId, turnId: requestedTurnId, - query + query, + provenance }); const state = { createdAt: new Date().toISOString(), @@ -224,6 +284,9 @@ async function startCapturedTurn(payload, prompt) { episodeId: normalizeText(turn && turn.episodeId) || undefined, query, sourceMemoryIds: Array.isArray(turn && turn.sourceMemoryIds) ? turn.sourceMemoryIds : undefined, + workspacePath: currentWorkspacePath, + projectId, + provenance, answer: "" }; await writeTurnState(payload, state); @@ -437,6 +500,51 @@ function workspacePath(payload) { return roots.map(item => normalizeText(item)).find(Boolean) || ""; } +function protocolNamespace(workspacePathValue, projectId) { + return { + source: SOURCE, + profileId: "default", + workspacePath: workspacePathValue, + projectId + }; +} + +function buildProtocolProvenance(input) { + const workspacePathValue = normalizeText(input && input.workspacePath) || undefined; + const sourceMemoryIds = Array.isArray(input && input.sourceMemoryIds) + ? input.sourceMemoryIds.filter(value => typeof value === "string" && value.trim()) + : []; + return { + ...(input && typeof input === "object" ? input : {}), + ...readGitProvenance(workspacePathValue), + sourceAgent: SOURCE, + adapterId: ADAPTER_ID, + workspacePath: workspacePathValue, + sourceMemoryIds, + capturedAt: new Date().toISOString() + }; +} + +function readGitProvenance(workspacePathValue) { + if (!workspacePathValue) return {}; + try { + const git = (...args) => execFileSync("git", ["-C", workspacePathValue, ...args], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"] + }).trim(); + const repository = git("rev-parse", "--show-toplevel"); + const branch = git("rev-parse", "--abbrev-ref", "HEAD"); + const commit = git("rev-parse", "HEAD"); + return { + ...(repository ? { repository } : {}), + ...(branch && branch !== "HEAD" ? { branch } : {}), + ...(commit ? { commit } : {}) + }; + } catch { + return {}; + } +} + function completedTurnStatus(payload) { const status = normalizeText( payload.status || diff --git a/Memory/package.json b/Memory/package.json index a64fdb501..e8c0c5f2d 100644 --- a/Memory/package.json +++ b/Memory/package.json @@ -22,7 +22,7 @@ "serve:local": "node dist/src/server/index.js", "serve:dev": "tsx src/server/index.ts", "worker:run": "node dist/src/cli/index.js raw POST /worker/run", - "test": "vitest run --dir tests", + "test": "vitest run --dir tests --testTimeout 20000", "typecheck": "tsc -p tsconfig.json --noEmit", "package:npm": "node src/cli/npm/build-package.mjs", "pack:npm": "npm run package:npm && npm pack ../dist/memmy-memory-npm", diff --git a/Memory/src/cli/commands.ts b/Memory/src/cli/commands.ts index f7a162011..04e8c45ff 100644 --- a/Memory/src/cli/commands.ts +++ b/Memory/src/cli/commands.ts @@ -194,7 +194,10 @@ async function addMemoryRequest(args: string[], parsed: ParsedArgs): Promise { + return this.request("POST", `/api/v1/sessions/${encodeURIComponent(sessionId)}/checkpoint`, request); + } + startTurn(request: TurnStartRequest): Promise { return this.request("POST", "/api/v1/turns/start", request); } @@ -69,6 +75,14 @@ export class MemoryRestClient { return this.request("POST", "/api/v1/memory/add", request); } + exportMarkdown(includeArchived = false): Promise { + return this.request("GET", `/api/v1/memory/audit/markdown${queryString({ includeArchived })}`); + } + + importMarkdown(request: MemoryMarkdownImportRequest): Promise { + return this.request("POST", "/api/v1/memory/audit/markdown/import", request); + } + getMemory(id: string): Promise { return this.request("GET", `/api/v1/memory/${encodeURIComponent(id)}`); } diff --git a/Memory/src/server/http.ts b/Memory/src/server/http.ts index f1bffbd73..5627c347a 100644 --- a/Memory/src/server/http.ts +++ b/Memory/src/server/http.ts @@ -9,14 +9,17 @@ import type { MemoryLayer, MemoryReloadConfigRequest, MemorySearchRequest, + MemoryMarkdownImportRequest, RequestEnvelope, RuntimeNamespace, + SessionCheckpointRequest, SessionOpenRequest, TurnCompleteRequest, TurnStartRequest } from "../types.js"; import { DEFAULT_NAMESPACE_SOURCE } from "../types.js"; import { MemoryService } from "../service/memory-service.js"; +import { normalizeNamespace } from "../service/namespace/namespace-scope.js"; import { MemoryServiceError, statusForCode } from "../utils/error.js"; import { createPluginRuntimeAnalytics, @@ -37,11 +40,14 @@ export const API_ROUTES = [ "POST /api/v1/admin/reload-config", "POST /api/v1/admin/shutdown", "POST /api/v1/sessions/open", + "POST /api/v1/sessions/:sessionId/checkpoint", "POST /api/v1/sessions/:sessionId/close", "POST /api/v1/turns/start", "POST /api/v1/turns/:turnId/complete", "POST /api/v1/memory/search", "POST /api/v1/memory/add", + "GET /api/v1/memory/audit/markdown", + "POST /api/v1/memory/audit/markdown/import", "POST /api/v1/memory/processing/status", "POST /api/v1/memory/:id/processing/retry", "GET /api/v1/memory/:id", @@ -410,14 +416,41 @@ async function routeRequest( requestId: request.requestId, adapterId: request.adapterId, namespace: request.namespace, + source: request.source ?? request.namespace?.source, + profileId: request.profileId ?? request.namespace?.profileId, + projectId: request.namespace?.projectId, + workspaceId: request.namespace?.workspaceId, sessionId: request.sessionId, - workspacePath: request.workspacePath + workspacePath: request.workspacePath ?? request.namespace?.workspacePath, + meta: isRecord(request.meta) ? request.meta : undefined, + protocolVersion: typeof request.protocolVersion === "string" ? request.protocolVersion : undefined, + provenance: isRecord(request.provenance) ? request.provenance : undefined }; return publicOpenSessionResponse( await service.idempotent("sessions.create", publicRequest, publicRequest, () => service.openSession(publicRequest)) ); } + const sessionCheckpoint = match(path, /^\/api\/v1\/sessions\/([^/]+)\/checkpoint$/); + if (method === "POST" && sessionCheckpoint) { + requireMemoryWrite(principal); + const request = requestWithPrincipal(body, "sessions.checkpoint", principal); + requireStringField(request, "task", "sessions.checkpoint"); + return service.checkpointSession(decodeMatchSegment(sessionCheckpoint, 1), { + namespace: request.namespace, + episodeId: request.episodeId, + task: request.task, + changes: Array.isArray(request.changes) ? request.changes.filter((item): item is string => typeof item === "string") : undefined, + validated: Array.isArray(request.validated) ? request.validated.filter((item): item is string => typeof item === "string") : undefined, + unverified: Array.isArray(request.unverified) ? request.unverified.filter((item): item is string => typeof item === "string") : undefined, + nextSteps: Array.isArray(request.nextSteps) ? request.nextSteps.filter((item): item is string => typeof item === "string") : undefined, + sourceTurnIds: Array.isArray(request.sourceTurnIds) ? request.sourceTurnIds.filter((item): item is string => typeof item === "string") : undefined, + sourceMemoryIds: Array.isArray(request.sourceMemoryIds) ? request.sourceMemoryIds.filter((item): item is string => typeof item === "string") : undefined, + tokenEstimate: typeof request.tokenEstimate === "number" && Number.isFinite(request.tokenEstimate) ? Math.max(0, Math.trunc(request.tokenEstimate)) : undefined, + createL1: request.createL1 !== false + }); + } + const sessionClose = match(path, /^\/api\/v1\/sessions\/([^/]+)\/close$/); if (method === "POST" && sessionClose) { requireMemoryWrite(principal); @@ -443,7 +476,9 @@ async function routeRequest( query: request.query, turnId: request.turnId, contextHints: request.contextHints, - contextBudget: request.contextBudget + contextBudget: request.contextBudget, + protocolVersion: typeof request.protocolVersion === "string" ? request.protocolVersion : undefined, + provenance: isRecord(request.provenance) ? request.provenance : undefined }; const result = await trackExternalHookRecall(pluginRuntimeAnalytics, request, () => service.idempotent("turn.start", publicRequest, { request: publicRequest }, () => @@ -476,6 +511,8 @@ async function routeRequest( toolResults: request.toolResults, artifacts: request.artifacts, sourceMemoryIds: request.sourceMemoryIds, + protocolVersion: typeof request.protocolVersion === "string" ? request.protocolVersion : undefined, + provenance: isRecord(request.provenance) ? request.provenance : undefined, usage: request.usage, status: request.status }; @@ -543,7 +580,11 @@ async function routeRequest( sessionId: request.sessionId, turnId: request.turnId, createdAt: typeof request.createdAt === "string" ? request.createdAt : undefined, - deferProcessing: request.deferProcessing === true + deferProcessing: request.deferProcessing === true, + sourceMemoryIds: parseOptionalStringArray(request.sourceMemoryIds, "memory.add sourceMemoryIds"), + provenance: isRecord(request.provenance) ? request.provenance : undefined, + supersedesMemoryId: typeof request.supersedesMemoryId === "string" ? request.supersedesMemoryId : undefined, + supersessionReason: typeof request.supersessionReason === "string" ? request.supersessionReason : undefined }; const result = await trackExternalToolCall( pluginRuntimeAnalytics, @@ -570,7 +611,7 @@ async function routeRequest( request.memoryIds, "worker.import-summaries.enqueue.memoryIds" ); - const result = service.enqueuePendingImportSummaries(10_000, memoryIds); + const result = service.enqueuePendingImportSummaries(10_000, memoryIds, { namespace: principal.namespace }); if (result.enqueued > 0) { autoWorker.schedule(); } @@ -661,6 +702,7 @@ async function routeRequest( if (method === "GET" && path === "/api/v1/memory/logs") { requirePanelRead(principal); return service.apiLogs({ + namespace: principal.namespace, tools: parseApiLogTools(url.searchParams.get("tools")), sourceAgent: url.searchParams.get("sourceAgent") ?? undefined, excludedSourceAgents: url.searchParams.getAll("excludedSourceAgents"), @@ -697,6 +739,25 @@ async function routeRequest( } const memoryGet = match(path, /^\/api\/v1\/memory\/([^/]+)$/); + if (method === "GET" && path === "/api/v1/memory/audit/markdown") { + requireMemoryRead(principal); + return service.exportMarkdown({ + namespace: principal.namespace, + includeArchived: url.searchParams.get("includeArchived") === "true" + }); + } + + if (method === "POST" && path === "/api/v1/memory/audit/markdown/import") { + requireMemoryWrite(principal); + const request = requestWithPrincipal(body, "memory.audit.markdown.import", principal); + requireStringField(request, "markdown", "memory.audit.markdown.import"); + return service.importMarkdown({ + namespace: request.namespace, + markdown: request.markdown, + apply: request.apply !== false + }); + } + if (method === "GET" && memoryGet) { requireMemoryRead(principal); return trackExternalToolCall( @@ -741,6 +802,9 @@ function publicOpenSessionResponse(result: unknown): Record { const record = responseRecord(result); return { sessionId: record.sessionId, + projectId: record.projectId, + workspaceId: record.workspaceId, + workspacePath: record.workspacePath, status: record.status, resumed: record.resumed, serverTime: record.serverTime @@ -815,6 +879,7 @@ function publicSearchResponse(result: unknown): Record { searchEventId: record.searchEventId, hits: record.hits, sourceMemoryIds: record.sourceMemoryIds, + retrievalDebug: record.retrievalDebug, status: record.status, sections: Array.isArray(injectedContext.sections) ? injectedContext.sections : [], tokenEstimate: typeof injectedContext.tokenEstimate === "number" ? injectedContext.tokenEstimate : undefined, @@ -1118,7 +1183,17 @@ function envelopeWithPrincipal>( principal: AuthPrincipal ): T & RequestEnvelope { const existing = isRecord(body.namespace) ? body.namespace as unknown as RuntimeNamespace : undefined; - const namespace = mergeNamespaces(mergeNamespaces(existing, namespaceFromSource(body.source)), principal.namespace); + const mergedNamespace = mergeNamespaces(mergeNamespaces(existing, namespaceFromSource(body.source)), principal.namespace); + // Source/profile identify provenance, but cannot establish an isolation + // boundary. Keep those fields on the request only when a project, tenant, + // or workspace scope is present; legacy source-only REST calls resolve IDs + // through their session and remain compatible with the service API. + const namespace = mergedNamespace && ( + Boolean(mergedNamespace.projectId) || + Boolean(mergedNamespace.workspaceId) || + Boolean(mergedNamespace.workspacePath) || + Boolean(mergedNamespace.tenantId) + ) ? mergedNamespace : undefined; assertNamespaceScope(existing, principal.namespace); return { ...body, @@ -1156,8 +1231,20 @@ function assertNamespaceScope( requestNamespace: RuntimeNamespace | undefined, principalNamespace: RuntimeNamespace | undefined ): void { - void requestNamespace; - void principalNamespace; + if (!requestNamespace || !principalNamespace) return; + const requested = normalizeNamespace(requestNamespace); + const allowed = normalizeNamespace(principalNamespace); + const checks: Array<[string, string | undefined, string | undefined]> = [ + ["tenantId", requestNamespace.tenantId, principalNamespace.tenantId], + ["userId", requestNamespace.userId, principalNamespace.userId], + ["projectId", requested.projectId, allowed.projectId], + ["workspaceId", requested.workspaceId, allowed.workspaceId] + ]; + for (const [field, actual, expected] of checks) { + if (actual && expected && actual !== expected) { + throw new MemoryServiceError("forbidden", `request namespace exceeds token scope: ${field}`); + } + } } function requireStringField(record: object, field: string, routeName: string): void { diff --git a/Memory/src/service/feedback/feedback-experience.ts b/Memory/src/service/feedback/feedback-experience.ts index db5f50c0f..b4b4546a3 100644 --- a/Memory/src/service/feedback/feedback-experience.ts +++ b/Memory/src/service/feedback/feedback-experience.ts @@ -44,6 +44,8 @@ import { clip } from "../../utils/text.js"; import { nowIso } from "../../utils/time.js"; import { updatePolicyStats } from "../evolution/policy-induction.js"; import { + memoryFilterForNamespace, + namespaceIdFromContext as canonicalNamespaceIdFromContext, namespaceForMemory, namespaceForSession, normalizeNamespace, @@ -214,6 +216,7 @@ async feedback(request: FeedbackRequest): Promise { const attribution = this.resolveFeedbackAttribution(request, context); const attributedRequest: FeedbackRequest = { ...request, + namespace: context.namespace, l1MemoryId: request.l1MemoryId ?? attribution.l1MemoryId, rawTurnId: request.rawTurnId ?? attribution.rawTurnId, episodeId: request.episodeId ?? attribution.episodeId, @@ -542,7 +545,7 @@ async maybeCreateFeedbackExperience( trace }); const vector = await this.deps.embedder.embedOne(draft.vectorText, "query"); - const existing = this.findSimilarFeedbackExperience(draft, vector); + const existing = this.findSimilarFeedbackExperience(draft, vector, context.namespace); const at = feedback.createdAt; const saved = existing ? this.mergeFeedbackExperiencePolicy(existing, draft, vector, at) @@ -860,10 +863,15 @@ feedbackExperienceEpisodeContext( findSimilarFeedbackExperience( draft: FeedbackExperienceDraft, - vector: number[] + vector: number[], + namespace: RuntimeNamespace ): MemoryRow | null { let best: { memory: MemoryRow; score: number; policy: PolicyMeta } | null = null; - for (const memory of this.deps.repos.memories.list({ memoryLayer: "L2", status: ["activated", "resolving"] }, 1000)) { + for (const memory of this.deps.repos.memories.list({ + ...memoryFilterForNamespace(namespace), + memoryLayer: "L2", + status: ["activated", "resolving"] + }, 1000)) { const policy = policyMetaFromMemory(memory); if (!policy) continue; const sourceFeedbackIds = stringArray(memory.properties.internal_info.source_feedback_ids) @@ -1118,7 +1126,7 @@ feedbackCandidatePolicyIds(request: FeedbackRequest, feedback: FeedbackRecord): const recall = this.deps.repos.runtime.getRecallEvent(request.recallEventId); for (const id of recall?.injectedMemoryIds ?? []) { const memory = this.deps.repos.memories.get(id); - if (memory?.memoryLayer === "L2") ids.add(memory.id); + if (memory?.memoryLayer === "L2" && this.memoryMatchesNamespace(memory, request.namespace)) ids.add(memory.id); } } if (feedback.l1MemoryId) { @@ -1127,13 +1135,15 @@ feedbackCandidatePolicyIds(request: FeedbackRequest, feedback: FeedbackRecord): l1MemoryId: feedback.l1MemoryId, limit: 20 })) { - ids.add(link.l2MemoryId); + const memory = this.deps.repos.memories.get(link.l2MemoryId); + if (memory && this.memoryMatchesNamespace(memory, request.namespace)) ids.add(memory.id); } } if (ids.size === 0 && feedback.rationale) { for (const hit of this.deps.repos.memories.search( feedback.rationale, { + ...memoryFilterForNamespace(normalizeNamespace(request.namespace)), memoryLayer: "L2", status: "activated" }, @@ -1166,6 +1176,8 @@ feedbackRepairEvidence( if (request.recallEventId) { const recall = this.deps.repos.runtime.getRecallEvent(request.recallEventId); for (const id of recall?.injectedMemoryIds ?? []) { + const memory = this.deps.repos.memories.get(id); + if (!memory || !this.memoryMatchesNamespace(memory, request.namespace)) continue; if (feedback.polarity === "negative") { low.add(id); } else if (feedback.polarity === "positive") { @@ -1174,6 +1186,7 @@ feedbackRepairEvidence( } } for (const policy of this.deps.repos.memories.getMany(policyIds)) { + if (!this.memoryMatchesNamespace(policy, request.namespace)) continue; const meta = policyMetaFromMemory(policy); if (!meta) continue; for (const id of meta.sourceTraceIds) { @@ -1199,6 +1212,7 @@ feedbackRepairEvidence( for (const memory of this.deps.repos.memories.search( searchText, { + ...memoryFilterForNamespace(normalizeNamespace(request.namespace)), memoryLayer: "L1", status: "activated" }, @@ -1214,6 +1228,7 @@ feedbackRepairEvidence( for (const memory of this.deps.repos.memories.search( searchText, { + ...memoryFilterForNamespace(normalizeNamespace(request.namespace)), memoryLayer: "L1", status: "activated" }, @@ -1240,6 +1255,13 @@ feedbackRepairEvidence( }; } +memoryMatchesNamespace(memory: MemoryRow, namespace: RuntimeNamespace | undefined): boolean { + if (!namespace) return true; + const filter = memoryFilterForNamespace(namespace); + const actual = memoryFilterForNamespace(namespaceForMemory(memory)); + return actual.tenantId === filter.tenantId && actual.projectId === filter.projectId; + } + sessionRepairEvidence( sessionId: string, keyword: string | undefined, @@ -2842,13 +2864,7 @@ function namespaceIdFromMemory(memory: MemoryRow): string { } function namespaceIdFromContext(namespace: RuntimeNamespace): string { - return [ - namespace.tenantId, - namespace.userId, - namespace.projectId ?? namespace.workspaceId, - namespace.source, - namespace.profileId - ].filter(Boolean).join(":"); + return canonicalNamespaceIdFromContext(namespace); } function memoryStatusForLifecycleStatus(status: "candidate" | "active" | "archived"): "activated" | "resolving" | "archived" { diff --git a/Memory/src/service/governance/markdown-audit.ts b/Memory/src/service/governance/markdown-audit.ts new file mode 100644 index 000000000..47e61f43d --- /dev/null +++ b/Memory/src/service/governance/markdown-audit.ts @@ -0,0 +1,126 @@ +import { parse, stringify } from "yaml"; +import type { MemoryProvenance, MemoryRow, MemoryStatus, RuntimeNamespace } from "../../types.js"; +import { kindFromMemory } from "../../storage/repositories.js"; +import { detailFromMemory } from "../read-model/memory.js"; + +export interface MemoryMarkdownFrontMatter { + id: string; + kind: string; + memoryLayer: string; + status: MemoryStatus; + title: string; + tags: string[]; + version: number; + createdAt: string; + updatedAt: string; + provenance?: MemoryProvenance; + supersession?: { + supersedesMemoryIds: string[]; + supersededByMemoryId?: string; + reason?: string; + }; + audit: { + source: "memmy-memory"; + editable: ["title", "tags", "body"]; + }; +} + +export interface ParsedMemoryMarkdown { + frontMatter: MemoryMarkdownFrontMatter; + body: string; +} + +export function renderMemoryMarkdown(memory: MemoryRow): string { + const detail = detailFromMemory(memory); + const frontMatter: MemoryMarkdownFrontMatter = { + id: memory.id, + kind: kindFromMemory(memory), + memoryLayer: memory.memoryLayer, + status: memory.status, + title: detail.title, + tags: memory.tags, + version: memory.version, + createdAt: memory.createdAt, + updatedAt: memory.updatedAt, + ...(detail.provenance ? { provenance: detail.provenance } : {}), + ...(detail.supersession ? { supersession: detail.supersession } : {}), + audit: { + source: "memmy-memory", + editable: ["title", "tags", "body"] + } + }; + return `---\n${stringify(frontMatter).trimEnd()}\n---\n# ${detail.title}\n\n${memory.memoryValue.trim()}\n`; +} + +export function renderMemoryMarkdownBundle(memories: MemoryRow[]): string { + return memories.map(renderMemoryMarkdown).join("\n---\n\n"); +} + +export function parseMemoryMarkdownBundle(markdown: string): ParsedMemoryMarkdown[] { + const documents = markdown + .replace(/^\uFEFF/, "") + .split(/\n---\n(?=\n?---\n|id:|$)/g) + .map((value) => value.trim()) + .filter(Boolean); + return documents.map(parseMemoryMarkdown); +} + +export function parseMemoryMarkdown(markdown: string): ParsedMemoryMarkdown { + const match = markdown.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/); + if (!match) throw new Error("markdown audit document must contain YAML front matter"); + const parsed = parse(match[1] ?? "") as unknown; + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("markdown audit front matter must be an object"); + } + const frontMatter = parsed as Partial; + if (typeof frontMatter.id !== "string" || !frontMatter.id.trim()) { + throw new Error("markdown audit front matter requires id"); + } + const body = stripTitleHeading(match[2] ?? ""); + return { + frontMatter: { + id: frontMatter.id.trim(), + kind: typeof frontMatter.kind === "string" ? frontMatter.kind : "trace", + memoryLayer: typeof frontMatter.memoryLayer === "string" ? frontMatter.memoryLayer : "L1", + status: frontMatter.status ?? "activated", + title: typeof frontMatter.title === "string" && frontMatter.title.trim() + ? frontMatter.title.trim() + : firstBodyLine(body) || frontMatter.id, + tags: Array.isArray(frontMatter.tags) + ? frontMatter.tags.filter((tag): tag is string => typeof tag === "string") + : [], + version: typeof frontMatter.version === "number" ? frontMatter.version : 1, + createdAt: typeof frontMatter.createdAt === "string" ? frontMatter.createdAt : new Date().toISOString(), + updatedAt: typeof frontMatter.updatedAt === "string" ? frontMatter.updatedAt : new Date().toISOString(), + ...(frontMatter.provenance ? { provenance: frontMatter.provenance } : {}), + ...(frontMatter.supersession ? { supersession: frontMatter.supersession } : {}), + audit: { + source: "memmy-memory", + editable: ["title", "tags", "body"] + } + }, + body + }; +} + +export function markdownNamespace(frontMatter: MemoryMarkdownFrontMatter): RuntimeNamespace | undefined { + const provenance = frontMatter.provenance; + if (!provenance) return undefined; + return { + source: provenance.sourceAgent, + profileId: provenance.profileId ?? "default", + projectId: provenance.projectId, + workspaceId: provenance.workspaceId, + workspacePath: provenance.workspacePath + }; +} + +function stripTitleHeading(value: string): string { + const lines = value.replace(/^\s+/, "").split(/\r?\n/); + if (lines[0]?.match(/^#\s+/)) lines.shift(); + return lines.join("\n").trim(); +} + +function firstBodyLine(value: string): string { + return value.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? ""; +} diff --git a/Memory/src/service/import/import-job-processor.ts b/Memory/src/service/import/import-job-processor.ts index b12608baf..51093b49a 100644 --- a/Memory/src/service/import/import-job-processor.ts +++ b/Memory/src/service/import/import-job-processor.ts @@ -59,7 +59,9 @@ export interface ImportJobProcessorDeps { assertMemoryAddEnabled(): void; assertMemoryInScope(memory: MemoryRow, namespace: unknown): void; sanitizeMemoryAddRequest(request: MemoryAddRequest): MemoryAddRequest; - resolveContext(request: MemoryAddRequest): { userId: string; namespace: { source?: string; projectId?: string; profileId?: string } }; + resolveContext(request: MemoryAddRequest): { userId: string; namespace: { + source?: string; tenantId?: string; projectId?: string; profileId?: string; workspaceId?: string; workspacePath?: string; + } }; requireSession(id: string): SessionRecord; assertSessionInScope(session: ReturnType, namespace: unknown): void; normalizeMemoryAddCreatedAt(value: string | undefined): string | undefined; @@ -158,6 +160,21 @@ export class ImportJobProcessor { source: request.source ?? "manual", title, summary: importSummary ?? firstLine(request.content), turn_id: request.turnId, ...(importTrace ? { plugin_algorithm: "memory.add.import_async.v2", trace: importTrace } : {}) }, + provenance: { + ...request.provenance, + sourceAgent: session?.source ?? request.source?.trim() ?? context.namespace.source, + tenantId: context.namespace.tenantId ?? "local", + profileId: session?.profileId ?? context.namespace.profileId, + projectId: session?.projectId ?? context.namespace.projectId, + workspaceId: session?.workspaceId ?? context.namespace.workspaceId, + workspacePath: session?.workspacePath ?? context.namespace.workspacePath, + sessionId: session?.id ?? request.sessionId, + turnId: request.turnId, + adapterId: request.adapterId, + requestId: request.requestId, + sourceMemoryIds: request.sourceMemoryIds ?? [], + capturedAt: at + }, createdAt: at }); diff --git a/Memory/src/service/memory-service.ts b/Memory/src/service/memory-service.ts index 0d0bc450f..b6ef6e9da 100644 --- a/Memory/src/service/memory-service.ts +++ b/Memory/src/service/memory-service.ts @@ -32,6 +32,7 @@ import { type SessionRecord } from "../storage/repositories.js"; import type { SerializedMemoryVector } from "../storage/sqlite-vec-store.js"; +import { DEFAULT_NAMESPACE_SOURCE } from "../types.js"; import type { FeedbackRequest, HealthResponse, @@ -42,9 +43,12 @@ import type { MemoryExportRequest, MemoryGovernanceRequest, MemoryImportRequest, + MemoryMarkdownExportRequest, + MemoryMarkdownImportRequest, MemoryKind, MemoryLayer, MemoryListItem, + MemoryProvenance, MemoryProcessingRecord, MemoryReloadConfigRequest, MemoryReloadConfigResponse, @@ -56,6 +60,8 @@ import type { RequestEnvelope, RetrievalMode, RuntimeNamespace, + SessionCheckpointPayload, + SessionCheckpointRequest, SessionCompactRequest, SessionOpenRequest, SkillUseRequest, @@ -97,10 +103,20 @@ import { } from "./import/memory-import-pipeline.js"; import { recordApiLog } from "./model-audit/model-call-audit.js"; import { + parseMemoryMarkdownBundle, + renderMemoryMarkdownBundle +} from "./governance/markdown-audit.js"; +import { + hasProjectScope, + namespaceIdFromContext as canonicalNamespaceIdFromContext, namespaceForMemory, namespaceForRawTurn, namespaceForSession, - normalizeNamespace + memoryFilterForNamespace, + normalizeNamespace, + projectIdFromMemory, + sameProjectScope, + tenantIdFromSession } from "./namespace/namespace-scope.js"; import { EpisodeReadModel, @@ -779,6 +795,64 @@ export class MemoryService { return this.sessionTurns.compactSession(sessionId, request); } + checkpointSession(sessionId: string, request: SessionCheckpointRequest): { + checkpointId: string; + checkpoint: SessionCheckpointPayload; + memorySnapshot: { + summary: string; + sourceTurnIds: string[]; + sourceMemoryIds: string[]; + tokenEstimate?: number; + }; + contextPacketId: string; + rawTurnId?: string; + l1MemoryId?: string; + changeSeq?: number; + syncCursor?: string; + jobs: JobRef[]; + serverTime: string; + } { + if (!request.task.trim()) { + throw new MemoryServiceError("invalid_argument", "session checkpoint requires a non-empty task"); + } + const list = (value: string[] | undefined): string[] => Array.isArray(value) + ? value.map((item) => item.trim()).filter(Boolean).slice(0, 32) + : []; + const checkpoint: SessionCheckpointPayload = { + task: request.task.trim(), + changes: list(request.changes), + validated: list(request.validated), + unverified: list(request.unverified), + nextSteps: list(request.nextSteps) + }; + const summary = [ + `Task: ${checkpoint.task}`, + "Changes:", + ...(checkpoint.changes.length ? checkpoint.changes.map((item) => `- ${item}`) : ["- none recorded"]), + "Validated:", + ...(checkpoint.validated.length ? checkpoint.validated.map((item) => `- ${item}`) : ["- none recorded"]), + "Unverified:", + ...(checkpoint.unverified.length ? checkpoint.unverified.map((item) => `- ${item}`) : ["- none recorded"]), + "Next steps:", + ...(checkpoint.nextSteps.length ? checkpoint.nextSteps.map((item) => `- ${item}`) : ["- none recorded"]) + ].join("\n"); + const result = this.compactSession(sessionId, { + namespace: request.namespace, + episodeId: request.episodeId, + summary, + sourceTurnIds: request.sourceTurnIds, + sourceMemoryIds: request.sourceMemoryIds, + tokenEstimate: request.tokenEstimate, + createL1: request.createL1, + checkpoint + }); + return { + ...result, + checkpointId: result.rawTurnId ?? result.contextPacketId, + checkpoint + }; + } + async startTurn(request: TurnStartRequest & Record): Promise<{ contextPacketId: string; turnId: string; @@ -924,7 +998,183 @@ export class MemoryService { createdAt: string; serverTime: string; } { - return this.importJobs.addMemory(request); + const previous = request.supersedesMemoryId + ? this.requireExistingMemory(request.supersedesMemoryId) + : undefined; + if (previous) { + this.assertMemoryInScope(previous, request.namespace); + if ((request.layer ?? "L1") !== previous.memoryLayer) { + throw new MemoryServiceError("conflict", "superseding memories must use the same memory layer"); + } + } + const added = this.importJobs.addMemory(request); + if (!previous) return added; + + const inserted = this.requireExistingMemory(added.id); + const at = nowIso(); + const supersession = this.repos.transaction(() => this.repos.memories.supersede({ + oldMemory: previous, + newMemory: inserted, + projectId: projectIdFromMemory(inserted), + reason: request.supersessionReason, + actor: request.namespace ? { ...request.namespace } : {}, + createdAt: at + })); + this.repos.runtime.appendChange({ + memoryId: supersession.oldMemory.id, + namespaceId: namespaceIdFromMemory(supersession.oldMemory), + kind: kindFromMemory(supersession.oldMemory), + op: "archived", + entityId: supersession.oldMemory.id, + userId: supersession.oldMemory.userId, + changeType: "superseded", + before: previous, + after: supersession.oldMemory, + source: "memory.supersede", + createdAt: at + }); + this.repos.runtime.insertAudit({ + userId: inserted.userId, + sessionId: inserted.sessionId, + actor: request.namespace ? { ...request.namespace } : {}, + action: "supersede", + targetKind: kindFromMemory(previous), + targetId: previous.id, + before: previous, + after: supersession.oldMemory, + meta: { + supersededByMemoryId: inserted.id, + relationId: supersession.relation.id, + reason: request.supersessionReason + }, + createdAt: at + }); + return added; + } + + exportMarkdown(request: MemoryMarkdownExportRequest = {}): { + markdown: string; + count: number; + projectId?: string; + generatedAt: string; + } { + this.assertMemorySearchEnabled(); + const context = this.resolveContext(request); + const memories = this.repos.memories.list({ + ...memoryFilterForNamespace(context.namespace), + status: request.includeArchived ? ["activated", "resolving", "archived"] : ["activated", "resolving"] + }, 100_000); + return { + markdown: renderMemoryMarkdownBundle(memories), + count: memories.length, + projectId: context.namespace.projectId, + generatedAt: nowIso() + }; + } + + importMarkdown(request: MemoryMarkdownImportRequest): { + applied: boolean; + count: number; + updated: string[]; + created: string[]; + rejected: Array<{ id: string; reason: string }>; + serverTime: string; + } { + this.assertMemoryAddEnabled(); + if (!request.markdown?.trim()) { + throw new MemoryServiceError("invalid_argument", "markdown audit import requires markdown"); + } + const context = this.resolveContext(request); + const documents = parseMemoryMarkdownBundle(request.markdown); + const updated: string[] = []; + const created: string[] = []; + const rejected: Array<{ id: string; reason: string }> = []; + for (const document of documents) { + try { + const existing = this.repos.memories.get(document.frontMatter.id); + if (existing) { + this.assertMemoryInScope(existing, request.namespace); + if (document.frontMatter.memoryLayer !== existing.memoryLayer) { + throw new MemoryServiceError("conflict", "markdown audit cannot change memory layer"); + } + if (request.apply !== false) { + const at = nowIso(); + const next = this.repos.memories.update({ + ...existing, + memoryValue: document.body, + tags: uniq(document.frontMatter.tags), + info: { ...existing.info, title: document.frontMatter.title, tags: uniq(document.frontMatter.tags) }, + properties: { + ...existing.properties, + tags: uniq(document.frontMatter.tags), + info: { ...existing.properties.info, title: document.frontMatter.title, tags: uniq(document.frontMatter.tags) }, + internal_info: { + ...existing.properties.internal_info, + title: document.frontMatter.title, + provenance: document.frontMatter.provenance ?? existing.properties.internal_info.provenance + } + }, + updatedAt: at, + contentHash: stableHash(document.body) + }); + this.repos.runtime.appendChange({ + memoryId: next.id, + namespaceId: namespaceIdFromMemory(next), + kind: kindFromMemory(next), + op: "updated", + entityId: next.id, + userId: next.userId, + changeType: "markdown_audit_update", + before: existing, + after: next, + source: "markdown.audit", + createdAt: at + }); + this.repos.runtime.insertAudit({ + userId: next.userId, + sessionId: next.sessionId, + actor: request.namespace ? { ...request.namespace } : {}, + action: "markdown_update", + targetKind: kindFromMemory(next), + targetId: next.id, + before: existing, + after: next, + meta: {}, + createdAt: at + }); + } + updated.push(existing.id); + continue; + } + if (request.apply === false) { + created.push(document.frontMatter.id); + continue; + } + const added = this.addMemory({ + namespace: request.namespace, + source: context.namespace.source, + content: document.body, + layer: document.frontMatter.memoryLayer as MemoryLayer, + title: document.frontMatter.title, + tags: document.frontMatter.tags, + provenance: document.frontMatter.provenance + }); + created.push(added.id); + } catch (error) { + rejected.push({ + id: document.frontMatter.id, + reason: error instanceof Error ? error.message : String(error) + }); + } + } + return { + applied: request.apply !== false, + count: documents.length, + updated, + created, + rejected, + serverTime: nowIso() + }; } timeline(input: RequestEnvelope & { @@ -1058,7 +1308,10 @@ export class MemoryService { this.repos.runtime.exportBundleTables(request.includeRawText === true), context.namespace ); - tables.memory_vectors = this.repos.vectors.exportRows().map((row) => ({ ...row })); + const scopedMemoryIds = new Set((tables.memories ?? []).map((row) => row.id).filter((id): id is string => typeof id === "string")); + tables.memory_vectors = this.repos.vectors.exportRows() + .filter((row) => scopedMemoryIds.has(row.memory_id)) + .map((row) => ({ ...row })); if (request.includeAudit === false) { delete tables.audit_logs; } @@ -1116,15 +1369,21 @@ export class MemoryService { throw new MemoryServiceError("invalid_argument", "import bundle must contain tables"); } const context = this.resolveContext(request); + const inputTables = request.bundle.tables as Record>>; + const enforceBundleScope = hasProjectScope(context.namespace) || Boolean(context.namespace.tenantId); + const scopedInputTables = enforceBundleScope ? scopeBundleTables(inputTables, context.namespace) : inputTables; + if (enforceBundleScope && bundleTableRowCount(scopedInputTables) !== bundleTableRowCount(inputTables)) { + throw new MemoryServiceError("not_found", "bundle contains records outside the requested namespace"); + } const importedAt = nowIso(); - const result = this.repos.runtime.importBundleTables(request.bundle.tables, { + const result = this.repos.runtime.importBundleTables(scopedInputTables, { conflictStrategy: request.conflictStrategy ?? "skip" }); - const importedVectors = importedMemoryVectors(request.bundle.tables.memory_vectors); + const importedVectors = importedMemoryVectors(scopedInputTables.memory_vectors); this.repos.vectors.importRows(importedVectors); result.inserted.memory_vectors = importedVectors.length; const reembedMemoryIds = importedReembedMemoryIds( - request.bundle.tables, + scopedInputTables, this.embedder.config.model ?? this.embedder.config.provider ); const audit = this.repos.runtime.insertAudit({ @@ -1416,7 +1675,7 @@ export class MemoryService { return this.panelReadModel.serviceLogs(input); } - apiLogs(input: { + apiLogs(input: RequestEnvelope & { tools?: Array<"memory_add" | "memory_search" | "skill_generate" | "skill_evolve">; sourceAgent?: string; excludedSourceAgents?: string[]; @@ -1628,12 +1887,22 @@ export class MemoryService { return this.importJobs.restartFailedProcessing(at, limit); } - enqueuePendingImportSummaries(limit = 10000, targetMemoryIds?: readonly string[]): { + enqueuePendingImportSummaries(limit = 10000, targetMemoryIds?: readonly string[], request: RequestEnvelope = {}): { enqueued: number; memoryIds: string[]; serverTime: string; } { - return this.importJobs.enqueuePendingImportSummaries(limit, targetMemoryIds); + const scopedTargetIds = request.namespace + ? targetMemoryIds + ? targetMemoryIds.filter((id) => { + const memory = this.repos.memories.get(id); + if (!memory) return false; + this.assertMemoryInScope(memory, request.namespace); + return true; + }) + : this.repos.memories.list({ ...memoryFilterForNamespace(request.namespace) }, 10_000).map((memory) => memory.id) + : targetMemoryIds; + return this.importJobs.enqueuePendingImportSummaries(limit, scopedTargetIds); } nextWorkerRunAt(): number | undefined { @@ -1648,6 +1917,18 @@ export class MemoryService { limit = 100, request: RequestEnvelope & { targetMemoryIds?: string[] } = {} ): ReturnType { + if (request.namespace && !request.targetMemoryIds) { + const targetMemoryIds = this.repos.memories.list({ ...memoryFilterForNamespace(request.namespace) }, 10_000) + .map((memory) => memory.id); + return this.workerRunner.runWorkerOnce(limit, { ...request, targetMemoryIds }); + } + if (request.namespace && request.targetMemoryIds) { + for (const id of request.targetMemoryIds) { + const memory = this.repos.memories.get(id); + if (!memory) continue; + this.assertMemoryInScope(memory, request.namespace); + } + } return this.workerRunner.runWorkerOnce(limit, request); } @@ -1686,8 +1967,11 @@ export class MemoryService { sessionId?: string; agentId?: string; appId?: string; + tenantId?: string; projectId?: string; profileId?: string; + workspacePath?: string; + provenance?: Partial; layer: MemoryLayer; kind: MemoryKind; lifecycleStatus?: "candidate" | "active" | "archived"; @@ -1703,9 +1987,34 @@ export class MemoryService { const tags = uniq(input.tags.filter(Boolean)); const memoryStatus = memoryStatusForLifecycleStatus(input.lifecycleStatus ?? "active"); const inputInfo = input.info ?? {}; + const inputInternal = input.internal ?? {}; + const sourceSession = input.sessionId ? this.repos.runtime.getSession(input.sessionId) : undefined; + const sessionTenantId = sourceSession ? tenantIdFromSession(sourceSession) : undefined; + const tenantId = input.tenantId ?? sessionTenantId ?? stringFromMeta(inputInfo, "tenant_id") ?? input.provenance?.tenantId ?? "local"; + const provenance: MemoryProvenance = { + sourceAgent: input.provenance?.sourceAgent ?? input.agentId ?? DEFAULT_NAMESPACE_SOURCE, + tenantId, + profileId: input.provenance?.profileId ?? input.profileId, + projectId: input.provenance?.projectId ?? input.projectId, + workspaceId: input.provenance?.workspaceId ?? input.appId, + workspacePath: input.provenance?.workspacePath ?? input.workspacePath, + sessionId: input.provenance?.sessionId ?? input.sessionId, + turnId: input.provenance?.turnId ?? stringFromMeta(inputInfo, "turn_id"), + adapterId: input.provenance?.adapterId, + requestId: input.provenance?.requestId, + sourceMemoryIds: uniq([ + ...(input.provenance?.sourceMemoryIds ?? []), + ...stringArray(inputInternal.source_memory_ids) + ]), + repository: input.provenance?.repository, + branch: input.provenance?.branch, + commit: input.provenance?.commit, + capturedAt: input.provenance?.capturedAt ?? at + }; const info = { ...inputInfo, tags: uniq([...tags, ...stringArray(inputInfo.tags)]), + tenant_id: tenantId, ...(input.projectId ? { project_id: input.projectId } : {}), ...(input.profileId ? { profile_id: input.profileId } : {}) }; @@ -1733,7 +2042,8 @@ export class MemoryService { memory_layer: input.layer, memory_kind: input.kind, schema_version: 1, - ...(input.internal ?? {}) + ...inputInternal, + provenance } }, memoryLayer: input.layer, @@ -1770,7 +2080,14 @@ export class MemoryService { } private openSessionNoWrite(request: SessionOpenRequest): ReturnType { - const namespace = normalizeNamespace(request.namespace); + const namespace = normalizeNamespace({ + ...request.namespace, + source: request.source ?? request.namespace?.source ?? DEFAULT_NAMESPACE_SOURCE, + profileId: request.profileId ?? request.namespace?.profileId ?? "default", + projectId: request.projectId ?? request.namespace?.projectId, + workspaceId: request.workspaceId ?? request.namespace?.workspaceId, + workspacePath: request.workspacePath ?? request.namespace?.workspacePath + }); const existing = request.sessionId ? this.repos.runtime.getSession(request.sessionId) : namespace.sessionKey @@ -1808,8 +2125,8 @@ export class MemoryService { userId: namespace.userId, source: request.source ?? namespace.source, profileId: request.profileId ?? namespace.profileId, - projectId: request.projectId ?? namespace.projectId ?? namespace.workspaceId, - workspaceId: request.workspaceId ?? namespace.workspaceId, + projectId: namespace.projectId, + workspaceId: namespace.workspaceId, conversationId: stringFromMeta(request.meta, "conversationId"), status: "open", resumed: false, @@ -1997,23 +2314,41 @@ export class MemoryService { } private assertSessionInScope(session: SessionRecord, namespace?: RuntimeNamespace): void { - void session; - void namespace; + if (namespace && !sameProjectScope(namespaceForSession(session), namespace)) { + throw new MemoryServiceError("not_found", `session not found: ${session.id}`); + } } private assertMemoryInScope(memory: MemoryRow, namespace?: RuntimeNamespace): void { - void memory; - void namespace; + if (namespace && !sameProjectScope(this.namespaceForMemoryScope(memory), namespace)) { + throw new MemoryServiceError("not_found", `memory not found: ${memory.id}`); + } } private assertEpisodeInScope(episode: EpisodeRecord, namespace?: RuntimeNamespace): void { - void episode; - void namespace; + const session = this.repos.runtime.getSession(episode.sessionId); + const actual = session + ? namespaceForSession(session) + : { source: DEFAULT_NAMESPACE_SOURCE, profileId: "default", userId: episode.userId, projectId: episode.projectId }; + if (namespace && !sameProjectScope(actual, namespace)) { + throw new MemoryServiceError("not_found", `episode not found: ${episode.id}`); + } } private assertRawTurnInScope(rawTurn: RawTurnRecord, namespace?: RuntimeNamespace): void { - void rawTurn; - void namespace; + const session = this.repos.runtime.getSession(rawTurn.sessionId); + const actual = session ? namespaceForSession(session) : namespaceForRawTurn(rawTurn); + if (namespace && !sameProjectScope(actual, namespace)) { + throw new MemoryServiceError("not_found", `raw turn not found: ${rawTurn.id}`); + } + } + + private namespaceForMemoryScope(memory: MemoryRow): RuntimeNamespace { + if (memory.sessionId) { + const session = this.repos.runtime.getSession(memory.sessionId); + if (session) return namespaceForSession(session); + } + return namespaceForMemory(memory); } @@ -2245,21 +2580,156 @@ function namespaceIdFromSession(session: SessionRecord): string { } function namespaceIdFromContext(namespace: RuntimeNamespace): string { - return [ - namespace.tenantId, - namespace.userId, - namespace.projectId ?? namespace.workspaceId, - namespace.source, - namespace.profileId - ].filter(Boolean).join(":"); + return canonicalNamespaceIdFromContext(namespace); } function scopeBundleTables( tables: Record>>, namespace: RuntimeNamespace ): Record>> { - void namespace; - return tables; + const normalized = normalizeNamespace(namespace); + const memoryIds = new Set(); + const sessionIds = new Set(); + const episodeIds = new Set(); + const rawTurnIds = new Set(); + + const memories = tables.memories ?? []; + for (const row of memories) { + const sessionId = stringField(row, "session_id"); + const projectId = memoryProjectId(row, tables.sessions); + const tenantId = memoryTenantId(row, tables.sessions); + if (sameProjectScope({ source: "unknown", profileId: "default", projectId, tenantId, userId: stringField(row, "user_id") }, normalized)) { + const id = stringField(row, "id"); + if (id) memoryIds.add(id); + if (sessionId) sessionIds.add(sessionId); + } + } + for (const row of tables.sessions ?? []) { + const id = stringField(row, "id"); + if (!id) continue; + const projectId = stringField(row, "project_id") ?? jsonStringField(row, "meta_json", "project_id"); + const tenantId = jsonStringField(row, "meta_json", "tenant_id") ?? jsonStringField(row, "meta_json", "tenantId"); + if (sameProjectScope({ source: stringField(row, "source") ?? "unknown", profileId: stringField(row, "profile_id") ?? "default", projectId, tenantId, userId: stringField(row, "user_id") }, normalized)) sessionIds.add(id); + } + for (const row of tables.episodes ?? []) { + const id = stringField(row, "id"); + const sessionId = stringField(row, "session_id"); + if (id && ((sessionId && sessionIds.has(sessionId)) || (!sessionId && sameProjectScope({ source: "unknown", profileId: "default", projectId: stringField(row, "project_id"), tenantId: "local", userId: stringField(row, "user_id") }, normalized)))) episodeIds.add(id); + } + for (const row of tables.raw_turns ?? []) { + const id = stringField(row, "id"); + const sessionId = stringField(row, "session_id"); + const episodeId = stringField(row, "episode_id"); + if (id && ((sessionId && sessionIds.has(sessionId)) || (episodeId && episodeIds.has(episodeId)))) rawTurnIds.add(id); + } + + const scoped = (table: string, rows: Array>): Array> => rows.filter((row) => { + if (table === "memories") return memoryIds.has(stringField(row, "id") ?? ""); + if (table === "memory_vectors") return memoryIds.has(stringField(row, "memory_id") ?? ""); + if (table === "sessions") return sessionIds.has(stringField(row, "id") ?? ""); + if (table === "episodes") return episodeIds.has(stringField(row, "id") ?? ""); + if (table === "raw_turns") return rawTurnIds.has(stringField(row, "id") ?? ""); + if (table === "memory_relations") return memoryIds.has(stringField(row, "source_memory_id") ?? "") && memoryIds.has(stringField(row, "target_memory_id") ?? ""); + if (table === "memory_change_log") return stringField(row, "namespace_id") === namespaceIdFromContext(normalized) || memoryIds.has(stringField(row, "memory_id") ?? "") || memoryIds.has(stringField(row, "entity_id") ?? ""); + if (table === "embedding_retry_queue" || table === "memory_processing_state") return memoryIds.has(stringField(row, "target_id") ?? stringField(row, "memory_id") ?? ""); + if (table === "trace_policy_links") return memoryIds.has(stringField(row, "l1_memory_id") ?? "") && memoryIds.has(stringField(row, "l2_memory_id") ?? ""); + if (table === "l2_candidate_pool") return memoryIds.has(stringField(row, "source_memory_id") ?? ""); + if (table === "skill_trials") return rowReferencesSets(row, sessionIds, episodeIds, rawTurnIds, memoryIds, ["skill_memory_id", "l1_memory_id"]); + if (table === "artifacts") return rowReferencesSets(row, sessionIds, episodeIds, rawTurnIds, memoryIds, []); + if (table === "feedback" || table === "decision_repairs" || table === "evolution_jobs") return rowReferencesSets(row, sessionIds, episodeIds, rawTurnIds, memoryIds, ["l1_memory_id", "target_memory_id"]); + if (table === "recall_events") return stringField(row, "namespace_id") === namespaceIdFromContext(normalized) || rowReferencesSets(row, sessionIds, episodeIds, rawTurnIds, memoryIds, []); + if (table === "api_logs" || table === "audit_logs") return bundleLogReferencesScope(row, sessionIds, episodeIds, rawTurnIds, memoryIds, normalized); + return false; + }); + + const result: Record>> = {}; + for (const [table, rows] of Object.entries(tables)) result[table] = scoped(table, rows); + return result; +} + +function bundleTableRowCount(tables: Record>>): number { + return Object.values(tables).reduce((total, rows) => total + rows.length, 0); +} + +function stringField(row: Record, key: string): string | undefined { + const value = row[key]; + return typeof value === "string" && value ? value : undefined; +} + +function jsonStringField(row: Record, jsonKey: string, key: string): string | undefined { + const raw = stringField(row, jsonKey); + if (!raw) return undefined; + try { + const parsed = JSON.parse(raw) as Record; + return typeof parsed[key] === "string" && parsed[key] ? parsed[key] as string : undefined; + } catch { + return undefined; + } +} + +function memoryProjectId(row: Record, sessions: Array> | undefined): string | undefined { + const sessionId = stringField(row, "session_id"); + const session = sessions?.find((candidate) => stringField(candidate, "id") === sessionId); + return stringField(row, "project_id") ?? jsonStringField(row, "info_json", "project_id") ?? stringField(row, "app_id") ?? stringField(session ?? {}, "project_id"); +} + +function memoryTenantId(row: Record, sessions: Array> | undefined): string | undefined { + const sessionId = stringField(row, "session_id"); + const session = sessions?.find((candidate) => stringField(candidate, "id") === sessionId); + return jsonStringField(row, "info_json", "tenant_id") ?? jsonStringField(row, "info_json", "tenantId") ?? jsonStringField(session ?? {}, "meta_json", "tenant_id") ?? "local"; +} + +function rowReferencesSets( + row: Record, + sessions: Set, + episodes: Set, + rawTurns: Set, + memories: Set, + extraMemoryKeys: string[] +): boolean { + const refs = [ + ["session_id", sessions], ["episode_id", episodes], ["raw_turn_id", rawTurns], + ...extraMemoryKeys.map((key) => [key, memories] as const) + ] as Array<[string, Set]>; + return refs.some(([key, ids]) => { + const value = stringField(row, key); + return Boolean(value && ids.has(value)); + }); +} + +function bundleLogReferencesScope( + row: Record, + sessions: Set, + episodes: Set, + rawTurns: Set, + memories: Set, + namespace: RuntimeNamespace +): boolean { + const actor = parseBundleJson(row.actor_json); + const input = parseBundleJson(row.input_json); + const output = parseBundleJson(row.output_json); + const namespaceRecord = isRecord(actor) ? actor : undefined; + if (namespaceRecord && sameProjectScope({ source: "unknown", profileId: "default", projectId: stringValue(namespaceRecord, "projectId") ?? stringValue(namespaceRecord, "project_id"), tenantId: stringValue(namespaceRecord, "tenantId") ?? stringValue(namespaceRecord, "tenant_id") }, namespace)) return true; + return [row, input, output].some((value) => bundleValueReferencesSets(value, sessions, episodes, rawTurns, memories)); +} + +function bundleValueReferencesSets(value: unknown, sessions: Set, episodes: Set, rawTurns: Set, memories: Set): boolean { + if (Array.isArray(value)) return value.some((item) => bundleValueReferencesSets(item, sessions, episodes, rawTurns, memories)); + if (!isRecord(value)) return false; + for (const [key, next] of Object.entries(value)) { + if (typeof next === "string" && ((key.toLowerCase().includes("session") && sessions.has(next)) || (key.toLowerCase().includes("episode") && episodes.has(next)) || (key.toLowerCase().includes("raw_turn") && rawTurns.has(next)) || ((key.toLowerCase().includes("memory") || key.toLowerCase().includes("trace") || key.toLowerCase().includes("span")) && memories.has(next)))) return true; + if (bundleValueReferencesSets(next, sessions, episodes, rawTurns, memories)) return true; + } + return false; +} + +function parseBundleJson(value: unknown): unknown { + if (typeof value !== "string" || !value) return undefined; + try { return JSON.parse(value) as unknown; } catch { return undefined; } +} + +function stringValue(value: Record, key: string): string | undefined { + return typeof value[key] === "string" && value[key] ? value[key] as string : undefined; } function uniq(values: T[]): T[] { diff --git a/Memory/src/service/namespace/namespace-scope.ts b/Memory/src/service/namespace/namespace-scope.ts index 539ab1c1c..ab0933330 100644 --- a/Memory/src/service/namespace/namespace-scope.ts +++ b/Memory/src/service/namespace/namespace-scope.ts @@ -1,15 +1,20 @@ -import type { MemoryRow, RuntimeNamespace, SessionOpenRequest } from "../../types.js"; +import type { MemoryFilter, MemoryRow, RuntimeNamespace, SessionOpenRequest } from "../../types.js"; import { DEFAULT_NAMESPACE_SOURCE } from "../../types.js"; import type { RawTurnRecord, SessionRecord } from "../../storage/repositories.js"; +import { stableHash } from "../../utils/id.js"; + +export const GLOBAL_PROJECT_ID = "global"; export function normalizeNamespace(namespace?: RuntimeNamespace): RuntimeNamespace & { userId: string; source: string; profileId: string } { + const workspacePath = normalizeWorkspacePath(namespace?.workspacePath); + const workspaceId = clean(namespace?.workspaceId) ?? (workspacePath ? workspaceIdFromPath(workspacePath) : undefined); return { source: namespace?.source ?? DEFAULT_NAMESPACE_SOURCE, profileId: namespace?.profileId ?? "default", profileLabel: namespace?.profileLabel, - projectId: namespace?.projectId, - workspaceId: namespace?.workspaceId, - workspacePath: namespace?.workspacePath, + projectId: clean(namespace?.projectId) ?? workspaceId, + workspaceId, + workspacePath, sessionKey: namespace?.sessionKey, userId: namespace?.userId ?? "local-user", tenantId: namespace?.tenantId @@ -17,21 +22,41 @@ export function normalizeNamespace(namespace?: RuntimeNamespace): RuntimeNamespa } export function sessionScopeForOpenRequest(request: SessionOpenRequest, namespace: RuntimeNamespace): Partial> { + const resolved = normalizeNamespace({ + ...namespace, + source: request.source ?? namespace.source, + profileId: request.profileId ?? namespace.profileId, + projectId: request.projectId ?? namespace.projectId, + workspaceId: request.workspaceId ?? namespace.workspaceId, + workspacePath: request.workspacePath ?? namespace.workspacePath + }); return { - source: request.source ?? request.namespace?.source, - profileId: request.profileId ?? request.namespace?.profileId, - projectId: request.projectId ?? request.namespace?.projectId ?? request.namespace?.workspaceId, - workspaceId: request.workspaceId ?? request.namespace?.workspaceId, - workspacePath: request.workspacePath ?? request.namespace?.workspacePath ?? namespace.workspacePath + source: resolved.source, + profileId: resolved.profileId, + projectId: resolved.projectId, + workspaceId: resolved.workspaceId, + workspacePath: resolved.workspacePath }; } export function namespaceForSession(session: SessionRecord): RuntimeNamespace { - return { source: session.source, profileId: session.profileId, profileLabel: session.profileLabel, projectId: session.projectId, workspaceId: session.workspaceId, workspacePath: session.workspacePath, sessionKey: session.hostSessionKey, userId: session.userId }; + return { source: session.source, profileId: session.profileId, profileLabel: session.profileLabel, projectId: session.projectId, workspaceId: session.workspaceId, workspacePath: session.workspacePath, sessionKey: session.hostSessionKey, userId: session.userId, tenantId: tenantIdFromSession(session) }; } export function namespaceForMemory(memory: MemoryRow): RuntimeNamespace { - return { source: memory.agentId ?? DEFAULT_NAMESPACE_SOURCE, profileId: profileIdFromMemory(memory) ?? "default", projectId: projectIdFromMemory(memory), workspaceId: memory.appId, userId: memory.userId }; + return { source: memory.agentId ?? DEFAULT_NAMESPACE_SOURCE, profileId: profileIdFromMemory(memory) ?? "default", projectId: projectIdFromMemory(memory), workspaceId: memory.appId, userId: memory.userId, tenantId: tenantIdFromMemory(memory) }; +} + +export function tenantIdFromSession(session: SessionRecord): string | undefined { + const value = session.meta.tenant_id ?? session.meta.tenantId; + return typeof value === "string" && value.trim() ? value.trim() : undefined; +} + +export function tenantIdFromMemory(memory: MemoryRow): string | undefined { + const direct = memory.info.tenant_id ?? memory.info.tenantId; + if (typeof direct === "string" && direct.trim()) return direct.trim(); + const nested = memory.properties.info?.tenant_id ?? memory.properties.info?.tenantId; + return typeof nested === "string" && nested.trim() ? nested.trim() : undefined; } export function projectIdFromMemory(memory: MemoryRow): string | undefined { @@ -53,3 +78,68 @@ export function profileIdFromMemory(memory: MemoryRow): string | undefined { export function namespaceForRawTurn(rawTurn: RawTurnRecord): RuntimeNamespace { return { source: DEFAULT_NAMESPACE_SOURCE, profileId: "default", sessionKey: rawTurn.sessionId, userId: rawTurn.userId }; } + +/** + * Stable project boundary shared by every agent adapter. Agent source and + * profile are provenance, not isolation keys. + */ +export function namespaceIdFromContext(namespace: RuntimeNamespace): string { + const normalized = normalizeNamespace(namespace); + return [ + normalized.tenantId ?? "local", + normalized.projectId ?? "unscoped" + ].join(":"); +} + +export function workspaceIdFromPath(workspacePath: string): string { + const normalized = normalizeWorkspacePath(workspacePath); + if (!normalized) return ""; + return `workspace_${stableHash({ path: normalized }).slice(0, 24)}`; +} + +export function sameProjectScope( + actual: RuntimeNamespace, + requested: RuntimeNamespace | undefined +): boolean { + if (!requested) return true; + const expected = normalizeNamespace(requested); + const observed = normalizeNamespace(actual); + if ((observed.tenantId ?? "local") !== (expected.tenantId ?? "local")) return false; + + // A scoped request never inherits legacy/unscoped memories. Unscoped + // callers remain limited to the unscoped quarantine. + return (observed.projectId ?? "unscoped") === (expected.projectId ?? "unscoped"); +} + +export function hasProjectScope(namespace: RuntimeNamespace | undefined): boolean { + if (!namespace) return false; + return Boolean( + clean(namespace.projectId) || + clean(namespace.workspaceId) || + normalizeWorkspacePath(namespace.workspacePath) + ); +} + +export function memoryFilterForNamespace(namespace: RuntimeNamespace): MemoryFilter { + const normalized = normalizeNamespace(namespace); + return { + tenantId: normalized.tenantId ?? "local", + projectId: normalized.projectId ?? "unscoped" + }; +} + +function normalizeWorkspacePath(value: string | undefined): string | undefined { + const trimmed = clean(value); + if (!trimmed) return undefined; + let normalized = trimmed.replace(/\\/g, "/").replace(/\/{2,}/g, "/"); + if (/^[A-Z]:\//.test(normalized)) { + normalized = normalized[0]!.toLowerCase() + normalized.slice(1); + } + if (normalized.length > 1) normalized = normalized.replace(/\/+$/, ""); + return normalized || "/"; +} + +function clean(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed || undefined; +} diff --git a/Memory/src/service/read-model/episode.ts b/Memory/src/service/read-model/episode.ts index f9e0ba24a..ba61f6b7d 100644 --- a/Memory/src/service/read-model/episode.ts +++ b/Memory/src/service/read-model/episode.ts @@ -84,6 +84,16 @@ export interface EpisodeReadModelRepositories { getMany(ids: readonly string[]): MemoryRow[]; list(filter: { memoryLayer?: MemoryLayer[]; tags?: string[] }, limit: number, cursor: number): MemoryRow[]; toListItem(memory: MemoryRow): MemoryListItem; + relationsFor(id: string): Array<{ + id: string; + projectId?: string; + sourceMemoryId: string; + targetMemoryId: string; + relation: "supersedes"; + reason?: string; + actor: Record; + createdAt: string; + }>; }; processing: { get(memoryId: string): MemoryProcessingRecord | undefined; @@ -228,7 +238,10 @@ export class EpisodeReadModel { const memory = this.deps.repos.memories.get(id); if (!memory) throw this.deps.notFound(`memory not found: ${id}`); this.deps.assertMemoryInScope(memory, request.namespace); - const detail = this.deps.detailFromMemory(memory, this.deps.repos.processing.get(memory.id)); + const detail = { + ...this.deps.detailFromMemory(memory, this.deps.repos.processing.get(memory.id)), + relations: this.deps.repos.memories.relationsFor(memory.id) + }; const refs = this.refsForMemory(memory); const item = this.deps.memoryDetailWithLayerPayload(detail, memory); if (memory.properties.internal_info.memory_kind === "span") { diff --git a/Memory/src/service/read-model/memory.ts b/Memory/src/service/read-model/memory.ts index 2335485b2..39a9270be 100644 --- a/Memory/src/service/read-model/memory.ts +++ b/Memory/src/service/read-model/memory.ts @@ -7,10 +7,21 @@ import { firstLine } from "../../utils/text.js"; export function detailFromMemory(memory: MemoryRow, processing?: MemoryProcessingRecord): MemoryDetailItem { const sourceMemoryIds = memory.properties.internal_info.source_memory_ids; + const provenance = isRecord(memory.properties.internal_info.provenance) + ? memory.properties.internal_info.provenance as unknown as MemoryDetailItem["provenance"] + : undefined; + const supersedesMemoryIds = stringArray(memory.properties.internal_info.supersedes_memory_ids); + const supersededByMemoryId = stringFromMaybeRecord(memory.properties.internal_info, "superseded_by_memory_id"); + const supersessionReason = stringFromMaybeRecord(memory.properties.internal_info, "supersession_reason"); return { id: memory.id, kind: kindFromMemory(memory), memoryLayer: memory.memoryLayer, status: memory.status, title: detailTitleForMemory(memory), summary: detailSummaryForMemory(memory), tags: panelTagsForMemory(memory, processing), updatedAt: memory.updatedAt, version: memory.version, processing, body: memory.memoryValue, createdAt: memory.createdAt, - sourceMemoryIds: stringArray(sourceMemoryIds), metadata: { source: panelSourceForMemory(memory), info: memory.info, properties: memory.properties } }; + sourceMemoryIds: stringArray(sourceMemoryIds), + provenance, + supersession: supersedesMemoryIds.length || supersededByMemoryId + ? { supersedesMemoryIds, supersededByMemoryId, reason: supersessionReason } + : undefined, + metadata: { source: panelSourceForMemory(memory), info: memory.info, properties: memory.properties } }; } export function detailTitleForMemory(memory: MemoryRow): string { diff --git a/Memory/src/service/read-model/panel-read.ts b/Memory/src/service/read-model/panel-read.ts index 794f951bb..4e0878033 100644 --- a/Memory/src/service/read-model/panel-read.ts +++ b/Memory/src/service/read-model/panel-read.ts @@ -3,7 +3,9 @@ import { isRecord } from "../../utils/json.js"; import type { StorageBackendCapabilities } from "../../storage/backend.js"; import type { ApiLogRecord, + AuditLogRecord, ChangeLogRecord, + EmbeddingRetryRecord, EmbeddingRetryStatus, EpisodeRecord, EvolutionJobRecord, @@ -11,6 +13,14 @@ import type { Repositories } from "../../storage/repositories.js"; import { detailSummaryForMemory } from "./memory.js"; +import { + memoryFilterForNamespace, + namespaceForMemory, + namespaceForSession, + namespaceIdFromContext, + normalizeNamespace, + sameProjectScope +} from "../namespace/namespace-scope.js"; import type { HealthResponse, MemoryFilter, @@ -92,13 +102,17 @@ export class PanelReadModel { items: ReturnType; serverTime: string; } { + const context = this.deps.resolveContext(input); + const limit = input.limit ?? 50; + const items = this.deps.repos.runtime.listAudit({ + userId: input.userId ?? context.userId, + targetKind: input.targetKind, + targetId: input.targetId, + limit: input.namespace ? 10_000 : limit + }).filter((audit) => this.auditMatchesNamespace(audit, input.namespace ? context.namespace : undefined)) + .slice(0, limit); return { - items: this.deps.repos.runtime.listAudit({ - userId: input.userId ?? this.deps.resolveContext(input).userId, - targetKind: input.targetKind, - targetId: input.targetId, - limit: input.limit - }), + items, serverTime: this.now() }; } @@ -131,11 +145,11 @@ export class PanelReadModel { limit, cursor: input.cursor }); - const audits = this.deps.repos.runtime.listAudit({ limit }); + const audits = this.auditLogs({ ...input, limit }).items; const jobs = [ - ...this.deps.repos.runtime.listJobs("failed", limit), - ...this.deps.repos.runtime.listJobs("dead_letter", limit) - ].slice(0, limit); + ...this.scopedJobs("failed", input, limit), + ...this.scopedJobs("dead_letter", input, limit) + ].sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt)).slice(0, limit); const entries = [ ...changes.items.map((change) => ({ type: "change" as const, @@ -181,7 +195,7 @@ export class PanelReadModel { }; } - apiLogs(input: { + apiLogs(input: RequestEnvelope & { tools?: Array<"memory_add" | "memory_search" | "skill_generate" | "skill_evolve">; sourceAgent?: string; excludedSourceAgents?: string[]; @@ -197,19 +211,26 @@ export class PanelReadModel { } { const limit = Math.max(1, Math.min(input.limit ?? 50, 500)); const offset = Math.max(0, input.offset ?? 0); + const queryLimit = input.namespace ? 10_000 : limit; const result = this.deps.repos.runtime.listApiLogs({ toolNames: input.tools, sourceAgent: input.sourceAgent, excludedSourceAgents: input.excludedSourceAgents, - limit, - offset + limit: queryLimit, + offset: input.namespace ? 0 : offset }); + const context = input.namespace ? this.deps.resolveContext(input) : undefined; + const scopedLogs = context + ? result.logs.filter((log) => this.apiLogMatchesNamespace(log, context.namespace)) + : result.logs; + const logs = input.namespace ? scopedLogs.slice(offset, offset + limit) : scopedLogs; + const total = input.namespace ? scopedLogs.length : result.total; return { - logs: result.logs.map((log) => this.withCurrentTraceSummaries(log)), - total: result.total, + logs: logs.map((log) => this.withCurrentTraceSummaries(log)), + total, limit, offset, - nextOffset: offset + result.logs.length < result.total ? offset + result.logs.length : undefined, + nextOffset: offset + logs.length < total ? offset + logs.length : undefined, serverTime: this.now() }; } @@ -269,7 +290,10 @@ export class PanelReadModel { schema: this.deps.schemaVersion(), memory: overview.stats, changeSeq: overview.latestChangeSeq, - feedback: { recent: this.deps.repos.runtime.listFeedback({ limit: 1000 }).length }, + feedback: { + recent: this.deps.repos.runtime.listFeedback({ limit: 1000 }) + .filter((feedback) => this.entityReferencesNamespace(feedback, input.namespace)).length + }, jobs: overview.stats.jobs, embeddingRetries: overview.stats.embeddingRetries, models: this.deps.models(), @@ -284,11 +308,13 @@ export class PanelReadModel { deadLetterJobs: EvolutionJobRecord[]; serverTime: string; } { + const failedJobs = this.scopedJobs("failed", input, 20); + const deadLetterJobs = this.scopedJobs("dead_letter", input, 20); return { health: this.deps.health(routes), overview: this.panelOverview(input), - failedJobs: this.deps.repos.runtime.listJobs("failed", 20), - deadLetterJobs: this.deps.repos.runtime.listJobs("dead_letter", 20), + failedJobs, + deadLetterJobs, serverTime: this.now() }; } @@ -308,7 +334,7 @@ export class PanelReadModel { }; } - panelOverview(_input: RequestEnvelope & { userId?: string } = {}): { + panelOverview(input: RequestEnvelope & { userId?: string } = {}): { stats: { byLayer: Record; byStatus: Record<"activated" | "resolving" | "archived" | "deleted", number>; @@ -324,16 +350,20 @@ export class PanelReadModel { etag: string; serverTime: string; } { - const byLayer = this.memoryLayerCounts(); - const byStatus = this.memoryStatusCounts(); - const latestChangeSeq = this.deps.repos.runtime.latestChangeSeq(); - const jobs = this.jobStatusCounts(); - const embeddingRetries = this.embeddingRetryStatusCounts(); + const memories = this.listAllMemoriesForStats(input); + const byLayer = this.memoryLayerCounts(memories); + const byStatus = this.memoryStatusCounts(memories); + const context = input.namespace ? this.deps.resolveContext(input) : undefined; + const namespaceId = context ? namespaceIdFromContext(context.namespace) : undefined; + const userId = input.userId ?? context?.userId; + const latestChangeSeq = this.deps.repos.runtime.latestChangeSeq(userId, namespaceId); + const jobs = this.jobStatusCounts(input); + const embeddingRetries = this.embeddingRetryStatusCounts(input); return { stats: { byLayer, byStatus, - episodes: this.episodeStatusCounts(), + episodes: this.episodeStatusCounts(input), jobs, embeddingRetries, lastChangeSeq: latestChangeSeq || undefined @@ -341,18 +371,18 @@ export class PanelReadModel { counts: byLayer, queuedJobs: jobs.queued, latestChangeSeq, - cursor: this.deps.encodeChangeCursor(latestChangeSeq), + cursor: this.deps.encodeChangeCursor(latestChangeSeq, context?.namespace), etag: `panel-overview-v${latestChangeSeq}`, serverTime: this.now() }; } - panelOverviewSummary(_input: RequestEnvelope & { userId?: string } = {}): { + panelOverviewSummary(input: RequestEnvelope & { userId?: string } = {}): { counts: { memories: number; skills: number; experiences: number; worldModels: number }; sourceDistribution: Array<{ source: string; count: number; percentage: number }>; dailyActivity: Array<{ date: string; count: number }>; } { - const memories = this.listAllMemoriesForStats(); + const memories = this.listAllMemoriesForStats(input); const dates = panelDateKeys(this.now(), PANEL_DAILY_ACTIVITY_DAYS); return { counts: { @@ -366,7 +396,7 @@ export class PanelReadModel { }; } - panelAnalysis(_input: RequestEnvelope & { userId?: string } = {}): { + panelAnalysis(input: RequestEnvelope & { userId?: string } = {}): { metrics: { avgRecallScore: number; recallEvents: number; @@ -383,9 +413,9 @@ export class PanelReadModel { }; } { const dates = panelLastSevenDateKeys(this.now()); - const memories = this.listAllMemoriesForStats(); + const memories = this.listAllMemoriesForStats(input); const skillMemories = memories.filter((memory) => memory.memoryLayer === "Skill"); - const logs = this.deps.repos.runtime.listApiLogs({ limit: 10_000, offset: 0 }).logs + const logs = this.apiLogs({ ...input, limit: 10_000, offset: 0 }).logs .filter((log) => dates.includes(panelDateKey(log.calledAt))); const recallScores = logs .filter((log) => log.toolName === "memory_search") @@ -432,6 +462,8 @@ export class PanelReadModel { } { const pageSize = normalizePanelItemsLimit(input.limit); const filter: MemoryFilter = { + ...(input.namespace ? memoryFilterForNamespace(input.namespace) : {}), + ...(input.userId ? { userId: input.userId } : {}), memoryLayer: input.layer, status: input.status, tags: input.tags, @@ -489,13 +521,16 @@ export class PanelReadModel { } { const pageSize = 20 as const; const query = input.q?.trim() || undefined; - const userId = input.namespace?.userId; - const total = this.deps.repos.runtime.countEpisodes(userId, query); + const context = input.namespace ? this.deps.resolveContext(input) : undefined; + const userId = input.namespace?.userId ?? context?.userId; + const episodes = this.deps.repos.runtime.listEpisodes(userId, 10_000, 0, query) + .filter((episode) => this.episodeMatchesNamespace(episode, context?.namespace)); + const total = episodes.length; const totalPages = Math.max(1, Math.ceil(total / pageSize)); const page = Math.min(normalizePageNumber(input.page), totalPages); - const episodes = this.deps.repos.runtime.listEpisodes(userId, pageSize, (page - 1) * pageSize, query); + const pageEpisodes = episodes.slice((page - 1) * pageSize, page * pageSize); return { - tasks: episodes.map((episode) => ({ + tasks: pageEpisodes.map((episode) => ({ id: episode.id, episode: this.deps.episodeRef(episode), memoryIds: episode.l1MemoryIds.filter((memoryId) => Boolean(this.deps.repos.memories.get(memoryId))), @@ -524,11 +559,14 @@ export class PanelReadModel { serverTime: string; } { const limit = input.limit ?? 50; - const cursorSeq = this.deps.decodeChangeCursor(input.cursor); - const items = this.deps.repos.runtime.listChanges(undefined, limit, cursorSeq); + const context = input.namespace ? this.deps.resolveContext(input) : undefined; + const namespaceId = context ? namespaceIdFromContext(context.namespace) : undefined; + const userId = input.userId ?? context?.userId; + const cursorSeq = this.deps.decodeChangeCursor(input.cursor, context?.namespace); + const items = this.deps.repos.runtime.listChanges(userId, limit, cursorSeq, namespaceId); const lastSeq = items.reduce((max, item) => Math.max(max, item.seq), cursorSeq); return { - cursor: this.deps.encodeChangeCursor(lastSeq), + cursor: this.deps.encodeChangeCursor(lastSeq, context?.namespace), changes: items.map(changeLogToPanelChange), hasMore: items.length === limit, items, @@ -546,7 +584,7 @@ export class PanelReadModel { nextCursor?: string; serverTime: string; } { - const items = this.deps.repos.runtime.listJobs(input.status, input.limit ?? 50); + const items = this.scopedJobs(input.status, input, input.limit ?? 50); return { jobs: items.map((job) => ({ ...job, @@ -557,42 +595,168 @@ export class PanelReadModel { }; } - private jobStatusCounts(): Record<"queued" | "leased" | "succeeded" | "failed" | "dead_letter", number> { + private scopedJobs( + status: EvolutionJobRecord["status"] | undefined, + input: RequestEnvelope & { userId?: string }, + limit: number + ): EvolutionJobRecord[] { + const context = input.namespace ? this.deps.resolveContext(input) : undefined; + const userId = input.userId ?? context?.userId; + return this.deps.repos.runtime.listJobs(status, input.namespace ? 10_000 : limit, userId) + .filter((job) => !userId || job.userId === userId) + .filter((job) => this.jobMatchesNamespace(job, context?.namespace)) + .slice(0, limit); + } + + private jobMatchesNamespace(job: EvolutionJobRecord, namespace: RuntimeNamespace | undefined): boolean { + if (!namespace) return true; + if (job.sessionId) { + const session = this.deps.repos.runtime.getSession(job.sessionId); + if (session) return sameProjectScope(namespaceForSession(session), namespace); + } + if (job.episodeId) { + const episode = this.deps.repos.runtime.getEpisode(job.episodeId); + if (episode) return this.episodeMatchesNamespace(episode, namespace); + } + if (job.targetMemoryId) { + const memory = this.deps.repos.memories.get(job.targetMemoryId); + if (memory) return sameProjectScope(namespaceForMemory(memory), namespace); + } + return this.entityReferencesNamespace(job.payload, namespace); + } + + private episodeMatchesNamespace(episode: EpisodeRecord, namespace: RuntimeNamespace | undefined): boolean { + if (!namespace) return true; + const session = this.deps.repos.runtime.getSession(episode.sessionId); + if (session) return sameProjectScope(namespaceForSession(session), namespace); + return sameProjectScope({ + source: "unknown", + profileId: "default", + userId: episode.userId, + tenantId: "local", + projectId: episode.projectId + }, namespace); + } + + private auditMatchesNamespace(audit: AuditLogRecord, namespace: RuntimeNamespace | undefined): boolean { + if (!namespace) return true; + if (audit.sessionId) { + const session = this.deps.repos.runtime.getSession(audit.sessionId); + if (session) return sameProjectScope(namespaceForSession(session), namespace); + } + if (this.entityReferencesNamespace(audit.actor, namespace)) return true; + return this.idMatchesNamespace(audit.targetId, namespace) || + this.entityReferencesNamespace(audit.after, namespace) || + this.entityReferencesNamespace(audit.before, namespace); + } + + private apiLogMatchesNamespace(log: ApiLogRecord, namespace: RuntimeNamespace): boolean { + try { + const input = JSON.parse(log.inputJson) as unknown; + const output = JSON.parse(log.outputJson) as unknown; + return this.entityReferencesNamespace(input, namespace) || this.entityReferencesNamespace(output, namespace); + } catch { + return false; + } + } + + private embeddingRetryMatchesNamespace(retry: EmbeddingRetryRecord, namespace: RuntimeNamespace | undefined): boolean { + if (!namespace) return true; + const memory = this.deps.repos.memories.get(retry.targetId); + return Boolean(memory && sameProjectScope(namespaceForMemory(memory), namespace)); + } + + private entityReferencesNamespace(value: unknown, namespace: RuntimeNamespace | undefined): boolean { + if (!namespace) return true; + if (!isRecord(value)) return false; + const embedded = isRecord(value.namespace) ? value.namespace : value; + const embeddedNamespace = runtimeNamespaceFromRecord(embedded); + if (embeddedNamespace && sameProjectScope(embeddedNamespace, namespace)) return true; + + for (const key of ["sessionId", "session_id", "episodeId", "episode_id", "rawTurnId", "raw_turn_id", "memoryId", "memory_id", "targetMemoryId", "target_memory_id", "traceId", "trace_id", "spanId", "span_id"]) { + const id = value[key]; + if (typeof id === "string" && this.idMatchesNamespace(id, namespace)) return true; + } + for (const key of ["details", "candidates", "filtered", "items"]) { + const entries = value[key]; + if (Array.isArray(entries) && entries.some((entry) => this.entityReferencesNamespace(entry, namespace))) return true; + } + return false; + } + + private idMatchesNamespace(id: string, namespace: RuntimeNamespace): boolean { + const session = this.deps.repos.runtime.getSession(id); + if (session) return sameProjectScope(namespaceForSession(session), namespace); + const episode = this.deps.repos.runtime.getEpisode(id); + if (episode) return this.episodeMatchesNamespace(episode, namespace); + const rawTurn = this.deps.repos.runtime.getRawTurn(id); + if (rawTurn) { + const rawSession = this.deps.repos.runtime.getSession(rawTurn.sessionId); + return Boolean(rawSession && sameProjectScope(namespaceForSession(rawSession), namespace)); + } + const memory = this.deps.repos.memories.get(id); + return Boolean(memory && sameProjectScope(namespaceForMemory(memory), namespace)); + } + + private jobStatusCounts(input: RequestEnvelope & { userId?: string }): Record<"queued" | "leased" | "succeeded" | "failed" | "dead_letter", number> { return { - queued: this.deps.repos.runtime.listJobs("queued", 1000).length, - leased: this.deps.repos.runtime.listJobs("leased", 1000).length, - succeeded: this.deps.repos.runtime.listJobs("succeeded", 1000).length, - failed: this.deps.repos.runtime.listJobs("failed", 1000).length, - dead_letter: this.deps.repos.runtime.listJobs("dead_letter", 1000).length + queued: this.scopedJobs("queued", input, 10_000).length, + leased: this.scopedJobs("leased", input, 10_000).length, + succeeded: this.scopedJobs("succeeded", input, 10_000).length, + failed: this.scopedJobs("failed", input, 10_000).length, + dead_letter: this.scopedJobs("dead_letter", input, 10_000).length }; } - private memoryLayerCounts(): Record { - return this.deps.repos.memories.countByLayer(); + private memoryLayerCounts(memories: ReturnType): Record { + return { + L1: memories.filter((memory) => memory.memoryLayer === "L1").length, + L2: memories.filter((memory) => memory.memoryLayer === "L2").length, + L3: memories.filter((memory) => memory.memoryLayer === "L3").length, + Skill: memories.filter((memory) => memory.memoryLayer === "Skill").length + }; } - private memoryStatusCounts(): Record<"activated" | "resolving" | "archived" | "deleted", number> { - return this.deps.repos.memories.countByStatus(); + private memoryStatusCounts(memories: ReturnType): Record<"activated" | "resolving" | "archived" | "deleted", number> { + return { + activated: memories.filter((memory) => memory.status === "activated").length, + resolving: memories.filter((memory) => memory.status === "resolving").length, + archived: memories.filter((memory) => memory.status === "archived").length, + deleted: memories.filter((memory) => memory.status === "deleted").length + }; } - private episodeStatusCounts(): Record<"open" | "processing" | "closed", number> { - return this.deps.repos.runtime.countEpisodesByStatus(); + private episodeStatusCounts(input: RequestEnvelope & { userId?: string }): Record<"open" | "processing" | "closed", number> { + const context = input.namespace ? this.deps.resolveContext(input) : undefined; + const episodes = this.deps.repos.runtime.listEpisodes(input.userId ?? context?.userId, 10_000) + .filter((episode) => this.episodeMatchesNamespace(episode, context?.namespace)); + return { + open: episodes.filter((episode) => episode.status === "open").length, + processing: episodes.filter((episode) => episode.status === "processing").length, + closed: episodes.filter((episode) => episode.status === "closed").length + }; } - private embeddingRetryStatusCounts(): Record<"pending" | "in_progress" | "succeeded" | "failed", number> { + private embeddingRetryStatusCounts(input: RequestEnvelope & { userId?: string }): Record<"pending" | "in_progress" | "succeeded" | "failed", number> { const statuses: EmbeddingRetryStatus[] = ["pending", "in_progress", "succeeded", "failed"]; const counts = { pending: 0, in_progress: 0, succeeded: 0, failed: 0 }; for (const status of statuses) { - counts[status] = this.deps.repos.runtime.countEmbeddingRetriesByStatus(status); + counts[status] = this.deps.repos.runtime.listEmbeddingRetries(status, 10_000, input.userId) + .filter((retry) => this.embeddingRetryMatchesNamespace(retry, input.namespace)).length; } return counts; } - private listAllMemoriesForStats() { + private listAllMemoriesForStats(input: RequestEnvelope & { userId?: string } = {}) { const rows = [] as ReturnType; const pageSize = 1000; + const context = input.namespace ? this.deps.resolveContext(input) : undefined; + const filter: MemoryFilter = { + ...(context ? memoryFilterForNamespace(context.namespace) : {}), + ...(input.userId ?? context?.userId ? { userId: input.userId ?? context?.userId } : {}) + }; for (let offset = 0;; offset += pageSize) { - const batch = this.deps.repos.memories.list({}, pageSize, offset); + const batch = this.deps.repos.memories.list(filter, pageSize, offset); rows.push(...batch); if (batch.length < pageSize) break; } @@ -600,6 +764,24 @@ export class PanelReadModel { } } +function runtimeNamespaceFromRecord(value: Record): RuntimeNamespace | undefined { + const stringValue = (key: string): string | undefined => typeof value[key] === "string" ? value[key] as string : undefined; + const projectId = stringValue("projectId") ?? stringValue("project_id"); + const workspaceId = stringValue("workspaceId") ?? stringValue("workspace_id"); + const workspacePath = stringValue("workspacePath") ?? stringValue("workspace_path"); + const tenantId = stringValue("tenantId") ?? stringValue("tenant_id"); + if (!projectId && !workspaceId && !workspacePath && !tenantId) return undefined; + return normalizeNamespace({ + source: stringValue("source") ?? "unknown", + profileId: stringValue("profileId") ?? stringValue("profile_id") ?? "default", + userId: stringValue("userId") ?? stringValue("user_id"), + tenantId, + projectId, + workspaceId, + workspacePath + }); +} + export function redactConfig(value: unknown): unknown { if (Array.isArray(value)) return value.map(redactConfig); if (!isRecord(value)) return value; diff --git a/Memory/src/service/read-model/skill.ts b/Memory/src/service/read-model/skill.ts index f73d7ce3e..83dc50722 100644 --- a/Memory/src/service/read-model/skill.ts +++ b/Memory/src/service/read-model/skill.ts @@ -19,6 +19,7 @@ import type { RuntimeNamespace, SkillUseRequest } from "../../types.js"; +import { memoryFilterForNamespace } from "../namespace/namespace-scope.js"; export type SkillServiceErrorCode = | "invalid_argument" @@ -211,6 +212,8 @@ export class SkillReadModel { const limit = input.limit ?? 50; const cursor = input.cursor ?? 0; const filter: MemoryFilter = { + ...(input.namespace ? memoryFilterForNamespace(input.namespace) : {}), + ...(input.userId ? { userId: input.userId } : {}), memoryLayer: "Skill", status: ["activated", "resolving"], tags: input.tags diff --git a/Memory/src/service/retrieval/indexed-candidate-pool.ts b/Memory/src/service/retrieval/indexed-candidate-pool.ts index 7eeb499b3..05ceca1fa 100644 --- a/Memory/src/service/retrieval/indexed-candidate-pool.ts +++ b/Memory/src/service/retrieval/indexed-candidate-pool.ts @@ -14,6 +14,7 @@ import type { MemoryLayer, MemoryRow } from "../../types.js"; +import { projectIdFromMemory } from "../namespace/namespace-scope.js"; export function dedupeStrings(values: readonly string[]): string[] { const out: string[] = []; @@ -37,8 +38,10 @@ export class IndexedCandidatePool { retrievalCandidateCount(input: { layers: MemoryLayer[]; tags?: string[]; + scope: MemoryFilter; }): number { const baseFilter: MemoryFilter = { + ...input.scope, memoryLayer: input.layers, status: ["activated", "resolving"] }; @@ -48,9 +51,11 @@ export class IndexedCandidatePool { hasRetrievalVectorCandidates(input: { layers: MemoryLayer[]; tags?: string[]; + scope: MemoryFilter; }): boolean { if (input.layers.length === 0) return false; const baseFilter: MemoryFilter = { + ...input.scope, memoryLayer: input.layers, status: ["activated", "resolving"] }; @@ -63,6 +68,7 @@ export class IndexedCandidatePool { layers: MemoryLayer[]; tags?: string[]; targetSkillId?: string; + scope: MemoryFilter; config: { tier1TopK: number; tier2TopK: number; @@ -84,6 +90,7 @@ export class IndexedCandidatePool { for (const layer of layers) { const filter: MemoryFilter = { + ...input.scope, memoryLayer: layer, status: ["activated", "resolving"], ...(input.tags?.length ? { tags: input.tags } : {}) @@ -132,7 +139,10 @@ export class IndexedCandidatePool { channelScoresByMemory.set(hit.id, scores); } return { - memories: this.deps.repos.memories.getMany(candidateIds).filter((memory) => this.isMemoryReadyForRetrieval(memory)), + memories: this.deps.repos.memories.getMany(candidateIds).filter((memory) => + this.projectIdForMemory(memory) === input.scope.projectId && + this.isMemoryReadyForRetrieval(memory) + ), channelScoresByMemory }; } @@ -176,6 +186,16 @@ export class IndexedCandidatePool { return false; } + private projectIdForMemory(memory: MemoryRow): string { + const direct = projectIdFromMemory(memory); + if (direct) return direct; + if (memory.sessionId) { + const session = this.deps.repos.runtime.getSession(memory.sessionId); + if (session) return session.projectId ?? "unscoped"; + } + return memory.appId ?? "unscoped"; + } + private retrievalVectorPoolSize( layer: MemoryLayer, config: { diff --git a/Memory/src/service/retrieval/retrieval-service.ts b/Memory/src/service/retrieval/retrieval-service.ts index dbd083363..645923225 100644 --- a/Memory/src/service/retrieval/retrieval-service.ts +++ b/Memory/src/service/retrieval/retrieval-service.ts @@ -50,6 +50,7 @@ import type { import { newId, stableHash } from "../../utils/id.js"; import { nowIso } from "../../utils/time.js"; import { recordApiLog } from "../model-audit/model-call-audit.js"; +import { memoryFilterForNamespace } from "../namespace/namespace-scope.js"; import { sourceMemoryIdsFromMemory } from "../read-model/memory.js"; @@ -1214,6 +1215,11 @@ export class RetrievalService { budget: number; total: number; }; + retrievalDebug: RetrievalResult["debug"] & { + candidateCount: number; + scopedProjectId?: string; + scopedWorkspaceId?: string; + }; status: string[]; verbose: boolean; serverTime: string; @@ -1240,6 +1246,7 @@ export class RetrievalService { ) : undefined; const tuning = this.retrievalTuningConfig(); + const scope = memoryFilterForNamespace(context.namespace); const allowedLayers = retrievalLayersForProfile(retrievalLayersForMode(retrievalMode), tuning); const layers = request.layers === undefined ? allowedLayers @@ -1249,7 +1256,8 @@ export class RetrievalService { ? 0 : this.candidatePool.retrievalCandidateCount({ layers, - tags: request.tags + tags: request.tags, + scope }); const retrievalQuery = focusResearchRetrievalQuery(request.query, tuning.domain).text; const queryExtract = candidateCount > 0 ? await this.extractRetrievalQuery(retrievalQuery) : null; @@ -1264,7 +1272,8 @@ export class RetrievalService { limit: retrievalLimit, mode: retrievalMode, excludeTraceRawTurnIds: recentRawTurnIds, - targetSkillId: request.targetSkillId + targetSkillId: request.targetSkillId, + scope }); const retrieval = retrievalOutput.retrieval; const memories = retrievalOutput.memories; @@ -1333,6 +1342,12 @@ export class RetrievalService { budget: budgetAt - rerankAt, total: Date.now() - startedAt }, + retrievalDebug: { + ...retrieval.debug, + candidateCount: memories.length, + scopedProjectId: context.namespace.projectId, + scopedWorkspaceId: context.namespace.workspaceId + }, status: uniq([ ...filteredHits.status, ...(!this.deps.memoryAddEnabled() ? ["memory_add:disabled:no_recall_log"] : []) @@ -1384,6 +1399,7 @@ export class RetrievalService { mode: RetrievalMode; excludeTraceRawTurnIds?: ReadonlySet; targetSkillId?: string; + scope: ReturnType; }): Promise<{ retrieval: RetrievalResult; memories: MemoryRow[] }> { if (input.limit <= 0 || input.layers.length === 0) { return { retrieval: emptyRetrievalResult(), memories: [] }; @@ -1399,7 +1415,8 @@ export class RetrievalService { }); const hasVectorCandidates = this.candidatePool.hasRetrievalVectorCandidates({ layers: input.layers, - tags: input.tags + tags: input.tags, + scope: input.scope }); const queryVector = hasVectorCandidates ? await this.queryVector(queryVectorText) : undefined; const candidatePool = await this.candidatePool.indexedRetrievalCandidatePool({ @@ -1408,6 +1425,7 @@ export class RetrievalService { layers: input.layers, tags: input.tags, targetSkillId: input.targetSkillId, + scope: input.scope, config }); const memories = candidatePool.memories; @@ -1824,6 +1842,7 @@ export class RetrievalService { startedAt: number ): ReturnType { const total = Date.now() - startedAt; + const context = this.deps.resolveContext(request); const tuning = this.retrievalTuningConfig(); const contextPacket = request.includeInjectedContext === false ? { @@ -1858,6 +1877,15 @@ export class RetrievalService { budget: 0, total }, + retrievalDebug: { + tierSizes: { tier1: 0, tier2: 0, tier3: 0 }, + kept: { tier1: 0, tier2: 0, tier3: 0 }, + topRelevance: 0, + droppedByThreshold: 0, + candidateCount: 0, + scopedProjectId: context.namespace.projectId, + scopedWorkspaceId: context.namespace.workspaceId + }, status: ["memory_search:disabled"], verbose: request.verbose === true, serverTime: nowIso() diff --git a/Memory/src/service/session/session-turn-service.ts b/Memory/src/service/session/session-turn-service.ts index 951d39b68..7a63f3ef0 100644 --- a/Memory/src/service/session/session-turn-service.ts +++ b/Memory/src/service/session/session-turn-service.ts @@ -53,6 +53,7 @@ import type { } from "../feedback/feedback-experience.js"; import { recordApiLog } from "../model-audit/model-call-audit.js"; import { + memoryFilterForNamespace, namespaceForRawTurn, namespaceForSession, normalizeNamespace, @@ -304,7 +305,14 @@ export class SessionTurnService { return this.deps.withDuplicateFlag(existing.response) as ReturnType; } } - const namespace = normalizeNamespace(request.namespace); + const namespace = normalizeNamespace({ + ...request.namespace, + source: request.source ?? request.namespace?.source ?? "unknown", + profileId: request.profileId ?? request.namespace?.profileId ?? "default", + projectId: request.projectId ?? request.namespace?.projectId, + workspaceId: request.workspaceId ?? request.namespace?.workspaceId, + workspacePath: request.workspacePath ?? request.namespace?.workspacePath + }); const at = nowIso(); if (request.sessionId) { const existingSession = this.deps.repos.runtime.getSession(request.sessionId); @@ -377,13 +385,18 @@ export class SessionTurnService { source: request.source ?? namespace.source, profileId: request.profileId ?? namespace.profileId, profileLabel: namespace.profileLabel, - projectId: request.projectId ?? namespace.projectId ?? namespace.workspaceId, - workspaceId: request.workspaceId ?? namespace.workspaceId, - workspacePath: request.workspacePath ?? namespace.workspacePath, + projectId: namespace.projectId, + workspaceId: namespace.workspaceId, + workspacePath: namespace.workspacePath, hostSessionKey, conversationId: this.deps.stringFromMeta(request.meta, "conversationId"), status: "open" as const, - meta: request.meta ?? {}, + meta: { + ...(request.meta ?? {}), + ...(request.protocolVersion ? { protocolVersion: request.protocolVersion } : {}), + ...(request.provenance ? { provenance: request.provenance } : {}), + tenant_id: namespace.tenantId ?? "local" + }, openedAt: at, lastSeenAt: at, updatedAt: at @@ -539,7 +552,8 @@ export class SessionTurnService { contextPacketId, sourceTurnIds, sourceMemoryIds, - tokenEstimate: request.tokenEstimate + tokenEstimate: request.tokenEstimate, + ...(request.checkpoint ? { checkpoint: request.checkpoint } : {}) } }, status: "succeeded", @@ -787,6 +801,8 @@ export class SessionTurnService { contextPacketId, searchEventId: search.searchEventId, sourceMemoryIds: search.sourceMemoryIds, + protocolVersion: request.protocolVersion, + provenance: request.provenance, intent_decision: intentDecision, ...(endTopicDecision ? { @@ -924,6 +940,8 @@ export class SessionTurnService { const requestToolResults = normalizeCompleteTurnToolResults(completionRequest); const requestArtifacts = normalizeCompleteTurnArtifacts(completionRequest); const turnStartPayload = { + protocolVersion: request.protocolVersion, + provenance: request.provenance, intent_decision: intentDecision, ...(turnStartRecall ? { @@ -964,7 +982,9 @@ export class SessionTurnService { turn_start: turnStartPayload, turn_complete: { completed_at: at, - source_memory_ids: sourceMemoryIds + source_memory_ids: sourceMemoryIds, + protocolVersion: request.protocolVersion, + provenance: request.provenance } }, status: request.status ?? "succeeded", @@ -1045,6 +1065,21 @@ export class SessionTurnService { appId: session.workspaceId, projectId: session.projectId, profileId: session.profileId, + workspacePath: session.workspacePath, + provenance: { + ...request.provenance, + sourceAgent: session.source, + tenantId: namespaceForSession(session).tenantId ?? "local", + profileId: session.profileId, + projectId: session.projectId, + workspaceId: session.workspaceId, + workspacePath: session.workspacePath, + sessionId: session.id, + turnId: step.turnId, + adapterId: request.adapterId ?? request.provenance?.adapterId, + requestId: request.requestId ?? request.provenance?.requestId, + sourceMemoryIds: rawTurn.sourceMemoryIds + }, layer: "L1", kind: "trace", memoryType: "LongTermMemory", @@ -1617,6 +1652,7 @@ export class SessionTurnService { const policies = this.deps.repos.memories.search( query, { + ...memoryFilterForNamespace(namespaceForSession(input.session)), memoryLayer: "L2", status: "activated" }, @@ -1626,6 +1662,7 @@ export class SessionTurnService { const l1Hits = this.deps.repos.memories.search( query, { + ...memoryFilterForNamespace(namespaceForSession(input.session)), memoryLayer: "L1", status: "activated" }, @@ -1874,6 +1911,7 @@ export class SessionTurnService { const repairLayers = retrievalLayersForMode("decision_repair"); const candidates = this.deps.repos.memories.list( { + ...memoryFilterForNamespace(namespaceForSession(session)), memoryLayer: repairLayers, status: ["activated", "resolving"] }, diff --git a/Memory/src/service/trials/skill-trial-resolver.ts b/Memory/src/service/trials/skill-trial-resolver.ts index b02e327bf..c81dd331d 100644 --- a/Memory/src/service/trials/skill-trial-resolver.ts +++ b/Memory/src/service/trials/skill-trial-resolver.ts @@ -28,6 +28,7 @@ import { MemoryServiceError } from "../../utils/error.js"; import { nowIso } from "../../utils/time.js"; import { recordApiLog } from "../model-audit/model-call-audit.js"; import { + namespaceIdFromContext as canonicalNamespaceIdFromContext, namespaceForMemory } from "../namespace/namespace-scope.js"; import { @@ -453,13 +454,7 @@ function namespaceIdFromMemory(memory: MemoryRow): string { } function namespaceIdFromContext(namespace: RuntimeNamespace): string { - return [ - namespace.tenantId, - namespace.userId, - namespace.projectId ?? namespace.workspaceId, - namespace.source, - namespace.profileId - ].filter(Boolean).join(":"); + return canonicalNamespaceIdFromContext(namespace); } function numberOr(value: unknown, fallback: number): number { diff --git a/Memory/src/service/turn/turn-normalization.ts b/Memory/src/service/turn/turn-normalization.ts index 1b6dbd017..0bc1ce159 100644 --- a/Memory/src/service/turn/turn-normalization.ts +++ b/Memory/src/service/turn/turn-normalization.ts @@ -13,7 +13,7 @@ export function buildRepairSuggestionQuery(request: RepairSuggestionRequest): st export function sanitizeTurnStartRequest>(request: T): T { return { ...request, query: sanitizeMemmyProtocolText(String(request.query ?? "")) }; } export function sanitizeTurnCompleteRequest>(request: T): T { const toolCalls = Array.isArray(request.toolCalls) ? request.toolCalls : []; return { ...request, query: sanitizeMemmyProtocolText(String(request.query ?? "")), answer: sanitizeMemmyProtocolText(String(request.answer ?? "")), toolCalls: Array.isArray(request.toolCalls) ? request.toolCalls.map(sanitizeMemmyProtocolValue) : request.toolCalls, toolResults: Array.isArray(request.toolResults) ? request.toolResults.map((result, index) => sanitizeCompleteTurnToolResult(result, toolNameFromToolCall(toolCalls[index]))) : request.toolResults }; } export function sanitizeMemoryAddRequest(request: T): T { return { ...request, content: sanitizeMemmyProtocolText(request.content ?? ""), title: typeof request.title === "string" ? sanitizeMemmyProtocolText(request.title) : request.title }; } -export function completeObservedRawTurn(existing: RawTurnRecord, request: TurnCompleteRequest & Record, completedAt: string): RawTurnRecord { const toolCalls = normalizeCompleteTurnToolCalls(request); const toolResults = normalizeCompleteTurnToolResults(request); return { ...existing, userText: request.query ?? existing.userText, assistantText: request.answer, reasoningSummary: stringFromMaybeRecord(request, "reasoningSummary") ?? existing.reasoningSummary, toolCalls: toolCalls.length ? toolCalls : existing.toolCalls, toolResults: toolResults.length ? toolResults : existing.toolResults, sourceMemoryIds: normalizeCompleteTurnSourceMemoryIds(request, existing.sourceMemoryIds), usage: isRecord(request.usage) ? request.usage : existing.usage, messagePayload: { ...(existing.messagePayload ?? {}), turn_complete: { completed_at: completedAt, source_memory_ids: normalizeCompleteTurnSourceMemoryIds(request, existing.sourceMemoryIds) } }, status: request.status ?? "succeeded" }; } +export function completeObservedRawTurn(existing: RawTurnRecord, request: TurnCompleteRequest & Record, completedAt: string): RawTurnRecord { const toolCalls = normalizeCompleteTurnToolCalls(request); const toolResults = normalizeCompleteTurnToolResults(request); return { ...existing, userText: request.query ?? existing.userText, assistantText: request.answer, reasoningSummary: stringFromMaybeRecord(request, "reasoningSummary") ?? existing.reasoningSummary, toolCalls: toolCalls.length ? toolCalls : existing.toolCalls, toolResults: toolResults.length ? toolResults : existing.toolResults, sourceMemoryIds: normalizeCompleteTurnSourceMemoryIds(request, existing.sourceMemoryIds), usage: isRecord(request.usage) ? request.usage : existing.usage, messagePayload: { ...(existing.messagePayload ?? {}), turn_complete: { completed_at: completedAt, source_memory_ids: normalizeCompleteTurnSourceMemoryIds(request, existing.sourceMemoryIds), protocolVersion: request.protocolVersion, provenance: request.provenance } }, status: request.status ?? "succeeded" }; } export function normalizeCompleteTurnSourceMemoryIds(request: TurnCompleteRequest & Record, fallback: string[] = []): string[] { return Array.isArray(request.sourceMemoryIds) ? request.sourceMemoryIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0) : fallback; } export function normalizeCompleteTurnArtifacts(request: TurnCompleteRequest): NormalizedCompleteTurnArtifact[] { if (!Array.isArray(request.artifacts)) return []; return request.artifacts.map((artifact) => { if (!isRecord(artifact)) return null; const normalized: NormalizedCompleteTurnArtifact = { kind: stringFromRecord(artifact, "kind") ?? "artifact", payload: artifact }; const uri = stringFromRecord(artifact, "uri"); if (uri) normalized.uri = uri; return normalized; }).filter((artifact): artifact is NormalizedCompleteTurnArtifact => Boolean(artifact)); } export function normalizeCompleteTurnToolCalls(request: TurnCompleteRequest): ToolCallPayload[] { const results = normalizeCompleteTurnToolResults(request); return (Array.isArray(request.toolCalls) ? request.toolCalls : []).map((call, index) => normalizeCompleteTurnToolCall(call, results[index])).filter((call): call is ToolCallPayload => Boolean(call)); } diff --git a/Memory/src/service/worker/job-handlers.ts b/Memory/src/service/worker/job-handlers.ts index c144cc439..722a952d3 100644 --- a/Memory/src/service/worker/job-handlers.ts +++ b/Memory/src/service/worker/job-handlers.ts @@ -22,6 +22,7 @@ import { } from "../embedding/embedding-pipeline.js"; import { memoryHasImportPipeline } from "../import/import-job-processor.js"; import { + namespaceIdFromContext as canonicalNamespaceIdFromContext, namespaceForMemory, namespaceForSession } from "../namespace/namespace-scope.js"; @@ -545,11 +546,5 @@ function namespaceIdFromSession(session: SessionRecord): string { } function namespaceIdFromContext(namespace: RuntimeNamespace): string { - return [ - namespace.tenantId, - namespace.userId, - namespace.projectId ?? namespace.workspaceId, - namespace.source, - namespace.profileId - ].filter(Boolean).join(":"); + return canonicalNamespaceIdFromContext(namespace); } diff --git a/Memory/src/storage/repositories.ts b/Memory/src/storage/repositories.ts index f4abd3266..923878c17 100644 --- a/Memory/src/storage/repositories.ts +++ b/Memory/src/storage/repositories.ts @@ -35,6 +35,7 @@ import { type SqlValue = string | number | Buffer | null; const BUNDLE_TABLES = [ "memories", + "memory_relations", "sessions", "episodes", "raw_turns", @@ -349,6 +350,17 @@ export interface AuditLogRecord { createdAt: string; } +export interface MemoryRelationRecord { + id: string; + projectId?: string; + sourceMemoryId: string; + targetMemoryId: string; + relation: "supersedes"; + reason?: string; + actor: Record; + createdAt: string; +} + interface SqlApiLogRow { id: number; tool_name: ApiLogRecord["toolName"]; @@ -870,6 +882,112 @@ export class MemoryRepository { }); } + supersede(input: { + oldMemory: MemoryRow; + newMemory: MemoryRow; + projectId?: string; + reason?: string; + actor?: Record; + createdAt?: string; + }): { oldMemory: MemoryRow; newMemory: MemoryRow; relation: MemoryRelationRecord } { + const at = input.createdAt ?? nowIso(); + const oldInternal = input.oldMemory.properties.internal_info; + const newInternal = input.newMemory.properties.internal_info; + const updatedOld = this.update({ + ...input.oldMemory, + status: "archived", + info: { + ...input.oldMemory.info, + superseded_by_memory_id: input.newMemory.id, + supersession_reason: input.reason + }, + properties: { + ...input.oldMemory.properties, + status: "archived", + internal_info: { + ...oldInternal, + superseded_by_memory_id: input.newMemory.id, + supersession_reason: input.reason + } + }, + updatedAt: at + }); + const updatedNew = this.update({ + ...input.newMemory, + info: { + ...input.newMemory.info, + supersedes_memory_ids: uniq([ + ...asStringArray(input.newMemory.info.supersedes_memory_ids), + input.oldMemory.id + ]), + supersession_reason: input.reason + }, + properties: { + ...input.newMemory.properties, + internal_info: { + ...newInternal, + supersedes_memory_ids: uniq([ + ...asStringArray(newInternal.supersedes_memory_ids), + input.oldMemory.id + ]), + supersession_reason: input.reason + } + }, + updatedAt: at + }); + const relation: MemoryRelationRecord = { + id: `relation_${stableHash({ source: updatedNew.id, target: updatedOld.id, relation: "supersedes" }).slice(0, 24)}`, + projectId: input.projectId, + sourceMemoryId: updatedNew.id, + targetMemoryId: updatedOld.id, + relation: "supersedes", + reason: input.reason, + actor: input.actor ?? {}, + createdAt: at + }; + this.db.prepare( + `INSERT INTO memory_relations ( + id, project_id, source_memory_id, target_memory_id, relation, reason, actor_json, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(source_memory_id, target_memory_id, relation) DO UPDATE SET + reason = excluded.reason, + actor_json = excluded.actor_json, + created_at = excluded.created_at` + ).run( + relation.id, + relation.projectId ?? null, + relation.sourceMemoryId, + relation.targetMemoryId, + relation.relation, + relation.reason ?? null, + toJson(relation.actor), + relation.createdAt + ); + return { oldMemory: updatedOld, newMemory: updatedNew, relation }; + } + + relationsFor(id: string): MemoryRelationRecord[] { + const rows = this.db.prepare( + `SELECT * FROM memory_relations + WHERE source_memory_id = ? OR target_memory_id = ? + ORDER BY created_at ASC, id ASC` + ).all(id, id) as Array<{ + id: string; project_id: string | null; source_memory_id: string; + target_memory_id: string; relation: "supersedes"; reason: string | null; + actor_json: string; created_at: string; + }>; + return rows.map((row) => ({ + id: row.id, + projectId: row.project_id ?? undefined, + sourceMemoryId: row.source_memory_id, + targetMemoryId: row.target_memory_id, + relation: row.relation, + reason: row.reason ?? undefined, + actor: parseJson(row.actor_json, {}), + createdAt: row.created_at + })); + } + softDelete(id: string, deletedAt = nowIso()): MemoryRow | undefined { const memory = this.get(id); if (!memory) { @@ -1365,9 +1483,12 @@ export class RuntimeRepository { } countEpisodesByStatus(userId?: string): Record<"open" | "processing" | "closed", number> { - void userId; const clauses = ["1=1"]; const params: SqlValue[] = []; + if (userId) { + clauses.push("user_id = ?"); + params.push(userId); + } const rows = this.db .prepare( `SELECT status, COUNT(*) AS count @@ -1911,10 +2032,16 @@ export class RuntimeRepository { } latestChangeSeq(userId?: string, namespaceId?: string): number { - void userId; - void namespaceId; const clauses = ["1=1"]; const params: SqlValue[] = []; + if (userId) { + clauses.push("user_id = ?"); + params.push(userId); + } + if (namespaceId) { + clauses.push("namespace_id = ?"); + params.push(namespaceId); + } const row = this.db .prepare(`SELECT MAX(seq) AS seq FROM memory_change_log WHERE ${clauses.join(" AND ")}`) .get(...params) as @@ -1924,10 +2051,16 @@ export class RuntimeRepository { } listChanges(userId?: string, limit = 50, cursor?: number, namespaceId?: string): ChangeLogRecord[] { - void userId; - void namespaceId; const clauses = ["1=1"]; const params: SqlValue[] = []; + if (userId) { + clauses.push("user_id = ?"); + params.push(userId); + } + if (namespaceId) { + clauses.push("namespace_id = ?"); + params.push(namespaceId); + } if (cursor) { clauses.push("seq > ?"); params.push(cursor); @@ -2049,13 +2182,16 @@ export class RuntimeRepository { } listJobs(status?: JobStatus, limit = 50, userId?: string): EvolutionJobRecord[] { - void userId; const clauses: string[] = []; const params: SqlValue[] = []; if (status) { clauses.push("status = ?"); params.push(status); } + if (userId) { + clauses.push("user_id = ?"); + params.push(userId); + } const rows = this.db .prepare( `SELECT * @@ -2475,13 +2611,16 @@ export class RuntimeRepository { userId?: string, offset = 0 ): EmbeddingRetryRecord[] { - void userId; const clauses: string[] = []; const params: SqlValue[] = []; if (status) { clauses.push("q.status = ?"); params.push(status); } + if (userId) { + clauses.push("m.user_id = ?"); + params.push(userId); + } const rows = this.db .prepare( `SELECT q.* @@ -2499,10 +2638,12 @@ export class RuntimeRepository { status: EmbeddingRetryStatus, userId?: string ): number { - void userId; const row = this.db - .prepare(`SELECT COUNT(*) AS count FROM embedding_retry_queue WHERE status = ?`) - .get(status) as { count: number } | undefined; + .prepare(`SELECT COUNT(*) AS count + FROM embedding_retry_queue q + LEFT JOIN memories m ON m.id = q.target_id + WHERE q.status = ?${userId ? " AND m.user_id = ?" : ""}`) + .get(...(userId ? [status, userId] : [status])) as { count: number } | undefined; return row?.count ?? 0; } @@ -2758,7 +2899,7 @@ export class RuntimeRepository { } = {}): SkillTrialRecord[] { const clauses = ["1=1"]; const params: SqlValue[] = []; - void input.userId; + addOptional("user_id", input.userId); addOptional("skill_memory_id", input.skillMemoryId); addOptional("session_id", input.sessionId); addOptional("episode_id", input.episodeId); @@ -3835,6 +3976,9 @@ function buildMemoryWhere(filter: MemoryFilter): { where: string; params: SqlVal const clauses = ["deleted_at IS NULL"]; const params: SqlValue[] = []; + addTenantClause(filter.tenantId); + addValueClause("user_id", filter.userId); + addProjectClause(filter.projectId); addValueClause("session_id", filter.sessionId); addValueClause("conversation_id", filter.conversationId); addAgentIdClause(filter.agentId, filter.excludedAgentIds); @@ -3857,6 +4001,49 @@ function buildMemoryWhere(filter: MemoryFilter): { where: string; params: SqlVal params.push(value); } + function addTenantClause(tenantId: string | undefined): void { + if (tenantId === undefined) return; + const effectiveTenant = `CASE + WHEN session_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM sessions AS tenant_session WHERE tenant_session.id = memories.session_id + ) THEN COALESCE( + NULLIF(json_extract((SELECT tenant_session.meta_json FROM sessions AS tenant_session WHERE tenant_session.id = memories.session_id), '$.tenant_id'), ''), + NULLIF(json_extract((SELECT tenant_session.meta_json FROM sessions AS tenant_session WHERE tenant_session.id = memories.session_id), '$.tenantId'), ''), + 'local' + ) + ELSE COALESCE( + NULLIF(json_extract(info_json, '$.tenant_id'), ''), + NULLIF(json_extract(info_json, '$.tenantId'), ''), + NULLIF(json_extract(properties_json, '$.internal_info.provenance.tenantId'), ''), + 'local' + ) + END`; + clauses.push(`${effectiveTenant} = ?`); + params.push(tenantId); + } + + function addProjectClause(projectId: string | undefined): void { + if (projectId === undefined) return; + const effectiveProject = `CASE + WHEN session_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM sessions AS memory_session WHERE memory_session.id = memories.session_id + ) THEN ( + SELECT memory_session.project_id FROM sessions AS memory_session WHERE memory_session.id = memories.session_id + ) + ELSE COALESCE( + NULLIF(json_extract(info_json, '$.project_id'), ''), + NULLIF(json_extract(info_json, '$.projectId'), ''), + NULLIF(app_id, '') + ) + END`; + if (projectId === "unscoped") { + clauses.push(`${effectiveProject} IS NULL`); + return; + } + clauses.push(`${effectiveProject} = ?`); + params.push(projectId); + } + function addAgentIdClause(value: string | undefined, excludedValues: string[] | undefined): void { if (value?.trim()) { clauses.push("lower(replace(replace(trim(agent_id), '-', '_'), ' ', '_')) = ?"); diff --git a/Memory/src/storage/schema.ts b/Memory/src/storage/schema.ts index f8f0bc64d..111a4cb8a 100644 --- a/Memory/src/storage/schema.ts +++ b/Memory/src/storage/schema.ts @@ -1,7 +1,7 @@ import type Database from "better-sqlite3"; -export const SCHEMA_VERSION = 4; -export const SCHEMA_MIGRATION_ID = "004_memory_processing_state"; +export const SCHEMA_VERSION = 5; +export const SCHEMA_MIGRATION_ID = "005_memory_governance"; const API_LOG_SOURCE_AGENT_MIGRATION_FROM_VERSION = 2; const PROCESSING_TAGS = new Set([ "摘要排队中", @@ -59,6 +59,22 @@ const statements = [ `CREATE INDEX IF NOT EXISTS idx_memories_key_layer ON memories (memory_key, memory_layer)`, + `CREATE TABLE IF NOT EXISTS memory_relations ( + id TEXT PRIMARY KEY, + project_id TEXT, + source_memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE, + target_memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE, + relation TEXT NOT NULL CHECK (relation IN ('supersedes')), + reason TEXT, + actor_json TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(actor_json)), + created_at TEXT NOT NULL, + UNIQUE (source_memory_id, target_memory_id, relation) + )`, + `CREATE INDEX IF NOT EXISTS idx_memory_relations_source + ON memory_relations (source_memory_id, relation, created_at DESC)`, + `CREATE INDEX IF NOT EXISTS idx_memory_relations_target + ON memory_relations (target_memory_id, relation, created_at DESC)`, + `CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5 ( id UNINDEXED, identifier, @@ -460,7 +476,7 @@ export function migrate(db: Database.Database): void { const hasMemories = tableExists(db, "memories"); const version = currentSchemaVersion(db); - if (hasMemories && version !== SCHEMA_VERSION && version !== 2 && version !== 3) { + if (hasMemories && version !== SCHEMA_VERSION && version !== 2 && version !== 3 && version !== 4) { throw new Error( `Unsupported memory database schema version ${version}; the database was left unchanged` ); diff --git a/Memory/src/types.ts b/Memory/src/types.ts index 97206e100..53c0d8ce0 100644 --- a/Memory/src/types.ts +++ b/Memory/src/types.ts @@ -4,6 +4,25 @@ export type Cursor = string; export type MemoryLayer = "L1" | "L2" | "L3" | "Skill"; export type MemoryKind = "trace" | "span" | "policy" | "world_model" | "skill"; export type MemoryStatus = "activated" | "resolving" | "archived" | "deleted"; +export type MemoryRelation = "supersedes"; + +export interface MemoryProvenance { + sourceAgent: string; + tenantId?: string; + profileId?: string; + projectId?: string; + workspaceId?: string; + workspacePath?: string; + sessionId?: string; + turnId?: string; + adapterId?: string; + requestId?: string; + sourceMemoryIds: string[]; + repository?: string; + branch?: string; + commit?: string; + capturedAt: IsoTime; +} export type RetrievalMode = | "search" | "turn_start" @@ -129,6 +148,9 @@ export interface MemoryRow { } export interface MemoryFilter { + tenantId?: string; + userId?: string; + projectId?: string; sessionId?: string; conversationId?: string; agentId?: string; @@ -192,6 +214,22 @@ export interface MemoryDetailItem extends MemoryListItem { createdAt: IsoTime; sourceMemoryIds: string[]; metadata: Record; + provenance?: MemoryProvenance; + relations?: Array<{ + id: string; + projectId?: string; + sourceMemoryId: string; + targetMemoryId: string; + relation: MemoryRelation; + reason?: string; + actor: Record; + createdAt: IsoTime; + }>; + supersession?: { + supersedesMemoryIds: string[]; + supersededByMemoryId?: string; + reason?: string; + }; } export interface RawTurnSummary { @@ -227,6 +265,28 @@ export interface SessionCompactRequest extends RequestEnvelope { sourceMemoryIds?: string[]; tokenEstimate?: number; createL1?: boolean; + checkpoint?: SessionCheckpointPayload; +} + +export interface SessionCheckpointPayload { + task: string; + changes: string[]; + validated: string[]; + unverified: string[]; + nextSteps: string[]; +} + +export interface SessionCheckpointRequest extends RequestEnvelope { + episodeId?: string; + task: string; + changes?: string[]; + validated?: string[]; + unverified?: string[]; + nextSteps?: string[]; + sourceTurnIds?: string[]; + sourceMemoryIds?: string[]; + tokenEstimate?: number; + createL1?: boolean; } export interface SessionOpenRequest extends RequestEnvelope { @@ -237,6 +297,8 @@ export interface SessionOpenRequest extends RequestEnvelope { workspacePath?: string; sessionId?: string; meta?: Record; + protocolVersion?: string; + provenance?: Partial; } export interface TurnStartRequest extends RequestEnvelope { @@ -245,6 +307,8 @@ export interface TurnStartRequest extends RequestEnvelope { turnId?: string; contextHints?: Record; contextBudget?: number; + protocolVersion?: string; + provenance?: Partial; } export interface TurnCompleteRequest extends RequestEnvelope { @@ -258,6 +322,8 @@ export interface TurnCompleteRequest extends RequestEnvelope { toolResults?: unknown[]; artifacts?: unknown[]; sourceMemoryIds?: string[]; + protocolVersion?: string; + provenance?: Partial; usage?: Record; status?: "succeeded" | "failed" | "cancelled"; } @@ -324,6 +390,10 @@ export interface MemoryAddRequest extends RequestEnvelope { turnId?: string; createdAt?: string; deferProcessing?: boolean; + sourceMemoryIds?: string[]; + provenance?: Partial; + supersedesMemoryId?: string; + supersessionReason?: string; } export interface FeedbackTarget { @@ -375,6 +445,15 @@ export interface MemoryGovernanceRequest extends RequestEnvelope { reason?: string; } +export interface MemoryMarkdownExportRequest extends RequestEnvelope { + includeArchived?: boolean; +} + +export interface MemoryMarkdownImportRequest extends RequestEnvelope { + markdown: string; + apply?: boolean; +} + export interface RawTurnRedactRequest extends RequestEnvelope { reason?: string; mode?: "redact" | "delete"; diff --git a/Memory/tests/contract/memory-rest-service.test.ts b/Memory/tests/contract/memory-rest-service.test.ts index 04ba953c6..260471408 100644 --- a/Memory/tests/contract/memory-rest-service.test.ts +++ b/Memory/tests/contract/memory-rest-service.test.ts @@ -99,6 +99,40 @@ describe("MemoryService / REST contract", () => { db.close(); }); + it("serves structured session checkpoints through the REST client", async () => { + const { db, service } = createTestService(); + const server = createMemoryHttpServer({ service }); + await withServerClosed(server, async () => { + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("expected TCP address"); + const client = new MemoryRestClient({ endpoint: `http://127.0.0.1:${address.port}` }); + const opened = await client.openSession({ + sessionId: "checkpoint-rest-session", + source: "codex", + workspacePath: "/work/checkpoint-rest" + }) as { sessionId: string }; + const result = await client.checkpointSession(opened.sessionId, { + task: "Prepare a resumable handoff", + changes: ["Stored structured fields"], + validated: ["REST route exercised"], + unverified: ["Production load"], + nextSteps: ["Resume from checkpoint"] + }) as { + checkpointId: string; + checkpoint: { task: string; nextSteps: string[] }; + l1MemoryId?: string; + }; + expect(result.checkpointId).toMatch(/^raw_/u); + expect(result.checkpoint).toMatchObject({ + task: "Prepare a resumable handoff", + nextSteps: ["Resume from checkpoint"] + }); + expect(result.l1MemoryId).toMatch(/^trace_/u); + }); + db.close(); + }); + it("serves the manual memory-processing retry endpoint", async () => { const root = mkdtempSync(join(tmpdir(), "mindock-memory-http-processing-retry-")); roots.push(root); @@ -280,7 +314,10 @@ describe("MemoryService / REST contract", () => { const completeResponse = await fetch(baseUrl + "/turns/cursor-http-turn/complete", { method: "POST", - headers: { "content-type": "application/json" }, + headers: { + "content-type": "application/json", + "x-memmy-workspace-path": "/tmp/hook-workspace" + }, body: JSON.stringify({ adapterId: "memmy-cursor-hook", requestId: "cursor-complete:http-fields", @@ -912,7 +949,7 @@ describe("MemoryService / REST contract", () => { } }) }); - expect(crossNamespace.status).toBe(200); + expect(crossNamespace.status).toBe(403); const sessionResponse = await fetch(`${baseUrl}/sessions/open`, { method: "POST", @@ -995,6 +1032,10 @@ describe("MemoryService / REST contract", () => { const body = await response.json() as { debug: { hits: Array<{ id: string; tags: string[] }>; + retrievalDebug: { + candidateCount: number; + tierSizes: Record; + }; }; }; @@ -1002,7 +1043,78 @@ describe("MemoryService / REST contract", () => { expect(body.debug.hits).toHaveLength(1); expect(body.debug.hits[0]?.tags).toContain("conv-26"); expect(body.debug.hits[0]?.tags).not.toContain("conv-30"); + expect(body.debug.retrievalDebug.candidateCount).toBe(2); + expect(body.debug.retrievalDebug.tierSizes).toBeDefined(); + + }); + db.close(); + }); + + it("serves scoped Markdown audit export, preview, apply, and isolation", async () => { + const { db, service } = createTestService(); + const projectA = { + source: "codex", + profileId: "main", + userId: "markdown-http-user", + projectId: "project-a", + workspaceId: "workspace-a", + workspacePath: "/work/project-a" + }; + const projectB = { ...projectA, projectId: "project-b", workspaceId: "workspace-b", workspacePath: "/work/project-b" }; + service.addMemory({ namespace: projectA, layer: "L2", title: "Project A policy", content: "Only project A should be exported.", deferProcessing: true }); + service.addMemory({ namespace: projectB, layer: "L2", title: "Project B policy", content: "This must stay out of project A.", deferProcessing: true }); + const server = createMemoryHttpServer({ + service, + auth: { + cloudAccessTokens: { + "markdown-http-token": { + source: "codex", + profileId: "main", + userId: "markdown-http-user", + projectId: "project-a", + workspaceId: "workspace-a" + } + } + } + }); + await withServerClosed(server, async () => { + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("expected TCP address"); + const endpoint = `http://127.0.0.1:${address.port}/api/v1/memory/audit/markdown`; + const headers = { authorization: "Bearer markdown-http-token" }; + const exported = await fetch(endpoint, { headers }); + const exportedBody = await exported.json() as { markdown: string; count: number }; + expect(exported.status).toBe(200); + expect(exportedBody.count).toBe(1); + expect(exportedBody.markdown).toContain("Project A policy"); + expect(exportedBody.markdown).not.toContain("Project B policy"); + + const editedMarkdown = exportedBody.markdown + .replace("Project A policy", "Project A reviewed policy") + .replace("Only project A should be exported.", "Project A was reviewed through REST."); + const preview = await fetch(endpoint + "/import", { + method: "POST", + headers: { ...headers, "content-type": "application/json" }, + body: JSON.stringify({ markdown: editedMarkdown, apply: false }) + }); + expect(preview.status).toBe(200); + expect(await preview.json()).toMatchObject({ applied: false, count: 1, updated: [expect.any(String)] }); + + const applied = await fetch(endpoint + "/import", { + method: "POST", + headers: { ...headers, "content-type": "application/json" }, + body: JSON.stringify({ markdown: editedMarkdown, apply: true }) + }); + expect(applied.status).toBe(200); + expect(await applied.json()).toMatchObject({ applied: true, count: 1, updated: [expect.any(String)] }); + const crossProject = await fetch(endpoint + "/import", { + method: "POST", + headers: { ...headers, "content-type": "application/json" }, + body: JSON.stringify({ namespace: projectB, markdown: editedMarkdown, apply: true }) + }); + expect(crossProject.status).toBe(403); }); db.close(); }); @@ -1143,9 +1255,9 @@ describe("MemoryService / REST contract", () => { authorization: "Bearer reader-b" } }); - const crossUserBody = await crossUserGet.json() as { id: string }; - expect(crossUserGet.status).toBe(200); - expect(crossUserBody.id).toBe(complete.l1MemoryId); + const crossUserBody = await crossUserGet.json() as { error: { code: string } }; + expect(crossUserGet.status).toBe(404); + expect(crossUserBody.error.code).toBe("not_found"); }); db.close(); }); diff --git a/Memory/tests/service/bundle/bundle.test.ts b/Memory/tests/service/bundle/bundle.test.ts index ccaf83c03..08d2d9a44 100644 --- a/Memory/tests/service/bundle/bundle.test.ts +++ b/Memory/tests/service/bundle/bundle.test.ts @@ -9,7 +9,7 @@ const { afterEach(cleanup); describe("MemoryService / bundle", () => { - it("exports bundles across namespaces", async () => { + it("exports only the requested namespace and its dependency rows", async () => { const { db, service } = createTestService(); const namespaceA = { source: "codex", @@ -56,25 +56,25 @@ describe("MemoryService / bundle", () => { const bundleA = service.exportBundle({ namespace: namespaceA }); const memoryIds = (bundleA.tables.memories as Array>).map((row) => row.id); expect(memoryIds).toContain(completeA.l1MemoryId); - expect(memoryIds).toContain(completeB.l1MemoryId); + expect(memoryIds).not.toContain(completeB.l1MemoryId); const sessionIds = (bundleA.tables.sessions as Array>).map((row) => row.id); - expect(sessionIds.sort()).toEqual([sessionA.sessionId, sessionB.sessionId].sort()); + expect(sessionIds).toEqual([sessionA.sessionId]); const rawTurnIds = (bundleA.tables.raw_turns as Array>).map((row) => row.id); expect(rawTurnIds).toContain(completeA.rawTurnId); - expect(rawTurnIds).toContain(completeB.rawTurnId); + expect(rawTurnIds).not.toContain(completeB.rawTurnId); const recallIds = (bundleA.tables.recall_events as Array>).map((row) => row.id); expect(recallIds).toContain(recallA.searchEventId); - expect(recallIds).toContain(recallB.searchEventId); + expect(recallIds).not.toContain(recallB.searchEventId); const artifactRawTurnIds = (bundleA.tables.artifacts as Array>) .map((row) => row.raw_turn_id); expect(artifactRawTurnIds).toEqual([completeA.rawTurnId]); const jobSessionIds = new Set((bundleA.tables.evolution_jobs as Array>) .map((row) => row.session_id)); - expect(jobSessionIds).toEqual(new Set([sessionA.sessionId, sessionB.sessionId])); + expect(jobSessionIds).toEqual(new Set([sessionA.sessionId])); const changeNamespaces = new Set((bundleA.tables.memory_change_log as Array>) .map((row) => row.namespace_id)); expect([...changeNamespaces].some((namespace) => String(namespace).includes("workspace-export-a"))).toBe(true); - expect([...changeNamespaces].some((namespace) => String(namespace).includes("workspace-export-b"))).toBe(true); + expect([...changeNamespaces].some((namespace) => String(namespace).includes("workspace-export-b"))).toBe(false); db.close(); }); diff --git a/Memory/tests/service/evolution/orchestration.test.ts b/Memory/tests/service/evolution/orchestration.test.ts index 751b0aa5f..01332c682 100644 --- a/Memory/tests/service/evolution/orchestration.test.ts +++ b/Memory/tests/service/evolution/orchestration.test.ts @@ -180,7 +180,7 @@ describe("MemoryService / evolution / orchestration", () => { ).all() as Array<{ namespace_id: string | null; kind: string | null; op: string | null; entity_id: string | null }>; expect(workerMemoryChanges.length).toBeGreaterThan(0); for (const change of workerMemoryChanges) { - expect(change.namespace_id).toContain("user-2"); + expect(change.namespace_id).toBe("local:unscoped"); expect(change.kind).toMatch(/^(trace|policy|world_model|skill)$/); expect(change.op).toMatch(/^(created|updated)$/); expect(change.entity_id).toBeTruthy(); diff --git a/Memory/tests/service/governance/markdown-audit.test.ts b/Memory/tests/service/governance/markdown-audit.test.ts new file mode 100644 index 000000000..24696afb5 --- /dev/null +++ b/Memory/tests/service/governance/markdown-audit.test.ts @@ -0,0 +1,59 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { createMemoryServiceFixture } from "../../fixtures/memory-service-fixture.js"; + +const { cleanup, createTestService } = createMemoryServiceFixture(); + +afterEach(cleanup); + +describe("MemoryService / governance / markdown audit", () => { + it("exports editable front matter and applies a scoped body/title edit", () => { + const { db, service } = createTestService(); + const namespace = { + source: "codex", + profileId: "main", + userId: "markdown-user", + workspacePath: "/work/markdown-project" + }; + const added = service.addMemory({ + namespace, + layer: "L2", + title: "Original title", + tags: ["audit"], + content: "Original body for the audit file." + }); + const exported = service.exportMarkdown({ namespace }); + expect(exported.count).toBe(1); + expect(exported.markdown).toContain(`id: ${added.id}`); + expect(exported.markdown).toContain("editable:"); + expect(exported.markdown).toContain("Original body for the audit file."); + + const edited = exported.markdown + .replace("Original title", "Reviewed title") + .replace("Original body for the audit file.", "Reviewed body after human audit."); + const preview = service.importMarkdown({ namespace, markdown: edited, apply: false }); + expect(preview.updated).toEqual([added.id]); + expect(service.getMemory(added.id, { namespace })).toMatchObject({ id: added.id }); + + const applied = service.importMarkdown({ namespace, markdown: edited, apply: true }); + expect(applied.updated).toEqual([added.id]); + const detail = service.getMemory(added.id, { namespace }) as { title: string; body: string }; + expect(detail.title).toBe("Reviewed title"); + expect(detail.body).toBe("Reviewed body after human audit."); + expect(db.db.prepare("SELECT COUNT(*) AS count FROM audit_logs WHERE action = 'markdown_update'").get()).toMatchObject({ count: 1 }); + db.close(); + }); + + it("rejects a markdown document from another project", () => { + const { db, service } = createTestService(); + const a = { source: "codex", profileId: "main", userId: "markdown-user", workspacePath: "/work/markdown-a" }; + const b = { source: "codex", profileId: "main", userId: "markdown-user", workspacePath: "/work/markdown-b" }; + service.addMemory({ namespace: a, layer: "L2", title: "Scoped", content: "Only project A." }); + const exported = service.exportMarkdown({ namespace: a }); + const result = service.importMarkdown({ namespace: b, markdown: exported.markdown, apply: true }); + expect(result.updated).toEqual([]); + expect(result.created).toEqual([]); + expect(result.rejected).toHaveLength(1); + expect(result.rejected[0]?.reason).toMatch(/memory not found/); + db.close(); + }); +}); diff --git a/Memory/tests/service/governance/provenance-supersession.test.ts b/Memory/tests/service/governance/provenance-supersession.test.ts new file mode 100644 index 000000000..81ee6eb83 --- /dev/null +++ b/Memory/tests/service/governance/provenance-supersession.test.ts @@ -0,0 +1,177 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { createMemoryServiceFixture } from "../../fixtures/memory-service-fixture.js"; + +const { cleanup, createTestService } = createMemoryServiceFixture(); + +afterEach(cleanup); + +describe("MemoryService / governance / provenance and supersession", () => { + it("carries the agent lifecycle protocol provenance into captured L1 memory", async () => { + const { db, service } = createTestService(); + const namespace = { + source: "codex", + profileId: "main", + userId: "lifecycle-provenance-user", + workspacePath: "/work/lifecycle-provenance" + }; + const opened = service.openSession({ + namespace, + source: "codex", + sessionId: "lifecycle-provenance-session", + workspacePath: namespace.workspacePath, + protocolVersion: "memmy.agent.v1", + adapterId: "memmy-codex-hook", + requestId: "codex-open:lifecycle" + }); + const started = await service.startTurn({ + namespace, + protocolVersion: "memmy.agent.v1", + adapterId: "memmy-codex-hook", + requestId: "codex-start:lifecycle", + sessionId: opened.sessionId, + turnId: "lifecycle-turn", + query: "Capture the adapter provenance", + provenance: { + repository: "/work/memmy-agent", + branch: "feature/protocol", + commit: "abc123def456", + sourceMemoryIds: [], + sourceAgent: "codex", + capturedAt: "2026-08-04T10:00:00.000Z" + } + }); + const completed = service.completeTurn(started.turnId, { + namespace, + protocolVersion: "memmy.agent.v1", + adapterId: "memmy-codex-hook", + requestId: "codex-complete:lifecycle", + sessionId: opened.sessionId, + episodeId: started.episodeId, + query: "Capture the adapter provenance", + answer: "The lifecycle provenance is now captured.", + sourceMemoryIds: started.sourceMemoryIds, + provenance: { + repository: "/work/memmy-agent", + branch: "feature/protocol", + commit: "abc123def456", + sourceMemoryIds: started.sourceMemoryIds, + sourceAgent: "codex", + capturedAt: "2026-08-04T10:01:00.000Z" + } + }); + + const detail = service.getMemory(completed.l1MemoryId, { namespace }) as { + provenance?: Record; + }; + expect(detail.provenance).toMatchObject({ + sourceAgent: "codex", + adapterId: "memmy-codex-hook", + requestId: "codex-complete:lifecycle", + workspacePath: "/work/lifecycle-provenance", + sessionId: opened.sessionId, + turnId: "lifecycle-turn", + repository: "/work/memmy-agent", + branch: "feature/protocol", + commit: "abc123def456", + sourceMemoryIds: started.sourceMemoryIds + }); + const rawTurn = db.db.prepare( + "SELECT message_payload_json FROM raw_turns WHERE id = ?" + ).get(completed.rawTurnId) as { message_payload_json: string }; + expect(JSON.parse(rawTurn.message_payload_json)).toMatchObject({ + turn_start: { protocolVersion: "memmy.agent.v1" }, + turn_complete: { protocolVersion: "memmy.agent.v1" } + }); + db.close(); + }); + + it("records provenance and supersedes an older memory without deleting its history", async () => { + const { db, service } = createTestService(); + const namespace = { + source: "codex", + profileId: "main", + userId: "governance-user", + workspacePath: "/work/governance-project" + }; + const old = service.addMemory({ + namespace, + source: "codex", + layer: "L2", + title: "Old migration policy", + content: "Use the old migration procedure for the legacy database.", + adapterId: "codex-memory-hook", + requestId: "add-old", + provenance: { + repository: "memmy-agent", + branch: "main", + commit: "abc123" + } + }); + const replacement = service.addMemory({ + namespace: { ...namespace, source: "pi", profileId: "pi-main" }, + source: "pi", + layer: "L2", + title: "Current migration policy", + content: "Use the current migration procedure and verify the SQLite backup.", + adapterId: "pi-memory-extension", + requestId: "add-new", + supersedesMemoryId: old.id, + supersessionReason: "The old procedure predates the SQLite migration.", + sourceMemoryIds: [old.id], + provenance: { + repository: "memmy-agent", + branch: "feature/governance", + commit: "def456" + } + }); + + const oldDetail = service.getMemory(old.id, { namespace }) as Extract, { id: string }> & { supersession?: unknown; provenance?: unknown }; + const newDetail = service.getMemory(replacement.id, { namespace }) as Extract, { id: string }> & { supersession?: unknown; provenance?: unknown; relations?: Array> }; + expect(oldDetail.status).toBe("archived"); + expect(oldDetail.supersession).toMatchObject({ + supersededByMemoryId: replacement.id, + reason: "The old procedure predates the SQLite migration." + }); + expect(newDetail.supersession).toMatchObject({ + supersedesMemoryIds: [old.id], + reason: "The old procedure predates the SQLite migration." + }); + expect(newDetail.provenance).toMatchObject({ + sourceAgent: "pi", + adapterId: "pi-memory-extension", + requestId: "add-new", + repository: "memmy-agent", + branch: "feature/governance", + commit: "def456", + sourceMemoryIds: [old.id] + }); + expect(newDetail.relations).toEqual([ + expect.objectContaining({ + sourceMemoryId: replacement.id, + targetMemoryId: old.id, + relation: "supersedes" + }) + ]); + + const relations = db.db.prepare( + `SELECT source_memory_id, target_memory_id, relation, reason + FROM memory_relations + WHERE source_memory_id = ?` + ).all(replacement.id) as Array>; + expect(relations).toEqual([expect.objectContaining({ + source_memory_id: replacement.id, + target_memory_id: old.id, + relation: "supersedes" + })]); + + const recall = await service.search({ + namespace, + query: "old migration procedure legacy database", + layers: ["L2"], + limit: 10 + }); + expect(recall.hits.map((hit) => hit.id)).not.toContain(old.id); + expect(db.db.prepare("SELECT COUNT(*) AS count FROM audit_logs WHERE action = 'supersede'").get()).toMatchObject({ count: 1 }); + db.close(); + }); +}); diff --git a/Memory/tests/service/namespace/project-isolation.test.ts b/Memory/tests/service/namespace/project-isolation.test.ts new file mode 100644 index 000000000..791c647cd --- /dev/null +++ b/Memory/tests/service/namespace/project-isolation.test.ts @@ -0,0 +1,147 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { createMemoryServiceFixture } from "../../fixtures/memory-service-fixture.js"; + +const { cleanup, createTestService } = createMemoryServiceFixture(); + +afterEach(cleanup); + +describe("MemoryService / namespace / project isolation", () => { + it("shares a project across agents while isolating projects A, B, and blank C", async () => { + const { db, service } = createTestService(); + const projectA = { + source: "codex", + profileId: "codex-main", + userId: "local-user", + workspacePath: "/work/project-a" + }; + const projectAFromPi = { + source: "pi", + profileId: "pi-main", + userId: "local-user", + workspacePath: "/work/project-a/" + }; + const projectB = { + source: "claude-code", + profileId: "claude-main", + userId: "local-user", + workspacePath: "/work/project-b" + }; + const blankProjectC = { + source: "codex", + profileId: "codex-main", + userId: "local-user" + }; + + const added = service.addMemory({ + namespace: projectA, + source: "codex", + layer: "L2", + title: "Project A boundary marker", + content: "zircon canary sevenfox only belongs to project A" + }); + + const sameProject = await service.search({ + namespace: projectAFromPi, + query: "zircon canary sevenfox", + includeInjectedContext: true + }); + expect(sameProject.hits.map((hit) => hit.id)).toContain(added.id); + expect(sameProject.injectedContext.markdown).toContain("zircon canary sevenfox"); + + for (const namespace of [projectB, blankProjectC]) { + const result = await service.search({ + namespace, + query: "zircon canary sevenfox", + includeInjectedContext: true + }); + expect(result.hits).toEqual([]); + expect(result.candidateMemoryIds).toEqual([]); + expect(result.injectedContext.markdown).not.toContain("zircon canary sevenfox"); + expect(() => service.getMemory(added.id, { namespace })).toThrow(/memory not found/); + } + + expect(service.getMemory(added.id, { namespace: projectAFromPi }).id).toBe(added.id); + db.close(); + }); + + it("derives the same stable project id from normalized workspace paths", () => { + const { db, service } = createTestService(); + const first = service.openSession({ + namespace: { source: "codex", profileId: "default", userId: "local-user" }, + workspacePath: "C:\\Work\\Memmy\\" + }); + const second = service.openSession({ + namespace: { source: "pi", profileId: "main", userId: "local-user" }, + workspacePath: "c:/Work/Memmy" + }); + + expect(first.projectId).toMatch(/^workspace_[a-f0-9]{24}$/); + expect(second.projectId).toBe(first.projectId); + expect(second.workspaceId).toBe(first.workspaceId); + db.close(); + }); + + it("isolates tenants that use the same project id and persists tenant provenance", async () => { + const { db, service } = createTestService(); + const tenantA = { + source: "codex", + profileId: "default", + userId: "shared-user", + tenantId: "tenant-a", + projectId: "shared-project" + }; + const tenantB = { ...tenantA, tenantId: "tenant-b" }; + const added = service.addMemory({ + namespace: tenantA, + source: "codex", + layer: "L2", + title: "Tenant A marker", + content: "tenant alpha heliotrope marker" + }); + + expect((await service.search({ namespace: tenantA, query: "heliotrope marker" })).hits) + .toEqual(expect.arrayContaining([expect.objectContaining({ id: added.id })])); + expect((await service.search({ namespace: tenantB, query: "heliotrope marker" })).hits).toEqual([]); + expect(() => service.getMemory(added.id, { namespace: tenantB })).toThrow(/memory not found/); + const detail = service.getMemory(added.id, { namespace: tenantA }); + expect("provenance" in detail ? detail.provenance : undefined).toMatchObject({ + tenantId: "tenant-a", + projectId: "shared-project" + }); + + const opened = service.openSession({ namespace: tenantA }); + const session = db.db.prepare("SELECT meta_json FROM sessions WHERE id = ?") + .get(opened.sessionId) as { meta_json: string }; + expect(JSON.parse(session.meta_json)).toMatchObject({ tenant_id: "tenant-a" }); + db.close(); + }); + + it("treats a blank namespace as unscoped for known session, episode, and raw-turn ids", () => { + const { db, service } = createTestService(); + const scoped = { + source: "codex", + profileId: "default", + userId: "local-user", + tenantId: "tenant-a", + projectId: "private-project" + }; + const blank = { + source: "codex", + profileId: "default", + userId: "local-user", + tenantId: "tenant-a" + }; + const opened = service.openSession({ namespace: scoped }); + const completed = service.completeTurn("known-id-turn", { + sessionId: opened.sessionId, + query: "known id isolation", + answer: "keep this scoped" + }); + + expect(() => service.closeSession(opened.sessionId, { namespace: blank })).toThrow(/session not found/); + expect(() => service.deletePanelTask(completed.episodeId, { namespace: blank })).toThrow(/episode not found/); + expect(() => service.redactRawTurn(completed.rawTurnId, { namespace: blank })).toThrow(/raw turn not found/); + expect(service.closeSession(opened.sessionId, { namespace: scoped }).status).toBe("closed"); + db.close(); + }); +}); diff --git a/Memory/tests/service/read-model/panel-read.test.ts b/Memory/tests/service/read-model/panel-read.test.ts index f2777105f..8289af507 100644 --- a/Memory/tests/service/read-model/panel-read.test.ts +++ b/Memory/tests/service/read-model/panel-read.test.ts @@ -532,7 +532,7 @@ describe("MemoryService / read model / panel", () => { db.close(); }); - it("shows panel change logs, jobs, and overview across namespaces", () => { + it("scopes panel change logs, jobs, and overview by namespace", () => { const { db, service } = createTestService(); const namespaceA = { source: "codex", @@ -561,29 +561,29 @@ describe("MemoryService / read model / panel", () => { const changesA = service.panelChanges({ namespace: namespaceA }); expect(changesA.changes.map((change) => change.id)).toContain(completeA.l1MemoryId); - expect(changesA.changes.map((change) => change.id)).toContain(completeB.l1MemoryId); + expect(changesA.changes.map((change) => change.id)).not.toContain(completeB.l1MemoryId); expect(changesA.changes.some((change) => change.kind === "job")).toBe(true); const jobIdsA = service.panelJobs({ namespace: namespaceA }).items.map((job) => job.id); expect(jobIdsA).toEqual(expect.arrayContaining(completeA.jobs.map((job) => job.jobId))); - expect(jobIdsA).toEqual(expect.arrayContaining(completeB.jobs.map((job) => job.jobId))); + expect(jobIdsA).not.toEqual(expect.arrayContaining(completeB.jobs.map((job) => job.jobId))); const overviewA = service.panelOverview({ namespace: namespaceA }); - expect(overviewA.stats.jobs.queued).toBe(completeA.jobs.length + completeB.jobs.length); - expect(overviewA.stats.byLayer.L1).toBe(completeA.l1MemoryIds.length + completeB.l1MemoryIds.length); - expect(overviewA.stats.byStatus.activated).toBe(completeA.l1MemoryIds.length + completeB.l1MemoryIds.length); - expect(overviewA.stats.episodes.open).toBe(2); + expect(overviewA.stats.jobs.queued).toBe(completeA.jobs.length); + expect(overviewA.stats.byLayer.L1).toBe(completeA.l1MemoryIds.length); + expect(overviewA.stats.byStatus.activated).toBe(completeA.l1MemoryIds.length); + expect(overviewA.stats.episodes.open).toBe(1); const changesB = service.panelChanges({ namespace: namespaceB }); expect(changesB.changes.map((change) => change.id)).toContain(completeB.l1MemoryId); - expect(changesB.changes.map((change) => change.id)).toContain(completeA.l1MemoryId); + expect(changesB.changes.map((change) => change.id)).not.toContain(completeA.l1MemoryId); expect(changesB.changes.some((change) => change.kind === "job")).toBe(true); const jobIdsB = service.panelJobs({ namespace: namespaceB }).items.map((job) => job.id); expect(jobIdsB).toEqual(expect.arrayContaining(completeB.jobs.map((job) => job.jobId))); - expect(jobIdsB).toEqual(expect.arrayContaining(completeA.jobs.map((job) => job.jobId))); + expect(jobIdsB).not.toEqual(expect.arrayContaining(completeA.jobs.map((job) => job.jobId))); const overviewB = service.panelOverview({ namespace: namespaceB }); - expect(overviewB.stats.jobs.queued).toBe(completeA.jobs.length + completeB.jobs.length); - expect(overviewB.stats.byLayer.L1).toBe(completeA.l1MemoryIds.length + completeB.l1MemoryIds.length); - expect(overviewB.stats.byStatus.activated).toBe(completeA.l1MemoryIds.length + completeB.l1MemoryIds.length); - expect(overviewB.stats.episodes.open).toBe(2); + expect(overviewB.stats.jobs.queued).toBe(completeB.jobs.length); + expect(overviewB.stats.byLayer.L1).toBe(completeB.l1MemoryIds.length); + expect(overviewB.stats.byStatus.activated).toBe(completeB.l1MemoryIds.length); + expect(overviewB.stats.episodes.open).toBe(1); db.close(); }); diff --git a/Memory/tests/service/read-model/skill-read.test.ts b/Memory/tests/service/read-model/skill-read.test.ts index 3e3aca225..a5951dd17 100644 --- a/Memory/tests/service/read-model/skill-read.test.ts +++ b/Memory/tests/service/read-model/skill-read.test.ts @@ -72,10 +72,10 @@ describe("MemoryService / read model / skill list", () => { expect(firstPage.skills).toHaveLength(2); expect(firstPage.nextCursor).toBe("2"); const secondPage = service.listSkills({ namespace: namespaceA, limit: 2, cursor: Number(firstPage.nextCursor) }); - expect(secondPage.skills).toHaveLength(2); + expect(secondPage.skills).toHaveLength(1); expect(secondPage.nextCursor).toBeUndefined(); const pagedIds = [...firstPage.skills, ...secondPage.skills].map((skill) => skill.id).sort(); - expect(pagedIds).toEqual(["skill_page_a_1", "skill_page_a_2", "skill_page_a_3", "skill_page_b_1"].sort()); + expect(pagedIds).toEqual(["skill_page_a_1", "skill_page_a_2", "skill_page_a_3"].sort()); const sqliteSkills = service.listSkills({ namespace: namespaceA, tags: ["sqlite"], limit: 10 }); expect(sqliteSkills.skills.map((skill) => skill.id).sort()).toEqual(["skill_page_a_1", "skill_page_a_3"]); expect(sqliteSkills.skills.find((skill) => skill.id === "skill_page_a_3")?.tags).toContain("sqlite"); diff --git a/Memory/tests/service/retrieval/query-and-filter.test.ts b/Memory/tests/service/retrieval/query-and-filter.test.ts index ad5c70bd3..e36900c2b 100644 --- a/Memory/tests/service/retrieval/query-and-filter.test.ts +++ b/Memory/tests/service/retrieval/query-and-filter.test.ts @@ -242,7 +242,8 @@ describe("MemoryService / retrieval / query and filtering", () => { namespace: { source: "codex", profileId: "jiang", - userId: memory.userId + userId: memory.userId, + workspaceId: memory.appId }, query: "query with no lexical overlap", layers: ["L1"], @@ -846,7 +847,8 @@ describe("MemoryService / retrieval / query and filtering", () => { namespace: { source: "codex", profileId: "profile-a", - userId: "shared-recall-user" + userId: "shared-recall-user", + workspaceId: "workspace-recall" }, workspaceId: "workspace-recall" }); @@ -906,7 +908,8 @@ describe("MemoryService / retrieval / query and filtering", () => { namespace: { source: "codex", profileId: "profile-a", - userId: "shared-recall-user" + userId: "shared-recall-user", + workspaceId: "workspace-recall" }, userId: "shared-recall-user", layers: ["L1"], @@ -920,7 +923,8 @@ describe("MemoryService / retrieval / query and filtering", () => { namespace: { source: "codex", profileId: "profile-a", - userId: "shared-recall-user" + userId: "shared-recall-user", + workspaceId: "workspace-recall" }, episodeId: profileAMemory.episodeId, limit: 10 diff --git a/Memory/tests/service/session/checkpoint.test.ts b/Memory/tests/service/session/checkpoint.test.ts new file mode 100644 index 000000000..fe10cdfb7 --- /dev/null +++ b/Memory/tests/service/session/checkpoint.test.ts @@ -0,0 +1,55 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { createMemoryServiceFixture } from "../../fixtures/memory-service-fixture.js"; + +const { cleanup, createTestService } = createMemoryServiceFixture(); + +afterEach(cleanup); + +describe("MemoryService / session / checkpoint", () => { + it("persists a structured handoff through the existing compact trace path", () => { + const { db, service } = createTestService(); + const namespace = { + source: "codex", + profileId: "main", + userId: "checkpoint-user", + workspacePath: "/work/checkpoint-project" + }; + const session = service.openSession({ namespace, sessionId: "checkpoint-session" }); + const result = service.checkpointSession(session.sessionId, { + namespace, + task: "Absorb the useful Remnic memory behaviors", + changes: ["Added project isolation", "Added Markdown audit"], + validated: ["Namespace tests pass"], + unverified: ["Full suite not run"], + nextSteps: ["Inspect graph relations", "Measure rerank filtering"], + tokenEstimate: 120 + }); + + expect(result.checkpointId).toBe(result.rawTurnId); + expect(result.checkpoint).toEqual({ + task: "Absorb the useful Remnic memory behaviors", + changes: ["Added project isolation", "Added Markdown audit"], + validated: ["Namespace tests pass"], + unverified: ["Full suite not run"], + nextSteps: ["Inspect graph relations", "Measure rerank filtering"] + }); + expect(result.memorySnapshot.summary).toContain("Task: Absorb the useful Remnic memory behaviors"); + expect(result.memorySnapshot.summary).toContain("- Full suite not run"); + expect(result.l1MemoryId).toMatch(/^trace_/u); + + const row = db.db.prepare( + "SELECT message_payload_json FROM raw_turns WHERE id = ?" + ).get(result.rawTurnId) as { message_payload_json: string }; + expect(JSON.parse(row.message_payload_json)).toMatchObject({ + compact: { + checkpoint: { + task: "Absorb the useful Remnic memory behaviors", + validated: ["Namespace tests pass"], + unverified: ["Full suite not run"] + } + } + }); + db.close(); + }); +}); + diff --git a/Memory/tests/service/session/session-lifecycle.test.ts b/Memory/tests/service/session/session-lifecycle.test.ts index 6d0a9405b..729f52ddd 100644 --- a/Memory/tests/service/session/session-lifecycle.test.ts +++ b/Memory/tests/service/session/session-lifecycle.test.ts @@ -17,7 +17,8 @@ describe("MemoryService / session / lifecycle", () => { namespace: { source: "codex", profileId: "jiang", - userId: "user-1" + userId: "user-1", + workspaceId: "workspace-1" }, workspaceId: "workspace-1", meta: { @@ -30,7 +31,8 @@ describe("MemoryService / session / lifecycle", () => { namespace: { source: "codex", profileId: "jiang", - userId: "user-1" + userId: "user-1", + workspaceId: "workspace-1" }, workspaceId: "workspace-1", meta: { @@ -207,7 +209,8 @@ describe("MemoryService / session / lifecycle", () => { namespace: { source: "codex", profileId: "jiang", - userId: "user-1" + userId: "user-1", + workspaceId: "workspace-1" }, query: "SQLite 记忆底座服务", includeInjectedContext: true @@ -229,7 +232,8 @@ describe("MemoryService / session / lifecycle", () => { namespace: { source: "codex", profileId: "jiang", - userId: "user-1" + userId: "user-1", + workspaceId: "workspace-1" }, sessionId: session.sessionId, retrievalMode: "turn_start", @@ -250,7 +254,7 @@ describe("MemoryService / session / lifecycle", () => { injected_memory_ids_json: string; outcome: string; } | undefined; - expect(recallRow?.namespace_id).toContain("user-1"); + expect(recallRow?.namespace_id).toBe("local:workspace-1"); expect(recallRow?.query_hash).toBeTruthy(); expect(JSON.parse(recallRow!.candidate_memory_ids_json)).toEqual(recall.candidateMemoryIds); expect(JSON.parse(recallRow!.injected_memory_ids_json)).toEqual(recall.sourceMemoryIds); diff --git a/Memory/tests/service/session/turn-capture.test.ts b/Memory/tests/service/session/turn-capture.test.ts index 832505212..a6714f0c7 100644 --- a/Memory/tests/service/session/turn-capture.test.ts +++ b/Memory/tests/service/session/turn-capture.test.ts @@ -799,7 +799,8 @@ describe("MemoryService / session / turn capture", () => { namespace: { source: "codex", profileId: "jiang", - userId: "user-structural" + userId: "user-structural", + workspaceId: "workspace-structural" }, workspaceId: "workspace-structural" }); @@ -835,7 +836,8 @@ describe("MemoryService / session / turn capture", () => { namespace: { source: "codex", profileId: "jiang", - userId: "user-structural" + userId: "user-structural", + workspaceId: "workspace-structural" }, query: "pg_config executable not found", layers: ["L1"], From f02ccc8c281d41ea06fbbfd0fd7d8af5de5624f1 Mon Sep 17 00:00:00 2001 From: bluewater_cg Date: Tue, 4 Aug 2026 18:30:13 -0700 Subject: [PATCH 10/23] docs: document fork-specific memory updates Document second-party updates based on upstream MemTensor/memmy-agent in both English and Chinese README files. --- README.md | 17 +++++++++++++++++ README.zh-CN.md | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/README.md b/README.md index 9218dff38..ba7a4934b 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,23 @@ Sign up to get free tokens. Model routing is automatic — start exp > **Registration grants Agent task trial tokens; the current amount and usage are shown in the app.** > Once the trial credits run out, you can switch to BYOK and use your own model API. +## Fork Updates + +This branch is a second-party modification based on the upstream +[`MemTensor/memmy-agent`](https://github.com/MemTensor/memmy-agent) repository. +The working fork is [`bluewatercg/memmy-agent`](https://github.com/bluewatercg/memmy-agent), +branch `actions/windows-package/remote-memory`. + +Changes in this fork that are not part of the original upstream README: + +- Added the shared `memmy.agent.v1` lifecycle protocol for external adapters, including Codex, Claude Code, and Pi hook templates. +- Added provenance capture for Memory writes: source agent, adapter id, request id, workspace path, project id, source memory ids, and Git repository / branch / commit where available. +- Added project-scoped Memory isolation so different workspaces are separated while agents in the same project can share context. +- Added Memory governance features: Markdown audit export/import, stable supersession relations, and detailed provenance/supersession fields in read models. +- Added structured session checkpoints for resumable handoff state. +- Scoped panel, retrieval, bundle import/export, worker, and API log views by tenant/project namespace. +- Ignored generated `output/` artifacts and raised the Memory test timeout to reduce load-related flakes. + ## What Is Memmy? Every AI session generates context. Most of it gets thrown away. diff --git a/README.zh-CN.md b/README.zh-CN.md index c2afcde93..744f6b403 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -44,6 +44,24 @@ > 注册赠送 Agent 任务体验 Token,当前额度和使用情况以应用内显示为准 > 当体验额度用尽后,可切换至 BYOK 模式,使用自己的模型 API。 +## Fork 二次修改说明 + +当前分支是在上游 +[`MemTensor/memmy-agent`](https://github.com/MemTensor/memmy-agent) +仓库基础上的二次修改版本。当前工作 fork 为 +[`bluewatercg/memmy-agent`](https://github.com/bluewatercg/memmy-agent), +分支为 `actions/windows-package/remote-memory`。 + +以下内容是本 fork 相比原作者 README 新增的更新说明: + +- 新增外部 Agent 适配器共享的 `memmy.agent.v1` 生命周期协议,覆盖 Codex、Claude Code 和 Pi hook 模板。 +- 新增 Memory 写入 provenance 捕获:来源 Agent、adapter id、request id、workspace path、project id、source memory ids,以及可用时的 Git repository / branch / commit。 +- 新增按项目隔离的 Memory namespace:不同 workspace 隔离,同一项目内的不同 Agent 可共享上下文。 +- 新增 Memory 治理能力:Markdown audit 导出/导入、稳定 supersession 关系,以及 read model 中的 provenance / supersession 字段。 +- 新增结构化 session checkpoint,用于可恢复的任务交接状态。 +- panel、retrieval、bundle import/export、worker、API log 等视图按 tenant/project namespace 收敛。 +- 忽略生成的 `output/` 产物,并提高 Memory 全量测试 timeout,降低本机或 CI 负载下的偶发超时。 + ## 什么是 Memmy? 每一次 AI 协作都会产生新的上下文和经验,但这些信息通常被隔离在不同工具和会话中。当你切换 Agent 或工作场景时,新的 AI 又需要重新认识你。 From b38546f29b5971a4065623414372424bcb43458d Mon Sep 17 00:00:00 2001 From: bluewater_cg Date: Wed, 5 Aug 2026 03:23:04 -0700 Subject: [PATCH 11/23] feat(memory): add project-scoped review synthesis --- .env.example | 16 + Memory/src/cli/commands.ts | 70 ++++ Memory/src/cli/workspace.ts | 53 +++ Memory/src/server/http.ts | 119 ++++++- .../src/service/evolution/policy-induction.ts | 20 +- .../src/service/evolution/project-evidence.ts | 64 ++++ .../src/service/evolution/skill-pipeline.ts | 6 +- .../service/evolution/world-model-pipeline.ts | 1 + .../service/import/import-job-processor.ts | 8 +- Memory/src/service/memory-service.ts | 319 +++++++++++++++++- Memory/src/service/read-model/memory.ts | 4 +- Memory/src/service/read-model/panel-read.ts | 188 ++++++++++- Memory/src/service/read-model/panel.ts | 88 ++++- .../retrieval/indexed-candidate-pool.ts | 9 +- Memory/src/storage/repositories.ts | 102 +++++- Memory/src/types.ts | 1 + Memory/src/viewer/static.ts | 241 ++++++++++++- Memory/tests/cli-command-map.test.ts | 83 ++++- .../tests/contract/rest-panel-events.test.ts | 43 ++- .../fixtures/project-evidence-dataset.ts | 41 +++ .../project-evidence-dataset.test.ts | 16 + .../evolution/project-evidence.test.ts | 30 ++ .../service/evolution/world-model.test.ts | 2 +- .../service/read-model/panel-read.test.ts | 4 +- .../tests/service/review-candidates.test.ts | 42 +++ .../service/session/turn-capture.test.ts | 17 +- .../service/worker/worker-runtime.test.ts | 40 +++ Memory/tests/viewer-static.test.ts | 81 ++++- README.md | 3 + README.zh-CN.md | 3 + compose.yaml | 13 + deploy/memory/docker-compose.yml | 13 + docs/cn/memory/memory-cli.mdx | 7 +- docs/en/memory/memory-cli.mdx | 7 +- 34 files changed, 1704 insertions(+), 50 deletions(-) create mode 100644 Memory/src/cli/workspace.ts create mode 100644 Memory/src/service/evolution/project-evidence.ts create mode 100644 Memory/tests/fixtures/project-evidence-dataset.ts create mode 100644 Memory/tests/service/evolution/project-evidence-dataset.test.ts create mode 100644 Memory/tests/service/evolution/project-evidence.test.ts create mode 100644 Memory/tests/service/review-candidates.test.ts diff --git a/.env.example b/.env.example index 734631ecc..93d864348 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,19 @@ MEMMY_LEGAL_INTL_BASE_URL=https://memmy.bot MEMMY_MEMORY_TOKEN= # Host-side port; keep the default for a same-machine Windows desktop client. MEMMY_MEMORY_HOST_PORT=18960 + +# OpenAI-compatible project memory models. Keep the key only in local .env. +MEMMY_SUMMARY_PROVIDER=openai_compatible +MEMMY_SUMMARY_ENDPOINT=http://192.168.8.191:20128/v1 +MEMMY_SUMMARY_MODEL=auto/best-fast +MEMMY_SUMMARY_API_KEY= +MEMMY_SUMMARY_MAX_TOKENS=768 +MEMMY_SUMMARY_TIMEOUT_MS=60000 + +MEMMY_EVOLUTION_PROVIDER=openai_compatible +MEMMY_EVOLUTION_ENDPOINT=http://192.168.8.191:20128/v1 +MEMMY_EVOLUTION_MODEL=auto/best-reasoning +MEMMY_EVOLUTION_API_KEY= +MEMMY_EVOLUTION_ENABLE_THINKING=true +MEMMY_EVOLUTION_MAX_TOKENS=4096 +MEMMY_EVOLUTION_TIMEOUT_MS=180000 diff --git a/Memory/src/cli/commands.ts b/Memory/src/cli/commands.ts index 04e8c45ff..27e1383ab 100644 --- a/Memory/src/cli/commands.ts +++ b/Memory/src/cli/commands.ts @@ -16,6 +16,7 @@ import { renderCliOutput } from "./render/index.js"; import { initMemoryCli, installMemoryCli } from "./setup.js"; import { DEFAULT_MEMORY_URL, loadCliMemoryConfig } from "./config.js"; import { PROJECT_VERSION } from "./project-version.js"; +import { workspaceNamespaceFromOptions } from "./workspace.js"; type Method = "GET" | "POST" | "DELETE"; const CLI_NAME = "memmy-memory"; @@ -57,6 +58,18 @@ export async function runCommand(context: CommandContext): Promise { return serveMemory(parsed); } + if (words[0] === "namespace" && words[1] === "current") { + return currentNamespace(parsed); + } + + if (words[0] === "stats") { + return executeRequest({ method: "GET", path: "/panel/overview" }, requestOptions(parsed, context.fetch)); + } + + if (words[0] === "doctor") { + return runDoctor(parsed, context.fetch); + } + const getVerbose = words[0] === "get" && optionBoolean(options, "verbose") === true; const request = withSource(await mapTopLevelCommand(words, parsed), parsed); const result = await executeRequest(request, requestOptions(parsed, context.fetch)); @@ -252,6 +265,47 @@ async function executeRequest(request: CliRequest, options: CliRequestOptions): return sendRequest(request, options); } +function currentNamespace(parsed: ParsedArgs): Record { + const workspace = workspaceNamespaceFromOptions({ + projectId: optionString(parsed.options, "project-id") ?? optionString(parsed.options, "project_id"), + workspaceId: optionString(parsed.options, "workspace-id") ?? optionString(parsed.options, "workspace_id"), + workspacePath: optionString(parsed.options, "workspace-path") ?? optionString(parsed.options, "workspace_path"), + noWorkspace: optionBoolean(parsed.options, "no-workspace") === true + }); + return { + source: optionString(parsed.options, "source") ?? "unknown", + projectId: workspace.projectId, + workspaceId: workspace.workspaceId, + workspacePath: workspace.workspacePath, + scoped: Boolean(workspace.projectId || workspace.workspaceId || workspace.workspacePath) + }; +} + +async function runDoctor(parsed: ParsedArgs, fetchImpl?: typeof fetch): Promise> { + const options = requestOptions(parsed, fetchImpl); + const [health, audit, evolution] = await Promise.all([ + executeRequest({ method: "GET", path: "/health" }, options), + executeRequest({ method: "GET", path: "/panel/namespace-audit" }, options), + executeRequest({ method: "GET", path: "/panel/evolution" }, options) + ]); + const auditSummary = isRecord(audit) && isRecord(audit.summary) ? audit.summary : {}; + const healthOk = isRecord(health) && health.ok === true; + const riskCount = Number(auditSummary.crossWorkspaceRisk ?? 0); + return { + ok: healthOk && riskCount === 0, + health, + namespace: currentNamespace(parsed), + audit, + evolution, + checks: { + service: healthOk ? "ok" : "failed", + workspaceIsolation: riskCount === 0 ? "ok" : "risk", + unknownSources: Number(auditSummary.unknownSource ?? 0), + missingAgentSourceTags: Number(auditSummary.missingAgentSourceTag ?? 0) + } + }; +} + function compactMemoryGetOutput(result: unknown): string | undefined { const detail = memoryDetailRecord(result); if (!detail) return undefined; @@ -440,8 +494,17 @@ async function requestBody( function requestOptions(parsed: ParsedArgs, fetchImpl?: typeof fetch): CliRequestOptions { const configPath = optionString(parsed.options, "config"); const userId = userIdOption(parsed) ?? loadCliMemoryConfig(configPath).config.userId; + const workspace = workspaceNamespaceFromOptions({ + projectId: optionString(parsed.options, "project-id") ?? optionString(parsed.options, "project_id"), + workspaceId: optionString(parsed.options, "workspace-id") ?? optionString(parsed.options, "workspace_id"), + workspacePath: optionString(parsed.options, "workspace-path") ?? optionString(parsed.options, "workspace_path"), + noWorkspace: optionBoolean(parsed.options, "no-workspace") === true + }); const headers: Record = {}; if (userId) headers["x-memmy-user-id"] = userId; + if (workspace.projectId) headers["x-memmy-project-id"] = workspace.projectId; + if (workspace.workspaceId) headers["x-memmy-workspace-id"] = workspace.workspaceId; + if (workspace.workspacePath) headers["x-memmy-workspace-path"] = workspace.workspacePath; return { url: optionString(parsed.options, "url"), token: optionString(parsed.options, "token"), @@ -553,6 +616,9 @@ function helpText(): string { " add Add a memory manually", " get Read one memory by id", " delete Delete one memory by id", + " doctor Check service, evolution, and isolation health", + " namespace current Show the current workspace namespace", + " stats --workspace Show statistics scoped to this workspace", " raw Call an exposed Memory API route directly", "", "Setup examples:", @@ -574,6 +640,10 @@ function helpText(): string { " --url Memory HTTP service URL", " --token Memory HTTP bearer token", " --user-id Memory namespace user id", + " --project-id Memory project namespace id", + " --workspace-id Memory workspace namespace id", + " --workspace-path Memory workspace path; defaults to nearest project root", + " --no-workspace Do not send workspace namespace headers", " --source Calling agent/source id", " --config Memmy config path", " --skip-agent-skills Initialize config without installing agent skills", diff --git a/Memory/src/cli/workspace.ts b/Memory/src/cli/workspace.ts new file mode 100644 index 000000000..0cc90f6ff --- /dev/null +++ b/Memory/src/cli/workspace.ts @@ -0,0 +1,53 @@ +import { existsSync } from "node:fs"; +import { dirname, resolve } from "node:path"; + +export interface CliWorkspaceNamespace { + projectId?: string; + workspaceId?: string; + workspacePath?: string; +} + +export function workspaceNamespaceFromOptions(options: { + projectId?: string; + workspaceId?: string; + workspacePath?: string; + noWorkspace?: boolean; + cwd?: string; +}): CliWorkspaceNamespace { + if (options.noWorkspace) return {}; + const explicitWorkspacePath = cleanPath(options.workspacePath); + const workspacePath = explicitWorkspacePath ?? discoverWorkspacePath(options.cwd ?? process.cwd()); + return { + projectId: clean(options.projectId), + workspaceId: clean(options.workspaceId), + workspacePath + }; +} + +export function discoverWorkspacePath(cwd = process.cwd()): string | undefined { + let current = resolve(cwd); + for (;;) { + if (isProjectRoot(current)) return current; + const parent = dirname(current); + if (parent === current) return undefined; + current = parent; + } +} + +function isProjectRoot(path: string): boolean { + return existsSync(`${path}/.git`) || + existsSync(`${path}/package.json`) || + existsSync(`${path}/pyproject.toml`) || + existsSync(`${path}/go.mod`) || + existsSync(`${path}/Cargo.toml`); +} + +function clean(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed || undefined; +} + +function cleanPath(value: string | undefined): string | undefined { + const trimmed = clean(value); + return trimmed ? resolve(trimmed) : undefined; +} diff --git a/Memory/src/server/http.ts b/Memory/src/server/http.ts index 5627c347a..790b376e9 100644 --- a/Memory/src/server/http.ts +++ b/Memory/src/server/http.ts @@ -50,12 +50,26 @@ export const API_ROUTES = [ "POST /api/v1/memory/audit/markdown/import", "POST /api/v1/memory/processing/status", "POST /api/v1/memory/:id/processing/retry", + "POST /api/v1/memory/:id/quality", + "POST /api/v1/memory/:id/archive", + "POST /api/v1/memory/:id/promote", + "GET /api/v1/panel/review/candidates", + "POST /api/v1/panel/review/candidates/:id/approve", + "POST /api/v1/panel/review/candidates/:id/reject", + "POST /api/v1/panel/review/candidates/bulk-approve", + "POST /api/v1/memory/:id/merge", "GET /api/v1/memory/:id", "DELETE /api/v1/memory/:id", "POST /api/v1/worker/run", + "POST /api/v1/worker/retry-failed", + "POST /api/v1/worker/promote-candidates", "POST /api/v1/worker/import-summaries/enqueue", "GET /api/v1/memory/logs", "GET /api/v1/panel/overview", + "GET /api/v1/panel/evolution", + "GET /api/v1/panel/context-pack", + "GET /api/v1/panel/context-packs", + "GET /api/v1/panel/namespace-audit", "GET /api/v1/panel/analysis", "GET /api/v1/panel/metrics", "GET /api/v1/panel/status", @@ -624,7 +638,7 @@ async function routeRequest( limit?: unknown; targetMemoryIds?: unknown; }; - return service.runWorkerOnce( + return service.runWorkerWithEvolutionSummary( parseNumberValue(request.limit) ?? parseNumber(url.searchParams.get("limit")) ?? 20, { ...request, @@ -633,6 +647,23 @@ async function routeRequest( ); } + if (method === "POST" && path === "/api/v1/worker/retry-failed") { + requireMemoryWrite(principal); + const request = envelopeWithPrincipal(asObject(body, "worker.retry-failed"), principal) as RequestEnvelope & { limit?: unknown }; + const limit = parseNumberValue(request.limit) ?? 100; + const retry = service.retryFailedWorkerJobs({ ...request, limit }); + const worker = await service.runWorkerWithEvolutionSummary(limit, request); + return { ...retry, worker, generated: worker.generated }; + } + + if (method === "POST" && path === "/api/v1/worker/promote-candidates") { + requireMemoryWrite(principal); + const request = envelopeWithPrincipal(asObject(body, "worker.promote-candidates"), principal) as RequestEnvelope & { limit?: unknown }; + const promotion = service.promoteCandidates(request); + const worker = await service.runWorkerWithEvolutionSummary(parseNumberValue(request.limit) ?? 100, request); + return { ...promotion, worker, generated: worker.generated }; + } + if (method === "GET" && path === "/api/v1/panel/overview") { requirePanelRead(principal); return service.panelOverviewSummary({ @@ -640,6 +671,26 @@ async function routeRequest( }); } + if (method === "GET" && path === "/api/v1/panel/evolution") { + requirePanelRead(principal); + return service.evolutionOverview({ namespace: principal.namespace }); + } + + if (method === "GET" && path === "/api/v1/panel/context-pack") { + requirePanelRead(principal); + return service.projectContextPack({ namespace: principal.namespace }); + } + + if (method === "GET" && path === "/api/v1/panel/context-packs") { + requirePanelRead(principal); + return service.projectContextPacks({ namespace: principal.namespace }); + } + + if (method === "GET" && path === "/api/v1/panel/namespace-audit") { + requirePanelRead(principal); + return service.namespaceAudit({ namespace: principal.namespace }); + } + if (method === "GET" && path === "/api/v1/panel/analysis") { requirePanelRead(principal); return service.panelAnalysis({ @@ -686,10 +737,46 @@ async function routeRequest( q: url.searchParams.get("q") ?? undefined, sourceAgent: url.searchParams.get("sourceAgent") ?? undefined, excludedSourceAgents: url.searchParams.getAll("excludedSourceAgents"), + projectId: url.searchParams.get("projectId") ?? undefined, + workspaceId: url.searchParams.get("workspaceId") ?? undefined, page: parseNumber(url.searchParams.get("page")) })); } + if (method === "GET" && path === "/api/v1/panel/review/candidates") { + requirePanelRead(principal); + const layer = parseLayer(url.searchParams.get("layer")); + return service.reviewCandidates({ namespace: principal.namespace, layer, limit: parseNumber(url.searchParams.get("limit")) }); + } + + const reviewApprove = match(path, /^\/api\/v1\/panel\/review\/candidates\/([^/]+)\/approve$/); + if (method === "POST" && reviewApprove) { + requireMemoryWrite(principal); + const request = envelopeWithPrincipal(asObject(body, "panel.review.approve"), principal) as MemoryGovernanceRequest & { content?: unknown; title?: unknown }; + return service.approveCandidate(decodeMatchSegment(reviewApprove, 1), { + ...request, + content: typeof request.content === "string" ? request.content : undefined, + title: typeof request.title === "string" ? request.title : undefined + }); + } + + const reviewReject = match(path, /^\/api\/v1\/panel\/review\/candidates\/([^/]+)\/reject$/); + if (method === "POST" && reviewReject) { + requireMemoryWrite(principal); + const request = envelopeWithPrincipal(asObject(body, "panel.review.reject"), principal) as MemoryGovernanceRequest; + return service.rejectCandidate(decodeMatchSegment(reviewReject, 1), request); + } + + if (method === "POST" && path === "/api/v1/panel/review/candidates/bulk-approve") { + requireMemoryWrite(principal); + const request = envelopeWithPrincipal(asObject(body, "panel.review.bulk-approve"), principal) as MemoryGovernanceRequest & { minimumConfidence?: unknown; layer?: unknown }; + return service.bulkApproveHighConfidenceCandidates({ + ...request, + minimumConfidence: typeof request.minimumConfidence === "number" ? request.minimumConfidence : undefined, + layer: parseLayer(typeof request.layer === "string" ? request.layer : null) + }); + } + if (method === "GET" && path === "/api/v1/panel/tasks") { requirePanelRead(principal); return publicPanelTasksResponse(service.panelTasks({ @@ -738,6 +825,36 @@ async function routeRequest( return result; } + const memoryQuality = match(path, /^\/api\/v1\/memory\/([^/]+)\/quality$/); + if (method === "POST" && memoryQuality) { + requireMemoryWrite(principal); + const request = envelopeWithPrincipal(asObject(body, "memory.quality"), principal) as MemoryGovernanceRequest & { useful?: unknown }; + if (typeof request.useful !== "boolean") throw new MemoryServiceError("invalid_argument", "memory.quality useful must be boolean"); + return service.rateMemory(decodeMatchSegment(memoryQuality, 1), request.useful, request); + } + + const memoryArchive = match(path, /^\/api\/v1\/memory\/([^/]+)\/archive$/); + if (method === "POST" && memoryArchive) { + requireMemoryWrite(principal); + const request = envelopeWithPrincipal(asObject(body, "memory.archive"), principal) as MemoryGovernanceRequest; + return service.archiveMemory(decodeMatchSegment(memoryArchive, 1), request); + } + + const memoryPromote = match(path, /^\/api\/v1\/memory\/([^/]+)\/promote$/); + if (method === "POST" && memoryPromote) { + requireMemoryWrite(principal); + const request = envelopeWithPrincipal(asObject(body, "memory.promote"), principal) as MemoryGovernanceRequest; + return service.promoteL1ToL2(decodeMatchSegment(memoryPromote, 1), request); + } + + const memoryMerge = match(path, /^\/api\/v1\/memory\/([^/]+)\/merge$/); + if (method === "POST" && memoryMerge) { + requireMemoryWrite(principal); + const request = envelopeWithPrincipal(asObject(body, "memory.merge"), principal) as MemoryGovernanceRequest & { sourceMemoryId?: unknown }; + if (typeof request.sourceMemoryId !== "string" || !request.sourceMemoryId.trim()) throw new MemoryServiceError("invalid_argument", "memory.merge sourceMemoryId is required"); + return service.mergeMemories(decodeMatchSegment(memoryMerge, 1), request.sourceMemoryId, request); + } + const memoryGet = match(path, /^\/api\/v1\/memory\/([^/]+)$/); if (method === "GET" && path === "/api/v1/memory/audit/markdown") { requireMemoryRead(principal); diff --git a/Memory/src/service/evolution/policy-induction.ts b/Memory/src/service/evolution/policy-induction.ts index 25ab5bfb8..ba6d4cdc8 100644 --- a/Memory/src/service/evolution/policy-induction.ts +++ b/Memory/src/service/evolution/policy-induction.ts @@ -20,6 +20,7 @@ import { isRecord } from "../../utils/json.js"; import { stableHash } from "../../utils/id.js"; import type { EnqueueJobInput } from "../worker/job-handlers.js"; import { logEvolutionDecision } from "./evolution-logging.js"; +import { extractProjectEvidence } from "./project-evidence.js"; export type PolicyDraft = ReturnType; export type PolicyEnhancementResult = @@ -293,7 +294,7 @@ export class PolicyInductionEngine { profileId: this.deps.profileIdFromMemory(source), layer: "L2", kind: "policy", - lifecycleStatus: draft.status, + lifecycleStatus: "candidate", memoryType: "LongTermMemory", key: draft.key, value: draft.body, @@ -304,7 +305,7 @@ export class PolicyInductionEngine { gain: draft.gain, raw_gain: draft.rawGain, policy_confidence: draft.confidence, - status: draft.status, + status: "candidate", source_memory_ids: draft.sourceTraceIds }, internal: { @@ -321,7 +322,7 @@ export class PolicyInductionEngine { gain: draft.gain, raw_gain: draft.rawGain, policy_confidence: draft.confidence, - status: draft.status, + status: "candidate", source_episode_ids: draft.sourceEpisodeIds, source_trace_ids: draft.sourceTraceIds, policy: { @@ -334,7 +335,7 @@ export class PolicyInductionEngine { gain: draft.gain, raw_gain: draft.rawGain, policy_confidence: draft.confidence, - status: draft.status, + status: "candidate", experience_type: "success_pattern", evidence_polarity: "positive", skill_eligible: true, @@ -752,7 +753,16 @@ export class PolicyInductionEngine { } isTraceEligibleForL2(trace: TraceMeta): boolean { - return trace.value >= this.deps.config.algorithm.l2Induction.minTraceValue && + const evidence = extractProjectEvidence({ + id: trace.id, + userText: trace.userText, + agentText: trace.agentText, + reflection: trace.reflection, + toolCalls: trace.toolCalls, + tags: trace.tags, + value: trace.value + }); + return evidence.eligible && trace.value >= this.deps.config.algorithm.l2Induction.minTraceValue && Boolean(trace.vecSummary ?? trace.vecAction); } diff --git a/Memory/src/service/evolution/project-evidence.ts b/Memory/src/service/evolution/project-evidence.ts new file mode 100644 index 000000000..784eaff87 --- /dev/null +++ b/Memory/src/service/evolution/project-evidence.ts @@ -0,0 +1,64 @@ +import type { ToolCallPayload } from "../../types.js"; +import { stableHash, stableStringify } from "../../utils/id.js"; + +export type ProjectEvidenceKind = "fact" | "decision" | "procedure" | "outcome" | "noise"; + +export interface ProjectEvidence { + kind: ProjectEvidenceKind; + eligible: boolean; + confidence: number; + subject: string; + claim: string; + sourceText: string; + stableKey: string; + reasons: string[]; +} + +export interface ProjectEvidenceInput { + id: string; + userText?: string; + agentText?: string; + reflection?: string | null; + toolCalls?: ToolCallPayload[]; + tags?: string[]; + value?: number; +} + +const INJECTED_CONTEXT_RE = /<\/?(?:codex_internal_context|memmy_memory_context|objective|current_user_request)\b/i; +const CHILD_AGENT_RE = /(?:focused child agent|spawned by a parent agent|child-agent instruction|subagent instruction)/i; +const META_NOISE_RE = /^(?:continue|keep working|go on|看看|继续|再看看|有了吗|什么情况|下一步(?:怎么|是什么)?工作)\s*[??!!。.]?$/i; +const QUESTION_RE = /[??]\s*$/; +const RESULT_RE = /(?:成功|失败|通过|报错|error|failed|passed|fixed|修复|完成|created|updated|deleted|exit code|status\s*[:=])/i; +const PROCEDURE_RE = /(?:run|执行|使用|调用|install|测试|部署|重启|patch|修改|命令|procedure|步骤|步骤|must|should|不要|必须)/i; + +export function extractProjectEvidence(input: ProjectEvidenceInput): ProjectEvidence { + const user = normalize(input.userText); + const agent = normalize(input.agentText); + const reflection = normalize(input.reflection); + const tools = input.toolCalls ?? []; + const reasons: string[] = []; + const sourceText = [user && `USER: ${user}`, agent && `AGENT: ${agent}`, reflection && `REFLECTION: ${reflection}`] + .filter(Boolean) + .join("\n"); + let kind: ProjectEvidenceKind = "fact"; + if (!sourceText || INJECTED_CONTEXT_RE.test(sourceText)) { + reasons.push("injected_or_empty"); + } else if (CHILD_AGENT_RE.test(sourceText)) { + reasons.push("agent_instruction_noise"); + } else if (META_NOISE_RE.test(user) || (QUESTION_RE.test(user) && !agent && tools.length === 0 && !reflection)) { + reasons.push("question_or_meta_noise"); + } + if (tools.length > 0 || PROCEDURE_RE.test(`${user}\n${agent}`)) kind = "procedure"; + if (RESULT_RE.test(`${agent}\n${reflection}`) || tools.some((tool) => Boolean(tool.output || tool.error))) kind = "outcome"; + if (/(?:we will|we decided|adopt|use|采用|决定|约定|规范|架构事实)/i.test(`${user}\n${agent}`)) kind = "decision"; + if (reasons.length > 0) kind = "noise"; + const confidence = kind === "noise" ? 0 : Math.min(1, 0.45 + (agent.length > 40 ? 0.2 : 0) + (tools.length > 0 ? 0.2 : 0) + (reflection ? 0.15 : 0)); + const claim = normalize(agent || user); + const subject = normalize(user).slice(0, 160); + const stableKey = `evidence:${stableHash(stableStringify({ kind, subject, claim }))}`; + return { kind, eligible: kind !== "noise" && confidence >= 0.6, confidence, subject, claim, sourceText, stableKey, reasons }; +} + +function normalize(value: string | null | undefined): string { + return (value ?? "").replace(/\s+/g, " ").trim(); +} diff --git a/Memory/src/service/evolution/skill-pipeline.ts b/Memory/src/service/evolution/skill-pipeline.ts index 5a40c31e6..da7194490 100644 --- a/Memory/src/service/evolution/skill-pipeline.ts +++ b/Memory/src/service/evolution/skill-pipeline.ts @@ -183,7 +183,7 @@ export class SkillPipeline { profileId: profileIdFromMemory(policyMemory), layer: "Skill", kind: "skill", - lifecycleStatus: verifiedDraft.status, + lifecycleStatus: "candidate", memoryType: "SkillMemory", key: verifiedDraft.key, value: verifiedDraft.invocationGuide, @@ -191,7 +191,7 @@ export class SkillPipeline { info: { name: verifiedDraft.name, eta: verifiedDraft.eta, - status: verifiedDraft.status, + status: "candidate", source_memory_ids: verifiedDraft.sourcePolicyIds }, internal: { @@ -211,7 +211,7 @@ export class SkillPipeline { skill: { name: verifiedDraft.name, eta: verifiedDraft.eta, - status: verifiedDraft.status, + status: "candidate", support: verifiedDraft.support, gain: verifiedDraft.gain, policy_content_hash: skillPolicyContentHash(policy), diff --git a/Memory/src/service/evolution/world-model-pipeline.ts b/Memory/src/service/evolution/world-model-pipeline.ts index ad94bdcf4..8bb4e4b5b 100644 --- a/Memory/src/service/evolution/world-model-pipeline.ts +++ b/Memory/src/service/evolution/world-model-pipeline.ts @@ -153,6 +153,7 @@ export class WorldModelPipeline { profileId: source ? profileIdFromMemory(source) : undefined, layer: "L3", kind: "world_model", + lifecycleStatus: "candidate", memoryType: "LongTermMemory", key: draft.key, value: draft.body, diff --git a/Memory/src/service/import/import-job-processor.ts b/Memory/src/service/import/import-job-processor.ts index 51093b49a..ba5b5d4a1 100644 --- a/Memory/src/service/import/import-job-processor.ts +++ b/Memory/src/service/import/import-job-processor.ts @@ -204,7 +204,7 @@ export class ImportJobProcessor { return { upsert, changeSeq }; }); const inserted = persisted.upsert.memory; - if (persisted.upsert.created && !d.isAgentSourceImportMemoryAdd(request)) { + if (persisted.upsert.created && layer === "L1" && !d.isAgentSourceImportMemoryAdd(request)) { d.enqueueJob({ jobType: "episode_idle_close", userId: inserted.userId, sessionId: inserted.sessionId, dedupeKey: `episode_idle_close:memory.add:${inserted.id}`, payload: { triggerMemoryId: inserted.id, triggerSource: "memory.add", triggeredAt: receivedAt }, createdAt: receivedAt }); @@ -319,7 +319,11 @@ export class ImportJobProcessor { export function memoryHasImportPipeline(memory: MemoryRow): boolean { const algorithm = stringFromRecord(memory.properties.internal_info, "plugin_algorithm"); - return algorithm?.startsWith("memory.add.import_async.") === true || memory.tags.some((tag) => tag.trim().toLowerCase() === "agent-source"); + const provenance = isRecord(memory.properties.internal_info.provenance) + ? memory.properties.internal_info.provenance + : {}; + const adapterId = stringFromRecord(provenance, "adapterId"); + return algorithm?.startsWith("memory.add.import_async.") === true || adapterId?.startsWith("agent-source:") === true; } export function memoryNeedsImportSummary(memory: MemoryRow): boolean { diff --git a/Memory/src/service/memory-service.ts b/Memory/src/service/memory-service.ts index b6ef6e9da..020085f69 100644 --- a/Memory/src/service/memory-service.ts +++ b/Memory/src/service/memory-service.ts @@ -1763,6 +1763,22 @@ export class MemoryService { return this.panelReadModel.panelOverviewSummary(input); } + evolutionOverview(input: RequestEnvelope & { userId?: string } = {}) { + return this.panelReadModel.evolutionOverview(input); + } + + namespaceAudit(input: RequestEnvelope & { userId?: string } = {}) { + return this.panelReadModel.namespaceAudit(input); + } + + projectContextPack(input: RequestEnvelope & { userId?: string } = {}) { + return this.panelReadModel.projectContextPack(input); + } + + projectContextPacks(input: RequestEnvelope & { userId?: string } = {}) { + return this.panelReadModel.projectContextPacks(input); + } + panelAnalysis(input: RequestEnvelope & { userId?: string } = {}): { metrics: { avgRecallScore: number; @@ -1790,6 +1806,8 @@ export class MemoryService { tags?: string[]; sourceAgent?: string; excludedSourceAgents?: string[]; + projectId?: string; + workspaceId?: string; page?: number; limit?: number; cursor?: string | number; @@ -1883,6 +1901,234 @@ export class MemoryService { return this.importJobs.retryMemoryProcessing(memoryId, request); } + rateMemory(id: string, useful: boolean, request: MemoryGovernanceRequest = {}) { + this.assertMemoryAddEnabled(); + const memory = this.requireExistingMemory(id); + this.assertMemoryInScope(memory, request.namespace); + const at = nowIso(); + const before = memory; + const opposite = useful ? "quality-not-useful" : "quality-useful"; + const tag = useful ? "quality-useful" : "quality-not-useful"; + const updated = this.repos.memories.update({ + ...memory, + tags: uniq([...memory.tags.filter((item) => item !== opposite), tag]), + info: { ...memory.info, quality_rating: useful ? "useful" : "not_useful", quality_rated_at: at }, + updatedAt: at + }); + const changeSeq = this.repos.runtime.appendChange({ + memoryId: updated.id, namespaceId: namespaceIdFromMemory(updated), kind: kindFromMemory(updated), op: "updated", + entityId: updated.id, userId: updated.userId, changeType: "quality_rating", before, after: updated, + source: "panel.quality", createdAt: at + }); + const audit = this.repos.runtime.insertAudit({ + userId: updated.userId, sessionId: updated.sessionId, actor: request.namespace ? { ...request.namespace } : {}, + action: useful ? "mark_useful" : "mark_not_useful", targetKind: kindFromMemory(updated), targetId: updated.id, + before, after: updated, meta: { reason: request.reason }, createdAt: at + }); + return { ok: true, id: updated.id, useful, changeSeq, auditId: audit.id, serverTime: at }; + } + + reviewCandidates(request: RequestEnvelope & { layer?: MemoryLayer; limit?: number } = {}) { + const limit = Math.max(1, Math.min(request.limit ?? 100, 1000)); + const memories = this.repos.memories.list({ + ...request.namespace ? memoryFilterForNamespace(request.namespace) : {}, + memoryLayer: request.layer ?? ["L2", "L3", "Skill"], + status: "resolving" + }, limit); + return { + items: memories.map((memory) => candidateReviewCard(memory)), + total: memories.length, + serverTime: nowIso() + }; + } + + approveCandidate( + id: string, + request: MemoryGovernanceRequest & { content?: string; title?: string } = {} + ) { + this.assertMemoryAddEnabled(); + const memory = this.requireReviewCandidate(id, request); + const at = nowIso(); + const title = request.title?.trim(); + const content = request.content?.trim(); + const internal = memory.properties.internal_info; + const updated = this.repos.memories.update({ + ...memory, + status: "activated", + memoryValue: content || memory.memoryValue, + tags: uniq([...memory.tags, "review-approved"]), + info: { + ...memory.info, + ...(title ? { title } : {}), + review_status: "approved", + reviewed_at: at + }, + properties: { + ...memory.properties, + status: "activated", + tags: uniq([...(memory.properties.tags ?? []), "review-approved"]), + info: { + ...memory.properties.info, + ...(title ? { title } : {}), + review_status: "approved", + reviewed_at: at + }, + internal_info: { + ...internal, + ...(title ? { title } : {}), + review: { status: "approved", reviewed_at: at, edited: Boolean(title || content) } + } + }, + contentHash: content ? stableHash(content) : memory.contentHash, + updatedAt: at + }); + return this.recordCandidateReview(memory, updated, "approved", request); + } + + rejectCandidate(id: string, request: MemoryGovernanceRequest = {}) { + this.assertMemoryAddEnabled(); + const memory = this.requireReviewCandidate(id, request); + const at = nowIso(); + const internal = memory.properties.internal_info; + const updated = this.repos.memories.update({ + ...memory, + status: "archived", + tags: uniq([...memory.tags, "review-rejected"]), + info: { ...memory.info, review_status: "rejected", reviewed_at: at, review_reason: request.reason }, + properties: { + ...memory.properties, + status: "archived", + tags: uniq([...(memory.properties.tags ?? []), "review-rejected"]), + info: { ...memory.properties.info, review_status: "rejected", reviewed_at: at, review_reason: request.reason }, + internal_info: { ...internal, review: { status: "rejected", reviewed_at: at, reason: request.reason } } + }, + updatedAt: at + }); + return this.recordCandidateReview(memory, updated, "rejected", request); + } + + bulkApproveHighConfidenceCandidates( + request: MemoryGovernanceRequest & { minimumConfidence?: number; layer?: MemoryLayer } = {} + ) { + const minimumConfidence = Math.max(0, Math.min(request.minimumConfidence ?? 0.8, 1)); + const candidates = this.reviewCandidates({ ...request, layer: request.layer, limit: 1000 }).items + .filter((candidate) => candidate.confidence >= minimumConfidence); + const approved = candidates.map((candidate) => this.approveCandidate(candidate.id, request)); + return { approved: approved.length, minimumConfidence, ids: approved.map((item) => item.id), serverTime: nowIso() }; + } + + private requireReviewCandidate(id: string, request: MemoryGovernanceRequest): MemoryRow { + const memory = this.requireExistingMemory(id); + this.assertMemoryInScope(memory, request.namespace); + if (memory.memoryLayer === "L1" || memory.status !== "resolving") { + throw new MemoryServiceError("conflict", "only resolving L2/L3/Skill memories can be reviewed"); + } + return memory; + } + + private recordCandidateReview( + before: MemoryRow, + after: MemoryRow, + decision: "approved" | "rejected", + request: MemoryGovernanceRequest + ) { + const at = after.updatedAt; + const changeSeq = this.repos.runtime.appendChange({ + memoryId: after.id, namespaceId: namespaceIdFromMemory(after), kind: kindFromMemory(after), + op: decision === "approved" ? "updated" : "archived", entityId: after.id, userId: after.userId, + changeType: `review_${decision}`, before, after, source: "panel.review", createdAt: at + }); + const audit = this.repos.runtime.insertAudit({ + userId: after.userId, sessionId: after.sessionId, actor: request.namespace ? { ...request.namespace } : {}, + action: `review_${decision}`, targetKind: kindFromMemory(after), targetId: after.id, before, after, + meta: { reason: request.reason }, createdAt: at + }); + return { ok: true, id: after.id, layer: after.memoryLayer, status: after.status, decision, changeSeq, auditId: audit.id, serverTime: nowIso() }; + } + + mergeMemories(targetId: string, sourceId: string, request: MemoryGovernanceRequest = {}) { + this.assertMemoryAddEnabled(); + if (targetId === sourceId) throw new MemoryServiceError("invalid_argument", "merge source and target must differ"); + const target = this.requireExistingMemory(targetId); + const source = this.requireExistingMemory(sourceId); + this.assertMemoryInScope(target, request.namespace); + this.assertMemoryInScope(source, request.namespace); + if (target.memoryLayer !== source.memoryLayer) throw new MemoryServiceError("conflict", "merged memories must use the same layer"); + const at = nowIso(); + const merged = this.repos.transaction(() => this.repos.memories.supersede({ + oldMemory: source, newMemory: target, projectId: projectIdFromMemory(target), + reason: request.reason ?? `duplicate of ${target.id}`, actor: request.namespace ? { ...request.namespace } : {}, createdAt: at + })); + const changeSeq = this.repos.runtime.appendChange({ + memoryId: merged.oldMemory.id, namespaceId: namespaceIdFromMemory(merged.oldMemory), kind: kindFromMemory(merged.oldMemory), op: "archived", + entityId: merged.oldMemory.id, userId: merged.oldMemory.userId, changeType: "merged_duplicate", before: source, + after: merged.oldMemory, source: "panel.merge", createdAt: at + }); + const audit = this.repos.runtime.insertAudit({ + userId: target.userId, sessionId: target.sessionId, actor: request.namespace ? { ...request.namespace } : {}, action: "merge", + targetKind: kindFromMemory(target), targetId: target.id, before: { target, source }, after: merged, + meta: { sourceMemoryId: source.id, relationId: merged.relation.id, reason: request.reason }, createdAt: at + }); + return { ok: true, targetId, archivedSourceId: sourceId, relationId: merged.relation.id, changeSeq, auditId: audit.id, serverTime: at }; + } + + promoteL1ToL2(id: string, request: MemoryGovernanceRequest = {}) { + const source = this.requireExistingMemory(id); + this.assertMemoryInScope(source, request.namespace); + if (source.memoryLayer !== "L1") throw new MemoryServiceError("invalid_argument", "manual promotion requires an L1 memory"); + return this.addMemory({ + ...request, + content: source.memoryValue, + layer: "L2", + title: `Experience: ${firstLine(source.memoryValue).slice(0, 100)}`, + tags: uniq([...source.tags, "manual-promotion", "experience"]), + source: request.source ?? "memory-console", + sessionId: source.sessionId, + sourceMemoryIds: [source.id], + provenance: { sourceMemoryIds: [source.id] } + }); + } + + promoteCandidates(request: RequestEnvelope = {}) { + this.assertMemoryAddEnabled(); + const candidates = this.repos.runtime.listPendingCandidatePool({ now: nowIso(), limit: 10_000 }) + .filter((candidate) => { + const memory = this.repos.memories.get(candidate.sourceMemoryId); + return Boolean(memory && (!request.namespace || sameProjectScope(namespaceForMemory(memory), request.namespace))); + }); + const memoryIds = uniq(candidates.map((candidate) => candidate.sourceMemoryId)); + const jobs = memoryIds.map((memoryId) => { + const memory = this.requireExistingMemory(memoryId); + return this.enqueueJob({ + jobType: "l2_induction", userId: memory.userId, sessionId: memory.sessionId, targetMemoryId: memory.id, + payload: { sourceMemoryId: memory.id, reason: "manual_candidate_promotion" }, createdAt: nowIso() + }); + }); + return { accepted: jobs.length, candidateCount: candidates.length, memoryIds, jobs: jobs.map(jobToRef), serverTime: nowIso() }; + } + + retryFailedWorkerJobs(request: RequestEnvelope & { limit?: number } = {}) { + this.assertMemoryAddEnabled(); + const limit = Math.max(1, Math.min(request.limit ?? 100, 10_000)); + const failed = [ + ...this.panelReadModel.panelJobs({ ...request, status: "failed", limit }).items, + ...this.panelReadModel.panelJobs({ ...request, status: "dead_letter", limit }).items + ].slice(0, limit); + const retried = this.repos.runtime.retryFailedJobIds(failed.map((job) => job.id)); + for (const { before, after } of retried) this.workerHandlers.appendJobChange(after, "queued", before); + return { retried: retried.length, jobIds: retried.map((item) => item.after.id), serverTime: nowIso() }; + } + + async runWorkerWithEvolutionSummary(limit = 100, request: RequestEnvelope & { targetMemoryIds?: string[] } = {}) { + const before = this.panelReadModel.panelOverviewSummary(request).layerCounts; + const worker = await this.runWorkerOnce(limit, request); + const after = this.panelReadModel.panelOverviewSummary(request).layerCounts; + return { + ...worker, + generated: { L2: Math.max(0, after.L2 - before.L2), L3: Math.max(0, after.L3 - before.L3), Skill: Math.max(0, after.Skill - before.Skill) } + }; + } + private restartFailedProcessing(at: string, limit = 10000): number { return this.importJobs.restartFailedProcessing(at, limit); } @@ -1984,15 +2230,19 @@ export class MemoryService { createdAt?: string; }): MemoryRow { const at = input.createdAt ?? nowIso(); - const tags = uniq(input.tags.filter(Boolean)); const memoryStatus = memoryStatusForLifecycleStatus(input.lifecycleStatus ?? "active"); const inputInfo = input.info ?? {}; const inputInternal = input.internal ?? {}; + const semanticTags = uniq(input.tags.map((tag) => tag.trim()).filter(Boolean)); const sourceSession = input.sessionId ? this.repos.runtime.getSession(input.sessionId) : undefined; const sessionTenantId = sourceSession ? tenantIdFromSession(sourceSession) : undefined; const tenantId = input.tenantId ?? sessionTenantId ?? stringFromMeta(inputInfo, "tenant_id") ?? input.provenance?.tenantId ?? "local"; + const sourceAgent = normalizeMemorySourceAgent( + input.provenance?.sourceAgent ?? input.agentId ?? stringFromMeta(inputInfo, "source") ?? DEFAULT_NAMESPACE_SOURCE + ); + const tags = normalizeMemoryTagsForSource(semanticTags, sourceAgent); const provenance: MemoryProvenance = { - sourceAgent: input.provenance?.sourceAgent ?? input.agentId ?? DEFAULT_NAMESPACE_SOURCE, + sourceAgent, tenantId, profileId: input.provenance?.profileId ?? input.profileId, projectId: input.provenance?.projectId ?? input.projectId, @@ -2014,6 +2264,7 @@ export class MemoryService { const info = { ...inputInfo, tags: uniq([...tags, ...stringArray(inputInfo.tags)]), + ...(stringFromMeta(inputInfo, "source") ? { source: sourceAgent } : {}), tenant_id: tenantId, ...(input.projectId ? { project_id: input.projectId } : {}), ...(input.profileId ? { profile_id: input.profileId } : {}) @@ -2043,6 +2294,9 @@ export class MemoryService { memory_kind: input.kind, schema_version: 1, ...inputInternal, + ...(isRecord(inputInternal.trace) + ? { trace: { ...inputInternal.trace, tags: semanticTags } } + : {}), provenance } }, @@ -2803,6 +3057,67 @@ function cloneMemmyConfig(config: MemmyConfig): MemmyConfig { return structuredClone(config); } +function normalizeMemorySourceAgent(value: string | undefined): string { + const normalized = value?.trim(); + return normalized || DEFAULT_NAMESPACE_SOURCE; +} + +function normalizeMemoryTagsForSource(tags: string[], sourceAgent: string): string[] { + const base = tags.map((tag) => tag.trim()).filter(Boolean); + if (sourceAgent === DEFAULT_NAMESPACE_SOURCE) { + return uniq(base); + } + return uniq([...base, "agent-source", sourceAgent]); +} + +function candidateReviewCard(memory: MemoryRow) { + const internal = memory.properties.internal_info; + const policy = isRecord(internal.policy) ? internal.policy : {}; + const world = isRecord(internal.world_model) ? internal.world_model : {}; + const skill = isRecord(internal.skill) ? internal.skill : {}; + const confidence = clamp01(firstFiniteNumber( + memory.info.policy_confidence, + memory.info.confidence, + memory.info.eta, + policy.policy_confidence, + world.confidence, + skill.eta + ) ?? 0); + const evidenceIds = uniq([ + ...stringArray(internal.source_memory_ids), + ...stringArray(internal.source_trace_ids), + ...stringArray(internal.source_policy_ids), + ...stringArray(internal.evidence_anchor_ids) + ]).slice(0, 5); + const episodeIds = uniq([ + ...stringArray(internal.source_episode_ids), + ...stringArray(policy.source_episode_ids) + ]); + return { + id: memory.id, + suggestedLayer: memory.memoryLayer, + title: stringFromMeta(memory.info, "title") ?? stringFromMeta(internal, "title") ?? firstLine(memory.memoryValue), + conclusion: memory.memoryValue, + confidence, + confidenceLabel: confidence >= 0.8 ? "high" : confidence >= 0.6 ? "medium" : "low", + risk: confidence >= 0.8 && evidenceIds.length >= 2 ? "low" : confidence >= 0.6 ? "medium" : "high", + evidence: { + episodeCount: episodeIds.length, + l1Count: evidenceIds.filter((id) => id.startsWith("trace_")).length, + ids: evidenceIds + }, + updatedAt: memory.updatedAt + }; +} + +function firstFiniteNumber(...values: unknown[]): number | undefined { + return values.find((value): value is number => typeof value === "number" && Number.isFinite(value)); +} + +function clamp01(value: number): number { + return Math.max(0, Math.min(value, 1)); +} + function memoryConfigLogFields(config: MemmyConfig): Record { const evolution = resolveEvolutionConfig(config); return { diff --git a/Memory/src/service/read-model/memory.ts b/Memory/src/service/read-model/memory.ts index 39a9270be..16bf91429 100644 --- a/Memory/src/service/read-model/memory.ts +++ b/Memory/src/service/read-model/memory.ts @@ -1,7 +1,7 @@ import type { MemoryDetailItem, MemoryProcessingRecord, MemoryRow } from "../../types.js"; import { kindFromMemory } from "../../storage/repositories.js"; import { policyMetaFromMemory, skillMetaFromMemory, traceMetaFromMemory, worldModelMetaFromMemory } from "../../algorithm/plugin-algorithms.js"; -import { panelSourceForMemory, panelTagsForMemory } from "./panel.js"; +import { panelNamespaceForMemory, panelSourceForMemory, panelTagsForMemory } from "./panel.js"; import { isRecord } from "../../utils/json.js"; import { firstLine } from "../../utils/text.js"; @@ -21,7 +21,7 @@ export function detailFromMemory(memory: MemoryRow, processing?: MemoryProcessin supersession: supersedesMemoryIds.length || supersededByMemoryId ? { supersedesMemoryIds, supersededByMemoryId, reason: supersessionReason } : undefined, - metadata: { source: panelSourceForMemory(memory), info: memory.info, properties: memory.properties } }; + metadata: { source: panelSourceForMemory(memory), namespace: panelNamespaceForMemory(memory), info: memory.info, properties: memory.properties } }; } export function detailTitleForMemory(memory: MemoryRow): string { diff --git a/Memory/src/service/read-model/panel-read.ts b/Memory/src/service/read-model/panel-read.ts index 4e0878033..ec440b979 100644 --- a/Memory/src/service/read-model/panel-read.ts +++ b/Memory/src/service/read-model/panel-read.ts @@ -46,6 +46,7 @@ import { import { panelCountByDate, panelListItemFromMemory, + panelNamespaceDistribution, panelSourceDistribution } from "./panel.js"; @@ -379,7 +380,9 @@ export class PanelReadModel { panelOverviewSummary(input: RequestEnvelope & { userId?: string } = {}): { counts: { memories: number; skills: number; experiences: number; worldModels: number }; + layerCounts: Record; sourceDistribution: Array<{ source: string; count: number; percentage: number }>; + namespaceDistribution: Array<{ tenantId: string; projectId: string; workspaceId?: string; workspacePath?: string; label: string; count: number; percentage: number }>; dailyActivity: Array<{ date: string; count: number }>; } { const memories = this.listAllMemoriesForStats(input); @@ -391,8 +394,175 @@ export class PanelReadModel { experiences: memories.filter((memory) => memory.memoryLayer === "L2").length, worldModels: memories.filter((memory) => memory.memoryLayer === "L3").length }, + layerCounts: this.memoryLayerCounts(memories), dailyActivity: panelCountByDate(memories, dates, (memory) => memory.createdAt), - sourceDistribution: panelSourceDistribution(memories) + sourceDistribution: panelSourceDistribution(memories), + namespaceDistribution: panelNamespaceDistribution(memories, (memory) => + memory.sessionId ? this.deps.repos.runtime.getSession(memory.sessionId) : undefined + ) + }; + } + + evolutionOverview(input: RequestEnvelope & { userId?: string } = {}): { + layers: Array<{ + layer: MemoryLayer; + count: number; + candidates: number; + failed: number; + queued: number; + recentJob?: EvolutionJobRecord; + }>; + l2Resolving: { active: boolean; count: number; reason: string }; + recentJobs: EvolutionJobRecord[]; + serverTime: string; + } { + const memories = this.listAllMemoriesForStats(input); + const candidates = this.deps.repos.runtime.listPendingCandidatePool({ + userId: input.userId, + now: this.now(), + limit: 10_000 + }).filter((candidate) => { + const memory = this.deps.repos.memories.get(candidate.sourceMemoryId); + return Boolean(memory && this.memoryMatchesRequest(memory, input)); + }); + const recentJobs = this.scopedJobs(undefined, input, 200); + const jobLayers: Record = { + L1: ["trace_summary", "import_summary", "reflection", "reward", "span_big_turn", "negative_experience", "embedding"], + L2: ["l2_association", "l2_induction"], + L3: ["l3_abstraction"], + Skill: ["skill_crystallization", "skill_trial_resolve"] + }; + const layers = (["L1", "L2", "L3", "Skill"] as const).map((layer) => { + const jobs = recentJobs.filter((job) => jobLayers[layer].includes(job.jobType)); + return { + layer, + count: memories.filter((memory) => memory.memoryLayer === layer).length, + candidates: layer === "L2" + ? candidates.length + : memories.filter((memory) => memory.memoryLayer === layer && memory.status === "resolving").length, + failed: jobs.filter((job) => job.status === "failed" || job.status === "dead_letter").length, + queued: jobs.filter((job) => job.status === "queued" || job.status === "leased").length, + recentJob: jobs[0] + }; + }); + const resolvingCount = memories.filter((memory) => memory.memoryLayer === "L2" && memory.status === "resolving").length; + const eligibleEpisodes = new Set(candidates.map((candidate) => + isRecord(candidate.evidence) && typeof candidate.evidence.episodeId === "string" + ? candidate.evidence.episodeId + : undefined + ).filter(Boolean)).size; + const requiredEpisodes = this.deps.config().algorithm.l2Induction.minEpisodesForInduction; + const reason = resolvingCount > 0 + ? `${resolvingCount} 条 L2 候选正在等待支持度、增益或试验门槛` + : candidates.length === 0 + ? "没有符合价值与向量条件的 L1 候选" + : eligibleEpisodes < requiredEpisodes + ? `候选证据不足:${eligibleEpisodes}/${requiredEpisodes} 个独立 episode` + : "候选已就绪,等待 l2_induction worker job"; + return { + layers, + l2Resolving: { active: resolvingCount > 0, count: resolvingCount, reason }, + recentJobs: recentJobs.slice(0, 10), + serverTime: this.now() + }; + } + + namespaceAudit(input: RequestEnvelope & { userId?: string } = {}): { + summary: { + total: number; + missingWorkspace: number; + unknownSource: number; + missingAgentSourceTag: number; + crossWorkspaceRisk: number; + }; + issues: Array<{ memoryId: string; issue: string; severity: "warning" | "risk"; projectId?: string; workspaceId?: string; source?: string }>; + serverTime: string; + } { + const memories = this.listAllMemoriesForStats(input); + const issues: Array<{ memoryId: string; issue: string; severity: "warning" | "risk"; projectId?: string; workspaceId?: string; source?: string }> = []; + let missingWorkspace = 0; + let unknownSource = 0; + let missingAgentSourceTag = 0; + let crossWorkspaceRisk = 0; + for (const memory of memories) { + const namespace = namespaceForMemory(memory); + const source = memory.agentId || (typeof memory.info.source === "string" ? memory.info.source : undefined) || "unknown"; + if (!namespace.workspaceId && !namespace.workspacePath) { + missingWorkspace += 1; + issues.push({ memoryId: memory.id, issue: "missing_workspace", severity: "risk", projectId: namespace.projectId, source }); + } + if (source === "unknown") { + unknownSource += 1; + issues.push({ memoryId: memory.id, issue: "unknown_source", severity: "warning", projectId: namespace.projectId, workspaceId: namespace.workspaceId, source }); + } + if (source !== "unknown" && !memory.tags.includes("agent-source")) { + missingAgentSourceTag += 1; + issues.push({ memoryId: memory.id, issue: "missing_agent_source_tag", severity: "warning", projectId: namespace.projectId, workspaceId: namespace.workspaceId, source }); + } + if (memory.sessionId) { + const session = this.deps.repos.runtime.getSession(memory.sessionId); + if (session && namespace.workspaceId && session.workspaceId && namespace.workspaceId !== session.workspaceId) { + crossWorkspaceRisk += 1; + issues.push({ memoryId: memory.id, issue: "workspace_mismatch", severity: "risk", projectId: namespace.projectId, workspaceId: namespace.workspaceId, source }); + } + } + } + return { + summary: { total: memories.length, missingWorkspace, unknownSource, missingAgentSourceTag, crossWorkspaceRisk: crossWorkspaceRisk + missingWorkspace }, + issues: issues.slice(0, 500), + serverTime: this.now() + }; + } + + projectContextPack(input: RequestEnvelope & { userId?: string } = {}): { + namespace: RuntimeNamespace; + conventions: MemoryListItem[]; + commands: MemoryListItem[]; + architectureFacts: MemoryListItem[]; + recentTasks: Array<{ id: string; title: string; updatedAt: string }>; + userPreferences: MemoryListItem[]; + markdown: string; + generatedAt: string; + } { + const context = this.deps.resolveContext(input); + const memories = this.listAllMemoriesForStats({ ...input, namespace: context.namespace }) + .filter((memory) => memory.status === "activated" || memory.status === "resolving"); + const select = (pattern: RegExp, limit = 8) => memories + .filter((memory) => pattern.test(`${memory.tags.join(" ")} ${detailSummaryForMemory(memory)} ${memory.memoryValue}`)) + .slice(0, limit) + .map((memory) => this.deps.repos.memories.toListItem(memory)); + const conventions = select(/convention|约定|规范|rule|必须|should/i); + const commands = select(/command|命令|npm |pnpm |yarn |cargo |pytest|docker |git /i); + const architectureFacts = select(/architecture|架构|module|模块|service|database|repository|api/i); + const userPreferences = select(/preference|偏好|喜欢|不喜欢|prefer/i); + const recentTasks = this.deps.repos.runtime.listEpisodes(context.userId, 20) + .filter((episode) => this.episodeMatchesNamespace(episode, context.namespace)) + .slice(0, 8) + .map((episode) => ({ id: episode.id, title: String(this.deps.episodeRef(episode).title ?? this.deps.episodeRef(episode).summary ?? episode.id), updatedAt: episode.updatedAt })); + const section = (title: string, items: MemoryListItem[]) => `## ${title}\n${items.length ? items.map((item) => `- ${item.summary || item.title} (${item.id})`).join("\n") : "- 暂无"}`; + const markdown = [ + `# Project Memory Pack: ${context.namespace.projectId ?? context.namespace.workspaceId ?? "unscoped"}`, + section("当前项目约定", conventions), + section("常用命令", commands), + section("架构事实", architectureFacts), + `## 最近任务\n${recentTasks.length ? recentTasks.map((task) => `- ${task.title} (${task.id})`).join("\n") : "- 暂无"}`, + section("用户偏好", userPreferences) + ].join("\n\n"); + return { namespace: context.namespace, conventions, commands, architectureFacts, recentTasks, userPreferences, markdown, generatedAt: this.now() }; + } + + projectContextPacks(input: RequestEnvelope & { userId?: string } = {}) { + if (input.namespace) return { packs: [this.projectContextPack(input)], generatedAt: this.now() }; + const memories = this.listAllMemoriesForStats(input); + const namespaces = new Map(); + for (const memory of memories) { + const namespace = namespaceForMemory(memory); + const key = `${namespace.tenantId ?? "local"}:${namespace.projectId ?? "unscoped"}:${namespace.workspaceId ?? namespace.workspacePath ?? ""}`; + namespaces.set(key, namespace); + } + return { + packs: [...namespaces.values()].map((namespace) => this.projectContextPack({ ...input, namespace })), + generatedAt: this.now() }; } @@ -445,6 +615,8 @@ export class PanelReadModel { tags?: string[]; sourceAgent?: string; excludedSourceAgents?: string[]; + projectId?: string; + workspaceId?: string; page?: number; limit?: number; cursor?: string | number; @@ -468,7 +640,11 @@ export class PanelReadModel { status: input.status, tags: input.tags, agentId: input.sourceAgent, - excludedAgentIds: input.excludedSourceAgents + excludedAgentIds: input.excludedSourceAgents, + ...(input.namespace ? {} : { + projectId: input.projectId, + workspaceId: input.workspaceId + }) }; const total = input.q?.trim() ? this.deps.repos.memories.searchCount(input.q, { ...filter, status: filter.status ?? ["activated", "resolving"] }) @@ -489,7 +665,8 @@ export class PanelReadModel { items: memories.map((memory) => panelListItemFromMemory( this.deps.repos.memories.toListItem(memory), memory, - this.deps.repos.processing.get(memory.id) + this.deps.repos.processing.get(memory.id), + memory.sessionId ? this.deps.repos.runtime.getSession(memory.sessionId) : undefined )), page, pageSize, @@ -762,6 +939,11 @@ export class PanelReadModel { } return rows; } + + private memoryMatchesRequest(memory: Parameters[0], input: RequestEnvelope & { userId?: string }): boolean { + if (input.userId && memory.userId !== input.userId) return false; + return !input.namespace || sameProjectScope(namespaceForMemory(memory), input.namespace); + } } function runtimeNamespaceFromRecord(value: Record): RuntimeNamespace | undefined { diff --git a/Memory/src/service/read-model/panel.ts b/Memory/src/service/read-model/panel.ts index f548ca9ba..1bcd0f592 100644 --- a/Memory/src/service/read-model/panel.ts +++ b/Memory/src/service/read-model/panel.ts @@ -1,5 +1,6 @@ import type { MemoryListItem, MemoryProcessingRecord, MemoryRow } from "../../types.js"; import { isRecord } from "../../utils/json.js"; +import type { SessionRecord } from "../../storage/repositories.js"; import { IMPORT_FAILED_TAG, IMPORT_INDEXING_TAG, @@ -11,15 +12,18 @@ import { panelDateKey, panelRoundDecimal } from "./model-costs.js"; export function panelListItemFromMemory( item: MemoryListItem, memory: MemoryRow, - processing?: MemoryProcessingRecord + processing?: MemoryProcessingRecord, + session?: SessionRecord ): MemoryListItem { const spanGoal = panelSpanGoalForMemory(memory); + const namespace = panelNamespaceForMemory(memory, session); return { ...item, processing, metadata: { ...(item.metadata ?? {}), source: panelSourceForMemory(memory), + namespace, ...(spanGoal ? { spanGoal } : {}) }, tags: panelTagsForMemory(memory, processing) @@ -49,6 +53,79 @@ export function panelSourceDistribution(memories: MemoryRow[]): Array<{ source: .sort((a, b) => b.count - a.count || a.source.localeCompare(b.source)); } +export interface PanelNamespaceSummary { + tenantId: string; + projectId: string; + workspaceId?: string; + workspacePath?: string; + label: string; +} + +export function panelNamespaceForMemory(memory: MemoryRow, session?: SessionRecord): PanelNamespaceSummary { + const provenance = isRecord(memory.properties.internal_info.provenance) + ? memory.properties.internal_info.provenance + : {}; + const tenantId = firstString( + session?.meta.tenant_id, + session?.meta.tenantId, + memory.info.tenant_id, + memory.info.tenantId, + provenance.tenantId + ) ?? "local"; + const projectId = firstString( + session?.projectId, + memory.info.project_id, + memory.info.projectId, + provenance.projectId, + memory.appId + ) ?? "unscoped"; + const workspaceId = firstString( + session?.workspaceId, + memory.info.workspace_id, + memory.info.workspaceId, + provenance.workspaceId, + memory.appId + ); + const workspacePath = firstString( + session?.workspacePath, + memory.info.workspace_path, + memory.info.workspacePath, + provenance.workspacePath + ); + return { + tenantId, + projectId, + workspaceId, + workspacePath, + label: panelNamespaceLabel(projectId, workspacePath, workspaceId) + }; +} + +export function panelNamespaceDistribution( + memories: MemoryRow[], + sessionForMemory: (memory: MemoryRow) => SessionRecord | undefined +): Array { + const counts = new Map(); + for (const memory of memories) { + const namespace = panelNamespaceForMemory(memory, sessionForMemory(memory)); + const key = `${namespace.tenantId}:${namespace.projectId}:${namespace.workspaceId ?? ""}`; + const current = counts.get(key); + if (current) { + current.count += 1; + } else { + counts.set(key, { namespace, count: 1 }); + } + } + + return Array.from(counts.values()) + .map(({ namespace, count }) => ({ + ...namespace, + count, + percentage: memories.length > 0 ? panelRoundDecimal((count / memories.length) * 100, 1) : 0 + })) + .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label)); +} + export function panelCountByDate( rows: T[], dates: string[], @@ -100,6 +177,15 @@ function panelNormalizeExplicitSource(value: unknown): string | undefined { return panelNormalizeKnownSource(normalized) ?? normalized; } +function panelNamespaceLabel(projectId: string, workspacePath: string | undefined, workspaceId: string | undefined): string { + if (workspacePath) { + const parts = workspacePath.split("/").filter(Boolean); + return parts[parts.length - 1] || workspacePath; + } + if (projectId !== "unscoped") return projectId; + return workspaceId ?? "unscoped"; +} + function panelNormalizeKnownSource(value: unknown): string | undefined { if (typeof value !== "string" || !value.trim()) return undefined; const normalized = value.trim().toLowerCase(); diff --git a/Memory/src/service/retrieval/indexed-candidate-pool.ts b/Memory/src/service/retrieval/indexed-candidate-pool.ts index 05ceca1fa..753fcf004 100644 --- a/Memory/src/service/retrieval/indexed-candidate-pool.ts +++ b/Memory/src/service/retrieval/indexed-candidate-pool.ts @@ -156,7 +156,7 @@ export class IndexedCandidatePool { tagFilter: "auto" | "on" | "off"; } ): MemorySearchIdHit[] { - const tags = config.tagFilter === "off" ? [] : compiledQuery.tags; + const tags = config.tagFilter === "off" ? [] : retrievalSemanticTags(compiledQuery.tags); const search = (anyOfTags?: string[]): MemorySearchIdHit[] => { const summary = this.deps.repos.memories.searchVectorIds(queryVector, "vec_summary", filter, vectorPool, { anyOfTags @@ -235,3 +235,10 @@ export class IndexedCandidatePool { return total <= 0 ? [] : this.deps.repos.memories.list(filter, total); } } + +function retrievalSemanticTags(tags: readonly string[]): string[] { + const ignored = new Set(["agent-source", "codex", "pi", "hermes", "claude_code", "cursor", "opencode", "memmy"]); + return tags + .map((tag) => tag.trim()) + .filter((tag) => tag && !ignored.has(tag.toLowerCase())); +} diff --git a/Memory/src/storage/repositories.ts b/Memory/src/storage/repositories.ts index 923878c17..bce03962a 100644 --- a/Memory/src/storage/repositories.ts +++ b/Memory/src/storage/repositories.ts @@ -16,7 +16,7 @@ import type { } from "../types.js"; import { DEFAULT_NAMESPACE_SOURCE } from "../types.js"; import { newId, stableHash } from "../utils/id.js"; -import { asStringArray, parseJson, toJson } from "../utils/json.js"; +import { asStringArray, isRecord, parseJson, toJson } from "../utils/json.js"; import { nowIso } from "../utils/time.js"; import { attachMemoryVectors, @@ -1052,6 +1052,7 @@ export class MemoryRepository { summary: listSummaryForMemory(memory), tags: memory.tags, metrics: listMetricsForMemory(memory), + metadata: { namespace: namespaceSummaryFromMemory(memory) }, createdAt: memory.createdAt, updatedAt: memory.updatedAt, version: memory.version @@ -2462,6 +2463,33 @@ export class RuntimeRepository { return transaction(); } + retryFailedJobIds( + jobIds: readonly string[], + at = nowIso() + ): Array<{ before: EvolutionJobRecord; after: EvolutionJobRecord }> { + const ids = uniq([...jobIds]); + if (ids.length === 0) return []; + const placeholders = ids.map(() => "?").join(", "); + const transaction = this.db.transaction(() => { + const rows = this.db.prepare( + `SELECT * FROM evolution_jobs + WHERE id IN (${placeholders}) AND status IN ('failed', 'dead_letter')` + ).all(...ids) as SqlJobRow[]; + for (const row of rows) { + this.db.prepare( + `UPDATE evolution_jobs + SET status = 'queued', attempts = 0, leased_until = NULL, last_error = NULL, updated_at = ? + WHERE id = ?` + ).run(at, row.id); + } + return rows.map((row) => ({ + before: jobFromSql(row), + after: jobFromSql({ ...row, status: "queued", attempts: 0, leased_until: null, last_error: null, updated_at: at }) + })); + }); + return transaction(); + } + requeueLeasedJobsAfterRestart( at = nowIso() ): Array<{ before: EvolutionJobRecord; after: EvolutionJobRecord }> { @@ -3726,6 +3754,54 @@ function listSummaryForMemory(memory: MemoryRow): string { ) ?? ""; } +function namespaceSummaryFromMemory(memory: MemoryRow): { + tenantId: string; + projectId: string; + workspaceId?: string; + workspacePath?: string; + label: string; +} { + const provenance = isRecord(memory.properties.internal_info.provenance) + ? memory.properties.internal_info.provenance as Record + : {}; + const tenantId = firstString( + memory.info.tenant_id, + memory.info.tenantId, + provenance.tenantId + ) ?? "local"; + const projectId = firstString( + memory.info.project_id, + memory.info.projectId, + provenance.projectId, + memory.appId + ) ?? "unscoped"; + const workspaceId = firstString( + memory.info.workspace_id, + memory.info.workspaceId, + provenance.workspaceId, + memory.appId + ); + const workspacePath = firstString( + memory.info.workspace_path, + memory.info.workspacePath, + provenance.workspacePath + ); + return { + tenantId, + projectId, + workspaceId, + workspacePath, + label: workspacePath ? workspacePath.split("/").filter(Boolean).pop() || workspacePath : (projectId !== "unscoped" ? projectId : workspaceId ?? "unscoped") + }; +} + +function firstString(...values: unknown[]): string | undefined { + for (const value of values) { + if (typeof value === "string" && value.trim()) return value.trim(); + } + return undefined; +} + function listMetricsForMemory(memory: MemoryRow): MemoryListItem["metrics"] | undefined { const internal = memory.properties.internal_info; const trace = recordValue(internal.trace); @@ -3979,6 +4055,7 @@ function buildMemoryWhere(filter: MemoryFilter): { where: string; params: SqlVal addTenantClause(filter.tenantId); addValueClause("user_id", filter.userId); addProjectClause(filter.projectId); + addWorkspaceClause(filter.workspaceId); addValueClause("session_id", filter.sessionId); addValueClause("conversation_id", filter.conversationId); addAgentIdClause(filter.agentId, filter.excludedAgentIds); @@ -4044,6 +4121,29 @@ function buildMemoryWhere(filter: MemoryFilter): { where: string; params: SqlVal params.push(projectId); } + function addWorkspaceClause(workspaceId: string | undefined): void { + if (workspaceId === undefined) return; + const effectiveWorkspace = `CASE + WHEN session_id IS NOT NULL AND EXISTS ( + SELECT 1 FROM sessions AS workspace_session WHERE workspace_session.id = memories.session_id + ) THEN ( + SELECT workspace_session.workspace_id FROM sessions AS workspace_session WHERE workspace_session.id = memories.session_id + ) + ELSE COALESCE( + NULLIF(json_extract(info_json, '$.workspace_id'), ''), + NULLIF(json_extract(info_json, '$.workspaceId'), ''), + NULLIF(json_extract(properties_json, '$.internal_info.provenance.workspaceId'), ''), + NULLIF(app_id, '') + ) + END`; + if (workspaceId === "unscoped") { + clauses.push(`${effectiveWorkspace} IS NULL`); + return; + } + clauses.push(`${effectiveWorkspace} = ?`); + params.push(workspaceId); + } + function addAgentIdClause(value: string | undefined, excludedValues: string[] | undefined): void { if (value?.trim()) { clauses.push("lower(replace(replace(trim(agent_id), '-', '_'), ' ', '_')) = ?"); diff --git a/Memory/src/types.ts b/Memory/src/types.ts index 53c0d8ce0..bef7e77ab 100644 --- a/Memory/src/types.ts +++ b/Memory/src/types.ts @@ -151,6 +151,7 @@ export interface MemoryFilter { tenantId?: string; userId?: string; projectId?: string; + workspaceId?: string; sessionId?: string; conversationId?: string; agentId?: string; diff --git a/Memory/src/viewer/static.ts b/Memory/src/viewer/static.ts index 9939ae9da..d23c52f1f 100644 --- a/Memory/src/viewer/static.ts +++ b/Memory/src/viewer/static.ts @@ -196,10 +196,21 @@ export function memoryPanelHtml(): string { .progress-track { height: 7px; border-radius: 4px; background: var(--surface-soft); overflow: hidden; } .progress-fill { height: 100%; border-radius: inherit; background: var(--blue); } .source-count { text-align: right; color: var(--muted); } + .namespace-list { display: grid; gap: 9px; } + .namespace-row { display: grid; grid-template-columns: minmax(0, 1fr) 44px; gap: 8px; align-items: start; padding: 8px 0; border-bottom: 1px solid var(--line); } + .namespace-row:last-child { border-bottom: 0; } + .namespace-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .namespace-meta { margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); } .queue-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } .queue-item { padding: 10px; background: var(--surface-soft); border-radius: 6px; } .queue-item strong, .queue-item span { display: block; } .queue-item span { color: var(--muted); font-size: 10px; margin-top: 4px; } + .pipeline { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; } + .pipeline-stage { padding: 11px; border: 1px solid var(--line); border-radius: 7px; background: var(--surface-soft); } + .pipeline-stage strong, .pipeline-stage span { display: block; } + .pipeline-stage strong { font-size: 20px; margin: 6px 0; } + .pipeline-stage span, .pipeline-stage small { color: var(--muted); } + .audit-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 7px; } .toolbar { display: grid; grid-template-columns: minmax(220px, 1fr) 120px 130px 140px auto auto; gap: 7px; align-items: center; margin-bottom: 10px; } .workspace { display: grid; grid-template-columns: minmax(500px, 1.15fr) minmax(420px, .85fr); gap: 10px; height: calc(100vh - 150px); min-height: 500px; } .panel { min-width: 0; height: 100%; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); overflow: hidden; display: flex; flex-direction: column; } @@ -244,6 +255,13 @@ export function memoryPanelHtml(): string { .system-row span { color: var(--muted); } .model-list { display: grid; gap: 8px; } .model-item { display: flex; justify-content: space-between; gap: 10px; padding: 9px; border: 1px solid var(--line); border-radius: 6px; } + .review-list { display: grid; gap: 8px; } + .review-card { padding: 12px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); } + .review-card-head, .review-card-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; } + .review-card h4 { margin: 0; font-size: 13px; } + .review-card p { margin: 9px 0; color: var(--ink-secondary); line-height: 1.55; white-space: pre-wrap; } + .review-card-meta { display: flex; gap: 6px; flex-wrap: wrap; } + .review-card-actions { justify-content: flex-end; margin-top: 10px; } .auth-screen { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; padding: 18px; background: color-mix(in srgb, var(--canvas) 92%, transparent); backdrop-filter: blur(14px); } .auth-dialog { width: min(420px, 100%); border: 1px solid var(--line); border-radius: 8px; background: var(--surface-raised); box-shadow: var(--shadow); padding: 20px; } .auth-dialog .brand { padding: 0; margin-bottom: 18px; } @@ -302,6 +320,7 @@ export function memoryPanelHtml(): string { + @@ -357,6 +396,7 @@ export function memoryPanelHtml(): string { + @@ -373,7 +413,7 @@ export function memoryPanelHtml(): string { @@ -402,8 +442,16 @@ export function memoryPanelHtml(): string { +
+

Agent 来源与项目隔离审计

检查缺失 workspace、未知来源、旧来源标签与跨项目泄漏风险

+
+

审计问题

最多显示 500 条
Memory问题Project / WorkspaceSource
+

Workspace 上下文包

+
+
+
-

系统状态

服务、存储、模型与脱敏配置

+

系统状态

服务、存储、模型与脱敏配置

Memory 服务

存储

@@ -445,6 +493,7 @@ export function memoryPanelHtml(): string { total: 0, totalPages: 1, selectedMemoryId: undefined, + namespaceOptions: [], detailJson: {}, selectedActivityId: undefined, activityJson: {}, @@ -459,6 +508,10 @@ export function memoryPanelHtml(): string { metrics: {}, status: {}, config: {}, + evolution: {}, + reviewCandidates: [], + contextPack: {}, + namespaceAudit: {}, serviceActivity: {}, lastRequestMs: 0, toastTimer: undefined @@ -470,6 +523,7 @@ export function memoryPanelHtml(): string { memories: ["Memories", "搜索、检查与治理记忆"], activity: ["活动日志", "Codex、Pi 与工具调用"], tasks: ["任务与 Episodes", "对话批次、Turn 与关联记忆"], + audit: ["治理审计", "Agent 来源、Workspace 隔离与项目上下文包"], system: ["系统", "服务、存储、模型与配置"] }; @@ -516,6 +570,20 @@ export function memoryPanelHtml(): string { } function displayMemoryTitle(title, fallback) { const cleaned = String(title || "").replace(/^\\s*Summary:\\s*/i, "").trim(); return cleaned || fallback; } function valueAt(object, path, fallback = undefined) { let value = object; for (const key of path) value = value && typeof value === "object" ? value[key] : undefined; return value === undefined ? fallback : value; } + function namespaceLabel(namespace) { return namespace && namespace.label ? namespace.label : namespace && namespace.projectId ? namespace.projectId : "unscoped"; } + function namespaceMeta(namespace) { + if (!namespace) return "tenant local · project unscoped"; + const bits = ["tenant " + (namespace.tenantId || "local"), "project " + (namespace.projectId || "unscoped")]; + if (namespace.workspacePath) bits.push(namespace.workspacePath); + else if (namespace.workspaceId) bits.push(namespace.workspaceId); + return bits.join(" · "); + } + function namespaceOptionValue(namespace) { + if (!namespace) return ""; + if (namespace.workspaceId) return "workspace:" + namespace.workspaceId; + if (namespace.projectId) return "project:" + namespace.projectId; + return ""; + } function jsonText(value) { return JSON.stringify(value || {}, null, 2); } function copyJson(value) { return navigator.clipboard.writeText(jsonText(value)).then(() => showToast("已复制")); } function requestBody(reason) { return JSON.stringify({ requestId: "panel-" + Date.now(), adapterId: "memory-console", reason }); } @@ -542,6 +610,7 @@ export function memoryPanelHtml(): string { ["L3 世界模型", counts.worldModels, "长期模型"], ["Skills", counts.skills, "可复用能力"] ].map((item) => '
' + esc(item[0]) + '' + esc(formatNumber(item[1])) + '' + esc(item[2]) + '
').join(""); + renderLayerFilter(overview); } function renderAnalysis(analysis) { @@ -582,6 +651,37 @@ export function memoryPanelHtml(): string { $("activitySource").value = currentActivitySource; } + function renderNamespaces(overview) { + const namespaces = (overview.namespaceDistribution || []).slice(0, 8); + const total = namespaces.reduce((sum, namespace) => sum + Number(namespace.count || 0), 0); + $("namespaceTotal").textContent = formatNumber(total); + $("namespaceDistribution").innerHTML = namespaces.length ? namespaces.map((namespace) => + '
' + esc(namespaceLabel(namespace)) + '
' + esc(namespaceMeta(namespace)) + '
' + esc(namespace.count) + '
' + ).join("") : '
暂无项目数据
'; + const currentProjectScope = $("projectScope").value; + const options = [''].concat(namespaces.map((namespace) => '')).join(""); + $("projectScope").innerHTML = options; + $("projectScope").value = currentProjectScope; + } + + function renderLayerFilter(overview) { + const layerCounts = overview.layerCounts || (overview.stats && overview.stats.byLayer) || {}; + const currentLayer = $("layer").value; + const options = [ + ["", "全部层"], + ["L1", "L1"], + ["L2", "L2"], + ["L3", "L3"], + ["Skill", "Skill"] + ].map(([value, label]) => { + const count = value ? Number(layerCounts[value] || 0) : 0; + const suffix = value ? " (" + formatNumber(count) + ")" : ""; + return ''; + }).join(""); + $("layer").innerHTML = options; + $("layer").value = currentLayer; + } + function renderQueues(metrics) { const jobs = metrics.jobs || {}; const retries = metrics.embeddingRetries || {}; @@ -594,6 +694,94 @@ export function memoryPanelHtml(): string { ].map((item) => '
' + esc(formatNumber(item[0])) + '' + esc(item[1]) + '
').join(""); } + function renderEvolution(evolution) { + const layers = evolution.layers || []; + $("evolutionPipeline").innerHTML = layers.map((stage) => + '
' + esc(stage.layer) + '' + esc(formatNumber(stage.count)) + '' + esc(formatNumber(stage.candidates)) + ' 候选 · ' + esc(formatNumber(stage.queued)) + ' 排队 · ' + esc(formatNumber(stage.failed)) + ' 失败最近:' + esc(stage.recentJob ? stage.recentJob.jobType + ' / ' + stage.recentJob.status + ' / ' + stage.recentJob.id : '无') + '
' + ).join("") || '
暂无流水线数据
'; + $("l2ResolvingReason").textContent = valueAt(evolution, ["l2Resolving", "reason"], ""); + $("evolutionJobState").textContent = formatNumber((evolution.recentJobs || []).length) + " recent jobs"; + } + + function renderContextPack(result) { + const packs = result.packs || []; + const markdown = packs.length + ? packs.map((pack) => pack.markdown).join("\\n\\n---\\n\\n") + : "暂无项目上下文"; + $("contextPackMarkdown").textContent = markdown; + $("auditPacks").textContent = markdown; + } + function renderNamespaceAudit(audit) { + const summary = audit.summary || {}; + $("auditRiskState").textContent = Number(summary.crossWorkspaceRisk || 0) ? "存在风险" : "未发现泄漏风险"; + $("isolationAudit").innerHTML = [ + [summary.missingWorkspace, "无 Workspace"], [summary.unknownSource, "未知来源"], + [summary.missingAgentSourceTag, "缺来源标签"], [summary.crossWorkspaceRisk, "跨 Workspace 风险"] + ].map((item) => '
' + esc(formatNumber(item[0])) + '' + esc(item[1]) + '
').join(""); + const issues = audit.issues || []; + $("auditIssueCount").textContent = formatNumber(issues.length) + " issues"; + $("auditEmpty").classList.toggle("hidden", issues.length > 0); + $("auditRows").innerHTML = issues.map((issue) => '' + esc(issue.memoryId) + '' + esc(issue.issue) + '' + esc((issue.projectId || "unscoped") + " / " + (issue.workspaceId || "none")) + '' + esc(issue.source || "unknown") + '').join(""); + } + + function renderReviewCandidates(result) { + const candidates = Array.isArray(result.items) ? result.items : []; + state.reviewCandidates = candidates; + $("reviewCount").textContent = candidates.length ? formatNumber(candidates.length) + " 条候选" : "暂无待审核内容"; + $("bulkApproveCandidates").disabled = !candidates.some((candidate) => Number(candidate.confidence || 0) >= 0.8); + $("reviewCandidates").innerHTML = candidates.length ? candidates.map((candidate) => { + const evidence = candidate.evidence || {}; + const confidence = Number(candidate.confidence || 0); + const evidenceIds = Array.isArray(evidence.ids) ? evidence.ids : []; + return '
' + + '

' + esc(candidate.title || candidate.suggestedLayer || "候选提炼") + '

' + esc(candidate.suggestedLayer || "-") + '' + esc(candidate.confidenceLabel || "low") + ' ' + esc(Math.round(confidence * 100)) + '%风险 ' + esc(candidate.risk || "-") + '
' + + '

' + esc(candidate.conclusion || "") + '

' + + '
' + esc(formatNumber(evidence.episodeCount)) + ' episodes · ' + esc(formatNumber(evidence.l1Count)) + ' L1 evidence' + (evidenceIds.length ? ' · ' + esc(evidenceIds.join(", ")) : "") + '
' + + '
' + + '
'; + }).join("") : '
暂无待审核提炼
'; + for (const button of $("reviewCandidates").querySelectorAll("button[data-review-action]")) { + button.onclick = () => handleReviewAction(button.dataset.reviewAction, button.dataset.reviewId).catch(showError); + } + } + + async function loadReviewCandidates() { + const result = await api("/api/v1/panel/review/candidates?limit=100"); + renderReviewCandidates(result); + } + + async function handleReviewAction(action, id) { + const candidate = state.reviewCandidates.find((item) => item.id === id); + if (!candidate) return; + if (action === "reject") { + const reason = prompt("拒绝原因(可选)") ?? ""; + await api("/api/v1/panel/review/candidates/" + encodeURIComponent(id) + "/reject", { method: "POST", body: JSON.stringify({ requestId: "panel-" + Date.now(), adapterId: "memory-console", reason }) }); + showToast("候选已拒绝"); + } else { + let title; + let content; + if (action === "edit") { + title = prompt("修改标题", candidate.title || ""); + if (title === null) return; + content = prompt("修改结论", candidate.conclusion || ""); + if (content === null || !content.trim()) return; + } + await api("/api/v1/panel/review/candidates/" + encodeURIComponent(id) + "/approve", { method: "POST", body: JSON.stringify({ requestId: "panel-" + Date.now(), adapterId: "memory-console", ...(title !== undefined ? { title, content } : {}) }) }); + showToast(action === "edit" ? "已修改并批准" : "候选已批准"); + } + await Promise.all([loadDashboard(), loadReviewCandidates(), loadMemories()]); + } + + async function bulkApproveCandidates() { + const button = $("bulkApproveCandidates"); + button.disabled = true; + try { + const result = await api("/api/v1/panel/review/candidates/bulk-approve", { method: "POST", body: JSON.stringify({ requestId: "panel-" + Date.now(), adapterId: "memory-console", minimumConfidence: 0.8 }) }); + showToast("已批准 " + formatNumber(result.approved) + " 条高置信度候选"); + await Promise.all([loadDashboard(), loadReviewCandidates(), loadMemories()]); + } finally { button.disabled = false; } + } + function renderRecentActivity(activity) { const entries = (activity.entries || []).slice(0, 6); $("recentActivity").innerHTML = entries.length ? entries.map((entry) => @@ -602,11 +790,12 @@ export function memoryPanelHtml(): string { } async function loadDashboard() { - const [overview, analysis, metrics, status, activity] = await Promise.all([ - api("/api/v1/panel/overview"), api("/api/v1/panel/analysis"), api("/api/v1/panel/metrics"), api("/api/v1/panel/status"), api("/api/v1/panel/activity?limit=20") + const [overview, analysis, metrics, status, activity, evolution, contextPack, namespaceAudit, reviewCandidates] = await Promise.all([ + api("/api/v1/panel/overview"), api("/api/v1/panel/analysis"), api("/api/v1/panel/metrics"), api("/api/v1/panel/status"), api("/api/v1/panel/activity?limit=20"), + api("/api/v1/panel/evolution"), api("/api/v1/panel/context-packs"), api("/api/v1/panel/namespace-audit"), api("/api/v1/panel/review/candidates?limit=100") ]); - state.overview = overview; state.analysis = analysis; state.metrics = metrics; state.status = status; state.serviceActivity = activity; - renderStats(overview); renderAnalysis(analysis); renderActivityChart(overview); renderSources(overview); renderQueues(metrics); renderRecentActivity(activity); renderConnectionStatus(status); + state.overview = overview; state.analysis = analysis; state.metrics = metrics; state.status = status; state.serviceActivity = activity; state.evolution = evolution; state.contextPack = contextPack; state.namespaceAudit = namespaceAudit; + renderStats(overview); renderAnalysis(analysis); renderActivityChart(overview); renderSources(overview); renderNamespaces(overview); renderQueues(metrics); renderRecentActivity(activity); renderEvolution(evolution); renderContextPack(contextPack); renderNamespaceAudit(namespaceAudit); renderReviewCandidates(reviewCandidates); renderConnectionStatus(status); } function paramsForList() { @@ -617,6 +806,11 @@ export function memoryPanelHtml(): string { if ($("layer").value) params.set("layer", $("layer").value); if ($("status").value) params.set("status", $("status").value); if ($("sourceAgent").value) params.set("sourceAgent", $("sourceAgent").value); + if ($("projectScope").value) { + const parts = $("projectScope").value.split(":"); + if (parts[0] === "workspace") params.set("workspaceId", parts.slice(1).join(":")); + if (parts[0] === "project") params.set("projectId", parts.slice(1).join(":")); + } return params; } @@ -624,9 +818,10 @@ export function memoryPanelHtml(): string { $("emptyState").classList.toggle("hidden", items.length > 0); $("memoryRows").innerHTML = items.map((item) => { const source = valueAt(item, ["metadata", "source"], "unknown"); + const namespace = valueAt(item, ["metadata", "namespace"], undefined); return '' + '' + esc(item.memoryLayer) + '' + - '
' + esc(displayMemoryTitle(item.title, item.id)) + '
' + esc(item.summary || "") + '
' + esc(item.id) + ' · ' + esc(source) + '
' + + '
' + esc(displayMemoryTitle(item.title, item.id)) + '
' + esc(item.summary || "") + '
' + esc(item.id) + ' · ' + esc(source) + ' · ' + esc(namespaceLabel(namespace)) + '
' + '' + esc(item.status) + '' + '' + esc(formatDate(item.updatedAt || item.createdAt)) + '
v' + esc(item.version || 1) + '
'; }).join(""); @@ -657,6 +852,7 @@ export function memoryPanelHtml(): string { const item = data.item || {}; const tags = Array.isArray(item.tags) ? item.tags : []; const source = valueAt(item, ["metadata", "source"], valueAt(item, ["info", "source"], "unknown")); + const namespace = valueAt(item, ["metadata", "namespace"], undefined); const summary = item.summary || item.content || valueAt(item, ["properties", "summary"], ""); $("detailTitle").textContent = displayMemoryTitle(item.title, requestedMemoryId); $("detailId").textContent = requestedMemoryId; @@ -666,12 +862,16 @@ export function memoryPanelHtml(): string { '
Memory Layer' + esc(item.memoryLayer || item.layer || "-") + '
' + '
Status' + esc(item.status || "-") + '
' + '
Source' + esc(source) + '
' + + '
Project' + esc(namespaceLabel(namespace)) + '
' + + '
Workspace' + esc(namespace && (namespace.workspacePath || namespace.workspaceId) || "-") + '
' + '
Updated' + esc(formatDate(item.updatedAt || item.createdAt, true)) + '
' + '
' + '

Tags

' + (tags.length ? tags.map((tag) => '' + esc(tag) + '').join("") : '') + '
' + '

Raw JSON

' + esc(jsonText(data)) + '
'; $("detailJson").textContent = jsonText(data); $("deleteMemory").disabled = false; + $("markUseful").disabled = false; $("markNotUseful").disabled = false; $("mergeMemory").disabled = false; $("archiveMemory").disabled = false; + $("promoteMemory").disabled = (item.memoryLayer || item.layer) !== "L1"; } async function loadMemoryDetail(id) { @@ -700,6 +900,19 @@ export function memoryPanelHtml(): string { await Promise.all([loadMemories(), loadDashboard()]); } + async function memoryAction(action, body = {}) { + const id = state.selectedMemoryId; if (!id) return; + const result = await api("/api/v1/memory/" + encodeURIComponent(id) + "/" + action, { method: "POST", body: JSON.stringify({ requestId: "panel-" + Date.now(), adapterId: "memory-console", ...body }) }); + showToast(action === "quality" ? "质量标记已保存" : action === "promote" ? "已提升为 L2" : action === "archive" ? "记忆已归档" : "记忆已合并"); + await Promise.all([loadMemories(), loadDashboard()]); + return result; + } + async function mergeSelectedMemory() { + const sourceMemoryId = prompt("输入要合并进当前记忆的重复 Memory ID"); + if (!sourceMemoryId) return; + await memoryAction("merge", { sourceMemoryId, reason: "duplicate memory merged in console" }); + } + function activityParams() { const params = new URLSearchParams(); params.set("limit", "100"); if ($("activityTool").value) params.set("tools", $("activityTool").value); @@ -787,7 +1000,9 @@ export function memoryPanelHtml(): string { state.status = status; state.metrics = metrics; state.config = config; renderSystem(status, metrics, config); } - async function runWorker() { $("runWorker").disabled = true; try { const result = await api("/api/v1/worker/run", { method: "POST", body: JSON.stringify({ limit: 50, adapterId: "memory-console" }) }); showToast("Worker 完成:" + formatNumber(result.processed || result.completed || 0)); await loadSystem(); } finally { $("runWorker").disabled = false; } } + function generatedSummary(result) { const generated = result.generated || {}; return "L2 +" + formatNumber(generated.L2) + " · L3 +" + formatNumber(generated.L3) + " · Skill +" + formatNumber(generated.Skill); } + async function workerAction(buttonId, path) { $(buttonId).disabled = true; try { const result = await api(path, { method: "POST", body: JSON.stringify({ limit: 100, adapterId: "memory-console" }) }); showToast(generatedSummary(result)); await Promise.all([loadSystem(), loadDashboard(), loadMemories()]); } finally { $(buttonId).disabled = false; } } + async function runWorker() { return workerAction("runWorker", "/api/v1/worker/run"); } async function reloadConfig() { $("reloadConfig").disabled = true; try { await api("/api/v1/admin/reload-config", { method: "POST", body: JSON.stringify({ reason: "memory_console", adapterId: "memory-console" }) }); showToast("配置已重新加载"); await Promise.all([loadDashboard(), loadSystem()]); } finally { $("reloadConfig").disabled = false; } } async function refreshCurrentView() { @@ -798,6 +1013,7 @@ export function memoryPanelHtml(): string { else if (state.view === "memories") await loadMemories(); else if (state.view === "activity") await loadApiActivity(); else if (state.view === "tasks") await loadTasks(); + else if (state.view === "audit") await loadDashboard(); else if (state.view === "system") await loadSystem(); } catch (error) { showError(error); } } @@ -811,16 +1027,17 @@ export function memoryPanelHtml(): string { function applyTheme(theme) { document.documentElement.classList.toggle("dark", theme === "dark"); if (typeof localStorage !== "undefined") localStorage.setItem("memmyMemoryTheme", theme); } function initTheme() { const stored = typeof localStorage !== "undefined" ? localStorage.getItem("memmyMemoryTheme") : ""; const preferred = typeof matchMedia !== "undefined" && matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; applyTheme(stored || preferred); } - $("navDashboard").onclick = () => setView("dashboard"); $("navMemories").onclick = () => setView("memories"); $("navActivity").onclick = () => setView("activity"); $("navTasks").onclick = () => setView("tasks"); $("navSystem").onclick = () => setView("system"); + $("navDashboard").onclick = () => setView("dashboard"); $("navMemories").onclick = () => setView("memories"); $("navActivity").onclick = () => setView("activity"); $("navTasks").onclick = () => setView("tasks"); $("navAudit").onclick = () => setView("audit"); $("navSystem").onclick = () => setView("system"); $("refresh").onclick = refreshCurrentView; $("openActivity").onclick = () => setView("activity"); - $("search").onclick = applyFilters; $("clearFilters").onclick = () => { $("query").value = ""; $("layer").value = ""; $("status").value = ""; $("sourceAgent").value = ""; applyFilters(); }; + $("search").onclick = applyFilters; $("clearFilters").onclick = () => { $("query").value = ""; $("layer").value = ""; $("status").value = ""; $("sourceAgent").value = ""; $("projectScope").value = ""; applyFilters(); }; $("prevPage").onclick = () => { if (state.page > 1) { state.page -= 1; loadMemories(); } }; $("nextPage").onclick = () => { if (state.page < state.totalPages) { state.page += 1; loadMemories(); } }; $("pageInput").onkeydown = (event) => { if (event.key === "Enter") goToPage(); }; $("pageInput").onfocus = () => $("pageInput").select(); $("pageInput").onchange = goToPage; - $("query").onkeydown = (event) => { if (event.key === "Enter") applyFilters(); }; $("layer").onchange = applyFilters; $("status").onchange = applyFilters; $("sourceAgent").onchange = applyFilters; + $("query").onkeydown = (event) => { if (event.key === "Enter") applyFilters(); }; $("layer").onchange = applyFilters; $("status").onchange = applyFilters; $("sourceAgent").onchange = applyFilters; $("projectScope").onchange = applyFilters; $("copyJson").onclick = () => copyJson(state.detailJson); $("deleteMemory").onclick = () => removeSelectedMemory().catch(showError); + $("markUseful").onclick = () => memoryAction("quality", { useful: true }).catch(showError); $("markNotUseful").onclick = () => memoryAction("quality", { useful: false }).catch(showError); $("archiveMemory").onclick = () => memoryAction("archive", { reason: "noise archived in console" }).catch(showError); $("promoteMemory").onclick = () => memoryAction("promote", { reason: "manual L1 promotion" }).catch(showError); $("mergeMemory").onclick = () => mergeSelectedMemory().catch(showError); $("loadActivity").onclick = () => loadApiActivity().catch(showError); $("clearActivity").onclick = () => { $("activityQuery").value = ""; $("activityTool").value = ""; $("activitySource").value = ""; loadApiActivity().catch(showError); }; $("activityQuery").onkeydown = (event) => { if (event.key === "Enter") renderActivityRows(state.activityLogs); }; $("activityTool").onchange = () => loadApiActivity().catch(showError); $("activitySource").onchange = () => loadApiActivity().catch(showError); $("copyActivity").onclick = () => copyJson(state.activityJson); $("searchTasks").onclick = () => { state.taskPage = 1; loadTasks().catch(showError); }; $("clearTasks").onclick = () => { $("taskQuery").value = ""; state.taskPage = 1; loadTasks().catch(showError); }; $("taskQuery").onkeydown = (event) => { if (event.key === "Enter") { state.taskPage = 1; loadTasks().catch(showError); } }; $("prevTaskPage").onclick = () => { if (state.taskPage > 1) { state.taskPage -= 1; loadTasks().catch(showError); } }; $("nextTaskPage").onclick = () => { if (state.taskPage < state.taskTotalPages) { state.taskPage += 1; loadTasks().catch(showError); } }; $("copyTask").onclick = () => copyJson(state.taskJson); $("deleteTask").onclick = () => removeSelectedTask().catch(showError); - $("runWorker").onclick = () => runWorker().catch(showError); $("reloadConfig").onclick = () => reloadConfig().catch(showError); $("copyConfig").onclick = () => copyJson(state.config); + $("runWorker").onclick = () => runWorker().catch(showError); $("retryFailed").onclick = () => workerAction("retryFailed", "/api/v1/worker/retry-failed").catch(showError); $("promoteCandidates").onclick = () => workerAction("promoteCandidates", "/api/v1/worker/promote-candidates").catch(showError); $("reloadConfig").onclick = () => reloadConfig().catch(showError); $("copyConfig").onclick = () => copyJson(state.config); $("copyContextPack").onclick = () => navigator.clipboard.writeText((state.contextPack.packs || []).map((pack) => pack.markdown).join("\\n\\n---\\n\\n")).then(() => showToast("上下文包已复制")); $("copyAuditPacks").onclick = $("copyContextPack").onclick; $("bulkApproveCandidates").onclick = () => bulkApproveCandidates().catch(showError); $("themeToggle").onclick = () => applyTheme(document.documentElement.classList.contains("dark") ? "light" : "dark"); $("lockConsole").onclick = () => { memoryToken = ""; if (typeof sessionStorage !== "undefined") sessionStorage.removeItem("memmyMemoryToken"); showAuth(); }; $("connectToken").onclick = async () => { const token = $("tokenInput").value.trim(); if (!token) { showAuth("请输入访问令牌"); return; } memoryToken = token; if (typeof sessionStorage !== "undefined") sessionStorage.setItem("memmyMemoryToken", token); try { await api("/api/v1/panel/status"); hideAuth(); $("tokenInput").value = ""; await refreshAll(); } catch (error) { showAuth(error.message || String(error)); } }; diff --git a/Memory/tests/cli-command-map.test.ts b/Memory/tests/cli-command-map.test.ts index 6c70b1cf7..1914f1871 100644 --- a/Memory/tests/cli-command-map.test.ts +++ b/Memory/tests/cli-command-map.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; @@ -354,6 +354,81 @@ describe("memmy CLI command map", () => { expect(requests[0]?.headers["x-memmy-user-id"]).toBe("user_cli_1"); }); + it("sends the nearest project root as the default workspace namespace header", async () => { + const root = mkdtempSync(join(tmpdir(), "memmy-cli-workspace-")); + roots.push(root); + writeFileSync(join(root, "package.json"), "{}"); + const nested = join(root, "src"); + const previousCwd = process.cwd(); + mkdirSync(nested, { recursive: true }); + process.chdir(nested); + try { + const { requests } = await runMappedCommand(["search", "test failures"]); + expect(requests[0]?.headers["x-memmy-workspace-path"]).toBe(root); + } finally { + process.chdir(previousCwd); + } + }); + + it("sends explicit project and workspace namespace headers", async () => { + const { requests } = await runMappedCommand([ + "search", "test failures", + "--project-id", "project-alpha", + "--workspace-id", "workspace-alpha", + "--workspace-path", "/work/project-alpha" + ]); + + expect(requests[0]?.headers["x-memmy-project-id"]).toBe("project-alpha"); + expect(requests[0]?.headers["x-memmy-workspace-id"]).toBe("workspace-alpha"); + expect(requests[0]?.headers["x-memmy-workspace-path"]).toBe("/work/project-alpha"); + }); + + it("can disable automatic workspace namespace headers", async () => { + const { requests } = await runMappedCommand([ + "search", "test failures", + "--no-workspace" + ]); + + expect(requests[0]?.headers["x-memmy-project-id"]).toBeUndefined(); + expect(requests[0]?.headers["x-memmy-workspace-id"]).toBeUndefined(); + expect(requests[0]?.headers["x-memmy-workspace-path"]).toBeUndefined(); + }); + + it("reports the current workspace namespace without an HTTP call", async () => { + const root = mkdtempSync(join(tmpdir(), "memmy-cli-namespace-")); + roots.push(root); + writeFileSync(join(root, "package.json"), "{}"); + const previousCwd = process.cwd(); + process.chdir(root); + try { + await expect(runCommand({ argv: ["namespace", "current", "--source", "codex"] })).resolves.toMatchObject({ + source: "codex", + workspacePath: root, + scoped: true + }); + } finally { + process.chdir(previousCwd); + } + }); + + it("maps workspace stats to the scoped overview endpoint", async () => { + const { requests } = await runMappedCommand(["stats", "--workspace", "--workspace-id", "workspace-alpha"]); + expect(requests[0]?.method).toBe("GET"); + expect(requests[0]?.url).toBe(`${baseUrl}/api/v1/panel/overview`); + expect(requests[0]?.headers["x-memmy-workspace-id"]).toBe("workspace-alpha"); + }); + + it("runs doctor health, audit, and evolution checks", async () => { + const requests: CapturedRequest[] = []; + const result = await runCommand({ argv: ["doctor", "--url", baseUrl, "--no-workspace"], fetch: mockFetch(requests) }); + expect(requests.map((request) => new URL(request.url).pathname)).toEqual([ + "/api/v1/health", + "/api/v1/panel/namespace-audit", + "/api/v1/panel/evolution" + ]); + expect(result).toMatchObject({ checks: { service: "ok", workspaceIsolation: "ok" } }); + }); + it("sends source in the request body for CLI memory commands", async () => { const { requests } = await runMappedCommand([ "turn", "start", "test failures", @@ -513,7 +588,11 @@ function mockFetch(requests: CapturedRequest[]): typeof fetch { ? JSON.parse(init.body) as unknown : undefined; requests.push({ method, url, headers, body }); - return new Response(JSON.stringify({ ok: true }), { + const path = new URL(url).pathname; + const response = path === "/api/v1/panel/namespace-audit" + ? { summary: { crossWorkspaceRisk: 0, unknownSource: 0, missingAgentSourceTag: 0 } } + : { ok: true }; + return new Response(JSON.stringify(response), { status: 200, headers: { "content-type": "application/json" diff --git a/Memory/tests/contract/rest-panel-events.test.ts b/Memory/tests/contract/rest-panel-events.test.ts index e2ad9a26b..216eed401 100644 --- a/Memory/tests/contract/rest-panel-events.test.ts +++ b/Memory/tests/contract/rest-panel-events.test.ts @@ -78,20 +78,29 @@ describe("REST panel contract", () => { const overview = await client.panelOverview() as { counts: { memories: number } }; expect(overview.counts.memories).toBeGreaterThan(0); const panelHeaders = { authorization: "Bearer panel-token" }; - const [analysisResponse, metricsResponse, statusResponse, configResponse, activityResponse] = await Promise.all([ + const [analysisResponse, metricsResponse, statusResponse, configResponse, activityResponse, evolutionResponse, contextPackResponse, namespaceAuditResponse] = await Promise.all([ fetch(`${endpoint}/api/v1/panel/analysis`, { headers: panelHeaders }), fetch(`${endpoint}/api/v1/panel/metrics`, { headers: panelHeaders }), fetch(`${endpoint}/api/v1/panel/status`, { headers: panelHeaders }), fetch(`${endpoint}/api/v1/panel/config`, { headers: panelHeaders }), - fetch(`${endpoint}/api/v1/panel/activity?limit=10`, { headers: panelHeaders }) + fetch(`${endpoint}/api/v1/panel/activity?limit=10`, { headers: panelHeaders }), + fetch(`${endpoint}/api/v1/panel/evolution`, { headers: panelHeaders }), + fetch(`${endpoint}/api/v1/panel/context-packs`, { headers: panelHeaders }), + fetch(`${endpoint}/api/v1/panel/namespace-audit`, { headers: panelHeaders }) ]); expect([ analysisResponse.status, metricsResponse.status, statusResponse.status, configResponse.status, - activityResponse.status - ]).toEqual([200, 200, 200, 200, 200]); + activityResponse.status, + evolutionResponse.status, + contextPackResponse.status, + namespaceAuditResponse.status + ]).toEqual([200, 200, 200, 200, 200, 200, 200, 200]); + await expect(evolutionResponse.json()).resolves.toMatchObject({ l2Resolving: { active: expect.any(Boolean) } }); + await expect(contextPackResponse.json()).resolves.toMatchObject({ packs: [expect.objectContaining({ markdown: expect.stringContaining("Project Memory Pack") })] }); + await expect(namespaceAuditResponse.json()).resolves.toMatchObject({ summary: { total: expect.any(Number) } }); const configStatus = await configResponse.json() as { redacted: boolean; config: Record }; expect(configStatus.redacted).toBe(true); expect(configStatus.config).toBeTypeOf("object"); @@ -100,6 +109,32 @@ describe("REST panel contract", () => { expect(items.items.find((item) => item.id === completed.l1MemoryId)?.metadata?.source).toBe("openclaw"); const detail = await client.getMemory(completed.l1MemoryId) as { item: { id: string } }; expect(detail.item.id).toBe(completed.l1MemoryId); + + const actionHeaders = { ...panelHeaders, "content-type": "application/json" }; + const qualityResponse = await fetch(`${endpoint}/api/v1/memory/${completed.l1MemoryId}/quality`, { + method: "POST", headers: actionHeaders, body: JSON.stringify({ useful: true }) + }); + await expect(qualityResponse.json()).resolves.toMatchObject({ ok: true, useful: true }); + const promoteResponse = await fetch(`${endpoint}/api/v1/memory/${completed.l1MemoryId}/promote`, { + method: "POST", headers: actionHeaders, body: JSON.stringify({ reason: "contract promotion" }) + }); + const promoted = await promoteResponse.json() as { id: string; memoryLayer: string }; + expect(promoted).toMatchObject({ memoryLayer: "L2" }); + + const duplicate = service.addMemory({ content: "Duplicate L1 for merge", layer: "L1", source: "openclaw" }); + const mergeResponse = await fetch(`${endpoint}/api/v1/memory/${completed.l1MemoryId}/merge`, { + method: "POST", headers: actionHeaders, body: JSON.stringify({ sourceMemoryId: duplicate.id }) + }); + await expect(mergeResponse.json()).resolves.toMatchObject({ ok: true, archivedSourceId: duplicate.id }); + + for (const path of ["run", "retry-failed", "promote-candidates"]) { + const response = await fetch(`${endpoint}/api/v1/worker/${path}`, { + method: "POST", headers: actionHeaders, body: JSON.stringify({ limit: 20 }) + }); + const result = await response.json() as { generated?: { L2: number; L3: number; Skill: number } }; + expect(response.status).toBe(200); + expect(result.generated).toMatchObject({ L2: expect.any(Number), L3: expect.any(Number), Skill: expect.any(Number) }); + } const deleted = await client.deleteMemory(completed.l1MemoryId) as { ok: boolean; id: string; diff --git a/Memory/tests/fixtures/project-evidence-dataset.ts b/Memory/tests/fixtures/project-evidence-dataset.ts new file mode 100644 index 000000000..31d9d4060 --- /dev/null +++ b/Memory/tests/fixtures/project-evidence-dataset.ts @@ -0,0 +1,41 @@ +import type { ProjectEvidenceInput } from "../../src/service/evolution/project-evidence.js"; + +export interface ProjectEvidenceEpisodeFixture { + id: string; + label: "noise" | "evidence"; + input: ProjectEvidenceInput; +} + +// Compact replay set sampled from the failure modes seen in imported agent traces. +export const PROJECT_EVIDENCE_DATASET: ProjectEvidenceEpisodeFixture[] = [ + { id: "ep-01", label: "noise", input: { id: "ep-01", userText: "继续" } }, + { id: "ep-02", label: "noise", input: { id: "ep-02", userText: "有了吗" } }, + { id: "ep-03", label: "noise", input: { id: "ep-03", userText: "What is the architecture?" } }, + { id: "ep-04", label: "noise", input: { id: "ep-04", userText: "看看现在 L2 L3 skill 的质量如何" } }, + { id: "ep-05", label: "noise", input: { id: "ep-05", userText: "continue" } }, + { id: "ep-06", label: "noise", input: { id: "ep-06", userText: "You are a focused child agent spawned by a parent agent." } }, + { id: "ep-07", label: "noise", input: { id: "ep-07", userText: "下一步什么工作" } }, + { id: "ep-08", label: "noise", input: { id: "ep-08", userText: "Can you look at the API?" } }, + { id: "ep-09", label: "noise", input: { id: "ep-09", userText: "再看看" } }, + { id: "ep-10", label: "noise", input: { id: "ep-10", userText: "historical context" } }, + { id: "ep-11", label: "noise", input: { id: "ep-11", userText: "What commands should we use?" } }, + { id: "ep-12", label: "noise", input: { id: "ep-12", userText: "继续检查数据库" } }, + { id: "ep-13", label: "noise", input: { id: "ep-13", userText: "Are there any skills?" } }, + { id: "ep-14", label: "noise", input: { id: "ep-14", userText: "看看这个项目" } }, + { id: "ep-15", label: "noise", input: { id: "ep-15", userText: "The next task is to continue." } }, + { id: "ep-16", label: "evidence", input: { id: "ep-16", userText: "Run the migration and verify the schema.", agentText: "Migration completed successfully; schema check passed.", toolCalls: [{ name: "shell", output: "exit code 0" }] } }, + { id: "ep-17", label: "evidence", input: { id: "ep-17", userText: "Use workspace_id for isolation.", agentText: "Implemented the namespace filter and tests pass.", reflection: "The API now enforces workspace scope." } }, + { id: "ep-18", label: "evidence", input: { id: "ep-18", userText: "Retry the failed worker job.", agentText: "The job succeeded on attempt two.", toolCalls: [{ name: "worker", output: "status: succeeded" }] } }, + { id: "ep-19", label: "evidence", input: { id: "ep-19", userText: "Add a stable dedupe key.", agentText: "The key is now derived from normalized namespace and signature.", reflection: "Equivalent inputs produce the same key." } }, + { id: "ep-20", label: "evidence", input: { id: "ep-20", userText: "Run the TypeScript tests.", agentText: "16 tests passed with exit code 0.", toolCalls: [{ name: "shell", output: "16 passed" }] } }, + { id: "ep-21", label: "evidence", input: { id: "ep-21", userText: "Do not activate a candidate automatically.", agentText: "Candidates remain resolving until the quality gate is met.", reflection: "Activation now requires explicit evidence." } }, + { id: "ep-22", label: "evidence", input: { id: "ep-22", userText: "Delete the generated project summaries.", agentText: "Removed 43 derived memories and preserved all L1 rows.", toolCalls: [{ name: "sqlite", output: "integrity_check: ok" }] } }, + { id: "ep-23", label: "evidence", input: { id: "ep-23", userText: "Use structured JSON from the evolution model.", agentText: "The client retries malformed JSON and validates the object shape.", reflection: "Unstructured completions are rejected." } }, + { id: "ep-24", label: "evidence", input: { id: "ep-24", userText: "Run the Docker health check.", agentText: "The service is healthy on port 18960.", toolCalls: [{ name: "docker", output: "healthy" }] } }, + { id: "ep-25", label: "evidence", input: { id: "ep-25", userText: "Keep raw L1 evidence immutable.", agentText: "The cleanup transaction changed only derived rows.", reflection: "L1 count is unchanged after replay." } }, + { id: "ep-26", label: "evidence", input: { id: "ep-26", userText: "Require two independent policy supports.", agentText: "The L3 gate skipped the single-policy cluster.", reflection: "A lone policy cannot establish a world model." } }, + { id: "ep-27", label: "evidence", input: { id: "ep-27", userText: "Generate the skill only after a passed trial.", agentText: "The skill stayed resolving until the trial passed.", toolCalls: [{ name: "trial", output: "pass" }] } }, + { id: "ep-28", label: "evidence", input: { id: "ep-28", userText: "Filter injected historical context.", agentText: "The extractor classified the trace as noise and skipped it.", reflection: "Prompt wrappers are not project evidence." } }, + { id: "ep-29", label: "evidence", input: { id: "ep-29", userText: "Replay the historical L1 set.", agentText: "The replay produced no project-synthesis records.", toolCalls: [{ name: "replay", output: "bad=0" }] } }, + { id: "ep-30", label: "evidence", input: { id: "ep-30", userText: "Persist evidence anchors on every skill.", agentText: "The generated skill references three source trace IDs.", reflection: "The provenance chain is inspectable." } } +]; diff --git a/Memory/tests/service/evolution/project-evidence-dataset.test.ts b/Memory/tests/service/evolution/project-evidence-dataset.test.ts new file mode 100644 index 000000000..27beebaa1 --- /dev/null +++ b/Memory/tests/service/evolution/project-evidence-dataset.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { PROJECT_EVIDENCE_DATASET } from "../../fixtures/project-evidence-dataset.js"; +import { extractProjectEvidence } from "../../../src/service/evolution/project-evidence.js"; + +describe("project evidence replay quality set", () => { + it("contains 30 episodes with explicit expected labels", () => { + expect(PROJECT_EVIDENCE_DATASET).toHaveLength(30); + expect(new Set(PROJECT_EVIDENCE_DATASET.map((item) => item.label))).toEqual(new Set(["noise", "evidence"])); + }); + + it("keeps noise out and admits only evidence episodes", () => { + const results = PROJECT_EVIDENCE_DATASET.map((item) => ({ item, result: extractProjectEvidence(item.input) })); + expect(results.filter(({ item, result }) => item.label === "noise" && result.eligible)).toHaveLength(0); + expect(results.filter(({ item, result }) => item.label === "evidence" && !result.eligible)).toHaveLength(0); + }); +}); diff --git a/Memory/tests/service/evolution/project-evidence.test.ts b/Memory/tests/service/evolution/project-evidence.test.ts new file mode 100644 index 000000000..78f08636e --- /dev/null +++ b/Memory/tests/service/evolution/project-evidence.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { extractProjectEvidence } from "../../../src/service/evolution/project-evidence.js"; + +describe("project evidence extraction", () => { + it.each([ + "continue working", + "You are a focused child agent spawned by a parent agent.", + "What is the architecture?", + "继续" + ])("rejects %s as non-evidence", (userText) => { + expect(extractProjectEvidence({ id: "noise", userText }).eligible).toBe(false); + }); + + it("classifies a tool-backed successful procedure as eligible outcome evidence", () => { + const evidence = extractProjectEvidence({ + id: "trace-1", + userText: "Run the migration and verify the schema.", + agentText: "Migration completed successfully; the schema check passed.", + toolCalls: [{ name: "shell", output: "exit code 0" }] + }); + expect(evidence).toMatchObject({ kind: "outcome", eligible: true }); + expect(evidence.stableKey).toMatch(/^evidence:/); + }); + + it("uses the same stable key for equivalent evidence", () => { + const a = extractProjectEvidence({ id: "a", userText: "Run tests", agentText: "Tests passed" }); + const b = extractProjectEvidence({ id: "b", userText: " Run tests ", agentText: "Tests passed" }); + expect(a.stableKey).toBe(b.stableKey); + }); +}); diff --git a/Memory/tests/service/evolution/world-model.test.ts b/Memory/tests/service/evolution/world-model.test.ts index 6669b4168..65b9551fc 100644 --- a/Memory/tests/service/evolution/world-model.test.ts +++ b/Memory/tests/service/evolution/world-model.test.ts @@ -401,7 +401,7 @@ describe("MemoryService / evolution / world model", () => { updated_at: archivedWorld.updated_at }); expect(fresh).toMatchObject({ - status: "activated", + status: "resolving", memory_key: firstWorld!.memory_key }); diff --git a/Memory/tests/service/read-model/panel-read.test.ts b/Memory/tests/service/read-model/panel-read.test.ts index 8289af507..d35cc94fb 100644 --- a/Memory/tests/service/read-model/panel-read.test.ts +++ b/Memory/tests/service/read-model/panel-read.test.ts @@ -508,12 +508,12 @@ describe("MemoryService / read model / panel", () => { const list = service.panelItems({ namespace, layer: "L1" }); const itemBeforeEmbedding = list.items.find((item) => item.id === complete.l1MemoryId); expect(itemBeforeEmbedding?.tags).toContain("摘要总结中"); - expect(itemBeforeEmbedding?.tags).not.toContain("openclaw"); + expect(itemBeforeEmbedding?.tags).toEqual(expect.arrayContaining(["agent-source", "openclaw"])); expect(itemBeforeEmbedding?.metadata?.source).toBe("openclaw"); const detail = service.getMemory(complete.l1MemoryId, { namespace }); expect(detail.item.tags).toContain("摘要总结中"); - expect(detail.item.tags).not.toContain("openclaw"); + expect(detail.item.tags).toEqual(expect.arrayContaining(["agent-source", "openclaw"])); expect(detail.item.metadata.source).toBe("openclaw"); expect(detail.refs.episode).toMatchObject({ id: complete.episodeId, diff --git a/Memory/tests/service/review-candidates.test.ts b/Memory/tests/service/review-candidates.test.ts new file mode 100644 index 000000000..ac5cb4bc5 --- /dev/null +++ b/Memory/tests/service/review-candidates.test.ts @@ -0,0 +1,42 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { createMemoryServiceFixture } from "../fixtures/memory-service-fixture.js"; + +const { cleanup, createTestService } = createMemoryServiceFixture(); + +afterEach(() => cleanup()); + +describe("candidate review workflow", () => { + it("keeps AI conclusions resolving until approve, supports edit-approve and reject", () => { + const { db, service } = createTestService(); + const namespace = { source: "codex", profileId: "default", userId: "review-user", projectId: "review-project", workspaceId: "review-workspace" }; + const first = service.addMemory({ namespace, layer: "L2", content: "Use the workspace namespace for every read.", source: "worker", deferProcessing: true }); + const second = service.addMemory({ namespace, layer: "Skill", content: "Run tests, then verify the result.", source: "worker", deferProcessing: true }); + for (const [memory, confidence] of [[first, 0.92], [second, 0.86]] as const) { + db.db.prepare(`UPDATE memories + SET status = 'resolving', + info_json = json_set(info_json, '$.confidence', ?), + properties_json = json_set(properties_json, '$.status', 'resolving') + WHERE id = ?`).run(confidence, memory.id); + } + + expect(service.reviewCandidates({ namespace }).items).toHaveLength(2); + const approved = service.approveCandidate(first.id, { namespace, content: "Every memory read must include workspace namespace." }); + expect(approved).toMatchObject({ decision: "approved", status: "activated", layer: "L2" }); + const rejected = service.rejectCandidate(second.id, { namespace, reason: "procedure is incomplete" }); + expect(rejected).toMatchObject({ decision: "rejected", status: "archived", layer: "Skill" }); + expect(service.reviewCandidates({ namespace }).items).toHaveLength(0); + expect(db.db.prepare("SELECT memory_value FROM memories WHERE id = ?").get(first.id)).toMatchObject({ memory_value: "Every memory read must include workspace namespace." }); + }); + + it("bulk approves only high-confidence candidates", () => { + const { db, service } = createTestService(); + const namespace = { source: "codex", profileId: "default", userId: "bulk-review-user", projectId: "bulk-review-project", workspaceId: "bulk-review-workspace" }; + const ids = [0.95, 0.55].map((confidence, index) => { + const memory = service.addMemory({ namespace, layer: "L2", content: `Conclusion ${index}`, source: "worker", deferProcessing: true }); + db.db.prepare(`UPDATE memories SET status='resolving', info_json=json_set(info_json, '$.confidence', ?), properties_json=json_set(properties_json, '$.status', 'resolving') WHERE id=?`).run(confidence, memory.id); + return memory.id; + }); + expect(service.bulkApproveHighConfidenceCandidates({ namespace, minimumConfidence: 0.8 })).toMatchObject({ approved: 1, ids: [ids[0]] }); + expect(db.db.prepare("SELECT status FROM memories WHERE id = ?").get(ids[1])).toMatchObject({ status: "resolving" }); + }); +}); diff --git a/Memory/tests/service/session/turn-capture.test.ts b/Memory/tests/service/session/turn-capture.test.ts index a6714f0c7..f7ff907b7 100644 --- a/Memory/tests/service/session/turn-capture.test.ts +++ b/Memory/tests/service/session/turn-capture.test.ts @@ -467,7 +467,7 @@ describe("MemoryService / session / turn capture", () => { expect(detail.item.body).toContain("User:\nRun pwd in the terminal."); expect(detail.item.body).toContain("Tool calls:\n- terminal_bash"); expect(detail.item.body).toContain("Agent:\nThe command completed."); - expect(detail.item.tags).toEqual(expect.arrayContaining(["shell", "terminal"])); + expect(detail.item.tags).toEqual(expect.arrayContaining(["shell", "terminal", "agent-source", "memmy-agent"])); expect(detail.item.tags).not.toContain("trace"); expect(detail.item.tags).not.toContain("turn"); expect(detail.item.tags).not.toContain("memmy"); @@ -551,6 +551,11 @@ describe("MemoryService / session / turn capture", () => { expect(db.db.prepare( `SELECT agent_id FROM memories WHERE id = ?` ).get(added.id)).toEqual({ agent_id: "unknown" }); + const addedTags = db.db.prepare( + `SELECT tags_json, info_json FROM memories WHERE id = ?` + ).get(added.id) as { tags_json: string; info_json: string }; + expect(JSON.parse(addedTags.tags_json)).not.toContain("agent-source"); + expect(JSON.parse(addedTags.info_json)).toMatchObject({ source: "unknown" }); expect(db.db.prepare( `SELECT tool_name, source_agent FROM api_logs ORDER BY called_at DESC, id DESC` ).all()).toEqual([ @@ -584,16 +589,20 @@ describe("MemoryService / session / turn capture", () => { }); const inserted = db.db.prepare( - `SELECT memory_value, info_json + `SELECT memory_value, agent_id, tags_json, info_json FROM memories WHERE id = ?` - ).get(added.id) as { memory_value: string; info_json: string }; + ).get(added.id) as { memory_value: string; agent_id: string; tags_json: string; info_json: string }; expect(inserted.memory_value).toBe("The user prefers dev-jiang for this project."); expect(inserted.memory_value).not.toContain("Historical User"); expect(inserted.memory_value).not.toContain("current_user_request"); + expect(inserted.agent_id).toBe("codex"); + expect(JSON.parse(inserted.tags_json)).toEqual(expect.arrayContaining(["agent-source", "codex"])); expect(JSON.parse(inserted.info_json)).toMatchObject({ + source: "codex", title: "Project branch preference", - summary: "The user prefers dev-jiang for this project." + summary: "The user prefers dev-jiang for this project.", + tags: expect.arrayContaining(["agent-source", "codex"]) }); db.close(); diff --git a/Memory/tests/service/worker/worker-runtime.test.ts b/Memory/tests/service/worker/worker-runtime.test.ts index ab26b7407..b79e0b265 100644 --- a/Memory/tests/service/worker/worker-runtime.test.ts +++ b/Memory/tests/service/worker/worker-runtime.test.ts @@ -12,6 +12,46 @@ afterEach(() => { }); describe("MemoryService / worker / runtime", () => { + it("does not synthesize higher layers directly from workspace L1 keyword noise", async () => { + const { db, service } = createTestService(); + const namespace = { + source: "codex", + profileId: "default", + userId: "project-noise-user", + projectId: "project-noise", + workspaceId: "workspace-noise", + workspacePath: "/tmp/project-noise" + }; + const noise = [ + "Does the CLI filter by project and workspace namespace?", + "Can you look at the API architecture?", + "Should we run npm test now?", + "Continue checking the database service.", + "What commands did we discuss for docker?" + ]; + for (const [index, content] of noise.entries()) { + service.addMemory({ + namespace, + content, + layer: "L1", + source: "codex", + adapterId: `noise-${index}`, + deferProcessing: true + }); + } + + await service.runWorkerOnce(100, { namespace }); + + const layers = db.db.prepare( + `SELECT memory_layer AS layer, COUNT(*) AS count + FROM memories + GROUP BY memory_layer` + ).all() as Array<{ layer: string; count: number }>; + expect(layers).toEqual([{ layer: "L1", count: 5 }]); + + db.close(); + }); + it("selects the earliest worker wake across evolution and embedding queues", () => { const { db, service } = createTestService(); const repos = new Repositories(db.db); diff --git a/Memory/tests/viewer-static.test.ts b/Memory/tests/viewer-static.test.ts index 99c7e0229..9fdeee9ed 100644 --- a/Memory/tests/viewer-static.test.ts +++ b/Memory/tests/viewer-static.test.ts @@ -17,6 +17,16 @@ describe("memoryPanelHtml", () => { expect(harness.rowHtml()).not.toContain('
Summary:'); }); + it("shows layer counts in the memory layer filter", async () => { + const harness = createViewerHarness(); + runViewerScript(harness); + await flushPromises(); + + const layerHtml = harness.element("layer").innerHTML; + expect(layerHtml).toContain("L1 (1,297)"); + expect(layerHtml).toContain("L2 (2)"); + }); + it("keeps the right JSON panel on the latest clicked memory detail", async () => { const harness = createViewerHarness(); runViewerScript(harness); @@ -150,11 +160,13 @@ function createViewerHarness() { "navMemories", "navActivity", "navTasks", + "navAudit", "navSystem", "viewDashboard", "viewMemories", "viewActivity", "viewTasks", + "viewAudit", "viewSystem", "pageTitle", "pageSubtitle", @@ -171,14 +183,29 @@ function createViewerHarness() { "activityTotal", "sourceDistribution", "sourceTotal", + "namespaceDistribution", + "namespaceTotal", "queueSummary", "queueState", "recentActivity", "openActivity", + "evolutionPipeline", + "l2ResolvingReason", + "evolutionJobState", + "contextPackMarkdown", + "copyContextPack", + "isolationAudit", + "auditRiskState", + "auditIssueCount", + "auditRows", + "auditEmpty", + "auditPacks", + "copyAuditPacks", "query", "layer", "status", "sourceAgent", + "projectScope", "memoryRows", "emptyState", "listMeta", @@ -192,6 +219,11 @@ function createViewerHarness() { "detailContent", "detailJson", "deleteMemory", + "markUseful", + "markNotUseful", + "mergeMemory", + "promoteMemory", + "archiveMemory", "activityQuery", "activityTool", "activitySource", @@ -228,6 +260,8 @@ function createViewerHarness() { "systemQueues", "configJson", "runWorker", + "retryFailed", + "promoteCandidates", "reloadConfig", "copyConfig", "authScreen", @@ -239,6 +273,20 @@ function createViewerHarness() { "search", "clearFilters", "copyJson" + ,"reviewHeading" + ,"reviewCount" + ,"reviewCandidates" + ,"bulkApproveCandidates" + ,"evolutionPipeline" + ,"l2ResolvingReason" + ,"evolutionJobState" + ,"contextPackMarkdown" + ,"auditPacks" + ,"auditRiskState" + ,"isolationAudit" + ,"auditIssueCount" + ,"auditEmpty" + ,"auditRows" ]; for (const id of ids) { @@ -269,7 +317,13 @@ function createViewerHarness() { const fetch = async (path: string, options: { headers?: Record } = {}) => { requests.push({ path, authorization: options.headers?.authorization }); if (path === "/api/v1/panel/overview") { - return jsonResponse({ counts: { memories: 2, experiences: 0, worldModels: 0, skills: 0 } }); + return jsonResponse({ + counts: { memories: 2, experiences: 0, worldModels: 0, skills: 0 }, + layerCounts: { L1: 1297, L2: 2, L3: 0, Skill: 0 }, + sourceDistribution: [{ source: "codex", count: 2, percentage: 100 }], + namespaceDistribution: [{ tenantId: "local", projectId: "demo", workspaceId: "workspace_1", workspacePath: "/tmp/demo", label: "demo", count: 2, percentage: 100 }], + dailyActivity: [] + }); } if (path === "/api/v1/panel/analysis") { return jsonResponse({ metrics: { avgRecallScore: 0.8, recallEvents: 2, activeSkills: 0, recentlyUsedSkills: 0, avgToolLatencyMs: 12, p95ToolLatencyMs: 20 }, dailyMemoryWrites: [], dailySkillEvolutions: [], toolLatency: { tools: [], series: [] } }); @@ -283,6 +337,18 @@ function createViewerHarness() { if (path === "/api/v1/panel/activity?limit=20") { return jsonResponse({ entries: [] }); } + if (path === "/api/v1/panel/evolution") { + return jsonResponse({ layers: [{ layer: "L1", count: 2, candidates: 0, queued: 0, failed: 0 }], l2Resolving: { active: false, count: 0, reason: "没有符合条件的候选" }, recentJobs: [] }); + } + if (path === "/api/v1/panel/context-packs") { + return jsonResponse({ packs: [{ markdown: "# Project Memory Pack: demo" }] }); + } + if (path === "/api/v1/panel/namespace-audit") { + return jsonResponse({ summary: { total: 2, missingWorkspace: 0, unknownSource: 0, missingAgentSourceTag: 0, crossWorkspaceRisk: 0 }, issues: [] }); + } + if (path === "/api/v1/panel/review/candidates?limit=100") { + return jsonResponse({ items: [], total: 0 }); + } if (path.startsWith("/api/v1/panel/items?")) { return jsonResponse({ items: [ @@ -342,6 +408,7 @@ function createViewerHarness() { class FakeElement { html = ""; + innerHTML = ""; textContent = ""; value = ""; disabled = false; @@ -386,7 +453,17 @@ function listItem(id: string, title: string) { tags: [], createdAt: "2026-06-22T00:00:00.000Z", updatedAt: "2026-06-22T00:00:00.000Z", - version: 1 + version: 1, + metadata: { + source: "codex", + namespace: { + tenantId: "local", + projectId: "demo", + workspaceId: "workspace_1", + workspacePath: "/tmp/demo", + label: "demo" + } + } }; } diff --git a/README.md b/README.md index ba7a4934b..51e0b1c90 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,9 @@ The desktop app and `scripts/dev-start.sh` prepare the matching managed Chromium ```bash memmy-memory init # Write the Memory config and install Skills for each Agent as needed memmy-memory health +memmy-memory doctor +memmy-memory namespace current +memmy-memory stats --workspace memmy-memory search "memory policies in this project" memmy-memory add "a piece of knowledge worth saving" memmy-memory get diff --git a/README.zh-CN.md b/README.zh-CN.md index 744f6b403..6a5b44015 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -188,6 +188,9 @@ providers: ```bash memmy-memory init # 写入 Memory 配置,并按需为各 Agent 安装 Skill memmy-memory health +memmy-memory doctor +memmy-memory namespace current +memmy-memory stats --workspace memmy-memory search "项目里的记忆策略" memmy-memory add "这是一条需要保存的知识" memmy-memory get diff --git a/compose.yaml b/compose.yaml index cf10e49f1..fb4460fa0 100644 --- a/compose.yaml +++ b/compose.yaml @@ -12,6 +12,19 @@ services: MEMMY_MEMORY_PORT: "18960" MEMMY_MEMORY_DB: /data/memory.sqlite MEMMY_MEMORY_TOKEN: "${MEMMY_MEMORY_TOKEN:?Set a strong random MEMMY_MEMORY_TOKEN in .env}" + MEMMY_SUMMARY_PROVIDER: "${MEMMY_SUMMARY_PROVIDER:-openai_compatible}" + MEMMY_SUMMARY_ENDPOINT: "${MEMMY_SUMMARY_ENDPOINT:-}" + MEMMY_SUMMARY_MODEL: "${MEMMY_SUMMARY_MODEL:-auto/best-fast}" + MEMMY_SUMMARY_API_KEY: "${MEMMY_SUMMARY_API_KEY:-}" + MEMMY_SUMMARY_MAX_TOKENS: "${MEMMY_SUMMARY_MAX_TOKENS:-768}" + MEMMY_SUMMARY_TIMEOUT_MS: "${MEMMY_SUMMARY_TIMEOUT_MS:-60000}" + MEMMY_EVOLUTION_PROVIDER: "${MEMMY_EVOLUTION_PROVIDER:-openai_compatible}" + MEMMY_EVOLUTION_ENDPOINT: "${MEMMY_EVOLUTION_ENDPOINT:-}" + MEMMY_EVOLUTION_MODEL: "${MEMMY_EVOLUTION_MODEL:-auto/best-reasoning}" + MEMMY_EVOLUTION_API_KEY: "${MEMMY_EVOLUTION_API_KEY:-}" + MEMMY_EVOLUTION_ENABLE_THINKING: "${MEMMY_EVOLUTION_ENABLE_THINKING:-true}" + MEMMY_EVOLUTION_MAX_TOKENS: "${MEMMY_EVOLUTION_MAX_TOKENS:-4096}" + MEMMY_EVOLUTION_TIMEOUT_MS: "${MEMMY_EVOLUTION_TIMEOUT_MS:-180000}" ports: - "127.0.0.1:${MEMMY_MEMORY_HOST_PORT:-18960}:18960" volumes: diff --git a/deploy/memory/docker-compose.yml b/deploy/memory/docker-compose.yml index 3c40a7dd6..d34374293 100644 --- a/deploy/memory/docker-compose.yml +++ b/deploy/memory/docker-compose.yml @@ -11,6 +11,19 @@ services: MEMMY_MEMORY_PORT: "18960" MEMMY_MEMORY_DB: /data/memory.sqlite MEMMY_MEMORY_TOKEN: "${MEMMY_MEMORY_TOKEN:?Set a strong random MEMMY_MEMORY_TOKEN in .env}" + MEMMY_SUMMARY_PROVIDER: "${MEMMY_SUMMARY_PROVIDER:-openai_compatible}" + MEMMY_SUMMARY_ENDPOINT: "${MEMMY_SUMMARY_ENDPOINT:-}" + MEMMY_SUMMARY_MODEL: "${MEMMY_SUMMARY_MODEL:-auto/best-fast}" + MEMMY_SUMMARY_API_KEY: "${MEMMY_SUMMARY_API_KEY:-}" + MEMMY_SUMMARY_MAX_TOKENS: "${MEMMY_SUMMARY_MAX_TOKENS:-768}" + MEMMY_SUMMARY_TIMEOUT_MS: "${MEMMY_SUMMARY_TIMEOUT_MS:-60000}" + MEMMY_EVOLUTION_PROVIDER: "${MEMMY_EVOLUTION_PROVIDER:-openai_compatible}" + MEMMY_EVOLUTION_ENDPOINT: "${MEMMY_EVOLUTION_ENDPOINT:-}" + MEMMY_EVOLUTION_MODEL: "${MEMMY_EVOLUTION_MODEL:-auto/best-reasoning}" + MEMMY_EVOLUTION_API_KEY: "${MEMMY_EVOLUTION_API_KEY:-}" + MEMMY_EVOLUTION_ENABLE_THINKING: "${MEMMY_EVOLUTION_ENABLE_THINKING:-true}" + MEMMY_EVOLUTION_MAX_TOKENS: "${MEMMY_EVOLUTION_MAX_TOKENS:-4096}" + MEMMY_EVOLUTION_TIMEOUT_MS: "${MEMMY_EVOLUTION_TIMEOUT_MS:-180000}" ports: - "127.0.0.1:${MEMMY_MEMORY_HOST_PORT:-18960}:18960" volumes: diff --git a/docs/cn/memory/memory-cli.mdx b/docs/cn/memory/memory-cli.mdx index feb074cb0..9be98c34e 100644 --- a/docs/cn/memory/memory-cli.mdx +++ b/docs/cn/memory/memory-cli.mdx @@ -8,12 +8,17 @@ icon: Terminal ```bash memmy-memory init memmy-memory health +memmy-memory doctor +memmy-memory namespace current +memmy-memory stats --workspace memmy-memory search "项目里的记忆策略" memmy-memory add "这是一条需要保存的知识" memmy-memory get memmy-memory delete ``` -默认连接 `http://127.0.0.1:18960`。可用 `--url`、`--token`、`--config`、`--source`、`--user-id` 指定目标服务、认证、调用来源和用户命名空间。 +默认连接 `http://127.0.0.1:18960`。可用 `--url`、`--token`、`--config`、`--source`、`--user-id`、`--project-id`、`--workspace-id`、`--workspace-path` 指定目标服务、认证、调用来源和命名空间。除非传入 `--no-workspace`,CLI 会自动发现最近的项目根目录并作为 workspace namespace 发送。 + +`doctor` 会检查服务健康、记忆演化、未知 Agent 来源、缺失 workspace 绑定和跨 workspace 泄漏风险;`stats --workspace` 只返回当前 workspace 的层级与队列统计。 另支持 `reload-config`、`session open/close`、`turn start/complete` 和 `raw` 调用。 diff --git a/docs/en/memory/memory-cli.mdx b/docs/en/memory/memory-cli.mdx index 469261f46..c8222860e 100644 --- a/docs/en/memory/memory-cli.mdx +++ b/docs/en/memory/memory-cli.mdx @@ -8,12 +8,17 @@ Gives external Agents or scripts access to the Memory service: ```bash memmy-memory init memmy-memory health +memmy-memory doctor +memmy-memory namespace current +memmy-memory stats --workspace memmy-memory search "memory policies in this project" memmy-memory add "a piece of knowledge worth saving" memmy-memory get memmy-memory delete ``` -Connects to `http://127.0.0.1:18960` by default. Use `--url`, `--token`, `--config`, `--source`, and `--user-id` to specify the target service, authentication, calling source, and user namespace. +Connects to `http://127.0.0.1:18960` by default. Use `--url`, `--token`, `--config`, `--source`, `--user-id`, `--project-id`, `--workspace-id`, and `--workspace-path` to specify the target service, authentication, source, and namespace. Unless `--no-workspace` is set, the CLI discovers the nearest project root and sends it as the workspace namespace. + +`doctor` checks service health, memory evolution, unknown Agent sources, missing workspace bindings, and cross-workspace leakage risk. `stats --workspace` limits layer and queue statistics to the current workspace. Also supports reload-config, session open/close, turn start/complete, and raw calls. From 24328772c8f3b8d16a7d8b021bbe3d2eea3cb265 Mon Sep 17 00:00:00 2001 From: bluewater_cg Date: Sat, 8 Aug 2026 08:39:05 -0700 Subject: [PATCH 12/23] feat: add context pack views and token usage stats --- App/backend/local-api-contracts/src/index.ts | 37 ++ .../local-api/routes/agent-token-stats.ts | 25 + .../src/adapters/inbound/local-api/server.ts | 5 + .../src/services/agent-token-stats-service.ts | 457 ++++++++++++++++++ App/backend/src/services/index.ts | 9 +- .../desktop/src/analytics/page-view.ts | 1 + .../src/api/agent-token-stats-client.ts | 22 + App/frontend/desktop/src/api/client-types.ts | 8 +- App/frontend/desktop/src/i18n/messages.ts | 42 ++ .../desktop/src/pages/memory-page.tsx | 8 +- .../pages/memory/memory-prototype-icons.tsx | 11 + .../src/pages/memory/token-stats-sub-page.tsx | 250 ++++++++++ Memory/src/server/http.ts | 38 +- .../src/service/agent-token-stats-service.ts | 420 ++++++++++++++++ Memory/src/service/memory-service.ts | 67 +++ Memory/src/viewer/static.ts | 348 ++++++++++++- .../tests/agent-token-stats-service.test.ts | 133 +++++ .../contract/memory-rest-service.test.ts | 66 +++ Memory/tests/viewer-static.test.ts | 55 ++- compose.yaml | 3 + 20 files changed, 1986 insertions(+), 19 deletions(-) create mode 100644 App/backend/src/adapters/inbound/local-api/routes/agent-token-stats.ts create mode 100644 App/backend/src/services/agent-token-stats-service.ts create mode 100644 App/frontend/desktop/src/api/agent-token-stats-client.ts create mode 100644 App/frontend/desktop/src/pages/memory/token-stats-sub-page.tsx create mode 100644 Memory/src/service/agent-token-stats-service.ts create mode 100644 Memory/tests/agent-token-stats-service.test.ts diff --git a/App/backend/local-api-contracts/src/index.ts b/App/backend/local-api-contracts/src/index.ts index a8eb62b50..6a383c4f5 100644 --- a/App/backend/local-api-contracts/src/index.ts +++ b/App/backend/local-api-contracts/src/index.ts @@ -169,6 +169,43 @@ export const ByokTokenUsageSummarySchema = z.object({ }); export type ByokTokenUsageSummary = z.infer; +// --- Agent Token Usage Stats (Pi / Codex / Claude Code) --- + +export const AgentKindSchema = z.enum(["pi", "codex", "claude_code"]); +export type AgentKind = z.infer; + +export const AgentTokenStatsDtoSchema = z.object({ + agent: AgentKindSchema, + sessions: z.number().int().nonnegative(), + apiCalls: z.number().int().nonnegative(), + inputTokens: z.number().int().nonnegative(), + outputTokens: z.number().int().nonnegative(), + cacheReadTokens: z.number().int().nonnegative(), + cacheWriteTokens: z.number().int().nonnegative(), + reasoningTokens: z.number().int().nonnegative().optional(), + totalTokens: z.number().int().nonnegative(), + cost: z.number().nonnegative().optional(), + available: z.boolean() +}); +export type AgentTokenStatsDto = z.infer; + +export const ProjectTokenStatsDtoSchema = z.object({ + project: z.string(), + agents: z.array(AgentTokenStatsDtoSchema), + combinedInputTokens: z.number().int().nonnegative(), + combinedOutputTokens: z.number().int().nonnegative(), + combinedCacheReadTokens: z.number().int().nonnegative(), + combinedTotalTokens: z.number().int().nonnegative(), + estimatedCost: z.number().nonnegative().optional() +}); +export type ProjectTokenStatsDto = z.infer; + +export const AgentTokenStatsResponseSchema = z.object({ + projects: z.array(ProjectTokenStatsDtoSchema), + scannedAt: z.string().datetime() +}); +export type AgentTokenStatsResponse = z.infer; + export const AgentGatewayRuntimeConfigSchema = z.object({ baseUrl: z.string().url(), bootstrapSecret: z.string().min(1).optional() diff --git a/App/backend/src/adapters/inbound/local-api/routes/agent-token-stats.ts b/App/backend/src/adapters/inbound/local-api/routes/agent-token-stats.ts new file mode 100644 index 000000000..5366d898c --- /dev/null +++ b/App/backend/src/adapters/inbound/local-api/routes/agent-token-stats.ts @@ -0,0 +1,25 @@ +import { AgentTokenStatsResponseSchema } from "@memmy/local-api-contracts"; +import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; +import type { AgentTokenStatsService } from "../../../../services/agent-token-stats-service.js"; +import { withErrorEnvelope } from "../../../../services/error-envelope.js"; + +export interface RegisterAgentTokenStatsRoutesOptions { + agentTokenStats: AgentTokenStatsService; + authenticateRuntimeToken: (request: FastifyRequest, reply: FastifyReply) => Promise; +} + +export function registerAgentTokenStatsRoutes( + app: FastifyInstance, + options: RegisterAgentTokenStatsRoutesOptions +): void { + app.get( + "/api/app/agent-token-stats", + { preHandler: options.authenticateRuntimeToken }, + withErrorEnvelope(async (_request, reply) => { + const response = AgentTokenStatsResponseSchema.parse( + await options.agentTokenStats.getStats() + ); + return reply.send(response); + }) + ); +} diff --git a/App/backend/src/adapters/inbound/local-api/server.ts b/App/backend/src/adapters/inbound/local-api/server.ts index ed5d346e8..14f189e74 100644 --- a/App/backend/src/adapters/inbound/local-api/server.ts +++ b/App/backend/src/adapters/inbound/local-api/server.ts @@ -11,6 +11,7 @@ import { registerAgentSourceRoutes } from "./routes/agent-sources.js"; import { registerAgentRuntimeRoutes } from "./routes/agent-runtime/index.js"; import { registerAsrRoutes } from "./routes/asr.js"; import { registerByokTokenUsageRoutes } from "./routes/byok-token-usage.js"; +import { registerAgentTokenStatsRoutes } from "./routes/agent-token-stats.js"; import { registerChannelRoutes } from "./routes/channels.js"; import { registerComposioMcpRoutes } from "./routes/composio-mcp.js"; import { registerIntegrationRoutes } from "./routes/integrations.js"; @@ -122,6 +123,10 @@ export function createLocalApiServer(options: CreateLocalApiServerOptions): Fast byokTokenUsage: options.services.byokTokenUsage, authenticateRuntimeToken }); + registerAgentTokenStatsRoutes(app, { + agentTokenStats: options.services.agentTokenStats, + authenticateRuntimeToken + }); registerAsrRoutes(app, { asr: options.services.asr, authenticateRuntimeToken diff --git a/App/backend/src/services/agent-token-stats-service.ts b/App/backend/src/services/agent-token-stats-service.ts new file mode 100644 index 000000000..f808a69c8 --- /dev/null +++ b/App/backend/src/services/agent-token-stats-service.ts @@ -0,0 +1,457 @@ +import { + AgentKindSchema, + AgentTokenStatsDtoSchema, + ProjectTokenStatsDtoSchema, + AgentTokenStatsResponseSchema, + type AgentKind, + type AgentTokenStatsDto, + type ProjectTokenStatsDto, + type AgentTokenStatsResponse +} from "@memmy/local-api-contracts"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { readJsonlObjects, type JsonObject } from "../adapters/outbound/agent-source/jsonl-lines.js"; + +export interface AgentTokenStatsService { + getStats(): Promise; +} + +export interface CreateAgentTokenStatsServiceOptions { + /** Override home directory for testing. */ + homeDir?: string; +} + +interface PerAgentAccumulator { + sessions: number; + apiCalls: number; + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + cacheWriteTokens: number; + reasoningTokens: number; + totalTokens: number; + cost: number; +} + +interface ProjectAccumulator { + pi: PerAgentAccumulator; + codex: PerAgentAccumulator; + claude_code: PerAgentAccumulator; +} + +const CACHE_TTL_MS = 5 * 60 * 1000; + +interface CacheEntry { + value: AgentTokenStatsResponse; + expiresAt: number; +} + +export function createAgentTokenStatsService( + options: CreateAgentTokenStatsServiceOptions = {} +): AgentTokenStatsService { + const homeDir = options.homeDir ?? os.homedir(); + let cache: CacheEntry | null = null; + + return { + async getStats() { + const now = Date.now(); + if (cache && cache.expiresAt > now) { + return cache.value; + } + + const projects = new Map(); + + // Scan Pi sessions + await scanPiSessions(homeDir, projects); + + // Scan Codex sessions + await scanCodexSessions(homeDir, projects); + + // Scan Claude Code transcripts (session count only, no token data) + await scanClaudeCodeTranscripts(homeDir, projects); + + // Build response + const response = buildResponse(projects); + const validated = AgentTokenStatsResponseSchema.parse(response); + + cache = { + value: validated, + expiresAt: now + CACHE_TTL_MS + }; + + return validated; + } + }; +} + +function getOrCreateProject( + projects: Map, + projectPath: string +): ProjectAccumulator { + let acc = projects.get(projectPath); + if (!acc) { + acc = { + pi: createEmptyAccumulator(), + codex: createEmptyAccumulator(), + claude_code: createEmptyAccumulator() + }; + projects.set(projectPath, acc); + } + return acc; +} + +function createEmptyAccumulator(): PerAgentAccumulator { + return { + sessions: 0, + apiCalls: 0, + inputTokens: 0, + outputTokens: 0, + cacheReadTokens: 0, + cacheWriteTokens: 0, + reasoningTokens: 0, + totalTokens: 0, + cost: 0 + }; +} + +async function scanPiSessions( + homeDir: string, + projects: Map +): Promise { + const piSessionsDir = path.join(homeDir, ".pi", "agent", "sessions"); + if (!fs.existsSync(piSessionsDir)) { + return; + } + + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(piSessionsDir, { withFileTypes: true }); + } catch { + return; + } + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + if (!entry.name.startsWith("--") || !entry.name.endsWith("--")) continue; + + // Decode path: --mnt-d-Project-Miller-memmy-agent-- → /mnt/d/Project/Miller/memmy-agent + const projectPath = decodePiPath(entry.name); + const sessionDir = path.join(piSessionsDir, entry.name); + const acc = getOrCreateProject(projects, projectPath); + + // Find JSONL files in this session directory + let files: string[]; + try { + files = fs.readdirSync(sessionDir).filter((f) => f.endsWith(".jsonl")); + } catch { + continue; + } + + for (const file of files) { + const filePath = path.join(sessionDir, file); + await processPiSessionFile(filePath, acc.pi); + } + } +} + +function decodePiPath(encoded: string): string { + // Remove leading -- and trailing -- + const inner = encoded.slice(2, -2); + // Replace - with / + return "/" + inner.replace(/-/g, "/"); +} + +async function processPiSessionFile( + filePath: string, + acc: PerAgentAccumulator +): Promise { + let hasUsage = false; + + for await (const obj of readJsonlObjects(filePath)) { + // Look for assistant messages with usage data + if (obj.type === "assistant" && obj.usage && typeof obj.usage === "object") { + const usage = obj.usage as JsonObject; + const input = toNumber(usage.input); + const output = toNumber(usage.output); + const cacheRead = toNumber(usage.cacheRead); + const cacheWrite = toNumber(usage.cacheWrite); + const total = toNumber(usage.totalTokens); + const cost = obj.cost && typeof obj.cost === "object" + ? toNumber((obj.cost as JsonObject).total) + : 0; + + acc.inputTokens += input; + acc.outputTokens += output; + acc.cacheReadTokens += cacheRead; + acc.cacheWriteTokens += cacheWrite; + acc.totalTokens += total; + acc.cost += cost; + acc.apiCalls += 1; + hasUsage = true; + } + + // Also check for compaction/branch_summary entries with usage + if ((obj.type === "compaction" || obj.type === "branch_summary") && obj.usage && typeof obj.usage === "object") { + const usage = obj.usage as JsonObject; + const input = toNumber(usage.input); + const output = toNumber(usage.output); + const cacheRead = toNumber(usage.cacheRead); + const cacheWrite = toNumber(usage.cacheWrite); + const total = toNumber(usage.totalTokens); + const cost = obj.cost && typeof obj.cost === "object" + ? toNumber((obj.cost as JsonObject).total) + : 0; + + acc.inputTokens += input; + acc.outputTokens += output; + acc.cacheReadTokens += cacheRead; + acc.cacheWriteTokens += cacheWrite; + acc.totalTokens += total; + acc.cost += cost; + acc.apiCalls += 1; + hasUsage = true; + } + } + + if (hasUsage) { + acc.sessions += 1; + } +} + +async function scanCodexSessions( + homeDir: string, + projects: Map +): Promise { + const codexSessionsDir = path.join(homeDir, ".codex", "sessions"); + if (!fs.existsSync(codexSessionsDir)) { + return; + } + + // Structure: ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl + const dateDirs = walkDirectoryTree(codexSessionsDir, 3); // YYYY/MM/DD + + for (const dateDir of dateDirs) { + let files: string[]; + try { + files = fs.readdirSync(dateDir).filter((f) => f.startsWith("rollout-") && f.endsWith(".jsonl")); + } catch { + continue; + } + + for (const file of files) { + const filePath = path.join(dateDir, file); + await processCodexSessionFile(filePath, projects); + } + } +} + +function walkDirectoryTree(rootDir: string, depth: number): string[] { + const results: string[] = []; + + function walk(dir: string, currentDepth: number) { + if (currentDepth > depth) return; + + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const fullPath = path.join(dir, entry.name); + if (currentDepth === depth) { + results.push(fullPath); + } else { + walk(fullPath, currentDepth + 1); + } + } + } + + walk(rootDir, 1); + return results; +} + +async function processCodexSessionFile( + filePath: string, + projects: Map +): Promise { + let cwd: string | null = null; + let lastTokenCount: { + input_tokens: number; + output_tokens: number; + cached_input_tokens: number; + cache_write_input_tokens: number; + reasoning_output_tokens: number; + total_tokens: number; + } | null = null; + + for await (const obj of readJsonlObjects(filePath)) { + // Extract cwd from session_meta + if (obj.type === "session_meta" && obj.payload && typeof obj.payload === "object") { + const payload = obj.payload as JsonObject; + if (typeof payload.cwd === "string") { + cwd = payload.cwd; + } + } + + // Take the LAST token_count event (values are cumulative) + if (obj.type === "event_msg" && obj.payload && typeof obj.payload === "object") { + const payload = obj.payload as JsonObject; + if (payload.type === "token_count" && payload.info && typeof payload.info === "object") { + const info = payload.info as JsonObject; + // Structure: info.total_token_usage.{input_tokens, output_tokens, ...} + const totalUsage = info.total_token_usage; + if (totalUsage && typeof totalUsage === "object") { + const usage = totalUsage as JsonObject; + lastTokenCount = { + input_tokens: toNumber(usage.input_tokens), + output_tokens: toNumber(usage.output_tokens), + cached_input_tokens: toNumber(usage.cached_input_tokens), + cache_write_input_tokens: toNumber(usage.cache_write_input_tokens), + reasoning_output_tokens: toNumber(usage.reasoning_output_tokens), + total_tokens: toNumber(usage.total_tokens) + }; + } + } + } + } + + if (!cwd || !lastTokenCount) { + return; + } + + const acc = getOrCreateProject(projects, cwd); + acc.codex.sessions += 1; + acc.codex.apiCalls += 1; + acc.codex.inputTokens += lastTokenCount.input_tokens; + acc.codex.outputTokens += lastTokenCount.output_tokens; + acc.codex.cacheReadTokens += lastTokenCount.cached_input_tokens; + acc.codex.cacheWriteTokens += lastTokenCount.cache_write_input_tokens; + acc.codex.reasoningTokens += lastTokenCount.reasoning_output_tokens; + acc.codex.totalTokens += lastTokenCount.total_tokens; +} + +async function scanClaudeCodeTranscripts( + homeDir: string, + projects: Map +): Promise { + const claudeTranscriptsDir = path.join(homeDir, ".claude", "transcripts"); + if (!fs.existsSync(claudeTranscriptsDir)) { + return; + } + + let files: string[]; + try { + files = fs.readdirSync(claudeTranscriptsDir).filter((f) => f.endsWith(".jsonl")); + } catch { + return; + } + + // Claude Code transcripts don't have token data, but we can count sessions + // We'll attribute them to a generic "claude" project or try to extract from content + for (const file of files) { + const filePath = path.join(claudeTranscriptsDir, file); + await processClaudeCodeTranscript(filePath, projects); + } +} + +async function processClaudeCodeTranscript( + filePath: string, + projects: Map +): Promise { + let cwd: string | null = null; + + // Try to find cwd or project identifier from the transcript + for await (const obj of readJsonlObjects(filePath)) { + if (obj.cwd && typeof obj.cwd === "string") { + cwd = obj.cwd; + break; + } + } + + if (!cwd) { + // Use a generic path if we can't determine the project + cwd = "claude-code-unknown"; + } + + const acc = getOrCreateProject(projects, cwd); + acc.claude_code.sessions += 1; + // No token data available for Claude Code +} + +function buildResponse( + projects: Map +): AgentTokenStatsResponse { + const projectStats: ProjectTokenStatsDto[] = []; + + for (const [projectPath, acc] of projects) { + const agents: AgentTokenStatsDto[] = []; + + // Pi + if (acc.pi.sessions > 0 || acc.codex.sessions > 0 || acc.claude_code.sessions > 0) { + agents.push(buildAgentDto("pi", acc.pi, true)); + agents.push(buildAgentDto("codex", acc.codex, true)); + agents.push(buildAgentDto("claude_code", acc.claude_code, false)); + } + + if (agents.length === 0) continue; + + const combinedInputTokens = agents.reduce((sum, a) => sum + a.inputTokens, 0); + const combinedOutputTokens = agents.reduce((sum, a) => sum + a.outputTokens, 0); + const combinedCacheReadTokens = agents.reduce((sum, a) => sum + (a.cacheReadTokens ?? 0), 0); + const combinedTotalTokens = agents.reduce((sum, a) => sum + a.totalTokens, 0); + const estimatedCost = agents.reduce((sum, a) => sum + (a.cost ?? 0), 0); + + projectStats.push( + ProjectTokenStatsDtoSchema.parse({ + project: projectPath, + agents, + combinedInputTokens, + combinedOutputTokens, + combinedCacheReadTokens, + combinedTotalTokens, + estimatedCost: estimatedCost > 0 ? estimatedCost : undefined + }) + ); + } + + // Sort by total tokens descending + projectStats.sort((a, b) => b.combinedTotalTokens - a.combinedTotalTokens); + + return { + projects: projectStats, + scannedAt: new Date().toISOString() + }; +} + +function buildAgentDto( + agent: AgentKind, + acc: PerAgentAccumulator, + available: boolean +): AgentTokenStatsDto { + return AgentTokenStatsDtoSchema.parse({ + agent, + sessions: acc.sessions, + apiCalls: acc.apiCalls, + inputTokens: acc.inputTokens, + outputTokens: acc.outputTokens, + cacheReadTokens: acc.cacheReadTokens, + cacheWriteTokens: acc.cacheWriteTokens, + reasoningTokens: acc.reasoningTokens > 0 ? acc.reasoningTokens : undefined, + totalTokens: acc.totalTokens, + cost: acc.cost > 0 ? acc.cost : undefined, + available + }); +} + +function toNumber(value: unknown): number { + if (typeof value === "number") return value; + if (typeof value === "string") { + const parsed = parseInt(value, 10); + return Number.isFinite(parsed) ? parsed : 0; + } + return 0; +} diff --git a/App/backend/src/services/index.ts b/App/backend/src/services/index.ts index 325ad319b..be16213cb 100644 --- a/App/backend/src/services/index.ts +++ b/App/backend/src/services/index.ts @@ -36,6 +36,10 @@ import { createByokTokenUsageService, type ByokTokenUsageService } from "./byok-token-usage-service.js"; +import { + createAgentTokenStatsService, + type AgentTokenStatsService +} from "./agent-token-stats-service.js"; import { createBootstrapService, type BootstrapScenario, @@ -86,6 +90,8 @@ export interface BackendServices { asr: AsrService; /** Token quota. */ tokenQuota: TokenQuotaService; + /** Agent token stats. */ + agentTokenStats: AgentTokenStatsService; } export interface CreateBackendServicesOptions { @@ -235,7 +241,8 @@ export function createBackendServices(options: CreateBackendServicesOptions): Ba tokenQuota: createTokenQuotaService({ cloudClient: options.cloudClient, accountSessionRepository: options.appStateStore.repositories.accountSession - }) + }), + agentTokenStats: createAgentTokenStatsService() }; } diff --git a/App/frontend/desktop/src/analytics/page-view.ts b/App/frontend/desktop/src/analytics/page-view.ts index 9bebe1750..74752ca56 100644 --- a/App/frontend/desktop/src/analytics/page-view.ts +++ b/App/frontend/desktop/src/analytics/page-view.ts @@ -27,6 +27,7 @@ const MEMORY_SUB_PAGE_TITLES: Record = { "world-model": "World Model", skills: "Skills", analytics: "Analytics", + "token-stats": "Token Stats", logs: "Logs", sources: "Sources" }; diff --git a/App/frontend/desktop/src/api/agent-token-stats-client.ts b/App/frontend/desktop/src/api/agent-token-stats-client.ts new file mode 100644 index 000000000..20eaf3b1b --- /dev/null +++ b/App/frontend/desktop/src/api/agent-token-stats-client.ts @@ -0,0 +1,22 @@ +import { + AgentTokenStatsResponseSchema, + type AgentTokenStatsResponse, + type RuntimeConfig +} from "@memmy/local-api-contracts"; +import { requestJson } from "./http.js"; + +export interface AgentTokenStatsClient { + getStats(): Promise; +} + +export function createHttpAgentTokenStatsClient(config: RuntimeConfig): AgentTokenStatsClient { + return { + async getStats() { + return requestJson({ + config, + path: "/api/app/agent-token-stats", + schema: AgentTokenStatsResponseSchema + }); + } + }; +} diff --git a/App/frontend/desktop/src/api/client-types.ts b/App/frontend/desktop/src/api/client-types.ts index 14c3325ac..5e1158807 100644 --- a/App/frontend/desktop/src/api/client-types.ts +++ b/App/frontend/desktop/src/api/client-types.ts @@ -7,6 +7,10 @@ import { createHttpByokTokenUsageClient, type ByokTokenUsageClient } from "./byok-token-usage-client.js"; +import { + createHttpAgentTokenStatsClient, + type AgentTokenStatsClient +} from "./agent-token-stats-client.js"; import { createHttpChannelsClient, type ChannelsClient } from "./channels-client.js"; import { createHttpConfigClient, type ConfigClient } from "./config-client.js"; import { @@ -32,6 +36,7 @@ export interface AppClients { asr: AsrClient; memmyAgent: MemmyAgentClient; tokenQuota: TokenQuotaClient; + agentTokenStats: AgentTokenStatsClient; } export interface CreateAppClientsInput { @@ -56,6 +61,7 @@ export function createAppClients(input: CreateAppClientsInput): AppClients { byokTokenUsage: createHttpByokTokenUsageClient(input.runtimeConfig), asr: createHttpAsrClient(input.runtimeConfig), memmyAgent: createMemmyAgentClient(input.runtimeConfig.agentGateway), - tokenQuota: createHttpTokenQuotaClient(input.runtimeConfig) + tokenQuota: createHttpTokenQuotaClient(input.runtimeConfig), + agentTokenStats: createHttpAgentTokenStatsClient(input.runtimeConfig) }; } diff --git a/App/frontend/desktop/src/i18n/messages.ts b/App/frontend/desktop/src/i18n/messages.ts index 1b90ba3aa..631455799 100644 --- a/App/frontend/desktop/src/i18n/messages.ts +++ b/App/frontend/desktop/src/i18n/messages.ts @@ -731,6 +731,7 @@ export const zhCNMessages = { "memory.nav.skills": "技能", "memory.nav.analytics": "分析", "memory.nav.logs": "日志", + "memory.nav.tokenStats": "Token 用量", "memory.sourcesNav": "跨Agent接入", "memory.overview.title": "概览", "memory.overview.loading": "正在加载记忆概览", @@ -948,6 +949,26 @@ export const zhCNMessages = { "memory.analytics.trendTitle": "最近30天 - 记忆写入 vs 调用", "memory.analytics.writes": "写入", "memory.analytics.calls": "调用", + "memory.tokenStats.title": "Token 用量统计", + "memory.tokenStats.description": "Pi、Codex、Claude Code 各 Agent 的 Token 消耗", + "memory.tokenStats.loading": "正在扫描会话文件...", + "memory.tokenStats.empty": "未找到 Token 用量数据", + "memory.tokenStats.project": "项目", + "memory.tokenStats.allProjects": "所有项目", + "memory.tokenStats.pi": "Pi", + "memory.tokenStats.codex": "Codex", + "memory.tokenStats.claudeCode": "Claude Code", + "memory.tokenStats.sessions": "会话数", + "memory.tokenStats.apiCalls": "API 调用数", + "memory.tokenStats.inputTokens": "输入 Token", + "memory.tokenStats.outputTokens": "输出 Token", + "memory.tokenStats.cacheRead": "缓存读取", + "memory.tokenStats.cacheWrite": "缓存写入", + "memory.tokenStats.reasoning": "推理", + "memory.tokenStats.total": "总 Token", + "memory.tokenStats.cost": "预估费用", + "memory.tokenStats.unavailable": "Token 数据不可用", + "memory.tokenStats.combined": "合计", "memory.worldModel.title": "场域认知", "memory.worldModel.description": "助手对你工作场景的整体认知——常用工具、文件布局、反复出现的约束。", "memory.worldModel.loading": "正在加载场域认知", @@ -2096,6 +2117,7 @@ export const enUSMessages: Record = { "memory.nav.skills": "Skills", "memory.nav.analytics": "Analytics", "memory.nav.logs": "Logs", + "memory.nav.tokenStats": "Token Usage", "memory.sourcesNav": "Cross-Agent access", "memory.overview.title": "Overview", "memory.overview.loading": "Loading memory overview", @@ -2313,6 +2335,26 @@ export const enUSMessages: Record = { "memory.analytics.trendTitle": "Last 30 days - memory writes vs calls", "memory.analytics.writes": "Writes", "memory.analytics.calls": "Calls", + "memory.tokenStats.title": "Token Usage by Agent", + "memory.tokenStats.description": "Token consumption across Pi, Codex, and Claude Code sessions", + "memory.tokenStats.loading": "Scanning session files...", + "memory.tokenStats.empty": "No token usage data found", + "memory.tokenStats.project": "Project", + "memory.tokenStats.allProjects": "All Projects", + "memory.tokenStats.pi": "Pi", + "memory.tokenStats.codex": "Codex", + "memory.tokenStats.claudeCode": "Claude Code", + "memory.tokenStats.sessions": "Sessions", + "memory.tokenStats.apiCalls": "API Calls", + "memory.tokenStats.inputTokens": "Input Tokens", + "memory.tokenStats.outputTokens": "Output Tokens", + "memory.tokenStats.cacheRead": "Cache Read", + "memory.tokenStats.cacheWrite": "Cache Write", + "memory.tokenStats.reasoning": "Reasoning", + "memory.tokenStats.total": "Total Tokens", + "memory.tokenStats.cost": "Estimated Cost", + "memory.tokenStats.unavailable": "Token data unavailable", + "memory.tokenStats.combined": "Combined Totals", "memory.worldModel.title": "World models", "memory.worldModel.description": "The assistant's models of your working context: common tools, file layout, and recurring constraints.", "memory.worldModel.loading": "Loading world models", diff --git a/App/frontend/desktop/src/pages/memory-page.tsx b/App/frontend/desktop/src/pages/memory-page.tsx index 120b3807f..bfe66156c 100644 --- a/App/frontend/desktop/src/pages/memory-page.tsx +++ b/App/frontend/desktop/src/pages/memory-page.tsx @@ -16,11 +16,13 @@ import { PoliciesSubPage } from "./memory/policies-sub-page.js"; import { SkillsSubPage } from "./memory/skills-sub-page.js"; import { SourcesSubPage } from "./memory/sources-sub-page.js"; import { TasksSubPage } from "./memory/tasks-sub-page.js"; +import { TokenStatsSubPage } from "./memory/token-stats-sub-page.js"; import { WorldModelSubPage } from "./memory/world-model-sub-page.js"; import { ArrowLeft, BarChart3, BrainCircuit, + Gauge, Globe2, Layers, Link2, @@ -41,7 +43,8 @@ export type MemorySubPageId = | "skills" | "analytics" | "logs" - | "sources"; + | "sources" + | "token-stats"; interface MemoryNavSection { titleKey: MessageKey; @@ -70,6 +73,7 @@ const memoryNavSections: MemoryNavSection[] = [ titleKey: "memory.nav.insights", items: [ { id: "analytics", labelKey: "memory.nav.analytics", icon: }, + { id: "token-stats", labelKey: "memory.nav.tokenStats", icon: }, { id: "logs", labelKey: "memory.nav.logs", icon: } ] }, @@ -121,6 +125,7 @@ export function MemoryPage(props: MemoryPageProps) { "world-model": , skills: , analytics: , + "token-stats": , logs: , sources: }), @@ -295,6 +300,7 @@ function createPreviewChildByPage(t: (key: MessageKey) => string): Record{t("memory.worldModel.title")}
, skills:
{t("memory.skills.title")}
, analytics:
{t("memory.analytics.title")}
, + "token-stats":
{t("memory.tokenStats.title")}
, logs:
{t("memory.logs.title")}
, sources:
{t("memory.sourcesTitle")} {t("memory.scan")}
}; diff --git a/App/frontend/desktop/src/pages/memory/memory-prototype-icons.tsx b/App/frontend/desktop/src/pages/memory/memory-prototype-icons.tsx index a76001a3f..ea0f68895 100644 --- a/App/frontend/desktop/src/pages/memory/memory-prototype-icons.tsx +++ b/App/frontend/desktop/src/pages/memory/memory-prototype-icons.tsx @@ -550,3 +550,14 @@ export function ImagePlus(props: MemoryIconProps) { ); } + +export function Gauge(props: MemoryIconProps) { + return ( + + + + + + + ); +} diff --git a/App/frontend/desktop/src/pages/memory/token-stats-sub-page.tsx b/App/frontend/desktop/src/pages/memory/token-stats-sub-page.tsx new file mode 100644 index 000000000..1cc26b636 --- /dev/null +++ b/App/frontend/desktop/src/pages/memory/token-stats-sub-page.tsx @@ -0,0 +1,250 @@ +import { useEffect, useState } from "react"; +import type { + AgentTokenStatsDto, + AgentTokenStatsResponse +} from "@memmy/local-api-contracts"; +import type { AgentTokenStatsClient } from "../../api/agent-token-stats-client.js"; +import { useTranslation } from "../../i18n/use-translation.js"; +import { Gauge } from "./memory-prototype-icons.js"; +import { type RemoteData, toErrorMessage } from "./remote-state.js"; + +export interface TokenStatsSubPageProps { + client: AgentTokenStatsClient | null; +} + +export function TokenStatsSubPage(props: TokenStatsSubPageProps) { + const { t } = useTranslation(); + const [state, setState] = useState>({ status: "loading" }); + const [selectedProject, setSelectedProject] = useState(null); + + useEffect(() => { + if (!props.client) { + setState({ status: "error", message: t("memory.clientNotReady") }); + return; + } + + let active = true; + setState({ status: "loading" }); + + props.client + .getStats() + .then((data) => { + if (!active) return; + setState({ status: "ready", data }); + // Auto-select first project if none selected + if (!selectedProject && data.projects.length > 0 && data.projects[0]) { + setSelectedProject(data.projects[0].project); + } + }) + .catch((error: unknown) => { + if (!active) return; + setState({ status: "error", message: toErrorMessage(error) }); + }); + + return () => { + active = false; + }; + }, [props.client, t, selectedProject]); + + if (state.status === "loading") { + return ( +
+
+

+ + {t("memory.tokenStats.title")} +

+

{t("memory.tokenStats.description")}

+
+
{t("memory.tokenStats.loading")}
+
+ ); + } + + if (state.status === "error") { + return ( +
+
+

+ + {t("memory.tokenStats.title")} +

+

{t("memory.tokenStats.description")}

+
+
{state.message}
+
+ ); + } + + const { data } = state; + const projects = data.projects; + + if (projects.length === 0) { + return ( +
+
+

+ + {t("memory.tokenStats.title")} +

+

{t("memory.tokenStats.description")}

+
+
{t("memory.tokenStats.empty")}
+
+ ); + } + + // Find selected project or use first + const currentProject = projects.find((p) => p.project === selectedProject) ?? projects[0]; + + if (!currentProject) { + return ( +
+
+

+ + {t("memory.tokenStats.title")} +

+

{t("memory.tokenStats.description")}

+
+
{t("memory.tokenStats.empty")}
+
+ ); + } + + return ( +
+
+

+ + {t("memory.tokenStats.title")} +

+

{t("memory.tokenStats.description")}

+
+ + {/* Project selector */} + {projects.length > 1 && ( +
+ + +
+ )} + + {/* Agent cards */} +
+ {currentProject.agents.map((agent) => ( + + ))} +
+ + {/* Combined totals */} +
+

{t("memory.tokenStats.combined")}

+
+ + + + + {currentProject.estimatedCost !== undefined && ( + + )} +
+
+ + {/* Scan timestamp */} +
+ Scanned at: {new Date(data.scannedAt).toLocaleString()} +
+
+ ); +} + +interface AgentCardProps { + agent: AgentTokenStatsDto; +} + +function AgentCard(props: AgentCardProps) { + const { t } = useTranslation(); + const { agent } = props; + + const agentLabel = + agent.agent === "pi" + ? t("memory.tokenStats.pi") + : agent.agent === "codex" + ? t("memory.tokenStats.codex") + : t("memory.tokenStats.claudeCode"); + + const icon = ; + + return ( +
+
+ {icon} +

{agentLabel}

+
+ + {!agent.available ? ( +
{t("memory.tokenStats.unavailable")}
+ ) : ( + <> +
+ + {t("memory.tokenStats.sessions")}: {agent.sessions} + + + {t("memory.tokenStats.apiCalls")}: {agent.apiCalls} + +
+ +
+ + + + + {agent.reasoningTokens !== undefined && agent.reasoningTokens > 0 && ( + + )} + + {agent.cost !== undefined && agent.cost > 0 && ( + + )} +
+ + )} +
+ ); +} + +interface StatItemProps { + label: string; + value: number | string; + highlight?: boolean; +} + +function StatItem(props: StatItemProps) { + const displayValue = typeof props.value === "number" ? props.value.toLocaleString() : props.value; + + return ( +
+
{props.label}
+
+ {displayValue} +
+
+ ); +} diff --git a/Memory/src/server/http.ts b/Memory/src/server/http.ts index 790b376e9..e843ae8a8 100644 --- a/Memory/src/server/http.ts +++ b/Memory/src/server/http.ts @@ -19,6 +19,7 @@ import type { } from "../types.js"; import { DEFAULT_NAMESPACE_SOURCE } from "../types.js"; import { MemoryService } from "../service/memory-service.js"; +import { createAgentTokenStatsService } from "../service/agent-token-stats-service.js"; import { normalizeNamespace } from "../service/namespace/namespace-scope.js"; import { MemoryServiceError, statusForCode } from "../utils/error.js"; import { @@ -34,6 +35,7 @@ import { const logger = createMemoryLogger("http"); const workerLogger = createMemoryLogger("worker"); +const agentTokenStatsService = createAgentTokenStatsService(); export const API_ROUTES = [ "GET /api/v1/health", @@ -51,6 +53,7 @@ export const API_ROUTES = [ "POST /api/v1/memory/processing/status", "POST /api/v1/memory/:id/processing/retry", "POST /api/v1/memory/:id/quality", + "POST /api/v1/memory/:id/edit", "POST /api/v1/memory/:id/archive", "POST /api/v1/memory/:id/promote", "GET /api/v1/panel/review/candidates", @@ -77,7 +80,8 @@ export const API_ROUTES = [ "GET /api/v1/panel/activity", "GET /api/v1/panel/items", "GET /api/v1/panel/tasks", - "DELETE /api/v1/panel/tasks/:id" + "DELETE /api/v1/panel/tasks/:id", + "GET /api/v1/agent-token-stats" ] as const; export interface MemoryHttpServerOptions { @@ -719,6 +723,11 @@ async function routeRequest( }); } + if (method === "GET" && path === "/api/v1/agent-token-stats") { + requirePanelRead(principal); + return agentTokenStatsService.getStats(); + } + if (method === "GET" && path === "/api/v1/panel/activity") { requirePanelRead(principal); return service.serviceLogs({ @@ -833,6 +842,33 @@ async function routeRequest( return service.rateMemory(decodeMatchSegment(memoryQuality, 1), request.useful, request); } + const memoryEdit = match(path, /^\/api\/v1\/memory\/([^/]+)\/edit$/); + if (method === "POST" && memoryEdit) { + requireMemoryWrite(principal); + const request = envelopeWithPrincipal(asObject(body, "memory.edit"), principal) as MemoryGovernanceRequest & { + title?: unknown; + content?: unknown; + tags?: unknown; + }; + if (typeof request.title !== "string" || !request.title.trim()) { + throw new MemoryServiceError("invalid_argument", "memory.edit title is required"); + } + if (typeof request.content !== "string" || !request.content.trim()) { + throw new MemoryServiceError("invalid_argument", "memory.edit content is required"); + } + if (!Array.isArray(request.tags) || request.tags.some((tag) => typeof tag !== "string")) { + throw new MemoryServiceError("invalid_argument", "memory.edit tags must be an array of strings"); + } + const result = service.editMemory(decodeMatchSegment(memoryEdit, 1), { + ...request, + title: request.title.trim(), + content: request.content.trim(), + tags: request.tags.map((tag) => String(tag).trim()).filter(Boolean) + }); + if (result.embeddingJobId) autoWorker.schedule(); + return result; + } + const memoryArchive = match(path, /^\/api\/v1\/memory\/([^/]+)\/archive$/); if (method === "POST" && memoryArchive) { requireMemoryWrite(principal); diff --git a/Memory/src/service/agent-token-stats-service.ts b/Memory/src/service/agent-token-stats-service.ts new file mode 100644 index 000000000..89f797664 --- /dev/null +++ b/Memory/src/service/agent-token-stats-service.ts @@ -0,0 +1,420 @@ +import { createReadStream } from "node:fs"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { createInterface } from "node:readline"; + +export type AgentKind = "pi" | "codex" | "claude_code"; + +export interface AgentTokenStats { + agent: AgentKind; + sessions: number; + apiCalls: number; + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + cacheWriteTokens: number; + reasoningTokens?: number; + totalTokens: number; + cost?: number; + available: boolean; +} + +export interface ProjectTokenStats { + project: string; + agents: AgentTokenStats[]; + combinedInputTokens: number; + combinedOutputTokens: number; + combinedCacheReadTokens: number; + combinedTotalTokens: number; + estimatedCost?: number; +} + +export interface MonthlyAgentTokenStats { + month: string; + projects: ProjectTokenStats[]; +} + +export interface AgentTokenStatsResponse { + projects: ProjectTokenStats[]; + monthly: MonthlyAgentTokenStats[]; + scannedAt: string; +} + +export interface AgentTokenStatsService { + getStats(): Promise; +} + +export interface CreateAgentTokenStatsServiceOptions { + homeDir?: string; + cacheTtlMs?: number; +} + +interface PerAgentAccumulator { + sessions: number; + apiCalls: number; + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + cacheWriteTokens: number; + reasoningTokens: number; + totalTokens: number; + cost: number; +} + +interface ProjectAccumulator { + pi: PerAgentAccumulator; + codex: PerAgentAccumulator; + claude_code: PerAgentAccumulator; +} + +interface UsageRecord { + month: string; + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + cacheWriteTokens: number; + reasoningTokens: number; + totalTokens: number; + cost: number; +} + +interface ScanState { + projects: Map; + monthlyProjects: Map>; +} + +const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000; + +export function createAgentTokenStatsService( + options: CreateAgentTokenStatsServiceOptions = {} +): AgentTokenStatsService { + const homeDir = options.homeDir ?? os.homedir(); + const cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS; + let cache: { expiresAt: number; value: AgentTokenStatsResponse } | undefined; + let pending: Promise | undefined; + + return { + async getStats() { + const now = Date.now(); + if (cache && cache.expiresAt > now) return cache.value; + if (pending) return pending; + + pending = scanAgentTokenStats(homeDir); + try { + const value = await pending; + cache = { expiresAt: Date.now() + cacheTtlMs, value }; + return value; + } finally { + pending = undefined; + } + } + }; +} + +async function scanAgentTokenStats(homeDir: string): Promise { + const state: ScanState = { + projects: new Map(), + monthlyProjects: new Map() + }; + await scanPiSessions(homeDir, state); + await scanCodexSessions(homeDir, state); + await scanClaudeCodeTranscripts(homeDir, state); + + return { + projects: buildProjectStats(state.projects), + monthly: [...state.monthlyProjects.entries()] + .sort(([left], [right]) => right.localeCompare(left)) + .map(([month, projects]) => ({ month, projects: buildProjectStats(projects) })), + scannedAt: new Date().toISOString() + }; +} + +function createEmptyAccumulator(): PerAgentAccumulator { + return { + sessions: 0, + apiCalls: 0, + inputTokens: 0, + outputTokens: 0, + cacheReadTokens: 0, + cacheWriteTokens: 0, + reasoningTokens: 0, + totalTokens: 0, + cost: 0 + }; +} + +function getOrCreateProject( + projects: Map, + projectPath: string +): ProjectAccumulator { + let accumulator = projects.get(projectPath); + if (!accumulator) { + accumulator = { + pi: createEmptyAccumulator(), + codex: createEmptyAccumulator(), + claude_code: createEmptyAccumulator() + }; + projects.set(projectPath, accumulator); + } + return accumulator; +} + +function getMonthlyProject(state: ScanState, month: string, projectPath: string): ProjectAccumulator { + let projects = state.monthlyProjects.get(month); + if (!projects) { + projects = new Map(); + state.monthlyProjects.set(month, projects); + } + return getOrCreateProject(projects, projectPath); +} + +async function* readJsonlObjects(filePath: string): AsyncIterable> { + const stream = createReadStream(filePath, { encoding: "utf8" }); + const lines = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY }); + try { + for await (const line of lines) { + if (!line.trim()) continue; + try { + const value = JSON.parse(line); + if (value && typeof value === "object" && !Array.isArray(value)) { + yield value as Record; + } + } catch { + // Session files can contain an incomplete final line after an interrupted write. + } + } + } finally { + lines.close(); + stream.destroy(); + } +} + +async function scanPiSessions(homeDir: string, state: ScanState): Promise { + const root = path.join(homeDir, ".pi", "agent", "sessions"); + for (const entry of readDirectories(root)) { + if (!entry.name.startsWith("--") || !entry.name.endsWith("--")) continue; + const projectPath = decodePiPath(entry.name); + for (const file of readJsonlFiles(path.join(root, entry.name))) { + await processPiSessionFile(file, projectPath, state); + } + } +} + +async function processPiSessionFile( + filePath: string, + projectPath: string, + state: ScanState +): Promise { + const usedMonths = new Set(); + let hasUsage = false; + for await (const object of readJsonlObjects(filePath)) { + const usage = piUsageFromObject(object, filePath); + if (!usage) continue; + addUsage(getOrCreateProject(state.projects, projectPath).pi, usage); + addUsage(getMonthlyProject(state, usage.month, projectPath).pi, usage); + usedMonths.add(usage.month); + hasUsage = true; + } + if (!hasUsage) return; + getOrCreateProject(state.projects, projectPath).pi.sessions += 1; + for (const month of usedMonths) { + getMonthlyProject(state, month, projectPath).pi.sessions += 1; + } +} + +function piUsageFromObject(object: Record, filePath: string): UsageRecord | undefined { + let usage: Record | undefined; + if ( + (object.type === "assistant" || object.type === "compaction" || object.type === "branch_summary") + && isRecord(object.usage) + ) { + usage = object.usage; + } else if (object.type === "message" && isRecord(object.message)) { + const message = object.message; + if (message.role === "assistant" && isRecord(message.usage)) usage = message.usage; + } + if (!usage) return undefined; + + const cost = isRecord(usage.cost) + ? toNumber(usage.cost.total) + : isRecord(object.cost) + ? toNumber(object.cost.total) + : 0; + return { + month: monthFromTimestamp(object.timestamp, filePath), + inputTokens: toNumber(usage.input), + outputTokens: toNumber(usage.output), + cacheReadTokens: toNumber(usage.cacheRead), + cacheWriteTokens: toNumber(usage.cacheWrite), + reasoningTokens: toNumber(usage.reasoning), + totalTokens: toNumber(usage.totalTokens), + cost + }; +} + +async function scanCodexSessions(homeDir: string, state: ScanState): Promise { + const root = path.join(homeDir, ".codex", "sessions"); + for (const dateDirectory of walkDirectoryTree(root, 3)) { + for (const file of readJsonlFiles(dateDirectory, "rollout-")) { + await processCodexSessionFile(file, state); + } + } +} + +async function processCodexSessionFile(filePath: string, state: ScanState): Promise { + let cwd: string | undefined; + let lastUsage: UsageRecord | undefined; + for await (const object of readJsonlObjects(filePath)) { + if (object.type === "session_meta" && isRecord(object.payload) && typeof object.payload.cwd === "string") { + cwd = object.payload.cwd; + } + if (object.type !== "event_msg" || !isRecord(object.payload)) continue; + const payload = object.payload; + if (payload.type !== "token_count" || !isRecord(payload.info) || !isRecord(payload.info.total_token_usage)) continue; + const usage = payload.info.total_token_usage; + lastUsage = { + month: monthFromTimestamp(object.timestamp, filePath), + inputTokens: toNumber(usage.input_tokens), + outputTokens: toNumber(usage.output_tokens), + cacheReadTokens: toNumber(usage.cached_input_tokens), + cacheWriteTokens: toNumber(usage.cache_write_input_tokens), + reasoningTokens: toNumber(usage.reasoning_output_tokens), + totalTokens: toNumber(usage.total_tokens), + cost: 0 + }; + } + if (!cwd || !lastUsage) return; + const overall = getOrCreateProject(state.projects, cwd).codex; + const monthly = getMonthlyProject(state, lastUsage.month, cwd).codex; + addUsage(overall, lastUsage); + addUsage(monthly, lastUsage); + overall.sessions += 1; + monthly.sessions += 1; +} + +async function scanClaudeCodeTranscripts(homeDir: string, state: ScanState): Promise { + const root = path.join(homeDir, ".claude", "transcripts"); + for (const file of readJsonlFiles(root)) { + let cwd: string | undefined; + let month = monthFromTimestamp(undefined, file); + for await (const object of readJsonlObjects(file)) { + if (typeof object.cwd === "string") cwd = object.cwd; + if (object.timestamp !== undefined) month = monthFromTimestamp(object.timestamp, file); + if (cwd) break; + } + const projectPath = cwd ?? "claude-code-unknown"; + getOrCreateProject(state.projects, projectPath).claude_code.sessions += 1; + getMonthlyProject(state, month, projectPath).claude_code.sessions += 1; + } +} + +function addUsage(accumulator: PerAgentAccumulator, usage: UsageRecord): void { + accumulator.apiCalls += 1; + accumulator.inputTokens += usage.inputTokens; + accumulator.outputTokens += usage.outputTokens; + accumulator.cacheReadTokens += usage.cacheReadTokens; + accumulator.cacheWriteTokens += usage.cacheWriteTokens; + accumulator.reasoningTokens += usage.reasoningTokens; + accumulator.totalTokens += usage.totalTokens; + accumulator.cost += usage.cost; +} + +function buildProjectStats(projects: Map): ProjectTokenStats[] { + const result = [...projects.entries()].map(([project, accumulator]) => { + const agents = [ + buildAgentStats("pi", accumulator.pi, true), + buildAgentStats("codex", accumulator.codex, true), + buildAgentStats("claude_code", accumulator.claude_code, false) + ]; + const estimatedCost = agents.reduce((sum, agent) => sum + (agent.cost ?? 0), 0); + return { + project, + agents, + combinedInputTokens: agents.reduce((sum, agent) => sum + agent.inputTokens, 0), + combinedOutputTokens: agents.reduce((sum, agent) => sum + agent.outputTokens, 0), + combinedCacheReadTokens: agents.reduce((sum, agent) => sum + agent.cacheReadTokens, 0), + combinedTotalTokens: agents.reduce((sum, agent) => sum + agent.totalTokens, 0), + estimatedCost: estimatedCost > 0 ? estimatedCost : undefined + }; + }); + return result.sort((left, right) => right.combinedTotalTokens - left.combinedTotalTokens); +} + +function buildAgentStats(agent: AgentKind, accumulator: PerAgentAccumulator, available: boolean): AgentTokenStats { + return { + agent, + sessions: accumulator.sessions, + apiCalls: accumulator.apiCalls, + inputTokens: accumulator.inputTokens, + outputTokens: accumulator.outputTokens, + cacheReadTokens: accumulator.cacheReadTokens, + cacheWriteTokens: accumulator.cacheWriteTokens, + reasoningTokens: accumulator.reasoningTokens || undefined, + totalTokens: accumulator.totalTokens, + cost: accumulator.cost || undefined, + available + }; +} + +function monthFromTimestamp(timestamp: unknown, filePath: string): string { + if (typeof timestamp === "string" || typeof timestamp === "number") { + const date = new Date(timestamp); + if (!Number.isNaN(date.getTime())) return date.toISOString().slice(0, 7); + } + const match = path.basename(filePath).match(/(20\d{2})[-_/](0[1-9]|1[0-2])/); + if (match) return `${match[1]}-${match[2]}`; + try { + return fs.statSync(filePath).mtime.toISOString().slice(0, 7); + } catch { + return "unknown"; + } +} + +function decodePiPath(encoded: string): string { + return "/" + encoded.slice(2, -2).replace(/-/g, "/"); +} + +function readDirectories(root: string): fs.Dirent[] { + try { + return fs.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()); + } catch { + return []; + } +} + +function readJsonlFiles(root: string, prefix = ""): string[] { + try { + return fs.readdirSync(root, { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name.startsWith(prefix) && entry.name.endsWith(".jsonl")) + .map((entry) => path.join(root, entry.name)); + } catch { + return []; + } +} + +function walkDirectoryTree(root: string, depth: number): string[] { + const results: string[] = []; + function walk(directory: string, currentDepth: number): void { + for (const entry of readDirectories(directory)) { + const fullPath = path.join(directory, entry.name); + if (currentDepth === depth) results.push(fullPath); + else walk(fullPath, currentDepth + 1); + } + } + walk(root, 1); + return results; +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function toNumber(value: unknown): number { + if (typeof value === "number") return Number.isFinite(value) ? value : 0; + if (typeof value === "string") { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; + } + return 0; +} diff --git a/Memory/src/service/memory-service.ts b/Memory/src/service/memory-service.ts index 020085f69..038fa7a0b 100644 --- a/Memory/src/service/memory-service.ts +++ b/Memory/src/service/memory-service.ts @@ -1928,6 +1928,73 @@ export class MemoryService { return { ok: true, id: updated.id, useful, changeSeq, auditId: audit.id, serverTime: at }; } + editMemory(id: string, request: MemoryGovernanceRequest & { + title: string; + content: string; + tags: string[]; + }) { + this.assertMemoryAddEnabled(); + const memory = this.requireExistingMemory(id); + this.assertMemoryInScope(memory, request.namespace); + const at = nowIso(); + const before = memory; + const title = request.title.trim(); + const content = request.content.trim(); + const tags = uniq(request.tags.map((tag) => tag.trim()).filter(Boolean)); + const updated = this.repos.memories.update({ + ...memory, + memoryValue: content, + tags, + info: { ...memory.info, title, summary: content, tags }, + properties: { + ...memory.properties, + tags, + info: { ...memory.properties.info, title, summary: content, tags }, + internal_info: { ...memory.properties.internal_info, title, summary: content } + }, + contentHash: stableHash(content), + updatedAt: at + }); + const changeSeq = this.repos.runtime.appendChange({ + memoryId: updated.id, + namespaceId: namespaceIdFromMemory(updated), + kind: kindFromMemory(updated), + op: "updated", + entityId: updated.id, + userId: updated.userId, + changeType: "content_edit", + before, + after: updated, + source: "panel.edit", + createdAt: at + }); + const audit = this.repos.runtime.insertAudit({ + userId: updated.userId, + sessionId: updated.sessionId, + actor: request.namespace ? { ...request.namespace } : {}, + action: "edit_memory", + targetKind: kindFromMemory(updated), + targetId: updated.id, + before, + after: updated, + meta: { reason: request.reason, source: "context_pack" }, + createdAt: at + }); + let embeddingJobId: string | undefined; + if (this.config.algorithm.capture.embedAfterCapture) { + this.repos.memories.deleteVector(updated.id, updated.memoryLayer === "L1" ? "vec_summary" : "vec"); + embeddingJobId = this.workerHandlers.enqueueJob({ + jobType: "embedding", + userId: updated.userId, + sessionId: updated.sessionId, + targetMemoryId: updated.id, + payload: { source: "panel.edit", changeSeq, contentHash: updated.contentHash }, + createdAt: at + }).id; + } + return { ok: true, id: updated.id, version: updated.version, changeSeq, auditId: audit.id, embeddingJobId, serverTime: at }; + } + reviewCandidates(request: RequestEnvelope & { layer?: MemoryLayer; limit?: number } = {}) { const limit = Math.max(1, Math.min(request.limit ?? 100, 1000)); const memories = this.repos.memories.list({ diff --git a/Memory/src/viewer/static.ts b/Memory/src/viewer/static.ts index d23c52f1f..788d4fcbe 100644 --- a/Memory/src/viewer/static.ts +++ b/Memory/src/viewer/static.ts @@ -205,6 +205,17 @@ export function memoryPanelHtml(): string { .queue-item { padding: 10px; background: var(--surface-soft); border-radius: 6px; } .queue-item strong, .queue-item span { display: block; } .queue-item span { color: var(--muted); font-size: 10px; margin-top: 4px; } + .token-chart { display: flex; align-items: stretch; gap: 12px; min-height: 300px; padding: 24px 20px 12px; overflow-x: auto; border-top: 1px solid var(--line); } + .token-chart-month { display: flex; flex: 0 0 92px; min-height: 260px; flex-direction: column; justify-content: flex-end; align-items: center; gap: 8px; } + .token-chart-bars { display: flex; align-items: flex-end; justify-content: center; gap: 5px; height: 220px; width: 100%; border-bottom: 1px solid var(--line-strong); } + .token-chart-bar { width: 20px; min-height: 2px; border-radius: 4px 4px 0 0; cursor: help; transition: opacity .15s ease; } + .token-chart-bar:hover { opacity: .72; } + .token-chart-bar.pi { background: var(--accent); } + .token-chart-bar.codex { background: var(--blue); } + .token-chart-bar.claude_code { background: var(--violet); } + .token-chart-label { color: var(--muted); font-size: 11px; white-space: nowrap; } + .token-chart-value { font-size: 10px; color: var(--ink-secondary); white-space: nowrap; } + .token-chart-empty { width: 100%; display: grid; place-items: center; color: var(--muted); } .pipeline { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; } .pipeline-stage { padding: 11px; border: 1px solid var(--line); border-radius: 7px; background: var(--surface-soft); } .pipeline-stage strong, .pipeline-stage span { display: block; } @@ -262,6 +273,47 @@ export function memoryPanelHtml(): string { .review-card p { margin: 9px 0; color: var(--ink-secondary); line-height: 1.55; white-space: pre-wrap; } .review-card-meta { display: flex; gap: 6px; flex-wrap: wrap; } .review-card-actions { justify-content: flex-end; margin-top: 10px; } + .context-pack-head { align-items: flex-start; } + .context-pack-head > div:first-child { min-width: 0; } + .context-pack-controls { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; } + .context-pack-controls select { width: min(260px, 100%); } + .context-pack-tabs { display: inline-flex; gap: 2px; padding: 3px; border: 1px solid var(--line); border-radius: 7px; background: var(--surface-soft); } + .context-pack-tab { min-height: 28px; padding: 0 10px; border: 0; background: transparent; color: var(--muted); } + .context-pack-tab.active { background: var(--surface); color: var(--ink); box-shadow: 0 1px 3px rgba(20, 35, 30, .08); } + .context-pack-view { min-height: 260px; max-height: 520px; overflow: auto; } + .context-outline { display: grid; gap: 16px; } + .context-outline-pack { display: grid; gap: 12px; } + .context-outline-pack + .context-outline-pack { padding-top: 16px; border-top: 1px solid var(--line); } + .context-outline-title { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; } + .context-outline-title span { color: var(--muted); font-size: 10px; } + .context-outline-sections { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; } + .context-outline-section { min-width: 0; border-left: 2px solid var(--line-strong); padding-left: 10px; } + .context-outline-section h4 { margin: 0 0 6px; font-size: 11px; color: var(--muted); } + .context-item { width: 100%; min-height: 0; padding: 7px 8px; border: 0; border-radius: 5px; background: transparent; text-align: left; } + .context-item:hover { background: var(--surface-hover); } + .context-item strong, .context-item span { display: block; overflow-wrap: anywhere; } + .context-item span { margin-top: 3px; color: var(--muted); font-size: 10px; } + .context-graph { min-width: 760px; padding: 8px 4px; } + .context-graph-pack { display: grid; grid-template-columns: minmax(150px, .7fr) minmax(170px, .8fr) minmax(320px, 2fr); gap: 28px; align-items: center; padding: 18px 0; } + .context-graph-pack + .context-graph-pack { border-top: 1px solid var(--line); } + .graph-root, .graph-section, .graph-node { position: relative; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); } + .graph-root { padding: 12px; border-color: var(--accent); background: var(--accent-soft); font-weight: 750; } + .graph-branches, .graph-leaves { display: grid; gap: 8px; } + .graph-section { padding: 8px 10px; color: var(--ink-secondary); font-size: 11px; } + .graph-node { width: 100%; min-height: 34px; padding: 7px 9px; text-align: left; } + .graph-root::after, .graph-section::before, .graph-section::after, .graph-node::before { content: ""; position: absolute; top: 50%; height: 1px; background: var(--line-strong); } + .graph-root::after { left: 100%; width: 28px; } + .graph-section::before, .graph-node::before { right: 100%; width: 28px; } + .graph-section::after { left: 100%; width: 28px; } + .context-markdown { max-height: none; min-height: 260px; } + .dialog-screen { position: fixed; inset: 0; z-index: 45; display: grid; place-items: center; padding: 18px; background: rgba(10, 18, 15, .48); } + .dialog { width: min(680px, 100%); max-height: min(760px, calc(100vh - 36px)); overflow: auto; border: 1px solid var(--line); border-radius: 8px; background: var(--surface-raised); box-shadow: var(--shadow); } + .dialog-head, .dialog-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 11px 14px; border-bottom: 1px solid var(--line); } + .dialog-body { display: grid; gap: 12px; padding: 14px; } + .dialog-actions { justify-content: flex-end; border-top: 1px solid var(--line); border-bottom: 0; } + .dialog label { display: grid; gap: 6px; font-weight: 700; } + .dialog textarea { width: 100%; min-height: 220px; resize: vertical; padding: 10px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--ink); font: 12px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } + .context-detail-body { white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.6; } .auth-screen { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; padding: 18px; background: color-mix(in srgb, var(--canvas) 92%, transparent); backdrop-filter: blur(14px); } .auth-dialog { width: min(420px, 100%); border: 1px solid var(--line); border-radius: 8px; background: var(--surface-raised); box-shadow: var(--shadow); padding: 20px; } .auth-dialog .brand { padding: 0; margin-bottom: 18px; } @@ -289,6 +341,8 @@ export function memoryPanelHtml(): string { .toolbar { grid-template-columns: 1fr 1fr; } .toolbar input:first-child { grid-column: 1 / -1; } .system-grid { grid-template-columns: 1fr; } + .context-outline-sections { grid-template-columns: 1fr; } + .context-pack-head { display: grid; } } @media (max-width: 560px) { .topbar-actions button:not(.icon-button) { padding: 0 8px; } @@ -320,6 +374,7 @@ export function memoryPanelHtml(): string { + @@ -375,9 +430,9 @@ export function memoryPanelHtml(): string {

待审核提炼

-
-

项目上下文包

-
+
+

项目上下文包

由原始记忆实时生成
+

来源与项目隔离审计

@@ -442,6 +497,27 @@ export function memoryPanelHtml(): string {
+
+

Agent Token 用量统计

Pi、Codex、Claude Code 各 Agent 的 Token 消耗

+
+
+

选择项目

按月 Token 用量
+
+
+
+

月度用量

+
+
+
+

项目累计

+
+
+
+
+
+
+
+

Agent 来源与项目隔离审计

检查缺失 workspace、未知来源、旧来源标签与跨项目泄漏风险

@@ -463,6 +539,17 @@ export function memoryPanelHtml(): string {
+
@@ -2228,12 +2281,36 @@ export function HomePage() { />
+
+ {contextPackProject ? ( + + ) : null} +

{t("home.notice")}

void selectMedia(event)} />
)} + {contextPackProject ? ( + setOpenContextPackProjectId(null)} + /> + ) : null} ); } diff --git a/App/frontend/desktop/src/pages/memory/tests/fixtures.ts b/App/frontend/desktop/src/pages/memory/tests/fixtures.ts index 27abc3880..f3ed16d60 100644 --- a/App/frontend/desktop/src/pages/memory/tests/fixtures.ts +++ b/App/frontend/desktop/src/pages/memory/tests/fixtures.ts @@ -45,6 +45,8 @@ export function createMemoryRuntimeClientStub(overrides: Partial { "Cursor", "Claude Code", "Codex", + "Pi", "OpenCode", "OpenClaw", "Hermes", diff --git a/App/frontend/desktop/src/pages/memory/tests/memory-runtime-fixtures.ts b/App/frontend/desktop/src/pages/memory/tests/memory-runtime-fixtures.ts index 9b65d30a6..544cf3abb 100644 --- a/App/frontend/desktop/src/pages/memory/tests/memory-runtime-fixtures.ts +++ b/App/frontend/desktop/src/pages/memory/tests/memory-runtime-fixtures.ts @@ -397,6 +397,28 @@ export function createMockMemoryRuntimeClient(): MemoryRuntimeClient { return { item: detail.item, refs: {}, version: detail.version, etag: detail.etag }; }, + async getMemoryHistory(id) { + const detail = findMemoryDetail(id); + return { + id, + currentVersion: detail.version, + items: [{ seq: detail.version, version: detail.version, changeType: "created", source: "fixture", createdAt: detail.item.createdAt, after: {} }], + serverTime: now + }; + }, + + async restoreMemory(id, targetVersion, input) { + return { + ok: true, + id, + version: input.version + 1, + restoredVersion: targetVersion, + changeSeq: 47, + auditId: "audit-restore-fixture", + serverTime: now + }; + }, + async deleteMemory(id): Promise { return { ok: true, @@ -507,6 +529,19 @@ export function createMockMemoryRuntimeClient(): MemoryRuntimeClient { async getPanelAnalysis(): Promise { return mockPanelAnalysis; }, + async getProjectContextPack(projectId) { + return { + namespace: { projectId }, + conventions: [], + commands: [], + architectureFacts: [], + recentTasks: [], + userPreferences: [], + graph: { nodes: [], edges: [] }, + markdown: `# Project Memory Pack: ${projectId}`, + generatedAt: now + }; + }, async listPanelItems(input): Promise { return filterMemoryItems(input); }, diff --git a/App/frontend/desktop/src/pages/project-context-pack-dialog.tsx b/App/frontend/desktop/src/pages/project-context-pack-dialog.tsx new file mode 100644 index 000000000..4ce052726 --- /dev/null +++ b/App/frontend/desktop/src/pages/project-context-pack-dialog.tsx @@ -0,0 +1,591 @@ +import { useEffect, useState, type ReactNode } from "react"; +import type { GetMemoryOutput, MemoryHistoryOutput, MemoryListItem, ProjectContextPackOutput } from "@memmy/local-api-contracts"; +import { ArrowLeft, BookOpen, Check, Copy, Network, RefreshCw, X } from "lucide-react"; +import type { MemoryRuntimeClient } from "../api/memory-runtime-client.js"; +import { Modal } from "../components/modal.js"; +import type { MessageKey } from "../i18n/messages.js"; +import { ContextPackRelationGraph } from "./context-pack-relation-graph.js"; + +type Translate = (key: MessageKey) => string; + +type ContextPackState = + | { status: "loading"; pack: null } + | { status: "ready"; pack: ProjectContextPackOutput } + | { status: "error"; pack: null }; + +type MemoryDetailState = + | { status: "loading"; detail: null } + | { status: "ready"; detail: GetMemoryOutput } + | { status: "error"; detail: null }; + +type MemoryHistoryState = + | { status: "loading"; history: null } + | { status: "ready"; history: MemoryHistoryOutput } + | { status: "error"; history: null }; + +type RestoreState = + | { status: "idle" } + | { status: "restoring"; targetVersion: number } + | { status: "success"; targetVersion: number } + | { status: "error"; targetVersion: number }; + +type SelectedMemory = { projectId: string; memoryId: string }; + +const sections = [ + ["conventions", "home.contextPack.conventions"], + ["commands", "home.contextPack.commands"], + ["architectureFacts", "home.contextPack.architectureFacts"], + ["recentTasks", "home.contextPack.recentTasks"], + ["userPreferences", "home.contextPack.userPreferences"] +] as const; + +export function ProjectContextPackDialog(props: { + open: boolean; + projectId: string; + projectName: string; + client: Pick | null; + t: Translate; + onClose: () => void; +}) { + const [requestKey, setRequestKey] = useState(0); + const [state, setState] = useState({ status: "loading", pack: null }); + const [copied, setCopied] = useState(false); + const [selectedMemory, setSelectedMemory] = useState(null); + const [detailState, setDetailState] = useState(null); + const [detailRequestKey, setDetailRequestKey] = useState(0); + const [graphOpen, setGraphOpen] = useState(false); + const [historyState, setHistoryState] = useState(null); + const [pendingRestoreVersion, setPendingRestoreVersion] = useState(null); + const [restoreState, setRestoreState] = useState({ status: "idle" }); + + useEffect(() => { + if (!props.open || !props.client) return undefined; + let active = true; + setState({ status: "loading", pack: null }); + setCopied(false); + void props.client.getProjectContextPack(props.projectId) + .then((pack) => { + if (active) setState({ status: "ready", pack }); + }) + .catch(() => { + if (active) setState({ status: "error", pack: null }); + }); + return () => { active = false; }; + }, [props.client, props.open, props.projectId, requestKey]); + + const selectedMemoryId = selectedMemory?.projectId === props.projectId + ? selectedMemory.memoryId + : null; + + useEffect(() => { + if (!props.open || !props.client || !selectedMemoryId) { + setDetailState(null); + return undefined; + } + + const controller = new AbortController(); + setDetailState({ status: "loading", detail: null }); + setHistoryState({ status: "loading", history: null }); + void props.client.getMemory(selectedMemoryId, { signal: controller.signal }) + .then((detail) => { + if (!controller.signal.aborted) setDetailState({ status: "ready", detail }); + }) + .catch(() => { + if (!controller.signal.aborted) setDetailState({ status: "error", detail: null }); + }); + void props.client.getMemoryHistory(selectedMemoryId, { signal: controller.signal }) + .then((history) => { + if (!controller.signal.aborted) setHistoryState({ status: "ready", history }); + }) + .catch(() => { + if (!controller.signal.aborted) setHistoryState({ status: "error", history: null }); + }); + + return () => controller.abort(); + }, [props.client, props.open, props.projectId, selectedMemoryId, detailRequestKey]); + + useEffect(() => { + setPendingRestoreVersion(null); + setRestoreState({ status: "idle" }); + }, [selectedMemoryId]); + + useEffect(() => { + setSelectedMemory(null); + setDetailState(null); + setHistoryState(null); + setGraphOpen(false); + }, [props.projectId]); + + const pack = state.pack; + const itemCount = pack + ? sections.reduce((total, [key]) => total + pack[key].length, 0) + : 0; + + async function copyMarkdown() { + if (!pack) return; + await navigator.clipboard.writeText(pack.markdown); + setCopied(true); + } + + function openMemory(memoryId: string) { + setGraphOpen(false); + setSelectedMemory({ projectId: props.projectId, memoryId }); + } + + function closeDialog() { + setSelectedMemory(null); + setDetailState(null); + setHistoryState(null); + setGraphOpen(false); + props.onClose(); + } + + async function restoreSelectedMemory(targetVersion: number) { + if (!props.client || !selectedMemoryId || detailState?.status !== "ready") return; + setRestoreState({ status: "restoring", targetVersion }); + try { + await props.client.restoreMemory(selectedMemoryId, targetVersion, { + version: detailState.detail.version, + reason: "restored from desktop context pack" + }); + setPendingRestoreVersion(null); + setRestoreState({ status: "success", targetVersion }); + setDetailRequestKey((value) => value + 1); + } catch { + setRestoreState({ status: "error", targetVersion }); + } + } + + const title = graphOpen + ? props.t("home.contextPack.graph.title") + : selectedMemoryId + ? props.t("home.contextPack.detail.title") + : props.t("home.contextPack.title"); + + return ( + } + closeLabel={props.t("common.close")} + closeContent={} + className={`project-context-pack-dialog${graphOpen ? " project-context-pack-dialog--graph" : ""}`} + bodyClassName="project-context-pack-dialog__body" + onClose={closeDialog} + > + {selectedMemoryId && graphOpen && pack ? ( + setGraphOpen(false)} + onOpenMemory={openMemory} + /> + ) : selectedMemoryId ? ( + setSelectedMemory(null)} + onOpenGraph={() => setGraphOpen(true)} + onOpenMemory={openMemory} + onRequestRestore={setPendingRestoreVersion} + onCancelRestore={() => setPendingRestoreVersion(null)} + onConfirmRestore={(version) => void restoreSelectedMemory(version)} + onRetry={() => setDetailRequestKey((value) => value + 1)} + /> + ) : state.status === "loading" ? ( + {props.t("home.contextPack.loading")} + ) : state.status === "error" ? ( + + {props.t("home.contextPack.error")} + + + ) : itemCount === 0 ? ( + {props.t("home.contextPack.empty")} + ) : ( + <> +
+ {props.t("home.contextPack.generated")} + +
+
+ {sections.map(([key, label]) => { + const items = pack![key]; + if (items.length === 0) return null; + return ( +
+

{props.t(label)}

+
    + {items.map((item) => ( + + ))} +
+
+ ); + })} +
+ + )} +
+ ); +} + +function ContextPackItem(props: { + item: MemoryListItem | ProjectContextPackOutput["recentTasks"][number]; + onOpen: (memoryId: string) => void; +}) { + const title = props.item.title || ("summary" in props.item ? props.item.summary : "") || props.item.id; + const summary = "summary" in props.item ? props.item.summary : ""; + + return ( +
  • + {"memoryLayer" in props.item ? ( + + ) : ( +
    + {title} +
    + )} +
  • + ); +} + +function MemoryDetailView(props: { + state: MemoryDetailState | null; + historyState: MemoryHistoryState | null; + restoreState: RestoreState; + pendingRestoreVersion: number | null; + graph: ProjectContextPackOutput["graph"] | null; + memoryId: string; + t: Translate; + onBack: () => void; + onOpenGraph: () => void; + onOpenMemory: (memoryId: string) => void; + onRequestRestore: (version: number) => void; + onCancelRestore: () => void; + onConfirmRestore: (version: number) => void; + onRetry: () => void; +}) { + const hasGraphNode = props.graph?.nodes.some((node) => node.id === props.memoryId) ?? false; + return ( +
    +
    + + {hasGraphNode ? ( + + ) : null} +
    + {!props.state || props.state.status === "loading" ? ( + {props.t("home.contextPack.detail.loading")} + ) : props.state.status === "error" ? ( + + {props.t("home.contextPack.detail.error")} + + + ) : ( + + )} +
    + ); +} + +function MemoryDetailContent(props: { + detail: GetMemoryOutput; + historyState: MemoryHistoryState | null; + restoreState: RestoreState; + pendingRestoreVersion: number | null; + t: Translate; + onOpenMemory: (memoryId: string) => void; + onRequestRestore: (version: number) => void; + onCancelRestore: () => void; + onConfirmRestore: (version: number) => void; +}) { + const item = props.detail.item; + const evidenceIds = uniqueIds([ + ...item.sourceMemoryIds, + ...(item.policy?.evidenceMemoryIds ?? []), + ...(item.worldModel?.sourceMemoryIds ?? []), + ...(item.skill?.sourcePolicyIds ?? []), + ...(item.skill?.sourceWorldModelIds ?? []) + ]); + const source = item.provenance?.sourceAgent + ?? (typeof item.metadata.source === "string" ? item.metadata.source : undefined) + ?? props.t("home.contextPack.detail.unknown"); + + return ( +
    +
    +

    {item.title}

    + {item.summary && item.summary !== item.title ?

    {item.summary}

    : null} +
    +
    +

    {props.t("home.contextPack.detail.metadata")}

    +
    + + + + + + +
    +
    +
    +

    {props.t("home.contextPack.detail.content")}

    +

    {item.body || props.t("home.contextPack.detail.empty")}

    +
    + + + + {item.provenance ? ( +
    +

    {props.t("home.contextPack.detail.provenance")}

    +
    + + {item.provenance.repository ? : null} + {item.provenance.branch ? : null} + {item.provenance.commit ? : null} + +
    +
    + ) : null} +
    + ); +} + +function DetailField(props: { label: string; value: string }) { + return <>
    {props.label}
    {props.value}
    ; +} + +function IdSection(props: { + title: string; + ids: string[]; + empty: string; + onOpenMemory: (memoryId: string) => void; +}) { + return ( +
    +

    {props.title}

    + {props.ids.length > 0 ? ( +
      + {props.ids.map((id) => ( +
    • + +
    • + ))} +
    + ) :

    {props.empty}

    } +
    + ); +} + +function VersionRelations(props: { + detail: GetMemoryOutput; + t: Translate; + onOpenMemory: (memoryId: string) => void; +}) { + const supersession = props.detail.item.supersession; + const relatedIds = supersession + ? uniqueIds([...supersession.supersedesMemoryIds, ...(supersession.supersededByMemoryId ? [supersession.supersededByMemoryId] : [])]) + : []; + return ( +
    +

    {props.t("home.contextPack.detail.versionRelations")}

    + {relatedIds.length > 0 ? ( +
    + {supersession!.supersedesMemoryIds.length > 0 ? ( + + ) : null} + {supersession!.supersededByMemoryId ? ( + + ) : null} + {supersession!.reason ?

    {props.t("home.contextPack.detail.reason")}: {supersession!.reason}

    : null} +
    + ) :

    {props.t("home.contextPack.detail.none")}

    } +
    + ); +} + +function LinkedRelation(props: { + label: string; + ids: string[]; + onOpenMemory: (memoryId: string) => void; +}) { + return ( +
    + {props.label} + {props.ids.map((id) => ( + + ))} +
    + ); +} + +function MemoryHistorySection(props: { + currentVersion: number; + state: MemoryHistoryState | null; + restoreState: RestoreState; + pendingRestoreVersion: number | null; + t: Translate; + onRequestRestore: (version: number) => void; + onCancelRestore: () => void; + onConfirmRestore: (version: number) => void; +}) { + return ( +
    +

    {props.t("home.contextPack.history.title")}

    + {!props.state || props.state.status === "loading" ? ( +

    {props.t("home.contextPack.history.loading")}

    + ) : props.state.status === "error" ? ( +

    + {props.t("home.contextPack.history.error")} +

    + ) : props.state.history.items.length === 0 ? ( +

    {props.t("home.contextPack.history.empty")}

    + ) : ( +
    + {props.state.history.items.map((entry) => { + const version = entry.version; + const current = version === props.currentVersion; + const restorable = version !== undefined && entry.after !== undefined && !current; + const confirming = version !== undefined && props.pendingRestoreVersion === version; + const restoring = version !== undefined + && props.restoreState.status === "restoring" + && props.restoreState.targetVersion === version; + return ( +
    +
    + {version === undefined ? "-" : `v${version}`} · {historySnapshotTitle(entry.after, entry.changeType)} + {entry.changeType} · {entry.source} · {formatDateTime(entry.createdAt)} + {historySnapshotBody(entry.after) ?

    {historySnapshotBody(entry.after)}

    : null} +
    + {confirming ? ( +
    +

    {props.t("home.contextPack.history.confirm")}

    +
    + + +
    +
    + ) : ( + + )} +
    + ); + })} +
    + )} + {props.restoreState.status === "success" ? ( +

    + {props.t("home.contextPack.history.success")} v{props.restoreState.targetVersion} +

    + ) : null} + {props.restoreState.status === "error" ? ( +

    + {props.t("home.contextPack.history.restoreError")} +

    + ) : null} +
    + ); +} + +function historySnapshotTitle(snapshot: unknown, fallback: string): string { + const record = snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) + ? snapshot as Record + : null; + const info = record && typeof record.info === "object" && record.info !== null + ? record.info as Record + : null; + return typeof info?.title === "string" && info.title.trim() + ? info.title + : fallback; +} + +function historySnapshotBody(snapshot: unknown): string { + const record = snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) + ? snapshot as Record + : null; + return typeof record?.memoryValue === "string" ? record.memoryValue : ""; +} + +function statusLabel(status: GetMemoryOutput["item"]["status"], t: Translate): string { + const keys = { + activated: "memory.memories.status.activated", + resolving: "memory.memories.status.resolving", + archived: "memory.memories.status.archived", + deleted: "memory.memories.status.deleted" + } as const; + return t(keys[status]); +} + +function formatDateTime(value: string): string { + return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value)); +} + +function uniqueIds(ids: string[]): string[] { + return [...new Set(ids)]; +} + +function ContextPackStatus(props: { children: ReactNode }) { + return
    {props.children}
    ; +} diff --git a/App/frontend/desktop/src/pages/tests/app-frame.test.tsx b/App/frontend/desktop/src/pages/tests/app-frame.test.tsx index 62b675292..665ef1604 100644 --- a/App/frontend/desktop/src/pages/tests/app-frame.test.tsx +++ b/App/frontend/desktop/src/pages/tests/app-frame.test.tsx @@ -945,13 +945,14 @@ describe("AppFrame", () => { }); it("groups active tasks by time buckets (today, yesterday, last 7 days, older)", () => { - const now = new Date("2026-07-02T14:00:00+08:00"); + const now = new Date(2026, 6, 2, 14); + const localTimestamp = (month: number, day: number, hour: number) => new Date(2026, month, day, hour).toISOString(); const groups = groupTasksByTime([ - task("today-1", { title: "今天的任务", updatedAt: "2026-07-02T10:00:00+08:00" }), - task("today-2", { title: "今天的另一个", updatedAt: "2026-07-02T01:00:00+08:00" }), - task("yesterday", { title: "昨天的任务", updatedAt: "2026-07-01T18:00:00+08:00" }), - task("week", { title: "三天前", updatedAt: "2026-06-29T12:00:00+08:00" }), - task("older", { title: "很久以前", updatedAt: "2026-06-20T12:00:00+08:00" }), + task("today-1", { title: "今天的任务", updatedAt: localTimestamp(6, 2, 10) }), + task("today-2", { title: "今天的另一个", updatedAt: localTimestamp(6, 2, 1) }), + task("yesterday", { title: "昨天的任务", updatedAt: localTimestamp(6, 1, 18) }), + task("week", { title: "三天前", updatedAt: localTimestamp(5, 29, 12) }), + task("older", { title: "很久以前", updatedAt: localTimestamp(5, 20, 12) }), task("no-date", { title: "无日期", updatedAt: null }) ], now); diff --git a/App/frontend/desktop/src/pages/tests/context-pack-relation-graph-layout.test.ts b/App/frontend/desktop/src/pages/tests/context-pack-relation-graph-layout.test.ts new file mode 100644 index 000000000..fb2f68d97 --- /dev/null +++ b/App/frontend/desktop/src/pages/tests/context-pack-relation-graph-layout.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "vitest"; +import type { ProjectContextPackOutput } from "@memmy/local-api-contracts"; +import { layoutMemoryRelationGraph } from "../context-pack-relation-graph-layout.js"; + +describe("layoutMemoryRelationGraph", () => { + it("places memory layers in stable columns and rows", () => { + const layout = layoutMemoryRelationGraph(graph(), "memory-l2-a"); + const l1 = layout.nodes.find((node) => node.id === "memory-l1"); + const l2a = layout.nodes.find((node) => node.id === "memory-l2-a"); + const l2b = layout.nodes.find((node) => node.id === "memory-l2-b"); + const skill = layout.nodes.find((node) => node.id === "memory-skill"); + + expect(l2a?.position.x).toBeGreaterThan(l1?.position.x ?? 0); + expect(skill?.position.x).toBeGreaterThan(l2a?.position.x ?? 0); + expect(l2b?.position.x).toBe(l2a?.position.x); + expect(l2b?.position.y).toBeGreaterThan(l2a?.position.y ?? 0); + expect(l2a?.data.anchor).toBe(true); + expect(skill?.data.external).toBe(true); + }); + + it("keeps typed relations and drops edges whose nodes are absent", () => { + const layout = layoutMemoryRelationGraph(graph(), "memory-l2-a"); + + expect(layout.edges).toContainEqual(expect.objectContaining({ + source: "memory-l1", + target: "memory-l2-a", + label: "source", + className: "context-pack-relation-edge context-pack-relation-edge--source" + })); + expect(layout.edges).toContainEqual(expect.objectContaining({ + source: "memory-l2-a", + target: "memory-l2-b", + label: "supersedes", + style: expect.objectContaining({ strokeDasharray: "5 4" }) + })); + expect(layout.edges.some((edge) => edge.target === "missing-memory")).toBe(false); + }); +}); + +function graph(): ProjectContextPackOutput["graph"] { + return { + nodes: [ + node("memory-l1", "L1"), + node("memory-l2-a", "L2"), + node("memory-l2-b", "L2"), + { ...node("memory-skill", "Skill"), external: true } + ], + edges: [ + { sourceId: "memory-l1", targetId: "memory-l2-a", relation: "source" }, + { sourceId: "memory-l2-a", targetId: "memory-l2-b", relation: "supersedes", reason: "new evidence" }, + { sourceId: "memory-l2-a", targetId: "missing-memory", relation: "source" } + ] + }; +} + +function node(id: string, memoryLayer: "L1" | "L2" | "L3" | "Skill") { + return { + id, + kind: memoryLayer === "Skill" ? "skill" as const : "policy" as const, + memoryLayer, + status: "activated" as const, + title: id, + summary: "", + tags: [], + createdAt: "2026-08-07T10:00:00.000Z", + updatedAt: "2026-08-08T12:00:00.000Z", + version: 1 + }; +} diff --git a/App/frontend/desktop/src/pages/tests/home-page-context-pack.interaction.test.tsx b/App/frontend/desktop/src/pages/tests/home-page-context-pack.interaction.test.tsx new file mode 100644 index 000000000..6e614efec --- /dev/null +++ b/App/frontend/desktop/src/pages/tests/home-page-context-pack.interaction.test.tsx @@ -0,0 +1,152 @@ +// @vitest-environment happy-dom + +import { act, useEffect } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { ProjectContextPackOutput } from "@memmy/local-api-contracts"; +import { AgentRuntimeBridge } from "../../app/agent-runtime-bridge.js"; +import { AppProviders, useApiClients } from "../../app/providers.js"; +import type { AppClients } from "../../api/client-types.js"; +import { agentActions } from "../../state/app-actions.js"; +import { useAppState } from "../../state/app-state.js"; +import { HomePage } from "../home-page.js"; + +(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + +describe("HomePage project context pack session coverage", () => { + let container: HTMLDivElement; + let root: Root; + + beforeEach(() => { + Object.defineProperty(window, "localStorage", { configurable: true, value: createMemoryStorage() }); + Object.defineProperty(window, "sessionStorage", { configurable: true, value: createMemoryStorage() }); + container = document.createElement("div"); + document.body.append(container); + root = createRoot(container); + }); + + afterEach(() => { + act(() => root.unmount()); + document.body.replaceChildren(); + vi.restoreAllMocks(); + }); + + it("keeps the entry in running and historical project chats and closes it on project switch", async () => { + const getProjectContextPack = vi.fn(async (projectId: string) => contextPack(projectId)); + + await act(async () => { + root.render( + + + + + + + ); + }); + + expect(contextPackTrigger()).not.toBeNull(); + await act(async () => contextPackTrigger()?.click()); + expect(getProjectContextPack).toHaveBeenLastCalledWith("project-running"); + expect(document.querySelector('[role="dialog"]')?.textContent).toContain("Running Project"); + + await act(async () => switchButton("history").click()); + expect(document.querySelector('[role="dialog"]')).toBeNull(); + expect(contextPackTrigger()).not.toBeNull(); + + await act(async () => switchButton("running").click()); + expect(document.querySelector('[role="dialog"]')).toBeNull(); + await act(async () => switchButton("history").click()); + + await act(async () => contextPackTrigger()?.click()); + expect(getProjectContextPack).toHaveBeenLastCalledWith("project-history"); + expect(document.querySelector('[role="dialog"]')?.textContent).toContain("History Project"); + + await act(async () => switchButton("standalone").click()); + expect(document.querySelector('[role="dialog"]')).toBeNull(); + expect(contextPackTrigger()).toBeNull(); + }); +}); + +function SessionHarness(props: { + getProjectContextPack: (projectId: string) => Promise; +}) { + const { dispatch } = useAppState(); + const { setClients } = useApiClients(); + + useEffect(() => { + setClients({ memoryRuntime: { getProjectContextPack: props.getProjectContextPack } } as unknown as AppClients); + dispatch(agentActions.sessionSnapshotApplied({ + projectRegistryState: "ready", + projects: [ + { id: "project-running", name: "Running Project", rootPath: "/running", pinned: false, createdAt: "2026-08-08T00:00:00.000Z" }, + { id: "project-history", name: "History Project", rootPath: "/history", pinned: false, createdAt: "2026-08-08T00:00:00.000Z" } + ], + sessions: [ + { key: "websocket:running", title: "Running", projectId: "project-running", cwd: "/running", run_started_at: 1 }, + { key: "websocket:history", title: "History", projectId: "project-history", cwd: "/history" }, + { key: "websocket:standalone", title: "Standalone", projectId: null, cwd: "/tmp" } + ] + })); + openChat(dispatch, "running", false); + }, [dispatch, props.getProjectContextPack, setClients]); + + return ( + <> + + + + + ); +} + +function openChat(dispatch: ReturnType["dispatch"], chatId: string, closed: boolean) { + const sessionKey = `websocket:${chatId}`; + const requestId = `request-${chatId}`; + dispatch(agentActions.historyLoading(sessionKey, chatId, requestId)); + dispatch(agentActions.historyLoaded({ + schemaVersion: 1, + sessionKey, + last_turn_closed: closed, + messages: [ + { role: "user", content: `${chatId} question` }, + { role: "assistant", content: `${chatId} answer` } + ] + }, requestId)); +} + +function contextPackTrigger(): HTMLButtonElement | null { + return document.querySelector(".home-context-pack-trigger"); +} + +function switchButton(target: string): HTMLButtonElement { + const button = document.querySelector(`[data-switch="${target}"]`); + if (!button) throw new Error(`Missing ${target} switch button`); + return button; +} + +function contextPack(projectId: string): ProjectContextPackOutput { + return { + namespace: { projectId }, + conventions: [], + commands: [], + architectureFacts: [], + recentTasks: [{ id: `task-${projectId}`, title: projectId, updatedAt: "2026-08-08T12:00:00.000Z" }], + userPreferences: [], + graph: { nodes: [], edges: [] }, + markdown: `# Project Memory Pack: ${projectId}`, + generatedAt: "2026-08-08T12:00:00.000Z" + }; +} + +function createMemoryStorage(): Storage { + const values = new Map(); + return { + get length() { return values.size; }, + clear: () => values.clear(), + getItem: (key) => values.get(key) ?? null, + key: (index) => Array.from(values.keys())[index] ?? null, + removeItem: (key) => values.delete(key), + setItem: (key, value) => values.set(key, value) + }; +} diff --git a/App/frontend/desktop/src/pages/tests/project-context-pack-dialog.interaction.test.tsx b/App/frontend/desktop/src/pages/tests/project-context-pack-dialog.interaction.test.tsx new file mode 100644 index 000000000..ad062ddf0 --- /dev/null +++ b/App/frontend/desktop/src/pages/tests/project-context-pack-dialog.interaction.test.tsx @@ -0,0 +1,404 @@ +// @vitest-environment happy-dom + +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { GetMemoryOutput, MemoryListItem, ProjectContextPackOutput } from "@memmy/local-api-contracts"; +import type { MemoryRuntimeClient } from "../../api/memory-runtime-client.js"; +import type { MessageKey } from "../../i18n/messages.js"; +import { ProjectContextPackDialog } from "../project-context-pack-dialog.js"; + +(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + +describe("ProjectContextPackDialog", () => { + let container: HTMLDivElement; + let root: Root; + + beforeEach(() => { + container = document.createElement("div"); + document.body.append(container); + root = createRoot(container); + }); + + afterEach(() => { + act(() => root.unmount()); + document.body.replaceChildren(); + vi.restoreAllMocks(); + }); + + it("loads the selected project pack and copies its Markdown", async () => { + const writeText = vi.fn(async () => undefined); + Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText } }); + const getProjectContextPack = vi.fn(async () => contextPack()); + const getMemory = vi.fn(async () => memoryDetail()); + + await act(async () => { + root.render( + undefined} + /> + ); + }); + + expect(getProjectContextPack).toHaveBeenCalledWith("project-1"); + expect(document.body.textContent).toContain("Memmy Agent"); + expect(document.body.textContent).toContain("Ship desktop context pack entry"); + + const copyButton = Array.from(document.querySelectorAll("button")) + .find((button) => button.textContent?.includes("home.contextPack.copy")); + expect(copyButton).toBeDefined(); + await act(async () => copyButton?.click()); + expect(writeText).toHaveBeenCalledWith("# Project Memory Pack: project-1"); + expect(document.body.textContent).toContain("home.contextPack.copied"); + }); + + it("reloads when the open dialog is pointed at another project", async () => { + const getProjectContextPack = vi.fn(async (projectId: string) => contextPack(projectId)); + const getMemory = vi.fn(async () => memoryDetail()); + + await act(async () => { + root.render( + undefined} + /> + ); + }); + await act(async () => { + root.render( + undefined} + /> + ); + }); + + expect(getProjectContextPack.mock.calls.map(([projectId]) => projectId)).toEqual(["project-1", "project-2"]); + expect(document.body.textContent).toContain("Project Two"); + }); + + it("loads a clicked memory by id and renders its complete detail contract", async () => { + const getProjectContextPack = vi.fn(async () => contextPack()); + const getMemory = vi.fn(async () => memoryDetail()); + + await act(async () => { + root.render( + undefined} + /> + ); + }); + await act(async () => memoryButton().click()); + + expect(getMemory).toHaveBeenCalledWith("memory-1", { signal: expect.any(AbortSignal) }); + expect(document.body.textContent).toContain("Complete memory body"); + expect(document.body.textContent).toContain("L2"); + expect(document.body.textContent).toContain("codex"); + expect(document.body.textContent).toContain("memory-evidence-1"); + expect(document.body.textContent).toContain("memory-old-1"); + expect(document.body.textContent).toContain("memory-new-1"); + expect(document.body.textContent).toContain("repo/memmy-agent"); + }); + + it("opens the relation graph, selects a linked node, and jumps to its memory detail", async () => { + const getProjectContextPack = vi.fn(async () => contextPack()); + const getMemory = vi.fn(async (id: string) => memoryDetail(id)); + + await act(async () => { + root.render( + undefined} + /> + ); + }); + await act(async () => memoryButton().click()); + await act(async () => buttonContaining("home.contextPack.detail.openGraph").click()); + + expect(document.body.textContent).toContain("Related evidence memory"); + expect(document.body.textContent).toContain("home.contextPack.graph.locate"); + const linkedNode = document.querySelector('.react-flow__node[data-id="memory-evidence-1"]'); + expect(linkedNode).not.toBeNull(); + await act(async () => linkedNode?.click()); + expect(document.body.textContent).toContain("memory-evidence-1"); + + await act(async () => buttonContaining("home.contextPack.graph.openMemory").click()); + expect(getMemory.mock.calls.map(([id]) => id)).toEqual(["memory-1", "memory-evidence-1"]); + expect(document.body.textContent).toContain("Complete memory body: memory-evidence-1"); + }); + + it("confirms a historical restore and reloads detail from its recorded source version", async () => { + const getProjectContextPack = vi.fn(async () => contextPack()); + const getMemory = vi.fn(async (id: string) => memoryDetail(id)); + const restoreMemory = vi.fn(async (id: string, targetVersion: number) => ({ + ok: true as const, + id, + version: 4, + restoredVersion: targetVersion, + changeSeq: 4, + auditId: "audit-restore-1", + serverTime: "2026-08-08T13:00:00.000Z" + })); + + await act(async () => { + root.render( + undefined} + /> + ); + }); + await act(async () => memoryButton().click()); + await act(async () => buttonContaining("home.contextPack.history.restore").click()); + expect(document.body.textContent).toContain("home.contextPack.history.confirm"); + + await act(async () => buttonContaining("home.contextPack.history.confirmAction").click()); + + expect(restoreMemory).toHaveBeenCalledWith("memory-1", 1, { + version: 3, + reason: "restored from desktop context pack" + }); + expect(getMemory).toHaveBeenCalledTimes(2); + expect(document.body.textContent).toContain("home.contextPack.history.success"); + expect(document.body.textContent).toContain("v1"); + }); + + it("shows detail loading and recovers from a failed request", async () => { + const pending = deferred(); + let callCount = 0; + const getMemory = vi.fn((_id: string, _options?: { signal?: AbortSignal }): Promise => { + callCount += 1; + return callCount === 1 ? pending.promise : Promise.resolve(memoryDetail()); + }); + + await renderDialog({ getMemory }); + await act(async () => memoryButton().click()); + expect(document.body.textContent).toContain("home.contextPack.detail.loading"); + + await act(async () => pending.reject(new Error("service unavailable"))); + expect(document.body.textContent).toContain("home.contextPack.detail.error"); + + const retry = buttonContaining("common.retry"); + await act(async () => retry.click()); + expect(getMemory).toHaveBeenCalledTimes(2); + expect(document.body.textContent).toContain("Complete memory body"); + }); + + it("closes detail and aborts its old request when the project changes", async () => { + const pending = deferred(); + const historyPending = deferred>(); + const getProjectContextPack = vi.fn(async (projectId: string) => contextPack(projectId)); + const getMemory = vi.fn((_id: string, _options?: { signal?: AbortSignal }) => pending.promise); + const getMemoryHistory = vi.fn((_id: string, _options?: { signal?: AbortSignal }) => historyPending.promise); + const client = dialogClient({ getProjectContextPack, getMemory, getMemoryHistory }); + + await act(async () => { + root.render( undefined} />); + }); + await act(async () => memoryButton().click()); + const signal = getMemory.mock.calls[0]?.[1]?.signal; + const historySignal = getMemoryHistory.mock.calls[0]?.[1]?.signal; + expect(signal?.aborted).toBe(false); + expect(historySignal).toBe(signal); + + await act(async () => { + root.render( undefined} />); + }); + + expect(signal?.aborted).toBe(true); + expect(historySignal?.aborted).toBe(true); + expect(document.body.textContent).not.toContain("home.contextPack.detail.title"); + expect(document.body.textContent).toContain("Two"); + }); + + it("keeps an invalid memory response inside the detail error state", async () => { + const getMemory = vi.fn(async (_id: string, _options?: { signal?: AbortSignal }): Promise => { + throw new Error("Invalid input: memory detail contract mismatch"); + }); + await renderDialog({ getMemory }); + await act(async () => memoryButton().click()); + + expect(document.body.textContent).toContain("home.contextPack.detail.error"); + expect(document.body.textContent).toContain("home.contextPack.detail.back"); + expect(document.body.textContent).not.toContain("Complete memory body"); + }); + + async function renderDialog(input: { + getMemory: (id: string, options?: { signal?: AbortSignal }) => Promise; + }) { + await act(async () => { + root.render( + contextPack()), getMemory: input.getMemory })} + t={translate} + onClose={() => undefined} + /> + ); + }); + } +}); + +function translate(key: MessageKey): string { + return key; +} + +function contextPack(projectId = "project-1"): ProjectContextPackOutput { + return { + namespace: { projectId }, + conventions: [memoryListItem()], + commands: [], + architectureFacts: [], + recentTasks: [{ id: "episode-1", title: "Ship desktop context pack entry", updatedAt: "2026-08-08T12:00:00.000Z" }], + userPreferences: [], + graph: { + nodes: [ + memoryListItem(), + { ...memoryListItem("memory-evidence-1", "Related evidence memory"), external: true } + ], + edges: [ + { sourceId: "memory-evidence-1", targetId: "memory-1", relation: "source" }, + { sourceId: "memory-1", targetId: "memory-evidence-1", relation: "supersedes", reason: "New evidence" } + ] + }, + markdown: `# Project Memory Pack: ${projectId}`, + generatedAt: "2026-08-08T12:00:00.000Z" + }; +} + +function memoryListItem(id = "memory-1", title = "Use stable detail contracts"): MemoryListItem { + return { + id, + kind: "policy", + memoryLayer: "L2", + status: "activated", + title, + summary: "Load complete memory content by id.", + tags: ["architecture"], + createdAt: "2026-08-07T10:00:00.000Z", + updatedAt: "2026-08-08T12:00:00.000Z", + version: 3 + }; +} + +function memoryDetail(id = "memory-1"): GetMemoryOutput { + return { + item: { + ...memoryListItem(id, id === "memory-1" ? "Use stable detail contracts" : "Related evidence memory"), + body: `Complete memory body: ${id}`, + sourceMemoryIds: ["memory-source-1"], + policy: { evidenceMemoryIds: ["memory-evidence-1"], confidence: 0.9 }, + provenance: { + sourceAgent: "codex", + repository: "repo/memmy-agent", + branch: "main", + commit: "abc123", + sourceMemoryIds: ["memory-source-1"], + capturedAt: "2026-08-07T10:00:00.000Z" + }, + supersession: { + supersedesMemoryIds: ["memory-old-1"], + supersededByMemoryId: "memory-new-1", + reason: "New evidence" + }, + metadata: { source: "codex" } + }, + version: 3, + etag: "memory-1-v3" + }; +} + +function memoryHistory(id: string) { + return { + id, + currentVersion: 3, + items: [ + { + seq: 3, + version: 3, + changeType: "updated", + source: "panel.edit", + createdAt: "2026-08-08T12:00:00.000Z", + after: { info: { title: "Use stable detail contracts" }, memoryValue: "Current body" } + }, + { + seq: 1, + version: 1, + changeType: "created", + source: "turn_complete", + createdAt: "2026-08-07T10:00:00.000Z", + after: { info: { title: "Original contract" }, memoryValue: "Original body" } + } + ], + serverTime: "2026-08-08T12:00:00.000Z" + }; +} + +function dialogClient( + overrides: Partial> = {} +) { + return { + getProjectContextPack: async () => contextPack(), + getMemory: async (id: string) => memoryDetail(id), + getMemoryHistory: async (id: string) => memoryHistory(id), + restoreMemory: async (id: string, targetVersion: number) => ({ + ok: true as const, + id, + version: 4, + restoredVersion: targetVersion, + changeSeq: 4, + auditId: "audit-restore", + serverTime: "2026-08-08T13:00:00.000Z" + }), + ...overrides + }; +} + +function memoryButton(): HTMLButtonElement { + return buttonContaining("Use stable detail contracts"); +} + +function buttonContaining(text: string): HTMLButtonElement { + const button = Array.from(document.querySelectorAll("button")) + .find((candidate) => candidate.textContent?.includes(text)); + if (!button) throw new Error(`Missing button containing: ${text}`); + return button; +} + +function deferred() { + let resolve!: (value: T) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, resolve, reject }; +} diff --git a/App/frontend/desktop/src/pages/tests/project-target-picker.interaction.test.tsx b/App/frontend/desktop/src/pages/tests/project-target-picker.interaction.test.tsx index 0ed115695..c81841dd0 100644 --- a/App/frontend/desktop/src/pages/tests/project-target-picker.interaction.test.tsx +++ b/App/frontend/desktop/src/pages/tests/project-target-picker.interaction.test.tsx @@ -25,6 +25,7 @@ describe("ProjectTargetPicker interactions", () => { beforeEach(() => { Object.defineProperty(HTMLElement.prototype, "scrollIntoView", { configurable: true, value: scrollIntoView }); + Object.defineProperty(window, "localStorage", { configurable: true, value: createMemoryStorage() }); container = document.createElement("div"); document.body.append(container); root = createRoot(container); @@ -322,3 +323,17 @@ function getStandaloneButton(): HTMLButtonElement | null { return Array.from(document.querySelectorAll(".home-project-picker__actions [role='option']")) .find((button) => button.querySelector(".lucide-x")) ?? null; } + +function createMemoryStorage(): Storage { + const values = new Map(); + return { + get length() { + return values.size; + }, + clear: () => values.clear(), + getItem: (key) => values.get(key) ?? null, + key: (index) => Array.from(values.keys())[index] ?? null, + removeItem: (key) => values.delete(key), + setItem: (key, value) => values.set(key, value) + }; +} diff --git a/App/frontend/desktop/src/styles.css b/App/frontend/desktop/src/styles.css index 30688f84d..12d80030e 100644 --- a/App/frontend/desktop/src/styles.css +++ b/App/frontend/desktop/src/styles.css @@ -336,6 +336,10 @@ body.memmy-window-fullscreen { background: transparent; } +.home-composer-toolbar--conversation { + padding: 4px 0 0; +} + .home-project-picker { position: relative; z-index: 45; @@ -647,6 +651,333 @@ body.memmy-window-fullscreen { line-height: 16px; } +.home-context-pack-trigger { + display: inline-flex; + height: 28px; + align-items: center; + gap: 5px; + padding: 0 7px; + border: 0; + border-radius: var(--radius-input); + background: transparent; + color: color-mix(in srgb, var(--color-text-ink) 62%, transparent); + font-family: var(--font-sans); + font-size: var(--codex-text-sm); + line-height: 1; + cursor: pointer; +} + +.home-context-pack-trigger:hover, +.home-context-pack-trigger:focus-visible { + background: color-mix(in srgb, var(--color-action-sky) 10%, transparent); + color: var(--color-action-sky-hover); +} + +.project-context-pack-dialog { + width: min(680px, calc(100vw - 32px)); + max-height: min(720px, calc(100vh - 48px)); +} + +.project-context-pack-dialog--graph { width: min(900px, calc(100vw - 32px)); } + +.project-context-pack-dialog__body { + min-height: 260px; + overflow-y: auto; +} + +.project-context-pack-dialog__status { + display: flex; + min-height: 240px; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 12px; + color: color-mix(in srgb, var(--color-text-ink) 55%, transparent); + font-size: var(--codex-text-sm); +} + +.project-context-pack-dialog__retry, +.project-context-pack-dialog__copy { + display: inline-flex; + align-items: center; + gap: 6px; + border: 0; + border-radius: var(--radius-input); + background: color-mix(in srgb, var(--color-action-sky) 10%, transparent); + color: var(--color-action-sky-hover); + font-family: var(--font-sans); + font-size: var(--codex-text-sm); + font-weight: 500; + line-height: 16px; + cursor: pointer; +} + +.project-context-pack-dialog__retry { padding: 7px 10px; } + +.project-context-pack-dialog__summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding-bottom: 12px; + border-bottom: 1px solid color-mix(in srgb, var(--color-border-stone) 45%, transparent); + color: color-mix(in srgb, var(--color-text-ink) 52%, transparent); + font-size: var(--codex-text-xs); +} + +.project-context-pack-dialog__copy { flex-shrink: 0; padding: 6px 9px; } + +.project-context-pack-dialog__sections { display: grid; } + +.project-context-pack-dialog__section { + padding: 16px 0; + border-bottom: 1px solid color-mix(in srgb, var(--color-border-stone) 35%, transparent); +} + +.project-context-pack-dialog__section:last-child { border-bottom: 0; } + +.project-context-pack-dialog__section h3 { + margin: 0 0 8px; + color: var(--color-text-ink); + font-size: var(--codex-text-sm); + font-weight: 700; + letter-spacing: 0; +} + +.project-context-pack-dialog__section ul { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; } +.project-context-pack-dialog__section li { min-width: 0; } +.project-context-pack-dialog__section strong { font-size: var(--codex-text-sm); font-weight: 600; overflow-wrap: anywhere; } +.project-context-pack-dialog__section li span { color: color-mix(in srgb, var(--color-text-ink) 58%, transparent); font-size: var(--codex-text-xs); overflow-wrap: anywhere; } + +.project-context-pack-dialog__item { + display: grid; + width: 100%; + min-width: 0; + gap: 2px; + padding: 7px 8px; + border: 0; + border-radius: var(--radius-input); + background: transparent; + color: inherit; + font-family: inherit; + letter-spacing: 0; + text-align: left; + cursor: pointer; +} + +.project-context-pack-dialog__item:hover, +.project-context-pack-dialog__item:focus-visible { + background: color-mix(in srgb, var(--color-action-sky) 8%, transparent); + outline: none; +} + +.project-context-pack-dialog__item--static { cursor: default; } + +.project-context-pack-detail { display: grid; min-height: 240px; } + +.project-context-pack-detail__toolbar, +.context-pack-relation-graph__toolbar { + display: flex; + min-width: 0; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.project-context-pack-detail__back { + display: inline-flex; + width: fit-content; + align-items: center; + gap: 5px; + margin-bottom: 12px; + padding: 5px 7px; + border: 0; + border-radius: var(--radius-input); + background: transparent; + color: var(--color-action-sky-hover); + font-family: var(--font-sans); + font-size: var(--codex-text-xs); + cursor: pointer; +} + +.project-context-pack-detail__back:hover, +.project-context-pack-detail__back:focus-visible { background: color-mix(in srgb, var(--color-action-sky) 10%, transparent); } + +.project-context-pack-detail__graph, +.context-pack-relation-graph__locate, +.context-pack-relation-graph__selection button { + display: inline-flex; + flex-shrink: 0; + align-items: center; + gap: 6px; + padding: 6px 8px; + border: 0; + border-radius: var(--radius-input); + background: color-mix(in srgb, var(--color-action-sky) 10%, transparent); + color: var(--color-action-sky-hover); + font-family: var(--font-sans); + font-size: var(--codex-text-xs); + cursor: pointer; +} + +.project-context-pack-detail__graph:hover, +.project-context-pack-detail__graph:focus-visible, +.context-pack-relation-graph__locate:hover, +.context-pack-relation-graph__locate:focus-visible, +.context-pack-relation-graph__selection button:hover, +.context-pack-relation-graph__selection button:focus-visible { + background: color-mix(in srgb, var(--color-action-sky) 16%, transparent); +} + +.project-context-pack-detail__content { display: grid; gap: 18px; } +.project-context-pack-detail__content section { display: grid; gap: 8px; } +.project-context-pack-detail__heading { display: grid; gap: 5px; padding-bottom: 14px; border-bottom: 1px solid color-mix(in srgb, var(--color-border-stone) 45%, transparent); } +.project-context-pack-detail__heading h3 { margin: 0; font-size: var(--codex-text-base); letter-spacing: 0; overflow-wrap: anywhere; } +.project-context-pack-detail__heading p { margin: 0; color: color-mix(in srgb, var(--color-text-ink) 60%, transparent); font-size: var(--codex-text-sm); line-height: 1.55; white-space: pre-wrap; } +.project-context-pack-detail__content h4 { margin: 0; font-size: var(--codex-text-sm); letter-spacing: 0; } + +.project-context-pack-detail__grid { + display: grid; + grid-template-columns: minmax(96px, 0.35fr) minmax(0, 1fr); + gap: 7px 12px; + margin: 0; + font-size: var(--codex-text-xs); +} + +.project-context-pack-detail__grid dt { color: color-mix(in srgb, var(--color-text-ink) 52%, transparent); } +.project-context-pack-detail__grid dd { min-width: 0; margin: 0; overflow-wrap: anywhere; } +.project-context-pack-detail__body { margin: 0; font-size: var(--codex-text-sm); line-height: 1.65; white-space: pre-wrap; overflow-wrap: anywhere; } +.project-context-pack-detail__ids { display: grid; gap: 5px; margin: 0; padding: 0; list-style: none; } +.project-context-pack-detail__ids button, +.project-context-pack-detail__relations button { + max-width: 100%; + padding: 3px 5px; + border: 0; + border-radius: 3px; + background: color-mix(in srgb, var(--color-action-sky) 7%, transparent); + color: var(--color-action-sky-hover); + text-align: left; + cursor: pointer; +} + +.project-context-pack-detail__ids button:hover, +.project-context-pack-detail__ids button:focus-visible, +.project-context-pack-detail__relations button:hover, +.project-context-pack-detail__relations button:focus-visible { background: color-mix(in srgb, var(--color-action-sky) 13%, transparent); } +.project-context-pack-detail__ids code, +.project-context-pack-detail__relations code { font-size: var(--codex-text-xs); overflow-wrap: anywhere; } +.project-context-pack-detail__empty { margin: 0; color: color-mix(in srgb, var(--color-text-ink) 48%, transparent); font-size: var(--codex-text-xs); } + +.project-context-pack-detail__relations { display: grid; gap: 8px; } +.project-context-pack-detail__relations > div { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; } +.project-context-pack-detail__relations span { color: color-mix(in srgb, var(--color-text-ink) 52%, transparent); font-size: var(--codex-text-xs); } +.project-context-pack-detail__relations p { margin: 0; font-size: var(--codex-text-xs); } + +.project-context-pack-history__list { display: grid; border-top: 1px solid color-mix(in srgb, var(--color-border-stone) 38%, transparent); } + +.project-context-pack-history__item { + display: grid; + grid-template-columns: minmax(0, 1fr) max-content; + align-items: center; + gap: 12px; + padding: 10px 0; + border-bottom: 1px solid color-mix(in srgb, var(--color-border-stone) 32%, transparent); +} + +.project-context-pack-history__summary { display: grid; min-width: 0; gap: 3px; } +.project-context-pack-history__summary strong { overflow-wrap: anywhere; font-size: var(--codex-text-xs); } +.project-context-pack-history__summary span { color: color-mix(in srgb, var(--color-text-ink) 48%, transparent); font-size: 10px; } +.project-context-pack-history__summary p { display: -webkit-box; margin: 2px 0 0; overflow: hidden; color: color-mix(in srgb, var(--color-text-ink) 62%, transparent); font-size: var(--codex-text-xs); line-height: 16px; -webkit-box-orient: vertical; -webkit-line-clamp: 2; } + +.project-context-pack-history__restore, +.project-context-pack-history__confirm button { + padding: 6px 8px; + border: 0; + border-radius: var(--radius-input); + background: color-mix(in srgb, var(--color-action-sky) 10%, transparent); + color: var(--color-action-sky-hover); + font-family: var(--font-sans); + font-size: var(--codex-text-xs); + cursor: pointer; +} + +.project-context-pack-history__restore:disabled, +.project-context-pack-history__confirm button:disabled { cursor: default; opacity: 0.48; } +.project-context-pack-history__confirm { display: grid; max-width: 270px; gap: 7px; } +.project-context-pack-history__confirm p { margin: 0; color: color-mix(in srgb, var(--color-text-ink) 65%, transparent); font-size: var(--codex-text-xs); line-height: 16px; } +.project-context-pack-history__confirm > div { display: flex; justify-content: flex-end; gap: 6px; } +.project-context-pack-history__confirm button:first-child { background: transparent; color: color-mix(in srgb, var(--color-text-ink) 60%, transparent); } +.project-context-pack-history__feedback { margin: 0; color: #548b75; font-size: var(--codex-text-xs); } +.project-context-pack-history__feedback--error { color: var(--color-danger, #b54f4f); } + +.context-pack-relation-graph { display: grid; min-height: 0; gap: 10px; } +.context-pack-relation-graph__toolbar .project-context-pack-detail__back { margin-bottom: 0; } + +.context-pack-relation-graph__canvas { + width: 100%; + height: min(420px, calc(100vh - 260px)); + min-height: 300px; + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--color-border-stone) 42%, transparent); + border-radius: var(--radius-input); + background: color-mix(in srgb, var(--color-canvas-oat) 24%, var(--color-background-paper)); +} + +.context-pack-relation-graph__canvas .react-flow__node { cursor: pointer; } +.context-pack-relation-graph__canvas .react-flow__node.selected .context-pack-relation-node { box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-action-sky) 30%, transparent); } +.context-pack-relation-graph__canvas .react-flow__edge-text { font-size: 9px; fill: color-mix(in srgb, var(--color-text-ink) 48%, transparent); } +.context-pack-relation-graph__canvas .react-flow__edge-textbg { fill: var(--color-background-paper); fill-opacity: 0.9; } +.context-pack-relation-graph__canvas .context-pack-relation-edge--source path { stroke: #548b75; } +.context-pack-relation-graph__canvas .context-pack-relation-edge--supersedes path { stroke: #9b6d55; } + +.context-pack-relation-node { + display: grid; + width: 100%; + height: 100%; + align-content: center; + gap: 7px; + padding: 10px 12px; + border: 1px solid color-mix(in srgb, var(--color-text-ink) 18%, transparent); + border-left: 3px solid #548b75; + border-radius: var(--radius-input); + background: var(--color-background-paper); + color: var(--color-text-ink); +} + +.context-pack-relation-node[data-layer="L2"] { border-left-color: #4f7fa9; } +.context-pack-relation-node[data-layer="L3"] { border-left-color: #806f9e; } +.context-pack-relation-node[data-layer="Skill"] { border-left-color: #9b7851; } +.context-pack-relation-node[data-anchor="true"] { background: color-mix(in srgb, var(--color-action-sky) 7%, var(--color-background-paper)); } +.context-pack-relation-node[data-external="true"] { border-style: dashed; opacity: 0.8; } +.context-pack-relation-node__meta { display: flex; align-items: center; justify-content: space-between; gap: 8px; color: color-mix(in srgb, var(--color-text-ink) 48%, transparent); font-size: 10px; } +.context-pack-relation-node strong { min-width: 0; overflow: hidden; font-size: var(--codex-text-xs); line-height: 16px; text-overflow: ellipsis; white-space: nowrap; } +.context-pack-relation-node__handle { width: 7px; height: 7px; border: 1px solid var(--color-background-paper); background: color-mix(in srgb, var(--color-text-ink) 42%, transparent); } + +.context-pack-relation-graph__selection { + display: flex; + min-width: 0; + align-items: center; + justify-content: space-between; + gap: 12px; + padding-top: 2px; +} + +.context-pack-relation-graph__selection > div { display: grid; min-width: 0; gap: 2px; } +.context-pack-relation-graph__selection span { color: color-mix(in srgb, var(--color-text-ink) 48%, transparent); font-size: 10px; } +.context-pack-relation-graph__selection strong { overflow: hidden; font-size: var(--codex-text-sm); text-overflow: ellipsis; white-space: nowrap; } +.context-pack-relation-graph__selection code { overflow: hidden; color: color-mix(in srgb, var(--color-text-ink) 55%, transparent); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; } + +@media (max-width: 560px) { + .project-context-pack-detail__grid { grid-template-columns: 1fr; gap: 3px; } + .project-context-pack-detail__grid dd { margin-bottom: 6px; } + .project-context-pack-detail__toolbar, + .context-pack-relation-graph__toolbar, + .context-pack-relation-graph__selection { align-items: flex-start; flex-direction: column; } + .context-pack-relation-graph__canvas { height: 340px; min-height: 280px; } + .project-context-pack-history__item { grid-template-columns: 1fr; } + .project-context-pack-history__confirm { max-width: none; } +} + .border-content-panel { border: 1px solid var(--border-content-panel); box-shadow: none; diff --git a/App/shell/desktop/package.json b/App/shell/desktop/package.json index e14fa46f8..1dc184f0e 100644 --- a/App/shell/desktop/package.json +++ b/App/shell/desktop/package.json @@ -29,7 +29,7 @@ "yaml": "^2.9.0" }, "devDependencies": { - "electron": "38.4.0", + "electron": "43.3.0", "electron-builder": "^26.0.12" } } diff --git a/Memory/Dockerfile b/Memory/Dockerfile index 1f9b0500e..e38fa8a3d 100644 --- a/Memory/Dockerfile +++ b/Memory/Dockerfile @@ -11,9 +11,11 @@ RUN npm ci \ && npm run memory:build \ && mkdir -p /runtime \ && cp Memory/package.json /runtime/package.json \ + && cp Memory/package-lock.json /runtime/package-lock.json \ && cp -R Memory/dist /runtime/dist \ && cd /runtime \ - && npm install --omit=dev --no-package-lock + && npm ci --omit=dev \ + && npm audit --omit=dev --audit-level=high FROM node:24-bookworm-slim AS runtime diff --git a/Memory/package-lock.json b/Memory/package-lock.json new file mode 100644 index 000000000..cd9f6a21e --- /dev/null +++ b/Memory/package-lock.json @@ -0,0 +1,3381 @@ +{ + "name": "@memmy/memory", + "version": "1.0.4", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@memmy/memory", + "version": "1.0.4", + "dependencies": { + "@huggingface/transformers": "^3.8.0", + "better-sqlite3": "^12.6.3", + "dotenv": "^16.6.1", + "sharp": "0.35.3", + "sqlite-vec": "0.1.9", + "yaml": "^2.9.0" + }, + "bin": { + "memmy-memory": "dist/src/cli/index.js" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/node": "^25.9.1", + "tsx": "^4.22.3", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/jinja": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.9.tgz", + "integrity": "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/transformers": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.8.1.tgz", + "integrity": "sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==", + "license": "Apache-2.0", + "dependencies": { + "@huggingface/jinja": "^0.5.3", + "onnxruntime-node": "1.21.0", + "onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4", + "sharp": "^0.34.1" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.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/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "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.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "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.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "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.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "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.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "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.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "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.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "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.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "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/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "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" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "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/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "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/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.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/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/flatbuffers": { + "version": "25.9.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", + "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", + "license": "Apache-2.0" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "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/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "license": "ISC" + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "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.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "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/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "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/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "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/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onnxruntime-common": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz", + "integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==", + "license": "MIT" + }, + "node_modules/onnxruntime-node": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.21.0.tgz", + "integrity": "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "global-agent": "^3.0.0", + "onnxruntime-common": "1.21.0", + "tar": "^7.0.1" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.22.0-dev.20250409-89f8206ba4", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.22.0-dev.20250409-89f8206ba4.tgz", + "integrity": "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==", + "license": "MIT", + "dependencies": { + "flatbuffers": "^25.1.24", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, + "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { + "version": "1.22.0-dev.20250409-89f8206ba4", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.22.0-dev.20250409-89f8206ba4.tgz", + "integrity": "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==", + "license": "MIT" + }, + "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/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.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.143.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT" + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sharp": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "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/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "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/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/sqlite-vec": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec/-/sqlite-vec-0.1.9.tgz", + "integrity": "sha512-L7XJWRIBNvR9O5+vh1FQ+IGkh/3D2AzVksW5gdtk28m78Hy8skFD0pqReKH1Yp0/BUKRGcffgKvyO/EON5JXpA==", + "license": "MIT OR Apache", + "optionalDependencies": { + "sqlite-vec-darwin-arm64": "0.1.9", + "sqlite-vec-darwin-x64": "0.1.9", + "sqlite-vec-linux-arm64": "0.1.9", + "sqlite-vec-linux-x64": "0.1.9", + "sqlite-vec-windows-x64": "0.1.9" + } + }, + "node_modules/sqlite-vec-darwin-arm64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-arm64/-/sqlite-vec-darwin-arm64-0.1.9.tgz", + "integrity": "sha512-jSsZpE42OfBkGL/ItyJTVCUwl6o6Ka3U5rc4j+UBDIQzC1ulSSKMEhQLthsOnF/MdAf1MuAkYhkdKmmcjaIZQg==", + "cpu": [ + "arm64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/sqlite-vec-darwin-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-x64/-/sqlite-vec-darwin-x64-0.1.9.tgz", + "integrity": "sha512-KDlVyqQT7pnOhU1ymB9gs7dMbSoVmKHitT+k1/xkjarcX8bBqPxWrGlK/R+C5WmWkfvWwyq5FfXfiBYCBs6PlA==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/sqlite-vec-linux-arm64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-arm64/-/sqlite-vec-linux-arm64-0.1.9.tgz", + "integrity": "sha512-5wXVJ9c9kR4CHm/wVqXb/R+XUHTdpZ4nWbPHlS+gc9qQFVHs92Km4bPnCKX4rtcPMzvNis+SIzMJR1SCEwpuUw==", + "cpu": [ + "arm64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sqlite-vec-linux-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-x64/-/sqlite-vec-linux-x64-0.1.9.tgz", + "integrity": "sha512-w3tCH8xK2finW8fQJ/m8uqKodXUZ9KAuAar2UIhz4BHILfpE0WM/MTGCRfa7RjYbrYim5Luk3guvMOGI7T7JQA==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sqlite-vec-windows-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-windows-x64/-/sqlite-vec-windows-x64-0.1.9.tgz", + "integrity": "sha512-y3gEIyy/17bq2QFPQOWLE68TYWcRZkBQVA2XLrTPHNTOp55xJi/BBBmOm40tVMDMjtP+Elpk6UBUXdaq+46b0Q==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "win32" + ] + }, + "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/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "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.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "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==", + "license": "0BSD", + "optional": true + }, + "node_modules/tsx": { + "version": "4.23.11", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.11.tgz", + "integrity": "sha512-Ry2oTEUnhBdeEdWIztY8kf3/nBGnPnjMLVGL0YfdRXMORuPER5NlKmayqxtxRxwB1xBN+RivRaJfe7PM1rtiyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "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.4.0", + "esbuild": "^0.27.0 || ^0.28.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.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "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.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.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.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.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/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/Memory/package.json b/Memory/package.json index e8c0c5f2d..0bf031e7c 100644 --- a/Memory/package.json +++ b/Memory/package.json @@ -31,10 +31,16 @@ "engines": { "node": ">=20" }, + "overrides": { + "@huggingface/transformers": { + "sharp": "$sharp" + } + }, "dependencies": { "@huggingface/transformers": "^3.8.0", "better-sqlite3": "^12.6.3", "dotenv": "^16.6.1", + "sharp": "0.35.3", "sqlite-vec": "0.1.9", "yaml": "^2.9.0" }, diff --git a/Memory/src/server/http.ts b/Memory/src/server/http.ts index e843ae8a8..4f894a3bf 100644 --- a/Memory/src/server/http.ts +++ b/Memory/src/server/http.ts @@ -54,6 +54,8 @@ export const API_ROUTES = [ "POST /api/v1/memory/:id/processing/retry", "POST /api/v1/memory/:id/quality", "POST /api/v1/memory/:id/edit", + "GET /api/v1/memory/:id/history", + "POST /api/v1/memory/:id/history/:version/restore", "POST /api/v1/memory/:id/archive", "POST /api/v1/memory/:id/promote", "GET /api/v1/panel/review/candidates", @@ -849,6 +851,7 @@ async function routeRequest( title?: unknown; content?: unknown; tags?: unknown; + version?: unknown; }; if (typeof request.title !== "string" || !request.title.trim()) { throw new MemoryServiceError("invalid_argument", "memory.edit title is required"); @@ -859,6 +862,9 @@ async function routeRequest( if (!Array.isArray(request.tags) || request.tags.some((tag) => typeof tag !== "string")) { throw new MemoryServiceError("invalid_argument", "memory.edit tags must be an array of strings"); } + if (!Number.isInteger(request.version) || Number(request.version) < 1) { + throw new MemoryServiceError("invalid_argument", "memory.edit version must be a positive integer"); + } const result = service.editMemory(decodeMatchSegment(memoryEdit, 1), { ...request, title: request.title.trim(), @@ -869,6 +875,31 @@ async function routeRequest( return result; } + const memoryHistory = match(path, /^\/api\/v1\/memory\/([^/]+)\/history$/); + if (method === "GET" && memoryHistory) { + requireMemoryRead(principal); + return service.memoryHistory(decodeMatchSegment(memoryHistory, 1), { + namespace: principal.namespace, + limit: parseNumber(url.searchParams.get("limit")) + }); + } + + const memoryRestore = match(path, /^\/api\/v1\/memory\/([^/]+)\/history\/(\d+)\/restore$/); + if (method === "POST" && memoryRestore) { + requireMemoryWrite(principal); + const request = envelopeWithPrincipal(asObject(body, "memory.restore"), principal) as MemoryGovernanceRequest; + if (!Number.isInteger(request.version) || Number(request.version) < 1) { + throw new MemoryServiceError("invalid_argument", "memory.restore version must be a positive integer"); + } + const result = service.restoreMemory( + decodeMatchSegment(memoryRestore, 1), + Number(decodeMatchSegment(memoryRestore, 2)), + request + ); + if (result.embeddingJobId) autoWorker.schedule(); + return result; + } + const memoryArchive = match(path, /^\/api\/v1\/memory\/([^/]+)\/archive$/); if (method === "POST" && memoryArchive) { requireMemoryWrite(principal); diff --git a/Memory/src/service/memory-service.ts b/Memory/src/service/memory-service.ts index 038fa7a0b..acc978875 100644 --- a/Memory/src/service/memory-service.ts +++ b/Memory/src/service/memory-service.ts @@ -22,6 +22,7 @@ import { } from "../storage/backend.js"; import type { MemoryDb } from "../storage/db.js"; import { + MemoryVersionConflictError, Repositories, jobToRef, kindFromMemory, @@ -1941,7 +1942,12 @@ export class MemoryService { const title = request.title.trim(); const content = request.content.trim(); const tags = uniq(request.tags.map((tag) => tag.trim()).filter(Boolean)); - const updated = this.repos.memories.update({ + if (!Number.isInteger(request.version) || request.version! < 1) { + throw new MemoryServiceError("invalid_argument", "memory.edit version is required"); + } + let updated: MemoryRow; + try { + updated = this.repos.memories.update({ ...memory, memoryValue: content, tags, @@ -1954,7 +1960,13 @@ export class MemoryService { }, contentHash: stableHash(content), updatedAt: at - }); + }, request.version); + } catch (error) { + if (error instanceof MemoryVersionConflictError) { + throw new MemoryServiceError("conflict", `memory was modified by another agent (current version ${error.actualVersion})`); + } + throw error; + } const changeSeq = this.repos.runtime.appendChange({ memoryId: updated.id, namespaceId: namespaceIdFromMemory(updated), @@ -1995,6 +2007,57 @@ export class MemoryService { return { ok: true, id: updated.id, version: updated.version, changeSeq, auditId: audit.id, embeddingJobId, serverTime: at }; } + memoryHistory(id: string, request: RequestEnvelope & { limit?: number } = {}) { + const memory = this.requireExistingMemory(id); + this.assertMemoryInScope(memory, request.namespace); + return { + id, + currentVersion: memory.version, + items: this.repos.runtime.listMemoryChanges(id, request.limit ?? 100).map((change) => ({ + seq: change.seq, + version: change.version, + changeType: change.changeType, + source: change.source, + createdAt: change.createdAt, + before: change.before, + after: change.after + })), + serverTime: nowIso() + }; + } + + restoreMemory(id: string, targetVersion: number, request: MemoryGovernanceRequest = {}) { + this.assertMemoryAddEnabled(); + if (!Number.isInteger(targetVersion) || targetVersion < 1) { + throw new MemoryServiceError("invalid_argument", "target version must be a positive integer"); + } + const memory = this.requireExistingMemory(id); + this.assertMemoryInScope(memory, request.namespace); + if (!Number.isInteger(request.version) || request.version! < 1) { + throw new MemoryServiceError("invalid_argument", "memory.restore version is required"); + } + const history = this.repos.runtime.listMemoryChanges(id, 500); + const target = history.find((change) => change.version === targetVersion && change.after && typeof change.after === "object")?.after; + if (!target || typeof target !== "object") throw new MemoryServiceError("not_found", `memory history version not found: ${targetVersion}`); + const at = nowIso(); + const before = memory; + let updated: MemoryRow; + try { + updated = this.repos.memories.update({ ...(target as MemoryRow), id, version: memory.version, updatedAt: at }, request.version); + } catch (error) { + if (error instanceof MemoryVersionConflictError) throw new MemoryServiceError("conflict", `memory was modified by another agent (current version ${error.actualVersion})`); + throw error; + } + const changeSeq = this.repos.runtime.appendChange({ memoryId: id, namespaceId: namespaceIdFromMemory(updated), kind: kindFromMemory(updated), op: "updated", entityId: id, userId: updated.userId, changeType: "restore", before, after: updated, source: "panel.restore", createdAt: at }); + const audit = this.repos.runtime.insertAudit({ userId: updated.userId, sessionId: updated.sessionId, actor: request.namespace ? { ...request.namespace } : {}, action: "restore_memory", targetKind: kindFromMemory(updated), targetId: id, before, after: updated, meta: { reason: request.reason, targetVersion }, createdAt: at }); + let embeddingJobId: string | undefined; + if (this.config.algorithm.capture.embedAfterCapture) { + this.repos.memories.deleteVector(updated.id, updated.memoryLayer === "L1" ? "vec_summary" : "vec"); + embeddingJobId = this.workerHandlers.enqueueJob({ jobType: "embedding", userId: updated.userId, sessionId: updated.sessionId, targetMemoryId: updated.id, payload: { source: "panel.restore", changeSeq, contentHash: updated.contentHash }, createdAt: at }).id; + } + return { ok: true, id, version: updated.version, restoredVersion: targetVersion, changeSeq, auditId: audit.id, embeddingJobId, serverTime: at }; + } + reviewCandidates(request: RequestEnvelope & { layer?: MemoryLayer; limit?: number } = {}) { const limit = Math.max(1, Math.min(request.limit ?? 100, 1000)); const memories = this.repos.memories.list({ diff --git a/Memory/src/service/read-model/memory.ts b/Memory/src/service/read-model/memory.ts index 16bf91429..5ac024d74 100644 --- a/Memory/src/service/read-model/memory.ts +++ b/Memory/src/service/read-model/memory.ts @@ -6,7 +6,6 @@ import { isRecord } from "../../utils/json.js"; import { firstLine } from "../../utils/text.js"; export function detailFromMemory(memory: MemoryRow, processing?: MemoryProcessingRecord): MemoryDetailItem { - const sourceMemoryIds = memory.properties.internal_info.source_memory_ids; const provenance = isRecord(memory.properties.internal_info.provenance) ? memory.properties.internal_info.provenance as unknown as MemoryDetailItem["provenance"] : undefined; @@ -16,7 +15,7 @@ export function detailFromMemory(memory: MemoryRow, processing?: MemoryProcessin return { id: memory.id, kind: kindFromMemory(memory), memoryLayer: memory.memoryLayer, status: memory.status, title: detailTitleForMemory(memory), summary: detailSummaryForMemory(memory), tags: panelTagsForMemory(memory, processing), updatedAt: memory.updatedAt, version: memory.version, processing, body: memory.memoryValue, createdAt: memory.createdAt, - sourceMemoryIds: stringArray(sourceMemoryIds), + sourceMemoryIds: sourceMemoryIdsFromMemory(memory), provenance, supersession: supersedesMemoryIds.length || supersededByMemoryId ? { supersedesMemoryIds, supersededByMemoryId, reason: supersessionReason } @@ -56,7 +55,12 @@ export function memoryDetailWithLayerPayload(detail: MemoryDetailItem, memory: M } export function memoryEtag(memory: MemoryRow): string { return `${memory.id}-v${memory.version}`; } -export function sourceMemoryIdsFromMemory(memory: MemoryRow): string[] { return stringArrayFromInternal(memory, "source_memory_ids").concat(stringArrayFromInternal(memory, "source_l1_memory_ids")).concat(stringArrayFromInternal(memory, "source_policy_ids")).concat(stringArrayFromInternal(memory, "evidence_anchor_ids")); } +export function sourceMemoryIdsFromMemory(memory: MemoryRow): string[] { + const provenance = isRecord(memory.properties.internal_info.provenance) + ? stringArray(memory.properties.internal_info.provenance.sourceMemoryIds) + : []; + return [...new Set(provenance.concat(stringArrayFromInternal(memory, "source_memory_ids"), stringArrayFromInternal(memory, "source_l1_memory_ids"), stringArrayFromInternal(memory, "source_policy_ids"), stringArrayFromInternal(memory, "evidence_anchor_ids")))]; +} export function procedureFromSkillMemory(memory: MemoryRow): string[] | undefined { const value = memory.properties.internal_info.procedure_json ?? memory.properties.internal_info.procedure; if (Array.isArray(value)) return value.filter((item): item is string => typeof item === "string"); if (typeof value === "string") { try { const parsed = JSON.parse(value) as unknown; if (Array.isArray(parsed)) return parsed.filter((item): item is string => typeof item === "string"); } catch { return value.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean); } } return undefined; } function firstReadableDetailMemoryLine(value: string): string | undefined { return value.split(/\r?\n/).map(cleanDetailDisplayText).find((line): line is string => Boolean(line && !isWorldSectionHeadingForDisplay(line) && !isInternalMemoryKeyForDisplay(line))); } diff --git a/Memory/src/service/read-model/panel-read.ts b/Memory/src/service/read-model/panel-read.ts index ec440b979..fed456b97 100644 --- a/Memory/src/service/read-model/panel-read.ts +++ b/Memory/src/service/read-model/panel-read.ts @@ -12,7 +12,7 @@ import type { RawTurnRecord, Repositories } from "../../storage/repositories.js"; -import { detailSummaryForMemory } from "./memory.js"; +import { detailSummaryForMemory, sourceMemoryIdsFromMemory } from "./memory.js"; import { memoryFilterForNamespace, namespaceForMemory, @@ -521,6 +521,10 @@ export class PanelReadModel { architectureFacts: MemoryListItem[]; recentTasks: Array<{ id: string; title: string; updatedAt: string }>; userPreferences: MemoryListItem[]; + graph: { + nodes: Array; + edges: Array<{ sourceId: string; targetId: string; relation: "source" | "supersedes"; reason?: string }>; + }; markdown: string; generatedAt: string; } { @@ -539,6 +543,17 @@ export class PanelReadModel { .filter((episode) => this.episodeMatchesNamespace(episode, context.namespace)) .slice(0, 8) .map((episode) => ({ id: episode.id, title: String(this.deps.episodeRef(episode).title ?? this.deps.episodeRef(episode).summary ?? episode.id), updatedAt: episode.updatedAt })); + const selectedIds = new Set([...conventions, ...commands, ...architectureFacts, ...userPreferences].map((item) => item.id)); + const selectedMemories = memories.filter((memory) => selectedIds.has(memory.id)); + const edges = selectedMemories.flatMap((memory) => [ + ...sourceMemoryIdsFromMemory(memory).map((sourceId) => ({ sourceId, targetId: memory.id, relation: "source" as const })), + ...this.deps.repos.memories.relationsFor(memory.id).map((relation) => ({ sourceId: relation.sourceMemoryId, targetId: relation.targetMemoryId, relation: "supersedes" as const, reason: relation.reason })) + ]).filter((edge, index, all) => all.findIndex((item) => item.sourceId === edge.sourceId && item.targetId === edge.targetId && item.relation === edge.relation) === index); + const graphIds = new Set([...selectedIds, ...edges.flatMap((edge) => [edge.sourceId, edge.targetId])]); + const graphNodes = this.deps.repos.memories.getMany([...graphIds]).map((memory) => ({ + ...this.deps.repos.memories.toListItem(memory), + ...(selectedIds.has(memory.id) ? {} : { external: true }) + })); const section = (title: string, items: MemoryListItem[]) => `## ${title}\n${items.length ? items.map((item) => `- ${item.summary || item.title} (${item.id})`).join("\n") : "- 暂无"}`; const markdown = [ `# Project Memory Pack: ${context.namespace.projectId ?? context.namespace.workspaceId ?? "unscoped"}`, @@ -548,7 +563,7 @@ export class PanelReadModel { `## 最近任务\n${recentTasks.length ? recentTasks.map((task) => `- ${task.title} (${task.id})`).join("\n") : "- 暂无"}`, section("用户偏好", userPreferences) ].join("\n\n"); - return { namespace: context.namespace, conventions, commands, architectureFacts, recentTasks, userPreferences, markdown, generatedAt: this.now() }; + return { namespace: context.namespace, conventions, commands, architectureFacts, recentTasks, userPreferences, graph: { nodes: graphNodes, edges }, markdown, generatedAt: this.now() }; } projectContextPacks(input: RequestEnvelope & { userId?: string } = {}) { diff --git a/Memory/src/storage/repositories.ts b/Memory/src/storage/repositories.ts index bce03962a..fd2dc518b 100644 --- a/Memory/src/storage/repositories.ts +++ b/Memory/src/storage/repositories.ts @@ -361,6 +361,14 @@ export interface MemoryRelationRecord { createdAt: string; } +export class MemoryVersionConflictError extends Error { + readonly code = "memory_version_conflict"; + constructor(readonly memoryId: string, readonly expectedVersion: number, readonly actualVersion: number) { + super(`memory version conflict for ${memoryId}: expected ${expectedVersion}, actual ${actualVersion}`); + this.name = "MemoryVersionConflictError"; + } +} + interface SqlApiLogRow { id: number; tool_name: ApiLogRecord["toolName"]; @@ -400,15 +408,15 @@ export class MemoryRepository { return attachMemoryVectors(prepared.memory, prepared.vectors); } - update(memory: MemoryRow): MemoryRow { - return this.updateRow(memory, true); + update(memory: MemoryRow, expectedVersion?: number): MemoryRow { + return this.updateRow(memory, true, expectedVersion); } updateMaintenance(memory: MemoryRow): MemoryRow { return this.updateRow(memory, false); } - private updateRow(memory: MemoryRow, bumpVersion: boolean): MemoryRow { + private updateRow(memory: MemoryRow, bumpVersion: boolean, expectedVersion?: number): MemoryRow { const existing = this.get(memory.id); if (!existing) { throw new Error(`memory not found: ${memory.id}`); @@ -418,7 +426,7 @@ export class MemoryRepository { ...prepared.memory, version: bumpVersion ? memory.version + 1 : existing.version }; - this.db + const result = this.db .prepare( `UPDATE memories SET timeline = @timeline, @@ -440,9 +448,13 @@ export class MemoryRepository { version = @version, updated_at = @updatedAt, deleted_at = @deletedAt - WHERE id = @id` + WHERE id = @id AND (@expectedVersion IS NULL OR version = @expectedVersion)` ) - .run(memoryToSql(updated)); + .run({ ...memoryToSql(updated), expectedVersion: expectedVersion ?? null }); + if (result.changes === 0 && expectedVersion !== undefined) { + const actual = this.get(memory.id)?.version ?? 0; + throw new MemoryVersionConflictError(memory.id, expectedVersion, actual); + } const mergedVectors = mergeMemoryVectors( attachedMemoryVectorEntries(existing), prepared.vectorUpdates @@ -2092,6 +2104,20 @@ export class RuntimeRepository { })); } + listMemoryChanges(memoryId: string, limit = 100): ChangeLogRecord[] { + const rows = this.db.prepare( + `SELECT * FROM memory_change_log WHERE memory_id = ? ORDER BY seq DESC LIMIT ?` + ).all(memoryId, Math.max(1, Math.min(limit, 500))) as SqlChangeRow[]; + return rows.map((row) => ({ + seq: row.seq, memoryId: row.memory_id, namespaceId: row.namespace_id ?? undefined, + kind: row.kind ?? undefined, op: row.op ?? undefined, entityId: row.entity_id ?? undefined, + userId: row.user_id, changeType: row.change_type, version: row.version ?? undefined, + before: row.before_json ? parseJson(row.before_json, undefined) : undefined, + after: row.after_json ? parseJson(row.after_json, undefined) : undefined, + source: row.source, createdAt: row.created_at + })); + } + saveIdempotency(key: string, requestHash: string, response: unknown, createdAt = nowIso()): void { this.db .prepare( diff --git a/Memory/src/types.ts b/Memory/src/types.ts index bef7e77ab..317a937c4 100644 --- a/Memory/src/types.ts +++ b/Memory/src/types.ts @@ -444,6 +444,7 @@ export interface MemoryImportRequest extends RequestEnvelope { export interface MemoryGovernanceRequest extends RequestEnvelope { reason?: string; + version?: number; } export interface MemoryMarkdownExportRequest extends RequestEnvelope { diff --git a/Memory/src/viewer/static.ts b/Memory/src/viewer/static.ts index 788d4fcbe..5db3376e8 100644 --- a/Memory/src/viewer/static.ts +++ b/Memory/src/viewer/static.ts @@ -293,18 +293,18 @@ export function memoryPanelHtml(): string { .context-item:hover { background: var(--surface-hover); } .context-item strong, .context-item span { display: block; overflow-wrap: anywhere; } .context-item span { margin-top: 3px; color: var(--muted); font-size: 10px; } - .context-graph { min-width: 760px; padding: 8px 4px; } - .context-graph-pack { display: grid; grid-template-columns: minmax(150px, .7fr) minmax(170px, .8fr) minmax(320px, 2fr); gap: 28px; align-items: center; padding: 18px 0; } + .context-graph { min-width: 680px; padding: 8px 4px; } + .context-graph-pack { padding: 18px 0; } .context-graph-pack + .context-graph-pack { border-top: 1px solid var(--line); } - .graph-root, .graph-section, .graph-node { position: relative; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); } - .graph-root { padding: 12px; border-color: var(--accent); background: var(--accent-soft); font-weight: 750; } - .graph-branches, .graph-leaves { display: grid; gap: 8px; } - .graph-section { padding: 8px 10px; color: var(--ink-secondary); font-size: 11px; } - .graph-node { width: 100%; min-height: 34px; padding: 7px 9px; text-align: left; } - .graph-root::after, .graph-section::before, .graph-section::after, .graph-node::before { content: ""; position: absolute; top: 50%; height: 1px; background: var(--line-strong); } - .graph-root::after { left: 100%; width: 28px; } - .graph-section::before, .graph-node::before { right: 100%; width: 28px; } - .graph-section::after { left: 100%; width: 28px; } + .graph-title { margin: 0 0 10px; } + .graph-layout { display: grid; grid-template-columns: minmax(260px, 1fr) minmax(340px, 1.3fr); gap: 14px; align-items: start; } + .graph-nodes, .graph-edges { display: grid; gap: 7px; } + .graph-node, .graph-edge { min-height: 38px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); } + .graph-node { width: 100%; text-align: left; } + .graph-node.external { border-style: dashed; color: var(--muted); } + .graph-edge { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); gap: 8px; align-items: center; font-size: 10px; } + .graph-edge strong { overflow-wrap: anywhere; } + .graph-edge span { color: var(--accent); font-weight: 750; text-align: center; } .context-markdown { max-height: none; min-height: 260px; } .dialog-screen { position: fixed; inset: 0; z-index: 45; display: grid; place-items: center; padding: 18px; background: rgba(10, 18, 15, .48); } .dialog { width: min(680px, 100%); max-height: min(760px, calc(100vh - 36px)); overflow: auto; border: 1px solid var(--line); border-radius: 8px; background: var(--surface-raised); box-shadow: var(--shadow); } @@ -314,6 +314,11 @@ export function memoryPanelHtml(): string { .dialog label { display: grid; gap: 6px; font-weight: 700; } .dialog textarea { width: 100%; min-height: 220px; resize: vertical; padding: 10px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--ink); font: 12px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } .context-detail-body { white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.6; } + .history-list { display: grid; gap: 8px; } + .history-item { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; align-items: center; padding: 9px 0; border-top: 1px solid var(--line); } + .history-item:first-child { border-top: 0; } + .history-item strong, .history-item span { display: block; overflow-wrap: anywhere; } + .history-item span { margin-top: 3px; color: var(--muted); font-size: 10px; } .auth-screen { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; padding: 18px; background: color-mix(in srgb, var(--canvas) 92%, transparent); backdrop-filter: blur(14px); } .auth-dialog { width: min(420px, 100%); border: 1px solid var(--line); border-radius: 8px; background: var(--surface-raised); box-shadow: var(--shadow); padding: 20px; } .auth-dialog .brand { padding: 0; margin-bottom: 18px; } @@ -632,7 +637,7 @@ export function memoryPanelHtml(): string { if (!response.ok) { if (response.status === 401) showAuth("令牌无效或已过期"); const message = body.error && body.error.message ? body.error.message : text || response.statusText; - throw new Error(message); + const error = new Error(message); error.status = response.status; error.code = body.error && body.error.code; throw error; } return body; } @@ -862,10 +867,11 @@ export function memoryPanelHtml(): string { function renderContextGraph(packs) { $("contextPackGraph").innerHTML = packs.length ? '
    ' + packs.map((pack) => { - const populated = contextPackSections.map(([key, label]) => [key, label, Array.isArray(pack[key]) ? pack[key] : []]).filter((section) => section[2].length); - const branches = populated.map(([, label, items]) => '
    ' + esc(label) + ' · ' + formatNumber(items.length) + '
    ').join(""); - const leaves = populated.map(([key, , items]) => '
    ' + items.map((item) => '').join("") + '
    ').join(""); - return '
    ' + esc(namespaceLabel(pack.namespace)) + '
    ' + (branches || '
    暂无节点
    ') + '
    ' + leaves + '
    '; + const graph = pack.graph || { nodes: [], edges: [] }; + const nodesById = new Map((graph.nodes || []).map((node) => [node.id, node])); + const nodes = (graph.nodes || []).map((node) => '').join(""); + const edges = (graph.edges || []).map((edge) => '
    ' + esc(contextItemTitle(nodesById.get(edge.sourceId) || { id: edge.sourceId })) + '' + (edge.relation === "supersedes" ? '取代 →' : '来源 →') + '' + esc(contextItemTitle(nodesById.get(edge.targetId) || { id: edge.targetId })) + '
    ').join(""); + return '

    ' + esc(namespaceLabel(pack.namespace)) + '

    ' + (nodes || '
    暂无节点
    ') + '
    ' + (edges || '
    暂无来源或取代关系
    ') + '
    '; }).join("") + '
    ' : '
    暂无项目上下文
    '; bindContextNodes($("contextPackGraph")); } @@ -909,12 +915,17 @@ export function memoryPanelHtml(): string { $("contextMemoryDetailActions").classList.add("hidden"); showContextDialog(); try { - const data = await api("/api/v1/memory/" + encodeURIComponent(id)); + const [data, history] = await Promise.all([ + api("/api/v1/memory/" + encodeURIComponent(id)), + api("/api/v1/memory/" + encodeURIComponent(id) + "/history?limit=100") + ]); const item = data.item || {}; state.contextMemory = item; + state.contextMemoryHistory = history.items || []; $("contextMemoryDialogTitle").textContent = contextItemTitle(item); $("contextMemoryDialogId").textContent = item.id || id; - $("contextMemoryDetail").innerHTML = '
    ' + esc(item.body || item.summary || "暂无内容") + '
    ' + ((item.tags || []).map((tag) => '' + esc(tag) + '').join("") || '无标签') + '
    来源 ' + esc(valueAt(item, ["metadata", "source"], "unknown")) + ' · v' + esc(item.version || 1) + ' · ' + esc(formatDate(item.updatedAt, true)) + '
    '; + $("contextMemoryDetail").innerHTML = '
    ' + esc(item.body || item.summary || "暂无内容") + '
    ' + ((item.tags || []).map((tag) => '' + esc(tag) + '').join("") || '无标签') + '
    来源 ' + esc(valueAt(item, ["metadata", "source"], "unknown")) + ' · v' + esc(item.version || 1) + ' · ' + esc(formatDate(item.updatedAt, true)) + '

    版本历史

    ' + renderMemoryHistory(history.items || [], item.version) + '
    '; + for (const button of $("contextMemoryDetail").querySelectorAll("button[data-restore-version]")) button.onclick = () => restoreContextMemory(Number(button.dataset.restoreVersion)).catch(showError); $("contextMemoryDetailActions").classList.remove("hidden"); } catch (error) { closeContextDialog(); showError(error); } } @@ -944,13 +955,39 @@ export function memoryPanelHtml(): string { const button = $("saveContextMemory"); button.disabled = true; try { - await api("/api/v1/memory/" + encodeURIComponent(item.id) + "/edit", { method: "POST", body: JSON.stringify({ requestId: "panel-" + Date.now(), adapterId: "memory-console", reason: "edited from generated context pack", title: $("contextMemoryTitle").value, tags: $("contextMemoryTags").value.split(/[,,]/).map((tag) => tag.trim()).filter(Boolean), content: $("contextMemoryBody").value }) }); + await api("/api/v1/memory/" + encodeURIComponent(item.id) + "/edit", { method: "POST", body: JSON.stringify({ requestId: "panel-" + Date.now(), adapterId: "memory-console", reason: "edited from generated context pack", version: item.version, title: $("contextMemoryTitle").value, tags: $("contextMemoryTags").value.split(/[,,]/).map((tag) => tag.trim()).filter(Boolean), content: $("contextMemoryBody").value }) }); closeContextDialog(); await Promise.all([loadDashboard(), loadMemories()]); showToast("原始记忆已更新,上下文包已重新生成"); + } catch (error) { + if (error.status === 409) { await openContextMemory(item.id); showToast("记忆已被其他 Agent 修改,已加载最新版本"); } + throw error; } finally { button.disabled = false; } } + function renderMemoryHistory(items, currentVersion) { + return items.length ? items.map((entry) => { + const snapshot = entry.after || {}; + const title = valueAt(snapshot, ["info", "title"], snapshot.memoryValue || entry.changeType || "变更"); + const isCurrent = Number(entry.version) === Number(currentVersion); + return '
    v' + esc(entry.version || "-") + ' · ' + esc(title) + '' + esc(entry.changeType || "updated") + ' · ' + esc(entry.source || "unknown") + ' · ' + esc(formatDate(entry.createdAt, true)) + '
    '; + }).join("") : '
    暂无版本记录
    '; + } + + async function restoreContextMemory(targetVersion) { + const item = state.contextMemory; + if (!item || !confirm("恢复到 v" + targetVersion + "?当前内容会保留在版本历史中。")) return; + try { + await api("/api/v1/memory/" + encodeURIComponent(item.id) + "/history/" + targetVersion + "/restore", { method: "POST", body: JSON.stringify({ requestId: "panel-" + Date.now(), adapterId: "memory-console", reason: "restored from memory console", version: item.version }) }); + await Promise.all([loadDashboard(), loadMemories()]); + await openContextMemory(item.id); + showToast("已恢复到 v" + targetVersion); + } catch (error) { + if (error.status === 409) { await openContextMemory(item.id); showToast("记忆已被其他 Agent 修改,已加载最新版本"); } + throw error; + } + } + function setContextPackView(view) { state.contextPackView = view; for (const name of ["outline", "graph", "markdown"]) { diff --git a/Memory/tests/contract/memory-rest-service.test.ts b/Memory/tests/contract/memory-rest-service.test.ts index e63e83351..288438bbd 100644 --- a/Memory/tests/contract/memory-rest-service.test.ts +++ b/Memory/tests/contract/memory-rest-service.test.ts @@ -1156,6 +1156,7 @@ describe("MemoryService / REST contract", () => { method: "POST", headers: { ...headers, "content-type": "application/json" }, body: JSON.stringify({ + version: 1, title: "Architecture API boundary", content: "Architecture module now uses an API repository boundary.", tags: ["architecture", "api"], @@ -1181,6 +1182,44 @@ describe("MemoryService / REST contract", () => { expect(afterPackBody.packs[0]?.architectureFacts).not.toContainEqual(expect.objectContaining({ title: "Architecture service boundary" })); expect(afterPackBody.packs[0]?.markdown).toContain("Architecture module now uses an API repository boundary."); expect(afterPackBody.packs[0]?.markdown).not.toContain("Architecture module uses a repository boundary."); + + const staleEdit = await fetch(`${baseUrl}/memory/${encodeURIComponent(added.id)}/edit`, { + method: "POST", + headers: { ...headers, "content-type": "application/json" }, + body: JSON.stringify({ version: 1, title: "Stale title", content: "This must not overwrite version 2.", tags: [] }) + }); + expect(staleEdit.status).toBe(409); + expect(await staleEdit.json()).toMatchObject({ error: { code: "conflict" } }); + + const history = await fetch(`${baseUrl}/memory/${encodeURIComponent(added.id)}/history`, { headers }); + expect(history.status).toBe(200); + expect(await history.json()).toMatchObject({ + id: added.id, + currentVersion: 2, + items: expect.arrayContaining([ + expect.objectContaining({ version: 1 }), + expect.objectContaining({ version: 2, changeType: "content_edit" }) + ]) + }); + + const restore = await fetch(`${baseUrl}/memory/${encodeURIComponent(added.id)}/history/1/restore`, { + method: "POST", + headers: { ...headers, "content-type": "application/json" }, + body: JSON.stringify({ version: 2, reason: "restore original wording" }) + }); + expect(restore.status).toBe(200); + expect(await restore.json()).toMatchObject({ ok: true, id: added.id, version: 3, restoredVersion: 1 }); + + const restoredDetail = await fetch(`${baseUrl}/memory/${encodeURIComponent(added.id)}`, { headers }); + expect(await restoredDetail.json()).toMatchObject({ + item: { title: "Architecture service boundary", body: "Architecture module uses a repository boundary.", version: 3 } + }); + + const restoredHistory = await fetch(`${baseUrl}/memory/${encodeURIComponent(added.id)}/history`, { headers }); + expect(await restoredHistory.json()).toMatchObject({ + currentVersion: 3, + items: expect.arrayContaining([expect.objectContaining({ version: 3, changeType: "restore" })]) + }); }); db.close(); }); diff --git a/Memory/tests/service/governance/provenance-supersession.test.ts b/Memory/tests/service/governance/provenance-supersession.test.ts index 81ee6eb83..3daa4d777 100644 --- a/Memory/tests/service/governance/provenance-supersession.test.ts +++ b/Memory/tests/service/governance/provenance-supersession.test.ts @@ -113,6 +113,7 @@ describe("MemoryService / governance / provenance and supersession", () => { layer: "L2", title: "Current migration policy", content: "Use the current migration procedure and verify the SQLite backup.", + tags: ["architecture"], adapterId: "pi-memory-extension", requestId: "add-new", supersedesMemoryId: old.id, @@ -153,6 +154,16 @@ describe("MemoryService / governance / provenance and supersession", () => { }) ]); + const contextPack = service.projectContextPack({ namespace }); + expect(contextPack.graph.nodes).toEqual(expect.arrayContaining([ + expect.objectContaining({ id: replacement.id }), + expect.objectContaining({ id: old.id, external: true }) + ])); + expect(contextPack.graph.edges).toEqual(expect.arrayContaining([ + expect.objectContaining({ sourceId: old.id, targetId: replacement.id, relation: "source" }), + expect.objectContaining({ sourceId: replacement.id, targetId: old.id, relation: "supersedes" }) + ])); + const relations = db.db.prepare( `SELECT source_memory_id, target_memory_id, relation, reason FROM memory_relations diff --git a/deploy/memory/README.md b/deploy/memory/README.md index 843142ac3..c605d8387 100644 --- a/deploy/memory/README.md +++ b/deploy/memory/README.md @@ -10,6 +10,15 @@ Build or update the image from the source repository: docker build -f /root/memmy-agent/Memory/Dockerfile -t memmy-memory:local /root/memmy-agent ``` +Versioned multi-architecture images are published to GitHub Container Registry +by `.github/workflows/docker-publish.yml`. Pushing a `vX.Y.Z` tag publishes the +matching version and immutable commit tag. The workflow can also be dispatched +manually with an explicit image tag. The GitHub Release workflow calls the same +reusable workflow with its already verified release commit. It uses the repository `GITHUB_TOKEN`, so +the workflow requires package write permission but no separate registry secret. +Before pushing, the workflow starts the built image with the production security +flags and verifies health, write, edit, history, and restore behavior. + Start and inspect the service from the deployment directory: ```bash diff --git a/package-lock.json b/package-lock.json index e53ec1f11..58b7dadc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,12 +18,14 @@ ], "devDependencies": { "@eslint/js": "^9.39.4", + "@huggingface/transformers": "^3.8.0", "@types/node": "^25.9.1", "@vitest/coverage-v8": "^4.1.7", "concurrently": "^9.2.1", "eslint": "^9.39.4", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", + "sharp": "0.35.3", "tsx": "^4.22.3", "typescript": "^6.0.3", "typescript-eslint": "^8.60.0", @@ -96,7 +98,7 @@ "yaml": "^2.9.0" }, "devDependencies": { - "electron": "38.4.0", + "electron": "43.3.0", "electron-builder": "^26.0.12" } }, @@ -107,119 +109,6 @@ "@memmy/local-api-contracts": "0.0.0" } }, - "App/shell/desktop/node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "App/shell/desktop/node_modules/@types/node": { - "version": "22.19.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.20.tgz", - "integrity": "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "App/shell/desktop/node_modules/electron": { - "version": "38.4.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-38.4.0.tgz", - "integrity": "sha512-9CsXKbGf2qpofVe2pQYSgom2E//zLDJO2rGLLbxgy9tkdTOs7000Gte+d/PUtzLjI/DS95jDK0ojYAeqjLvpYg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^22.7.7", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js" - }, - "engines": { - "node": ">= 12.20.55" - } - }, - "App/shell/desktop/node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "App/shell/desktop/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "App/shell/desktop/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "App/shell/desktop/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" - } - }, - "App/shell/desktop/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "App/shell/desktop/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, "Memory": { "name": "@memmy/memory", "version": "1.0.4", @@ -227,6 +116,7 @@ "@huggingface/transformers": "^3.8.0", "better-sqlite3": "^12.6.3", "dotenv": "^16.6.1", + "sharp": "0.35.3", "sqlite-vec": "0.1.9", "yaml": "^2.9.0" }, @@ -262,8 +152,6 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -272,8 +160,6 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -282,8 +168,6 @@ }, "node_modules/@babel/parser": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { @@ -298,8 +182,6 @@ }, "node_modules/@babel/runtime": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -307,8 +189,6 @@ }, "node_modules/@babel/types": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { @@ -321,18 +201,22 @@ }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, "license": "MIT", "engines": { "node": ">=18" } }, + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.5", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/@electron/asar": { "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", - "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", "dev": true, "license": "MIT", "dependencies": { @@ -349,8 +233,6 @@ }, "node_modules/@electron/asar/node_modules/commander": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", "dev": true, "license": "MIT", "engines": { @@ -359,8 +241,6 @@ }, "node_modules/@electron/fuses": { "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", - "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", "dev": true, "license": "MIT", "dependencies": { @@ -374,8 +254,6 @@ }, "node_modules/@electron/fuses/node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -388,10 +266,47 @@ "node": ">=10" } }, + "node_modules/@electron/get": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^3.0.0", + "graceful-fs": "^4.2.11", + "progress": "^2.0.3", + "semver": "^7.6.3", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=22.12.0" + }, + "optionalDependencies": { + "undici": "^7.24.4" + } + }, + "node_modules/@electron/get/node_modules/env-paths": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@electron/get/node_modules/undici": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/@electron/notarize": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", - "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", "dev": true, "license": "MIT", "dependencies": { @@ -405,8 +320,6 @@ }, "node_modules/@electron/notarize/node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -421,8 +334,6 @@ }, "node_modules/@electron/osx-sign": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", - "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -443,8 +354,6 @@ }, "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", "dev": true, "license": "MIT", "engines": { @@ -456,8 +365,6 @@ }, "node_modules/@electron/rebuild": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", - "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", "dev": true, "license": "MIT", "dependencies": { @@ -476,9 +383,7 @@ } }, "node_modules/@electron/rebuild/node_modules/node-abi": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.31.0.tgz", - "integrity": "sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==", + "version": "4.33.0", "dev": true, "license": "MIT", "dependencies": { @@ -490,8 +395,6 @@ }, "node_modules/@electron/universal": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", - "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", "dev": true, "license": "MIT", "dependencies": { @@ -508,9 +411,7 @@ } }, "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.4", "dev": true, "license": "MIT", "dependencies": { @@ -518,9 +419,7 @@ } }, "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", - "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "version": "11.4.0", "dev": true, "license": "MIT", "dependencies": { @@ -534,8 +433,6 @@ }, "node_modules/@electron/universal/node_modules/minimatch": { "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { @@ -550,8 +447,6 @@ }, "node_modules/@electron/windows-sign": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", "dev": true, "license": "BSD-2-Clause", "optional": true, @@ -571,9 +466,7 @@ } }, "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", - "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "version": "11.4.0", "dev": true, "license": "MIT", "optional": true, @@ -591,6 +484,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -600,8 +494,7 @@ }, "node_modules/@emnapi/runtime": { "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -612,6 +505,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -619,9 +513,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], @@ -635,9 +529,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], @@ -651,9 +545,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], @@ -667,9 +561,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], @@ -683,9 +577,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], @@ -699,9 +593,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], @@ -715,9 +609,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], @@ -731,9 +625,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], @@ -747,9 +641,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], @@ -763,9 +657,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], @@ -779,9 +673,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], @@ -795,9 +689,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], @@ -811,9 +705,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], @@ -827,9 +721,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], @@ -843,9 +737,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], @@ -859,9 +753,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], @@ -875,9 +769,7 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "version": "0.28.2", "cpu": [ "x64" ], @@ -891,9 +783,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], @@ -907,9 +799,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], @@ -923,9 +815,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], @@ -939,9 +831,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], @@ -955,9 +847,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], @@ -971,9 +863,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], @@ -987,9 +879,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], @@ -1003,9 +895,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], @@ -1019,9 +911,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -1036,8 +928,6 @@ }, "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": { @@ -1055,8 +945,6 @@ }, "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": { @@ -1068,8 +956,6 @@ }, "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": { @@ -1078,8 +964,6 @@ }, "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": { @@ -1093,8 +977,6 @@ }, "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": { @@ -1106,8 +988,6 @@ }, "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": { @@ -1119,8 +999,6 @@ }, "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": { @@ -1143,8 +1021,6 @@ }, "node_modules/@eslint/eslintrc/node_modules/ajv": { "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1160,15 +1036,11 @@ }, "node_modules/@eslint/eslintrc/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/@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": { @@ -1180,8 +1052,6 @@ }, "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": { @@ -1190,8 +1060,6 @@ }, "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": { @@ -1204,8 +1072,6 @@ }, "node_modules/@fastify/ajv-compiler": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", - "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", "funding": [ { "type": "github", @@ -1225,8 +1091,6 @@ }, "node_modules/@fastify/error": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", - "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", "funding": [ { "type": "github", @@ -1241,8 +1105,6 @@ }, "node_modules/@fastify/fast-json-stringify-compiler": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", - "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", "funding": [ { "type": "github", @@ -1260,8 +1122,6 @@ }, "node_modules/@fastify/forwarded": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", - "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", "funding": [ { "type": "github", @@ -1276,8 +1136,6 @@ }, "node_modules/@fastify/merge-json-schemas": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", - "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", "funding": [ { "type": "github", @@ -1295,8 +1153,6 @@ }, "node_modules/@fastify/proxy-addr": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", - "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", "funding": [ { "type": "github", @@ -1315,8 +1171,6 @@ }, "node_modules/@hapi/address": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", - "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1328,29 +1182,21 @@ }, "node_modules/@hapi/formula": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", - "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@hapi/hoek": { "version": "11.0.7", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", - "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@hapi/pinpoint": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", - "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@hapi/tlds": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz", - "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -1359,8 +1205,6 @@ }, "node_modules/@hapi/topo": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", - "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1368,12 +1212,10 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "version": "2.1.0", "license": "MIT", "engines": { - "node": ">=18.14.1" + "node": ">=20" }, "peerDependencies": { "hono": "^4" @@ -1381,8 +1223,6 @@ }, "node_modules/@huggingface/jinja": { "version": "0.5.9", - "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.9.tgz", - "integrity": "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw==", "license": "MIT", "engines": { "node": ">=18" @@ -1390,8 +1230,6 @@ }, "node_modules/@huggingface/transformers": { "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.8.1.tgz", - "integrity": "sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==", "license": "Apache-2.0", "dependencies": { "@huggingface/jinja": "^0.5.3", @@ -1402,8 +1240,6 @@ }, "node_modules/@humanfs/core": { "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1415,8 +1251,6 @@ }, "node_modules/@humanfs/node": { "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1430,8 +1264,6 @@ }, "node_modules/@humanfs/types": { "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1440,8 +1272,6 @@ }, "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": { @@ -1454,8 +1284,6 @@ }, "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": { @@ -1468,17 +1296,15 @@ }, "node_modules/@img/colour": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", "cpu": [ "arm64" ], @@ -1488,19 +1314,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.3.2" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", "cpu": [ "x64" ], @@ -1510,19 +1336,38 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", "cpu": [ "arm64" ], @@ -1536,9 +1381,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", "cpu": [ "x64" ], @@ -1552,12 +1397,15 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1568,12 +1416,15 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1584,12 +1435,15 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1600,12 +1454,15 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1616,12 +1473,15 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1632,12 +1492,13 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.3.2", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1648,12 +1509,15 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1664,12 +1528,15 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1680,204 +1547,248 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.3.2" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.3.2" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "@img/sharp-libvips-linux-ppc64": "1.3.2" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "@img/sharp-libvips-linux-riscv64": "1.3.2" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.3.2" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.35.3", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.3.2" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "version": "0.35.3", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-wasm32/node_modules/@emnapi/runtime": { + "version": "1.11.3", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", "cpu": [ "wasm32" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@img/sharp-wasm32": "0.35.3" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", "cpu": [ "arm64" ], @@ -1887,16 +1798,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", "cpu": [ "ia32" ], @@ -1906,16 +1817,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", "cpu": [ "x64" ], @@ -1925,7 +1836,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -1933,8 +1844,6 @@ }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -1945,8 +1854,6 @@ }, "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": { @@ -1955,15 +1862,11 @@ }, "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": { @@ -1973,8 +1876,6 @@ }, "node_modules/@malept/cross-spawn-promise": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", - "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", "dev": true, "funding": [ { @@ -1996,8 +1897,6 @@ }, "node_modules/@malept/flatpak-bundler": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", - "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2012,8 +1911,6 @@ }, "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2055,12 +1952,10 @@ "link": true }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "version": "1.30.0", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9", + "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -2095,27 +1990,29 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", + "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" } }, "node_modules/@noble/hashes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", - "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "version": "2.3.0", "dev": true, "license": "MIT", "engines": { @@ -2126,9 +2023,7 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.132.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", - "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "version": "0.143.0", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -2136,8 +2031,6 @@ }, "node_modules/@peculiar/asn1-schema": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.7.0.tgz", - "integrity": "sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg==", "dev": true, "license": "MIT", "dependencies": { @@ -2148,8 +2041,6 @@ }, "node_modules/@peculiar/json-schema": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", - "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", "dev": true, "license": "MIT", "dependencies": { @@ -2161,8 +2052,6 @@ }, "node_modules/@peculiar/utils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", - "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2171,8 +2060,6 @@ }, "node_modules/@peculiar/webcrypto": { "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", - "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2188,38 +2075,26 @@ }, "node_modules/@pinojs/redact": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", - "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", "license": "MIT" }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", - "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", - "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.1" @@ -2227,38 +2102,24 @@ }, "node_modules/@protobufjs/float": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", - "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", - "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", "cpu": [ "arm64" ], @@ -2272,9 +2133,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", "cpu": [ "arm64" ], @@ -2288,9 +2149,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", - "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", "cpu": [ "x64" ], @@ -2304,9 +2165,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", - "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", "cpu": [ "x64" ], @@ -2320,9 +2181,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", - "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", "cpu": [ "arm" ], @@ -2336,12 +2197,15 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", - "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2352,12 +2216,15 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", - "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2368,12 +2235,15 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", - "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2384,12 +2254,15 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", - "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2400,12 +2273,13 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", - "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "version": "1.2.3", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2416,12 +2290,15 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", - "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2432,9 +2309,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", - "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", "cpu": [ "arm64" ], @@ -2447,28 +2324,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", - "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", - "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", "cpu": [ "arm64" ], @@ -2482,9 +2341,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", - "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", "cpu": [ "x64" ], @@ -2499,21 +2358,15 @@ }, "node_modules/@rolldown/pluginutils": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "license": "MIT" }, "node_modules/@rtsao/scc": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true, "license": "MIT" }, "node_modules/@sindresorhus/is": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "dev": true, "license": "MIT", "engines": { @@ -2525,15 +2378,11 @@ }, "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/@szmarczak/http-timer": { "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", "dev": true, "license": "MIT", "dependencies": { @@ -2544,9 +2393,10 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2555,8 +2405,6 @@ }, "node_modules/@types/better-sqlite3": { "version": "7.6.13", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", - "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", "dev": true, "license": "MIT", "dependencies": { @@ -2565,8 +2413,6 @@ }, "node_modules/@types/cacheable-request": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", "dev": true, "license": "MIT", "dependencies": { @@ -2578,8 +2424,6 @@ }, "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": { @@ -2589,14 +2433,10 @@ }, "node_modules/@types/d3-color": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, "node_modules/@types/d3-drag": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", - "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", "license": "MIT", "dependencies": { "@types/d3-selection": "*" @@ -2604,8 +2444,6 @@ }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "license": "MIT", "dependencies": { "@types/d3-color": "*" @@ -2613,14 +2451,10 @@ }, "node_modules/@types/d3-selection": { "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", - "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", "license": "MIT" }, "node_modules/@types/d3-transition": { "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", - "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", "license": "MIT", "dependencies": { "@types/d3-selection": "*" @@ -2628,8 +2462,6 @@ }, "node_modules/@types/d3-zoom": { "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", - "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", "license": "MIT", "dependencies": { "@types/d3-interpolate": "*", @@ -2638,8 +2470,6 @@ }, "node_modules/@types/debug": { "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", "license": "MIT", "dependencies": { "@types/ms": "*" @@ -2647,21 +2477,15 @@ }, "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.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "license": "MIT" }, "node_modules/@types/estree-jsx": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", "license": "MIT", "dependencies": { "@types/estree": "*" @@ -2669,8 +2493,6 @@ }, "node_modules/@types/fs-extra": { "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", "dev": true, "license": "MIT", "dependencies": { @@ -2679,8 +2501,6 @@ }, "node_modules/@types/hast": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "license": "MIT", "dependencies": { "@types/unist": "*" @@ -2688,35 +2508,25 @@ }, "node_modules/@types/http-cache-semantics": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", "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/json5": { "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true, "license": "MIT" }, "node_modules/@types/katex": { "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", - "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", "license": "MIT" }, "node_modules/@types/keyv": { "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", "dev": true, "license": "MIT", "dependencies": { @@ -2725,8 +2535,6 @@ }, "node_modules/@types/mdast": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "license": "MIT", "dependencies": { "@types/unist": "*" @@ -2734,14 +2542,10 @@ }, "node_modules/@types/ms": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, "node_modules/@types/node": { "version": "25.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", "license": "MIT", "dependencies": { "undici-types": ">=7.24.0 <7.24.7" @@ -2749,14 +2553,10 @@ }, "node_modules/@types/prismjs": { "version": "1.26.6", - "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", - "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", "license": "MIT" }, "node_modules/@types/proper-lockfile": { "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.4.tgz", - "integrity": "sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2765,8 +2565,6 @@ }, "node_modules/@types/react": { "version": "19.2.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", - "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2774,8 +2572,6 @@ }, "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==", "devOptional": true, "license": "MIT", "peerDependencies": { @@ -2784,8 +2580,6 @@ }, "node_modules/@types/react-syntax-highlighter": { "version": "15.5.13", - "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", - "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", "license": "MIT", "dependencies": { "@types/react": "*" @@ -2793,8 +2587,6 @@ }, "node_modules/@types/responselike": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "dev": true, "license": "MIT", "dependencies": { @@ -2803,49 +2595,28 @@ }, "node_modules/@types/retry": { "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "dev": true, "license": "MIT" }, "node_modules/@types/unist": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, "node_modules/@types/whatwg-mimetype": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", - "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", "dev": true, "license": "MIT" }, "node_modules/@types/ws": { "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "@types/node": "*" } }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz", - "integrity": "sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==", "dev": true, "license": "MIT", "dependencies": { @@ -2872,9 +2643,7 @@ } }, "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==", + "version": "7.0.6", "dev": true, "license": "MIT", "engines": { @@ -2883,8 +2652,6 @@ }, "node_modules/@typescript-eslint/parser": { "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.0.tgz", - "integrity": "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==", "dev": true, "license": "MIT", "dependencies": { @@ -2908,8 +2675,6 @@ }, "node_modules/@typescript-eslint/project-service": { "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.0.tgz", - "integrity": "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==", "dev": true, "license": "MIT", "dependencies": { @@ -2930,8 +2695,6 @@ }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz", - "integrity": "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==", "dev": true, "license": "MIT", "dependencies": { @@ -2948,8 +2711,6 @@ }, "node_modules/@typescript-eslint/tsconfig-utils": { "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz", - "integrity": "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==", "dev": true, "license": "MIT", "engines": { @@ -2965,8 +2726,6 @@ }, "node_modules/@typescript-eslint/type-utils": { "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz", - "integrity": "sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==", "dev": true, "license": "MIT", "dependencies": { @@ -2990,8 +2749,6 @@ }, "node_modules/@typescript-eslint/types": { "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.0.tgz", - "integrity": "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==", "dev": true, "license": "MIT", "engines": { @@ -3004,8 +2761,6 @@ }, "node_modules/@typescript-eslint/typescript-estree": { "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz", - "integrity": "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==", "dev": true, "license": "MIT", "dependencies": { @@ -3032,8 +2787,6 @@ }, "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": { @@ -3041,26 +2794,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -3071,8 +2820,6 @@ }, "node_modules/@typescript-eslint/utils": { "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.0.tgz", - "integrity": "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==", "dev": true, "license": "MIT", "dependencies": { @@ -3095,8 +2842,6 @@ }, "node_modules/@typescript-eslint/visitor-keys": { "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz", - "integrity": "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==", "dev": true, "license": "MIT", "dependencies": { @@ -3113,8 +2858,6 @@ }, "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": { @@ -3126,8 +2869,6 @@ }, "node_modules/@ungap/structured-clone": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", - "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", "license": "ISC" }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { @@ -3236,6 +2977,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3250,6 +2994,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3264,6 +3011,9 @@ "loong64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3278,6 +3028,9 @@ "loong64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3292,6 +3045,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3306,6 +3062,9 @@ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3320,6 +3079,9 @@ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3334,6 +3096,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3342,12 +3107,13 @@ }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", - "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3356,12 +3122,13 @@ }, "node_modules/@unrs/resolver-binding-linux-x64-musl": { "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", - "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3445,8 +3212,6 @@ }, "node_modules/@vitejs/plugin-react": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", - "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", "license": "MIT", "dependencies": { "@rolldown/pluginutils": "^1.0.0" @@ -3470,8 +3235,6 @@ }, "node_modules/@vitest/coverage-v8": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.7.tgz", - "integrity": "sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3501,8 +3264,6 @@ }, "node_modules/@vitest/expect": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", - "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", "dev": true, "license": "MIT", "dependencies": { @@ -3519,8 +3280,6 @@ }, "node_modules/@vitest/mocker": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", - "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", "dev": true, "license": "MIT", "dependencies": { @@ -3546,8 +3305,6 @@ }, "node_modules/@vitest/pretty-format": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", - "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", "dev": true, "license": "MIT", "dependencies": { @@ -3559,8 +3316,6 @@ }, "node_modules/@vitest/runner": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", - "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", "dev": true, "license": "MIT", "dependencies": { @@ -3573,8 +3328,6 @@ }, "node_modules/@vitest/snapshot": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", - "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", "dev": true, "license": "MIT", "dependencies": { @@ -3589,8 +3342,6 @@ }, "node_modules/@vitest/spy": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", - "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", "dev": true, "license": "MIT", "funding": { @@ -3599,8 +3350,6 @@ }, "node_modules/@vitest/utils": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", - "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", "dev": true, "license": "MIT", "dependencies": { @@ -3614,8 +3363,6 @@ }, "node_modules/@xmldom/xmldom": { "version": "0.8.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", - "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", "dev": true, "license": "MIT", "engines": { @@ -3624,8 +3371,6 @@ }, "node_modules/@xyflow/react": { "version": "12.11.2", - "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.2.tgz", - "integrity": "sha512-eLAlDWJfWnQEhJwGMjlWdAXO9eYllKpliUmPQlAmOLxz6mExXuzMVDUKLMquixgkrtmMFFtug3jGKmYYld12cA==", "license": "MIT", "dependencies": { "@xyflow/system": "0.0.79", @@ -3649,8 +3394,6 @@ }, "node_modules/@xyflow/system": { "version": "0.0.79", - "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.79.tgz", - "integrity": "sha512-czLyOh91NF0hIzbNzwi8I6GlqG23BHh2435OddfI6uiaLH3xdrdygO93gqgH1Bv9mhy8XPFQJOBn1FTq4LvEWA==", "license": "MIT", "dependencies": { "@types/d3-drag": "^3.0.7", @@ -3666,8 +3409,6 @@ }, "node_modules/abbrev": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", - "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", "dev": true, "license": "ISC", "engines": { @@ -3676,14 +3417,10 @@ }, "node_modules/abstract-logging": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, "node_modules/accepts": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "dependencies": { "mime-types": "^3.0.0", @@ -3695,8 +3432,6 @@ }, "node_modules/accepts/node_modules/mime-db": { "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -3704,8 +3439,6 @@ }, "node_modules/accepts/node_modules/mime-types": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -3720,8 +3453,6 @@ }, "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", "bin": { @@ -3733,8 +3464,6 @@ }, "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": { @@ -3743,8 +3472,6 @@ }, "node_modules/agent-base": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3756,8 +3483,6 @@ }, "node_modules/ajv": { "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -3772,8 +3497,6 @@ }, "node_modules/ajv-formats": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -3789,8 +3512,6 @@ }, "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": { @@ -3799,8 +3520,6 @@ }, "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": { @@ -3815,8 +3534,6 @@ }, "node_modules/app-builder-lib": { "version": "26.15.2", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.15.2.tgz", - "integrity": "sha512-3mYfKOjr/ZY7gFESOcq8kylBMgGPpmlQYnpBVit4p6zIg0t/8bkWBILdMMtnjFyN2jllyBf225T8dLlz3D6oBQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3872,8 +3589,6 @@ }, "node_modules/app-builder-lib/node_modules/@electron/get": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", - "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3894,8 +3609,6 @@ }, "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", "dependencies": { @@ -3909,8 +3622,6 @@ }, "node_modules/app-builder-lib/node_modules/@electron/get/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": { @@ -3919,8 +3630,6 @@ }, "node_modules/app-builder-lib/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": { @@ -3928,48 +3637,18 @@ } }, "node_modules/app-builder-lib/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/app-builder-lib/node_modules/ci-info": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", - "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/app-builder-lib/node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" + "node": "20 || >=22" } }, "node_modules/app-builder-lib/node_modules/isexe": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3978,8 +3657,6 @@ }, "node_modules/app-builder-lib/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": { @@ -3991,8 +3668,6 @@ }, "node_modules/app-builder-lib/node_modules/jsonfile": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", "optionalDependencies": { @@ -4000,13 +3675,11 @@ } }, "node_modules/app-builder-lib/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -4017,8 +3690,6 @@ }, "node_modules/app-builder-lib/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": { @@ -4030,8 +3701,6 @@ }, "node_modules/app-builder-lib/node_modules/universalify": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", "engines": { @@ -4040,8 +3709,6 @@ }, "node_modules/app-builder-lib/node_modules/which": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, "license": "ISC", "dependencies": { @@ -4056,15 +3723,11 @@ }, "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/array-buffer-byte-length": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, "license": "MIT", "dependencies": { @@ -4080,8 +3743,6 @@ }, "node_modules/array-includes": { "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4103,8 +3764,6 @@ }, "node_modules/array.prototype.findlastindex": { "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4125,8 +3784,6 @@ }, "node_modules/array.prototype.flat": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, "license": "MIT", "dependencies": { @@ -4144,8 +3801,6 @@ }, "node_modules/array.prototype.flatmap": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, "license": "MIT", "dependencies": { @@ -4163,8 +3818,6 @@ }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4185,8 +3838,6 @@ }, "node_modules/asn1js": { "version": "3.0.10", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", - "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4200,8 +3851,6 @@ }, "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": { @@ -4210,8 +3859,6 @@ }, "node_modules/ast-v8-to-istanbul": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.2.tgz", - "integrity": "sha512-dKmJxJsGItLmc5CYZKuEjuG6GnBs6PG4gohMhyFOWKaNQoYCuRZJDECaBlHmcG0lv2wc2E0uU8lESmBEumC3DQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4222,15 +3869,11 @@ }, "node_modules/async": { "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "dev": true, "license": "MIT" }, "node_modules/async-exit-hook": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", - "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", "dev": true, "license": "MIT", "engines": { @@ -4239,8 +3882,6 @@ }, "node_modules/async-function": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, "license": "MIT", "engines": { @@ -4249,15 +3890,11 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, "license": "MIT" }, "node_modules/at-least-node": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true, "license": "ISC", "engines": { @@ -4266,8 +3903,6 @@ }, "node_modules/atomic-sleep": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", "license": "MIT", "engines": { "node": ">=8.0.0" @@ -4275,8 +3910,6 @@ }, "node_modules/available-typed-arrays": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4291,8 +3924,6 @@ }, "node_modules/avvio": { "version": "9.2.0", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", - "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", "funding": [ { "type": "github", @@ -4311,28 +3942,22 @@ }, "node_modules/aws4": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", - "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", "dev": true, "license": "MIT" }, "node_modules/axios": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", - "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "version": "1.19.0", "dev": true, "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", + "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "node_modules/bail": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", "license": "MIT", "funding": { "type": "github", @@ -4341,15 +3966,11 @@ }, "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-js": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "funding": [ { "type": "github", @@ -4368,8 +3989,6 @@ }, "node_modules/better-sqlite3": { "version": "12.10.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz", - "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -4382,8 +4001,6 @@ }, "node_modules/bindings": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", "license": "MIT", "dependencies": { "file-uri-to-path": "1.0.0" @@ -4391,8 +4008,6 @@ }, "node_modules/bl": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -4402,15 +4017,11 @@ }, "node_modules/bluebird": { "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "dev": true, "license": "MIT" }, "node_modules/body-parser": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", @@ -4433,8 +4044,6 @@ }, "node_modules/body-parser/node_modules/content-type": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", "license": "MIT", "engines": { "node": ">=18" @@ -4446,15 +4055,10 @@ }, "node_modules/boolean": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.18", "dev": true, "license": "MIT", "dependencies": { @@ -4464,8 +4068,6 @@ }, "node_modules/buffer": { "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", @@ -4486,27 +4088,13 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/buffer-from": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, "license": "MIT" }, "node_modules/buffer-image-size": { "version": "0.6.4", - "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", - "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4518,8 +4106,6 @@ }, "node_modules/builder-util": { "version": "26.15.0", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.15.0.tgz", - "integrity": "sha512-dUx+HxVbiNsNQ4mGe1PyoC/tBmsHwBNDLdBuqWCj+rhHFE9lHgrXiGYKAM1uNlznhAaUSyMlms84VeSSr3gOBA==", "dev": true, "license": "MIT", "dependencies": { @@ -4544,8 +4130,6 @@ }, "node_modules/builder-util-runtime": { "version": "9.7.0", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", - "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", "dev": true, "license": "MIT", "dependencies": { @@ -4558,8 +4142,6 @@ }, "node_modules/builder-util/node_modules/agent-base": { "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { @@ -4568,8 +4150,6 @@ }, "node_modules/builder-util/node_modules/https-proxy-agent": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { @@ -4582,8 +4162,6 @@ }, "node_modules/bytes": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -4591,8 +4169,6 @@ }, "node_modules/bytestreamjs": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", - "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -4601,8 +4177,6 @@ }, "node_modules/cacheable-lookup": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", "dev": true, "license": "MIT", "engines": { @@ -4611,8 +4185,6 @@ }, "node_modules/cacheable-request": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", "dev": true, "license": "MIT", "dependencies": { @@ -4630,8 +4202,6 @@ }, "node_modules/call-bind": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", - "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4649,8 +4219,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4662,8 +4230,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4678,8 +4244,6 @@ }, "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": { @@ -4688,8 +4252,6 @@ }, "node_modules/ccount": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", "license": "MIT", "funding": { "type": "github", @@ -4698,8 +4260,6 @@ }, "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": { @@ -4708,8 +4268,6 @@ }, "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": { @@ -4725,8 +4283,6 @@ }, "node_modules/chalk/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": { @@ -4738,8 +4294,6 @@ }, "node_modules/character-entities": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", "license": "MIT", "funding": { "type": "github", @@ -4748,8 +4302,6 @@ }, "node_modules/character-entities-html4": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", "license": "MIT", "funding": { "type": "github", @@ -4758,8 +4310,6 @@ }, "node_modules/character-entities-legacy": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", "license": "MIT", "funding": { "type": "github", @@ -4768,8 +4318,6 @@ }, "node_modules/character-reference-invalid": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", "license": "MIT", "funding": { "type": "github", @@ -4778,21 +4326,15 @@ }, "node_modules/chownr": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, "node_modules/chromium-pickle-js": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", "dev": true, "license": "MIT" }, "node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "version": "4.3.1", "dev": true, "funding": [ { @@ -4807,14 +4349,10 @@ }, "node_modules/classcat": { "version": "5.0.5", - "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", - "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", "license": "MIT" }, "node_modules/cliui": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "license": "ISC", "dependencies": { @@ -4828,8 +4366,6 @@ }, "node_modules/clone-response": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "dev": true, "license": "MIT", "dependencies": { @@ -4841,8 +4377,6 @@ }, "node_modules/clone-response/node_modules/mimic-response": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "dev": true, "license": "MIT", "engines": { @@ -4851,8 +4385,6 @@ }, "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": { @@ -4864,15 +4396,11 @@ }, "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/combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "license": "MIT", "dependencies": { @@ -4884,8 +4412,6 @@ }, "node_modules/comma-separated-tokens": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", "license": "MIT", "funding": { "type": "github", @@ -4894,8 +4420,6 @@ }, "node_modules/commander": { "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "license": "MIT", "engines": { "node": ">= 12" @@ -4903,8 +4427,6 @@ }, "node_modules/compare-version": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", "dev": true, "license": "MIT", "engines": { @@ -4913,21 +4435,17 @@ }, "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/concurrently": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", - "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "version": "9.2.4", "dev": true, "license": "MIT", "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", - "shell-quote": "1.8.3", + "shell-quote": "1.9.0", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" @@ -4945,8 +4463,6 @@ }, "node_modules/content-disposition": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", "engines": { "node": ">=18" @@ -4958,8 +4474,6 @@ }, "node_modules/content-type": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -4967,15 +4481,11 @@ }, "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/cookie": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", "engines": { "node": ">=18" @@ -4987,8 +4497,6 @@ }, "node_modules/cookie-signature": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", "engines": { "node": ">=6.6.0" @@ -4996,15 +4504,11 @@ }, "node_modules/core-util-is": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true, "license": "MIT" }, "node_modules/cors": { "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "license": "MIT", "dependencies": { "object-assign": "^4", @@ -5020,8 +4524,6 @@ }, "node_modules/cross-dirname": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", "optional": true, @@ -5029,8 +4531,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5043,14 +4543,10 @@ }, "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==", "license": "MIT" }, "node_modules/d3-color": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", "license": "ISC", "engines": { "node": ">=12" @@ -5058,8 +4554,6 @@ }, "node_modules/d3-dispatch": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", "license": "ISC", "engines": { "node": ">=12" @@ -5067,8 +4561,6 @@ }, "node_modules/d3-drag": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", @@ -5080,8 +4572,6 @@ }, "node_modules/d3-ease": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", "license": "BSD-3-Clause", "engines": { "node": ">=12" @@ -5089,8 +4579,6 @@ }, "node_modules/d3-interpolate": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", "license": "ISC", "dependencies": { "d3-color": "1 - 3" @@ -5101,8 +4589,6 @@ }, "node_modules/d3-selection": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", "engines": { "node": ">=12" @@ -5110,8 +4596,6 @@ }, "node_modules/d3-timer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", "license": "ISC", "engines": { "node": ">=12" @@ -5119,8 +4603,6 @@ }, "node_modules/d3-transition": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", "license": "ISC", "dependencies": { "d3-color": "1 - 3", @@ -5138,8 +4620,6 @@ }, "node_modules/d3-zoom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", @@ -5154,8 +4634,6 @@ }, "node_modules/data-view-buffer": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5172,8 +4650,6 @@ }, "node_modules/data-view-byte-length": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5190,8 +4666,6 @@ }, "node_modules/data-view-byte-offset": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5208,8 +4682,6 @@ }, "node_modules/debug": { "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5225,8 +4697,6 @@ }, "node_modules/decode-named-character-reference": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", - "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", "license": "MIT", "dependencies": { "character-entities": "^2.0.0" @@ -5238,8 +4708,6 @@ }, "node_modules/decompress-response": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" @@ -5253,8 +4721,6 @@ }, "node_modules/deep-extend": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "license": "MIT", "engines": { "node": ">=4.0.0" @@ -5262,15 +4728,11 @@ }, "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/defer-to-connect": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "dev": true, "license": "MIT", "engines": { @@ -5279,8 +4741,6 @@ }, "node_modules/define-data-property": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -5296,8 +4756,6 @@ }, "node_modules/define-properties": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", @@ -5313,8 +4771,6 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", "engines": { @@ -5323,8 +4779,6 @@ }, "node_modules/depd": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -5332,8 +4786,6 @@ }, "node_modules/dequal": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "license": "MIT", "engines": { "node": ">=6" @@ -5341,8 +4793,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==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -5350,14 +4800,10 @@ }, "node_modules/detect-node": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "license": "MIT" }, "node_modules/devlop": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", "license": "MIT", "dependencies": { "dequal": "^2.0.0" @@ -5369,8 +4815,6 @@ }, "node_modules/dir-compare": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", - "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5380,8 +4824,6 @@ }, "node_modules/dmg-builder": { "version": "26.15.2", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.15.2.tgz", - "integrity": "sha512-fMkjRqKyPtsz4Kzu/qGP0BGjqzMCIgp+/7kw/u6YH6lvn/8hvL3c0TXhoFayBoYdpPCnEinnCHztd4bW7/jetA==", "dev": true, "license": "MIT", "dependencies": { @@ -5393,8 +4835,6 @@ }, "node_modules/doctrine": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5406,8 +4846,6 @@ }, "node_modules/dotenv": { "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -5418,8 +4856,6 @@ }, "node_modules/dotenv-expand": { "version": "11.0.7", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", - "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5434,8 +4870,6 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -5448,8 +4882,6 @@ }, "node_modules/duplexer2": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5458,15 +4890,11 @@ }, "node_modules/duplexer2/node_modules/isarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true, "license": "MIT" }, "node_modules/duplexer2/node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", "dependencies": { @@ -5479,33 +4907,12 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/duplexer2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/duplexer2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/ee-first": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, "node_modules/ejs": { "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5518,10 +4925,25 @@ "node": ">=0.10.0" } }, + "node_modules/electron": { + "version": "43.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-internal/extract-zip": "^1.0.1", + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0" + }, + "bin": { + "electron": "cli.js", + "install-electron": "install.js" + }, + "engines": { + "node": ">= 22.12.0" + } + }, "node_modules/electron-builder": { "version": "26.15.2", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.2.tgz", - "integrity": "sha512-veKM9+dCljaC5A74Pwc0ZWQ9arOHREXWh9hUIf8NGg49ch7x+IB4QhbMzIrV5ONZIXM2OEkaxW11cAPjPtoi4A==", "dev": true, "license": "MIT", "dependencies": { @@ -5546,8 +4968,6 @@ }, "node_modules/electron-builder-squirrel-windows": { "version": "26.15.2", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.2.tgz", - "integrity": "sha512-PNl+SSRoma9mXhxycNGxutZPgvmK19v41mn8F9oecpAU2QNAldpB4HfMuA1LwFC2j8aRzzV5M9HKlKe6dfpvNw==", "dev": true, "license": "MIT", "peer": true, @@ -5559,8 +4979,6 @@ }, "node_modules/electron-log": { "version": "5.4.4", - "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.4.4.tgz", - "integrity": "sha512-istWgaXjBfURBSS8LWVW9C3jsc6+ac+tY1lXrQEOTp0lVj+a4OlO1Tmqb36GgnEUDv92DGC9VI1HNXwJinWpgA==", "license": "MIT", "engines": { "node": ">= 14" @@ -5568,8 +4986,6 @@ }, "node_modules/electron-publish": { "version": "26.15.1", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.1.tgz", - "integrity": "sha512-BMgMHOyexWn0UnOC+Afffw0DMrr0yfLp4U8YsLXwoJ3Da7LS7WUnz21teYZqO0gaApE1KgsjREWmbPqvF5JcPg==", "dev": true, "license": "MIT", "dependencies": { @@ -5586,8 +5002,6 @@ }, "node_modules/electron-winstaller": { "version": "5.4.0", - "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", - "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5608,8 +5022,6 @@ }, "node_modules/electron-winstaller/node_modules/fs-extra": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", "peer": true, @@ -5624,8 +5036,6 @@ }, "node_modules/electron-winstaller/node_modules/jsonfile": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", "peer": true, @@ -5635,8 +5045,6 @@ }, "node_modules/electron-winstaller/node_modules/universalify": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", "peer": true, @@ -5644,17 +5052,26 @@ "node": ">= 4.0.0" } }, + "node_modules/electron/node_modules/@types/node": { + "version": "24.13.3", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "7.18.2", + "dev": true, + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/encodeurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -5662,8 +5079,6 @@ }, "node_modules/end-of-stream": { "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -5671,8 +5086,6 @@ }, "node_modules/entities": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5682,17 +5095,21 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/err-code": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "dev": true, "license": "MIT" }, "node_modules/es-abstract": { "version": "1.24.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", - "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", "dev": true, "license": "MIT", "dependencies": { @@ -5760,8 +5177,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -5769,8 +5184,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -5778,15 +5191,11 @@ }, "node_modules/es-module-lexer": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -5797,8 +5206,6 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", "dependencies": { @@ -5813,8 +5220,6 @@ }, "node_modules/es-shim-unscopables": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, "license": "MIT", "dependencies": { @@ -5826,8 +5231,6 @@ }, "node_modules/es-to-primitive": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "license": "MIT", "dependencies": { @@ -5844,14 +5247,10 @@ }, "node_modules/es6-error": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "license": "MIT" }, "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "version": "0.28.2", "devOptional": true, "hasInstallScript": true, "license": "MIT", @@ -5862,38 +5261,36 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "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": { @@ -5902,14 +5299,10 @@ }, "node_modules/escape-html": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, "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==", "license": "MIT", "engines": { "node": ">=10" @@ -5920,8 +5313,6 @@ }, "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", "dependencies": { @@ -5980,8 +5371,6 @@ }, "node_modules/eslint-import-context": { "version": "0.1.9", - "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", - "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", "dev": true, "license": "MIT", "dependencies": { @@ -6005,8 +5394,6 @@ }, "node_modules/eslint-import-resolver-node": { "version": "0.3.10", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", - "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6017,8 +5404,6 @@ }, "node_modules/eslint-import-resolver-node/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6027,8 +5412,6 @@ }, "node_modules/eslint-import-resolver-typescript": { "version": "4.4.4", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz", - "integrity": "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==", "dev": true, "license": "ISC", "dependencies": { @@ -6062,8 +5445,6 @@ }, "node_modules/eslint-module-utils": { "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -6080,8 +5461,6 @@ }, "node_modules/eslint-module-utils/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6090,8 +5469,6 @@ }, "node_modules/eslint-plugin-import": { "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { @@ -6124,8 +5501,6 @@ }, "node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6134,8 +5509,6 @@ }, "node_modules/eslint-plugin-import/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": { @@ -6144,8 +5517,6 @@ }, "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": { @@ -6161,8 +5532,6 @@ }, "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": { @@ -6174,8 +5543,6 @@ }, "node_modules/eslint/node_modules/ajv": { "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -6191,15 +5558,11 @@ }, "node_modules/eslint/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/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": { @@ -6216,8 +5579,6 @@ }, "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": { @@ -6229,8 +5590,6 @@ }, "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": { @@ -6242,8 +5601,6 @@ }, "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": { @@ -6252,8 +5609,6 @@ }, "node_modules/estree-util-is-identifier-name": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", "license": "MIT", "funding": { "type": "opencollective", @@ -6262,8 +5617,6 @@ }, "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": { @@ -6272,8 +5625,6 @@ }, "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": { @@ -6282,8 +5633,6 @@ }, "node_modules/etag": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -6291,8 +5640,6 @@ }, "node_modules/eventsource": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", "license": "MIT", "dependencies": { "eventsource-parser": "^3.0.1" @@ -6303,8 +5650,6 @@ }, "node_modules/eventsource-parser": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", - "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -6312,8 +5657,6 @@ }, "node_modules/expand-template": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "license": "(MIT OR WTFPL)", "engines": { "node": ">=6" @@ -6321,8 +5664,6 @@ }, "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": { @@ -6331,15 +5672,11 @@ }, "node_modules/exponential-backoff": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", "dev": true, "license": "Apache-2.0" }, "node_modules/express": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", @@ -6381,8 +5718,6 @@ }, "node_modules/express-rate-limit": { "version": "8.5.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", - "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "license": "MIT", "dependencies": { "ip-address": "^10.2.0" @@ -6399,8 +5734,6 @@ }, "node_modules/express/node_modules/cookie": { "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -6408,8 +5741,6 @@ }, "node_modules/express/node_modules/mime-db": { "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -6417,8 +5748,6 @@ }, "node_modules/express/node_modules/mime-types": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -6433,54 +5762,23 @@ }, "node_modules/extend": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", - "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", "license": "MIT" }, "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==", "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-json-stringify": { "version": "6.4.0", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.4.0.tgz", - "integrity": "sha512-ibRCQ0GZKJIQ+P3Et1h0LhPgp3PMTYk0MH8O+kW3lNYsvmaQww5Nn3f1jf73Q0jR1Yz3a1CDP4/NZD3vOajWJQ==", "funding": [ { "type": "github", @@ -6503,24 +5801,18 @@ }, "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-querystring": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", - "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", "license": "MIT", "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.5", "funding": [ { "type": "github", @@ -6535,8 +5827,6 @@ }, "node_modules/fastify": { "version": "5.8.5", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.5.tgz", - "integrity": "sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==", "funding": [ { "type": "github", @@ -6568,8 +5858,6 @@ }, "node_modules/fastq": { "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -6577,8 +5865,6 @@ }, "node_modules/fault": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", - "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", "license": "MIT", "dependencies": { "format": "^0.2.0" @@ -6588,20 +5874,8 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.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==", "license": "MIT", "engines": { "node": ">=12.0.0" @@ -6617,8 +5891,6 @@ }, "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": { @@ -6630,14 +5902,10 @@ }, "node_modules/file-uri-to-path": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, "node_modules/filelist": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", - "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -6645,9 +5913,7 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.4", "dev": true, "license": "MIT", "dependencies": { @@ -6656,8 +5922,6 @@ }, "node_modules/filelist/node_modules/minimatch": { "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -6669,8 +5933,6 @@ }, "node_modules/finalhandler": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -6689,9 +5951,7 @@ } }, "node_modules/find-my-way": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", - "integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==", + "version": "9.7.0", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -6704,8 +5964,6 @@ }, "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": { @@ -6721,8 +5979,6 @@ }, "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": { @@ -6735,21 +5991,15 @@ }, "node_modules/flatbuffers": { "version": "25.9.23", - "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", - "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", "license": "Apache-2.0" }, "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/follow-redirects": { "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, "funding": [ { @@ -6769,8 +6019,6 @@ }, "node_modules/for-each": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "license": "MIT", "dependencies": { @@ -6784,17 +6032,15 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -6802,16 +6048,12 @@ }, "node_modules/format": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", "engines": { "node": ">=0.4.x" } }, "node_modules/forwarded": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -6819,8 +6061,6 @@ }, "node_modules/fresh": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -6828,14 +6068,10 @@ }, "node_modules/fs-constants": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, "node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6849,8 +6085,6 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, "license": "ISC" }, @@ -6870,8 +6104,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6879,8 +6111,6 @@ }, "node_modules/function.prototype.name": { "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -6900,8 +6130,6 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, "license": "MIT", "funding": { @@ -6910,8 +6138,6 @@ }, "node_modules/generator-function": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", "dev": true, "license": "MIT", "engines": { @@ -6920,8 +6146,6 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, "license": "ISC", "engines": { @@ -6930,8 +6154,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -6954,8 +6176,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -6967,8 +6187,6 @@ }, "node_modules/get-stream": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, "license": "MIT", "dependencies": { @@ -6983,8 +6201,6 @@ }, "node_modules/get-symbol-description": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "license": "MIT", "dependencies": { @@ -7001,8 +6217,6 @@ }, "node_modules/get-tsconfig": { "version": "4.14.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", - "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, "license": "MIT", "dependencies": { @@ -7014,15 +6228,10 @@ }, "node_modules/github-from-package": { "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, "node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -7042,8 +6251,6 @@ }, "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": { @@ -7055,8 +6262,6 @@ }, "node_modules/global-agent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", "license": "BSD-3-Clause", "dependencies": { "boolean": "^3.0.1", @@ -7072,8 +6277,6 @@ }, "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": { @@ -7085,8 +6288,6 @@ }, "node_modules/globalthis": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "license": "MIT", "dependencies": { "define-properties": "^1.2.1", @@ -7101,8 +6302,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -7113,8 +6312,6 @@ }, "node_modules/got": { "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", "dev": true, "license": "MIT", "dependencies": { @@ -7139,20 +6336,14 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, "node_modules/guid-typescript": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", - "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", "license": "ISC" }, "node_modules/happy-dom": { "version": "20.10.6", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.10.6.tgz", - "integrity": "sha512-6QD0ilzDDt93tX44y8tbmZdAcdTRYDhUP+Asgi6pC8Pp5IA3cvaZGyoVN/EGtlq9ziT65iPuBBn3ASLr6hCgVw==", "dev": true, "license": "MIT", "dependencies": { @@ -7170,8 +6361,6 @@ }, "node_modules/has-bigints": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, "license": "MIT", "engines": { @@ -7183,8 +6372,6 @@ }, "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": { @@ -7193,8 +6380,6 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -7205,8 +6390,6 @@ }, "node_modules/has-proto": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7221,8 +6404,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -7233,8 +6414,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", "dependencies": { @@ -7248,9 +6427,7 @@ } }, "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "version": "2.0.4", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -7261,8 +6438,6 @@ }, "node_modules/hast-util-from-dom": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", - "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", "license": "ISC", "dependencies": { "@types/hast": "^3.0.0", @@ -7276,8 +6451,6 @@ }, "node_modules/hast-util-from-html": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", - "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -7294,8 +6467,6 @@ }, "node_modules/hast-util-from-html-isomorphic": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", - "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -7310,8 +6481,6 @@ }, "node_modules/hast-util-from-parse5": { "version": "8.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", - "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -7330,8 +6499,6 @@ }, "node_modules/hast-util-is-element": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", - "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -7343,8 +6510,6 @@ }, "node_modules/hast-util-parse-selector": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", - "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -7356,8 +6521,6 @@ }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", - "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -7383,8 +6546,6 @@ }, "node_modules/hast-util-to-text": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", - "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -7399,8 +6560,6 @@ }, "node_modules/hast-util-whitespace": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -7412,8 +6571,6 @@ }, "node_modules/hastscript": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", - "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -7429,8 +6586,6 @@ }, "node_modules/highlight.js": { "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", "license": "BSD-3-Clause", "engines": { "node": "*" @@ -7438,14 +6593,10 @@ }, "node_modules/highlightjs-vue": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", - "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", "license": "CC0-1.0" }, "node_modules/hono": { - "version": "4.12.27", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", - "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", + "version": "4.13.1", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -7453,8 +6604,6 @@ }, "node_modules/hosted-git-info": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, "license": "ISC", "dependencies": { @@ -7466,15 +6615,11 @@ }, "node_modules/html-escaper": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, "license": "MIT" }, "node_modules/html-url-attributes": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", - "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", "license": "MIT", "funding": { "type": "opencollective", @@ -7483,15 +6628,11 @@ }, "node_modules/http-cache-semantics": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/http-errors": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { "depd": "~2.0.0", @@ -7510,8 +6651,6 @@ }, "node_modules/http-proxy-agent": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", "dependencies": { @@ -7524,8 +6663,6 @@ }, "node_modules/http-proxy-agent/node_modules/agent-base": { "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { @@ -7534,8 +6671,6 @@ }, "node_modules/http2-wrapper": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", "dev": true, "license": "MIT", "dependencies": { @@ -7548,8 +6683,6 @@ }, "node_modules/https-proxy-agent": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "license": "MIT", "dependencies": { @@ -7562,8 +6695,6 @@ }, "node_modules/iconv-lite": { "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -7578,8 +6709,6 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", @@ -7598,8 +6727,6 @@ }, "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": { @@ -7608,8 +6735,6 @@ }, "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": { @@ -7625,8 +6750,6 @@ }, "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": { @@ -7635,9 +6758,6 @@ }, "node_modules/inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "license": "ISC", "dependencies": { @@ -7647,26 +6767,18 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/inline-style-parser": { "version": "0.2.7", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", - "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, "node_modules/internal-slot": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "license": "MIT", "dependencies": { @@ -7679,18 +6791,14 @@ } }, "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "version": "10.4.0", "license": "MIT", "engines": { "node": ">= 12" } }, "node_modules/ipaddr.js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", - "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "version": "2.5.0", "license": "MIT", "engines": { "node": ">= 10" @@ -7698,8 +6806,6 @@ }, "node_modules/is-alphabetical": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", "license": "MIT", "funding": { "type": "github", @@ -7708,8 +6814,6 @@ }, "node_modules/is-alphanumerical": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", "license": "MIT", "dependencies": { "is-alphabetical": "^2.0.0", @@ -7722,8 +6826,6 @@ }, "node_modules/is-array-buffer": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "license": "MIT", "dependencies": { @@ -7740,8 +6842,6 @@ }, "node_modules/is-async-function": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7760,8 +6860,6 @@ }, "node_modules/is-bigint": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7776,8 +6874,6 @@ }, "node_modules/is-boolean-object": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, "license": "MIT", "dependencies": { @@ -7793,8 +6889,6 @@ }, "node_modules/is-bun-module": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", - "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7803,8 +6897,6 @@ }, "node_modules/is-callable": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, "license": "MIT", "engines": { @@ -7816,8 +6908,6 @@ }, "node_modules/is-core-module": { "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -7832,8 +6922,6 @@ }, "node_modules/is-data-view": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, "license": "MIT", "dependencies": { @@ -7850,8 +6938,6 @@ }, "node_modules/is-date-object": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "license": "MIT", "dependencies": { @@ -7867,8 +6953,6 @@ }, "node_modules/is-decimal": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", "license": "MIT", "funding": { "type": "github", @@ -7877,8 +6961,6 @@ }, "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": { @@ -7887,8 +6969,6 @@ }, "node_modules/is-finalizationregistry": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, "license": "MIT", "dependencies": { @@ -7903,8 +6983,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { @@ -7913,8 +6991,6 @@ }, "node_modules/is-generator-function": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", "dependencies": { @@ -7933,8 +7009,6 @@ }, "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": { @@ -7946,8 +7020,6 @@ }, "node_modules/is-hexadecimal": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", "license": "MIT", "funding": { "type": "github", @@ -7956,8 +7028,6 @@ }, "node_modules/is-map": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, "license": "MIT", "engines": { @@ -7969,8 +7039,6 @@ }, "node_modules/is-negative-zero": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "license": "MIT", "engines": { @@ -7982,8 +7050,6 @@ }, "node_modules/is-number-object": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "license": "MIT", "dependencies": { @@ -7999,8 +7065,6 @@ }, "node_modules/is-plain-obj": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "license": "MIT", "engines": { "node": ">=12" @@ -8011,14 +7075,10 @@ }, "node_modules/is-promise": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "license": "MIT", "dependencies": { @@ -8036,8 +7096,6 @@ }, "node_modules/is-set": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, "license": "MIT", "engines": { @@ -8049,8 +7107,6 @@ }, "node_modules/is-shared-array-buffer": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "license": "MIT", "dependencies": { @@ -8065,8 +7121,6 @@ }, "node_modules/is-string": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "license": "MIT", "dependencies": { @@ -8082,8 +7136,6 @@ }, "node_modules/is-symbol": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "license": "MIT", "dependencies": { @@ -8100,8 +7152,6 @@ }, "node_modules/is-typed-array": { "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8116,8 +7166,6 @@ }, "node_modules/is-weakmap": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, "license": "MIT", "engines": { @@ -8129,8 +7177,6 @@ }, "node_modules/is-weakref": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, "license": "MIT", "dependencies": { @@ -8145,8 +7191,6 @@ }, "node_modules/is-weakset": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8162,15 +7206,11 @@ }, "node_modules/isarray": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, "node_modules/isbinaryfile": { "version": "5.0.7", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", - "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", "dev": true, "license": "MIT", "engines": { @@ -8182,14 +7222,10 @@ }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -8198,8 +7234,6 @@ }, "node_modules/istanbul-lib-report": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -8213,8 +7247,6 @@ }, "node_modules/istanbul-lib-report/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": { @@ -8226,8 +7258,6 @@ }, "node_modules/istanbul-reports": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -8240,8 +7270,6 @@ }, "node_modules/jake": { "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -8258,8 +7286,6 @@ }, "node_modules/jiti": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "devOptional": true, "license": "MIT", "bin": { @@ -8268,8 +7294,6 @@ }, "node_modules/joi": { "version": "18.2.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.1.tgz", - "integrity": "sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -8287,8 +7311,6 @@ }, "node_modules/jose": { "version": "6.2.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", - "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -8296,16 +7318,22 @@ }, "node_modules/js-tokens": { "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "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==", + "version": "4.3.1", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -8316,15 +7344,11 @@ }, "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-ref-resolver": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", - "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", "funding": [ { "type": "github", @@ -8342,33 +7366,23 @@ }, "node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, "node_modules/json-schema-typed": { "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "license": "BSD-2-Clause" }, "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/json-stringify-safe": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "license": "ISC" }, "node_modules/json5": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "license": "MIT", "dependencies": { @@ -8380,8 +7394,6 @@ }, "node_modules/jsonfile": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -8393,8 +7405,6 @@ }, "node_modules/katex": { "version": "0.16.47", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", - "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -8409,8 +7419,6 @@ }, "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": { @@ -8419,15 +7427,11 @@ }, "node_modules/lazy-val": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", "dev": true, "license": "MIT" }, "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": { @@ -8440,8 +7444,6 @@ }, "node_modules/light-my-request": { "version": "6.6.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", - "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", "funding": [ { "type": "github", @@ -8461,8 +7463,6 @@ }, "node_modules/light-my-request/node_modules/process-warning": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", - "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", "funding": [ { "type": "github", @@ -8476,9 +7476,7 @@ "license": "MIT" }, "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "version": "1.33.0", "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -8491,23 +7489,23 @@ "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" + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.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==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", "cpu": [ "arm64" ], @@ -8525,9 +7523,9 @@ } }, "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==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", "cpu": [ "arm64" ], @@ -8545,9 +7543,9 @@ } }, "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==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", "cpu": [ "x64" ], @@ -8565,9 +7563,9 @@ } }, "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==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", "cpu": [ "x64" ], @@ -8585,9 +7583,9 @@ } }, "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==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", "cpu": [ "arm" ], @@ -8605,12 +7603,15 @@ } }, "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==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8625,12 +7626,15 @@ } }, "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==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8645,12 +7649,13 @@ } }, "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==", + "version": "1.33.0", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8665,12 +7670,15 @@ } }, "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==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8685,9 +7693,9 @@ } }, "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==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", "cpu": [ "arm64" ], @@ -8705,9 +7713,9 @@ } }, "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==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", "cpu": [ "x64" ], @@ -8726,8 +7734,6 @@ }, "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": { @@ -8742,28 +7748,20 @@ }, "node_modules/lodash": { "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, "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/long": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, "node_modules/longest-streak": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", "license": "MIT", "funding": { "type": "github", @@ -8772,8 +7770,6 @@ }, "node_modules/lowercase-keys": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "dev": true, "license": "MIT", "engines": { @@ -8782,8 +7778,6 @@ }, "node_modules/lowlight": { "version": "1.20.0", - "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", - "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", "license": "MIT", "dependencies": { "fault": "^1.0.0", @@ -8796,8 +7790,6 @@ }, "node_modules/lru-cache": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "license": "ISC", "dependencies": { @@ -8809,8 +7801,6 @@ }, "node_modules/lucide-react": { "version": "1.17.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.17.0.tgz", - "integrity": "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -8818,8 +7808,6 @@ }, "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": { @@ -8828,8 +7816,6 @@ }, "node_modules/magicast": { "version": "0.5.3", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", - "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", "dev": true, "license": "MIT", "dependencies": { @@ -8840,8 +7826,6 @@ }, "node_modules/make-dir": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", "dependencies": { @@ -8856,8 +7840,6 @@ }, "node_modules/markdown-table": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", - "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", "license": "MIT", "funding": { "type": "github", @@ -8866,8 +7848,6 @@ }, "node_modules/matcher": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", "license": "MIT", "dependencies": { "escape-string-regexp": "^4.0.0" @@ -8878,8 +7858,6 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -8887,8 +7865,6 @@ }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", - "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -8903,8 +7879,6 @@ }, "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "license": "MIT", "engines": { "node": ">=12" @@ -8915,8 +7889,6 @@ }, "node_modules/mdast-util-from-markdown": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", - "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -8939,8 +7911,6 @@ }, "node_modules/mdast-util-gfm": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", - "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", "license": "MIT", "dependencies": { "mdast-util-from-markdown": "^2.0.0", @@ -8958,8 +7928,6 @@ }, "node_modules/mdast-util-gfm-autolink-literal": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", - "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -8975,8 +7943,6 @@ }, "node_modules/mdast-util-gfm-footnote": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -8992,8 +7958,6 @@ }, "node_modules/mdast-util-gfm-strikethrough": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -9007,8 +7971,6 @@ }, "node_modules/mdast-util-gfm-table": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", - "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -9024,8 +7986,6 @@ }, "node_modules/mdast-util-gfm-task-list-item": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", - "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -9040,8 +8000,6 @@ }, "node_modules/mdast-util-math": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", - "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -9059,8 +8017,6 @@ }, "node_modules/mdast-util-mdx-expression": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", - "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -9077,8 +8033,6 @@ }, "node_modules/mdast-util-mdx-jsx": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", - "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -9101,8 +8055,6 @@ }, "node_modules/mdast-util-mdxjs-esm": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -9119,8 +8071,6 @@ }, "node_modules/mdast-util-newline-to-break": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz", - "integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -9133,8 +8083,6 @@ }, "node_modules/mdast-util-phrasing": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -9147,8 +8095,6 @@ }, "node_modules/mdast-util-to-hast": { "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -9168,8 +8114,6 @@ }, "node_modules/mdast-util-to-markdown": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -9189,8 +8133,6 @@ }, "node_modules/mdast-util-to-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0" @@ -9202,8 +8144,6 @@ }, "node_modules/media-typer": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -9211,8 +8151,6 @@ }, "node_modules/merge-descriptors": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", "engines": { "node": ">=18" @@ -9223,8 +8161,6 @@ }, "node_modules/micromark": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", "funding": [ { "type": "GitHub Sponsors", @@ -9258,8 +8194,6 @@ }, "node_modules/micromark-core-commonmark": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", "funding": [ { "type": "GitHub Sponsors", @@ -9292,8 +8226,6 @@ }, "node_modules/micromark-extension-gfm": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", - "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", "license": "MIT", "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", @@ -9312,8 +8244,6 @@ }, "node_modules/micromark-extension-gfm-autolink-literal": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", - "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", @@ -9328,8 +8258,6 @@ }, "node_modules/micromark-extension-gfm-footnote": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -9348,8 +8276,6 @@ }, "node_modules/micromark-extension-gfm-strikethrough": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", - "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -9366,8 +8292,6 @@ }, "node_modules/micromark-extension-gfm-table": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", - "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -9383,8 +8307,6 @@ }, "node_modules/micromark-extension-gfm-tagfilter": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", - "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", "license": "MIT", "dependencies": { "micromark-util-types": "^2.0.0" @@ -9396,8 +8318,6 @@ }, "node_modules/micromark-extension-gfm-task-list-item": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", - "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -9413,8 +8333,6 @@ }, "node_modules/micromark-extension-math": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", - "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", "license": "MIT", "dependencies": { "@types/katex": "^0.16.0", @@ -9432,8 +8350,6 @@ }, "node_modules/micromark-factory-destination": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", "funding": [ { "type": "GitHub Sponsors", @@ -9453,8 +8369,6 @@ }, "node_modules/micromark-factory-label": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", "funding": [ { "type": "GitHub Sponsors", @@ -9475,8 +8389,6 @@ }, "node_modules/micromark-factory-space": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", @@ -9495,8 +8407,6 @@ }, "node_modules/micromark-factory-title": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", "funding": [ { "type": "GitHub Sponsors", @@ -9517,8 +8427,6 @@ }, "node_modules/micromark-factory-whitespace": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", "funding": [ { "type": "GitHub Sponsors", @@ -9539,8 +8447,6 @@ }, "node_modules/micromark-util-character": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -9559,8 +8465,6 @@ }, "node_modules/micromark-util-chunked": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", "funding": [ { "type": "GitHub Sponsors", @@ -9578,8 +8482,6 @@ }, "node_modules/micromark-util-classify-character": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", "funding": [ { "type": "GitHub Sponsors", @@ -9599,8 +8501,6 @@ }, "node_modules/micromark-util-combine-extensions": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", "funding": [ { "type": "GitHub Sponsors", @@ -9619,8 +8519,6 @@ }, "node_modules/micromark-util-decode-numeric-character-reference": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", "funding": [ { "type": "GitHub Sponsors", @@ -9638,8 +8536,6 @@ }, "node_modules/micromark-util-decode-string": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", "funding": [ { "type": "GitHub Sponsors", @@ -9660,8 +8556,6 @@ }, "node_modules/micromark-util-encode": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", "funding": [ { "type": "GitHub Sponsors", @@ -9676,8 +8570,6 @@ }, "node_modules/micromark-util-html-tag-name": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", "funding": [ { "type": "GitHub Sponsors", @@ -9692,8 +8584,6 @@ }, "node_modules/micromark-util-normalize-identifier": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", "funding": [ { "type": "GitHub Sponsors", @@ -9711,8 +8601,6 @@ }, "node_modules/micromark-util-resolve-all": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", "funding": [ { "type": "GitHub Sponsors", @@ -9730,8 +8618,6 @@ }, "node_modules/micromark-util-sanitize-uri": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", "funding": [ { "type": "GitHub Sponsors", @@ -9751,8 +8637,6 @@ }, "node_modules/micromark-util-subtokenize": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", "funding": [ { "type": "GitHub Sponsors", @@ -9773,8 +8657,6 @@ }, "node_modules/micromark-util-symbol": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -9789,8 +8671,6 @@ }, "node_modules/micromark-util-types": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", "funding": [ { "type": "GitHub Sponsors", @@ -9805,8 +8685,6 @@ }, "node_modules/mime": { "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, "license": "MIT", "bin": { @@ -9818,8 +8696,6 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", "engines": { @@ -9828,8 +8704,6 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", "dependencies": { @@ -9841,8 +8715,6 @@ }, "node_modules/mimic-response": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "license": "MIT", "engines": { "node": ">=10" @@ -9853,8 +8725,6 @@ }, "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": { @@ -9866,8 +8736,6 @@ }, "node_modules/minimist": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9875,8 +8743,6 @@ }, "node_modules/minipass": { "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" @@ -9884,8 +8750,6 @@ }, "node_modules/minizlib": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -9896,8 +8760,6 @@ }, "node_modules/mkdirp": { "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", "peer": true, @@ -9910,20 +8772,14 @@ }, "node_modules/mkdirp-classic": { "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, "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==", "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.18", "funding": [ { "type": "github", @@ -9940,14 +8796,10 @@ }, "node_modules/napi-build-utils": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "license": "MIT" }, "node_modules/napi-postinstall": { "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", "dev": true, "license": "MIT", "bin": { @@ -9962,24 +8814,18 @@ }, "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/negotiator": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/node-abi": { - "version": "3.92.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", - "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "version": "3.94.0", "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -9990,8 +8836,6 @@ }, "node_modules/node-api-version": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", - "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -10000,8 +8844,6 @@ }, "node_modules/node-exports-info": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", - "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", "dev": true, "license": "MIT", "dependencies": { @@ -10019,8 +8861,6 @@ }, "node_modules/node-exports-info/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": { @@ -10029,8 +8869,6 @@ }, "node_modules/node-gyp": { "version": "12.4.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", - "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", "dev": true, "license": "MIT", "dependencies": { @@ -10052,20 +8890,8 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/node-gyp/node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/node-gyp/node_modules/isexe": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -10074,8 +8900,6 @@ }, "node_modules/node-gyp/node_modules/which": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "dependencies": { @@ -10090,15 +8914,11 @@ }, "node_modules/node-int64": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true, "license": "MIT" }, "node_modules/nopt": { "version": "9.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", - "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", "dev": true, "license": "ISC", "dependencies": { @@ -10113,8 +8933,6 @@ }, "node_modules/normalize-url": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "dev": true, "license": "MIT", "engines": { @@ -10126,8 +8944,6 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10135,8 +8951,6 @@ }, "node_modules/object-inspect": { "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -10147,8 +8961,6 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -10156,8 +8968,6 @@ }, "node_modules/object.assign": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "license": "MIT", "dependencies": { @@ -10177,8 +8987,6 @@ }, "node_modules/object.entries": { "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, "license": "MIT", "dependencies": { @@ -10193,8 +9001,6 @@ }, "node_modules/object.fromentries": { "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10212,8 +9018,6 @@ }, "node_modules/object.groupby": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10227,8 +9031,6 @@ }, "node_modules/object.values": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -10246,8 +9048,6 @@ }, "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", @@ -10257,8 +9057,6 @@ }, "node_modules/on-exit-leak-free": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -10266,8 +9064,6 @@ }, "node_modules/on-finished": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -10278,8 +9074,6 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", "dependencies": { "wrappy": "1" @@ -10287,14 +9081,10 @@ }, "node_modules/onnxruntime-common": { "version": "1.21.0", - "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz", - "integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==", "license": "MIT" }, "node_modules/onnxruntime-node": { "version": "1.21.0", - "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.21.0.tgz", - "integrity": "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==", "hasInstallScript": true, "license": "MIT", "os": [ @@ -10310,8 +9100,6 @@ }, "node_modules/onnxruntime-web": { "version": "1.22.0-dev.20250409-89f8206ba4", - "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.22.0-dev.20250409-89f8206ba4.tgz", - "integrity": "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==", "license": "MIT", "dependencies": { "flatbuffers": "^25.1.24", @@ -10324,14 +9112,10 @@ }, "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { "version": "1.22.0-dev.20250409-89f8206ba4", - "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.22.0-dev.20250409-89f8206ba4.tgz", - "integrity": "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==", "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": { @@ -10348,8 +9132,6 @@ }, "node_modules/own-keys": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "dev": true, "license": "MIT", "dependencies": { @@ -10366,8 +9148,6 @@ }, "node_modules/p-cancelable": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "dev": true, "license": "MIT", "engines": { @@ -10376,8 +9156,6 @@ }, "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": { @@ -10392,8 +9170,6 @@ }, "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": { @@ -10408,8 +9184,6 @@ }, "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": { @@ -10421,8 +9195,6 @@ }, "node_modules/parse-entities": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", @@ -10440,14 +9212,10 @@ }, "node_modules/parse-entities/node_modules/@types/unist": { "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, "node_modules/parse5": { "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "license": "MIT", "dependencies": { "entities": "^6.0.0" @@ -10458,8 +9226,6 @@ }, "node_modules/parse5/node_modules/entities": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -10470,8 +9236,6 @@ }, "node_modules/parseurl": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -10479,8 +9243,6 @@ }, "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": { @@ -10489,8 +9251,6 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "license": "MIT", "engines": { @@ -10499,8 +9259,6 @@ }, "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==", "license": "MIT", "engines": { "node": ">=8" @@ -10508,15 +9266,11 @@ }, "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true, "license": "MIT" }, "node_modules/path-to-regexp": { "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", @@ -10525,15 +9279,11 @@ }, "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/pe-library": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", - "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", "dev": true, "license": "MIT", "engines": { @@ -10545,23 +9295,12 @@ "url": "https://github.com/sponsors/jet2jet" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", "license": "MIT", "engines": { "node": ">=12" @@ -10572,8 +9311,6 @@ }, "node_modules/pino": { "version": "10.3.1", - "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", - "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", "license": "MIT", "dependencies": { "@pinojs/redact": "^0.4.0", @@ -10594,8 +9331,6 @@ }, "node_modules/pino-abstract-transport": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", - "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", "license": "MIT", "dependencies": { "split2": "^4.0.0" @@ -10603,14 +9338,10 @@ }, "node_modules/pino-std-serializers": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", - "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", "license": "MIT" }, "node_modules/pkce-challenge": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", "license": "MIT", "engines": { "node": ">=16.20.0" @@ -10618,8 +9349,6 @@ }, "node_modules/pkijs": { "version": "3.4.0", - "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", - "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -10636,8 +9365,6 @@ }, "node_modules/pkijs/node_modules/@noble/hashes": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "dev": true, "license": "MIT", "engines": { @@ -10649,14 +9376,10 @@ }, "node_modules/platform": { "version": "1.3.6", - "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", - "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", "license": "MIT" }, "node_modules/plist": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10670,8 +9393,6 @@ }, "node_modules/possible-typed-array-names": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, "license": "MIT", "engines": { @@ -10679,9 +9400,7 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.26", "funding": [ { "type": "opencollective", @@ -10698,7 +9417,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -10708,8 +9427,6 @@ }, "node_modules/postject": { "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", "dev": true, "license": "MIT", "optional": true, @@ -10726,8 +9443,6 @@ }, "node_modules/postject/node_modules/commander": { "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true, "license": "MIT", "optional": true, @@ -10738,9 +9453,6 @@ }, "node_modules/prebuild-install": { "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", "license": "MIT", "dependencies": { "detect-libc": "^2.0.0", @@ -10765,8 +9477,6 @@ }, "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": { @@ -10775,8 +9485,6 @@ }, "node_modules/prismjs": { "version": "1.30.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", - "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", "license": "MIT", "engines": { "node": ">=6" @@ -10784,8 +9492,6 @@ }, "node_modules/proc-log": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", "engines": { @@ -10794,15 +9500,11 @@ }, "node_modules/process-nextick-args": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true, "license": "MIT" }, "node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "version": "5.1.0", "funding": [ { "type": "github", @@ -10817,8 +9519,6 @@ }, "node_modules/progress": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, "license": "MIT", "engines": { @@ -10827,8 +9527,6 @@ }, "node_modules/promise-retry": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "dev": true, "license": "MIT", "dependencies": { @@ -10841,8 +9539,6 @@ }, "node_modules/proper-lockfile": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -10852,8 +9548,6 @@ }, "node_modules/property-information": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", - "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", "license": "MIT", "funding": { "type": "github", @@ -10861,9 +9555,7 @@ } }, "node_modules/protobufjs": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.3.tgz", - "integrity": "sha512-+k0vdJKNdW+Vu+dYe8tZA/VvQb6XKNWexC6URwBFXxNnjLJz9nQJCemGyNgRAWD+B7+nGNc9qMPGwcD7s4nzUw==", + "version": "7.6.5", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -10873,7 +9565,6 @@ "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", @@ -10886,8 +9577,6 @@ }, "node_modules/proxy-addr": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -10899,8 +9588,6 @@ }, "node_modules/proxy-addr/node_modules/ipaddr.js": { "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "license": "MIT", "engines": { "node": ">= 0.10" @@ -10908,8 +9595,6 @@ }, "node_modules/proxy-from-env": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", "dev": true, "license": "MIT", "engines": { @@ -10918,8 +9603,6 @@ }, "node_modules/pump": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -10928,8 +9611,6 @@ }, "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": { @@ -10938,8 +9619,6 @@ }, "node_modules/pvtsutils": { "version": "1.3.6", - "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", - "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", "dev": true, "license": "MIT", "dependencies": { @@ -10948,8 +9627,6 @@ }, "node_modules/pvutils": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", - "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", "dev": true, "license": "MIT", "engines": { @@ -10958,8 +9635,6 @@ }, "node_modules/qs": { "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "license": "BSD-3-Clause", "dependencies": { "es-define-property": "^1.0.1", @@ -10974,14 +9649,10 @@ }, "node_modules/quick-format-unescaped": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, "node_modules/quick-lru": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true, "license": "MIT", "engines": { @@ -10993,8 +9664,6 @@ }, "node_modules/range-parser": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -11002,8 +9671,6 @@ }, "node_modules/raw-body": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -11017,8 +9684,6 @@ }, "node_modules/rc": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", @@ -11032,8 +9697,6 @@ }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11041,8 +9704,6 @@ }, "node_modules/react": { "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", - "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11050,8 +9711,6 @@ }, "node_modules/react-dom": { "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", - "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" @@ -11062,8 +9721,6 @@ }, "node_modules/react-markdown": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", - "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -11089,8 +9746,6 @@ }, "node_modules/react-syntax-highlighter": { "version": "16.1.1", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", - "integrity": "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", @@ -11109,8 +9764,6 @@ }, "node_modules/read-binary-file-arch": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", - "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", "dev": true, "license": "MIT", "dependencies": { @@ -11122,8 +9775,6 @@ }, "node_modules/readable-stream": { "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -11136,8 +9787,6 @@ }, "node_modules/real-require": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", "license": "MIT", "engines": { "node": ">= 12.13.0" @@ -11145,8 +9794,6 @@ }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, "license": "MIT", "dependencies": { @@ -11168,8 +9815,6 @@ }, "node_modules/refractor": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz", - "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -11184,8 +9829,6 @@ }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, "license": "MIT", "dependencies": { @@ -11205,8 +9848,6 @@ }, "node_modules/rehype-katex": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", - "integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -11224,8 +9865,6 @@ }, "node_modules/remark-breaks": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz", - "integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -11239,8 +9878,6 @@ }, "node_modules/remark-gfm": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", - "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -11257,8 +9894,6 @@ }, "node_modules/remark-math": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", - "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -11273,8 +9908,6 @@ }, "node_modules/remark-parse": { "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -11289,8 +9922,6 @@ }, "node_modules/remark-rehype": { "version": "11.1.2", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", - "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -11306,8 +9937,6 @@ }, "node_modules/remark-stringify": { "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", - "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -11321,8 +9950,6 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "license": "MIT", "engines": { @@ -11331,8 +9958,6 @@ }, "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==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11340,8 +9965,6 @@ }, "node_modules/resedit": { "version": "1.7.2", - "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", - "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", "dev": true, "license": "MIT", "dependencies": { @@ -11358,8 +9981,6 @@ }, "node_modules/resolve": { "version": "2.0.0-next.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", - "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11382,15 +10003,11 @@ }, "node_modules/resolve-alpn": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "dev": true, "license": "MIT" }, "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": { @@ -11399,8 +10016,6 @@ }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, "license": "MIT", "funding": { @@ -11409,8 +10024,6 @@ }, "node_modules/responselike": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", "dev": true, "license": "MIT", "dependencies": { @@ -11422,8 +10035,6 @@ }, "node_modules/ret": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", - "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", "license": "MIT", "engines": { "node": ">=10" @@ -11431,8 +10042,6 @@ }, "node_modules/retry": { "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "license": "MIT", "engines": { "node": ">= 4" @@ -11440,8 +10049,6 @@ }, "node_modules/reusify": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -11450,15 +10057,10 @@ }, "node_modules/rfdc": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "license": "MIT" }, "node_modules/rimraf": { "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", "peer": true, @@ -11471,8 +10073,6 @@ }, "node_modules/roarr": { "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", "license": "BSD-3-Clause", "dependencies": { "boolean": "^3.0.1", @@ -11487,12 +10087,10 @@ } }, "node_modules/rolldown": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", - "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "version": "1.2.3", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.132.0", + "@oxc-project/types": "=0.143.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -11502,27 +10100,24 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.2", - "@rolldown/binding-darwin-arm64": "1.0.2", - "@rolldown/binding-darwin-x64": "1.0.2", - "@rolldown/binding-freebsd-x64": "1.0.2", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", - "@rolldown/binding-linux-arm64-gnu": "1.0.2", - "@rolldown/binding-linux-arm64-musl": "1.0.2", - "@rolldown/binding-linux-ppc64-gnu": "1.0.2", - "@rolldown/binding-linux-s390x-gnu": "1.0.2", - "@rolldown/binding-linux-x64-gnu": "1.0.2", - "@rolldown/binding-linux-x64-musl": "1.0.2", - "@rolldown/binding-openharmony-arm64": "1.0.2", - "@rolldown/binding-wasm32-wasi": "1.0.2", - "@rolldown/binding-win32-arm64-msvc": "1.0.2", - "@rolldown/binding-win32-x64-msvc": "1.0.2" + "@rolldown/binding-android-arm64": "1.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" } }, "node_modules/router": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -11537,8 +10132,6 @@ }, "node_modules/rxjs": { "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -11547,8 +10140,6 @@ }, "node_modules/safe-array-concat": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", - "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", "dev": true, "license": "MIT", "dependencies": { @@ -11566,29 +10157,11 @@ } }, "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "version": "5.1.2", "license": "MIT" }, "node_modules/safe-push-apply": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, "license": "MIT", "dependencies": { @@ -11604,8 +10177,6 @@ }, "node_modules/safe-regex-test": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, "license": "MIT", "dependencies": { @@ -11622,8 +10193,6 @@ }, "node_modules/safe-regex2": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", - "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", "funding": [ { "type": "github", @@ -11644,8 +10213,6 @@ }, "node_modules/safe-stable-stringify": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", "license": "MIT", "engines": { "node": ">=10" @@ -11653,14 +10220,10 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, "node_modules/sanitize-filename": { "version": "1.6.4", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", - "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", "dev": true, "license": "WTFPL OR ISC", "dependencies": { @@ -11669,8 +10232,6 @@ }, "node_modules/sax": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -11679,14 +10240,10 @@ }, "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/secure-json-parse": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", - "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", "funding": [ { "type": "github", @@ -11700,9 +10257,7 @@ "license": "BSD-3-Clause" }, "node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.8.5", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -11713,14 +10268,10 @@ }, "node_modules/semver-compare": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "license": "MIT" }, "node_modules/send": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "license": "MIT", "dependencies": { "debug": "^4.4.3", @@ -11745,8 +10296,6 @@ }, "node_modules/send/node_modules/mime-db": { "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -11754,8 +10303,6 @@ }, "node_modules/send/node_modules/mime-types": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -11770,8 +10317,6 @@ }, "node_modules/serialize-error": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", "license": "MIT", "dependencies": { "type-fest": "^0.13.1" @@ -11785,8 +10330,6 @@ }, "node_modules/serve-static": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -11804,14 +10347,10 @@ }, "node_modules/set-cookie-parser": { "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, "node_modules/set-function-length": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "license": "MIT", "dependencies": { @@ -11828,8 +10367,6 @@ }, "node_modules/set-function-name": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11844,8 +10381,6 @@ }, "node_modules/set-proto": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "dev": true, "license": "MIT", "dependencies": { @@ -11859,58 +10394,57 @@ }, "node_modules/setprototypeof": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, + "version": "0.35.3", "license": "Apache-2.0", "dependencies": { - "@img/colour": "^1.0.0", + "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "semver": "^7.8.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "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==", "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -11921,17 +10455,13 @@ }, "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==", "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "version": "1.9.0", "dev": true, "license": "MIT", "engines": { @@ -11943,8 +10473,6 @@ }, "node_modules/side-channel": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -11962,8 +10490,6 @@ }, "node_modules/side-channel-list": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -11978,8 +10504,6 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -11996,8 +10520,6 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -12015,21 +10537,15 @@ }, "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/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, "node_modules/simple-concat": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", "funding": [ { "type": "github", @@ -12048,8 +10564,6 @@ }, "node_modules/simple-get": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "funding": [ { "type": "github", @@ -12073,8 +10587,6 @@ }, "node_modules/simple-update-notifier": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", "dev": true, "license": "MIT", "dependencies": { @@ -12086,8 +10598,6 @@ }, "node_modules/sonic-boom": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", - "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", "license": "MIT", "dependencies": { "atomic-sleep": "^1.0.0" @@ -12095,8 +10605,6 @@ }, "node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -12105,8 +10613,6 @@ }, "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==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -12114,8 +10620,6 @@ }, "node_modules/source-map-support": { "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "license": "MIT", "dependencies": { @@ -12125,8 +10629,6 @@ }, "node_modules/space-separated-tokens": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", "license": "MIT", "funding": { "type": "github", @@ -12135,8 +10637,6 @@ }, "node_modules/split2": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", "license": "ISC", "engines": { "node": ">= 10.x" @@ -12144,14 +10644,10 @@ }, "node_modules/sprintf-js": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "license": "BSD-3-Clause" }, "node_modules/sqlite-vec": { "version": "0.1.9", - "resolved": "https://registry.npmjs.org/sqlite-vec/-/sqlite-vec-0.1.9.tgz", - "integrity": "sha512-L7XJWRIBNvR9O5+vh1FQ+IGkh/3D2AzVksW5gdtk28m78Hy8skFD0pqReKH1Yp0/BUKRGcffgKvyO/EON5JXpA==", "license": "MIT OR Apache", "optionalDependencies": { "sqlite-vec-darwin-arm64": "0.1.9", @@ -12161,49 +10657,8 @@ "sqlite-vec-windows-x64": "0.1.9" } }, - "node_modules/sqlite-vec-darwin-arm64": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-arm64/-/sqlite-vec-darwin-arm64-0.1.9.tgz", - "integrity": "sha512-jSsZpE42OfBkGL/ItyJTVCUwl6o6Ka3U5rc4j+UBDIQzC1ulSSKMEhQLthsOnF/MdAf1MuAkYhkdKmmcjaIZQg==", - "cpu": [ - "arm64" - ], - "license": "MIT OR Apache", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/sqlite-vec-darwin-x64": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-x64/-/sqlite-vec-darwin-x64-0.1.9.tgz", - "integrity": "sha512-KDlVyqQT7pnOhU1ymB9gs7dMbSoVmKHitT+k1/xkjarcX8bBqPxWrGlK/R+C5WmWkfvWwyq5FfXfiBYCBs6PlA==", - "cpu": [ - "x64" - ], - "license": "MIT OR Apache", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/sqlite-vec-linux-arm64": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/sqlite-vec-linux-arm64/-/sqlite-vec-linux-arm64-0.1.9.tgz", - "integrity": "sha512-5wXVJ9c9kR4CHm/wVqXb/R+XUHTdpZ4nWbPHlS+gc9qQFVHs92Km4bPnCKX4rtcPMzvNis+SIzMJR1SCEwpuUw==", - "cpu": [ - "arm64" - ], - "license": "MIT OR Apache", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/sqlite-vec-linux-x64": { "version": "0.1.9", - "resolved": "https://registry.npmjs.org/sqlite-vec-linux-x64/-/sqlite-vec-linux-x64-0.1.9.tgz", - "integrity": "sha512-w3tCH8xK2finW8fQJ/m8uqKodXUZ9KAuAar2UIhz4BHILfpE0WM/MTGCRfa7RjYbrYim5Luk3guvMOGI7T7JQA==", "cpu": [ "x64" ], @@ -12213,23 +10668,8 @@ "linux" ] }, - "node_modules/sqlite-vec-windows-x64": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/sqlite-vec-windows-x64/-/sqlite-vec-windows-x64-0.1.9.tgz", - "integrity": "sha512-y3gEIyy/17bq2QFPQOWLE68TYWcRZkBQVA2XLrTPHNTOp55xJi/BBBmOm40tVMDMjtP+Elpk6UBUXdaq+46b0Q==", - "cpu": [ - "x64" - ], - "license": "MIT OR Apache", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/stable-hash-x": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", - "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", "dev": true, "license": "MIT", "engines": { @@ -12238,15 +10678,11 @@ }, "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/stat-mode": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", - "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", "dev": true, "license": "MIT", "engines": { @@ -12255,8 +10691,6 @@ }, "node_modules/statuses": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -12264,15 +10698,11 @@ }, "node_modules/std-env": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12284,18 +10714,14 @@ } }, "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "version": "1.1.1", "license": "MIT", "dependencies": { - "safe-buffer": "~5.2.0" + "safe-buffer": "~5.1.0" } }, "node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -12309,8 +10735,6 @@ }, "node_modules/string.prototype.trim": { "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, "license": "MIT", "dependencies": { @@ -12331,8 +10755,6 @@ }, "node_modules/string.prototype.trimend": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12350,8 +10772,6 @@ }, "node_modules/string.prototype.trimstart": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "license": "MIT", "dependencies": { @@ -12368,8 +10788,6 @@ }, "node_modules/stringify-entities": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", "license": "MIT", "dependencies": { "character-entities-html4": "^2.0.0", @@ -12382,8 +10800,6 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -12395,8 +10811,6 @@ }, "node_modules/strip-bom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, "license": "MIT", "engines": { @@ -12405,8 +10819,6 @@ }, "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": { @@ -12418,8 +10830,6 @@ }, "node_modules/style-to-js": { "version": "1.1.21", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", - "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", "license": "MIT", "dependencies": { "style-to-object": "1.0.14" @@ -12427,8 +10837,6 @@ }, "node_modules/style-to-object": { "version": "1.0.14", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", - "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", "license": "MIT", "dependencies": { "inline-style-parser": "0.2.7" @@ -12436,8 +10844,6 @@ }, "node_modules/sumchecker": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -12449,8 +10855,6 @@ }, "node_modules/supports-color": { "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -12465,8 +10869,6 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, "license": "MIT", "engines": { @@ -12477,9 +10879,7 @@ } }, "node_modules/tar": { - "version": "7.5.16", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", - "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", + "version": "7.5.22", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -12494,8 +10894,6 @@ }, "node_modules/tar-fs": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -12506,8 +10904,6 @@ }, "node_modules/tar-stream": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "license": "MIT", "dependencies": { "bl": "^4.0.3", @@ -12522,8 +10918,6 @@ }, "node_modules/tar/node_modules/chownr": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -12531,8 +10925,6 @@ }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -12540,8 +10932,6 @@ }, "node_modules/temp": { "version": "0.9.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", - "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", "peer": true, @@ -12555,8 +10945,6 @@ }, "node_modules/temp-file": { "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", - "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", "dev": true, "license": "MIT", "dependencies": { @@ -12566,8 +10954,6 @@ }, "node_modules/thread-stream": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", - "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", "license": "MIT", "dependencies": { "real-require": "^1.0.0" @@ -12578,14 +10964,10 @@ }, "node_modules/thread-stream/node_modules/real-require": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", - "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", "license": "MIT" }, "node_modules/tiny-async-pool": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", - "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", "dev": true, "license": "MIT", "dependencies": { @@ -12594,8 +10976,6 @@ }, "node_modules/tiny-async-pool/node_modules/semver": { "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { @@ -12604,15 +10984,11 @@ }, "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.2.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.2.tgz", - "integrity": "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==", "dev": true, "license": "MIT", "engines": { @@ -12620,9 +10996,7 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -12637,8 +11011,6 @@ }, "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": { @@ -12647,8 +11019,6 @@ }, "node_modules/tmp": { "version": "0.2.7", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", - "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, "license": "MIT", "engines": { @@ -12657,8 +11027,6 @@ }, "node_modules/tmp-promise": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12667,8 +11035,6 @@ }, "node_modules/toad-cache": { "version": "3.7.1", - "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.1.tgz", - "integrity": "sha512-5DXWzE4Vz7xNHsv+xQ+MGfJYyC78Aok3tEr0MNwHoRf7vZnga1mQXZ4/Nsodld4VR6Wd+VhfmqnNrsRJyYPfrQ==", "license": "MIT", "engines": { "node": ">=20" @@ -12676,8 +11042,6 @@ }, "node_modules/toidentifier": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", "engines": { "node": ">=0.6" @@ -12685,8 +11049,6 @@ }, "node_modules/tree-kill": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, "license": "MIT", "bin": { @@ -12695,8 +11057,6 @@ }, "node_modules/trim-lines": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", "license": "MIT", "funding": { "type": "github", @@ -12705,8 +11065,6 @@ }, "node_modules/trough": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", "license": "MIT", "funding": { "type": "github", @@ -12715,8 +11073,6 @@ }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", "dev": true, "license": "WTFPL", "dependencies": { @@ -12725,8 +11081,6 @@ }, "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": { @@ -12738,8 +11092,6 @@ }, "node_modules/tsconfig-paths": { "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, "license": "MIT", "dependencies": { @@ -12751,15 +11103,11 @@ }, "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==", "devOptional": true, "license": "0BSD" }, "node_modules/tsx": { "version": "4.22.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", - "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -12777,8 +11125,6 @@ }, "node_modules/tunnel-agent": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" @@ -12789,8 +11135,6 @@ }, "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": { @@ -12802,8 +11146,6 @@ }, "node_modules/type-fest": { "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -12814,8 +11156,6 @@ }, "node_modules/type-is": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", "dependencies": { "content-type": "^2.0.0", @@ -12832,8 +11172,6 @@ }, "node_modules/type-is/node_modules/content-type": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", "license": "MIT", "engines": { "node": ">=18" @@ -12845,8 +11183,6 @@ }, "node_modules/type-is/node_modules/mime-db": { "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -12854,8 +11190,6 @@ }, "node_modules/type-is/node_modules/mime-types": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -12870,8 +11204,6 @@ }, "node_modules/typed-array-buffer": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "license": "MIT", "dependencies": { @@ -12885,8 +11217,6 @@ }, "node_modules/typed-array-byte-length": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, "license": "MIT", "dependencies": { @@ -12905,8 +11235,6 @@ }, "node_modules/typed-array-byte-offset": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12927,8 +11255,6 @@ }, "node_modules/typed-array-length": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, "license": "MIT", "dependencies": { @@ -12948,8 +11274,6 @@ }, "node_modules/typescript": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -12962,8 +11286,6 @@ }, "node_modules/typescript-eslint": { "version": "8.60.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.0.tgz", - "integrity": "sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==", "dev": true, "license": "MIT", "dependencies": { @@ -12986,8 +11308,6 @@ }, "node_modules/unbox-primitive": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, "license": "MIT", "dependencies": { @@ -13004,9 +11324,7 @@ } }, "node_modules/undici": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", - "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==", + "version": "6.28.0", "license": "MIT", "engines": { "node": ">=18.17" @@ -13014,14 +11332,10 @@ }, "node_modules/undici-types": { "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "license": "MIT" }, "node_modules/unified": { "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -13039,8 +11353,6 @@ }, "node_modules/unist-util-find-after": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", - "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -13053,8 +11365,6 @@ }, "node_modules/unist-util-is": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -13066,8 +11376,6 @@ }, "node_modules/unist-util-position": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -13079,8 +11387,6 @@ }, "node_modules/unist-util-remove-position": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", - "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -13093,8 +11399,6 @@ }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -13106,8 +11410,6 @@ }, "node_modules/unist-util-visit": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -13121,8 +11423,6 @@ }, "node_modules/unist-util-visit-parents": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -13135,8 +11435,6 @@ }, "node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { @@ -13145,8 +11443,6 @@ }, "node_modules/unpipe": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -13154,8 +11450,6 @@ }, "node_modules/unrs-resolver": { "version": "1.12.2", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", - "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -13192,8 +11486,6 @@ }, "node_modules/unzipper": { "version": "0.12.3", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz", - "integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==", "dev": true, "license": "MIT", "dependencies": { @@ -13205,9 +11497,7 @@ } }, "node_modules/unzipper/node_modules/fs-extra": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", - "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "version": "11.4.0", "dev": true, "license": "MIT", "dependencies": { @@ -13221,8 +11511,6 @@ }, "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": { @@ -13231,8 +11519,6 @@ }, "node_modules/use-sync-external-store": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -13240,21 +11526,15 @@ }, "node_modules/utf8-byte-length": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", "dev": true, "license": "(WTFPL OR MIT)" }, "node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, "node_modules/vary": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -13262,8 +11542,6 @@ }, "node_modules/vfile": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -13276,8 +11554,6 @@ }, "node_modules/vfile-location": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", - "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -13290,8 +11566,6 @@ }, "node_modules/vfile-message": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -13303,16 +11577,14 @@ } }, "node_modules/vite": { - "version": "8.0.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", - "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "version": "8.2.1", "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.2", - "tinyglobby": "^0.2.16" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -13328,7 +11600,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", + "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -13381,8 +11653,6 @@ }, "node_modules/vitest": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz", - "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", "dev": true, "license": "MIT", "dependencies": { @@ -13471,8 +11741,6 @@ }, "node_modules/wait-on": { "version": "9.0.10", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.10.tgz", - "integrity": "sha512-rCoJEhvMr0X6alHmwc9abbrA5ZrLZFKpFQVKPNFwl2h7DapXOGdmimIHDtLOWhT4PjhZhxFEtZoQgEXbkDWdZw==", "dev": true, "license": "MIT", "dependencies": { @@ -13491,8 +11759,6 @@ }, "node_modules/web-namespaces": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", - "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", "license": "MIT", "funding": { "type": "github", @@ -13501,8 +11767,6 @@ }, "node_modules/webcrypto-core": { "version": "1.9.2", - "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz", - "integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==", "dev": true, "license": "MIT", "dependencies": { @@ -13515,8 +11779,6 @@ }, "node_modules/whatwg-mimetype": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "dev": true, "license": "MIT", "engines": { @@ -13525,8 +11787,6 @@ }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -13540,8 +11800,6 @@ }, "node_modules/which-boxed-primitive": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, "license": "MIT", "dependencies": { @@ -13560,8 +11818,6 @@ }, "node_modules/which-builtin-type": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -13588,8 +11844,6 @@ }, "node_modules/which-collection": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, "license": "MIT", "dependencies": { @@ -13607,8 +11861,6 @@ }, "node_modules/which-typed-array": { "version": "1.1.21", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.21.tgz", - "integrity": "sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw==", "dev": true, "license": "MIT", "dependencies": { @@ -13629,8 +11881,6 @@ }, "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": { @@ -13646,8 +11896,6 @@ }, "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": { @@ -13656,8 +11904,6 @@ }, "node_modules/wrap-ansi": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { @@ -13674,14 +11920,10 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, "node_modules/ws": { "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, "license": "MIT", "engines": { @@ -13702,8 +11944,6 @@ }, "node_modules/xmlbuilder": { "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", "dev": true, "license": "MIT", "engines": { @@ -13712,8 +11952,6 @@ }, "node_modules/y18n": { "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, "license": "ISC", "engines": { @@ -13722,15 +11960,11 @@ }, "node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, "license": "ISC" }, "node_modules/yaml": { "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -13744,8 +11978,6 @@ }, "node_modules/yargs": { "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "license": "MIT", "dependencies": { @@ -13763,29 +11995,14 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { "node": ">=12" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "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": { @@ -13797,8 +12014,6 @@ }, "node_modules/zod": { "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -13806,8 +12021,6 @@ }, "node_modules/zod-to-json-schema": { "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", "license": "ISC", "peerDependencies": { "zod": "^3.25.28 || ^4" @@ -13815,8 +12028,6 @@ }, "node_modules/zustand": { "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", "license": "MIT", "dependencies": { "use-sync-external-store": "^1.2.2" @@ -13843,8 +12054,6 @@ }, "node_modules/zwitch": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", "license": "MIT", "funding": { "type": "github", diff --git a/package.json b/package.json index 1ffb4cfa2..53fe2b522 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,13 @@ "engines": { "node": ">=20" }, + "overrides": { + "@huggingface/transformers": { + "sharp": "$sharp" + } + }, "devDependencies": { + "@huggingface/transformers": "^3.8.0", "@eslint/js": "^9.39.4", "@types/node": "^25.9.1", "@vitest/coverage-v8": "^4.1.7", @@ -77,6 +83,7 @@ "eslint": "^9.39.4", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", + "sharp": "0.35.3", "tsx": "^4.22.3", "typescript": "^6.0.3", "typescript-eslint": "^8.60.0", diff --git a/scripts/smoke-memory-docker.mjs b/scripts/smoke-memory-docker.mjs new file mode 100644 index 000000000..c71833a96 --- /dev/null +++ b/scripts/smoke-memory-docker.mjs @@ -0,0 +1,106 @@ +const baseUrl = (process.env.MEMMY_SMOKE_BASE_URL ?? "http://127.0.0.1:28960/api/v1").replace(/\/$/u, ""); +const token = process.env.MEMMY_SMOKE_TOKEN ?? "release-smoke-token"; +const timeoutMs = Number(process.env.MEMMY_SMOKE_TIMEOUT_MS ?? 30_000); +const runId = crypto.randomUUID(); +const originalTitle = `Release smoke original ${runId}`; +const originalContent = `Original release smoke content ${runId}.`; +const namespaceHeaders = { + authorization: `Bearer ${token}`, + "x-memmy-user-id": "release-smoke-user", + "x-memmy-project-id": "release-smoke-project", + "x-memmy-workspace-id": "release-smoke-workspace", + "content-type": "application/json" +}; + +async function request(path, options = {}) { + const response = await fetch(`${baseUrl}${path}`, { + ...options, + headers: { ...namespaceHeaders, ...options.headers } + }); + const body = await response.json(); + if (!response.ok) { + throw new Error(`${response.status} ${JSON.stringify(body)}`); + } + return body; +} + +async function waitForHealth() { + const deadline = Date.now() + timeoutMs; + let lastError; + while (Date.now() < deadline) { + try { + const health = await request("/health"); + if (health.ok === true && health.storage?.ready === true) { + return health; + } + lastError = new Error(`Memory health is not ready: ${JSON.stringify(health)}`); + } catch (error) { + lastError = error; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + throw lastError ?? new Error("Memory health check timed out"); +} + +const health = await waitForHealth(); +const added = await request("/memory/add?source=codex", { + method: "POST", + body: JSON.stringify({ + layer: "L2", + title: originalTitle, + content: originalContent, + tags: ["release-smoke"], + deferProcessing: true + }) +}); +if (typeof added.id !== "string") { + throw new Error("Memory add did not return an id"); +} + +const memoryPath = `/memory/${encodeURIComponent(added.id)}`; +const edited = await request(`${memoryPath}/edit?source=codex`, { + method: "POST", + body: JSON.stringify({ + version: 1, + title: "Release smoke edited", + content: "Edited release smoke content.", + tags: ["release-smoke"], + reason: "container smoke edit" + }) +}); +if (edited.ok !== true || edited.version !== 2) { + throw new Error(`Memory edit assertion failed: ${JSON.stringify(edited)}`); +} + +const history = await request(`${memoryPath}/history?source=codex`); +const historyVersions = history.items?.map((item) => item.version) ?? []; +if (history.currentVersion !== 2 || !historyVersions.includes(1) || !historyVersions.includes(2)) { + throw new Error(`Memory history assertion failed: ${JSON.stringify(history)}`); +} + +const restored = await request(`${memoryPath}/history/1/restore?source=codex`, { + method: "POST", + body: JSON.stringify({ version: 2, reason: "container smoke restore" }) +}); +if (restored.ok !== true || restored.version !== 3 || restored.restoredVersion !== 1) { + throw new Error(`Memory restore assertion failed: ${JSON.stringify(restored)}`); +} + +const detail = await request(`${memoryPath}?source=codex`); +if ( + detail.item?.version !== 3 || + detail.item?.title !== originalTitle || + detail.item?.body !== originalContent +) { + throw new Error(`Restored memory assertion failed: ${JSON.stringify(detail)}`); +} + +console.log(JSON.stringify({ + health: health.ok, + storageReady: health.storage.ready, + version: health.version, + memoryId: added.id, + editVersion: edited.version, + restoreVersion: restored.version, + restoredFrom: restored.restoredVersion +})); diff --git a/tests/release-workflow.test.ts b/tests/release-workflow.test.ts index 484d98b66..f0e48314b 100644 --- a/tests/release-workflow.test.ts +++ b/tests/release-workflow.test.ts @@ -5,9 +5,13 @@ import { describe, expect, it } from "vitest"; import YAML from "yaml"; const workflowPath = resolve(import.meta.dirname, "../.github/workflows/github-release.yml"); +const dockerWorkflowPath = resolve(import.meta.dirname, "../.github/workflows/docker-publish.yml"); +const memoryDockerfilePath = resolve(import.meta.dirname, "../Memory/Dockerfile"); const repoRoot = resolve(import.meta.dirname, ".."); const source = readFileSync(workflowPath, "utf8"); const workflow = YAML.parse(source); +const dockerWorkflow = YAML.parse(readFileSync(dockerWorkflowPath, "utf8")); +const memoryDockerfile = readFileSync(memoryDockerfilePath, "utf8"); const releaseJob = workflow.jobs.release; const steps = releaseJob.steps as Array>; const script = (name: string) => String(steps.find((step) => step.name === name)?.run ?? ""); @@ -151,13 +155,25 @@ describe("GitHub release workflow", () => { }); it("uses the release environment, minimal permissions, and per-version concurrency", () => { - expect(workflow.permissions).toEqual({ contents: "write" }); + expect(workflow.permissions).toEqual({}); + expect(releaseJob.permissions).toEqual({ contents: "write" }); expect(releaseJob.environment).toBe("release"); expect(workflow.concurrency["cancel-in-progress"]).toBe(false); expect(workflow.concurrency.group).toContain("inputs.version"); expect(workflow.concurrency.group).toContain("pull_request.head.ref"); }); + it("publishes the verified release commit through the reusable Docker workflow", () => { + const dockerJob = workflow.jobs.docker; + expect(dockerJob.needs).toBe("release"); + expect(dockerJob.permissions).toEqual({ contents: "read", packages: "write" }); + expect(dockerJob.uses).toBe("./.github/workflows/docker-publish.yml"); + expect(dockerJob.with).toEqual({ + tag: "${{ needs.release.outputs.tag }}", + ref: "${{ needs.release.outputs.target_sha }}" + }); + }); + it("embeds the repository .env required by packaged desktop runtimes", () => { for (const config of packagingConfigs) { const packagingSource = readFileSync( @@ -169,3 +185,91 @@ describe("GitHub release workflow", () => { } }); }); + +describe("Memory Docker publish workflow", () => { + const smokeJob = dockerWorkflow.jobs.smoke; + const publishJob = dockerWorkflow.jobs.publish; + const publishSteps = publishJob.steps as Array>; + const publishStep = publishSteps.find((step) => step.name === "Build and push image"); + + it("publishes version tags and supports an explicit manual tag", () => { + expect(dockerWorkflow.on.push.tags).toEqual(["v*.*.*"]); + expect(dockerWorkflow.on.workflow_call.inputs).toMatchObject({ + tag: { required: true, type: "string" }, + ref: { required: true, type: "string" } + }); + expect(dockerWorkflow.on.workflow_dispatch.inputs.tag).toMatchObject({ + required: true, + type: "string" + }); + }); + + it("rejects a release tag that disagrees with the package version", () => { + const validation = publishSteps.find((step) => step.name === "Validate release tag"); + expect(validation?.env).toEqual({ + IMAGE_TAG: "${{ github.event_name == 'push' && github.ref_name || inputs.tag }}" + }); + expect(validation?.run).toContain("require('./package.json').version"); + expect(validation?.run).toContain('test "$IMAGE_TAG" = "v$version"'); + }); + + it("uses scoped GHCR permissions and the repository token", () => { + expect(dockerWorkflow.permissions).toEqual({ contents: "read", packages: "write" }); + const login = publishSteps.find((step) => step.name === "Log in to GHCR"); + expect(login?.with).toMatchObject({ + registry: "ghcr.io", + username: "${{ github.actor }}", + password: "${{ secrets.GITHUB_TOKEN }}" + }); + }); + + it("builds the Memory Dockerfile for amd64 and arm64 with attestations", () => { + expect(publishJob.needs).toBe("smoke"); + expect(publishStep?.uses).toBe("docker/build-push-action@v6"); + expect(publishStep?.with).toMatchObject({ + context: ".", + file: "Memory/Dockerfile", + platforms: "linux/amd64,linux/arm64", + push: true, + provenance: "mode=max", + sbom: true + }); + }); + + it("runs the built image through health, history, and restore smoke checks before publishing", () => { + const smokeSteps = smokeJob.steps as Array>; + const smokeBuild = smokeSteps.find((step) => step.name === "Build smoke image"); + expect(smokeBuild?.with).toMatchObject({ + file: "Memory/Dockerfile", + platforms: "linux/amd64", + load: true, + tags: "memmy-memory:smoke" + }); + const smokeRun = smokeSteps.find((step) => step.name === "Run container smoke test"); + expect(smokeRun?.run).toContain("node scripts/smoke-memory-docker.mjs"); + expect(smokeRun?.run).toContain("uid=1000,gid=1000"); + }); + + it("installs a locked production tree and blocks high-severity runtime advisories", () => { + const rootManifest = readJson("package.json") as { overrides?: Record }; + const memoryManifest = readJson("Memory/package.json") as { overrides?: Record }; + const runtimeLock = readJson("Memory/package-lock.json") as { + packages?: Record; + }; + + expect(rootManifest.overrides).toMatchObject({ + "@huggingface/transformers": { sharp: "$sharp" } + }); + expect(memoryManifest.overrides).toMatchObject({ + "@huggingface/transformers": { sharp: "$sharp" } + }); + expect(readJson("Memory/package.json")).toMatchObject({ + dependencies: { sharp: "0.35.3" } + }); + expect(runtimeLock.packages?.["node_modules/sharp"]?.version).toBe("0.35.3"); + expect(memoryDockerfile).toContain("cp Memory/package-lock.json /runtime/package-lock.json"); + expect(memoryDockerfile).toContain("npm ci --omit=dev"); + expect(memoryDockerfile).toContain("npm audit --omit=dev --audit-level=high"); + expect(memoryDockerfile).not.toContain("npm install --omit=dev --no-package-lock"); + }); +}); From 25adf2e900286855102018943c661f3b7e022493 Mon Sep 17 00:00:00 2001 From: bluewater_cg Date: Mon, 10 Aug 2026 01:34:30 -0700 Subject: [PATCH 14/23] docs: design authoritative project context pack --- .../2026-08-10-project-context-pack-design.md | 261 ++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-10-project-context-pack-design.md diff --git a/docs/superpowers/specs/2026-08-10-project-context-pack-design.md b/docs/superpowers/specs/2026-08-10-project-context-pack-design.md new file mode 100644 index 000000000..181226001 --- /dev/null +++ b/docs/superpowers/specs/2026-08-10-project-context-pack-design.md @@ -0,0 +1,261 @@ +# Project Context Pack Design + +Date: 2026-08-10 +Status: Approved for planning + +## Problem + +Memmy stores project-scoped memories and `turns/start` retrieves relevant memories, but external CLI agents can still forget facts the user has already established. The current project context pack is a panel read model: it selects memories with keyword patterns and renders them for inspection. It is not an authoritative project state and is not guaranteed to be injected into every Claude Code, Codex, or Pi turn. + +Semantic retrieval remains useful for historical detail, but it cannot guarantee that a confirmed project goal, hard constraint, or active work item is present when the current prompt uses different wording. The system must separate facts that are always required from memories that are only relevant to a particular query. + +## Goals + +- Give each project one confirmed primary goal that every CLI agent receives on every turn. +- Support both short and long goals without truncating the authoritative source. +- Keep several work items under the primary goal while requiring the user to select the current focus manually. +- Keep confirmed decisions and hard constraints present independently of semantic similarity. +- Preserve ordinary memory retrieval as a supplemental source of history and detail. +- Make updates visible on the next turn without relying on hook-process caches. +- Preserve project namespace isolation, source-agent provenance, version history, and supersession relations. + +## Non-goals + +- Replacing the existing memory retrieval pipeline. +- Automatically changing the active work item based on conversational guesses. +- Treating unconfirmed model-generated summaries as authoritative project state. +- Introducing another project identifier or a second configuration system. +- Integrating an external agent runtime or control plane. + +## Considered Approaches + +### 1. Improve semantic retrieval only + +Increase result limits, adjust ranking, or boost project-context tags. + +This is the smallest change, but it cannot guarantee recall. A confirmed constraint can still be absent when the current prompt has low lexical or semantic similarity. It does not solve the reported failure mode. + +### 2. Inject the complete project context pack every turn + +Render all selected conventions, commands, architecture facts, tasks, and preferences on every turn. + +This improves recall initially, but token usage and noise grow with the project. The current pack is also generated by keyword matching, so injection would make heuristic selections look authoritative. + +### 3. Authoritative core plus supplemental retrieval + +Maintain a compact, confirmed project state that is always injected. Preserve full goal details and ordinary memories for explicit or query-driven expansion. + +This is the selected approach. It guarantees the presence of critical facts while keeping the stable per-turn payload bounded. + +## Domain Model + +### Project Goal + +A project has at most one active primary goal. + +Fields: + +- `title`: one-line project direction; always injected. +- `summary`: stable concise representation; always injected. +- `detail`: complete source text for a long goal; available on demand. +- `acceptanceCriteria`: confirmed conditions for considering the goal achieved. +- `constraints`: confirmed project-wide hard constraints. +- `status`: `candidate`, `active`, `completed`, or `archived`. +- `version`: monotonically increasing within the project. +- `updatedAt`: last confirmed update time. +- `sourceMemoryIds`: evidence memories used to produce the proposal. +- `supersedesId`: previous confirmed goal version, when applicable. + +A short goal can use the same text for `title`, `summary`, and `detail`. A long goal keeps the complete text in `detail`; `summary` is a bounded execution-oriented view, not a replacement for the source. + +Only a confirmed goal can become `active`. A generated proposal remains `candidate` and must not enter fixed CLI context. + +### Work Item + +A project can contain several work items under its primary goal. + +Fields: + +- `title` +- `summary` +- `status`: `pending`, `active`, `blocked`, `completed`, or `archived` +- `nextStep` +- `acceptanceCriteria` +- `constraints` +- `updatedAt` +- `sourceMemoryIds` + +Exactly zero or one work item is the project focus. Focus selection is an explicit user action. The system never switches focus from ordinary conversation. + +### Confirmed Project Facts + +Confirmed decisions and hard constraints remain separate normalized facts so they can be versioned and superseded independently. A new fact that conflicts with an active fact becomes a candidate for review; it does not silently replace the active fact. + +## Proposal And Confirmation + +Memmy may generate a goal proposal from: + +- explicit user statements; +- current workspace-scoped memories; +- verified tool or test outcomes; +- current repository evidence when available through the authorized summarization flow. + +A proposal includes the intended title, summary, detail, acceptance criteria, constraints, and evidence references. The user can approve, edit and approve, or reject it. + +Approval activates the new version and records a supersession relation when replacing an older goal. Rejection leaves the current project context unchanged. + +Work items may also be proposed, but selecting the current work item is always manual. Creating a work item and focusing it are separate actions. + +## Context Pack Views + +The project context pack has two views over the same authoritative records. + +### Stable Turn Context + +Injected on every `turns/start` call for a resolved project namespace: + +- project identity; +- primary goal title and summary; +- primary goal status and version; +- selected work item summary, status, next step, and acceptance criteria; +- active confirmed decisions; +- active hard constraints; +- last confirmed update timestamp. + +If no work item is selected, the turn continues normally. The context states that no work item is selected and gives a brief non-blocking reminder to select one. It must not infer a focus. + +The stable context carries a distinct `project_context` marker and is rendered separately from historical memory. The host agent is told that it is current confirmed state, while retrieved memories are supporting historical evidence. + +### Expanded Context + +Fetched only when requested or relevant: + +- complete long-goal detail; +- full work-item history; +- completed and archived work items; +- supporting memories and source relations; +- architecture and command details selected by normal retrieval. + +The expanded view may be larger. It must not redefine the active goal or work item. + +## Turn Start Data Flow + +1. The CLI hook resolves the workspace path and opens the project-scoped session through `memmy.agent.v1`. +2. `turns/start` resolves the authoritative namespace. +3. The Memory service reads the current confirmed project state. +4. The service renders the stable turn context from active records. +5. Existing semantic retrieval runs for the current user prompt. +6. The response returns stable project context and supplemental memory context as separate fields or separately marked sections. +7. The hook injects both sections into the host CLI prompt, with the stable project context first. +8. The hook stores all injected memory IDs and project-state version in turn provenance. + +The hook fetches project state on every turn. It does not keep an authoritative cached copy, so an approved update becomes visible on the next prompt. + +## Precedence + +For project state: + +1. current confirmed project records; +2. current executable repository and verified tool evidence when a task explicitly checks them; +3. supplemental active memories; +4. historical or superseded memories. + +If supplemental retrieval conflicts with confirmed project state, the host receives the conflict as historical evidence and follows the confirmed state. The conflict should be surfaced for review rather than silently merged. + +A current repository check can show that stored project state is stale, but it does not automatically rewrite a user-confirmed goal or decision. + +## API Boundary + +The existing project namespace remains the owner of the records. The implementation should extend current Memory service and local API contracts instead of creating a parallel store. + +Required operations: + +- read the current project state and both context views; +- create a goal proposal; +- approve, edit-and-approve, or reject a proposal; +- create and list work items; +- select or clear the focus work item; +- update work-item status and next step; +- read version and supersession history. + +Mutation requests must carry project namespace, source agent, adapter ID, request ID, and provenance. Read responses include the active version and generated timestamp. + +## UI And CLI Behavior + +The existing Project Context Pack dialog becomes an inspection and confirmation surface rather than a keyword-only report. It shows: + +- current primary goal; +- pending proposal, when present; +- active work-item focus; +- available work items; +- confirmed decisions and constraints; +- version and evidence links. + +CLI integrations require a small explicit command surface for selecting a work item and expanding context. Ordinary prompts remain unblocked when no work item is selected. + +Exact command names are an implementation-planning decision and should reuse the existing Memmy command installation pattern. + +## Failure Handling + +- Memory unavailable: the hook must report that current project context was unavailable; it must not fabricate or reuse stale authoritative state. +- No confirmed goal: inject a compact `no confirmed project goal` state and continue. +- Candidate exists but is unconfirmed: do not inject candidate content as current state. +- Missing work-item focus: inject the primary goal and a non-blocking reminder. +- Context exceeds its stable budget: preserve title, hard constraints, active work item, and acceptance criteria; compact summaries deterministically. Full detail remains retrievable. +- Namespace mismatch: fail closed for project state; never inject another workspace's goal. +- Conflicting active records: return an explicit conflict status and exclude ambiguous records from authoritative prose until reviewed. + +## Token Budget + +The stable view must have a fixed character or token budget independent of the total memory count. Priority within that budget is: + +1. hard constraints; +2. primary goal title and summary; +3. selected work item and next step; +4. acceptance criteria; +5. confirmed decisions; +6. status metadata. + +Full goal detail is never destroyed to meet the stable-view budget; it remains in the expanded view. + +## Verification + +### Contract tests + +- An approved goal appears in stable context for unrelated prompts. +- An unconfirmed candidate never appears in stable context. +- Updating a goal creates a new version and supersedes the previous version. +- A short goal preserves its complete text. +- A long goal keeps full detail while stable context uses the bounded summary. +- Selecting a work item changes only the focus section, not the primary goal. +- Clearing focus produces a non-blocking reminder. +- Conflicting or cross-namespace records are not injected as authoritative context. + +### Hook tests + +- Claude Code, Codex, and Pi receive the same project-state version for the same workspace. +- Stable project context appears before supplemental memory context. +- Every turn fetches current state; a newly approved update appears on the next turn. +- Turn completion provenance records the project-state version and injected source memory IDs. + +### End-to-end scenario + +1. Create a project with a long confirmed goal and a hard constraint. +2. Create two work items and manually select the first. +3. Start a fresh CLI session and ask an unrelated question. +4. Verify the injected context contains the goal summary, constraint, and first work item. +5. Switch to a different CLI and verify the same project-state version. +6. Manually select the second work item. +7. On the next prompt, verify the primary goal is unchanged and only the focus work item changes. +8. Propose but do not approve a goal revision; verify the revision is absent from stable context. +9. Approve the revision; verify it appears on the next prompt and the previous goal is retained in history. + +## Migration + +Existing memories remain unchanged. The current keyword-based context pack can continue as an expanded historical view during migration. No existing memory is promoted to authoritative state without confirmation. + +The first project goal may be proposed from existing high-confidence project memories, but it requires user approval before activation. Existing project namespaces, provenance fields, memory IDs, and supersession records are reused. + +## Completion Criteria + +The feature is complete when a confirmed goal, hard constraint, and manually selected work item survive fresh sessions and CLI switches because they are present in every project-scoped `turns/start` response, while unconfirmed proposals and unrelated historical memories remain outside the stable authoritative section. From 7de46ebcf5c4fe438edbe12e3bc3a6232a875440 Mon Sep 17 00:00:00 2001 From: bluewater_cg Date: Mon, 10 Aug 2026 06:59:43 -0700 Subject: [PATCH 15/23] feat(memory): add authoritative project context --- Memory/package.json | 7 + Memory/src/index.ts | 5 + Memory/src/server/http.ts | 147 ++++++- Memory/src/service/memory-service.ts | 108 ++++-- .../project-context-service.ts | 367 ++++++++++++++++++ .../project-context/project-context-types.ts | 107 +++++ Memory/src/service/read-model/panel-read.ts | 38 +- Memory/src/service/read-model/panel.ts | 25 +- .../retrieval/indexed-candidate-pool.ts | 2 +- .../service/retrieval/retrieval-service.ts | 21 +- .../service/session/session-turn-service.ts | 169 +++++--- Memory/src/storage/repositories.ts | 118 ++++++ Memory/src/storage/schema.ts | 74 +++- Memory/src/types.ts | 28 ++ .../contract/memory-rest-service.test.ts | 60 +++ .../project-context-repository.test.ts | 157 ++++++++ Memory/tests/repository/sqlite-schema.test.ts | 17 +- .../project-context-service.test.ts | 275 +++++++++++++ .../service/read-model/panel-read.test.ts | 44 ++- .../retrieval/query-and-filter.test.ts | 54 +++ .../service/session/turn-capture.test.ts | 239 ++++++++++++ Memory/tsconfig.json | 2 +- 22 files changed, 1964 insertions(+), 100 deletions(-) create mode 100644 Memory/src/service/project-context/project-context-service.ts create mode 100644 Memory/src/service/project-context/project-context-types.ts create mode 100644 Memory/tests/repository/project-context-repository.test.ts create mode 100644 Memory/tests/service/project-context/project-context-service.test.ts diff --git a/Memory/package.json b/Memory/package.json index 0bf031e7c..ea2a64ed0 100644 --- a/Memory/package.json +++ b/Memory/package.json @@ -4,6 +4,13 @@ "private": true, "type": "module", "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "default": "./dist/src/index.js" + } + }, "bin": { "memmy-memory": "./dist/src/cli/index.js" }, diff --git a/Memory/src/index.ts b/Memory/src/index.ts index 68f60ca7f..b10754411 100644 --- a/Memory/src/index.ts +++ b/Memory/src/index.ts @@ -41,6 +41,9 @@ export type { } from "./storage/backend.js"; export { SCHEMA_VERSION, SCHEMA_MIGRATION_ID } from "./storage/schema.js"; export { MemoryService } from "./service/memory-service.js"; +export { ProjectContextService } from "./service/project-context/project-context-service.js"; +export { Repositories } from "./storage/repositories.js"; +export type * from "./service/project-context/project-context-service.js"; export { API_ROUTES, createMemoryHttpServer, listenMemoryHttpServer } from "./server/http.js"; export { DEFAULT_MEMMY_CONFIG, loadMemmyConfig, resolveEvolutionConfig } from "./config/index.js"; export { DEFAULT_NAMESPACE_SOURCE } from "./types.js"; @@ -49,3 +52,5 @@ export { createLlmClient } from "./model/llm.js"; export type * from "./types.js"; export type * from "./config/index.js"; export type * from "./model/types.js"; + +export type * from "./service/project-context/project-context-types.js"; \ No newline at end of file diff --git a/Memory/src/server/http.ts b/Memory/src/server/http.ts index 4f894a3bf..3ebf3c5f4 100644 --- a/Memory/src/server/http.ts +++ b/Memory/src/server/http.ts @@ -3,6 +3,13 @@ import { randomUUID } from "node:crypto"; import type { AddressInfo } from "node:net"; import { createMemoryLogger, memoryErrorFields } from "../logging/logger.js"; import { memoryPanelHtml } from "../viewer/static.js"; +import type { + ProjectGoalDecisionRequest, + ProjectWorkItemCreateRequest, + ProjectWorkItemSelectRequest, + ProjectWorkItemUpdateRequest +} from "../service/project-context/project-context-service.js"; +import type { ProjectContextProposeGoalRequest } from "../service/project-context/project-context-types.js"; import type { MemoryAddRequest, MemoryGovernanceRequest, @@ -73,6 +80,13 @@ export const API_ROUTES = [ "GET /api/v1/panel/overview", "GET /api/v1/panel/evolution", "GET /api/v1/panel/context-pack", + "GET /api/v1/project-context/state", + "POST /api/v1/project-context/goals/propose", + "POST /api/v1/project-context/goals/:id/approve", + "POST /api/v1/project-context/goals/:id/reject", + "POST /api/v1/project-context/work-items", + "PATCH /api/v1/project-context/work-items/:id", + "PUT /api/v1/project-context/focus", "GET /api/v1/panel/context-packs", "GET /api/v1/panel/namespace-audit", "GET /api/v1/panel/analysis", @@ -681,10 +695,60 @@ async function routeRequest( requirePanelRead(principal); return service.evolutionOverview({ namespace: principal.namespace }); } - + if (method === "GET" && path === "/api/v1/project-context/state") { + requirePanelRead(principal); + const namespace = projectContextNamespace(url, principal); + const state = service.readProjectContext(namespace); + return { ...state, activeGoal: state.activeGoal ?? null, focusedWorkItem: state.focusedWorkItem ?? null }; + } + if (method === "POST" && path === "/api/v1/project-context/goals/propose") { + requirePanelWrite(principal); + const request = projectContextProposeGoal(body, "project-context.goals.propose", principal); + return service.idempotent("project-context.goals.propose", request, request, () => service.proposeProjectGoal(request)); + } + const goalApprove = match(path, /^\/api\/v1\/project-context\/goals\/([^/]+)\/approve$/); + if (method === "POST" && goalApprove) { + requirePanelWrite(principal); + const request = projectContextGoalDecision(body, "project-context.goals.approve", principal); + const candidateId = decodeMatchSegment(goalApprove, 1); + return service.idempotent("project-context.goals.approve", request, { candidateId, request }, () => service.approveProjectGoal({ namespace: request.namespace, candidateId })); + } + const goalReject = match(path, /^\/api\/v1\/project-context\/goals\/([^/]+)\/reject$/); + if (method === "POST" && goalReject) { + requirePanelWrite(principal); + const request = projectContextGoalDecision(body, "project-context.goals.reject", principal); + const candidateId = decodeMatchSegment(goalReject, 1); + return service.idempotent("project-context.goals.reject", request, { candidateId, request }, () => service.rejectProjectGoal({ namespace: request.namespace, candidateId })); + } + if (method === "POST" && path === "/api/v1/project-context/work-items") { + requirePanelWrite(principal); + const request = projectContextWorkItemCreate(body, "project-context.work-items.create", principal); + return service.idempotent("project-context.work-items.create", request, request, () => service.createProjectWorkItem(request)); + } + const workItemUpdate = match(path, /^\/api\/v1\/project-context\/work-items\/([^/]+)$/); + if (method === "PATCH" && workItemUpdate) { + requirePanelWrite(principal); + const request = projectContextWorkItemUpdate(body, "project-context.work-items.update", principal); + const workItemId = decodeMatchSegment(workItemUpdate, 1); + return service.idempotent("project-context.work-items.update", request, { workItemId, request }, () => service.updateProjectWorkItem({ ...request, workItemId })); + } + if (method === "PUT" && path === "/api/v1/project-context/focus") { + requirePanelWrite(principal); + const request = projectContextFocus(body, "project-context.focus", principal); + return service.idempotent("project-context.focus", request, request, () => service.selectProjectWorkItem(request) ?? null); + } if (method === "GET" && path === "/api/v1/panel/context-pack") { requirePanelRead(principal); - return service.projectContextPack({ namespace: principal.namespace }); + const pack = service.projectContextPack({ namespace: principal.namespace }); + if (!principal.namespace) return pack; + const state = service.readProjectContext(principal.namespace); + return { + ...pack, + authoritative: { + state: { ...state, activeGoal: state.activeGoal ?? null, focusedWorkItem: state.focusedWorkItem ?? null }, + stable: service.renderStableProjectContext(principal.namespace) + } + }; } if (method === "GET" && path === "/api/v1/panel/context-packs") { @@ -1040,10 +1104,13 @@ function publicStartTurnResponse(result: unknown): Record { contextPacketId: record.contextPacketId, sessionId: record.sessionId, episodeId: record.episodeId, + closedEpisodeIds: record.closedEpisodeIds, searchEventId: record.searchEventId, injectedContext: record.injectedContext, + projectContext: record.projectContext, sourceMemoryIds: record.sourceMemoryIds, hits: record.hits, + droppedDueToBudget: record.droppedDueToBudget, status: record.status, serverTime: record.serverTime }; @@ -1324,6 +1391,10 @@ function requireMemoryWrite(principal: AuthPrincipal): void { function requirePanelRead(principal: AuthPrincipal): void { requireAnyScope(principal, ["panel:read", "panel:write", "memory:read", "memory:write", "admin:read", "admin:write"]); } +function requirePanelWrite(principal: AuthPrincipal): void { + requireAnyScope(principal, ["panel:write", "memory:write", "admin:write"]); +} + function requireAdminWrite(principal: AuthPrincipal): void { requireAnyScope(principal, ["admin:write"]); @@ -1385,6 +1456,78 @@ function envelopeWithPrincipal>( } as T & RequestEnvelope; } +function projectContextNamespace(url: URL, principal: AuthPrincipal): RuntimeNamespace { + const raw = url.searchParams.get("namespace"); + let requested: RuntimeNamespace | undefined; + if (raw) { + try { + const parsed = JSON.parse(raw) as unknown; + requested = isRecord(parsed) ? parsed as unknown as RuntimeNamespace : undefined; + } catch { + throw new MemoryServiceError("invalid_argument", "project-context namespace must be valid JSON"); + } + } + const namespace = mergeNamespaces(requested, principal.namespace); + assertNamespaceScope(requested, principal.namespace); + if (!namespace) throw new MemoryServiceError("invalid_argument", "project-context namespace is required"); + return namespace; +} + +function projectContextMutation(body: unknown, routeName: string, principal: AuthPrincipal): RequestEnvelope & Record & { namespace: RuntimeNamespace; provenance: Record } { + const raw = asObject(body, routeName); + if (!isRecord(raw.namespace)) throw new MemoryServiceError("invalid_argument", `${routeName} namespace is required`); + if (!isRecord(raw.provenance)) throw new MemoryServiceError("invalid_argument", `${routeName} provenance is required`); + requireStringField(raw, "source", routeName); + requireStringField(raw, "adapterId", routeName); + requireStringField(raw, "requestId", routeName); + requireStringField(raw.provenance, "sourceAgent", `${routeName} provenance`); + requireStringField(raw.provenance, "capturedAt", `${routeName} provenance`); + if (!Array.isArray(raw.provenance.sourceMemoryIds) || raw.provenance.sourceMemoryIds.some((id) => typeof id !== "string" || !id.trim())) throw new MemoryServiceError("invalid_argument", `${routeName} provenance sourceMemoryIds must be an array of strings`); + const request = envelopeWithPrincipal(raw, principal); + if (!request.namespace) throw new MemoryServiceError("invalid_argument", `${routeName} project namespace is required`); + return { ...request, namespace: request.namespace, provenance: raw.provenance }; +} + +function projectContextProposeGoal(body: unknown, routeName: string, principal: AuthPrincipal): ProjectContextProposeGoalRequest & RequestEnvelope { + const request = projectContextMutation(body, routeName, principal); + if (typeof request.title !== "string" || !request.title.trim() || typeof request.summary !== "string" || typeof request.detail !== "string") throw new MemoryServiceError("invalid_argument", `${routeName} goal fields are required`); + return { ...request, namespace: request.namespace, title: request.title, summary: request.summary, detail: request.detail, acceptanceCriteria: stringList(request.acceptanceCriteria, routeName), constraints: stringList(request.constraints, routeName), sourceMemoryIds: stringList(request.sourceMemoryIds, routeName), provenance: request.provenance }; +} + +function projectContextGoalDecision(body: unknown, routeName: string, principal: AuthPrincipal): ProjectGoalDecisionRequest & RequestEnvelope { + const request = projectContextMutation(body, routeName, principal); + return { ...request, namespace: request.namespace, candidateId: "" }; +} + +function projectContextWorkItemCreate(body: unknown, routeName: string, principal: AuthPrincipal): ProjectWorkItemCreateRequest & RequestEnvelope { + const request = projectContextMutation(body, routeName, principal); + if (typeof request.title !== "string" || !request.title.trim() || typeof request.summary !== "string" || typeof request.nextStep !== "string") throw new MemoryServiceError("invalid_argument", `${routeName} work item fields are required`); + return { ...request, namespace: request.namespace, title: request.title, summary: request.summary, nextStep: request.nextStep, goalId: optionalString(request.goalId), status: workItemStatus(request.status, routeName), acceptanceCriteria: stringList(request.acceptanceCriteria, routeName), constraints: stringList(request.constraints, routeName), sourceMemoryIds: stringList(request.sourceMemoryIds, routeName), provenance: request.provenance }; +} + +function projectContextWorkItemUpdate(body: unknown, routeName: string, principal: AuthPrincipal): Omit & RequestEnvelope { + const request = projectContextMutation(body, routeName, principal); + return { ...request, namespace: request.namespace, goalId: nullableString(request.goalId, routeName), title: nullableString(request.title, routeName), summary: nullableString(request.summary, routeName), nextStep: nullableString(request.nextStep, routeName), status: nullableWorkItemStatus(request.status, routeName), acceptanceCriteria: nullableStringList(request.acceptanceCriteria, routeName), constraints: nullableStringList(request.constraints, routeName), sourceMemoryIds: nullableStringList(request.sourceMemoryIds, routeName), provenance: request.provenance }; +} + +function projectContextFocus(body: unknown, routeName: string, principal: AuthPrincipal): ProjectWorkItemSelectRequest & RequestEnvelope { + const request = projectContextMutation(body, routeName, principal); + if (request.workItemId !== null && typeof request.workItemId !== "string") throw new MemoryServiceError("invalid_argument", `${routeName} workItemId must be a string or null`); + return { ...request, namespace: request.namespace, workItemId: request.workItemId }; +} + +function stringList(value: unknown, routeName: string): string[] | undefined { return parseOptionalStringArray(value, routeName); } +function nullableStringList(value: unknown, routeName: string): string[] | null | undefined { return value === null ? null : stringList(value, routeName); } +function optionalString(value: unknown): string | undefined { return typeof value === "string" ? value : undefined; } +function nullableString(value: unknown, routeName: string): string | null | undefined { if (value === undefined || value === null || typeof value === "string") return value; throw new MemoryServiceError("invalid_argument", `${routeName} field must be a string or null`); } +function workItemStatus(value: unknown, routeName: string): ProjectWorkItemCreateRequest["status"] | undefined { + if (value === undefined) return undefined; + if (value === "pending" || value === "active" || value === "blocked" || value === "completed" || value === "archived") return value; + throw new MemoryServiceError("invalid_argument", `${routeName} status is invalid`); +} +function nullableWorkItemStatus(value: unknown, routeName: string): ProjectWorkItemUpdateRequest["status"] { + return value === null ? null : workItemStatus(value, routeName); +} function namespaceFromSource(source: unknown): RuntimeNamespace | undefined { if (typeof source !== "string" || !source.trim()) { return undefined; diff --git a/Memory/src/service/memory-service.ts b/Memory/src/service/memory-service.ts index acc978875..4ef9529ac 100644 --- a/Memory/src/service/memory-service.ts +++ b/Memory/src/service/memory-service.ts @@ -71,7 +71,8 @@ import type { ToolCallPayload, ToolObserveRequest, TurnCompleteRequest, - TurnStartRequest + TurnStartRequest, + TurnStartResponse } from "../types.js"; import { MemoryServiceError } from "../utils/error.js"; import { newId,stableHash,stableStringify } from "../utils/id.js"; @@ -131,6 +132,19 @@ import { procedureFromSkillMemory } from "./read-model/memory.js"; import { PanelReadModel } from "./read-model/panel-read.js"; +import { ProjectContextService } from "./project-context/project-context-service.js"; +import type { + ProjectContextProposeGoalRequest, + ProjectContextReadState, + ProjectContextStableResult, + ProjectGoalRecord, + ProjectWorkItemRecord +} from "./project-context/project-context-types.js"; +import type { + ProjectWorkItemCreateRequest, + ProjectWorkItemSelectRequest, + ProjectWorkItemUpdateRequest +} from "./project-context/project-context-service.js"; import { SkillReadModel } from "./read-model/skill.js"; @@ -249,6 +263,7 @@ export class MemoryService { private readonly episodeReadModel: EpisodeReadModel; private readonly importJobs: ImportJobProcessor; private readonly panelReadModel: PanelReadModel; + private readonly projectContext: ProjectContextService; private readonly retrieval: RetrievalService; private readonly sessionTurns: SessionTurnService; private readonly skillReadModel: SkillReadModel; @@ -507,12 +522,14 @@ export class MemoryService { namespaceIdFromContext, withTimeout }); + this.projectContext = new ProjectContextService({ repositories: this.repos }); const sessionTurnOwner = this; this.sessionTurns = new SessionTurnService({ repos: this.repos, get config() { return sessionTurnOwner.config; }, get llm() { return sessionTurnOwner.llm; }, get skillLlm() { return sessionTurnOwner.skillLlm; }, + projectContext: this.projectContext, assertEpisodeInScope: this.assertEpisodeInScope.bind(this), assertMemoryAddEnabled: this.assertMemoryAddEnabled.bind(this), assertRawTurnInScope: this.assertRawTurnInScope.bind(this), @@ -854,26 +871,7 @@ export class MemoryService { }; } - async startTurn(request: TurnStartRequest & Record): Promise<{ - contextPacketId: string; - turnId: string; - sessionId: string; - episodeId: string; - closedEpisodeIds: string[]; - searchEventId: string; - hits: RecallHit[]; - injectedContext: InjectedContext; - sourceMemoryIds: string[]; - droppedDueToBudget: Array<{ - id: string; - kind: MemoryKind; - memoryLayer: MemoryLayer; - reason: "token_budget"; - tokenEstimate?: number; - }>; - status: string[]; - serverTime: string; - }> { + async startTurn(request: TurnStartRequest & Record): Promise { return this.sessionTurns.startTurn(request); } @@ -1768,6 +1766,52 @@ export class MemoryService { return this.panelReadModel.evolutionOverview(input); } + readProjectContext(namespace: RuntimeNamespace): ProjectContextReadState { + this.assertProjectContextScope(namespace); + return this.projectContext.read(namespace); + } + + proposeProjectGoal(input: ProjectContextProposeGoalRequest): ProjectGoalRecord { + this.assertProjectContextScope(input.namespace); + return this.projectContext.proposeGoal(input); + } + + approveProjectGoal(input: { namespace: RuntimeNamespace; candidateId: string }): ProjectGoalRecord { + this.assertProjectContextScope(input.namespace); + return this.projectContext.approveGoal(input); + } + + rejectProjectGoal(input: { namespace: RuntimeNamespace; candidateId: string }): ProjectGoalRecord { + this.assertProjectContextScope(input.namespace); + return this.projectContext.rejectGoal(input); + } + + createProjectWorkItem(input: ProjectWorkItemCreateRequest): ProjectWorkItemRecord { + this.assertProjectContextScope(input.namespace); + return this.projectContext.createWorkItem(input); + } + + updateProjectWorkItem(input: ProjectWorkItemUpdateRequest): ProjectWorkItemRecord { + this.assertProjectContextScope(input.namespace); + return this.projectContext.updateWorkItem(input); + } + + selectProjectWorkItem(input: ProjectWorkItemSelectRequest): ProjectWorkItemRecord | undefined { + this.assertProjectContextScope(input.namespace); + return this.projectContext.selectWorkItem(input); + } + + renderStableProjectContext(namespace: RuntimeNamespace, budget?: number): ProjectContextStableResult { + this.assertProjectContextScope(namespace); + return this.projectContext.renderStable(namespace, budget); + } + + private assertProjectContextScope(namespace: RuntimeNamespace): void { + if (!hasProjectScope(namespace)) { + throw new MemoryServiceError("invalid_argument", "project context requires projectId, workspaceId, or workspacePath"); + } + } + namespaceAudit(input: RequestEnvelope & { userId?: string } = {}) { return this.panelReadModel.namespaceAudit(input); } @@ -2558,6 +2602,20 @@ export class MemoryService { contextHints, injectedContextQuery: request.query }); + const noWriteNamespace = normalizeNamespace(request.namespace); + const noGoalMarkdown = '\nNo confirmed project goal.\n'; + const projectContext: ProjectContextStableResult = { + namespaceId: namespaceIdFromContext(noWriteNamespace), + status: "no_confirmed_goal", + version: 0, + goal: null, + focusedWorkItem: null, + facts: [], + markdown: noGoalMarkdown, + sourceMemoryIds: [], + generatedAt: nowIso() + }; + const supplementalMarkdown = search.injectedContext.markdown.trim(); return { contextPacketId: `ctx_${stableHash(`${request.sessionId}:${episodeId}:${turnId}:${search.searchEventId}`).slice(0, 20)}`, turnId, @@ -2566,8 +2624,12 @@ export class MemoryService { closedEpisodeIds: [], searchEventId: search.searchEventId, hits: search.hits, - injectedContext: search.injectedContext, - sourceMemoryIds: search.sourceMemoryIds, + injectedContext: { + ...search.injectedContext, + markdown: supplementalMarkdown ? `${projectContext.markdown}\n\n${supplementalMarkdown}` : projectContext.markdown + }, + projectContext, + sourceMemoryIds: uniq([...projectContext.sourceMemoryIds, ...search.sourceMemoryIds]), droppedDueToBudget: search.droppedDueToBudget, status: uniq([...search.status, "memory_add:disabled:no_turn_write"]), serverTime: nowIso() diff --git a/Memory/src/service/project-context/project-context-service.ts b/Memory/src/service/project-context/project-context-service.ts new file mode 100644 index 000000000..0127a987f --- /dev/null +++ b/Memory/src/service/project-context/project-context-service.ts @@ -0,0 +1,367 @@ +import type { MemoryProvenance, RuntimeNamespace } from "../../types.js"; +import { newId } from "../../utils/id.js"; +import { nowIso } from "../../utils/time.js"; +import { namespaceIdFromContext, normalizeNamespace } from "../namespace/namespace-scope.js"; +import type { + ProjectContextProposeGoalRequest, + ProjectContextReadState, + ProjectContextRequest, + ProjectContextServiceOptions, + ProjectContextStableResult, + ProjectFactRecord, + ProjectGoalRecord, + ProjectWorkItemRecord, + ProjectWorkItemStatus +} from "./project-context-types.js"; + +export interface ProjectGoalDecisionRequest extends ProjectContextRequest { + candidateId: string; +} + +export interface ProjectWorkItemCreateRequest extends ProjectContextRequest { + goalId?: string; + title: string; + summary: string; + nextStep: string; + acceptanceCriteria?: string[]; + constraints?: string[]; + status?: ProjectWorkItemStatus; + sourceMemoryIds?: string[]; + provenance?: Partial & Record; +} + +export interface ProjectWorkItemUpdateRequest extends ProjectContextRequest { + workItemId: string; + goalId?: string | null; + title?: string | null; + summary?: string | null; + nextStep?: string | null; + acceptanceCriteria?: string[] | null; + constraints?: string[] | null; + status?: ProjectWorkItemStatus | null; + sourceMemoryIds?: string[] | null; + provenance?: (Partial & Record) | null; +} + +export interface ProjectWorkItemSelectRequest extends ProjectContextRequest { + workItemId: string | null; +} + + +export class ProjectContextService { + private readonly now: () => string; + private static readonly MIN_RENDER_BUDGET = 120; + private readonly id: (prefix: string) => string; + + constructor(private readonly options: ProjectContextServiceOptions) { + this.now = options.now ?? nowIso; + this.id = options.id ?? newId; + } + + read(namespace: RuntimeNamespace): ProjectContextReadState { + const namespaceId = this.namespaceId(namespace); + return { + namespaceId, + activeGoal: this.options.repositories.projectContext.getActiveGoal(namespaceId), + goals: this.options.repositories.projectContext.listGoals(namespaceId), + workItems: this.options.repositories.projectContext.listWorkItems(namespaceId), + focusedWorkItem: this.options.repositories.projectContext.getFocusedWorkItem(namespaceId), + facts: this.options.repositories.projectContext.listActiveFacts(namespaceId) + }; + } + + proposeGoal(request: ProjectContextProposeGoalRequest): ProjectGoalRecord { + const namespace = normalizeNamespace(request.namespace); + const at = this.now(); + const activeVersion = this.options.repositories.projectContext.getActiveGoal(this.namespaceId(namespace))?.version ?? 0; + return this.options.repositories.projectContext.insertGoal({ + ...identity(namespace), + id: this.id("project_goal"), + title: required(request.title, "goal title"), + summary: request.summary.trim(), + detail: request.detail, + acceptanceCriteria: cleanList(request.acceptanceCriteria), + constraints: cleanList(request.constraints), + status: "candidate", + version: activeVersion, + sourceMemoryIds: uniq(request.sourceMemoryIds), + provenance: request.provenance ?? {}, + createdAt: at, + updatedAt: at + }); + } + + approveGoal(request: ProjectGoalDecisionRequest): ProjectGoalRecord { + const namespaceId = this.namespaceId(request.namespace); + const candidate = this.requireCandidate(request.candidateId, namespaceId); + const at = this.now(); + return this.options.repositories.transaction(() => { + const current = this.options.repositories.projectContext.getActiveGoal(namespaceId); + const version = (current?.version ?? 0) + 1; + this.options.repositories.projectContext.archiveGoalCandidate(candidate.id, namespaceId, at); + const active: ProjectGoalRecord = { + ...candidate, + id: this.id("project_goal"), + status: "active", + version, + supersedesId: current?.id, + createdAt: at, + updatedAt: at + }; + return current + ? this.options.repositories.projectContext.replaceActiveGoal(active) + : this.options.repositories.projectContext.insertGoal(active); + }); + } + + rejectGoal(request: ProjectGoalDecisionRequest): ProjectGoalRecord { + const namespaceId = this.namespaceId(request.namespace); + this.requireCandidate(request.candidateId, namespaceId); + return this.options.repositories.projectContext.archiveGoalCandidate(request.candidateId, namespaceId, this.now()); + } + + createWorkItem(request: ProjectWorkItemCreateRequest): ProjectWorkItemRecord { + const namespace = normalizeNamespace(request.namespace); + const at = this.now(); + return this.options.repositories.projectContext.insertWorkItem({ + ...identity(namespace), + id: this.id("project_work_item"), + goalId: request.goalId, + title: required(request.title, "work item title"), + summary: request.summary.trim(), + nextStep: request.nextStep.trim(), + acceptanceCriteria: cleanList(request.acceptanceCriteria), + constraints: cleanList(request.constraints), + status: request.status ?? "pending", + focused: false, + sourceMemoryIds: uniq(request.sourceMemoryIds), + provenance: request.provenance ?? {}, + createdAt: at, + updatedAt: at + }); + } + + updateWorkItem(request: ProjectWorkItemUpdateRequest): ProjectWorkItemRecord { + const namespaceId = this.namespaceId(request.namespace); + const stored = this.options.repositories.projectContext.listWorkItems(namespaceId).find((item) => item.id === request.workItemId); + if (!stored) throw new Error(`work item not found in namespace: ${request.workItemId}`); + const status = request.status === undefined || request.status === null ? stored.status : request.status; + const next: ProjectWorkItemRecord = { + ...stored, + goalId: patchOptional(stored.goalId, request.goalId), + title: patchString(stored.title, request.title), + summary: patchString(stored.summary, request.summary), + nextStep: patchString(stored.nextStep, request.nextStep), + acceptanceCriteria: patchList(stored.acceptanceCriteria, request.acceptanceCriteria), + constraints: patchList(stored.constraints, request.constraints), + status, + focused: status === "completed" || status === "archived" ? false : stored.focused, + sourceMemoryIds: patchList(stored.sourceMemoryIds, request.sourceMemoryIds), + provenance: request.provenance === undefined ? stored.provenance : request.provenance ?? {}, + updatedAt: this.now() + }; + return this.options.repositories.projectContext.updateWorkItem(next); + } + + selectWorkItem(request: ProjectWorkItemSelectRequest): ProjectWorkItemRecord | undefined { + return this.options.repositories.projectContext.setFocusedWorkItem(this.namespaceId(request.namespace), request.workItemId, this.now()); + } + + renderStable(namespace: RuntimeNamespace, budget = 4_000): ProjectContextStableResult { + if (budget < ProjectContextService.MIN_RENDER_BUDGET) { + throw new RangeError(`project context render budget must be at least ${ProjectContextService.MIN_RENDER_BUDGET}`); + } + const state = this.read(namespace); + const generatedAt = this.now(); + if (!state.activeGoal) { + const noGoal = `\nNo confirmed project goal.\n`; + return { + namespaceId: state.namespaceId, + status: "no_confirmed_goal", + version: 0, + goal: null, + focusedWorkItem: null, + facts: state.facts, + markdown: noGoal.length <= budget ? noGoal : fitNoGoal(budget), + sourceMemoryIds: [], + generatedAt + }; + } + const conflictKeys = conflictingFactKeys(state.facts); + const status = conflictKeys.size > 0 ? "conflict" : "ready"; + const authoritativeFacts = state.facts.filter((fact) => !conflictKeys.has(factKey(fact))); + const markdown = fitSections( + state.activeGoal.version, + status, + renderSections(state.activeGoal, state.focusedWorkItem ?? null, authoritativeFacts), + budget + ); + return { + namespaceId: state.namespaceId, + status, + version: state.activeGoal.version, + goal: state.activeGoal, + focusedWorkItem: state.focusedWorkItem ?? null, + facts: state.facts, + markdown, + sourceMemoryIds: uniq([ + ...state.activeGoal.sourceMemoryIds, + ...(state.focusedWorkItem?.sourceMemoryIds ?? []), + ...state.facts.flatMap((fact) => fact.sourceMemoryIds) + ]), + generatedAt + }; + } + + private namespaceId(namespace: RuntimeNamespace): string { + return namespaceIdFromContext(namespace); + } + + private requireCandidate(id: string, namespaceId: string): ProjectGoalRecord { + const goal = this.options.repositories.projectContext.getGoal(id); + if (!goal || goal.namespaceId !== namespaceId) throw new Error("project goal namespace mismatch"); + if (goal.status !== "candidate") throw new Error("project goal is not a candidate"); + return goal; + } +} + +function identity(namespace: RuntimeNamespace & { userId: string; source: string; profileId: string }) { + return { + namespaceId: namespaceIdFromContext(namespace), + userId: namespace.userId, + projectId: namespace.projectId, + workspaceId: namespace.workspaceId, + workspacePath: namespace.workspacePath + }; +} + +function renderSections( + goal: ProjectGoalRecord, + focus: ProjectWorkItemRecord | null, + facts: ProjectFactRecord[] +): RenderSection[] { + const constraints = uniq([...goal.constraints, ...facts.filter((fact) => fact.kind === "constraint").map((fact) => fact.content)]); + const decisions = facts.filter((fact) => fact.kind === "decision").map((fact) => fact.content); + const acceptance = uniq([...goal.acceptanceCriteria, ...(focus?.acceptanceCriteria ?? [])]); + return [ + { label: "Constraint:", value: constraints.join("; ") || "none" }, + { label: "Goal:", value: goal.title }, + { label: "Goal summary:", value: goal.summary }, + { label: "Goal status:", value: `${goal.status}; version=${goal.version}` }, + { label: "Focus:", value: focus?.title ?? "No work item is explicitly focused." }, + { label: "Focus summary:", value: focus?.summary ?? "none" }, + { label: "Focus status:", value: focus?.status ?? "none" }, + { label: "Next step:", value: focus?.nextStep ?? "none" }, + { label: "Acceptance:", value: acceptance.join("; ") || "none" }, + { label: "Decision:", value: decisions.join("; ") || "none" }, + { label: "Metadata:", value: `goal_id=${goal.id}; namespace_id=${goal.namespaceId}; project_id=${goal.projectId ?? "none"}; workspace_id=${goal.workspaceId ?? "none"}; confirmed_updated_at=${goal.updatedAt}` } + ]; +} + +interface RenderSection { + label: string; + value: string; +} + +function fitSections(version: number, status: ProjectContextStableResult["status"], sections: RenderSection[], budget: number): string { + const open = ``; + const close = ""; + const normal = [open, ...sections.map((section) => `${section.label} ${section.value}`), close].join("\n"); + if (normal.length <= budget) return normal; + + const section = (label: string) => sections.find((candidate) => candidate.label === label)?.value ?? "none"; + const updatedAt = /(?:^|; )confirmed_updated_at=([^;]+)/.exec(section("Metadata:"))?.[1] ?? "unknown"; + const values = [ + section("Goal:"), + section("Constraint:"), + section("Focus status:"), + section("Focus:"), + section("Next step:"), + section("Acceptance:"), + updatedAt + ]; + const fixedLength = [open, "G=", "C=", "W=||", "A=", "U=", close].join("\n").length; + const lengths = fairLengths(values, fixedLength, budget); + return [ + open, + `G=${values[0]!.slice(0, lengths[0])}`, + `C=${values[1]!.slice(0, lengths[1])}`, + `W=${values[2]!.slice(0, lengths[2])}|${values[3]!.slice(0, lengths[3])}|${values[4]!.slice(0, lengths[4])}`, + `A=${values[5]!.slice(0, lengths[5])}`, + `U=${values[6]!.slice(0, lengths[6])}`, + close + ].join("\n"); +} + +function fairLengths(values: string[], fixedLength: number, budget: number): number[] { + const lengths = values.map(() => 0); + let remaining = budget - fixedLength; + while (remaining > 0) { + let allocated = false; + for (let index = 0; index < values.length && remaining > 0; index += 1) { + if (lengths[index]! >= values[index]!.length) continue; + lengths[index]! += 1; + remaining -= 1; + allocated = true; + } + if (!allocated) break; + } + return lengths; +} + +function fitNoGoal(budget: number): string { + const open = ''; + const close = ""; + const available = budget - open.length - close.length - 2; + return `${open}\n${"No confirmed project goal.".slice(0, Math.max(0, available))}\n${close}`; +} + +function conflictingFactKeys(facts: ProjectFactRecord[]): Set { + const values = new Map>(); + for (const fact of facts) { + const key = factKey(fact); + const group = values.get(key) ?? new Set(); + group.add(fact.content.trim().toLowerCase()); + values.set(key, group); + } + return new Set([...values].filter(([, group]) => group.size > 1).map(([key]) => key)); +} + +function factKey(fact: ProjectFactRecord): string { + const content = fact.content.trim().toLowerCase(); + const separator = content.indexOf(":"); + const domain = separator >= 0 ? content.slice(0, separator).trim() : content; + return `${fact.kind}:${domain}`; +} + +function required(value: string, label: string): string { + const clean = value.trim(); + if (!clean) throw new Error(`${label} is required`); + return clean; +} + +function bounded(value: string, max: number): string { + if (value.length <= max) return value; + if (max <= 3) return value.slice(0, Math.max(0, max)); + return `${value.slice(0, max - 3).trimEnd()}...`; +} + +function cleanList(values: string[] | undefined): string[] { + return uniq((values ?? []).map((value) => value.trim()).filter(Boolean)); +} + +function uniq(values: string[] | undefined): string[] { + return [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))]; +} + +function patchString(current: string, value: string | null | undefined): string { + return value === undefined ? current : value?.trim() ?? ""; +} + +function patchList(current: string[], value: string[] | null | undefined): string[] { + return value === undefined ? current : cleanList(value ?? []); +} + +function patchOptional(current: string | undefined, value: string | null | undefined): string | undefined { + return value === undefined ? current : value ?? undefined; +} diff --git a/Memory/src/service/project-context/project-context-types.ts b/Memory/src/service/project-context/project-context-types.ts new file mode 100644 index 000000000..a8c14e5c6 --- /dev/null +++ b/Memory/src/service/project-context/project-context-types.ts @@ -0,0 +1,107 @@ +import type { RuntimeNamespace } from "../../types.js"; +import type { Repositories } from "../../storage/repositories.js"; + +export type ProjectGoalStatus = "candidate" | "active" | "completed" | "archived"; +export type ProjectWorkItemStatus = "pending" | "active" | "blocked" | "completed" | "archived"; +export type ProjectFactKind = "decision" | "constraint"; +export type ProjectFactStatus = "candidate" | "active" | "superseded" | "archived"; + +export interface ProjectGoalRecord { + id: string; + namespaceId: string; + userId: string; + projectId?: string; + workspaceId?: string; + workspacePath?: string; + title: string; + summary: string; + detail: string; + acceptanceCriteria: string[]; + constraints: string[]; + status: ProjectGoalStatus; + version: number; + supersedesId?: string; + sourceMemoryIds: string[]; + provenance: Record; + createdAt: string; + updatedAt: string; +} + +export interface ProjectWorkItemRecord { + id: string; + namespaceId: string; + userId: string; + projectId?: string; + workspaceId?: string; + workspacePath?: string; + goalId?: string; + title: string; + summary: string; + nextStep: string; + acceptanceCriteria: string[]; + constraints: string[]; + status: ProjectWorkItemStatus; + focused: boolean; + sourceMemoryIds: string[]; + provenance: Record; + createdAt: string; + updatedAt: string; +} + +export interface ProjectFactRecord { + id: string; + namespaceId: string; + userId: string; + projectId?: string; + workspaceId?: string; + workspacePath?: string; + kind: ProjectFactKind; + content: string; + status: ProjectFactStatus; + supersedesId?: string; + sourceMemoryIds: string[]; + provenance: Record; + createdAt: string; + updatedAt: string; +} + +export interface ProjectContextRequest { + namespace: RuntimeNamespace; +} + +export interface ProjectContextProposeGoalRequest extends ProjectContextRequest { + title: string; + summary: string; + detail: string; + acceptanceCriteria?: string[]; + constraints?: string[]; + sourceMemoryIds?: string[]; + provenance?: Record; +} + +export interface ProjectContextReadState { + namespaceId: string; + activeGoal?: ProjectGoalRecord; + goals: ProjectGoalRecord[]; + workItems: ProjectWorkItemRecord[]; + focusedWorkItem?: ProjectWorkItemRecord; + facts: ProjectFactRecord[]; +} + +export interface ProjectContextStableResult { + namespaceId: string; + status: "ready" | "no_confirmed_goal" | "conflict"; + version: number; + goal: ProjectGoalRecord | null; + focusedWorkItem: ProjectWorkItemRecord | null; + facts: ProjectFactRecord[]; + markdown: string; + sourceMemoryIds: string[]; + generatedAt: string; +} + +export interface ProjectContextServiceOptions { + repositories: Repositories; + now?: () => string; + id?: (prefix: string) => string; +} diff --git a/Memory/src/service/read-model/panel-read.ts b/Memory/src/service/read-model/panel-read.ts index fed456b97..b0fa10fa7 100644 --- a/Memory/src/service/read-model/panel-read.ts +++ b/Memory/src/service/read-model/panel-read.ts @@ -46,6 +46,7 @@ import { import { panelCountByDate, panelListItemFromMemory, + panelMemoryMatchesSourceFilter, panelNamespaceDistribution, panelSourceDistribution } from "./panel.js"; @@ -654,28 +655,43 @@ export class PanelReadModel { memoryLayer: input.layer, status: input.status, tags: input.tags, - agentId: input.sourceAgent, - excludedAgentIds: input.excludedSourceAgents, ...(input.namespace ? {} : { projectId: input.projectId, workspaceId: input.workspaceId }) }; - const total = input.q?.trim() + const hasSourceFilter = Boolean( + input.sourceAgent?.trim() || input.excludedSourceAgents?.some((source) => source.trim()) + ); + const candidateTotal = input.q?.trim() ? this.deps.repos.memories.searchCount(input.q, { ...filter, status: filter.status ?? ["activated", "resolving"] }) : this.deps.repos.memories.count(filter); + const sourceFilteredMemories = hasSourceFilter + ? (input.q?.trim() + ? this.deps.repos.memories.getMany(this.deps.repos.memories.searchPanelIds( + input.q, + { ...filter, status: filter.status ?? ["activated", "resolving"] }, + candidateTotal, + 0 + ).map((hit) => hit.id)) + : this.deps.repos.memories.list(filter, candidateTotal, 0)) + .filter((memory) => panelMemoryMatchesSourceFilter(memory, input.sourceAgent, input.excludedSourceAgents)) + : undefined; + const total = sourceFilteredMemories?.length ?? candidateTotal; const totalPages = Math.max(1, Math.ceil(total / pageSize)); const requestedPage = normalizePageNumber(input.page); const page = Math.min(requestedPage, totalPages); const offset = normalizeOffsetCursor(input.cursor) ?? ((page - 1) * pageSize); - const memories = input.q?.trim() - ? this.deps.repos.memories.getMany(this.deps.repos.memories.searchPanelIds( - input.q, - { ...filter, status: filter.status ?? ["activated", "resolving"] }, - pageSize, - offset - ).map((hit) => hit.id)) - : this.deps.repos.memories.list(filter, pageSize, offset); + const memories = sourceFilteredMemories + ? sourceFilteredMemories.slice(offset, offset + pageSize) + : input.q?.trim() + ? this.deps.repos.memories.getMany(this.deps.repos.memories.searchPanelIds( + input.q, + { ...filter, status: filter.status ?? ["activated", "resolving"] }, + pageSize, + offset + ).map((hit) => hit.id)) + : this.deps.repos.memories.list(filter, pageSize, offset); return { items: memories.map((memory) => panelListItemFromMemory( this.deps.repos.memories.toListItem(memory), diff --git a/Memory/src/service/read-model/panel.ts b/Memory/src/service/read-model/panel.ts index 1bcd0f592..2143973d6 100644 --- a/Memory/src/service/read-model/panel.ts +++ b/Memory/src/service/read-model/panel.ts @@ -170,6 +170,18 @@ export function panelSourceForMemory(memory: MemoryRow): string { return explicitSources.some(panelIsInternalSourceValue) ? "memmy" : "unknown"; } +export function panelMemoryMatchesSourceFilter( + memory: MemoryRow, + sourceAgent: string | undefined, + excludedSourceAgents: readonly string[] | undefined +): boolean { + const sourceKey = panelSourceKey(panelSourceForMemory(memory)); + const selectedSourceKey = panelSourceKey(sourceAgent); + if (selectedSourceKey) return sourceKey === selectedSourceKey; + const excludedSourceKeys = new Set((excludedSourceAgents ?? []).map(panelSourceKey).filter(Boolean)); + return !excludedSourceKeys.has(sourceKey); +} + function panelNormalizeExplicitSource(value: unknown): string | undefined { if (typeof value !== "string" || !value.trim()) return undefined; const normalized = value.trim().toLowerCase(); @@ -188,15 +200,24 @@ function panelNamespaceLabel(projectId: string, workspacePath: string | undefine function panelNormalizeKnownSource(value: unknown): string | undefined { if (typeof value !== "string" || !value.trim()) return undefined; - const normalized = value.trim().toLowerCase(); + const normalized = value.trim().toLowerCase().replace(/[\s_:/\\]+/gu, "-"); if (normalized === "claude" || normalized.startsWith("claude-")) return "claude-code"; if (normalized === "open-code" || normalized.startsWith("open-code-")) return "opencode"; - for (const source of ["hermes", "openclaw", "codex", "cursor", "claude-code", "opencode", "manual", "memmy"]) { + for (const source of ["hermes", "openclaw", "codex", "cursor", "claude-code", "opencode", "pi", "workbuddy", "omp", "manual", "memmy"]) { if (normalized === source || normalized.startsWith(`${source}-`)) return source; } return undefined; } +function panelSourceKey(value: unknown): string { + if (typeof value !== "string" || !value.trim()) return ""; + const normalized = value.trim().toLowerCase().replace(/[\s_:/\\-]+/gu, "_"); + if (normalized === "claude") return "claude_code"; + if (normalized === "open_code") return "opencode"; + if (normalized === "memmy_agent") return "memmy"; + return normalized; +} + function panelNormalizeSourceAgent(value: unknown): string | undefined { if (typeof value !== "string" || !value.trim()) return undefined; const normalized = value.trim().toLowerCase(); diff --git a/Memory/src/service/retrieval/indexed-candidate-pool.ts b/Memory/src/service/retrieval/indexed-candidate-pool.ts index 753fcf004..d47588d49 100644 --- a/Memory/src/service/retrieval/indexed-candidate-pool.ts +++ b/Memory/src/service/retrieval/indexed-candidate-pool.ts @@ -237,7 +237,7 @@ export class IndexedCandidatePool { } function retrievalSemanticTags(tags: readonly string[]): string[] { - const ignored = new Set(["agent-source", "codex", "pi", "hermes", "claude_code", "cursor", "opencode", "memmy"]); + const ignored = new Set(["agent-source", "codex", "pi", "hermes", "claude_code", "cursor", "opencode", "omp", "memmy"]); return tags .map((tag) => tag.trim()) .filter((tag) => tag && !ignored.has(tag.toLowerCase())); diff --git a/Memory/src/service/retrieval/retrieval-service.ts b/Memory/src/service/retrieval/retrieval-service.ts index 645923225..442d9d2ff 100644 --- a/Memory/src/service/retrieval/retrieval-service.ts +++ b/Memory/src/service/retrieval/retrieval-service.ts @@ -80,7 +80,7 @@ type TraceMeta = NonNullable>; const RETRIEVAL_QUERY_EXTRACT_TIMEOUT_MS = 60_000; -const RETRIEVAL_FILTER_TIMEOUT_MS = 30_000; +const RETRIEVAL_FILTER_TIMEOUT_MS = 20_000; const QUERY_REWRITE_TIMEOUT_MS = 30_000; @@ -1260,7 +1260,11 @@ export class RetrievalService { scope }); const retrievalQuery = focusResearchRetrievalQuery(request.query, tuning.domain).text; - const queryExtract = candidateCount > 0 ? await this.extractRetrievalQuery(retrievalQuery) : null; + // Turn-start recall runs inside agent hook deadlines. Keep it to one LLM stage: + // filtering actual candidates is more useful here than extracting the query first. + const queryExtract = candidateCount > 0 && retrievalMode !== "turn_start" + ? await this.extractRetrievalQuery(retrievalQuery) + : null; const queryVectorText = queryExtract?.queryVecText?.trim() || retrievalQuery; const retrievalLimit = request.limit ?? this.deps.turnStartRetrievalLimit(); const retrievalOutput = await this.retrieveSearchMemories({ @@ -1278,7 +1282,9 @@ export class RetrievalService { const retrieval = retrievalOutput.retrieval; const memories = retrievalOutput.memories; const rerankAt = Date.now(); - const filteredHits = await this.filterRecallHits(queryVectorText, retrieval.hits); + const filteredHits = await this.filterRecallHits(queryVectorText, retrieval.hits, { + allowModelFallback: retrievalMode !== "turn_start" + }); const hits = filterL1TraceSpanRecallHits(filteredHits.hits,memories); const contextPacket = buildInjectedContext( hits, @@ -1477,7 +1483,11 @@ export class RetrievalService { }; } - private async filterRecallHits(query: string, hits: RecallHit[]): Promise<{ + private async filterRecallHits( + query: string, + hits: RecallHit[], + options: { allowModelFallback: boolean } + ): Promise<{ hits: RecallHit[]; status: string[]; }> { @@ -1543,7 +1553,8 @@ export class RetrievalService { try { result = await completeFilter(filterLlm, usesSummaryLlm); } catch (primaryError) { - const evolutionFallback = usesSummaryLlm && + const evolutionFallback = options.allowModelFallback && + usesSummaryLlm && this.deps.skillLlm.isConfigured() && this.deps.skillLlm !== filterLlm ? this.deps.skillLlm diff --git a/Memory/src/service/session/session-turn-service.ts b/Memory/src/service/session/session-turn-service.ts index 7a63f3ef0..c9fc56873 100644 --- a/Memory/src/service/session/session-turn-service.ts +++ b/Memory/src/service/session/session-turn-service.ts @@ -40,7 +40,8 @@ import type { ToolCallPayload, ToolObserveRequest, TurnCompleteRequest, - TurnStartRequest + TurnStartRequest, + TurnStartResponse } from "../../types.js"; import { MemoryServiceError } from "../../utils/error.js"; import { newId,stableHash,stableStringify } from "../../utils/id.js"; @@ -51,6 +52,8 @@ import type { DecisionRepairLlmDraft, SynthesizeDecisionRepairDraft } from "../feedback/feedback-experience.js"; +import type { DecisionRepairSummary } from "../evolution/reward-pipeline.js"; +import type { ProjectContextService } from "../project-context/project-context-service.js"; import { recordApiLog } from "../model-audit/model-call-audit.js"; import { memoryFilterForNamespace, @@ -82,9 +85,9 @@ type TraceMeta = NonNullable>; interface ToolFailureRecord { toolId: string; context: string; step: number; reason: string; ts: number; rawTurnId?: string; sessionId?: string; episodeId?: string; } interface ToolFailureState { toolId: string; context: string; firstSeen: number; lastSeen: number; windowStart: number; occurrences: ToolFailureRecord[]; } interface ToolFailureBurst extends ToolFailureState { contextHash: string; failureCount: number; } -interface DecisionRepairSummary { repairId?: string; contextHash?: string; skipped?: boolean; reason?: string; attachedPolicyIds?: string[]; } type SessionTurnDependencies = { repos: Repositories; + projectContext: ProjectContextService; readonly config: MemmyConfig; readonly llm: LlmClient; readonly skillLlm: LlmClient; @@ -182,6 +185,38 @@ function endTopicDecisionFromRawTurn(rawTurn: RawTurnRecord): EndTopicDecision | }; } +function persistedTurnStartResponse(rawTurn: RawTurnRecord): TurnStartResponse | undefined { + const turnStart = isRecord(rawTurn.messagePayload?.turn_start) + ? rawTurn.messagePayload.turn_start + : undefined; + const response = turnStart?.response; + if (!isPersistedTurnStartResponse(response)) return undefined; + return response; +} + +function isPersistedTurnStartResponse(value: unknown): value is TurnStartResponse { + if (!isRecord(value)) return false; + return typeof value.contextPacketId === "string" + && typeof value.turnId === "string" + && typeof value.sessionId === "string" + && typeof value.episodeId === "string" + && Array.isArray(value.closedEpisodeIds) + && typeof value.searchEventId === "string" + && Array.isArray(value.hits) + && isRecord(value.injectedContext) + && typeof value.injectedContext.markdown === "string" + && Array.isArray(value.injectedContext.sections) + && isRecord(value.projectContext) + && typeof value.projectContext.namespaceId === "string" + && typeof value.projectContext.version === "number" + && typeof value.projectContext.markdown === "string" + && Array.isArray(value.projectContext.sourceMemoryIds) + && Array.isArray(value.sourceMemoryIds) + && Array.isArray(value.droppedDueToBudget) + && Array.isArray(value.status) + && typeof value.serverTime === "string"; +} + function rawTurnIsExcludedFromMemory(rawTurn: RawTurnRecord): boolean { if (endTopicDecisionFromRawTurn(rawTurn)) { return true; @@ -710,26 +745,7 @@ export class SessionTurnService { }; } - async startTurn(request: TurnStartRequest & Record): Promise<{ - contextPacketId: string; - turnId: string; - sessionId: string; - episodeId: string; - closedEpisodeIds: string[]; - searchEventId: string; - hits: RecallHit[]; - injectedContext: InjectedContext; - sourceMemoryIds: string[]; - droppedDueToBudget: Array<{ - id: string; - kind: MemoryKind; - memoryLayer: MemoryLayer; - reason: "token_budget"; - tokenEstimate?: number; - }>; - status: string[]; - serverTime: string; - }> { + async startTurn(request: TurnStartRequest & Record): Promise { request = sanitizeTurnStartRequest(request); if (!this.deps.memoryAddEnabled()) { return this.deps.startTurnNoWrite(request); @@ -737,12 +753,23 @@ export class SessionTurnService { const session = this.deps.requireOpenSession(request.sessionId); this.deps.assertSessionInScope(session, request.namespace); const turnId = request.turnId ?? newId("turn"); - const intentDecision = classifyIntent(request.query); - const endTopicDecision = explicitEndTopicDecision(request.query); const existingRawTurn = this.deps.repos.runtime.getRawTurnBySessionTurn(session.id, turnId); if (existingRawTurn) { this.deps.assertRawTurnInScope(existingRawTurn, request.namespace); + const persistedResponse = persistedTurnStartResponse(existingRawTurn); + if (persistedResponse) return persistedResponse; } + const requestedContextBudget = typeof request.contextBudget === "number" ? request.contextBudget : undefined; + const projectContext = this.deps.projectContext.renderStable( + namespaceForSession(session), + requestedContextBudget === undefined ? 4_000 : Math.max(120, requestedContextBudget * 4) + ); + const projectTokenEstimate = Math.ceil(projectContext.markdown.length / 4); + const supplementalContextBudget = requestedContextBudget === undefined + ? undefined + : Math.max(0, requestedContextBudget - projectTokenEstimate); + const intentDecision = classifyIntent(request.query); + const endTopicDecision = explicitEndTopicDecision(request.query); const latestEpisodeBefore = existingRawTurn ? undefined : this.deps.repos.runtime.latestEpisodeForSession(session.id); @@ -774,13 +801,69 @@ export class SessionTurnService { ? [] : this.deps.memoryLayersForIntent(intentDecision.kind), limit: this.deps.turnStartRetrievalLimit(), - contextBudget: typeof request.contextBudget === "number" ? request.contextBudget : undefined, + contextBudget: supplementalContextBudget, includeInjectedContext: true, retrievalMode: "turn_start", contextHints, injectedContextQuery: request.query }); + const supplementalMarkdown = search.injectedContext.markdown.trim(); + const combinedMarkdown = supplementalMarkdown + ? `${projectContext.markdown}\n\n${supplementalMarkdown}` + : projectContext.markdown; + const combinedTokenEstimate = Math.ceil(combinedMarkdown.length / 4); + const includeSupplemental = supplementalContextBudget !== 0 + && (requestedContextBudget === undefined || combinedTokenEstimate <= requestedContextBudget); + const droppedDueToBudget = includeSupplemental + ? search.droppedDueToBudget + : [...search.hits.reduce( + (droppedById: Map, hit: TurnStartResponse["hits"][number]) => { + if (droppedById.has(hit.id)) return droppedById; + const section = search.injectedContext.sections.find((candidate: InjectedContext["sections"][number]) => + candidate.id === `memory-${hit.id}` || candidate.memoryIds.includes(hit.id) + ); + droppedById.set(hit.id, { + id: hit.id, + kind: hit.kind, + memoryLayer: hit.memoryLayer, + reason: "token_budget", + ...(section?.tokenEstimate === undefined ? {} : { tokenEstimate: section.tokenEstimate }) + }); + return droppedById; + }, + new Map(search.droppedDueToBudget.map((dropped: TurnStartResponse["droppedDueToBudget"][number]) => [dropped.id, dropped])) + ).values()]; + const sourceMemoryIds = uniq([ + ...projectContext.sourceMemoryIds, + ...(includeSupplemental ? search.sourceMemoryIds : []) + ]); + const injectedContext: InjectedContext = { + ...(includeSupplemental ? search.injectedContext : { sections: [] }), + markdown: includeSupplemental ? combinedMarkdown : projectContext.markdown, + tokenEstimate: includeSupplemental ? combinedTokenEstimate : projectTokenEstimate + }; const contextPacketId = `ctx_${stableHash(`${session.id}:${episode.id}:${turnId}:${search.searchEventId}`).slice(0, 20)}`; + const response: TurnStartResponse = { + contextPacketId, + turnId, + sessionId: session.id, + episodeId: episode.id, + closedEpisodeIds, + searchEventId: search.searchEventId, + hits: search.hits, + injectedContext, + projectContext, + sourceMemoryIds, + droppedDueToBudget, + status: [ + ...search.status, + ...(intentDecision.kind === "chitchat" || intentDecision.kind === "meta" + ? [`intent:${intentDecision.kind}:retrieval_skipped`] + : []), + ...(endTopicDecision ? ["relation:end_topic"] : []) + ], + serverTime: nowIso() + }; if (!existingRawTurn) { const at = nowIso(); this.deps.repos.runtime.touchSession(session.id, at); @@ -794,13 +877,15 @@ export class SessionTurnService { userText: request.query, toolCalls: [], toolResults: [], - sourceMemoryIds: search.sourceMemoryIds, + sourceMemoryIds, usage: {}, messagePayload: { turn_start: { contextPacketId, searchEventId: search.searchEventId, - sourceMemoryIds: search.sourceMemoryIds, + sourceMemoryIds, + projectContextVersion: projectContext.version, + projectContextStatus: projectContext.status, protocolVersion: request.protocolVersion, provenance: request.provenance, intent_decision: intentDecision, @@ -811,7 +896,8 @@ export class SessionTurnService { decision: endTopicDecision } } - : {}) + : {}), + response } }, status: "started", @@ -832,26 +918,7 @@ export class SessionTurnService { }); } - return { - contextPacketId, - turnId, - sessionId: session.id, - episodeId: episode.id, - closedEpisodeIds, - searchEventId: search.searchEventId, - hits: search.hits, - injectedContext: search.injectedContext, - sourceMemoryIds: search.sourceMemoryIds, - droppedDueToBudget: search.droppedDueToBudget, - status: [ - ...search.status, - ...(intentDecision.kind === "chitchat" || intentDecision.kind === "meta" - ? [`intent:${intentDecision.kind}:retrieval_skipped`] - : []), - ...(endTopicDecision ? ["relation:end_topic"] : []) - ], - serverTime: nowIso() - }; + return response; } completeTurn(turnId: string, request: TurnCompleteRequest & Record): CompleteTurnResponse { @@ -939,6 +1006,9 @@ export class SessionTurnService { const requestToolCalls = normalizeCompleteTurnToolCalls(completionRequest); const requestToolResults = normalizeCompleteTurnToolResults(completionRequest); const requestArtifacts = normalizeCompleteTurnArtifacts(completionRequest); + const persistedStartResponse = existingRawTurn + ? persistedTurnStartResponse(existingRawTurn) + : undefined; const turnStartPayload = { protocolVersion: request.protocolVersion, provenance: request.provenance, @@ -959,7 +1029,8 @@ export class SessionTurnService { decision: endTopicDecision } } - : {}) + : {}), + ...(persistedStartResponse ? { response: persistedStartResponse } : {}) }; const insertedRawTurn: RawTurnRecord = diff --git a/Memory/src/storage/repositories.ts b/Memory/src/storage/repositories.ts index fd2dc518b..d72955ec4 100644 --- a/Memory/src/storage/repositories.ts +++ b/Memory/src/storage/repositories.ts @@ -1,3 +1,4 @@ +import type { ProjectFactRecord, ProjectGoalRecord, ProjectWorkItemRecord } from "../service/project-context/project-context-types.js"; import type Database from "better-sqlite3"; import type { FeedbackRequest, @@ -36,6 +37,9 @@ type SqlValue = string | number | Buffer | null; const BUNDLE_TABLES = [ "memories", "memory_relations", + "project_context_goals", + "project_context_work_items", + "project_context_facts", "sessions", "episodes", "raw_turns", @@ -3609,16 +3613,130 @@ export class RuntimeRepository { } } +export class ProjectContextRepository { + constructor(private readonly db: Database.Database) {} + + getActiveGoal(namespaceId: string): ProjectGoalRecord | undefined { + return goalFromSql(this.db.prepare(`SELECT * FROM project_context_goals WHERE namespace_id = ? AND status = 'active'`).get(namespaceId) as ProjectGoalSqlRow | undefined); + } + getGoal(id: string): ProjectGoalRecord | undefined { + return goalFromSql(this.db.prepare(`SELECT * FROM project_context_goals WHERE id = ?`).get(id) as ProjectGoalSqlRow | undefined); + } + listGoals(namespaceId: string): ProjectGoalRecord[] { + return (this.db.prepare(`SELECT * FROM project_context_goals WHERE namespace_id = ? ORDER BY version DESC`).all(namespaceId) as ProjectGoalSqlRow[]).map((row) => goalFromSql(row)!); + } + insertGoal(goal: ProjectGoalRecord): ProjectGoalRecord { + if (goal.supersedesId && this.getGoal(goal.supersedesId)?.namespaceId !== goal.namespaceId) throw new Error("project goal namespace mismatch"); + try { + this.db.prepare(`INSERT INTO project_context_goals (id, namespace_id, user_id, project_id, workspace_id, workspace_path, title, summary, detail, acceptance_criteria_json, constraints_json, status, version, supersedes_id, source_memory_ids_json, provenance_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(goal.id, goal.namespaceId, goal.userId, goal.projectId ?? null, goal.workspaceId ?? null, goal.workspacePath ?? null, goal.title, goal.summary, goal.detail, toJson(goal.acceptanceCriteria), toJson(goal.constraints), goal.status, goal.version, goal.supersedesId ?? null, toJson(goal.sourceMemoryIds), toJson(goal.provenance), goal.createdAt, goal.updatedAt); + } catch (error) { + if (error instanceof Error && (error.message.includes("uq_project_context_active_goal") || (error.message.includes("project_context_goals.namespace_id") && goal.status === "active"))) throw new Error(`active project goal already exists for namespace ${goal.namespaceId}`); + throw error; + } + return goal; + } + replaceActiveGoal(next: ProjectGoalRecord): ProjectGoalRecord { + return this.db.transaction(() => { + const previous = this.getActiveGoal(next.namespaceId); + if (next.status !== "active" || (previous && next.supersedesId !== previous.id)) throw new Error("replacement active project goal must supersede the current namespace goal"); + this.db.prepare(`UPDATE project_context_goals SET status = 'archived', updated_at = ? WHERE namespace_id = ? AND status = 'active'`).run(next.updatedAt, next.namespaceId); + return this.insertGoal(next); + })(); + } + archiveGoalCandidate(id: string, namespaceId: string, at: string): ProjectGoalRecord { + const goal = this.getGoal(id); + if (!goal || goal.namespaceId !== namespaceId) throw new Error("project goal namespace mismatch"); + if (goal.status !== "candidate") throw new Error("only candidate project goals can be archived"); + this.db.prepare(`UPDATE project_context_goals SET status = 'archived', updated_at = ? WHERE id = ? AND namespace_id = ? AND status = 'candidate'`).run(at, id, namespaceId); + return { ...goal, status: "archived", updatedAt: at }; + } + + getFocusedWorkItem(namespaceId: string): ProjectWorkItemRecord | undefined { + return workItemFromSql(this.db.prepare(`SELECT * FROM project_context_work_items WHERE namespace_id = ? AND focused = 1`).get(namespaceId) as ProjectWorkItemSqlRow | undefined); + } + listWorkItems(namespaceId: string): ProjectWorkItemRecord[] { + return (this.db.prepare(`SELECT * FROM project_context_work_items WHERE namespace_id = ? ORDER BY updated_at DESC, id ASC`).all(namespaceId) as ProjectWorkItemSqlRow[]).map((row) => workItemFromSql(row)!); + } + insertWorkItem(item: ProjectWorkItemRecord): ProjectWorkItemRecord { + this.assertWorkItemGoalNamespace(item); + this.db.prepare(`INSERT INTO project_context_work_items (id, namespace_id, user_id, project_id, workspace_id, workspace_path, goal_id, title, summary, next_step, acceptance_criteria_json, constraints_json, status, focused, source_memory_ids_json, provenance_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(item.id, item.namespaceId, item.userId, item.projectId ?? null, item.workspaceId ?? null, item.workspacePath ?? null, item.goalId ?? null, item.title, item.summary, item.nextStep, toJson(item.acceptanceCriteria), toJson(item.constraints), item.status, item.focused ? 1 : 0, toJson(item.sourceMemoryIds), toJson(item.provenance), item.createdAt, item.updatedAt); + return item; + } + updateWorkItem(item: ProjectWorkItemRecord): ProjectWorkItemRecord { + const stored = workItemFromSql(this.db.prepare(`SELECT * FROM project_context_work_items WHERE id = ?`).get(item.id) as ProjectWorkItemSqlRow | undefined); + if (!stored || stored.namespaceId !== item.namespaceId) throw new Error("work item namespace mismatch"); + const next = { ...item, userId: stored.userId, projectId: stored.projectId, workspaceId: stored.workspaceId, workspacePath: stored.workspacePath, createdAt: stored.createdAt }; + this.assertWorkItemGoalNamespace(next); + this.db.prepare(`UPDATE project_context_work_items SET goal_id = ?, title = ?, summary = ?, next_step = ?, acceptance_criteria_json = ?, constraints_json = ?, status = ?, focused = ?, source_memory_ids_json = ?, provenance_json = ?, updated_at = ? WHERE id = ?`).run(next.goalId ?? null, next.title, next.summary, next.nextStep, toJson(next.acceptanceCriteria), toJson(next.constraints), next.status, next.focused ? 1 : 0, toJson(next.sourceMemoryIds), toJson(next.provenance), next.updatedAt, next.id); + return next; + } + setFocusedWorkItem(namespaceId: string, itemId: string | null, at: string): ProjectWorkItemRecord | undefined { + return this.db.transaction(() => { + const item = itemId ? workItemFromSql(this.db.prepare(`SELECT * FROM project_context_work_items WHERE id = ?`).get(itemId) as ProjectWorkItemSqlRow | undefined) : undefined; + if (itemId && !item) throw new Error(`work item not found: ${itemId}`); + if (item && item.namespaceId !== namespaceId) throw new Error("work item namespace mismatch"); + this.db.prepare(`UPDATE project_context_work_items SET focused = 0, updated_at = ? WHERE namespace_id = ? AND focused = 1`).run(at, namespaceId); + if (!item) return undefined; + this.db.prepare(`UPDATE project_context_work_items SET focused = 1, updated_at = ? WHERE id = ?`).run(at, item.id); + return { ...item, focused: true, updatedAt: at }; + })(); + } + listActiveFacts(namespaceId: string): ProjectFactRecord[] { + return (this.db.prepare(`SELECT * FROM project_context_facts WHERE namespace_id = ? AND status = 'active' ORDER BY updated_at DESC, id ASC`).all(namespaceId) as ProjectFactSqlRow[]).map(factFromSql); + } + insertFact(fact: ProjectFactRecord): ProjectFactRecord { + if (fact.supersedesId) { + const previous = this.db.prepare(`SELECT namespace_id FROM project_context_facts WHERE id = ?`).get(fact.supersedesId) as { namespace_id: string } | undefined; + if (!previous || previous.namespace_id !== fact.namespaceId) throw new Error("fact namespace mismatch"); + } + this.db.prepare(`INSERT INTO project_context_facts (id, namespace_id, user_id, project_id, workspace_id, workspace_path, kind, content, status, supersedes_id, source_memory_ids_json, provenance_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(fact.id, fact.namespaceId, fact.userId, fact.projectId ?? null, fact.workspaceId ?? null, fact.workspacePath ?? null, fact.kind, fact.content, fact.status, fact.supersedesId ?? null, toJson(fact.sourceMemoryIds), toJson(fact.provenance), fact.createdAt, fact.updatedAt); + return fact; + } + supersedeFact(previousId: string, next: ProjectFactRecord): ProjectFactRecord { + return this.db.transaction(() => { + const previous = this.db.prepare(`SELECT namespace_id FROM project_context_facts WHERE id = ?`).get(previousId) as { namespace_id: string } | undefined; + if (!previous || previous.namespace_id !== next.namespaceId) throw new Error("fact namespace mismatch"); + if (next.supersedesId !== previousId || next.status !== "active") throw new Error("replacement fact must be active and supersede the previous fact"); + this.db.prepare(`UPDATE project_context_facts SET status = 'superseded', updated_at = ? WHERE id = ?`).run(next.updatedAt, previousId); + return this.insertFact(next); + })(); + } + private assertWorkItemGoalNamespace(item: ProjectWorkItemRecord): void { + if (item.goalId && this.getGoal(item.goalId)?.namespaceId !== item.namespaceId) throw new Error("work item namespace mismatch"); + } +} + +interface ProjectContextSqlBase { id: string; namespace_id: string; user_id: string; project_id: string | null; workspace_id: string | null; workspace_path: string | null; source_memory_ids_json: string; provenance_json: string; created_at: string; updated_at: string } +interface ProjectGoalSqlRow extends ProjectContextSqlBase { title: string; summary: string; detail: string; acceptance_criteria_json: string; constraints_json: string; status: ProjectGoalRecord["status"]; version: number; supersedes_id: string | null } +interface ProjectWorkItemSqlRow extends ProjectContextSqlBase { goal_id: string | null; title: string; summary: string; next_step: string; acceptance_criteria_json: string; constraints_json: string; status: ProjectWorkItemRecord["status"]; focused: number } +interface ProjectFactSqlRow extends ProjectContextSqlBase { kind: ProjectFactRecord["kind"]; content: string; status: ProjectFactRecord["status"]; supersedes_id: string | null } + +function projectContextBase(row: ProjectContextSqlBase) { + const provenance = parseJson(row.provenance_json, {}); + return { id: row.id, namespaceId: row.namespace_id, userId: row.user_id, projectId: row.project_id ?? undefined, workspaceId: row.workspace_id ?? undefined, workspacePath: row.workspace_path ?? undefined, sourceMemoryIds: asStringArray(parseJson(row.source_memory_ids_json, [])), provenance: isRecord(provenance) ? provenance : {}, createdAt: row.created_at, updatedAt: row.updated_at }; +} +function goalFromSql(row: ProjectGoalSqlRow | undefined): ProjectGoalRecord | undefined { + return row ? { ...projectContextBase(row), title: row.title, summary: row.summary, detail: row.detail, acceptanceCriteria: asStringArray(parseJson(row.acceptance_criteria_json, [])), constraints: asStringArray(parseJson(row.constraints_json, [])), status: row.status, version: row.version, supersedesId: row.supersedes_id ?? undefined } : undefined; +} +function workItemFromSql(row: ProjectWorkItemSqlRow | undefined): ProjectWorkItemRecord | undefined { + return row ? { ...projectContextBase(row), goalId: row.goal_id ?? undefined, title: row.title, summary: row.summary, nextStep: row.next_step, acceptanceCriteria: asStringArray(parseJson(row.acceptance_criteria_json, [])), constraints: asStringArray(parseJson(row.constraints_json, [])), status: row.status, focused: row.focused === 1 } : undefined; +} +function factFromSql(row: ProjectFactSqlRow): ProjectFactRecord { + return { ...projectContextBase(row), kind: row.kind, content: row.content, status: row.status, supersedesId: row.supersedes_id ?? undefined }; +} + export class Repositories { readonly memories: MemoryRepository; readonly processing: MemoryProcessingRepository; readonly runtime: RuntimeRepository; + readonly projectContext: ProjectContextRepository; readonly vectors: SqliteVecStore; constructor(readonly db: Database.Database) { this.vectors = new SqliteVecStore(db); this.memories = new MemoryRepository(db, this.vectors); this.processing = new MemoryProcessingRepository(db); + this.projectContext = new ProjectContextRepository(db); this.runtime = new RuntimeRepository(db); } diff --git a/Memory/src/storage/schema.ts b/Memory/src/storage/schema.ts index 111a4cb8a..14ba4c6be 100644 --- a/Memory/src/storage/schema.ts +++ b/Memory/src/storage/schema.ts @@ -1,7 +1,7 @@ import type Database from "better-sqlite3"; -export const SCHEMA_VERSION = 5; -export const SCHEMA_MIGRATION_ID = "005_memory_governance"; +export const SCHEMA_VERSION = 6; +export const SCHEMA_MIGRATION_ID = "006_project_context"; const API_LOG_SOURCE_AGENT_MIGRATION_FROM_VERSION = 2; const PROCESSING_TAGS = new Set([ "摘要排队中", @@ -466,7 +466,73 @@ const statements = [ created_at TEXT NOT NULL )`, `CREATE INDEX IF NOT EXISTS idx_audit_logs_user_created - ON audit_logs (user_id, created_at DESC)` + ON audit_logs (user_id, created_at DESC)`, + `CREATE TABLE IF NOT EXISTS project_context_goals ( + id TEXT PRIMARY KEY, + namespace_id TEXT NOT NULL, + user_id TEXT NOT NULL, + project_id TEXT, + workspace_id TEXT, + workspace_path TEXT, + title TEXT NOT NULL, + summary TEXT NOT NULL, + detail TEXT NOT NULL, + acceptance_criteria_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(acceptance_criteria_json)), + constraints_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(constraints_json)), + status TEXT NOT NULL CHECK (status IN ('candidate', 'active', 'completed', 'archived')), + version INTEGER NOT NULL, + supersedes_id TEXT REFERENCES project_context_goals(id), + source_memory_ids_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(source_memory_ids_json)), + provenance_json TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(provenance_json)), + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + )`, + `CREATE UNIQUE INDEX IF NOT EXISTS uq_project_context_active_goal + ON project_context_goals(namespace_id) WHERE status = 'active'`, + `CREATE INDEX IF NOT EXISTS idx_project_context_goals_namespace + ON project_context_goals(namespace_id, updated_at DESC)`, + `CREATE TABLE IF NOT EXISTS project_context_work_items ( + id TEXT PRIMARY KEY, + namespace_id TEXT NOT NULL, + user_id TEXT NOT NULL, + project_id TEXT, + workspace_id TEXT, + workspace_path TEXT, + goal_id TEXT REFERENCES project_context_goals(id), + title TEXT NOT NULL, + summary TEXT NOT NULL, + next_step TEXT NOT NULL, + acceptance_criteria_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(acceptance_criteria_json)), + constraints_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(constraints_json)), + status TEXT NOT NULL CHECK (status IN ('pending', 'active', 'blocked', 'completed', 'archived')), + focused INTEGER NOT NULL DEFAULT 0 CHECK (focused IN (0, 1)), + source_memory_ids_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(source_memory_ids_json)), + provenance_json TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(provenance_json)), + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + )`, + `CREATE UNIQUE INDEX IF NOT EXISTS uq_project_context_focused_work_item + ON project_context_work_items(namespace_id) WHERE focused = 1`, + `CREATE INDEX IF NOT EXISTS idx_project_context_work_items_namespace + ON project_context_work_items(namespace_id, updated_at DESC)`, + `CREATE TABLE IF NOT EXISTS project_context_facts ( + id TEXT PRIMARY KEY, + namespace_id TEXT NOT NULL, + user_id TEXT NOT NULL, + project_id TEXT, + workspace_id TEXT, + workspace_path TEXT, + kind TEXT NOT NULL CHECK (kind IN ('decision', 'constraint')), + content TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('candidate', 'active', 'superseded', 'archived')), + supersedes_id TEXT REFERENCES project_context_facts(id), + source_memory_ids_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(source_memory_ids_json)), + provenance_json TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(provenance_json)), + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_project_context_facts_namespace + ON project_context_facts(namespace_id, status, updated_at DESC)` ]; export function migrate(db: Database.Database): void { @@ -476,7 +542,7 @@ export function migrate(db: Database.Database): void { const hasMemories = tableExists(db, "memories"); const version = currentSchemaVersion(db); - if (hasMemories && version !== SCHEMA_VERSION && version !== 2 && version !== 3 && version !== 4) { + if (hasMemories && version !== SCHEMA_VERSION && version !== 2 && version !== 3 && version !== 4 && version !== 5) { throw new Error( `Unsupported memory database schema version ${version}; the database was left unchanged` ); diff --git a/Memory/src/types.ts b/Memory/src/types.ts index 317a937c4..4b1249604 100644 --- a/Memory/src/types.ts +++ b/Memory/src/types.ts @@ -1,3 +1,5 @@ +import type { ProjectContextStableResult } from "./service/project-context/project-context-types.js"; + export type IsoTime = string; export const DEFAULT_NAMESPACE_SOURCE = "unknown"; export type Cursor = string; @@ -90,6 +92,16 @@ export interface RequestEnvelope { namespace?: RuntimeNamespace; } +export type { + ProjectContextProposeGoalRequest, + ProjectContextReadState, + ProjectContextRequest, + ProjectContextStableResult, + ProjectFactRecord, + ProjectGoalRecord, + ProjectWorkItemRecord +} from "./service/project-context/project-context-types.js"; + export interface ApiErrorBody { error: { code: @@ -302,6 +314,22 @@ export interface SessionOpenRequest extends RequestEnvelope { provenance?: Partial; } +export interface TurnStartResponse { + contextPacketId: string; + turnId: string; + sessionId: string; + episodeId: string; + closedEpisodeIds: string[]; + searchEventId: string; + hits: RecallHit[]; + injectedContext: InjectedContext; + projectContext: ProjectContextStableResult; + sourceMemoryIds: string[]; + droppedDueToBudget: Array<{ id: string; kind: MemoryKind; memoryLayer: MemoryLayer; reason: "token_budget"; tokenEstimate?: number }>; + status: string[]; + serverTime: string; +} + export interface TurnStartRequest extends RequestEnvelope { sessionId: string; query: string; diff --git a/Memory/tests/contract/memory-rest-service.test.ts b/Memory/tests/contract/memory-rest-service.test.ts index 288438bbd..f9aea319f 100644 --- a/Memory/tests/contract/memory-rest-service.test.ts +++ b/Memory/tests/contract/memory-rest-service.test.ts @@ -98,6 +98,59 @@ describe("MemoryService / REST contract", () => { }); db.close(); }); + it("serves all project-context routes with strict inputs, auth, idempotency, and null focus", async () => { + const { db, service } = createTestService(); + const server = createMemoryHttpServer({ service, auth: { localServiceToken: "memory-token" } }); + await withServerClosed(server, async () => { + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("expected TCP address"); + const base = `http://127.0.0.1:${address.port}`; + const namespace = { source: "codex", profileId: "default", userId: "rest-user", projectId: "project-4" }; + const provenance = { sourceAgent: "codex", sourceMemoryIds: [], capturedAt: "2026-06-08T10:00:00.000Z", adapterId: "adapter-4", requestId: "req-4" }; + const headers = { authorization: "Bearer memory-token", "content-type": "application/json" }; + const goalInput = { namespace, source: "codex", adapterId: "adapter-4", requestId: "req-goal", provenance: { ...provenance, requestId: "req-goal" }, title: "Ship Task 4", summary: "Finish context API", detail: "All routes are covered", acceptanceCriteria: ["REST works"], constraints: ["strict schemas"] }; + const createGoal = await fetch(`${base}/api/v1/project-context/goals/propose`, { method: "POST", headers, body: JSON.stringify(goalInput) }); + const goal = await createGoal.json() as { id: string; status: string }; + expect(createGoal.status).toBe(200); + expect(goal.status).toBe("candidate"); + const duplicateGoal = await fetch(`${base}/api/v1/project-context/goals/propose`, { method: "POST", headers, body: JSON.stringify(goalInput) }); + const duplicateGoalBody = await duplicateGoal.json() as { id: string; duplicate?: boolean }; + expect(duplicateGoalBody.id).toBe(goal.id); + expect(duplicateGoalBody.duplicate).toBe(true); + const state = await fetch(`${base}/api/v1/project-context/state?namespace=${encodeURIComponent(JSON.stringify(namespace))}`, { headers: { authorization: "Bearer memory-token" } }); + expect(state.status).toBe(200); + expect((await state.json()).goals).toHaveLength(1); + const decision = { namespace, source: "codex", adapterId: "adapter-4", requestId: "req-decision", provenance: { ...provenance, requestId: "req-decision" } }; + const rejectedCandidateResponse = await fetch(`${base}/api/v1/project-context/goals/propose`, { method: "POST", headers, body: JSON.stringify({ ...goalInput, requestId: "req-reject-candidate", provenance: { ...provenance, requestId: "req-reject-candidate" }, title: "Discard me" }) }); + const rejectedCandidate = await rejectedCandidateResponse.json() as { id: string }; + const rejected = await fetch(`${base}/api/v1/project-context/goals/${encodeURIComponent(rejectedCandidate.id)}/reject`, { method: "POST", headers, body: JSON.stringify({ ...decision, requestId: "req-reject", provenance: { ...provenance, requestId: "req-reject" } }) }); + expect((await rejected.json()).status).toBe("archived"); + const approved = await fetch(`${base}/api/v1/project-context/goals/${encodeURIComponent(goal.id)}/approve`, { method: "POST", headers, body: JSON.stringify(decision) }); + expect((await approved.json()).status).toBe("active"); + const workInput = { ...decision, requestId: "req-work", provenance: { ...provenance, requestId: "req-work" }, title: "Implement tests", summary: "Add route tests", nextStep: "Review failures" }; + const workResponse = await fetch(`${base}/api/v1/project-context/work-items`, { method: "POST", headers, body: JSON.stringify(workInput) }); + const work = await workResponse.json() as { id: string }; + expect(workResponse.status).toBe(200); + const update = await fetch(`${base}/api/v1/project-context/work-items/${encodeURIComponent(work.id)}`, { method: "PATCH", headers, body: JSON.stringify({ ...decision, requestId: "req-update", provenance: { ...provenance, requestId: "req-update" }, status: "active" }) }); + expect((await update.json()).status).toBe("active"); + const focused = await fetch(`${base}/api/v1/project-context/focus`, { method: "PUT", headers, body: JSON.stringify({ ...decision, requestId: "req-focus", provenance: { ...provenance, requestId: "req-focus" }, workItemId: work.id }) }); + expect((await focused.json()).focused).toBe(true); + const cleared = await fetch(`${base}/api/v1/project-context/focus`, { method: "PUT", headers, body: JSON.stringify({ ...decision, requestId: "req-clear", provenance: { ...provenance, requestId: "req-clear" }, workItemId: null }) }); + expect(await cleared.json()).toBeNull(); + const missing = await fetch(`${base}/api/v1/project-context/goals/propose`, { method: "POST", headers, body: JSON.stringify({ ...goalInput, requestId: "req-invalid", provenance: { ...provenance, requestId: "req-invalid" }, source: undefined }) }); + expect(missing.status).toBe(400); + const unauthorized = await fetch(`${base}/api/v1/project-context/state?namespace=${encodeURIComponent(JSON.stringify(namespace))}`); + for (const field of ["namespace", "source", "adapterId", "requestId", "provenance"] as const) { + const invalid: Record = { ...goalInput, requestId: `req-invalid-${field}`, provenance: { ...provenance, requestId: `req-invalid-${field}` } }; + delete invalid[field]; + const response = await fetch(`${base}/api/v1/project-context/goals/propose`, { method: "POST", headers, body: JSON.stringify(invalid) }); + expect(response.status, field).toBe(400); + } + expect(unauthorized.status).toBe(401); + }); + db.close(); + }); it("serves structured session checkpoints through the REST client", async () => { const { db, service } = createTestService(); @@ -249,10 +302,17 @@ describe("MemoryService / REST contract", () => { episodeId: string; searchEventId: string; turnId: string; + closedEpisodeIds: string[]; + droppedDueToBudget: unknown[]; + projectContext: { version: number; status: string; goal: unknown; focusedWorkItem: unknown; markdown: string }; }; expect(startResponse.status).toBe(200); expect(started.turnId).toBe("cursor-http-turn"); expect(started.episodeId).toMatch(/^episode_/u); + expect(started.projectContext).toMatchObject({ version: 0, status: "no_confirmed_goal", goal: null, focusedWorkItem: null }); + expect(started.projectContext.markdown).toContain(''); + expect(started.closedEpisodeIds).toEqual([]); + expect(started.droppedDueToBudget).toEqual([]); const afterFirstStart = { episodes: (db.db.prepare("SELECT COUNT(*) AS count FROM episodes").get() as { count: number }).count, rawTurns: (db.db.prepare("SELECT COUNT(*) AS count FROM raw_turns").get() as { count: number }).count, diff --git a/Memory/tests/repository/project-context-repository.test.ts b/Memory/tests/repository/project-context-repository.test.ts new file mode 100644 index 000000000..37776623a --- /dev/null +++ b/Memory/tests/repository/project-context-repository.test.ts @@ -0,0 +1,157 @@ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { MemoryDb } from "../../src/storage/db.js"; +import { ProjectContextRepository, Repositories } from "../../src/storage/repositories.js"; +import type { + ProjectFactRecord, + ProjectGoalRecord, + ProjectWorkItemRecord +} from "../../src/service/project-context/project-context-types.js"; + +const NOW = "2026-08-10T00:00:00.000Z"; + +function goal(overrides: Partial = {}): ProjectGoalRecord { + return { + id: "goal-1", + namespaceId: "ns-1", + userId: "user-1", + title: "Ship project context", + summary: "Persist authoritative context", + detail: "Persist authoritative context in SQLite", + acceptanceCriteria: ["Tests pass"], + constraints: ["Keep namespace scoped"], + status: "active", + version: 1, + supersedesId: undefined, + sourceMemoryIds: ["memory-1"], + provenance: { source: "test" }, + createdAt: NOW, + updatedAt: NOW, + ...overrides + }; +} + +function workItem(overrides: Partial = {}): ProjectWorkItemRecord { + return { + id: "work-1", + namespaceId: "ns-1", + userId: "user-1", + goalId: "goal-1", + title: "Add persistence", + summary: "Add SQLite records", + nextStep: "Write repository", + acceptanceCriteria: ["Records round trip"], + constraints: [], + status: "pending", + focused: false, + sourceMemoryIds: [], + provenance: { source: "test" }, + createdAt: NOW, + updatedAt: NOW, + ...overrides + }; +} + +function fact(overrides: Partial = {}): ProjectFactRecord { + return { + id: "fact-1", + namespaceId: "ns-1", + userId: "user-1", + kind: "decision", + content: "SQLite is authoritative", + status: "active", + supersedesId: undefined, + sourceMemoryIds: [], + provenance: { source: "test" }, + createdAt: NOW, + updatedAt: NOW, + ...overrides + }; +} + +function withRepo(run: (repo: ProjectContextRepository, db: MemoryDb["db"]) => T): T { + const root = mkdtempSync(join(tmpdir(), "project-context-repository-")); + const db = new MemoryDb({ path: join(root, "memory.sqlite") }); + try { + return run(new ProjectContextRepository(db.db), db.db); + } finally { + db.close(); + rmSync(root, { recursive: true, force: true }); + } +} + +describe("project context repository", () => { + it("allows only one active goal per namespace", () => withRepo((repo) => { + repo.insertGoal(goal({ id: "goal-1" })); + expect(() => repo.insertGoal(goal({ id: "goal-2", version: 2 }))) + .toThrow(/active project goal/i); + })); + + it("replaces the active goal by archiving the prior version transactionally", () => withRepo((repo) => { + repo.insertGoal(goal({ id: "goal-1" })); + const next = repo.replaceActiveGoal(goal({ id: "goal-2", version: 2, supersedesId: "goal-1", title: "Updated" })); + expect(next).toMatchObject({ id: "goal-2", status: "active", supersedesId: "goal-1" }); + expect(repo.getGoal("goal-1")?.status).toBe("archived"); + expect(repo.getActiveGoal("ns-1")?.id).toBe("goal-2"); + })); + + it("focuses exactly one work item without changing the goal", () => withRepo((repo) => { + repo.insertGoal(goal()); + repo.insertWorkItem(workItem({ id: "work-1" })); + repo.insertWorkItem(workItem({ id: "work-2" })); + repo.setFocusedWorkItem("ns-1", "work-1", NOW); + repo.setFocusedWorkItem("ns-1", "work-2", NOW); + expect(repo.getFocusedWorkItem("ns-1")?.id).toBe("work-2"); + expect(repo.getActiveGoal("ns-1")?.id).toBe("goal-1"); + })); + + it("rejects cross-namespace focus", () => withRepo((repo) => { + repo.insertWorkItem(workItem({ id: "work-other", namespaceId: "ns-2", goalId: undefined })); + expect(() => repo.setFocusedWorkItem("ns-1", "work-other", NOW)).toThrow(/namespace/i); + })); + + it("preserves stored identity fields when updating a work item", () => withRepo((repo) => { + repo.insertGoal(goal()); + repo.insertWorkItem(workItem({ projectId: "project-1", workspaceId: "workspace-1" })); + const updated = repo.updateWorkItem(workItem({ + projectId: "changed-project", + workspaceId: "changed-workspace", + title: "Updated title" + })); + expect(updated).toMatchObject({ + projectId: "project-1", + workspaceId: "workspace-1", + title: "Updated title" + }); + expect(repo.listWorkItems("ns-1")[0]).toEqual(updated); + })); + it("safely hydrates invalid JSON shapes", () => withRepo((repo, db) => { + repo.insertGoal(goal()); + db.prepare(` + UPDATE project_context_goals + SET acceptance_criteria_json = 'null', constraints_json = '{}', + source_memory_ids_json = '["memory-1", 2]', provenance_json = '[]' + WHERE id = 'goal-1' + `).run(); + expect(repo.getGoal("goal-1")).toMatchObject({ + acceptanceCriteria: [], constraints: [], sourceMemoryIds: ["memory-1"], provenance: {} + }); + })); + + it("includes project context records in bundle export order", () => withRepo((_repo, db) => { + const tables = new Repositories(db).runtime.exportBundleTables(); + const names = Object.keys(tables); + expect(names).toEqual(expect.arrayContaining([ + "project_context_goals", "project_context_work_items", "project_context_facts" + ])); + expect(names.indexOf("project_context_goals")).toBeLessThan(names.indexOf("project_context_work_items")); + })); + + it("supersedes a fact and lists only active facts", () => withRepo((repo) => { + repo.insertFact(fact()); + repo.supersedeFact("fact-1", fact({ id: "fact-2", content: "Postgres is authoritative", supersedesId: "fact-1" })); + expect(repo.listActiveFacts("ns-1").map((item) => item.id)).toEqual(["fact-2"]); + })); +}); diff --git a/Memory/tests/repository/sqlite-schema.test.ts b/Memory/tests/repository/sqlite-schema.test.ts index 87027eda6..1d4ae51df 100644 --- a/Memory/tests/repository/sqlite-schema.test.ts +++ b/Memory/tests/repository/sqlite-schema.test.ts @@ -83,7 +83,10 @@ describe("repository sqlite schema contract", () => { "memory_processing_state", "artifacts", "audit_logs", - "memory_vector_entries" + "memory_vector_entries", + "project_context_goals", + "project_context_work_items", + "project_context_facts", ])); expect(tables.map((table) => table.name)).not.toEqual(expect.arrayContaining([ "memory_embeddings", @@ -216,6 +219,18 @@ describe("repository sqlite schema contract", () => { expect(apiLogColumns.map((column) => column.name)).toContain("source_agent"); const apiLogIndexes = db.db.prepare(`PRAGMA index_list(api_logs)`).all() as Array<{ name: string }>; expect(apiLogIndexes.map((index) => index.name)).toContain("idx_api_logs_tool_source_time"); + const goalIndexes = db.db.prepare(`PRAGMA index_list(project_context_goals)`).all() as Array<{ name: string; partial: number }>; + expect(goalIndexes).toEqual(expect.arrayContaining([ + expect.objectContaining({ name: "uq_project_context_active_goal", partial: 1 }) + ])); + const workItemIndexes = db.db.prepare(`PRAGMA index_list(project_context_work_items)`).all() as Array<{ name: string; partial: number }>; + expect(workItemIndexes).toEqual(expect.arrayContaining([ + expect.objectContaining({ name: "uq_project_context_focused_work_item", partial: 1 }) + ])); + const workItemForeignKeys = db.db.prepare(`PRAGMA foreign_key_list(project_context_work_items)`).all() as Array<{ from: string; table: string }>; + expect(workItemForeignKeys).toEqual(expect.arrayContaining([ + expect.objectContaining({ from: "goal_id", table: "project_context_goals" }) + ])); db.close(); } finally { rmSync(root, { recursive: true, force: true }); diff --git a/Memory/tests/service/project-context/project-context-service.test.ts b/Memory/tests/service/project-context/project-context-service.test.ts new file mode 100644 index 000000000..6be87d3cc --- /dev/null +++ b/Memory/tests/service/project-context/project-context-service.test.ts @@ -0,0 +1,275 @@ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { + MemoryDb, + MemoryService, + ProjectContextService, + type ProjectContextProposeGoalRequest, + type RuntimeNamespace +} from "../../../src/index.js"; +import { Repositories } from "../../../src/storage/repositories.js"; +import type { ProjectFactRecord } from "../../../src/service/project-context/project-context-types.js"; +import { namespaceIdFromContext } from "../../../src/service/namespace/namespace-scope.js"; + +const NOW = "2026-08-10T12:00:00.000Z"; +const ALPHA: RuntimeNamespace = { source: "codex", profileId: "default", tenantId: "tenant-a", userId: "user-a", projectId: "alpha" }; +const BETA: RuntimeNamespace = { ...ALPHA, projectId: "beta" }; + +function proposal(namespace: RuntimeNamespace = ALPHA, overrides: Partial = {}): ProjectContextProposeGoalRequest { + return { + namespace, + title: "Ship durable project context", + summary: "Keep the approved project goal available on every turn.", + detail: "Full authoritative implementation detail.", + acceptanceCriteria: ["Approved context renders", "Candidates stay hidden"], + constraints: ["Do not infer user intent"], + sourceMemoryIds: ["memory-a"], + provenance: { sourceAgent: "codex", capturedAt: NOW, sourceMemoryIds: ["memory-a"] }, + ...overrides + }; +} + +function withContext(run: (context: { service: ProjectContextService; memory: MemoryService; repos: Repositories }) => T): T { + const root = mkdtempSync(join(tmpdir(), "project-context-service-")); + const db = new MemoryDb({ path: join(root, "memory.sqlite") }); + const repos = new Repositories(db.db); + try { + return run({ service: new ProjectContextService({ repositories: repos, now: () => NOW }), memory: new MemoryService({ db, mode: "dev" }), repos }); + } finally { + db.close(); + rmSync(root, { recursive: true, force: true }); + } +} + +function approve(service: ProjectContextService, input = proposal()) { + const candidate = service.proposeGoal(input); + return service.approveGoal({ namespace: input.namespace, candidateId: candidate.id }); +} + +function insertFact(repos: Repositories, overrides: Partial = {}): ProjectFactRecord { + return repos.projectContext.insertFact({ + id: overrides.id ?? `fact-${Math.random()}`, + namespaceId: overrides.namespaceId ?? namespaceIdFromContext(ALPHA), + userId: "user-a", + projectId: "alpha", + kind: "constraint", + content: "runtime: node 22", + status: "active", + sourceMemoryIds: [], + provenance: {}, + createdAt: NOW, + updatedAt: NOW, + ...overrides + }); +} + +describe("ProjectContextService", () => { + it("returns the exact no_confirmed_goal status when no goal is approved", () => withContext(({ service }) => { + const candidate = service.proposeGoal(proposal()); + const stable = service.renderStable(ALPHA); + expect(candidate.status).toBe("candidate"); + expect(stable.status).toBe("no_confirmed_goal"); + expect(stable.goal).toBeNull(); + expect(stable.focusedWorkItem).toBeNull(); + expect(stable.markdown).toContain('status="no_confirmed_goal"'); + expect(stable.markdown).toContain("No confirmed project goal"); + expect(stable.markdown).not.toContain(candidate.title); + })); + + it("renders an approved goal even for an unrelated prompt", () => withContext(({ service }) => { + const active = approve(service); + const stable = service.renderStable(ALPHA); + expect(stable.status).toBe("ready"); + expect(stable.goal).toEqual(active); + expect(stable.markdown).toContain("Ship durable project context"); + })); + + it("returns full stable records while bounding only markdown", () => withContext(({ service }) => { + const detail = "implementation detail ".repeat(500); + const active = approve(service, proposal(ALPHA, { detail, summary: "summary ".repeat(200) })); + expect(service.read(ALPHA).activeGoal?.detail).toBe(detail); + const stable = service.renderStable(ALPHA); + expect(stable.goal).toEqual(active); + expect(stable.goal?.detail).toBe(detail); + expect(stable.markdown).not.toContain(detail); + })); + + it("changes focus explicitly without mutating the goal", () => withContext(({ service }) => { + const goal = approve(service); + const first = service.createWorkItem({ namespace: ALPHA, goalId: goal.id, title: "First", summary: "First item", nextStep: "Do first" }); + const second = service.createWorkItem({ namespace: ALPHA, goalId: goal.id, title: "Second", summary: "Second item", nextStep: "Do second" }); + expect(service.read(ALPHA).focusedWorkItem).toBeUndefined(); + service.selectWorkItem({ namespace: ALPHA, workItemId: first.id }); + service.selectWorkItem({ namespace: ALPHA, workItemId: second.id }); + const state = service.read(ALPHA); + expect(state.activeGoal).toEqual(goal); + expect(state.focusedWorkItem?.id).toBe(second.id); + expect(state.workItems.find((item) => item.id === first.id)?.focused).toBe(false); + })); + + it("clears focus when the focused work item becomes terminal", () => withContext(({ service }) => { + const goal = approve(service); + const item = service.createWorkItem({ namespace: ALPHA, goalId: goal.id, title: "Item", summary: "Summary", nextStep: "Next" }); + service.selectWorkItem({ namespace: ALPHA, workItemId: item.id }); + + const completed = service.updateWorkItem({ namespace: ALPHA, workItemId: item.id, status: "completed" }); + + expect(completed.focused).toBe(false); + expect(service.read(ALPHA).focusedWorkItem).toBeUndefined(); + expect(service.renderStable(ALPHA).markdown).toContain("No work item is explicitly focused"); + })); + + it("supports explicit focus clearing and renders a no-focus reminder", () => withContext(({ service }) => { + const goal = approve(service); + const item = service.createWorkItem({ namespace: ALPHA, goalId: goal.id, title: "Item", summary: "Summary", nextStep: "Next" }); + service.selectWorkItem({ namespace: ALPHA, workItemId: item.id }); + service.selectWorkItem({ namespace: ALPHA, workItemId: null }); + const stable = service.renderStable(ALPHA); + expect(stable.focusedWorkItem).toBeNull(); + expect(stable.markdown).toContain("No work item is explicitly focused"); + })); + + it("supersedes the active goal with monotonically increasing versions", () => withContext(({ service }) => { + const first = approve(service); + const candidate = service.proposeGoal(proposal(ALPHA, { title: "Second goal" })); + const second = service.approveGoal({ namespace: ALPHA, candidateId: candidate.id }); + const state = service.read(ALPHA); + expect(first.version).toBe(1); + expect(second).toMatchObject({ version: 2, supersedesId: first.id, status: "active" }); + expect(state.activeGoal?.id).toBe(second.id); + expect(state.goals.find((goal) => goal.id === first.id)?.status).toBe("archived"); + expect(state.goals.find((goal) => goal.id === candidate.id)?.status).toBe("archived"); + })); + + it("does not let candidates consume approval versions", () => withContext(({ service }) => { + const firstCandidate = service.proposeGoal(proposal(ALPHA, { title: "First candidate" })); + service.proposeGoal(proposal(ALPHA, { title: "Other candidate" })); + const first = service.approveGoal({ namespace: ALPHA, candidateId: firstCandidate.id }); + service.proposeGoal(proposal(ALPHA, { title: "Unapproved candidate" })); + const secondCandidate = service.proposeGoal(proposal(ALPHA, { title: "Second approved" })); + const second = service.approveGoal({ namespace: ALPHA, candidateId: secondCandidate.id }); + expect([first.version, second.version]).toEqual([1, 2]); + })); + + it("rejects and archives only the named candidate", () => withContext(({ service }) => { + const active = approve(service); + const rejected = service.proposeGoal(proposal(ALPHA, { title: "Rejected goal" })); + const retained = service.proposeGoal(proposal(ALPHA, { title: "Retained candidate" })); + expect(service.rejectGoal({ namespace: ALPHA, candidateId: rejected.id }).status).toBe("archived"); + const state = service.read(ALPHA); + expect(state.activeGoal?.id).toBe(active.id); + expect(state.goals.find((goal) => goal.id === retained.id)?.status).toBe("candidate"); + })); + + it.each([120, 240])("renders mandatory compact context deterministically within a %i-character budget", (budget) => withContext(({ service, repos }) => { + const long = (value: string) => `${value} ${"extended context ".repeat(100)}`; + const goal = approve(service, proposal(ALPHA, { + title: long("Goal title"), + summary: long("Goal summary"), + constraints: [long("goal constraint")], + acceptanceCriteria: [long("acceptance criterion")] + })); + const item = service.createWorkItem({ + namespace: ALPHA, + goalId: goal.id, + title: long("Focused item"), + summary: long("Focused summary"), + nextStep: long("Focused next step"), + acceptanceCriteria: [long("work acceptance")], + status: "active" + }); + service.selectWorkItem({ namespace: ALPHA, workItemId: item.id }); + insertFact(repos, { id: "constraint", kind: "constraint", content: long("runtime: node 22") }); + insertFact(repos, { id: "decision", kind: "decision", content: long("storage: sqlite") }); + + const constrained = service.renderStable(ALPHA, budget); + + expect(service.renderStable(ALPHA, budget).markdown).toBe(constrained.markdown); + expect(constrained.markdown).toMatch(/^]*>\n/); + expect(constrained.markdown).toMatch(/\nG=.+/); + expect(constrained.markdown).toMatch(/\nC=.+/); + expect(constrained.markdown).toMatch(/\nW=.+\|.+\|.+/); + expect(constrained.markdown).toMatch(/\nA=.+/); + expect(constrained.markdown).toMatch(/\nU=.+/); + expect(constrained.markdown).toMatch(/\n<\/memmy_project_context>$/); + expect(constrained.markdown).not.toContain("Decision:"); + expect(constrained.markdown).not.toContain("Metadata:"); + expect(constrained.markdown.length).toBeLessThanOrEqual(budget); + expect(constrained.goal).toEqual(goal); + expect(constrained.focusedWorkItem).toEqual({ ...item, focused: true }); + expect(constrained.facts).toHaveLength(2); + })); + + it("bounds the no-goal marker at the minimum supported budget", () => withContext(({ service }) => { + const stable = service.renderStable(ALPHA, 120); + expect(stable.markdown).toContain('status="no_confirmed_goal"'); + expect(stable.markdown.length).toBeLessThanOrEqual(120); + })); + + it("rejects budgets below the documented minimum", () => withContext(({ service }) => { + expect(() => service.renderStable(ALPHA, 119)).toThrow(/at least 120/); + })); + + it("marks conflicts, preserves their facts for review, and excludes them from authoritative prose", () => withContext(({ service, repos }) => { + approve(service); + insertFact(repos, { id: "node-20", content: "runtime: node 20" }); + insertFact(repos, { id: "node-22", content: "runtime: node 22" }); + insertFact(repos, { id: "storage", kind: "decision", content: "storage: sqlite" }); + const stable = service.renderStable(ALPHA); + expect(stable.status).toBe("conflict"); + expect(stable.facts).toHaveLength(3); + expect(stable.markdown).not.toContain("runtime: node 20"); + expect(stable.markdown).not.toContain("runtime: node 22"); + expect(stable.markdown).toContain("storage: sqlite"); + })); + + it("renders focused work details and confirmed goal metadata", () => withContext(({ service, repos }) => { + const goal = approve(service, proposal(ALPHA, { sourceMemoryIds: ["shared", "goal"] })); + const item = service.createWorkItem({ namespace: ALPHA, goalId: goal.id, title: "Item", summary: "Focused summary", nextStep: "Focused next", acceptanceCriteria: ["Focused accepted"], status: "blocked", sourceMemoryIds: ["shared", "work"] }); + service.selectWorkItem({ namespace: ALPHA, workItemId: item.id }); + insertFact(repos, { id: "fact", content: "runtime: node 22", sourceMemoryIds: ["shared", "fact"] }); + const stable = service.renderStable(ALPHA); + expect(stable.focusedWorkItem).toEqual(expect.objectContaining({ id: item.id, summary: "Focused summary", status: "blocked", nextStep: "Focused next", acceptanceCriteria: ["Focused accepted"] })); + expect(stable.focusedWorkItem).toEqual(service.read(ALPHA).focusedWorkItem); + expect(stable.markdown).toContain("Focus status: blocked"); + expect(stable.markdown).toContain("Focus summary: Focused summary"); + expect(stable.markdown).toContain(`Metadata: goal_id=${goal.id}`); + expect(stable.markdown).toContain(`confirmed_updated_at=${goal.updatedAt}`); + expect(stable.sourceMemoryIds).toEqual(["shared", "goal", "work", "fact"]); + })); + + it("isolates every read and mutation by canonical namespace", () => withContext(({ service }) => { + const alpha = approve(service); + const beta = approve(service, proposal(BETA, { title: "Beta goal" })); + expect(service.read(ALPHA).activeGoal?.id).toBe(alpha.id); + expect(service.read(BETA).activeGoal?.id).toBe(beta.id); + expect(() => service.approveGoal({ namespace: BETA, candidateId: service.proposeGoal(proposal(ALPHA)).id })).toThrow(/namespace/i); + })); + + it("exposes direct namespace reads and renders through MemoryService", () => withContext(({ memory }) => { + const candidate = memory.proposeProjectGoal(proposal()); + const active = memory.approveProjectGoal({ namespace: ALPHA, candidateId: candidate.id }); + const item = memory.createProjectWorkItem({ namespace: ALPHA, goalId: active.id, title: "Facade item", summary: "Summary", nextStep: "Next" }); + memory.updateProjectWorkItem({ namespace: ALPHA, workItemId: item.id, nextStep: null }); + memory.selectProjectWorkItem({ namespace: ALPHA, workItemId: item.id }); + expect(memory.readProjectContext(ALPHA).focusedWorkItem?.nextStep).toBe(""); + expect(memory.renderStableProjectContext(ALPHA, 500).markdown).toContain("Facade item"); + })); + + it("rejects every unscoped facade mutation, read, and render", () => withContext(({ memory }) => { + const unscoped: RuntimeNamespace = { source: "codex", profileId: "default", userId: "user-a" }; + const calls = [ + () => memory.proposeProjectGoal(proposal(unscoped)), + () => memory.approveProjectGoal({ namespace: unscoped, candidateId: "candidate" }), + () => memory.rejectProjectGoal({ namespace: unscoped, candidateId: "candidate" }), + () => memory.createProjectWorkItem({ namespace: unscoped, title: "Item", summary: "Summary", nextStep: "Next" }), + () => memory.updateProjectWorkItem({ namespace: unscoped, workItemId: "item", title: "Updated" }), + () => memory.selectProjectWorkItem({ namespace: unscoped, workItemId: null }), + () => memory.readProjectContext(unscoped), + () => memory.renderStableProjectContext(unscoped) + ]; + for (const call of calls) expect(call).toThrow(/projectId, workspaceId, or workspacePath/i); + })); +}); diff --git a/Memory/tests/service/read-model/panel-read.test.ts b/Memory/tests/service/read-model/panel-read.test.ts index d35cc94fb..216e6ccdb 100644 --- a/Memory/tests/service/read-model/panel-read.test.ts +++ b/Memory/tests/service/read-model/panel-read.test.ts @@ -314,6 +314,9 @@ describe("MemoryService / read model / panel", () => { const codexSession = service.openSession({ namespace: { source: "codex", profileId: "default", userId } }); + const ompSession = service.openSession({ + namespace: { source: "omp", profileId: "default", userId } + }); const cursorMemory = service.completeTurn("turn-panel-source-cursor", { sessionId: cursorSession.sessionId, query: "cursor panel source memory", @@ -329,6 +332,11 @@ describe("MemoryService / read model / panel", () => { query: "other panel source memory", answer: "other answer" }); + const ompMemory = service.completeTurn("turn-panel-source-omp", { + sessionId: ompSession.sessionId, + query: "omp panel source memory", + answer: "omp answer" + }); db.db.prepare("UPDATE memories SET agent_id = 'test_agent', session_id = NULL WHERE id = ?") .run(otherMemory.l1MemoryId); @@ -340,9 +348,13 @@ describe("MemoryService / read model / panel", () => { total: 1, items: [{ id: memmyMemory.l1MemoryId }] }); + expect(service.panelItems({ layer: "L1", sourceAgent: "omp", limit: 1 })).toMatchObject({ + total: 1, + items: [{ id: ompMemory.l1MemoryId, metadata: { source: "omp" } }] + }); expect(service.panelItems({ layer: "L1", - excludedSourceAgents: ["memmy-agent", "cursor", "claude_code", "codex", "opencode", "openclaw", "hermes"], + excludedSourceAgents: ["memmy-agent", "cursor", "claude_code", "codex", "pi", "opencode", "openclaw", "hermes", "workbuddy", "omp"], limit: 1 })).toMatchObject({ total: 1, @@ -351,6 +363,36 @@ describe("MemoryService / read model / panel", () => { db.close(); }); + it("derives OMP panel sources from imported session ids", () => { + const { db, service } = createTestService(); + const session = service.openSession({ + namespace: { source: "codex", profileId: "default", userId: "user-panel-omp" } + }); + const completed = service.completeTurn("turn-panel-omp", { + sessionId: session.sessionId, + query: "omp panel source memory", + answer: "omp answer" + }); + db.db.prepare("UPDATE memories SET agent_id = 'codex', session_id = 'omp::session-imported' WHERE id = ?") + .run(completed.l1MemoryId); + + expect(service.panelItems({ layer: "L1" })).toMatchObject({ + items: [{ id: completed.l1MemoryId, metadata: { source: "omp" } }] + }); + expect(service.panelOverviewSummary({}).sourceDistribution).toContainEqual( + expect.objectContaining({ source: "omp", count: 1 }) + ); + expect(service.panelItems({ layer: "L1", sourceAgent: "omp" })).toMatchObject({ + total: 1, + items: [{ id: completed.l1MemoryId, metadata: { source: "omp" } }] + }); + expect(service.panelItems({ layer: "L1", excludedSourceAgents: ["omp"] })).toMatchObject({ + total: 0, + items: [] + }); + db.close(); + }); + it("lists tasks from episodes, clamps pages, and deletes a whole task transactionally", () => { const { db, service } = createTestService(); const namespace = { diff --git a/Memory/tests/service/retrieval/query-and-filter.test.ts b/Memory/tests/service/retrieval/query-and-filter.test.ts index e36900c2b..0187c92c6 100644 --- a/Memory/tests/service/retrieval/query-and-filter.test.ts +++ b/Memory/tests/service/retrieval/query-and-filter.test.ts @@ -120,6 +120,60 @@ describe("MemoryService / retrieval / query and filtering", () => { db.close(); }); + it("keeps synchronous turn-start recall to one bounded LLM stage", async () => { + const summaryCalls: Array<{ + messages: Array<{ role: string; content: string }>; + options: { operation: string; timeoutMs?: number; maxRetries?: number }; + }> = []; + const evolutionCalls: typeof summaryCalls = []; + const config = { + ...DEFAULT_MEMMY_CONFIG, + algorithm: { + ...DEFAULT_MEMMY_CONFIG.algorithm, + retrieval: { + ...DEFAULT_MEMMY_CONFIG.algorithm.retrieval, + relativeThresholdFloor: 0, + smartSeed: false, + llmFilterEnabled: true, + llmFilterMinCandidates: 1 + } + } + }; + const { db, service } = createTestService({ + config, + llm: createQueryRewriteLlm(summaryCalls, []), + skillLlm: createQueryRewriteLlm(evolutionCalls, []), + embedder: createCapturingEmbedder([]) + }); + const namespace = { + source: "omp", + profileId: "jiang", + userId: "user-turn-start-llm-budget" + }; + service.addMemory({ + namespace, + layer: "L2", + title: "OMP handler deadline", + content: "Keep automatic memory recall within the OMP handler deadline." + }); + await service.runWorkerOnce(20); + const session = service.openSession({ namespace }); + + await service.startTurn({ + namespace, + sessionId: session.sessionId, + turnId: "turn-start-llm-budget", + query: "How should automatic memory recall stay within the OMP handler deadline?" + }); + + expect(summaryCalls.map((call) => call.options.operation)).toEqual([ + "retrieval.retrieval.filter.v5" + ]); + expect(summaryCalls[0]?.options.timeoutMs).toBe(20_000); + expect(evolutionCalls).toEqual([]); + db.close(); + }); + it("rewrites the retrieval query only when enabled", async () => { const summaryCalls: Array<{ messages: Array<{ role: string; content: string }>; diff --git a/Memory/tests/service/session/turn-capture.test.ts b/Memory/tests/service/session/turn-capture.test.ts index f7ff907b7..80da36fe5 100644 --- a/Memory/tests/service/session/turn-capture.test.ts +++ b/Memory/tests/service/session/turn-capture.test.ts @@ -113,6 +113,245 @@ describe("MemoryService / session / turn capture", () => { expect(completed.jobs.map((job) => job.jobType)).toContain("episode_idle_close"); db.close(); }); + it("prepends fresh authoritative project context and persists its metadata", async () => { + const { db, service } = createTestService(); + const namespace = { source: "codex", profileId: "default", userId: "project-turn-user", workspacePath: "/tmp/project-turn" }; + const supplemental = service.addMemory({ namespace, content: "Supplemental unrelated question guidance", layer: "L2" }); + const session = service.openSession({ namespace }); + const candidate = service.proposeProjectGoal({ namespace, title: "Ship context", summary: "Authoritative goal", detail: "Details", constraints: ["Hard constraint"], sourceMemoryIds: [supplemental.id] }); + const active = service.approveProjectGoal({ namespace, candidateId: candidate.id }); + const first = await service.startTurn({ sessionId: session.sessionId, query: "Supplemental unrelated question guidance", contextBudget: 300 }); + expect(first.projectContext.version).toBe(active.version); + expect(first.projectContext.status).toBe("ready"); + expect(first.projectContext.markdown).toContain("Ship context"); + expect(first.projectContext.markdown).toContain("Hard constraint"); + expect(first.injectedContext.markdown.indexOf(" id === supplemental.id)).toHaveLength(1); + const raw = db.db.prepare("SELECT source_memory_ids_json, message_payload_json FROM raw_turns WHERE session_id = ? AND turn_id = ?").get(session.sessionId, first.turnId) as { source_memory_ids_json: string; message_payload_json: string }; + const payload = JSON.parse(raw.message_payload_json) as { turn_start: Record }; + expect(JSON.parse(raw.source_memory_ids_json)).toEqual(first.sourceMemoryIds); + expect(payload.turn_start).toMatchObject({ projectContextVersion: first.projectContext.version, projectContextStatus: first.projectContext.status, sourceMemoryIds: first.sourceMemoryIds }); + const revision = service.proposeProjectGoal({ namespace, title: "Ship revision", summary: "Revised authoritative goal", detail: "Revision", sourceMemoryIds: [supplemental.id] }); + const revised = service.approveProjectGoal({ namespace, candidateId: revision.id }); + const next = await service.startTurn({ sessionId: session.sessionId, query: "Another unrelated question", contextBudget: 300 }); + expect(next.projectContext.version).toBe(revised.version); + expect(next.projectContext.version).toBeGreaterThan(first.projectContext.version); + const otherNamespace = { ...namespace, workspacePath: "/tmp/other" }; + const otherSession = service.openSession({ namespace: otherNamespace }); + const other = await service.startTurn({ sessionId: otherSession.sessionId, query: "Other workspace", contextBudget: 300 }); + expect(other.projectContext.status).toBe("no_confirmed_goal"); + expect(other.injectedContext.markdown).not.toContain("Ship revision"); + db.close(); + }); + + it("shares approved project context across CLI agents in the same workspace", async () => { + const { db, service } = createTestService(); + const workspacePath = "/tmp/cross-cli-project"; + const codexNamespace = { + source: "codex", + profileId: "codex-default", + userId: "cross-cli-user", + workspacePath + }; + const piNamespace = { + source: "pi", + profileId: "pi-default", + userId: "cross-cli-user", + workspacePath + }; + const candidate = service.proposeProjectGoal({ + namespace: codexNamespace, + title: "Ship cross-CLI context", + summary: "Keep every agent aligned to the approved project goal", + detail: "Codex approves the goal and Pi receives it automatically", + constraints: ["Do not treat historical recall as project authority"] + }); + const approved = service.approveProjectGoal({ + namespace: codexNamespace, + candidateId: candidate.id + }); + const workItem = service.createProjectWorkItem({ + namespace: codexNamespace, + goalId: approved.id, + title: "Verify Pi turn injection", + summary: "Open a Pi session in the same workspace", + nextStep: "Start the next Pi turn" + }); + service.selectProjectWorkItem({ + namespace: codexNamespace, + workItemId: workItem.id + }); + + const piSession = service.openSession({ namespace: piNamespace }); + const started = await service.startTurn({ + adapterId: "memmy-pi-hook", + requestId: "pi-start:cross-cli", + sessionId: piSession.sessionId, + query: "Continue the current project" + }); + + expect(started.projectContext).toMatchObject({ + status: "ready", + goal: { id: approved.id, title: "Ship cross-CLI context" }, + focusedWorkItem: { id: workItem.id, title: "Verify Pi turn injection" } + }); + expect(started.injectedContext.markdown).toContain("Ship cross-CLI context"); + expect(started.injectedContext.markdown).toContain("Verify Pi turn injection"); + expect(started.injectedContext.markdown).toContain("Do not treat historical recall as project authority"); + expect(started.injectedContext.markdown.indexOf(" { + const { db, service } = createTestService(); + const namespace = { source: "codex", profileId: "default", userId: "turn-budget-user", workspacePath: "/tmp/turn-budget" }; + const supplemental = service.addMemory({ namespace, content: "Budgeted supplemental retrieval guidance", layer: "L2" }); + const session = service.openSession({ namespace }); + const candidate = service.proposeProjectGoal({ + namespace, + title: "Budget authority", + summary: "Authoritative project context", + detail: "Keep authority ahead of supplemental recall" + }); + service.approveProjectGoal({ namespace, candidateId: candidate.id }); + const contextBudget = 300; + + const started = await service.startTurn({ + sessionId: session.sessionId, + query: "Budgeted supplemental retrieval guidance", + contextBudget + }); + + const projectEstimate = Math.ceil(started.projectContext.markdown.length / 4); + const recallRequest = db.db.prepare( + "SELECT request_json FROM recall_events WHERE id = ?" + ).get(started.searchEventId) as { request_json: string }; + expect(JSON.parse(recallRequest.request_json)).toMatchObject({ + contextBudget: contextBudget - projectEstimate + }); + expect(started.injectedContext.markdown).toContain("Budget authority"); + expect(started.injectedContext.markdown).toContain("Budgeted supplemental retrieval guidance"); + expect(started.injectedContext.tokenEstimate).toBe(Math.ceil(started.injectedContext.markdown.length / 4)); + expect(started.injectedContext.tokenEstimate).toBeLessThanOrEqual(contextBudget); + expect(started.hits.map((hit) => hit.id)).toContain(supplemental.id); + expect(started.droppedDueToBudget).not.toContainEqual(expect.objectContaining({ id: supplemental.id })); + db.close(); + }); + + it("keeps mandatory project context and suppresses supplemental recall for a tiny token budget", async () => { + const { db, service } = createTestService(); + const namespace = { source: "codex", profileId: "default", userId: "tiny-turn-budget-user", workspacePath: "/tmp/tiny-turn-budget" }; + const supplemental = service.addMemory({ namespace, content: "Tiny budget supplemental guidance", layer: "L2" }); + const session = service.openSession({ namespace }); + const candidate = service.proposeProjectGoal({ + namespace, + title: "Mandatory authority", + summary: "Must remain injected", + detail: "Even when the caller budget is below the renderer minimum" + }); + service.approveProjectGoal({ namespace, candidateId: candidate.id }); + + const request = { + sessionId: session.sessionId, + turnId: "tiny-budget-dropped-replay", + query: "Tiny budget supplemental guidance", + contextBudget: 5 + }; + const started = await service.startTurn(request); + + const recallRequest = db.db.prepare( + "SELECT request_json FROM recall_events WHERE id = ?" + ).get(started.searchEventId) as { request_json: string }; + expect(JSON.parse(recallRequest.request_json)).toMatchObject({ contextBudget: 0 }); + expect(started.injectedContext.markdown).toBe(started.projectContext.markdown); + expect(started.injectedContext.markdown).toContain(" hit.id)).toContain(supplemental.id); + expect(started.sourceMemoryIds).not.toContain(supplemental.id); + const droppedSupplemental = started.droppedDueToBudget.filter((dropped) => dropped.id === supplemental.id); + expect(droppedSupplemental).toHaveLength(1); + expect(droppedSupplemental[0]).toMatchObject({ + id: supplemental.id, + kind: "policy", + memoryLayer: "L2", + reason: "token_budget" + }); + expect(droppedSupplemental[0]?.tokenEstimate).toEqual(expect.any(Number)); + const retried = await service.startTurn(request); + expect(retried.droppedDueToBudget).toEqual(started.droppedDueToBudget); + expect(retried).toEqual(started); + db.close(); + }); + it("returns the exact persisted turn-start response when a raw turn is retried", async () => { + const { db, service } = createTestService(); + const namespace = { source: "codex", profileId: "default", userId: "turn-retry-user", workspacePath: "/tmp/turn-retry" }; + const originalSource = service.addMemory({ namespace, content: "Original retry retrieval source", layer: "L2" }); + const session = service.openSession({ namespace }); + const originalGoal = service.proposeProjectGoal({ + namespace, + title: "Original retry goal", + summary: "Keep the first packet stable", + detail: "Original project context", + sourceMemoryIds: [originalSource.id] + }); + service.approveProjectGoal({ namespace, candidateId: originalGoal.id }); + const request = { + sessionId: session.sessionId, + turnId: "turn-retry-persisted-packet", + query: "Original retry retrieval source", + contextBudget: 300 + }; + const first = await service.startTurn(request); + + const revision = service.proposeProjectGoal({ + namespace, + title: "Revised retry goal", + summary: "This must not replace the original packet", + detail: "Revised project context" + }); + service.approveProjectGoal({ namespace, candidateId: revision.id }); + const changedSource = service.addMemory({ namespace, content: "Changed retrieval source", layer: "L2" }); + const retried = await service.startTurn({ ...request, query: "Changed retrieval source" }); + + expect(retried).toEqual(first); + expect(retried.contextPacketId).toBe(first.contextPacketId); + expect(retried.searchEventId).toBe(first.searchEventId); + expect(retried.projectContext.version).toBe(first.projectContext.version); + expect(retried.sourceMemoryIds).toEqual(first.sourceMemoryIds); + expect(retried.sourceMemoryIds).not.toContain(changedSource.id); + expect(retried.injectedContext).toEqual(first.injectedContext); + const recallCount = db.db.prepare("SELECT COUNT(*) AS count FROM recall_events WHERE session_id = ? AND turn_id = ?").pluck().get(session.sessionId, request.turnId); + expect(recallCount).toBe(1); + db.close(); + }); + + it("returns bounded no-goal project context when turn writes are disabled", async () => { + const root = createTestRoot("memmy-turn-no-write-project-context-"); + const db = new MemoryDb({ path: join(root, "memory.sqlite") }); + const service = createTestMemoryService({ + db, + mode: "dev", + config: { + ...DEFAULT_MEMMY_CONFIG, + algorithm: { ...DEFAULT_MEMMY_CONFIG.algorithm, enableMemoryAdd: false } + } + }); + const started = await service.startTurn({ + sessionId: "no-write-session", + namespace: { source: "codex", profileId: "default", userId: "no-write-user", workspacePath: "/tmp/no-write" }, + query: "Read-only turn", + contextBudget: 120 + }); + expect(started.projectContext).toMatchObject({ version: 0, status: "no_confirmed_goal", goal: null, focusedWorkItem: null, facts: [], sourceMemoryIds: [] }); + expect(started.projectContext.markdown.length).toBeLessThanOrEqual(120); + expect(started.injectedContext.markdown).toContain(''); + db.close(); + }); + + it("does not capture an interrupted started turn when a newer turn completes", async () => { const { db, service } = createTestService(); diff --git a/Memory/tsconfig.json b/Memory/tsconfig.json index dd292f5e6..2dc1269cd 100644 --- a/Memory/tsconfig.json +++ b/Memory/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "dist", "rootDir": ".", - "declaration": false, + "declaration": true, "sourceMap": false, "types": ["node", "vitest/globals"] }, From 4dbfc27c63172435eb618315c023311a1da02f74 Mon Sep 17 00:00:00 2001 From: bluewater_cg Date: Mon, 10 Aug 2026 07:00:13 -0700 Subject: [PATCH 16/23] feat(backend): expose project context governance --- .../local-api-contracts/src/memory-runtime.ts | 165 +++++++++++++++++- App/backend/package.json | 9 +- .../local-api/routes/agent-runtime/panel.ts | 42 ++++- .../tests/agent-runtime-routes.test.ts | 136 ++++++++++++++- .../memory-client/http-memory-client.ts | 34 +++- .../adapters/outbound/memory-client/index.ts | 1 + .../memory-client/memory-layer-endpoints.ts | 7 + .../memos-sqlite-memory-client.ts | 150 +++++++++++----- .../tests/http-memory-client.test.ts | 73 +++++++- .../tests/memos-sqlite-memory-client.test.ts | 128 +++++++++++--- .../adapters/outbound/memory-client/types.ts | 16 ++ App/backend/src/services/panel-service.ts | 55 +++++- package-lock.json | 1 + 13 files changed, 727 insertions(+), 90 deletions(-) diff --git a/App/backend/local-api-contracts/src/memory-runtime.ts b/App/backend/local-api-contracts/src/memory-runtime.ts index d3d783086..b5f7a8170 100644 --- a/App/backend/local-api-contracts/src/memory-runtime.ts +++ b/App/backend/local-api-contracts/src/memory-runtime.ts @@ -333,7 +333,7 @@ export type StartTurnOutput = z.infer; /** Definition for complete turn input. */ export const CompleteTurnInputSchema = RuntimeRequestFieldsSchema.extend({ - sessionId: NonEmptyStringSchema, + contextPacketId: NonEmptyStringSchema.optional(), episodeId: NonEmptyStringSchema.optional(), query: NonEmptyStringSchema, answer: NonEmptyStringSchema, @@ -750,9 +750,172 @@ export const ProjectContextPackOutputSchema = z.object({ })) }), markdown: z.string(), + authoritative: z.object({ + state: z.lazy(() => ProjectContextStateSchema), + stable: z.lazy(() => ProjectContextStableStateSchema) + }).optional(), generatedAt: IsoTimeSchema }); export type ProjectContextPackOutput = z.infer; +/** Schema for a runtime namespace used by project context operations. */ +export const RuntimeNamespaceSchema = z.object({ + source: NonEmptyStringSchema, + profileId: NonEmptyStringSchema, + profileLabel: NonEmptyStringSchema.optional(), + projectId: NonEmptyStringSchema.optional(), + workspaceId: NonEmptyStringSchema.optional(), + workspacePath: NonEmptyStringSchema.optional(), + sessionKey: NonEmptyStringSchema.optional(), + userId: NonEmptyStringSchema.optional(), + tenantId: NonEmptyStringSchema.optional() +}).strict(); +export type RuntimeNamespace = z.infer; + +/** Schema for project-context mutation provenance. */ +export const ProjectContextProvenanceSchema = z.object({ + sourceAgent: NonEmptyStringSchema, + sourceMemoryIds: z.array(NonEmptyStringSchema), + capturedAt: IsoTimeSchema, + tenantId: NonEmptyStringSchema.optional(), + profileId: NonEmptyStringSchema.optional(), + projectId: NonEmptyStringSchema.optional(), + workspaceId: NonEmptyStringSchema.optional(), + workspacePath: NonEmptyStringSchema.optional(), + sessionId: NonEmptyStringSchema.optional(), + turnId: NonEmptyStringSchema.optional(), + adapterId: NonEmptyStringSchema.optional(), + requestId: NonEmptyStringSchema.optional(), + repository: NonEmptyStringSchema.optional(), + branch: NonEmptyStringSchema.optional(), + commit: NonEmptyStringSchema.optional() +}).strict(); +export type ProjectContextProvenance = z.infer; + +const ProjectContextMutationFieldsSchema = z.object({ + namespace: RuntimeNamespaceSchema, + source: NonEmptyStringSchema, + adapterId: NonEmptyStringSchema, + requestId: NonEmptyStringSchema, + provenance: ProjectContextProvenanceSchema +}).strict(); + +const ProjectGoalSchema = z.object({ + id: NonEmptyStringSchema, + namespaceId: NonEmptyStringSchema, + userId: NonEmptyStringSchema, + projectId: NonEmptyStringSchema.optional(), + workspaceId: NonEmptyStringSchema.optional(), + workspacePath: NonEmptyStringSchema.optional(), + title: NonEmptyStringSchema, + summary: z.string(), + detail: z.string(), + acceptanceCriteria: z.array(z.string()), + constraints: z.array(z.string()), + status: z.enum(["candidate", "active", "completed", "archived"]), + version: z.number().int().nonnegative(), + supersedesId: NonEmptyStringSchema.optional(), + sourceMemoryIds: z.array(NonEmptyStringSchema), + provenance: z.record(z.string(), z.unknown()), + createdAt: IsoTimeSchema, + updatedAt: IsoTimeSchema +}).strict(); +export const ProjectGoalRecordSchema = ProjectGoalSchema; +export type ProjectGoalRecord = z.infer; + +const ProjectWorkItemSchema = z.object({ + id: NonEmptyStringSchema, + namespaceId: NonEmptyStringSchema, + userId: NonEmptyStringSchema, + projectId: NonEmptyStringSchema.optional(), + workspaceId: NonEmptyStringSchema.optional(), + workspacePath: NonEmptyStringSchema.optional(), + goalId: NonEmptyStringSchema.optional(), + title: NonEmptyStringSchema, + summary: z.string(), + nextStep: z.string(), + acceptanceCriteria: z.array(z.string()), + constraints: z.array(z.string()), + status: z.enum(["pending", "active", "blocked", "completed", "archived"]), + focused: z.boolean(), + sourceMemoryIds: z.array(NonEmptyStringSchema), + provenance: z.record(z.string(), z.unknown()), + createdAt: IsoTimeSchema, + updatedAt: IsoTimeSchema +}).strict(); +export const ProjectWorkItemRecordSchema = ProjectWorkItemSchema; +export type ProjectWorkItemRecord = z.infer; + +export const ProjectFactRecordSchema = z.object({ + id: NonEmptyStringSchema, + namespaceId: NonEmptyStringSchema, + userId: NonEmptyStringSchema, + projectId: NonEmptyStringSchema.optional(), + workspaceId: NonEmptyStringSchema.optional(), + workspacePath: NonEmptyStringSchema.optional(), + kind: z.enum(["decision", "constraint"]), + content: NonEmptyStringSchema, + status: z.enum(["candidate", "active", "superseded", "archived"]), + supersedesId: NonEmptyStringSchema.optional(), + sourceMemoryIds: z.array(NonEmptyStringSchema), + provenance: z.record(z.string(), z.unknown()), + createdAt: IsoTimeSchema, + updatedAt: IsoTimeSchema +}).strict(); +export type ProjectFactRecord = z.infer; + +export const ProjectContextStateSchema = z.object({ + namespaceId: NonEmptyStringSchema, + activeGoal: ProjectGoalRecordSchema.nullable(), + goals: z.array(ProjectGoalRecordSchema), + workItems: z.array(ProjectWorkItemRecordSchema), + focusedWorkItem: ProjectWorkItemRecordSchema.nullable(), + facts: z.array(ProjectFactRecordSchema) +}).strict(); +export type ProjectContextState = z.infer; + +export const ProjectContextStableStateSchema = z.object({ + namespaceId: NonEmptyStringSchema, + status: z.enum(["ready", "no_confirmed_goal", "conflict"]), + version: z.number().int().nonnegative(), + goal: ProjectGoalRecordSchema.nullable(), + focusedWorkItem: ProjectWorkItemRecordSchema.nullable(), + facts: z.array(ProjectFactRecordSchema), + markdown: z.string(), + sourceMemoryIds: z.array(NonEmptyStringSchema), + generatedAt: IsoTimeSchema +}).strict(); +export type ProjectContextStableState = z.infer; + +export const ProjectContextReadStateSchema = ProjectContextStateSchema; +export type ProjectContextReadState = z.infer; +export const ProjectContextProposeGoalInputSchema = ProjectContextMutationFieldsSchema.extend({ + title: NonEmptyStringSchema, + summary: z.string(), + detail: z.string(), + acceptanceCriteria: z.array(z.string()).optional(), + constraints: z.array(z.string()).optional(), + sourceMemoryIds: z.array(NonEmptyStringSchema).optional() +}).strict(); +export type ProjectContextProposeGoalInput = z.infer; +export const ProjectContextGoalDecisionInputSchema = ProjectContextMutationFieldsSchema.strict(); +export type ProjectContextGoalDecisionInput = z.infer; +export const ProjectContextWorkItemCreateInputSchema = ProjectContextMutationFieldsSchema.extend({ + goalId: NonEmptyStringSchema.optional(), + title: NonEmptyStringSchema, + summary: z.string(), + nextStep: z.string(), + acceptanceCriteria: z.array(z.string()).optional(), + constraints: z.array(z.string()).optional(), + status: z.enum(["pending", "active", "blocked", "completed", "archived"]).optional(), + sourceMemoryIds: z.array(NonEmptyStringSchema).optional() +}).strict(); +export type ProjectContextWorkItemCreateInput = z.infer; +export const ProjectContextWorkItemUpdateInputSchema = ProjectContextMutationFieldsSchema.extend({ + goalId: NonEmptyStringSchema.nullable().optional(), title: NonEmptyStringSchema.nullable().optional(), summary: z.string().nullable().optional(), nextStep: z.string().nullable().optional(), acceptanceCriteria: z.array(z.string()).nullable().optional(), constraints: z.array(z.string()).nullable().optional(), status: z.enum(["pending", "active", "blocked", "completed", "archived"]).nullable().optional(), sourceMemoryIds: z.array(NonEmptyStringSchema).nullable().optional() +}).strict(); +export type ProjectContextWorkItemUpdateInput = z.infer; +export const ProjectContextFocusInputSchema = ProjectContextMutationFieldsSchema.extend({ workItemId: NonEmptyStringSchema.nullable() }).strict(); +export type ProjectContextFocusInput = z.infer; /** Schema for panel items output. */ export const PanelItemsOutputSchema = z.object({ diff --git a/App/backend/package.json b/App/backend/package.json index 448975071..521b8dc44 100644 --- a/App/backend/package.json +++ b/App/backend/package.json @@ -12,15 +12,16 @@ } }, "scripts": { - "build": "npm run build -w @memmy/local-api-contracts && node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json && node -e \"const fs=require('node:fs'); fs.cpSync('src/infrastructure/app-state-store/migrations', 'dist/src/infrastructure/app-state-store/migrations', { recursive: true }); fs.cpSync('src/infrastructure/agent-source-store/migrations', 'dist/src/infrastructure/agent-source-store/migrations', { recursive: true }); fs.cpSync('src/infrastructure/idempotency-store/migrations', 'dist/src/infrastructure/idempotency-store/migrations', { recursive: true }); if (fs.existsSync('src/adapters/outbound/agent-adapter/plugins')) fs.cpSync('src/adapters/outbound/agent-adapter/plugins', 'dist/src/adapters/outbound/agent-adapter/plugins', { recursive: true });\"", + "build": "npm run build -w @memmy/memory && npm run build -w @memmy/local-api-contracts && node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json && node -e \"const fs=require('node:fs'); fs.cpSync('src/infrastructure/app-state-store/migrations', 'dist/src/infrastructure/app-state-store/migrations', { recursive: true }); fs.cpSync('src/infrastructure/agent-source-store/migrations', 'dist/src/infrastructure/agent-source-store/migrations', { recursive: true }); fs.cpSync('src/infrastructure/idempotency-store/migrations', 'dist/src/infrastructure/idempotency-store/migrations', { recursive: true }); if (fs.existsSync('src/adapters/outbound/agent-adapter/plugins')) fs.cpSync('src/adapters/outbound/agent-adapter/plugins', 'dist/src/adapters/outbound/agent-adapter/plugins', { recursive: true });\"", "lint": "eslint \"src/**/*.ts\" \"vitest.config.ts\"", - "typecheck": "npm run build -w @memmy/local-api-contracts && tsc -p tsconfig.json --noEmit", - "test": "npm run build -w @memmy/local-api-contracts && vitest run", - "test:agent-adapter:coverage": "npm run build -w @memmy/local-api-contracts && vitest run src/adapters/outbound/agent-adapter/tests --coverage", + "typecheck": "npm run build -w @memmy/memory && npm run build -w @memmy/local-api-contracts && tsc -p tsconfig.json --noEmit", + "test": "npm run build -w @memmy/memory && npm run build -w @memmy/local-api-contracts && vitest run", + "test:agent-adapter:coverage": "npm run build -w @memmy/memory && npm run build -w @memmy/local-api-contracts && vitest run src/adapters/outbound/agent-adapter/tests --coverage", "db:migrate": "tsx src/infrastructure/app-state-store/cli/migrate.ts" }, "dependencies": { "@memmy/local-api-contracts": "0.0.0", + "@memmy/memory": "1.0.4", "@modelcontextprotocol/sdk": "^1.29.0", "dotenv": "^16.6.1", "fastify": "^5.8.5", diff --git a/App/backend/src/adapters/inbound/local-api/routes/agent-runtime/panel.ts b/App/backend/src/adapters/inbound/local-api/routes/agent-runtime/panel.ts index 6b0017f70..9b26e9ed9 100644 --- a/App/backend/src/adapters/inbound/local-api/routes/agent-runtime/panel.ts +++ b/App/backend/src/adapters/inbound/local-api/routes/agent-runtime/panel.ts @@ -1,5 +1,5 @@ /** Memory Panel runtime routes. */ -import { PanelItemsInputSchema, PanelTasksInputSchema } from "@memmy/local-api-contracts"; +import { PanelItemsInputSchema, PanelTasksInputSchema, ProjectContextFocusInputSchema, ProjectContextGoalDecisionInputSchema, ProjectContextProposeGoalInputSchema, ProjectContextWorkItemCreateInputSchema, ProjectContextWorkItemUpdateInputSchema, RuntimeNamespaceSchema } from "@memmy/local-api-contracts"; import type { FastifyInstance } from "fastify"; import { z } from "zod"; import { withErrorEnvelope } from "../../../../../services/error-envelope.js"; @@ -32,6 +32,39 @@ export function registerPanelRoutes(app: FastifyInstance, deps: AgentRuntimeRout }) ); + app.get("/api/v1/project-context/state", { preHandler: deps.authenticateRuntimeToken }, withErrorEnvelope(async (request, reply) => { + const query = request.query as Record; + const namespace = typeof query.namespace === "string" + ? z.string().transform((value, ctx) => { + try { + return JSON.parse(value) as unknown; + } catch { + ctx.addIssue({ code: "custom", message: "namespace must be valid JSON" }); + return z.NEVER; + } + }).pipe(RuntimeNamespaceSchema).parse(query.namespace) + : RuntimeNamespaceSchema.parse(query); + return reply.send(await deps.services.panel.projectContextState(namespace, runtimeContext(request))); + })); + app.post("/api/v1/project-context/goals/propose", { preHandler: deps.authenticateRuntimeToken }, withErrorEnvelope(async (request, reply) => { + const input = ProjectContextProposeGoalInputSchema.parse(request.body); + return reply.send(await deps.services.panel.proposeProjectGoal(input, runtimeContext(request))); + })); + app.post("/api/v1/project-context/goals/:id/approve", { preHandler: deps.authenticateRuntimeToken }, withErrorEnvelope(async (request, reply) => { + const { id } = z.object({ id: z.string().min(1) }).parse(request.params); + return reply.send(await deps.services.panel.approveProjectGoal(id, ProjectContextGoalDecisionInputSchema.parse(request.body), runtimeContext(request))); + })); + app.post("/api/v1/project-context/goals/:id/reject", { preHandler: deps.authenticateRuntimeToken }, withErrorEnvelope(async (request, reply) => { + const { id } = z.object({ id: z.string().min(1) }).parse(request.params); + return reply.send(await deps.services.panel.rejectProjectGoal(id, ProjectContextGoalDecisionInputSchema.parse(request.body), runtimeContext(request))); + })); + app.post("/api/v1/project-context/work-items", { preHandler: deps.authenticateRuntimeToken }, withErrorEnvelope(async (request, reply) => reply.send(await deps.services.panel.createProjectWorkItem(ProjectContextWorkItemCreateInputSchema.parse(request.body), runtimeContext(request))))); + app.patch("/api/v1/project-context/work-items/:id", { preHandler: deps.authenticateRuntimeToken }, withErrorEnvelope(async (request, reply) => { + const { id } = z.object({ id: z.string().min(1) }).parse(request.params); + return reply.send(await deps.services.panel.updateProjectWorkItem(id, ProjectContextWorkItemUpdateInputSchema.parse(request.body), runtimeContext(request))); + })); + app.put("/api/v1/project-context/focus", { preHandler: deps.authenticateRuntimeToken }, withErrorEnvelope(async (request, reply) => reply.send(await deps.services.panel.setProjectFocus(ProjectContextFocusInputSchema.parse(request.body), runtimeContext(request))))); + app.get( "/api/v1/panel/items", { preHandler: deps.authenticateRuntimeToken }, @@ -66,8 +99,11 @@ export function registerPanelRoutes(app: FastifyInstance, deps: AgentRuntimeRout } -function runtimeContext(): RuntimeContext { - return { adapterId: "runtime" }; +function runtimeContext(request?: { headers?: Record; body?: unknown }): RuntimeContext { + const body = request?.body as { requestId?: unknown } | undefined; + const header = request?.headers?.["x-request-id"]; + const requestId = typeof body?.requestId === "string" ? body.requestId : typeof header === "string" ? header : undefined; + return { adapterId: "runtime", requestId }; } function queryValues(rawUrl: string | undefined, name: string): string[] | undefined { diff --git a/App/backend/src/adapters/inbound/local-api/tests/agent-runtime-routes.test.ts b/App/backend/src/adapters/inbound/local-api/tests/agent-runtime-routes.test.ts index 27821269c..f0c2622de 100644 --- a/App/backend/src/adapters/inbound/local-api/tests/agent-runtime-routes.test.ts +++ b/App/backend/src/adapters/inbound/local-api/tests/agent-runtime-routes.test.ts @@ -36,6 +36,13 @@ describe("agent runtime local api routes", () => { { method: "GET", url: "/api/v1/panel/overview" }, { method: "GET", url: "/api/v1/panel/analysis" }, { method: "GET", url: "/api/v1/panel/context-pack?projectId=project-1" }, + { method: "GET", url: `/api/v1/project-context/state?namespace=${encodeURIComponent(JSON.stringify(projectNamespace()))}` }, + { method: "POST", url: "/api/v1/project-context/goals/propose", payload: { ...projectMutation(), title: "Ship context", summary: "", detail: "" } }, + { method: "POST", url: "/api/v1/project-context/goals/goal-1/approve", payload: projectMutation() }, + { method: "POST", url: "/api/v1/project-context/goals/goal-1/reject", payload: projectMutation() }, + { method: "POST", url: "/api/v1/project-context/work-items", payload: { ...projectMutation(), title: "Verify context", summary: "", nextStep: "Run smoke" } }, + { method: "PATCH", url: "/api/v1/project-context/work-items/work-1", payload: { ...projectMutation(), status: "active" } }, + { method: "PUT", url: "/api/v1/project-context/focus", payload: { ...projectMutation(), workItemId: "work-1" } }, { method: "GET", url: "/api/v1/panel/items?layer=L1&status=activated&page=1" }, { method: "GET", url: "/api/v1/panel/tasks?page=1" }, { method: "DELETE", url: "/api/v1/panel/tasks/episode-1" } @@ -49,7 +56,7 @@ describe("agent runtime local api routes", () => { payload: request.payload }); - expect(response.statusCode, `${request.method} ${request.url}`).toBe(200); + expect(response.statusCode, `${request.method} ${request.url}: ${response.body}`).toBe(200); } }); @@ -215,6 +222,24 @@ describe("agent runtime local api routes", () => { }); }); + it("returns invalid_argument for malformed project namespace JSON", async () => { + app = createServer(); + + const response = await app.inject({ + method: "GET", + url: "/api/v1/project-context/state?namespace=%7Bbroken", + headers: { "x-memmy-local-token": "test-token", "x-request-id": "req-namespace" } + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toMatchObject({ + error: { + code: "invalid_argument", + requestId: "req-namespace" + } + }); + }); + it("unwraps duplicate service responses", async () => { app = createServer({ turn: { @@ -264,6 +289,64 @@ describe("agent runtime local api routes", () => { } }); }); + + it("forwards project context governance through the authenticated local API", async () => { + const calls: Array<{ operation: string; id?: string; input: unknown; context?: unknown }> = []; + app = createServer({ + panel: { + async projectContextState(input: unknown, context: unknown) { + calls.push({ operation: "state", input, context }); + return projectContextStateOutput(); + }, + async proposeProjectGoal(input: unknown, context: unknown) { + calls.push({ operation: "propose", input, context }); + return projectGoalOutput("candidate"); + }, + async approveProjectGoal(id: string, input: unknown, context: unknown) { + calls.push({ operation: "approve", id, input, context }); + return projectGoalOutput("active"); + }, + async rejectProjectGoal(id: string, input: unknown, context: unknown) { + calls.push({ operation: "reject", id, input, context }); + return projectGoalOutput("archived"); + }, + async createProjectWorkItem(input: unknown, context: unknown) { + calls.push({ operation: "create-work", input, context }); + return projectWorkItemOutput(); + }, + async updateProjectWorkItem(id: string, input: unknown, context: unknown) { + calls.push({ operation: "update-work", id, input, context }); + return projectWorkItemOutput(); + }, + async setProjectFocus(input: unknown, context: unknown) { + calls.push({ operation: "focus", input, context }); + return projectWorkItemOutput(); + } + } + }); + const headers = { "x-memmy-local-token": "test-token", "x-request-id": "route-request" }; + const requests = [ + { method: "GET", url: `/api/v1/project-context/state?namespace=${encodeURIComponent(JSON.stringify(projectNamespace()))}` }, + { method: "POST", url: "/api/v1/project-context/goals/propose", payload: { ...projectMutation(), title: "Ship context", summary: "", detail: "" } }, + { method: "POST", url: "/api/v1/project-context/goals/goal-1/approve", payload: projectMutation() }, + { method: "POST", url: "/api/v1/project-context/goals/goal-1/reject", payload: projectMutation() }, + { method: "POST", url: "/api/v1/project-context/work-items", payload: { ...projectMutation(), title: "Verify context", summary: "", nextStep: "Run smoke" } }, + { method: "PATCH", url: "/api/v1/project-context/work-items/work-1", payload: { ...projectMutation(), status: "active" } }, + { method: "PUT", url: "/api/v1/project-context/focus", payload: { ...projectMutation(), workItemId: "work-1" } } + ]; + + for (const request of requests) { + const response = await app.inject({ ...request, headers }); + expect(response.statusCode, `${request.method} ${request.url}: ${response.body}`).toBe(200); + } + + expect(calls.map(({ operation, id }) => id ? `${operation}:${id}` : operation)).toEqual([ + "state", "propose", "approve:goal-1", "reject:goal-1", "create-work", "update-work:work-1", "focus" + ]); + expect(calls[0]?.input).toEqual(projectNamespace()); + expect(hasRuntimeProvenance(calls[0]?.context, "runtime", "route-request")).toBe(true); + expect(calls.slice(1).every(({ context }) => hasRuntimeProvenance(context, "runtime", "client-request"))).toBe(true); + }); }); function createServer(overrides: Record = {}): FastifyInstance { @@ -374,7 +457,14 @@ function createServer(overrides: Record = {}): FastifyInstance async items() { return panelItemsOutput(); }, async tasks() { return panelTasksOutput(); }, async deleteTask(id: string) { return { ok: true as const, id, deletedMemoryIds: [], serverTime: now() }; }, - async memoryApiLogs() { return { logs: [], total: 0, limit: 20, offset: 0, serverTime: now() }; } + async memoryApiLogs() { return { logs: [], total: 0, limit: 20, offset: 0, serverTime: now() }; }, + async projectContextState() { return projectContextStateOutput(); }, + async proposeProjectGoal() { return projectGoalOutput("candidate"); }, + async approveProjectGoal() { return projectGoalOutput("active"); }, + async rejectProjectGoal() { return projectGoalOutput("archived"); }, + async createProjectWorkItem() { return projectWorkItemOutput(); }, + async updateProjectWorkItem() { return projectWorkItemOutput(); }, + async setProjectFocus() { return projectWorkItemOutput(); }, }, ...overrides } as unknown as BackendServices; @@ -386,6 +476,48 @@ function createServer(overrides: Record = {}): FastifyInstance }); } +function projectNamespace() { + return { source: "codex", profileId: "default", userId: "user-1", projectId: "project-1" }; +} + +function projectMutation() { + return { + namespace: projectNamespace(), + source: "desktop", + adapterId: "desktop-client", + requestId: "client-request", + provenance: { sourceAgent: "desktop", sourceMemoryIds: [], capturedAt: now() } + }; +} + +function hasRuntimeProvenance(input: unknown, adapterId: string, requestId: string): boolean { + if (!input || typeof input !== "object") return false; + if (!("adapterId" in input) || !("requestId" in input)) return false; + return input.adapterId === adapterId && input.requestId === requestId; +} + +function projectGoalOutput(status: "candidate" | "active" | "archived") { + return { + id: "goal-1", namespaceId: "local:project-1", userId: "user-1", projectId: "project-1", + title: "Ship context", summary: "", detail: "", acceptanceCriteria: [], constraints: [], status, + version: 1, sourceMemoryIds: [], provenance: {}, createdAt: now(), updatedAt: now() + }; +} + +function projectWorkItemOutput() { + return { + id: "work-1", namespaceId: "local:project-1", userId: "user-1", projectId: "project-1", goalId: "goal-1", + title: "Verify context", summary: "", nextStep: "Run smoke", acceptanceCriteria: [], constraints: [], + status: "active" as const, focused: true, sourceMemoryIds: [], provenance: {}, createdAt: now(), updatedAt: now() + }; +} + +function projectContextStateOutput() { + const goal = projectGoalOutput("active"); + const workItem = projectWorkItemOutput(); + return { namespaceId: "local:project-1", activeGoal: goal, goals: [goal], workItems: [workItem], focusedWorkItem: workItem, facts: [] }; +} + function memoryModels() { return { summary: { provider: "openai_compatible", model: "memory_summary", configured: true, remote: true }, diff --git a/App/backend/src/adapters/outbound/memory-client/http-memory-client.ts b/App/backend/src/adapters/outbound/memory-client/http-memory-client.ts index 9a98be05e..ecc7767f1 100644 --- a/App/backend/src/adapters/outbound/memory-client/http-memory-client.ts +++ b/App/backend/src/adapters/outbound/memory-client/http-memory-client.ts @@ -17,6 +17,9 @@ import { PanelItemsOutputSchema, PanelOverviewOutputSchema, ProjectContextPackOutputSchema, + ProjectContextReadStateSchema, + ProjectGoalRecordSchema, + ProjectWorkItemRecordSchema, PanelTasksOutputSchema, OpenSessionOutputSchema, SearchOutputSchema, @@ -57,7 +60,7 @@ export function createHttpMemoryClient( const fetchImpl = options.fetchImpl ?? globalThis.fetch; async function request( - method: "GET" | "POST" | "DELETE", + method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", pathKey: PathKey, responseSchema: ZodType, requestOptions: { @@ -231,6 +234,35 @@ export function createHttpMemoryClient( headers: { "x-memmy-project-id": projectId } }); }, + async projectContextState(namespace) { + return request("GET", "projectContextState", ProjectContextReadStateSchema, { + query: { namespace: JSON.stringify(namespace) } + }); + }, + + async proposeProjectGoal(input) { + return request("POST", "proposeProjectGoal", ProjectGoalRecordSchema, { body: input }); + }, + + async approveProjectGoal(goalId, input) { + return request("POST", "approveProjectGoal", ProjectGoalRecordSchema, { params: { id: goalId }, body: input }); + }, + + async rejectProjectGoal(goalId, input) { + return request("POST", "rejectProjectGoal", ProjectGoalRecordSchema, { params: { id: goalId }, body: input }); + }, + + async createProjectWorkItem(input) { + return request("POST", "createProjectWorkItem", ProjectWorkItemRecordSchema, { body: input }); + }, + + async updateProjectWorkItem(workItemId, input) { + return request("PATCH", "updateProjectWorkItem", ProjectWorkItemRecordSchema, { params: { id: workItemId }, body: input }); + }, + + async setProjectFocus(input) { + return request("PUT", "setProjectFocus", ProjectWorkItemRecordSchema.nullable(), { body: input }); + }, async panelItems(input) { return request("GET", "panelItems", PanelItemsOutputSchema, { query: input }); diff --git a/App/backend/src/adapters/outbound/memory-client/index.ts b/App/backend/src/adapters/outbound/memory-client/index.ts index b0e999c73..c99ab027d 100644 --- a/App/backend/src/adapters/outbound/memory-client/index.ts +++ b/App/backend/src/adapters/outbound/memory-client/index.ts @@ -4,6 +4,7 @@ export { createMemosSqliteMemoryClient, discoverMemosSqliteSources, type CreateMemosSqliteMemoryClientOptions, + type EmbeddedProjectContextService, type MemosSqliteSource } from "./memos-sqlite-memory-client.js"; export { MemoryLayerError, MemoryLayerNetworkError } from "./errors.js"; diff --git a/App/backend/src/adapters/outbound/memory-client/memory-layer-endpoints.ts b/App/backend/src/adapters/outbound/memory-client/memory-layer-endpoints.ts index aedec46c3..2ba329d4d 100644 --- a/App/backend/src/adapters/outbound/memory-client/memory-layer-endpoints.ts +++ b/App/backend/src/adapters/outbound/memory-client/memory-layer-endpoints.ts @@ -21,6 +21,13 @@ export const MEMORY_LAYER_PATHS = Object.freeze({ panelOverview: "/api/v1/panel/overview", panelAnalysis: "/api/v1/panel/analysis", projectContextPack: "/api/v1/panel/context-pack", + projectContextState: "/api/v1/project-context/state", + proposeProjectGoal: "/api/v1/project-context/goals/propose", + approveProjectGoal: "/api/v1/project-context/goals/:id/approve", + rejectProjectGoal: "/api/v1/project-context/goals/:id/reject", + createProjectWorkItem: "/api/v1/project-context/work-items", + updateProjectWorkItem: "/api/v1/project-context/work-items/:id", + setProjectFocus: "/api/v1/project-context/focus", panelItems: "/api/v1/panel/items", panelTasks: "/api/v1/panel/tasks", deletePanelTask: "/api/v1/panel/tasks/:id" diff --git a/App/backend/src/adapters/outbound/memory-client/memos-sqlite-memory-client.ts b/App/backend/src/adapters/outbound/memory-client/memos-sqlite-memory-client.ts index 7aaa7a88b..55df71a95 100644 --- a/App/backend/src/adapters/outbound/memory-client/memos-sqlite-memory-client.ts +++ b/App/backend/src/adapters/outbound/memory-client/memos-sqlite-memory-client.ts @@ -4,6 +4,7 @@ import { homedir } from "node:os"; import { basename, join, resolve } from "node:path"; import { DatabaseSync } from "node:sqlite"; import { getLoadablePath as getSqliteVecLoadablePath } from "sqlite-vec"; +import { MemoryDb, ProjectContextService, Repositories } from "@memmy/memory"; import type { AddMemoryInput, AddMemoryOutput, @@ -40,6 +41,16 @@ import type { MemoryClient } from "./types.js"; const SOURCE_ID_SEPARATOR = "::"; const DEFAULT_MEMORY_HOME = join(homedir(), ".memmy"); const PANEL_DAILY_ACTIVITY_DAYS = 371; +export interface EmbeddedProjectContextService { + readProjectContext(namespace: Parameters[0]): Awaited>; + proposeProjectGoal(input: Parameters[0]): Awaited>; + approveProjectGoal(input: { namespace: Parameters[0]; candidateId: string }): Awaited>; + rejectProjectGoal(input: { namespace: Parameters[0]; candidateId: string }): Awaited>; + createProjectWorkItem(input: Parameters[0]): Awaited>; + updateProjectWorkItem(input: Parameters[1] & { workItemId: string }): Awaited>; + selectProjectWorkItem(input: Parameters[0]): Awaited> | undefined; +} + export interface MemosSqliteSource { id: string; @@ -50,6 +61,7 @@ export interface MemosSqliteSource { export interface CreateMemosSqliteMemoryClientOptions { sources: readonly MemosSqliteSource[]; now?: () => string; + memoryService?: EmbeddedProjectContextService; } interface LocalMemoryRow { @@ -160,6 +172,7 @@ export function discoverMemosSqliteSources(env: NodeJS.ProcessEnv = process.env) export function createMemosSqliteMemoryClient(options: CreateMemosSqliteMemoryClientOptions): MemoryClient { const now = options.now ?? (() => new Date().toISOString()); const sources = options.sources.filter((source) => existsSync(source.dbPath)); + const memoryService = options.memoryService ?? createEmbeddedProjectContextService(sources); return { async health() { @@ -345,7 +358,7 @@ export function createMemosSqliteMemoryClient(options: CreateMemosSqliteMemoryCl async panelAnalysis(): Promise { const rows = listMemoryRows(sources); const dates = lastSevenDateKeys(now()); - const logs = listApiLogRows(sources, {}, 10_000) + const logs = readApiLogRows(sources, {}, 10_000).rows .filter((row) => dates.includes(dateKey(row.called_at))); const skillRows = rows.filter((item) => item.row.memory_layer === "Skill"); const recallScores = logs @@ -373,6 +386,35 @@ export function createMemosSqliteMemoryClient(options: CreateMemosSqliteMemoryCl return readOnlyOperationUnavailable(); }, + async projectContextState(namespace) { + const state = requireProjectContextService(memoryService).readProjectContext(namespace); + return { ...state, activeGoal: state.activeGoal ?? null, focusedWorkItem: state.focusedWorkItem ?? null }; + }, + + async proposeProjectGoal(input) { + return requireProjectContextService(memoryService).proposeProjectGoal(input); + }, + + async approveProjectGoal(goalId, input) { + return requireProjectContextService(memoryService).approveProjectGoal({ namespace: input.namespace, candidateId: goalId }); + }, + + async rejectProjectGoal(goalId, input) { + return requireProjectContextService(memoryService).rejectProjectGoal({ namespace: input.namespace, candidateId: goalId }); + }, + + async createProjectWorkItem(input) { + return requireProjectContextService(memoryService).createProjectWorkItem(input); + }, + + async updateProjectWorkItem(workItemId, input) { + return requireProjectContextService(memoryService).updateProjectWorkItem({ ...input, workItemId }); + }, + + async setProjectFocus(input) { + return requireProjectContextService(memoryService).selectProjectWorkItem(input) ?? null; + }, + async panelItems(input: PanelItemsInput): Promise { const pageSize = 20; const filtered = listMemoryRows(sources) @@ -446,7 +488,8 @@ export function createMemosSqliteMemoryClient(options: CreateMemosSqliteMemoryCl async memoryApiLogs(input: MemoryApiLogsInput): Promise { const limit = normalizeLimit(input.limit); const offset = normalizeOffset(input.offset); - const rows = listApiLogRows(sources, input, limit + offset); + const result = readApiLogRows(sources, input, limit + offset); + const rows = result.rows; return { logs: rows.slice(offset, offset + limit).map((row) => ({ @@ -454,12 +497,12 @@ export function createMemosSqliteMemoryClient(options: CreateMemosSqliteMemoryCl toolName: row.tool_name, ...(row.source_agent ? { sourceAgent: row.source_agent } : {}), inputJson: row.input_json, - outputJson: apiLogOutputWithCurrentTraceSummary(row), + outputJson: row.output_json, durationMs: nonNegativeInt(row.duration_ms, 0), success: row.success !== 0, calledAt: normalizeIsoTime(row.called_at) })), - total: countApiLogRows(sources, input), + total: result.total, limit, offset, nextOffset: rows.length > offset + limit ? offset + limit : undefined, @@ -469,6 +512,33 @@ export function createMemosSqliteMemoryClient(options: CreateMemosSqliteMemoryCl }; } +function createEmbeddedProjectContextService(sources: readonly MemosSqliteSource[]): EmbeddedProjectContextService | undefined { + const source = sources.find((candidate) => basename(candidate.dbPath) === "memory.sqlite") ?? sources[0]; + if (!source) return undefined; + const invoke = (operation: (service: ProjectContextService) => Result): Result => { + const db = new MemoryDb({ path: source.dbPath }); + try { + return operation(new ProjectContextService({ repositories: new Repositories(db.db) })); + } finally { + db.close(); + } + }; + return { + readProjectContext: (namespace) => invoke((service) => { const state = service.read(namespace); return { ...state, activeGoal: state.activeGoal ?? null, focusedWorkItem: state.focusedWorkItem ?? null }; }), + proposeProjectGoal: (input) => invoke((service) => service.proposeGoal(input)), + approveProjectGoal: (input) => invoke((service) => service.approveGoal(input)), + rejectProjectGoal: (input) => invoke((service) => service.rejectGoal(input)), + createProjectWorkItem: (input) => invoke((service) => service.createWorkItem(input)), + updateProjectWorkItem: (input) => invoke((service) => service.updateWorkItem(input)), + selectProjectWorkItem: (input) => invoke((service) => service.selectWorkItem(input)) + }; +} + +function requireProjectContextService(service: EmbeddedProjectContextService | undefined): EmbeddedProjectContextService { + if (!service) throw new MemoryLayerError("memory_layer_unavailable", 503, "project context requires a Memory service SQLite source"); + return service; +} + /** * Throws the unified error for write operations not supported by the local SQLite data source. */ @@ -497,21 +567,20 @@ function listMemoryRows(sources: readonly MemosSqliteSource[]): MemoryRow[] { * @param maxRows the maximum number of rows to prefetch for cross-source merge sorting. * @returns log rows sorted by call time in descending order. */ -function listApiLogRows( +function readApiLogRows( sources: readonly MemosSqliteSource[], input: MemoryApiLogsInput, maxRows: number -): LocalApiLogRow[] { +): { rows: LocalApiLogRow[]; total: number } { const tools = normalizeApiLogTools(input.tools); const placeholders = tools.map(() => "?").join(", "); const agentFilter = apiLogSourceAgentFilter(input); - return sources - .flatMap((source) => withDb(source, (db) => { + const sourceResults = sources.map((source) => withDb(source, (db) => { if (!tableExists(db, "api_logs")) { - return []; + return { rows: [] as LocalApiLogRow[], total: 0 }; } - return db + const rows = db .prepare( `SELECT id, tool_name, source_agent, input_json, output_json, duration_ms, success, called_at FROM api_logs @@ -521,13 +590,28 @@ function listApiLogRows( LIMIT ?` ) .all(...tools, ...agentFilter.parameters, maxRows) - .map((row) => ({ ...row as unknown as Omit, source })); - })) + .map((row) => { + const logRow = { ...row as unknown as Omit, source }; + return { + ...logRow, + output_json: apiLogOutputWithCurrentTraceSummary(logRow, db) + }; + }); + const countRow = db + .prepare(`SELECT COUNT(*) AS count FROM api_logs WHERE tool_name IN (${placeholders}) ${agentFilter.sql}`) + .get(...tools, ...agentFilter.parameters) as { count: number }; + return { rows, total: nonNegativeInt(countRow.count, 0) }; + })); + return { + rows: sourceResults + .flatMap((result) => result.rows) .sort((a, b) => b.called_at.localeCompare(a.called_at) || b.id - a.id) - .slice(0, maxRows); + .slice(0, maxRows), + total: sourceResults.reduce((total, result) => total + result.total, 0) + }; } -function apiLogOutputWithCurrentTraceSummary(row: LocalApiLogRow): string { +function apiLogOutputWithCurrentTraceSummary(row: LocalApiLogRow, db: DatabaseSync): string { if (row.tool_name !== "memory_add") return row.output_json; try { @@ -543,10 +627,9 @@ function apiLogOutputWithCurrentTraceSummary(row: LocalApiLogRow): string { const memoryId = stringValue(role === "span" ? record.spanId : record.traceId) ?? stringValue(record.traceId); if (!memoryId) return detail; - const memory = withDb(row.source, (db) => { - if (!tableExists(db, "memories")) return undefined; - return db.prepare("SELECT * FROM memories WHERE id = ?").get(memoryId) as LocalMemoryRow | undefined; - }); + const memory = tableExists(db, "memories") + ? db.prepare("SELECT * FROM memories WHERE id = ?").get(memoryId) as LocalMemoryRow | undefined + : undefined; const value = memory ? role === "span" ? spanGoalFromParsed(parsedRow(memory)) @@ -564,29 +647,6 @@ function apiLogOutputWithCurrentTraceSummary(row: LocalApiLogRow): string { } } -/** - * Counts the Memory API logs in local SQLite data sources. - * - * @param sources the list of SQLite data sources. - * @param input the log filter conditions. - * @returns the total number of logs matching the filter conditions. - */ -function countApiLogRows(sources: readonly MemosSqliteSource[], input: MemoryApiLogsInput): number { - const tools = normalizeApiLogTools(input.tools); - const placeholders = tools.map(() => "?").join(", "); - const agentFilter = apiLogSourceAgentFilter(input); - return sources.reduce((total, source) => total + withDb(source, (db) => { - if (!tableExists(db, "api_logs")) { - return 0; - } - - const row = db - .prepare(`SELECT COUNT(*) AS count FROM api_logs WHERE tool_name IN (${placeholders}) ${agentFilter.sql}`) - .get(...tools, ...agentFilter.parameters) as { count: number }; - return nonNegativeInt(row.count, 0); - }), 0); -} - function apiLogSourceAgentFilter(input: MemoryApiLogsInput): { sql: string; parameters: string[] } { const sourceAgent = input.sourceAgent?.trim(); const excludedSourceAgents = uniqueStrings( @@ -1161,21 +1221,21 @@ function sourceLabelFromParsed(parsed: ParsedRow): string | undefined { } function sourceLabelFromSessionId(value: string | null): string | undefined { - const normalized = value?.trim().toLowerCase(); + const normalized = value?.trim().toLowerCase().replace(/[\s_:/\\]+/gu, "-"); if (!normalized) return undefined; if (normalized === "claude" || normalized.startsWith("claude-")) return "claude-code"; if (normalized === "open-code" || normalized.startsWith("open-code-")) return "opencode"; - for (const source of ["hermes", "openclaw", "codex", "cursor", "claude-code", "opencode", "workbuddy"]) { + for (const source of ["hermes", "openclaw", "codex", "cursor", "claude-code", "opencode", "workbuddy", "omp"]) { if (normalized === source || normalized.startsWith(`${source}-`)) return source; } return undefined; } function normalizedAgentSource(value: string | undefined): string | undefined { - const normalized = value?.trim().toLowerCase(); + const normalized = value?.trim().toLowerCase().replace(/[\s_:/\\]+/gu, "-"); if (normalized === "claude") return "claude-code"; if (normalized === "open-code") return "opencode"; - return ["hermes", "openclaw", "codex", "cursor", "claude-code", "opencode", "workbuddy"].includes(normalized ?? "") + return ["hermes", "openclaw", "codex", "cursor", "claude-code", "opencode", "workbuddy", "omp"].includes(normalized ?? "") ? normalized : undefined; } diff --git a/App/backend/src/adapters/outbound/memory-client/tests/http-memory-client.test.ts b/App/backend/src/adapters/outbound/memory-client/tests/http-memory-client.test.ts index b9cfae10e..6798b1d00 100644 --- a/App/backend/src/adapters/outbound/memory-client/tests/http-memory-client.test.ts +++ b/App/backend/src/adapters/outbound/memory-client/tests/http-memory-client.test.ts @@ -34,6 +34,13 @@ describe("HttpMemoryClient", () => { "/api/v1/panel/overview", "/api/v1/panel/analysis", "/api/v1/panel/context-pack", + "/api/v1/project-context/state", + "/api/v1/project-context/goals/propose", + "/api/v1/project-context/goals/:id/approve", + "/api/v1/project-context/goals/:id/reject", + "/api/v1/project-context/work-items", + "/api/v1/project-context/work-items/:id", + "/api/v1/project-context/focus", "/api/v1/panel/items", "/api/v1/panel/tasks", "/api/v1/panel/tasks/:id" @@ -144,6 +151,42 @@ describe("HttpMemoryClient", () => { source: "codex" }); }); + it("uses the exact methods, paths, bodies, query, and response schemas for project context", async () => { + const requests: Array<{ method: string; url: URL; body: unknown }> = []; + const baseUrl = await startServer(async (request, response) => { + const body = await readJson(request); + const url = new URL(request.url ?? "/", "http://localhost"); + requests.push({ method: request.method ?? "", url, body }); + if (url.pathname.endsWith("/state")) return sendJson(response, projectContextStateOutput()); + if (url.pathname.endsWith("/focus")) return sendJson(response, body && (body as { workItemId?: unknown }).workItemId === null ? null : projectWorkItemOutput()); + if (url.pathname.includes("work-items")) return sendJson(response, projectWorkItemOutput()); + return sendJson(response, projectGoalOutput(url.pathname.endsWith("/reject") ? "archived" : url.pathname.endsWith("/approve") ? "active" : "candidate")); + }); + const client = createHttpMemoryClient({ baseUrl, token: "memory-token", timeoutMs: 500, maxRetries: 0 }); + const namespace = projectNamespace(); + const mutation = projectMutation(); + await client.projectContextState(namespace); + await client.proposeProjectGoal({ ...mutation, title: "Task 4", summary: "", detail: "" }); + await client.approveProjectGoal("goal 1", mutation); + await client.rejectProjectGoal("goal 1", { ...mutation, requestId: "req-reject" }); + await client.createProjectWorkItem({ ...mutation, title: "Tests", summary: "", nextStep: "Fix" }); + await client.updateProjectWorkItem("work 1", { ...mutation, status: "active" }); + await expect(client.setProjectFocus({ ...mutation, workItemId: null })).resolves.toBeNull(); + expect(requests.map(({ method, url }) => `${method} ${url.pathname}`)).toEqual([ + "GET /api/v1/project-context/state", "POST /api/v1/project-context/goals/propose", "POST /api/v1/project-context/goals/goal%201/approve", + "POST /api/v1/project-context/goals/goal%201/reject", "POST /api/v1/project-context/work-items", "PATCH /api/v1/project-context/work-items/work%201", "PUT /api/v1/project-context/focus" + ]); + expect(JSON.parse(requests[0]!.url.searchParams.get("namespace")!)).toEqual(namespace); + expect(requests.slice(1).map(({ body }) => body)).toEqual([ + { ...mutation, title: "Task 4", summary: "", detail: "" }, mutation, { ...mutation, requestId: "req-reject" }, + { ...mutation, title: "Tests", summary: "", nextStep: "Fix" }, { ...mutation, status: "active" }, { ...mutation, workItemId: null } + ]); + }); + it("rejects schema-invalid project-context responses", async () => { + const baseUrl = await startServer(async (_request, response) => sendJson(response, { id: "missing-fields" })); + const client = createHttpMemoryClient({ baseUrl, token: "", timeoutMs: 500, maxRetries: 0 }); + await expect(client.proposeProjectGoal({ ...projectMutation(), title: "Task 4", summary: "", detail: "" })).rejects.toThrow(); + }); it("forwards memory log Agent filters to the Memory service", async () => { const requestUrls: URL[] = []; @@ -600,15 +643,27 @@ function now() { } function panelDays() { - return [ - "2026-05-23", - "2026-05-24", - "2026-05-25", - "2026-05-26", - "2026-05-27", - "2026-05-28", - "2026-05-29" - ].map((date) => ({ date, count: 0 })); + return ["2026-05-23", "2026-05-24", "2026-05-25", "2026-05-26", "2026-05-27", "2026-05-28", "2026-05-29"].map((date) => ({ date, count: 0 })); +} + +function projectNamespace() { + return { source: "codex", profileId: "default", userId: "user-4", projectId: "project-4" }; +} + +function projectMutation() { + return { namespace: projectNamespace(), source: "codex", adapterId: "adapter-4", requestId: "req-4", provenance: { sourceAgent: "codex", sourceMemoryIds: [], capturedAt: now(), adapterId: "adapter-4", requestId: "req-4" } }; +} + +function projectGoalOutput(status: "candidate" | "active" | "archived") { + return { id: "goal-1", namespaceId: "ns-1", userId: "user-4", projectId: "project-4", title: "Task 4", summary: "", detail: "", acceptanceCriteria: [], constraints: [], status, version: status === "active" ? 1 : 0, sourceMemoryIds: [], provenance: {}, createdAt: now(), updatedAt: now() }; +} + +function projectWorkItemOutput() { + return { id: "work-1", namespaceId: "ns-1", userId: "user-4", projectId: "project-4", title: "Tests", summary: "", nextStep: "Fix", acceptanceCriteria: [], constraints: [], status: "active", focused: true, sourceMemoryIds: [], provenance: {}, createdAt: now(), updatedAt: now() }; +} + +function projectContextStateOutput() { + return { namespaceId: "ns-1", activeGoal: null, goals: [], workItems: [], focusedWorkItem: null, facts: [] }; } function openSessionInput() { diff --git a/App/backend/src/adapters/outbound/memory-client/tests/memos-sqlite-memory-client.test.ts b/App/backend/src/adapters/outbound/memory-client/tests/memos-sqlite-memory-client.test.ts index 51e3e964e..4998cda58 100644 --- a/App/backend/src/adapters/outbound/memory-client/tests/memos-sqlite-memory-client.test.ts +++ b/App/backend/src/adapters/outbound/memory-client/tests/memos-sqlite-memory-client.test.ts @@ -6,6 +6,7 @@ import { DatabaseSync } from "node:sqlite"; import { getLoadablePath as getSqliteVecLoadablePath } from "sqlite-vec"; import { afterEach, describe, expect, it } from "vitest"; import { createMemosSqliteMemoryClient } from "../memos-sqlite-memory-client.js"; +import { MemoryDb } from "@memmy/memory"; const NOW = "2026-06-08T10:00:00.000Z"; @@ -18,7 +19,45 @@ afterEach(() => { } }); -describe("createMemosSqliteMemoryClient", () => { +describe("createMemosSqliteMemoryClient", { timeout: 10_000 }, () => { + it("delegates all project-context operations to the embedded Memory service", async () => { + const calls: Array<{ operation: string; input: unknown }> = []; + const namespace = { source: "codex", profileId: "default", userId: "user-4", projectId: "project-4" }; + const goal = projectGoalRecord(); + const work = projectWorkItemRecord(); + const memoryService = { + readProjectContext(input: unknown) { calls.push({ operation: "read", input }); return { namespaceId: "ns-1", activeGoal: null, goals: [], workItems: [], focusedWorkItem: null, facts: [] }; }, + proposeProjectGoal(input: unknown) { calls.push({ operation: "propose", input }); return goal; }, + approveProjectGoal(input: unknown) { calls.push({ operation: "approve", input }); return { ...goal, status: "active" as const, version: 1 }; }, + rejectProjectGoal(input: unknown) { calls.push({ operation: "reject", input }); return { ...goal, status: "archived" as const }; }, + createProjectWorkItem(input: unknown) { calls.push({ operation: "create", input }); return work; }, + updateProjectWorkItem(input: unknown) { calls.push({ operation: "update", input }); return { ...work, status: "active" as const }; }, + selectProjectWorkItem(input: unknown) { calls.push({ operation: "focus", input }); return (input as { workItemId: string | null }).workItemId ? { ...work, focused: true } : undefined; } + }; + const client = createMemosSqliteMemoryClient({ sources: [], memoryService }); + const mutation = projectMutation(namespace); + await client.projectContextState(namespace); + await client.proposeProjectGoal({ ...mutation, title: "Task 4", summary: "", detail: "" }); + await client.approveProjectGoal("goal-1", mutation); + await client.rejectProjectGoal("goal-1", mutation); + await client.createProjectWorkItem({ ...mutation, title: "Tests", summary: "", nextStep: "Fix" }); + await client.updateProjectWorkItem("work-1", { ...mutation, status: "active" }); + await expect(client.setProjectFocus({ ...mutation, workItemId: null })).resolves.toBeNull(); + expect(calls.map(({ operation }) => operation)).toEqual(["read", "propose", "approve", "reject", "create", "update", "focus"]); + }); + it("uses the packaged Memory service when no delegate is supplied", async () => { + tempDir = mkdtempSync(join(tmpdir(), "memmy-project-context-")); + const dbPath = join(tempDir, "memory.sqlite"); + const db = new MemoryDb({ path: dbPath }); + db.close(); + const namespace = { source: "codex", profileId: "default", userId: "user-4", projectId: "project-4" }; + const client = createMemosSqliteMemoryClient({ sources: [{ id: "memmy-memory", label: "memmy", dbPath }] }); + const input = projectMutation(namespace); + const goal = await client.proposeProjectGoal({ ...input, title: "Task 4", summary: "API", detail: "Authoritative" }); + const state = await client.projectContextState(namespace); + expect(state.goals.map((item) => item.id)).toEqual([goal.id]); + expect(state.goals[0]?.title).toBe("Task 4"); + }); it("preserves Span memory kinds in panel responses", async () => { const dbPath = createMemoryDatabase({ id: "span_sqlite_1", @@ -118,6 +157,29 @@ describe("createMemosSqliteMemoryClient", () => { expect(detail.item.metrics).toEqual({ value: 0.42, alpha: 0.8, reflectionDone: true }); }); + it("derives OMP source from the session id when the row agent is the default", async () => { + const dbPath = createMemoryDatabase({ + id: "trace_omp_1", + sessionId: "omp::session-20260608", + agentId: "codex", + tagsJson: JSON.stringify(["trace"]), + infoJson: "{}", + propertiesJson: JSON.stringify({ internal_info: { source: "turn.complete" } }) + }); + const client = createMemosSqliteMemoryClient({ + sources: [{ id: "memmy-memory", label: "memmy", dbPath }], + now: () => NOW + }); + + const list = await client.panelItems({ layer: "L1", page: 1 }); + expect(list.items[0]?.tags).toEqual(["omp", "trace"]); + expect(list.items[0]?.metadata?.source).toBe("omp"); + await expect(client.panelItems({ layer: "L1", sourceAgent: "omp", page: 1 })) + .resolves.toMatchObject({ total: 1, items: [{ id: expect.stringContaining("trace_omp_1") }] }); + await expect(client.panelItems({ layer: "L1", sourceAgent: "codex", page: 1 })) + .resolves.toMatchObject({ total: 0, items: [] }); + }); + it("filters custom L1 panel item sources as other", async () => { const dbPath = createMemoryDatabase({ id: "trace_other_1", @@ -461,7 +523,8 @@ describe("createMemosSqliteMemoryClient", () => { memoryValue: "Delete this exact SQLite memory.", tagsJson: JSON.stringify(["trace", "codex", "delete-me"]), infoJson: JSON.stringify({ source: "codex" }), - propertiesJson: JSON.stringify({ internal_info: { source: "codex", memory_kind: "trace" } }) + propertiesJson: JSON.stringify({ internal_info: { source: "codex", memory_kind: "trace" } }), + withVector: true }); const client = createMemosSqliteMemoryClient({ sources: [{ id: "memmy-memory", label: "memmy", dbPath }], @@ -519,6 +582,18 @@ describe("createMemosSqliteMemoryClient", () => { }); }); +function projectMutation(namespace: { source: string; profileId: string; userId: string; projectId: string }) { + return { namespace, source: "codex", adapterId: "adapter-4", requestId: "req-4", provenance: { sourceAgent: "codex", sourceMemoryIds: [], capturedAt: NOW } }; +} + +function projectGoalRecord() { + return { id: "goal-1", namespaceId: "ns-1", userId: "user-4", projectId: "project-4", title: "Task 4", summary: "", detail: "", acceptanceCriteria: [], constraints: [], status: "candidate" as const, version: 0, sourceMemoryIds: [], provenance: {}, createdAt: NOW, updatedAt: NOW }; +} + +function projectWorkItemRecord() { + return { id: "work-1", namespaceId: "ns-1", userId: "user-4", projectId: "project-4", title: "Tests", summary: "", nextStep: "Fix", acceptanceCriteria: [], constraints: [], status: "pending" as const, focused: false, sourceMemoryIds: [], provenance: {}, createdAt: NOW, updatedAt: NOW }; +} + function createMemoryDatabase(row: { id: string; sessionId: string | null; @@ -536,11 +611,14 @@ function createMemoryDatabase(row: { id: string; toolCalls: Array>; }; + withVector?: boolean; }): string { tempDir = mkdtempSync(join(tmpdir(), "memmy-sqlite-client-")); const dbPath = join(tempDir, "memory.sqlite"); - const db = new DatabaseSync(dbPath, { allowExtension: true }); - db.loadExtension(getSqliteVecLoadablePath()); + const db = new DatabaseSync(dbPath, { allowExtension: row.withVector === true }); + if (row.withVector) { + db.loadExtension(getSqliteVecLoadablePath()); + } db.exec(` CREATE TABLE memories ( id TEXT PRIMARY KEY, @@ -596,26 +674,28 @@ function createMemoryDatabase(row: { NOW, null ); - db.exec(` - CREATE TABLE memory_vector_entries ( - id INTEGER PRIMARY KEY, - memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE, - vector_field TEXT NOT NULL, - embedding_model TEXT, - embedding_provider TEXT, - embedding_dim INTEGER NOT NULL, - updated_at TEXT NOT NULL, - UNIQUE (memory_id, vector_field) - ); - CREATE VIRTUAL TABLE memory_vec_3 USING vec0(embedding float[3] distance_metric=cosine); - `); - db.prepare(` - INSERT INTO memory_vector_entries ( - id, memory_id, vector_field, embedding_model, embedding_provider, embedding_dim, updated_at - ) VALUES (1, ?, 'vec_summary', 'test', 'openai_compatible', 3, ?) - `).run(row.id, NOW); - db.prepare(`INSERT INTO memory_vec_3 (rowid, embedding) VALUES (?, ?)`) - .run(1n, Buffer.from(new Float32Array([1, 0, 0]).buffer)); + if (row.withVector) { + db.exec(` + CREATE TABLE memory_vector_entries ( + id INTEGER PRIMARY KEY, + memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE, + vector_field TEXT NOT NULL, + embedding_model TEXT, + embedding_provider TEXT, + embedding_dim INTEGER NOT NULL, + updated_at TEXT NOT NULL, + UNIQUE (memory_id, vector_field) + ); + CREATE VIRTUAL TABLE memory_vec_3 USING vec0(embedding float[3] distance_metric=cosine); + `); + db.prepare(` + INSERT INTO memory_vector_entries ( + id, memory_id, vector_field, embedding_model, embedding_provider, embedding_dim, updated_at + ) VALUES (1, ?, 'vec_summary', 'test', 'openai_compatible', 3, ?) + `).run(row.id, NOW); + db.prepare(`INSERT INTO memory_vec_3 (rowid, embedding) VALUES (?, ?)`) + .run(1n, Buffer.from(new Float32Array([1, 0, 0]).buffer)); + } if (row.rawTurn) { db.exec(` CREATE TABLE raw_turns ( diff --git a/App/backend/src/adapters/outbound/memory-client/types.ts b/App/backend/src/adapters/outbound/memory-client/types.ts index a0617fc7a..e292b3d2a 100644 --- a/App/backend/src/adapters/outbound/memory-client/types.ts +++ b/App/backend/src/adapters/outbound/memory-client/types.ts @@ -23,6 +23,15 @@ import type { PanelItemsOutput, PanelOverviewOutput, ProjectContextPackOutput, + ProjectContextFocusInput, + ProjectContextGoalDecisionInput, + ProjectContextProposeGoalInput, + ProjectContextReadState, + ProjectContextWorkItemCreateInput, + ProjectContextWorkItemUpdateInput, + ProjectGoalRecord, + ProjectWorkItemRecord, + RuntimeNamespace, PanelTasksInput, PanelTasksOutput, OpenSessionInput, @@ -68,6 +77,13 @@ export interface MemoryClient { panelOverview(): Promise; panelAnalysis(): Promise; projectContextPack(projectId: string): Promise; + projectContextState(namespace: RuntimeNamespace): Promise; + proposeProjectGoal(input: ProjectContextProposeGoalInput): Promise; + approveProjectGoal(goalId: string, input: ProjectContextGoalDecisionInput): Promise; + rejectProjectGoal(goalId: string, input: ProjectContextGoalDecisionInput): Promise; + createProjectWorkItem(input: ProjectContextWorkItemCreateInput): Promise; + updateProjectWorkItem(workItemId: string, input: ProjectContextWorkItemUpdateInput): Promise; + setProjectFocus(input: ProjectContextFocusInput): Promise; panelItems(input: PanelItemsInput): Promise; panelTasks(input: PanelTasksInput): Promise; deletePanelTask(taskId: string): Promise; diff --git a/App/backend/src/services/panel-service.ts b/App/backend/src/services/panel-service.ts index 91b460592..6b065a6b6 100644 --- a/App/backend/src/services/panel-service.ts +++ b/App/backend/src/services/panel-service.ts @@ -9,7 +9,16 @@ import type { MemoryApiLogsInput, MemoryApiLogsOutput, PanelOverviewOutput, - ProjectContextPackOutput + ProjectContextPackOutput, + ProjectContextFocusInput, + ProjectContextGoalDecisionInput, + ProjectContextProposeGoalInput, + ProjectContextReadState, + ProjectContextWorkItemCreateInput, + ProjectContextWorkItemUpdateInput, + ProjectGoalRecord, + ProjectWorkItemRecord, + RuntimeNamespace, } from "@memmy/local-api-contracts"; import { MemoryLayerError } from "../adapters/outbound/memory-client/index.js"; import type { MemoryClient } from "../adapters/outbound/memory-client/index.js"; @@ -20,6 +29,13 @@ export interface PanelService { overview(ctx: RuntimeContext): Promise; analysis(ctx: RuntimeContext): Promise; contextPack(projectId: string, ctx: RuntimeContext): Promise; + projectContextState(namespace: RuntimeNamespace, ctx: RuntimeContext): Promise; + proposeProjectGoal(input: ProjectContextProposeGoalInput, ctx: RuntimeContext): Promise; + approveProjectGoal(id: string, input: ProjectContextGoalDecisionInput, ctx: RuntimeContext): Promise; + rejectProjectGoal(id: string, input: ProjectContextGoalDecisionInput, ctx: RuntimeContext): Promise; + createProjectWorkItem(input: ProjectContextWorkItemCreateInput, ctx: RuntimeContext): Promise; + updateProjectWorkItem(id: string, input: ProjectContextWorkItemUpdateInput, ctx: RuntimeContext): Promise; + setProjectFocus(input: ProjectContextFocusInput, ctx: RuntimeContext): Promise; items(input: PanelItemsInput, ctx: RuntimeContext): Promise; tasks(input: PanelTasksInput, ctx: RuntimeContext): Promise; deleteTask(id: string, ctx: RuntimeContext): Promise; @@ -41,6 +57,34 @@ export function createPanelService(deps: { memoryClient: MemoryClient }): PanelS return deps.memoryClient.projectContextPack(projectId); }, + async projectContextState(namespace, _ctx) { + return deps.memoryClient.projectContextState(namespace); + }, + + async proposeProjectGoal(input, ctx) { + return deps.memoryClient.proposeProjectGoal(withRuntimeProvenance(input, ctx)); + }, + + async approveProjectGoal(id, input, ctx) { + return deps.memoryClient.approveProjectGoal(id, withRuntimeProvenance(input, ctx)); + }, + + async rejectProjectGoal(id, input, ctx) { + return deps.memoryClient.rejectProjectGoal(id, withRuntimeProvenance(input, ctx)); + }, + + async createProjectWorkItem(input, ctx) { + return deps.memoryClient.createProjectWorkItem(withRuntimeProvenance(input, ctx)); + }, + + async updateProjectWorkItem(id, input, ctx) { + return deps.memoryClient.updateProjectWorkItem(id, withRuntimeProvenance(input, ctx)); + }, + + async setProjectFocus(input, ctx) { + return deps.memoryClient.setProjectFocus(withRuntimeProvenance(input, ctx)); + }, + async items(input, _ctx) { return deps.memoryClient.panelItems(input); }, @@ -81,3 +125,12 @@ function isMissingMemoryLogsRoute(error: unknown): boolean { error.message.toLowerCase().includes("logs") ); } + +function withRuntimeProvenance(input: T, ctx: RuntimeContext): T { + return { + ...input, + adapterId: ctx.adapterId, + requestId: ctx.requestId ?? input.requestId, + provenance: { ...input.provenance, adapterId: ctx.adapterId, requestId: ctx.requestId ?? input.requestId } + } as T; +} diff --git a/package-lock.json b/package-lock.json index 58b7dadc6..39f04a4bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "version": "0.0.0", "dependencies": { "@memmy/local-api-contracts": "0.0.0", + "@memmy/memory": "1.0.4", "@modelcontextprotocol/sdk": "^1.29.0", "dotenv": "^16.6.1", "fastify": "^5.8.5", From b7c274ec93f6d93681620e35172b4f830c0c1689 Mon Sep 17 00:00:00 2001 From: bluewater_cg Date: Mon, 10 Aug 2026 07:00:32 -0700 Subject: [PATCH 17/23] feat(agent): preserve project context turn provenance --- App/memmy-agent/src/memmy-memory/hook.ts | 2 ++ App/memmy-agent/src/memmy-memory/types.ts | 1 + App/memmy-agent/tests/memmy-memory/hook.test.ts | 6 ++++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/App/memmy-agent/src/memmy-memory/hook.ts b/App/memmy-agent/src/memmy-memory/hook.ts index 53ea98930..9a998c1d0 100644 --- a/App/memmy-agent/src/memmy-memory/hook.ts +++ b/App/memmy-agent/src/memmy-memory/hook.ts @@ -149,6 +149,7 @@ export class MemmyMemoryHook extends AgentHook implements MemmyMemoryToolRuntime sessionId, query: userText || "(conversation continued)", })); + turn.contextPacketId = stringOrUndefined(response?.contextPacketId); turn.episodeId = stringOrUndefined(response?.episodeId); turn.sourceMemoryIds = arrayOfStrings(response?.sourceMemoryIds); turn.hasInjectedContext = hasInjectedContextValue(response?.injectedContext); @@ -227,6 +228,7 @@ export class MemmyMemoryHook extends AgentHook implements MemmyMemoryToolRuntime const response = await this.client.completeTurn(turn.turnId, compact({ ...this.requestEnvelope(sessionKey, ctx), requestId: completeRequestId(turn.turnId, status, turn.userText, answer), + contextPacketId: turn.contextPacketId, sessionId: turn.sessionId, episodeId: turn.episodeId, query: turn.userText, diff --git a/App/memmy-agent/src/memmy-memory/types.ts b/App/memmy-agent/src/memmy-memory/types.ts index 6d4ebcd29..ff32fa8c6 100644 --- a/App/memmy-agent/src/memmy-memory/types.ts +++ b/App/memmy-agent/src/memmy-memory/types.ts @@ -57,6 +57,7 @@ export type MemmyMemoryTurnState = { turnId: string; userText: string; messageStartIndex: number; + contextPacketId?: string; episodeId?: string; sourceMemoryIds?: string[]; rawTurnId?: string; diff --git a/App/memmy-agent/tests/memmy-memory/hook.test.ts b/App/memmy-agent/tests/memmy-memory/hook.test.ts index b95261f5c..408dc2c98 100644 --- a/App/memmy-agent/tests/memmy-memory/hook.test.ts +++ b/App/memmy-agent/tests/memmy-memory/hook.test.ts @@ -14,8 +14,9 @@ function fakeClient() { turnId, sessionId: body.sessionId, episodeId: "ep-1", + contextPacketId: "ctx-packet-1", sourceMemoryIds: ["trace-source"], - injectedContext: { markdown: "Relevant prior memory." }, + injectedContext: { markdown: "## Confirmed project context\nGoal: Ship authoritative context.\n\n## Relevant prior memory\nRelevant prior memory." }, })), completeTurn: vi.fn(async () => ({ rawTurnId: "raw-1", l1MemoryId: "l1-1" })), closeSession: vi.fn(async (sessionId: string) => ({ ok: true, sessionId, status: "closed" })), @@ -98,7 +99,7 @@ describe("MemmyMemoryHook", () => { expect(messages[0].content).toBe("System prompt"); const userBlocks = messages[1].content as unknown as Array<{ type: string; text: string }>; expect(userBlocks.map((block) => block.text)).toEqual([ - '\nRelevant prior memory.\n', + '\n## Confirmed project context\nGoal: Ship authoritative context.\n\n## Relevant prior memory\nRelevant prior memory.\n', "", "Please continue\n\n", "", @@ -116,6 +117,7 @@ describe("MemmyMemoryHook", () => { expect(completeBody).toMatchObject({ sessionId: "session-generated-1", episodeId: "ep-1", + contextPacketId: "ctx-packet-1", query: "Please continue", answer: "Done", sourceMemoryIds: ["trace-source"], From 64d559a13af7e8865bce61f0c8f91d3ab437d619 Mon Sep 17 00:00:00 2001 From: bluewater_cg Date: Mon, 10 Aug 2026 07:00:52 -0700 Subject: [PATCH 18/23] feat(desktop): add project context governance --- .../desktop/src/api/memory-runtime-client.ts | 67 +++++++++++ .../api/tests/memory-runtime-client.test.ts | 35 +++++- App/frontend/desktop/src/i18n/messages.ts | 26 +++++ .../memory/tests/memory-runtime-fixtures.ts | 21 ++++ .../src/pages/project-context-pack-dialog.tsx | 109 +++++++++++++++++- ...ome-page-context-pack.interaction.test.tsx | 14 ++- ...t-context-pack-dialog.interaction.test.tsx | 42 ++++++- App/frontend/desktop/src/styles.css | 33 ++++++ 8 files changed, 338 insertions(+), 9 deletions(-) diff --git a/App/frontend/desktop/src/api/memory-runtime-client.ts b/App/frontend/desktop/src/api/memory-runtime-client.ts index 915d89ce3..62b6dba4d 100644 --- a/App/frontend/desktop/src/api/memory-runtime-client.ts +++ b/App/frontend/desktop/src/api/memory-runtime-client.ts @@ -23,6 +23,15 @@ import { PanelItemsOutputSchema, PanelOverviewOutputSchema, ProjectContextPackOutputSchema, + ProjectContextFocusInputSchema, + ProjectContextGoalDecisionInputSchema, + ProjectContextProposeGoalInputSchema, + ProjectContextReadStateSchema, + ProjectContextWorkItemCreateInputSchema, + ProjectContextWorkItemUpdateInputSchema, + ProjectGoalRecordSchema, + ProjectWorkItemRecordSchema, + RuntimeNamespaceSchema, PanelTasksInputSchema, PanelTasksOutputSchema, SearchInputSchema, @@ -55,6 +64,15 @@ import { type PanelItemsOutput, type PanelOverviewOutput, type ProjectContextPackOutput, + type ProjectContextFocusInput, + type ProjectContextGoalDecisionInput, + type ProjectContextProposeGoalInput, + type ProjectContextReadState, + type ProjectContextWorkItemCreateInput, + type ProjectContextWorkItemUpdateInput, + type ProjectGoalRecord, + type ProjectWorkItemRecord, + type RuntimeNamespace, type PanelTasksInput, type PanelTasksOutput, type SearchInput, @@ -87,6 +105,13 @@ export const MEMORY_RUNTIME_ENDPOINTS = [ "GET /api/v1/panel/overview", "GET /api/v1/panel/analysis", "GET /api/v1/panel/context-pack", + "GET /api/v1/project-context/state", + "POST /api/v1/project-context/goals/propose", + "POST /api/v1/project-context/goals/:id/approve", + "POST /api/v1/project-context/goals/:id/reject", + "POST /api/v1/project-context/work-items", + "PATCH /api/v1/project-context/work-items/:id", + "PUT /api/v1/project-context/focus", "GET /api/v1/panel/items", "GET /api/v1/panel/tasks", "DELETE /api/v1/panel/tasks/:id" @@ -111,6 +136,13 @@ export interface MemoryRuntimeClient { getPanelOverview(): Promise; getPanelAnalysis(): Promise; getProjectContextPack(projectId: string): Promise; + getProjectContextState(namespace: RuntimeNamespace): Promise; + proposeProjectGoal(input: ProjectContextProposeGoalInput): Promise; + approveProjectGoal(id: string, input: ProjectContextGoalDecisionInput): Promise; + rejectProjectGoal(id: string, input: ProjectContextGoalDecisionInput): Promise; + createProjectWorkItem(input: ProjectContextWorkItemCreateInput): Promise; + updateProjectWorkItem(id: string, input: ProjectContextWorkItemUpdateInput): Promise; + setProjectFocus(input: ProjectContextFocusInput): Promise; listPanelItems(input: PanelItemsInput): Promise; listPanelTasks(input: PanelTasksInput): Promise; deletePanelTask(id: string): Promise; @@ -247,6 +279,34 @@ export function createHttpMemoryRuntimeClient(config: RuntimeConfig): MemoryRunt schema: ProjectContextPackOutputSchema }); }, + async getProjectContextState(namespace) { + const parsed = RuntimeNamespaceSchema.parse(namespace); + return requestJson({ config, path: withQuery("/api/v1/project-context/state", { namespace: JSON.stringify(parsed) }), schema: ProjectContextReadStateSchema }); + }, + + async proposeProjectGoal(input) { + return requestJson({ config, path: "/api/v1/project-context/goals/propose", schema: ProjectGoalRecordSchema, body: ProjectContextProposeGoalInputSchema.parse(input) }); + }, + + async approveProjectGoal(id, input) { + return requestJson({ config, path: `/api/v1/project-context/goals/${encodeURIComponent(id)}/approve`, schema: ProjectGoalRecordSchema, body: ProjectContextGoalDecisionInputSchema.parse(input) }); + }, + + async rejectProjectGoal(id, input) { + return requestJson({ config, path: `/api/v1/project-context/goals/${encodeURIComponent(id)}/reject`, schema: ProjectGoalRecordSchema, body: ProjectContextGoalDecisionInputSchema.parse(input) }); + }, + + async createProjectWorkItem(input) { + return requestJson({ config, path: "/api/v1/project-context/work-items", schema: ProjectWorkItemRecordSchema, body: ProjectContextWorkItemCreateInputSchema.parse(input) }); + }, + + async updateProjectWorkItem(id, input) { + return requestJson({ config, path: `/api/v1/project-context/work-items/${encodeURIComponent(id)}`, schema: ProjectWorkItemRecordSchema, body: ProjectContextWorkItemUpdateInputSchema.parse(input), init: { method: "PATCH" } }); + }, + + async setProjectFocus(input) { + return requestJson({ config, path: "/api/v1/project-context/focus", schema: ProjectWorkItemRecordSchema.nullable(), body: ProjectContextFocusInputSchema.parse(input), init: { method: "PUT" } }); + }, async listPanelItems(input) { return requestJson({ config, path: withQuery("/api/v1/panel/items", PanelItemsInputSchema.parse(input)), schema: PanelItemsOutputSchema }); @@ -348,6 +408,13 @@ export function createUnavailableMemoryRuntimeClient(): MemoryRuntimeClient { async getProjectContextPack() { throw unavailable(); }, + async getProjectContextState() { throw unavailable(); }, + async proposeProjectGoal() { throw unavailable(); }, + async approveProjectGoal() { throw unavailable(); }, + async rejectProjectGoal() { throw unavailable(); }, + async createProjectWorkItem() { throw unavailable(); }, + async updateProjectWorkItem() { throw unavailable(); }, + async setProjectFocus() { throw unavailable(); }, async listPanelItems() { throw unavailable(); }, diff --git a/App/frontend/desktop/src/api/tests/memory-runtime-client.test.ts b/App/frontend/desktop/src/api/tests/memory-runtime-client.test.ts index ea602ea3c..13a493d20 100644 --- a/App/frontend/desktop/src/api/tests/memory-runtime-client.test.ts +++ b/App/frontend/desktop/src/api/tests/memory-runtime-client.test.ts @@ -12,7 +12,7 @@ describe("memory runtime client", () => { }); it("declares the memory runtime endpoints exposed under /api/v1", () => { - expect(MEMORY_RUNTIME_ENDPOINTS).toHaveLength(21); + expect(MEMORY_RUNTIME_ENDPOINTS).toHaveLength(28); expect(MEMORY_RUNTIME_ENDPOINTS).toEqual([ "GET /api/v1/health", "POST /api/v1/admin/reload-config", @@ -32,6 +32,13 @@ describe("memory runtime client", () => { "GET /api/v1/panel/overview", "GET /api/v1/panel/analysis", "GET /api/v1/panel/context-pack", + "GET /api/v1/project-context/state", + "POST /api/v1/project-context/goals/propose", + "POST /api/v1/project-context/goals/:id/approve", + "POST /api/v1/project-context/goals/:id/reject", + "POST /api/v1/project-context/work-items", + "PATCH /api/v1/project-context/work-items/:id", + "PUT /api/v1/project-context/focus", "GET /api/v1/panel/items", "GET /api/v1/panel/tasks", "DELETE /api/v1/panel/tasks/:id" @@ -210,6 +217,32 @@ describe("memory runtime client", () => { expect(url.searchParams.get("projectId")).toBe("project-1"); }); + it("reads and mutates authoritative project context through typed routes", async () => { + const fetchMock = vi.fn(async (input: URL | RequestInfo, _init?: RequestInit) => { + const path = (input as URL).pathname; + const body = path.endsWith("/state") + ? { namespaceId: "namespace-1", activeGoal: null, goals: [], workItems: [], focusedWorkItem: null, facts: [] } + : path.endsWith("/focus") + ? null + : { id: "goal-1", namespaceId: "namespace-1", userId: "user-1", projectId: "project-1", title: "Goal", summary: "", detail: "", acceptanceCriteria: [], constraints: [], status: "active", version: 1, sourceMemoryIds: [], provenance: {}, createdAt: "2026-08-08T12:00:00.000Z", updatedAt: "2026-08-08T12:00:00.000Z" }; + return new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } }); + }); + vi.stubGlobal("fetch", fetchMock); + const client = createHttpMemoryRuntimeClient(runtimeConfig); + const namespace = { source: "desktop", profileId: "default", projectId: "project-1" }; + const mutation = { namespace, source: "desktop", adapterId: "memmy-desktop", requestId: "request-1", provenance: { sourceAgent: "memmy-desktop", sourceMemoryIds: [], capturedAt: "2026-08-08T12:00:00.000Z", projectId: "project-1" } }; + + await client.getProjectContextState(namespace); + await client.approveProjectGoal("goal/1", mutation); + await client.setProjectFocus({ ...mutation, workItemId: null }); + + expect((fetchMock.mock.calls[0]?.[0] as URL).pathname).toBe("/api/v1/project-context/state"); + expect((fetchMock.mock.calls[0]?.[0] as URL).searchParams.get("namespace")).toBe(JSON.stringify(namespace)); + expect((fetchMock.mock.calls[1]?.[0] as URL).pathname).toBe("/api/v1/project-context/goals/goal%2F1/approve"); + expect(fetchMock.mock.calls[1]?.[1]).toEqual(expect.objectContaining({ body: JSON.stringify(mutation) })); + expect((fetchMock.mock.calls[2]?.[0] as URL).pathname).toBe("/api/v1/project-context/focus"); + }); + it("passes a memory detail abort signal through to fetch", async () => { const fetchMock = vi.fn(async (_input: URL | RequestInfo, _init?: RequestInit) => new Response(JSON.stringify({ item: { diff --git a/App/frontend/desktop/src/i18n/messages.ts b/App/frontend/desktop/src/i18n/messages.ts index 4d0bc99de..d363ee89b 100644 --- a/App/frontend/desktop/src/i18n/messages.ts +++ b/App/frontend/desktop/src/i18n/messages.ts @@ -487,6 +487,19 @@ export const zhCNMessages = { "home.contextPack.architectureFacts": "架构事实", "home.contextPack.recentTasks": "最近任务", "home.contextPack.userPreferences": "用户偏好", + "home.contextPack.governance.title": "项目治理", + "home.contextPack.governance.loading": "正在读取确认状态...", + "home.contextPack.governance.error": "项目治理状态暂时无法读取", + "home.contextPack.governance.mutationError": "操作未完成,当前状态已保留,请重试。", + "home.contextPack.governance.currentGoal": "当前目标", + "home.contextPack.governance.currentFocus": "当前焦点", + "home.contextPack.governance.none": "未设置", + "home.contextPack.governance.candidate": "候选目标", + "home.contextPack.governance.workItem": "工作项", + "home.contextPack.governance.approve": "批准目标", + "home.contextPack.governance.reject": "拒绝", + "home.contextPack.governance.setFocus": "设为焦点", + "home.contextPack.governance.clearFocus": "取消焦点", "home.contextPack.detail.title": "记忆详情", "home.contextPack.detail.back": "返回上下文包", "home.contextPack.detail.loading": "正在加载记忆详情...", @@ -1927,6 +1940,19 @@ export const enUSMessages: Record = { "home.contextPack.architectureFacts": "Architecture facts", "home.contextPack.recentTasks": "Recent tasks", "home.contextPack.userPreferences": "User preferences", + "home.contextPack.governance.title": "Project governance", + "home.contextPack.governance.loading": "Loading confirmed project state...", + "home.contextPack.governance.error": "Project governance is temporarily unavailable", + "home.contextPack.governance.mutationError": "The action did not complete. Existing state was preserved; try again.", + "home.contextPack.governance.currentGoal": "Current goal", + "home.contextPack.governance.currentFocus": "Current focus", + "home.contextPack.governance.none": "Not set", + "home.contextPack.governance.candidate": "Candidate goal", + "home.contextPack.governance.workItem": "Work item", + "home.contextPack.governance.approve": "Approve goal", + "home.contextPack.governance.reject": "Reject", + "home.contextPack.governance.setFocus": "Set focus", + "home.contextPack.governance.clearFocus": "Clear focus", "home.contextPack.detail.title": "Memory detail", "home.contextPack.detail.back": "Back to context pack", "home.contextPack.detail.loading": "Loading memory detail...", diff --git a/App/frontend/desktop/src/pages/memory/tests/memory-runtime-fixtures.ts b/App/frontend/desktop/src/pages/memory/tests/memory-runtime-fixtures.ts index 544cf3abb..709ac3c1a 100644 --- a/App/frontend/desktop/src/pages/memory/tests/memory-runtime-fixtures.ts +++ b/App/frontend/desktop/src/pages/memory/tests/memory-runtime-fixtures.ts @@ -542,6 +542,27 @@ export function createMockMemoryRuntimeClient(): MemoryRuntimeClient { generatedAt: now }; }, + async getProjectContextState() { + return { namespaceId: "fixture-namespace", activeGoal: null, goals: [], workItems: [], focusedWorkItem: null, facts: [] }; + }, + async proposeProjectGoal(input) { + return { id: "fixture-goal", namespaceId: "fixture-namespace", userId: "fixture-user", projectId: input.namespace.projectId, title: input.title, summary: input.summary, detail: input.detail, acceptanceCriteria: input.acceptanceCriteria ?? [], constraints: input.constraints ?? [], status: "candidate" as const, version: 0, sourceMemoryIds: input.sourceMemoryIds ?? [], provenance: input.provenance, createdAt: now, updatedAt: now }; + }, + async approveProjectGoal(id, input) { + return { id, namespaceId: "fixture-namespace", userId: "fixture-user", projectId: input.namespace.projectId, title: "Fixture goal", summary: "", detail: "", acceptanceCriteria: [], constraints: [], status: "active" as const, version: 1, sourceMemoryIds: [], provenance: input.provenance, createdAt: now, updatedAt: now }; + }, + async rejectProjectGoal(id, input) { + return { id, namespaceId: "fixture-namespace", userId: "fixture-user", projectId: input.namespace.projectId, title: "Fixture goal", summary: "", detail: "", acceptanceCriteria: [], constraints: [], status: "archived" as const, version: 1, sourceMemoryIds: [], provenance: input.provenance, createdAt: now, updatedAt: now }; + }, + async createProjectWorkItem(input) { + return { id: "fixture-work", namespaceId: "fixture-namespace", userId: "fixture-user", projectId: input.namespace.projectId, goalId: input.goalId, title: input.title, summary: input.summary, nextStep: input.nextStep, acceptanceCriteria: input.acceptanceCriteria ?? [], constraints: input.constraints ?? [], status: input.status ?? "pending", focused: false, sourceMemoryIds: input.sourceMemoryIds ?? [], provenance: input.provenance, createdAt: now, updatedAt: now }; + }, + async updateProjectWorkItem(id, input) { + return { id, namespaceId: "fixture-namespace", userId: "fixture-user", projectId: input.namespace.projectId, goalId: input.goalId ?? undefined, title: input.title ?? "Fixture work", summary: input.summary ?? "", nextStep: input.nextStep ?? "", acceptanceCriteria: input.acceptanceCriteria ?? [], constraints: input.constraints ?? [], status: input.status ?? "pending", focused: false, sourceMemoryIds: input.sourceMemoryIds ?? [], provenance: input.provenance, createdAt: now, updatedAt: now }; + }, + async setProjectFocus() { + return null; + }, async listPanelItems(input): Promise { return filterMemoryItems(input); }, diff --git a/App/frontend/desktop/src/pages/project-context-pack-dialog.tsx b/App/frontend/desktop/src/pages/project-context-pack-dialog.tsx index 4ce052726..f8bd0f4f6 100644 --- a/App/frontend/desktop/src/pages/project-context-pack-dialog.tsx +++ b/App/frontend/desktop/src/pages/project-context-pack-dialog.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, type ReactNode } from "react"; -import type { GetMemoryOutput, MemoryHistoryOutput, MemoryListItem, ProjectContextPackOutput } from "@memmy/local-api-contracts"; -import { ArrowLeft, BookOpen, Check, Copy, Network, RefreshCw, X } from "lucide-react"; +import type { GetMemoryOutput, MemoryHistoryOutput, MemoryListItem, ProjectContextGoalDecisionInput, ProjectContextPackOutput, ProjectContextReadState, ProjectGoalRecord, ProjectWorkItemRecord, RuntimeNamespace } from "@memmy/local-api-contracts"; +import { ArrowLeft, BookOpen, Check, Copy, Network, RefreshCw, Target, X } from "lucide-react"; import type { MemoryRuntimeClient } from "../api/memory-runtime-client.js"; import { Modal } from "../components/modal.js"; import type { MessageKey } from "../i18n/messages.js"; @@ -28,6 +28,11 @@ type RestoreState = | { status: "restoring"; targetVersion: number } | { status: "success"; targetVersion: number } | { status: "error"; targetVersion: number }; +type GovernanceState = + | { status: "loading"; context: null; error: false } + | { status: "ready"; context: ProjectContextReadState; error: boolean } + | { status: "error"; context: null; error: true }; + type SelectedMemory = { projectId: string; memoryId: string }; @@ -43,7 +48,7 @@ export function ProjectContextPackDialog(props: { open: boolean; projectId: string; projectName: string; - client: Pick | null; + client: Pick | null; t: Translate; onClose: () => void; }) { @@ -57,6 +62,9 @@ export function ProjectContextPackDialog(props: { const [historyState, setHistoryState] = useState(null); const [pendingRestoreVersion, setPendingRestoreVersion] = useState(null); const [restoreState, setRestoreState] = useState({ status: "idle" }); + const [governanceRequestKey, setGovernanceRequestKey] = useState(0); + const [governanceState, setGovernanceState] = useState({ status: "loading", context: null, error: false }); + const [pendingGovernanceAction, setPendingGovernanceAction] = useState(null); useEffect(() => { if (!props.open || !props.client) return undefined; @@ -72,6 +80,17 @@ export function ProjectContextPackDialog(props: { }); return () => { active = false; }; }, [props.client, props.open, props.projectId, requestKey]); + const namespace = projectNamespace(props.projectId); + + useEffect(() => { + if (!props.open || !props.client) return undefined; + let active = true; + setGovernanceState({ status: "loading", context: null, error: false }); + void props.client.getProjectContextState(namespace) + .then((context) => { if (active) setGovernanceState({ status: "ready", context, error: false }); }) + .catch(() => { if (active) setGovernanceState({ status: "error", context: null, error: true }); }); + return () => { active = false; }; + }, [props.client, props.open, props.projectId, governanceRequestKey]); const selectedMemoryId = selectedMemory?.projectId === props.projectId ? selectedMemory.memoryId @@ -155,6 +174,25 @@ export function ProjectContextPackDialog(props: { setRestoreState({ status: "error", targetVersion }); } } + async function runGovernanceAction(actionKey: string, action: (input: ProjectContextGoalDecisionInput) => Promise) { + if (!props.client || pendingGovernanceAction) return; + setPendingGovernanceAction(actionKey); + try { + await action(mutationEnvelope(props.projectId)); + const [context] = await Promise.all([ + props.client.getProjectContextState(namespace), + props.client.getProjectContextPack(props.projectId).then((nextPack) => setState({ status: "ready", pack: nextPack })) + ]); + setGovernanceState({ status: "ready", context, error: false }); + } catch { + setGovernanceState((current) => current.context + ? { status: "ready", context: current.context, error: true } + : { status: "error", context: null, error: true }); + } finally { + setPendingGovernanceAction(null); + } + } + const title = graphOpen ? props.t("home.contextPack.graph.title") @@ -209,8 +247,6 @@ export function ProjectContextPackDialog(props: { {props.t("common.retry")} - ) : itemCount === 0 ? ( - {props.t("home.contextPack.empty")} ) : ( <>
    @@ -220,6 +256,17 @@ export function ProjectContextPackDialog(props: { {props.t(copied ? "home.contextPack.copied" : "home.contextPack.copy")}
    + setGovernanceRequestKey((value) => value + 1)} + onApprove={(goal) => void runGovernanceAction(`approve:${goal.id}`, (input) => props.client!.approveProjectGoal(goal.id, input))} + onReject={(goal) => void runGovernanceAction(`reject:${goal.id}`, (input) => props.client!.rejectProjectGoal(goal.id, input))} + onFocus={(item) => void runGovernanceAction(`focus:${item.id}`, (input) => props.client!.setProjectFocus({ ...input, workItemId: item.id }))} + onClearFocus={() => void runGovernanceAction("focus:clear", (input) => props.client!.setProjectFocus({ ...input, workItemId: null }))} + /> + {itemCount === 0 ? {props.t("home.contextPack.empty")} : (
    {sections.map(([key, label]) => { const items = pack![key]; @@ -236,12 +283,64 @@ export function ProjectContextPackDialog(props: { ); })}
    + )} )} ); } +function ContextGovernance(props: { + state: GovernanceState; + pendingAction: string | null; + t: Translate; + onRetry: () => void; + onApprove: (goal: ProjectGoalRecord) => void; + onReject: (goal: ProjectGoalRecord) => void; + onFocus: (item: ProjectWorkItemRecord) => void; + onClearFocus: () => void; +}) { + if (props.state.status === "loading") return
    {props.t("home.contextPack.governance.loading")}
    ; + if (!props.state.context) return ( +
    + {props.t("home.contextPack.governance.error")} + +
    + ); + const candidates = props.state.context.goals.filter((goal) => goal.status === "candidate"); + const focusedId = props.state.context.focusedWorkItem?.id; + return ( +
    +

    {props.t("home.contextPack.governance.title")}

    + {props.state.error ?

    {props.t("home.contextPack.governance.mutationError")}

    : null} +
    + {props.t("home.contextPack.governance.currentGoal")} + {props.state.context.activeGoal?.title ?? props.t("home.contextPack.governance.none")} + {props.t("home.contextPack.governance.currentFocus")} + {props.state.context.focusedWorkItem?.title ?? props.t("home.contextPack.governance.none")} + {focusedId ? : null} +
    + {candidates.map((goal) =>
    {props.t("home.contextPack.governance.candidate")}{goal.title}
    )} + {props.state.context.workItems.filter((item) => item.status !== "completed" && item.status !== "archived").map((item) =>
    {props.t("home.contextPack.governance.workItem")}{item.title}{item.nextStep ? {item.nextStep} : null}
    {item.id !== focusedId ? : null}
    )} +
    + ); +} + +function projectNamespace(projectId: string): RuntimeNamespace { + return { source: "desktop", profileId: "default", projectId }; +} + +function mutationEnvelope(projectId: string) { + const requestId = `desktop-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; + return { + namespace: projectNamespace(projectId), + source: "desktop", + adapterId: "memmy-desktop", + requestId, + provenance: { sourceAgent: "memmy-desktop", sourceMemoryIds: [], capturedAt: new Date().toISOString(), projectId, adapterId: "memmy-desktop", requestId } + }; +} + function ContextPackItem(props: { item: MemoryListItem | ProjectContextPackOutput["recentTasks"][number]; onOpen: (memoryId: string) => void; diff --git a/App/frontend/desktop/src/pages/tests/home-page-context-pack.interaction.test.tsx b/App/frontend/desktop/src/pages/tests/home-page-context-pack.interaction.test.tsx index 6e614efec..3f4d850f9 100644 --- a/App/frontend/desktop/src/pages/tests/home-page-context-pack.interaction.test.tsx +++ b/App/frontend/desktop/src/pages/tests/home-page-context-pack.interaction.test.tsx @@ -75,7 +75,19 @@ function SessionHarness(props: { const { setClients } = useApiClients(); useEffect(() => { - setClients({ memoryRuntime: { getProjectContextPack: props.getProjectContextPack } } as unknown as AppClients); + setClients({ + memoryRuntime: { + getProjectContextPack: props.getProjectContextPack, + getProjectContextState: async (namespace: { projectId?: string }) => ({ + namespaceId: `local:${namespace.projectId ?? "unscoped"}`, + activeGoal: null, + goals: [], + workItems: [], + focusedWorkItem: null, + facts: [] + }) + } + } as unknown as AppClients); dispatch(agentActions.sessionSnapshotApplied({ projectRegistryState: "ready", projects: [ diff --git a/App/frontend/desktop/src/pages/tests/project-context-pack-dialog.interaction.test.tsx b/App/frontend/desktop/src/pages/tests/project-context-pack-dialog.interaction.test.tsx index ad062ddf0..717b49f0d 100644 --- a/App/frontend/desktop/src/pages/tests/project-context-pack-dialog.interaction.test.tsx +++ b/App/frontend/desktop/src/pages/tests/project-context-pack-dialog.interaction.test.tsx @@ -3,7 +3,7 @@ import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { GetMemoryOutput, MemoryListItem, ProjectContextPackOutput } from "@memmy/local-api-contracts"; +import type { GetMemoryOutput, MemoryListItem, ProjectContextPackOutput, ProjectContextReadState, ProjectGoalRecord } from "@memmy/local-api-contracts"; import type { MemoryRuntimeClient } from "../../api/memory-runtime-client.js"; import type { MessageKey } from "../../i18n/messages.js"; import { ProjectContextPackDialog } from "../project-context-pack-dialog.js"; @@ -188,6 +188,31 @@ describe("ProjectContextPackDialog", () => { expect(document.body.textContent).toContain("home.contextPack.history.success"); expect(document.body.textContent).toContain("v1"); }); + it("loads governance state and approves a candidate goal", async () => { + const getProjectContextPack = vi.fn(async () => contextPack()); + const getProjectContextState = vi.fn(async () => contextState()); + const approveProjectGoal = vi.fn(async () => goalRecord("active")); + + await act(async () => { + root.render( undefined} />); + }); + + expect(getProjectContextState).toHaveBeenCalledWith(expect.objectContaining({ projectId: "project-1" })); + expect(document.body.textContent).toContain("home.contextPack.governance.approve"); + await act(async () => buttonContaining("home.contextPack.governance.approve").click()); + expect(approveProjectGoal).toHaveBeenCalledWith("goal-1", expect.objectContaining({ namespace: expect.objectContaining({ projectId: "project-1" }) })); + expect(getProjectContextPack).toHaveBeenCalledTimes(2); + }); + + it("sets and clears the focused work item through governance controls", async () => { + const getProjectContextState = vi.fn(async () => contextState()); + const setProjectFocus = vi.fn(async () => null); + await act(async () => { + root.render( contextPack()), getProjectContextState, setProjectFocus })} t={translate} onClose={() => undefined} />); + }); + await act(async () => buttonContaining("home.contextPack.governance.setFocus").click()); + expect(setProjectFocus).toHaveBeenCalledWith(expect.objectContaining({ workItemId: "work-1" })); + }); it("shows detail loading and recovers from a failed request", async () => { const pending = deferred(); @@ -362,8 +387,17 @@ function memoryHistory(id: string) { }; } +function goalRecord(status: ProjectGoalRecord["status"] = "candidate"): ProjectGoalRecord { + return { id: "goal-1", namespaceId: "namespace-1", userId: "user-1", projectId: "project-1", title: "Ship authoritative context", summary: "", detail: "", acceptanceCriteria: [], constraints: [], status, version: 1, sourceMemoryIds: ["memory-1"], provenance: {}, createdAt: "2026-08-08T12:00:00.000Z", updatedAt: "2026-08-08T12:00:00.000Z" }; +} + +function contextState(): ProjectContextReadState { + const workItem = { id: "work-1", namespaceId: "namespace-1", userId: "user-1", projectId: "project-1", goalId: "goal-1", title: "Wire desktop governance", summary: "", nextStep: "Approve and focus", acceptanceCriteria: [], constraints: [], status: "active" as const, focused: false, sourceMemoryIds: ["memory-1"], provenance: {}, createdAt: "2026-08-08T12:00:00.000Z", updatedAt: "2026-08-08T12:00:00.000Z" }; + return { namespaceId: "namespace-1", activeGoal: null, goals: [goalRecord()], workItems: [workItem], focusedWorkItem: null, facts: [] }; +} + function dialogClient( - overrides: Partial> = {} + overrides: Partial> = {} ) { return { getProjectContextPack: async () => contextPack(), @@ -378,6 +412,10 @@ function dialogClient( auditId: "audit-restore", serverTime: "2026-08-08T13:00:00.000Z" }), + getProjectContextState: async () => contextState(), + approveProjectGoal: async () => goalRecord("active"), + rejectProjectGoal: async () => goalRecord("archived"), + setProjectFocus: async () => null, ...overrides }; } diff --git a/App/frontend/desktop/src/styles.css b/App/frontend/desktop/src/styles.css index 12d80030e..d8c0ddde5 100644 --- a/App/frontend/desktop/src/styles.css +++ b/App/frontend/desktop/src/styles.css @@ -727,6 +727,39 @@ body.memmy-window-fullscreen { .project-context-pack-dialog__copy { flex-shrink: 0; padding: 6px 9px; } +.project-context-governance { + display: grid; + gap: 10px; + padding: 14px 0; + border-bottom: 1px solid color-mix(in srgb, var(--color-border-stone) 45%, transparent); +} + +.project-context-governance__heading { display: flex; align-items: center; gap: 7px; color: var(--color-text-ink); } +.project-context-governance__heading h3 { margin: 0; font-size: var(--codex-text-sm); font-weight: 700; letter-spacing: 0; } +.project-context-governance__current { display: grid; grid-template-columns: max-content minmax(0, 1fr) max-content; align-items: center; gap: 5px 10px; } +.project-context-governance__current span, +.project-context-governance__row span { color: color-mix(in srgb, var(--color-text-ink) 55%, transparent); font-size: var(--codex-text-xs); } +.project-context-governance__current strong, +.project-context-governance__row strong { min-width: 0; overflow-wrap: anywhere; font-size: var(--codex-text-sm); font-weight: 600; } +.project-context-governance__row { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 10px; border: 1px solid color-mix(in srgb, var(--color-border-stone) 45%, transparent); border-radius: var(--radius-input); } +.project-context-governance__row > div:first-child { display: grid; min-width: 0; gap: 2px; } +.project-context-governance__row small { color: color-mix(in srgb, var(--color-text-ink) 62%, transparent); font-size: var(--codex-text-xs); overflow-wrap: anywhere; } +.project-context-governance__actions { display: flex; flex-shrink: 0; gap: 6px; } +.project-context-governance button, +.project-context-governance__status button { display: inline-flex; align-items: center; justify-content: center; gap: 5px; padding: 6px 9px; border: 1px solid color-mix(in srgb, var(--color-border-stone) 70%, transparent); border-radius: var(--radius-input); background: var(--color-bg-elevated); color: var(--color-text-ink); font: inherit; font-size: var(--codex-text-xs); cursor: pointer; } +.project-context-governance button:disabled { opacity: .55; cursor: wait; } +.project-context-governance .project-context-governance__primary { border-color: transparent; background: var(--color-action-sky); color: white; } +.project-context-governance__status { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 12px 0; color: color-mix(in srgb, var(--color-text-ink) 55%, transparent); font-size: var(--codex-text-xs); } +.project-context-governance__status--error, +.project-context-governance__error { color: var(--color-status-error, #b42318); } +.project-context-governance__error { margin: 0; font-size: var(--codex-text-xs); } + +@media (max-width: 560px) { + .project-context-governance__current { grid-template-columns: 1fr; } + .project-context-governance__row { align-items: stretch; flex-direction: column; } + .project-context-governance__actions { justify-content: flex-end; } +} + .project-context-pack-dialog__sections { display: grid; } .project-context-pack-dialog__section { From 305b003e87534355746914a7337ca10b30eb019c Mon Sep 17 00:00:00 2001 From: bluewater_cg Date: Mon, 10 Aug 2026 07:01:09 -0700 Subject: [PATCH 19/23] docs: explain authoritative project context --- docs/cn/desktop/workbench.mdx | 9 + docs/cn/memory/memory-cli.mdx | 2 + docs/cn/memory/overview.mdx | 16 +- docs/cn/memory/sources.mdx | 17 +- docs/en/desktop/workbench.mdx | 9 + docs/en/memory/memory-cli.mdx | 2 + docs/en/memory/overview.mdx | 15 +- docs/en/memory/sources.mdx | 17 +- .../plans/2026-08-10-project-context-pack.md | 665 ++++++++++++++++++ 9 files changed, 734 insertions(+), 18 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-10-project-context-pack.md diff --git a/docs/cn/desktop/workbench.mdx b/docs/cn/desktop/workbench.mdx index 1426915f5..99651f333 100644 --- a/docs/cn/desktop/workbench.mdx +++ b/docs/cn/desktop/workbench.mdx @@ -32,3 +32,12 @@ icon: LayoutDashboard ## 任务列表 按置顶、今天、昨天、最近 7 天、更早、已归档分组。支持重命名、置顶、归档、恢复、删除;运行中任务显示旋转状态,完成未读任务显示未读点。`Cmd/Ctrl + K` 呼出任务搜索。 + + +## 项目上下文包 + +当前对话属于已登记项目时,输入框工具栏会显示**上下文包**。对话框展示由当前项目有效记忆实时生成的 Markdown,包括项目约定、常用命令、架构事实、最近任务和用户偏好。点击条目可以查看完整记忆详情及证据关系图。 + +**项目治理**区域显示已确认目标和焦点工作项。你可以批准或拒绝候选目标,也可以选择或清除当前焦点。候选目标在批准前不会进入 Agent 提示词。 + +这份状态按项目隔离,不属于某一个 Agent。已安装的 CLI 接入只要解析到同一 workspace,就会在下一回合开始时自动收到已确认的目标、约束和焦点。 diff --git a/docs/cn/memory/memory-cli.mdx b/docs/cn/memory/memory-cli.mdx index 9be98c34e..33e888522 100644 --- a/docs/cn/memory/memory-cli.mdx +++ b/docs/cn/memory/memory-cli.mdx @@ -21,4 +21,6 @@ memmy-memory delete `doctor` 会检查服务健康、记忆演化、未知 Agent 来源、缺失 workspace 绑定和跨 workspace 泄漏风险;`stats --workspace` 只返回当前 workspace 的层级与队列统计。 +workspace 能解析到项目时,`turn start` 还会返回权威项目上下文。它与历史记忆分开渲染并排在召回结果之前;该项目中的 CLI Agent 共享已确认目标和焦点状态。 + 另支持 `reload-config`、`session open/close`、`turn start/complete` 和 `raw` 调用。 diff --git a/docs/cn/memory/overview.mdx b/docs/cn/memory/overview.mdx index 7f1afb04d..0a07e2ee7 100644 --- a/docs/cn/memory/overview.mdx +++ b/docs/cn/memory/overview.mdx @@ -10,12 +10,13 @@ Memmy 的记忆由本地 Memory 服务承载。默认服务地址是 `http://127 链路可以拆成七步: 1. Hook 或插件在请求进入 Agent 前打开或复用 Memmy 会话,并调用 `turn.start`。 -2. Memory 服务在新 episode 的第一轮判断请求意图,决定是否召回以及允许召回哪些层。 -3. 查询被整理为语义查询、关键词、短词模式和结构化错误片段。 -4. 各召回通道并行返回候选,再按记忆层、通道排名、质量和时间衰减融合。 -5. 候选经过相对阈值、去重、MMR 多样性选择和可选的 LLM 过滤。 -6. 命中项被渲染为“历史上下文”注入 Agent;当前用户请求始终优先。 -7. 回合结束后,`turn.complete` 写入原始回合和 L1 轨迹,后台任务再继续摘要、反思、评分、向量化和高层记忆演化。 +2. workspace 能解析到项目时,Memory 服务把该项目已确认的目标、焦点工作项和约束渲染为权威项目上下文。 +3. Memory 服务在新 episode 的第一轮判断请求意图,决定是否召回历史记忆以及允许召回哪些层。 +4. 查询被整理为语义查询、关键词、短词模式和结构化错误片段。 +5. 各召回通道并行返回候选,再按记忆层、通道排名、质量和时间衰减融合。 +6. 候选经过相对阈值、去重、MMR 多样性选择和可选的 LLM 过滤。 +7. 项目上下文先注入,随后才是历史记忆命中;当前用户请求始终高于两者。 +8. 回合结束后,`turn.complete` 写入原始回合和 L1 轨迹,后台任务再继续摘要、反思、评分、向量化和高层记忆演化。 ## 四个记忆层 @@ -179,6 +180,9 @@ LLM 可以明确丢弃全部候选;如果输出格式无效或调用失败, ## 如何注入 Agent 上下文 +workspace 能解析到项目时,`turn.start` 会在召回记忆前先放入独立的 `` 区块。这里仅包含已确认的项目权威状态:当前目标及其约束、选中的工作项和下一步。Agent 来源与 profile 用于记录出处,不会切分项目,因此同一 workspace 中的 Codex、Pi 和其他已安装接入会收到同一份确认状态。Hook 与插件的具体行为见[Agent 来源与扫描](/cn/memory/sources)。 + + 命中项会按以下顺序渲染: 1. L1 Trace 和相似 episode; diff --git a/docs/cn/memory/sources.mdx b/docs/cn/memory/sources.mdx index ff12ced9d..b06ce0b52 100644 --- a/docs/cn/memory/sources.mdx +++ b/docs/cn/memory/sources.mdx @@ -95,14 +95,23 @@ Memmy 通过 Agent 原生支持的 **Hook** 或**插件**接入实时对话。 | OpenClaw | Memory 插件 | `before_prompt_build`、`agent_end` | 构建提示词前注入记忆,Agent 结束时采集完整回合,并提供原生记忆工具 | | Hermes | Memory Provider 插件 | `prefetch`、`sync_turn` 等 Provider 生命周期 | 自动召回和采集,镜像 Hermes 的显式记忆写入,并提供原生记忆工具 | +### 回合开始时的权威项目上下文 + +Hook 或插件把当前 workspace 解析到项目后,`turn.start` 会先读取该项目已确认的上下文,再召回历史记忆。Memmy 将当前目标、目标约束、焦点工作项、下一步和验收标准放进独立的 `` 区块,然后追加相关历史记忆。 + +两者职责不同:已批准的项目上下文是当前工作状态,L1/L2/L3/Skill 召回结果是历史证据;当前用户请求仍高于两者。项目边界由项目本身决定,不按 Agent 来源或 profile 切分,因此一个 CLI 批准的目标会自动提供给同一项目中的另一个 CLI;不同项目或 tenant 不会继承。 + +项目上下文治理在桌面端的**项目上下文包**对话框中完成。候选目标只有批准后才会注入,任一时刻只能有一个焦点工作项。 + ### 一次对话中会发生什么 ```mermaid flowchart LR Request["用户请求"] --> Entry["Hook / 插件"] Entry --> Start["打开会话并开始回合"] - Start --> Recall["检索相关 Memmy 记忆"] - Recall --> Context["作为历史上下文注入"] + Start --> Authority["读取已确认项目上下文"] + Authority --> Recall["检索相关历史记忆"] + Recall --> Context["先注入项目上下文,再注入历史"] Context --> Agent["Agent 执行任务"] Agent --> Complete["采集回答、状态和可用的工具轨迹"] Complete --> Memory["完成回合并写入本地 Memory 服务"] @@ -110,8 +119,10 @@ flowchart LR 安装后的共同效果: -- **自动召回**:Claude Code、Codex、Pi、OpenCode、OpenClaw 和 Hermes 会在正常请求执行前检索相关记忆并注入上下文。 +- **权威项目上下文**:受支持的接入会先收到当前项目相同的已批准目标、约束和焦点,再收到历史召回。 +- **自动召回**:Claude Code、Codex、Pi、OpenCode、OpenClaw 和 Hermes 会在正常请求执行前检索并注入相关历史记忆。 - **自动采集**:Hook 或插件会在回合结束时提交用户请求、Agent 回答和成功/失败状态,不需要 Agent 手动执行 `memmy-memory add`。 + - **任务接续**:输入 `/memmy-resume ` 会搜索最多 5 个候选 L1 episode;继续输入 `1`–`5` 可读取完整 episode 并注入接续上下文。 - **按需查询**:随接入安装的 Skill 保留 `memmy-memory search` 和 `memmy-memory get`,只在自动上下文不足时使用。 - **来源标记**:采集结果会记录 `cursor`、`claude_code`、`codex`、`pi`、`opencode`、`openclaw` 或 `hermes`,便于过滤和追踪来源。 diff --git a/docs/en/desktop/workbench.mdx b/docs/en/desktop/workbench.mdx index 90ec7b639..f92ac185c 100644 --- a/docs/en/desktop/workbench.mdx +++ b/docs/en/desktop/workbench.mdx @@ -32,3 +32,12 @@ Type `/` to open the command panel: ## Task List Grouped by Pinned, Today, Yesterday, Last 7 Days, Older, and Archived. Supports rename, pin, archive, restore, and delete; running tasks show a spinner, and completed-but-unread tasks show an unread dot. `Cmd/Ctrl + K` opens task search. + + +## Project Context Pack + +When the current chat belongs to a registered project, the composer toolbar shows **Context Pack**. The dialog displays a Markdown pack generated from active project memories, including conventions, common commands, architecture facts, recent tasks, and user preferences. Individual entries open their complete memory detail and evidence graph. + +The **Project governance** section shows the confirmed goal and focused work item. You can approve or reject candidate goals and select or clear the current focus. Candidate goals do not affect Agent prompts until approved. + +This state is scoped to the project rather than to one Agent. Installed CLI integrations that resolve the same workspace receive the confirmed goal, constraints, and focus automatically at the start of their next turn. diff --git a/docs/en/memory/memory-cli.mdx b/docs/en/memory/memory-cli.mdx index c8222860e..f66409315 100644 --- a/docs/en/memory/memory-cli.mdx +++ b/docs/en/memory/memory-cli.mdx @@ -21,4 +21,6 @@ Connects to `http://127.0.0.1:18960` by default. Use `--url`, `--token`, `--conf `doctor` checks service health, memory evolution, unknown Agent sources, missing workspace bindings, and cross-workspace leakage risk. `stats --workspace` limits layer and queue statistics to the current workspace. +`turn start` also returns authoritative project context when the workspace resolves to a project. It is rendered separately and before historical-memory recall; confirmed goal and focus state are shared by CLI agents in that project. + Also supports reload-config, session open/close, turn start/complete, and raw calls. diff --git a/docs/en/memory/overview.mdx b/docs/en/memory/overview.mdx index 6f25032b6..f48ae2410 100644 --- a/docs/en/memory/overview.mdx +++ b/docs/en/memory/overview.mdx @@ -10,12 +10,13 @@ Memmy memory is hosted by the local Memory service. The default endpoint is `htt The path has seven stages: 1. Before a request reaches the Agent, a Hook or plugin opens or reuses a Memmy session and calls `turn.start`. -2. On the first turn of a new episode, the Memory service classifies intent and decides whether to recall memory and which layers are allowed. -3. The request is converted into a semantic query, keywords, short-pattern terms, and structural error fragments. -4. Recall channels run in parallel, then candidates are fused using memory layer, channel rank, quality, and time decay. -5. Candidates pass a relative threshold, deduplication, MMR diversity selection, and an optional LLM filter. -6. Hits are rendered as historical context and injected into the Agent. The current user request always remains authoritative. -7. At the end of the turn, `turn.complete` stores the raw turn and L1 traces. Background jobs then summarize, reflect, score, embed, and evolve higher-level memory. +2. When the workspace resolves to a project, the Memory service renders that project's confirmed goal, focused work item, and constraints as authoritative project context. +3. On the first turn of a new episode, the Memory service classifies intent and decides whether to recall historical memory and which layers are allowed. +4. The request is converted into a semantic query, keywords, short-pattern terms, and structural error fragments. +5. Recall channels run in parallel, then candidates are fused using memory layer, channel rank, quality, and time decay. +6. Candidates pass a relative threshold, deduplication, MMR diversity selection, and an optional LLM filter. +7. Project context is injected first, followed by any historical-memory hits. The current user request remains authoritative over both. +8. At the end of the turn, `turn.complete` stores the raw turn and L1 traces. Background jobs then summarize, reflect, score, embed, and evolve higher-level memory. ## Four Memory Layers @@ -179,6 +180,8 @@ The LLM may intentionally drop every candidate. If its output is invalid or the ## How Context Is Injected +When the workspace resolves to a project, `turn.start` prepends a separate `` block before recalled memory. This block contains only confirmed project authority: the active goal, its constraints, and the selected work item and next step. Agent source and profile identify provenance but do not split the project, so Codex, Pi, and other installed integrations in the same workspace receive the same confirmed state. See [Agent Sources & Scanning](/en/memory/sources) for Hook and plugin behavior. + Hits are rendered in this order: 1. L1 traces and similar episodes; diff --git a/docs/en/memory/sources.mdx b/docs/en/memory/sources.mdx index 24ba0fedb..61115cab6 100644 --- a/docs/en/memory/sources.mdx +++ b/docs/en/memory/sources.mdx @@ -95,14 +95,23 @@ Memmy connects to live Agent conversations through each host's native **Hooks** | OpenClaw | Memory plugin | `before_prompt_build`, `agent_end` | Injects memory while building the prompt, captures the completed turn, and exposes native memory tools | | Hermes | Memory Provider plugin | Provider lifecycle methods such as `prefetch` and `sync_turn` | Recalls and captures automatically, mirrors explicit Hermes memory writes, and exposes native memory tools | +### Authoritative project context at turn start + +When a Hook or plugin resolves the current workspace to a project, `turn.start` reads that project's confirmed context before historical recall. Memmy renders the active goal, goal constraints, focused work item, next step, and acceptance criteria in a separate `` block, then appends any relevant historical memory. + +This distinction is intentional: approved project context is the current operating state, while L1/L2/L3/Skill results are historical evidence. The current user request remains authoritative over both. The project boundary is shared across Agent sources and profiles, so a goal approved from one CLI is available automatically to another CLI working in the same project. A different project or tenant does not inherit it. + +Project context governance is performed in the desktop **Project Context Pack** dialog. Goal proposals are not injected until approved, and only one work item is focused at a time. + ### What happens during a turn ```mermaid flowchart LR Request["User request"] --> Entry["Hook / plugin"] Entry --> Start["Open session and start turn"] - Start --> Recall["Retrieve relevant Memmy memory"] - Recall --> Context["Inject it as historical context"] + Start --> Authority["Read confirmed project context"] + Authority --> Recall["Retrieve relevant historical memory"] + Recall --> Context["Inject project context, then history"] Context --> Agent["Agent performs the task"] Agent --> Complete["Capture answer, status, and available tool traces"] Complete --> Memory["Complete the turn in the local Memory service"] @@ -110,8 +119,10 @@ flowchart LR Common results after installation: -- **Automatic recall:** Claude Code, Codex, Pi, OpenCode, OpenClaw, and Hermes retrieve and inject relevant memory before normal requests run. +- **Authoritative project context:** supported integrations receive the same approved goal, constraints, and selected focus for the resolved project before historical recall. +- **Automatic recall:** Claude Code, Codex, Pi, OpenCode, OpenClaw, and Hermes retrieve and inject relevant historical memory before normal requests run. - **Automatic capture:** the Hook or plugin submits the user request, Agent answer, and success/failure status at the end of a turn. The Agent does not need to run `memmy-memory add` manually. + - **Task resumption:** `/memmy-resume ` returns up to five L1 episode candidates. Enter `1`–`5` to load the complete episode and inject continuation context. - **On-demand lookup:** the bundled Skill keeps `memmy-memory search` and `memmy-memory get` for cases where automatic context is insufficient. - **Source attribution:** captured turns carry `cursor`, `claude_code`, `codex`, `pi`, `opencode`, `openclaw`, or `hermes`, so you can filter and trace their origin. diff --git a/docs/superpowers/plans/2026-08-10-project-context-pack.md b/docs/superpowers/plans/2026-08-10-project-context-pack.md new file mode 100644 index 000000000..8bfaadad7 --- /dev/null +++ b/docs/superpowers/plans/2026-08-10-project-context-pack.md @@ -0,0 +1,665 @@ +# Authoritative Project Context Pack Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make confirmed project goals, hard constraints, decisions, and a manually selected work item survive fresh sessions and CLI switches by injecting a bounded authoritative project context on every project-scoped turn. + +**Architecture:** Add a project-context domain service backed by dedicated SQLite records scoped by the existing Memmy namespace. `turns/start` composes a stable authoritative section before existing supplemental retrieval, while local API clients, installed CLI hooks, and the existing Project Context Pack dialog expose proposal review and manual work-item focus. Existing Memory rows remain evidence and retrieval material, not the source of truth for project state. + +**Tech Stack:** TypeScript 6, Node.js 20+, better-sqlite3, Zod 4, Fastify 5, React 19, Vitest 4. + +## Global Constraints + +- Preserve all existing workspace changes; `Memory/src/service/read-model/panel-read.ts` and retrieval files are already modified and must be re-read immediately before editing. +- Use the existing `RuntimeNamespace` and `namespaceIdFromContext`; do not introduce another project identifier. +- Exactly zero or one confirmed primary goal and zero or one focused work item may exist per namespace. +- Generated proposals remain non-authoritative until explicit approval. +- Short goals preserve their complete text; long goals preserve full `detail` while stable injection uses bounded `summary`. +- Work-item focus changes only by explicit mutation; ordinary turns never infer or switch focus. +- Stable project context precedes supplemental memory context and is fetched on every turn. +- Existing retrieval behavior remains supplemental and otherwise unchanged. +- Mutation requests carry namespace, source, adapter ID, request ID, and provenance. +- Write failing behavioral tests before each production change and observe the intended failure. + +--- + +## File Structure + +- `Memory/src/service/project-context/project-context-service.ts`: owns proposal, approval, goal versioning, work-item focus, context rendering, and invariants. +- `Memory/src/service/project-context/project-context-types.ts`: domain records and request/response contracts internal to Memory. +- `Memory/src/storage/schema.ts`: creates the three authoritative project-context tables and uniqueness indexes. +- `Memory/src/storage/repositories.ts`: persists project goals, work items, and confirmed facts in the current namespace. +- `Memory/src/service/session/session-turn-service.ts`: prepends authoritative context to the existing injected context and records its version in provenance. +- `Memory/src/types.ts`: public Memory service turn-start and project-context types. +- `Memory/src/service/memory-service.ts`: exposes project-context operations through the existing service facade. +- `Memory/src/server/http.ts`: Memory HTTP read/mutation routes. +- `App/backend/local-api-contracts/src/memory-runtime.ts`: Zod contracts shared by backend and UI. +- `App/backend/src/adapters/outbound/memory-client/*`: Memory client operations and endpoint map. +- `App/backend/src/adapters/inbound/local-api/routes/agent-runtime/panel.ts`: authenticated local project-context routes. +- `App/backend/src/services/panel-service.ts`: local service facade. +- `App/backend/src/adapters/outbound/skill-writer/templates/memmy-resume-hook.ts`: marks authoritative project context separately and records the injected version. +- `App/frontend/desktop/src/pages/project-context-pack-dialog.tsx`: goal review, work-item selection, and authoritative state display. + +--- + +### Task 1: Persist Authoritative Project Context + +**Files:** +- Create: `Memory/src/service/project-context/project-context-types.ts` +- Modify: `Memory/src/storage/schema.ts` +- Modify: `Memory/src/storage/repositories.ts` +- Test: `Memory/tests/repository/project-context-repository.test.ts` +- Test: `Memory/tests/repository/sqlite-schema.test.ts` + +**Interfaces:** +- Produces `ProjectGoalRecord`, `ProjectWorkItemRecord`, `ProjectFactRecord`, and `ProjectContextRepository`. +- `ProjectContextRepository` methods: + +```ts +interface ProjectContextRepository { + getActiveGoal(namespaceId: string): ProjectGoalRecord | undefined; + getGoal(id: string): ProjectGoalRecord | undefined; + listGoals(namespaceId: string): ProjectGoalRecord[]; + insertGoal(goal: ProjectGoalRecord): ProjectGoalRecord; + replaceActiveGoal(next: ProjectGoalRecord): ProjectGoalRecord; + getFocusedWorkItem(namespaceId: string): ProjectWorkItemRecord | undefined; + listWorkItems(namespaceId: string): ProjectWorkItemRecord[]; + insertWorkItem(item: ProjectWorkItemRecord): ProjectWorkItemRecord; + updateWorkItem(item: ProjectWorkItemRecord): ProjectWorkItemRecord; + setFocusedWorkItem(namespaceId: string, itemId: string | null, at: string): ProjectWorkItemRecord | undefined; + listActiveFacts(namespaceId: string): ProjectFactRecord[]; + insertFact(fact: ProjectFactRecord): ProjectFactRecord; + supersedeFact(previousId: string, next: ProjectFactRecord): ProjectFactRecord; +} +``` + +- [ ] **Step 1: Write failing repository tests** + +Create tests proving: + +```ts +it("allows only one active goal per namespace", () => { + repo.insertGoal(goal({ id: "goal-1", namespaceId: "ns-1", status: "active", version: 1 })); + expect(() => repo.insertGoal(goal({ id: "goal-2", namespaceId: "ns-1", status: "active", version: 2 }))) + .toThrow(/active project goal/i); +}); + +it("focuses exactly one work item without changing the goal", () => { + repo.insertGoal(goal({ id: "goal-1", namespaceId: "ns-1", status: "active", version: 1 })); + repo.insertWorkItem(workItem({ id: "work-1", namespaceId: "ns-1" })); + repo.insertWorkItem(workItem({ id: "work-2", namespaceId: "ns-1" })); + repo.setFocusedWorkItem("ns-1", "work-1", NOW); + repo.setFocusedWorkItem("ns-1", "work-2", NOW); + expect(repo.getFocusedWorkItem("ns-1")?.id).toBe("work-2"); + expect(repo.getActiveGoal("ns-1")?.id).toBe("goal-1"); +}); + +it("rejects cross-namespace focus", () => { + repo.insertWorkItem(workItem({ id: "work-other", namespaceId: "ns-2" })); + expect(() => repo.setFocusedWorkItem("ns-1", "work-other", NOW)).toThrow(/namespace/i); +}); +``` + +Extend the schema test to assert the new tables and indexes exist. + +- [ ] **Step 2: Run the focused tests and verify RED** + +Run: + +```bash +npm --prefix Memory test -- tests/repository/project-context-repository.test.ts tests/repository/sqlite-schema.test.ts +``` + +Expected: failure because the records, repository, and tables do not exist. + +- [ ] **Step 3: Add domain record types** + +Define records with explicit statuses: + +```ts +export type ProjectGoalStatus = "candidate" | "active" | "completed" | "archived"; +export type ProjectWorkItemStatus = "pending" | "active" | "blocked" | "completed" | "archived"; +export type ProjectFactKind = "decision" | "constraint"; +export type ProjectFactStatus = "candidate" | "active" | "superseded" | "archived"; +``` + +Each record includes `id`, `namespaceId`, `userId`, optional `projectId`, optional `workspaceId`, optional `workspacePath`, content fields, `sourceMemoryIds`, `provenance`, `createdAt`, and `updatedAt`. Goals additionally include `version` and `supersedesId`; work items include `focused`; facts include `kind` and `supersedesId`. + +- [ ] **Step 4: Add schema tables and indexes** + +Add `project_context_goals`, `project_context_work_items`, and `project_context_facts`. Store arrays and provenance as JSON text. Add partial unique indexes: + +```sql +CREATE UNIQUE INDEX IF NOT EXISTS uq_project_context_active_goal +ON project_context_goals(namespace_id) +WHERE status = 'active'; + +CREATE UNIQUE INDEX IF NOT EXISTS uq_project_context_focused_work_item +ON project_context_work_items(namespace_id) +WHERE focused = 1; +``` + +Use foreign keys for `supersedes_id` and work-item `goal_id`. Increment `SCHEMA_VERSION` and allow migration from the immediately previous version. + +- [ ] **Step 5: Implement repository methods transactionally** + +`replaceActiveGoal` archives the prior active goal, inserts the next version with `supersedesId`, and commits atomically. `setFocusedWorkItem` clears current focus and sets the requested item in one transaction. Reject records whose stored namespace differs from the mutation namespace. + +- [ ] **Step 6: Run repository tests and verify GREEN** + +Run the command from Step 2. Expected: all selected tests pass. + +- [ ] **Step 7: Commit the storage unit** + +```bash +git add Memory/src/service/project-context/project-context-types.ts Memory/src/storage/schema.ts Memory/src/storage/repositories.ts Memory/tests/repository/project-context-repository.test.ts Memory/tests/repository/sqlite-schema.test.ts +git commit -m "feat(memory): persist authoritative project context" +``` + +--- + +### Task 2: Implement Goal Approval, Work Items, And Stable Rendering + +**Files:** +- Create: `Memory/src/service/project-context/project-context-service.ts` +- Create: `Memory/tests/service/project-context/project-context-service.test.ts` +- Modify: `Memory/src/types.ts` +- Modify: `Memory/src/service/memory-service.ts` + +**Interfaces:** +- Consumes `ProjectContextRepository` from Task 1. +- Produces: + +```ts +interface StableProjectContext { + namespaceId: string; + status: "ready" | "no_confirmed_goal" | "conflict"; + version: number; + goal: ProjectGoalRecord | null; + focusedWorkItem: ProjectWorkItemRecord | null; + facts: ProjectFactRecord[]; + markdown: string; + sourceMemoryIds: string[]; + generatedAt: string; +} + +class ProjectContextService { + read(namespace: RuntimeNamespace): ProjectContextState; + proposeGoal(input: ProposeProjectGoalRequest): Promise; + approveGoal(input: ApproveProjectGoalRequest): ProjectGoalRecord; + rejectGoal(input: RejectProjectGoalRequest): ProjectGoalRecord; + createWorkItem(input: CreateProjectWorkItemRequest): ProjectWorkItemRecord; + updateWorkItem(input: UpdateProjectWorkItemRequest): ProjectWorkItemRecord; + selectWorkItem(input: SelectProjectWorkItemRequest): ProjectWorkItemRecord | null; + renderStable(namespace: RuntimeNamespace, budget?: number): StableProjectContext; +} +``` + +- [ ] **Step 1: Write failing service tests** + +Cover these observable contracts: + +```ts +it("does not render an unconfirmed goal proposal", async () => { + await service.proposeGoal(proposalInput({ title: "Candidate only" })); + expect(service.renderStable(NS).status).toBe("no_confirmed_goal"); + expect(service.renderStable(NS).markdown).not.toContain("Candidate only"); +}); + +it("renders an approved goal for an unrelated prompt", async () => { + const candidate = await service.proposeGoal(proposalInput({ title: "Ship reliable project memory" })); + service.approveGoal(approveInput(candidate.id)); + const stable = service.renderStable(NS); + expect(stable.markdown).toContain("Ship reliable project memory"); + expect(stable.version).toBe(1); +}); + +it("preserves long detail outside the bounded stable view", async () => { + const detail = "Long goal ".repeat(2000); + const candidate = await service.proposeGoal(proposalInput({ detail, summary: "Bounded summary" })); + const active = service.approveGoal(approveInput(candidate.id)); + expect(service.read(NS).activeGoal?.detail).toBe(detail); + expect(service.renderStable(NS, 1200).markdown).toContain("Bounded summary"); + expect(service.renderStable(NS, 1200).markdown).not.toContain(detail); +}); + +it("changes focused work item without changing the primary goal", async () => { + const goal = await approveGoal(service, "Primary goal"); + const first = service.createWorkItem(workInput(goal.id, "First")); + const second = service.createWorkItem(workInput(goal.id, "Second")); + service.selectWorkItem(selectInput(first.id)); + service.selectWorkItem(selectInput(second.id)); + expect(service.renderStable(NS).goal?.id).toBe(goal.id); + expect(service.renderStable(NS).focusedWorkItem?.id).toBe(second.id); +}); + +it("renders a non-blocking reminder when no work item is selected", async () => { + await approveGoal(service, "Primary goal"); + expect(service.renderStable(NS).markdown).toContain("No work item is selected"); +}); +``` + +Also test goal supersession, candidate rejection, deterministic budget priority, active fact conflicts, and source-memory ID deduplication. + +- [ ] **Step 2: Run service tests and verify RED** + +```bash +npm --prefix Memory test -- tests/service/project-context/project-context-service.test.ts +``` + +Expected: failure because `ProjectContextService` does not exist. + +- [ ] **Step 3: Implement proposal and approval rules** + +`proposeGoal` creates only `candidate` records. For initial delivery, accept a normalized candidate payload produced by the authorized summarization caller; do not silently activate it. Keep the LLM proposal producer behind the existing project summarization flow instead of embedding model calls in the repository service. + +`approveGoal` validates namespace and candidate status, assigns the next version, and calls `replaceActiveGoal`. `rejectGoal` archives the candidate without changing active state. + +- [ ] **Step 4: Implement stable rendering** + +Render a separate marker: + +```md + +# Confirmed Project Context +## Primary Goal +... +## Focused Work Item +... +## Hard Constraints +... +## Confirmed Decisions +... + +``` + +Use deterministic section priorities: constraints, goal title/summary, focused work item/next step, acceptance criteria, decisions, metadata. If no goal exists, render only a compact `no confirmed project goal` state. Never render candidates. + +- [ ] **Step 5: Expose facade methods from `MemoryService`** + +Add read and mutation methods that resolve the existing namespace, enforce project scope, and delegate to `ProjectContextService`. Do not modify ordinary `addMemory` or search semantics. + +- [ ] **Step 6: Run service tests and verify GREEN** + +Run the command from Step 2. Expected: all service tests pass. + +- [ ] **Step 7: Commit the domain unit** + +```bash +git add Memory/src/service/project-context Memory/src/types.ts Memory/src/service/memory-service.ts Memory/tests/service/project-context +git commit -m "feat(memory): manage confirmed project goals" +``` + +--- + +### Task 3: Prepend Stable Project Context At Turn Start + +**Files:** +- Modify: `Memory/src/types.ts` +- Modify: `Memory/src/service/session/session-turn-service.ts` +- Modify: `Memory/src/server/http.ts` +- Test: `Memory/tests/service/session/turn-capture.test.ts` +- Test: `Memory/tests/contract/memory-rest-service.test.ts` + +**Interfaces:** +- Consumes `ProjectContextService.renderStable` from Task 2. +- Extends turn-start output with: + +```ts +projectContext: { + namespaceId: string; + status: "ready" | "no_confirmed_goal" | "conflict"; + version: number; + markdown: string; + sourceMemoryIds: string[]; + generatedAt: string; +}; +``` + +- `injectedContext.markdown` contains project context before supplemental retrieval. +- `sourceMemoryIds` remains the deduplicated union of authoritative evidence IDs and retrieval IDs. + +- [ ] **Step 1: Add failing turn-start tests** + +Add a test that approves a goal and hard constraint, starts a turn with an unrelated query, and asserts: + +```ts +expect(started.projectContext.version).toBe(1); +expect(started.injectedContext.markdown.indexOf("confirmed current state +supporting historical memories +``` + +- Saves `projectContextVersion` and combined `sourceMemoryIds` in turn state used by completion provenance. + +- [ ] **Step 1: Write failing rendered-hook tests** + +Execute rendered Claude Code, Codex, and Pi hook scripts against a fake Memory endpoint. Assert all three outputs contain the same project-context version before historical context. Return version 2 on a second prompt and assert the hook emits version 2, proving no authoritative cache. + +Also assert an unavailable Memory service produces an explicit context-unavailable notice rather than stale project content. + +- [ ] **Step 2: Run hook tests and verify RED** + +```bash +npm --prefix App/backend test -- src/adapters/outbound/skill-writer/templates/memmy-agent-protocol.test.ts src/adapters/outbound/skill-writer/claude-code/tests/target.test.ts src/adapters/outbound/skill-writer/codex/tests/target.test.ts src/adapters/outbound/skill-writer/pi/tests/target.test.ts +``` + +Expected: failure because project context is not separately rendered or persisted. + +- [ ] **Step 3: Update turn-start output rendering** + +Read `started.projectContext` directly. Keep its authoritative marker intact and wrap only supplemental retrieval in `memmy_memory_context`. Do not convert candidates or legacy panel sections into authoritative prose. + +- [ ] **Step 4: Update turn-state provenance** + +Persist: + +```ts +{ + projectContextVersion: turn.projectContext?.version, + projectContextStatus: turn.projectContext?.status, + sourceMemoryIds: turn.sourceMemoryIds +} +``` + +Forward these fields on turn completion. + +- [ ] **Step 5: Run hook tests and verify GREEN** + +Run the command from Step 2. Expected: all selected tests pass. + +- [ ] **Step 6: Commit the hook unit** + +```bash +git add App/backend/src/adapters/outbound/skill-writer/templates App/backend/src/adapters/outbound/skill-writer/claude-code/tests/target.test.ts App/backend/src/adapters/outbound/skill-writer/codex/tests/target.test.ts App/backend/src/adapters/outbound/skill-writer/pi/tests/target.test.ts +git commit -m "feat(hooks): inject authoritative project state" +``` + +--- + +### Task 6: Convert The Context Pack Dialog Into A Governance Surface + +**Files:** +- Modify: `App/frontend/desktop/src/api/memory-runtime-client.ts` +- Modify: `App/frontend/desktop/src/pages/project-context-pack-dialog.tsx` +- Modify: `App/frontend/desktop/src/pages/context-pack-relation-graph.tsx` +- Modify: `App/frontend/desktop/src/i18n/messages.ts` +- Modify: `App/frontend/desktop/src/styles.css` +- Test: `App/frontend/desktop/src/pages/tests/project-context-pack-dialog.interaction.test.tsx` +- Test: `App/frontend/desktop/src/pages/tests/home-page-context-pack.interaction.test.tsx` + +**Interfaces:** +- Consumes Task 4 local API schemas. +- Displays current goal, candidate goal, active focus, work-item list, confirmed facts, version, and evidence relations. +- Commands: approve/edit-and-approve/reject proposal; create work item; select/clear focus; update work-item status and next step. + +- [ ] **Step 1: Write failing interaction tests** + +Test from the user perspective: + +```ts +it("does not label a candidate goal as current before approval", async () => { + await renderDialog({ state: stateWithCandidate("Remember the project") }); + expect(screen.getByText("Pending proposal")).toBeTruthy(); + expect(screen.queryByText("Current goal: Remember the project")).toBeNull(); +}); + +it("approves a proposal and refreshes the authoritative state", async () => { + const approve = vi.fn().mockResolvedValue(activeGoalState()); + await renderDialog({ client: dialogClient({ approveProjectGoal: approve }) }); + click(buttonContaining("Approve")); + await waitFor(() => expect(approve).toHaveBeenCalledOnce()); + expect(screen.getByText("Version 1")).toBeTruthy(); +}); + +it("requires an explicit click to change focused work item", async () => { + await renderDialog({ state: stateWithTwoWorkItems() }); + expect(selectFocus).not.toHaveBeenCalled(); + click(buttonContaining("Focus second item")); + expect(selectFocus).toHaveBeenCalledWith("work-2"); +}); +``` + +Also cover clearing focus, editing long goal details, conflict state, failed mutation feedback, and preserving graph/detail navigation. + +- [ ] **Step 2: Run UI tests and verify RED** + +```bash +npm --prefix App/frontend/desktop test -- src/pages/tests/project-context-pack-dialog.interaction.test.tsx src/pages/tests/home-page-context-pack.interaction.test.tsx +``` + +Expected: failure because governance controls and client methods do not exist. + +- [ ] **Step 3: Extend the frontend Memory client** + +Add exact methods for Task 4 operations using the shared schemas. Keep all mutation state in the dialog and refresh from server responses; do not optimistically declare an unconfirmed goal active. + +- [ ] **Step 4: Implement the dialog states** + +Use unframed sections inside the modal, not nested cards. Provide explicit text controls for approval commands and a standard select/list control for work-item focus. Preserve existing memory details, history restore, copy Markdown, and relation graph behavior. + +- [ ] **Step 5: Add bilingual copy and focused styling** + +Add Chinese and English labels for goal status, proposal review, focus selection, conflicts, no-goal state, and non-blocking no-focus reminder. Keep current visual tokens and dialog radius conventions. + +- [ ] **Step 6: Run UI tests and verify GREEN** + +Run the command from Step 2. Expected: all selected interaction tests pass. + +- [ ] **Step 7: Run the desktop UI and verify visually** + +Start the existing desktop frontend development server through the project process manager, open the context-pack dialog at desktop and narrow viewport sizes, and verify no overlapping text, preserved relation navigation, proposal status clarity, and manual focus controls. + +- [ ] **Step 8: Commit the UI unit** + +```bash +git add App/frontend/desktop/src/api/memory-runtime-client.ts App/frontend/desktop/src/pages/project-context-pack-dialog.tsx App/frontend/desktop/src/pages/context-pack-relation-graph.tsx App/frontend/desktop/src/i18n/messages.ts App/frontend/desktop/src/styles.css App/frontend/desktop/src/pages/tests/project-context-pack-dialog.interaction.test.tsx App/frontend/desktop/src/pages/tests/home-page-context-pack.interaction.test.tsx +git commit -m "feat(ui): govern project context packs" +``` + +--- + +### Task 7: Verify Cross-CLI Persistence End To End + +**Files:** +- Create: `tests/smoke/project-context-pack-smoke.test.ts` +- Modify: `README.zh-CN.md` +- Modify: `README.md` + +**Interfaces:** +- Uses the public Memory HTTP service and rendered Claude Code, Codex, and Pi hooks. +- Proves the completion criterion from the design with one real workspace namespace. + +- [ ] **Step 1: Write the failing smoke scenario** + +The smoke test must: + +1. start an isolated Memory service/database; +2. create and approve a long primary goal and a hard constraint; +3. create two work items and focus the first; +4. invoke the three rendered hook modes with unrelated prompts; +5. assert identical namespace/version and stable authoritative content; +6. focus the second item; +7. invoke another prompt and assert the goal is unchanged while focus changes; +8. create but do not approve a revision and assert it is absent; +9. approve the revision and assert it appears on the next prompt with history retained. + +- [ ] **Step 2: Run the smoke test and verify RED** + +```bash +npx vitest run tests/smoke/project-context-pack-smoke.test.ts +``` + +Expected: failure until the complete API/hook chain is wired. + +- [ ] **Step 3: Fix only integration gaps revealed by the smoke test** + +Do not add new behavior. Correct contract mismatches, missing provenance fields, or namespace propagation errors in the owning modules from Tasks 1-6. + +- [ ] **Step 4: Run the smoke test and verify GREEN** + +Run the command from Step 2. Expected: one passing end-to-end scenario with all three hook modes. + +- [ ] **Step 5: Update user-facing documentation** + +Document: + +- confirmed project goal versus ordinary Memory; +- long goal detail versus stable per-turn summary; +- proposal approval requirement; +- one primary goal with multiple manually selected work items; +- no-focus behavior; +- cross-CLI fixed injection. + +Do not claim automatic goal activation. + +- [ ] **Step 6: Run targeted regression suites** + +```bash +npm --prefix Memory test -- tests/repository/project-context-repository.test.ts tests/service/project-context/project-context-service.test.ts tests/service/session/turn-capture.test.ts tests/contract/memory-rest-service.test.ts +npm --prefix App/backend test -- src/adapters/outbound/memory-client/tests/http-memory-client.test.ts src/adapters/outbound/memory-client/tests/memos-sqlite-memory-client.test.ts src/adapters/outbound/skill-writer/templates/memmy-agent-protocol.test.ts src/adapters/inbound/local-api/tests/agent-runtime-routes.test.ts +npm --prefix App/frontend/desktop test -- src/pages/tests/project-context-pack-dialog.interaction.test.tsx src/pages/tests/home-page-context-pack.interaction.test.tsx +npx vitest run tests/smoke/project-context-pack-smoke.test.ts +``` + +Expected: all selected suites pass with zero failures. + +- [ ] **Step 7: Typecheck affected packages** + +```bash +npm --prefix Memory run typecheck +npm --prefix App/backend run typecheck +npm --prefix App/frontend/desktop run typecheck +``` + +Expected: all commands exit 0. + +- [ ] **Step 8: Commit verification and documentation** + +```bash +git add tests/smoke/project-context-pack-smoke.test.ts README.md README.zh-CN.md +git commit -m "test: verify project context across cli agents" +``` From 80731befe22d196a6d1e6e5e43b6f7a2fd472f4d Mon Sep 17 00:00:00 2001 From: bluewater_cg Date: Mon, 10 Aug 2026 07:10:24 -0700 Subject: [PATCH 20/23] fix(memory): preserve actual injected source ids --- Memory/src/service/session/session-turn-service.ts | 8 ++++---- Memory/tests/service/retrieval/injected-context.test.ts | 4 ++-- Memory/tests/service/retrieval/query-and-filter.test.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Memory/src/service/session/session-turn-service.ts b/Memory/src/service/session/session-turn-service.ts index c9fc56873..83b59b2da 100644 --- a/Memory/src/service/session/session-turn-service.ts +++ b/Memory/src/service/session/session-turn-service.ts @@ -964,11 +964,14 @@ export class SessionTurnService { if (existingRawTurn) { this.deps.assertRawTurnInScope(existingRawTurn, request.namespace); } + const persistedStartResponse = existingRawTurn + ? persistedTurnStartResponse(existingRawTurn) + : undefined; const turnStartRecall = this.deps.repos.runtime.getTurnStartRecallEvent(session.id, turnId); const requestSourceMemoryIds = normalizeCompleteTurnSourceMemoryIds(request); const sourceMemoryIds = requestSourceMemoryIds.length > 0 ? requestSourceMemoryIds - : turnStartRecall?.injectedMemoryIds ?? []; + : persistedStartResponse?.sourceMemoryIds ?? turnStartRecall?.injectedMemoryIds ?? []; const completionRequest = sourceMemoryIds === requestSourceMemoryIds ? request : { ...request, sourceMemoryIds }; @@ -1006,9 +1009,6 @@ export class SessionTurnService { const requestToolCalls = normalizeCompleteTurnToolCalls(completionRequest); const requestToolResults = normalizeCompleteTurnToolResults(completionRequest); const requestArtifacts = normalizeCompleteTurnArtifacts(completionRequest); - const persistedStartResponse = existingRawTurn - ? persistedTurnStartResponse(existingRawTurn) - : undefined; const turnStartPayload = { protocolVersion: request.protocolVersion, provenance: request.provenance, diff --git a/Memory/tests/service/retrieval/injected-context.test.ts b/Memory/tests/service/retrieval/injected-context.test.ts index 219ac90b1..292af6fd1 100644 --- a/Memory/tests/service/retrieval/injected-context.test.ts +++ b/Memory/tests/service/retrieval/injected-context.test.ts @@ -348,8 +348,8 @@ describe("MemoryService / retrieval / injected context", () => { }); expect(prepared.hits.length).toBeGreaterThan(1); - expect(prepared.sourceMemoryIds.length).toBeGreaterThanOrEqual(prepared.hits.length); - expect(prepared.droppedDueToBudget).toEqual([]); + expect(prepared.sourceMemoryIds).toEqual([]); + expect(prepared.droppedDueToBudget.map(({ id }) => id).sort()).toEqual(prepared.hits.map(({ id }) => id).sort()); expect(db.db.prepare( `SELECT turn_id, json_extract(request_json, '$.retrievalMode') AS retrieval_mode diff --git a/Memory/tests/service/retrieval/query-and-filter.test.ts b/Memory/tests/service/retrieval/query-and-filter.test.ts index 0187c92c6..7482e41c3 100644 --- a/Memory/tests/service/retrieval/query-and-filter.test.ts +++ b/Memory/tests/service/retrieval/query-and-filter.test.ts @@ -51,7 +51,7 @@ describe("MemoryService / retrieval / query and filtering", () => { query: "Training turn should not retrieve memory." }); expect(start.hits).toEqual([]); - expect(start.injectedContext.markdown).toBe(""); + expect(start.injectedContext.markdown).toContain(''); expect(start.sourceMemoryIds).toEqual([]); expect(start.status).toContain("memory_search:disabled"); From 544a957e2b76431bac763ac4951e1826fc731e8f Mon Sep 17 00:00:00 2001 From: bluewater_cg Date: Mon, 10 Aug 2026 07:19:43 -0700 Subject: [PATCH 21/23] fix(desktop): preserve governance button typography --- App/frontend/desktop/src/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/App/frontend/desktop/src/styles.css b/App/frontend/desktop/src/styles.css index d8c0ddde5..047a00c64 100644 --- a/App/frontend/desktop/src/styles.css +++ b/App/frontend/desktop/src/styles.css @@ -746,7 +746,7 @@ body.memmy-window-fullscreen { .project-context-governance__row small { color: color-mix(in srgb, var(--color-text-ink) 62%, transparent); font-size: var(--codex-text-xs); overflow-wrap: anywhere; } .project-context-governance__actions { display: flex; flex-shrink: 0; gap: 6px; } .project-context-governance button, -.project-context-governance__status button { display: inline-flex; align-items: center; justify-content: center; gap: 5px; padding: 6px 9px; border: 1px solid color-mix(in srgb, var(--color-border-stone) 70%, transparent); border-radius: var(--radius-input); background: var(--color-bg-elevated); color: var(--color-text-ink); font: inherit; font-size: var(--codex-text-xs); cursor: pointer; } +.project-context-governance__status button { display: inline-flex; align-items: center; justify-content: center; gap: 5px; padding: 6px 9px; border: 1px solid color-mix(in srgb, var(--color-border-stone) 70%, transparent); border-radius: var(--radius-input); background: var(--color-bg-elevated); color: var(--color-text-ink); font-family: inherit; font-size: var(--codex-text-xs); cursor: pointer; } .project-context-governance button:disabled { opacity: .55; cursor: wait; } .project-context-governance .project-context-governance__primary { border-color: transparent; background: var(--color-action-sky); color: white; } .project-context-governance__status { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 12px 0; color: color-mix(in srgb, var(--color-text-ink) 55%, transparent); font-size: var(--codex-text-xs); } From 4100c2264f25044cd6964ecd6c6e987e5c080a5f Mon Sep 17 00:00:00 2001 From: bluewater_cg Date: Mon, 10 Aug 2026 07:19:45 -0700 Subject: [PATCH 22/23] test(backend): align runtime registry expectations --- App/backend/src/tests/index.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/App/backend/src/tests/index.test.ts b/App/backend/src/tests/index.test.ts index e24885d3a..800187412 100644 --- a/App/backend/src/tests/index.test.ts +++ b/App/backend/src/tests/index.test.ts @@ -84,7 +84,10 @@ describe("local api", () => { }); expect(reloadReasons).toEqual([{ reason: "desktop_startup" }]); - expect(backend.runtimeConfig.memory).toEqual({ baseUrl: "http://127.0.0.1:18960" }); + expect(backend.runtimeConfig.memory).toEqual({ + baseUrl: "http://127.0.0.1:18960", + ownership: "managed" + }); }); it("uses the built-in default Cloud client when MEMMY_CLOUD_URL is missing", async () => { @@ -1007,7 +1010,7 @@ describe("local api", () => { } }); - it("exposes the seven built-in agent sources in registry order", async () => { + it("exposes the eight built-in agent sources in registry order", async () => { backend = await createTempBackend(); const response = await fetch(`${backend.runtimeConfig.baseUrl}/api/agent-sources`, { @@ -1022,6 +1025,7 @@ describe("local api", () => { expect.objectContaining({ sourceId: "cursor", displayName: "Cursor" }), expect.objectContaining({ sourceId: "claude_code", displayName: "Claude Code" }), expect.objectContaining({ sourceId: "codex", displayName: "Codex" }), + expect.objectContaining({ sourceId: "pi", displayName: "Pi" }), expect.objectContaining({ sourceId: "opencode", displayName: "Opencode" }), expect.objectContaining({ sourceId: "openclaw", displayName: "OpenClaw" }), expect.objectContaining({ sourceId: "hermes", displayName: "Hermes" }), From 6d03352da4a2578434f9b7f76ebe6843a871bbec Mon Sep 17 00:00:00 2001 From: bluewater_cg Date: Mon, 10 Aug 2026 07:20:18 -0700 Subject: [PATCH 23/23] test(backend): stabilize sqlite suites on WSL --- .../src/infrastructure/app-state-store/tests/index.test.ts | 2 +- App/backend/vitest.config.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/App/backend/src/infrastructure/app-state-store/tests/index.test.ts b/App/backend/src/infrastructure/app-state-store/tests/index.test.ts index aafc529ca..8cbbac2aa 100644 --- a/App/backend/src/infrastructure/app-state-store/tests/index.test.ts +++ b/App/backend/src/infrastructure/app-state-store/tests/index.test.ts @@ -17,7 +17,7 @@ afterEach(() => { } }); -describe("app state store migrations", () => { +describe("app state store migrations", { timeout: 60_000 }, () => { it("creates initial tables and seed rows idempotently", () => { tempDir = mkdtempSync(join(tmpdir(), "memmy-app-state-")); const databasePath = join(tempDir, "app.sqlite"); diff --git a/App/backend/vitest.config.ts b/App/backend/vitest.config.ts index 5bc0dc538..77a10ab07 100644 --- a/App/backend/vitest.config.ts +++ b/App/backend/vitest.config.ts @@ -26,6 +26,11 @@ function findRepoEnvFile(startDir: string): string | null { const envPath = findRepoEnvFile(moduleDir); const parsed = envPath ? (loadDotenv({ path: envPath, processEnv: {} }).parsed ?? {}) : {}; +const configuredMaxWorkers = Number.parseInt(process.env.MEMMY_TEST_MAX_WORKERS ?? "", 10); +const isWindowsMountedWorkspace = process.platform === "linux" && /^\/mnt\/[a-z]\//iu.test(moduleDir); +const maxWorkers = Number.isInteger(configuredMaxWorkers) && configuredMaxWorkers > 0 + ? configuredMaxWorkers + : isWindowsMountedWorkspace ? 1 : 4; const testEnv = { ...parsed, MEMMY_CLOUD_SERVICE: "https://cloud.test.invalid" @@ -33,6 +38,8 @@ const testEnv = { export default defineConfig({ test: { + maxWorkers, + testTimeout: 10_000, env: testEnv, coverage: { provider: "v8",