@webstew/bridge 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 +102 -0
- package/bin/webstew-bridge +2 -0
- package/dist/auth.d.ts +25 -0
- package/dist/auth.js +142 -0
- package/dist/claude-runner.d.ts +10 -0
- package/dist/claude-runner.js +532 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +162 -0
- package/dist/http.d.ts +14 -0
- package/dist/http.js +56 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +20 -0
- package/dist/protocol.d.ts +151 -0
- package/dist/protocol.js +50 -0
- package/dist/runtime.d.ts +7 -0
- package/dist/runtime.js +113 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @webstew/bridge
|
|
2
|
+
|
|
3
|
+
Run [Webstew](https://webstew.net) workspace chats against your **local Claude Code Pro/Max subscription** instead of paying separate Anthropic API rates.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npx @webstew/bridge connect <code>
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Get the `<code>` from Webstew → **/integrations → Connect Local Bridge**.
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
Webstew's AI builder (`/workspace`) talks to Claude. By default, that goes through the Console API and bills against your API credits. If you already pay for **Claude Pro** or **Claude Max**, your subscription already covers Claude usage on your own machine via Claude Code — this bridge proxies your Webstew chats through that same path so your subscription handles the bill.
|
|
14
|
+
|
|
15
|
+
## How it works
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
[your browser] ──► Webstew server ──► (you have a bridge?) ──► your local CLI
|
|
19
|
+
│
|
|
20
|
+
└─► Claude (your sub)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
1. You click **Connect Local Bridge** in Webstew settings — get a pairing code.
|
|
24
|
+
2. You paste the code into `webstew-bridge connect <code>` in your terminal.
|
|
25
|
+
3. The bridge process stays running. It long-polls Webstew for any agent requests originating from your account.
|
|
26
|
+
4. Each request runs through Claude Agent SDK on your machine, using your Pro/Max subscription. Output streams back to your browser chat.
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- Node.js ≥ 18
|
|
31
|
+
- An active Webstew account
|
|
32
|
+
- An active **Claude Pro** or **Claude Max** subscription, authenticated locally (e.g. via Claude Code or the Anthropic CLI). The bridge uses whatever OAuth credentials Claude Code uses.
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
| Command | What it does |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `webstew-bridge connect <code>` | Pair with a workspace and start the bridge |
|
|
39
|
+
| `webstew-bridge status` | Print connection state |
|
|
40
|
+
| `webstew-bridge logout` | Forget saved pairing token |
|
|
41
|
+
| `webstew-bridge --version` | Print version + protocol version |
|
|
42
|
+
|
|
43
|
+
## Where your auth lives
|
|
44
|
+
|
|
45
|
+
The bridge stores its pairing token at `~/.webstew/bridge.json` with `0600` permissions on POSIX (owner-only). The Claude subscription token itself is owned by Claude Code; the bridge invokes Claude Code rather than re-implementing auth.
|
|
46
|
+
|
|
47
|
+
## Protocol
|
|
48
|
+
|
|
49
|
+
See [`src/protocol.ts`](./src/protocol.ts) for the wire contract. Versioned via `PROTOCOL_VERSION`; the server rejects bridges on an incompatible major version with an explicit upgrade hint.
|
|
50
|
+
|
|
51
|
+
## Not for
|
|
52
|
+
|
|
53
|
+
- **Production deployments** — the bridge is a developer tool. If you publish a site via Webstew, the deploy pipeline runs on Webstew's infra and doesn't touch your bridge.
|
|
54
|
+
- **Multi-user teams** — each user runs their own bridge. There's no concept of a "team bridge" (yet).
|
|
55
|
+
|
|
56
|
+
## Live E2E test (developer only)
|
|
57
|
+
|
|
58
|
+
To validate the full flow against a local Webstew dev server:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Terminal 1 — Webstew dev server
|
|
62
|
+
cd /Users/homepc/ai-website-builder/apps/web
|
|
63
|
+
env -u MONGODB_URI npx next dev -p 3000 -H 0.0.0.0
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
# Browser
|
|
68
|
+
http://localhost:3000/integrations
|
|
69
|
+
# → Click "Connect Local Bridge" → copy the pairing code
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Terminal 2 — bridge process pointing at local server
|
|
74
|
+
cd /Users/homepc/ai-website-builder/packages/bridge
|
|
75
|
+
WEBSTEW_SERVER_URL=http://localhost:3000 npx tsx src/cli.ts connect <code>
|
|
76
|
+
# expect: "paired ✓ bridgeId=brg_…", then "bridge online — waiting for work"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
# Browser /integrations should auto-flip to "Connected ✓"
|
|
81
|
+
# Browser /workspace → send any chat
|
|
82
|
+
# Terminal 2 should log "▶ request …" → "✓ request … N.Ns"
|
|
83
|
+
# Browser chat should show streaming text + file updates
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Troubleshooting
|
|
87
|
+
|
|
88
|
+
- **`error: pairing failed (HTTP 400): Pairing code is invalid or expired`**
|
|
89
|
+
Pairing codes are single-use and expire in 10 minutes. Generate a fresh one in `/integrations`.
|
|
90
|
+
|
|
91
|
+
- **`error: pairing failed (HTTP 426)`**
|
|
92
|
+
Your bridge version is on an incompatible protocol. `npm i -g @webstew/bridge@latest`.
|
|
93
|
+
|
|
94
|
+
- **`claude exited with code 127` (in bridge logs)**
|
|
95
|
+
The bridge couldn't find the `claude` binary on `PATH`. Install Claude Code first ([claude.com/code](https://claude.com/code)), then restart the bridge.
|
|
96
|
+
|
|
97
|
+
- **`Local bridge is offline. Start it with webstew-bridge connect …` (in /workspace chat)**
|
|
98
|
+
Bridge process isn't running, or hasn't checked in for 60s. Restart it in the terminal.
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface BridgeAuth {
|
|
2
|
+
serverUrl: string;
|
|
3
|
+
bridgeId: string;
|
|
4
|
+
pairingToken: string;
|
|
5
|
+
pairedAt: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function loadAuth(): BridgeAuth | null;
|
|
8
|
+
export declare function saveAuth(a: BridgeAuth): void;
|
|
9
|
+
export declare function clearAuth(): boolean;
|
|
10
|
+
export declare function workspaceDirFor(projectId: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Ensure ~/.webstew/mcp.json points at the @webstew/agent-tools MCP
|
|
13
|
+
* server so claude loads webstew_* tools (CMS, integrations, etc.).
|
|
14
|
+
* Returns the absolute config path that should be passed to claude as
|
|
15
|
+
* `--mcp-config <path>`.
|
|
16
|
+
*
|
|
17
|
+
* Behavior:
|
|
18
|
+
* • If the config already exists, leave it alone (user may have
|
|
19
|
+
* customized it).
|
|
20
|
+
* • Otherwise, write a default config. In monorepo dev, point at the
|
|
21
|
+
* local agent-tools tsx so we don't need a build/publish step.
|
|
22
|
+
* In a production install (no local monorepo), point at npx so it
|
|
23
|
+
* resolves the published package.
|
|
24
|
+
*/
|
|
25
|
+
export declare function ensureMcpConfig(): string;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Local config — pairing token, bridgeId, serverUrl. Stored at
|
|
3
|
+
// ~/.webstew/bridge.json with 0600 perms (owner-only read/write) on
|
|
4
|
+
// POSIX so other local users can't read the token. Same model as
|
|
5
|
+
// ~/.gitconfig + ~/.npmrc style configs.
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.loadAuth = loadAuth;
|
|
11
|
+
exports.saveAuth = saveAuth;
|
|
12
|
+
exports.clearAuth = clearAuth;
|
|
13
|
+
exports.workspaceDirFor = workspaceDirFor;
|
|
14
|
+
exports.ensureMcpConfig = ensureMcpConfig;
|
|
15
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
18
|
+
function configDir() {
|
|
19
|
+
return node_path_1.default.join(node_os_1.default.homedir(), '.webstew');
|
|
20
|
+
}
|
|
21
|
+
function configPath() {
|
|
22
|
+
return node_path_1.default.join(configDir(), 'bridge.json');
|
|
23
|
+
}
|
|
24
|
+
function loadAuth() {
|
|
25
|
+
try {
|
|
26
|
+
const raw = node_fs_1.default.readFileSync(configPath(), 'utf8');
|
|
27
|
+
const parsed = JSON.parse(raw);
|
|
28
|
+
if (!parsed.serverUrl || !parsed.bridgeId || !parsed.pairingToken)
|
|
29
|
+
return null;
|
|
30
|
+
return {
|
|
31
|
+
serverUrl: parsed.serverUrl,
|
|
32
|
+
bridgeId: parsed.bridgeId,
|
|
33
|
+
pairingToken: parsed.pairingToken,
|
|
34
|
+
pairedAt: parsed.pairedAt || new Date().toISOString(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function saveAuth(a) {
|
|
42
|
+
const dir = configDir();
|
|
43
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
44
|
+
const tmp = configPath() + '.tmp';
|
|
45
|
+
node_fs_1.default.writeFileSync(tmp, JSON.stringify(a, null, 2), { mode: 0o600 });
|
|
46
|
+
node_fs_1.default.renameSync(tmp, configPath());
|
|
47
|
+
try {
|
|
48
|
+
// Belt + suspenders on POSIX — Windows ignores mode but the user
|
|
49
|
+
// gets owner-only by default anyway.
|
|
50
|
+
node_fs_1.default.chmodSync(configPath(), 0o600);
|
|
51
|
+
}
|
|
52
|
+
catch { }
|
|
53
|
+
}
|
|
54
|
+
function clearAuth() {
|
|
55
|
+
try {
|
|
56
|
+
node_fs_1.default.unlinkSync(configPath());
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Per-project workspace dirs. Each Webstew projectId gets its own dir
|
|
64
|
+
// under ~/.webstew/workspaces/<projectId>/ so Claude Code's CLAUDE.md +
|
|
65
|
+
// auto-memory namespace cleanly to that project — same as cd-ing into
|
|
66
|
+
// different repos in regular Claude Code usage.
|
|
67
|
+
function workspaceDirFor(projectId) {
|
|
68
|
+
const safe = projectId.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64) || 'unscoped';
|
|
69
|
+
const dir = node_path_1.default.join(configDir(), 'workspaces', safe);
|
|
70
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
71
|
+
// Opportunistically sweep workspaces older than 30 days. Cheap O(n)
|
|
72
|
+
// scan on entry; the workspace dir is tiny so this never blocks.
|
|
73
|
+
sweepStaleWorkspaces();
|
|
74
|
+
return dir;
|
|
75
|
+
}
|
|
76
|
+
function sweepStaleWorkspaces() {
|
|
77
|
+
const root = node_path_1.default.join(configDir(), 'workspaces');
|
|
78
|
+
if (!node_fs_1.default.existsSync(root))
|
|
79
|
+
return;
|
|
80
|
+
const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000;
|
|
81
|
+
const cutoff = Date.now() - THIRTY_DAYS;
|
|
82
|
+
try {
|
|
83
|
+
for (const entry of node_fs_1.default.readdirSync(root, { withFileTypes: true })) {
|
|
84
|
+
if (!entry.isDirectory())
|
|
85
|
+
continue;
|
|
86
|
+
const full = node_path_1.default.join(root, entry.name);
|
|
87
|
+
try {
|
|
88
|
+
const { mtimeMs } = node_fs_1.default.statSync(full);
|
|
89
|
+
if (mtimeMs < cutoff)
|
|
90
|
+
node_fs_1.default.rmSync(full, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
catch { /* non-fatal — skip this dir */ }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch { /* non-fatal */ }
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Ensure ~/.webstew/mcp.json points at the @webstew/agent-tools MCP
|
|
99
|
+
* server so claude loads webstew_* tools (CMS, integrations, etc.).
|
|
100
|
+
* Returns the absolute config path that should be passed to claude as
|
|
101
|
+
* `--mcp-config <path>`.
|
|
102
|
+
*
|
|
103
|
+
* Behavior:
|
|
104
|
+
* • If the config already exists, leave it alone (user may have
|
|
105
|
+
* customized it).
|
|
106
|
+
* • Otherwise, write a default config. In monorepo dev, point at the
|
|
107
|
+
* local agent-tools tsx so we don't need a build/publish step.
|
|
108
|
+
* In a production install (no local monorepo), point at npx so it
|
|
109
|
+
* resolves the published package.
|
|
110
|
+
*/
|
|
111
|
+
function ensureMcpConfig() {
|
|
112
|
+
const dir = configDir();
|
|
113
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
114
|
+
const cfgPath = node_path_1.default.join(dir, 'mcp.json');
|
|
115
|
+
if (node_fs_1.default.existsSync(cfgPath))
|
|
116
|
+
return cfgPath;
|
|
117
|
+
// Detect monorepo dev. __dirname of this module sits at
|
|
118
|
+
// packages/bridge/src/ during dev; sibling packages/agent-tools/src/index.ts
|
|
119
|
+
// tells us we're in the monorepo and can run tsx directly.
|
|
120
|
+
const dirname = __dirname;
|
|
121
|
+
const siblingTs = node_path_1.default.resolve(dirname, '..', '..', 'agent-tools', 'src', 'index.ts');
|
|
122
|
+
const isMonorepo = node_fs_1.default.existsSync(siblingTs);
|
|
123
|
+
const config = isMonorepo
|
|
124
|
+
? {
|
|
125
|
+
mcpServers: {
|
|
126
|
+
webstew: {
|
|
127
|
+
command: 'npx',
|
|
128
|
+
args: ['--yes', 'tsx', siblingTs],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
: {
|
|
133
|
+
mcpServers: {
|
|
134
|
+
webstew: {
|
|
135
|
+
command: 'npx',
|
|
136
|
+
args: ['--yes', '@webstew/agent-tools'],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
node_fs_1.default.writeFileSync(cfgPath, JSON.stringify(config, null, 2));
|
|
141
|
+
return cfgPath;
|
|
142
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AgentRunRequest, BridgeResponse } from './protocol';
|
|
2
|
+
interface RunOpts {
|
|
3
|
+
request: AgentRunRequest;
|
|
4
|
+
requestId: string;
|
|
5
|
+
onEvent: (chunk: BridgeResponse) => Promise<void>;
|
|
6
|
+
claudeBin?: string;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
}
|
|
9
|
+
export declare function runClaudeOnce(opts: RunOpts): Promise<void>;
|
|
10
|
+
export {};
|