@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.
Files changed (145) hide show
  1. package/README.md +117 -0
  2. package/bin/openbridge.js +10 -0
  3. package/package.json +85 -0
  4. package/packages/cli/dist/args.d.ts +30 -0
  5. package/packages/cli/dist/args.js +160 -0
  6. package/packages/cli/dist/cli.d.ts +2 -0
  7. package/packages/cli/dist/cli.js +9 -0
  8. package/packages/cli/dist/index.d.ts +26 -0
  9. package/packages/cli/dist/index.js +76 -0
  10. package/packages/runtime/dist/assistant-protocol.d.ts +34 -0
  11. package/packages/runtime/dist/assistant-protocol.js +121 -0
  12. package/packages/runtime/dist/execution/in-process.d.ts +14 -0
  13. package/packages/runtime/dist/execution/in-process.js +45 -0
  14. package/packages/runtime/dist/execution/types.d.ts +49 -0
  15. package/packages/runtime/dist/execution/types.js +20 -0
  16. package/packages/runtime/dist/index.d.ts +86 -0
  17. package/packages/runtime/dist/index.js +60 -0
  18. package/packages/runtime/dist/normalizers/index.d.ts +6 -0
  19. package/packages/runtime/dist/normalizers/index.js +12 -0
  20. package/packages/runtime/dist/normalizers/legacy-packet.d.ts +6 -0
  21. package/packages/runtime/dist/normalizers/legacy-packet.js +131 -0
  22. package/packages/runtime/dist/output-sanitizer.d.ts +23 -0
  23. package/packages/runtime/dist/output-sanitizer.js +78 -0
  24. package/packages/runtime/dist/packet-extractor.d.ts +17 -0
  25. package/packages/runtime/dist/packet-extractor.js +43 -0
  26. package/packages/runtime/dist/packet-normalizer.d.ts +21 -0
  27. package/packages/runtime/dist/packet-normalizer.js +47 -0
  28. package/packages/runtime/dist/prompt-compiler.d.ts +28 -0
  29. package/packages/runtime/dist/prompt-compiler.js +301 -0
  30. package/packages/runtime/dist/protocol.d.ts +44 -0
  31. package/packages/runtime/dist/protocol.js +165 -0
  32. package/packages/runtime/dist/provider-failure.d.ts +52 -0
  33. package/packages/runtime/dist/provider-failure.js +236 -0
  34. package/packages/runtime/dist/provider.d.ts +40 -0
  35. package/packages/runtime/dist/provider.js +1 -0
  36. package/packages/runtime/dist/runtime.d.ts +86 -0
  37. package/packages/runtime/dist/runtime.js +462 -0
  38. package/packages/runtime/dist/session-bound-provider.d.ts +52 -0
  39. package/packages/runtime/dist/session-bound-provider.js +366 -0
  40. package/packages/runtime/dist/tool-name-aliases.d.ts +5 -0
  41. package/packages/runtime/dist/tool-name-aliases.js +13 -0
  42. package/packages/runtime/dist/tools/bash.d.ts +9 -0
  43. package/packages/runtime/dist/tools/bash.js +157 -0
  44. package/packages/runtime/dist/tools/edit.d.ts +9 -0
  45. package/packages/runtime/dist/tools/edit.js +94 -0
  46. package/packages/runtime/dist/tools/index.d.ts +39 -0
  47. package/packages/runtime/dist/tools/index.js +27 -0
  48. package/packages/runtime/dist/tools/list-dir.d.ts +9 -0
  49. package/packages/runtime/dist/tools/list-dir.js +127 -0
  50. package/packages/runtime/dist/tools/read.d.ts +9 -0
  51. package/packages/runtime/dist/tools/read.js +56 -0
  52. package/packages/runtime/dist/tools/registry.d.ts +15 -0
  53. package/packages/runtime/dist/tools/registry.js +38 -0
  54. package/packages/runtime/dist/tools/runtime-path.d.ts +7 -0
  55. package/packages/runtime/dist/tools/runtime-path.js +22 -0
  56. package/packages/runtime/dist/tools/search-files.d.ts +9 -0
  57. package/packages/runtime/dist/tools/search-files.js +149 -0
  58. package/packages/runtime/dist/tools/text-file.d.ts +32 -0
  59. package/packages/runtime/dist/tools/text-file.js +101 -0
  60. package/packages/runtime/dist/tools/workspace-path.d.ts +17 -0
  61. package/packages/runtime/dist/tools/workspace-path.js +70 -0
  62. package/packages/runtime/dist/tools/write.d.ts +9 -0
  63. package/packages/runtime/dist/tools/write.js +59 -0
  64. package/packages/server/dist/bridge/bridge-model-catalog.d.ts +56 -0
  65. package/packages/server/dist/bridge/bridge-model-catalog.js +100 -0
  66. package/packages/server/dist/bridge/bridge-runtime-service.d.ts +61 -0
  67. package/packages/server/dist/bridge/bridge-runtime-service.js +1386 -0
  68. package/packages/server/dist/bridge/chat-completions/chat-completion-service.d.ts +127 -0
  69. package/packages/server/dist/bridge/chat-completions/chat-completion-service.js +1026 -0
  70. package/packages/server/dist/bridge/index.d.ts +335 -0
  71. package/packages/server/dist/bridge/index.js +45 -0
  72. package/packages/server/dist/bridge/live-provider-extraction-canary.d.ts +69 -0
  73. package/packages/server/dist/bridge/live-provider-extraction-canary.js +186 -0
  74. package/packages/server/dist/bridge/providers/generic-provider-transport.d.ts +53 -0
  75. package/packages/server/dist/bridge/providers/generic-provider-transport.js +973 -0
  76. package/packages/server/dist/bridge/providers/provider-session-resolver.d.ts +17 -0
  77. package/packages/server/dist/bridge/providers/provider-session-resolver.js +95 -0
  78. package/packages/server/dist/bridge/providers/provider-streams.d.ts +80 -0
  79. package/packages/server/dist/bridge/providers/provider-streams.js +844 -0
  80. package/packages/server/dist/bridge/providers/provider-transport-profile.d.ts +194 -0
  81. package/packages/server/dist/bridge/providers/provider-transport-profile.js +198 -0
  82. package/packages/server/dist/bridge/providers/web-provider-transport.d.ts +30 -0
  83. package/packages/server/dist/bridge/providers/web-provider-transport.js +151 -0
  84. package/packages/server/dist/bridge/state/file-bridge-state-store.d.ts +36 -0
  85. package/packages/server/dist/bridge/state/file-bridge-state-store.js +164 -0
  86. package/packages/server/dist/bridge/stores/local-session-package-store.d.ts +23 -0
  87. package/packages/server/dist/bridge/stores/local-session-package-store.js +548 -0
  88. package/packages/server/dist/bridge/stores/provider-store.d.ts +94 -0
  89. package/packages/server/dist/bridge/stores/provider-store.js +143 -0
  90. package/packages/server/dist/bridge/stores/session-backed-provider-store.d.ts +7 -0
  91. package/packages/server/dist/bridge/stores/session-backed-provider-store.js +26 -0
  92. package/packages/server/dist/bridge/stores/session-package-store.d.ts +286 -0
  93. package/packages/server/dist/bridge/stores/session-package-store.js +1527 -0
  94. package/packages/server/dist/bridge/stores/session-store.d.ts +120 -0
  95. package/packages/server/dist/bridge/stores/session-store.js +139 -0
  96. package/packages/server/dist/cli/index.d.ts +9 -0
  97. package/packages/server/dist/cli/index.js +6 -0
  98. package/packages/server/dist/cli/main.d.ts +2 -0
  99. package/packages/server/dist/cli/main.js +9 -0
  100. package/packages/server/dist/cli/run-bridge-server-cli.d.ts +54 -0
  101. package/packages/server/dist/cli/run-bridge-server-cli.js +371 -0
  102. package/packages/server/dist/client/bridge-api-client.d.ts +61 -0
  103. package/packages/server/dist/client/bridge-api-client.js +267 -0
  104. package/packages/server/dist/client/index.d.ts +11 -0
  105. package/packages/server/dist/client/index.js +11 -0
  106. package/packages/server/dist/config/bridge-server-config.d.ts +52 -0
  107. package/packages/server/dist/config/bridge-server-config.js +118 -0
  108. package/packages/server/dist/config/index.d.ts +20 -0
  109. package/packages/server/dist/config/index.js +8 -0
  110. package/packages/server/dist/http/bridge-api-route-context.d.ts +14 -0
  111. package/packages/server/dist/http/bridge-api-route-context.js +1 -0
  112. package/packages/server/dist/http/create-bridge-api-server.d.ts +72 -0
  113. package/packages/server/dist/http/create-bridge-api-server.js +225 -0
  114. package/packages/server/dist/http/index.d.ts +5 -0
  115. package/packages/server/dist/http/index.js +5 -0
  116. package/packages/server/dist/http/parse-request.d.ts +6 -0
  117. package/packages/server/dist/http/parse-request.js +27 -0
  118. package/packages/server/dist/http/register-bridge-api-routes.d.ts +7 -0
  119. package/packages/server/dist/http/register-bridge-api-routes.js +17 -0
  120. package/packages/server/dist/http/routes/admin-routes.d.ts +7 -0
  121. package/packages/server/dist/http/routes/admin-routes.js +135 -0
  122. package/packages/server/dist/http/routes/chat-completions-route.d.ts +7 -0
  123. package/packages/server/dist/http/routes/chat-completions-route.js +49 -0
  124. package/packages/server/dist/http/routes/health-routes.d.ts +6 -0
  125. package/packages/server/dist/http/routes/health-routes.js +7 -0
  126. package/packages/server/dist/http/routes/message-routes.d.ts +7 -0
  127. package/packages/server/dist/http/routes/message-routes.js +7 -0
  128. package/packages/server/dist/index.d.ts +85 -0
  129. package/packages/server/dist/index.js +28 -0
  130. package/packages/server/dist/security/bridge-auth.d.ts +9 -0
  131. package/packages/server/dist/security/bridge-auth.js +41 -0
  132. package/packages/server/dist/security/cors-policy.d.ts +5 -0
  133. package/packages/server/dist/security/cors-policy.js +34 -0
  134. package/packages/server/dist/security/index.d.ts +16 -0
  135. package/packages/server/dist/security/index.js +12 -0
  136. package/packages/server/dist/security/redact-sensitive-values.d.ts +19 -0
  137. package/packages/server/dist/security/redact-sensitive-values.js +67 -0
  138. package/packages/server/dist/shared/api-schema.d.ts +133 -0
  139. package/packages/server/dist/shared/api-schema.js +1 -0
  140. package/packages/server/dist/shared/bridge-api-error.d.ts +17 -0
  141. package/packages/server/dist/shared/bridge-api-error.js +19 -0
  142. package/packages/server/dist/shared/index.d.ts +7 -0
  143. package/packages/server/dist/shared/index.js +7 -0
  144. package/packages/server/dist/shared/output.d.ts +5 -0
  145. 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.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { bridgeCli } from "@uncensoredcode/openbridge/cli";
3
+
4
+ const { runBridgeCli } = bridgeCli;
5
+ const exitCode = await runBridgeCli({
6
+ argv: process.argv.slice(2)
7
+ });
8
+ if (exitCode !== 0) {
9
+ process.exitCode = exitCode;
10
+ }
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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { bridgeCli } from "./index.js";
3
+ const { runBridgeCli } = bridgeCli;
4
+ const exitCode = await runBridgeCli({
5
+ argv: process.argv.slice(2)
6
+ });
7
+ if (exitCode !== 0) {
8
+ process.exitCode = exitCode;
9
+ }
@@ -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
+ };