@lovelybunch/api 1.0.78-alpha.0 → 1.0.78-alpha.2
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 +38 -3
- package/dist/routes/api/v1/context/architecture/route.d.ts +3 -0
- package/dist/routes/api/v1/context/architecture/route.js +245 -0
- package/dist/routes/api/v1/context/project/route.d.ts +3 -0
- package/dist/routes/api/v1/context/project/route.js +200 -0
- package/dist/routes/api/v1/jobs/[id]/route.d.ts +8 -8
- package/dist/routes/api/v1/jobs/route.d.ts +8 -8
- package/dist/routes/api/v1/mail/route.d.ts +3 -3
- package/package.json +4 -4
- package/static/assets/{ActivityPage-DiwGC2nD.js → ActivityPage-B83wvBeD.js} +1 -1
- package/static/assets/{AgentsContextEditPage-Xh10BIpD.js → AgentsContextEditPage-CgHVDbMV.js} +1 -1
- package/static/assets/{AgentsContextPage-DYLVgjBy.js → AgentsContextPage-DMf6khxC.js} +1 -1
- package/static/assets/{ApiKeysSettingsPage-21Rz7etb.js → ApiKeysSettingsPage-DzwmYsom.js} +1 -1
- package/static/assets/{AuthSettingsPage-CuDQH02c.js → AuthSettingsPage-B5auAs_s.js} +1 -1
- package/static/assets/{CallbackPage-Fok1FSep.js → CallbackPage-Cxc5lXE4.js} +1 -1
- package/static/assets/{CoconutCallbackPage-CJCSsq2v.js → CoconutCallbackPage-D7Xkrh70.js} +1 -1
- package/static/assets/{CodePage-CzEzWmjy.js → CodePage-D37jAzN1.js} +1 -1
- package/static/assets/{CollapsibleSection-CAtdqP8-.js → CollapsibleSection-Bp5C0iRa.js} +1 -1
- package/static/assets/{DashboardPage-i5uE_kdH.js → DashboardPage-DvWWsMjr.js} +1 -1
- package/static/assets/{GitPage-Do-L6D0R.js → GitPage-yXgpwI2f.js} +1 -1
- package/static/assets/{GitSettingsPage-GZ5wAGvy.js → GitSettingsPage-B5bgtLBU.js} +1 -1
- package/static/assets/{IdentityPage-BJe8PLkl.js → IdentityPage-Dpr6Z5Id.js} +1 -1
- package/static/assets/{ImplementationStepsEditor-C5ERpOXx.js → ImplementationStepsEditor-B2e4ML0a.js} +1 -1
- package/static/assets/{IntegrationsSettingsPage-Bw9hlc_l.js → IntegrationsSettingsPage-CHknhE_l.js} +1 -1
- package/static/assets/{JobDetailPage-DvPv3vVS.js → JobDetailPage-BnuDmxZF.js} +1 -1
- package/static/assets/{KnowledgeDetailPage-BPhauuYL.js → KnowledgeDetailPage-DFxjt46s.js} +1 -1
- package/static/assets/{KnowledgeEditPage-CQUswyeH.js → KnowledgeEditPage-Bu1l5VHq.js} +1 -1
- package/static/assets/{KnowledgePage-KTCjoesA.js → KnowledgePage-CzPB44SA.js} +1 -1
- package/static/assets/{LoginPage-BYAGsbfR.js → LoginPage-D_ZO_RBy.js} +1 -1
- package/static/assets/{MailInboxPage-DSTys1-R.js → MailInboxPage-CXlwKTvH.js} +1 -1
- package/static/assets/{MailProcessingModal-Ctknm_wU.js → MailProcessingModal-_C2SsbSm.js} +1 -1
- package/static/assets/{MailReadPage-AOANr3XI.js → MailReadPage-BtgLL4bW.js} +1 -1
- package/static/assets/{MailSentPage-BnSYoKkQ.js → MailSentPage-nhZKfZOk.js} +1 -1
- package/static/assets/{McpSettingsPage-CY3iobWJ.js → McpSettingsPage-C6PdoBPD.js} +1 -1
- package/static/assets/{MemoryEditPage-Bh_d-iqV.js → MemoryEditPage-BpioH6_S.js} +1 -1
- package/static/assets/{MemoryPage-CMPTTwSH.js → MemoryPage-DrPL8CG7.js} +1 -1
- package/static/assets/{NewKnowledgePage-B00wknYX.js → NewKnowledgePage-rqgKv0VA.js} +1 -1
- package/static/assets/{NewSkillPage-hNhVRNjh.js → NewSkillPage-0pSUX9z9.js} +1 -1
- package/static/assets/{NewTaskPage-Dq75MGuc.js → NewTaskPage-BpCJGis6.js} +1 -1
- package/static/assets/{NotFoundPage-CR1-vMeX.js → NotFoundPage-sSYe1QSz.js} +1 -1
- package/static/assets/{NotificationsSettingsPage-5aeTZq8-.js → NotificationsSettingsPage-CS_ZT6t0.js} +1 -1
- package/static/assets/{PromptsSettingsPage-Brs1vikj.js → PromptsSettingsPage-DTq8cZqP.js} +1 -1
- package/static/assets/{ResourceDetailPage-3r3KPdJy.js → ResourceDetailPage-DYriLbDS.js} +1 -1
- package/static/assets/{ResourcesPage-CFtSgZva.js → ResourcesPage-D8QBQrZP.js} +1 -1
- package/static/assets/{RoleEditPage-DmSqJEHZ.js → RoleEditPage-Ci_1L_e_.js} +1 -1
- package/static/assets/{RolePage-Dm523gez.js → RolePage-DMJ7_wD0.js} +1 -1
- package/static/assets/{RulesSettingsPage-DewqNsAk.js → RulesSettingsPage-CuB55bRV.js} +1 -1
- package/static/assets/{RunDetailPage-BDUVebTX.js → RunDetailPage-CBW-swIi.js} +1 -1
- package/static/assets/{SchedulePage-DFnoNF-X.js → SchedulePage-BIklILDy.js} +1 -1
- package/static/assets/{SkillDetailPage-DnCPbpIZ.js → SkillDetailPage-D-1PWQiM.js} +1 -1
- package/static/assets/{SkillEditPage-Inn-YsZP.js → SkillEditPage-BZB-YL56.js} +1 -1
- package/static/assets/{SkillsPage-CD-eiUnZ.js → SkillsPage-BezXmUjc.js} +1 -1
- package/static/assets/{SkillsSettingsPage-C1bs2gVd.js → SkillsSettingsPage-Cqo9_iDG.js} +1 -1
- package/static/assets/{SourceInput-CIyYOUd-.js → SourceInput-TEQpt4P-.js} +1 -1
- package/static/assets/{TagInput-fSSAsdov.js → TagInput-DBUiVBgy.js} +1 -1
- package/static/assets/{TaskDetailPage-B2AeYerz.js → TaskDetailPage-Dq0gDy_e.js} +1 -1
- package/static/assets/{TaskEditPage-B9dA1Nr4.js → TaskEditPage-CWGvjTjo.js} +1 -1
- package/static/assets/{TasksPage-DJBVDNw1.js → TasksPage-BIPx8Qde.js} +1 -1
- package/static/assets/{TeamEditPage-BGHM7C8n.js → TeamEditPage-BlNa1eMV.js} +1 -1
- package/static/assets/{TeamPage-CGxpXKc4.js → TeamPage-D1usM-fA.js} +1 -1
- package/static/assets/{TerminalPage-z5NbYWjF.js → TerminalPage-CLh6hap0.js} +1 -1
- package/static/assets/{TerminalSessionPage-eDZDlEeb.js → TerminalSessionPage-DaHcsdNf.js} +1 -1
- package/static/assets/{UserPreferencesPage-uTC5S4Ig.js → UserPreferencesPage-CL-nPXv7.js} +1 -1
- package/static/assets/{UserSettingsPage-D6uxgoz5.js → UserSettingsPage-BGIalWgn.js} +1 -1
- package/static/assets/{UtilitiesPage-BwoS0vin.js → UtilitiesPage-ewlEzigp.js} +1 -1
- package/static/assets/{alert-CKLRmrya.js → alert-0GENnoy_.js} +1 -1
- package/static/assets/{arrow-down-B7v_FmC2.js → arrow-down-Bfya-JPw.js} +1 -1
- package/static/assets/{arrow-left-CrE2UamQ.js → arrow-left-CY9PaWRG.js} +1 -1
- package/static/assets/{arrow-up-DkJyEUv_.js → arrow-up-BAAEIUsa.js} +1 -1
- package/static/assets/{arrow-up-down-CyKWLTed.js → arrow-up-down-0F-DzXlw.js} +1 -1
- package/static/assets/{badge-CEhr50DH.js → badge-D1sVaZJG.js} +1 -1
- package/static/assets/{browser-modal-BaiHyGXz.js → browser-modal-CELhtOVM.js} +1 -1
- package/static/assets/{card-CEBVsJGA.js → card-D2eA49nP.js} +1 -1
- package/static/assets/{chevron-left-Dwnb59xB.js → chevron-left-Cbc591ge.js} +1 -1
- package/static/assets/{chevron-up-CLuT-8K8.js → chevron-up-DD_GdI6e.js} +1 -1
- package/static/assets/{chevrons-up-DiHFV5Rg.js → chevrons-up-CQ8ZAERh.js} +1 -1
- package/static/assets/{circle-alert-DbwPGG3s.js → circle-alert-DsESVtwL.js} +1 -1
- package/static/assets/{circle-check-DoS8t4b0.js → circle-check-BXiPFdlh.js} +1 -1
- package/static/assets/{circle-check-big-DW9BPHOx.js → circle-check-big-HtBmMXFt.js} +1 -1
- package/static/assets/{circle-play-B5pWyVQu.js → circle-play-KOICOGsB.js} +1 -1
- package/static/assets/{circle-x-D0FfNqWc.js → circle-x-B_4fmWCG.js} +1 -1
- package/static/assets/{clipboard-DesoxJnQ.js → clipboard-ByOf6HYl.js} +1 -1
- package/static/assets/{clock-KMBAqOpw.js → clock-CdH0GtRs.js} +1 -1
- package/static/assets/{code-Jj7V1OI-.js → code-7mdwwNlt.js} +1 -1
- package/static/assets/{download-bCNOpuix.js → download-Bh7rihtL.js} +1 -1
- package/static/assets/{external-link-BbvQVJFO.js → external-link-DGl8WNOe.js} +1 -1
- package/static/assets/{eye-B3bkY8eg.js → eye-C_ckl4MB.js} +1 -1
- package/static/assets/{folder-git-2-DK4rLsn4.js → folder-git-2-C9MVgN94.js} +1 -1
- package/static/assets/{globe-bTMV8w2D.js → globe-BvwRv4le.js} +1 -1
- package/static/assets/{index-4_sGNb1Z.js → index-5M1EX2JZ.js} +1 -1
- package/static/assets/{index-gnXQirFQ.js → index-BD57V9U9.js} +1 -1
- package/static/assets/{index-X-V7B-bx.js → index-BUOPcczo.js} +1 -1
- package/static/assets/{index-D5ZLXUjG.js → index-BjiLTiAs.js} +1 -1
- package/static/assets/{index-C0uIENKu.js → index-Bx26Q2JC.js} +3 -3
- package/static/assets/{index-fkGtFh-U.js → index-C611wGVc.js} +1 -1
- package/static/assets/{index-DtuMv8f8.js → index-CCq3bqJI.js} +1 -1
- package/static/assets/{index-BQ6iW0x3.js → index-CLD_gdRP.js} +1 -1
- package/static/assets/{index-DVczQIDE.js → index-Ci5Lsqgb.js} +1 -1
- package/static/assets/{index-D82OGwJi.js → index-CjL8t7WM.js} +1 -1
- package/static/assets/{index-DHFK1JbA.js → index-CjU4ihqi.js} +1 -1
- package/static/assets/{index-BcFOHKDW.js → index-CnEgD2Gk.js} +1 -1
- package/static/assets/{index-BnXRNrU8.js → index-CzcqLknE.js} +1 -1
- package/static/assets/{index-CZiSycvj.js → index-D1CqgHy0.js} +1 -1
- package/static/assets/{index-CpjimEBF.js → index-DFPcCMnD.js} +1 -1
- package/static/assets/{index-Bys9M37r.js → index-DH0iEeh5.js} +1 -1
- package/static/assets/{index-BbgmwHwE.js → index-DrTHjwZ8.js} +1 -1
- package/static/assets/{index--BGsGBNQ.js → index-Dx8evZfS.js} +1 -1
- package/static/assets/{index-DUahdX7L.js → index-ja6BPXln.js} +1 -1
- package/static/assets/{info-CMb2giNJ.js → info-_JNAJSs5.js} +1 -1
- package/static/assets/{label-DXN216Ba.js → label-fHWG59jZ.js} +1 -1
- package/static/assets/{markdown-editor-MQ7X6BHT.js → markdown-editor-Dmq8d9Xs.js} +3 -3
- package/static/assets/{message-square-f1hNpr6b.js → message-square-BaejhTPJ.js} +1 -1
- package/static/assets/{paperclip-BiwY4VlV.js → paperclip-xnYUFZwQ.js} +1 -1
- package/static/assets/{pause-EbBEDWDr.js → pause-plM13_Um.js} +1 -1
- package/static/assets/{play-BHqUO04d.js → play-DR4AKc9R.js} +1 -1
- package/static/assets/{radio-group-CzhYpC89.js → radio-group-CE9meIEG.js} +1 -1
- package/static/assets/{refresh-cw-BPr-EVyC.js → refresh-cw-CLSROYiq.js} +1 -1
- package/static/assets/{search-BBw4K5iK.js → search-VcMqZsDm.js} +1 -1
- package/static/assets/{select-BLUGdloJ.js → select-ChtjmmOf.js} +1 -1
- package/static/assets/{server-2mf5MwhW.js → server-DVlnoAGu.js} +1 -1
- package/static/assets/{switch-DwGQE3tG.js → switch-BMBFkcUU.js} +1 -1
- package/static/assets/{tabs-33myWTAS.js → tabs-DE4DShnU.js} +1 -1
- package/static/assets/{tag-C-PW20qp.js → tag-czmQSC6D.js} +1 -1
- package/static/assets/{terminal-preview-BHb0aOSR.js → terminal-preview-B8UB7Pz6.js} +1 -1
- package/static/assets/{triangle-alert-CNER-68k.js → triangle-alert-RuOrTU7H.js} +1 -1
- package/static/assets/{use-terminal-GvBteSXs.js → use-terminal-C3O4m83z.js} +1 -1
- package/static/assets/{video-3hk0v1CB.js → video-BEV0GqmB.js} +1 -1
- package/static/index.html +1 -1
- package/dist/config/m2m.d.ts +0 -26
- package/dist/config/m2m.js +0 -27
- package/dist/lib/auth/clerk-m2m-verifier.d.ts +0 -34
- package/dist/lib/auth/clerk-m2m-verifier.js +0 -47
|
@@ -125,6 +125,18 @@ export class JobScheduler {
|
|
|
125
125
|
const job = await this.store.getJob(jobId);
|
|
126
126
|
if (!job) {
|
|
127
127
|
console.warn(`Attempted to run missing job ${jobId}`);
|
|
128
|
+
this.clearTimer(jobId);
|
|
129
|
+
this.jobs.delete(jobId);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
// Re-validate status after the disk read. A scheduled timer may have been
|
|
133
|
+
// armed when the job was active, but the user (or another process) may have
|
|
134
|
+
// paused the job in the interim. Refuse to spawn in that case and drop the
|
|
135
|
+
// stale timer so we don't keep re-firing.
|
|
136
|
+
if (trigger === 'scheduled' && job.status !== 'active') {
|
|
137
|
+
console.warn(`Skipping scheduled run for job ${jobId}: status is ${job.status}`);
|
|
138
|
+
this.clearTimer(jobId);
|
|
139
|
+
this.jobs.set(jobId, { job });
|
|
128
140
|
return null;
|
|
129
141
|
}
|
|
130
142
|
this.runningJobs.add(jobId);
|
|
@@ -245,15 +257,38 @@ export class JobScheduler {
|
|
|
245
257
|
}).catch(err => console.warn('[jobs] Slack notification failed:', err));
|
|
246
258
|
}).catch(() => { });
|
|
247
259
|
}
|
|
260
|
+
// Re-read the job from disk so any pause/edit/delete that happened during
|
|
261
|
+
// the run wins over our in-memory snapshot. Without this, the completion
|
|
262
|
+
// write silently reverts concurrent user changes and resurrects deleted
|
|
263
|
+
// job files, and the subsequent register() re-arms a timer based on the
|
|
264
|
+
// stale start-of-run status.
|
|
265
|
+
const fresh = await this.store.getJob(job.id);
|
|
266
|
+
if (!fresh || fresh._error) {
|
|
267
|
+
if (!fresh) {
|
|
268
|
+
console.warn(`Job ${job.id} was deleted during run; not persisting completion state`);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
console.warn(`Job ${job.id} is corrupted on disk; not persisting completion state`);
|
|
272
|
+
}
|
|
273
|
+
this.runningJobs.delete(jobId);
|
|
274
|
+
this.clearTimer(jobId);
|
|
275
|
+
this.jobs.delete(jobId);
|
|
276
|
+
return outcome;
|
|
277
|
+
}
|
|
278
|
+
fresh.runs = [runRecord, ...fresh.runs.filter((r) => r.id !== runRecord.id)].slice(0, 50);
|
|
279
|
+
fresh.metadata.lastRunAt = start;
|
|
280
|
+
fresh.metadata.updatedAt = new Date();
|
|
248
281
|
try {
|
|
249
|
-
await this.store.saveJob(
|
|
282
|
+
await this.store.saveJob(fresh);
|
|
250
283
|
}
|
|
251
284
|
catch (error) {
|
|
252
285
|
console.error(`Failed to persist run state for job ${job.id}:`, error);
|
|
253
286
|
}
|
|
254
287
|
this.runningJobs.delete(jobId);
|
|
255
|
-
// Refresh schedule for next execution
|
|
256
|
-
|
|
288
|
+
// Refresh schedule for next execution from the fresh on-disk state. If the
|
|
289
|
+
// user paused mid-run, fresh.status === 'paused' and register() will clear
|
|
290
|
+
// the timer rather than re-arming it.
|
|
291
|
+
await this.register(fresh);
|
|
257
292
|
return outcome;
|
|
258
293
|
}
|
|
259
294
|
async updateJobMetadata(job, nextRun) {
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import matter from 'gray-matter';
|
|
5
|
+
import { findGaitDirectory } from '../../../../../lib/gait-path.js';
|
|
6
|
+
import { getLogger, ContextKinds } from '@lovelybunch/core/logging';
|
|
7
|
+
import { requireAuth } from '../../../../../middleware/auth.js';
|
|
8
|
+
// Helper function to generate a simple summary from content
|
|
9
|
+
function generateSummary(content, maxLines = 3, maxChars = 200) {
|
|
10
|
+
// Remove markdown formatting for cleaner summary
|
|
11
|
+
const cleanContent = content
|
|
12
|
+
.replace(/^#+\s+/gm, '') // Remove headings
|
|
13
|
+
.replace(/[*_`]/g, '') // Remove bold, italic, code
|
|
14
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Convert links to text
|
|
15
|
+
.trim();
|
|
16
|
+
// Get first few lines
|
|
17
|
+
const lines = cleanContent.split('\n').filter(line => line.trim().length > 0);
|
|
18
|
+
const summary = lines.slice(0, maxLines).join(' ');
|
|
19
|
+
// Truncate if too long
|
|
20
|
+
return summary.length > maxChars
|
|
21
|
+
? summary.substring(0, maxChars) + '...'
|
|
22
|
+
: summary;
|
|
23
|
+
}
|
|
24
|
+
const app = new Hono();
|
|
25
|
+
async function getArchitecturePath() {
|
|
26
|
+
const gaitDir = await findGaitDirectory();
|
|
27
|
+
if (!gaitDir)
|
|
28
|
+
return null;
|
|
29
|
+
return path.join(gaitDir, 'context');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* GET /api/v1/context/architecture
|
|
33
|
+
* Load architecture document
|
|
34
|
+
*/
|
|
35
|
+
app.get('/', async (c) => {
|
|
36
|
+
try {
|
|
37
|
+
const architecturePath = await getArchitecturePath();
|
|
38
|
+
if (!architecturePath) {
|
|
39
|
+
return c.json({
|
|
40
|
+
success: false,
|
|
41
|
+
error: 'GAIT directory not found'
|
|
42
|
+
}, 404);
|
|
43
|
+
}
|
|
44
|
+
// Ensure directory exists
|
|
45
|
+
await fs.mkdir(architecturePath, { recursive: true });
|
|
46
|
+
// Look for architecture.md file
|
|
47
|
+
const filePath = path.join(architecturePath, 'architecture.md');
|
|
48
|
+
try {
|
|
49
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
50
|
+
const { data, content } = matter(fileContent);
|
|
51
|
+
// Extract title from first heading or use default
|
|
52
|
+
const title = content.match(/^#\s+(.+)$/m)?.[1] || 'Architecture Overview';
|
|
53
|
+
const document = {
|
|
54
|
+
filename: 'architecture.md',
|
|
55
|
+
metadata: {
|
|
56
|
+
...data,
|
|
57
|
+
updated: data.updated || new Date().toISOString().split('T')[0],
|
|
58
|
+
type: 'architecture',
|
|
59
|
+
category: 'design',
|
|
60
|
+
tags: data.tags || []
|
|
61
|
+
},
|
|
62
|
+
content,
|
|
63
|
+
title
|
|
64
|
+
};
|
|
65
|
+
return c.json({
|
|
66
|
+
success: true,
|
|
67
|
+
document
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (fileError) {
|
|
71
|
+
// If file doesn't exist, return default content
|
|
72
|
+
const defaultContent = `# Architecture Overview
|
|
73
|
+
|
|
74
|
+
This document describes the overall architecture and design patterns used in this project.
|
|
75
|
+
|
|
76
|
+
## System Architecture
|
|
77
|
+
|
|
78
|
+
Describe the high-level system architecture, including major components and their relationships.
|
|
79
|
+
|
|
80
|
+
## Design Patterns
|
|
81
|
+
|
|
82
|
+
Document the key design patterns and architectural principles followed in this project.
|
|
83
|
+
|
|
84
|
+
### Frontend Architecture
|
|
85
|
+
|
|
86
|
+
- Component structure
|
|
87
|
+
- State management
|
|
88
|
+
- Routing approach
|
|
89
|
+
- UI/UX patterns
|
|
90
|
+
|
|
91
|
+
### Backend Architecture
|
|
92
|
+
|
|
93
|
+
- API design
|
|
94
|
+
- Data layer
|
|
95
|
+
- Service architecture
|
|
96
|
+
- Authentication & authorization
|
|
97
|
+
|
|
98
|
+
## Data Flow
|
|
99
|
+
|
|
100
|
+
Explain how data flows through the system, including:
|
|
101
|
+
|
|
102
|
+
- Request/response cycles
|
|
103
|
+
- Data transformations
|
|
104
|
+
- Caching strategies
|
|
105
|
+
- Error handling
|
|
106
|
+
|
|
107
|
+
## Technology Decisions
|
|
108
|
+
|
|
109
|
+
Document key technology choices and the reasoning behind them:
|
|
110
|
+
|
|
111
|
+
- Framework selections
|
|
112
|
+
- Database choices
|
|
113
|
+
- Third-party integrations
|
|
114
|
+
- Development tools
|
|
115
|
+
|
|
116
|
+
## Deployment Architecture
|
|
117
|
+
|
|
118
|
+
Describe the deployment strategy and infrastructure:
|
|
119
|
+
|
|
120
|
+
- Environment setup
|
|
121
|
+
- CI/CD pipeline
|
|
122
|
+
- Monitoring and logging
|
|
123
|
+
- Scaling considerations
|
|
124
|
+
|
|
125
|
+
## Security Considerations
|
|
126
|
+
|
|
127
|
+
Outline security measures and best practices:
|
|
128
|
+
|
|
129
|
+
- Authentication mechanisms
|
|
130
|
+
- Data protection
|
|
131
|
+
- API security
|
|
132
|
+
- Infrastructure security
|
|
133
|
+
|
|
134
|
+
## Performance Considerations
|
|
135
|
+
|
|
136
|
+
Document performance optimization strategies:
|
|
137
|
+
|
|
138
|
+
- Caching layers
|
|
139
|
+
- Database optimization
|
|
140
|
+
- Frontend performance
|
|
141
|
+
- Monitoring and metrics
|
|
142
|
+
`;
|
|
143
|
+
const document = {
|
|
144
|
+
filename: 'architecture.md',
|
|
145
|
+
metadata: {
|
|
146
|
+
version: '1.0',
|
|
147
|
+
updated: new Date().toISOString().split('T')[0],
|
|
148
|
+
type: 'architecture',
|
|
149
|
+
category: 'design',
|
|
150
|
+
tags: []
|
|
151
|
+
},
|
|
152
|
+
content: defaultContent,
|
|
153
|
+
title: 'Architecture Overview'
|
|
154
|
+
};
|
|
155
|
+
return c.json({
|
|
156
|
+
success: true,
|
|
157
|
+
document
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
console.error('Error loading architecture document:', error);
|
|
163
|
+
return c.json({
|
|
164
|
+
success: false,
|
|
165
|
+
error: 'Failed to load architecture document'
|
|
166
|
+
}, 500);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
/**
|
|
170
|
+
* PUT /api/v1/context/architecture
|
|
171
|
+
* Update architecture document
|
|
172
|
+
*/
|
|
173
|
+
app.put('/', async (c) => {
|
|
174
|
+
try {
|
|
175
|
+
const body = await c.req.json();
|
|
176
|
+
if (!body.content) {
|
|
177
|
+
return c.json({ success: false, error: 'Content is required' }, 400);
|
|
178
|
+
}
|
|
179
|
+
const architecturePath = await getArchitecturePath();
|
|
180
|
+
if (!architecturePath) {
|
|
181
|
+
return c.json({ success: false, error: 'GAIT directory not found' }, 404);
|
|
182
|
+
}
|
|
183
|
+
await fs.mkdir(architecturePath, { recursive: true });
|
|
184
|
+
const filePath = path.join(architecturePath, 'architecture.md');
|
|
185
|
+
// Read current content if it exists
|
|
186
|
+
let currentData = {};
|
|
187
|
+
try {
|
|
188
|
+
const currentContent = await fs.readFile(filePath, 'utf-8');
|
|
189
|
+
const { data } = matter(currentContent);
|
|
190
|
+
currentData = data;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// File doesn't exist, use defaults
|
|
194
|
+
}
|
|
195
|
+
// Prepare updated metadata
|
|
196
|
+
const updatedMetadata = {
|
|
197
|
+
...currentData,
|
|
198
|
+
...body.metadata,
|
|
199
|
+
updated: new Date().toISOString().split('T')[0],
|
|
200
|
+
type: 'architecture',
|
|
201
|
+
category: 'design',
|
|
202
|
+
version: currentData.version || '1.0'
|
|
203
|
+
};
|
|
204
|
+
// Create the markdown content with frontmatter
|
|
205
|
+
const fileContent = matter.stringify(body.content, updatedMetadata);
|
|
206
|
+
await fs.writeFile(filePath, fileContent, 'utf-8');
|
|
207
|
+
// Extract updated title
|
|
208
|
+
const title = body.title ||
|
|
209
|
+
body.content.match(/^#\s+(.+)$/m)?.[1] ||
|
|
210
|
+
'Architecture Overview';
|
|
211
|
+
// Log architecture update event
|
|
212
|
+
try {
|
|
213
|
+
const session = await requireAuth(c);
|
|
214
|
+
const actor = session ? `human:${session.email}` : "human:unknown";
|
|
215
|
+
const logger = getLogger();
|
|
216
|
+
logger.log({
|
|
217
|
+
kind: ContextKinds.ARCHITECTURE_UPDATE,
|
|
218
|
+
actor,
|
|
219
|
+
subject: `context:architecture.md`,
|
|
220
|
+
tags: ["context", "architecture"],
|
|
221
|
+
payload: {
|
|
222
|
+
title,
|
|
223
|
+
summary: generateSummary(body.content),
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
catch (logError) {
|
|
228
|
+
console.error('Error logging architecture update:', logError);
|
|
229
|
+
}
|
|
230
|
+
return c.json({
|
|
231
|
+
success: true,
|
|
232
|
+
document: {
|
|
233
|
+
filename: 'architecture.md',
|
|
234
|
+
title,
|
|
235
|
+
metadata: updatedMetadata,
|
|
236
|
+
content: body.content
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
console.error('Error updating architecture document:', error);
|
|
242
|
+
return c.json({ success: false, error: 'Failed to update architecture document' }, 500);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
export default app;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import matter from 'gray-matter';
|
|
5
|
+
import { findGaitDirectory } from '../../../../../lib/gait-path.js';
|
|
6
|
+
import { getLogger, ContextKinds } from '@lovelybunch/core/logging';
|
|
7
|
+
import { requireAuth } from '../../../../../middleware/auth.js';
|
|
8
|
+
// Helper function to generate a simple summary from content
|
|
9
|
+
function generateSummary(content, maxLines = 3, maxChars = 200) {
|
|
10
|
+
// Remove markdown formatting for cleaner summary
|
|
11
|
+
const cleanContent = content
|
|
12
|
+
.replace(/^#+\s+/gm, '') // Remove headings
|
|
13
|
+
.replace(/[*_`]/g, '') // Remove bold, italic, code
|
|
14
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Convert links to text
|
|
15
|
+
.trim();
|
|
16
|
+
// Get first few lines
|
|
17
|
+
const lines = cleanContent.split('\n').filter(line => line.trim().length > 0);
|
|
18
|
+
const summary = lines.slice(0, maxLines).join(' ');
|
|
19
|
+
// Truncate if too long
|
|
20
|
+
return summary.length > maxChars
|
|
21
|
+
? summary.substring(0, maxChars) + '...'
|
|
22
|
+
: summary;
|
|
23
|
+
}
|
|
24
|
+
const app = new Hono();
|
|
25
|
+
async function getProjectPath() {
|
|
26
|
+
const gaitDir = await findGaitDirectory();
|
|
27
|
+
if (!gaitDir)
|
|
28
|
+
return null;
|
|
29
|
+
return path.join(gaitDir, 'context');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* GET /api/v1/context/project
|
|
33
|
+
* Load project overview document
|
|
34
|
+
*/
|
|
35
|
+
app.get('/', async (c) => {
|
|
36
|
+
try {
|
|
37
|
+
const projectPath = await getProjectPath();
|
|
38
|
+
if (!projectPath) {
|
|
39
|
+
return c.json({
|
|
40
|
+
success: false,
|
|
41
|
+
error: 'GAIT directory not found'
|
|
42
|
+
}, 404);
|
|
43
|
+
}
|
|
44
|
+
// Ensure directory exists
|
|
45
|
+
await fs.mkdir(projectPath, { recursive: true });
|
|
46
|
+
// Look for project.md file
|
|
47
|
+
const filePath = path.join(projectPath, 'project.md');
|
|
48
|
+
try {
|
|
49
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
50
|
+
const { data, content } = matter(fileContent);
|
|
51
|
+
// Extract title from first heading or use default
|
|
52
|
+
const title = content.match(/^#\s+(.+)$/m)?.[1] || 'Project Overview';
|
|
53
|
+
const document = {
|
|
54
|
+
filename: 'project.md',
|
|
55
|
+
metadata: {
|
|
56
|
+
...data,
|
|
57
|
+
updated: data.updated || new Date().toISOString().split('T')[0],
|
|
58
|
+
type: 'project',
|
|
59
|
+
category: 'overview',
|
|
60
|
+
tags: data.tags || []
|
|
61
|
+
},
|
|
62
|
+
content,
|
|
63
|
+
title
|
|
64
|
+
};
|
|
65
|
+
return c.json({
|
|
66
|
+
success: true,
|
|
67
|
+
document
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (fileError) {
|
|
71
|
+
// If file doesn't exist, return default content
|
|
72
|
+
const defaultContent = `# Project Overview
|
|
73
|
+
|
|
74
|
+
This document provides an overview of the project, including its purpose, goals, and key information.
|
|
75
|
+
|
|
76
|
+
## Purpose
|
|
77
|
+
|
|
78
|
+
Describe the main purpose and objectives of this project.
|
|
79
|
+
|
|
80
|
+
## Key Features
|
|
81
|
+
|
|
82
|
+
- Feature 1
|
|
83
|
+
- Feature 2
|
|
84
|
+
- Feature 3
|
|
85
|
+
|
|
86
|
+
## Technology Stack
|
|
87
|
+
|
|
88
|
+
List the main technologies, frameworks, and tools used in this project.
|
|
89
|
+
|
|
90
|
+
## Getting Started
|
|
91
|
+
|
|
92
|
+
Provide instructions for setting up and running the project locally.
|
|
93
|
+
|
|
94
|
+
## Documentation
|
|
95
|
+
|
|
96
|
+
Links to additional documentation, guides, and resources.
|
|
97
|
+
`;
|
|
98
|
+
const document = {
|
|
99
|
+
filename: 'project.md',
|
|
100
|
+
metadata: {
|
|
101
|
+
version: '1.0',
|
|
102
|
+
updated: new Date().toISOString().split('T')[0],
|
|
103
|
+
type: 'project',
|
|
104
|
+
category: 'overview',
|
|
105
|
+
tags: []
|
|
106
|
+
},
|
|
107
|
+
content: defaultContent,
|
|
108
|
+
title: 'Project Overview'
|
|
109
|
+
};
|
|
110
|
+
return c.json({
|
|
111
|
+
success: true,
|
|
112
|
+
document
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error('Error loading project document:', error);
|
|
118
|
+
return c.json({
|
|
119
|
+
success: false,
|
|
120
|
+
error: 'Failed to load project document'
|
|
121
|
+
}, 500);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
/**
|
|
125
|
+
* PUT /api/v1/context/project
|
|
126
|
+
* Update project overview document
|
|
127
|
+
*/
|
|
128
|
+
app.put('/', async (c) => {
|
|
129
|
+
try {
|
|
130
|
+
const body = await c.req.json();
|
|
131
|
+
if (!body.content) {
|
|
132
|
+
return c.json({ success: false, error: 'Content is required' }, 400);
|
|
133
|
+
}
|
|
134
|
+
const projectPath = await getProjectPath();
|
|
135
|
+
if (!projectPath) {
|
|
136
|
+
return c.json({ success: false, error: 'GAIT directory not found' }, 404);
|
|
137
|
+
}
|
|
138
|
+
await fs.mkdir(projectPath, { recursive: true });
|
|
139
|
+
const filePath = path.join(projectPath, 'project.md');
|
|
140
|
+
// Read current content if it exists
|
|
141
|
+
let currentData = {};
|
|
142
|
+
try {
|
|
143
|
+
const currentContent = await fs.readFile(filePath, 'utf-8');
|
|
144
|
+
const { data } = matter(currentContent);
|
|
145
|
+
currentData = data;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// File doesn't exist, use defaults
|
|
149
|
+
}
|
|
150
|
+
// Prepare updated metadata
|
|
151
|
+
const updatedMetadata = {
|
|
152
|
+
...currentData,
|
|
153
|
+
...body.metadata,
|
|
154
|
+
updated: new Date().toISOString().split('T')[0],
|
|
155
|
+
type: 'project',
|
|
156
|
+
category: 'overview',
|
|
157
|
+
version: currentData.version || '1.0'
|
|
158
|
+
};
|
|
159
|
+
// Create the markdown content with frontmatter
|
|
160
|
+
const fileContent = matter.stringify(body.content, updatedMetadata);
|
|
161
|
+
await fs.writeFile(filePath, fileContent, 'utf-8');
|
|
162
|
+
// Extract updated title
|
|
163
|
+
const title = body.title ||
|
|
164
|
+
body.content.match(/^#\s+(.+)$/m)?.[1] ||
|
|
165
|
+
'Project Overview';
|
|
166
|
+
// Log project update event
|
|
167
|
+
try {
|
|
168
|
+
const session = await requireAuth(c);
|
|
169
|
+
const actor = session ? `human:${session.email}` : "human:unknown";
|
|
170
|
+
const logger = getLogger();
|
|
171
|
+
logger.log({
|
|
172
|
+
kind: ContextKinds.PROJECT_UPDATE,
|
|
173
|
+
actor,
|
|
174
|
+
subject: `context:project.md`,
|
|
175
|
+
tags: ["context", "project"],
|
|
176
|
+
payload: {
|
|
177
|
+
title,
|
|
178
|
+
summary: generateSummary(body.content),
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch (logError) {
|
|
183
|
+
console.error('Error logging project update:', logError);
|
|
184
|
+
}
|
|
185
|
+
return c.json({
|
|
186
|
+
success: true,
|
|
187
|
+
document: {
|
|
188
|
+
filename: 'project.md',
|
|
189
|
+
title,
|
|
190
|
+
metadata: updatedMetadata,
|
|
191
|
+
content: body.content
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
console.error('Error updating project document:', error);
|
|
197
|
+
return c.json({ success: false, error: 'Failed to update project document' }, 500);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
export default app;
|
|
@@ -9,6 +9,9 @@ export declare function GET(c: Context): Promise<(Response & import("hono").Type
|
|
|
9
9
|
}, 404, "json">) | (Response & import("hono").TypedResponse<{
|
|
10
10
|
success: true;
|
|
11
11
|
data: {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
status: ScheduledJobStatus;
|
|
12
15
|
schedule: {
|
|
13
16
|
type: "cron";
|
|
14
17
|
expression: string;
|
|
@@ -22,11 +25,8 @@ export declare function GET(c: Context): Promise<(Response & import("hono").Type
|
|
|
22
25
|
anchorHour?: number;
|
|
23
26
|
};
|
|
24
27
|
description?: string;
|
|
25
|
-
id: string;
|
|
26
|
-
name: string;
|
|
27
28
|
prompt: string;
|
|
28
29
|
model: string;
|
|
29
|
-
status: ScheduledJobStatus;
|
|
30
30
|
metadata: {
|
|
31
31
|
createdAt: string;
|
|
32
32
|
updatedAt: string;
|
|
@@ -39,13 +39,13 @@ export declare function GET(c: Context): Promise<(Response & import("hono").Type
|
|
|
39
39
|
agentIds?: string[];
|
|
40
40
|
mcpServers?: string[];
|
|
41
41
|
runs: {
|
|
42
|
+
error?: string;
|
|
42
43
|
id: string;
|
|
43
44
|
status: import("@lovelybunch/types").ScheduledJobRunStatus;
|
|
44
45
|
jobId: string;
|
|
45
46
|
trigger: import("@lovelybunch/types").ScheduledJobTrigger;
|
|
46
47
|
startedAt: string;
|
|
47
48
|
finishedAt?: string;
|
|
48
|
-
error?: string;
|
|
49
49
|
}[];
|
|
50
50
|
};
|
|
51
51
|
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
@@ -70,6 +70,9 @@ export declare function PATCH(c: Context): Promise<(Response & import("hono").Ty
|
|
|
70
70
|
}, 400, "json">) | (Response & import("hono").TypedResponse<{
|
|
71
71
|
success: true;
|
|
72
72
|
data: {
|
|
73
|
+
id: string;
|
|
74
|
+
name: string;
|
|
75
|
+
status: ScheduledJobStatus;
|
|
73
76
|
schedule: {
|
|
74
77
|
type: "cron";
|
|
75
78
|
expression: string;
|
|
@@ -83,11 +86,8 @@ export declare function PATCH(c: Context): Promise<(Response & import("hono").Ty
|
|
|
83
86
|
anchorHour?: number;
|
|
84
87
|
};
|
|
85
88
|
description?: string;
|
|
86
|
-
id: string;
|
|
87
|
-
name: string;
|
|
88
89
|
prompt: string;
|
|
89
90
|
model: string;
|
|
90
|
-
status: ScheduledJobStatus;
|
|
91
91
|
metadata: {
|
|
92
92
|
createdAt: string;
|
|
93
93
|
updatedAt: string;
|
|
@@ -100,13 +100,13 @@ export declare function PATCH(c: Context): Promise<(Response & import("hono").Ty
|
|
|
100
100
|
agentIds?: string[];
|
|
101
101
|
mcpServers?: string[];
|
|
102
102
|
runs: {
|
|
103
|
+
error?: string;
|
|
103
104
|
id: string;
|
|
104
105
|
status: import("@lovelybunch/types").ScheduledJobRunStatus;
|
|
105
106
|
jobId: string;
|
|
106
107
|
trigger: import("@lovelybunch/types").ScheduledJobTrigger;
|
|
107
108
|
startedAt: string;
|
|
108
109
|
finishedAt?: string;
|
|
109
|
-
error?: string;
|
|
110
110
|
}[];
|
|
111
111
|
};
|
|
112
112
|
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
@@ -11,6 +11,9 @@ export declare function withLightweightRuns<T extends {
|
|
|
11
11
|
export declare function GET(c: Context): Promise<(Response & import("hono").TypedResponse<{
|
|
12
12
|
success: true;
|
|
13
13
|
data: {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
status: ScheduledJobStatus;
|
|
14
17
|
schedule: {
|
|
15
18
|
type: "cron";
|
|
16
19
|
expression: string;
|
|
@@ -24,11 +27,8 @@ export declare function GET(c: Context): Promise<(Response & import("hono").Type
|
|
|
24
27
|
anchorHour?: number;
|
|
25
28
|
};
|
|
26
29
|
description?: string;
|
|
27
|
-
id: string;
|
|
28
|
-
name: string;
|
|
29
30
|
prompt: string;
|
|
30
31
|
model: string;
|
|
31
|
-
status: ScheduledJobStatus;
|
|
32
32
|
metadata: {
|
|
33
33
|
createdAt: string;
|
|
34
34
|
updatedAt: string;
|
|
@@ -41,13 +41,13 @@ export declare function GET(c: Context): Promise<(Response & import("hono").Type
|
|
|
41
41
|
agentIds?: string[];
|
|
42
42
|
mcpServers?: string[];
|
|
43
43
|
runs: {
|
|
44
|
+
error?: string;
|
|
44
45
|
id: string;
|
|
45
46
|
status: import("@lovelybunch/types").ScheduledJobRunStatus;
|
|
46
47
|
jobId: string;
|
|
47
48
|
trigger: import("@lovelybunch/types").ScheduledJobTrigger;
|
|
48
49
|
startedAt: string;
|
|
49
50
|
finishedAt?: string;
|
|
50
|
-
error?: string;
|
|
51
51
|
}[];
|
|
52
52
|
}[];
|
|
53
53
|
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
@@ -66,6 +66,9 @@ export declare function POST(c: Context): Promise<(Response & import("hono").Typ
|
|
|
66
66
|
}, 400, "json">) | (Response & import("hono").TypedResponse<{
|
|
67
67
|
success: true;
|
|
68
68
|
data: {
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
status: ScheduledJobStatus;
|
|
69
72
|
schedule: {
|
|
70
73
|
type: "cron";
|
|
71
74
|
expression: string;
|
|
@@ -79,11 +82,8 @@ export declare function POST(c: Context): Promise<(Response & import("hono").Typ
|
|
|
79
82
|
anchorHour?: number;
|
|
80
83
|
};
|
|
81
84
|
description?: string;
|
|
82
|
-
id: string;
|
|
83
|
-
name: string;
|
|
84
85
|
prompt: string;
|
|
85
86
|
model: string;
|
|
86
|
-
status: ScheduledJobStatus;
|
|
87
87
|
metadata: {
|
|
88
88
|
createdAt: string;
|
|
89
89
|
updatedAt: string;
|
|
@@ -96,13 +96,13 @@ export declare function POST(c: Context): Promise<(Response & import("hono").Typ
|
|
|
96
96
|
agentIds?: string[];
|
|
97
97
|
mcpServers?: string[];
|
|
98
98
|
runs: {
|
|
99
|
+
error?: string;
|
|
99
100
|
id: string;
|
|
100
101
|
status: import("@lovelybunch/types").ScheduledJobRunStatus;
|
|
101
102
|
jobId: string;
|
|
102
103
|
trigger: import("@lovelybunch/types").ScheduledJobTrigger;
|
|
103
104
|
startedAt: string;
|
|
104
105
|
finishedAt?: string;
|
|
105
|
-
error?: string;
|
|
106
106
|
}[];
|
|
107
107
|
};
|
|
108
108
|
}, 201, "json">) | (Response & import("hono").TypedResponse<{
|
|
@@ -116,7 +116,7 @@ export declare function setMailStatusHandler(c: Context): Promise<(Response & im
|
|
|
116
116
|
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
117
117
|
success: false;
|
|
118
118
|
error: any;
|
|
119
|
-
},
|
|
119
|
+
}, 404 | 500, "json">)>;
|
|
120
120
|
/**
|
|
121
121
|
* POST /api/v1/mail/:id/reply
|
|
122
122
|
* Reply to an email
|
|
@@ -154,7 +154,7 @@ export declare function replyMailHandler(c: Context): Promise<(Response & import
|
|
|
154
154
|
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
155
155
|
success: false;
|
|
156
156
|
error: any;
|
|
157
|
-
},
|
|
157
|
+
}, 404 | 500, "json">)>;
|
|
158
158
|
/**
|
|
159
159
|
* POST /api/v1/mail/send
|
|
160
160
|
* Send an email (coming soon)
|
|
@@ -217,7 +217,7 @@ export declare function setMailActionHandler(c: Context): Promise<(Response & im
|
|
|
217
217
|
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
218
218
|
success: false;
|
|
219
219
|
error: any;
|
|
220
|
-
},
|
|
220
|
+
}, 404 | 500, "json">)>;
|
|
221
221
|
/**
|
|
222
222
|
* GET /api/v1/mail/:id/processing
|
|
223
223
|
* Get processing status and log tail for an email
|