@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.
- package/dist/lib/jobs/job-runner.d.ts +4 -0
- package/dist/lib/jobs/job-runner.js +126 -15
- package/dist/lib/jobs/job-store.js +4 -0
- package/dist/routes/api/v1/jobs/[id]/route.d.ts +4 -0
- package/dist/routes/api/v1/jobs/[id]/route.js +6 -0
- package/dist/routes/api/v1/jobs/route.d.ts +4 -0
- package/dist/routes/api/v1/jobs/route.js +2 -0
- package/package.json +4 -4
- package/static/assets/{index-gdnIvn_s.js → index-D3PxQUiY.js} +168 -166
- package/static/assets/index-ZOkx8dA0.css +33 -0
- package/static/index.html +2 -2
- package/static/assets/index-DIVD0EVP.css +0 -33
|
@@ -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,
|
|
21
|
+
function buildCommand(agent, instruction, config) {
|
|
22
22
|
const quotedInstruction = shellQuote(instruction);
|
|
23
|
+
let mainCommand = '';
|
|
23
24
|
switch (agent) {
|
|
24
25
|
case 'gemini': {
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
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
|
-
|
|
43
|
+
break;
|
|
35
44
|
}
|
|
36
45
|
case 'claude':
|
|
37
46
|
default: {
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
40
|
-
"@lovelybunch/mcp": "^1.0.69-alpha.
|
|
41
|
-
"@lovelybunch/types": "^1.0.69-alpha.
|
|
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",
|