@lovelybunch/api 1.0.69-alpha.1 → 1.0.69-alpha.3

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.
@@ -11,6 +11,10 @@ export declare class JobRunner {
11
11
  constructor();
12
12
  private ensureCliAvailable;
13
13
  private ensureLogPath;
14
+ private loadAgentInstructions;
15
+ private loadMcpConfigs;
16
+ private writeMcpJson;
17
+ private cleanupMcpJson;
14
18
  private buildInstruction;
15
19
  run(job: ScheduledJob, runId: string): Promise<JobRunResult>;
16
20
  }
@@ -18,28 +18,39 @@ function resolveAgent(model) {
18
18
  return 'codex';
19
19
  return 'claude';
20
20
  }
21
- function buildCommand(agent, instruction, options) {
21
+ function buildCommand(agent, instruction, config) {
22
22
  const quotedInstruction = shellQuote(instruction);
23
+ let mainCommand = '';
23
24
  switch (agent) {
24
25
  case 'gemini': {
25
- const cmd = `gemini --yolo -i ${quotedInstruction}`;
26
- return { command: 'gemini', shellCommand: cmd };
26
+ // For non-Claude agents, use the --mcp flag approach if supported
27
+ const mcpFlags = config.mcpServers && config.mcpServers.length > 0
28
+ ? config.mcpServers.map(server => `--mcp ${shellQuote(server)}`).join(' ')
29
+ : '';
30
+ mainCommand = `gemini --yolo ${mcpFlags} -i ${quotedInstruction}`;
31
+ break;
27
32
  }
28
33
  case 'codex': {
29
- const baseCmd = `codex ${quotedInstruction} --dangerously-bypass-approvals-and-sandbox`;
30
- const needsPseudoTty = options.runningAsRoot && process.platform !== 'win32';
31
- const wrappedCmd = needsPseudoTty
34
+ // For non-Claude agents, use the --mcp flag approach if supported
35
+ const mcpFlags = config.mcpServers && config.mcpServers.length > 0
36
+ ? config.mcpServers.map(server => `--mcp ${shellQuote(server)}`).join(' ')
37
+ : '';
38
+ const baseCmd = `codex ${quotedInstruction} --dangerously-bypass-approvals-and-sandbox ${mcpFlags}`.trim();
39
+ const needsPseudoTty = config.runningAsRoot && process.platform !== 'win32';
40
+ mainCommand = needsPseudoTty
32
41
  ? `script -q -e -c ${shellQuote(baseCmd)} /dev/null`
33
42
  : baseCmd;
34
- return { command: 'codex', shellCommand: wrappedCmd };
43
+ break;
35
44
  }
36
45
  case 'claude':
37
46
  default: {
38
- const prefix = options.runningAsRoot ? 'IS_SANDBOX=1 ' : '';
39
- const cmd = `${prefix}claude ${quotedInstruction} --dangerously-skip-permissions`;
40
- return { command: 'claude', shellCommand: cmd };
47
+ // Claude uses .mcp.json for MCP server configuration (no --mcp flag)
48
+ const prefix = config.runningAsRoot ? 'IS_SANDBOX=1 ' : '';
49
+ mainCommand = `${prefix}claude ${quotedInstruction} --dangerously-skip-permissions`.trim();
50
+ break;
41
51
  }
42
52
  }
53
+ return { command: agent === 'claude' ? 'claude' : agent, shellCommand: mainCommand };
43
54
  }
44
55
  const CLI_AGENT_LABEL = {
45
56
  claude: 'Claude',
@@ -80,18 +91,103 @@ export class JobRunner {
80
91
  await fs.mkdir(logsDir, { recursive: true });
81
92
  return path.join(logsDir, `${runId}.log`);
82
93
  }
83
- buildInstruction(job, agentLabel) {
84
- const prompt = job.prompt.trim();
94
+ async loadAgentInstructions(agentId) {
95
+ try {
96
+ const projectRoot = await this.projectRootPromise;
97
+ const agentPath = path.join(projectRoot, '.nut', 'agents', agentId);
98
+ const content = await fs.readFile(agentPath, 'utf-8');
99
+ // Extract content after frontmatter
100
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
101
+ if (frontmatterMatch && frontmatterMatch[2]) {
102
+ return frontmatterMatch[2].trim();
103
+ }
104
+ return content.trim();
105
+ }
106
+ catch (error) {
107
+ console.warn(`Failed to load agent ${agentId}:`, error);
108
+ return null;
109
+ }
110
+ }
111
+ async loadMcpConfigs() {
112
+ try {
113
+ const projectRoot = await this.projectRootPromise;
114
+ const mcpConfigPath = path.join(projectRoot, '.nut', 'mcp', 'config.json');
115
+ const content = await fs.readFile(mcpConfigPath, 'utf-8');
116
+ const json = JSON.parse(content);
117
+ return json.mcpServers || {};
118
+ }
119
+ catch (error) {
120
+ if (error.code !== 'ENOENT') {
121
+ console.warn('Failed to load MCP config:', error);
122
+ }
123
+ return {};
124
+ }
125
+ }
126
+ async writeMcpJson(mcpServers, allMcpConfigs) {
127
+ const projectRoot = await this.projectRootPromise;
128
+ const mcpJsonPath = path.join(projectRoot, '.mcp.json');
129
+ // Filter to only include the requested servers that are enabled
130
+ const filteredConfigs = {};
131
+ for (const serverName of mcpServers) {
132
+ const config = allMcpConfigs[serverName];
133
+ if (config && config.enabled !== false) {
134
+ filteredConfigs[serverName] = config;
135
+ }
136
+ }
137
+ const mcpJson = {
138
+ mcpServers: filteredConfigs
139
+ };
140
+ await fs.writeFile(mcpJsonPath, JSON.stringify(mcpJson, null, 2), 'utf-8');
141
+ }
142
+ async cleanupMcpJson() {
143
+ try {
144
+ const projectRoot = await this.projectRootPromise;
145
+ const mcpJsonPath = path.join(projectRoot, '.mcp.json');
146
+ // Reset to empty mcpServers object
147
+ const emptyMcpJson = {
148
+ mcpServers: {}
149
+ };
150
+ await fs.writeFile(mcpJsonPath, JSON.stringify(emptyMcpJson, null, 2), 'utf-8');
151
+ }
152
+ catch (error) {
153
+ // Don't fail the job if cleanup fails
154
+ console.warn('Failed to cleanup .mcp.json:', error);
155
+ }
156
+ }
157
+ async buildInstruction(job, agentLabel) {
85
158
  const scheduleDescription = job.schedule.type === 'cron'
86
159
  ? `Cron: ${job.schedule.expression}`
87
160
  : `Interval: every ${job.schedule.hours}h on ${job.schedule.daysOfWeek.join(', ')}`;
88
- return `Run this scheduled Coconut job (${job.id}).\nSchedule: ${scheduleDescription}.\nPreferred CLI agent: ${agentLabel}.\nInstruction:\n${prompt}`;
161
+ let instruction = `Run this scheduled Coconut job (${job.id}).\nSchedule: ${scheduleDescription}.\nPreferred CLI agent: ${agentLabel}.\n\n`;
162
+ // If an agent is specified, load and use its instructions
163
+ if (job.agentId) {
164
+ const agentInstructions = await this.loadAgentInstructions(job.agentId);
165
+ if (agentInstructions) {
166
+ instruction += `Agent Instructions (${job.agentId}):\n${agentInstructions}\n\n`;
167
+ }
168
+ }
169
+ // Add custom instructions (or main prompt if no agent)
170
+ const customInstructions = job.prompt.trim();
171
+ if (customInstructions) {
172
+ const label = job.agentId ? 'Additional Custom Instructions' : 'Instructions';
173
+ instruction += `${label}:\n${customInstructions}`;
174
+ }
175
+ return instruction;
89
176
  }
90
177
  async run(job, runId) {
91
178
  const agent = resolveAgent(job.model);
92
- const instruction = this.buildInstruction(job, CLI_AGENT_LABEL[agent] || agent);
179
+ const instruction = await this.buildInstruction(job, CLI_AGENT_LABEL[agent] || agent);
93
180
  const runningAsRoot = typeof process.getuid === 'function' && process.getuid() === 0;
94
- const { shellCommand } = buildCommand(agent, instruction, { runningAsRoot });
181
+ // Write .mcp.json if MCP servers are specified (Claude reads this from project root)
182
+ const mcpJsonWritten = job.mcpServers && job.mcpServers.length > 0 && agent === 'claude';
183
+ if (mcpJsonWritten) {
184
+ const allMcpConfigs = await this.loadMcpConfigs();
185
+ await this.writeMcpJson(job.mcpServers, allMcpConfigs);
186
+ }
187
+ const { shellCommand } = buildCommand(agent, instruction, {
188
+ runningAsRoot,
189
+ mcpServers: job.mcpServers
190
+ });
95
191
  const projectRoot = await this.projectRootPromise;
96
192
  const logPath = await this.ensureLogPath(job.id, runId);
97
193
  const logStream = createWriteStream(logPath, { flags: 'a' });
@@ -99,6 +195,9 @@ export class JobRunner {
99
195
  logStream.write(`[${new Date().toISOString()}] Starting job ${job.id} using ${agent} CLI\n`);
100
196
  logStream.write(`Instruction: ${instruction}\n`);
101
197
  logStream.write(`Command: ${shellCommand}\n`);
198
+ if (job.mcpServers && job.mcpServers.length > 0) {
199
+ logStream.write(`MCP Servers: ${job.mcpServers.join(', ')} (configured via .mcp.json)\n`);
200
+ }
102
201
  return new Promise((resolve) => {
103
202
  let cliMissingError = null;
104
203
  try {
@@ -111,6 +210,10 @@ export class JobRunner {
111
210
  const message = cliMissingError.message;
112
211
  logStream.write(`${message}\n`);
113
212
  logStream.end();
213
+ // Cleanup .mcp.json if it was written
214
+ if (mcpJsonWritten) {
215
+ this.cleanupMcpJson().catch(err => console.warn('Cleanup failed:', err));
216
+ }
114
217
  resolve({
115
218
  status: 'failed',
116
219
  error: message,
@@ -146,6 +249,10 @@ export class JobRunner {
146
249
  logStream.write(`${message}\n`);
147
250
  logStream.end();
148
251
  clearTimeout(abortTimeout);
252
+ // Cleanup .mcp.json if it was written
253
+ if (mcpJsonWritten) {
254
+ this.cleanupMcpJson().catch(err => console.warn('Cleanup failed:', err));
255
+ }
149
256
  resolve({
150
257
  status: 'failed',
151
258
  error: message,
@@ -159,6 +266,10 @@ export class JobRunner {
159
266
  logStream.write(`\n[${new Date().toISOString()}] Job ${job.id} completed with exit code ${code}\n`);
160
267
  logStream.end();
161
268
  clearTimeout(abortTimeout);
269
+ // Cleanup .mcp.json if it was written
270
+ if (mcpJsonWritten) {
271
+ this.cleanupMcpJson().catch(err => console.warn('Cleanup failed:', err));
272
+ }
162
273
  const summary = summaryChunks.join('');
163
274
  resolve({
164
275
  status,
@@ -175,6 +175,8 @@ export class JobStore {
175
175
  runs,
176
176
  tags: data.tags ?? [],
177
177
  contextPaths: data.contextPaths ?? [],
178
+ agentId: data.agentId,
179
+ mcpServers: data.mcpServers,
178
180
  };
179
181
  }
180
182
  toFrontmatter(job) {
@@ -206,6 +208,8 @@ export class JobStore {
206
208
  })),
207
209
  tags: job.tags ?? [],
208
210
  contextPaths: job.contextPaths ?? [],
211
+ agentId: job.agentId,
212
+ mcpServers: job.mcpServers,
209
213
  };
210
214
  }
211
215
  }
@@ -47,6 +47,8 @@ export declare function GET(c: Context): Promise<(Response & import("hono").Type
47
47
  }[];
48
48
  tags?: string[];
49
49
  contextPaths?: string[];
50
+ agentId?: string;
51
+ mcpServers?: string[];
50
52
  };
51
53
  }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
52
54
  success: false;
@@ -108,6 +110,8 @@ export declare function PATCH(c: Context): Promise<(Response & import("hono").Ty
108
110
  }[];
109
111
  tags?: string[];
110
112
  contextPaths?: string[];
113
+ agentId?: string;
114
+ mcpServers?: string[];
111
115
  };
112
116
  }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
113
117
  success: false;
@@ -82,6 +82,12 @@ export async function PATCH(c) {
82
82
  schedule,
83
83
  tags: Array.isArray(body.tags) ? body.tags : existing.tags,
84
84
  contextPaths: Array.isArray(body.contextPaths) ? body.contextPaths : existing.contextPaths,
85
+ agentId: body.agentId !== undefined
86
+ ? (typeof body.agentId === 'string' && body.agentId ? body.agentId : undefined)
87
+ : existing.agentId,
88
+ mcpServers: body.mcpServers !== undefined
89
+ ? (Array.isArray(body.mcpServers) ? body.mcpServers.filter((s) => typeof s === 'string') : undefined)
90
+ : existing.mcpServers,
85
91
  metadata: {
86
92
  ...existing.metadata,
87
93
  updatedAt: new Date()
@@ -43,6 +43,8 @@ export declare function GET(c: Context): Promise<(Response & import("hono").Type
43
43
  }[];
44
44
  tags?: string[];
45
45
  contextPaths?: string[];
46
+ agentId?: string;
47
+ mcpServers?: string[];
46
48
  }[];
47
49
  }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
48
50
  success: false;
@@ -98,6 +100,8 @@ export declare function POST(c: Context): Promise<(Response & import("hono").Typ
98
100
  }[];
99
101
  tags?: string[];
100
102
  contextPaths?: string[];
103
+ agentId?: string;
104
+ mcpServers?: string[];
101
105
  };
102
106
  }, 201, "json">) | (Response & import("hono").TypedResponse<{
103
107
  success: false;
@@ -122,6 +122,8 @@ export async function POST(c) {
122
122
  runs: [],
123
123
  tags: Array.isArray(body.tags) ? body.tags : [],
124
124
  contextPaths: Array.isArray(body.contextPaths) ? body.contextPaths : [],
125
+ agentId: body.agentId && typeof body.agentId === 'string' ? body.agentId : undefined,
126
+ mcpServers: Array.isArray(body.mcpServers) ? body.mcpServers.filter((s) => typeof s === 'string') : undefined,
125
127
  };
126
128
  await store.saveJob(job);
127
129
  await scheduler.refresh(job.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovelybunch/api",
3
- "version": "1.0.69-alpha.1",
3
+ "version": "1.0.69-alpha.3",
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.1",
40
- "@lovelybunch/mcp": "^1.0.69-alpha.1",
41
- "@lovelybunch/types": "^1.0.69-alpha.1",
39
+ "@lovelybunch/core": "^1.0.69-alpha.3",
40
+ "@lovelybunch/mcp": "^1.0.69-alpha.3",
41
+ "@lovelybunch/types": "^1.0.69-alpha.3",
42
42
  "arctic": "^1.9.2",
43
43
  "bcrypt": "^5.1.1",
44
44
  "cookie": "^0.6.0",