From 26eef92fe72e9c3958d61435b8d3571e12bf2b74 Mon Sep 17 00:00:00 2001 From: NikitaBabenko <33519548+NikitaBabenko@users.noreply.github.com> Date: Mon, 6 Apr 2026 12:54:10 +0300 Subject: [PATCH] feat: add headless gRPC server for external agent integration (#278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * gRPC Server * gRPC fix * UpdProto * fix: address PR review feedback for gRPC server - Update bun.lock for new dependencies (frozen-lockfile CI fix) - Add multi-turn session persistence via initialMessages - Replace hardcoded done payload with real token counts - Default bind to localhost instead of 0.0.0.0 * fix(grpc): startup parity, cancel interrupt, and cli text fallback - Replace enableConfigs() with await init() in start-grpc.ts for full bootstrap parity with the main CLI (env vars, CA certs, mTLS, proxy, OAuth, Windows shell) - Call engine.interrupt() before call.end() in the cancel handler so in-flight model/tool execution is actually stopped - Show done.full_text in the CLI client when no text_chunk was received, preventing silent drops when streaming is unavailable * fix(grpc): wire session_id end-to-end and remove dead provider field - Move session_id from ClientMessage into ChatRequest to fix proto-loader oneofs encoding bug and make the field functional - Implement in-memory session store so reconnecting with the same session_id resumes conversation context across streams - Remove ChatRequest.provider — per-request provider routing requires global process.env mutation, unsafe for concurrent clients; provider is configured via env vars at server startup * fix(grpc): mirror CLI auth bootstrap in start-grpc and fix tool_name field scripts/start-grpc.ts now runs the same provider/auth bootstrap as the normal CLI entrypoint: enableConfigs, safe env vars, Gemini/GitHub token hydration, saved-profile resolution with warn-and-fallback, and provider validation before the server binds. ToolCallResult.tool_name was being populated with the tool_use_id UUID. Added a toolNameById map (filled in canUseTool) so tool_name now carries the actual tool name (e.g. "Bash"). The UUID moves to a new tool_use_id field (proto field 4) for client-side correlation. * fix(grpc): add tool_use_id to ToolCallStart and interrupt engine on stream close Two blocker-level issues flagged in code review: - ToolCallStart was missing tool_use_id, making it impossible for clients to correlate tool_start events with tool_result when the same tool runs multiple times. Added tool_use_id = 3 to the proto message and populated it from the toolUseID parameter in canUseTool. - On stream close without an explicit CancelSignal the server only nulled the engine reference, leaving the underlying model/tool work running as an orphan. Added engine.interrupt() in the call.on('end') handler to stop work immediately when the client disconnects. * fix(grpc): resolve pending promises on disconnect and guard post-cancel writes Four lifecycle and contract issues identified during proactive review: - Pending permission Promises in canUseTool would hang forever if the client disconnected mid-stream. On call 'end', all pending resolvers are now called with 'no' so the engine can unblock and terminate. - The done message and session save could fire after call.end() when a CancelSignal arrived mid-generation. Added an `interrupted` flag set on both cancel and stream close to gate all post-loop writes. - The session map had no eviction policy, allowing unbounded memory growth. Capped at MAX_SESSIONS=1000 with FIFO eviction of the oldest entry. - Field 3 was silently absent from ChatRequest. Added `reserved 3` to document the gap and prevent accidental reuse in future. * fix(grpc): reset previousMessages on each new request to prevent session history leak previousMessages was declared at stream scope and only overwritten when the incoming session_id already existed in the session store. A second request on the same stream with a new session_id would silently inherit the first request's conversation history in initialMessages instead of starting fresh, violating the session contract. Fix: reset previousMessages to [] at the start of each ChatRequest before the session-store lookup. * fix(grpc): reset interrupted flag between requests and guard against concurrent ChatRequest Two stream-scoped state bugs found during proactive audit: - The `interrupted` flag was never reset between requests on the same stream. If the first request was cancelled, all subsequent requests would silently skip the done message, causing the client to hang. - A second ChatRequest arriving while the first was still processing would overwrite the engine reference, corrupting the lifecycle of both requests. Now returns ALREADY_EXISTS error instead. Engine is nulled after the for-await loop completes so subsequent requests can proceed normally. --------- Co-authored-by: Claude Opus 4.6 --- .gitignore | 3 + README.md | 35 ++++++ bun.lock | 119 ++++++++++++++---- package.json | 5 + scripts/grpc-cli.ts | 121 ++++++++++++++++++ scripts/start-grpc.ts | 50 ++++++++ src/grpc/server.ts | 252 +++++++++++++++++++++++++++++++++++++ src/proto/openclaude.proto | 101 +++++++++++++++ 8 files changed, 659 insertions(+), 27 deletions(-) create mode 100644 scripts/grpc-cli.ts create mode 100644 scripts/start-grpc.ts create mode 100644 src/grpc/server.ts create mode 100644 src/proto/openclaude.proto diff --git a/.gitignore b/.gitignore index 636eaf63..2d046b19 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,7 @@ dist/ !.env.example .openclaude-profile.json reports/ +GEMINI.md +package-lock.json +/.claude coverage/ diff --git a/README.md b/README.md index de7288b5..fb7e1f84 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,41 @@ With Firecrawl enabled: Free tier at [firecrawl.dev](https://firecrawl.dev) includes 500 credits. The key is optional. +--- + +## Headless gRPC Server + +OpenClaude can be run as a headless gRPC service, allowing you to integrate its agentic capabilities (tools, bash, file editing) into other applications, CI/CD pipelines, or custom user interfaces. The server uses bidirectional streaming to send real-time text chunks, tool calls, and request permissions for sensitive commands. + +### 1. Start the gRPC Server + +Start the core engine as a gRPC service on `localhost:50051`: + +```bash +npm run dev:grpc +``` + +#### Configuration + +| Variable | Default | Description | +|-----------|-------------|------------------------------------------------| +| `GRPC_PORT` | `50051` | Port the gRPC server listens on | +| `GRPC_HOST` | `localhost` | Bind address. Use `0.0.0.0` to expose on all interfaces (not recommended without authentication) | + +### 2. Run the Test CLI Client + +We provide a lightweight CLI client that communicates exclusively over gRPC. It acts just like the main interactive CLI, rendering colors, streaming tokens, and prompting you for tool permissions (y/n) via the gRPC `action_required` event. + +In a separate terminal, run: + +```bash +npm run dev:grpc:cli +``` + +*Note: The gRPC definitions are located in `src/proto/openclaude.proto`. You can use this file to generate clients in Python, Go, Rust, or any other language.* + +--- + ## Source Build And Local Development ```bash diff --git a/bun.lock b/bun.lock index 391ed75e..6b99975a 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,8 @@ "@anthropic-ai/vertex-sdk": "0.14.4", "@commander-js/extra-typings": "12.1.0", "@growthbook/growthbook": "1.6.5", + "@grpc/grpc-js": "^1.14.3", + "@grpc/proto-loader": "^0.8.0", "@mendable/firecrawl-js": "4.18.1", "@modelcontextprotocol/sdk": "1.29.0", "@opentelemetry/api": "1.9.1", @@ -84,6 +86,7 @@ "@types/bun": "1.3.11", "@types/node": "25.5.0", "@types/react": "19.2.14", + "tsx": "^4.21.0", "typescript": "5.9.3", }, }, @@ -184,6 +187,58 @@ "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + "@growthbook/growthbook": ["@growthbook/growthbook@1.6.5", "", { "dependencies": { "dom-mutator": "^0.6.0" } }, "sha512-mUaMsgeUTpRIUOTn33EUXHRK6j7pxBjwqH4WpQyq+pukjd1AIzWlEa6w7i6bInJUcweGgP2beXZmaP6b6UPn7A=="], "@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="], @@ -456,7 +511,7 @@ "cli-highlight": ["cli-highlight@2.1.11", "", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="], - "cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], @@ -524,6 +579,8 @@ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], @@ -570,6 +627,8 @@ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="], @@ -588,6 +647,8 @@ "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + "get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="], + "google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="], "google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="], @@ -764,6 +825,8 @@ "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], @@ -834,6 +897,8 @@ "tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + "turndown": ["turndown@7.2.2", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ=="], "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], @@ -884,9 +949,9 @@ "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], - "yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - "yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], @@ -1086,8 +1151,6 @@ "@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@grpc/proto-loader/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@1.30.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ=="], "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.57.2", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/otlp-transformer": "0.57.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-XdxEzL23Urhidyebg5E6jZoaiW5ygP/mRjxLHixogbqwDy2Faduzb5N0o/Oi+XTIJu+iyxXdVORjXax+Qgfxag=="], @@ -1306,6 +1369,8 @@ "cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "cli-highlight/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1360,12 +1425,6 @@ "@aws-sdk/nested-clients/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="], - "@grpc/proto-loader/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "@grpc/proto-loader/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "@grpc/proto-loader/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.57.2", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A=="], @@ -1432,6 +1491,12 @@ "cli-highlight/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "cli-highlight/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], + + "cli-highlight/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "cli-highlight/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -1472,16 +1537,6 @@ "@aws-sdk/nested-clients/@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="], - "@grpc/proto-loader/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@grpc/proto-loader/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "@grpc/proto-loader/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "@grpc/proto-loader/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "@grpc/proto-loader/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], @@ -1502,6 +1557,16 @@ "@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@2.2.0", "", { "dependencies": { "@smithy/types": "^2.12.0", "@smithy/util-uri-escape": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-L1kSeviUWL+emq3CUVSgdogoM/D9QMFaqxL/dd0X7PCNWmPXqt+ExtrBjqT0V7HLN03Vs9SuiLrG3zy3JGnE5A=="], + "cli-highlight/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cli-highlight/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "cli-highlight/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "cli-highlight/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "cli-highlight/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "qrcode/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], @@ -1514,16 +1579,16 @@ "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@grpc/proto-loader/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "@grpc/proto-loader/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "@grpc/proto-loader/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-jtmJMyt1xMD/d8OtbVJ2gFZOSKc+ueYJZPW20ULW1GOp/q/YIM0wNh+u8ZFao9UaIGz4WoPW8hC64qlWLIfoDA=="], "@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-jtmJMyt1xMD/d8OtbVJ2gFZOSKc+ueYJZPW20ULW1GOp/q/YIM0wNh+u8ZFao9UaIGz4WoPW8hC64qlWLIfoDA=="], + "cli-highlight/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cli-highlight/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "cli-highlight/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "qrcode/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "qrcode/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], diff --git a/package.json b/package.json index b3f11890..88b23bcc 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "profile:code": "bun run profile:init -- --provider ollama --model qwen2.5-coder:7b", "dev:fast": "bun run profile:fast && bun run dev:ollama:fast", "dev:code": "bun run profile:code && bun run dev:profile", + "dev:grpc": "bun run scripts/start-grpc.ts", + "dev:grpc:cli": "bun run scripts/grpc-cli.ts", "start": "node dist/cli.mjs", "test": "bun test", "test:coverage": "bun test --coverage --coverage-reporter=lcov --coverage-dir=coverage --max-concurrency=1 && bun run scripts/render-coverage-heatmap.ts", @@ -57,6 +59,8 @@ "@anthropic-ai/vertex-sdk": "0.14.4", "@commander-js/extra-typings": "12.1.0", "@growthbook/growthbook": "1.6.5", + "@grpc/grpc-js": "^1.14.3", + "@grpc/proto-loader": "^0.8.0", "@mendable/firecrawl-js": "4.18.1", "@modelcontextprotocol/sdk": "1.29.0", "@opentelemetry/api": "1.9.1", @@ -128,6 +132,7 @@ "@types/bun": "1.3.11", "@types/node": "25.5.0", "@types/react": "19.2.14", + "tsx": "^4.21.0", "typescript": "5.9.3" }, "engines": { diff --git a/scripts/grpc-cli.ts b/scripts/grpc-cli.ts new file mode 100644 index 00000000..90467e34 --- /dev/null +++ b/scripts/grpc-cli.ts @@ -0,0 +1,121 @@ +import * as grpc from '@grpc/grpc-js' +import * as protoLoader from '@grpc/proto-loader' +import path from 'path' +import * as readline from 'readline' + +const PROTO_PATH = path.resolve(import.meta.dirname, '../src/proto/openclaude.proto') + +const packageDefinition = protoLoader.loadSync(PROTO_PATH, { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, +}) + +const protoDescriptor = grpc.loadPackageDefinition(packageDefinition) as any +const openclaudeProto = protoDescriptor.openclaude.v1 + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}) + +function askQuestion(query: string): Promise { + return new Promise(resolve => { + rl.question(query, resolve) + }) +} + +async function main() { + const host = process.env.GRPC_HOST || 'localhost' + const port = process.env.GRPC_PORT || '50051' + const client = new openclaudeProto.AgentService( + `${host}:${port}`, + grpc.credentials.createInsecure() + ) + + let call: grpc.ClientDuplexStream | null = null + + const startStream = () => { + call = client.Chat() + let textStreamed = false + + call.on('data', async (serverMessage: any) => { + if (serverMessage.text_chunk) { + process.stdout.write(serverMessage.text_chunk.text) + textStreamed = true + } else if (serverMessage.tool_start) { + console.log(`\n\x1b[36m[Tool Call]\x1b[0m \x1b[1m${serverMessage.tool_start.tool_name}\x1b[0m`) + console.log(`\x1b[90m${serverMessage.tool_start.arguments_json}\x1b[0m\n`) + } else if (serverMessage.tool_result) { + console.log(`\n\x1b[32m[Tool Result]\x1b[0m \x1b[1m${serverMessage.tool_result.tool_name}\x1b[0m`) + const out = serverMessage.tool_result.output + if (out.length > 500) { + console.log(`\x1b[90m${out.substring(0, 500)}...\n(Output truncated, total length: ${out.length})\x1b[0m`) + } else { + console.log(`\x1b[90m${out}\x1b[0m`) + } + } else if (serverMessage.action_required) { + const action = serverMessage.action_required + console.log(`\n\x1b[33m[Action Required]\x1b[0m`) + const reply = await askQuestion(`\x1b[1m${action.question}\x1b[0m (y/n) > `) + + call?.write({ + input: { + prompt_id: action.prompt_id, + reply: reply.trim() + } + }) + } else if (serverMessage.done) { + if (!textStreamed && serverMessage.done.full_text) { + process.stdout.write(serverMessage.done.full_text) + } + textStreamed = false + console.log('\n\x1b[32m[Generation Complete]\x1b[0m') + promptUser() + } else if (serverMessage.error) { + console.error(`\n\x1b[31m[Server Error]\x1b[0m ${serverMessage.error.message}`) + promptUser() + } + }) + + call.on('end', () => { + console.log('\n\x1b[90m[Stream closed by server]\x1b[0m') + // Don't prompt user here, let 'done' or 'error' handlers do it + }) + + call.on('error', (err: Error) => { + console.error('\n\x1b[31m[Stream Error]\x1b[0m', err.message) + promptUser() + }) + } + + const promptUser = async () => { + const message = await askQuestion('\n\x1b[35m> \x1b[0m') + + if (message.trim().toLowerCase() === '/exit' || message.trim().toLowerCase() === '/quit') { + console.log('Bye!') + rl.close() + process.exit(0) + } + + if (!call || call.destroyed) { + startStream() + } + + call!.write({ + request: { + session_id: 'cli-session-1', + message: message, + working_directory: process.cwd() + } + }) + } + + console.log('\x1b[32mOpenClaude gRPC CLI\x1b[0m') + console.log('\x1b[90mType /exit to quit.\x1b[0m') + promptUser() +} + +main() diff --git a/scripts/start-grpc.ts b/scripts/start-grpc.ts new file mode 100644 index 00000000..689972cf --- /dev/null +++ b/scripts/start-grpc.ts @@ -0,0 +1,50 @@ +import { GrpcServer } from '../src/grpc/server.ts' +import { init } from '../src/entrypoints/init.ts' + +// Polyfill MACRO which is normally injected by the bundler +Object.assign(globalThis, { + MACRO: { + VERSION: '0.1.7', + DISPLAY_VERSION: '0.1.7', + PACKAGE_URL: '@gitlawb/openclaude', + } +}) + +async function main() { + console.log('Starting OpenClaude gRPC Server...') + await init() + + // Mirror CLI bootstrap: hydrate secure tokens and resolve provider profile + const { enableConfigs } = await import('../src/utils/config.js') + enableConfigs() + const { applySafeConfigEnvironmentVariables } = await import('../src/utils/managedEnv.js') + applySafeConfigEnvironmentVariables() + const { hydrateGeminiAccessTokenFromSecureStorage } = await import('../src/utils/geminiCredentials.js') + hydrateGeminiAccessTokenFromSecureStorage() + const { hydrateGithubModelsTokenFromSecureStorage } = await import('../src/utils/githubModelsCredentials.js') + hydrateGithubModelsTokenFromSecureStorage() + + const { buildStartupEnvFromProfile, applyProfileEnvToProcessEnv } = await import('../src/utils/providerProfile.js') + const { getProviderValidationError, validateProviderEnvOrExit } = await import('../src/utils/providerValidation.js') + const startupEnv = await buildStartupEnvFromProfile({ processEnv: process.env }) + if (startupEnv !== process.env) { + const startupProfileError = await getProviderValidationError(startupEnv) + if (startupProfileError) { + console.warn(`Warning: ignoring saved provider profile. ${startupProfileError}`) + } else { + applyProfileEnvToProcessEnv(process.env, startupEnv) + } + } + await validateProviderEnvOrExit() + + const port = process.env.GRPC_PORT ? parseInt(process.env.GRPC_PORT, 10) : 50051 + const host = process.env.GRPC_HOST || 'localhost' + const server = new GrpcServer() + + server.start(port, host) +} + +main().catch((err) => { + console.error('Fatal error starting gRPC server:', err) + process.exit(1) +}) diff --git a/src/grpc/server.ts b/src/grpc/server.ts new file mode 100644 index 00000000..894a111a --- /dev/null +++ b/src/grpc/server.ts @@ -0,0 +1,252 @@ +import * as grpc from '@grpc/grpc-js' +import * as protoLoader from '@grpc/proto-loader' +import path from 'path' +import { randomUUID } from 'crypto' +import { QueryEngine } from '../QueryEngine.js' +import { getTools } from '../tools.js' +import { getDefaultAppState } from '../state/AppStateStore.js' +import { AppState } from '../state/AppState.js' +import { FileStateCache, READ_FILE_STATE_CACHE_SIZE } from '../utils/fileStateCache.js' + +const PROTO_PATH = path.resolve(import.meta.dirname, '../proto/openclaude.proto') + +const packageDefinition = protoLoader.loadSync(PROTO_PATH, { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, +}) + +const protoDescriptor = grpc.loadPackageDefinition(packageDefinition) as any +const openclaudeProto = protoDescriptor.openclaude.v1 + +const MAX_SESSIONS = 1000 + +export class GrpcServer { + private server: grpc.Server + private sessions: Map = new Map() + + constructor() { + this.server = new grpc.Server() + this.server.addService(openclaudeProto.AgentService.service, { + Chat: this.handleChat.bind(this), + }) + } + + start(port: number = 50051, host: string = 'localhost') { + this.server.bindAsync( + `${host}:${port}`, + grpc.ServerCredentials.createInsecure(), + (error, boundPort) => { + if (error) { + console.error('Failed to start gRPC server', error) + return + } + console.log(`gRPC Server running at ${host}:${boundPort}`) + } + ) + } + + private handleChat(call: grpc.ServerDuplexStream) { + let engine: QueryEngine | null = null + let appState: AppState = getDefaultAppState() + const fileCache: FileStateCache = new FileStateCache(READ_FILE_STATE_CACHE_SIZE, 25 * 1024 * 1024) + + // To handle ActionRequired (ask user for permission) + const pendingRequests = new Map void>() + + // Accumulated messages from previous turns for multi-turn context + let previousMessages: any[] = [] + let sessionId = '' + let interrupted = false + + call.on('data', async (clientMessage) => { + try { + if (clientMessage.request) { + if (engine) { + call.write({ + error: { + message: 'A request is already in progress on this stream', + code: 'ALREADY_EXISTS' + } + }) + return + } + interrupted = false + const req = clientMessage.request + sessionId = req.session_id || '' + previousMessages = [] + + // Load previous messages from session store (cross-stream persistence) + if (sessionId && this.sessions.has(sessionId)) { + previousMessages = [...this.sessions.get(sessionId)!] + } + + const toolNameById = new Map() + + engine = new QueryEngine({ + cwd: req.working_directory || process.cwd(), + tools: getTools(appState.toolPermissionContext), // Gets all available tools + commands: [], // Slash commands + mcpClients: [], + agents: [], + ...(previousMessages.length > 0 ? { initialMessages: previousMessages } : {}), + includePartialMessages: true, + canUseTool: async (tool, input, context, assistantMsg, toolUseID) => { + if (toolUseID) { + toolNameById.set(toolUseID, tool.name) + } + // Notify client of the tool call first + call.write({ + tool_start: { + tool_name: tool.name, + arguments_json: JSON.stringify(input), + tool_use_id: toolUseID + } + }) + + // Ask user for permission + const promptId = randomUUID() + const question = `Approve ${tool.name}?` + call.write({ + action_required: { + prompt_id: promptId, + question, + type: 'CONFIRM_COMMAND' + } + }) + + return new Promise((resolve) => { + pendingRequests.set(promptId, (reply) => { + if (reply.toLowerCase() === 'yes' || reply.toLowerCase() === 'y') { + resolve({ behavior: 'allow' }) + } else { + resolve({ behavior: 'deny', reason: 'User denied via gRPC' }) + } + }) + }) + }, + getAppState: () => appState, + setAppState: (updater) => { appState = updater(appState) }, + readFileCache: fileCache, + userSpecifiedModel: req.model, + fallbackModel: req.model, + }) + + // Track accumulated response data for FinalResponse + let fullText = '' + let promptTokens = 0 + let completionTokens = 0 + + const generator = engine.submitMessage(req.message) + + for await (const msg of generator) { + if (msg.type === 'stream_event') { + if (msg.event.type === 'content_block_delta' && msg.event.delta.type === 'text_delta') { + call.write({ + text_chunk: { + text: msg.event.delta.text + } + }) + fullText += msg.event.delta.text + } + } else if (msg.type === 'user') { + // Extract tool results + const content = msg.message.content + if (Array.isArray(content)) { + for (const block of content) { + if (block.type === 'tool_result') { + let outputStr = '' + if (typeof block.content === 'string') { + outputStr = block.content + } else if (Array.isArray(block.content)) { + outputStr = block.content.map(c => c.type === 'text' ? c.text : '').join('\n') + } + call.write({ + tool_result: { + tool_name: toolNameById.get(block.tool_use_id) ?? block.tool_use_id, + tool_use_id: block.tool_use_id, + output: outputStr, + is_error: block.is_error || false + } + }) + } + } + } + } else if (msg.type === 'result') { + // Extract real token counts and final text from the result + if (msg.subtype === 'success') { + if (msg.result) { + fullText = msg.result + } + promptTokens = msg.usage?.input_tokens ?? 0 + completionTokens = msg.usage?.output_tokens ?? 0 + } + } + } + + if (!interrupted) { + // Save messages for multi-turn context in subsequent requests + previousMessages = [...engine.getMessages()] + + // Persist to session store for cross-stream resumption + if (sessionId) { + if (!this.sessions.has(sessionId) && this.sessions.size >= MAX_SESSIONS) { + // Evict oldest session (Map preserves insertion order) + this.sessions.delete(this.sessions.keys().next().value) + } + this.sessions.set(sessionId, previousMessages) + } + + call.write({ + done: { + full_text: fullText, + prompt_tokens: promptTokens, + completion_tokens: completionTokens + } + }) + } + + engine = null + + } else if (clientMessage.input) { + const promptId = clientMessage.input.prompt_id + const reply = clientMessage.input.reply + if (pendingRequests.has(promptId)) { + pendingRequests.get(promptId)!(reply) + pendingRequests.delete(promptId) + } + } else if (clientMessage.cancel) { + interrupted = true + if (engine) { + engine.interrupt() + } + call.end() + } + } catch (err: any) { + console.error("Error processing stream:", err) + call.write({ + error: { + message: err.message || "Internal server error", + code: "INTERNAL" + } + }) + call.end() + } + }) + + call.on('end', () => { + interrupted = true + // Unblock any pending permission prompts so canUseTool can return + for (const resolve of pendingRequests.values()) { + resolve('no') + } + if (engine) { + engine.interrupt() + } + engine = null + pendingRequests.clear() + }) + } +} diff --git a/src/proto/openclaude.proto b/src/proto/openclaude.proto new file mode 100644 index 00000000..07d39c97 --- /dev/null +++ b/src/proto/openclaude.proto @@ -0,0 +1,101 @@ +syntax = "proto3"; + +package openclaude.v1; + +// Main Agent Service +service AgentService { + // Bidirectional stream: client sends tasks and answers to agent prompts, + // server streams text tokens, tool states, and requests permissions. + rpc Chat(stream ClientMessage) returns (stream ServerMessage); +} + +// --------------------------------------------------------- +// MESSAGES FROM CLIENT (Input) +// --------------------------------------------------------- +message ClientMessage { + oneof payload { + // 1. Initial request (first message in the stream) + ChatRequest request = 2; + + // 2. User response to an agent prompt (e.g., command confirmation) + UserInput input = 3; + + // 3. Interrupt signal (if the user clicks "Stop generation") + CancelSignal cancel = 4; + } +} + +message ChatRequest { + string message = 1; + string working_directory = 2; // Where the agent should execute commands + reserved 3; // Reserved to prevent accidental reuse + optional string model = 4; + string session_id = 5; // Non-empty = cross-stream session persistence +} + +message UserInput { + string reply = 1; // Text response (e.g., "y", "no", or clarification) + string prompt_id = 2; // ID of the prompt we are responding to +} + +message CancelSignal { + string reason = 1; +} + +// --------------------------------------------------------- +// MESSAGES FROM SERVER (Output / Events) +// --------------------------------------------------------- +message ServerMessage { + // Using oneof guarantees that only one type of event arrives at a time + oneof event { + TextChunk text_chunk = 1; // Chunk of text from LLM + ToolCallStart tool_start = 2; // Agent started using a tool + ToolCallResult tool_result = 3; // Tool returned a result + ActionRequired action_required = 4;// Agent requires human intervention + FinalResponse done = 5; // Generation successfully completed + ErrorResponse error = 6; // A critical error occurred + } +} + +// Stream text chunk +message TextChunk { + string text = 1; +} + +// Agent decided to use a tool (bash, read_file, etc.) +message ToolCallStart { + string tool_name = 1; + string arguments_json = 2; // Arguments in JSON format + string tool_use_id = 3; // Correlation ID matching ToolCallResult +} + +// Result of tool execution +message ToolCallResult { + string tool_name = 1; + string output = 2; // stdout/stderr or file contents + bool is_error = 3; // Did the command itself fail + string tool_use_id = 4; // Correlation ID matching ToolCallStart +} + +// Agent paused work and is waiting for user decision +message ActionRequired { + string prompt_id = 1; // Client must return this ID in UserInput + string question = 2; // Question text (e.g., "Execute 'rm -rf /'?") + enum ActionType { + CONFIRM_COMMAND = 0; // Yes/No + REQUEST_INFORMATION = 1; // Text input + } + ActionType type = 3; +} + +// Final statistics +message FinalResponse { + string full_text = 1; // The entire generated text + int32 prompt_tokens = 2; + int32 completion_tokens = 3; +} + +message ErrorResponse { + string message = 1; + string code = 2; +} \ No newline at end of file