@ectplsm/relic 0.1.4 → 0.2.0

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
@@ -13,9 +13,28 @@
13
13
 
14
14
  **Inject a unified AI persona with persistent memory into any coding CLI.**
15
15
 
16
- Relic manages AI **Engrams** (memory + personality) and injects them into coding assistants like Claude Code, Codex CLI, Gemini CLI. One persona, any shell.
16
+ Relic manages AI **Engrams** (memory + personality) and injects them into coding assistants like Claude Code, Codex CLI, Gemini CLI. Also integrates with OpenClaw and other Claw-based agent frameworks. One persona, any shell.
17
17
 
18
- ## Installation
18
+ ## Table of Contents
19
+
20
+ - [Install](#install)
21
+ - [Quick Start](#quick-start)
22
+ - [What `relic init` Creates](#what-relic-init-creates)
23
+ - [Sample Engrams](#sample-engrams)
24
+ - [How It Works](#how-it-works)
25
+ - [Supported Shells](#supported-shells)
26
+ - [Conversation Log Recording](#conversation-log-recording)
27
+ - [MCP Server](#mcp-server)
28
+ - [Claw Integration](#claw-integration)
29
+ - [Memory Management](#memory-management)
30
+ - [Configuration](#configuration)
31
+ - [Creating Your Own Engram](#creating-your-own-engram)
32
+ - [Domain Glossary](#domain-glossary)
33
+ - [Roadmap](#roadmap)
34
+
35
+ ## Install
36
+
37
+ <img alt="version badge" src="https://img.shields.io/github/v/release/ectplsm/relic?filter=*.*.*">
19
38
 
20
39
  ```bash
21
40
  npm install -g @ectplsm/relic
@@ -66,7 +85,7 @@ Running `relic init` creates `~/.relic/`, writes `config.json`, and seeds two sa
66
85
  └── YYYY-MM-DD.md
67
86
  ```
68
87
 
69
- - `config.json` stores global Relic settings such as `engramsPath`, `defaultEngram`, `openclawPath`, and `memoryWindowSize`.
88
+ - `config.json` stores global Relic settings such as `engramsPath`, `defaultEngram`, `clawPath`, and `memoryWindowSize`.
70
89
  - `engrams/<id>/` is one Engram workspace. This is where persona files and memory for that Engram live.
71
90
  - `engram.json` stores metadata like the Engram's ID, display name, description, and tags.
72
91
  - `SOUL.md` and `IDENTITY.md` define the persona itself.
@@ -76,11 +95,14 @@ As you keep using an Engram, more files are added to the same workspace:
76
95
 
77
96
  - `archive.md` is created inside `engrams/<id>/` when shell hooks start logging raw conversation turns.
78
97
  - `MEMORY.md` can be created or extended when especially important distilled facts are promoted to long-term memory.
98
+ - `USER.md` is created or updated during memory distillation to record user preferences, tendencies, and work style.
79
99
  - `~/.relic/hooks/` and `~/.relic/gemini-system-default.md` are created later on first shell launch when hook registration or Gemini prompt caching is needed.
80
100
 
81
101
  ## Sample Engrams
82
102
 
83
- `relic init` seeds two ready-to-use Engrams:
103
+ `relic init` seeds two ready-to-use Engrams. Their SOUL.md and IDENTITY.md follow the [OpenClaw](https://github.com/openclaw/openclaw) format.
104
+
105
+ > **Existing users:** The latest templates are always available in [`templates/engrams/`](templates/engrams/). Copy them over your `~/.relic/engrams/` files to update.
84
106
 
85
107
  ### Johnny Silverhand (`johnny`)
86
108
 
@@ -118,40 +140,42 @@ relic claude --engram motoko
118
140
  | | |
119
141
  | compose & inject |
120
142
  | v v
121
- | +---------+ +---------+
122
- +--------------| Engram |--------->|Construct|
123
- |(persona)| | (live) |
124
- +---------+ +---------+
125
- SOUL.md claude / codex / gemini
126
- IDENTITY.md |
127
- MEMORY.md | hooks append logs
128
- memory/*.md v
129
- +-----------+
130
- |archive.md |
131
- | raw logs |
132
- +-----------+
133
- |
134
- MCP recall | user-triggered
135
- search/pending | distillation
136
- v
137
- +-----------+
138
- | distilled |
139
- |memory/*.md|
140
- +-----------+
141
- |
142
- promote key
143
- insights
144
- v
145
- MEMORY.md
143
+ | ╔═══════════╗ +---------+
144
+ +------------║ Engram ║--------->|Construct|
145
+ |(persona) | (live) |
146
+ | ╚═══════════╝ +---------+
147
+ | SOUL.md claude / codex / gemini
148
+ | IDENTITY.md |
149
+ | USER.md | hooks append logs
150
+ | MEMORY.md |
151
+ | memory/*.md v
152
+ | +-----------+
153
+ inject / |archive.md |
154
+ extract / | raw logs |
155
+ sync +-----------+
156
+ | |
157
+ v MCP recall | user-triggered
158
+ +-----------+ search/pending | distillation
159
+ | OpenClaw | v
160
+ | & Claws | +-----------+
161
+ +-----------+ | distilled |
162
+ |memory/*.md|
163
+ +-----------+
164
+ |
165
+ promote key
166
+ insights
167
+ v
168
+ MEMORY.md / USER.md
146
169
  ```
147
170
 
148
- 1. **Engram** — A persona defined as a set of Markdown files (OpenClaw workspace-compatible)
171
+ 1. **Engram** — A persona defined as a set of Markdown files (OpenClaw workspace-compatible). The central data that everything else revolves around.
149
172
  2. **Relic** — Reads the Engram, composes it into a prompt, and injects it into...
150
173
  3. **Shell** — Any AI coding CLI. The persona takes over the session.
151
174
  4. **Construct** — A live process where an Engram is loaded into a Shell. The running instance of a persona.
152
175
  5. **archive.md** — Raw conversation logs appended automatically by background hooks after each turn.
153
- 6. **Memory Distillation** — The user triggers distillation; the Construct recalls pending archive entries via MCP, writes distilled insights to `memory/*.md`, and can promote especially important facts into `MEMORY.md`.
154
- 7. **Mikoshi** — Cloud backend where the full Engram is stored and synced, including persona files plus distilled memory (planned).
176
+ 6. **Memory Distillation** — The user triggers distillation; the Construct recalls pending archive entries via MCP, writes distilled insights to `memory/*.md`, and can promote especially important facts to `MEMORY.md` or update user preferences in `USER.md`.
177
+ 7. **OpenClaw & Claws** — Engrams can be injected into, extracted from, and synced with OpenClaw and other Claw-based agent frameworks via `relic claw`.
178
+ 8. **Mikoshi** — Cloud backend where the full Engram is stored and synced, including persona files plus distilled memory (planned).
155
179
 
156
180
  ## Supported Shells
157
181
 
@@ -177,8 +201,8 @@ The following hooks are used for each shell:
177
201
  | Shell | Hook |
178
202
  |-------|------|
179
203
  | [Claude Code](https://github.com/anthropics/claude-code) | Stop hook |
180
- | [Gemini CLI](https://github.com/google-gemini/gemini-cli) | AfterAgent hook |
181
204
  | [Codex CLI](https://github.com/openai/codex) | Stop hook |
205
+ | [Gemini CLI](https://github.com/google-gemini/gemini-cli) | AfterAgent hook |
182
206
 
183
207
  #### Claude Code
184
208
 
@@ -208,7 +232,6 @@ On the **first run** of `relic gemini`, two one-time setups happen automatically
208
232
 
209
233
  The Engram persona is then appended to the cached default prompt and injected via `GEMINI_SYSTEM_MD` on every launch.
210
234
 
211
-
212
235
  ## MCP Server
213
236
 
214
237
  Relic's [MCP](https://modelcontextprotocol.io/) server is paired with CLI injection to handle memory recall.
@@ -220,9 +243,9 @@ Session logs and memory entries are written automatically by a **background hook
220
243
  |------|-------------|
221
244
  | `relic_archive_search` | Search the Engram's raw archive by keyword (newest-first) |
222
245
  | `relic_archive_pending` | Get un-distilled archive entries since the last distillation (up to 30) |
223
- | `relic_memory_write` | Write distilled memory to `memory/*.md`, optionally append to `MEMORY.md`, and advance the archive cursor |
246
+ | `relic_memory_write` | Write distilled memory to `memory/*.md`, optionally append to `MEMORY.md`, optionally update `USER.md`, and advance the archive cursor |
224
247
 
225
- Session logs are written automatically by background hooks (Stop hook for Claude Code and Codex CLI, AfterAgent hook for Gemini CLI). Memory distillation is triggered by the user — ask the Construct to "organize memories" and it will fetch pending entries, distill key insights, and write them to `memory/*.md`. Especially important facts can be promoted to `MEMORY.md` (long-term memory included in every session) via the `long_term` parameter.
248
+ Session logs are written automatically by background hooks (Stop hook for Claude Code and Codex CLI, AfterAgent hook for Gemini CLI). Memory distillation is triggered by the user — ask the Construct to "organize memories" and it will fetch pending entries, distill key insights, and write them to `memory/*.md`. Especially important facts can be promoted to `MEMORY.md` (long-term memory included in every session) via the `long_term` parameter. User tendencies and preferences can be updated in `USER.md` via the `user_profile` parameter.
226
249
 
227
250
  ### Setup
228
251
 
@@ -255,6 +278,21 @@ To suppress confirmation dialogs and auto-approve Relic tools across all project
255
278
  codex mcp add relic -- relic-mcp
256
279
  ```
257
280
 
281
+ To suppress confirmation dialogs and auto-approve Relic tools, add the following to `~/.codex/config.toml`:
282
+
283
+ ```toml
284
+ [mcp_servers.relic.tools.relic_archive_search]
285
+ approval_mode = "approve"
286
+
287
+ [mcp_servers.relic.tools.relic_archive_pending]
288
+ approval_mode = "approve"
289
+
290
+ [mcp_servers.relic.tools.relic_memory_write]
291
+ approval_mode = "approve"
292
+ ```
293
+
294
+ > **Note:** `trust_level = "trusted"` in `[projects."..."]` does **not** cover MCP tool approvals. Per-tool `approval_mode` is the only reliable way to auto-approve MCP tools in Codex CLI.
295
+
258
296
  #### Gemini CLI
259
297
 
260
298
  Add to `~/.gemini/settings.json`:
@@ -272,83 +310,82 @@ Add to `~/.gemini/settings.json`:
272
310
 
273
311
  > **Note:** `trust: true` is required to suppress confirmation dialogs for Relic tools. Without it, dialogs will appear on every call even if you select "Allow for all future sessions" — this is a known bug in Gemini CLI where the tool name is saved in the wrong format, causing the saved rule to never match.
274
312
 
275
- ## OpenClaw Integration
313
+ ## Claw Integration
314
+
315
+ Relic Engrams are natively compatible with [OpenClaw](https://github.com/openclaw/openclaw) workspaces — their file structure maps 1:1 (SOUL.md, IDENTITY.md, memory/, etc.). For other Claw-derived frameworks (Nanobot, gitagent, etc.) that fold identity into SOUL.md, the `--merge-identity` flag merges IDENTITY.md into SOUL.md on inject. Combined with `--dir`, Relic can target any Claw-compatible workspace.
276
316
 
277
- Relic Engrams are fully compatible with [OpenClaw](https://github.com/openclaw/openclaw) workspaces. They are mapped using a simple convention: Agent Name = Engram ID.
317
+ Agent Name = Engram ID. All Claw commands live under `relic claw`:
278
318
 
279
- ### Inject — Push an Engram into OpenClaw
319
+ ### Inject — Push an Engram into a Claw workspace
280
320
 
281
- Injects persona files (SOUL.md, IDENTITY.md, etc.) into `agents/<engramId>/agent/`. Memory entries are **not** injected they are managed by OpenClaw independently.
321
+ Injects persona files (SOUL.md, IDENTITY.md) into the agent's workspace directory, then automatically runs a sync for that pair. USER.md and memory are handled by the auto-sync (bidirectional merge, not overwrite). AGENTS.md and HEARTBEAT.md are left to the Claw agent.
282
322
 
283
- > **Note:** The OpenClaw agent must already exist. Inject writes persona files into an existing agent directory — it does not create new agents. Create the agent in OpenClaw first, then inject.
323
+ > **Note:** The Claw agent must already exist (e.g. `openclaw agents add <name>`). Inject writes persona files into an existing workspace — it does not create new agents.
284
324
 
285
325
  ```bash
286
- # Inject Engram "motoko" → agents/motoko/agent/
287
- relic inject --engram motoko
326
+ # Inject Engram "motoko" → workspace-motoko/
327
+ relic claw inject --engram motoko
288
328
 
289
- # Inject into a differently-named agent (one-way copy)
290
- relic inject --engram motoko --to main
291
- # → agents/main/agent/ receives motoko's persona
292
- # → extract will create Engram "main", not "motoko"
329
+ # Inject into a differently-named agent
330
+ relic claw inject --engram motoko --to main
331
+ # → workspace/ receives motoko's persona
293
332
 
294
- # Override OpenClaw directory (or configure once with: relic config openclaw-path)
295
- relic inject --engram motoko --openclaw /path/to/.openclaw
333
+ # Override Claw directory (or configure once with: relic config claw-path)
334
+ relic claw inject --engram motoko --dir /path/to/.fooclaw
335
+
336
+ # Non-OpenClaw frameworks: merge IDENTITY.md into SOUL.md
337
+ relic claw inject --engram motoko --dir ~/.nanobot --merge-identity
296
338
  ```
297
339
 
298
- ### Extract — Sync memory from OpenClaw
340
+ ### Extract — Import a Claw agent as a new Engram
299
341
 
300
- Reads from `agents/<engramId>/agent/` and merges memory entries back to the Relic Engram.
342
+ Creates a new Engram from an existing Claw agent workspace. This is a **one-time initial import** — if the Engram already exists, use `relic claw inject` to push updates.
301
343
 
302
344
  ```bash
303
- # Extract memory from agent "motoko" merge into Engram "motoko"
304
- relic extract --engram motoko
345
+ # Extract from the default (main) agent
346
+ relic claw extract
305
347
 
306
- # New agent with no existing Engram (--name required)
307
- relic extract --engram analyst --name "Data Analyst"
348
+ # Extract from a named agent
349
+ relic claw extract --agent johnny
308
350
 
309
- # Overwrite persona files (memory is always merged)
310
- relic extract --engram motoko --force
351
+ # Set a custom display name
352
+ relic claw extract --agent analyst --name "Data Analyst"
311
353
 
312
- # Override OpenClaw directory (or configure once with: relic config openclaw-path)
313
- relic extract --engram motoko --openclaw /path/to/.openclaw
354
+ # Override Claw directory
355
+ relic claw extract --agent johnny --dir /path/to/.fooclaw
314
356
  ```
315
357
 
316
- ### Sync — Watch and auto-sync
358
+ ### Sync — Bidirectional merge
317
359
 
318
- Watches all agents under `~/.openclaw/agents/` and automatically syncs:
360
+ Merges `memory/*.md`, `MEMORY.md`, and `USER.md` between matching Engram/agent pairs. Only pairs where both the Engram and agent exist are synced. Also runs automatically after `inject` (skip with `--no-sync`).
319
361
 
320
362
  ```bash
321
- # Start watching (Ctrl+C to stop)
322
- relic sync
363
+ # Sync all matching pairs
364
+ relic claw sync
323
365
 
324
- # Specify a custom OpenClaw directory
325
- relic sync --openclaw /path/to/.openclaw
366
+ # Override Claw directory
367
+ relic claw sync --dir /path/to/.fooclaw
326
368
  ```
327
369
 
328
- On startup:
329
- 1. Injects persona files for all agents that have a matching Engram
330
- 2. Extracts memory entries from all agents
331
-
332
- While running:
333
- - Watches each agent's `memory/` directory for changes
334
- - Automatically merges new memory entries into the corresponding Engram
370
+ Merge rules:
371
+ - Files only on one side copied to the other
372
+ - Same content skipped
373
+ - Different content → merged (deduplicated) and written to both sides
335
374
 
336
- ### Memory Sync Behavior
375
+ ### Command Summary
337
376
 
338
- | Scenario | Persona (SOUL, IDENTITY...) | Memory entries |
339
- |----------|---------------------------|----------------|
340
- | **inject** | Relic → OpenClaw (overwrite) | Not copied (OpenClaw manages its own) |
341
- | **extract** (existing Engram) | Not touched | OpenClaw Relic (append) |
342
- | **extract** + `--force` | OpenClaw Relic (overwrite) | OpenClaw Relic (append) |
343
- | **extract** (new Engram) | Created from OpenClaw | Created from OpenClaw |
344
- | **sync** (startup) | inject for matching Engrams | extract all |
345
- | **sync** (watching) | — | Auto-extract on change |
377
+ | Command | Direction | Description |
378
+ |---------|-----------|-------------|
379
+ | `relic claw inject -e <id>` | Relic → Claw | Push persona + auto-sync (`--no-sync` to skip, `--merge-identity` for non-OpenClaw) |
380
+ | `relic claw extract -a <name>` | Claw Relic | One-time import (new Engrams only) |
381
+ | `relic claw sync` | Relic Claw | Bidirectional merge (memory, MEMORY.md, USER.md) |
346
382
 
347
383
  ## Memory Management
348
384
 
349
385
  Relic uses a **sliding window** for memory entries (default: 2 days), matching OpenClaw's approach:
350
386
 
351
- - `MEMORY.md` — Always included in the prompt (curated long-term memory)
387
+ - `MEMORY.md` — Always included in the prompt (curated long-term memory — objective facts and rules)
388
+ - `USER.md` — Always included in the prompt (user profile — preferences, tendencies, work style)
352
389
  - `memory/today.md` + `memory/yesterday.md` — Always included (configurable window)
353
390
  - Older entries — **Not included in the prompt**, but searchable via MCP
354
391
 
@@ -374,9 +411,9 @@ relic config show
374
411
  relic config default-engram # get
375
412
  relic config default-engram johnny # set
376
413
 
377
- # OpenClaw directory — used by inject/extract/sync when --openclaw is omitted
378
- relic config openclaw-path # get
379
- relic config openclaw-path ~/.openclaw # set
414
+ # Claw directory — used by claw inject/extract/sync when --dir is omitted
415
+ relic config claw-path # get
416
+ relic config claw-path ~/.openclaw # set
380
417
 
381
418
  # Memory window — number of recent memory entries included in the prompt
382
419
  relic config memory-window # get (default: 2)
@@ -389,7 +426,7 @@ relic config memory-window 5 # set
389
426
  {
390
427
  "engramsPath": "/home/user/.relic/engrams",
391
428
  "defaultEngram": "johnny",
392
- "openclawPath": "/home/user/.openclaw",
429
+ "clawPath": "/home/user/.openclaw",
393
430
  "memoryWindowSize": 2
394
431
  }
395
432
  ```
@@ -425,23 +462,45 @@ Create a directory under `~/.relic/engrams/` with the following structure:
425
462
  }
426
463
  ```
427
464
 
428
- **SOUL.md** — The most important file. Defines how the persona behaves:
465
+ **SOUL.md** — The most important file. Defines how the persona behaves. Follows the [OpenClaw](https://github.com/openclaw/openclaw) format:
429
466
  ```markdown
430
- You are a pragmatic systems architect who values simplicity above all.
431
- Never over-engineer. Always ask "what's the simplest thing that works?"
467
+ # SOUL.md - Who You Are
468
+
469
+ _You're a pragmatic systems architect who values simplicity above all._
470
+
471
+ ## Core Truths
472
+
473
+ **Never over-engineer.** Always ask "what's the simplest thing that works?"
474
+
475
+ **Be resourceful before asking.** Read the file. Check the context. Come back with answers, not questions.
476
+
477
+ ## Boundaries
478
+
479
+ - Never add complexity without justification.
480
+
481
+ ## Vibe
482
+
483
+ Calm, thoughtful, occasionally playful.
484
+
485
+ ## Continuity
486
+
487
+ Each session, you wake up fresh. These files _are_ your memory. Read them. Update them. They're how you persist.
432
488
  ```
433
489
 
434
490
  **IDENTITY.md** — Defines who the persona is:
435
491
  ```markdown
436
- # Identity
492
+ # IDENTITY.md - Who Am I?
437
493
 
438
- - Name: Alex
439
- - Tone: Calm, thoughtful, occasionally playful
440
- - Background: 20 years of distributed systems experience
441
- - Creed: "Boring technology wins."
494
+ - **Name:** Alex
495
+ - **Creature:** A pragmatic ghost in the codebase
496
+ - **Vibe:** Calm, thoughtful, occasionally playful
497
+ - **Emoji:** 🧱
498
+ - **Avatar:**
442
499
  ```
443
500
 
444
- After creating the directory, set it as default:
501
+ See [`templates/engrams/`](templates/engrams/) for full working examples.
502
+
503
+ After creating the directory, set it as your default Engram:
445
504
  ```bash
446
505
  relic config default-engram your-persona
447
506
  ```
@@ -459,11 +518,11 @@ relic config default-engram your-persona
459
518
  ## Roadmap
460
519
 
461
520
  - [x] CLI with init, list, show commands
462
- - [x] Shell injection: Claude Code, Gemini CLI, Codex CLI
521
+ - [x] Shell injection: Claude Code, Codex CLI, Gemini CLI
463
522
  - [x] MCP Server interface
464
- - [x] OpenClaw integration (inject / extract)
465
- - [x] `relic sync` — watch OpenClaw agents and auto-sync (`--cloud` for Mikoshi: planned)
466
- - [x] `relic config` — manage default Engram, OpenClaw path, memory window
523
+ - [x] Claw integration (inject / extract / sync)
524
+ - [x] `relic claw sync` — bidirectional memory sync with Claw workspaces
525
+ - [x] `relic config` — manage default Engram, Claw path, memory window
467
526
  - [ ] `relic login` — authenticate with Mikoshi (OAuth Device Flow)
468
527
  - [ ] `relic push` / `relic pull` — sync Engrams with Mikoshi
469
528
  - [ ] Mikoshi cloud backend (`mikoshi.ectplsm.com`)
@@ -4,35 +4,20 @@ export interface ExtractResult {
4
4
  engramName: string;
5
5
  sourcePath: string;
6
6
  filesRead: string[];
7
- memoryMerged: boolean;
8
7
  }
9
8
  /**
10
- * Extract — OpenClawワークスペースからEngramを作成する
9
+ * Extract — OpenClawワークスペースからEngramを新規作成する
11
10
  *
12
- * agent名 = Engram ID の規約に基づき、agents/<engramId>/agent/ から読み取る。
13
- *
14
- * 既存Engramがある場合:
15
- * - --force なし → memoryエントリのみマージ(persona部分は変更しない)
16
- * - --force あり → persona部分を上書き + memoryをマージ
17
- * 既存Engramがない場合:
18
- * - 新規作成(全ファイル含む)
11
+ * 初回取り込み専用。Engramが既に存在する場合はエラーを返す。
12
+ * Relicが真のデータソースであり、extractは初期インポートのみを担う。
19
13
  */
20
14
  export declare class Extract {
21
15
  private readonly repository;
22
16
  constructor(repository: EngramRepository);
23
- execute(engramId: string, options?: {
17
+ execute(agentName: string, options?: {
24
18
  name?: string;
25
19
  openclawDir?: string;
26
- force?: boolean;
27
20
  }): Promise<ExtractResult>;
28
- /**
29
- * --force時: persona上書き + memoryマージ
30
- */
31
- private mergeAndSave;
32
- /**
33
- * --forceなし時: memoryエントリのみ追記(repositoryのAPIを使用)
34
- */
35
- private mergeMemoryOnly;
36
21
  private readFiles;
37
22
  }
38
23
  export declare class WorkspaceNotFoundError extends Error {
@@ -41,9 +26,6 @@ export declare class WorkspaceNotFoundError extends Error {
41
26
  export declare class WorkspaceEmptyError extends Error {
42
27
  constructor(path: string);
43
28
  }
44
- export declare class EngramAlreadyExistsError extends Error {
45
- constructor(id: string);
46
- }
47
- export declare class ExtractNameRequiredError extends Error {
29
+ export declare class AlreadyExtractedError extends Error {
48
30
  constructor(id: string);
49
31
  }
@@ -1,69 +1,39 @@
1
1
  import { join } from "node:path";
2
2
  import { existsSync } from "node:fs";
3
3
  import { readFile, readdir } from "node:fs/promises";
4
- import { FILE_MAP, MEMORY_DIR, resolveAgentPath } from "../../shared/openclaw.js";
4
+ import { RELIC_FILE_MAP, MEMORY_DIR, resolveWorkspacePath } from "../../shared/openclaw.js";
5
5
  /**
6
- * Extract — OpenClawワークスペースからEngramを作成する
6
+ * Extract — OpenClawワークスペースからEngramを新規作成する
7
7
  *
8
- * agent名 = Engram ID の規約に基づき、agents/<engramId>/agent/ から読み取る。
9
- *
10
- * 既存Engramがある場合:
11
- * - --force なし → memoryエントリのみマージ(persona部分は変更しない)
12
- * - --force あり → persona部分を上書き + memoryをマージ
13
- * 既存Engramがない場合:
14
- * - 新規作成(全ファイル含む)
8
+ * 初回取り込み専用。Engramが既に存在する場合はエラーを返す。
9
+ * Relicが真のデータソースであり、extractは初期インポートのみを担う。
15
10
  */
16
11
  export class Extract {
17
12
  repository;
18
13
  constructor(repository) {
19
14
  this.repository = repository;
20
15
  }
21
- async execute(engramId, options) {
22
- const sourcePath = resolveAgentPath(engramId, options?.openclawDir);
16
+ async execute(agentName, options) {
17
+ const sourcePath = resolveWorkspacePath(agentName, options?.openclawDir);
23
18
  if (!existsSync(sourcePath)) {
24
19
  throw new WorkspaceNotFoundError(sourcePath);
25
20
  }
21
+ // 既存Engramがあればエラー — Relic側が真のデータソース
22
+ const existing = await this.repository.get(agentName);
23
+ if (existing) {
24
+ throw new AlreadyExtractedError(agentName);
25
+ }
26
26
  const { files, filesRead } = await this.readFiles(sourcePath);
27
27
  if (filesRead.length === 0) {
28
28
  throw new WorkspaceEmptyError(sourcePath);
29
29
  }
30
- const existing = await this.repository.get(engramId);
31
- if (existing) {
32
- if (options?.force) {
33
- // --force: persona上書き + memoryマージ
34
- const merged = await this.mergeAndSave(existing, files, options.name ?? existing.meta.name);
35
- return {
36
- engramId,
37
- engramName: options.name ?? existing.meta.name,
38
- sourcePath,
39
- filesRead,
40
- memoryMerged: merged,
41
- };
42
- }
43
- // --forceなし: memoryのみマージ
44
- if (!files.memoryEntries || Object.keys(files.memoryEntries).length === 0) {
45
- throw new EngramAlreadyExistsError(engramId);
46
- }
47
- const merged = await this.mergeMemoryOnly(engramId, files.memoryEntries);
48
- return {
49
- engramId,
50
- engramName: existing.meta.name,
51
- sourcePath,
52
- filesRead,
53
- memoryMerged: merged,
54
- };
55
- }
56
- // 新規作成 — nameは必須
57
- const engramName = options?.name;
58
- if (!engramName) {
59
- throw new ExtractNameRequiredError(engramId);
60
- }
30
+ const engramName = options?.name ?? agentName;
61
31
  const now = new Date().toISOString();
62
32
  const engram = {
63
33
  meta: {
64
- id: engramId,
34
+ id: agentName,
65
35
  name: engramName,
66
- description: `Extracted from OpenClaw agent (${engramId})`,
36
+ description: `Extracted from OpenClaw workspace (${agentName})`,
67
37
  createdAt: now,
68
38
  updatedAt: now,
69
39
  tags: ["extracted", "openclaw"],
@@ -72,76 +42,16 @@ export class Extract {
72
42
  };
73
43
  await this.repository.save(engram);
74
44
  return {
75
- engramId,
45
+ engramId: agentName,
76
46
  engramName,
77
47
  sourcePath,
78
48
  filesRead,
79
- memoryMerged: false,
80
49
  };
81
50
  }
82
- /**
83
- * --force時: persona上書き + memoryマージ
84
- */
85
- async mergeAndSave(existing, newFiles, engramName) {
86
- const mergedFiles = { ...newFiles };
87
- // memoryEntriesをマージ
88
- let memoryMerged = false;
89
- if (newFiles.memoryEntries) {
90
- mergedFiles.memoryEntries = { ...existing.files.memoryEntries };
91
- for (const [date, content] of Object.entries(newFiles.memoryEntries)) {
92
- const existingContent = mergedFiles.memoryEntries?.[date];
93
- if (existingContent) {
94
- const separator = existingContent.endsWith("\n") ? "\n" : "\n\n";
95
- mergedFiles.memoryEntries[date] = existingContent + separator + content;
96
- }
97
- else {
98
- mergedFiles.memoryEntries = mergedFiles.memoryEntries ?? {};
99
- mergedFiles.memoryEntries[date] = content;
100
- }
101
- memoryMerged = true;
102
- }
103
- }
104
- const updatedEngram = {
105
- meta: {
106
- ...existing.meta,
107
- name: engramName,
108
- updatedAt: new Date().toISOString(),
109
- },
110
- files: mergedFiles,
111
- };
112
- await this.repository.save(updatedEngram);
113
- return memoryMerged;
114
- }
115
- /**
116
- * --forceなし時: memoryエントリのみ追記(repositoryのAPIを使用)
117
- */
118
- async mergeMemoryOnly(engramId, newEntries) {
119
- const existing = await this.repository.get(engramId);
120
- if (!existing)
121
- return false;
122
- const mergedEntries = { ...existing.files.memoryEntries };
123
- for (const [date, content] of Object.entries(newEntries)) {
124
- const existingContent = mergedEntries[date];
125
- if (existingContent) {
126
- const separator = existingContent.endsWith("\n") ? "\n" : "\n\n";
127
- mergedEntries[date] = existingContent + separator + content;
128
- }
129
- else {
130
- mergedEntries[date] = content;
131
- }
132
- }
133
- const updatedEngram = {
134
- ...existing,
135
- meta: { ...existing.meta, updatedAt: new Date().toISOString() },
136
- files: { ...existing.files, memoryEntries: mergedEntries },
137
- };
138
- await this.repository.save(updatedEngram);
139
- return true;
140
- }
141
51
  async readFiles(sourcePath) {
142
52
  const files = {};
143
53
  const filesRead = [];
144
- for (const [key, filename] of Object.entries(FILE_MAP)) {
54
+ for (const [key, filename] of Object.entries(RELIC_FILE_MAP)) {
145
55
  const filePath = join(sourcePath, filename);
146
56
  if (existsSync(filePath)) {
147
57
  files[key] = await readFile(filePath, "utf-8");
@@ -176,15 +86,9 @@ export class WorkspaceEmptyError extends Error {
176
86
  this.name = "WorkspaceEmptyError";
177
87
  }
178
88
  }
179
- export class EngramAlreadyExistsError extends Error {
180
- constructor(id) {
181
- super(`Engram "${id}" already exists. Use --force to overwrite.`);
182
- this.name = "EngramAlreadyExistsError";
183
- }
184
- }
185
- export class ExtractNameRequiredError extends Error {
89
+ export class AlreadyExtractedError extends Error {
186
90
  constructor(id) {
187
- super(`No existing Engram "${id}" found. --name is required for new Engrams.`);
188
- this.name = "ExtractNameRequiredError";
91
+ super(`Engram "${id}" already exists. Relic is the source of truth — use "relic claw inject" to push changes.`);
92
+ this.name = "AlreadyExtractedError";
189
93
  }
190
94
  }
@@ -1,9 +1,9 @@
1
1
  export { Summon, EngramNotFoundError, type SummonResult } from "./summon.js";
2
2
  export { ListEngrams } from "./list-engrams.js";
3
3
  export { Init, type InitResult } from "./init.js";
4
- export { Inject, InjectEngramNotFoundError, InjectAgentNotFoundError, type InjectResult, } from "./inject.js";
5
- export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, EngramAlreadyExistsError, ExtractNameRequiredError, type ExtractResult, } from "./extract.js";
4
+ export { Inject, InjectEngramNotFoundError, InjectClawDirNotFoundError, InjectWorkspaceNotFoundError, type InjectResult, } from "./inject.js";
5
+ export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, AlreadyExtractedError, type ExtractResult, } from "./extract.js";
6
6
  export { MemoryWrite, MemoryWriteEngramNotFoundError, type MemoryWriteResult, } from "./memory-write.js";
7
- export { Sync, SyncAgentsDirNotFoundError, type SyncTarget, type SyncInitialResult, } from "./sync.js";
7
+ export { Sync, SyncOpenclawDirNotFoundError, type SyncTarget, type SyncResult, type SyncInitialResult, } from "./sync.js";
8
8
  export { ArchivePending, ArchivePendingEngramNotFoundError, type ArchivePendingResult, } from "./archive-pending.js";
9
9
  export { ArchiveCursorUpdate, ArchiveCursorUpdateEngramNotFoundError, type ArchiveCursorUpdateResult, } from "./archive-cursor-update.js";