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

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/git.d.ts CHANGED
@@ -63,4 +63,31 @@ export declare function commitInWorktree(name: string, message: string, files?:
63
63
  }>;
64
64
  export declare function pushWorktree(name: string): Promise<string>;
65
65
  export declare function pullWorktree(name: string, strategy?: PullStrategy): Promise<string>;
66
+ export interface CommitFileChange {
67
+ path: string;
68
+ status: string;
69
+ insertions: number;
70
+ deletions: number;
71
+ }
72
+ export interface CommitDetails {
73
+ sha: string;
74
+ message: string;
75
+ author: {
76
+ name: string;
77
+ email: string;
78
+ date: string;
79
+ };
80
+ committer: {
81
+ name: string;
82
+ email: string;
83
+ date: string;
84
+ };
85
+ branch?: string;
86
+ filesChanged: number;
87
+ insertions: number;
88
+ deletions: number;
89
+ files: CommitFileChange[];
90
+ }
91
+ export declare function getCommitDetails(sha: string): Promise<CommitDetails>;
92
+ export declare function getFileDiff(sha: string, filepath: string): Promise<string>;
66
93
  export {};
package/dist/lib/git.js CHANGED
@@ -366,3 +366,114 @@ export async function pullWorktree(name, strategy = 'rebase') {
366
366
  const { stdout } = await runGit(args, { timeout: 30000 }); // 30 second timeout
367
367
  return stdout;
368
368
  }
369
+ export async function getCommitDetails(sha) {
370
+ // Get commit metadata
371
+ const { stdout: metaOutput } = await runGit([
372
+ 'show',
373
+ '--format=%H%n%an%n%ae%n%aI%n%cn%n%ce%n%cI%n%B',
374
+ '--no-patch',
375
+ sha
376
+ ]);
377
+ const lines = metaOutput.trim().split('\n');
378
+ const commitSha = lines[0];
379
+ const authorName = lines[1];
380
+ const authorEmail = lines[2];
381
+ const authorDate = lines[3];
382
+ const committerName = lines[4];
383
+ const committerEmail = lines[5];
384
+ const committerDate = lines[6];
385
+ const message = lines.slice(7).join('\n').trim();
386
+ // Get file changes with status
387
+ const { stdout: filesOutput } = await runGit([
388
+ 'diff-tree',
389
+ '--no-commit-id',
390
+ '--name-status',
391
+ '-r',
392
+ sha
393
+ ]);
394
+ // Get numstat for insertions/deletions per file
395
+ const { stdout: numstatOutput } = await runGit([
396
+ 'show',
397
+ '--numstat',
398
+ '--format=',
399
+ sha
400
+ ]);
401
+ // Parse file changes
402
+ const fileStatusMap = new Map();
403
+ filesOutput.trim().split('\n').filter(Boolean).forEach((line) => {
404
+ const [status, ...pathParts] = line.split('\t');
405
+ const path = pathParts.join('\t'); // Handle filenames with tabs
406
+ fileStatusMap.set(path, status);
407
+ });
408
+ const files = [];
409
+ let totalInsertions = 0;
410
+ let totalDeletions = 0;
411
+ numstatOutput.trim().split('\n').filter(Boolean).forEach((line) => {
412
+ const parts = line.split('\t');
413
+ if (parts.length >= 3) {
414
+ const insertions = parts[0] === '-' ? 0 : parseInt(parts[0], 10);
415
+ const deletions = parts[1] === '-' ? 0 : parseInt(parts[1], 10);
416
+ const path = parts.slice(2).join('\t');
417
+ const status = fileStatusMap.get(path) || 'M';
418
+ files.push({
419
+ path,
420
+ status,
421
+ insertions,
422
+ deletions
423
+ });
424
+ totalInsertions += insertions;
425
+ totalDeletions += deletions;
426
+ }
427
+ });
428
+ // Try to get branch name (may not always be available)
429
+ let branch;
430
+ try {
431
+ const { stdout: branchOutput } = await runGit([
432
+ 'branch',
433
+ '--contains',
434
+ sha,
435
+ '--format=%(refname:short)'
436
+ ]);
437
+ const branches = branchOutput.trim().split('\n').filter(Boolean);
438
+ branch = branches[0]; // Use first branch that contains this commit
439
+ }
440
+ catch {
441
+ // Branch info not available
442
+ }
443
+ return {
444
+ sha: commitSha,
445
+ message,
446
+ author: {
447
+ name: authorName,
448
+ email: authorEmail,
449
+ date: authorDate
450
+ },
451
+ committer: {
452
+ name: committerName,
453
+ email: committerEmail,
454
+ date: committerDate
455
+ },
456
+ branch,
457
+ filesChanged: files.length,
458
+ insertions: totalInsertions,
459
+ deletions: totalDeletions,
460
+ files
461
+ };
462
+ }
463
+ export async function getFileDiff(sha, filepath) {
464
+ const { stdout } = await runGit([
465
+ 'show',
466
+ `${sha}:${filepath}`,
467
+ '--',
468
+ filepath
469
+ ]);
470
+ // Get the actual diff for this file
471
+ const { stdout: diffOutput } = await runGit([
472
+ 'show',
473
+ '--format=',
474
+ sha,
475
+ '--',
476
+ filepath
477
+ ]);
478
+ return diffOutput;
479
+ }
@@ -176,6 +176,7 @@ export class JobStore {
176
176
  tags: data.tags ?? [],
177
177
  contextPaths: data.contextPaths ?? [],
178
178
  agentId: data.agentId,
179
+ agentIds: data.agentIds,
179
180
  mcpServers: data.mcpServers,
180
181
  };
181
182
  }
@@ -209,6 +210,7 @@ export class JobStore {
209
210
  tags: job.tags ?? [],
210
211
  contextPaths: job.contextPaths ?? [],
211
212
  agentId: job.agentId,
213
+ agentIds: job.agentIds,
212
214
  mcpServers: job.mcpServers,
213
215
  };
214
216
  }
@@ -1,6 +1,6 @@
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';
3
+ import { getRepoStatus, listBranches, createBranch, deleteBranch, switchBranch, mergeBranch, pushCurrent, pullCurrent, listWorktrees, addWorktree, removeWorktree, commitInWorktree, pushWorktree, pullWorktree, checkRemoteAuth, getCredentialConfig, storeCredentials, getCommitDetails, getFileDiff, } from '../../../../lib/git.js';
4
4
  import { saveGithubToken, clearGithubToken } 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';
@@ -523,4 +523,31 @@ app.post('/worktrees/:name/pull', async (c) => {
523
523
  return c.json({ success: false, error: { message: e.message } }, 500);
524
524
  }
525
525
  });
526
+ // Get commit details
527
+ app.get('/commits/:sha', async (c) => {
528
+ try {
529
+ const sha = c.req.param('sha');
530
+ const details = await getCommitDetails(sha);
531
+ return c.json({ success: true, data: details });
532
+ }
533
+ catch (e) {
534
+ return c.json({ success: false, error: { message: e.message || 'Failed to get commit details' } }, 500);
535
+ }
536
+ });
537
+ // Get file diff for a specific commit
538
+ app.get('/commits/:sha/files/:filepath{.+}', async (c) => {
539
+ try {
540
+ const sha = c.req.param('sha');
541
+ const filepath = c.req.param('filepath');
542
+ if (!filepath) {
543
+ return c.json({ success: false, error: { message: 'filepath is required' } }, 400);
544
+ }
545
+ const diff = await getFileDiff(sha, filepath);
546
+ // Return as plain text for easier viewing
547
+ return c.text(diff);
548
+ }
549
+ catch (e) {
550
+ return c.json({ success: false, error: { message: e.message || 'Failed to get file diff' } }, 500);
551
+ }
552
+ });
526
553
  export default app;
@@ -48,6 +48,7 @@ export declare function GET(c: Context): Promise<(Response & import("hono").Type
48
48
  tags?: string[];
49
49
  contextPaths?: string[];
50
50
  agentId?: string;
51
+ agentIds?: string[];
51
52
  mcpServers?: string[];
52
53
  };
53
54
  }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
@@ -111,6 +112,7 @@ export declare function PATCH(c: Context): Promise<(Response & import("hono").Ty
111
112
  tags?: string[];
112
113
  contextPaths?: string[];
113
114
  agentId?: string;
115
+ agentIds?: string[];
114
116
  mcpServers?: string[];
115
117
  };
116
118
  }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
@@ -85,6 +85,9 @@ export async function PATCH(c) {
85
85
  agentId: body.agentId !== undefined
86
86
  ? (typeof body.agentId === 'string' && body.agentId ? body.agentId : undefined)
87
87
  : existing.agentId,
88
+ agentIds: body.agentIds !== undefined
89
+ ? (Array.isArray(body.agentIds) ? body.agentIds.filter((s) => typeof s === 'string') : undefined)
90
+ : existing.agentIds,
88
91
  mcpServers: body.mcpServers !== undefined
89
92
  ? (Array.isArray(body.mcpServers) ? body.mcpServers.filter((s) => typeof s === 'string') : undefined)
90
93
  : existing.mcpServers,
@@ -0,0 +1,14 @@
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<string, import("hono/utils/http-status").ContentfulStatusCode, "text">) | (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;
@@ -44,6 +44,7 @@ export declare function GET(c: Context): Promise<(Response & import("hono").Type
44
44
  tags?: string[];
45
45
  contextPaths?: string[];
46
46
  agentId?: string;
47
+ agentIds?: string[];
47
48
  mcpServers?: string[];
48
49
  }[];
49
50
  }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
@@ -101,6 +102,7 @@ export declare function POST(c: Context): Promise<(Response & import("hono").Typ
101
102
  tags?: string[];
102
103
  contextPaths?: string[];
103
104
  agentId?: string;
105
+ agentIds?: string[];
104
106
  mcpServers?: string[];
105
107
  };
106
108
  }, 201, "json">) | (Response & import("hono").TypedResponse<{
@@ -123,6 +123,7 @@ export async function POST(c) {
123
123
  tags: Array.isArray(body.tags) ? body.tags : [],
124
124
  contextPaths: Array.isArray(body.contextPaths) ? body.contextPaths : [],
125
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,
126
127
  mcpServers: Array.isArray(body.mcpServers) ? body.mcpServers.filter((s) => typeof s === 'string') : undefined,
127
128
  };
128
129
  await store.saveJob(job);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovelybunch/api",
3
- "version": "1.0.69-alpha.3",
3
+ "version": "1.0.69-alpha.5",
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.3",
40
- "@lovelybunch/mcp": "^1.0.69-alpha.3",
41
- "@lovelybunch/types": "^1.0.69-alpha.3",
39
+ "@lovelybunch/core": "^1.0.69-alpha.5",
40
+ "@lovelybunch/mcp": "^1.0.69-alpha.5",
41
+ "@lovelybunch/types": "^1.0.69-alpha.5",
42
42
  "arctic": "^1.9.2",
43
43
  "bcrypt": "^5.1.1",
44
44
  "cookie": "^0.6.0",