Unified · Split
| @@ -0,0 +1 @@ | ||
| 1 | +Subproject commit 6f4665dda3af787c50685e440cca21a57816b5a6 | |
| @@ -0,0 +1 @@ | ||
| 1 | +Subproject commit 43c45b2cbe0e64fb7859164a28e29da427c37930 | |
| @@ -0,0 +1,36 @@ | ||
| 1 | +# dependencies (bun install) | |
| 2 | +node_modules | |
| 3 | + | |
| 4 | +# output | |
| 5 | +out | |
| 6 | +dist | |
| 7 | +*.tgz | |
| 8 | + | |
| 9 | +# code coverage | |
| 10 | +coverage | |
| 11 | +*.lcov | |
| 12 | + | |
| 13 | +# logs | |
| 14 | +logs | |
| 15 | +_.log | |
| 16 | +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json | |
| 17 | + | |
| 18 | +# dotenv environment variable files | |
| 19 | +.env | |
| 20 | +.env.production | |
| 21 | +.env.development.local | |
| 22 | +.env.test.local | |
| 23 | +.env.production.local | |
| 24 | +.env.local | |
| 25 | + | |
| 26 | +# caches | |
| 27 | +.eslintcache | |
| 28 | +.cache | |
| 29 | +*.tsbuildinfo | |
| 30 | +.reference/ | |
| 31 | + | |
| 32 | +# IntelliJ based IDEs | |
| 33 | +.idea | |
| 34 | + | |
| 35 | +# Finder (MacOS) folder config | |
| 36 | +.DS_Store | |
| @@ -0,0 +1 @@ | ||
| 1 | +CLAUDE.md | |
| @@ -0,0 +1,120 @@ | ||
| 1 | + | |
| 2 | +Default to using Bun instead of Node.js. | |
| 3 | + | |
| 4 | +- Use `bun <file>` instead of `node <file>` or `ts-node <file>` | |
| 5 | +- Use `bun test` instead of `jest` or `vitest` | |
| 6 | +- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild` | |
| 7 | +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` | |
| 8 | +- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>` | |
| 9 | +- Use `bunx <package> <command>` instead of `npx <package> <command>` | |
| 10 | +- Bun automatically loads .env, so don't use dotenv. | |
| 11 | + | |
| 12 | +## APIs | |
| 13 | + | |
| 14 | +- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`. | |
| 15 | +- `bun:sqlite` for SQLite. Don't use `better-sqlite3`. | |
| 16 | +- `Bun.redis` for Redis. Don't use `ioredis`. | |
| 17 | +- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`. | |
| 18 | +- `WebSocket` is built-in. Don't use `ws`. | |
| 19 | +- Prefer `Bun.file` over `node:fs`'s readFile/writeFile | |
| 20 | +- Bun.$`ls` instead of execa. | |
| 21 | + | |
| 22 | +## Testing | |
| 23 | + | |
| 24 | +Use `bun test` to run tests. | |
| 25 | + | |
| 26 | +```ts#index.test.ts | |
| 27 | +import { test, expect } from "bun:test"; | |
| 28 | + | |
| 29 | +test("hello world", () => { | |
| 30 | + expect(1).toBe(1); | |
| 31 | +}); | |
| 32 | +``` | |
| 33 | + | |
| 34 | +## Frontend | |
| 35 | + | |
| 36 | +Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind. | |
| 37 | + | |
| 38 | +Server: | |
| 39 | + | |
| 40 | +```ts#index.ts | |
| 41 | +import index from "./index.html" | |
| 42 | + | |
| 43 | +Bun.serve({ | |
| 44 | + routes: { | |
| 45 | + "/": index, | |
| 46 | + "/api/users/:id": { | |
| 47 | + GET: (req) => { | |
| 48 | + return new Response(JSON.stringify({ id: req.params.id })); | |
| 49 | + }, | |
| 50 | + }, | |
| 51 | + }, | |
| 52 | + // optional websocket support | |
| 53 | + websocket: { | |
| 54 | + open: (ws) => { | |
| 55 | + ws.send("Hello, world!"); | |
| 56 | + }, | |
| 57 | + message: (ws, message) => { | |
| 58 | + ws.send(message); | |
| 59 | + }, | |
| 60 | + close: (ws) => { | |
| 61 | + // handle close | |
| 62 | + } | |
| 63 | + }, | |
| 64 | + development: { | |
| 65 | + hmr: true, | |
| 66 | + console: true, | |
| 67 | + } | |
| 68 | +}) | |
| 69 | +``` | |
| 70 | + | |
| 71 | +HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle. | |
| 72 | + | |
| 73 | +```html#index.html | |
| 74 | +<html> | |
| 75 | + <body> | |
| 76 | + <h1>Hello, world!</h1> | |
| 77 | + <script type="module" src="./frontend.tsx"></script> | |
| 78 | + </body> | |
| 79 | +</html> | |
| 80 | +``` | |
| 81 | + | |
| 82 | +With the following `frontend.tsx`: | |
| 83 | + | |
| 84 | +```tsx#frontend.tsx | |
| 85 | +import React from "react"; | |
| 86 | +import { createRoot } from "react-dom/client"; | |
| 87 | + | |
| 88 | +// import .css files directly and it works | |
| 89 | +import './index.css'; | |
| 90 | + | |
| 91 | +const root = createRoot(document.body); | |
| 92 | + | |
| 93 | +export default function Frontend() { | |
| 94 | + return <h1>Hello, world!</h1>; | |
| 95 | +} | |
| 96 | + | |
| 97 | +root.render(<Frontend />); | |
| 98 | +``` | |
| 99 | + | |
| 100 | +Then, run index.ts | |
| 101 | + | |
| 102 | +```sh | |
| 103 | +bun --hot ./index.ts | |
| 104 | +``` | |
| 105 | + | |
| 106 | +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`. | |
| 107 | + | |
| 108 | +<!-- effect-solutions:start --> | |
| 109 | +## Effect Best Practices | |
| 110 | + | |
| 111 | +**IMPORTANT:** Always consult effect-solutions before writing Effect code. | |
| 112 | + | |
| 113 | +1. Run `effect-solutions list` to see available guides | |
| 114 | +2. Run `effect-solutions show <topic>...` for relevant patterns (supports multiple topics) | |
| 115 | +3. Search `.reference/effect/` for real implementations (run `effect-solutions setup` first) | |
| 116 | + | |
| 117 | +Topics: quick-start, project-setup, tsconfig, basics, services-and-layers, data-modeling, error-handling, config, testing, cli. | |
| 118 | + | |
| 119 | +Never guess at Effect patterns - check the guide first. | |
| 120 | +<!-- effect-solutions:end --> | |
| @@ -0,0 +1,309 @@ | ||
| 1 | +# OpenAssistant: High-Level Architecture Plan | |
| 2 | + | |
| 3 | +## 1) Product Thesis | |
| 4 | + | |
| 5 | +Build a personal agent platform with OpenClaw-like flexibility, but with a strict trust model: | |
| 6 | + | |
| 7 | +- No free-form shell as the default execution surface. | |
| 8 | +- Actions run through typed TypeScript clients with explicit capability boundaries. | |
| 9 | +- Destructive operations require approval by policy. | |
| 10 | +- Every claim from the model is backed by auditable execution evidence. | |
| 11 | + | |
| 12 | +## 2) Core Principles | |
| 13 | + | |
| 14 | +- Capability-first execution: the model can only call registered client methods. | |
| 15 | +- Policy as code: permissions, approval gates, and risk tiers are explicit config. | |
| 16 | +- Evidence over narration: the assistant response is generated from execution receipts. | |
| 17 | +- Durable automation: reminders/schedules are owned by scheduler state, not model memory. | |
| 18 | +- Portable runtime: Bun + TypeScript monorepo, predictable local and remote deploy paths. | |
| 19 | + | |
| 20 | +## 3) What To Keep vs Replace | |
| 21 | + | |
| 22 | +Keep from OpenClaw: | |
| 23 | + | |
| 24 | +- Gateway + session architecture. | |
| 25 | +- Multi-client model (Discord, CLI, web, device nodes). | |
| 26 | +- Event stream for system and execution updates. | |
| 27 | +- Cron/heartbeat concept for always-on automation. | |
| 28 | + | |
| 29 | +Replace: | |
| 30 | + | |
| 31 | +- Bash-first action model. | |
| 32 | +- Implicit tool use visibility. | |
| 33 | +- Broad account credentials without fine-grained scope boundaries. | |
| 34 | + | |
| 35 | +## 4) System Shape | |
| 36 | + | |
| 37 | +Control plane: | |
| 38 | + | |
| 39 | +- `gateway`: authoritative orchestration service. | |
| 40 | +- `policy-engine`: evaluates if a call is auto-allowed, approval-required, or denied. | |
| 41 | +- `approval-service`: manages pending approvals, TTL, decisions, audit trail. | |
| 42 | +- `scheduler`: durable jobs, reminders, retries, dead-letter. | |
| 43 | +- `event-log`: append-only event stream and query API for UI and audits. | |
| 44 | + | |
| 45 | +Execution plane: | |
| 46 | + | |
| 47 | +- `code-runner`: executes generated TypeScript in sandboxed runtime. | |
| 48 | +- `client-runtime`: injects typed clients into script context. | |
| 49 | +- `tool-host-local`: local client execution with local creds. | |
| 50 | +- `tool-host-remote`: remote tool execution with scoped ephemeral creds. | |
| 51 | + | |
| 52 | +Interaction plane: | |
| 53 | + | |
| 54 | +- `chat-clients`: Discord/web/CLI channels. | |
| 55 | +- `ops-ui`: approvals, receipts, schedules, policy edits, run timelines. | |
| 56 | + | |
| 57 | +## 5) Execution Model (Codemode + Typed Clients) | |
| 58 | + | |
| 59 | +Single model-facing tool: | |
| 60 | + | |
| 61 | +- `run_code({ code, intent, sessionId })`. | |
| 62 | + | |
| 63 | +Injected runtime in script: | |
| 64 | + | |
| 65 | +- `clients.<integration>.<method>(input)` | |
| 66 | +- `Result<T, E>` return style. | |
| 67 | +- No `child_process`, no `fetch` directly, no unrestricted FS/network. | |
| 68 | + | |
| 69 | +Example shape (conceptual): | |
| 70 | + | |
| 71 | +```ts | |
| 72 | +const result = await clients.calendar.updateEvent({ id, patch }) | |
| 73 | +if (result.isError()) { | |
| 74 | + return Result.err(result.error) | |
| 75 | +} | |
| 76 | +return Result.ok(result.value) | |
| 77 | +``` | |
| 78 | + | |
| 79 | +Outcome: | |
| 80 | + | |
| 81 | +- OpenClaw-level composability. | |
| 82 | +- MCP/tool-call-style control boundaries. | |
| 83 | +- Better determinism and auditability than shell command synthesis. | |
| 84 | + | |
| 85 | +## 6) Capability and Permission Model | |
| 86 | + | |
| 87 | +Permission unit: | |
| 88 | + | |
| 89 | +- `integration.resource.action` (example: `vercel.firewall.rule.delete`). | |
| 90 | + | |
| 91 | +Client method metadata: | |
| 92 | + | |
| 93 | +- `risk`: `read | write | destructive | privileged`. | |
| 94 | +- `autoRun`: boolean default. | |
| 95 | +- `requiresApproval`: optional override. | |
| 96 | +- `idempotent`: boolean. | |
| 97 | +- `allowedScopes`: account/project/environment constraints. | |
| 98 | + | |
| 99 | +Policy decision: | |
| 100 | + | |
| 101 | +- `allow` if method scope and risk pass policy. | |
| 102 | +- `ask` if action is sensitive or outside auto-run envelope. | |
| 103 | +- `deny` if forbidden by capability map. | |
| 104 | + | |
| 105 | +## 7) Approval Flow | |
| 106 | + | |
| 107 | +Request path: | |
| 108 | + | |
| 109 | +1. Script calls client method. | |
| 110 | +2. Runtime asks policy engine. | |
| 111 | +3. If `ask`, execution pauses with `ApprovalPending` receipt. | |
| 112 | +4. Approval prompt sent to origin chat + optional ops targets. | |
| 113 | +5. Approver chooses `allow-once`, `allow-window`, `deny`, or `allow-and-policy-update`. | |
| 114 | +6. Execution resumes or returns denial as value. | |
| 115 | + | |
| 116 | +Key UX requirements: | |
| 117 | + | |
| 118 | +- User always sees which exact method and arguments are pending. | |
| 119 | +- Expiration and timeout reason are explicit. | |
| 120 | +- Run status updates: `pending`, `running`, `completed`, `denied`, `failed`. | |
| 121 | + | |
| 122 | +## 8) Receipts and Anti-Lying Guarantees | |
| 123 | + | |
| 124 | +Every side-effectful call emits immutable receipts: | |
| 125 | + | |
| 126 | +- `call.requested` | |
| 127 | +- `call.approved|call.denied` | |
| 128 | +- `call.started` | |
| 129 | +- `call.finished|call.failed` | |
| 130 | + | |
| 131 | +Assistant response rule: | |
| 132 | + | |
| 133 | +- For external actions, summarize from receipts only. | |
| 134 | +- If no receipt exists, assistant must state action was not completed. | |
| 135 | + | |
| 136 | +UI requirement: | |
| 137 | + | |
| 138 | +- Inline "did it really run?" evidence panel per assistant answer. | |
| 139 | + | |
| 140 | +## 9) Scheduler and Reminders | |
| 141 | + | |
| 142 | +Scheduler owns state, not the LLM: | |
| 143 | + | |
| 144 | +- Reminders become scheduler jobs with a stable `jobId`. | |
| 145 | +- Jobs are persisted in a durable store. | |
| 146 | +- Delivery attempts and outcomes are logged as receipts. | |
| 147 | +- If delivery target fails, retries/backoff are deterministic. | |
| 148 | + | |
| 149 | +Contract: | |
| 150 | + | |
| 151 | +- "Remind me tomorrow at 9am" returns a created job receipt immediately. | |
| 152 | +- User can query, edit, pause, or delete by `jobId`. | |
| 153 | + | |
| 154 | +## 10) OpenAPI to Client Pipeline | |
| 155 | + | |
| 156 | +Goal: | |
| 157 | + | |
| 158 | +- Convert API specs into safe typed clients and policy metadata. | |
| 159 | + | |
| 160 | +Pipeline: | |
| 161 | + | |
| 162 | +1. Ingest OpenAPI spec. | |
| 163 | +2. Generate TypeScript client methods. | |
| 164 | +3. Classify methods by risk heuristics and optional provider-specific rules. | |
| 165 | +4. Emit package with runtime validators + method metadata. | |
| 166 | +5. Register integration in gateway catalog. | |
| 167 | + | |
| 168 | +Outputs per integration package: | |
| 169 | + | |
| 170 | +- `client.ts` typed method surface. | |
| 171 | +- `schema.ts` input/output validation. | |
| 172 | +- `policy.ts` default risk/approval metadata. | |
| 173 | +- `README.md` capability list and example usage. | |
| 174 | + | |
| 175 | +## 11) Remote Tools and Ephemeral Credentials | |
| 176 | + | |
| 177 | +Remote execution model: | |
| 178 | + | |
| 179 | +- Gateway requests short-lived scoped token from credential broker. | |
| 180 | +- Tool host executes only permitted client methods. | |
| 181 | +- Token TTL and scope are embedded in receipt for audit. | |
| 182 | + | |
| 183 | +Security baseline: | |
| 184 | + | |
| 185 | +- No long-lived broad API keys in model runtime. | |
| 186 | +- Per-tool/per-method scope limits. | |
| 187 | +- Revocation and rotation built into credential broker. | |
| 188 | + | |
| 189 | +## 12) Monorepo Layout (Bun) | |
| 190 | + | |
| 191 | +```text | |
| 192 | +apps/ | |
| 193 | + gateway/ | |
| 194 | + ops-ui/ | |
| 195 | + discord-client/ | |
| 196 | + cli/ | |
| 197 | +packages/ | |
| 198 | + sdk/ | |
| 199 | + runner/ | |
| 200 | + policy-engine/ | |
| 201 | + approval-service/ | |
| 202 | + scheduler/ | |
| 203 | + event-log/ | |
| 204 | + receipt-model/ | |
| 205 | + integration-registry/ | |
| 206 | + integration-openapi-gen/ | |
| 207 | + integrations/ | |
| 208 | + posthog/ | |
| 209 | + github/ | |
| 210 | + vercel/ | |
| 211 | + gcal/ | |
| 212 | + transport-ws/ | |
| 213 | + transport-http/ | |
| 214 | + shared-types/ | |
| 215 | +``` | |
| 216 | + | |
| 217 | +## 13) Data Model (High-Level) | |
| 218 | + | |
| 219 | +Core entities: | |
| 220 | + | |
| 221 | +- `Session` | |
| 222 | +- `Run` | |
| 223 | +- `Script` | |
| 224 | +- `ToolCall` | |
| 225 | +- `ApprovalRequest` | |
| 226 | +- `Receipt` | |
| 227 | +- `Job` | |
| 228 | +- `CredentialLease` | |
| 229 | +- `PolicySnapshot` | |
| 230 | + | |
| 231 | +Strong recommendation: | |
| 232 | + | |
| 233 | +- Event-sourced append log for execution and approvals. | |
| 234 | +- Materialized read views for chat timeline and dashboard. | |
| 235 | + | |
| 236 | +## 14) MVP Scope (Phase 1) | |
| 237 | + | |
| 238 | +Include: | |
| 239 | + | |
| 240 | +- Gateway + WS event stream. | |
| 241 | +- Code runner with sandboxed TS execution. | |
| 242 | +- Typed integration runtime (manual first: GitHub + Calendar + PostHog). | |
| 243 | +- Policy engine with `read/write/destructive`. | |
| 244 | +- Approval UI in Discord + lightweight web panel. | |
| 245 | +- Scheduler with reminders and hourly monitors. | |
| 246 | +- Receipt-backed final answer generation. | |
| 247 | + | |
| 248 | +Exclude initially: | |
| 249 | + | |
| 250 | +- General bash execution. | |
| 251 | +- Arbitrary plugin execution from untrusted sources. | |
| 252 | +- Complex multi-tenant org model. | |
| 253 | + | |
| 254 | +## 15) Phase Plan | |
| 255 | + | |
| 256 | +Phase A: Foundation | |
| 257 | + | |
| 258 | +- Monorepo skeleton, shared types, event bus, gateway sessions. | |
| 259 | +- Runner sandbox and `run_code` API. | |
| 260 | + | |
| 261 | +Phase B: Trust Layer | |
| 262 | + | |
| 263 | +- Policy engine, approvals, receipt model, auditable run timeline. | |
| 264 | + | |
| 265 | +Phase C: Integrations | |
| 266 | + | |
| 267 | +- Manual clients + openapi codegen pipeline. | |
| 268 | +- Scoped credentials and remote tool host. | |
| 269 | + | |
| 270 | +Phase D: Automation | |
| 271 | + | |
| 272 | +- Scheduler, durable reminders, monitors, retry semantics. | |
| 273 | + | |
| 274 | +Phase E: UX | |
| 275 | + | |
| 276 | +- Discord + web ops surface for approvals and evidence links. | |
| 277 | + | |
| 278 | +## 16) Key Risks and Mitigations | |
| 279 | + | |
| 280 | +Risk: Model-generated TS can still be unsafe. | |
| 281 | +Mitigation: hard sandbox + no direct network/process APIs + capability-injected clients only. | |
| 282 | + | |
| 283 | +Risk: Approval fatigue. | |
| 284 | +Mitigation: risk tiers, approval windows, per-integration trusted scopes. | |
| 285 | + | |
| 286 | +Risk: "Says it did it" regressions. | |
| 287 | +Mitigation: receipt-backed response contract + explicit uncertainty language. | |
| 288 | + | |
| 289 | +Risk: Credential sprawl. | |
| 290 | +Mitigation: brokered ephemeral tokens and strict method-level scopes. | |
| 291 | + | |
| 292 | +## 17) First Technical Decision Set | |
| 293 | + | |
| 294 | +Decide now: | |
| 295 | + | |
| 296 | +- Bun runtime boundary for runner isolation. | |
| 297 | +- Storage stack for append log + views. | |
| 298 | +- Event transport protocol (WS-first, HTTP fallback). | |
| 299 | +- Policy DSL format (JSON policy vs TS policy modules). | |
| 300 | +- Credential broker interface and lease format. | |
| 301 | + | |
| 302 | +## 18) Success Criteria | |
| 303 | + | |
| 304 | +You should be able to do all of the following with evidence: | |
| 305 | + | |
| 306 | +- "Monitor PostHog and ping me at 10M" (job created, triggered, delivered). | |
| 307 | +- "Check Vercel anomaly email and advise" (reads logged, recommendation traceable). | |
| 308 | +- "Change firewall rule" (approval prompted, approved actor recorded, call receipt linked). | |
| 309 | +- "Remind me tomorrow at 9" (deterministic scheduler receipt, guaranteed delivery attempts). | |
| @@ -0,0 +1,324 @@ | ||
| 1 | +# OpenAssistant MVP: Primitive-First Plan | |
| 2 | + | |
| 3 | +## Goal | |
| 4 | + | |
| 5 | +Ship an MVP that is: | |
| 6 | + | |
| 7 | +- Small enough to build fast. | |
| 8 | +- Safe enough to trust. | |
| 9 | +- Extensible enough to add integrations later. | |
| 10 | + | |
| 11 | +Core rule: the system does not guarantee outcomes, it guarantees evidence of attempted/completed calls to integrations. | |
| 12 | + | |
| 13 | +## Non-Goals (MVP) | |
| 14 | + | |
| 15 | +- No default reminder/calendar system. | |
| 16 | +- No giant plugin marketplace. | |
| 17 | +- No many-package architecture upfront. | |
| 18 | +- No bash-first execution path. | |
| 19 | + | |
| 20 | +## MVP Building Blocks (Primitives) | |
| 21 | + | |
| 22 | +### 1) `Run Primitive` | |
| 23 | + | |
| 24 | +Executes model-generated TypeScript in a sandbox with a single injected surface: | |
| 25 | + | |
| 26 | +- `tools.<integration>.<method>(input): Result<ok, err>` | |
| 27 | + | |
| 28 | +No direct shell/network/process access from model code. | |
| 29 | + | |
| 30 | +### 2) `Tool Contract Primitive` | |
| 31 | + | |
| 32 | +Each tool method has metadata: | |
| 33 | + | |
| 34 | +- `id`: `integration.resource.action` | |
| 35 | +- `risk`: `read | write | destructive` | |
| 36 | +- `requiresApproval`: boolean or policy-driven | |
| 37 | +- `schema`: input/output validation | |
| 38 | + | |
| 39 | +This is the capability boundary. | |
| 40 | + | |
| 41 | +### 3) `Policy + Approval Primitive` | |
| 42 | + | |
| 43 | +Policy returns one of: | |
| 44 | + | |
| 45 | +- `allow` | |
| 46 | +- `ask` | |
| 47 | +- `deny` | |
| 48 | + | |
| 49 | +If `ask`, run pauses until explicit decision (`allow-once`, `deny`, optional short-lived window). | |
| 50 | + | |
| 51 | +### 4) `Receipt Primitive` (Proof of Work) | |
| 52 | + | |
| 53 | +Append-only receipts for every tool interaction: | |
| 54 | + | |
| 55 | +- `tool.call.requested` | |
| 56 | +- `tool.call.approved | tool.call.denied` (if applicable) | |
| 57 | +- `tool.call.started` | |
| 58 | +- `tool.call.succeeded | tool.call.failed` | |
| 59 | + | |
| 60 | +These receipts are the source of truth for what actually happened. | |
| 61 | + | |
| 62 | +### 5) `Relevant Tool Activity Primitive` (Evidence Surface) | |
| 63 | + | |
| 64 | +The gateway derives a system-generated summary of relevant tool activity from receipts. | |
| 65 | + | |
| 66 | +- This summary is passed to the model for final response generation. | |
| 67 | +- This same summary is surfaced to the user/client. | |
| 68 | +- The summary is filtered to relevant calls, not a full noisy log. | |
| 69 | + | |
| 70 | +Summary fields (MVP): | |
| 71 | + | |
| 72 | +- `toolId` | |
| 73 | +- `status` (`succeeded | failed | denied | pending`) | |
| 74 | +- `approval` (`required/decision` when applicable) | |
| 75 | +- `when` (timestamp) | |
| 76 | +- `receiptRef` (id/link for audit trail) | |
| 77 | + | |
| 78 | +Relevance rules (MVP): | |
| 79 | + | |
| 80 | +- Include calls from the current run. | |
| 81 | +- Always include failed/denied/pending calls. | |
| 82 | +- Include side-effectful calls (`write | destructive`). | |
| 83 | +- Optionally include key reads used to justify decisions. | |
| 84 | + | |
| 85 | +This is the anti-lying mechanism: responses are grounded in gateway-produced evidence summaries, not free model claims. | |
| 86 | + | |
| 87 | +### 6) `Response Grounding Primitive` | |
| 88 | + | |
| 89 | +Assistant action statements should be based on the relevant tool activity summary. | |
| 90 | + | |
| 91 | +If no successful call exists for an intended action, response must say it could not verify completion. | |
| 92 | + | |
| 93 | +### 7) `Tool Workspace Primitive` (User-Authored Tools) | |
| 94 | + | |
| 95 | +Users (or the agent) can create tools in an assistant workspace directory. | |
| 96 | + | |
| 97 | +- Tool files are TypeScript modules using a shared `defineTool()` contract. | |
| 98 | +- Each tool exports metadata, schema, and handler. | |
| 99 | +- Gateway discovers and reloads tools from the workspace on file change or explicit reload. | |
| 100 | + | |
| 101 | +This enables "write your own tools" without touching core runtime code. | |
| 102 | + | |
| 103 | +### 8) `Auth Profile Primitive` (Claude Max Support) | |
| 104 | + | |
| 105 | +MVP supports Anthropic Claude Max the same way OpenClaw does: | |
| 106 | + | |
| 107 | +- User runs `claude setup-token` outside the app. | |
| 108 | +- User pastes token into OpenAssistant auth command. | |
| 109 | +- Token is stored as an auth profile of `type: "token"` for provider `anthropic`. | |
| 110 | +- Model runs resolve credentials from profile order first, then env fallback. | |
| 111 | + | |
| 112 | +This avoids coupling to Claude CLI internals while still leveraging Claude Max plan auth. | |
| 113 | + | |
| 114 | +## Minimal Repository Shape | |
| 115 | + | |
| 116 | +Keep it simple first: | |
| 117 | + | |
| 118 | +```text | |
| 119 | +apps/ | |
| 120 | + gateway/ # chat loop, run orchestration, ws/http API | |
| 121 | + discord-client/ # primary MVP chat + approval interaction surface | |
| 122 | +packages/ | |
| 123 | + core/ # shared types, Result, receipt models, policy types | |
| 124 | + runtime/ # TS runner sandbox + tool bridge | |
| 125 | + sdk/ # defineTool(), integration client wrappers | |
| 126 | +integrations/ | |
| 127 | + example-calendar/ | |
| 128 | + example-posthog/ | |
| 129 | +``` | |
| 130 | + | |
| 131 | +Only 3 packages. Add more only when pain is real. | |
| 132 | + | |
| 133 | +Notes: | |
| 134 | + | |
| 135 | +- `ops` means operator surface (approvals + receipts), not necessarily a web app. | |
| 136 | +- MVP should use Discord as the operator surface first to avoid UI overhead. | |
| 137 | +- Optional web ops UI can be phase 2. | |
| 138 | + | |
| 139 | +## Data Model (MVP) | |
| 140 | + | |
| 141 | +- `Run { id, sessionId, status, startedAt, endedAt }` | |
| 142 | +- `ToolCall { id, runId, toolId, inputHash, risk }` | |
| 143 | +- `ApprovalRequest { id, toolCallId, status, decidedBy, decidedAt }` | |
| 144 | +- `Receipt { id, runId, toolCallId?, type, ts, payload }` | |
| 145 | +- `RelevantToolActivity { runId, items[] }` (derived view from receipts) | |
| 146 | +- `AuthProfile { id, provider, type, secretRef, expiresAt?, lastUsedAt?, cooldownUntil? }` | |
| 147 | + | |
| 148 | +Use one append-only receipts table/log first. Build read views later. | |
| 149 | + | |
| 150 | +## How “Reminder Truthfulness” Works Without Built-In Reminder System | |
| 151 | + | |
| 152 | +User says: “Remind me tomorrow at 9.” | |
| 153 | + | |
| 154 | +MVP behavior: | |
| 155 | + | |
| 156 | +1. Model chooses an integration tool (calendar/todo/automation/etc). | |
| 157 | +2. Runtime executes that tool call. | |
| 158 | +3. Receipt(s) are written. | |
| 159 | +4. Gateway derives relevant tool activity summary. | |
| 160 | +5. Assistant response + client surface that summary. | |
| 161 | + | |
| 162 | +So: | |
| 163 | + | |
| 164 | +- If scheduling tool call is `succeeded`, response can say it was scheduled and show that tool call. | |
| 165 | +- If scheduling call is `failed/denied/pending`, response should say that and surface the relevant call. | |
| 166 | + | |
| 167 | +No assumption about which reminder provider is used. | |
| 168 | + | |
| 169 | +## User-Authored Tool Flow (MVP) | |
| 170 | + | |
| 171 | +Assistant workspace (example): | |
| 172 | + | |
| 173 | +```text | |
| 174 | +~/.config/openassistant/ | |
| 175 | + tools/ | |
| 176 | + posthog/ | |
| 177 | + client.ts | |
| 178 | + tools.ts | |
| 179 | +``` | |
| 180 | + | |
| 181 | +Tool definition shape (conceptual): | |
| 182 | + | |
| 183 | +```ts | |
| 184 | +export const checkVisitors = defineTool({ | |
| 185 | + id: "posthog.analytics.getVisitors", | |
| 186 | + risk: "read", | |
| 187 | + input: z.object({ projectId: z.string(), website: z.string() }), | |
| 188 | + policy: { default: "allow" }, | |
| 189 | + handler: async (input, ctx) => { /* call posthog */ }, | |
| 190 | +}) | |
| 191 | + | |
| 192 | +export const createThresholdMonitor = defineTool({ | |
| 193 | + id: "posthog.monitor.createThresholdJob", | |
| 194 | + risk: "write", | |
| 195 | + input: z.object({ | |
| 196 | + website: z.string(), | |
| 197 | + threshold: z.number(), | |
| 198 | + cron: z.string(), // e.g. daily 9am | |
| 199 | + }), | |
| 200 | + policy: { default: "ask" }, | |
| 201 | + handler: async (input, ctx) => { /* call scheduler or remote endpoint */ }, | |
| 202 | +}) | |
| 203 | +``` | |
| 204 | + | |
| 205 | +Runtime flow: | |
| 206 | + | |
| 207 | +1. Prompt asks for PostHog threshold monitor. | |
| 208 | +2. Agent writes/updates PostHog tool module in workspace. | |
| 209 | +3. Gateway reloads tool registry. | |
| 210 | +4. Run uses newly available tool methods. | |
| 211 | +5. Every call emits receipts. | |
| 212 | +6. Gateway computes relevant tool activity. | |
| 213 | +7. Assistant + client surface relevant tool activity. | |
| 214 | + | |
| 215 | +What is guaranteed: | |
| 216 | + | |
| 217 | +- Proof that the tool endpoint was called and what happened. | |
| 218 | + | |
| 219 | +What is not guaranteed by core: | |
| 220 | + | |
| 221 | +- Which reminder/scheduler provider is used internally by that tool. | |
| 222 | + | |
| 223 | +## Claude Max Compatibility (OpenClaw Pattern) | |
| 224 | + | |
| 225 | +### Storage | |
| 226 | + | |
| 227 | +Auth store file (example): | |
| 228 | + | |
| 229 | +```text | |
| 230 | +~/.config/openassistant/auth-profiles.json | |
| 231 | +``` | |
| 232 | + | |
| 233 | +Credential shape: | |
| 234 | + | |
| 235 | +```ts | |
| 236 | +type AuthCredential = | |
| 237 | + | { type: "api_key"; provider: string; key: string } | |
| 238 | + | { type: "token"; provider: string; token: string; expires?: number } | |
| 239 | + | { type: "oauth"; provider: string; access: string; refresh: string; expires: number } | |
| 240 | +``` | |
| 241 | + | |
| 242 | +For Claude Max setup-token: | |
| 243 | + | |
| 244 | +- write profile like `anthropic:manual` with `type: "token"`. | |
| 245 | +- mark config profile mode as `token` (oauth-compatible for provider auth logic). | |
| 246 | + | |
| 247 | +### CLI Surface (MVP) | |
| 248 | + | |
| 249 | +```bash | |
| 250 | +openassistant auth anthropic setup-token | |
| 251 | +``` | |
| 252 | + | |
| 253 | +Flow: | |
| 254 | + | |
| 255 | +1. Prompt user to run `claude setup-token`. | |
| 256 | +2. Validate token prefix/shape. | |
| 257 | +3. Save token profile in auth store. | |
| 258 | +4. Set profile as preferred for Anthropic provider in config/order. | |
| 259 | + | |
| 260 | +### Provider Auth Resolution | |
| 261 | + | |
| 262 | +Resolution order: | |
| 263 | + | |
| 264 | +1. Explicit profile override (if run/session pinned). | |
| 265 | +2. Provider profile order from config/store. | |
| 266 | +3. Env fallback (`ANTHROPIC_OAUTH_TOKEN`, then `ANTHROPIC_API_KEY`). | |
| 267 | +4. Error with actionable re-auth instructions. | |
| 268 | + | |
| 269 | +### Runtime Behavior | |
| 270 | + | |
| 271 | +At run start: | |
| 272 | + | |
| 273 | +- Resolve provider auth once for selected model/provider. | |
| 274 | +- Inject credential into model client adapter. | |
| 275 | + | |
| 276 | +On provider failures (rate-limit/auth/billing): | |
| 277 | + | |
| 278 | +- mark profile failure/cooldown. | |
| 279 | +- rotate to next profile when available. | |
| 280 | +- emit receipt/event that profile rotation occurred. | |
| 281 | + | |
| 282 | +This gives Claude Max usability plus long-term multi-profile resilience. | |
| 283 | + | |
| 284 | +## Extensibility Path | |
| 285 | + | |
| 286 | +New integration = add one folder under `integrations/*` implementing: | |
| 287 | + | |
| 288 | +- Tool contract metadata | |
| 289 | +- Runtime client wrapper | |
| 290 | +- Schema validation | |
| 291 | + | |
| 292 | +No core refactor needed if primitives are stable. | |
| 293 | + | |
| 294 | +## MVP Scope Checklist | |
| 295 | + | |
| 296 | +- Sandbox run primitive working. | |
| 297 | +- Tool contract registry working. | |
| 298 | +- Workspace tool discovery/reload working. | |
| 299 | +- Policy/approval gate working. | |
| 300 | +- Receipt log working and queryable. | |
| 301 | +- Relevant tool activity summary derivation working. | |
| 302 | +- Response grounding from relevant tool activity working. | |
| 303 | +- Anthropic setup-token auth flow working (`claude setup-token` paste path). | |
| 304 | +- Provider auth resolver supports profile order + env fallback. | |
| 305 | +- Discord client handles chat + approvals + receipt links. | |
| 306 | +- 2-3 integrations to prove composability. | |
| 307 | + | |
| 308 | +## Suggested Order (Build Sequence) | |
| 309 | + | |
| 310 | +1. `core`: types + receipt/event schema + policy decision enums. | |
| 311 | +2. `runtime`: sandbox executor + injected tool bridge. | |
| 312 | +3. `gateway`: run orchestration + tool registry + persistence + evidence summarization. | |
| 313 | +4. `discord-client`: chat loop + approve/deny actions + receipt viewing links. | |
| 314 | +5. `integrations`: add calendar/posthog/github as examples. | |
| 315 | + | |
| 316 | +## Success Criteria | |
| 317 | + | |
| 318 | +You can reliably demonstrate: | |
| 319 | + | |
| 320 | +- A destructive action pauses for approval and logs full receipt trail. | |
| 321 | +- A non-destructive read auto-runs and logs receipt trail. | |
| 322 | +- Assistant response includes relevant tool activity for claims it makes. | |
| 323 | +- Assistant cannot claim tool success when relevant activity has no success state. | |
| 324 | +- A reminder-style request is only “done” when an integration success receipt exists. | |
| @@ -0,0 +1,15 @@ | ||
| 1 | +# assistant | |
| 2 | + | |
| 3 | +To install dependencies: | |
| 4 | + | |
| 5 | +```bash | |
| 6 | +bun install | |
| 7 | +``` | |
| 8 | + | |
| 9 | +To run: | |
| 10 | + | |
| 11 | +```bash | |
| 12 | +bun run index.ts | |
| 13 | +``` | |
| 14 | + | |
| 15 | +This project was created using `bun init` in bun v1.3.5. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. | |
| @@ -0,0 +1,22 @@ | ||
| 1 | +{ | |
| 2 | + "name": "@openassistant/discord-bot", | |
| 3 | + "private": true, | |
| 4 | + "type": "module", | |
| 5 | + "scripts": { | |
| 6 | + "with-env": "bun run --env-file=../../.env --", | |
| 7 | + "with-env:prod": "bun run --env-file=../../.env.production --", | |
| 8 | + "dev": "bun with-env bun run src/index.ts", | |
| 9 | + "start": "bun with-env bun run src/index.ts", | |
| 10 | + "start:prod": "bun with-env:prod bun run src/index.ts", | |
| 11 | + "typecheck": "tsgo --noEmit --incremental false -p tsconfig.json", | |
| 12 | + "clean": "bun -e \"import { rmSync } from 'node:fs'; for (const p of ['dist','node_modules','.turbo','coverage','.tsbuildinfo']) rmSync(p,{recursive:true,force:true});\"" | |
| 13 | + }, | |
| 14 | + "dependencies": { | |
| 15 | + "@openassistant/core": "workspace:*", | |
| 16 | + "ai": "^6.0.73", | |
| 17 | + "ai-sdk-provider-claude-code": "^3.3.6", | |
| 18 | + "discord.js": "^14.17.3", | |
| 19 | + "effect": "latest", | |
| 20 | + "zod": "^4.3.6" | |
| 21 | + } | |
| 22 | +} | |
| @@ -0,0 +1,109 @@ | ||
| 1 | +import type { ApprovalDecision, ApprovalRequest } from "@openassistant/core"; | |
| 2 | +import { | |
| 3 | + ActionRowBuilder, | |
| 4 | + ButtonBuilder, | |
| 5 | + ButtonStyle, | |
| 6 | + type ButtonInteraction, | |
| 7 | + type MessageCreateOptions, | |
| 8 | +} from "discord.js"; | |
| 9 | +import { ApprovalRegistry } from "./approval-registry.js"; | |
| 10 | + | |
| 11 | +const CUSTOM_ID_PREFIX = "oa.approval"; | |
| 12 | + | |
| 13 | +export class DiscordApprovalBridge { | |
| 14 | + private readonly registry: ApprovalRegistry; | |
| 15 | + | |
| 16 | + constructor(timeoutMs: number = 5 * 60_000) { | |
| 17 | + this.registry = new ApprovalRegistry(timeoutMs); | |
| 18 | + } | |
| 19 | + | |
| 20 | + async requestApproval(params: { | |
| 21 | + request: ApprovalRequest; | |
| 22 | + channel: { send: (message: MessageCreateOptions) => Promise<unknown> }; | |
| 23 | + requesterId: string; | |
| 24 | + }): Promise<ApprovalDecision> { | |
| 25 | + const { request, channel, requesterId } = params; | |
| 26 | + const pending = this.registry.open(request.callId, requesterId); | |
| 27 | + | |
| 28 | + try { | |
| 29 | + const message: MessageCreateOptions = { | |
| 30 | + content: [ | |
| 31 | + `Approval needed for \`${request.toolPath}\``, | |
| 32 | + `Call ID: \`${request.callId}\``, | |
| 33 | + request.inputPreview ? `Input: \`${request.inputPreview}\`` : null, | |
| 34 | + "Only the requesting user can approve or deny.", | |
| 35 | + ] | |
| 36 | + .filter(Boolean) | |
| 37 | + .join("\n"), | |
| 38 | + components: [approvalButtons(request.callId)], | |
| 39 | + }; | |
| 40 | + | |
| 41 | + await channel.send(message); | |
| 42 | + } catch { | |
| 43 | + this.registry.cancel(request.callId, "denied"); | |
| 44 | + } | |
| 45 | + | |
| 46 | + return pending; | |
| 47 | + } | |
| 48 | + | |
| 49 | + async handleInteraction(interaction: ButtonInteraction): Promise<boolean> { | |
| 50 | + const parsed = parseCustomId(interaction.customId); | |
| 51 | + if (!parsed) { | |
| 52 | + return false; | |
| 53 | + } | |
| 54 | + | |
| 55 | + const decision: ApprovalDecision = parsed.action === "approve" ? "approved" : "denied"; | |
| 56 | + const status = this.registry.resolve(parsed.callId, interaction.user.id, decision); | |
| 57 | + | |
| 58 | + if (status === "unauthorized") { | |
| 59 | + await interaction.reply({ | |
| 60 | + content: "Only the requesting user can resolve this approval.", | |
| 61 | + ephemeral: true, | |
| 62 | + }); | |
| 63 | + return true; | |
| 64 | + } | |
| 65 | + | |
| 66 | + if (status === "not_found") { | |
| 67 | + await interaction.reply({ | |
| 68 | + content: "This approval is no longer pending.", | |
| 69 | + ephemeral: true, | |
| 70 | + }); | |
| 71 | + return true; | |
| 72 | + } | |
| 73 | + | |
| 74 | + await interaction.update({ | |
| 75 | + content: `Approval ${decision} by <@${interaction.user.id}> for \`${parsed.callId}\`.`, | |
| 76 | + components: [], | |
| 77 | + }); | |
| 78 | + return true; | |
| 79 | + } | |
| 80 | +} | |
| 81 | + | |
| 82 | +function approvalButtons(callId: string): ActionRowBuilder<ButtonBuilder> { | |
| 83 | + return new ActionRowBuilder<ButtonBuilder>().addComponents( | |
| 84 | + new ButtonBuilder() | |
| 85 | + .setCustomId(`${CUSTOM_ID_PREFIX}:${callId}:approve`) | |
| 86 | + .setLabel("Approve") | |
| 87 | + .setStyle(ButtonStyle.Success), | |
| 88 | + new ButtonBuilder() | |
| 89 | + .setCustomId(`${CUSTOM_ID_PREFIX}:${callId}:deny`) | |
| 90 | + .setLabel("Deny") | |
| 91 | + .setStyle(ButtonStyle.Danger), | |
| 92 | + ); | |
| 93 | +} | |
| 94 | + | |
| 95 | +function parseCustomId(customId: string): { callId: string; action: "approve" | "deny" } | null { | |
| 96 | + const parts = customId.split(":"); | |
| 97 | + if (parts.length !== 3 || parts[0] !== CUSTOM_ID_PREFIX) { | |
| 98 | + return null; | |
| 99 | + } | |
| 100 | + const callId = parts[1]; | |
| 101 | + if (!callId) { | |
| 102 | + return null; | |
| 103 | + } | |
| 104 | + const action = parts[2]; | |
| 105 | + if (action !== "approve" && action !== "deny") { | |
| 106 | + return null; | |
| 107 | + } | |
| 108 | + return { callId, action }; | |
| 109 | +} | |
| @@ -0,0 +1,38 @@ | ||
| 1 | +import { describe, expect, it } from "vitest"; | |
| 2 | +import { ApprovalRegistry } from "./approval-registry.js"; | |
| 3 | + | |
| 4 | +describe("ApprovalRegistry", () => { | |
| 5 | + it("resolves for requester", async () => { | |
| 6 | + const registry = new ApprovalRegistry(1_000); | |
| 7 | + const pending = registry.open("call_1", "user_a"); | |
| 8 | + | |
| 9 | + const result = registry.resolve("call_1", "user_a", "approved"); | |
| 10 | + const decision = await pending; | |
| 11 | + | |
| 12 | + expect(result).toBe("resolved"); | |
| 13 | + expect(decision).toBe("approved"); | |
| 14 | + expect(registry.size()).toBe(0); | |
| 15 | + }); | |
| 16 | + | |
| 17 | + it("rejects unauthorized actor", async () => { | |
| 18 | + const registry = new ApprovalRegistry(1_000); | |
| 19 | + const pending = registry.open("call_2", "user_a"); | |
| 20 | + | |
| 21 | + const unauthorized = registry.resolve("call_2", "user_b", "approved"); | |
| 22 | + expect(unauthorized).toBe("unauthorized"); | |
| 23 | + expect(registry.size()).toBe(1); | |
| 24 | + | |
| 25 | + registry.cancel("call_2"); | |
| 26 | + const decision = await pending; | |
| 27 | + expect(decision).toBe("denied"); | |
| 28 | + }); | |
| 29 | + | |
| 30 | + it("times out to denied", async () => { | |
| 31 | + const registry = new ApprovalRegistry(10); | |
| 32 | + const pending = registry.open("call_3", "user_a"); | |
| 33 | + | |
| 34 | + const decision = await pending; | |
| 35 | + expect(decision).toBe("denied"); | |
| 36 | + expect(registry.size()).toBe(0); | |
| 37 | + }); | |
| 38 | +}); | |
| @@ -0,0 +1,67 @@ | ||
| 1 | +import type { ApprovalDecision } from "@openassistant/core"; | |
| 2 | + | |
| 3 | +type PendingApproval = { | |
| 4 | + requesterId: string; | |
| 5 | + resolve: (decision: ApprovalDecision) => void; | |
| 6 | + timeout: ReturnType<typeof setTimeout>; | |
| 7 | +}; | |
| 8 | + | |
| 9 | +export type ResolveApprovalResult = "resolved" | "not_found" | "unauthorized"; | |
| 10 | + | |
| 11 | +export class ApprovalRegistry { | |
| 12 | + private readonly pending = new Map<string, PendingApproval>(); | |
| 13 | + | |
| 14 | + constructor(private readonly timeoutMs: number = 5 * 60_000) {} | |
| 15 | + | |
| 16 | + open(callId: string, requesterId: string): Promise<ApprovalDecision> { | |
| 17 | + if (this.pending.has(callId)) { | |
| 18 | + throw new Error(`Approval already pending: ${callId}`); | |
| 19 | + } | |
| 20 | + | |
| 21 | + return new Promise<ApprovalDecision>((resolve) => { | |
| 22 | + const timeout = setTimeout(() => { | |
| 23 | + this.finish(callId, "denied"); | |
| 24 | + }, this.timeoutMs); | |
| 25 | + | |
| 26 | + this.pending.set(callId, { | |
| 27 | + requesterId, | |
| 28 | + resolve, | |
| 29 | + timeout, | |
| 30 | + }); | |
| 31 | + }); | |
| 32 | + } | |
| 33 | + | |
| 34 | + resolve(callId: string, actorId: string, decision: ApprovalDecision): ResolveApprovalResult { | |
| 35 | + const entry = this.pending.get(callId); | |
| 36 | + if (!entry) { | |
| 37 | + return "not_found"; | |
| 38 | + } | |
| 39 | + if (entry.requesterId !== actorId) { | |
| 40 | + return "unauthorized"; | |
| 41 | + } | |
| 42 | + | |
| 43 | + this.finish(callId, decision); | |
| 44 | + return "resolved"; | |
| 45 | + } | |
| 46 | + | |
| 47 | + cancel(callId: string, decision: ApprovalDecision = "denied"): void { | |
| 48 | + if (!this.pending.has(callId)) { | |
| 49 | + return; | |
| 50 | + } | |
| 51 | + this.finish(callId, decision); | |
| 52 | + } | |
| 53 | + | |
| 54 | + size(): number { | |
| 55 | + return this.pending.size; | |
| 56 | + } | |
| 57 | + | |
| 58 | + private finish(callId: string, decision: ApprovalDecision): void { | |
| 59 | + const entry = this.pending.get(callId); | |
| 60 | + if (!entry) { | |
| 61 | + return; | |
| 62 | + } | |
| 63 | + this.pending.delete(callId); | |
| 64 | + clearTimeout(entry.timeout); | |
| 65 | + entry.resolve(decision); | |
| 66 | + } | |
| 67 | +} | |
| @@ -0,0 +1,32 @@ | ||
| 1 | +export interface CalendarEvent { | |
| 2 | + id: string; | |
| 3 | + title: string; | |
| 4 | + startsAt: string; | |
| 5 | + notes?: string; | |
| 6 | +} | |
| 7 | + | |
| 8 | +export interface CalendarUpdateInput { | |
| 9 | + title: string; | |
| 10 | + startsAt: string; | |
| 11 | + notes?: string; | |
| 12 | +} | |
| 13 | + | |
| 14 | +export class InMemoryCalendarStore { | |
| 15 | + private readonly events = new Map<string, CalendarEvent>(); | |
| 16 | + | |
| 17 | + update(input: CalendarUpdateInput): CalendarEvent { | |
| 18 | + const id = `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`; | |
| 19 | + const event: CalendarEvent = { | |
| 20 | + id, | |
| 21 | + title: input.title, | |
| 22 | + startsAt: input.startsAt, | |
| 23 | + ...(input.notes ? { notes: input.notes } : {}), | |
| 24 | + }; | |
| 25 | + this.events.set(id, event); | |
| 26 | + return event; | |
| 27 | + } | |
| 28 | + | |
| 29 | + list(): CalendarEvent[] { | |
| 30 | + return [...this.events.values()]; | |
| 31 | + } | |
| 32 | +} | |
| @@ -0,0 +1,77 @@ | ||
| 1 | +import { describe, expect, it } from "vitest"; | |
| 2 | +import { generateCodeFromPrompt } from "./codegen.js"; | |
| 3 | + | |
| 4 | +describe("generateCodeFromPrompt", () => { | |
| 5 | + it("maps calendar prompts with heuristic provider", async () => { | |
| 6 | + const now = new Date("2026-02-06T10:00:00.000Z"); | |
| 7 | + const generated = await generateCodeFromPrompt( | |
| 8 | + "update my calendar to put dinner with ella at 5 pm", | |
| 9 | + { now, provider: "heuristic" }, | |
| 10 | + ); | |
| 11 | + | |
| 12 | + expect(generated.provider).toBe("heuristic"); | |
| 13 | + expect(generated.code).toContain("tools.calendar.update"); | |
| 14 | + expect(generated.code.toLowerCase()).toContain("dinner with ella"); | |
| 15 | + expect(generated.rationale).toContain("calendar.update"); | |
| 16 | + }); | |
| 17 | + | |
| 18 | + it("supports direct code mode via code prefix", async () => { | |
| 19 | + const generated = await generateCodeFromPrompt("code: return 42;", { | |
| 20 | + provider: "heuristic", | |
| 21 | + }); | |
| 22 | + expect(generated.code).toBe("return 42;"); | |
| 23 | + expect(generated.rationale).toContain("direct code"); | |
| 24 | + }); | |
| 25 | + | |
| 26 | + it("parses structured output from claude provider", async () => { | |
| 27 | + const generated = await generateCodeFromPrompt("make an event", { | |
| 28 | + provider: "claude", | |
| 29 | + generateWithClaude: async () => ({ | |
| 30 | + code: "const event = await tools.calendar.update({ title: 'Dinner', startsAt: '2026-02-06T17:00:00.000Z' }); return { event };", | |
| 31 | + rationale: "Mapped user prompt to calendar.update", | |
| 32 | + }), | |
| 33 | + }); | |
| 34 | + | |
| 35 | + expect(generated.provider).toBe("claude"); | |
| 36 | + expect(generated.code).toContain("tools.calendar.update"); | |
| 37 | + expect(generated.rationale).toContain("Mapped"); | |
| 38 | + }); | |
| 39 | + | |
| 40 | + it("handles multi-line calendar lists with relative days", async () => { | |
| 41 | + const now = new Date("2026-02-06T10:00:00.000Z"); // Friday | |
| 42 | + const prompt = [ | |
| 43 | + "Please add the following to my calendar:", | |
| 44 | + "", | |
| 45 | + "Dinner 5pm tomorrow", | |
| 46 | + "", | |
| 47 | + "Lunch 2 pm sunday", | |
| 48 | + "", | |
| 49 | + "Breakfast 3 am Monday", | |
| 50 | + ].join("\n"); | |
| 51 | + | |
| 52 | + const generated = await generateCodeFromPrompt(prompt, { | |
| 53 | + now, | |
| 54 | + provider: "heuristic", | |
| 55 | + }); | |
| 56 | + | |
| 57 | + expect(generated.code.match(/tools\.calendar\.update\(/g)?.length).toBe(3); | |
| 58 | + expect(generated.code).toContain('"title":"Dinner"'); | |
| 59 | + expect(generated.code).toContain('"title":"Lunch"'); | |
| 60 | + expect(generated.code).toContain('"title":"Breakfast"'); | |
| 61 | + expect(generated.code).toContain('return { message: "Calendar updated", events };'); | |
| 62 | + | |
| 63 | + const startsAt = [...generated.code.matchAll(/"startsAt":"([^"]+)"/g)] | |
| 64 | + .map((match) => match[1]) | |
| 65 | + .filter((value): value is string => typeof value === "string"); | |
| 66 | + expect(startsAt).toHaveLength(3); | |
| 67 | + if (startsAt.length !== 3) { | |
| 68 | + throw new Error("expected 3 startsAt values"); | |
| 69 | + } | |
| 70 | + const dinnerAt = new Date(startsAt[0]!); | |
| 71 | + const lunchAt = new Date(startsAt[1]!); | |
| 72 | + const breakfastAt = new Date(startsAt[2]!); | |
| 73 | + expect(dinnerAt.getDay()).toBe(6); // Saturday | |
| 74 | + expect(lunchAt.getDay()).toBe(0); // Sunday | |
| 75 | + expect(breakfastAt.getDay()).toBe(1); // Monday | |
| 76 | + }); | |
| 77 | +}); | |
| @@ -0,0 +1,322 @@ | ||
| 1 | +import { generateObject } from "ai"; | |
| 2 | +import { createClaudeCode } from "ai-sdk-provider-claude-code"; | |
| 3 | +import { z } from "zod"; | |
| 4 | + | |
| 5 | +export interface GeneratedCode { | |
| 6 | + code: string; | |
| 7 | + rationale: string; | |
| 8 | + provider: "claude" | "heuristic"; | |
| 9 | +} | |
| 10 | + | |
| 11 | +interface CodegenOptions { | |
| 12 | + now?: Date; | |
| 13 | + provider?: "claude" | "heuristic"; | |
| 14 | + generateWithClaude?: (params: { prompt: string; now: Date }) => Promise<{ code: string; rationale: string }>; | |
| 15 | +} | |
| 16 | + | |
| 17 | +const DEFAULT_PROVIDER = resolveDefaultProvider(); | |
| 18 | +const DEFAULT_CLAUDE_MODEL = readEnv("OPENASSISTANT_CLAUDE_MODEL")?.trim() || "sonnet"; | |
| 19 | +const CLAUDE_TIMEOUT_MS = Number(readEnv("OPENASSISTANT_CLAUDE_TIMEOUT_MS") ?? 30_000); | |
| 20 | +const CLAUDE_PATH = readEnv("OPENASSISTANT_CLAUDE_PATH")?.trim(); | |
| 21 | +const CODEGEN_SCHEMA = z.object({ | |
| 22 | + code: z.string().min(1), | |
| 23 | + rationale: z.string().min(1), | |
| 24 | +}); | |
| 25 | + | |
| 26 | +const claudeCode = createClaudeCode({ | |
| 27 | + defaultSettings: { | |
| 28 | + permissionMode: "dontAsk", | |
| 29 | + tools: [], | |
| 30 | + maxTurns: 1, | |
| 31 | + ...(CLAUDE_PATH ? { pathToClaudeCodeExecutable: CLAUDE_PATH } : {}), | |
| 32 | + }, | |
| 33 | +}); | |
| 34 | + | |
| 35 | +export async function generateCodeFromPrompt( | |
| 36 | + prompt: string, | |
| 37 | + options: CodegenOptions = {}, | |
| 38 | +): Promise<GeneratedCode> { | |
| 39 | + const now = options.now ?? new Date(); | |
| 40 | + const provider = options.provider ?? DEFAULT_PROVIDER; | |
| 41 | + | |
| 42 | + if (provider === "claude") { | |
| 43 | + try { | |
| 44 | + const generated = await (options.generateWithClaude ?? generateWithClaude)({ | |
| 45 | + prompt, | |
| 46 | + now, | |
| 47 | + }); | |
| 48 | + return { ...generated, provider: "claude" }; | |
| 49 | + } catch (error) { | |
| 50 | + const fallback = generateHeuristic(prompt, now); | |
| 51 | + return { | |
| 52 | + ...fallback, | |
| 53 | + provider: "heuristic", | |
| 54 | + rationale: `${fallback.rationale} Claude fallback: ${describeUnknown(error)}`, | |
| 55 | + }; | |
| 56 | + } | |
| 57 | + } | |
| 58 | + | |
| 59 | + const generated = generateHeuristic(prompt, now); | |
| 60 | + return { ...generated, provider: "heuristic" }; | |
| 61 | +} | |
| 62 | + | |
| 63 | +async function generateWithClaude(params: { | |
| 64 | + prompt: string; | |
| 65 | + now: Date; | |
| 66 | +}): Promise<Omit<GeneratedCode, "provider">> { | |
| 67 | + const { prompt, now } = params; | |
| 68 | + const abortController = new AbortController(); | |
| 69 | + const timeout = setTimeout(() => abortController.abort(), CLAUDE_TIMEOUT_MS); | |
| 70 | + try { | |
| 71 | + const result = await generateObject({ | |
| 72 | + model: claudeCode(DEFAULT_CLAUDE_MODEL), | |
| 73 | + schema: CODEGEN_SCHEMA, | |
| 74 | + prompt: buildPlannerPrompt(prompt, now), | |
| 75 | + temperature: 0, | |
| 76 | + abortSignal: abortController.signal, | |
| 77 | + }); | |
| 78 | + | |
| 79 | + return result.object; | |
| 80 | + } finally { | |
| 81 | + clearTimeout(timeout); | |
| 82 | + } | |
| 83 | +} | |
| 84 | + | |
| 85 | +function buildPlannerPrompt(userPrompt: string, now: Date): string { | |
| 86 | + return [ | |
| 87 | + "You are a code generator for a Bun TypeScript codemode runner.", | |
| 88 | + "Return a function body only (no markdown, no fences, no explanations in code).", | |
| 89 | + "The runtime invokes: new AsyncFunction('tools', code).", | |
| 90 | + "Available tools:", | |
| 91 | + "- await tools.calendar.update({ title: string, startsAt: string, notes?: string })", | |
| 92 | + "- await tools.calendar.list()", | |
| 93 | + "Rules:", | |
| 94 | + "- Use await for tool calls.", | |
| 95 | + "- If request implies creating/updating a calendar event, call tools.calendar.update.", | |
| 96 | + "- startsAt must be an ISO-8601 string.", | |
| 97 | + "- If time/date missing, use current timestamp.", | |
| 98 | + "- Always return a final object (e.g. { message, event }).", | |
| 99 | + `Current timestamp: ${now.toISOString()}`, | |
| 100 | + `User request: ${userPrompt}`, | |
| 101 | + ].join("\n"); | |
| 102 | +} | |
| 103 | + | |
| 104 | +function resolveDefaultProvider(): "claude" | "heuristic" { | |
| 105 | + const configured = readEnv("OPENASSISTANT_CODEGEN_PROVIDER")?.trim().toLowerCase(); | |
| 106 | + if (configured === "heuristic") { | |
| 107 | + return "heuristic"; | |
| 108 | + } | |
| 109 | + return "claude"; | |
| 110 | +} | |
| 111 | + | |
| 112 | +function generateHeuristic(prompt: string, now: Date): Omit<GeneratedCode, "provider"> { | |
| 113 | + const trimmed = prompt.trim(); | |
| 114 | + if (!trimmed) { | |
| 115 | + return { | |
| 116 | + code: "return { message: 'Empty prompt.' };", | |
| 117 | + rationale: "Empty prompt fallback.", | |
| 118 | + }; | |
| 119 | + } | |
| 120 | + | |
| 121 | + if (trimmed.toLowerCase().startsWith("code:")) { | |
| 122 | + return { | |
| 123 | + code: trimmed.slice(5).trim(), | |
| 124 | + rationale: "User provided direct code.", | |
| 125 | + }; | |
| 126 | + } | |
| 127 | + | |
| 128 | + const lower = trimmed.toLowerCase(); | |
| 129 | + if (lower.includes("calendar") || lower.includes("event") || lower.includes("schedule")) { | |
| 130 | + const updates = extractCalendarUpdates(trimmed, now); | |
| 131 | + if (updates.length > 0) { | |
| 132 | + const code = buildCalendarCode(updates); | |
| 133 | + return { | |
| 134 | + code, | |
| 135 | + rationale: | |
| 136 | + updates.length === 1 | |
| 137 | + ? "Mapped prompt to calendar.update tool call." | |
| 138 | + : `Mapped prompt to ${updates.length} calendar.update tool calls.`, | |
| 139 | + }; | |
| 140 | + } | |
| 141 | + } | |
| 142 | + | |
| 143 | + return { | |
| 144 | + code: `return { message: "No tool mapping yet for prompt", prompt: ${JSON.stringify(trimmed)} };`, | |
| 145 | + rationale: "Fallback response when no tool intent is detected.", | |
| 146 | + }; | |
| 147 | +} | |
| 148 | + | |
| 149 | +function inferTitle(prompt: string): string { | |
| 150 | + const match = prompt.match( | |
| 151 | + /(?:put|add|create)\s+(?:an?\s+)?(?:calendar\s+)?(?:event\s+)?(?:to\s+)?(.+?)(?:\s+at\s+|$)/i, | |
| 152 | + ); | |
| 153 | + if (match?.[1]) { | |
| 154 | + return cleanTitle(match[1]); | |
| 155 | + } | |
| 156 | + return "Untitled event"; | |
| 157 | +} | |
| 158 | + | |
| 159 | +function cleanTitle(input: string): string { | |
| 160 | + return input.replace(/^my\s+/i, "").replace(/^calendar\s+/i, "").trim(); | |
| 161 | +} | |
| 162 | + | |
| 163 | +function inferStartTime(prompt: string, now: Date): string { | |
| 164 | + const time = prompt.match(/(?:\bat\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b/i); | |
| 165 | + if (!time) { | |
| 166 | + return now.toISOString(); | |
| 167 | + } | |
| 168 | + | |
| 169 | + const dayToken = prompt.match( | |
| 170 | + /\b(today|tomorrow|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/i, | |
| 171 | + )?.[1]; | |
| 172 | + return resolveStartsAt(now, Number(time[1]), time[2] ? Number(time[2]) : 0, time[3], dayToken); | |
| 173 | +} | |
| 174 | + | |
| 175 | +function describeUnknown(error: unknown): string { | |
| 176 | + if (error instanceof Error) { | |
| 177 | + return error.message; | |
| 178 | + } | |
| 179 | + return String(error); | |
| 180 | +} | |
| 181 | + | |
| 182 | +function readEnv(key: string): string | undefined { | |
| 183 | + const bun = (globalThis as { Bun?: { env?: Record<string, string | undefined> } }).Bun; | |
| 184 | + return bun?.env?.[key] ?? process.env[key]; | |
| 185 | +} | |
| 186 | + | |
| 187 | +type CalendarUpdateInput = { | |
| 188 | + title: string; | |
| 189 | + startsAt: string; | |
| 190 | + notes: string; | |
| 191 | +}; | |
| 192 | + | |
| 193 | +const WEEKDAY_INDEX: Record<string, number> = { | |
| 194 | + sunday: 0, | |
| 195 | + monday: 1, | |
| 196 | + tuesday: 2, | |
| 197 | + wednesday: 3, | |
| 198 | + thursday: 4, | |
| 199 | + friday: 5, | |
| 200 | + saturday: 6, | |
| 201 | +}; | |
| 202 | + | |
| 203 | +function buildCalendarCode(updates: CalendarUpdateInput[]): string { | |
| 204 | + const lines: string[] = ["const events = [];"]; | |
| 205 | + for (const update of updates) { | |
| 206 | + lines.push(`events.push(await tools.calendar.update(${JSON.stringify(update)}));`); | |
| 207 | + } | |
| 208 | + lines.push('return { message: "Calendar updated", events };'); | |
| 209 | + return lines.join(" "); | |
| 210 | +} | |
| 211 | + | |
| 212 | +function extractCalendarUpdates(prompt: string, now: Date): CalendarUpdateInput[] { | |
| 213 | + const lines = prompt | |
| 214 | + .split(/\r?\n/) | |
| 215 | + .map((line) => line.trim()) | |
| 216 | + .filter((line) => line.length > 0); | |
| 217 | + | |
| 218 | + const updates: CalendarUpdateInput[] = []; | |
| 219 | + for (const line of lines) { | |
| 220 | + if (isInstructionLine(line)) { | |
| 221 | + continue; | |
| 222 | + } | |
| 223 | + const parsed = parseCalendarLine(line, now, prompt); | |
| 224 | + if (parsed) { | |
| 225 | + updates.push(parsed); | |
| 226 | + } | |
| 227 | + } | |
| 228 | + | |
| 229 | + if (updates.length > 0) { | |
| 230 | + return updates; | |
| 231 | + } | |
| 232 | + | |
| 233 | + return [ | |
| 234 | + { | |
| 235 | + title: inferTitle(prompt), | |
| 236 | + startsAt: inferStartTime(prompt, now), | |
| 237 | + notes: `Requested via Discord prompt: ${prompt}`, | |
| 238 | + }, | |
| 239 | + ]; | |
| 240 | +} | |
| 241 | + | |
| 242 | +function isInstructionLine(line: string): boolean { | |
| 243 | + const value = line.toLowerCase(); | |
| 244 | + if (value.includes("calendar") && (value.includes("add") || value.includes("following"))) { | |
| 245 | + return true; | |
| 246 | + } | |
| 247 | + return value.startsWith("please "); | |
| 248 | +} | |
| 249 | + | |
| 250 | +function parseCalendarLine( | |
| 251 | + rawLine: string, | |
| 252 | + now: Date, | |
| 253 | + fullPrompt: string, | |
| 254 | +): CalendarUpdateInput | null { | |
| 255 | + const line = rawLine.replace(/^[-*]\s*/, "").replace(/^\d+[.)]\s*/, "").trim(); | |
| 256 | + const match = line.match( | |
| 257 | + /^(?<title>.+?)\s+(?:at\s+)?(?<hour>\d{1,2})(?::(?<minute>\d{2}))?\s*(?<meridiem>am|pm)\b(?:\s+(?<day>today|tomorrow|monday|tuesday|wednesday|thursday|friday|saturday|sunday))?$/i, | |
| 258 | + ); | |
| 259 | + | |
| 260 | + if (!match?.groups) { | |
| 261 | + return null; | |
| 262 | + } | |
| 263 | + | |
| 264 | + const titleRaw = match.groups.title; | |
| 265 | + const hourRaw = match.groups.hour; | |
| 266 | + const meridiemRaw = match.groups.meridiem; | |
| 267 | + if (!titleRaw || !hourRaw || !meridiemRaw) { | |
| 268 | + return null; | |
| 269 | + } | |
| 270 | + | |
| 271 | + const title = cleanTitle(titleRaw); | |
| 272 | + if (!title) { | |
| 273 | + return null; | |
| 274 | + } | |
| 275 | + | |
| 276 | + const hour = Number(hourRaw); | |
| 277 | + const minute = match.groups.minute ? Number(match.groups.minute) : 0; | |
| 278 | + const meridiem = meridiemRaw; | |
| 279 | + const dayToken = match.groups.day; | |
| 280 | + const startsAt = resolveStartsAt(now, hour, minute, meridiem, dayToken); | |
| 281 | + | |
| 282 | + return { | |
| 283 | + title, | |
| 284 | + startsAt, | |
| 285 | + notes: `Requested via Discord prompt: ${fullPrompt}`, | |
| 286 | + }; | |
| 287 | +} | |
| 288 | + | |
| 289 | +function resolveStartsAt( | |
| 290 | + now: Date, | |
| 291 | + hour12: number, | |
| 292 | + minute: number, | |
| 293 | + meridiemRaw: string | undefined, | |
| 294 | + dayTokenRaw: string | undefined, | |
| 295 | +): string { | |
| 296 | + const meridiem = meridiemRaw?.toLowerCase(); | |
| 297 | + const dayToken = dayTokenRaw?.toLowerCase(); | |
| 298 | + let hour = hour12; | |
| 299 | + | |
| 300 | + if (meridiem === "pm" && hour < 12) { | |
| 301 | + hour += 12; | |
| 302 | + } else if (meridiem === "am" && hour === 12) { | |
| 303 | + hour = 0; | |
| 304 | + } | |
| 305 | + | |
| 306 | + const date = new Date(now); | |
| 307 | + if (dayToken === "tomorrow") { | |
| 308 | + date.setDate(date.getDate() + 1); | |
| 309 | + } else if (dayToken) { | |
| 310 | + const target = WEEKDAY_INDEX[dayToken]; | |
| 311 | + if (target === undefined) { | |
| 312 | + date.setHours(hour, minute, 0, 0); | |
| 313 | + return date.toISOString(); | |
| 314 | + } | |
| 315 | + const current = date.getDay(); | |
| 316 | + const delta = (target - current + 7) % 7; | |
| 317 | + date.setDate(date.getDate() + delta); | |
| 318 | + } | |
| 319 | + | |
| 320 | + date.setHours(hour, minute, 0, 0); | |
| 321 | + return date.toISOString(); | |
| 322 | +} | |
| @@ -0,0 +1,61 @@ | ||
| 1 | +import type { CodeModeRunResult } from "@openassistant/core"; | |
| 2 | + | |
| 3 | +export function formatDiscordResponse(params: { | |
| 4 | + prompt: string; | |
| 5 | + generatedCode: string; | |
| 6 | + rationale: string; | |
| 7 | + provider: "claude" | "heuristic"; | |
| 8 | + result: CodeModeRunResult; | |
| 9 | +}): string { | |
| 10 | + const { prompt, generatedCode, rationale, provider, result } = params; | |
| 11 | + const sections = [ | |
| 12 | + `Prompt: ${prompt}`, | |
| 13 | + `Planner (${provider}): ${rationale}`, | |
| 14 | + "Code:", | |
| 15 | + fenced("ts", generatedCode), | |
| 16 | + result.ok ? `Result: ${safeJson(result.value)}` : `Error: ${result.error}`, | |
| 17 | + "Receipts:", | |
| 18 | + formatReceipts(result.receipts), | |
| 19 | + ]; | |
| 20 | + | |
| 21 | + return truncateDiscord(sections.join("\n\n")); | |
| 22 | +} | |
| 23 | + | |
| 24 | +function formatReceipts(receipts: CodeModeRunResult["receipts"]): string { | |
| 25 | + if (receipts.length === 0) { | |
| 26 | + return "- none"; | |
| 27 | + } | |
| 28 | + return receipts | |
| 29 | + .map((receipt) => { | |
| 30 | + const parts = [ | |
| 31 | + `- \`${receipt.toolPath}\``, | |
| 32 | + `decision=${receipt.decision}`, | |
| 33 | + `status=${receipt.status}`, | |
| 34 | + ]; | |
| 35 | + if (receipt.inputPreview) { | |
| 36 | + parts.push(`input="${receipt.inputPreview}"`); | |
| 37 | + } | |
| 38 | + return parts.join(" "); | |
| 39 | + }) | |
| 40 | + .join("\n"); | |
| 41 | +} | |
| 42 | + | |
| 43 | +function safeJson(value: unknown): string { | |
| 44 | + try { | |
| 45 | + return JSON.stringify(value); | |
| 46 | + } catch { | |
| 47 | + return String(value); | |
| 48 | + } | |
| 49 | +} | |
| 50 | + | |
| 51 | +function fenced(lang: string, content: string): string { | |
| 52 | + return `\`\`\`${lang}\n${content}\n\`\`\``; | |
| 53 | +} | |
| 54 | + | |
| 55 | +function truncateDiscord(value: string): string { | |
| 56 | + const limit = 1_900; | |
| 57 | + if (value.length <= limit) { | |
| 58 | + return value; | |
| 59 | + } | |
| 60 | + return `${value.slice(0, limit)}\n...`; | |
| 61 | +} | |
| @@ -0,0 +1,102 @@ | ||
| 1 | +import { createCodeModeRunner } from "@openassistant/core"; | |
| 2 | +import { | |
| 3 | + ChannelType, | |
| 4 | + Client, | |
| 5 | + Events, | |
| 6 | + GatewayIntentBits, | |
| 7 | + type Message, | |
| 8 | + type MessageCreateOptions, | |
| 9 | + Partials, | |
| 10 | +} from "discord.js"; | |
| 11 | +import { Effect } from "effect"; | |
| 12 | +import { DiscordApprovalBridge } from "./approval-bridge.js"; | |
| 13 | +import { InMemoryCalendarStore } from "./calendar-store.js"; | |
| 14 | +import { generateCodeFromPrompt } from "./codegen.js"; | |
| 15 | +import { formatDiscordResponse } from "./format-response.js"; | |
| 16 | +import { createToolTree } from "./tools.js"; | |
| 17 | + | |
| 18 | +const token = Bun.env.DISCORD_BOT_TOKEN; | |
| 19 | +if (!token) { | |
| 20 | + throw new Error("Missing DISCORD_BOT_TOKEN"); | |
| 21 | +} | |
| 22 | + | |
| 23 | +const approvalTimeoutMs = Number(Bun.env.OPENASSISTANT_APPROVAL_TIMEOUT_MS ?? 300_000); | |
| 24 | + | |
| 25 | +const client = new Client({ | |
| 26 | + intents: [ | |
| 27 | + GatewayIntentBits.Guilds, | |
| 28 | + GatewayIntentBits.GuildMessages, | |
| 29 | + GatewayIntentBits.DirectMessages, | |
| 30 | + GatewayIntentBits.MessageContent, | |
| 31 | + ], | |
| 32 | + partials: [Partials.Channel], | |
| 33 | +}); | |
| 34 | + | |
| 35 | +const approvalBridge = new DiscordApprovalBridge(approvalTimeoutMs); | |
| 36 | +const calendarStore = new InMemoryCalendarStore(); | |
| 37 | +const tools = createToolTree(calendarStore); | |
| 38 | + | |
| 39 | +client.once(Events.ClientReady, (readyClient) => { | |
| 40 | + console.log(`[discord-bot] logged in as ${readyClient.user.tag}`); | |
| 41 | +}); | |
| 42 | + | |
| 43 | +client.on(Events.InteractionCreate, async (interaction) => { | |
| 44 | + if (!interaction.isButton()) { | |
| 45 | + return; | |
| 46 | + } | |
| 47 | + await approvalBridge.handleInteraction(interaction); | |
| 48 | +}); | |
| 49 | + | |
| 50 | +client.on(Events.MessageCreate, async (message) => { | |
| 51 | + if (shouldIgnore(message)) { | |
| 52 | + return; | |
| 53 | + } | |
| 54 | + await handleMessage(message); | |
| 55 | +}); | |
| 56 | + | |
| 57 | +await client.login(token); | |
| 58 | + | |
| 59 | +function shouldIgnore(message: Message): boolean { | |
| 60 | + return message.author.bot || message.content.trim().length === 0 || message.channel.type !== ChannelType.DM; | |
| 61 | +} | |
| 62 | + | |
| 63 | +async function handleMessage(message: Message): Promise<void> { | |
| 64 | + const ack = await message.reply("Working on it..."); | |
| 65 | + const approvalChannel = message.channel as unknown; | |
| 66 | + if ( | |
| 67 | + !approvalChannel || | |
| 68 | + typeof approvalChannel !== "object" || | |
| 69 | + !("send" in approvalChannel) || | |
| 70 | + typeof (approvalChannel as { send?: unknown }).send !== "function" | |
| 71 | + ) { | |
| 72 | + await ack.edit("This channel does not support approval prompts."); | |
| 73 | + return; | |
| 74 | + } | |
| 75 | + | |
| 76 | + const generated = await generateCodeFromPrompt(message.content); | |
| 77 | + | |
| 78 | + const runner = createCodeModeRunner({ | |
| 79 | + tools, | |
| 80 | + requestApproval: (request) => | |
| 81 | + Effect.tryPromise({ | |
| 82 | + try: () => | |
| 83 | + approvalBridge.requestApproval({ | |
| 84 | + request, | |
| 85 | + channel: approvalChannel as { send: (message: MessageCreateOptions) => Promise<unknown> }, | |
| 86 | + requesterId: message.author.id, | |
| 87 | + }), | |
| 88 | + catch: (error) => error, | |
| 89 | + }), | |
| 90 | + }); | |
| 91 | + | |
| 92 | + const result = await Effect.runPromise(runner.run({ code: generated.code })); | |
| 93 | + const response = formatDiscordResponse({ | |
| 94 | + prompt: message.content, | |
| 95 | + generatedCode: generated.code, | |
| 96 | + rationale: generated.rationale, | |
| 97 | + provider: generated.provider, | |
| 98 | + result, | |
| 99 | + }); | |
| 100 | + | |
| 101 | + await ack.edit(response); | |
| 102 | +} | |
| @@ -0,0 +1,22 @@ | ||
| 1 | +import { defineTool, type ToolTree } from "@openassistant/core"; | |
| 2 | +import { Effect } from "effect"; | |
| 3 | +import type { InMemoryCalendarStore } from "./calendar-store.js"; | |
| 4 | + | |
| 5 | +export function createToolTree(calendarStore: InMemoryCalendarStore): ToolTree { | |
| 6 | + return { | |
| 7 | + calendar: { | |
| 8 | + update: defineTool({ | |
| 9 | + kind: "write", | |
| 10 | + approval: "required", | |
| 11 | + run: (input: { title: string; startsAt: string; notes?: string }) => | |
| 12 | + Effect.sync(() => calendarStore.update(input)), | |
| 13 | + previewInput: (input) => `${input.title} @ ${input.startsAt}`, | |
| 14 | + }), | |
| 15 | + list: defineTool({ | |
| 16 | + kind: "read", | |
| 17 | + approval: "auto", | |
| 18 | + run: () => Effect.sync(() => calendarStore.list()), | |
| 19 | + }), | |
| 20 | + }, | |
| 21 | + }; | |
| 22 | +} | |
| @@ -0,0 +1,9 @@ | ||
| 1 | +{ | |
| 2 | + "extends": "../../tsconfig.base.json", | |
| 3 | + "compilerOptions": { | |
| 4 | + "rootDir": "src" | |
| 5 | + }, | |
| 6 | + "include": [ | |
| 7 | + "src/**/*.ts" | |
| 8 | + ] | |
| 9 | +} | |
| @@ -0,0 +1,21 @@ | ||
| 1 | +{ | |
| 2 | + "name": "@openassistant/gateway", | |
| 3 | + "private": true, | |
| 4 | + "type": "module", | |
| 5 | + "exports": { | |
| 6 | + ".": "./src/index.ts" | |
| 7 | + }, | |
| 8 | + "scripts": { | |
| 9 | + "with-env": "bun run --env-file=../../.env --", | |
| 10 | + "with-env:prod": "bun run --env-file=../../.env.production --", | |
| 11 | + "dev": "bun with-env bun run src/index.ts", | |
| 12 | + "start": "bun with-env bun run src/index.ts", | |
| 13 | + "start:prod": "bun with-env:prod bun run src/index.ts", | |
| 14 | + "typecheck": "tsgo --noEmit --incremental false -p tsconfig.json", | |
| 15 | + "clean": "bun -e \"import { rmSync } from 'node:fs'; for (const p of ['dist','node_modules','.turbo','coverage','.tsbuildinfo']) rmSync(p,{recursive:true,force:true});\"" | |
| 16 | + }, | |
| 17 | + "dependencies": { | |
| 18 | + "@openassistant/core": "workspace:*", | |
| 19 | + "effect": "latest" | |
| 20 | + } | |
| 21 | +} | |
| @@ -0,0 +1,10 @@ | ||
| 1 | +import { healthcheck } from "@openassistant/core"; | |
| 2 | +import { Effect } from "effect"; | |
| 3 | + | |
| 4 | +const program = Effect.flatMap(healthcheck, (status) => | |
| 5 | + Effect.sync(() => { | |
| 6 | + console.log(`[gateway] healthcheck=${status}`); | |
| 7 | + }), | |
| 8 | +); | |
| 9 | + | |
| 10 | +await Effect.runPromise(program); | |
| @@ -0,0 +1,9 @@ | ||
| 1 | +{ | |
| 2 | + "extends": "../../tsconfig.base.json", | |
| 3 | + "compilerOptions": { | |
| 4 | + "rootDir": "src" | |
| 5 | + }, | |
| 6 | + "include": [ | |
| 7 | + "src/**/*.ts" | |
| 8 | + ] | |
| 9 | +} | |
| @@ -0,0 +1,384 @@ | ||
| 1 | +{ | |
| 2 | + "lockfileVersion": 1, | |
| 3 | + "configVersion": 1, | |
| 4 | + "workspaces": { | |
| 5 | + "": { | |
| 6 | + "name": "assistant", | |
| 7 | + "devDependencies": { | |
| 8 | + "@effect/language-service": "latest", | |
| 9 | + "@effect/vitest": "^0.27.0", | |
| 10 | + "@types/bun": "latest", | |
| 11 | + "@typescript/native-preview": "7.0.0-dev.20260205.1", | |
| 12 | + "typescript": "latest", | |
| 13 | + "vitest": "^3.2.0", | |
| 14 | + }, | |
| 15 | + }, | |
| 16 | + "apps/discord-bot": { | |
| 17 | + "name": "@openassistant/discord-bot", | |
| 18 | + "dependencies": { | |
| 19 | + "@openassistant/core": "workspace:*", | |
| 20 | + "ai": "^6.0.73", | |
| 21 | + "ai-sdk-provider-claude-code": "^3.3.6", | |
| 22 | + "discord.js": "^14.17.3", | |
| 23 | + "effect": "latest", | |
| 24 | + "zod": "^4.3.6", | |
| 25 | + }, | |
| 26 | + }, | |
| 27 | + "apps/gateway": { | |
| 28 | + "name": "@openassistant/gateway", | |
| 29 | + "dependencies": { | |
| 30 | + "@openassistant/core": "workspace:*", | |
| 31 | + "effect": "latest", | |
| 32 | + }, | |
| 33 | + }, | |
| 34 | + "packages/core": { | |
| 35 | + "name": "@openassistant/core", | |
| 36 | + "dependencies": { | |
| 37 | + "effect": "latest", | |
| 38 | + }, | |
| 39 | + }, | |
| 40 | + }, | |
| 41 | + "packages": { | |
| 42 | + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.36", "", { "dependencies": { "@ai-sdk/provider": "3.0.7", "@ai-sdk/provider-utils": "4.0.13", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2r1Q6azvqMYxQ1hqfWZmWg4+8MajoldD/ty65XdhCaCoBfvDu7trcvxXDfTSU+3/wZ1JIDky46SWYFOHnTbsBw=="], | |
| 43 | + | |
| 44 | + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-VkPLrutM6VdA924/mG8OS+5frbVTcu6e046D2bgDo00tehBANR1QBJ/mPcZ9tXMFOsVcm6SQArOregxePzTFPw=="], | |
| 45 | + | |
| 46 | + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.13", "", { "dependencies": { "@ai-sdk/provider": "3.0.7", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HHG72BN4d+OWTcq2NwTxOm/2qvk1duYsnhCDtsbYwn/h/4zeqURu1S0+Cn0nY2Ysq9a9HGKvrYuMn9bgFhR2Og=="], | |
| 47 | + | |
| 48 | + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.32", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-8AtsSx/M9jxd0ihS08eqa7VireTEuwQy0i1+6ZJX93LECT6Svlf47dPJiAm7JB+BhVMmwTfQeS6x1akIcCfvbQ=="], | |
| 49 | + | |
| 50 | + "@discordjs/builders": ["@discordjs/builders@1.13.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.33", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w=="], | |
| 51 | + | |
| 52 | + "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], | |
| 53 | + | |
| 54 | + "@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="], | |
| 55 | + | |
| 56 | + "@discordjs/rest": ["@discordjs/rest@2.6.0", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.16", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w=="], | |
| 57 | + | |
| 58 | + "@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="], | |
| 59 | + | |
| 60 | + "@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="], | |
| 61 | + | |
| 62 | + "@effect/language-service": ["@effect/language-service@0.73.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-/SGoq50VDm/XwQI6d0cReYcLjwfdqwZv7uEJp92+ssx5vLCsm+QvHf4Ul6l6PYVzorsgAp/b6fhAwI2VSkqcJQ=="], | |
| 63 | + | |
| 64 | + "@effect/vitest": ["@effect/vitest@0.27.0", "", { "peerDependencies": { "effect": "^3.19.0", "vitest": "^3.2.0" } }, "sha512-8bM7n9xlMUYw9GqPIVgXFwFm2jf27m/R7psI64PGpwU5+26iwyxp9eAXEsfT5S6lqztYfpQQ1Ubp5o6HfNYzJQ=="], | |
| 65 | + | |
| 66 | + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], | |
| 67 | + | |
| 68 | + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], | |
| 69 | + | |
| 70 | + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], | |
| 71 | + | |
| 72 | + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], | |
| 73 | + | |
| 74 | + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], | |
| 75 | + | |
| 76 | + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], | |
| 77 | + | |
| 78 | + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], | |
| 79 | + | |
| 80 | + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], | |
| 81 | + | |
| 82 | + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], | |
| 83 | + | |
| 84 | + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], | |
| 85 | + | |
| 86 | + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], | |
| 87 | + | |
| 88 | + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], | |
| 89 | + | |
| 90 | + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], | |
| 91 | + | |
| 92 | + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], | |
| 93 | + | |
| 94 | + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], | |
| 95 | + | |
| 96 | + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], | |
| 97 | + | |
| 98 | + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], | |
| 99 | + | |
| 100 | + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], | |
| 101 | + | |
| 102 | + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], | |
| 103 | + | |
| 104 | + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], | |
| 105 | + | |
| 106 | + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], | |
| 107 | + | |
| 108 | + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], | |
| 109 | + | |
| 110 | + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], | |
| 111 | + | |
| 112 | + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], | |
| 113 | + | |
| 114 | + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], | |
| 115 | + | |
| 116 | + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], | |
| 117 | + | |
| 118 | + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], | |
| 119 | + | |
| 120 | + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], | |
| 121 | + | |
| 122 | + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], | |
| 123 | + | |
| 124 | + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], | |
| 125 | + | |
| 126 | + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], | |
| 127 | + | |
| 128 | + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], | |
| 129 | + | |
| 130 | + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], | |
| 131 | + | |
| 132 | + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], | |
| 133 | + | |
| 134 | + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], | |
| 135 | + | |
| 136 | + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], | |
| 137 | + | |
| 138 | + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], | |
| 139 | + | |
| 140 | + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], | |
| 141 | + | |
| 142 | + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], | |
| 143 | + | |
| 144 | + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], | |
| 145 | + | |
| 146 | + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], | |
| 147 | + | |
| 148 | + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], | |
| 149 | + | |
| 150 | + "@openassistant/core": ["@openassistant/core@workspace:packages/core"], | |
| 151 | + | |
| 152 | + "@openassistant/discord-bot": ["@openassistant/discord-bot@workspace:apps/discord-bot"], | |
| 153 | + | |
| 154 | + "@openassistant/gateway": ["@openassistant/gateway@workspace:apps/gateway"], | |
| 155 | + | |
| 156 | + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], | |
| 157 | + | |
| 158 | + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="], | |
| 159 | + | |
| 160 | + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="], | |
| 161 | + | |
| 162 | + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="], | |
| 163 | + | |
| 164 | + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="], | |
| 165 | + | |
| 166 | + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="], | |
| 167 | + | |
| 168 | + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="], | |
| 169 | + | |
| 170 | + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="], | |
| 171 | + | |
| 172 | + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="], | |
| 173 | + | |
| 174 | + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="], | |
| 175 | + | |
| 176 | + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="], | |
| 177 | + | |
| 178 | + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="], | |
| 179 | + | |
| 180 | + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="], | |
| 181 | + | |
| 182 | + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="], | |
| 183 | + | |
| 184 | + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="], | |
| 185 | + | |
| 186 | + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="], | |
| 187 | + | |
| 188 | + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="], | |
| 189 | + | |
| 190 | + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="], | |
| 191 | + | |
| 192 | + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="], | |
| 193 | + | |
| 194 | + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="], | |
| 195 | + | |
| 196 | + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="], | |
| 197 | + | |
| 198 | + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="], | |
| 199 | + | |
| 200 | + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="], | |
| 201 | + | |
| 202 | + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="], | |
| 203 | + | |
| 204 | + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="], | |
| 205 | + | |
| 206 | + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="], | |
| 207 | + | |
| 208 | + "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], | |
| 209 | + | |
| 210 | + "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="], | |
| 211 | + | |
| 212 | + "@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="], | |
| 213 | + | |
| 214 | + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], | |
| 215 | + | |
| 216 | + "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], | |
| 217 | + | |
| 218 | + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], | |
| 219 | + | |
| 220 | + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], | |
| 221 | + | |
| 222 | + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], | |
| 223 | + | |
| 224 | + "@types/node": ["@types/node@25.2.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg=="], | |
| 225 | + | |
| 226 | + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], | |
| 227 | + | |
| 228 | + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260205.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260205.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260205.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260205.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260205.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260205.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260205.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260205.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-eSgzYCbdCXP/E0XL53yIMZNLoY3z1xMOgGyjstVLgUCMLv1yNrFvkhKhHFjM84OTY/LxqRb6ACtvjFO/oSZzvQ=="], | |
| 229 | + | |
| 230 | + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260205.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ULATKP9a26qh8vcmP4qPz8UugGKIwhQPKi3NhvlbTPwhl3fMd3GJd9/B9LJSHw7lIuELQGZxhSlDq9l0FMb/FQ=="], | |
| 231 | + | |
| 232 | + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260205.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-moaKDZHK2dbgcHCnxcwhH8kYRgY69wzPcH5hCNaSrmpbC+Garr78oLtyXot2EDotRDT9foeYsWKdmD6Hx/ypxg=="], | |
| 233 | + | |
| 234 | + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260205.1", "", { "os": "linux", "cpu": "arm" }, "sha512-3qfjUQlYCkwQmbpIeXMw75bLXkCI3Uo88Ug1n9p4j6KFaek5TjnHOTmlO6V3pkyH9pEXQEVXTn0pXzQytxqEqw=="], | |
| 235 | + | |
| 236 | + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260205.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Wfp2bPmrTLb+dpp2bHDjMqMKGjQ9dp5KSw0jV4LSlbgcVvRSEWqs2ByVVj61Z4qiHgwlVyoPTewdan2CWnoBgQ=="], | |
| 237 | + | |
| 238 | + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260205.1", "", { "os": "linux", "cpu": "x64" }, "sha512-p59oY35gvvmdy/iZYxdbFAUXusb7joX2i1Nwl15i4TOn52NcIcW3wb9U/uBrIXKev5VEdlH6BS6VA6dM57zD6w=="], | |
| 239 | + | |
| 240 | + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260205.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-+NQTlmvtZEXwIlw8j+tvAAn1gLDqyWJEjnA5vmT9MoJuEBrxvuS8azn/q26MOp/w8bWfxe3haVyB+L4VurCF6w=="], | |
| 241 | + | |
| 242 | + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260205.1", "", { "os": "win32", "cpu": "x64" }, "sha512-kRa4kaiORAWQx9sHylewUhKsNxz3dRBy6AM/U02UebJRlt6c+JnSjIxAFP+iNQaRpoYNs8UdKKGPrHc7Q0oYow=="], | |
| 243 | + | |
| 244 | + "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], | |
| 245 | + | |
| 246 | + "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], | |
| 247 | + | |
| 248 | + "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], | |
| 249 | + | |
| 250 | + "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], | |
| 251 | + | |
| 252 | + "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], | |
| 253 | + | |
| 254 | + "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], | |
| 255 | + | |
| 256 | + "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], | |
| 257 | + | |
| 258 | + "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], | |
| 259 | + | |
| 260 | + "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], | |
| 261 | + | |
| 262 | + "ai": ["ai@6.0.73", "", { "dependencies": { "@ai-sdk/gateway": "3.0.36", "@ai-sdk/provider": "3.0.7", "@ai-sdk/provider-utils": "4.0.13", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-p2/ICXIjAM4+bIFHEkAB+l58zq+aTmxAkotsb6doNt/CEms72zt6gxv2ky1fQDwU4ecMOcmMh78VJUSEKECzlg=="], | |
| 263 | + | |
| 264 | + "ai-sdk-provider-claude-code": ["ai-sdk-provider-claude-code@3.3.6", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.1", "@anthropic-ai/claude-agent-sdk": "^0.2.23" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-lDJJrQqe2A6tAH96ZLsZW39blkBh3jF1A4rs8B6CJyjNhLNeq46KmxkKDPYDoZw4xbILvnAknjXQNhMSZLnEYg=="], | |
| 265 | + | |
| 266 | + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], | |
| 267 | + | |
| 268 | + "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], | |
| 269 | + | |
| 270 | + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], | |
| 271 | + | |
| 272 | + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], | |
| 273 | + | |
| 274 | + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], | |
| 275 | + | |
| 276 | + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], | |
| 277 | + | |
| 278 | + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], | |
| 279 | + | |
| 280 | + "discord-api-types": ["discord-api-types@0.38.38", "", {}, "sha512-7qcM5IeZrfb+LXW07HvoI5L+j4PQeMZXEkSm1htHAHh4Y9JSMXBWjy/r7zmUCOj4F7zNjMcm7IMWr131MT2h0Q=="], | |
| 281 | + | |
| 282 | + "discord.js": ["discord.js@14.25.1", "", { "dependencies": { "@discordjs/builders": "^1.13.0", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.33", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g=="], | |
| 283 | + | |
| 284 | + "effect": ["effect@3.19.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-7+XC3vGrbAhCHd8LTFHvnZjRpZKZ8YHRZqJTkpNoxcJ2mCyNs2SwI+6VkV/ij8Y3YW7wfBN4EbU06/F5+m/wkQ=="], | |
| 285 | + | |
| 286 | + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], | |
| 287 | + | |
| 288 | + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], | |
| 289 | + | |
| 290 | + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], | |
| 291 | + | |
| 292 | + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], | |
| 293 | + | |
| 294 | + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], | |
| 295 | + | |
| 296 | + "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], | |
| 297 | + | |
| 298 | + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], | |
| 299 | + | |
| 300 | + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], | |
| 301 | + | |
| 302 | + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], | |
| 303 | + | |
| 304 | + "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], | |
| 305 | + | |
| 306 | + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], | |
| 307 | + | |
| 308 | + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], | |
| 309 | + | |
| 310 | + "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], | |
| 311 | + | |
| 312 | + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], | |
| 313 | + | |
| 314 | + "magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="], | |
| 315 | + | |
| 316 | + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], | |
| 317 | + | |
| 318 | + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], | |
| 319 | + | |
| 320 | + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], | |
| 321 | + | |
| 322 | + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], | |
| 323 | + | |
| 324 | + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], | |
| 325 | + | |
| 326 | + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], | |
| 327 | + | |
| 328 | + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], | |
| 329 | + | |
| 330 | + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], | |
| 331 | + | |
| 332 | + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], | |
| 333 | + | |
| 334 | + "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="], | |
| 335 | + | |
| 336 | + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], | |
| 337 | + | |
| 338 | + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], | |
| 339 | + | |
| 340 | + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], | |
| 341 | + | |
| 342 | + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], | |
| 343 | + | |
| 344 | + "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], | |
| 345 | + | |
| 346 | + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], | |
| 347 | + | |
| 348 | + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], | |
| 349 | + | |
| 350 | + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], | |
| 351 | + | |
| 352 | + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], | |
| 353 | + | |
| 354 | + "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], | |
| 355 | + | |
| 356 | + "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], | |
| 357 | + | |
| 358 | + "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], | |
| 359 | + | |
| 360 | + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | |
| 361 | + | |
| 362 | + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], | |
| 363 | + | |
| 364 | + "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], | |
| 365 | + | |
| 366 | + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], | |
| 367 | + | |
| 368 | + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], | |
| 369 | + | |
| 370 | + "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], | |
| 371 | + | |
| 372 | + "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], | |
| 373 | + | |
| 374 | + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], | |
| 375 | + | |
| 376 | + "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], | |
| 377 | + | |
| 378 | + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], | |
| 379 | + | |
| 380 | + "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], | |
| 381 | + | |
| 382 | + "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], | |
| 383 | + } | |
| 384 | +} | |
| @@ -0,0 +1 @@ | ||
| 1 | +console.log("Hello via Bun!"); | |
| @@ -0,0 +1,30 @@ | ||
| 1 | +{ | |
| 2 | + "name": "openassistant", | |
| 3 | + "private": true, | |
| 4 | + "type": "module", | |
| 5 | + "workspaces": [ | |
| 6 | + "apps/*", | |
| 7 | + "packages/*" | |
| 8 | + ], | |
| 9 | + "scripts": { | |
| 10 | + "with-env": "bun run --env-file=.env --", | |
| 11 | + "with-env:prod": "bun run --env-file=.env.production --", | |
| 12 | + "prepare": "effect-language-service patch", | |
| 13 | + "typecheck": "bun run --filter '@openassistant/core' typecheck && bun run --filter '@openassistant/gateway' typecheck && bun run --filter '@openassistant/discord-bot' typecheck", | |
| 14 | + "typecheck:tsc": "tsc --noEmit --incremental false -p packages/core/tsconfig.json && tsc --noEmit --incremental false -p apps/gateway/tsconfig.json && tsc --noEmit --incremental false -p apps/discord-bot/tsconfig.json", | |
| 15 | + "build": "bun run typecheck", | |
| 16 | + "test": "vitest run", | |
| 17 | + "test:watch": "vitest", | |
| 18 | + "clean": "bun run --filter '@openassistant/core' clean && bun run --filter '@openassistant/gateway' clean && bun run --filter '@openassistant/discord-bot' clean", | |
| 19 | + "dev:gateway": "bun run --cwd apps/gateway dev", | |
| 20 | + "dev:discord": "bun run --cwd apps/discord-bot dev" | |
| 21 | + }, | |
| 22 | + "devDependencies": { | |
| 23 | + "@effect/language-service": "latest", | |
| 24 | + "@effect/vitest": "^0.27.0", | |
| 25 | + "@types/bun": "latest", | |
| 26 | + "@typescript/native-preview": "7.0.0-dev.20260205.1", | |
| 27 | + "typescript": "latest", | |
| 28 | + "vitest": "^3.2.0" | |
| 29 | + } | |
| 30 | +} | |
| @@ -0,0 +1,17 @@ | ||
| 1 | +{ | |
| 2 | + "name": "@openassistant/core", | |
| 3 | + "private": true, | |
| 4 | + "type": "module", | |
| 5 | + "exports": { | |
| 6 | + ".": "./src/index.ts" | |
| 7 | + }, | |
| 8 | + "scripts": { | |
| 9 | + "with-env": "bun run --env-file=../../.env --", | |
| 10 | + "with-env:prod": "bun run --env-file=../../.env.production --", | |
| 11 | + "typecheck": "tsgo --noEmit --incremental false -p tsconfig.json", | |
| 12 | + "clean": "bun -e \"import { rmSync } from 'node:fs'; for (const p of ['dist','node_modules','.turbo','coverage','.tsbuildinfo']) rmSync(p,{recursive:true,force:true});\"" | |
| 13 | + }, | |
| 14 | + "dependencies": { | |
| 15 | + "effect": "latest" | |
| 16 | + } | |
| 17 | +} | |
| @@ -0,0 +1,117 @@ | ||
| 1 | +import { expect, it } from "@effect/vitest"; | |
| 2 | +import { Effect } from "effect"; | |
| 3 | +import { describe } from "vitest"; | |
| 4 | +import { | |
| 5 | + createCodeModeRunner, | |
| 6 | + defineTool, | |
| 7 | +} from "./runner.js"; | |
| 8 | + | |
| 9 | +describe("codemode runner", () => { | |
| 10 | + it.effect("runs read tools without approval", () => | |
| 11 | + Effect.gen(function* () { | |
| 12 | + const approvalCalls: Array<{ toolPath: string }> = []; | |
| 13 | + const runner = createCodeModeRunner({ | |
| 14 | + tools: { | |
| 15 | + calendar: { | |
| 16 | + read: defineTool({ | |
| 17 | + kind: "read", | |
| 18 | + approval: "auto", | |
| 19 | + run: (input: { id: string }) => Effect.succeed({ id: input.id }), | |
| 20 | + }), | |
| 21 | + }, | |
| 22 | + }, | |
| 23 | + requestApproval: (request) => | |
| 24 | + Effect.sync(() => { | |
| 25 | + approvalCalls.push({ toolPath: request.toolPath }); | |
| 26 | + return "approved" as const; | |
| 27 | + }), | |
| 28 | + }); | |
| 29 | + | |
| 30 | + const result = yield* runner.run({ | |
| 31 | + code: "const event = await tools.calendar.read({ id: 'evt_1' }); return event.id;", | |
| 32 | + }); | |
| 33 | + | |
| 34 | + expect(result.ok).toBe(true); | |
| 35 | + if (result.ok) { | |
| 36 | + expect(result.value).toBe("evt_1"); | |
| 37 | + } | |
| 38 | + expect(approvalCalls).toHaveLength(0); | |
| 39 | + expect(result.receipts).toHaveLength(1); | |
| 40 | + expect(result.receipts[0]?.decision).toBe("auto"); | |
| 41 | + expect(result.receipts[0]?.status).toBe("succeeded"); | |
| 42 | + expect(result.receipts[0]?.toolPath).toBe("calendar.read"); | |
| 43 | + }), | |
| 44 | + ); | |
| 45 | + | |
| 46 | + it.effect("blocks writes when denied and records receipt", () => | |
| 47 | + Effect.gen(function* () { | |
| 48 | + let ranMutation = false; | |
| 49 | + const runner = createCodeModeRunner({ | |
| 50 | + tools: { | |
| 51 | + calendar: { | |
| 52 | + update: defineTool({ | |
| 53 | + kind: "write", | |
| 54 | + approval: "required", | |
| 55 | + run: (_input: { id: string }) => | |
| 56 | + Effect.sync(() => { | |
| 57 | + ranMutation = true; | |
| 58 | + return { ok: true }; | |
| 59 | + }), | |
| 60 | + }), | |
| 61 | + }, | |
| 62 | + }, | |
| 63 | + requestApproval: () => Effect.succeed("denied" as const), | |
| 64 | + }); | |
| 65 | + | |
| 66 | + const result = yield* runner.run({ | |
| 67 | + code: "await tools.calendar.update({ id: 'evt_2' }); return 'done';", | |
| 68 | + }); | |
| 69 | + | |
| 70 | + expect(result.ok).toBe(false); | |
| 71 | + if (!result.ok) { | |
| 72 | + expect(result.error).toContain("denied"); | |
| 73 | + } | |
| 74 | + expect(ranMutation).toBe(false); | |
| 75 | + expect(result.receipts).toHaveLength(1); | |
| 76 | + expect(result.receipts[0]?.decision).toBe("denied"); | |
| 77 | + expect(result.receipts[0]?.status).toBe("denied"); | |
| 78 | + expect(result.receipts[0]?.toolPath).toBe("calendar.update"); | |
| 79 | + }), | |
| 80 | + ); | |
| 81 | + | |
| 82 | + it.effect("runs writes when approved and records success receipt", () => | |
| 83 | + Effect.gen(function* () { | |
| 84 | + let ranMutation = false; | |
| 85 | + const runner = createCodeModeRunner({ | |
| 86 | + tools: { | |
| 87 | + calendar: { | |
| 88 | + update: defineTool({ | |
| 89 | + kind: "write", | |
| 90 | + approval: "required", | |
| 91 | + run: (input: { id: string; title: string }) => | |
| 92 | + Effect.sync(() => { | |
| 93 | + ranMutation = true; | |
| 94 | + return { id: input.id, title: input.title }; | |
| 95 | + }), | |
| 96 | + }), | |
| 97 | + }, | |
| 98 | + }, | |
| 99 | + requestApproval: () => Effect.succeed("approved" as const), | |
| 100 | + }); | |
| 101 | + | |
| 102 | + const result = yield* runner.run({ | |
| 103 | + code: "const out = await tools.calendar.update({ id: 'evt_3', title: 'Dinner' }); return out.title;", | |
| 104 | + }); | |
| 105 | + | |
| 106 | + expect(result.ok).toBe(true); | |
| 107 | + if (result.ok) { | |
| 108 | + expect(result.value).toBe("Dinner"); | |
| 109 | + } | |
| 110 | + expect(ranMutation).toBe(true); | |
| 111 | + expect(result.receipts).toHaveLength(1); | |
| 112 | + expect(result.receipts[0]?.decision).toBe("approved"); | |
| 113 | + expect(result.receipts[0]?.status).toBe("succeeded"); | |
| 114 | + expect(result.receipts[0]?.toolPath).toBe("calendar.update"); | |
| 115 | + }), | |
| 116 | + ); | |
| 117 | +}); | |
| @@ -0,0 +1,318 @@ | ||
| 1 | +import { Effect } from "effect"; | |
| 2 | + | |
| 3 | +export type ToolKind = "read" | "write"; | |
| 4 | +export type ToolApprovalMode = "auto" | "required"; | |
| 5 | +export type ApprovalDecision = "approved" | "denied"; | |
| 6 | +export type ToolReceiptDecision = "auto" | ApprovalDecision; | |
| 7 | +export type ToolReceiptStatus = "succeeded" | "failed" | "denied"; | |
| 8 | + | |
| 9 | +export interface ToolCallReceipt { | |
| 10 | + callId: string; | |
| 11 | + toolPath: string; | |
| 12 | + kind: ToolKind; | |
| 13 | + approval: ToolApprovalMode; | |
| 14 | + decision: ToolReceiptDecision; | |
| 15 | + status: ToolReceiptStatus; | |
| 16 | + timestamp: string; | |
| 17 | + inputPreview?: string; | |
| 18 | + outputPreview?: string; | |
| 19 | + error?: string; | |
| 20 | +} | |
| 21 | + | |
| 22 | +export interface ApprovalRequest { | |
| 23 | + callId: string; | |
| 24 | + toolPath: string; | |
| 25 | + kind: ToolKind; | |
| 26 | + approval: ToolApprovalMode; | |
| 27 | + input: unknown; | |
| 28 | + inputPreview?: string; | |
| 29 | +} | |
| 30 | + | |
| 31 | +export interface ToolDefinition<TInput = unknown, TOutput = unknown> { | |
| 32 | + readonly _tag: "ToolDefinition"; | |
| 33 | + readonly kind: ToolKind; | |
| 34 | + readonly approval: ToolApprovalMode; | |
| 35 | + readonly run: (input: TInput) => Effect.Effect<TOutput, unknown>; | |
| 36 | + readonly previewInput?: (input: TInput) => string | undefined; | |
| 37 | + readonly previewOutput?: (output: TOutput) => string | undefined; | |
| 38 | +} | |
| 39 | + | |
| 40 | +export type ToolTree = { | |
| 41 | + readonly [name: string]: ToolTree | ToolDefinition<any, any>; | |
| 42 | +}; | |
| 43 | + | |
| 44 | +export interface CodeModeRunnerOptions { | |
| 45 | + tools: ToolTree; | |
| 46 | + requestApproval: (request: ApprovalRequest) => Effect.Effect<ApprovalDecision, unknown>; | |
| 47 | + now?: () => Date; | |
| 48 | + newCallId?: () => string; | |
| 49 | +} | |
| 50 | + | |
| 51 | +export interface RunCodeOptions { | |
| 52 | + code: string; | |
| 53 | +} | |
| 54 | + | |
| 55 | +export type CodeModeRunResult = | |
| 56 | + | { | |
| 57 | + ok: true; | |
| 58 | + value: unknown; | |
| 59 | + receipts: ToolCallReceipt[]; | |
| 60 | + } | |
| 61 | + | { | |
| 62 | + ok: false; | |
| 63 | + error: string; | |
| 64 | + receipts: ToolCallReceipt[]; | |
| 65 | + }; | |
| 66 | + | |
| 67 | +const MAX_PREVIEW_CHARS = 180; | |
| 68 | +const AsyncFunctionCtor = Object.getPrototypeOf( | |
| 69 | + async (_tools: unknown) => undefined, | |
| 70 | +).constructor as new (argName: string, body: string) => (tools: unknown) => Promise<unknown>; | |
| 71 | + | |
| 72 | +export function defineTool<TInput, TOutput>(definition: { | |
| 73 | + kind: ToolKind; | |
| 74 | + approval: ToolApprovalMode; | |
| 75 | + run: (input: TInput) => Effect.Effect<TOutput, unknown>; | |
| 76 | + previewInput?: (input: TInput) => string | undefined; | |
| 77 | + previewOutput?: (output: TOutput) => string | undefined; | |
| 78 | +}): ToolDefinition<TInput, TOutput> { | |
| 79 | + return { | |
| 80 | + _tag: "ToolDefinition", | |
| 81 | + ...definition, | |
| 82 | + }; | |
| 83 | +} | |
| 84 | + | |
| 85 | +export function createCodeModeRunner(options: CodeModeRunnerOptions): { | |
| 86 | + run: (params: RunCodeOptions) => Effect.Effect<CodeModeRunResult, never>; | |
| 87 | +} { | |
| 88 | + const now = options.now ?? (() => new Date()); | |
| 89 | + const newCallId = options.newCallId ?? defaultCallId; | |
| 90 | + | |
| 91 | + return { | |
| 92 | + run: (params) => { | |
| 93 | + const receipts: ToolCallReceipt[] = []; | |
| 94 | + const tools = materializeToolTree({ | |
| 95 | + tools: options.tools, | |
| 96 | + requestApproval: options.requestApproval, | |
| 97 | + newCallId, | |
| 98 | + now, | |
| 99 | + receipts, | |
| 100 | + }); | |
| 101 | + | |
| 102 | + return Effect.tryPromise({ | |
| 103 | + try: async () => { | |
| 104 | + const execute = new AsyncFunctionCtor("tools", `"use strict";\n${params.code}`); | |
| 105 | + const value = await execute(tools); | |
| 106 | + return { | |
| 107 | + ok: true as const, | |
| 108 | + value, | |
| 109 | + receipts: [...receipts], | |
| 110 | + }; | |
| 111 | + }, | |
| 112 | + catch: (cause) => ({ | |
| 113 | + ok: false as const, | |
| 114 | + error: describeUnknown(cause), | |
| 115 | + receipts: [...receipts], | |
| 116 | + }), | |
| 117 | + }).pipe( | |
| 118 | + Effect.catchAll((result) => | |
| 119 | + Effect.succeed({ | |
| 120 | + ok: false as const, | |
| 121 | + error: result.error, | |
| 122 | + receipts: result.receipts, | |
| 123 | + }), | |
| 124 | + ), | |
| 125 | + ); | |
| 126 | + }, | |
| 127 | + }; | |
| 128 | +} | |
| 129 | + | |
| 130 | +function materializeToolTree(params: { | |
| 131 | + tools: ToolTree; | |
| 132 | + requestApproval: (request: ApprovalRequest) => Effect.Effect<ApprovalDecision, unknown>; | |
| 133 | + newCallId: () => string; | |
| 134 | + now: () => Date; | |
| 135 | + receipts: ToolCallReceipt[]; | |
| 136 | +}): Record<string, unknown> { | |
| 137 | + const out: Record<string, unknown> = {}; | |
| 138 | + for (const [name, node] of Object.entries(params.tools)) { | |
| 139 | + const toolPath = name; | |
| 140 | + out[name] = materializeToolNode({ | |
| 141 | + node, | |
| 142 | + toolPath, | |
| 143 | + requestApproval: params.requestApproval, | |
| 144 | + newCallId: params.newCallId, | |
| 145 | + now: params.now, | |
| 146 | + receipts: params.receipts, | |
| 147 | + }); | |
| 148 | + } | |
| 149 | + return out; | |
| 150 | +} | |
| 151 | + | |
| 152 | +function materializeToolNode(params: { | |
| 153 | + node: ToolTree | ToolDefinition<any, any>; | |
| 154 | + toolPath: string; | |
| 155 | + requestApproval: (request: ApprovalRequest) => Effect.Effect<ApprovalDecision, unknown>; | |
| 156 | + newCallId: () => string; | |
| 157 | + now: () => Date; | |
| 158 | + receipts: ToolCallReceipt[]; | |
| 159 | +}): unknown { | |
| 160 | + if (isToolDefinition(params.node)) { | |
| 161 | + return createToolInvoker({ | |
| 162 | + definition: params.node, | |
| 163 | + toolPath: params.toolPath, | |
| 164 | + requestApproval: params.requestApproval, | |
| 165 | + newCallId: params.newCallId, | |
| 166 | + now: params.now, | |
| 167 | + receipts: params.receipts, | |
| 168 | + }); | |
| 169 | + } | |
| 170 | + | |
| 171 | + const out: Record<string, unknown> = {}; | |
| 172 | + for (const [name, child] of Object.entries(params.node)) { | |
| 173 | + out[name] = materializeToolNode({ | |
| 174 | + node: child, | |
| 175 | + toolPath: `${params.toolPath}.${name}`, | |
| 176 | + requestApproval: params.requestApproval, | |
| 177 | + newCallId: params.newCallId, | |
| 178 | + now: params.now, | |
| 179 | + receipts: params.receipts, | |
| 180 | + }); | |
| 181 | + } | |
| 182 | + return out; | |
| 183 | +} | |
| 184 | + | |
| 185 | +function createToolInvoker(params: { | |
| 186 | + definition: ToolDefinition<any, any>; | |
| 187 | + toolPath: string; | |
| 188 | + requestApproval: (request: ApprovalRequest) => Effect.Effect<ApprovalDecision, unknown>; | |
| 189 | + newCallId: () => string; | |
| 190 | + now: () => Date; | |
| 191 | + receipts: ToolCallReceipt[]; | |
| 192 | +}): (input: unknown) => Promise<unknown> { | |
| 193 | + return async (input: unknown) => { | |
| 194 | + const callId = params.newCallId(); | |
| 195 | + const timestamp = params.now().toISOString(); | |
| 196 | + const inputPreview = previewValue(input, params.definition.previewInput); | |
| 197 | + | |
| 198 | + let decision: ToolReceiptDecision = "auto"; | |
| 199 | + if (params.definition.approval === "required") { | |
| 200 | + const approval = await Effect.runPromise( | |
| 201 | + params.requestApproval({ | |
| 202 | + callId, | |
| 203 | + toolPath: params.toolPath, | |
| 204 | + kind: params.definition.kind, | |
| 205 | + approval: params.definition.approval, | |
| 206 | + input, | |
| 207 | + ...(inputPreview !== undefined ? { inputPreview } : {}), | |
| 208 | + }), | |
| 209 | + ); | |
| 210 | + decision = approval; | |
| 211 | + if (approval === "denied") { | |
| 212 | + params.receipts.push({ | |
| 213 | + callId, | |
| 214 | + toolPath: params.toolPath, | |
| 215 | + kind: params.definition.kind, | |
| 216 | + approval: params.definition.approval, | |
| 217 | + decision, | |
| 218 | + status: "denied", | |
| 219 | + timestamp, | |
| 220 | + ...(inputPreview !== undefined ? { inputPreview } : {}), | |
| 221 | + }); | |
| 222 | + throw new Error(`Tool call denied: ${params.toolPath}`); | |
| 223 | + } | |
| 224 | + } | |
| 225 | + | |
| 226 | + const execution = await Effect.runPromiseExit(params.definition.run(input)); | |
| 227 | + if (execution._tag === "Success") { | |
| 228 | + const value = execution.value; | |
| 229 | + const outputPreview = previewValue(value, params.definition.previewOutput); | |
| 230 | + params.receipts.push({ | |
| 231 | + callId, | |
| 232 | + toolPath: params.toolPath, | |
| 233 | + kind: params.definition.kind, | |
| 234 | + approval: params.definition.approval, | |
| 235 | + decision, | |
| 236 | + status: "succeeded", | |
| 237 | + timestamp, | |
| 238 | + ...(inputPreview !== undefined ? { inputPreview } : {}), | |
| 239 | + ...(outputPreview !== undefined ? { outputPreview } : {}), | |
| 240 | + }); | |
| 241 | + return value; | |
| 242 | + } | |
| 243 | + | |
| 244 | + const error = describeUnknown(execution.cause); | |
| 245 | + params.receipts.push({ | |
| 246 | + callId, | |
| 247 | + toolPath: params.toolPath, | |
| 248 | + kind: params.definition.kind, | |
| 249 | + approval: params.definition.approval, | |
| 250 | + decision, | |
| 251 | + status: "failed", | |
| 252 | + timestamp, | |
| 253 | + ...(inputPreview !== undefined ? { inputPreview } : {}), | |
| 254 | + error, | |
| 255 | + }); | |
| 256 | + throw new Error(error); | |
| 257 | + }; | |
| 258 | +} | |
| 259 | + | |
| 260 | +function isToolDefinition(value: unknown): value is ToolDefinition<any, any> { | |
| 261 | + return ( | |
| 262 | + typeof value === "object" && | |
| 263 | + value !== null && | |
| 264 | + "_tag" in value && | |
| 265 | + (value as { _tag?: unknown })._tag === "ToolDefinition" | |
| 266 | + ); | |
| 267 | +} | |
| 268 | + | |
| 269 | +function previewValue<T>( | |
| 270 | + value: T, | |
| 271 | + customPreview: ((value: T) => string | undefined) | undefined, | |
| 272 | +): string | undefined { | |
| 273 | + if (customPreview) { | |
| 274 | + return truncatePreview(customPreview(value)); | |
| 275 | + } | |
| 276 | + if (value === undefined) { | |
| 277 | + return undefined; | |
| 278 | + } | |
| 279 | + return truncatePreview(safeStringify(value)); | |
| 280 | +} | |
| 281 | + | |
| 282 | +function truncatePreview(value: string | undefined): string | undefined { | |
| 283 | + if (!value) { | |
| 284 | + return undefined; | |
| 285 | + } | |
| 286 | + if (value.length <= MAX_PREVIEW_CHARS) { | |
| 287 | + return value; | |
| 288 | + } | |
| 289 | + return `${value.slice(0, MAX_PREVIEW_CHARS)}...`; | |
| 290 | +} | |
| 291 | + | |
| 292 | +function safeStringify(value: unknown): string { | |
| 293 | + if (typeof value === "string") { | |
| 294 | + return value; | |
| 295 | + } | |
| 296 | + try { | |
| 297 | + return JSON.stringify(value); | |
| 298 | + } catch { | |
| 299 | + return String(value); | |
| 300 | + } | |
| 301 | +} | |
| 302 | + | |
| 303 | +function defaultCallId(): string { | |
| 304 | + if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { | |
| 305 | + return crypto.randomUUID(); | |
| 306 | + } | |
| 307 | + return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; | |
| 308 | +} | |
| 309 | + | |
| 310 | +function describeUnknown(cause: unknown): string { | |
| 311 | + if (cause instanceof Error) { | |
| 312 | + return cause.message; | |
| 313 | + } | |
| 314 | + if (typeof cause === "string") { | |
| 315 | + return cause; | |
| 316 | + } | |
| 317 | + return safeStringify(cause); | |
| 318 | +} | |
| @@ -0,0 +1,4 @@ | ||
| 1 | +import { Effect } from "effect"; | |
| 2 | + | |
| 3 | +export const healthcheck = Effect.succeed("ok"); | |
| 4 | +export * from "./codemode/runner.js"; | |
| @@ -0,0 +1,9 @@ | ||
| 1 | +{ | |
| 2 | + "extends": "../../tsconfig.base.json", | |
| 3 | + "compilerOptions": { | |
| 4 | + "rootDir": "src" | |
| 5 | + }, | |
| 6 | + "include": [ | |
| 7 | + "src/**/*.ts" | |
| 8 | + ] | |
| 9 | +} | |
| @@ -0,0 +1,22 @@ | ||
| 1 | +{ | |
| 2 | + "compilerOptions": { | |
| 3 | + "target": "ES2022", | |
| 4 | + "module": "NodeNext", | |
| 5 | + "moduleResolution": "NodeNext", | |
| 6 | + "moduleDetection": "force", | |
| 7 | + "verbatimModuleSyntax": true, | |
| 8 | + "rewriteRelativeImportExtensions": true, | |
| 9 | + "strict": true, | |
| 10 | + "exactOptionalPropertyTypes": true, | |
| 11 | + "noUncheckedIndexedAccess": true, | |
| 12 | + "noImplicitOverride": true, | |
| 13 | + "noUnusedLocals": true, | |
| 14 | + "noUnusedParameters": true, | |
| 15 | + "skipLibCheck": true, | |
| 16 | + "plugins": [ | |
| 17 | + { | |
| 18 | + "name": "@effect/language-service" | |
| 19 | + } | |
| 20 | + ] | |
| 21 | + } | |
| 22 | +} | |
| @@ -0,0 +1,14 @@ | ||
| 1 | +{ | |
| 2 | + "files": [], | |
| 3 | + "references": [ | |
| 4 | + { | |
| 5 | + "path": "./packages/core" | |
| 6 | + }, | |
| 7 | + { | |
| 8 | + "path": "./apps/gateway" | |
| 9 | + }, | |
| 10 | + { | |
| 11 | + "path": "./apps/discord-bot" | |
| 12 | + } | |
| 13 | + ] | |
| 14 | +} | |
| @@ -0,0 +1,8 @@ | ||
| 1 | +import { defineConfig } from "vitest/config"; | |
| 2 | + | |
| 3 | +export default defineConfig({ | |
| 4 | + test: { | |
| 5 | + include: ["packages/**/*.test.ts", "apps/**/*.test.ts"], | |
| 6 | + exclude: ["**/node_modules/**", ".context/**", ".reference/**"], | |
| 7 | + }, | |
| 8 | +}); | |