suemo 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,11 +34,14 @@ suemo gives AI agents a memory that survives across sessions, models, and runtim
34
34
  SurrealDB must be started with SurrealKV and a retention window:
35
35
 
36
36
  ```sh
37
- SURREAL_DATASTORE_RETENTION=90d surreal start \
37
+ SURREAL_DATASTORE_VERSIONED=true SURREAL_DATASTORE_RETENTION=90d surreal start \
38
38
  --bind 0.0.0.0:8000 \
39
- surrealkv:///path/to/data
39
+ --allow-funcs "fn::*" \
40
+ -- surrealkv:///path/to/data
40
41
  ```
41
42
 
43
+ For suemo, the critical capability is allowing custom functions (`fn::embed`) via `--allow-funcs "fn::*"`.
44
+
42
45
  An embedding model must be configured in SurrealDB for `fn::embed()` to work.
43
46
 
44
47
  ---
@@ -64,6 +67,19 @@ bun run src/cli/index.ts init config
64
67
 
65
68
  This writes `~/.suemo/suemo.ts`. Edit it with your SurrealDB URL, credentials, and LLM endpoint.
66
69
 
70
+ Set project scope discovery in config (recommended):
71
+
72
+ ```ts
73
+ export default defineConfig({
74
+ main: {
75
+ projectDir: '.ua',
76
+ },
77
+ // ...
78
+ })
79
+ ```
80
+
81
+ With this, suemo resolves default scope from nearest `.ua/suemo.json` (auto-created if missing).
82
+
67
83
  **2. Apply schema**
68
84
 
69
85
  Apply schema after you set/edit namespace/database:
@@ -106,6 +122,30 @@ suemo serve --dev
106
122
 
107
123
  ---
108
124
 
125
+ ## OpenCode setup (only integration target)
126
+
127
+ Print copy-paste setup snippets:
128
+
129
+ ```sh
130
+ suemo init opencode
131
+ ```
132
+
133
+ This prints:
134
+
135
+ - MCP config snippet (`command: suemo`, `args: ["serve", "--stdio", ...]`)
136
+ - minimal AGENTS.md guidance snippet
137
+
138
+ To fetch the latest skill docs directly from your local checkout:
139
+
140
+ ```sh
141
+ suemo skill
142
+ suemo skill core-workflow
143
+ ```
144
+
145
+ No files are auto-written by this command.
146
+
147
+ ---
148
+
109
149
  ## CLI Reference
110
150
 
111
151
  ```
@@ -119,6 +159,7 @@ Commands:
119
159
  init Show init subcommands and usage guidance
120
160
  init config Create/update ~/.suemo/suemo.ts
121
161
  init schema Apply DB schema from current config (with confirm)
162
+ skill Print suemo skill docs (or specific reference)
122
163
  serve Start the MCP server (HTTP or stdio)
123
164
  observe <content> Store an observation
124
165
  believe <content> Store a belief (triggers contradiction detection)
@@ -277,12 +318,16 @@ This prints your active target (`url`, `namespace`, `database`) and step-by-step
277
318
  | `recall` | Fetch a node and its 1-hop neighbourhood; ticks FSRS |
278
319
  | `wander` | Spreading-activation walk through the graph |
279
320
  | `timeline` | Chronological memory slice with optional date range |
321
+ | `context` | Recover recent session context for current/default scope |
280
322
  | `episode_start` | Begin a bounded session window |
281
323
  | `episode_end` | Close a session, optionally with a summary |
282
324
  | `goal_set` | Create a goal node |
283
325
  | `goal_resolve` | Mark a goal achieved |
284
326
  | `goal_list` | List active (or all) goals |
285
327
  | `upsert_by_key` | Upsert a memory node by stable topic key |
328
+ | `update` | Update an existing memory node by ID (re-embeds if content changed) |
329
+ | `suggest_topic_key` | Suggest deterministic canonical topic key from free text |
330
+ | `skill` | Return current suemo skill docs or one named reference |
286
331
  | `capture_prompt` | Capture raw prompt and link derived observations |
287
332
  | `session_context_get` | Fetch open episode summary/context by session ID |
288
333
  | `session_context_set` | Update open episode summary/context by session ID |
@@ -293,6 +338,13 @@ This prints your active target (`url`, `namespace`, `database`) and step-by-step
293
338
 
294
339
  Agents never supply temporal fields (`valid_from`, `valid_until`). These are system-managed.
295
340
 
341
+ ### Scope and longevity notes
342
+
343
+ - Default inferred project scope now uses nearest `<projectDir>/suemo.json` with `main.projectDir` defaulting to `.ua`.
344
+ - Semantic operations are scope-aware (dedup, contradiction detection, upsert-by-key, consolidation).
345
+ - Retention probe is fail-closed on unexpected probe errors.
346
+ - Episode records are included in sync and export/import flows.
347
+
296
348
  ---
297
349
 
298
350
  ## Architecture
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suemo",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Persistent semantic memory for AI agents — backed by SurrealDB.",
5
5
  "author": {
6
6
  "name": "Umar Alfarouk",
@@ -37,7 +37,10 @@
37
37
  "scripts": {
38
38
  "dev": "bun run src/cli/index.ts",
39
39
  "start": "bun run src/cli/index.ts",
40
- "check": "bun tsc --noEmit"
40
+ "ssot:check": "bun run scripts/ssot.ts check",
41
+ "ssot:sync": "bun run scripts/ssot.ts sync",
42
+ "check": "bun tsc --noEmit && bun run ssot:check",
43
+ "sync": "bun run ssot:sync"
41
44
  },
42
45
  "dependencies": {
43
46
  "@crustjs/core": "^0.0.15",
@@ -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, resolveScopeLabel } from '../shared.ts'
6
6
 
7
7
  const log = getLogger(['suemo', 'cli', 'believe'])
8
8
 
@@ -12,25 +12,42 @@ 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)
27
+ const resolvedScope = resolveScopeLabel(flags.scope, config)
28
+ log.debug('Resolved believe scope', { scope: resolvedScope, explicit: flags.scope ?? null })
19
29
  const db = await connect(config.surreal)
20
30
  try {
21
31
  log.debug('Running believe command', {
22
- hasScope: Boolean(flags.scope),
32
+ hasScope: Boolean(resolvedScope),
33
+ scope: resolvedScope,
23
34
  confidence: flags.confidence,
24
35
  contentLength: args.content.length,
25
36
  })
26
37
  const { node, contradicted } = await believe(db, {
27
38
  content: args.content,
28
- scope: flags.scope,
39
+ scope: resolvedScope,
29
40
  confidence: flags.confidence,
30
41
  }, 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))
42
+ if (outputMode === 'json') {
43
+ const out: Record<string, unknown> = { id: node.id, valid_from: node.valid_from }
44
+ if (contradicted) out.contradicted = contradicted.id
45
+ printCliJson(out, flags)
46
+ } else {
47
+ console.log(`Stored belief memory: ${node.id}`)
48
+ console.log(` valid_from: ${node.valid_from}`)
49
+ if (contradicted) console.log(` contradicted: ${contradicted.id}`)
50
+ }
34
51
  } finally {
35
52
  await disconnect()
36
53
  }
@@ -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, resolveScopeLabel } from '../shared.ts'
6
6
 
7
7
  const log = getLogger(['suemo', 'cli', 'consolidate'])
8
8
 
@@ -10,11 +10,21 @@ 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
+ scope: { type: 'string', short: 's', description: 'Scope label (defaults to inferred project scope)' },
14
+ json: { type: 'boolean', description: 'Output full JSON' },
15
+ pretty: { type: 'boolean', description: 'Human-readable output (default)' },
13
16
  })
14
17
  .run(async ({ flags }) => {
15
- await initCliCommand('consolidate', { debug: flags.debug, config: flags.config })
18
+ const outputMode = resolveOutputModeOrExit(flags)
19
+ await initCliCommand('consolidate', {
20
+ debug: flags.debug,
21
+ config: flags.config,
22
+ json: outputMode === 'json',
23
+ quiet: flags.quiet,
24
+ })
16
25
  log.debug('Running consolidate command', { nremOnly: flags['nrem-only'] })
17
26
  const config = await loadConfig(process.cwd(), flags.config)
27
+ const scope = resolveScopeLabel(flags.scope, config)
18
28
  const db = await connect(config.surreal)
19
29
  try {
20
30
  const run = await consolidate(db, {
@@ -23,8 +33,16 @@ export const consolidateCmd = app.sub('consolidate')
23
33
  remRelationThreshold: config.consolidation.remRelationThreshold,
24
34
  llm: config.consolidation.llm,
25
35
  embedding: config.embedding,
36
+ ...(scope ? { scope } : {}),
26
37
  })
27
- console.log(JSON.stringify(run, null, 2))
38
+ if (outputMode === 'json') {
39
+ printCliJson(run, flags)
40
+ } else {
41
+ console.log(
42
+ `Consolidation ${run.status}: phase=${run.phase} nodes_in=${run.nodes_in} nodes_out=${run.nodes_out}`,
43
+ )
44
+ if (run.error) console.log(` error: ${run.error}`)
45
+ }
28
46
  } finally {
29
47
  await disconnect()
30
48
  }
@@ -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)
@@ -93,6 +104,8 @@ const doctorEmbedCmd = doctor.sub('embed')
93
104
  console.log('\nHow to set up fn::embed() (step-by-step):')
94
105
  console.log('\n1) Ensure SurrealDB CLI exposes ML commands:')
95
106
  console.log(' surreal ml --help')
107
+ console.log('\n and start SurrealDB with custom function capability:')
108
+ console.log(' surreal start --allow-funcs "fn::*" ...')
96
109
  console.log('\n2) Import a .surml embedding model into this exact NS/DB:')
97
110
  console.log(
98
111
  ` surreal ml import --conn ${endpoint} --user <USER> --pass <PASS> --ns ${config.surreal.namespace} --db ${config.surreal.database} path/to/your-model.surml`,
@@ -115,8 +128,19 @@ const doctorEmbedCmd = doctor.sub('embed')
115
128
 
116
129
  export const doctorCmd = doctor
117
130
  .command(doctorEmbedCmd)
131
+ .flags({
132
+ json: { type: 'boolean', description: 'Machine-readable output mode' },
133
+ 'verbose-json': { type: 'boolean', description: 'Include embeddings in JSON output (implies --json)' },
134
+ pretty: { type: 'boolean', description: 'Human-readable output (default)' },
135
+ })
118
136
  .run(async ({ flags }) => {
119
- await initCliCommand('doctor', { debug: flags.debug, config: flags.config })
137
+ const outputMode = resolveOutputModeOrExit(flags)
138
+ await initCliCommand('doctor', {
139
+ debug: flags.debug,
140
+ config: flags.config,
141
+ json: outputMode === 'json',
142
+ quiet: flags.quiet,
143
+ })
120
144
  console.log('Use one of:')
121
145
  console.log(' suemo doctor embed')
122
146
  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, resolveScopeLabel } from '../shared.ts'
8
8
 
9
9
  const log = getLogger(['suemo', 'cli', 'export-import'])
10
10
 
@@ -14,27 +14,41 @@ 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),
23
31
  })
24
32
  const config = await loadConfig(process.cwd(), flags.config)
33
+ const resolvedScope = resolveScopeLabel(flags.scope, config)
34
+ log.debug('Resolved export scope', { scope: resolvedScope, explicit: flags.scope ?? null })
25
35
  const db = await connect(config.surreal)
26
36
  try {
27
37
  const activeFilter = flags.all ? 'true' : '(valid_until = NONE OR valid_until > time::now())'
28
38
  const scopeFilter = '($scope = NONE OR scope = $scope)'
29
39
 
30
- const [nodesResult, relationsResult] = await Promise.all([
40
+ const [nodesResult, relationsResult, episodesResult] = await Promise.all([
31
41
  db.query<[MemoryNode[]]>(
32
42
  `
33
43
  SELECT * FROM memory WHERE ${activeFilter} AND ${scopeFilter} ORDER BY created_at ASC
34
44
  `,
35
- { scope: flags.scope ?? null },
45
+ { scope: resolvedScope },
36
46
  ),
37
47
  db.query<[Relation[]]>('SELECT * FROM relates_to ORDER BY created_at ASC'),
48
+ db.query<[Record<string, unknown>[]]>(
49
+ `SELECT * FROM episode WHERE ($scope = NONE OR session_id = $scope) ORDER BY started_at ASC`,
50
+ { scope: resolvedScope },
51
+ ),
38
52
  ])
39
53
 
40
54
  for (const node of nodesResult[0] ?? []) {
@@ -43,6 +57,9 @@ export const exportCmd = app.sub('export')
43
57
  for (const rel of relationsResult[0] ?? []) {
44
58
  process.stdout.write(JSON.stringify({ _type: 'relation', ...rel }) + '\n')
45
59
  }
60
+ for (const ep of episodesResult[0] ?? []) {
61
+ process.stdout.write(JSON.stringify({ _type: 'episode', ...ep }) + '\n')
62
+ }
46
63
  } finally {
47
64
  await disconnect()
48
65
  }
@@ -52,8 +69,18 @@ export const exportCmd = app.sub('export')
52
69
  export const importCmd = app.sub('import')
53
70
  .meta({ description: 'Import memories from a JSONL file' })
54
71
  .args([{ name: 'file', type: 'string', required: true }])
72
+ .flags({
73
+ json: { type: 'boolean', description: 'Output JSON summary' },
74
+ pretty: { type: 'boolean', description: 'Human-readable output (default)' },
75
+ })
55
76
  .run(async ({ args, flags }) => {
56
- await initCliCommand('import', { debug: flags.debug, config: flags.config })
77
+ const outputMode = resolveOutputModeOrExit(flags)
78
+ await initCliCommand('import', {
79
+ debug: flags.debug,
80
+ config: flags.config,
81
+ json: outputMode === 'json',
82
+ quiet: flags.quiet,
83
+ })
57
84
  log.debug('Running import command', { file: args.file })
58
85
  const config = await loadConfig(process.cwd(), flags.config)
59
86
  const db = await connect(config.surreal)
@@ -61,6 +88,7 @@ export const importCmd = app.sub('import')
61
88
  const rl = createInterface({ input: createReadStream(args.file) })
62
89
  let lineNum = 0
63
90
  let imported = 0
91
+ let importedEpisode = 0
64
92
  let skipped = 0
65
93
  let errors = 0
66
94
 
@@ -111,6 +139,13 @@ export const importCmd = app.sub('import')
111
139
  },
112
140
  )
113
141
  imported++
142
+ } else if (type === 'episode') {
143
+ await db.query('UPSERT <record<episode>>$id CONTENT $row', {
144
+ id: row['id'],
145
+ row,
146
+ })
147
+ importedEpisode++
148
+ imported++
114
149
  } else {
115
150
  skipped++
116
151
  }
@@ -123,5 +158,11 @@ export const importCmd = app.sub('import')
123
158
  rl.close()
124
159
  await disconnect()
125
160
  }
126
- console.log(JSON.stringify({ imported, skipped, errors, lines: lineNum }, null, 2))
161
+ if (outputMode === 'json') {
162
+ printCliJson({ imported, importedEpisode, skipped, errors, lines: lineNum }, flags)
163
+ } else {
164
+ console.log(
165
+ `import done: imported=${imported} (episodes=${importedEpisode}) skipped=${skipped} errors=${errors} lines=${lineNum}`,
166
+ )
167
+ }
127
168
  })
@@ -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, resolveScopeLabel } from '../shared.ts'
7
7
 
8
8
  const log = getLogger(['suemo', 'cli', 'goal'])
9
9
 
@@ -17,23 +17,38 @@ 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,
26
34
  contentLength: args.content.length,
27
35
  })
28
36
  const config = await loadConfig(process.cwd(), flags.config)
37
+ const resolvedScope = resolveScopeLabel(flags.scope, config)
38
+ log.debug('Resolved goal set scope', { scope: resolvedScope, explicit: flags.scope ?? null })
29
39
  let db: Surreal | undefined
30
40
  try {
31
41
  db = await connect(config.surreal)
32
42
  const node = await goalSet(db, args.content, config, {
33
- ...(flags.scope ? { scope: flags.scope } : {}),
43
+ scope: resolvedScope,
34
44
  tags: flags.tags ? flags.tags.split(',').map((t) => t.trim()) : [],
35
45
  })
36
- console.log(JSON.stringify({ id: node.id, content: node.content }, null, 2))
46
+ if (outputMode === 'json') {
47
+ printCliJson({ id: node.id, content: node.content }, flags)
48
+ } else {
49
+ console.log(`Created goal: ${node.id}`)
50
+ console.log(` ${node.content}`)
51
+ }
37
52
  } finally {
38
53
  if (db) await disconnect()
39
54
  }
@@ -45,26 +60,45 @@ const listCmd = goal.sub('list')
45
60
  .flags({
46
61
  scope: { type: 'string', short: 's', description: 'Filter by scope' },
47
62
  resolved: { type: 'boolean', description: 'Include resolved goals', default: false },
63
+ json: { type: 'boolean', description: 'Output JSON result' },
64
+ pretty: { type: 'boolean', description: 'Human-readable output (default)' },
48
65
  })
49
66
  .run(async ({ flags }) => {
50
- await initCliCommand('goal list', { debug: flags.debug, config: flags.config })
67
+ const outputMode = resolveOutputModeOrExit(flags)
68
+ await initCliCommand('goal list', {
69
+ debug: flags.debug,
70
+ config: flags.config,
71
+ json: outputMode === 'json',
72
+ quiet: flags.quiet,
73
+ })
74
+ const config = await loadConfig(process.cwd(), flags.config)
75
+ const resolvedScope = resolveScopeLabel(flags.scope, config)
76
+ log.debug('Resolved goal list scope', { scope: resolvedScope, explicit: flags.scope ?? null })
51
77
  log.debug('Running goal list command', {
52
- hasScope: Boolean(flags.scope),
78
+ hasScope: Boolean(resolvedScope),
79
+ scope: resolvedScope,
53
80
  includeResolved: Boolean(flags.resolved),
54
81
  })
55
- const config = await loadConfig(process.cwd(), flags.config)
56
82
  let db: Surreal | undefined
57
83
  try {
58
84
  db = await connect(config.surreal)
59
85
  const goals = await goalList(db, {
60
- ...(flags.scope ? { scope: flags.scope } : {}),
86
+ scope: resolvedScope,
61
87
  includeResolved: flags.resolved,
62
88
  })
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()
89
+ if (outputMode === 'json') {
90
+ printCliJson(goals, flags)
91
+ } else {
92
+ if (goals.length === 0) {
93
+ console.log('No goals found.')
94
+ return
95
+ }
96
+ for (const g of goals) {
97
+ const status = g.valid_until ? `resolved ${g.valid_until}` : 'active'
98
+ console.log(`[${status}] ${g.id}`)
99
+ console.log(` ${g.content}`)
100
+ console.log()
101
+ }
68
102
  }
69
103
  } finally {
70
104
  if (db) await disconnect()
@@ -75,15 +109,29 @@ const listCmd = goal.sub('list')
75
109
  const resolveCmd = goal.sub('resolve')
76
110
  .meta({ description: 'Mark a goal as resolved' })
77
111
  .args([{ name: 'goalId', type: 'string', required: true }])
112
+ .flags({
113
+ json: { type: 'boolean', description: 'Output JSON result' },
114
+ pretty: { type: 'boolean', description: 'Human-readable output (default)' },
115
+ })
78
116
  .run(async ({ args, flags }) => {
79
- await initCliCommand('goal resolve', { debug: flags.debug, config: flags.config })
117
+ const outputMode = resolveOutputModeOrExit(flags)
118
+ await initCliCommand('goal resolve', {
119
+ debug: flags.debug,
120
+ config: flags.config,
121
+ json: outputMode === 'json',
122
+ quiet: flags.quiet,
123
+ })
80
124
  log.debug('Running goal resolve command', { goalId: args.goalId })
81
125
  const config = await loadConfig(process.cwd(), flags.config)
82
126
  let db: Surreal | undefined
83
127
  try {
84
128
  db = await connect(config.surreal)
85
129
  await goalResolve(db, args.goalId)
86
- console.log(`✓ Goal ${args.goalId} resolved`)
130
+ if (outputMode === 'json') {
131
+ printCliJson({ ok: true, goalId: args.goalId }, flags)
132
+ } else {
133
+ console.log(`✓ Goal ${args.goalId} resolved`)
134
+ }
87
135
  } finally {
88
136
  if (db) await disconnect()
89
137
  }