suemo 0.0.5 → 0.0.7

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
@@ -5,7 +5,9 @@
5
5
  > [!CAUTION]
6
6
  > **Bun-only.** This project will not run on Node.js and there are no plans to support it.
7
7
  >
8
- > suemo is experimental software built for the author's personal agent infrastructure. APIs may change without notice. Use at your own risk.
8
+ > suemo is experimental software built for the author's personal agent infrastructure. APIs may change without notice.
9
+ >
10
+ > Most of the code is AI-generated and only tested briefly by a single person. Use at your own risk.
9
11
 
10
12
  suemo gives AI agents a memory that survives across sessions, models, and runtimes. Write observations from a Telegram bot, query them from OpenCode, consolidate overnight — all agents share one source of truth in SurrealDB.
11
13
 
@@ -17,7 +19,7 @@ suemo gives AI agents a memory that survives across sessions, models, and runtim
17
19
  - **Bi-temporal nodes** — every fact has `valid_from` / `valid_until`; nothing is hard-deleted
18
20
  - **Contradiction detection** — calling `believe()` with a conflicting belief automatically invalidates the old one and links them with a `contradicts` edge
19
21
  - **Two-phase consolidation** — NREM clusters and compresses redundant observations via LLM; REM integrates new summaries into the broader graph with auto-scored relations
20
- - **SurrealKV time-travel** — `VERSION d'...'` queries let you inspect any node's state at any past datetime (requires `SURREAL_DATASTORE_RETENTION=90d`)
22
+ - **SurrealKV time-travel** — `VERSION d'...'` queries let you inspect any node's state at any past datetime (requires `SURREAL_DATASTORE_RETENTION=90d` / `0` for infinite)
21
23
  - **Namespace isolation** — one SurrealDB instance, multiple agents, zero collision (`namespace` = agent group, `database` = agent identity)
22
24
  - **MCP + CLI** — both interfaces are thin shells over the same domain functions; no business logic duplication
23
25
  - **Lightweight** — no bundled vector store, no embedded database process, no OpenAI SDK; embedding via `fn::embed()` runs server-side in SurrealDB
@@ -34,12 +36,25 @@ suemo gives AI agents a memory that survives across sessions, models, and runtim
34
36
  SurrealDB must be started with SurrealKV and a retention window:
35
37
 
36
38
  ```sh
37
- SURREAL_DATASTORE_RETENTION=90d surreal start \
39
+ SURREAL_DATASTORE_VERSIONED=true SURREAL_DATASTORE_RETENTION=90d surreal start \
38
40
  --bind 0.0.0.0:8000 \
39
- surrealkv:///path/to/data
41
+ --allow-funcs "*" \
42
+ -- surrealkv:///path/to/data
40
43
  ```
41
44
 
42
- An embedding model must be configured in SurrealDB for `fn::embed()` to work.
45
+ For suemo, the critical capability is allowing custom functions:
46
+
47
+ - `fn::*`
48
+ - `time::*`
49
+ - `vector::*`
50
+ - `search::*` (for `search::score`)
51
+ - `math::*` (for `math::mean`, `math::min`)
52
+ - `rand::*` (used in `wander` query)
53
+
54
+ If you run with strict capability mode, use an allowlist like:
55
+
56
+ - for **openai-compatible/stub**: `fn,time,vector,search,math,rand`
57
+ - for **surreal** provider (with `ml::...` in `fn::embed` wrapper): add `ml`
43
58
 
44
59
  ---
45
60
 
@@ -64,6 +79,69 @@ bun run src/cli/index.ts init config
64
79
 
65
80
  This writes `~/.suemo/suemo.ts`. Edit it with your SurrealDB URL, credentials, and LLM endpoint.
66
81
 
82
+ Set project scope discovery in config (recommended):
83
+
84
+ ```ts
85
+ export default defineConfig({
86
+ main: {
87
+ projectDir: '.ua',
88
+ },
89
+ // ...
90
+ })
91
+ ```
92
+
93
+ With this, suemo resolves default scope from nearest `.ua/suemo.json` (auto-created if missing).
94
+
95
+ ### Optional: one-command local service setup (Arch Linux)
96
+
97
+ **NOTE:** These init commands are Arch Linux–specific (author's primary development environment). They rely on `pacman` package checks and systemd paths matching Arch Linux layouts.
98
+
99
+ For local host deployments, suemo can install systemd service assets directly.
100
+
101
+ These commands **must be run as root**:
102
+
103
+ ```sh
104
+ sudo suemo init surreal 2gb --dry-run
105
+ sudo suemo init surreal 6gb --dry-run
106
+ sudo suemo init fastembed --dry-run
107
+ ```
108
+
109
+ If `suemo` is not on root PATH, use `sudo bunx suemo ...`.
110
+
111
+ Drop `--dry-run` to apply changes.
112
+
113
+ `suemo init surreal` also creates `/opt/suemo/surrealdb/local.env` with commented overrides and does not overwrite it if it already exists. If defined in `local.env`, these values override `common.env`:
114
+
115
+ - `SURREAL_USER`
116
+ - `SURREAL_PASS`
117
+ - `SURREAL_BIND`
118
+ - `SURREAL_PATH`
119
+
120
+ Add `--force` to regenerate managed env files (`common.env` and profile env).
121
+
122
+ What these commands do:
123
+
124
+ - `suemo init surreal <2gb|6gb>`
125
+ - checks `surrealdb` package via `pacman -Q`
126
+ - creates `/opt/suemo/surrealdb/common.env` + profile env (`2gb.env` or `6gb.env`)
127
+ - creates `/opt/suemo/surrealdb/local.env` once (user override file; not overwritten)
128
+ - writes `/etc/systemd/system/suemo-surrealdb@.service`
129
+ - enables + starts `suemo-surrealdb@<profile>.service`
130
+ - includes `VERSIONED` + retention config and strict capability allowlist:
131
+ - `SURREAL_DATASTORE_VERSIONED=true`
132
+ - `SURREAL_DATASTORE_RETENTION=90d`
133
+ - `SURREAL_CAPS_DENY_ALL=true`
134
+ - `SURREAL_CAPS_ALLOW_FUNC=fn,time,vector,search,math,rand,ml`
135
+
136
+ - `suemo init fastembed`
137
+ - checks `python-fastembed`, `python-fastapi`, `python-uvicorn` via `pacman -Q`
138
+ - installs `data/fastembed-server.py` to `/opt/suemo/fastembed-server.py`
139
+ - creates `/opt/fastembed/local.env` once (user override file; not overwritten)
140
+ - writes `/etc/systemd/system/suemo-fastembed.service`
141
+ - enables + starts `suemo-fastembed.service`
142
+
143
+ `--dry-run` prints all generated file content and planned commands to stdout without writing anything.
144
+
67
145
  **2. Apply schema**
68
146
 
69
147
  Apply schema after you set/edit namespace/database:
@@ -106,6 +184,30 @@ suemo serve --dev
106
184
 
107
185
  ---
108
186
 
187
+ ## OpenCode setup (only integration target)
188
+
189
+ Print copy-paste setup snippets:
190
+
191
+ ```sh
192
+ suemo init opencode
193
+ ```
194
+
195
+ This prints:
196
+
197
+ - MCP config snippet (`command: suemo`, `args: ["serve", "--stdio", ...]`)
198
+ - minimal AGENTS.md guidance snippet
199
+
200
+ To fetch the latest skill docs directly from your local checkout:
201
+
202
+ ```sh
203
+ suemo skill
204
+ suemo skill core-workflow
205
+ ```
206
+
207
+ No files are auto-written by this command.
208
+
209
+ ---
210
+
109
211
  ## CLI Reference
110
212
 
111
213
  ```
@@ -115,10 +217,13 @@ Global flags (inherited by all commands):
115
217
  -c, --config <path> Path to config file
116
218
  -d, --debug Verbose debug logging
117
219
 
118
- Commands:
119
- init Show init subcommands and usage guidance
120
- init config Create/update ~/.suemo/suemo.ts
121
- init schema Apply DB schema from current config (with confirm)
220
+ Commands:
221
+ init Show init subcommands and usage guidance
222
+ init config Create/update ~/.suemo/suemo.ts
223
+ init schema Apply DB schema from current config (with confirm)
224
+ init surreal Install systemd SurrealDB profile (2gb/6gb) with VERSIONED + allowlist config
225
+ init fastembed Install systemd fastembed service
226
+ skill Print suemo skill docs (or specific reference)
122
227
  serve Start the MCP server (HTTP or stdio)
123
228
  observe <content> Store an observation
124
229
  believe <content> Store a belief (triggers contradiction detection)
@@ -277,12 +382,16 @@ This prints your active target (`url`, `namespace`, `database`) and step-by-step
277
382
  | `recall` | Fetch a node and its 1-hop neighbourhood; ticks FSRS |
278
383
  | `wander` | Spreading-activation walk through the graph |
279
384
  | `timeline` | Chronological memory slice with optional date range |
385
+ | `context` | Recover recent session context for current/default scope |
280
386
  | `episode_start` | Begin a bounded session window |
281
387
  | `episode_end` | Close a session, optionally with a summary |
282
388
  | `goal_set` | Create a goal node |
283
389
  | `goal_resolve` | Mark a goal achieved |
284
390
  | `goal_list` | List active (or all) goals |
285
391
  | `upsert_by_key` | Upsert a memory node by stable topic key |
392
+ | `update` | Update an existing memory node by ID (re-embeds if content changed) |
393
+ | `suggest_topic_key` | Suggest deterministic canonical topic key from free text |
394
+ | `skill` | Return current suemo skill docs or one named reference |
286
395
  | `capture_prompt` | Capture raw prompt and link derived observations |
287
396
  | `session_context_get` | Fetch open episode summary/context by session ID |
288
397
  | `session_context_set` | Update open episode summary/context by session ID |
@@ -293,6 +402,13 @@ This prints your active target (`url`, `namespace`, `database`) and step-by-step
293
402
 
294
403
  Agents never supply temporal fields (`valid_from`, `valid_until`). These are system-managed.
295
404
 
405
+ ### Scope and longevity notes
406
+
407
+ - Default inferred project scope now uses nearest `<projectDir>/suemo.json` with `main.projectDir` defaulting to `.ua`.
408
+ - Semantic operations are scope-aware (dedup, contradiction detection, upsert-by-key, consolidation).
409
+ - Retention probe is fail-closed on unexpected probe errors.
410
+ - Episode records are included in sync and export/import flows.
411
+
296
412
  ---
297
413
 
298
414
  ## Architecture
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suemo",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Persistent semantic memory for AI agents — backed by SurrealDB.",
5
5
  "author": {
6
6
  "name": "Umar Alfarouk",
@@ -37,13 +37,16 @@
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",
44
- "@crustjs/plugins": "^0.0.19",
45
- "@crustjs/prompts": "^0.0.9",
46
- "@crustjs/style": "^0.0.5",
47
+ "@crustjs/plugins": "^0.0.20",
48
+ "@crustjs/prompts": "^0.0.10",
49
+ "@crustjs/style": "^0.0.6",
47
50
  "@logtape/file": "^2.0.4",
48
51
  "@logtape/logtape": "^2.0.4",
49
52
  "@surrealdb/node": "^3.0.3",
@@ -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, printCliJson, resolveOutputModeOrExit } 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
 
@@ -24,16 +24,19 @@ export const believeCmd = app.sub('believe')
24
24
  quiet: flags.quiet,
25
25
  })
26
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 })
27
29
  const db = await connect(config.surreal)
28
30
  try {
29
31
  log.debug('Running believe command', {
30
- hasScope: Boolean(flags.scope),
32
+ hasScope: Boolean(resolvedScope),
33
+ scope: resolvedScope,
31
34
  confidence: flags.confidence,
32
35
  contentLength: args.content.length,
33
36
  })
34
37
  const { node, contradicted } = await believe(db, {
35
38
  content: args.content,
36
- scope: flags.scope,
39
+ scope: resolvedScope,
37
40
  confidence: flags.confidence,
38
41
  }, config)
39
42
  if (outputMode === 'json') {
@@ -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, printCliJson, resolveOutputModeOrExit } 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,6 +10,7 @@ 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)' },
13
14
  json: { type: 'boolean', description: 'Output full JSON' },
14
15
  pretty: { type: 'boolean', description: 'Human-readable output (default)' },
15
16
  })
@@ -23,6 +24,7 @@ export const consolidateCmd = app.sub('consolidate')
23
24
  })
24
25
  log.debug('Running consolidate command', { nremOnly: flags['nrem-only'] })
25
26
  const config = await loadConfig(process.cwd(), flags.config)
27
+ const scope = resolveScopeLabel(flags.scope, config)
26
28
  const db = await connect(config.surreal)
27
29
  try {
28
30
  const run = await consolidate(db, {
@@ -31,6 +33,7 @@ export const consolidateCmd = app.sub('consolidate')
31
33
  remRelationThreshold: config.consolidation.remRelationThreshold,
32
34
  llm: config.consolidation.llm,
33
35
  embedding: config.embedding,
36
+ ...(scope ? { scope } : {}),
34
37
  })
35
38
  if (outputMode === 'json') {
36
39
  printCliJson(run, flags)
@@ -104,6 +104,8 @@ const doctorEmbedCmd = doctor.sub('embed')
104
104
  console.log('\nHow to set up fn::embed() (step-by-step):')
105
105
  console.log('\n1) Ensure SurrealDB CLI exposes ML commands:')
106
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::*" ...')
107
109
  console.log('\n2) Import a .surml embedding model into this exact NS/DB:')
108
110
  console.log(
109
111
  ` surreal ml import --conn ${endpoint} --user <USER> --pass <PASS> --ns ${config.surreal.namespace} --db ${config.surreal.database} path/to/your-model.surml`,
@@ -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, printCliJson, resolveOutputModeOrExit } 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
 
@@ -30,19 +30,25 @@ export const exportCmd = app.sub('export')
30
30
  includeInvalidated: Boolean(flags.all),
31
31
  })
32
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 })
33
35
  const db = await connect(config.surreal)
34
36
  try {
35
37
  const activeFilter = flags.all ? 'true' : '(valid_until = NONE OR valid_until > time::now())'
36
38
  const scopeFilter = '($scope = NONE OR scope = $scope)'
37
39
 
38
- const [nodesResult, relationsResult] = await Promise.all([
40
+ const [nodesResult, relationsResult, episodesResult] = await Promise.all([
39
41
  db.query<[MemoryNode[]]>(
40
42
  `
41
43
  SELECT * FROM memory WHERE ${activeFilter} AND ${scopeFilter} ORDER BY created_at ASC
42
44
  `,
43
- { scope: flags.scope ?? null },
45
+ { scope: resolvedScope },
44
46
  ),
45
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
+ ),
46
52
  ])
47
53
 
48
54
  for (const node of nodesResult[0] ?? []) {
@@ -51,6 +57,9 @@ export const exportCmd = app.sub('export')
51
57
  for (const rel of relationsResult[0] ?? []) {
52
58
  process.stdout.write(JSON.stringify({ _type: 'relation', ...rel }) + '\n')
53
59
  }
60
+ for (const ep of episodesResult[0] ?? []) {
61
+ process.stdout.write(JSON.stringify({ _type: 'episode', ...ep }) + '\n')
62
+ }
54
63
  } finally {
55
64
  await disconnect()
56
65
  }
@@ -79,6 +88,7 @@ export const importCmd = app.sub('import')
79
88
  const rl = createInterface({ input: createReadStream(args.file) })
80
89
  let lineNum = 0
81
90
  let imported = 0
91
+ let importedEpisode = 0
82
92
  let skipped = 0
83
93
  let errors = 0
84
94
 
@@ -129,6 +139,13 @@ export const importCmd = app.sub('import')
129
139
  },
130
140
  )
131
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++
132
149
  } else {
133
150
  skipped++
134
151
  }
@@ -142,8 +159,10 @@ export const importCmd = app.sub('import')
142
159
  await disconnect()
143
160
  }
144
161
  if (outputMode === 'json') {
145
- printCliJson({ imported, skipped, errors, lines: lineNum }, flags)
162
+ printCliJson({ imported, importedEpisode, skipped, errors, lines: lineNum }, flags)
146
163
  } else {
147
- console.log(`import done: imported=${imported} skipped=${skipped} errors=${errors} lines=${lineNum}`)
164
+ console.log(
165
+ `import done: imported=${imported} (episodes=${importedEpisode}) skipped=${skipped} errors=${errors} lines=${lineNum}`,
166
+ )
148
167
  }
149
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, printCliJson, resolveOutputModeOrExit } 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
 
@@ -34,11 +34,13 @@ const setCmd = goal.sub('set')
34
34
  contentLength: args.content.length,
35
35
  })
36
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 })
37
39
  let db: Surreal | undefined
38
40
  try {
39
41
  db = await connect(config.surreal)
40
42
  const node = await goalSet(db, args.content, config, {
41
- ...(flags.scope ? { scope: flags.scope } : {}),
43
+ scope: resolvedScope,
42
44
  tags: flags.tags ? flags.tags.split(',').map((t) => t.trim()) : [],
43
45
  })
44
46
  if (outputMode === 'json') {
@@ -69,16 +71,19 @@ const listCmd = goal.sub('list')
69
71
  json: outputMode === 'json',
70
72
  quiet: flags.quiet,
71
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 })
72
77
  log.debug('Running goal list command', {
73
- hasScope: Boolean(flags.scope),
78
+ hasScope: Boolean(resolvedScope),
79
+ scope: resolvedScope,
74
80
  includeResolved: Boolean(flags.resolved),
75
81
  })
76
- const config = await loadConfig(process.cwd(), flags.config)
77
82
  let db: Surreal | undefined
78
83
  try {
79
84
  db = await connect(config.surreal)
80
85
  const goals = await goalList(db, {
81
- ...(flags.scope ? { scope: flags.scope } : {}),
86
+ scope: resolvedScope,
82
87
  includeResolved: flags.resolved,
83
88
  })
84
89
  if (outputMode === 'json') {