@lovelybunch/api 1.0.69-alpha.8 → 1.0.69

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.
Files changed (47) hide show
  1. package/dist/lib/auth/auth-manager.d.ts +10 -2
  2. package/dist/lib/auth/auth-manager.js +16 -5
  3. package/dist/lib/env-injection.d.ts +6 -0
  4. package/dist/lib/env-injection.js +64 -0
  5. package/dist/lib/git.d.ts +1 -0
  6. package/dist/lib/git.js +39 -1
  7. package/dist/lib/jobs/job-runner.js +22 -3
  8. package/dist/lib/jobs/job-scheduler.js +12 -1
  9. package/dist/lib/jobs/job-store.d.ts +1 -0
  10. package/dist/lib/jobs/job-store.js +150 -28
  11. package/dist/lib/storage/file-storage.js +16 -7
  12. package/dist/lib/terminal/terminal-manager.js +3 -2
  13. package/dist/lib/user-preferences.d.ts +1 -0
  14. package/dist/routes/api/v1/config/route.d.ts +1 -1
  15. package/dist/routes/api/v1/config/route.js +65 -1
  16. package/dist/routes/api/v1/context/knowledge/[filename]/route.js +18 -11
  17. package/dist/routes/api/v1/context/knowledge/route.js +5 -2
  18. package/dist/routes/api/v1/events/purge/route.d.ts +0 -2
  19. package/dist/routes/api/v1/events/purge/route.js +2 -14
  20. package/dist/routes/api/v1/events/route.d.ts +0 -2
  21. package/dist/routes/api/v1/events/route.js +2 -14
  22. package/dist/routes/api/v1/events/status/route.d.ts +0 -2
  23. package/dist/routes/api/v1/events/status/route.js +2 -14
  24. package/dist/routes/api/v1/events/stream/route.js +2 -14
  25. package/dist/routes/api/v1/git/index.js +66 -6
  26. package/dist/routes/api/v1/jobs/[id]/run/route.d.ts +2 -2
  27. package/dist/routes/api/v1/jobs/status/route.d.ts +1 -1
  28. package/dist/routes/api/v1/proposals/[id]/route.d.ts +8 -8
  29. package/dist/routes/api/v1/resources/[id]/route.js +11 -7
  30. package/dist/routes/api/v1/resources/generate/index.d.ts +3 -0
  31. package/dist/routes/api/v1/resources/generate/index.js +5 -0
  32. package/dist/routes/api/v1/resources/generate/route.d.ts +19 -0
  33. package/dist/routes/api/v1/resources/generate/route.js +257 -0
  34. package/dist/routes/api/v1/resources/index.js +2 -0
  35. package/dist/routes/api/v1/user/settings/route.js +43 -3
  36. package/dist/routes/api/v1/version/index.d.ts +3 -0
  37. package/dist/routes/api/v1/version/index.js +5 -0
  38. package/dist/routes/api/v1/version/route.d.ts +24 -0
  39. package/dist/routes/api/v1/version/route.js +51 -0
  40. package/dist/server-with-static.js +40 -23
  41. package/dist/server.js +40 -23
  42. package/package.json +5 -4
  43. package/static/assets/index-CfRmV6nM.css +33 -0
  44. package/static/assets/index-DzYTksNb.js +969 -0
  45. package/static/index.html +2 -2
  46. package/static/assets/index-CHBcfq10.css +0 -33
  47. package/static/assets/index-DgXTT9AK.js +0 -906
@@ -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
- constructor(dataPath: string);
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
- export declare function getAuthManager(dataPath?: string): AuthManager;
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
- constructor(dataPath) {
13
- this.authConfigPath = path.join(dataPath, '.nut', 'auth.json');
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
- export function getAuthManager(dataPath) {
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
- const path = dataPath || process.env.GAIT_DATA_PATH || process.cwd();
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
- const { stdout } = await runGit(['push'], { timeout: 30000 }); // 30 second timeout for push
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: 'Code'
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: process.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
- await this.register(job);
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
- throw error;
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.map((run) => ({
145
- id: run.id ?? `run-${randomUUID()}`,
146
- jobId: data.id,
147
- trigger: run.trigger,
148
- status: run.status,
149
- startedAt: toDate(run.startedAt) ?? new Date(),
150
- finishedAt: toDate(run.finishedAt),
151
- outputPath: run.outputPath,
152
- summary: run.summary,
153
- error: run.error,
154
- cliCommand: run.cliCommand,
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: data.name || data.id,
160
- description: data.description,
161
- prompt: data.prompt || body.trim(),
162
- model: data.model || 'anthropic/claude-sonnet-4',
163
- status: data.status || 'paused',
164
- schedule: data.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: data.tags ?? [],
177
- contextPaths: data.contextPaths ?? [],
178
- agentId: data.agentId,
179
- agentIds: data.agentIds,
180
- mcpServers: data.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
- let updated = {
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
- ...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`),
@@ -4,6 +4,7 @@ export interface UserProfile {
4
4
  lastName?: string;
5
5
  email?: string;
6
6
  role?: AgentRoleId;
7
+ roleDescription?: string;
7
8
  }
8
9
  export interface UserPreferences {
9
10
  theme?: 'light' | 'dark' | 'coconut' | 'system';
@@ -16,7 +16,7 @@ export declare function PUT(c: Context): Promise<(Response & import("hono").Type
16
16
  error: string;
17
17
  }, 500, "json">) | (Response & import("hono").TypedResponse<{
18
18
  error: string;
19
- }, 405, "json">)>;
19
+ }, 404, "json">)>;
20
20
  export declare function TEST(c: Context): Promise<(Response & import("hono").TypedResponse<{
21
21
  success: false;
22
22
  message: string;