@lovelybunch/api 1.0.69-alpha.9 → 1.0.70
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/auth/auth-manager.d.ts +10 -2
- package/dist/lib/auth/auth-manager.js +16 -5
- package/dist/lib/env-injection.d.ts +6 -0
- package/dist/lib/env-injection.js +64 -0
- package/dist/lib/git.d.ts +1 -0
- package/dist/lib/git.js +39 -1
- package/dist/lib/jobs/job-runner.js +22 -3
- package/dist/lib/jobs/job-scheduler.js +12 -1
- package/dist/lib/jobs/job-store.d.ts +1 -0
- package/dist/lib/jobs/job-store.js +150 -28
- package/dist/lib/storage/file-storage.js +16 -7
- package/dist/lib/terminal/terminal-manager.js +3 -2
- package/dist/routes/api/v1/config/route.js +20 -0
- package/dist/routes/api/v1/context/knowledge/[filename]/route.js +18 -11
- package/dist/routes/api/v1/context/knowledge/route.js +5 -2
- package/dist/routes/api/v1/events/purge/route.d.ts +0 -2
- package/dist/routes/api/v1/events/purge/route.js +2 -14
- package/dist/routes/api/v1/events/route.d.ts +0 -2
- package/dist/routes/api/v1/events/route.js +2 -14
- package/dist/routes/api/v1/events/status/route.d.ts +0 -2
- package/dist/routes/api/v1/events/status/route.js +2 -14
- package/dist/routes/api/v1/events/stream/route.js +2 -14
- package/dist/routes/api/v1/git/index.js +66 -6
- package/dist/routes/api/v1/jobs/[id]/run/route.d.ts +2 -2
- package/dist/routes/api/v1/jobs/status/route.d.ts +1 -1
- package/dist/routes/api/v1/proposals/[id]/route.d.ts +8 -8
- package/dist/routes/api/v1/resources/[id]/route.js +11 -7
- package/dist/routes/api/v1/resources/generate/index.d.ts +3 -0
- package/dist/routes/api/v1/resources/generate/index.js +5 -0
- package/dist/routes/api/v1/resources/generate/route.d.ts +19 -0
- package/dist/routes/api/v1/resources/generate/route.js +257 -0
- package/dist/routes/api/v1/resources/index.js +2 -0
- package/dist/routes/api/v1/version/index.d.ts +3 -0
- package/dist/routes/api/v1/version/index.js +5 -0
- package/dist/routes/api/v1/version/route.d.ts +24 -0
- package/dist/routes/api/v1/version/route.js +51 -0
- package/dist/server-with-static.js +40 -23
- package/dist/server.js +40 -23
- package/package.json +5 -4
- package/static/assets/index-BmLW21zG.js +969 -0
- package/static/assets/index-CfRmV6nM.css +33 -0
- package/static/index.html +2 -2
- package/static/assets/index-BFXazLjO.js +0 -911
- package/static/assets/index-CHBcfq10.css +0 -33
|
@@ -2,7 +2,11 @@ import { AuthConfig, LocalAuthUser, AuthSession, ApiKey, UserRole } from '@lovel
|
|
|
2
2
|
export declare class AuthManager {
|
|
3
3
|
private authConfigPath;
|
|
4
4
|
private authConfig;
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Create an AuthManager instance.
|
|
7
|
+
* @param configPath - Optional custom path to auth.json. If not provided, uses OS app data directory.
|
|
8
|
+
*/
|
|
9
|
+
constructor(configPath?: string);
|
|
6
10
|
/**
|
|
7
11
|
* Check if auth is enabled
|
|
8
12
|
*/
|
|
@@ -119,4 +123,8 @@ export declare class AuthManager {
|
|
|
119
123
|
*/
|
|
120
124
|
clearCache(): void;
|
|
121
125
|
}
|
|
122
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Get the singleton AuthManager instance.
|
|
128
|
+
* @param configPath - Optional custom path to auth.json. If not provided, uses OS app data directory.
|
|
129
|
+
*/
|
|
130
|
+
export declare function getAuthManager(configPath?: string): AuthManager;
|
|
@@ -3,14 +3,19 @@ import path from 'path';
|
|
|
3
3
|
import bcrypt from 'bcrypt';
|
|
4
4
|
import jwt from 'jsonwebtoken';
|
|
5
5
|
import crypto from 'crypto';
|
|
6
|
+
import { getAuthConfigPath } from '@lovelybunch/core';
|
|
6
7
|
const SALT_ROUNDS = 10;
|
|
7
8
|
const DEFAULT_SESSION_EXPIRY = '7d';
|
|
8
9
|
const DEFAULT_COOKIE_NAME = 'nut-session';
|
|
9
10
|
export class AuthManager {
|
|
10
11
|
authConfigPath;
|
|
11
12
|
authConfig = null;
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Create an AuthManager instance.
|
|
15
|
+
* @param configPath - Optional custom path to auth.json. If not provided, uses OS app data directory.
|
|
16
|
+
*/
|
|
17
|
+
constructor(configPath) {
|
|
18
|
+
this.authConfigPath = configPath ?? getAuthConfigPath();
|
|
14
19
|
}
|
|
15
20
|
/**
|
|
16
21
|
* Check if auth is enabled
|
|
@@ -45,6 +50,9 @@ export class AuthManager {
|
|
|
45
50
|
* Save auth config to file
|
|
46
51
|
*/
|
|
47
52
|
async saveAuthConfig(config) {
|
|
53
|
+
// Ensure the directory exists
|
|
54
|
+
const dir = path.dirname(this.authConfigPath);
|
|
55
|
+
await fs.mkdir(dir, { recursive: true });
|
|
48
56
|
await fs.writeFile(this.authConfigPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
49
57
|
this.authConfig = config;
|
|
50
58
|
}
|
|
@@ -402,10 +410,13 @@ export class AuthManager {
|
|
|
402
410
|
}
|
|
403
411
|
// Singleton instance
|
|
404
412
|
let authManagerInstance = null;
|
|
405
|
-
|
|
413
|
+
/**
|
|
414
|
+
* Get the singleton AuthManager instance.
|
|
415
|
+
* @param configPath - Optional custom path to auth.json. If not provided, uses OS app data directory.
|
|
416
|
+
*/
|
|
417
|
+
export function getAuthManager(configPath) {
|
|
406
418
|
if (!authManagerInstance) {
|
|
407
|
-
|
|
408
|
-
authManagerInstance = new AuthManager(path);
|
|
419
|
+
authManagerInstance = new AuthManager(configPath);
|
|
409
420
|
}
|
|
410
421
|
return authManagerInstance;
|
|
411
422
|
}
|
|
@@ -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
|
+
}
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ export declare function getCredentialConfig(): Promise<{
|
|
|
44
44
|
origin?: string;
|
|
45
45
|
}>;
|
|
46
46
|
export declare function setRemoteUrl(remoteUrl: string): Promise<void>;
|
|
47
|
+
export declare function removeRemote(): Promise<void>;
|
|
47
48
|
export declare function storeCredentials(username: string, password: string, remoteUrl?: string): Promise<void>;
|
|
48
49
|
export interface WorktreeInfo {
|
|
49
50
|
name: string;
|
package/dist/lib/git.js
CHANGED
|
@@ -134,7 +134,33 @@ export async function mergeBranch(branchName, strategy = 'merge') {
|
|
|
134
134
|
}
|
|
135
135
|
// --- Push / Pull ---
|
|
136
136
|
export async function pushCurrent() {
|
|
137
|
-
|
|
137
|
+
// Ensure GitHub token is in credential helper before pushing
|
|
138
|
+
try {
|
|
139
|
+
const tokenRecord = await readGithubToken();
|
|
140
|
+
if (tokenRecord && isGithubTokenValid(tokenRecord)) {
|
|
141
|
+
console.log('[git] Found valid GitHub token, ensuring it\'s in credential helper');
|
|
142
|
+
// Ensure token is stored in credential helper
|
|
143
|
+
try {
|
|
144
|
+
await storeCredentials('x-access-token', tokenRecord.token);
|
|
145
|
+
console.log('[git] Successfully stored token in credential helper');
|
|
146
|
+
}
|
|
147
|
+
catch (credError) {
|
|
148
|
+
// Log but don't fail - credential helper might already have it
|
|
149
|
+
console.error('[git] Failed to store token in credential helper:', credError?.message);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
console.log('[git] No valid GitHub token found');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (tokenError) {
|
|
157
|
+
// Log but don't fail - might not be using GitHub auth
|
|
158
|
+
console.error('[git] Error reading GitHub token:', tokenError);
|
|
159
|
+
}
|
|
160
|
+
console.log('[git] Executing git push...');
|
|
161
|
+
// Use -u to set upstream tracking if not already set (harmless if already configured)
|
|
162
|
+
const { stdout } = await runGit(['push', '-u', 'origin', 'HEAD'], { timeout: 30000 }); // 30 second timeout for push
|
|
163
|
+
console.log('[git] Push completed successfully');
|
|
138
164
|
return stdout;
|
|
139
165
|
}
|
|
140
166
|
export async function pullCurrent(strategy = 'rebase') {
|
|
@@ -232,6 +258,18 @@ export async function setRemoteUrl(remoteUrl) {
|
|
|
232
258
|
await runGit(['remote', 'add', 'origin', trimmed]);
|
|
233
259
|
}
|
|
234
260
|
}
|
|
261
|
+
export async function removeRemote() {
|
|
262
|
+
// Check if origin exists
|
|
263
|
+
try {
|
|
264
|
+
await runGit(['config', '--get', 'remote.origin.url']);
|
|
265
|
+
// If we get here, origin exists, so remove it
|
|
266
|
+
await runGit(['remote', 'remove', 'origin']);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// Origin doesn't exist, nothing to do
|
|
270
|
+
throw new Error('No remote configured');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
235
273
|
export async function storeCredentials(username, password, remoteUrl) {
|
|
236
274
|
// If a remote URL is provided, set it first
|
|
237
275
|
if (remoteUrl && remoteUrl.trim()) {
|
|
@@ -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,15 @@ function buildCommand(agent, instruction, config) {
|
|
|
42
45
|
: baseCmd;
|
|
43
46
|
break;
|
|
44
47
|
}
|
|
48
|
+
case 'droid': {
|
|
49
|
+
// For Factory Droid, use exec mode with --auto high for non-interactive scheduled jobs
|
|
50
|
+
// See: https://docs.factory.ai/reference/cli-reference#autonomy-levels
|
|
51
|
+
const mcpFlags = config.mcpServers && config.mcpServers.length > 0
|
|
52
|
+
? config.mcpServers.map(server => `--mcp ${shellQuote(server)}`).join(' ')
|
|
53
|
+
: '';
|
|
54
|
+
mainCommand = `droid exec --auto high ${mcpFlags} ${quotedInstruction}`.trim();
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
45
57
|
case 'claude':
|
|
46
58
|
default: {
|
|
47
59
|
// Claude uses .mcp.json for MCP server configuration (no --mcp flag)
|
|
@@ -55,12 +67,14 @@ function buildCommand(agent, instruction, config) {
|
|
|
55
67
|
const CLI_AGENT_LABEL = {
|
|
56
68
|
claude: 'Claude',
|
|
57
69
|
gemini: 'Gemini',
|
|
58
|
-
codex: '
|
|
70
|
+
codex: 'Codex',
|
|
71
|
+
droid: 'Factory Droid'
|
|
59
72
|
};
|
|
60
73
|
const CLI_AGENT_BINARY = {
|
|
61
74
|
claude: 'claude',
|
|
62
75
|
gemini: 'gemini',
|
|
63
|
-
codex: 'codex'
|
|
76
|
+
codex: 'codex',
|
|
77
|
+
droid: 'droid'
|
|
64
78
|
};
|
|
65
79
|
const DEFAULT_MAX_RUNTIME_MS = 30 * 60 * 1000; // 30 minutes
|
|
66
80
|
function getMaxRuntime() {
|
|
@@ -223,9 +237,14 @@ export class JobRunner {
|
|
|
223
237
|
});
|
|
224
238
|
return;
|
|
225
239
|
}
|
|
240
|
+
// Inject API keys from global config into child process environment
|
|
241
|
+
const injectedEnv = getInjectedEnv();
|
|
226
242
|
const child = spawn('bash', ['-lc', shellCommand], {
|
|
227
243
|
cwd: projectRoot,
|
|
228
|
-
env:
|
|
244
|
+
env: {
|
|
245
|
+
...process.env,
|
|
246
|
+
...injectedEnv
|
|
247
|
+
},
|
|
229
248
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
230
249
|
});
|
|
231
250
|
const maxRuntime = getMaxRuntime();
|
|
@@ -26,7 +26,18 @@ export class JobScheduler {
|
|
|
26
26
|
return;
|
|
27
27
|
const jobs = await this.store.listJobs();
|
|
28
28
|
for (const job of jobs) {
|
|
29
|
-
|
|
29
|
+
// Skip corrupted jobs (jobs with _error field)
|
|
30
|
+
if (job._error) {
|
|
31
|
+
console.warn(`Skipping corrupted job ${job.id}: ${job._error}`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
await this.register(job);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.error(`Failed to register job ${job.id}:`, error);
|
|
39
|
+
// Continue with other jobs even if one fails
|
|
40
|
+
}
|
|
30
41
|
}
|
|
31
42
|
this.initialized = true;
|
|
32
43
|
}
|
|
@@ -8,6 +8,7 @@ export declare class JobStore {
|
|
|
8
8
|
private getJobFilePath;
|
|
9
9
|
listJobs(): Promise<ScheduledJob[]>;
|
|
10
10
|
getJob(id: string): Promise<ScheduledJob | null>;
|
|
11
|
+
private createErrorJob;
|
|
11
12
|
saveJob(job: ScheduledJob, bodyContent?: string): Promise<void>;
|
|
12
13
|
deleteJob(id: string): Promise<boolean>;
|
|
13
14
|
appendRun(jobId: string, run: ScheduledJobRun): Promise<ScheduledJob>;
|
|
@@ -90,15 +90,53 @@ export class JobStore {
|
|
|
90
90
|
try {
|
|
91
91
|
const filePath = await this.getJobFilePath(id);
|
|
92
92
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
93
|
+
// Handle empty files
|
|
94
|
+
if (!content || content.trim().length === 0) {
|
|
95
|
+
return this.createErrorJob(id, 'Job file is empty');
|
|
96
|
+
}
|
|
93
97
|
const { data, content: body } = matter(content);
|
|
98
|
+
// Validate that we have at least an id field
|
|
99
|
+
if (!data || typeof data !== 'object' || !data.id) {
|
|
100
|
+
return this.createErrorJob(id, 'Job file is missing required id field');
|
|
101
|
+
}
|
|
94
102
|
return this.fromFrontmatter(data, body);
|
|
95
103
|
}
|
|
96
104
|
catch (error) {
|
|
97
105
|
if (error?.code === 'ENOENT')
|
|
98
106
|
return null;
|
|
99
|
-
|
|
107
|
+
// Handle parsing errors (e.g., invalid YAML, corrupted frontmatter)
|
|
108
|
+
const errorMessage = error?.message || 'Unknown error parsing job file';
|
|
109
|
+
return this.createErrorJob(id, errorMessage);
|
|
100
110
|
}
|
|
101
111
|
}
|
|
112
|
+
createErrorJob(id, errorMessage) {
|
|
113
|
+
const now = new Date();
|
|
114
|
+
return {
|
|
115
|
+
id,
|
|
116
|
+
name: id,
|
|
117
|
+
description: undefined,
|
|
118
|
+
prompt: '',
|
|
119
|
+
model: 'anthropic/claude-sonnet-4',
|
|
120
|
+
status: 'paused',
|
|
121
|
+
schedule: {
|
|
122
|
+
type: 'interval',
|
|
123
|
+
hours: 6,
|
|
124
|
+
daysOfWeek: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday']
|
|
125
|
+
},
|
|
126
|
+
metadata: {
|
|
127
|
+
createdAt: now,
|
|
128
|
+
updatedAt: now,
|
|
129
|
+
lastRunAt: undefined,
|
|
130
|
+
nextRunAt: undefined,
|
|
131
|
+
},
|
|
132
|
+
runs: [],
|
|
133
|
+
tags: [],
|
|
134
|
+
contextPaths: [],
|
|
135
|
+
// Store error in a way that won't break serialization
|
|
136
|
+
// We'll use a special tag to mark error jobs
|
|
137
|
+
_error: errorMessage
|
|
138
|
+
};
|
|
139
|
+
}
|
|
102
140
|
async saveJob(job, bodyContent = '') {
|
|
103
141
|
const filePath = await this.getJobFilePath(job.id);
|
|
104
142
|
const normalizedJob = {
|
|
@@ -138,34 +176,118 @@ export class JobStore {
|
|
|
138
176
|
if (!data?.id) {
|
|
139
177
|
throw new Error('Scheduled job is missing required id field');
|
|
140
178
|
}
|
|
179
|
+
// Validate and sanitize fields to prevent issues with corrupted data
|
|
141
180
|
const createdAt = toDate(data.metadata?.createdAt) ?? new Date();
|
|
142
181
|
const updatedAt = toDate(data.metadata?.updatedAt) ?? createdAt;
|
|
182
|
+
// Validate schedule structure
|
|
183
|
+
let schedule;
|
|
184
|
+
if (data.schedule && typeof data.schedule === 'object') {
|
|
185
|
+
if (data.schedule.type === 'cron') {
|
|
186
|
+
const cronSchedule = data.schedule;
|
|
187
|
+
if (typeof cronSchedule.expression === 'string' && cronSchedule.expression.trim()) {
|
|
188
|
+
schedule = {
|
|
189
|
+
type: 'cron',
|
|
190
|
+
expression: cronSchedule.expression.trim(),
|
|
191
|
+
timezone: typeof cronSchedule.timezone === 'string' ? cronSchedule.timezone : undefined,
|
|
192
|
+
description: typeof cronSchedule.description === 'string' ? cronSchedule.description : undefined,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// Invalid cron schedule, fall back to default
|
|
197
|
+
schedule = {
|
|
198
|
+
type: 'interval',
|
|
199
|
+
hours: 6,
|
|
200
|
+
daysOfWeek: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday']
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
const intervalSchedule = data.schedule;
|
|
206
|
+
const hours = typeof intervalSchedule.hours === 'number' && intervalSchedule.hours >= 1
|
|
207
|
+
? intervalSchedule.hours
|
|
208
|
+
: 6;
|
|
209
|
+
const daysOfWeek = Array.isArray(intervalSchedule.daysOfWeek)
|
|
210
|
+
? intervalSchedule.daysOfWeek.filter((day) => typeof day === 'string' && ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'].includes(day.toLowerCase()))
|
|
211
|
+
: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'];
|
|
212
|
+
schedule = {
|
|
213
|
+
type: 'interval',
|
|
214
|
+
hours,
|
|
215
|
+
daysOfWeek: daysOfWeek.length > 0 ? daysOfWeek : ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
|
|
216
|
+
anchorHour: typeof intervalSchedule.anchorHour === 'number' && intervalSchedule.anchorHour >= 0 && intervalSchedule.anchorHour <= 23
|
|
217
|
+
? intervalSchedule.anchorHour
|
|
218
|
+
: undefined,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
schedule = {
|
|
224
|
+
type: 'interval',
|
|
225
|
+
hours: 6,
|
|
226
|
+
daysOfWeek: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday']
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
// Validate and sanitize runs
|
|
143
230
|
const runs = Array.isArray(data.runs)
|
|
144
|
-
? data.runs
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
231
|
+
? data.runs
|
|
232
|
+
.filter((run) => run && typeof run === 'object')
|
|
233
|
+
.map((run) => {
|
|
234
|
+
const startedAt = toDate(run.startedAt);
|
|
235
|
+
if (!startedAt) {
|
|
236
|
+
// Skip runs with invalid dates
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
id: typeof run.id === 'string' ? run.id : `run-${randomUUID()}`,
|
|
241
|
+
jobId: data.id,
|
|
242
|
+
trigger: (run.trigger === 'manual' || run.trigger === 'scheduled') ? run.trigger : 'manual',
|
|
243
|
+
status: (['pending', 'running', 'succeeded', 'failed'].includes(run.status)) ? run.status : 'pending',
|
|
244
|
+
startedAt,
|
|
245
|
+
finishedAt: toDate(run.finishedAt),
|
|
246
|
+
outputPath: typeof run.outputPath === 'string' ? run.outputPath : undefined,
|
|
247
|
+
summary: typeof run.summary === 'string' ? run.summary : undefined,
|
|
248
|
+
error: typeof run.error === 'string' ? run.error : undefined,
|
|
249
|
+
cliCommand: typeof run.cliCommand === 'string' ? run.cliCommand : undefined,
|
|
250
|
+
};
|
|
251
|
+
})
|
|
252
|
+
.filter((run) => run !== null)
|
|
253
|
+
: [];
|
|
254
|
+
// Validate and sanitize string fields
|
|
255
|
+
const name = typeof data.name === 'string' && data.name.trim()
|
|
256
|
+
? data.name.trim().slice(0, 500) // Limit length
|
|
257
|
+
: data.id;
|
|
258
|
+
const description = typeof data.description === 'string'
|
|
259
|
+
? data.description.slice(0, 1000) // Limit length
|
|
260
|
+
: undefined;
|
|
261
|
+
const prompt = typeof data.prompt === 'string'
|
|
262
|
+
? data.prompt.trim() || body.trim()
|
|
263
|
+
: body.trim();
|
|
264
|
+
const model = typeof data.model === 'string' && data.model.trim()
|
|
265
|
+
? data.model.trim()
|
|
266
|
+
: 'anthropic/claude-sonnet-4';
|
|
267
|
+
const status = (data.status === 'active' || data.status === 'paused')
|
|
268
|
+
? data.status
|
|
269
|
+
: 'paused';
|
|
270
|
+
// Validate arrays
|
|
271
|
+
const tags = Array.isArray(data.tags)
|
|
272
|
+
? data.tags.filter((tag) => typeof tag === 'string').slice(0, 50)
|
|
273
|
+
: [];
|
|
274
|
+
const contextPaths = Array.isArray(data.contextPaths)
|
|
275
|
+
? data.contextPaths.filter((path) => typeof path === 'string').slice(0, 100)
|
|
156
276
|
: [];
|
|
277
|
+
const agentIds = Array.isArray(data.agentIds)
|
|
278
|
+
? data.agentIds.filter((id) => typeof id === 'string').slice(0, 50)
|
|
279
|
+
: undefined;
|
|
280
|
+
const mcpServers = Array.isArray(data.mcpServers)
|
|
281
|
+
? data.mcpServers.filter((server) => typeof server === 'string').slice(0, 50)
|
|
282
|
+
: undefined;
|
|
157
283
|
return {
|
|
158
284
|
id: data.id,
|
|
159
|
-
name
|
|
160
|
-
description
|
|
161
|
-
prompt
|
|
162
|
-
model
|
|
163
|
-
status
|
|
164
|
-
schedule
|
|
165
|
-
type: 'interval',
|
|
166
|
-
hours: 6,
|
|
167
|
-
daysOfWeek: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday']
|
|
168
|
-
},
|
|
285
|
+
name,
|
|
286
|
+
description,
|
|
287
|
+
prompt,
|
|
288
|
+
model,
|
|
289
|
+
status,
|
|
290
|
+
schedule,
|
|
169
291
|
metadata: {
|
|
170
292
|
createdAt,
|
|
171
293
|
updatedAt,
|
|
@@ -173,11 +295,11 @@ export class JobStore {
|
|
|
173
295
|
nextRunAt: toDate(data.metadata?.nextRunAt),
|
|
174
296
|
},
|
|
175
297
|
runs,
|
|
176
|
-
tags
|
|
177
|
-
contextPaths
|
|
178
|
-
agentId: data.agentId,
|
|
179
|
-
agentIds
|
|
180
|
-
mcpServers
|
|
298
|
+
tags,
|
|
299
|
+
contextPaths,
|
|
300
|
+
agentId: typeof data.agentId === 'string' ? data.agentId : undefined,
|
|
301
|
+
agentIds,
|
|
302
|
+
mcpServers,
|
|
181
303
|
};
|
|
182
304
|
}
|
|
183
305
|
toFrontmatter(job) {
|
|
@@ -122,19 +122,28 @@ export class FileStorageAdapter {
|
|
|
122
122
|
// Never allow id to be updated to prevent overwriting other proposals
|
|
123
123
|
const { id: _, ...safeUpdates } = updates;
|
|
124
124
|
// Handle comments separately as they're stored at root level in file format
|
|
125
|
-
|
|
125
|
+
// Comments can come from either root level or metadata.comments
|
|
126
|
+
let commentsToStore = existing.metadata.comments || [];
|
|
127
|
+
if (safeUpdates.comments) {
|
|
128
|
+
// If comments are provided at root level, use them
|
|
129
|
+
commentsToStore = safeUpdates.comments;
|
|
130
|
+
}
|
|
131
|
+
else if (safeUpdates.metadata?.comments) {
|
|
132
|
+
// If comments are provided in metadata, use them
|
|
133
|
+
commentsToStore = safeUpdates.metadata.comments;
|
|
134
|
+
}
|
|
135
|
+
const updated = {
|
|
126
136
|
...existing,
|
|
127
137
|
...safeUpdates,
|
|
128
138
|
metadata: {
|
|
129
139
|
...existing.metadata,
|
|
130
140
|
...safeUpdates.metadata,
|
|
131
|
-
updatedAt: new Date()
|
|
132
|
-
|
|
141
|
+
updatedAt: new Date(),
|
|
142
|
+
comments: commentsToStore
|
|
143
|
+
},
|
|
144
|
+
// Store comments at the root level for the file format
|
|
145
|
+
comments: commentsToStore
|
|
133
146
|
};
|
|
134
|
-
// If comments are being updated, store them at the root level for the file format
|
|
135
|
-
if (safeUpdates.comments) {
|
|
136
|
-
updated.comments = safeUpdates.comments;
|
|
137
|
-
}
|
|
138
147
|
await this.createCP(updated);
|
|
139
148
|
}
|
|
140
149
|
async deleteCP(id) {
|
|
@@ -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
|
-
...
|
|
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`),
|
|
@@ -233,6 +233,26 @@ 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
|
+
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' });
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
return c.json({ success: false, message: err instanceof Error ? err.message : 'Network error' }, 200);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
236
256
|
// Other providers not wired up yet
|
|
237
257
|
return c.json({ success: false, message: `Provider '${provider}' test not implemented yet` }, 501);
|
|
238
258
|
}
|