suemo 0.0.1 → 0.0.2

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.
@@ -1,90 +1,170 @@
1
+ import { confirm } from '@crustjs/prompts'
1
2
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
2
- import { join } from 'node:path'
3
+ import { dirname, join, resolve as resolvePath } from 'node:path'
3
4
  import { loadConfig } from '../../config.ts'
4
5
  import { connect, disconnect } from '../../db/client.ts'
5
6
  import { checkCompatibility } from '../../db/preflight.ts'
6
7
  import { runSchema } from '../../db/schema.ts'
7
- import { initLogger } from '../../logger.ts'
8
- import { app } from '../shared.ts'
8
+ import { getLogger } from '../../logger.ts'
9
+ import { app, initCliCommand } from '../shared.ts'
9
10
 
10
- export const initCmd = app.sub('init')
11
- .meta({ description: 'Initialize suemo: create config template and apply DB schema' })
11
+ import template from '../../config.template.ts' with { type: 'text' }
12
+
13
+ interface InitFlags {
14
+ debug?: boolean | undefined
15
+ config?: string | undefined
16
+ force?: boolean | undefined
17
+ yes?: boolean | undefined
18
+ }
19
+
20
+ const log = getLogger(['suemo', 'cli', 'init'])
21
+
22
+ const init = app.sub('init')
23
+ .meta({ description: 'Initialize suemo config and/or database schema' })
24
+
25
+ function homeConfigPath(): string {
26
+ const home = process.env.HOME ?? process.env.USERPROFILE
27
+ if (!home) throw new Error('HOME/USERPROFILE is not set; cannot resolve ~/.suemo path')
28
+ return join(home, '.suemo', 'suemo.ts')
29
+ }
30
+
31
+ function writeConfig(force: boolean, outputPath?: string): 'written' | 'skipped' {
32
+ const configPath = outputPath ? resolveConfigOutputPath(outputPath) : homeConfigPath()
33
+ log.debug('Preparing config template write', { configPath, force })
34
+ if (existsSync(configPath) && !force) {
35
+ console.log(`Config already exists at ${configPath}`)
36
+ console.log('Pass --force to overwrite.')
37
+ return 'skipped'
38
+ }
39
+
40
+ mkdirSync(dirname(configPath), { recursive: true })
41
+ writeFileSync(configPath, template as unknown as string, 'utf-8')
42
+ console.log(`✓ Config written to ${configPath}`)
43
+ return 'written'
44
+ }
45
+
46
+ function configOutputPath(flags: InitFlags): string | undefined {
47
+ const fromFlag = flags.config?.trim()
48
+ if (fromFlag) return fromFlag
49
+ const fromEnv = process.env.SUEMO_CONFIG_PATH?.trim()
50
+ return fromEnv || undefined
51
+ }
52
+
53
+ function resolveConfigOutputPath(configPath: string): string {
54
+ if (configPath.startsWith('~/')) {
55
+ const home = process.env.HOME ?? process.env.USERPROFILE
56
+ if (!home) throw new Error('HOME/USERPROFILE is not set; cannot expand ~ in config path')
57
+ return resolvePath(home, configPath.slice(2))
58
+ }
59
+ return resolvePath(configPath)
60
+ }
61
+
62
+ async function applySchema(configPath?: string): Promise<boolean> {
63
+ let connected = false
64
+ try {
65
+ log.debug('Loading config for schema apply', { cwd: process.cwd(), overridePath: configPath ?? null })
66
+ const config = await loadConfig(process.cwd(), configPath)
67
+ log.debug('Resolved schema apply target', {
68
+ namespace: config.surreal.namespace,
69
+ database: config.surreal.database,
70
+ url: config.surreal.url,
71
+ })
72
+ log.debug('Connecting for schema apply', {
73
+ url: config.surreal.url,
74
+ namespace: config.surreal.namespace,
75
+ database: config.surreal.database,
76
+ })
77
+ const db = await connect(config.surreal)
78
+ connected = true
79
+
80
+ const compat = await checkCompatibility(db, {
81
+ requireEmbedding: false,
82
+ context: 'cli:init-schema',
83
+ })
84
+ log.debug('Preflight result for init schema', {
85
+ ok: compat.ok,
86
+ surrealVersion: compat.surrealVersion,
87
+ surrealkv: compat.surrealkv,
88
+ retention_ok: compat.retention_ok,
89
+ embedding: compat.embedding,
90
+ errorCount: compat.errors.length,
91
+ })
92
+ if (!compat.ok) {
93
+ console.error('\n✗ Compatibility check failed:')
94
+ for (const e of compat.errors) console.error(` • ${e}`)
95
+ return false
96
+ }
97
+
98
+ if (!compat.embedding) {
99
+ console.warn(
100
+ '\n⚠ fn::embed() is unavailable in this DB. Schema can still be applied, but observe/query/consolidate will fail until embedding model is configured.',
101
+ )
102
+ }
103
+
104
+ await runSchema(db)
105
+ console.log('✓ Schema applied to SurrealDB')
106
+ return true
107
+ } catch (e) {
108
+ console.error(`✗ Schema not applied: ${String(e)}`)
109
+ return false
110
+ } finally {
111
+ if (connected) {
112
+ await disconnect()
113
+ }
114
+ }
115
+ }
116
+
117
+ const initConfigCmd = init.sub('config')
118
+ .meta({ description: 'Create/update ~/.suemo/suemo.ts config template' })
12
119
  .flags({
13
- force: { type: 'boolean', description: 'Overwrite existing config without prompting' },
120
+ force: { type: 'boolean', description: 'Overwrite existing config file' },
14
121
  })
15
122
  .run(async ({ flags }) => {
16
- await initLogger({ level: flags.debug ? 'debug' : 'info' })
123
+ await initCliCommand('init config', { debug: flags.debug, config: flags.config })
124
+ writeConfig(Boolean(flags.force), configOutputPath(flags))
125
+ })
17
126
 
18
- const homeConfig = join(process.env.HOME ?? '~', '.suemo', 'suemo.ts')
19
- const configExists = existsSync(homeConfig)
127
+ const initSchemaCmd = init.sub('schema')
128
+ .meta({ description: 'Apply database schema using current config' })
129
+ .flags({
130
+ yes: { type: 'boolean', short: 'y', description: 'Skip schema confirmation prompt' },
131
+ })
132
+ .run(async ({ flags }) => {
133
+ await initCliCommand('init schema', { debug: flags.debug, config: flags.config })
20
134
 
21
- if (configExists && !flags.force) {
22
- console.log(`Config already exists at ${homeConfig}`)
23
- console.log('Pass --force to overwrite.')
24
- return
135
+ const confirmOptions: {
136
+ message: string
137
+ default: boolean
138
+ active: string
139
+ inactive: string
140
+ initial?: boolean
141
+ } = {
142
+ message: 'Apply schema to the configured SurrealDB now?',
143
+ default: false,
144
+ active: 'Apply',
145
+ inactive: 'Cancel',
25
146
  }
147
+ if (flags.yes !== undefined) {
148
+ confirmOptions.initial = flags.yes
149
+ }
150
+ const proceed = await confirm(confirmOptions)
26
151
 
27
- const template = `import { defineConfig } from "suemo";
28
-
29
- export default defineConfig({
30
- surreal: {
31
- url: process.env.SURREAL_URL ?? "ws://localhost:8000",
32
- namespace: process.env.SURREAL_NS ?? "myagents",
33
- database: process.env.SURREAL_DB ?? "default",
34
- auth: {
35
- user: process.env.SURREAL_USER!,
36
- pass: process.env.SURREAL_PASS!,
37
- },
38
- },
39
- embedding: {
40
- provider: "surreal",
41
- dimension: 1536,
42
- },
43
- consolidation: {
44
- trigger: "timer",
45
- intervalMinutes: 30,
46
- reactiveThreshold: 50,
47
- nremSimilarityThreshold: 0.85,
48
- remRelationThreshold: 0.4,
49
- llm: {
50
- url: process.env.LLM_URL!,
51
- model: process.env.LLM_MODEL ?? "gpt-4o",
52
- apiKey: process.env.LLM_API_KEY!,
53
- },
54
- },
55
- retrieval: {
56
- weights: { vector: 0.5, bm25: 0.25, graph: 0.15, temporal: 0.1 },
57
- },
58
- mcp: {
59
- port: Number(process.env.SUEMO_PORT) || 4242,
60
- host: "127.0.0.1",
61
- },
62
- });
63
- `
64
-
65
- mkdirSync(join(process.env.HOME ?? '~', '.suemo'), { recursive: true })
66
- writeFileSync(homeConfig, template, 'utf-8')
67
- console.log(`✓ Config written to ${homeConfig}`)
68
-
69
- // Apply schema if SURREAL_* env vars are present
70
- if (process.env.SURREAL_URL && process.env.SURREAL_USER) {
71
- try {
72
- const config = await loadConfig(process.cwd(), flags.config)
73
- const db = await connect(config.surreal)
74
- const compat = await checkCompatibility(db)
75
- if (!compat.ok) {
76
- console.error('\n✗ Compatibility check failed:')
77
- for (const e of compat.errors) console.error(` • ${e}`)
78
- await disconnect()
79
- return
80
- }
81
- await runSchema(db)
82
- await disconnect()
83
- console.log('✓ Schema applied to SurrealDB')
84
- } catch (e) {
85
- console.warn(` (Schema not applied — configure SURREAL_* env vars first: ${String(e)})`)
86
- }
87
- } else {
88
- console.log(' Set SURREAL_URL, SURREAL_USER, SURREAL_PASS, then run `suemo init` again to apply schema.')
152
+ if (!proceed) {
153
+ console.log('Schema apply cancelled.')
154
+ return
89
155
  }
156
+
157
+ const ok = await applySchema(flags.config)
158
+ if (!ok) process.exitCode = 1
159
+ })
160
+
161
+ export const initCmd = init
162
+ .command(initConfigCmd)
163
+ .command(initSchemaCmd)
164
+ .run(async ({ flags }) => {
165
+ await initCliCommand('init', { debug: flags.debug, config: flags.config })
166
+ console.log('Use one of:')
167
+ console.log(' suemo init config [--force]')
168
+ console.log(' suemo init schema [--yes]')
169
+ console.log('\nRun `suemo init --help` for full details.')
90
170
  })
@@ -1,9 +1,11 @@
1
1
  import { loadConfig } from '../../config.ts'
2
2
  import { connect, disconnect } from '../../db/client.ts'
3
- import { initLogger } from '../../logger.ts'
3
+ import { getLogger } from '../../logger.ts'
4
4
  import { observe } from '../../memory/write.ts'
5
5
  import { MemoryKindSchema } from '../../types.ts'
6
- import { app } from '../shared.ts'
6
+ import { app, initCliCommand } from '../shared.ts'
7
+
8
+ const log = getLogger(['suemo', 'cli', 'observe'])
7
9
 
8
10
  export const observeCmd = app.sub('observe')
9
11
  .meta({ description: 'Store a new observation in memory' })
@@ -17,7 +19,7 @@ export const observeCmd = app.sub('observe')
17
19
  session: { type: 'string', description: 'Session ID (attach to open episode)' },
18
20
  })
19
21
  .run(async ({ args, flags }) => {
20
- await initLogger({ level: flags.debug ? 'debug' : 'info' })
22
+ await initCliCommand('observe', { debug: flags.debug, config: flags.config })
21
23
 
22
24
  const kindParse = MemoryKindSchema.safeParse(flags.kind)
23
25
  if (!kindParse.success) {
@@ -27,14 +29,24 @@ export const observeCmd = app.sub('observe')
27
29
 
28
30
  const config = await loadConfig(process.cwd(), flags.config)
29
31
  const db = await connect(config.surreal)
30
- const node = await observe(db, {
31
- content: args.content,
32
- kind: kindParse.data,
33
- tags: flags.tags ? flags.tags.split(',').map((t) => t.trim()) : [],
34
- scope: flags.scope,
35
- confidence: flags.confidence,
36
- source: flags.source,
37
- })
38
- await disconnect()
39
- console.log(JSON.stringify({ id: node.id, kind: node.kind, valid_from: node.valid_from }, null, 2))
32
+ try {
33
+ log.debug('Running observe command', {
34
+ kind: kindParse.data,
35
+ hasScope: Boolean(flags.scope),
36
+ tagCount: flags.tags ? flags.tags.split(',').filter(Boolean).length : 0,
37
+ confidence: flags.confidence,
38
+ contentLength: args.content.length,
39
+ })
40
+ const node = await observe(db, {
41
+ content: args.content,
42
+ kind: kindParse.data,
43
+ tags: flags.tags ? flags.tags.split(',').map((t) => t.trim()) : [],
44
+ scope: flags.scope,
45
+ confidence: flags.confidence,
46
+ source: flags.source,
47
+ }, config)
48
+ console.log(JSON.stringify({ id: node.id, kind: node.kind, valid_from: node.valid_from }, null, 2))
49
+ } finally {
50
+ await disconnect()
51
+ }
40
52
  })
@@ -1,8 +1,11 @@
1
1
  import { loadConfig } from '../../config.ts'
2
2
  import { connect, disconnect } from '../../db/client.ts'
3
- import { initLogger } from '../../logger.ts'
3
+ import { getLogger } from '../../logger.ts'
4
4
  import { query } from '../../memory/read.ts'
5
- import { app } from '../shared.ts'
5
+ import type { MemoryNode } from '../../types.ts'
6
+ import { app, initCliCommand } from '../shared.ts'
7
+
8
+ const log = getLogger(['suemo', 'cli', 'query'])
6
9
 
7
10
  export const queryCmd = app.sub('query')
8
11
  .meta({ description: 'Hybrid semantic search over memories' })
@@ -13,11 +16,24 @@ export const queryCmd = app.sub('query')
13
16
  json: { type: 'boolean', short: 'j', description: 'Output full JSON nodes' },
14
17
  })
15
18
  .run(async ({ args, flags }) => {
16
- await initLogger({ level: flags.debug ? 'debug' : 'info' })
17
- const config = await loadConfig(process.cwd(), flags.config)
18
- const db = await connect(config.surreal)
19
- const nodes = await query(db, { input: args.input, scope: flags.scope, topK: flags.top })
20
- await disconnect()
19
+ await initCliCommand('query', { debug: flags.debug, config: flags.config })
20
+ log.debug('Running query command', {
21
+ hasScope: Boolean(flags.scope),
22
+ top: flags.top,
23
+ json: Boolean(flags.json),
24
+ inputLength: args.input.length,
25
+ })
26
+ const nodes: MemoryNode[] = await (async () => {
27
+ let connected = false
28
+ try {
29
+ const config = await loadConfig(process.cwd(), flags.config)
30
+ const db = await connect(config.surreal)
31
+ connected = true
32
+ return await query(db, { input: args.input, scope: flags.scope, topK: flags.top }, config)
33
+ } finally {
34
+ if (connected) await disconnect()
35
+ }
36
+ })()
21
37
  if (flags.json) {
22
38
  console.log(JSON.stringify(nodes, null, 2))
23
39
  } else {
@@ -1,17 +1,23 @@
1
1
  import { loadConfig } from '../../config.ts'
2
2
  import { connect, disconnect } from '../../db/client.ts'
3
- import { initLogger } from '../../logger.ts'
3
+ import { getLogger } from '../../logger.ts'
4
4
  import { recall } from '../../memory/read.ts'
5
- import { app } from '../shared.ts'
5
+ import { app, initCliCommand } from '../shared.ts'
6
+
7
+ const log = getLogger(['suemo', 'cli', 'recall'])
6
8
 
7
9
  export const recallCmd = app.sub('recall')
8
10
  .meta({ description: 'Fetch a single node + its neighbours (ticks FSRS)' })
9
11
  .args([{ name: 'nodeId', type: 'string', required: true }])
10
12
  .run(async ({ args, flags }) => {
11
- await initLogger({ level: flags.debug ? 'debug' : 'info' })
13
+ await initCliCommand('recall', { debug: flags.debug, config: flags.config })
14
+ log.debug('Running recall command', { nodeId: args.nodeId })
12
15
  const config = await loadConfig(process.cwd(), flags.config)
13
16
  const db = await connect(config.surreal)
14
- const result = await recall(db, args.nodeId)
15
- await disconnect()
16
- console.log(JSON.stringify(result, null, 2))
17
+ try {
18
+ const result = await recall(db, args.nodeId)
19
+ console.log(JSON.stringify(result, null, 2))
20
+ } finally {
21
+ await disconnect()
22
+ }
17
23
  })
@@ -1,19 +1,28 @@
1
1
  import { loadConfig } from '../../config.ts'
2
- import { initLogger } from '../../logger.ts'
3
- import { startMcpServer } from '../../mcp/server.ts'
4
- import { app } from '../shared.ts'
2
+ import { getLogger } from '../../logger.ts'
3
+ import { startMcpServer, startMcpStdioServer } from '../../mcp/server.ts'
4
+ import { app, initCliCommand } from '../shared.ts'
5
+
6
+ const log = getLogger(['suemo', 'cli', 'serve'])
5
7
 
6
8
  export const serveCmd = app.sub('serve')
7
- .meta({ description: 'Start the MCP server' })
9
+ .meta({ description: 'Start the MCP server (HTTP or stdio)' })
8
10
  .flags({
9
11
  port: { type: 'number', short: 'p', description: 'Port to listen on (overrides config)' },
10
12
  host: { type: 'string', description: 'Host to bind to (overrides config)' },
13
+ stdio: { type: 'boolean', description: 'Use stdio transport instead of HTTP' },
11
14
  })
12
15
  .run(async ({ flags }) => {
13
- await initLogger({ level: flags.debug ? 'debug' : 'info' })
16
+ await initCliCommand('serve', { debug: flags.debug, config: flags.config })
14
17
  const config = await loadConfig(process.cwd(), flags.config)
18
+ if (flags.stdio) {
19
+ log.debug('Starting MCP stdio transport')
20
+ await startMcpStdioServer(config)
21
+ return
22
+ }
15
23
  if (flags.port) config.mcp.port = flags.port
16
24
  if (flags.host) config.mcp.host = flags.host
25
+ log.debug('Starting MCP HTTP transport', { host: config.mcp.host, port: config.mcp.port })
17
26
  await startMcpServer(config)
18
27
  // Server runs indefinitely — no disconnect
19
28
  })
@@ -1,8 +1,10 @@
1
1
  import { loadConfig } from '../../config.ts'
2
2
  import { connect, disconnect } from '../../db/client.ts'
3
- import { initLogger } from '../../logger.ts'
3
+ import { getLogger } from '../../logger.ts'
4
4
  import { syncTo } from '../../sync.ts'
5
- import { app } from '../shared.ts'
5
+ import { app, initCliCommand } from '../shared.ts'
6
+
7
+ const log = getLogger(['suemo', 'cli', 'sync'])
6
8
 
7
9
  export const syncCmd = app.sub('sync')
8
10
  .meta({ description: 'Push memories to remote SurrealDB (append-only)' })
@@ -10,14 +12,21 @@ export const syncCmd = app.sub('sync')
10
12
  'dry-run': { type: 'boolean', description: 'Show what would be pushed without writing', default: false },
11
13
  })
12
14
  .run(async ({ flags }) => {
13
- await initLogger({ level: flags.debug ? 'debug' : 'info' })
15
+ await initCliCommand('sync', { debug: flags.debug, config: flags.config })
14
16
  const config = await loadConfig(process.cwd(), flags.config)
15
17
  if (!config.sync) {
16
18
  console.error('No sync.remote configured in suemo config.')
17
19
  process.exit(1)
18
20
  }
21
+ log.debug('Running sync command', {
22
+ dryRun: flags['dry-run'],
23
+ target: `${config.sync.remote.url}/${config.sync.remote.namespace}/${config.sync.remote.database}`,
24
+ })
19
25
  const db = await connect(config.surreal)
20
- const result = await syncTo(db, config.sync.remote, { dryRun: flags['dry-run'] })
21
- await disconnect()
22
- console.log(JSON.stringify(result, null, 2))
26
+ try {
27
+ const result = await syncTo(db, config.sync.remote, { dryRun: flags['dry-run'] })
28
+ console.log(JSON.stringify(result, null, 2))
29
+ } finally {
30
+ await disconnect()
31
+ }
23
32
  })
@@ -1,8 +1,10 @@
1
1
  import { loadConfig } from '../../config.ts'
2
2
  import { connect, disconnect } from '../../db/client.ts'
3
- import { initLogger } from '../../logger.ts'
3
+ import { getLogger } from '../../logger.ts'
4
4
  import { timeline } from '../../memory/read.ts'
5
- import { app } from '../shared.ts'
5
+ import { app, initCliCommand } from '../shared.ts'
6
+
7
+ const log = getLogger(['suemo', 'cli', 'timeline'])
6
8
 
7
9
  export const timelineCmd = app.sub('timeline')
8
10
  .meta({ description: 'Chronological view of memories' })
@@ -14,24 +16,34 @@ export const timelineCmd = app.sub('timeline')
14
16
  json: { type: 'boolean', short: 'j', description: 'Output full JSON' },
15
17
  })
16
18
  .run(async ({ flags }) => {
17
- await initLogger({ level: flags.debug ? 'debug' : 'info' })
19
+ await initCliCommand('timeline', { debug: flags.debug, config: flags.config })
20
+ log.debug('Running timeline command', {
21
+ hasScope: Boolean(flags.scope),
22
+ from: flags.from ?? null,
23
+ until: flags.until ?? null,
24
+ limit: flags.limit,
25
+ json: Boolean(flags.json),
26
+ })
18
27
  const config = await loadConfig(process.cwd(), flags.config)
19
28
  const db = await connect(config.surreal)
20
- const nodes = await timeline(db, {
21
- ...(flags.scope ? { scope: flags.scope } : {}),
22
- ...(flags.from ? { from: flags.from } : {}),
23
- ...(flags.until ? { until: flags.until } : {}),
24
- ...(flags.limit ? { limit: flags.limit } : {}),
25
- })
26
- await disconnect()
27
- if (flags.json) {
28
- console.log(JSON.stringify(nodes, null, 2))
29
- } else {
30
- for (const n of nodes) {
31
- const ts = new Date(n.created_at).toLocaleString()
32
- console.log(`${ts} [${n.kind}] ${n.id}`)
33
- console.log(` ${n.content.slice(0, 120)}`)
34
- console.log()
29
+ try {
30
+ const nodes = await timeline(db, {
31
+ ...(flags.scope ? { scope: flags.scope } : {}),
32
+ ...(flags.from ? { from: flags.from } : {}),
33
+ ...(flags.until ? { until: flags.until } : {}),
34
+ ...(flags.limit ? { limit: flags.limit } : {}),
35
+ })
36
+ if (flags.json) {
37
+ console.log(JSON.stringify(nodes, null, 2))
38
+ } else {
39
+ for (const n of nodes) {
40
+ const ts = new Date(n.created_at).toLocaleString()
41
+ console.log(`${ts} [${n.kind}] ${n.id}`)
42
+ console.log(` ${n.content.slice(0, 120)}`)
43
+ console.log()
44
+ }
35
45
  }
46
+ } finally {
47
+ await disconnect()
36
48
  }
37
49
  })
@@ -1,8 +1,10 @@
1
1
  import { loadConfig } from '../../config.ts'
2
2
  import { connect, disconnect } from '../../db/client.ts'
3
- import { initLogger } from '../../logger.ts'
3
+ import { getLogger } from '../../logger.ts'
4
4
  import { wander } from '../../memory/read.ts'
5
- import { app } from '../shared.ts'
5
+ import { app, initCliCommand } from '../shared.ts'
6
+
7
+ const log = getLogger(['suemo', 'cli', 'wander'])
6
8
 
7
9
  export const wanderCmd = app.sub('wander')
8
10
  .meta({ description: 'Spreading-activation walk through the memory graph' })
@@ -13,22 +15,31 @@ export const wanderCmd = app.sub('wander')
13
15
  json: { type: 'boolean', short: 'j', description: 'Output full JSON nodes' },
14
16
  })
15
17
  .run(async ({ flags }) => {
16
- await initLogger({ level: flags.debug ? 'debug' : 'info' })
18
+ await initCliCommand('wander', { debug: flags.debug, config: flags.config })
19
+ log.debug('Running wander command', {
20
+ hasAnchor: Boolean(flags.from),
21
+ hops: flags.hops,
22
+ hasScope: Boolean(flags.scope),
23
+ json: Boolean(flags.json),
24
+ })
17
25
  const config = await loadConfig(process.cwd(), flags.config)
18
26
  const db = await connect(config.surreal)
19
- const nodes = await wander(db, {
20
- ...(flags.from ? { anchor: flags.from } : {}),
21
- ...(flags.hops ? { hops: flags.hops } : {}),
22
- ...(flags.scope ? { scope: flags.scope } : {}),
23
- })
24
- await disconnect()
25
- if (flags.json) {
26
- console.log(JSON.stringify(nodes, null, 2))
27
- } else {
28
- for (const n of nodes) {
29
- console.log(`[${n.kind}] ${n.id} salience=${n.salience.toFixed(2)}`)
30
- console.log(` ${n.content.slice(0, 120)}`)
31
- console.log()
27
+ try {
28
+ const nodes = await wander(db, {
29
+ ...(flags.from ? { anchor: flags.from } : {}),
30
+ ...(flags.hops ? { hops: flags.hops } : {}),
31
+ ...(flags.scope ? { scope: flags.scope } : {}),
32
+ })
33
+ if (flags.json) {
34
+ console.log(JSON.stringify(nodes, null, 2))
35
+ } else {
36
+ for (const n of nodes) {
37
+ console.log(`[${n.kind}] ${n.id} salience=${n.salience.toFixed(2)}`)
38
+ console.log(` ${n.content.slice(0, 120)}`)
39
+ console.log()
40
+ }
32
41
  }
42
+ } finally {
43
+ await disconnect()
33
44
  }
34
45
  })
package/src/cli/index.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { helpPlugin, versionPlugin } from '@crustjs/plugins'
4
- import { initLogger } from '../logger.ts'
5
4
  import { believeCmd } from './commands/believe.ts'
6
5
  import { consolidateCmd } from './commands/consolidate.ts'
6
+ import { doctorCmd } from './commands/doctor.ts'
7
7
  import { exportCmd, importCmd } from './commands/export-import.ts'
8
8
  import { goalCmd } from './commands/goal.ts'
9
9
  import { healthCmd } from './commands/health.ts'
@@ -17,9 +17,7 @@ import { timelineCmd } from './commands/timeline.ts'
17
17
  import { wanderCmd } from './commands/wander.ts'
18
18
  import { app } from './shared.ts'
19
19
 
20
- import packageJson from '../../package.json' with { type: 'json' }
21
-
22
- await initLogger({ level: 'info' })
20
+ import packageJson from '@/package.json' with { type: 'json' }
23
21
 
24
22
  await app
25
23
  .use(versionPlugin(packageJson.version ?? '0.0.0'))
@@ -34,6 +32,7 @@ await app
34
32
  .command(timelineCmd)
35
33
  .command(goalCmd) // goal is a container: goal set / goal list / goal resolve
36
34
  .command(consolidateCmd)
35
+ .command(doctorCmd)
37
36
  .command(healthCmd)
38
37
  .command(syncCmd)
39
38
  .command(exportCmd)
package/src/cli/shared.ts CHANGED
@@ -1,5 +1,39 @@
1
1
  // src/cli/shared.ts — single shared root, holds inheritable flags
2
2
  import { Crust } from '@crustjs/core'
3
+ import { getLogger, initLogger } from '../logger.ts'
4
+
5
+ const cliLog = getLogger(['suemo', 'cli'])
6
+
7
+ const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'off'])
8
+
9
+ function isTruthyEnv(name: string): boolean {
10
+ const raw = process.env[name]
11
+ if (raw === undefined) return false
12
+ const normalized = raw.trim().toLowerCase()
13
+ if (normalized === '') return true
14
+ return !FALSE_ENV_VALUES.has(normalized)
15
+ }
16
+
17
+ export function isDebugEnabled(debugFlag?: boolean): boolean {
18
+ return Boolean(debugFlag) || isTruthyEnv('SUEMO_DEBUG')
19
+ }
20
+
21
+ export async function initCliCommand(
22
+ command: string,
23
+ flags: { debug?: boolean | undefined; config?: string | undefined },
24
+ ): Promise<void> {
25
+ const debug = isDebugEnabled(flags.debug)
26
+ await initLogger({ level: debug ? 'debug' : 'info' })
27
+ cliLog.debug('CLI command invocation', {
28
+ command,
29
+ cwd: process.cwd(),
30
+ debugFlag: flags.debug ?? false,
31
+ envDebug: process.env.SUEMO_DEBUG ?? null,
32
+ debugEnabled: debug,
33
+ configFlag: flags.config ?? null,
34
+ envConfigPath: process.env.SUEMO_CONFIG_PATH ?? null,
35
+ })
36
+ }
3
37
 
4
38
  export const app = new Crust('suemo')
5
39
  .meta({ description: 'Persistent semantic memory for AI agents' })