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

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.
@@ -1,7 +1,7 @@
1
1
  import { Hono } from 'hono';
2
2
  import crypto from 'crypto';
3
- import { getRepoStatus, listBranches, createBranch, deleteBranch, switchBranch, mergeBranch, pushCurrent, pullCurrent, listWorktrees, addWorktree, removeWorktree, commitInWorktree, pushWorktree, pullWorktree, checkRemoteAuth, getCredentialConfig, storeCredentials, } from '../../../../lib/git.js';
4
- import { saveGithubToken, clearGithubToken } from '../../../../lib/github-token.js';
3
+ import { getRepoStatus, listBranches, createBranch, deleteBranch, switchBranch, mergeBranch, pushCurrent, pullCurrent, listWorktrees, addWorktree, removeWorktree, commitInWorktree, pushWorktree, pullWorktree, checkRemoteAuth, getCredentialConfig, storeCredentials, getCommitDetails, getFileDiff, setRemoteUrl, } from '../../../../lib/git.js';
4
+ import { saveGithubToken, clearGithubToken, readGithubToken, isGithubTokenValid } from '../../../../lib/github-token.js';
5
5
  import { createGithubAuthState, consumeGithubAuthState } from '../../../../lib/github-auth-state.js';
6
6
  import { resolveCoconutId, resolveControlPlaneUrl } from '../../../../lib/coconut-context.js';
7
7
  import { loadGitSettings, saveGitSettings, isGitAuthMode } from '../../../../lib/git-settings.js';
@@ -78,16 +78,50 @@ app.get('/credential-config', async (c) => {
78
78
  return c.json({ success: false, error: { message: e.message } }, 500);
79
79
  }
80
80
  });
81
+ // Set Remote URL
82
+ app.put('/remote', async (c) => {
83
+ try {
84
+ const body = await c.req.json();
85
+ const remoteUrl = String(body?.remoteUrl || '');
86
+ if (!remoteUrl) {
87
+ return c.json({ success: false, error: { message: 'Remote URL is required' } }, 400);
88
+ }
89
+ await setRemoteUrl(remoteUrl);
90
+ // If a GitHub token exists and is valid, store it in the credential helper now that we have a remote
91
+ try {
92
+ const tokenRecord = await readGithubToken();
93
+ if (tokenRecord && isGithubTokenValid(tokenRecord)) {
94
+ try {
95
+ await storeCredentials('x-access-token', tokenRecord.token);
96
+ console.log('[git] Successfully stored GitHub token in credential helper after remote creation');
97
+ }
98
+ catch (credError) {
99
+ console.warn('[git] Failed to store GitHub token in credential helper after remote creation:', credError);
100
+ // Don't fail the remote creation if credential storage fails
101
+ }
102
+ }
103
+ }
104
+ catch (tokenError) {
105
+ // Ignore token read errors - remote creation should still succeed
106
+ console.warn('[git] Could not check for GitHub token after remote creation:', tokenError);
107
+ }
108
+ return c.json({ success: true, data: { message: 'Remote URL set successfully' } });
109
+ }
110
+ catch (e) {
111
+ return c.json({ success: false, error: { message: e.message } }, 500);
112
+ }
113
+ });
81
114
  // Store Credentials
82
115
  app.post('/credentials', async (c) => {
83
116
  try {
84
117
  const body = await c.req.json();
85
118
  const username = String(body?.username || '');
86
119
  const password = String(body?.password || '');
120
+ const remoteUrl = body?.remoteUrl ? String(body.remoteUrl) : undefined;
87
121
  if (!username || !password) {
88
122
  return c.json({ success: false, error: { message: 'Username and password are required' } }, 400);
89
123
  }
90
- await storeCredentials(username, password);
124
+ await storeCredentials(username, password, remoteUrl);
91
125
  return c.json({ success: true, data: { message: 'Credentials stored successfully' } });
92
126
  }
93
127
  catch (e) {
@@ -162,11 +196,21 @@ app.post('/providers/github/token', async (c) => {
162
196
  }
163
197
  }
164
198
  const record = await saveGithubToken(token, expiresAt);
199
+ // Try to store credentials in the credential helper if a remote exists
165
200
  try {
166
201
  await storeCredentials('x-access-token', token);
202
+ console.log('[git] Successfully stored GitHub token in credential helper');
167
203
  }
168
204
  catch (credError) {
169
- console.warn('[git] Failed to install GitHub token into credential store:', credError);
205
+ // Check if the error is because no remote exists (expected case)
206
+ const errorMessage = credError?.message || '';
207
+ if (errorMessage.includes('No git remote configured')) {
208
+ console.log('[git] GitHub token saved, but no remote configured yet. Token will be stored in credential helper when remote is created.');
209
+ }
210
+ else {
211
+ console.warn('[git] Failed to install GitHub token into credential store:', credError);
212
+ }
213
+ // Don't fail token storage if credential helper setup fails - the token is still saved
170
214
  }
171
215
  return c.json({ success: true, data: { expiresAt: record.expiresAt } });
172
216
  }
@@ -340,6 +384,46 @@ app.post('/commits', async (c) => {
340
384
  return c.json({ success: false, error: { message: e.message } }, 500);
341
385
  }
342
386
  });
387
+ // Discard file changes
388
+ app.post('/discard', async (c) => {
389
+ try {
390
+ const body = await c.req.json();
391
+ const filePath = String(body?.file || body?.path || '');
392
+ if (!filePath) {
393
+ return c.json({ success: false, error: { message: 'file path required' } }, 400);
394
+ }
395
+ const { runGit, getRepoStatus } = await import('../../../../lib/git.js');
396
+ // Get current status to determine file state
397
+ const status = await getRepoStatus();
398
+ const fileChange = status.changes.find((c) => c.path === filePath);
399
+ if (!fileChange) {
400
+ return c.json({ success: false, error: { message: 'File not found in uncommitted changes' } }, 404);
401
+ }
402
+ const statusCode = fileChange.status.trim();
403
+ // Handle different file statuses
404
+ if (statusCode === '??' || statusCode.includes('U')) {
405
+ // Untracked file - remove it
406
+ const { unlink } = await import('fs/promises');
407
+ const { getRepoRoot } = await import('../../../../lib/git.js');
408
+ const repoRoot = await getRepoRoot();
409
+ const { join } = await import('path');
410
+ const fullPath = join(repoRoot, filePath);
411
+ await unlink(fullPath);
412
+ }
413
+ else if (statusCode.includes('D')) {
414
+ // Deleted file - restore it
415
+ await runGit(['checkout', '--', filePath]);
416
+ }
417
+ else {
418
+ // Modified or Added file - discard changes
419
+ await runGit(['checkout', '--', filePath]);
420
+ }
421
+ return c.json({ success: true, data: { file: filePath, status: statusCode } });
422
+ }
423
+ catch (e) {
424
+ return c.json({ success: false, error: { message: e.message } }, 500);
425
+ }
426
+ });
343
427
  // Push / Pull (current)
344
428
  app.post('/push', async (c) => {
345
429
  try {
@@ -523,4 +607,31 @@ app.post('/worktrees/:name/pull', async (c) => {
523
607
  return c.json({ success: false, error: { message: e.message } }, 500);
524
608
  }
525
609
  });
610
+ // Get commit details
611
+ app.get('/commits/:sha', async (c) => {
612
+ try {
613
+ const sha = c.req.param('sha');
614
+ const details = await getCommitDetails(sha);
615
+ return c.json({ success: true, data: details });
616
+ }
617
+ catch (e) {
618
+ return c.json({ success: false, error: { message: e.message || 'Failed to get commit details' } }, 500);
619
+ }
620
+ });
621
+ // Get file diff for a specific commit
622
+ app.get('/commits/:sha/files/:filepath{.+}', async (c) => {
623
+ try {
624
+ const sha = c.req.param('sha');
625
+ const filepath = c.req.param('filepath');
626
+ if (!filepath) {
627
+ return c.json({ success: false, error: { message: 'filepath is required' } }, 400);
628
+ }
629
+ const diff = await getFileDiff(sha, filepath);
630
+ // Return as plain text for easier viewing
631
+ return c.text(diff);
632
+ }
633
+ catch (e) {
634
+ return c.json({ success: false, error: { message: e.message || 'Failed to get file diff' } }, 500);
635
+ }
636
+ });
526
637
  export default app;
@@ -47,6 +47,9 @@ 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
+ agentIds?: string[];
52
+ mcpServers?: string[];
50
53
  };
51
54
  }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
52
55
  success: false;
@@ -108,6 +111,9 @@ export declare function PATCH(c: Context): Promise<(Response & import("hono").Ty
108
111
  }[];
109
112
  tags?: string[];
110
113
  contextPaths?: string[];
114
+ agentId?: string;
115
+ agentIds?: string[];
116
+ mcpServers?: string[];
111
117
  };
112
118
  }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
113
119
  success: false;
@@ -82,6 +82,15 @@ 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
+ agentIds: body.agentIds !== undefined
89
+ ? (Array.isArray(body.agentIds) ? body.agentIds.filter((s) => typeof s === 'string') : undefined)
90
+ : existing.agentIds,
91
+ mcpServers: body.mcpServers !== undefined
92
+ ? (Array.isArray(body.mcpServers) ? body.mcpServers.filter((s) => typeof s === 'string') : undefined)
93
+ : existing.mcpServers,
85
94
  metadata: {
86
95
  ...existing.metadata,
87
96
  updatedAt: new Date()
@@ -0,0 +1,14 @@
1
+ import { Context } from 'hono';
2
+ export declare function GET(c: Context): Promise<(Response & import("hono").TypedResponse<string, import("hono/utils/http-status").ContentfulStatusCode, "text">) | (Response & import("hono").TypedResponse<{
3
+ success: false;
4
+ error: {
5
+ code: string;
6
+ message: string;
7
+ };
8
+ }, 404, "json">) | (Response & import("hono").TypedResponse<{
9
+ success: false;
10
+ error: {
11
+ code: string;
12
+ message: any;
13
+ };
14
+ }, 500, "json">)>;
@@ -0,0 +1,64 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import { JobStore } from '../../../../../../../../lib/jobs/job-store.js';
4
+ import { getProjectRoot } from '../../../../../../../../lib/project-paths.js';
5
+ const store = new JobStore();
6
+ export async function GET(c) {
7
+ try {
8
+ const jobId = c.req.param('id');
9
+ const runId = c.req.param('runId');
10
+ const job = await store.getJob(jobId);
11
+ if (!job) {
12
+ return c.json({
13
+ success: false,
14
+ error: {
15
+ code: 'JOB_NOT_FOUND',
16
+ message: `Job ${jobId} not found`
17
+ }
18
+ }, 404);
19
+ }
20
+ const run = job.runs.find((r) => r.id === runId);
21
+ if (!run) {
22
+ return c.json({
23
+ success: false,
24
+ error: {
25
+ code: 'RUN_NOT_FOUND',
26
+ message: `Run ${runId} not found for job ${jobId}`
27
+ }
28
+ }, 404);
29
+ }
30
+ // Construct log path - use outputPath from run if available, otherwise construct it
31
+ const projectRoot = await getProjectRoot();
32
+ const logPath = run.outputPath
33
+ ? path.join(projectRoot, run.outputPath)
34
+ : path.join(projectRoot, '.nut', 'jobs', 'logs', jobId, `${runId}.log`);
35
+ try {
36
+ const logContent = await fs.readFile(logPath, 'utf-8');
37
+ // Return as plain text for easier viewing
38
+ return c.text(logContent);
39
+ }
40
+ catch (fileError) {
41
+ if (fileError?.code === 'ENOENT') {
42
+ return c.json({
43
+ success: false,
44
+ error: {
45
+ code: 'LOG_NOT_FOUND',
46
+ message: `Log file not found for run ${runId}`,
47
+ logPath: run.outputPath || `(expected at ${logPath})`
48
+ }
49
+ }, 404);
50
+ }
51
+ throw fileError;
52
+ }
53
+ }
54
+ catch (error) {
55
+ console.error('Failed to fetch job run log:', error);
56
+ return c.json({
57
+ success: false,
58
+ error: {
59
+ code: 'GET_LOG_ERROR',
60
+ message: error?.message ?? 'Unknown error retrieving job run log'
61
+ }
62
+ }, 500);
63
+ }
64
+ }
@@ -0,0 +1,28 @@
1
+ import { Context } from 'hono';
2
+ export declare function GET(c: Context): Promise<(Response & import("hono").TypedResponse<{
3
+ success: false;
4
+ error: {
5
+ code: string;
6
+ message: string;
7
+ };
8
+ }, 404, "json">) | (Response & import("hono").TypedResponse<{
9
+ success: true;
10
+ data: {
11
+ id: string;
12
+ jobId: string;
13
+ trigger: import("@lovelybunch/types").ScheduledJobTrigger;
14
+ status: import("@lovelybunch/types").ScheduledJobRunStatus;
15
+ startedAt: string;
16
+ finishedAt?: string;
17
+ outputPath?: string;
18
+ summary?: string;
19
+ error?: string;
20
+ cliCommand?: string;
21
+ };
22
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
23
+ success: false;
24
+ error: {
25
+ code: string;
26
+ message: any;
27
+ };
28
+ }, 500, "json">)>;
@@ -0,0 +1,39 @@
1
+ import { JobStore } from '../../../../../../../lib/jobs/job-store.js';
2
+ const store = new JobStore();
3
+ export async function GET(c) {
4
+ try {
5
+ const jobId = c.req.param('id');
6
+ const runId = c.req.param('runId');
7
+ const job = await store.getJob(jobId);
8
+ if (!job) {
9
+ return c.json({
10
+ success: false,
11
+ error: {
12
+ code: 'JOB_NOT_FOUND',
13
+ message: `Job ${jobId} not found`
14
+ }
15
+ }, 404);
16
+ }
17
+ const run = job.runs.find((r) => r.id === runId);
18
+ if (!run) {
19
+ return c.json({
20
+ success: false,
21
+ error: {
22
+ code: 'RUN_NOT_FOUND',
23
+ message: `Run ${runId} not found for job ${jobId}`
24
+ }
25
+ }, 404);
26
+ }
27
+ return c.json({ success: true, data: run });
28
+ }
29
+ catch (error) {
30
+ console.error('Failed to fetch job run:', error);
31
+ return c.json({
32
+ success: false,
33
+ error: {
34
+ code: 'GET_RUN_ERROR',
35
+ message: error?.message ?? 'Unknown error retrieving job run'
36
+ }
37
+ }, 500);
38
+ }
39
+ }
@@ -2,6 +2,8 @@ import { Hono } from 'hono';
2
2
  import { GET, POST } from './route.js';
3
3
  import { GET as getJob, PATCH as patchJob, DELETE as deleteJob } from './[id]/route.js';
4
4
  import { POST as runJob } from './[id]/run/route.js';
5
+ import { GET as getRun } from './[id]/runs/[runId]/route.js';
6
+ import { GET as getRunLog } from './[id]/runs/[runId]/log/route.js';
5
7
  import { GET as getStatus } from './status/route.js';
6
8
  const jobs = new Hono();
7
9
  jobs.get('/', GET);
@@ -11,4 +13,6 @@ jobs.get('/:id', getJob);
11
13
  jobs.patch('/:id', patchJob);
12
14
  jobs.delete('/:id', deleteJob);
13
15
  jobs.post('/:id/run', runJob);
16
+ jobs.get('/:id/runs/:runId', getRun);
17
+ jobs.get('/:id/runs/:runId/log', getRunLog);
14
18
  export default jobs;
@@ -43,6 +43,9 @@ 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
+ agentIds?: string[];
48
+ mcpServers?: string[];
46
49
  }[];
47
50
  }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
48
51
  success: false;
@@ -98,6 +101,9 @@ export declare function POST(c: Context): Promise<(Response & import("hono").Typ
98
101
  }[];
99
102
  tags?: string[];
100
103
  contextPaths?: string[];
104
+ agentId?: string;
105
+ agentIds?: string[];
106
+ mcpServers?: string[];
101
107
  };
102
108
  }, 201, "json">) | (Response & import("hono").TypedResponse<{
103
109
  success: false;
@@ -122,6 +122,9 @@ 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
+ agentIds: Array.isArray(body.agentIds) ? body.agentIds.filter((s) => typeof s === 'string') : undefined,
127
+ mcpServers: Array.isArray(body.mcpServers) ? body.mcpServers.filter((s) => typeof s === 'string') : undefined,
125
128
  };
126
129
  await store.saveJob(job);
127
130
  await scheduler.refresh(job.id);
@@ -1,5 +1,8 @@
1
1
  import { Hono } from 'hono';
2
+ import { promises as fs } from 'fs';
3
+ import path from 'path';
2
4
  import { loadUserSettings, saveUserSettings } from '../../../../../lib/user-preferences.js';
5
+ import { findGaitDirectory } from '../../../../../lib/gait-path.js';
3
6
  const app = new Hono();
4
7
  /**
5
8
  * GET /api/v1/user/settings
@@ -22,9 +25,9 @@ app.get('/', async (c) => {
22
25
  app.put('/', async (c) => {
23
26
  try {
24
27
  const body = await c.req.json();
25
- const { profile, preferences } = body;
26
- if (!profile && !preferences) {
27
- return c.json({ success: false, error: 'Profile or preferences data required' }, 400);
28
+ const { profile, preferences, coconut } = body;
29
+ if (!profile && !preferences && !coconut) {
30
+ return c.json({ success: false, error: 'Profile, preferences, or coconut data required' }, 400);
28
31
  }
29
32
  let settings = await loadUserSettings();
30
33
  if (profile) {
@@ -41,6 +44,43 @@ app.put('/', async (c) => {
41
44
  };
42
45
  }
43
46
  await saveUserSettings(settings);
47
+ // If coconut.id is provided, update .nut/config.json
48
+ if (coconut?.id) {
49
+ try {
50
+ const gaitDir = await findGaitDirectory();
51
+ if (gaitDir) {
52
+ const configPath = path.join(gaitDir, 'config.json');
53
+ // Load existing config
54
+ let config = {};
55
+ try {
56
+ const content = await fs.readFile(configPath, 'utf-8');
57
+ config = JSON.parse(content);
58
+ }
59
+ catch (error) {
60
+ if (error.code === 'ENOENT') {
61
+ config = {};
62
+ }
63
+ else {
64
+ throw error;
65
+ }
66
+ }
67
+ // Ensure .nut directory exists
68
+ const configDir = path.dirname(configPath);
69
+ await fs.mkdir(configDir, { recursive: true });
70
+ // Update coconut.id
71
+ if (!config.coconut) {
72
+ config.coconut = {};
73
+ }
74
+ config.coconut.id = coconut.id;
75
+ // Save config
76
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
77
+ }
78
+ }
79
+ catch (error) {
80
+ console.error('Failed to update coconut.id in config.json:', error);
81
+ // Don't fail the request if config update fails
82
+ }
83
+ }
44
84
  return c.json({ success: true, data: settings });
45
85
  }
46
86
  catch (error) {
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.11",
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.11",
40
+ "@lovelybunch/mcp": "^1.0.69-alpha.11",
41
+ "@lovelybunch/types": "^1.0.69-alpha.11",
42
42
  "arctic": "^1.9.2",
43
43
  "bcrypt": "^5.1.1",
44
44
  "cookie": "^0.6.0",