suemo 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  }
@@ -1,12 +1,13 @@
1
1
  import { confirm } from '@crustjs/prompts'
2
2
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
3
3
  import { dirname, join, resolve as resolvePath } from 'node:path'
4
+ import packageJson from '../../../package.json' with { type: 'json' }
4
5
  import { loadConfig } from '../../config.ts'
5
6
  import { connect, disconnect } from '../../db/client.ts'
6
7
  import { checkCompatibility } from '../../db/preflight.ts'
7
8
  import { runSchema } from '../../db/schema.ts'
8
9
  import { getLogger } from '../../logger.ts'
9
- import { app, initCliCommand } from '../shared.ts'
10
+ import { app, initCliCommand, resolveOutputModeOrExit } from '../shared.ts'
10
11
 
11
12
  import template from '../../config.template.ts' with { type: 'text' }
12
13
 
@@ -15,6 +16,8 @@ interface InitFlags {
15
16
  config?: string | undefined
16
17
  force?: boolean | undefined
17
18
  yes?: boolean | undefined
19
+ json?: boolean | undefined
20
+ quiet?: boolean | undefined
18
21
  }
19
22
 
20
23
  const log = getLogger(['suemo', 'cli', 'init'])
@@ -118,9 +121,18 @@ const initConfigCmd = init.sub('config')
118
121
  .meta({ description: 'Create/update ~/.suemo/suemo.ts config template' })
119
122
  .flags({
120
123
  force: { type: 'boolean', description: 'Overwrite existing config file' },
124
+ json: { type: 'boolean', description: 'Machine-readable output mode' },
125
+ 'verbose-json': { type: 'boolean', description: 'Include embeddings in JSON output (implies --json)' },
126
+ pretty: { type: 'boolean', description: 'Human-readable output (default)' },
121
127
  })
122
128
  .run(async ({ flags }) => {
123
- await initCliCommand('init config', { debug: flags.debug, config: flags.config })
129
+ const outputMode = resolveOutputModeOrExit(flags)
130
+ await initCliCommand('init config', {
131
+ debug: flags.debug,
132
+ config: flags.config,
133
+ json: outputMode === 'json',
134
+ quiet: flags.quiet,
135
+ })
124
136
  writeConfig(Boolean(flags.force), configOutputPath(flags))
125
137
  })
126
138
 
@@ -128,9 +140,18 @@ const initSchemaCmd = init.sub('schema')
128
140
  .meta({ description: 'Apply database schema using current config' })
129
141
  .flags({
130
142
  yes: { type: 'boolean', short: 'y', description: 'Skip schema confirmation prompt' },
143
+ json: { type: 'boolean', description: 'Machine-readable output mode' },
144
+ 'verbose-json': { type: 'boolean', description: 'Include embeddings in JSON output (implies --json)' },
145
+ pretty: { type: 'boolean', description: 'Human-readable output (default)' },
131
146
  })
132
147
  .run(async ({ flags }) => {
133
- await initCliCommand('init schema', { debug: flags.debug, config: flags.config })
148
+ const outputMode = resolveOutputModeOrExit(flags)
149
+ await initCliCommand('init schema', {
150
+ debug: flags.debug,
151
+ config: flags.config,
152
+ json: outputMode === 'json',
153
+ quiet: flags.quiet,
154
+ })
134
155
 
135
156
  const confirmOptions: {
136
157
  message: string
@@ -158,13 +179,89 @@ const initSchemaCmd = init.sub('schema')
158
179
  if (!ok) process.exitCode = 1
159
180
  })
160
181
 
182
+ const initOpenCodeCmd = init.sub('opencode')
183
+ .meta({ description: 'Print OpenCode MCP + AGENTS.md setup snippets' })
184
+ .flags({
185
+ json: { type: 'boolean', description: 'Machine-readable output mode' },
186
+ 'verbose-json': { type: 'boolean', description: 'Include embeddings in JSON output (implies --json)' },
187
+ pretty: { type: 'boolean', description: 'Human-readable output (default)' },
188
+ })
189
+ .run(async ({ flags }) => {
190
+ const outputMode = resolveOutputModeOrExit(flags)
191
+ await initCliCommand('init opencode', {
192
+ debug: flags.debug,
193
+ config: flags.config,
194
+ json: outputMode === 'json',
195
+ quiet: flags.quiet,
196
+ })
197
+
198
+ const configPath = flags.config?.trim() || '~/.suemo/suemo.ts'
199
+ const npmVersion = packageJson.version ?? '0.0.0'
200
+
201
+ if (outputMode === 'json') {
202
+ console.log(JSON.stringify(
203
+ {
204
+ opencode: {
205
+ mcp: {
206
+ suemo: {
207
+ command: 'suemo',
208
+ args: ['serve', '--stdio', '--config', configPath],
209
+ },
210
+ },
211
+ agentsSnippetPath: 'data/AGENTS.md',
212
+ },
213
+ suemoVersion: npmVersion,
214
+ },
215
+ null,
216
+ 2,
217
+ ))
218
+ return
219
+ }
220
+
221
+ console.log('OpenCode MCP snippet (add to your OpenCode MCP config):')
222
+ console.log('{')
223
+ console.log(' "mcpServers": {')
224
+ console.log(' "suemo": {')
225
+ console.log(' "command": "suemo",')
226
+ console.log(` "args": ["serve", "--stdio", "--config", "${configPath}"]`)
227
+ console.log(' }')
228
+ console.log(' }')
229
+ console.log('}')
230
+ console.log('\nMinimal AGENTS.md snippet:')
231
+ console.log('```md')
232
+ console.log('Before starting significant work:')
233
+ console.log('- query("what do I know about <topic>")')
234
+ console.log('- suemo skill (or suemo skill core-workflow) when you need latest workflow/docs')
235
+ console.log('During work:')
236
+ console.log('- observe("...") / believe("...")')
237
+ console.log('After completing work:')
238
+ console.log('- episode_end(session_id, summary="...")')
239
+ console.log('```')
240
+ console.log('\nAGENTS.md guidance source: data/AGENTS.md')
241
+ console.log(`Installed suemo version: ${npmVersion}`)
242
+ })
243
+
161
244
  export const initCmd = init
162
245
  .command(initConfigCmd)
163
246
  .command(initSchemaCmd)
247
+ .command(initOpenCodeCmd)
248
+ .flags({
249
+ json: { type: 'boolean', description: 'Machine-readable output mode' },
250
+ 'verbose-json': { type: 'boolean', description: 'Include embeddings in JSON output (implies --json)' },
251
+ pretty: { type: 'boolean', description: 'Human-readable output (default)' },
252
+ })
164
253
  .run(async ({ flags }) => {
165
- await initCliCommand('init', { debug: flags.debug, config: flags.config })
254
+ const outputMode = resolveOutputModeOrExit(flags)
255
+ await initCliCommand('init', {
256
+ debug: flags.debug,
257
+ config: flags.config,
258
+ json: outputMode === 'json',
259
+ quiet: flags.quiet,
260
+ })
166
261
  console.log('Use one of:')
167
262
  console.log(' suemo init config [--force]')
168
263
  console.log(' suemo init schema [--yes]')
264
+ console.log(' suemo init opencode')
265
+ console.log(' suemo skill [reference]')
169
266
  console.log('\nRun `suemo init --help` for full details.')
170
267
  })
@@ -3,7 +3,7 @@ import { connect, disconnect } from '../../db/client.ts'
3
3
  import { getLogger } from '../../logger.ts'
4
4
  import { observe } from '../../memory/write.ts'
5
5
  import { MemoryKindSchema } from '../../types.ts'
6
- import { app, initCliCommand } from '../shared.ts'
6
+ import { app, initCliCommand, printCliJson, resolveOutputModeOrExit, resolveScopeLabel } from '../shared.ts'
7
7
 
8
8
  const log = getLogger(['suemo', 'cli', 'observe'])
9
9
 
@@ -17,9 +17,17 @@ export const observeCmd = app.sub('observe')
17
17
  confidence: { type: 'number', description: 'Confidence 0.0–1.0', default: 1.0 },
18
18
  source: { type: 'string', description: 'Source label' },
19
19
  session: { type: 'string', description: 'Session ID (attach to open episode)' },
20
+ json: { type: 'boolean', description: 'Output JSON result' },
21
+ pretty: { type: 'boolean', description: 'Human-readable output (default)' },
20
22
  })
21
23
  .run(async ({ args, flags }) => {
22
- 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) {
@@ -28,11 +36,14 @@ export const observeCmd = app.sub('observe')
28
36
  }
29
37
 
30
38
  const config = await loadConfig(process.cwd(), flags.config)
39
+ const resolvedScope = resolveScopeLabel(flags.scope, config)
40
+ log.debug('Resolved observe scope', { scope: resolvedScope, explicit: flags.scope ?? null })
31
41
  const db = await connect(config.surreal)
32
42
  try {
33
43
  log.debug('Running observe command', {
34
44
  kind: kindParse.data,
35
- hasScope: Boolean(flags.scope),
45
+ hasScope: Boolean(resolvedScope),
46
+ scope: resolvedScope,
36
47
  tagCount: flags.tags ? flags.tags.split(',').filter(Boolean).length : 0,
37
48
  confidence: flags.confidence,
38
49
  contentLength: args.content.length,
@@ -41,12 +52,17 @@ export const observeCmd = app.sub('observe')
41
52
  content: args.content,
42
53
  kind: kindParse.data,
43
54
  tags: flags.tags ? flags.tags.split(',').map((t) => t.trim()) : [],
44
- scope: flags.scope,
55
+ scope: resolvedScope,
45
56
  confidence: flags.confidence,
46
57
  source: flags.source,
47
58
  sessionId: flags.session,
48
59
  }, config)
49
- console.log(JSON.stringify({ id: node.id, kind: node.kind, valid_from: node.valid_from }, null, 2))
60
+ if (outputMode === 'json') {
61
+ printCliJson({ id: node.id, kind: node.kind, valid_from: node.valid_from }, flags)
62
+ } else {
63
+ console.log(`Stored ${node.kind} memory: ${node.id}`)
64
+ console.log(` valid_from: ${node.valid_from}`)
65
+ }
50
66
  } finally {
51
67
  await disconnect()
52
68
  }
@@ -3,7 +3,7 @@ import { connect, disconnect } from '../../db/client.ts'
3
3
  import { getLogger } from '../../logger.ts'
4
4
  import { query } from '../../memory/read.ts'
5
5
  import type { MemoryNode } from '../../types.ts'
6
- import { app, initCliCommand } from '../shared.ts'
6
+ import { app, initCliCommand, printCliJson, resolveOutputModeOrExit, resolveScopeLabel } from '../shared.ts'
7
7
 
8
8
  const log = getLogger(['suemo', 'cli', 'query'])
9
9
 
@@ -13,30 +13,44 @@ export const queryCmd = app.sub('query')
13
13
  .flags({
14
14
  scope: { type: 'string', short: 's', description: 'Filter by scope' },
15
15
  top: { type: 'number', short: 'n', description: 'Number of results', default: 5 },
16
- json: { type: 'boolean', 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
+ })
27
+ const config = await loadConfig(process.cwd(), flags.config)
28
+ const resolvedScope = resolveScopeLabel(flags.scope, config)
29
+ log.debug('Resolved query scope', { scope: resolvedScope, explicit: flags.scope ?? null })
20
30
  log.debug('Running query command', {
21
- hasScope: Boolean(flags.scope),
31
+ hasScope: Boolean(resolvedScope),
32
+ scope: resolvedScope,
22
33
  top: flags.top,
23
- json: Boolean(flags.json),
34
+ outputMode,
24
35
  inputLength: args.input.length,
25
36
  })
26
37
  const nodes: MemoryNode[] = await (async () => {
27
38
  let connected = false
28
39
  try {
29
- const config = await loadConfig(process.cwd(), flags.config)
30
40
  const db = await connect(config.surreal)
31
41
  connected = true
32
- return await query(db, { input: args.input, scope: flags.scope, topK: flags.top }, config)
42
+ return await query(db, { input: args.input, scope: resolvedScope, topK: flags.top }, config)
33
43
  } finally {
34
44
  if (connected) await disconnect()
35
45
  }
36
46
  })()
37
- if (flags.json) {
38
- console.log(JSON.stringify(nodes, null, 2))
47
+ if (outputMode === 'json') {
48
+ printCliJson(nodes, flags)
39
49
  } else {
50
+ if (nodes.length === 0) {
51
+ console.log('No memories matched this query.')
52
+ return
53
+ }
40
54
  for (const n of nodes) {
41
55
  console.log(`[${n.kind}] ${n.id}`)
42
56
  console.log(` ${n.content.slice(0, 120)}`)
@@ -2,21 +2,37 @@ import { loadConfig } from '../../config.ts'
2
2
  import { connect, disconnect } from '../../db/client.ts'
3
3
  import { getLogger } from '../../logger.ts'
4
4
  import { recall } from '../../memory/read.ts'
5
- import { app, initCliCommand } from '../shared.ts'
5
+ import { app, initCliCommand, printCliJson, resolveOutputModeOrExit } from '../shared.ts'
6
6
 
7
7
  const log = getLogger(['suemo', 'cli', 'recall'])
8
8
 
9
9
  export const recallCmd = app.sub('recall')
10
10
  .meta({ description: 'Fetch a single node + its neighbours (ticks FSRS)' })
11
11
  .args([{ name: 'nodeId', type: 'string', required: true }])
12
+ .flags({
13
+ json: { type: 'boolean', description: 'Output full JSON' },
14
+ pretty: { type: 'boolean', description: 'Human-readable output (default)' },
15
+ })
12
16
  .run(async ({ args, flags }) => {
13
- 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
- import { loadConfig, resolveSyncConfig } from '../../config.ts'
1
+ import { inferProjectScope, loadConfig, resolveSyncConfig } from '../../config.ts'
2
2
  import { getLogger } from '../../logger.ts'
3
3
  import { startMcpServer, startMcpStdioServer } from '../../mcp/server.ts'
4
- import { app, initCliCommand } from '../shared.ts'
4
+ import { app, initCliCommand, resolveOutputModeOrExit } from '../shared.ts'
5
5
 
6
6
  const log = getLogger(['suemo', 'cli', 'serve'])
7
7
 
8
8
  function printDevRestartBanner(): void {
9
9
  if (process.env.SUEMO_DEV_WATCH !== '1') return
10
10
  const now = new Date().toISOString()
11
- console.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,15 +156,30 @@ export const serveCmd = app.sub('serve')
68
156
  host: { type: 'string', description: 'Host to bind to (overrides config)' },
69
157
  stdio: { type: 'boolean', description: 'Use stdio transport instead of HTTP' },
70
158
  dev: { type: 'boolean', description: 'Restart MCP server on code changes (bun --watch)' },
159
+ json: { type: 'boolean', description: 'Machine-readable startup logs only' },
160
+ 'verbose-json': { type: 'boolean', description: 'Include embeddings in JSON output (implies --json)' },
161
+ pretty: { type: 'boolean', description: 'Human-readable output (default)' },
71
162
  })
72
163
  .run(async ({ flags }) => {
164
+ const outputMode = resolveOutputModeOrExit(flags)
165
+ startParentWatchdog(Boolean(flags.stdio))
73
166
  printDevRestartBanner()
74
167
  await maybeDelayDevStartup()
75
- await initCliCommand('serve', { 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
  }
79
177
  const config = await loadConfig(process.cwd(), flags.config)
178
+ const inferredScope = inferProjectScope(process.cwd(), config)
179
+ log.info('Resolved default project scope', {
180
+ scope: inferredScope,
181
+ projectDir: config.main?.projectDir ?? '.ua',
182
+ })
80
183
  const sync = resolveSyncConfig(config)
81
184
  if (sync?.auto.enabled) {
82
185
  log.info('Auto-sync enabled', {
@@ -89,12 +192,12 @@ export const serveCmd = app.sub('serve')
89
192
  }
90
193
  if (flags.stdio) {
91
194
  log.debug('Starting MCP stdio transport')
92
- await startMcpStdioServer(config)
195
+ await runServerWithDevRetry({ stdio: true, config })
93
196
  return
94
197
  }
95
198
  if (flags.port) config.mcp.port = flags.port
96
199
  if (flags.host) config.mcp.host = flags.host
97
200
  log.debug('Starting MCP HTTP transport', { host: config.mcp.host, port: config.mcp.port })
98
- await startMcpServer(config)
201
+ await runServerWithDevRetry({ stdio: false, config })
99
202
  // Server runs indefinitely — no disconnect
100
203
  })