suemo 0.0.4 → 0.0.5
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/package.json +1 -1
- package/src/cli/commands/believe.ts +19 -5
- package/src/cli/commands/consolidate.ts +18 -3
- package/src/cli/commands/doctor.ts +25 -3
- package/src/cli/commands/export-import.ts +26 -4
- package/src/cli/commands/goal.ts +54 -11
- package/src/cli/commands/health.ts +77 -9
- package/src/cli/commands/init.ts +35 -4
- package/src/cli/commands/observe.ts +16 -3
- package/src/cli/commands/query.ts +17 -6
- package/src/cli/commands/recall.ts +19 -3
- package/src/cli/commands/serve.ts +104 -6
- package/src/cli/commands/sync.ts +22 -3
- package/src/cli/commands/timeline.ts +17 -6
- package/src/cli/commands/wander.ts +13 -6
- package/src/cli/shared.ts +78 -2
- package/src/cognitive/health.ts +3 -3
- package/src/db/client.ts +3 -3
- package/src/db/preflight.ts +2 -6
- package/src/logger.ts +23 -1
- package/src/mcp/server.ts +11 -10
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ import { loadConfig } from '../../config.ts'
|
|
|
2
2
|
import { connect, disconnect } from '../../db/client.ts'
|
|
3
3
|
import { getLogger } from '../../logger.ts'
|
|
4
4
|
import { believe } from '../../memory/write.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', 'believe'])
|
|
8
8
|
|
|
@@ -12,9 +12,17 @@ export const believeCmd = app.sub('believe')
|
|
|
12
12
|
.flags({
|
|
13
13
|
scope: { type: 'string', short: 's', description: 'Scope label' },
|
|
14
14
|
confidence: { type: 'number', description: 'Confidence 0.0–1.0', default: 1.0 },
|
|
15
|
+
json: { type: 'boolean', description: 'Output JSON result' },
|
|
16
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
15
17
|
})
|
|
16
18
|
.run(async ({ args, flags }) => {
|
|
17
|
-
|
|
19
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
20
|
+
await initCliCommand('believe', {
|
|
21
|
+
debug: flags.debug,
|
|
22
|
+
config: flags.config,
|
|
23
|
+
json: outputMode === 'json',
|
|
24
|
+
quiet: flags.quiet,
|
|
25
|
+
})
|
|
18
26
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
19
27
|
const db = await connect(config.surreal)
|
|
20
28
|
try {
|
|
@@ -28,9 +36,15 @@ export const believeCmd = app.sub('believe')
|
|
|
28
36
|
scope: flags.scope,
|
|
29
37
|
confidence: flags.confidence,
|
|
30
38
|
}, config)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
if (outputMode === 'json') {
|
|
40
|
+
const out: Record<string, unknown> = { id: node.id, valid_from: node.valid_from }
|
|
41
|
+
if (contradicted) out.contradicted = contradicted.id
|
|
42
|
+
printCliJson(out, flags)
|
|
43
|
+
} else {
|
|
44
|
+
console.log(`Stored belief memory: ${node.id}`)
|
|
45
|
+
console.log(` valid_from: ${node.valid_from}`)
|
|
46
|
+
if (contradicted) console.log(` contradicted: ${contradicted.id}`)
|
|
47
|
+
}
|
|
34
48
|
} finally {
|
|
35
49
|
await disconnect()
|
|
36
50
|
}
|
|
@@ -2,7 +2,7 @@ import { consolidate } from '../../cognitive/consolidate.ts'
|
|
|
2
2
|
import { loadConfig } from '../../config.ts'
|
|
3
3
|
import { connect, disconnect } from '../../db/client.ts'
|
|
4
4
|
import { getLogger } from '../../logger.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', 'consolidate'])
|
|
8
8
|
|
|
@@ -10,9 +10,17 @@ export const consolidateCmd = app.sub('consolidate')
|
|
|
10
10
|
.meta({ description: 'Manually trigger memory consolidation (NREM + REM)' })
|
|
11
11
|
.flags({
|
|
12
12
|
'nrem-only': { type: 'boolean', description: 'Run only NREM (compression) phase', default: false },
|
|
13
|
+
json: { type: 'boolean', description: 'Output full JSON' },
|
|
14
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
13
15
|
})
|
|
14
16
|
.run(async ({ flags }) => {
|
|
15
|
-
|
|
17
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
18
|
+
await initCliCommand('consolidate', {
|
|
19
|
+
debug: flags.debug,
|
|
20
|
+
config: flags.config,
|
|
21
|
+
json: outputMode === 'json',
|
|
22
|
+
quiet: flags.quiet,
|
|
23
|
+
})
|
|
16
24
|
log.debug('Running consolidate command', { nremOnly: flags['nrem-only'] })
|
|
17
25
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
18
26
|
const db = await connect(config.surreal)
|
|
@@ -24,7 +32,14 @@ export const consolidateCmd = app.sub('consolidate')
|
|
|
24
32
|
llm: config.consolidation.llm,
|
|
25
33
|
embedding: config.embedding,
|
|
26
34
|
})
|
|
27
|
-
|
|
35
|
+
if (outputMode === 'json') {
|
|
36
|
+
printCliJson(run, flags)
|
|
37
|
+
} else {
|
|
38
|
+
console.log(
|
|
39
|
+
`Consolidation ${run.status}: phase=${run.phase} nodes_in=${run.nodes_in} nodes_out=${run.nodes_out}`,
|
|
40
|
+
)
|
|
41
|
+
if (run.error) console.log(` error: ${run.error}`)
|
|
42
|
+
}
|
|
28
43
|
} finally {
|
|
29
44
|
await disconnect()
|
|
30
45
|
}
|
|
@@ -3,7 +3,7 @@ import { loadConfig } from '../../config.ts'
|
|
|
3
3
|
import { connect, disconnect } from '../../db/client.ts'
|
|
4
4
|
import { checkCompatibility } from '../../db/preflight.ts'
|
|
5
5
|
import { getLogger } from '../../logger.ts'
|
|
6
|
-
import { app, initCliCommand } from '../shared.ts'
|
|
6
|
+
import { app, initCliCommand, resolveOutputModeOrExit } from '../shared.ts'
|
|
7
7
|
|
|
8
8
|
const log = getLogger(['suemo', 'cli', 'doctor'])
|
|
9
9
|
|
|
@@ -50,8 +50,19 @@ async function detectModelNames(db: Surreal): Promise<string[]> {
|
|
|
50
50
|
|
|
51
51
|
const doctorEmbedCmd = doctor.sub('embed')
|
|
52
52
|
.meta({ description: 'Diagnose fn::embed() and show setup steps' })
|
|
53
|
+
.flags({
|
|
54
|
+
json: { type: 'boolean', description: 'Machine-readable output mode' },
|
|
55
|
+
'verbose-json': { type: 'boolean', description: 'Include embeddings in JSON output (implies --json)' },
|
|
56
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
57
|
+
})
|
|
53
58
|
.run(async ({ flags }) => {
|
|
54
|
-
|
|
59
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
60
|
+
await initCliCommand('doctor embed', {
|
|
61
|
+
debug: flags.debug,
|
|
62
|
+
config: flags.config,
|
|
63
|
+
json: outputMode === 'json',
|
|
64
|
+
quiet: flags.quiet,
|
|
65
|
+
})
|
|
55
66
|
|
|
56
67
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
57
68
|
const endpoint = toCliEndpoint(config.surreal.url)
|
|
@@ -115,8 +126,19 @@ const doctorEmbedCmd = doctor.sub('embed')
|
|
|
115
126
|
|
|
116
127
|
export const doctorCmd = doctor
|
|
117
128
|
.command(doctorEmbedCmd)
|
|
129
|
+
.flags({
|
|
130
|
+
json: { type: 'boolean', description: 'Machine-readable output mode' },
|
|
131
|
+
'verbose-json': { type: 'boolean', description: 'Include embeddings in JSON output (implies --json)' },
|
|
132
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
133
|
+
})
|
|
118
134
|
.run(async ({ flags }) => {
|
|
119
|
-
|
|
135
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
136
|
+
await initCliCommand('doctor', {
|
|
137
|
+
debug: flags.debug,
|
|
138
|
+
config: flags.config,
|
|
139
|
+
json: outputMode === 'json',
|
|
140
|
+
quiet: flags.quiet,
|
|
141
|
+
})
|
|
120
142
|
console.log('Use one of:')
|
|
121
143
|
console.log(' suemo doctor embed')
|
|
122
144
|
console.log('\nRun `suemo doctor --help` for full details.')
|
|
@@ -4,7 +4,7 @@ import { loadConfig } from '../../config.ts'
|
|
|
4
4
|
import { connect, disconnect } from '../../db/client.ts'
|
|
5
5
|
import { getLogger } from '../../logger.ts'
|
|
6
6
|
import type { MemoryNode, Relation } from '../../types.ts'
|
|
7
|
-
import { app, initCliCommand } from '../shared.ts'
|
|
7
|
+
import { app, initCliCommand, printCliJson, resolveOutputModeOrExit } from '../shared.ts'
|
|
8
8
|
|
|
9
9
|
const log = getLogger(['suemo', 'cli', 'export-import'])
|
|
10
10
|
|
|
@@ -14,9 +14,17 @@ export const exportCmd = app.sub('export')
|
|
|
14
14
|
.flags({
|
|
15
15
|
scope: { type: 'string', short: 's', description: 'Filter by scope' },
|
|
16
16
|
all: { type: 'boolean', description: 'Include invalidated nodes' },
|
|
17
|
+
json: { type: 'boolean', description: 'Force JSONL-safe mode (stderr logs only)' },
|
|
18
|
+
pretty: { type: 'boolean', description: 'Human-readable output mode (default)' },
|
|
17
19
|
})
|
|
18
20
|
.run(async ({ flags }) => {
|
|
19
|
-
|
|
21
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
22
|
+
await initCliCommand('export', {
|
|
23
|
+
debug: flags.debug,
|
|
24
|
+
config: flags.config,
|
|
25
|
+
json: outputMode === 'json',
|
|
26
|
+
quiet: flags.quiet,
|
|
27
|
+
})
|
|
20
28
|
log.debug('Running export command', {
|
|
21
29
|
hasScope: Boolean(flags.scope),
|
|
22
30
|
includeInvalidated: Boolean(flags.all),
|
|
@@ -52,8 +60,18 @@ export const exportCmd = app.sub('export')
|
|
|
52
60
|
export const importCmd = app.sub('import')
|
|
53
61
|
.meta({ description: 'Import memories from a JSONL file' })
|
|
54
62
|
.args([{ name: 'file', type: 'string', required: true }])
|
|
63
|
+
.flags({
|
|
64
|
+
json: { type: 'boolean', description: 'Output JSON summary' },
|
|
65
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
66
|
+
})
|
|
55
67
|
.run(async ({ args, flags }) => {
|
|
56
|
-
|
|
68
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
69
|
+
await initCliCommand('import', {
|
|
70
|
+
debug: flags.debug,
|
|
71
|
+
config: flags.config,
|
|
72
|
+
json: outputMode === 'json',
|
|
73
|
+
quiet: flags.quiet,
|
|
74
|
+
})
|
|
57
75
|
log.debug('Running import command', { file: args.file })
|
|
58
76
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
59
77
|
const db = await connect(config.surreal)
|
|
@@ -123,5 +141,9 @@ export const importCmd = app.sub('import')
|
|
|
123
141
|
rl.close()
|
|
124
142
|
await disconnect()
|
|
125
143
|
}
|
|
126
|
-
|
|
144
|
+
if (outputMode === 'json') {
|
|
145
|
+
printCliJson({ imported, skipped, errors, lines: lineNum }, flags)
|
|
146
|
+
} else {
|
|
147
|
+
console.log(`import done: imported=${imported} skipped=${skipped} errors=${errors} lines=${lineNum}`)
|
|
148
|
+
}
|
|
127
149
|
})
|
package/src/cli/commands/goal.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { loadConfig } from '../../config.ts'
|
|
|
3
3
|
import { connect, disconnect } from '../../db/client.ts'
|
|
4
4
|
import { goalList, goalResolve, goalSet } from '../../goal.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', 'goal'])
|
|
9
9
|
|
|
@@ -17,9 +17,17 @@ const setCmd = goal.sub('set')
|
|
|
17
17
|
.flags({
|
|
18
18
|
scope: { type: 'string', short: 's', description: 'Scope label' },
|
|
19
19
|
tags: { type: 'string', short: 't', description: 'Comma-separated tags' },
|
|
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('goal set', {
|
|
26
|
+
debug: flags.debug,
|
|
27
|
+
config: flags.config,
|
|
28
|
+
json: outputMode === 'json',
|
|
29
|
+
quiet: flags.quiet,
|
|
30
|
+
})
|
|
23
31
|
log.debug('Running goal set command', {
|
|
24
32
|
hasScope: Boolean(flags.scope),
|
|
25
33
|
tagCount: flags.tags ? flags.tags.split(',').filter(Boolean).length : 0,
|
|
@@ -33,7 +41,12 @@ const setCmd = goal.sub('set')
|
|
|
33
41
|
...(flags.scope ? { scope: flags.scope } : {}),
|
|
34
42
|
tags: flags.tags ? flags.tags.split(',').map((t) => t.trim()) : [],
|
|
35
43
|
})
|
|
36
|
-
|
|
44
|
+
if (outputMode === 'json') {
|
|
45
|
+
printCliJson({ id: node.id, content: node.content }, flags)
|
|
46
|
+
} else {
|
|
47
|
+
console.log(`Created goal: ${node.id}`)
|
|
48
|
+
console.log(` ${node.content}`)
|
|
49
|
+
}
|
|
37
50
|
} finally {
|
|
38
51
|
if (db) await disconnect()
|
|
39
52
|
}
|
|
@@ -45,9 +58,17 @@ const listCmd = goal.sub('list')
|
|
|
45
58
|
.flags({
|
|
46
59
|
scope: { type: 'string', short: 's', description: 'Filter by scope' },
|
|
47
60
|
resolved: { type: 'boolean', description: 'Include resolved goals', default: false },
|
|
61
|
+
json: { type: 'boolean', description: 'Output JSON result' },
|
|
62
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
48
63
|
})
|
|
49
64
|
.run(async ({ flags }) => {
|
|
50
|
-
|
|
65
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
66
|
+
await initCliCommand('goal list', {
|
|
67
|
+
debug: flags.debug,
|
|
68
|
+
config: flags.config,
|
|
69
|
+
json: outputMode === 'json',
|
|
70
|
+
quiet: flags.quiet,
|
|
71
|
+
})
|
|
51
72
|
log.debug('Running goal list command', {
|
|
52
73
|
hasScope: Boolean(flags.scope),
|
|
53
74
|
includeResolved: Boolean(flags.resolved),
|
|
@@ -60,11 +81,19 @@ const listCmd = goal.sub('list')
|
|
|
60
81
|
...(flags.scope ? { scope: flags.scope } : {}),
|
|
61
82
|
includeResolved: flags.resolved,
|
|
62
83
|
})
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
84
|
+
if (outputMode === 'json') {
|
|
85
|
+
printCliJson(goals, flags)
|
|
86
|
+
} else {
|
|
87
|
+
if (goals.length === 0) {
|
|
88
|
+
console.log('No goals found.')
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
for (const g of goals) {
|
|
92
|
+
const status = g.valid_until ? `resolved ${g.valid_until}` : 'active'
|
|
93
|
+
console.log(`[${status}] ${g.id}`)
|
|
94
|
+
console.log(` ${g.content}`)
|
|
95
|
+
console.log()
|
|
96
|
+
}
|
|
68
97
|
}
|
|
69
98
|
} finally {
|
|
70
99
|
if (db) await disconnect()
|
|
@@ -75,15 +104,29 @@ const listCmd = goal.sub('list')
|
|
|
75
104
|
const resolveCmd = goal.sub('resolve')
|
|
76
105
|
.meta({ description: 'Mark a goal as resolved' })
|
|
77
106
|
.args([{ name: 'goalId', type: 'string', required: true }])
|
|
107
|
+
.flags({
|
|
108
|
+
json: { type: 'boolean', description: 'Output JSON result' },
|
|
109
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
110
|
+
})
|
|
78
111
|
.run(async ({ args, flags }) => {
|
|
79
|
-
|
|
112
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
113
|
+
await initCliCommand('goal resolve', {
|
|
114
|
+
debug: flags.debug,
|
|
115
|
+
config: flags.config,
|
|
116
|
+
json: outputMode === 'json',
|
|
117
|
+
quiet: flags.quiet,
|
|
118
|
+
})
|
|
80
119
|
log.debug('Running goal resolve command', { goalId: args.goalId })
|
|
81
120
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
82
121
|
let db: Surreal | undefined
|
|
83
122
|
try {
|
|
84
123
|
db = await connect(config.surreal)
|
|
85
124
|
await goalResolve(db, args.goalId)
|
|
86
|
-
|
|
125
|
+
if (outputMode === 'json') {
|
|
126
|
+
printCliJson({ ok: true, goalId: args.goalId }, flags)
|
|
127
|
+
} else {
|
|
128
|
+
console.log(`✓ Goal ${args.goalId} resolved`)
|
|
129
|
+
}
|
|
87
130
|
} finally {
|
|
88
131
|
if (db) await disconnect()
|
|
89
132
|
}
|
|
@@ -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
|
@@ -6,7 +6,7 @@ import { connect, disconnect } from '../../db/client.ts'
|
|
|
6
6
|
import { checkCompatibility } from '../../db/preflight.ts'
|
|
7
7
|
import { runSchema } from '../../db/schema.ts'
|
|
8
8
|
import { getLogger } from '../../logger.ts'
|
|
9
|
-
import { app, initCliCommand } from '../shared.ts'
|
|
9
|
+
import { app, initCliCommand, resolveOutputModeOrExit } from '../shared.ts'
|
|
10
10
|
|
|
11
11
|
import template from '../../config.template.ts' with { type: 'text' }
|
|
12
12
|
|
|
@@ -15,6 +15,8 @@ interface InitFlags {
|
|
|
15
15
|
config?: string | undefined
|
|
16
16
|
force?: boolean | undefined
|
|
17
17
|
yes?: boolean | undefined
|
|
18
|
+
json?: boolean | undefined
|
|
19
|
+
quiet?: boolean | undefined
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
const log = getLogger(['suemo', 'cli', 'init'])
|
|
@@ -118,9 +120,18 @@ const initConfigCmd = init.sub('config')
|
|
|
118
120
|
.meta({ description: 'Create/update ~/.suemo/suemo.ts config template' })
|
|
119
121
|
.flags({
|
|
120
122
|
force: { type: 'boolean', description: 'Overwrite existing config file' },
|
|
123
|
+
json: { type: 'boolean', description: 'Machine-readable output mode' },
|
|
124
|
+
'verbose-json': { type: 'boolean', description: 'Include embeddings in JSON output (implies --json)' },
|
|
125
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
121
126
|
})
|
|
122
127
|
.run(async ({ flags }) => {
|
|
123
|
-
|
|
128
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
129
|
+
await initCliCommand('init config', {
|
|
130
|
+
debug: flags.debug,
|
|
131
|
+
config: flags.config,
|
|
132
|
+
json: outputMode === 'json',
|
|
133
|
+
quiet: flags.quiet,
|
|
134
|
+
})
|
|
124
135
|
writeConfig(Boolean(flags.force), configOutputPath(flags))
|
|
125
136
|
})
|
|
126
137
|
|
|
@@ -128,9 +139,18 @@ const initSchemaCmd = init.sub('schema')
|
|
|
128
139
|
.meta({ description: 'Apply database schema using current config' })
|
|
129
140
|
.flags({
|
|
130
141
|
yes: { type: 'boolean', short: 'y', description: 'Skip schema confirmation prompt' },
|
|
142
|
+
json: { type: 'boolean', description: 'Machine-readable output mode' },
|
|
143
|
+
'verbose-json': { type: 'boolean', description: 'Include embeddings in JSON output (implies --json)' },
|
|
144
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
131
145
|
})
|
|
132
146
|
.run(async ({ flags }) => {
|
|
133
|
-
|
|
147
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
148
|
+
await initCliCommand('init schema', {
|
|
149
|
+
debug: flags.debug,
|
|
150
|
+
config: flags.config,
|
|
151
|
+
json: outputMode === 'json',
|
|
152
|
+
quiet: flags.quiet,
|
|
153
|
+
})
|
|
134
154
|
|
|
135
155
|
const confirmOptions: {
|
|
136
156
|
message: string
|
|
@@ -161,8 +181,19 @@ const initSchemaCmd = init.sub('schema')
|
|
|
161
181
|
export const initCmd = init
|
|
162
182
|
.command(initConfigCmd)
|
|
163
183
|
.command(initSchemaCmd)
|
|
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
|
+
})
|
|
164
189
|
.run(async ({ flags }) => {
|
|
165
|
-
|
|
190
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
191
|
+
await initCliCommand('init', {
|
|
192
|
+
debug: flags.debug,
|
|
193
|
+
config: flags.config,
|
|
194
|
+
json: outputMode === 'json',
|
|
195
|
+
quiet: flags.quiet,
|
|
196
|
+
})
|
|
166
197
|
console.log('Use one of:')
|
|
167
198
|
console.log(' suemo init config [--force]')
|
|
168
199
|
console.log(' suemo init schema [--yes]')
|
|
@@ -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 } 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) {
|
|
@@ -46,7 +54,12 @@ export const observeCmd = app.sub('observe')
|
|
|
46
54
|
source: flags.source,
|
|
47
55
|
sessionId: flags.session,
|
|
48
56
|
}, config)
|
|
49
|
-
|
|
57
|
+
if (outputMode === 'json') {
|
|
58
|
+
printCliJson({ id: node.id, kind: node.kind, valid_from: node.valid_from }, flags)
|
|
59
|
+
} else {
|
|
60
|
+
console.log(`Stored ${node.kind} memory: ${node.id}`)
|
|
61
|
+
console.log(` valid_from: ${node.valid_from}`)
|
|
62
|
+
}
|
|
50
63
|
} finally {
|
|
51
64
|
await disconnect()
|
|
52
65
|
}
|
|
@@ -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 } from '../shared.ts'
|
|
7
7
|
|
|
8
8
|
const log = getLogger(['suemo', 'cli', 'query'])
|
|
9
9
|
|
|
@@ -13,14 +13,21 @@ 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
|
+
})
|
|
20
27
|
log.debug('Running query command', {
|
|
21
28
|
hasScope: Boolean(flags.scope),
|
|
22
29
|
top: flags.top,
|
|
23
|
-
|
|
30
|
+
outputMode,
|
|
24
31
|
inputLength: args.input.length,
|
|
25
32
|
})
|
|
26
33
|
const nodes: MemoryNode[] = await (async () => {
|
|
@@ -34,9 +41,13 @@ export const queryCmd = app.sub('query')
|
|
|
34
41
|
if (connected) await disconnect()
|
|
35
42
|
}
|
|
36
43
|
})()
|
|
37
|
-
if (
|
|
38
|
-
|
|
44
|
+
if (outputMode === 'json') {
|
|
45
|
+
printCliJson(nodes, flags)
|
|
39
46
|
} else {
|
|
47
|
+
if (nodes.length === 0) {
|
|
48
|
+
console.log('No memories matched this query.')
|
|
49
|
+
return
|
|
50
|
+
}
|
|
40
51
|
for (const n of nodes) {
|
|
41
52
|
console.log(`[${n.kind}] ${n.id}`)
|
|
42
53
|
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
1
|
import { 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,11 +156,21 @@ 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
|
}
|
|
@@ -89,12 +187,12 @@ export const serveCmd = app.sub('serve')
|
|
|
89
187
|
}
|
|
90
188
|
if (flags.stdio) {
|
|
91
189
|
log.debug('Starting MCP stdio transport')
|
|
92
|
-
await
|
|
190
|
+
await runServerWithDevRetry({ stdio: true, config })
|
|
93
191
|
return
|
|
94
192
|
}
|
|
95
193
|
if (flags.port) config.mcp.port = flags.port
|
|
96
194
|
if (flags.host) config.mcp.host = flags.host
|
|
97
195
|
log.debug('Starting MCP HTTP transport', { host: config.mcp.host, port: config.mcp.port })
|
|
98
|
-
await
|
|
196
|
+
await runServerWithDevRetry({ stdio: false, config })
|
|
99
197
|
// Server runs indefinitely — no disconnect
|
|
100
198
|
})
|
package/src/cli/commands/sync.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { loadConfig, resolveSyncConfig, type SurrealTarget } from '../../config.
|
|
|
2
2
|
import { connect, disconnect } from '../../db/client.ts'
|
|
3
3
|
import { getLogger } from '../../logger.ts'
|
|
4
4
|
import { syncTo } from '../../sync.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', 'sync'])
|
|
8
8
|
|
|
@@ -12,9 +12,17 @@ export const syncCmd = app.sub('sync')
|
|
|
12
12
|
'dry-run': { type: 'boolean', description: 'Show what would be pushed without writing', default: false },
|
|
13
13
|
direction: { type: 'string', description: 'Sync direction: push | pull | both', default: 'push' },
|
|
14
14
|
remote: { type: 'string', description: 'Named remote from sync.remotes (defaults to sync.defaultRemote)' },
|
|
15
|
+
json: { type: 'boolean', description: 'Output JSON result' },
|
|
16
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
15
17
|
})
|
|
16
18
|
.run(async ({ flags }) => {
|
|
17
|
-
|
|
19
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
20
|
+
await initCliCommand('sync', {
|
|
21
|
+
debug: flags.debug,
|
|
22
|
+
config: flags.config,
|
|
23
|
+
json: outputMode === 'json',
|
|
24
|
+
quiet: flags.quiet,
|
|
25
|
+
})
|
|
18
26
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
19
27
|
const sync = resolveSyncConfig(config)
|
|
20
28
|
if (!sync) {
|
|
@@ -50,7 +58,18 @@ export const syncCmd = app.sub('sync')
|
|
|
50
58
|
dryRun: flags['dry-run'],
|
|
51
59
|
direction,
|
|
52
60
|
})
|
|
53
|
-
|
|
61
|
+
if (outputMode === 'json') {
|
|
62
|
+
printCliJson(result, flags)
|
|
63
|
+
} else {
|
|
64
|
+
const pushed = result.pushed ?? 0
|
|
65
|
+
const skipped = result.skipped ?? 0
|
|
66
|
+
const errors = result.errors ?? 0
|
|
67
|
+
console.log(
|
|
68
|
+
`sync ${
|
|
69
|
+
errors > 0 ? 'failed' : 'ok'
|
|
70
|
+
}: pushed=${pushed} skipped=${skipped} errors=${errors} cursor=${result.cursor}`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
54
73
|
} finally {
|
|
55
74
|
await disconnect()
|
|
56
75
|
}
|
|
@@ -2,7 +2,7 @@ import { loadConfig } from '../../config.ts'
|
|
|
2
2
|
import { connect, disconnect } from '../../db/client.ts'
|
|
3
3
|
import { getLogger } from '../../logger.ts'
|
|
4
4
|
import { timeline } 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', 'timeline'])
|
|
8
8
|
|
|
@@ -13,16 +13,23 @@ export const timelineCmd = app.sub('timeline')
|
|
|
13
13
|
from: { type: 'string', description: 'Start datetime (ISO)' },
|
|
14
14
|
until: { type: 'string', description: 'End datetime (ISO)' },
|
|
15
15
|
limit: { type: 'number', short: 'n', description: 'Max results', default: 20 },
|
|
16
|
-
json: { type: 'boolean',
|
|
16
|
+
json: { type: 'boolean', description: 'Output full JSON' },
|
|
17
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
17
18
|
})
|
|
18
19
|
.run(async ({ flags }) => {
|
|
19
|
-
|
|
20
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
21
|
+
await initCliCommand('timeline', {
|
|
22
|
+
debug: flags.debug,
|
|
23
|
+
config: flags.config,
|
|
24
|
+
json: outputMode === 'json',
|
|
25
|
+
quiet: flags.quiet,
|
|
26
|
+
})
|
|
20
27
|
log.debug('Running timeline command', {
|
|
21
28
|
hasScope: Boolean(flags.scope),
|
|
22
29
|
from: flags.from ?? null,
|
|
23
30
|
until: flags.until ?? null,
|
|
24
31
|
limit: flags.limit,
|
|
25
|
-
|
|
32
|
+
outputMode,
|
|
26
33
|
})
|
|
27
34
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
28
35
|
const db = await connect(config.surreal)
|
|
@@ -33,9 +40,13 @@ export const timelineCmd = app.sub('timeline')
|
|
|
33
40
|
...(flags.until ? { until: flags.until } : {}),
|
|
34
41
|
...(flags.limit ? { limit: flags.limit } : {}),
|
|
35
42
|
})
|
|
36
|
-
if (
|
|
37
|
-
|
|
43
|
+
if (outputMode === 'json') {
|
|
44
|
+
printCliJson(nodes, flags)
|
|
38
45
|
} else {
|
|
46
|
+
if (nodes.length === 0) {
|
|
47
|
+
console.log('No timeline entries found for this filter.')
|
|
48
|
+
return
|
|
49
|
+
}
|
|
39
50
|
for (const n of nodes) {
|
|
40
51
|
const ts = new Date(n.created_at).toLocaleString()
|
|
41
52
|
console.log(`${ts} [${n.kind}] ${n.id}`)
|
|
@@ -2,7 +2,7 @@ import { loadConfig } from '../../config.ts'
|
|
|
2
2
|
import { connect, disconnect } from '../../db/client.ts'
|
|
3
3
|
import { getLogger } from '../../logger.ts'
|
|
4
4
|
import { wander } 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', 'wander'])
|
|
8
8
|
|
|
@@ -12,15 +12,22 @@ export const wanderCmd = app.sub('wander')
|
|
|
12
12
|
from: { type: 'string', description: 'Anchor node ID (random if omitted)' },
|
|
13
13
|
hops: { type: 'number', description: 'Traversal depth', default: 3 },
|
|
14
14
|
scope: { type: 'string', short: 's', description: 'Filter by scope' },
|
|
15
|
-
json: { type: 'boolean',
|
|
15
|
+
json: { type: 'boolean', description: 'Output full JSON nodes' },
|
|
16
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
16
17
|
})
|
|
17
18
|
.run(async ({ flags }) => {
|
|
18
|
-
|
|
19
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
20
|
+
await initCliCommand('wander', {
|
|
21
|
+
debug: flags.debug,
|
|
22
|
+
config: flags.config,
|
|
23
|
+
json: outputMode === 'json',
|
|
24
|
+
quiet: flags.quiet,
|
|
25
|
+
})
|
|
19
26
|
log.debug('Running wander command', {
|
|
20
27
|
hasAnchor: Boolean(flags.from),
|
|
21
28
|
hops: flags.hops,
|
|
22
29
|
hasScope: Boolean(flags.scope),
|
|
23
|
-
|
|
30
|
+
outputMode,
|
|
24
31
|
})
|
|
25
32
|
const config = await loadConfig(process.cwd(), flags.config)
|
|
26
33
|
const db = await connect(config.surreal)
|
|
@@ -30,8 +37,8 @@ export const wanderCmd = app.sub('wander')
|
|
|
30
37
|
...(flags.hops ? { hops: flags.hops } : {}),
|
|
31
38
|
...(flags.scope ? { scope: flags.scope } : {}),
|
|
32
39
|
})
|
|
33
|
-
if (
|
|
34
|
-
|
|
40
|
+
if (outputMode === 'json') {
|
|
41
|
+
printCliJson(nodes, flags)
|
|
35
42
|
} else {
|
|
36
43
|
if (nodes.length === 0) {
|
|
37
44
|
console.log('No memories found for this wander query.')
|
package/src/cli/shared.ts
CHANGED
|
@@ -6,6 +6,13 @@ const cliLog = getLogger(['suemo', 'cli'])
|
|
|
6
6
|
|
|
7
7
|
const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'off'])
|
|
8
8
|
|
|
9
|
+
export type OutputMode = 'pretty' | 'json'
|
|
10
|
+
export interface OutputModeFlags {
|
|
11
|
+
json?: boolean | undefined
|
|
12
|
+
pretty?: boolean | undefined
|
|
13
|
+
['verbose-json']?: boolean | undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
function isTruthyEnv(name: string): boolean {
|
|
10
17
|
const raw = process.env[name]
|
|
11
18
|
if (raw === undefined) return false
|
|
@@ -18,18 +25,79 @@ export function isDebugEnabled(debugFlag?: boolean): boolean {
|
|
|
18
25
|
return Boolean(debugFlag) || isTruthyEnv('SUEMO_DEBUG')
|
|
19
26
|
}
|
|
20
27
|
|
|
28
|
+
export function isQuietEnabled(quietFlag?: boolean): boolean {
|
|
29
|
+
const envQuiet = isTruthyEnv('SUEMO_QUIET')
|
|
30
|
+
const envLogLevel = (process.env.SUEMO_LOG_LEVEL ?? '').trim().toLowerCase()
|
|
31
|
+
const envLogOff = envLogLevel === 'off' || envLogLevel === 'silent' || envLogLevel === 'none'
|
|
32
|
+
return Boolean(quietFlag) || envQuiet || envLogOff
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isVerboseJsonEnabled(flags: OutputModeFlags): boolean {
|
|
36
|
+
return Boolean(flags['verbose-json'])
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resolveOutputMode(flags: OutputModeFlags): OutputMode {
|
|
40
|
+
const verboseJson = isVerboseJsonEnabled(flags)
|
|
41
|
+
const jsonMode = Boolean(flags.json) || verboseJson
|
|
42
|
+
if (jsonMode && flags.pretty) {
|
|
43
|
+
throw new Error('Cannot use --json and --pretty together')
|
|
44
|
+
}
|
|
45
|
+
return jsonMode ? 'json' : 'pretty'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolveOutputModeOrExit(
|
|
49
|
+
flags: OutputModeFlags,
|
|
50
|
+
): OutputMode {
|
|
51
|
+
try {
|
|
52
|
+
return resolveOutputMode(flags)
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(String(error))
|
|
55
|
+
process.exit(1)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function redactEmbeddings(value: unknown): unknown {
|
|
60
|
+
if (Array.isArray(value)) return value.map((entry) => redactEmbeddings(entry))
|
|
61
|
+
if (!value || typeof value !== 'object') return value
|
|
62
|
+
const src = value as Record<string, unknown>
|
|
63
|
+
const out: Record<string, unknown> = {}
|
|
64
|
+
for (const [key, val] of Object.entries(src)) {
|
|
65
|
+
if (key === 'embedding') continue
|
|
66
|
+
out[key] = redactEmbeddings(val)
|
|
67
|
+
}
|
|
68
|
+
return out
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function printCliJson(payload: unknown, flags: OutputModeFlags): void {
|
|
72
|
+
const verboseJson = isVerboseJsonEnabled(flags)
|
|
73
|
+
const output = verboseJson ? payload : redactEmbeddings(payload)
|
|
74
|
+
console.log(JSON.stringify(output, null, 2))
|
|
75
|
+
}
|
|
76
|
+
|
|
21
77
|
export async function initCliCommand(
|
|
22
78
|
command: string,
|
|
23
|
-
flags: {
|
|
79
|
+
flags: {
|
|
80
|
+
debug?: boolean | undefined
|
|
81
|
+
config?: string | undefined
|
|
82
|
+
json?: boolean | undefined
|
|
83
|
+
quiet?: boolean | undefined
|
|
84
|
+
},
|
|
24
85
|
): Promise<void> {
|
|
25
86
|
const debug = isDebugEnabled(flags.debug)
|
|
26
|
-
|
|
87
|
+
const quiet = isQuietEnabled(flags.quiet)
|
|
88
|
+
const level = quiet ? 'fatal' : (debug ? 'debug' : (flags.json ? 'warning' : 'info'))
|
|
89
|
+
await initLogger({ level, quiet })
|
|
27
90
|
cliLog.debug('CLI command invocation', {
|
|
28
91
|
command,
|
|
29
92
|
cwd: process.cwd(),
|
|
30
93
|
debugFlag: flags.debug ?? false,
|
|
31
94
|
envDebug: process.env.SUEMO_DEBUG ?? null,
|
|
95
|
+
quietFlag: flags.quiet ?? false,
|
|
96
|
+
envQuiet: process.env.SUEMO_QUIET ?? null,
|
|
97
|
+
envLogLevel: process.env.SUEMO_LOG_LEVEL ?? null,
|
|
32
98
|
debugEnabled: debug,
|
|
99
|
+
quietEnabled: quiet,
|
|
100
|
+
jsonOutput: flags.json ?? false,
|
|
33
101
|
configFlag: flags.config ?? null,
|
|
34
102
|
envConfigPath: process.env.SUEMO_CONFIG_PATH ?? null,
|
|
35
103
|
})
|
|
@@ -40,4 +108,12 @@ export const app = new Crust('suemo')
|
|
|
40
108
|
.flags({
|
|
41
109
|
config: { type: 'string', short: 'c', description: 'Path to config file', inherit: true },
|
|
42
110
|
debug: { type: 'boolean', short: 'd', description: 'Verbose debug logging', inherit: true },
|
|
111
|
+
json: { type: 'boolean', short: 'j', description: 'Machine-readable JSON output', inherit: true },
|
|
112
|
+
'verbose-json': {
|
|
113
|
+
type: 'boolean',
|
|
114
|
+
description: 'Include embeddings in JSON output (implies --json)',
|
|
115
|
+
inherit: true,
|
|
116
|
+
},
|
|
117
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)', inherit: true },
|
|
118
|
+
quiet: { type: 'boolean', short: 'q', description: 'Suppress non-fatal logs', inherit: true },
|
|
43
119
|
})
|
package/src/cognitive/health.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { ConsolidationRun, HealthReport, SuemoStats } from '../types.ts'
|
|
|
6
6
|
const log = getLogger(['suemo', 'cognitive', 'health'])
|
|
7
7
|
|
|
8
8
|
export async function healthReport(db: Surreal): Promise<HealthReport> {
|
|
9
|
-
log.
|
|
9
|
+
log.debug('healthReport()')
|
|
10
10
|
|
|
11
11
|
const [
|
|
12
12
|
totalResult,
|
|
@@ -97,7 +97,7 @@ export async function vitals(db: Surreal): Promise<{
|
|
|
97
97
|
nodesByKind: Record<string, number>
|
|
98
98
|
nodesByScope: Record<string, number>
|
|
99
99
|
}> {
|
|
100
|
-
log.
|
|
100
|
+
log.debug('vitals()')
|
|
101
101
|
const [runsResult, byKindResult, byScopeResult] = await Promise.all([
|
|
102
102
|
db.query<[ConsolidationRun[]]>(
|
|
103
103
|
'SELECT * FROM consolidation_run ORDER BY started_at DESC LIMIT 10',
|
|
@@ -123,7 +123,7 @@ export async function vitals(db: Surreal): Promise<{
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
export async function suemoStats(db: Surreal): Promise<SuemoStats> {
|
|
126
|
-
log.
|
|
126
|
+
log.debug('suemoStats()')
|
|
127
127
|
|
|
128
128
|
const [totalR, activeR, kindR, relR, runR, statsR] = await Promise.all([
|
|
129
129
|
db.query<[{ count: number }[]]>('SELECT count() AS count FROM memory GROUP ALL'),
|
package/src/db/client.ts
CHANGED
|
@@ -13,7 +13,7 @@ let _db: Surreal | null = null
|
|
|
13
13
|
export async function connect(target: SurrealTarget): Promise<Surreal> {
|
|
14
14
|
if (_db) return _db
|
|
15
15
|
|
|
16
|
-
log.
|
|
16
|
+
log.debug('Connecting to SurrealDB', {
|
|
17
17
|
url: target.url,
|
|
18
18
|
ns: target.namespace,
|
|
19
19
|
db: target.database,
|
|
@@ -41,7 +41,7 @@ export async function connect(target: SurrealTarget): Promise<Surreal> {
|
|
|
41
41
|
})
|
|
42
42
|
|
|
43
43
|
_db = db
|
|
44
|
-
log.
|
|
44
|
+
log.debug('Connected')
|
|
45
45
|
return db
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -54,6 +54,6 @@ export async function disconnect(): Promise<void> {
|
|
|
54
54
|
if (_db) {
|
|
55
55
|
await _db.close()
|
|
56
56
|
_db = null
|
|
57
|
-
log.
|
|
57
|
+
log.debug('Disconnected')
|
|
58
58
|
}
|
|
59
59
|
}
|
package/src/db/preflight.ts
CHANGED
|
@@ -172,11 +172,7 @@ export async function checkCompatibility(
|
|
|
172
172
|
export async function requireCompatibility(db: Surreal): Promise<void> {
|
|
173
173
|
const result = await checkCompatibility(db)
|
|
174
174
|
if (!result.ok) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
console.error(` ✗ ${err}`)
|
|
178
|
-
}
|
|
179
|
-
console.error('\nFix the issues above and retry.\n')
|
|
180
|
-
process.exit(1)
|
|
175
|
+
const details = result.errors.map((err) => ` ✗ ${err}`).join('\n')
|
|
176
|
+
throw new Error(`\n[suemo] Compatibility check failed:\n\n${details}\n\nFix the issues above and retry.\n`)
|
|
181
177
|
}
|
|
182
178
|
}
|
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/server.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -153,12 +158,8 @@ export async function startMcpStdioServer(config: SuemoConfig): Promise<void> {
|
|
|
153
158
|
context: 'mcp:stdio-startup',
|
|
154
159
|
})
|
|
155
160
|
if (!compat.ok) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
console.error(` ✗ ${err}`)
|
|
159
|
-
}
|
|
160
|
-
console.error('\nFix the issues above and retry.\n')
|
|
161
|
-
process.exit(1)
|
|
161
|
+
const details = compat.errors.map((err) => ` ✗ ${err}`).join('\n')
|
|
162
|
+
throw new Error(`\n[suemo] Compatibility check failed:\n\n${details}\n\nFix the issues above and retry.\n`)
|
|
162
163
|
}
|
|
163
164
|
await runSchema(db)
|
|
164
165
|
await runStdioServer(db, config, { onMutation: autoSync.onWrite })
|