@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 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env sh
2
+ exec node "$(dirname "$0")/../dist/cli.js" "$@"
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 {};