@lovelybunch/api 1.0.75-alpha.10 → 1.0.75-alpha.12
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/jobs/job-scheduler.js +21 -0
- package/dist/lib/mail/mail-runner.d.ts +19 -0
- package/dist/lib/mail/mail-runner.js +223 -0
- package/dist/lib/slack/slack-service.d.ts +2 -0
- package/dist/lib/slack/slack-service.js +3 -0
- package/dist/routes/api/v1/ai/route.js +23 -3
- package/dist/routes/api/v1/git/index.js +23 -0
- package/dist/routes/api/v1/mail/index.d.ts +3 -0
- package/dist/routes/api/v1/mail/index.js +20 -0
- package/dist/routes/api/v1/mail/route.d.ts +252 -0
- package/dist/routes/api/v1/mail/route.js +261 -0
- package/dist/routes/api/v1/slack/index.d.ts +3 -0
- package/dist/routes/api/v1/slack/index.js +15 -0
- package/dist/routes/api/v1/slack/route.d.ts +124 -0
- package/dist/routes/api/v1/slack/route.js +192 -0
- package/dist/routes/api/v1/tasks/[id]/route.js +10 -0
- package/dist/routes/api/v1/tasks/route.d.ts +1 -0
- package/dist/routes/api/v1/tasks/route.js +18 -2
- package/dist/server-with-static.js +2 -0
- package/dist/server.js +4 -0
- package/package.json +4 -4
- package/static/assets/{ActivityPage-CbmEnYhg.js → ActivityPage-nSSbSFAB.js} +1 -1
- package/static/assets/{ApiKeysSettingsPage-DLxQIqTT.js → ApiKeysSettingsPage-xrhhroQ_.js} +1 -1
- package/static/assets/{ArchitectureEditPage-CbtzgIQv.js → ArchitectureEditPage-CHFEQNgk.js} +1 -1
- package/static/assets/{ArchitecturePage-CcAMfFZ8.js → ArchitecturePage-EudLa-GY.js} +1 -1
- package/static/assets/{AuthSettingsPage-4Prb7nAt.js → AuthSettingsPage-C3YTYgj_.js} +1 -1
- package/static/assets/{CallbackPage-CmWv_5Nh.js → CallbackPage-VI4m1EWU.js} +1 -1
- package/static/assets/{CodePage-COC7rcwM.js → CodePage-CcH6EB2S.js} +1 -1
- package/static/assets/{CollapsibleSection-C_VVeVSc.js → CollapsibleSection-4LvowJQN.js} +1 -1
- package/static/assets/{DashboardPage-BACFVIaW.js → DashboardPage-Dj2GNTQg.js} +1 -1
- package/static/assets/{GitPage-rFngEr_A.js → GitPage-E9px87JY.js} +2 -2
- package/static/assets/{GitSettingsPage-CDQovOqx.js → GitSettingsPage-BepF5Dpg.js} +1 -1
- package/static/assets/{IdentityPage-CS4REq2E.js → IdentityPage-Ccarv5Tz.js} +2 -7
- package/static/assets/{ImplementationStepsEditor-CBIgppkZ.js → ImplementationStepsEditor-BPeqMfAd.js} +1 -1
- package/static/assets/{IntegrationsSettingsPage-B7HoraBH.js → IntegrationsSettingsPage-Dp3FRWNn.js} +1 -1
- package/static/assets/{JobDetailPage-QUhWsaWV.js → JobDetailPage-DOJKC2Oq.js} +1 -1
- package/static/assets/{KnowledgeDetailPage-DCJstmIr.js → KnowledgeDetailPage-BT0czLqw.js} +1 -1
- package/static/assets/{KnowledgeEditPage-EYpFPfcJ.js → KnowledgeEditPage-D7gBI6r-.js} +1 -1
- package/static/assets/{KnowledgePage-D-7skvn5.js → KnowledgePage-CcfftMdn.js} +1 -1
- package/static/assets/{LoginPage-L2aoqSDT.js → LoginPage-nYfkZp1m.js} +1 -1
- package/static/assets/MailInboxPage-DvD2sH0g.js +1 -0
- package/static/assets/MailReadPage-CZ9stBHd.js +1 -0
- package/static/assets/MailSentPage-BqtUkhvw.js +1 -0
- package/static/assets/{McpSettingsPage-CLwqDjw_.js → McpSettingsPage-CrNaaXtj.js} +1 -1
- package/static/assets/{NewKnowledgePage-Fq_QD8um.js → NewKnowledgePage-BsiP9u8K.js} +1 -1
- package/static/assets/{NewSkillPage-DRcgovk0.js → NewSkillPage-BuwLNwdi.js} +1 -1
- package/static/assets/{NewTaskPage-CleA8rH5.js → NewTaskPage-DVh5Xv1O.js} +1 -1
- package/static/assets/NotificationsSettingsPage-BbDASNoh.js +1 -0
- package/static/assets/{ProjectEditPage-GIMOKgmh.js → ProjectEditPage-DdCvF5RR.js} +1 -1
- package/static/assets/{ProjectPage-DlWZOqnb.js → ProjectPage-2ujqpmN5.js} +1 -1
- package/static/assets/{PromptsSettingsPage-DFagGfo-.js → PromptsSettingsPage-xmsIGnw7.js} +1 -1
- package/static/assets/{ResourceDetailPage-CnFRpDec.js → ResourceDetailPage--4tTrZYy.js} +1 -1
- package/static/assets/{ResourcesPage-Cw3fu0JE.js → ResourcesPage-Nrgt2hCS.js} +1 -1
- package/static/assets/{RoleEditPage-ARXq_eCs.js → RoleEditPage-D8XSVw9S.js} +1 -1
- package/static/assets/{RolePage--XBBIrq8.js → RolePage-hS7J_Bhy.js} +1 -1
- package/static/assets/{RulesSettingsPage-DEqaRH96.js → RulesSettingsPage-D4K8SBOu.js} +1 -1
- package/static/assets/{SchedulePage-C_B2k9P6.js → SchedulePage-BiJmkZ3Q.js} +1 -1
- package/static/assets/{SkillDetailPage-Dr6xyKAX.js → SkillDetailPage-DXtASGaY.js} +1 -1
- package/static/assets/{SkillEditPage-BsHXmfEZ.js → SkillEditPage-D75sp5rI.js} +1 -1
- package/static/assets/{SkillsPage-BrDdAAPx.js → SkillsPage-CsutZlkM.js} +1 -1
- package/static/assets/{SkillsSettingsPage-Cvi7xaDE.js → SkillsSettingsPage-B8z_IMlS.js} +1 -1
- package/static/assets/{SourceInput-DxF_GGj8.js → SourceInput-DkQR44OP.js} +1 -1
- package/static/assets/{TagInput-DYvHbVZq.js → TagInput-BkkvY_kU.js} +1 -1
- package/static/assets/{TaskDetailPage-DzcD6t03.js → TaskDetailPage-Bh3Cb6ce.js} +1 -1
- package/static/assets/{TaskEditPage-C7a6FOeJ.js → TaskEditPage-CnKiBPvU.js} +1 -1
- package/static/assets/{TasksPage-ChWRSO_S.js → TasksPage-3UvOASrt.js} +1 -1
- package/static/assets/{TerminalPage-CoPwn8cU.js → TerminalPage-DIXOrK6a.js} +1 -1
- package/static/assets/{TerminalSessionPage-BQBZrOJa.js → TerminalSessionPage-Bu0_HyWE.js} +3 -8
- package/static/assets/{UserPreferencesPage-DjtU7veO.js → UserPreferencesPage-_ZVEA1Ec.js} +1 -1
- package/static/assets/{UserSettingsPage-CfU8boJQ.js → UserSettingsPage-hOWbcJ8X.js} +1 -1
- package/static/assets/{UtilitiesPage-sP1Crg-X.js → UtilitiesPage-D4Ae45jX.js} +1 -1
- package/static/assets/{alert-BqZa-crG.js → alert-oIIrVTSO.js} +1 -1
- package/static/assets/{arrow-down-3faV_GyO.js → arrow-down-Bzd7RaRH.js} +1 -1
- package/static/assets/{arrow-left-FD3wQmzH.js → arrow-left-BjR0fDgf.js} +1 -1
- package/static/assets/{arrow-up-BzP0YNVk.js → arrow-up-C7p7WCEm.js} +1 -1
- package/static/assets/{badge-DRyeFib9.js → badge-C2DkFMMB.js} +1 -1
- package/static/assets/{browser-modal-vnePkRfO.js → browser-modal-C6te-uYZ.js} +1 -1
- package/static/assets/{card-CuQs3dpy.js → card-WnET8BI8.js} +1 -1
- package/static/assets/{chevron-left-QZIoYcVa.js → chevron-left-BIRokVSR.js} +1 -1
- package/static/assets/{chevron-up-DreyvhRd.js → chevron-up-Cn_oyEAo.js} +1 -1
- package/static/assets/{chevrons-up-CsAkc9vE.js → chevrons-up-CupaYlEL.js} +1 -1
- package/static/assets/{circle-alert-ewz28SE3.js → circle-alert-aIhIWzZ9.js} +1 -1
- package/static/assets/{circle-check-2TuD-EHs.js → circle-check-Cji8oWtr.js} +1 -1
- package/static/assets/{circle-check-big-1xNuBPkR.js → circle-check-big-CfzDBM2L.js} +1 -1
- package/static/assets/{circle-play-C_w-qCn4.js → circle-play-BAAWTmz3.js} +1 -1
- package/static/assets/{circle-x-B_d4wjG-.js → circle-x-DUrreDIn.js} +1 -1
- package/static/assets/{clipboard-B6vBm1BP.js → clipboard-BM4PIYFM.js} +1 -1
- package/static/assets/{clock-BIzEsx1g.js → clock-PqCucnPP.js} +1 -1
- package/static/assets/{download-Dv-RIvKK.js → download-BfRAPZa0.js} +1 -1
- package/static/assets/{external-link-Cr8wjV6X.js → external-link-BHVp3obJ.js} +1 -1
- package/static/assets/{eye-DN958vyL.js → eye-DSRHxLqT.js} +1 -1
- package/static/assets/{folder-git-2-BproRzAR.js → folder-git-2-C2qrNCQi.js} +1 -1
- package/static/assets/index-CkH7-cOr.css +2 -0
- package/static/assets/{index-Co_SJV3n.js → index-Cv-tURje.js} +115 -95
- package/static/assets/{info-CzKk8mbR.js → info-yTHl9iJb.js} +1 -1
- package/static/assets/{label-h5GIKGcJ.js → label-DjRbZW_E.js} +1 -1
- package/static/assets/{markdown-editor-C6il4XWv.js → markdown-editor-CKaviYH9.js} +1 -1
- package/static/assets/message-square-D-mJ-M1p.js +6 -0
- package/static/assets/paperclip-BSIzwgES.js +6 -0
- package/static/assets/{pause-BDsjEmXM.js → pause-DJ-9PRz9.js} +1 -1
- package/static/assets/{play-CGsVQUJG.js → play-WwrfRXB7.js} +1 -1
- package/static/assets/{radio-group-Bsd75ahK.js → radio-group-Cahzon6Q.js} +1 -1
- package/static/assets/{refresh-cw-qE1iNkL_.js → refresh-cw-DNc78_Y9.js} +1 -1
- package/static/assets/{search-dvi0J4Dr.js → search-k0E_myGY.js} +1 -1
- package/static/assets/{select-DylRS99W.js → select-DXmoGuE-.js} +1 -1
- package/static/assets/{switch-CsB3wpq9.js → switch-D_BwNkIO.js} +1 -1
- package/static/assets/{tabs-Bw_4k2Rs.js → tabs-Bc7iwFOR.js} +1 -1
- package/static/assets/{tag-Dh5PraRd.js → tag-C0GUie9u.js} +1 -1
- package/static/assets/{terminal-preview-CfOb7xMx.js → terminal-preview-COHnoVSQ.js} +1 -1
- package/static/assets/{use-terminal-T_tdJTCU.js → use-terminal-Cnylz5l4.js} +1 -1
- package/static/assets/{video-bO6uuAjA.js → video-BC93G8Yl.js} +1 -1
- package/static/assets/{zap-DHZ91NcK.js → zap-Bf22dXda.js} +1 -1
- package/static/index.html +2 -2
- package/static/assets/index-GFQ5RqVh.css +0 -2
|
@@ -195,6 +195,18 @@ export class JobScheduler {
|
|
|
195
195
|
catch (logError) {
|
|
196
196
|
console.error('Error logging job run end:', logError);
|
|
197
197
|
}
|
|
198
|
+
// Send Slack notification for job completion (non-blocking)
|
|
199
|
+
const notificationType = result.status === 'succeeded' ? 'job.completed' : 'job.failed';
|
|
200
|
+
import('../slack/slack-service.js').then(({ getSlackService }) => {
|
|
201
|
+
const duration = runRecord.finishedAt.getTime() - start.getTime();
|
|
202
|
+
getSlackService().sendNotification({
|
|
203
|
+
type: notificationType,
|
|
204
|
+
jobId: job.id,
|
|
205
|
+
jobName: job.name || job.id,
|
|
206
|
+
duration,
|
|
207
|
+
error: result.error,
|
|
208
|
+
}).catch(err => console.warn('[jobs] Slack notification failed:', err));
|
|
209
|
+
}).catch(() => { });
|
|
198
210
|
}
|
|
199
211
|
catch (error) {
|
|
200
212
|
runRecord.status = 'failed';
|
|
@@ -223,6 +235,15 @@ export class JobScheduler {
|
|
|
223
235
|
catch (logError) {
|
|
224
236
|
console.error('Error logging job run error:', logError);
|
|
225
237
|
}
|
|
238
|
+
// Send Slack notification for job failure (non-blocking)
|
|
239
|
+
import('../slack/slack-service.js').then(({ getSlackService }) => {
|
|
240
|
+
getSlackService().sendNotification({
|
|
241
|
+
type: 'job.failed',
|
|
242
|
+
jobId: job.id,
|
|
243
|
+
jobName: job.name || job.id,
|
|
244
|
+
error: error?.message || 'Unknown error',
|
|
245
|
+
}).catch(err => console.warn('[jobs] Slack notification failed:', err));
|
|
246
|
+
}).catch(() => { });
|
|
226
247
|
}
|
|
227
248
|
try {
|
|
228
249
|
await this.store.saveJob(job);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface MailRunResult {
|
|
2
|
+
status: 'succeeded' | 'failed';
|
|
3
|
+
summary?: string;
|
|
4
|
+
outputPath?: string;
|
|
5
|
+
error?: string;
|
|
6
|
+
cliCommand: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class MailRunner {
|
|
9
|
+
private projectRootPromise;
|
|
10
|
+
constructor();
|
|
11
|
+
private ensureCliAvailable;
|
|
12
|
+
private ensureLogPath;
|
|
13
|
+
private loadSystemPrompt;
|
|
14
|
+
private loadConfigModel;
|
|
15
|
+
private buildInstruction;
|
|
16
|
+
run(mailId: string, mailFilePath: string): Promise<MailRunResult>;
|
|
17
|
+
}
|
|
18
|
+
export declare function getMailRunner(): MailRunner;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'child_process';
|
|
2
|
+
import { createWriteStream } from 'fs';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { getProjectRoot } from '../project-paths.js';
|
|
6
|
+
import { getInjectedEnv } from '../env-injection.js';
|
|
7
|
+
import { setMailProcessing } from '@lovelybunch/core';
|
|
8
|
+
function shellQuote(value) {
|
|
9
|
+
if (value === '')
|
|
10
|
+
return "''";
|
|
11
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
12
|
+
}
|
|
13
|
+
function resolveAgent(model) {
|
|
14
|
+
if (!model)
|
|
15
|
+
return 'claude';
|
|
16
|
+
const lower = model.toLowerCase();
|
|
17
|
+
if (lower.includes('gemini'))
|
|
18
|
+
return 'gemini';
|
|
19
|
+
if (lower.includes('codex') || lower.includes('gpt') || lower.includes('openai'))
|
|
20
|
+
return 'codex';
|
|
21
|
+
if (lower.includes('droid') || lower.includes('factory'))
|
|
22
|
+
return 'droid';
|
|
23
|
+
return 'claude';
|
|
24
|
+
}
|
|
25
|
+
function buildCommand(agent, instruction, runningAsRoot) {
|
|
26
|
+
const quotedInstruction = shellQuote(instruction);
|
|
27
|
+
let mainCommand = '';
|
|
28
|
+
switch (agent) {
|
|
29
|
+
case 'gemini':
|
|
30
|
+
mainCommand = `gemini --yolo -i ${quotedInstruction}`;
|
|
31
|
+
break;
|
|
32
|
+
case 'codex': {
|
|
33
|
+
const baseCmd = `codex ${quotedInstruction} --dangerously-bypass-approvals-and-sandbox`.trim();
|
|
34
|
+
const needsPseudoTty = runningAsRoot && process.platform !== 'win32';
|
|
35
|
+
mainCommand = needsPseudoTty
|
|
36
|
+
? `script -q -e -c ${shellQuote(baseCmd)} /dev/null`
|
|
37
|
+
: baseCmd;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
case 'droid':
|
|
41
|
+
mainCommand = `droid exec --auto high ${quotedInstruction}`.trim();
|
|
42
|
+
break;
|
|
43
|
+
case 'claude':
|
|
44
|
+
default: {
|
|
45
|
+
const prefix = runningAsRoot ? 'IS_SANDBOX=1 ' : '';
|
|
46
|
+
mainCommand = `${prefix}claude ${quotedInstruction} --dangerously-skip-permissions`.trim();
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { command: agent === 'claude' ? 'claude' : agent, shellCommand: mainCommand };
|
|
51
|
+
}
|
|
52
|
+
const CLI_AGENT_LABEL = {
|
|
53
|
+
claude: 'Claude',
|
|
54
|
+
gemini: 'Gemini',
|
|
55
|
+
codex: 'Codex',
|
|
56
|
+
droid: 'Factory Droid'
|
|
57
|
+
};
|
|
58
|
+
const CLI_AGENT_BINARY = {
|
|
59
|
+
claude: 'claude',
|
|
60
|
+
gemini: 'gemini',
|
|
61
|
+
codex: 'codex',
|
|
62
|
+
droid: 'droid'
|
|
63
|
+
};
|
|
64
|
+
const DEFAULT_MAX_RUNTIME_MS = 15 * 60 * 1000; // 15 minutes for mail processing
|
|
65
|
+
function getMaxRuntime() {
|
|
66
|
+
const raw = process.env.COCONUT_MAIL_MAX_RUNTIME_MS;
|
|
67
|
+
if (!raw)
|
|
68
|
+
return DEFAULT_MAX_RUNTIME_MS;
|
|
69
|
+
const parsed = Number(raw);
|
|
70
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
71
|
+
return DEFAULT_MAX_RUNTIME_MS;
|
|
72
|
+
}
|
|
73
|
+
return parsed;
|
|
74
|
+
}
|
|
75
|
+
export class MailRunner {
|
|
76
|
+
projectRootPromise;
|
|
77
|
+
constructor() {
|
|
78
|
+
this.projectRootPromise = getProjectRoot();
|
|
79
|
+
}
|
|
80
|
+
ensureCliAvailable(agent) {
|
|
81
|
+
const binary = CLI_AGENT_BINARY[agent];
|
|
82
|
+
const result = spawnSync('bash', ['-lc', `command -v ${binary}`], { stdio: 'ignore' });
|
|
83
|
+
if (result.status !== 0) {
|
|
84
|
+
throw new Error(`${CLI_AGENT_LABEL[agent]} CLI ("${binary}") is not installed or not on PATH.`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async ensureLogPath(mailId) {
|
|
88
|
+
const projectRoot = await this.projectRootPromise;
|
|
89
|
+
const logsDir = path.join(projectRoot, '.nut', 'mail', 'logs', mailId);
|
|
90
|
+
await fs.mkdir(logsDir, { recursive: true });
|
|
91
|
+
const runId = `run-${Date.now()}`;
|
|
92
|
+
return path.join(logsDir, `${runId}.log`);
|
|
93
|
+
}
|
|
94
|
+
async loadSystemPrompt() {
|
|
95
|
+
const projectRoot = await this.projectRootPromise;
|
|
96
|
+
// Look for the system prompt in the shared package (relative to project root)
|
|
97
|
+
const promptPath = path.join(projectRoot, 'packages', 'shared', 'system-prompts', 'mail-processor.md');
|
|
98
|
+
try {
|
|
99
|
+
return await fs.readFile(promptPath, 'utf-8');
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
throw new Error(`Failed to load mail processor system prompt from ${promptPath}: ${err.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async loadConfigModel() {
|
|
106
|
+
const projectRoot = await this.projectRootPromise;
|
|
107
|
+
const configPath = path.join(projectRoot, '.nut', 'config.json');
|
|
108
|
+
try {
|
|
109
|
+
const raw = await fs.readFile(configPath, 'utf-8');
|
|
110
|
+
const config = JSON.parse(raw);
|
|
111
|
+
return config?.ai?.model;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async buildInstruction(mailId, mailFilePath) {
|
|
118
|
+
const systemPrompt = await this.loadSystemPrompt();
|
|
119
|
+
// Substitute placeholders in the system prompt
|
|
120
|
+
const instruction = systemPrompt
|
|
121
|
+
.replace(/\{\{mailFilePath\}\}/g, mailFilePath)
|
|
122
|
+
.replace(/\{\{mailId\}\}/g, mailId);
|
|
123
|
+
return instruction;
|
|
124
|
+
}
|
|
125
|
+
async run(mailId, mailFilePath) {
|
|
126
|
+
const model = await this.loadConfigModel();
|
|
127
|
+
const agent = resolveAgent(model);
|
|
128
|
+
const instruction = await this.buildInstruction(mailId, mailFilePath);
|
|
129
|
+
const runningAsRoot = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
130
|
+
const { shellCommand } = buildCommand(agent, instruction, runningAsRoot);
|
|
131
|
+
const projectRoot = await this.projectRootPromise;
|
|
132
|
+
const logPath = await this.ensureLogPath(mailId);
|
|
133
|
+
const logStream = createWriteStream(logPath, { flags: 'a' });
|
|
134
|
+
const summaryChunks = [];
|
|
135
|
+
logStream.write(`[${new Date().toISOString()}] Starting mail processing for ${mailId} using ${agent} CLI\n`);
|
|
136
|
+
logStream.write(`Mail file: ${mailFilePath}\n`);
|
|
137
|
+
logStream.write(`Command: ${shellCommand}\n`);
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
let cliMissingError = null;
|
|
140
|
+
try {
|
|
141
|
+
this.ensureCliAvailable(agent);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
cliMissingError = error instanceof Error ? error : new Error(String(error));
|
|
145
|
+
}
|
|
146
|
+
if (cliMissingError) {
|
|
147
|
+
const message = cliMissingError.message;
|
|
148
|
+
logStream.write(`${message}\n`);
|
|
149
|
+
logStream.end();
|
|
150
|
+
setMailProcessing(mailId, false).catch(err => console.warn('[mail] failed to clear processing:', err));
|
|
151
|
+
resolve({
|
|
152
|
+
status: 'failed',
|
|
153
|
+
error: message,
|
|
154
|
+
summary: message,
|
|
155
|
+
outputPath: path.relative(projectRoot, logPath),
|
|
156
|
+
cliCommand: shellCommand,
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const injectedEnv = getInjectedEnv();
|
|
161
|
+
const child = spawn('bash', ['-lc', shellCommand], {
|
|
162
|
+
cwd: projectRoot,
|
|
163
|
+
env: {
|
|
164
|
+
...process.env,
|
|
165
|
+
...injectedEnv
|
|
166
|
+
},
|
|
167
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
168
|
+
});
|
|
169
|
+
const maxRuntime = getMaxRuntime();
|
|
170
|
+
const abortTimeout = setTimeout(() => {
|
|
171
|
+
logStream.write(`\n[${new Date().toISOString()}] Max runtime ${maxRuntime}ms exceeded. Sending SIGTERM...\n`);
|
|
172
|
+
child.kill('SIGTERM');
|
|
173
|
+
setTimeout(() => child.kill('SIGKILL'), 10_000);
|
|
174
|
+
}, maxRuntime);
|
|
175
|
+
child.stdout?.on('data', (chunk) => {
|
|
176
|
+
const text = chunk.toString();
|
|
177
|
+
logStream.write(text);
|
|
178
|
+
summaryChunks.push(text);
|
|
179
|
+
});
|
|
180
|
+
child.stderr?.on('data', (chunk) => {
|
|
181
|
+
const text = chunk.toString();
|
|
182
|
+
logStream.write(text);
|
|
183
|
+
summaryChunks.push(text);
|
|
184
|
+
});
|
|
185
|
+
child.on('error', (error) => {
|
|
186
|
+
const message = `Failed to start CLI command: ${error.message}`;
|
|
187
|
+
logStream.write(`${message}\n`);
|
|
188
|
+
logStream.end();
|
|
189
|
+
clearTimeout(abortTimeout);
|
|
190
|
+
setMailProcessing(mailId, false).catch(err => console.warn('[mail] failed to clear processing:', err));
|
|
191
|
+
resolve({
|
|
192
|
+
status: 'failed',
|
|
193
|
+
error: message,
|
|
194
|
+
summary: summaryChunks.join('').slice(-600),
|
|
195
|
+
outputPath: path.relative(projectRoot, logPath),
|
|
196
|
+
cliCommand: shellCommand,
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
child.on('close', (code) => {
|
|
200
|
+
const status = code === 0 ? 'succeeded' : 'failed';
|
|
201
|
+
logStream.write(`\n[${new Date().toISOString()}] Mail processing for ${mailId} completed with exit code ${code}\n`);
|
|
202
|
+
logStream.end();
|
|
203
|
+
clearTimeout(abortTimeout);
|
|
204
|
+
setMailProcessing(mailId, false).catch(err => console.warn('[mail] failed to clear processing:', err));
|
|
205
|
+
const summary = summaryChunks.join('');
|
|
206
|
+
resolve({
|
|
207
|
+
status,
|
|
208
|
+
summary: summary.slice(Math.max(0, summary.length - 2000)),
|
|
209
|
+
outputPath: path.relative(projectRoot, logPath),
|
|
210
|
+
error: code === 0 ? undefined : `CLI exited with code ${code}`,
|
|
211
|
+
cliCommand: shellCommand,
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
let mailRunnerInstance = null;
|
|
218
|
+
export function getMailRunner() {
|
|
219
|
+
if (!mailRunnerInstance) {
|
|
220
|
+
mailRunnerInstance = new MailRunner();
|
|
221
|
+
}
|
|
222
|
+
return mailRunnerInstance;
|
|
223
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { SlackService, getSlackService, DEFAULT_SLACK_CONFIG, } from '@lovelybunch/core';
|
|
2
|
+
export type { SlackConfig, SlackNotificationSettings, SlackChannel, ProposalNotificationPayload, JobNotificationPayload, GitNotificationPayload, NotificationPayload, NotificationType, FreeformMessageOptions, } from '@lovelybunch/core';
|
|
@@ -188,12 +188,32 @@ async function executeTasksToolDirect(args, _storage) {
|
|
|
188
188
|
try {
|
|
189
189
|
switch (operation) {
|
|
190
190
|
case 'list': {
|
|
191
|
+
const DEFAULT_LIST_LIMIT = 20;
|
|
192
|
+
const requestedLimit = filters?.limit;
|
|
193
|
+
const effectiveLimit = requestedLimit ?? DEFAULT_LIST_LIMIT;
|
|
191
194
|
const tasks = await listTasks(filters || {});
|
|
195
|
+
const totalCount = tasks.length;
|
|
196
|
+
const limited = tasks.slice(0, effectiveLimit);
|
|
197
|
+
// Return lightweight summaries to keep token usage manageable.
|
|
198
|
+
// The AI can use "get" with a specific task ID to retrieve full details.
|
|
199
|
+
const summaries = limited.map((t) => ({
|
|
200
|
+
id: t.id,
|
|
201
|
+
title: t.title,
|
|
202
|
+
status: t.status,
|
|
203
|
+
priority: t.metadata?.priority,
|
|
204
|
+
tags: t.metadata?.tags,
|
|
205
|
+
author: t.author ? { name: t.author.name, type: t.author.type } : undefined,
|
|
206
|
+
createdAt: t.metadata?.createdAt,
|
|
207
|
+
updatedAt: t.metadata?.updatedAt,
|
|
208
|
+
}));
|
|
192
209
|
return {
|
|
193
210
|
success: true,
|
|
194
|
-
data:
|
|
195
|
-
count:
|
|
196
|
-
|
|
211
|
+
data: summaries,
|
|
212
|
+
count: summaries.length,
|
|
213
|
+
totalCount,
|
|
214
|
+
message: totalCount > effectiveLimit
|
|
215
|
+
? `Showing ${summaries.length} of ${totalCount} tasks (limit: ${effectiveLimit}). Use filters or increase limit to see more. Use "get" with a task ID for full details.`
|
|
216
|
+
: `Found ${totalCount} tasks. Use "get" with a task ID for full details.`
|
|
197
217
|
};
|
|
198
218
|
}
|
|
199
219
|
case 'get': {
|
|
@@ -331,6 +331,22 @@ app.post('/branches/:branch/merge', async (c) => {
|
|
|
331
331
|
catch { }
|
|
332
332
|
const mergeStrategy = strategy?.strategy === 'squash' || strategy?.strategy === 'rebase' ? strategy.strategy : 'merge';
|
|
333
333
|
const result = await mergeBranch(name, mergeStrategy);
|
|
334
|
+
// Determine target branch for notification
|
|
335
|
+
try {
|
|
336
|
+
const { runGit } = await import('../../../../lib/git.js');
|
|
337
|
+
const { stdout: branchOutput } = await runGit(['branch', '--show-current']);
|
|
338
|
+
const targetBranch = branchOutput.trim();
|
|
339
|
+
// Send Slack notification (non-blocking)
|
|
340
|
+
import('../../../../lib/slack/slack-service.js').then(({ getSlackService }) => {
|
|
341
|
+
getSlackService().sendNotification({
|
|
342
|
+
type: 'git.merge',
|
|
343
|
+
branch: name,
|
|
344
|
+
targetBranch,
|
|
345
|
+
message: `Merged ${name} into ${targetBranch} using ${mergeStrategy}`,
|
|
346
|
+
}).catch(err => console.warn('[git] Slack notification failed:', err));
|
|
347
|
+
}).catch(() => { });
|
|
348
|
+
}
|
|
349
|
+
catch { }
|
|
334
350
|
return c.json({ success: true, data: { branch: name, strategy: mergeStrategy, result } });
|
|
335
351
|
}
|
|
336
352
|
catch (e) {
|
|
@@ -466,6 +482,13 @@ app.post('/push', async (c) => {
|
|
|
466
482
|
remote: remoteName,
|
|
467
483
|
}
|
|
468
484
|
});
|
|
485
|
+
// Send Slack notification (non-blocking)
|
|
486
|
+
import('../../../../lib/slack/slack-service.js').then(({ getSlackService }) => {
|
|
487
|
+
getSlackService().sendNotification({
|
|
488
|
+
type: 'git.push',
|
|
489
|
+
branch: currentBranch,
|
|
490
|
+
}).catch(err => console.warn('[git] Slack notification failed:', err));
|
|
491
|
+
}).catch(() => { });
|
|
469
492
|
}
|
|
470
493
|
catch (logError) {
|
|
471
494
|
console.error('Error logging push:', logError);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { listMailHandler, getMailHandler, setMailStatusHandler, setMailActionHandler, replyMailHandler, sendMailHandler, deleteMailHandler, inboundWebhookHandler, } from './route.js';
|
|
3
|
+
const mailRoutes = new Hono();
|
|
4
|
+
// Resend inbound webhook
|
|
5
|
+
mailRoutes.post('/inbound', inboundWebhookHandler);
|
|
6
|
+
// Send email (coming soon)
|
|
7
|
+
mailRoutes.post('/send', sendMailHandler);
|
|
8
|
+
// Set email status (read/unread)
|
|
9
|
+
mailRoutes.put('/:id/status', setMailStatusHandler);
|
|
10
|
+
// Set agent action summary
|
|
11
|
+
mailRoutes.put('/:id/action', setMailActionHandler);
|
|
12
|
+
// Reply to email
|
|
13
|
+
mailRoutes.post('/:id/reply', replyMailHandler);
|
|
14
|
+
// List emails in folder
|
|
15
|
+
mailRoutes.get('/:folder', listMailHandler);
|
|
16
|
+
// Get specific email in folder
|
|
17
|
+
mailRoutes.get('/:folder/:id', getMailHandler);
|
|
18
|
+
// Delete email from folder
|
|
19
|
+
mailRoutes.delete('/:folder/:id', deleteMailHandler);
|
|
20
|
+
export { mailRoutes };
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
import type { MailFolder } from '@lovelybunch/types';
|
|
3
|
+
/**
|
|
4
|
+
* GET /api/v1/mail/:folder
|
|
5
|
+
* List emails in a folder
|
|
6
|
+
*/
|
|
7
|
+
export declare function listMailHandler(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
8
|
+
success: false;
|
|
9
|
+
error: string;
|
|
10
|
+
}, 400, "json">) | (Response & import("hono").TypedResponse<{
|
|
11
|
+
success: true;
|
|
12
|
+
data: {
|
|
13
|
+
messages: {
|
|
14
|
+
id: string;
|
|
15
|
+
emailId: string;
|
|
16
|
+
from: string;
|
|
17
|
+
to: string[];
|
|
18
|
+
cc: string[];
|
|
19
|
+
bcc: string[];
|
|
20
|
+
subject: string;
|
|
21
|
+
messageId: string;
|
|
22
|
+
attachments: {
|
|
23
|
+
id: string;
|
|
24
|
+
filename: string;
|
|
25
|
+
contentType: string;
|
|
26
|
+
contentDisposition?: string;
|
|
27
|
+
contentId?: string;
|
|
28
|
+
}[];
|
|
29
|
+
receivedAt: string;
|
|
30
|
+
content: string;
|
|
31
|
+
folder: MailFolder;
|
|
32
|
+
action?: string;
|
|
33
|
+
processing?: boolean;
|
|
34
|
+
}[];
|
|
35
|
+
};
|
|
36
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
37
|
+
success: false;
|
|
38
|
+
error: any;
|
|
39
|
+
}, 500, "json">)>;
|
|
40
|
+
/**
|
|
41
|
+
* GET /api/v1/mail/:folder/:id
|
|
42
|
+
* Get a specific email
|
|
43
|
+
*/
|
|
44
|
+
export declare function getMailHandler(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
45
|
+
success: false;
|
|
46
|
+
error: string;
|
|
47
|
+
}, 400, "json">) | (Response & import("hono").TypedResponse<{
|
|
48
|
+
success: false;
|
|
49
|
+
error: string;
|
|
50
|
+
}, 404, "json">) | (Response & import("hono").TypedResponse<{
|
|
51
|
+
success: true;
|
|
52
|
+
data: {
|
|
53
|
+
message: {
|
|
54
|
+
id: string;
|
|
55
|
+
emailId: string;
|
|
56
|
+
from: string;
|
|
57
|
+
to: string[];
|
|
58
|
+
cc: string[];
|
|
59
|
+
bcc: string[];
|
|
60
|
+
subject: string;
|
|
61
|
+
messageId: string;
|
|
62
|
+
attachments: {
|
|
63
|
+
id: string;
|
|
64
|
+
filename: string;
|
|
65
|
+
contentType: string;
|
|
66
|
+
contentDisposition?: string;
|
|
67
|
+
contentId?: string;
|
|
68
|
+
}[];
|
|
69
|
+
receivedAt: string;
|
|
70
|
+
content: string;
|
|
71
|
+
folder: MailFolder;
|
|
72
|
+
action?: string;
|
|
73
|
+
processing?: boolean;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
77
|
+
success: false;
|
|
78
|
+
error: any;
|
|
79
|
+
}, 500, "json">)>;
|
|
80
|
+
/**
|
|
81
|
+
* PUT /api/v1/mail/:id/status
|
|
82
|
+
* Set email read/unread status
|
|
83
|
+
*/
|
|
84
|
+
export declare function setMailStatusHandler(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
85
|
+
success: false;
|
|
86
|
+
error: string;
|
|
87
|
+
}, 400, "json">) | (Response & import("hono").TypedResponse<{
|
|
88
|
+
success: true;
|
|
89
|
+
data: {
|
|
90
|
+
message: {
|
|
91
|
+
id: string;
|
|
92
|
+
emailId: string;
|
|
93
|
+
from: string;
|
|
94
|
+
to: string[];
|
|
95
|
+
cc: string[];
|
|
96
|
+
bcc: string[];
|
|
97
|
+
subject: string;
|
|
98
|
+
messageId: string;
|
|
99
|
+
attachments: {
|
|
100
|
+
id: string;
|
|
101
|
+
filename: string;
|
|
102
|
+
contentType: string;
|
|
103
|
+
contentDisposition?: string;
|
|
104
|
+
contentId?: string;
|
|
105
|
+
}[];
|
|
106
|
+
receivedAt: string;
|
|
107
|
+
content: string;
|
|
108
|
+
folder: MailFolder;
|
|
109
|
+
action?: string;
|
|
110
|
+
processing?: boolean;
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
114
|
+
success: false;
|
|
115
|
+
error: any;
|
|
116
|
+
}, 404 | 500, "json">)>;
|
|
117
|
+
/**
|
|
118
|
+
* POST /api/v1/mail/:id/reply
|
|
119
|
+
* Reply to an email
|
|
120
|
+
*/
|
|
121
|
+
export declare function replyMailHandler(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
122
|
+
success: false;
|
|
123
|
+
error: string;
|
|
124
|
+
}, 400, "json">) | (Response & import("hono").TypedResponse<{
|
|
125
|
+
success: true;
|
|
126
|
+
data: {
|
|
127
|
+
message: {
|
|
128
|
+
id: string;
|
|
129
|
+
emailId: string;
|
|
130
|
+
from: string;
|
|
131
|
+
to: string[];
|
|
132
|
+
cc: string[];
|
|
133
|
+
bcc: string[];
|
|
134
|
+
subject: string;
|
|
135
|
+
messageId: string;
|
|
136
|
+
attachments: {
|
|
137
|
+
id: string;
|
|
138
|
+
filename: string;
|
|
139
|
+
contentType: string;
|
|
140
|
+
contentDisposition?: string;
|
|
141
|
+
contentId?: string;
|
|
142
|
+
}[];
|
|
143
|
+
receivedAt: string;
|
|
144
|
+
content: string;
|
|
145
|
+
folder: MailFolder;
|
|
146
|
+
action?: string;
|
|
147
|
+
processing?: boolean;
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
151
|
+
success: false;
|
|
152
|
+
error: any;
|
|
153
|
+
}, 404 | 500, "json">)>;
|
|
154
|
+
/**
|
|
155
|
+
* POST /api/v1/mail/send
|
|
156
|
+
* Send an email (coming soon)
|
|
157
|
+
*/
|
|
158
|
+
export declare function sendMailHandler(c: Context): Promise<Response & import("hono").TypedResponse<{
|
|
159
|
+
success: false;
|
|
160
|
+
error: string;
|
|
161
|
+
}, 501, "json">>;
|
|
162
|
+
/**
|
|
163
|
+
* DELETE /api/v1/mail/:folder/:id
|
|
164
|
+
* Delete an email
|
|
165
|
+
*/
|
|
166
|
+
export declare function deleteMailHandler(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
167
|
+
success: false;
|
|
168
|
+
error: string;
|
|
169
|
+
}, 400, "json">) | (Response & import("hono").TypedResponse<{
|
|
170
|
+
success: false;
|
|
171
|
+
error: string;
|
|
172
|
+
}, 404, "json">) | (Response & import("hono").TypedResponse<{
|
|
173
|
+
success: true;
|
|
174
|
+
message: string;
|
|
175
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
176
|
+
success: false;
|
|
177
|
+
error: any;
|
|
178
|
+
}, 500, "json">)>;
|
|
179
|
+
/**
|
|
180
|
+
* PUT /api/v1/mail/:id/action
|
|
181
|
+
* Set agent action summary on an email
|
|
182
|
+
*/
|
|
183
|
+
export declare function setMailActionHandler(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
184
|
+
success: false;
|
|
185
|
+
error: string;
|
|
186
|
+
}, 400, "json">) | (Response & import("hono").TypedResponse<{
|
|
187
|
+
success: true;
|
|
188
|
+
data: {
|
|
189
|
+
message: {
|
|
190
|
+
id: string;
|
|
191
|
+
emailId: string;
|
|
192
|
+
from: string;
|
|
193
|
+
to: string[];
|
|
194
|
+
cc: string[];
|
|
195
|
+
bcc: string[];
|
|
196
|
+
subject: string;
|
|
197
|
+
messageId: string;
|
|
198
|
+
attachments: {
|
|
199
|
+
id: string;
|
|
200
|
+
filename: string;
|
|
201
|
+
contentType: string;
|
|
202
|
+
contentDisposition?: string;
|
|
203
|
+
contentId?: string;
|
|
204
|
+
}[];
|
|
205
|
+
receivedAt: string;
|
|
206
|
+
content: string;
|
|
207
|
+
folder: MailFolder;
|
|
208
|
+
action?: string;
|
|
209
|
+
processing?: boolean;
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
213
|
+
success: false;
|
|
214
|
+
error: any;
|
|
215
|
+
}, 404 | 500, "json">)>;
|
|
216
|
+
/**
|
|
217
|
+
* POST /api/v1/mail/inbound
|
|
218
|
+
* Receive email via Resend webhook
|
|
219
|
+
*/
|
|
220
|
+
export declare function inboundWebhookHandler(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
221
|
+
success: false;
|
|
222
|
+
error: string;
|
|
223
|
+
}, 400, "json">) | (Response & import("hono").TypedResponse<{
|
|
224
|
+
success: true;
|
|
225
|
+
data: {
|
|
226
|
+
message: {
|
|
227
|
+
id: string;
|
|
228
|
+
emailId: string;
|
|
229
|
+
from: string;
|
|
230
|
+
to: string[];
|
|
231
|
+
cc: string[];
|
|
232
|
+
bcc: string[];
|
|
233
|
+
subject: string;
|
|
234
|
+
messageId: string;
|
|
235
|
+
attachments: {
|
|
236
|
+
id: string;
|
|
237
|
+
filename: string;
|
|
238
|
+
contentType: string;
|
|
239
|
+
contentDisposition?: string;
|
|
240
|
+
contentId?: string;
|
|
241
|
+
}[];
|
|
242
|
+
receivedAt: string;
|
|
243
|
+
content: string;
|
|
244
|
+
folder: MailFolder;
|
|
245
|
+
action?: string;
|
|
246
|
+
processing?: boolean;
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
}, 201, "json">) | (Response & import("hono").TypedResponse<{
|
|
250
|
+
success: false;
|
|
251
|
+
error: any;
|
|
252
|
+
}, 500, "json">)>;
|