@lovelybunch/api 1.0.75-alpha.9 → 1.0.76-alpha.0

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 (167) hide show
  1. package/dist/lib/jobs/job-runner.js +10 -2
  2. package/dist/lib/jobs/job-scheduler.js +21 -0
  3. package/dist/lib/mail/mail-runner.d.ts +51 -0
  4. package/dist/lib/mail/mail-runner.js +342 -0
  5. package/dist/lib/slack/slack-service.d.ts +2 -0
  6. package/dist/lib/slack/slack-service.js +3 -0
  7. package/dist/lib/storage/file-storage.d.ts +17 -16
  8. package/dist/lib/storage/file-storage.js +68 -65
  9. package/dist/lib/terminal/terminal-manager.d.ts +3 -3
  10. package/dist/lib/terminal/terminal-manager.js +10 -10
  11. package/dist/routes/api/v1/ai/route.js +39 -19
  12. package/dist/routes/api/v1/git/index.js +23 -0
  13. package/dist/routes/api/v1/mail/index.d.ts +3 -0
  14. package/dist/routes/api/v1/mail/index.js +23 -0
  15. package/dist/routes/api/v1/mail/route.d.ts +294 -0
  16. package/dist/routes/api/v1/mail/route.js +344 -0
  17. package/dist/routes/api/v1/mcp/index.js +32 -32
  18. package/dist/routes/api/v1/slack/index.d.ts +3 -0
  19. package/dist/routes/api/v1/slack/index.js +15 -0
  20. package/dist/routes/api/v1/slack/route.d.ts +124 -0
  21. package/dist/routes/api/v1/slack/route.js +192 -0
  22. package/dist/routes/api/v1/tasks/[id]/route.d.ts +117 -0
  23. package/dist/routes/api/v1/tasks/[id]/route.js +166 -0
  24. package/dist/routes/api/v1/tasks/[id]/steps/[stepId]/route.d.ts +60 -0
  25. package/dist/routes/api/v1/tasks/[id]/steps/[stepId]/route.js +81 -0
  26. package/dist/routes/api/v1/tasks/index.d.ts +3 -0
  27. package/dist/routes/api/v1/tasks/index.js +12 -0
  28. package/dist/routes/api/v1/tasks/route.d.ts +96 -0
  29. package/dist/routes/api/v1/tasks/route.js +136 -0
  30. package/dist/routes/api/v1/terminal/[taskId]/create/index.d.ts +3 -0
  31. package/dist/routes/api/v1/terminal/[taskId]/create/index.js +5 -0
  32. package/dist/routes/api/v1/terminal/[taskId]/create/route.d.ts +10 -0
  33. package/dist/routes/api/v1/terminal/[taskId]/create/route.js +27 -0
  34. package/dist/routes/api/v1/terminal/[taskId]/destroy/index.d.ts +3 -0
  35. package/dist/routes/api/v1/terminal/[taskId]/destroy/index.js +5 -0
  36. package/dist/routes/api/v1/terminal/[taskId]/destroy/route.d.ts +10 -0
  37. package/dist/routes/api/v1/terminal/[taskId]/destroy/route.js +21 -0
  38. package/dist/routes/api/v1/terminal/[taskId]/resize/index.d.ts +3 -0
  39. package/dist/routes/api/v1/terminal/[taskId]/resize/index.js +5 -0
  40. package/dist/routes/api/v1/terminal/[taskId]/resize/route.d.ts +10 -0
  41. package/dist/routes/api/v1/terminal/[taskId]/resize/route.js +21 -0
  42. package/dist/routes/api/v1/terminal/sessions/route.js +4 -4
  43. package/dist/server-with-static.js +12 -8
  44. package/dist/server.js +12 -8
  45. package/package.json +4 -4
  46. package/static/assets/{ActivityPage-OxRci_V2.js → ActivityPage-DxajSxG1.js} +1 -1
  47. package/static/assets/ApiKeysSettingsPage-DiBqSUMz.js +2 -0
  48. package/static/assets/{ArchitectureEditPage-D7xcH6dY.js → ArchitectureEditPage-iflIJaCh.js} +4 -4
  49. package/static/assets/{ArchitecturePage-pvnlX-NW.js → ArchitecturePage-oB3FtdZ7.js} +1 -1
  50. package/static/assets/{AuthSettingsPage-Bu0CZ1rY.js → AuthSettingsPage-BY7x-6wd.js} +2 -2
  51. package/static/assets/{CallbackPage-D0lkjxCT.js → CallbackPage-CYcV3OHa.js} +1 -1
  52. package/static/assets/CodePage-DxJYFhiT.js +2 -0
  53. package/static/assets/{CollapsibleSection-Bt_ZLnJc.js → CollapsibleSection-BLZv27JC.js} +1 -1
  54. package/static/assets/DashboardPage-BdsK2Vor.js +51 -0
  55. package/static/assets/{GitPage-TrTxZ27J.js → GitPage-CKIIMGjF.js} +2 -2
  56. package/static/assets/GitSettingsPage-sI4uzGzt.js +6 -0
  57. package/static/assets/IdentityPage-vzzaKU2-.js +6 -0
  58. package/static/assets/ImplementationStepsEditor-BMHTxNEX.js +6 -0
  59. package/static/assets/{IntegrationsSettingsPage-C2wJVdM7.js → IntegrationsSettingsPage-BzuUrHO1.js} +1 -1
  60. package/static/assets/JobDetailPage-C-IKmhAF.js +1 -0
  61. package/static/assets/{KnowledgeDetailPage-BdTUfWqj.js → KnowledgeDetailPage-C1RHtPzz.js} +1 -1
  62. package/static/assets/{KnowledgeEditPage-D8XK4IUf.js → KnowledgeEditPage-D3gIqqKn.js} +1 -1
  63. package/static/assets/KnowledgePage-DJphs1Kg.js +3 -0
  64. package/static/assets/{LoginPage-Dqxd7cTa.js → LoginPage-DVaToPHL.js} +1 -1
  65. package/static/assets/MailInboxPage-I-MbS647.js +1 -0
  66. package/static/assets/MailProcessingModal-DXtDHWM_.js +6 -0
  67. package/static/assets/MailReadPage-DUN8EQjl.js +1 -0
  68. package/static/assets/MailSentPage-DsGgBGBQ.js +1 -0
  69. package/static/assets/{McpSettingsPage-10n35zXi.js → McpSettingsPage-d66ZIwm7.js} +1 -1
  70. package/static/assets/{NewKnowledgePage-BlJzzuh7.js → NewKnowledgePage-WbN6BikQ.js} +1 -1
  71. package/static/assets/{NewSkillPage-ByqN--mH.js → NewSkillPage-DesCsYgS.js} +1 -1
  72. package/static/assets/NewTaskPage-CrmiPuuw.js +91 -0
  73. package/static/assets/NotFoundPage-BcTtqwNP.js +6 -0
  74. package/static/assets/NotificationsSettingsPage-BeJw7gY7.js +1 -0
  75. package/static/assets/{ProjectEditPage-DKJTY2uc.js → ProjectEditPage-DiHz-pYk.js} +1 -1
  76. package/static/assets/{ProjectPage-2VblKCWz.js → ProjectPage-CKaHBZlw.js} +1 -1
  77. package/static/assets/{PromptsSettingsPage-B4mOhXuo.js → PromptsSettingsPage-B_Opt_KA.js} +1 -1
  78. package/static/assets/ResourceDetailPage-BSeDQxri.js +1 -0
  79. package/static/assets/{ResourcesPage-2BbjIWfF.js → ResourcesPage-YmerqN0J.js} +1 -1
  80. package/static/assets/RoleEditPage-Bm7HG4sg.js +13 -0
  81. package/static/assets/{RolePage-qXWXZ2FZ.js → RolePage-DoN5_uka.js} +1 -1
  82. package/static/assets/{RulesSettingsPage-BtM7p8F6.js → RulesSettingsPage-yQELBKgb.js} +3 -3
  83. package/static/assets/SchedulePage-nmchdGUK.js +4 -0
  84. package/static/assets/SkillDetailPage-BFjBVPS8.js +1 -0
  85. package/static/assets/{SkillEditPage-Czlo8WWT.js → SkillEditPage-CjpscD5K.js} +1 -1
  86. package/static/assets/SkillsPage-Dr_uyKVB.js +8 -0
  87. package/static/assets/{SkillsSettingsPage-DKtpy7qk.js → SkillsSettingsPage-BknrbJBC.js} +1 -1
  88. package/static/assets/{SourceInput-BITn1Y15.js → SourceInput-LclyzQLW.js} +1 -1
  89. package/static/assets/{TagInput-BK91_M1N.js → TagInput-BZ6JyIo1.js} +1 -1
  90. package/static/assets/TaskDetailPage-D97oWW98.js +16 -0
  91. package/static/assets/TaskEditPage-BTfzRYwM.js +1 -0
  92. package/static/assets/TasksPage-COvedmQz.js +17 -0
  93. package/static/assets/TerminalPage-DhbOmISZ.js +1 -0
  94. package/static/assets/TerminalSessionPage-D2rZb8Ej.js +8 -0
  95. package/static/assets/{UserPreferencesPage-DrgYEcxO.js → UserPreferencesPage-CgtsVqvs.js} +1 -1
  96. package/static/assets/UserSettingsPage-AXLWqe0G.js +1 -0
  97. package/static/assets/{UtilitiesPage-Djr4qT5L.js → UtilitiesPage-CxQkYrza.js} +1 -1
  98. package/static/assets/{alert-CsMvyYoX.js → alert-BUrHsk9_.js} +1 -1
  99. package/static/assets/{arrow-down-BZnfbld8.js → arrow-down-J9YP8VW9.js} +1 -1
  100. package/static/assets/{arrow-left-WGBYWq3h.js → arrow-left-RAzvFXpe.js} +1 -1
  101. package/static/assets/{arrow-up-BByVUPE7.js → arrow-up-CjXXRPYC.js} +1 -1
  102. package/static/assets/arrow-up-down-BRoDh-fK.js +6 -0
  103. package/static/assets/{badge-AwLOflf5.js → badge-CA-A_JCd.js} +1 -1
  104. package/static/assets/{browser-modal-BzGNFfTG.js → browser-modal-DVtwh2h7.js} +2 -2
  105. package/static/assets/{card-SN5gKnu7.js → card-DdrUHBUG.js} +1 -1
  106. package/static/assets/{chevron-left-C7uNq9l_.js → chevron-left-C4bGr2Al.js} +1 -1
  107. package/static/assets/{chevron-up-CHdIiLxL.js → chevron-up-CPpQ_jgb.js} +1 -1
  108. package/static/assets/{chevrons-up-TXwQuoUN.js → chevrons-up-C8oR0iOR.js} +1 -1
  109. package/static/assets/{circle-alert-37E5gU9K.js → circle-alert-B3zeVGHG.js} +1 -1
  110. package/static/assets/{circle-check-D02pWDME.js → circle-check-OfRBf8tJ.js} +1 -1
  111. package/static/assets/{circle-check-big-nY4PntB5.js → circle-check-big-CeVxJ4hA.js} +1 -1
  112. package/static/assets/{circle-play-7EXFLo4F.js → circle-play-C_Chmziu.js} +1 -1
  113. package/static/assets/{circle-x-By4JoTHB.js → circle-x-CtfEmATn.js} +1 -1
  114. package/static/assets/{clipboard-BdymjxLO.js → clipboard-Z_0Z5-r1.js} +1 -1
  115. package/static/assets/{clock-HDu44KTo.js → clock-B43LjbrK.js} +1 -1
  116. package/static/assets/code-D77i0toJ.js +6 -0
  117. package/static/assets/{download-Cv2G2Eg9.js → download-C8rLaNF6.js} +1 -1
  118. package/static/assets/{external-link-DwMXcCCj.js → external-link-D6UvIQYD.js} +1 -1
  119. package/static/assets/{eye-DYnjJzdb.js → eye-C3fWwYx-.js} +1 -1
  120. package/static/assets/{folder-git-2-COeWFPHS.js → folder-git-2-Crtn4eyJ.js} +1 -1
  121. package/static/assets/index-CHdBxVyk.css +2 -0
  122. package/static/assets/{index-9Tv-j_Ga.js → index-SWBrq2bx.js} +118 -103
  123. package/static/assets/{info-BmtuPMhv.js → info-BZi8bEGv.js} +1 -1
  124. package/static/assets/kiro-CX1mOsRO.js +17 -0
  125. package/static/assets/{label-TGqbNfMO.js → label-C8Wxd6GE.js} +1 -1
  126. package/static/assets/{markdown-editor-ls1JPK_e.js → markdown-editor-DAk7M9Ju.js} +1 -1
  127. package/static/assets/message-square-BpqFAvyq.js +6 -0
  128. package/static/assets/paperclip-Dk7jEYtI.js +6 -0
  129. package/static/assets/{pause-CAWbvTiL.js → pause-CX3StCWt.js} +1 -1
  130. package/static/assets/{play-DF_Qeu0H.js → play-CRPN1vUy.js} +1 -1
  131. package/static/assets/{radio-group-DYTbywtK.js → radio-group-DFIfCEpZ.js} +1 -1
  132. package/static/assets/{refresh-cw-BFZxHqbC.js → refresh-cw-DGBXSYy5.js} +1 -1
  133. package/static/assets/{search-Dr90tbch.js → search-C9jI6Lg7.js} +1 -1
  134. package/static/assets/{select-Cs5qtMYV.js → select-DgNHsbaX.js} +1 -1
  135. package/static/assets/status-utils-PwF3DXLL.js +1 -0
  136. package/static/assets/{switch-4TDb6YiQ.js → switch-C2zPfM3G.js} +1 -1
  137. package/static/assets/{tabs-BrbEvF4V.js → tabs-B7fjkp5h.js} +1 -1
  138. package/static/assets/{tag-DrQkepeD.js → tag-BOwlwHfi.js} +1 -1
  139. package/static/assets/{terminal-preview-uuKF9_x4.js → terminal-preview-BvBQu5Sd.js} +1 -1
  140. package/static/assets/use-terminal-xv9qDaX1.js +1 -0
  141. package/static/assets/{video-DYA2WfbA.js → video-CW0zFsfp.js} +1 -1
  142. package/static/index.html +2 -2
  143. package/static/assets/ApiKeysSettingsPage-C0evI19e.js +0 -2
  144. package/static/assets/CodePage-BJ4PC5nb.js +0 -2
  145. package/static/assets/DashboardPage-BiffPdmj.js +0 -41
  146. package/static/assets/GitSettingsPage-D7q5xQd_.js +0 -6
  147. package/static/assets/IdentityPage-CY0Ak2j0.js +0 -11
  148. package/static/assets/ImplementationStepsEditor-Ctx0CvbU.js +0 -6
  149. package/static/assets/JobDetailPage-Phx_IlKX.js +0 -1
  150. package/static/assets/KnowledgePage-Ci9G7Br-.js +0 -8
  151. package/static/assets/NewProposalPage-BP7Ttoxk.js +0 -90
  152. package/static/assets/ProposalDetailPage-m3ysyzpj.js +0 -1
  153. package/static/assets/ProposalEditPage-3XVg_paW.js +0 -1
  154. package/static/assets/ProposalsPage-B3u0aFFz.js +0 -17
  155. package/static/assets/ResourceDetailPage-somBLUpC.js +0 -1
  156. package/static/assets/RoleEditPage-CLzX7Xhi.js +0 -13
  157. package/static/assets/SchedulePage-4tFcIBSs.js +0 -4
  158. package/static/assets/SkillDetailPage-CroSdaju.js +0 -1
  159. package/static/assets/SkillsPage-CgULbcI-.js +0 -8
  160. package/static/assets/TerminalPage-8fwvnOo2.js +0 -1
  161. package/static/assets/TerminalSessionPage-BhO5U48p.js +0 -13
  162. package/static/assets/UserSettingsPage-Dj6lKLi8.js +0 -1
  163. package/static/assets/droid-CPteN3f9.js +0 -17
  164. package/static/assets/index-GFQ5RqVh.css +0 -2
  165. package/static/assets/status-utils-BDOyevaX.js +0 -1
  166. package/static/assets/use-terminal-BG5UXuVE.js +0 -1
  167. package/static/assets/zap-h9QOsasv.js +0 -6
@@ -19,6 +19,8 @@ function resolveAgent(model) {
19
19
  return 'codex';
20
20
  if (lower.includes('droid') || lower.includes('factory'))
21
21
  return 'droid';
22
+ if (lower.includes('kiro'))
23
+ return 'kiro';
22
24
  return 'claude';
23
25
  }
24
26
  function buildCommand(agent, instruction, config) {
@@ -54,6 +56,10 @@ function buildCommand(agent, instruction, config) {
54
56
  mainCommand = `droid exec --auto high ${mcpFlags} ${quotedInstruction}`.trim();
55
57
  break;
56
58
  }
59
+ case 'kiro': {
60
+ mainCommand = `kiro-cli chat --no-interactive --trust-all-tools ${quotedInstruction}`;
61
+ break;
62
+ }
57
63
  case 'claude':
58
64
  default: {
59
65
  // Claude uses .mcp.json for MCP server configuration (no --mcp flag)
@@ -68,13 +74,15 @@ const CLI_AGENT_LABEL = {
68
74
  claude: 'Claude',
69
75
  gemini: 'Gemini',
70
76
  codex: 'Codex',
71
- droid: 'Factory Droid'
77
+ droid: 'Factory Droid',
78
+ kiro: 'Kiro'
72
79
  };
73
80
  const CLI_AGENT_BINARY = {
74
81
  claude: 'claude',
75
82
  gemini: 'gemini',
76
83
  codex: 'codex',
77
- droid: 'droid'
84
+ droid: 'droid',
85
+ kiro: 'kiro-cli'
78
86
  };
79
87
  const DEFAULT_MAX_RUNTIME_MS = 30 * 60 * 1000; // 30 minutes
80
88
  function getMaxRuntime() {
@@ -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,51 @@
1
+ interface MailRunResult {
2
+ status: 'succeeded' | 'failed';
3
+ summary?: string;
4
+ outputPath?: string;
5
+ error?: string;
6
+ cliCommand: string;
7
+ }
8
+ export interface ActiveMailProcess {
9
+ mailId: string;
10
+ pid: number;
11
+ agent: string;
12
+ startedAt: string;
13
+ logPath: string;
14
+ }
15
+ export declare class MailRunner {
16
+ private projectRootPromise;
17
+ private activeProcesses;
18
+ constructor();
19
+ private ensureCliAvailable;
20
+ private ensureLogPath;
21
+ private loadSystemPrompt;
22
+ private loadConfig;
23
+ private loadConfigModel;
24
+ isMailProcessingEnabled(): Promise<boolean>;
25
+ private buildInstruction;
26
+ /**
27
+ * Get the status of an active mail processing job.
28
+ * Returns null if no active process exists for this mailId.
29
+ */
30
+ getActiveProcess(mailId: string): ActiveMailProcess | null;
31
+ /**
32
+ * Get all active mail processing jobs.
33
+ */
34
+ getActiveProcesses(): ActiveMailProcess[];
35
+ /**
36
+ * Read the latest log file for a mail processing job.
37
+ * Returns the tail of the log (up to maxBytes).
38
+ */
39
+ readLog(mailId: string, maxBytes?: number): Promise<{
40
+ log: string;
41
+ logPath: string;
42
+ } | null>;
43
+ /**
44
+ * Stop an active mail processing job.
45
+ * Sends SIGTERM, then SIGKILL after 5s.
46
+ */
47
+ stop(mailId: string): Promise<boolean>;
48
+ run(mailId: string, mailFilePath: string): Promise<MailRunResult>;
49
+ }
50
+ export declare function getMailRunner(): MailRunner;
51
+ export {};
@@ -0,0 +1,342 @@
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
+ if (lower.includes('kiro'))
24
+ return 'kiro';
25
+ return 'claude';
26
+ }
27
+ function buildCommand(agent, instruction, runningAsRoot) {
28
+ const quotedInstruction = shellQuote(instruction);
29
+ let mainCommand = '';
30
+ switch (agent) {
31
+ case 'gemini':
32
+ mainCommand = `gemini --yolo -i ${quotedInstruction}`;
33
+ break;
34
+ case 'codex': {
35
+ const baseCmd = `codex ${quotedInstruction} --dangerously-bypass-approvals-and-sandbox`.trim();
36
+ const needsPseudoTty = runningAsRoot && process.platform !== 'win32';
37
+ mainCommand = needsPseudoTty
38
+ ? `script -q -e -c ${shellQuote(baseCmd)} /dev/null`
39
+ : baseCmd;
40
+ break;
41
+ }
42
+ case 'droid':
43
+ mainCommand = `droid exec --auto high ${quotedInstruction}`.trim();
44
+ break;
45
+ case 'kiro':
46
+ mainCommand = `kiro-cli chat --no-interactive --trust-all-tools ${quotedInstruction}`;
47
+ break;
48
+ case 'claude':
49
+ default: {
50
+ const prefix = runningAsRoot ? 'IS_SANDBOX=1 ' : '';
51
+ mainCommand = `${prefix}claude ${quotedInstruction} --dangerously-skip-permissions`.trim();
52
+ break;
53
+ }
54
+ }
55
+ return { command: agent === 'claude' ? 'claude' : agent, shellCommand: mainCommand };
56
+ }
57
+ const CLI_AGENT_LABEL = {
58
+ claude: 'Claude',
59
+ gemini: 'Gemini',
60
+ codex: 'Codex',
61
+ droid: 'Factory Droid',
62
+ kiro: 'Kiro'
63
+ };
64
+ const CLI_AGENT_BINARY = {
65
+ claude: 'claude',
66
+ gemini: 'gemini',
67
+ codex: 'codex',
68
+ droid: 'droid',
69
+ kiro: 'kiro-cli'
70
+ };
71
+ const DEFAULT_MAX_RUNTIME_MS = 15 * 60 * 1000; // 15 minutes for mail processing
72
+ function getMaxRuntime() {
73
+ const raw = process.env.COCONUT_MAIL_MAX_RUNTIME_MS;
74
+ if (!raw)
75
+ return DEFAULT_MAX_RUNTIME_MS;
76
+ const parsed = Number(raw);
77
+ if (!Number.isFinite(parsed) || parsed <= 0) {
78
+ return DEFAULT_MAX_RUNTIME_MS;
79
+ }
80
+ return parsed;
81
+ }
82
+ export class MailRunner {
83
+ projectRootPromise;
84
+ activeProcesses = new Map();
85
+ constructor() {
86
+ this.projectRootPromise = getProjectRoot();
87
+ }
88
+ ensureCliAvailable(agent) {
89
+ const binary = CLI_AGENT_BINARY[agent];
90
+ const result = spawnSync('bash', ['-lc', `command -v ${binary}`], { stdio: 'ignore' });
91
+ if (result.status !== 0) {
92
+ throw new Error(`${CLI_AGENT_LABEL[agent]} CLI ("${binary}") is not installed or not on PATH.`);
93
+ }
94
+ }
95
+ async ensureLogPath(mailId) {
96
+ const projectRoot = await this.projectRootPromise;
97
+ const logsDir = path.join(projectRoot, '.nut', 'mail', 'logs', mailId);
98
+ await fs.mkdir(logsDir, { recursive: true });
99
+ const runId = `run-${Date.now()}`;
100
+ return path.join(logsDir, `${runId}.log`);
101
+ }
102
+ async loadSystemPrompt() {
103
+ const projectRoot = await this.projectRootPromise;
104
+ // Look for the system prompt in the shared package (relative to project root)
105
+ const promptPath = path.join(projectRoot, 'packages', 'shared', 'system-prompts', 'mail-processor.md');
106
+ try {
107
+ return await fs.readFile(promptPath, 'utf-8');
108
+ }
109
+ catch (err) {
110
+ throw new Error(`Failed to load mail processor system prompt from ${promptPath}: ${err.message}`);
111
+ }
112
+ }
113
+ async loadConfig() {
114
+ const projectRoot = await this.projectRootPromise;
115
+ const configPath = path.join(projectRoot, '.nut', 'config.json');
116
+ try {
117
+ const raw = await fs.readFile(configPath, 'utf-8');
118
+ return JSON.parse(raw);
119
+ }
120
+ catch {
121
+ return undefined;
122
+ }
123
+ }
124
+ async loadConfigModel() {
125
+ const config = await this.loadConfig();
126
+ return config?.ai?.model;
127
+ }
128
+ async isMailProcessingEnabled() {
129
+ const config = await this.loadConfig();
130
+ return config?.mail?.enabled ?? false;
131
+ }
132
+ async buildInstruction(mailId, mailFilePath) {
133
+ const systemPrompt = await this.loadSystemPrompt();
134
+ // Substitute placeholders in the system prompt
135
+ const instruction = systemPrompt
136
+ .replace(/\{\{mailFilePath\}\}/g, mailFilePath)
137
+ .replace(/\{\{mailId\}\}/g, mailId);
138
+ return instruction;
139
+ }
140
+ /**
141
+ * Get the status of an active mail processing job.
142
+ * Returns null if no active process exists for this mailId.
143
+ */
144
+ getActiveProcess(mailId) {
145
+ const tracked = this.activeProcesses.get(mailId);
146
+ if (!tracked || !tracked.child.pid)
147
+ return null;
148
+ return {
149
+ mailId,
150
+ pid: tracked.child.pid,
151
+ agent: tracked.agent,
152
+ startedAt: tracked.startedAt,
153
+ logPath: tracked.logPath,
154
+ };
155
+ }
156
+ /**
157
+ * Get all active mail processing jobs.
158
+ */
159
+ getActiveProcesses() {
160
+ const result = [];
161
+ for (const [mailId, tracked] of this.activeProcesses) {
162
+ if (tracked.child.pid) {
163
+ result.push({
164
+ mailId,
165
+ pid: tracked.child.pid,
166
+ agent: tracked.agent,
167
+ startedAt: tracked.startedAt,
168
+ logPath: tracked.logPath,
169
+ });
170
+ }
171
+ }
172
+ return result;
173
+ }
174
+ /**
175
+ * Read the latest log file for a mail processing job.
176
+ * Returns the tail of the log (up to maxBytes).
177
+ */
178
+ async readLog(mailId, maxBytes = 8192) {
179
+ const projectRoot = await this.projectRootPromise;
180
+ const logsDir = path.join(projectRoot, '.nut', 'mail', 'logs', mailId);
181
+ let files;
182
+ try {
183
+ files = await fs.readdir(logsDir);
184
+ }
185
+ catch {
186
+ return null;
187
+ }
188
+ const logFiles = files.filter(f => f.endsWith('.log')).sort();
189
+ if (logFiles.length === 0)
190
+ return null;
191
+ const latestLog = path.join(logsDir, logFiles[logFiles.length - 1]);
192
+ try {
193
+ const stat = await fs.stat(latestLog);
194
+ const start = Math.max(0, stat.size - maxBytes);
195
+ const fh = await fs.open(latestLog, 'r');
196
+ try {
197
+ const buf = Buffer.alloc(Math.min(stat.size, maxBytes));
198
+ await fh.read(buf, 0, buf.length, start);
199
+ let log = buf.toString('utf-8');
200
+ if (start > 0) {
201
+ log = '...' + log.slice(log.indexOf('\n') + 1);
202
+ }
203
+ return { log, logPath: path.relative(projectRoot, latestLog) };
204
+ }
205
+ finally {
206
+ await fh.close();
207
+ }
208
+ }
209
+ catch {
210
+ return null;
211
+ }
212
+ }
213
+ /**
214
+ * Stop an active mail processing job.
215
+ * Sends SIGTERM, then SIGKILL after 5s.
216
+ */
217
+ async stop(mailId) {
218
+ const tracked = this.activeProcesses.get(mailId);
219
+ if (!tracked)
220
+ return false;
221
+ console.log(`[mail] Stopping processing for ${mailId} (PID: ${tracked.child.pid})`);
222
+ tracked.child.kill('SIGTERM');
223
+ setTimeout(() => {
224
+ try {
225
+ tracked.child.kill('SIGKILL');
226
+ }
227
+ catch { /* already dead */ }
228
+ }, 5000);
229
+ return true;
230
+ }
231
+ async run(mailId, mailFilePath) {
232
+ const model = await this.loadConfigModel();
233
+ const agent = resolveAgent(model);
234
+ const instruction = await this.buildInstruction(mailId, mailFilePath);
235
+ const runningAsRoot = typeof process.getuid === 'function' && process.getuid() === 0;
236
+ const { shellCommand } = buildCommand(agent, instruction, runningAsRoot);
237
+ const projectRoot = await this.projectRootPromise;
238
+ const logPath = await this.ensureLogPath(mailId);
239
+ const logStream = createWriteStream(logPath, { flags: 'a' });
240
+ const summaryChunks = [];
241
+ const startedAt = new Date().toISOString();
242
+ logStream.write(`[${startedAt}] Starting mail processing for ${mailId} using ${agent} CLI\n`);
243
+ logStream.write(`Mail file: ${mailFilePath}\n`);
244
+ logStream.write(`Command: ${shellCommand}\n`);
245
+ return new Promise((resolve) => {
246
+ let cliMissingError = null;
247
+ try {
248
+ this.ensureCliAvailable(agent);
249
+ }
250
+ catch (error) {
251
+ cliMissingError = error instanceof Error ? error : new Error(String(error));
252
+ }
253
+ if (cliMissingError) {
254
+ const message = cliMissingError.message;
255
+ logStream.write(`${message}\n`);
256
+ logStream.end();
257
+ setMailProcessing(mailId, false).catch(err => console.warn('[mail] failed to clear processing:', err));
258
+ resolve({
259
+ status: 'failed',
260
+ error: message,
261
+ summary: message,
262
+ outputPath: path.relative(projectRoot, logPath),
263
+ cliCommand: shellCommand,
264
+ });
265
+ return;
266
+ }
267
+ const injectedEnv = getInjectedEnv();
268
+ const child = spawn('bash', ['-lc', shellCommand], {
269
+ cwd: projectRoot,
270
+ env: {
271
+ ...process.env,
272
+ ...injectedEnv
273
+ },
274
+ stdio: ['ignore', 'pipe', 'pipe'],
275
+ });
276
+ const maxRuntime = getMaxRuntime();
277
+ const abortTimeout = setTimeout(() => {
278
+ logStream.write(`\n[${new Date().toISOString()}] Max runtime ${maxRuntime}ms exceeded. Sending SIGTERM...\n`);
279
+ child.kill('SIGTERM');
280
+ setTimeout(() => child.kill('SIGKILL'), 10_000);
281
+ }, maxRuntime);
282
+ // Track the active process
283
+ this.activeProcesses.set(mailId, {
284
+ child,
285
+ agent: CLI_AGENT_LABEL[agent],
286
+ startedAt,
287
+ logPath: path.relative(projectRoot, logPath),
288
+ abortTimeout,
289
+ });
290
+ child.stdout?.on('data', (chunk) => {
291
+ const text = chunk.toString();
292
+ logStream.write(text);
293
+ summaryChunks.push(text);
294
+ });
295
+ child.stderr?.on('data', (chunk) => {
296
+ const text = chunk.toString();
297
+ logStream.write(text);
298
+ summaryChunks.push(text);
299
+ });
300
+ const cleanup = () => {
301
+ this.activeProcesses.delete(mailId);
302
+ clearTimeout(abortTimeout);
303
+ };
304
+ child.on('error', (error) => {
305
+ const message = `Failed to start CLI command: ${error.message}`;
306
+ logStream.write(`${message}\n`);
307
+ logStream.end();
308
+ cleanup();
309
+ setMailProcessing(mailId, false).catch(err => console.warn('[mail] failed to clear processing:', err));
310
+ resolve({
311
+ status: 'failed',
312
+ error: message,
313
+ summary: summaryChunks.join('').slice(-600),
314
+ outputPath: path.relative(projectRoot, logPath),
315
+ cliCommand: shellCommand,
316
+ });
317
+ });
318
+ child.on('close', (code) => {
319
+ const status = code === 0 ? 'succeeded' : 'failed';
320
+ logStream.write(`\n[${new Date().toISOString()}] Mail processing for ${mailId} completed with exit code ${code}\n`);
321
+ logStream.end();
322
+ cleanup();
323
+ setMailProcessing(mailId, false).catch(err => console.warn('[mail] failed to clear processing:', err));
324
+ const summary = summaryChunks.join('');
325
+ resolve({
326
+ status,
327
+ summary: summary.slice(Math.max(0, summary.length - 2000)),
328
+ outputPath: path.relative(projectRoot, logPath),
329
+ error: code === 0 ? undefined : `CLI exited with code ${code}`,
330
+ cliCommand: shellCommand,
331
+ });
332
+ });
333
+ });
334
+ }
335
+ }
336
+ let mailRunnerInstance = null;
337
+ export function getMailRunner() {
338
+ if (!mailRunnerInstance) {
339
+ mailRunnerInstance = new MailRunner();
340
+ }
341
+ return mailRunnerInstance;
342
+ }
@@ -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';
@@ -0,0 +1,3 @@
1
+ // Re-export from @lovelybunch/core for backward compatibility
2
+ // All Slack logic now lives in packages/core/src/slack.ts
3
+ export { SlackService, getSlackService, DEFAULT_SLACK_CONFIG, } from '@lovelybunch/core';
@@ -1,30 +1,31 @@
1
- import { ChangeProposal, CPStatus } from '@lovelybunch/types';
2
- export interface CPFilter {
3
- status?: CPStatus;
1
+ import { Task, TaskStatus } from '@lovelybunch/types';
2
+ export interface TaskFilter {
3
+ status?: TaskStatus;
4
4
  author?: string;
5
5
  priority?: string;
6
6
  tags?: string[];
7
7
  search?: string;
8
8
  }
9
9
  export interface StorageAdapter {
10
- createCP(cp: ChangeProposal): Promise<void>;
11
- getCP(id: string): Promise<ChangeProposal | null>;
12
- updateCP(id: string, cp: Partial<ChangeProposal>): Promise<void>;
13
- deleteCP(id: string): Promise<void>;
14
- listCPs(filter?: CPFilter): Promise<ChangeProposal[]>;
15
- searchCPs(query: string): Promise<ChangeProposal[]>;
10
+ createTask(task: Task): Promise<void>;
11
+ getTask(id: string): Promise<Task | null>;
12
+ updateTask(id: string, task: Partial<Task>): Promise<void>;
13
+ deleteTask(id: string): Promise<void>;
14
+ listTasks(filter?: TaskFilter): Promise<Task[]>;
15
+ searchTasks(query: string): Promise<Task[]>;
16
16
  }
17
17
  export declare class FileStorageAdapter implements StorageAdapter {
18
18
  private basePath;
19
19
  private sanitizeForYAML;
20
20
  constructor(basePath?: string);
21
21
  ensureDirectories(): Promise<void>;
22
- createCP(cp: ChangeProposal): Promise<void>;
23
- getCP(id: string): Promise<ChangeProposal | null>;
24
- updateCP(id: string, updates: Partial<ChangeProposal>): Promise<void>;
25
- deleteCP(id: string): Promise<void>;
26
- listCPs(filter?: CPFilter): Promise<ChangeProposal[]>;
27
- searchCPs(query: string): Promise<ChangeProposal[]>;
28
- private frontmatterToCP;
22
+ createTask(task: Task): Promise<void>;
23
+ getTask(id: string): Promise<Task | null>;
24
+ updateTask(id: string, updates: Partial<Task>): Promise<void>;
25
+ deleteTask(id: string): Promise<void>;
26
+ listTasks(filter?: TaskFilter): Promise<Task[]>;
27
+ searchTasks(query: string): Promise<Task[]>;
28
+ private normalizeStepStatus;
29
+ private frontmatterToTask;
29
30
  private getDefaultContent;
30
31
  }