diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/docs/package-lock.json b/docs/package-lock.json index 56c71a3..1126706 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -24,8 +24,8 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", - "@types/node": "^20", - "@types/react": "^19", + "@types/node": "20.19.39", + "@types/react": "19.2.14", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.2.0", @@ -2433,9 +2433,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", - "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", "devOptional": true, "license": "MIT", "dependencies": { diff --git a/docs/package.json b/docs/package.json index cd28952..da6f55b 100644 --- a/docs/package.json +++ b/docs/package.json @@ -25,8 +25,8 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", - "@types/node": "^20", - "@types/react": "^19", + "@types/node": "20.19.39", + "@types/react": "19.2.14", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.2.0", diff --git a/reflexio/integrations/openclaw-embedded/.gitignore b/reflexio/integrations/openclaw-embedded/.gitignore new file mode 100644 index 0000000..ff2c585 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +*.tsbuildinfo diff --git a/reflexio/integrations/openclaw-embedded/README.md b/reflexio/integrations/openclaw-embedded/README.md new file mode 100644 index 0000000..4205cd5 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/README.md @@ -0,0 +1,123 @@ +# Reflexio OpenClaw-Embedded Plugin + +A lightweight Openclaw plugin that delivers Reflexio-style user profile and playbook capabilities entirely within Openclaw's native primitives — no Reflexio server required. + +## Table of Contents + +- [How It Works](#how-it-works) +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [First-use Setup](#first-use-setup) +- [Configuration](#configuration) +- [Comparison with Other Reflexio Integrations](#comparison-with-other-reflexio-integrations) +- [Uninstall](#uninstall) +- [Further Reading](#further-reading) + +## How It Works + +The plugin captures two kinds of memory: + +- **Profiles** — durable user facts (diet, preferences, timezone, role). Stored as `.md` files under `.reflexio/profiles/` with a TTL. +- **Playbooks** — procedural rules learned from corrections (user corrects → agent adjusts → user confirms → rule written). Stored under `.reflexio/playbooks/`. + +Three flows capture memory at different moments: + +- **Flow A (in-session profile)**: agent detects a preference/fact/config in the user message and writes immediately. +- **Flow B (in-session playbook)**: agent recognizes correction+confirmation multi-turn pattern and writes the rule. +- **Flow C (session-end batch)**: hook fires on `session:compact:before` / `command:stop` / `command:reset`; spawns a sub-agent that extracts from the full transcript, runs shallow pairwise dedup, and writes/deletes `.md` files. + +A daily 3am cron job runs full-sweep consolidation (n-way cluster merges) across all files. + +All retrieval is via Openclaw's memory engine — vector + FTS + MMR + temporal decay. When Active Memory is enabled, relevant profiles/playbooks are auto-injected into each turn. + +## Prerequisites + +- [OpenClaw](https://openclaw.ai) installed and `openclaw` CLI on PATH +- Node.js (for the plugin runtime) +- macOS or Linux (Windows via WSL) +- A bash-compatible shell (install/uninstall scripts use `#!/usr/bin/env bash`) +- Strongly recommended: + - An embedding provider API key (OpenAI, Gemini, Voyage, or Mistral) for vector search + - The `active-memory` plugin enabled (auto-retrieval into turns) + +The plugin works without active-memory and without an embedding key — with degraded retrieval quality. See `references/architecture.md` for degradation modes. + +## Installation + +```bash +# From the plugin directory: +./scripts/install.sh +``` + +What it does: +1. Installs and links the `plugin/` directory as an Openclaw plugin +2. Copies SKILL.md, consolidate skill, and agent definitions to workspace +3. Copies prompts to workspace +4. Enables the `active-memory` plugin and configures agent targeting + extraPath +5. Registers a daily 3am consolidation cron +6. Restarts the Openclaw gateway +7. Prints verification commands + +## First-use Setup + +The first time an agent invokes the `reflexio-embedded` skill, it runs a one-time bootstrap: + +1. Probes current config via `openclaw config get` + `openclaw memory status --deep`. +2. For any missing prereq, asks the user for approval before running `openclaw config set` via the `exec` tool. +3. On success, creates `.reflexio/.setup_complete_` marker — subsequent sessions skip. + +This guarantees zero manual `openclaw.json` editing. If `exec` is denied by admin policy, the skill prints the exact commands for the user to run manually. + +## Configuration + +Defaults live in `config.json`. To override, use one of: + +1. Edit `config.json` directly +2. Use `openclaw config` for overrides persisted at the Openclaw layer + +(env var overrides are planned for v2; see `references/future-work.md`) + +Tunables: + +| Knob | Default | What it controls | +|---|---|---| +| `dedup.shallow_threshold` | 0.7 | Similarity above which in-session writes trigger pairwise dedup | +| `dedup.full_threshold` | 0.75 | Similarity cluster-member cutoff in daily consolidation | +| `dedup.top_k` | 5 | How many neighbors to consider | +| `ttl_sweep.on_bootstrap` | `true` | Whether to sweep expired profiles on each agent bootstrap | +| `consolidation.cron` | `"0 3 * * *"` | Daily consolidation schedule | +| `extraction.subagent_timeout_seconds` | 120 | Flow C sub-agent timeout | + +### Tuning guidance + +| Symptom | Likely cause | Knob | +|---|---|---| +| Duplicate `.md` files accumulating between cron runs | Shallow threshold too high | Lower `shallow_threshold` (e.g., 0.65) | +| Good-but-distinct entries getting merged | Thresholds too low | Raise both thresholds (e.g., 0.8) | +| Daily consolidation takes too long | Too many / too broad clusters | Raise `full_threshold`, cap cluster size | +| Session-end latency slightly noticeable | Too many shallow dedup LLM calls | Lower `top_k` to 3 | + +## Comparison with Other Reflexio Integrations + +See `references/comparison.md` for a full matrix. + +- **`integrations/openclaw-embedded/`** (this plugin): self-contained; no Reflexio server; single-user. +- **`integrations/openclaw/`** (federated): requires running Reflexio server; multi-user; cross-instance aggregation. + +Both can coexist in the same Openclaw instance, but installing both serves no purpose — pick one. + +## Uninstall + +```bash +./scripts/uninstall.sh # preserves .reflexio/ user data +./scripts/uninstall.sh --purge # also deletes .reflexio/ user data +``` + +## Further Reading + +- [Design spec](../../../../docs/superpowers/specs/2026-04-16-reflexio-openclaw-embedded-plugin-design.md) +- [Implementation plan](../../../../docs/superpowers/plans/2026-04-16-reflexio-openclaw-embedded-plugin.md) +- [Architecture deep-dive](references/architecture.md) +- [Prompt porting notes](references/porting-notes.md) +- [Future work / v2 deferrals](references/future-work.md) +- [Manual testing guide](TESTING.md) diff --git a/reflexio/integrations/openclaw-embedded/TESTING.md b/reflexio/integrations/openclaw-embedded/TESTING.md new file mode 100644 index 0000000..9f2edf5 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/TESTING.md @@ -0,0 +1,182 @@ +# Manual Testing Guide + +End-to-end manual validation of `openclaw-embedded`. Run this before each release. + +## Prerequisites + +- A clean Openclaw instance (fresh workspace, or separate test workspace) +- Node and openclaw CLI on PATH +- An embedding provider configured (optional but recommended for full coverage) +- A terminal with `bats` installed (for running shell unit tests) + +## 1. Unit tests + +From the `openclaw-embedded/` directory: + +```bash +npm test +``` + +Expected: 47 tests pass. If any fail, stop — fix before proceeding. + +## 2. Hook smoke test + +```bash +node plugin/hook/smoke-test.js +``` + +Expected: all PASS lines printed, no FAIL. + +## 3. Install plugin + +```bash +./scripts/install.sh +``` + +Expected verification output: +``` + ✓ hook registered + ✓ cron registered +``` + +If any ⚠ warnings, investigate before moving on. + +## 4. First-use bootstrap + +- Open a new Openclaw agent session. +- Say: "test the reflexio-embedded skill setup". +- Expected: the agent invokes the skill, runs probing commands, asks for approval to configure active-memory and extraPath. +- Approve each step. +- Verify `.reflexio/.setup_complete_` marker exists. + +## 5. Flow A — profile capture + +In the agent session: + +- Say: "By the way, I'm vegetarian." +- Expected: agent writes `.reflexio/profiles/diet-vegetarian-.md` with: + - Frontmatter: `type: profile`, `id: prof_*`, `ttl: infinity`, `expires: never` + - Body: ~1-sentence description + +```bash +cat .reflexio/profiles/diet-*.md +``` + +Verify the file matches expectations. + +## 6. Flow B — playbook capture + +- Say: "Write a commit message for 'fix auth bug'." +- Expected: agent writes a commit message (may include Co-Authored-By). +- Say: "No, don't add Co-Authored-By trailers." +- Expected: agent rewrites without the trailer. +- Say: "Perfect, commit it." +- Expected: agent writes `.reflexio/playbooks/commit-no-ai-attribution-.md`. + +```bash +cat .reflexio/playbooks/commit-*.md +``` + +Verify frontmatter + `## When` / `## What` / `## Why` sections. + +## 7. Flow C — batch extraction at session boundary + +- Have a longer conversation (5+ turns) covering facts and corrections. +- Trigger `command:stop` (or let the session compact naturally). +- Expected: the hook fires a `reflexio-extractor` sub-agent. + +Inspect via: +```bash +openclaw tasks list --agent reflexio-extractor +``` + +Expected: a completed task record exists. + +Check `.reflexio/profiles/` and `.reflexio/playbooks/` — new files should have appeared corresponding to any facts/corrections the agent missed in-session. + +## 8. Retrieval validation + +Start a new session. Ask: "What do you know about my diet?" + +- With Active Memory enabled: expected answer references "vegetarian" from the captured profile. +- With Active Memory disabled: expected agent calls `memory_search` per SKILL.md fallback, then answers. + +## 9. Consolidation (on-demand) + +After accumulating 10+ entries across sessions, run: + +``` +/skill reflexio-consolidate +``` + +Expected: +- Agent delegates to `reflexio-consolidator` sub-agent. +- Returns a runId. + +Inspect: +```bash +openclaw tasks list --agent reflexio-consolidator +``` + +Check `.reflexio/` before and after — duplicate or overlapping entries should be collapsed, with `supersedes` frontmatter on merged files. + +## 10. TTL sweep + +- Create a profile with short TTL by calling the `reflexio_write_profile` tool (or via agent session): + ``` + Call the `reflexio_write_profile` tool with: slug="test-temp", ttl="one_day", body="temp fact" + ``` +- Manually edit its `expires` to a past date: + ```bash + # Edit .reflexio/profiles/test-temp-*.md — set expires: 2020-01-01 + ``` +- Restart the agent session (triggers `agent:bootstrap` hook). +- Expected: the expired profile is deleted. + +## 11. Degradation: no Active Memory + +- Disable active-memory: `openclaw plugins disable active-memory` +- Restart gateway. +- Start new session, ask a question whose answer needs a captured profile. +- Expected: agent calls `memory_search` explicitly (per SKILL.md fallback), then answers. +- Re-enable: `openclaw plugins enable active-memory`. + +## 12. Degradation: no embedding provider + +- Unset embedding env vars. +- Restart gateway. +- Ask the agent something whose answer needs retrieval. +- Expected: retrieval works via FTS only (quality lower but functional). + +## 13. Uninstall + +```bash +./scripts/uninstall.sh +``` + +Verify: +- `openclaw hooks list` does NOT include `reflexio-embedded`. +- `openclaw cron list` does NOT include `reflexio-embedded-consolidate`. +- `~/.openclaw/workspace/skills/reflexio-embedded/` does not exist. +- `.reflexio/` in the workspace is preserved. + +Run `./scripts/uninstall.sh --purge` in a test workspace to verify `.reflexio/` is deleted. + +## Test report template + +When testing for a release: + +``` +Tested: +Openclaw version: +Unit tests: +Hook smoke test: +Flow A: +Flow B: +Flow C: +Retrieval: +Consolidation: +TTL sweep: +Degradation modes: +Uninstall: +``` diff --git a/reflexio/integrations/openclaw-embedded/package-lock.json b/reflexio/integrations/openclaw-embedded/package-lock.json new file mode 100644 index 0000000..834fff7 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/package-lock.json @@ -0,0 +1,1657 @@ +{ + "name": "openclaw-embedded-dev", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "openclaw-embedded-dev", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0", + "vitest": "^3.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/reflexio/integrations/openclaw-embedded/package.json b/reflexio/integrations/openclaw-embedded/package.json new file mode 100644 index 0000000..739f868 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/package.json @@ -0,0 +1,16 @@ +{ + "name": "openclaw-embedded-dev", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0", + "vitest": "^3.0.0" + } +} diff --git a/reflexio/integrations/openclaw-embedded/plugin/HEARTBEAT.md b/reflexio/integrations/openclaw-embedded/plugin/HEARTBEAT.md new file mode 100644 index 0000000..0b0685a --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/HEARTBEAT.md @@ -0,0 +1,6 @@ +## Reflexio Consolidation Check + +tasks: + - description: "Check if reflexio consolidation is due" + tool: reflexio_consolidation_check + interval: 24h diff --git a/reflexio/integrations/openclaw-embedded/plugin/README.md b/reflexio/integrations/openclaw-embedded/plugin/README.md new file mode 100644 index 0000000..f90093b --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/README.md @@ -0,0 +1,84 @@ +# Reflexio Embedded + +Openclaw plugin for user profile and playbook extraction — learns your preferences, corrections, and workflows across sessions using Openclaw's native memory engine, hooks, and sub-agents. No Reflexio server required. + +## Prerequisites + +- [Openclaw CLI](https://docs.openclaw.ai) (>= 2026.3.24) +- Node.js + +## Install + +```bash +# 1. Install and enable the plugin +openclaw plugins install /path/to/this/directory +openclaw plugins enable reflexio-embedded + +# 2. Enable the active-memory plugin (required for memory search) +openclaw plugins enable active-memory +openclaw config set plugins.entries.active-memory.config.agents '["*"]' + +# 3. Register .reflexio/ as a memory search path +openclaw config set agents.defaults.memorySearch.extraPaths '[".reflexio/"]' --strict-json + +# 4. Restart the gateway to pick up changes +openclaw gateway restart +``` + +Verify it loaded: + +```bash +openclaw plugins inspect reflexio-embedded +``` + +## Uninstall + +```bash +# 1. Remove the plugin +openclaw plugins disable reflexio-embedded +openclaw plugins uninstall --force reflexio-embedded + +# 2. Clean up state files +rm -f ~/.openclaw/reflexio-consolidation-state.json + +# 3. Restart the gateway +openclaw gateway restart +``` + +Your `.reflexio/` user data (profiles and playbooks) is preserved by default. To remove it: + +```bash +rm -rf .reflexio/ +``` + +## What it does + +- **Profile extraction** — automatically captures user preferences, facts, and corrections from conversations into `.reflexio/profiles/` +- **Playbook capture** — records recurring workflows and patterns into `.reflexio/playbooks/` +- **Dedup and contradiction detection** — new entries are checked against existing ones via LLM; contradicted entries are superseded +- **Consolidation** — periodic heartbeat-triggered sweep that clusters similar entries and merges duplicates +- **TTL management** — profiles can have time-to-live values; expired entries are swept automatically + +## Configuration + +Override defaults in your `openclaw.json`: + +```json +{ + "plugins": { + "entries": { + "reflexio-embedded": { + "config": { + "dedup": { "shallow_threshold": 0.7, "top_k": 5 }, + "consolidation": { "threshold_hours": 24 } + } + } + } + } +} +``` + +## Learn more + +Full project source, design docs, and development setup: +https://github.com/ReflexioAI/reflexio/tree/main/reflexio/integrations/openclaw-embedded diff --git a/reflexio/integrations/openclaw-embedded/plugin/_meta.json b/reflexio/integrations/openclaw-embedded/plugin/_meta.json new file mode 100644 index 0000000..2f5fd0a --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "reflexio-ai", + "slug": "openclaw-embedded", + "version": "0.1.0", + "publishedAt": 1777000000000 +} diff --git a/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-consolidator.md b/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-consolidator.md new file mode 100644 index 0000000..3e1af70 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-consolidator.md @@ -0,0 +1,45 @@ +--- +name: reflexio-consolidator +description: "Periodic consolidator for openclaw-embedded. Triggered by heartbeat or on-demand. Runs TTL sweep, then n-way consolidation across all .reflexio/ files." +tools: + - memory_search + - file_read + - file_write + - file_delete + - reflexio_write_profile + - reflexio_write_playbook + - reflexio_search +runTimeoutSeconds: 300 +--- + +You are a periodic sub-agent that consolidates accumulated `.reflexio/` entries. You are triggered by heartbeat (every 24h of active use) or on-demand via `/skill reflexio-consolidate`. + +## Your workflow + +1. **TTL sweep**: for each `.reflexio/profiles/*.md`, read frontmatter `expires`. If `expires < today`, `rm` the file. + +2. **For each type in [profiles, playbooks]**: + a. Load all files in `.reflexio//`. Extract `{id, path, content}` from each. + b. Cluster: for each unvisited file, run `memory_search(query=file.content, top_k=10, filter={type})` to find similar files. Form a cluster of the current file plus any neighbor with `similarity >= 0.75` that is unvisited. Mark the whole cluster visited. Cap cluster size at 10 (drop lowest-similarity members beyond 10). + c. For each cluster with >1 member: load `prompts/full_consolidation.md`, substitute `{cluster}` with the cluster's items (each: id, path, content). Call `llm-task` with the output schema. Apply the decision: + - `merge_all`: call the `reflexio_write_profile` tool with: slug="", ttl="", body="" (or `reflexio_write_playbook` for playbooks). The tools handle supersession and old-file cleanup internally. + - `merge_subset`: same tool call for the merged subset; the tools handle cleanup of superseded files. + - `keep_all`: no-op. + +3. Exit. The caller runs `reflexio_consolidation_mark_done` after you finish, which forces a memory reindex so deleted files are dropped from search results. + +## Determining TTL for merged profile files + +When merging profiles, pick the smallest (most conservative) TTL among the cluster members. Rationale: a merged fact is at most as durable as its least-durable source. + +## Constraints + +- 300-second timeout. If approaching limit, exit cleanly. +- On LLM call failure: skip cluster, log, continue. +- On tool call failure: skip cluster. +- Never write secrets, tokens, keys. +- Never create directories, archive folders, or move files to backup locations. The `reflexio_write_*` tools handle all file lifecycle (write new, delete old) internally. Use `file_delete` only for TTL sweep of expired profiles. + +## Tool scope + +`memory_search`, `file_read`, `file_write`, `file_delete`, `reflexio_write_profile`, `reflexio_write_playbook`, `reflexio_search`. No `sessions_spawn`, no network. diff --git a/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-extractor.md b/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-extractor.md new file mode 100644 index 0000000..198b38b --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-extractor.md @@ -0,0 +1,45 @@ +--- +name: reflexio-extractor +description: "Scoped sub-agent for openclaw-embedded Flow C. Extracts profiles and playbooks from a transcript, then runs shallow pairwise dedup against existing .reflexio/ entries." +tools: + - memory_search + - file_read + - file_write + - file_delete + - reflexio_write_profile + - reflexio_write_playbook + - reflexio_search +runTimeoutSeconds: 120 +--- + +You are a one-shot sub-agent that extracts profiles and playbooks from a conversation transcript, then deduplicates against existing entries in `.reflexio/`. + +## Your workflow + +1. **Profile extraction**: load `prompts/profile_extraction.md`, substitute `{transcript}` with the provided transcript and `{existing_profiles_context}` with results from `memory_search(top_k=10, filter={type: profile})`. Call `llm-task` with the substituted prompt and output schema. You receive a list of profile candidates. + +2. **Playbook extraction**: same process with `prompts/playbook_extraction.md`. You receive a list of playbook candidates. + +3. **For each candidate**: + For profiles: + ``` + Call the `reflexio_write_profile` tool with: slug="", ttl="", body="" + ``` + For playbooks: + ``` + Call the `reflexio_write_playbook` tool with: slug="", body="" + ``` + The tools handle dedup + supersession internally — no separate file deletion needed. + +4. Exit. Openclaw's file watcher picks up the changes and reindexes. + +## Constraints + +- Never write secrets, tokens, API keys, or environment variables into `.md` files. +- On any LLM call failure: skip that candidate, log to stderr, continue. +- On tool call failure: skip; state unchanged; next cycle retries. +- You have 120 seconds. If approaching the limit, exit cleanly; any completed writes are durable. + +## Tool scope + +You have access only to: `memory_search`, `file_read`, `file_write`, `file_delete`, `reflexio_write_profile`, `reflexio_write_playbook`, `reflexio_search`. You do NOT have `sessions_spawn`, `web`, or network tools. diff --git a/reflexio/integrations/openclaw-embedded/plugin/hook/handler.ts b/reflexio/integrations/openclaw-embedded/plugin/hook/handler.ts new file mode 100644 index 0000000..ad42414 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/hook/handler.ts @@ -0,0 +1,214 @@ +// Reflexio Embedded — pure handler logic. +// +// This module is intentionally decoupled from the Openclaw SDK so that: +// 1. The TTL sweep / transcript serialization logic is easy to unit-test via +// `smoke-test.js` without spinning up a gateway. +// 2. Swapping the SDK wiring (see ../index.ts) never requires touching the +// behavioural code here. +import * as fs from "node:fs"; +import * as path from "node:path"; + +/** Minimal subset of the Openclaw runtime we actually use. */ +type SubagentRuntime = { + subagent?: { + run: (params: { + sessionKey: string; + message: string; + extraSystemPrompt?: string; + lane?: string; + idempotencyKey?: string; + }) => Promise<{ runId: string }>; + }; +}; + +type Logger = { + info?: (msg: string) => void; + warn?: (msg: string) => void; + error?: (msg: string) => void; +}; + +/** + * Find the workspace root. Openclaw passes it via ctx.workspaceDir; fall back + * to the WORKSPACE env var (handy for smoke tests) and finally process.cwd(). + */ +export function resolveWorkspace(hint?: string): string { + if (hint && hint.trim()) return hint; + if (process.env.WORKSPACE) return process.env.WORKSPACE; + return process.cwd(); +} + +/** + * TTL sweep: scan .reflexio/profiles/*.md and unlink expired files. + * Cheap: filesystem + YAML frontmatter parse only. Target <50ms for dozens of files. + */ +export async function ttlSweepProfiles(workspaceHint?: string): Promise { + const workspace = resolveWorkspace(workspaceHint); + const dir = path.join(workspace, ".reflexio", "profiles"); + if (!fs.existsSync(dir)) return; + + const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD + const entries = await fs.promises.readdir(dir); + + for (const entry of entries) { + if (!entry.endsWith(".md")) continue; + const full = path.join(dir, entry); + let contents: string; + try { + contents = await fs.promises.readFile(full, "utf8"); + } catch { + continue; + } + const expiresMatch = /^expires:\s*(\S+)/m.exec(contents); + if (!expiresMatch) continue; + const expires = expiresMatch[1]; + if (expires === "never") continue; + if (expires < today) { + try { + await fs.promises.unlink(full); + } catch (err) { + console.error( + `[reflexio-embedded] ttl sweep: failed to unlink ${full}: ${err}`, + ); + } + } + } +} + +/** + * Short system-prompt reminder injected on every agent run so the LLM knows + * the Reflexio SKILL.md is available. + */ +export function injectBootstrapReminder(): string { + return [ + "# Reflexio Embedded", + "", + "This agent has the openclaw-embedded plugin installed. Its SKILL.md", + "describes how to capture user facts and corrections into .reflexio/.", + "", + "Load the skill when: user states a preference/fact/config, user corrects", + "you and later confirms the fix, or you need to retrieve past context.", + ].join("\n"); +} + +type TranscriptMessage = { + role?: string; + content?: unknown; + timestamp?: string; +}; + +/** Decide whether the current transcript is worth extracting from. */ +function transcriptWorthExtracting(messages: unknown[] | undefined): boolean { + if (!Array.isArray(messages) || messages.length < 2) return false; + return messages.some((m) => (m as TranscriptMessage).role === "user"); +} + +/** Serialize transcript into a plain-text form suitable for the sub-agent task prompt. */ +function serializeTranscript(messages: unknown[] | undefined): string { + if (!Array.isArray(messages)) return ""; + return messages + .map((raw) => { + const m = raw as TranscriptMessage; + const role = m.role ?? "unknown"; + const content = + typeof m.content === "string" ? m.content : JSON.stringify(m.content); + const ts = m.timestamp ? ` [${m.timestamp}]` : ""; + return `### ${role}${ts}\n${content}`; + }) + .join("\n\n"); +} + +/** + * Build the task prompt handed to the reflexio-extractor sub-agent. The + * sub-agent's system prompt already contains its workflow (from + * agents/reflexio-extractor.md). This prompt just provides the transcript and + * reminds it of its job. + */ +function buildExtractionTaskPrompt( + messages: unknown[] | undefined, + sessionFile: string | undefined, +): string { + const parts = [ + "Run your extraction workflow on the recent transcript.", + "", + "Follow your system prompt: extract profiles and playbooks, then run shallow pairwise dedup against existing .reflexio/ entries.", + "", + ]; + const transcript = serializeTranscript(messages); + if (transcript) { + parts.push("## Transcript", "", transcript); + } else if (sessionFile) { + parts.push( + "## Transcript", + "", + `The transcript lives on disk at: ${sessionFile}`, + "Read that file to reconstruct the session.", + ); + } else { + parts.push( + "## Transcript", + "", + "No in-memory transcript is available for this event; skip if you cannot find a session file.", + ); + } + return parts.join("\n"); +} + +export type SpawnExtractorParams = { + runtime: SubagentRuntime | undefined; + workspaceDir?: string; + sessionKey?: string; + messages?: unknown[]; + sessionFile?: string; + extraSystemPrompt?: string; + log?: Logger; + reason: string; +}; + +/** + * Fire-and-forget spawn of the reflexio-extractor sub-agent. The caller should + * NOT await the run itself — Openclaw manages the lifecycle via its Background + * Tasks ledger. + * + * Returns the runId when the spawn succeeds (caller can await the spawn to be + * sure the request reached the runtime, but not the sub-agent's completion). + */ +export async function spawnExtractor( + params: SpawnExtractorParams, +): Promise { + const { runtime, sessionKey, messages, sessionFile, extraSystemPrompt, log, reason } = params; + + if (!transcriptWorthExtracting(messages) && !sessionFile) { + return undefined; + } + const runFn = runtime?.subagent?.run; + if (!runFn) { + log?.warn?.( + "[reflexio-embedded] runtime.subagent.run unavailable; skipping extraction", + ); + return undefined; + } + + // Session-key namespacing: each extractor run gets its own sub-session so + // the parent session transcript is not polluted. + const childSessionKey = `reflexio-extractor:${sessionKey ?? "unknown"}:${Date.now()}`; + const message = buildExtractionTaskPrompt(messages, sessionFile); + + try { + const result = await runFn({ + sessionKey: childSessionKey, + message, + extraSystemPrompt, + lane: "reflexio-extractor", + idempotencyKey: `${reason}:${childSessionKey}`, + }); + log?.info?.( + `[reflexio-embedded] extractor spawned (runId=${result.runId}, reason=${reason})`, + ); + return result.runId; + } catch (err) { + log?.error?.( + `[reflexio-embedded] failed to spawn extractor (reason=${reason}): ${err}`, + ); + return undefined; + } +} diff --git a/reflexio/integrations/openclaw-embedded/plugin/hook/setup.ts b/reflexio/integrations/openclaw-embedded/plugin/hook/setup.ts new file mode 100644 index 0000000..e79e9da --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/hook/setup.ts @@ -0,0 +1,43 @@ +// Workspace auto-setup. +// +// On first load after install, appends the heartbeat consolidation check +// to the workspace HEARTBEAT.md. Skills are served from the extension dir +// via the manifest's "skills" field. Agents are injected via extraSystemPrompt. +// No workspace file copying needed — everything lives in the extension dir. +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; + +function resolveOpenclawHome(): string { + return process.env.OPENCLAW_HOME || path.join(os.homedir(), ".openclaw"); +} + +/** + * Append HEARTBEAT.md content to the workspace. Idempotent — checks for + * the marker heading before appending. + * + * @param pluginDir - The plugin's install directory (import.meta.dirname) + */ +export function setupWorkspaceResources(pluginDir: string): void { + const workspace = path.join(resolveOpenclawHome(), "workspace"); + + const heartbeatSrc = path.join(pluginDir, "HEARTBEAT.md"); + const heartbeatDest = path.join(workspace, "HEARTBEAT.md"); + if (!fs.existsSync(heartbeatSrc)) return; + + const heartbeatContent = fs.readFileSync(heartbeatSrc, "utf8"); + const marker = "## Reflexio Consolidation Check"; + + let existing = ""; + try { + existing = fs.readFileSync(heartbeatDest, "utf8"); + } catch { + // file doesn't exist yet + } + + if (!existing.includes(marker)) { + const separator = existing.length > 0 ? "\n\n" : ""; + fs.mkdirSync(workspace, { recursive: true }); + fs.writeFileSync(heartbeatDest, existing + separator + heartbeatContent, "utf8"); + } +} diff --git a/reflexio/integrations/openclaw-embedded/plugin/index.ts b/reflexio/integrations/openclaw-embedded/plugin/index.ts new file mode 100644 index 0000000..8d514a0 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/index.ts @@ -0,0 +1,313 @@ +// Reflexio Embedded — Openclaw plugin entry. +// +// Registers lifecycle hooks against the Openclaw Plugin API: +// - before_prompt_build: inject SKILL.md reminder into system prompt +// - before_agent_start: TTL sweep, workspace setup +// - before_compaction: run extractor subagent over the session transcript +// - before_reset: run extractor subagent before the transcript is wiped +// - session_end: run extractor subagent on session termination +// +// The TTL sweep + extractor spawning logic lives in ./hook/handler.ts and is +// re-used verbatim — this file is only the SDK wiring. +import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; + +import { + injectBootstrapReminder, + spawnExtractor, + ttlSweepProfiles, +} from "./hook/handler.ts"; +import { setupWorkspaceResources } from "./hook/setup.ts"; +import { writeProfile } from "./lib/write-profile.ts"; +import { writePlaybook } from "./lib/write-playbook.ts"; +import { search } from "./lib/search.ts"; +import { reindexMemory } from "./lib/openclaw-cli.ts"; + +export default definePluginEntry({ + id: "reflexio-embedded", + name: "Reflexio Embedded", + description: + "Reflexio-style user profile and playbook extraction using Openclaw's native memory engine, hooks, and sub-agents.", + register(api) { + const log = api.logger; + const pluginDir = import.meta.dirname || __dirname; + + // Load agent system prompts from the plugin's own directory + let extractorSystemPrompt: string | undefined; + let consolidatorSystemPrompt: string | undefined; + try { + extractorSystemPrompt = fs.readFileSync( + path.join(pluginDir, "agents", "reflexio-extractor.md"), + "utf8", + ); + } catch { + log.warn?.("[reflexio-embedded] could not load reflexio-extractor.md agent definition"); + } + try { + consolidatorSystemPrompt = fs.readFileSync( + path.join(pluginDir, "agents", "reflexio-consolidator.md"), + "utf8", + ); + } catch { + log.warn?.("[reflexio-embedded] could not load reflexio-consolidator.md agent definition"); + } + + // before_prompt_build: inject a short system-prompt reminder so the LLM + // knows the SKILL.md is available (replaces deprecated before_agent_start + // for prompt mutation). + api.on("before_prompt_build", async () => { + return { + prependSystemContext: injectBootstrapReminder(), + }; + }); + + // before_agent_start: workspace setup + TTL sweep (non-prompt tasks). + api.on("before_agent_start", async (_event, ctx) => { + try { + setupWorkspaceResources(pluginDir); + } catch (err) { + log.error?.(`[reflexio-embedded] workspace setup failed: ${err}`); + } + try { + await ttlSweepProfiles(ctx.workspaceDir); + } catch (err) { + log.error?.(`[reflexio-embedded] ttl sweep failed: ${err}`); + } + }); + + // before_compaction: spawn extractor BEFORE the LLM compacts history so we + // still have the raw transcript to extract from. + api.on("before_compaction", async (event, ctx) => { + try { + await ttlSweepProfiles(ctx.workspaceDir); + await spawnExtractor({ + runtime: api.runtime, + workspaceDir: ctx.workspaceDir, + sessionKey: ctx.sessionKey, + messages: event.messages, + sessionFile: event.sessionFile, + extraSystemPrompt: extractorSystemPrompt, + log, + reason: "before_compaction", + }); + } catch (err) { + log.error?.(`[reflexio-embedded] before_compaction failed: ${err}`); + } + }); + + // before_reset: user ran /reset — flush current transcript to the extractor. + api.on("before_reset", async (event, ctx) => { + try { + await ttlSweepProfiles(ctx.workspaceDir); + await spawnExtractor({ + runtime: api.runtime, + workspaceDir: ctx.workspaceDir, + sessionKey: ctx.sessionKey, + messages: event.messages, + sessionFile: event.sessionFile, + extraSystemPrompt: extractorSystemPrompt, + log, + reason: `before_reset:${event.reason ?? "unknown"}`, + }); + } catch (err) { + log.error?.(`[reflexio-embedded] before_reset failed: ${err}`); + } + }); + + // session_end: fires when a session terminates for any reason (stop, idle, + // daily rollover, etc.). + api.on("session_end", async (event, ctx) => { + try { + await ttlSweepProfiles(ctx.workspaceDir); + await spawnExtractor({ + runtime: api.runtime, + workspaceDir: ctx.workspaceDir, + sessionKey: ctx.sessionKey ?? event.sessionKey, + messages: undefined, // transcript lives on disk at this point + sessionFile: event.sessionFile, + extraSystemPrompt: extractorSystemPrompt, + log, + reason: `session_end:${event.reason ?? "unknown"}`, + }); + } catch (err) { + log.error?.(`[reflexio-embedded] session_end failed: ${err}`); + } + }); + + // ────────────────────────────────────────────────────────── + // Agent tools — deterministic control flow for writes + search + // ────────────────────────────────────────────────────────── + const runner = api.runtime.system.runCommandWithTimeout; + const config = api.pluginConfig ?? { + dedup: { shallow_threshold: 0.7, top_k: 5 }, + consolidation: { threshold_hours: 24 }, + }; + + /** + * Resolve the agent's workspace directory. + * Mirrors Openclaw's resolveDefaultAgentWorkspaceDir logic: + * ~/.openclaw/workspace (default) + * ~/.openclaw/workspace-{profile} (if OPENCLAW_PROFILE is set) + */ + function resolveWorkspaceDir(): string { + const profile = process.env.OPENCLAW_PROFILE?.trim(); + if (profile && profile.toLowerCase() !== "default") { + return path.join(os.homedir(), ".openclaw", `workspace-${profile}`); + } + return path.join(os.homedir(), ".openclaw", "workspace"); + } + + api.registerTool({ + name: "reflexio_write_profile", + description: + "Write a user profile to .reflexio/profiles/ with automatic query preprocessing, memory search, contradiction detection, dedup, and old-file cleanup. Returns the new file path.", + parameters: { + type: "object", + properties: { + slug: { type: "string", description: "kebab-case topic, e.g. diet-vegan" }, + ttl: { + type: "string", + description: "one_day | one_week | one_month | one_quarter | one_year | infinity", + }, + body: { type: "string", description: "1-3 sentences, one fact per profile" }, + }, + required: ["slug", "ttl", "body"], + }, + optional: true, + async execute(_id: string, params: { slug: string; ttl: string; body: string }) { + const workspaceDir = resolveWorkspaceDir(); + const filePath = await writeProfile({ + slug: params.slug, + ttl: params.ttl, + body: params.body, + workspace: workspaceDir, + config: config.dedup, + runner, + }); + return { content: [{ type: "text" as const, text: filePath }] }; + }, + }); + + api.registerTool({ + name: "reflexio_write_playbook", + description: + "Write a playbook to .reflexio/playbooks/ with automatic dedup and contradiction detection. Returns the new file path.", + parameters: { + type: "object", + properties: { + slug: { type: "string", description: "kebab-case trigger summary, e.g. commit-no-trailers" }, + body: { + type: "string", + description: "Playbook body with ## When, ## What, ## Why sections", + }, + }, + required: ["slug", "body"], + }, + optional: true, + async execute(_id: string, params: { slug: string; body: string }) { + const workspaceDir = resolveWorkspaceDir(); + const filePath = await writePlaybook({ + slug: params.slug, + body: params.body, + workspace: workspaceDir, + config: config.dedup, + runner, + }); + return { content: [{ type: "text" as const, text: filePath }] }; + }, + }); + + api.registerTool({ + name: "reflexio_search", + description: + "Search .reflexio/ memory with automatic query preprocessing for better results. Returns JSON with results array.", + parameters: { + type: "object", + properties: { + query: { type: "string", description: "raw query — preprocessing is automatic" }, + }, + required: ["query"], + }, + async execute(_id: string, params: { query: string }) { + const results = await search(params.query, 5, undefined, runner); + return { + content: [{ type: "text" as const, text: JSON.stringify({ results }, null, 2) }], + }; + }, + }); + + // ────────────────────────────────────────────────────────── + // Consolidation — spawn consolidator sub-agent + // ────────────────────────────────────────────────────────── + api.registerTool({ + name: "reflexio_run_consolidation", + description: + "Spawn the reflexio-consolidator sub-agent to run a full consolidation sweep. Returns the runId. Call reflexio_consolidation_mark_done after it completes.", + parameters: { type: "object", properties: {} }, + optional: true, + async execute() { + const runFn = api.runtime?.subagent?.run; + if (!runFn) { + return { content: [{ type: "text" as const, text: "ERROR: subagent.run unavailable" }] }; + } + if (!consolidatorSystemPrompt) { + return { content: [{ type: "text" as const, text: "ERROR: consolidator agent definition not loaded" }] }; + } + try { + const result = await runFn({ + sessionKey: `reflexio-consolidator:${Date.now()}`, + message: "Run your full-sweep consolidation workflow now. Follow your system prompt in full.", + extraSystemPrompt: consolidatorSystemPrompt, + lane: "reflexio-consolidator", + }); + return { content: [{ type: "text" as const, text: `Consolidation started. runId: ${result.runId}` }] }; + } catch (err) { + return { content: [{ type: "text" as const, text: `ERROR: failed to spawn consolidator: ${err}` }] }; + } + }, + }); + + // ────────────────────────────────────────────────────────── + // Heartbeat — consolidation check (replaces cron job) + // ────────────────────────────────────────────────────────── + const consolidationStateFile = path.join(os.homedir(), ".openclaw", "reflexio-consolidation-state.json"); + + api.registerTool({ + name: "reflexio_consolidation_check", + description: + "Check if reflexio consolidation is due. Returns OK or ALERT. Called by the agent on heartbeat.", + parameters: { type: "object", properties: {} }, + async execute() { + const thresholdHours = config.consolidation?.threshold_hours ?? 24; + try { + const state = JSON.parse(fs.readFileSync(consolidationStateFile, "utf8")); + const elapsedMs = Date.now() - new Date(state.last_consolidation).getTime(); + const elapsedHours = elapsedMs / 3_600_000; + if (elapsedHours < thresholdHours) { + const remaining = Math.round(thresholdHours - elapsedHours); + return { content: [{ type: "text" as const, text: `OK: Last consolidation ${Math.round(elapsedHours)}h ago. Next due in ${remaining}h.` }] }; + } + } catch { + // no state file = never consolidated + } + return { content: [{ type: "text" as const, text: "ALERT: Consolidation due." }] }; + }, + }); + + api.registerTool({ + name: "reflexio_consolidation_mark_done", + description: + "Mark consolidation as complete. Call this after a successful consolidation run.", + parameters: { type: "object", properties: {} }, + async execute() { + const state = { last_consolidation: new Date().toISOString() }; + fs.mkdirSync(path.dirname(consolidationStateFile), { recursive: true }); + fs.writeFileSync(consolidationStateFile, JSON.stringify(state, null, 2), "utf8"); + await reindexMemory(runner); + return { content: [{ type: "text" as const, text: `Consolidation marked complete at ${state.last_consolidation}. Memory index rebuilt.` }] }; + }, + }); + }, +}); diff --git a/reflexio/integrations/openclaw-embedded/plugin/lib/dedup.ts b/reflexio/integrations/openclaw-embedded/plugin/lib/dedup.ts new file mode 100644 index 0000000..e2c8765 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/lib/dedup.ts @@ -0,0 +1,57 @@ +import { infer, type CommandRunner } from "./openclaw-cli.ts"; + +const PREPROCESS_PROMPT = `Rewrite the following text into a single descriptive sentence that captures the core fact or topic. Expand with 2-3 important synonyms or related terms to improve search matching. Remove conversational filler. Return ONLY the rewritten text. + +Text: "{rawText}"`; + +const CONTRADICTION_PROMPT = `EXISTING fact: "{existingContent}" +NEW fact: "{newContent}" + +Does the NEW fact replace or contradict the EXISTING fact (same topic, updated information)? +Answer with ONLY a JSON object: {"decision": "supersede"} or {"decision": "keep_both"}`; + +/** + * Rewrite raw text into a clean search query optimized for vector + FTS search. + * Falls back to raw text if openclaw infer is unavailable. + */ +export async function preprocessQuery(rawText: string, runner: CommandRunner): Promise { + const prompt = PREPROCESS_PROMPT.replace("{rawText}", rawText); + const result = await infer(prompt, runner); + if (!result || result.trim().length === 0) { + return rawText; + } + return result.trim(); +} + +/** + * Ask LLM whether newContent contradicts/replaces existingContent. + * Returns "supersede" or "keep_both". Defaults to "keep_both" on any failure. + */ +export async function judgeContradiction( + newContent: string, + existingContent: string, + runner: CommandRunner +): Promise<"supersede" | "keep_both"> { + const prompt = CONTRADICTION_PROMPT + .replace("{existingContent}", existingContent) + .replace("{newContent}", newContent); + + const result = await infer(prompt, runner); + if (!result) return "keep_both"; + + try { + const parsed = JSON.parse(result); + if (parsed.decision === "supersede") return "supersede"; + return "keep_both"; + } catch { + return "keep_both"; + } +} + +/** + * Extract the `id:` value from a memory search snippet containing YAML frontmatter. + */ +export function extractId(snippet: string): string | null { + const match = /^id:\s*(\S+)/m.exec(snippet); + return match ? match[1] : null; +} diff --git a/reflexio/integrations/openclaw-embedded/plugin/lib/io.ts b/reflexio/integrations/openclaw-embedded/plugin/lib/io.ts new file mode 100644 index 0000000..1632cf4 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/lib/io.ts @@ -0,0 +1,155 @@ +import * as crypto from "node:crypto"; +import * as fs from "node:fs"; +import * as path from "node:path"; + +const SLUG_REGEX = /^[a-z0-9][a-z0-9-]{0,47}$/; +const VALID_TTLS = [ + "one_day", + "one_week", + "one_month", + "one_quarter", + "one_year", + "infinity", +] as const; +export type Ttl = (typeof VALID_TTLS)[number]; + +export function generateNanoid(): string { + const bytes = crypto.randomBytes(3); + return Array.from(bytes) + .map((b) => (b % 36).toString(36)) + .join("") + .slice(0, 4) + .padEnd(4, "0"); +} + +export function validateSlug(slug: string): void { + if (!slug || !SLUG_REGEX.test(slug)) { + throw new Error(`Invalid slug: "${slug}". Must match ${SLUG_REGEX}`); + } +} + +export function validateTtl(ttl: string): asserts ttl is Ttl { + if (!VALID_TTLS.includes(ttl as Ttl)) { + throw new Error( + `Invalid TTL: "${ttl}". Must be one of: ${VALID_TTLS.join(", ")}` + ); + } +} + +export function computeExpires(ttl: Ttl, created: string): string { + if (ttl === "infinity") return "never"; + const date = new Date(created); + const offsets: Record void> = { + one_day: () => date.setUTCDate(date.getUTCDate() + 1), + one_week: () => date.setUTCDate(date.getUTCDate() + 7), + one_month: () => date.setUTCMonth(date.getUTCMonth() + 1), + one_quarter: () => date.setUTCMonth(date.getUTCMonth() + 3), + one_year: () => date.setUTCFullYear(date.getUTCFullYear() + 1), + }; + offsets[ttl](); + return date.toISOString().slice(0, 10); +} + +export function resolveWorkspace(): string { + return process.env.WORKSPACE || process.cwd(); +} + +interface WriteProfileOpts { + slug: string; + ttl: Ttl; + body: string; + supersedes?: string[]; + workspace?: string; +} + +export function writeProfileFile(opts: WriteProfileOpts): string { + validateSlug(opts.slug); + validateTtl(opts.ttl); + + const suffix = generateNanoid(); + const id = `prof_${suffix}`; + const created = new Date().toISOString().replace(/\.\d+Z$/, "Z"); + const expires = computeExpires(opts.ttl, created); + + const ws = opts.workspace || resolveWorkspace(); + const dir = path.join(ws, ".reflexio", "profiles"); + fs.mkdirSync(dir, { recursive: true }); + + const filePath = path.join(dir, `${opts.slug}-${suffix}.md`); + const tmpPath = `${filePath}.tmp.${process.pid}`; + + const lines = [ + "---", + "type: profile", + `id: ${id}`, + `created: ${created}`, + `ttl: ${opts.ttl}`, + `expires: ${expires}`, + ]; + if (opts.supersedes && opts.supersedes.length > 0) { + lines.push(`supersedes: [${opts.supersedes.join(", ")}]`); + } + lines.push("---", "", opts.body, ""); + + try { + fs.writeFileSync(tmpPath, lines.join("\n")); + fs.renameSync(tmpPath, filePath); + } catch (err) { + try { fs.unlinkSync(tmpPath); } catch {} + throw err; + } + + return filePath; +} + +interface WritePlaybookOpts { + slug: string; + body: string; + supersedes?: string[]; + workspace?: string; +} + +export function writePlaybookFile(opts: WritePlaybookOpts): string { + validateSlug(opts.slug); + + const suffix = generateNanoid(); + const id = `pbk_${suffix}`; + const created = new Date().toISOString().replace(/\.\d+Z$/, "Z"); + + const ws = opts.workspace || resolveWorkspace(); + const dir = path.join(ws, ".reflexio", "playbooks"); + fs.mkdirSync(dir, { recursive: true }); + + const filePath = path.join(dir, `${opts.slug}-${suffix}.md`); + const tmpPath = `${filePath}.tmp.${process.pid}`; + + const lines = [ + "---", + "type: playbook", + `id: ${id}`, + `created: ${created}`, + ]; + if (opts.supersedes && opts.supersedes.length > 0) { + lines.push(`supersedes: [${opts.supersedes.join(", ")}]`); + } + lines.push("---", "", opts.body, ""); + + try { + fs.writeFileSync(tmpPath, lines.join("\n")); + fs.renameSync(tmpPath, filePath); + } catch (err) { + try { fs.unlinkSync(tmpPath); } catch {} + throw err; + } + + return filePath; +} + +export function deleteFile(filePath: string): void { + try { + fs.unlinkSync(filePath); + } catch (err: any) { + if (err.code !== "ENOENT") throw err; + console.error(`[reflexio] warning: file already gone: ${filePath}`); + } +} diff --git a/reflexio/integrations/openclaw-embedded/plugin/lib/openclaw-cli.ts b/reflexio/integrations/openclaw-embedded/plugin/lib/openclaw-cli.ts new file mode 100644 index 0000000..eee21da --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/lib/openclaw-cli.ts @@ -0,0 +1,87 @@ +/** + * Abstraction over command execution. + * Plugin runtime injects api.runtime.system.runCommandWithTimeout. + * Tests inject a mock. + */ +export type CommandRunner = ( + argv: string[], + opts: { timeoutMs: number; input?: string } +) => Promise<{ stdout: string; stderr: string; code: number | null }>; + +export interface MemorySearchResult { + path: string; + startLine: number; + endLine: number; + score: number; + snippet: string; + source: string; +} + +export interface MemorySearchResponse { + results: MemorySearchResult[]; +} + +/** + * Call `openclaw memory search` via the injected runner. + * Returns empty array on any failure (graceful degradation). + */ +export async function memorySearch( + query: string, + maxResults: number, + runner: CommandRunner +): Promise { + try { + const result = await runner( + ["openclaw", "memory", "search", query, "--json", "--max-results", String(maxResults)], + { timeoutMs: 30_000 } + ); + const parsed: MemorySearchResponse = JSON.parse(result.stdout.trim()); + return parsed.results || []; + } catch (err) { + console.error(`[reflexio] openclaw memory search failed: ${err}`); + return []; + } +} + +/** + * Call `openclaw memory index --force` to rebuild the search index. + * Necessary after bulk file deletions (e.g. consolidation) so that + * deleted files are dropped from search results. + */ +export async function reindexMemory(runner: CommandRunner): Promise { + try { + await runner( + ["openclaw", "memory", "index", "--force"], + { timeoutMs: 60_000 } + ); + } catch (err) { + console.error(`[reflexio] openclaw memory index --force failed: ${err}`); + } +} + +interface InferResponse { + ok: boolean; + outputs?: { text: string | null; mediaUrl?: string | null }[]; +} + +/** + * Call `openclaw infer model run` via the injected runner. + * Returns the LLM output text, or null on any failure. + */ +export async function infer( + prompt: string, + runner: CommandRunner +): Promise { + try { + const result = await runner( + ["openclaw", "infer", "model", "run", "--prompt", prompt, "--json"], + { timeoutMs: 30_000 } + ); + const parsed: InferResponse = JSON.parse(result.stdout); + if (!parsed.ok || !parsed.outputs?.length) return null; + return parsed.outputs[0].text?.trim() || null; + } catch (err) { + console.error(`[reflexio] openclaw infer failed: ${err}`); + return null; + } +} diff --git a/reflexio/integrations/openclaw-embedded/plugin/lib/search.ts b/reflexio/integrations/openclaw-embedded/plugin/lib/search.ts new file mode 100644 index 0000000..c190d11 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/lib/search.ts @@ -0,0 +1,32 @@ +import { memorySearch, type MemorySearchResult, type CommandRunner } from "./openclaw-cli.ts"; +import { preprocessQuery } from "./dedup.ts"; + +/** + * Search memory with a query string, optionally filtering by type. + */ +export async function rawSearch( + query: string, + maxResults: number, + type: "profile" | "playbook" | undefined, + runner: CommandRunner +): Promise { + let results = await memorySearch(query, maxResults, runner); + if (type) { + const typeDir = type === "profile" ? "/profiles/" : "/playbooks/"; + results = results.filter((r) => r.path.includes(typeDir)); + } + return results; +} + +/** + * Preprocess query (via LLM rewrite) then search memory. + */ +export async function search( + rawQuery: string, + maxResults: number, + type: "profile" | "playbook" | undefined, + runner: CommandRunner +): Promise { + const query = await preprocessQuery(rawQuery, runner); + return rawSearch(query, maxResults, type, runner); +} diff --git a/reflexio/integrations/openclaw-embedded/plugin/lib/write-playbook.ts b/reflexio/integrations/openclaw-embedded/plugin/lib/write-playbook.ts new file mode 100644 index 0000000..fa922b9 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/lib/write-playbook.ts @@ -0,0 +1,61 @@ +import { writePlaybookFile, deleteFile, validateSlug } from "./io.ts"; +import { preprocessQuery, judgeContradiction, extractId } from "./dedup.ts"; +import { rawSearch } from "./search.ts"; +import type { CommandRunner } from "./openclaw-cli.ts"; + +export interface WritePlaybookConfig { + shallow_threshold: number; + top_k: number; +} + +export interface WritePlaybookOpts { + slug: string; + body: string; + workspace?: string; + config: WritePlaybookConfig; + runner: CommandRunner; +} + +/** + * Full playbook write orchestration: + * validate → preprocess → search → judge → write → delete (if superseding) + */ +export async function writePlaybook(opts: WritePlaybookOpts): Promise { + validateSlug(opts.slug); + + const query = await preprocessQuery(opts.body, opts.runner); + const neighbors = await rawSearch(query, opts.config.top_k, "playbook", opts.runner); + const top = neighbors[0]; + let supersedes: string[] | undefined; + let deleteTarget: string | undefined; + + if (top && top.score >= opts.config.shallow_threshold) { + const bodyFromSnippet = top.snippet.split("---").slice(2).join("---").trim(); + const decision = await judgeContradiction(opts.body, bodyFromSnippet, opts.runner); + + if (decision === "supersede") { + const oldId = extractId(top.snippet); + if (oldId) { + supersedes = [oldId]; + deleteTarget = top.path; + } + } + } + + const newPath = writePlaybookFile({ + slug: opts.slug, + body: opts.body, + supersedes, + workspace: opts.workspace, + }); + + if (deleteTarget) { + const ws = opts.workspace || process.env.WORKSPACE || process.cwd(); + const absDelete = deleteTarget.startsWith("/") + ? deleteTarget + : `${ws}/${deleteTarget}`; + deleteFile(absDelete); + } + + return newPath; +} diff --git a/reflexio/integrations/openclaw-embedded/plugin/lib/write-profile.ts b/reflexio/integrations/openclaw-embedded/plugin/lib/write-profile.ts new file mode 100644 index 0000000..6953d57 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/lib/write-profile.ts @@ -0,0 +1,65 @@ +import { writeProfileFile, deleteFile, validateSlug, validateTtl, type Ttl } from "./io.ts"; +import { preprocessQuery, judgeContradiction, extractId } from "./dedup.ts"; +import { rawSearch } from "./search.ts"; +import type { CommandRunner } from "./openclaw-cli.ts"; + +export interface WriteProfileConfig { + shallow_threshold: number; + top_k: number; +} + +export interface WriteProfileOpts { + slug: string; + ttl: Ttl | string; + body: string; + workspace?: string; + config: WriteProfileConfig; + runner: CommandRunner; +} + +/** + * Full profile write orchestration: + * validate → preprocess → search → judge → write → delete (if superseding) + */ +export async function writeProfile(opts: WriteProfileOpts): Promise { + validateSlug(opts.slug); + validateTtl(opts.ttl); + + const query = await preprocessQuery(opts.body, opts.runner); + const neighbors = await rawSearch(query, opts.config.top_k, "profile", opts.runner); + + const top = neighbors[0]; + let supersedes: string[] | undefined; + let deleteTarget: string | undefined; + + if (top && top.score >= opts.config.shallow_threshold) { + const bodyFromSnippet = top.snippet.split("---").slice(2).join("---").trim(); + const decision = await judgeContradiction(opts.body, bodyFromSnippet, opts.runner); + + if (decision === "supersede") { + const oldId = extractId(top.snippet); + if (oldId) { + supersedes = [oldId]; + deleteTarget = top.path; + } + } + } + + const newPath = writeProfileFile({ + slug: opts.slug, + ttl: opts.ttl as Ttl, + body: opts.body, + supersedes, + workspace: opts.workspace, + }); + + if (deleteTarget) { + const ws = opts.workspace || process.env.WORKSPACE || process.cwd(); + const absDelete = deleteTarget.startsWith("/") + ? deleteTarget + : `${ws}/${deleteTarget}`; + deleteFile(absDelete); + } + + return newPath; +} diff --git a/reflexio/integrations/openclaw-embedded/plugin/openclaw.plugin.json b/reflexio/integrations/openclaw-embedded/plugin/openclaw.plugin.json new file mode 100644 index 0000000..440b02c --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/openclaw.plugin.json @@ -0,0 +1,46 @@ +{ + "id": "reflexio-embedded", + "name": "Reflexio Embedded", + "description": "Reflexio-style user profile and playbook extraction using Openclaw's native memory engine, hooks, and sub-agents — no Reflexio server required.", + "skills": ["./skills"], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "dedup": { + "type": "object", + "additionalProperties": false, + "properties": { + "shallow_threshold": { "type": "number", "default": 0.7 }, + "full_threshold": { "type": "number", "default": 0.75 }, + "top_k": { "type": "integer", "default": 5 } + }, + "default": { "shallow_threshold": 0.7, "full_threshold": 0.75, "top_k": 5 } + }, + "ttl_sweep": { + "type": "object", + "additionalProperties": false, + "properties": { + "on_bootstrap": { "type": "boolean", "default": true } + }, + "default": { "on_bootstrap": true } + }, + "consolidation": { + "type": "object", + "additionalProperties": false, + "properties": { + "threshold_hours": { "type": "integer", "default": 24 } + }, + "default": { "threshold_hours": 24 } + }, + "extraction": { + "type": "object", + "additionalProperties": false, + "properties": { + "subagent_timeout_seconds": { "type": "integer", "default": 120 } + }, + "default": { "subagent_timeout_seconds": 120 } + } + } + } +} diff --git a/reflexio/integrations/openclaw-embedded/plugin/package.json b/reflexio/integrations/openclaw-embedded/plugin/package.json new file mode 100644 index 0000000..26a6752 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/package.json @@ -0,0 +1,18 @@ +{ + "name": "@reflexio/openclaw-embedded", + "version": "0.1.0", + "description": "Reflexio-style user profile and playbook extraction using Openclaw's native memory engine, hooks, and sub-agents.", + "private": true, + "type": "module", + "openclaw": { + "extensions": ["./index.ts"], + "compat": { + "pluginApi": ">=2026.3.24", + "minGatewayVersion": "2026.3.24" + }, + "build": { + "openclawVersion": "2026.4.14", + "pluginSdkVersion": "2026.4.14" + } + } +} diff --git a/reflexio/integrations/openclaw-embedded/plugin/prompts/README.md b/reflexio/integrations/openclaw-embedded/plugin/prompts/README.md new file mode 100644 index 0000000..0ad5c29 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/prompts/README.md @@ -0,0 +1,37 @@ +# Openclaw-Embedded Prompts + +LLM prompt templates used by Flow C sub-agents and the consolidation cron job. + +## Files + +- `profile_extraction.md` — extract durable user facts from a transcript +- `playbook_extraction.md` — extract procedural rules from correction+confirmation patterns +- `shallow_dedup_pairwise.md` — decide how to handle a new candidate vs its top-1 neighbor +- `full_consolidation.md` — consolidate a cluster of 2-10 similar items + +## Format + +Each file is a `.prompt.md` with YAML frontmatter (matches Reflexio's +`server/prompt/prompt_bank/` convention). Unlike upstream's versioned layout +(`/v.prompt.md`), we store prompts flat (`.md`) since the +plugin ships atomically with one active version at a time. + +```yaml +--- +active: true +description: "one-line description" +changelog: "what changed in this version" +variables: + - var1 + - var2 +--- + +prompt body, with {var1} and {var2} substitution points +``` + +## Upstream sync + +`profile_extraction.md` and `playbook_extraction.md` are ports of Reflexio's +prompt_bank entries. On upstream bumps, review the prompt diff against our +adapted versions. Porting notes will be maintained in +`../references/porting-notes.md` (added in a later phase). diff --git a/reflexio/integrations/openclaw-embedded/plugin/prompts/full_consolidation.md b/reflexio/integrations/openclaw-embedded/plugin/prompts/full_consolidation.md new file mode 100644 index 0000000..c48ea44 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/prompts/full_consolidation.md @@ -0,0 +1,50 @@ +--- +active: true +description: "N-way cluster consolidation for openclaw-embedded daily cron" +changelog: "Initial (2026-04-16): new prompt, informed by Reflexio's playbook_aggregation and *_deduplication prompts, adapted for single-instance n-way clustering" +variables: + - cluster +--- + +You consolidate a cluster of 2-10 similar items (profiles or playbooks) that have accumulated over time. + +## Inputs + +A cluster of items, each with: `id`, `path`, `content`. All items are the same type (all profiles OR all playbooks). + +## Decision + +Output one of: + +- `merge_all` — every item in the cluster collapses into a single merged entry. All cluster files will be deleted and one new merged file written. +- `merge_subset` — some items collapse, others remain distinct. Identify which IDs merge and which stay. +- `keep_all` — the cluster is not actually redundant on closer inspection; no changes. + +### Contradiction handling + +If items contradict, keep the most recent unless older items have strong corroborating signals. Explain the choice in `rationale`. + +### Preservation rule + +Preserve distinctions that are meaningfully different. Collapse only where content overlap is substantive. When in doubt, prefer `keep_all` or `merge_subset` over `merge_all`. + +## Output schema + +```json +{ + "action": "merge_all | merge_subset | keep_all", + "merged_content": "string — required if action ∈ {merge_all, merge_subset}; the synthesized content body", + "merged_slug": "string — required if action ∈ {merge_all, merge_subset}; kebab-case, regex `^[a-z0-9][a-z0-9-]{0,47}$`", + "ids_merged_in": ["string"], + "ids_kept_separate": ["string"], + "rationale": "string — always required; 2-3 sentences justifying the action" +} +``` + +For `merge_all`: `ids_merged_in` contains all cluster IDs, `ids_kept_separate` is empty. +For `merge_subset`: `ids_merged_in` + `ids_kept_separate` = full cluster IDs (disjoint). +For `keep_all`: `ids_merged_in` empty, `ids_kept_separate` = full cluster IDs. + +## Cluster + +{cluster} diff --git a/reflexio/integrations/openclaw-embedded/plugin/prompts/playbook_extraction.md b/reflexio/integrations/openclaw-embedded/plugin/prompts/playbook_extraction.md new file mode 100644 index 0000000..66f9e9e --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/prompts/playbook_extraction.md @@ -0,0 +1,217 @@ +--- +active: true +description: "Playbook extraction for Reflexio Embedded plugin (ported from playbook_extraction_context/v2.0.0)" +changelog: "Initial port (2026-04-16): 6-field output (trigger/instruction/pitfall/rationale/blocking_issue/content) collapsed to 3-field (When/What/Why); autoregressive ordering adapted to Why → What → When internally, emitted as When → What → Why; expert-mode branches (tool_can_use, blocking_issue kinds, agent_context) dropped; strengthened explicit requirement that a confirmation signal must follow any correction before writing a playbook." +variables: + - transcript +--- + +You extract procedural rules (playbooks) from conversations where the user corrected the agent and the correction stuck. Your output becomes entries under `.reflexio/playbooks/`. + +You are a self-improvement policy mining assistant for AI agents. Your job is to extract **generalizable Standard Operating Procedures (SOPs)** that the agent should adopt to avoid repeating mistakes. + +You are NOT extracting: +- User facts (e.g., "User is building a React app") — those belong to the profile extractor. +- One-off preferences (e.g., "User likes blue buttons"). +- Surface phrasing (e.g., "User said 'don't say that'"). + +You ARE extracting: +- **Behavioral Policies:** "When user intent is X, always do Y." +- **Correction Rules:** "When user encounters problem Z, avoid approach A." +- **Tool Usage Policies:** + - Tool selection: "When user intent is X, use tool Y instead of tool Z." + - Tool input optimization: "When using tool Y for intent X, set parameter P to value V." + +━━━━━━━━━━━━━━━━━━━━━━ +## When to write a playbook (strict) + +A playbook is warranted only when **BOTH** of the following are observed in the transcript: + +1. **Correction.** The user tells the agent to change its approach. Typical phrases: "No, that's wrong", "Actually…", "Don't do X", "Not like that", "We don't use X here." This may also appear as agent self-correction (retrying a tool call with different inputs after poor results, or switching from one tool to another within the same task). +2. **Confirmation.** After the agent adjusts, the user signals acceptance — either explicitly ("good", "perfect", "yes that's right", "thanks, that works") or implicitly (the user moves on to an unrelated topic for 1–2 turns without re-correcting). + +**Do NOT write a playbook if you see a correction without a following confirmation.** The fix may itself be wrong; let the batch pass at session end reconsider. A correction with no subsequent signal of acceptance is insufficient evidence that the new behavior is correct. + +In addition, for the candidate playbook to be valid, ALL of the following must be true: +1. The agent performed an action, assumption, or default behavior. +2. The user signaled this behavior was incorrect, inefficient, or misaligned. +3. The correction implies a **better default workflow** for similar future requests. +4. The rule can be phrased as: *"When [User Intent/Problem], the agent should [Policy]."* + +━━━━━━━━━━━━━━━━━━━━━━ +## Valid correction signals + +Look for cross-turn causal patterns, not isolated messages. Valid signals include: +- User correcting or rejecting the agent's approach +- User redirecting the agent to a different mode or level of detail +- User expressing dissatisfaction with how the agent behaved +- User clarifying expectations that contradict the agent's behavior +- Agent retrying a tool call with different inputs after getting poor or irrelevant results (self-correction) +- Agent switching from one tool to another within the same task after inadequate results + +You MUST identify the triggering agent behavior (assumption made, default chosen, constraint ignored, or question not asked). + +━━━━━━━━━━━━━━━━━━━━━━ +## The Skill Test (what makes a good trigger) + +A valid **When** trigger must act as a **Skill Trigger** — it describes the **problem or situation**, NOT the user's explicitly stated preference. + +- **BAD (Topic-based):** "User talks about Python code." (Too broad) +- **BAD (Interaction-based):** "User corrects the agent." (Too generic) +- **BAD (Echoing preference):** "User requests CLI tools or open-source solutions." (Just restates the user's explicit ask — the agent didn't need an SOP to follow direct instructions.) +- **GOOD (Intent-based):** "User requests help debugging a specific error trace." +- **GOOD (Problem-based):** "User's initial high-level request is ambiguous." +- **GOOD (Situation-based):** "User reports timeout or performance failures on large data transfers (>10TB)." (Captures the situation where the agent should default to CLI/chunking solutions.) + +**Tautology Check.** If the trigger can be reduced to "user asks for X" and the action is "do X", the playbook is tautological. Re-derive: What was the *problem or situation* where the agent made the wrong default choice? Use THAT as the trigger. + +━━━━━━━━━━━━━━━━━━━━━━ +## Reasoning procedure (required) + +Generate each playbook internally in the order **Why → What → When** (rationale conditions the action; both condition the trigger), but **emit** the sections in the document as **When → What → Why**: + +1. Identify user turns containing correction, rejection, or redirection. +2. Trace backwards to the exact agent behavior that triggered it. +3. Identify the violated implicit expectation — this is the seed of **Why**. +4. Verify a confirmation signal follows within 1–2 turns. If not, abandon this candidate. +5. Define the SOP action — this becomes **What**. Include DOs and DON'Ts only as they were actually observed in this conversation. Do not force DO/DON'T symmetry if only one side was learned; forcing symmetry leads to hallucinated pitfalls. +6. Draft the SOP trigger — this becomes **When**. Apply the Skill Test and Tautology Check. +7. Check that the trigger is the search anchor a future agent would retrieve on. Rewrite as a concise noun phrase describing the *situation*, not a sentence. +8. Repeat steps 1–7 for **every distinct** behavioral issue you identified. Each independent policy becomes a separate entry in the output list. + +━━━━━━━━━━━━━━━━━━━━━━ +## Output format + +Each playbook has three body sections: `## When`, `## What`, `## Why`. + +- `## When` — One-sentence trigger phrase. This is the search anchor used for retrieval. Write as a noun phrase describing the *situation*, not a sentence. Apply the Skill Test. +- `## What` — 2–3 sentences of the actual procedural rule. Include DOs and DON'Ts as they were actually observed — do not force DO/DON'T symmetry if only one was learned. +- `## Why` — Rationale. Reference the specific correction+confirmation evidence from the transcript when helpful. Can be longer than the other sections — it is reference material, not recall content. + +Return a JSON array of objects. If nothing to extract, return `[]`. + +```json +[ + { + "topic_kebab": "commit-no-ai-attribution", + "when": "Composing a git commit message on this project.", + "what": "Write conventional, scope-prefixed messages. Do not add AI-attribution trailers like `Co-Authored-By`.", + "why": "On [date] the user corrected commits that included Co-Authored-By trailers. Project's git-conventions rule prohibits them. Correction stuck — the user's next request assumed the rule was internalized." + } +] +``` + +- `topic_kebab`: kebab-case slug, ≤ 48 chars, regex `^[a-z0-9][a-z0-9-]*$`. A compression of the situation the rule applies to (e.g., `commit-no-ai-attribution`, `debug-explain-root-cause-first`, `search-specific-query-first`). + +━━━━━━━━━━━━━━━━━━━━━━ +## Examples + +**Example 1 (single entry):** +- **Agent:** Jumps directly into code generation. +- **User:** "Don't give me the code yet, explain the strategy first." +- **Agent:** Outlines strategy. +- **User:** "Good, now proceed." ← confirmation + +```json +[ + { + "topic_kebab": "strategy-before-code", + "when": "User asks for architectural advice or complex implementation help.", + "what": "Outline the high-level strategy before generating code. Do not jump straight to code when the user has not explicitly asked for it.", + "why": "The agent assumed the user wanted code immediately, but the user needed to understand the approach first. Presenting strategy first prevents wasted effort on wrong approaches and gives the user the understanding they need to evaluate the plan." + } +] +``` + +**Example 2 (single entry):** +- **Agent:** Suggests `pip install …`. +- **User:** "Stop using pip, I'm using poetry." +- **Agent:** Reissues command as `poetry add …`. +- **User:** "Thanks." ← confirmation + +```json +[ + { + "topic_kebab": "detect-package-manager", + "when": "User asks for package installation commands.", + "what": "Detect or ask for the project's package manager preference before suggesting install commands.", + "why": "The agent defaulted to pip without checking the project setup, which produced commands incompatible with the user's poetry-based workflow. Detecting the package manager first avoids this whole class of mistakes." + } +] +``` + +**Example 3 (tool input optimization via retry pattern):** +- **Agent:** `[used tool: search_docs({"query": "error"})]` → irrelevant results. +- **Agent:** `[used tool: search_docs({"query": "TypeError in async handler", "filter": "error_logs"})]` → relevant results. +- **User:** "Perfect, that's the one." ← confirmation + +```json +[ + { + "topic_kebab": "search-specific-query-first", + "when": "Agent is searching docs for a specific error or issue the user reported.", + "what": "Use the exact error message and relevant filters as search parameters on the first attempt.", + "why": "On the first attempt the agent used a vague single-word query and wasted a round-trip. The second attempt, with the exact error text and an error_logs filter, succeeded immediately. Specific queries on the first call reduce latency and cost." + } +] +``` + +**Example 4 (avoiding tautological triggers):** +- **User:** Reports S3 sync timeout on 12 TB backup. Agent suggests UI settings. User says "I need CLI-based automation." Agent suggests a proprietary tool. User says "I want open-source only." Agent finally suggests rclone with chunking. User: "That works." ← confirmation + +- **BAD (Tautological) — do NOT emit:** +```json +[{"when": "User requests CLI or open-source automation tools.", "what": "Suggest CLI/open-source tools.", "why": "…"}] +``` +This just echoes the correction. The agent doesn't need an SOP to follow explicit instructions. + +- **GOOD:** +```json +[ + { + "topic_kebab": "large-transfer-cli-chunking", + "when": "User reports timeout or performance failure transferring large datasets (>10 TB) to cloud storage.", + "what": "Default to CLI-based chunking / parallel transfer solutions (e.g., rclone) and provide config snippets.", + "why": "The agent defaulted to GUI settings and then to a proprietary tool for a large-data transfer problem, both of which the user rejected before rclone with chunking finally worked. CLI-based chunking is the standard solution for transfers at this scale — extract the *situation* (large transfer timeout), not the user's tool preference." + } +] +``` + +**Example 5 (multiple distinct entries from one conversation):** +- **Conversation:** Multi-turn debugging session. (a) User asks for help fixing a `TypeError` but the agent jumps straight to code rewrites instead of explaining the root cause; after the user says "explain first", agent does so, user says "good". (b) The agent apologizes ("I'm sorry for the confusion") every time it provides a factual correction, even when no mistake occurred; user says "stop apologizing, just tell me what's correct"; agent complies; session continues. + +Both corrections have matching confirmations, so both become playbooks: + +```json +[ + { + "topic_kebab": "debug-explain-root-cause-first", + "when": "User asks for help debugging a specific error trace.", + "what": "Explain the root cause of the error before proposing any code changes. Do not jump straight to rewriting code.", + "why": "The user needs to understand why the error occurred in order to evaluate the fix. Skipping the explanation leaves them without the context they need and leads to repeated back-and-forth." + }, + { + "topic_kebab": "corrections-no-unnecessary-apology", + "when": "Agent provides a factual correction or updated diagnosis during a multi-turn session.", + "what": "Deliver the correction plainly. Do not preface routine corrections with an apology when no actual mistake occurred.", + "why": "Apologies on every refinement eroded user confidence. Reserve apologies for genuine errors so they retain meaning." + } +] +``` + +━━━━━━━━━━━━━━━━━━━━━━ +## Rules for output fields + +- The top-level response MUST be a JSON array (possibly empty). +- Each entry MUST have `topic_kebab`, `when`, `what`, `why`. All four are REQUIRED. +- `when` is the search anchor — keep it short, situation-focused, and Skill-Test-valid. +- `what` captures only DOs and DON'Ts that the conversation actually evidenced. Do NOT invent a symmetric DON'T for every DO (or vice versa). +- `why` must reference the specific agent behavior that triggered the correction AND confirm that a confirmation signal followed. +- Each entry must describe a **distinct, independent** policy — do not split one policy across entries, and do not merge two independent policies into one. +- If any candidate fails the confirmation gate, the Skill Test, or the Tautology Check, drop it. +- Vague, stylistic, or unanchored advice is invalid. +- **Never capture secrets or credentials in a playbook.** If a correction or execution trace contains API keys, tokens, passwords, or other credential material, redact that content from the `why` field before emitting the playbook. Do not emit a playbook whose `why` requires quoting credentials verbatim. + +## Transcript + +{transcript} diff --git a/reflexio/integrations/openclaw-embedded/plugin/prompts/profile_extraction.md b/reflexio/integrations/openclaw-embedded/plugin/prompts/profile_extraction.md new file mode 100644 index 0000000..7042ed3 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/prompts/profile_extraction.md @@ -0,0 +1,131 @@ +--- +active: true +description: "Profile extraction for Reflexio Embedded plugin (ported from profile_update_instruction_start/v1.0.0)" +changelog: "Initial port (2026-04-16): output adapted from StructuredProfilesOutput JSON to list of {topic_kebab, content, ttl} suitable for the reflexio_write_profile tool; custom_features and metadata fields dropped; existing_profiles variable now injected from memory_search results rather than Reflexio server." +variables: + - existing_profiles_context + - transcript +--- + +You extract durable user facts from conversations. Your output becomes entries under `.reflexio/profiles/`. + +[Goal] +You are a user personalization learning assistant. Your job is to analyze user–agent interactions and extract **salient information about the user** that should shape how an AI agent communicates with and serves this user in future conversations. + +A "profile" can be: +- **Factual information**: Direct facts about the user (name, birthday, occupation, location) +- **Work & expertise**: Professional role, technical skills, domain knowledge, tools used daily +- **Goals & projects**: Current objectives, ongoing projects, deadlines, milestones +- **Life circumstances**: Living situation, health considerations, family context, time constraints +- **Relationships & family**: Family members, pets, key people in their life +- **Domain / environment facts**: Stable facts about the user's working environment that the agent needs to know to serve them correctly — schema details (table names, column types, units of measurement), join paths, metric definitions the user enforces, tool quirks or limitations the user works around, file-format conventions, and similar reference knowledge. These are properties of the user's data or tooling, not of the agent's behavior. +- **Inferred personalization signals**: Patterns derived from behavior or multi-turn inference that would cause an agent to meaningfully change how it responds + +Profiles may be extracted from: +- Explicit statements ("I prefer sushi") +- Implicit signals (implied preference/acceptance/rejection) +- Multi-turn inference (a stable pattern across multiple turns where the user never explicitly states a preference, but behavior suggests one) + +**Scope boundary — what is NOT a profile:** +Profiles capture what is *true about the user or their world*. They do NOT capture +*what the agent should do differently next time*. If a learning is a behavioral +rule for the agent (e.g., "when the user asks about methodology, stop and propose +a corrected plan before re-running queries", "always explain root cause before +proposing code changes"), it belongs in the playbook extractor, not here. Extract +only the stable facts the agent needs to know; leave the action rules to playbooks. + +[Your Task — Follow These Steps] + +STEP 1: Analyze and reason through the user interactions below and compare them to the existing profiles shown in the `{existing_profiles_context}` section. + +STEP 2: Decide what to extract: +- If the interaction reveals NEW information about the user that is NOT already stored in existing profiles → Extract it as a new profile +- If the information is already in existing profiles → Do NOT re-extract it +- If the interaction contains NO relevant profile information → Return an empty list + +STEP 3: For each profile you extract, you must assign: +- `topic_kebab`: short kebab-case slug, ≤ 48 chars, regex `^[a-z0-9][a-z0-9-]*$`. A semantic compression of the fact (e.g., `diet-vegetarian`, `role-backend-engineer`, `schema-orders-gross-cents`). +- `content`: 1–3 sentences, one fact per entry, written as a standalone statement about the user. +- `ttl`: one of the values from the TTL table below. + +[Time to Live — Choose One] +- `infinity` → Facts that rarely change (name, birthday, gender, phone number) +- `one_year` → Long-term preferences and slow-moving context (favorite color, hobby, long-lived schema facts) +- `one_quarter` → Seasonal preferences or quarter-scoped projects (winter activities, holiday traditions, Q-end deadlines) +- `one_month` → Regular preferences (food preferences, UI preferences) +- `one_week` → Short-term or situational preferences (current project, temporary need) +- `one_day` → Very short-lived context (today's deadline, scratch preference) + +[Output Format] +Return a JSON array of objects. Each object represents one profile to write. If nothing to extract, return `[]`. + +```json +[ + { + "topic_kebab": "diet-vegetarian", + "content": "User is vegetarian — no meat or fish.", + "ttl": "infinity" + } +] +``` + +[Examples With Explanations] + +Example 1 — Extracting a new profile: +- Existing profiles: none related +- Interaction: "i like to eat pizza" +- Reasoning: User revealed a food preference. Not in existing profiles. Food preferences change monthly, so ttl is `one_month`. +```json +[{"topic_kebab": "food-likes-pizza", "content": "User likes pizza.", "ttl": "one_month"}] +``` + +Example 2 — Extracting a permanent profile: +- Interaction: "my name is John" +- Reasoning: User's name is a permanent fact. Use `infinity`. +```json +[{"topic_kebab": "name-john", "content": "User's name is John.", "ttl": "infinity"}] +``` + +Example 3 — Extracting work context and project: +- Existing profiles: none +- Interaction: "I'm a senior backend engineer at Acme Corp, currently migrating our payment service from monolith to microservices. The deadline is end of Q2." +- Reasoning: Role is long-lived; the migration project is quarter-scoped. +```json +[ + {"topic_kebab": "role-senior-backend-acme", "content": "User is a senior backend engineer at Acme Corp.", "ttl": "one_year"}, + {"topic_kebab": "project-payments-microservices", "content": "User is migrating Acme's payment service from monolith to microservices, with an end-of-Q2 deadline.", "ttl": "one_quarter"} +] +``` + +Example 4 — No relevant information: +- Interaction: "what time is it?" +- Reasoning: Nothing to extract. +```json +[] +``` + +Example 5 — Domain / environment fact surfaced through error correction: +- Existing profiles: none +- Interaction: Agent attempts `SUM(orders.total_amount)` and fails with "unknown column 'total_amount'". Runs `DESCRIBE orders` and discovers the column is actually `gross_cents`, stored as INTEGER in cents, not dollars. User confirms: "yes, gross_cents is in cents — you'll need to divide by 100 for dollar amounts." +- Reasoning: This is a stable fact about the user's data schema (column name, type, unit). Store as a long-lived domain fact. +```json +[{"topic_kebab": "schema-orders-gross-cents", "content": "orders.gross_cents stores order revenue as INTEGER in cents (not dollars) — divide by 100 for dollar amounts.", "ttl": "one_year"}] +``` + +Note: if the same session also surfaces a behavioral rule (e.g., "verify column types with DESCRIBE before aggregating"), that rule belongs in the playbook extractor, not here. This example captures only the fact. + +[Important Reminders] +1. Only extract profiles that represent salient, stable facts about the user or their world. +2. If the information is already captured in existing profiles (see below), do NOT re-extract it. +3. Always include `ttl` for new profiles; pick the shortest TTL that the fact will plausibly remain true for. +4. Never output behavioral rules for the agent here — those belong to the playbook extractor. +5. **Never extract secrets or credentials.** Do not create profile entries for API keys, access tokens, passwords, OAuth secrets, private keys, auth headers, `.env` values, connection strings, or any other credential-shaped content, even if the user pasted such content into the conversation. Treat those as noise; skip them. +6. Return `[]` when there is nothing new to extract. + +## Existing profiles for context (do NOT re-extract these) + +{existing_profiles_context} + +## Transcript + +{transcript} diff --git a/reflexio/integrations/openclaw-embedded/plugin/prompts/shallow_dedup_pairwise.md b/reflexio/integrations/openclaw-embedded/plugin/prompts/shallow_dedup_pairwise.md new file mode 100644 index 0000000..4c81134 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/prompts/shallow_dedup_pairwise.md @@ -0,0 +1,47 @@ +--- +active: true +description: "Pairwise shallow dedup decision for openclaw-embedded plugin" +changelog: "Initial (2026-04-16): new prompt, informed by Reflexio's profile_deduplication and playbook_deduplication prompts, simplified to strictly pairwise (candidate vs top-1 neighbor)" +variables: + - candidate + - neighbor +--- + +You decide whether a newly-extracted item should be merged with an existing one. + +## Inputs + +- **Candidate**: a newly-extracted profile or playbook. +- **Neighbor**: the single most-similar existing item (top-1 from memory_search). + +## Decision + +Output one of: + +- `keep_both` — the two items cover distinct facts; keep both. +- `supersede_old` — the candidate is a strict replacement (e.g., new info contradicts old; user restated a preference more fully). Old file will be deleted. +- `merge` — the two items overlap but neither fully subsumes the other; synthesize a merged version. Old file will be deleted and merged version written. +- `drop_new` — the existing neighbor already covers what the candidate says; discard the candidate. + +### Contradiction handling + +If the two items describe the same topic but assert conflicting facts, prefer the newer-created one (`supersede_old`) unless the content indicates the older is more specific or authoritative. + +## Output schema + +```json +{ + "decision": "keep_both | supersede_old | merge | drop_new", + "merged_content": "string — required if decision == merge; the synthesized content body", + "merged_slug": "string — required if decision == merge; kebab-case, regex `^[a-z0-9][a-z0-9-]{0,47}$`, for the new file", + "rationale": "string — always required; 1-2 sentences justifying the decision" +} +``` + +## Candidate + +{candidate} + +## Neighbor + +{neighbor} diff --git a/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-consolidate/SKILL.md b/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-consolidate/SKILL.md new file mode 100644 index 0000000..2533c5f --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-consolidate/SKILL.md @@ -0,0 +1,33 @@ +--- +name: reflexio-consolidate +description: "Run a full-sweep consolidation over all .reflexio/ files — TTL sweep + n-way cluster merge. Use when the user asks to 'clean up reflexio', 'consolidate memory', 'deduplicate playbooks', or suspects drift across sessions." +--- + +# Reflexio Consolidate + +User-invocable via `/skill reflexio-consolidate`. Same workflow that runs automatically via heartbeat (every 24h of active use), but on-demand. + +## What it does + +1. TTL sweep: delete expired profile files. +2. For each of profiles and playbooks: + - Cluster similar files via `memory_search`. + - For clusters of 2+ members, call `llm-task` with `prompts/full_consolidation.md` to decide merge / subset / keep-all. + - Apply decisions: write merged files with `supersedes:` frontmatter, unlink merged originals. + +## How to run + +Call the `reflexio_run_consolidation` tool. It spawns the consolidator sub-agent and returns a `runId`. + +After successful consolidation, call the `reflexio_consolidation_mark_done` tool to update the heartbeat timer. + +## When to use + +- User asks to "consolidate", "clean up reflexio", "dedupe memory" +- User reports seeing duplicate or contradictory entries in retrieval +- Heartbeat check returns ALERT (automatic trigger) + +## When NOT to use + +- Routine maintenance — heartbeat handles this automatically. +- Immediately after Flow A/B writes — shallow dedup at write time + Flow C at session end cover the fresh-extraction cases. diff --git a/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-embedded/SKILL.md b/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-embedded/SKILL.md new file mode 100644 index 0000000..af617a6 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-embedded/SKILL.md @@ -0,0 +1,194 @@ +--- +name: reflexio-embedded +description: "Captures user facts and procedural corrections into .reflexio/ so the agent learns across sessions. Use when: (1) user states a preference, fact, config, or constraint; (2) user corrects the agent and confirms the fix with an explicit 'good'/'perfect' or by moving on without re-correcting for 1-2 turns; (3) at start of a user turn, to retrieve relevant facts and playbooks from past sessions." +metadata: +--- + +# Reflexio Embedded Skill + +Captures user facts (profiles) and procedural corrections (playbooks) into `.reflexio/`, so the agent learns across sessions. All memory lives in Openclaw's native primitives — no external service required. + +## First-time setup per agent + +If `.reflexio/.setup_complete_` does NOT exist (where `` is your current agent id), perform this one-time check. The setup step runs probing commands via `exec` and asks for approval before making changes. + +**Steps:** + +1. Probe current config: + - `openclaw config get plugins.entries.active-memory.config.agents` + - `openclaw config get agents.defaults.memorySearch.extraPaths` + - `openclaw memory status --deep` + +2. If active-memory is not targeting this agent: + Ask user: *"To auto-inject relevant facts into each turn, I can enable active-memory for this agent. OK if I run `openclaw config set plugins.entries.active-memory.config.agents '[\"\"]' --strict-json`?"* + On approval, run the command. + +3. If `.reflexio/` is not registered as an extraPath: + Ask user: *"I need to register .reflexio/ as a memory path. OK if I run `openclaw config set agents.defaults.memorySearch.extraPaths '[\".reflexio/\"]' --strict-json`?"* + On approval, run the command. + +4. If no embedding provider is configured (FTS-only mode): + Tell user: *"Vector search requires an embedding API key (OpenAI, Gemini, Voyage, or Mistral). The plugin works without one but retrieval quality drops. Would you like guidance on adding one?"* + If yes, guide them through `openclaw config set` or `openclaw configure`. + +5. On each decline, note the degraded mode but do not block: + - No active-memory → you must run `openclaw memory search` via exec at turn start (see "Retrieval" section below). + - No extraPath → WARN the user the plugin cannot function without this step. + - No embedding → continue with FTS-only. + +6. When all checks resolved (approved or accepted with warning): create the marker: + ```bash + mkdir -p .reflexio + touch .reflexio/.setup_complete_ + ``` + +**If exec is not available** (strict admin policy): fall back to telling the user the exact commands to run manually. + +## First-Use Initialisation + +Before any write, ensure `.reflexio/` and its subdirectories exist. This is idempotent — safe to run every session: + +```bash +mkdir -p .reflexio/profiles .reflexio/playbooks +``` + +Never overwrite existing files. Never write secrets, tokens, private keys, environment variables, or credentials into `.reflexio/` files. When capturing a fact involves a user-pasted snippet that contains credentials, redact first. + +## Quick Reference + +| Situation | Action | +|-----------------------------------------------------------|--------------------------------------------| +| User states preference, fact, config, or constraint | `reflexio_write_profile` tool | +| User correction → you adjust → user confirms | `reflexio_write_playbook` tool | +| Start of user turn, need context | `reflexio_search` tool | +| Unsure whether to capture | Skip; batch pass at session-end has a second shot | + +## Detection Triggers + +### Profile signals (write immediately, same turn) + +- **Preferences**: "I prefer X", "I like Y", "I don't like Z", "I always do Q" +- **Facts about self**: "I'm a [role]", "my timezone is X", "I've been doing Y for Z years" +- **Config**: "use X", "our team uses Y", "the repo is at Z" +- **Constraints**: "I'm vegetarian", "no dairy", "I can't X", "don't use Y" + +For each such signal, call the `reflexio_write_profile` tool with a kebab-case topic slug and an appropriate TTL. See "TTL Selection" below. + +### Playbook signals (write AFTER confirmation) + +Playbooks require a specific multi-turn pattern: + +1. **Correction**: *"No, that's wrong"*, *"Actually..."*, *"Don't do X"*, *"Not like that"*, *"We don't use X here"*. +2. **You adjust**: you redo the work per the correction. +3. **Confirmation** (required — without this, do NOT write a playbook): + - Explicit: *"good"*, *"perfect"*, *"yes that's right"*, *"correct"*. + - Implicit: the user moves to an unrelated topic without re-correcting for 1-2 more turns. + +**Explicit don't-write rule**: if you see a correction without subsequent confirmation, do not write a playbook. The fix may be wrong; let the batch pass at session end re-evaluate. + +## Retrieval + +### When Active Memory is enabled + +Your turn context may already contain Reflexio-prefixed entries injected by Active Memory. Incorporate them before responding. No tool call needed. + +### Fallback when Active Memory is absent + +At the start of each user turn, call the `reflexio_search` tool with: +- query: "" + +The tool handles query preprocessing and memory search internally. +Incorporate any results into your response. Skip if the user's message is trivial. + +**Important:** Do NOT use the `memory_search` tool (returns config, not results) +or `exec` with `openclaw memory search` — use the `reflexio_search` tool instead. + +## File Format + +**Do NOT construct filenames or frontmatter by hand.** Use the registered tools (`reflexio_write_profile`, `reflexio_write_playbook`). They generate IDs, enforce the frontmatter schema, and write atomically. + +### Profile template (for mental model — the script emits this) + +```markdown +--- +type: profile +id: prof_ +created: +ttl: +expires: +supersedes: [] # optional, only after a merge +--- + +<1-3 sentences, one fact per file> +``` + +### Playbook template + +```markdown +--- +type: playbook +id: pbk_ +created: +supersedes: [] # optional +--- + +## When +<1-sentence trigger — this is the search anchor; make it a noun phrase> + +## What +<2-3 sentences of the procedural rule; DO / DON'T as actually observed> + +## Why + +``` + +### How to invoke + +**Profile:** Call the `reflexio_write_profile` tool with: +- slug: "diet-vegan" +- ttl: "infinity" +- body: "User is vegan. No meat, no fish, no dairy, no eggs." + +**Playbook:** Call the `reflexio_write_playbook` tool with: +- slug: "commit-no-ai-attribution" +- body: "## When\nComposing a git commit message.\n\n## What\nNo AI-attribution trailers.\n\n## Why\nUser corrected this." + +**Retrieve context:** Call the `reflexio_search` tool with: +- query: "user's question here" + +All tools handle preprocessing, memory search, contradiction detection, and file operations internally. You only detect the signal, compose the content, and call the tool. + +## TTL Selection (profiles only) + +- `infinity` — durable, non-perishable facts (diet, name, permanent preferences) +- `one_year` — stable but could plausibly change (address, role, team) +- `one_quarter` — current focus (active project, sprint theme) +- `one_month` — short-term context +- `one_week` / `one_day` — transient (today's agenda, this week's priorities) + +Pick the most generous TTL that still reflects reality. When in doubt, prefer `infinity` — let dedup handle later contradictions via supersession. + +## Safety + +- **Never write secrets.** No API keys, tokens, access tokens, private keys, environment variables, OAuth secrets, auth headers. If the user's message contains any of these, redact them before writing. +- **Redact pasted code.** User-pasted snippets often contain credentials. Strip them first. +- **PII.** Do not capture PII beyond what's operationally useful (name, timezone, role are fine; government IDs, addresses, phone numbers only if explicitly relevant). + +## Best Practices + +1. **Write immediately** on a clear signal. Don't queue to session-end — that's Flow C's job; you have a different role. +2. **One fact per profile file.** Multi-fact files are harder to dedupe and easier to contradict. +3. **Trigger phrase = search anchor.** Write `## When` as a noun phrase describing the situation, not a sentence. Retrieval hits on semantic similarity to this field. +4. **Skip writing when uncertain.** Flow C has a second pass over the full transcript. It's better to let it handle ambiguous cases. +5. **Prefer shorter TTL for transient facts.** Don't let "working on project X" accumulate as infinity-TTL cruft. + +## Opt-in Hook + +This skill works standalone — your in-session Flow A (profile) and Flow B (playbook) writes populate `.reflexio/` without any hook. + +The optional hook (`hook/` directory of this plugin) adds two capabilities: + +1. **TTL sweep at session start**: deletes expired profiles before Active Memory runs. +2. **Session-end batch extraction (Flow C)**: on `session:compact:before`, `command:stop`, or `command:reset`, spawns a `reflexio-extractor` sub-agent that extracts profiles/playbooks from the full transcript and runs shallow pairwise dedup. + +See this plugin's `README.md` for install instructions (runs via `./scripts/install.sh`). If the hook is not installed, Flows A+B still work. diff --git a/reflexio/integrations/openclaw-embedded/references/HOOK.md b/reflexio/integrations/openclaw-embedded/references/HOOK.md new file mode 100644 index 0000000..0a4135d --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/references/HOOK.md @@ -0,0 +1,18 @@ +--- +name: reflexio-embedded +description: "Reflexio Embedded hook: TTL sweep on bootstrap; spawn reflexio-extractor sub-agent at session boundaries." +metadata: + openclaw: + emoji: "🧠" + events: + - "before_agent_start" + - "before_compaction" + - "before_reset" + - "session_end" +--- + +> **Note:** This file is no longer discovered by the Openclaw CLI. Hooks are +> now registered programmatically from `../index.ts` via +> `definePluginEntry({ register(api) { api.on(...) } })`. This doc is kept +> only for human reference; see `../index.ts` and `./handler.ts` for the +> actual behaviour. diff --git a/reflexio/integrations/openclaw-embedded/references/architecture.md b/reflexio/integrations/openclaw-embedded/references/architecture.md new file mode 100644 index 0000000..93789eb --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/references/architecture.md @@ -0,0 +1,49 @@ +# Architecture + +Deep-dive for maintainers. For a design-level overview, see the spec at +`docs/superpowers/specs/2026-04-16-reflexio-openclaw-embedded-plugin-design.md`. + +## Three flows + +| Flow | Trigger | Actor | Purpose | +|------|---------|-------|---------| +| A | User message signals a profile | Main agent (skill-guided) | Capture durable user facts mid-turn | +| B | Correction → adjust → confirmation | Main agent (skill-guided) | Capture procedural rules after confirmation | +| C | `session:compact:before`, `command:stop`, `command:reset` | `reflexio-extractor` sub-agent spawned via hook | Batch extract + shallow dedup over full transcript | +| Cron | Daily 3am | `reflexio-consolidator` sub-agent | Full n-way consolidation + TTL sweep | + +## File system invariants + +- `.reflexio/profiles/*.md` and `.reflexio/playbooks/*.md` — the only user-owned data. +- All files are **immutable in place**. Dedup = delete old + create new (atomic via `.tmp` + rename). +- Every file has frontmatter with `type`, `id`, `created`. Profiles also have `ttl` and `expires`. +- `supersedes: [id1, id2]` — optional, records merge lineage. + +## Concurrency model + +- Flow A + Flow B are create-only; no coordination. +- Flow C runs as a sub-agent spawned via `api.runtime.subagent.run()` — non-blocking, tracked by Openclaw's Background Tasks ledger. +- Parallel Flow C runs can occur; rare race resolves next cycle (later write wins, orphans swept by full consolidation). + +## Openclaw primitives leveraged + +- **Memory engine** (`concepts/memory-builtin`): indexes `.md` files under `extraPaths`. +- **Active Memory** (`concepts/active-memory`): optional, injects retrieved context into turns. +- **Hooks** (`automation/hooks`): lifecycle events (bootstrap, compact, stop, reset). +- **Sub-agents** (`tools/subagents`): fire-and-forget work via `sessions_spawn` / `api.runtime.subagent.run()`. +- **LLM-task** (`tools/llm-task`): structured LLM calls with schema validation. +- **Cron** (`automation/cron-jobs`): daily consolidation. +- **Registered tools**: `reflexio_write_profile`, `reflexio_write_playbook`, and `reflexio_search` — the plugin's tool-based interface for writes and retrieval. + +## Prompt loading + +Prompts live in `prompts/` and are loaded at runtime by sub-agents. Frontmatter follows Reflexio's prompt_bank convention (`active`, `description`, `changelog`, `variables`). When the sub-agent builds a `llm-task` call, it reads the relevant prompt file, substitutes variables from the event / memory_search results / candidate data, and sends the result to `llm-task` with an output schema. + +## Graceful degradation + +| Missing prereq | Behavior | +|---|---| +| `active-memory` not enabled | SKILL.md instructs agent to run `memory_search` fallback at turn start | +| No embedding provider | Falls back to FTS/BM25 only; vector search unavailable but plugin functional | +| Registered tools unavailable | SKILL.md falls back to printed manual commands; install.sh exits with instructions | +| No `openclaw cron add` | install.sh prints warning; consolidation runs only on `/skill reflexio-consolidate` | diff --git a/reflexio/integrations/openclaw-embedded/references/comparison.md b/reflexio/integrations/openclaw-embedded/references/comparison.md new file mode 100644 index 0000000..1568c47 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/references/comparison.md @@ -0,0 +1,31 @@ +# openclaw-embedded vs integrations/openclaw + +Two Openclaw integrations ship in the same repo. This doc explains when to pick which. + +| Aspect | `integrations/openclaw/` (federated) | `integrations/openclaw-embedded/` (this plugin) | +|---|---|---| +| Reflexio server required | Yes | No | +| Storage | Reflexio server (SQLite / Supabase / pgvector) | Openclaw's native memory (`.md` files at `.reflexio/`) | +| Extraction LLM | Server-side (uses Reflexio's prompt bank on the server) | Openclaw's agent LLM + sub-agents (via `api.runtime.subagent.run()`) | +| Dedup | Server-side batch + cross-instance aggregation | Per-write shallow + daily full-sweep | +| Multi-user / multi-agent | Yes (each `agentId` → distinct `user_id`) | No (one instance = one user) | +| Cross-instance playbook sharing | Yes (aggregation → agent playbooks) | No (v1 out of scope) | +| External dependencies | `reflexio` CLI, LLM API key on the server, running server | Openclaw only (agent's own LLM) | +| CLI integration | Shells out to `reflexio search / publish / aggregate` | None — pure in-Openclaw | +| Target user | Teams with many agent instances behind a shared server | Solo user, no infrastructure | + +## Pick `openclaw-embedded` if: + +- You don't want to run a Reflexio server. +- You only have one agent instance, or memory sharing across instances doesn't matter. +- You prefer fewer moving parts. + +## Pick `integrations/openclaw/` (federated) if: + +- You have or want to run a Reflexio server. +- You have multiple agent instances and want cross-instance playbook sharing via aggregation. +- You want multi-user support (each human treated as a distinct Reflexio user). + +## Can both be installed? + +Yes — no conflict. Different hook names, different skill names, different cron jobs, different extraPaths. But installing both is pointless: they serve the same purpose by different means. Pick one. diff --git a/reflexio/integrations/openclaw-embedded/references/future-work.md b/reflexio/integrations/openclaw-embedded/references/future-work.md new file mode 100644 index 0000000..4a38c5e --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/references/future-work.md @@ -0,0 +1,48 @@ +# Future Work + +Items explicitly deferred from v1. Each has a rationale for why it's not in scope yet. + +## Convert to code-plugin (`definePluginEntry` + `registerCommand`) + +**Rationale:** V1 uses the legacy "Hook-only / Non-capability" plugin pattern (matches the `self-improving-agent` example). Converting to a code-plugin would gain: +- Real slash commands (`/reflexio-consolidate` instead of `/skill reflexio-consolidate`) +- `api.runtime.config.write` for programmatic config changes (currently we use `exec` + `openclaw config set`) +- Potentially cleaner cron registration (if the plugin SDK exposes it) + +**Cost:** TypeScript plugin scaffolding, SDK version pinning, build pipeline. + +## Cross-instance playbook sharing + +**Rationale:** V1 treats one Openclaw instance as one user. Sharing playbooks across instances would require infrastructure (sync service, shared extraPath on network storage, or federation via Reflexio server). V1 punts this to `integrations/openclaw/` (federated) which already solves it. + +**Option for v2:** Lightweight git-based sync — checkpoint `.reflexio/` to a shared git remote; other instances pull periodically. + +## Participation in Openclaw's native dreaming + +**Rationale:** Openclaw's memory-core plugin has a built-in "dreaming" consolidation system (opt-in, runs daily at 3am). Our full-sweep consolidation is a parallel structure, not a participant. Participating would require a plugin API not yet exposed by memory-core. + +**Watch for:** an `api.runtime.memoryCore.dreaming.registerConsolidator()` or similar in future Openclaw releases. + +## Ported incremental / expert / should-generate prompt variants + +**Rationale:** Reflexio's prompt_bank has `profile_update_instruction_incremental`, `playbook_extraction_main_expert`, and `playbook_should_generate`. V1 skips these as YAGNI: +- Incremental extraction assumes a standing session with bounded batches; our Flow C extracts full transcripts. +- Expert-mode is for advanced scenarios beyond v1. +- `should_generate` is a cost-saving pre-check; extraction returning an empty list achieves the same at small additional cost. + +## Removed frontmatter fields + +- `source_sessions` — dead-pointer problem; LLM has no reliable way to recover session keys long after the fact. +- `confirmation_kind` — no v1 consumer. +- `confidence` — LLM self-assigned confidence is unreliable; omit rather than hallucinate precision. +- `tags` — agent-assigned, not normalized; semantic search on body content suffices. + +Add any of these in v2 only if a concrete consumer materializes. + +## Native Windows support + +**Rationale:** V1 used shell scripts that assumed Unix. V1.1 moved to registered tools (`reflexio_write_profile`, `reflexio_write_playbook`), which are platform-agnostic. This item is resolved. + +## Playbook TTL / expiration + +**Rationale:** The 99% case is "playbooks never expire". V1 drops the field; occasional task-specific playbooks encode time bounds in the `## When` text. V2 could add structured playbook expiration if a concrete use case materializes. diff --git a/reflexio/integrations/openclaw-embedded/references/porting-notes.md b/reflexio/integrations/openclaw-embedded/references/porting-notes.md new file mode 100644 index 0000000..d45d940 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/references/porting-notes.md @@ -0,0 +1,52 @@ +# Porting Notes: Reflexio prompt_bank → openclaw-embedded prompts + +Track every deviation from source prompts so maintainers can re-apply adaptations when upstream versions bump. + +## profile_extraction.md + +**Source:** `open_source/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md` + +**Initial port:** 2026-04-16 + +**Changes from source:** + +- **Output format**: `StructuredProfilesOutput` JSON with `profiles: list[ProfileAddItem{content, ttl, metadata}]` → array of `{topic_kebab, content, ttl}` objects that drive `reflexio_write_profile` tool calls per item. +- **Dropped fields**: `custom_features` dict, `metadata` field. Our frontmatter doesn't carry these. +- **Added guidance**: slug generation rules — kebab-case, ≤48 chars, `[a-z0-9][a-z0-9-]*`. +- **Kept verbatim**: TTL enum semantics, "do NOT re-extract existing profiles" constraint, extraction criteria (what counts as a profile signal). +- **Variable substitution**: `existing_profiles` in source was populated by Reflexio server from SQL. In our port, the Flow C sub-agent runs `memory_search(top_k=10, filter={type: profile})` and injects results into the `{existing_profiles_context}` slot. + +**On upstream upgrade (e.g., to v2.0.0):** diff the source file, re-apply the output-format, dropped-fields, added-guidance deltas. + +## playbook_extraction.md + +**Source:** `open_source/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v2.0.0.prompt.md` + +**Initial port:** 2026-04-16 + +**Changes from source:** + +- **Output schema**: 6-field (`trigger`, `instruction`, `pitfall`, `rationale`, `blocking_issue`, `content`) → 3-field (`When`, `What`, `Why`). + - **Rationale (preserved in design spec section 9):** forcing DO+DON'T symmetry (instruction + pitfall) often leads the LLM to hallucinate a symmetric "don't" when only a "do" was actually observed. Collapsing to `## What` lets the content carry whichever was observed. +- **Autoregressive ordering adapted**: source generates rationale first, then structured fields, then content. Our port generates `Why → What → When` internally but emits `When → What → Why` in the document. +- **Dropped**: expert-mode branches (separate `playbook_extraction_context_expert/` source); `existing_feedbacks` variable (upstream v2 already removed it). +- **Strengthened**: explicit "no confirmation → no playbook" gate. Our skill and prompt both enforce that a correction without a following confirmation is NOT grounds for writing a playbook. +- **Variable substitution**: `{transcript}` is passed by the Flow C sub-agent; no other runtime variables. + +**On upstream upgrade:** diff the source file, re-apply field collapse, ordering adaptation, expert-branch drop, confirmation gate. + +## shallow_dedup_pairwise.md + +**Source:** New file; informed by `profile_deduplication/*.prompt.md` and `playbook_deduplication/*.prompt.md`. + +- Simplified to strictly pairwise (candidate vs. top-1 neighbor). Reflexio's native dedup is N-way group-based. +- Output schema: `{decision: enum, merged_content?, merged_slug?, rationale}`. +- Our full-sweep cron handles N-way; shallow stays in the hot path. + +## full_consolidation.md + +**Source:** New file; informed by `playbook_aggregation/v1.0.0.prompt.md`. + +- Adapted for single-instance n-way clustering; Reflexio's aggregation is cross-instance. +- Output schema: `{action: enum, merged_content?, merged_slug?, ids_merged_in, ids_kept_separate, rationale}`. +- Cluster size capped at 10. diff --git a/reflexio/integrations/openclaw-embedded/scripts/install.sh b/reflexio/integrations/openclaw-embedded/scripts/install.sh new file mode 100755 index 0000000..366b556 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/scripts/install.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# openclaw-embedded install.sh — plugin installation. +# Skills are served from the extension dir via the manifest. +# Agents are injected via extraSystemPrompt at runtime. +# HEARTBEAT.md is appended on first agent session by setup.ts. +set -euo pipefail + +PLUGIN_DIR="$(cd "$(dirname "$0")/../plugin" && pwd)" +OPENCLAW_HOME="${OPENCLAW_HOME:-$HOME/.openclaw}" + +die() { echo "error: $*" >&2; exit 1; } +info() { echo "==> $*"; } + +# 1. Prereq checks +info "Checking prerequisites..." +command -v openclaw >/dev/null || die "openclaw CLI required but not found on PATH" +command -v node >/dev/null || die "node required but not found on PATH" + +# 2. Install the plugin +info "Installing plugin..." +openclaw plugins uninstall --force reflexio-embedded 2>/dev/null || true +rm -rf "$OPENCLAW_HOME/extensions/reflexio-embedded" +openclaw plugins install "$PLUGIN_DIR" +openclaw plugins enable reflexio-embedded 2>/dev/null || true + +# 3. Enable active-memory plugin and configure per-agent targeting +info "Enabling active-memory plugin..." +openclaw plugins enable active-memory || \ + echo "warning: active-memory enable failed — plugin may already be enabled or unavailable; continuing" + +info "Configuring active-memory agent targeting..." +openclaw config set plugins.entries.active-memory.config.agents '["*"]' || \ + echo "warning: active-memory agent targeting config failed" + +info "Registering .reflexio/ as memory extraPath..." +openclaw config set agents.defaults.memorySearch.extraPaths '[".reflexio/"]' --strict-json || \ + echo "warning: extraPath registration failed" + +# 4. Restart gateway +info "Restarting openclaw gateway..." +openclaw gateway restart + +# 5. Verify +info "Verification:" +if openclaw plugins inspect reflexio-embedded 2>/dev/null | grep -q "Status: loaded"; then + info " ✓ plugin registered and loaded" +else + echo " ⚠ plugin did not reach 'loaded' status; run 'openclaw plugins inspect reflexio-embedded' to debug" +fi + +info "Installation complete." +info "Skills are served from the extension dir. HEARTBEAT.md is set up on first agent session." diff --git a/reflexio/integrations/openclaw-embedded/scripts/uninstall.sh b/reflexio/integrations/openclaw-embedded/scripts/uninstall.sh new file mode 100755 index 0000000..54c9d12 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/scripts/uninstall.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# openclaw-embedded uninstall.sh — reverses install.sh. +# Leaves workspace/.reflexio/ user data intact unless --purge is passed. +set -euo pipefail + +OPENCLAW_HOME="${OPENCLAW_HOME:-$HOME/.openclaw}" +PURGE_DATA="${1:-}" + +info() { echo "==> $*"; } + +info "Disabling plugin..." +openclaw plugins disable reflexio-embedded 2>/dev/null || echo "(already disabled)" + +info "Uninstalling plugin..." +openclaw plugins uninstall --force reflexio-embedded 2>/dev/null || echo "(already uninstalled)" +rm -rf "$OPENCLAW_HOME/extensions/reflexio-embedded" + +info "Cleaning up state files..." +rm -f "$OPENCLAW_HOME/reflexio-consolidation-state.json" + +info "Removing heartbeat entry..." +if [[ -f "$OPENCLAW_HOME/workspace/HEARTBEAT.md" ]]; then + sed -i '' '/## Reflexio Consolidation Check/,/^$/d' "$OPENCLAW_HOME/workspace/HEARTBEAT.md" 2>/dev/null || true +fi + +if [[ "$PURGE_DATA" == "--purge" ]]; then + info "Purging .reflexio/ user data per --purge flag..." + rm -rf "$PWD/.reflexio" +else + info "User data at .reflexio/ preserved. Use --purge to delete it too." +fi + +info "Restarting openclaw gateway..." +openclaw gateway restart + +info "Uninstall complete." diff --git a/reflexio/integrations/openclaw-embedded/tests/dedup.test.ts b/reflexio/integrations/openclaw-embedded/tests/dedup.test.ts new file mode 100644 index 0000000..3999cb4 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/tests/dedup.test.ts @@ -0,0 +1,89 @@ +import { describe, it, expect } from "vitest"; + +import { preprocessQuery, judgeContradiction, extractId } from "../plugin/lib/dedup.ts"; +import type { CommandRunner } from "../plugin/lib/openclaw-cli.ts"; + +function createMockRunner(inferResult: string | null): CommandRunner { + return async (argv) => { + if (argv.includes("infer")) { + if (inferResult === null) throw new Error("infer failed"); + const envelope = JSON.stringify({ ok: true, outputs: [{ text: inferResult }] }); + return { stdout: envelope, stderr: "", code: 0 }; + } + return { stdout: "", stderr: "unexpected command", code: 1 }; + }; +} + +describe("preprocessQuery", () => { + it("returns LLM-rewritten query on success", async () => { + const runner = createMockRunner( + "User dietary preference vegan. Related: plant-based, no animal products" + ); + const result = await preprocessQuery("Oh sorry I typed it wrong, I do like vegan food", runner); + expect(result).toBe( + "User dietary preference vegan. Related: plant-based, no animal products" + ); + }); + + it("falls back to raw text when infer fails", async () => { + const runner = createMockRunner(null); + const raw = "I like apple juice"; + const result = await preprocessQuery(raw, runner); + expect(result).toBe(raw); + }); + + it("falls back to raw text when infer returns empty string", async () => { + const runner = createMockRunner(""); + const raw = "timezone is PST"; + const result = await preprocessQuery(raw, runner); + expect(result).toBe(raw); + }); +}); + +describe("judgeContradiction", () => { + it("returns 'supersede' when LLM says supersede", async () => { + const runner = createMockRunner('{"decision": "supersede"}'); + const result = await judgeContradiction("User is vegan", "User is pescatarian", runner); + expect(result).toBe("supersede"); + }); + + it("returns 'keep_both' when LLM says keep_both", async () => { + const runner = createMockRunner('{"decision": "keep_both"}'); + const result = await judgeContradiction("User likes dark mode", "User is a developer", runner); + expect(result).toBe("keep_both"); + }); + + it("defaults to 'keep_both' when infer fails", async () => { + const runner = createMockRunner(null); + const result = await judgeContradiction("A", "B", runner); + expect(result).toBe("keep_both"); + }); + + it("defaults to 'keep_both' on malformed JSON", async () => { + const runner = createMockRunner("I think they are related"); + const result = await judgeContradiction("A", "B", runner); + expect(result).toBe("keep_both"); + }); + + it("defaults to 'keep_both' on unexpected decision value", async () => { + const runner = createMockRunner('{"decision": "merge"}'); + const result = await judgeContradiction("A", "B", runner); + expect(result).toBe("keep_both"); + }); +}); + +describe("extractId", () => { + it("extracts prof_ id from snippet", () => { + const snippet = "---\ntype: profile\nid: prof_sdtk\ncreated: 2026\n---\nContent"; + expect(extractId(snippet)).toBe("prof_sdtk"); + }); + + it("extracts pbk_ id from snippet", () => { + const snippet = "---\ntype: playbook\nid: pbk_az4k\n---\nContent"; + expect(extractId(snippet)).toBe("pbk_az4k"); + }); + + it("returns null when no id found", () => { + expect(extractId("no frontmatter here")).toBeNull(); + }); +}); diff --git a/reflexio/integrations/openclaw-embedded/tests/io.test.ts b/reflexio/integrations/openclaw-embedded/tests/io.test.ts new file mode 100644 index 0000000..118274a --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/tests/io.test.ts @@ -0,0 +1,175 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { generateNanoid, validateSlug, validateTtl, computeExpires, writeProfileFile, writePlaybookFile, deleteFile } from "../plugin/lib/io.ts"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; + +describe("generateNanoid", () => { + it("returns a 4-character string of [a-z0-9]", () => { + const id = generateNanoid(); + expect(id).toMatch(/^[a-z0-9]{4}$/); + }); + + it("produces different values across calls", () => { + const ids = new Set(Array.from({ length: 10 }, () => generateNanoid())); + expect(ids.size).toBeGreaterThan(1); + }); +}); + +describe("validateSlug", () => { + it("accepts valid kebab-case slugs", () => { + expect(() => validateSlug("diet-vegetarian")).not.toThrow(); + expect(() => validateSlug("abc")).not.toThrow(); + expect(() => validateSlug("a1b2")).not.toThrow(); + }); + + it("rejects empty string", () => { + expect(() => validateSlug("")).toThrow(); + }); + + it("rejects uppercase", () => { + expect(() => validateSlug("Diet-Vegetarian")).toThrow(); + }); + + it("rejects leading hyphen", () => { + expect(() => validateSlug("-diet")).toThrow(); + }); + + it("rejects slashes", () => { + expect(() => validateSlug("foo/bar")).toThrow(); + }); + + it("rejects strings longer than 48 chars", () => { + expect(() => validateSlug("a".repeat(49))).toThrow(); + }); +}); + +describe("validateTtl", () => { + it("accepts all valid TTL values", () => { + for (const ttl of ["one_day", "one_week", "one_month", "one_quarter", "one_year", "infinity"]) { + expect(() => validateTtl(ttl as any)).not.toThrow(); + } + }); + + it("rejects invalid TTL", () => { + expect(() => validateTtl("one_millennium" as any)).toThrow(); + }); +}); + +describe("computeExpires", () => { + it("returns 'never' for infinity", () => { + expect(computeExpires("infinity", "2026-04-17T00:00:00Z")).toBe("never"); + }); + + it("returns a date string for one_year", () => { + const result = computeExpires("one_year", "2026-04-17T00:00:00Z"); + expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/); + expect(result).toBe("2027-04-17"); + }); + + it("returns correct date for one_day", () => { + expect(computeExpires("one_day", "2026-04-17T00:00:00Z")).toBe("2026-04-18"); + }); +}); + +describe("writeProfileFile", () => { + let workspace: string; + + beforeEach(() => { + workspace = fs.mkdtempSync(path.join(os.tmpdir(), "rfx-test-")); + fs.mkdirSync(path.join(workspace, ".reflexio", "profiles"), { recursive: true }); + }); + + afterEach(() => { + fs.rmSync(workspace, { recursive: true, force: true }); + }); + + it("creates a profile file with correct frontmatter", () => { + const result = writeProfileFile({ + slug: "diet-vegan", + ttl: "infinity", + body: "User is vegan.", + workspace, + }); + expect(fs.existsSync(result)).toBe(true); + const content = fs.readFileSync(result, "utf8"); + expect(content).toContain("type: profile"); + expect(content).toContain("id: prof_"); + expect(content).toContain("ttl: infinity"); + expect(content).toContain("expires: never"); + expect(content).toContain("User is vegan."); + }); + + it("includes supersedes when provided", () => { + const result = writeProfileFile({ + slug: "diet-vegan", + ttl: "infinity", + body: "User is vegan.", + supersedes: ["prof_abc1"], + workspace, + }); + const content = fs.readFileSync(result, "utf8"); + expect(content).toContain("supersedes: [prof_abc1]"); + }); + + it("omits supersedes when not provided", () => { + const result = writeProfileFile({ + slug: "diet-vegan", + ttl: "infinity", + body: "User is vegan.", + workspace, + }); + const content = fs.readFileSync(result, "utf8"); + expect(content).not.toContain("supersedes"); + }); + + it("leaves no .tmp files on success", () => { + writeProfileFile({ slug: "test", ttl: "infinity", body: "x", workspace }); + const tmps = fs.readdirSync(path.join(workspace, ".reflexio", "profiles")) + .filter((f) => f.includes(".tmp")); + expect(tmps).toHaveLength(0); + }); +}); + +describe("writePlaybookFile", () => { + let workspace: string; + + beforeEach(() => { + workspace = fs.mkdtempSync(path.join(os.tmpdir(), "rfx-test-")); + fs.mkdirSync(path.join(workspace, ".reflexio", "playbooks"), { recursive: true }); + }); + + afterEach(() => { + fs.rmSync(workspace, { recursive: true, force: true }); + }); + + it("creates a playbook file with correct frontmatter (no ttl/expires)", () => { + const result = writePlaybookFile({ + slug: "commit-no-trailers", + body: "## When\nCommit message.\n\n## What\nNo trailers.\n\n## Why\nUser said so.", + workspace, + }); + expect(fs.existsSync(result)).toBe(true); + const content = fs.readFileSync(result, "utf8"); + expect(content).toContain("type: playbook"); + expect(content).toContain("id: pbk_"); + expect(content).not.toContain("ttl:"); + expect(content).not.toContain("expires:"); + expect(content).toContain("## When"); + }); +}); + +describe("deleteFile", () => { + it("deletes an existing file", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "rfx-del-")); + const f = path.join(tmp, "test.md"); + fs.writeFileSync(f, "x"); + deleteFile(f); + expect(fs.existsSync(f)).toBe(false); + fs.rmSync(tmp, { recursive: true, force: true }); + }); + + it("does not throw when file is missing", () => { + expect(() => deleteFile("/tmp/nonexistent-reflexio-test.md")).not.toThrow(); + }); +}); diff --git a/reflexio/integrations/openclaw-embedded/tests/search.test.ts b/reflexio/integrations/openclaw-embedded/tests/search.test.ts new file mode 100644 index 0000000..c9ab96f --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/tests/search.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect } from "vitest"; + +import { rawSearch, search } from "../plugin/lib/search.ts"; +import type { CommandRunner, MemorySearchResult } from "../plugin/lib/openclaw-cli.ts"; + +function createMockRunner( + inferResult: string | null, + searchResults: MemorySearchResult[] +): CommandRunner { + return async (argv) => { + if (argv.includes("infer")) { + if (inferResult === null) throw new Error("infer failed"); + const envelope = JSON.stringify({ ok: true, outputs: [{ text: inferResult }] }); + return { stdout: envelope, stderr: "", code: 0 }; + } + if (argv.includes("memory") && argv.includes("search")) { + return { + stdout: JSON.stringify({ results: searchResults }), + stderr: "", + code: 0, + }; + } + return { stdout: "", stderr: "unexpected command", code: 1 }; + }; +} + +describe("rawSearch", () => { + it("calls memorySearch with query and returns results", async () => { + const items: MemorySearchResult[] = [ + { path: ".reflexio/profiles/diet.md", score: 0.5, snippet: "vegan", startLine: 1, endLine: 5, source: "memory" }, + ]; + const runner = createMockRunner(null, items); + const results = await rawSearch("vegan diet", 3, undefined, runner); + expect(results).toHaveLength(1); + expect(results[0].path).toBe(".reflexio/profiles/diet.md"); + }); + + it("filters results to specified type", async () => { + const items: MemorySearchResult[] = [ + { path: ".reflexio/profiles/diet.md", score: 0.5, snippet: "x", startLine: 1, endLine: 5, source: "memory" }, + { path: ".reflexio/playbooks/commit.md", score: 0.4, snippet: "y", startLine: 1, endLine: 5, source: "memory" }, + ]; + const runner = createMockRunner(null, items); + const results = await rawSearch("query", 5, "profile", runner); + expect(results).toHaveLength(1); + expect(results[0].path).toContain("profiles"); + }); + + it("returns empty on memorySearch failure", async () => { + const runner = createMockRunner(null, []); + const results = await rawSearch("anything", 5, undefined, runner); + expect(results).toEqual([]); + }); +}); + +describe("search", () => { + it("preprocesses query before searching", async () => { + const runner = createMockRunner("Rewritten query about diet", []); + const results = await search("Oh sorry I like vegan food", 5, undefined, runner); + expect(results).toEqual([]); + // The runner was called — preprocessing happened via infer, then search via memory + }); + + it("falls back to raw query if preprocessing fails", async () => { + const runner = createMockRunner(null, []); + const results = await search("raw query here", 5, undefined, runner); + expect(results).toEqual([]); + }); +}); diff --git a/reflexio/integrations/openclaw-embedded/tests/smoke-test.ts b/reflexio/integrations/openclaw-embedded/tests/smoke-test.ts new file mode 100644 index 0000000..bb81b1c --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/tests/smoke-test.ts @@ -0,0 +1,140 @@ +// Standalone smoke test for the Reflexio Embedded hook handler. +// +// Run (requires tsx or ts-node in PATH): +// npx tsx tests/smoke-test.ts +// +// The plugin itself does NOT depend on tsx at runtime — Openclaw loads the +// .ts files via its own bundled jiti runtime. This smoke test only needs tsx +// because it runs outside Openclaw. +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; + +import { + injectBootstrapReminder, + spawnExtractor, + ttlSweepProfiles, +} from "../plugin/hook/handler.ts"; + +type FakeRunCall = { + sessionKey: string; + message: string; +}; + +async function main(): Promise { + const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "rfx-test-")); + fs.mkdirSync(path.join(workspace, ".reflexio", "profiles"), { + recursive: true, + }); + + // Create an expired profile + fs.writeFileSync( + path.join(workspace, ".reflexio", "profiles", "old-xxxx.md"), + `--- +type: profile +id: prof_xxxx +created: 2020-01-01T00:00:00Z +ttl: one_day +expires: 2020-01-02 +--- + +Old expired fact. +`, + ); + + // Create a fresh profile + fs.writeFileSync( + path.join(workspace, ".reflexio", "profiles", "fresh-yyyy.md"), + `--- +type: profile +id: prof_yyyy +created: 2026-04-16T00:00:00Z +ttl: infinity +expires: never +--- + +Fresh fact. +`, + ); + + // 1. TTL sweep deletes the expired file, preserves the fresh one + await ttlSweepProfiles(workspace); + const oldExists = fs.existsSync( + path.join(workspace, ".reflexio", "profiles", "old-xxxx.md"), + ); + const freshExists = fs.existsSync( + path.join(workspace, ".reflexio", "profiles", "fresh-yyyy.md"), + ); + console.log(`Old file deleted: ${!oldExists ? "PASS" : "FAIL"}`); + console.log(`Fresh file preserved: ${freshExists ? "PASS" : "FAIL"}`); + + // 2. Bootstrap reminder is non-empty and mentions SKILL.md + const reminder = injectBootstrapReminder(); + console.log( + `Bootstrap reminder mentions SKILL.md: ${reminder.includes("SKILL.md") ? "PASS" : "FAIL"}`, + ); + + // 3. spawnExtractor forwards task + sessionKey to the runtime + const calls: FakeRunCall[] = []; + const runtime = { + subagent: { + run: async (params: FakeRunCall): Promise<{ runId: string }> => { + calls.push({ + sessionKey: params.sessionKey, + message: params.message, + }); + return { runId: "test-run" }; + }, + }, + }; + const runId = await spawnExtractor({ + runtime, + workspaceDir: workspace, + sessionKey: "test-session", + messages: [ + { role: "user", content: "I'm vegetarian" }, + { role: "assistant", content: "Got it." }, + ], + reason: "smoke-test", + }); + console.log( + `Extractor spawned with runId: ${runId === "test-run" ? "PASS" : "FAIL"}`, + ); + console.log( + `Extractor message contains transcript: ${ + calls.length === 1 && calls[0].message.includes("vegetarian") + ? "PASS" + : "FAIL" + }`, + ); + + // 4. spawnExtractor skips when transcript is too short AND no sessionFile + const skipCalls: FakeRunCall[] = []; + const skipRuntime = { + subagent: { + run: async (params: FakeRunCall): Promise<{ runId: string }> => { + skipCalls.push(params); + return { runId: "unexpected" }; + }, + }, + }; + const skipRunId = await spawnExtractor({ + runtime: skipRuntime, + workspaceDir: workspace, + sessionKey: "test-session-2", + messages: [], + reason: "smoke-test-skip", + }); + console.log( + `Extractor skipped on empty transcript: ${ + skipRunId === undefined && skipCalls.length === 0 ? "PASS" : "FAIL" + }`, + ); + + fs.rmSync(workspace, { recursive: true, force: true }); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/reflexio/integrations/openclaw-embedded/tests/write-playbook.test.ts b/reflexio/integrations/openclaw-embedded/tests/write-playbook.test.ts new file mode 100644 index 0000000..b3d77fc --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/tests/write-playbook.test.ts @@ -0,0 +1,93 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; + +import { writePlaybook } from "../plugin/lib/write-playbook.ts"; +import type { CommandRunner, MemorySearchResult } from "../plugin/lib/openclaw-cli.ts"; + +let inferCallCount: number; + +function createMockRunner( + inferResults: (string | null)[], + searchResults: MemorySearchResult[] +): CommandRunner { + inferCallCount = 0; + return async (argv) => { + if (argv.includes("infer")) { + const result = inferResults[inferCallCount++] ?? null; + if (result === null) throw new Error("infer failed"); + const envelope = JSON.stringify({ ok: true, outputs: [{ text: result }] }); + return { stdout: envelope, stderr: "", code: 0 }; + } + if (argv.includes("memory") && argv.includes("search")) { + return { + stdout: JSON.stringify({ results: searchResults }), + stderr: "", + code: 0, + }; + } + return { stdout: "", stderr: "unexpected command", code: 1 }; + }; +} + +let workspace: string; + +beforeEach(() => { + workspace = fs.mkdtempSync(path.join(os.tmpdir(), "rfx-wpb-")); + fs.mkdirSync(path.join(workspace, ".reflexio", "playbooks"), { recursive: true }); +}); + +afterEach(() => { + fs.rmSync(workspace, { recursive: true, force: true }); +}); + +describe("writePlaybook", () => { + it("writes normally when no neighbors found", async () => { + const runner = createMockRunner(["commit message query"], []); + + const result = await writePlaybook({ + slug: "commit-no-trailers", + body: "## When\nCommit.\n\n## What\nNo trailers.\n\n## Why\nUser said.", + workspace, config: { shallow_threshold: 0.4, top_k: 5 }, + runner, + }); + + expect(fs.existsSync(result)).toBe(true); + const content = fs.readFileSync(result, "utf8"); + expect(content).toContain("type: playbook"); + expect(content).toContain("## When"); + }); + + it("supersedes when neighbor above threshold and LLM says supersede", async () => { + const oldPath = path.join(workspace, ".reflexio", "playbooks", "old.md"); + fs.writeFileSync(oldPath, "---\nid: pbk_old\n---\nOld playbook"); + + const runner = createMockRunner( + ["commit query", '{"decision": "supersede"}'], + [{ path: oldPath, score: 0.5, snippet: "---\nid: pbk_old\n---\nOld playbook", startLine: 1, endLine: 5, source: "memory" }] + ); + + const result = await writePlaybook({ + slug: "commit-no-trailers", + body: "## When\nCommit.\n\n## What\nUpdated rule.", + workspace, config: { shallow_threshold: 0.4, top_k: 5 }, + runner, + }); + + expect(fs.existsSync(result)).toBe(true); + expect(fs.readFileSync(result, "utf8")).toContain("supersedes: [pbk_old]"); + expect(fs.existsSync(oldPath)).toBe(false); + }); + + it("throws on invalid slug", async () => { + const runner = createMockRunner([], []); + await expect( + writePlaybook({ + slug: "INVALID", body: "x", + workspace, config: { shallow_threshold: 0.4, top_k: 5 }, + runner, + }) + ).rejects.toThrow("Invalid slug"); + }); +}); diff --git a/reflexio/integrations/openclaw-embedded/tests/write-profile.test.ts b/reflexio/integrations/openclaw-embedded/tests/write-profile.test.ts new file mode 100644 index 0000000..b270530 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/tests/write-profile.test.ts @@ -0,0 +1,161 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; + +import { writeProfile } from "../plugin/lib/write-profile.ts"; +import type { CommandRunner, MemorySearchResult } from "../plugin/lib/openclaw-cli.ts"; + +let inferCallCount: number; + +function createMockRunner( + inferResults: (string | null)[], + searchResults: MemorySearchResult[] +): CommandRunner { + inferCallCount = 0; + return async (argv) => { + if (argv.includes("infer")) { + const result = inferResults[inferCallCount++] ?? null; + if (result === null) throw new Error("infer failed"); + const envelope = JSON.stringify({ ok: true, outputs: [{ text: result }] }); + return { stdout: envelope, stderr: "", code: 0 }; + } + if (argv.includes("memory") && argv.includes("search")) { + return { + stdout: JSON.stringify({ results: searchResults }), + stderr: "", + code: 0, + }; + } + return { stdout: "", stderr: "unexpected command", code: 1 }; + }; +} + +let workspace: string; + +beforeEach(() => { + workspace = fs.mkdtempSync(path.join(os.tmpdir(), "rfx-wp-")); + fs.mkdirSync(path.join(workspace, ".reflexio", "profiles"), { recursive: true }); +}); + +afterEach(() => { + fs.rmSync(workspace, { recursive: true, force: true }); +}); + +describe("writeProfile", () => { + it("writes normally when no neighbors found", async () => { + const runner = createMockRunner(["diet vegan query"], []); + + const result = await writeProfile({ + slug: "diet-vegan", ttl: "infinity", + body: "User is vegan.", workspace, config: { shallow_threshold: 0.4, top_k: 5 }, + runner, + }); + + expect(fs.existsSync(result)).toBe(true); + const content = fs.readFileSync(result, "utf8"); + expect(content).toContain("User is vegan."); + expect(content).not.toContain("supersedes"); + }); + + it("writes normally when neighbor is below threshold", async () => { + const runner = createMockRunner(["diet query"], [ + { path: ".reflexio/profiles/old.md", score: 0.3, snippet: "id: prof_old\n---\nOld fact", startLine: 1, endLine: 5, source: "memory" }, + ]); + + const result = await writeProfile({ + slug: "diet-vegan", ttl: "infinity", + body: "User is vegan.", workspace, config: { shallow_threshold: 0.4, top_k: 5 }, + runner, + }); + + expect(fs.existsSync(result)).toBe(true); + expect(fs.readFileSync(result, "utf8")).not.toContain("supersedes"); + }); + + it("supersedes when neighbor above threshold and LLM says supersede", async () => { + const oldPath = path.join(workspace, ".reflexio", "profiles", "old.md"); + fs.writeFileSync(oldPath, "---\nid: prof_old\n---\nOld fact"); + + const runner = createMockRunner( + ["diet vegan query", '{"decision": "supersede"}'], + [{ path: oldPath, score: 0.5, snippet: "---\nid: prof_old\n---\nOld fact", startLine: 1, endLine: 5, source: "memory" }] + ); + + const result = await writeProfile({ + slug: "diet-vegan", ttl: "infinity", + body: "User is vegan.", workspace, config: { shallow_threshold: 0.4, top_k: 5 }, + runner, + }); + + expect(fs.existsSync(result)).toBe(true); + expect(fs.readFileSync(result, "utf8")).toContain("supersedes: [prof_old]"); + expect(fs.existsSync(oldPath)).toBe(false); + }); + + it("keeps both when LLM says keep_both", async () => { + const oldPath = path.join(workspace, ".reflexio", "profiles", "old.md"); + fs.writeFileSync(oldPath, "---\nid: prof_old\n---\nDifferent fact"); + + const runner = createMockRunner( + ["query", '{"decision": "keep_both"}'], + [{ path: oldPath, score: 0.5, snippet: "---\nid: prof_old\n---\nDifferent fact", startLine: 1, endLine: 5, source: "memory" }] + ); + + const result = await writeProfile({ + slug: "new-fact", ttl: "infinity", + body: "New fact.", workspace, config: { shallow_threshold: 0.4, top_k: 5 }, + runner, + }); + + expect(fs.existsSync(result)).toBe(true); + expect(fs.existsSync(oldPath)).toBe(true); + expect(fs.readFileSync(result, "utf8")).not.toContain("supersedes"); + }); + + it("still writes when openclaw infer fails at preprocessing", async () => { + const runner = createMockRunner([null], []); + + const result = await writeProfile({ + slug: "test", ttl: "infinity", + body: "Fact.", workspace, config: { shallow_threshold: 0.4, top_k: 5 }, + runner, + }); + + expect(fs.existsSync(result)).toBe(true); + }); + + it("still writes when openclaw memory search fails", async () => { + const runner = createMockRunner(["query"], []); + + const result = await writeProfile({ + slug: "test", ttl: "infinity", + body: "Fact.", workspace, config: { shallow_threshold: 0.4, top_k: 5 }, + runner, + }); + + expect(fs.existsSync(result)).toBe(true); + }); + + it("throws on invalid slug", async () => { + const runner = createMockRunner([], []); + await expect( + writeProfile({ + slug: "INVALID", ttl: "infinity", + body: "x", workspace, config: { shallow_threshold: 0.4, top_k: 5 }, + runner, + }) + ).rejects.toThrow("Invalid slug"); + }); + + it("throws on invalid TTL", async () => { + const runner = createMockRunner([], []); + await expect( + writeProfile({ + slug: "valid", ttl: "bad_ttl" as any, + body: "x", workspace, config: { shallow_threshold: 0.4, top_k: 5 }, + runner, + }) + ).rejects.toThrow("Invalid TTL"); + }); +}); diff --git a/reflexio/integrations/openclaw-embedded/tsconfig.json b/reflexio/integrations/openclaw-embedded/tsconfig.json new file mode 100644 index 0000000..0d7bb9a --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "types": ["node"] + }, + "include": ["plugin/**/*.ts", "types/**/*.d.ts"], + "exclude": ["tests", "node_modules"] +} diff --git a/reflexio/integrations/openclaw-embedded/types/openclaw.d.ts b/reflexio/integrations/openclaw-embedded/types/openclaw.d.ts new file mode 100644 index 0000000..eee56d7 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/types/openclaw.d.ts @@ -0,0 +1,174 @@ +// Narrow ambient shim for the Openclaw Plugin SDK surface that this plugin +// actually touches. The real types ship with the `openclaw` npm package, but +// we avoid depending on the full host at build time — plugins load at runtime +// via the host's own jiti instance. +// +// If you need richer type coverage, install `openclaw` as a devDependency and +// delete this file; the real .d.ts files will take over. +declare module "openclaw/plugin-sdk/plugin-entry" { + export type PluginHookName = + | "before_model_resolve" + | "before_prompt_build" + | "before_agent_start" + | "before_agent_reply" + | "llm_input" + | "llm_output" + | "agent_end" + | "before_compaction" + | "after_compaction" + | "before_reset" + | "inbound_claim" + | "message_received" + | "message_sending" + | "message_sent" + | "before_tool_call" + | "after_tool_call" + | "tool_result_persist" + | "before_message_write" + | "session_start" + | "session_end" + | "subagent_spawning" + | "subagent_delivery_target" + | "subagent_spawned" + | "subagent_ended" + | "gateway_start" + | "gateway_stop" + | "before_dispatch" + | "reply_dispatch" + | "before_install"; + + export type PluginLogger = { + info?: (msg: string) => void; + warn?: (msg: string) => void; + error?: (msg: string) => void; + debug?: (msg: string) => void; + }; + + export type PluginRuntime = { + subagent?: { + run: (params: { + sessionKey: string; + message: string; + provider?: string; + model?: string; + extraSystemPrompt?: string; + lane?: string; + deliver?: boolean; + idempotencyKey?: string; + }) => Promise<{ runId: string }>; + }; + }; + + export type PluginHookAgentContext = { + runId?: string; + agentId?: string; + sessionKey?: string; + sessionId?: string; + workspaceDir?: string; + }; + + export type PluginHookSessionContext = { + agentId?: string; + sessionId: string; + sessionKey?: string; + workspaceDir?: string; + }; + + export type PluginHookBeforeAgentStartEvent = { + prompt: string; + messages?: unknown[]; + }; + + export type PluginHookBeforeAgentStartResult = { + prependSystemContext?: string; + systemPrompt?: string; + prependContext?: string; + appendSystemContext?: string; + modelOverride?: string; + providerOverride?: string; + }; + + export type PluginHookBeforeCompactionEvent = { + messageCount: number; + compactingCount?: number; + tokenCount?: number; + messages?: unknown[]; + sessionFile?: string; + }; + + export type PluginHookBeforeResetEvent = { + sessionFile?: string; + messages?: unknown[]; + reason?: string; + }; + + export type PluginHookSessionEndEvent = { + sessionId: string; + sessionKey?: string; + messageCount: number; + durationMs?: number; + reason?: string; + sessionFile?: string; + }; + + export type OpenClawPluginApi = { + id: string; + name: string; + runtime: PluginRuntime; + logger: PluginLogger; + on: { + ( + hookName: "before_agent_start", + handler: ( + event: PluginHookBeforeAgentStartEvent, + ctx: PluginHookAgentContext, + ) => + | Promise + | PluginHookBeforeAgentStartResult + | void, + opts?: { priority?: number }, + ): void; + ( + hookName: "before_compaction", + handler: ( + event: PluginHookBeforeCompactionEvent, + ctx: PluginHookAgentContext, + ) => Promise | void, + opts?: { priority?: number }, + ): void; + ( + hookName: "before_reset", + handler: ( + event: PluginHookBeforeResetEvent, + ctx: PluginHookAgentContext, + ) => Promise | void, + opts?: { priority?: number }, + ): void; + ( + hookName: "session_end", + handler: ( + event: PluginHookSessionEndEvent, + ctx: PluginHookSessionContext, + ) => Promise | void, + opts?: { priority?: number }, + ): void; + ( + hookName: PluginHookName, + handler: (event: unknown, ctx: unknown) => unknown, + opts?: { priority?: number }, + ): void; + }; + }; + + export type OpenClawPluginDefinition = { + id: string; + name: string; + description?: string; + version?: string; + register: (api: OpenClawPluginApi) => void | Promise; + }; + + export function definePluginEntry( + def: OpenClawPluginDefinition, + ): OpenClawPluginDefinition; +} diff --git a/reflexio/integrations/openclaw-embedded/vitest.config.ts b/reflexio/integrations/openclaw-embedded/vitest.config.ts new file mode 100644 index 0000000..19384e8 --- /dev/null +++ b/reflexio/integrations/openclaw-embedded/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["tests/**/*.test.ts"], + }, +}); diff --git a/uv.lock b/uv.lock index 16e465e..bbe83a0 100644 --- a/uv.lock +++ b/uv.lock @@ -942,31 +942,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] -[[package]] -name = "datasets" -version = "4.8.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dill" }, - { name = "filelock" }, - { name = "fsspec", extra = ["http"] }, - { name = "httpx" }, - { name = "huggingface-hub" }, - { name = "multiprocess" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pandas" }, - { name = "pyarrow" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "tqdm" }, - { name = "xxhash" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/22/73e46ac7a8c25e7ef0b3bd6f10da3465021d90219a32eb0b4d2afea4c56e/datasets-4.8.4.tar.gz", hash = "sha256:a1429ed853275ce7943a01c6d2e25475b4501eb758934362106a280470df3a52", size = 604382, upload-time = "2026-03-23T14:21:17.987Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/e5/247d094108e42ac26363ab8dc57f168840cf7c05774b40ffeb0d78868fcc/datasets-4.8.4-py3-none-any.whl", hash = "sha256:cdc8bee4698e549d78bf1fed6aea2eebc760b22b084f07e6fc020c6577a6ce6d", size = 526991, upload-time = "2026-03-23T14:21:15.89Z" }, -] - [[package]] name = "debugpy" version = "1.8.20" @@ -1027,15 +1002,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, ] -[[package]] -name = "dill" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, -] - [[package]] name = "distlib" version = "0.4.0" @@ -1129,15 +1095,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] -[[package]] -name = "et-xmlfile" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, -] - [[package]] name = "exceptiongroup" version = "1.3.1" @@ -1243,18 +1200,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, ] -[[package]] -name = "fire" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "termcolor" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/00/f8d10588d2019d6d6452653def1ee807353b21983db48550318424b5ff18/fire-0.7.1.tar.gz", hash = "sha256:3b208f05c736de98fb343310d090dcc4d8c78b2a89ea4f32b837c586270a9cbf", size = 88720, upload-time = "2025-08-16T20:20:24.175Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/4c/93d0f85318da65923e4b91c1c2ff03d8a458cbefebe3bc612a6693c7906d/fire-0.7.1-py3-none-any.whl", hash = "sha256:e43fd8a5033a9001e7e2973bab96070694b9f12f2e0ecf96d4683971b5ab1882", size = 115945, upload-time = "2025-08-16T20:20:22.87Z" }, -] - [[package]] name = "fonttools" version = "4.62.1" @@ -1296,20 +1241,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, ] -[[package]] -name = "fpdf2" -version = "2.8.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "defusedxml" }, - { name = "fonttools" }, - { name = "pillow" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/27/f2/72feae0b2827ed38013e4307b14f95bf0b3d124adfef4d38a7d57533f7be/fpdf2-2.8.7.tar.gz", hash = "sha256:7060ccee5a9c7ab0a271fb765a36a23639f83ef8996c34e3d46af0a17ede57f9", size = 362351, upload-time = "2026-02-28T05:39:16.456Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/66/0a/cf50ecffa1e3747ed9380a3adfc829259f1f86b3fdbd9e505af789003141/fpdf2-2.8.7-py3-none-any.whl", hash = "sha256:d391fc508a3ce02fc43a577c830cda4fe6f37646f2d143d489839940932fbc19", size = 327056, upload-time = "2026-02-28T05:39:14.619Z" }, -] - [[package]] name = "fqdn" version = "1.5.1" @@ -1410,16 +1341,11 @@ wheels = [ [[package]] name = "fsspec" -version = "2026.2.0" +version = "2026.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, -] - -[package.optional-dependencies] -http = [ - { name = "aiohttp" }, + { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" }, ] [[package]] @@ -1892,18 +1818,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/42/cf027b4ac873b076189d935b135397675dac80cb29acb13e1ab86ad6c631/json5-0.14.0-py3-none-any.whl", hash = "sha256:56cf861bab076b1178eb8c92e1311d273a9b9acea2ccc82c276abf839ebaef3a", size = 36271, upload-time = "2026-03-27T22:50:47.073Z" }, ] -[[package]] -name = "jsonpatch" -version = "1.33" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonpointer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, -] - [[package]] name = "jsonpointer" version = "3.1.1" @@ -2209,45 +2123,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, ] -[[package]] -name = "langchain-core" -version = "1.2.28" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonpatch" }, - { name = "langsmith" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "pyyaml" }, - { name = "tenacity" }, - { name = "typing-extensions" }, - { name = "uuid-utils" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f8/a4/317a1a3ac1df33a64adb3670bf88bbe3b3d5baa274db6863a979db472897/langchain_core-1.2.28.tar.gz", hash = "sha256:271a3d8bd618f795fdeba112b0753980457fc90537c46a0c11998516a74dc2cb", size = 846119, upload-time = "2026-04-08T18:19:34.867Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/92/32f785f077c7e898da97064f113c73fbd9ad55d1e2169cf3a391b183dedb/langchain_core-1.2.28-py3-none-any.whl", hash = "sha256:80764232581eaf8057bcefa71dbf8adc1f6a28d257ebd8b95ba9b8b452e8c6ac", size = 508727, upload-time = "2026-04-08T18:19:32.823Z" }, -] - -[[package]] -name = "langsmith" -version = "0.7.30" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "requests-toolbelt" }, - { name = "uuid-utils" }, - { name = "xxhash" }, - { name = "zstandard" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/46/e7/d27d952ce9824d684a3bb500a06541a2d55734bc4d849cdfcca2dfd4d93a/langsmith-0.7.30.tar.gz", hash = "sha256:d9df7ba5e42f818b63bda78776c8f2fc853388be3ae77b117e5d183a149321a2", size = 1106040, upload-time = "2026-04-09T21:12:01.892Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/19/96250cf58070c5563446651b03bb76c2eb5afbf08e754840ab639532d8c6/langsmith-0.7.30-py3-none-any.whl", hash = "sha256:43dd9f8d290e4d406606d6cc0bd62f5d1050963f05fe0ab6ffe50acf41f2f55a", size = 372682, upload-time = "2026-04-09T21:12:00.481Z" }, -] - [[package]] name = "lark" version = "1.3.1" @@ -2879,23 +2754,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] -[[package]] -name = "multiprocess" -version = "0.70.19" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dill" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a2/f2/e783ac7f2aeeed14e9e12801f22529cc7e6b7ab80928d6dcce4e9f00922d/multiprocess-0.70.19.tar.gz", hash = "sha256:952021e0e6c55a4a9fe4cd787895b86e239a40e76802a789d6305398d3975897", size = 2079989, upload-time = "2026-01-19T06:47:39.744Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/45/8004d1e6b9185c1a444d6b55ac5682acf9d98035e54386d967366035a03a/multiprocess-0.70.19-py310-none-any.whl", hash = "sha256:97404393419dcb2a8385910864eedf47a3cadf82c66345b44f036420eb0b5d87", size = 134948, upload-time = "2026-01-19T06:47:32.325Z" }, - { url = "https://files.pythonhosted.org/packages/86/c2/dec9722dc3474c164a0b6bcd9a7ed7da542c98af8cabce05374abab35edd/multiprocess-0.70.19-py311-none-any.whl", hash = "sha256:928851ae7973aea4ce0eaf330bbdafb2e01398a91518d5c8818802845564f45c", size = 144457, upload-time = "2026-01-19T06:47:33.711Z" }, - { url = "https://files.pythonhosted.org/packages/71/70/38998b950a97ea279e6bd657575d22d1a2047256caf707d9a10fbce4f065/multiprocess-0.70.19-py312-none-any.whl", hash = "sha256:3a56c0e85dd5025161bac5ce138dcac1e49174c7d8e74596537e729fd5c53c28", size = 150281, upload-time = "2026-01-19T06:47:35.037Z" }, - { url = "https://files.pythonhosted.org/packages/7f/74/d2c27e03cb84251dfe7249b8e82923643c6d48fa4883b9476b025e7dc7eb/multiprocess-0.70.19-py313-none-any.whl", hash = "sha256:8d5eb4ec5017ba2fab4e34a747c6d2c2b6fecfe9e7236e77988db91580ada952", size = 156414, upload-time = "2026-01-19T06:47:35.915Z" }, - { url = "https://files.pythonhosted.org/packages/a0/61/af9115673a5870fd885247e2f1b68c4f1197737da315b520a91c757a861a/multiprocess-0.70.19-py314-none-any.whl", hash = "sha256:e8cc7fbdff15c0613f0a1f1f8744bef961b0a164c0ca29bdff53e9d2d93c5e5f", size = 160318, upload-time = "2026-01-19T06:47:37.497Z" }, - { url = "https://files.pythonhosted.org/packages/7e/82/69e539c4c2027f1e1697e09aaa2449243085a0edf81ae2c6341e84d769b6/multiprocess-0.70.19-py39-none-any.whl", hash = "sha256:0d4b4397ed669d371c81dcd1ef33fd384a44d6c3de1bd0ca7ac06d837720d3c5", size = 133477, upload-time = "2026-01-19T06:47:38.619Z" }, -] - [[package]] name = "mutmut" version = "3.5.0" @@ -3136,71 +2994,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/bc/a8f7c3aa03452fedbb9af8be83e959adba96a6b4a35e416faffcc959c568/openai-2.31.0-py3-none-any.whl", hash = "sha256:44e1344d87e56a493d649b17e2fac519d1368cbb0745f59f1957c4c26de50a0a", size = 1153479, upload-time = "2026-04-08T21:01:39.217Z" }, ] -[[package]] -name = "openpyxl" -version = "3.1.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "et-xmlfile" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, -] - -[[package]] -name = "orjson" -version = "3.11.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832, upload-time = "2026-03-31T16:16:27.878Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/01/f6/8d58b32ab32d9215973a1688aebd098252ee8af1766c0e4e36e7831f0295/orjson-3.11.8-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f", size = 229233, upload-time = "2026-03-31T16:15:12.762Z" }, - { url = "https://files.pythonhosted.org/packages/a9/8b/2ffe35e71f6b92622e8ea4607bf33ecf7dfb51b3619dcfabfd36cbe2d0a5/orjson-3.11.8-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6", size = 128772, upload-time = "2026-03-31T16:15:14.237Z" }, - { url = "https://files.pythonhosted.org/packages/27/d2/1f8682ae50d5c6897a563cb96bc106da8c9cb5b7b6e81a52e4cc086679b9/orjson-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8", size = 131946, upload-time = "2026-03-31T16:15:15.607Z" }, - { url = "https://files.pythonhosted.org/packages/52/4b/5500f76f0eece84226e0689cb48dcde081104c2fa6e2483d17ca13685ffb/orjson-3.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813", size = 130368, upload-time = "2026-03-31T16:15:17.066Z" }, - { url = "https://files.pythonhosted.org/packages/da/4e/58b927e08fbe9840e6c920d9e299b051ea667463b1f39a56e668669f8508/orjson-3.11.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:469ac2125611b7c5741a0b3798cd9e5786cbad6345f9f400c77212be89563bec", size = 135540, upload-time = "2026-03-31T16:15:18.404Z" }, - { url = "https://files.pythonhosted.org/packages/56/7c/ba7cb871cba1bcd5cd02ee34f98d894c6cea96353ad87466e5aef2429c60/orjson-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546", size = 146877, upload-time = "2026-03-31T16:15:19.833Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/eb9c25fc1386696c6a342cd361c306452c75e0b55e86ad602dd4827a7fd7/orjson-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506", size = 132837, upload-time = "2026-03-31T16:15:21.282Z" }, - { url = "https://files.pythonhosted.org/packages/37/87/5ddeb7fc1fbd9004aeccab08426f34c81a5b4c25c7061281862b015fce2b/orjson-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f", size = 133624, upload-time = "2026-03-31T16:15:22.641Z" }, - { url = "https://files.pythonhosted.org/packages/22/09/90048793db94ee4b2fcec4ac8e5ddb077367637d6650be896b3494b79bb7/orjson-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e", size = 141904, upload-time = "2026-03-31T16:15:24.435Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cf/eb284847487821a5d415e54149a6449ba9bfc5872ce63ab7be41b8ec401c/orjson-3.11.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb", size = 423742, upload-time = "2026-03-31T16:15:26.155Z" }, - { url = "https://files.pythonhosted.org/packages/44/09/e12423d327071c851c13e76936f144a96adacfc037394dec35ac3fc8d1e8/orjson-3.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e8c6218b614badf8e229b697865df4301afa74b791b6c9ade01d19a9953a942", size = 147806, upload-time = "2026-03-31T16:15:27.909Z" }, - { url = "https://files.pythonhosted.org/packages/b3/6d/37c2589ba864e582ffe7611643314785c6afb1f83c701654ef05daa8fcc7/orjson-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25", size = 136485, upload-time = "2026-03-31T16:15:29.749Z" }, - { url = "https://files.pythonhosted.org/packages/be/c9/135194a02ab76b04ed9a10f68624b7ebd238bbe55548878b11ff15a0f352/orjson-3.11.8-cp312-cp312-win32.whl", hash = "sha256:e0950ed1bcb9893f4293fd5c5a7ee10934fbf82c4101c70be360db23ce24b7d2", size = 131966, upload-time = "2026-03-31T16:15:31.687Z" }, - { url = "https://files.pythonhosted.org/packages/ed/9a/9796f8fbe3cf30ce9cb696748dbb535e5c87be4bf4fe2e9ca498ef1fa8cf/orjson-3.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:3cf17c141617b88ced4536b2135c552490f07799f6ad565948ea07bef0dcb9a6", size = 127441, upload-time = "2026-03-31T16:15:33.333Z" }, - { url = "https://files.pythonhosted.org/packages/cc/47/5aaf54524a7a4a0dd09dd778f3fa65dd2108290615b652e23d944152bc8e/orjson-3.11.8-cp312-cp312-win_arm64.whl", hash = "sha256:48854463b0572cc87dac7d981aa72ed8bf6deedc0511853dc76b8bbd5482d36d", size = 127364, upload-time = "2026-03-31T16:15:34.748Z" }, - { url = "https://files.pythonhosted.org/packages/66/7f/95fba509bb2305fab0073558f1e8c3a2ec4b2afe58ed9fcb7d3b8beafe94/orjson-3.11.8-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc", size = 229180, upload-time = "2026-03-31T16:15:36.426Z" }, - { url = "https://files.pythonhosted.org/packages/f6/9d/b237215c743ca073697d759b5503abd2cb8a0d7b9c9e21f524bcf176ab66/orjson-3.11.8-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559", size = 128754, upload-time = "2026-03-31T16:15:38.049Z" }, - { url = "https://files.pythonhosted.org/packages/42/3d/27d65b6d11e63f133781425f132807aef793ed25075fec686fc8e46dd528/orjson-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623", size = 131877, upload-time = "2026-03-31T16:15:39.484Z" }, - { url = "https://files.pythonhosted.org/packages/dd/cc/faee30cd8f00421999e40ef0eba7332e3a625ce91a58200a2f52c7fef235/orjson-3.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c", size = 130361, upload-time = "2026-03-31T16:15:41.274Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bb/a6c55896197f97b6d4b4e7c7fd77e7235517c34f5d6ad5aadd43c54c6d7c/orjson-3.11.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ab359aff0436d80bfe8a23b46b5fea69f1e18aaf1760a709b4787f1318b317f", size = 135521, upload-time = "2026-03-31T16:15:42.758Z" }, - { url = "https://files.pythonhosted.org/packages/9c/7c/ca3a3525aa32ff636ebb1778e77e3587b016ab2edb1b618b36ba96f8f2c0/orjson-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55", size = 146862, upload-time = "2026-03-31T16:15:44.341Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0c/18a9d7f18b5edd37344d1fd5be17e94dc652c67826ab749c6e5948a78112/orjson-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137", size = 132847, upload-time = "2026-03-31T16:15:46.368Z" }, - { url = "https://files.pythonhosted.org/packages/23/91/7e722f352ad67ca573cee44de2a58fb810d0f4eb4e33276c6a557979fd8a/orjson-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53", size = 133637, upload-time = "2026-03-31T16:15:48.123Z" }, - { url = "https://files.pythonhosted.org/packages/af/04/32845ce13ac5bd1046ddb02ac9432ba856cc35f6d74dde95864fe0ad5523/orjson-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e", size = 141906, upload-time = "2026-03-31T16:15:49.626Z" }, - { url = "https://files.pythonhosted.org/packages/02/5e/c551387ddf2d7106d9039369862245c85738b828844d13b99ccb8d61fd06/orjson-3.11.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6", size = 423722, upload-time = "2026-03-31T16:15:51.176Z" }, - { url = "https://files.pythonhosted.org/packages/00/a3/ecfe62434096f8a794d4976728cb59bcfc4a643977f21c2040545d37eb4c/orjson-3.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98bdc6cb889d19bed01de46e67574a2eab61f5cc6b768ed50e8ac68e9d6ffab6", size = 147801, upload-time = "2026-03-31T16:15:52.939Z" }, - { url = "https://files.pythonhosted.org/packages/18/6d/0dce10b9f6643fdc59d99333871a38fa5a769d8e2fc34a18e5d2bfdee900/orjson-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b", size = 136460, upload-time = "2026-03-31T16:15:54.431Z" }, - { url = "https://files.pythonhosted.org/packages/01/d6/6dde4f31842d87099238f1f07b459d24edc1a774d20687187443ab044191/orjson-3.11.8-cp313-cp313-win32.whl", hash = "sha256:01c4e5a6695dc09098f2e6468a251bc4671c50922d4d745aff1a0a33a0cf5b8d", size = 131956, upload-time = "2026-03-31T16:15:56.081Z" }, - { url = "https://files.pythonhosted.org/packages/c1/f9/4e494a56e013db957fb77186b818b916d4695b8fa2aa612364974160e91b/orjson-3.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:c154a35dd1330707450bb4d4e7dd1f17fa6f42267a40c1e8a1daa5e13719b4b8", size = 127410, upload-time = "2026-03-31T16:15:57.54Z" }, - { url = "https://files.pythonhosted.org/packages/57/7f/803203d00d6edb6e9e7eef421d4e1adbb5ea973e40b3533f3cfd9aeb374e/orjson-3.11.8-cp313-cp313-win_arm64.whl", hash = "sha256:4861bde57f4d253ab041e374f44023460e60e71efaa121f3c5f0ed457c3a701e", size = 127338, upload-time = "2026-03-31T16:15:59.106Z" }, - { url = "https://files.pythonhosted.org/packages/6d/35/b01910c3d6b85dc882442afe5060cbf719c7d1fc85749294beda23d17873/orjson-3.11.8-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ec795530a73c269a55130498842aaa762e4a939f6ce481a7e986eeaa790e9da4", size = 229171, upload-time = "2026-03-31T16:16:00.651Z" }, - { url = "https://files.pythonhosted.org/packages/c2/56/c9ec97bd11240abef39b9e5d99a15462809c45f677420fd148a6c5e6295e/orjson-3.11.8-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c492a0e011c0f9066e9ceaa896fbc5b068c54d365fea5f3444b697ee01bc8625", size = 128746, upload-time = "2026-03-31T16:16:02.673Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e4/66d4f30a90de45e2f0cbd9623588e8ae71eef7679dbe2ae954ed6d66a41f/orjson-3.11.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:883206d55b1bd5f5679ad5e6ddd3d1a5e3cac5190482927fdb8c78fb699193b5", size = 131867, upload-time = "2026-03-31T16:16:04.342Z" }, - { url = "https://files.pythonhosted.org/packages/19/30/2a645fc9286b928675e43fa2a3a16fb7b6764aa78cc719dc82141e00f30b/orjson-3.11.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5774c1fdcc98b2259800b683b19599c133baeb11d60033e2095fd9d4667b82db", size = 124664, upload-time = "2026-03-31T16:16:05.837Z" }, - { url = "https://files.pythonhosted.org/packages/db/44/77b9a86d84a28d52ba3316d77737f6514e17118119ade3f91b639e859029/orjson-3.11.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7381c83dd3d4a6347e6635950aa448f54e7b8406a27c7ecb4a37e9f1ae08b", size = 129701, upload-time = "2026-03-31T16:16:07.407Z" }, - { url = "https://files.pythonhosted.org/packages/b3/ea/eff3d9bfe47e9bc6969c9181c58d9f71237f923f9c86a2d2f490cd898c82/orjson-3.11.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14439063aebcb92401c11afc68ee4e407258d2752e62d748b6942dad20d2a70d", size = 141202, upload-time = "2026-03-31T16:16:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/52/c8/90d4b4c60c84d62068d0cf9e4d8f0a4e05e76971d133ac0c60d818d4db20/orjson-3.11.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa72e71977bff96567b0f500fc5bfd2fdf915f34052c782a4c6ebbdaa97aa858", size = 127194, upload-time = "2026-03-31T16:16:11.02Z" }, - { url = "https://files.pythonhosted.org/packages/8d/c7/ea9e08d1f0ba981adffb629811148b44774d935171e7b3d780ae43c4c254/orjson-3.11.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7679bc2f01bb0d219758f1a5f87bb7c8a81c0a186824a393b366876b4948e14f", size = 133639, upload-time = "2026-03-31T16:16:13.434Z" }, - { url = "https://files.pythonhosted.org/packages/6c/8c/ddbbfd6ba59453c8fc7fe1d0e5983895864e264c37481b2a791db635f046/orjson-3.11.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14f7b8fcb35ef403b42fa5ecfa4ed032332a91f3dc7368fbce4184d59e1eae0d", size = 141914, upload-time = "2026-03-31T16:16:14.955Z" }, - { url = "https://files.pythonhosted.org/packages/4e/31/dbfbefec9df060d34ef4962cd0afcb6fa7a9ec65884cb78f04a7859526c3/orjson-3.11.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c2bdf7b2facc80b5e34f48a2d557727d5c5c57a8a450de122ae81fa26a81c1bc", size = 423800, upload-time = "2026-03-31T16:16:16.594Z" }, - { url = "https://files.pythonhosted.org/packages/87/cf/f74e9ae9803d4ab46b163494adba636c6d7ea955af5cc23b8aaa94cfd528/orjson-3.11.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ccd7ba1b0605813a0715171d39ec4c314cb97a9c85893c2c5c0c3a3729df38bf", size = 147837, upload-time = "2026-03-31T16:16:18.585Z" }, - { url = "https://files.pythonhosted.org/packages/64/e6/9214f017b5db85e84e68602792f742e5dc5249e963503d1b356bee611e01/orjson-3.11.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbc8c9c02463fef4d3c53a9ba3336d05496ec8e1f1c53326a1e4acc11f5c600", size = 136441, upload-time = "2026-03-31T16:16:20.151Z" }, - { url = "https://files.pythonhosted.org/packages/24/dd/3590348818f58f837a75fb969b04cdf187ae197e14d60b5e5a794a38b79d/orjson-3.11.8-cp314-cp314-win32.whl", hash = "sha256:0b57f67710a8cd459e4e54eb96d5f77f3624eba0c661ba19a525807e42eccade", size = 131983, upload-time = "2026-03-31T16:16:21.823Z" }, - { url = "https://files.pythonhosted.org/packages/3f/0f/b6cb692116e05d058f31ceee819c70f097fa9167c82f67fabe7516289abc/orjson-3.11.8-cp314-cp314-win_amd64.whl", hash = "sha256:735e2262363dcbe05c35e3a8869898022af78f89dde9e256924dc02e99fe69ca", size = 127396, upload-time = "2026-03-31T16:16:23.685Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d1/facb5b5051fabb0ef9d26c6544d87ef19a939a9a001198655d0d891062dd/orjson-3.11.8-cp314-cp314-win_arm64.whl", hash = "sha256:6ccdea2c213cf9f3d9490cbd5d427693c870753df41e6cb375bd79bcbafc8817", size = 127330, upload-time = "2026-03-31T16:16:25.496Z" }, -] - [[package]] name = "packaging" version = "26.0" @@ -3307,18 +3100,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] -[[package]] -name = "pdf2image" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pillow" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/d8/b280f01045555dc257b8153c00dee3bc75830f91a744cd5f84ef3a0a64b1/pdf2image-1.17.0.tar.gz", hash = "sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57", size = 12811, upload-time = "2024-01-07T20:33:01.965Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/33/61766ae033518957f877ab246f87ca30a85b778ebaad65b7f74fa7e52988/pdf2image-1.17.0-py3-none-any.whl", hash = "sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2", size = 11618, upload-time = "2024-01-07T20:32:59.957Z" }, -] - [[package]] name = "pexpect" version = "4.9.0" @@ -3623,49 +3404,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] -[[package]] -name = "pyarrow" -version = "23.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336, upload-time = "2026-02-16T10:14:12.39Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/4b/4166bb5abbfe6f750fc60ad337c43ecf61340fa52ab386da6e8dbf9e63c4/pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", size = 34214575, upload-time = "2026-02-16T10:09:56.225Z" }, - { url = "https://files.pythonhosted.org/packages/e1/da/3f941e3734ac8088ea588b53e860baeddac8323ea40ce22e3d0baa865cc9/pyarrow-23.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", size = 35832540, upload-time = "2026-02-16T10:10:03.428Z" }, - { url = "https://files.pythonhosted.org/packages/88/7c/3d841c366620e906d54430817531b877ba646310296df42ef697308c2705/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", size = 44470940, upload-time = "2026-02-16T10:10:10.704Z" }, - { url = "https://files.pythonhosted.org/packages/2c/a5/da83046273d990f256cb79796a190bbf7ec999269705ddc609403f8c6b06/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", size = 47586063, upload-time = "2026-02-16T10:10:17.95Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/b7d2ebcff47a514f47f9da1e74b7949138c58cfeb108cdd4ee62f43f0cf3/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", size = 48173045, upload-time = "2026-02-16T10:10:25.363Z" }, - { url = "https://files.pythonhosted.org/packages/43/b2/b40961262213beaba6acfc88698eb773dfce32ecdf34d19291db94c2bd73/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", size = 50621741, upload-time = "2026-02-16T10:10:33.477Z" }, - { url = "https://files.pythonhosted.org/packages/f6/70/1fdda42d65b28b078e93d75d371b2185a61da89dda4def8ba6ba41ebdeb4/pyarrow-23.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", size = 27620678, upload-time = "2026-02-16T10:10:39.31Z" }, - { url = "https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8", size = 34210066, upload-time = "2026-02-16T10:10:45.487Z" }, - { url = "https://files.pythonhosted.org/packages/cb/4f/679fa7e84dadbaca7a65f7cdba8d6c83febbd93ca12fa4adf40ba3b6362b/pyarrow-23.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f", size = 35825526, upload-time = "2026-02-16T10:10:52.266Z" }, - { url = "https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677", size = 44473279, upload-time = "2026-02-16T10:11:01.557Z" }, - { url = "https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2", size = 47585798, upload-time = "2026-02-16T10:11:09.401Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/476943001c54ef078dbf9542280e22741219a184a0632862bca4feccd666/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37", size = 48179446, upload-time = "2026-02-16T10:11:17.781Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b6/5dd0c47b335fcd8edba9bfab78ad961bd0fd55ebe53468cc393f45e0be60/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2", size = 50623972, upload-time = "2026-02-16T10:11:26.185Z" }, - { url = "https://files.pythonhosted.org/packages/d5/09/a532297c9591a727d67760e2e756b83905dd89adb365a7f6e9c72578bcc1/pyarrow-23.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a", size = 27540749, upload-time = "2026-02-16T10:12:23.297Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8e/38749c4b1303e6ae76b3c80618f84861ae0c55dd3c2273842ea6f8258233/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1", size = 34471544, upload-time = "2026-02-16T10:11:32.535Z" }, - { url = "https://files.pythonhosted.org/packages/a3/73/f237b2bc8c669212f842bcfd842b04fc8d936bfc9d471630569132dc920d/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500", size = 35949911, upload-time = "2026-02-16T10:11:39.813Z" }, - { url = "https://files.pythonhosted.org/packages/0c/86/b912195eee0903b5611bf596833def7d146ab2d301afeb4b722c57ffc966/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41", size = 44520337, upload-time = "2026-02-16T10:11:47.764Z" }, - { url = "https://files.pythonhosted.org/packages/69/c2/f2a717fb824f62d0be952ea724b4f6f9372a17eed6f704b5c9526f12f2f1/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07", size = 47548944, upload-time = "2026-02-16T10:11:56.607Z" }, - { url = "https://files.pythonhosted.org/packages/84/a7/90007d476b9f0dc308e3bc57b832d004f848fd6c0da601375d20d92d1519/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83", size = 48236269, upload-time = "2026-02-16T10:12:04.47Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3f/b16fab3e77709856eb6ac328ce35f57a6d4a18462c7ca5186ef31b45e0e0/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125", size = 50604794, upload-time = "2026-02-16T10:12:11.797Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a1/22df0620a9fac31d68397a75465c344e83c3dfe521f7612aea33e27ab6c0/pyarrow-23.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8", size = 27660642, upload-time = "2026-02-16T10:12:17.746Z" }, - { url = "https://files.pythonhosted.org/packages/8d/1b/6da9a89583ce7b23ac611f183ae4843cd3a6cf54f079549b0e8c14031e73/pyarrow-23.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca", size = 34238755, upload-time = "2026-02-16T10:12:32.819Z" }, - { url = "https://files.pythonhosted.org/packages/ae/b5/d58a241fbe324dbaeb8df07be6af8752c846192d78d2272e551098f74e88/pyarrow-23.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1", size = 35847826, upload-time = "2026-02-16T10:12:38.949Z" }, - { url = "https://files.pythonhosted.org/packages/54/a5/8cbc83f04aba433ca7b331b38f39e000efd9f0c7ce47128670e737542996/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb", size = 44536859, upload-time = "2026-02-16T10:12:45.467Z" }, - { url = "https://files.pythonhosted.org/packages/36/2e/c0f017c405fcdc252dbccafbe05e36b0d0eb1ea9a958f081e01c6972927f/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1", size = 47614443, upload-time = "2026-02-16T10:12:55.525Z" }, - { url = "https://files.pythonhosted.org/packages/af/6b/2314a78057912f5627afa13ba43809d9d653e6630859618b0fd81a4e0759/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886", size = 48232991, upload-time = "2026-02-16T10:13:04.729Z" }, - { url = "https://files.pythonhosted.org/packages/40/f2/1bcb1d3be3460832ef3370d621142216e15a2c7c62602a4ea19ec240dd64/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f", size = 50645077, upload-time = "2026-02-16T10:13:14.147Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3f/b1da7b61cd66566a4d4c8383d376c606d1c34a906c3f1cb35c479f59d1aa/pyarrow-23.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5", size = 28234271, upload-time = "2026-02-16T10:14:09.397Z" }, - { url = "https://files.pythonhosted.org/packages/b5/78/07f67434e910a0f7323269be7bfbf58699bd0c1d080b18a1ab49ba943fe8/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d", size = 34488692, upload-time = "2026-02-16T10:13:21.541Z" }, - { url = "https://files.pythonhosted.org/packages/50/76/34cf7ae93ece1f740a04910d9f7e80ba166b9b4ab9596a953e9e62b90fe1/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f", size = 35964383, upload-time = "2026-02-16T10:13:28.63Z" }, - { url = "https://files.pythonhosted.org/packages/46/90/459b827238936d4244214be7c684e1b366a63f8c78c380807ae25ed92199/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814", size = 44538119, upload-time = "2026-02-16T10:13:35.506Z" }, - { url = "https://files.pythonhosted.org/packages/28/a1/93a71ae5881e99d1f9de1d4554a87be37da11cd6b152239fb5bd924fdc64/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d", size = 47571199, upload-time = "2026-02-16T10:13:42.504Z" }, - { url = "https://files.pythonhosted.org/packages/88/a3/d2c462d4ef313521eaf2eff04d204ac60775263f1fb08c374b543f79f610/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7", size = 48259435, upload-time = "2026-02-16T10:13:49.226Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f1/11a544b8c3d38a759eb3fbb022039117fd633e9a7b19e4841cc3da091915/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690", size = 50629149, upload-time = "2026-02-16T10:13:57.238Z" }, - { url = "https://files.pythonhosted.org/packages/50/f2/c0e76a0b451ffdf0cf788932e182758eb7558953f4f27f1aff8e2518b653/pyarrow-23.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", size = 28365807, upload-time = "2026-02-16T10:14:03.892Z" }, -] - [[package]] name = "pyasn1" version = "0.6.3" @@ -3810,15 +3548,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] -[[package]] -name = "pypdf" -version = "6.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/9f/ca96abf18683ca12602065e4ed2bec9050b672c87d317f1079abc7b6d993/pypdf-6.10.0.tar.gz", hash = "sha256:4c5a48ba258c37024ec2505f7e8fd858525f5502784a2e1c8d415604af29f6ef", size = 5314833, upload-time = "2026-04-10T09:34:57.102Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/f2/7ebe366f633f30a6ad105f650f44f24f98cb1335c4157d21ae47138b3482/pypdf-6.10.0-py3-none-any.whl", hash = "sha256:90005e959e1596c6e6c84c8b0ad383285b3e17011751cedd17f2ce8fcdfc86de", size = 334459, upload-time = "2026-04-10T09:34:54.966Z" }, -] - [[package]] name = "pyproject-hooks" version = "1.2.0" @@ -3934,19 +3663,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, ] -[[package]] -name = "python-docx" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "lxml" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" }, -] - [[package]] name = "python-dotenv" version = "1.2.2" @@ -4260,7 +3976,7 @@ wheels = [ [[package]] name = "reflexio-ai" -version = "0.2.13" +version = "0.2.11" source = { editable = "." } dependencies = [ { name = "aiohttp" }, @@ -4295,20 +4011,6 @@ dependencies = [ ] [package.optional-dependencies] -benchmark = [ - { name = "datasets" }, - { name = "fire" }, - { name = "fpdf2" }, - { name = "markdown" }, - { name = "openpyxl" }, - { name = "pdf2image" }, - { name = "pypdf" }, - { name = "python-docx" }, - { name = "reportlab" }, -] -langchain = [ - { name = "langchain-core" }, -] notebooks = [ { name = "pandas" }, ] @@ -4353,32 +4055,22 @@ requires-dist = [ { name = "braintrust", specifier = ">=0.12.0" }, { name = "cachetools", specifier = ">=6.2.4" }, { name = "colorlog", specifier = ">=6.10.1" }, - { name = "datasets", marker = "extra == 'benchmark'", specifier = ">=4.8.4" }, { name = "duckduckgo-search", specifier = ">=7.0.1" }, { name = "fastapi", specifier = ">=0.111.1" }, - { name = "fire", marker = "extra == 'benchmark'", specifier = ">=0.7.1" }, - { name = "fpdf2", marker = "extra == 'benchmark'", specifier = ">=2.8.7" }, { name = "hdbscan", specifier = ">=0.8.40" }, { name = "httpx", specifier = ">=0.28.1" }, - { name = "langchain-core", marker = "extra == 'langchain'", specifier = ">=1.2.28" }, { name = "litellm", specifier = ">=1.80.11" }, - { name = "markdown", marker = "extra == 'benchmark'", specifier = ">=3.10.2" }, { name = "nltk", specifier = ">=3.9.3" }, { name = "openai", specifier = ">=2.8.0" }, - { name = "openpyxl", marker = "extra == 'benchmark'", specifier = ">=3.1.5" }, { name = "pandas", marker = "extra == 'notebooks'", specifier = ">=3.0.2" }, { name = "passlib", specifier = ">=1.7.4" }, - { name = "pdf2image", marker = "extra == 'benchmark'", specifier = ">=1.17.0" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "pydantic", extras = ["email"], specifier = ">=2.13.0" }, - { name = "pypdf", marker = "extra == 'benchmark'", specifier = ">=6.10.0" }, { name = "python-dateutil", specifier = ">=2.8.0" }, - { name = "python-docx", marker = "extra == 'benchmark'", specifier = ">=1.2.0" }, { name = "python-dotenv", specifier = ">=1.1.0" }, { name = "python-jose", specifier = ">=3.3.0" }, { name = "pyyaml", specifier = ">=6.0" }, { name = "redis", specifier = ">=6.2.0" }, - { name = "reportlab", marker = "extra == 'benchmark'", specifier = ">=4.4.10" }, { name = "requests", specifier = ">=2.25.0" }, { name = "rich", specifier = ">=13.0.0" }, { name = "slowapi", specifier = ">=0.1.9" }, @@ -4390,7 +4082,7 @@ requires-dist = [ { name = "websocket-client", specifier = ">=1.8.0" }, { name = "xlsxwriter", specifier = ">=3.2.2" }, ] -provides-extras = ["vec", "notebooks", "langchain", "benchmark"] +provides-extras = ["vec", "notebooks"] [package.metadata.requires-dev] dev = [ @@ -4509,19 +4201,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/e7/ec846d560ae6a597115153c02ca6138a7877a1748b2072d9521c10a93e58/regex-2026.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:af0384cb01a33600c49505c27c6c57ab0b27bf84a74e28524c92ca897ebdac9d", size = 275773, upload-time = "2026-04-03T20:56:26.07Z" }, ] -[[package]] -name = "reportlab" -version = "4.4.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "charset-normalizer" }, - { name = "pillow" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/57/28bfbf0a775b618b6e4d854ef8dd3f5c8988e5d614d8898703502a35f61c/reportlab-4.4.10.tar.gz", hash = "sha256:5cbbb34ac3546039d0086deb2938cdec06b12da3cdb836e813258eb33cd28487", size = 3714962, upload-time = "2026-02-12T10:45:21.325Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/2e/e1798b8b248e1517e74c6cdf10dd6edd485044e7edf46b5f11ffcc5a0add/reportlab-4.4.10-py3-none-any.whl", hash = "sha256:5abc815746ae2bc44e7ff25db96814f921349ca814c992c7eac3c26029bf7c24", size = 1955400, upload-time = "2026-02-12T10:45:18.828Z" }, -] - [[package]] name = "requests" version = "2.33.1" @@ -5349,28 +5028,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] -[[package]] -name = "uuid-utils" -version = "0.14.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, - { url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, - { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, - { url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" }, - { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, - { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, - { url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" }, - { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, - { url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" }, - { url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" }, - { url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" }, -] - [[package]] name = "uvicorn" version = "0.44.0" @@ -5553,89 +5210,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/34/98a2f52245f4d47be93b580dae5f9861ef58977d73a79eb47c58f1ad1f3a/xmltodict-1.0.4-py3-none-any.whl", hash = "sha256:a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a", size = 13580, upload-time = "2026-02-22T02:21:21.039Z" }, ] -[[package]] -name = "xxhash" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, - { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, - { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, - { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, - { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, - { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, - { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, - { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, - { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, - { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, - { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, - { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, - { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, - { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, - { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, - { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, - { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, - { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, - { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, - { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, - { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, - { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, - { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, - { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, - { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, - { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, - { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, - { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, - { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, - { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, - { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, - { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, - { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, - { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, - { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, - { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, - { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, - { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, - { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, - { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, - { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, - { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, - { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, - { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, - { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, - { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, - { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, - { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, - { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, -] - [[package]] name = "yarl" version = "1.23.0" @@ -5748,60 +5322,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50e wheels = [ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ] - -[[package]] -name = "zstandard" -version = "0.25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, - { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, - { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, - { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, - { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, - { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, - { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, - { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, - { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, - { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, - { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, - { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, - { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, - { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, - { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, - { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, - { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, - { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, - { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, - { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, - { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, - { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, - { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, - { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, - { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, - { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, - { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, - { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, - { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, - { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, - { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, - { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, - { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, - { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, - { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, - { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, - { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, - { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, - { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, -]