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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suemo",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Persistent semantic memory for AI agents — backed by SurrealDB.",
5
5
  "author": {
6
6
  "name": "Umar Alfarouk",
@@ -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
- await initCliCommand('believe', { debug: flags.debug, config: flags.config })
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
- const out: Record<string, unknown> = { id: node.id, valid_from: node.valid_from }
32
- if (contradicted) out.contradicted = contradicted.id
33
- console.log(JSON.stringify(out, null, 2))
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
- await initCliCommand('consolidate', { debug: flags.debug, config: flags.config })
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
- console.log(JSON.stringify(run, null, 2))
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
- await initCliCommand('doctor embed', { debug: flags.debug, config: flags.config })
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
- await initCliCommand('doctor', { debug: flags.debug, config: flags.config })
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
- await initCliCommand('export', { debug: flags.debug, config: flags.config })
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
- await initCliCommand('import', { debug: flags.debug, config: flags.config })
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
- console.log(JSON.stringify({ imported, skipped, errors, lines: lineNum }, null, 2))
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
  })
@@ -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
- await initCliCommand('goal set', { debug: flags.debug, config: flags.config })
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
- console.log(JSON.stringify({ id: node.id, content: node.content }, null, 2))
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
- await initCliCommand('goal list', { debug: flags.debug, config: flags.config })
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
- for (const g of goals) {
64
- const status = g.valid_until ? `resolved ${g.valid_until}` : 'active'
65
- console.log(`[${status}] ${g.id}`)
66
- console.log(` ${g.content}`)
67
- console.log()
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
- await initCliCommand('goal resolve', { debug: flags.debug, config: flags.config })
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
- console.log(`✓ Goal ${args.goalId} resolved`)
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
- await initCliCommand('health report', { debug: flags.debug, config: flags.config })
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
- console.log(JSON.stringify(report, null, 2))
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
- await initCliCommand('health vitals', { debug: flags.debug, config: flags.config })
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
- console.log(JSON.stringify(v, null, 2))
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
- await initCliCommand('health stats', { debug: flags.debug, config: flags.config })
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
- console.log(JSON.stringify(s, null, 2))
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
- await initCliCommand('health', { debug: flags.debug, config: flags.config })
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
- console.log(JSON.stringify(report, null, 2))
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
  }
@@ -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
- await initCliCommand('init config', { debug: flags.debug, config: flags.config })
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
- await initCliCommand('init schema', { debug: flags.debug, config: flags.config })
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
- await initCliCommand('init', { debug: flags.debug, config: flags.config })
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
- await initCliCommand('observe', { debug: flags.debug, config: flags.config })
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
- console.log(JSON.stringify({ id: node.id, kind: node.kind, valid_from: node.valid_from }, null, 2))
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', short: 'j', description: 'Output full JSON nodes' },
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
- await initCliCommand('query', { debug: flags.debug, config: flags.config })
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
- json: Boolean(flags.json),
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 (flags.json) {
38
- console.log(JSON.stringify(nodes, null, 2))
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
- await initCliCommand('recall', { debug: flags.debug, config: flags.config })
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
- console.log(JSON.stringify(result, null, 2))
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.log(`\n[suemo:dev] MCP restarted (${now})\n`)
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: { ...process.env, SUEMO_DEV_WATCH: '1' },
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', { debug: flags.debug, config: flags.config })
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 startMcpStdioServer(config)
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 startMcpServer(config)
196
+ await runServerWithDevRetry({ stdio: false, config })
99
197
  // Server runs indefinitely — no disconnect
100
198
  })
@@ -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
- await initCliCommand('sync', { debug: flags.debug, config: flags.config })
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
- console.log(JSON.stringify(result, null, 2))
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', short: 'j', description: 'Output full JSON' },
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
- await initCliCommand('timeline', { debug: flags.debug, config: flags.config })
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
- json: Boolean(flags.json),
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 (flags.json) {
37
- console.log(JSON.stringify(nodes, null, 2))
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', short: 'j', description: 'Output full JSON nodes' },
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
- await initCliCommand('wander', { debug: flags.debug, config: flags.config })
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
- json: Boolean(flags.json),
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 (flags.json) {
34
- console.log(JSON.stringify(nodes, null, 2))
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: { debug?: boolean | undefined; config?: string | undefined },
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
- await initLogger({ level: debug ? 'debug' : 'info' })
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
  })
@@ -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.info('healthReport()')
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.info('vitals()')
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.info('suemoStats()')
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.info('Connecting to SurrealDB', {
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.info('Connected')
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.info('Disconnected')
57
+ log.debug('Disconnected')
58
58
  }
59
59
  }
@@ -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
- console.error('\n[suemo] Compatibility check failed:\n')
176
- for (const err of result.errors) {
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: Object.keys(sinks).filter(item => item !== 'json'),
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
- if (!force && reason === 'write' && lastRunAt > 0 && now - lastRunAt < minIntervalMs) {
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
- queueMicrotask(() => {
82
- void runAutoSync({ reason: 'write', force: true })
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
- console.error('\n[suemo] Compatibility check failed:\n')
157
- for (const err of compat.errors) {
158
- console.error(` ✗ ${err}`)
159
- }
160
- console.error('\nFix the issues above and retry.\n')
161
- process.exit(1)
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 })