@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 +102 -28
- package/dist/core/usecases/extract.d.ts +17 -0
- package/dist/core/usecases/extract.js +70 -16
- package/dist/core/usecases/index.d.ts +2 -2
- package/dist/core/usecases/inject.d.ts +16 -1
- package/dist/core/usecases/inject.js +51 -17
- package/dist/interfaces/cli/commands/claw.js +143 -12
- package/package.json +1 -1
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
|
|
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
|
-
|
|
54
|
-
relic show motoko
|
|
72
|
+
### 3. Launch a Shell
|
|
55
73
|
|
|
56
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
376
|
+
### Extract — Import a Claw agent as an Engram
|
|
377
|
+
|
|
378
|
+
Creates a new Engram from an existing Claw agent workspace.
|
|
341
379
|
|
|
342
|
-
|
|
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
|
|
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
|
|
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
|
-
###
|
|
376
|
-
|
|
377
|
-
| Command |
|
|
378
|
-
|
|
379
|
-
| `
|
|
380
|
-
| `
|
|
381
|
-
| `
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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.
|
|
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
|
|
27
|
-
const targetPath = resolveWorkspacePath(agentName, options?.openclawDir);
|
|
51
|
+
const targetPath = resolveWorkspacePath(engramId, options?.openclawDir);
|
|
28
52
|
if (!existsSync(targetPath)) {
|
|
29
|
-
throw new InjectWorkspaceNotFoundError(
|
|
53
|
+
throw new InjectWorkspaceNotFoundError(engramId);
|
|
30
54
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
26
|
-
to: opts.to,
|
|
28
|
+
const diff = await inject.inspectPersona(opts.engram, {
|
|
27
29
|
openclawDir: clawDir,
|
|
28
30
|
mergeIdentity: opts.mergeIdentity,
|
|
29
31
|
});
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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
|
|
106
|
+
const diff = await extract.inspectPersona(agentName, {
|
|
85
107
|
name: opts.name,
|
|
86
108
|
openclawDir: clawDir,
|
|
87
109
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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}
|
|
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
|
+
}
|