suemo 0.0.4 → 0.0.6
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 +54 -2
- package/package.json +5 -2
- package/src/cli/commands/believe.ts +24 -7
- package/src/cli/commands/consolidate.ts +21 -3
- package/src/cli/commands/doctor.ts +27 -3
- package/src/cli/commands/export-import.ts +47 -6
- package/src/cli/commands/goal.ts +63 -15
- package/src/cli/commands/health.ts +77 -9
- package/src/cli/commands/init.ts +101 -4
- package/src/cli/commands/observe.ts +21 -5
- package/src/cli/commands/query.ts +23 -9
- package/src/cli/commands/recall.ts +19 -3
- package/src/cli/commands/serve.ts +110 -7
- package/src/cli/commands/skill.ts +56 -0
- package/src/cli/commands/sync.ts +22 -3
- package/src/cli/commands/timeline.ts +23 -9
- package/src/cli/commands/wander.ts +19 -9
- package/src/cli/index.ts +2 -0
- package/src/cli/shared.ts +89 -2
- package/src/cognitive/consolidate.ts +21 -7
- package/src/cognitive/contradiction.ts +2 -1
- package/src/cognitive/health.ts +3 -3
- package/src/config.template.ts +3 -0
- package/src/config.ts +122 -2
- package/src/db/client.ts +3 -3
- package/src/db/preflight.ts +8 -9
- package/src/db/schema.surql +17 -0
- package/src/index.ts +3 -1
- package/src/logger.ts +23 -1
- package/src/mcp/dispatch.ts +116 -16
- package/src/mcp/server.ts +22 -11
- package/src/mcp/stdio.ts +57 -1
- package/src/memory/episode.ts +38 -6
- package/src/memory/read.ts +69 -1
- package/src/memory/write.ts +95 -13
- package/src/skill/catalog.ts +39 -0
- package/src/sync.ts +85 -0
- package/src/types.ts +13 -0
package/src/db/preflight.ts
CHANGED
|
@@ -109,9 +109,12 @@ export async function checkCompatibility(
|
|
|
109
109
|
+ 'Start SurrealDB with: SURREAL_DATASTORE_RETENTION=90d surreal start surrealkv://path/to/data',
|
|
110
110
|
)
|
|
111
111
|
} else {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
retention_ok = false
|
|
113
|
+
errors.push(
|
|
114
|
+
`Retention probe failed unexpectedly: ${String(e)}. `
|
|
115
|
+
+ 'Verify server capabilities and retention env (SURREAL_DATASTORE_VERSIONED=true and SURREAL_DATASTORE_RETENTION=90d).',
|
|
116
|
+
)
|
|
117
|
+
log.debug('Retention probe returned unexpected error', { error: String(e) })
|
|
115
118
|
}
|
|
116
119
|
} finally {
|
|
117
120
|
try {
|
|
@@ -172,11 +175,7 @@ export async function checkCompatibility(
|
|
|
172
175
|
export async function requireCompatibility(db: Surreal): Promise<void> {
|
|
173
176
|
const result = await checkCompatibility(db)
|
|
174
177
|
if (!result.ok) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
console.error(` ✗ ${err}`)
|
|
178
|
-
}
|
|
179
|
-
console.error('\nFix the issues above and retry.\n')
|
|
180
|
-
process.exit(1)
|
|
178
|
+
const details = result.errors.map((err) => ` ✗ ${err}`).join('\n')
|
|
179
|
+
throw new Error(`\n[suemo] Compatibility check failed:\n\n${details}\n\nFix the issues above and retry.\n`)
|
|
181
180
|
}
|
|
182
181
|
}
|
package/src/db/schema.surql
CHANGED
|
@@ -70,6 +70,9 @@ DEFINE INDEX OVERWRITE idx_memory_salience
|
|
|
70
70
|
DEFINE INDEX OVERWRITE idx_memory_topic_key
|
|
71
71
|
ON memory FIELDS topic_key;
|
|
72
72
|
|
|
73
|
+
DEFINE INDEX OVERWRITE idx_memory_updated_at
|
|
74
|
+
ON memory FIELDS updated_at;
|
|
75
|
+
|
|
73
76
|
-- ── relates_to ───────────────────────────────────────────────────────────────
|
|
74
77
|
DEFINE TABLE OVERWRITE relates_to SCHEMAFULL
|
|
75
78
|
TYPE RELATION IN memory OUT memory;
|
|
@@ -83,14 +86,28 @@ DEFINE FIELD OVERWRITE valid_until ON relates_to TYPE option<datetime> DEFAULT N
|
|
|
83
86
|
DEFINE FIELD OVERWRITE created_at ON relates_to TYPE datetime DEFAULT time::now();
|
|
84
87
|
DEFINE FIELD OVERWRITE updated_at ON relates_to TYPE datetime DEFAULT time::now();
|
|
85
88
|
|
|
89
|
+
DEFINE INDEX OVERWRITE idx_relates_to_updated_at
|
|
90
|
+
ON relates_to FIELDS updated_at;
|
|
91
|
+
|
|
86
92
|
-- ── episode ───────────────────────────────────────────────────────────────────
|
|
87
93
|
DEFINE TABLE OVERWRITE episode SCHEMAFULL;
|
|
88
94
|
|
|
89
95
|
DEFINE FIELD OVERWRITE session_id ON episode TYPE string;
|
|
90
96
|
DEFINE FIELD OVERWRITE started_at ON episode TYPE datetime DEFAULT time::now();
|
|
91
97
|
DEFINE FIELD OVERWRITE ended_at ON episode TYPE option<datetime> DEFAULT NONE;
|
|
98
|
+
DEFINE FIELD OVERWRITE updated_at ON episode TYPE datetime DEFAULT time::now();
|
|
92
99
|
DEFINE FIELD OVERWRITE summary ON episode TYPE option<string>;
|
|
93
100
|
DEFINE FIELD OVERWRITE context ON episode TYPE option<object> FLEXIBLE DEFAULT NONE;
|
|
101
|
+
DEFINE FIELD OVERWRITE goal ON episode TYPE option<string> DEFAULT NONE;
|
|
102
|
+
DEFINE FIELD OVERWRITE discoveries ON episode TYPE option<array<string>> DEFAULT NONE;
|
|
103
|
+
DEFINE FIELD OVERWRITE accomplished ON episode TYPE option<array<string>> DEFAULT NONE;
|
|
104
|
+
DEFINE FIELD OVERWRITE files_changed ON episode TYPE option<array<string>> DEFAULT NONE;
|
|
105
|
+
|
|
106
|
+
DEFINE INDEX OVERWRITE idx_episode_session_time
|
|
107
|
+
ON episode FIELDS session_id, ended_at, started_at;
|
|
108
|
+
|
|
109
|
+
DEFINE INDEX OVERWRITE idx_episode_updated_at
|
|
110
|
+
ON episode FIELDS updated_at;
|
|
94
111
|
|
|
95
112
|
-- REFERENCES: bidirectional array of memory links
|
|
96
113
|
DEFINE FIELD OVERWRITE memory_ids ON episode
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// src/index.ts — public API surface
|
|
2
|
-
export { defineConfig, loadConfig, resolveSyncConfig } from './config.ts'
|
|
2
|
+
export { defineConfig, inferProjectIdentity, inferProjectScope, loadConfig, resolveSyncConfig } from './config.ts'
|
|
3
3
|
export type {
|
|
4
4
|
AuthConfig,
|
|
5
5
|
ConsolidationConfig,
|
|
6
6
|
EmbeddingProvider,
|
|
7
7
|
LLMConfig,
|
|
8
|
+
MainConfig,
|
|
8
9
|
McpConfig,
|
|
10
|
+
ProjectIdentity,
|
|
9
11
|
ResolvedSyncAutoConfig,
|
|
10
12
|
ResolvedSyncConfig,
|
|
11
13
|
RetrievalConfig,
|
package/src/logger.ts
CHANGED
|
@@ -10,10 +10,26 @@ export type LogLevel = 'debug' | 'info' | 'warning' | 'error' | 'fatal'
|
|
|
10
10
|
|
|
11
11
|
// Different formatters for different purposes
|
|
12
12
|
const prettySink: Sink = getConsoleSink({
|
|
13
|
+
console: {
|
|
14
|
+
debug: console.error,
|
|
15
|
+
info: console.error,
|
|
16
|
+
log: console.error,
|
|
17
|
+
warn: console.error,
|
|
18
|
+
error: console.error,
|
|
19
|
+
trace: console.error,
|
|
20
|
+
} as Console,
|
|
13
21
|
formatter: record => `${record.level}: ${String(record.message)}\n`,
|
|
14
22
|
})
|
|
15
23
|
|
|
16
24
|
const jsonSink: Sink = getConsoleSink({
|
|
25
|
+
console: {
|
|
26
|
+
debug: console.error,
|
|
27
|
+
info: console.error,
|
|
28
|
+
log: console.error,
|
|
29
|
+
warn: console.error,
|
|
30
|
+
error: console.error,
|
|
31
|
+
trace: console.error,
|
|
32
|
+
} as Console,
|
|
17
33
|
formatter: record =>
|
|
18
34
|
JSON.stringify({
|
|
19
35
|
level: record.level,
|
|
@@ -26,12 +42,16 @@ const jsonSink: Sink = getConsoleSink({
|
|
|
26
42
|
export async function initLogger(options: {
|
|
27
43
|
level: LogLevel
|
|
28
44
|
logFile?: string // if set, also write to file
|
|
45
|
+
quiet?: boolean
|
|
29
46
|
}): Promise<void> {
|
|
30
47
|
const sinks: Record<string, ReturnType<typeof getConsoleSink>> = {
|
|
31
48
|
pretty: prettySink,
|
|
32
49
|
json: jsonSink,
|
|
33
50
|
}
|
|
34
51
|
|
|
52
|
+
const noopSink: Sink = () => {}
|
|
53
|
+
sinks['noop'] = noopSink as ReturnType<typeof getConsoleSink>
|
|
54
|
+
|
|
35
55
|
if (options.logFile) {
|
|
36
56
|
sinks['file'] = getFileSink(options.logFile)
|
|
37
57
|
}
|
|
@@ -48,7 +68,9 @@ export async function initLogger(options: {
|
|
|
48
68
|
{
|
|
49
69
|
category: ['suemo'],
|
|
50
70
|
lowestLevel: options.level,
|
|
51
|
-
sinks:
|
|
71
|
+
sinks: options.quiet
|
|
72
|
+
? ['noop']
|
|
73
|
+
: Object.keys(sinks).filter(item => item !== 'json' && item !== 'noop'),
|
|
52
74
|
},
|
|
53
75
|
],
|
|
54
76
|
})
|
package/src/mcp/dispatch.ts
CHANGED
|
@@ -2,12 +2,22 @@ import type { Surreal } from 'surrealdb'
|
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { consolidate } from '../cognitive/consolidate.ts'
|
|
4
4
|
import { healthReport, suemoStats, vitals } from '../cognitive/health.ts'
|
|
5
|
+
import { inferProjectScope } from '../config.ts'
|
|
5
6
|
import type { SuemoConfig } from '../config.ts'
|
|
6
7
|
import { goalList, goalResolve, goalSet } from '../goal.ts'
|
|
7
8
|
import { getLogger } from '../logger.ts'
|
|
8
9
|
import { episodeEnd, episodeStart, getSessionContext, setSessionContext } from '../memory/episode.ts'
|
|
9
|
-
import { query, recall, timeline, wander } from '../memory/read.ts'
|
|
10
|
-
import {
|
|
10
|
+
import { context, query, recall, timeline, wander } from '../memory/read.ts'
|
|
11
|
+
import {
|
|
12
|
+
believe,
|
|
13
|
+
capturePrompt,
|
|
14
|
+
invalidate,
|
|
15
|
+
observe,
|
|
16
|
+
suggestTopicKey,
|
|
17
|
+
updateMemoryById,
|
|
18
|
+
upsertByKey,
|
|
19
|
+
} from '../memory/write.ts'
|
|
20
|
+
import { listSkillReferences, readSkillDoc, readSkillReference } from '../skill/catalog.ts'
|
|
11
21
|
import { ObserveInputSchema, QueryInputSchema } from '../types.ts'
|
|
12
22
|
|
|
13
23
|
const log = getLogger(['suemo', 'mcp', 'dispatch'])
|
|
@@ -20,7 +30,10 @@ const MUTATING_TOOLS = new Set([
|
|
|
20
30
|
'goal_resolve',
|
|
21
31
|
'upsert_by_key',
|
|
22
32
|
'capture_prompt',
|
|
33
|
+
'episode_start',
|
|
34
|
+
'episode_end',
|
|
23
35
|
'session_context_set',
|
|
36
|
+
'update',
|
|
24
37
|
])
|
|
25
38
|
|
|
26
39
|
interface DispatchOptions {
|
|
@@ -35,6 +48,7 @@ export async function handleToolCall(
|
|
|
35
48
|
opts: DispatchOptions = {},
|
|
36
49
|
): Promise<unknown> {
|
|
37
50
|
log.debug('Dispatching MCP tool call', { method, paramKeys: Object.keys(params) })
|
|
51
|
+
const inferredScope = inferProjectScope(process.cwd(), config)
|
|
38
52
|
|
|
39
53
|
const maybeTriggerMutationSync = (): void => {
|
|
40
54
|
if (!MUTATING_TOOLS.has(method) || !opts.onMutation) return
|
|
@@ -45,7 +59,11 @@ export async function handleToolCall(
|
|
|
45
59
|
|
|
46
60
|
switch (method) {
|
|
47
61
|
case 'observe': {
|
|
48
|
-
const
|
|
62
|
+
const parsed = ObserveInputSchema.parse(params)
|
|
63
|
+
const result = await observe(db, {
|
|
64
|
+
...parsed,
|
|
65
|
+
scope: parsed.scope?.trim() || inferredScope,
|
|
66
|
+
}, config)
|
|
49
67
|
maybeTriggerMutationSync()
|
|
50
68
|
return result
|
|
51
69
|
}
|
|
@@ -58,7 +76,10 @@ export async function handleToolCall(
|
|
|
58
76
|
confidence: z.number().optional(),
|
|
59
77
|
})
|
|
60
78
|
.parse(params)
|
|
61
|
-
const result = await believe(db,
|
|
79
|
+
const result = await believe(db, {
|
|
80
|
+
...parsed,
|
|
81
|
+
scope: parsed.scope?.trim() || inferredScope,
|
|
82
|
+
}, config)
|
|
62
83
|
maybeTriggerMutationSync()
|
|
63
84
|
return result
|
|
64
85
|
}
|
|
@@ -70,8 +91,13 @@ export async function handleToolCall(
|
|
|
70
91
|
return { ok: true }
|
|
71
92
|
}
|
|
72
93
|
|
|
73
|
-
case 'query':
|
|
74
|
-
|
|
94
|
+
case 'query': {
|
|
95
|
+
const parsed = QueryInputSchema.parse(params)
|
|
96
|
+
return query(db, {
|
|
97
|
+
...parsed,
|
|
98
|
+
scope: parsed.scope?.trim() || inferredScope,
|
|
99
|
+
}, config)
|
|
100
|
+
}
|
|
75
101
|
|
|
76
102
|
case 'recall': {
|
|
77
103
|
const parsed = z.object({ nodeId: z.string() }).parse(params)
|
|
@@ -85,7 +111,7 @@ export async function handleToolCall(
|
|
|
85
111
|
return wander(db, {
|
|
86
112
|
...(parsed.anchor ? { anchor: parsed.anchor } : {}),
|
|
87
113
|
...(parsed.hops ? { hops: parsed.hops } : {}),
|
|
88
|
-
|
|
114
|
+
scope: parsed.scope?.trim() || inferredScope,
|
|
89
115
|
})
|
|
90
116
|
}
|
|
91
117
|
|
|
@@ -99,21 +125,46 @@ export async function handleToolCall(
|
|
|
99
125
|
})
|
|
100
126
|
.parse(params)
|
|
101
127
|
return timeline(db, {
|
|
102
|
-
|
|
128
|
+
scope: parsed.scope?.trim() || inferredScope,
|
|
103
129
|
...(parsed.from ? { from: parsed.from } : {}),
|
|
104
130
|
...(parsed.until ? { until: parsed.until } : {}),
|
|
105
131
|
...(parsed.limit ? { limit: parsed.limit } : {}),
|
|
106
132
|
})
|
|
107
133
|
}
|
|
108
134
|
|
|
135
|
+
case 'context': {
|
|
136
|
+
const parsed = z.object({ scope: z.string().optional(), limit: z.number().optional() }).parse(params)
|
|
137
|
+
return context(db, {
|
|
138
|
+
scope: parsed.scope?.trim() || inferredScope,
|
|
139
|
+
...(parsed.limit !== undefined ? { limit: parsed.limit } : {}),
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
109
143
|
case 'episode_start': {
|
|
110
144
|
const parsed = z.object({ sessionId: z.string() }).parse(params)
|
|
111
|
-
|
|
145
|
+
const result = await episodeStart(db, parsed.sessionId)
|
|
146
|
+
maybeTriggerMutationSync()
|
|
147
|
+
return result
|
|
112
148
|
}
|
|
113
149
|
|
|
114
150
|
case 'episode_end': {
|
|
115
|
-
const parsed = z.object({
|
|
116
|
-
|
|
151
|
+
const parsed = z.object({
|
|
152
|
+
sessionId: z.string(),
|
|
153
|
+
summary: z.string().optional(),
|
|
154
|
+
goal: z.string().optional(),
|
|
155
|
+
discoveries: z.array(z.string()).optional(),
|
|
156
|
+
accomplished: z.array(z.string()).optional(),
|
|
157
|
+
files_changed: z.array(z.string()).optional(),
|
|
158
|
+
}).parse(params)
|
|
159
|
+
const result = await episodeEnd(db, parsed.sessionId, parsed.summary, {
|
|
160
|
+
...(parsed.summary !== undefined ? { summary: parsed.summary } : {}),
|
|
161
|
+
...(parsed.goal !== undefined ? { goal: parsed.goal } : {}),
|
|
162
|
+
...(parsed.discoveries !== undefined ? { discoveries: parsed.discoveries } : {}),
|
|
163
|
+
...(parsed.accomplished !== undefined ? { accomplished: parsed.accomplished } : {}),
|
|
164
|
+
...(parsed.files_changed !== undefined ? { files_changed: parsed.files_changed } : {}),
|
|
165
|
+
})
|
|
166
|
+
maybeTriggerMutationSync()
|
|
167
|
+
return result
|
|
117
168
|
}
|
|
118
169
|
|
|
119
170
|
case 'goal_set': {
|
|
@@ -121,7 +172,7 @@ export async function handleToolCall(
|
|
|
121
172
|
.object({ content: z.string(), scope: z.string().optional(), tags: z.array(z.string()).optional() })
|
|
122
173
|
.parse(params)
|
|
123
174
|
const result = await goalSet(db, parsed.content, config, {
|
|
124
|
-
|
|
175
|
+
scope: parsed.scope?.trim() || inferredScope,
|
|
125
176
|
tags: parsed.tags ?? [],
|
|
126
177
|
})
|
|
127
178
|
maybeTriggerMutationSync()
|
|
@@ -138,7 +189,7 @@ export async function handleToolCall(
|
|
|
138
189
|
case 'goal_list': {
|
|
139
190
|
const parsed = z.object({ scope: z.string().optional(), includeResolved: z.boolean().optional() }).parse(params)
|
|
140
191
|
return goalList(db, {
|
|
141
|
-
|
|
192
|
+
scope: parsed.scope?.trim() || inferredScope,
|
|
142
193
|
includeResolved: parsed.includeResolved ?? false,
|
|
143
194
|
})
|
|
144
195
|
}
|
|
@@ -153,13 +204,14 @@ export async function handleToolCall(
|
|
|
153
204
|
return vitals(db)
|
|
154
205
|
|
|
155
206
|
case 'consolidate': {
|
|
156
|
-
const parsed = z.object({ nremOnly: z.boolean().optional() }).parse(params)
|
|
207
|
+
const parsed = z.object({ nremOnly: z.boolean().optional(), scope: z.string().optional() }).parse(params)
|
|
157
208
|
return consolidate(db, {
|
|
158
209
|
nremOnly: parsed.nremOnly ?? false,
|
|
159
210
|
nremSimilarityThreshold: config.consolidation.nremSimilarityThreshold,
|
|
160
211
|
remRelationThreshold: config.consolidation.remRelationThreshold,
|
|
161
212
|
llm: config.consolidation.llm,
|
|
162
213
|
embedding: config.embedding,
|
|
214
|
+
scope: parsed.scope?.trim() || inferredScope,
|
|
163
215
|
})
|
|
164
216
|
}
|
|
165
217
|
|
|
@@ -177,7 +229,7 @@ export async function handleToolCall(
|
|
|
177
229
|
})
|
|
178
230
|
.parse(params)
|
|
179
231
|
const result = await upsertByKey(db, config, parsed.topicKey, parsed.content, {
|
|
180
|
-
|
|
232
|
+
scope: parsed.scope?.trim() || inferredScope,
|
|
181
233
|
tags: parsed.tags ?? [],
|
|
182
234
|
...(parsed.source ? { source: parsed.source } : {}),
|
|
183
235
|
...(parsed.sessionId ? { sessionId: parsed.sessionId } : {}),
|
|
@@ -198,13 +250,61 @@ export async function handleToolCall(
|
|
|
198
250
|
})
|
|
199
251
|
.parse(params)
|
|
200
252
|
const result = await capturePrompt(db, config, parsed.prompt, parsed.derivedIds ?? [], {
|
|
201
|
-
|
|
253
|
+
scope: parsed.scope?.trim() || inferredScope,
|
|
202
254
|
...(parsed.sessionId ? { sessionId: parsed.sessionId } : {}),
|
|
203
255
|
})
|
|
204
256
|
maybeTriggerMutationSync()
|
|
205
257
|
return result
|
|
206
258
|
}
|
|
207
259
|
|
|
260
|
+
case 'update': {
|
|
261
|
+
const parsed = z
|
|
262
|
+
.object({
|
|
263
|
+
nodeId: z.string(),
|
|
264
|
+
content: z.string().optional(),
|
|
265
|
+
kind: z.enum(['observation', 'belief', 'question', 'hypothesis', 'goal']).optional(),
|
|
266
|
+
tags: z.array(z.string()).optional(),
|
|
267
|
+
scope: z.string().nullable().optional(),
|
|
268
|
+
source: z.string().nullable().optional(),
|
|
269
|
+
confidence: z.number().optional(),
|
|
270
|
+
})
|
|
271
|
+
.parse(params)
|
|
272
|
+
|
|
273
|
+
const result = await updateMemoryById(db, config, parsed.nodeId, {
|
|
274
|
+
...(parsed.content !== undefined ? { content: parsed.content } : {}),
|
|
275
|
+
...(parsed.kind !== undefined ? { kind: parsed.kind } : {}),
|
|
276
|
+
...(parsed.tags !== undefined ? { tags: parsed.tags } : {}),
|
|
277
|
+
...(parsed.scope !== undefined ? { scope: parsed.scope ?? null } : {}),
|
|
278
|
+
...(parsed.source !== undefined ? { source: parsed.source ?? null } : {}),
|
|
279
|
+
...(parsed.confidence !== undefined ? { confidence: parsed.confidence } : {}),
|
|
280
|
+
})
|
|
281
|
+
maybeTriggerMutationSync()
|
|
282
|
+
return result
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
case 'suggest_topic_key': {
|
|
286
|
+
const parsed = z.object({ input: z.string() }).parse(params)
|
|
287
|
+
return { topicKey: suggestTopicKey(parsed.input) }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
case 'skill': {
|
|
291
|
+
const parsed = z.object({ reference: z.string().optional() }).parse(params)
|
|
292
|
+
const reference = parsed.reference?.trim()
|
|
293
|
+
if (!reference) {
|
|
294
|
+
const references = listSkillReferences()
|
|
295
|
+
return {
|
|
296
|
+
name: 'suemo',
|
|
297
|
+
content: readSkillDoc(),
|
|
298
|
+
references,
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const resolved = readSkillReference(reference)
|
|
302
|
+
if (!resolved) {
|
|
303
|
+
throw new Error(`Unknown skill reference: ${reference}`)
|
|
304
|
+
}
|
|
305
|
+
return { name: resolved.name, content: resolved.content }
|
|
306
|
+
}
|
|
307
|
+
|
|
208
308
|
case 'session_context_get': {
|
|
209
309
|
const parsed = z.object({ sessionId: z.string() }).parse(params)
|
|
210
310
|
return getSessionContext(db, parsed.sessionId)
|
package/src/mcp/server.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/mcp/server.ts
|
|
2
2
|
import { Elysia } from 'elysia'
|
|
3
|
-
import { resolveSyncConfig, type SuemoConfig } from '../config.ts'
|
|
3
|
+
import { inferProjectScope, resolveSyncConfig, type SuemoConfig } from '../config.ts'
|
|
4
4
|
import { connect, disconnect } from '../db/client.ts'
|
|
5
5
|
import { checkCompatibility, requireCompatibility } from '../db/preflight.ts'
|
|
6
6
|
import { runSchema } from '../db/schema.ts'
|
|
@@ -37,6 +37,7 @@ function createAutoSyncRunner(
|
|
|
37
37
|
let running = false
|
|
38
38
|
let queued = false
|
|
39
39
|
let lastRunAt = 0
|
|
40
|
+
let lastAttemptAt = 0
|
|
40
41
|
|
|
41
42
|
const runAutoSync = async ({ reason, force = false }: AutoSyncOptions): Promise<void> => {
|
|
42
43
|
if (!resolvedSync.auto.enabled) return
|
|
@@ -44,7 +45,8 @@ function createAutoSyncRunner(
|
|
|
44
45
|
|
|
45
46
|
const now = Date.now()
|
|
46
47
|
const minIntervalMs = resolvedSync.auto.minWriteIntervalSeconds * 1000
|
|
47
|
-
|
|
48
|
+
const throttleFrom = Math.max(lastRunAt, lastAttemptAt)
|
|
49
|
+
if (!force && reason === 'write' && throttleFrom > 0 && now - throttleFrom < minIntervalMs) {
|
|
48
50
|
return
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -60,6 +62,7 @@ function createAutoSyncRunner(
|
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
running = true
|
|
65
|
+
lastAttemptAt = Date.now()
|
|
63
66
|
try {
|
|
64
67
|
const result = await syncTo(db, target, {
|
|
65
68
|
direction: resolvedSync.auto.direction,
|
|
@@ -78,9 +81,11 @@ function createAutoSyncRunner(
|
|
|
78
81
|
running = false
|
|
79
82
|
if (queued) {
|
|
80
83
|
queued = false
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
const nowAfterRun = Date.now()
|
|
85
|
+
const minDelay = Math.max(0, minIntervalMs - (nowAfterRun - Math.max(lastRunAt, lastAttemptAt)))
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
void runAutoSync({ reason: 'write' })
|
|
88
|
+
}, minDelay)
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
}
|
|
@@ -116,6 +121,11 @@ function createAutoSyncRunner(
|
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
export async function startMcpServer(config: SuemoConfig): Promise<void> {
|
|
124
|
+
const inferredScope = inferProjectScope(process.cwd(), config)
|
|
125
|
+
log.info('Resolved default project scope', {
|
|
126
|
+
scope: inferredScope,
|
|
127
|
+
projectDir: config.main?.projectDir ?? '.ua',
|
|
128
|
+
})
|
|
119
129
|
const db = await connect(config.surreal)
|
|
120
130
|
await requireCompatibility(db)
|
|
121
131
|
await runSchema(db)
|
|
@@ -144,6 +154,11 @@ export async function startMcpServer(config: SuemoConfig): Promise<void> {
|
|
|
144
154
|
}
|
|
145
155
|
|
|
146
156
|
export async function startMcpStdioServer(config: SuemoConfig): Promise<void> {
|
|
157
|
+
const inferredScope = inferProjectScope(process.cwd(), config)
|
|
158
|
+
log.info('Resolved default project scope', {
|
|
159
|
+
scope: inferredScope,
|
|
160
|
+
projectDir: config.main?.projectDir ?? '.ua',
|
|
161
|
+
})
|
|
147
162
|
const db = await connect(config.surreal)
|
|
148
163
|
const autoSync = createAutoSyncRunner(db, config)
|
|
149
164
|
autoSync.start()
|
|
@@ -153,12 +168,8 @@ export async function startMcpStdioServer(config: SuemoConfig): Promise<void> {
|
|
|
153
168
|
context: 'mcp:stdio-startup',
|
|
154
169
|
})
|
|
155
170
|
if (!compat.ok) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
console.error(` ✗ ${err}`)
|
|
159
|
-
}
|
|
160
|
-
console.error('\nFix the issues above and retry.\n')
|
|
161
|
-
process.exit(1)
|
|
171
|
+
const details = compat.errors.map((err) => ` ✗ ${err}`).join('\n')
|
|
172
|
+
throw new Error(`\n[suemo] Compatibility check failed:\n\n${details}\n\nFix the issues above and retry.\n`)
|
|
162
173
|
}
|
|
163
174
|
await runSchema(db)
|
|
164
175
|
await runStdioServer(db, config, { onMutation: autoSync.onWrite })
|
package/src/mcp/stdio.ts
CHANGED
|
@@ -121,6 +121,17 @@ const MCP_TOOLS: McpToolDefinition[] = [
|
|
|
121
121
|
},
|
|
122
122
|
},
|
|
123
123
|
},
|
|
124
|
+
{
|
|
125
|
+
name: 'context',
|
|
126
|
+
description: 'Recover recent session context by scope',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
scope: { type: 'string' },
|
|
131
|
+
limit: { type: 'number' },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
124
135
|
{
|
|
125
136
|
name: 'episode_start',
|
|
126
137
|
description: 'Start an episode for a session ID',
|
|
@@ -138,6 +149,10 @@ const MCP_TOOLS: McpToolDefinition[] = [
|
|
|
138
149
|
properties: {
|
|
139
150
|
sessionId: { type: 'string' },
|
|
140
151
|
summary: { type: 'string' },
|
|
152
|
+
goal: { type: 'string' },
|
|
153
|
+
discoveries: { type: 'array', items: { type: 'string' } },
|
|
154
|
+
accomplished: { type: 'array', items: { type: 'string' } },
|
|
155
|
+
files_changed: { type: 'array', items: { type: 'string' } },
|
|
141
156
|
},
|
|
142
157
|
required: ['sessionId'],
|
|
143
158
|
},
|
|
@@ -195,7 +210,10 @@ const MCP_TOOLS: McpToolDefinition[] = [
|
|
|
195
210
|
description: 'Trigger memory consolidation',
|
|
196
211
|
inputSchema: {
|
|
197
212
|
type: 'object',
|
|
198
|
-
properties: {
|
|
213
|
+
properties: {
|
|
214
|
+
nremOnly: { type: 'boolean' },
|
|
215
|
+
scope: { type: 'string' },
|
|
216
|
+
},
|
|
199
217
|
},
|
|
200
218
|
},
|
|
201
219
|
{
|
|
@@ -216,6 +234,44 @@ const MCP_TOOLS: McpToolDefinition[] = [
|
|
|
216
234
|
required: ['topicKey', 'content'],
|
|
217
235
|
},
|
|
218
236
|
},
|
|
237
|
+
{
|
|
238
|
+
name: 'update',
|
|
239
|
+
description: 'Update an existing memory node by ID',
|
|
240
|
+
inputSchema: {
|
|
241
|
+
type: 'object',
|
|
242
|
+
properties: {
|
|
243
|
+
nodeId: { type: 'string' },
|
|
244
|
+
content: { type: 'string' },
|
|
245
|
+
kind: { type: 'string', enum: ['observation', 'belief', 'question', 'hypothesis', 'goal'] },
|
|
246
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
247
|
+
scope: { type: ['string', 'null'] },
|
|
248
|
+
source: { type: ['string', 'null'] },
|
|
249
|
+
confidence: { type: 'number' },
|
|
250
|
+
},
|
|
251
|
+
required: ['nodeId'],
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: 'suggest_topic_key',
|
|
256
|
+
description: 'Suggest a deterministic canonical topic key from free text',
|
|
257
|
+
inputSchema: {
|
|
258
|
+
type: 'object',
|
|
259
|
+
properties: {
|
|
260
|
+
input: { type: 'string' },
|
|
261
|
+
},
|
|
262
|
+
required: ['input'],
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: 'skill',
|
|
267
|
+
description: 'Get suemo skill doc or a specific versioned reference',
|
|
268
|
+
inputSchema: {
|
|
269
|
+
type: 'object',
|
|
270
|
+
properties: {
|
|
271
|
+
reference: { type: 'string' },
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
},
|
|
219
275
|
{
|
|
220
276
|
name: 'capture_prompt',
|
|
221
277
|
description: 'Capture a raw prompt and link derived observations',
|
package/src/memory/episode.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Surreal } from 'surrealdb'
|
|
2
2
|
import { incrementWriteStats } from '../cognitive/health.ts'
|
|
3
3
|
import { getLogger } from '../logger.ts'
|
|
4
|
-
import type
|
|
4
|
+
import { type Episode, type EpisodeSummaryFields, EpisodeSummaryFieldsSchema } from '../types.ts'
|
|
5
5
|
|
|
6
6
|
const log = getLogger(['suemo', 'memory', 'episode'])
|
|
7
7
|
|
|
@@ -17,6 +17,10 @@ export async function episodeStart(
|
|
|
17
17
|
ended_at: NONE,
|
|
18
18
|
summary: NONE,
|
|
19
19
|
context: NONE,
|
|
20
|
+
goal: NONE,
|
|
21
|
+
discoveries: NONE,
|
|
22
|
+
accomplished: NONE,
|
|
23
|
+
files_changed: NONE,
|
|
20
24
|
memory_ids: []
|
|
21
25
|
}
|
|
22
26
|
`,
|
|
@@ -30,18 +34,42 @@ export async function episodeEnd(
|
|
|
30
34
|
db: Surreal,
|
|
31
35
|
sessionId: string,
|
|
32
36
|
summary?: string,
|
|
37
|
+
structured?: EpisodeSummaryFields,
|
|
33
38
|
): Promise<Episode> {
|
|
34
39
|
log.info('episodeEnd()', { sessionId })
|
|
40
|
+
const fields = structured ? EpisodeSummaryFieldsSchema.parse(structured) : undefined
|
|
41
|
+
const summaryText = summary ?? fields?.summary
|
|
35
42
|
const result = await db.query<[Episode[]]>(
|
|
36
43
|
`
|
|
37
44
|
UPDATE episode
|
|
38
45
|
SET ended_at = time::now(),
|
|
39
|
-
|
|
46
|
+
updated_at = time::now(),
|
|
47
|
+
summary = $summary,
|
|
48
|
+
goal = $goal,
|
|
49
|
+
discoveries = $discoveries,
|
|
50
|
+
accomplished = $accomplished,
|
|
51
|
+
files_changed = $filesChanged,
|
|
52
|
+
context = $context
|
|
40
53
|
WHERE session_id = $sessionId
|
|
41
54
|
AND ended_at = NONE
|
|
42
55
|
RETURN AFTER
|
|
43
56
|
`,
|
|
44
|
-
{
|
|
57
|
+
{
|
|
58
|
+
sessionId,
|
|
59
|
+
summary: summaryText ?? null,
|
|
60
|
+
goal: fields?.goal ?? null,
|
|
61
|
+
discoveries: fields?.discoveries ?? null,
|
|
62
|
+
accomplished: fields?.accomplished ?? null,
|
|
63
|
+
filesChanged: fields?.files_changed ?? null,
|
|
64
|
+
context: fields
|
|
65
|
+
? {
|
|
66
|
+
...(fields.goal !== undefined ? { goal: fields.goal } : {}),
|
|
67
|
+
...(fields.discoveries !== undefined ? { discoveries: fields.discoveries } : {}),
|
|
68
|
+
...(fields.accomplished !== undefined ? { accomplished: fields.accomplished } : {}),
|
|
69
|
+
...(fields.files_changed !== undefined ? { files_changed: fields.files_changed } : {}),
|
|
70
|
+
}
|
|
71
|
+
: null,
|
|
72
|
+
},
|
|
45
73
|
)
|
|
46
74
|
const episode = result[0]?.[0]
|
|
47
75
|
if (!episode) throw new Error(`No open episode found for session: ${sessionId}`)
|
|
@@ -63,6 +91,7 @@ export async function attachToEpisode(
|
|
|
63
91
|
`
|
|
64
92
|
UPDATE episode
|
|
65
93
|
SET memory_ids += [$memoryId]
|
|
94
|
+
, updated_at = time::now()
|
|
66
95
|
WHERE session_id = $sessionId AND ended_at = NONE
|
|
67
96
|
`,
|
|
68
97
|
{ sessionId, memoryId },
|
|
@@ -124,7 +153,8 @@ export async function setSessionContext(
|
|
|
124
153
|
`
|
|
125
154
|
UPDATE episode
|
|
126
155
|
SET summary = $summary,
|
|
127
|
-
context = $context
|
|
156
|
+
context = $context,
|
|
157
|
+
updated_at = time::now()
|
|
128
158
|
WHERE session_id = $sessionId AND ended_at = NONE
|
|
129
159
|
RETURN AFTER
|
|
130
160
|
`,
|
|
@@ -139,7 +169,8 @@ export async function setSessionContext(
|
|
|
139
169
|
const result = await db.query<[Episode[]]>(
|
|
140
170
|
`
|
|
141
171
|
UPDATE episode
|
|
142
|
-
SET summary = $summary
|
|
172
|
+
SET summary = $summary,
|
|
173
|
+
updated_at = time::now()
|
|
143
174
|
WHERE session_id = $sessionId AND ended_at = NONE
|
|
144
175
|
RETURN AFTER
|
|
145
176
|
`,
|
|
@@ -153,7 +184,8 @@ export async function setSessionContext(
|
|
|
153
184
|
const result = await db.query<[Episode[]]>(
|
|
154
185
|
`
|
|
155
186
|
UPDATE episode
|
|
156
|
-
SET context = $context
|
|
187
|
+
SET context = $context,
|
|
188
|
+
updated_at = time::now()
|
|
157
189
|
WHERE session_id = $sessionId AND ended_at = NONE
|
|
158
190
|
RETURN AFTER
|
|
159
191
|
`,
|