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.
- package/.env.example +58 -0
- package/Claude/CLAUDE.md +92 -0
- package/Claude/hooks/lib/__init__.py +1 -0
- package/Claude/hooks/lib/hook_client.py +207 -0
- package/Claude/hooks/log-event.py +97 -0
- package/Claude/hooks/pre-compact.py +46 -0
- package/Claude/hooks/session-end.py +26 -0
- package/Claude/hooks/session-start.py +35 -0
- package/Claude/hooks/stop-extract.py +40 -0
- package/Claude/rules/frontmatter.md +54 -0
- package/Claude/rules/markdown.md +43 -0
- package/Claude/rules/note-organization.md +33 -0
- package/Claude/settings.json +54 -0
- package/Claude/skills/README.md +136 -0
- package/Claude/skills/_lib/__init__.py +1 -0
- package/Claude/skills/_lib/api.py +164 -0
- package/Claude/skills/_lib/output.py +95 -0
- package/Claude/skills/environment/SKILL.md +73 -0
- package/Claude/skills/environment/environment.py +239 -0
- package/Claude/skills/memory/SKILL.md +153 -0
- package/Claude/skills/memory/memory.py +270 -0
- package/Claude/skills/project/SKILL.md +105 -0
- package/Claude/skills/project/project.py +203 -0
- package/Claude/skills/skill-creator/SKILL.md +261 -0
- package/Claude/skills/task/SKILL.md +135 -0
- package/Claude/skills/task/task.py +310 -0
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +39 -0
- package/app/assets/css/main.css +10 -0
- package/app/components/AppLogo.vue +40 -0
- package/app/components/AssistantPanel.client.vue +518 -0
- package/app/components/ConfirmModal.vue +84 -0
- package/app/components/TemplateMenu.vue +49 -0
- package/app/components/agents/AgentActivityChart.client.vue +105 -0
- package/app/components/agents/AgentActivityChart.server.vue +25 -0
- package/app/components/agents/AgentForm.vue +304 -0
- package/app/components/agents/AgentRunModal.vue +154 -0
- package/app/components/agents/AgentStatsCards.vue +98 -0
- package/app/components/chat/ChatInput.vue +85 -0
- package/app/components/chat/ConversationList.vue +78 -0
- package/app/components/chat/MessageBubble.vue +81 -0
- package/app/components/chat/StreamingMessage.vue +36 -0
- package/app/components/chat/ToolCallBlock.vue +77 -0
- package/app/components/editor/CodeEditor.client.vue +212 -0
- package/app/components/editor/CodeEditorFallback.vue +12 -0
- package/app/components/editor/DocumentEditor.vue +326 -0
- package/app/components/editor/DocumentMetadata.vue +140 -0
- package/app/components/editor/MarkdownEditor.vue +146 -0
- package/app/components/files/FileTree.vue +436 -0
- package/app/components/hooks/HookActivityChart.client.vue +117 -0
- package/app/components/hooks/HookActivityChart.server.vue +25 -0
- package/app/components/hooks/HookStatsCards.vue +63 -0
- package/app/components/hooks/RecentEventsTable.vue +123 -0
- package/app/components/hooks/ToolBreakdownTable.vue +72 -0
- package/app/components/search/DashboardSearch.vue +122 -0
- package/app/components/tasks/ProjectSelect.vue +35 -0
- package/app/components/tasks/TaskCard.vue +182 -0
- package/app/components/tasks/TaskDetail.vue +160 -0
- package/app/components/tasks/TaskForm.vue +280 -0
- package/app/components/tasks/TaskList.vue +69 -0
- package/app/components/view/ViewToc.vue +85 -0
- package/app/composables/useAgents.ts +153 -0
- package/app/composables/useAuth.ts +73 -0
- package/app/composables/useChat.ts +298 -0
- package/app/composables/useDocument.ts +141 -0
- package/app/composables/useEditor.ts +100 -0
- package/app/composables/useFileTree.ts +220 -0
- package/app/composables/useHookEvents.ts +68 -0
- package/app/composables/useMemories.ts +83 -0
- package/app/composables/useNotificationBus.ts +154 -0
- package/app/composables/usePreferences.ts +131 -0
- package/app/composables/useProjects.ts +97 -0
- package/app/composables/useSearch.ts +52 -0
- package/app/composables/useTasks.ts +201 -0
- package/app/composables/useTerminal.ts +135 -0
- package/app/layouts/auth.vue +20 -0
- package/app/layouts/dashboard.vue +186 -0
- package/app/layouts/view.vue +60 -0
- package/app/middleware/auth.ts +9 -0
- package/app/pages/agents/[id].vue +602 -0
- package/app/pages/agents/index.vue +412 -0
- package/app/pages/chat.vue +146 -0
- package/app/pages/dashboard.vue +80 -0
- package/app/pages/docs.vue +131 -0
- package/app/pages/hooks.vue +163 -0
- package/app/pages/index.vue +249 -0
- package/app/pages/login.vue +60 -0
- package/app/pages/memories.vue +282 -0
- package/app/pages/settings.vue +625 -0
- package/app/pages/tasks.vue +312 -0
- package/app/pages/view/[uuid].vue +376 -0
- package/dist/cli/index.js +2711 -0
- package/drizzle.config.ts +10 -0
- package/nuxt.config.ts +98 -0
- package/package.json +107 -0
- package/server/api/agents/[id]/cancel.post.ts +27 -0
- package/server/api/agents/[id]/run.post.ts +34 -0
- package/server/api/agents/[id]/runs.get.ts +45 -0
- package/server/api/agents/[id]/stats.get.ts +94 -0
- package/server/api/agents/[id].delete.ts +29 -0
- package/server/api/agents/[id].get.ts +25 -0
- package/server/api/agents/[id].patch.ts +55 -0
- package/server/api/agents/index.get.ts +15 -0
- package/server/api/agents/index.post.ts +48 -0
- package/server/api/agents/stats.get.ts +86 -0
- package/server/api/auth/[...all].ts +5 -0
- package/server/api/conversations/[id].delete.ts +16 -0
- package/server/api/conversations/[id].get.ts +34 -0
- package/server/api/conversations/index.get.ts +17 -0
- package/server/api/documents/[id]/index.delete.ts +47 -0
- package/server/api/documents/[id]/index.put.ts +102 -0
- package/server/api/documents/[id]/public.get.ts +60 -0
- package/server/api/documents/[id]/restore.post.ts +65 -0
- package/server/api/documents/by-path.post.ts +168 -0
- package/server/api/documents/index.get.ts +48 -0
- package/server/api/fs/delete.post.ts +41 -0
- package/server/api/fs/list.get.ts +99 -0
- package/server/api/fs/mkdir.post.ts +44 -0
- package/server/api/fs/move.post.ts +68 -0
- package/server/api/fs/read.post.ts +48 -0
- package/server/api/fs/rename.post.ts +55 -0
- package/server/api/fs/write.post.ts +51 -0
- package/server/api/health.get.ts +40 -0
- package/server/api/home.get.ts +26 -0
- package/server/api/hooks/events/index.get.ts +56 -0
- package/server/api/hooks/events/index.post.ts +36 -0
- package/server/api/hooks/stats.get.ts +99 -0
- package/server/api/memory/[id].delete.ts +26 -0
- package/server/api/memory/context.get.ts +83 -0
- package/server/api/memory/extract.post.ts +42 -0
- package/server/api/memory/search.get.ts +70 -0
- package/server/api/memory/store.post.ts +31 -0
- package/server/api/projects/[id]/index.delete.ts +40 -0
- package/server/api/projects/[id]/index.get.ts +25 -0
- package/server/api/projects/[id]/index.put.ts +50 -0
- package/server/api/projects/index.get.ts +20 -0
- package/server/api/projects/index.post.ts +34 -0
- package/server/api/secrets/[key].delete.ts +31 -0
- package/server/api/secrets/[key].get.ts +30 -0
- package/server/api/secrets/[key].put.ts +52 -0
- package/server/api/secrets/index.get.ts +20 -0
- package/server/api/secrets/index.post.ts +58 -0
- package/server/api/tasks/[id]/index.delete.ts +46 -0
- package/server/api/tasks/[id]/index.get.ts +24 -0
- package/server/api/tasks/[id]/index.put.ts +70 -0
- package/server/api/tasks/[id]/restore.post.ts +49 -0
- package/server/api/tasks/index.get.ts +53 -0
- package/server/api/tasks/index.post.ts +47 -0
- package/server/api/tasks/tags.get.ts +21 -0
- package/server/api/user/email.patch.ts +56 -0
- package/server/db/index.ts +76 -0
- package/server/db/migrate.ts +41 -0
- package/server/db/schema.ts +345 -0
- package/server/db/seed.ts +46 -0
- package/server/db/types.ts +28 -0
- package/server/drizzle/migrations/0000_brown_george_stacy.sql +34 -0
- package/server/drizzle/migrations/0001_stormy_pyro.sql +16 -0
- package/server/drizzle/migrations/0002_clean_colossus.sql +50 -0
- package/server/drizzle/migrations/0003_fine_joystick.sql +12 -0
- package/server/drizzle/migrations/0004_tan_groot.sql +26 -0
- package/server/drizzle/migrations/0005_cloudy_lilith.sql +33 -0
- package/server/drizzle/migrations/0006_ordinary_retro_girl.sql +13 -0
- package/server/drizzle/migrations/0007_flowery_venus.sql +15 -0
- package/server/drizzle/migrations/0008_talented_zombie.sql +13 -0
- package/server/drizzle/migrations/0009_gray_shen.sql +15 -0
- package/server/drizzle/migrations/meta/0000_snapshot.json +230 -0
- package/server/drizzle/migrations/meta/0001_snapshot.json +306 -0
- package/server/drizzle/migrations/meta/0002_snapshot.json +615 -0
- package/server/drizzle/migrations/meta/0003_snapshot.json +730 -0
- package/server/drizzle/migrations/meta/0004_snapshot.json +916 -0
- package/server/drizzle/migrations/meta/0005_snapshot.json +1127 -0
- package/server/drizzle/migrations/meta/0006_snapshot.json +1213 -0
- package/server/drizzle/migrations/meta/0007_snapshot.json +1307 -0
- package/server/drizzle/migrations/meta/0008_snapshot.json +1390 -0
- package/server/drizzle/migrations/meta/0009_snapshot.json +1487 -0
- package/server/drizzle/migrations/meta/_journal.json +76 -0
- package/server/middleware/auth.ts +79 -0
- package/server/plugins/00.env-validate.ts +38 -0
- package/server/plugins/01.api-token.ts +31 -0
- package/server/plugins/02.database.ts +54 -0
- package/server/plugins/03.file-watcher.ts +65 -0
- package/server/plugins/04.cron-agents.ts +26 -0
- package/server/routes/_ws/chat.ts +252 -0
- package/server/routes/notifications.ts +47 -0
- package/server/routes/terminal.ts +98 -0
- package/server/services/agent-executor.ts +218 -0
- package/server/services/cron-scheduler.ts +78 -0
- package/server/services/memory-extractor.ts +120 -0
- package/server/utils/agent-cleanup.ts +91 -0
- package/server/utils/agent-registry.ts +95 -0
- package/server/utils/auth.ts +33 -0
- package/server/utils/chat-session-manager.ts +59 -0
- package/server/utils/crypto.ts +40 -0
- package/server/utils/db-guard.ts +12 -0
- package/server/utils/db-state.ts +63 -0
- package/server/utils/document-sync.ts +207 -0
- package/server/utils/frontmatter.ts +84 -0
- package/server/utils/notification-bus.ts +60 -0
- package/server/utils/path-validator.ts +55 -0
- package/server/utils/pty-manager.ts +130 -0
- package/shared/types/index.ts +604 -0
- package/shared/utils/language-detection.ts +87 -0
- 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()
|