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
|
@@ -3,7 +3,7 @@ import { healthReport, suemoStats, vitals } from '../../cognitive/health.ts'
|
|
|
3
3
|
import { loadConfig } from '../../config.ts'
|
|
4
4
|
import { connect, disconnect } from '../../db/client.ts'
|
|
5
5
|
import { getLogger } from '../../logger.ts'
|
|
6
|
-
import { app, initCliCommand } from '../shared.ts'
|
|
6
|
+
import { app, initCliCommand, printCliJson, resolveOutputModeOrExit } from '../shared.ts'
|
|
7
7
|
|
|
8
8
|
const log = getLogger(['suemo', 'cli', 'health'])
|
|
9
9
|
|
|
@@ -12,15 +12,34 @@ const health = app.sub('health')
|
|
|
12
12
|
|
|
13
13
|
const reportCmd = health.sub('report')
|
|
14
14
|
.meta({ description: 'Full health report (default)' })
|
|
15
|
+
.flags({
|
|
16
|
+
json: { type: 'boolean', description: 'Output full JSON' },
|
|
17
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
18
|
+
})
|
|
15
19
|
.run(async ({ flags }) => {
|
|
16
|
-
|
|
20
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
21
|
+
await initCliCommand('health report', {
|
|
22
|
+
debug: flags.debug,
|
|
23
|
+
config: flags.config,
|
|
24
|
+
json: outputMode === 'json',
|
|
25
|
+
quiet: flags.quiet,
|
|
26
|
+
})
|
|
17
27
|
log.debug('Running health report command')
|
|
18
28
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
19
29
|
let db: Surreal | undefined
|
|
20
30
|
try {
|
|
21
31
|
db = await connect(config.surreal)
|
|
22
32
|
const report = await healthReport(db)
|
|
23
|
-
|
|
33
|
+
if (outputMode === 'json') {
|
|
34
|
+
printCliJson(report, flags)
|
|
35
|
+
} else {
|
|
36
|
+
console.log(
|
|
37
|
+
`nodes: total=${report.nodes.total} active=${report.nodes.active} consolidated=${report.nodes.consolidated}`,
|
|
38
|
+
)
|
|
39
|
+
console.log(`relations: ${report.relations}`)
|
|
40
|
+
console.log(`goals_active: ${report.goals_active} fsrs_due: ${report.fsrs_due}`)
|
|
41
|
+
console.log(`surreal: ${report.version_check.surreal_version} surrealkv=${report.version_check.surrealkv}`)
|
|
42
|
+
}
|
|
24
43
|
} finally {
|
|
25
44
|
if (db) await disconnect()
|
|
26
45
|
}
|
|
@@ -28,15 +47,30 @@ const reportCmd = health.sub('report')
|
|
|
28
47
|
|
|
29
48
|
const vitalsCmd = health.sub('vitals')
|
|
30
49
|
.meta({ description: 'Last 10 consolidation runs + node counts' })
|
|
50
|
+
.flags({
|
|
51
|
+
json: { type: 'boolean', description: 'Output full JSON' },
|
|
52
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
53
|
+
})
|
|
31
54
|
.run(async ({ flags }) => {
|
|
32
|
-
|
|
55
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
56
|
+
await initCliCommand('health vitals', {
|
|
57
|
+
debug: flags.debug,
|
|
58
|
+
config: flags.config,
|
|
59
|
+
json: outputMode === 'json',
|
|
60
|
+
quiet: flags.quiet,
|
|
61
|
+
})
|
|
33
62
|
log.debug('Running health vitals command')
|
|
34
63
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
35
64
|
let db: Surreal | undefined
|
|
36
65
|
try {
|
|
37
66
|
db = await connect(config.surreal)
|
|
38
67
|
const v = await vitals(db)
|
|
39
|
-
|
|
68
|
+
if (outputMode === 'json') {
|
|
69
|
+
printCliJson(v, flags)
|
|
70
|
+
} else {
|
|
71
|
+
console.log(`last10Runs: ${v.last10Runs.length}`)
|
|
72
|
+
console.log(`kinds: ${Object.keys(v.nodesByKind).length} scopes: ${Object.keys(v.nodesByScope).length}`)
|
|
73
|
+
}
|
|
40
74
|
} finally {
|
|
41
75
|
if (db) await disconnect()
|
|
42
76
|
}
|
|
@@ -44,15 +78,30 @@ const vitalsCmd = health.sub('vitals')
|
|
|
44
78
|
|
|
45
79
|
const statsCmd = health.sub('stats')
|
|
46
80
|
.meta({ description: 'Lightweight usage stats' })
|
|
81
|
+
.flags({
|
|
82
|
+
json: { type: 'boolean', description: 'Output full JSON' },
|
|
83
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
84
|
+
})
|
|
47
85
|
.run(async ({ flags }) => {
|
|
48
|
-
|
|
86
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
87
|
+
await initCliCommand('health stats', {
|
|
88
|
+
debug: flags.debug,
|
|
89
|
+
config: flags.config,
|
|
90
|
+
json: outputMode === 'json',
|
|
91
|
+
quiet: flags.quiet,
|
|
92
|
+
})
|
|
49
93
|
log.debug('Running health stats command')
|
|
50
94
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
51
95
|
let db: Surreal | undefined
|
|
52
96
|
try {
|
|
53
97
|
db = await connect(config.surreal)
|
|
54
98
|
const s = await suemoStats(db)
|
|
55
|
-
|
|
99
|
+
if (outputMode === 'json') {
|
|
100
|
+
printCliJson(s, flags)
|
|
101
|
+
} else {
|
|
102
|
+
console.log(`nodes: total=${s.totalNodes} active=${s.activeNodes} relations=${s.relations}`)
|
|
103
|
+
console.log(`writes=${s.totalWrites} queries=${s.totalQueries}`)
|
|
104
|
+
}
|
|
56
105
|
} finally {
|
|
57
106
|
if (db) await disconnect()
|
|
58
107
|
}
|
|
@@ -63,15 +112,34 @@ export const healthCmd = health
|
|
|
63
112
|
.command(vitalsCmd)
|
|
64
113
|
.command(statsCmd)
|
|
65
114
|
// Default: run the report when just `suemo health` is called
|
|
115
|
+
.flags({
|
|
116
|
+
json: { type: 'boolean', description: 'Output full JSON' },
|
|
117
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
118
|
+
})
|
|
66
119
|
.run(async ({ flags }) => {
|
|
67
|
-
|
|
120
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
121
|
+
await initCliCommand('health', {
|
|
122
|
+
debug: flags.debug,
|
|
123
|
+
config: flags.config,
|
|
124
|
+
json: outputMode === 'json',
|
|
125
|
+
quiet: flags.quiet,
|
|
126
|
+
})
|
|
68
127
|
log.debug('Running default health command')
|
|
69
128
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
70
129
|
let db: Surreal | undefined
|
|
71
130
|
try {
|
|
72
131
|
db = await connect(config.surreal)
|
|
73
132
|
const report = await healthReport(db)
|
|
74
|
-
|
|
133
|
+
if (outputMode === 'json') {
|
|
134
|
+
printCliJson(report, flags)
|
|
135
|
+
} else {
|
|
136
|
+
console.log(
|
|
137
|
+
`nodes: total=${report.nodes.total} active=${report.nodes.active} consolidated=${report.nodes.consolidated}`,
|
|
138
|
+
)
|
|
139
|
+
console.log(`relations: ${report.relations}`)
|
|
140
|
+
console.log(`goals_active: ${report.goals_active} fsrs_due: ${report.fsrs_due}`)
|
|
141
|
+
console.log(`surreal: ${report.version_check.surreal_version} surrealkv=${report.version_check.surrealkv}`)
|
|
142
|
+
}
|
|
75
143
|
} finally {
|
|
76
144
|
if (db) await disconnect()
|
|
77
145
|
}
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { confirm } from '@crustjs/prompts'
|
|
2
2
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import { dirname, join, resolve as resolvePath } from 'node:path'
|
|
4
|
+
import packageJson from '../../../package.json' with { type: 'json' }
|
|
4
5
|
import { loadConfig } from '../../config.ts'
|
|
5
6
|
import { connect, disconnect } from '../../db/client.ts'
|
|
6
7
|
import { checkCompatibility } from '../../db/preflight.ts'
|
|
7
8
|
import { runSchema } from '../../db/schema.ts'
|
|
8
9
|
import { getLogger } from '../../logger.ts'
|
|
9
|
-
import { app, initCliCommand } from '../shared.ts'
|
|
10
|
+
import { app, initCliCommand, resolveOutputModeOrExit } from '../shared.ts'
|
|
10
11
|
|
|
11
12
|
import template from '../../config.template.ts' with { type: 'text' }
|
|
12
13
|
|
|
@@ -15,6 +16,8 @@ interface InitFlags {
|
|
|
15
16
|
config?: string | undefined
|
|
16
17
|
force?: boolean | undefined
|
|
17
18
|
yes?: boolean | undefined
|
|
19
|
+
json?: boolean | undefined
|
|
20
|
+
quiet?: boolean | undefined
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
const log = getLogger(['suemo', 'cli', 'init'])
|
|
@@ -118,9 +121,18 @@ const initConfigCmd = init.sub('config')
|
|
|
118
121
|
.meta({ description: 'Create/update ~/.suemo/suemo.ts config template' })
|
|
119
122
|
.flags({
|
|
120
123
|
force: { type: 'boolean', description: 'Overwrite existing config file' },
|
|
124
|
+
json: { type: 'boolean', description: 'Machine-readable output mode' },
|
|
125
|
+
'verbose-json': { type: 'boolean', description: 'Include embeddings in JSON output (implies --json)' },
|
|
126
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
121
127
|
})
|
|
122
128
|
.run(async ({ flags }) => {
|
|
123
|
-
|
|
129
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
130
|
+
await initCliCommand('init config', {
|
|
131
|
+
debug: flags.debug,
|
|
132
|
+
config: flags.config,
|
|
133
|
+
json: outputMode === 'json',
|
|
134
|
+
quiet: flags.quiet,
|
|
135
|
+
})
|
|
124
136
|
writeConfig(Boolean(flags.force), configOutputPath(flags))
|
|
125
137
|
})
|
|
126
138
|
|
|
@@ -128,9 +140,18 @@ const initSchemaCmd = init.sub('schema')
|
|
|
128
140
|
.meta({ description: 'Apply database schema using current config' })
|
|
129
141
|
.flags({
|
|
130
142
|
yes: { type: 'boolean', short: 'y', description: 'Skip schema confirmation prompt' },
|
|
143
|
+
json: { type: 'boolean', description: 'Machine-readable output mode' },
|
|
144
|
+
'verbose-json': { type: 'boolean', description: 'Include embeddings in JSON output (implies --json)' },
|
|
145
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
131
146
|
})
|
|
132
147
|
.run(async ({ flags }) => {
|
|
133
|
-
|
|
148
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
149
|
+
await initCliCommand('init schema', {
|
|
150
|
+
debug: flags.debug,
|
|
151
|
+
config: flags.config,
|
|
152
|
+
json: outputMode === 'json',
|
|
153
|
+
quiet: flags.quiet,
|
|
154
|
+
})
|
|
134
155
|
|
|
135
156
|
const confirmOptions: {
|
|
136
157
|
message: string
|
|
@@ -158,13 +179,89 @@ const initSchemaCmd = init.sub('schema')
|
|
|
158
179
|
if (!ok) process.exitCode = 1
|
|
159
180
|
})
|
|
160
181
|
|
|
182
|
+
const initOpenCodeCmd = init.sub('opencode')
|
|
183
|
+
.meta({ description: 'Print OpenCode MCP + AGENTS.md setup snippets' })
|
|
184
|
+
.flags({
|
|
185
|
+
json: { type: 'boolean', description: 'Machine-readable output mode' },
|
|
186
|
+
'verbose-json': { type: 'boolean', description: 'Include embeddings in JSON output (implies --json)' },
|
|
187
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
188
|
+
})
|
|
189
|
+
.run(async ({ flags }) => {
|
|
190
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
191
|
+
await initCliCommand('init opencode', {
|
|
192
|
+
debug: flags.debug,
|
|
193
|
+
config: flags.config,
|
|
194
|
+
json: outputMode === 'json',
|
|
195
|
+
quiet: flags.quiet,
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
const configPath = flags.config?.trim() || '~/.suemo/suemo.ts'
|
|
199
|
+
const npmVersion = packageJson.version ?? '0.0.0'
|
|
200
|
+
|
|
201
|
+
if (outputMode === 'json') {
|
|
202
|
+
console.log(JSON.stringify(
|
|
203
|
+
{
|
|
204
|
+
opencode: {
|
|
205
|
+
mcp: {
|
|
206
|
+
suemo: {
|
|
207
|
+
command: 'suemo',
|
|
208
|
+
args: ['serve', '--stdio', '--config', configPath],
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
agentsSnippetPath: 'data/AGENTS.md',
|
|
212
|
+
},
|
|
213
|
+
suemoVersion: npmVersion,
|
|
214
|
+
},
|
|
215
|
+
null,
|
|
216
|
+
2,
|
|
217
|
+
))
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
console.log('OpenCode MCP snippet (add to your OpenCode MCP config):')
|
|
222
|
+
console.log('{')
|
|
223
|
+
console.log(' "mcpServers": {')
|
|
224
|
+
console.log(' "suemo": {')
|
|
225
|
+
console.log(' "command": "suemo",')
|
|
226
|
+
console.log(` "args": ["serve", "--stdio", "--config", "${configPath}"]`)
|
|
227
|
+
console.log(' }')
|
|
228
|
+
console.log(' }')
|
|
229
|
+
console.log('}')
|
|
230
|
+
console.log('\nMinimal AGENTS.md snippet:')
|
|
231
|
+
console.log('```md')
|
|
232
|
+
console.log('Before starting significant work:')
|
|
233
|
+
console.log('- query("what do I know about <topic>")')
|
|
234
|
+
console.log('- suemo skill (or suemo skill core-workflow) when you need latest workflow/docs')
|
|
235
|
+
console.log('During work:')
|
|
236
|
+
console.log('- observe("...") / believe("...")')
|
|
237
|
+
console.log('After completing work:')
|
|
238
|
+
console.log('- episode_end(session_id, summary="...")')
|
|
239
|
+
console.log('```')
|
|
240
|
+
console.log('\nAGENTS.md guidance source: data/AGENTS.md')
|
|
241
|
+
console.log(`Installed suemo version: ${npmVersion}`)
|
|
242
|
+
})
|
|
243
|
+
|
|
161
244
|
export const initCmd = init
|
|
162
245
|
.command(initConfigCmd)
|
|
163
246
|
.command(initSchemaCmd)
|
|
247
|
+
.command(initOpenCodeCmd)
|
|
248
|
+
.flags({
|
|
249
|
+
json: { type: 'boolean', description: 'Machine-readable output mode' },
|
|
250
|
+
'verbose-json': { type: 'boolean', description: 'Include embeddings in JSON output (implies --json)' },
|
|
251
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
252
|
+
})
|
|
164
253
|
.run(async ({ flags }) => {
|
|
165
|
-
|
|
254
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
255
|
+
await initCliCommand('init', {
|
|
256
|
+
debug: flags.debug,
|
|
257
|
+
config: flags.config,
|
|
258
|
+
json: outputMode === 'json',
|
|
259
|
+
quiet: flags.quiet,
|
|
260
|
+
})
|
|
166
261
|
console.log('Use one of:')
|
|
167
262
|
console.log(' suemo init config [--force]')
|
|
168
263
|
console.log(' suemo init schema [--yes]')
|
|
264
|
+
console.log(' suemo init opencode')
|
|
265
|
+
console.log(' suemo skill [reference]')
|
|
169
266
|
console.log('\nRun `suemo init --help` for full details.')
|
|
170
267
|
})
|
|
@@ -3,7 +3,7 @@ import { connect, disconnect } from '../../db/client.ts'
|
|
|
3
3
|
import { getLogger } from '../../logger.ts'
|
|
4
4
|
import { observe } from '../../memory/write.ts'
|
|
5
5
|
import { MemoryKindSchema } from '../../types.ts'
|
|
6
|
-
import { app, initCliCommand } from '../shared.ts'
|
|
6
|
+
import { app, initCliCommand, printCliJson, resolveOutputModeOrExit, resolveScopeLabel } from '../shared.ts'
|
|
7
7
|
|
|
8
8
|
const log = getLogger(['suemo', 'cli', 'observe'])
|
|
9
9
|
|
|
@@ -17,9 +17,17 @@ export const observeCmd = app.sub('observe')
|
|
|
17
17
|
confidence: { type: 'number', description: 'Confidence 0.0–1.0', default: 1.0 },
|
|
18
18
|
source: { type: 'string', description: 'Source label' },
|
|
19
19
|
session: { type: 'string', description: 'Session ID (attach to open episode)' },
|
|
20
|
+
json: { type: 'boolean', description: 'Output JSON result' },
|
|
21
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
20
22
|
})
|
|
21
23
|
.run(async ({ args, flags }) => {
|
|
22
|
-
|
|
24
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
25
|
+
await initCliCommand('observe', {
|
|
26
|
+
debug: flags.debug,
|
|
27
|
+
config: flags.config,
|
|
28
|
+
json: outputMode === 'json',
|
|
29
|
+
quiet: flags.quiet,
|
|
30
|
+
})
|
|
23
31
|
|
|
24
32
|
const kindParse = MemoryKindSchema.safeParse(flags.kind)
|
|
25
33
|
if (!kindParse.success) {
|
|
@@ -28,11 +36,14 @@ export const observeCmd = app.sub('observe')
|
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
39
|
+
const resolvedScope = resolveScopeLabel(flags.scope, config)
|
|
40
|
+
log.debug('Resolved observe scope', { scope: resolvedScope, explicit: flags.scope ?? null })
|
|
31
41
|
const db = await connect(config.surreal)
|
|
32
42
|
try {
|
|
33
43
|
log.debug('Running observe command', {
|
|
34
44
|
kind: kindParse.data,
|
|
35
|
-
hasScope: Boolean(
|
|
45
|
+
hasScope: Boolean(resolvedScope),
|
|
46
|
+
scope: resolvedScope,
|
|
36
47
|
tagCount: flags.tags ? flags.tags.split(',').filter(Boolean).length : 0,
|
|
37
48
|
confidence: flags.confidence,
|
|
38
49
|
contentLength: args.content.length,
|
|
@@ -41,12 +52,17 @@ export const observeCmd = app.sub('observe')
|
|
|
41
52
|
content: args.content,
|
|
42
53
|
kind: kindParse.data,
|
|
43
54
|
tags: flags.tags ? flags.tags.split(',').map((t) => t.trim()) : [],
|
|
44
|
-
scope:
|
|
55
|
+
scope: resolvedScope,
|
|
45
56
|
confidence: flags.confidence,
|
|
46
57
|
source: flags.source,
|
|
47
58
|
sessionId: flags.session,
|
|
48
59
|
}, config)
|
|
49
|
-
|
|
60
|
+
if (outputMode === 'json') {
|
|
61
|
+
printCliJson({ id: node.id, kind: node.kind, valid_from: node.valid_from }, flags)
|
|
62
|
+
} else {
|
|
63
|
+
console.log(`Stored ${node.kind} memory: ${node.id}`)
|
|
64
|
+
console.log(` valid_from: ${node.valid_from}`)
|
|
65
|
+
}
|
|
50
66
|
} finally {
|
|
51
67
|
await disconnect()
|
|
52
68
|
}
|
|
@@ -3,7 +3,7 @@ import { connect, disconnect } from '../../db/client.ts'
|
|
|
3
3
|
import { getLogger } from '../../logger.ts'
|
|
4
4
|
import { query } from '../../memory/read.ts'
|
|
5
5
|
import type { MemoryNode } from '../../types.ts'
|
|
6
|
-
import { app, initCliCommand } from '../shared.ts'
|
|
6
|
+
import { app, initCliCommand, printCliJson, resolveOutputModeOrExit, resolveScopeLabel } from '../shared.ts'
|
|
7
7
|
|
|
8
8
|
const log = getLogger(['suemo', 'cli', 'query'])
|
|
9
9
|
|
|
@@ -13,30 +13,44 @@ export const queryCmd = app.sub('query')
|
|
|
13
13
|
.flags({
|
|
14
14
|
scope: { type: 'string', short: 's', description: 'Filter by scope' },
|
|
15
15
|
top: { type: 'number', short: 'n', description: 'Number of results', default: 5 },
|
|
16
|
-
json: { type: 'boolean',
|
|
16
|
+
json: { type: 'boolean', description: 'Output full JSON nodes' },
|
|
17
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
17
18
|
})
|
|
18
19
|
.run(async ({ args, flags }) => {
|
|
19
|
-
|
|
20
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
21
|
+
await initCliCommand('query', {
|
|
22
|
+
debug: flags.debug,
|
|
23
|
+
config: flags.config,
|
|
24
|
+
json: outputMode === 'json',
|
|
25
|
+
quiet: flags.quiet,
|
|
26
|
+
})
|
|
27
|
+
const config = await loadConfig(process.cwd(), flags.config)
|
|
28
|
+
const resolvedScope = resolveScopeLabel(flags.scope, config)
|
|
29
|
+
log.debug('Resolved query scope', { scope: resolvedScope, explicit: flags.scope ?? null })
|
|
20
30
|
log.debug('Running query command', {
|
|
21
|
-
hasScope: Boolean(
|
|
31
|
+
hasScope: Boolean(resolvedScope),
|
|
32
|
+
scope: resolvedScope,
|
|
22
33
|
top: flags.top,
|
|
23
|
-
|
|
34
|
+
outputMode,
|
|
24
35
|
inputLength: args.input.length,
|
|
25
36
|
})
|
|
26
37
|
const nodes: MemoryNode[] = await (async () => {
|
|
27
38
|
let connected = false
|
|
28
39
|
try {
|
|
29
|
-
const config = await loadConfig(process.cwd(), flags.config)
|
|
30
40
|
const db = await connect(config.surreal)
|
|
31
41
|
connected = true
|
|
32
|
-
return await query(db, { input: args.input, scope:
|
|
42
|
+
return await query(db, { input: args.input, scope: resolvedScope, topK: flags.top }, config)
|
|
33
43
|
} finally {
|
|
34
44
|
if (connected) await disconnect()
|
|
35
45
|
}
|
|
36
46
|
})()
|
|
37
|
-
if (
|
|
38
|
-
|
|
47
|
+
if (outputMode === 'json') {
|
|
48
|
+
printCliJson(nodes, flags)
|
|
39
49
|
} else {
|
|
50
|
+
if (nodes.length === 0) {
|
|
51
|
+
console.log('No memories matched this query.')
|
|
52
|
+
return
|
|
53
|
+
}
|
|
40
54
|
for (const n of nodes) {
|
|
41
55
|
console.log(`[${n.kind}] ${n.id}`)
|
|
42
56
|
console.log(` ${n.content.slice(0, 120)}`)
|
|
@@ -2,21 +2,37 @@ import { loadConfig } from '../../config.ts'
|
|
|
2
2
|
import { connect, disconnect } from '../../db/client.ts'
|
|
3
3
|
import { getLogger } from '../../logger.ts'
|
|
4
4
|
import { recall } from '../../memory/read.ts'
|
|
5
|
-
import { app, initCliCommand } from '../shared.ts'
|
|
5
|
+
import { app, initCliCommand, printCliJson, resolveOutputModeOrExit } from '../shared.ts'
|
|
6
6
|
|
|
7
7
|
const log = getLogger(['suemo', 'cli', 'recall'])
|
|
8
8
|
|
|
9
9
|
export const recallCmd = app.sub('recall')
|
|
10
10
|
.meta({ description: 'Fetch a single node + its neighbours (ticks FSRS)' })
|
|
11
11
|
.args([{ name: 'nodeId', type: 'string', required: true }])
|
|
12
|
+
.flags({
|
|
13
|
+
json: { type: 'boolean', description: 'Output full JSON' },
|
|
14
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
15
|
+
})
|
|
12
16
|
.run(async ({ args, flags }) => {
|
|
13
|
-
|
|
17
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
18
|
+
await initCliCommand('recall', {
|
|
19
|
+
debug: flags.debug,
|
|
20
|
+
config: flags.config,
|
|
21
|
+
json: outputMode === 'json',
|
|
22
|
+
quiet: flags.quiet,
|
|
23
|
+
})
|
|
14
24
|
log.debug('Running recall command', { nodeId: args.nodeId })
|
|
15
25
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
16
26
|
const db = await connect(config.surreal)
|
|
17
27
|
try {
|
|
18
28
|
const result = await recall(db, args.nodeId)
|
|
19
|
-
|
|
29
|
+
if (outputMode === 'json') {
|
|
30
|
+
printCliJson(result, flags)
|
|
31
|
+
} else {
|
|
32
|
+
console.log(`[${result.node.kind}] ${result.node.id}`)
|
|
33
|
+
console.log(` ${result.node.content.slice(0, 120)}`)
|
|
34
|
+
console.log(` neighbors: ${result.neighbors.length}`)
|
|
35
|
+
}
|
|
20
36
|
} finally {
|
|
21
37
|
await disconnect()
|
|
22
38
|
}
|
|
@@ -1,14 +1,45 @@
|
|
|
1
|
-
import { loadConfig, resolveSyncConfig } from '../../config.ts'
|
|
1
|
+
import { inferProjectScope, loadConfig, resolveSyncConfig } from '../../config.ts'
|
|
2
2
|
import { getLogger } from '../../logger.ts'
|
|
3
3
|
import { startMcpServer, startMcpStdioServer } from '../../mcp/server.ts'
|
|
4
|
-
import { app, initCliCommand } from '../shared.ts'
|
|
4
|
+
import { app, initCliCommand, resolveOutputModeOrExit } from '../shared.ts'
|
|
5
5
|
|
|
6
6
|
const log = getLogger(['suemo', 'cli', 'serve'])
|
|
7
7
|
|
|
8
8
|
function printDevRestartBanner(): void {
|
|
9
9
|
if (process.env.SUEMO_DEV_WATCH !== '1') return
|
|
10
10
|
const now = new Date().toISOString()
|
|
11
|
-
console.
|
|
11
|
+
console.error(`\n[suemo:dev] MCP restarted (${now})\n`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isProcessAlive(pid: number): boolean {
|
|
15
|
+
if (!Number.isInteger(pid) || pid <= 1) return false
|
|
16
|
+
try {
|
|
17
|
+
process.kill(pid, 0)
|
|
18
|
+
return true
|
|
19
|
+
} catch {
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function startParentWatchdog(stdioMode: boolean): void {
|
|
25
|
+
const explicitParentPid = Number(process.env.SUEMO_PARENT_PID ?? process.env.SUEMO_DEV_PARENT_PID)
|
|
26
|
+
const inferredParentPid = process.ppid
|
|
27
|
+
const parentPid = Number.isInteger(explicitParentPid) && explicitParentPid > 1
|
|
28
|
+
? explicitParentPid
|
|
29
|
+
: (stdioMode ? inferredParentPid : NaN)
|
|
30
|
+
|
|
31
|
+
if (!Number.isInteger(parentPid) || parentPid <= 1 || parentPid === process.pid) return
|
|
32
|
+
|
|
33
|
+
const rawInterval = Number(process.env.SUEMO_PARENT_WATCH_INTERVAL_MS ?? '2000')
|
|
34
|
+
const intervalMs = Number.isFinite(rawInterval) && rawInterval >= 500 ? rawInterval : 2000
|
|
35
|
+
|
|
36
|
+
const timer = setInterval(() => {
|
|
37
|
+
if (!isProcessAlive(parentPid)) {
|
|
38
|
+
log.warning('Parent process not found; shutting down stdio server', { parentPid })
|
|
39
|
+
process.exit(0)
|
|
40
|
+
}
|
|
41
|
+
}, intervalMs)
|
|
42
|
+
timer.unref()
|
|
12
43
|
}
|
|
13
44
|
|
|
14
45
|
async function runServeDevMode(): Promise<never> {
|
|
@@ -24,7 +55,12 @@ async function runServeDevMode(): Promise<never> {
|
|
|
24
55
|
|
|
25
56
|
const child = Bun.spawn({
|
|
26
57
|
cmd,
|
|
27
|
-
env: {
|
|
58
|
+
env: {
|
|
59
|
+
...process.env,
|
|
60
|
+
SUEMO_DEV_WATCH: '1',
|
|
61
|
+
SUEMO_PARENT_PID: String(process.pid),
|
|
62
|
+
SUEMO_DEV_PARENT_PID: String(process.pid),
|
|
63
|
+
},
|
|
28
64
|
stdin: 'inherit',
|
|
29
65
|
stdout: 'inherit',
|
|
30
66
|
stderr: 'inherit',
|
|
@@ -45,6 +81,9 @@ async function runServeDevMode(): Promise<never> {
|
|
|
45
81
|
process.once('SIGTERM', () => {
|
|
46
82
|
shutdown('SIGTERM')
|
|
47
83
|
})
|
|
84
|
+
process.once('SIGHUP', () => {
|
|
85
|
+
shutdown('SIGHUP')
|
|
86
|
+
})
|
|
48
87
|
|
|
49
88
|
const exitCode = await child.exited
|
|
50
89
|
process.exit(exitCode)
|
|
@@ -61,6 +100,55 @@ async function maybeDelayDevStartup(): Promise<void> {
|
|
|
61
100
|
}
|
|
62
101
|
}
|
|
63
102
|
|
|
103
|
+
function parseDevRetryDelayMs(attempt: number): number {
|
|
104
|
+
const base = Number(process.env.SUEMO_DEV_RETRY_BASE_MS ?? '1000')
|
|
105
|
+
const cap = Number(process.env.SUEMO_DEV_RETRY_MAX_MS ?? '30000')
|
|
106
|
+
const safeBase = Number.isFinite(base) && base > 0 ? base : 1000
|
|
107
|
+
const safeCap = Number.isFinite(cap) && cap > 0 ? cap : 30000
|
|
108
|
+
return Math.min(safeCap, safeBase * (2 ** attempt))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function runServerWithDevRetry(opts: {
|
|
112
|
+
stdio: boolean
|
|
113
|
+
config: Awaited<ReturnType<typeof loadConfig>>
|
|
114
|
+
}): Promise<void> {
|
|
115
|
+
const isDevChild = process.env.SUEMO_DEV_WATCH === '1'
|
|
116
|
+
if (!isDevChild) {
|
|
117
|
+
if (opts.stdio) {
|
|
118
|
+
await startMcpStdioServer(opts.config)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
await startMcpServer(opts.config)
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let attempt = 0
|
|
126
|
+
while (true) {
|
|
127
|
+
try {
|
|
128
|
+
if (opts.stdio) {
|
|
129
|
+
await startMcpStdioServer(opts.config)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
await startMcpServer(opts.config)
|
|
133
|
+
return
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error instanceof Error && /compatibility check failed/i.test(error.message)) {
|
|
136
|
+
console.error(error.message)
|
|
137
|
+
} else {
|
|
138
|
+
console.error(String(error))
|
|
139
|
+
}
|
|
140
|
+
const delayMs = parseDevRetryDelayMs(attempt)
|
|
141
|
+
attempt += 1
|
|
142
|
+
log.warning('Dev server start failed; retrying with backoff', {
|
|
143
|
+
attempt,
|
|
144
|
+
delayMs,
|
|
145
|
+
error: String(error),
|
|
146
|
+
})
|
|
147
|
+
await Bun.sleep(delayMs)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
64
152
|
export const serveCmd = app.sub('serve')
|
|
65
153
|
.meta({ description: 'Start the MCP server (HTTP or stdio)' })
|
|
66
154
|
.flags({
|
|
@@ -68,15 +156,30 @@ export const serveCmd = app.sub('serve')
|
|
|
68
156
|
host: { type: 'string', description: 'Host to bind to (overrides config)' },
|
|
69
157
|
stdio: { type: 'boolean', description: 'Use stdio transport instead of HTTP' },
|
|
70
158
|
dev: { type: 'boolean', description: 'Restart MCP server on code changes (bun --watch)' },
|
|
159
|
+
json: { type: 'boolean', description: 'Machine-readable startup logs only' },
|
|
160
|
+
'verbose-json': { type: 'boolean', description: 'Include embeddings in JSON output (implies --json)' },
|
|
161
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
71
162
|
})
|
|
72
163
|
.run(async ({ flags }) => {
|
|
164
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
165
|
+
startParentWatchdog(Boolean(flags.stdio))
|
|
73
166
|
printDevRestartBanner()
|
|
74
167
|
await maybeDelayDevStartup()
|
|
75
|
-
await initCliCommand('serve', {
|
|
168
|
+
await initCliCommand('serve', {
|
|
169
|
+
debug: flags.debug,
|
|
170
|
+
config: flags.config,
|
|
171
|
+
json: outputMode === 'json',
|
|
172
|
+
quiet: flags.quiet,
|
|
173
|
+
})
|
|
76
174
|
if (flags.dev) {
|
|
77
175
|
await runServeDevMode()
|
|
78
176
|
}
|
|
79
177
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
178
|
+
const inferredScope = inferProjectScope(process.cwd(), config)
|
|
179
|
+
log.info('Resolved default project scope', {
|
|
180
|
+
scope: inferredScope,
|
|
181
|
+
projectDir: config.main?.projectDir ?? '.ua',
|
|
182
|
+
})
|
|
80
183
|
const sync = resolveSyncConfig(config)
|
|
81
184
|
if (sync?.auto.enabled) {
|
|
82
185
|
log.info('Auto-sync enabled', {
|
|
@@ -89,12 +192,12 @@ export const serveCmd = app.sub('serve')
|
|
|
89
192
|
}
|
|
90
193
|
if (flags.stdio) {
|
|
91
194
|
log.debug('Starting MCP stdio transport')
|
|
92
|
-
await
|
|
195
|
+
await runServerWithDevRetry({ stdio: true, config })
|
|
93
196
|
return
|
|
94
197
|
}
|
|
95
198
|
if (flags.port) config.mcp.port = flags.port
|
|
96
199
|
if (flags.host) config.mcp.host = flags.host
|
|
97
200
|
log.debug('Starting MCP HTTP transport', { host: config.mcp.host, port: config.mcp.port })
|
|
98
|
-
await
|
|
201
|
+
await runServerWithDevRetry({ stdio: false, config })
|
|
99
202
|
// Server runs indefinitely — no disconnect
|
|
100
203
|
})
|