@lovelybunch/api 1.0.66 → 1.0.67

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 (35) hide show
  1. package/dist/routes/api/v1/events/events.test.d.ts +4 -0
  2. package/dist/routes/api/v1/events/events.test.js +289 -0
  3. package/dist/routes/api/v1/events/index.d.ts +7 -0
  4. package/dist/routes/api/v1/events/index.js +19 -0
  5. package/dist/routes/api/v1/events/purge/route.d.ts +19 -0
  6. package/dist/routes/api/v1/events/purge/route.js +62 -0
  7. package/dist/routes/api/v1/events/route.d.ts +30 -0
  8. package/dist/routes/api/v1/events/route.js +109 -0
  9. package/dist/routes/api/v1/events/status/route.d.ts +20 -0
  10. package/dist/routes/api/v1/events/status/route.js +53 -0
  11. package/dist/routes/api/v1/events/stream/route.d.ts +9 -0
  12. package/dist/routes/api/v1/events/stream/route.js +132 -0
  13. package/dist/routes/api/v1/init/index.d.ts +1 -0
  14. package/dist/routes/api/v1/init/index.js +1 -0
  15. package/dist/routes/api/v1/init/route.d.ts +3 -0
  16. package/dist/routes/api/v1/init/route.js +129 -0
  17. package/dist/routes/api/v1/onboard/index.d.ts +3 -0
  18. package/dist/routes/api/v1/onboard/index.js +8 -0
  19. package/dist/routes/api/v1/onboard/route.d.ts +13 -0
  20. package/dist/routes/api/v1/onboard/route.js +311 -0
  21. package/dist/routes/api/v1/onboarding/check/index.d.ts +3 -0
  22. package/dist/routes/api/v1/onboarding/check/index.js +5 -0
  23. package/dist/routes/api/v1/onboarding/check/route.d.ts +12 -0
  24. package/dist/routes/api/v1/onboarding/check/route.js +24 -0
  25. package/dist/routes/api/v1/onboarding/index.d.ts +1 -0
  26. package/dist/routes/api/v1/onboarding/index.js +1 -0
  27. package/dist/routes/api/v1/onboarding/route.d.ts +3 -0
  28. package/dist/routes/api/v1/onboarding/route.js +158 -0
  29. package/dist/routes/api/v1/proposals/[id]/route.js +57 -0
  30. package/dist/routes/api/v1/proposals/route.js +18 -0
  31. package/dist/server-with-static.js +62 -0
  32. package/dist/server.js +63 -0
  33. package/package.json +4 -4
  34. package/static/assets/{index-DuLX7Zvh.js → index-aLGL6jN0.js} +33 -33
  35. package/static/index.html +1 -1
@@ -0,0 +1,158 @@
1
+ import { Hono } from 'hono';
2
+ import path from 'path';
3
+ import { promises as fs } from 'fs';
4
+ import { execFile } from 'child_process';
5
+ import { promisify } from 'util';
6
+ import { MarkdownStorageAdapter } from '@lovelybunch/core';
7
+ import { getGlobalJobScheduler } from '../../../../lib/jobs/global-job-scheduler.js';
8
+ const execFileAsync = promisify(execFile);
9
+ const onboarding = new Hono();
10
+ function isOnboardingActive() {
11
+ return process.env.COCONUT_ONBOARDING_MODE === '1';
12
+ }
13
+ function getProjectRoot() {
14
+ if (process.env.GAIT_DATA_PATH) {
15
+ return path.resolve(process.env.GAIT_DATA_PATH);
16
+ }
17
+ return process.cwd();
18
+ }
19
+ async function nutDirectoryExists(root) {
20
+ try {
21
+ await fs.access(path.join(root, '.nut'));
22
+ return true;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ async function isGitRepository(root) {
29
+ try {
30
+ await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: root });
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ function sanitizeString(value) {
38
+ return typeof value === 'string' ? value.trim() : '';
39
+ }
40
+ async function ensureSchedulerStarted() {
41
+ try {
42
+ const scheduler = getGlobalJobScheduler();
43
+ await scheduler.initialize();
44
+ }
45
+ catch (error) {
46
+ console.error('Failed to initialize job scheduler after onboarding:', error);
47
+ }
48
+ }
49
+ onboarding.get('/status', async (c) => {
50
+ const root = getProjectRoot();
51
+ const coconutInitialized = await nutDirectoryExists(root);
52
+ const gitInitialized = await isGitRepository(root);
53
+ return c.json({
54
+ success: true,
55
+ data: {
56
+ onboardingActive: isOnboardingActive() && !coconutInitialized,
57
+ coconutInitialized,
58
+ gitInitialized
59
+ }
60
+ });
61
+ });
62
+ onboarding.post('/start-empty', async (c) => {
63
+ const root = getProjectRoot();
64
+ const body = await c.req.json().catch(() => ({}));
65
+ const name = sanitizeString(body?.name);
66
+ const description = sanitizeString(body?.description);
67
+ if (!name) {
68
+ return c.json({ success: false, error: 'Repository name is required' }, 400);
69
+ }
70
+ if (!description) {
71
+ return c.json({ success: false, error: 'Repository description is required' }, 400);
72
+ }
73
+ if (await nutDirectoryExists(root)) {
74
+ return c.json({ success: false, error: 'Coconut is already initialized in this directory' }, 409);
75
+ }
76
+ try {
77
+ const storage = new MarkdownStorageAdapter(path.join(root, '.nut'));
78
+ await storage.init();
79
+ const config = (await storage.loadConfig()) ?? {
80
+ version: '1.0.0',
81
+ repository: {
82
+ name,
83
+ description
84
+ },
85
+ policies: {
86
+ requireApproval: true,
87
+ minApprovers: 1,
88
+ allowSelfApproval: false,
89
+ autoMerge: false
90
+ },
91
+ storage: {
92
+ type: 'file',
93
+ path: '.nut'
94
+ }
95
+ };
96
+ config.repository.name = name;
97
+ config.repository.description = description;
98
+ await storage.saveConfig(config);
99
+ const gitAlreadyInitialized = await isGitRepository(root);
100
+ if (!gitAlreadyInitialized) {
101
+ await execFileAsync('git', ['init'], { cwd: root });
102
+ }
103
+ await ensureSchedulerStarted();
104
+ return c.json({
105
+ success: true,
106
+ data: {
107
+ coconutInitialized: true,
108
+ gitInitialized: true
109
+ }
110
+ }, 201);
111
+ }
112
+ catch (error) {
113
+ console.error('Failed to initialize Coconut during onboarding:', error);
114
+ return c.json({
115
+ success: false,
116
+ error: error?.message || 'Failed to initialize Coconut'
117
+ }, 500);
118
+ }
119
+ });
120
+ onboarding.post('/clone', async (c) => {
121
+ const root = getProjectRoot();
122
+ const body = await c.req.json().catch(() => ({}));
123
+ const repositoryUrl = sanitizeString(body?.url);
124
+ if (!repositoryUrl) {
125
+ return c.json({ success: false, error: 'Repository URL is required' }, 400);
126
+ }
127
+ if (await nutDirectoryExists(root)) {
128
+ return c.json({ success: false, error: 'Coconut is already initialized in this directory' }, 409);
129
+ }
130
+ if (await isGitRepository(root)) {
131
+ return c.json({ success: false, error: 'Git repository already exists in this directory' }, 409);
132
+ }
133
+ try {
134
+ await execFileAsync('git', ['clone', repositoryUrl, '.'], { cwd: root });
135
+ const coconutInitialized = await nutDirectoryExists(root);
136
+ const gitInitialized = await isGitRepository(root);
137
+ if (coconutInitialized) {
138
+ await ensureSchedulerStarted();
139
+ }
140
+ return c.json({
141
+ success: true,
142
+ data: {
143
+ coconutInitialized,
144
+ gitInitialized
145
+ }
146
+ });
147
+ }
148
+ catch (error) {
149
+ console.error('Failed to clone repository during onboarding:', error);
150
+ const stderr = error?.stderr?.toString?.() || '';
151
+ const message = stderr.trim() || error?.message || 'Failed to clone repository';
152
+ return c.json({
153
+ success: false,
154
+ error: message
155
+ }, 400);
156
+ }
157
+ });
158
+ export default onboarding;
@@ -1,5 +1,7 @@
1
1
  import { FileStorageAdapter } from '../../../../../lib/storage/file-storage.js';
2
+ import { getLogger } from '@lovelybunch/core/logging';
2
3
  const storage = new FileStorageAdapter();
4
+ // Logger is lazily initialized inside handlers to use server config
3
5
  export async function GET(c) {
4
6
  try {
5
7
  const id = c.req.param('id');
@@ -52,6 +54,43 @@ export async function PATCH(c) {
52
54
  await storage.updateCP(id, finalUpdates);
53
55
  // Fetch the updated proposal
54
56
  const updatedProposal = await storage.getCP(id);
57
+ // Determine what changed
58
+ const changedFields = Object.keys(updates);
59
+ const oldStatus = existing.status;
60
+ const newStatus = updatedProposal?.status || existing.status;
61
+ // Log proposal.update event
62
+ const logger = getLogger();
63
+ logger.log({
64
+ kind: 'proposal.update',
65
+ actor: updatedProposal?.author.type === 'agent'
66
+ ? `agent:${updatedProposal.author.name}`
67
+ : `human:${updatedProposal?.author.email || 'unknown'}`,
68
+ subject: `proposal:${id}`,
69
+ tags: ['proposal'],
70
+ payload: {
71
+ id,
72
+ changes: changedFields,
73
+ oldStatus,
74
+ newStatus
75
+ }
76
+ });
77
+ // If status changed, also log a status.change event
78
+ if (oldStatus !== newStatus) {
79
+ logger.log({
80
+ kind: 'proposal.status.change',
81
+ actor: updatedProposal?.author.type === 'agent'
82
+ ? `agent:${updatedProposal.author.name}`
83
+ : `human:${updatedProposal?.author.email || 'unknown'}`,
84
+ subject: `proposal:${id}`,
85
+ tags: ['proposal', 'status'],
86
+ payload: {
87
+ id,
88
+ from: oldStatus,
89
+ to: newStatus,
90
+ reason: null
91
+ }
92
+ });
93
+ }
55
94
  return c.json({
56
95
  success: true,
57
96
  data: updatedProposal
@@ -80,7 +119,25 @@ export async function PATCH(c) {
80
119
  export async function DELETE(c) {
81
120
  try {
82
121
  const id = c.req.param('id');
122
+ // Get proposal info before deleting
123
+ const proposal = await storage.getCP(id);
83
124
  await storage.deleteCP(id);
125
+ // Log the deletion event
126
+ if (proposal) {
127
+ const logger = getLogger();
128
+ logger.log({
129
+ kind: 'proposal.delete',
130
+ actor: proposal.author.type === 'agent'
131
+ ? `agent:${proposal.author.name}`
132
+ : `human:${proposal.author.email || 'unknown'}`,
133
+ subject: `proposal:${id}`,
134
+ tags: ['proposal'],
135
+ payload: {
136
+ id,
137
+ intent: proposal.intent
138
+ }
139
+ });
140
+ }
84
141
  return c.json({
85
142
  success: true,
86
143
  message: `Proposal ${id} deleted successfully`
@@ -1,7 +1,9 @@
1
1
  import { FileStorageAdapter } from '../../../../lib/storage/file-storage.js';
2
2
  import { getAuthorInfo } from '../../../../lib/user-preferences.js';
3
3
  import Fuse from 'fuse.js';
4
+ import { getLogger } from '@lovelybunch/core/logging';
4
5
  const storage = new FileStorageAdapter();
6
+ // Logger is lazily initialized inside handlers to use server config
5
7
  export async function GET(c) {
6
8
  try {
7
9
  const url = new URL(c.req.url);
@@ -85,6 +87,22 @@ export async function POST(c) {
85
87
  productSpecRef: body.productSpecRef
86
88
  };
87
89
  await storage.createCP(proposal);
90
+ // Log the proposal creation event
91
+ const logger = getLogger();
92
+ logger.log({
93
+ kind: 'proposal.create',
94
+ actor: proposal.author.type === 'agent'
95
+ ? `agent:${proposal.author.name}`
96
+ : `human:${proposal.author.email}`,
97
+ subject: `proposal:${proposal.id}`,
98
+ tags: ['proposal'],
99
+ payload: {
100
+ id: proposal.id,
101
+ intent: proposal.intent,
102
+ priority: proposal.metadata.priority,
103
+ author: proposal.author
104
+ }
105
+ });
88
106
  return c.json({
89
107
  success: true,
90
108
  data: proposal
@@ -10,10 +10,70 @@ import fs from 'fs';
10
10
  import { getGlobalTerminalManager } from './lib/terminal/global-manager.js';
11
11
  import { getGlobalJobScheduler } from './lib/jobs/global-job-scheduler.js';
12
12
  import { fileURLToPath } from 'url';
13
+ import { getLogger } from '@lovelybunch/core/logging';
13
14
  const __filename = fileURLToPath(import.meta.url);
14
15
  const __dirname = path.dirname(__filename);
15
16
  // Load environment variables from .env file in project root
16
17
  dotenvConfig({ path: path.resolve(__dirname, '../../../.env') });
18
+ // Helper: Find .nut directory by traversing up from cwd
19
+ // Only returns a .nut directory if it contains config.json
20
+ function findNutDirectorySync() {
21
+ let currentDir = process.cwd();
22
+ while (currentDir !== path.parse(currentDir).root) {
23
+ const nutPath = path.join(currentDir, '.nut');
24
+ const configPath = path.join(nutPath, 'config.json');
25
+ // Check if .nut exists AND has config.json
26
+ if (fs.existsSync(nutPath) && fs.existsSync(configPath)) {
27
+ return nutPath;
28
+ }
29
+ currentDir = path.dirname(currentDir);
30
+ }
31
+ return null;
32
+ }
33
+ // Initialize logger with config from .nut/config.json
34
+ // This must happen BEFORE importing route handlers (they call getLogger at module level)
35
+ console.log('🔍 Initializing activity logging...');
36
+ try {
37
+ const nutDir = findNutDirectorySync();
38
+ if (nutDir) {
39
+ const projectRoot = path.dirname(nutDir);
40
+ const configPath = path.join(nutDir, 'config.json');
41
+ console.log(' Project root:', projectRoot);
42
+ console.log(' Config path:', configPath);
43
+ const configData = fs.readFileSync(configPath, 'utf-8');
44
+ const config = JSON.parse(configData);
45
+ if (config.logging?.enabled) {
46
+ const logsDir = path.resolve(projectRoot, config.logging?.location || '.nut/logs');
47
+ const logger = getLogger({
48
+ coconutId: config.coconut?.id || 'unknown.coconut',
49
+ logsDir: logsDir,
50
+ rotateBytes: config.logging?.rotateBytes || 128 * 1024 * 1024
51
+ });
52
+ console.log('📝 Activity logging ENABLED');
53
+ console.log(' Logs directory:', logsDir);
54
+ console.log(' Coconut ID:', config.coconut?.id || 'unknown.coconut');
55
+ // Test log immediately
56
+ logger.log({
57
+ kind: 'system.startup',
58
+ actor: 'system',
59
+ subject: 'server',
60
+ tags: ['system', 'startup'],
61
+ payload: { message: 'Server starting with logging enabled' }
62
+ });
63
+ console.log(' ✓ Test event logged');
64
+ }
65
+ else {
66
+ console.log('📝 Activity logging disabled in config');
67
+ }
68
+ }
69
+ else {
70
+ console.log(' ✗ .nut directory not found');
71
+ }
72
+ }
73
+ catch (error) {
74
+ console.error('⚠️ Failed to initialize logger:');
75
+ console.error(error);
76
+ }
17
77
  const app = new Hono();
18
78
  // Enable CORS for development
19
79
  app.use('/api/*', cors({
@@ -101,6 +161,7 @@ import agentsById from './routes/api/v1/agents/[id]/index.js';
101
161
  import git from './routes/api/v1/git/index.js';
102
162
  import mcp from './routes/api/v1/mcp/index.js';
103
163
  import jobs from './routes/api/v1/jobs/index.js';
164
+ import events from './routes/api/v1/events/index.js';
104
165
  // Register API routes FIRST
105
166
  console.log('🔗 Registering API routes...');
106
167
  app.route('/api/v1/auth', auth);
@@ -125,6 +186,7 @@ app.route('/api/v1/agents/:id', agentsById);
125
186
  app.route('/api/v1/git', git);
126
187
  app.route('/api/v1/mcp', mcp);
127
188
  app.route('/api/v1/jobs', jobs);
189
+ app.route('/api/v1/events', events);
128
190
  console.log('✅ API routes registered');
129
191
  // Initialize background services
130
192
  getGlobalJobScheduler();
package/dist/server.js CHANGED
@@ -8,10 +8,71 @@ import { getGlobalTerminalManager } from './lib/terminal/global-manager.js';
8
8
  import { getGlobalJobScheduler } from './lib/jobs/global-job-scheduler.js';
9
9
  import path from 'path';
10
10
  import { fileURLToPath } from 'url';
11
+ import fs from 'fs';
12
+ import { getLogger } from '@lovelybunch/core/logging';
11
13
  const __filename = fileURLToPath(import.meta.url);
12
14
  const __dirname = path.dirname(__filename);
13
15
  // Load environment variables from .env file in project root
14
16
  dotenvConfig({ path: path.resolve(__dirname, '../../../.env') });
17
+ // Helper: Find .nut directory by traversing up from cwd
18
+ // Only returns a .nut directory if it contains config.json
19
+ function findNutDirectorySync() {
20
+ let currentDir = process.cwd();
21
+ while (currentDir !== path.parse(currentDir).root) {
22
+ const nutPath = path.join(currentDir, '.nut');
23
+ const configPath = path.join(nutPath, 'config.json');
24
+ // Check if .nut exists AND has config.json
25
+ if (fs.existsSync(nutPath) && fs.existsSync(configPath)) {
26
+ return nutPath;
27
+ }
28
+ currentDir = path.dirname(currentDir);
29
+ }
30
+ return null;
31
+ }
32
+ // Initialize logger with config from .nut/config.json
33
+ // This must happen BEFORE importing route handlers (they call getLogger at module level)
34
+ console.log('🔍 Initializing activity logging...');
35
+ try {
36
+ const nutDir = findNutDirectorySync();
37
+ if (nutDir) {
38
+ const projectRoot = path.dirname(nutDir);
39
+ const configPath = path.join(nutDir, 'config.json');
40
+ console.log(' Project root:', projectRoot);
41
+ console.log(' Config path:', configPath);
42
+ const configData = fs.readFileSync(configPath, 'utf-8');
43
+ const config = JSON.parse(configData);
44
+ if (config.logging?.enabled) {
45
+ const logsDir = path.resolve(projectRoot, config.logging?.location || '.nut/logs');
46
+ const logger = getLogger({
47
+ coconutId: config.coconut?.id || 'unknown.coconut',
48
+ logsDir: logsDir,
49
+ rotateBytes: config.logging?.rotateBytes || 128 * 1024 * 1024
50
+ });
51
+ console.log('📝 Activity logging ENABLED');
52
+ console.log(' Logs directory:', logsDir);
53
+ console.log(' Coconut ID:', config.coconut?.id || 'unknown.coconut');
54
+ // Test log immediately
55
+ logger.log({
56
+ kind: 'system.startup',
57
+ actor: 'system',
58
+ subject: 'server',
59
+ tags: ['system', 'startup'],
60
+ payload: { message: 'Server starting with logging enabled' }
61
+ });
62
+ console.log(' ✓ Test event logged');
63
+ }
64
+ else {
65
+ console.log('📝 Activity logging disabled in config');
66
+ }
67
+ }
68
+ else {
69
+ console.log(' ✗ .nut directory not found');
70
+ }
71
+ }
72
+ catch (error) {
73
+ console.error('⚠️ Failed to initialize logger:');
74
+ console.error(error);
75
+ }
15
76
  const app = new Hono();
16
77
  // Enable CORS for frontend
17
78
  app.use('*', cors({
@@ -100,6 +161,7 @@ import git from './routes/api/v1/git/index.js';
100
161
  import mcp from './routes/api/v1/mcp/index.js';
101
162
  import symlinks from './routes/api/v1/symlinks/index.js';
102
163
  import jobs from './routes/api/v1/jobs/index.js';
164
+ import events from './routes/api/v1/events/index.js';
103
165
  // Register API routes
104
166
  app.route('/api/v1/auth', auth);
105
167
  app.route('/api/v1/auth-settings', authSettings);
@@ -124,6 +186,7 @@ app.route('/api/v1/git', git);
124
186
  app.route('/api/v1/mcp', mcp);
125
187
  app.route('/api/v1/symlinks', symlinks);
126
188
  app.route('/api/v1/jobs', jobs);
189
+ app.route('/api/v1/events', events);
127
190
  // Health check endpoint
128
191
  app.get('/health', (c) => {
129
192
  return c.json({ status: 'ok', timestamp: new Date().toISOString() });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovelybunch/api",
3
- "version": "1.0.66",
3
+ "version": "1.0.67",
4
4
  "type": "module",
5
5
  "main": "dist/server-with-static.js",
6
6
  "exports": {
@@ -32,9 +32,9 @@
32
32
  "dependencies": {
33
33
  "@hono/node-server": "^1.13.7",
34
34
  "@hono/node-ws": "^1.0.6",
35
- "@lovelybunch/core": "^1.0.66",
36
- "@lovelybunch/mcp": "^1.0.66",
37
- "@lovelybunch/types": "^1.0.66",
35
+ "@lovelybunch/core": "^1.0.67",
36
+ "@lovelybunch/mcp": "^1.0.67",
37
+ "@lovelybunch/types": "^1.0.67",
38
38
  "arctic": "^1.9.2",
39
39
  "bcrypt": "^5.1.1",
40
40
  "cookie": "^0.6.0",