clawport-ui 0.1.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 (132) hide show
  1. package/.env.example +35 -0
  2. package/BRANDING.md +131 -0
  3. package/CLAUDE.md +252 -0
  4. package/README.md +262 -0
  5. package/SETUP.md +337 -0
  6. package/app/agents/[id]/page.tsx +727 -0
  7. package/app/api/agents/route.ts +12 -0
  8. package/app/api/chat/[id]/route.ts +139 -0
  9. package/app/api/cron-runs/route.ts +13 -0
  10. package/app/api/crons/route.ts +12 -0
  11. package/app/api/kanban/chat/[id]/route.ts +119 -0
  12. package/app/api/kanban/chat-history/[ticketId]/route.ts +36 -0
  13. package/app/api/memory/route.ts +12 -0
  14. package/app/api/transcribe/route.ts +37 -0
  15. package/app/api/tts/route.ts +42 -0
  16. package/app/chat/[id]/page.tsx +10 -0
  17. package/app/chat/page.tsx +200 -0
  18. package/app/crons/page.tsx +870 -0
  19. package/app/docs/page.tsx +399 -0
  20. package/app/favicon.ico +0 -0
  21. package/app/globals.css +692 -0
  22. package/app/kanban/page.tsx +327 -0
  23. package/app/layout.tsx +45 -0
  24. package/app/memory/page.tsx +685 -0
  25. package/app/page.tsx +817 -0
  26. package/app/providers.tsx +37 -0
  27. package/app/settings/page.tsx +901 -0
  28. package/app/settings-provider.tsx +209 -0
  29. package/components/AgentAvatar.tsx +54 -0
  30. package/components/AgentNode.tsx +122 -0
  31. package/components/Breadcrumbs.tsx +126 -0
  32. package/components/DynamicFavicon.tsx +62 -0
  33. package/components/ErrorState.tsx +97 -0
  34. package/components/FeedView.tsx +494 -0
  35. package/components/GlobalSearch.tsx +571 -0
  36. package/components/GridView.tsx +532 -0
  37. package/components/ManorMap.tsx +157 -0
  38. package/components/MobileSidebar.tsx +251 -0
  39. package/components/NavLinks.tsx +271 -0
  40. package/components/OnboardingWizard.tsx +1067 -0
  41. package/components/Sidebar.tsx +115 -0
  42. package/components/ThemeToggle.tsx +108 -0
  43. package/components/chat/AgentList.tsx +537 -0
  44. package/components/chat/ConversationView.tsx +1047 -0
  45. package/components/chat/FileAttachment.tsx +140 -0
  46. package/components/chat/MediaPreview.tsx +111 -0
  47. package/components/chat/VoiceMessage.tsx +139 -0
  48. package/components/crons/PipelineGraph.tsx +327 -0
  49. package/components/crons/WeeklySchedule.tsx +630 -0
  50. package/components/docs/AgentsSection.tsx +209 -0
  51. package/components/docs/ApiReferenceSection.tsx +256 -0
  52. package/components/docs/ArchitectureSection.tsx +221 -0
  53. package/components/docs/ComponentsSection.tsx +253 -0
  54. package/components/docs/CronSystemSection.tsx +235 -0
  55. package/components/docs/DocSection.tsx +346 -0
  56. package/components/docs/GettingStartedSection.tsx +169 -0
  57. package/components/docs/ThemingSection.tsx +257 -0
  58. package/components/docs/TroubleshootingSection.tsx +200 -0
  59. package/components/kanban/AgentPicker.tsx +321 -0
  60. package/components/kanban/CreateTicketModal.tsx +333 -0
  61. package/components/kanban/KanbanBoard.tsx +70 -0
  62. package/components/kanban/KanbanColumn.tsx +166 -0
  63. package/components/kanban/TicketCard.tsx +245 -0
  64. package/components/kanban/TicketDetailPanel.tsx +850 -0
  65. package/components/ui/badge.tsx +48 -0
  66. package/components/ui/button.tsx +64 -0
  67. package/components/ui/card.tsx +92 -0
  68. package/components/ui/dialog.tsx +158 -0
  69. package/components/ui/scroll-area.tsx +58 -0
  70. package/components/ui/separator.tsx +28 -0
  71. package/components/ui/skeleton.tsx +27 -0
  72. package/components/ui/tabs.tsx +91 -0
  73. package/components/ui/tooltip.tsx +57 -0
  74. package/components.json +23 -0
  75. package/docs/API.md +648 -0
  76. package/docs/COMPONENTS.md +1059 -0
  77. package/docs/THEMING.md +795 -0
  78. package/lib/agents-registry.ts +35 -0
  79. package/lib/agents.json +282 -0
  80. package/lib/agents.test.ts +367 -0
  81. package/lib/agents.ts +32 -0
  82. package/lib/anthropic.test.ts +422 -0
  83. package/lib/anthropic.ts +220 -0
  84. package/lib/api-error.ts +16 -0
  85. package/lib/audio-recorder.test.ts +72 -0
  86. package/lib/audio-recorder.ts +169 -0
  87. package/lib/conversations.test.ts +331 -0
  88. package/lib/conversations.ts +117 -0
  89. package/lib/cron-pipelines.test.ts +69 -0
  90. package/lib/cron-pipelines.ts +58 -0
  91. package/lib/cron-runs.test.ts +118 -0
  92. package/lib/cron-runs.ts +67 -0
  93. package/lib/cron-utils.test.ts +222 -0
  94. package/lib/cron-utils.ts +160 -0
  95. package/lib/crons.test.ts +502 -0
  96. package/lib/crons.ts +114 -0
  97. package/lib/env.test.ts +44 -0
  98. package/lib/env.ts +14 -0
  99. package/lib/kanban/automation.test.ts +245 -0
  100. package/lib/kanban/automation.ts +143 -0
  101. package/lib/kanban/chat-store.test.ts +149 -0
  102. package/lib/kanban/chat-store.ts +81 -0
  103. package/lib/kanban/store.test.ts +238 -0
  104. package/lib/kanban/store.ts +98 -0
  105. package/lib/kanban/types.ts +50 -0
  106. package/lib/kanban/useAgentWork.ts +78 -0
  107. package/lib/memory.ts +45 -0
  108. package/lib/multimodal.test.ts +219 -0
  109. package/lib/multimodal.ts +68 -0
  110. package/lib/pipeline.integration.test.ts +343 -0
  111. package/lib/sanitize.ts +194 -0
  112. package/lib/settings.test.ts +137 -0
  113. package/lib/settings.ts +94 -0
  114. package/lib/styles.ts +24 -0
  115. package/lib/themes.ts +9 -0
  116. package/lib/transcribe.test.ts +141 -0
  117. package/lib/transcribe.ts +111 -0
  118. package/lib/types.ts +66 -0
  119. package/lib/utils.ts +6 -0
  120. package/lib/validation.test.ts +132 -0
  121. package/lib/validation.ts +80 -0
  122. package/next.config.ts +7 -0
  123. package/package.json +56 -0
  124. package/postcss.config.mjs +7 -0
  125. package/public/file.svg +1 -0
  126. package/public/globe.svg +1 -0
  127. package/public/next.svg +1 -0
  128. package/public/vercel.svg +1 -0
  129. package/public/window.svg +1 -0
  130. package/scripts/setup.mjs +215 -0
  131. package/tsconfig.json +34 -0
  132. package/vitest.config.ts +17 -0
@@ -0,0 +1,238 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import {
3
+ loadTickets,
4
+ saveTickets,
5
+ createTicket,
6
+ updateTicket,
7
+ moveTicket,
8
+ deleteTicket,
9
+ getTicketsByStatus,
10
+ type KanbanStore,
11
+ } from './store'
12
+
13
+ // Mock localStorage
14
+ const storage: Record<string, string> = {}
15
+ beforeEach(() => {
16
+ Object.keys(storage).forEach((k) => delete storage[k])
17
+ vi.stubGlobal('localStorage', {
18
+ getItem: (key: string) => storage[key] ?? null,
19
+ setItem: (key: string, val: string) => { storage[key] = val },
20
+ removeItem: (key: string) => { delete storage[key] },
21
+ })
22
+ })
23
+
24
+ // Mock crypto.randomUUID
25
+ beforeEach(() => {
26
+ let counter = 0
27
+ vi.stubGlobal('crypto', {
28
+ randomUUID: () => `test-uuid-${++counter}`,
29
+ })
30
+ })
31
+
32
+ // Default work state fields for test tickets
33
+ const WORK_DEFAULTS = { workState: 'idle' as const, workStartedAt: null, workError: null, workResult: null }
34
+
35
+ describe('loadTickets', () => {
36
+ it('returns empty object when nothing stored', () => {
37
+ expect(loadTickets()).toEqual({})
38
+ })
39
+
40
+ it('returns parsed data from localStorage', () => {
41
+ const data = { 'id-1': { id: 'id-1', title: 'Test' } }
42
+ storage['clawport-kanban'] = JSON.stringify(data)
43
+ const loaded = loadTickets()
44
+ expect(loaded['id-1'].id).toBe('id-1')
45
+ expect(loaded['id-1'].title).toBe('Test')
46
+ // Backfilled work state fields
47
+ expect(loaded['id-1'].workState).toBe('idle')
48
+ expect(loaded['id-1'].workStartedAt).toBeNull()
49
+ expect(loaded['id-1'].workError).toBeNull()
50
+ })
51
+
52
+ it('returns empty object on invalid JSON', () => {
53
+ storage['clawport-kanban'] = 'not-json'
54
+ expect(loadTickets()).toEqual({})
55
+ })
56
+ })
57
+
58
+ describe('saveTickets', () => {
59
+ it('persists to localStorage', () => {
60
+ const store: KanbanStore = {}
61
+ saveTickets(store)
62
+ expect(storage['clawport-kanban']).toBe('{}')
63
+ })
64
+ })
65
+
66
+ describe('createTicket', () => {
67
+ it('adds a ticket with generated id and timestamps', () => {
68
+ const store: KanbanStore = {}
69
+ const result = createTicket(store, {
70
+ title: 'New ticket',
71
+ description: 'Do the thing',
72
+ status: 'backlog',
73
+ priority: 'medium',
74
+ assigneeId: null,
75
+ assigneeRole: null,
76
+ })
77
+
78
+ const ticket = result['test-uuid-1']
79
+ expect(ticket).toBeDefined()
80
+ expect(ticket.title).toBe('New ticket')
81
+ expect(ticket.status).toBe('backlog')
82
+ expect(ticket.id).toBe('test-uuid-1')
83
+ expect(ticket.createdAt).toBeTypeOf('number')
84
+ expect(ticket.updatedAt).toBe(ticket.createdAt)
85
+ })
86
+
87
+ it('preserves existing tickets', () => {
88
+ const store: KanbanStore = {
89
+ existing: {
90
+ id: 'existing',
91
+ title: 'Existing',
92
+ description: '',
93
+ status: 'todo',
94
+ priority: 'low',
95
+ assigneeId: null,
96
+ assigneeRole: null,
97
+ ...WORK_DEFAULTS,
98
+ createdAt: 1000,
99
+ updatedAt: 1000,
100
+ },
101
+ }
102
+ const result = createTicket(store, {
103
+ title: 'New',
104
+ description: '',
105
+ status: 'backlog',
106
+ priority: 'medium',
107
+ assigneeId: null,
108
+ assigneeRole: null,
109
+ })
110
+ expect(result['existing']).toBeDefined()
111
+ expect(Object.keys(result)).toHaveLength(2)
112
+ })
113
+ })
114
+
115
+ describe('updateTicket', () => {
116
+ const baseStore: KanbanStore = {
117
+ 't1': {
118
+ id: 't1',
119
+ title: 'Original',
120
+ description: 'Desc',
121
+ status: 'backlog',
122
+ priority: 'low',
123
+ assigneeId: null,
124
+ assigneeRole: null,
125
+ ...WORK_DEFAULTS,
126
+ createdAt: 1000,
127
+ updatedAt: 1000,
128
+ },
129
+ }
130
+
131
+ it('updates specified fields', () => {
132
+ const result = updateTicket(baseStore, 't1', { title: 'Updated' })
133
+ expect(result['t1'].title).toBe('Updated')
134
+ expect(result['t1'].description).toBe('Desc')
135
+ expect(result['t1'].updatedAt).toBeGreaterThan(1000)
136
+ })
137
+
138
+ it('returns store unchanged for missing ticket', () => {
139
+ const result = updateTicket(baseStore, 'missing', { title: 'X' })
140
+ expect(result).toBe(baseStore)
141
+ })
142
+ })
143
+
144
+ describe('moveTicket', () => {
145
+ it('changes ticket status', () => {
146
+ const store: KanbanStore = {
147
+ 't1': {
148
+ id: 't1',
149
+ title: 'Task',
150
+ description: '',
151
+ status: 'backlog',
152
+ priority: 'medium',
153
+ assigneeId: null,
154
+ assigneeRole: null,
155
+ ...WORK_DEFAULTS,
156
+ createdAt: 1000,
157
+ updatedAt: 1000,
158
+ },
159
+ }
160
+ const result = moveTicket(store, 't1', 'in-progress')
161
+ expect(result['t1'].status).toBe('in-progress')
162
+ })
163
+ })
164
+
165
+ describe('deleteTicket', () => {
166
+ it('removes the ticket', () => {
167
+ const store: KanbanStore = {
168
+ 't1': {
169
+ id: 't1',
170
+ title: 'Task',
171
+ description: '',
172
+ status: 'backlog',
173
+ priority: 'medium',
174
+ assigneeId: null,
175
+ assigneeRole: null,
176
+ ...WORK_DEFAULTS,
177
+ createdAt: 1000,
178
+ updatedAt: 1000,
179
+ },
180
+ }
181
+ const result = deleteTicket(store, 't1')
182
+ expect(result['t1']).toBeUndefined()
183
+ })
184
+
185
+ it('preserves other tickets', () => {
186
+ const store: KanbanStore = {
187
+ 't1': {
188
+ id: 't1', title: 'A', description: '', status: 'backlog',
189
+ priority: 'low', assigneeId: null, assigneeRole: null,
190
+ ...WORK_DEFAULTS, createdAt: 1000, updatedAt: 1000,
191
+ },
192
+ 't2': {
193
+ id: 't2', title: 'B', description: '', status: 'todo',
194
+ priority: 'high', assigneeId: null, assigneeRole: null,
195
+ ...WORK_DEFAULTS, createdAt: 2000, updatedAt: 2000,
196
+ },
197
+ }
198
+ const result = deleteTicket(store, 't1')
199
+ expect(result['t2']).toBeDefined()
200
+ expect(Object.keys(result)).toHaveLength(1)
201
+ })
202
+ })
203
+
204
+ describe('getTicketsByStatus', () => {
205
+ const store: KanbanStore = {
206
+ 't1': {
207
+ id: 't1', title: 'Old', description: '', status: 'backlog',
208
+ priority: 'low', assigneeId: null, assigneeRole: null,
209
+ ...WORK_DEFAULTS, createdAt: 1000, updatedAt: 1000,
210
+ },
211
+ 't2': {
212
+ id: 't2', title: 'New', description: '', status: 'backlog',
213
+ priority: 'medium', assigneeId: null, assigneeRole: null,
214
+ ...WORK_DEFAULTS, createdAt: 2000, updatedAt: 3000,
215
+ },
216
+ 't3': {
217
+ id: 't3', title: 'Other', description: '', status: 'todo',
218
+ priority: 'high', assigneeId: null, assigneeRole: null,
219
+ ...WORK_DEFAULTS, createdAt: 1500, updatedAt: 1500,
220
+ },
221
+ }
222
+
223
+ it('filters by status', () => {
224
+ const backlog = getTicketsByStatus(store, 'backlog')
225
+ expect(backlog).toHaveLength(2)
226
+ expect(backlog.every((t) => t.status === 'backlog')).toBe(true)
227
+ })
228
+
229
+ it('sorts by updatedAt descending', () => {
230
+ const backlog = getTicketsByStatus(store, 'backlog')
231
+ expect(backlog[0].id).toBe('t2')
232
+ expect(backlog[1].id).toBe('t1')
233
+ })
234
+
235
+ it('returns empty array for empty column', () => {
236
+ expect(getTicketsByStatus(store, 'done')).toEqual([])
237
+ })
238
+ })
@@ -0,0 +1,98 @@
1
+ 'use client'
2
+
3
+ import type { KanbanTicket, TicketStatus } from './types'
4
+
5
+ export type KanbanStore = Record<string, KanbanTicket>
6
+
7
+ const STORAGE_KEY = 'clawport-kanban'
8
+
9
+ export function loadTickets(): KanbanStore {
10
+ if (typeof window === 'undefined') return {}
11
+ try {
12
+ const raw = localStorage.getItem(STORAGE_KEY)
13
+ if (!raw) return {}
14
+ const store: KanbanStore = JSON.parse(raw)
15
+ // Backfill work state fields for existing tickets
16
+ for (const id of Object.keys(store)) {
17
+ store[id] = {
18
+ ...store[id],
19
+ workState: store[id].workState ?? 'idle',
20
+ workStartedAt: store[id].workStartedAt ?? null,
21
+ workError: store[id].workError ?? null,
22
+ workResult: store[id].workResult ?? null,
23
+ }
24
+ }
25
+ return store
26
+ } catch {
27
+ return {}
28
+ }
29
+ }
30
+
31
+ export function saveTickets(store: KanbanStore): void {
32
+ if (typeof window === 'undefined') return
33
+ try {
34
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(store))
35
+ } catch {}
36
+ }
37
+
38
+ export function createTicket(
39
+ store: KanbanStore,
40
+ ticket: Omit<KanbanTicket, 'id' | 'createdAt' | 'updatedAt' | 'workState' | 'workStartedAt' | 'workError' | 'workResult'> & {
41
+ workState?: KanbanTicket['workState']
42
+ workStartedAt?: KanbanTicket['workStartedAt']
43
+ workError?: KanbanTicket['workError']
44
+ workResult?: KanbanTicket['workResult']
45
+ },
46
+ ): KanbanStore {
47
+ const id = crypto.randomUUID()
48
+ const now = Date.now()
49
+ return {
50
+ ...store,
51
+ [id]: {
52
+ ...ticket,
53
+ id,
54
+ workState: ticket.workState ?? 'idle',
55
+ workStartedAt: ticket.workStartedAt ?? null,
56
+ workError: ticket.workError ?? null,
57
+ workResult: ticket.workResult ?? null,
58
+ createdAt: now,
59
+ updatedAt: now,
60
+ },
61
+ }
62
+ }
63
+
64
+ export function updateTicket(
65
+ store: KanbanStore,
66
+ id: string,
67
+ updates: Partial<Omit<KanbanTicket, 'id' | 'createdAt'>>,
68
+ ): KanbanStore {
69
+ const existing = store[id]
70
+ if (!existing) return store
71
+ return {
72
+ ...store,
73
+ [id]: { ...existing, ...updates, updatedAt: Date.now() },
74
+ }
75
+ }
76
+
77
+ export function moveTicket(
78
+ store: KanbanStore,
79
+ id: string,
80
+ status: TicketStatus,
81
+ ): KanbanStore {
82
+ return updateTicket(store, id, { status })
83
+ }
84
+
85
+ export function deleteTicket(store: KanbanStore, id: string): KanbanStore {
86
+ const next = { ...store }
87
+ delete next[id]
88
+ return next
89
+ }
90
+
91
+ export function getTicketsByStatus(
92
+ store: KanbanStore,
93
+ status: TicketStatus,
94
+ ): KanbanTicket[] {
95
+ return Object.values(store)
96
+ .filter((t) => t.status === status)
97
+ .sort((a, b) => b.updatedAt - a.updatedAt)
98
+ }
@@ -0,0 +1,50 @@
1
+ // Kanban board types
2
+
3
+ export type TicketStatus = 'backlog' | 'todo' | 'in-progress' | 'review' | 'done'
4
+
5
+ export type TicketPriority = 'low' | 'medium' | 'high'
6
+
7
+ export type TeamRole = 'lead-dev' | 'ux-ui' | 'qa'
8
+
9
+ export type WorkState = 'idle' | 'starting' | 'working' | 'done' | 'failed'
10
+
11
+ export interface KanbanTicket {
12
+ id: string
13
+ title: string
14
+ description: string
15
+ status: TicketStatus
16
+ priority: TicketPriority
17
+ assigneeId: string | null // agent id from agents.json
18
+ assigneeRole: TeamRole | null
19
+ workState: WorkState
20
+ workStartedAt: number | null
21
+ workError: string | null
22
+ workResult: string | null
23
+ createdAt: number
24
+ updatedAt: number
25
+ }
26
+
27
+ export interface KanbanColumn {
28
+ id: TicketStatus
29
+ title: string
30
+ }
31
+
32
+ export const COLUMNS: KanbanColumn[] = [
33
+ { id: 'backlog', title: 'Backlog' },
34
+ { id: 'todo', title: 'To Do' },
35
+ { id: 'in-progress', title: 'In Progress' },
36
+ { id: 'review', title: 'Review' },
37
+ { id: 'done', title: 'Done' },
38
+ ]
39
+
40
+ export const PRIORITY_COLORS: Record<TicketPriority, string> = {
41
+ low: 'var(--system-green)',
42
+ medium: 'var(--system-orange)',
43
+ high: 'var(--system-red)',
44
+ }
45
+
46
+ export const ROLE_LABELS: Record<TeamRole, string> = {
47
+ 'lead-dev': 'Lead Dev',
48
+ 'ux-ui': 'UX/UI Lead',
49
+ 'qa': 'QA',
50
+ }
@@ -0,0 +1,78 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useCallback } from 'react'
4
+ import type { KanbanTicket, TicketStatus } from './types'
5
+ import type { KanbanStore } from './store'
6
+ import { executeWork, getWorkPrompt, persistWorkChat } from './automation'
7
+
8
+ interface UseAgentWorkOptions {
9
+ tickets: KanbanStore
10
+ onUpdateTicket: (ticketId: string, updates: Partial<KanbanTicket>) => void
11
+ }
12
+
13
+ export function useAgentWork({ tickets, onUpdateTicket }: UseAgentWorkOptions) {
14
+ const activeWork = useRef<Set<string>>(new Set())
15
+
16
+ const runWork = useCallback(async (ticket: KanbanTicket) => {
17
+ const { id, assigneeId } = ticket
18
+ if (!assigneeId) return
19
+
20
+ // Move to in-progress + set working state
21
+ onUpdateTicket(id, {
22
+ status: 'in-progress' as TicketStatus,
23
+ workState: 'working',
24
+ workStartedAt: Date.now(),
25
+ workError: null,
26
+ })
27
+
28
+ const result = await executeWork(assigneeId, ticket)
29
+
30
+ if (result.success) {
31
+ // Save chat history so TicketDetailPanel picks it up
32
+ const prompt = getWorkPrompt(ticket)
33
+ persistWorkChat(id, prompt, result.content)
34
+
35
+ // Move to review with result
36
+ onUpdateTicket(id, {
37
+ status: 'review' as TicketStatus,
38
+ workState: 'done',
39
+ workResult: result.content,
40
+ })
41
+ } else {
42
+ // Stay in-progress with error
43
+ onUpdateTicket(id, {
44
+ workState: 'failed',
45
+ workError: result.error || 'Agent work failed',
46
+ })
47
+ }
48
+
49
+ activeWork.current.delete(id)
50
+ }, [onUpdateTicket])
51
+
52
+ // Scan for eligible tickets
53
+ useEffect(() => {
54
+ const eligible = Object.values(tickets).filter(
55
+ (t) => t.status === 'todo' && t.assigneeId && t.workState === 'idle',
56
+ )
57
+
58
+ for (const ticket of eligible) {
59
+ if (activeWork.current.has(ticket.id)) continue
60
+
61
+ // Mark as active immediately to prevent double-execution
62
+ activeWork.current.add(ticket.id)
63
+
64
+ // Set starting state synchronously to prevent re-triggers on next render
65
+ onUpdateTicket(ticket.id, { workState: 'starting' })
66
+
67
+ // Fire async work
68
+ runWork(ticket)
69
+ }
70
+ }, [tickets, onUpdateTicket, runWork])
71
+
72
+ const isWorking = useCallback(
73
+ (ticketId: string): boolean => activeWork.current.has(ticketId),
74
+ [],
75
+ )
76
+
77
+ return { isWorking }
78
+ }
package/lib/memory.ts ADDED
@@ -0,0 +1,45 @@
1
+ import { MemoryFile } from '@/lib/types'
2
+ import { readFileSync, existsSync, statSync } from 'fs'
3
+ import { requireEnv } from '@/lib/env'
4
+
5
+ function readMemoryFile(label: string, filePath: string): MemoryFile | null {
6
+ try {
7
+ if (!existsSync(filePath)) return null
8
+ const content = readFileSync(filePath, 'utf-8')
9
+ const stats = statSync(filePath)
10
+ return {
11
+ label,
12
+ path: filePath,
13
+ content,
14
+ lastModified: stats.mtime.toISOString(),
15
+ }
16
+ } catch {
17
+ return null
18
+ }
19
+ }
20
+
21
+ export async function getMemoryFiles(): Promise<MemoryFile[]> {
22
+ const workspacePath = requireEnv('WORKSPACE_PATH')
23
+ const today = new Date()
24
+ const yesterday = new Date(today)
25
+ yesterday.setDate(yesterday.getDate() - 1)
26
+
27
+ const todayStr = today.toISOString().slice(0, 10)
28
+ const yesterdayStr = yesterday.toISOString().slice(0, 10)
29
+
30
+ const candidates: [string, string][] = [
31
+ ['Long-Term Memory (Jarvis)', workspacePath + '/MEMORY.md'],
32
+ ['Team Memory', workspacePath + '/memory/team-memory.md'],
33
+ ['Team Intel (JSON)', workspacePath + '/memory/team-intel.json'],
34
+ ['Daily Log (Today)', workspacePath + '/memory/' + todayStr + '.md'],
35
+ ['Daily Log (Yesterday)', workspacePath + '/memory/' + yesterdayStr + '.md'],
36
+ ]
37
+
38
+ const files: MemoryFile[] = []
39
+ for (const [label, path] of candidates) {
40
+ const file = readMemoryFile(label, path)
41
+ if (file) files.push(file)
42
+ }
43
+
44
+ return files
45
+ }