@bamboo-ai/core 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/dist/auth.d.ts +47 -0
- package/dist/auth.js +341 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +31 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +20 -0
- package/dist/process.d.ts +11 -0
- package/dist/process.js +38 -0
- package/dist/resolve-bin.d.ts +14 -0
- package/dist/resolve-bin.js +150 -0
- package/package.json +38 -0
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export declare class AuthService {
|
|
2
|
+
/** new-api URL — used for LLM API calls via /v1 */
|
|
3
|
+
private apiBaseUrl;
|
|
4
|
+
/** Libra URL — used for login and API key generation */
|
|
5
|
+
private authUrl;
|
|
6
|
+
/**
|
|
7
|
+
* @param apiBaseUrl new-api base URL (e.g. http://localhost:3010)
|
|
8
|
+
* @param authUrl Libra base URL (e.g. http://localhost:2999)
|
|
9
|
+
* Defaults to apiBaseUrl when not specified.
|
|
10
|
+
*/
|
|
11
|
+
constructor(apiBaseUrl?: string, authUrl?: string);
|
|
12
|
+
/**
|
|
13
|
+
* Primary: Browser-based login via Libra → returns sk-xxx API key.
|
|
14
|
+
* Fallback: Terminal email/password login via Libra CLI API.
|
|
15
|
+
*
|
|
16
|
+
* Both paths end with a persistent API key stored in ~/.bamboo/credentials.
|
|
17
|
+
*/
|
|
18
|
+
login(): Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* 1. Start a local HTTP server on a random port
|
|
21
|
+
* 2. Open the browser to Libra's /api/auth/cli/authorize
|
|
22
|
+
* 3. Libra authenticates the user and creates/finds an sk-xxx API key
|
|
23
|
+
* 4. Libra redirects to our callback with ?api_key=sk-xxx
|
|
24
|
+
* 5. Save the API key to disk
|
|
25
|
+
*/
|
|
26
|
+
private browserLogin;
|
|
27
|
+
/**
|
|
28
|
+
* Prompt email + password in the terminal, then call Libra's
|
|
29
|
+
* /api/auth/cli/login endpoint which returns an sk-xxx API key.
|
|
30
|
+
*/
|
|
31
|
+
private terminalLogin;
|
|
32
|
+
private askPassword;
|
|
33
|
+
/**
|
|
34
|
+
* Read the stored API key (sk-xxx) from disk.
|
|
35
|
+
*
|
|
36
|
+
* Supports three formats for backward compatibility:
|
|
37
|
+
* 1. JSON { "api_key": "sk-xxx" } ← current
|
|
38
|
+
* 2. JSON { "access_token": "..." } ← old JWT format (ignored)
|
|
39
|
+
* 3. Plain text "sk-xxx" ← legacy
|
|
40
|
+
*/
|
|
41
|
+
getToken(): Promise<string | null>;
|
|
42
|
+
private saveApiKey;
|
|
43
|
+
/**
|
|
44
|
+
* Clear stored credentials (logout).
|
|
45
|
+
*/
|
|
46
|
+
logout(): Promise<void>;
|
|
47
|
+
}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AuthService = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const http_1 = __importDefault(require("http"));
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const readline_1 = __importDefault(require("readline"));
|
|
13
|
+
const axios_1 = __importDefault(require("axios"));
|
|
14
|
+
const CREDENTIALS_DIR = path_1.default.join(os_1.default.homedir(), '.bamboo');
|
|
15
|
+
const CREDENTIALS_FILE = path_1.default.join(CREDENTIALS_DIR, 'credentials');
|
|
16
|
+
// ─── HTML Templates ──────────────────────────────────────────────────
|
|
17
|
+
const SUCCESS_HTML = `<!DOCTYPE html>
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<head>
|
|
20
|
+
<meta charset="utf-8">
|
|
21
|
+
<title>Bamboo CLI — Login Successful</title>
|
|
22
|
+
<style>
|
|
23
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
24
|
+
body {
|
|
25
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
26
|
+
display: flex; justify-content: center; align-items: center;
|
|
27
|
+
height: 100vh;
|
|
28
|
+
background: linear-gradient(135deg, #f0fdf4 0%, #ecfdf5 50%, #d1fae5 100%);
|
|
29
|
+
}
|
|
30
|
+
.card {
|
|
31
|
+
text-align: center; padding: 3rem 2.5rem;
|
|
32
|
+
background: white; border-radius: 1rem;
|
|
33
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.06);
|
|
34
|
+
max-width: 420px; width: 90%;
|
|
35
|
+
}
|
|
36
|
+
.icon { font-size: 3rem; margin-bottom: 1rem; }
|
|
37
|
+
h1 { color: #16a34a; font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
38
|
+
p { color: #6b7280; line-height: 1.6; }
|
|
39
|
+
</style>
|
|
40
|
+
</head>
|
|
41
|
+
<body>
|
|
42
|
+
<div class="card">
|
|
43
|
+
<div class="icon">🎋</div>
|
|
44
|
+
<h1>Login Successful!</h1>
|
|
45
|
+
<p>You can close this tab and return to your terminal.</p>
|
|
46
|
+
</div>
|
|
47
|
+
</body>
|
|
48
|
+
</html>`;
|
|
49
|
+
function errorHtml(message) {
|
|
50
|
+
return `<!DOCTYPE html>
|
|
51
|
+
<html lang="en">
|
|
52
|
+
<head>
|
|
53
|
+
<meta charset="utf-8">
|
|
54
|
+
<title>Bamboo CLI — Login Failed</title>
|
|
55
|
+
<style>
|
|
56
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
57
|
+
body {
|
|
58
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
59
|
+
display: flex; justify-content: center; align-items: center;
|
|
60
|
+
height: 100vh;
|
|
61
|
+
background: linear-gradient(135deg, #fef2f2 0%, #fecaca 100%);
|
|
62
|
+
}
|
|
63
|
+
.card {
|
|
64
|
+
text-align: center; padding: 3rem 2.5rem;
|
|
65
|
+
background: white; border-radius: 1rem;
|
|
66
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.06);
|
|
67
|
+
max-width: 420px; width: 90%;
|
|
68
|
+
}
|
|
69
|
+
.icon { font-size: 3rem; margin-bottom: 1rem; }
|
|
70
|
+
h1 { color: #dc2626; font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
71
|
+
p { color: #6b7280; line-height: 1.6; }
|
|
72
|
+
</style>
|
|
73
|
+
</head>
|
|
74
|
+
<body>
|
|
75
|
+
<div class="card">
|
|
76
|
+
<div class="icon">❌</div>
|
|
77
|
+
<h1>Login Failed</h1>
|
|
78
|
+
<p>${message}</p>
|
|
79
|
+
<p style="margin-top:1rem;">Please close this tab and try again in your terminal.</p>
|
|
80
|
+
</div>
|
|
81
|
+
</body>
|
|
82
|
+
</html>`;
|
|
83
|
+
}
|
|
84
|
+
// ─── AuthService ─────────────────────────────────────────────────────
|
|
85
|
+
class AuthService {
|
|
86
|
+
/**
|
|
87
|
+
* @param apiBaseUrl new-api base URL (e.g. http://localhost:3010)
|
|
88
|
+
* @param authUrl Libra base URL (e.g. http://localhost:2999)
|
|
89
|
+
* Defaults to apiBaseUrl when not specified.
|
|
90
|
+
*/
|
|
91
|
+
constructor(apiBaseUrl = 'https://api.bamboonode.cn', authUrl) {
|
|
92
|
+
this.apiBaseUrl = apiBaseUrl;
|
|
93
|
+
this.authUrl = authUrl || apiBaseUrl;
|
|
94
|
+
}
|
|
95
|
+
// ─── Login (primary + fallback) ─────────────────────────────────
|
|
96
|
+
/**
|
|
97
|
+
* Primary: Browser-based login via Libra → returns sk-xxx API key.
|
|
98
|
+
* Fallback: Terminal email/password login via Libra CLI API.
|
|
99
|
+
*
|
|
100
|
+
* Both paths end with a persistent API key stored in ~/.bamboo/credentials.
|
|
101
|
+
*/
|
|
102
|
+
async login() {
|
|
103
|
+
try {
|
|
104
|
+
return await this.browserLogin();
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
console.error(`Bamboo: Browser login unavailable (${err.message}).`);
|
|
108
|
+
console.error('Bamboo: Falling back to terminal login...');
|
|
109
|
+
console.error('');
|
|
110
|
+
return await this.terminalLogin();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// ─── Browser Login ──────────────────────────────────────────────
|
|
114
|
+
/**
|
|
115
|
+
* 1. Start a local HTTP server on a random port
|
|
116
|
+
* 2. Open the browser to Libra's /api/auth/cli/authorize
|
|
117
|
+
* 3. Libra authenticates the user and creates/finds an sk-xxx API key
|
|
118
|
+
* 4. Libra redirects to our callback with ?api_key=sk-xxx
|
|
119
|
+
* 5. Save the API key to disk
|
|
120
|
+
*/
|
|
121
|
+
browserLogin() {
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const server = http_1.default.createServer();
|
|
124
|
+
let timeoutId;
|
|
125
|
+
server.listen(0, '127.0.0.1', () => {
|
|
126
|
+
const addr = server.address();
|
|
127
|
+
const port = addr.port;
|
|
128
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
129
|
+
const authorizeUrl = `${this.authUrl}/api/auth/cli/authorize?redirect_uri=${encodeURIComponent(redirectUri)}`;
|
|
130
|
+
console.error('');
|
|
131
|
+
console.error('══════════════════════════════════════');
|
|
132
|
+
console.error(' 🎋 Bamboo CLI Login');
|
|
133
|
+
console.error('══════════════════════════════════════');
|
|
134
|
+
console.error('');
|
|
135
|
+
console.error(' Opening browser for login...');
|
|
136
|
+
console.error(` If the browser does not open, visit:`);
|
|
137
|
+
console.error(` ${authorizeUrl}`);
|
|
138
|
+
console.error('');
|
|
139
|
+
console.error(' Waiting for authentication...');
|
|
140
|
+
openBrowser(authorizeUrl);
|
|
141
|
+
// 5-minute timeout
|
|
142
|
+
timeoutId = setTimeout(() => {
|
|
143
|
+
server.close();
|
|
144
|
+
reject(new Error('Login timed out (5 min). Please try again.'));
|
|
145
|
+
}, 5 * 60 * 1000);
|
|
146
|
+
});
|
|
147
|
+
server.on('request', async (req, res) => {
|
|
148
|
+
const reqUrl = new URL(req.url || '/', 'http://localhost');
|
|
149
|
+
if (reqUrl.pathname !== '/callback') {
|
|
150
|
+
res.writeHead(404);
|
|
151
|
+
res.end('Not Found');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const apiKey = reqUrl.searchParams.get('api_key');
|
|
155
|
+
const error = reqUrl.searchParams.get('error');
|
|
156
|
+
if (apiKey) {
|
|
157
|
+
await this.saveApiKey(apiKey);
|
|
158
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
159
|
+
res.end(SUCCESS_HTML);
|
|
160
|
+
clearTimeout(timeoutId);
|
|
161
|
+
server.close();
|
|
162
|
+
console.error('');
|
|
163
|
+
console.error(' ✅ Login successful!');
|
|
164
|
+
console.error(` API key saved to: ${CREDENTIALS_FILE}`);
|
|
165
|
+
console.error('');
|
|
166
|
+
resolve(apiKey);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const errMsg = error || 'Unknown error';
|
|
170
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
171
|
+
res.end(errorHtml(errMsg));
|
|
172
|
+
clearTimeout(timeoutId);
|
|
173
|
+
server.close();
|
|
174
|
+
reject(new Error(`Login failed: ${errMsg}`));
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
server.on('error', (err) => {
|
|
178
|
+
clearTimeout(timeoutId);
|
|
179
|
+
reject(new Error(`Failed to start callback server: ${err.message}`));
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// ─── Terminal Login (fallback) ──────────────────────────────────
|
|
184
|
+
/**
|
|
185
|
+
* Prompt email + password in the terminal, then call Libra's
|
|
186
|
+
* /api/auth/cli/login endpoint which returns an sk-xxx API key.
|
|
187
|
+
*/
|
|
188
|
+
async terminalLogin() {
|
|
189
|
+
const rl = readline_1.default.createInterface({
|
|
190
|
+
input: process.stdin,
|
|
191
|
+
output: process.stderr,
|
|
192
|
+
});
|
|
193
|
+
const ask = (question) => new Promise((resolve) => rl.question(question, resolve));
|
|
194
|
+
try {
|
|
195
|
+
console.error('══════════════════════════════════════');
|
|
196
|
+
console.error(' 🎋 Bamboo CLI Login (Terminal)');
|
|
197
|
+
console.error('══════════════════════════════════════');
|
|
198
|
+
console.error(` Server: ${this.authUrl}`);
|
|
199
|
+
console.error('');
|
|
200
|
+
const email = await ask(' Email: ');
|
|
201
|
+
const password = await this.askPassword(' Password: ', rl);
|
|
202
|
+
console.error('');
|
|
203
|
+
if (!email || !password) {
|
|
204
|
+
throw new Error('Email and password are required.');
|
|
205
|
+
}
|
|
206
|
+
console.error(' Authenticating...');
|
|
207
|
+
const resp = await axios_1.default.post(`${this.authUrl}/api/auth/cli/login`, { email, password }, { headers: { 'Content-Type': 'application/json' } });
|
|
208
|
+
if (!resp.data?.success) {
|
|
209
|
+
throw new Error(resp.data?.message || 'Login failed');
|
|
210
|
+
}
|
|
211
|
+
const apiKey = resp.data.api_key;
|
|
212
|
+
if (!apiKey) {
|
|
213
|
+
throw new Error('Server did not return a valid API key.');
|
|
214
|
+
}
|
|
215
|
+
await this.saveApiKey(apiKey);
|
|
216
|
+
console.error(' ✅ Login successful!');
|
|
217
|
+
console.error(` API key saved to: ${CREDENTIALS_FILE}`);
|
|
218
|
+
console.error('');
|
|
219
|
+
return apiKey;
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
rl.close();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// ─── Password Prompt (hidden input) ─────────────────────────────
|
|
226
|
+
askPassword(prompt, rl) {
|
|
227
|
+
return new Promise((resolve) => {
|
|
228
|
+
const stdin = process.stdin;
|
|
229
|
+
const stderr = process.stderr;
|
|
230
|
+
stderr.write(prompt);
|
|
231
|
+
if (stdin.isTTY) {
|
|
232
|
+
stdin.setRawMode(true);
|
|
233
|
+
stdin.resume();
|
|
234
|
+
stdin.setEncoding('utf8');
|
|
235
|
+
let password = '';
|
|
236
|
+
const onData = (char) => {
|
|
237
|
+
const c = char.toString();
|
|
238
|
+
if (c === '\n' || c === '\r' || c === '\u0004') {
|
|
239
|
+
stdin.setRawMode(false);
|
|
240
|
+
stdin.pause();
|
|
241
|
+
stdin.removeListener('data', onData);
|
|
242
|
+
stderr.write('\n');
|
|
243
|
+
resolve(password);
|
|
244
|
+
}
|
|
245
|
+
else if (c === '\u0003') {
|
|
246
|
+
stdin.setRawMode(false);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
else if (c === '\u007f' || c === '\b') {
|
|
250
|
+
if (password.length > 0) {
|
|
251
|
+
password = password.slice(0, -1);
|
|
252
|
+
stderr.write('\b \b');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
password += c;
|
|
257
|
+
stderr.write('*');
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
stdin.on('data', onData);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
rl.question('', (answer) => resolve(answer));
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
// ─── Credential Storage ─────────────────────────────────────────
|
|
268
|
+
/**
|
|
269
|
+
* Read the stored API key (sk-xxx) from disk.
|
|
270
|
+
*
|
|
271
|
+
* Supports three formats for backward compatibility:
|
|
272
|
+
* 1. JSON { "api_key": "sk-xxx" } ← current
|
|
273
|
+
* 2. JSON { "access_token": "..." } ← old JWT format (ignored)
|
|
274
|
+
* 3. Plain text "sk-xxx" ← legacy
|
|
275
|
+
*/
|
|
276
|
+
async getToken() {
|
|
277
|
+
try {
|
|
278
|
+
if (!fs_1.default.existsSync(CREDENTIALS_FILE))
|
|
279
|
+
return null;
|
|
280
|
+
const raw = fs_1.default.readFileSync(CREDENTIALS_FILE, 'utf-8').trim();
|
|
281
|
+
if (!raw)
|
|
282
|
+
return null;
|
|
283
|
+
// JSON format
|
|
284
|
+
if (raw.startsWith('{')) {
|
|
285
|
+
const parsed = JSON.parse(raw);
|
|
286
|
+
// Current format
|
|
287
|
+
if (parsed.api_key)
|
|
288
|
+
return parsed.api_key;
|
|
289
|
+
// Old JWT format — can't use, need re-login
|
|
290
|
+
if (parsed.access_token)
|
|
291
|
+
return null;
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
// Plain text — accept if it looks like an API key
|
|
295
|
+
if (raw.startsWith('sk-'))
|
|
296
|
+
return raw;
|
|
297
|
+
// Old plain-text JWT — can't use
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async saveApiKey(apiKey) {
|
|
305
|
+
if (!fs_1.default.existsSync(CREDENTIALS_DIR)) {
|
|
306
|
+
fs_1.default.mkdirSync(CREDENTIALS_DIR, { recursive: true });
|
|
307
|
+
}
|
|
308
|
+
const data = JSON.stringify({ api_key: apiKey }, null, 2);
|
|
309
|
+
fs_1.default.writeFileSync(CREDENTIALS_FILE, data, { mode: 0o600 });
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Clear stored credentials (logout).
|
|
313
|
+
*/
|
|
314
|
+
async logout() {
|
|
315
|
+
try {
|
|
316
|
+
if (fs_1.default.existsSync(CREDENTIALS_FILE)) {
|
|
317
|
+
fs_1.default.unlinkSync(CREDENTIALS_FILE);
|
|
318
|
+
console.error('Bamboo: Logged out. Credentials cleared.');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// ignore
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
exports.AuthService = AuthService;
|
|
327
|
+
// ─── Utilities ───────────────────────────────────────────────────────
|
|
328
|
+
/**
|
|
329
|
+
* Open a URL in the default browser (cross-platform, no external deps).
|
|
330
|
+
*/
|
|
331
|
+
function openBrowser(url) {
|
|
332
|
+
const escaped = url.replace(/"/g, '\\"');
|
|
333
|
+
const cmd = process.platform === 'darwin' ? `open "${escaped}"` :
|
|
334
|
+
process.platform === 'win32' ? `start "" "${escaped}"` :
|
|
335
|
+
`xdg-open "${escaped}"`;
|
|
336
|
+
(0, child_process_1.exec)(cmd, (err) => {
|
|
337
|
+
if (err) {
|
|
338
|
+
console.error(`Bamboo: Could not open browser automatically.`);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AuthService } from './auth';
|
|
2
|
+
export interface ToolConfig {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class ConfigService {
|
|
7
|
+
private authService;
|
|
8
|
+
private apiBaseUrl;
|
|
9
|
+
constructor(authService: AuthService, apiBaseUrl?: string);
|
|
10
|
+
/**
|
|
11
|
+
* Get the runtime configuration for a specific tool.
|
|
12
|
+
*
|
|
13
|
+
* The API key (sk-xxx) is stored directly in ~/.bamboo/credentials
|
|
14
|
+
* and was generated during the login flow. No intermediate token needed.
|
|
15
|
+
*
|
|
16
|
+
* Base URL handling:
|
|
17
|
+
* - Anthropic SDK adds /v1 internally, so ANTHROPIC_BASE_URL must NOT include /v1
|
|
18
|
+
* - OpenAI SDK expects /v1 in the base URL, so OPENAI_BASE_URL must include /v1
|
|
19
|
+
*/
|
|
20
|
+
getConfig(toolName: string): Promise<ToolConfig>;
|
|
21
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConfigService = void 0;
|
|
4
|
+
class ConfigService {
|
|
5
|
+
constructor(authService, apiBaseUrl = 'https://api.bamboonode.cn') {
|
|
6
|
+
this.authService = authService;
|
|
7
|
+
this.apiBaseUrl = apiBaseUrl;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Get the runtime configuration for a specific tool.
|
|
11
|
+
*
|
|
12
|
+
* The API key (sk-xxx) is stored directly in ~/.bamboo/credentials
|
|
13
|
+
* and was generated during the login flow. No intermediate token needed.
|
|
14
|
+
*
|
|
15
|
+
* Base URL handling:
|
|
16
|
+
* - Anthropic SDK adds /v1 internally, so ANTHROPIC_BASE_URL must NOT include /v1
|
|
17
|
+
* - OpenAI SDK expects /v1 in the base URL, so OPENAI_BASE_URL must include /v1
|
|
18
|
+
*/
|
|
19
|
+
async getConfig(toolName) {
|
|
20
|
+
const apiKey = await this.authService.getToken();
|
|
21
|
+
if (!apiKey) {
|
|
22
|
+
throw new Error('Not logged in. Please run the CLI to start the login flow.');
|
|
23
|
+
}
|
|
24
|
+
// Anthropic SDK appends /v1 itself; OpenAI SDK expects /v1 in the base URL
|
|
25
|
+
const baseUrl = toolName === 'claude'
|
|
26
|
+
? this.apiBaseUrl
|
|
27
|
+
: `${this.apiBaseUrl}/v1`;
|
|
28
|
+
return { apiKey, baseUrl };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.ConfigService = ConfigService;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./auth"), exports);
|
|
18
|
+
__exportStar(require("./config"), exports);
|
|
19
|
+
__exportStar(require("./process"), exports);
|
|
20
|
+
__exportStar(require("./resolve-bin"), exports);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class ProcessManager {
|
|
2
|
+
/**
|
|
3
|
+
* Spawns a child process with full stdio inheritance.
|
|
4
|
+
* Uses child_process.spawn directly — no need for execa dependency.
|
|
5
|
+
* Propagates the child's exit code to the parent.
|
|
6
|
+
*
|
|
7
|
+
* Cross-platform: on Windows, .cmd/.bat shims require `shell: true`
|
|
8
|
+
* for spawn to execute them correctly.
|
|
9
|
+
*/
|
|
10
|
+
run(command: string, args: string[], env: NodeJS.ProcessEnv): Promise<void>;
|
|
11
|
+
}
|
package/dist/process.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProcessManager = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
class ProcessManager {
|
|
6
|
+
/**
|
|
7
|
+
* Spawns a child process with full stdio inheritance.
|
|
8
|
+
* Uses child_process.spawn directly — no need for execa dependency.
|
|
9
|
+
* Propagates the child's exit code to the parent.
|
|
10
|
+
*
|
|
11
|
+
* Cross-platform: on Windows, .cmd/.bat shims require `shell: true`
|
|
12
|
+
* for spawn to execute them correctly.
|
|
13
|
+
*/
|
|
14
|
+
async run(command, args, env) {
|
|
15
|
+
const finalEnv = { ...process.env, ...env };
|
|
16
|
+
// Windows .cmd/.bat shims need shell: true
|
|
17
|
+
const needsShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(command);
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const child = (0, child_process_1.spawn)(command, args, {
|
|
20
|
+
env: finalEnv,
|
|
21
|
+
stdio: 'inherit',
|
|
22
|
+
shell: needsShell,
|
|
23
|
+
});
|
|
24
|
+
child.on('error', (err) => {
|
|
25
|
+
reject(new Error(`Failed to start process "${command}": ${err.message}`));
|
|
26
|
+
});
|
|
27
|
+
child.on('exit', (code, signal) => {
|
|
28
|
+
if (signal) {
|
|
29
|
+
// Child was killed by a signal
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
// Propagate child's exit code
|
|
33
|
+
process.exit(code ?? 0);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.ProcessManager = ProcessManager;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a CLI binary from the system PATH.
|
|
3
|
+
*
|
|
4
|
+
* Strategy B: users install the underlying CLI tools globally themselves
|
|
5
|
+
* (e.g. `npm i -g @anthropic-ai/claude-code` or `npm i -g @openai/codex`).
|
|
6
|
+
* Bamboo locates the binary at runtime.
|
|
7
|
+
*
|
|
8
|
+
* Cross-platform: uses `where` on Windows, `which` on macOS/Linux.
|
|
9
|
+
*
|
|
10
|
+
* @param name The binary name to look up (e.g. "claude", "codex")
|
|
11
|
+
* @param pkgHint Human-readable install hint shown on failure
|
|
12
|
+
* @returns Absolute path to the binary
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveBin(name: string, pkgHint: string): string;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveBin = resolveBin;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const isWindows = process.platform === 'win32';
|
|
11
|
+
/**
|
|
12
|
+
* Known binary → expected version output substring.
|
|
13
|
+
* Used to verify we found the correct binary, not a different package
|
|
14
|
+
* with the same name (e.g. "codex" static site generator vs OpenAI Codex).
|
|
15
|
+
*/
|
|
16
|
+
const VERSION_CHECKS = {
|
|
17
|
+
claude: 'Claude Code',
|
|
18
|
+
codex: 'codex-cli',
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a CLI binary from the system PATH.
|
|
22
|
+
*
|
|
23
|
+
* Strategy B: users install the underlying CLI tools globally themselves
|
|
24
|
+
* (e.g. `npm i -g @anthropic-ai/claude-code` or `npm i -g @openai/codex`).
|
|
25
|
+
* Bamboo locates the binary at runtime.
|
|
26
|
+
*
|
|
27
|
+
* Cross-platform: uses `where` on Windows, `which` on macOS/Linux.
|
|
28
|
+
*
|
|
29
|
+
* @param name The binary name to look up (e.g. "claude", "codex")
|
|
30
|
+
* @param pkgHint Human-readable install hint shown on failure
|
|
31
|
+
* @returns Absolute path to the binary
|
|
32
|
+
*/
|
|
33
|
+
function resolveBin(name, pkgHint) {
|
|
34
|
+
const candidates = [];
|
|
35
|
+
// 1. Lookup from system PATH (cross-platform)
|
|
36
|
+
const pathResults = lookupFromPath(name);
|
|
37
|
+
for (const p of pathResults) {
|
|
38
|
+
if (!candidates.includes(p))
|
|
39
|
+
candidates.push(p);
|
|
40
|
+
}
|
|
41
|
+
// 2. Try common global npm/pnpm paths
|
|
42
|
+
const globalDirs = getGlobalNodeModulesDirs();
|
|
43
|
+
for (const dir of globalDirs) {
|
|
44
|
+
// On Windows, npm creates .cmd shims in the parent of node_modules
|
|
45
|
+
const binDir = isWindows ? path_1.default.resolve(dir, '..') : path_1.default.join(dir, '.bin');
|
|
46
|
+
const extensions = isWindows ? ['.cmd', '.ps1', '.exe', ''] : [''];
|
|
47
|
+
for (const ext of extensions) {
|
|
48
|
+
const binPath = path_1.default.join(binDir, name + ext);
|
|
49
|
+
if (fs_1.default.existsSync(binPath) && !candidates.includes(binPath)) {
|
|
50
|
+
candidates.push(binPath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// 3. Verify the binary is the one we expect
|
|
55
|
+
const expectedSubstring = VERSION_CHECKS[name];
|
|
56
|
+
for (const binPath of candidates) {
|
|
57
|
+
if (!expectedSubstring || verifyBinary(binPath, expectedSubstring)) {
|
|
58
|
+
return binPath;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (candidates.length > 0) {
|
|
62
|
+
throw new Error(`Bamboo: Found "${name}" at ${candidates[0]}, but it is not the expected tool.\n` +
|
|
63
|
+
`Please install the correct version:\n\n` +
|
|
64
|
+
` npm install -g ${pkgHint}\n`);
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`Bamboo: Could not find "${name}" binary.\n` +
|
|
67
|
+
`Please install it globally first:\n\n` +
|
|
68
|
+
` npm install -g ${pkgHint}\n`);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Use system command to find a binary in PATH.
|
|
72
|
+
* - Windows: `where <name>` (may return multiple lines)
|
|
73
|
+
* - macOS/Linux: `which <name>`
|
|
74
|
+
*
|
|
75
|
+
* Returns an array of resolved paths.
|
|
76
|
+
*/
|
|
77
|
+
function lookupFromPath(name) {
|
|
78
|
+
const results = [];
|
|
79
|
+
const cmd = isWindows ? `where ${name}` : `which ${name}`;
|
|
80
|
+
try {
|
|
81
|
+
const output = (0, child_process_1.execSync)(cmd, {
|
|
82
|
+
encoding: 'utf-8',
|
|
83
|
+
timeout: 5000,
|
|
84
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
85
|
+
}).trim();
|
|
86
|
+
if (!output)
|
|
87
|
+
return results;
|
|
88
|
+
// `where` on Windows can return multiple lines (one per match)
|
|
89
|
+
const lines = output.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
if (fs_1.default.existsSync(line)) {
|
|
92
|
+
results.push(line);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// command not found or binary not in PATH
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Run `<bin> --version` and check if the output contains the expected substring.
|
|
103
|
+
*/
|
|
104
|
+
function verifyBinary(binPath, expected) {
|
|
105
|
+
try {
|
|
106
|
+
// On Windows, .cmd/.ps1 shims need shell: true to execute
|
|
107
|
+
// On Windows, .cmd/.ps1 shims need shell mode to execute
|
|
108
|
+
const needsShell = isWindows && /\.(cmd|ps1)$/i.test(binPath);
|
|
109
|
+
const cmd = needsShell
|
|
110
|
+
? `"${binPath}" --version`
|
|
111
|
+
: `"${binPath}" --version`;
|
|
112
|
+
const output = (0, child_process_1.execSync)(cmd, {
|
|
113
|
+
encoding: 'utf-8',
|
|
114
|
+
timeout: 5000,
|
|
115
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
116
|
+
...(needsShell ? { shell: 'cmd.exe' } : {}),
|
|
117
|
+
});
|
|
118
|
+
return output.includes(expected);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// --version failed; accept the binary anyway (some tools exit non-zero)
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function getGlobalNodeModulesDirs() {
|
|
126
|
+
const dirs = [];
|
|
127
|
+
// npm global
|
|
128
|
+
try {
|
|
129
|
+
const npmRoot = (0, child_process_1.execSync)('npm root -g', {
|
|
130
|
+
encoding: 'utf-8',
|
|
131
|
+
timeout: 5000,
|
|
132
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
133
|
+
}).trim();
|
|
134
|
+
if (npmRoot)
|
|
135
|
+
dirs.push(npmRoot);
|
|
136
|
+
}
|
|
137
|
+
catch { /* ignore */ }
|
|
138
|
+
// pnpm global
|
|
139
|
+
try {
|
|
140
|
+
const pnpmRoot = (0, child_process_1.execSync)('pnpm root -g', {
|
|
141
|
+
encoding: 'utf-8',
|
|
142
|
+
timeout: 5000,
|
|
143
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
144
|
+
}).trim();
|
|
145
|
+
if (pnpmRoot)
|
|
146
|
+
dirs.push(pnpmRoot);
|
|
147
|
+
}
|
|
148
|
+
catch { /* ignore */ }
|
|
149
|
+
return dirs;
|
|
150
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bamboo-ai/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Core logic for Bamboo CLI wrappers — SSO auth, config fetching, process management",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=20.0.0"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"bamboo",
|
|
16
|
+
"claude-code",
|
|
17
|
+
"codex",
|
|
18
|
+
"ai",
|
|
19
|
+
"cli",
|
|
20
|
+
"auth"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/bamboo-ai/claude-code-router.git",
|
|
25
|
+
"directory": "packages/bamboo-core"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"clean": "rm -rf dist"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"axios": "^1.6.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^20.0.0",
|
|
36
|
+
"typescript": "^5.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|