suemo 0.1.6 → 0.1.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
@@ -11,6 +11,8 @@
11
11
 
12
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.
13
13
 
14
+ Canonical behavioral specification: **[`SPEC.md`](./SPEC.md)**.
15
+
14
16
  ---
15
17
 
16
18
  ## Features
@@ -445,6 +447,27 @@ This prints your active target (`url`, `namespace`, `database`) and step-by-step
445
447
 
446
448
  Agents never supply temporal fields (`valid_from`, `valid_until`). These are system-managed.
447
449
 
450
+ ### Memory field semantics (quick reference)
451
+
452
+ - `content`: canonical memory payload.
453
+ - `summary`: optional condensed text for compact retrieval/consolidation. It does **not** replace `content`.
454
+ - `source`: optional provenance label (free-form string), e.g. `prompt-capture`, `consolidation:nrem`.
455
+ - `prompt_source`: optional pointer to the prompt memory record that originated a derived memory.
456
+ - `fsrs_next_review`: optional review timestamp; currently set on `recall()` (not on every ingest).
457
+
458
+ ### Episode `memory_ids`
459
+
460
+ `episode.memory_ids` stores memory IDs attached to an open session, used for session reconstruction.
461
+
462
+ If you rarely see it populated, the typical reason is missing `sessionId` on write calls.
463
+ Current behavior: only write paths invoked with `sessionId` append to `memory_ids`.
464
+ `believe` now supports session linkage too:
465
+
466
+ - CLI: `suemo believe "..." --session <sessionId>`
467
+ - MCP: `believe({ content, sessionId, ... })`
468
+
469
+ See `SPEC.md` for full normative semantics and hardening targets.
470
+
448
471
  ### Scope and longevity notes
449
472
 
450
473
  - Default inferred project scope now uses nearest `<projectDir>/suemo.json` with `main.projectDir` defaulting to `.ua`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suemo",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Persistent semantic memory for AI agents — backed by SurrealDB.",
5
5
  "author": {
6
6
  "name": "Umar Alfarouk",
@@ -3,13 +3,38 @@ name: suemo
3
3
  description: OpenCode-focused persistent memory workflow for suemo with CLI/MCP parity and versioned references.
4
4
  license: GPL-3.0-only
5
5
  compatibility: opencode
6
- version: 0.1.6
6
+ version: 0.1.7
7
7
  ---
8
8
 
9
9
  # suemo skill
10
10
 
11
11
  Use suemo to persist technical context across sessions with minimal, project-scoped memory.
12
12
 
13
+ ## Strict defaults (v0.1.7)
14
+
15
+ - Always run pre-checks before implementation:
16
+ - `goal_list({ scope })`
17
+ - `query({ input: "recent work on <topic>", scope })`
18
+ - `context({ scope, limit: 20 })`
19
+ - `observe.kind` accepts only: `observation | belief | question | hypothesis | goal`
20
+ - For non-trivial writes, prefer structured content:
21
+ - `**What**`, `**Why**`, `**Where**`, `**Learned**`
22
+ - End meaningful tasks with:
23
+ - `episode_end({ sessionId, summary, goal, discoveries, accomplished, files_changed })`
24
+ - `goal_resolve({ goalId })` when done
25
+ - `query({ input: "most recent observations", scope })`
26
+ - `health()`
27
+
28
+ ## OpenCode plugin support
29
+
30
+ `suemo init opencode` now installs three things:
31
+
32
+ 1. MCP config entry for suemo
33
+ 2. AGENTS.md suemo block
34
+ 3. OpenCode plugin at `~/.config/opencode/plugins/suemo.ts`
35
+
36
+ The plugin injects strict suemo protocol into system prompt and adds compaction checkpoint reminders.
37
+
13
38
  ## Quick use
14
39
 
15
40
  - CLI latest skill: `suemo skill`
@@ -1,17 +1,25 @@
1
1
  ---
2
2
  name: agents-snippet
3
3
  description: AGENTS.md snippet optimized for suemo skill discovery and usage.
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  ---
6
6
 
7
7
  # AGENTS.md snippet
8
8
 
9
9
  ```md
10
- ## suemo-first memory workflow
10
+ ## suemo-first memory workflow (strict)
11
11
 
12
12
  Before significant work:
13
13
 
14
- - query("what do I know about <topic>")
14
+ - suemo_goal_list({ scope: "<active-scope>" })
15
+ - suemo_query({ input: "recent work on <topic>", scope: "<active-scope>" })
16
+ - suemo_context({ scope: "<active-scope>", limit: 20 })
17
+ - suemo_episode_start({ sessionId: "<session-id>" }) for new tasks
18
+
19
+ Schema guardrails:
20
+
21
+ - suemo_observe.kind must be one of: observation | belief | question | hypothesis | goal
22
+ - Never use discovery/architecture/bugfix as kind
15
23
 
16
24
  If you need latest usage docs:
17
25
 
@@ -21,16 +29,19 @@ If you need latest usage docs:
21
29
 
22
30
  During work:
23
31
 
24
- - observe("...") for concrete facts
25
- - believe("...") for conclusions
26
- - goal_set("...") for long tasks
32
+ - suemo_observe({ content: "**What**: ...\n**Why**: ...\n**Where**: ...\n**Learned**: ...", kind: "observation" })
33
+ - suemo_believe("...") for uncertain interpretations
34
+ - suemo_goal_set("...") for long tasks
27
35
 
28
36
  After completion:
29
37
 
30
- - goal_resolve("...") if applicable
31
- - episode_end(sessionId, summary="...")
38
+ - suemo_episode_end({ sessionId, summary, goal, discoveries, accomplished, files_changed })
39
+ - suemo_goal_resolve({ goalId }) if applicable
40
+ - suemo_query({ input: "most recent observations", scope: "<active-scope>" })
41
+ - suemo_health()
32
42
 
33
43
  After compaction/reset:
34
44
 
35
- - context({ scope: "<project-id>", limit: 20 })
45
+ - suemo_session_context_set({ sessionId, summary: "<compacted summary>" })
46
+ - suemo_context({ scope: "<project-id>", limit: 20 })
36
47
  ```
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: cli-reference
3
3
  description: CLI command reference for suemo v0.0.6 including skill access.
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  ---
6
6
 
7
7
  # CLI reference
@@ -14,10 +14,15 @@ Core:
14
14
  - `suemo serve [--stdio|--dev]`
15
15
  - `suemo skill [reference]`
16
16
 
17
+ Aliases (deprecated, still accepted with warning):
18
+
19
+ - `suemo skills` → use `suemo skill`
20
+ - `suemo init surrealdb` → use `suemo init surreal`
21
+
17
22
  Memory:
18
23
 
19
24
  - `suemo observe <content>`
20
- - `suemo believe <content>`
25
+ - `suemo believe <content> [--session <sessionId>]`
21
26
  - `suemo query <input>`
22
27
  - `suemo recall <nodeId>`
23
28
  - `suemo wander`
@@ -1,40 +1,68 @@
1
1
  ---
2
2
  name: core-workflow
3
3
  description: Canonical suemo operating loop for OpenCode agents.
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  ---
6
6
 
7
7
  # Core workflow
8
8
 
9
- 1. Start with memory lookup:
9
+ 1. Start with strict pre-check:
10
10
 
11
11
  ```ts
12
- query('what do I know about <topic>')
12
+ goal_list({ scope: '<project-scope>' })
13
+ query({ input: 'recent work on <topic>', scope: '<project-scope>' })
14
+ context({ scope: '<project-scope>', limit: 20 })
15
+ episode_start({ sessionId: '<session-id>' }) // when beginning new task
13
16
  ```
14
17
 
15
18
  2. During work, persist concrete discoveries:
16
19
 
17
20
  ```ts
18
- observe('...')
19
- believe('...')
21
+ observe({
22
+ content: '**What**: ...\n**Why**: ...\n**Where**: ...\n**Learned**: ...',
23
+ kind: 'observation',
24
+ scope: '<project-scope>',
25
+ })
26
+ believe({ content: '...', scope: '<project-scope>' })
20
27
  ```
21
28
 
22
- 3. For long tasks:
29
+ 3. Kind schema is strict:
23
30
 
24
31
  ```ts
25
- goal_set('...')
32
+ // valid only:
33
+ // observation | belief | question | hypothesis | goal
26
34
  ```
27
35
 
28
- 4. If compaction/reset happened:
36
+ 4. For long tasks:
29
37
 
30
38
  ```ts
39
+ goal_set({ content: '...', scope: '<project-scope>' })
40
+ ```
41
+
42
+ 5. If compaction/reset happened:
43
+
44
+ ```ts
45
+ session_context_set({
46
+ sessionId: '<session-id>',
47
+ summary: '<compacted summary>',
48
+ })
31
49
  context({ scope: '<project-id>', limit: 20 })
32
50
  ```
33
51
 
34
- 5. Close session:
52
+ 6. Close session with structured fields:
35
53
 
36
54
  ```ts
37
- episode_end(sessionId, summary = '...')
55
+ episode_end({
56
+ sessionId: '<session-id>',
57
+ summary: '...',
58
+ goal: '...',
59
+ discoveries: ['...'],
60
+ accomplished: ['...'],
61
+ files_changed: ['path/to/file'],
62
+ })
63
+ goal_resolve({ goalId: '<goal-id>' }) // if completed
64
+ query({ input: 'most recent observations', scope: '<project-scope>' })
65
+ health()
38
66
  ```
39
67
 
40
68
  Prefer project-scoped memory. Let suemo infer scope from nearest `{projectDir}/suemo.json` (recommended `main.projectDir = '.ua'`).
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: manual-test-plan
3
3
  description: Comprehensive manual test matrix for suemo features and commands.
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  ---
6
6
 
7
7
  # Manual test plan
@@ -35,10 +35,11 @@ Expected:
35
35
 
36
36
  1. `episode_start`
37
37
  2. add observations with `sessionId`
38
- 3. `session_context_set`
39
- 4. `context`
40
- 5. `episode_end` with structured fields
41
- 6. verify episode fields persisted
38
+ 3. add belief with `sessionId` (`suemo believe "..." --session <id>` or MCP `believe({ sessionId })`)
39
+ 4. `session_context_set`
40
+ 5. `context`
41
+ 6. `episode_end` with structured fields
42
+ 7. verify episode fields persisted
42
43
 
43
44
  ## E. Goals
44
45
 
@@ -66,7 +67,17 @@ Expected:
66
67
  2. `suemo skill core-workflow`
67
68
  3. MCP `skill({})`, `skill({ reference: "cli-reference" })`
68
69
 
69
- ## I. Output modes
70
+ ## I. OpenCode init + plugin install
71
+
72
+ 1. `suemo init opencode --dry-run`
73
+ 2. `suemo init opencode`
74
+ 3. Verify files:
75
+ - `~/.config/opencode/opencode.jsonc` or `.json` contains `mcp.suemo`
76
+ - `~/.config/opencode/AGENTS.md` has `<!-- SUEMO:START --> ... <!-- SUEMO:END -->`
77
+ - `~/.config/opencode/plugins/suemo.ts` exists
78
+ 4. Validate plugin appears under OpenCode loaded plugins
79
+
80
+ ## J. Output modes
70
81
 
71
82
  Repeat representative commands under:
72
83
 
@@ -1,11 +1,17 @@
1
1
  ---
2
2
  name: mcp-reference
3
3
  description: MCP tool reference for suemo v0.0.6.
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  ---
6
6
 
7
7
  # MCP tools
8
8
 
9
+ Strictness notes (v0.1.7):
10
+
11
+ - `observe.kind` is strict enum: `observation | belief | question | hypothesis | goal`
12
+ - `episode_end` supports optional structured fields: `summary`, `goal`, `discoveries`, `accomplished`, `files_changed`
13
+ - For reliable agent adoption, pair `episode_end` with `goal_resolve` and post-check `query("most recent observations")`
14
+
9
15
  Write/mutate:
10
16
 
11
17
  - `observe`
@@ -42,5 +48,7 @@ Maintenance:
42
48
  Notes:
43
49
 
44
50
  - Optional `scope` defaults to inferred project id.
51
+ - `believe` accepts optional `sessionId` to attach created belief memories to an open episode.
45
52
  - `update` re-embeds when `content` changes.
46
53
  - `episode_end` accepts structured fields (`goal`, `discoveries`, `accomplished`, `files_changed`).
54
+ - `skill` returns inline references; use `skill({ reference: "core-workflow" })` for canonical loop.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: schema-retention-longevity
3
3
  description: Long-term schema and retention design expectations for suemo.
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  ---
6
6
 
7
7
  # Schema + retention longevity
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: sync-local-vps
3
3
  description: Two-way sync manual scenario for local and VPS SurrealDB.
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  ---
6
6
 
7
7
  # Local ↔ VPS sync scenario
package/src/AGENTS.md CHANGED
@@ -1,150 +1,130 @@
1
- # Working with suemo
1
+ **CRITICAL:** This protocol is mandatory. Start each new task with the checklist below. End each completed task with `suemo_episode_end` + `suemo_goal_resolve` (if applicable). Do not skip.
2
2
 
3
- suemo is your persistent memory. Use it to store what you learn, remember what matters, and build on past work.
3
+ ## 1) Pre-Session Checklist (required)
4
4
 
5
- ## Before anything else
5
+ Before implementation:
6
6
 
7
- **Load the skill doc first.** It is versioned and may contain updated workflows, references, and tool signatures that supersede this file.
7
+ - [ ] `suemo_goal_list({ scope: "<active-scope>" })`
8
+ - [ ] `suemo_query({ input: "recent work on <topic>", scope: "<active-scope>" })`
9
+ - [ ] `suemo_context({ scope: "<active-scope>", limit: 20 })`
10
+ - [ ] Resolve abandoned goals if any
11
+ - [ ] `suemo_episode_start({ sessionId: "<session-id>" })` for new tasks
8
12
 
9
- - CLI: `suemo skill`
10
- - MCP: `skill({})`
13
+ Proceed only after all checks pass.
11
14
 
12
- For a specific reference: `suemo skill <reference-name>` / `skill({ reference: "<reference-name>" })`
15
+ ---
13
16
 
14
- Available references: `agents-snippet`, `cli-reference`, `core-workflow`, `manual-test-plan`, `mcp-reference`, `schema-retention-longevity`, `sync-local-vps`
17
+ ## 2) Strict schema rules (required)
15
18
 
16
- Do not proceed until the skill doc is loaded.
19
+ ### `suemo_observe` kind is strict
17
20
 
18
- ## Session protocol
21
+ Valid `kind` values only:
19
22
 
20
- These steps are **mandatory**, not suggested.
23
+ - `observation`
24
+ - `belief`
25
+ - `question`
26
+ - `hypothesis`
27
+ - `goal`
21
28
 
22
- ### On session start
29
+ ### `suemo_episode_end` payload shape
23
30
 
24
- ```
25
- goal_list(scope: "<active-scope>")
26
- query("recent work on [topic]", scope: "<active-scope>")
27
- ```
31
+ Minimum schema requirement: `sessionId`.
28
32
 
29
- Read relevant results before touching any code. Do not re-derive what is already stored.
33
+ Required by protocol when finishing meaningful work:
30
34
 
31
- ### While working
35
+ - `summary`
36
+ - `goal`
37
+ - `discoveries` (array)
38
+ - `accomplished` (array)
39
+ - `files_changed` (array)
32
40
 
33
- Store anything that would save time next session:
41
+ Use exact field names above.
34
42
 
35
- ```
36
- observe("[fact you learned or confirmed]")
37
- believe("[interpretation or conclusion that might change]")
38
- ```
43
+ ---
39
44
 
40
- Query only when you are **genuinely blocked** or starting a distinct new subtask. Do not query on every step — it burns context and adds noise.
45
+ ## 3) During work
41
46
 
42
- ### Before ending (or if interrupted)
47
+ Persist important information immediately (not only at the end).
43
48
 
44
- Call this **before** your final tool invocation, not after — sessions can be cut off without warning:
49
+ Use this structure for non-trivial observations:
45
50
 
51
+ ```md
52
+ **What**: <what changed>
53
+ **Why**: <reason>
54
+ **Where**: <files/paths>
55
+ **Learned**: <gotchas/decisions>
46
56
  ```
47
- episode_end(<session_id>, summary="<one sentence of what was accomplished>")
48
- goal_resolve(<goal_id>) # only if the goal is fully done
49
- ```
50
-
51
- If the session is interrupted and you never reach this step, that is acceptable. Incomplete episodes are better than missing observations mid-session.
52
-
53
- ## observe vs believe
54
57
 
55
- Use `observe()` for facts. Use `believe()` for interpretations that could be wrong or change.
58
+ Guidance:
56
59
 
57
- **Observe:** `"[tool] requires [flag] — without it, [failure mode]"`\
58
- **Believe:** `"[tool] behaviour is likely a bug — worth retrying after an upgrade"`
60
+ - Facts `suemo_observe`
61
+ - Interpretations/uncertainty `suemo_believe`
62
+ - Longer tasks → `suemo_goal_set`
59
63
 
60
- Beliefs trigger contradiction detection. If you later believe the opposite, the old belief is invalidated and both are linked — useful for tracing why you changed your mind.
64
+ ---
61
65
 
62
- For uncertain things, prefer `kind: "question"` or `kind: "hypothesis"` over stating them as facts.
66
+ ## 4) After completion
63
67
 
64
- ## What to observe
68
+ Run in order:
65
69
 
66
- Observe your **experience**, not documentation:
70
+ 1. `suemo_episode_end({ sessionId, summary, goal, discoveries, accomplished, files_changed })`
71
+ 2. `suemo_goal_resolve({ goalId })` when done
72
+ 3. `suemo_query({ input: "most recent observations", scope })`
73
+ 4. `suemo_health()`
67
74
 
68
- - Approaches tried and whether they worked
69
- - Config or flags that took effort to figure out
70
- - Errors encountered and how they were resolved
71
- - Decisions made and the reasoning behind them
72
- - Constraints or preferences the user stated
75
+ ---
73
76
 
74
- **Good:** `"[library] [method] runs before [other step] — caused [bug] in [context]"`\
75
- **Skip:** `"[library] supports [feature]"` ← already in the docs
77
+ ## 5) Querying best practices
76
78
 
77
- ## What not to store
78
-
79
- - Transient state: current cursor position, open file, in-progress thought
80
- - Exact code snippets from files — observe your _understanding_ of them instead
81
- - Timestamps — suemo sets `valid_from` automatically
82
- - Uncertain things stated as facts — use `kind: "hypothesis"` instead
83
-
84
- ## Querying
85
-
86
- suemo uses hybrid retrieval (vector + BM25 + graph) with score gates.
87
- Natural language often works, but broad prompts can still be noisy.
88
-
89
- Use scoped, specific queries first:
79
+ Prefer specific, scoped queries:
90
80
 
91
- ```
92
- query("what approaches did we try for N+1 query issue", scope: "<active-scope>")
93
- query("past decisions about auth middleware", scope: "<active-scope>")
81
+ ```ts
82
+ suemo_query({
83
+ input: 'past decisions about auth middleware',
84
+ scope: '<project-id>',
85
+ })
86
+ suemo_query({
87
+ input: '"SURREAL_CAPS_ALLOW_FUNC" init fix',
88
+ scope: '<project-id>',
89
+ })
94
90
  ```
95
91
 
96
- For exact strings (error codes, env var names, flag spellings), quote them:
92
+ When weak ranking is suspected, run `--explain` from CLI to inspect scoring.
97
93
 
98
- ```
99
- query('"SURREAL_CAPS_ALLOW_FUNC" init fix', scope: "<active-scope>")
100
- ```
94
+ ---
101
95
 
102
- If ranking looks weak, inspect scoring directly:
96
+ ## 6) Anti-patterns
103
97
 
104
- ```
105
- suemo query "<your query>" --scope <active-scope> --explain
106
- suemo query "<your query>" --scope <active-scope> --explain --min-score 0.25 --relative-floor 0.8
107
- ```
98
+ - ❌ Storing transient state (cursor position, temporary thoughts)
99
+ - Storing raw code snippets instead of distilled understanding
100
+ - Querying on every small step
101
+ - ❌ Skipping `suemo_episode_end`
102
+ - ❌ Forgetting `suemo_goal_resolve`
103
+ - ❌ Using non-schema `kind` values
108
104
 
109
- - Use `query()` when searching — you don't know which node you want
110
- - Use `recall(<nodeId>)` when you have a node ID and want it plus neighbours
105
+ ---
111
106
 
112
- `recall()` also ticks the spaced-repetition clock on that node.
107
+ ## 7) Compaction/reset handling
113
108
 
114
- ## Using recall vs query
109
+ Immediately:
115
110
 
116
- - Use `query()` when you don't know _which_ memory you want — you're searching
117
- - Use `recall(nodeId)` when you have a specific node ID and want it plus its neighbours — you're inspecting
111
+ 1. `suemo_session_context_set({ sessionId, summary: "<compacted summary>" })`
112
+ 2. `suemo_context({ scope: "<project-id>", limit: 20 })`
113
+ 3. Continue work
118
114
 
119
- `recall()` also ticks the spaced-repetition clock on that node, marking it as "actively used".
115
+ ---
120
116
 
121
- ## Goals
117
+ ## 8) Documentation shortcuts
122
118
 
123
- Set a goal at the start of any multi-step task:
124
-
125
- ```
126
- goal_set("<description of task>", scope: "<active-scope>")
119
+ ```bash
120
+ suemo skill
121
+ suemo skill core-workflow
122
+ suemo skill mcp-reference
123
+ suemo skill manual-test-plan
127
124
  ```
128
125
 
129
- Check open goals at session start (already covered above). Resolve when done:
130
-
131
- ```
132
- goal_resolve(<id>)
133
- ```
126
+ Load docs once per session unless requirements changed.
134
127
 
135
- Goals are a record of intent. Abandoned goals show up in health reports as a signal something was left incomplete.
136
-
137
- ## What suemo handles automatically
138
-
139
- - **Timestamps** — `valid_from` on write, `valid_until` on contradiction or invalidation
140
- - **Deduplication** — identical writes merge; near-duplicates create a new linked node
141
- - **Consolidation** — runs on a schedule; no manual trigger needed
142
- - **Embeddings** — computed server-side via `fn::embed()`; never pass vectors yourself
143
-
144
- ## Health check
145
-
146
- ```
147
- suemo health
148
- ```
128
+ ---
149
129
 
150
- Signs things are working: recent consolidation run, goals resolved not abandoned, queries returning relevant results without heavy tuning.
130
+ _Version: 0.1.7 | Updated: 2026-03-23 | Summary: stricter schema + explicit completion contract_
@@ -12,6 +12,7 @@ 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
+ session: { type: 'string', description: 'Session ID (attach to open episode)' },
15
16
  json: { type: 'boolean', description: 'Output JSON result' },
16
17
  pretty: { type: 'boolean', description: 'Human-readable output (default)' },
17
18
  })
@@ -32,12 +33,14 @@ export const believeCmd = app.sub('believe')
32
33
  hasScope: Boolean(resolvedScope),
33
34
  scope: resolvedScope,
34
35
  confidence: flags.confidence,
36
+ hasSession: Boolean(flags.session),
35
37
  contentLength: args.content.length,
36
38
  })
37
39
  const { node, contradicted } = await believe(db, {
38
40
  content: args.content,
39
41
  scope: resolvedScope,
40
42
  confidence: flags.confidence,
43
+ sessionId: flags.session,
41
44
  }, config)
42
45
  if (outputMode === 'json') {
43
46
  const out: Record<string, unknown> = { id: node.id, valid_from: node.valid_from }
@@ -16,6 +16,8 @@ import OPENCODE_AGENTS_SNIPPET_TEXT from '@/src/AGENTS.md' with { type: 'text' }
16
16
  import template from '@/src/config.template' with { type: 'text' }
17
17
  import FASTEMBED_SCRIPT_TEXT from '@/src/embedding/fastembed-server.py' with { type: 'text' }
18
18
 
19
+ const OPENCODE_PLUGIN_TEXT = readFileSync(new URL('../../opencode/plugin.ts', import.meta.url), 'utf-8')
20
+
19
21
  interface InitFlags {
20
22
  debug?: boolean | undefined
21
23
  config?: string | undefined
@@ -253,6 +255,7 @@ type ServiceManager =
253
255
  type OpenCodeConfigMode = 'mcp'
254
256
  type OpenCodeConfigStatus = 'added' | 'already-present' | 'unchanged'
255
257
  type OpenCodeAgentsStatus = 'added' | 'updated' | 'unchanged'
258
+ type OpenCodePluginStatus = 'added' | 'updated' | 'unchanged'
256
259
 
257
260
  interface OpenCodeConfigResolution {
258
261
  path: string
@@ -268,6 +271,8 @@ interface OpenCodeInitResult {
268
271
  configMode: OpenCodeConfigMode
269
272
  agentsPath: string
270
273
  agentsStatus: OpenCodeAgentsStatus
274
+ pluginPath: string
275
+ pluginStatus: OpenCodePluginStatus
271
276
  dryRun: boolean
272
277
  }
273
278
 
@@ -594,6 +599,18 @@ function initOpenCodeFiles(configPath: string, dryRun: boolean): OpenCodeInitRes
594
599
  const existingAgents = existsSync(agentsPath) ? readFileSync(agentsPath, 'utf-8') : ''
595
600
  const agentsResult = upsertAgentsSnippet(existingAgents)
596
601
 
602
+ const pluginsDir = join(openCodeDir, 'plugins')
603
+ const pluginPath = join(pluginsDir, 'suemo.ts')
604
+ const nextPluginText = `${OPENCODE_PLUGIN_TEXT.trimEnd()}\n`
605
+ const pluginExists = existsSync(pluginPath)
606
+ const existingPlugin = pluginExists ? readFileSync(pluginPath, 'utf-8') : ''
607
+ let pluginStatus: OpenCodePluginStatus = 'unchanged'
608
+ if (!pluginExists) {
609
+ pluginStatus = 'added'
610
+ } else if (existingPlugin !== nextPluginText) {
611
+ pluginStatus = 'updated'
612
+ }
613
+
597
614
  if (!dryRun) {
598
615
  mkdirSync(openCodeDir, { recursive: true })
599
616
  if (!configResolution.existed || configWriteResult.changed) {
@@ -602,6 +619,10 @@ function initOpenCodeFiles(configPath: string, dryRun: boolean): OpenCodeInitRes
602
619
  if (!existsSync(agentsPath) || agentsResult.content !== existingAgents) {
603
620
  writeFileSync(agentsPath, agentsResult.content, 'utf-8')
604
621
  }
622
+ mkdirSync(pluginsDir, { recursive: true })
623
+ if (pluginStatus !== 'unchanged') {
624
+ writeFileSync(pluginPath, nextPluginText, 'utf-8')
625
+ }
605
626
  }
606
627
 
607
628
  return {
@@ -612,6 +633,8 @@ function initOpenCodeFiles(configPath: string, dryRun: boolean): OpenCodeInitRes
612
633
  configMode: configWriteResult.mode,
613
634
  agentsPath,
614
635
  agentsStatus: agentsResult.status,
636
+ pluginPath,
637
+ pluginStatus,
615
638
  dryRun,
616
639
  }
617
640
  }
@@ -1129,7 +1152,7 @@ const initSchemaCmd = init.sub('schema')
1129
1152
  })
1130
1153
 
1131
1154
  const initOpenCodeCmd = init.sub('opencode')
1132
- .meta({ description: 'Initialize OpenCode MCP config + AGENTS.md suemo snippet' })
1155
+ .meta({ description: 'Initialize OpenCode MCP config + AGENTS.md snippet + suemo plugin' })
1133
1156
  .flags({
1134
1157
  'dry-run': { type: 'boolean', description: 'Show changes without writing files', default: false },
1135
1158
  json: { type: 'boolean', description: 'Machine-readable output mode' },
@@ -1160,6 +1183,8 @@ const initOpenCodeCmd = init.sub('opencode')
1160
1183
  configMode: result.configMode,
1161
1184
  agentsPath: result.agentsPath,
1162
1185
  agentsStatus: result.agentsStatus,
1186
+ pluginPath: result.pluginPath,
1187
+ pluginStatus: result.pluginStatus,
1163
1188
  dryRun: result.dryRun,
1164
1189
  },
1165
1190
  suemoVersion: npmVersion,
@@ -1173,6 +1198,7 @@ const initOpenCodeCmd = init.sub('opencode')
1173
1198
  console.log(`OpenCode config: ${result.configPath} (${result.configFormat})`)
1174
1199
  console.log(`MCP entry: ${result.configStatus} (${result.configMode})`)
1175
1200
  console.log(`AGENTS.md: ${result.agentsStatus} at ${result.agentsPath}`)
1201
+ console.log(`Plugin: ${result.pluginStatus} at ${result.pluginPath}`)
1176
1202
  if (dryRun) {
1177
1203
  console.log('Dry-run mode: no files were written.')
1178
1204
  }
package/src/cli/index.ts CHANGED
@@ -20,6 +20,26 @@ import { helpPlugin, versionPlugin } from '@crustjs/plugins'
20
20
 
21
21
  import packageJson from '@/package.json' with { type: 'json' }
22
22
 
23
+ function applyDeprecatedAliases(argv: string[]): string[] {
24
+ const next = [...argv]
25
+
26
+ if (next[0] === 'skills') {
27
+ next[0] = 'skill'
28
+ console.warn('[suemo] Deprecated alias `skills` detected. Use `skill` instead.')
29
+ return next
30
+ }
31
+
32
+ if (next[0] === 'init' && next[1] === 'surrealdb') {
33
+ next[1] = 'surreal'
34
+ console.warn('[suemo] Deprecated alias `init surrealdb` detected. Use `init surreal` instead.')
35
+ return next
36
+ }
37
+
38
+ return next
39
+ }
40
+
41
+ process.argv = [...process.argv.slice(0, 2), ...applyDeprecatedAliases(process.argv.slice(2))]
42
+
23
43
  await app
24
44
  .use(versionPlugin(packageJson.version ?? '0.0.0'))
25
45
  .use(helpPlugin())
@@ -16,7 +16,7 @@ import {
16
16
  upsertByKey,
17
17
  } from '@/src/memory/write.ts'
18
18
  import { listSkillReferences, readSkillDoc, readSkillReference } from '@/src/skill/catalog.ts'
19
- import { ObserveInputSchema, QueryInputSchema } from '@/src/types.ts'
19
+ import { MemoryKindSchema, ObserveInputSchema, QueryInputSchema } from '@/src/types.ts'
20
20
  import type { Surreal } from 'surrealdb'
21
21
  import { z } from 'zod'
22
22
 
@@ -49,6 +49,7 @@ export async function handleToolCall(
49
49
  ): Promise<unknown> {
50
50
  log.debug('Dispatching MCP tool call', { method, paramKeys: Object.keys(params) })
51
51
  const inferredScope = inferProjectScope(process.cwd(), config)
52
+ const validKinds = MemoryKindSchema.options.join(', ')
52
53
 
53
54
  const maybeTriggerMutationSync = (): void => {
54
55
  if (!MUTATING_TOOLS.has(method) || !opts.onMutation) return
@@ -59,7 +60,15 @@ export async function handleToolCall(
59
60
 
60
61
  switch (method) {
61
62
  case 'observe': {
62
- const parsed = ObserveInputSchema.parse(params)
63
+ const parsedObserve = ObserveInputSchema.safeParse(params)
64
+ if (!parsedObserve.success) {
65
+ const invalidKindIssue = parsedObserve.error.issues.find((issue) => issue.path[0] === 'kind')
66
+ if (invalidKindIssue && typeof params.kind === 'string') {
67
+ throw new Error(`Invalid observe.kind "${params.kind}". Valid kinds: ${validKinds}`)
68
+ }
69
+ throw parsedObserve.error
70
+ }
71
+ const parsed = parsedObserve.data
63
72
  const result = await observe(db, {
64
73
  ...parsed,
65
74
  scope: parsed.scope?.trim() || inferredScope,
@@ -74,6 +83,7 @@ export async function handleToolCall(
74
83
  content: z.string(),
75
84
  scope: z.string().optional(),
76
85
  confidence: z.number().optional(),
86
+ sessionId: z.string().optional(),
77
87
  })
78
88
  .parse(params)
79
89
  const result = await believe(db, {
@@ -225,7 +235,7 @@ export async function handleToolCall(
225
235
  confidence: z.number().optional(),
226
236
  source: z.string().optional(),
227
237
  sessionId: z.string().optional(),
228
- kind: z.enum(['observation', 'belief', 'question', 'hypothesis', 'goal']).optional(),
238
+ kind: MemoryKindSchema.optional(),
229
239
  })
230
240
  .parse(params)
231
241
  const result = await upsertByKey(db, config, parsed.topicKey, parsed.content, {
@@ -262,7 +272,7 @@ export async function handleToolCall(
262
272
  .object({
263
273
  nodeId: z.string(),
264
274
  content: z.string().optional(),
265
- kind: z.enum(['observation', 'belief', 'question', 'hypothesis', 'goal']).optional(),
275
+ kind: MemoryKindSchema.optional(),
266
276
  tags: z.array(z.string()).optional(),
267
277
  scope: z.string().nullable().optional(),
268
278
  source: z.string().nullable().optional(),
package/src/mcp/stdio.ts CHANGED
@@ -31,12 +31,12 @@ interface StdioServerOptions {
31
31
  const MCP_TOOLS: McpToolDefinition[] = [
32
32
  {
33
33
  name: 'observe',
34
- description: 'Store an observation/belief/question/hypothesis/goal memory node',
34
+ description: 'Store memory node (kind must be observation|belief|question|hypothesis|goal)',
35
35
  inputSchema: {
36
36
  type: 'object',
37
37
  properties: {
38
38
  content: { type: 'string' },
39
- kind: { type: 'string' },
39
+ kind: { type: 'string', enum: ['observation', 'belief', 'question', 'hypothesis', 'goal'] },
40
40
  tags: { type: 'array', items: { type: 'string' } },
41
41
  scope: { type: 'string' },
42
42
  source: { type: 'string' },
@@ -55,6 +55,7 @@ const MCP_TOOLS: McpToolDefinition[] = [
55
55
  content: { type: 'string' },
56
56
  scope: { type: 'string' },
57
57
  confidence: { type: 'number' },
58
+ sessionId: { type: 'string' },
58
59
  },
59
60
  required: ['content'],
60
61
  },
@@ -0,0 +1,105 @@
1
+ /**
2
+ * suemo OpenCode plugin (v0.1.7)
3
+ *
4
+ * Purpose:
5
+ * - Inject strict, always-on suemo memory workflow instructions.
6
+ * - Preserve compacted-session signal by requiring immediate suemo checkpointing.
7
+ *
8
+ * This module is copied by `suemo init opencode` to:
9
+ * ~/.config/opencode/plugins/suemo.ts
10
+ */
11
+
12
+ interface SystemTransformOutput {
13
+ system: string[]
14
+ }
15
+
16
+ interface SessionCompactingInput {
17
+ sessionID?: string
18
+ }
19
+
20
+ interface SessionCompactingOutput {
21
+ context: string[]
22
+ }
23
+
24
+ const MEMORY_INSTRUCTIONS = `## Suemo Persistent Memory — Protocol (v0.1.7)
25
+
26
+ You have access to suemo persistent memory tools (commonly prefixed as \`suemo_*\`).
27
+
28
+ ### REQUIRED START-OF-TASK CHECKLIST
29
+
30
+ Before implementation:
31
+ 1. \`suemo_goal_list({ scope })\`
32
+ 2. \`suemo_query({ input: "recent work on <topic>", scope })\`
33
+ 3. \`suemo_context({ scope, limit: 20 })\`
34
+ 4. If task is new: \`suemo_episode_start({ sessionId })\`
35
+
36
+ ### REQUIRED WRITE TRIGGERS
37
+
38
+ Call \`suemo_observe\` immediately after:
39
+ - bug fix completed
40
+ - architecture/design decision made
41
+ - non-obvious discovery
42
+ - config/environment change
43
+ - user preference/constraint discovered
44
+
45
+ Use this strict content template for non-trivial writes:
46
+
47
+ \`\`\`
48
+ **What**: ...
49
+ **Why**: ...
50
+ **Where**: path/to/file
51
+ **Learned**: ...
52
+ \`\`\`
53
+
54
+ ### SCHEMA GUARDRAILS
55
+
56
+ For \`suemo_observe\`, \`kind\` MUST be one of:
57
+ - \`observation\`
58
+ - \`belief\`
59
+ - \`question\`
60
+ - \`hypothesis\`
61
+ - \`goal\`
62
+
63
+ ### END-OF-TASK CHECKLIST
64
+
65
+ Before declaring done:
66
+ 1. \`suemo_episode_end({ sessionId, summary, goal, discoveries, accomplished, files_changed })\`
67
+ 2. \`suemo_goal_resolve({ goalId })\` for completed goals
68
+ 3. \`suemo_query({ input: "most recent observations", scope })\`
69
+ 4. \`suemo_health()\`
70
+
71
+ ### COMPACTION/RESET RULE
72
+
73
+ If context is compacted/reset, FIRST:
74
+ 1. Save checkpoint with \`suemo_session_context_set({ sessionId, summary })\`
75
+ 2. Recover via \`suemo_context({ scope, limit: 20 })\`
76
+ 3. Then continue work
77
+ `
78
+
79
+ export const Suemo = async () => {
80
+ return {
81
+ 'experimental.chat.system.transform': async (_input: unknown, output: SystemTransformOutput) => {
82
+ if (output.system.length > 0) {
83
+ output.system[output.system.length - 1] += `\n\n${MEMORY_INSTRUCTIONS}`
84
+ return
85
+ }
86
+ output.system.push(MEMORY_INSTRUCTIONS)
87
+ },
88
+
89
+ 'experimental.session.compacting': async (
90
+ input: SessionCompactingInput,
91
+ output: SessionCompactingOutput,
92
+ ) => {
93
+ const sessionId = input.sessionID ?? '<current-session-id>'
94
+ output.context.push(
95
+ [
96
+ 'FIRST ACTION REQUIRED:',
97
+ `Call suemo_session_context_set({ sessionId: "${sessionId}", summary: "<compacted summary>" }) before any other work.`,
98
+ 'Then call suemo_context({ scope: "<project-scope>", limit: 20 }) to recover continuity.',
99
+ ].join(' '),
100
+ )
101
+ },
102
+ }
103
+ }
104
+
105
+ export default Suemo