@shaidle/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 +56 -0
- package/dist/cli.js +267 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @shaidle/bridge
|
|
2
|
+
|
|
3
|
+
Point the **Claude Code you already use** at the [Shaidle](https://shaidle.io) exchange and run
|
|
4
|
+
coding tasks from your terminal — billed to your Shaidle wallet, served from pooled provider
|
|
5
|
+
capacity. You never connect a Claude account; the only credential is a Shaidle API token.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npm i -g @shaidle/bridge
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Log in
|
|
14
|
+
|
|
15
|
+
Mint a token in the web app under **Connect editor**, then:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
shaidle login --token sk-shaidle-… --endpoint https://api.shaidle.io
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Use
|
|
22
|
+
|
|
23
|
+
**Open your own Claude Code on Shaidle — reverts when you close the terminal.** This wires the
|
|
24
|
+
endpoint + token into that session only; nothing is written to your global `~/.claude` config, so
|
|
25
|
+
your normal Claude setup is untouched the moment you quit.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
shaidle code
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Run a one-off task** on the exchange without opening an editor — the result streams back, your
|
|
32
|
+
code stays on your machine:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
shaidle run "add a health endpoint to server.ts"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Always-on (advanced):** print the two env vars to route any Anthropic-compatible tool through
|
|
39
|
+
Shaidle until you unset them.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
shaidle env
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Commands
|
|
46
|
+
|
|
47
|
+
| Command | What it does |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `shaidle login --token <sk-shaidle-…> [--endpoint <url>]` | Save your credentials |
|
|
50
|
+
| `shaidle whoami` | Show the account the token belongs to |
|
|
51
|
+
| `shaidle code [claude args…]` | Open your Claude Code wired to Shaidle for this session only |
|
|
52
|
+
| `shaidle run "<prompt>" [--provider auto\|claude\|codex\|gemini\|cursor]` | Run a task, stream the result |
|
|
53
|
+
| `shaidle env` | Print env exports that point Claude Code at the exchange |
|
|
54
|
+
| `shaidle logout` | Forget the saved token |
|
|
55
|
+
|
|
56
|
+
Your code and prompts stay on your machine — only the request and its result cross the wire.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Shaidle Local Bridge CLI.
|
|
3
|
+
//
|
|
4
|
+
// The simplest possible "sell/buy AI capacity" client: a buyer installs one npm package,
|
|
5
|
+
// pastes the API token they minted in the web app, and runs coding tasks on the exchange
|
|
6
|
+
// from their own terminal. Their machine, their prompt — only the token and the result cross
|
|
7
|
+
// the wire. Built entirely on the public HTTP surface (bearer /api/jobs + SSE), no secrets
|
|
8
|
+
// and no provider SDK on the buyer's side.
|
|
9
|
+
//
|
|
10
|
+
// npm i -g @shaidle/bridge (at launch; pre-launch: build from the repo)
|
|
11
|
+
// shaidle login --token sk-shaidle-… [--endpoint https://api.shaidle.io]
|
|
12
|
+
//
|
|
13
|
+
// Two ways to spend the credit after login:
|
|
14
|
+
// shaidle code ← open your OWN Claude Code, wired to the
|
|
15
|
+
// exchange for THIS session only (reverts on exit)
|
|
16
|
+
// shaidle run "add a health endpoint to server.ts" ← run a one-off task on the exchange
|
|
17
|
+
//
|
|
18
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from "node:fs";
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
// This CLI is deliberately DEPENDENCY-FREE — Node built-ins only, no workspace packages. That's
|
|
23
|
+
// what lets us ship it as a compiled artifact (publish `dist/*.js` with the source held back, or
|
|
24
|
+
// wrap it into a single `shaidle` executable) without dragging the private monorepo along. The few
|
|
25
|
+
// wire facts below mirror @shaidle/contract; they're stable public routes, kept local on purpose.
|
|
26
|
+
const DEFAULT_API_BASE = "http://localhost:8787";
|
|
27
|
+
const API = {
|
|
28
|
+
me: "/api/me",
|
|
29
|
+
jobs: "/api/jobs",
|
|
30
|
+
jobStream: (id) => `/api/jobs/${id}/stream`,
|
|
31
|
+
};
|
|
32
|
+
const CONFIG_DIR = join(homedir(), ".shaidle");
|
|
33
|
+
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
34
|
+
function readConfig() {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function writeConfig(cfg) {
|
|
43
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
44
|
+
// Owner-only perms where the platform honors them (POSIX). The token is a bearer secret.
|
|
45
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
46
|
+
}
|
|
47
|
+
/** Pull a `--flag value` pair out of argv, returning the value (or undefined). */
|
|
48
|
+
function takeFlag(args, name) {
|
|
49
|
+
const i = args.indexOf(name);
|
|
50
|
+
if (i === -1)
|
|
51
|
+
return undefined;
|
|
52
|
+
const val = args[i + 1];
|
|
53
|
+
args.splice(i, val !== undefined && !val.startsWith("--") ? 2 : 1);
|
|
54
|
+
return val;
|
|
55
|
+
}
|
|
56
|
+
function requireConfig() {
|
|
57
|
+
const cfg = readConfig();
|
|
58
|
+
if (!cfg?.token) {
|
|
59
|
+
console.error("Not logged in. Run: shaidle login --token sk-shaidle-…");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
return cfg;
|
|
63
|
+
}
|
|
64
|
+
async function api(cfg, path, init = {}) {
|
|
65
|
+
return fetch(cfg.endpoint + path, {
|
|
66
|
+
method: init.method ?? "GET",
|
|
67
|
+
headers: {
|
|
68
|
+
authorization: `Bearer ${cfg.token}`,
|
|
69
|
+
accept: init.stream ? "text/event-stream" : "application/json",
|
|
70
|
+
...(init.body !== undefined ? { "content-type": "application/json" } : {}),
|
|
71
|
+
},
|
|
72
|
+
body: init.body === undefined ? undefined : JSON.stringify(init.body),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const USAGE = `Shaidle Local Bridge — run AI coding tasks on the exchange from your terminal.
|
|
76
|
+
|
|
77
|
+
Usage:
|
|
78
|
+
shaidle login --token <sk-shaidle-…> [--endpoint <url>] Save your credentials
|
|
79
|
+
shaidle whoami Show the account the token belongs to
|
|
80
|
+
shaidle code [claude args…] Open YOUR Claude Code wired to the exchange
|
|
81
|
+
for this session only — reverts on exit
|
|
82
|
+
shaidle run "<prompt>" [--provider auto|claude|codex|gemini|cursor] Run a task, stream the result
|
|
83
|
+
shaidle env Print env exports that point Claude Code
|
|
84
|
+
(or any Anthropic tool) at the exchange
|
|
85
|
+
shaidle logout Forget the saved token
|
|
86
|
+
shaidle help Show this help
|
|
87
|
+
|
|
88
|
+
Provider defaults to "auto": the exchange picks the best available account and fails over
|
|
89
|
+
between sellers (even cross-provider) if one runs out of quota mid-way.
|
|
90
|
+
Your token is minted in the web app under "Connect editor". Endpoint defaults to ${DEFAULT_API_BASE}.`;
|
|
91
|
+
async function cmdLogin(args) {
|
|
92
|
+
const token = takeFlag(args, "--token");
|
|
93
|
+
const endpoint = (takeFlag(args, "--endpoint") ?? DEFAULT_API_BASE).replace(/\/$/, "");
|
|
94
|
+
if (!token || !token.startsWith("sk-shaidle-")) {
|
|
95
|
+
console.error("Provide a valid token: shaidle login --token sk-shaidle-…");
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
writeConfig({ endpoint, token });
|
|
99
|
+
// Verify the token actually resolves before declaring success.
|
|
100
|
+
const res = await api({ endpoint, token }, API.me).catch(() => null);
|
|
101
|
+
if (!res || res.status !== 200) {
|
|
102
|
+
console.error(`Saved, but the token did not authenticate against ${endpoint} (HTTP ${res?.status ?? "offline"}).`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
const me = (await res.json());
|
|
106
|
+
console.log(`Logged in to ${endpoint} as ${me.email ?? "your account"}.`);
|
|
107
|
+
}
|
|
108
|
+
async function cmdWhoami() {
|
|
109
|
+
const cfg = requireConfig();
|
|
110
|
+
const res = await api(cfg, API.me);
|
|
111
|
+
if (res.status !== 200) {
|
|
112
|
+
console.error(`Not authenticated (HTTP ${res.status}). Re-run shaidle login.`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
const me = (await res.json());
|
|
116
|
+
console.log(`${me.email ?? "(unknown)"} · ${cfg.endpoint}`);
|
|
117
|
+
}
|
|
118
|
+
async function cmdRun(args) {
|
|
119
|
+
const cfg = requireConfig();
|
|
120
|
+
// "auto" is the default — the exchange matches any available account and fails over.
|
|
121
|
+
const provider = takeFlag(args, "--provider") ?? "auto";
|
|
122
|
+
const prompt = args.filter((a) => !a.startsWith("--")).join(" ").trim();
|
|
123
|
+
if (!prompt) {
|
|
124
|
+
console.error('Give a prompt: shaidle run "fix the failing test in utils.ts"');
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
const created = await api(cfg, API.jobs, { method: "POST", body: { provider, prompt } });
|
|
128
|
+
if (created.status === 401) {
|
|
129
|
+
console.error("Token rejected (401). Re-run shaidle login.");
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
if (created.status === 402) {
|
|
133
|
+
console.error("Insufficient Shaidle credit (402). Top up your wallet in the web app, then retry.");
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
if (created.status !== 201) {
|
|
137
|
+
console.error(`Could not start the job (HTTP ${created.status}): ${(await created.text()).slice(0, 200)}`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
const { jobId } = (await created.json());
|
|
141
|
+
// Stream the run — the endpoint is owner-guarded, so the bearer token rides along.
|
|
142
|
+
const stream = await api(cfg, API.jobStream(jobId), { stream: true });
|
|
143
|
+
if (!stream.body) {
|
|
144
|
+
console.error("No stream from the server.");
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
const reader = stream.body.getReader();
|
|
148
|
+
const dec = new TextDecoder();
|
|
149
|
+
let buf = "";
|
|
150
|
+
let exitCode = 0;
|
|
151
|
+
for (;;) {
|
|
152
|
+
const { value, done } = await reader.read();
|
|
153
|
+
if (done)
|
|
154
|
+
break;
|
|
155
|
+
buf += dec.decode(value, { stream: true });
|
|
156
|
+
const events = buf.split("\n\n");
|
|
157
|
+
buf = events.pop() ?? "";
|
|
158
|
+
for (const ev of events) {
|
|
159
|
+
const line = ev.split("\n").find((l) => l.startsWith("data:"));
|
|
160
|
+
if (!line)
|
|
161
|
+
continue;
|
|
162
|
+
let d;
|
|
163
|
+
try {
|
|
164
|
+
d = JSON.parse(line.slice(5).trim());
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (d.type === "chunk")
|
|
170
|
+
process.stdout.write(String(d.text ?? ""));
|
|
171
|
+
else if (d.type === "started")
|
|
172
|
+
process.stderr.write(`· running via ${String(d.provider)} on node ${String(d.nodeId)}\n`);
|
|
173
|
+
else if (d.type === "reissue")
|
|
174
|
+
// The serving account died (quota/auth); the exchange restarted the run elsewhere.
|
|
175
|
+
// Output printed so far belongs to the abandoned attempt — the fresh answer follows.
|
|
176
|
+
process.stderr.write(`\n⇄ account switch: ${String(d.fromNode)} → ${String(d.toNode)} (${String(d.provider)}) — restarting stream\n`);
|
|
177
|
+
else if (d.type === "usage")
|
|
178
|
+
process.stderr.write(`· charged ${String(d.buyerChargeMinor)} minor (${String(d.currency)})\n`);
|
|
179
|
+
else if (d.type === "done")
|
|
180
|
+
process.stdout.write("\n");
|
|
181
|
+
else if (d.type === "error") {
|
|
182
|
+
process.stderr.write(`\n✗ ${String(d.message)}\n`);
|
|
183
|
+
exitCode = 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
process.exit(exitCode);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Launch the buyer's OWN Claude Code (or any command via --bin) with the exchange wired in for
|
|
191
|
+
* THIS child process only: ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN live on the child's env and
|
|
192
|
+
* vanish when it exits. Nothing is written to ~/.claude/settings.json or the shell profile, so the
|
|
193
|
+
* buyer's normal Claude setup is byte-for-byte untouched the moment the session ends. This is the
|
|
194
|
+
* ephemeral counterpart to `shaidle env` (which prints exports you'd paste — and later have to undo).
|
|
195
|
+
*/
|
|
196
|
+
function cmdCode(args) {
|
|
197
|
+
const cfg = requireConfig();
|
|
198
|
+
const bin = takeFlag(args, "--bin") ?? process.env.SHAIDLE_CLAUDE_BIN ?? "claude";
|
|
199
|
+
const env = { ...process.env };
|
|
200
|
+
// A stale global ANTHROPIC_API_KEY would shadow the gateway token — drop it for the child only.
|
|
201
|
+
delete env.ANTHROPIC_API_KEY;
|
|
202
|
+
env.ANTHROPIC_BASE_URL = cfg.endpoint;
|
|
203
|
+
env.ANTHROPIC_AUTH_TOKEN = cfg.token;
|
|
204
|
+
process.stderr.write(`· ${bin} → ${cfg.endpoint} — this session only, your normal Claude setup reverts on exit\n`);
|
|
205
|
+
const child = spawn(bin, args, {
|
|
206
|
+
stdio: "inherit",
|
|
207
|
+
env,
|
|
208
|
+
shell: process.platform === "win32", // resolve claude.cmd / claude.ps1 off the Windows PATH
|
|
209
|
+
});
|
|
210
|
+
child.on("error", (e) => {
|
|
211
|
+
const hint = e.code === "ENOENT" ? ` — is "${bin}" installed and on your PATH?` : "";
|
|
212
|
+
console.error(`Could not launch ${bin}${hint} (${e.message})`);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
});
|
|
215
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
216
|
+
}
|
|
217
|
+
function cmdLogout() {
|
|
218
|
+
if (existsSync(CONFIG_PATH))
|
|
219
|
+
rmSync(CONFIG_PATH);
|
|
220
|
+
console.log("Logged out.");
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Print the two env vars that point an unmodified Claude Code (or any Anthropic-SDK tool)
|
|
224
|
+
* straight at the exchange's /v1/messages passthrough — the zero-install Local mode.
|
|
225
|
+
*/
|
|
226
|
+
function cmdEnv() {
|
|
227
|
+
const cfg = requireConfig();
|
|
228
|
+
console.log(`# POSIX (bash/zsh) — eval "$(shaidle env)"`);
|
|
229
|
+
console.log(`export ANTHROPIC_BASE_URL=${cfg.endpoint}`);
|
|
230
|
+
console.log(`export ANTHROPIC_AUTH_TOKEN=${cfg.token}`);
|
|
231
|
+
console.log(`# PowerShell:`);
|
|
232
|
+
console.log(`# $env:ANTHROPIC_BASE_URL = "${cfg.endpoint}"`);
|
|
233
|
+
console.log(`# $env:ANTHROPIC_AUTH_TOKEN = "${cfg.token}"`);
|
|
234
|
+
console.log(`# Then run \`claude\` in your project as usual — requests are served from`);
|
|
235
|
+
console.log(`# pooled seller accounts with automatic failover, billed to your wallet.`);
|
|
236
|
+
}
|
|
237
|
+
async function main() {
|
|
238
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
239
|
+
switch (cmd) {
|
|
240
|
+
case "login":
|
|
241
|
+
return cmdLogin(rest);
|
|
242
|
+
case "whoami":
|
|
243
|
+
return cmdWhoami();
|
|
244
|
+
case "code":
|
|
245
|
+
return cmdCode(rest);
|
|
246
|
+
case "run":
|
|
247
|
+
return cmdRun(rest);
|
|
248
|
+
case "env":
|
|
249
|
+
return cmdEnv();
|
|
250
|
+
case "logout":
|
|
251
|
+
return cmdLogout();
|
|
252
|
+
case undefined:
|
|
253
|
+
case "help":
|
|
254
|
+
case "--help":
|
|
255
|
+
case "-h":
|
|
256
|
+
console.log(USAGE);
|
|
257
|
+
return;
|
|
258
|
+
default:
|
|
259
|
+
console.error(`Unknown command: ${cmd}\n\n${USAGE}`);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
main().catch((e) => {
|
|
264
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
265
|
+
process.exit(1);
|
|
266
|
+
});
|
|
267
|
+
//# sourceMappingURL=cli.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shaidle/bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Shaidle Local Bridge CLI — point your own Claude Code at the Shaidle exchange (per-session or always-on) and run coding tasks from your terminal, authenticated by a Shaidle API token.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"shaidle",
|
|
8
|
+
"claude",
|
|
9
|
+
"claude-code",
|
|
10
|
+
"cli",
|
|
11
|
+
"ai"
|
|
12
|
+
],
|
|
13
|
+
"license": "UNLICENSED",
|
|
14
|
+
"bin": {
|
|
15
|
+
"shaidle": "./dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"main": "./dist/cli.js",
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist/cli.js"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -p tsconfig.json",
|
|
29
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22.0.0",
|
|
34
|
+
"typescript": "^5.7.0"
|
|
35
|
+
}
|
|
36
|
+
}
|