cognova 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 (205) hide show
  1. package/.env.example +58 -0
  2. package/Claude/CLAUDE.md +92 -0
  3. package/Claude/hooks/lib/__init__.py +1 -0
  4. package/Claude/hooks/lib/hook_client.py +207 -0
  5. package/Claude/hooks/log-event.py +97 -0
  6. package/Claude/hooks/pre-compact.py +46 -0
  7. package/Claude/hooks/session-end.py +26 -0
  8. package/Claude/hooks/session-start.py +35 -0
  9. package/Claude/hooks/stop-extract.py +40 -0
  10. package/Claude/rules/frontmatter.md +54 -0
  11. package/Claude/rules/markdown.md +43 -0
  12. package/Claude/rules/note-organization.md +33 -0
  13. package/Claude/settings.json +54 -0
  14. package/Claude/skills/README.md +136 -0
  15. package/Claude/skills/_lib/__init__.py +1 -0
  16. package/Claude/skills/_lib/api.py +164 -0
  17. package/Claude/skills/_lib/output.py +95 -0
  18. package/Claude/skills/environment/SKILL.md +73 -0
  19. package/Claude/skills/environment/environment.py +239 -0
  20. package/Claude/skills/memory/SKILL.md +153 -0
  21. package/Claude/skills/memory/memory.py +270 -0
  22. package/Claude/skills/project/SKILL.md +105 -0
  23. package/Claude/skills/project/project.py +203 -0
  24. package/Claude/skills/skill-creator/SKILL.md +261 -0
  25. package/Claude/skills/task/SKILL.md +135 -0
  26. package/Claude/skills/task/task.py +310 -0
  27. package/LICENSE +21 -0
  28. package/README.md +176 -0
  29. package/app/app.config.ts +8 -0
  30. package/app/app.vue +39 -0
  31. package/app/assets/css/main.css +10 -0
  32. package/app/components/AppLogo.vue +40 -0
  33. package/app/components/AssistantPanel.client.vue +518 -0
  34. package/app/components/ConfirmModal.vue +84 -0
  35. package/app/components/TemplateMenu.vue +49 -0
  36. package/app/components/agents/AgentActivityChart.client.vue +105 -0
  37. package/app/components/agents/AgentActivityChart.server.vue +25 -0
  38. package/app/components/agents/AgentForm.vue +304 -0
  39. package/app/components/agents/AgentRunModal.vue +154 -0
  40. package/app/components/agents/AgentStatsCards.vue +98 -0
  41. package/app/components/chat/ChatInput.vue +85 -0
  42. package/app/components/chat/ConversationList.vue +78 -0
  43. package/app/components/chat/MessageBubble.vue +81 -0
  44. package/app/components/chat/StreamingMessage.vue +36 -0
  45. package/app/components/chat/ToolCallBlock.vue +77 -0
  46. package/app/components/editor/CodeEditor.client.vue +212 -0
  47. package/app/components/editor/CodeEditorFallback.vue +12 -0
  48. package/app/components/editor/DocumentEditor.vue +326 -0
  49. package/app/components/editor/DocumentMetadata.vue +140 -0
  50. package/app/components/editor/MarkdownEditor.vue +146 -0
  51. package/app/components/files/FileTree.vue +436 -0
  52. package/app/components/hooks/HookActivityChart.client.vue +117 -0
  53. package/app/components/hooks/HookActivityChart.server.vue +25 -0
  54. package/app/components/hooks/HookStatsCards.vue +63 -0
  55. package/app/components/hooks/RecentEventsTable.vue +123 -0
  56. package/app/components/hooks/ToolBreakdownTable.vue +72 -0
  57. package/app/components/search/DashboardSearch.vue +122 -0
  58. package/app/components/tasks/ProjectSelect.vue +35 -0
  59. package/app/components/tasks/TaskCard.vue +182 -0
  60. package/app/components/tasks/TaskDetail.vue +160 -0
  61. package/app/components/tasks/TaskForm.vue +280 -0
  62. package/app/components/tasks/TaskList.vue +69 -0
  63. package/app/components/view/ViewToc.vue +85 -0
  64. package/app/composables/useAgents.ts +153 -0
  65. package/app/composables/useAuth.ts +73 -0
  66. package/app/composables/useChat.ts +298 -0
  67. package/app/composables/useDocument.ts +141 -0
  68. package/app/composables/useEditor.ts +100 -0
  69. package/app/composables/useFileTree.ts +220 -0
  70. package/app/composables/useHookEvents.ts +68 -0
  71. package/app/composables/useMemories.ts +83 -0
  72. package/app/composables/useNotificationBus.ts +154 -0
  73. package/app/composables/usePreferences.ts +131 -0
  74. package/app/composables/useProjects.ts +97 -0
  75. package/app/composables/useSearch.ts +52 -0
  76. package/app/composables/useTasks.ts +201 -0
  77. package/app/composables/useTerminal.ts +135 -0
  78. package/app/layouts/auth.vue +20 -0
  79. package/app/layouts/dashboard.vue +186 -0
  80. package/app/layouts/view.vue +60 -0
  81. package/app/middleware/auth.ts +9 -0
  82. package/app/pages/agents/[id].vue +602 -0
  83. package/app/pages/agents/index.vue +412 -0
  84. package/app/pages/chat.vue +146 -0
  85. package/app/pages/dashboard.vue +80 -0
  86. package/app/pages/docs.vue +131 -0
  87. package/app/pages/hooks.vue +163 -0
  88. package/app/pages/index.vue +249 -0
  89. package/app/pages/login.vue +60 -0
  90. package/app/pages/memories.vue +282 -0
  91. package/app/pages/settings.vue +625 -0
  92. package/app/pages/tasks.vue +312 -0
  93. package/app/pages/view/[uuid].vue +376 -0
  94. package/dist/cli/index.js +2711 -0
  95. package/drizzle.config.ts +10 -0
  96. package/nuxt.config.ts +98 -0
  97. package/package.json +107 -0
  98. package/server/api/agents/[id]/cancel.post.ts +27 -0
  99. package/server/api/agents/[id]/run.post.ts +34 -0
  100. package/server/api/agents/[id]/runs.get.ts +45 -0
  101. package/server/api/agents/[id]/stats.get.ts +94 -0
  102. package/server/api/agents/[id].delete.ts +29 -0
  103. package/server/api/agents/[id].get.ts +25 -0
  104. package/server/api/agents/[id].patch.ts +55 -0
  105. package/server/api/agents/index.get.ts +15 -0
  106. package/server/api/agents/index.post.ts +48 -0
  107. package/server/api/agents/stats.get.ts +86 -0
  108. package/server/api/auth/[...all].ts +5 -0
  109. package/server/api/conversations/[id].delete.ts +16 -0
  110. package/server/api/conversations/[id].get.ts +34 -0
  111. package/server/api/conversations/index.get.ts +17 -0
  112. package/server/api/documents/[id]/index.delete.ts +47 -0
  113. package/server/api/documents/[id]/index.put.ts +102 -0
  114. package/server/api/documents/[id]/public.get.ts +60 -0
  115. package/server/api/documents/[id]/restore.post.ts +65 -0
  116. package/server/api/documents/by-path.post.ts +168 -0
  117. package/server/api/documents/index.get.ts +48 -0
  118. package/server/api/fs/delete.post.ts +41 -0
  119. package/server/api/fs/list.get.ts +99 -0
  120. package/server/api/fs/mkdir.post.ts +44 -0
  121. package/server/api/fs/move.post.ts +68 -0
  122. package/server/api/fs/read.post.ts +48 -0
  123. package/server/api/fs/rename.post.ts +55 -0
  124. package/server/api/fs/write.post.ts +51 -0
  125. package/server/api/health.get.ts +40 -0
  126. package/server/api/home.get.ts +26 -0
  127. package/server/api/hooks/events/index.get.ts +56 -0
  128. package/server/api/hooks/events/index.post.ts +36 -0
  129. package/server/api/hooks/stats.get.ts +99 -0
  130. package/server/api/memory/[id].delete.ts +26 -0
  131. package/server/api/memory/context.get.ts +83 -0
  132. package/server/api/memory/extract.post.ts +42 -0
  133. package/server/api/memory/search.get.ts +70 -0
  134. package/server/api/memory/store.post.ts +31 -0
  135. package/server/api/projects/[id]/index.delete.ts +40 -0
  136. package/server/api/projects/[id]/index.get.ts +25 -0
  137. package/server/api/projects/[id]/index.put.ts +50 -0
  138. package/server/api/projects/index.get.ts +20 -0
  139. package/server/api/projects/index.post.ts +34 -0
  140. package/server/api/secrets/[key].delete.ts +31 -0
  141. package/server/api/secrets/[key].get.ts +30 -0
  142. package/server/api/secrets/[key].put.ts +52 -0
  143. package/server/api/secrets/index.get.ts +20 -0
  144. package/server/api/secrets/index.post.ts +58 -0
  145. package/server/api/tasks/[id]/index.delete.ts +46 -0
  146. package/server/api/tasks/[id]/index.get.ts +24 -0
  147. package/server/api/tasks/[id]/index.put.ts +70 -0
  148. package/server/api/tasks/[id]/restore.post.ts +49 -0
  149. package/server/api/tasks/index.get.ts +53 -0
  150. package/server/api/tasks/index.post.ts +47 -0
  151. package/server/api/tasks/tags.get.ts +21 -0
  152. package/server/api/user/email.patch.ts +56 -0
  153. package/server/db/index.ts +76 -0
  154. package/server/db/migrate.ts +41 -0
  155. package/server/db/schema.ts +345 -0
  156. package/server/db/seed.ts +46 -0
  157. package/server/db/types.ts +28 -0
  158. package/server/drizzle/migrations/0000_brown_george_stacy.sql +34 -0
  159. package/server/drizzle/migrations/0001_stormy_pyro.sql +16 -0
  160. package/server/drizzle/migrations/0002_clean_colossus.sql +50 -0
  161. package/server/drizzle/migrations/0003_fine_joystick.sql +12 -0
  162. package/server/drizzle/migrations/0004_tan_groot.sql +26 -0
  163. package/server/drizzle/migrations/0005_cloudy_lilith.sql +33 -0
  164. package/server/drizzle/migrations/0006_ordinary_retro_girl.sql +13 -0
  165. package/server/drizzle/migrations/0007_flowery_venus.sql +15 -0
  166. package/server/drizzle/migrations/0008_talented_zombie.sql +13 -0
  167. package/server/drizzle/migrations/0009_gray_shen.sql +15 -0
  168. package/server/drizzle/migrations/meta/0000_snapshot.json +230 -0
  169. package/server/drizzle/migrations/meta/0001_snapshot.json +306 -0
  170. package/server/drizzle/migrations/meta/0002_snapshot.json +615 -0
  171. package/server/drizzle/migrations/meta/0003_snapshot.json +730 -0
  172. package/server/drizzle/migrations/meta/0004_snapshot.json +916 -0
  173. package/server/drizzle/migrations/meta/0005_snapshot.json +1127 -0
  174. package/server/drizzle/migrations/meta/0006_snapshot.json +1213 -0
  175. package/server/drizzle/migrations/meta/0007_snapshot.json +1307 -0
  176. package/server/drizzle/migrations/meta/0008_snapshot.json +1390 -0
  177. package/server/drizzle/migrations/meta/0009_snapshot.json +1487 -0
  178. package/server/drizzle/migrations/meta/_journal.json +76 -0
  179. package/server/middleware/auth.ts +79 -0
  180. package/server/plugins/00.env-validate.ts +38 -0
  181. package/server/plugins/01.api-token.ts +31 -0
  182. package/server/plugins/02.database.ts +54 -0
  183. package/server/plugins/03.file-watcher.ts +65 -0
  184. package/server/plugins/04.cron-agents.ts +26 -0
  185. package/server/routes/_ws/chat.ts +252 -0
  186. package/server/routes/notifications.ts +47 -0
  187. package/server/routes/terminal.ts +98 -0
  188. package/server/services/agent-executor.ts +218 -0
  189. package/server/services/cron-scheduler.ts +78 -0
  190. package/server/services/memory-extractor.ts +120 -0
  191. package/server/utils/agent-cleanup.ts +91 -0
  192. package/server/utils/agent-registry.ts +95 -0
  193. package/server/utils/auth.ts +33 -0
  194. package/server/utils/chat-session-manager.ts +59 -0
  195. package/server/utils/crypto.ts +40 -0
  196. package/server/utils/db-guard.ts +12 -0
  197. package/server/utils/db-state.ts +63 -0
  198. package/server/utils/document-sync.ts +207 -0
  199. package/server/utils/frontmatter.ts +84 -0
  200. package/server/utils/notification-bus.ts +60 -0
  201. package/server/utils/path-validator.ts +55 -0
  202. package/server/utils/pty-manager.ts +130 -0
  203. package/shared/types/index.ts +604 -0
  204. package/shared/utils/language-detection.ts +87 -0
  205. package/tsconfig.json +10 -0
@@ -0,0 +1,98 @@
1
+ import {
2
+ getOrCreatePtySession,
3
+ getOutputBuffer,
4
+ resizePty,
5
+ writeToPty,
6
+ getPtySession
7
+ } from '../utils/pty-manager'
8
+
9
+ interface TerminalMessage {
10
+ type: 'input' | 'resize' | 'ping'
11
+ data?: string
12
+ cols?: number
13
+ rows?: number
14
+ }
15
+
16
+ export default defineWebSocketHandler({
17
+ open(peer) {
18
+ const sessionId = 'default' // For now, single session per app
19
+ const cols = 80
20
+ const rows = 24
21
+
22
+ console.log(`Terminal WebSocket opened: ${peer.id}`)
23
+
24
+ try {
25
+ const { isNew } = getOrCreatePtySession(sessionId, cols, rows)
26
+
27
+ // If reconnecting, replay buffer
28
+ if (!isNew) {
29
+ const buffer = getOutputBuffer(sessionId)
30
+ if (buffer.length > 0) {
31
+ peer.send(JSON.stringify({
32
+ type: 'output',
33
+ data: buffer.join('')
34
+ }))
35
+ }
36
+ }
37
+
38
+ // Forward PTY output to client
39
+ const session = getPtySession(sessionId)
40
+ if (session) {
41
+ session.pty.onData((data: string) => {
42
+ try {
43
+ peer.send(JSON.stringify({
44
+ type: 'output',
45
+ data
46
+ }))
47
+ } catch {
48
+ // Client disconnected
49
+ }
50
+ })
51
+ }
52
+ } catch (error) {
53
+ console.error('Failed to create PTY session:', error)
54
+ peer.send(JSON.stringify({
55
+ type: 'error',
56
+ data: `Failed to start terminal: ${error instanceof Error ? error.message : 'Unknown error'}\r\n`
57
+ }))
58
+ }
59
+ },
60
+
61
+ message(peer, message) {
62
+ const sessionId = 'default'
63
+
64
+ try {
65
+ const msg = JSON.parse(message.text()) as TerminalMessage
66
+
67
+ switch (msg.type) {
68
+ case 'input':
69
+ if (msg.data) {
70
+ writeToPty(sessionId, msg.data)
71
+ }
72
+ break
73
+
74
+ case 'resize':
75
+ if (msg.cols && msg.rows) {
76
+ resizePty(sessionId, msg.cols, msg.rows)
77
+ }
78
+ break
79
+
80
+ case 'ping':
81
+ peer.send(JSON.stringify({ type: 'pong' }))
82
+ break
83
+ }
84
+ } catch (e) {
85
+ console.error('Terminal message error:', e)
86
+ }
87
+ },
88
+
89
+ close(peer) {
90
+ console.log(`Terminal WebSocket closed: ${peer.id}`)
91
+ // Don't destroy session on close - allow reconnection
92
+ // Session will be cleaned up by timeout in pty-manager
93
+ },
94
+
95
+ error(peer, error) {
96
+ console.error(`Terminal WebSocket error for ${peer.id}:`, error)
97
+ }
98
+ })
@@ -0,0 +1,218 @@
1
+ import { query } from '@anthropic-ai/claude-agent-sdk'
2
+ import { eq } from 'drizzle-orm'
3
+ import { getDb } from '../db'
4
+ import * as schema from '../db/schema'
5
+ import { notificationBus } from '../utils/notification-bus'
6
+ import { agentRegistry } from '../utils/agent-registry'
7
+
8
+ // Custom error for cancellation
9
+ export class AgentCancelledError extends Error {
10
+ constructor() {
11
+ super('Agent execution was cancelled')
12
+ this.name = 'AgentCancelledError'
13
+ }
14
+ }
15
+
16
+ export interface AgentConfig {
17
+ id: string
18
+ name: string
19
+ prompt: string
20
+ maxTurns?: number
21
+ maxBudgetUsd?: number | null
22
+ }
23
+
24
+ // Result type matching SDK structure
25
+ interface AgentResult {
26
+ subtype: string
27
+ total_cost_usd: number
28
+ num_turns: number
29
+ result?: string
30
+ errors?: string[]
31
+ usage: {
32
+ input_tokens: number
33
+ output_tokens: number
34
+ }
35
+ }
36
+
37
+ export async function executeAgent(config: AgentConfig): Promise<void> {
38
+ const db = getDb()
39
+ const startTime = Date.now()
40
+
41
+ // Create run record
42
+ const [run] = await db.insert(schema.cronAgentRuns)
43
+ .values({ agentId: config.id, status: 'running' })
44
+ .returning()
45
+
46
+ const runId = run!.id
47
+
48
+ // Register in the agent registry for cancellation support
49
+ agentRegistry.register(runId, config.id, config.name)
50
+
51
+ // Notify: agent started
52
+ notificationBus.broadcast({
53
+ type: 'agent:started',
54
+ agentId: config.id,
55
+ agentName: config.name,
56
+ runId
57
+ })
58
+
59
+ try {
60
+ const result = await runAgentSDK(config, runId)
61
+ const durationMs = Date.now() - startTime
62
+
63
+ // Determine status based on result subtype
64
+ const status = result.subtype === 'success'
65
+ ? 'success'
66
+ : result.subtype === 'error_max_budget_usd'
67
+ ? 'budget_exceeded'
68
+ : 'error'
69
+
70
+ // Update run record with full metrics
71
+ await db.update(schema.cronAgentRuns)
72
+ .set({
73
+ status,
74
+ output: result.subtype === 'success' ? result.result : undefined,
75
+ error: result.subtype !== 'success' ? result.errors?.join('\n') : undefined,
76
+ costUsd: result.total_cost_usd,
77
+ inputTokens: result.usage.input_tokens,
78
+ outputTokens: result.usage.output_tokens,
79
+ numTurns: result.num_turns,
80
+ completedAt: new Date(),
81
+ durationMs
82
+ })
83
+ .where(eq(schema.cronAgentRuns.id, run!.id))
84
+
85
+ // Update agent last run
86
+ await db.update(schema.cronAgents)
87
+ .set({ lastRunAt: new Date(), lastStatus: status })
88
+ .where(eq(schema.cronAgents.id, config.id))
89
+
90
+ console.log(`[agent] ${config.name} completed: ${status} (${durationMs}ms, $${result.total_cost_usd.toFixed(4)})`)
91
+
92
+ // Notify: agent completed
93
+ notificationBus.broadcast({
94
+ type: 'agent:completed',
95
+ agentId: config.id,
96
+ agentName: config.name,
97
+ runId,
98
+ status,
99
+ color: status === 'success' ? 'success' : 'warning'
100
+ })
101
+ } catch (error) {
102
+ const durationMs = Date.now() - startTime
103
+ const isCancelled = error instanceof AgentCancelledError
104
+
105
+ if (isCancelled) {
106
+ // Handle cancellation
107
+ await db.update(schema.cronAgentRuns)
108
+ .set({
109
+ status: 'cancelled',
110
+ error: 'Cancelled by user',
111
+ completedAt: new Date(),
112
+ durationMs
113
+ })
114
+ .where(eq(schema.cronAgentRuns.id, runId))
115
+
116
+ await db.update(schema.cronAgents)
117
+ .set({ lastRunAt: new Date(), lastStatus: 'cancelled' })
118
+ .where(eq(schema.cronAgents.id, config.id))
119
+
120
+ console.log(`[agent] ${config.name} cancelled after ${durationMs}ms`)
121
+
122
+ notificationBus.broadcast({
123
+ type: 'agent:failed',
124
+ agentId: config.id,
125
+ agentName: config.name,
126
+ runId,
127
+ message: 'Cancelled by user',
128
+ color: 'warning'
129
+ })
130
+ } else {
131
+ // Handle other errors
132
+ const errorMessage = error instanceof Error ? error.message : String(error)
133
+
134
+ await db.update(schema.cronAgentRuns)
135
+ .set({
136
+ status: 'error',
137
+ error: errorMessage,
138
+ completedAt: new Date(),
139
+ durationMs
140
+ })
141
+ .where(eq(schema.cronAgentRuns.id, runId))
142
+
143
+ await db.update(schema.cronAgents)
144
+ .set({ lastRunAt: new Date(), lastStatus: 'error' })
145
+ .where(eq(schema.cronAgents.id, config.id))
146
+
147
+ console.error(`[agent] ${config.name} failed:`, errorMessage)
148
+
149
+ notificationBus.broadcast({
150
+ type: 'agent:failed',
151
+ agentId: config.id,
152
+ agentName: config.name,
153
+ runId,
154
+ message: errorMessage,
155
+ color: 'error'
156
+ })
157
+ }
158
+ } finally {
159
+ // Always unregister the agent when done
160
+ agentRegistry.unregister(runId)
161
+ }
162
+ }
163
+
164
+ async function runAgentSDK(config: AgentConfig, runId: string): Promise<AgentResult> {
165
+ // SDK checks CLAUDE_CODE_OAUTH_TOKEN first (Max subscription),
166
+ // then falls back to ANTHROPIC_API_KEY (API billing)
167
+ const conversation = query({
168
+ prompt: config.prompt,
169
+ options: {
170
+ cwd: process.env.VAULT_PATH || process.cwd(),
171
+ settingSources: ['user', 'project'],
172
+ permissionMode: 'bypassPermissions',
173
+ allowDangerouslySkipPermissions: true,
174
+ maxTurns: config.maxTurns ?? 50,
175
+ maxBudgetUsd: config.maxBudgetUsd ?? undefined
176
+ }
177
+ })
178
+
179
+ let resultMessage: AgentResult | undefined
180
+
181
+ // Stream through messages and collect output
182
+ for await (const message of conversation) {
183
+ // Check for cancellation between messages
184
+ if (agentRegistry.isCancelled(runId)) {
185
+ throw new AgentCancelledError()
186
+ }
187
+
188
+ if (message.type === 'result') {
189
+ // Extract the fields we need from the SDK result
190
+ // Cast through unknown as SDK types don't expose usage properties correctly
191
+ const msg = message as unknown as {
192
+ subtype: string
193
+ total_cost_usd: number
194
+ num_turns: number
195
+ result?: string
196
+ errors?: string[]
197
+ usage: { input_tokens: number, output_tokens: number }
198
+ }
199
+ resultMessage = {
200
+ subtype: msg.subtype,
201
+ total_cost_usd: msg.total_cost_usd,
202
+ num_turns: msg.num_turns,
203
+ result: msg.result,
204
+ errors: msg.errors,
205
+ usage: {
206
+ input_tokens: msg.usage.input_tokens,
207
+ output_tokens: msg.usage.output_tokens
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ if (!resultMessage) {
214
+ throw new Error('No result message received from SDK')
215
+ }
216
+
217
+ return resultMessage
218
+ }
@@ -0,0 +1,78 @@
1
+ import { CronJob } from 'cron'
2
+ import { eq } from 'drizzle-orm'
3
+ import { getDb } from '../db'
4
+ import * as schema from '../db/schema'
5
+ import { executeAgent } from './agent-executor'
6
+
7
+ // Use globalThis to ensure the same Map instance across Nitro module boundaries
8
+ // This prevents issues where the plugin and API handlers get different module instances
9
+ const JOBS_KEY = '__secondBrain_cronJobs__' as const
10
+ declare global {
11
+
12
+ var __secondBrain_cronJobs__: Map<string, CronJob> | undefined
13
+ }
14
+
15
+ function getJobs(): Map<string, CronJob> {
16
+ if (!globalThis[JOBS_KEY])
17
+ globalThis[JOBS_KEY] = new Map<string, CronJob>()
18
+ return globalThis[JOBS_KEY]
19
+ }
20
+
21
+ export async function initCronScheduler(): Promise<number> {
22
+ const db = getDb()
23
+ const agents = await db.query.cronAgents.findMany({
24
+ where: eq(schema.cronAgents.enabled, true)
25
+ })
26
+
27
+ for (const agent of agents) {
28
+ scheduleAgent(agent)
29
+ }
30
+
31
+ return agents.length
32
+ }
33
+
34
+ export function scheduleAgent(agent: typeof schema.cronAgents.$inferSelect) {
35
+ unscheduleAgent(agent.id)
36
+
37
+ if (!agent.enabled) return
38
+
39
+ const jobs = getJobs()
40
+
41
+ try {
42
+ const job = new CronJob(
43
+ agent.schedule,
44
+ () => {
45
+ console.log(`[cron] Triggering agent: ${agent.name}`)
46
+ executeAgent({
47
+ id: agent.id,
48
+ name: agent.name,
49
+ prompt: agent.prompt,
50
+ maxTurns: agent.maxTurns ?? 50,
51
+ maxBudgetUsd: agent.maxBudgetUsd ?? undefined
52
+ }).catch(err => console.error(`[cron] Agent ${agent.name} error:`, err))
53
+ },
54
+ null,
55
+ true,
56
+ 'UTC'
57
+ )
58
+
59
+ jobs.set(agent.id, job)
60
+ console.log(`[cron] Scheduled: ${agent.name} (${agent.schedule})`)
61
+ } catch {
62
+ console.error(`[cron] Invalid schedule for ${agent.name}: ${agent.schedule}`)
63
+ }
64
+ }
65
+
66
+ export function unscheduleAgent(agentId: string) {
67
+ const jobs = getJobs()
68
+ const job = jobs.get(agentId)
69
+ if (job) {
70
+ job.stop()
71
+ jobs.delete(agentId)
72
+ console.log(`[cron] Unscheduled agent: ${agentId}`)
73
+ }
74
+ }
75
+
76
+ export function getScheduledAgentIds(): string[] {
77
+ return Array.from(getJobs().keys())
78
+ }
@@ -0,0 +1,120 @@
1
+ import { query } from '@anthropic-ai/claude-agent-sdk'
2
+ import type { ExtractedMemory, MemoryChunkType } from '~~/shared/types'
3
+
4
+ const EXTRACTION_PROMPT = `You are a memory extraction assistant. Analyze this conversation excerpt and extract key memories worth preserving for future reference.
5
+
6
+ Output ONLY a JSON array with this exact structure (no markdown, no code blocks, no explanation):
7
+ [{"type": "decision|fact|solution|pattern|preference", "content": "concise statement", "relevance": 0.0-1.0}]
8
+
9
+ Memory types:
10
+ - decision: Choices made about implementation, architecture, or approach
11
+ - fact: Important information about the codebase, APIs, or constraints
12
+ - solution: How a problem was solved or a bug was fixed
13
+ - pattern: Recurring patterns or conventions identified
14
+ - preference: User preferences for code style, tools, or workflows
15
+
16
+ Rules:
17
+ - Only extract genuinely useful information
18
+ - Skip routine acknowledgments ("I'll do that", "Sure", "Let me...")
19
+ - Skip obvious facts already in code, debugging steps, greetings
20
+ - Content max 100 characters
21
+ - relevance: 1.0 = highly important, 0.5 = moderately useful, 0.1 = minor detail
22
+ - If nothing worth extracting, return []
23
+
24
+ Conversation to analyze:
25
+ `
26
+
27
+ export async function extractMemories(transcript: string): Promise<ExtractedMemory[]> {
28
+ if (!transcript || transcript.trim().length < 50)
29
+ return []
30
+
31
+ try {
32
+ // Use Agent SDK which checks CLAUDE_CODE_OAUTH_TOKEN first (Max subscription),
33
+ // then falls back to ANTHROPIC_API_KEY (API billing)
34
+ const conversation = query({
35
+ prompt: `${EXTRACTION_PROMPT}\n\n${transcript.slice(0, 8000)}`,
36
+ options: {
37
+ maxTurns: 1, // Single turn extraction
38
+ permissionMode: 'bypassPermissions',
39
+ allowDangerouslySkipPermissions: true
40
+ }
41
+ })
42
+
43
+ let result = ''
44
+
45
+ // Stream through messages and get the result
46
+ for await (const message of conversation) {
47
+ if (message.type === 'result') {
48
+ const msg = message as unknown as {
49
+ subtype: string
50
+ result?: string
51
+ }
52
+ if (msg.subtype === 'success' && msg.result)
53
+ result = msg.result
54
+ }
55
+ }
56
+
57
+ if (!result)
58
+ return []
59
+
60
+ // Extract JSON array from result (may have surrounding text)
61
+ const jsonMatch = result.match(/\[[\s\S]*\]/)
62
+ if (!jsonMatch)
63
+ return []
64
+
65
+ const memories = JSON.parse(jsonMatch[0]) as Array<{
66
+ type: string
67
+ content: string
68
+ relevance: number
69
+ }>
70
+
71
+ return memories
72
+ .filter(m => isValidMemoryType(m.type) && m.content && typeof m.relevance === 'number')
73
+ .map(m => ({
74
+ type: m.type as MemoryChunkType,
75
+ content: m.content.slice(0, 200),
76
+ relevance: Math.max(0, Math.min(1, m.relevance))
77
+ }))
78
+ } catch (error) {
79
+ console.error('[memory-extractor] Failed to extract memories:', error)
80
+ return []
81
+ }
82
+ }
83
+
84
+ function isValidMemoryType(type: string): type is MemoryChunkType {
85
+ return ['decision', 'fact', 'solution', 'pattern', 'preference', 'summary'].includes(type)
86
+ }
87
+
88
+ export async function extractMemoriesFromTranscriptFile(transcriptPath: string): Promise<ExtractedMemory[]> {
89
+ const fs = await import('node:fs/promises')
90
+
91
+ try {
92
+ const content = await fs.readFile(transcriptPath, 'utf-8')
93
+ const lines = content.trim().split('\n')
94
+
95
+ // Parse JSONL and extract recent messages
96
+ const messages: Array<{ role: string, content: string }> = []
97
+ for (const line of lines.slice(-20)) {
98
+ try {
99
+ const entry = JSON.parse(line)
100
+ if (entry.role && entry.content)
101
+ messages.push({ role: entry.role, content: entry.content })
102
+ } catch {
103
+ // Skip invalid lines
104
+ }
105
+ }
106
+
107
+ if (messages.length === 0)
108
+ return []
109
+
110
+ // Format for extraction
111
+ const formatted = messages
112
+ .map(m => `${m.role.toUpperCase()}: ${m.content.slice(0, 1000)}`)
113
+ .join('\n\n')
114
+
115
+ return extractMemories(formatted)
116
+ } catch (error) {
117
+ console.error('[memory-extractor] Failed to read transcript:', error)
118
+ return []
119
+ }
120
+ }
@@ -0,0 +1,91 @@
1
+ import { eq, and, isNull } from 'drizzle-orm'
2
+ import { getDb, schema } from '../db'
3
+
4
+ /**
5
+ * Cleans up orphaned agent runs on server startup.
6
+ *
7
+ * Scenarios handled:
8
+ * 1. Runs with status='running' - server shutdown mid-execution
9
+ * 2. Terminal runs (success/error/budget_exceeded) missing completedAt timestamp
10
+ */
11
+ export async function cleanupOrphanedRuns(): Promise<{ cancelled: number, fixed: number }> {
12
+ const db = getDb()
13
+
14
+ let cancelled = 0
15
+ let fixed = 0
16
+
17
+ // 1. Cancel all currently 'running' runs (server must have shut down mid-run)
18
+ const runningRuns = await db
19
+ .update(schema.cronAgentRuns)
20
+ .set({
21
+ status: 'cancelled',
22
+ error: 'Server shutdown during execution',
23
+ completedAt: new Date(),
24
+ durationMs: null // We don't know the actual duration
25
+ })
26
+ .where(eq(schema.cronAgentRuns.status, 'running'))
27
+ .returning({ id: schema.cronAgentRuns.id, agentId: schema.cronAgentRuns.agentId })
28
+
29
+ cancelled = runningRuns.length
30
+
31
+ // 2. For each cancelled run, update the agent's lastStatus if this was their latest run
32
+ for (const run of runningRuns) {
33
+ // Check if this was the agent's most recent run
34
+ const latestRun = await db.query.cronAgentRuns.findFirst({
35
+ where: eq(schema.cronAgentRuns.agentId, run.agentId),
36
+ orderBy: (runs, { desc }) => [desc(runs.startedAt)]
37
+ })
38
+
39
+ if (latestRun && latestRun.id === run.id) {
40
+ await db
41
+ .update(schema.cronAgents)
42
+ .set({ lastStatus: 'cancelled' })
43
+ .where(eq(schema.cronAgents.id, run.agentId))
44
+ }
45
+ }
46
+
47
+ // 3. Fix terminal runs missing completedAt timestamp
48
+ const fixedRuns = await db
49
+ .update(schema.cronAgentRuns)
50
+ .set({
51
+ completedAt: new Date()
52
+ })
53
+ .where(
54
+ and(
55
+ // Terminal statuses that should have completedAt
56
+ eq(schema.cronAgentRuns.status, 'success'),
57
+ isNull(schema.cronAgentRuns.completedAt)
58
+ )
59
+ )
60
+ .returning({ id: schema.cronAgentRuns.id })
61
+
62
+ const fixedErrorRuns = await db
63
+ .update(schema.cronAgentRuns)
64
+ .set({
65
+ completedAt: new Date()
66
+ })
67
+ .where(
68
+ and(
69
+ eq(schema.cronAgentRuns.status, 'error'),
70
+ isNull(schema.cronAgentRuns.completedAt)
71
+ )
72
+ )
73
+ .returning({ id: schema.cronAgentRuns.id })
74
+
75
+ const fixedBudgetRuns = await db
76
+ .update(schema.cronAgentRuns)
77
+ .set({
78
+ completedAt: new Date()
79
+ })
80
+ .where(
81
+ and(
82
+ eq(schema.cronAgentRuns.status, 'budget_exceeded'),
83
+ isNull(schema.cronAgentRuns.completedAt)
84
+ )
85
+ )
86
+ .returning({ id: schema.cronAgentRuns.id })
87
+
88
+ fixed = fixedRuns.length + fixedErrorRuns.length + fixedBudgetRuns.length
89
+
90
+ return { cancelled, fixed }
91
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Registry for tracking running agent executions.
3
+ * Allows cancellation of in-progress agent runs.
4
+ */
5
+
6
+ interface RunningAgent {
7
+ agentId: string
8
+ runId: string
9
+ agentName: string
10
+ startedAt: Date
11
+ cancelled: boolean
12
+ }
13
+
14
+ class AgentRegistry {
15
+ private runningAgents = new Map<string, RunningAgent>()
16
+
17
+ /**
18
+ * Register a new running agent
19
+ */
20
+ register(runId: string, agentId: string, agentName: string): void {
21
+ this.runningAgents.set(runId, {
22
+ agentId,
23
+ runId,
24
+ agentName,
25
+ startedAt: new Date(),
26
+ cancelled: false
27
+ })
28
+ }
29
+
30
+ /**
31
+ * Unregister a completed/cancelled agent
32
+ */
33
+ unregister(runId: string): void {
34
+ this.runningAgents.delete(runId)
35
+ }
36
+
37
+ /**
38
+ * Check if a specific run is cancelled
39
+ */
40
+ isCancelled(runId: string): boolean {
41
+ return this.runningAgents.get(runId)?.cancelled ?? false
42
+ }
43
+
44
+ /**
45
+ * Cancel a running agent by run ID
46
+ * Returns true if the agent was found and cancelled
47
+ */
48
+ cancelByRunId(runId: string): boolean {
49
+ const agent = this.runningAgents.get(runId)
50
+ if (agent && !agent.cancelled) {
51
+ agent.cancelled = true
52
+ return true
53
+ }
54
+ return false
55
+ }
56
+
57
+ /**
58
+ * Cancel all running agents for a specific agent ID
59
+ * Returns the number of runs cancelled
60
+ */
61
+ cancelByAgentId(agentId: string): number {
62
+ let cancelled = 0
63
+ for (const agent of this.runningAgents.values()) {
64
+ if (agent.agentId === agentId && !agent.cancelled) {
65
+ agent.cancelled = true
66
+ cancelled++
67
+ }
68
+ }
69
+ return cancelled
70
+ }
71
+
72
+ /**
73
+ * Get all running agents
74
+ */
75
+ getRunning(): RunningAgent[] {
76
+ return Array.from(this.runningAgents.values()).filter(a => !a.cancelled)
77
+ }
78
+
79
+ /**
80
+ * Get running agent IDs
81
+ */
82
+ getRunningAgentIds(): string[] {
83
+ return [...new Set(this.getRunning().map(a => a.agentId))]
84
+ }
85
+
86
+ /**
87
+ * Check if an agent has any running executions
88
+ */
89
+ isAgentRunning(agentId: string): boolean {
90
+ return this.getRunning().some(a => a.agentId === agentId)
91
+ }
92
+ }
93
+
94
+ // Singleton instance
95
+ export const agentRegistry = new AgentRegistry()