feat(cad): LLM-orchestrated MCP tool-calling for KiCad and FreeCAD

Add Gemini Flash agentic loop that converts natural language prompts
into real MCP tool call sequences for PCB design (KiCad) and parametric
CAD (FreeCAD). Dynamic schema conversion from MCP tools to Gemini
function declarations, 8-turn/60s loop with real execution and result
feedback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-24 14:07:19 -07:00
parent d8736ba341
commit bf0661fab2
14 changed files with 1197 additions and 61 deletions

View File

@ -0,0 +1,51 @@
---
id: TASK-124
title: Encrypt all PII at rest in EncryptID database
status: Done
assignee: []
created_date: '2026-03-24 00:29'
updated_date: '2026-03-24 00:29'
labels:
- security
- encryptid
- database
dependencies: []
references:
- src/encryptid/server-crypto.ts
- src/encryptid/migrations/encrypt-pii.ts
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Server-side AES-256-GCM encryption for all PII fields stored in PostgreSQL. Keys derived from JWT_SECRET via HKDF with dedicated salts (`pii-v1` for encryption, `pii-hash-v1` for HMAC). HMAC-SHA256 hash indexes for equality lookups on email and UP address fields.
**Scope:** 18 fields across 6 tables (users, guardians, identity_invites, space_invites, notifications, fund_claims). Username and display_name excluded (public identifiers, needed for ILIKE search).
**Files:**
- `src/encryptid/server-crypto.ts` — NEW: encryptField(), decryptField(), hashForLookup()
- `src/encryptid/schema.sql` — 18 _enc/_hash columns + 4 indexes
- `src/encryptid/db.ts` — async row mappers with decrypt fallback, dual-write on inserts/updates, hash-based lookups
- `src/encryptid/server.ts` — replaced unkeyed hashEmail() with HMAC hashForLookup()
- `src/encryptid/migrations/encrypt-pii.ts` — NEW: idempotent backfill script
**Remaining:** Drop plaintext columns after extended verification period.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 All PII fields have corresponding _enc columns with AES-256-GCM ciphertext
- [x] #2 HMAC-SHA256 hash indexes enable email and UP address lookups without plaintext
- [x] #3 Row mappers decrypt transparently — callers receive plaintext
- [x] #4 Wrong encryption key cannot decrypt (verified with test)
- [x] #5 Same plaintext produces different ciphertext each time (random IV)
- [x] #6 Backfill migration encrypts all existing rows (0 remaining unencrypted)
- [x] #7 Legacy plaintext fallback works for pre-migration rows during transition
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Deployed 2026-03-23. Commit `9695e95`. Backfill completed: 1 user, 2 guardians, 8 identity invites, 2 fund claims encrypted. 19/19 verification tests passed (ciphertext format, decryption, HMAC determinism, wrong-key rejection, random IV uniqueness). Plaintext columns retained for rollback safety — drop in follow-up task after extended verification.
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@ -0,0 +1,63 @@
---
id: TASK-125
title: Configure Stripe & Mollie API keys and test HyperSwitch payment channels
status: To Do
assignee: []
created_date: '2026-03-24 00:56'
labels:
- payments
- hyperswitch
- infrastructure
dependencies: []
references:
- 'https://pay.rspace.online/health'
- 'https://dashboard.stripe.com/test/apikeys'
- 'https://my.mollie.com/dashboard'
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
HyperSwitch payment orchestrator is deployed at `pay.rspace.online` with merchant account `rspace_merchant` and DB migrations complete. The connector configuration and end-to-end payment testing is blocked on obtaining real API keys from Stripe and Mollie.
## Context
- HyperSwitch is live: `https://pay.rspace.online/health`
- Merchant account created with publishable key `pk_snd_9167de4f...`
- Merchant API key saved to `.env` as `HS_MERCHANT_SECRET_KEY`
- Internal mint/escrow/confirm APIs verified working on rspace-online
- Bonding curve ($MYCO) endpoints live and tested
- `INTERNAL_API_KEY` and `RSPACE_INTERNAL_API_KEY` deployed to both repos
## Steps
1. **Obtain Stripe API key** — create Stripe account or use existing, get test mode API key (`sk_test_...`)
2. **Obtain Mollie API key** — create Mollie account, get test API key
3. **Add keys to Infisical**`STRIPE_API_KEY`, `STRIPE_WEBHOOK_SECRET`, `MOLLIE_API_KEY` in rspace project
4. **Add keys to payment-infra `.env`** on Netcup
5. **Run `scripts/setup-hyperswitch.sh`** — configures Stripe + Mollie connectors, geo-based routing (EU→Mollie, US→Stripe), webhook endpoint
6. **Rebuild payment-infra onramp/offramp services** — they have new HyperSwitch integration code (`hyperswitch.ts`, `hyperswitch-offramp.ts`) but haven't been rebuilt
7. **Test Stripe channel** — create payment intent, complete with test card `4242424242424242`, verify cUSDC minted
8. **Test Mollie channel** — create payment intent with EU billing, complete via Mollie test mode, verify cUSDC minted
9. **Test off-ramp** — initiate withdrawal, verify escrow burn, simulate payout webhook, verify confirm/reverse
10. **Run `bun scripts/test-full-loop.ts`** — full loop: fiat in → cUSDC → $MYCO → cUSDC → fiat out
## Key files
- `payment-infra/scripts/setup-hyperswitch.sh` — connector + routing setup script
- `payment-infra/services/onramp-service/src/hyperswitch.ts` — on-ramp integration
- `payment-infra/services/offramp-service/src/hyperswitch-offramp.ts` — off-ramp integration
- `rspace-online/scripts/test-full-loop.ts` — end-to-end test script
- `rspace-online/server/index.ts` — internal mint/escrow/confirm endpoints (lines 570-680)
- `payment-infra/config/hyperswitch/config.toml` — HyperSwitch TOML config on Netcup
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 Stripe test API key obtained and added to Infisical + payment-infra .env
- [ ] #2 Mollie test API key obtained and added to Infisical + payment-infra .env
- [ ] #3 setup-hyperswitch.sh runs successfully — Stripe + Mollie connectors configured with geo-based routing
- [ ] #4 onramp-service and offramp-service rebuilt with HyperSwitch integration code
- [ ] #5 Stripe test payment completes end-to-end: card payment → webhook → cUSDC minted in CRDT ledger
- [ ] #6 Mollie test payment completes end-to-end: iDEAL/SEPA → webhook → cUSDC minted
- [ ] #7 Off-ramp escrow flow verified: escrow burn → payout → confirm (or reverse on failure)
- [ ] #8 Full loop test passes: fiat → cUSDC → $MYCO swap → cUSDC → fiat withdrawal
<!-- AC:END -->

136
bun.lock
View File

@ -11,6 +11,7 @@
"@google/genai": "^1.43.0",
"@google/generative-ai": "^0.24.1",
"@lit/reactive-element": "^2.0.4",
"@modelcontextprotocol/sdk": "^1.27.1",
"@noble/curves": "^1.8.0",
"@noble/hashes": "^1.7.0",
"@openfort/openfort-node": "^0.7.0",
@ -42,6 +43,7 @@
"mailparser": "^3.7.2",
"marked": "^17.0.3",
"nodemailer": "^6.9.0",
"pdf-lib": "^1.17.1",
"perfect-arrows": "^0.3.7",
"perfect-freehand": "^1.2.2",
"postgres": "^3.4.5",
@ -215,6 +217,8 @@
"@google/generative-ai": ["@google/generative-ai@0.24.1", "", {}, "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q=="],
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
@ -261,6 +265,8 @@
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
"@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
"@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="],
@ -271,6 +277,10 @@
"@openfort/shield-js": ["@openfort/shield-js@0.1.35", "", { "dependencies": { "axios": "1.13.6", "axios-retry": "4.5.0" } }, "sha512-S/v73xRnbgv5i47IRJ7cPWOnJ1bUTOJN+048Y8mJ+ya+iRJUXKTTqePaWApvfrVHHcKA+li8QGyDsRRDefLLVQ=="],
"@pdf-lib/standard-fonts": ["@pdf-lib/standard-fonts@1.0.0", "", { "dependencies": { "pako": "^1.0.6" } }, "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA=="],
"@pdf-lib/upng": ["@pdf-lib/upng@1.0.1", "", { "dependencies": { "pako": "^1.0.10" } }, "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ=="],
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
@ -603,12 +613,16 @@
"abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"aes-js": ["aes-js@4.0.0-beta.5", "", {}, "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
@ -637,6 +651,8 @@
"bn.js": ["bn.js@4.12.3", "", {}, "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g=="],
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
"bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="],
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
@ -647,8 +663,12 @@
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
"cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
@ -663,8 +683,18 @@
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
"cron-parser": ["cron-parser@5.5.0", "", { "dependencies": { "luxon": "^3.7.1" } }, "sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww=="],
@ -683,6 +713,8 @@
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
@ -705,8 +737,12 @@
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"encoding-japanese": ["encoding-japanese@2.2.0", "", {}, "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
@ -721,12 +757,24 @@
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"ethers": ["ethers@6.16.0", "", { "dependencies": { "@adraffy/ens-normalize": "1.10.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.2", "@types/node": "22.7.5", "aes-js": "4.0.0-beta.5", "tslib": "2.7.0", "ws": "8.17.1" } }, "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A=="],
"eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
@ -739,6 +787,8 @@
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
@ -749,6 +799,10 @@
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
@ -789,6 +843,8 @@
"htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"http_ece": ["http_ece@1.2.0", "", {}, "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
@ -803,10 +859,14 @@
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"is-retry-allowed": ["is-retry-allowed@2.2.0", "", {}, "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg=="],
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
@ -825,6 +885,8 @@
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
"jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="],
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
@ -867,9 +929,13 @@
"mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="],
@ -883,14 +949,24 @@
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"nodemailer": ["nodemailer@6.10.1", "", {}, "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"orderedmap": ["orderedmap@2.1.1", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="],
"ox": ["ox@0.12.4", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-+P+C7QzuwPV8lu79dOwjBKfB2CbnbEXe/hfyyrff1drrO1nOOj3Hc87svHfcW1yneRr3WXaKr6nz11nq+/DF9Q=="],
@ -909,12 +985,18 @@
"parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
"pdf-lib": ["pdf-lib@1.17.1", "", { "dependencies": { "@pdf-lib/standard-fonts": "^1.0.0", "@pdf-lib/upng": "^1.0.1", "pako": "^1.0.11", "tslib": "^1.11.1" } }, "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw=="],
"peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="],
"perfect-arrows": ["perfect-arrows@0.3.7", "", {}, "sha512-wEN2gerTPVWl3yqoFEF8OeGbg3aRe2sxNUi9rnyYrCsL4JcI6K2tBDezRtqVrYG0BNtsWLdYiiTrYm+X//8yLQ=="],
@ -931,6 +1013,8 @@
"pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="],
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
"playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
"playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="],
@ -983,6 +1067,8 @@
"protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
@ -991,8 +1077,14 @@
"qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
@ -1011,6 +1103,8 @@
"rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
@ -1021,16 +1115,30 @@
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="],
@ -1047,6 +1155,8 @@
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@ -1065,6 +1175,8 @@
"tlds": ["tlds@1.261.0", "", { "bin": { "tlds": "bin.js" } }, "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@ -1073,12 +1185,16 @@
"tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
@ -1087,6 +1203,8 @@
"valid-url": ["valid-url@1.0.9", "", {}, "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"viem": ["viem@2.46.3", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.12.4", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-2LJS+Hyh2sYjHXQtzfv1kU9pZx9dxFzvoU/ZKIcn0FNtOU0HQuIICuYdWtUDFHaGXbAdVo8J1eCvmjkL9JVGwg=="],
"vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
@ -1113,6 +1231,8 @@
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"y-indexeddb": ["y-indexeddb@9.0.12", "", { "dependencies": { "lib0": "^0.2.74" }, "peerDependencies": { "yjs": "^13.0.0" } }, "sha512-9oCFRSPPzBK7/w5vOkJBaVCQZKHXB/v6SIT+WYhnJxlEC61juqG0hBrAf+y3gmSMLFLwICNH9nQ53uscuse6Hg=="],
@ -1133,6 +1253,8 @@
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
"@automerge/automerge/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
@ -1153,6 +1275,8 @@
"@openfort/openfort-node/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
"body-parser/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"ecdsa-sig-formatter/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"ethers/@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.10.1", "", {}, "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw=="],
@ -1167,6 +1291,8 @@
"ethers/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="],
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"imapflow/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
@ -1181,8 +1307,12 @@
"mailparser/nodemailer": ["nodemailer@7.0.13", "", {}, "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw=="],
"pdf-lib/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
"raw-body/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
@ -1197,6 +1327,8 @@
"ethers/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],

View File

@ -269,6 +269,7 @@ export class FolkBlender extends FolkShape {
#isLoading = false;
#error: string | null = null;
#prompt: string | null = null;
#renderUrl: string | null = null;
#script: string | null = null;
#blendUrl: string | null = null;
@ -365,6 +366,25 @@ export class FolkBlender extends FolkShape {
this.dispatchEvent(new CustomEvent("close"));
});
// Restore persisted state
if (this.#prompt && this.#promptInput) this.#promptInput.value = this.#prompt;
if (this.#renderUrl || this.#script) {
this.#renderResult();
}
// Health check
fetch("/api/blender-gen/health").then(r => r.json()).then((h: any) => {
if (!h.available && this.#generateBtn) {
this.#generateBtn.disabled = true;
this.#generateBtn.title = (h.issues || []).join(", ") || "Blender service unavailable";
}
}).catch(() => {
if (this.#generateBtn) {
this.#generateBtn.disabled = true;
this.#generateBtn.title = "Cannot reach Blender health endpoint";
}
});
return root;
}
@ -372,6 +392,7 @@ export class FolkBlender extends FolkShape {
const prompt = this.#promptInput?.value.trim();
if (!prompt || this.#isLoading) return;
this.#prompt = prompt;
this.#isLoading = true;
this.#error = null;
if (this.#generateBtn) this.#generateBtn.disabled = true;
@ -437,6 +458,10 @@ export class FolkBlender extends FolkShape {
static override fromData(data: Record<string, any>): FolkBlender {
const shape = FolkShape.fromData(data) as FolkBlender;
if (data.prompt) shape.#prompt = data.prompt;
if (data.renderUrl) shape.#renderUrl = data.renderUrl;
if (data.script) shape.#script = data.script;
if (data.blendUrl) shape.#blendUrl = data.blendUrl;
return shape;
}
@ -444,6 +469,7 @@ export class FolkBlender extends FolkShape {
return {
...super.toJSON(),
type: "folk-blender",
prompt: this.#prompt,
renderUrl: this.#renderUrl,
script: this.#script,
blendUrl: this.#blendUrl,
@ -452,5 +478,13 @@ export class FolkBlender extends FolkShape {
override applyData(data: Record<string, any>): void {
super.applyData(data);
if ("prompt" in data) this.#prompt = data.prompt;
if ("renderUrl" in data) this.#renderUrl = data.renderUrl;
if ("script" in data) this.#script = data.script;
if ("blendUrl" in data) this.#blendUrl = data.blendUrl;
if (this.#promptInput && this.#prompt) this.#promptInput.value = this.#prompt;
if (this.#renderUrl || this.#script) {
this.#renderResult();
}
}
}

View File

@ -369,6 +369,7 @@ export class FolkEmbed extends FolkShape {
"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture";
iframe.allowFullscreen = true;
iframe.referrerPolicy = "no-referrer";
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups allow-presentation");
content.appendChild(iframe);
}

View File

@ -221,6 +221,7 @@ export class FolkFreeCAD extends FolkShape {
#isLoading = false;
#error: string | null = null;
#prompt: string | null = null;
#previewUrl: string | null = null;
#stepUrl: string | null = null;
#stlUrl: string | null = null;
@ -293,6 +294,25 @@ export class FolkFreeCAD extends FolkShape {
this.dispatchEvent(new CustomEvent("close"));
});
// Restore persisted state
if (this.#prompt && this.#promptInput) this.#promptInput.value = this.#prompt;
if (this.#previewUrl || this.#stepUrl || this.#stlUrl) {
this.#renderResult();
}
// Health check
fetch("/api/freecad/health").then(r => r.json()).then((h: any) => {
if (!h.available && this.#generateBtn) {
this.#generateBtn.disabled = true;
this.#generateBtn.title = h.error || "FreeCAD MCP server unavailable";
}
}).catch(() => {
if (this.#generateBtn) {
this.#generateBtn.disabled = true;
this.#generateBtn.title = "Cannot reach FreeCAD health endpoint";
}
});
return root;
}
@ -300,12 +320,13 @@ export class FolkFreeCAD extends FolkShape {
const prompt = this.#promptInput?.value.trim();
if (!prompt || this.#isLoading) return;
this.#prompt = prompt;
this.#isLoading = true;
this.#error = null;
if (this.#generateBtn) this.#generateBtn.disabled = true;
if (this.#previewArea) {
this.#previewArea.innerHTML = '<div class="loading"><div class="spinner"></div><span>Generating CAD model...</span></div>';
this.#previewArea.innerHTML = '<div class="loading"><div class="spinner"></div><span>Generating CAD model with AI... (may take 30-60s)</span></div>';
}
try {
@ -369,6 +390,10 @@ export class FolkFreeCAD extends FolkShape {
static override fromData(data: Record<string, any>): FolkFreeCAD {
const shape = FolkShape.fromData(data) as FolkFreeCAD;
if (data.prompt) shape.#prompt = data.prompt;
if (data.previewUrl) shape.#previewUrl = data.previewUrl;
if (data.stepUrl) shape.#stepUrl = data.stepUrl;
if (data.stlUrl) shape.#stlUrl = data.stlUrl;
return shape;
}
@ -376,6 +401,7 @@ export class FolkFreeCAD extends FolkShape {
return {
...super.toJSON(),
type: "folk-freecad",
prompt: this.#prompt,
previewUrl: this.#previewUrl,
stepUrl: this.#stepUrl,
stlUrl: this.#stlUrl,
@ -384,5 +410,13 @@ export class FolkFreeCAD extends FolkShape {
override applyData(data: Record<string, any>): void {
super.applyData(data);
if ("prompt" in data) this.#prompt = data.prompt;
if ("previewUrl" in data) this.#previewUrl = data.previewUrl;
if ("stepUrl" in data) this.#stepUrl = data.stepUrl;
if ("stlUrl" in data) this.#stlUrl = data.stlUrl;
if (this.#promptInput && this.#prompt) this.#promptInput.value = this.#prompt;
if (this.#previewUrl || this.#stepUrl || this.#stlUrl) {
this.#renderResult();
}
}
}

View File

@ -275,6 +275,8 @@ export class FolkKiCAD extends FolkShape {
#isLoading = false;
#error: string | null = null;
#prompt: string | null = null;
#components: string[] = [];
#schematicSvg: string | null = null;
#boardSvg: string | null = null;
#gerberUrl: string | null = null;
@ -373,6 +375,27 @@ export class FolkKiCAD extends FolkShape {
this.dispatchEvent(new CustomEvent("close"));
});
// Restore persisted state
if (this.#prompt && this.#promptInput) this.#promptInput.value = this.#prompt;
if (this.#components.length && this.#componentInput) this.#componentInput.value = this.#components.join(", ");
if (this.#schematicSvg || this.#boardSvg || this.#drcResults) {
this.#renderPreview();
this.#showExports();
}
// Health check
fetch("/api/kicad/health").then(r => r.json()).then((h: any) => {
if (!h.available && this.#generateBtn) {
this.#generateBtn.disabled = true;
this.#generateBtn.title = h.error || "KiCAD MCP server unavailable";
}
}).catch(() => {
if (this.#generateBtn) {
this.#generateBtn.disabled = true;
this.#generateBtn.title = "Cannot reach KiCAD health endpoint";
}
});
return root;
}
@ -385,17 +408,18 @@ export class FolkKiCAD extends FolkShape {
.map((c) => c.trim())
.filter(Boolean) || [];
this.#prompt = prompt;
this.#components = components;
this.#isLoading = true;
this.#error = null;
if (this.#generateBtn) this.#generateBtn.disabled = true;
if (this.#previewArea) {
this.#previewArea.innerHTML = '<div class="loading"><div class="spinner"></div><span>Generating PCB design...</span></div>';
this.#previewArea.innerHTML = '<div class="loading"><div class="spinner"></div><span>Designing PCB with AI... (may take 30-60s)</span></div>';
}
try {
// Step 1: Create project
const createRes = await fetch("/api/kicad/create_project", {
const createRes = await fetch("/api/kicad/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt, components }),
@ -485,6 +509,14 @@ export class FolkKiCAD extends FolkShape {
static override fromData(data: Record<string, any>): FolkKiCAD {
const shape = FolkShape.fromData(data) as FolkKiCAD;
if (data.prompt) shape.#prompt = data.prompt;
if (data.components) shape.#components = data.components;
if (data.schematicSvg) shape.#schematicSvg = data.schematicSvg;
if (data.boardSvg) shape.#boardSvg = data.boardSvg;
if (data.gerberUrl) shape.#gerberUrl = data.gerberUrl;
if (data.bomUrl) shape.#bomUrl = data.bomUrl;
if (data.pdfUrl) shape.#pdfUrl = data.pdfUrl;
if (data.drcResults) shape.#drcResults = data.drcResults;
return shape;
}
@ -492,15 +524,32 @@ export class FolkKiCAD extends FolkShape {
return {
...super.toJSON(),
type: "folk-kicad",
prompt: this.#prompt,
components: this.#components,
schematicSvg: this.#schematicSvg,
boardSvg: this.#boardSvg,
gerberUrl: this.#gerberUrl,
bomUrl: this.#bomUrl,
pdfUrl: this.#pdfUrl,
drcResults: this.#drcResults,
};
}
override applyData(data: Record<string, any>): void {
super.applyData(data);
if ("prompt" in data) this.#prompt = data.prompt;
if ("components" in data) this.#components = data.components || [];
if ("schematicSvg" in data) this.#schematicSvg = data.schematicSvg;
if ("boardSvg" in data) this.#boardSvg = data.boardSvg;
if ("gerberUrl" in data) this.#gerberUrl = data.gerberUrl;
if ("bomUrl" in data) this.#bomUrl = data.bomUrl;
if ("pdfUrl" in data) this.#pdfUrl = data.pdfUrl;
if ("drcResults" in data) this.#drcResults = data.drcResults;
if (this.#promptInput && this.#prompt) this.#promptInput.value = this.#prompt;
if (this.#componentInput && this.#components.length) this.#componentInput.value = this.#components.join(", ");
if (this.#schematicSvg || this.#boardSvg || this.#drcResults) {
this.#renderPreview();
this.#showExports();
}
}
}

View File

@ -29,7 +29,6 @@ import { createSlashCommandPlugin } from './slash-command';
import type { ImportExportDialog } from './import-export-dialog';
import { SpeechDictation } from '../../../lib/speech-dictation';
import { TourEngine } from '../../../shared/tour-engine';
import { ViewHistory } from '../../../shared/view-history.js';
import * as Y from 'yjs';
import { IndexeddbPersistence } from 'y-indexeddb';
import { ySyncPlugin, yUndoPlugin, yCursorPlugin } from '@tiptap/y-tiptap';
@ -124,7 +123,6 @@ interface NotebookDoc {
class FolkNotesApp extends HTMLElement {
private shadow!: ShadowRoot;
private space = "";
private view: "notebooks" | "notebook" | "note" = "notebooks";
private notebooks: Notebook[] = [];
private selectedNotebook: (Notebook & { notes: Note[] }) | null = null;
private selectedNote: Note | null = null;
@ -134,19 +132,21 @@ class FolkNotesApp extends HTMLElement {
private loading = false;
private error = "";
// Sidebar state
private expandedNotebooks = new Set<string>();
private notebookNotes = new Map<string, Note[]>();
private sidebarOpen = true;
// Zone-based rendering
private navZone!: HTMLDivElement;
private contentZone!: HTMLDivElement;
private metaZone!: HTMLDivElement;
// Navigation history
private _history = new ViewHistory<"notebooks" | "notebook" | "note">("notebooks");
// Guided tour
private _tour!: TourEngine;
private static readonly TOUR_STEPS = [
{ target: '#create-notebook', title: "Create a Notebook", message: "Notebooks organise your notes by topic. Click '+ New Notebook' to create one.", advanceOnClick: true },
{ target: '#create-note', title: "Create a Note", message: "Inside a notebook you can add notes, links, tasks, and more. Click '+ New Note' to add one.", advanceOnClick: true },
{ target: '.sbt-nb-add', title: "Create a Note", message: "Each notebook has a '+' button to add new notes. Click it to create one.", advanceOnClick: true },
{ target: '#editor-toolbar', title: "Editor Toolbar", message: "Format text with the toolbar — bold, lists, code blocks, headings, and more. Click Next to continue.", advanceOnClick: false },
{ target: '[data-cmd="mic"]', title: "Voice Notes", message: "Record voice notes with live transcription. Your words appear as you speak — no uploads needed.", advanceOnClick: false },
];
@ -260,16 +260,50 @@ class FolkNotesApp extends HTMLElement {
private setupShadow() {
const style = document.createElement('style');
style.textContent = this.getStyles();
const layout = document.createElement('div');
layout.id = 'notes-layout';
this.navZone = document.createElement('div');
this.navZone.id = 'nav-zone';
const rightCol = document.createElement('div');
rightCol.className = 'notes-right-col';
this.contentZone = document.createElement('div');
this.contentZone.id = 'content-zone';
this.metaZone = document.createElement('div');
this.metaZone.id = 'meta-zone';
rightCol.appendChild(this.contentZone);
rightCol.appendChild(this.metaZone);
layout.appendChild(this.navZone);
layout.appendChild(rightCol);
this.shadow.appendChild(style);
this.shadow.appendChild(this.navZone);
this.shadow.appendChild(this.contentZone);
this.shadow.appendChild(this.metaZone);
this.shadow.appendChild(layout);
// Mobile sidebar toggle
const mobileToggle = document.createElement('button');
mobileToggle.className = 'mobile-sidebar-toggle';
mobileToggle.innerHTML = '\u2630';
mobileToggle.addEventListener('click', () => {
this.sidebarOpen = !this.sidebarOpen;
this.navZone.querySelector('.notes-sidebar')?.classList.toggle('open', this.sidebarOpen);
this.shadow.querySelector('.sidebar-overlay')?.classList.toggle('open', this.sidebarOpen);
});
this.shadow.appendChild(mobileToggle);
// Mobile overlay
const overlay = document.createElement('div');
overlay.className = 'sidebar-overlay';
overlay.addEventListener('click', () => {
this.sidebarOpen = false;
this.navZone.querySelector('.notes-sidebar')?.classList.remove('open');
overlay.classList.remove('open');
});
this.shadow.appendChild(overlay);
}
// ── Demo data ──
@ -429,6 +463,13 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
];
this.notebooks = this.demoNotebooks.map(({ notes, ...nb }) => nb as Notebook);
// Populate sidebar note cache and expand first notebook
for (const nb of this.demoNotebooks) {
this.notebookNotes.set(nb.id, nb.notes);
}
if (this.demoNotebooks.length > 0) {
this.expandedNotebooks.add(this.demoNotebooks[0].id);
}
this.loading = false;
this.render();
}

View File

@ -401,6 +401,126 @@
background: rgba(20, 184, 166, 0.1);
}
/* ── Platform selector chips ── */
.cw-platform-selector {
margin-top: 1rem;
}
.cw-platform-selector__label {
font-size: 0.8rem;
color: var(--rs-text-muted, #64748b);
display: block;
margin-bottom: 0.5rem;
}
.cw-platform-selector__chips {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.cw-platform-chip {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.35rem 0.7rem;
border-radius: 20px;
border: 1px solid var(--rs-border, #444);
background: transparent;
color: var(--rs-text-muted, #64748b);
font-size: 0.8rem;
font-family: inherit;
cursor: pointer;
transition: all 0.15s;
text-transform: capitalize;
}
.cw-platform-chip:hover {
border-color: var(--rs-accent, #14b8a6);
color: var(--rs-text-primary, #e1e1e1);
}
.cw-platform-chip--active {
background: rgba(20, 184, 166, 0.15);
border-color: var(--rs-accent, #14b8a6);
color: var(--rs-accent, #14b8a6);
}
/* ── Clickable phase card headers ── */
.cw-phase-card__header--clickable {
cursor: pointer;
user-select: none;
border-radius: 6px;
transition: background 0.15s;
}
.cw-phase-card__header--clickable:hover {
background: rgba(255, 255, 255, 0.03);
}
/* ── Phase chevron ── */
.cw-phase-chevron {
display: inline-block;
font-size: 0.65rem;
color: var(--rs-text-muted, #64748b);
transition: transform 0.2s ease;
}
.cw-phase-chevron--open {
transform: rotate(90deg);
}
/* ── Phase expanded preview ── */
.cw-phase-card__preview {
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid var(--rs-border, #333);
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.cw-phase-preview-post {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8rem;
color: var(--rs-text-secondary, #94a3b8);
}
.cw-phase-preview-post__spread {
color: var(--rs-text-muted, #64748b);
font-size: 0.75rem;
margin-left: auto;
}
/* ── Phase instructions textarea ── */
.cw-phase-instructions {
width: 100%;
min-height: 56px;
padding: 0.5rem 0.75rem;
margin-top: 0.5rem;
border: 1px solid var(--rs-input-border, #334155);
border-radius: 6px;
background: var(--rs-bg-surface, #1e1e2e);
color: var(--rs-text-primary, #e1e1e1);
font-family: inherit;
font-size: 0.8rem;
line-height: 1.5;
resize: vertical;
box-sizing: border-box;
}
.cw-phase-instructions:focus {
outline: none;
border-color: var(--rs-accent, #14b8a6);
}
.cw-phase-instructions::placeholder {
color: var(--rs-text-muted, #64748b);
font-style: italic;
}
/* ── Responsive ── */
@media (max-width: 640px) {
.cw-brief-summary {

View File

@ -101,6 +101,9 @@ export class FolkCampaignWizard extends HTMLElement {
private _expandedPosts: Set<number> = new Set();
private _regenIndex = -1;
private _regenInstructions = '';
private _selectedPlatforms: Set<string> = new Set(['x', 'linkedin', 'instagram', 'youtube', 'threads', 'bluesky', 'newsletter']);
private _expandedPhases: Set<number> = new Set();
private _phaseInstructions: Map<number, string> = new Map();
static get observedAttributes() { return ['space', 'wizard-id']; }
@ -155,6 +158,11 @@ export class FolkCampaignWizard extends HTMLElement {
this._campaignDraft = data.campaignDraft;
this._committedCampaignId = data.committedCampaignId;
// Sync selected platforms from extracted brief
if (data.extractedBrief?.platforms?.length) {
this._selectedPlatforms = new Set(data.extractedBrief.platforms);
}
if (data.step === 'committed') {
this._step = 'activate';
} else if (data.step === 'abandoned') {
@ -212,7 +220,7 @@ export class FolkCampaignWizard extends HTMLElement {
try {
const res = await this.apiFetch(`/api/campaign/wizard/${this._wizardId}/structure`, {
method: 'POST',
body: JSON.stringify({ rawBrief: this._rawBrief }),
body: JSON.stringify({ rawBrief: this._rawBrief, selectedPlatforms: Array.from(this._selectedPlatforms) }),
});
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Analysis failed');
const data: WizardState = await res.json();
@ -232,9 +240,11 @@ export class FolkCampaignWizard extends HTMLElement {
this.render();
try {
const phaseInstructions: Record<number, string> = {};
this._phaseInstructions.forEach((v, k) => { if (v.trim()) phaseInstructions[k] = v; });
const res = await this.apiFetch(`/api/campaign/wizard/${this._wizardId}/content`, {
method: 'POST',
body: JSON.stringify({}),
body: JSON.stringify({ phaseInstructions }),
});
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Generation failed');
const data: WizardState = await res.json();
@ -367,10 +377,20 @@ export class FolkCampaignWizard extends HTMLElement {
}
private renderBriefStep(): string {
const allPlatforms = ['x', 'linkedin', 'instagram', 'youtube', 'threads', 'bluesky', 'newsletter'];
const platformChips = allPlatforms.map(p => {
const active = this._selectedPlatforms.has(p);
return `<button class="cw-platform-chip ${active ? 'cw-platform-chip--active' : ''}" data-platform="${p}">${PLATFORM_ICONS[p] || ''} ${p}</button>`;
}).join('');
return `<div class="cw-panel">
<h2>Step 1: Paste Your Campaign Brief</h2>
<p class="cw-hint">Paste raw text describing your event, product launch, or campaign goals. The AI will extract key details and propose a structure.</p>
<textarea class="cw-textarea" id="brief-input" placeholder="Example: We're launching MycoFi at ETHDenver on March 25, 2026. Target audience is web3 developers and DeFi enthusiasts. We want to build hype for 2 weeks before, have a strong presence during the 3-day event, and follow up for a week after...">${this.escHtml(this._rawBrief)}</textarea>
<div class="cw-platform-selector">
<span class="cw-platform-selector__label">Include platforms:</span>
<div class="cw-platform-selector__chips">${platformChips}</div>
</div>
<div class="cw-actions">
<button class="cw-btn cw-btn--primary" id="analyze-btn" ${this._rawBrief.trim().length < 10 ? 'disabled' : ''}>Analyze Brief</button>
<button class="cw-btn cw-btn--ghost" id="mi-btn">Refine with MI</button>
@ -394,19 +414,48 @@ export class FolkCampaignWizard extends HTMLElement {
</div>
`;
const phases = structure.phases.map(p => `
const phases = structure.phases.map((p, pi) => {
const isExpanded = this._expandedPhases.has(pi);
const chevronCls = isExpanded ? 'cw-phase-chevron cw-phase-chevron--open' : 'cw-phase-chevron';
const instructions = this._phaseInstructions.get(pi) || '';
// Infer post type per platform
const postTypeMap: Record<string, string> = {
x: 'thread', linkedin: 'text', instagram: 'carousel',
youtube: 'video', threads: 'thread', bluesky: 'text', newsletter: 'email',
};
const postStubs = Object.entries(p.cadence || {}).map(([plat, count]) =>
`<div class="cw-phase-preview-post">
<span class="cw-cadence-badge">${PLATFORM_ICONS[plat] || ''} ${plat}</span>
<span>${count} ${postTypeMap[plat] || 'text'} post${count !== 1 ? 's' : ''}</span>
<span class="cw-phase-preview-post__spread">spread over ${this.escHtml(p.days)}</span>
</div>`
).join('');
return `
<div class="cw-phase-card">
<div class="cw-phase-card__header">
<div class="cw-phase-card__header cw-phase-card__header--clickable" data-phase-toggle="${pi}">
<span class="cw-phase-card__name">${this.escHtml(p.label)}</span>
<span style="display:flex;align-items:center;gap:0.5rem">
<span class="cw-phase-card__days">${this.escHtml(p.days)}</span>
<span class="${chevronCls}">\u25B6</span>
</span>
</div>
<div class="cw-phase-card__cadence">
${Object.entries(p.cadence || {}).map(([plat, count]) =>
`<span class="cw-cadence-badge">${PLATFORM_ICONS[plat] || ''} ${plat}: ${count} post${count !== 1 ? 's' : ''}</span>`
).join('')}
</div>
${isExpanded ? `
<div class="cw-phase-card__preview">
${postStubs}
<textarea class="cw-phase-instructions" data-phase-instructions="${pi}" placeholder="Optional: guide the AI for this phase (e.g. 'focus on countdown energy')">${this.escHtml(instructions)}</textarea>
</div>
`).join('');
` : ''}
</div>
`;
}).join('');
return `
<div class="cw-panel">
@ -567,6 +616,21 @@ export class FolkCampaignWizard extends HTMLElement {
});
}
// Platform chip toggles
sr.querySelectorAll('[data-platform]').forEach(el => {
el.addEventListener('click', () => {
const plat = el.getAttribute('data-platform')!;
if (this._selectedPlatforms.has(plat)) {
if (this._selectedPlatforms.size > 1) {
this._selectedPlatforms.delete(plat);
}
} else {
this._selectedPlatforms.add(plat);
}
this.render();
});
});
sr.querySelector('#analyze-btn')?.addEventListener('click', () => this.analyzebrief());
sr.querySelector('#mi-btn')?.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('mi-prompt', {
@ -575,7 +639,24 @@ export class FolkCampaignWizard extends HTMLElement {
}));
});
// Structure step
// Structure step — phase card expand/collapse
sr.querySelectorAll('[data-phase-toggle]').forEach(el => {
el.addEventListener('click', () => {
const pi = parseInt(el.getAttribute('data-phase-toggle')!);
if (this._expandedPhases.has(pi)) this._expandedPhases.delete(pi);
else this._expandedPhases.add(pi);
this.render();
});
});
sr.querySelectorAll('[data-phase-instructions]').forEach(el => {
const ta = el as HTMLTextAreaElement;
ta.addEventListener('click', (e) => e.stopPropagation());
ta.addEventListener('input', () => {
const pi = parseInt(ta.getAttribute('data-phase-instructions')!);
this._phaseInstructions.set(pi, ta.value);
});
});
sr.querySelector('#approve-structure-btn')?.addEventListener('click', () => this.generateContent());
sr.querySelector('#regen-structure-btn')?.addEventListener('click', () => this.analyzebrief());
sr.querySelector('#back-to-brief-btn')?.addEventListener('click', () => { this._step = 'brief'; this.render(); });

View File

@ -1241,6 +1241,13 @@ routes.post("/api/campaign/wizard/:id/structure", async (c) => {
return c.json({ error: "Brief is required (min 10 characters)" }, 400);
}
const selectedPlatforms: string[] = Array.isArray(body.selectedPlatforms) && body.selectedPlatforms.length > 0
? body.selectedPlatforms
: null;
const platformConstraint = selectedPlatforms
? `\n- ONLY use these platforms: ${selectedPlatforms.join(', ')}. Do NOT include any other platforms.`
: '';
const { GoogleGenerativeAI } = await import("@google/generative-ai");
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
const model = genAI.getGenerativeModel({
@ -1284,7 +1291,7 @@ Return JSON with this exact shape:
Rules:
- Generate 3-5 phases (pre-launch, launch, amplification, follow-up, etc.)
- Cadence = number of posts per platform for that phase
- Platforms should be realistic for the brief content
- Platforms should be realistic for the brief content${platformConstraint}
- Use today's date (${new Date().toISOString().split('T')[0]}) as reference for dates`;
try {
@ -1292,6 +1299,25 @@ Rules:
const text = result.response.text();
const parsed = JSON.parse(text);
// Safety net: filter platforms and cadences to only selected platforms
if (selectedPlatforms && parsed.extractedBrief) {
parsed.extractedBrief.platforms = (parsed.extractedBrief.platforms || [])
.filter((p: string) => selectedPlatforms.includes(p));
if (parsed.extractedBrief.platforms.length === 0) {
parsed.extractedBrief.platforms = selectedPlatforms;
}
}
if (selectedPlatforms && parsed.structure?.phases) {
for (const phase of parsed.structure.phases) {
phase.platforms = (phase.platforms || []).filter((p: string) => selectedPlatforms.includes(p));
if (phase.cadence) {
for (const key of Object.keys(phase.cadence)) {
if (!selectedPlatforms.includes(key)) delete phase.cadence[key];
}
}
}
}
_syncServer!.changeDoc<SocialsDoc>(docId, `wizard ${id} → structure`, (d) => {
const w = d.campaignWizards?.[id];
if (!w) return;
@ -1325,10 +1351,19 @@ routes.post("/api/campaign/wizard/:id/content", async (c) => {
return c.json({ error: "Must complete structure step first" }, 400);
}
const body = await c.req.json().catch(() => ({}));
const phaseInstructions: Record<string, string> = body.phaseInstructions || {};
const brief = wizard.extractedBrief;
const structure = wizard.structure;
const selectedPlatforms = brief.platforms.length > 0 ? brief.platforms : ["x", "linkedin", "instagram", "newsletter"];
// Build phase-specific instruction hints
const phaseHints = structure.phases.map((p, i) => {
const hint = phaseInstructions[String(i)];
return hint ? `- Phase ${i + 1} (${p.label}): ${hint}` : '';
}).filter(Boolean).join('\n');
const { GoogleGenerativeAI } = await import("@google/generative-ai");
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
const model = genAI.getGenerativeModel({
@ -1391,7 +1426,7 @@ Rules:
- For newsletter, include emailSubject + emailHtml with inline CSS
- scheduledAt dates should spread across the phase day ranges, during working hours (9am-5pm)
- Content must reference specific details from the brief key messages
- Respect each platform's character limits`;
- Respect each platform's character limits${phaseHints ? `\n\nPhase-specific instructions from the user:\n${phaseHints}` : ''}`;
try {
const result = await model.generateContent(prompt);

View File

@ -24,6 +24,7 @@
"@google/genai": "^1.43.0",
"@google/generative-ai": "^0.24.1",
"@lit/reactive-element": "^2.0.4",
"@modelcontextprotocol/sdk": "^1.27.1",
"@noble/curves": "^1.8.0",
"@noble/hashes": "^1.7.0",
"@openfort/openfort-node": "^0.7.0",

324
server/cad-orchestrator.ts Normal file
View File

@ -0,0 +1,324 @@
/**
* CAD Orchestrator LLM-driven MCP tool calling for KiCad and FreeCAD
*
* Converts MCP tool schemas to Gemini function declarations, runs an agentic
* loop where Gemini Flash plans and executes real MCP tool sequences, then
* assembles artifacts (SVGs, exports) for the frontend.
*/
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
// ── MCP → Gemini schema conversion ──
/** Strip keys that Gemini function-calling schema rejects */
function cleanSchema(obj: any): any {
if (obj === null || obj === undefined) return obj;
if (Array.isArray(obj)) return obj.map(cleanSchema);
if (typeof obj !== "object") return obj;
const out: any = {};
for (const [k, v] of Object.entries(obj)) {
// Gemini rejects these in FunctionDeclaration parameters
if (["default", "additionalProperties", "$schema", "examples", "title"].includes(k)) continue;
out[k] = cleanSchema(v);
}
return out;
}
/** Convert a single MCP Tool to a Gemini FunctionDeclaration */
export function mcpToolToGeminiFn(tool: Tool): any {
const params = tool.inputSchema ? cleanSchema(tool.inputSchema) : { type: "object", properties: {} };
// Gemini requires type:"OBJECT" (uppercase) in some SDK versions, but @google/generative-ai handles lowercase
return {
name: tool.name,
description: tool.description || tool.name,
parameters: params,
};
}
// ── Extract text from MCP CallToolResult ──
export function extractMcpText(result: any): string {
const content = result?.content;
if (!Array.isArray(content)) return JSON.stringify(result);
return content
.filter((c: any) => c.type === "text")
.map((c: any) => c.text || "")
.join("\n");
}
// ── Agentic loop ──
export interface ToolCallLogEntry {
tool: string;
args: Record<string, any>;
result: string;
}
export interface OrchestrationResult {
artifacts: Map<string, string>;
finalMessage: string;
toolCallLog: ToolCallLogEntry[];
}
export async function runCadAgentLoop(
client: Client,
systemPrompt: string,
userPrompt: string,
geminiApiKey: string,
maxTurns = 8,
): Promise<OrchestrationResult> {
// 1. Fetch real tool schemas from MCP server
const { tools } = await client.listTools();
const functionDeclarations = tools.map(mcpToolToGeminiFn);
// 2. Initialize Gemini Flash with function calling
const { GoogleGenerativeAI } = await import("@google/generative-ai");
const genAI = new GoogleGenerativeAI(geminiApiKey);
const model = genAI.getGenerativeModel({
model: "gemini-2.5-flash",
systemInstruction: systemPrompt,
generationConfig: { temperature: 0.2 },
tools: [{ functionDeclarations }],
});
// 3. Agentic loop
const artifacts = new Map<string, string>();
const toolCallLog: ToolCallLogEntry[] = [];
const deadline = Date.now() + 60_000;
let contents: any[] = [
{ role: "user", parts: [{ text: userPrompt }] },
];
for (let turn = 0; turn < maxTurns; turn++) {
if (Date.now() > deadline) {
console.warn("[cad-orchestrator] 60s deadline reached");
break;
}
const result = await model.generateContent({ contents });
const candidate = result.response.candidates?.[0];
if (!candidate) break;
const parts = candidate.content?.parts || [];
const fnCalls = parts.filter((p: any) => p.functionCall);
if (fnCalls.length === 0) {
// Done — extract final text
const finalText = parts
.filter((p: any) => p.text)
.map((p: any) => p.text)
.join("\n");
return { artifacts, finalMessage: finalText || "Design complete.", toolCallLog };
}
// Execute each function call on MCP
const fnResponseParts: any[] = [];
for (const part of fnCalls) {
const fc = part.functionCall!;
console.log(`[cad-orchestrator] Turn ${turn + 1}: ${fc.name}(${JSON.stringify(fc.args).slice(0, 200)})`);
let mcpResultText: string;
try {
const mcpResult = await client.callTool({
name: fc.name,
arguments: (fc.args || {}) as Record<string, unknown>,
});
mcpResultText = extractMcpText(mcpResult);
} catch (err) {
mcpResultText = `Error: ${err instanceof Error ? err.message : String(err)}`;
}
toolCallLog.push({ tool: fc.name, args: fc.args || {}, result: mcpResultText });
artifacts.set(fc.name, mcpResultText);
fnResponseParts.push({
functionResponse: {
name: fc.name,
response: { result: mcpResultText },
},
});
}
// Feed results back for next turn
contents.push({ role: "model", parts });
contents.push({ role: "user", parts: fnResponseParts });
}
return {
artifacts,
finalMessage: "Design generation completed (max turns reached).",
toolCallLog,
};
}
// ── Result assemblers ──
export interface KicadResult {
schematicSvg: string | null;
boardSvg: string | null;
gerberUrl: string | null;
bomUrl: string | null;
pdfUrl: string | null;
drcResults: { violations: string[] } | null;
summary: string;
toolCallLog: ToolCallLogEntry[];
}
export function assembleKicadResult(orch: OrchestrationResult): KicadResult {
let schematicSvg: string | null = null;
let boardSvg: string | null = null;
let gerberUrl: string | null = null;
let bomUrl: string | null = null;
let pdfUrl: string | null = null;
let drcResults: { violations: string[] } | null = null;
for (const entry of orch.toolCallLog) {
try {
const parsed = JSON.parse(entry.result);
switch (entry.tool) {
case "export_svg":
// Could be schematic or board SVG — check args or content
if (entry.args.type === "board" || entry.args.board) {
boardSvg = parsed.svg_path || parsed.path || parsed.url || null;
} else {
schematicSvg = parsed.svg_path || parsed.path || parsed.url || null;
}
break;
case "run_drc":
drcResults = {
violations: parsed.violations || parsed.errors || [],
};
break;
case "export_gerber":
gerberUrl = parsed.gerber_path || parsed.path || parsed.url || null;
break;
case "export_bom":
bomUrl = parsed.bom_path || parsed.path || parsed.url || null;
break;
case "export_pdf":
pdfUrl = parsed.pdf_path || parsed.path || parsed.url || null;
break;
}
} catch {
// Non-JSON results are fine (intermediate steps)
}
}
return {
schematicSvg,
boardSvg,
gerberUrl,
bomUrl,
pdfUrl,
drcResults,
summary: orch.finalMessage,
toolCallLog: orch.toolCallLog,
};
}
export interface FreecadResult {
previewUrl: string | null;
stepUrl: string | null;
stlUrl: string | null;
summary: string;
toolCallLog: ToolCallLogEntry[];
}
export function assembleFreecadResult(orch: OrchestrationResult): FreecadResult {
let previewUrl: string | null = null;
let stepUrl: string | null = null;
let stlUrl: string | null = null;
for (const entry of orch.toolCallLog) {
try {
const parsed = JSON.parse(entry.result);
// FreeCAD exports via execute_python_script — look for file paths in results
if (entry.tool === "execute_python_script" || entry.tool === "execute_script") {
const text = entry.result.toLowerCase();
if (text.includes(".step") || text.includes(".stp")) {
stepUrl = parsed.path || parsed.file_path || extractPathFromText(entry.result, [".step", ".stp"]);
}
if (text.includes(".stl")) {
stlUrl = parsed.path || parsed.file_path || extractPathFromText(entry.result, [".stl"]);
}
}
// save_document may also produce a path
if (entry.tool === "save_document") {
const path = parsed.path || parsed.file_path || null;
if (path && (path.endsWith(".FCStd") || path.endsWith(".fcstd"))) {
// Not directly servable, but note it
}
}
} catch {
// Try extracting paths from raw text
stepUrl = stepUrl || extractPathFromText(entry.result, [".step", ".stp"]);
stlUrl = stlUrl || extractPathFromText(entry.result, [".stl"]);
}
}
return {
previewUrl,
stepUrl,
stlUrl,
summary: orch.finalMessage,
toolCallLog: orch.toolCallLog,
};
}
/** Extract a file path ending with one of the given extensions from text */
function extractPathFromText(text: string, extensions: string[]): string | null {
for (const ext of extensions) {
const regex = new RegExp(`(/[\\w./-]+${ext.replace(".", "\\.")})`, "i");
const match = text.match(regex);
if (match) return match[1];
}
return null;
}
// ── System prompts ──
export const KICAD_SYSTEM_PROMPT = `You are a KiCad PCB design assistant. You have access to KiCad MCP tools to create real PCB designs.
Follow this workflow:
1. create_project Create a new KiCad project in /tmp/kicad-gen-<timestamp>/
2. search_symbols Find component symbols in KiCad libraries (e.g. ESP32, BME280, capacitors, resistors)
3. add_schematic_component Place each component on the schematic
4. add_schematic_net_label Add net labels for connections
5. add_schematic_connection Wire components together
6. generate_netlist Generate the netlist from schematic
7. place_component Place footprints on the board
8. route_trace Route traces between pads
9. export_svg Export schematic SVG (type: "schematic") and board SVG (type: "board")
10. run_drc Run Design Rule Check
11. export_gerber, export_bom, export_pdf Generate manufacturing outputs
Important:
- Use /tmp/kicad-gen-${Date.now()}/ as the project directory
- Search for real symbols before placing components
- Add decoupling capacitors and pull-up resistors as needed
- Set reasonable board outline dimensions
- After placing components, route critical traces
- Always run DRC before exporting
- If a tool call fails, try an alternative approach rather than repeating the same call`;
export const FREECAD_SYSTEM_PROMPT = `You are a FreeCAD parametric CAD assistant. You have access to FreeCAD MCP tools to create real 3D models.
Follow this workflow:
1. execute_python_script Create output directory: import os; os.makedirs("/tmp/freecad-gen-<timestamp>", exist_ok=True)
2. Create base geometry using create_box, create_cylinder, or create_sphere
3. Use boolean_operation (union, cut, intersection) to combine shapes
4. list_objects to verify the model state
5. save_document to save the FreeCAD file
6. execute_python_script to export STEP: Part.export([obj], "/tmp/freecad-gen-<id>/model.step")
7. execute_python_script to export STL: Mesh.export([obj], "/tmp/freecad-gen-<id>/model.stl")
Important:
- Use /tmp/freecad-gen-${Date.now()}/ as the working directory
- For hollow objects, create the outer shell then cut the inner volume
- For complex shapes, build up from primitives with boolean operations
- Wall thickness should be at least 1mm for 3D printing
- Always export both STEP (for CAD) and STL (for 3D printing)
- If a tool call fails, try an alternative approach`;

View File

@ -1144,6 +1144,20 @@ async function process3DGenJob(job: Gen3DJob) {
// ── Image helpers ──
/** Copy a file from a tmp path to the served generated directory → return server-relative URL */
async function copyToServed(srcPath: string): Promise<string | null> {
try {
const srcFile = Bun.file(srcPath);
if (!(await srcFile.exists())) return null;
const basename = srcPath.split("/").pop() || `file-${Date.now()}`;
const dir = resolve(process.env.FILES_DIR || "./data/files", "generated");
await Bun.write(resolve(dir, basename), srcFile);
return `/data/files/generated/${basename}`;
} catch {
return null;
}
}
/** Read a /data/files/generated/... path from disk → base64 */
async function readFileAsBase64(serverPath: string): Promise<string> {
const filename = serverPath.split("/").pop();
@ -1606,6 +1620,19 @@ app.get("/api/3d-gen/:jobId", async (c) => {
// Blender 3D generation via LLM + RunPod
const RUNPOD_API_KEY = process.env.RUNPOD_API_KEY || "";
app.get("/api/blender-gen/health", async (c) => {
const issues: string[] = [];
if (!RUNPOD_API_KEY) issues.push("RUNPOD_API_KEY not configured");
const OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434";
try {
const res = await fetch(`${OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(3000) });
if (!res.ok) issues.push("Ollama not responding");
} catch {
issues.push("Ollama unreachable");
}
return c.json({ available: issues.length === 0, issues });
});
app.post("/api/blender-gen", async (c) => {
if (!RUNPOD_API_KEY) return c.json({ error: "RUNPOD_API_KEY not configured" }, 503);
@ -1675,8 +1702,89 @@ app.post("/api/blender-gen", async (c) => {
}
});
// KiCAD PCB design — REST-to-MCP bridge
const KICAD_MCP_URL = process.env.KICAD_MCP_URL || "http://localhost:3001";
// KiCAD PCB design — MCP stdio bridge
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { runCadAgentLoop, assembleKicadResult, assembleFreecadResult, KICAD_SYSTEM_PROMPT, FREECAD_SYSTEM_PROMPT } from "./cad-orchestrator";
const KICAD_MCP_PATH = process.env.KICAD_MCP_PATH || "/home/jeffe/KiCAD-MCP-Server/dist/index.js";
let kicadClient: Client | null = null;
async function getKicadClient(): Promise<Client> {
if (kicadClient) return kicadClient;
const transport = new StdioClientTransport({
command: "node",
args: [KICAD_MCP_PATH],
});
const client = new Client({ name: "rspace-kicad-bridge", version: "1.0.0" });
transport.onclose = () => { kicadClient = null; };
transport.onerror = () => { kicadClient = null; };
await client.connect(transport);
kicadClient = client;
return client;
}
app.get("/api/kicad/health", async (c) => {
try {
const client = await getKicadClient();
const tools = await client.listTools();
return c.json({ available: true, tools: tools.tools.length });
} catch (e) {
return c.json({ available: false, error: e instanceof Error ? e.message : "Connection failed" });
}
});
// KiCAD AI-orchestrated design generation
app.post("/api/kicad/generate", async (c) => {
if (!GEMINI_API_KEY) return c.json({ error: "GEMINI_API_KEY not configured" }, 503);
const { prompt, components } = await c.req.json();
if (!prompt) return c.json({ error: "prompt required" }, 400);
const enrichedPrompt = components?.length
? `${prompt}\n\nKey components to use: ${components.join(", ")}`
: prompt;
try {
const client = await getKicadClient();
const orch = await runCadAgentLoop(client, KICAD_SYSTEM_PROMPT, enrichedPrompt, GEMINI_API_KEY);
const result = assembleKicadResult(orch);
// Copy generated files to served directory
const filesToCopy = [
{ path: result.schematicSvg, key: "schematicSvg" },
{ path: result.boardSvg, key: "boardSvg" },
{ path: result.gerberUrl, key: "gerberUrl" },
{ path: result.bomUrl, key: "bomUrl" },
{ path: result.pdfUrl, key: "pdfUrl" },
];
for (const { path, key } of filesToCopy) {
if (path && path.startsWith("/tmp/")) {
const served = await copyToServed(path);
if (served) (result as any)[key] = served;
}
}
return c.json({
schematic_svg: result.schematicSvg,
board_svg: result.boardSvg,
gerber_url: result.gerberUrl,
bom_url: result.bomUrl,
pdf_url: result.pdfUrl,
drc: result.drcResults,
summary: result.summary,
});
} catch (e) {
console.error("[kicad/generate] error:", e);
kicadClient = null;
return c.json({ error: e instanceof Error ? e.message : "KiCAD generation failed" }, 502);
}
});
app.post("/api/kicad/:action", async (c) => {
const action = c.req.param("action");
@ -1693,38 +1801,102 @@ app.post("/api/kicad/:action", async (c) => {
}
try {
const mcpRes = await fetch(`${KICAD_MCP_URL}/call-tool`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: action,
arguments: body,
}),
});
const client = await getKicadClient();
const result = await client.callTool({ name: action, arguments: body });
if (!mcpRes.ok) {
const err = await mcpRes.text();
console.error(`[kicad/${action}] MCP error:`, err);
return c.json({ error: `KiCAD action failed: ${action}` }, 502);
// Extract text content and parse as JSON if possible
const content = result.content as Array<{ type: string; text?: string }>;
const textContent = content
?.filter((c) => c.type === "text")
.map((c) => c.text)
.join("\n");
try {
return c.json(JSON.parse(textContent || "{}"));
} catch {
return c.json({ result: textContent });
}
const data = await mcpRes.json();
return c.json(data);
} catch (e) {
console.error(`[kicad/${action}] Connection error:`, e);
console.error(`[kicad/${action}] MCP error:`, e);
kicadClient = null;
return c.json({ error: "KiCAD MCP server not available" }, 503);
}
});
// FreeCAD parametric CAD — REST-to-MCP bridge
const FREECAD_MCP_URL = process.env.FREECAD_MCP_URL || "http://localhost:3002";
// FreeCAD parametric CAD — MCP stdio bridge
const FREECAD_MCP_PATH = process.env.FREECAD_MCP_PATH || "/home/jeffe/freecad-mcp-server/build/index.js";
let freecadClient: Client | null = null;
async function getFreecadClient(): Promise<Client> {
if (freecadClient) return freecadClient;
const transport = new StdioClientTransport({
command: "node",
args: [FREECAD_MCP_PATH],
});
const client = new Client({ name: "rspace-freecad-bridge", version: "1.0.0" });
transport.onclose = () => { freecadClient = null; };
transport.onerror = () => { freecadClient = null; };
await client.connect(transport);
freecadClient = client;
return client;
}
app.get("/api/freecad/health", async (c) => {
try {
const client = await getFreecadClient();
const tools = await client.listTools();
return c.json({ available: true, tools: tools.tools.length });
} catch (e) {
return c.json({ available: false, error: e instanceof Error ? e.message : "Connection failed" });
}
});
// FreeCAD AI-orchestrated CAD generation
app.post("/api/freecad/generate", async (c) => {
if (!GEMINI_API_KEY) return c.json({ error: "GEMINI_API_KEY not configured" }, 503);
const { prompt } = await c.req.json();
if (!prompt) return c.json({ error: "prompt required" }, 400);
try {
const client = await getFreecadClient();
const orch = await runCadAgentLoop(client, FREECAD_SYSTEM_PROMPT, prompt, GEMINI_API_KEY);
const result = assembleFreecadResult(orch);
// Copy generated files to served directory
for (const key of ["stepUrl", "stlUrl"] as const) {
const path = result[key];
if (path && path.startsWith("/tmp/")) {
const served = await copyToServed(path);
if (served) (result as any)[key] = served;
}
}
return c.json({
preview_url: result.previewUrl,
step_url: result.stepUrl,
stl_url: result.stlUrl,
summary: result.summary,
});
} catch (e) {
console.error("[freecad/generate] error:", e);
freecadClient = null;
return c.json({ error: e instanceof Error ? e.message : "FreeCAD generation failed" }, 502);
}
});
app.post("/api/freecad/:action", async (c) => {
const action = c.req.param("action");
const body = await c.req.json();
const validActions = [
"generate", "export_step", "export_stl", "update_parameters",
"generate", "create_box", "create_cylinder", "create_sphere",
"boolean_operation", "save_document", "list_objects", "execute_script",
"export_step", "export_stl", "update_parameters",
];
if (!validActions.includes(action)) {
@ -1732,25 +1904,23 @@ app.post("/api/freecad/:action", async (c) => {
}
try {
const mcpRes = await fetch(`${FREECAD_MCP_URL}/call-tool`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: action,
arguments: body,
}),
});
const client = await getFreecadClient();
const result = await client.callTool({ name: action, arguments: body });
if (!mcpRes.ok) {
const err = await mcpRes.text();
console.error(`[freecad/${action}] MCP error:`, err);
return c.json({ error: `FreeCAD action failed: ${action}` }, 502);
const fcContent = result.content as Array<{ type: string; text?: string }>;
const textContent = fcContent
?.filter((c) => c.type === "text")
.map((c) => c.text)
.join("\n");
try {
return c.json(JSON.parse(textContent || "{}"));
} catch {
return c.json({ result: textContent });
}
const data = await mcpRes.json();
return c.json(data);
} catch (e) {
console.error(`[freecad/${action}] Connection error:`, e);
console.error(`[freecad/${action}] MCP error:`, e);
freecadClient = null;
return c.json({ error: "FreeCAD MCP server not available" }, 503);
}
});