suemo 0.0.1 → 0.0.3
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/README.md +127 -27
- package/package.json +1 -1
- package/src/cli/commands/believe.ts +22 -12
- package/src/cli/commands/consolidate.ts +18 -11
- package/src/cli/commands/doctor.ts +123 -0
- package/src/cli/commands/export-import.ts +83 -47
- package/src/cli/commands/goal.ts +52 -27
- package/src/cli/commands/health.ts +53 -18
- package/src/cli/commands/init.ts +155 -75
- package/src/cli/commands/observe.ts +26 -13
- package/src/cli/commands/query.ts +23 -7
- package/src/cli/commands/recall.ts +12 -6
- package/src/cli/commands/serve.ts +25 -6
- package/src/cli/commands/sync.ts +44 -10
- package/src/cli/commands/timeline.ts +30 -18
- package/src/cli/commands/wander.ts +27 -16
- package/src/cli/index.ts +3 -4
- package/src/cli/shared.ts +34 -0
- package/src/cognitive/consolidate.ts +48 -19
- package/src/cognitive/contradiction.ts +19 -7
- package/src/cognitive/health.ts +61 -1
- package/src/config.template.ts +58 -0
- package/src/config.ts +124 -14
- package/src/db/preflight.ts +32 -6
- package/src/db/schema.surql +30 -9
- package/src/db/schema.ts +6 -3
- package/src/embedding/index.ts +52 -0
- package/src/embedding/openai-compatible.ts +43 -0
- package/src/goal.ts +3 -1
- package/src/index.ts +5 -1
- package/src/mcp/dispatch.ts +232 -0
- package/src/mcp/server.ts +150 -4
- package/src/mcp/stdio.ts +385 -0
- package/src/mcp/tools.ts +13 -90
- package/src/memory/episode.ts +92 -0
- package/src/memory/read.ts +76 -19
- package/src/memory/write.ts +253 -20
- package/src/sync.ts +310 -66
- package/src/types.ts +30 -5
- package/src/cli/commands/shared.ts +0 -20
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { Surreal } from 'surrealdb'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { consolidate } from '../cognitive/consolidate.ts'
|
|
4
|
+
import { healthReport, suemoStats, vitals } from '../cognitive/health.ts'
|
|
5
|
+
import type { SuemoConfig } from '../config.ts'
|
|
6
|
+
import { goalList, goalResolve, goalSet } from '../goal.ts'
|
|
7
|
+
import { getLogger } from '../logger.ts'
|
|
8
|
+
import { episodeEnd, episodeStart, getSessionContext, setSessionContext } from '../memory/episode.ts'
|
|
9
|
+
import { query, recall, timeline, wander } from '../memory/read.ts'
|
|
10
|
+
import { believe, capturePrompt, invalidate, observe, upsertByKey } from '../memory/write.ts'
|
|
11
|
+
import { ObserveInputSchema, QueryInputSchema } from '../types.ts'
|
|
12
|
+
|
|
13
|
+
const log = getLogger(['suemo', 'mcp', 'dispatch'])
|
|
14
|
+
|
|
15
|
+
const MUTATING_TOOLS = new Set([
|
|
16
|
+
'observe',
|
|
17
|
+
'believe',
|
|
18
|
+
'invalidate',
|
|
19
|
+
'goal_set',
|
|
20
|
+
'goal_resolve',
|
|
21
|
+
'upsert_by_key',
|
|
22
|
+
'capture_prompt',
|
|
23
|
+
'session_context_set',
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
interface DispatchOptions {
|
|
27
|
+
onMutation?: (tool: string) => Promise<void>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function handleToolCall(
|
|
31
|
+
db: Surreal,
|
|
32
|
+
config: SuemoConfig,
|
|
33
|
+
method: string,
|
|
34
|
+
params: Record<string, unknown>,
|
|
35
|
+
opts: DispatchOptions = {},
|
|
36
|
+
): Promise<unknown> {
|
|
37
|
+
log.debug('Dispatching MCP tool call', { method, paramKeys: Object.keys(params) })
|
|
38
|
+
|
|
39
|
+
const maybeTriggerMutationSync = (): void => {
|
|
40
|
+
if (!MUTATING_TOOLS.has(method) || !opts.onMutation) return
|
|
41
|
+
void opts.onMutation(method).catch((error) => {
|
|
42
|
+
log.warning('Post-mutation hook failed', { method, error: String(error) })
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
switch (method) {
|
|
47
|
+
case 'observe': {
|
|
48
|
+
const result = await observe(db, ObserveInputSchema.parse(params), config)
|
|
49
|
+
maybeTriggerMutationSync()
|
|
50
|
+
return result
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case 'believe': {
|
|
54
|
+
const parsed = z
|
|
55
|
+
.object({
|
|
56
|
+
content: z.string(),
|
|
57
|
+
scope: z.string().optional(),
|
|
58
|
+
confidence: z.number().optional(),
|
|
59
|
+
})
|
|
60
|
+
.parse(params)
|
|
61
|
+
const result = await believe(db, parsed, config)
|
|
62
|
+
maybeTriggerMutationSync()
|
|
63
|
+
return result
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
case 'invalidate': {
|
|
67
|
+
const parsed = z.object({ nodeId: z.string(), reason: z.string().optional() }).parse(params)
|
|
68
|
+
await invalidate(db, parsed.nodeId, parsed.reason)
|
|
69
|
+
maybeTriggerMutationSync()
|
|
70
|
+
return { ok: true }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
case 'query':
|
|
74
|
+
return query(db, QueryInputSchema.parse(params), config)
|
|
75
|
+
|
|
76
|
+
case 'recall': {
|
|
77
|
+
const parsed = z.object({ nodeId: z.string() }).parse(params)
|
|
78
|
+
return recall(db, parsed.nodeId)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case 'wander': {
|
|
82
|
+
const parsed = z
|
|
83
|
+
.object({ anchor: z.string().optional(), hops: z.number().optional(), scope: z.string().optional() })
|
|
84
|
+
.parse(params)
|
|
85
|
+
return wander(db, {
|
|
86
|
+
...(parsed.anchor ? { anchor: parsed.anchor } : {}),
|
|
87
|
+
...(parsed.hops ? { hops: parsed.hops } : {}),
|
|
88
|
+
...(parsed.scope ? { scope: parsed.scope } : {}),
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
case 'timeline': {
|
|
93
|
+
const parsed = z
|
|
94
|
+
.object({
|
|
95
|
+
scope: z.string().optional(),
|
|
96
|
+
from: z.string().optional(),
|
|
97
|
+
until: z.string().optional(),
|
|
98
|
+
limit: z.number().optional(),
|
|
99
|
+
})
|
|
100
|
+
.parse(params)
|
|
101
|
+
return timeline(db, {
|
|
102
|
+
...(parsed.scope ? { scope: parsed.scope } : {}),
|
|
103
|
+
...(parsed.from ? { from: parsed.from } : {}),
|
|
104
|
+
...(parsed.until ? { until: parsed.until } : {}),
|
|
105
|
+
...(parsed.limit ? { limit: parsed.limit } : {}),
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case 'episode_start': {
|
|
110
|
+
const parsed = z.object({ sessionId: z.string() }).parse(params)
|
|
111
|
+
return episodeStart(db, parsed.sessionId)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
case 'episode_end': {
|
|
115
|
+
const parsed = z.object({ sessionId: z.string(), summary: z.string().optional() }).parse(params)
|
|
116
|
+
return episodeEnd(db, parsed.sessionId, parsed.summary)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
case 'goal_set': {
|
|
120
|
+
const parsed = z
|
|
121
|
+
.object({ content: z.string(), scope: z.string().optional(), tags: z.array(z.string()).optional() })
|
|
122
|
+
.parse(params)
|
|
123
|
+
const result = await goalSet(db, parsed.content, config, {
|
|
124
|
+
...(parsed.scope ? { scope: parsed.scope } : {}),
|
|
125
|
+
tags: parsed.tags ?? [],
|
|
126
|
+
})
|
|
127
|
+
maybeTriggerMutationSync()
|
|
128
|
+
return result
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
case 'goal_resolve': {
|
|
132
|
+
const parsed = z.object({ goalId: z.string() }).parse(params)
|
|
133
|
+
await goalResolve(db, parsed.goalId)
|
|
134
|
+
maybeTriggerMutationSync()
|
|
135
|
+
return { ok: true }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case 'goal_list': {
|
|
139
|
+
const parsed = z.object({ scope: z.string().optional(), includeResolved: z.boolean().optional() }).parse(params)
|
|
140
|
+
return goalList(db, {
|
|
141
|
+
...(parsed.scope ? { scope: parsed.scope } : {}),
|
|
142
|
+
includeResolved: parsed.includeResolved ?? false,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
case 'health':
|
|
147
|
+
return healthReport(db)
|
|
148
|
+
|
|
149
|
+
case 'stats':
|
|
150
|
+
return suemoStats(db)
|
|
151
|
+
|
|
152
|
+
case 'vitals':
|
|
153
|
+
return vitals(db)
|
|
154
|
+
|
|
155
|
+
case 'consolidate': {
|
|
156
|
+
const parsed = z.object({ nremOnly: z.boolean().optional() }).parse(params)
|
|
157
|
+
return consolidate(db, {
|
|
158
|
+
nremOnly: parsed.nremOnly ?? false,
|
|
159
|
+
nremSimilarityThreshold: config.consolidation.nremSimilarityThreshold,
|
|
160
|
+
remRelationThreshold: config.consolidation.remRelationThreshold,
|
|
161
|
+
llm: config.consolidation.llm,
|
|
162
|
+
embedding: config.embedding,
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case 'upsert_by_key': {
|
|
167
|
+
const parsed = z
|
|
168
|
+
.object({
|
|
169
|
+
topicKey: z.string(),
|
|
170
|
+
content: z.string(),
|
|
171
|
+
scope: z.string().optional(),
|
|
172
|
+
tags: z.array(z.string()).optional(),
|
|
173
|
+
confidence: z.number().optional(),
|
|
174
|
+
source: z.string().optional(),
|
|
175
|
+
sessionId: z.string().optional(),
|
|
176
|
+
kind: z.enum(['observation', 'belief', 'question', 'hypothesis', 'goal']).optional(),
|
|
177
|
+
})
|
|
178
|
+
.parse(params)
|
|
179
|
+
const result = await upsertByKey(db, config, parsed.topicKey, parsed.content, {
|
|
180
|
+
...(parsed.scope ? { scope: parsed.scope } : {}),
|
|
181
|
+
tags: parsed.tags ?? [],
|
|
182
|
+
...(parsed.source ? { source: parsed.source } : {}),
|
|
183
|
+
...(parsed.sessionId ? { sessionId: parsed.sessionId } : {}),
|
|
184
|
+
confidence: parsed.confidence ?? 1.0,
|
|
185
|
+
...(parsed.kind ? { kind: parsed.kind } : {}),
|
|
186
|
+
})
|
|
187
|
+
maybeTriggerMutationSync()
|
|
188
|
+
return result
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
case 'capture_prompt': {
|
|
192
|
+
const parsed = z
|
|
193
|
+
.object({
|
|
194
|
+
prompt: z.string(),
|
|
195
|
+
derivedIds: z.array(z.string()).optional(),
|
|
196
|
+
scope: z.string().optional(),
|
|
197
|
+
sessionId: z.string().optional(),
|
|
198
|
+
})
|
|
199
|
+
.parse(params)
|
|
200
|
+
const result = await capturePrompt(db, config, parsed.prompt, parsed.derivedIds ?? [], {
|
|
201
|
+
...(parsed.scope ? { scope: parsed.scope } : {}),
|
|
202
|
+
...(parsed.sessionId ? { sessionId: parsed.sessionId } : {}),
|
|
203
|
+
})
|
|
204
|
+
maybeTriggerMutationSync()
|
|
205
|
+
return result
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
case 'session_context_get': {
|
|
209
|
+
const parsed = z.object({ sessionId: z.string() }).parse(params)
|
|
210
|
+
return getSessionContext(db, parsed.sessionId)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
case 'session_context_set': {
|
|
214
|
+
const parsed = z
|
|
215
|
+
.object({
|
|
216
|
+
sessionId: z.string(),
|
|
217
|
+
summary: z.string().optional(),
|
|
218
|
+
context: z.record(z.string(), z.unknown()).optional(),
|
|
219
|
+
})
|
|
220
|
+
.parse(params)
|
|
221
|
+
await setSessionContext(db, parsed.sessionId, {
|
|
222
|
+
...(parsed.summary !== undefined ? { summary: parsed.summary } : {}),
|
|
223
|
+
...(parsed.context !== undefined ? { context: parsed.context } : {}),
|
|
224
|
+
})
|
|
225
|
+
maybeTriggerMutationSync()
|
|
226
|
+
return { ok: true }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
default:
|
|
230
|
+
throw new Error(`Unknown MCP tool: ${method}`)
|
|
231
|
+
}
|
|
232
|
+
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -1,23 +1,169 @@
|
|
|
1
1
|
// src/mcp/server.ts
|
|
2
2
|
import { Elysia } from 'elysia'
|
|
3
|
-
import type
|
|
4
|
-
import { connect } from '../db/client.ts'
|
|
5
|
-
import { requireCompatibility } from '../db/preflight.ts'
|
|
3
|
+
import { resolveSyncConfig, type SuemoConfig } from '../config.ts'
|
|
4
|
+
import { connect, disconnect } from '../db/client.ts'
|
|
5
|
+
import { checkCompatibility, requireCompatibility } from '../db/preflight.ts'
|
|
6
6
|
import { runSchema } from '../db/schema.ts'
|
|
7
7
|
import { getLogger } from '../logger.ts'
|
|
8
|
+
import { syncTo } from '../sync.ts'
|
|
9
|
+
import { runStdioServer } from './stdio.ts'
|
|
8
10
|
import { buildMcpRouter } from './tools.ts'
|
|
9
11
|
|
|
10
12
|
const log = getLogger(['suemo', 'mcp'])
|
|
11
13
|
|
|
14
|
+
interface AutoSyncOptions {
|
|
15
|
+
reason: 'timer' | 'write'
|
|
16
|
+
force?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createAutoSyncRunner(
|
|
20
|
+
db: ReturnType<typeof connect> extends Promise<infer T> ? T : never,
|
|
21
|
+
config: SuemoConfig,
|
|
22
|
+
): {
|
|
23
|
+
start: () => void
|
|
24
|
+
onWrite: (tool: string) => Promise<void>
|
|
25
|
+
stop: () => void
|
|
26
|
+
} {
|
|
27
|
+
const resolvedSync = resolveSyncConfig(config)
|
|
28
|
+
if (!resolvedSync) {
|
|
29
|
+
return {
|
|
30
|
+
start: () => {},
|
|
31
|
+
onWrite: async () => {},
|
|
32
|
+
stop: () => {},
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let timer: ReturnType<typeof setInterval> | null = null
|
|
37
|
+
let running = false
|
|
38
|
+
let queued = false
|
|
39
|
+
let lastRunAt = 0
|
|
40
|
+
|
|
41
|
+
const runAutoSync = async ({ reason, force = false }: AutoSyncOptions): Promise<void> => {
|
|
42
|
+
if (!resolvedSync.auto.enabled) return
|
|
43
|
+
if (reason === 'write' && !resolvedSync.auto.onWrite) return
|
|
44
|
+
|
|
45
|
+
const now = Date.now()
|
|
46
|
+
const minIntervalMs = resolvedSync.auto.minWriteIntervalSeconds * 1000
|
|
47
|
+
if (!force && reason === 'write' && lastRunAt > 0 && now - lastRunAt < minIntervalMs) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (running) {
|
|
52
|
+
queued = true
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const target = resolvedSync.remotes[resolvedSync.auto.remote]
|
|
57
|
+
if (!target) {
|
|
58
|
+
log.warning('Auto-sync skipped: remote missing', { remote: resolvedSync.auto.remote, reason })
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
running = true
|
|
63
|
+
try {
|
|
64
|
+
const result = await syncTo(db, target, {
|
|
65
|
+
direction: resolvedSync.auto.direction,
|
|
66
|
+
})
|
|
67
|
+
lastRunAt = Date.now()
|
|
68
|
+
log.info('Auto-sync completed', {
|
|
69
|
+
reason,
|
|
70
|
+
remote: resolvedSync.auto.remote,
|
|
71
|
+
direction: resolvedSync.auto.direction,
|
|
72
|
+
pushed: result.pushed,
|
|
73
|
+
errors: result.errors,
|
|
74
|
+
})
|
|
75
|
+
} catch (error) {
|
|
76
|
+
log.warning('Auto-sync failed', { reason, error: String(error) })
|
|
77
|
+
} finally {
|
|
78
|
+
running = false
|
|
79
|
+
if (queued) {
|
|
80
|
+
queued = false
|
|
81
|
+
queueMicrotask(() => {
|
|
82
|
+
void runAutoSync({ reason: 'write', force: true })
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const start = (): void => {
|
|
89
|
+
if (!resolvedSync.auto.enabled) return
|
|
90
|
+
if (timer) return
|
|
91
|
+
const intervalMs = resolvedSync.auto.intervalSeconds * 1000
|
|
92
|
+
timer = setInterval(() => {
|
|
93
|
+
void runAutoSync({ reason: 'timer' })
|
|
94
|
+
}, intervalMs)
|
|
95
|
+
log.info('Auto-sync timer started', {
|
|
96
|
+
intervalSeconds: resolvedSync.auto.intervalSeconds,
|
|
97
|
+
remote: resolvedSync.auto.remote,
|
|
98
|
+
direction: resolvedSync.auto.direction,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const stop = (): void => {
|
|
103
|
+
if (!timer) return
|
|
104
|
+
clearInterval(timer)
|
|
105
|
+
timer = null
|
|
106
|
+
log.info('Auto-sync timer stopped')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const onWrite = async (tool: string): Promise<void> => {
|
|
110
|
+
if (!resolvedSync.auto.enabled || !resolvedSync.auto.onWrite) return
|
|
111
|
+
log.debug('Auto-sync write trigger', { tool })
|
|
112
|
+
await runAutoSync({ reason: 'write' })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { start, onWrite, stop }
|
|
116
|
+
}
|
|
117
|
+
|
|
12
118
|
export async function startMcpServer(config: SuemoConfig): Promise<void> {
|
|
13
119
|
const db = await connect(config.surreal)
|
|
14
120
|
await requireCompatibility(db)
|
|
15
121
|
await runSchema(db)
|
|
122
|
+
const autoSync = createAutoSyncRunner(db, config)
|
|
123
|
+
autoSync.start()
|
|
124
|
+
|
|
125
|
+
const shutdown = async (): Promise<void> => {
|
|
126
|
+
autoSync.stop()
|
|
127
|
+
await disconnect()
|
|
128
|
+
process.exit(0)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
process.once('SIGINT', () => {
|
|
132
|
+
void shutdown()
|
|
133
|
+
})
|
|
134
|
+
process.once('SIGTERM', () => {
|
|
135
|
+
void shutdown()
|
|
136
|
+
})
|
|
16
137
|
|
|
17
138
|
new Elysia()
|
|
18
|
-
.use(buildMcpRouter(db, config))
|
|
139
|
+
.use(buildMcpRouter(db, config, { onMutation: autoSync.onWrite }))
|
|
19
140
|
.get('/health', () => ({ status: 'ok' }))
|
|
20
141
|
.listen({ port: config.mcp.port, hostname: config.mcp.host })
|
|
21
142
|
|
|
22
143
|
log.info('MCP server listening', { port: config.mcp.port, host: config.mcp.host })
|
|
23
144
|
}
|
|
145
|
+
|
|
146
|
+
export async function startMcpStdioServer(config: SuemoConfig): Promise<void> {
|
|
147
|
+
const db = await connect(config.surreal)
|
|
148
|
+
const autoSync = createAutoSyncRunner(db, config)
|
|
149
|
+
autoSync.start()
|
|
150
|
+
try {
|
|
151
|
+
const compat = await checkCompatibility(db, {
|
|
152
|
+
requireEmbedding: config.embedding.provider === 'surreal',
|
|
153
|
+
context: 'mcp:stdio-startup',
|
|
154
|
+
})
|
|
155
|
+
if (!compat.ok) {
|
|
156
|
+
console.error('\n[suemo] Compatibility check failed:\n')
|
|
157
|
+
for (const err of compat.errors) {
|
|
158
|
+
console.error(` ✗ ${err}`)
|
|
159
|
+
}
|
|
160
|
+
console.error('\nFix the issues above and retry.\n')
|
|
161
|
+
process.exit(1)
|
|
162
|
+
}
|
|
163
|
+
await runSchema(db)
|
|
164
|
+
await runStdioServer(db, config, { onMutation: autoSync.onWrite })
|
|
165
|
+
} finally {
|
|
166
|
+
autoSync.stop()
|
|
167
|
+
await disconnect()
|
|
168
|
+
}
|
|
169
|
+
}
|