@ectplsm/relic 0.2.0 → 0.2.1

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
@@ -42,27 +42,52 @@ npm install -g @ectplsm/relic
42
42
 
43
43
  ## Quick Start
44
44
 
45
+ ### 1. Initialize
46
+
45
47
  ```bash
46
- # Initialize — creates config and sample Engrams
47
48
  relic init
48
49
  # → Prompts: "Set a default Engram? (press Enter for "johnny", or enter ID, or "n" to skip):"
49
50
 
50
- # List available Engrams
51
- relic list
51
+ relic list # List available Engrams
52
+ relic config default-engram motoko # (Optional) Set your default Engram
53
+ ```
54
+
55
+ ### 2. Set Up Memory (MCP)
56
+
57
+ Register the MCP server so the Construct can search past conversations and distill memories. Pick your shell:
58
+
59
+ ```bash
60
+ # Claude Code
61
+ claude mcp add --scope user relic -- relic-mcp
62
+
63
+ # Codex CLI
64
+ codex mcp add relic -- relic-mcp
65
+
66
+ # Gemini CLI — add to ~/.gemini/settings.json:
67
+ # { "mcpServers": { "relic": { "command": "relic-mcp", "trust": true } } }
68
+ ```
69
+
70
+ > For auto-approval setup and per-shell details, see [MCP Server](#mcp-server).
52
71
 
53
- # Preview an Engram's composed prompt
54
- relic show motoko
72
+ ### 3. Launch a Shell
55
73
 
56
- # Launch a Shell (uses default Engram if --engram is omitted)
57
- relic claude
74
+ ```bash
75
+ relic claude # Uses default Engram
76
+ relic claude --engram motoko # Specify explicitly
58
77
  relic codex
59
78
  relic gemini
60
-
61
- # Or specify explicitly
62
- relic claude --engram motoko
63
- relic codex --engram johnny
64
79
  ```
65
80
 
81
+ ### 4. Organize Memories
82
+
83
+ As you use a Construct, conversation logs are automatically saved to `archive.md` by background hooks. To distill these into lasting memory, periodically tell the Construct:
84
+
85
+ > **"Organize my memories"**
86
+
87
+ The Construct will review recent conversations, extract key facts and decisions into `memory/*.md`, promote important long-term insights to `MEMORY.md`, and update your preferences in `USER.md`. These distilled memories are then loaded into future sessions automatically.
88
+
89
+ > For details on the memory system, see [Memory Management](#memory-management).
90
+
66
91
  ## What `relic init` Creates
67
92
 
68
93
  Running `relic init` creates `~/.relic/`, writes `config.json`, and seeds two sample Engrams under `~/.relic/engrams/`.
@@ -314,11 +339,23 @@ Add to `~/.gemini/settings.json`:
314
339
 
315
340
  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.
316
341
 
317
- Agent Name = Engram ID. All Claw commands live under `relic claw`:
342
+ Current rule: **Agent Name = Engram ID**. Relic treats them as the same name by default. This keeps Claw integration simple: once Engram and agent names diverge, Relic has to introduce explicit mapping logic, which adds complexity that the current workflow does not need.
343
+
344
+ All Claw commands live under `relic claw`:
345
+
346
+ ### Command Summary
347
+
348
+ | Command | Direction | Description |
349
+ |---------|-----------|-------------|
350
+ | `relic claw inject -e <id>` | Relic → Claw | Push persona + auto-sync (`--yes` skips overwrite confirmation, `--no-sync` skips sync, `--merge-identity` for non-OpenClaw) |
351
+ | `relic claw extract -a <name>` | Claw → Relic | New import or persona-only overwrite, then auto-sync that target (`--force`, `--yes`, `--no-sync`) |
352
+ | `relic claw sync` | Relic ↔ Claw | Bidirectional merge (memory, MEMORY.md, USER.md; `--target` limits sync to one target) |
318
353
 
319
354
  ### Inject — Push an Engram into a Claw workspace
320
355
 
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.
356
+ Writes the persona files (`SOUL.md`, `IDENTITY.md`) into the agent workspace, then syncs `USER.md` and memory files (`MEMORY.md`, `memory/*.md`). The sync is bidirectional and merge-based, not a blind overwrite. `AGENTS.md` and `HEARTBEAT.md` remain under Claw's control.
357
+
358
+ If persona files already exist in the target workspace and differ from the local Relic Engram, `inject` asks for confirmation by default. Use `--yes` to skip the prompt. If the target persona already matches, Relic skips the persona rewrite and only runs the memory sync.
322
359
 
323
360
  > **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.
324
361
 
@@ -326,20 +363,26 @@ Injects persona files (SOUL.md, IDENTITY.md) into the agent's workspace director
326
363
  # Inject Engram "motoko" → workspace-motoko/
327
364
  relic claw inject --engram motoko
328
365
 
329
- # Inject into a differently-named agent
330
- relic claw inject --engram motoko --to main
331
- # → workspace/ receives motoko's persona
332
-
333
366
  # Override Claw directory (or configure once with: relic config claw-path)
334
367
  relic claw inject --engram motoko --dir /path/to/.fooclaw
335
368
 
336
369
  # Non-OpenClaw frameworks: merge IDENTITY.md into SOUL.md
337
370
  relic claw inject --engram motoko --dir ~/.nanobot --merge-identity
371
+
372
+ # Skip overwrite confirmation if persona files differ
373
+ relic claw inject --engram motoko --yes
338
374
  ```
339
375
 
340
- ### Extract — Import a Claw agent as a new Engram
376
+ ### Extract — Import a Claw agent as an Engram
377
+
378
+ Creates a new Engram from an existing Claw agent workspace.
341
379
 
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.
380
+ What `extract` writes locally:
381
+ - New extract: `engram.json`, `SOUL.md`, `IDENTITY.md`, `USER.md`, `MEMORY.md`, `memory/*.md`
382
+ - `extract --force`: only `SOUL.md` and `IDENTITY.md`
383
+ - `extract --force --name`: `SOUL.md`, `IDENTITY.md`, and `engram.json.name`
384
+
385
+ After `extract`, Relic automatically runs a targeted sync for that same Engram/agent target. Use `--no-sync` to skip it.
343
386
 
344
387
  ```bash
345
388
  # Extract from the default (main) agent
@@ -351,18 +394,32 @@ relic claw extract --agent johnny
351
394
  # Set a custom display name
352
395
  relic claw extract --agent analyst --name "Data Analyst"
353
396
 
397
+ # Overwrite local persona files from the Claw workspace
398
+ relic claw extract --agent johnny --force
399
+
400
+ # Skip overwrite confirmation
401
+ relic claw extract --agent johnny --force --yes
402
+
403
+ # Skip the automatic targeted sync after extract
404
+ relic claw extract --agent johnny --no-sync
405
+
354
406
  # Override Claw directory
355
407
  relic claw extract --agent johnny --dir /path/to/.fooclaw
356
408
  ```
357
409
 
358
410
  ### Sync — Bidirectional merge
359
411
 
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`).
412
+ Merges `memory/*.md`, `MEMORY.md`, and `USER.md` between matching Engram/agent targets. Only targets where both the Engram and agent exist are synced. Also runs automatically after `inject` (skip with `--no-sync`).
413
+
414
+ By default, `sync` scans all matching targets. Use `--target <id>` to sync only one target by shared Engram/agent name.
361
415
 
362
416
  ```bash
363
- # Sync all matching pairs
417
+ # Sync all matching targets
364
418
  relic claw sync
365
419
 
420
+ # Sync only one matching target
421
+ relic claw sync --target johnny
422
+
366
423
  # Override Claw directory
367
424
  relic claw sync --dir /path/to/.fooclaw
368
425
  ```
@@ -372,13 +429,30 @@ Merge rules:
372
429
  - Same content → skipped
373
430
  - Different content → merged (deduplicated) and written to both sides
374
431
 
375
- ### Command Summary
376
-
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) |
432
+ ### Behavior Matrix
433
+
434
+ | Command | State | Flags | Result |
435
+ |---------|------|------|------|
436
+ | `inject` | Workspace missing | none | Fail and ask you to create the agent first |
437
+ | `inject` | Persona matches local Engram | none | Skip persona rewrite, then auto-sync that target |
438
+ | `inject` | Persona differs from local Engram | none | Ask for confirmation before overwriting persona, then auto-sync that target |
439
+ | `inject` | Persona differs from local Engram | `--yes` | Overwrite persona without confirmation, then auto-sync that target |
440
+ | `inject` | any successful inject | `--no-sync` | Skip the automatic targeted sync |
441
+ | `extract` | Local Engram missing | none | Create a new Engram from workspace files, then auto-sync that target |
442
+ | `extract` | Local Engram missing | `--force` | Same as normal new extract, then auto-sync that target |
443
+ | `extract` | Local Engram exists | none | Fail and require `--force` |
444
+ | `extract` | Local Engram exists, no persona drift | `--force` | Skip persona overwrite, then auto-sync that target |
445
+ | `extract` | Local Engram exists, persona differs | `--force` | Ask for confirmation before overwriting `SOUL.md` / `IDENTITY.md`, then auto-sync that target |
446
+ | `extract` | Local Engram exists, persona differs | `--force --yes` | Overwrite `SOUL.md` / `IDENTITY.md` without confirmation, then auto-sync that target |
447
+ | `extract` | any successful extract | `--no-sync` | Skip the automatic targeted sync |
448
+ | `sync` | no target | none | Scan and sync all matching targets |
449
+ | `sync` | explicit target | `--target <id>` | Sync one matching target where `agentName = engramId` |
450
+
451
+ Notes:
452
+ - "Persona" means `SOUL.md` and `IDENTITY.md`
453
+ - `extract --force` only overwrites `SOUL.md` and `IDENTITY.md`
454
+ - `extract --force` does not overwrite `USER.md`, `MEMORY.md`, or `memory/*.md`
455
+ - If `--name` is provided together with `extract --force`, Relic also updates `engram.json.name`
382
456
 
383
457
  ## Memory Management
384
458
 
@@ -1,4 +1,15 @@
1
1
  import type { EngramRepository } from "../ports/engram-repository.js";
2
+ export type ExtractPersonaFileDiff = "missing" | "same" | "different";
3
+ export interface ExtractPersonaDiffResult {
4
+ engramId: string;
5
+ engramName: string;
6
+ sourcePath: string;
7
+ existing: boolean;
8
+ name: ExtractPersonaFileDiff;
9
+ soul: ExtractPersonaFileDiff;
10
+ identity: ExtractPersonaFileDiff;
11
+ overwriteRequired: boolean;
12
+ }
2
13
  export interface ExtractResult {
3
14
  engramId: string;
4
15
  engramName: string;
@@ -14,10 +25,16 @@ export interface ExtractResult {
14
25
  export declare class Extract {
15
26
  private readonly repository;
16
27
  constructor(repository: EngramRepository);
28
+ inspectPersona(agentName: string, options?: {
29
+ name?: string;
30
+ openclawDir?: string;
31
+ }): Promise<ExtractPersonaDiffResult>;
17
32
  execute(agentName: string, options?: {
18
33
  name?: string;
19
34
  openclawDir?: string;
35
+ force?: boolean;
20
36
  }): Promise<ExtractResult>;
37
+ private comparePersonaFile;
21
38
  private readFiles;
22
39
  }
23
40
  export declare class WorkspaceNotFoundError extends Error {
@@ -13,41 +13,95 @@ export class Extract {
13
13
  constructor(repository) {
14
14
  this.repository = repository;
15
15
  }
16
+ async inspectPersona(agentName, options) {
17
+ const sourcePath = resolveWorkspacePath(agentName, options?.openclawDir);
18
+ if (!existsSync(sourcePath)) {
19
+ throw new WorkspaceNotFoundError(sourcePath);
20
+ }
21
+ const { files } = await this.readFiles(sourcePath);
22
+ const existing = await this.repository.get(agentName);
23
+ if (!existing) {
24
+ return {
25
+ engramId: agentName,
26
+ engramName: options?.name ?? agentName,
27
+ sourcePath,
28
+ existing: false,
29
+ name: "missing",
30
+ soul: "missing",
31
+ identity: "missing",
32
+ overwriteRequired: false,
33
+ };
34
+ }
35
+ const requestedName = options?.name ?? existing.meta.name;
36
+ const name = requestedName === existing.meta.name ? "same" : "different";
37
+ const soul = this.comparePersonaFile(existing.files.soul, files.soul);
38
+ const identity = this.comparePersonaFile(existing.files.identity, files.identity);
39
+ return {
40
+ engramId: agentName,
41
+ engramName: existing.meta.name,
42
+ sourcePath,
43
+ existing: true,
44
+ name,
45
+ soul,
46
+ identity,
47
+ overwriteRequired: name === "different" ||
48
+ soul === "different" ||
49
+ identity === "different",
50
+ };
51
+ }
16
52
  async execute(agentName, options) {
17
53
  const sourcePath = resolveWorkspacePath(agentName, options?.openclawDir);
18
54
  if (!existsSync(sourcePath)) {
19
55
  throw new WorkspaceNotFoundError(sourcePath);
20
56
  }
21
- // 既存Engramがあればエラー — Relic側が真のデータソース
22
57
  const existing = await this.repository.get(agentName);
23
- if (existing) {
58
+ if (existing && !options?.force) {
24
59
  throw new AlreadyExtractedError(agentName);
25
60
  }
26
61
  const { files, filesRead } = await this.readFiles(sourcePath);
27
62
  if (filesRead.length === 0) {
28
63
  throw new WorkspaceEmptyError(sourcePath);
29
64
  }
30
- const engramName = options?.name ?? agentName;
31
65
  const now = new Date().toISOString();
32
- const engram = {
33
- meta: {
34
- id: agentName,
35
- name: engramName,
36
- description: `Extracted from OpenClaw workspace (${agentName})`,
37
- createdAt: now,
38
- updatedAt: now,
39
- tags: ["extracted", "openclaw"],
40
- },
41
- files,
42
- };
66
+ const engram = existing && options?.force
67
+ ? {
68
+ meta: {
69
+ ...existing.meta,
70
+ name: options?.name ?? existing.meta.name,
71
+ updatedAt: now,
72
+ },
73
+ // Force extract only replaces persona files from Claw.
74
+ files: {
75
+ ...existing.files,
76
+ soul: files.soul ?? existing.files.soul,
77
+ identity: files.identity ?? existing.files.identity,
78
+ },
79
+ }
80
+ : {
81
+ meta: {
82
+ id: agentName,
83
+ name: options?.name ?? agentName,
84
+ description: `Extracted from OpenClaw workspace (${agentName})`,
85
+ createdAt: now,
86
+ updatedAt: now,
87
+ tags: ["extracted", "openclaw"],
88
+ },
89
+ files,
90
+ };
43
91
  await this.repository.save(engram);
44
92
  return {
45
93
  engramId: agentName,
46
- engramName,
94
+ engramName: engram.meta.name,
47
95
  sourcePath,
48
96
  filesRead,
49
97
  };
50
98
  }
99
+ comparePersonaFile(currentContent, incomingContent) {
100
+ if (currentContent === undefined || incomingContent === undefined) {
101
+ return "missing";
102
+ }
103
+ return currentContent === incomingContent ? "same" : "different";
104
+ }
51
105
  async readFiles(sourcePath) {
52
106
  const files = {};
53
107
  const filesRead = [];
@@ -88,7 +142,7 @@ export class WorkspaceEmptyError extends Error {
88
142
  }
89
143
  export class AlreadyExtractedError extends Error {
90
144
  constructor(id) {
91
- super(`Engram "${id}" already exists. Relic is the source of truth use "relic claw inject" to push changes.`);
145
+ super(`Engram "${id}" already exists. Re-run with "--force" option to overwrite local persona files from the Claw workspace.`);
92
146
  this.name = "AlreadyExtractedError";
93
147
  }
94
148
  }
@@ -1,8 +1,8 @@
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, InjectClawDirNotFoundError, InjectWorkspaceNotFoundError, type InjectResult, } from "./inject.js";
5
- export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, AlreadyExtractedError, type ExtractResult, } from "./extract.js";
4
+ export { Inject, InjectEngramNotFoundError, InjectClawDirNotFoundError, InjectWorkspaceNotFoundError, type InjectPersonaFileDiff, type InjectPersonaDiffResult, type InjectResult, } from "./inject.js";
5
+ export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, AlreadyExtractedError, type ExtractPersonaFileDiff, type ExtractPersonaDiffResult, type ExtractResult, } from "./extract.js";
6
6
  export { MemoryWrite, MemoryWriteEngramNotFoundError, type MemoryWriteResult, } from "./memory-write.js";
7
7
  export { Sync, SyncOpenclawDirNotFoundError, type SyncTarget, type SyncResult, type SyncInitialResult, } from "./sync.js";
8
8
  export { ArchivePending, ArchivePendingEngramNotFoundError, type ArchivePendingResult, } from "./archive-pending.js";
@@ -1,4 +1,13 @@
1
1
  import type { EngramRepository } from "../ports/engram-repository.js";
2
+ export type InjectPersonaFileDiff = "missing" | "same" | "different" | "skipped";
3
+ export interface InjectPersonaDiffResult {
4
+ engramId: string;
5
+ engramName: string;
6
+ targetPath: string;
7
+ soul: InjectPersonaFileDiff;
8
+ identity: InjectPersonaFileDiff;
9
+ overwriteRequired: boolean;
10
+ }
2
11
  export interface InjectResult {
3
12
  engramId: string;
4
13
  engramName: string;
@@ -15,11 +24,17 @@ export interface InjectResult {
15
24
  export declare class Inject {
16
25
  private readonly repository;
17
26
  constructor(repository: EngramRepository);
27
+ inspectPersona(engramId: string, options?: {
28
+ openclawDir?: string;
29
+ mergeIdentity?: boolean;
30
+ }): Promise<InjectPersonaDiffResult>;
18
31
  execute(engramId: string, options?: {
19
- to?: string;
20
32
  openclawDir?: string;
21
33
  mergeIdentity?: boolean;
22
34
  }): Promise<InjectResult>;
35
+ private loadInjectTarget;
36
+ private resolveInjectedContent;
37
+ private compareTargetFile;
23
38
  private writeFiles;
24
39
  }
25
40
  export declare class InjectEngramNotFoundError extends Error {
@@ -1,6 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { existsSync } from "node:fs";
3
- import { writeFile } from "node:fs/promises";
3
+ import { readFile, writeFile } from "node:fs/promises";
4
4
  import { INJECT_FILE_MAP, resolveWorkspacePath } from "../../shared/openclaw.js";
5
5
  /**
6
6
  * Inject — EngramのファイルをOpenClawワークスペースに注入する
@@ -14,27 +14,65 @@ export class Inject {
14
14
  constructor(repository) {
15
15
  this.repository = repository;
16
16
  }
17
+ async inspectPersona(engramId, options) {
18
+ const { engram, targetPath } = await this.loadInjectTarget(engramId, options);
19
+ const mergeIdentity = options?.mergeIdentity ?? false;
20
+ const soul = await this.compareTargetFile(join(targetPath, INJECT_FILE_MAP.soul), this.resolveInjectedContent("soul", engram.files, mergeIdentity));
21
+ const identity = mergeIdentity
22
+ ? "skipped"
23
+ : await this.compareTargetFile(join(targetPath, INJECT_FILE_MAP.identity), this.resolveInjectedContent("identity", engram.files, mergeIdentity));
24
+ return {
25
+ engramId: engram.meta.id,
26
+ engramName: engram.meta.name,
27
+ targetPath,
28
+ soul,
29
+ identity,
30
+ overwriteRequired: soul === "different" || identity === "different",
31
+ };
32
+ }
17
33
  async execute(engramId, options) {
34
+ const { engram, targetPath } = await this.loadInjectTarget(engramId, options);
35
+ const filesWritten = await this.writeFiles(targetPath, engram.files, options?.mergeIdentity ?? false);
36
+ return {
37
+ engramId: engram.meta.id,
38
+ engramName: engram.meta.name,
39
+ targetPath,
40
+ filesWritten,
41
+ };
42
+ }
43
+ async loadInjectTarget(engramId, options) {
18
44
  const engram = await this.repository.get(engramId);
19
45
  if (!engram) {
20
46
  throw new InjectEngramNotFoundError(engramId);
21
47
  }
22
- // ベースディレクトリ(--dir)の存在チェック
23
48
  if (options?.openclawDir && !existsSync(options.openclawDir)) {
24
49
  throw new InjectClawDirNotFoundError(options.openclawDir);
25
50
  }
26
- const agentName = options?.to ?? engramId;
27
- const targetPath = resolveWorkspacePath(agentName, options?.openclawDir);
51
+ const targetPath = resolveWorkspacePath(engramId, options?.openclawDir);
28
52
  if (!existsSync(targetPath)) {
29
- throw new InjectWorkspaceNotFoundError(agentName);
53
+ throw new InjectWorkspaceNotFoundError(engramId);
30
54
  }
31
- const filesWritten = await this.writeFiles(targetPath, engram.files, options?.mergeIdentity ?? false);
32
- return {
33
- engramId: engram.meta.id,
34
- engramName: engram.meta.name,
35
- targetPath,
36
- filesWritten,
37
- };
55
+ return { engram, targetPath };
56
+ }
57
+ resolveInjectedContent(key, files, mergeIdentity) {
58
+ if (mergeIdentity && key === "identity") {
59
+ return undefined;
60
+ }
61
+ let content = files[key];
62
+ if (mergeIdentity && key === "soul" && files.identity) {
63
+ content = content + "\n" + files.identity;
64
+ }
65
+ return content;
66
+ }
67
+ async compareTargetFile(filePath, expectedContent) {
68
+ if (expectedContent === undefined) {
69
+ return "skipped";
70
+ }
71
+ if (!existsSync(filePath)) {
72
+ return "missing";
73
+ }
74
+ const currentContent = await readFile(filePath, "utf-8");
75
+ return currentContent === expectedContent ? "same" : "different";
38
76
  }
39
77
  async writeFiles(targetPath, files, mergeIdentity) {
40
78
  const written = [];
@@ -43,11 +81,7 @@ export class Inject {
43
81
  if (mergeIdentity && key === "identity") {
44
82
  continue;
45
83
  }
46
- let content = files[key];
47
- // --merge-identity: append IDENTITY.md content to SOUL.md
48
- if (mergeIdentity && key === "soul" && files.identity) {
49
- content = content + "\n" + files.identity;
50
- }
84
+ const content = this.resolveInjectedContent(key, files, mergeIdentity);
51
85
  if (content !== undefined) {
52
86
  await writeFile(join(targetPath, filename), content, "utf-8");
53
87
  written.push(filename);
@@ -1,3 +1,6 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { existsSync } from "node:fs";
1
4
  import { LocalEngramRepository } from "../../../adapters/local/index.js";
2
5
  import { Inject, InjectEngramNotFoundError, InjectClawDirNotFoundError, InjectWorkspaceNotFoundError, Extract, WorkspaceNotFoundError, WorkspaceEmptyError, AlreadyExtractedError, Sync, SyncOpenclawDirNotFoundError, } from "../../../core/usecases/index.js";
3
6
  import { resolveEngramsPath, resolveClawPath } from "../../../shared/config.js";
@@ -11,9 +14,9 @@ export function registerClawCommand(program) {
11
14
  .command("inject")
12
15
  .description("Push an Engram into a Claw workspace")
13
16
  .requiredOption("-e, --engram <id>", "Engram ID to inject")
14
- .option("--to <agent>", "Inject into a different agent name")
15
17
  .option("--dir <dir>", "Override Claw directory path (default: ~/.openclaw)")
16
18
  .option("--merge-identity", "Merge IDENTITY.md into SOUL.md (for non-OpenClaw Claw frameworks)")
19
+ .option("-y, --yes", "Skip persona overwrite confirmation")
17
20
  .option("--no-sync", "Skip automatic memory sync after inject")
18
21
  .option("-p, --path <dir>", "Override engrams directory path")
19
22
  .action(async (opts) => {
@@ -22,19 +25,34 @@ export function registerClawCommand(program) {
22
25
  const repo = new LocalEngramRepository(engramsPath);
23
26
  const inject = new Inject(repo);
24
27
  try {
25
- const result = await inject.execute(opts.engram, {
26
- to: opts.to,
28
+ const diff = await inject.inspectPersona(opts.engram, {
27
29
  openclawDir: clawDir,
28
30
  mergeIdentity: opts.mergeIdentity,
29
31
  });
30
- console.log(`Injected "${result.engramName}" into ${result.targetPath}`);
31
- console.log(` Files written: ${result.filesWritten.join(", ")}`);
32
+ const alreadyInjected = diff.soul === "same" &&
33
+ (diff.identity === "same" || diff.identity === "skipped");
34
+ if (diff.overwriteRequired &&
35
+ !opts.yes &&
36
+ !(await confirmOverwrite(`SOUL.md and/or IDENTITY.md already exist and differ in ${diff.targetPath}. Overwrite with local Relic version? [y/N] `))) {
37
+ console.error("Error: Persona files already exist and differ. Re-run with --yes to overwrite from local Relic Engram.");
38
+ process.exit(1);
39
+ }
40
+ if (alreadyInjected) {
41
+ console.log(` Already injected (${diff.targetPath})`);
42
+ }
43
+ else {
44
+ const result = await inject.execute(opts.engram, {
45
+ openclawDir: clawDir,
46
+ mergeIdentity: opts.mergeIdentity,
47
+ });
48
+ console.log(`Injected "${result.engramName}" into ${result.targetPath}`);
49
+ console.log(` Files written: ${result.filesWritten.join(", ")}`);
50
+ }
32
51
  if (!opts.sync)
33
52
  return;
34
53
  // Auto-sync memory after inject
35
54
  const sync = new Sync(repo, engramsPath);
36
- const agentName = opts.to ?? opts.engram;
37
- const workspacePath = resolveWorkspacePath(agentName, clawDir);
55
+ const workspacePath = resolveWorkspacePath(opts.engram, clawDir);
38
56
  const syncResult = await sync.syncPair({
39
57
  engramId: opts.engram,
40
58
  workspacePath,
@@ -73,21 +91,82 @@ export function registerClawCommand(program) {
73
91
  .option("-a, --agent <name>", "Agent name to extract from (default: main)")
74
92
  .option("--name <name>", "Engram display name (defaults to agent name)")
75
93
  .option("--dir <dir>", "Override Claw directory path (default: ~/.openclaw)")
94
+ .option("-f, --force", "Allow overwriting local persona files from the Claw workspace")
95
+ .option("-y, --yes", "Skip persona overwrite confirmation")
96
+ .option("--no-sync", "Skip automatic memory sync after extract")
76
97
  .option("-p, --path <dir>", "Override engrams directory path")
77
98
  .action(async (opts) => {
78
99
  const engramsPath = await resolveEngramsPath(opts.path);
79
100
  const clawDir = await resolveClawPath(opts.dir);
80
101
  const repo = new LocalEngramRepository(engramsPath);
81
102
  const extract = new Extract(repo);
103
+ const sync = new Sync(repo, engramsPath);
82
104
  try {
83
105
  const agentName = opts.agent ?? "main";
84
- const result = await extract.execute(agentName, {
106
+ const diff = await extract.inspectPersona(agentName, {
85
107
  name: opts.name,
86
108
  openclawDir: clawDir,
87
109
  });
88
- console.log(`Extracted "${result.engramName}" from ${result.sourcePath}`);
89
- console.log(` Files read: ${result.filesRead.join(", ")}`);
90
- console.log(` Saved as Engram: ${result.engramId}`);
110
+ if (diff.existing && !opts.force) {
111
+ throw new AlreadyExtractedError(agentName);
112
+ }
113
+ const alreadyExtracted = diff.existing &&
114
+ diff.name === "same" &&
115
+ diff.soul === "same" &&
116
+ diff.identity === "same";
117
+ if (diff.existing &&
118
+ diff.overwriteRequired &&
119
+ !opts.yes &&
120
+ !(await confirmOverwrite(`SOUL.md and/or IDENTITY.md already exist in local Engram "${agentName}" and differ from ${diff.sourcePath}. Overwrite with the Claw version? [y/N] `))) {
121
+ console.error("Error: Persona files already exist and differ. Re-run with --yes to overwrite from the Claw workspace.");
122
+ process.exit(1);
123
+ }
124
+ if (alreadyExtracted) {
125
+ console.log(opts.force
126
+ ? ` Already extracted and updated (${agentName})`
127
+ : ` Already extracted (${agentName})`);
128
+ }
129
+ else {
130
+ const result = await extract.execute(agentName, {
131
+ name: opts.name,
132
+ openclawDir: clawDir,
133
+ force: opts.force,
134
+ });
135
+ console.log(`Extracted "${result.engramName}" from ${result.sourcePath}`);
136
+ if (diff.existing) {
137
+ console.log(` Files overwritten: SOUL.md, IDENTITY.md`);
138
+ if (diff.name === "different") {
139
+ console.log(` Metadata updated: engram.json (name)`);
140
+ }
141
+ console.log(` Saved as Engram: ${result.engramId}`);
142
+ }
143
+ else {
144
+ console.log(` Files extracted: ${result.filesRead.join(", ")}`);
145
+ console.log(` Saved as Engram: ${result.engramId}`);
146
+ }
147
+ }
148
+ if (!opts.sync)
149
+ return;
150
+ const syncResult = await sync.syncPair({
151
+ engramId: agentName,
152
+ workspacePath: diff.sourcePath,
153
+ });
154
+ const details = [];
155
+ if (syncResult.memoryFilesMerged > 0) {
156
+ details.push(`${syncResult.memoryFilesMerged} memory file(s)`);
157
+ }
158
+ if (syncResult.memoryIndexMerged) {
159
+ details.push("MEMORY.md");
160
+ }
161
+ if (syncResult.userMerged) {
162
+ details.push("USER.md");
163
+ }
164
+ if (details.length > 0) {
165
+ console.log(` Synced: ${details.join(", ")}`);
166
+ }
167
+ else {
168
+ console.log(` Already in sync`);
169
+ }
91
170
  }
92
171
  catch (err) {
93
172
  if (err instanceof WorkspaceNotFoundError ||
@@ -103,6 +182,7 @@ export function registerClawCommand(program) {
103
182
  claw
104
183
  .command("sync")
105
184
  .description("Bidirectional memory sync between Engrams and Claw workspaces")
185
+ .option("-t, --target <id>", "Sync one target only by shared Engram/agent name")
106
186
  .option("--dir <dir>", "Override Claw directory path (default: ~/.openclaw)")
107
187
  .option("-p, --path <dir>", "Override engrams directory path")
108
188
  .action(async (opts) => {
@@ -111,6 +191,44 @@ export function registerClawCommand(program) {
111
191
  const repo = new LocalEngramRepository(engramsPath);
112
192
  const sync = new Sync(repo, engramsPath);
113
193
  try {
194
+ if (opts.target) {
195
+ const targetId = opts.target.trim();
196
+ if (!targetId) {
197
+ console.error(`Error: Invalid sync target "${opts.target}".`);
198
+ process.exit(1);
199
+ }
200
+ const engram = await repo.get(targetId);
201
+ if (!engram) {
202
+ console.error(`Error: Engram "${targetId}" not found.`);
203
+ process.exit(1);
204
+ }
205
+ const workspacePath = resolveWorkspacePath(targetId, clawDir);
206
+ if (!existsSync(workspacePath)) {
207
+ console.error(`Error: Claw agent "${targetId}" workspace not found.`);
208
+ process.exit(1);
209
+ }
210
+ const result = await sync.syncPair({
211
+ engramId: targetId,
212
+ workspacePath,
213
+ });
214
+ const details = [];
215
+ if (result.memoryFilesMerged > 0) {
216
+ details.push(`${result.memoryFilesMerged} memory file(s)`);
217
+ }
218
+ if (result.memoryIndexMerged) {
219
+ details.push("MEMORY.md");
220
+ }
221
+ if (result.userMerged) {
222
+ details.push("USER.md");
223
+ }
224
+ if (details.length > 0) {
225
+ console.log(` ${targetId}: merged ${details.join(", ")}`);
226
+ }
227
+ else {
228
+ console.log(` Already in sync (${targetId})`);
229
+ }
230
+ return;
231
+ }
114
232
  const result = await sync.execute(clawDir);
115
233
  if (result.synced.length === 0 && result.skipped.length === 0) {
116
234
  console.log("No Claw workspaces found.");
@@ -131,7 +249,7 @@ export function registerClawCommand(program) {
131
249
  console.log(` ${s.engramId}: merged ${details.join(", ")}`);
132
250
  }
133
251
  else {
134
- console.log(` ${s.engramId}: already in sync`);
252
+ console.log(` Already in sync (${s.engramId})`);
135
253
  }
136
254
  }
137
255
  if (result.skipped.length > 0) {
@@ -147,3 +265,16 @@ export function registerClawCommand(program) {
147
265
  }
148
266
  });
149
267
  }
268
+ async function confirmOverwrite(message) {
269
+ if (!input.isTTY || !output.isTTY) {
270
+ return false;
271
+ }
272
+ const rl = createInterface({ input, output });
273
+ try {
274
+ const answer = await rl.question(message);
275
+ return /^(y|yes)$/i.test(answer.trim());
276
+ }
277
+ finally {
278
+ rl.close();
279
+ }
280
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ectplsm/relic",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "PROJECT RELIC — Engram injection system for AI constructs",
5
5
  "license": "MIT",
6
6
  "repository": {