@lovelybunch/api 1.0.69-alpha.16 → 1.0.69-alpha.17

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.
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Get environment variables to inject into spawned child processes
3
+ * These are the API keys that coding agents (Claude Code, Codex, Gemini CLI) will use
4
+ * Returns a full environment object with process.env merged with API keys
5
+ */
6
+ export declare function getInjectedEnv(): Record<string, string>;
@@ -0,0 +1,64 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { existsSync, readFileSync } from 'fs';
4
+ /**
5
+ * Get the path to the global config file
6
+ */
7
+ function getGlobalConfigPath() {
8
+ const platform = process.platform;
9
+ let configDir;
10
+ if (platform === 'win32') {
11
+ configDir = join(process.env.APPDATA || homedir(), 'coconuts');
12
+ }
13
+ else if (platform === 'darwin') {
14
+ configDir = join(homedir(), 'Library', 'Application Support', 'coconuts');
15
+ }
16
+ else {
17
+ // Linux/Unix
18
+ configDir = join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'coconuts');
19
+ }
20
+ return join(configDir, 'config.json');
21
+ }
22
+ /**
23
+ * Load global config from OS-specific location
24
+ */
25
+ function loadGlobalConfig() {
26
+ const configPath = getGlobalConfigPath();
27
+ if (!existsSync(configPath)) {
28
+ return { apiKeys: {}, defaults: {} };
29
+ }
30
+ try {
31
+ const content = readFileSync(configPath, 'utf-8');
32
+ return JSON.parse(content);
33
+ }
34
+ catch (error) {
35
+ console.warn('Warning: Could not parse global config file, using defaults');
36
+ return { apiKeys: {}, defaults: {} };
37
+ }
38
+ }
39
+ /**
40
+ * Get environment variables to inject into spawned child processes
41
+ * These are the API keys that coding agents (Claude Code, Codex, Gemini CLI) will use
42
+ * Returns a full environment object with process.env merged with API keys
43
+ */
44
+ export function getInjectedEnv() {
45
+ const config = loadGlobalConfig();
46
+ const env = { ...process.env };
47
+ // Only inject keys that are actually configured
48
+ if (config.apiKeys?.anthropic) {
49
+ env.ANTHROPIC_API_KEY = config.apiKeys.anthropic;
50
+ }
51
+ if (config.apiKeys?.openai) {
52
+ env.OPENAI_API_KEY = config.apiKeys.openai;
53
+ }
54
+ if (config.apiKeys?.gemini) {
55
+ env.GEMINI_API_KEY = config.apiKeys.gemini;
56
+ }
57
+ if (config.apiKeys?.replicate) {
58
+ env.REPLICATE_API_TOKEN = config.apiKeys.replicate;
59
+ }
60
+ if (config.apiKeys?.factorydroid) {
61
+ env.FACTORY_API_KEY = config.apiKeys.factorydroid;
62
+ }
63
+ return env;
64
+ }
@@ -3,6 +3,7 @@ import { createWriteStream } from 'fs';
3
3
  import { promises as fs } from 'fs';
4
4
  import path from 'path';
5
5
  import { getProjectRoot } from '../project-paths.js';
6
+ import { getInjectedEnv } from '../env-injection.js';
6
7
  function shellQuote(value) {
7
8
  if (value === '')
8
9
  return "''";
@@ -16,6 +17,8 @@ function resolveAgent(model) {
16
17
  return 'gemini';
17
18
  if (lower.includes('codex') || lower.includes('gpt') || lower.includes('openai'))
18
19
  return 'codex';
20
+ if (lower.includes('droid') || lower.includes('factory'))
21
+ return 'droid';
19
22
  return 'claude';
20
23
  }
21
24
  function buildCommand(agent, instruction, config) {
@@ -42,6 +45,14 @@ function buildCommand(agent, instruction, config) {
42
45
  : baseCmd;
43
46
  break;
44
47
  }
48
+ case 'droid': {
49
+ // For Factory Droid, use the --mcp flag approach if supported
50
+ const mcpFlags = config.mcpServers && config.mcpServers.length > 0
51
+ ? config.mcpServers.map(server => `--mcp ${shellQuote(server)}`).join(' ')
52
+ : '';
53
+ mainCommand = `droid ${quotedInstruction} --skip-permissions-unsafe ${mcpFlags}`.trim();
54
+ break;
55
+ }
45
56
  case 'claude':
46
57
  default: {
47
58
  // Claude uses .mcp.json for MCP server configuration (no --mcp flag)
@@ -55,12 +66,14 @@ function buildCommand(agent, instruction, config) {
55
66
  const CLI_AGENT_LABEL = {
56
67
  claude: 'Claude',
57
68
  gemini: 'Gemini',
58
- codex: 'Code'
69
+ codex: 'Codex',
70
+ droid: 'Factory Droid'
59
71
  };
60
72
  const CLI_AGENT_BINARY = {
61
73
  claude: 'claude',
62
74
  gemini: 'gemini',
63
- codex: 'codex'
75
+ codex: 'codex',
76
+ droid: 'droid'
64
77
  };
65
78
  const DEFAULT_MAX_RUNTIME_MS = 30 * 60 * 1000; // 30 minutes
66
79
  function getMaxRuntime() {
@@ -223,9 +236,14 @@ export class JobRunner {
223
236
  });
224
237
  return;
225
238
  }
239
+ // Inject API keys from global config into child process environment
240
+ const injectedEnv = getInjectedEnv();
226
241
  const child = spawn('bash', ['-lc', shellCommand], {
227
242
  cwd: projectRoot,
228
- env: process.env,
243
+ env: {
244
+ ...process.env,
245
+ ...injectedEnv
246
+ },
229
247
  stdio: ['ignore', 'pipe', 'pipe'],
230
248
  });
231
249
  const maxRuntime = getMaxRuntime();
@@ -5,6 +5,7 @@ import fs from 'fs';
5
5
  import { createInitScript } from './context-helper.js';
6
6
  import { getShellPath, getShellArgs, prepareShellInit } from './shell-utils.js';
7
7
  import { getLogger, AgentKinds } from '@lovelybunch/core/logging';
8
+ import { getInjectedEnv } from '../env-injection.js';
8
9
  export class TerminalManager {
9
10
  sessions = new Map();
10
11
  cleanupInterval;
@@ -82,9 +83,9 @@ export class TerminalManager {
82
83
  }
83
84
  // Get shell arguments
84
85
  const shellArgs = getShellArgs(shellPath, initScriptPath);
85
- // Prepare environment variables
86
+ // Prepare environment variables with API keys injected
86
87
  const env = {
87
- ...process.env,
88
+ ...getInjectedEnv(),
88
89
  COCONUT_PROPOSAL_ID: proposalId,
89
90
  COCONUT_CONTEXT_PATH: path.join(projectRoot, '.nut', 'context'),
90
91
  COCONUT_PROPOSAL_PATH: path.join(projectRoot, '.nut', 'proposals', `${proposalId}.md`),
@@ -1,8 +1,7 @@
1
1
  import { Hono } from 'hono';
2
- import { GET, PUT, TEST, EXPORT } from './route.js';
2
+ import { GET, PUT, TEST } from './route.js';
3
3
  const config = new Hono();
4
4
  config.get('/', GET);
5
5
  config.put('/', PUT);
6
6
  config.post('/test', TEST);
7
- config.post('/export-to-env', EXPORT);
8
7
  export default config;
@@ -30,14 +30,3 @@ export declare function TEST(c: Context): Promise<(Response & import("hono").Typ
30
30
  success: false;
31
31
  message: string;
32
32
  }, 501, "json">)>;
33
- export declare function EXPORT(c: Context): Promise<(Response & import("hono").TypedResponse<{
34
- success: false;
35
- message: string;
36
- }, 400, "json">) | (Response & import("hono").TypedResponse<{
37
- success: false;
38
- message: string;
39
- }, 500, "json">) | (Response & import("hono").TypedResponse<{
40
- success: true;
41
- message: string;
42
- envPath: string;
43
- }, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
@@ -233,114 +233,30 @@ export async function TEST(c) {
233
233
  return c.json({ success: false, message: err instanceof Error ? err.message : 'Network error' }, 200);
234
234
  }
235
235
  }
236
- // Other providers not wired up yet
237
- return c.json({ success: false, message: `Provider '${provider}' test not implemented yet` }, 501);
238
- }
239
- catch (error) {
240
- return c.json({ success: false, message: 'Invalid request body' }, 400);
241
- }
242
- }
243
- // Provider to environment variable mapping
244
- const PROVIDER_ENV_VARS = {
245
- openrouter: 'OPENROUTER_API_KEY',
246
- anthropic: 'ANTHROPIC_API_KEY',
247
- openai: 'OPENAI_API_KEY',
248
- gemini: 'GEMINI_API_KEY',
249
- factorydroid: 'FACTORY_DROID_API_KEY',
250
- bedrock: 'AWS_BEDROCK_API_KEY',
251
- baseten: 'BASETEN_API_KEY',
252
- fireworks: 'FIREWORKS_API_KEY',
253
- deepinfra: 'DEEPINFRA_API_KEY'
254
- };
255
- // Helper to find workspace root (where .env should be written)
256
- async function findWorkspaceRoot() {
257
- // Try to find .nut directory first
258
- let currentDir = process.cwd();
259
- while (currentDir !== path.parse(currentDir).root) {
260
- const nutPath = path.join(currentDir, '.nut');
261
- try {
262
- await fs.access(nutPath);
263
- // Found .nut directory, return parent as workspace root
264
- return currentDir;
265
- }
266
- catch {
267
- currentDir = path.dirname(currentDir);
268
- }
269
- }
270
- // Fallback to cwd if no .nut found
271
- return process.cwd();
272
- }
273
- // POST /api/v1/config/export-to-env
274
- // Body: { provider: string }
275
- // Exports the saved API key for a provider to a .env file in workspace root
276
- export async function EXPORT(c) {
277
- try {
278
- const body = await c.req.json();
279
- const provider = (body?.provider || '').toString();
280
- if (!provider) {
281
- return c.json({ success: false, message: 'Missing provider' }, 400);
282
- }
283
- const envVarName = PROVIDER_ENV_VARS[provider];
284
- if (!envVarName) {
285
- return c.json({ success: false, message: `Unknown provider: ${provider}` }, 400);
286
- }
287
- // Load the API key from global config
288
- const configPath = await getGlobalConfigPath();
289
- let config = { apiKeys: {}, defaults: {} };
290
- try {
291
- const content = await fs.readFile(configPath, 'utf-8');
292
- config = JSON.parse(content);
293
- }
294
- catch {
295
- // Config doesn't exist yet
296
- }
297
- const apiKey = config.apiKeys?.[provider];
298
- if (!apiKey) {
299
- return c.json({ success: false, message: `No API key configured for ${provider}` }, 400);
300
- }
301
- // Find workspace root
302
- const workspaceRoot = await findWorkspaceRoot();
303
- if (!workspaceRoot) {
304
- return c.json({ success: false, message: 'Could not find workspace root' }, 500);
305
- }
306
- const envPath = path.join(workspaceRoot, '.env');
307
- // Read existing .env file if it exists
308
- let envContent = '';
309
- try {
310
- envContent = await fs.readFile(envPath, 'utf-8');
311
- }
312
- catch {
313
- // File doesn't exist, will create new
314
- }
315
- // Parse existing env vars
316
- const envLines = envContent.split('\n');
317
- let found = false;
318
- const updatedLines = envLines.map(line => {
319
- const trimmed = line.trim();
320
- if (trimmed.startsWith(envVarName + '=') || trimmed.startsWith(envVarName + ' =')) {
321
- found = true;
322
- return `${envVarName}=${apiKey}`;
236
+ if (provider === 'replicate') {
237
+ try {
238
+ // Test Replicate API by listing models (lightweight endpoint)
239
+ const resp = await fetch('https://api.replicate.com/v1/models', {
240
+ method: 'GET',
241
+ headers: {
242
+ 'Authorization': `Bearer ${effectiveKey}`,
243
+ 'Content-Type': 'application/json',
244
+ },
245
+ });
246
+ if (!resp.ok) {
247
+ const text = await resp.text();
248
+ return c.json({ success: false, message: `Replicate rejected token: ${text.slice(0, 200)}` }, 200);
249
+ }
250
+ return c.json({ success: true, message: 'Replicate token is valid' });
323
251
  }
324
- return line;
325
- });
326
- // If not found, append to the end
327
- if (!found) {
328
- // Add a newline if the file doesn't end with one
329
- if (updatedLines.length > 0 && updatedLines[updatedLines.length - 1] !== '') {
330
- updatedLines.push('');
252
+ catch (err) {
253
+ return c.json({ success: false, message: err instanceof Error ? err.message : 'Network error' }, 200);
331
254
  }
332
- updatedLines.push(`${envVarName}=${apiKey}`);
333
255
  }
334
- // Write back to .env file
335
- await fs.writeFile(envPath, updatedLines.join('\n'), 'utf-8');
336
- return c.json({
337
- success: true,
338
- message: `${envVarName} saved to .env file`,
339
- envPath
340
- });
256
+ // Other providers not wired up yet
257
+ return c.json({ success: false, message: `Provider '${provider}' test not implemented yet` }, 501);
341
258
  }
342
259
  catch (error) {
343
- console.error('Error exporting to env:', error);
344
- return c.json({ success: false, message: error instanceof Error ? error.message : 'Failed to export to env' }, 500);
260
+ return c.json({ success: false, message: 'Invalid request body' }, 400);
345
261
  }
346
262
  }
@@ -1,10 +1,49 @@
1
1
  import Replicate from 'replicate';
2
2
  import { promises as fs } from 'fs';
3
3
  import path from 'path';
4
- const REPLICATE_API_TOKEN = 'r8_6IsX89og0JuK6Ay8RXADjp8RjnEOpfK3BpNXu';
5
- const replicate = new Replicate({
6
- auth: REPLICATE_API_TOKEN,
7
- });
4
+ import { homedir } from 'os';
5
+ import { existsSync, readFileSync } from 'fs';
6
+ /**
7
+ * Get Replicate API token from global config or environment variable
8
+ */
9
+ function getReplicateApiToken() {
10
+ // First try global config
11
+ try {
12
+ const platform = process.platform;
13
+ let configDir;
14
+ if (platform === 'win32') {
15
+ configDir = path.join(process.env.APPDATA || homedir(), 'coconuts');
16
+ }
17
+ else if (platform === 'darwin') {
18
+ configDir = path.join(homedir(), 'Library', 'Application Support', 'coconuts');
19
+ }
20
+ else {
21
+ configDir = path.join(process.env.XDG_CONFIG_HOME || path.join(homedir(), '.config'), 'coconuts');
22
+ }
23
+ const configPath = path.join(configDir, 'config.json');
24
+ if (existsSync(configPath)) {
25
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
26
+ if (config.apiKeys?.replicate) {
27
+ return config.apiKeys.replicate;
28
+ }
29
+ }
30
+ }
31
+ catch (error) {
32
+ console.warn('Failed to load Replicate token from config:', error);
33
+ }
34
+ // Fallback to environment variable
35
+ return process.env.REPLICATE_API_TOKEN || null;
36
+ }
37
+ // Initialize Replicate client lazily to ensure token is loaded at request time
38
+ function getReplicateClient() {
39
+ const token = getReplicateApiToken();
40
+ if (!token) {
41
+ throw new Error('Replicate API token not configured');
42
+ }
43
+ return new Replicate({
44
+ auth: token,
45
+ });
46
+ }
8
47
  function getResourcesPath() {
9
48
  let basePath;
10
49
  if (process.env.NODE_ENV === 'development' && process.env.GAIT_DEV_ROOT) {
@@ -69,6 +108,17 @@ function getAspectRatio(dimensions) {
69
108
  }
70
109
  export async function POST(c) {
71
110
  try {
111
+ // Check if Replicate API token is configured
112
+ const replicateToken = getReplicateApiToken();
113
+ if (!replicateToken) {
114
+ return c.json({
115
+ success: false,
116
+ error: {
117
+ code: 'MISSING_API_TOKEN',
118
+ message: 'Replicate API token not configured. Please add it in Settings → Integrations.'
119
+ }
120
+ }, 400);
121
+ }
72
122
  const body = await c.req.json();
73
123
  const { prompt, inspiration, dimensions, resolution, model, image_input } = body;
74
124
  if (!prompt) {
@@ -130,7 +180,8 @@ export async function POST(c) {
130
180
  continue;
131
181
  }
132
182
  const fileBuffer = await fs.readFile(filePath);
133
- const uploadedFile = await replicate.files.create(fileBuffer, {
183
+ const replicateClient = getReplicateClient();
184
+ const uploadedFile = await replicateClient.files.create(fileBuffer, {
134
185
  resourceId,
135
186
  originalName: resource.name,
136
187
  });
@@ -160,7 +211,8 @@ export async function POST(c) {
160
211
  }
161
212
  // Run the model (defaulting to nano-banana-pro)
162
213
  const modelId = model === 'Nano Banana Pro' ? 'google/nano-banana-pro' : 'google/nano-banana-pro';
163
- const output = await replicate.run(modelId, { input });
214
+ const replicateClient = getReplicateClient();
215
+ const output = await replicateClient.run(modelId, { input });
164
216
  // Extract URL from output
165
217
  // Replicate output can be: string URL, array of URLs, or FileOutput object with url() method
166
218
  let imageUrl;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovelybunch/api",
3
- "version": "1.0.69-alpha.16",
3
+ "version": "1.0.69-alpha.17",
4
4
  "type": "module",
5
5
  "main": "dist/server-with-static.js",
6
6
  "exports": {
@@ -36,9 +36,9 @@
36
36
  "dependencies": {
37
37
  "@hono/node-server": "^1.13.7",
38
38
  "@hono/node-ws": "^1.0.6",
39
- "@lovelybunch/core": "^1.0.69-alpha.16",
40
- "@lovelybunch/mcp": "^1.0.69-alpha.16",
41
- "@lovelybunch/types": "^1.0.69-alpha.16",
39
+ "@lovelybunch/core": "^1.0.69-alpha.17",
40
+ "@lovelybunch/mcp": "^1.0.69-alpha.17",
41
+ "@lovelybunch/types": "^1.0.69-alpha.17",
42
42
  "arctic": "^1.9.2",
43
43
  "bcrypt": "^5.1.1",
44
44
  "cookie": "^0.6.0",