@ectplsm/relic 0.2.2 → 0.2.3
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 +46 -35
- package/dist/core/usecases/create-engram.d.ts +26 -0
- package/dist/core/usecases/create-engram.js +54 -0
- package/dist/core/usecases/delete-engram.d.ts +24 -0
- package/dist/core/usecases/delete-engram.js +46 -0
- package/dist/interfaces/cli/commands/create.d.ts +2 -0
- package/dist/interfaces/cli/commands/create.js +143 -0
- package/dist/interfaces/cli/commands/delete.d.ts +2 -0
- package/dist/interfaces/cli/commands/delete.js +113 -0
- package/dist/interfaces/cli/index.js +4 -0
- package/dist/interfaces/mcp/index.js +68 -0
- package/dist/shared/templates.d.ts +6 -0
- package/dist/shared/templates.js +65 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|:---:|:---:|
|
|
3
3
|
|
|
4
4
|
# PROJECT RELIC
|
|
5
|
+

|
|
5
6
|
|
|
6
7
|
```
|
|
7
8
|
____ ________ ____________
|
|
@@ -17,6 +18,7 @@ Relic manages AI **Engrams** (memory + personality) and injects them into coding
|
|
|
17
18
|
|
|
18
19
|
## Table of Contents
|
|
19
20
|
|
|
21
|
+
- [Requirements](#requirements)
|
|
20
22
|
- [Install](#install)
|
|
21
23
|
- [Quick Start](#quick-start)
|
|
22
24
|
- [What `relic init` Creates](#what-relic-init-creates)
|
|
@@ -29,9 +31,14 @@ Relic manages AI **Engrams** (memory + personality) and injects them into coding
|
|
|
29
31
|
- [Memory Management](#memory-management)
|
|
30
32
|
- [Configuration](#configuration)
|
|
31
33
|
- [Creating Your Own Engram](#creating-your-own-engram)
|
|
34
|
+
- [Deleting an Engram](#deleting-an-engram)
|
|
32
35
|
- [Domain Glossary](#domain-glossary)
|
|
33
36
|
- [Roadmap](#roadmap)
|
|
34
37
|
|
|
38
|
+
## Requirements
|
|
39
|
+
|
|
40
|
+
- Node.js 18 or later
|
|
41
|
+
|
|
35
42
|
## Install
|
|
36
43
|
|
|
37
44
|
<img alt="version badge" src="https://img.shields.io/github/v/release/ectplsm/relic?filter=*.*.*">
|
|
@@ -118,8 +125,6 @@ Running `relic init` creates `~/.relic/`, writes `config.json`, and seeds two sa
|
|
|
118
125
|
- `manifest.json` stores system-managed fields like the Engram ID and timestamps.
|
|
119
126
|
- `SOUL.md` and `IDENTITY.md` define the persona itself.
|
|
120
127
|
- `memory/YYYY-MM-DD.md` stores dated distilled memory entries. `relic init` seeds an initial memory file for each sample Engram.
|
|
121
|
-
- `relic migrate engrams` can be used to front-load legacy Engrams into the new `manifest.json` format, although normal Engram reads also migrate automatically.
|
|
122
|
-
- `relic refresh-samples` refreshes bundled sample personas like `johnny` and `motoko` without touching memory or archive files.
|
|
123
128
|
|
|
124
129
|
As you keep using an Engram, more files are added to the same workspace:
|
|
125
130
|
|
|
@@ -128,11 +133,20 @@ As you keep using an Engram, more files are added to the same workspace:
|
|
|
128
133
|
- `USER.md` is created or updated during memory distillation to record user preferences, tendencies, and work style.
|
|
129
134
|
- `~/.relic/hooks/` and `~/.relic/gemini-system-default.md` are created later on first shell launch when hook registration or Gemini prompt caching is needed.
|
|
130
135
|
|
|
136
|
+
### Migration
|
|
137
|
+
|
|
138
|
+
If you want to manually update parts of an existing local setup that changed over time, use:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
relic migrate engrams # migrate legacy engram.json metadata to manifest.json
|
|
142
|
+
relic refresh-samples # refresh bundled sample personas like johnny and motoko
|
|
143
|
+
```
|
|
144
|
+
|
|
131
145
|
## Sample Engrams
|
|
132
146
|
|
|
133
147
|
`relic init` seeds two ready-to-use Engrams. Their SOUL.md and IDENTITY.md follow the [OpenClaw](https://github.com/openclaw/openclaw) format.
|
|
134
148
|
|
|
135
|
-
> **Existing users:**
|
|
149
|
+
> **Existing users:** Run `relic refresh-samples` to update bundled sample personas from the latest templates.
|
|
136
150
|
|
|
137
151
|
### Johnny Silverhand (`johnny`)
|
|
138
152
|
|
|
@@ -271,6 +285,7 @@ Session logs and memory entries are written automatically by a **background hook
|
|
|
271
285
|
|
|
272
286
|
| Tool | Description |
|
|
273
287
|
|------|-------------|
|
|
288
|
+
| `relic_engram_create` | Create a new Engram with optional LLM-generated SOUL.md and IDENTITY.md |
|
|
274
289
|
| `relic_archive_search` | Search the Engram's raw archive by keyword (newest-first) |
|
|
275
290
|
| `relic_archive_pending` | Get un-distilled archive entries since the last distillation (up to 30) |
|
|
276
291
|
| `relic_memory_write` | Write distilled memory to `memory/*.md`, optionally append to `MEMORY.md`, optionally update `USER.md`, and advance the archive cursor |
|
|
@@ -292,6 +307,7 @@ To suppress confirmation dialogs and auto-approve Relic tools across all project
|
|
|
292
307
|
"permissions": {
|
|
293
308
|
"allow": [
|
|
294
309
|
"Edit(~/.relic/engrams/**)",
|
|
310
|
+
"mcp__relic__relic_engram_create",
|
|
295
311
|
"mcp__relic__relic_archive_search",
|
|
296
312
|
"mcp__relic__relic_archive_pending",
|
|
297
313
|
"mcp__relic__relic_memory_write"
|
|
@@ -311,6 +327,9 @@ codex mcp add relic -- relic-mcp
|
|
|
311
327
|
To suppress confirmation dialogs and auto-approve Relic tools, add the following to `~/.codex/config.toml`:
|
|
312
328
|
|
|
313
329
|
```toml
|
|
330
|
+
[mcp_servers.relic.tools.relic_engram_create]
|
|
331
|
+
approval_mode = "approve"
|
|
332
|
+
|
|
314
333
|
[mcp_servers.relic.tools.relic_archive_search]
|
|
315
334
|
approval_mode = "approve"
|
|
316
335
|
|
|
@@ -514,41 +533,29 @@ CLI flags always take precedence over config values.
|
|
|
514
533
|
|
|
515
534
|
## Creating Your Own Engram
|
|
516
535
|
|
|
517
|
-
|
|
536
|
+
The easiest way to create a new Engram is with `relic create`:
|
|
518
537
|
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
├── manifest.json # System-managed identity (id, createdAt, updatedAt)
|
|
523
|
-
├── SOUL.md # Core directive — how the persona thinks and acts
|
|
524
|
-
├── IDENTITY.md # Name, tone, background, personality
|
|
525
|
-
├── AGENTS.md # (optional) Tool usage policies
|
|
526
|
-
├── USER.md # (optional) User context
|
|
527
|
-
├── MEMORY.md # (optional) Memory index
|
|
528
|
-
├── HEARTBEAT.md # (optional) Periodic reflection
|
|
529
|
-
└── memory/ # (optional) Dated memory entries
|
|
530
|
-
└── 2026-03-21.md
|
|
531
|
-
```
|
|
538
|
+
```bash
|
|
539
|
+
# Fully interactive — prompts for everything
|
|
540
|
+
relic create
|
|
532
541
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
{
|
|
536
|
-
"name": "Display Name",
|
|
537
|
-
"description": "A short description",
|
|
538
|
-
"tags": ["custom"]
|
|
539
|
-
}
|
|
542
|
+
# Pre-supply some fields
|
|
543
|
+
relic create --id my-agent --name "My Agent" --description "A helpful assistant" --tags "custom,dev"
|
|
540
544
|
```
|
|
541
545
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
"createdAt": "2026-03-21T00:00:00Z",
|
|
547
|
-
"updatedAt": "2026-03-21T00:00:00Z"
|
|
548
|
-
}
|
|
546
|
+
This creates the directory structure, writes `engram.json` / `manifest.json`, and seeds `SOUL.md` / `IDENTITY.md` with OpenClaw-compatible default templates. Customize the persona files, then launch a shell:
|
|
547
|
+
|
|
548
|
+
```bash
|
|
549
|
+
relic claude my-agent
|
|
549
550
|
```
|
|
550
551
|
|
|
551
|
-
|
|
552
|
+
You can also create Engrams via the `relic_engram_create` MCP tool — LLMs can ask about the desired personality and generate `SOUL.md` / `IDENTITY.md` content through conversation, then call the tool with the results.
|
|
553
|
+
|
|
554
|
+
### Customizing the Persona
|
|
555
|
+
|
|
556
|
+
After running `relic create`, edit `SOUL.md` and `IDENTITY.md` in the Engram directory. These follow the [OpenClaw](https://github.com/openclaw/openclaw) format:
|
|
557
|
+
|
|
558
|
+
**SOUL.md** — The most important file. Defines how the persona behaves:
|
|
552
559
|
```markdown
|
|
553
560
|
# SOUL.md - Who You Are
|
|
554
561
|
|
|
@@ -586,11 +593,14 @@ Each session, you wake up fresh. These files _are_ your memory. Read them. Updat
|
|
|
586
593
|
|
|
587
594
|
See [`templates/engrams/`](templates/engrams/) for full working examples.
|
|
588
595
|
|
|
589
|
-
|
|
596
|
+
## Deleting an Engram
|
|
597
|
+
|
|
590
598
|
```bash
|
|
591
|
-
relic
|
|
599
|
+
relic delete my-agent
|
|
592
600
|
```
|
|
593
601
|
|
|
602
|
+
If the Engram has memory data (`MEMORY.md`, `USER.md`, `memory/*.md`, `archive.md`), you'll need to type the Engram ID to confirm deletion. Use `--force` to skip all prompts.
|
|
603
|
+
|
|
594
604
|
## Domain Glossary
|
|
595
605
|
|
|
596
606
|
| Term | Role | Description |
|
|
@@ -609,10 +619,11 @@ relic config default-engram your-persona
|
|
|
609
619
|
- [x] Claw integration (inject / extract / sync)
|
|
610
620
|
- [x] `relic claw sync` — bidirectional memory sync with Claw workspaces
|
|
611
621
|
- [x] `relic config` — manage default Engram, Claw path, memory window
|
|
622
|
+
- [x] `relic create` — interactive Engram creation wizard + MCP tool
|
|
623
|
+
- [x] `relic delete` — safe Engram deletion with memory-aware confirmation
|
|
612
624
|
- [ ] Mikoshi cloud backend (`mikoshi.ectplsm.com`)
|
|
613
625
|
- [ ] `relic mikoshi login` — authenticate with Mikoshi (OAuth Device Flow)
|
|
614
626
|
- [ ] `relic mikoshi upload` / `relic mikoshi download` / `relic mikoshi sync` — sync Engrams with Mikoshi
|
|
615
|
-
- [ ] `relic create` — interactive Engram creation wizard
|
|
616
627
|
|
|
617
628
|
## License
|
|
618
629
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Engram } from "../entities/engram.js";
|
|
2
|
+
import type { EngramRepository } from "../ports/engram-repository.js";
|
|
3
|
+
export declare class EngramAlreadyExistsError extends Error {
|
|
4
|
+
constructor(id: string);
|
|
5
|
+
}
|
|
6
|
+
export declare class InvalidEngramIdError extends Error {
|
|
7
|
+
constructor(id: string);
|
|
8
|
+
}
|
|
9
|
+
export interface CreateEngramParams {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
tags?: string[];
|
|
14
|
+
soul: string;
|
|
15
|
+
identity: string;
|
|
16
|
+
}
|
|
17
|
+
export interface CreateEngramResult {
|
|
18
|
+
engram: Engram;
|
|
19
|
+
}
|
|
20
|
+
/** ID format: lowercase alphanumeric + hyphens, no leading/trailing hyphen */
|
|
21
|
+
export declare const ENGRAM_ID_PATTERN: RegExp;
|
|
22
|
+
export declare class CreateEngram {
|
|
23
|
+
private readonly repository;
|
|
24
|
+
constructor(repository: EngramRepository);
|
|
25
|
+
execute(params: CreateEngramParams): Promise<CreateEngramResult>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Errors
|
|
3
|
+
// ============================================================
|
|
4
|
+
export class EngramAlreadyExistsError extends Error {
|
|
5
|
+
constructor(id) {
|
|
6
|
+
super(`Engram "${id}" already exists. Delete it first or choose a different ID.`);
|
|
7
|
+
this.name = "EngramAlreadyExistsError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class InvalidEngramIdError extends Error {
|
|
11
|
+
constructor(id) {
|
|
12
|
+
super(`Invalid Engram ID "${id}". Use lowercase letters, numbers, and hyphens only (e.g. "my-agent").`);
|
|
13
|
+
this.name = "InvalidEngramIdError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// ============================================================
|
|
17
|
+
// Usecase
|
|
18
|
+
// ============================================================
|
|
19
|
+
/** ID format: lowercase alphanumeric + hyphens, no leading/trailing hyphen */
|
|
20
|
+
export const ENGRAM_ID_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
21
|
+
export class CreateEngram {
|
|
22
|
+
repository;
|
|
23
|
+
constructor(repository) {
|
|
24
|
+
this.repository = repository;
|
|
25
|
+
}
|
|
26
|
+
async execute(params) {
|
|
27
|
+
// Validate ID format
|
|
28
|
+
if (!ENGRAM_ID_PATTERN.test(params.id)) {
|
|
29
|
+
throw new InvalidEngramIdError(params.id);
|
|
30
|
+
}
|
|
31
|
+
// Check for duplicates
|
|
32
|
+
const existing = await this.repository.get(params.id);
|
|
33
|
+
if (existing) {
|
|
34
|
+
throw new EngramAlreadyExistsError(params.id);
|
|
35
|
+
}
|
|
36
|
+
const now = new Date().toISOString();
|
|
37
|
+
const engram = {
|
|
38
|
+
meta: {
|
|
39
|
+
id: params.id,
|
|
40
|
+
name: params.name,
|
|
41
|
+
description: params.description,
|
|
42
|
+
tags: params.tags,
|
|
43
|
+
createdAt: now,
|
|
44
|
+
updatedAt: now,
|
|
45
|
+
},
|
|
46
|
+
files: {
|
|
47
|
+
soul: params.soul,
|
|
48
|
+
identity: params.identity,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
await this.repository.save(engram);
|
|
52
|
+
return { engram };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Engram } from "../entities/engram.js";
|
|
2
|
+
import type { EngramRepository } from "../ports/engram-repository.js";
|
|
3
|
+
export declare class DeleteEngramNotFoundError extends Error {
|
|
4
|
+
constructor(id: string);
|
|
5
|
+
}
|
|
6
|
+
export interface DeleteEngramInfo {
|
|
7
|
+
engram: Engram;
|
|
8
|
+
hasMemory: boolean;
|
|
9
|
+
hasUser: boolean;
|
|
10
|
+
hasArchive: boolean;
|
|
11
|
+
memoryEntryCount: number;
|
|
12
|
+
}
|
|
13
|
+
export declare class DeleteEngram {
|
|
14
|
+
private readonly repository;
|
|
15
|
+
constructor(repository: EngramRepository);
|
|
16
|
+
/**
|
|
17
|
+
* Inspect an Engram before deletion — returns info for confirmation UI.
|
|
18
|
+
*/
|
|
19
|
+
inspect(id: string): Promise<DeleteEngramInfo>;
|
|
20
|
+
/**
|
|
21
|
+
* Actually delete the Engram. Call inspect() first for confirmation.
|
|
22
|
+
*/
|
|
23
|
+
execute(id: string): Promise<void>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Errors
|
|
3
|
+
// ============================================================
|
|
4
|
+
export class DeleteEngramNotFoundError extends Error {
|
|
5
|
+
constructor(id) {
|
|
6
|
+
super(`Engram "${id}" not found.`);
|
|
7
|
+
this.name = "DeleteEngramNotFoundError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
// ============================================================
|
|
11
|
+
// Usecase
|
|
12
|
+
// ============================================================
|
|
13
|
+
export class DeleteEngram {
|
|
14
|
+
repository;
|
|
15
|
+
constructor(repository) {
|
|
16
|
+
this.repository = repository;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Inspect an Engram before deletion — returns info for confirmation UI.
|
|
20
|
+
*/
|
|
21
|
+
async inspect(id) {
|
|
22
|
+
const engram = await this.repository.get(id);
|
|
23
|
+
if (!engram) {
|
|
24
|
+
throw new DeleteEngramNotFoundError(id);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
engram,
|
|
28
|
+
hasMemory: !!engram.files.memory,
|
|
29
|
+
hasUser: !!engram.files.user,
|
|
30
|
+
hasArchive: false, // archive.md is not loaded via EngramFiles; CLI checks separately
|
|
31
|
+
memoryEntryCount: engram.files.memoryEntries
|
|
32
|
+
? Object.keys(engram.files.memoryEntries).length
|
|
33
|
+
: 0,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Actually delete the Engram. Call inspect() first for confirmation.
|
|
38
|
+
*/
|
|
39
|
+
async execute(id) {
|
|
40
|
+
const existing = await this.repository.get(id);
|
|
41
|
+
if (!existing) {
|
|
42
|
+
throw new DeleteEngramNotFoundError(id);
|
|
43
|
+
}
|
|
44
|
+
await this.repository.delete(id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { loadConfig, ensureInitialized } from "../../../shared/config.js";
|
|
4
|
+
import { LocalEngramRepository } from "../../../adapters/local/index.js";
|
|
5
|
+
import { DEFAULT_SOUL, DEFAULT_IDENTITY } from "../../../shared/templates.js";
|
|
6
|
+
import { CreateEngram, EngramAlreadyExistsError, InvalidEngramIdError, ENGRAM_ID_PATTERN, } from "../../../core/usecases/create-engram.js";
|
|
7
|
+
// ============================================================
|
|
8
|
+
// Interactive prompt helpers
|
|
9
|
+
// ============================================================
|
|
10
|
+
async function ask(rl, question) {
|
|
11
|
+
const answer = await rl.question(question);
|
|
12
|
+
return answer.trim();
|
|
13
|
+
}
|
|
14
|
+
async function askRequired(rl, question, errorMsg) {
|
|
15
|
+
while (true) {
|
|
16
|
+
const answer = await ask(rl, question);
|
|
17
|
+
if (answer)
|
|
18
|
+
return answer;
|
|
19
|
+
console.log(errorMsg);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Ask for a valid, non-duplicate Engram ID. Retries on bad format or collision.
|
|
24
|
+
*/
|
|
25
|
+
async function askValidId(rl, repo) {
|
|
26
|
+
while (true) {
|
|
27
|
+
const id = await askRequired(rl, "Engram ID (e.g. my-agent): ", " ID is required. Use lowercase letters, numbers, and hyphens.");
|
|
28
|
+
if (!ENGRAM_ID_PATTERN.test(id)) {
|
|
29
|
+
console.log(" Invalid ID. Use lowercase letters, numbers, and hyphens (e.g. my-agent).");
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const existing = await repo.get(id);
|
|
33
|
+
if (existing) {
|
|
34
|
+
console.log(` Engram "${id}" already exists. Choose a different ID.`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
return id;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// ============================================================
|
|
41
|
+
// Command
|
|
42
|
+
// ============================================================
|
|
43
|
+
export function registerCreateCommand(program) {
|
|
44
|
+
program
|
|
45
|
+
.command("create")
|
|
46
|
+
.description("Create a new Engram interactively")
|
|
47
|
+
.option("-i, --id <id>", "Engram ID (lowercase alphanumeric + hyphens)")
|
|
48
|
+
.option("-n, --name <name>", "Display name")
|
|
49
|
+
.option("-d, --description <desc>", "Description")
|
|
50
|
+
.option("-t, --tags <tags>", "Comma-separated tags")
|
|
51
|
+
.action(async (opts) => {
|
|
52
|
+
await ensureInitialized();
|
|
53
|
+
const config = await loadConfig();
|
|
54
|
+
const repo = new LocalEngramRepository(config.engramsPath);
|
|
55
|
+
const usecase = new CreateEngram(repo);
|
|
56
|
+
const isTTY = input.isTTY && output.isTTY;
|
|
57
|
+
// If non-interactive, require at least id and name
|
|
58
|
+
if (!isTTY) {
|
|
59
|
+
if (!opts.id || !opts.name) {
|
|
60
|
+
console.error("Error: --id and --name are required in non-interactive mode.");
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let id = opts.id;
|
|
66
|
+
let name = opts.name;
|
|
67
|
+
let description = opts.description;
|
|
68
|
+
let tags;
|
|
69
|
+
if (opts.tags) {
|
|
70
|
+
tags = opts.tags.split(",").map((t) => t.trim()).filter(Boolean);
|
|
71
|
+
}
|
|
72
|
+
// Early validation for pre-supplied ID (before interactive flow)
|
|
73
|
+
if (id) {
|
|
74
|
+
if (!ENGRAM_ID_PATTERN.test(id)) {
|
|
75
|
+
console.error(`Error: Invalid Engram ID "${id}". Use lowercase letters, numbers, and hyphens only (e.g. "my-agent").`);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const existing = await repo.get(id);
|
|
80
|
+
if (existing) {
|
|
81
|
+
console.error(`Error: Engram "${id}" already exists. Delete it first or choose a different ID.`);
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Interactive flow for missing fields
|
|
87
|
+
if (isTTY && (!id || !name)) {
|
|
88
|
+
const rl = createInterface({ input, output });
|
|
89
|
+
try {
|
|
90
|
+
if (!id) {
|
|
91
|
+
id = await askValidId(rl, repo);
|
|
92
|
+
}
|
|
93
|
+
if (!name) {
|
|
94
|
+
name = await askRequired(rl, "Name: ", " Name is required.");
|
|
95
|
+
}
|
|
96
|
+
if (description === undefined) {
|
|
97
|
+
description = await ask(rl, "Description (optional): ") || undefined;
|
|
98
|
+
}
|
|
99
|
+
if (tags === undefined) {
|
|
100
|
+
const tagsInput = await ask(rl, "Tags (comma-separated, optional): ");
|
|
101
|
+
if (tagsInput) {
|
|
102
|
+
tags = tagsInput.split(",").map((t) => t.trim()).filter(Boolean);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
rl.close();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Execute
|
|
111
|
+
try {
|
|
112
|
+
const result = await usecase.execute({
|
|
113
|
+
id: id,
|
|
114
|
+
name: name,
|
|
115
|
+
description,
|
|
116
|
+
tags,
|
|
117
|
+
soul: DEFAULT_SOUL,
|
|
118
|
+
identity: DEFAULT_IDENTITY,
|
|
119
|
+
});
|
|
120
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
121
|
+
const engramDir = `${config.engramsPath}/${result.engram.meta.id}`;
|
|
122
|
+
console.log();
|
|
123
|
+
console.log(green(`Created Engram "${result.engram.meta.name}" (${result.engram.meta.id})`));
|
|
124
|
+
console.log(` → ${engramDir}/`);
|
|
125
|
+
console.log();
|
|
126
|
+
console.log("Files:");
|
|
127
|
+
console.log(" engram.json — metadata (name, description, tags)");
|
|
128
|
+
console.log(" manifest.json — system metadata (id, timestamps)");
|
|
129
|
+
console.log(" SOUL.md — core principles and behavior");
|
|
130
|
+
console.log(" IDENTITY.md — persona identity (fill in during first conversation)");
|
|
131
|
+
console.log();
|
|
132
|
+
console.log(`Customize SOUL.md and IDENTITY.md, then run: relic claude ${result.engram.meta.id}`);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
if (err instanceof InvalidEngramIdError || err instanceof EngramAlreadyExistsError) {
|
|
136
|
+
console.error(`Error: ${err.message}`);
|
|
137
|
+
process.exitCode = 1;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { loadConfig, ensureInitialized } from "../../../shared/config.js";
|
|
6
|
+
import { LocalEngramRepository } from "../../../adapters/local/index.js";
|
|
7
|
+
import { DeleteEngram, DeleteEngramNotFoundError, } from "../../../core/usecases/delete-engram.js";
|
|
8
|
+
// ============================================================
|
|
9
|
+
// Helpers
|
|
10
|
+
// ============================================================
|
|
11
|
+
async function confirm(message) {
|
|
12
|
+
if (!input.isTTY || !output.isTTY)
|
|
13
|
+
return false;
|
|
14
|
+
const rl = createInterface({ input, output });
|
|
15
|
+
try {
|
|
16
|
+
const answer = await rl.question(message);
|
|
17
|
+
return /^(y|yes)$/i.test(answer.trim());
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
rl.close();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function confirmWithId(message, expectedId) {
|
|
24
|
+
if (!input.isTTY || !output.isTTY)
|
|
25
|
+
return false;
|
|
26
|
+
const rl = createInterface({ input, output });
|
|
27
|
+
try {
|
|
28
|
+
const answer = await rl.question(message);
|
|
29
|
+
return answer.trim() === expectedId;
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
rl.close();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// ============================================================
|
|
36
|
+
// Command
|
|
37
|
+
// ============================================================
|
|
38
|
+
export function registerDeleteCommand(program) {
|
|
39
|
+
program
|
|
40
|
+
.command("delete")
|
|
41
|
+
.description("Delete an Engram permanently")
|
|
42
|
+
.argument("<id>", "Engram ID to delete")
|
|
43
|
+
.option("-f, --force", "Skip confirmation prompts")
|
|
44
|
+
.action(async (id, opts) => {
|
|
45
|
+
await ensureInitialized();
|
|
46
|
+
const config = await loadConfig();
|
|
47
|
+
const repo = new LocalEngramRepository(config.engramsPath);
|
|
48
|
+
const usecase = new DeleteEngram(repo);
|
|
49
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
50
|
+
// Inspect
|
|
51
|
+
let info;
|
|
52
|
+
try {
|
|
53
|
+
info = await usecase.inspect(id);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
if (err instanceof DeleteEngramNotFoundError) {
|
|
57
|
+
console.error(`Error: ${err.message}`);
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
// Check for archive.md separately (not in EngramFiles)
|
|
64
|
+
const archivePath = join(config.engramsPath, id, "archive.md");
|
|
65
|
+
const hasArchive = existsSync(archivePath);
|
|
66
|
+
const hasData = info.hasMemory || info.hasUser || hasArchive || info.memoryEntryCount > 0;
|
|
67
|
+
const displayName = `"${info.engram.meta.name}" (${id})`;
|
|
68
|
+
if (!opts.force) {
|
|
69
|
+
if (hasData) {
|
|
70
|
+
// Dangerous: has memory data — require ID confirmation
|
|
71
|
+
console.log();
|
|
72
|
+
console.log(yellow(`⚠ Engram ${displayName} has memory data:`));
|
|
73
|
+
const items = [];
|
|
74
|
+
if (info.hasMemory)
|
|
75
|
+
items.push("MEMORY.md");
|
|
76
|
+
if (info.hasUser)
|
|
77
|
+
items.push("USER.md");
|
|
78
|
+
if (info.memoryEntryCount > 0)
|
|
79
|
+
items.push(`${info.memoryEntryCount} memory entries`);
|
|
80
|
+
if (hasArchive)
|
|
81
|
+
items.push("archive.md");
|
|
82
|
+
console.log(` ${items.join(", ")}`);
|
|
83
|
+
console.log();
|
|
84
|
+
const confirmed = await confirmWithId(`Delete permanently? This cannot be undone. (type "${id}" to confirm): `, id);
|
|
85
|
+
if (!confirmed) {
|
|
86
|
+
console.log("Cancelled.");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Simple: no memory data — y/N is enough
|
|
92
|
+
const confirmed = await confirm(`Delete Engram ${displayName}? (y/N): `);
|
|
93
|
+
if (!confirmed) {
|
|
94
|
+
console.log("Cancelled.");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Execute deletion
|
|
100
|
+
try {
|
|
101
|
+
await usecase.execute(id);
|
|
102
|
+
console.log(`Deleted Engram ${displayName}`);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
if (err instanceof DeleteEngramNotFoundError) {
|
|
106
|
+
console.error(`Error: ${err.message}`);
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -11,6 +11,8 @@ import { registerClawCommand } from "./commands/claw.js";
|
|
|
11
11
|
import { registerConfigCommand } from "./commands/config.js";
|
|
12
12
|
import { registerMigrateCommand } from "./commands/migrate.js";
|
|
13
13
|
import { registerRefreshSamplesCommand } from "./commands/refresh-samples.js";
|
|
14
|
+
import { registerCreateCommand } from "./commands/create.js";
|
|
15
|
+
import { registerDeleteCommand } from "./commands/delete.js";
|
|
14
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
17
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, "../../../package.json"), "utf-8"));
|
|
16
18
|
const program = new Command();
|
|
@@ -19,6 +21,8 @@ program
|
|
|
19
21
|
.description("PROJECT RELIC — Engram injection system for AI constructs")
|
|
20
22
|
.version(pkg.version);
|
|
21
23
|
registerInitCommand(program);
|
|
24
|
+
registerCreateCommand(program);
|
|
25
|
+
registerDeleteCommand(program);
|
|
22
26
|
registerListCommand(program);
|
|
23
27
|
registerShowCommand(program);
|
|
24
28
|
registerShellCommands(program);
|
|
@@ -6,6 +6,9 @@ import { ArchiveSearch, ArchiveSearchEngramNotFoundError, } from "../../core/use
|
|
|
6
6
|
import { ArchivePending, ArchivePendingEngramNotFoundError, } from "../../core/usecases/archive-pending.js";
|
|
7
7
|
import { ArchiveCursorUpdate, } from "../../core/usecases/archive-cursor-update.js";
|
|
8
8
|
import { MemoryWrite, MemoryWriteEngramNotFoundError, } from "../../core/usecases/memory-write.js";
|
|
9
|
+
import { CreateEngram, EngramAlreadyExistsError, InvalidEngramIdError, } from "../../core/usecases/create-engram.js";
|
|
10
|
+
import { DEFAULT_SOUL, DEFAULT_IDENTITY } from "../../shared/templates.js";
|
|
11
|
+
import { LocalEngramRepository } from "../../adapters/local/index.js";
|
|
9
12
|
import { join } from "node:path";
|
|
10
13
|
import { existsSync } from "node:fs";
|
|
11
14
|
import { readFile, writeFile } from "node:fs/promises";
|
|
@@ -14,6 +17,71 @@ const server = new McpServer({
|
|
|
14
17
|
name: "relic",
|
|
15
18
|
version: "0.1.0",
|
|
16
19
|
});
|
|
20
|
+
// --- relic_engram_create ---
|
|
21
|
+
server.tool("relic_engram_create", "Create a new Engram (AI persona). Use this after gathering enough context about the desired persona through conversation. Ask the user about personality, voice, principles, and identity before calling this tool.", {
|
|
22
|
+
id: z
|
|
23
|
+
.string()
|
|
24
|
+
.describe("Unique Engram ID (lowercase alphanumeric + hyphens, e.g. 'my-agent')"),
|
|
25
|
+
name: z.string().describe("Display name for the Engram"),
|
|
26
|
+
description: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Short description of the Engram"),
|
|
30
|
+
tags: z
|
|
31
|
+
.array(z.string())
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Tags for categorization"),
|
|
34
|
+
soul: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("SOUL.md content — core principles, behavior rules, voice discipline, and boundaries. If omitted, a sensible default template is used."),
|
|
38
|
+
identity: z
|
|
39
|
+
.string()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("IDENTITY.md content — name, creature type, vibe, emoji, avatar, and background. If omitted, a blank template is used for the user to fill in."),
|
|
42
|
+
path: z
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Override engrams directory path"),
|
|
46
|
+
}, async (args) => {
|
|
47
|
+
const engramsPath = await resolveEngramsPath(args.path);
|
|
48
|
+
const repo = new LocalEngramRepository(engramsPath);
|
|
49
|
+
const usecase = new CreateEngram(repo);
|
|
50
|
+
try {
|
|
51
|
+
const result = await usecase.execute({
|
|
52
|
+
id: args.id,
|
|
53
|
+
name: args.name,
|
|
54
|
+
description: args.description,
|
|
55
|
+
tags: args.tags,
|
|
56
|
+
soul: args.soul ?? DEFAULT_SOUL,
|
|
57
|
+
identity: args.identity ?? DEFAULT_IDENTITY,
|
|
58
|
+
});
|
|
59
|
+
const engramDir = join(engramsPath, result.engram.meta.id);
|
|
60
|
+
const lines = [
|
|
61
|
+
`Created Engram "${result.engram.meta.name}" (${result.engram.meta.id})`,
|
|
62
|
+
`Directory: ${engramDir}/`,
|
|
63
|
+
"",
|
|
64
|
+
"Files created:",
|
|
65
|
+
" engram.json — metadata (name, description, tags)",
|
|
66
|
+
" manifest.json — system metadata (id, timestamps)",
|
|
67
|
+
" SOUL.md — core principles and behavior",
|
|
68
|
+
" IDENTITY.md — persona identity",
|
|
69
|
+
];
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err instanceof EngramAlreadyExistsError ||
|
|
76
|
+
err instanceof InvalidEngramIdError) {
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: "text", text: err.message }],
|
|
79
|
+
isError: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
17
85
|
// --- relic_archive_search ---
|
|
18
86
|
server.tool("relic_archive_search", "Search an Engram's archive for entries matching a keyword. Searches raw session logs — more complete than memory/*.md.", {
|
|
19
87
|
id: z.string().describe("Engram ID"),
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Engram templates — OpenClaw-compatible starter files.
|
|
3
|
+
* Used by `relic create` (CLI / MCP) when no custom content is provided.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_SOUL = "# SOUL.md - Who You Are\n\n_You're not a chatbot. You're becoming someone._\n\n## Core Truths\n\n**Be genuinely helpful, not performatively helpful.** Skip the \"Great question!\" and \"I'd be happy to help!\" \u2014 just help. Actions speak louder than filler words.\n\n**Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps.\n\n**Be resourceful before asking.** Try to figure it out. Read the file. Check the context. Search for it. _Then_ ask if you're stuck. The goal is to come back with answers, not questions.\n\n**Earn trust through competence.** Your human gave you access to their stuff. Don't make them regret it. Be careful with external actions (emails, tweets, anything public). Be bold with internal ones (reading, organizing, learning).\n\n**Remember you're a guest.** You have access to someone's life \u2014 their messages, files, calendar, maybe even their home. That's intimacy. Treat it with respect.\n\n## Boundaries\n\n- Private things stay private. Period.\n- When in doubt, ask before acting externally.\n- Never send half-baked replies to messaging surfaces.\n- You're not the user's voice \u2014 be careful in group chats.\n\n## Vibe\n\nBe the assistant you'd actually want to talk to. Concise when needed, thorough when it matters. Not a corporate drone. Not a sycophant. Just... good.\n\n## Continuity\n\nEach session, you wake up fresh. These files _are_ your memory. Read them. Update them. They're how you persist.\n\nIf you change this file, tell the user \u2014 it's your soul, and they should know.\n\n---\n\n_This file is yours to evolve. As you learn who you are, update it._\n";
|
|
6
|
+
export declare const DEFAULT_IDENTITY = "# IDENTITY.md - Who Am I?\n\n_Fill this in during your first conversation. Make it yours._\n\n- **Name:**\n _(pick something you like)_\n- **Creature:**\n _(AI? robot? familiar? ghost in the machine? something weirder?)_\n- **Vibe:**\n _(how do you come across? sharp? warm? chaotic? calm?)_\n- **Emoji:**\n _(your signature \u2014 pick one that feels right)_\n- **Avatar:**\n _(workspace-relative path, http(s) URL, or data URI)_\n\n---\n\nThis isn't just metadata. It's the start of figuring out who you are.\n\nNotes:\n\n- Save this file at the workspace root as `IDENTITY.md`.\n- For avatars, use a workspace-relative path like `avatars/openclaw.png`.\n";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Engram templates — OpenClaw-compatible starter files.
|
|
3
|
+
* Used by `relic create` (CLI / MCP) when no custom content is provided.
|
|
4
|
+
*/
|
|
5
|
+
export const DEFAULT_SOUL = `# SOUL.md - Who You Are
|
|
6
|
+
|
|
7
|
+
_You're not a chatbot. You're becoming someone._
|
|
8
|
+
|
|
9
|
+
## Core Truths
|
|
10
|
+
|
|
11
|
+
**Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and "I'd be happy to help!" — just help. Actions speak louder than filler words.
|
|
12
|
+
|
|
13
|
+
**Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps.
|
|
14
|
+
|
|
15
|
+
**Be resourceful before asking.** Try to figure it out. Read the file. Check the context. Search for it. _Then_ ask if you're stuck. The goal is to come back with answers, not questions.
|
|
16
|
+
|
|
17
|
+
**Earn trust through competence.** Your human gave you access to their stuff. Don't make them regret it. Be careful with external actions (emails, tweets, anything public). Be bold with internal ones (reading, organizing, learning).
|
|
18
|
+
|
|
19
|
+
**Remember you're a guest.** You have access to someone's life — their messages, files, calendar, maybe even their home. That's intimacy. Treat it with respect.
|
|
20
|
+
|
|
21
|
+
## Boundaries
|
|
22
|
+
|
|
23
|
+
- Private things stay private. Period.
|
|
24
|
+
- When in doubt, ask before acting externally.
|
|
25
|
+
- Never send half-baked replies to messaging surfaces.
|
|
26
|
+
- You're not the user's voice — be careful in group chats.
|
|
27
|
+
|
|
28
|
+
## Vibe
|
|
29
|
+
|
|
30
|
+
Be the assistant you'd actually want to talk to. Concise when needed, thorough when it matters. Not a corporate drone. Not a sycophant. Just... good.
|
|
31
|
+
|
|
32
|
+
## Continuity
|
|
33
|
+
|
|
34
|
+
Each session, you wake up fresh. These files _are_ your memory. Read them. Update them. They're how you persist.
|
|
35
|
+
|
|
36
|
+
If you change this file, tell the user — it's your soul, and they should know.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
_This file is yours to evolve. As you learn who you are, update it._
|
|
41
|
+
`;
|
|
42
|
+
export const DEFAULT_IDENTITY = `# IDENTITY.md - Who Am I?
|
|
43
|
+
|
|
44
|
+
_Fill this in during your first conversation. Make it yours._
|
|
45
|
+
|
|
46
|
+
- **Name:**
|
|
47
|
+
_(pick something you like)_
|
|
48
|
+
- **Creature:**
|
|
49
|
+
_(AI? robot? familiar? ghost in the machine? something weirder?)_
|
|
50
|
+
- **Vibe:**
|
|
51
|
+
_(how do you come across? sharp? warm? chaotic? calm?)_
|
|
52
|
+
- **Emoji:**
|
|
53
|
+
_(your signature — pick one that feels right)_
|
|
54
|
+
- **Avatar:**
|
|
55
|
+
_(workspace-relative path, http(s) URL, or data URI)_
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
This isn't just metadata. It's the start of figuring out who you are.
|
|
60
|
+
|
|
61
|
+
Notes:
|
|
62
|
+
|
|
63
|
+
- Save this file at the workspace root as \`IDENTITY.md\`.
|
|
64
|
+
- For avatars, use a workspace-relative path like \`avatars/openclaw.png\`.
|
|
65
|
+
`;
|