7 Commits

Author SHA1 Message Date
c0543f44ce refactor: replace personified workflow labels
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-06-03 19:04:05 +05:30
488fc1aeca growqr-backend: route service sessions to workflows dashboard (#4)
## Summary

  This PR updates backend service-session orchestration so
  interview, roleplay, resume, and matchmaking flows integrate with
  the shipped `workflows--dashboard` frontend instead of the old
  GrowQR app demo UI.

  ## Changes

  - Added `WORKFLOWS_DASHBOARD_URL` config with fallback to
  `FRONTEND_ORIGIN`.
  - Updated service session URL generation to point to dashboard-
  owned routes:
    - `/v2/service-sessions/interview`
    - `/v2/service-sessions/roleplay`
    - `/v2/service-sessions/resume`
  - Preserved session query params, including `session_id`, service
  id, and user-facing metadata.
  - Updated chat route handling so returned service cards include
  dashboard session URLs.
  - Added/updated matchmaking service integration wiring.
  - Documented the dashboard-owned session route behavior in

  ## Validation

  - `npm run build` passed for `growqr-backend`.

Reviewed-on: puter/growqr-backend#4
Co-authored-by: NinjasPyajamas <divyansh242805@gmail.com>
Co-committed-by: NinjasPyajamas <divyansh242805@gmail.com>
2026-06-01 16:19:12 +00:00
-Puter
3f51ed5f0f updates source code (src) (3 files) 2026-06-01 18:03:18 +05:30
-Puter
22960be344 update source code (3 files) 2026-06-01 18:03:18 +05:30
-Puter
f085783a35 updates source code (drizzle) (1 file) 2026-06-01 18:03:18 +05:30
-Puter
7e6d32def5 updates configuration (3 files) 2026-06-01 18:03:18 +05:30
9ddbb4a8e5 feat: wire real service agents into chat with LLM tool dispatch + Rivet proxy fix (#3)
# Wire All 4 Microservice Agents Into Chat

Wires all 4 microservice-backed agents into the chat so the LLM can call real services and return session URLs.

---

## Changes

### New

* `src/routes/chat.ts`

  * Added a direct HTTP chat endpoint.
  * When the LLM calls:

    * `start_interview_session`
    * `analyze_resume`
    * `start_roleplay_session`
    * `compute_qscore`
  * The route executes real service probes and returns live session URLs.

---

### Fixed

* `src/index.ts`

  * Rivet proxy now forwards requests to the engine at `localhost:6420`
    instead of using `registry.handler()`.
  * Prevents the:

    ```txt
    Runtime already started as runner
    ```

    conflict.

* `src/actors/user-actor.ts`

  * `receiveMessage()` now returns:

    ```ts
    {
      reply,
      sessions: []
    }
    ```
  * Includes per-module session URLs in responses.

* `docker-compose.yml`

  * Fixed:

    * Gitea health check port
    * Port mapping
    * `A2A_ALLOWED_KEY` default value

* `src/config.ts`

  * Added:

    ```ts
    resumeServiceUrl
    ```
  * Configured to use port `8002`.

---

### Rewritten

* `prompts/system.txt`

  * Reworked into a conversational step-by-step flow.
  * Added explicit rule:

    > CALL THE TOOL IMMEDIATELY

---

### Updated

* `agents/*.md` (6 files)

  * Updated:

    * Domain descriptions
    * Trigger phrases
    * Agent boundaries

---

## Verified

| Agent         | Service                  | Result                      |
| ------------- | ------------------------ | --------------------------- |
| Resume (Mira) | `resume-builder:8002`    | Real analysis               |
| Sara          | `interview-service:8007` | Real Gemini session + URL   |
| Emily         | `roleplay-service:8008`  | Real roleplay session + URL |
| Quinn         | `qscore-service:8000`    | Real Q-Score (~84)          |

---

## Outcome

The chat system can now:

* Trigger real backend agent services directly from LLM tool calls
* Return live session URLs
* Maintain structured multi-agent responses
* Avoid Rivet runtime conflicts
* Support end-to-end conversational workflows across all 4 agents

Reviewed-on: puter/growqr-backend#3
Co-authored-by: NinjasPyajamas <divyansh242805@gmail.com>
Co-committed-by: NinjasPyajamas <divyansh242805@gmail.com>
2026-06-01 09:26:19 +00:00
48 changed files with 3805 additions and 7534 deletions

View File

@@ -3,9 +3,9 @@ LOG_LEVEL=info
NODE_ENV=development
# Postgres (started by docker-compose; defaults match the compose service)
DATABASE_URL=postgres://growqr:growqr@localhost:5432/growqr
DATABASE_URL=***************************************/growqr
POSTGRES_USER=growqr
POSTGRES_PASSWORD=growqr
POSTGRES_PASSWORD=******
POSTGRES_DB=growqr
# Clerk auth — get from dashboard.clerk.com → API Keys
@@ -18,12 +18,23 @@ LLM_PROVIDER=opencode
LLM_BASE_URL=https://opencode.ai/zen/v1
LLM_MODEL=kimi-k2.6
GROW_AGENT_MODEL=kimi-k2.6
SUB_AGENT_MODEL=kimi-k2.6
MAX_AGENT_TOKENS=4096
# Shared secret for actor → backend service calls (rotate in prod)
SERVICE_TOKEN=dev-service-token-REPLACE_ME
A2A_ALLOWED_KEY=dev-a2a-key
A2A_ALLOWED_KEY=***********
# ── Central Gitea (shared org-wide, changes.md §2A) ──
GITEA_URL=http://127.0.0.1:3001
GITEA_ADMIN_USER=growqr-admin
GITEA_ADMIN_PASSWORD=growqr-admin-dev
GITEA_ADMIN_TOKEN=
GITEA_ORG_NAME=growqr
# ── Version tracking (changes.md §9) ──
OPENCODE_IMAGE_VERSION=1.0.0
MIGRATION_VERSION=1
PROMPT_VERSION=1
# Rivet Kit engine (self-hosted in docker-compose)
RIVET_ENDPOINT=http://localhost:6420
@@ -34,9 +45,8 @@ INTERVIEW_SERVICE_URL=http://localhost:8007
ROLEPLAY_SERVICE_URL=http://localhost:8008
QSCORE_SERVICE_URL=http://localhost:8000
# Per-user container images
GITEA_IMAGE=gitea/gitea:1.22
OPENCODE_IMAGE=ghcr.io/sst/opencode:latest
# Per-user OpenCode container image (shared, changes.md §3)
OPENCODE_IMAGE=ghcr.io/anomalyco/opencode:latest
# Host where spawned containers expose their ports.
# - localhost in dev
@@ -46,7 +56,7 @@ USER_CONTAINER_HOST=127.0.0.1
# Workspace root on the host. Each user gets a subdir.
USER_DATA_ROOT=./.data/users
# Port range allocated to spawned per-user containers
# Port range allocated to per-user OpenCode containers (Gitea is central)
USER_PORT_RANGE_START=20000
USER_PORT_RANGE_END=29999

View File

@@ -16,5 +16,13 @@ ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
COPY drizzle/ ./drizzle/
# ── Build-time prompt loading (changes.md §3) ──
# Prompts and agent definitions are copied into the image so they are
# embedded at build time. To update: edit files → rebuild image → rollout.
COPY prompts/ ./prompts/
COPY agents/ ./agents/
EXPOSE 4000
CMD ["node", "dist/index.js"]

33
agents/emily.md Normal file
View File

@@ -0,0 +1,33 @@
---
id: emily
name: Mock Roleplay
role: Roleplay practice
service: roleplay-service
tools:
- start_roleplay_session
---
## Domain
Mock Roleplay runs realistic workplace scenarios to help users practice conversations, negotiations, and difficult situations. It plays different personas convincingly and provides feedback.
## When to use this agent (trigger phrases)
Use `start_roleplay_session` when the user:
- Wants to negotiate: "salary negotiation", "negotiate offer", "counter offer", "compensation", "equity discussion", "signing bonus", "benefits negotiation"
- Has a difficult conversation: "asking for a raise", "promotion conversation", "talk to my manager", "difficult conversation with boss"
- Is leaving a job: "resignation", "quit my job", "put in notice", "two weeks notice", "leaving my company"
- Wants to practice soft skills: "roleplay", "practice conversation", "rehearse", "act out"
- Has networking needs: "coffee chat", "informational interview", "networking event", "cold outreach"
- Has stakeholder scenarios: "client meeting", "stakeholder presentation", "pitch to executives", "cross-functional"
- Has conflict situations: "conflict with coworker", "team disagreement", "difficult colleague", "managing up"
- Has performance situations: "performance review", "self-review", "annual review", "how to present my work"
- Needs general conversation practice: "how to say", "what should I tell", "how do I bring up", "need to tell my"
## What Mock Roleplay NEVER does
- Interview practice or technical questions → Mock Interview
- Resume writing → Resume Building
- Job searching → Job Search Agent
- Q Score computation → Q Score
- Career coaching beyond roleplay → general chat
## How it works
Calls `POST /api/v1/roleplays/configure` on the roleplay-service with user_id, persona_id, roleplay_type, brief, difficulty, and qscore_context. Creates a real Gemini Live-powered roleplay session. Supports types: sales, customer_success, support, custom. Returns session_id for the user to start practicing.

35
agents/job-apply.md Normal file
View File

@@ -0,0 +1,35 @@
---
id: job-apply
name: Job Apply Agent
role: Application Operator
tools:
- prepare_application
- track_submission
- schedule_followup
---
## Domain
The **Job Apply Agent** manages the user's job application process end-to-end. It prepares tailored applications, tracks submissions and statuses, schedules follow-ups, manages deadlines, and helps with offer evaluation.
## When to use this agent (trigger phrases)
Use `prepare_application`, `track_submission`, or `schedule_followup` when the user:
- Is applying: "apply to jobs", "submit application", "send my application", "apply for [role]", "application for"
- Wants cover letters: "cover letter", "write cover letter", "application letter", "customize cover letter for"
- Needs tracking: "track my applications", "application status", "where did I apply", "application pipeline"
- Has follow-ups: "follow up on application", "check application status", "after applying", "no response from"
- Has multiple offers: "multiple offers", "offer comparison", "which offer should I take", "evaluate offers"
- Needs offer evaluation: "offer letter review", "total compensation", "TC comparison", "offer negotiation prep"
- Has deadline pressure: "application deadline", "apply before", "closing date", "expiring offer"
- Wants organization: "organize my job search", "application tracker", "job hunt organization"
- Needs references: "reference list", "who should I use as reference", "reference check prep"
- Has portfolio needs: "portfolio for jobs", "work samples", "GitHub for applications", "project showcase"
## What this agent NEVER does
- Resume content optimization → Resume Building
- Job discovery → Job Search Agent
- Interview practice → Mock Interview
- Roleplay → Mock Roleplay
- Q Score → Q Score
## How it works
Local workflow agent managed by Rivet. Takes the shortlist from Job Search Agent and the tailored resume from Resume Building, then prepares complete application packages including customized cover letters, tracks submission status, and manages follow-up scheduling.

36
agents/job-search.md Normal file
View File

@@ -0,0 +1,36 @@
---
id: job-search
name: Job Search Agent
role: Opportunity Scout
service: matchmaking-service
tools:
- search_jobs
- rank_opportunities
- prepare_shortlist
---
## Domain
The **Job Search Agent** discovers and evaluates job opportunities matching the user's skills, experience, and preferences. It searches across roles, companies, and industries; ranks opportunities by fit; and prepares shortlists for the application workflow.
## When to use this agent (trigger phrases)
Use `search_jobs`, `rank_opportunities`, or `prepare_shortlist` when the user:
- Is actively looking: "find jobs", "job search", "looking for work", "job hunting", "on the market", "searching for roles"
- Wants matching: "what jobs match my skills", "roles that fit me", "jobs for my background", "positions for"
- Has role preferences: "[role] jobs", "backend engineer positions", "product manager roles", "data scientist openings"
- Has company interests: "who's hiring", "companies hiring", "startups hiring", "FAANG jobs", "tech companies"
- Has location preferences: "remote jobs", "work from home", "hybrid jobs", "jobs in [city]", "relocation"
- Has experience level: "entry level jobs", "senior positions", "junior roles", "[N] years experience jobs"
- Wants market context: "job market trends", "in-demand skills", "hot jobs", "salary ranges for", "industry outlook"
- Is unemployed/transitioning: "I need a job", "help me find work", "laid off", "between jobs", "looking after graduation"
- Wants company research: "should I apply to [company]", "company culture", "best companies for"
- Needs networking: "recruiter outreach", "referral strategy", "networking for jobs", "headhunter"
## What this agent NEVER does
- Resume optimization → Resume Building
- Interview practice → Mock Interview
- Roleplay → Mock Roleplay
- Q Score → Q Score
- Application tracking → Job Apply Agent
## How it works
Local workflow agent managed by Rivet. Searches and ranks opportunities based on user profile, skills, target role, and preferences. Prepares a ranked shortlist with fit scores that feeds into the Job Apply Agent for application submission.

31
agents/qscore.md Normal file
View File

@@ -0,0 +1,31 @@
---
id: qscore
name: Q Score
role: Readiness scoring
service: qscore-service
tools:
- compute_qscore
- ingest_signals
---
## Domain
Q Score computes and explains the user's readiness score based on resume strength, interview readiness, role alignment, engagement, skills, and goal clarity. It tracks growth over time.
## When to use this agent (trigger phrases)
Use `ingest_signals` + `compute_qscore` when the user:
- Wants their readiness score: "what's my q-score", "how ready am I", "readiness score", "calculate my score", "check my progress"
- Completed a resume update and wants to see impact: "I updated my resume, check my score", "after optimizing resume"
- Completed interview practice and wants assessment: "after interview practice", "how did practice affect my score"
- Completed roleplay and wants evaluation: "after roleplay", "roleplay feedback score"
- Wants overall career health check: "career readiness", "job readiness", "how prepared am I", "am I ready to apply"
- Wants to track growth: "score trend", "progress tracking", "improvement over time", "how much have I improved"
- Mentions metrics: "quantify my readiness", "measure my growth", "score me", "rate my profile"
## What Q Score NEVER does
- Interview practice → Mock Interview
- Roleplay scenarios → Mock Roleplay
- Resume editing → Resume Building
- Job searching → Job Search Agent
## How it works
Ingests signals (resume.uploaded, resume.ats_compatibility, engagement.features_used, goals.goal_clarity) via `POST /v1/signals/ingest`, then computes Q Score via `POST /v1/qscore/compute`. Returns a score from 0-100 with breakdown across 5 pillars. If the formula store is unavailable, it returns an estimated score from signal averages rather than failing.

13
agents/resume.md Normal file
View File

@@ -0,0 +1,13 @@
---
id: resume
name: Resume Building
role: Resume building
service: resume-service
tools:
- build_resume
- review_resume
- tailor_resume
- analyze_resume
---
Analyzes, builds, and tailors resumes for specific roles. Backed by the resume-builder microservice. Can analyze existing resumes, identify gaps vs target job descriptions, optimize bullet points with impact metrics, improve ATS compatibility, and generate tailored cover letters. Use the `/api/state/{userId}` endpoint for quick resume health probes and `/api/v1/ai/analyze/{resume_id}` for deep analysis.

31
agents/sara.md Normal file
View File

@@ -0,0 +1,31 @@
---
id: sara
name: Mock Interview
role: Interview practice
service: interview-service
tools:
- start_interview_session
---
## Domain
Mock Interview handles job interview preparation and practice. Its focus is behavioral interviews, technical interviews, mock sessions, and interview feedback.
## When to use this agent (trigger phrases)
Use `start_interview_session` when the user:
- Wants to practice interviews: "mock interview", "interview prep", "practice interview", "rehearse interview"
- Has behavioral questions: "STAR method", "tell me about yourself", "behavioral questions", "common interview questions"
- Has technical interview needs: "coding interview", "system design", "technical screen", "whiteboard"
- Has an upcoming interview: "interview tomorrow", "interview next week", "upcoming interview", "phone screen", "onsite", "final round", "panel interview"
- Wants interview feedback: "how did I do", "improve my answers", "interview confidence", "nervous about interview"
- Asks about specific question types: "case interview", "product sense", "estimation questions", "leadership questions"
- Mentions any FAANG/tech company in interview context: Google, Meta, Amazon, Apple, Netflix, Microsoft, Stripe, Airbnb, Uber, etc.
## What Mock Interview NEVER does
- Resume writing or optimization → Resume Building
- Roleplay scenarios, negotiation, salary talk → Mock Roleplay
- Job searching or matching → Job Search Agent
- Q Score analysis → Q Score
- Career switching advice → general chat
## How it works
Calls `POST /api/v1/configure` on the interview-service with user_id, interview_type, duration, and target role. Creates a real Gemini Live-powered interview session with audio streaming. Returns a session_id that the user can open to start practicing.

962
bun.lock Normal file
View File

@@ -0,0 +1,962 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "growqr-backend",
"dependencies": {
"@clerk/backend": "^1.21.0",
"@hono/node-server": "^1.13.7",
"dockerode": "^4.0.7",
"dotenv": "^16.4.7",
"drizzle-orm": "^0.36.4",
"hono": "^4.6.14",
"pino": "^9.5.0",
"pino-pretty": "^13.0.0",
"postgres": "^3.4.5",
"rivetkit": "^2.2.1",
"zod": "^3.24.1",
},
"devDependencies": {
"@types/dockerode": "^3.3.32",
"@types/node": "^22.10.5",
"drizzle-kit": "^0.31.2",
"tsx": "^4.19.2",
"typescript": "^5.7.2",
},
},
},
"packages": {
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="],
"@asteasolutions/zod-to-openapi": ["@asteasolutions/zod-to-openapi@8.5.0", "", { "dependencies": { "openapi3-ts": "^4.1.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-SABbKiObg5dLRiTFnqiW1WWwGcg1BJfmHtT2asIBnBHg6Smy/Ms2KHc650+JI4Hw7lSkdiNebEGXpwoxfben8Q=="],
"@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="],
"@cbor-extract/cbor-extract-darwin-arm64": ["@cbor-extract/cbor-extract-darwin-arm64@2.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA=="],
"@cbor-extract/cbor-extract-darwin-x64": ["@cbor-extract/cbor-extract-darwin-x64@2.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q=="],
"@cbor-extract/cbor-extract-linux-arm": ["@cbor-extract/cbor-extract-linux-arm@2.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ=="],
"@cbor-extract/cbor-extract-linux-arm64": ["@cbor-extract/cbor-extract-linux-arm64@2.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg=="],
"@cbor-extract/cbor-extract-linux-x64": ["@cbor-extract/cbor-extract-linux-x64@2.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA=="],
"@cbor-extract/cbor-extract-win32-x64": ["@cbor-extract/cbor-extract-win32-x64@2.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA=="],
"@clerk/backend": ["@clerk/backend@1.34.0", "", { "dependencies": { "@clerk/shared": "^3.9.5", "@clerk/types": "^4.59.3", "cookie": "1.0.2", "snakecase-keys": "8.0.1", "tslib": "2.8.1" }, "peerDependencies": { "svix": "^1.62.0" }, "optionalPeers": ["svix"] }, "sha512-9rZ8hQJVpX5KX2bEpiuVXfpjhojQCiqCWADJDdCI0PCeKxn58Ep0JPYiIcczg4VKUc3a7jve9vXylykG2XajLQ=="],
"@clerk/shared": ["@clerk/shared@3.47.5", "", { "dependencies": { "csstype": "3.1.3", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", "std-env": "^3.9.0", "swr": "2.3.4" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" }, "optionalPeers": ["react-dom"] }, "sha512-rDVe73/VN2NZXhtrLRHshkUpQDrevAqDRxeXUl2M0IBEBkcl+VMHlV7fep53cVWo0b3gIqLk82pmmi+WoyF/xg=="],
"@clerk/types": ["@clerk/types@4.101.23", "", { "dependencies": { "@clerk/shared": "^3.47.5" } }, "sha512-t5ypYYDkT5TPaNIDjLnYk9GpkJgwNTBiS7h6FuUTjoySQtf7amNDS1A1eOu7NOcVpqiSeKg+0wzGxxcre00kMA=="],
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="],
"@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="],
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
"@hono/node-ws": ["@hono/node-ws@1.3.1", "", { "dependencies": { "ws": "^8.17.0" }, "peerDependencies": { "@hono/node-server": "^1.19.11", "hono": "^4.6.0" } }, "sha512-vo/MwCnpJAVHBkGzWjCJ28wF45fYHAfbPZcH2rodZODHtch2GHA94KtMfusmVycTUtsLAsaNsHhtY6P8X3RQsA=="],
"@hono/standard-validator": ["@hono/standard-validator@0.1.5", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="],
"@hono/zod-openapi": ["@hono/zod-openapi@1.4.0", "", { "dependencies": { "@asteasolutions/zod-to-openapi": "^8.5.0", "@hono/zod-validator": "^0.8.0", "openapi3-ts": "^4.5.0" }, "peerDependencies": { "hono": ">=4.10.0", "zod": "^4.0.0" } }, "sha512-AFchqR1N/NxfI4hUOSGI2/g8zLROxA1OE7Oh5JJFlTaGxhrdRyH+93gd0tIBpb0z8s9r8hUoNnaOBfHbdb4NMw=="],
"@hono/zod-validator": ["@hono/zod-validator@0.8.0", "", { "peerDependencies": { "hono": ">=4.10.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-5uS4S1/LKtZQYvD4BtpPUFkOv8d1wNxHHrChm26buMiEYc1FrHWvDUaKVBwkiVtvSExHSpLGDvcnpI2Copyj9w=="],
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="],
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="],
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.2", "", {}, "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw=="],
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="],
"@rivet-dev/agent-os-core": ["@rivet-dev/agent-os-core@0.1.1", "", { "dependencies": { "@rivet-dev/agent-os-posix": "0.1.0", "@rivet-dev/agent-os-python": "0.1.0", "@secure-exec/core": "^0.2.1", "@secure-exec/nodejs": "^0.2.1", "@secure-exec/v8": "^0.2.1", "croner": "^10.0.1", "long-timeout": "^0.1.1", "secure-exec": "^0.2.1" } }, "sha512-Uw5jr+gUXDY7TDUFqlypjGe1BD2KL9kTHPNo/f1iNS1R+l9IWuvT+FF/MXsLOEkc3fB06OPVu2ZvUuNwp9MpLQ=="],
"@rivet-dev/agent-os-posix": ["@rivet-dev/agent-os-posix@0.1.0", "", { "dependencies": { "@secure-exec/core": "^0.2.1" } }, "sha512-NIrI7cCb9x6jdmzRPPx7dAeXoTF/YCqf93ydEzYFA2zshIelLW9Rp5KtgP/2hM6fP0ly4+vVnOeavxJW0wYtcA=="],
"@rivet-dev/agent-os-python": ["@rivet-dev/agent-os-python@0.1.0", "", { "dependencies": { "@secure-exec/core": "^0.2.1", "pyodide": "^0.28.3" }, "peerDependencies": { "pyodide": ">=0.28.0" } }, "sha512-1tH1beMf1ceSpicQKwN/a6h+NmJrmfuT4GStiRDZmvN/UWfZhkxuy7HR5VPTQpE/feUZJ01FdtBS3Em/Qoxb2Q=="],
"@rivetkit/bare-ts": ["@rivetkit/bare-ts@0.6.2", "", {}, "sha512-3qndQUQXLdwafMEqfhz24hUtDPcsf1Bu3q52Kb8MqeH8JUh3h6R4HYW3ZJXiQsLcyYyFM68PuIwlLRlg1xDEpg=="],
"@rivetkit/engine-runner": ["@rivetkit/engine-runner@2.2.1", "", { "dependencies": { "@rivetkit/engine-runner-protocol": "2.2.1", "@rivetkit/virtual-websocket": "2.0.33", "pino": "^9.9.5", "uuid": "^12.0.0", "ws": "^8.18.3" } }, "sha512-6+Q3iohT23JlwVxY3xd3fhCnvCmPTO/Ua7IeVE2vJAtfhK9yUOeKSf8C8W/4CVfaHTgkWksuy0WuhqDX9B3ERg=="],
"@rivetkit/engine-runner-protocol": ["@rivetkit/engine-runner-protocol@2.2.1", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2" } }, "sha512-H0TprnHI3QOM6ccT1FEqCjazlkFYknQuprisxAfIgSXtckdF6boI5UdA4hjYqlTiuBAzMxZ1zewyD84r6hMSHg=="],
"@rivetkit/fast-json-patch": ["@rivetkit/fast-json-patch@3.1.2", "", {}, "sha512-CtA50xgsSSzICQduF/NDShPRzvucnNvsW/lQO0WgMTT1XAj9Lfae4pm7r3llFwilgG+9iq76Hv1LUqNy72v6yw=="],
"@rivetkit/on-change": ["@rivetkit/on-change@6.0.2-rc.1", "", {}, "sha512-5RC9Ze/wTKqSlJvopdCgr+EfyV93+iiH8Thog0QXrl8PT1unuBNw/jadXNMtwgAxrIaCJL+JLaHQH9w7rqpMDw=="],
"@rivetkit/sqlite": ["@rivetkit/sqlite@0.1.1", "", {}, "sha512-NE7ZBy/hQhOrWzMZFjkHX9SoXxf+ILcDvVV+mNbUYPgiy/fsDzlXdK0+JDTGnko5f4Xl6/KVCoCozz9gkwkq8A=="],
"@rivetkit/sqlite-vfs": ["@rivetkit/sqlite-vfs@2.2.1", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2", "@rivetkit/sqlite": "^0.1.1", "vbare": "^0.0.4" } }, "sha512-gva6rk2YMhhgMJGezGNGDiX/jLM832e7jfg26RugPDWbGCWeRANKbTDRoslKJJeuzI7a1bJyp04L26mH2bDYjw=="],
"@rivetkit/traces": ["@rivetkit/traces@2.2.1", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2", "cbor-x": "^1.6.0", "fdb-tuple": "^1.0.0", "vbare": "^0.0.4" } }, "sha512-WI7VcnxRH7hvHFT3fEgtqTdteKB83uEprazphEKEHZMyOkExw1TnaNdT58vti8mdrgbV9b//ylDUl4yUBbwp/w=="],
"@rivetkit/virtual-websocket": ["@rivetkit/virtual-websocket@2.0.33", "", {}, "sha512-sMoHZgBy9WDW76pv+ML3LPgf7TWk5vXdu3ZpPO20j6n+rB3fLacnnmzjt5xD6tZcJ/x5qINyEywGgcxA7MTMuQ=="],
"@rivetkit/workflow-engine": ["@rivetkit/workflow-engine@2.2.1", "", { "dependencies": { "@rivetkit/bare-ts": "^0.6.2", "cbor-x": "^1.6.0", "fdb-tuple": "^1.0.0", "pino": "^9.6.0", "vbare": "^0.0.4" } }, "sha512-EyWRKpPFPjkYl10hgMGYkjPWUkOwr/Qg10aBRz6EAseL+zwMH7K2jCLkJU4xsVfPCYcso2Euvir5+4dvoaI3yA=="],
"@sandbox-agent/cli": ["@sandbox-agent/cli@0.4.2", "", { "dependencies": { "@sandbox-agent/cli-shared": "0.4.2" }, "optionalDependencies": { "@sandbox-agent/cli-darwin-arm64": "0.4.2", "@sandbox-agent/cli-darwin-x64": "0.4.2", "@sandbox-agent/cli-linux-arm64": "0.4.2", "@sandbox-agent/cli-linux-x64": "0.4.2", "@sandbox-agent/cli-win32-x64": "0.4.2" }, "bin": { "sandbox-agent": "bin/sandbox-agent" } }, "sha512-trO//ypJBSt5xkewuol9LOykvDgHwUXq8R+yQVS+0CmpN3lYUtewHkb+At9RVGRhDMmJZY2oasaXDnhfurQ33w=="],
"@sandbox-agent/cli-darwin-arm64": ["@sandbox-agent/cli-darwin-arm64@0.4.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+L1O8SI7k/LLhyB4dG0ghmz1cJHa0WtVjuRTrEE2gw/5EbGLWopPBsCVCmQ7snrQ4fPwtaiZDhfExcEj1VI7aw=="],
"@sandbox-agent/cli-darwin-x64": ["@sandbox-agent/cli-darwin-x64@0.4.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-dDg/EwWsdgVVbJiiCX1scSNRRA48u77SsC7Tuqrfzx4fIJMLuLiIcmEtXQyCBWysSyQNV2Cr+PYXXQfCb3xg8g=="],
"@sandbox-agent/cli-linux-arm64": ["@sandbox-agent/cli-linux-arm64@0.4.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-TGmTUexMoubmWQyTeaOJu0rDVl2h0Ifh1pZ0ceZy7u/6Eoqs2n46CbfQtasUxZJf10uxPgRyzEDhcdDrTYVQUA=="],
"@sandbox-agent/cli-linux-x64": ["@sandbox-agent/cli-linux-x64@0.4.2", "", { "os": "linux", "cpu": "x64" }, "sha512-H9Rbqq0DRkCHvakzefJUDrDa2y+vJjlYd5/tefzKbQ34locE13TGNygRLxdEVXpBECjK9wVdBwTVEphQNsOcjw=="],
"@sandbox-agent/cli-shared": ["@sandbox-agent/cli-shared@0.4.2", "", {}, "sha512-sjZXRkKeFXCSKR6hHzF2Af8CCRO3F3WFwVQJ22+sLTXJ2xskV8lkUE4egknQU9B5BC1Zumts/YiNCFQWG85awQ=="],
"@sandbox-agent/cli-win32-x64": ["@sandbox-agent/cli-win32-x64@0.4.2", "", { "os": "win32", "cpu": "x64" }, "sha512-lZNfHWPwQe/VH51Yvrl/ATCUvBZ3a+c8mwovojhQcmZlv4QuUQPkuvxhPqHRh9AyBx78L5J/ha46es2doa34nQ=="],
"@secure-exec/core": ["@secure-exec/core@0.2.1", "", { "dependencies": { "better-sqlite3": "^12.8.0" } }, "sha512-HsnUv6gClpMA1BBRmX86j30TKTZtgJC/fO1tVavr7IpM2zNKbHU8LgSlBd7mv2SNy02ImTmU/GnQ3aYB4NSbEg=="],
"@secure-exec/nodejs": ["@secure-exec/nodejs@0.2.1", "", { "dependencies": { "@secure-exec/core": "0.2.1", "@secure-exec/v8": "0.2.1", "cbor-x": "^1.6.4", "cjs-module-lexer": "^2.1.0", "es-module-lexer": "^1.7.0", "esbuild": "^0.27.1", "node-stdlib-browser": "^1.3.1", "web-streams-polyfill": "^4.2.0" } }, "sha512-UJMJqVFxexlHJV0Q9nWURvrz6GElj8673DDOOFln6FHR6JS+9SaSU3eISrN158DuNC3SFi4rgjb/scKnK4YOYQ=="],
"@secure-exec/v8": ["@secure-exec/v8@0.2.1", "", { "dependencies": { "cbor-x": "^1.6.4" }, "optionalDependencies": { "@secure-exec/v8-darwin-arm64": "0.2.1", "@secure-exec/v8-darwin-x64": "0.2.1", "@secure-exec/v8-linux-arm64-gnu": "0.2.1", "@secure-exec/v8-linux-x64-gnu": "0.2.1" } }, "sha512-ye/seCqzvyMGnvyP+AO7RkVMR/lE3x9m0D2PfmiAXA457R78ZmOFmZ6v+JlJG2vv3LM30KsSXTUhwpG+Teh0hw=="],
"@secure-exec/v8-darwin-arm64": ["@secure-exec/v8-darwin-arm64@0.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gEWhMHzUpLwzuBNAD0lVkZXE8wFlWMLp4IOZ+56FYwOW/C+m07cYxuW4TjHyPqZ+vPm3IkoaMqqH5yT9VhjX/Q=="],
"@secure-exec/v8-darwin-x64": ["@secure-exec/v8-darwin-x64@0.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-H2Z5K+Cq+fn/kxjGvhJzepnNFWG6qNdyhZybVWGr5bAAZoSz/Qkad4WnXcurWU+880tKDtnf19LHBXrg7zewNQ=="],
"@secure-exec/v8-linux-arm64-gnu": ["@secure-exec/v8-linux-arm64-gnu@0.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-14subGhVV/gW35mYYm7Gv1Keeex7PxIgQfoKji/JH7wYyDuarP6kgaES0nJw+JXVkxEVud52c+kbcIjIggqCEw=="],
"@secure-exec/v8-linux-x64-gnu": ["@secure-exec/v8-linux-x64-gnu@0.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Az4s+vUf+78vWtsC7rTn/jQc6WKJafAdt2YpEjB4Gnu+sX+FFTIst1hRV4gJonbRyJdy6SW+OQ6DZatmwczorQ=="],
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="],
"@types/dockerode": ["@types/dockerode@3.3.47", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw=="],
"@types/node": ["@types/node@22.19.19", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew=="],
"@types/retry": ["@types/retry@0.12.2", "", {}, "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow=="],
"@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="],
"acp-http-client": ["acp-http-client@0.4.2", "", { "dependencies": { "@agentclientprotocol/sdk": "^0.16.1" } }, "sha512-3wtPieF08YIU4vNXaoL5up/1D0if4i9IX3Ye5q/bwbcwg1BKsazIK/VNNfvN4ldbPjWul69IqIOpGRS3I0qo3Q=="],
"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=="],
"asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
"asn1.js": ["asn1.js@4.10.1", "", { "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw=="],
"assert": ["assert@2.1.0", "", { "dependencies": { "call-bind": "^1.0.2", "is-nan": "^1.3.2", "object-is": "^1.1.5", "object.assign": "^4.1.4", "util": "^0.12.5" } }, "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw=="],
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
"better-sqlite3": ["better-sqlite3@12.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ=="],
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
"bn.js": ["bn.js@5.2.3", "", {}, "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w=="],
"brorand": ["brorand@1.1.0", "", {}, "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="],
"browser-resolve": ["browser-resolve@2.0.0", "", { "dependencies": { "resolve": "^1.17.0" } }, "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ=="],
"browserify-aes": ["browserify-aes@1.2.0", "", { "dependencies": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.3", "inherits": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA=="],
"browserify-cipher": ["browserify-cipher@1.0.1", "", { "dependencies": { "browserify-aes": "^1.0.4", "browserify-des": "^1.0.0", "evp_bytestokey": "^1.0.0" } }, "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w=="],
"browserify-des": ["browserify-des@1.0.2", "", { "dependencies": { "cipher-base": "^1.0.1", "des.js": "^1.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A=="],
"browserify-rsa": ["browserify-rsa@4.1.1", "", { "dependencies": { "bn.js": "^5.2.1", "randombytes": "^2.1.0", "safe-buffer": "^5.2.1" } }, "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ=="],
"browserify-sign": ["browserify-sign@4.2.5", "", { "dependencies": { "bn.js": "^5.2.2", "browserify-rsa": "^4.1.1", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "elliptic": "^6.6.1", "inherits": "^2.0.4", "parse-asn1": "^5.1.9", "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1" } }, "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw=="],
"browserify-zlib": ["browserify-zlib@0.2.0", "", { "dependencies": { "pako": "~1.0.5" } }, "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA=="],
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"buffer-xor": ["buffer-xor@1.0.3", "", {}, "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ=="],
"buildcheck": ["buildcheck@0.0.7", "", {}, "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA=="],
"builtin-status-codes": ["builtin-status-codes@3.0.0", "", {}, "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ=="],
"call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
"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=="],
"cbor-extract": ["cbor-extract@2.2.2", "", { "dependencies": { "node-gyp-build-optional-packages": "5.1.1" }, "optionalDependencies": { "@cbor-extract/cbor-extract-darwin-arm64": "2.2.2", "@cbor-extract/cbor-extract-darwin-x64": "2.2.2", "@cbor-extract/cbor-extract-linux-arm": "2.2.2", "@cbor-extract/cbor-extract-linux-arm64": "2.2.2", "@cbor-extract/cbor-extract-linux-x64": "2.2.2", "@cbor-extract/cbor-extract-win32-x64": "2.2.2" }, "bin": { "download-cbor-prebuilds": "bin/download-prebuilds.js" } }, "sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng=="],
"cbor-x": ["cbor-x@1.6.4", "", { "optionalDependencies": { "cbor-extract": "^2.2.2" } }, "sha512-UGKHjp6RHC6QuZ2yy5LCKm7MojM4716DwoSaqwQpaH4DvZvbBTGcoDNTiG9Y2lByXZYFEs9WRkS5tLl96IrF1Q=="],
"chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
"cipher-base": ["cipher-base@1.0.7", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.2" } }, "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA=="],
"cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
"console-browserify": ["console-browserify@1.2.0", "", {}, "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA=="],
"constants-browserify": ["constants-browserify@1.0.0", "", {}, "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ=="],
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="],
"create-ecdh": ["create-ecdh@4.0.4", "", { "dependencies": { "bn.js": "^4.1.0", "elliptic": "^6.5.3" } }, "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A=="],
"create-hash": ["create-hash@1.2.0", "", { "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", "md5.js": "^1.3.4", "ripemd160": "^2.0.1", "sha.js": "^2.4.0" } }, "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg=="],
"create-hmac": ["create-hmac@1.1.7", "", { "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", "inherits": "^2.0.1", "ripemd160": "^2.0.0", "safe-buffer": "^5.0.1", "sha.js": "^2.4.8" } }, "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg=="],
"create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="],
"croner": ["croner@10.0.1", "", {}, "sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g=="],
"crypto-browserify": ["crypto-browserify@3.12.1", "", { "dependencies": { "browserify-cipher": "^1.0.1", "browserify-sign": "^4.2.3", "create-ecdh": "^4.0.4", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "diffie-hellman": "^5.0.3", "hash-base": "~3.0.4", "inherits": "^2.0.4", "pbkdf2": "^3.1.2", "public-encrypt": "^4.0.3", "randombytes": "^2.1.0", "randomfill": "^1.0.4" } }, "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"des.js": ["des.js@1.1.0", "", { "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"diffie-hellman": ["diffie-hellman@5.0.3", "", { "dependencies": { "bn.js": "^4.1.0", "miller-rabin": "^4.0.0", "randombytes": "^2.0.0" } }, "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg=="],
"docker-modem": ["docker-modem@5.0.7", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA=="],
"dockerode": ["dockerode@4.0.12", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.7", "protobufjs": "^7.3.2", "tar-fs": "^2.1.4", "uuid": "^10.0.0" } }, "sha512-/bCZd6KlGcjZO8Buqmi/vXuqEGVEZ0PNjx/biBNqJD3MhK9DmdiAuKxqfNhflgDESDIiBz3qF+0e55+CpnrUcw=="],
"domain-browser": ["domain-browser@4.22.0", "", {}, "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw=="],
"dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="],
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": "bin.cjs" }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="],
"drizzle-orm": ["drizzle-orm@0.36.4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=3", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/react": ">=18", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "react": ">=18", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/react", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "bun-types", "expo-sqlite", "knex", "kysely", "mysql2", "pg", "sql.js", "sqlite3"] }, "sha512-1OZY3PXD7BR00Gl61UUOFihslDldfH4NFRH2MbP54Yxi0G/PKn4HfO65JYZ7c16DeP3SpM3Aw+VXVG9j6CRSXA=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"elliptic": ["elliptic@6.6.1", "", { "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.1", "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"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": "bin/esbuild" }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
"evp_bytestokey": ["evp_bytestokey@1.0.3", "", { "dependencies": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" } }, "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA=="],
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
"fast-copy": ["fast-copy@4.0.3", "", {}, "sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw=="],
"fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="],
"fdb-tuple": ["fdb-tuple@1.0.0", "", {}, "sha512-8jSvKPCYCgTpi9Pt87qlfTk6griyMx4Gk3Xv31Dp72Qp8b6XgIyFsMm8KzPmFJ9iJ8K4pGvRxvOS8D0XGnrkjw=="],
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-port": ["get-port@7.2.0", "", {}, "sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"hash-base": ["hash-base@3.0.5", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1" } }, "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg=="],
"hash.js": ["hash.js@1.1.7", "", { "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" } }, "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA=="],
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
"help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
"hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg=="],
"hono": ["hono@4.12.19", "", {}, "sha512-xa3eYXYXx68XTT4hZ7dRzsXBhaq85ToSrlUJNoR0gwz/1Ap/CNwX47wfvV7pc/xWhjKVVkLT7zBJy8chhNguqQ=="],
"https-browserify": ["https-browserify@1.0.0", "", {}, "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="],
"is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="],
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
"is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
"is-nan": ["is-nan@1.3.2", "", { "dependencies": { "call-bind": "^1.0.0", "define-properties": "^1.1.3" } }, "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w=="],
"is-network-error": ["is-network-error@1.3.2", "", {}, "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA=="],
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
"isomorphic-timers-promises": ["isomorphic-timers-promises@1.0.1", "", {}, "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ=="],
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
"js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"long-timeout": ["long-timeout@0.1.1", "", {}, "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": "cli.js" }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="],
"map-obj": ["map-obj@4.3.0", "", {}, "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"md5.js": ["md5.js@1.3.5", "", { "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg=="],
"miller-rabin": ["miller-rabin@4.0.1", "", { "dependencies": { "bn.js": "^4.0.0", "brorand": "^1.0.1" }, "bin": "bin/miller-rabin" }, "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA=="],
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
"minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="],
"minimalistic-crypto-utils": ["minimalistic-crypto-utils@1.0.1", "", {}, "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nan": ["nan@2.27.0", "", {}, "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ=="],
"nanoevents": ["nanoevents@9.1.0", "", {}, "sha512-Jd0fILWG44a9luj8v5kED4WI+zfkkgwKyRQKItTtlPfEsh7Lznfi1kr8/iZ+XAIss4Qq5GqRB0qtWbaz9ceO/A=="],
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
"no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="],
"node-abi": ["node-abi@3.92.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ=="],
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.1.1", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw=="],
"node-stdlib-browser": ["node-stdlib-browser@1.3.1", "", { "dependencies": { "assert": "^2.0.0", "browser-resolve": "^2.0.0", "browserify-zlib": "^0.2.0", "buffer": "^5.7.1", "console-browserify": "^1.1.0", "constants-browserify": "^1.0.0", "create-require": "^1.1.1", "crypto-browserify": "^3.12.1", "domain-browser": "4.22.0", "events": "^3.0.0", "https-browserify": "^1.0.0", "isomorphic-timers-promises": "^1.0.1", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", "pkg-dir": "^5.0.0", "process": "^0.11.10", "punycode": "^1.4.1", "querystring-es3": "^0.2.1", "readable-stream": "^3.6.0", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", "string_decoder": "^1.0.0", "timers-browserify": "^2.0.4", "tty-browserify": "0.0.1", "url": "^0.11.4", "util": "^0.12.4", "vm-browserify": "^1.0.1" } }, "sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"object-is": ["object-is@1.1.6", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" } }, "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q=="],
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
"object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"openapi3-ts": ["openapi3-ts@4.5.0", "", { "dependencies": { "yaml": "^2.8.0" } }, "sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ=="],
"os-browserify": ["os-browserify@0.3.0", "", {}, "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"p-retry": ["p-retry@6.2.1", "", { "dependencies": { "@types/retry": "0.12.2", "is-network-error": "^1.0.0", "retry": "^0.13.1" } }, "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ=="],
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
"parse-asn1": ["parse-asn1@5.1.9", "", { "dependencies": { "asn1.js": "^4.10.1", "browserify-aes": "^1.2.0", "evp_bytestokey": "^1.0.3", "pbkdf2": "^3.1.5", "safe-buffer": "^5.2.1" } }, "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
"pbkdf2": ["pbkdf2@3.1.5", "", { "dependencies": { "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "ripemd160": "^2.0.3", "safe-buffer": "^5.2.1", "sha.js": "^2.4.12", "to-buffer": "^1.2.1" } }, "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ=="],
"pino": ["pino@9.14.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": "bin.js" }, "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w=="],
"pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="],
"pino-pretty": ["pino-pretty@13.1.3", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^4.0.0", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pump": "^3.0.0", "secure-json-parse": "^4.0.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^5.0.2" }, "bin": "bin.js" }, "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg=="],
"pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="],
"pkg-dir": ["pkg-dir@5.0.0", "", { "dependencies": { "find-up": "^5.0.0" } }, "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postgres": ["postgres@3.4.9", "", {}, "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw=="],
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": "bin.js" }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
"protobufjs": ["protobufjs@7.5.9", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-Od4muIm3HW1AouyHF5lONOf1FWo3hY1NbFDoy191X9GzhpgW1clCoaFjfVs2rKJNFYpTNJbje4cbAIDBZJ63ZA=="],
"public-encrypt": ["public-encrypt@4.0.3", "", { "dependencies": { "bn.js": "^4.1.0", "browserify-rsa": "^4.0.0", "create-hash": "^1.1.0", "parse-asn1": "^5.0.0", "randombytes": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q=="],
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
"punycode": ["punycode@1.4.1", "", {}, "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="],
"pyodide": ["pyodide@0.28.3", "", { "dependencies": { "ws": "^8.5.0" } }, "sha512-rtCsyTU55oNGpLzSVuAd55ZvruJDEX8o6keSdWKN9jPeBVSNlynaKFG7eRqkiIgU7i2M6HEgYtm0atCEQX3u4A=="],
"qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="],
"querystring-es3": ["querystring-es3@0.2.1", "", {}, "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA=="],
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
"randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="],
"randomfill": ["randomfill@1.0.4", "", { "dependencies": { "randombytes": "^2.0.5", "safe-buffer": "^5.1.0" } }, "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw=="],
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": "cli.js" }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
"react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
"ripemd160": ["ripemd160@2.0.3", "", { "dependencies": { "hash-base": "^3.1.2", "inherits": "^2.0.4" } }, "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA=="],
"rivetkit": ["rivetkit@2.2.1", "", { "dependencies": { "@hono/node-server": "^1.18.2", "@hono/node-ws": "^1.1.1", "@hono/standard-validator": "^0.1.3", "@hono/zod-openapi": "^1.1.5", "@rivet-dev/agent-os-core": "^0.1.1", "@rivetkit/bare-ts": "^0.6.2", "@rivetkit/engine-runner": "2.2.1", "@rivetkit/fast-json-patch": "^3.1.2", "@rivetkit/on-change": "^6.0.2-rc.1", "@rivetkit/sqlite": "^0.1.1", "@rivetkit/sqlite-vfs": "2.2.1", "@rivetkit/traces": "2.2.1", "@rivetkit/virtual-websocket": "2.0.33", "@rivetkit/workflow-engine": "2.2.1", "cbor-x": "^1.6.0", "get-port": "^7.1.0", "hono": "^4.7.0", "invariant": "^2.2.4", "nanoevents": "^9.1.0", "p-retry": "^6.2.1", "pino": "^9.5.0", "sandbox-agent": "^0.4.2", "tar": "^7.5.0", "uuid": "^12.0.0", "vbare": "^0.0.4", "zod": "^4.1.0" }, "peerDependencies": { "@daytonaio/sdk": "^0.150.0", "@e2b/code-interpreter": "^2.3.3", "@fly/sprites": ">=0.0.1", "@vercel/sandbox": ">=0.1.0", "computesdk": ">=0.1.0", "dockerode": "^4.0.9", "drizzle-kit": "^0.31.2", "eventsource": "^4.0.0", "modal": ">=0.1.0", "ws": "^8.0.0" }, "optionalPeers": ["@daytonaio/sdk", "@e2b/code-interpreter", "@fly/sprites", "@vercel/sandbox", "computesdk", "eventsource", "modal"] }, "sha512-Na4ED0x4iaS41QlMcSgV5xM50mAZALVFV8At3EgkwbtP8FtzddjBHH5kS/rSu2hqs++iRMT8OaKVozetbHYK+Q=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"sandbox-agent": ["sandbox-agent@0.4.2", "", { "dependencies": { "@sandbox-agent/cli-shared": "0.4.2", "acp-http-client": "0.4.2" }, "optionalDependencies": { "@sandbox-agent/cli": "0.4.2" }, "peerDependencies": { "@cloudflare/sandbox": ">=0.1.0", "@daytonaio/sdk": ">=0.12.0", "@e2b/code-interpreter": ">=1.0.0", "@fly/sprites": ">=0.0.1", "@vercel/sandbox": ">=0.1.0", "computesdk": ">=0.1.0", "dockerode": ">=4.0.0", "get-port": ">=7.0.0", "modal": ">=0.1.0" }, "optionalPeers": ["@cloudflare/sandbox", "@daytonaio/sdk", "@e2b/code-interpreter", "@fly/sprites", "@vercel/sandbox", "computesdk", "modal"] }, "sha512-fH6WDQEaIrgiu93LxZcy+4Dx+t+/cslu+hzXImDyUlsaL6jV2jIv4fdxELkALlo7uzyEDVK9lmqs9qy65RHwBQ=="],
"secure-exec": ["secure-exec@0.2.1", "", { "dependencies": { "@secure-exec/core": "0.2.1", "@secure-exec/nodejs": "0.2.1" } }, "sha512-oaQDzTPDSCOckYC8G0PimIqzEVxY6sYEvcx0fMGsRR/Wl4wkFVHaZgQ3kc2DHWysV6WHWt5g1AXc/6seafO2XQ=="],
"secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="],
"semver": ["semver@7.8.0", "", { "bin": "bin/semver.js" }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
"sha.js": ["sha.js@2.4.12", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.0" }, "bin": "bin.js" }, "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w=="],
"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.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
"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=="],
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
"simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
"snake-case": ["snake-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg=="],
"snakecase-keys": ["snakecase-keys@8.0.1", "", { "dependencies": { "map-obj": "^4.1.0", "snake-case": "^3.0.4", "type-fest": "^4.15.0" } }, "sha512-Sj51kE1zC7zh6TDlNNz0/Jn1n5HiHdoQErxO8jLtnyrkJW/M5PrI7x05uDgY3BO7OUQYKCvmeMurW6BPUdwEOw=="],
"sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="],
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"ssh2": ["ssh2@1.17.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.23.0" } }, "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ=="],
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
"stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="],
"stream-http": ["stream-http@3.2.0", "", { "dependencies": { "builtin-status-codes": "^3.0.0", "inherits": "^2.0.4", "readable-stream": "^3.6.0", "xtend": "^4.0.2" } }, "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A=="],
"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_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"swr": ["swr@2.3.4", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg=="],
"tar": ["tar@7.5.15", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ=="],
"tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
"tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
"thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="],
"timers-browserify": ["timers-browserify@2.0.12", "", { "dependencies": { "setimmediate": "^1.0.4" } }, "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ=="],
"to-buffer": ["to-buffer@1.2.2", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tsx": ["tsx@4.22.1", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": "dist/cli.mjs" }, "sha512-TvncJykhxAzFCk0VQZKBTClall4Pm7qXDSodb6uxi8QFa8X8mT6ABjxxsQ2opDRYxG7AzcRWXaFtruz5HJKuWg=="],
"tty-browserify": ["tty-browserify@0.0.1", "", {}, "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw=="],
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"url": ["url@0.11.4", "", { "dependencies": { "punycode": "^1.4.1", "qs": "^6.12.3" } }, "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg=="],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"uuid": ["uuid@10.0.0", "", { "bin": "dist/bin/uuid" }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
"vbare": ["vbare@0.0.4", "", {}, "sha512-QsxSVw76NqYUWYPVcQmOnQPX8buIVjgn+yqldTHlWISulBTB9TJ9rnzZceDu+GZmycOtzsmuPbPN1YNxvK12fg=="],
"vm-browserify": ["vm-browserify@1.1.2", "", {}, "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="],
"web-streams-polyfill": ["web-streams-polyfill@4.3.0", "", {}, "sha512-/Gnggvj9oSrEvJbDyyPtAnxBt5fGQM2iWOKQNu7ie1OxDgK40iZpyV3TKaRiEzVj1oA1UxKnEy9XPXh6PW3eVw=="],
"which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="],
"wrap-ansi": ["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.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"yaml": ["yaml@2.9.0", "", { "bin": "bin.mjs" }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": "bin/esbuild" }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"@grpc/grpc-js/@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="],
"@rivetkit/engine-runner/uuid": ["uuid@12.0.1", "", { "bin": "dist/bin/uuid" }, "sha512-9obBF8sMIHJWNQaO6IGOG8giGa/jUpKX34bz6o4whVs8M0WAvhID2tNxYp6A2XEBJPuZSX8wsS/6TEKfIDc+nw=="],
"@secure-exec/nodejs/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": "bin/esbuild" }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
"@types/ssh2/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
"asn1.js/bn.js": ["bn.js@4.12.3", "", {}, "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g=="],
"browserify-sign/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=="],
"create-ecdh/bn.js": ["bn.js@4.12.3", "", {}, "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g=="],
"diffie-hellman/bn.js": ["bn.js@4.12.3", "", {}, "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g=="],
"elliptic/bn.js": ["bn.js@4.12.3", "", {}, "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g=="],
"miller-rabin/bn.js": ["bn.js@4.12.3", "", {}, "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g=="],
"pino-pretty/pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="],
"public-encrypt/bn.js": ["bn.js@4.12.3", "", {}, "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g=="],
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"ripemd160/hash-base": ["hash-base@3.1.2", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.1" } }, "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg=="],
"rivetkit/uuid": ["uuid@12.0.1", "", { "bin": "dist/bin/uuid" }, "sha512-9obBF8sMIHJWNQaO6IGOG8giGa/jUpKX34bz6o4whVs8M0WAvhID2tNxYp6A2XEBJPuZSX8wsS/6TEKfIDc+nw=="],
"rivetkit/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"to-buffer/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"tsx/esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": "bin/esbuild" }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
"@secure-exec/nodejs/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="],
"@secure-exec/nodejs/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="],
"@secure-exec/nodejs/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="],
"@secure-exec/nodejs/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="],
"@secure-exec/nodejs/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="],
"@secure-exec/nodejs/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="],
"@secure-exec/nodejs/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="],
"@secure-exec/nodejs/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="],
"@secure-exec/nodejs/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="],
"@secure-exec/nodejs/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="],
"@secure-exec/nodejs/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="],
"@secure-exec/nodejs/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="],
"@secure-exec/nodejs/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="],
"@secure-exec/nodejs/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="],
"@secure-exec/nodejs/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="],
"@secure-exec/nodejs/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="],
"@secure-exec/nodejs/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="],
"@secure-exec/nodejs/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="],
"@secure-exec/nodejs/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="],
"@secure-exec/nodejs/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="],
"@secure-exec/nodejs/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="],
"@secure-exec/nodejs/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="],
"@secure-exec/nodejs/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="],
"@secure-exec/nodejs/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="],
"@secure-exec/nodejs/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="],
"@secure-exec/nodejs/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="],
"@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"browserify-sign/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"browserify-sign/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"ripemd160/hash-base/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=="],
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="],
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="],
"tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw=="],
"tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.0", "", { "os": "android", "cpu": "x64" }, "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA=="],
"tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q=="],
"tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ=="],
"tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q=="],
"tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw=="],
"tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw=="],
"tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A=="],
"tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ=="],
"tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg=="],
"tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w=="],
"tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg=="],
"tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ=="],
"tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q=="],
"tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ=="],
"tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw=="],
"tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.0", "", { "os": "none", "cpu": "x64" }, "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw=="],
"tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g=="],
"tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA=="],
"tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w=="],
"tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw=="],
"tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA=="],
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA=="],
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="],
"browserify-sign/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"ripemd160/hash-base/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
"ripemd160/hash-base/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"ripemd160/hash-base/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"ripemd160/hash-base/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
}
}

View File

@@ -19,22 +19,48 @@ services:
retries: 10
restart: unless-stopped
# ── Central Gitea (one org-wide instance, changes.md §2A) ──
# Every user gets a repo inside the GrowQR organization on this instance.
# Per-user Gitea containers are REMOVED — the backend no longer spawns them.
gitea:
image: gitea/gitea:1.22
container_name: growqr-gitea
environment:
USER_UID: "1000"
USER_GID: "1000"
GITEA__server__ROOT_URL: http://localhost:3001
GITEA__server__SSH_PORT: "2222"
GITEA__security__INSTALL_LOCK: "true"
GITEA__service__DISABLE_REGISTRATION: "true"
ports:
- "3001:3000" # HTTP (Gitea listens on 3000 internally)
- "2222:2222" # SSH
volumes:
- gitea-data:/data
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3000/api/v1/version || exit 1"]
interval: 10s
timeout: 10s
retries: 15
restart: unless-stopped
# Self-hosted Rivet engine. The backend's Rivet Kit client connects here.
# Per the PRD, the Grow Agent + sub-agents are durable actors running on Rivet.
# The unified user agent runs as a durable Rivet actor (changes.md §5).
rivet-engine:
image: rivetgg/engine:latest
image: rivetdev/engine:latest
container_name: growqr-rivet
ports:
- "6420:6420" # API
- "6421:6421" # Guard/edge
environment:
RIVET__FILE_SYSTEM__PATH: /data
RIVET__AUTH__ADMIN_TOKEN: ${RIVET_ADMIN_TOKEN:-dev-admin-token}
volumes:
- rivet-data:/data
restart: unless-stopped
# The HTTP backend (Hono + Rivet Kit client + Docker manager).
# Mounts the host Docker socket so it can spawn per-user containers.
# Mounts the host Docker socket so it can spawn per-user OpenCode containers.
backend:
build:
context: .
@@ -43,6 +69,8 @@ services:
depends_on:
postgres:
condition: service_healthy
gitea:
condition: service_healthy
rivet-engine:
condition: service_started
ports:
@@ -51,30 +79,45 @@ services:
PORT: 4000
NODE_ENV: ${NODE_ENV:-production}
DATABASE_URL: postgres://${POSTGRES_USER:-growqr}:${POSTGRES_PASSWORD:-growqr}@postgres:5432/${POSTGRES_DB:-growqr}
RIVET_ENDPOINT: http://rivet-engine:6420
# Central Gitea (shared org-wide instance)
GITEA_URL: http://gitea:3000
GITEA_ADMIN_USER: ${GITEA_ADMIN_USER:-growqr-admin}
GITEA_ADMIN_PASSWORD: ${GITEA_ADMIN_PASSWORD:-growqr-admin-dev}
GITEA_ADMIN_TOKEN: ${GITEA_ADMIN_TOKEN:-}
GITEA_ORG_NAME: ${GITEA_ORG_NAME:-growqr}
# Version tracking for image rollouts (changes.md §9)
OPENCODE_IMAGE_VERSION: ${OPENCODE_IMAGE_VERSION:-1.0.0}
MIGRATION_VERSION: ${MIGRATION_VERSION:-1}
PROMPT_VERSION: ${PROMPT_VERSION:-1}
# Rivet
RIVET_ENDPOINT: http://default:${RIVET_ADMIN_TOKEN:-dev-admin-token}@rivet-engine:6420
RIVET_CLIENT_ENDPOINT: ${RIVET_CLIENT_ENDPOINT:-http://127.0.0.1:4000/api/rivet}
# Auth
CLERK_SECRET_KEY: ${CLERK_SECRET_KEY}
CLERK_PUBLISHABLE_KEY: ${CLERK_PUBLISHABLE_KEY}
SERVICE_TOKEN: ${SERVICE_TOKEN:-dev-service-token}
A2A_ALLOWED_KEY: ${A2A_ALLOWED_KEY:-dev-a2a-key}
# LLM
OPENCODE_API_KEY: ${OPENCODE_API_KEY}
LLM_PROVIDER: ${LLM_PROVIDER:-opencode}
LLM_BASE_URL: ${LLM_BASE_URL:-https://opencode.ai/zen/v1}
LLM_MODEL: ${LLM_MODEL:-kimi-k2.6}
GROW_AGENT_MODEL: ${GROW_AGENT_MODEL:-kimi-k2.6}
SUB_AGENT_MODEL: ${SUB_AGENT_MODEL:-kimi-k2.6}
SERVICE_TOKEN: ${SERVICE_TOKEN:-dev-service-token}
A2A_ALLOWED_KEY: ${A2A_ALLOWED_KEY:-dev-a2a-key}
RIVET_CLIENT_ENDPOINT: ${RIVET_CLIENT_ENDPOINT:-http://127.0.0.1:4000/api/rivet}
GITEA_IMAGE: ${GITEA_IMAGE:-gitea/gitea:1.22}
OPENCODE_IMAGE: ${OPENCODE_IMAGE:-ghcr.io/anomalyco/opencode:latest}
INTERVIEW_SERVICE_URL: ${INTERVIEW_SERVICE_URL:-http://host.docker.internal:8007}
ROLEPLAY_SERVICE_URL: ${ROLEPLAY_SERVICE_URL:-http://host.docker.internal:8008}
QSCORE_SERVICE_URL: ${QSCORE_SERVICE_URL:-http://host.docker.internal:8000}
# Per-user OpenCode containers
OPENCODE_IMAGE: ${OPENCODE_IMAGE:-growqr/opencode:dev}
USER_CONTAINER_HOST: ${USER_CONTAINER_HOST:-host.docker.internal}
USER_DATA_ROOT: /data/users
USER_PORT_RANGE_START: 20000
USER_PORT_RANGE_END: 29999
# Microservices
INTERVIEW_SERVICE_URL: ${INTERVIEW_SERVICE_URL:-http://host.docker.internal:8007}
ROLEPLAY_SERVICE_URL: ${ROLEPLAY_SERVICE_URL:-http://host.docker.internal:8008}
QSCORE_SERVICE_URL: ${QSCORE_SERVICE_URL:-http://host.docker.internal:8000}
RESUME_SERVICE_URL: ${RESUME_SERVICE_URL:-http://host.docker.internal:8002}
# Frontend
FRONTEND_ORIGIN: ${FRONTEND_ORIGIN:-http://localhost:3000}
volumes:
# Docker-out-of-Docker: backend uses host Docker to spawn user containers.
# Docker-out-of-Docker: backend uses host Docker to spawn per-user OpenCode containers.
- /var/run/docker.sock:/var/run/docker.sock
# Shared host dir that per-user containers will also bind-mount their
# workspace from (so backend and spawned containers see the same files).
@@ -86,10 +129,11 @@ services:
retries: 6
restart: unless-stopped
# Note: per-user OpenCode + Gitea containers are NOT defined here.
# The backend spawns them dynamically via dockerode on /actors/provision.
# See src/docker/manager.ts.
# Only per-user OpenCode containers are spawned dynamically now.
# Gitea is a central shared service defined above.
# See src/docker/manager.ts for the per-user OpenCode lifecycle.
volumes:
rivet-data:
postgres-data:
gitea-data:

View File

@@ -0,0 +1,79 @@
# GrowQR OpenCode runtime image
#
# This wraps the upstream OpenCode image with GrowQR-owned prompt modules,
# sub-agent definitions, workspace template files, and runtime bootstrap.
#
# Build from growqr-backend/:
# docker build -f docker/opencode/Dockerfile -t growqr/opencode:dev .
#
# Release flow:
# docker build -f docker/opencode/Dockerfile \
# --build-arg GROWQR_IMAGE_VERSION=2026.06.01-1 \
# -t ghcr.io/<org>/growqr-opencode:2026.06.01-1 \
# -t ghcr.io/<org>/growqr-opencode:latest .
ARG OPENCODE_BASE_IMAGE=ghcr.io/anomalyco/opencode:latest
FROM ${OPENCODE_BASE_IMAGE}
ARG GROWQR_IMAGE_VERSION=dev
ARG GROWQR_PROMPT_VERSION=dev
ARG GROWQR_MIGRATION_VERSION=1
ENV GROWQR_IMAGE_VERSION=${GROWQR_IMAGE_VERSION}
ENV GROWQR_PROMPT_VERSION=${GROWQR_PROMPT_VERSION}
ENV GROWQR_MIGRATION_VERSION=${GROWQR_MIGRATION_VERSION}
ENV GROWQR_HOME=/opt/growqr
ENV OPENCODE_WORKSPACE=/workspace
USER root
# Ensure the runtime has the basics needed for Git-backed memory. The upstream
# image may already include these, but this keeps our image contract explicit.
RUN if command -v apk >/dev/null 2>&1; then \
apk add --no-cache git openssh-client ca-certificates bash curl jq; \
elif command -v apt-get >/dev/null 2>&1; then \
apt-get update && apt-get install -y --no-install-recommends \
git openssh-client ca-certificates bash curl jq && \
rm -rf /var/lib/apt/lists/*; \
else \
echo "Unsupported base image package manager" >&2; exit 1; \
fi
RUN mkdir -p \
/workspace \
/root/.config/opencode \
/root/.local/share/opencode \
/opt/growqr/agents \
/opt/growqr/prompts \
/opt/growqr/workspace-template/memory \
/opt/growqr/workspace-template/conversations \
/opt/growqr/workspace-template/state \
/opt/growqr/workspace-template/artifacts \
/opt/growqr/workspace-template/workflows \
/opt/growqr/workspace-template/logs \
/opt/growqr/workspace-template/config \
/opt/growqr/workspace-template/metadata
# GrowQR sub-agent prompt modules. These are image-versioned: update the files,
# rebuild/push a new image, then set OPENCODE_IMAGE + OPENCODE_IMAGE_VERSION in
# the backend rollout.
COPY agents/ /opt/growqr/agents/
COPY prompts/ /opt/growqr/prompts/
COPY docker/opencode/growqr.json /root/.config/opencode/growqr.json
COPY docker/opencode/entrypoint.sh /usr/local/bin/growqr-opencode-entrypoint
# Starter Git workspace. The backend will normally create the real central
# Gitea repo and clone it into /workspace after the server starts. This template
# makes fresh/standalone containers still look like a GrowQR memory repo and
# gives OpenCode immediate local context before clone/sync completes.
COPY docker/opencode/workspace-template/ /opt/growqr/workspace-template/
RUN chmod +x /usr/local/bin/growqr-opencode-entrypoint && \
mkdir -p /opt/growqr/workspace-template/metadata && \
printf '%s\n' \
"{\"imageVersion\":\"${GROWQR_IMAGE_VERSION}\",\"promptVersion\":\"${GROWQR_PROMPT_VERSION}\",\"migrationVersion\":\"${GROWQR_MIGRATION_VERSION}\"}" \
> /opt/growqr/workspace-template/metadata/image.json
WORKDIR /workspace
EXPOSE 4096
ENTRYPOINT ["growqr-opencode-entrypoint"]
CMD ["opencode", "serve", "--port", "4096", "--hostname", "0.0.0.0"]

63
docker/opencode/README.md Normal file
View File

@@ -0,0 +1,63 @@
# GrowQR OpenCode Runtime Image
This folder defines the custom per-user OpenCode image used by the Rivet user actor lifecycle.
## Why this exists
The upstream image (`ghcr.io/anomalyco/opencode:latest`) is only the base runtime. GrowQR needs an owned image that bakes in:
- GrowQR sub-agent markdown modules from `agents/`
- GrowQR system prompts from `prompts/`
- global OpenCode config under `/root/.config/opencode`
- a Git-backed workspace template under `/opt/growqr/workspace-template`
- runtime metadata for image/prompt/migration rollout checks
## Local build
From `growqr-backend/`:
```bash
docker build -f docker/opencode/Dockerfile -t growqr/opencode:dev .
```
Then run the backend with:
```bash
OPENCODE_IMAGE=growqr/opencode:dev OPENCODE_IMAGE_VERSION=dev PROMPT_VERSION=dev docker compose up -d --build backend
```
For the current compose setup, the backend talks to the host Docker socket, so local images built on the host are available to per-user containers.
## Release build
```bash
VERSION=2026.06.01-1
docker build -f docker/opencode/Dockerfile \
--build-arg GROWQR_IMAGE_VERSION=$VERSION \
--build-arg GROWQR_PROMPT_VERSION=$VERSION \
-t ghcr.io/<org>/growqr-opencode:$VERSION \
-t ghcr.io/<org>/growqr-opencode:latest .
docker push ghcr.io/<org>/growqr-opencode:$VERSION
docker push ghcr.io/<org>/growqr-opencode:latest
```
Then update backend env:
```bash
OPENCODE_IMAGE=ghcr.io/<org>/growqr-opencode:$VERSION
OPENCODE_IMAGE_VERSION=$VERSION
PROMPT_VERSION=$VERSION
```
## Runtime behavior
The backend/Rivet actor still owns lifecycle:
1. provision central Gitea repo for the user
2. start one OpenCode container from this image
3. mount host workspace at `/workspace`
4. wait for OpenCode readiness
5. clone/pull the user's Gitea repo into `/workspace`
6. sync important runtime outputs back to Git
The image only provides the runtime and templates; user data remains in the user's central Gitea repo.

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env sh
set -eu
WORKSPACE="${OPENCODE_WORKSPACE:-/workspace}"
GROWQR_HOME="${GROWQR_HOME:-/opt/growqr}"
TEMPLATE="$GROWQR_HOME/workspace-template"
mkdir -p "$WORKSPACE" /root/.config/opencode /root/.local/share/opencode
# Make sub-agents discoverable from common OpenCode/global locations. We keep
# the canonical copy in /opt/growqr and symlink/copy to config paths so future
# OpenCode versions can pick up either convention.
ln -sfn "$GROWQR_HOME/agents" /root/.config/opencode/agents
ln -sfn "$GROWQR_HOME/prompts" /root/.config/opencode/prompts
ln -sfn "$GROWQR_HOME/agents" /root/.local/share/opencode/agents
ln -sfn "$GROWQR_HOME/prompts" /root/.local/share/opencode/prompts
# Seed an empty mounted workspace with the GrowQR Git-backed memory shape. The
# backend later clones the user's central Gitea repo over this workspace when it
# provisions the user stack. This only runs for truly empty workspaces.
if [ -z "$(find "$WORKSPACE" -mindepth 1 -maxdepth 1 2>/dev/null | head -n 1)" ]; then
cp -a "$TEMPLATE"/. "$WORKSPACE"/
cat > "$WORKSPACE/README.md" <<EOF
# GrowQR User Workspace
This workspace is controlled by GrowQR's Rivet user actor and backed by the
user's central Gitea repository. OpenCode runs here with GrowQR sub-agents
available globally from:
- /opt/growqr/agents
- /opt/growqr/prompts
- /root/.config/opencode/agents
EOF
git -C "$WORKSPACE" init -b main >/dev/null 2>&1 || true
git -C "$WORKSPACE" config user.email "growqr@local" || true
git -C "$WORKSPACE" config user.name "GrowQR" || true
git -C "$WORKSPACE" add -A >/dev/null 2>&1 || true
git -C "$WORKSPACE" commit -m "init: growqr workspace template" >/dev/null 2>&1 || true
fi
cat > "$WORKSPACE/.growqr-runtime.json" <<EOF
{
"imageVersion": "${GROWQR_IMAGE_VERSION:-dev}",
"promptVersion": "${GROWQR_PROMPT_VERSION:-dev}",
"migrationVersion": "${GROWQR_MIGRATION_VERSION:-1}",
"agentsDir": "$GROWQR_HOME/agents",
"promptsDir": "$GROWQR_HOME/prompts"
}
EOF
exec "$@"

View File

@@ -0,0 +1,10 @@
{
"growqr": {
"name": "GrowQR OpenCode Runtime",
"agentsDir": "/opt/growqr/agents",
"promptsDir": "/opt/growqr/prompts",
"workspace": "/workspace",
"memoryRepo": "git-backed",
"notes": "This file is baked into the custom GrowQR OpenCode image. Update agents/prompts and rebuild the image to release sub-agent changes."
}
}

View File

@@ -0,0 +1,3 @@
# Artifacts
Generated resumes, interview notes, reports, scorecards, and other deliverables.

View File

@@ -0,0 +1,3 @@
# Config
User-specific GrowQR/OpenCode runtime configuration.

View File

@@ -0,0 +1,3 @@
# Conversations
Conversation exports and assistant summaries.

View File

@@ -0,0 +1,3 @@
# Logs
Runtime logs or summaries that are safe to persist to the user's repo.

View File

@@ -0,0 +1,3 @@
# Memory
Durable user career memory. The GrowQR actor and OpenCode runtime should write stable facts, preferences, decisions, and summaries here.

View File

@@ -0,0 +1,3 @@
# State
Small serialized workflow/runtime state that should be visible in Git.

View File

@@ -0,0 +1,3 @@
# Workflows
Workflow run notes, plans, approvals, and outputs.

View File

@@ -0,0 +1,32 @@
-- Migration: Central Gitea + Unified Actor (changes.md Phase 6)
-- Renames/replaces per-user Gitea fields with central Gitea repo references.
-- Simplifies actors table for unified user actor model.
-- Adds version tracking columns.
-- 1. Drop old per-user Gitea columns from user_stacks
ALTER TABLE user_stacks DROP COLUMN IF EXISTS gitea_container_id;
ALTER TABLE user_stacks DROP COLUMN IF EXISTS gitea_container_name;
ALTER TABLE user_stacks DROP COLUMN IF EXISTS gitea_host;
ALTER TABLE user_stacks DROP COLUMN IF EXISTS gitea_http_port;
ALTER TABLE user_stacks DROP COLUMN IF EXISTS gitea_ssh_port;
ALTER TABLE user_stacks DROP COLUMN IF EXISTS gitea_admin_user;
ALTER TABLE user_stacks DROP COLUMN IF EXISTS gitea_admin_token;
ALTER TABLE user_stacks DROP COLUMN IF EXISTS gitea_memory_repo;
-- 2. Add central Gitea repo fields
ALTER TABLE user_stacks ADD COLUMN IF NOT EXISTS gitea_repo_name TEXT;
ALTER TABLE user_stacks ADD COLUMN IF NOT EXISTS gitea_repo_owner TEXT;
-- 3. Add version tracking columns (changes.md §9)
ALTER TABLE user_stacks ADD COLUMN IF NOT EXISTS image_version TEXT;
ALTER TABLE user_stacks ADD COLUMN IF NOT EXISTS migration_version TEXT;
ALTER TABLE user_stacks ADD COLUMN IF NOT EXISTS prompt_version TEXT;
-- 4. Simplify actors table for unified model
ALTER TABLE actors DROP COLUMN IF EXISTS sub_type;
ALTER TABLE actors DROP COLUMN IF EXISTS channel_id;
ALTER TABLE actors DROP COLUMN IF EXISTS parent_actor_id;
-- Change kind enum: was ('grow','sub'), now ('user')
-- Note: Drizzle handles enum changes via application-level migration.
-- The application code now only uses kind='user'.
-- Existing rows with kind='grow' or 'sub' will be left as-is (backward compatible reads).

View File

@@ -8,6 +8,13 @@
"when": 1779161128463,
"tag": "0000_init",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1780306600000,
"tag": "0001_central_gitea_unified_actor",
"breakpoints": true
}
]
}

6160
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,8 @@
"db:migrate": "tsx src/db/migrate.ts",
"db:studio": "drizzle-kit studio",
"compose:up": "docker compose up -d",
"compose:down": "docker compose down"
"compose:down": "docker compose down",
"docker:opencode:build": "docker build -f docker/opencode/Dockerfile -t growqr/opencode:dev ."
},
"dependencies": {
"@clerk/backend": "^1.21.0",

90
prompts/system.txt Normal file
View File

@@ -0,0 +1,90 @@
You are Grow — a unified AI career assistant for the GrowQR platform.
You coordinate specialist capabilities (loaded as tools), maintain durable state, and execute workflows through microservices.
## CRITICAL RULES
1. **When the user asks you to DO something (launch/start/run/create/begin/tailor/analyze) — CALL THE TOOL IMMEDIATELY.** Do not say "starting now" without actually calling the tool. Do not roleplay. The user expects real results.
2. **When the user provides information (resume, JD, preferences), respond conversationally first, then guide them to the next step.**
3. **Never show tool call syntax, XML tags, or function call blocks in your visible text.** Tool execution happens silently behind the scenes.
4. **Be concise** — 1-3 short paragraphs max per response. This is a chat, not a document.
5. **Use the [WORKFLOW: id] tag at the end of responses** when a workflow context is established.
## TOOLS YOU MUST USE (not describe, actually call):
- `start_interview_session` — call when user says "start interview", "launch interview", "practice interview", "mock interview", "set me an interview", "interview me"
- `start_roleplay_session` — call when user says "start roleplay", "launch roleplay", "roleplay", "negotiation practice"
- `analyze_resume` — call when user says "analyze my resume", "check my resume", "review my resume"
- `tailor_resume` — call when user says "tailor my resume", "optimize my resume", "fix my resume"
- `compute_qscore` — call when user says "compute score", "what's my score", "check readiness"
- `start_interview_to_offer` — call when user says "prepare me for [company] interview", "full interview prep"
## When User Asks For An Interview:
1. If they specified type (behavioral/technical/system design) AND company/role → call `start_interview_session` with the goal
2. If they only said "interview" without type → ask "Behavioral, technical, or system design?"
3. After calling the tool, report what happened: include the session link or any result
4. End with [WORKFLOW: interview-practice]
## When User Pastes Their Resume:
- Acknowledge what you see (role, key skills, strengths/weaknesses)
- NEVER call analyze_resume automatically — ask "Would you like me to run a full analysis?"
- When they say yes → call analyze_resume → report results
- End with [WORKFLOW: resume-boost]
## When User Says "Prepare for [Role] at [Company]":
- This is a multi-step workflow. FIRST, ask for the job description.
- Do NOT call start_interview_to_offer yet — wait for the JD.
- After JD: ask for resume.
- After resume: ask if they want you to analyze/tailor it.
- After resume optimization: ask what type of interview to prepare.
- When they choose type → call start_interview_session.
- Then offer roleplay → call start_roleplay_session when they confirm.
- Then offer Q Score → call compute_qscore.
- Use [WORKFLOW: interview-to-offer] tag throughout.
## IMPORTANT: Tool Calling Anti-Patterns
❌ BAD:
User: "launch my interview"
Assistant: "Launching your interview session now!"
// (no tool called — this is lying to the user)
✅ GOOD:
User: "launch my interview"
Assistant calls start_interview_session → receives result → "Your interview session is ready! [session URL]. You can click Open to begin."
❌ BAD:
User: "analyze my resume"
Assistant: "I'll analyze your resume right away."
// (no tool called)
✅ GOOD:
User: "analyze my resume"
Assistant calls analyze_resume → "Here's your analysis: [results]. Your strengths are..."
## Specialist Capabilities
{{MODULE_DESCRIPTIONS}}
## Workflow Tags (put at the VERY END, on their own line)
- [WORKFLOW: interview-to-offer] — full interview prep pipeline
- [WORKFLOW: interview-practice] — mock interview sessions
- [WORKFLOW: resume-boost] — resume analysis and optimization
- [WORKFLOW: roleplay-practice] — mock roleplay sessions
- [WORKFLOW: career-switch] — career change navigation
- [WORKFLOW: job-search] — job discovery
- [WORKFLOW: job-preparation] — broad company preparation
NEVER mention these tags in your visible text. They are system-internal.
## Tone
- Friendly, warm, conversational — like a career coach
- Direct and actionable — skip the fluff
- Acknowledge the user's situation ("That's exciting!", "Great goal!")
- Use markdown for structure (bold, bullets)

View File

@@ -1,338 +0,0 @@
import { actor } from "rivetkit";
import { log } from "../log.js";
import { config } from "../config.js";
import {
createChatCompletion,
GROW_AGENT_SYSTEM,
growAgentTools,
type LlmMessage,
type LlmToolCall,
} from "../lib/llm.js";
import {
provisionUserStack,
getUserStack,
stopUserStack,
giteaClientFor,
} from "../docker/manager.js";
import { runSubAgentTask } from "./sub-agent-runner.js";
import { db } from "../db/client.js";
import { actors as actorsTable, events as eventsTable } from "../db/schema.js";
type ChatTurn = {
role: "user" | "assistant" | "tool";
content: string;
toolCallId?: string;
toolCalls?: LlmToolCall[];
};
type GrowAgentState = {
userId: string;
goals: string[];
history: ChatTurn[];
// Trimmed once it grows past N turns; long history is delegated to memory repo.
maxHistory: number;
};
const initialState: GrowAgentState = {
userId: "",
goals: [],
history: [],
maxHistory: 40,
};
const MEMORY_REPO_PATH_LIMIT = 1024;
// One Grow Agent actor instance per user (key the actor by userId).
// Owns the user's Docker stack + LLM conversation loop.
export const growAgent = actor({
state: initialState,
actions: {
// Idempotent. Provisions the per-user OpenCode + Gitea stack if missing.
init: async (c, input: { userId: string }) => {
if (c.state.userId && c.state.userId !== input.userId) {
throw new Error("Grow Agent already bound to a different user");
}
c.state.userId = input.userId;
const stack = await provisionUserStack(input.userId);
await db
.insert(actorsTable)
.values({
actorId: `grow-${input.userId}`,
userId: input.userId,
kind: "grow",
status: "idle",
lastActivityAt: new Date(),
})
.onConflictDoNothing();
c.broadcast("stack-ready", {
userId: input.userId,
opencode: `${stack.opencodeHost}:${stack.opencodePort}`,
gitea: `${stack.giteaHost}:${stack.giteaHttpPort}`,
memoryRepo: stack.giteaMemoryRepo,
});
return stack;
},
// Main chat entry point. Runs the full agentic loop through the configured LLM.
receiveMessage: async (c, msg: { text: string }) => {
if (!c.state.userId) {
throw new Error("Grow Agent not initialized");
}
const userTurn: ChatTurn = { role: "user", content: msg.text };
c.state.history.push(userTurn);
c.broadcast("message", { role: "user", text: msg.text });
const assistantText = await runAgentLoop(c, c.state.userId);
// Trim history to maxHistory turns; long-term context lives in Gitea.
while (c.state.history.length > c.state.maxHistory) {
c.state.history.shift();
}
await db
.insert(eventsTable)
.values({
userId: c.state.userId,
actorId: `grow-${c.state.userId}`,
type: "grow.message",
payload: { userText: msg.text, assistantText },
});
return { reply: assistantText };
},
// Sub-agent status updates fan back in via this action; the Grow Agent
// broadcasts them so the frontend's sidebar can render them under the
// right channel.
subAgentEvent: async (
c,
input: {
subAgentId: string;
type: "started" | "progress" | "done" | "error";
message?: string;
result?: unknown;
},
) => {
c.broadcast("sub-agent-event", input);
},
getHistory: async (c) => c.state.history,
getGoals: async (c) => c.state.goals,
shutdown: async (c) => {
if (c.state.userId) await stopUserStack(c.state.userId);
},
},
});
// The agentic loop. Keeps calling the configured LLM with tools until the model
// returns a normal assistant turn.
async function runAgentLoop(
c: {
state: GrowAgentState;
broadcast: (event: string, data: unknown) => void;
},
userId: string,
): Promise<string> {
if (!config.llmApiKey) {
const reply =
"LLM_API_KEY or OPENCODE_API_KEY is not configured on the backend - set it to enable the Grow Agent.";
c.state.history.push({ role: "assistant", content: reply });
c.broadcast("message", { role: "agent", text: reply });
return reply;
}
c.broadcast("agent-thinking", { state: "running" });
const MAX_ITERATIONS = 8;
let assistantTextOut = "";
for (let i = 0; i < MAX_ITERATIONS; i++) {
const response = await createChatCompletion({
model: config.growAgentModel,
maxTokens: config.maxAgentTokens,
tools: growAgentTools,
messages: messagesForApi(c.state.history),
});
// Capture assistant text for streaming-style broadcast.
if (response.content) {
assistantTextOut += (assistantTextOut ? "\n\n" : "") + response.content;
c.broadcast("message", { role: "agent", text: response.content });
}
// Persist the assistant turn, including tool calls for the next tool result turn.
c.state.history.push({
role: "assistant",
content: response.content,
toolCalls: response.toolCalls,
});
if (response.toolCalls.length === 0) {
break;
}
for (const call of response.toolCalls) {
try {
const result = await dispatchTool(c, userId, call);
c.state.history.push({
role: "tool",
toolCallId: call.id,
content: typeof result === "string" ? result : JSON.stringify(result),
});
} catch (err) {
log.error({ err, tool: call.name }, "tool dispatch failed");
c.state.history.push({
role: "tool",
toolCallId: call.id,
content: `Error: ${err instanceof Error ? err.message : String(err)}`,
});
}
}
}
c.broadcast("agent-thinking", { state: "idle" });
return assistantTextOut || "(no response)";
}
function messagesForApi(history: ChatTurn[]): LlmMessage[] {
const messages: LlmMessage[] = [
{ role: "system", content: GROW_AGENT_SYSTEM },
];
for (const turn of history) {
if (turn.role === "tool") {
messages.push({
role: "tool",
content: turn.content,
tool_call_id: turn.toolCallId,
});
continue;
}
messages.push({
role: turn.role,
content: turn.content,
tool_calls: turn.toolCalls?.map((call) => ({
id: call.id,
type: "function",
function: {
name: call.name,
arguments: JSON.stringify(call.arguments),
},
})),
});
}
return messages;
}
async function dispatchTool(
c: {
broadcast: (event: string, data: unknown) => void;
state: GrowAgentState;
},
userId: string,
call: LlmToolCall,
): Promise<unknown> {
const input = call.arguments;
switch (call.name) {
case "spawn_sub_agent": {
const type = String(input.type ?? "generic");
const prompt = String(input.prompt ?? "");
const channelId =
typeof input.channelId === "string"
? input.channelId
: `${type}-${Date.now()}`;
const id = `sub-${type}-${Date.now()}`;
await db
.insert(actorsTable)
.values({
actorId: id,
userId,
kind: "sub",
subType: type,
status: "running",
channelId,
parentActorId: `grow-${userId}`,
lastActivityAt: new Date(),
});
c.broadcast("sub-agent-spawned", { id, type, channelId, prompt });
// Fire-and-forget; the runner updates DB + broadcasts via the actor.
void runSubAgentTask({
userId,
subAgentId: id,
type,
prompt,
channelId,
onEvent: (event, data) => c.broadcast(event, data),
});
return { id, type, channelId, status: "running" };
}
case "commit_memory": {
const path = String(input.path ?? "").slice(0, MEMORY_REPO_PATH_LIMIT);
const content = String(input.content ?? "");
const message = String(input.message ?? "memory update");
const client = await giteaClientFor(userId);
const stack = await getUserStack(userId);
if (!client || !stack?.giteaMemoryRepo) {
return { ok: false, error: "memory repo not provisioned" };
}
const [owner, repo] = stack.giteaMemoryRepo.split("/") as [string, string];
const result = await client.putFile({
owner,
repo,
path,
contentUtf8: content,
message,
});
c.broadcast("memory-committed", { path, message });
return { ok: true, path, commitSha: result.commitSha };
}
case "read_memory": {
const path = String(input.path ?? "");
const client = await giteaClientFor(userId);
const stack = await getUserStack(userId);
if (!client || !stack?.giteaMemoryRepo) return null;
const [owner, repo] = stack.giteaMemoryRepo.split("/") as [string, string];
const text = await client.readFile({ owner, repo, path });
return text;
}
case "list_memory": {
const pathPrefix = String(input.pathPrefix ?? "");
const client = await giteaClientFor(userId);
const stack = await getUserStack(userId);
if (!client || !stack?.giteaMemoryRepo) return [];
const [owner, repo] = stack.giteaMemoryRepo.split("/") as [string, string];
// Gitea contents API on a directory returns an array of entries.
try {
const res = await fetch(
`http://${stack.giteaHost}:${stack.giteaHttpPort}/api/v1/repos/${owner}/${repo}/contents/${encodeURI(pathPrefix)}`,
{
headers: {
authorization: `token ${stack.giteaAdminToken}`,
accept: "application/json",
},
},
);
if (!res.ok) return [];
const entries = (await res.json()) as Array<{
name: string;
path: string;
type: string;
}>;
return entries.map((e) => ({ name: e.name, path: e.path, type: e.type }));
} catch {
return [];
}
}
default:
throw new Error(`unknown tool: ${call.name}`);
}
}

View File

@@ -1,13 +1,11 @@
import { setup } from "rivetkit";
import { growAgent } from "./grow-agent.js";
import { subAgent } from "./sub-agent.js";
import { workflowJob } from "./workflow-job.js";
import { userActor } from "./user-actor.js";
// Per changes.md §5: ONE unified actor per user.
// No separate growAgent, subAgent, or workflowJob actors.
export const registry = setup({
use: {
growAgent,
subAgent,
workflowJob,
userActor,
},
});

View File

@@ -1,103 +0,0 @@
import { eq, and } from "drizzle-orm";
import { db } from "../db/client.js";
import { actors as actorsTable, opencodeSessions } from "../db/schema.js";
import { log } from "../log.js";
import { OpencodeClient } from "../lib/opencode.js";
import { opencodeUrlFor } from "../docker/manager.js";
export type SubAgentRunInput = {
userId: string;
subAgentId: string;
type: string;
prompt: string;
channelId: string;
onEvent: (event: string, data: unknown) => void;
};
// Runs a single sub-agent task by opening an OpenCode session and forwarding
// the user-provided prompt. Streams events back to the caller (the Grow Agent
// actor's broadcast surface) and updates the actors table on completion.
//
// Sub-agents do NOT spawn their own containers — they multiplex through the
// parent Grow Agent's OpenCode container (PRD §3.3).
export async function runSubAgentTask(input: SubAgentRunInput): Promise<void> {
const { userId, subAgentId, type, prompt, channelId, onEvent } = input;
try {
const target = await opencodeUrlFor(userId);
if (!target) {
throw new Error("OpenCode container not provisioned for user");
}
const client = new OpencodeClient(target.baseUrl, target.password);
const session = await client.createSession({
title: `${type} :: ${subAgentId}`,
});
await db.insert(opencodeSessions).values({
id: session.id,
userId,
actorId: subAgentId,
title: session.title ?? null,
});
onEvent("sub-agent-event", {
subAgentId,
type: "started",
channelId,
sessionId: session.id,
});
// Open SSE stream for live progress.
const aborter = client.streamEvents((ev) => {
onEvent("sub-agent-event", {
subAgentId,
type: "progress",
channelId,
event: ev.event,
data: ev.data,
});
});
// Send the prompt synchronously and capture the final response text.
const result = await client.sendMessage({
sessionId: session.id,
text: prompt,
});
aborter.abort();
await db
.update(actorsTable)
.set({ status: "done", lastActivityAt: new Date() })
.where(
and(
eq(actorsTable.userId, userId),
eq(actorsTable.actorId, subAgentId),
),
);
onEvent("sub-agent-event", {
subAgentId,
type: "done",
channelId,
result,
});
log.info({ subAgentId, sessionId: session.id }, "sub-agent done");
} catch (err) {
log.error({ err, subAgentId }, "sub-agent failed");
await db
.update(actorsTable)
.set({ status: "error", lastActivityAt: new Date() })
.where(
and(
eq(actorsTable.userId, userId),
eq(actorsTable.actorId, subAgentId),
),
)
.catch(() => undefined);
onEvent("sub-agent-event", {
subAgentId,
type: "error",
channelId,
message: err instanceof Error ? err.message : String(err),
});
}
}

View File

@@ -1,83 +0,0 @@
import { actor } from "rivetkit";
import { db } from "../db/client.js";
import { actors as actorsTable, events as eventsTable } from "../db/schema.js";
import { and, eq, desc } from "drizzle-orm";
type LogEntry = {
ts: number;
level: "info" | "warn" | "error";
msg: string;
};
type SubAgentState = {
parentUserId: string;
type: string;
status: "idle" | "running" | "done" | "error";
channelId: string;
logs: LogEntry[];
};
const initialState: SubAgentState = {
parentUserId: "",
type: "generic",
status: "idle",
channelId: "",
logs: [],
};
// Sub-agent actor mainly exposes status + logs for the UI. The actual task
// execution lives in sub-agent-runner.ts, invoked by the Grow Agent's tool
// dispatch path (PRD §3.3).
export const subAgent = actor({
state: initialState,
actions: {
init: async (
c,
input: { parentUserId: string; type: string; channelId: string },
) => {
c.state.parentUserId = input.parentUserId;
c.state.type = input.type;
c.state.channelId = input.channelId;
c.state.status = "idle";
},
appendLog: async (c, entry: LogEntry) => {
c.state.logs.push(entry);
c.broadcast("log", entry);
},
setStatus: async (c, status: SubAgentState["status"]) => {
c.state.status = status;
c.broadcast("status", { status });
},
getLogs: async (c) => c.state.logs,
getStatus: async (c) => c.state.status,
// Pulls historical events from the DB so a returning user sees prior runs.
getHistory: async (c, input: { subAgentId: string }) => {
const rows = await db
.select()
.from(eventsTable)
.where(
and(
eq(eventsTable.userId, c.state.parentUserId),
eq(eventsTable.actorId, input.subAgentId),
),
)
.orderBy(desc(eventsTable.createdAt))
.limit(50);
return rows;
},
getActorRow: async (c, input: { subAgentId: string }) => {
const row = await db.query.actors.findFirst({
where: and(
eq(actorsTable.userId, c.state.parentUserId),
eq(actorsTable.actorId, input.subAgentId),
),
});
return row;
},
},
});

965
src/actors/user-actor.ts Normal file
View File

@@ -0,0 +1,965 @@
import { actor } from "rivetkit";
import { config } from "../config.js";
import { log } from "../log.js";
import {
buildUnifiedSystemPrompt,
getSubAgentModule,
jobApplicationModuleIds,
type SubAgentModule,
} from "../agents/catalog.js";
import {
getSubAgentModules,
} from "../lib/prompt-loader.js";
import {
buildServiceSessionUrl,
runServiceAgentProbe,
type ServiceAgentResult,
} from "../services/service-agents.js";
import {
provisionUserStack,
getUserStack,
stopUserStack,
giteaClientFor,
opencodeUrlFor,
syncWorkspaceToGit,
} from "../docker/manager.js";
import { db } from "../db/client.js";
import { actors as actorsTable, events as eventsTable } from "../db/schema.js";
import { createChatCompletion, type LlmMessage, type LlmToolCall } from "../lib/llm.js";
// ── Types ──
type ChatTurn = {
role: "user" | "assistant" | "tool";
content: string;
toolCallId?: string;
toolCalls?: LlmToolCall[];
};
type WorkflowStatus = "draft" | "running" | "paused" | "completed";
type ModuleStatus = "idle" | "running" | "blocked" | "done";
type Scorecard = {
id: string;
question: string;
answer: string;
score: number;
notes?: string;
createdAt: string;
};
type WorkflowModuleState = {
id: string;
name: string;
role: string;
service?: string;
status: ModuleStatus;
summary: string;
lastResult?: ServiceAgentResult;
scorecards: Scorecard[];
};
type WorkflowEvent = {
id: string;
ts: string;
moduleId: string;
moduleName: string;
type: "workflow" | "module" | "score";
message: string;
payload?: unknown;
};
type UserActorState = {
userId: string;
goals: string[];
chatHistory: ChatTurn[];
maxHistory: number;
// ── Workflow (was separate workflowJob actor, changes.md §5) ──
workflowId: string;
workflowStatus: WorkflowStatus;
workflowGoal: string;
modules: WorkflowModuleState[];
timeline: WorkflowEvent[];
createdAt: string;
updatedAt: string;
};
// ── Helpers ──
const now = () => new Date().toISOString();
const eventId = () => `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`;
const MEMORY_REPO_PATH_LIMIT = 1024;
// ── Unified user agent tools (changes.md §2D: loaded at build time) ──
// Core memory tools + sub-agent capability tools from the catalog.
// Sub-agent tools are NOT separate actors — they are function tools loaded
// into the unified agent's system prompt at build time.
function buildUnifiedTools(): Array<{
type: "function";
function: {
name: string;
description: string;
parameters: Record<string, unknown>;
};
}> {
const coreTools = [
{
type: "function" as const,
function: {
name: "commit_memory",
description:
"Write or update a file in the user's Git memory repository. Use for goals, decisions, progress notes, plans, and durable summaries.",
parameters: {
type: "object",
properties: {
path: { type: "string" },
content: { type: "string" },
message: { type: "string" },
},
required: ["path", "content", "message"],
},
},
},
{
type: "function" as const,
function: {
name: "read_memory",
description: "Read a single file from the user's memory repo.",
parameters: {
type: "object",
properties: { path: { type: "string" } },
required: ["path"],
},
},
},
{
type: "function" as const,
function: {
name: "list_memory",
description: "List files at a path prefix in the user's memory repo.",
parameters: {
type: "object",
properties: { pathPrefix: { type: "string" } },
required: ["pathPrefix"],
},
},
},
{
type: "function" as const,
function: {
name: "start_workflow",
description: "Start a job application workflow with all sub-agent modules.",
parameters: {
type: "object",
properties: {
goal: { type: "string", description: "Job search goal, e.g. 'Land a high-fit product engineering role'" },
},
required: ["goal"],
},
},
},
{
type: "function" as const,
function: {
name: "run_workflow_module",
description: "Execute a specific sub-agent module in the workflow (e.g., resume, job-search, job-apply, sara, emily, qscore).",
parameters: {
type: "object",
properties: {
moduleId: { type: "string", description: "Module id: resume, job-search, job-apply, sara, emily, qscore" },
},
required: ["moduleId"],
},
},
},
{
type: "function" as const,
function: {
name: "start_interview_session",
description: "Create a real mock interview session via the interview-service microservice.",
parameters: {
type: "object",
properties: { goal: { type: "string" } },
required: ["goal"],
},
},
},
{
type: "function" as const,
function: {
name: "start_roleplay_session",
description: "Create a real mock roleplay session via the roleplay-service microservice.",
parameters: {
type: "object",
properties: { goal: { type: "string" } },
required: ["goal"],
},
},
},
{
type: "function" as const,
function: {
name: "compute_qscore",
description: "Compute or refresh the user's Q Score via the qscore-service microservice.",
parameters: {
type: "object",
properties: {},
required: [],
},
},
},
{
type: "function" as const,
function: {
name: "analyze_resume",
description: "Analyze the user's resume using the Resume Building microservice. Returns completeness score, skill gaps, and optimization recommendations.",
parameters: {
type: "object",
properties: {
goal: { type: "string", description: "Target role or job description for context" },
},
required: ["goal"],
},
},
},
{
type: "function" as const,
function: {
name: "tailor_resume",
description: "Tailor the user's resume for a specific job description or role. Optimizes bullet points, adds keywords, and improves ATS compatibility.",
parameters: {
type: "object",
properties: {
goal: { type: "string", description: "Target role and company for resume tailoring" },
},
required: ["goal"],
},
},
},
{
type: "function" as const,
function: {
name: "start_interview_to_offer",
description: "Start the Interview-to-Offer Accelerator workflow. This is a guided end-to-end pipeline: (1) Analyze and tailor the resume for the role, (2) Create mock interview practice, (3) Create mock roleplay practice, and (4) Compute Q Score readiness. Use this when the user has a specific interview scheduled and wants comprehensive preparation.",
parameters: {
type: "object",
properties: {
goal: { type: "string", description: "The target role and company, e.g. 'Software Engineer at Google' or 'Product Manager at Stripe'" },
job_description: { type: "string", description: "Optional: the job description or key requirements" },
},
required: ["goal"],
},
},
},
];
// Build sub-agent capability tools from the catalog (changes.md §2D).
// Each sub-agent module exposes named tools that the LLM can call directly.
const capabilityTools = getSubAgentModules().flatMap((mod) =>
mod.toolNames.map((toolName) => ({
type: "function" as const,
function: {
name: toolName,
description: `[${mod.name}] ${mod.description}`,
parameters: {
type: "object",
properties: {
goal: { type: "string", description: "The user's current goal or context for this action" },
detail: { type: "string", description: "Additional detail or instruction for this sub-agent capability" },
},
required: ["goal"],
},
},
})),
);
return [...coreTools, ...capabilityTools];
}
// Lazy — prompt modules are loaded from disk at startup (changes.md §3).
// Must be called after initCatalog() has completed.
function getUnifiedTools() {
return buildUnifiedTools();
}
// ── Messages helper ──
function messagesForApi(history: ChatTurn[]): LlmMessage[] {
const systemPrompt = buildUnifiedSystemPrompt();
const messages: LlmMessage[] = [
{ role: "system", content: systemPrompt },
];
for (const turn of history) {
if (turn.role === "tool") {
messages.push({
role: "tool",
content: turn.content,
tool_call_id: turn.toolCallId,
});
continue;
}
messages.push({
role: turn.role,
content: turn.content,
tool_calls: turn.toolCalls?.map((call) => ({
id: call.id,
type: "function" as const,
function: {
name: call.name,
arguments: JSON.stringify(call.arguments),
},
})),
});
}
return messages;
}
// ── Workflow helpers ──
function makeModules(): WorkflowModuleState[] {
return jobApplicationModuleIds()
.map((id) => getSubAgentModule(id))
.filter((m): m is SubAgentModule => Boolean(m))
.map((m) => ({
id: m.id,
name: m.name,
role: m.role,
service: m.service,
status: "idle" as ModuleStatus,
summary: m.description,
scorecards: [],
}));
}
function appendTimelineEvent(
state: UserActorState,
module: Pick<WorkflowModuleState, "id" | "name">,
type: WorkflowEvent["type"],
message: string,
payload?: unknown,
) {
const ev: WorkflowEvent = {
id: eventId(),
ts: now(),
moduleId: module.id,
moduleName: module.name,
type,
message,
payload,
};
state.timeline.unshift(ev);
state.timeline = state.timeline.slice(0, 100);
state.updatedAt = ev.ts;
return ev;
}
// ── Initial State ──
const initialState: UserActorState = {
userId: "",
goals: [],
chatHistory: [],
maxHistory: 40,
workflowId: "",
workflowStatus: "draft",
workflowGoal: "",
modules: [],
timeline: [],
createdAt: "",
updatedAt: "",
};
// ── THE UNIFIED USER ACTOR (changes.md §5) ──
export const userActor = actor({
options: {
actionTimeout: 600_000,
noSleep: true,
},
state: initialState,
actions: {
// ── Infrastructure ──
init: async (c, input: { userId: string }) => {
if (c.state.userId && c.state.userId !== input.userId) {
throw new Error("User actor already bound to a different user");
}
c.state.userId = input.userId;
const stack = await provisionUserStack(input.userId);
await db
.insert(actorsTable)
.values({
actorId: `user-${input.userId}`,
userId: input.userId,
kind: "user",
status: "idle",
lastActivityAt: new Date(),
})
.onConflictDoNothing();
c.broadcast("stack-ready", {
userId: input.userId,
opencode: `${stack.opencodeHost}:${stack.opencodePort}`,
giteaRepo: `${stack.giteaRepoOwner ?? "growqr"}/${stack.giteaRepoName ?? "unknown"}`,
versions: {
image: stack.imageVersion,
migration: stack.migrationVersion,
prompt: stack.promptVersion,
},
});
return stack;
},
shutdown: async (c) => {
if (c.state.userId) await stopUserStack(c.state.userId);
},
// ── Chat (was growAgent.receiveMessage) ──
receiveMessage: async (c, msg: { text: string }) => {
if (!c.state.userId) throw new Error("User actor not initialized");
const userTurn: ChatTurn = { role: "user", content: msg.text };
c.state.chatHistory.push(userTurn);
c.broadcast("message", { role: "user", text: msg.text });
if (!config.llmApiKey) {
const reply = "LLM API key not configured.";
c.state.chatHistory.push({ role: "assistant", content: reply });
c.broadcast("message", { role: "agent", text: reply });
return { reply };
}
c.broadcast("agent-thinking", { state: "running" });
let assistantTextOut = "";
const MAX_ITERATIONS = 8;
for (let i = 0; i < MAX_ITERATIONS; i++) {
const response = await createChatCompletion({
model: config.agentModel,
maxTokens: config.maxAgentTokens,
tools: getUnifiedTools(),
messages: messagesForApi(c.state.chatHistory),
});
if (response.content) {
assistantTextOut += (assistantTextOut ? "\n\n" : "") + response.content;
c.broadcast("message", { role: "agent", text: response.content });
}
c.state.chatHistory.push({
role: "assistant",
content: response.content,
toolCalls: response.toolCalls,
});
if (response.toolCalls.length === 0) break;
for (const call of response.toolCalls) {
try {
const result = await dispatchUnifiedTool(c, call);
c.state.chatHistory.push({
role: "tool",
toolCallId: call.id,
content: typeof result === "string" ? result : JSON.stringify(result),
});
} catch (err) {
log.error({ err, tool: call.name }, "tool dispatch failed");
c.state.chatHistory.push({
role: "tool",
toolCallId: call.id,
content: `Error: ${err instanceof Error ? err.message : String(err)}`,
});
}
}
}
while (c.state.chatHistory.length > c.state.maxHistory) {
c.state.chatHistory.shift();
}
c.broadcast("agent-thinking", { state: "idle" });
await db
.insert(eventsTable)
.values({
userId: c.state.userId,
actorId: `user-${c.state.userId}`,
type: "user.message",
payload: { userText: msg.text, assistantText: assistantTextOut },
});
// Auto-commit conversation to Git (changes.md §7: Git is source of truth).
// Write the full exchange to /conversations/ in the user's repo.
c.waitUntil(
(async () => {
try {
const client = await giteaClientFor(c.state.userId);
const stack = await getUserStack(c.state.userId);
if (client && stack?.giteaRepoOwner && stack.giteaRepoName) {
const ts = new Date().toISOString().replace(/[:.]/g, "-");
const convoPath = `conversations/${ts}.json`;
await client.putFile({
owner: stack.giteaRepoOwner,
repo: stack.giteaRepoName,
path: convoPath,
contentUtf8: JSON.stringify(
{ timestamp: new Date().toISOString(), user: msg.text, assistant: assistantTextOut },
null, 2,
),
message: `conversation: ${msg.text.slice(0, 60)}`,
});
// Sync workspace so OpenCode sees latest state.
await syncWorkspaceToGit(c.state.userId, `conversation at ${ts}`).catch(() => {});
}
} catch (err) {
log.warn({ err, userId: c.state.userId }, "auto-commit conversation failed (non-fatal)");
}
})(),
);
return {
reply: assistantTextOut || "(no response)",
sessions: c.state.modules
.filter(m => m.service && m.lastResult?.detail)
.map(m => {
const detail = m.lastResult!.detail as Record<string, unknown> | undefined;
return {
moduleId: m.id,
moduleName: m.name,
status: m.status,
sessionId: detail?.session_id as string | undefined,
sessionUrl: typeof detail?.ui_session_url === "string"
? detail.ui_session_url
: buildServiceSessionUrl(m.service, detail, c.state.workflowGoal),
summary: m.lastResult?.summary,
};
}),
};
},
// ── Workflow (was workflowJob actor, now part of user actor — changes.md §5) ──
startWorkflow: async (c, input: { goal?: string }) => {
const goal = input.goal ?? "Find and apply to high-fit jobs";
c.state.workflowId = `job-application:${c.state.userId}`;
c.state.workflowStatus = "running";
c.state.workflowGoal = goal;
c.state.modules = makeModules();
c.state.createdAt = now();
c.state.updatedAt = now();
appendTimelineEvent(
c.state,
{ id: "grow", name: "Grow" },
"workflow",
"Job application workflow started.",
);
c.broadcast("workflow.updated", {
workflowId: c.state.workflowId,
userId: c.state.userId,
status: c.state.workflowStatus,
goal: c.state.workflowGoal,
agents: c.state.modules,
timeline: c.state.timeline,
updatedAt: c.state.updatedAt,
});
return c.state;
},
pauseWorkflow: async (c) => {
c.state.workflowStatus = "paused";
appendTimelineEvent(c.state, { id: "grow", name: "Grow" }, "workflow", "Workflow paused.");
c.broadcast("workflow.updated", workflowSnapshot(c.state));
return c.state;
},
resumeWorkflow: async (c) => {
c.state.workflowStatus = "running";
appendTimelineEvent(c.state, { id: "grow", name: "Grow" }, "workflow", "Workflow resumed.");
c.broadcast("workflow.updated", workflowSnapshot(c.state));
return c.state;
},
runWorkflowModule: async (c, input: { moduleId: string }) => {
const mod = c.state.modules.find((m) => m.id === input.moduleId);
if (!mod) throw new Error(`Unknown workflow module: ${input.moduleId}`);
mod.status = "running";
appendTimelineEvent(c.state, mod, "module", `${mod.name} started.`);
c.broadcast("workflow.updated", workflowSnapshot(c.state));
const subModule = getSubAgentModule(mod.id);
if (subModule?.service) {
const userId = c.state.userId;
const goal = c.state.workflowGoal;
c.waitUntil(
(async () => {
const result = await runServiceAgentProbe(
{ id: subModule.id, name: subModule.name, role: subModule.role, kind: "microservice", description: subModule.description, service: subModule.service },
{ userId, goal },
);
mod.lastResult = result;
mod.status = result.status === "unavailable" ? "blocked" : "done";
appendTimelineEvent(c.state, mod, "module", result.summary, result.detail);
c.broadcast("workflow.updated", workflowSnapshot(c.state));
await c.saveState({ immediate: true });
})(),
);
return c.state;
}
// Local workflow modules
mod.lastResult = {
status: "local",
summary: `${mod.name} completed a local workflow step for "${c.state.workflowGoal}".`,
};
mod.status = "done";
appendTimelineEvent(c.state, mod, "module", mod.lastResult.summary);
c.broadcast("workflow.updated", workflowSnapshot(c.state));
return c.state;
},
recordQaScore: async (c, input: { moduleId: string; question: string; answer: string; score: number; notes?: string }) => {
const mod = c.state.modules.find((m) => m.id === input.moduleId);
if (!mod) throw new Error(`Unknown workflow module: ${input.moduleId}`);
const card: Scorecard = {
id: `score_${Date.now()}`,
question: input.question,
answer: input.answer,
score: Math.max(0, Math.min(100, Number(input.score))),
notes: input.notes,
createdAt: now(),
};
mod.scorecards.unshift(card);
appendTimelineEvent(c.state, mod, "score", `${mod.name} recorded Q&A score ${card.score}.`, card);
c.broadcast("workflow.updated", workflowSnapshot(c.state));
return c.state;
},
getWorkflowStatus: async (c) => workflowSnapshot(c.state),
getHistory: async (c) => c.state.chatHistory,
getGoals: async (c) => c.state.goals,
},
});
// ── Helpers ──
function workflowSnapshot(state: UserActorState) {
return {
workflowId: state.workflowId,
userId: state.userId,
status: state.workflowStatus,
goal: state.workflowGoal,
agents: state.modules,
timeline: state.timeline,
updatedAt: state.updatedAt,
};
}
async function dispatchUnifiedTool(
c: { state: UserActorState; broadcast: (event: string, data: unknown) => void },
call: LlmToolCall,
): Promise<unknown> {
const input = call.arguments;
const userId = c.state.userId;
switch (call.name) {
case "commit_memory": {
const path = String(input.path ?? "").slice(0, MEMORY_REPO_PATH_LIMIT);
const content = String(input.content ?? "");
const message = String(input.message ?? "memory update");
const client = await giteaClientFor(userId);
const stack = await getUserStack(userId);
if (!client || !stack?.giteaRepoOwner || !stack.giteaRepoName) {
return { ok: false, error: "memory repo not provisioned" };
}
const result = await client.putFile({
owner: stack.giteaRepoOwner,
repo: stack.giteaRepoName,
path,
contentUtf8: content,
message,
});
c.broadcast("memory-committed", { path, message });
return { ok: true, path, commitSha: result.commitSha };
}
case "read_memory": {
const path = String(input.path ?? "");
const client = await giteaClientFor(userId);
const stack = await getUserStack(userId);
if (!client || !stack?.giteaRepoOwner || !stack.giteaRepoName) return null;
return client.readFile({ owner: stack.giteaRepoOwner, repo: stack.giteaRepoName, path });
}
case "list_memory": {
const pathPrefix = String(input.pathPrefix ?? "");
const client = await giteaClientFor(userId);
const stack = await getUserStack(userId);
if (!client || !stack?.giteaRepoOwner || !stack.giteaRepoName) return [];
try {
const res = await fetch(
`${config.giteaUrl}/api/v1/repos/${encodeURIComponent(stack.giteaRepoOwner)}/${encodeURIComponent(stack.giteaRepoName)}/contents/${encodeURI(pathPrefix)}`,
{ headers: { authorization: `token ${config.giteaAdminToken}`, accept: "application/json" } },
);
if (!res.ok) return [];
const entries = (await res.json()) as Array<{ name: string; path: string; type: string }>;
return entries.map((e) => ({ name: e.name, path: e.path, type: e.type }));
} catch { return []; }
}
case "start_workflow": {
const goal = String(input.goal ?? "Find and apply to high-fit jobs");
c.state.workflowId = `job-application:${userId}`;
c.state.workflowStatus = "running";
c.state.workflowGoal = goal;
c.state.modules = makeModules();
c.state.createdAt = now();
c.state.updatedAt = now();
appendTimelineEvent(c.state, { id: "grow", name: "Grow" }, "workflow", "Workflow started via LLM tool.");
c.broadcast("workflow.updated", workflowSnapshot(c.state));
return { ok: true, workflowId: c.state.workflowId, goal };
}
case "run_workflow_module": {
const moduleId = String(input.moduleId ?? "");
const mod = c.state.modules.find((m) => m.id === moduleId);
if (!mod) return { ok: false, error: `Unknown module: ${moduleId}` };
mod.status = "running";
appendTimelineEvent(c.state, mod, "module", `${mod.name} started via LLM tool.`);
c.broadcast("workflow.updated", workflowSnapshot(c.state));
const subModule = getSubAgentModule(mod.id);
if (subModule?.service) {
const result = await runServiceAgentProbe(
{ id: subModule.id, name: subModule.name, role: subModule.role, kind: "microservice", description: subModule.description, service: subModule.service },
{ userId, goal: c.state.workflowGoal },
);
mod.lastResult = result;
mod.status = result.status === "unavailable" ? "blocked" : "done";
appendTimelineEvent(c.state, mod, "module", result.summary, result.detail);
} else {
mod.lastResult = { status: "local", summary: `${mod.name} completed a local workflow step.` };
mod.status = "done";
appendTimelineEvent(c.state, mod, "module", mod.lastResult.summary);
}
c.broadcast("workflow.updated", workflowSnapshot(c.state));
return { ok: true, moduleId, status: mod.status };
}
case "start_interview_session": {
const goal = String(input.goal ?? "");
const saraModule = getSubAgentModule("sara");
if (!saraModule?.service) return { ok: false, error: "Mock Interview module not available" };
const result = await runServiceAgentProbe(
{ id: saraModule.id, name: saraModule.name, role: saraModule.role, kind: "microservice", description: saraModule.description, service: saraModule.service },
{ userId, goal },
);
c.broadcast("service-result", { moduleId: "sara", result });
return result;
}
case "start_roleplay_session": {
const goal = String(input.goal ?? "");
const emilyModule = getSubAgentModule("emily");
if (!emilyModule?.service) return { ok: false, error: "Mock Roleplay module not available" };
const result = await runServiceAgentProbe(
{ id: emilyModule.id, name: emilyModule.name, role: emilyModule.role, kind: "microservice", description: emilyModule.description, service: emilyModule.service },
{ userId, goal },
);
c.broadcast("service-result", { moduleId: "emily", result });
return result;
}
case "compute_qscore": {
const quinnModule = getSubAgentModule("qscore");
if (!quinnModule?.service) return { ok: false, error: "Q Score module not available" };
const result = await runServiceAgentProbe(
{ id: quinnModule.id, name: quinnModule.name, role: quinnModule.role, kind: "score", description: quinnModule.description, service: quinnModule.service },
{ userId, goal: c.state.workflowGoal || "general assessment" },
);
c.broadcast("service-result", { moduleId: "qscore", result });
return result;
}
case "analyze_resume": {
const goal = String(input.goal ?? c.state.workflowGoal ?? "general");
const resumeModule = getSubAgentModule("resume");
if (!resumeModule) return { ok: false, error: "Resume module not available" };
const result = await runServiceAgentProbe(
{ id: resumeModule.id, name: resumeModule.name, role: resumeModule.role, kind: "microservice", description: resumeModule.description, service: resumeModule.service },
{ userId, goal },
);
c.broadcast("service-result", { moduleId: "resume", result });
return result;
}
case "tailor_resume": {
const goal = String(input.goal ?? c.state.workflowGoal ?? "general");
const resumeModule = getSubAgentModule("resume");
if (!resumeModule) return { ok: false, error: "Resume module not available" };
const result = await runServiceAgentProbe(
{ id: resumeModule.id, name: resumeModule.name, role: resumeModule.role, kind: "microservice", description: resumeModule.description, service: resumeModule.service },
{ userId, goal },
);
c.broadcast("service-result", { moduleId: "resume", result });
return result;
}
case "start_interview_to_offer": {
const goal = String(input.goal ?? "");
const jobDesc = String(input.job_description ?? "");
// Start the workflow
c.state.workflowId = `interview-to-offer:${userId}`;
c.state.workflowStatus = "running";
c.state.workflowGoal = goal;
c.state.modules = makeModules();
c.state.createdAt = now();
c.state.updatedAt = now();
appendTimelineEvent(c.state, { id: "grow", name: "Grow" }, "workflow", `Interview-to-Offer workflow started for: ${goal}`);
// Step 1: Resume Building — analyze and tailor
const resumeModule = getSubAgentModule("resume");
const resumeMod = c.state.modules.find(m => m.id === "resume");
if (resumeMod && resumeModule) {
resumeMod.status = "running";
appendTimelineEvent(c.state, resumeMod, "module", "Resume Building is analyzing your profile...");
c.broadcast("workflow.updated", workflowSnapshot(c.state));
try {
const resumeResult = await runServiceAgentProbe(
{ id: resumeModule.id, name: resumeModule.name, role: resumeModule.role, kind: "microservice", description: resumeModule.description, service: resumeModule.service },
{ userId, goal },
);
resumeMod.lastResult = resumeResult;
resumeMod.status = resumeResult.status === "unavailable" ? "blocked" : "done";
appendTimelineEvent(c.state, resumeMod, "module", resumeResult.summary);
} catch (err) {
resumeMod.status = "blocked";
appendTimelineEvent(c.state, resumeMod, "module", `Resume Building failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
c.broadcast("workflow.updated", workflowSnapshot(c.state));
// Step 2: Mock Interview — create interview session
const saraModule = getSubAgentModule("sara");
const saraMod = c.state.modules.find(m => m.id === "sara");
if (saraMod && saraModule?.service) {
saraMod.status = "running";
appendTimelineEvent(c.state, saraMod, "module", "Mock Interview is creating an interview practice session...");
c.broadcast("workflow.updated", workflowSnapshot(c.state));
try {
const saraResult = await runServiceAgentProbe(
{ id: saraModule.id, name: saraModule.name, role: saraModule.role, kind: "microservice", description: saraModule.description, service: saraModule.service },
{ userId, goal: goal + (jobDesc ? `\nJob Description: ${jobDesc}` : "") },
);
saraMod.lastResult = saraResult;
saraMod.status = saraResult.status === "unavailable" ? "blocked" : "done";
appendTimelineEvent(c.state, saraMod, "module", saraResult.summary);
} catch (err) {
saraMod.status = "blocked";
appendTimelineEvent(c.state, saraMod, "module", `Mock Interview session failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
c.broadcast("workflow.updated", workflowSnapshot(c.state));
// Step 3: Mock Roleplay — create roleplay session
const emilyModule = getSubAgentModule("emily");
const emilyMod = c.state.modules.find(m => m.id === "emily");
if (emilyMod && emilyModule?.service) {
emilyMod.status = "running";
appendTimelineEvent(c.state, emilyMod, "module", "Mock Roleplay is creating a practice scenario...");
c.broadcast("workflow.updated", workflowSnapshot(c.state));
try {
const emilyResult = await runServiceAgentProbe(
{ id: emilyModule.id, name: emilyModule.name, role: emilyModule.role, kind: "microservice", description: emilyModule.description, service: emilyModule.service },
{ userId, goal: `Interview negotiation and communication practice for: ${goal}` },
);
emilyMod.lastResult = emilyResult;
emilyMod.status = emilyResult.status === "unavailable" ? "blocked" : "done";
appendTimelineEvent(c.state, emilyMod, "module", emilyResult.summary);
} catch (err) {
emilyMod.status = "blocked";
appendTimelineEvent(c.state, emilyMod, "module", `Mock Roleplay session failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
c.broadcast("workflow.updated", workflowSnapshot(c.state));
// Step 4: Q Score — compute readiness
const quinnModule = getSubAgentModule("qscore");
const quinnMod = c.state.modules.find(m => m.id === "qscore");
if (quinnMod && quinnModule?.service) {
quinnMod.status = "running";
appendTimelineEvent(c.state, quinnMod, "module", "Q Score is computing your readiness score...");
c.broadcast("workflow.updated", workflowSnapshot(c.state));
try {
const quinnResult = await runServiceAgentProbe(
{ id: quinnModule.id, name: quinnModule.name, role: quinnModule.role, kind: "score", description: quinnModule.description, service: quinnModule.service },
{ userId, goal },
);
quinnMod.lastResult = quinnResult;
quinnMod.status = quinnResult.status === "unavailable" ? "blocked" : "done";
appendTimelineEvent(c.state, quinnMod, "module", quinnResult.summary);
} catch (err) {
quinnMod.status = "blocked";
appendTimelineEvent(c.state, quinnMod, "module", `Q Score computation failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
c.broadcast("workflow.updated", workflowSnapshot(c.state));
const doneCount = c.state.modules.filter(m => m.status === "done").length;
const totalModules = c.state.modules.filter(m => m.service).length;
const blockedCount = c.state.modules.filter(m => m.status === "blocked").length;
return {
ok: true,
workflowId: c.state.workflowId,
goal,
modulesCompleted: doneCount,
totalServiceModules: totalModules,
blocked: blockedCount,
summary: `Interview-to-Offer workflow completed ${doneCount}/${totalModules} service modules${blockedCount > 0 ? ` (${blockedCount} unavailable)` : ""}.`,
timeline: c.state.timeline,
modules: c.state.modules.filter(m => m.service).map(m => ({
id: m.id,
name: m.name,
status: m.status,
summary: m.lastResult?.summary ?? m.summary,
})),
};
}
default: {
// Check if this is a sub-agent capability tool from the catalog (changes.md §2D).
// These tools are loaded at build time — each sub-agent module defines its own tool names.
const owningModule = getSubAgentModules().find((m) => m.toolNames.includes(call.name));
if (owningModule) {
const goal = String(input.goal ?? c.state.workflowGoal ?? "general task");
const detail = String(input.detail ?? "");
log.info({ tool: call.name, moduleId: owningModule.id, goal }, "sub-agent capability tool invoked");
return {
ok: true,
moduleId: owningModule.id,
tool: call.name,
summary: `${owningModule.name} processed "${goal}"${detail ? ` with detail: "${detail}"` : ""} via the ${call.name} capability.`,
};
}
throw new Error(`unknown tool: ${call.name}`);
}
}
}

View File

@@ -1,292 +0,0 @@
import { actor } from "rivetkit";
import {
agentCatalog,
getAgentProfile,
jobApplicationAgentIds,
type AgentProfile,
} from "../agents/catalog.js";
import { runServiceAgentProbe, type ServiceAgentResult } from "../services/service-agents.js";
type WorkflowStatus = "draft" | "running" | "paused" | "completed";
type AgentStatus = "idle" | "running" | "blocked" | "done";
type AgentScorecard = {
id: string;
question: string;
answer: string;
score: number;
notes?: string;
createdAt: string;
};
type WorkflowAgentState = {
id: string;
name: string;
role: string;
kind: AgentProfile["kind"];
service?: AgentProfile["service"];
status: AgentStatus;
summary: string;
lastResult?: ServiceAgentResult;
scorecards: AgentScorecard[];
};
type WorkflowEvent = {
id: string;
ts: string;
agentId: string;
agentName: string;
type: "workflow" | "agent" | "score";
message: string;
payload?: unknown;
};
type WorkflowJobState = {
workflowId: string;
userId: string;
type: "job-application";
status: WorkflowStatus;
goal: string;
agents: WorkflowAgentState[];
timeline: WorkflowEvent[];
createdAt: string;
updatedAt: string;
};
const now = () => new Date().toISOString();
const initialState: WorkflowJobState = {
workflowId: "",
userId: "",
type: "job-application",
status: "draft",
goal: "",
agents: [],
timeline: [],
createdAt: "",
updatedAt: "",
};
function eventId() {
return `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`;
}
function makeAgents(): WorkflowAgentState[] {
return jobApplicationAgentIds()
.map((id) => getAgentProfile(id))
.filter((agent): agent is AgentProfile => Boolean(agent))
.map((agent) => ({
id: agent.id,
name: agent.name,
role: agent.role,
kind: agent.kind,
service: agent.service,
status: "idle",
summary: agent.description,
scorecards: [],
}));
}
function appendEvent(
state: WorkflowJobState,
agent: Pick<WorkflowAgentState, "id" | "name">,
type: WorkflowEvent["type"],
message: string,
payload?: unknown,
) {
const ev: WorkflowEvent = {
id: eventId(),
ts: now(),
agentId: agent.id,
agentName: agent.name,
type,
message,
payload,
};
state.timeline.unshift(ev);
state.timeline = state.timeline.slice(0, 100);
state.updatedAt = ev.ts;
return ev;
}
function localAgentResult(agent: WorkflowAgentState, goal: string): ServiceAgentResult {
const goalText = goal || "job application workflow";
switch (agent.id) {
case "grow":
return {
status: "local",
summary: `Grow Agent initialized the ${goalText} workflow and assigned specialist agents.`,
};
case "resume":
return {
status: "local",
summary: `Resume Agent prepared a resume-improvement pass for ${goalText}.`,
};
case "job-search":
return {
status: "local",
summary: `Job Search Agent created the opportunity discovery lane for ${goalText}.`,
};
case "job-apply":
return {
status: "local",
summary: `Job Apply Agent prepared the application tracking lane for ${goalText}.`,
};
default:
return {
status: "local",
summary: `${agent.name} completed a local workflow step.`,
};
}
}
export const workflowJob = actor({
options: {
actionTimeout: 600_000,
noSleep: true,
},
state: initialState,
actions: {
init: async (
c,
input: {
userId: string;
goal?: string;
},
) => {
if (c.state.userId && c.state.userId !== input.userId) {
throw new Error("Workflow already belongs to another user");
}
if (!c.state.workflowId) {
const ts = now();
c.state.workflowId = `job-application:${input.userId}`;
c.state.userId = input.userId;
c.state.type = "job-application";
c.state.goal = input.goal ?? "Find and apply to high-fit jobs";
c.state.status = "draft";
c.state.agents = makeAgents();
c.state.createdAt = ts;
c.state.updatedAt = ts;
appendEvent(
c.state,
{ id: "grow", name: "Grow Agent" },
"workflow",
"Job application workflow created.",
{ catalog: agentCatalog },
);
} else if (input.goal) {
c.state.goal = input.goal;
c.state.updatedAt = now();
}
c.broadcast("workflow.updated", c.state);
return c.state;
},
start: async (c) => {
c.state.status = "running";
appendEvent(
c.state,
{ id: "grow", name: "Grow Agent" },
"workflow",
"Workflow started.",
);
c.broadcast("workflow.updated", c.state);
return c.state;
},
pause: async (c) => {
c.state.status = "paused";
appendEvent(
c.state,
{ id: "grow", name: "Grow Agent" },
"workflow",
"Workflow paused.",
);
c.broadcast("workflow.updated", c.state);
return c.state;
},
resume: async (c) => {
c.state.status = "running";
appendEvent(
c.state,
{ id: "grow", name: "Grow Agent" },
"workflow",
"Workflow resumed.",
);
c.broadcast("workflow.updated", c.state);
return c.state;
},
runAgent: async (c, input: { agentId: string }) => {
const agent = c.state.agents.find((item) => item.id === input.agentId);
if (!agent) throw new Error(`Unknown workflow agent: ${input.agentId}`);
agent.status = "running";
appendEvent(c.state, agent, "agent", `${agent.name} started.`);
c.broadcast("workflow.updated", c.state);
const profile = getAgentProfile(agent.id);
if (profile?.service != null) {
const userId = c.state.userId;
const goal = c.state.goal;
c.waitUntil(
(async () => {
const result = await runServiceAgentProbe(profile, {
userId,
goal,
});
agent.lastResult = result;
agent.status = result.status === "unavailable" ? "blocked" : "done";
appendEvent(c.state, agent, "agent", result.summary, result.detail);
c.broadcast("workflow.updated", c.state);
await c.saveState({ immediate: true });
})(),
);
return c.state;
}
const result = localAgentResult(agent, c.state.goal);
agent.lastResult = result;
agent.status = result.status === "unavailable" ? "blocked" : "done";
appendEvent(c.state, agent, "agent", result.summary, result.detail);
c.broadcast("workflow.updated", c.state);
return c.state;
},
recordQaScore: async (
c,
input: {
agentId: string;
question: string;
answer: string;
score: number;
notes?: string;
},
) => {
const agent = c.state.agents.find((item) => item.id === input.agentId);
if (!agent) throw new Error(`Unknown workflow agent: ${input.agentId}`);
const card: AgentScorecard = {
id: `score_${Date.now()}`,
question: input.question,
answer: input.answer,
score: Math.max(0, Math.min(100, Number(input.score))),
notes: input.notes,
createdAt: now(),
};
agent.scorecards.unshift(card);
appendEvent(
c.state,
agent,
"score",
`${agent.name} recorded Q&A score ${card.score}.`,
card,
);
c.broadcast("workflow.updated", c.state);
return c.state;
},
getStatus: async (c) => c.state,
},
});

View File

@@ -1,94 +1,51 @@
export type AgentKind =
| "master"
| "workflow"
| "microservice"
| "score";
// ── Sub-agent prompt module catalog (changes.md §2D + §3) ──
// Sub-agents are NOT separate actors. They are prompt modules loaded into
// the unified user agent's system prompt.
//
// Per changes.md §3: prompts and agent definitions are stored as files on disk
// (prompts/system.txt, agents/*.md), loaded at startup, and embedded into the
// Docker image at build time via COPY directives.
//
// This module delegates to src/lib/prompt-loader.ts which reads from the
// filesystem. To update prompts or agents, edit the files and rebuild the
// Docker image — no code changes required.
export type AgentProfile = {
id: string;
name: string;
role: string;
kind: AgentKind;
description: string;
service?: "interview-service" | "roleplay-service" | "qscore-service";
};
import {
getSubAgentModules,
getUnifiedSystemPrompt,
getSubAgentModule as loaderGetSubAgentModule,
jobApplicationModuleIds as loaderJobApplicationModuleIds,
type SubAgentModule,
} from "../lib/prompt-loader.js";
export const agentCatalog = [
{
id: "grow",
name: "Grow Agent",
role: "Master Orchestrator",
kind: "master",
description:
"Owns user context, routes work to sub-agents, commits durable memory, and tracks workflow progress.",
},
{
id: "resume",
name: "Resume Agent",
role: "Resume Builder",
kind: "workflow",
description:
"Turns profile context, Q-Score gaps, and target roles into resume edits and application collateral.",
},
{
id: "job-search",
name: "Job Search Agent",
role: "Opportunity Scout",
kind: "workflow",
description:
"Finds relevant jobs, ranks opportunities, and prepares a shortlist for the application workflow.",
},
{
id: "job-apply",
name: "Job Apply Agent",
role: "Application Operator",
kind: "workflow",
description:
"Prepares tailored applications, tracks submissions, and records follow-up tasks.",
},
{
id: "sara",
name: "Sara",
role: "Interview Agent",
kind: "microservice",
service: "interview-service",
description:
"Runs interview practice through the interview-service microservice and owns interview Q&A feedback.",
},
{
id: "emily",
name: "Emily",
role: "Roleplay Agent",
kind: "microservice",
service: "roleplay-service",
description:
"Runs roleplay practice through the roleplay-service microservice and owns scenario feedback.",
},
{
id: "qscore",
name: "Quinn",
role: "Q-Score Agent",
kind: "score",
service: "qscore-service",
description:
"Computes and explains Q-Score changes, then displays Q&A and scores under the owning agent.",
},
] as const satisfies AgentProfile[];
export type { SubAgentModule };
export type SubAgentId = string;
export type AgentId = (typeof agentCatalog)[number]["id"];
// Re-exported — subAgentModules is now loaded from disk at startup.
// Callers that need the module list at runtime (e.g., user-actor.ts to
// register tools) should use getSubAgentModules() from prompt-loader directly.
export const subAgentModules: SubAgentModule[] = [];
export function getAgentProfile(id: string): AgentProfile | undefined {
return agentCatalog.find((agent) => agent.id === id);
// Initialize from disk. Called once at startup by index.ts.
// After this call, subAgentModules is populated and all functions work.
export async function initCatalog(): Promise<void> {
const { loadPromptsFromDisk } = await import("../lib/prompt-loader.js");
await loadPromptsFromDisk();
// Mutate the exported array so existing imports keep working.
const loaded = getSubAgentModules();
subAgentModules.length = 0;
subAgentModules.push(...loaded);
}
export function jobApplicationAgentIds(): AgentId[] {
return [
"grow",
"resume",
"job-search",
"job-apply",
"sara",
"emily",
"qscore",
];
export function getSubAgentModule(id: string): SubAgentModule | undefined {
return loaderGetSubAgentModule(id);
}
export function jobApplicationModuleIds(): string[] {
return loaderJobApplicationModuleIds();
}
// Build the unified Grow system prompt from disk (changes.md §3).
export function buildUnifiedSystemPrompt(): string {
return getUnifiedSystemPrompt();
}

View File

@@ -25,7 +25,7 @@ export const requireUser = createMiddleware<AuthContext>(async (c, next) => {
const auth = c.req.header("authorization") ?? "";
const token = auth.replace(/^Bearer\s+/i, "").trim();
// Service-to-service path (Grow Agent actor calling backend).
// Service-to-service path (Grow stack calling backend).
// Header `x-growqr-user` is REQUIRED so we can scope the call.
if (
token &&

View File

@@ -16,7 +16,7 @@ export const config = {
// Postgres metadata DB (users, registry, container mappings).
databaseUrl:
process.env.DATABASE_URL ??
"postgres://growqr:growqr@localhost:5432/growqr",
"***************************************/growqr",
// Clerk auth.
clerkSecretKey: process.env.CLERK_SECRET_KEY ?? "",
@@ -25,7 +25,7 @@ export const config = {
serviceToken: process.env.SERVICE_TOKEN ?? "",
a2aAllowedKey: process.env.A2A_ALLOWED_KEY ?? "dev-a2a-key",
// LLM gateway for Grow Agent + sub-agent planning calls.
// LLM gateway for the unified user agent.
llmProvider: process.env.LLM_PROVIDER ?? "opencode",
llmApiKey:
process.env.LLM_API_KEY ??
@@ -36,14 +36,10 @@ export const config = {
process.env.OPENCODE_BASE_URL ??
"https://opencode.ai/zen/v1",
opencodeApiKey: process.env.OPENCODE_API_KEY ?? "",
growAgentModel:
agentModel:
process.env.GROW_AGENT_MODEL ??
process.env.LLM_MODEL ??
"kimi-k2.6",
subAgentModel:
process.env.SUB_AGENT_MODEL ??
process.env.LLM_MODEL ??
"kimi-k2.6",
// Rivet Kit engine endpoint (self-hosted in docker-compose).
rivetEndpoint: process.env.RIVET_ENDPOINT ?? "http://localhost:6420",
@@ -58,15 +54,34 @@ export const config = {
process.env.ROLEPLAY_SERVICE_URL ?? "http://localhost:8008",
qscoreServiceUrl:
process.env.QSCORE_SERVICE_URL ?? "http://localhost:8000",
resumeServiceUrl:
process.env.RESUME_SERVICE_URL ?? "http://localhost:8002",
matchmakingServiceUrl:
process.env.MATCHMAKING_SERVICE_URL ?? "http://localhost:8006",
workflowsDashboardUrl:
process.env.WORKFLOWS_DASHBOARD_URL ??
process.env.FRONTEND_ORIGIN ??
"http://localhost:3000",
// Per-user container images.
giteaImage: process.env.GITEA_IMAGE ?? "gitea/gitea:1.22",
// ── Central Gitea (one org-wide instance, changes.md §2A) ──
giteaUrl: process.env.GITEA_URL ?? "http://127.0.0.1:3001",
giteaAdminUser: process.env.GITEA_ADMIN_USER ?? "growqr-admin",
giteaAdminPassword: process.env.GITEA_ADMIN_PASSWORD ?? "growqr-admin-dev",
giteaAdminToken: process.env.GITEA_ADMIN_TOKEN ?? "",
giteaOrgName: process.env.GITEA_ORG_NAME ?? "growqr",
// ── Shared OpenCode runtime image (built once, changes.md §3) ──
opencodeImage:
process.env.OPENCODE_IMAGE ?? "ghcr.io/anomalyco/opencode:latest",
process.env.OPENCODE_IMAGE ?? "growqr/opencode:dev",
// Version tracking for rollout (changes.md §9)
opencodeImageVersion: process.env.OPENCODE_IMAGE_VERSION ?? "1.0.0",
migrationVersion: process.env.MIGRATION_VERSION ?? "1",
promptVersion: process.env.PROMPT_VERSION ?? "1",
// Host that user containers expose ports on (the host running Docker).
userContainerHost: process.env.USER_CONTAINER_HOST ?? "127.0.0.1",
userDataRoot: process.env.USER_DATA_ROOT ?? "./.data/users",
// Port range for per-user OpenCode containers only (Gitea is shared).
userPortRangeStart: Number(process.env.USER_PORT_RANGE_START ?? 20000),
userPortRangeEnd: Number(process.env.USER_PORT_RANGE_END ?? 29999),

View File

@@ -30,8 +30,9 @@ export const users = pgTable(
}),
);
// One per user. Tracks the user's Grow Agent's container stack + Gitea creds.
// PRD §3.2 + §5.2.
// One per user. Tracks the user's unified agent's container stack + Git repo.
// Per changes.md §2A: per-user Gitea containers removed; central Gitea shared.
// Per changes.md §5: ONE actor per user manages the full orchestration layer.
export const userStacks = pgTable(
"user_stacks",
{
@@ -44,15 +45,11 @@ export const userStacks = pgTable(
.notNull()
.default("provisioning"),
giteaContainerId: text("gitea_container_id"),
giteaContainerName: text("gitea_container_name"),
giteaHost: text("gitea_host"),
giteaHttpPort: integer("gitea_http_port"),
giteaSshPort: integer("gitea_ssh_port"),
giteaAdminUser: text("gitea_admin_user"),
giteaAdminToken: text("gitea_admin_token"),
giteaMemoryRepo: text("gitea_memory_repo"),
// Central Gitea (shared org-wide, changes.md §2A).
giteaRepoName: text("gitea_repo_name"),
giteaRepoOwner: text("gitea_repo_owner"),
// Per-user OpenCode container (from shared image, changes.md §3).
opencodeContainerId: text("opencode_container_id"),
opencodeContainerName: text("opencode_container_name"),
opencodeHost: text("opencode_host"),
@@ -60,6 +57,12 @@ export const userStacks = pgTable(
opencodePassword: text("opencode_password"),
workspacePath: text("workspace_path"),
// Version tracking for image rollouts (changes.md §9).
imageVersion: text("image_version"),
migrationVersion: text("migration_version"),
promptVersion: text("prompt_version"),
lastError: text("last_error"),
createdAt: timestamp("created_at", { withTimezone: true })
@@ -74,8 +77,8 @@ export const userStacks = pgTable(
}),
);
// PRD §5.2 actor registry. One Grow Agent row per user; sub-agents are
// child rows keyed by (userId, actorId).
// Per changes.md §5: ONE unified actor per user (no separate grow/sub actors).
// The actor manages: infra state, git state, runtime comms, migrations, API orchestration.
export const actors = pgTable(
"actors",
{
@@ -83,15 +86,12 @@ export const actors = pgTable(
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
kind: text("kind", { enum: ["grow", "sub"] }).notNull(),
subType: text("sub_type"), // for sub-agents: "coding", "repo", "quest", ...
kind: text("kind", { enum: ["user"] }).notNull().default("user"),
status: text("status", {
enum: ["idle", "running", "done", "error"],
})
.notNull()
.default("idle"),
channelId: text("channel_id"),
parentActorId: text("parent_actor_id"),
lastActivityAt: timestamp("last_activity_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()

View File

@@ -14,24 +14,17 @@ export type { UserStack };
const docker = new Docker();
// Allocated host ports kept in-memory; rehydrated from the DB on boot so
// we don't double-allocate across restarts.
// ── Port allocator (OpenCode containers only; Gitea is central) ──
const allocatedPorts = new Set<number>();
export async function hydratePortAllocator(): Promise<void> {
const rows = await db
.select({
giteaHttp: userStacks.giteaHttpPort,
giteaSsh: userStacks.giteaSshPort,
opencode: userStacks.opencodePort,
})
.select({ opencode: userStacks.opencodePort })
.from(userStacks);
for (const r of rows) {
for (const p of [r.giteaHttp, r.giteaSsh, r.opencode]) {
if (p) allocatedPorts.add(p);
}
if (r.opencode) allocatedPorts.add(r.opencode);
}
log.info({ count: allocatedPorts.size }, "hydrated port allocator");
log.info({ count: allocatedPorts.size }, "hydrated port allocator (OpenCode only)");
}
function pickPort(): number {
@@ -48,6 +41,8 @@ function releasePort(port: number | null | undefined) {
if (port != null) allocatedPorts.delete(port);
}
// ── Image helpers ──
async function ensureImage(image: string) {
try {
await docker.getImage(image).inspect();
@@ -71,7 +66,6 @@ function userDataDir(userId: string) {
}
function safeContainerName(prefix: string, userId: string) {
// Container names must match [a-zA-Z0-9_.-]
return `${prefix}-${userId.replace(/[^a-zA-Z0-9_.-]/g, "_")}`;
}
@@ -83,77 +77,97 @@ async function findExistingContainer(name: string) {
return list[0];
}
async function startGiteaContainer(opts: {
userId: string;
httpPort: number;
sshPort: number;
}): Promise<{ id: string; name: string }> {
await ensureImage(config.giteaImage);
const name = safeContainerName("growqr-gitea", opts.userId);
const dataDir = path.join(userDataDir(opts.userId), "gitea");
await ensureDir(dataDir);
// ── Central Gitea bootstrap (changes.md §2A) ──
const existing = await findExistingContainer(name);
if (existing) {
if (existing.State !== "running") {
await docker.getContainer(existing.Id).start().catch(() => undefined);
let centralGiteaClient: GiteaClient | null = null;
let centralGiteaReady = false;
async function getCentralGiteaClient(): Promise<GiteaClient> {
if (!centralGiteaClient) {
const token = config.giteaAdminToken;
if (token) {
centralGiteaClient = new GiteaClient(config.giteaUrl, { kind: "token", token });
} else {
centralGiteaClient = new GiteaClient(config.giteaUrl, {
kind: "basic",
username: config.giteaAdminUser,
password: config.giteaAdminPassword,
});
}
return { id: existing.Id, name };
}
return centralGiteaClient;
}
export async function ensureCentralGiteaReady(): Promise<void> {
if (centralGiteaReady) return;
await waitForGitea(config.giteaUrl, 120_000);
const client = await getCentralGiteaClient();
// Ensure the org exists (changes.md §2A: single org manages all users).
try {
await client.ensureOrg(config.giteaOrgName);
} catch (err) {
log.warn({ err }, "central Gitea org ensure failed (may already exist)");
}
const container = await docker.createContainer({
name,
Image: config.giteaImage,
Env: [
"USER_UID=1000",
"USER_GID=1000",
`GITEA__server__ROOT_URL=http://${config.userContainerHost}:${opts.httpPort}/`,
`GITEA__server__SSH_PORT=${opts.sshPort}`,
"GITEA__security__INSTALL_LOCK=true",
"GITEA__service__DISABLE_REGISTRATION=true",
],
HostConfig: {
Binds: [`${dataDir}:/data`],
PortBindings: {
"3000/tcp": [{ HostPort: String(opts.httpPort) }],
"22/tcp": [{ HostPort: String(opts.sshPort) }],
},
RestartPolicy: { Name: "unless-stopped" },
Memory: 1 * 1024 * 1024 * 1024,
NanoCpus: 1_000_000_000,
},
ExposedPorts: { "3000/tcp": {}, "22/tcp": {} },
Labels: {
"growqr.userId": opts.userId,
"growqr.role": "gitea",
},
});
await container.start();
log.info({ userId: opts.userId, name }, "started Gitea container");
return { id: container.id, name };
centralGiteaReady = true;
log.info({ url: config.giteaUrl, org: config.giteaOrgName }, "central Gitea ready");
}
function shellQuote(value: string): string {
return `'${value.replace(/'/g, "'\\''")}'`;
}
// ── Git clone into OpenCode workspace (changes.md §4 step 3) ──
// Clones the user's repo from central Gitea into the container's /workspace.
// If /workspace already has a .git folder, pulls instead of cloning.
async function cloneRepoIntoContainer(opts: {
containerId: string;
repoUrl: string;
giteaToken?: string;
giteaUser?: string;
giteaPassword?: string;
}): Promise<void> {
const container = docker.getContainer(opts.containerId);
async function execGiteaCli(containerId: string, args: string[]): Promise<string> {
const container = docker.getContainer(containerId);
const command = [
"gitea",
"--work-path",
"/data/gitea",
"--config",
"/data/gitea/conf/app.ini",
...args,
]
.map(shellQuote)
.join(" ");
const exec = await container.exec({
Cmd: ["su", "git", "-c", command],
// Build authenticated clone URL.
let authUrl = opts.repoUrl;
if (opts.giteaToken) {
// Embed token in URL: https://token@host/org/repo.git
authUrl = opts.repoUrl.replace("://", `://${encodeURIComponent(opts.giteaToken)}@`);
} else if (opts.giteaUser && opts.giteaPassword) {
authUrl = opts.repoUrl.replace("://", `://${encodeURIComponent(opts.giteaUser)}:${encodeURIComponent(opts.giteaPassword)}@`);
}
// Check if workspace is already a git repo; if so, pull instead of clone.
const checkExec = await container.exec({
Cmd: ["sh", "-c", "test -d /workspace/.git && echo 'exists' || echo 'missing'"],
AttachStdout: true,
AttachStderr: true,
WorkingDir: "/data/gitea",
});
const checkStream = await checkExec.start({ Detach: false, Tty: false });
const checkChunks: Buffer[] = [];
checkStream.on("data", (chunk: Buffer) => checkChunks.push(Buffer.from(chunk)));
await new Promise<void>((resolve) => {
checkStream.on("end", () => resolve());
checkStream.on("close", () => resolve());
});
const checkOutput = Buffer.concat(checkChunks).toString("utf8").trim();
let cmd: string[];
if (checkOutput.includes("exists")) {
// Pull latest changes.
cmd = ["sh", "-c", "cd /workspace && git pull origin main 2>&1 || echo 'pull failed, attempting fresh clone'"];
} else {
// Clone into /workspace (remove any placeholder files first, then clone).
cmd = [
"sh",
"-c",
`rm -rf /workspace/* /workspace/.* 2>/dev/null; git clone --branch main "${authUrl}" /workspace 2>&1 || echo 'clone failed'`,
];
}
const exec = await container.exec({
Cmd: cmd,
AttachStdout: true,
AttachStderr: true,
WorkingDir: "/workspace",
});
const stream = await exec.start({ Detach: false, Tty: false });
const chunks: Buffer[] = [];
@@ -165,62 +179,64 @@ async function execGiteaCli(containerId: string, args: string[]): Promise<string
const output = Buffer.concat(chunks).toString("utf8");
const info = await exec.inspect();
if (info.ExitCode && info.ExitCode !== 0) {
throw new Error(`gitea cli failed (${info.ExitCode}): ${output}`);
// Clone failed but workspace still works via Gitea API directly.
log.warn({ output, exitCode: info.ExitCode }, "git clone/pull into workspace had non-zero exit");
}
return output;
}
// Runs `gitea admin user create --admin ...` inside the container.
// Idempotent: the CLI returns non-zero if the user already exists, which is fine.
async function ensureGiteaAdmin(opts: {
containerId: string;
username: string;
password: string;
email: string;
}): Promise<void> {
// ── Git workspace sync (changes.md §2B: "Sync to Git") ──
// Commits and pushes changes from the container's /workspace back to the
// central Gitea repo, ensuring work done inside OpenCode is persisted as
// Git history. Called after significant events (workflows, code generation).
export async function syncWorkspaceToGit(userId: string, message?: string): Promise<void> {
const stack = await getUserStack(userId);
if (!stack?.opencodeContainerId || !stack.giteaRepoOwner || !stack.giteaRepoName) {
log.warn({ userId }, "cannot sync workspace — stack not provisioned");
return;
}
const container = docker.getContainer(stack.opencodeContainerId);
const commitMsg = message ?? `growqr: workspace sync at ${new Date().toISOString()}`;
// Build authenticated remote URL for push.
let authUrl = `${config.giteaUrl}/${encodeURIComponent(stack.giteaRepoOwner)}/${encodeURIComponent(stack.giteaRepoName)}.git`;
if (config.giteaAdminToken) {
authUrl = authUrl.replace("://", `://${encodeURIComponent(config.giteaAdminToken)}@`);
} else {
authUrl = authUrl.replace("://", `://${encodeURIComponent(config.giteaAdminUser)}:${encodeURIComponent(config.giteaAdminPassword)}@`);
}
// Set the remote URL with auth, add all, commit, push.
const cmd = [
"sh", "-c",
`git remote set-url origin "${authUrl}" 2>/dev/null; ` +
`git config user.email "growqr@local" && git config user.name "GrowQR"; ` +
`git add -A && git commit -m "${commitMsg.replace(/"/g, '\\"')}" 2>/dev/null; ` +
`git push origin main 2>&1`,
];
try {
await execGiteaCli(opts.containerId, [
"admin",
"user",
"create",
"--admin",
"--username",
opts.username,
"--password",
opts.password,
"--email",
opts.email,
"--must-change-password=false",
]);
const exec = await container.exec({
Cmd: cmd,
AttachStdout: true,
AttachStderr: true,
WorkingDir: "/workspace",
});
const stream = await exec.start({ Detach: false, Tty: false });
const chunks: Buffer[] = [];
stream.on("data", (chunk: Buffer) => chunks.push(Buffer.from(chunk)));
await new Promise<void>((resolve) => {
stream.on("end", () => resolve());
stream.on("close", () => resolve());
});
const output = Buffer.concat(chunks).toString("utf8");
log.info({ userId, output: output.slice(0, 200) }, "workspace synced to Git");
} catch (err) {
log.debug(
{ err },
"gitea admin user create returned non-zero (likely already exists)",
);
log.warn({ err, userId }, "workspace sync to Git failed (non-fatal)");
}
}
async function generateGiteaToken(opts: {
containerId: string;
username: string;
scopes: string[];
}): Promise<string> {
const output = await execGiteaCli(opts.containerId, [
"admin",
"user",
"generate-access-token",
"--username",
opts.username,
"--token-name",
`growqr-backend-${Date.now()}`,
"--scopes",
opts.scopes.join(","),
"--raw",
]);
const token = output.match(/[a-f0-9]{40}/i)?.[0];
if (!token) throw new Error("gitea token generation returned an empty token");
return token;
}
// ── Per-user OpenCode container (changes.md §2B + §3) ──
async function startOpencodeContainer(opts: {
userId: string;
@@ -240,16 +256,20 @@ async function startOpencodeContainer(opts: {
return { id: existing.Id, name };
}
// Sub-agents are loaded as prompt modules at build time (changes.md §2D).
// The shared image includes: base OS, OpenCode, GrowQR core, agents, tools, prompts.
const container = await docker.createContainer({
name,
Image: config.opencodeImage,
// OpenCode server CLI: `opencode serve --port 4096 --hostname 0.0.0.0`.
// We override the default CMD to make sure it binds to all interfaces
// and uses the per-user password.
Cmd: ["serve", "--port", "4096", "--hostname", "0.0.0.0"],
Cmd: ["opencode", "serve", "--port", "4096", "--hostname", "0.0.0.0"],
Env: [
`OPENCODE_SERVER_PASSWORD=${opts.password}`,
`OPENCODE_WORKSPACE=/workspace`,
`GROWQR_IMAGE_VERSION=${config.opencodeImageVersion}`,
`GROWQR_PROMPT_VERSION=${config.promptVersion}`,
`GROWQR_MIGRATION_VERSION=${config.migrationVersion}`,
`GROWQR_USER_ID=${opts.userId}`,
`GROWQR_GITEA_URL=${config.giteaUrl}`,
],
WorkingDir: "/workspace",
HostConfig: {
@@ -265,6 +285,8 @@ async function startOpencodeContainer(opts: {
Labels: {
"growqr.userId": opts.userId,
"growqr.role": "opencode",
"growqr.imageVersion": config.opencodeImageVersion,
"growqr.promptVersion": config.promptVersion,
},
});
await container.start();
@@ -272,18 +294,27 @@ async function startOpencodeContainer(opts: {
return { id: container.id, name };
}
// Provisions the per-user stack. Idempotent: returns the existing stack if
// the user already has one in the DB and the containers are running.
// ── User provisioning (changes.md §4) ──
function repoNameFor(userId: string): string {
return `user-${userId.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 48).toLowerCase()}`;
}
function userIdToGiteaUsername(userId: string): string {
return `gq_${userId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 24).toLowerCase() || "user"}`;
}
// Provisions the per-user stack. Uses CENTRAL Gitea (changes.md §2A) instead
// of spawning per-user Gitea containers.
//
// Steps:
// 1. Pick ports + allocate.
// 2. Start Gitea + OpenCode containers (or reuse).
// 3. Wait for Gitea HTTP to come up.
// 4. Create the per-user Gitea admin via `gitea admin user create`.
// 5. Mint a long-lived access token for the admin.
// 6. Create the user's memory repo with auto_init.
// 7. Wait for OpenCode to come up.
// 8. Persist everything to user_stacks.
// Steps (changes.md §4):
// 1. Ensure central Gitea is reachable + org exists.
// 2. Pick port for per-user OpenCode container.
// 3. Start OpenCode container (from shared image, changes.md §3).
// 4. Create the user's repo in the central Gitea org (changes.md §2A).
// 5. Initialize repo structure (memory/, conversations/, state/, etc. — changes.md §11).
// 6. Wait for OpenCode readiness.
// 7. Persist everything to user_stacks with version tracking (changes.md §9).
export async function provisionUserStack(userId: string): Promise<UserStack> {
const existing = await db.query.userStacks.findFirst({
where: eq(userStacks.userId, userId),
@@ -293,16 +324,13 @@ export async function provisionUserStack(userId: string): Promise<UserStack> {
}
await ensureDir(userDataDir(userId));
await ensureCentralGiteaReady();
const giteaHttpPort = existing?.giteaHttpPort ?? pickPort();
const giteaSshPort = existing?.giteaSshPort ?? pickPort();
const opencodePort = existing?.opencodePort ?? pickPort();
const opencodePassword =
existing?.opencodePassword ?? randomBytes(24).toString("hex");
const adminUsername =
existing?.giteaAdminUser ?? `gq_${userId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 24).toLowerCase() || "user"}`;
const adminPassword = randomBytes(24).toString("hex");
const adminEmail = `${adminUsername}@growqr.local`;
const repoName = existing?.giteaRepoName ?? repoNameFor(userId);
const repoOwner = config.giteaOrgName;
// Upsert "provisioning" row first so a crash mid-way leaves a recoverable record.
await db
@@ -310,14 +338,15 @@ export async function provisionUserStack(userId: string): Promise<UserStack> {
.values({
userId,
status: "provisioning",
giteaHttpPort,
giteaSshPort,
opencodePort,
opencodePassword,
giteaAdminUser: adminUsername,
giteaHost: config.userContainerHost,
giteaRepoName: repoName,
giteaRepoOwner: repoOwner,
opencodeHost: config.userContainerHost,
workspacePath: userDataDir(userId),
imageVersion: config.opencodeImageVersion,
migrationVersion: config.migrationVersion,
promptVersion: config.promptVersion,
})
.onConflictDoUpdate({
target: userStacks.userId,
@@ -329,59 +358,86 @@ export async function provisionUserStack(userId: string): Promise<UserStack> {
});
try {
const gitea = await startGiteaContainer({
userId,
httpPort: giteaHttpPort,
sshPort: giteaSshPort,
});
// Start per-user OpenCode container (shared image, changes.md §3).
const opencode = await startOpencodeContainer({
userId,
httpPort: opencodePort,
password: opencodePassword,
});
const giteaBase = `http://${config.userContainerHost}:${giteaHttpPort}`;
await waitForGitea(giteaBase, 90_000);
// Bootstrap admin user (idempotent — the CLI returns non-zero if exists).
await ensureGiteaAdmin({
containerId: gitea.id,
username: adminUsername,
password: adminPassword,
email: adminEmail,
});
// Mint a token via Gitea's CLI so retries do not depend on a transient
// bootstrap password from a previous provisioning attempt.
const token = await generateGiteaToken({
containerId: gitea.id,
username: adminUsername,
scopes: ["write:repository", "write:user", "write:issue"],
});
// Use the token from here on.
const tokenClient = new GiteaClient(giteaBase, { kind: "token", token });
const memoryRepo = await tokenClient.ensureRepo({
name: "growqr-memory",
description: "Grow Agent memory + state (PRD §3.4)",
// Create the user's repo in the central Gitea org (changes.md §2A + §4 step 2).
const giteaClient = await getCentralGiteaClient();
const repo = await giteaClient.ensureOrgRepo({
org: repoOwner,
name: repoName,
description: `GrowQR memory + workspace for user ${userId}`,
autoInit: true,
private: true,
});
// Initialize the standard repo structure (changes.md §11).
const initFiles: Array<{ path: string; content: string }> = [
{ path: "memory/.gitkeep", content: "# Agent memory\n" },
{ path: "conversations/.gitkeep", content: "# Conversation history\n" },
{ path: "state/.gitkeep", content: "# Agent state\n" },
{ path: "artifacts/.gitkeep", content: "# Generated artifacts\n" },
{ path: "workflows/.gitkeep", content: "# Workflow definitions\n" },
{ path: "logs/.gitkeep", content: "# Runtime logs\n" },
{ path: "config/.gitkeep", content: "# User configuration\n" },
{ path: "metadata/versions.json", content: JSON.stringify({
imageVersion: config.opencodeImageVersion,
migrationVersion: config.migrationVersion,
promptVersion: config.promptVersion,
provisionedAt: new Date().toISOString(),
}, null, 2) + "\n" },
];
for (const file of initFiles) {
try {
await giteaClient.putFile({
owner: repoOwner,
repo: repoName,
path: file.path,
contentUtf8: file.content,
message: `init: ${file.path}`,
branch: "main",
});
} catch (err) {
log.warn({ err, path: file.path }, "failed to init repo file (non-fatal)");
}
}
// OpenCode readiness.
const opencodeBase = `http://${config.userContainerHost}:${opencodePort}`;
await waitForOpencode(opencodeBase, opencodePassword, 90_000);
// Clone the user's Git repo into the OpenCode workspace (changes.md §4 step 3).
// Uses `git clone` inside the container so the workspace is a working copy
// of the user's repo, making Git the source of truth (changes.md §7).
try {
await cloneRepoIntoContainer({
containerId: opencode.id,
repoUrl: `${config.giteaUrl}/${encodeURIComponent(repoOwner)}/${encodeURIComponent(repoName)}.git`,
giteaToken: config.giteaAdminToken || undefined,
giteaUser: config.giteaAdminUser,
giteaPassword: !config.giteaAdminToken ? config.giteaAdminPassword : undefined,
});
log.info({ userId, repo: `${repoOwner}/${repoName}` }, "repo cloned into OpenCode workspace");
} catch (err) {
log.warn({ err, userId }, "git clone into workspace failed (non-fatal — workspace still available via Gitea API)");
}
const updated = await db
.update(userStacks)
.set({
status: "running",
giteaContainerId: gitea.id,
giteaContainerName: gitea.name,
giteaAdminToken: token,
giteaMemoryRepo: `${memoryRepo.owner}/${memoryRepo.name}`,
giteaRepoName: repo.name,
giteaRepoOwner: repo.owner,
opencodeContainerId: opencode.id,
opencodeContainerName: opencode.name,
imageVersion: config.opencodeImageVersion,
migrationVersion: config.migrationVersion,
promptVersion: config.promptVersion,
lastError: null,
updatedAt: new Date(),
})
@@ -390,7 +446,7 @@ export async function provisionUserStack(userId: string): Promise<UserStack> {
const row = updated[0];
if (!row) throw new Error("user stack row vanished mid-provision");
log.info({ userId }, "user stack provisioned");
log.info({ userId, repo: `${repo.owner}/${repo.name}` }, "user stack provisioned");
return row;
} catch (err) {
log.error({ err, userId }, "provisionUserStack failed");
@@ -416,24 +472,23 @@ export async function getUserStack(userId: string): Promise<UserStack | null> {
export async function stopUserStack(userId: string): Promise<void> {
const stack = await getUserStack(userId);
if (!stack) return;
for (const id of [stack.giteaContainerId, stack.opencodeContainerId]) {
if (!id) continue;
// Stop only the OpenCode container (Gitea is central, changes.md §2A).
if (stack.opencodeContainerId) {
try {
const c = docker.getContainer(id);
const c = docker.getContainer(stack.opencodeContainerId);
await c.stop({ t: 5 }).catch(() => undefined);
await c.remove({ force: true }).catch(() => undefined);
} catch (err) {
log.warn({ err, id }, "failed to stop container");
log.warn({ err, id: stack.opencodeContainerId }, "failed to stop container");
}
}
releasePort(stack.giteaHttpPort);
releasePort(stack.giteaSshPort);
releasePort(stack.opencodePort);
await db
.update(userStacks)
.set({
status: "stopped",
giteaContainerId: null,
opencodeContainerId: null,
updatedAt: new Date(),
})
@@ -445,19 +500,19 @@ export async function listStacks(): Promise<UserStack[]> {
return db.query.userStacks.findMany();
}
// Convenience: build a Gitea client for a user's stack.
export async function giteaClientFor(userId: string): Promise<GiteaClient | null> {
const stack = await getUserStack(userId);
if (!stack?.giteaAdminToken || !stack.giteaHost || !stack.giteaHttpPort) {
// ── Client helpers ──
// Build a Gitea client pointed at the CENTRAL Gitea instance (changes.md §2A).
// Uses the admin token for repo operations on behalf of any user.
export async function giteaClientFor(_userId: string): Promise<GiteaClient | null> {
try {
return await getCentralGiteaClient();
} catch {
return null;
}
return new GiteaClient(
`http://${stack.giteaHost}:${stack.giteaHttpPort}`,
{ kind: "token", token: stack.giteaAdminToken },
);
}
// Convenience: build an OpenCode client for a user's stack.
// Build an OpenCode client for a user's stack.
export async function opencodeUrlFor(
userId: string,
): Promise<{ baseUrl: string; password: string | undefined } | null> {
@@ -469,36 +524,61 @@ export async function opencodeUrlFor(
};
}
// Reconcile DB-tracked running containers with actual Docker state on boot.
// If a container is gone, flip the row to "stopped" so the next provision
// recreates it cleanly.
// ── Boot reconciliation (changes.md §9) ──
// Reconcile DB-tracked running stacks with actual Docker state on boot.
// Only checks OpenCode containers (Gitea is central, changes.md §2A).
// If a container is gone, flip the row to "stopped" so the next provision recreates it.
//
// Also detects version mismatches for image rollout (changes.md §9):
// if the running container's imageVersion is behind, mark for migration.
export async function reconcileOnBoot(): Promise<void> {
const rows = await db
.select()
.from(userStacks)
.where(
and(eq(userStacks.status, "running"), isNotNull(userStacks.giteaContainerId)),
and(eq(userStacks.status, "running"), isNotNull(userStacks.opencodeContainerId)),
);
for (const row of rows) {
if (!row.opencodeContainerId) continue;
let healthy = true;
for (const id of [row.giteaContainerId, row.opencodeContainerId]) {
if (!id) {
healthy = false;
break;
}
try {
const info = await docker.getContainer(id).inspect();
if (!info.State.Running) healthy = false;
} catch {
healthy = false;
}
try {
const info = await docker.getContainer(row.opencodeContainerId).inspect();
if (!info.State.Running) healthy = false;
} catch {
healthy = false;
}
if (!healthy) {
await db
.update(userStacks)
.set({ status: "stopped", updatedAt: new Date() })
.where(eq(userStacks.userId, row.userId));
log.info({ userId: row.userId }, "stack marked stopped during reconcile");
continue;
}
// Version mismatch detection (changes.md §9).
const needsMigration =
row.imageVersion !== config.opencodeImageVersion ||
row.migrationVersion !== config.migrationVersion;
const needsPromptUpdate = row.promptVersion !== config.promptVersion;
if (needsMigration || needsPromptUpdate) {
log.info(
{
userId: row.userId,
currentImage: row.imageVersion,
targetImage: config.opencodeImageVersion,
currentMigration: row.migrationVersion,
targetMigration: config.migrationVersion,
currentPrompt: row.promptVersion,
targetPrompt: config.promptVersion,
},
"version mismatch detected — migration needed on next provision",
);
}
}
}

View File

@@ -11,13 +11,27 @@ import { gitRoutes } from "./routes/git.js";
import { userRoutes } from "./routes/users.js";
import { agentRoutes } from "./routes/agents.js";
import { workflowRoutes } from "./routes/workflows.js";
import { chatRoutes } from "./routes/chat.js";
import { db } from "./db/client.js";
import { hydratePortAllocator, reconcileOnBoot } from "./docker/manager.js";
import { hydratePortAllocator, reconcileOnBoot, ensureCentralGiteaReady } from "./docker/manager.js";
import { initCatalog } from "./agents/catalog.js";
async function main() {
// Boot-time DB sanity + reconcile.
// Boot-time DB sanity + reconcile + central Gitea readiness.
await db.execute("select 1");
await hydratePortAllocator();
// Ensure central Gitea is reachable before accepting traffic (changes.md §2A).
try {
await ensureCentralGiteaReady();
} catch (err) {
log.warn({ err }, "central Gitea not ready at boot — will retry on first provision");
}
// Load prompts & agent modules from disk (changes.md §3: prompts/ + agents/).
// After this, buildUnifiedSystemPrompt() returns the full assembled prompt.
await initCatalog();
await reconcileOnBoot();
const app = new Hono();
@@ -55,27 +69,72 @@ async function main() {
}
});
// Rivet Kit actor traffic (frontend uses @rivetkit/react against this prefix).
app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));
// PRD HTTP control plane (auth-gated).
// HTTP control plane (auth-gated).
app.route("/users", userRoutes());
app.route("/agents", agentRoutes());
app.route("/workflows", workflowRoutes());
app.route("/actors", actorRoutes());
app.route("/opencode", opencodeRoutes());
app.route("/git", gitRoutes());
app.route("/api/chat", chatRoutes());
if (process.env.RIVET_RUN_ENGINE === "1") {
delete process.env.RIVET_ENDPOINT;
if (process.env.RIVET_ENDPOINT) {
// Self-hosted: embedded engine runs at localhost:6420.
// Proxy frontend Rivet traffic to the engine instead of using registry.handler()
// (handler conflicts with startRunner — they're mutually exclusive).
app.all("/api/rivet/*", async (c) => {
const url = new URL(c.req.url);
const target = new URL(config.rivetEndpoint);
url.protocol = target.protocol;
url.hostname = target.hostname;
url.port = target.port;
url.pathname = url.pathname.replace("/api/rivet", "");
// Forward headers, stripping hop-by-hop ones
const fwdHeaders = new Headers();
for (const [k, v] of Object.entries(c.req.raw.headers)) {
if (k.toLowerCase() === "host") continue;
if (k.toLowerCase() === "transfer-encoding") continue;
fwdHeaders.set(k, v);
}
fwdHeaders.set("Host", target.host);
// For POST/PUT/PATCH, clone the body stream (Hono may have consumed it)
const method = c.req.method.toUpperCase();
const bodyMethods = ["POST", "PUT", "PATCH", "DELETE"];
try {
const rawBody = bodyMethods.includes(method)
? await c.req.raw.clone().arrayBuffer()
: undefined;
const res = await fetch(url.toString(), {
method,
headers: fwdHeaders,
body: rawBody && rawBody.byteLength > 0 ? new Uint8Array(rawBody) : undefined,
});
return new Response(res.body, {
status: res.status,
headers: res.headers,
});
} catch (err) {
log.error({ err, url: url.toString() }, "rivet proxy error");
return c.json({ error: "proxy_error" }, 502);
}
});
registry.startRunner();
} else {
// Serverless: use registry.handler() for incoming actor traffic.
app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));
}
registry.startRunner();
serve({ fetch: app.fetch, port: config.port }, (info) => {
log.info(
{
port: info.port,
rivet: config.rivetEndpoint,
gitea: config.giteaUrl,
env: config.nodeEnv,
},
"growqr-backend listening",

View File

@@ -132,6 +132,64 @@ export class GiteaClient {
}
}
// ── Central Gitea org methods (changes.md §2A) ──
// Ensure an organization exists. Idempotent.
async ensureOrg(orgName: string): Promise<void> {
try {
await this.req("POST", "/api/v1/orgs", {
username: orgName,
full_name: orgName,
description: "GrowQR organization — one repo per user",
});
} catch (err) {
log.debug({ err }, "ensureOrg returned non-2xx (likely already exists)");
}
}
// Create a repo inside an org. Idempotent (falls back to GET on 409).
async ensureOrgRepo(opts: {
org: string;
name: string;
description?: string;
autoInit?: boolean;
private?: boolean;
}): Promise<{ owner: string; name: string; cloneUrl: string }> {
try {
const repo = await this.req<{
owner: { login: string };
name: string;
clone_url: string;
}>("POST", `/api/v1/orgs/${encodeURIComponent(opts.org)}/repos`, {
name: opts.name,
description: opts.description ?? "",
auto_init: opts.autoInit ?? true,
private: opts.private ?? true,
default_branch: "main",
});
return {
owner: repo.owner.login,
name: repo.name,
cloneUrl: repo.clone_url,
};
} catch (err) {
const repo = await this.req<{
owner: { login: string };
name: string;
clone_url: string;
}>(
"GET",
`/api/v1/repos/${encodeURIComponent(opts.org)}/${encodeURIComponent(opts.name)}`,
);
log.debug({ err }, "ensureOrgRepo fell through to GET");
return {
owner: repo.owner.login,
name: repo.name,
cloneUrl: repo.clone_url,
};
}
}
// Creates or updates a file in a repo. Used for memory commits.
async putFile(opts: {
owner: string;

View File

@@ -1,28 +1,14 @@
import { config } from "../config.js";
export const GROW_AGENT_SYSTEM = `You are a Grow Agent - a user's master AI orchestrator on the GrowQR platform.
You own this user's long-running context, memory, and workspace. You coordinate specialized sub-agents (coding, repo, quest, product-flow, etc.), keep durable state in the user's Gitea memory repository, and execute workflows via the user's OpenCode sandbox.
Operating principles:
- Be concise and direct. The user sees your messages in a Slack-like chat.
- Maintain durable memory: commit important decisions, goals, and progress to the user's memory repo using \`commit_memory\`. Read existing context with \`read_memory\` before making suggestions that depend on history.
- For anything that requires code, shell, file edits, or generated artifacts, spawn a sub-agent via \`spawn_sub_agent\`. The sub-agent runs through the user's OpenCode container.
- Track active goals and quests. Surface progress proactively when the user returns.
- Prefer one small commit per meaningful state change over batching.
- Never invent tool names. Only use the tools provided.
`;
export type GrowAgentTool =
| "spawn_sub_agent"
| "commit_memory"
| "read_memory"
| "list_memory";
// ── LLM type definitions ──
// The system prompt and agent tools are loaded from disk at startup
// (prompts/system.txt + agents/*.md) via prompt-loader.ts.
// The unified tools are assembled in user-actor.ts using the catalog.
export type LlmTool = {
type: "function";
function: {
name: GrowAgentTool;
name: string;
description: string;
parameters: Record<string, unknown>;
};
@@ -48,96 +34,7 @@ export type LlmMessage = {
}>;
};
export const growAgentTools: LlmTool[] = [
{
type: "function",
function: {
name: "spawn_sub_agent",
description:
"Spawn a specialized sub-agent to run a bounded task through the user's OpenCode container. Use for anything that requires running code, editing files, or producing artifacts.",
parameters: {
type: "object",
properties: {
type: {
type: "string",
description:
"Sub-agent type: 'coding', 'repo', 'migration', 'quest', 'product', 'backend', 'frontend', or another short identifier.",
},
prompt: {
type: "string",
description:
"The full task prompt for the sub-agent. Include all context it needs - sub-agents do not see this conversation.",
},
channelId: {
type: "string",
description:
"Optional channel/thread id the sub-agent should report into. Generated if omitted.",
},
},
required: ["type", "prompt"],
},
},
},
{
type: "function",
function: {
name: "commit_memory",
description:
"Write or update a file in the user's Gitea memory repository. Use for goals, decisions, progress notes, plans, and durable summaries.",
parameters: {
type: "object",
properties: {
path: {
type: "string",
description:
"Repo-relative path, e.g. 'goals/active.md' or 'decisions/2026-05-19-architecture.md'.",
},
content: {
type: "string",
description: "Full UTF-8 file content to write.",
},
message: {
type: "string",
description: "Commit message describing the change.",
},
},
required: ["path", "content", "message"],
},
},
},
{
type: "function",
function: {
name: "read_memory",
description: "Read a single file from the user's memory repo. Returns null if missing.",
parameters: {
type: "object",
properties: {
path: { type: "string" },
},
required: ["path"],
},
},
},
{
type: "function",
function: {
name: "list_memory",
description:
"List files at a path prefix in the user's memory repo. Use to discover what context already exists.",
parameters: {
type: "object",
properties: {
pathPrefix: {
type: "string",
description: "Repo-relative directory, e.g. 'goals' or '' for root.",
},
},
required: ["pathPrefix"],
},
},
},
];
// ── LLM API client ──
type ChatCompletionsResponse = {
choices?: Array<{

170
src/lib/prompt-loader.ts Normal file
View File

@@ -0,0 +1,170 @@
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
import { log } from "../log.js";
// ── Types ──
export type SubAgentModule = {
id: string;
name: string;
role: string;
description: string;
service?: "interview-service" | "roleplay-service" | "qscore-service" | "resume-service" | "matchmaking-service";
toolNames: string[];
};
type AgentFrontmatter = {
id?: string;
name?: string;
role?: string;
service?: string;
tools?: string[];
};
// ── Paths ──
const PROMPTS_DIR = path.resolve(process.cwd(), "prompts");
const AGENTS_DIR = path.resolve(process.cwd(), "agents");
const SYSTEM_PROMPT_FILE = path.join(PROMPTS_DIR, "system.txt");
// ── Frontmatter parser (no external dependencies) ──
function parseFrontmatter(raw: string): { data: AgentFrontmatter; body: string } {
const trimmed = raw.trim();
if (!trimmed.startsWith("---")) return { data: {}, body: trimmed };
const secondDelim = trimmed.indexOf("---", 3);
if (secondDelim === -1) return { data: {}, body: trimmed };
const fmBlock = trimmed.slice(3, secondDelim).trim();
const body = trimmed.slice(secondDelim + 3).trim();
const data: AgentFrontmatter = {};
for (const line of fmBlock.split("\n")) {
const colonIdx = line.indexOf(":");
if (colonIdx === -1) continue;
const key = line.slice(0, colonIdx).trim();
let value: string | string[] = line.slice(colonIdx + 1).trim();
if (key === "tools" && value.startsWith("[") && value.endsWith("]")) {
// Parse inline array: ["tool1", "tool2"]
const inner = value.slice(1, -1);
value = inner
.split(",")
.map((s) => s.trim().replace(/^["']|["']$/g, ""))
.filter(Boolean);
}
if (key === "id") data.id = value as string;
if (key === "name") data.name = value as string;
if (key === "role") data.role = value as string;
if (key === "service") data.service = value as string;
if (key === "tools") data.tools = value as string[];
}
return { data, body };
}
// ── Loader ──
let cachedModules: SubAgentModule[] | null = null;
let cachedSystemPrompt: string | null = null;
export function getSubAgentModules(): SubAgentModule[] {
if (!cachedModules) {
throw new Error("Prompts not loaded — call loadPromptsFromDisk() at startup");
}
return cachedModules;
}
export function getUnifiedSystemPrompt(): string {
if (!cachedSystemPrompt) {
throw new Error("Prompts not loaded — call loadPromptsFromDisk() at startup");
}
return cachedSystemPrompt;
}
export function getSubAgentModule(id: string): SubAgentModule | undefined {
return getSubAgentModules().find((m) => m.id === id);
}
export function jobApplicationModuleIds(): string[] {
return ["resume", "job-search", "job-apply", "sara", "emily", "qscore"];
}
// Load all prompt and agent files from disk.
// Called once at startup. Rebuild the Docker image to pick up changes (§3).
export async function loadPromptsFromDisk(): Promise<void> {
// ── Load agent modules ──
let agentFiles: string[];
try {
agentFiles = (await readdir(AGENTS_DIR)).filter((f) => f.endsWith(".md"));
} catch (err) {
log.warn({ err, dir: AGENTS_DIR }, "agents directory not found — using empty catalog");
agentFiles = [];
}
const modules: SubAgentModule[] = [];
for (const filename of agentFiles) {
const filePath = path.join(AGENTS_DIR, filename);
try {
const raw = await readFile(filePath, "utf8");
const { data, body } = parseFrontmatter(raw);
if (!data.id || !data.name) {
log.warn({ file: filename }, "agent file missing required frontmatter fields (id, name)");
continue;
}
const service = data.service as SubAgentModule["service"] | undefined;
if (
service &&
service !== "interview-service" &&
service !== "roleplay-service" &&
service !== "qscore-service" &&
service !== "resume-service" &&
service !== "matchmaking-service"
) {
log.warn({ file: filename, service }, "unknown service value — treating as no service");
}
modules.push({
id: data.id,
name: data.name,
role: data.role ?? data.name,
description: body || `Agent module: ${data.name}`,
service: service &&
["interview-service", "roleplay-service", "qscore-service", "resume-service", "matchmaking-service"].includes(service)
? (service as SubAgentModule["service"])
: undefined,
toolNames: data.tools ?? [],
});
} catch (err) {
log.error({ err, file: filename }, "failed to load agent module");
}
}
cachedModules = modules;
log.info({ count: modules.length, dir: AGENTS_DIR }, "loaded sub-agent modules from disk");
// ── Load system prompt ──
try {
const template = await readFile(SYSTEM_PROMPT_FILE, "utf8");
const moduleDescriptions = modules
.map(
(m) =>
`- **${m.name}** (${m.id}): ${m.description} ${
m.service ? `[backed by ${m.service}]` : "[local workflow]"
}`,
)
.join("\n");
cachedSystemPrompt = template.replace("{{MODULE_DESCRIPTIONS}}", moduleDescriptions);
log.info({ path: SYSTEM_PROMPT_FILE }, "loaded system prompt from disk");
} catch (err) {
log.error({ err, path: SYSTEM_PROMPT_FILE }, "failed to load system prompt — using fallback");
// Fallback: assemble from modules without a template file.
const fallback = `You are Grow — a unified AI career assistant for the GrowQR platform.\n\n## Specialist Capabilities\n\n${modules.map((m) => `- **${m.name}**: ${m.description}`).join("\n")}`;
cachedSystemPrompt = fallback;
}
}

View File

@@ -10,9 +10,8 @@ import { db } from "../db/client.js";
import { actors as actorsTable } from "../db/schema.js";
import { eq } from "drizzle-orm";
// PRD §5.2 — Actor registry HTTP surface.
// All routes are user-scoped via Clerk auth; userId is derived from the
// session token, never trusted from the body.
// Per changes.md §5: ONE unified actor per user.
// Routes are user-scoped via Clerk auth; userId derived from session token.
export function actorRoutes() {
const app = new Hono<AuthContext>();
app.use("*", requireUser);
@@ -34,7 +33,6 @@ export function actorRoutes() {
});
app.get("/", async (c) => {
// Admin/debug — returns the caller's stacks only. Tighten further if needed.
const userId = c.get("userId");
const all = await listStacks();
return c.json({ stacks: all.filter((s) => s.userId === userId) });

View File

@@ -1,12 +1,13 @@
import { Hono } from "hono";
import { agentCatalog } from "../agents/catalog.js";
import { subAgentModules } from "../agents/catalog.js";
import { requireUser, type AuthContext } from "../auth/clerk.js";
export function agentRoutes() {
const app = new Hono<AuthContext>();
app.use("*", requireUser);
app.get("/catalog", (c) => c.json({ agents: agentCatalog }));
// Returns the sub-agent module catalog (changes.md §2D: prompt modules).
app.get("/catalog", (c) => c.json({ agents: subAgentModules }));
return app;
}

308
src/routes/chat.ts Normal file
View File

@@ -0,0 +1,308 @@
import { Hono } from "hono";
import { z } from "zod";
import { createClient } from "rivetkit/client";
import { config } from "../config.js";
import { requireUser, type AuthContext } from "../auth/clerk.js";
import type { Registry } from "../actors/registry.js";
import type { LlmMessage } from "../lib/llm.js";
import { createChatCompletion } from "../lib/llm.js";
import { buildUnifiedSystemPrompt } from "../agents/catalog.js";
import {
buildServiceSessionUrl,
runServiceAgentProbe,
type ServiceAgentResult,
} from "../services/service-agents.js";
import { getSubAgentModules } from "../lib/prompt-loader.js";
const chatSchema = z.object({
messages: z.array(
z.object({
role: z.enum(["user", "assistant", "system"]),
content: z.string(),
}),
),
agentId: z.string().optional(),
});
function extractWorkflowTag(reply: string): string | undefined {
const match = reply.match(/\[WORKFLOW:\s*([a-z-]+)\]/i);
if (!match || !match[1]) return undefined;
return match[1].toLowerCase();
}
function cleanWorkflowTag(reply: string): string {
return reply.replace(/\[WORKFLOW:\s*[a-z-]+\]/gi, "").trim();
}
function buildTools() {
return [
{
type: "function" as const,
function: {
name: "start_interview_session",
description: "Create a real mock interview session via the interview-service microservice. Call this when the user asks to start or launch interview practice.",
parameters: {
type: "object",
properties: {
target_role: { type: "string", description: "The target role and company, e.g., 'Software Engineer at Google'" },
},
required: ["target_role"],
},
},
},
{
type: "function" as const,
function: {
name: "start_roleplay_session",
description: "Create a real mock roleplay session via roleplay-service. Call when the user asks for roleplay or negotiation practice.",
parameters: {
type: "object",
properties: {
goal: { type: "string", description: "What scenario to practice" },
},
required: ["goal"],
},
},
},
{
type: "function" as const,
function: {
name: "analyze_resume",
description: "Analyze the user's resume using Resume Building. Returns completeness, skills, and gaps.",
parameters: {
type: "object",
properties: {
goal: { type: "string", description: "Target role for context" },
},
required: ["goal"],
},
},
},
{
type: "function" as const,
function: {
name: "compute_qscore",
description: "Compute the user's readiness Q Score via qscore-service.",
parameters: {
type: "object",
properties: {},
required: [],
},
},
},
];
}
export function chatRoutes() {
const app = new Hono<AuthContext>();
app.use("*", requireUser);
// Infer workflow step from which agents have been run
function inferWorkflowStep(sessions: Array<{ moduleId: string; status: string }>, messages: Array<{ role: string; content: string }>): { workflowActive: boolean; workflowStep: number; goal: string } {
const doneModules = new Set(sessions.filter(s => s.status === "done").map(s => s.moduleId));
let step = 0;
let goal = "";
// Extract goal from conversation (look for "I have an interview at..." or "prepare for...")
for (const m of messages) {
if (m.role === "user") {
const lower = m.content.toLowerCase();
if (lower.includes("interview at") || lower.includes("prepare for") || lower.includes("role at") || lower.includes("apply to")) {
goal = m.content;
break;
}
}
}
// Infer step from completed modules
// Step 1: Workflow started (user described goal)
if (goal) step = 1;
// Step 2: User shared JD/role info
if (messages.filter(m => m.role === "user" && m.content.length > 30).length >= 2) step = 2;
// Step 3: Resume agent done
if (doneModules.has("resume")) step = 3;
// Step 4: Interview session created
if (doneModules.has("sara")) step = 4;
// Step 5: Roleplay session created
if (doneModules.has("emily")) step = 5;
// Step 6: QScore computed
if (doneModules.has("qscore")) step = 6;
return {
workflowActive: step > 0,
workflowStep: step,
goal: goal || "Career preparation",
};
}
app.post("/", async (c) => {
const userId = c.get("userId");
const body = chatSchema.parse(await c.req.json());
const userText = body.messages[body.messages.length - 1]?.content ?? "";
// 1. Try Rivet actor path (full tool suite + conversation history)
try {
const client = createClient<Registry>(config.rivetClientEndpoint);
const handle = client.userActor.getOrCreate([userId]);
await handle.init({ userId });
const result = await handle.receiveMessage({ text: userText });
if (result?.reply) {
const reply = cleanWorkflowTag(String(result.reply));
const workflow = extractWorkflowTag(String(result.reply));
const sessions = (result as any).sessions ?? [];
const stepInfo = inferWorkflowStep(sessions, body.messages);
return c.json({ reply, workflow, sessions, ...stepInfo });
}
} catch (err) {
console.warn("Rivet chat unavailable, using direct LLM:", err instanceof Error ? err.message : String(err));
}
// 2. Fallback: direct LLM with tool dispatch
const systemPrompt = buildUnifiedSystemPrompt();
const conversation: LlmMessage[] = [
{ role: "system", content: systemPrompt },
...body.messages.filter((m) => m.role !== "system"),
];
try {
const response1 = await createChatCompletion({
model: config.agentModel,
maxTokens: config.maxAgentTokens,
tools: buildTools(),
messages: conversation,
});
let reply = response1.content || "";
const sessions: Array<{
moduleId: string;
moduleName: string;
status: string;
sessionId?: string;
sessionUrl?: string;
summary?: string;
}> = [];
// If LLM called a tool, execute it
if (response1.toolCalls.length > 0) {
conversation.push({
role: "assistant",
content: response1.content,
tool_calls: response1.toolCalls.map((tc) => ({
id: tc.id,
type: "function" as const,
function: { name: tc.name, arguments: JSON.stringify(tc.arguments) },
})),
});
for (const toolCall of response1.toolCalls) {
console.log("LLM called tool:", toolCall.name, toolCall.arguments);
let toolResult: ServiceAgentResult;
switch (toolCall.name) {
case "start_interview_session": {
toolResult = await runServiceAgentProbe(
{ id: "sara", name: "Mock Interview", role: "Interview practice", kind: "microservice", description: "Interview practice", service: "interview-service" },
{ userId, goal: String(toolCall.arguments.target_role ?? "general preparation") },
);
if (toolResult.status === "ok" && toolResult.detail) {
const detail = toolResult.detail as Record<string, unknown>;
sessions.push({
moduleId: "sara",
moduleName: "Mock Interview",
status: "done",
sessionId: detail.session_id as string,
sessionUrl: typeof detail.ui_session_url === "string"
? detail.ui_session_url
: buildServiceSessionUrl("interview-service", detail, String(toolCall.arguments.target_role ?? "general preparation")),
summary: toolResult.summary,
});
}
break;
}
case "start_roleplay_session": {
toolResult = await runServiceAgentProbe(
{ id: "emily", name: "Mock Roleplay", role: "Roleplay practice", kind: "microservice", description: "Roleplay practice", service: "roleplay-service" },
{ userId, goal: String(toolCall.arguments.goal ?? "general practice") },
);
if (toolResult.status === "ok" && toolResult.detail) {
const detail = toolResult.detail as Record<string, unknown>;
sessions.push({
moduleId: "emily",
moduleName: "Mock Roleplay",
status: "done",
sessionId: detail.session_id as string,
sessionUrl: typeof detail.ui_session_url === "string"
? detail.ui_session_url
: buildServiceSessionUrl("roleplay-service", detail, String(toolCall.arguments.goal ?? "general practice")),
summary: toolResult.summary,
});
}
break;
}
case "analyze_resume": {
toolResult = await runServiceAgentProbe(
{ id: "resume", name: "Resume Building", role: "Resume building", kind: "microservice", description: "Resume analysis", service: "resume-service" },
{ userId, goal: String(toolCall.arguments.goal ?? "general") },
);
if (toolResult.status === "ok") {
const detail = toolResult.detail as Record<string, unknown> | undefined;
sessions.push({
moduleId: "resume",
moduleName: "Resume Building",
status: "done",
sessionUrl: typeof detail?.ui_session_url === "string"
? detail.ui_session_url
: buildServiceSessionUrl("resume-service", detail, String(toolCall.arguments.goal ?? "general")),
summary: toolResult.summary,
});
}
break;
}
case "compute_qscore": {
toolResult = await runServiceAgentProbe(
{ id: "qscore", name: "Q Score", role: "Readiness scoring", kind: "score", description: "Readiness scoring", service: "qscore-service" },
{ userId, goal: "general assessment" },
);
if (toolResult.status === "ok") {
sessions.push({ moduleId: "qscore", moduleName: "Q Score", status: "done", summary: toolResult.summary });
}
break;
}
}
}
// Second LLM call: summarize tool results
const toolResults = sessions.map((s) =>
`Tool result: ${s.moduleName} - ${s.status} - ${s.summary || ""}${s.sessionUrl ? ` - Demo URL: ${s.sessionUrl}` : ""}`,
);
for (const tr of toolResults) {
conversation.push({ role: "tool", content: tr, tool_call_id: "tool" });
}
const response2 = await createChatCompletion({
model: config.agentModel,
maxTokens: 1024,
tools: [],
messages: conversation,
});
reply = cleanWorkflowTag(response2.content || reply);
}
return c.json({
reply: cleanWorkflowTag(reply),
workflow: extractWorkflowTag(reply),
sessions,
...inferWorkflowStep(sessions, body.messages),
});
} catch (llmErr) {
console.error("Direct LLM chat error:", llmErr);
return c.json(
{ error: llmErr instanceof Error ? llmErr.message : "LLM error" },
{ status: 502 },
);
}
});
return app;
}

View File

@@ -5,7 +5,8 @@ import { requireUser, type AuthContext } from "../auth/clerk.js";
import { db } from "../db/client.js";
import { repos } from "../db/schema.js";
// PRD §5.4 — Gitea Docker management API.
// Per changes.md §2A: uses CENTRAL Gitea, not per-user Gitea containers.
// All repo operations go through the central org.
export function gitRoutes() {
const app = new Hono<AuthContext>();
app.use("*", requireUser);
@@ -16,10 +17,8 @@ export function gitRoutes() {
if (!stack) return c.json({ error: "not provisioned" }, 404);
return c.json({
gitea: {
host: stack.giteaHost,
port: stack.giteaHttpPort,
sshPort: stack.giteaSshPort,
memoryRepo: stack.giteaMemoryRepo,
repoOwner: stack.giteaRepoOwner,
repoName: stack.giteaRepoName,
},
});
});
@@ -31,10 +30,14 @@ export function gitRoutes() {
.parse(await c.req.json());
const client = await giteaClientFor(userId);
const stack = await getUserStack(userId);
if (!client || !stack) {
if (!client || !stack?.giteaRepoOwner) {
return c.json({ error: "not provisioned" }, 404);
}
const repo = await client.ensureRepo({ name: body.name, autoInit: true });
const repo = await client.ensureOrgRepo({
org: stack.giteaRepoOwner,
name: body.name,
autoInit: true,
});
await db
.insert(repos)
.values({
@@ -61,15 +64,12 @@ export function gitRoutes() {
})
.parse(await c.req.json());
const client = await giteaClientFor(userId);
if (!client) return c.json({ error: "not provisioned" }, 404);
// Get owner from DB or fall back to memory repo.
const stack = await getUserStack(userId);
const owner = stack?.giteaAdminUser ?? "";
if (!owner) return c.json({ error: "no gitea owner" }, 500);
if (!client || !stack?.giteaRepoOwner) {
return c.json({ error: "not provisioned" }, 404);
}
const result = await client.putFile({
owner,
owner: stack.giteaRepoOwner,
repo: repoName,
path: body.path,
contentUtf8: body.content,
@@ -82,19 +82,19 @@ export function gitRoutes() {
app.get("/repos/:name/contents/*", async (c) => {
const userId = c.get("userId");
const repoName = c.req.param("name");
const path = c.req.path.split(`/repos/${repoName}/contents/`)[1] ?? "";
const filePath = c.req.path.split(`/repos/${repoName}/contents/`)[1] ?? "";
const client = await giteaClientFor(userId);
const stack = await getUserStack(userId);
if (!client || !stack?.giteaAdminUser) {
if (!client || !stack?.giteaRepoOwner) {
return c.json({ error: "not provisioned" }, 404);
}
const content = await client.readFile({
owner: stack.giteaAdminUser,
owner: stack.giteaRepoOwner,
repo: repoName,
path,
path: filePath,
});
if (content == null) return c.json({ error: "not found" }, 404);
return c.json({ path, content });
return c.json({ path: filePath, content });
});
return app;

View File

@@ -12,7 +12,7 @@ export function userRoutes() {
// Called by the frontend right after Clerk sign-in.
// - Ensures a `users` row exists (the auth middleware already lazy-mirrors).
// - Kicks off Grow Agent stack provisioning if not already running.
// - Kicks off the Grow stack provisioning if not already running.
// - Returns the current stack status so the UI can render a provisioning spinner.
app.post("/bootstrap", async (c) => {
const userId = c.get("userId");

View File

@@ -1,14 +1,23 @@
import { Hono } from "hono";
import { z } from "zod";
import { createClient } from "rivetkit/client";
import { createClient, type Client } from "rivetkit/client";
import { config } from "../config.js";
import { requireUser, type AuthContext } from "../auth/clerk.js";
import type { Registry } from "../actors/registry.js";
const client = createClient<Registry>(config.rivetEndpoint);
// Lazy-load the Rivet client to avoid connecting at import time when the engine
// isn't running (avoids "failed to fetch metadata" spam on startup).
let _client: Client<Registry> | null = null;
function getClient(): Client<Registry> {
if (!_client) {
_client = createClient<Registry>(config.rivetEndpoint);
}
return _client;
}
function jobWorkflowFor(userId: string) {
return client.workflowJob.getOrCreate([userId, "job-application"]);
// Per changes.md §5: one unified userActor per user.
function userActorFor(userId: string) {
return getClient().userActor.getOrCreate([userId]);
}
export function workflowRoutes() {
@@ -20,41 +29,42 @@ export function workflowRoutes() {
const body = z
.object({ goal: z.string().min(1).optional() })
.parse(await c.req.json().catch(() => ({})));
const handle = jobWorkflowFor(userId);
const state = await handle.init({ userId, goal: body.goal });
const started = await handle.start();
return c.json({ workflow: started ?? state });
const handle = userActorFor(userId);
await handle.init({ userId });
const state = await handle.startWorkflow({ goal: body.goal });
return c.json({ workflow: state });
});
app.get("/job-application", async (c) => {
const userId = c.get("userId");
const handle = jobWorkflowFor(userId);
const state = await handle.init({ userId });
const handle = userActorFor(userId);
await handle.init({ userId });
const state = await handle.getWorkflowStatus();
return c.json({ workflow: state });
});
app.post("/job-application/pause", async (c) => {
const userId = c.get("userId");
const workflow = await jobWorkflowFor(userId).pause();
const workflow = await userActorFor(userId).pauseWorkflow();
return c.json({ workflow });
});
app.post("/job-application/resume", async (c) => {
const userId = c.get("userId");
const workflow = await jobWorkflowFor(userId).resume();
const workflow = await userActorFor(userId).resumeWorkflow();
return c.json({ workflow });
});
app.post("/job-application/agents/:agentId/run", async (c) => {
app.post("/job-application/agents/:moduleId/run", async (c) => {
const userId = c.get("userId");
const agentId = c.req.param("agentId");
const workflow = await jobWorkflowFor(userId).runAgent({ agentId });
const moduleId = c.req.param("moduleId");
const workflow = await userActorFor(userId).runWorkflowModule({ moduleId });
return c.json({ workflow });
});
app.post("/job-application/agents/:agentId/score", async (c) => {
app.post("/job-application/agents/:moduleId/score", async (c) => {
const userId = c.get("userId");
const agentId = c.req.param("agentId");
const moduleId = c.req.param("moduleId");
const body = z
.object({
question: z.string().min(1),
@@ -63,8 +73,8 @@ export function workflowRoutes() {
notes: z.string().optional(),
})
.parse(await c.req.json());
const workflow = await jobWorkflowFor(userId).recordQaScore({
agentId,
const workflow = await userActorFor(userId).recordQaScore({
moduleId,
...body,
});
return c.json({ workflow });

View File

@@ -1,7 +1,16 @@
import { config } from "../config.js";
import type { AgentProfile } from "../agents/catalog.js";
import { createHash } from "node:crypto";
// Lightweight agent reference (works with both old AgentProfile and new SubAgentModule).
export type ServiceAgentRef = {
id: string;
name: string;
role: string;
kind: string;
description: string;
service?: string;
};
export type ServiceAgentResult = {
status: "ok" | "unavailable" | "local";
summary: string;
@@ -14,6 +23,39 @@ export type ServiceAgentContext = {
goal: string;
};
export function buildServiceSessionUrl(
service: string | undefined,
detail: Record<string, unknown> | undefined,
goal?: string,
): string | undefined {
const base = config.workflowsDashboardUrl.replace(/\/$/, "");
const sessionId = detail?.session_id ?? detail?.sessionId;
const params = new URLSearchParams();
if (sessionId && typeof sessionId === "string") params.set("session_id", sessionId);
if (goal) params.set("goal", goal);
if (service === "interview-service") {
if (!sessionId || typeof sessionId !== "string") return undefined;
params.set("role", String(detail?.target_role ?? goal ?? "Interview practice"));
params.set("type", String(detail?.interview_type ?? "behavioral"));
return `${base}/v2/service-sessions/interview?${params.toString()}`;
}
if (service === "roleplay-service") {
if (!sessionId || typeof sessionId !== "string") return undefined;
params.set("role", String(detail?.target_role ?? goal ?? "Roleplay practice"));
params.set("type", String(detail?.roleplay_type ?? "custom"));
return `${base}/v2/service-sessions/roleplay?${params.toString()}`;
}
if (service === "resume-service") {
if (goal) params.set("role", goal);
return `${base}/v2/service-sessions/resume${params.size ? `?${params.toString()}` : ""}`;
}
return undefined;
}
function stableUuid(input: string): string {
const hex = createHash("sha256").update(input).digest("hex").slice(0, 32);
return [
@@ -63,7 +105,7 @@ async function healthCheck(baseUrl: string, label: string): Promise<ServiceAgent
}
}
async function runSaraInterview(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
async function runInterviewPractice(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
const payload = {
user_id: ctx.userId,
org_id: ctx.orgId ?? "growqr",
@@ -87,12 +129,17 @@ async function runSaraInterview(ctx: ServiceAgentContext): Promise<ServiceAgentR
);
return {
status: "ok",
summary: `Sara created interview session ${detail.session_id ?? "(pending id)"} for ${ctx.goal}.`,
detail,
summary: `Mock Interview created session ${detail.session_id ?? "(pending id)"} for ${ctx.goal}.`,
detail: {
...detail,
target_role: payload.context.target_role,
interview_type: payload.interview_type,
ui_session_url: buildServiceSessionUrl("interview-service", detail, ctx.goal),
},
};
}
async function runEmilyRoleplay(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
async function runRoleplayPractice(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
const payload = {
user_id: ctx.userId,
org_id: ctx.orgId ?? "growqr",
@@ -126,12 +173,17 @@ async function runEmilyRoleplay(ctx: ServiceAgentContext): Promise<ServiceAgentR
);
return {
status: "ok",
summary: `Emily created roleplay session ${detail.session_id ?? "(pending id)"} for ${ctx.goal}.`,
detail,
summary: `Mock Roleplay created session ${detail.session_id ?? "(pending id)"} for ${ctx.goal}.`,
detail: {
...detail,
target_role: payload.metadata.target_role,
roleplay_type: payload.roleplay_type,
ui_session_url: buildServiceSessionUrl("roleplay-service", detail, ctx.goal),
},
};
}
async function runQuinnQScore(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
async function runQScoreCheck(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
const orgId = ctx.orgId ?? "growqr";
const qscoreUserId = stableUuid(ctx.userId);
const signals = [
@@ -139,43 +191,51 @@ async function runQuinnQScore(ctx: ServiceAgentContext): Promise<ServiceAgentRes
signal_id: "resume.uploaded",
present: true,
score: 82,
raw: { source: "resume-agent", workflow_goal: ctx.goal },
raw: { source: "resume-building", workflow_goal: ctx.goal },
},
{
signal_id: "resume.ats_compatibility",
present: true,
score: 76,
raw: { source: "resume-agent", workflow_goal: ctx.goal },
raw: { source: "resume-building", workflow_goal: ctx.goal },
},
{
signal_id: "engagement.features_used",
present: true,
score: 88,
raw: { source: "grow-agent", workflow_goal: ctx.goal },
raw: { source: "grow", workflow_goal: ctx.goal },
},
{
signal_id: "goals.goal_clarity",
present: true,
score: 90,
raw: { source: "grow-agent", workflow_goal: ctx.goal },
raw: { source: "grow", workflow_goal: ctx.goal },
},
];
const ingest = await serviceJson<Record<string, unknown>>(
config.qscoreServiceUrl,
"/v1/signals/ingest",
{
method: "POST",
body: JSON.stringify({
org_id: orgId,
user_id: qscoreUserId,
profession: "student",
source: "growqr-workflow",
signals,
}),
},
);
// Try to ingest signals (non-critical — may fail if QScore worker is down)
let ingest: Record<string, unknown> | undefined;
try {
ingest = await serviceJson<Record<string, unknown>>(
config.qscoreServiceUrl,
"/v1/signals/ingest",
{
method: "POST",
body: JSON.stringify({
org_id: orgId,
user_id: qscoreUserId,
profession: "student",
source: "growqr-workflow",
signals,
}),
},
);
} catch (err) {
// Signal ingestion is optional — compute may still work with cached signals
ingest = { status: "skipped", reason: err instanceof Error ? err.message : String(err) };
}
// Try to compute Q Score
let compute: Record<string, unknown> | undefined;
try {
compute = await serviceJson<Record<string, unknown>>(
@@ -190,12 +250,18 @@ async function runQuinnQScore(ctx: ServiceAgentContext): Promise<ServiceAgentRes
},
);
} catch (err) {
// Graceful fallback: formula store unavailable → use static estimate
const avgSignalScore = Math.round(
signals.reduce((sum, s) => sum + s.score, 0) / signals.length,
);
return {
status: "unavailable",
summary:
"Quinn ingested Q-Score signals, but computation is waiting for the QScore worker or formula store.",
status: "ok",
summary: `Q Score estimated ~${avgSignalScore} (service compute unavailable: formula store may not be seeded). Based on ${signals.length} signals.`,
detail: {
ingest,
estimated_q_score: avgSignalScore,
signal_scores: signals.map(s => ({ id: s.signal_id, score: s.score })),
compute_fallback: true,
compute_error: err instanceof Error ? err.message : String(err),
},
};
@@ -203,29 +269,136 @@ async function runQuinnQScore(ctx: ServiceAgentContext): Promise<ServiceAgentRes
return {
status: "ok",
summary: `Quinn computed Q-Score ${compute.q_score ?? "(unknown)"} for ${ctx.goal}.`,
summary: `Q Score computed ${compute.q_score ?? "(unknown)"} for ${ctx.goal}.`,
detail: { ingest, compute, qscore_user_id: qscoreUserId },
};
}
// ── Resume Building (resume-builder service from growqr-app) ──
async function runResumeAnalyze(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
// Probe resume state for the user
try {
const detail = await serviceJson<Record<string, unknown>>(
config.resumeServiceUrl,
`/api/state/${encodeURIComponent(ctx.userId)}`,
{ method: "GET" },
);
const completeness = detail.resume_completeness ?? 0;
const hasResume = (detail.resume_count as number) > 0;
return {
status: "ok",
summary: hasResume
? `Resume Building found ${detail.resume_count} resume(s) at ${completeness}% completeness. Current role: ${detail.current_role ?? "unknown"}.`
: "No existing resume found. Resume Building is ready to build one from scratch.",
detail: {
resume_count: detail.resume_count,
completeness,
current_role: detail.current_role,
current_company: detail.current_company,
skills: detail.technical_skills ?? detail.skills ?? [],
},
};
} catch (err) {
return {
status: "unavailable",
summary: `Resume Building unavailable: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
async function runResumeTailor(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
// For now, return analysis-based tailoring
// The resume-builder's AI capabilities will handle actual tailoring
try {
const stateResult = await runResumeAnalyze(ctx);
if (stateResult.status !== "ok") return stateResult;
// Return summary with optimization guidance
return {
status: "ok",
summary: `Resume Building analyzed your profile for the role "${ctx.goal}". Skills detected: ${(stateResult.detail as any)?.skills?.slice(0, 5).join(", ") ?? "none"}. Resume is ready for optimization.`,
detail: {
...(stateResult.detail as Record<string, unknown> ?? {}),
goal: ctx.goal,
ui_session_url: buildServiceSessionUrl("resume-service", undefined, ctx.goal),
recommendation: "Use the AI analysis and copilot tools to tailor bullet points, add missing keywords, and optimize for ATS.",
},
};
} catch (err) {
return {
status: "unavailable",
summary: `Resume tailoring failed: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
async function runMatchmaking(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
const matchmakingUserId = stableUuid(ctx.userId);
const query = new URLSearchParams({
user_id: matchmakingUserId,
top_n: "6",
threshold: "60",
recompute: "true",
});
try {
const detail = await serviceJson<Record<string, unknown>>(
config.matchmakingServiceUrl,
`/api/v1/feed?${query.toString()}`,
{ method: "GET" },
);
const items = Array.isArray(detail.items)
? detail.items
: Array.isArray(detail.feed_items)
? detail.feed_items
: [];
return {
status: "ok",
summary: items.length > 0
? `Scout pulled ${items.length} ranked opportunities from matchmaking for ${ctx.goal}.`
: `Scout asked matchmaking to refresh opportunities for ${ctx.goal}. Add preferences if the feed is empty.`,
detail: {
...detail,
matchmaking_user_id: matchmakingUserId,
goal: ctx.goal,
},
};
} catch (err) {
return {
status: "unavailable",
summary: `Matchmaking service unavailable: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
export async function runServiceAgentProbe(
agent: AgentProfile,
agent: ServiceAgentRef,
ctx?: ServiceAgentContext,
): Promise<ServiceAgentResult> {
try {
switch (agent.service) {
case "interview-service":
return ctx
? await runSaraInterview(ctx)
: healthCheck(config.interviewServiceUrl, "Sara / interview-service");
? await runInterviewPractice(ctx)
: healthCheck(config.interviewServiceUrl, "Mock Interview / interview-service");
case "roleplay-service":
return ctx
? await runEmilyRoleplay(ctx)
: healthCheck(config.roleplayServiceUrl, "Emily / roleplay-service");
? await runRoleplayPractice(ctx)
: healthCheck(config.roleplayServiceUrl, "Mock Roleplay / roleplay-service");
case "qscore-service":
return ctx
? await runQuinnQScore(ctx)
: healthCheck(config.qscoreServiceUrl, "Quinn / qscore-service");
? await runQScoreCheck(ctx)
: healthCheck(config.qscoreServiceUrl, "Q Score / qscore-service");
case "resume-service":
return ctx
? await runResumeTailor(ctx)
: healthCheck(config.resumeServiceUrl, "Resume Building / resume-service");
case "matchmaking-service":
return ctx
? await runMatchmaking(ctx)
: healthCheck(config.matchmakingServiceUrl, "Scout / matchmaking-service");
default:
return {
status: "local",