From 730b02197a60a7931ef952752cd00ea5145c4291 Mon Sep 17 00:00:00 2001 From: bernatsampera Date: Sat, 1 Aug 2026 12:08:51 +0200 Subject: [PATCH] Rework the agent surface: commands, saved scripts, handshake instructions, dashboard; drop flows and chat State changes (from the maat-agent spikes): - run_script runs saved scripts by path with args and params (PARAM_ env vars), plus ad-hoc code; results start with a status header and a missing-package hint points at connection.yaml deps - commands/ folders in connections and modules register as MCP prompts (__), surfaced as slash commands; .md and .py formats - instructions.md is pushed at connect through the MCP handshake and declared as ledger pipe G0: one controllable file is what the agent receives at start - read_context/write_context renamed to read_file/write_file; new list_dir and grep tools - stateless HTTP: server restarts no longer strand attached clients Removed: - flows (never met a real use case; modules plus commands cover process needs; design.md records the return condition) - gcontext chat (redundant once the handshake delivers instructions); the controlled claude invocation is documented in the README instead - docs/templates (duplicated README sections) - the ledger's dual chat/mcp mode, collapsed to one Structure: - server.py is only the MCP surface; concerns split into fs.py, exec.py, secrets.py, state.py, ledger.py, commands.py; agent-facing tool text lives in prompts/tools/*.md - read-only web dashboard served at the root: overview, ledger, files, live activity feed (web/ Vite app, bundled into the wheel) - secrets.env is now unreadable through the agent (read guard) 39 tests. Version 0.4.0. Co-Authored-By: Claude Fable 5 --- .gitignore | 4 + Makefile | 19 + README.md | 78 +- docs/design.md | 46 +- docs/modules.md | 2 +- examples/README.md | 1 - .../archive/modules/legacy-audit/index.md | 2 +- examples/ops-agent/flows/demo-brief/flow.yaml | 33 - examples/ops-agent/gcontext.yaml | 1 + .../support-workflow/commands/refund-reply.md | 12 + .../modules/support-workflow/index.md | 1 + pyproject.toml | 15 +- src/gcontext/cli.py | 261 +- src/gcontext/commands.py | 151 + src/gcontext/dashboard.py | 230 ++ src/gcontext/exec.py | 147 + src/gcontext/flows.py | 113 - src/gcontext/fs.py | 140 + src/gcontext/ledger.py | 51 + src/gcontext/prompts/README.md | 14 + src/gcontext/prompts/tools/grep.md | 9 + src/gcontext/prompts/tools/list_dir.md | 7 + src/gcontext/prompts/tools/overview.md | 1 + src/gcontext/prompts/tools/read_file.md | 3 + src/gcontext/prompts/tools/run_script.md | 19 + src/gcontext/prompts/tools/write_file.md | 8 + src/gcontext/secrets.py | 29 + src/gcontext/server.py | 558 +-- src/gcontext/state.py | 104 + tests/test_commands.py | 111 + tests/test_dashboard.py | 169 + tests/test_flows.py | 71 - tests/test_init.py | 7 +- tests/test_server.py | 113 +- uv.lock | 2 +- web/index.html | 18 + web/package-lock.json | 3424 +++++++++++++++++ web/package.json | 22 + web/public/icon-dark-32x32.png | Bin 0 -> 1119 bytes web/public/icon-dark-48x48.png | Bin 0 -> 1882 bytes web/public/icon-light-32x32.png | Bin 0 -> 1080 bytes web/public/icon-light-48x48.png | Bin 0 -> 1757 bytes web/public/icon.svg | 12 + web/public/logo-black.png | Bin 0 -> 2627 bytes web/public/logo-white.png | Bin 0 -> 2967 bytes web/src/Activity.jsx | 82 + web/src/App.jsx | 74 + web/src/Files.jsx | 106 + web/src/Overview.jsx | 129 + web/src/index.css | 45 + web/src/lib.js | 30 + web/src/main.jsx | 6 + web/src/ui.jsx | 40 + web/vite.config.js | 18 + 54 files changed, 5614 insertions(+), 924 deletions(-) create mode 100644 Makefile delete mode 100644 examples/ops-agent/flows/demo-brief/flow.yaml create mode 100644 examples/ops-agent/modules/support-workflow/commands/refund-reply.md create mode 100644 src/gcontext/commands.py create mode 100644 src/gcontext/dashboard.py create mode 100644 src/gcontext/exec.py delete mode 100644 src/gcontext/flows.py create mode 100644 src/gcontext/fs.py create mode 100644 src/gcontext/ledger.py create mode 100644 src/gcontext/prompts/README.md create mode 100644 src/gcontext/prompts/tools/grep.md create mode 100644 src/gcontext/prompts/tools/list_dir.md create mode 100644 src/gcontext/prompts/tools/overview.md create mode 100644 src/gcontext/prompts/tools/read_file.md create mode 100644 src/gcontext/prompts/tools/run_script.md create mode 100644 src/gcontext/prompts/tools/write_file.md create mode 100644 src/gcontext/secrets.py create mode 100644 src/gcontext/state.py create mode 100644 tests/test_commands.py create mode 100644 tests/test_dashboard.py delete mode 100644 tests/test_flows.py create mode 100644 web/index.html create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/public/icon-dark-32x32.png create mode 100644 web/public/icon-dark-48x48.png create mode 100644 web/public/icon-light-32x32.png create mode 100644 web/public/icon-light-48x48.png create mode 100644 web/public/icon.svg create mode 100644 web/public/logo-black.png create mode 100644 web/public/logo-white.png create mode 100644 web/src/Activity.jsx create mode 100644 web/src/App.jsx create mode 100644 web/src/Files.jsx create mode 100644 web/src/Overview.jsx create mode 100644 web/src/index.css create mode 100644 web/src/lib.js create mode 100644 web/src/main.jsx create mode 100644 web/src/ui.jsx create mode 100644 web/vite.config.js diff --git a/.gitignore b/.gitignore index 39144a5..d639c7f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ dist/ # secret values never leave the machine secrets.env + +# dashboard build artifacts +web/node_modules/ +src/gcontext/web_dist/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a8cf546 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +# Dashboard + package build. The Python package itself needs no Makefile; +# these targets exist because the wheel embeds the built web app. + +.PHONY: web-dev web-build build test + +# Vite dev server on :5179, proxying /api to a running `gcontext up` (:4242 +# by default; override with VITE_API=http://127.0.0.1:). +web-dev: + cd web && npm install && npm run dev + +web-build: + cd web && npm install && npm run build + +# Wheel + sdist. web-build first: hatchling force-includes web/dist. +build: web-build + uv build + +test: + uv run --group dev python -m pytest -q diff --git a/README.md b/README.md index 37b7509..3c13b76 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ claude mcp add --transport http my-agent http://127.0.0.1:4242/mcp ``` my-agent/ gcontext.yaml # name, description, optional port - instructions.md # standing instructions for whatever runtime attaches + instructions.md # pushed to every agent at connect: what it starts with secrets.env # secret values, gitignored connections/ # services the agent can use @@ -41,13 +41,14 @@ my-agent/ index.md # API notes, usage patterns modules/ # accumulated knowledge - flows/ # multi-step work, tracked as files (see below) archive/ # excluded from scanning, still readable ``` -Markdown holds the context, YAML holds the config. Edit any of it with a text editor; the server reads the files on demand, so changes apply immediately. +Markdown holds the context, YAML holds the config. Edit any of it with a text editor; the server reads the files on demand, so changes apply immediately. Two exceptions load at server start and need a restart to pick up edits: `instructions.md` (pushed in the MCP handshake) and command files. -Connected clients get six tools: `overview`, `read_context`, `write_context`, `run_script`, `list_connections`, `flows`. +Connected clients get six tools: `overview`, `read_file`, `write_file`, `list_dir`, `grep`, `run_script`. + +`run_script` runs either ad-hoc code or a saved script by path (`scripts/` folders hold proven procedures, so they are reused instead of rewritten). Files under `connections/*/commands/` and `modules/*/commands/` register as MCP prompts, which Claude Code shows as slash commands; see "Commands" below. ## Your first connection @@ -80,35 +81,26 @@ That's it. The server picks the connection up on the next tool call (no restart) ## Context ledger -`gcontext context` lists every channel through which context reaches the agent, marked as `loaded` (pushed at start), `on demand` (agent pulls it via a visible tool call), `skipped` (closed by a launch flag), or `uncontrolled` (owned by the runtime, outside gcontext's view). gcontext only inserts context through the channels on that list. If you want to know what the agent is seeing, this is the answer. +`gcontext context` lists every channel through which context reaches the agent, marked as `loaded` (pushed at connect), `on demand` (agent pulls it via a visible tool call), `skipped` (nothing to push), or `uncontrolled` (owned by the runtime, outside gcontext's view). gcontext only inserts context through the channels on that list. If you want to know what the agent is seeing, this is the answer. + +## Controlled session + +The ledger marks runtime-owned pipes (the runtime's system prompt, its config files, its other MCP servers) as `uncontrolled`, because gcontext cannot close them. If you want a claude session with those pipes closed, launch claude yourself with its own flags; there is no gcontext command for this, since it is a runtime invocation, not framework behavior: + +```bash +claude --mcp-config '{"mcpServers":{"gcontext":{"type":"http","url":"http://127.0.0.1:4242/mcp"}}}' \ + --strict-mcp-config \ + --setting-sources "" +``` + +`--strict-mcp-config` ignores every other configured MCP server, and `--setting-sources ""` skips CLAUDE.md files and user settings. Your `instructions.md` still arrives through the MCP handshake, like in any session. Adjust the URL to your project's port. ## Secrets -`connection.yaml` declares secret names; `secrets.env` holds the values. When the agent calls `run_script`, the values are injected as environment variables and scrubbed from the script's output. The agent can know that `STRIPE_API_KEY` exists and use it in a script, but never reads the value. `secrets.env` is gitignored by `init` and the `write_context` tool refuses to touch it. +`connection.yaml` declares secret names; `secrets.env` holds the values. When the agent calls `run_script`, the values are injected as environment variables and scrubbed from the script's output. The agent can know that `STRIPE_API_KEY` exists and use it in a script, but never reads the value. `secrets.env` is gitignored by `init` and the `write_file` tool refuses to touch it. `run_script` executes Python in a per-project venv with each connection's declared deps preinstalled (via uv). -## Flows - -A flow is a YAML file describing multi-step work as file dependencies: - -```yaml -steps: - - id: draft - needs: [flows/brief/brief.md] - produces: [flows/brief/draft.md] - instructions: Read the brief, write the draft. -``` - -Step status is derived from the filesystem, like make targets: - -- `blocked`: a needed file doesn't exist -- `ready`: needs exist, produces don't -- `stale`: a needed file was modified after the produced files -- `done`: everything exists and is up to date - -There is no engine and no stored run state. A step is completed by writing the files it declares, whether that's done by an attached runtime, a script, or you in an editor. If an upstream file changes, downstream steps become stale on the next read. `gcontext flows` prints the board; attached clients get the same via the `flows()` tool, which includes step instructions only for steps that are currently actionable. - ## Archiving When old modules or connections start cluttering the context, move them: @@ -121,6 +113,34 @@ Anything under `archive/` is skipped when scanning, but stays readable by path, ## Commands +A command is a user-invokable entry point stored next to the knowledge it belongs to: a file under `connections//commands/` or `modules//commands/`. The server registers each one as an MCP prompt named `__`; Claude Code shows it as a slash command (`/mcp______`). Prompts cost no tool-schema context: a command's text enters the conversation only when you invoke it. + +Two file types: + +- `.md`: YAML frontmatter (description, parameters), then the body that gets injected, with `$name` placeholders filled from the arguments. + + ```markdown + --- + description: Draft a refund reply + parameters: + - name: email + required: true + --- + Draft a refund reply for $email and show it to the user. + ``` + +- `.py`: a runnable script with the same frontmatter as a `# ---` comment block at the top. Invoking it instructs the agent to run the file through `run_script`, with the arguments passed as `params` (they reach the script as `PARAM_` env vars). + +Commands are discovered at server start; restart to pick up new files. + +## Dashboard + +`gcontext up` also serves a read-only dashboard at the server root, for example `http://127.0.0.1:4242/`. It shows the project overview and context ledger, connections with secret status (names only, never values), modules, commands, a file browser, and a live activity feed of every tool call agents make. The feed lives in server memory and empties on restart. The dashboard changes nothing; agents make the changes. + +Developing the dashboard itself needs node: `make web-dev` runs a Vite dev server on `http://localhost:5179` that proxies to the gcontext server, and `make web-build` produces the static bundle that `gcontext up` serves. + +## CLI + | Command | Description | |---|---| | `gcontext init ` | Scaffold a new state folder | @@ -128,12 +148,10 @@ Anything under `archive/` is skipped when scanning, but stays readable by path, | `gcontext status [dir]` | Server state, connected clients, state overview | | `gcontext connect [client]` | Connection steps for claude, desktop, codex, cursor | | `gcontext context [dir]` | Print the context ledger | -| `gcontext flows [dir]` | Print the flow boards | -| `gcontext chat [dir]` | Launch a dedicated claude session against the folder | ## Going further -- [examples/ops-agent](examples/ops-agent): a complete agent folder with connections, modules, a flow, and an archived module +- [examples/ops-agent](examples/ops-agent): a complete agent folder with connections, modules, a command, and an archived module - [docs/design.md](docs/design.md): why gcontext is built this way, decision by decision - [docs/modules.md](docs/modules.md): writing portable, shareable modules diff --git a/docs/design.md b/docs/design.md index 1960f12..530a21e 100644 --- a/docs/design.md +++ b/docs/design.md @@ -8,7 +8,7 @@ Claude Code, Codex, Cursor: these are runtimes. They run the loop, stream tokens gcontext never competes with runtimes, it feeds them. Anything that looks like a message loop, a streaming handler, or a session manager belongs to the runtime. Runtimes are a competitive, fast-moving space owned by large companies; state is not. Runtimes are interchangeable; the state is not. -An earlier version shipped a ~230 line custom chat REPL wrapping `claude -p`. It was deleted: it reimplemented what the runtime ships for free, and a homemade REPL is a runtime. Its replacement, `gcontext chat`, is a small launcher that prints the context ledger and execs the real `claude` with the right flags. The launcher gives more control over context than the REPL ever did, because the runtime's own flags can close pipes the REPL couldn't. +This principle removed two features in sequence. An early version shipped a ~230 line custom chat REPL wrapping `claude -p`; deleted, because a homemade REPL is a runtime. Its replacement, `gcontext chat`, was a launcher that execed the real `claude` with lockdown flags; also deleted, once the handshake started delivering `instructions.md` to every client and the launcher's only remaining job was passing claude-specific flags gcontext has no business owning. What survives is a documented claude invocation in the README ("Controlled session") for anyone who wants those pipes closed. Each step moved the same direction: gcontext feeds runtimes and launches none. ## A folder is the agent's state @@ -20,7 +20,7 @@ The predecessor of this design had typed modules (integration / task / workflow) Each concern has exactly one representation: -- **YAML** (`gcontext.yaml`, `connection.yaml`, `flow.yaml`) is data the framework reads. It is statically parseable: gcontext can list every connection, every required secret, and every flow step without executing anything, without deps installed, without secrets set. +- **YAML** (`gcontext.yaml`, `connection.yaml`) is data the framework reads. It is statically parseable: gcontext can list every connection and every required secret without executing anything, without deps installed, without secrets set. - **Markdown** is context the agent reads. Free-form, no schema, grows however makes sense. - **Python** exists only as scripts the agent writes and runs on demand. @@ -36,7 +36,7 @@ This is the core difference from tool-centric frameworks: there, a developer pre Secret NAMES are declared in `connection.yaml`. Secret VALUES live in `secrets.env`, gitignored, never leaving the machine. -The agent knows `STRIPE_API_KEY` exists and writes `os.environ["STRIPE_API_KEY"]` in scripts, but can never read the value: values are injected as environment variables at execution time and scrubbed from the script's output before the agent sees it. The `write_context` tool refuses to touch `secrets.env`. +The agent knows `STRIPE_API_KEY` exists and writes `os.environ["STRIPE_API_KEY"]` in scripts, but can never read the value: values are injected as environment variables at execution time and scrubbed from the script's output before the agent sees it. The `write_file` tool refuses to touch `secrets.env`. This is the invariant that never changes: secrets never enter the context window. Names are visible, values are local, injected at runtime, scrubbed from output. @@ -52,45 +52,35 @@ Rejected along the way: per-harness adapters that write each client's config (sc The accepted tradeoff: something must be running. -## The context ledger: nothing is pushed invisibly +## The context ledger: everything pushed is declared -Every pipe that inserts context into the agent is enumerated in one ledger, computed live from the folder so it cannot go stale. Each pipe is marked `loaded` (pushed at start), `on demand` (agent pulls it via a visible tool call), `skipped` (closed by a launch flag), or `uncontrolled` (runtime-owned, outside gcontext's view). The ledger appears in `gcontext context`, at `chat` launch, in `overview()`, and after `connect`. +Every pipe that inserts context into the agent is enumerated in one ledger, computed live from the folder so it cannot go stale. Each pipe is marked `loaded` (pushed at connect), `on demand` (agent pulls it via a visible tool call), `skipped` (nothing to push), or `uncontrolled` (runtime-owned, outside gcontext's view). The ledger appears in `gcontext context`, in `overview()`, after `connect`, and in the dashboard. -This section exists because of a reverted feature. instructions.md was once wired into the MCP handshake's `instructions` field, so any connecting client received it automatically. It worked, and it was reverted the same day. gcontext's entire promise is legibility of context: you look at the folder and you know what the agent knows. Content that teleports into the agent's head through a handshake side channel is invisible; a user debugging their agent cannot tell where an instruction came from or that it was loaded at all. +The one thing gcontext pushes at connect is `instructions.md`, through the MCP handshake's `instructions` field, declared as ledger pipe G0. This is the design's answer to "what does the agent receive when it attaches": one file, in the folder, versioned with git, and nothing else. Edit it and you have edited what every future session starts with. -The invariant: a context management system must make context flow explicit and inspectable. Convenience never justifies a hidden push. Consequence: in attach mode, instructions.md is not auto-loaded; the ledger and `overview()` tell the agent to read it. +This position was reached in two steps. The handshake push was first rejected outright, on the argument that content arriving through a side channel is invisible in the conversation. Living with the alternative showed the real cost: the agent started blind, had to be told (via `overview()`) to read its own instructions, and a runtime that never asked never saw them. The rejection was aimed at the wrong target. The problem was never pushing at connect; it was pushing without declaring. So the invariant is: everything pushed is declared in the ledger, and everything declared is a file you control. The ledger is also honest about its limits. When a runtime keeps pipes gcontext cannot close (its own system prompt, its config files, other MCP servers), the ledger marks them UNCONTROLLED instead of pretending the session is cleaner than it is. -## Flows: reactive state, not reactive execution - -The tempting design was a workflow engine inside gcontext: watch state, fire LLM calls, run steps. That would make gcontext a runtime and an orchestrator, competing with graph frameworks and the harnesses themselves. Rejected. - -Instead, flows are data. A step declares `needs` and `produces` (file paths), and status is a pure function of the filesystem, computed on read with make-style mtime staleness: `blocked`, `ready`, `stale`, `done`. No run state is stored anywhere. - -Everything else falls out for free: - -- Completing a step IS writing its declared files. There is no `complete_step` call, so there is no stored state to drift from reality. -- Reactivity with zero machinery: change an upstream file and downstream steps become stale on the next read. -- Progress is git-diffable and revertible, because progress is files. -- Any runtime, several at once, a script, or a human with a text editor can advance the same flow. - -A graph engine keeps state in a checkpointer and drives execution; gcontext keeps state in files and drives nothing. - -The `flows()` tool shows every step's status but includes instructions only for actionable (ready or stale) steps: the runtime receives exactly the work the current state unlocks. This stays inside the legibility invariant because it is a pull, and the deferral rule is itself declared in the ledger. - ## Archive: a location, not metadata State accumulates until it pollutes the context and the agent can't find the right thing. The rejected fix was a `surface: active | background | archived` field with agent-driven housekeeping and staleness hints: too much magic. gcontext must have no background or automatic behaviors; humans must always see how state is controlled. -So visibility is a function of file location, the same way flow status is a function of file existence. Move a folder into `archive/` and it stops being scanned, while staying readable by path, and every summary mentions what's archived so nothing disappears silently. A folder move is an action everyone understands, is visible in git, and cannot happen behind anyone's back. gcontext never moves, archives, or deletes anything on its own. +So visibility is a function of file location. Move a folder into `archive/` and it stops being scanned, while staying readable by path, and every summary mentions what's archived so nothing disappears silently. A folder move is an action everyone understands, is visible in git, and cannot happen behind anyone's back. gcontext never moves, archives, or deletes anything on its own. + +## Scripts and commands: knowledge that graduated + +Two features share one idea: when the agent produces something that works, keep it as a file next to the knowledge it belongs to, and reuse it instead of regenerating it. + +A **saved script** is a proven procedure under a `scripts/` folder, run by path through `run_script` (with `args` and named `params` that arrive as `PARAM_` env vars). Writing it is an ordinary `write_file` call, visible like every other state change. + +A **command** is a user-invokable entry point under a `commands/` folder, registered as an MCP prompt named `__` and surfaced by Claude Code as a slash command. Commands are prompts, not tools, on purpose: a tool's schema is pushed into context at connect time for every session, while a prompt is only listed, and its text enters the conversation exactly when the user invokes it. That keeps the tool list at five and honors the no-invisible-push rule: the injection is user-triggered and the ledger lists commands as their own pipe. ## Deferred, deliberately - **Remote variant**: same model, URL plus token, for a served-anywhere agent. The URL transport is the bridge to it. - **Triggers/watchers**: something that pokes a runtime when state changes. Even then, gcontext would trigger a runtime, never run the loop. -- **Multi-instance flow runs** (the same flow over many tickets): needs a parametrization story, waiting for a real need. -- **Non-file flow conditions** (a step gated on a secret being filled): plausible, waiting for a real need. +- **Flows**: an earlier release shipped declarative multi-step work (steps declaring `needs`/`produces` files, status computed from the filesystem). It was removed because no real process ever needed it: a module with a steps file, entered through a command, covered every case that came up. The idea returns only if a real recurring process appears that modules plus commands cannot express, and it will be designed against that process. ## Principles @@ -100,6 +90,6 @@ So visibility is a function of file location, the same way flow status is a func 4. **Static metadata, dynamic execution.** Everything is inspectable without running anything; scripts run only when explicitly invoked. 5. **The agent is already smart, it just needs the right information.** Good context plus usable credentials beats pre-built tools. 6. **Secrets never enter the context window.** -7. **Nothing is pushed invisibly.** Every piece of context is a file in the folder, a visible tool result, or explicitly listed as runtime-owned. +7. **Everything pushed is declared.** Every piece of context is a file in the folder reaching the agent through a ledger-declared pipe, a visible tool result, or explicitly listed as runtime-owned. 8. **Status is a pure function of the filesystem.** Flow progress, archive visibility, connection readiness: all derived from files on read, never stored and synced. 9. **Honesty over the illusion of control.** Uncontrolled pipes are labeled uncontrolled. diff --git a/docs/modules.md b/docs/modules.md index 0f018d9..f93eeaa 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -44,7 +44,7 @@ modules/company/ infrastructure.md # how things are deployed ``` -There is no enforced schema beyond `index.md`. Different modules have different structures depending on what they do. +There is no enforced schema beyond `index.md`. Different modules have different structures depending on what they do. When `index.md` gets long, split it into more files and link them from `index.md`. ## How someone uses a module diff --git a/examples/README.md b/examples/README.md index 688f1d2..467d4f1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -9,7 +9,6 @@ An operations agent for a fictional SaaS company. It shows every part of the fol - `connections/stripe/`, `connections/cloudflare/`: connection config plus API context docs - `modules/company/`: a knowledge module (team, infrastructure) - `modules/support-workflow/`: a process module with steps, empty playbooks and logs that fill up with use -- `flows/demo-brief/`: a three-step flow (capture, draft, finalize) - `archive/modules/legacy-audit/`: an archived module, out of every scan but still readable by path Run it: diff --git a/examples/ops-agent/archive/modules/legacy-audit/index.md b/examples/ops-agent/archive/modules/legacy-audit/index.md index 6db8dc1..748a4e2 100644 --- a/examples/ops-agent/archive/modules/legacy-audit/index.md +++ b/examples/ops-agent/archive/modules/legacy-audit/index.md @@ -2,7 +2,7 @@ Example of an archived module. It sits under archive/modules/, so it is never scanned into overview(), status, or the ledger counts. It stays readable by -path: read_context("archive/modules/legacy-audit/index.md"). +path: read_file("archive/modules/legacy-audit/index.md"). To bring it back, move the folder to modules/legacy-audit/. Archiving is a plain folder move, nothing more. diff --git a/examples/ops-agent/flows/demo-brief/flow.yaml b/examples/ops-agent/flows/demo-brief/flow.yaml deleted file mode 100644 index 30f749f..0000000 --- a/examples/ops-agent/flows/demo-brief/flow.yaml +++ /dev/null @@ -1,33 +0,0 @@ -name: demo-brief -description: Dummy flow to exercise v4 reactive state. Capture a brief, draft from it, then finalize. -steps: - - id: capture - description: Capture what the user wants covered - produces: - - flows/demo-brief/brief.md - instructions: | - Ask the user what they want a short write-up about: topic, audience, - and the three points that must appear. Write their answers to - flows/demo-brief/brief.md as a short markdown brief. - - - id: draft - description: Draft the write-up from the brief - needs: - - flows/demo-brief/brief.md - produces: - - flows/demo-brief/draft.md - instructions: | - Read flows/demo-brief/brief.md and write a first draft (150-250 words) - to flows/demo-brief/draft.md. Cover every point in the brief. - - - id: finalize - description: Polish the draft into the final version - needs: - - flows/demo-brief/brief.md - - flows/demo-brief/draft.md - produces: - - flows/demo-brief/final.md - instructions: | - Read the brief and the draft, tighten the language, and write the final - version to flows/demo-brief/final.md. If the brief changed since the - draft was written, this step shows as stale: redo it from the current brief. diff --git a/examples/ops-agent/gcontext.yaml b/examples/ops-agent/gcontext.yaml index 1f24a65..d195c73 100644 --- a/examples/ops-agent/gcontext.yaml +++ b/examples/ops-agent/gcontext.yaml @@ -1,2 +1,3 @@ name: ops-agent description: An operations agent that manages Stripe billing and Cloudflare infrastructure. +port: 4299 diff --git a/examples/ops-agent/modules/support-workflow/commands/refund-reply.md b/examples/ops-agent/modules/support-workflow/commands/refund-reply.md new file mode 100644 index 0000000..47516cd --- /dev/null +++ b/examples/ops-agent/modules/support-workflow/commands/refund-reply.md @@ -0,0 +1,12 @@ +--- +description: Draft a refund reply for a customer, following the support workflow +parameters: + - name: email + description: The customer's email address + required: true +--- +A customer with email $email asked for a refund. Follow the support workflow +in modules/support-workflow/steps.md: look the customer up in stripe first, +then draft a reply using the closest playbook under +modules/support-workflow/playbooks/. Show the draft to the user before +anything is sent or refunded. diff --git a/examples/ops-agent/modules/support-workflow/index.md b/examples/ops-agent/modules/support-workflow/index.md index e0081a7..71b8adb 100644 --- a/examples/ops-agent/modules/support-workflow/index.md +++ b/examples/ops-agent/modules/support-workflow/index.md @@ -6,3 +6,4 @@ Read [steps.md](steps.md) for the process. Playbooks are built over time as issu - steps.md: the 5-phase workflow - playbooks/: reusable procedures, accumulated per company - logs/: resolution records +- commands/: slash-command entry points (refund-reply) diff --git a/pyproject.toml b/pyproject.toml index 353ca22..d8981db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "gcontext-ai" -version = "0.3.1" +version = "0.4.0" description = "The framework for building stateful agents. Your agent is a folder of state, served over MCP, used from any runtime." readme = "README.md" license = { text = "MIT" } @@ -28,3 +28,16 @@ build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/gcontext"] + +# The built dashboard rides inside the wheel; hatchling fails the build if +# web/dist is missing, so `make build` (vite first) is the only build path. +# artifacts: web/dist is gitignored, and hatchling drops VCS-ignored files +# unless they are declared as build artifacts. +[tool.hatch.build] +artifacts = ["web/dist"] + +[tool.hatch.build.targets.wheel.force-include] +"web/dist" = "gcontext/web_dist" + +[tool.hatch.build.targets.sdist] +only-include = ["src", "web/dist", "tests", "README.md", "LICENSE"] diff --git a/src/gcontext/cli.py b/src/gcontext/cli.py index 80d6163..790e82f 100644 --- a/src/gcontext/cli.py +++ b/src/gcontext/cli.py @@ -3,16 +3,16 @@ import argparse import json import socket -import subprocess import sys -import tempfile -import time import urllib.error import urllib.request from pathlib import Path -from . import flows as flows_mod +from . import exec as exec_mod +from . import ledger as ledger_mod +from . import secrets as secrets_mod from . import server +from . import state BOLD = "\033[1m" DIM = "\033[2m" @@ -30,8 +30,8 @@ STATUS_COLOR = { } -def print_ledger(mode: str): - for i, pipe in enumerate(server.build_ledger(mode), 1): +def print_ledger(project_dir: Path): + for i, pipe in enumerate(ledger_mod.build(project_dir), 1): color = STATUS_COLOR.get(pipe["status"], "") label = f"{pipe['label']} ".ljust(36, ".") status = pipe["status"].upper() if pipe["status"] == "uncontrolled" else pipe["status"] @@ -48,12 +48,14 @@ INIT_INSTRUCTIONS = """\ # Instructions You are the agent for this gcontext project. Your state lives in this folder: -read it with read_context, keep it current with write_context. +read it with read_file, keep it current with write_file, find things with +list_dir and grep. -- Call overview() first to see connections, modules, flows, and the context ledger. -- Call flows() to see multi-step work and what is actionable right now. +- Call overview() first to see connections, modules, and the context ledger. - Use run_script for anything that needs an API: secrets are injected as env vars (you only ever see their names), deps are preinstalled. +- When a script proves itself, save it with write_file under a scripts/ + folder and run it by path from then on, instead of rewriting it. - Record what you learn: update the relevant index.md or module so the next session starts smarter than this one. """ @@ -69,40 +71,6 @@ secrets.env .venv/ """ -INIT_FLOW_YAML = """\ -name: demo-brief -description: Demo flow. Capture a brief, draft from it, then finalize. -steps: - - id: capture - description: Capture what the user wants covered - produces: - - flows/demo-brief/brief.md - instructions: | - Ask the user what they want a short write-up about and save the answers - to flows/demo-brief/brief.md as a short markdown brief. - - - id: draft - description: Draft the write-up from the brief - needs: - - flows/demo-brief/brief.md - produces: - - flows/demo-brief/draft.md - instructions: | - Read the brief and write a first draft to flows/demo-brief/draft.md. - - - id: finalize - description: Polish the draft into the final version - needs: - - flows/demo-brief/brief.md - - flows/demo-brief/draft.md - produces: - - flows/demo-brief/final.md - instructions: | - Tighten the draft into flows/demo-brief/final.md. If the brief changed - since the draft, this step shows as stale: redo it from the current brief. -""" - - def cmd_init(args): target = Path(args.directory).resolve() if target.exists() and any(target.iterdir()): @@ -116,7 +84,6 @@ def cmd_init(args): "secrets.env": INIT_SECRETS, ".gitignore": INIT_AGENT_GITIGNORE, "connections/.gitkeep": "", - "flows/demo-brief/flow.yaml": INIT_FLOW_YAML, "modules/.gitkeep": "", "archive/.gitkeep": "", } @@ -132,7 +99,6 @@ def cmd_init(args): print("Next steps:") print(f" 1. gcontext up {args.directory} start the server") print(f" 2. gcontext connect claude attach a harness (or: desktop, codex, cursor)") - print(f" 3. gcontext chat {args.directory} or talk to a dedicated, fully controlled session") print() print("Give the agent its first connection (any service with an API):") print(f" {args.directory}/connections//connection.yaml secret NAMEs + Python deps") @@ -151,10 +117,10 @@ def find_project_dir(path: str | None) -> Path: sys.exit(1) -def resolve_port(args) -> int: +def resolve_port(args, project_dir: Path) -> int: if getattr(args, "port", None): return args.port - config = server._load_gcontext_yaml() + config = state.load_gcontext_yaml(project_dir) return int(config.get("port", DEFAULT_PORT)) @@ -209,10 +175,10 @@ def fetch_status(port: int) -> dict | None: def cmd_up(args): project_dir = find_project_dir(args.project) server.PROJECT_DIR = project_dir - config = server._load_gcontext_yaml() + config = state.load_gcontext_yaml(project_dir) name = config.get("name", project_dir.name) configured = config.get("port") - port = resolve_port(args) + port = resolve_port(args, project_dir) if not port_is_free(port): running = fetch_status(port) @@ -233,12 +199,15 @@ def cmd_up(args): url = server_url(port) - server.ensure_venv() + exec_mod.ensure_venv(project_dir) + n_commands = server.register_commands() + n_instruction_lines = server.load_instructions() print(f"{BOLD}gcontext{RESET} {DIM}-{RESET} {name}") print(f"{DIM}State: {project_dir}{RESET}") print() print(f"Serving at {BOLD}{url}{RESET}") + print(f"Dashboard: http://127.0.0.1:{port}/") print() print("Connect a harness (once per harness, works from any directory):") print(f" Claude Code: claude mcp add --transport http {name} {url}") @@ -247,25 +216,31 @@ def cmd_up(args): print(f' Codex: [mcp_servers.{name}] url = "{url}" in ~/.codex/config.toml') print(" Details: gcontext connect") print() + if n_instruction_lines: + print(f"Instructions: instructions.md ({n_instruction_lines} lines) is pushed to every agent at connect.") + else: + print(f"{YELLOW}Instructions: no instructions.md, agents receive nothing at connect.{RESET}") + if n_commands: + print(f"Commands: {n_commands} registered as MCP prompts (slash commands in Claude Code).") + print() print("Connections appear below as harnesses attach. Ctrl+C stops the server,") print("and every harness cleanly loses access.") print() server.mcp.run( transport="http", host="127.0.0.1", port=port, path="/mcp", - show_banner=False, log_level="warning", + show_banner=False, log_level="warning", stateless_http=True, ) def cmd_status(args): project_dir = find_project_dir(args.project) - server.PROJECT_DIR = project_dir - config = server._load_gcontext_yaml() - connections = server._load_connections() - secrets = server._load_secrets_env() - modules = server._discover_modules() - port = resolve_port(args) + config = state.load_gcontext_yaml(project_dir) + connections = state.load_connections(project_dir) + secrets = secrets_mod.load(project_dir) + modules = state.discover_modules(project_dir) + port = resolve_port(args, project_dir) name = config.get("name", project_dir.name) desc = config.get("description", "") @@ -315,21 +290,7 @@ def cmd_status(args): print(f" {mname}{suffix}") print() - all_flows = flows_mod.load_flows(project_dir) - if all_flows: - print("Flows:") - for fname, flow in all_flows.items(): - board = flows_mod.flow_board(project_dir, flow) - done = sum(1 for s in board if s["status"] == "done") - ready = [s["id"] for s in flows_mod.actionable(board)] - if ready: - print(f" {fname}: {done}/{len(board)} done, {GREEN}actionable: {', '.join(ready)}{RESET}") - else: - print(f" {fname}: {done}/{len(board)} done") - print(f" {DIM}details: gcontext flows{RESET}") - print() - - archived_line = server._archived_line() + archived_line = state.archived_line(project_dir) if archived_line: print(f"{DIM}{archived_line}{RESET}") print() @@ -339,10 +300,9 @@ def cmd_status(args): def cmd_connect(args): project_dir = find_project_dir(args.project) - server.PROJECT_DIR = project_dir - config = server._load_gcontext_yaml() + config = state.load_gcontext_yaml(project_dir) name = config.get("name", project_dir.name) - port = resolve_port(args) + port = resolve_port(args, project_dir) url = server_url(port) live = fetch_status(port) @@ -393,154 +353,20 @@ def cmd_connect(args): print() print("Context this client will receive:") - print_ledger("mcp") + print_ledger(project_dir) print() print(f"{DIM}Verify anytime with: gcontext status{RESET}") def cmd_context(args): project_dir = find_project_dir(args.project) - server.PROJECT_DIR = project_dir - config = server._load_gcontext_yaml() + config = state.load_gcontext_yaml(project_dir) name = config.get("name", project_dir.name) print(f"{BOLD}gcontext{RESET} {DIM}-{RESET} {name}") - print(f"{DIM}Every pipe that inserts context into the agent, per mode.{RESET}") + print(f"{DIM}Every pipe that inserts context into an attached agent.{RESET}") print() - print(f"{BOLD}gcontext chat{RESET} {DIM}(dedicated claude, fully controlled){RESET}") - print_ledger("chat") - print() - print(f"{BOLD}MCP attach{RESET} {DIM}(any harness pointed at the URL, shared agent){RESET}") - print_ledger("mcp") - - -FLOW_STATUS_COLOR = { - "ready": GREEN, - "stale": YELLOW, - "blocked": DIM, - "done": DIM, -} - - -def cmd_flows(args): - project_dir = find_project_dir(args.project) - server.PROJECT_DIR = project_dir - config = server._load_gcontext_yaml() - name = config.get("name", project_dir.name) - - all_flows = flows_mod.load_flows(project_dir) - print(f"{BOLD}gcontext{RESET} {DIM}-{RESET} {name}") - print(f"{DIM}Flow state is computed from files, nothing else tracks progress.{RESET}") - print() - - if not all_flows: - print(f"{DIM}No flows defined in flows/*/flow.yaml{RESET}") - return - - if args.flow: - if args.flow not in all_flows: - print(f"Error: no flow named {args.flow}. Available: {', '.join(all_flows)}", file=sys.stderr) - sys.exit(1) - all_flows = {args.flow: all_flows[args.flow]} - - for flow in all_flows.values(): - board = flows_mod.flow_board(project_dir, flow) - done = sum(1 for s in board if s["status"] == "done") - print(f"{BOLD}{flow.name}{RESET} {DIM}({done}/{len(board)} done){RESET}") - if flow.description: - print(f"{DIM}{flow.description}{RESET}") - for step in board: - color = FLOW_STATUS_COLOR.get(step["status"], "") - status = f"{color}{step['status']:<7}{RESET}" - print(f" {status} {step['id']}: {step['description']}") - if step["status"] == "blocked": - print(f" {DIM}waiting on: {', '.join(step['missing'])}{RESET}") - elif step["status"] == "ready": - print(f" {DIM}complete by writing: {', '.join(step['missing'])}{RESET}") - elif step["status"] == "stale": - print(f" {YELLOW}{', '.join(step['stale_needs'])} changed after the produces were written{RESET}") - print() - - -CHAT_TOOLS = ",".join( - f"mcp__gcontext__{t}" - for t in ["overview", "read_context", "write_context", "run_script", "list_connections", "flows"] -) - - -def wait_for_server(port: int, project_dir: Path, timeout: float = 15.0) -> bool: - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - live = fetch_status(port) - if live is not None and live.get("project_dir") == str(project_dir.resolve()): - return True - time.sleep(0.3) - return False - - -def cmd_chat(args): - project_dir = find_project_dir(args.project) - server.PROJECT_DIR = project_dir - config = server._load_gcontext_yaml() - name = config.get("name", project_dir.name) - port = resolve_port(args) - url = server_url(port) - - print(f"{BOLD}gcontext{RESET} {DIM}-{RESET} {name}") - print() - print("Context loaded into this session:") - print_ledger("chat") - print() - - own_server = None - live = fetch_status(port) - if live is not None and live.get("project_dir") != str(project_dir.resolve()): - print(f"Error: port {port} is serving a different project ({live.get('name', '?')})", file=sys.stderr) - sys.exit(1) - if live is None: - print(f"{DIM}Starting server at {url}...{RESET}") - own_server = subprocess.Popen( - [sys.executable, "-m", "gcontext.cli", "up", str(project_dir), "--port", str(port)], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - if not wait_for_server(port, project_dir): - own_server.terminate() - print("Error: server did not come up.", file=sys.stderr) - sys.exit(1) - else: - print(f"{DIM}Using the already running server at {url}{RESET}") - - mcp_config = {"mcpServers": {"gcontext": {"type": "http", "url": url}}} - with tempfile.NamedTemporaryFile( - mode="w", suffix=".json", delete=False, prefix="gcontext-mcp-" - ) as f: - json.dump(mcp_config, f) - mcp_config_path = f.name - - cmd = [ - "claude", - "--mcp-config", mcp_config_path, - "--strict-mcp-config", - "--setting-sources", "", - "--allowedTools", CHAT_TOOLS, - ] - instructions = project_dir / "instructions.md" - if instructions.exists(): - cmd.extend(["--system-prompt", instructions.read_text()]) - - print(f"{DIM}Starting claude...{RESET}") - try: - subprocess.run(cmd, cwd=project_dir) - finally: - Path(mcp_config_path).unlink(missing_ok=True) - if own_server is not None: - own_server.terminate() - try: - own_server.wait(timeout=5) - except subprocess.TimeoutExpired: - own_server.kill() - print(f"{DIM}Stopped the session's server.{RESET}") + print_ledger(project_dir) def main(): @@ -576,13 +402,6 @@ def main(): context_parser = subparsers.add_parser("context", help="Show the context ledger: every pipe into the agent, per mode") add_common(context_parser) - flows_parser = subparsers.add_parser("flows", help="Show flow boards: step status computed from the files") - flows_parser.add_argument("--flow", help="Show a single flow by name") - add_common(flows_parser) - - chat_parser = subparsers.add_parser("chat", help="Launch a dedicated claude session against this project") - add_common(chat_parser) - args = parser.parse_args() commands = { @@ -591,8 +410,6 @@ def main(): "status": cmd_status, "connect": cmd_connect, "context": cmd_context, - "flows": cmd_flows, - "chat": cmd_chat, } if args.command in commands: commands[args.command](args) diff --git a/src/gcontext/commands.py b/src/gcontext/commands.py new file mode 100644 index 0000000..4b2bb2e --- /dev/null +++ b/src/gcontext/commands.py @@ -0,0 +1,151 @@ +"""Commands: files under `connections/*/commands/` and `modules/*/commands/` +exposed as MCP prompts. + +Two file types (design ported from the maat-agent S13 spike). Both surface as +slash commands in Claude Code (`/mcp______`); neither +adds a tool, so the tool list stays at the six generic tools and the command +text enters context only when the user invokes it. + +- `.md` (prompt command): the rendered body is injected into the conversation + and the agent acts on it. `$name` placeholders are filled from the prompt + arguments declared in the frontmatter. +- `.py` (script command): the injected text instructs the agent to execute the + file through the generic `run_script` tool, passing the arguments as + `params` (which the server turns into `PARAM_` environment variables). + +Commands are discovered once at server startup; restart to pick up new files. +""" + +from __future__ import annotations + +import inspect +import sys +from pathlib import Path +from string import Template +from typing import Any + +import yaml + +FRONTMATTER_DELIM = "---" +COMMAND_GLOBS = ("connections/*/commands/*", "modules/*/commands/*") + + +def parse_command(text: str) -> tuple[dict[str, Any], str]: + """Split a `.md` command file into (frontmatter, body). + + The file must start with a `---` YAML block. Raises ValueError otherwise, + so a malformed file fails loudly at startup instead of silently missing + from the prompt list. + """ + lines = text.split("\n") + if not lines or lines[0].strip() != FRONTMATTER_DELIM: + raise ValueError("missing frontmatter: file must start with ---") + try: + end = next(i for i, ln in enumerate(lines[1:], 1) if ln.strip() == FRONTMATTER_DELIM) + except StopIteration: + raise ValueError("unterminated frontmatter: no closing ---") + meta = yaml.safe_load("\n".join(lines[1:end])) or {} + if not isinstance(meta, dict): + raise ValueError("frontmatter must be a YAML mapping") + body = "\n".join(lines[end + 1 :]).strip() + return meta, body + + +def parse_script_command(text: str) -> dict[str, Any]: + """Read the frontmatter of a `.py` command: a `# ---` comment block at the top. + + # --- + # description: ... + # parameters: + # - name: email + # required: true + # --- + """ + lines = text.split("\n") + if not lines or lines[0].strip() != f"# {FRONTMATTER_DELIM}": + raise ValueError("missing frontmatter: file must start with # ---") + block: list[str] = [] + for line in lines[1:]: + if line.strip() == f"# {FRONTMATTER_DELIM}": + meta = yaml.safe_load("\n".join(block)) or {} + if not isinstance(meta, dict): + raise ValueError("frontmatter must be a YAML mapping") + return meta + if not line.startswith("#"): + raise ValueError("non-comment line inside frontmatter block") + block.append(line[1:].removeprefix(" ")) + raise ValueError("unterminated frontmatter: no closing # ---") + + +def _script_prompt_body(rel_path: str, meta: dict[str, Any]) -> str: + """The injected text for a script command invoked as a slash command.""" + params = meta.get("parameters") or [] + if params: + rendered = ", ".join(f'"{p["name"]}": "${p["name"]}"' for p in params) + params_line = f" and params {{{rendered}}}" + else: + params_line = "" + return ( + f"Execute the script command `{rel_path}`: call the `run_script` tool " + f"with path `{rel_path}`{params_line}, then report its output to the " + "user. Do not read or rewrite the script first; run it as is." + ) + + +def _render_fn(body: str, params: list[dict[str, Any]]): + """A render function whose signature carries the declared parameters, so + FastMCP derives the prompt arguments (and rejects missing required ones).""" + + def render(**kwargs: str) -> str: + return Template(body).safe_substitute(**kwargs) + + sig_params = [ + inspect.Parameter( + p["name"], + inspect.Parameter.KEYWORD_ONLY, + default=inspect.Parameter.empty if p.get("required", False) else "", + annotation=str, + ) + for p in params + ] + render.__signature__ = inspect.Signature(sig_params) + render.__annotations__ = {p["name"]: str for p in params} | {"return": str} + return render + + +def discover(root: Path) -> list[Path]: + """Command files in registration order.""" + return sorted( + p + for pattern in COMMAND_GLOBS + for p in root.glob(pattern) + if p.suffix in (".md", ".py") + ) + + +def register_commands(mcp, root: Path) -> int: + """Scan connection and module `commands/` folders and register each file + as a prompt named `__`.""" + from fastmcp.prompts.prompt import Prompt + + count = 0 + for path in discover(root): + owner = path.parent.parent.name + name = f"{owner}__{path.stem}" + try: + text = path.read_text(encoding="utf-8") + if path.suffix == ".md": + meta, body = parse_command(text) + else: + meta = parse_script_command(text) + body = _script_prompt_body(str(path.relative_to(root)), meta) + fn = _render_fn(body, meta.get("parameters") or []) + fn.__name__ = name + mcp.add_prompt( + Prompt.from_function(fn, name=name, description=meta.get("description", "")) + ) + except (ValueError, KeyError, yaml.YAMLError) as e: + print(f" ! skipping command {path}: {e}", file=sys.stderr) + continue + count += 1 + return count diff --git a/src/gcontext/dashboard.py b/src/gcontext/dashboard.py new file mode 100644 index 0000000..e41b306 --- /dev/null +++ b/src/gcontext/dashboard.py @@ -0,0 +1,230 @@ +"""The local dashboard: read-only JSON API plus the built web app. + +Registered on the same server as the MCP endpoint, so `gcontext up` serves +the dashboard at / while agents talk to /mcp. Everything here is a pure read +of the project folder or of in-memory server state (SESSIONS, EVENTS). +Nothing writes; secret values never leave secrets.py as anything but +presence booleans. + +Route order matters: fastmcp appends custom routes after the MCP routes in +registration order, so the /{path:path} catch-all at the bottom of this file +must stay last. +""" + +from importlib import metadata +from pathlib import Path + +import yaml + +from starlette.requests import Request +from starlette.responses import FileResponse, JSONResponse, PlainTextResponse + +from . import commands as commands_mod +from . import fs +from . import ledger as ledger_mod +from . import secrets as secrets_mod +from . import server +from . import state + +mcp = server.mcp + +# cli.py mutates server.PROJECT_DIR after import; always read it via the module. + + +def _root() -> Path: + return server.PROJECT_DIR + + +def _version() -> str: + try: + return metadata.version("gcontext-ai") + except metadata.PackageNotFoundError: + return "dev" + + +@mcp.custom_route("/api/project", methods=["GET"]) +async def api_project(request: Request) -> JSONResponse: + root = _root() + config = state.load_gcontext_yaml(root) + instructions = root / "instructions.md" + return JSONResponse({ + "name": config.get("name", root.name), + "description": config.get("description", ""), + "project_dir": str(root.resolve()), + "has_instructions": instructions.exists(), + "instructions_lines": len(instructions.read_text().splitlines()) if instructions.exists() else 0, + "archived": state.archived(root), + "version": _version(), + }) + + +@mcp.custom_route("/api/connections", methods=["GET"]) +async def api_connections(request: Request) -> JSONResponse: + root = _root() + secrets = secrets_mod.load(root) + result = [] + for cname, conn in state.load_connections(root).items(): + secret_status = [ + {"name": s, "filled": bool(secrets.get(s))} for s in conn.secrets + ] + result.append({ + "name": cname, + "description": conn.description, + "deps": conn.deps, + "secrets": secret_status, + "ready": all(s["filled"] for s in secret_status), + "files": state.connection_files(root, cname), + }) + return JSONResponse(result) + + +@mcp.custom_route("/api/modules", methods=["GET"]) +async def api_modules(request: Request) -> JSONResponse: + root = _root() + result = [] + for mname, mod in state.discover_modules(root).items(): + result.append({ + "name": mname, + "description": mod.description, + "version": mod.version, + "tags": mod.tags, + "files": state.module_files(root, mname), + }) + return JSONResponse(result) + + +@mcp.custom_route("/api/commands", methods=["GET"]) +async def api_commands(request: Request) -> JSONResponse: + root = _root() + result = [] + for path in commands_mod.discover(root): + rel = str(path.relative_to(root)) + owner = path.parent.parent.name + entry = { + "owner": owner, + "name": f"{owner}__{path.stem}", + "kind": path.suffix.lstrip("."), + "path": rel, + } + try: + text = path.read_text(encoding="utf-8") + if path.suffix == ".md": + meta, _ = commands_mod.parse_command(text) + else: + meta = commands_mod.parse_script_command(text) + entry["description"] = meta.get("description", "") + entry["args"] = [ + { + "name": p.get("name", "?"), + "description": p.get("description", ""), + "required": bool(p.get("required", False)), + } + for p in (meta.get("parameters") or []) + ] + except (ValueError, KeyError, yaml.YAMLError) as e: + entry["error"] = str(e) + result.append(entry) + return JSONResponse(result) + + +@mcp.custom_route("/api/ledger", methods=["GET"]) +async def api_ledger(request: Request) -> JSONResponse: + return JSONResponse({"ledger": ledger_mod.build(_root())}) + + +@mcp.custom_route("/api/tree", methods=["GET"]) +async def api_tree(request: Request) -> JSONResponse: + root = _root().resolve() + entries = [] + for f in sorted(root.rglob("*")): + rel_parts = f.relative_to(root).parts + if fs.BROWSER_BLOCKED & set(rel_parts): + continue + if f.name == "secrets.env": + continue + stat = f.stat() + entries.append({ + "path": str(f.relative_to(root)), + "name": f.name, + "dir": f.is_dir(), + "size": 0 if f.is_dir() else stat.st_size, + "mtime": int(stat.st_mtime * 1000), + }) + return JSONResponse({"tree": entries}) + + +@mcp.custom_route("/api/file", methods=["GET"]) +async def api_file(request: Request) -> JSONResponse: + path = request.query_params.get("path", "") + if not path: + return JSONResponse({"error": "path query parameter required"}, status_code=400) + target, error = fs.resolve_browser_path(_root(), path) + if error: + return JSONResponse({"error": error}, status_code=403) + if not target.is_file(): + return JSONResponse({"error": f"{path} does not exist"}, status_code=404) + try: + content = target.read_text(encoding="utf-8") + except UnicodeDecodeError: + return JSONResponse({"error": "binary file"}, status_code=400) + stat = target.stat() + return JSONResponse({ + "path": path, + "content": content, + "size": stat.st_size, + "mtime": int(stat.st_mtime * 1000), + }) + + +@mcp.custom_route("/api/sessions", methods=["GET"]) +async def api_sessions(request: Request) -> JSONResponse: + sessions = [{"id": sid, **info} for sid, info in server.SESSIONS.items()] + return JSONResponse({"sessions": sessions}) + + +@mcp.custom_route("/api/events", methods=["GET"]) +async def api_events(request: Request) -> JSONResponse: + try: + limit = min(int(request.query_params.get("limit", 100)), 300) + since = int(request.query_params.get("since", 0)) + except ValueError: + return JSONResponse({"error": "limit and since must be integers"}, status_code=400) + events = [e for e in server.EVENTS if e["id"] > since][-limit:] + latest = server.EVENTS[-1]["id"] if server.EVENTS else 0 + return JSONResponse({"events": events, "latest_id": latest}) + + +# --------------------------------------------------------------------------- +# Static app. The built frontend ships inside the wheel at gcontext/web_dist; +# a repo checkout uses web/dist so `make web-build` + `uv run` works too. + +_DIST_CANDIDATES = [ + Path(__file__).parent / "web_dist", + Path(__file__).parents[2] / "web" / "dist", +] + + +def _dist_dir() -> Path | None: + for candidate in _DIST_CANDIDATES: + if (candidate / "index.html").is_file(): + return candidate + return None + + +@mcp.custom_route("/{path:path}", methods=["GET"]) +async def spa(request: Request): + rel = request.path_params["path"] + if rel.startswith("api/"): + return JSONResponse({"error": "not found"}, status_code=404) + dist = _dist_dir() + if dist is None: + return PlainTextResponse( + "gcontext dashboard is not built. Run `make web-build` in the repo, " + "or reinstall the package.", + status_code=503, + ) + if rel: + target = (dist / rel).resolve() + if target.is_relative_to(dist.resolve()) and target.is_file(): + return FileResponse(target) + return FileResponse(dist / "index.html") diff --git a/src/gcontext/exec.py b/src/gcontext/exec.py new file mode 100644 index 0000000..90897b4 --- /dev/null +++ b/src/gcontext/exec.py @@ -0,0 +1,147 @@ +"""Script execution: ad-hoc agent code and saved scripts, in the project venv. + +The venv lives at /.venv and syncs the deps declared across all +connection.yaml files on every run (uv makes the satisfied case near-instant). +Secrets are injected as env vars and scrubbed from the output; results are +plain text starting with a status line the agent and the user can both read. +""" + +import os +import re +import subprocess +import sys +import tempfile +import time +from pathlib import Path + +from . import secrets as secrets_mod +from . import state + +SCRIPT_TIMEOUT = 60 + + +def venv_dir(root: Path) -> Path: + return root.resolve() / ".venv" + + +def venv_python(root: Path) -> Path: + venv = venv_dir(root) + if sys.platform == "win32": + return venv / "Scripts" / "python.exe" + return venv / "bin" / "python" + + +def collect_deps(root: Path) -> set[str]: + all_deps = set() + for conn in state.load_connections(root).values(): + all_deps.update(conn.deps) + return all_deps + + +def ensure_venv(root: Path) -> None: + """Create the project venv if missing and sync connection deps into it.""" + if not venv_dir(root).is_dir(): + subprocess.run( + ["uv", "venv", str(venv_dir(root)), "--quiet"], + check=True, + cwd=str(root), + ) + + all_deps = collect_deps(root) + if all_deps: + subprocess.run( + ["uv", "pip", "install", "--quiet", "--python", str(venv_python(root))] + + sorted(all_deps), + check=True, + cwd=str(root), + ) + + +_MISSING_MODULE_RE = re.compile( + r"ModuleNotFoundError: No module named ['\"]([^'\"]+)['\"]" +) + + +def missing_module_hint(root: Path, stderr: str) -> str | None: + """A hint shown only when a run fails on a missing import.""" + match = _MISSING_MODULE_RE.search(stderr) + if not match: + return None + module = match.group(1).split(".")[0] + declared = sorted(collect_deps(root)) + declared_line = f" Currently declared: {', '.join(declared)}." if declared else "" + return ( + f"Package '{module}' is not installed in the project venv. Declare it " + f"under deps: in the relevant connection.yaml (ask the user, that file " + f"is human-edited), then rerun: the venv syncs on the next call." + f"{declared_line} Note the pip name can differ from the import name." + ) + + +def run( + root: Path, + code: str = "", + path: str = "", + args: list[str] | None = None, + params: dict[str, str] | None = None, +) -> str: + if bool(code) == bool(path): + return "Error: pass exactly one of code or path." + + secrets = secrets_mod.load(root) + + if path: + target = (root / path).resolve() + if not target.is_relative_to(root.resolve()): + return f"Error: path {path} is outside the project directory." + if not target.is_file(): + return f"Error: {path} is not a file." + script_path = str(target) + cleanup = False + label = path + else: + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False, dir=root + ) as f: + f.write(code) + script_path = f.name + cleanup = True + label = "code" + + ensure_venv(root) + + try: + env = os.environ.copy() + env.update(secrets) + for k, v in (params or {}).items(): + env[f"PARAM_{k.upper()}"] = str(v) + + start = time.perf_counter() + result = subprocess.run( + [str(venv_python(root)), script_path, *(args or [])], + capture_output=True, + text=True, + timeout=SCRIPT_TIMEOUT, + env=env, + cwd=str(root), + ) + duration_ms = round((time.perf_counter() - start) * 1000) + + output_parts = [f"[{label} | exit {result.returncode} | {duration_ms} ms]"] + if result.stdout.strip(): + output_parts.append(result.stdout.strip()) + if result.stderr.strip(): + output_parts.append(f"[stderr]\n{result.stderr.strip()}") + if not result.stdout.strip() and not result.stderr.strip(): + output_parts.append("(no output)") + hint = missing_module_hint(root, result.stderr) + if hint: + output_parts.append(f"[hint] {hint}") + + return secrets_mod.scrub("\n".join(output_parts), secrets) + + except subprocess.TimeoutExpired: + return f"Error: script timed out after {SCRIPT_TIMEOUT} seconds." + finally: + if cleanup: + Path(script_path).unlink(missing_ok=True) diff --git a/src/gcontext/flows.py b/src/gcontext/flows.py deleted file mode 100644 index 2850298..0000000 --- a/src/gcontext/flows.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Flows: declarative information dependencies, computed from the filesystem. - -A flow is data, not a program. Each step declares which files it needs and -which files it produces. Status is a pure function of the filesystem: - - blocked some needed file does not exist yet - ready all needs exist, some produced file is missing - stale everything exists, but a need is newer than a produce (make semantics) - done all produces exist and are up to date - -gcontext never executes a step. A runtime completes a step by writing the -declared produces (write_context, or any editor); status recomputes from the -files on the next read. There is no run state stored anywhere else. -""" - -from pathlib import Path - -import yaml - -from .models import FlowManifest, FlowStep - - -def load_flows(project_dir: Path) -> dict[str, FlowManifest]: - """Scan flows/ for subdirectories containing flow.yaml.""" - flows_dir = project_dir / "flows" - if not flows_dir.is_dir(): - return {} - result = {} - for item in sorted(flows_dir.iterdir()): - flow_file = item / "flow.yaml" - if not item.is_dir() or not flow_file.exists(): - continue - data = yaml.safe_load(flow_file.read_text()) or {} - manifest = FlowManifest(**data) - result[manifest.name] = manifest - return result - - -def step_state(project_dir: Path, step: FlowStep) -> dict: - """Compute a step's status purely from the files it declares.""" - needs = [(p, project_dir / p) for p in step.needs] - produces = [(p, project_dir / p) for p in step.produces] - - missing_needs = [p for p, f in needs if not f.is_file()] - if missing_needs: - return {"status": "blocked", "missing": missing_needs} - - missing_produces = [p for p, f in produces if not f.is_file()] - if missing_produces: - return {"status": "ready", "missing": missing_produces} - - if needs and produces: - oldest_produce = min(f.stat().st_mtime for _, f in produces) - stale_needs = [p for p, f in needs if f.stat().st_mtime > oldest_produce] - if stale_needs: - return {"status": "stale", "stale_needs": stale_needs} - - return {"status": "done"} - - -def flow_board(project_dir: Path, flow: FlowManifest) -> list[dict]: - """Every step of a flow with its computed state.""" - board = [] - for step in flow.steps: - state = step_state(project_dir, step) - board.append({ - "id": step.id, - "description": step.description, - "needs": step.needs, - "produces": step.produces, - "instructions": step.instructions, - **state, - }) - return board - - -def actionable(board: list[dict]) -> list[dict]: - return [s for s in board if s["status"] in ("ready", "stale")] - - -def render_flow(project_dir: Path, flow: FlowManifest, with_instructions: bool = True) -> list[str]: - """Plain-text board for one flow. Instructions surface only for actionable steps.""" - board = flow_board(project_dir, flow) - done = sum(1 for s in board if s["status"] == "done") - - lines = [f"## {flow.name} ({done}/{len(board)} done)"] - if flow.description: - lines.append(flow.description) - lines.append("") - - for step in board: - lines.append(f"- [{step['status']}] {step['id']}: {step['description']}") - if step["needs"]: - lines.append(f" needs: {', '.join(step['needs'])}") - if step["produces"]: - lines.append(f" produces: {', '.join(step['produces'])}") - if step["status"] == "blocked": - lines.append(f" waiting on: {', '.join(step['missing'])}") - if step["status"] == "stale": - lines.append(f" stale: {', '.join(step['stale_needs'])} changed after the produces were written") - - ready = actionable(board) - if ready and with_instructions: - lines.append("") - lines.append("Actionable now:") - for step in ready: - lines.append(f"### {step['id']}") - if step["instructions"]: - lines.append(step["instructions"].rstrip()) - missing = step.get("missing") or step["produces"] - lines.append(f"Complete it by writing: {', '.join(missing)}") - - return lines diff --git a/src/gcontext/fs.py b/src/gcontext/fs.py new file mode 100644 index 0000000..4b56f23 --- /dev/null +++ b/src/gcontext/fs.py @@ -0,0 +1,140 @@ +"""File access for the read_file, write_file, list_dir and grep tools. + +Every path is resolved and confined to the project root; secrets.env is +unreadable and unwritable, connection.yaml is unwritable (the secret grant +stays human-edited). Errors come back as strings because tool results are +strings the agent reads. +""" + +import fnmatch +import re +from pathlib import Path + +# Machine folders: never served to the dashboard browser, skipped by +# list_dir and grep. +SKIP_DIRS = {".venv", ".git", "__pycache__", "node_modules"} +BROWSER_BLOCKED = SKIP_DIRS + +GREP_MAX_MATCHES = 100 +GREP_MAX_LINE = 200 + + +def resolve_path(root: Path, path: str) -> tuple[Path | None, str | None]: + """Resolve an agent path to (target, None) or (None, error). + + Confinement to the project root plus the secrets.env block, shared by + every file tool. + """ + target = (root / path).resolve() + if not target.is_relative_to(root.resolve()): + return None, f"path {path} is outside the project directory" + if target.name == "secrets.env": + return None, "secrets.env is not accessible through the agent" + return target, None + + +def resolve_browser_path(root: Path, path: str) -> tuple[Path | None, str | None]: + """Resolve a dashboard read to (target, None) or (None, error). + + Same confinement as read_file, plus the browser surface never sees + machine folders. secrets.env stays unreadable everywhere. + """ + target, error = resolve_path(root, path) + if error: + return None, error + if SKIP_DIRS & set(target.relative_to(root.resolve()).parts): + return None, f"path {path} is not readable" + return target, None + + +def read_file(root: Path, path: str) -> str: + target, error = resolve_path(root, path) + if error: + return f"Error: {error}." + if not target.exists(): + return f"Error: {path} does not exist." + if not target.is_file(): + return f"Error: {path} is not a file." + return target.read_text() + + +def write_file(root: Path, path: str, content: str) -> str: + target, error = resolve_path(root, path) + if error: + return f"Error: {error}." + if target.name == "connection.yaml": + return "Error: cannot write to connection.yaml through the agent." + + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(content) + return f"Written: {path} ({len(content)} bytes)" + + +def list_dir(root: Path, path: str = ".") -> str: + target = (root / path).resolve() + if not target.is_relative_to(root.resolve()): + return f"Error: path {path} is outside the project directory." + if not target.exists(): + return f"Error: {path} does not exist." + if not target.is_dir(): + return f"Error: {path} is not a directory." + + dirs, files = [], [] + for entry in sorted(target.iterdir(), key=lambda e: e.name): + if entry.name in SKIP_DIRS: + continue + if entry.is_dir(): + dirs.append(f"{entry.name}/") + else: + files.append(f"{entry.name} ({entry.stat().st_size} bytes)") + entries = dirs + files + if not entries: + return f"{path}: empty directory" + return "\n".join(entries) + + +def grep(root: Path, pattern: str, path: str = ".", glob: str = "") -> str: + target = (root / path).resolve() + if not target.is_relative_to(root.resolve()): + return f"Error: path {path} is outside the project directory." + if not target.exists(): + return f"Error: {path} does not exist." + + try: + rx = re.compile(pattern) + except re.error as exc: + return f"Error: invalid regex: {exc}" + + resolved_root = root.resolve() + candidates = [target] if target.is_file() else sorted(target.rglob("*")) + matches = [] + truncated = False + for f in candidates: + if not f.is_file(): + continue + rel_parts = f.relative_to(resolved_root).parts + if SKIP_DIRS & set(rel_parts): + continue + if f.name == "secrets.env": + continue + if glob and not fnmatch.fnmatch(f.name, glob): + continue + try: + text = f.read_text() + except (UnicodeDecodeError, OSError): + continue + rel = "/".join(rel_parts) + for lineno, line in enumerate(text.splitlines(), 1): + if rx.search(line): + matches.append(f"{rel}:{lineno}: {line.strip()[:GREP_MAX_LINE]}") + if len(matches) >= GREP_MAX_MATCHES: + truncated = True + break + if truncated: + break + + if not matches: + return f"No matches for {pattern!r}." + if truncated: + matches.append(f"... truncated at {GREP_MAX_MATCHES} matches, narrow the pattern or path.") + return "\n".join(matches) diff --git a/src/gcontext/ledger.py b/src/gcontext/ledger.py new file mode 100644 index 0000000..95f7510 --- /dev/null +++ b/src/gcontext/ledger.py @@ -0,0 +1,51 @@ +"""The context ledger: every pipe that inserts context into the agent. + +Statuses: loaded (pushed at start), on demand (agent pulls, visible as a +tool call), skipped (nothing to push), uncontrolled (runtime-owned). +""" + +from pathlib import Path + +from . import commands as commands_mod +from . import state + + +def build(root: Path) -> list[dict]: + instructions = root / "instructions.md" + connections = state.load_connections(root) + modules = state.discover_modules(root) + n_files = sum(len(state.connection_files(root, c)) for c in connections) + n_files += sum(len(state.module_files(root, m)) for m in modules) + + ledger = [] + + if instructions.exists(): + n = len(instructions.read_text().splitlines()) + ledger.append({"id": "G0", "label": "instructions.md", "detail": f"pushed at connect in the MCP handshake ({n} lines)", "status": "loaded"}) + else: + ledger.append({"id": "G0", "label": "instructions.md", "detail": "file missing, nothing pushed at connect", "status": "skipped"}) + + ledger.append({"id": "G1", "label": "tool descriptions", "detail": "6 gcontext tools, pushed at connect", "status": "loaded"}) + ledger.append({"id": "G2", "label": "overview()", "detail": "project map, secret status", "status": "on demand"}) + g3_detail = f"{n_files} files in connections/ + modules/" + if state.archived(root): + g3_detail += "; archive/ not scanned, readable by path" + ledger.append({"id": "G3", "label": "read_file()", "detail": g3_detail, "status": "on demand"}) + ledger.append({"id": "G4", "label": "list_dir() / grep()", "detail": "tree navigation and search, matches only", "status": "on demand"}) + ledger.append({"id": "G5", "label": "run_script() output", "detail": "secret values scrubbed", "status": "on demand"}) + n_commands = len(commands_mod.discover(root)) + ledger.append({"id": "G6", "label": "commands", "detail": f"{n_commands} command(s) as MCP prompts; a command's text enters context only when the user invokes it", "status": "on demand"}) + + ledger.append({"id": "R1", "label": "runtime system prompt", "detail": "runtime-owned", "status": "uncontrolled"}) + ledger.append({"id": "R2", "label": "user/project CLAUDE.md", "detail": "runtime-owned", "status": "uncontrolled"}) + ledger.append({"id": "R3", "label": "other MCP servers, skills, memory", "detail": "runtime-owned", "status": "uncontrolled"}) + + return ledger + + +def render_plain(root: Path) -> list[str]: + lines = [] + for i, pipe in enumerate(build(root), 1): + label = f"{pipe['label']} ".ljust(36, ".") + lines.append(f"{i}. [{pipe['id']}] {label} {pipe['status']}: {pipe['detail']}") + return lines diff --git a/src/gcontext/prompts/README.md b/src/gcontext/prompts/README.md new file mode 100644 index 0000000..0fb32dd --- /dev/null +++ b/src/gcontext/prompts/README.md @@ -0,0 +1,14 @@ +# prompts/ + +Everything gcontext itself says to an attached agent lives in this folder, +as markdown, not in Python strings. + +- `tools/*.md`: one file per tool. These are the tool descriptions pushed to + every client at connect time (ledger pipe G1). Edit a file, restart the + server, and every session sees the new text. + +The instructions an agent receives at connect are NOT here: they are the +served project's own `instructions.md`, pushed through the MCP handshake and +declared as ledger pipe G0. That file belongs to the agent folder (versioned +with its state), not to the framework; this folder only holds the fixed +framework text. diff --git a/src/gcontext/prompts/tools/grep.md b/src/gcontext/prompts/tools/grep.md new file mode 100644 index 0000000..0c2d3a8 --- /dev/null +++ b/src/gcontext/prompts/tools/grep.md @@ -0,0 +1,9 @@ +Search project files with a regex. Returns path:line: matching-line, capped at 100 matches. + +Skips machine folders (.venv, .git, __pycache__, node_modules) and +secrets.env. Use it to locate playbooks, logs, or docs before reading them. + +Args: + pattern: Python regex matched against each line. + path: File or directory to search, relative to the project root (default '.'). + glob: Optional filename filter, e.g. '*.md' or 'refund*'. diff --git a/src/gcontext/prompts/tools/list_dir.md b/src/gcontext/prompts/tools/list_dir.md new file mode 100644 index 0000000..ccba035 --- /dev/null +++ b/src/gcontext/prompts/tools/list_dir.md @@ -0,0 +1,7 @@ +List one directory in the project: subdirectories first (trailing /), then files with sizes. + +Paths are confined to the project root. Machine folders (.venv, .git, +__pycache__, node_modules) are hidden. + +Args: + path: Directory relative to the project root (default '.'). diff --git a/src/gcontext/prompts/tools/overview.md b/src/gcontext/prompts/tools/overview.md new file mode 100644 index 0000000..53550f4 --- /dev/null +++ b/src/gcontext/prompts/tools/overview.md @@ -0,0 +1 @@ +Show project info, all connections with per-secret fill status, and all modules with descriptions. diff --git a/src/gcontext/prompts/tools/read_file.md b/src/gcontext/prompts/tools/read_file.md new file mode 100644 index 0000000..7a6c98b --- /dev/null +++ b/src/gcontext/prompts/tools/read_file.md @@ -0,0 +1,3 @@ +Read a file from the project. Use overview() or list_dir first to see available files. + +Cannot read secrets.env: secret values never enter the context window. diff --git a/src/gcontext/prompts/tools/run_script.md b/src/gcontext/prompts/tools/run_script.md new file mode 100644 index 0000000..d64ed47 --- /dev/null +++ b/src/gcontext/prompts/tools/run_script.md @@ -0,0 +1,19 @@ +Run Python in the project's .venv with secrets as env vars. + +Two modes, pass exactly one of `code` or `path`: +- code: ad-hoc Python source, written to a temp file and executed. +- path: a saved script inside the project (e.g. 'connections/stripe/scripts/refund.py'). + Save proven procedures with write_file under a scripts/ folder, then run + them by path so they are reused instead of rewritten. + +The .venv has all connection deps pre-installed. Access secrets with +os.environ["SECRET_NAME"]. Secret values are scrubbed from stdout/stderr +before returning. The result starts with a status line: mode, exit code, +duration. + +Args: + code: Python source code to execute (ad-hoc mode). + path: Project-relative path of a saved .py script to run. + args: Optional argv list passed to the script. + params: Optional named parameters; each becomes a PARAM_ env var + (e.g. {"email": "x@y.z"} -> PARAM_EMAIL). diff --git a/src/gcontext/prompts/tools/write_file.md b/src/gcontext/prompts/tools/write_file.md new file mode 100644 index 0000000..b947b2f --- /dev/null +++ b/src/gcontext/prompts/tools/write_file.md @@ -0,0 +1,8 @@ +Write or update a file in the project. Creates parent directories if needed. + +Use this to update connection context docs, create playbooks, write logs, etc. +Cannot write to secrets.env or connection.yaml files. + +Args: + path: Relative path within the project (e.g. 'modules/support-workflow/playbooks/refund.md') + content: The full file content to write. diff --git a/src/gcontext/secrets.py b/src/gcontext/secrets.py new file mode 100644 index 0000000..9b23c7b --- /dev/null +++ b/src/gcontext/secrets.py @@ -0,0 +1,29 @@ +"""Secrets: names are public, values never enter the context window. + +Values live in secrets.env at the project root, parsed here and injected as +env vars at run time. Anything returned to the agent goes through scrub(). +""" + +from pathlib import Path + + +def load(root: Path) -> dict[str, str]: + env_file = root / "secrets.env" + if not env_file.exists(): + return {} + pairs = {} + for line in env_file.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + if "=" in line: + key, _, value = line.partition("=") + pairs[key.strip()] = value.strip() + return pairs + + +def scrub(text: str, secrets: dict[str, str]) -> str: + for value in secrets.values(): + if value and len(value) > 3: + text = text.replace(value, "***") + return text diff --git a/src/gcontext/server.py b/src/gcontext/server.py index 774cbd9..cb1e062 100644 --- a/src/gcontext/server.py +++ b/src/gcontext/server.py @@ -1,35 +1,108 @@ -"""gcontext MCP server. Reads a project directory and exposes it to any MCP client.""" +"""The MCP surface: everything an attached agent can reach, in one file. -import os +Six tools (defined below, their agent-facing text in prompts/tools/*.md), +commands registered as prompts, a /status route, and session tracking. +The actual work lives in the per-concern modules: + + fs.py read_file / write_file / list_dir / grep (path confinement, guards) + exec.py run_script (venv, secrets injection, output scrubbing) + state.py connections / modules / archive scanning + secrets.py secrets.env parsing and output scrubbing + ledger.py the context ledger + commands.py commands/ folders -> MCP prompts + +If it is not in this file, the agent cannot invoke it. +""" + +import itertools +import json import sys +import time +from collections import deque from datetime import datetime from pathlib import Path -import subprocess -import tempfile -import yaml from fastmcp import FastMCP from fastmcp.server.middleware import Middleware from starlette.requests import Request from starlette.responses import JSONResponse -from . import flows as flows_mod -from .models import ConnectionManifest, ModuleManifest +from . import commands as commands_mod +from . import exec as exec_mod +from . import fs +from . import ledger as ledger_mod +from . import secrets as secrets_mod +from . import state mcp = FastMCP("gcontext") # Set by cli.py before the server starts PROJECT_DIR: Path = Path(".") +# Agent-facing tool text lives in markdown, not in code. +_PROMPTS_DIR = Path(__file__).parent / "prompts" + + +def _tool_doc(name: str) -> str: + return (_PROMPTS_DIR / "tools" / f"{name}.md").read_text().strip() + + # Live MCP sessions, keyed by session id: {"client": ..., "connected": ..., "last_seen": ...} SESSIONS: dict[str, dict] = {} +# Activity feed for the dashboard: in-memory ring buffer, gone on restart. +EVENTS: deque = deque(maxlen=300) +_EVENT_SEQ = itertools.count(1) + def _session_id(context) -> str: ctx = getattr(context, "fastmcp_context", None) return getattr(ctx, "session_id", None) or "session" +def record_event(session: str, kind: str, name: str, detail: str = "", + preview: str = "", error: bool = False, tier: int = 1, + tokens_in: int = 0, tokens_out: int = 0, duration_ms: int = 0): + EVENTS.append({ + "id": next(_EVENT_SEQ), + "ts": int(time.time() * 1000), + "session": session, + "kind": kind, + "name": name, + "detail": detail, + "preview": preview, + "error": error, + "tier": tier, + "tokens_in": tokens_in, + "tokens_out": tokens_out, + "duration_ms": duration_ms, + }) + + +def _event_detail(name: str, arguments: dict) -> str: + """Summarize tool arguments for the feed. Never file content or code: + the feed goes to a browser, tool arguments may hold whole documents.""" + if name == "write_file": + path = arguments.get("path", "?") + return f"{path} ({len(arguments.get('content') or '')} bytes)" + if name == "grep": + pattern = arguments.get("pattern", "?") + path = arguments.get("path") or "." + return f"{pattern!r} in {path}" + if name == "run_script": + if arguments.get("path"): + return str(arguments["path"]) + return f"inline code ({len(arguments.get('code') or '')} chars)" + if arguments.get("path"): + return str(arguments["path"]) + return ", ".join(sorted(arguments)) if arguments else "" + + +def _result_text(result) -> str: + content = getattr(result, "content", None) or [] + return "\n".join(t for t in (getattr(b, "text", None) for b in content) if t) + + class ConnectionTracker(Middleware): """Records who is connected, straight from the MCP initialize handshake.""" @@ -45,9 +118,40 @@ class ConnectionTracker(Middleware): "connected": now, "last_seen": now, } + record_event(_session_id(context), "connect", client, + detail=version, tier=0) print(f" + {client} {version} connected ({now})", file=sys.stderr) return await call_next(context) + async def on_call_tool(self, context, call_next): + name = getattr(context.message, "name", "?") + arguments = getattr(context.message, "arguments", None) or {} + detail = _event_detail(name, arguments) + tokens_in = len(json.dumps(arguments, default=str)) // 4 + start = time.perf_counter() + try: + result = await call_next(context) + except Exception as exc: + record_event(_session_id(context), "error", name, detail=detail, + preview=str(exc)[:400], error=True, tokens_in=tokens_in, + duration_ms=round((time.perf_counter() - start) * 1000)) + raise + text = _result_text(result) + preview = secrets_mod.scrub(text[:400], secrets_mod.load(PROJECT_DIR)) + record_event(_session_id(context), "tool", name, detail=detail, + preview=preview, error=text.startswith("Error:"), + tokens_in=tokens_in, tokens_out=len(text) // 4, + duration_ms=round((time.perf_counter() - start) * 1000)) + return result + + async def on_get_prompt(self, context, call_next): + name = getattr(context.message, "name", "?") + arguments = getattr(context.message, "arguments", None) or {} + record_event(_session_id(context), "prompt", name, + detail=", ".join(sorted(arguments)) if arguments else "", + tier=2) + return await call_next(context) + async def on_message(self, context, call_next): session = SESSIONS.get(_session_id(context)) if session: @@ -60,247 +164,46 @@ mcp.add_middleware(ConnectionTracker()) @mcp.custom_route("/status", methods=["GET"]) async def status_route(request: Request) -> JSONResponse: - config = _load_gcontext_yaml() - flow_summary = {} - for fname, flow in flows_mod.load_flows(PROJECT_DIR).items(): - board = flows_mod.flow_board(PROJECT_DIR, flow) - flow_summary[fname] = { - "done": sum(1 for s in board if s["status"] == "done"), - "total": len(board), - "actionable": [s["id"] for s in flows_mod.actionable(board)], - } + config = state.load_gcontext_yaml(PROJECT_DIR) return JSONResponse({ "name": config.get("name", PROJECT_DIR.name), "project_dir": str(PROJECT_DIR.resolve()), "sessions": list(SESSIONS.values()), - "flows": flow_summary, }) -def _load_gcontext_yaml() -> dict: - p = PROJECT_DIR / "gcontext.yaml" - if p.exists(): - return yaml.safe_load(p.read_text()) or {} - return {} +def register_commands() -> int: + """Register command files as MCP prompts. Call once, after PROJECT_DIR is set.""" + return commands_mod.register_commands(mcp, PROJECT_DIR) -def _load_connections() -> dict[str, ConnectionManifest]: - """Scan connections/ for subdirectories containing connection.yaml.""" - conns_dir = PROJECT_DIR / "connections" - if not conns_dir.is_dir(): - return {} - result = {} - for item in sorted(conns_dir.iterdir()): - if not item.is_dir(): - continue - conn_file = item / "connection.yaml" - if not conn_file.exists(): - continue - data = yaml.safe_load(conn_file.read_text()) or {} - manifest = ConnectionManifest(**data) - result[manifest.name] = manifest - return result +def load_instructions() -> int: + """Serve the project's instructions.md in the MCP handshake. - -def _connection_files(name: str) -> list[str]: - """List non-yaml files in a connection folder.""" - conn_dir = PROJECT_DIR / "connections" / name - if not conn_dir.is_dir(): - return [] - files = [] - for f in sorted(conn_dir.rglob("*")): - if f.is_file() and f.name != "connection.yaml": - files.append(str(f.relative_to(PROJECT_DIR))) - return files - - -def _load_secrets_env() -> dict[str, str]: - env_file = PROJECT_DIR / "secrets.env" - if not env_file.exists(): - return {} - pairs = {} - for line in env_file.read_text().splitlines(): - line = line.strip() - if not line or line.startswith("#"): - continue - if "=" in line: - key, _, value = line.partition("=") - pairs[key.strip()] = value.strip() - return pairs - - -def _archived() -> dict[str, list[str]]: - """Names of archived items per category, from archive/{connections,modules,flows}/. - - Anything under archive/ is never scanned into overview, the ledger counts, - or the flow boards. It stays readable by path via read_context. Archiving - is a plain folder move; there is no metadata and no automatic behavior. - """ - result = {} - for category in ("connections", "modules", "flows"): - d = PROJECT_DIR / "archive" / category - if d.is_dir(): - items = [i.name for i in sorted(d.iterdir()) if i.is_dir()] - if items: - result[category] = items - return result - - -def _archived_line() -> str: - archived = _archived() - if not archived: - return "" - parts = [f"{len(items)} {cat}" for cat, items in archived.items()] - return f"archive/: {', '.join(parts)} (not scanned, readable by path)" - - -def _discover_modules() -> dict[str, ModuleManifest]: - """Scan modules/ for folders with module.yaml.""" - modules_dir = PROJECT_DIR / "modules" - if not modules_dir.is_dir(): - return {} - result = {} - for item in sorted(modules_dir.iterdir()): - if not item.is_dir(): - continue - manifest_file = item / "module.yaml" - if manifest_file.exists(): - data = yaml.safe_load(manifest_file.read_text()) or {} - manifest = ModuleManifest(**data) - else: - manifest = ModuleManifest(name=item.name, description="") - result[manifest.name] = manifest - return result - - -def _module_files(name: str) -> list[str]: - """List content files in a module folder.""" - mod_dir = PROJECT_DIR / "modules" / name - if not mod_dir.is_dir(): - return [] - files = [] - for f in sorted(mod_dir.rglob("*")): - if f.is_file() and f.name not in ("module.yaml", ".gitkeep"): - files.append(str(f.relative_to(PROJECT_DIR))) - return files - - -SCRIPT_TIMEOUT = 60 - - -def _scrub_output(text: str, secrets: dict[str, str]) -> str: - for value in secrets.values(): - if value and len(value) > 3: - text = text.replace(value, "***") - return text - - -def _venv_dir() -> Path: - return PROJECT_DIR.resolve() / ".venv" - - -def _venv_python() -> Path: - venv = _venv_dir() - if sys.platform == "win32": - return venv / "Scripts" / "python.exe" - return venv / "bin" / "python" - - -def _collect_deps() -> set[str]: - connections = _load_connections() - all_deps = set() - for conn in connections.values(): - for dep in conn.deps: - all_deps.add(dep) - return all_deps - - -def ensure_venv() -> None: - """Create the project venv if missing and sync connection deps into it.""" - venv_dir = _venv_dir() - - if not venv_dir.is_dir(): - subprocess.run( - ["uv", "venv", str(venv_dir), "--quiet"], - check=True, - cwd=str(PROJECT_DIR), - ) - - all_deps = _collect_deps() - if all_deps: - subprocess.run( - ["uv", "pip", "install", "--quiet", "--python", str(_venv_python())] - + sorted(all_deps), - check=True, - cwd=str(PROJECT_DIR), - ) - - -def build_ledger(mode: str) -> list[dict]: - """Every pipe that inserts context into the agent for a mode ('chat' or 'mcp'). - - Statuses: loaded (pushed at start), on demand (agent pulls, visible as a - tool call), skipped (closed by a launch flag), uncontrolled (runtime-owned). + This is THE file pushed to every agent at connect: what it says is exactly + what the agent starts with, the ledger declares it as G0, and editing the + file (plus a restart) changes what every future session receives. Returns + the line count, 0 if the file does not exist (nothing is pushed then). """ instructions = PROJECT_DIR / "instructions.md" - connections = _load_connections() - modules = _discover_modules() - n_files = sum(len(_connection_files(c)) for c in connections) - n_files += sum(len(_module_files(m)) for m in modules) - - ledger = [] - - if mode == "chat": - if instructions.exists(): - n = len(instructions.read_text().splitlines()) - ledger.append({"id": "G0", "label": "instructions.md", "detail": f"system prompt ({n} lines)", "status": "loaded"}) - else: - ledger.append({"id": "G0", "label": "instructions.md", "detail": "file missing, no system prompt", "status": "skipped"}) - else: - ledger.append({"id": "G0", "label": "instructions.md", "detail": "not auto-loaded in MCP mode, read it via read_context", "status": "on demand"}) - - ledger.append({"id": "G1", "label": "tool descriptions", "detail": "6 gcontext tools, pushed at connect", "status": "loaded"}) - ledger.append({"id": "G2", "label": "overview()", "detail": "project map, secret status", "status": "on demand"}) - g3_detail = f"{n_files} files in connections/ + modules/" - if _archived(): - g3_detail += "; archive/ not scanned, readable by path" - ledger.append({"id": "G3", "label": "read_context()", "detail": g3_detail, "status": "on demand"}) - ledger.append({"id": "G4", "label": "list_connections()", "detail": f"{len(connections)} connection(s)", "status": "on demand"}) - ledger.append({"id": "G5", "label": "run_script() output", "detail": "secret values scrubbed", "status": "on demand"}) - all_flows = flows_mod.load_flows(PROJECT_DIR) - ledger.append({"id": "G6", "label": "flows()", "detail": f"{len(all_flows)} flow(s); step instructions surface only when the step is actionable", "status": "on demand"}) - - if mode == "chat": - ledger.append({"id": "R1", "label": "claude default system prompt", "detail": "replaced by --system-prompt", "status": "skipped"}) - ledger.append({"id": "R2", "label": "~/.claude/CLAUDE.md + settings", "detail": "closed via --setting-sources ''", "status": "skipped"}) - ledger.append({"id": "R3", "label": "other MCP servers", "detail": "closed via --strict-mcp-config", "status": "skipped"}) - ledger.append({"id": "R4", "label": "claude tool harness", "detail": "runtime-owned", "status": "uncontrolled"}) - else: - ledger.append({"id": "R1", "label": "runtime system prompt", "detail": "runtime-owned", "status": "uncontrolled"}) - ledger.append({"id": "R2", "label": "user/project CLAUDE.md", "detail": "runtime-owned", "status": "uncontrolled"}) - ledger.append({"id": "R3", "label": "other MCP servers, skills, memory", "detail": "runtime-owned", "status": "uncontrolled"}) - - return ledger + if not instructions.exists(): + mcp.instructions = None + return 0 + text = instructions.read_text() + mcp.instructions = text + return len(text.splitlines()) -def render_ledger_plain(mode: str) -> list[str]: - lines = [] - for i, pipe in enumerate(build_ledger(mode), 1): - label = f"{pipe['label']} ".ljust(36, ".") - lines.append(f"{i}. [{pipe['id']}] {label} {pipe['status']}: {pipe['detail']}") - return lines - - -@mcp.tool +@mcp.tool(description=_tool_doc("overview")) def overview() -> str: - """Show project info, all connections with their secret status, and all modules with descriptions.""" - config = _load_gcontext_yaml() - connections = _load_connections() - secrets = _load_secrets_env() - modules = _discover_modules() + root = PROJECT_DIR + config = state.load_gcontext_yaml(root) + connections = state.load_connections(root) + secrets = secrets_mod.load(root) + modules = state.discover_modules(root) lines = [] - name = config.get("name", PROJECT_DIR.name) + name = config.get("name", root.name) desc = config.get("description", "") lines.append(f"# {name}") if desc: @@ -309,10 +212,10 @@ def overview() -> str: lines.append("## Context ledger") lines.append("Everything that enters your context from this server, and how:") - lines.extend(render_ledger_plain("mcp")) + lines.extend(ledger_mod.render_plain(root)) lines.append("") - instructions = PROJECT_DIR / "instructions.md" + instructions = root / "instructions.md" if instructions.exists(): lines.append(f"System prompt: instructions.md ({len(instructions.read_text().splitlines())} lines)") lines.append("") @@ -324,15 +227,15 @@ def overview() -> str: filled = sum(1 for s in conn.secrets if s in secrets and secrets[s]) total = len(conn.secrets) status = "ready" if filled == total else f"missing {total - filled} secret(s)" - missing = [s for s in conn.secrets if s not in secrets or not secrets[s]] lines.append(f"- **{cname}**: {status}") if conn.description: lines.append(f" {conn.description}") - if missing: - lines.append(f" Missing: {', '.join(missing)}") + for s in conn.secrets: + has_value = s in secrets and bool(secrets[s]) + lines.append(f" - {s}: {'filled' if has_value else 'MISSING'}") if conn.deps: lines.append(f" Deps: {', '.join(conn.deps)}") - for f in _connection_files(cname): + for f in state.connection_files(root, cname): lines.append(f" - {f}") lines.append("") @@ -343,25 +246,11 @@ def overview() -> str: lines.append(f"- **{mname}** (v{mod.version}){tag_str}") if mod.description: lines.append(f" {mod.description}") - for f in _module_files(mname): + for f in state.module_files(root, mname): lines.append(f" - {f}") lines.append("") - all_flows = flows_mod.load_flows(PROJECT_DIR) - if all_flows: - lines.append("## Flows") - for fname, flow in all_flows.items(): - board = flows_mod.flow_board(PROJECT_DIR, flow) - done = sum(1 for s in board if s["status"] == "done") - ready = [s["id"] for s in flows_mod.actionable(board)] - ready_str = f", actionable: {', '.join(ready)}" if ready else "" - lines.append(f"- **{fname}**: {done}/{len(board)} done{ready_str}") - if flow.description: - lines.append(f" {flow.description}") - lines.append("Call flows() for step details and instructions.") - lines.append("") - - archived = _archived() + archived = state.archived(root) if archived: lines.append("## Archive") for cat, items in archived.items(): @@ -371,155 +260,36 @@ def overview() -> str: return "\n".join(lines).rstrip() -@mcp.tool -def read_context(path: str) -> str: - """Read a file from the project. Use overview() first to see available files.""" - target = (PROJECT_DIR / path).resolve() - if not target.is_relative_to(PROJECT_DIR.resolve()): - return f"Error: path {path} is outside the project directory." - if not target.exists(): - return f"Error: {path} does not exist." - if not target.is_file(): - return f"Error: {path} is not a file." - return target.read_text() +@mcp.tool(description=_tool_doc("read_file")) +def read_file(path: str) -> str: + return fs.read_file(PROJECT_DIR, path) -@mcp.tool -def write_context(path: str, content: str) -> str: - """Write or update a file in the project. Creates parent directories if needed. - - Use this to update connection context docs, create playbooks, write logs, etc. - Cannot write to secrets.env or connection.yaml files. - - Args: - path: Relative path within the project (e.g. 'modules/support-workflow/playbooks/refund.md') - content: The full file content to write. - """ - target = (PROJECT_DIR / path).resolve() - if not target.is_relative_to(PROJECT_DIR.resolve()): - return f"Error: path {path} is outside the project directory." - if target.name == "secrets.env": - return "Error: cannot write to secrets.env through the agent." - if target.name == "connection.yaml": - return "Error: cannot write to connection.yaml through the agent." - - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(content) - return f"Written: {path} ({len(content)} bytes)" +@mcp.tool(description=_tool_doc("write_file")) +def write_file(path: str, content: str) -> str: + return fs.write_file(PROJECT_DIR, path, content) -@mcp.tool -def run_script(code: str) -> str: - """Run a Python script in the project's .venv with secrets as env vars. - - The .venv has all connection deps pre-installed. - Access secrets with os.environ["SECRET_NAME"]. - Secret values are scrubbed from stdout/stderr before returning. - - Args: - code: Python source code to execute. - """ - secrets = _load_secrets_env() - python = _venv_python() - - if not python.exists(): - ensure_venv() - - with tempfile.NamedTemporaryFile( - mode="w", suffix=".py", delete=False, dir=PROJECT_DIR - ) as f: - f.write(code) - script_path = f.name - - try: - env = os.environ.copy() - env.update(secrets) - - result = subprocess.run( - [str(python), script_path], - capture_output=True, - text=True, - timeout=SCRIPT_TIMEOUT, - env=env, - cwd=str(PROJECT_DIR), - ) - - output_parts = [] - if result.stdout.strip(): - output_parts.append(result.stdout.strip()) - if result.stderr.strip(): - output_parts.append(f"[stderr]\n{result.stderr.strip()}") - if result.returncode != 0: - output_parts.append(f"[exit code: {result.returncode}]") - - output = "\n".join(output_parts) if output_parts else "(no output)" - return _scrub_output(output, secrets) - - except subprocess.TimeoutExpired: - return f"Error: script timed out after {SCRIPT_TIMEOUT} seconds." - finally: - Path(script_path).unlink(missing_ok=True) +@mcp.tool(description=_tool_doc("list_dir")) +def list_dir(path: str = ".") -> str: + return fs.list_dir(PROJECT_DIR, path) -@mcp.tool -def flows(name: str = "") -> str: - """Show flows: declarative multi-step work whose state lives in files. - - Each step declares which files it needs and which it produces. Status is - computed purely from the filesystem: blocked (a needed file is missing), - ready (needs exist, produces missing), stale (a need changed after the - produces were written), done. Instructions are shown only for actionable - (ready or stale) steps. - - You complete a step by writing its declared produces with write_context. - Nothing else tracks progress; the files are the state. - - Args: - name: Optional flow name to show just one flow. - """ - all_flows = flows_mod.load_flows(PROJECT_DIR) - if not all_flows: - return "No flows defined in flows/*/flow.yaml" - - if name: - if name not in all_flows: - return f"Error: no flow named {name}. Available: {', '.join(all_flows)}" - all_flows = {name: all_flows[name]} - - lines = [] - for flow in all_flows.values(): - lines.extend(flows_mod.render_flow(PROJECT_DIR, flow)) - lines.append("") - return "\n".join(lines).rstrip() +@mcp.tool(description=_tool_doc("grep")) +def grep(pattern: str, path: str = ".", glob: str = "") -> str: + return fs.grep(PROJECT_DIR, pattern, path=path, glob=glob) -@mcp.tool -def list_connections() -> str: - """Show all connections with their secrets, deps, context files, and whether each secret has a value.""" - connections = _load_connections() - secrets = _load_secrets_env() +@mcp.tool(description=_tool_doc("run_script")) +def run_script( + code: str = "", + path: str = "", + args: list[str] | None = None, + params: dict[str, str] | None = None, +) -> str: + return exec_mod.run(PROJECT_DIR, code=code, path=path, args=args, params=params) - if not connections: - return "No connections defined in connections/*/connection.yaml" - lines = [] - for cname, conn in connections.items(): - lines.append(f"## {cname}") - if conn.description: - lines.append(conn.description) - lines.append("") - lines.append("Secrets:") - for s in conn.secrets: - has_value = s in secrets and bool(secrets[s]) - icon = "filled" if has_value else "MISSING" - lines.append(f" - {s}: {icon}") - if conn.deps: - lines.append(f"Deps: {', '.join(conn.deps)}") - context_files = _connection_files(cname) - if context_files: - lines.append("Context:") - for f in context_files: - lines.append(f" - {f}") - lines.append("") - return "\n".join(lines) + +from . import dashboard # noqa: E402,F401 registers /api/* and the static catch-all diff --git a/src/gcontext/state.py b/src/gcontext/state.py new file mode 100644 index 0000000..0de9124 --- /dev/null +++ b/src/gcontext/state.py @@ -0,0 +1,104 @@ +"""Project state scanning: connections, modules, archive. + +Everything here is a pure read of the project folder. Nothing is cached and +nothing is stored: state is computed from the files on every call. +""" + +from pathlib import Path + +import yaml + +from .models import ConnectionManifest, ModuleManifest + + +def load_gcontext_yaml(root: Path) -> dict: + p = root / "gcontext.yaml" + if p.exists(): + return yaml.safe_load(p.read_text()) or {} + return {} + + +def load_connections(root: Path) -> dict[str, ConnectionManifest]: + """Scan connections/ for subdirectories containing connection.yaml.""" + conns_dir = root / "connections" + if not conns_dir.is_dir(): + return {} + result = {} + for item in sorted(conns_dir.iterdir()): + if not item.is_dir(): + continue + conn_file = item / "connection.yaml" + if not conn_file.exists(): + continue + data = yaml.safe_load(conn_file.read_text()) or {} + manifest = ConnectionManifest(**data) + result[manifest.name] = manifest + return result + + +def connection_files(root: Path, name: str) -> list[str]: + """List non-yaml files in a connection folder.""" + conn_dir = root / "connections" / name + if not conn_dir.is_dir(): + return [] + files = [] + for f in sorted(conn_dir.rglob("*")): + if f.is_file() and f.name != "connection.yaml": + files.append(str(f.relative_to(root))) + return files + + +def discover_modules(root: Path) -> dict[str, ModuleManifest]: + """Scan modules/ for folders; module.yaml is optional.""" + modules_dir = root / "modules" + if not modules_dir.is_dir(): + return {} + result = {} + for item in sorted(modules_dir.iterdir()): + if not item.is_dir(): + continue + manifest_file = item / "module.yaml" + if manifest_file.exists(): + data = yaml.safe_load(manifest_file.read_text()) or {} + manifest = ModuleManifest(**data) + else: + manifest = ModuleManifest(name=item.name, description="") + result[manifest.name] = manifest + return result + + +def module_files(root: Path, name: str) -> list[str]: + """List content files in a module folder.""" + mod_dir = root / "modules" / name + if not mod_dir.is_dir(): + return [] + files = [] + for f in sorted(mod_dir.rglob("*")): + if f.is_file() and f.name not in ("module.yaml", ".gitkeep"): + files.append(str(f.relative_to(root))) + return files + + +def archived(root: Path) -> dict[str, list[str]]: + """Names of archived items per category, from archive/{connections,modules}/. + + Anything under archive/ is never scanned into overview or the ledger + counts. It stays readable by path via read_file. Archiving is a plain + folder move; there is no metadata and no automatic behavior. + """ + result = {} + for category in ("connections", "modules"): + d = root / "archive" / category + if d.is_dir(): + items = [i.name for i in sorted(d.iterdir()) if i.is_dir()] + if items: + result[category] = items + return result + + +def archived_line(root: Path) -> str: + items_by_cat = archived(root) + if not items_by_cat: + return "" + parts = [f"{len(items)} {cat}" for cat, items in items_by_cat.items()] + return f"archive/: {', '.join(parts)} (not scanned, readable by path)" diff --git a/tests/test_commands.py b/tests/test_commands.py new file mode 100644 index 0000000..e3071ce --- /dev/null +++ b/tests/test_commands.py @@ -0,0 +1,111 @@ +import asyncio + +import pytest +from fastmcp import Client, FastMCP + +from gcontext import commands, ledger, server + +MD_COMMAND = """\ +--- +description: Draft a refund reply +parameters: + - name: email + description: Customer email + required: true +--- +Draft a refund reply for $email and show it to the user. +""" + +PY_COMMAND = """\ +# --- +# description: Cancel a subscription +# parameters: +# - name: email +# required: true +# --- +print("would cancel") +""" + + +@pytest.fixture +def project(tmp_path, monkeypatch): + (tmp_path / "gcontext.yaml").write_text("name: t\n") + monkeypatch.setattr(server, "PROJECT_DIR", tmp_path) + return tmp_path + + +def _write_commands(root): + md = root / "modules" / "support" / "commands" / "refund_reply.md" + md.parent.mkdir(parents=True) + md.write_text(MD_COMMAND) + py = root / "connections" / "stripe" / "commands" / "cancel.py" + py.parent.mkdir(parents=True) + py.write_text(PY_COMMAND) + + +def test_parse_command_frontmatter_and_body(): + meta, body = commands.parse_command(MD_COMMAND) + assert meta["description"] == "Draft a refund reply" + assert meta["parameters"][0]["name"] == "email" + assert body.startswith("Draft a refund reply for $email") + + +def test_parse_command_rejects_missing_frontmatter(): + with pytest.raises(ValueError): + commands.parse_command("no frontmatter here") + + +def test_parse_script_command_comment_block(): + meta = commands.parse_script_command(PY_COMMAND) + assert meta["description"] == "Cancel a subscription" + assert meta["parameters"][0]["required"] is True + + +def test_register_commands_counts_and_skips_malformed(tmp_path): + _write_commands(tmp_path) + bad = tmp_path / "modules" / "support" / "commands" / "broken.md" + bad.write_text("no frontmatter") + mcp = FastMCP("t") + assert commands.register_commands(mcp, tmp_path) == 2 + + +def test_prompt_roundtrip_over_protocol(tmp_path): + _write_commands(tmp_path) + mcp = FastMCP("t") + commands.register_commands(mcp, tmp_path) + + async def go(): + async with Client(mcp) as c: + listed = await c.list_prompts() + names = sorted(p.name for p in listed) + md = await c.get_prompt("support__refund_reply", {"email": "a@b.c"}) + py = await c.get_prompt("stripe__cancel", {"email": "a@b.c"}) + return names, md, py + + names, md, py = asyncio.run(go()) + assert names == ["stripe__cancel", "support__refund_reply"] + md_text = md.messages[0].content.text + assert "a@b.c" in md_text and "$email" not in md_text + py_text = py.messages[0].content.text + assert "run_script" in py_text + assert "connections/stripe/commands/cancel.py" in py_text + assert '"email": "a@b.c"' in py_text + + +def test_prompt_rejects_missing_required_argument(tmp_path): + _write_commands(tmp_path) + mcp = FastMCP("t") + commands.register_commands(mcp, tmp_path) + + async def go(): + async with Client(mcp) as c: + await c.get_prompt("support__refund_reply", {}) + + with pytest.raises(Exception): + asyncio.run(go()) + + +def test_commands_ledger_pipe(project): + _write_commands(project) + g6 = [p for p in ledger.build(project) if p["id"] == "G6"] + assert g6 and "2 command(s)" in g6[0]["detail"] diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py new file mode 100644 index 0000000..bcc230f --- /dev/null +++ b/tests/test_dashboard.py @@ -0,0 +1,169 @@ +import asyncio +import json + +import pytest +from starlette.testclient import TestClient + +from gcontext import dashboard, server + + +@pytest.fixture +def project(tmp_path, monkeypatch): + (tmp_path / "gcontext.yaml").write_text("name: t\ndescription: test agent\n") + (tmp_path / "instructions.md").write_text("# Instructions\nbe useful\n") + (tmp_path / "secrets.env").write_text("API_KEY=sk-verysecret\nEMPTY=\n") + conn = tmp_path / "connections" / "gmail" + conn.mkdir(parents=True) + conn.joinpath("connection.yaml").write_text( + "name: gmail\ndescription: mail\nsecrets: [API_KEY, MISSING_KEY]\ndeps: [requests]\n" + ) + conn.joinpath("index.md").write_text("# gmail docs") + mod = tmp_path / "modules" / "notes" + mod.mkdir(parents=True) + mod.joinpath("module.yaml").write_text("name: notes\ndescription: keep notes\n") + mod.joinpath("index.md").write_text("# notes") + (tmp_path / ".venv" / "bin").mkdir(parents=True) + (tmp_path / ".venv" / "bin" / "python").write_text("") + monkeypatch.setattr(server, "PROJECT_DIR", tmp_path) + server.EVENTS.clear() + return tmp_path + + +@pytest.fixture +def client(project): + with TestClient(server.mcp.http_app()) as c: + yield c + + +def test_api_project(client): + data = client.get("/api/project").json() + assert data["name"] == "t" + assert data["description"] == "test agent" + assert data["has_instructions"] is True + assert data["instructions_lines"] == 2 + + +def test_api_connections_no_secret_values(client): + resp = client.get("/api/connections") + data = resp.json() + assert len(data) == 1 + gmail = data[0] + assert gmail["ready"] is False + assert {"name": "API_KEY", "filled": True} in gmail["secrets"] + assert {"name": "MISSING_KEY", "filled": False} in gmail["secrets"] + assert "connections/gmail/index.md" in gmail["files"] + assert "sk-verysecret" not in resp.text + + +def test_api_modules(client): + data = client.get("/api/modules").json() + assert data[0]["name"] == "notes" + assert "modules/notes/index.md" in data[0]["files"] + + +def test_api_ledger(client): + data = client.get("/api/ledger").json() + assert any(p["id"] == "G0" for p in data["ledger"]) + + +def test_api_file(client): + data = client.get("/api/file", params={"path": "connections/gmail/index.md"}).json() + assert data["content"] == "# gmail docs" + assert client.get("/api/file", params={"path": "secrets.env"}).status_code == 403 + assert client.get("/api/file", params={"path": "../outside.txt"}).status_code == 403 + assert client.get("/api/file", params={"path": ".venv/bin/python"}).status_code == 403 + assert client.get("/api/file", params={"path": "nope.md"}).status_code == 404 + assert client.get("/api/file").status_code == 400 + + +def test_api_tree_excludes_machine_and_secret_files(client): + paths = [e["path"] for e in client.get("/api/tree").json()["tree"]] + assert "connections/gmail/index.md" in paths + assert "secrets.env" not in paths + assert not any(p.startswith(".venv") for p in paths) + + +def test_api_events_limit_since_and_ring_cap(client): + for i in range(350): + server.record_event("s", "tool", f"tool{i}") + assert len(server.EVENTS) == 300 + + data = client.get("/api/events?limit=10").json() + assert len(data["events"]) == 10 + assert data["latest_id"] == data["events"][-1]["id"] + + since = data["events"][-1]["id"] - 3 + newer = client.get(f"/api/events?since={since}").json()["events"] + assert all(e["id"] > since for e in newer) + + assert client.get("/api/events?limit=x").status_code == 400 + + +def test_middleware_records_scrubbed_tool_event(project): + class Msg: + name = "write_file" + arguments = {"path": "a.md", "content": "top secret document"} + + class Ctx: + message = Msg() + fastmcp_context = None + + class Result: + class Block: + text = "Written: a.md, key sk-verysecret leaked" + content = [Block()] + + async def call_next(context): + return Result() + + tracker = server.ConnectionTracker() + asyncio.run(tracker.on_call_tool(Ctx(), call_next)) + + event = server.EVENTS[-1] + assert event["kind"] == "tool" + assert event["name"] == "write_file" + assert "top secret document" not in json.dumps(event) + assert "sk-verysecret" not in event["preview"] + assert "***" in event["preview"] + assert event["detail"] == "a.md (19 bytes)" + + +def test_middleware_records_error_and_reraises(project): + class Msg: + name = "overview" + arguments = {} + + class Ctx: + message = Msg() + fastmcp_context = None + + async def call_next(context): + raise RuntimeError("boom") + + tracker = server.ConnectionTracker() + with pytest.raises(RuntimeError): + asyncio.run(tracker.on_call_tool(Ctx(), call_next)) + event = server.EVENTS[-1] + assert event["kind"] == "error" + assert event["error"] is True + assert "boom" in event["preview"] + + +def test_catch_all_serves_spa(client, tmp_path, monkeypatch): + dist = tmp_path / "dist" + (dist / "assets").mkdir(parents=True) + (dist / "index.html").write_text("app") + (dist / "assets" / "x.js").write_text("js") + monkeypatch.setattr(dashboard, "_DIST_CANDIDATES", [dist]) + + assert client.get("/").text == "app" + assert client.get("/some/route").text == "app" + assert client.get("/assets/x.js").text == "js" + assert client.get("/api/nope").status_code == 404 + + +def test_catch_all_without_dist(client, monkeypatch, tmp_path): + monkeypatch.setattr(dashboard, "_DIST_CANDIDATES", [tmp_path / "missing"]) + resp = client.get("/") + assert resp.status_code == 503 + assert "not built" in resp.text diff --git a/tests/test_flows.py b/tests/test_flows.py deleted file mode 100644 index fa6d029..0000000 --- a/tests/test_flows.py +++ /dev/null @@ -1,71 +0,0 @@ -import os - -import yaml - -from gcontext.flows import flow_board, load_flows, step_state -from gcontext.models import FlowStep - -FLOW = { - "name": "f", - "steps": [ - {"id": "capture", "produces": ["brief.md"]}, - {"id": "draft", "needs": ["brief.md"], "produces": ["draft.md"]}, - ], -} - - -def make_flow(project, data=FLOW): - d = project / "flows" / data["name"] - d.mkdir(parents=True) - (d / "flow.yaml").write_text(yaml.safe_dump(data)) - - -def test_load_flows(tmp_path): - make_flow(tmp_path) - flows = load_flows(tmp_path) - assert list(flows) == ["f"] - assert [s.id for s in flows["f"].steps] == ["capture", "draft"] - - -def test_no_needs_is_ready(tmp_path): - state = step_state(tmp_path, FlowStep(id="s", produces=["out.md"])) - assert state["status"] == "ready" - assert state["missing"] == ["out.md"] - - -def test_blocked_then_ready_then_done(tmp_path): - step = FlowStep(id="s", needs=["brief.md"], produces=["draft.md"]) - - state = step_state(tmp_path, step) - assert state["status"] == "blocked" - assert state["missing"] == ["brief.md"] - - (tmp_path / "brief.md").write_text("brief") - assert step_state(tmp_path, step)["status"] == "ready" - - (tmp_path / "draft.md").write_text("draft") - assert step_state(tmp_path, step)["status"] == "done" - - -def test_stale_when_need_changes_after_produce(tmp_path): - step = FlowStep(id="s", needs=["brief.md"], produces=["draft.md"]) - brief = tmp_path / "brief.md" - draft = tmp_path / "draft.md" - brief.write_text("brief") - draft.write_text("draft") - - now = draft.stat().st_mtime - os.utime(brief, (now + 10, now + 10)) - - state = step_state(tmp_path, step) - assert state["status"] == "stale" - assert state["stale_needs"] == ["brief.md"] - - -def test_board_statuses(tmp_path): - make_flow(tmp_path) - flow = load_flows(tmp_path)["f"] - assert [s["status"] for s in flow_board(tmp_path, flow)] == ["ready", "blocked"] - - (tmp_path / "brief.md").write_text("brief") - assert [s["status"] for s in flow_board(tmp_path, flow)] == ["done", "ready"] diff --git a/tests/test_init.py b/tests/test_init.py index 3eb475a..c88579a 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -19,7 +19,6 @@ def test_init_scaffolds_agent(tmp_path): "instructions.md", "secrets.env", ".gitignore", - "flows/demo-brief/flow.yaml", ]: assert (agent / rel).is_file(), rel assert (agent / "connections").is_dir() @@ -38,10 +37,10 @@ def test_init_refuses_non_empty_dir(tmp_path): def test_scaffolded_agent_works_with_cli(tmp_path): run_cli("init", "a", cwd=tmp_path) - result = run_cli("flows", "a", cwd=tmp_path) + result = run_cli("context", "a", cwd=tmp_path) assert result.returncode == 0, result.stderr - assert "demo-brief" in result.stdout - assert "capture" in result.stdout + assert "instructions.md" in result.stdout + assert "commands" in result.stdout def test_persist_port_replaces_commented_template_line(tmp_path): diff --git a/tests/test_server.py b/tests/test_server.py index 11e20e7..d7f3971 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,6 +1,7 @@ import pytest -from gcontext import server +from gcontext import ledger, server, state +from gcontext.secrets import scrub @pytest.fixture @@ -12,26 +13,33 @@ def project(tmp_path, monkeypatch): def test_scrub_output(): secrets = {"API_KEY": "sk-verysecret", "SHORT": "ab"} - out = server._scrub_output("token sk-verysecret used, ab kept", secrets) + out = scrub("token sk-verysecret used, ab kept", secrets) assert "sk-verysecret" not in out assert "***" in out assert "ab kept" in out # values of length <= 3 are not scrubbed -def test_read_context_blocks_traversal(project): - assert "outside the project" in server.read_context("../gcontext.yaml") - assert "outside the project" in server.read_context("/etc/hosts") +def test_read_file_blocks_traversal(project): + assert "outside the project" in server.read_file("../gcontext.yaml") + assert "outside the project" in server.read_file("/etc/hosts") -def test_write_context_blocks_traversal_and_protected_files(project): - assert "outside the project" in server.write_context("../x.md", "hi") - assert "Error" in server.write_context("secrets.env", "STOLEN=1") - assert "Error" in server.write_context("connections/a/connection.yaml", "nope") +def test_read_file_refuses_secrets_env(project): + (project / "secrets.env").write_text("API_KEY=sk-verysecret\n") + result = server.read_file("secrets.env") + assert "Error" in result + assert "sk-verysecret" not in result + + +def test_write_file_blocks_traversal_and_protected_files(project): + assert "outside the project" in server.write_file("../x.md", "hi") + assert "Error" in server.write_file("secrets.env", "STOLEN=1") + assert "Error" in server.write_file("connections/a/connection.yaml", "nope") def test_write_then_read_roundtrip(project): - server.write_context("modules/notes/index.md", "hello") - assert server.read_context("modules/notes/index.md") == "hello" + server.write_file("modules/notes/index.md", "hello") + assert server.read_file("modules/notes/index.md") == "hello" def test_archive_not_scanned_but_reported(project): @@ -40,10 +48,10 @@ def test_archive_not_scanned_but_reported(project): (project / "archive" / "modules" / "old").mkdir(parents=True) (project / "archive" / "modules" / "old" / "index.md").write_text("x") - modules = server._discover_modules() + modules = state.discover_modules(project) assert "active" in modules and "old" not in modules - assert server._archived() == {"modules": ["old"]} + assert state.archived(project) == {"modules": ["old"]} overview = server.overview() assert "## Archive" in overview assert "old" in overview @@ -52,18 +60,75 @@ def test_archive_not_scanned_but_reported(project): def test_archive_readable_by_path(project): (project / "archive").mkdir() (project / "archive" / "note.md").write_text("kept") - assert server.read_context("archive/note.md") == "kept" + assert server.read_file("archive/note.md") == "kept" -def test_flows_tool_and_ledger(project): - d = project / "flows" / "f" - d.mkdir(parents=True) - (d / "flow.yaml").write_text( - "name: f\nsteps:\n - id: s\n produces: [flows/f/out.md]\n instructions: write it\n" +def test_run_script_requires_exactly_one_mode(project): + assert "exactly one" in server.run_script() + assert "exactly one" in server.run_script(code="print(1)", path="x.py") + + +def test_run_script_code_mode_header(project): + out = server.run_script(code="print('hi')") + assert out.splitlines()[0].startswith("[code | exit 0 | ") + assert "hi" in out + + +def test_run_script_path_mode_with_args_and_params(project): + script = project / "modules" / "m" / "scripts" / "s.py" + script.parent.mkdir(parents=True) + script.write_text( + "import os, sys\nprint(sys.argv[1], os.environ['PARAM_EMAIL'])\n" ) - out = server.flows() - assert "[ready] s" in out - assert "write it" in out # actionable steps expose instructions + out = server.run_script( + path="modules/m/scripts/s.py", args=["a1"], params={"email": "x@y.z"} + ) + assert out.splitlines()[0].startswith("[modules/m/scripts/s.py | exit 0 | ") + assert "a1 x@y.z" in out - g6 = [p for p in server.build_ledger("mcp") if p["id"] == "G6"] - assert g6 and "1 flow(s)" in g6[0]["detail"] + +def test_run_script_path_blocks_traversal(project): + assert "outside the project" in server.run_script(path="../evil.py") + + +def test_run_script_missing_module_hint(project): + (project / "connections" / "c").mkdir(parents=True) + (project / "connections" / "c" / "connection.yaml").write_text( + "name: c\ndeps: [pyyaml]\n" + ) + out = server.run_script(code="import definitely_not_a_module") + assert "[hint]" in out + assert "definitely_not_a_module" in out + assert "connection.yaml" in out + + +def test_instructions_pushed_in_handshake(project): + (project / "instructions.md").write_text("line one\nline two\n") + assert server.load_instructions() == 2 + + import asyncio + + from fastmcp import Client + + async def go(): + async with Client(server.mcp) as c: + return c.initialize_result.instructions + + assert asyncio.run(go()) == "line one\nline two\n" + + g0 = [p for p in ledger.build(project) if p["id"] == "G0"] + assert g0[0]["status"] == "loaded" + assert "pushed at connect" in g0[0]["detail"] + + +def test_no_instructions_file_pushes_nothing(project): + assert server.load_instructions() == 0 + assert server.mcp.instructions is None + g0 = [p for p in ledger.build(project) if p["id"] == "G0"] + assert g0[0]["status"] == "skipped" + + +def test_ledger_has_no_flow_pipe(project): + ids = [p["id"] for p in ledger.build(project)] + assert "G6" in ids # commands pipe + assert not any("flow" in p["label"] for p in ledger.build(project)) diff --git a/uv.lock b/uv.lock index 7c6708e..d95d888 100644 --- a/uv.lock +++ b/uv.lock @@ -423,7 +423,7 @@ server = [ [[package]] name = "gcontext-ai" -version = "0.3.0" +version = "0.4.0" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..98915a3 --- /dev/null +++ b/web/index.html @@ -0,0 +1,18 @@ + + + + + + gcontext + + + + + + + + +
+ + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..6063794 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,3424 @@ +{ + "name": "gcontext-dashboard", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gcontext-dashboard", + "dependencies": { + "highlight.js": "^11.11.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-markdown": "^10.1.0", + "rehype-highlight": "^7.0.2", + "remark-gfm": "^4.0.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "vite": "^6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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": { + "node": ">=6.9.0" + } + }, + "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": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "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": "*" + } + }, + "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": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "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": "*" + } + }, + "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/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "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/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.9", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.9.tgz", + "integrity": "sha512-cp447VUsGS07+n1Dqf7YSQ8maeJrjEhaDxTm1ZefbqDtypHBC5GzGMQbklR6IPR13Y8OAJRHZWEMtZipJLCttg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/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", + "peer": true + }, + "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" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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" + } + }, + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.399", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.399.tgz", + "integrity": "sha512-lEcqhErbHjXRvd41rnWLpzbyU/IXfIYo7QwaFWmxGeLiLyY2TBCdHnWY88vB+p3ubnihRypDm66panXl7TylLA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "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" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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", + "url": "https://opencollective.com/unified" + } + }, + "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/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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "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", + "url": "https://opencollective.com/unified" + } + }, + "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/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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lowlight": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", + "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "highlight.js": "~11.11.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "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.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "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/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/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/postcss": { + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "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.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "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", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rehype-highlight": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz", + "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-text": "^4.0.0", + "lowlight": "^3.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/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/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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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" + } + }, + "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" + } + }, + "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/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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/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", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "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/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/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", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..ae0bae7 --- /dev/null +++ b/web/package.json @@ -0,0 +1,22 @@ +{ + "name": "gcontext-dashboard", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "highlight.js": "^11.11.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-markdown": "^10.1.0", + "rehype-highlight": "^7.0.2", + "remark-gfm": "^4.0.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "vite": "^6.0.0" + } +} diff --git a/web/public/icon-dark-32x32.png b/web/public/icon-dark-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..f1162e3a4a24d3ccdaeea773f29efa0f50f16bee GIT binary patch literal 1119 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4zl0AZa85pY67#JE_7#My5g&JNk zFq9fFFuY1&V6d9Oz#v{QXIG#NP=YDR+uenMVO6iP5s=4T;_2(k{)$zSS5bM#txu1E zLITN&eq}(~3X1uFG+%FfX+Hx4v%05?V~EG`qgP`yrlm@-f4IL}ckb@;DAW3DFa6yAJXk+zZ&-9%YhwJ)7!DuN zgVJkCv?SR( z`0M5+7~&(hi8;d|DY*s(w-dbuKC1t_}|)hcmaIcv|fNy~c8@~jMO zCsDXc%O z_L|osaq(idNz1IZ+|J&|_nSv`zTQ;z86h+0#w9PzJFR;;S;;y)b8_*o9D`R5L2EC4 z{gb|D$vyoUxy_1?%YI!9^xnjN<6YtItJRm<{Z!LK^L6|Syt*CNOcMJxGj+F~^`=8B zN)IvKj``@>xFy=i>x{shFAX!*Vm5j2>}~rJ`SNbguO)S%^S0hU-+3d;Kx(jrXXV6;YkJlO8c$U}+k1V{ z-(}OT$yqPb)jVw*=7A&}8EP9CSQ!{Z99WQrq9HdwB{QuO vw+79*64!tlG~hOrWag$8mn7yEVCpe8g;?U2D+??k7#KWV{an^LB{Ts50r%b~ literal 0 HcmV?d00001 diff --git a/web/public/icon-dark-48x48.png b/web/public/icon-dark-48x48.png new file mode 100644 index 0000000000000000000000000000000000000000..660d5eced8cb466ccf84e202e929353a87695758 GIT binary patch literal 1882 zcmZ{lXHe4%7RCQhMUbKpg^;Kq0U`lI2?R)Di}Xa0CMAL)KteOrP>dqTfJzs9NKuMl zD8bN+(p3aOOeSJzLc~WCo}hGi3%Yr;J3F(pvmeg=%`Io{$CGGlV<966l>`7l#>&#v zUPRr$C9zLbcct{CML6MQX>SbxOgI3<#sk1#qEYNu00B&(%l)-yRr7*NlNp zOdhqspO&J{WCM~@GYF(61#d)=OtS1gZ%EgHz52S~$GL-dZ+st5g_Ncj{Cf93X!{m# z1vj^e%dT!!!xkoe<3UZN^4Nn#u-j27IUq~_FTlzd}!kB$P#u-E<*3MQma?MXRM0&;~ z;!AU%@Np3=C-oNiy%gTu*27dMj35_|ky{9A`m{Spm`iqsYu8s4_R2JTY0(-PiW%1CrMoyZJlr3Hx?WPo+4c!^PX))?O-K&m4S# zBd#c1zR_(~P5n}Gg-pt8f7VyRoHf_T*jrfB<*38v`QN`9m32$A#Q^rCPj7Mm)(iSi z4U{4klN=a)*I5A0DbO99GLOW|N)VAlOTQbPp=P`1_SGFMk_@9am%QCPe9BOXlMnj+ zOMH5?Sj5P+j_6F444;?pt7;7bNTmKV*V-I}p<8u&L6MEh5RXRW@r@n9YGAD)uc!PT zxo30b!b8c)=Oc&ZIuqG8CT+Uh-oBhHqm79u6-`y0{i@Qn2E%7EuwM14p>I)0O=)`$ ztT&B#kc}cYa-eQnDjx%Sn1eFSxa>*V-D61Xw(3B}k0<$enEmcd&F?u{jqkkRd2!1^ z5aHPJ;nt9GFnk8&s7QCPQzQ-;_JqWM3YO#T$urk~I3Q;$1?+wa*2tuUZ{usntV~E; zrC`iCYQ@c~Dm12f!ZfVm3dP;Vo!&TP97*`AeNl-~&3uQeDr)cWx^Dt`?zc=>Lr7G+ z9O)}hcjc=pSai=SUX>NHqf}u{-A0}s&l2xZ`C1>W&~cUFUR-T8#Z zp`%l0=?`%^*nNo?pTb)#qIi`0iy=dj*2nFxg>bu0;mLCi^0MQ8b>=|<%dr_}*YO)E ztixexlnoaa*Mv>4(bGAXx84VG4CrGT6%E})4_yh07mx2Wwk_)NtxveTk?WU4d*={| z7sn?TxW>I=_tQ)L>INX2M;Bu^8)oLov-sE1fh_2ud3thsXuZTDVj#1jSF`TOSNiP^ zLszdH3^VGpaL;b_YAi^3tD93wBrSO>-MoP{^^ecX} z$C$>lUloi}!f)?kF)Npm;5NGpsWBd`wowCk)zA zt~2Q$ALbn(L>R0Xof(3F{gjRVW7Jp~jO!rE@s)@raCq+*-&C@xD_0esu(BA1VAm_S zH4K|7`@fMb`8J$J+Kry4%lBXlHn&>5^VPLU9yt?2Ew(4C=hGv?#6|SPY=p_No&%Q% z67mJBH8G#2<9t@DoU}0rzv3IqMmMJC)A4u8K>EaB+x9ERdlDFgR3m&nl{*>s#y+qh zC!M3FX|BY}N|gxAwze)98V~53~9#_tQ(H#Z@*( zOgQ%_a1#ecMa-OSBkXkTNbU=8oxH|x)>obWJb=!vUAl?+k^Z%iX&m{pg;rh`Js&P(CZ(-)TxDs_ zZg*!4yxpoj94oz+BNVRnWg)PWy`<@9&W|<5`L0svVd+1g^^fqWQUYE(Z7W-zD_{Ms z?LZsf?F^^hH9FgRl1}q)c$F52zI|Oy0B^P6>}|po;ZbF&NN1m?N8PLYEDYVMc3V;S zq-kukBC9MJiY64h>9k+Q1r>5O%DR5(#KH2e-cj_gIXmA8;u`ZOIzb&jSI#L(a!%ZF zNRAbyhC0K{h2c$MP_bU2R1pA_F3J$8i$Ju#;r7lk7LSeek4zl0AZa85pY67#JE_7#My5g&JNk zFq9fFFuY1&V6d9Oz#v{QXIG#NP=YDR+uenMVO6iP5s=4T;_2(k{)$zSS5fOAo0mIK zNFX`UuM9|AK`|eY=Id=Q?Pp+MX7F@z4DmRA^lH4%)KUrd5B$$3C^b3!k=!EM(HWKz z9q3e_an@*)>)MsBJtaH0nm&8AXixUp*?Ff{hpjg4*^!YwBR4nikjg+TW03P z+;>Q!qtHpv+)sSY=Y7u|Qp)YlpZ)#k_rH1fKii%E{5UQ68rQF)*`eB7*us5xzu0r} zKX<@d$BBlA)y{gJ2wW7jbN`&Ohi(knwol7y>%M(AIxWV(>xuPdZRRPrlCO07Oy`*( zt0iOl@BOnNcAJvJt4u3`BQ@Nb*T<}7n=>u1yG!SBV&pdK*S{`l+i2f5Zwz@Rx^tmkXl2lQ$?G$|R2(_|QgVuBv5oG{=_$WM_Nlop5>jIO`@-|HTzI}izwx`^G#l1{ z9T8!qM@_kFb`G)8PwO;T^QPxWoxZ_0U2z8o7A7auOuypX!w_tw|$goCB-Uo4;0 zUZ2C@{O-zZCJiIOmir5jdK$LZuD`u?Nm+mOUd8KG?^SC5hp-E7xHXmGL8YkO^%Y*> z0`v0cJ1&1?{^<6SymPAr6yCHKt*x57J$(N?hAn}6^+Kg;HO2m1TO*L*=NEK0rJ1jE z+Y1r@S4D4%Bm2!J@8feQ7OO7Y$QJDNr^_ifDNW`}cirqstZTO4O;nxtEi!7Ow@-U% zeFXQ7w_TCbkL-P=YiOwKcIL(Ao9_PtW`A2B_a$P+!3DpccF!*IxDsb*yX~ieC1Af|wENhPQsbU9arA3>j7nmF_o?TX{ zsc3RmdQd^Eb-m`dD~|D|7X}b z`_{yH!5v<}JfT|R8c~vxSdwa$T$Bo=7>o>z%ybP*b&X6z42`Xfjjc?Kv<(cb3=G!0 zZ!SR5kei>9nO2Eg!#yePGe8X*a2rZ8b5n~;5_1c1>*?gz5(esF@O1TaS?83{1OQ9B B$xHwM literal 0 HcmV?d00001 diff --git a/web/public/icon-light-48x48.png b/web/public/icon-light-48x48.png new file mode 100644 index 0000000000000000000000000000000000000000..0a1d107fb0d0a2f046c5ef172692841f82fe499a GIT binary patch literal 1757 zcmZ{kX)xOf8^Hh7R4EzMRSZith?aG$tZ1nS35g@l*DB&Dl653>omEHcT9n3BinPwU zg(8ikmX?U4sA7n2x1w>S>7lER&Ca~@&b%M?!}I*+Ieznedomp{NbniOGXMa9?QE@` z1=RZ|L_mUkD5ftifGXbB*#Q9JGyotu4FL89t>g^=h!!kzeE|Sg0s!(6<$X@(f`O1f z8fgsxjlZcI2&&U$I|syR?kNSZn)Z+|3Mptw;{7l&0=oY!c^b#am04Q8d#^RsPK)hdLuBnZ(+K>eCHx_7%1=2$Bbh(WyX2I`ME{ZF_ z*0TtdQdM@8#A5h^(GyM6T4)`f>3?Y89N)*rqi^bM^nh=mar^5F8}1ek?g*<DJyjCU3ght<x*PU5#n{pl7waQpv~rpaop^u z(D^$ost@1(PLsBax@6Bxuo#F7pL;j0xG64*jHp~Dh(m&MU4ueD7j-JQzv2&c40694*{sQDfS>}M6U#}NHpo!PNqHc<>NZFgLj!v%;&<>HK8rxLV zpP3pXGjD0$7vGyTjcs>;6lwmg=^71ajh-n5&NWb>PI2H}MQQWa=Zv;0zDb zArAy5?O=fA5_N#+GGLAnyEPm9W0r__M*Nc=EfuQFRyA@&U^{43`3IASyTvwXBK?rs zp|h#Y@|SNomsu<*3EfQ?I!6bYKsRQmk8MU$A#oBeVm&VPt z+vYvd6VZ$7kI&BTWa%?N=`9;5rGAfIx$v;3_6-t5>?~5pbVh$!Cf@5|;#bX(cV5z@ z0@atZ4`WnhJ)vx$c!zHLwY+LReBSvs5#tfi;^qnsw)Cb~GeC@i6@KzxElZCVrE6h< zVP0ve<{<@n#Cy9T!^-?5MJw~$rHQ0bDpY3JRhvAhE3IStCO)?gJTPpq*!_L*P%f(> z3W}(NxHlD7*(IGBB1mM%z6e&)XDaM+ZI$}xjN}PY?au9wVrtDKovM0^*Qi3V@NqS_ zbK7moSW|~jcXR}f@~%s zdt&-Asu=ZdF78KMI#Qv)JC2&P^-*O9EOZ;AuPiiR{9J6wdpAT`C{pSD#hY$*#m>3S zX$dpfv!nvYpKp4XD#P?wWi&^e0`5;e(+Ta{zi+hD_Ck2UwXA;fwWDly$%kM4H)BXY zv=9Co963Yk08xEt<80*!mRN{IOwWbd3H-NCyxK9^p1WGIauORBG0@o)LqfC2t!@vD z-r@bm+N~N4k0kk(v1NUxL8g`yx|E}(LY5S^O%ZZY&ayq7qc+r;^ zVC=0EbeHC<-tm>q=qgOCFt;_X#`Hrxr+Y#`V=LRxeIM>|hYAB+NG&*SZD|&iFFsJ~c-V zxeJr6ZLoU+Kbd~q-R|SSxLn6>S)TJG^ue~Did=csIl5oEKexmCzLury(jlTnL$DN zTJi74Vn4&AtOA<0e<8_7(Oac0Kh=c zz(hySSjWH^r*8-|G=v!$T-4Kp>FKp1J0t#2AchBo1jYWlu&O9FArQ3wcYz#23?z~L Xh!OwpWJ+jD3mm`>fw69}@=f_0Q$Z@Q literal 0 HcmV?d00001 diff --git a/web/public/icon.svg b/web/public/icon.svg new file mode 100644 index 0000000..43887d3 --- /dev/null +++ b/web/public/icon.svg @@ -0,0 +1,12 @@ + + + + + diff --git a/web/public/logo-black.png b/web/public/logo-black.png new file mode 100644 index 0000000000000000000000000000000000000000..1b9772cf96a91ead405c65de7b28005454fcc963 GIT binary patch literal 2627 zcmaKuc{J1u8^?b}vP2`s5*107ER8AV&VrD1c`KqOp9RS=^+v_I)z>k0CivSP}1pvMe03h=KKrXzf&Czgg0P?f3 zKmof+SwmfWl`z4|&Rm!;BrmHa%-&+z0)VKx73u=|I(M0e%SN+gy1!mwinr*&KzCG? zRcfo^Z9Jip$s(evYI&&Fi5mMQKjc!>@RoxVOaYqr#@fnBPs;I9k7SS3hxC{6KAI|3 z5$4V)lOmFUFsHY(Hni|Ldth5V$!I% zI;!j>u434L6Z1*2)I_)j=?wles0I}g7$5SO&fuwpZNEA7JkwB6l@Q%|fYvzaZWCAO zo-sT-<##kX;!Y!Xaz5_#%~9C%EQlb5zYw|bE~E@GjSes)56)>3i&`YYi*J3iGkBY8 z=9>`gv1^|yW+ zePugKQ#co(L#xm?%!soegLgjW+vIaQgmPcLL_A6&tdb)Rm4!h+1A6WoadOh&=L zGfqw80RF&<2Ex)TQqB`k)yp8n6flP`D;5M{6h9EsGMw|)W5m)K6Nr4et~2?wMh>-& zR{)6&;A(|kuD5vrzQQmtFSCiuP05t=e+9f<{5%HX%PI2l>UfU7plU;O%9CE=67P!} zyiLn2jtLlPM|gFd|XKaB);9CA#ZF{kW~M<*_W zNKfL-59mOu+ub!>G$kkb<@@p4j<`R{J?kBm^gHjHR-+S5e26)`$_oaf`{q?sG?QHG zt~9c)6>SYF)(tLY;s_l=#Zn@F5in9Iu+xX{hi;D31T)V}zA!)yLKGTLg0hx9G_uC+ zJJ}N7*GYJ#$&bP03k>}c9<%G`BzKjiHvV%WfHNnXb&*{e0-LX)_u&LDE%$mGNMFZ9S|*(|tLABwUtdWl*>-oT=CZ!E2w zIj|(AY2n>@ny}Xip7a^6$8DNv^?e5+P#OV5taSZWf=QTB)wAEd$;kQ|V+~flw>BRi zn#q7B4pDtgzdfXna;QEvF50;6lLu{^z2sct*NdNEN}m+_WWMDOoL-ey_g6a=c<`-j zwM`|8hyju2?DxZyX69aGrfNjnXy|(zyWh}U6Yh%5_P0oDDIz8NH@Gm;DNoO!=LFu2 zn39zHUUGWQ&BpH{jfv%0{r90Vh0<)A;NQ02(Ek2!Sr#A?KU^fMIre;%>Q#cqIy_NB z-$uFV#K5iHTHI$<-?4!i3V%`hLc+_wHd4N984r5n`-JqN0Is|y*u%C}P;gBstbkSe z(|TEEp&v-JoEuuo6_ET!hILD`i@C&AWVdt-QAFmzR3N7 zb<_ukHoKG#{>+~?sjHXqBU>F(oUu48Q+)V3Pj>B~T<7Mnt&?A2Go{QmS3LWg!%NTv z8`?A~y-^xgYaQ^Zmvbd-LgG89qSqv_9AjnOaM$vhoJz-$P!+#bmc-n`@a7G}raskk z_i{C1rlK3yhuk;%1k;RiT8BnnxoOnz7x{G3YuMv(72shQDm?Nv1j=krR@jyYWb;!J zX3Orw`_!#ow_>t0MaT5IvAcF{ju<#)b3XEKapKvSJV?ZRMRzTH`kTxSgop0kc?e2iKk8!yYyo0V-M8@QI?yB+(rp3Y4#@z#hgT1k2My`oT2 zUBX1C(e#go%{z~BCl$f-SL-dD8dhmVUx*pJYlHU$ycw+bJK|0Kt2?lLTepKG@kSX5 zIIQ|Q*VtKx?zDjF!iJ`@g$L8tw85euhn#aid4sj~fwBx{JsqAQKqN0KIEt^aaYyvo zdEIel>KpR8yzQJ%krm%bAsIY#1)qSiT<~#oY{EeOj|qf{bAe=I%(rF}pR6S(OW6Xt z3eC3Ldu-;|OlW)8_dim}Up&jTe(O*JbvvEFv+QC{YB z&;}`aD%5rqTbEz0WSpe7U5luFecr}w=c`o>3we>wR+si-4jr%CQfp&rO>;J_f@Ye> zLUjrktAn2m<*0EXn$f9x#b?j&ZB3|*^$-*#7`nHvlot+bgp~_CuC;o^vCmKG{PdyH zt@p^N3**t@b20ae42gBthfd5V-(^2NzVslynfuDKo(tW&jib)Gg?yB%pSu0MSnf2T zi>qDm9;OtlOv`Bc%h|7i)i~Tsq^!6zj{SCv{mUP+MRnLwo3oxiJ2>@#O-t(fksbDQ zuKJwLRs9&aS;lD6O3Uxij*j5`C8r}Lf#Ovc-GfO7A|E(8BiKfl>EX8(H0w>}^6L8I zo(*s7-%q%Wt%Y6a!WMm=+8*b)Q%9I_V-;JJr;8ozo^h(sFP~{uK5E!yFhGzP|%r_G?Be8>LkJ3mEiA72tZzq4A?UO289{u zK=pNCXVEYi^6VKT6s`q@BB9VLclo~m1B8V6R8GD1PkMF zCaP8G9E+%vv6UGBgh?IG;{jmrAAbn|0+9f);s5}cTmaznEoikoe{6sqO^x({zeySI zI*vOC31?;iSz+QoEejdl9l-+tTR2W%mk=_sRuJw>m>1}YZp-&X=V$VJS8zIruAH4N zFR?u~wvY!i(b<0M1ws{H7V7@|Nv87)=zWvgz zM;p749pBGkO|<^$N1WTvsJfrM zBnGic6M{l;@?vy$D;yWTbxIOp`5{x%av5hR4iledGJf>2v8q6EI}ejF9US(Qt%7DKRI2Y0FPEnjZG(;R&e8;^{h1PPsxP#vgS zqehpenlgyoVabJ`aRtK0%&p1AvbUGT^Ya1qo*I(q0+N@^_EnhlS0Sp408Q$4bmqps zwJ!A>TGX}=q?Fkedf;6(L+qUJ;`H_&dyhG8lOdR))*t(h2vO7h^Tn!Y@JV8l6`m=i zpSBUteUCOtnF}n?HL1`-Vh%n*;FedIE3aS`tV33WWjY1GK@}T$!JB!1^P}=?uZ#WRE#`Cl4KmN|Pcq{s95+YxQ)5uQN@b z(n4MC>s1bW6NCWxWc?W0Yk$LaqHcxv-c}W~%=Xy@h;^{PdM{ zL#zrg8h}h5mLE&K6*SBQj_RCNlOqMfdYE zt0Hrv1i3+3s=p&f#U>MI+LW85mq)R475Ht&n^&Zjmyo6Y1jJeYR zuY-&^(Zv_f?Z+SII8~CB$=2jAJ?bQXSUMTwe+*7k(C_L2!v!sO4S9O_ab#0lA(f}ypUJl+op=a zxd3i4csrZx(icJam6@F=qMIZ%o;^bK@!S9?ZTaNzVbkL zNOwTSUp6jeiU*)hHw3)Vln2obujh6(-c+=%+uENLi}$vsR{W8_^ugvaoOt&U@Wh=S zrZY{cbDXW4VlT#3t1K{V;wwM9t?*9rWVetxbtQ2HuZ}>bR_GihVKPDZC3B+Nlqst( zD402U;+xP5W*zo!;0v8ZSMfl)!iW(Auu8ifvx-5L>)w7)taJVm>!{wi`OnQcT6j;1HaTrZQ<}6KBI$ZQ>nd zGXl`zk#VB=22I=f1yTa${=C+7YU+$_=DqY^f(C?{JT43wdL*Ro@v|QyJoz{zEHP2U%%u16YhtKo!<8@r=wHTvec*( zK5Zq@dBqHrGPm=KVTpu6YcU*Vd7N8`I9~p#$>xdeI>-D9dE>zB6gFe$`r9*(Erx}Z zh&E#>c}0RAEvHmomxBJj9;a5FV3A%8$HDfYZxiLtg_%-@Yd-7s2UqL6xSIRlWQh)c zo}zk?;A626JdddWA*tFNGP5Vm6=oyo8X_vM&in7)@Rb&j>>Yw!8y-E|V`YW7+!U#h9f?;eF+&r0XJ{lnMd6&LZ z#PPy_q0(4jfmD+sc;8J^8sD}ZTDuS$WZ1c7d?x#?Gw;K>fZ<$8c(xuaQwtB8AkKyO z4l!o-g}vx%*1g19$|a_9;Wxsn6A8YLQzE{uOCq?TjZ&$=0}J`qp;R4xyUWE=r_!&< z&hRhxe+;x>Ds?K*{fkjuKKi4n_+xn1U6_-VV^3{KSySwi(BO_hUah$5GTP^di0%e- zvcgr9hwstU@G$%ko3zy+O)*-f=8-f@Ts8SWZAC6%2sAsdL8b+*$lkdDl9x@XP0#!o zm*bXDd%56b-JPu4(-P}8#oNTYb;%kZMLM^Xf5TKC;5YZL<(@%@R@PZm3I5JSU4ZQ! zvW(rSd0#zbH;<4ZP?VEfcSgotsUAnf?Uhvk@utcN83+u;1uynRjn1 zXG3BhA18*X|EM^<|85rU^O(vtc0pDX6y+eDCUdnDtI*x;jc!OA9Y8DFiC8oQD5Uf{ zF)O577;&mL^KW|0FE8V}(o*$!zcD6N zl~cH?4z5OGC1@PeyUDln^#>aUO!(HCBE7Xs91;4foQ?nQwR_pg2mT8wym(vV?S^;D z@n new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); + +function dayLabel(ts) { + const d = new Date(ts), now = new Date(); + const day = (a) => new Date(a.getFullYear(), a.getMonth(), a.getDate()).getTime(); + const diff = Math.round((day(now) - day(d)) / 86400000); + if (diff === 0) return "today"; + if (diff === 1) return "yesterday"; + return d.toLocaleDateString([], { month: "short", day: "numeric" }); +} + +function Row({ e, open, onToggle }) { + const kindColor = e.error ? C.danger : e.kind === "prompt" ? C.ok : C.t3; + return ( +
+
+ {fmtTime(e.ts)} + {e.error ? "error" : e.kind} + {e.name} + {e.detail} + {e.tokens_out ? `~${e.tokens_out} tk` : ""} +
+ {open && e.preview && ( +
+          {e.preview}{e.preview.length >= 400 ? "\n┅ first 400 chars, the agent received the rest too" : ""}
+        
+ )} +
+ ); +} + +export default function Activity() { + const [events, setEvents] = useState(null); // newest first + const [err, setErr] = useState(null); + const [open, setOpen] = useState(null); // event id expanded + const timer = useRef(null); + + const load = () => getJSON("/api/events?limit=300") + .then((d) => { setEvents(d.events.slice().reverse()); setErr(null); }) + .catch((e) => setErr(e.message)); + + useEffect(() => { + load(); + timer.current = setInterval(() => { if (!document.hidden) load(); }, 3000); + const onFocus = () => { if (!document.hidden) load(); }; + window.addEventListener("focus", onFocus); + return () => { clearInterval(timer.current); window.removeEventListener("focus", onFocus); }; + }, []); + + if (err) return

{err}

; + if (!events) return

loading…

; + if (events.length === 0) { + return

no activity yet. Events appear here as harnesses connect and call tools. The feed empties on restart.

; + } + + return ( +
+
activity · newest first · empties on restart
+ {events.map((e) => ( + + {e.kind === "connect" && ( +
+ session · {e.name} {e.detail} · {dayLabel(e.ts)} {fmtTime(e.ts)} +
+ )} + {e.kind !== "connect" && ( + setOpen(open === e.id ? null : e.id)} /> + )} +
+ ))} +
+ ); +} diff --git a/web/src/App.jsx b/web/src/App.jsx new file mode 100644 index 0000000..9534790 --- /dev/null +++ b/web/src/App.jsx @@ -0,0 +1,74 @@ +import React, { useEffect, useState } from "react"; +import { getJSON } from "./lib.js"; +import { C, mono } from "./ui.jsx"; +import Overview from "./Overview.jsx"; +import Files from "./Files.jsx"; +import Activity from "./Activity.jsx"; + +// Read-only local dashboard for one gcontext project: a plain sidebar and +// three views. Every view fetches fresh from /api/*; refetch on tab focus. + +const SECTIONS = ["overview", "files", "activity"]; +const savedSection = () => { + const s = localStorage.getItem("gc.section"); + return SECTIONS.includes(s) ? s : "overview"; +}; + +function Sidebar({ section, setSection, project, sessions }) { + return ( + + ); +} + +export default function App() { + const [section, setSection] = useState(savedSection); + const [project, setProject] = useState(null); + const [sessions, setSessions] = useState([]); + const [err, setErr] = useState(null); + useEffect(() => { localStorage.setItem("gc.section", section); }, [section]); + + const refresh = () => { + getJSON("/api/project").then((p) => { setProject(p); setErr(null); }).catch((e) => setErr(e.message)); + getJSON("/api/sessions").then((d) => setSessions(d.sessions)).catch(() => {}); + }; + useEffect(() => { + refresh(); + const onFocus = () => { if (!document.hidden) refresh(); }; + window.addEventListener("focus", onFocus); + document.addEventListener("visibilitychange", onFocus); + return () => { window.removeEventListener("focus", onFocus); document.removeEventListener("visibilitychange", onFocus); }; + }, []); + + return ( +
+ +
+
+ {err && ( +

+ cannot reach the server: {err}. Is `gcontext up` running? +

+ )} + {section === "overview" && } + {section === "files" && } + {section === "activity" && } +
+
+
+ ); +} diff --git a/web/src/Files.jsx b/web/src/Files.jsx new file mode 100644 index 0000000..6a9fd05 --- /dev/null +++ b/web/src/Files.jsx @@ -0,0 +1,106 @@ +import React, { useEffect, useMemo, useState } from "react"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import rehypeHighlight from "rehype-highlight"; +import { getJSON } from "./lib.js"; +import { C, mono, label } from "./ui.jsx"; + +// Files = read-only browser over the project folder. Left: the tree from +// /api/tree (secrets.env and machine folders are excluded server-side). +// Right: the selected file, markdown rendered, everything else plain text. + +function buildTree(entries) { + const roots = []; + const byPath = {}; + for (const e of entries) { + const node = { ...e, children: [] }; + byPath[e.path] = node; + const slash = e.path.lastIndexOf("/"); + if (slash === -1) roots.push(node); + else byPath[e.path.slice(0, slash)]?.children.push(node); + } + const sortNodes = (nodes) => { + nodes.sort((a, b) => (b.dir - a.dir) || a.name.localeCompare(b.name)); + nodes.forEach((n) => sortNodes(n.children)); + }; + sortNodes(roots); + return roots; +} + +function TreeRow({ node, depth, selected, open, onToggle, onSelect }) { + const isSel = selected === node.path; + return ( + <> + + {node.dir && open.has(node.path) && node.children.map((c) => ( + + ))} + + ); +} + +function Viewer({ path }) { + const [file, setFile] = useState(null); + const [err, setErr] = useState(null); + useEffect(() => { + if (!path) return; + setFile(null); setErr(null); + getJSON(`/api/file?path=${encodeURIComponent(path)}`).then(setFile).catch((e) => setErr(e.message)); + }, [path]); + + if (!path) return

pick a file on the left

; + if (err) return

{path}: {err}

; + if (!file) return

loading…

; + + return ( +
+
+ {file.path} · {file.size} B +
+ {path.endsWith(".md") ? ( +
+ {file.content} +
+ ) : ( +
{file.content}
+ )} +
+ ); +} + +export default function Files() { + const [entries, setEntries] = useState(null); + const [err, setErr] = useState(null); + const [selected, setSelected] = useState(null); + const [open, setOpen] = useState(() => new Set(["connections", "modules"])); + + useEffect(() => { getJSON("/api/tree").then((d) => setEntries(d.tree)).catch((e) => setErr(e.message)); }, []); + const roots = useMemo(() => buildTree(entries || []), [entries]); + const toggle = (path) => setOpen((prev) => { + const next = new Set(prev); + next.has(path) ? next.delete(path) : next.add(path); + return next; + }); + + if (err) return

{err}

; + if (!entries) return

loading…

; + + return ( +
+
+
project
+ {roots.map((n) => ( + + ))} +
+
+ +
+
+ ); +} diff --git a/web/src/Overview.jsx b/web/src/Overview.jsx new file mode 100644 index 0000000..e536d88 --- /dev/null +++ b/web/src/Overview.jsx @@ -0,0 +1,129 @@ +import React, { useEffect, useState } from "react"; +import { getJSON, copyText, relSeen } from "./lib.js"; +import { C, mono, label } from "./ui.jsx"; + +// Overview = the whole project on one page: sessions, how to connect, +// connections, modules, commands, and the context ledger. Plain lists. + +function CopyLink({ text }) { + const [done, setDone] = useState(false); + return ( + + ); +} + +function Section({ title, children }) { + return ( +
+
{title}
+ {children} +
+ ); +} + +const row = { display: "flex", alignItems: "baseline", gap: 10, padding: "6px 0", borderTop: `1px solid ${C.borderInner}`, fontSize: 13, flexWrap: "wrap" }; +const dim = { fontFamily: mono, fontSize: 11.5, color: C.t3 }; + +export default function Overview({ project, sessions }) { + const [conns, setConns] = useState([]); + const [mods, setMods] = useState([]); + const [cmds, setCmds] = useState([]); + const [ledger, setLedger] = useState([]); + + useEffect(() => { + getJSON("/api/connections").then(setConns).catch(() => {}); + getJSON("/api/modules").then(setMods).catch(() => {}); + getJSON("/api/commands").then(setCmds).catch(() => {}); + getJSON("/api/ledger").then((d) => setLedger(d.ledger)).catch(() => {}); + }, []); + + if (!project) return

loading…

; + + const url = `${location.origin}/mcp`; + const connectCmd = `claude mcp add --transport http ${project.name} ${url}`; + const archived = Object.entries(project.archived || {}).map(([cat, items]) => `${items.length} ${cat}`).join(", "); + + return ( +
+

+ {project.description || "No description in gcontext.yaml."} +

+

{project.project_dir}

+ +
+ {sessions.length === 0 &&

none. Attach a harness with the command below

} + {sessions.map((s, i) => ( +
+ {s.client} + {s.version} + + last activity {relSeen(s.last_seen)} +
+ ))} +
+ {connectCmd} + +
+

any MCP client: {url}

+
+ +
+ {conns.length === 0 &&

none. Add connections/<service>/connection.yaml

} + {conns.map((c, i) => ( +
+ {c.name} + + {c.ready ? "ready" : "missing " + c.secrets.filter((s) => !s.filled).map((s) => s.name).join(", ")} + + {c.description} +
+ ))} +
+ +
+ {mods.length === 0 &&

none

} + {mods.map((m, i) => ( +
+ {m.name} + v{m.version}{m.tags?.length ? " · " + m.tags.join(", ") : ""} + {m.description} +
+ ))} +
+ +
+ {cmds.length === 0 &&

none. Drop .md or .py files into a commands/ folder

} + {cmds.map((c, i) => ( +
+ {c.name} + {c.error + ? malformed: {c.error} + : {c.description}} + {!c.error && } +
+ ))} +
+ +
+ {ledger.map((p, i) => ( +
+ {p.id} + {p.label} + {p.status} + {p.detail} +
+ ))} +
+ +

+ {project.has_instructions ? `instructions.md · ${project.instructions_lines} lines` : "no instructions.md"} + {archived ? ` · archive: ${archived}` : ""} + {` · gcontext ${project.version}`} +

+
+ ); +} diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000..9b18093 --- /dev/null +++ b/web/src/index.css @@ -0,0 +1,45 @@ +* { box-sizing: border-box; } +html, body, #root { height: 100%; } +body { + margin: 0; + font-family: 'IBM Plex Sans', system-ui, sans-serif; + -webkit-font-smoothing: antialiased; + color: #1C1B19; +} +::selection { background: #1C1B19; color: #fff; } +textarea, input, button { font-family: inherit; } +:focus { outline: none; } +:focus-visible { outline: 2px solid #c2603a; outline-offset: 2px; } +.gc-scroll::-webkit-scrollbar { width: 10px; height: 10px; } +.gc-scroll::-webkit-scrollbar-thumb { + background: #DAD6CF; border-radius: 3px; + border: 3px solid transparent; background-clip: content-box; +} +.gc-scroll::-webkit-scrollbar-track { background: transparent; } + +/* rendered markdown (file read view) */ +.gc-md { font-size: 13px; line-height: 1.7; color: #2a2724; } +.gc-md > :first-child { margin-top: 0; } +.gc-md > :last-child { margin-bottom: 0; } +.gc-md h1, .gc-md h2, .gc-md h3, .gc-md h4 { line-height: 1.3; font-weight: 600; margin: 1.4em 0 .5em; } +.gc-md h1 { font-size: 1.5em; } .gc-md h3 { font-size: 1.12em; } .gc-md h4 { font-size: 1em; } +.gc-md h1 { padding-bottom: .28em; border-bottom: 1px solid #ECE8E1; } +/* h2 = mono overline with a trailing hairline */ +.gc-md h2 { display: flex; align-items: center; gap: 10px; margin: 22px 0 11px; font-family: 'IBM Plex Mono', monospace; font-size: 10.5px; font-weight: 600; letter-spacing: .13em; text-transform: uppercase; color: rgba(0,0,0,.58); } +.gc-md h2::after { content: ""; flex: 1; height: 1px; background: #ddd7cb; } +.gc-md > h2:first-child { margin-top: 4px; } +.gc-md p, .gc-md ul, .gc-md ol, .gc-md blockquote, .gc-md table { margin: 0 0 .85em; } +.gc-md ul, .gc-md ol { padding-left: 1.5em; } +.gc-md li { margin: .2em 0; } +.gc-md a { color: #C2603A; text-decoration: none; } +.gc-md a:hover { text-decoration: underline; } +.gc-md code { font-family: 'IBM Plex Mono', monospace; font-size: 11px; background: #F4F1EB; border: 1px solid #ece8e1; padding: 1px 6px; border-radius: 5px; color: #1f1d1a; } +.gc-md pre { background: #F4F1EB; padding: 13px 15px; border-radius: 8px; overflow: auto; margin: 0 0 .85em; } +.gc-md pre code { background: none; border: none; padding: 0; font-size: 11.5px; line-height: 1.7; color: inherit; } +.gc-md blockquote { border-left: 3px solid #c9c4b8; padding-left: 1em; color: #4a4640; } +.gc-md .gc-callout p:last-child { margin-bottom: 0; } +.gc-md table { border-collapse: collapse; display: block; overflow: auto; } +.gc-md th, .gc-md td { border: 1px solid #ECE8E1; padding: 6px 11px; text-align: left; } +.gc-md th { background: #F7F5F1; font-weight: 600; } +.gc-md img { max-width: 100%; } +.gc-md hr { border: none; border-top: 1px solid #ECE8E1; margin: 1.4em 0; } diff --git a/web/src/lib.js b/web/src/lib.js new file mode 100644 index 0000000..7972c54 --- /dev/null +++ b/web/src/lib.js @@ -0,0 +1,30 @@ +// The whole data seam: every view reads the local server's /api/* routes. +// The dashboard is read-only; the agent (via MCP) is what changes the project. + +export async function getJSON(path) { + const r = await fetch(path); + // Non-JSON bodies (proxy 502 etc.) must not surface as parse errors. + const d = await r.json().catch(() => ({ error: `${r.status} ${r.statusText}` })); + if (!r.ok || (d && d.error)) throw new Error((d && d.error) || `${r.status}`); + return d; +} + +export function copyText(text) { + if (navigator.clipboard) return void navigator.clipboard.writeText(text); + const ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + document.execCommand("copy"); + ta.remove(); +} + +// "3h ago" / "2d ago": the one relative-time format for last-seen surfaces. +export const relSeen = (iso) => { + const d = iso ? (Date.now() - new Date(iso).getTime()) / 86400000 : Infinity; + if (!isFinite(d)) return "never"; + if (d < 1) { const h = Math.floor(d * 24); return h < 1 ? "just now" : `${h}h ago`; } + return `${Math.max(1, Math.round(d))}d ago`; +}; diff --git a/web/src/main.jsx b/web/src/main.jsx new file mode 100644 index 0000000..9c590b3 --- /dev/null +++ b/web/src/main.jsx @@ -0,0 +1,6 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App.jsx"; +import "./index.css"; + +createRoot(document.getElementById("root")).render(); diff --git a/web/src/ui.jsx b/web/src/ui.jsx new file mode 100644 index 0000000..3db86e9 --- /dev/null +++ b/web/src/ui.jsx @@ -0,0 +1,40 @@ +// Minimal design tokens: warm paper background, ink text, IBM Plex. +// Everything else is plain elements styled inline where they are used. + +import React, { useState } from "react"; +import { copyText } from "./lib.js"; + +export const C = { + bg: "#efece8", + panel: "#fff", + subtle: "#faf8f3", + ink: "#1f1d1a", + t2: "#4A4842", + tMuted: "rgba(0,0,0,.55)", + t3: "rgba(0,0,0,.45)", + border: "#e6e1d6", + borderInner: "#eee7da", + accent: "#c2603a", + ok: "#3d6b4a", + danger: "#a8492a", + amber: "#8a6d2e", +}; + +export const mono = "'IBM Plex Mono', ui-monospace, Menlo, monospace"; + +// Uppercase section label. +export const label = { fontFamily: mono, fontSize: 11, fontWeight: 600, letterSpacing: ".09em", textTransform: "uppercase", color: C.t3 }; + +// The one copy affordance: a small text link that flips to "copied". +// Used for connect commands, slash commands, and file/folder references. +export function CopyLink({ text, children, style }) { + const [done, setDone] = useState(false); + return ( + + ); +} diff --git a/web/vite.config.js b/web/vite.config.js new file mode 100644 index 0000000..e7f99e2 --- /dev/null +++ b/web/vite.config.js @@ -0,0 +1,18 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// Dev server proxies API calls to a running `gcontext up` server. +// Point elsewhere with VITE_API=http://127.0.0.1:4299 npm run dev +const API = process.env.VITE_API || "http://127.0.0.1:4242"; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5179, + strictPort: true, + proxy: { + "/api": { target: API, changeOrigin: true }, + "/status": { target: API, changeOrigin: true }, + }, + }, +});