@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 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
+ }
@@ -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;
@@ -0,0 +1,4 @@
1
+ export * from './auth';
2
+ export * from './config';
3
+ export * from './process';
4
+ export * from './resolve-bin';
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
+ }
@@ -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
+ }