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 +54 -2
- package/package.json +5 -2
- package/src/cli/commands/believe.ts +24 -7
- package/src/cli/commands/consolidate.ts +21 -3
- package/src/cli/commands/doctor.ts +27 -3
- package/src/cli/commands/export-import.ts +47 -6
- package/src/cli/commands/goal.ts +63 -15
- package/src/cli/commands/health.ts +77 -9
- package/src/cli/commands/init.ts +101 -4
- package/src/cli/commands/observe.ts +21 -5
- package/src/cli/commands/query.ts +23 -9
- package/src/cli/commands/recall.ts +19 -3
- package/src/cli/commands/serve.ts +110 -7
- package/src/cli/commands/skill.ts +56 -0
- package/src/cli/commands/sync.ts +22 -3
- package/src/cli/commands/timeline.ts +23 -9
- package/src/cli/commands/wander.ts +19 -9
- package/src/cli/index.ts +2 -0
- package/src/cli/shared.ts +89 -2
- package/src/cognitive/consolidate.ts +21 -7
- package/src/cognitive/contradiction.ts +2 -1
- package/src/cognitive/health.ts +3 -3
- package/src/config.template.ts +3 -0
- package/src/config.ts +122 -2
- package/src/db/client.ts +3 -3
- package/src/db/preflight.ts +8 -9
- package/src/db/schema.surql +17 -0
- package/src/index.ts +3 -1
- package/src/logger.ts +23 -1
- package/src/mcp/dispatch.ts +116 -16
- package/src/mcp/server.ts +22 -11
- package/src/mcp/stdio.ts +57 -1
- package/src/memory/episode.ts +38 -6
- package/src/memory/read.ts +69 -1
- package/src/memory/write.ts +95 -13
- package/src/skill/catalog.ts +39 -0
- package/src/sync.ts +85 -0
- package/src/types.ts +13 -0
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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(
|
|
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:
|
|
39
|
+
scope: resolvedScope,
|
|
29
40
|
confidence: flags.confidence,
|
|
30
41
|
}, config)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
})
|
package/src/cli/commands/goal.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { loadConfig } from '../../config.ts'
|
|
|
3
3
|
import { connect, disconnect } from '../../db/client.ts'
|
|
4
4
|
import { goalList, goalResolve, goalSet } from '../../goal.ts'
|
|
5
5
|
import { getLogger } from '../../logger.ts'
|
|
6
|
-
import { app, initCliCommand } from '../shared.ts'
|
|
6
|
+
import { app, initCliCommand, printCliJson, resolveOutputModeOrExit, 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
|
-
|
|
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
|
-
|
|
43
|
+
scope: resolvedScope,
|
|
34
44
|
tags: flags.tags ? flags.tags.split(',').map((t) => t.trim()) : [],
|
|
35
45
|
})
|
|
36
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
86
|
+
scope: resolvedScope,
|
|
61
87
|
includeResolved: flags.resolved,
|
|
62
88
|
})
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|