@uncensoredcode/openbridge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -0
- package/bin/openbridge.js +10 -0
- package/package.json +85 -0
- package/packages/cli/dist/args.d.ts +30 -0
- package/packages/cli/dist/args.js +160 -0
- package/packages/cli/dist/cli.d.ts +2 -0
- package/packages/cli/dist/cli.js +9 -0
- package/packages/cli/dist/index.d.ts +26 -0
- package/packages/cli/dist/index.js +76 -0
- package/packages/runtime/dist/assistant-protocol.d.ts +34 -0
- package/packages/runtime/dist/assistant-protocol.js +121 -0
- package/packages/runtime/dist/execution/in-process.d.ts +14 -0
- package/packages/runtime/dist/execution/in-process.js +45 -0
- package/packages/runtime/dist/execution/types.d.ts +49 -0
- package/packages/runtime/dist/execution/types.js +20 -0
- package/packages/runtime/dist/index.d.ts +86 -0
- package/packages/runtime/dist/index.js +60 -0
- package/packages/runtime/dist/normalizers/index.d.ts +6 -0
- package/packages/runtime/dist/normalizers/index.js +12 -0
- package/packages/runtime/dist/normalizers/legacy-packet.d.ts +6 -0
- package/packages/runtime/dist/normalizers/legacy-packet.js +131 -0
- package/packages/runtime/dist/output-sanitizer.d.ts +23 -0
- package/packages/runtime/dist/output-sanitizer.js +78 -0
- package/packages/runtime/dist/packet-extractor.d.ts +17 -0
- package/packages/runtime/dist/packet-extractor.js +43 -0
- package/packages/runtime/dist/packet-normalizer.d.ts +21 -0
- package/packages/runtime/dist/packet-normalizer.js +47 -0
- package/packages/runtime/dist/prompt-compiler.d.ts +28 -0
- package/packages/runtime/dist/prompt-compiler.js +301 -0
- package/packages/runtime/dist/protocol.d.ts +44 -0
- package/packages/runtime/dist/protocol.js +165 -0
- package/packages/runtime/dist/provider-failure.d.ts +52 -0
- package/packages/runtime/dist/provider-failure.js +236 -0
- package/packages/runtime/dist/provider.d.ts +40 -0
- package/packages/runtime/dist/provider.js +1 -0
- package/packages/runtime/dist/runtime.d.ts +86 -0
- package/packages/runtime/dist/runtime.js +462 -0
- package/packages/runtime/dist/session-bound-provider.d.ts +52 -0
- package/packages/runtime/dist/session-bound-provider.js +366 -0
- package/packages/runtime/dist/tool-name-aliases.d.ts +5 -0
- package/packages/runtime/dist/tool-name-aliases.js +13 -0
- package/packages/runtime/dist/tools/bash.d.ts +9 -0
- package/packages/runtime/dist/tools/bash.js +157 -0
- package/packages/runtime/dist/tools/edit.d.ts +9 -0
- package/packages/runtime/dist/tools/edit.js +94 -0
- package/packages/runtime/dist/tools/index.d.ts +39 -0
- package/packages/runtime/dist/tools/index.js +27 -0
- package/packages/runtime/dist/tools/list-dir.d.ts +9 -0
- package/packages/runtime/dist/tools/list-dir.js +127 -0
- package/packages/runtime/dist/tools/read.d.ts +9 -0
- package/packages/runtime/dist/tools/read.js +56 -0
- package/packages/runtime/dist/tools/registry.d.ts +15 -0
- package/packages/runtime/dist/tools/registry.js +38 -0
- package/packages/runtime/dist/tools/runtime-path.d.ts +7 -0
- package/packages/runtime/dist/tools/runtime-path.js +22 -0
- package/packages/runtime/dist/tools/search-files.d.ts +9 -0
- package/packages/runtime/dist/tools/search-files.js +149 -0
- package/packages/runtime/dist/tools/text-file.d.ts +32 -0
- package/packages/runtime/dist/tools/text-file.js +101 -0
- package/packages/runtime/dist/tools/workspace-path.d.ts +17 -0
- package/packages/runtime/dist/tools/workspace-path.js +70 -0
- package/packages/runtime/dist/tools/write.d.ts +9 -0
- package/packages/runtime/dist/tools/write.js +59 -0
- package/packages/server/dist/bridge/bridge-model-catalog.d.ts +56 -0
- package/packages/server/dist/bridge/bridge-model-catalog.js +100 -0
- package/packages/server/dist/bridge/bridge-runtime-service.d.ts +61 -0
- package/packages/server/dist/bridge/bridge-runtime-service.js +1386 -0
- package/packages/server/dist/bridge/chat-completions/chat-completion-service.d.ts +127 -0
- package/packages/server/dist/bridge/chat-completions/chat-completion-service.js +1026 -0
- package/packages/server/dist/bridge/index.d.ts +335 -0
- package/packages/server/dist/bridge/index.js +45 -0
- package/packages/server/dist/bridge/live-provider-extraction-canary.d.ts +69 -0
- package/packages/server/dist/bridge/live-provider-extraction-canary.js +186 -0
- package/packages/server/dist/bridge/providers/generic-provider-transport.d.ts +53 -0
- package/packages/server/dist/bridge/providers/generic-provider-transport.js +973 -0
- package/packages/server/dist/bridge/providers/provider-session-resolver.d.ts +17 -0
- package/packages/server/dist/bridge/providers/provider-session-resolver.js +95 -0
- package/packages/server/dist/bridge/providers/provider-streams.d.ts +80 -0
- package/packages/server/dist/bridge/providers/provider-streams.js +844 -0
- package/packages/server/dist/bridge/providers/provider-transport-profile.d.ts +194 -0
- package/packages/server/dist/bridge/providers/provider-transport-profile.js +198 -0
- package/packages/server/dist/bridge/providers/web-provider-transport.d.ts +30 -0
- package/packages/server/dist/bridge/providers/web-provider-transport.js +151 -0
- package/packages/server/dist/bridge/state/file-bridge-state-store.d.ts +36 -0
- package/packages/server/dist/bridge/state/file-bridge-state-store.js +164 -0
- package/packages/server/dist/bridge/stores/local-session-package-store.d.ts +23 -0
- package/packages/server/dist/bridge/stores/local-session-package-store.js +548 -0
- package/packages/server/dist/bridge/stores/provider-store.d.ts +94 -0
- package/packages/server/dist/bridge/stores/provider-store.js +143 -0
- package/packages/server/dist/bridge/stores/session-backed-provider-store.d.ts +7 -0
- package/packages/server/dist/bridge/stores/session-backed-provider-store.js +26 -0
- package/packages/server/dist/bridge/stores/session-package-store.d.ts +286 -0
- package/packages/server/dist/bridge/stores/session-package-store.js +1527 -0
- package/packages/server/dist/bridge/stores/session-store.d.ts +120 -0
- package/packages/server/dist/bridge/stores/session-store.js +139 -0
- package/packages/server/dist/cli/index.d.ts +9 -0
- package/packages/server/dist/cli/index.js +6 -0
- package/packages/server/dist/cli/main.d.ts +2 -0
- package/packages/server/dist/cli/main.js +9 -0
- package/packages/server/dist/cli/run-bridge-server-cli.d.ts +54 -0
- package/packages/server/dist/cli/run-bridge-server-cli.js +371 -0
- package/packages/server/dist/client/bridge-api-client.d.ts +61 -0
- package/packages/server/dist/client/bridge-api-client.js +267 -0
- package/packages/server/dist/client/index.d.ts +11 -0
- package/packages/server/dist/client/index.js +11 -0
- package/packages/server/dist/config/bridge-server-config.d.ts +52 -0
- package/packages/server/dist/config/bridge-server-config.js +118 -0
- package/packages/server/dist/config/index.d.ts +20 -0
- package/packages/server/dist/config/index.js +8 -0
- package/packages/server/dist/http/bridge-api-route-context.d.ts +14 -0
- package/packages/server/dist/http/bridge-api-route-context.js +1 -0
- package/packages/server/dist/http/create-bridge-api-server.d.ts +72 -0
- package/packages/server/dist/http/create-bridge-api-server.js +225 -0
- package/packages/server/dist/http/index.d.ts +5 -0
- package/packages/server/dist/http/index.js +5 -0
- package/packages/server/dist/http/parse-request.d.ts +6 -0
- package/packages/server/dist/http/parse-request.js +27 -0
- package/packages/server/dist/http/register-bridge-api-routes.d.ts +7 -0
- package/packages/server/dist/http/register-bridge-api-routes.js +17 -0
- package/packages/server/dist/http/routes/admin-routes.d.ts +7 -0
- package/packages/server/dist/http/routes/admin-routes.js +135 -0
- package/packages/server/dist/http/routes/chat-completions-route.d.ts +7 -0
- package/packages/server/dist/http/routes/chat-completions-route.js +49 -0
- package/packages/server/dist/http/routes/health-routes.d.ts +6 -0
- package/packages/server/dist/http/routes/health-routes.js +7 -0
- package/packages/server/dist/http/routes/message-routes.d.ts +7 -0
- package/packages/server/dist/http/routes/message-routes.js +7 -0
- package/packages/server/dist/index.d.ts +85 -0
- package/packages/server/dist/index.js +28 -0
- package/packages/server/dist/security/bridge-auth.d.ts +9 -0
- package/packages/server/dist/security/bridge-auth.js +41 -0
- package/packages/server/dist/security/cors-policy.d.ts +5 -0
- package/packages/server/dist/security/cors-policy.js +34 -0
- package/packages/server/dist/security/index.d.ts +16 -0
- package/packages/server/dist/security/index.js +12 -0
- package/packages/server/dist/security/redact-sensitive-values.d.ts +19 -0
- package/packages/server/dist/security/redact-sensitive-values.js +67 -0
- package/packages/server/dist/shared/api-schema.d.ts +133 -0
- package/packages/server/dist/shared/api-schema.js +1 -0
- package/packages/server/dist/shared/bridge-api-error.d.ts +17 -0
- package/packages/server/dist/shared/bridge-api-error.js +19 -0
- package/packages/server/dist/shared/index.d.ts +7 -0
- package/packages/server/dist/shared/index.js +7 -0
- package/packages/server/dist/shared/output.d.ts +5 -0
- package/packages/server/dist/shared/output.js +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# uncensoredcode // openbridge
|
|
2
|
+
|
|
3
|
+
Turn a glossy web-chat LLM into an agent-ready, tool-calling model.
|
|
4
|
+
|
|
5
|
+
`openbridge` reuses authenticated browser chat sessions and exposes them through an OpenAI-compatible API so agents like `opencode`, `pi`, `OpenClaw`, `Hermes`, and anything else that can speak OpenAI-style chat completions can drive them like real tool-call models.
|
|
6
|
+
|
|
7
|
+
This is not another chatbot wrapper. The point is to take the classic consumer web-chat experience, keep the auth session you already have, and bridge it into something agents can actually use.
|
|
8
|
+
|
|
9
|
+
## What It Does
|
|
10
|
+
|
|
11
|
+
- Reuses authenticated web sessions instead of forcing a separate API key flow.
|
|
12
|
+
- Preserves provider conversation bindings so follow-up turns keep working.
|
|
13
|
+
- Exposes an OpenAI-compatible `/v1/chat/completions` surface.
|
|
14
|
+
- Supports generic transport profiles for `http-sse`, `http-json`, and `http-connect`.
|
|
15
|
+
- Lets you run the same bridge against different upstream chat products without rewriting your agent stack every time.
|
|
16
|
+
|
|
17
|
+
Most of the currently tested targets are the usual big chat LLM products. The actual goal is broader: make this bridge generic enough to sit in front of any OpenAI-compatible API or browser-backed chat transport we can model cleanly.
|
|
18
|
+
|
|
19
|
+
## Critical Path: Session Extraction
|
|
20
|
+
|
|
21
|
+
The critical path is session extraction. That is why the extension exists.
|
|
22
|
+
|
|
23
|
+
Maintainer note: I personally believe users should be extremely careful with any extension that extracts auth sessions. That is powerful, invasive, and easy to misuse. If you do not want to trust an extension with that job, manually extract the session material yourself and install it through the session-package flow, or have an agent vibe-code a dedicated installer on top of this repo's formats and server endpoints.
|
|
24
|
+
|
|
25
|
+
In other words: the extension is a convenience path, not a trust requirement.
|
|
26
|
+
|
|
27
|
+
## Tested Targets
|
|
28
|
+
|
|
29
|
+
Confirmed in the repo's transport/session-package test suite:
|
|
30
|
+
|
|
31
|
+
- `chat.deepseek.com` with `deepseek-chat`
|
|
32
|
+
- `chat.qwen.ai` with `qwen3.6-plus`
|
|
33
|
+
- `chat.z.ai` with `glm-4.7`
|
|
34
|
+
- `www.kimi.com` as a Kimi-style `http-connect` target
|
|
35
|
+
|
|
36
|
+
Observed in checked-in bridge continuation state in this repo:
|
|
37
|
+
|
|
38
|
+
- `chat.qwen.ai` with `qwen3-max` and `qwen3.6-plus`
|
|
39
|
+
- `chat.deepseek.com` with `deepseek-chat`
|
|
40
|
+
- `chat.z.ai` with `GLM-5-Turbo` and `glm-4.7`
|
|
41
|
+
- `www.kimi.com` with `kimi-k2@instant`, `kimi-k2@no-thinking`, and `kimi-k2@thinking`
|
|
42
|
+
|
|
43
|
+
That list should be read as "known working surfaces we have evidence for", not as a product boundary. The bridge is intended to be generic.
|
|
44
|
+
|
|
45
|
+
## Package Surface
|
|
46
|
+
|
|
47
|
+
- `@uncensoredcode/openbridge`: default server export.
|
|
48
|
+
- `@uncensoredcode/openbridge/server`: Fastify HTTP API, provider/session-package storage, session vault, and standalone bridge server.
|
|
49
|
+
- `@uncensoredcode/openbridge/runtime`: provider turn compilation, packet handling, tool execution, and runtime helpers.
|
|
50
|
+
- `@uncensoredcode/openbridge/cli`: unified `openbridge` CLI for server control, health checks, and sending prompts to a running bridge server.
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
bun install
|
|
56
|
+
bun run build
|
|
57
|
+
bun run format:check
|
|
58
|
+
bun run lint
|
|
59
|
+
bun run test
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Use the local CLI during development:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
bun run ./bin/openbridge.js --help
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Start the server in watch mode:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bun run dev:server
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Or start the standalone server directly:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
openbridge start
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Check health:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
openbridge health
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Send a prompt through a bridge session:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
openbridge --session demo "Read README.md"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Use the OpenAI-compatible endpoint directly:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
curl http://127.0.0.1:4318/v1/chat/completions \
|
|
96
|
+
-H 'content-type: application/json' \
|
|
97
|
+
-d '{
|
|
98
|
+
"model": "chat-qwen-ai/qwen3-max",
|
|
99
|
+
"messages": [
|
|
100
|
+
{ "role": "user", "content": "Reply with exactly OK." }
|
|
101
|
+
]
|
|
102
|
+
}'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Session Package Install Flow
|
|
106
|
+
|
|
107
|
+
Captured session material is installed per provider through:
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
PUT /v1/providers/:id/session-package
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The server can infer provider transport details from captured browser requests and headers, then store the session package for later reuse.
|
|
114
|
+
|
|
115
|
+
## Why This Exists
|
|
116
|
+
|
|
117
|
+
The frontier labs keep shipping powerful models behind polished web apps. Agents want stable tool-calling APIs. `openbridge` is the splice point between those two worlds.
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uncensoredcode/openbridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenBridge runtime, server, and CLI for bridging AI providers through a unified interface.",
|
|
5
|
+
"packageManager": "bun@1.3.11",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"author": "Linuz",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"main": "./packages/server/dist/index.js",
|
|
10
|
+
"types": "./packages/server/dist/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"packages/runtime/dist",
|
|
14
|
+
"packages/server/dist",
|
|
15
|
+
"packages/cli/dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/uncensoredcode/openbridge.git"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/uncensoredcode/openbridge",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/uncensoredcode/openbridge/issues"
|
|
28
|
+
},
|
|
29
|
+
"bin": {
|
|
30
|
+
"openbridge": "bin/openbridge.js"
|
|
31
|
+
},
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./packages/server/dist/index.d.ts",
|
|
35
|
+
"default": "./packages/server/dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./server": {
|
|
38
|
+
"types": "./packages/server/dist/index.d.ts",
|
|
39
|
+
"default": "./packages/server/dist/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./runtime": {
|
|
42
|
+
"types": "./packages/runtime/dist/index.d.ts",
|
|
43
|
+
"default": "./packages/runtime/dist/index.js"
|
|
44
|
+
},
|
|
45
|
+
"./cli": {
|
|
46
|
+
"types": "./packages/cli/dist/index.d.ts",
|
|
47
|
+
"default": "./packages/cli/dist/index.js"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"workspaces": [
|
|
51
|
+
"packages/runtime",
|
|
52
|
+
"packages/server",
|
|
53
|
+
"packages/cli"
|
|
54
|
+
],
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "cd packages/runtime && bun run build && cd ../server && bun run build && cd ../cli && bun run build && cd ../..",
|
|
57
|
+
"typecheck": "bun run build && cd packages/runtime && bun run typecheck && cd ../server && bun run typecheck && cd ../cli && bun run typecheck",
|
|
58
|
+
"test": "bun run build && bun run test:workspaces",
|
|
59
|
+
"test:workspaces": "cd packages/runtime && bun run test && cd ../server && bun run test && cd ../cli && bun run test",
|
|
60
|
+
"format": "prettier --write .",
|
|
61
|
+
"format:check": "prettier --check .",
|
|
62
|
+
"lint": "eslint .",
|
|
63
|
+
"lint:fix": "eslint . --fix",
|
|
64
|
+
"prepare": "husky",
|
|
65
|
+
"dev:runtime": "cd packages/runtime && bun run dev",
|
|
66
|
+
"dev:server": "cd packages/server && bun run dev",
|
|
67
|
+
"dev:cli": "cd packages/cli && bun run dev",
|
|
68
|
+
"clear-session-vault": "cd packages/server && bun run build && bun run clear-session-vault"
|
|
69
|
+
},
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"commander": "*",
|
|
72
|
+
"fastify": "*",
|
|
73
|
+
"zod": "*"
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@eslint/js": "9.39.1",
|
|
77
|
+
"eslint": "9.39.1",
|
|
78
|
+
"eslint-plugin-import": "2.32.0",
|
|
79
|
+
"eslint-plugin-simple-import-sort": "12.1.1",
|
|
80
|
+
"globals": "16.4.0",
|
|
81
|
+
"husky": "9.1.7",
|
|
82
|
+
"prettier": "3.6.2",
|
|
83
|
+
"typescript-eslint": "8.46.1"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { BridgeApiToolProfile } from "@uncensoredcode/openbridge/server";
|
|
2
|
+
type BridgeCliCommand = {
|
|
3
|
+
kind: "help";
|
|
4
|
+
} | {
|
|
5
|
+
kind: "health";
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: "server";
|
|
9
|
+
argv: string[];
|
|
10
|
+
} | {
|
|
11
|
+
kind: "send";
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
sessionId: string;
|
|
14
|
+
input: string;
|
|
15
|
+
provider?: string;
|
|
16
|
+
model?: string;
|
|
17
|
+
metadata?: Record<string, unknown>;
|
|
18
|
+
toolProfile?: BridgeApiToolProfile;
|
|
19
|
+
};
|
|
20
|
+
type ParseBridgeCliArgsInput = {
|
|
21
|
+
argv: string[];
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
};
|
|
24
|
+
declare function parseBridgeCliArgs(input: ParseBridgeCliArgsInput): BridgeCliCommand;
|
|
25
|
+
declare function getBridgeCliHelpText(): string;
|
|
26
|
+
export declare const argsModule: {
|
|
27
|
+
parseBridgeCliArgs: typeof parseBridgeCliArgs;
|
|
28
|
+
getBridgeCliHelpText: typeof getBridgeCliHelpText;
|
|
29
|
+
};
|
|
30
|
+
export type { BridgeCliCommand, ParseBridgeCliArgsInput };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const DEFAULT_BASE_URL = "http://127.0.0.1:4318";
|
|
2
|
+
const SERVER_SUBCOMMANDS = new Set(["start", "chat", "live-canary", "clear-session-vault"]);
|
|
3
|
+
function parseBridgeCliArgs(input) {
|
|
4
|
+
const env = input.env ?? process.env;
|
|
5
|
+
const args = [...input.argv];
|
|
6
|
+
const command = args[0];
|
|
7
|
+
if (args.length === 0) {
|
|
8
|
+
return {
|
|
9
|
+
kind: "help"
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
if (command && SERVER_SUBCOMMANDS.has(command)) {
|
|
13
|
+
return {
|
|
14
|
+
kind: "server",
|
|
15
|
+
argv: args
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (command === "help" ||
|
|
19
|
+
command === "--help" ||
|
|
20
|
+
command === "-h" ||
|
|
21
|
+
args.includes("--help") ||
|
|
22
|
+
args.includes("-h")) {
|
|
23
|
+
return {
|
|
24
|
+
kind: "help"
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (command === "health") {
|
|
28
|
+
const { options, positionals } = parseFlags(args.slice(1));
|
|
29
|
+
if (positionals.length > 0) {
|
|
30
|
+
throw new Error("health does not accept positional arguments.");
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
kind: "health",
|
|
34
|
+
baseUrl: readBaseUrl(options.baseUrl, env)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const { options, positionals } = parseFlags(args);
|
|
38
|
+
const sessionId = requireNonEmptyString(options.session, "session");
|
|
39
|
+
const inputText = readInput(options.input, positionals);
|
|
40
|
+
const metadata = readMetadata(options.metadata);
|
|
41
|
+
const toolProfile = readToolProfile(options.toolProfile);
|
|
42
|
+
return {
|
|
43
|
+
kind: "send",
|
|
44
|
+
baseUrl: readBaseUrl(options.baseUrl, env),
|
|
45
|
+
sessionId,
|
|
46
|
+
input: inputText,
|
|
47
|
+
provider: optionalNonEmptyString(options.provider, "provider") ?? undefined,
|
|
48
|
+
model: optionalNonEmptyString(options.model, "model") ?? undefined,
|
|
49
|
+
metadata,
|
|
50
|
+
toolProfile
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function getBridgeCliHelpText() {
|
|
54
|
+
return [
|
|
55
|
+
"openbridge",
|
|
56
|
+
"",
|
|
57
|
+
"Usage:",
|
|
58
|
+
" openbridge start [--host <host>] [--port <port>] [--token <token>]",
|
|
59
|
+
" openbridge health [--base-url <url>]",
|
|
60
|
+
" openbridge --session <id> [--input <text>] [--base-url <url>] [--provider <id>] [--model <id>] [--metadata <json>]",
|
|
61
|
+
" openbridge chat --model <id> --message <text> [--system <text>] [--base-url <url>] [--stream]",
|
|
62
|
+
" openbridge live-canary [--state-root <path>] [--provider <id>] [--model <id>]",
|
|
63
|
+
" openbridge clear-session-vault [--state-root <path>] [--session-vault-path <path>]",
|
|
64
|
+
"",
|
|
65
|
+
"Examples:",
|
|
66
|
+
" openbridge start",
|
|
67
|
+
" openbridge health",
|
|
68
|
+
' openbridge --session demo "Read README.md"',
|
|
69
|
+
' openbridge --base-url http://127.0.0.1:4318 --session s1 --input "Run git status"'
|
|
70
|
+
].join("\n");
|
|
71
|
+
}
|
|
72
|
+
function parseFlags(argv) {
|
|
73
|
+
const options = {};
|
|
74
|
+
const positionals = [];
|
|
75
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
76
|
+
const value = argv[index] ?? "";
|
|
77
|
+
if (!value.startsWith("--")) {
|
|
78
|
+
positionals.push(value);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const key = value.slice(2);
|
|
82
|
+
const next = argv[index + 1];
|
|
83
|
+
if (!next || next.startsWith("--")) {
|
|
84
|
+
throw new Error(`Missing value for --${key}.`);
|
|
85
|
+
}
|
|
86
|
+
options[toOptionKey(key)] = next;
|
|
87
|
+
index += 1;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
options,
|
|
91
|
+
positionals
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function toOptionKey(value) {
|
|
95
|
+
return value.replace(/-([a-z])/g, (_match, character) => character.toUpperCase());
|
|
96
|
+
}
|
|
97
|
+
function readBaseUrl(value, env) {
|
|
98
|
+
return (optionalNonEmptyString(value, "base-url") ??
|
|
99
|
+
optionalNonEmptyString(env.BRIDGE_API_BASE_URL, "BRIDGE_API_BASE_URL") ??
|
|
100
|
+
optionalNonEmptyString(env.BRIDGE_SERVER_BASE_URL, "BRIDGE_SERVER_BASE_URL") ??
|
|
101
|
+
DEFAULT_BASE_URL);
|
|
102
|
+
}
|
|
103
|
+
function readInput(flagValue, positionals) {
|
|
104
|
+
const flagInput = optionalNonEmptyString(flagValue, "input");
|
|
105
|
+
const positionalInput = positionals.length > 0 ? positionals.join(" ") : null;
|
|
106
|
+
if (flagInput && positionalInput && flagInput !== positionalInput) {
|
|
107
|
+
throw new Error("Provide input either with --input or as a positional argument, not both.");
|
|
108
|
+
}
|
|
109
|
+
const resolved = flagInput ?? positionalInput;
|
|
110
|
+
if (!resolved?.trim()) {
|
|
111
|
+
throw new Error("input is required.");
|
|
112
|
+
}
|
|
113
|
+
return resolved;
|
|
114
|
+
}
|
|
115
|
+
function readMetadata(value) {
|
|
116
|
+
if (value === undefined) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
parsed = JSON.parse(value);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
throw new Error("metadata must be valid JSON.");
|
|
125
|
+
}
|
|
126
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
127
|
+
throw new Error("metadata must be a JSON object.");
|
|
128
|
+
}
|
|
129
|
+
return parsed;
|
|
130
|
+
}
|
|
131
|
+
function readToolProfile(value) {
|
|
132
|
+
if (value === undefined) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
if (value === "default" || value === "workspace") {
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
throw new Error('toolProfile must be either "default" or "workspace".');
|
|
139
|
+
}
|
|
140
|
+
function requireNonEmptyString(value, key) {
|
|
141
|
+
const normalized = optionalNonEmptyString(value, key);
|
|
142
|
+
if (!normalized) {
|
|
143
|
+
throw new Error(`${key} is required.`);
|
|
144
|
+
}
|
|
145
|
+
return normalized;
|
|
146
|
+
}
|
|
147
|
+
function optionalNonEmptyString(value, key) {
|
|
148
|
+
if (value === undefined) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const trimmed = value.trim();
|
|
152
|
+
if (!trimmed) {
|
|
153
|
+
throw new Error(`${key} must be a non-empty string.`);
|
|
154
|
+
}
|
|
155
|
+
return trimmed;
|
|
156
|
+
}
|
|
157
|
+
export const argsModule = {
|
|
158
|
+
parseBridgeCliArgs,
|
|
159
|
+
getBridgeCliHelpText
|
|
160
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BridgeApiClientFetch, RunBridgeServerCliInput } from "@uncensoredcode/openbridge/server";
|
|
2
|
+
declare const runBridgeServerCli: (input: import("../../server/dist/cli/run-bridge-server-cli.js").RunBridgeServerCliInput) => Promise<number>;
|
|
3
|
+
type BridgeCliDependencies = {
|
|
4
|
+
argv: string[];
|
|
5
|
+
env?: NodeJS.ProcessEnv;
|
|
6
|
+
fetchImpl?: BridgeApiClientFetch;
|
|
7
|
+
stdout?: {
|
|
8
|
+
write(value: string): void;
|
|
9
|
+
};
|
|
10
|
+
stderr?: {
|
|
11
|
+
write(value: string): void;
|
|
12
|
+
};
|
|
13
|
+
stdin?: RunBridgeServerCliInput["stdin"];
|
|
14
|
+
startServer?: RunBridgeServerCliInput["startServer"];
|
|
15
|
+
onServerStarted?: RunBridgeServerCliInput["onServerStarted"];
|
|
16
|
+
runLiveCanary?: RunBridgeServerCliInput["runLiveCanary"];
|
|
17
|
+
promptForVaultKey?: RunBridgeServerCliInput["promptForVaultKey"];
|
|
18
|
+
runServerCli?: typeof runBridgeServerCli;
|
|
19
|
+
};
|
|
20
|
+
declare function runBridgeCli(dependencies: BridgeCliDependencies): Promise<number>;
|
|
21
|
+
export declare const bridgeCli: {
|
|
22
|
+
getBridgeCliHelpText: () => string;
|
|
23
|
+
parseBridgeCliArgs: (input: import("./args.ts").ParseBridgeCliArgsInput) => import("./args.ts").BridgeCliCommand;
|
|
24
|
+
runBridgeCli: typeof runBridgeCli;
|
|
25
|
+
};
|
|
26
|
+
export type { BridgeCliCommand } from "./args.ts";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { bridgeServer } from "@uncensoredcode/openbridge/server";
|
|
2
|
+
import { argsModule } from "./args.js";
|
|
3
|
+
const { BridgeApiHttpError, checkBridgeHealth, runBridgeServerCli, sendBridgeMessage } = bridgeServer;
|
|
4
|
+
const { getBridgeCliHelpText, parseBridgeCliArgs } = argsModule;
|
|
5
|
+
async function runBridgeCli(dependencies) {
|
|
6
|
+
const stdout = dependencies.stdout ?? process.stdout;
|
|
7
|
+
const stderr = dependencies.stderr ?? process.stderr;
|
|
8
|
+
const stdin = dependencies.stdin ?? process.stdin;
|
|
9
|
+
const delegatedRunBridgeServerCli = dependencies.runServerCli ?? runBridgeServerCli;
|
|
10
|
+
let command;
|
|
11
|
+
try {
|
|
12
|
+
command = parseBridgeCliArgs({
|
|
13
|
+
argv: dependencies.argv,
|
|
14
|
+
env: dependencies.env
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
stderr.write(`${formatCliError(error)}\n`);
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
if (command.kind === "help") {
|
|
22
|
+
stdout.write(`${getBridgeCliHelpText()}\n`);
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
if (command.kind === "server") {
|
|
26
|
+
return delegatedRunBridgeServerCli({
|
|
27
|
+
argv: command.argv,
|
|
28
|
+
env: dependencies.env,
|
|
29
|
+
stdout,
|
|
30
|
+
stderr,
|
|
31
|
+
stdin,
|
|
32
|
+
startServer: dependencies.startServer,
|
|
33
|
+
onServerStarted: dependencies.onServerStarted,
|
|
34
|
+
runLiveCanary: dependencies.runLiveCanary,
|
|
35
|
+
fetchImpl: dependencies.fetchImpl,
|
|
36
|
+
promptForVaultKey: dependencies.promptForVaultKey
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
if (command.kind === "health") {
|
|
41
|
+
const health = await checkBridgeHealth({
|
|
42
|
+
baseUrl: command.baseUrl,
|
|
43
|
+
fetchImpl: dependencies.fetchImpl
|
|
44
|
+
});
|
|
45
|
+
stdout.write(`${health.ok ? "ok" : "unhealthy"}\n`);
|
|
46
|
+
return health.ok ? 0 : 1;
|
|
47
|
+
}
|
|
48
|
+
const response = await sendBridgeMessage({
|
|
49
|
+
baseUrl: command.baseUrl,
|
|
50
|
+
sessionId: command.sessionId,
|
|
51
|
+
input: command.input,
|
|
52
|
+
provider: command.provider,
|
|
53
|
+
model: command.model,
|
|
54
|
+
metadata: command.metadata,
|
|
55
|
+
toolProfile: command.toolProfile,
|
|
56
|
+
fetchImpl: dependencies.fetchImpl
|
|
57
|
+
});
|
|
58
|
+
stdout.write(`${response.output}\n`);
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
stderr.write(`${formatCliError(error)}\n`);
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function formatCliError(error) {
|
|
67
|
+
if (error instanceof BridgeApiHttpError) {
|
|
68
|
+
return `${error.code}: ${error.message}`;
|
|
69
|
+
}
|
|
70
|
+
return error instanceof Error ? error.message : String(error);
|
|
71
|
+
}
|
|
72
|
+
export const bridgeCli = {
|
|
73
|
+
getBridgeCliHelpText: argsModule.getBridgeCliHelpText,
|
|
74
|
+
parseBridgeCliArgs: argsModule.parseBridgeCliArgs,
|
|
75
|
+
runBridgeCli
|
|
76
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ToolDefinition } from "./execution/types.ts";
|
|
2
|
+
type AssistantToolCall = {
|
|
3
|
+
id?: string;
|
|
4
|
+
name: string;
|
|
5
|
+
arguments: Record<string, unknown>;
|
|
6
|
+
};
|
|
7
|
+
type AssistantFinalResponse = {
|
|
8
|
+
type: "final";
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
type AssistantToolResponse = {
|
|
12
|
+
type: "tool";
|
|
13
|
+
toolCall: AssistantToolCall;
|
|
14
|
+
};
|
|
15
|
+
type AssistantResponse = AssistantFinalResponse | AssistantToolResponse;
|
|
16
|
+
declare class AssistantProtocolError extends Error {
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
|
19
|
+
declare function parseAssistantResponse(rawText: string): AssistantResponse;
|
|
20
|
+
declare function validateAssistantResponse(response: AssistantResponse, availableTools: ToolDefinition[]): AssistantFinalResponse | AssistantToolResponse;
|
|
21
|
+
declare function parseAndValidateAssistantResponse(rawText: string, availableTools: ToolDefinition[]): AssistantFinalResponse | AssistantToolResponse;
|
|
22
|
+
declare function createFinalResponse(message: string): string;
|
|
23
|
+
declare function createToolResponse(toolCall: AssistantToolCall): string;
|
|
24
|
+
declare function serializeAssistantResponse(response: AssistantResponse): string;
|
|
25
|
+
export declare const assistantProtocolModule: {
|
|
26
|
+
AssistantProtocolError: typeof AssistantProtocolError;
|
|
27
|
+
parseAssistantResponse: typeof parseAssistantResponse;
|
|
28
|
+
validateAssistantResponse: typeof validateAssistantResponse;
|
|
29
|
+
parseAndValidateAssistantResponse: typeof parseAndValidateAssistantResponse;
|
|
30
|
+
createFinalResponse: typeof createFinalResponse;
|
|
31
|
+
createToolResponse: typeof createToolResponse;
|
|
32
|
+
serializeAssistantResponse: typeof serializeAssistantResponse;
|
|
33
|
+
};
|
|
34
|
+
export type { AssistantFinalResponse, AssistantProtocolError, AssistantResponse, AssistantToolCall, AssistantToolResponse };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
class AssistantProtocolError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "AssistantProtocolError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
function parseAssistantResponse(rawText) {
|
|
8
|
+
const trimmed = rawText.trim();
|
|
9
|
+
const finalMatch = trimmed.match(/^<final>([\s\S]*?)<\/final>$/);
|
|
10
|
+
if (finalMatch) {
|
|
11
|
+
const message = finalMatch[1] ?? "";
|
|
12
|
+
if (!message.trim()) {
|
|
13
|
+
throw new AssistantProtocolError("<final> block must not be empty.");
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
type: "final",
|
|
17
|
+
message
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const toolMatch = trimmed.match(/^<tool>([\s\S]*?)<\/tool>$/);
|
|
21
|
+
if (toolMatch) {
|
|
22
|
+
return {
|
|
23
|
+
type: "tool",
|
|
24
|
+
toolCall: parseToolPayload(toolMatch[1] ?? "")
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
throw new AssistantProtocolError('Assistant output must be exactly one <final>...</final> block or one <tool>{"name":"...","arguments":{...}}</tool> block.');
|
|
28
|
+
}
|
|
29
|
+
function validateAssistantResponse(response, availableTools) {
|
|
30
|
+
if (response.type !== "tool") {
|
|
31
|
+
return response;
|
|
32
|
+
}
|
|
33
|
+
const tool = availableTools.find((candidate) => candidate.name === response.toolCall.name);
|
|
34
|
+
if (!tool) {
|
|
35
|
+
throw new AssistantProtocolError(`Tool "${response.toolCall.name}" is not registered.`);
|
|
36
|
+
}
|
|
37
|
+
const schema = tool.inputSchema;
|
|
38
|
+
const argumentsRecord = response.toolCall.arguments;
|
|
39
|
+
const propertyNames = Object.keys(schema.properties);
|
|
40
|
+
const allowedProperties = new Set(propertyNames);
|
|
41
|
+
for (const name of Object.keys(argumentsRecord)) {
|
|
42
|
+
if (!allowedProperties.has(name)) {
|
|
43
|
+
throw new AssistantProtocolError(`Tool "${tool.name}" received unknown argument "${name}".`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const requiredName of schema.required) {
|
|
47
|
+
if (!(requiredName in argumentsRecord)) {
|
|
48
|
+
throw new AssistantProtocolError(`Tool "${tool.name}" is missing required argument "${requiredName}".`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const propertyName of propertyNames) {
|
|
52
|
+
if (!(propertyName in argumentsRecord)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const property = schema.properties[propertyName];
|
|
56
|
+
const value = argumentsRecord[propertyName];
|
|
57
|
+
if (property.type === "string" && typeof value !== "string") {
|
|
58
|
+
throw new AssistantProtocolError(`Tool "${tool.name}" argument "${propertyName}" must be a string.`);
|
|
59
|
+
}
|
|
60
|
+
if (property.type === "boolean" && typeof value !== "boolean") {
|
|
61
|
+
throw new AssistantProtocolError(`Tool "${tool.name}" argument "${propertyName}" must be a boolean.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return response;
|
|
65
|
+
}
|
|
66
|
+
function parseAndValidateAssistantResponse(rawText, availableTools) {
|
|
67
|
+
return validateAssistantResponse(parseAssistantResponse(rawText), availableTools);
|
|
68
|
+
}
|
|
69
|
+
function createFinalResponse(message) {
|
|
70
|
+
return `<final>${message}</final>`;
|
|
71
|
+
}
|
|
72
|
+
function createToolResponse(toolCall) {
|
|
73
|
+
return `<tool>${JSON.stringify({
|
|
74
|
+
name: toolCall.name,
|
|
75
|
+
arguments: toolCall.arguments
|
|
76
|
+
})}</tool>`;
|
|
77
|
+
}
|
|
78
|
+
function serializeAssistantResponse(response) {
|
|
79
|
+
return response.type === "final"
|
|
80
|
+
? createFinalResponse(response.message)
|
|
81
|
+
: createToolResponse(response.toolCall);
|
|
82
|
+
}
|
|
83
|
+
function parseToolPayload(raw) {
|
|
84
|
+
let parsed;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(raw.trim());
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw new AssistantProtocolError("<tool> payload must contain valid JSON.");
|
|
90
|
+
}
|
|
91
|
+
if (!isRecord(parsed)) {
|
|
92
|
+
throw new AssistantProtocolError("<tool> payload must decode to an object.");
|
|
93
|
+
}
|
|
94
|
+
const keys = Object.keys(parsed).sort();
|
|
95
|
+
if (keys.length !== 2 || keys[0] !== "arguments" || keys[1] !== "name") {
|
|
96
|
+
throw new AssistantProtocolError('<tool> JSON must contain only "name" and "arguments".');
|
|
97
|
+
}
|
|
98
|
+
const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
|
|
99
|
+
if (!name) {
|
|
100
|
+
throw new AssistantProtocolError('<tool> JSON field "name" must be a non-empty string.');
|
|
101
|
+
}
|
|
102
|
+
if (!isRecord(parsed.arguments)) {
|
|
103
|
+
throw new AssistantProtocolError('<tool> JSON field "arguments" must be an object.');
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
name,
|
|
107
|
+
arguments: parsed.arguments
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function isRecord(value) {
|
|
111
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
112
|
+
}
|
|
113
|
+
export const assistantProtocolModule = {
|
|
114
|
+
AssistantProtocolError,
|
|
115
|
+
parseAssistantResponse,
|
|
116
|
+
validateAssistantResponse,
|
|
117
|
+
parseAndValidateAssistantResponse,
|
|
118
|
+
createFinalResponse,
|
|
119
|
+
createToolResponse,
|
|
120
|
+
serializeAssistantResponse
|
|
121
|
+
};
|