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.
- package/README.md +37 -10
- package/package.json +1 -1
- package/src/cli/commands/believe.ts +22 -12
- package/src/cli/commands/consolidate.ts +18 -11
- package/src/cli/commands/doctor.ts +123 -0
- package/src/cli/commands/export-import.ts +58 -47
- package/src/cli/commands/goal.ts +52 -27
- package/src/cli/commands/health.ts +35 -17
- package/src/cli/commands/init.ts +155 -75
- package/src/cli/commands/observe.ts +25 -13
- package/src/cli/commands/query.ts +23 -7
- package/src/cli/commands/recall.ts +12 -6
- package/src/cli/commands/serve.ts +14 -5
- package/src/cli/commands/sync.ts +15 -6
- package/src/cli/commands/timeline.ts +30 -18
- package/src/cli/commands/wander.ts +27 -16
- package/src/cli/index.ts +3 -4
- package/src/cli/shared.ts +34 -0
- package/src/cognitive/consolidate.ts +48 -19
- package/src/cognitive/contradiction.ts +19 -7
- package/src/config.template.ts +36 -0
- package/src/config.ts +41 -12
- package/src/db/preflight.ts +32 -6
- package/src/db/schema.surql +11 -8
- package/src/db/schema.ts +6 -3
- package/src/embedding/index.ts +52 -0
- package/src/embedding/openai-compatible.ts +43 -0
- package/src/goal.ts +3 -1
- package/src/mcp/dispatch.ts +134 -0
- package/src/mcp/server.ts +25 -2
- package/src/mcp/stdio.ts +314 -0
- package/src/mcp/tools.ts +8 -89
- package/src/memory/read.ts +74 -19
- package/src/memory/write.ts +54 -18
- package/src/cli/commands/shared.ts +0 -20
package/src/cli/commands/init.ts
CHANGED
|
@@ -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 {
|
|
8
|
-
import { app } from '../shared.ts'
|
|
8
|
+
import { getLogger } from '../../logger.ts'
|
|
9
|
+
import { app, initCliCommand } from '../shared.ts'
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
120
|
+
force: { type: 'boolean', description: 'Overwrite existing config file' },
|
|
14
121
|
})
|
|
15
122
|
.run(async ({ flags }) => {
|
|
16
|
-
await
|
|
123
|
+
await initCliCommand('init config', { debug: flags.debug, config: flags.config })
|
|
124
|
+
writeConfig(Boolean(flags.force), configOutputPath(flags))
|
|
125
|
+
})
|
|
17
126
|
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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 {
|
|
3
|
+
import { getLogger } from '../../logger.ts'
|
|
4
4
|
import { query } from '../../memory/read.ts'
|
|
5
|
-
import {
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 {
|
|
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
|
|
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
|
})
|
package/src/cli/commands/sync.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { loadConfig } from '../../config.ts'
|
|
2
2
|
import { connect, disconnect } from '../../db/client.ts'
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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 '
|
|
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' })
|