diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 780e43e..53518d2 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -1,118 +1,118 @@ -# CLAUDE.md - Claude Integrations - -## Project Overview - -Monorepo for all Claude AI browser and service integrations, plus AI workflow tools. - -## Components - -| Directory | Description | Stack | -|-----------|-------------|-------| -| firefox-lsp/ | LSP for Firefox via Marionette protocol | Elixir | -| firefox-mcp/ | MCP server for Firefox browser control | Deno/JS | -| gecko-browser-extension/ | Extension for Gecko browsers | JS/WebExtensions | -| gitlab-bridge/ | Claude ↔ GitLab integration | ReScript/Deno | -| mozilla-extension/ | Claude extension for Mozilla products | JS/WebExtensions | -| **model-router/** | **Auto-select Opus/Sonnet/Haiku by task** | **Rust (planned)** | - -## Build - -```bash -just --list # See all build recipes -just build-firefox-lsp # Build Firefox LSP -just build-firefox-mcp # Build Firefox MCP -just build-gecko-extension # Build Gecko extension -just build-gitlab-bridge # Build GitLab bridge -just build-mozilla-extension # Build Mozilla extension -``` - -## Model Router (NEW - Sonnet Task) - -### Purpose - -Auto-select the right Claude model (Opus/Sonnet/Haiku) based on task complexity, saving money on simple tasks and ensuring quality on complex ones. - -### Architecture - -``` -User prompt + repo context - ↓ -Haiku classifier (< 1 second, < $0.001) - ↓ -Classification: trivial | standard | complex - ↓ -Route to: - ├── Haiku: single-file edits, template creation, simple queries - ├── Sonnet: multi-file implementation, feature work, testing - └── Opus: architecture decisions, debugging, cross-repo design -``` - -### Classification Signals - -| Signal | Haiku | Sonnet | Opus | -|--------|-------|--------|------| -| Files affected | 1 | 2-5 | 5+ | -| Task type | rename, typo fix | add feature, implement | debug, design, architect | -| Prompt keywords | "fix typo", "rename", "update" | "add", "create", "implement" | "why", "how", "design", "debug" | -| Repo complexity (from CLAUDE.md) | Low | Medium | High | -| Error recovery needed | No | Maybe | Likely | -| Cross-repo awareness | No | No | Yes | - -### Implementation Plan (for Sonnet) - -1. **Create `model-router/` directory** in this repo -2. **Rust CLI tool** that: - - Reads the prompt from stdin or argument - - Reads `.claude/CLAUDE.md` from current directory for repo context - - Calls Haiku API to classify complexity (one fast API call) - - Outputs recommended model to stdout: `haiku`, `sonnet`, or `opus` -3. **Claude Code hook integration**: - - Pre-prompt hook reads model recommendation - - Switches model via `/model` command or API parameter -4. **Escalation logic**: - - If chosen model fails or stalls (detected by output quality heuristics) - - Automatically escalate: Haiku → Sonnet → Opus -5. **Cost tracking**: - - Log which model was used, tokens consumed, task outcome - - Weekly summary of cost savings vs always-using-Opus - -### Cargo.toml Sketch - -```toml -[package] -name = "model-router" -version = "0.1.0" -authors = ["Jonathan D.A. Jewell "] -license = "PMPL-1.0-or-later" - -[dependencies] -clap = { version = "4", features = ["derive"] } -reqwest = { version = "0.12", features = ["json"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -tokio = { version = "1", features = ["full"] } -``` - -### Usage (Target) - -```bash -# Classify a task -echo "fix the typo in README.md" | model-router classify -# Output: haiku - -echo "implement the sweep subcommand for panic-attack" | model-router classify -# Output: sonnet - -echo "why is my system crashing and how should we architect the fix" | model-router classify -# Output: opus - -# With repo context -model-router classify --repo /path/to/repo "add authentication" -# Reads .claude/CLAUDE.md, factors in repo complexity -``` - -## Code Style - -- SPDX headers: `PMPL-1.0-or-later` -- Author: Jonathan D.A. Jewell -- Browser extensions use `MPL-2.0` fallback (store requirements) +# CLAUDE.md - Claude Integrations + +## Project Overview + +Monorepo for all Claude AI browser and service integrations, plus AI workflow tools. + +## Components + +| Directory | Description | Stack | +|-----------|-------------|-------| +| firefox-lsp/ | LSP for Firefox via Marionette protocol | Elixir | +| firefox-mcp/ | MCP server for Firefox browser control | Deno/JS | +| gecko-browser-extension/ | Extension for Gecko browsers | JS/WebExtensions | +| gitlab-bridge/ | Claude ↔ GitLab integration | ReScript/Deno | +| mozilla-extension/ | Claude extension for Mozilla products | JS/WebExtensions | +| **model-router/** | **Auto-select Opus/Sonnet/Haiku by task** | **Rust (planned)** | + +## Build + +```bash +just --list # See all build recipes +just build-firefox-lsp # Build Firefox LSP +just build-firefox-mcp # Build Firefox MCP +just build-gecko-extension # Build Gecko extension +just build-gitlab-bridge # Build GitLab bridge +just build-mozilla-extension # Build Mozilla extension +``` + +## Model Router (NEW - Sonnet Task) + +### Purpose + +Auto-select the right Claude model (Opus/Sonnet/Haiku) based on task complexity, saving money on simple tasks and ensuring quality on complex ones. + +### Architecture + +``` +User prompt + repo context + ↓ +Haiku classifier (< 1 second, < $0.001) + ↓ +Classification: trivial | standard | complex + ↓ +Route to: + ├── Haiku: single-file edits, template creation, simple queries + ├── Sonnet: multi-file implementation, feature work, testing + └── Opus: architecture decisions, debugging, cross-repo design +``` + +### Classification Signals + +| Signal | Haiku | Sonnet | Opus | +|--------|-------|--------|------| +| Files affected | 1 | 2-5 | 5+ | +| Task type | rename, typo fix | add feature, implement | debug, design, architect | +| Prompt keywords | "fix typo", "rename", "update" | "add", "create", "implement" | "why", "how", "design", "debug" | +| Repo complexity (from CLAUDE.md) | Low | Medium | High | +| Error recovery needed | No | Maybe | Likely | +| Cross-repo awareness | No | No | Yes | + +### Implementation Plan (for Sonnet) + +1. **Create `model-router/` directory** in this repo +2. **Rust CLI tool** that: + - Reads the prompt from stdin or argument + - Reads `.claude/CLAUDE.md` from current directory for repo context + - Calls Haiku API to classify complexity (one fast API call) + - Outputs recommended model to stdout: `haiku`, `sonnet`, or `opus` +3. **Claude Code hook integration**: + - Pre-prompt hook reads model recommendation + - Switches model via `/model` command or API parameter +4. **Escalation logic**: + - If chosen model fails or stalls (detected by output quality heuristics) + - Automatically escalate: Haiku → Sonnet → Opus +5. **Cost tracking**: + - Log which model was used, tokens consumed, task outcome + - Weekly summary of cost savings vs always-using-Opus + +### Cargo.toml Sketch + +```toml +[package] +name = "model-router" +version = "0.1.0" +authors = ["Jonathan D.A. Jewell "] +license = "PMPL-1.0-or-later" + +[dependencies] +clap = { version = "4", features = ["derive"] } +reqwest = { version = "0.12", features = ["json"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["full"] } +``` + +### Usage (Target) + +```bash +# Classify a task +echo "fix the typo in README.md" | model-router classify +# Output: haiku + +echo "implement the sweep subcommand for panic-attack" | model-router classify +# Output: sonnet + +echo "why is my system crashing and how should we architect the fix" | model-router classify +# Output: opus + +# With repo context +model-router classify --repo /path/to/repo "add authentication" +# Reads .claude/CLAUDE.md, factors in repo complexity +``` + +## Code Style + +- SPDX headers: `PMPL-1.0-or-later` +- Author: Jonathan D.A. Jewell +- Browser extensions use `MPL-2.0` fallback (store requirements) diff --git a/.claude/PROJECT.md b/.claude/PROJECT.md new file mode 100644 index 0000000..b6def45 --- /dev/null +++ b/.claude/PROJECT.md @@ -0,0 +1,31 @@ +# Claude Integrations - Claude Code Instructions + +This repository contains integrations for Claude Code and other AI assistants. + +## Project Structure + +``` +claude-integrations/ +├── .claude/ # AI assistant instructions +├── .git/ # Version control +├── .gitignore # Git ignore rules +├── .editorconfig # Editor configuration +└── ... # Integration files +``` + +## Build Commands + +Refer to individual integration documentation. + +## Coding Conventions + +- Follow hyperpolymath standards +- Prefer declarative configurations +- Document all integration points +- Test all integrations in isolation + +## Security + +- No hardcoded API keys +- All credentials through secure configuration +- Minimal permissions principle diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fc6650c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,68 @@ +# RSR-template-repo - Editor Configuration +# https://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.adoc] +trim_trailing_whitespace = false + +[*.rs] +indent_size = 4 + +[*.ex] +indent_size = 2 + +[*.exs] +indent_size = 2 + +[*.zig] +indent_size = 4 + +[*.ada] +indent_size = 3 + +[*.adb] +indent_size = 3 + +[*.ads] +indent_size = 3 + +[*.hs] +indent_size = 2 + +[*.res] +indent_size = 2 + +[*.resi] +indent_size = 2 + +[*.ncl] +indent_size = 2 + +[*.rkt] +indent_size = 2 + +[*.scm] +indent_size = 2 + +[*.nix] +indent_size = 2 + +[Justfile] +indent_style = space +indent_size = 4 + +[justfile] +indent_style = space +indent_size = 4 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index bf1ccd7..b6c07a8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,27 +1,27 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# Dependabot configuration for claude-integrations -# Covers npm (firefox-lsp, gitlab-bridge) and GitHub Actions - -version: 2 -updates: - # GitHub Actions - always include - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - groups: - actions: - patterns: - - "*" - - # npm - firefox-lsp (VS Code extension) - - package-ecosystem: "npm" - directory: "/firefox-lsp" - schedule: - interval: "weekly" - - # npm - gitlab-bridge - - package-ecosystem: "npm" - directory: "/gitlab-bridge" - schedule: +# SPDX-License-Identifier: PMPL-1.0-or-later +# Dependabot configuration for claude-integrations +# Covers npm (firefox-lsp, gitlab-bridge) and GitHub Actions + +version: 2 +updates: + # GitHub Actions - always include + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + actions: + patterns: + - "*" + + # npm - firefox-lsp (VS Code extension) + - package-ecosystem: "npm" + directory: "/firefox-lsp" + schedule: + interval: "weekly" + + # npm - gitlab-bridge + - package-ecosystem: "npm" + directory: "/gitlab-bridge" + schedule: interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index 0479dab..4881611 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -1,25 +1,25 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# Secret scanning for claude-integrations -# Prevents hardcoded secrets from reaching main -name: Secret Scanner - -on: - pull_request: - push: - branches: [main] - -permissions: - contents: read - -jobs: - trufflehog: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - name: TruffleHog Secret Scan - uses: trufflesecurity/trufflehog@37b77001d0174ebec2fcca2bd83ff83a6d45a3ab # v3 - with: - extra_args: --only-verified \ No newline at end of file +# SPDX-License-Identifier: PMPL-1.0-or-later +# Secret scanning for claude-integrations +# Prevents hardcoded secrets from reaching main +name: Secret Scanner + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + trufflehog: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: TruffleHog Secret Scan + uses: trufflesecurity/trufflehog@17456f8c7d042d8c82c9a8ca9e937231f9f42e26 # v3 + with: + extra_args: --only-verified --fail diff --git a/.github/workflows/security-policy.yml b/.github/workflows/security-policy.yml index cd440e5..4dddfbc 100644 --- a/.github/workflows/security-policy.yml +++ b/.github/workflows/security-policy.yml @@ -1,44 +1,44 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -name: Security Policy -on: [push, pull_request] - -permissions: - contents: read - -jobs: - check: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Security checks - run: | - FAILED=false - - # Block MD5/SHA1 for security (allow for checksums/caching) - WEAK_CRYPTO=$(grep -rE 'md5\(|sha1\(' --include="*.py" --include="*.rb" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" . 2>/dev/null | grep -v 'checksum\|cache\|test\|spec' | head -5 || true) - if [ -n "$WEAK_CRYPTO" ]; then - echo "⚠️ Weak crypto (MD5/SHA1) detected. Use SHA256+ for security:" - echo "$WEAK_CRYPTO" - fi - - # Block HTTP URLs (except localhost) - HTTP_URLS=$(grep -rE 'http://[^l][^o][^c]' --include="*.py" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" --include="*.yaml" --include="*.yml" . 2>/dev/null | grep -v 'localhost\|127.0.0.1\|example\|test\|spec' | head -5 || true) - if [ -n "$HTTP_URLS" ]; then - echo "⚠️ HTTP URLs found. Use HTTPS:" - echo "$HTTP_URLS" - fi - - # Block hardcoded secrets patterns - SECRETS=$(grep -rEi '(api_key|apikey|secret_key|password)\s*[=:]\s*["\x27][A-Za-z0-9+/=]{20,}' --include="*.py" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" --include="*.env" . 2>/dev/null | grep -v 'example\|sample\|test\|mock\|placeholder' | head -3 || true) - if [ -n "$SECRETS" ]; then - echo "❌ Potential hardcoded secrets detected!" - FAILED=true - fi - - if [ "$FAILED" = true ]; then - exit 1 - fi - +# SPDX-License-Identifier: PMPL-1.0-or-later +name: Security Policy +on: [push, pull_request] + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Security checks + run: | + FAILED=false + + # Block MD5/SHA1 for security (allow for checksums/caching) + WEAK_CRYPTO=$(grep -rE 'md5\(|sha1\(' --include="*.py" --include="*.rb" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" . 2>/dev/null | grep -v 'checksum\|cache\|test\|spec' | head -5 || true) + if [ -n "$WEAK_CRYPTO" ]; then + echo "⚠️ Weak crypto (MD5/SHA1) detected. Use SHA256+ for security:" + echo "$WEAK_CRYPTO" + fi + + # Block HTTP URLs (except localhost) + HTTP_URLS=$(grep -rE 'http://[^l][^o][^c]' --include="*.py" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" --include="*.yaml" --include="*.yml" . 2>/dev/null | grep -v 'localhost\|127.0.0.1\|example\|test\|spec' | head -5 || true) + if [ -n "$HTTP_URLS" ]; then + echo "⚠️ HTTP URLs found. Use HTTPS:" + echo "$HTTP_URLS" + fi + + # Block hardcoded secrets patterns + SECRETS=$(grep -rEi '(api_key|apikey|secret_key|password)\s*[=:]\s*["\x27][A-Za-z0-9+/=]{20,}' --include="*.py" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" --include="*.env" . 2>/dev/null | grep -v 'example\|sample\|test\|mock\|placeholder' | head -3 || true) + if [ -n "$SECRETS" ]; then + echo "❌ Potential hardcoded secrets detected!" + FAILED=true + fi + + if [ "$FAILED" = true ]; then + exit 1 + fi + echo "✅ Security policy check passed" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b306ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,82 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# RSR-compliant .gitignore + +# OS & Editor +.DS_Store +Thumbs.db +*.swp +*.swo +*~ +.idea/ +.vscode/ + +# Build +/target/ +/_build/ +/build/ +/dist/ +/out/ + +# Dependencies +/node_modules/ +/vendor/ +/deps/ +/elixir_ls/ + +# Rust + +# Elixir +/cover/ +/doc/ +*.ez +erl_crash.dump + +# Julia +*.jl.cov +*.jl.mem +/Manifest.toml + +# ReScript +/lib/bs/ +/.bsb.lock + +# Python +__pycache__/ +*.py[cod] +.venv/ + +# Ada/SPARK +*.ali +/obj/ +/bin/* +!bin/*.ml +!bin/dune + +# Haskell +/.stack-work/ +/dist-newstyle/ + +# Chapel +*.chpl.tmp.* + +# Secrets +.env +.env.* +*.pem +*.key +secrets/ + +# Test/Coverage +/coverage/ +htmlcov/ + +# Logs +*.log +/logs/ + +# Temp +/tmp/ +*.tmp +*.bak +*.wasm +/a.out diff --git a/.machine_readable/ECOSYSTEM.scm b/.machine_readable/ECOSYSTEM.scm index 566cabb..0cc16c2 100644 --- a/.machine_readable/ECOSYSTEM.scm +++ b/.machine_readable/ECOSYSTEM.scm @@ -1,17 +1,17 @@ -; SPDX-License-Identifier: PMPL-1.0-or-later -; ECOSYSTEM.scm - Ecosystem position for claude-integrations - -(ecosystem - (version "1.0") - (name "claude-integrations") - (type "monorepo") - (purpose "Unified Claude AI browser and service integrations") - - (position-in-ecosystem - (category "ai-tooling") - (role "Browser and service integration layer for Claude AI")) - - (related-projects - (project "claude-code" (relationship "upstream") (notes "Claude CLI tool")) - (project "mcp-repo-guardian" (relationship "sibling") (notes "MCP server for repo enforcement")) - (project "poly-orchestrator-lsp" (relationship "sibling") (notes "LSP orchestration")))) +; SPDX-License-Identifier: PMPL-1.0-or-later +; ECOSYSTEM.scm - Ecosystem position for claude-integrations + +(ecosystem + (version "1.0") + (name "claude-integrations") + (type "monorepo") + (purpose "Unified Claude AI browser and service integrations") + + (position-in-ecosystem + (category "ai-tooling") + (role "Browser and service integration layer for Claude AI")) + + (related-projects + (project "claude-code" (relationship "upstream") (notes "Claude CLI tool")) + (project "mcp-repo-guardian" (relationship "sibling") (notes "MCP server for repo enforcement")) + (project "poly-orchestrator-lsp" (relationship "sibling") (notes "LSP orchestration")))) diff --git a/.machine_readable/META.scm b/.machine_readable/META.scm index 6d17010..a15d5cd 100644 --- a/.machine_readable/META.scm +++ b/.machine_readable/META.scm @@ -1,21 +1,21 @@ -; SPDX-License-Identifier: PMPL-1.0-or-later -; META.scm - Meta-level information for claude-integrations - -(meta - (version "1.0") - (license "PMPL-1.0-or-later") - (author "Jonathan D.A. Jewell ") - - (architecture-decisions - (adr-001 - (title "Consolidate Claude integration repos into monorepo") - (status "accepted") - (date "2026-02-08") - (context "Five separate repos with overlapping concerns and shared patterns") - (decision "Merge into single monorepo with component subdirectories") - (consequences "Simplified dependency management, shared tooling, single CI config"))) - - (development-practices - (build-tool "just") - (license "PMPL-1.0-or-later") - (documentation-format "AsciiDoc"))) +; SPDX-License-Identifier: PMPL-1.0-or-later +; META.scm - Meta-level information for claude-integrations + +(meta + (version "1.0") + (license "PMPL-1.0-or-later") + (author "Jonathan D.A. Jewell ") + + (architecture-decisions + (adr-001 + (title "Consolidate Claude integration repos into monorepo") + (status "accepted") + (date "2026-02-08") + (context "Five separate repos with overlapping concerns and shared patterns") + (decision "Merge into single monorepo with component subdirectories") + (consequences "Simplified dependency management, shared tooling, single CI config"))) + + (development-practices + (build-tool "just") + (license "PMPL-1.0-or-later") + (documentation-format "AsciiDoc"))) diff --git a/.machine_readable/STATE.scm b/.machine_readable/STATE.scm index c659301..1d81af1 100644 --- a/.machine_readable/STATE.scm +++ b/.machine_readable/STATE.scm @@ -1,24 +1,24 @@ -; SPDX-License-Identifier: PMPL-1.0-or-later -; STATE.scm - Current project state for claude-integrations - -(state - (metadata - (version "1.0") - (last-updated "2026-02-08") - (updated-by "Claude Opus 4.6")) - - (project-context - (name "claude-integrations") - (type "monorepo") - (description "Unified Claude browser/service integrations") - (components - ("firefox-lsp" "Elixir LSP for Firefox via Marionette") - ("firefox-mcp" "MCP server for Firefox browser control") - ("gecko-browser-extension" "Gecko browser extension") - ("gitlab-bridge" "GitLab integration bridge") - ("mozilla-extension" "Mozilla products extension"))) - - (current-position - (phase "consolidation") - (status "initial-merge-complete") - (notes "Five repos merged into monorepo structure"))) +; SPDX-License-Identifier: PMPL-1.0-or-later +; STATE.scm - Current project state for claude-integrations + +(state + (metadata + (version "1.0") + (last-updated "2026-02-08") + (updated-by "Claude Opus 4.6")) + + (project-context + (name "claude-integrations") + (type "monorepo") + (description "Unified Claude browser/service integrations") + (components + ("firefox-lsp" "Elixir LSP for Firefox via Marionette") + ("firefox-mcp" "MCP server for Firefox browser control") + ("gecko-browser-extension" "Gecko browser extension") + ("gitlab-bridge" "GitLab integration bridge") + ("mozilla-extension" "Mozilla products extension"))) + + (current-position + (phase "consolidation") + (status "initial-merge-complete") + (notes "Five repos merged into monorepo structure"))) diff --git a/.machine_readable/svc/README.adoc b/.machine_readable/svc/README.adoc index 840ae51..01b9e3b 100644 --- a/.machine_readable/svc/README.adoc +++ b/.machine_readable/svc/README.adoc @@ -1,16 +1,16 @@ -= `.machine_readable/svc/` — Service components for claude-integrations -:toc: - -Service-layer components that sit alongside the contractile trident -but are not themselves verbs. Per **ADR-001 (2026-04-18)**, K9 lives -here rather than under `contractiles/` so the contractile directory -stays a pure six-verb set (intend, trust, must, bust, adjust, dust). - -== Contents - -* `k9/` — K9 self-validating templates and validators. - -== Pointer - -For the verb contractiles themselves, see -`.machine_readable/contractiles/`. += `.machine_readable/svc/` — Service components for claude-integrations +:toc: + +Service-layer components that sit alongside the contractile trident +but are not themselves verbs. Per **ADR-001 (2026-04-18)**, K9 lives +here rather than under `contractiles/` so the contractile directory +stays a pure six-verb set (intend, trust, must, bust, adjust, dust). + +== Contents + +* `k9/` — K9 self-validating templates and validators. + +== Pointer + +For the verb contractiles themselves, see +`.machine_readable/contractiles/`. diff --git a/.machine_readable/svc/k9/README.adoc b/.machine_readable/svc/k9/README.adoc index 1992752..0ebc32d 100644 --- a/.machine_readable/svc/k9/README.adoc +++ b/.machine_readable/svc/k9/README.adoc @@ -1,177 +1,177 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -= K9 Contractiles -:toc: left -:icons: font - -== What Are K9 Contractiles? - -K9 contractiles are self-validating components that combine configuration, validation, and deployment logic in a single file format. They implement the RSR principle of "self-describing artifacts" by embedding contracts and orchestration directly in the component. - -== The Three Security Levels - -K9 components declare their trust requirements using "The Leash" security model: - -[horizontal] -`'Kennel`:: Pure data, no execution (safest) -`'Yard`:: Nickel evaluation with contracts (medium trust) -`'Hunt`:: Full execution with Just recipes (requires signature) - -== Example Components - -This directory contains example K9 contractiles for common repository tasks: - -=== Kennel Level (Pure Data) - -**File:** `examples/project-metadata.k9.ncl` - -Pure configuration data with no execution. Safe to include in any repository. - -**Use cases:** -- Project metadata (name, version, description) -- Build configuration -- Tool settings -- Data schemas - -**Security:** No signature required, data-only. - -=== Yard Level (Validated Config) - -**File:** `examples/ci-config.k9.ncl` - -Configuration with Nickel contracts for runtime validation. Evaluated safely without I/O. - -**Use cases:** -- CI/CD configuration with validation -- Deployment parameters -- Database schemas with constraints -- API specifications - -**Security:** Signature recommended, Nickel evaluation only. - -=== Hunt Level (Full Execution) - -**File:** `examples/setup-repo.k9.ncl` - -Full execution with Just recipes. Can run shell commands and modify filesystem. - -**Use cases:** -- Repository setup scripts -- Deployment automation -- System configuration -- Package installation - -**Security:** **Signature required**, full system access. - -== Usage in Your Repository - -=== 1. Create K9 Components - -Choose the appropriate security level for your use case: - -[source,bash] ----- -# Kennel: Pure configuration -cp contractiles/k9/examples/project-metadata.k9.ncl config/metadata.k9.ncl - -# Yard: Validated configuration -cp contractiles/k9/examples/ci-config.k9.ncl .github/ci.k9.ncl - -# Hunt: Full automation -cp contractiles/k9/examples/setup-repo.k9.ncl scripts/setup.k9.ncl ----- - -=== 2. Validate Components - -[source,bash] ----- -# Validate Nickel syntax and contracts -nickel typecheck config/metadata.k9.ncl - -# Verify Hunt-level signature (if signed) -./must verify scripts/setup.k9.ncl ----- - -=== 3. Execute Components - -[source,bash] ----- -# Kennel: Export as JSON -nickel export config/metadata.k9.ncl > metadata.json - -# Yard: Evaluate with validation -nickel eval .github/ci.k9.ncl - -# Hunt: Run with Just (dry-run first!) -./must --dry-run run scripts/setup.k9.ncl -./must run scripts/setup.k9.ncl ----- - -== Integration with RSR - -K9 contractiles integrate with other RSR standards: - -**STATE.scm**:: K9 components can generate or validate STATE.scm -**ECOSYSTEM.scm**:: K9 can automate cross-repo operations -**META.scm**:: K9 can enforce architectural decisions - -== Security Best Practices - -=== For Kennel/Yard Components - -✅ **Safe to use without signatures** + -✅ **Review Nickel code before use** + -✅ **Validate contracts match expectations** - -=== For Hunt Components - -⚠️ **ALWAYS verify signatures** + -⚠️ **Review Just recipes carefully** + -⚠️ **Run dry-run mode first** + -⚠️ **Never run as root unless required** + -⚠️ **Sandbox external components** - -**See:** https://github.com/hyperpolymath/k9-svc/blob/main/docs/SECURITY-BEST-PRACTICES.adoc - -== Template Files - -Use these as starting points for your own K9 components: - -- `template-kennel.k9.ncl` - Pure data template -- `template-yard.k9.ncl` - Validated config template -- `template-hunt.k9.ncl` - Full execution template - -== Dependencies - -To use K9 contractiles in your repository: - -[source,bash] ----- -# Install Nickel (configuration language) -curl -L https://github.com/tweag/nickel/releases/latest/download/nickel-linux-x86_64 -o nickel -chmod +x nickel && sudo mv nickel /usr/local/bin/ - -# Install Just (task runner, for Hunt level) -cargo install just - -# Clone K9-SVC (for must shim and tooling) -git clone https://github.com/hyperpolymath/k9-svc.git ----- - -== Learn More - -- **K9-SVC Specification:** https://github.com/hyperpolymath/k9-svc/blob/main/SPEC.adoc -- **K9 User Guide:** https://github.com/hyperpolymath/k9-svc/blob/main/GUIDE.adoc -- **Security Documentation:** https://github.com/hyperpolymath/k9-svc/blob/main/docs/SECURITY-FAQ.adoc -- **IANA Media Type:** `application/vnd.k9+nickel` - -== Contributing - -When adding K9 contractiles to your repository: - -1. Use appropriate security level (Kennel > Yard > Hunt) -2. Document what each component does -3. Include validation contracts in Yard/Hunt components -4. Sign Hunt-level components before committing -5. Add K9 validation to CI/CD pipeline - -**Questions?** Open an issue on https://github.com/hyperpolymath/k9-svc +// SPDX-License-Identifier: PMPL-1.0-or-later += K9 Contractiles +:toc: left +:icons: font + +== What Are K9 Contractiles? + +K9 contractiles are self-validating components that combine configuration, validation, and deployment logic in a single file format. They implement the RSR principle of "self-describing artifacts" by embedding contracts and orchestration directly in the component. + +== The Three Security Levels + +K9 components declare their trust requirements using "The Leash" security model: + +[horizontal] +`'Kennel`:: Pure data, no execution (safest) +`'Yard`:: Nickel evaluation with contracts (medium trust) +`'Hunt`:: Full execution with Just recipes (requires signature) + +== Example Components + +This directory contains example K9 contractiles for common repository tasks: + +=== Kennel Level (Pure Data) + +**File:** `examples/project-metadata.k9.ncl` + +Pure configuration data with no execution. Safe to include in any repository. + +**Use cases:** +- Project metadata (name, version, description) +- Build configuration +- Tool settings +- Data schemas + +**Security:** No signature required, data-only. + +=== Yard Level (Validated Config) + +**File:** `examples/ci-config.k9.ncl` + +Configuration with Nickel contracts for runtime validation. Evaluated safely without I/O. + +**Use cases:** +- CI/CD configuration with validation +- Deployment parameters +- Database schemas with constraints +- API specifications + +**Security:** Signature recommended, Nickel evaluation only. + +=== Hunt Level (Full Execution) + +**File:** `examples/setup-repo.k9.ncl` + +Full execution with Just recipes. Can run shell commands and modify filesystem. + +**Use cases:** +- Repository setup scripts +- Deployment automation +- System configuration +- Package installation + +**Security:** **Signature required**, full system access. + +== Usage in Your Repository + +=== 1. Create K9 Components + +Choose the appropriate security level for your use case: + +[source,bash] +---- +# Kennel: Pure configuration +cp contractiles/k9/examples/project-metadata.k9.ncl config/metadata.k9.ncl + +# Yard: Validated configuration +cp contractiles/k9/examples/ci-config.k9.ncl .github/ci.k9.ncl + +# Hunt: Full automation +cp contractiles/k9/examples/setup-repo.k9.ncl scripts/setup.k9.ncl +---- + +=== 2. Validate Components + +[source,bash] +---- +# Validate Nickel syntax and contracts +nickel typecheck config/metadata.k9.ncl + +# Verify Hunt-level signature (if signed) +./must verify scripts/setup.k9.ncl +---- + +=== 3. Execute Components + +[source,bash] +---- +# Kennel: Export as JSON +nickel export config/metadata.k9.ncl > metadata.json + +# Yard: Evaluate with validation +nickel eval .github/ci.k9.ncl + +# Hunt: Run with Just (dry-run first!) +./must --dry-run run scripts/setup.k9.ncl +./must run scripts/setup.k9.ncl +---- + +== Integration with RSR + +K9 contractiles integrate with other RSR standards: + +**STATE.scm**:: K9 components can generate or validate STATE.scm +**ECOSYSTEM.scm**:: K9 can automate cross-repo operations +**META.scm**:: K9 can enforce architectural decisions + +== Security Best Practices + +=== For Kennel/Yard Components + +✅ **Safe to use without signatures** + +✅ **Review Nickel code before use** + +✅ **Validate contracts match expectations** + +=== For Hunt Components + +⚠️ **ALWAYS verify signatures** + +⚠️ **Review Just recipes carefully** + +⚠️ **Run dry-run mode first** + +⚠️ **Never run as root unless required** + +⚠️ **Sandbox external components** + +**See:** https://github.com/hyperpolymath/k9-svc/blob/main/docs/SECURITY-BEST-PRACTICES.adoc + +== Template Files + +Use these as starting points for your own K9 components: + +- `template-kennel.k9.ncl` - Pure data template +- `template-yard.k9.ncl` - Validated config template +- `template-hunt.k9.ncl` - Full execution template + +== Dependencies + +To use K9 contractiles in your repository: + +[source,bash] +---- +# Install Nickel (configuration language) +curl -L https://github.com/tweag/nickel/releases/latest/download/nickel-linux-x86_64 -o nickel +chmod +x nickel && sudo mv nickel /usr/local/bin/ + +# Install Just (task runner, for Hunt level) +cargo install just + +# Clone K9-SVC (for must shim and tooling) +git clone https://github.com/hyperpolymath/k9-svc.git +---- + +== Learn More + +- **K9-SVC Specification:** https://github.com/hyperpolymath/k9-svc/blob/main/SPEC.adoc +- **K9 User Guide:** https://github.com/hyperpolymath/k9-svc/blob/main/GUIDE.adoc +- **Security Documentation:** https://github.com/hyperpolymath/k9-svc/blob/main/docs/SECURITY-FAQ.adoc +- **IANA Media Type:** `application/vnd.k9+nickel` + +== Contributing + +When adding K9 contractiles to your repository: + +1. Use appropriate security level (Kennel > Yard > Hunt) +2. Document what each component does +3. Include validation contracts in Yard/Hunt components +4. Sign Hunt-level components before committing +5. Add K9 validation to CI/CD pipeline + +**Questions?** Open an issue on https://github.com/hyperpolymath/k9-svc diff --git a/.machine_readable/svc/k9/examples/ci-config.k9.ncl b/.machine_readable/svc/k9/examples/ci-config.k9.ncl index c140589..f9af550 100644 --- a/.machine_readable/svc/k9/examples/ci-config.k9.ncl +++ b/.machine_readable/svc/k9/examples/ci-config.k9.ncl @@ -1,126 +1,126 @@ -K9! -# SPDX-License-Identifier: PMPL-1.0-or-later -# Example Yard-level K9 component: CI/CD configuration with validation -# Security Level: Yard (Nickel evaluation, contract validation) -# Signature recommended but not required - -{ - pedigree = { - schema_version = "1.0.0", - component_type = "ci-configuration", - security = { - leash = 'Yard, - trust_level = "validated-config", - allow_network = false, - allow_filesystem_write = false, - allow_subprocess = false, - }, - metadata = { - name = "ci-config", - version = "1.0.0", - description = "CI/CD configuration with runtime validation", - author = "Jonathan D.A. Jewell ", - }, - }, - - # CI/CD configuration with Nickel contracts - ci = { - # Platform must be a known CI provider - platform - | [| 'GitHubActions, 'GitLabCI, 'CircleCI, 'TravisCI |] - = 'GitHubActions, - - # Build matrix with validation - matrix = { - # Operating systems to test on - os - | Array String - | std.array.NonEmpty - = ["ubuntu-latest", "macos-latest"], - - # Language versions to test - versions - | Array String - | std.array.NonEmpty - = ["stable", "beta"], - }, - - # Workflow steps with validation - steps = [ - { - name = "Checkout", - action = "actions/checkout@v4", - # Version must be SHA-pinned for security - sha | String | std.string.NonEmpty = "b4ffde65f46336ab88eb53be808477a3936bae11", - }, - { - name = "Build", - run = "just build", - }, - { - name = "Test", - run = "just test", - }, - { - name = "Lint", - run = "just lint", - }, - ], - - # Deployment configuration - deploy = { - enabled | Bool = false, - - # Only deploy from main branch - branch - | String - | std.contract.from_predicate (fun b => b == "main" || b == "master") - = "main", - - # Deployment requires manual approval - requires_approval | Bool = true, - }, - - # Security scanning - security = { - enabled | Bool = true, - - scanners = [ - { - name = "CodeQL", - languages = ["rust", "javascript"], - }, - { - name = "OSSF Scorecard", - enabled = true, - }, - { - name = "TruffleHog", - scan_for = "secrets", - }, - ], - }, - - # Notification settings - notifications = { - on_success = "never", - on_failure = "always", - channels = ["email"], - }, - }, - - # Validation rules (enforced by Nickel) - validation = { - # At least one OS must be specified - check_os = std.array.length ci.matrix.os > 0, - - # At least one version must be tested - check_versions = std.array.length ci.matrix.versions > 0, - - # Must have at least build and test steps - check_steps = std.array.length ci.steps >= 2, - - # Security scanning must be enabled - check_security = ci.security.enabled == true, - }, -} +K9! +# SPDX-License-Identifier: PMPL-1.0-or-later +# Example Yard-level K9 component: CI/CD configuration with validation +# Security Level: Yard (Nickel evaluation, contract validation) +# Signature recommended but not required + +{ + pedigree = { + schema_version = "1.0.0", + component_type = "ci-configuration", + security = { + leash = 'Yard, + trust_level = "validated-config", + allow_network = false, + allow_filesystem_write = false, + allow_subprocess = false, + }, + metadata = { + name = "ci-config", + version = "1.0.0", + description = "CI/CD configuration with runtime validation", + author = "Jonathan D.A. Jewell ", + }, + }, + + # CI/CD configuration with Nickel contracts + ci = { + # Platform must be a known CI provider + platform + | [| 'GitHubActions, 'GitLabCI, 'CircleCI, 'TravisCI |] + = 'GitHubActions, + + # Build matrix with validation + matrix = { + # Operating systems to test on + os + | Array String + | std.array.NonEmpty + = ["ubuntu-latest", "macos-latest"], + + # Language versions to test + versions + | Array String + | std.array.NonEmpty + = ["stable", "beta"], + }, + + # Workflow steps with validation + steps = [ + { + name = "Checkout", + action = "actions/checkout@v4", + # Version must be SHA-pinned for security + sha | String | std.string.NonEmpty = "b4ffde65f46336ab88eb53be808477a3936bae11", + }, + { + name = "Build", + run = "just build", + }, + { + name = "Test", + run = "just test", + }, + { + name = "Lint", + run = "just lint", + }, + ], + + # Deployment configuration + deploy = { + enabled | Bool = false, + + # Only deploy from main branch + branch + | String + | std.contract.from_predicate (fun b => b == "main" || b == "master") + = "main", + + # Deployment requires manual approval + requires_approval | Bool = true, + }, + + # Security scanning + security = { + enabled | Bool = true, + + scanners = [ + { + name = "CodeQL", + languages = ["rust", "javascript"], + }, + { + name = "OSSF Scorecard", + enabled = true, + }, + { + name = "TruffleHog", + scan_for = "secrets", + }, + ], + }, + + # Notification settings + notifications = { + on_success = "never", + on_failure = "always", + channels = ["email"], + }, + }, + + # Validation rules (enforced by Nickel) + validation = { + # At least one OS must be specified + check_os = std.array.length ci.matrix.os > 0, + + # At least one version must be tested + check_versions = std.array.length ci.matrix.versions > 0, + + # Must have at least build and test steps + check_steps = std.array.length ci.steps >= 2, + + # Security scanning must be enabled + check_security = ci.security.enabled == true, + }, +} diff --git a/.machine_readable/svc/k9/examples/project-metadata.k9.ncl b/.machine_readable/svc/k9/examples/project-metadata.k9.ncl index 859870c..e8c162f 100644 --- a/.machine_readable/svc/k9/examples/project-metadata.k9.ncl +++ b/.machine_readable/svc/k9/examples/project-metadata.k9.ncl @@ -1,57 +1,57 @@ -K9! -# SPDX-License-Identifier: PMPL-1.0-or-later -# Example Kennel-level K9 component: Project metadata -# Security Level: Kennel (pure data, no execution) -# No signature required - -{ - pedigree = { - schema_version = "1.0.0", - component_type = "project-metadata", - security = { - leash = 'Kennel, - trust_level = "data-only", - allow_network = false, - allow_filesystem_write = false, - allow_subprocess = false, - }, - metadata = { - name = "project-metadata", - version = "1.0.0", - description = "Pure data configuration for project metadata", - author = "Jonathan D.A. Jewell ", - }, - }, - - # Project configuration - project = { - name = "my-project", - version = "0.1.0", - description = "A project following Rhodium Standard Repositories", - - repository = { - url = "https://github.com/hyperpolymath/my-project", - type = "git", - }, - - author = { - name = "Jonathan D.A. Jewell", - email = "jonathan.jewell@open.ac.uk", - organization = "The Open University", - }, - - license = "PMPL-1.0-or-later", - - keywords = [ - "rhodium-standard", - "rsr", - "hyperpolymath", - ], - }, - - # Export as JSON for other tools - export = { - format = "json", - destination = "project-metadata.json", - }, -} +K9! +# SPDX-License-Identifier: PMPL-1.0-or-later +# Example Kennel-level K9 component: Project metadata +# Security Level: Kennel (pure data, no execution) +# No signature required + +{ + pedigree = { + schema_version = "1.0.0", + component_type = "project-metadata", + security = { + leash = 'Kennel, + trust_level = "data-only", + allow_network = false, + allow_filesystem_write = false, + allow_subprocess = false, + }, + metadata = { + name = "project-metadata", + version = "1.0.0", + description = "Pure data configuration for project metadata", + author = "Jonathan D.A. Jewell ", + }, + }, + + # Project configuration + project = { + name = "my-project", + version = "0.1.0", + description = "A project following Rhodium Standard Repositories", + + repository = { + url = "https://github.com/hyperpolymath/my-project", + type = "git", + }, + + author = { + name = "Jonathan D.A. Jewell", + email = "jonathan.jewell@open.ac.uk", + organization = "The Open University", + }, + + license = "PMPL-1.0-or-later", + + keywords = [ + "rhodium-standard", + "rsr", + "hyperpolymath", + ], + }, + + # Export as JSON for other tools + export = { + format = "json", + destination = "project-metadata.json", + }, +} diff --git a/.machine_readable/svc/k9/examples/setup-repo.k9.ncl b/.machine_readable/svc/k9/examples/setup-repo.k9.ncl index c182db9..a94dc84 100644 --- a/.machine_readable/svc/k9/examples/setup-repo.k9.ncl +++ b/.machine_readable/svc/k9/examples/setup-repo.k9.ncl @@ -1,167 +1,167 @@ -K9! -# SPDX-License-Identifier: PMPL-1.0-or-later -# Example Hunt-level K9 component: Repository setup automation -# Security Level: Hunt (full execution with Just recipes) -# ⚠️ SIGNATURE REQUIRED - DO NOT RUN WITHOUT VERIFICATION - -{ - pedigree = { - schema_version = "1.0.0", - component_type = "repository-setup", - security = { - leash = 'Hunt, - trust_level = "full-system-access", - allow_network = true, - allow_filesystem_write = true, - allow_subprocess = true, - signature_required = true, - }, - metadata = { - name = "setup-repo", - version = "1.0.0", - description = "Automated repository setup with RSR standards", - author = "Jonathan D.A. Jewell ", - }, - warnings = [ - "This component has full system access", - "Only run from trusted sources with verified signatures", - "Review Just recipes before execution", - "Use dry-run mode first: ./must --dry-run run setup-repo.k9.ncl", - ], - }, - - # Configuration with contracts - config = { - repo_name - | String - | std.string.NonEmpty - = "my-new-repo", - - repo_type - | [| 'Library, 'Application, 'Tool, 'Specification |] - = 'Application, - - primary_language - | String - | std.string.NonEmpty - = "rust", - - # RSR compliance features to enable - features = { - checkpoint_files | Bool = true, # STATE.scm, ECOSYSTEM.scm, META.scm - security_workflows | Bool = true, # CodeQL, Scorecard, etc. - quality_checks | Bool = true, # Linting, formatting - mirroring | Bool = false, # GitLab/Bitbucket mirrors - }, - - # Git configuration - git = { - default_branch = "main", - initial_commit | Bool = true, - remote_url | String = "", - }, - }, - - # Just recipes for execution - # These run when: ./must run setup-repo.k9.ncl - recipes = { - # Main entry point - default = { - recipe = "setup", - description = "Set up RSR-compliant repository", - }, - - # Individual setup tasks - setup = { - dependencies = ["check-env", "create-structure", "init-git", "setup-workflows"], - commands = [ - "echo '✅ Repository setup complete!'", - "echo 'Run: git status to see changes'", - ], - }, - - "check-env" = { - description = "Verify required tools are installed", - commands = [ - "command -v git || (echo 'ERROR: git not found' && exit 1)", - "command -v just || (echo 'ERROR: just not found' && exit 1)", - "command -v nickel || (echo 'ERROR: nickel not found' && exit 1)", - "echo '✓ All required tools present'", - ], - }, - - "create-structure" = { - description = "Create RSR directory structure", - commands = [ - "mkdir -p src/ docs/ tests/ scripts/", - "mkdir -p .github/workflows/", - "mkdir -p contractiles/k9/", - "echo '✓ Directory structure created'", - ], - }, - - "init-git" = { - description = "Initialize Git repository", - commands = [ - "git init -b %{config.git.default_branch}", - "git config user.name 'Jonathan D.A. Jewell'", - "git config user.email 'jonathan.jewell@open.ac.uk'", - "echo '✓ Git initialized'", - ], - }, - - "setup-workflows" = { - description = "Add RSR-compliant workflows", - commands = [ - # This would copy workflow templates - # In a real implementation, would fetch from rsr-template-repo - "echo '✓ Workflows configured'", - ], - }, - - "create-checkpoint-files" = { - description = "Create STATE.scm, ECOSYSTEM.scm, META.scm", - commands = [ - "echo '(state (version \"1.0.0\") (project \"%{config.repo_name}\"))' > STATE.scm", - "echo '(ecosystem (version \"1.0.0\") (name \"%{config.repo_name}\"))' > ECOSYSTEM.scm", - "echo '(meta (version \"1.0.0\") (project \"%{config.repo_name}\"))' > META.scm", - "echo '✓ Checkpoint files created'", - ], - }, - - "add-license" = { - description = "Add PMPL-1.0 license", - commands = [ - "curl -sL https://raw.githubusercontent.com/hyperpolymath/pmpl/main/LICENSE -o LICENSE", - "echo '✓ License added'", - ], - }, - - "add-readme" = { - description = "Create README.adoc from template", - commands = [ - "echo '= %{config.repo_name}' > README.adoc", - "echo '' >> README.adoc", - "echo 'Part of the Hyperpolymath ecosystem.' >> README.adoc", - "echo '✓ README created'", - ], - }, - - clean = { - description = "Remove generated files (careful!)", - commands = [ - "echo '⚠️ This will delete all generated files'", - "echo 'Press Ctrl+C to cancel, or wait 5 seconds...'", - "sleep 5", - "rm -f STATE.scm ECOSYSTEM.scm META.scm", - "echo '✓ Cleaned'", - ], - }, - }, - - # Validation (Yard-level checks before Hunt execution) - validation = { - check_repo_name = std.string.length config.repo_name > 0, - check_language = std.string.length config.primary_language > 0, - }, -} +K9! +# SPDX-License-Identifier: PMPL-1.0-or-later +# Example Hunt-level K9 component: Repository setup automation +# Security Level: Hunt (full execution with Just recipes) +# ⚠️ SIGNATURE REQUIRED - DO NOT RUN WITHOUT VERIFICATION + +{ + pedigree = { + schema_version = "1.0.0", + component_type = "repository-setup", + security = { + leash = 'Hunt, + trust_level = "full-system-access", + allow_network = true, + allow_filesystem_write = true, + allow_subprocess = true, + signature_required = true, + }, + metadata = { + name = "setup-repo", + version = "1.0.0", + description = "Automated repository setup with RSR standards", + author = "Jonathan D.A. Jewell ", + }, + warnings = [ + "This component has full system access", + "Only run from trusted sources with verified signatures", + "Review Just recipes before execution", + "Use dry-run mode first: ./must --dry-run run setup-repo.k9.ncl", + ], + }, + + # Configuration with contracts + config = { + repo_name + | String + | std.string.NonEmpty + = "my-new-repo", + + repo_type + | [| 'Library, 'Application, 'Tool, 'Specification |] + = 'Application, + + primary_language + | String + | std.string.NonEmpty + = "rust", + + # RSR compliance features to enable + features = { + checkpoint_files | Bool = true, # STATE.scm, ECOSYSTEM.scm, META.scm + security_workflows | Bool = true, # CodeQL, Scorecard, etc. + quality_checks | Bool = true, # Linting, formatting + mirroring | Bool = false, # GitLab/Bitbucket mirrors + }, + + # Git configuration + git = { + default_branch = "main", + initial_commit | Bool = true, + remote_url | String = "", + }, + }, + + # Just recipes for execution + # These run when: ./must run setup-repo.k9.ncl + recipes = { + # Main entry point + default = { + recipe = "setup", + description = "Set up RSR-compliant repository", + }, + + # Individual setup tasks + setup = { + dependencies = ["check-env", "create-structure", "init-git", "setup-workflows"], + commands = [ + "echo '✅ Repository setup complete!'", + "echo 'Run: git status to see changes'", + ], + }, + + "check-env" = { + description = "Verify required tools are installed", + commands = [ + "command -v git || (echo 'ERROR: git not found' && exit 1)", + "command -v just || (echo 'ERROR: just not found' && exit 1)", + "command -v nickel || (echo 'ERROR: nickel not found' && exit 1)", + "echo '✓ All required tools present'", + ], + }, + + "create-structure" = { + description = "Create RSR directory structure", + commands = [ + "mkdir -p src/ docs/ tests/ scripts/", + "mkdir -p .github/workflows/", + "mkdir -p contractiles/k9/", + "echo '✓ Directory structure created'", + ], + }, + + "init-git" = { + description = "Initialize Git repository", + commands = [ + "git init -b %{config.git.default_branch}", + "git config user.name 'Jonathan D.A. Jewell'", + "git config user.email 'jonathan.jewell@open.ac.uk'", + "echo '✓ Git initialized'", + ], + }, + + "setup-workflows" = { + description = "Add RSR-compliant workflows", + commands = [ + # This would copy workflow templates + # In a real implementation, would fetch from rsr-template-repo + "echo '✓ Workflows configured'", + ], + }, + + "create-checkpoint-files" = { + description = "Create STATE.scm, ECOSYSTEM.scm, META.scm", + commands = [ + "echo '(state (version \"1.0.0\") (project \"%{config.repo_name}\"))' > STATE.scm", + "echo '(ecosystem (version \"1.0.0\") (name \"%{config.repo_name}\"))' > ECOSYSTEM.scm", + "echo '(meta (version \"1.0.0\") (project \"%{config.repo_name}\"))' > META.scm", + "echo '✓ Checkpoint files created'", + ], + }, + + "add-license" = { + description = "Add PMPL-1.0 license", + commands = [ + "curl -sL https://raw.githubusercontent.com/hyperpolymath/pmpl/main/LICENSE -o LICENSE", + "echo '✓ License added'", + ], + }, + + "add-readme" = { + description = "Create README.adoc from template", + commands = [ + "echo '= %{config.repo_name}' > README.adoc", + "echo '' >> README.adoc", + "echo 'Part of the Hyperpolymath ecosystem.' >> README.adoc", + "echo '✓ README created'", + ], + }, + + clean = { + description = "Remove generated files (careful!)", + commands = [ + "echo '⚠️ This will delete all generated files'", + "echo 'Press Ctrl+C to cancel, or wait 5 seconds...'", + "sleep 5", + "rm -f STATE.scm ECOSYSTEM.scm META.scm", + "echo '✓ Cleaned'", + ], + }, + }, + + # Validation (Yard-level checks before Hunt execution) + validation = { + check_repo_name = std.string.length config.repo_name > 0, + check_language = std.string.length config.primary_language > 0, + }, +} diff --git a/.machine_readable/svc/k9/template-hunt.k9.ncl b/.machine_readable/svc/k9/template-hunt.k9.ncl index db25742..8b83372 100644 --- a/.machine_readable/svc/k9/template-hunt.k9.ncl +++ b/.machine_readable/svc/k9/template-hunt.k9.ncl @@ -1,136 +1,136 @@ -K9! -# SPDX-License-Identifier: PMPL-1.0-or-later -# K9 Hunt-level template: Full execution with Just recipes -# Security Level: Hunt (full system access) -# ⚠️ SIGNATURE REQUIRED - Review carefully before use - -{ - pedigree = { - schema_version = "1.0.0", - component_type = "TODO: describe component type (e.g., 'deployment', 'setup-script')", - security = { - leash = 'Hunt, - trust_level = "full-system-access", - allow_network = true, - allow_filesystem_write = true, - allow_subprocess = true, - signature_required = true, - }, - metadata = { - name = "TODO: component-name", - version = "1.0.0", - description = "TODO: Detailed description of what this component does", - author = "Jonathan D.A. Jewell ", - }, - warnings = [ - "This component has full system access", - "Only run from trusted sources with verified signatures", - "Review all Just recipes before execution", - "Use dry-run mode first: ./must --dry-run run your-file.k9.ncl", - ], - side_effects = [ - "TODO: List what files/directories this creates or modifies", - "TODO: List what commands this executes", - "TODO: List what network access this requires", - ], - }, - - # Configuration with contracts (Yard-level validation) - config = { - # Add your configuration here with appropriate contracts - target_dir - | String - | std.string.NonEmpty - = "/tmp/k9-output", - - dry_run | Bool = false, - - # Add more config as needed - }, - - # Just recipes for execution - # These run when: ./must run your-file.k9.ncl - recipes = { - # Main entry point (runs by default) - default = { - recipe = "TODO: main-task", - description = "TODO: What the default recipe does", - }, - - # Define your recipes here - "main-task" = { - dependencies = ["check-prerequisites"], - commands = [ - "echo 'TODO: Add your commands here'", - # Example: Create directory - # "mkdir -p %{config.target_dir}", - # Example: Run a command - # "just build", - # Example: Conditional execution - # "@if [ \"%{config.dry_run}\" = \"true\" ]; then echo '[DRY-RUN] Would execute'; else actual-command; fi", - ], - }, - - "check-prerequisites" = { - description = "Verify required tools and permissions", - commands = [ - # Example: Check for required tools - # "command -v git || (echo 'ERROR: git not found' && exit 1)", - # Example: Check permissions - # "[ -w %{config.target_dir} ] || (echo 'ERROR: Cannot write to target directory' && exit 1)", - "echo '✓ Prerequisites checked'", - ], - }, - - # Add more recipes as needed - "build" = { - description = "Build the project", - commands = [ - "echo 'TODO: Add build commands'", - ], - }, - - "deploy" = { - description = "Deploy the application", - dependencies = ["build"], - commands = [ - "echo 'TODO: Add deployment commands'", - ], - }, - - "clean" = { - description = "Clean up generated files", - commands = [ - "echo '⚠️ This will delete files - waiting 3 seconds...'", - "sleep 3", - "echo 'TODO: Add cleanup commands'", - # "rm -rf %{config.target_dir}", - ], - }, - }, - - # Validation (Yard-level checks before Hunt execution) - validation = { - check_target_dir = std.string.length config.target_dir > 0, - # Add more validation as needed - }, -} - -# Usage: -# 1. Fill in TODO items above -# 2. Define configuration with contracts -# 3. Implement Just recipes with your commands -# 4. Test with dry-run: ./must --dry-run run your-file.k9.ncl -# 5. Review dry-run output carefully -# 6. Sign the component: ./must sign your-file.k9.ncl -# 7. Distribute with signature: your-file.k9.ncl.sig -# 8. Users verify and run: ./must verify && ./must run your-file.k9.ncl -# -# Security checklist: -# ✓ All TODO items filled in -# ✓ side_effects documented accurately -# ✓ Commands reviewed for safety -# ✓ No hardcoded secrets or credentials -# ✓ Proper error handling in recipes -# ✓ Tested in dry-run mode -# ✓ Component signed with trusted key +K9! +# SPDX-License-Identifier: PMPL-1.0-or-later +# K9 Hunt-level template: Full execution with Just recipes +# Security Level: Hunt (full system access) +# ⚠️ SIGNATURE REQUIRED - Review carefully before use + +{ + pedigree = { + schema_version = "1.0.0", + component_type = "TODO: describe component type (e.g., 'deployment', 'setup-script')", + security = { + leash = 'Hunt, + trust_level = "full-system-access", + allow_network = true, + allow_filesystem_write = true, + allow_subprocess = true, + signature_required = true, + }, + metadata = { + name = "TODO: component-name", + version = "1.0.0", + description = "TODO: Detailed description of what this component does", + author = "Jonathan D.A. Jewell ", + }, + warnings = [ + "This component has full system access", + "Only run from trusted sources with verified signatures", + "Review all Just recipes before execution", + "Use dry-run mode first: ./must --dry-run run your-file.k9.ncl", + ], + side_effects = [ + "TODO: List what files/directories this creates or modifies", + "TODO: List what commands this executes", + "TODO: List what network access this requires", + ], + }, + + # Configuration with contracts (Yard-level validation) + config = { + # Add your configuration here with appropriate contracts + target_dir + | String + | std.string.NonEmpty + = "/tmp/k9-output", + + dry_run | Bool = false, + + # Add more config as needed + }, + + # Just recipes for execution + # These run when: ./must run your-file.k9.ncl + recipes = { + # Main entry point (runs by default) + default = { + recipe = "TODO: main-task", + description = "TODO: What the default recipe does", + }, + + # Define your recipes here + "main-task" = { + dependencies = ["check-prerequisites"], + commands = [ + "echo 'TODO: Add your commands here'", + # Example: Create directory + # "mkdir -p %{config.target_dir}", + # Example: Run a command + # "just build", + # Example: Conditional execution + # "@if [ \"%{config.dry_run}\" = \"true\" ]; then echo '[DRY-RUN] Would execute'; else actual-command; fi", + ], + }, + + "check-prerequisites" = { + description = "Verify required tools and permissions", + commands = [ + # Example: Check for required tools + # "command -v git || (echo 'ERROR: git not found' && exit 1)", + # Example: Check permissions + # "[ -w %{config.target_dir} ] || (echo 'ERROR: Cannot write to target directory' && exit 1)", + "echo '✓ Prerequisites checked'", + ], + }, + + # Add more recipes as needed + "build" = { + description = "Build the project", + commands = [ + "echo 'TODO: Add build commands'", + ], + }, + + "deploy" = { + description = "Deploy the application", + dependencies = ["build"], + commands = [ + "echo 'TODO: Add deployment commands'", + ], + }, + + "clean" = { + description = "Clean up generated files", + commands = [ + "echo '⚠️ This will delete files - waiting 3 seconds...'", + "sleep 3", + "echo 'TODO: Add cleanup commands'", + # "rm -rf %{config.target_dir}", + ], + }, + }, + + # Validation (Yard-level checks before Hunt execution) + validation = { + check_target_dir = std.string.length config.target_dir > 0, + # Add more validation as needed + }, +} + +# Usage: +# 1. Fill in TODO items above +# 2. Define configuration with contracts +# 3. Implement Just recipes with your commands +# 4. Test with dry-run: ./must --dry-run run your-file.k9.ncl +# 5. Review dry-run output carefully +# 6. Sign the component: ./must sign your-file.k9.ncl +# 7. Distribute with signature: your-file.k9.ncl.sig +# 8. Users verify and run: ./must verify && ./must run your-file.k9.ncl +# +# Security checklist: +# ✓ All TODO items filled in +# ✓ side_effects documented accurately +# ✓ Commands reviewed for safety +# ✓ No hardcoded secrets or credentials +# ✓ Proper error handling in recipes +# ✓ Tested in dry-run mode +# ✓ Component signed with trusted key diff --git a/.machine_readable/svc/k9/template-kennel.k9.ncl b/.machine_readable/svc/k9/template-kennel.k9.ncl index 4303558..d120b54 100644 --- a/.machine_readable/svc/k9/template-kennel.k9.ncl +++ b/.machine_readable/svc/k9/template-kennel.k9.ncl @@ -1,54 +1,54 @@ -K9! -# SPDX-License-Identifier: PMPL-1.0-or-later -# K9 Kennel-level template: Pure data configuration -# Security Level: Kennel (data-only, no execution) -# No signature required - safe for any use - -{ - pedigree = { - schema_version = "1.0.0", - component_type = "TODO: describe component type (e.g., 'build-config', 'metadata')", - security = { - leash = 'Kennel, - trust_level = "data-only", - allow_network = false, - allow_filesystem_write = false, - allow_subprocess = false, - }, - metadata = { - name = "TODO: component-name", - version = "1.0.0", - description = "TODO: Brief description of what this component contains", - author = "Jonathan D.A. Jewell ", - }, - }, - - # Your configuration data here - config = { - # Example: Pure data values - setting_1 = "value", - setting_2 = 42, - setting_3 = true, - - nested = { - key = "value", - }, - - list = [ - "item1", - "item2", - ], - }, - - # Optional: Export format specification - export = { - format = "json", # or "yaml", "toml" - destination = "output.json", - }, -} - -# Usage: -# 1. Fill in TODO items above -# 2. Add your configuration data to config = { ... } -# 3. Validate: nickel typecheck your-file.k9.ncl -# 4. Export: nickel export your-file.k9.ncl > output.json +K9! +# SPDX-License-Identifier: PMPL-1.0-or-later +# K9 Kennel-level template: Pure data configuration +# Security Level: Kennel (data-only, no execution) +# No signature required - safe for any use + +{ + pedigree = { + schema_version = "1.0.0", + component_type = "TODO: describe component type (e.g., 'build-config', 'metadata')", + security = { + leash = 'Kennel, + trust_level = "data-only", + allow_network = false, + allow_filesystem_write = false, + allow_subprocess = false, + }, + metadata = { + name = "TODO: component-name", + version = "1.0.0", + description = "TODO: Brief description of what this component contains", + author = "Jonathan D.A. Jewell ", + }, + }, + + # Your configuration data here + config = { + # Example: Pure data values + setting_1 = "value", + setting_2 = 42, + setting_3 = true, + + nested = { + key = "value", + }, + + list = [ + "item1", + "item2", + ], + }, + + # Optional: Export format specification + export = { + format = "json", # or "yaml", "toml" + destination = "output.json", + }, +} + +# Usage: +# 1. Fill in TODO items above +# 2. Add your configuration data to config = { ... } +# 3. Validate: nickel typecheck your-file.k9.ncl +# 4. Export: nickel export your-file.k9.ncl > output.json diff --git a/.machine_readable/svc/k9/template-yard.k9.ncl b/.machine_readable/svc/k9/template-yard.k9.ncl index 4b49947..4006690 100644 --- a/.machine_readable/svc/k9/template-yard.k9.ncl +++ b/.machine_readable/svc/k9/template-yard.k9.ncl @@ -1,84 +1,84 @@ -K9! -# SPDX-License-Identifier: PMPL-1.0-or-later -# K9 Yard-level template: Configuration with validation -# Security Level: Yard (Nickel evaluation with contracts) -# Signature recommended but not required - -{ - pedigree = { - schema_version = "1.0.0", - component_type = "TODO: describe component type (e.g., 'validated-config', 'schema')", - security = { - leash = 'Yard, - trust_level = "validated-config", - allow_network = false, - allow_filesystem_write = false, - allow_subprocess = false, - }, - metadata = { - name = "TODO: component-name", - version = "1.0.0", - description = "TODO: Brief description with validation details", - author = "Jonathan D.A. Jewell ", - }, - }, - - # Configuration with Nickel contracts for validation - config = { - # Example: String that cannot be empty - name - | String - | std.string.NonEmpty - = "TODO: default value", - - # Example: Number with range constraint - port - | Number - | std.contract.from_predicate (fun p => p > 0 && p < 65536) - = 8080, - - # Example: Boolean flag - enabled | Bool = true, - - # Example: Enum (one of several values) - environment - | [| 'Development, 'Staging, 'Production |] - = 'Development, - - # Example: List with non-empty constraint - items - | Array String - | std.array.NonEmpty - = ["item1", "item2"], - - # Example: Nested object with contracts - database = { - host | String | std.string.NonEmpty = "localhost", - port | Number | std.contract.from_predicate (fun p => p > 0 && p < 65536) = 5432, - name | String | std.string.NonEmpty = "mydb", - }, - }, - - # Validation rules (additional cross-field checks) - validation = { - # Example: Check that at least one item exists - check_items = std.array.length config.items > 0, - - # Example: Check that production has secure settings - check_production = - if config.environment == 'Production then - config.enabled == true - else - true, - - # Add your custom validation rules here - }, -} - -# Usage: -# 1. Fill in TODO items above -# 2. Define your config with appropriate contracts -# 3. Add validation rules in validation = { ... } -# 4. Validate: nickel typecheck your-file.k9.ncl -# 5. Evaluate: nickel eval your-file.k9.ncl -# 6. If validation passes, use in your application +K9! +# SPDX-License-Identifier: PMPL-1.0-or-later +# K9 Yard-level template: Configuration with validation +# Security Level: Yard (Nickel evaluation with contracts) +# Signature recommended but not required + +{ + pedigree = { + schema_version = "1.0.0", + component_type = "TODO: describe component type (e.g., 'validated-config', 'schema')", + security = { + leash = 'Yard, + trust_level = "validated-config", + allow_network = false, + allow_filesystem_write = false, + allow_subprocess = false, + }, + metadata = { + name = "TODO: component-name", + version = "1.0.0", + description = "TODO: Brief description with validation details", + author = "Jonathan D.A. Jewell ", + }, + }, + + # Configuration with Nickel contracts for validation + config = { + # Example: String that cannot be empty + name + | String + | std.string.NonEmpty + = "TODO: default value", + + # Example: Number with range constraint + port + | Number + | std.contract.from_predicate (fun p => p > 0 && p < 65536) + = 8080, + + # Example: Boolean flag + enabled | Bool = true, + + # Example: Enum (one of several values) + environment + | [| 'Development, 'Staging, 'Production |] + = 'Development, + + # Example: List with non-empty constraint + items + | Array String + | std.array.NonEmpty + = ["item1", "item2"], + + # Example: Nested object with contracts + database = { + host | String | std.string.NonEmpty = "localhost", + port | Number | std.contract.from_predicate (fun p => p > 0 && p < 65536) = 5432, + name | String | std.string.NonEmpty = "mydb", + }, + }, + + # Validation rules (additional cross-field checks) + validation = { + # Example: Check that at least one item exists + check_items = std.array.length config.items > 0, + + # Example: Check that production has secure settings + check_production = + if config.environment == 'Production then + config.enabled == true + else + true, + + # Add your custom validation rules here + }, +} + +# Usage: +# 1. Fill in TODO items above +# 2. Define your config with appropriate contracts +# 3. Add validation rules in validation = { ... } +# 4. Validate: nickel typecheck your-file.k9.ncl +# 5. Evaluate: nickel eval your-file.k9.ncl +# 6. If validation passes, use in your application diff --git a/AI.a2ml b/AI.a2ml index 4c3b31f..7469807 100644 --- a/AI.a2ml +++ b/AI.a2ml @@ -1,42 +1,42 @@ -; SPDX-License-Identifier: PMPL-1.0-or-later -; SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -; -; AI Manifest for claude-integrations -; Universal entry point for all AI agents. - -(ai-manifest - (version "1.0") - (name "claude-integrations") - (description "Unified monorepo for all Claude browser and service integrations") - (author "Jonathan D.A. Jewell ") - (license "PMPL-1.0-or-later") - - (canonical-locations - (scm-files ".machine_readable/") - (license "LICENSE") - (readme "README.adoc") - (build "justfile")) - - (critical-invariants - (rule "SCM files ONLY in .machine_readable/ - NEVER in repository root") - (rule "License is PMPL-1.0-or-later") - (rule "Each component directory is self-contained with its own README") - (rule "No TypeScript - use ReScript or Deno JavaScript") - (rule "No npm/bun - use Deno where JS runtime needed")) - - (structure - (component "firefox-lsp" "LSP for Firefox via Marionette" "Elixir") - (component "firefox-mcp" "MCP server for Firefox control" "Deno") - (component "gecko-browser-extension" "Gecko browser extension" "JavaScript") - (component "gitlab-bridge" "GitLab integration bridge" "ReScript") - (component "mozilla-extension" "Mozilla products extension" "JavaScript")) - - (lifecycle - (on-enter - (read "AI.a2ml") - (read ".machine_readable/STATE.scm") - (read ".machine_readable/META.scm") - (read ".machine_readable/ECOSYSTEM.scm") - (acknowledge "This is a monorepo - identify which component is relevant")) - (on-exit - (update ".machine_readable/STATE.scm" "if tasks completed")))) +; SPDX-License-Identifier: PMPL-1.0-or-later +; SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +; +; AI Manifest for claude-integrations +; Universal entry point for all AI agents. + +(ai-manifest + (version "1.0") + (name "claude-integrations") + (description "Unified monorepo for all Claude browser and service integrations") + (author "Jonathan D.A. Jewell ") + (license "PMPL-1.0-or-later") + + (canonical-locations + (scm-files ".machine_readable/") + (license "LICENSE") + (readme "README.adoc") + (build "justfile")) + + (critical-invariants + (rule "SCM files ONLY in .machine_readable/ - NEVER in repository root") + (rule "License is PMPL-1.0-or-later") + (rule "Each component directory is self-contained with its own README") + (rule "No TypeScript - use ReScript or Deno JavaScript") + (rule "No npm/bun - use Deno where JS runtime needed")) + + (structure + (component "firefox-lsp" "LSP for Firefox via Marionette" "Elixir") + (component "firefox-mcp" "MCP server for Firefox control" "Deno") + (component "gecko-browser-extension" "Gecko browser extension" "JavaScript") + (component "gitlab-bridge" "GitLab integration bridge" "ReScript") + (component "mozilla-extension" "Mozilla products extension" "JavaScript")) + + (lifecycle + (on-enter + (read "AI.a2ml") + (read ".machine_readable/STATE.scm") + (read ".machine_readable/META.scm") + (read ".machine_readable/ECOSYSTEM.scm") + (acknowledge "This is a monorepo - identify which component is relevant")) + (on-exit + (update ".machine_readable/STATE.scm" "if tasks completed")))) diff --git a/Justfile b/Justfile index cf9646d..60c33f7 100644 --- a/Justfile +++ b/Justfile @@ -1,79 +1,79 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -# -# claude-integrations - Unified Claude browser/service integrations monorepo. - -# List available recipes -default: - @just --list - -# --- Firefox LSP (Elixir) --- - -# Build the Firefox LSP component -build-firefox-lsp: - cd firefox-lsp && mix deps.get && mix compile - -# Run Firefox LSP tests -test-firefox-lsp: - cd firefox-lsp && mix test - -# --- Firefox MCP (Deno) --- - -# Build the Firefox MCP component -build-firefox-mcp: - cd firefox-mcp && deno cache src/main.ts 2>/dev/null || echo "No Deno entrypoint cached" - -# Run Firefox MCP tests -test-firefox-mcp: - cd firefox-mcp && deno test 2>/dev/null || echo "No Deno tests configured" - -# --- Gecko Browser Extension --- - -# Build the Gecko browser extension -build-gecko-extension: - @echo "Gecko extension is pure JS/WebExtensions - no build step required." - @echo "Load as temporary add-on in about:debugging" - -# Lint the Gecko browser extension -lint-gecko-extension: - cd gecko-browser-extension && web-ext lint 2>/dev/null || echo "Install web-ext: npm i -g web-ext" - -# --- GitLab Bridge (ReScript/Deno) --- - -# Build the GitLab Bridge component -build-gitlab-bridge: - cd gitlab-bridge && deno cache src/main.ts 2>/dev/null || echo "No Deno entrypoint cached" - -# Run GitLab Bridge tests -test-gitlab-bridge: - cd gitlab-bridge && deno test 2>/dev/null || echo "No Deno tests configured" - -# --- Mozilla Extension --- - -# Build the Mozilla extension -build-mozilla-extension: - @echo "Mozilla extension is pure JS/WebExtensions - no build step required." - @echo "Load as temporary add-on in about:debugging" - -# Lint the Mozilla extension -lint-mozilla-extension: - cd mozilla-extension && web-ext lint 2>/dev/null || echo "Install web-ext: npm i -g web-ext" - -# --- Aggregate Recipes --- - -# Build all components -build-all: build-firefox-lsp build-firefox-mcp build-gecko-extension build-gitlab-bridge build-mozilla-extension - -# Run all tests -test-all: test-firefox-lsp test-firefox-mcp test-gitlab-bridge - -# Lint all lintable components -lint-all: lint-gecko-extension lint-mozilla-extension - -# Show status of all components -status: - @echo "=== Firefox LSP ===" && ls firefox-lsp/mix.exs 2>/dev/null && echo "OK" || echo "MISSING" - @echo "=== Firefox MCP ===" && ls firefox-mcp/deno.json 2>/dev/null && echo "OK" || echo "MISSING" - @echo "=== Gecko Extension ===" && ls gecko-browser-extension/manifest.json 2>/dev/null && echo "OK" || echo "MISSING" - @echo "=== GitLab Bridge ===" && ls gitlab-bridge/deno.json 2>/dev/null && echo "OK" || echo "MISSING" - @echo "=== Mozilla Extension ===" && ls mozilla-extension/src/ 2>/dev/null && echo "OK" || echo "MISSING" +# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +# +# claude-integrations - Unified Claude browser/service integrations monorepo. + +# List available recipes +default: + @just --list + +# --- Firefox LSP (Elixir) --- + +# Build the Firefox LSP component +build-firefox-lsp: + cd firefox-lsp && mix deps.get && mix compile + +# Run Firefox LSP tests +test-firefox-lsp: + cd firefox-lsp && mix test + +# --- Firefox MCP (Deno) --- + +# Build the Firefox MCP component +build-firefox-mcp: + cd firefox-mcp && deno cache src/main.ts 2>/dev/null || echo "No Deno entrypoint cached" + +# Run Firefox MCP tests +test-firefox-mcp: + cd firefox-mcp && deno test 2>/dev/null || echo "No Deno tests configured" + +# --- Gecko Browser Extension --- + +# Build the Gecko browser extension +build-gecko-extension: + @echo "Gecko extension is pure JS/WebExtensions - no build step required." + @echo "Load as temporary add-on in about:debugging" + +# Lint the Gecko browser extension +lint-gecko-extension: + cd gecko-browser-extension && web-ext lint 2>/dev/null || echo "Install web-ext: npm i -g web-ext" + +# --- GitLab Bridge (ReScript/Deno) --- + +# Build the GitLab Bridge component +build-gitlab-bridge: + cd gitlab-bridge && deno cache src/main.ts 2>/dev/null || echo "No Deno entrypoint cached" + +# Run GitLab Bridge tests +test-gitlab-bridge: + cd gitlab-bridge && deno test 2>/dev/null || echo "No Deno tests configured" + +# --- Mozilla Extension --- + +# Build the Mozilla extension +build-mozilla-extension: + @echo "Mozilla extension is pure JS/WebExtensions - no build step required." + @echo "Load as temporary add-on in about:debugging" + +# Lint the Mozilla extension +lint-mozilla-extension: + cd mozilla-extension && web-ext lint 2>/dev/null || echo "Install web-ext: npm i -g web-ext" + +# --- Aggregate Recipes --- + +# Build all components +build-all: build-firefox-lsp build-firefox-mcp build-gecko-extension build-gitlab-bridge build-mozilla-extension + +# Run all tests +test-all: test-firefox-lsp test-firefox-mcp test-gitlab-bridge + +# Lint all lintable components +lint-all: lint-gecko-extension lint-mozilla-extension + +# Show status of all components +status: + @echo "=== Firefox LSP ===" && ls firefox-lsp/mix.exs 2>/dev/null && echo "OK" || echo "MISSING" + @echo "=== Firefox MCP ===" && ls firefox-mcp/deno.json 2>/dev/null && echo "OK" || echo "MISSING" + @echo "=== Gecko Extension ===" && ls gecko-browser-extension/manifest.json 2>/dev/null && echo "OK" || echo "MISSING" + @echo "=== GitLab Bridge ===" && ls gitlab-bridge/deno.json 2>/dev/null && echo "OK" || echo "MISSING" + @echo "=== Mozilla Extension ===" && ls mozilla-extension/src/ 2>/dev/null && echo "OK" || echo "MISSING" diff --git a/LICENSE b/LICENSE index d2dccf5..2078cd7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,153 +1,153 @@ -SPDX-License-Identifier: PMPL-1.0-or-later -SPDX-FileCopyrightText: 2024-2025 Palimpsest Stewardship Council - -================================================================================ -PALIMPSEST-MPL LICENSE VERSION 1.0 -================================================================================ - -File-level copyleft with ethical use and quantum-safe provenance - -Based on Mozilla Public License 2.0 - --------------------------------------------------------------------------------- -PREAMBLE --------------------------------------------------------------------------------- - -This License extends the Mozilla Public License 2.0 (MPL-2.0) with provisions -for ethical use, post-quantum cryptographic provenance, and emotional lineage -protection. The base MPL-2.0 terms apply except where explicitly modified by -the Exhibits below. - -Like a palimpsest manuscript where each layer builds upon what came before, -this license recognizes that creative works carry history, context, and meaning -that transcend mere code or text. - --------------------------------------------------------------------------------- -SECTION 1: BASE LICENSE --------------------------------------------------------------------------------- - -This License incorporates the full text of Mozilla Public License 2.0 by -reference. The complete MPL-2.0 text is available at: -https://www.mozilla.org/en-US/MPL/2.0/ - -All terms, conditions, and definitions from MPL-2.0 apply except where -explicitly modified by the Exhibits in this License. - --------------------------------------------------------------------------------- -SECTION 2: ADDITIONAL DEFINITIONS --------------------------------------------------------------------------------- - -2.1. "Emotional Lineage" - means the narrative, cultural, symbolic, and contextual meaning embedded - in Covered Software, including but not limited to: protest traditions, - cultural heritage, trauma narratives, and community stories. - -2.2. "Provenance Metadata" - means cryptographically signed attribution information attached to or - associated with Covered Software, including author identities, timestamps, - modification history, and lineage references. - -2.3. "Non-Interpretive System" - means any automated system that processes Covered Software without - preserving or considering its Emotional Lineage, including but not - limited to: AI training pipelines, content aggregators, and automated - summarization tools. - -2.4. "Quantum-Safe Signature" - means a cryptographic signature using algorithms resistant to attacks - by quantum computers, as specified in Exhibit B. - --------------------------------------------------------------------------------- -SECTION 3: ETHICAL USE REQUIREMENTS --------------------------------------------------------------------------------- - -In addition to the rights and obligations under MPL-2.0: - -3.1. Emotional Lineage Preservation - You must make reasonable efforts to preserve and communicate the - Emotional Lineage of Covered Software when distributing or creating - derivative works. This includes maintaining narrative context, cultural - attributions, and symbolic meaning where documented. - -3.2. Non-Interpretive System Notice - If You use Covered Software as input to a Non-Interpretive System, You - must: - (a) document such use in a publicly accessible manner; and - (b) not claim that outputs of such systems carry the Emotional Lineage - of the original work without explicit permission from Contributors. - -3.3. Ethical Use Declaration - Commercial use of Covered Software requires acknowledgment that You have - read and understood Exhibit A (Ethical Use Guidelines) and agree to act - in good faith accordance with its principles. - -See Exhibit A for complete Ethical Use Guidelines. - --------------------------------------------------------------------------------- -SECTION 4: PROVENANCE REQUIREMENTS --------------------------------------------------------------------------------- - -4.1. Metadata Preservation - You must not strip, alter, or obscure Provenance Metadata from Covered - Software except where technically necessary and with clear documentation - of any changes. - -4.2. Quantum-Safe Provenance (Optional) - Contributors may sign their Contributions using Quantum-Safe Signatures. - If Quantum-Safe Signatures are present, You must preserve them in all - distributions. - -4.3. Lineage Chain - When creating derivative works, You should extend the provenance chain - to include Your own contributions, maintaining cryptographic linkage to - prior Contributors where feasible. - -See Exhibit B for Quantum-Safe Provenance specifications. - --------------------------------------------------------------------------------- -SECTION 5: GOVERNANCE --------------------------------------------------------------------------------- - -5.1. Stewardship Council - This License is maintained by the Palimpsest Stewardship Council, which - may issue clarifications, interpretive guidance, and future versions. - -5.2. Version Selection - You may use Covered Software under this version of the License or any - later version published by the Palimpsest Stewardship Council. - -5.3. Dispute Resolution - Disputes regarding interpretation of Ethical Use Requirements (Section 3) - should first be submitted to the Palimpsest Stewardship Council for - non-binding guidance before pursuing legal remedies. - --------------------------------------------------------------------------------- -SECTION 6: COMPATIBILITY --------------------------------------------------------------------------------- - -6.1. MPL-2.0 Compatibility - Covered Software under this License may be combined with software under - MPL-2.0. The combined work must comply with both licenses. - -6.2. Secondary Licenses - The Secondary License provisions of MPL-2.0 Section 3.3 apply to this - License. - --------------------------------------------------------------------------------- -EXHIBITS --------------------------------------------------------------------------------- - -Exhibit A - Ethical Use Guidelines -Exhibit B - Quantum-Safe Provenance Specification - -See separate files: -- EXHIBIT-A-ETHICAL-USE.txt -- EXHIBIT-B-QUANTUM-SAFE.txt - --------------------------------------------------------------------------------- -END OF PALIMPSEST-MPL LICENSE VERSION 1.0 --------------------------------------------------------------------------------- - -For questions about this License: -- Repository: https://github.com/hyperpolymath/palimpsest-license -- Council: contact via repository Issues +SPDX-License-Identifier: PMPL-1.0-or-later +SPDX-FileCopyrightText: 2024-2025 Palimpsest Stewardship Council + +================================================================================ +PALIMPSEST-MPL LICENSE VERSION 1.0 +================================================================================ + +File-level copyleft with ethical use and quantum-safe provenance + +Based on Mozilla Public License 2.0 + +-------------------------------------------------------------------------------- +PREAMBLE +-------------------------------------------------------------------------------- + +This License extends the Mozilla Public License 2.0 (MPL-2.0) with provisions +for ethical use, post-quantum cryptographic provenance, and emotional lineage +protection. The base MPL-2.0 terms apply except where explicitly modified by +the Exhibits below. + +Like a palimpsest manuscript where each layer builds upon what came before, +this license recognizes that creative works carry history, context, and meaning +that transcend mere code or text. + +-------------------------------------------------------------------------------- +SECTION 1: BASE LICENSE +-------------------------------------------------------------------------------- + +This License incorporates the full text of Mozilla Public License 2.0 by +reference. The complete MPL-2.0 text is available at: +https://www.mozilla.org/en-US/MPL/2.0/ + +All terms, conditions, and definitions from MPL-2.0 apply except where +explicitly modified by the Exhibits in this License. + +-------------------------------------------------------------------------------- +SECTION 2: ADDITIONAL DEFINITIONS +-------------------------------------------------------------------------------- + +2.1. "Emotional Lineage" + means the narrative, cultural, symbolic, and contextual meaning embedded + in Covered Software, including but not limited to: protest traditions, + cultural heritage, trauma narratives, and community stories. + +2.2. "Provenance Metadata" + means cryptographically signed attribution information attached to or + associated with Covered Software, including author identities, timestamps, + modification history, and lineage references. + +2.3. "Non-Interpretive System" + means any automated system that processes Covered Software without + preserving or considering its Emotional Lineage, including but not + limited to: AI training pipelines, content aggregators, and automated + summarization tools. + +2.4. "Quantum-Safe Signature" + means a cryptographic signature using algorithms resistant to attacks + by quantum computers, as specified in Exhibit B. + +-------------------------------------------------------------------------------- +SECTION 3: ETHICAL USE REQUIREMENTS +-------------------------------------------------------------------------------- + +In addition to the rights and obligations under MPL-2.0: + +3.1. Emotional Lineage Preservation + You must make reasonable efforts to preserve and communicate the + Emotional Lineage of Covered Software when distributing or creating + derivative works. This includes maintaining narrative context, cultural + attributions, and symbolic meaning where documented. + +3.2. Non-Interpretive System Notice + If You use Covered Software as input to a Non-Interpretive System, You + must: + (a) document such use in a publicly accessible manner; and + (b) not claim that outputs of such systems carry the Emotional Lineage + of the original work without explicit permission from Contributors. + +3.3. Ethical Use Declaration + Commercial use of Covered Software requires acknowledgment that You have + read and understood Exhibit A (Ethical Use Guidelines) and agree to act + in good faith accordance with its principles. + +See Exhibit A for complete Ethical Use Guidelines. + +-------------------------------------------------------------------------------- +SECTION 4: PROVENANCE REQUIREMENTS +-------------------------------------------------------------------------------- + +4.1. Metadata Preservation + You must not strip, alter, or obscure Provenance Metadata from Covered + Software except where technically necessary and with clear documentation + of any changes. + +4.2. Quantum-Safe Provenance (Optional) + Contributors may sign their Contributions using Quantum-Safe Signatures. + If Quantum-Safe Signatures are present, You must preserve them in all + distributions. + +4.3. Lineage Chain + When creating derivative works, You should extend the provenance chain + to include Your own contributions, maintaining cryptographic linkage to + prior Contributors where feasible. + +See Exhibit B for Quantum-Safe Provenance specifications. + +-------------------------------------------------------------------------------- +SECTION 5: GOVERNANCE +-------------------------------------------------------------------------------- + +5.1. Stewardship Council + This License is maintained by the Palimpsest Stewardship Council, which + may issue clarifications, interpretive guidance, and future versions. + +5.2. Version Selection + You may use Covered Software under this version of the License or any + later version published by the Palimpsest Stewardship Council. + +5.3. Dispute Resolution + Disputes regarding interpretation of Ethical Use Requirements (Section 3) + should first be submitted to the Palimpsest Stewardship Council for + non-binding guidance before pursuing legal remedies. + +-------------------------------------------------------------------------------- +SECTION 6: COMPATIBILITY +-------------------------------------------------------------------------------- + +6.1. MPL-2.0 Compatibility + Covered Software under this License may be combined with software under + MPL-2.0. The combined work must comply with both licenses. + +6.2. Secondary Licenses + The Secondary License provisions of MPL-2.0 Section 3.3 apply to this + License. + +-------------------------------------------------------------------------------- +EXHIBITS +-------------------------------------------------------------------------------- + +Exhibit A - Ethical Use Guidelines +Exhibit B - Quantum-Safe Provenance Specification + +See separate files: +- EXHIBIT-A-ETHICAL-USE.txt +- EXHIBIT-B-QUANTUM-SAFE.txt + +-------------------------------------------------------------------------------- +END OF PALIMPSEST-MPL LICENSE VERSION 1.0 +-------------------------------------------------------------------------------- + +For questions about this License: +- Repository: https://github.com/hyperpolymath/palimpsest-license +- Council: contact via repository Issues diff --git a/README.adoc b/README.adoc index e953379..9b95e47 100644 --- a/README.adoc +++ b/README.adoc @@ -1,100 +1,100 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell - -= Claude Integrations -Jonathan D.A. Jewell -:toc: left -:toclevels: 3 -:icons: font -:source-highlighter: rouge - -Unified monorepo consolidating all Claude browser and service integration projects. - -image:https://img.shields.io/badge/license-PMPL--1.0--or--later-blue.svg[License,link="https://github.com/hyperpolymath/palimpsest-license"] - -== Overview - -This repository brings together five previously separate repositories into a single, -coherent workspace for developing Claude AI integrations across browsers and services. - -== Components - -[cols="1,3,1"] -|=== -| Directory | Description | Stack - -| firefox-lsp/ -| LSP implementation for Firefox browser automation via Marionette protocol -| Elixir - -| firefox-mcp/ -| Model Context Protocol server for Firefox browser control -| Deno / JavaScript - -| gecko-browser-extension/ -| Browser extension for Gecko-based browsers (Firefox, Thunderbird, SeaMonkey) -| JavaScript / WebExtensions API - -| gitlab-bridge/ -| Bridge between Claude AI and GitLab for developer productivity -| ReScript / Deno - -| mozilla-extension/ -| Claude AI assistant extension for Mozilla products -| JavaScript / WebExtensions API -|=== - -== Getting Started - -Each component has its own README with setup instructions. Navigate to the -relevant subdirectory for component-specific documentation. - -[source,bash] ----- -# List available build recipes -just --list - -# Build a specific component -just build-firefox-lsp -just build-firefox-mcp -just build-gecko-extension -just build-gitlab-bridge -just build-mozilla-extension ----- - -== Repository Structure - ----- -claude-integrations/ -├── firefox-lsp/ # Firefox LSP via Marionette protocol -├── firefox-mcp/ # Firefox MCP server -├── gecko-browser-extension/ # Gecko browser extension -├── gitlab-bridge/ # GitLab integration bridge -├── mozilla-extension/ # Mozilla products extension -├── .machine_readable/ # SCM checkpoint files -├── Justfile # Build recipes -├── LICENSE # PMPL-1.0-or-later -└── README.adoc # This file ----- - -== History - -This monorepo was created by consolidating the following repositories: - -* claude-firefox-lsp -* claude-firefox-mcp -* claude-gecko-browser-extension -* claude-gitlab-bridge -* claude-mozilla-extension - -Full git history for each component is preserved in the original repositories -(archived). - -== License - -SPDX-License-Identifier: PMPL-1.0-or-later - -Copyright (C) 2024-2026 Jonathan D.A. Jewell - -Licensed under the Palimpsest License, Version 1.0 or later. -See link:LICENSE[] for the full license text. +// SPDX-License-Identifier: PMPL-1.0-or-later +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell + += Claude Integrations +Jonathan D.A. Jewell +:toc: left +:toclevels: 3 +:icons: font +:source-highlighter: rouge + +Unified monorepo consolidating all Claude browser and service integration projects. + +image:https://img.shields.io/badge/license-PMPL--1.0--or--later-blue.svg[License,link="https://github.com/hyperpolymath/palimpsest-license"] + +== Overview + +This repository brings together five previously separate repositories into a single, +coherent workspace for developing Claude AI integrations across browsers and services. + +== Components + +[cols="1,3,1"] +|=== +| Directory | Description | Stack + +| firefox-lsp/ +| LSP implementation for Firefox browser automation via Marionette protocol +| Elixir + +| firefox-mcp/ +| Model Context Protocol server for Firefox browser control +| Deno / JavaScript + +| gecko-browser-extension/ +| Browser extension for Gecko-based browsers (Firefox, Thunderbird, SeaMonkey) +| JavaScript / WebExtensions API + +| gitlab-bridge/ +| Bridge between Claude AI and GitLab for developer productivity +| ReScript / Deno + +| mozilla-extension/ +| Claude AI assistant extension for Mozilla products +| JavaScript / WebExtensions API +|=== + +== Getting Started + +Each component has its own README with setup instructions. Navigate to the +relevant subdirectory for component-specific documentation. + +[source,bash] +---- +# List available build recipes +just --list + +# Build a specific component +just build-firefox-lsp +just build-firefox-mcp +just build-gecko-extension +just build-gitlab-bridge +just build-mozilla-extension +---- + +== Repository Structure + +---- +claude-integrations/ +├── firefox-lsp/ # Firefox LSP via Marionette protocol +├── firefox-mcp/ # Firefox MCP server +├── gecko-browser-extension/ # Gecko browser extension +├── gitlab-bridge/ # GitLab integration bridge +├── mozilla-extension/ # Mozilla products extension +├── .machine_readable/ # SCM checkpoint files +├── Justfile # Build recipes +├── LICENSE # PMPL-1.0-or-later +└── README.adoc # This file +---- + +== History + +This monorepo was created by consolidating the following repositories: + +* claude-firefox-lsp +* claude-firefox-mcp +* claude-gecko-browser-extension +* claude-gitlab-bridge +* claude-mozilla-extension + +Full git history for each component is preserved in the original repositories +(archived). + +== License + +SPDX-License-Identifier: PMPL-1.0-or-later + +Copyright (C) 2024-2026 Jonathan D.A. Jewell + +Licensed under the Palimpsest License, Version 1.0 or later. +See link:LICENSE[] for the full license text. diff --git a/firefox-lsp/.formatter.exs b/firefox-lsp/.formatter.exs index d2cda26..7338bbc 100644 --- a/firefox-lsp/.formatter.exs +++ b/firefox-lsp/.formatter.exs @@ -1,4 +1,4 @@ -# Used by "mix format" -[ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] -] +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/firefox-lsp/.github/release-template.md b/firefox-lsp/.github/release-template.md index 31e04e8..b0976f4 100644 --- a/firefox-lsp/.github/release-template.md +++ b/firefox-lsp/.github/release-template.md @@ -1,43 +1,43 @@ -# v1.0.0 - Production Release 🎉 - -First production-ready release of this LSP server! - -## ✨ Features - -- Complete LSP protocol implementation -- Text synchronization (didOpen, didChange, didClose) -- Intelligent completion based on context -- Real-time diagnostics and validation -- Hover documentation -- Multiple adapter implementations -- VSCode extension (.vsix included) -- Comprehensive test suite -- Enterprise-grade CI/CD - -## 📦 Installation - -### Via VSCode Extension - -1. Download the `.vsix` file from this release -2. Open VSCode -3. Press `Ctrl+Shift+P` (Cmd+Shift+P on Mac) -4. Type "Extensions: Install from VSIX" -5. Select the downloaded `.vsix` file - -### Manual Installation - -```bash -git clone https://github.com/hyperpolymath/REPO_NAME.git -cd REPO_NAME -./install.sh -``` - -## 📚 Documentation - -- [README](./README.md) - Overview and getting started -- [USAGE.md](./USAGE.md) - Detailed usage guide -- [Examples](./examples/) - Configuration examples - -## 🙏 Credits - -Built with ❤️ using Elixir, GenLSP, and the BEAM VM. +# v1.0.0 - Production Release 🎉 + +First production-ready release of this LSP server! + +## ✨ Features + +- Complete LSP protocol implementation +- Text synchronization (didOpen, didChange, didClose) +- Intelligent completion based on context +- Real-time diagnostics and validation +- Hover documentation +- Multiple adapter implementations +- VSCode extension (.vsix included) +- Comprehensive test suite +- Enterprise-grade CI/CD + +## 📦 Installation + +### Via VSCode Extension + +1. Download the `.vsix` file from this release +2. Open VSCode +3. Press `Ctrl+Shift+P` (Cmd+Shift+P on Mac) +4. Type "Extensions: Install from VSIX" +5. Select the downloaded `.vsix` file + +### Manual Installation + +```bash +git clone https://github.com/hyperpolymath/REPO_NAME.git +cd REPO_NAME +./install.sh +``` + +## 📚 Documentation + +- [README](./README.md) - Overview and getting started +- [USAGE.md](./USAGE.md) - Detailed usage guide +- [Examples](./examples/) - Configuration examples + +## 🙏 Credits + +Built with ❤️ using Elixir, GenLSP, and the BEAM VM. diff --git a/firefox-lsp/.github/workflows/codeql.yml b/firefox-lsp/.github/workflows/codeql.yml index 5d3b3a3..ca4f1f7 100644 --- a/firefox-lsp/.github/workflows/codeql.yml +++ b/firefox-lsp/.github/workflows/codeql.yml @@ -1,29 +1,29 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -name: CodeQL - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - schedule: - - cron: '0 0 * * 1' - -permissions: - security-events: write - contents: read - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: javascript - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 +# SPDX-License-Identifier: PMPL-1.0-or-later +name: CodeQL + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + - cron: '0 0 * * 1' + +permissions: + security-events: write + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: javascript + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/firefox-lsp/.github/workflows/coverage.yml b/firefox-lsp/.github/workflows/coverage.yml index 7e6a941..a3b7891 100644 --- a/firefox-lsp/.github/workflows/coverage.yml +++ b/firefox-lsp/.github/workflows/coverage.yml @@ -1,26 +1,26 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -name: Test Coverage - -on: - push: - branches: [ main ] - -permissions: read-all - -jobs: - coverage: - name: Test Coverage - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Set up Elixir - uses: erlef/setup-beam@v1 - with: - elixir-version: '1.17' - otp-version: '27' - - name: Install dependencies - run: mix deps.get - - name: Run tests with coverage - run: mix coveralls.github - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +# SPDX-License-Identifier: PMPL-1.0-or-later +name: Test Coverage + +on: + push: + branches: [ main ] + +permissions: read-all + +jobs: + coverage: + name: Test Coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: '1.17' + otp-version: '27' + - name: Install dependencies + run: mix deps.get + - name: Run tests with coverage + run: mix coveralls.github + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/firefox-lsp/.github/workflows/elixir-ci.yml b/firefox-lsp/.github/workflows/elixir-ci.yml index 6ce174a..ca626cd 100644 --- a/firefox-lsp/.github/workflows/elixir-ci.yml +++ b/firefox-lsp/.github/workflows/elixir-ci.yml @@ -1,38 +1,38 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -name: Elixir CI - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -permissions: read-all - -jobs: - test: - name: Test (Elixir ${{matrix.elixir}} / OTP ${{matrix.otp}}) - runs-on: ubuntu-latest - strategy: - matrix: - elixir: ['1.17'] - otp: ['27'] - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Set up Elixir - uses: erlef/setup-beam@v1 - with: - elixir-version: ${{matrix.elixir}} - otp-version: ${{matrix.otp}} - - name: Restore dependencies cache - uses: actions/cache@v4 - with: - path: deps - key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} - restore-keys: ${{ runner.os }}-mix- - - name: Install dependencies - run: mix deps.get - - name: Check formatting - run: mix format --check-formatted - - name: Run tests - run: mix test +# SPDX-License-Identifier: PMPL-1.0-or-later +name: Elixir CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +permissions: read-all + +jobs: + test: + name: Test (Elixir ${{matrix.elixir}} / OTP ${{matrix.otp}}) + runs-on: ubuntu-latest + strategy: + matrix: + elixir: ['1.17'] + otp: ['27'] + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{matrix.elixir}} + otp-version: ${{matrix.otp}} + - name: Restore dependencies cache + uses: actions/cache@v4 + with: + path: deps + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix- + - name: Install dependencies + run: mix deps.get + - name: Check formatting + run: mix format --check-formatted + - name: Run tests + run: mix test diff --git a/firefox-lsp/.github/workflows/quality.yml b/firefox-lsp/.github/workflows/quality.yml index c71cfd6..6ad21e6 100644 --- a/firefox-lsp/.github/workflows/quality.yml +++ b/firefox-lsp/.github/workflows/quality.yml @@ -1,33 +1,33 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -name: Quality - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -permissions: read-all - -jobs: - quality: - name: Code Quality - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Set up Elixir - uses: erlef/setup-beam@v1 - with: - elixir-version: '1.17' - otp-version: '27' - - name: Restore dependencies cache - uses: actions/cache@v4 - with: - path: deps - key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} - - name: Install dependencies - run: mix deps.get - - name: Run Credo - run: mix credo --strict - - name: Run Dialyzer - run: mix dialyzer +# SPDX-License-Identifier: PMPL-1.0-or-later +name: Quality + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +permissions: read-all + +jobs: + quality: + name: Code Quality + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: '1.17' + otp-version: '27' + - name: Restore dependencies cache + uses: actions/cache@v4 + with: + path: deps + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + - name: Install dependencies + run: mix deps.get + - name: Run Credo + run: mix credo --strict + - name: Run Dialyzer + run: mix dialyzer diff --git a/firefox-lsp/.gitignore b/firefox-lsp/.gitignore index ef31659..bd9b806 100644 --- a/firefox-lsp/.gitignore +++ b/firefox-lsp/.gitignore @@ -1,40 +1,40 @@ -# The directory Mix will write compiled artifacts to. -/_build/ - -# If you run "mix test --cover", coverage assets end up here. -/cover/ - -# The directory Mix downloads your dependencies sources to. -/deps/ - -# Where third-party dependencies like ExDoc output generated docs. -/doc/ - -# Temporary files, for example, from tests. -/tmp/ - -# If the VM crashes, it generates a dump, let's ignore it too. -erl_crash.dump - -# Also ignore archive artifacts (built via "mix archive.build"). -*.ez - -# Ignore package tarball (built via "mix hex.build"). -poly_ssg_lsp-*.tar - - -# VSCode Extension -vscode-extension/node_modules/ -vscode-extension/out/ -target/ -node_modules/ -_build/ -deps/ -.elixir_ls/ -.cache/ -*.log -*.tmp -.DS_Store -Thumbs.db -build/ -dist/ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Temporary files, for example, from tests. +/tmp/ + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +poly_ssg_lsp-*.tar + + +# VSCode Extension +vscode-extension/node_modules/ +vscode-extension/out/ +target/ +node_modules/ +_build/ +deps/ +.elixir_ls/ +.cache/ +*.log +*.tmp +.DS_Store +Thumbs.db +build/ +dist/ diff --git a/firefox-lsp/AI.a2ml b/firefox-lsp/AI.a2ml index c252b68..70c5960 100644 --- a/firefox-lsp/AI.a2ml +++ b/firefox-lsp/AI.a2ml @@ -1,41 +1,41 @@ - -# AI Assistant Instructions - -## CRITICAL: Language Immutability - -⚠️ **DO NOT CHANGE THE PROGRAMMING LANGUAGE OF THIS REPOSITORY** - -This repository is an **Elixir LSP server** leveraging the BEAM VM. -- The implementation language MUST remain Elixir -- Do NOT port, rewrite, or translate to Rust, Go, TypeScript, Python, or any other language -- Do NOT change the repository description or technical approach -- Elixir + BEAM provides: hot code reload, supervision trees, process isolation, fault tolerance -- The language and architecture are fundamental to the project's design - -## Repository Focus -- This is a Language Server Protocol (LSP) implementation in Elixir -- Uses GenLSP framework for LSP compliance -- Adapters run as isolated GenServer processes under supervision -- Follow Rhodium Standard Repository (RSR) conventions - -## Workflow -1. Inspect `STATE.scm` for current state, blockers, and next actions -2. Respect constraints in `META.scm` architecture decisions -3. After finishing edits, update STATE.scm and commit with imperative message -4. Never commit node_modules or build artifacts - -## Code Quality -- Run `just quality` before committing (fmt, lint, dialyzer) -- Maintain SPDX headers (PMPL-1.0-or-later) -- Use correct author: Jonathan D.A. Jewell -- Keep adapters isolated in separate GenServer processes - -## Testing -- Write ExUnit tests for all adapters -- Test LSP protocol compliance -- Mock external CLI tools in tests -- Run `just test` before pushing - -## Delivery Promises -- Mention if STATE.scm, checkpoints, or workflows changed -- Keep this file in sync with repository governance + +# AI Assistant Instructions + +## CRITICAL: Language Immutability + +⚠️ **DO NOT CHANGE THE PROGRAMMING LANGUAGE OF THIS REPOSITORY** + +This repository is an **Elixir LSP server** leveraging the BEAM VM. +- The implementation language MUST remain Elixir +- Do NOT port, rewrite, or translate to Rust, Go, TypeScript, Python, or any other language +- Do NOT change the repository description or technical approach +- Elixir + BEAM provides: hot code reload, supervision trees, process isolation, fault tolerance +- The language and architecture are fundamental to the project's design + +## Repository Focus +- This is a Language Server Protocol (LSP) implementation in Elixir +- Uses GenLSP framework for LSP compliance +- Adapters run as isolated GenServer processes under supervision +- Follow Rhodium Standard Repository (RSR) conventions + +## Workflow +1. Inspect `STATE.scm` for current state, blockers, and next actions +2. Respect constraints in `META.scm` architecture decisions +3. After finishing edits, update STATE.scm and commit with imperative message +4. Never commit node_modules or build artifacts + +## Code Quality +- Run `just quality` before committing (fmt, lint, dialyzer) +- Maintain SPDX headers (PMPL-1.0-or-later) +- Use correct author: Jonathan D.A. Jewell +- Keep adapters isolated in separate GenServer processes + +## Testing +- Write ExUnit tests for all adapters +- Test LSP protocol compliance +- Mock external CLI tools in tests +- Run `just test` before pushing + +## Delivery Promises +- Mention if STATE.scm, checkpoints, or workflows changed +- Keep this file in sync with repository governance diff --git a/firefox-lsp/CHANGELOG.md b/firefox-lsp/CHANGELOG.md index 596e6db..bc33b4d 100644 --- a/firefox-lsp/CHANGELOG.md +++ b/firefox-lsp/CHANGELOG.md @@ -1,33 +1,33 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -### Added -- Initial project structure -- Firefox adapter using Marionette protocol -- LSP server with GenLSP framework -- 7 LSP commands: navigate, click, typeText, screenshot, executeJs, getContent, detectBrowsers -- Adapter behaviour contract -- VS Code extension scaffold -- Comprehensive documentation (README.adoc) -- Checkpoint files (STATE.scm, META.scm, ECOSYSTEM.scm) -- Justfile for common tasks - -### Technical Details -- Marionette protocol implementation with TCP socket communication -- GenServer-based adapter with persistent state -- Base64 screenshot encoding for JSON transport -- Supervisor tree for fault isolation - -## [0.1.0] - 2026-02-05 - -### Added -- Initial project creation from poly-ssg-lsp template - -[Unreleased]: https://github.com/hyperpolymath/claude-firefox-lsp/compare/v0.1.0...HEAD -[0.1.0]: https://github.com/hyperpolymath/claude-firefox-lsp/releases/tag/v0.1.0 +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Initial project structure +- Firefox adapter using Marionette protocol +- LSP server with GenLSP framework +- 7 LSP commands: navigate, click, typeText, screenshot, executeJs, getContent, detectBrowsers +- Adapter behaviour contract +- VS Code extension scaffold +- Comprehensive documentation (README.adoc) +- Checkpoint files (STATE.scm, META.scm, ECOSYSTEM.scm) +- Justfile for common tasks + +### Technical Details +- Marionette protocol implementation with TCP socket communication +- GenServer-based adapter with persistent state +- Base64 screenshot encoding for JSON transport +- Supervisor tree for fault isolation + +## [0.1.0] - 2026-02-05 + +### Added +- Initial project creation from poly-ssg-lsp template + +[Unreleased]: https://github.com/hyperpolymath/claude-firefox-lsp/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/hyperpolymath/claude-firefox-lsp/releases/tag/v0.1.0 diff --git a/firefox-lsp/Justfile b/firefox-lsp/Justfile index a0c9604..c1e54b8 100644 --- a/firefox-lsp/Justfile +++ b/firefox-lsp/Justfile @@ -1,97 +1,97 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell - -# List available recipes -default: - @just --list - -# Setup project dependencies -setup: - mix deps.get - mix compile - cd vscode-extension && npm install - -# Install dependencies -deps: - mix deps.get - -# Compile the project -build: - mix compile - -# Run tests -test: - mix test - -# Run tests with coverage -test-coverage: - mix coveralls.html - -# Format code -fmt: - mix format - cd vscode-extension && npm run lint -- --fix - -# Check code formatting -fmt-check: - mix format --check-formatted - -# Run linter -lint: - mix credo --strict - -# Run type checker -dialyzer: - mix dialyzer - -# Run all quality checks -quality: fmt-check lint dialyzer - -# Generate documentation -docs: - mix docs - -# Clean build artifacts -clean: - mix clean - rm -rf _build deps doc - cd vscode-extension && rm -rf node_modules out - -# Start the LSP server (stdio mode) -start: - mix run --no-halt - -# Start Firefox with Marionette -firefox: - firefox -marionette -headless - -# Start IEx REPL with project loaded -repl: - iex -S mix - -# Run a specific adapter test -test-adapter adapter: - mix test test/adapters/{{adapter}}_test.exs - -# Check for outdated dependencies -deps-outdated: - mix hex.outdated - -# Update dependencies -deps-update: - mix deps.update --all - -# Create a release build -release: - MIX_ENV=prod mix release - -# Build VSCode extension -vscode-build: - cd vscode-extension && npm run compile - -# Package VSCode extension -vscode-package: - cd vscode-extension && npm run package - -# Run CI checks locally -ci: quality test +# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell + +# List available recipes +default: + @just --list + +# Setup project dependencies +setup: + mix deps.get + mix compile + cd vscode-extension && npm install + +# Install dependencies +deps: + mix deps.get + +# Compile the project +build: + mix compile + +# Run tests +test: + mix test + +# Run tests with coverage +test-coverage: + mix coveralls.html + +# Format code +fmt: + mix format + cd vscode-extension && npm run lint -- --fix + +# Check code formatting +fmt-check: + mix format --check-formatted + +# Run linter +lint: + mix credo --strict + +# Run type checker +dialyzer: + mix dialyzer + +# Run all quality checks +quality: fmt-check lint dialyzer + +# Generate documentation +docs: + mix docs + +# Clean build artifacts +clean: + mix clean + rm -rf _build deps doc + cd vscode-extension && rm -rf node_modules out + +# Start the LSP server (stdio mode) +start: + mix run --no-halt + +# Start Firefox with Marionette +firefox: + firefox -marionette -headless + +# Start IEx REPL with project loaded +repl: + iex -S mix + +# Run a specific adapter test +test-adapter adapter: + mix test test/adapters/{{adapter}}_test.exs + +# Check for outdated dependencies +deps-outdated: + mix hex.outdated + +# Update dependencies +deps-update: + mix deps.update --all + +# Create a release build +release: + MIX_ENV=prod mix release + +# Build VSCode extension +vscode-build: + cd vscode-extension && npm run compile + +# Package VSCode extension +vscode-package: + cd vscode-extension && npm run package + +# Run CI checks locally +ci: quality test diff --git a/firefox-lsp/LICENSE b/firefox-lsp/LICENSE index d2dccf5..2078cd7 100644 --- a/firefox-lsp/LICENSE +++ b/firefox-lsp/LICENSE @@ -1,153 +1,153 @@ -SPDX-License-Identifier: PMPL-1.0-or-later -SPDX-FileCopyrightText: 2024-2025 Palimpsest Stewardship Council - -================================================================================ -PALIMPSEST-MPL LICENSE VERSION 1.0 -================================================================================ - -File-level copyleft with ethical use and quantum-safe provenance - -Based on Mozilla Public License 2.0 - --------------------------------------------------------------------------------- -PREAMBLE --------------------------------------------------------------------------------- - -This License extends the Mozilla Public License 2.0 (MPL-2.0) with provisions -for ethical use, post-quantum cryptographic provenance, and emotional lineage -protection. The base MPL-2.0 terms apply except where explicitly modified by -the Exhibits below. - -Like a palimpsest manuscript where each layer builds upon what came before, -this license recognizes that creative works carry history, context, and meaning -that transcend mere code or text. - --------------------------------------------------------------------------------- -SECTION 1: BASE LICENSE --------------------------------------------------------------------------------- - -This License incorporates the full text of Mozilla Public License 2.0 by -reference. The complete MPL-2.0 text is available at: -https://www.mozilla.org/en-US/MPL/2.0/ - -All terms, conditions, and definitions from MPL-2.0 apply except where -explicitly modified by the Exhibits in this License. - --------------------------------------------------------------------------------- -SECTION 2: ADDITIONAL DEFINITIONS --------------------------------------------------------------------------------- - -2.1. "Emotional Lineage" - means the narrative, cultural, symbolic, and contextual meaning embedded - in Covered Software, including but not limited to: protest traditions, - cultural heritage, trauma narratives, and community stories. - -2.2. "Provenance Metadata" - means cryptographically signed attribution information attached to or - associated with Covered Software, including author identities, timestamps, - modification history, and lineage references. - -2.3. "Non-Interpretive System" - means any automated system that processes Covered Software without - preserving or considering its Emotional Lineage, including but not - limited to: AI training pipelines, content aggregators, and automated - summarization tools. - -2.4. "Quantum-Safe Signature" - means a cryptographic signature using algorithms resistant to attacks - by quantum computers, as specified in Exhibit B. - --------------------------------------------------------------------------------- -SECTION 3: ETHICAL USE REQUIREMENTS --------------------------------------------------------------------------------- - -In addition to the rights and obligations under MPL-2.0: - -3.1. Emotional Lineage Preservation - You must make reasonable efforts to preserve and communicate the - Emotional Lineage of Covered Software when distributing or creating - derivative works. This includes maintaining narrative context, cultural - attributions, and symbolic meaning where documented. - -3.2. Non-Interpretive System Notice - If You use Covered Software as input to a Non-Interpretive System, You - must: - (a) document such use in a publicly accessible manner; and - (b) not claim that outputs of such systems carry the Emotional Lineage - of the original work without explicit permission from Contributors. - -3.3. Ethical Use Declaration - Commercial use of Covered Software requires acknowledgment that You have - read and understood Exhibit A (Ethical Use Guidelines) and agree to act - in good faith accordance with its principles. - -See Exhibit A for complete Ethical Use Guidelines. - --------------------------------------------------------------------------------- -SECTION 4: PROVENANCE REQUIREMENTS --------------------------------------------------------------------------------- - -4.1. Metadata Preservation - You must not strip, alter, or obscure Provenance Metadata from Covered - Software except where technically necessary and with clear documentation - of any changes. - -4.2. Quantum-Safe Provenance (Optional) - Contributors may sign their Contributions using Quantum-Safe Signatures. - If Quantum-Safe Signatures are present, You must preserve them in all - distributions. - -4.3. Lineage Chain - When creating derivative works, You should extend the provenance chain - to include Your own contributions, maintaining cryptographic linkage to - prior Contributors where feasible. - -See Exhibit B for Quantum-Safe Provenance specifications. - --------------------------------------------------------------------------------- -SECTION 5: GOVERNANCE --------------------------------------------------------------------------------- - -5.1. Stewardship Council - This License is maintained by the Palimpsest Stewardship Council, which - may issue clarifications, interpretive guidance, and future versions. - -5.2. Version Selection - You may use Covered Software under this version of the License or any - later version published by the Palimpsest Stewardship Council. - -5.3. Dispute Resolution - Disputes regarding interpretation of Ethical Use Requirements (Section 3) - should first be submitted to the Palimpsest Stewardship Council for - non-binding guidance before pursuing legal remedies. - --------------------------------------------------------------------------------- -SECTION 6: COMPATIBILITY --------------------------------------------------------------------------------- - -6.1. MPL-2.0 Compatibility - Covered Software under this License may be combined with software under - MPL-2.0. The combined work must comply with both licenses. - -6.2. Secondary Licenses - The Secondary License provisions of MPL-2.0 Section 3.3 apply to this - License. - --------------------------------------------------------------------------------- -EXHIBITS --------------------------------------------------------------------------------- - -Exhibit A - Ethical Use Guidelines -Exhibit B - Quantum-Safe Provenance Specification - -See separate files: -- EXHIBIT-A-ETHICAL-USE.txt -- EXHIBIT-B-QUANTUM-SAFE.txt - --------------------------------------------------------------------------------- -END OF PALIMPSEST-MPL LICENSE VERSION 1.0 --------------------------------------------------------------------------------- - -For questions about this License: -- Repository: https://github.com/hyperpolymath/palimpsest-license -- Council: contact via repository Issues +SPDX-License-Identifier: PMPL-1.0-or-later +SPDX-FileCopyrightText: 2024-2025 Palimpsest Stewardship Council + +================================================================================ +PALIMPSEST-MPL LICENSE VERSION 1.0 +================================================================================ + +File-level copyleft with ethical use and quantum-safe provenance + +Based on Mozilla Public License 2.0 + +-------------------------------------------------------------------------------- +PREAMBLE +-------------------------------------------------------------------------------- + +This License extends the Mozilla Public License 2.0 (MPL-2.0) with provisions +for ethical use, post-quantum cryptographic provenance, and emotional lineage +protection. The base MPL-2.0 terms apply except where explicitly modified by +the Exhibits below. + +Like a palimpsest manuscript where each layer builds upon what came before, +this license recognizes that creative works carry history, context, and meaning +that transcend mere code or text. + +-------------------------------------------------------------------------------- +SECTION 1: BASE LICENSE +-------------------------------------------------------------------------------- + +This License incorporates the full text of Mozilla Public License 2.0 by +reference. The complete MPL-2.0 text is available at: +https://www.mozilla.org/en-US/MPL/2.0/ + +All terms, conditions, and definitions from MPL-2.0 apply except where +explicitly modified by the Exhibits in this License. + +-------------------------------------------------------------------------------- +SECTION 2: ADDITIONAL DEFINITIONS +-------------------------------------------------------------------------------- + +2.1. "Emotional Lineage" + means the narrative, cultural, symbolic, and contextual meaning embedded + in Covered Software, including but not limited to: protest traditions, + cultural heritage, trauma narratives, and community stories. + +2.2. "Provenance Metadata" + means cryptographically signed attribution information attached to or + associated with Covered Software, including author identities, timestamps, + modification history, and lineage references. + +2.3. "Non-Interpretive System" + means any automated system that processes Covered Software without + preserving or considering its Emotional Lineage, including but not + limited to: AI training pipelines, content aggregators, and automated + summarization tools. + +2.4. "Quantum-Safe Signature" + means a cryptographic signature using algorithms resistant to attacks + by quantum computers, as specified in Exhibit B. + +-------------------------------------------------------------------------------- +SECTION 3: ETHICAL USE REQUIREMENTS +-------------------------------------------------------------------------------- + +In addition to the rights and obligations under MPL-2.0: + +3.1. Emotional Lineage Preservation + You must make reasonable efforts to preserve and communicate the + Emotional Lineage of Covered Software when distributing or creating + derivative works. This includes maintaining narrative context, cultural + attributions, and symbolic meaning where documented. + +3.2. Non-Interpretive System Notice + If You use Covered Software as input to a Non-Interpretive System, You + must: + (a) document such use in a publicly accessible manner; and + (b) not claim that outputs of such systems carry the Emotional Lineage + of the original work without explicit permission from Contributors. + +3.3. Ethical Use Declaration + Commercial use of Covered Software requires acknowledgment that You have + read and understood Exhibit A (Ethical Use Guidelines) and agree to act + in good faith accordance with its principles. + +See Exhibit A for complete Ethical Use Guidelines. + +-------------------------------------------------------------------------------- +SECTION 4: PROVENANCE REQUIREMENTS +-------------------------------------------------------------------------------- + +4.1. Metadata Preservation + You must not strip, alter, or obscure Provenance Metadata from Covered + Software except where technically necessary and with clear documentation + of any changes. + +4.2. Quantum-Safe Provenance (Optional) + Contributors may sign their Contributions using Quantum-Safe Signatures. + If Quantum-Safe Signatures are present, You must preserve them in all + distributions. + +4.3. Lineage Chain + When creating derivative works, You should extend the provenance chain + to include Your own contributions, maintaining cryptographic linkage to + prior Contributors where feasible. + +See Exhibit B for Quantum-Safe Provenance specifications. + +-------------------------------------------------------------------------------- +SECTION 5: GOVERNANCE +-------------------------------------------------------------------------------- + +5.1. Stewardship Council + This License is maintained by the Palimpsest Stewardship Council, which + may issue clarifications, interpretive guidance, and future versions. + +5.2. Version Selection + You may use Covered Software under this version of the License or any + later version published by the Palimpsest Stewardship Council. + +5.3. Dispute Resolution + Disputes regarding interpretation of Ethical Use Requirements (Section 3) + should first be submitted to the Palimpsest Stewardship Council for + non-binding guidance before pursuing legal remedies. + +-------------------------------------------------------------------------------- +SECTION 6: COMPATIBILITY +-------------------------------------------------------------------------------- + +6.1. MPL-2.0 Compatibility + Covered Software under this License may be combined with software under + MPL-2.0. The combined work must comply with both licenses. + +6.2. Secondary Licenses + The Secondary License provisions of MPL-2.0 Section 3.3 apply to this + License. + +-------------------------------------------------------------------------------- +EXHIBITS +-------------------------------------------------------------------------------- + +Exhibit A - Ethical Use Guidelines +Exhibit B - Quantum-Safe Provenance Specification + +See separate files: +- EXHIBIT-A-ETHICAL-USE.txt +- EXHIBIT-B-QUANTUM-SAFE.txt + +-------------------------------------------------------------------------------- +END OF PALIMPSEST-MPL LICENSE VERSION 1.0 +-------------------------------------------------------------------------------- + +For questions about this License: +- Repository: https://github.com/hyperpolymath/palimpsest-license +- Council: contact via repository Issues diff --git a/firefox-lsp/PROJECT-SUMMARY.md b/firefox-lsp/PROJECT-SUMMARY.md index 212b7cd..3507519 100644 --- a/firefox-lsp/PROJECT-SUMMARY.md +++ b/firefox-lsp/PROJECT-SUMMARY.md @@ -1,216 +1,216 @@ -# Claude Firefox LSP - Project Summary - -**Created:** 2026-02-05 -**Status:** Initial Development (15% complete to MVP) -**Location:** ~/Documents/hyperpolymath-repos/claude-firefox-lsp - -## Overview - -Claude Firefox LSP is a Language Server Protocol implementation for Firefox browser automation. It enables editor-integrated web testing, scraping, and automation using Firefox's native Marionette protocol. - -## Architecture - -### Core Components - -1. **Adapter System** (lib/adapters/) - - `behaviour.ex` - Adapter contract defining 8 callbacks - - `firefox.ex` - Marionette protocol implementation (365 LOC) - - `supervisor.ex` - GenServer supervision tree - -2. **LSP Server** (lib/lsp/) - - `server.ex` - GenLSP-based server with 7 commands (138 LOC) - - Command execution and response formatting - -3. **VS Code Extension** (vscode-extension/) - - TypeScript client with command palette integration - - Base64 screenshot handling - - Configuration for server path and trace levels - -## Key Features - -### Implemented ✓ - -- **Navigate** - Load URLs with timeout and wait conditions -- **Click** - CSS selector/XPath element clicking -- **Type Text** - Form field text entry with clear option -- **Screenshot** - Full page or element capture (base64 encoded) -- **Execute JavaScript** - Arbitrary JS in page context -- **Get Content** - Extract HTML, text, or DOM representation -- **Detect Browsers** - Auto-detect available browsers - -### Technical Highlights - -- **Marionette Protocol**: Native Firefox automation via TCP (port 2828) -- **GenServer State**: Persistent session management with fault isolation -- **GenLSP Framework**: LSP protocol handling with STDIO transport -- **Supervisor Tree**: :one_for_one restart strategy for adapters -- **Base64 Screenshots**: JSON-compatible binary transport - -## File Structure - -``` -claude-firefox-lsp/ -├── lib/ -│ ├── adapters/ -│ │ ├── behaviour.ex # Adapter contract -│ │ ├── firefox.ex # Marionette implementation -│ │ └── supervisor.ex # Adapter supervision -│ ├── claude_firefox_lsp/ -│ │ └── application.ex # OTP application -│ ├── lsp/ -│ │ └── server.ex # GenLSP server -│ └── claude_firefox_lsp.ex # Main module -├── test/ -│ ├── claude_firefox/ -│ │ └── lsp_test.exs # Basic tests -│ └── test_helper.exs -├── vscode-extension/ -│ ├── src/ -│ │ └── extension.ts # VS Code client -│ ├── package.json # Extension manifest -│ ├── tsconfig.json # TypeScript config -│ └── README.md -├── CHANGELOG.md # Version history -├── ECOSYSTEM.scm # Ecosystem positioning -├── Justfile # Task automation -├── LICENSE # PMPL-1.0-or-later -├── META.scm # ADRs and design rationale -├── mix.exs # Elixir project config -├── README.adoc # User documentation -└── STATE.scm # Project state checkpoint -``` - -## Code Metrics - -- **Total Lines**: 748 LOC (Elixir) -- **Modules**: 6 (5 lib + 1 test) -- **Adapters**: 1 (Firefox) -- **LSP Commands**: 7 -- **Test Coverage**: Minimal (basic smoke tests only) - -## Technology Stack - -- **Language**: Elixir 1.17+ -- **Framework**: GenLSP 0.10+ -- **Protocol**: Marionette (Firefox native) -- **Transport**: TCP socket (port 2828) -- **Testing**: ExUnit, Mox -- **Quality**: Credo, Dialyzer, ExCoveralls -- **VS Code**: TypeScript, vscode-languageclient - -## Dependencies - -### Elixir (mix.exs) - -```elixir -{:gen_lsp, "~> 0.10"} # LSP framework -{:jason, "~> 1.4"} # JSON parsing -{:req, "~> 0.5"} # HTTP client (future use) -{:credo, "~> 1.7"} # Linter -{:dialyxir, "~> 1.4"} # Type checker -{:ex_doc, "~> 0.34"} # Documentation -{:excoveralls, "~> 0.18"} # Coverage -{:mox, "~> 1.1"} # Mocking -``` - -### TypeScript (package.json) - -```json -"vscode-languageclient": "^9.0.0" -"typescript": "^5.0.0" -``` - -## ADRs (Architecture Decision Records) - -### ADR-001: Use Marionette Protocol - -**Decision**: Use Marionette as primary Firefox automation interface - -**Rationale**: -- Native Firefox protocol (actively maintained by Mozilla) -- Full browser automation capabilities -- Lower latency than HTTP-based protocols - -**Trade-offs**: -- Requires manual Firefox startup with `-marionette` flag -- Protocol differs from Chrome (harder to unify multi-browser support) - -### ADR-002: Use GenLSP Framework - -**Decision**: Use GenLSP library for LSP server implementation - -**Rationale**: -- Proven framework (used by Elixir community) -- Handles protocol boilerplate (JSON-RPC, STDIO) -- Active maintenance and documentation - -### ADR-003: Base64 Screenshot Encoding - -**Decision**: Base64 encode screenshots for JSON transport - -**Rationale**: -- LSP is JSON-RPC based (text protocol) -- Maintains compatibility with standard LSP clients -- Avoids filesystem coupling - -**Trade-offs**: -- 33% payload size increase -- Higher memory usage for large screenshots - -## Usage - -### Start Firefox with Marionette - -```bash -firefox -marionette -headless -``` - -### Run LSP Server - -```bash -just setup # Install deps -just start # Start server -``` - -### VS Code Commands - -- `Firefox: Navigate to URL` -- `Firefox: Click Element` -- `Firefox: Type Text` -- `Firefox: Take Screenshot` -- `Firefox: Execute JavaScript` -- `Firefox: Get Page Content` -- `Firefox: Detect Available Browsers` - -## Next Steps - -### Phase 2: Polish (Pending) - -- [ ] Implement VS Code extension client -- [ ] Add comprehensive tests (integration, e2e) -- [ ] Error handling and retry logic -- [ ] Connection pooling for multiple sessions -- [ ] Document Marionette protocol details - -### Phase 3: Ecosystem Integration (Pending) - -- [ ] Add Chrome adapter (CDP protocol) -- [ ] Add WebKit adapter (Safari) -- [ ] Hypatia CI/CD integration -- [ ] GitHub workflows (hypatia-scan, codeql, mirror, etc.) -- [ ] Publish to Hex.pm - -## Related Projects - -- **poly-ssg-lsp**: Template source (adapter pattern, GenLSP) -- **poly-queue-lsp**: Architectural sibling (LSP for message queues) -- **poly-observability-lsp**: Architectural sibling (LSP for observability) -- **Hypatia**: Neurosymbolic CI/CD intelligence -- **gitbot-fleet**: Automated repo maintenance - -## License - -SPDX-License-Identifier: PMPL-1.0-or-later -Copyright (c) 2026 Jonathan D.A. Jewell - -Licensed under the Palimpsest License, version 1.0 or later. +# Claude Firefox LSP - Project Summary + +**Created:** 2026-02-05 +**Status:** Initial Development (15% complete to MVP) +**Location:** ~/Documents/hyperpolymath-repos/claude-firefox-lsp + +## Overview + +Claude Firefox LSP is a Language Server Protocol implementation for Firefox browser automation. It enables editor-integrated web testing, scraping, and automation using Firefox's native Marionette protocol. + +## Architecture + +### Core Components + +1. **Adapter System** (lib/adapters/) + - `behaviour.ex` - Adapter contract defining 8 callbacks + - `firefox.ex` - Marionette protocol implementation (365 LOC) + - `supervisor.ex` - GenServer supervision tree + +2. **LSP Server** (lib/lsp/) + - `server.ex` - GenLSP-based server with 7 commands (138 LOC) + - Command execution and response formatting + +3. **VS Code Extension** (vscode-extension/) + - TypeScript client with command palette integration + - Base64 screenshot handling + - Configuration for server path and trace levels + +## Key Features + +### Implemented ✓ + +- **Navigate** - Load URLs with timeout and wait conditions +- **Click** - CSS selector/XPath element clicking +- **Type Text** - Form field text entry with clear option +- **Screenshot** - Full page or element capture (base64 encoded) +- **Execute JavaScript** - Arbitrary JS in page context +- **Get Content** - Extract HTML, text, or DOM representation +- **Detect Browsers** - Auto-detect available browsers + +### Technical Highlights + +- **Marionette Protocol**: Native Firefox automation via TCP (port 2828) +- **GenServer State**: Persistent session management with fault isolation +- **GenLSP Framework**: LSP protocol handling with STDIO transport +- **Supervisor Tree**: :one_for_one restart strategy for adapters +- **Base64 Screenshots**: JSON-compatible binary transport + +## File Structure + +``` +claude-firefox-lsp/ +├── lib/ +│ ├── adapters/ +│ │ ├── behaviour.ex # Adapter contract +│ │ ├── firefox.ex # Marionette implementation +│ │ └── supervisor.ex # Adapter supervision +│ ├── claude_firefox_lsp/ +│ │ └── application.ex # OTP application +│ ├── lsp/ +│ │ └── server.ex # GenLSP server +│ └── claude_firefox_lsp.ex # Main module +├── test/ +│ ├── claude_firefox/ +│ │ └── lsp_test.exs # Basic tests +│ └── test_helper.exs +├── vscode-extension/ +│ ├── src/ +│ │ └── extension.ts # VS Code client +│ ├── package.json # Extension manifest +│ ├── tsconfig.json # TypeScript config +│ └── README.md +├── CHANGELOG.md # Version history +├── ECOSYSTEM.scm # Ecosystem positioning +├── Justfile # Task automation +├── LICENSE # PMPL-1.0-or-later +├── META.scm # ADRs and design rationale +├── mix.exs # Elixir project config +├── README.adoc # User documentation +└── STATE.scm # Project state checkpoint +``` + +## Code Metrics + +- **Total Lines**: 748 LOC (Elixir) +- **Modules**: 6 (5 lib + 1 test) +- **Adapters**: 1 (Firefox) +- **LSP Commands**: 7 +- **Test Coverage**: Minimal (basic smoke tests only) + +## Technology Stack + +- **Language**: Elixir 1.17+ +- **Framework**: GenLSP 0.10+ +- **Protocol**: Marionette (Firefox native) +- **Transport**: TCP socket (port 2828) +- **Testing**: ExUnit, Mox +- **Quality**: Credo, Dialyzer, ExCoveralls +- **VS Code**: TypeScript, vscode-languageclient + +## Dependencies + +### Elixir (mix.exs) + +```elixir +{:gen_lsp, "~> 0.10"} # LSP framework +{:jason, "~> 1.4"} # JSON parsing +{:req, "~> 0.5"} # HTTP client (future use) +{:credo, "~> 1.7"} # Linter +{:dialyxir, "~> 1.4"} # Type checker +{:ex_doc, "~> 0.34"} # Documentation +{:excoveralls, "~> 0.18"} # Coverage +{:mox, "~> 1.1"} # Mocking +``` + +### TypeScript (package.json) + +```json +"vscode-languageclient": "^9.0.0" +"typescript": "^5.0.0" +``` + +## ADRs (Architecture Decision Records) + +### ADR-001: Use Marionette Protocol + +**Decision**: Use Marionette as primary Firefox automation interface + +**Rationale**: +- Native Firefox protocol (actively maintained by Mozilla) +- Full browser automation capabilities +- Lower latency than HTTP-based protocols + +**Trade-offs**: +- Requires manual Firefox startup with `-marionette` flag +- Protocol differs from Chrome (harder to unify multi-browser support) + +### ADR-002: Use GenLSP Framework + +**Decision**: Use GenLSP library for LSP server implementation + +**Rationale**: +- Proven framework (used by Elixir community) +- Handles protocol boilerplate (JSON-RPC, STDIO) +- Active maintenance and documentation + +### ADR-003: Base64 Screenshot Encoding + +**Decision**: Base64 encode screenshots for JSON transport + +**Rationale**: +- LSP is JSON-RPC based (text protocol) +- Maintains compatibility with standard LSP clients +- Avoids filesystem coupling + +**Trade-offs**: +- 33% payload size increase +- Higher memory usage for large screenshots + +## Usage + +### Start Firefox with Marionette + +```bash +firefox -marionette -headless +``` + +### Run LSP Server + +```bash +just setup # Install deps +just start # Start server +``` + +### VS Code Commands + +- `Firefox: Navigate to URL` +- `Firefox: Click Element` +- `Firefox: Type Text` +- `Firefox: Take Screenshot` +- `Firefox: Execute JavaScript` +- `Firefox: Get Page Content` +- `Firefox: Detect Available Browsers` + +## Next Steps + +### Phase 2: Polish (Pending) + +- [ ] Implement VS Code extension client +- [ ] Add comprehensive tests (integration, e2e) +- [ ] Error handling and retry logic +- [ ] Connection pooling for multiple sessions +- [ ] Document Marionette protocol details + +### Phase 3: Ecosystem Integration (Pending) + +- [ ] Add Chrome adapter (CDP protocol) +- [ ] Add WebKit adapter (Safari) +- [ ] Hypatia CI/CD integration +- [ ] GitHub workflows (hypatia-scan, codeql, mirror, etc.) +- [ ] Publish to Hex.pm + +## Related Projects + +- **poly-ssg-lsp**: Template source (adapter pattern, GenLSP) +- **poly-queue-lsp**: Architectural sibling (LSP for message queues) +- **poly-observability-lsp**: Architectural sibling (LSP for observability) +- **Hypatia**: Neurosymbolic CI/CD intelligence +- **gitbot-fleet**: Automated repo maintenance + +## License + +SPDX-License-Identifier: PMPL-1.0-or-later +Copyright (c) 2026 Jonathan D.A. Jewell + +Licensed under the Palimpsest License, version 1.0 or later. diff --git a/firefox-lsp/QUICKSTART.md b/firefox-lsp/QUICKSTART.md index 1681a69..e66c323 100644 --- a/firefox-lsp/QUICKSTART.md +++ b/firefox-lsp/QUICKSTART.md @@ -1,49 +1,49 @@ -# Quick Start Guide - -## Prerequisites - -- Elixir 1.17+ -- Erlang/OTP 27+ -- VSCode (for extension) - -## 60-Second Setup - -```bash -# Clone and install -git clone https://github.com/hyperpolymath/claude-firefox-lsp.git -cd claude-firefox-lsp -./install.sh - -# Install VSCode extension -code --install-extension vscode-extension/*.vsix - -# Start using! -# Open a project in VSCode and the LSP will activate automatically -``` - -## Configuration - -Add to your VSCode `settings.json`: - -```json -{ - "lsp.serverPath": "/path/to/claude-firefox-lsp" -} -``` - -See [examples/vscode-settings.json](./examples/vscode-settings.json) for more options. - -## Troubleshooting - -**LSP not starting?** -- Run `mix test` to verify installation -- Check Output panel in VSCode (View → Output → select LSP) - -**Completions not working?** -- Ensure adapters are detected: `mix run -e 'IO.inspect(Adapters.detect())'` - -## Next Steps - -- Read [USAGE.md](./USAGE.md) for detailed features -- Check [examples/](./examples/) for configurations -- Join discussions on GitHub Issues +# Quick Start Guide + +## Prerequisites + +- Elixir 1.17+ +- Erlang/OTP 27+ +- VSCode (for extension) + +## 60-Second Setup + +```bash +# Clone and install +git clone https://github.com/hyperpolymath/claude-firefox-lsp.git +cd claude-firefox-lsp +./install.sh + +# Install VSCode extension +code --install-extension vscode-extension/*.vsix + +# Start using! +# Open a project in VSCode and the LSP will activate automatically +``` + +## Configuration + +Add to your VSCode `settings.json`: + +```json +{ + "lsp.serverPath": "/path/to/claude-firefox-lsp" +} +``` + +See [examples/vscode-settings.json](./examples/vscode-settings.json) for more options. + +## Troubleshooting + +**LSP not starting?** +- Run `mix test` to verify installation +- Check Output panel in VSCode (View → Output → select LSP) + +**Completions not working?** +- Ensure adapters are detected: `mix run -e 'IO.inspect(Adapters.detect())'` + +## Next Steps + +- Read [USAGE.md](./USAGE.md) for detailed features +- Check [examples/](./examples/) for configurations +- Join discussions on GitHub Issues diff --git a/firefox-lsp/README.adoc b/firefox-lsp/README.adoc index ad92fb3..0ab0d00 100644 --- a/firefox-lsp/README.adoc +++ b/firefox-lsp/README.adoc @@ -1,207 +1,207 @@ -= Claude Firefox LSP -Jonathan D.A. Jewell -:toc: left -:toclevels: 3 -:icons: font -:source-highlighter: rouge - -Language Server Protocol implementation for Firefox browser automation. - -== Overview - -Claude Firefox LSP provides LSP-based browser automation capabilities for Firefox, enabling editor integration for web testing, scraping, and automation tasks. - -Built on the https://firefox-source-docs.mozilla.org/testing/marionette/Protocol.html[Marionette protocol], Firefox's native automation interface. - -== Features - -* *Navigate* - Load URLs and control browser navigation -* *Click* - Click elements using CSS selectors or XPath -* *Type Text* - Enter text into form fields -* *Screenshot* - Capture full page or element screenshots -* *Execute JavaScript* - Run arbitrary JavaScript in page context -* *Extract Content* - Get page HTML, text, or DOM representation - -== Installation - -=== Prerequisites - -* Elixir 1.17+ -* Firefox (with Marionette support) - -=== Setup - -[source,bash] ----- -# Clone the repository -cd ~/Documents/hyperpolymath-repos -git clone https://github.com/hyperpolymath/claude-firefox-lsp -cd claude-firefox-lsp - -# Install dependencies -mix deps.get - -# Compile the project -mix compile ----- - -== Usage - -=== Starting Firefox with Marionette - -Firefox must be started with the `-marionette` flag: - -[source,bash] ----- -# Headless mode -firefox -marionette -headless - -# With visible UI (for debugging) -firefox -marionette ----- - -Or configure via `about:config`: - -* Set `marionette.enabled` to `true` -* Set `marionette.port` to `2828` (default) - -=== Running the LSP Server - -[source,bash] ----- -# Start the LSP server -mix run --no-halt - -# Or build a release -mix release -_build/prod/rel/claude_firefox_lsp/bin/claude_firefox_lsp start ----- - -=== LSP Commands - -The server exposes these workspace commands: - -[cols="1,2,1"] -|=== -| Command | Description | Arguments - -| `firefox.navigate` -| Navigate to URL -| `[url, opts?]` - -| `firefox.click` -| Click element -| `[selector, opts?]` - -| `firefox.typeText` -| Type into element -| `[selector, text, opts?]` - -| `firefox.screenshot` -| Take screenshot -| `[opts?]` - -| `firefox.executeJs` -| Execute JavaScript -| `[javascript, opts?]` - -| `firefox.getContent` -| Get page content -| `[opts?]` - -| `firefox.detectBrowsers` -| Detect available browsers -| `[]` -|=== - -=== Example: VS Code Integration - -[source,json] ----- -{ - "command": "firefox.navigate", - "arguments": ["https://example.com", {"timeout": 5000}] -} ----- - -== Architecture - -=== Components - -* *Adapters* - Browser-specific implementations (Firefox, Chrome, etc.) -** `ClaudeFirefox.Adapters.Behaviour` - Adapter contract -** `ClaudeFirefox.Adapters.Firefox` - Marionette protocol implementation -* *LSP Server* - GenLSP-based server handling LSP protocol -* *Handlers* - Command execution and response formatting - -=== Protocol: Marionette - -Firefox's Marionette protocol communicates over TCP (port 2828 by default). - -Message format: - ----- -: ----- - -Example: - ----- -47:{"id": 1, "name": "WebDriver:Navigate", "parameters": {"url": "https://example.com"}} ----- - -See https://firefox-source-docs.mozilla.org/testing/marionette/Protocol.html[Marionette Protocol Documentation]. - -== Development - -=== Running Tests - -[source,bash] ----- -mix test ----- - -=== Code Quality - -[source,bash] ----- -# Format code -mix format - -# Run linter -mix credo --strict - -# Type checking -mix dialyzer ----- - -=== Building Release - -[source,bash] ----- -MIX_ENV=prod mix release ----- - -== Ecosystem Integration - -Related projects: - -* *poly-ssg-lsp* - LSP for static site generators -* *poly-queue-lsp* - LSP for message queue systems -* *poly-observability-lsp* - LSP for observability platforms - -== License - -SPDX-License-Identifier: PMPL-1.0-or-later - -Copyright (c) 2026 Jonathan D.A. Jewell - -Licensed under the Palimpsest License, version 1.0 or later. - -== Contributing - -See link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] for contribution guidelines. - -== Security - -See link:SECURITY.md[SECURITY.md] for security policy. += Claude Firefox LSP +Jonathan D.A. Jewell +:toc: left +:toclevels: 3 +:icons: font +:source-highlighter: rouge + +Language Server Protocol implementation for Firefox browser automation. + +== Overview + +Claude Firefox LSP provides LSP-based browser automation capabilities for Firefox, enabling editor integration for web testing, scraping, and automation tasks. + +Built on the https://firefox-source-docs.mozilla.org/testing/marionette/Protocol.html[Marionette protocol], Firefox's native automation interface. + +== Features + +* *Navigate* - Load URLs and control browser navigation +* *Click* - Click elements using CSS selectors or XPath +* *Type Text* - Enter text into form fields +* *Screenshot* - Capture full page or element screenshots +* *Execute JavaScript* - Run arbitrary JavaScript in page context +* *Extract Content* - Get page HTML, text, or DOM representation + +== Installation + +=== Prerequisites + +* Elixir 1.17+ +* Firefox (with Marionette support) + +=== Setup + +[source,bash] +---- +# Clone the repository +cd ~/Documents/hyperpolymath-repos +git clone https://github.com/hyperpolymath/claude-firefox-lsp +cd claude-firefox-lsp + +# Install dependencies +mix deps.get + +# Compile the project +mix compile +---- + +== Usage + +=== Starting Firefox with Marionette + +Firefox must be started with the `-marionette` flag: + +[source,bash] +---- +# Headless mode +firefox -marionette -headless + +# With visible UI (for debugging) +firefox -marionette +---- + +Or configure via `about:config`: + +* Set `marionette.enabled` to `true` +* Set `marionette.port` to `2828` (default) + +=== Running the LSP Server + +[source,bash] +---- +# Start the LSP server +mix run --no-halt + +# Or build a release +mix release +_build/prod/rel/claude_firefox_lsp/bin/claude_firefox_lsp start +---- + +=== LSP Commands + +The server exposes these workspace commands: + +[cols="1,2,1"] +|=== +| Command | Description | Arguments + +| `firefox.navigate` +| Navigate to URL +| `[url, opts?]` + +| `firefox.click` +| Click element +| `[selector, opts?]` + +| `firefox.typeText` +| Type into element +| `[selector, text, opts?]` + +| `firefox.screenshot` +| Take screenshot +| `[opts?]` + +| `firefox.executeJs` +| Execute JavaScript +| `[javascript, opts?]` + +| `firefox.getContent` +| Get page content +| `[opts?]` + +| `firefox.detectBrowsers` +| Detect available browsers +| `[]` +|=== + +=== Example: VS Code Integration + +[source,json] +---- +{ + "command": "firefox.navigate", + "arguments": ["https://example.com", {"timeout": 5000}] +} +---- + +== Architecture + +=== Components + +* *Adapters* - Browser-specific implementations (Firefox, Chrome, etc.) +** `ClaudeFirefox.Adapters.Behaviour` - Adapter contract +** `ClaudeFirefox.Adapters.Firefox` - Marionette protocol implementation +* *LSP Server* - GenLSP-based server handling LSP protocol +* *Handlers* - Command execution and response formatting + +=== Protocol: Marionette + +Firefox's Marionette protocol communicates over TCP (port 2828 by default). + +Message format: + +---- +: +---- + +Example: + +---- +47:{"id": 1, "name": "WebDriver:Navigate", "parameters": {"url": "https://example.com"}} +---- + +See https://firefox-source-docs.mozilla.org/testing/marionette/Protocol.html[Marionette Protocol Documentation]. + +== Development + +=== Running Tests + +[source,bash] +---- +mix test +---- + +=== Code Quality + +[source,bash] +---- +# Format code +mix format + +# Run linter +mix credo --strict + +# Type checking +mix dialyzer +---- + +=== Building Release + +[source,bash] +---- +MIX_ENV=prod mix release +---- + +== Ecosystem Integration + +Related projects: + +* *poly-ssg-lsp* - LSP for static site generators +* *poly-queue-lsp* - LSP for message queue systems +* *poly-observability-lsp* - LSP for observability platforms + +== License + +SPDX-License-Identifier: PMPL-1.0-or-later + +Copyright (c) 2026 Jonathan D.A. Jewell + +Licensed under the Palimpsest License, version 1.0 or later. + +== Contributing + +See link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] for contribution guidelines. + +== Security + +See link:SECURITY.md[SECURITY.md] for security policy. diff --git a/firefox-lsp/USAGE.md b/firefox-lsp/USAGE.md index 56906ba..ea312e5 100644 --- a/firefox-lsp/USAGE.md +++ b/firefox-lsp/USAGE.md @@ -1,474 +1,474 @@ -# Usage Guide - -> Comprehensive guide for using claude-firefox-lsp across VSCode, Neovim, and Emacs - -## Table of Contents - -- [VSCode Setup](#vscode-setup) -- [Neovim Setup](#neovim-setup) -- [Emacs Setup](#emacs-setup) -- [Configuration](#configuration) -- [Commands](#commands) -- [Troubleshooting](#troubleshooting) -- [Adapter-Specific Notes](#adapter-specific-notes) - -## VSCode Setup - -### Installation - -1. **Install the LSP Server:** - ```bash - git clone https://github.com/hyperpolymath/claude-firefox-lsp.git - cd claude-firefox-lsp - ./install.sh - ``` - -2. **Install VSCode Extension:** - ```bash - cd vscode-extension - npm install - npm run compile - code --install-extension *.vsix - ``` - -### Features - -The VSCode extension provides: - -- **Firefox Automation**: Control Firefox via Marionette protocol -- **Browser Inspection**: View DOM, execute JavaScript -- **Navigation Control**: Navigate pages, manage tabs -- **Diagnostics**: Script errors, connection issues -- **Hover Documentation**: Element info, JavaScript API docs -- **Commands**: Execute browser actions directly from editor - -### Available Commands - -Access via Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`): - -- **Firefox: Connect** - Connect to Firefox instance -- **Firefox: Navigate** - Navigate to URL -- **Firefox: Execute Script** - Run JavaScript in page -- **Firefox: Get Element** - Find element by selector -- **Firefox: Take Screenshot** - Capture page screenshot -- **Firefox: Get Page Source** - View HTML source -- **Firefox: Close Tab** - Close active tab -- **Firefox: Disconnect** - Close connection - -### Settings - -Add to your workspace or user `settings.json`: - -```json -{ - "lsp.serverPath": "/path/to/claude-firefox-lsp", - "lsp.trace.server": "verbose", - "lsp.firefox.host": "localhost", - "lsp.firefox.port": 2828, - "lsp.firefox.autoConnect": false -} -``` - -## Neovim Setup - -### Using nvim-lspconfig - -Add to your Neovim configuration: - -```lua -local lspconfig = require('lspconfig') -local configs = require('lspconfig.configs') - --- Register claude-firefox-lsp if not already defined -if not configs.claude_firefox_lsp then - configs.claude_firefox_lsp = { - default_config = { - cmd = {'/path/to/claude-firefox-lsp/_build/prod/rel/claude_firefox_lsp/bin/claude_firefox_lsp'}, - filetypes = {'javascript', 'html', 'css'}, - root_dir = lspconfig.util.root_pattern('.git', 'package.json'), - settings = { - firefox = { - host = 'localhost', - port = 2828, - autoConnect = false - } - } - } - } -end - --- Setup the LSP -lspconfig.claude_firefox_lsp.setup({ - on_attach = function(client, bufnr) - local opts = { noremap=true, silent=true, buffer=bufnr } - - -- Custom commands - vim.api.nvim_buf_create_user_command(bufnr, 'FirefoxConnect', function() - vim.lsp.buf.execute_command({command = 'firefox.connect'}) - end, {}) - - vim.api.nvim_buf_create_user_command(bufnr, 'FirefoxNavigate', function(args) - vim.lsp.buf.execute_command({ - command = 'firefox.navigate', - arguments = {{url = args.args}} - }) - end, {nargs = 1}) - - vim.api.nvim_buf_create_user_command(bufnr, 'FirefoxExecute', function() - local script = vim.fn.input('Script: ') - vim.lsp.buf.execute_command({ - command = 'firefox.execute', - arguments = {{script = script}} - }) - end, {}) - end, - capabilities = require('cmp_nvim_lsp').default_capabilities() -}) -``` - -## Emacs Setup - -### Using lsp-mode - -Add to your Emacs configuration: - -```elisp -(use-package lsp-mode - :hook ((js-mode html-mode css-mode) . lsp) - :config - (lsp-register-client - (make-lsp-client - :new-connection (lsp-stdio-connection - '("/path/to/claude-firefox-lsp/_build/prod/rel/claude_firefox_lsp/bin/claude_firefox_lsp")) - :major-modes '(js-mode html-mode css-mode) - :server-id 'claude-firefox-lsp - :initialization-options (lambda () - '(:host "localhost" - :port 2828 - :autoConnect nil))))) - -;; Custom commands -(defun firefox-connect () - "Connect to Firefox." - (interactive) - (lsp-execute-command "firefox.connect")) - -(defun firefox-navigate (url) - "Navigate to URL." - (interactive "sURL: ") - (lsp-execute-command "firefox.navigate" (vector `(:url ,url)))) - -(defun firefox-execute (script) - "Execute JavaScript." - (interactive "sScript: ") - (lsp-execute-command "firefox.execute" (vector `(:script ,script)))) - -(define-key lsp-mode-map (kbd "C-c f c") 'firefox-connect) -(define-key lsp-mode-map (kbd "C-c f n") 'firefox-navigate) -(define-key lsp-mode-map (kbd "C-c f e") 'firefox-execute) -``` - -## Configuration - -### Server Configuration - -Create `.claude-firefox-lsp.json` in your project root: - -```json -{ - "firefox": { - "host": "localhost", - "port": 2828, - "autoConnect": false, - "timeout": 30000, - "enableDebugger": false - }, - "marionette": { - "enableWebDriver": true, - "acceptInsecureCerts": false, - "pageLoadStrategy": "normal" - } -} -``` - -### Environment Variables - -```bash -# Firefox Marionette -export FIREFOX_BIN=/usr/bin/firefox -export MARIONETTE_HOST=localhost -export MARIONETTE_PORT=2828 - -# Firefox profile -export FIREFOX_PROFILE=~/.mozilla/firefox/marionette.default -``` - -### Starting Firefox with Marionette - -```bash -# Enable remote debugging -firefox --marionette --start-debugger-server 2828 - -# Or with specific profile -firefox --marionette --profile ~/.mozilla/firefox/marionette.default -``` - -## Commands - -### LSP Commands - -#### firefox.connect -Connect to Firefox instance. - -**Parameters:** -- `host` (optional): Firefox host (default: localhost) -- `port` (optional): Marionette port (default: 2828) - -**Returns:** Connection status - -**Example (Neovim):** -```lua -vim.lsp.buf.execute_command({ - command = 'firefox.connect', - arguments = {{host = 'localhost', port = 2828}} -}) -``` - -#### firefox.navigate -Navigate to URL. - -**Parameters:** -- `url`: Target URL - -**Returns:** Navigation status - -**Example (Neovim):** -```lua -vim.lsp.buf.execute_command({ - command = 'firefox.navigate', - arguments = {{url = 'https://example.com'}} -}) -``` - -#### firefox.execute -Execute JavaScript in page context. - -**Parameters:** -- `script`: JavaScript code -- `args` (optional): Script arguments - -**Returns:** Script result - -**Example (Neovim):** -```lua -vim.lsp.buf.execute_command({ - command = 'firefox.execute', - arguments = {{ - script = 'return document.title;' - }} -}) -``` - -#### firefox.getElement -Find element by CSS selector. - -**Parameters:** -- `selector`: CSS selector - -**Returns:** Element reference - -#### firefox.getPageSource -Get page HTML source. - -**Parameters:** None - -**Returns:** HTML source code - -#### firefox.takeScreenshot -Capture page screenshot. - -**Parameters:** -- `fullPage` (optional): Capture full page (default: false) - -**Returns:** Base64-encoded PNG image - -#### firefox.closeTab -Close active tab. - -**Parameters:** None - -**Returns:** Close status - -#### firefox.disconnect -Close connection to Firefox. - -**Parameters:** None - -**Returns:** Disconnect status - -## Troubleshooting - -### Connection Refused - -**Symptoms:** "Connection refused" or "Unable to connect" error. - -**Solutions:** - -1. **Verify Firefox is running with Marionette:** - ```bash - ps aux | grep firefox - # Should show --marionette flag - ``` - -2. **Start Firefox with Marionette:** - ```bash - firefox --marionette --start-debugger-server 2828 - ``` - -3. **Check port is listening:** - ```bash - netstat -an | grep 2828 - # or - lsof -i :2828 - ``` - -4. **Test connection manually:** - ```bash - telnet localhost 2828 - ``` - -### Script Execution Errors - -**Symptoms:** JavaScript execution fails. - -**Solutions:** - -1. **Check script syntax:** - - Ensure valid JavaScript - - Use `return` statement for values - -2. **Verify page context:** - ```lua - -- Get current URL first - vim.lsp.buf.execute_command({ - command = 'firefox.execute', - arguments = {{script = 'return window.location.href;'}} - }) - ``` - -3. **Check for CSP restrictions:** - - Content Security Policy may block script execution - - Check browser console for errors - -### Timeout Errors - -**Symptoms:** "Operation timed out" error. - -**Solutions:** - -1. **Increase timeout:** - ```json - {"firefox": {"timeout": 60000}} - ``` - -2. **Check page load time:** - - Some pages may take longer to load - - Use explicit waits for elements - -## Adapter-Specific Notes - -### Firefox Marionette - -**Detection:** Firefox running with `--marionette` flag on port 2828 - -**Features:** -- WebDriver-compatible protocol -- JavaScript execution -- Element interaction -- Screenshot capture -- Cookie management -- Window/tab management - -**Configuration:** -```json -{ - "adapters": { - "marionette": { - "host": "localhost", - "port": 2828, - "enableWebDriver": true, - "acceptInsecureCerts": false, - "pageLoadStrategy": "normal", - "strictFileInteractability": false - } - } -} -``` - -**Marionette Protocol Capabilities:** -- `acceptInsecureCerts`: Accept self-signed certificates -- `pageLoadStrategy`: "normal", "eager", or "none" -- `strictFileInteractability`: Strict element interaction checks -- `unhandledPromptBehavior`: How to handle alerts ("dismiss", "accept") - -**Known Issues:** -- Headless mode has limited screenshot capabilities -- Some Firefox-specific features not available via WebDriver -- Private browsing mode may have restrictions - -**Firefox Preferences for Testing:** - -Create a custom profile with these preferences in `prefs.js`: - -```javascript -user_pref("marionette.port", 2828); -user_pref("devtools.debugger.remote-enabled", true); -user_pref("devtools.chrome.enabled", true); -``` - -**Automation Best Practices:** - -1. **Use explicit waits:** - ```lua - -- Wait for element to appear - vim.lsp.buf.execute_command({ - command = 'firefox.execute', - arguments = {{ - script = [[ - return new Promise((resolve) => { - const interval = setInterval(() => { - const el = document.querySelector('#my-element'); - if (el) { - clearInterval(interval); - resolve(el.textContent); - } - }, 100); - }); - ]] - }} - }) - ``` - -2. **Handle navigation timing:** - ```lua - -- Navigate and wait for load - vim.lsp.buf.execute_command({command = 'firefox.navigate', arguments = {{url = 'https://example.com'}}}) - vim.wait(2000) -- Wait 2 seconds for page load - ``` - -3. **Clean up connections:** - ```lua - -- Always disconnect when done - vim.lsp.buf.execute_command({command = 'firefox.disconnect'}) - ``` - -## Additional Resources - -- **GitHub Repository:** https://github.com/hyperpolymath/claude-firefox-lsp -- **Issue Tracker:** https://github.com/hyperpolymath/claude-firefox-lsp/issues -- **Examples:** See `examples/` directory for sample configurations -- **Marionette Protocol:** https://firefox-source-docs.mozilla.org/testing/marionette/ - -## License - -PMPL-1.0-or-later +# Usage Guide + +> Comprehensive guide for using claude-firefox-lsp across VSCode, Neovim, and Emacs + +## Table of Contents + +- [VSCode Setup](#vscode-setup) +- [Neovim Setup](#neovim-setup) +- [Emacs Setup](#emacs-setup) +- [Configuration](#configuration) +- [Commands](#commands) +- [Troubleshooting](#troubleshooting) +- [Adapter-Specific Notes](#adapter-specific-notes) + +## VSCode Setup + +### Installation + +1. **Install the LSP Server:** + ```bash + git clone https://github.com/hyperpolymath/claude-firefox-lsp.git + cd claude-firefox-lsp + ./install.sh + ``` + +2. **Install VSCode Extension:** + ```bash + cd vscode-extension + npm install + npm run compile + code --install-extension *.vsix + ``` + +### Features + +The VSCode extension provides: + +- **Firefox Automation**: Control Firefox via Marionette protocol +- **Browser Inspection**: View DOM, execute JavaScript +- **Navigation Control**: Navigate pages, manage tabs +- **Diagnostics**: Script errors, connection issues +- **Hover Documentation**: Element info, JavaScript API docs +- **Commands**: Execute browser actions directly from editor + +### Available Commands + +Access via Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`): + +- **Firefox: Connect** - Connect to Firefox instance +- **Firefox: Navigate** - Navigate to URL +- **Firefox: Execute Script** - Run JavaScript in page +- **Firefox: Get Element** - Find element by selector +- **Firefox: Take Screenshot** - Capture page screenshot +- **Firefox: Get Page Source** - View HTML source +- **Firefox: Close Tab** - Close active tab +- **Firefox: Disconnect** - Close connection + +### Settings + +Add to your workspace or user `settings.json`: + +```json +{ + "lsp.serverPath": "/path/to/claude-firefox-lsp", + "lsp.trace.server": "verbose", + "lsp.firefox.host": "localhost", + "lsp.firefox.port": 2828, + "lsp.firefox.autoConnect": false +} +``` + +## Neovim Setup + +### Using nvim-lspconfig + +Add to your Neovim configuration: + +```lua +local lspconfig = require('lspconfig') +local configs = require('lspconfig.configs') + +-- Register claude-firefox-lsp if not already defined +if not configs.claude_firefox_lsp then + configs.claude_firefox_lsp = { + default_config = { + cmd = {'/path/to/claude-firefox-lsp/_build/prod/rel/claude_firefox_lsp/bin/claude_firefox_lsp'}, + filetypes = {'javascript', 'html', 'css'}, + root_dir = lspconfig.util.root_pattern('.git', 'package.json'), + settings = { + firefox = { + host = 'localhost', + port = 2828, + autoConnect = false + } + } + } + } +end + +-- Setup the LSP +lspconfig.claude_firefox_lsp.setup({ + on_attach = function(client, bufnr) + local opts = { noremap=true, silent=true, buffer=bufnr } + + -- Custom commands + vim.api.nvim_buf_create_user_command(bufnr, 'FirefoxConnect', function() + vim.lsp.buf.execute_command({command = 'firefox.connect'}) + end, {}) + + vim.api.nvim_buf_create_user_command(bufnr, 'FirefoxNavigate', function(args) + vim.lsp.buf.execute_command({ + command = 'firefox.navigate', + arguments = {{url = args.args}} + }) + end, {nargs = 1}) + + vim.api.nvim_buf_create_user_command(bufnr, 'FirefoxExecute', function() + local script = vim.fn.input('Script: ') + vim.lsp.buf.execute_command({ + command = 'firefox.execute', + arguments = {{script = script}} + }) + end, {}) + end, + capabilities = require('cmp_nvim_lsp').default_capabilities() +}) +``` + +## Emacs Setup + +### Using lsp-mode + +Add to your Emacs configuration: + +```elisp +(use-package lsp-mode + :hook ((js-mode html-mode css-mode) . lsp) + :config + (lsp-register-client + (make-lsp-client + :new-connection (lsp-stdio-connection + '("/path/to/claude-firefox-lsp/_build/prod/rel/claude_firefox_lsp/bin/claude_firefox_lsp")) + :major-modes '(js-mode html-mode css-mode) + :server-id 'claude-firefox-lsp + :initialization-options (lambda () + '(:host "localhost" + :port 2828 + :autoConnect nil))))) + +;; Custom commands +(defun firefox-connect () + "Connect to Firefox." + (interactive) + (lsp-execute-command "firefox.connect")) + +(defun firefox-navigate (url) + "Navigate to URL." + (interactive "sURL: ") + (lsp-execute-command "firefox.navigate" (vector `(:url ,url)))) + +(defun firefox-execute (script) + "Execute JavaScript." + (interactive "sScript: ") + (lsp-execute-command "firefox.execute" (vector `(:script ,script)))) + +(define-key lsp-mode-map (kbd "C-c f c") 'firefox-connect) +(define-key lsp-mode-map (kbd "C-c f n") 'firefox-navigate) +(define-key lsp-mode-map (kbd "C-c f e") 'firefox-execute) +``` + +## Configuration + +### Server Configuration + +Create `.claude-firefox-lsp.json` in your project root: + +```json +{ + "firefox": { + "host": "localhost", + "port": 2828, + "autoConnect": false, + "timeout": 30000, + "enableDebugger": false + }, + "marionette": { + "enableWebDriver": true, + "acceptInsecureCerts": false, + "pageLoadStrategy": "normal" + } +} +``` + +### Environment Variables + +```bash +# Firefox Marionette +export FIREFOX_BIN=/usr/bin/firefox +export MARIONETTE_HOST=localhost +export MARIONETTE_PORT=2828 + +# Firefox profile +export FIREFOX_PROFILE=~/.mozilla/firefox/marionette.default +``` + +### Starting Firefox with Marionette + +```bash +# Enable remote debugging +firefox --marionette --start-debugger-server 2828 + +# Or with specific profile +firefox --marionette --profile ~/.mozilla/firefox/marionette.default +``` + +## Commands + +### LSP Commands + +#### firefox.connect +Connect to Firefox instance. + +**Parameters:** +- `host` (optional): Firefox host (default: localhost) +- `port` (optional): Marionette port (default: 2828) + +**Returns:** Connection status + +**Example (Neovim):** +```lua +vim.lsp.buf.execute_command({ + command = 'firefox.connect', + arguments = {{host = 'localhost', port = 2828}} +}) +``` + +#### firefox.navigate +Navigate to URL. + +**Parameters:** +- `url`: Target URL + +**Returns:** Navigation status + +**Example (Neovim):** +```lua +vim.lsp.buf.execute_command({ + command = 'firefox.navigate', + arguments = {{url = 'https://example.com'}} +}) +``` + +#### firefox.execute +Execute JavaScript in page context. + +**Parameters:** +- `script`: JavaScript code +- `args` (optional): Script arguments + +**Returns:** Script result + +**Example (Neovim):** +```lua +vim.lsp.buf.execute_command({ + command = 'firefox.execute', + arguments = {{ + script = 'return document.title;' + }} +}) +``` + +#### firefox.getElement +Find element by CSS selector. + +**Parameters:** +- `selector`: CSS selector + +**Returns:** Element reference + +#### firefox.getPageSource +Get page HTML source. + +**Parameters:** None + +**Returns:** HTML source code + +#### firefox.takeScreenshot +Capture page screenshot. + +**Parameters:** +- `fullPage` (optional): Capture full page (default: false) + +**Returns:** Base64-encoded PNG image + +#### firefox.closeTab +Close active tab. + +**Parameters:** None + +**Returns:** Close status + +#### firefox.disconnect +Close connection to Firefox. + +**Parameters:** None + +**Returns:** Disconnect status + +## Troubleshooting + +### Connection Refused + +**Symptoms:** "Connection refused" or "Unable to connect" error. + +**Solutions:** + +1. **Verify Firefox is running with Marionette:** + ```bash + ps aux | grep firefox + # Should show --marionette flag + ``` + +2. **Start Firefox with Marionette:** + ```bash + firefox --marionette --start-debugger-server 2828 + ``` + +3. **Check port is listening:** + ```bash + netstat -an | grep 2828 + # or + lsof -i :2828 + ``` + +4. **Test connection manually:** + ```bash + telnet localhost 2828 + ``` + +### Script Execution Errors + +**Symptoms:** JavaScript execution fails. + +**Solutions:** + +1. **Check script syntax:** + - Ensure valid JavaScript + - Use `return` statement for values + +2. **Verify page context:** + ```lua + -- Get current URL first + vim.lsp.buf.execute_command({ + command = 'firefox.execute', + arguments = {{script = 'return window.location.href;'}} + }) + ``` + +3. **Check for CSP restrictions:** + - Content Security Policy may block script execution + - Check browser console for errors + +### Timeout Errors + +**Symptoms:** "Operation timed out" error. + +**Solutions:** + +1. **Increase timeout:** + ```json + {"firefox": {"timeout": 60000}} + ``` + +2. **Check page load time:** + - Some pages may take longer to load + - Use explicit waits for elements + +## Adapter-Specific Notes + +### Firefox Marionette + +**Detection:** Firefox running with `--marionette` flag on port 2828 + +**Features:** +- WebDriver-compatible protocol +- JavaScript execution +- Element interaction +- Screenshot capture +- Cookie management +- Window/tab management + +**Configuration:** +```json +{ + "adapters": { + "marionette": { + "host": "localhost", + "port": 2828, + "enableWebDriver": true, + "acceptInsecureCerts": false, + "pageLoadStrategy": "normal", + "strictFileInteractability": false + } + } +} +``` + +**Marionette Protocol Capabilities:** +- `acceptInsecureCerts`: Accept self-signed certificates +- `pageLoadStrategy`: "normal", "eager", or "none" +- `strictFileInteractability`: Strict element interaction checks +- `unhandledPromptBehavior`: How to handle alerts ("dismiss", "accept") + +**Known Issues:** +- Headless mode has limited screenshot capabilities +- Some Firefox-specific features not available via WebDriver +- Private browsing mode may have restrictions + +**Firefox Preferences for Testing:** + +Create a custom profile with these preferences in `prefs.js`: + +```javascript +user_pref("marionette.port", 2828); +user_pref("devtools.debugger.remote-enabled", true); +user_pref("devtools.chrome.enabled", true); +``` + +**Automation Best Practices:** + +1. **Use explicit waits:** + ```lua + -- Wait for element to appear + vim.lsp.buf.execute_command({ + command = 'firefox.execute', + arguments = {{ + script = [[ + return new Promise((resolve) => { + const interval = setInterval(() => { + const el = document.querySelector('#my-element'); + if (el) { + clearInterval(interval); + resolve(el.textContent); + } + }, 100); + }); + ]] + }} + }) + ``` + +2. **Handle navigation timing:** + ```lua + -- Navigate and wait for load + vim.lsp.buf.execute_command({command = 'firefox.navigate', arguments = {{url = 'https://example.com'}}}) + vim.wait(2000) -- Wait 2 seconds for page load + ``` + +3. **Clean up connections:** + ```lua + -- Always disconnect when done + vim.lsp.buf.execute_command({command = 'firefox.disconnect'}) + ``` + +## Additional Resources + +- **GitHub Repository:** https://github.com/hyperpolymath/claude-firefox-lsp +- **Issue Tracker:** https://github.com/hyperpolymath/claude-firefox-lsp/issues +- **Examples:** See `examples/` directory for sample configurations +- **Marionette Protocol:** https://firefox-source-docs.mozilla.org/testing/marionette/ + +## License + +PMPL-1.0-or-later diff --git a/firefox-lsp/examples/vscode-settings.json b/firefox-lsp/examples/vscode-settings.json index 3ce02fc..a95263e 100644 --- a/firefox-lsp/examples/vscode-settings.json +++ b/firefox-lsp/examples/vscode-settings.json @@ -1,13 +1,13 @@ -{ - // Example VSCode settings for this LSP server - "editor.quickSuggestions": true, - "editor.suggestOnTriggerCharacters": true, - "editor.acceptSuggestionOnEnter": "on", - "editor.formatOnSave": true, - "files.watcherExclude": { - "**/.git/**": true, - "**/node_modules/**": true, - "**/_build/**": true, - "**/deps/**": true - } -} +{ + // Example VSCode settings for this LSP server + "editor.quickSuggestions": true, + "editor.suggestOnTriggerCharacters": true, + "editor.acceptSuggestionOnEnter": "on", + "editor.formatOnSave": true, + "files.watcherExclude": { + "**/.git/**": true, + "**/node_modules/**": true, + "**/_build/**": true, + "**/deps/**": true + } +} diff --git a/firefox-lsp/install.sh b/firefox-lsp/install.sh index 303572e..ee4c7fc 100755 --- a/firefox-lsp/install.sh +++ b/firefox-lsp/install.sh @@ -1,22 +1,22 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: PMPL-1.0-or-later -# Quick installation script - -set -e - -echo "Installing LSP server dependencies..." -mix deps.get -mix compile - -echo "Building VSCode extension..." -cd vscode-extension -npm install -npm run compile - -echo "✓ Installation complete!" -echo "" -echo "To install VSCode extension:" -echo " 1. Open VSCode" -echo " 2. Press Ctrl+Shift+P (Cmd+Shift+P on Mac)" -echo " 3. Type 'Extensions: Install from VSIX'" -echo " 4. Select the .vsix file from vscode-extension/" +#!/usr/bin/env bash +# SPDX-License-Identifier: PMPL-1.0-or-later +# Quick installation script + +set -e + +echo "Installing LSP server dependencies..." +mix deps.get +mix compile + +echo "Building VSCode extension..." +cd vscode-extension +npm install +npm run compile + +echo "✓ Installation complete!" +echo "" +echo "To install VSCode extension:" +echo " 1. Open VSCode" +echo " 2. Press Ctrl+Shift+P (Cmd+Shift+P on Mac)" +echo " 3. Type 'Extensions: Install from VSIX'" +echo " 4. Select the .vsix file from vscode-extension/" diff --git a/firefox-lsp/lib/adapters/behaviour.ex b/firefox-lsp/lib/adapters/behaviour.ex index abacb73..e0e6487 100644 --- a/firefox-lsp/lib/adapters/behaviour.ex +++ b/firefox-lsp/lib/adapters/behaviour.ex @@ -1,113 +1,113 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell - -defmodule ClaudeFirefox.Adapters.Behaviour do - @moduledoc """ - Behaviour defining the contract for browser adapters. - - Each adapter implements this behaviour to provide a consistent interface - for detecting, controlling, and automating browsers. - - ## Example - - defmodule ClaudeFirefox.Adapters.Firefox do - use GenServer - @behaviour ClaudeFirefox.Adapters.Behaviour - - @impl true - def detect() do - case System.find_executable("firefox") do - nil -> {:ok, false} - _path -> {:ok, true} - end - end - - @impl true - def navigate(url, opts) do - # Send Marionette protocol command - end - end - """ - - @type url :: String.t() - @type selector :: String.t() - @type javascript :: String.t() - @type opts :: keyword() - @type result :: {:ok, term()} | {:error, String.t()} - - @doc """ - Detect if this browser is installed and available. - - Returns `{:ok, true}` if the browser is detected, `{:ok, false}` otherwise. - """ - @callback detect() :: {:ok, boolean()} | {:error, String.t()} - - @doc """ - Navigate to a URL. - - ## Options - - - `:timeout` - Maximum time to wait for page load (milliseconds) - - `:wait_for` - Selector or condition to wait for before returning - """ - @callback navigate(url, opts) :: result - - @doc """ - Click an element specified by selector. - - ## Options - - - `:timeout` - Maximum time to wait for element (milliseconds) - - `:button` - Mouse button (`:left`, `:right`, `:middle`) - """ - @callback click(selector, opts) :: result - - @doc """ - Type text into an element specified by selector. - - ## Options - - - `:timeout` - Maximum time to wait for element (milliseconds) - - `:clear` - Clear existing text before typing (boolean) - """ - @callback type_text(selector, text :: String.t(), opts) :: result - - @doc """ - Take a screenshot of the current page or element. - - ## Options - - - `:selector` - Element selector (captures full page if omitted) - - `:format` - Image format (`:png`, `:jpeg`) - - `:quality` - JPEG quality (1-100) - - Returns `{:ok, binary()}` with image data. - """ - @callback screenshot(opts) :: {:ok, binary()} | {:error, String.t()} - - @doc """ - Execute JavaScript in the page context. - - Returns `{:ok, result}` where result is the JS return value (serialized as JSON). - """ - @callback execute_js(javascript, opts) :: result - - @doc """ - Get page content (HTML, text, or DOM representation). - - ## Options - - - `:format` - Content format (`:html`, `:text`, `:dom`) - """ - @callback get_content(opts) :: {:ok, String.t()} | {:error, String.t()} - - @doc """ - Get browser metadata (name, version, capabilities). - """ - @callback metadata() :: %{ - name: String.t(), - version: String.t() | nil, - protocol: String.t(), - capabilities: [atom()] - } -end +# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell + +defmodule ClaudeFirefox.Adapters.Behaviour do + @moduledoc """ + Behaviour defining the contract for browser adapters. + + Each adapter implements this behaviour to provide a consistent interface + for detecting, controlling, and automating browsers. + + ## Example + + defmodule ClaudeFirefox.Adapters.Firefox do + use GenServer + @behaviour ClaudeFirefox.Adapters.Behaviour + + @impl true + def detect() do + case System.find_executable("firefox") do + nil -> {:ok, false} + _path -> {:ok, true} + end + end + + @impl true + def navigate(url, opts) do + # Send Marionette protocol command + end + end + """ + + @type url :: String.t() + @type selector :: String.t() + @type javascript :: String.t() + @type opts :: keyword() + @type result :: {:ok, term()} | {:error, String.t()} + + @doc """ + Detect if this browser is installed and available. + + Returns `{:ok, true}` if the browser is detected, `{:ok, false}` otherwise. + """ + @callback detect() :: {:ok, boolean()} | {:error, String.t()} + + @doc """ + Navigate to a URL. + + ## Options + + - `:timeout` - Maximum time to wait for page load (milliseconds) + - `:wait_for` - Selector or condition to wait for before returning + """ + @callback navigate(url, opts) :: result + + @doc """ + Click an element specified by selector. + + ## Options + + - `:timeout` - Maximum time to wait for element (milliseconds) + - `:button` - Mouse button (`:left`, `:right`, `:middle`) + """ + @callback click(selector, opts) :: result + + @doc """ + Type text into an element specified by selector. + + ## Options + + - `:timeout` - Maximum time to wait for element (milliseconds) + - `:clear` - Clear existing text before typing (boolean) + """ + @callback type_text(selector, text :: String.t(), opts) :: result + + @doc """ + Take a screenshot of the current page or element. + + ## Options + + - `:selector` - Element selector (captures full page if omitted) + - `:format` - Image format (`:png`, `:jpeg`) + - `:quality` - JPEG quality (1-100) + + Returns `{:ok, binary()}` with image data. + """ + @callback screenshot(opts) :: {:ok, binary()} | {:error, String.t()} + + @doc """ + Execute JavaScript in the page context. + + Returns `{:ok, result}` where result is the JS return value (serialized as JSON). + """ + @callback execute_js(javascript, opts) :: result + + @doc """ + Get page content (HTML, text, or DOM representation). + + ## Options + + - `:format` - Content format (`:html`, `:text`, `:dom`) + """ + @callback get_content(opts) :: {:ok, String.t()} | {:error, String.t()} + + @doc """ + Get browser metadata (name, version, capabilities). + """ + @callback metadata() :: %{ + name: String.t(), + version: String.t() | nil, + protocol: String.t(), + capabilities: [atom()] + } +end diff --git a/firefox-lsp/lib/adapters/firefox.ex b/firefox-lsp/lib/adapters/firefox.ex index a2b6839..9c70ff1 100644 --- a/firefox-lsp/lib/adapters/firefox.ex +++ b/firefox-lsp/lib/adapters/firefox.ex @@ -1,365 +1,365 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell - -defmodule ClaudeFirefox.Adapters.Firefox do - @moduledoc """ - Firefox browser adapter using the Marionette protocol. - - Marionette is Firefox's native automation protocol, similar to Chrome DevTools Protocol. - It communicates over a TCP socket (default port 2828) when Firefox is started with - the `-marionette` flag. - - ## Setup - - Start Firefox with Marionette enabled: - - firefox -marionette -headless - - Or configure via about:config: - - Set `marionette.enabled` to true - - Set `marionette.port` to 2828 (default) - - ## Protocol - - Marionette uses a JSON-based protocol over TCP. Messages are formatted as: - - Length (bytes) : JSON payload - - Reference: https://firefox-source-docs.mozilla.org/testing/marionette/Protocol.html - """ - - use GenServer - require Logger - - @behaviour ClaudeFirefox.Adapters.Behaviour - - @default_host "127.0.0.1" - @default_port 2828 - @default_timeout 5000 - - # Client API - - def start_link(opts \\ []) do - GenServer.start_link(__MODULE__, opts, name: __MODULE__) - end - - @impl ClaudeFirefox.Adapters.Behaviour - def detect do - case System.find_executable("firefox") do - nil -> - {:ok, false} - - path -> - Logger.debug("Found Firefox at #{path}") - {:ok, true} - end - end - - @impl ClaudeFirefox.Adapters.Behaviour - def navigate(url, opts \\ []) do - GenServer.call(__MODULE__, {:navigate, url, opts}, timeout(opts)) - end - - @impl ClaudeFirefox.Adapters.Behaviour - def click(selector, opts \\ []) do - GenServer.call(__MODULE__, {:click, selector, opts}, timeout(opts)) - end - - @impl ClaudeFirefox.Adapters.Behaviour - def type_text(selector, text, opts \\ []) do - GenServer.call(__MODULE__, {:type_text, selector, text, opts}, timeout(opts)) - end - - @impl ClaudeFirefox.Adapters.Behaviour - def screenshot(opts \\ []) do - GenServer.call(__MODULE__, {:screenshot, opts}, timeout(opts)) - end - - @impl ClaudeFirefox.Adapters.Behaviour - def execute_js(javascript, opts \\ []) do - GenServer.call(__MODULE__, {:execute_js, javascript, opts}, timeout(opts)) - end - - @impl ClaudeFirefox.Adapters.Behaviour - def get_content(opts \\ []) do - GenServer.call(__MODULE__, {:get_content, opts}, timeout(opts)) - end - - @impl ClaudeFirefox.Adapters.Behaviour - def metadata do - %{ - name: "Firefox", - version: get_version(), - protocol: "Marionette", - capabilities: [ - :navigate, - :click, - :type_text, - :screenshot, - :execute_js, - :get_content, - :find_element, - :cookies, - :window_management - ] - } - end - - # Server callbacks - - @impl true - def init(opts) do - host = Keyword.get(opts, :host, @default_host) - port = Keyword.get(opts, :port, @default_port) - - state = %{ - host: host, - port: port, - socket: nil, - session_id: nil, - message_id: 0 - } - - {:ok, state, {:continue, :connect}} - end - - @impl true - def handle_continue(:connect, state) do - case connect(state) do - {:ok, new_state} -> - {:noreply, new_state} - - {:error, reason} -> - Logger.warning("Failed to connect to Firefox: #{inspect(reason)}") - Logger.info("Please start Firefox with: firefox -marionette -headless") - {:noreply, state} - end - end - - @impl true - def handle_call({:navigate, url, _opts}, _from, state) do - case send_command(state, "WebDriver:Navigate", %{url: url}) do - {:ok, _response, new_state} -> - {:reply, {:ok, %{url: url}}, new_state} - - {:error, reason, new_state} -> - {:reply, {:error, reason}, new_state} - end - end - - @impl true - def handle_call({:click, selector, opts}, _from, state) do - with {:ok, element, state} <- find_element(state, selector, opts), - {:ok, _response, state} <- send_command(state, "WebDriver:ElementClick", %{id: element}) do - {:reply, {:ok, %{clicked: true}}, state} - else - {:error, reason, state} -> {:reply, {:error, reason}, state} - end - end - - @impl true - def handle_call({:type_text, selector, text, opts}, _from, state) do - clear = Keyword.get(opts, :clear, false) - - with {:ok, element, state} <- find_element(state, selector, opts), - {:ok, state} <- maybe_clear_element(state, element, clear), - {:ok, _response, state} <- - send_command(state, "WebDriver:ElementSendKeys", %{id: element, text: text}) do - {:reply, {:ok, %{typed: true}}, state} - else - {:error, reason, state} -> {:reply, {:error, reason}, state} - end - end - - @impl true - def handle_call({:screenshot, opts}, _from, state) do - selector = Keyword.get(opts, :selector) - - params = - if selector do - with {:ok, element, state} <- find_element(state, selector, opts) do - %{id: element, full: false} - else - _ -> %{full: true} - end - else - %{full: true} - end - - case send_command(state, "WebDriver:TakeScreenshot", params) do - {:ok, %{"value" => base64_data}, new_state} -> - binary_data = Base.decode64!(base64_data) - {:reply, {:ok, binary_data}, new_state} - - {:error, reason, new_state} -> - {:reply, {:error, reason}, new_state} - end - end - - @impl true - def handle_call({:execute_js, javascript, _opts}, _from, state) do - params = %{ - script: javascript, - args: [] - } - - case send_command(state, "WebDriver:ExecuteScript", params) do - {:ok, %{"value" => result}, new_state} -> - {:reply, {:ok, result}, new_state} - - {:error, reason, new_state} -> - {:reply, {:error, reason}, new_state} - end - end - - @impl true - def handle_call({:get_content, opts}, _from, state) do - format = Keyword.get(opts, :format, :html) - - script = - case format do - :html -> "return document.documentElement.outerHTML;" - :text -> "return document.body.innerText;" - :dom -> "return document.documentElement.innerHTML;" - end - - case send_command(state, "WebDriver:ExecuteScript", %{script: script, args: []}) do - {:ok, %{"value" => content}, new_state} -> - {:reply, {:ok, content}, new_state} - - {:error, reason, new_state} -> - {:reply, {:error, reason}, new_state} - end - end - - # Private functions - - defp connect(state) do - case :gen_tcp.connect( - String.to_charlist(state.host), - state.port, - [:binary, active: false, packet: :raw], - @default_timeout - ) do - {:ok, socket} -> - # Read initial handshake - case read_message(socket) do - {:ok, _handshake} -> - # Start a new session - case send_command(%{state | socket: socket}, "WebDriver:NewSession", %{}) do - {:ok, %{"sessionId" => session_id}, new_state} -> - Logger.info("Connected to Firefox Marionette (session: #{session_id})") - {:ok, %{new_state | session_id: session_id}} - - {:error, reason, _state} -> - {:error, reason} - end - - {:error, reason} -> - {:error, reason} - end - - {:error, reason} -> - {:error, reason} - end - end - - defp send_command(state, command, params) do - if state.socket == nil do - {:error, "Not connected to Firefox", state} - else - message_id = state.message_id + 1 - - message = %{ - "id" => message_id, - "name" => command, - "parameters" => params - } - - payload = Jason.encode!(message) - packet = "#{byte_size(payload)}:#{payload}" - - case :gen_tcp.send(state.socket, packet) do - :ok -> - case read_message(state.socket) do - {:ok, response} -> - {:ok, response, %{state | message_id: message_id}} - - {:error, reason} -> - {:error, reason, state} - end - - {:error, reason} -> - {:error, inspect(reason), state} - end - end - end - - defp read_message(socket) do - case :gen_tcp.recv(socket, 0, @default_timeout) do - {:ok, data} -> - # Parse "length:payload" format - case String.split(data, ":", parts: 2) do - [_length, json] -> - case Jason.decode(json) do - {:ok, parsed} -> {:ok, parsed} - {:error, _} -> {:error, "Invalid JSON response"} - end - - _ -> - {:error, "Invalid message format"} - end - - {:error, reason} -> - {:error, inspect(reason)} - end - end - - defp find_element(state, selector, opts) do - strategy = Keyword.get(opts, :strategy, :css) - - strategy_name = - case strategy do - :css -> "css selector" - :xpath -> "xpath" - :id -> "id" - :name -> "name" - :tag -> "tag name" - end - - params = %{ - using: strategy_name, - value: selector - } - - case send_command(state, "WebDriver:FindElement", params) do - {:ok, %{"value" => element}, new_state} -> - {:ok, element, new_state} - - {:error, reason, new_state} -> - {:error, reason, new_state} - end - end - - defp maybe_clear_element(state, element, false), do: {:ok, state} - - defp maybe_clear_element(state, element, true) do - case send_command(state, "WebDriver:ElementClear", %{id: element}) do - {:ok, _response, new_state} -> {:ok, new_state} - {:error, reason, new_state} -> {:error, reason, new_state} - end - end - - defp get_version do - case System.cmd("firefox", ["--version"], stderr_to_stdout: true) do - {output, 0} -> - output - |> String.trim() - |> String.replace("Mozilla Firefox ", "") - - _ -> - nil - end - end - - defp timeout(opts), do: Keyword.get(opts, :timeout, @default_timeout) -end +# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell + +defmodule ClaudeFirefox.Adapters.Firefox do + @moduledoc """ + Firefox browser adapter using the Marionette protocol. + + Marionette is Firefox's native automation protocol, similar to Chrome DevTools Protocol. + It communicates over a TCP socket (default port 2828) when Firefox is started with + the `-marionette` flag. + + ## Setup + + Start Firefox with Marionette enabled: + + firefox -marionette -headless + + Or configure via about:config: + - Set `marionette.enabled` to true + - Set `marionette.port` to 2828 (default) + + ## Protocol + + Marionette uses a JSON-based protocol over TCP. Messages are formatted as: + - Length (bytes) : JSON payload + + Reference: https://firefox-source-docs.mozilla.org/testing/marionette/Protocol.html + """ + + use GenServer + require Logger + + @behaviour ClaudeFirefox.Adapters.Behaviour + + @default_host "127.0.0.1" + @default_port 2828 + @default_timeout 5000 + + # Client API + + def start_link(opts \\ []) do + GenServer.start_link(__MODULE__, opts, name: __MODULE__) + end + + @impl ClaudeFirefox.Adapters.Behaviour + def detect do + case System.find_executable("firefox") do + nil -> + {:ok, false} + + path -> + Logger.debug("Found Firefox at #{path}") + {:ok, true} + end + end + + @impl ClaudeFirefox.Adapters.Behaviour + def navigate(url, opts \\ []) do + GenServer.call(__MODULE__, {:navigate, url, opts}, timeout(opts)) + end + + @impl ClaudeFirefox.Adapters.Behaviour + def click(selector, opts \\ []) do + GenServer.call(__MODULE__, {:click, selector, opts}, timeout(opts)) + end + + @impl ClaudeFirefox.Adapters.Behaviour + def type_text(selector, text, opts \\ []) do + GenServer.call(__MODULE__, {:type_text, selector, text, opts}, timeout(opts)) + end + + @impl ClaudeFirefox.Adapters.Behaviour + def screenshot(opts \\ []) do + GenServer.call(__MODULE__, {:screenshot, opts}, timeout(opts)) + end + + @impl ClaudeFirefox.Adapters.Behaviour + def execute_js(javascript, opts \\ []) do + GenServer.call(__MODULE__, {:execute_js, javascript, opts}, timeout(opts)) + end + + @impl ClaudeFirefox.Adapters.Behaviour + def get_content(opts \\ []) do + GenServer.call(__MODULE__, {:get_content, opts}, timeout(opts)) + end + + @impl ClaudeFirefox.Adapters.Behaviour + def metadata do + %{ + name: "Firefox", + version: get_version(), + protocol: "Marionette", + capabilities: [ + :navigate, + :click, + :type_text, + :screenshot, + :execute_js, + :get_content, + :find_element, + :cookies, + :window_management + ] + } + end + + # Server callbacks + + @impl true + def init(opts) do + host = Keyword.get(opts, :host, @default_host) + port = Keyword.get(opts, :port, @default_port) + + state = %{ + host: host, + port: port, + socket: nil, + session_id: nil, + message_id: 0 + } + + {:ok, state, {:continue, :connect}} + end + + @impl true + def handle_continue(:connect, state) do + case connect(state) do + {:ok, new_state} -> + {:noreply, new_state} + + {:error, reason} -> + Logger.warning("Failed to connect to Firefox: #{inspect(reason)}") + Logger.info("Please start Firefox with: firefox -marionette -headless") + {:noreply, state} + end + end + + @impl true + def handle_call({:navigate, url, _opts}, _from, state) do + case send_command(state, "WebDriver:Navigate", %{url: url}) do + {:ok, _response, new_state} -> + {:reply, {:ok, %{url: url}}, new_state} + + {:error, reason, new_state} -> + {:reply, {:error, reason}, new_state} + end + end + + @impl true + def handle_call({:click, selector, opts}, _from, state) do + with {:ok, element, state} <- find_element(state, selector, opts), + {:ok, _response, state} <- send_command(state, "WebDriver:ElementClick", %{id: element}) do + {:reply, {:ok, %{clicked: true}}, state} + else + {:error, reason, state} -> {:reply, {:error, reason}, state} + end + end + + @impl true + def handle_call({:type_text, selector, text, opts}, _from, state) do + clear = Keyword.get(opts, :clear, false) + + with {:ok, element, state} <- find_element(state, selector, opts), + {:ok, state} <- maybe_clear_element(state, element, clear), + {:ok, _response, state} <- + send_command(state, "WebDriver:ElementSendKeys", %{id: element, text: text}) do + {:reply, {:ok, %{typed: true}}, state} + else + {:error, reason, state} -> {:reply, {:error, reason}, state} + end + end + + @impl true + def handle_call({:screenshot, opts}, _from, state) do + selector = Keyword.get(opts, :selector) + + params = + if selector do + with {:ok, element, state} <- find_element(state, selector, opts) do + %{id: element, full: false} + else + _ -> %{full: true} + end + else + %{full: true} + end + + case send_command(state, "WebDriver:TakeScreenshot", params) do + {:ok, %{"value" => base64_data}, new_state} -> + binary_data = Base.decode64!(base64_data) + {:reply, {:ok, binary_data}, new_state} + + {:error, reason, new_state} -> + {:reply, {:error, reason}, new_state} + end + end + + @impl true + def handle_call({:execute_js, javascript, _opts}, _from, state) do + params = %{ + script: javascript, + args: [] + } + + case send_command(state, "WebDriver:ExecuteScript", params) do + {:ok, %{"value" => result}, new_state} -> + {:reply, {:ok, result}, new_state} + + {:error, reason, new_state} -> + {:reply, {:error, reason}, new_state} + end + end + + @impl true + def handle_call({:get_content, opts}, _from, state) do + format = Keyword.get(opts, :format, :html) + + script = + case format do + :html -> "return document.documentElement.outerHTML;" + :text -> "return document.body.innerText;" + :dom -> "return document.documentElement.innerHTML;" + end + + case send_command(state, "WebDriver:ExecuteScript", %{script: script, args: []}) do + {:ok, %{"value" => content}, new_state} -> + {:reply, {:ok, content}, new_state} + + {:error, reason, new_state} -> + {:reply, {:error, reason}, new_state} + end + end + + # Private functions + + defp connect(state) do + case :gen_tcp.connect( + String.to_charlist(state.host), + state.port, + [:binary, active: false, packet: :raw], + @default_timeout + ) do + {:ok, socket} -> + # Read initial handshake + case read_message(socket) do + {:ok, _handshake} -> + # Start a new session + case send_command(%{state | socket: socket}, "WebDriver:NewSession", %{}) do + {:ok, %{"sessionId" => session_id}, new_state} -> + Logger.info("Connected to Firefox Marionette (session: #{session_id})") + {:ok, %{new_state | session_id: session_id}} + + {:error, reason, _state} -> + {:error, reason} + end + + {:error, reason} -> + {:error, reason} + end + + {:error, reason} -> + {:error, reason} + end + end + + defp send_command(state, command, params) do + if state.socket == nil do + {:error, "Not connected to Firefox", state} + else + message_id = state.message_id + 1 + + message = %{ + "id" => message_id, + "name" => command, + "parameters" => params + } + + payload = Jason.encode!(message) + packet = "#{byte_size(payload)}:#{payload}" + + case :gen_tcp.send(state.socket, packet) do + :ok -> + case read_message(state.socket) do + {:ok, response} -> + {:ok, response, %{state | message_id: message_id}} + + {:error, reason} -> + {:error, reason, state} + end + + {:error, reason} -> + {:error, inspect(reason), state} + end + end + end + + defp read_message(socket) do + case :gen_tcp.recv(socket, 0, @default_timeout) do + {:ok, data} -> + # Parse "length:payload" format + case String.split(data, ":", parts: 2) do + [_length, json] -> + case Jason.decode(json) do + {:ok, parsed} -> {:ok, parsed} + {:error, _} -> {:error, "Invalid JSON response"} + end + + _ -> + {:error, "Invalid message format"} + end + + {:error, reason} -> + {:error, inspect(reason)} + end + end + + defp find_element(state, selector, opts) do + strategy = Keyword.get(opts, :strategy, :css) + + strategy_name = + case strategy do + :css -> "css selector" + :xpath -> "xpath" + :id -> "id" + :name -> "name" + :tag -> "tag name" + end + + params = %{ + using: strategy_name, + value: selector + } + + case send_command(state, "WebDriver:FindElement", params) do + {:ok, %{"value" => element}, new_state} -> + {:ok, element, new_state} + + {:error, reason, new_state} -> + {:error, reason, new_state} + end + end + + defp maybe_clear_element(state, element, false), do: {:ok, state} + + defp maybe_clear_element(state, element, true) do + case send_command(state, "WebDriver:ElementClear", %{id: element}) do + {:ok, _response, new_state} -> {:ok, new_state} + {:error, reason, new_state} -> {:error, reason, new_state} + end + end + + defp get_version do + case System.cmd("firefox", ["--version"], stderr_to_stdout: true) do + {output, 0} -> + output + |> String.trim() + |> String.replace("Mozilla Firefox ", "") + + _ -> + nil + end + end + + defp timeout(opts), do: Keyword.get(opts, :timeout, @default_timeout) +end diff --git a/firefox-lsp/lib/adapters/supervisor.ex b/firefox-lsp/lib/adapters/supervisor.ex index 7bfb881..5c05c45 100644 --- a/firefox-lsp/lib/adapters/supervisor.ex +++ b/firefox-lsp/lib/adapters/supervisor.ex @@ -1,26 +1,26 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell - -defmodule ClaudeFirefox.Adapters.Supervisor do - @moduledoc """ - Supervisor for browser adapter processes. - - Manages the lifecycle of browser adapter GenServers, ensuring they are - restarted on failure with a :one_for_one strategy. - """ - - use Supervisor - - def start_link(opts) do - Supervisor.start_link(__MODULE__, opts, name: __MODULE__) - end - - @impl true - def init(_opts) do - children = [ - {ClaudeFirefox.Adapters.Firefox, []} - ] - - Supervisor.init(children, strategy: :one_for_one) - end -end +# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell + +defmodule ClaudeFirefox.Adapters.Supervisor do + @moduledoc """ + Supervisor for browser adapter processes. + + Manages the lifecycle of browser adapter GenServers, ensuring they are + restarted on failure with a :one_for_one strategy. + """ + + use Supervisor + + def start_link(opts) do + Supervisor.start_link(__MODULE__, opts, name: __MODULE__) + end + + @impl true + def init(_opts) do + children = [ + {ClaudeFirefox.Adapters.Firefox, []} + ] + + Supervisor.init(children, strategy: :one_for_one) + end +end diff --git a/firefox-lsp/lib/claude_firefox_lsp.ex b/firefox-lsp/lib/claude_firefox_lsp.ex index 18154da..ef2f0ff 100644 --- a/firefox-lsp/lib/claude_firefox_lsp.ex +++ b/firefox-lsp/lib/claude_firefox_lsp.ex @@ -1,63 +1,63 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell - -defmodule ClaudeFirefox.LSP do - @moduledoc """ - Claude Firefox LSP - Language Server Protocol for Firefox browser automation. - - Provides LSP-based browser automation capabilities for Firefox, enabling - editor integration for web testing, scraping, and automation tasks. - - ## Features - - - Navigate to URLs - - Click elements (CSS selector, XPath) - - Type text into form fields - - Take screenshots - - Execute JavaScript - - Extract page content - - ## Architecture - - Built on the Marionette protocol, Firefox's native automation interface. - Uses GenLSP framework for LSP communication. - - ## Usage - - # Start the LSP server - ClaudeFirefox.LSP.start() - - # In your editor (VS Code, Neovim, etc.) - # Use LSP commands for browser automation - """ - - @version Mix.Project.config()[:version] - - @doc """ - Returns the LSP server version. - """ - def version, do: @version - - @doc """ - List available browser adapters. - """ - def adapters do - [ - ClaudeFirefox.Adapters.Firefox - ] - end - - @doc """ - Detect which browsers are available on the system. - """ - def detect_browsers do - adapters() - |> Enum.map(fn adapter -> - case adapter.detect() do - {:ok, true} -> {adapter, adapter.metadata()} - _ -> nil - end - end) - |> Enum.reject(&is_nil/1) - end -end +# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell + +defmodule ClaudeFirefox.LSP do + @moduledoc """ + Claude Firefox LSP - Language Server Protocol for Firefox browser automation. + + Provides LSP-based browser automation capabilities for Firefox, enabling + editor integration for web testing, scraping, and automation tasks. + + ## Features + + - Navigate to URLs + - Click elements (CSS selector, XPath) + - Type text into form fields + - Take screenshots + - Execute JavaScript + - Extract page content + + ## Architecture + + Built on the Marionette protocol, Firefox's native automation interface. + Uses GenLSP framework for LSP communication. + + ## Usage + + # Start the LSP server + ClaudeFirefox.LSP.start() + + # In your editor (VS Code, Neovim, etc.) + # Use LSP commands for browser automation + """ + + @version Mix.Project.config()[:version] + + @doc """ + Returns the LSP server version. + """ + def version, do: @version + + @doc """ + List available browser adapters. + """ + def adapters do + [ + ClaudeFirefox.Adapters.Firefox + ] + end + + @doc """ + Detect which browsers are available on the system. + """ + def detect_browsers do + adapters() + |> Enum.map(fn adapter -> + case adapter.detect() do + {:ok, true} -> {adapter, adapter.metadata()} + _ -> nil + end + end) + |> Enum.reject(&is_nil/1) + end +end diff --git a/firefox-lsp/lib/claude_firefox_lsp/application.ex b/firefox-lsp/lib/claude_firefox_lsp/application.ex index 467ea1f..7e952fd 100644 --- a/firefox-lsp/lib/claude_firefox_lsp/application.ex +++ b/firefox-lsp/lib/claude_firefox_lsp/application.ex @@ -1,26 +1,26 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell - -defmodule ClaudeFirefox.LSP.Application do - @moduledoc false - use Application - - require Logger - - @impl true - def start(_type, _args) do - children = [ - # Adapter supervisor (manages browser adapter processes) - {ClaudeFirefox.Adapters.Supervisor, []}, - - # LSP server (GenLSP) - {ClaudeFirefox.LSP.Server, []} - ] - - opts = [strategy: :one_for_one, name: ClaudeFirefox.LSP.Supervisor] - - Logger.info("Starting Claude Firefox LSP server v#{ClaudeFirefox.LSP.version()}") - - Supervisor.start_link(children, opts) - end -end +# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell + +defmodule ClaudeFirefox.LSP.Application do + @moduledoc false + use Application + + require Logger + + @impl true + def start(_type, _args) do + children = [ + # Adapter supervisor (manages browser adapter processes) + {ClaudeFirefox.Adapters.Supervisor, []}, + + # LSP server (GenLSP) + {ClaudeFirefox.LSP.Server, []} + ] + + opts = [strategy: :one_for_one, name: ClaudeFirefox.LSP.Supervisor] + + Logger.info("Starting Claude Firefox LSP server v#{ClaudeFirefox.LSP.version()}") + + Supervisor.start_link(children, opts) + end +end diff --git a/firefox-lsp/lib/lsp/handlers/completion.ex b/firefox-lsp/lib/lsp/handlers/completion.ex index dc69ce0..6274d59 100644 --- a/firefox-lsp/lib/lsp/handlers/completion.ex +++ b/firefox-lsp/lib/lsp/handlers/completion.ex @@ -1,111 +1,111 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -defmodule ClaudeFirefox.LSP.Handlers.Completion do - @moduledoc """ - Provides auto-completion for Firefox Marionette automation. - - Supports: - - Marionette protocol commands - - WebDriver methods - - CSS selectors and XPath - """ - - def handle(params, assigns) do - uri = get_in(params, ["textDocument", "uri"]) - position = params["position"] - - # Get document text from state - doc = get_in(assigns, [:documents, uri]) - text = if doc, do: doc.text, else: "" - - # Get line and character position - line = position["line"] - character = position["character"] - - # Get context around cursor - context = get_line_context(text, line, character) - - # Provide completions based on context - completions = complete_marionette(context, uri) - - completions - end - - # Extract line context around cursor - defp get_line_context(text, line, character) do - lines = String.split(text, "\n") - current_line = Enum.at(lines, line, "") - before_cursor = String.slice(current_line, 0, character) - - %{ - line: current_line, - before_cursor: before_cursor, - trigger: get_trigger(before_cursor) - } - end - - # Detect completion trigger - defp get_trigger(text) do - cond do - String.match?(text, ~r/find_element\($/) -> :locator_strategy - String.match?(text, ~r/By\.$/) -> :webdriver_by - String.match?(text, ~r/execute_script\($/) -> :javascript - String.ends_with?(text, ".") -> :method - true -> :none - end - end - - # Marionette/Firefox completions - defp complete_marionette(context, uri) do - case context.trigger do - :locator_strategy -> - ["css selector", "xpath", "id", "name", "tag name", "class name", "link text"] - |> Enum.map(&create_completion_item(&1, "enum")) - - :webdriver_by -> - ["CSS_SELECTOR", "XPATH", "ID", "NAME", "TAG_NAME", "CLASS_NAME", "LINK_TEXT"] - |> Enum.map(&create_completion_item(&1, "enum")) - - :method -> - [ - "navigate", "find_element", "find_elements", - "click", "send_keys", "clear", - "get_text", "get_attribute", "get_property", - "execute_script", "execute_async_script", - "screenshot", "get_page_source", - "switch_to_frame", "switch_to_parent_frame", - "get_window_handle", "get_window_handles", - "switch_to_window", "close_window" - ] - |> Enum.map(&create_completion_item(&1, "function")) - - _ -> - # Common Marionette/WebDriver methods - methods = [ - "navigate_to", "get_url", "back", "forward", "refresh", - "find_element", "find_elements", "click", "send_keys", - "execute_script", "screenshot", "close" - ] - - methods - |> Enum.map(&create_completion_item(&1, "function")) - end - end - - # Create LSP completion item - defp create_completion_item(label, kind_str) do - kind = case kind_str do - "function" -> 3 # Function - "enum" -> 13 # Enum - _ -> 1 # Text - end - - %{ - "label" => label, - "kind" => kind, - "detail" => "#{kind_str}", - "insertText" => label - } - end -end +# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +defmodule ClaudeFirefox.LSP.Handlers.Completion do + @moduledoc """ + Provides auto-completion for Firefox Marionette automation. + + Supports: + - Marionette protocol commands + - WebDriver methods + - CSS selectors and XPath + """ + + def handle(params, assigns) do + uri = get_in(params, ["textDocument", "uri"]) + position = params["position"] + + # Get document text from state + doc = get_in(assigns, [:documents, uri]) + text = if doc, do: doc.text, else: "" + + # Get line and character position + line = position["line"] + character = position["character"] + + # Get context around cursor + context = get_line_context(text, line, character) + + # Provide completions based on context + completions = complete_marionette(context, uri) + + completions + end + + # Extract line context around cursor + defp get_line_context(text, line, character) do + lines = String.split(text, "\n") + current_line = Enum.at(lines, line, "") + before_cursor = String.slice(current_line, 0, character) + + %{ + line: current_line, + before_cursor: before_cursor, + trigger: get_trigger(before_cursor) + } + end + + # Detect completion trigger + defp get_trigger(text) do + cond do + String.match?(text, ~r/find_element\($/) -> :locator_strategy + String.match?(text, ~r/By\.$/) -> :webdriver_by + String.match?(text, ~r/execute_script\($/) -> :javascript + String.ends_with?(text, ".") -> :method + true -> :none + end + end + + # Marionette/Firefox completions + defp complete_marionette(context, uri) do + case context.trigger do + :locator_strategy -> + ["css selector", "xpath", "id", "name", "tag name", "class name", "link text"] + |> Enum.map(&create_completion_item(&1, "enum")) + + :webdriver_by -> + ["CSS_SELECTOR", "XPATH", "ID", "NAME", "TAG_NAME", "CLASS_NAME", "LINK_TEXT"] + |> Enum.map(&create_completion_item(&1, "enum")) + + :method -> + [ + "navigate", "find_element", "find_elements", + "click", "send_keys", "clear", + "get_text", "get_attribute", "get_property", + "execute_script", "execute_async_script", + "screenshot", "get_page_source", + "switch_to_frame", "switch_to_parent_frame", + "get_window_handle", "get_window_handles", + "switch_to_window", "close_window" + ] + |> Enum.map(&create_completion_item(&1, "function")) + + _ -> + # Common Marionette/WebDriver methods + methods = [ + "navigate_to", "get_url", "back", "forward", "refresh", + "find_element", "find_elements", "click", "send_keys", + "execute_script", "screenshot", "close" + ] + + methods + |> Enum.map(&create_completion_item(&1, "function")) + end + end + + # Create LSP completion item + defp create_completion_item(label, kind_str) do + kind = case kind_str do + "function" -> 3 # Function + "enum" -> 13 # Enum + _ -> 1 # Text + end + + %{ + "label" => label, + "kind" => kind, + "detail" => "#{kind_str}", + "insertText" => label + } + end +end diff --git a/firefox-lsp/lib/lsp/handlers/diagnostics.ex b/firefox-lsp/lib/lsp/handlers/diagnostics.ex index d6049d0..2836811 100644 --- a/firefox-lsp/lib/lsp/handlers/diagnostics.ex +++ b/firefox-lsp/lib/lsp/handlers/diagnostics.ex @@ -1,115 +1,115 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -defmodule ClaudeFirefox.LSP.Handlers.Diagnostics do - @moduledoc """ - Provides diagnostics for Firefox automation scripts. - - Validates: - - Selector syntax (CSS and XPath) - - Method calls and arguments - - Common automation patterns - """ - - require Logger - - @doc """ - Handle diagnostics request by analyzing automation script. - - Returns LSP diagnostics format. - """ - def handle(params, assigns) do - uri = get_in(params, ["textDocument", "uri"]) || "" - - # Get document text from state - doc = get_in(assigns, [:documents, uri]) - text = if doc, do: doc.text, else: "" - - diagnostics = analyze_script(text) - - %{ - "uri" => uri, - "diagnostics" => diagnostics - } - end - - # Analyze automation script for common issues - defp analyze_script(text) do - lines = String.split(text, "\n") - - lines - |> Enum.with_index() - |> Enum.flat_map(fn {line, line_num} -> - analyze_line(line, line_num) - end) - |> Enum.take(50) # Limit to 50 diagnostics - end - - # Analyze individual line - defp analyze_line(line, line_num) do - diagnostics = [] - - # Check for invalid CSS selectors - diagnostics = if String.contains?(line, "find_element") and String.contains?(line, "css selector") do - case extract_selector(line) do - {:ok, selector} -> - if valid_css_selector?(selector) do - diagnostics - else - [create_diagnostic("Invalid CSS selector: #{selector}", line_num, 2) | diagnostics] - end - - :error -> - diagnostics - end - else - diagnostics - end - - # Check for hardcoded waits - diagnostics = if String.match?(line, ~r/sleep\(|wait\(/) and String.match?(line, ~r/\d{4,}/) do - [create_diagnostic("Long hardcoded wait detected - consider using explicit waits", line_num, 3) | diagnostics] - else - diagnostics - end - - # Check for missing error handling - diagnostics = if String.contains?(line, "find_element") and not String.contains?(line, "try") do - # This is a hint, not an error - diagnostics - else - diagnostics - end - - diagnostics - end - - # Extract selector from line - defp extract_selector(line) do - case Regex.run(~r/"css selector",\s*"([^"]+)"/, line) do - [_, selector] -> {:ok, selector} - _ -> :error - end - end - - # Validate CSS selector (basic check) - defp valid_css_selector?(selector) do - # Basic validation - just check for obvious syntax errors - not String.contains?(selector, ["<<", ">>", "|||"]) and - not String.starts_with?(selector, ">") and - not String.ends_with?(selector, ">") - end - - # Create a diagnostic entry - defp create_diagnostic(message, line_num, severity) do - %{ - "range" => %{ - "start" => %{"line" => line_num, "character" => 0}, - "end" => %{"line" => line_num, "character" => 100} - }, - "severity" => severity, - "source" => "claude-firefox", - "message" => message - } - end -end +# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +defmodule ClaudeFirefox.LSP.Handlers.Diagnostics do + @moduledoc """ + Provides diagnostics for Firefox automation scripts. + + Validates: + - Selector syntax (CSS and XPath) + - Method calls and arguments + - Common automation patterns + """ + + require Logger + + @doc """ + Handle diagnostics request by analyzing automation script. + + Returns LSP diagnostics format. + """ + def handle(params, assigns) do + uri = get_in(params, ["textDocument", "uri"]) || "" + + # Get document text from state + doc = get_in(assigns, [:documents, uri]) + text = if doc, do: doc.text, else: "" + + diagnostics = analyze_script(text) + + %{ + "uri" => uri, + "diagnostics" => diagnostics + } + end + + # Analyze automation script for common issues + defp analyze_script(text) do + lines = String.split(text, "\n") + + lines + |> Enum.with_index() + |> Enum.flat_map(fn {line, line_num} -> + analyze_line(line, line_num) + end) + |> Enum.take(50) # Limit to 50 diagnostics + end + + # Analyze individual line + defp analyze_line(line, line_num) do + diagnostics = [] + + # Check for invalid CSS selectors + diagnostics = if String.contains?(line, "find_element") and String.contains?(line, "css selector") do + case extract_selector(line) do + {:ok, selector} -> + if valid_css_selector?(selector) do + diagnostics + else + [create_diagnostic("Invalid CSS selector: #{selector}", line_num, 2) | diagnostics] + end + + :error -> + diagnostics + end + else + diagnostics + end + + # Check for hardcoded waits + diagnostics = if String.match?(line, ~r/sleep\(|wait\(/) and String.match?(line, ~r/\d{4,}/) do + [create_diagnostic("Long hardcoded wait detected - consider using explicit waits", line_num, 3) | diagnostics] + else + diagnostics + end + + # Check for missing error handling + diagnostics = if String.contains?(line, "find_element") and not String.contains?(line, "try") do + # This is a hint, not an error + diagnostics + else + diagnostics + end + + diagnostics + end + + # Extract selector from line + defp extract_selector(line) do + case Regex.run(~r/"css selector",\s*"([^"]+)"/, line) do + [_, selector] -> {:ok, selector} + _ -> :error + end + end + + # Validate CSS selector (basic check) + defp valid_css_selector?(selector) do + # Basic validation - just check for obvious syntax errors + not String.contains?(selector, ["<<", ">>", "|||"]) and + not String.starts_with?(selector, ">") and + not String.ends_with?(selector, ">") + end + + # Create a diagnostic entry + defp create_diagnostic(message, line_num, severity) do + %{ + "range" => %{ + "start" => %{"line" => line_num, "character" => 0}, + "end" => %{"line" => line_num, "character" => 100} + }, + "severity" => severity, + "source" => "claude-firefox", + "message" => message + } + end +end diff --git a/firefox-lsp/lib/lsp/handlers/hover.ex b/firefox-lsp/lib/lsp/handlers/hover.ex index 3d4efd3..fa782b4 100644 --- a/firefox-lsp/lib/lsp/handlers/hover.ex +++ b/firefox-lsp/lib/lsp/handlers/hover.ex @@ -1,98 +1,98 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -defmodule ClaudeFirefox.LSP.Handlers.Hover do - @moduledoc """ - Provides hover documentation for Firefox Marionette automation. - - Shows: - - Marionette command documentation - - WebDriver method descriptions - - Locator strategy information - """ - - def handle(params, assigns) do - uri = get_in(params, ["textDocument", "uri"]) - position = params["position"] - - # Get document text from state - doc = get_in(assigns, [:documents, uri]) - text = if doc, do: doc.text, else: "" - - # Get word at cursor position - word = get_word_at_position(text, position["line"], position["character"]) - - if word do - # Get documentation - docs = get_marionette_docs(word) - - if docs do - %{ - "contents" => %{ - "kind" => "markdown", - "value" => docs - } - } - else - nil - end - else - nil - end - end - - # Extract word at position - defp get_word_at_position(text, line, character) do - lines = String.split(text, "\n") - current_line = Enum.at(lines, line, "") - - # Find word boundaries (including underscores) - before = String.slice(current_line, 0, character) |> String.reverse() - after_text = String.slice(current_line, character, String.length(current_line)) - - start = Regex.run(~r/^[a-zA-Z0-9_]*/, before) |> List.first() |> String.reverse() - end_part = Regex.run(~r/^[a-zA-Z0-9_]*/, after_text) |> List.first() - - word = start <> end_part - if String.length(word) > 0, do: word, else: nil - end - - # Marionette/Firefox documentation - defp get_marionette_docs(word) do - docs = %{ - "navigate" => "**navigate(url)** - Navigate to URL\n\nNavigates the browser to the specified URL.\n\nExample: `navigate(\"https://example.com\")`", - "navigate_to" => "**navigate_to(url)** - Navigate to URL\n\nAlias for navigate. Navigates to the specified URL.", - "get_url" => "**get_url()** - Get current URL\n\nReturns the current page URL.", - "back" => "**back()** - Navigate back\n\nNavigates to the previous page in history.", - "forward" => "**forward()** - Navigate forward\n\nNavigates to the next page in history.", - "refresh" => "**refresh()** - Refresh page\n\nReloads the current page.", - "find_element" => "**find_element(strategy, selector)** - Find element\n\nFinds the first element matching the selector.\n\nStrategies: 'css selector', 'xpath', 'id', 'name'\n\nExample: `find_element(\"css selector\", \"#submit-btn\")`", - "find_elements" => "**find_elements(strategy, selector)** - Find elements\n\nFinds all elements matching the selector.\n\nReturns a list of elements.", - "click" => "**click(element)** - Click element\n\nClicks on the specified element.", - "send_keys" => "**send_keys(element, text)** - Send keystrokes\n\nSends text to the specified element.\n\nExample: `send_keys(input_elem, \"Hello World\")`", - "clear" => "**clear(element)** - Clear element\n\nClears the text in an input or textarea element.", - "get_text" => "**get_text(element)** - Get element text\n\nReturns the visible text content of the element.", - "get_attribute" => "**get_attribute(element, name)** - Get attribute\n\nReturns the value of the specified attribute.\n\nExample: `get_attribute(elem, \"href\")`", - "get_property" => "**get_property(element, name)** - Get property\n\nReturns the value of the specified DOM property.", - "execute_script" => "**execute_script(script, args)** - Execute JavaScript\n\nExecutes JavaScript code in the page context.\n\nExample: `execute_script(\"return document.title\", [])`", - "execute_async_script" => "**execute_async_script(script, args)** - Execute async JavaScript\n\nExecutes asynchronous JavaScript code.", - "screenshot" => "**screenshot()** - Take screenshot\n\nCaptures a screenshot of the current page.\n\nReturns PNG image data.", - "get_page_source" => "**get_page_source()** - Get page HTML\n\nReturns the HTML source of the current page.", - "switch_to_frame" => "**switch_to_frame(frame)** - Switch to frame\n\nSwitches context to the specified frame.", - "switch_to_parent_frame" => "**switch_to_parent_frame()** - Switch to parent frame\n\nSwitches context to the parent frame.", - "get_window_handle" => "**get_window_handle()** - Get current window handle\n\nReturns the handle of the current window.", - "get_window_handles" => "**get_window_handles()** - Get all window handles\n\nReturns handles of all open windows.", - "switch_to_window" => "**switch_to_window(handle)** - Switch to window\n\nSwitches to the window with the specified handle.", - "close_window" => "**close_window()** - Close current window\n\nCloses the current browser window.", - "CSS_SELECTOR" => "**CSS_SELECTOR** - CSS selector locator\n\nUse CSS selectors to find elements.\n\nExample: `#id`, `.class`, `div > p`", - "XPATH" => "**XPATH** - XPath locator\n\nUse XPath expressions to find elements.\n\nExample: `//div[@class='content']`", - "ID" => "**ID** - ID locator\n\nFinds element by ID attribute.", - "NAME" => "**NAME** - Name locator\n\nFinds element by name attribute.", - "TAG_NAME" => "**TAG_NAME** - Tag name locator\n\nFinds elements by HTML tag name.", - "CLASS_NAME" => "**CLASS_NAME** - Class name locator\n\nFinds elements by CSS class name.", - "LINK_TEXT" => "**LINK_TEXT** - Link text locator\n\nFinds anchor elements by exact link text." - } - - Map.get(docs, word) - end -end +# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +defmodule ClaudeFirefox.LSP.Handlers.Hover do + @moduledoc """ + Provides hover documentation for Firefox Marionette automation. + + Shows: + - Marionette command documentation + - WebDriver method descriptions + - Locator strategy information + """ + + def handle(params, assigns) do + uri = get_in(params, ["textDocument", "uri"]) + position = params["position"] + + # Get document text from state + doc = get_in(assigns, [:documents, uri]) + text = if doc, do: doc.text, else: "" + + # Get word at cursor position + word = get_word_at_position(text, position["line"], position["character"]) + + if word do + # Get documentation + docs = get_marionette_docs(word) + + if docs do + %{ + "contents" => %{ + "kind" => "markdown", + "value" => docs + } + } + else + nil + end + else + nil + end + end + + # Extract word at position + defp get_word_at_position(text, line, character) do + lines = String.split(text, "\n") + current_line = Enum.at(lines, line, "") + + # Find word boundaries (including underscores) + before = String.slice(current_line, 0, character) |> String.reverse() + after_text = String.slice(current_line, character, String.length(current_line)) + + start = Regex.run(~r/^[a-zA-Z0-9_]*/, before) |> List.first() |> String.reverse() + end_part = Regex.run(~r/^[a-zA-Z0-9_]*/, after_text) |> List.first() + + word = start <> end_part + if String.length(word) > 0, do: word, else: nil + end + + # Marionette/Firefox documentation + defp get_marionette_docs(word) do + docs = %{ + "navigate" => "**navigate(url)** - Navigate to URL\n\nNavigates the browser to the specified URL.\n\nExample: `navigate(\"https://example.com\")`", + "navigate_to" => "**navigate_to(url)** - Navigate to URL\n\nAlias for navigate. Navigates to the specified URL.", + "get_url" => "**get_url()** - Get current URL\n\nReturns the current page URL.", + "back" => "**back()** - Navigate back\n\nNavigates to the previous page in history.", + "forward" => "**forward()** - Navigate forward\n\nNavigates to the next page in history.", + "refresh" => "**refresh()** - Refresh page\n\nReloads the current page.", + "find_element" => "**find_element(strategy, selector)** - Find element\n\nFinds the first element matching the selector.\n\nStrategies: 'css selector', 'xpath', 'id', 'name'\n\nExample: `find_element(\"css selector\", \"#submit-btn\")`", + "find_elements" => "**find_elements(strategy, selector)** - Find elements\n\nFinds all elements matching the selector.\n\nReturns a list of elements.", + "click" => "**click(element)** - Click element\n\nClicks on the specified element.", + "send_keys" => "**send_keys(element, text)** - Send keystrokes\n\nSends text to the specified element.\n\nExample: `send_keys(input_elem, \"Hello World\")`", + "clear" => "**clear(element)** - Clear element\n\nClears the text in an input or textarea element.", + "get_text" => "**get_text(element)** - Get element text\n\nReturns the visible text content of the element.", + "get_attribute" => "**get_attribute(element, name)** - Get attribute\n\nReturns the value of the specified attribute.\n\nExample: `get_attribute(elem, \"href\")`", + "get_property" => "**get_property(element, name)** - Get property\n\nReturns the value of the specified DOM property.", + "execute_script" => "**execute_script(script, args)** - Execute JavaScript\n\nExecutes JavaScript code in the page context.\n\nExample: `execute_script(\"return document.title\", [])`", + "execute_async_script" => "**execute_async_script(script, args)** - Execute async JavaScript\n\nExecutes asynchronous JavaScript code.", + "screenshot" => "**screenshot()** - Take screenshot\n\nCaptures a screenshot of the current page.\n\nReturns PNG image data.", + "get_page_source" => "**get_page_source()** - Get page HTML\n\nReturns the HTML source of the current page.", + "switch_to_frame" => "**switch_to_frame(frame)** - Switch to frame\n\nSwitches context to the specified frame.", + "switch_to_parent_frame" => "**switch_to_parent_frame()** - Switch to parent frame\n\nSwitches context to the parent frame.", + "get_window_handle" => "**get_window_handle()** - Get current window handle\n\nReturns the handle of the current window.", + "get_window_handles" => "**get_window_handles()** - Get all window handles\n\nReturns handles of all open windows.", + "switch_to_window" => "**switch_to_window(handle)** - Switch to window\n\nSwitches to the window with the specified handle.", + "close_window" => "**close_window()** - Close current window\n\nCloses the current browser window.", + "CSS_SELECTOR" => "**CSS_SELECTOR** - CSS selector locator\n\nUse CSS selectors to find elements.\n\nExample: `#id`, `.class`, `div > p`", + "XPATH" => "**XPATH** - XPath locator\n\nUse XPath expressions to find elements.\n\nExample: `//div[@class='content']`", + "ID" => "**ID** - ID locator\n\nFinds element by ID attribute.", + "NAME" => "**NAME** - Name locator\n\nFinds element by name attribute.", + "TAG_NAME" => "**TAG_NAME** - Tag name locator\n\nFinds elements by HTML tag name.", + "CLASS_NAME" => "**CLASS_NAME** - Class name locator\n\nFinds elements by CSS class name.", + "LINK_TEXT" => "**LINK_TEXT** - Link text locator\n\nFinds anchor elements by exact link text." + } + + Map.get(docs, word) + end +end diff --git a/firefox-lsp/lib/lsp/server.ex b/firefox-lsp/lib/lsp/server.ex index e483128..21642ce 100644 --- a/firefox-lsp/lib/lsp/server.ex +++ b/firefox-lsp/lib/lsp/server.ex @@ -1,245 +1,245 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell - -defmodule ClaudeFirefox.LSP.Server do - @moduledoc """ - GenLSP server implementation for ClaudeFirefox. - - Handles LSP protocol messages and delegates to appropriate handlers. - """ - use GenLSP - - require Logger - - alias ClaudeFirefox.Adapters.Firefox - alias ClaudeFirefox.LSP.Handlers.{Completion, Diagnostics, Hover} - - @impl GenLSP - def handle_info(_msg, lsp), do: {:noreply, lsp} - - def start_link(args) do - GenLSP.start_link(__MODULE__, args, []) - end - - @impl GenLSP - def init(_lsp, _args) do - {:ok, %{project_path: nil, browser: :firefox, session_active: false, documents: %{}}} - end - - @impl GenLSP - def handle_request(%{"method" => "initialize", "params" => params}, lsp) do - project_path = get_in(params, ["rootUri"]) |> parse_uri() - - Logger.info("Initializing LSP for project: #{inspect(project_path)}") - - # Detect available browsers - available_browsers = ClaudeFirefox.LSP.detect_browsers() - Logger.info("Available browsers: #{inspect(available_browsers)}") - - server_capabilities = %{ - "textDocumentSync" => %{ - "openClose" => true, - "change" => 1, # Full sync - "save" => %{"includeText" => false} - }, - "completionProvider" => %{ - "triggerCharacters" => [".", "(", "["], - "resolveProvider" => false - }, - "hoverProvider" => true, - "executeCommandProvider" => %{ - "commands" => [ - "firefox.navigate", - "firefox.click", - "firefox.typeText", - "firefox.screenshot", - "firefox.executeJs", - "firefox.getContent", - "firefox.detectBrowsers" - ] - } - } - - result = %{ - "capabilities" => server_capabilities, - "serverInfo" => %{ - "name" => "Claude Firefox LSP", - "version" => ClaudeFirefox.LSP.version() - } - } - - new_state = Map.merge(lsp, %{project_path: project_path}) - {:reply, result, new_state} - end - - @impl GenLSP - def handle_request(%{"method" => "textDocument/completion", "params" => params}, lsp) do - completions = Completion.handle(params, lsp.assigns) - {:reply, completions, lsp} - end - - @impl GenLSP - def handle_request(%{"method" => "textDocument/hover", "params" => params}, lsp) do - hover_info = Hover.handle(params, lsp.assigns) - {:reply, hover_info, lsp} - end - - @impl GenLSP - def handle_request(%{"method" => "workspace/executeCommand", "params" => params}, lsp) do - command = params["command"] - args = params["arguments"] || [] - result = execute_command(command, args, lsp.assigns) - {:reply, result, lsp} - end - - @impl GenLSP - def handle_request(_request, lsp) do - {:reply, nil, lsp} - end - - @impl GenLSP - def handle_notification(%{"method" => "initialized"}, lsp) do - Logger.info("LSP server initialized") - {:noreply, lsp} - end - - @impl GenLSP - def handle_notification(%{"method" => "textDocument/didOpen", "params" => params}, lsp) do - uri = params["textDocument"]["uri"] - text = params["textDocument"]["text"] - version = params["textDocument"]["version"] - - Logger.info("Document opened: #{uri}") - - # Store document state - documents = Map.put(lsp.assigns.documents, uri, %{text: text, version: version}) - new_state = put_in(lsp.assigns.documents, documents) - - # Trigger diagnostics on open - spawn(fn -> - diagnostics = Diagnostics.handle(params, new_state.assigns) - - GenLSP.notify(lsp, %{ - "method" => "textDocument/publishDiagnostics", - "params" => diagnostics - }) - end) - - {:noreply, new_state} - end - - @impl GenLSP - def handle_notification(%{"method" => "textDocument/didChange", "params" => params}, lsp) do - uri = params["textDocument"]["uri"] - changes = params["contentChanges"] - version = params["textDocument"]["version"] - - # Update document with full sync (change type 1) - new_text = List.first(changes)["text"] - documents = Map.update(lsp.assigns.documents, uri, %{text: new_text, version: version}, fn doc -> - %{doc | text: new_text, version: version} - end) - - new_state = put_in(lsp.assigns.documents, documents) - {:noreply, new_state} - end - - @impl GenLSP - def handle_notification(%{"method" => "textDocument/didClose", "params" => params}, lsp) do - uri = params["textDocument"]["uri"] - Logger.info("Document closed: #{uri}") - - # Remove document from state - documents = Map.delete(lsp.assigns.documents, uri) - new_state = put_in(lsp.assigns.documents, documents) - - {:noreply, new_state} - end - - @impl GenLSP - def handle_notification(%{"method" => "textDocument/didSave", "params" => params}, lsp) do - uri = params["textDocument"]["uri"] - Logger.info("File saved: #{uri}") - - # Trigger diagnostics on save - spawn(fn -> - diagnostics = Diagnostics.handle(params, lsp.assigns) - - GenLSP.notify(lsp, %{ - "method" => "textDocument/publishDiagnostics", - "params" => diagnostics - }) - end) - - {:noreply, lsp} - end - - @impl GenLSP - def handle_notification(_notification, lsp), do: {:noreply, lsp} - - # Private helpers - - defp parse_uri(nil), do: nil - defp parse_uri(uri) when is_binary(uri) do - case URI.parse(uri) do - %URI{scheme: "file", path: path} -> path - _ -> nil - end - end - - # Command execution - - defp execute_command("firefox.navigate", [url | opts], _assigns) do - opts_list = parse_opts(opts) - Firefox.navigate(url, opts_list) - end - - defp execute_command("firefox.click", [selector | opts], _assigns) do - opts_list = parse_opts(opts) - Firefox.click(selector, opts_list) - end - - defp execute_command("firefox.typeText", [selector, text | opts], _assigns) do - opts_list = parse_opts(opts) - Firefox.type_text(selector, text, opts_list) - end - - defp execute_command("firefox.screenshot", opts, _assigns) do - opts_list = parse_opts(opts) - - case Firefox.screenshot(opts_list) do - {:ok, binary_data} -> - # Encode as base64 for JSON transport - base64_data = Base.encode64(binary_data) - {:ok, %{screenshot: base64_data, format: "png"}} - - error -> error - end - end - - defp execute_command("firefox.executeJs", [javascript | opts], _assigns) do - opts_list = parse_opts(opts) - Firefox.execute_js(javascript, opts_list) - end - - defp execute_command("firefox.getContent", opts, _assigns) do - opts_list = parse_opts(opts) - Firefox.get_content(opts_list) - end - - defp execute_command("firefox.detectBrowsers", _args, _assigns) do - browsers = ClaudeFirefox.LSP.detect_browsers() - {:ok, %{browsers: browsers}} - end - - defp execute_command(_command, _args, _assigns) do - {:error, "Unknown command"} - end - - # Parse opts from LSP arguments (may be map or list) - defp parse_opts([]), do: [] - defp parse_opts([%{} = opts_map | _]) do - Enum.map(opts_map, fn {k, v} -> {String.to_atom(k), v} end) - end - defp parse_opts(_), do: [] -end +# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +defmodule ClaudeFirefox.LSP.Server do + @moduledoc """ + GenLSP server implementation for ClaudeFirefox. + + Handles LSP protocol messages and delegates to appropriate handlers. + """ + use GenLSP + + require Logger + + alias ClaudeFirefox.Adapters.Firefox + alias ClaudeFirefox.LSP.Handlers.{Completion, Diagnostics, Hover} + + @impl GenLSP + def handle_info(_msg, lsp), do: {:noreply, lsp} + + def start_link(args) do + GenLSP.start_link(__MODULE__, args, []) + end + + @impl GenLSP + def init(_lsp, _args) do + {:ok, %{project_path: nil, browser: :firefox, session_active: false, documents: %{}}} + end + + @impl GenLSP + def handle_request(%{"method" => "initialize", "params" => params}, lsp) do + project_path = get_in(params, ["rootUri"]) |> parse_uri() + + Logger.info("Initializing LSP for project: #{inspect(project_path)}") + + # Detect available browsers + available_browsers = ClaudeFirefox.LSP.detect_browsers() + Logger.info("Available browsers: #{inspect(available_browsers)}") + + server_capabilities = %{ + "textDocumentSync" => %{ + "openClose" => true, + "change" => 1, # Full sync + "save" => %{"includeText" => false} + }, + "completionProvider" => %{ + "triggerCharacters" => [".", "(", "["], + "resolveProvider" => false + }, + "hoverProvider" => true, + "executeCommandProvider" => %{ + "commands" => [ + "firefox.navigate", + "firefox.click", + "firefox.typeText", + "firefox.screenshot", + "firefox.executeJs", + "firefox.getContent", + "firefox.detectBrowsers" + ] + } + } + + result = %{ + "capabilities" => server_capabilities, + "serverInfo" => %{ + "name" => "Claude Firefox LSP", + "version" => ClaudeFirefox.LSP.version() + } + } + + new_state = Map.merge(lsp, %{project_path: project_path}) + {:reply, result, new_state} + end + + @impl GenLSP + def handle_request(%{"method" => "textDocument/completion", "params" => params}, lsp) do + completions = Completion.handle(params, lsp.assigns) + {:reply, completions, lsp} + end + + @impl GenLSP + def handle_request(%{"method" => "textDocument/hover", "params" => params}, lsp) do + hover_info = Hover.handle(params, lsp.assigns) + {:reply, hover_info, lsp} + end + + @impl GenLSP + def handle_request(%{"method" => "workspace/executeCommand", "params" => params}, lsp) do + command = params["command"] + args = params["arguments"] || [] + result = execute_command(command, args, lsp.assigns) + {:reply, result, lsp} + end + + @impl GenLSP + def handle_request(_request, lsp) do + {:reply, nil, lsp} + end + + @impl GenLSP + def handle_notification(%{"method" => "initialized"}, lsp) do + Logger.info("LSP server initialized") + {:noreply, lsp} + end + + @impl GenLSP + def handle_notification(%{"method" => "textDocument/didOpen", "params" => params}, lsp) do + uri = params["textDocument"]["uri"] + text = params["textDocument"]["text"] + version = params["textDocument"]["version"] + + Logger.info("Document opened: #{uri}") + + # Store document state + documents = Map.put(lsp.assigns.documents, uri, %{text: text, version: version}) + new_state = put_in(lsp.assigns.documents, documents) + + # Trigger diagnostics on open + spawn(fn -> + diagnostics = Diagnostics.handle(params, new_state.assigns) + + GenLSP.notify(lsp, %{ + "method" => "textDocument/publishDiagnostics", + "params" => diagnostics + }) + end) + + {:noreply, new_state} + end + + @impl GenLSP + def handle_notification(%{"method" => "textDocument/didChange", "params" => params}, lsp) do + uri = params["textDocument"]["uri"] + changes = params["contentChanges"] + version = params["textDocument"]["version"] + + # Update document with full sync (change type 1) + new_text = List.first(changes)["text"] + documents = Map.update(lsp.assigns.documents, uri, %{text: new_text, version: version}, fn doc -> + %{doc | text: new_text, version: version} + end) + + new_state = put_in(lsp.assigns.documents, documents) + {:noreply, new_state} + end + + @impl GenLSP + def handle_notification(%{"method" => "textDocument/didClose", "params" => params}, lsp) do + uri = params["textDocument"]["uri"] + Logger.info("Document closed: #{uri}") + + # Remove document from state + documents = Map.delete(lsp.assigns.documents, uri) + new_state = put_in(lsp.assigns.documents, documents) + + {:noreply, new_state} + end + + @impl GenLSP + def handle_notification(%{"method" => "textDocument/didSave", "params" => params}, lsp) do + uri = params["textDocument"]["uri"] + Logger.info("File saved: #{uri}") + + # Trigger diagnostics on save + spawn(fn -> + diagnostics = Diagnostics.handle(params, lsp.assigns) + + GenLSP.notify(lsp, %{ + "method" => "textDocument/publishDiagnostics", + "params" => diagnostics + }) + end) + + {:noreply, lsp} + end + + @impl GenLSP + def handle_notification(_notification, lsp), do: {:noreply, lsp} + + # Private helpers + + defp parse_uri(nil), do: nil + defp parse_uri(uri) when is_binary(uri) do + case URI.parse(uri) do + %URI{scheme: "file", path: path} -> path + _ -> nil + end + end + + # Command execution + + defp execute_command("firefox.navigate", [url | opts], _assigns) do + opts_list = parse_opts(opts) + Firefox.navigate(url, opts_list) + end + + defp execute_command("firefox.click", [selector | opts], _assigns) do + opts_list = parse_opts(opts) + Firefox.click(selector, opts_list) + end + + defp execute_command("firefox.typeText", [selector, text | opts], _assigns) do + opts_list = parse_opts(opts) + Firefox.type_text(selector, text, opts_list) + end + + defp execute_command("firefox.screenshot", opts, _assigns) do + opts_list = parse_opts(opts) + + case Firefox.screenshot(opts_list) do + {:ok, binary_data} -> + # Encode as base64 for JSON transport + base64_data = Base.encode64(binary_data) + {:ok, %{screenshot: base64_data, format: "png"}} + + error -> error + end + end + + defp execute_command("firefox.executeJs", [javascript | opts], _assigns) do + opts_list = parse_opts(opts) + Firefox.execute_js(javascript, opts_list) + end + + defp execute_command("firefox.getContent", opts, _assigns) do + opts_list = parse_opts(opts) + Firefox.get_content(opts_list) + end + + defp execute_command("firefox.detectBrowsers", _args, _assigns) do + browsers = ClaudeFirefox.LSP.detect_browsers() + {:ok, %{browsers: browsers}} + end + + defp execute_command(_command, _args, _assigns) do + {:error, "Unknown command"} + end + + # Parse opts from LSP arguments (may be map or list) + defp parse_opts([]), do: [] + defp parse_opts([%{} = opts_map | _]) do + Enum.map(opts_map, fn {k, v} -> {String.to_atom(k), v} end) + end + defp parse_opts(_), do: [] +end diff --git a/firefox-lsp/mix.exs b/firefox-lsp/mix.exs index 11b0184..4dc4c34 100644 --- a/firefox-lsp/mix.exs +++ b/firefox-lsp/mix.exs @@ -1,100 +1,100 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell - -defmodule ClaudeFirefox.LSP.MixProject do - use Mix.Project - - @version "0.1.0" - @source_url "https://github.com/hyperpolymath/claude-firefox-lsp" - - def project do - [ - app: :claude_firefox_lsp, - version: @version, - elixir: "~> 1.17", - start_permanent: Mix.env() == :prod, - deps: deps(), - aliases: aliases(), - - # Package info - name: "Claude Firefox LSP", - description: "Language Server Protocol implementation for Firefox browser automation", - package: package(), - source_url: @source_url, - homepage_url: @source_url, - docs: docs(), - - # Testing - test_coverage: [tool: ExCoveralls], - preferred_cli_env: [ - coveralls: :test, - "coveralls.detail": :test, - "coveralls.post": :test, - "coveralls.html": :test - ], - - # Dialyzer - dialyzer: [ - plt_add_apps: [:mix, :ex_unit], - plt_core_path: "priv/plts", - plt_file: {:no_warn, "priv/plts/dialyzer.plt"} - ] - ] - end - - def application do - [ - extra_applications: [:logger], - mod: {ClaudeFirefox.LSP.Application, []} - ] - end - - defp deps do - [ - # LSP Framework - {:gen_lsp, "~> 0.10"}, - - # JSON parsing - {:jason, "~> 1.4"}, - - # HTTP client for Firefox remote debugging - {:req, "~> 0.5"}, - - # Testing & quality - {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, - {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, - {:ex_doc, "~> 0.34", only: :dev, runtime: false}, - {:excoveralls, "~> 0.18", only: :test}, - {:mox, "~> 1.1", only: :test} - ] - end - - defp aliases do - [ - setup: ["deps.get", "compile"], - quality: ["format --check-formatted", "credo --strict", "dialyzer"], - "test.all": ["test", "dialyzer", "credo"] - ] - end - - defp package do - [ - name: "claude_firefox_lsp", - files: ~w(lib .formatter.exs mix.exs README.adoc LICENSE CHANGELOG.md), - licenses: ["PMPL-1.0-or-later"], - links: %{ - "GitHub" => @source_url - } - ] - end - - defp docs do - [ - main: "readme", - extras: ["README.adoc", "CHANGELOG.md"], - source_ref: "v#{@version}", - source_url: @source_url, - authors: ["Jonathan D.A. Jewell"] - ] - end -end +# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell + +defmodule ClaudeFirefox.LSP.MixProject do + use Mix.Project + + @version "0.1.0" + @source_url "https://github.com/hyperpolymath/claude-firefox-lsp" + + def project do + [ + app: :claude_firefox_lsp, + version: @version, + elixir: "~> 1.17", + start_permanent: Mix.env() == :prod, + deps: deps(), + aliases: aliases(), + + # Package info + name: "Claude Firefox LSP", + description: "Language Server Protocol implementation for Firefox browser automation", + package: package(), + source_url: @source_url, + homepage_url: @source_url, + docs: docs(), + + # Testing + test_coverage: [tool: ExCoveralls], + preferred_cli_env: [ + coveralls: :test, + "coveralls.detail": :test, + "coveralls.post": :test, + "coveralls.html": :test + ], + + # Dialyzer + dialyzer: [ + plt_add_apps: [:mix, :ex_unit], + plt_core_path: "priv/plts", + plt_file: {:no_warn, "priv/plts/dialyzer.plt"} + ] + ] + end + + def application do + [ + extra_applications: [:logger], + mod: {ClaudeFirefox.LSP.Application, []} + ] + end + + defp deps do + [ + # LSP Framework + {:gen_lsp, "~> 0.10"}, + + # JSON parsing + {:jason, "~> 1.4"}, + + # HTTP client for Firefox remote debugging + {:req, "~> 0.5"}, + + # Testing & quality + {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, + {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, + {:ex_doc, "~> 0.34", only: :dev, runtime: false}, + {:excoveralls, "~> 0.18", only: :test}, + {:mox, "~> 1.1", only: :test} + ] + end + + defp aliases do + [ + setup: ["deps.get", "compile"], + quality: ["format --check-formatted", "credo --strict", "dialyzer"], + "test.all": ["test", "dialyzer", "credo"] + ] + end + + defp package do + [ + name: "claude_firefox_lsp", + files: ~w(lib .formatter.exs mix.exs README.adoc LICENSE CHANGELOG.md), + licenses: ["PMPL-1.0-or-later"], + links: %{ + "GitHub" => @source_url + } + ] + end + + defp docs do + [ + main: "readme", + extras: ["README.adoc", "CHANGELOG.md"], + source_ref: "v#{@version}", + source_url: @source_url, + authors: ["Jonathan D.A. Jewell"] + ] + end +end diff --git a/firefox-lsp/test/adapters/firefox_test.exs b/firefox-lsp/test/adapters/firefox_test.exs index 6d85960..7a18cee 100644 --- a/firefox-lsp/test/adapters/firefox_test.exs +++ b/firefox-lsp/test/adapters/firefox_test.exs @@ -1,28 +1,28 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -defmodule PolyLSP.Adapters.FirefoxTest do - use ExUnit.Case - alias PolyLSP.Adapters.Firefox - - describe "detect/1" do - test "returns true when config exists" do - assert {:ok, true} = Firefox.detect(".") - end - end - - describe "version/0" do - test "returns version string" do - case Firefox.version() do - {:ok, version} -> assert is_binary(version) - {:error, _} -> :ok # CLI not installed - end - end - end - - describe "metadata/0" do - test "returns valid metadata" do - meta = Firefox.metadata() - assert is_map(meta) - assert Map.has_key?(meta, :name) - end - end -end +# SPDX-License-Identifier: PMPL-1.0-or-later +defmodule PolyLSP.Adapters.FirefoxTest do + use ExUnit.Case + alias PolyLSP.Adapters.Firefox + + describe "detect/1" do + test "returns true when config exists" do + assert {:ok, true} = Firefox.detect(".") + end + end + + describe "version/0" do + test "returns version string" do + case Firefox.version() do + {:ok, version} -> assert is_binary(version) + {:error, _} -> :ok # CLI not installed + end + end + end + + describe "metadata/0" do + test "returns valid metadata" do + meta = Firefox.metadata() + assert is_map(meta) + assert Map.has_key?(meta, :name) + end + end +end diff --git a/firefox-lsp/test/claude_firefox/lsp_test.exs b/firefox-lsp/test/claude_firefox/lsp_test.exs index 3ce7ec6..614f5b1 100644 --- a/firefox-lsp/test/claude_firefox/lsp_test.exs +++ b/firefox-lsp/test/claude_firefox/lsp_test.exs @@ -1,17 +1,17 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell - -defmodule ClaudeFirefox.LSPTest do - use ExUnit.Case - doctest ClaudeFirefox.LSP - - test "version returns string" do - assert is_binary(ClaudeFirefox.LSP.version()) - end - - test "adapters returns list" do - adapters = ClaudeFirefox.LSP.adapters() - assert is_list(adapters) - assert ClaudeFirefox.Adapters.Firefox in adapters - end -end +# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell + +defmodule ClaudeFirefox.LSPTest do + use ExUnit.Case + doctest ClaudeFirefox.LSP + + test "version returns string" do + assert is_binary(ClaudeFirefox.LSP.version()) + end + + test "adapters returns list" do + adapters = ClaudeFirefox.LSP.adapters() + assert is_list(adapters) + assert ClaudeFirefox.Adapters.Firefox in adapters + end +end diff --git a/firefox-lsp/test/support/lsp_helpers.ex b/firefox-lsp/test/support/lsp_helpers.ex index 07d3c65..f352ecb 100644 --- a/firefox-lsp/test/support/lsp_helpers.ex +++ b/firefox-lsp/test/support/lsp_helpers.ex @@ -1,91 +1,91 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -defmodule LSPHelpers do - @moduledoc """ - Common test helpers for LSP protocol testing. - """ - - @doc """ - Create a mock LSP initialize request. - """ - def initialize_request(root_uri \\ "file:///test") do - %{ - "jsonrpc" => "2.0", - "id" => 1, - "method" => "initialize", - "params" => %{ - "rootUri" => root_uri, - "capabilities" => %{ - "textDocument" => %{ - "completion" => %{"dynamicRegistration" => true}, - "hover" => %{"dynamicRegistration" => true} - } - } - } - } - end - - @doc """ - Create a mock textDocument/didOpen notification. - """ - def did_open_notification(uri, language_id, text) do - %{ - "jsonrpc" => "2.0", - "method" => "textDocument/didOpen", - "params" => %{ - "textDocument" => %{ - "uri" => uri, - "languageId" => language_id, - "version" => 1, - "text" => text - } - } - } - end - - @doc """ - Create a mock textDocument/completion request. - """ - def completion_request(uri, line, character) do - %{ - "jsonrpc" => "2.0", - "id" => 2, - "method" => "textDocument/completion", - "params" => %{ - "textDocument" => %{"uri" => uri}, - "position" => %{"line" => line, "character" => character} - } - } - end - - @doc """ - Create a mock textDocument/hover request. - """ - def hover_request(uri, line, character) do - %{ - "jsonrpc" => "2.0", - "id" => 3, - "method" => "textDocument/hover", - "params" => %{ - "textDocument" => %{"uri" => uri}, - "position" => %{"line" => line, "character" => character} - } - } - end - - @doc """ - Mock adapter implementation for testing. - """ - defmodule MockAdapter do - @behaviour :adapter_behaviour - - def detect(_path), do: {:ok, true} - def version, do: {:ok, "1.0.0-test"} - def metadata do - %{ - name: "Mock", - type: "test", - language: "elixir" - } - end - end -end +# SPDX-License-Identifier: PMPL-1.0-or-later +defmodule LSPHelpers do + @moduledoc """ + Common test helpers for LSP protocol testing. + """ + + @doc """ + Create a mock LSP initialize request. + """ + def initialize_request(root_uri \\ "file:///test") do + %{ + "jsonrpc" => "2.0", + "id" => 1, + "method" => "initialize", + "params" => %{ + "rootUri" => root_uri, + "capabilities" => %{ + "textDocument" => %{ + "completion" => %{"dynamicRegistration" => true}, + "hover" => %{"dynamicRegistration" => true} + } + } + } + } + end + + @doc """ + Create a mock textDocument/didOpen notification. + """ + def did_open_notification(uri, language_id, text) do + %{ + "jsonrpc" => "2.0", + "method" => "textDocument/didOpen", + "params" => %{ + "textDocument" => %{ + "uri" => uri, + "languageId" => language_id, + "version" => 1, + "text" => text + } + } + } + end + + @doc """ + Create a mock textDocument/completion request. + """ + def completion_request(uri, line, character) do + %{ + "jsonrpc" => "2.0", + "id" => 2, + "method" => "textDocument/completion", + "params" => %{ + "textDocument" => %{"uri" => uri}, + "position" => %{"line" => line, "character" => character} + } + } + end + + @doc """ + Create a mock textDocument/hover request. + """ + def hover_request(uri, line, character) do + %{ + "jsonrpc" => "2.0", + "id" => 3, + "method" => "textDocument/hover", + "params" => %{ + "textDocument" => %{"uri" => uri}, + "position" => %{"line" => line, "character" => character} + } + } + end + + @doc """ + Mock adapter implementation for testing. + """ + defmodule MockAdapter do + @behaviour :adapter_behaviour + + def detect(_path), do: {:ok, true} + def version, do: {:ok, "1.0.0-test"} + def metadata do + %{ + name: "Mock", + type: "test", + language: "elixir" + } + end + end +end diff --git a/firefox-lsp/test/test_helper.exs b/firefox-lsp/test/test_helper.exs index f58cc7d..b6eb14b 100644 --- a/firefox-lsp/test/test_helper.exs +++ b/firefox-lsp/test/test_helper.exs @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0-or-later -# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell - -ExUnit.start() +# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell + +ExUnit.start() diff --git a/firefox-lsp/vscode-extension/.vscodeignore b/firefox-lsp/vscode-extension/.vscodeignore index 3525607..f37a150 100644 --- a/firefox-lsp/vscode-extension/.vscodeignore +++ b/firefox-lsp/vscode-extension/.vscodeignore @@ -1,14 +1,14 @@ -.vscode/** -.vscode-test/** -src/** -.gitignore -.yarnrc -vsc-extension-quickstart.md -**/tsconfig.json -**/.eslintrc.json -**/*.map -**/*.ts -node_modules/**/test/** -node_modules/**/tests/** -node_modules/**/*.md -node_modules/**/*.txt +.vscode/** +.vscode-test/** +src/** +.gitignore +.yarnrc +vsc-extension-quickstart.md +**/tsconfig.json +**/.eslintrc.json +**/*.map +**/*.ts +node_modules/**/test/** +node_modules/**/tests/** +node_modules/**/*.md +node_modules/**/*.txt diff --git a/firefox-lsp/vscode-extension/README.md b/firefox-lsp/vscode-extension/README.md index 27ef39e..65b32ba 100644 --- a/firefox-lsp/vscode-extension/README.md +++ b/firefox-lsp/vscode-extension/README.md @@ -1,59 +1,59 @@ -# Claude Firefox LSP - VS Code Extension - -VS Code extension for Firefox browser automation via Language Server Protocol. - -## Features - -- **Navigate**: Load URLs in Firefox -- **Click**: Click elements by CSS selector or XPath -- **Type Text**: Enter text into form fields -- **Screenshot**: Capture page screenshots -- **Execute JavaScript**: Run custom JavaScript in page context -- **Get Content**: Extract page HTML, text, or DOM - -## Requirements - -- Firefox browser -- claude-firefox-lsp server installed -- Firefox started with `-marionette` flag - -## Extension Settings - -- `claude-firefox-lsp.serverPath`: Path to claude-firefox-lsp executable -- `claude-firefox-lsp.trace.server`: LSP trace level (off, messages, verbose) - -## Usage - -1. Start Firefox with Marionette: - ```bash - firefox -marionette -headless - ``` - -2. Open VS Code command palette (Ctrl+Shift+P) - -3. Run commands: - - `Firefox: Navigate to URL` - - `Firefox: Click Element` - - `Firefox: Type Text` - - `Firefox: Take Screenshot` - - `Firefox: Execute JavaScript` - - `Firefox: Get Page Content` - -## Development - -```bash -# Install dependencies -npm install - -# Compile TypeScript -npm run compile - -# Package extension -npm run package -``` - -## License - -SPDX-License-Identifier: PMPL-1.0-or-later - -Copyright (c) 2026 Jonathan D.A. Jewell +# Claude Firefox LSP - VS Code Extension + +VS Code extension for Firefox browser automation via Language Server Protocol. + +## Features + +- **Navigate**: Load URLs in Firefox +- **Click**: Click elements by CSS selector or XPath +- **Type Text**: Enter text into form fields +- **Screenshot**: Capture page screenshots +- **Execute JavaScript**: Run custom JavaScript in page context +- **Get Content**: Extract page HTML, text, or DOM + +## Requirements + +- Firefox browser +- claude-firefox-lsp server installed +- Firefox started with `-marionette` flag + +## Extension Settings + +- `claude-firefox-lsp.serverPath`: Path to claude-firefox-lsp executable +- `claude-firefox-lsp.trace.server`: LSP trace level (off, messages, verbose) + +## Usage + +1. Start Firefox with Marionette: + ```bash + firefox -marionette -headless + ``` + +2. Open VS Code command palette (Ctrl+Shift+P) + +3. Run commands: + - `Firefox: Navigate to URL` + - `Firefox: Click Element` + - `Firefox: Type Text` + - `Firefox: Take Screenshot` + - `Firefox: Execute JavaScript` + - `Firefox: Get Page Content` + +## Development + +```bash +# Install dependencies +npm install + +# Compile TypeScript +npm run compile + +# Package extension +npm run package +``` + +## License + +SPDX-License-Identifier: PMPL-1.0-or-later + +Copyright (c) 2026 Jonathan D.A. Jewell diff --git a/firefox-lsp/vscode-extension/package-lock.json b/firefox-lsp/vscode-extension/package-lock.json deleted file mode 100644 index 88334cc..0000000 --- a/firefox-lsp/vscode-extension/package-lock.json +++ /dev/null @@ -1,3095 +0,0 @@ -{ - "name": "claude-firefox-lsp", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "claude-firefox-lsp", - "version": "0.1.0", - "license": "PMPL-1.0-or-later", - "dependencies": { - "vscode-languageclient": "^9.0.0" - }, - "devDependencies": { - "@types/node": "^20.0.0", - "@types/vscode": "^1.75.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.0.0", - "typescript": "^5.0.0", - "vsce": "^2.15.0" - }, - "engines": { - "vscode": "^1.75.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "20.19.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.31.tgz", - "integrity": "sha512-5jsi0wpncvTD33Sh1UCgacK37FFwDn+EG7wCmEvs62fCvBL+n8/76cAYDok21NF6+jaVWIqKwCZyX7Vbu8eB3A==", - "dev": true, - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", - "dev": true - }, - "node_modules/@types/vscode": { - "version": "1.109.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.109.0.tgz", - "integrity": "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw==", - "dev": true - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", - "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/azure-devops-node-api": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz", - "integrity": "sha512-XdiGPhrpaT5J8wdERRKs5g8E0Zy1pvOYTli7z9E8nmOn3YGp4FhtjhrOyFmX/8veWCwdI69mCHKJw6l+4J/bHA==", - "dev": true, - "dependencies": { - "tunnel": "0.0.6", - "typed-rest-client": "^1.8.4" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cheerio": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", - "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", - "dev": true, - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "encoding-sniffer": "^0.2.1", - "htmlparser2": "^10.1.0", - "parse5": "^7.3.0", - "parse5-htmlparser2-tree-adapter": "^7.1.0", - "parse5-parser-stream": "^7.1.2", - "undici": "^7.19.0", - "whatwg-mimetype": "^4.0.0" - }, - "engines": { - "node": ">=20.18.1" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "dev": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dev": true, - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/encoding-sniffer": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", - "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", - "dev": true, - "dependencies": { - "iconv-lite": "^0.6.3", - "whatwg-encoding": "^3.1.1" - }, - "funding": { - "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/htmlparser2": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", - "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "entities": "^7.0.1" - } - }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/keytar": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", - "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "node-addon-api": "^4.3.0", - "prebuild-install": "^7.0.1" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", - "dev": true, - "dependencies": { - "uc.micro": "^1.0.1" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/markdown-it": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", - "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - }, - "bin": { - "markdown-it": "bin/markdown-it.js" - } - }, - "node_modules/markdown-it/node_modules/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", - "dev": true, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/node-abi": { - "version": "3.87.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", - "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", - "dev": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", - "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", - "dev": true - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-semver": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", - "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", - "dev": true, - "dependencies": { - "semver": "^5.1.0" - } - }, - "node_modules/parse-semver/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", - "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", - "dev": true, - "dependencies": { - "domhandler": "^5.0.3", - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-parser-stream": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", - "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", - "dev": true, - "dependencies": { - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "dev": true, - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", - "dev": true, - "dependencies": { - "mute-stream": "~0.0.4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "node_modules/sax": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", - "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", - "dev": true, - "engines": { - "node": ">=11.0.0" - } - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "dev": true, - "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-rest-client": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", - "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", - "dev": true, - "dependencies": { - "qs": "^6.9.1", - "tunnel": "0.0.6", - "underscore": "^1.12.1" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", - "dev": true - }, - "node_modules/underscore": { - "version": "1.13.7", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", - "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", - "dev": true - }, - "node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", - "dev": true - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "node_modules/vsce": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/vsce/-/vsce-2.15.0.tgz", - "integrity": "sha512-P8E9LAZvBCQnoGoizw65JfGvyMqNGlHdlUXD1VAuxtvYAaHBKLBdKPnpy60XKVDAkQCfmMu53g+gq9FM+ydepw==", - "deprecated": "vsce has been renamed to @vscode/vsce. Install using @vscode/vsce instead.", - "dev": true, - "dependencies": { - "azure-devops-node-api": "^11.0.1", - "chalk": "^2.4.2", - "cheerio": "^1.0.0-rc.9", - "commander": "^6.1.0", - "glob": "^7.0.6", - "hosted-git-info": "^4.0.2", - "keytar": "^7.7.0", - "leven": "^3.1.0", - "markdown-it": "^12.3.2", - "mime": "^1.3.4", - "minimatch": "^3.0.3", - "parse-semver": "^1.1.1", - "read": "^1.0.7", - "semver": "^5.1.0", - "tmp": "^0.2.1", - "typed-rest-client": "^1.8.4", - "url-join": "^4.0.1", - "xml2js": "^0.4.23", - "yauzl": "^2.3.1", - "yazl": "^2.2.2" - }, - "bin": { - "vsce": "vsce" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/vsce/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/vsce/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/vsce/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/vsce/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/vsce/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/vsce/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/vsce/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/vsce/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/vsce/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/vsce/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/vscode-languageclient": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", - "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", - "dependencies": { - "minimatch": "^5.1.0", - "semver": "^7.3.7", - "vscode-languageserver-protocol": "3.17.5" - }, - "engines": { - "vscode": "^1.82.0" - } - }, - "node_modules/vscode-languageclient/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" - }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", - "dev": true, - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "dev": true, - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yazl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", - "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/firefox-lsp/vscode-extension/package.json b/firefox-lsp/vscode-extension/package.json index 4da24de..5e95fb1 100644 --- a/firefox-lsp/vscode-extension/package.json +++ b/firefox-lsp/vscode-extension/package.json @@ -1,102 +1,102 @@ -{ - "name": "claude-firefox-lsp", - "displayName": "Claude Firefox LSP", - "description": "Firefox browser automation via Language Server Protocol", - "version": "0.1.0", - "publisher": "hyperpolymath", - "license": "PMPL-1.0-or-later", - "author": "Jonathan D.A. Jewell ", - "repository": { - "type": "git", - "url": "https://github.com/hyperpolymath/claude-firefox-lsp" - }, - "engines": { - "vscode": "^1.75.0" - }, - "categories": [ - "Testing", - "Other" - ], - "activationEvents": [ - "onCommand:claude-firefox-lsp.navigate", - "onCommand:claude-firefox-lsp.click", - "onCommand:claude-firefox-lsp.typeText", - "onCommand:claude-firefox-lsp.screenshot", - "onCommand:claude-firefox-lsp.executeJs", - "onCommand:claude-firefox-lsp.getContent", - "onCommand:claude-firefox-lsp.detectBrowsers" - ], - "main": "./out/extension.js", - "contributes": { - "commands": [ - { - "command": "claude-firefox-lsp.navigate", - "title": "Firefox: Navigate to URL" - }, - { - "command": "claude-firefox-lsp.click", - "title": "Firefox: Click Element" - }, - { - "command": "claude-firefox-lsp.typeText", - "title": "Firefox: Type Text" - }, - { - "command": "claude-firefox-lsp.screenshot", - "title": "Firefox: Take Screenshot" - }, - { - "command": "claude-firefox-lsp.executeJs", - "title": "Firefox: Execute JavaScript" - }, - { - "command": "claude-firefox-lsp.getContent", - "title": "Firefox: Get Page Content" - }, - { - "command": "claude-firefox-lsp.detectBrowsers", - "title": "Firefox: Detect Available Browsers" - } - ], - "configuration": { - "type": "object", - "title": "Claude Firefox LSP", - "properties": { - "claude-firefox-lsp.serverPath": { - "type": "string", - "default": "claude-firefox-lsp", - "description": "Path to the claude-firefox-lsp executable" - }, - "claude-firefox-lsp.trace.server": { - "type": "string", - "enum": [ - "off", - "messages", - "verbose" - ], - "default": "off", - "description": "Traces the communication between VS Code and the language server" - } - } - } - }, - "scripts": { - "vscode:prepublish": "npm run compile", - "compile": "tsc -p ./", - "watch": "tsc -watch -p ./", - "lint": "eslint src --ext ts", - "package": "vsce package" - }, - "devDependencies": { - "@types/node": "^20.0.0", - "@types/vscode": "^1.75.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.0.0", - "typescript": "^5.0.0", - "vsce": "^2.15.0" - }, - "dependencies": { - "vscode-languageclient": "^9.0.0" - } -} +{ + "name": "claude-firefox-lsp", + "displayName": "Claude Firefox LSP", + "description": "Firefox browser automation via Language Server Protocol", + "version": "0.1.0", + "publisher": "hyperpolymath", + "license": "PMPL-1.0-or-later", + "author": "Jonathan D.A. Jewell ", + "repository": { + "type": "git", + "url": "https://github.com/hyperpolymath/claude-firefox-lsp" + }, + "engines": { + "vscode": "^1.75.0" + }, + "categories": [ + "Testing", + "Other" + ], + "activationEvents": [ + "onCommand:claude-firefox-lsp.navigate", + "onCommand:claude-firefox-lsp.click", + "onCommand:claude-firefox-lsp.typeText", + "onCommand:claude-firefox-lsp.screenshot", + "onCommand:claude-firefox-lsp.executeJs", + "onCommand:claude-firefox-lsp.getContent", + "onCommand:claude-firefox-lsp.detectBrowsers" + ], + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "claude-firefox-lsp.navigate", + "title": "Firefox: Navigate to URL" + }, + { + "command": "claude-firefox-lsp.click", + "title": "Firefox: Click Element" + }, + { + "command": "claude-firefox-lsp.typeText", + "title": "Firefox: Type Text" + }, + { + "command": "claude-firefox-lsp.screenshot", + "title": "Firefox: Take Screenshot" + }, + { + "command": "claude-firefox-lsp.executeJs", + "title": "Firefox: Execute JavaScript" + }, + { + "command": "claude-firefox-lsp.getContent", + "title": "Firefox: Get Page Content" + }, + { + "command": "claude-firefox-lsp.detectBrowsers", + "title": "Firefox: Detect Available Browsers" + } + ], + "configuration": { + "type": "object", + "title": "Claude Firefox LSP", + "properties": { + "claude-firefox-lsp.serverPath": { + "type": "string", + "default": "claude-firefox-lsp", + "description": "Path to the claude-firefox-lsp executable" + }, + "claude-firefox-lsp.trace.server": { + "type": "string", + "enum": [ + "off", + "messages", + "verbose" + ], + "default": "off", + "description": "Traces the communication between VS Code and the language server" + } + } + } + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "lint": "eslint src --ext ts", + "package": "vsce package" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/vscode": "^1.75.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.0.0", + "typescript": "^5.0.0", + "vsce": "^2.15.0" + }, + "dependencies": { + "vscode-languageclient": "^9.0.0" + } +} diff --git a/firefox-lsp/vscode-extension/rescript.json b/firefox-lsp/vscode-extension/rescript.json index 1556854..2ec94ad 100644 --- a/firefox-lsp/vscode-extension/rescript.json +++ b/firefox-lsp/vscode-extension/rescript.json @@ -1,11 +1,11 @@ -{ - "name": "claude-firefox-lsp-extension", - "sources": [{"dir": "src", "subdirs": true}], - "package-specs": [{"module": "commonjs", "in-source": true}], - "suffix": ".res.js", - "bs-dependencies": ["@rescript/core"], - "warnings": { - "error": "+101+5" - }, - "bsc-flags": ["-bs-super-errors"] -} +{ + "name": "claude-firefox-lsp-extension", + "sources": [{"dir": "src", "subdirs": true}], + "package-specs": [{"module": "commonjs", "in-source": true}], + "suffix": ".res.js", + "bs-dependencies": ["@rescript/core"], + "warnings": { + "error": "+101+5" + }, + "bsc-flags": ["-bs-super-errors"] +} diff --git a/firefox-lsp/vscode-extension/src/Extension.res b/firefox-lsp/vscode-extension/src/Extension.res index aeae748..9bc26ba 100644 --- a/firefox-lsp/vscode-extension/src/Extension.res +++ b/firefox-lsp/vscode-extension/src/Extension.res @@ -1,291 +1,291 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// -// Claude Firefox LSP - VS Code Extension -// Registers commands for Firefox browser control (navigate, click, type, -// screenshot, executeJs, getContent, detectBrowsers) and manages the -// language client lifecycle connecting to the claude-firefox-lsp server. - -open Vscode - -// --- Module-level state --- - -let client: ref> = ref(None) - -// --- Helper to push a disposable into the extension context --- - -let pushSubscription = (context: ExtensionContext.t, disposable: Disposable.t): unit => { - let subs = ExtensionContext.subscriptions(context) - let _ = Js.Array2.push(subs, disposable) -} - -// --- Activate --- - -// Called by VS Code when the extension is activated. -// Sets up the language client and registers all browser control commands. -let activate = (context: ExtensionContext.t): unit => { - Console.log("Claude Firefox LSP extension activated") - - // Read server path from configuration - let config = Workspace.getConfiguration("claude-firefox-lsp") - let serverPath = WorkspaceConfiguration.get(config, "serverPath", "claude-firefox-lsp") - - let serverOptions: ServerOptions.t = { - command: serverPath, - args: [], - } - - let clientOptions: LanguageClientOptions.t = { - documentSelector: [], - synchronize: { - fileEvents: Workspace.createFileSystemWatcher("**/*"), - }, - } - - let lc = LanguageClient.make( - "claude-firefox-lsp", - "Claude Firefox LSP", - serverOptions, - clientOptions, - ) - - client := Some(lc) - - // --- Navigate command --- - pushSubscription( - context, - Commands.registerCommand("claude-firefox-lsp.navigate", async () => { - let urlResult = await Window.showInputBox({ - prompt: "Enter URL to navigate to", - placeHolder: "https://example.com", - }) - - switch Js.Nullable.toOption(urlResult) { - | Some(url) => { - let _ = await LanguageClient.sendRequest( - lc, - "workspace/executeCommand", - { - command: "firefox.navigate", - arguments: [Obj.magic(url)], - }, - ) - let _ = await Window.showInformationMessage(`Navigated to ${url}`) - } - | None => () - } - }), - ) - - // --- Click command --- - pushSubscription( - context, - Commands.registerCommand("claude-firefox-lsp.click", async () => { - let selectorResult = await Window.showInputBox({ - prompt: "Enter CSS selector or XPath", - placeHolder: "#submit-button", - }) - - switch Js.Nullable.toOption(selectorResult) { - | Some(selector) => { - let _ = await LanguageClient.sendRequest( - lc, - "workspace/executeCommand", - { - command: "firefox.click", - arguments: [Obj.magic(selector)], - }, - ) - let _ = await Window.showInformationMessage(`Clicked element: ${selector}`) - } - | None => () - } - }), - ) - - // --- Type text command --- - pushSubscription( - context, - Commands.registerCommand("claude-firefox-lsp.typeText", async () => { - let selectorResult = await Window.showInputBox({ - prompt: "Enter CSS selector or XPath", - placeHolder: "#search-input", - }) - - switch Js.Nullable.toOption(selectorResult) { - | Some(selector) => { - let textResult = await Window.showInputBox({ - prompt: "Enter text to type", - placeHolder: "Hello, world!", - }) - - switch Js.Nullable.toOption(textResult) { - | Some(text) => { - let _ = await LanguageClient.sendRequest( - lc, - "workspace/executeCommand", - { - command: "firefox.typeText", - arguments: [Obj.magic(selector), Obj.magic(text)], - }, - ) - let _ = await Window.showInformationMessage(`Typed text into: ${selector}`) - } - | None => () - } - } - | None => () - } - }), - ) - - // --- Screenshot command --- - pushSubscription( - context, - Commands.registerCommand("claude-firefox-lsp.screenshot", async () => { - let result = await LanguageClient.sendRequest( - lc, - "workspace/executeCommand", - { - command: "firefox.screenshot", - arguments: [], - }, - ) - - // Check if the result contains a screenshot field - let resultDict: option> = Obj.magic(Js.Nullable.toOption(Obj.magic(result))) - switch resultDict { - | Some(d) => - switch Dict.get(d, "screenshot") { - | Some(_screenshot) => - let _ = await Window.showInformationMessage("Screenshot captured") - // TODO: Display or save screenshot - | None => () - } - | None => () - } - }), - ) - - // --- Execute JS command --- - pushSubscription( - context, - Commands.registerCommand("claude-firefox-lsp.executeJs", async () => { - let jsResult = await Window.showInputBox({ - prompt: "Enter JavaScript to execute", - placeHolder: "return document.title;", - }) - - switch Js.Nullable.toOption(jsResult) { - | Some(javascript) => { - let result = await LanguageClient.sendRequest( - lc, - "workspace/executeCommand", - { - command: "firefox.executeJs", - arguments: [Obj.magic(javascript)], - }, - ) - let resultStr = Js.Json.stringify(result) - let _ = await Window.showInformationMessage(`Result: ${resultStr}`) - } - | None => () - } - }), - ) - - // --- Get content command --- - pushSubscription( - context, - Commands.registerCommand("claude-firefox-lsp.getContent", async () => { - let formatResult = await Window.showQuickPick( - ["html", "text", "dom"], - {"placeHolder": "Select content format"}, - ) - - switch Js.Nullable.toOption(formatResult) { - | Some(format) => { - let result = await LanguageClient.sendRequest( - lc, - "workspace/executeCommand", - { - command: "firefox.getContent", - arguments: [Obj.magic({"format": format})], - }, - ) - - let resultDict: option> = Obj.magic( - Js.Nullable.toOption(Obj.magic(result)), - ) - switch resultDict { - | Some(d) => - switch Dict.get(d, "content") { - | Some(content) => { - let contentStr: string = Obj.magic(content) - let language = if format === "html" { - "html" - } else { - "plaintext" - } - let doc = await Workspace.openTextDocument({ - "content": contentStr, - "language": language, - }) - let _ = await Window.showTextDocument(doc) - } - | None => () - } - | None => () - } - } - | None => () - } - }), - ) - - // --- Detect browsers command --- - pushSubscription( - context, - Commands.registerCommand("claude-firefox-lsp.detectBrowsers", async () => { - let result = await LanguageClient.sendRequest( - lc, - "workspace/executeCommand", - { - command: "firefox.detectBrowsers", - arguments: [], - }, - ) - - let resultDict: option> = Obj.magic( - Js.Nullable.toOption(Obj.magic(result)), - ) - switch resultDict { - | Some(d) => - switch Dict.get(d, "browsers") { - | Some(browsers) => { - let browserArr: array<(string, {"name": string})> = Obj.magic(browsers) - let names = Array.map(browserArr, ((_, b)) => b["name"]) - let joined = Array.join(names, ", ") - let _ = await Window.showInformationMessage(`Available browsers: ${joined}`) - } - | None => () - } - | None => () - } - }), - ) - - // Start the language client - let _ = LanguageClient.start(lc) -} - -// --- Deactivate --- - -// Called by VS Code when the extension is deactivated. -// Stops the language client if it is running. -let deactivate = (): option> => { - switch client.contents { - | Some(lc) => Some(LanguageClient.stop(lc)) - | None => None - } -} +// SPDX-License-Identifier: PMPL-1.0-or-later +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// +// Claude Firefox LSP - VS Code Extension +// Registers commands for Firefox browser control (navigate, click, type, +// screenshot, executeJs, getContent, detectBrowsers) and manages the +// language client lifecycle connecting to the claude-firefox-lsp server. + +open Vscode + +// --- Module-level state --- + +let client: ref> = ref(None) + +// --- Helper to push a disposable into the extension context --- + +let pushSubscription = (context: ExtensionContext.t, disposable: Disposable.t): unit => { + let subs = ExtensionContext.subscriptions(context) + let _ = Js.Array2.push(subs, disposable) +} + +// --- Activate --- + +// Called by VS Code when the extension is activated. +// Sets up the language client and registers all browser control commands. +let activate = (context: ExtensionContext.t): unit => { + Console.log("Claude Firefox LSP extension activated") + + // Read server path from configuration + let config = Workspace.getConfiguration("claude-firefox-lsp") + let serverPath = WorkspaceConfiguration.get(config, "serverPath", "claude-firefox-lsp") + + let serverOptions: ServerOptions.t = { + command: serverPath, + args: [], + } + + let clientOptions: LanguageClientOptions.t = { + documentSelector: [], + synchronize: { + fileEvents: Workspace.createFileSystemWatcher("**/*"), + }, + } + + let lc = LanguageClient.make( + "claude-firefox-lsp", + "Claude Firefox LSP", + serverOptions, + clientOptions, + ) + + client := Some(lc) + + // --- Navigate command --- + pushSubscription( + context, + Commands.registerCommand("claude-firefox-lsp.navigate", async () => { + let urlResult = await Window.showInputBox({ + prompt: "Enter URL to navigate to", + placeHolder: "https://example.com", + }) + + switch Js.Nullable.toOption(urlResult) { + | Some(url) => { + let _ = await LanguageClient.sendRequest( + lc, + "workspace/executeCommand", + { + command: "firefox.navigate", + arguments: [Obj.magic(url)], + }, + ) + let _ = await Window.showInformationMessage(`Navigated to ${url}`) + } + | None => () + } + }), + ) + + // --- Click command --- + pushSubscription( + context, + Commands.registerCommand("claude-firefox-lsp.click", async () => { + let selectorResult = await Window.showInputBox({ + prompt: "Enter CSS selector or XPath", + placeHolder: "#submit-button", + }) + + switch Js.Nullable.toOption(selectorResult) { + | Some(selector) => { + let _ = await LanguageClient.sendRequest( + lc, + "workspace/executeCommand", + { + command: "firefox.click", + arguments: [Obj.magic(selector)], + }, + ) + let _ = await Window.showInformationMessage(`Clicked element: ${selector}`) + } + | None => () + } + }), + ) + + // --- Type text command --- + pushSubscription( + context, + Commands.registerCommand("claude-firefox-lsp.typeText", async () => { + let selectorResult = await Window.showInputBox({ + prompt: "Enter CSS selector or XPath", + placeHolder: "#search-input", + }) + + switch Js.Nullable.toOption(selectorResult) { + | Some(selector) => { + let textResult = await Window.showInputBox({ + prompt: "Enter text to type", + placeHolder: "Hello, world!", + }) + + switch Js.Nullable.toOption(textResult) { + | Some(text) => { + let _ = await LanguageClient.sendRequest( + lc, + "workspace/executeCommand", + { + command: "firefox.typeText", + arguments: [Obj.magic(selector), Obj.magic(text)], + }, + ) + let _ = await Window.showInformationMessage(`Typed text into: ${selector}`) + } + | None => () + } + } + | None => () + } + }), + ) + + // --- Screenshot command --- + pushSubscription( + context, + Commands.registerCommand("claude-firefox-lsp.screenshot", async () => { + let result = await LanguageClient.sendRequest( + lc, + "workspace/executeCommand", + { + command: "firefox.screenshot", + arguments: [], + }, + ) + + // Check if the result contains a screenshot field + let resultDict: option> = Obj.magic(Js.Nullable.toOption(Obj.magic(result))) + switch resultDict { + | Some(d) => + switch Dict.get(d, "screenshot") { + | Some(_screenshot) => + let _ = await Window.showInformationMessage("Screenshot captured") + // TODO: Display or save screenshot + | None => () + } + | None => () + } + }), + ) + + // --- Execute JS command --- + pushSubscription( + context, + Commands.registerCommand("claude-firefox-lsp.executeJs", async () => { + let jsResult = await Window.showInputBox({ + prompt: "Enter JavaScript to execute", + placeHolder: "return document.title;", + }) + + switch Js.Nullable.toOption(jsResult) { + | Some(javascript) => { + let result = await LanguageClient.sendRequest( + lc, + "workspace/executeCommand", + { + command: "firefox.executeJs", + arguments: [Obj.magic(javascript)], + }, + ) + let resultStr = Js.Json.stringify(result) + let _ = await Window.showInformationMessage(`Result: ${resultStr}`) + } + | None => () + } + }), + ) + + // --- Get content command --- + pushSubscription( + context, + Commands.registerCommand("claude-firefox-lsp.getContent", async () => { + let formatResult = await Window.showQuickPick( + ["html", "text", "dom"], + {"placeHolder": "Select content format"}, + ) + + switch Js.Nullable.toOption(formatResult) { + | Some(format) => { + let result = await LanguageClient.sendRequest( + lc, + "workspace/executeCommand", + { + command: "firefox.getContent", + arguments: [Obj.magic({"format": format})], + }, + ) + + let resultDict: option> = Obj.magic( + Js.Nullable.toOption(Obj.magic(result)), + ) + switch resultDict { + | Some(d) => + switch Dict.get(d, "content") { + | Some(content) => { + let contentStr: string = Obj.magic(content) + let language = if format === "html" { + "html" + } else { + "plaintext" + } + let doc = await Workspace.openTextDocument({ + "content": contentStr, + "language": language, + }) + let _ = await Window.showTextDocument(doc) + } + | None => () + } + | None => () + } + } + | None => () + } + }), + ) + + // --- Detect browsers command --- + pushSubscription( + context, + Commands.registerCommand("claude-firefox-lsp.detectBrowsers", async () => { + let result = await LanguageClient.sendRequest( + lc, + "workspace/executeCommand", + { + command: "firefox.detectBrowsers", + arguments: [], + }, + ) + + let resultDict: option> = Obj.magic( + Js.Nullable.toOption(Obj.magic(result)), + ) + switch resultDict { + | Some(d) => + switch Dict.get(d, "browsers") { + | Some(browsers) => { + let browserArr: array<(string, {"name": string})> = Obj.magic(browsers) + let names = Array.map(browserArr, ((_, b)) => b["name"]) + let joined = Array.join(names, ", ") + let _ = await Window.showInformationMessage(`Available browsers: ${joined}`) + } + | None => () + } + | None => () + } + }), + ) + + // Start the language client + let _ = LanguageClient.start(lc) +} + +// --- Deactivate --- + +// Called by VS Code when the extension is deactivated. +// Stops the language client if it is running. +let deactivate = (): option> => { + switch client.contents { + | Some(lc) => Some(LanguageClient.stop(lc)) + | None => None + } +} diff --git a/firefox-lsp/vscode-extension/src/bindings/Vscode.res b/firefox-lsp/vscode-extension/src/bindings/Vscode.res index d15e911..bc91fda 100644 --- a/firefox-lsp/vscode-extension/src/bindings/Vscode.res +++ b/firefox-lsp/vscode-extension/src/bindings/Vscode.res @@ -1,132 +1,132 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// -// FFI bindings for the VS Code extension API and vscode-languageclient. -// Used by the Claude Firefox LSP extension to register commands, -// manage the language client lifecycle, and interact with the VS Code UI. - -// --- Core VSCode types --- - -module Disposable = { - type t -} - -module Uri = { - type t - - @module("vscode") @scope("Uri") - external parse: string => t = "parse" -} - -// --- ExtensionContext --- - -module ExtensionContext = { - type t - - @get external subscriptions: t => array = "subscriptions" -} - -// --- Workspace --- - -module WorkspaceConfiguration = { - type t - - @send external get: (t, string, string) => string = "get" -} - -module FileSystemWatcher = { - type t -} - -module TextDocument = { - type t -} - -module Workspace = { - @module("vscode") @scope("workspace") - external getConfiguration: string => WorkspaceConfiguration.t = "getConfiguration" - - @module("vscode") @scope("workspace") - external createFileSystemWatcher: string => FileSystemWatcher.t = "createFileSystemWatcher" - - @module("vscode") @scope("workspace") - external openTextDocument: {"content": string, "language": string} => promise = - "openTextDocument" -} - -// --- Window --- - -module InputBoxOptions = { - type t = { - prompt: string, - placeHolder: string, - } -} - -module Window = { - @module("vscode") @scope("window") - external showInputBox: InputBoxOptions.t => promise> = "showInputBox" - - @module("vscode") @scope("window") - external showInformationMessage: string => promise = "showInformationMessage" - - @module("vscode") @scope("window") - external showQuickPick: ( - array, - {"placeHolder": string}, - ) => promise> = "showQuickPick" - - @module("vscode") @scope("window") - external showTextDocument: TextDocument.t => promise = "showTextDocument" -} - -// --- Commands --- - -module Commands = { - @module("vscode") @scope("commands") - external registerCommand: (string, unit => promise) => Disposable.t = "registerCommand" -} - -// --- LanguageClient (from vscode-languageclient/node) --- - -module ServerOptions = { - type t = { - command: string, - args: array, - } -} - -module LanguageClientOptions = { - type documentSelector - - type synchronize = {fileEvents: FileSystemWatcher.t} - - type t = { - documentSelector: array, - synchronize: synchronize, - } -} - -module ExecuteCommandParams = { - type t = { - command: string, - arguments: array, - } -} - -module LanguageClient = { - type t - - @new @module("vscode-languageclient/node") - external make: ( - string, - string, - ServerOptions.t, - LanguageClientOptions.t, - ) => t = "LanguageClient" - - @send external start: t => promise = "start" - @send external stop: t => promise = "stop" - @send external sendRequest: (t, string, ExecuteCommandParams.t) => promise = - "sendRequest" -} +// SPDX-License-Identifier: PMPL-1.0-or-later +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// +// FFI bindings for the VS Code extension API and vscode-languageclient. +// Used by the Claude Firefox LSP extension to register commands, +// manage the language client lifecycle, and interact with the VS Code UI. + +// --- Core VSCode types --- + +module Disposable = { + type t +} + +module Uri = { + type t + + @module("vscode") @scope("Uri") + external parse: string => t = "parse" +} + +// --- ExtensionContext --- + +module ExtensionContext = { + type t + + @get external subscriptions: t => array = "subscriptions" +} + +// --- Workspace --- + +module WorkspaceConfiguration = { + type t + + @send external get: (t, string, string) => string = "get" +} + +module FileSystemWatcher = { + type t +} + +module TextDocument = { + type t +} + +module Workspace = { + @module("vscode") @scope("workspace") + external getConfiguration: string => WorkspaceConfiguration.t = "getConfiguration" + + @module("vscode") @scope("workspace") + external createFileSystemWatcher: string => FileSystemWatcher.t = "createFileSystemWatcher" + + @module("vscode") @scope("workspace") + external openTextDocument: {"content": string, "language": string} => promise = + "openTextDocument" +} + +// --- Window --- + +module InputBoxOptions = { + type t = { + prompt: string, + placeHolder: string, + } +} + +module Window = { + @module("vscode") @scope("window") + external showInputBox: InputBoxOptions.t => promise> = "showInputBox" + + @module("vscode") @scope("window") + external showInformationMessage: string => promise = "showInformationMessage" + + @module("vscode") @scope("window") + external showQuickPick: ( + array, + {"placeHolder": string}, + ) => promise> = "showQuickPick" + + @module("vscode") @scope("window") + external showTextDocument: TextDocument.t => promise = "showTextDocument" +} + +// --- Commands --- + +module Commands = { + @module("vscode") @scope("commands") + external registerCommand: (string, unit => promise) => Disposable.t = "registerCommand" +} + +// --- LanguageClient (from vscode-languageclient/node) --- + +module ServerOptions = { + type t = { + command: string, + args: array, + } +} + +module LanguageClientOptions = { + type documentSelector + + type synchronize = {fileEvents: FileSystemWatcher.t} + + type t = { + documentSelector: array, + synchronize: synchronize, + } +} + +module ExecuteCommandParams = { + type t = { + command: string, + arguments: array, + } +} + +module LanguageClient = { + type t + + @new @module("vscode-languageclient/node") + external make: ( + string, + string, + ServerOptions.t, + LanguageClientOptions.t, + ) => t = "LanguageClient" + + @send external start: t => promise = "start" + @send external stop: t => promise = "stop" + @send external sendRequest: (t, string, ExecuteCommandParams.t) => promise = + "sendRequest" +} diff --git a/firefox-mcp/LICENSE b/firefox-mcp/LICENSE deleted file mode 100644 index d2dccf5..0000000 --- a/firefox-mcp/LICENSE +++ /dev/null @@ -1,153 +0,0 @@ -SPDX-License-Identifier: PMPL-1.0-or-later -SPDX-FileCopyrightText: 2024-2025 Palimpsest Stewardship Council - -================================================================================ -PALIMPSEST-MPL LICENSE VERSION 1.0 -================================================================================ - -File-level copyleft with ethical use and quantum-safe provenance - -Based on Mozilla Public License 2.0 - --------------------------------------------------------------------------------- -PREAMBLE --------------------------------------------------------------------------------- - -This License extends the Mozilla Public License 2.0 (MPL-2.0) with provisions -for ethical use, post-quantum cryptographic provenance, and emotional lineage -protection. The base MPL-2.0 terms apply except where explicitly modified by -the Exhibits below. - -Like a palimpsest manuscript where each layer builds upon what came before, -this license recognizes that creative works carry history, context, and meaning -that transcend mere code or text. - --------------------------------------------------------------------------------- -SECTION 1: BASE LICENSE --------------------------------------------------------------------------------- - -This License incorporates the full text of Mozilla Public License 2.0 by -reference. The complete MPL-2.0 text is available at: -https://www.mozilla.org/en-US/MPL/2.0/ - -All terms, conditions, and definitions from MPL-2.0 apply except where -explicitly modified by the Exhibits in this License. - --------------------------------------------------------------------------------- -SECTION 2: ADDITIONAL DEFINITIONS --------------------------------------------------------------------------------- - -2.1. "Emotional Lineage" - means the narrative, cultural, symbolic, and contextual meaning embedded - in Covered Software, including but not limited to: protest traditions, - cultural heritage, trauma narratives, and community stories. - -2.2. "Provenance Metadata" - means cryptographically signed attribution information attached to or - associated with Covered Software, including author identities, timestamps, - modification history, and lineage references. - -2.3. "Non-Interpretive System" - means any automated system that processes Covered Software without - preserving or considering its Emotional Lineage, including but not - limited to: AI training pipelines, content aggregators, and automated - summarization tools. - -2.4. "Quantum-Safe Signature" - means a cryptographic signature using algorithms resistant to attacks - by quantum computers, as specified in Exhibit B. - --------------------------------------------------------------------------------- -SECTION 3: ETHICAL USE REQUIREMENTS --------------------------------------------------------------------------------- - -In addition to the rights and obligations under MPL-2.0: - -3.1. Emotional Lineage Preservation - You must make reasonable efforts to preserve and communicate the - Emotional Lineage of Covered Software when distributing or creating - derivative works. This includes maintaining narrative context, cultural - attributions, and symbolic meaning where documented. - -3.2. Non-Interpretive System Notice - If You use Covered Software as input to a Non-Interpretive System, You - must: - (a) document such use in a publicly accessible manner; and - (b) not claim that outputs of such systems carry the Emotional Lineage - of the original work without explicit permission from Contributors. - -3.3. Ethical Use Declaration - Commercial use of Covered Software requires acknowledgment that You have - read and understood Exhibit A (Ethical Use Guidelines) and agree to act - in good faith accordance with its principles. - -See Exhibit A for complete Ethical Use Guidelines. - --------------------------------------------------------------------------------- -SECTION 4: PROVENANCE REQUIREMENTS --------------------------------------------------------------------------------- - -4.1. Metadata Preservation - You must not strip, alter, or obscure Provenance Metadata from Covered - Software except where technically necessary and with clear documentation - of any changes. - -4.2. Quantum-Safe Provenance (Optional) - Contributors may sign their Contributions using Quantum-Safe Signatures. - If Quantum-Safe Signatures are present, You must preserve them in all - distributions. - -4.3. Lineage Chain - When creating derivative works, You should extend the provenance chain - to include Your own contributions, maintaining cryptographic linkage to - prior Contributors where feasible. - -See Exhibit B for Quantum-Safe Provenance specifications. - --------------------------------------------------------------------------------- -SECTION 5: GOVERNANCE --------------------------------------------------------------------------------- - -5.1. Stewardship Council - This License is maintained by the Palimpsest Stewardship Council, which - may issue clarifications, interpretive guidance, and future versions. - -5.2. Version Selection - You may use Covered Software under this version of the License or any - later version published by the Palimpsest Stewardship Council. - -5.3. Dispute Resolution - Disputes regarding interpretation of Ethical Use Requirements (Section 3) - should first be submitted to the Palimpsest Stewardship Council for - non-binding guidance before pursuing legal remedies. - --------------------------------------------------------------------------------- -SECTION 6: COMPATIBILITY --------------------------------------------------------------------------------- - -6.1. MPL-2.0 Compatibility - Covered Software under this License may be combined with software under - MPL-2.0. The combined work must comply with both licenses. - -6.2. Secondary Licenses - The Secondary License provisions of MPL-2.0 Section 3.3 apply to this - License. - --------------------------------------------------------------------------------- -EXHIBITS --------------------------------------------------------------------------------- - -Exhibit A - Ethical Use Guidelines -Exhibit B - Quantum-Safe Provenance Specification - -See separate files: -- EXHIBIT-A-ETHICAL-USE.txt -- EXHIBIT-B-QUANTUM-SAFE.txt - --------------------------------------------------------------------------------- -END OF PALIMPSEST-MPL LICENSE VERSION 1.0 --------------------------------------------------------------------------------- - -For questions about this License: -- Repository: https://github.com/hyperpolymath/palimpsest-license -- Council: contact via repository Issues diff --git a/gitlab-bridge/.gitignore b/gitlab-bridge/.gitignore index 7131853..c9139a3 100644 --- a/gitlab-bridge/.gitignore +++ b/gitlab-bridge/.gitignore @@ -40,6 +40,11 @@ erl_crash.dump # ReScript /lib/ /.bsb.lock +# `in-source: true` in rescript.json puts compiled JS alongside .res +# sources; those are build artefacts, not tracked. +*.res.js +*.resi.js +*.res.mjs # Python (SaltStack only) __pycache__/ diff --git a/gitlab-bridge/rescript.json b/gitlab-bridge/rescript.json index c7d2d19..a85a41c 100644 --- a/gitlab-bridge/rescript.json +++ b/gitlab-bridge/rescript.json @@ -1,6 +1,5 @@ { "name": "claude-gitlab-bridge", - "version": "2.0.0", "sources": [ { "dir": "src", @@ -13,7 +12,7 @@ } ], "package-specs": { - "module": "es6", + "module": "esmodule", "in-source": true }, "suffix": ".res.js", diff --git a/gitlab-bridge/src/auth/Errors.res b/gitlab-bridge/src/auth/Errors.res index 413b0c1..d73cef4 100644 --- a/gitlab-bridge/src/auth/Errors.res +++ b/gitlab-bridge/src/auth/Errors.res @@ -46,7 +46,7 @@ let insufficientScopeError = ( requiredScopes->Array.filter(scope => !(availableScopes->Array.includes(scope))) { name: "InsufficientScopeError", - message: `Missing required scopes: ${missing->Array.joinWith(", ")}`, + message: `Missing required scopes: ${missing->Array.join(", ")}`, code: "INSUFFICIENT_SCOPE", statusCode: 403, } @@ -57,7 +57,7 @@ exception DangerousScope(array) let dangerousScopeError = (~dangerousScopes: array): authError => { name: "DangerousScopeError", - message: `Token contains dangerous scopes: ${dangerousScopes->Array.joinWith(", ")}. These scopes are not allowed for security reasons.`, + message: `Token contains dangerous scopes: ${dangerousScopes->Array.join(", ")}. These scopes are not allowed for security reasons.`, code: "DANGEROUS_SCOPE", statusCode: 403, } @@ -112,18 +112,21 @@ let userBlockedError = (~userId: int, ~state: string): authError => { statusCode: 403, } -// Helper to convert error to JSON for API responses -let errorToJSON = (error: authError): JSON.t => { - open JSON - object_( +// Helper to convert error to JSON for API responses. +// In ReScript 12 / @rescript/core, the JSON constructor helpers +// (`string`, `number`, `object`, …) live under `JSON.Encode`, and +// `object_` was renamed to `object`. Calls are fully qualified +// rather than via `open JSON.Encode` to avoid shadowing the +// top-level `float` identifier used elsewhere. +let errorToJSON = (error: authError): JSON.t => + JSON.Encode.object( Dict.fromArray([ - ("name", string(error.name)), - ("message", string(error.message)), - ("code", string(error.code)), - ("statusCode", number(error.statusCode->Int.toFloat)), + ("name", JSON.Encode.string(error.name)), + ("message", JSON.Encode.string(error.message)), + ("code", JSON.Encode.string(error.code)), + ("statusCode", JSON.Encode.int(error.statusCode)), ]), ) -} // Helper to check if error is retryable let isRetryable = (error: authError): bool => diff --git a/gitlab-bridge/src/auth/Middleware.res b/gitlab-bridge/src/auth/Middleware.res index 0f41b3b..e177574 100644 --- a/gitlab-bridge/src/auth/Middleware.res +++ b/gitlab-bridge/src/auth/Middleware.res @@ -77,10 +77,10 @@ let createAuthMiddleware = (~config: authMiddlewareConfig=defaultAuthConfig): Ex res ->Express.status(error.statusCode) ->Express.json( - JSON.object_( + JSON.Encode.object( Dict.fromArray([ - ("error", JSON.string(error.code)), - ("message", JSON.string(error.message)), + ("error", JSON.Encode.string(error.code)), + ("message", JSON.Encode.string(error.message)), ]), ), ) @@ -92,10 +92,10 @@ let createAuthMiddleware = (~config: authMiddlewareConfig=defaultAuthConfig): Ex res ->Express.status(error.statusCode) ->Express.json( - JSON.object_( + JSON.Encode.object( Dict.fromArray([ - ("error", JSON.string(error.code)), - ("message", JSON.string(error.message)), + ("error", JSON.Encode.string(error.code)), + ("message", JSON.Encode.string(error.message)), ]), ), ) @@ -151,10 +151,10 @@ let createWebhookMiddleware = (~secret: string): Express.middleware => { res ->Express.status(error.statusCode) ->Express.json( - JSON.object_( + JSON.Encode.object( Dict.fromArray([ - ("error", JSON.string(error.code)), - ("message", JSON.string(error.message)), + ("error", JSON.Encode.string(error.code)), + ("message", JSON.Encode.string(error.message)), ]), ), ) @@ -173,7 +173,7 @@ let createWebhookMiddleware = (~secret: string): Express.middleware => { * Verifies the request has an auth context with sufficient scopes. */ let requirePermissionMiddleware = (~operation: string): Express.middleware => { - (req, res, next) => { + (_req, res, next) => { try { // In a real implementation, auth context would be attached to req // For now, we validate the operation exists @@ -193,10 +193,10 @@ let requirePermissionMiddleware = (~operation: string): Express.middleware => { res ->Express.status(error.statusCode) ->Express.json( - JSON.object_( + JSON.Encode.object( Dict.fromArray([ - ("error", JSON.string(error.code)), - ("message", JSON.string(error.message)), + ("error", JSON.Encode.string(error.code)), + ("message", JSON.Encode.string(error.message)), ]), ), ) @@ -211,10 +211,10 @@ let requirePermissionMiddleware = (~operation: string): Express.middleware => { res ->Express.status(error.statusCode) ->Express.json( - JSON.object_( + JSON.Encode.object( Dict.fromArray([ - ("error", JSON.string(error.code)), - ("message", JSON.string(error.message)), + ("error", JSON.Encode.string(error.code)), + ("message", JSON.Encode.string(error.message)), ]), ), ) @@ -225,10 +225,10 @@ let requirePermissionMiddleware = (~operation: string): Express.middleware => { res ->Express.status(error.statusCode) ->Express.json( - JSON.object_( + JSON.Encode.object( Dict.fromArray([ - ("error", JSON.string(error.code)), - ("message", JSON.string(error.message)), + ("error", JSON.Encode.string(error.code)), + ("message", JSON.Encode.string(error.message)), ]), ), ) @@ -244,16 +244,16 @@ let requirePermissionMiddleware = (~operation: string): Express.middleware => { */ let authErrorHandler: Express.errorMiddleware = (error, _req, res, next) => { // Check if we can extract error info - switch error->Js.Exn.message { + switch error->JsExn.message { | Some(msg) if msg->String.includes("AUTH") || msg->String.includes("TOKEN") => { let _ = res ->Express.status(401) ->Express.json( - JSON.object_( + JSON.Encode.object( Dict.fromArray([ - ("error", JSON.string("AUTH_ERROR")), - ("message", JSON.string(msg)), + ("error", JSON.Encode.string("AUTH_ERROR")), + ("message", JSON.Encode.string(msg)), ]), ), ) diff --git a/gitlab-bridge/src/auth/PermissionChecker.res b/gitlab-bridge/src/auth/PermissionChecker.res index 445fd94..234dae3 100644 --- a/gitlab-bridge/src/auth/PermissionChecker.res +++ b/gitlab-bridge/src/auth/PermissionChecker.res @@ -216,7 +216,8 @@ let getRequiredScopesForOperations = (operations: array): arrayArray.flat - ->Array.toSet + // ReScript 12: `Array.toSet` was removed; dedup via `Set.fromArray`. + ->Set.fromArray ->Set.toArray } diff --git a/gitlab-bridge/src/auth/RateLimiter.res b/gitlab-bridge/src/auth/RateLimiter.res index e7961f3..d506a1f 100644 --- a/gitlab-bridge/src/auth/RateLimiter.res +++ b/gitlab-bridge/src/auth/RateLimiter.res @@ -62,7 +62,9 @@ let make = (~config: rateLimitConfig=defaultConfig): rateLimiter => { let now = Date.now() let expireTime = config.windowMs->Int.toFloat *. 2.0 - limiter.store->Map.forEach((record, key) => { + // ReScript 12: `Map.forEach` now takes a single-argument callback + // (value only). Use `forEachWithKey` for the (value, key) form. + limiter.store->Map.forEachWithKey((record, key) => { if now -. record.lastAccess > expireTime { let _ = limiter.store->Map.delete(key) } diff --git a/gitlab-bridge/src/auth/WebhookValidator.res b/gitlab-bridge/src/auth/WebhookValidator.res index 09bb472..f53351d 100644 --- a/gitlab-bridge/src/auth/WebhookValidator.res +++ b/gitlab-bridge/src/auth/WebhookValidator.res @@ -220,8 +220,10 @@ let validateSecretStrength = (secret: string): secretStrengthResult => { let _ = issues->Array.push("Secret should contain mixed character types") } - // Check for repeated character - let repeatedChar = %re("/^(.)\1+$/") + // Check for repeated character. The `\1` backreference is escaped + // as `\\1` so ReScript 12's stricter string-literal lexer accepts + // it; the resulting JS regex is unchanged. + let repeatedChar = %re("/^(.)\\1+$/") if repeatedChar->RegExp.test(secret) { let _ = issues->Array.push("Secret should not be a repeated character") } diff --git a/gitlab-bridge/src/bindings/Express.res b/gitlab-bridge/src/bindings/Express.res index c352f3e..0f88ad3 100644 --- a/gitlab-bridge/src/bindings/Express.res +++ b/gitlab-bridge/src/bindings/Express.res @@ -10,14 +10,18 @@ type request type response type nextFunction = unit => unit type middleware = (request, response, nextFunction) => unit -type errorMiddleware = (Js.Exn.t, request, response, nextFunction) => unit +type errorMiddleware = (JsExn.t, request, response, nextFunction) => unit type app // Create Express app @module("express") external make: unit => app = "default" // Request methods -@get external getHeader: (request, string) => option = "get" +// `getHeader` is a method call (`req.get("Header-Name")`), not a property +// read, so it uses `@send`, not `@get`. ReScript 12's stricter +// `@get`-attribute checking rejected the prior `@get` form (which had +// two arguments, only valid for `@send`). +@send external getHeader: (request, string) => option = "get" @get external getBody: request => JSON.t = "body" @get external getParams: request => JSON.t = "params" @get external getQuery: request => JSON.t = "query" @@ -52,8 +56,13 @@ let sendSuccess = (res: response, ~data: JSON.t, ~statusCode=200, ()): response // Helper to create JSON error response let sendError = (res: response, ~message: string, ~statusCode=500, ()): response => { - let error = JSON.object_( - Dict.fromArray([("error", JSON.string(message)), ("status", JSON.number(statusCode->Int.toFloat))]), + // ReScript 12 / @rescript/core: JSON helpers moved to JSON.Encode; + // `object_` → `object`; no top-level `number` (use `int`/`float`). + let error = JSON.Encode.object( + Dict.fromArray([ + ("error", JSON.Encode.string(message)), + ("status", JSON.Encode.int(statusCode)), + ]), ) res->status(statusCode)->json(error) } diff --git a/gitlab-bridge/src/config/MultiRepo.res b/gitlab-bridge/src/config/MultiRepo.res index 7eb6c25..74310df 100644 --- a/gitlab-bridge/src/config/MultiRepo.res +++ b/gitlab-bridge/src/config/MultiRepo.res @@ -7,7 +7,9 @@ */ // Repository configuration -type repoConfig = { +// `type rec` required for the `and repoFeatures` chain below +// (ReScript 12 enforces explicit `rec` for mutually recursive types). +type rec repoConfig = { id: string, projectId: string, gitlabUrl: string, @@ -85,17 +87,17 @@ let validateRepoConfig = (config: repoConfig): result => { let parseRepoConfig = (json: JSON.t): result => { try { // Simplified parsing - real implementation would use proper JSON decoder - let obj = json->JSON.Decode.object->Option.getExn + let obj = json->JSON.Decode.object->Option.getOrThrow let id = obj ->Dict.get("id") ->Option.flatMap(JSON.Decode.string) - ->Option.getExn + ->Option.getOrThrow let projectId = obj ->Dict.get("projectId") ->Option.flatMap(JSON.Decode.string) - ->Option.getExn + ->Option.getOrThrow let gitlabUrl = obj ->Dict.get("gitlabUrl") @@ -105,7 +107,7 @@ let parseRepoConfig = (json: JSON.t): result => { let name = obj ->Dict.get("name") ->Option.flatMap(JSON.Decode.string) - ->Option.getExn + ->Option.getOrThrow let enabled = obj ->Dict.get("enabled") @@ -137,11 +139,17 @@ let parseRepoConfig = (json: JSON.t): result => { // Load multiple repositories from JSON let parseMultiRepoConfig = (json: JSON.t): result => { try { - let obj = json->JSON.Decode.object->Option.getExn + // ReScript 12 / @rescript/core changes used here: + // * `Option.getOrThrow` → `Option.getOrThrow` + // * `Array.keepSome` no longer takes a transform; it just + // filters `array>` → `array<_>`. The combined + // filter+map pattern is now `Array.filterMap`. + let obj = json->JSON.Decode.object->Option.getOrThrow - let reposJson = obj->Dict.get("repositories")->Option.flatMap(JSON.Decode.array)->Option.getExn + let reposJson = + obj->Dict.get("repositories")->Option.flatMap(JSON.Decode.array)->Option.getOrThrow - let repos = reposJson->Array.map(parseRepoConfig)->Array.keepSome((result) => + let repos = reposJson->Array.map(parseRepoConfig)->Array.filterMap(result => switch result { | Ok(config) => Some(config) | Error(_) => None @@ -184,9 +192,11 @@ let getStats = (registry: repoRegistry): repoStats => { let all = registry.repos->Dict.valuesToArray let enabled = all->Array.filter(r => r.enabled) let disabled = all->Array.filter(r => !r.enabled) + // ReScript 12: `Array.toSet` was removed; use `Set.fromArray` + // directly to deduplicate the URL array. let urls = all ->Array.map(r => r.gitlabUrl) - ->Array.toSet + ->Set.fromArray ->Set.toArray { @@ -200,21 +210,21 @@ let getStats = (registry: repoRegistry): repoStats => { // Export configuration to JSON let exportConfig = (config: multiRepoConfig): JSON.t => { let repos = config.repositories->Array.map(repo => { - JSON.object_( + JSON.Encode.object( Dict.fromArray([ - ("id", JSON.string(repo.id)), - ("projectId", JSON.string(repo.projectId)), - ("gitlabUrl", JSON.string(repo.gitlabUrl)), - ("name", JSON.string(repo.name)), - ("enabled", JSON.bool(repo.enabled)), + ("id", JSON.Encode.string(repo.id)), + ("projectId", JSON.Encode.string(repo.projectId)), + ("gitlabUrl", JSON.Encode.string(repo.gitlabUrl)), + ("name", JSON.Encode.string(repo.name)), + ("enabled", JSON.Encode.bool(repo.enabled)), ]), ) }) - JSON.object_( + JSON.Encode.object( Dict.fromArray([ - ("repositories", JSON.array(repos)), - ("defaultGitlabUrl", JSON.string(config.defaultGitlabUrl)), + ("repositories", JSON.Encode.array(repos)), + ("defaultGitlabUrl", JSON.Encode.string(config.defaultGitlabUrl)), ]), ) } diff --git a/gitlab-bridge/src/forges/ForgeAdapter.res b/gitlab-bridge/src/forges/ForgeAdapter.res index d54d4a3..5b7c8fb 100644 --- a/gitlab-bridge/src/forges/ForgeAdapter.res +++ b/gitlab-bridge/src/forges/ForgeAdapter.res @@ -17,13 +17,19 @@ type forgeType = | @as("codeberg") Codeberg | @as("forgejo") Forgejo -// Common issue type -type issue = { +// Common issue type. `type rec` (not `type`) is required because the +// `and user = ...` chain below makes the declaration mutually +// recursive — ReScript 12 enforces the `rec` keyword that earlier +// versions accepted implicitly. +type rec issue = { id: int, iid: int, // Internal ID (per-project) title: string, description: string, - state: [#open | #closed], + // `open` is reserved in ReScript 12 (module open syntax). The + // variant tag is quoted so the JSON wire format ("open") is + // preserved. + state: [#"open" | #closed], labels: array, author: user, assignees: array, @@ -46,7 +52,7 @@ type mergeRequest = { iid: int, title: string, description: string, - state: [#open | #merged | #closed], + state: [#"open" | #merged | #closed], sourceBranch: string, targetBranch: string, author: user, @@ -65,6 +71,9 @@ type repository = { fullName: string, description: option, defaultBranch: string, + // `private` is a reserved keyword in ReScript 12, so the variant + // tag is quoted. JSON serialization remains "private" — no + // wire-format change. visibility: [#public | #"private" | #internal], url: string, cloneUrl: string, diff --git a/gitlab-bridge/src/services/MRReviewer.res b/gitlab-bridge/src/services/MRReviewer.res index 29cbd2b..d581917 100644 --- a/gitlab-bridge/src/services/MRReviewer.res +++ b/gitlab-bridge/src/services/MRReviewer.res @@ -66,10 +66,12 @@ let parseDiff = (diffText: string): array => { let diffLines = lines ->Array.filter(line => { let trimmed = line->String.trim + // ReScript 12: prefix `!` binds tighter than `->`, so each + // `!x->String.startsWith(…)` must be parenthesised. trimmed->String.length > 0 && - !trimmed->String.startsWith("---") && - !trimmed->String.startsWith("+++") && - !trimmed->String.startsWith("@@") + !(trimmed->String.startsWith("---")) && + !(trimmed->String.startsWith("+++")) && + !(trimmed->String.startsWith("@@")) }) ->Array.mapWithIndex((line, idx) => { let changeType = if line->String.startsWith("+") { diff --git a/gitlab-bridge/src/templates/PromptTemplate.res b/gitlab-bridge/src/templates/PromptTemplate.res index 8b24292..34aa591 100644 --- a/gitlab-bridge/src/templates/PromptTemplate.res +++ b/gitlab-bridge/src/templates/PromptTemplate.res @@ -12,8 +12,10 @@ type templateVar = { value: string, } -// Template metadata -type templateMetadata = { +// Template metadata. `type rec` required for the `and templateCategory` +// chain below (ReScript 12 enforces explicit `rec` for mutually +// recursive type declarations). +type rec templateMetadata = { name: string, description: string, author: option, @@ -29,8 +31,9 @@ and templateCategory = | @as("testing") Testing | @as("custom") Custom -// Prompt template -type promptTemplate = { +// Prompt template. `type rec` required for the `and templateExample` +// chain below. +type rec promptTemplate = { metadata: templateMetadata, systemPrompt: option, userPromptTemplate: string, @@ -68,7 +71,10 @@ let getTemplate = (registry: templateRegistry, name: string): option => { // Find all {{variable}} patterns - let pattern = makeTokenPattern("\\{\\{([^}]+)\\}\\}") + // ReScript 12: use `RegExp.fromString` from `@rescript/core` rather + // than `TokenValidator.makeTokenPattern`, which is a private external + // not exported by that module. + let _pattern = RegExp.fromString("\\{\\{([^}]+)\\}\\}") let matches: array = [] // Simplified - real implementation would use proper regex matching @@ -228,12 +234,15 @@ let render = ( variables: array, ): result<(option, string), string> => { // Validate all required variables are provided + // ReScript 12: prefix `!` binds tighter than the `->` pipe, so + // `!variables->Array.some(...)` parsed as `(!variables)->...` and + // tried to negate an array. Parenthesise the pipeline result. let missingVars = template.variables->Array.filter(varName => - !variables->Array.some(v => v.name == varName) + !(variables->Array.some(v => v.name == varName)) ) if missingVars->Array.length > 0 { - Error(`Missing required variables: ${missingVars->Array.joinWith(", ")}`) + Error(`Missing required variables: ${missingVars->Array.join(", ")}`) } else { let userPrompt = substituteVariables(template.userPromptTemplate, variables) Ok((template.systemPrompt, userPrompt)) @@ -250,18 +259,18 @@ let parseTemplate = (json: JSON.t): result => { } } -// Export template to JSON -let exportTemplate = (template: promptTemplate): JSON.t => { - JSON.object_( +// Export template to JSON. ReScript 12 / @rescript/core: JSON +// helpers moved to `JSON.Encode`; `object_` → `object`. +let exportTemplate = (template: promptTemplate): JSON.t => + JSON.Encode.object( Dict.fromArray([ - ("name", JSON.string(template.metadata.name)), - ("description", JSON.string(template.metadata.description)), - ("version", JSON.string(template.metadata.version)), - ("userPromptTemplate", JSON.string(template.userPromptTemplate)), + ("name", JSON.Encode.string(template.metadata.name)), + ("description", JSON.Encode.string(template.metadata.description)), + ("version", JSON.Encode.string(template.metadata.version)), + ("userPromptTemplate", JSON.Encode.string(template.userPromptTemplate)), ( "variables", - JSON.array(template.variables->Array.map(v => JSON.string(v))), + JSON.Encode.array(template.variables->Array.map(v => JSON.Encode.string(v))), ), ]), ) -} diff --git a/gitlab-bridge/tests/Setup.res b/gitlab-bridge/tests/Setup.res index 08541df..efd3c9e 100644 --- a/gitlab-bridge/tests/Setup.res +++ b/gitlab-bridge/tests/Setup.res @@ -6,11 +6,19 @@ * Clears cached environment variables before each test suite. */ -// Env-var deletion is done entirely via %%raw below; no ReScript externals -// are needed. The previously-declared @val and @set externals on -// process.env were unused (no caller in this file or anywhere else) and -// the @set form was incompatible with rescript@12's tightened external -// validation, so they have been removed. +// External bindings for reading process.env. Setters are not needed +// here — the `%%raw` block below uses JS `delete` directly to clear +// env vars, which is what tests actually call. (The previous version +// also declared `@set @scope(("process", "env")) external …` setters +// for each variable; those were unused and rejected by ReScript 12's +// stricter handling of `@set` + nested `@scope`. Removing them fixes +// the build break flagged in PR #10's "out of scope" notes.) +@val @scope(("process", "env")) external gitlabToken: option = "GITLAB_TOKEN" +@val @scope(("process", "env")) external gitlabUrl: option = "GITLAB_URL" +@val @scope(("process", "env")) external anthropicApiKey: option = "ANTHROPIC_API_KEY" +@val @scope(("process", "env")) external webhookSecret: option = "WEBHOOK_SECRET" + +// We use JS interop to delete env vars %%raw(` import { beforeEach } from "vitest"; diff --git a/gitlab-bridge/tests/unit/auth/RateLimiterTest.res b/gitlab-bridge/tests/unit/auth/RateLimiterTest.res index ec0b2d9..2ed1db6 100644 --- a/gitlab-bridge/tests/unit/auth/RateLimiterTest.res +++ b/gitlab-bridge/tests/unit/auth/RateLimiterTest.res @@ -196,7 +196,9 @@ describe("rate-limiter", () => { }) it("should have valid configurations", () => { - RateLimiter.presets->Dict.forEachWithKey((_name, preset) => { + // ReScript 12: `Dict.forEachWithKey` callback is `(value, key)`, + // so the preset record (value) comes first; the name is the key. + RateLimiter.presets->Dict.forEachWithKey((preset, _name) => { expect(preset.limit)->toBeGreaterThan(0) expect(preset.windowMs)->toBeGreaterThan(0) }) diff --git a/mozilla-extension/LICENSE b/mozilla-extension/LICENSE deleted file mode 100644 index d2dccf5..0000000 --- a/mozilla-extension/LICENSE +++ /dev/null @@ -1,153 +0,0 @@ -SPDX-License-Identifier: PMPL-1.0-or-later -SPDX-FileCopyrightText: 2024-2025 Palimpsest Stewardship Council - -================================================================================ -PALIMPSEST-MPL LICENSE VERSION 1.0 -================================================================================ - -File-level copyleft with ethical use and quantum-safe provenance - -Based on Mozilla Public License 2.0 - --------------------------------------------------------------------------------- -PREAMBLE --------------------------------------------------------------------------------- - -This License extends the Mozilla Public License 2.0 (MPL-2.0) with provisions -for ethical use, post-quantum cryptographic provenance, and emotional lineage -protection. The base MPL-2.0 terms apply except where explicitly modified by -the Exhibits below. - -Like a palimpsest manuscript where each layer builds upon what came before, -this license recognizes that creative works carry history, context, and meaning -that transcend mere code or text. - --------------------------------------------------------------------------------- -SECTION 1: BASE LICENSE --------------------------------------------------------------------------------- - -This License incorporates the full text of Mozilla Public License 2.0 by -reference. The complete MPL-2.0 text is available at: -https://www.mozilla.org/en-US/MPL/2.0/ - -All terms, conditions, and definitions from MPL-2.0 apply except where -explicitly modified by the Exhibits in this License. - --------------------------------------------------------------------------------- -SECTION 2: ADDITIONAL DEFINITIONS --------------------------------------------------------------------------------- - -2.1. "Emotional Lineage" - means the narrative, cultural, symbolic, and contextual meaning embedded - in Covered Software, including but not limited to: protest traditions, - cultural heritage, trauma narratives, and community stories. - -2.2. "Provenance Metadata" - means cryptographically signed attribution information attached to or - associated with Covered Software, including author identities, timestamps, - modification history, and lineage references. - -2.3. "Non-Interpretive System" - means any automated system that processes Covered Software without - preserving or considering its Emotional Lineage, including but not - limited to: AI training pipelines, content aggregators, and automated - summarization tools. - -2.4. "Quantum-Safe Signature" - means a cryptographic signature using algorithms resistant to attacks - by quantum computers, as specified in Exhibit B. - --------------------------------------------------------------------------------- -SECTION 3: ETHICAL USE REQUIREMENTS --------------------------------------------------------------------------------- - -In addition to the rights and obligations under MPL-2.0: - -3.1. Emotional Lineage Preservation - You must make reasonable efforts to preserve and communicate the - Emotional Lineage of Covered Software when distributing or creating - derivative works. This includes maintaining narrative context, cultural - attributions, and symbolic meaning where documented. - -3.2. Non-Interpretive System Notice - If You use Covered Software as input to a Non-Interpretive System, You - must: - (a) document such use in a publicly accessible manner; and - (b) not claim that outputs of such systems carry the Emotional Lineage - of the original work without explicit permission from Contributors. - -3.3. Ethical Use Declaration - Commercial use of Covered Software requires acknowledgment that You have - read and understood Exhibit A (Ethical Use Guidelines) and agree to act - in good faith accordance with its principles. - -See Exhibit A for complete Ethical Use Guidelines. - --------------------------------------------------------------------------------- -SECTION 4: PROVENANCE REQUIREMENTS --------------------------------------------------------------------------------- - -4.1. Metadata Preservation - You must not strip, alter, or obscure Provenance Metadata from Covered - Software except where technically necessary and with clear documentation - of any changes. - -4.2. Quantum-Safe Provenance (Optional) - Contributors may sign their Contributions using Quantum-Safe Signatures. - If Quantum-Safe Signatures are present, You must preserve them in all - distributions. - -4.3. Lineage Chain - When creating derivative works, You should extend the provenance chain - to include Your own contributions, maintaining cryptographic linkage to - prior Contributors where feasible. - -See Exhibit B for Quantum-Safe Provenance specifications. - --------------------------------------------------------------------------------- -SECTION 5: GOVERNANCE --------------------------------------------------------------------------------- - -5.1. Stewardship Council - This License is maintained by the Palimpsest Stewardship Council, which - may issue clarifications, interpretive guidance, and future versions. - -5.2. Version Selection - You may use Covered Software under this version of the License or any - later version published by the Palimpsest Stewardship Council. - -5.3. Dispute Resolution - Disputes regarding interpretation of Ethical Use Requirements (Section 3) - should first be submitted to the Palimpsest Stewardship Council for - non-binding guidance before pursuing legal remedies. - --------------------------------------------------------------------------------- -SECTION 6: COMPATIBILITY --------------------------------------------------------------------------------- - -6.1. MPL-2.0 Compatibility - Covered Software under this License may be combined with software under - MPL-2.0. The combined work must comply with both licenses. - -6.2. Secondary Licenses - The Secondary License provisions of MPL-2.0 Section 3.3 apply to this - License. - --------------------------------------------------------------------------------- -EXHIBITS --------------------------------------------------------------------------------- - -Exhibit A - Ethical Use Guidelines -Exhibit B - Quantum-Safe Provenance Specification - -See separate files: -- EXHIBIT-A-ETHICAL-USE.txt -- EXHIBIT-B-QUANTUM-SAFE.txt - --------------------------------------------------------------------------------- -END OF PALIMPSEST-MPL LICENSE VERSION 1.0 --------------------------------------------------------------------------------- - -For questions about this License: -- Repository: https://github.com/hyperpolymath/palimpsest-license -- Council: contact via repository Issues