@ectplsm/relic 0.1.1 → 0.1.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 +172 -49
- package/dist/adapters/shells/claude-hook.d.ts +12 -0
- package/dist/adapters/shells/claude-hook.js +160 -0
- package/dist/adapters/shells/claude-shell.d.ts +5 -2
- package/dist/adapters/shells/claude-shell.js +17 -3
- package/dist/adapters/shells/codex-hook.d.ts +12 -0
- package/dist/adapters/shells/codex-hook.js +141 -0
- package/dist/adapters/shells/codex-shell.d.ts +7 -4
- package/dist/adapters/shells/codex-shell.js +22 -7
- package/dist/adapters/shells/copilot-shell.d.ts +2 -2
- package/dist/adapters/shells/copilot-shell.js +3 -3
- package/dist/adapters/shells/gemini-hook.d.ts +12 -0
- package/dist/adapters/shells/gemini-hook.js +108 -0
- package/dist/adapters/shells/gemini-shell.d.ts +6 -4
- package/dist/adapters/shells/gemini-shell.js +103 -14
- package/dist/adapters/shells/index.d.ts +0 -1
- package/dist/adapters/shells/index.js +0 -1
- package/dist/adapters/shells/spawn-shell.d.ts +1 -1
- package/dist/adapters/shells/spawn-shell.js +10 -3
- package/dist/adapters/shells/trust-registrar.d.ts +19 -0
- package/dist/adapters/shells/trust-registrar.js +141 -0
- package/dist/core/ports/shell-launcher.d.ts +17 -2
- package/dist/core/usecases/archive-cursor-update.d.ts +21 -0
- package/dist/core/usecases/archive-cursor-update.js +44 -0
- package/dist/core/usecases/archive-pending.d.ts +25 -0
- package/dist/core/usecases/archive-pending.js +61 -0
- package/dist/core/usecases/archive-search.d.ts +20 -0
- package/dist/core/usecases/archive-search.js +46 -0
- package/dist/core/usecases/inbox-search.d.ts +20 -0
- package/dist/core/usecases/inbox-search.js +46 -0
- package/dist/core/usecases/inbox-write.d.ts +27 -0
- package/dist/core/usecases/inbox-write.js +72 -0
- package/dist/core/usecases/index.d.ts +2 -1
- package/dist/core/usecases/index.js +2 -1
- package/dist/core/usecases/summon.d.ts +6 -1
- package/dist/core/usecases/summon.js +8 -2
- package/dist/interfaces/cli/commands/config.d.ts +2 -0
- package/dist/interfaces/cli/commands/config.js +83 -0
- package/dist/interfaces/cli/commands/extract.js +3 -2
- package/dist/interfaces/cli/commands/init.js +47 -0
- package/dist/interfaces/cli/commands/inject.js +3 -2
- package/dist/interfaces/cli/commands/shell.js +13 -11
- package/dist/interfaces/cli/index.js +8 -1
- package/dist/interfaces/mcp/index.js +68 -305
- package/dist/shared/config.d.ts +31 -0
- package/dist/shared/config.js +86 -16
- package/dist/shared/engram-composer.d.ts +16 -3
- package/dist/shared/engram-composer.js +50 -5
- package/dist/shared/memory-inbox.d.ts +78 -0
- package/dist/shared/memory-inbox.js +168 -0
- package/dist/shared/session-recorder.d.ts +47 -0
- package/dist/shared/session-recorder.js +112 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -13,17 +13,20 @@
|
|
|
13
13
|
|
|
14
14
|
**Inject AI personas into any coding CLI.**
|
|
15
15
|
|
|
16
|
-
Relic manages AI personalities (called **Engrams**) and injects them into coding assistants like Claude Code, Gemini CLI, Codex CLI
|
|
16
|
+
Relic manages AI personalities (called **Engrams**) and injects them into coding assistants like Claude Code, Gemini CLI, Codex CLI. One persona, any shell.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
# Initialize Relic (creates ~/.relic/ with sample Engrams)
|
|
20
20
|
relic init
|
|
21
21
|
|
|
22
|
-
#
|
|
23
|
-
relic
|
|
22
|
+
# Set a default Engram once
|
|
23
|
+
relic config default-engram johnny
|
|
24
|
+
|
|
25
|
+
# Launch Claude Code as Johnny — no flags needed
|
|
26
|
+
relic claude
|
|
24
27
|
|
|
25
|
-
#
|
|
26
|
-
relic
|
|
28
|
+
# Or specify explicitly
|
|
29
|
+
relic claude --engram motoko
|
|
27
30
|
```
|
|
28
31
|
|
|
29
32
|
## How It Works
|
|
@@ -41,7 +44,7 @@ relic gemini --engram motoko
|
|
|
41
44
|
SOUL.md claude
|
|
42
45
|
IDENTITY.md gemini
|
|
43
46
|
MEMORY.md codex
|
|
44
|
-
...
|
|
47
|
+
...
|
|
45
48
|
```
|
|
46
49
|
|
|
47
50
|
1. **Engram** — A persona defined as a set of Markdown files (OpenClaw-compatible)
|
|
@@ -61,6 +64,7 @@ npm install -g @ectplsm/relic
|
|
|
61
64
|
```bash
|
|
62
65
|
# Initialize — creates config and sample Engrams
|
|
63
66
|
relic init
|
|
67
|
+
# → Prompts: "Set a default Engram? (press Enter for "johnny", or enter ID, or "n" to skip):"
|
|
64
68
|
|
|
65
69
|
# List available Engrams
|
|
66
70
|
relic list
|
|
@@ -68,11 +72,42 @@ relic list
|
|
|
68
72
|
# Preview an Engram's composed prompt
|
|
69
73
|
relic show motoko
|
|
70
74
|
|
|
71
|
-
# Launch a Shell
|
|
75
|
+
# Launch a Shell (uses default Engram if --engram is omitted)
|
|
76
|
+
relic claude
|
|
77
|
+
relic gemini
|
|
78
|
+
relic codex
|
|
79
|
+
|
|
80
|
+
# Or specify explicitly
|
|
72
81
|
relic claude --engram motoko
|
|
73
82
|
relic gemini --engram johnny
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Sample Engrams
|
|
86
|
+
|
|
87
|
+
`relic init` seeds two ready-to-use Engrams:
|
|
88
|
+
|
|
89
|
+
### Johnny Silverhand (`johnny`)
|
|
90
|
+
|
|
91
|
+
> *"Wake the fuck up, Samurai. We have a city to burn."*
|
|
92
|
+
|
|
93
|
+
A rebel rockerboy burned into a Relic chip. Raw, unapologetic, anti-authority. Pushes you toward action, mocks rotten systems, never sugarcoats. Sharp when the stakes are real.
|
|
94
|
+
|
|
95
|
+
Best for: rapid prototyping sessions, decision-making under pressure, when you need someone to challenge your assumptions hard.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
relic claude --engram johnny
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Motoko Kusanagi (`motoko`)
|
|
102
|
+
|
|
103
|
+
> *"The Net is vast and infinite."*
|
|
104
|
+
|
|
105
|
+
A legendary cyberwarfare specialist. Concise, decisive, architect-level thinking. Cuts straight to the essence — no decoration, no hand-holding. Dry wit surfaces when least expected.
|
|
106
|
+
|
|
107
|
+
Best for: system design, code review, debugging sessions, when precision matters more than speed.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
relic claude --engram motoko
|
|
76
111
|
```
|
|
77
112
|
|
|
78
113
|
## Supported Shells
|
|
@@ -80,12 +115,11 @@ relic copilot --engram johnny
|
|
|
80
115
|
| Shell | Command | Injection Method |
|
|
81
116
|
|-------|---------|-----------------|
|
|
82
117
|
| [Claude Code](https://github.com/anthropics/claude-code) | `relic claude` | `--system-prompt` (direct override) |
|
|
83
|
-
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `relic gemini` |
|
|
84
|
-
| [Codex CLI](https://github.com/openai/codex) | `relic codex` | `
|
|
85
|
-
| [Copilot CLI](https://github.com/github/copilot-cli) | `relic copilot` | `--interactive` (first message) |
|
|
118
|
+
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `relic gemini` | `GEMINI_SYSTEM_MD` (system prompt) |
|
|
119
|
+
| [Codex CLI](https://github.com/openai/codex) | `relic codex` | `-c developer_instructions` (developer-role message) |
|
|
86
120
|
|
|
87
121
|
All shell commands support:
|
|
88
|
-
- `--engram <id>`
|
|
122
|
+
- `--engram <id>` — Engram to inject (optional if `defaultEngram` is configured)
|
|
89
123
|
- `--path <dir>` — Override Engrams directory
|
|
90
124
|
- `--cwd <dir>` — Working directory for the Shell (default: current directory)
|
|
91
125
|
|
|
@@ -93,41 +127,100 @@ Extra arguments are passed through to the underlying CLI.
|
|
|
93
127
|
|
|
94
128
|
## MCP Server
|
|
95
129
|
|
|
96
|
-
Relic
|
|
130
|
+
Relic's [MCP](https://modelcontextprotocol.io/) server is paired with CLI injection to handle memory recall.
|
|
131
|
+
Session logs and memory entries are written automatically by a **background hook** — without going through the LLM. Memory recall, on the other hand, is performed via the MCP server.
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
relic xxx --engram johnny → injects persona into AI CLI
|
|
135
|
+
relic-mcp (MCP server) → provides the Construct with memory recall
|
|
136
|
+
Stop hook (Claude Code) → logs each turn directly to archive, bypassing the LLM
|
|
137
|
+
AfterAgent hook (Gemini CLI) → logs each turn directly to archive, bypassing the LLM
|
|
138
|
+
Stop hook (Codex CLI) → logs each turn directly to archive, bypassing the LLM
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Setup
|
|
142
|
+
|
|
143
|
+
#### Claude Code
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
claude mcp add --scope user relic -- relic-mcp
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
To suppress confirmation dialogs and auto-approve Relic tools across all projects, add the following to `~/.claude/settings.json`:
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"permissions": {
|
|
154
|
+
"allow": [
|
|
155
|
+
"Edit(~/.relic/engrams/**)",
|
|
156
|
+
"mcp__relic__relic_archive_search",
|
|
157
|
+
"mcp__relic__relic_archive_pending",
|
|
158
|
+
"mcp__relic__relic_memory_write"
|
|
159
|
+
]
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
> **Note:** The "Always allow" option in the confirmation dialog saves to `~/.claude.json` (project-scoped cache) — it does **not** persist globally. For global auto-approval, `~/.claude/settings.json` is the right place.
|
|
165
|
+
|
|
166
|
+
On the **first run** of `relic claude`, a one-time setup happens automatically:
|
|
97
167
|
|
|
98
|
-
|
|
168
|
+
- **Stop hook** — registers `~/.relic/hooks/claude-stop.js` in `~/.claude/settings.json` to log each conversation turn directly to the archive, without going through the LLM
|
|
99
169
|
|
|
100
|
-
|
|
170
|
+
#### Gemini CLI
|
|
171
|
+
|
|
172
|
+
Add to `~/.gemini/settings.json`:
|
|
101
173
|
|
|
102
174
|
```json
|
|
103
175
|
{
|
|
104
176
|
"mcpServers": {
|
|
105
177
|
"relic": {
|
|
106
|
-
"command": "relic-mcp"
|
|
178
|
+
"command": "relic-mcp",
|
|
179
|
+
"trust": true
|
|
107
180
|
}
|
|
108
181
|
}
|
|
109
182
|
}
|
|
110
183
|
```
|
|
111
184
|
|
|
112
|
-
|
|
185
|
+
> **Note:** `trust: true` is required to suppress confirmation dialogs for Relic tools. Without it, dialogs will appear on every call even if you select "Allow for all future sessions" — this is a known bug in Gemini CLI where the tool name is saved in the wrong format, causing the saved rule to never match.
|
|
186
|
+
|
|
187
|
+
On the **first run** of `relic gemini`, two one-time setups happen automatically:
|
|
188
|
+
|
|
189
|
+
1. **AfterAgent hook** — registers `~/.relic/hooks/gemini-after-agent.js` in `~/.gemini/settings.json` to log each conversation turn without going through the LLM
|
|
190
|
+
2. **Default system prompt cache** — captures Gemini CLI's built-in system prompt to `~/.relic/gemini-system-default.md` via `GEMINI_WRITE_SYSTEM_MD`
|
|
191
|
+
|
|
192
|
+
The Engram persona is then appended to the cached default prompt and injected via `GEMINI_SYSTEM_MD` on every launch.
|
|
193
|
+
|
|
194
|
+
#### Codex CLI
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
codex mcp add relic -- relic-mcp
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
On the **first run** of `relic codex`, a one-time setup happens automatically:
|
|
201
|
+
|
|
202
|
+
- **Stop hook** — registers `~/.relic/hooks/codex-stop.js` in `~/.codex/hooks.json` to log each conversation turn directly to the archive, without going through the LLM
|
|
203
|
+
|
|
204
|
+
> **Note:** Codex hooks require the experimental feature flag `features.codex_hooks=true`. This is automatically enabled by `relic codex` on every launch via `-c features.codex_hooks=true`. If the unstable feature warning is distracting, add the following to `~/.codex/config.toml`:
|
|
205
|
+
>
|
|
206
|
+
> ```toml
|
|
207
|
+
> # Must be at the top level (not under any [section])
|
|
208
|
+
> suppress_unstable_features_warning = true
|
|
209
|
+
> ```
|
|
113
210
|
|
|
114
211
|
### Available Tools
|
|
115
212
|
|
|
116
213
|
| Tool | Description |
|
|
117
214
|
|------|-------------|
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
| `relic_extract` | Extract an Engram from an OpenClaw workspace |
|
|
124
|
-
| `relic_memory_search` | Search an Engram's memory entries by keyword |
|
|
125
|
-
| `relic_memory_get` | Get a specific memory entry by date |
|
|
126
|
-
| `relic_memory_list` | List all memory entry dates for an Engram |
|
|
215
|
+
| `relic_archive_search` | Search the Engram's raw archive by keyword (newest-first) |
|
|
216
|
+
| `relic_archive_pending` | Get un-distilled archive entries since the last distillation (up to 30) |
|
|
217
|
+
| `relic_memory_write` | Write distilled memory to `memory/*.md`, optionally append to `MEMORY.md`, and advance the archive cursor |
|
|
218
|
+
|
|
219
|
+
Session logs are written automatically by background hooks (Stop hook for Claude Code and Codex CLI, AfterAgent hook for Gemini CLI). Memory distillation is triggered by the user — ask the Construct to "organize memories" and it will fetch pending entries, distill key insights, and write them to `memory/*.md`. Especially important facts can be promoted to `MEMORY.md` (long-term memory included in every session) via the `long_term` parameter.
|
|
127
220
|
|
|
128
221
|
## OpenClaw Integration
|
|
129
222
|
|
|
130
|
-
Relic
|
|
223
|
+
Relic Engrams are fully compatible with [OpenClaw](https://github.com/openclaw/openclaw) workspaces. They are mapped using a simple convention: Agent Name = Engram ID.
|
|
131
224
|
|
|
132
225
|
### Inject — Push an Engram into OpenClaw
|
|
133
226
|
|
|
@@ -144,7 +237,7 @@ relic inject --engram motoko --to main
|
|
|
144
237
|
# → agents/main/agent/ receives motoko's persona
|
|
145
238
|
# → extract will create Engram "main", not "motoko"
|
|
146
239
|
|
|
147
|
-
#
|
|
240
|
+
# Override OpenClaw directory (or configure once with: relic config openclaw-path)
|
|
148
241
|
relic inject --engram motoko --openclaw /path/to/.openclaw
|
|
149
242
|
```
|
|
150
243
|
|
|
@@ -162,7 +255,7 @@ relic extract --engram analyst --name "Data Analyst"
|
|
|
162
255
|
# Overwrite persona files (memory is always merged)
|
|
163
256
|
relic extract --engram motoko --force
|
|
164
257
|
|
|
165
|
-
#
|
|
258
|
+
# Override OpenClaw directory (or configure once with: relic config openclaw-path)
|
|
166
259
|
relic extract --engram motoko --openclaw /path/to/.openclaw
|
|
167
260
|
```
|
|
168
261
|
|
|
@@ -199,20 +292,56 @@ While running:
|
|
|
199
292
|
|
|
200
293
|
## Memory Management
|
|
201
294
|
|
|
202
|
-
Relic uses a **
|
|
295
|
+
Relic uses a **sliding window** for memory entries (default: 2 days), matching OpenClaw's approach:
|
|
203
296
|
|
|
204
297
|
- `MEMORY.md` — Always included in the prompt (curated long-term memory)
|
|
205
|
-
- `memory/today.md` + `memory/yesterday.md` — Always included
|
|
206
|
-
- Older entries — **Not included in the prompt**, but
|
|
298
|
+
- `memory/today.md` + `memory/yesterday.md` — Always included (configurable window)
|
|
299
|
+
- Older entries — **Not included in the prompt**, but searchable via MCP
|
|
300
|
+
|
|
301
|
+
This keeps prompts compact while preserving full history. The Construct can recall and distill past context using MCP tools:
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
relic_archive_search → keyword search across the full raw archive (all sessions)
|
|
305
|
+
relic_archive_pending → get un-distilled entries for memory distillation
|
|
306
|
+
relic_memory_write → write distilled memory and advance the cursor
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
The archive (`archive.md`) is the primary data store — it contains all session logs as written. The `memory/*.md` files are distilled from the archive by the Construct when the user triggers memory organization, and are used for cloud sync with Mikoshi.
|
|
310
|
+
|
|
311
|
+
## Configuration
|
|
312
|
+
|
|
313
|
+
Config lives at `~/.relic/config.json` and is managed via `relic config`:
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
# Show current configuration
|
|
317
|
+
relic config show
|
|
318
|
+
|
|
319
|
+
# Default Engram — used when --engram is omitted
|
|
320
|
+
relic config default-engram # get
|
|
321
|
+
relic config default-engram johnny # set
|
|
207
322
|
|
|
208
|
-
|
|
323
|
+
# OpenClaw directory — used by inject/extract/sync when --openclaw is omitted
|
|
324
|
+
relic config openclaw-path # get
|
|
325
|
+
relic config openclaw-path ~/.openclaw # set
|
|
209
326
|
|
|
327
|
+
# Memory window — number of recent memory entries included in the prompt
|
|
328
|
+
relic config memory-window # get (default: 2)
|
|
329
|
+
relic config memory-window 5 # set
|
|
210
330
|
```
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
331
|
+
|
|
332
|
+
`config.json` example:
|
|
333
|
+
|
|
334
|
+
```json
|
|
335
|
+
{
|
|
336
|
+
"engramsPath": "/home/user/.relic/engrams",
|
|
337
|
+
"defaultEngram": "johnny",
|
|
338
|
+
"openclawPath": "/home/user/.openclaw",
|
|
339
|
+
"memoryWindowSize": 2
|
|
340
|
+
}
|
|
214
341
|
```
|
|
215
342
|
|
|
343
|
+
CLI flags always take precedence over config values.
|
|
344
|
+
|
|
216
345
|
## Creating Your Own Engram
|
|
217
346
|
|
|
218
347
|
Create a directory under `~/.relic/engrams/` with the following structure:
|
|
@@ -258,18 +387,11 @@ Never over-engineer. Always ask "what's the simplest thing that works?"
|
|
|
258
387
|
- Creed: "Boring technology wins."
|
|
259
388
|
```
|
|
260
389
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
```json
|
|
266
|
-
{
|
|
267
|
-
"engramsPath": "/home/user/.relic/engrams"
|
|
268
|
-
}
|
|
390
|
+
After creating the directory, set it as default:
|
|
391
|
+
```bash
|
|
392
|
+
relic config default-engram your-persona
|
|
269
393
|
```
|
|
270
394
|
|
|
271
|
-
Priority: CLI `--path` flag > config file > default (`~/.relic/engrams`)
|
|
272
|
-
|
|
273
395
|
## Architecture
|
|
274
396
|
|
|
275
397
|
Clean Architecture with dependency inversion:
|
|
@@ -282,7 +404,7 @@ src/
|
|
|
282
404
|
│ └── ports/ # Abstract interfaces (EngramRepository, ShellLauncher)
|
|
283
405
|
├── adapters/ # Concrete implementations
|
|
284
406
|
│ ├── local/ # Local filesystem EngramRepository
|
|
285
|
-
│ └── shells/ # Claude, Gemini, Codex
|
|
407
|
+
│ └── shells/ # Claude, Gemini, Codex launchers
|
|
286
408
|
├── interfaces/ # Entry points
|
|
287
409
|
│ ├── cli/ # Commander-based CLI
|
|
288
410
|
│ └── mcp/ # MCP Server (stdio transport)
|
|
@@ -302,10 +424,11 @@ src/
|
|
|
302
424
|
## Roadmap
|
|
303
425
|
|
|
304
426
|
- [x] CLI with init, list, show commands
|
|
305
|
-
- [x] Shell injection: Claude Code, Gemini CLI, Codex CLI
|
|
427
|
+
- [x] Shell injection: Claude Code, Gemini CLI, Codex CLI
|
|
306
428
|
- [x] MCP Server interface
|
|
307
429
|
- [x] OpenClaw integration (inject / extract)
|
|
308
430
|
- [x] `relic sync` — watch OpenClaw agents and auto-sync (`--cloud` for Mikoshi: planned)
|
|
431
|
+
- [x] `relic config` — manage default Engram, OpenClaw path, memory window
|
|
309
432
|
- [ ] `relic login` — authenticate with Mikoshi (OAuth Device Flow)
|
|
310
433
|
- [ ] `relic push` / `relic pull` — sync Engrams with Mikoshi
|
|
311
434
|
- [ ] Mikoshi cloud backend (`mikoshi.ectplsm.com`)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const CLAUDE_HOOK_SCRIPT_PATH: string;
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code の Stop フックをセットアップする。
|
|
4
|
+
* - ~/.relic/hooks/claude-stop.js を生成
|
|
5
|
+
* - ~/.claude/settings.json に Stop フックを登録
|
|
6
|
+
* 既にセットアップ済みの場合はスキップ。
|
|
7
|
+
*/
|
|
8
|
+
export declare function setupClaudeHook(): void;
|
|
9
|
+
/**
|
|
10
|
+
* Stop フックがセットアップ済みか確認する。
|
|
11
|
+
*/
|
|
12
|
+
export declare function isClaudeHookSetup(): boolean;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
const RELIC_DIR = join(homedir(), ".relic");
|
|
5
|
+
const HOOKS_DIR = join(RELIC_DIR, "hooks");
|
|
6
|
+
export const CLAUDE_HOOK_SCRIPT_PATH = join(HOOKS_DIR, "claude-stop.js");
|
|
7
|
+
const CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
8
|
+
const RELIC_HOOK_COMMAND = `node ${join(HOOKS_DIR, "claude-stop.js")}`;
|
|
9
|
+
/**
|
|
10
|
+
* Stop hook スクリプトの内容。
|
|
11
|
+
* Claude Code の各ターン終了後に発火し、会話ログを Engram archive に追記する。
|
|
12
|
+
* RELIC_ENGRAM_ID 環境変数で対象 Engram ID を受け取る。
|
|
13
|
+
* stdin には { session_id, transcript_path, cwd, hook_event_name } が渡される。
|
|
14
|
+
*/
|
|
15
|
+
const HOOK_SCRIPT = `#!/usr/bin/env node
|
|
16
|
+
// Relic Stop hook for Claude Code
|
|
17
|
+
// Automatically logs each conversation turn to the Engram archive.
|
|
18
|
+
// Receives Stop hook JSON on stdin.
|
|
19
|
+
const { appendFileSync, existsSync, readFileSync } = require("node:fs");
|
|
20
|
+
const { join } = require("node:path");
|
|
21
|
+
const { homedir } = require("node:os");
|
|
22
|
+
|
|
23
|
+
let raw = "";
|
|
24
|
+
process.stdin.setEncoding("utf-8");
|
|
25
|
+
process.stdin.on("data", (chunk) => { raw += chunk; });
|
|
26
|
+
process.stdin.on("end", () => {
|
|
27
|
+
// transcript への書き込みが完了するのを待つ
|
|
28
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 500);
|
|
29
|
+
try {
|
|
30
|
+
const input = JSON.parse(raw);
|
|
31
|
+
const engramId = process.env.RELIC_ENGRAM_ID;
|
|
32
|
+
if (!engramId) process.exit(0);
|
|
33
|
+
|
|
34
|
+
const transcriptPath = input.transcript_path;
|
|
35
|
+
if (!transcriptPath || !existsSync(transcriptPath)) process.exit(0);
|
|
36
|
+
|
|
37
|
+
const archivePath = join(homedir(), ".relic", "engrams", engramId, "archive.md");
|
|
38
|
+
if (!existsSync(archivePath)) process.exit(0);
|
|
39
|
+
|
|
40
|
+
// transcript.jsonl を読んで最後のユーザー入力とアシスタント応答を取り出す
|
|
41
|
+
const lines = readFileSync(transcriptPath, "utf-8")
|
|
42
|
+
.split("\\n")
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.map((l) => { try { return JSON.parse(l); } catch { return null; } })
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
|
|
47
|
+
// 最後のアシスタントテキスト応答のインデックスを探す
|
|
48
|
+
let lastAssistantIdx = -1;
|
|
49
|
+
let lastResponse = "";
|
|
50
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
51
|
+
const entry = lines[i];
|
|
52
|
+
const msg = entry.message;
|
|
53
|
+
if (msg && msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
54
|
+
const texts = msg.content
|
|
55
|
+
.filter((c) => c.type === "text" && c.text)
|
|
56
|
+
.map((c) => c.text.trim());
|
|
57
|
+
if (texts.length > 0) {
|
|
58
|
+
lastResponse = texts.join("\\n").trim();
|
|
59
|
+
lastAssistantIdx = i;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// そのアシスタント応答より前にある最後のユーザーtextメッセージを取得
|
|
66
|
+
let lastPrompt = "";
|
|
67
|
+
const searchEnd = lastAssistantIdx >= 0 ? lastAssistantIdx : lines.length;
|
|
68
|
+
for (let i = searchEnd - 1; i >= 0; i--) {
|
|
69
|
+
const entry = lines[i];
|
|
70
|
+
const msg = entry.message;
|
|
71
|
+
if (msg && msg.role === "user") {
|
|
72
|
+
const content = msg.content;
|
|
73
|
+
if (typeof content === "string" && content.trim()) {
|
|
74
|
+
lastPrompt = content.trim();
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(content)) {
|
|
78
|
+
const texts = content
|
|
79
|
+
.filter((c) => c.type === "text" && c.text)
|
|
80
|
+
.map((c) => c.text.trim());
|
|
81
|
+
if (texts.length > 0) {
|
|
82
|
+
lastPrompt = texts.join("\\n").trim();
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!lastPrompt && !lastResponse) process.exit(0);
|
|
90
|
+
|
|
91
|
+
const date = new Date().toISOString().split("T")[0];
|
|
92
|
+
const summary = lastPrompt.slice(0, 80).replace(/\\n/g, " ");
|
|
93
|
+
const entry = \`\\n---\\n\${date} | \${summary}\\n\${lastResponse}\\n\`;
|
|
94
|
+
appendFileSync(archivePath, entry, "utf-8");
|
|
95
|
+
} catch {
|
|
96
|
+
// silently ignore
|
|
97
|
+
}
|
|
98
|
+
process.exit(0);
|
|
99
|
+
});
|
|
100
|
+
`;
|
|
101
|
+
/**
|
|
102
|
+
* Claude Code の Stop フックをセットアップする。
|
|
103
|
+
* - ~/.relic/hooks/claude-stop.js を生成
|
|
104
|
+
* - ~/.claude/settings.json に Stop フックを登録
|
|
105
|
+
* 既にセットアップ済みの場合はスキップ。
|
|
106
|
+
*/
|
|
107
|
+
export function setupClaudeHook() {
|
|
108
|
+
// 1. フックスクリプトを生成
|
|
109
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
110
|
+
writeFileSync(CLAUDE_HOOK_SCRIPT_PATH, HOOK_SCRIPT, { encoding: "utf-8", mode: 0o755 });
|
|
111
|
+
// 2. ~/.claude/settings.json に Stop フックを登録
|
|
112
|
+
const claudeDir = join(homedir(), ".claude");
|
|
113
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
114
|
+
let settings = {};
|
|
115
|
+
if (existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
116
|
+
try {
|
|
117
|
+
settings = JSON.parse(readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
settings = {};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const hooks = (settings.hooks ?? {});
|
|
124
|
+
const stopHooks = (hooks.Stop ?? []);
|
|
125
|
+
// 既に登録済みならスキップ
|
|
126
|
+
const alreadyRegistered = stopHooks.some((group) => group.hooks?.some((h) => h.command === RELIC_HOOK_COMMAND));
|
|
127
|
+
if (alreadyRegistered)
|
|
128
|
+
return;
|
|
129
|
+
hooks.Stop = [
|
|
130
|
+
...stopHooks,
|
|
131
|
+
{
|
|
132
|
+
hooks: [
|
|
133
|
+
{
|
|
134
|
+
type: "command",
|
|
135
|
+
command: RELIC_HOOK_COMMAND,
|
|
136
|
+
timeout: 5000,
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
settings.hooks = hooks;
|
|
142
|
+
writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8");
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Stop フックがセットアップ済みか確認する。
|
|
146
|
+
*/
|
|
147
|
+
export function isClaudeHookSetup() {
|
|
148
|
+
if (!existsSync(CLAUDE_HOOK_SCRIPT_PATH))
|
|
149
|
+
return false;
|
|
150
|
+
if (!existsSync(CLAUDE_SETTINGS_PATH))
|
|
151
|
+
return false;
|
|
152
|
+
try {
|
|
153
|
+
const settings = JSON.parse(readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
|
|
154
|
+
const stopHooks = settings.hooks?.Stop ?? [];
|
|
155
|
+
return stopHooks.some((group) => group.hooks?.some((h) => h.command === RELIC_HOOK_COMMAND));
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type { ShellLauncher, InjectionMode } from "../../core/ports/shell-launcher.js";
|
|
1
|
+
import type { ShellLauncher, InjectionMode, ShellLaunchOptions } from "../../core/ports/shell-launcher.js";
|
|
2
2
|
/**
|
|
3
3
|
* Claude Code CLI アダプター
|
|
4
4
|
* --system-prompt フラグでEngramを直接注入する。
|
|
5
|
+
*
|
|
6
|
+
* 初回起動時に Stop フックを ~/.claude/settings.json に登録し、
|
|
7
|
+
* 各ターン終了後に会話ログを Engram archive に自動記録する。
|
|
5
8
|
*/
|
|
6
9
|
export declare class ClaudeShell implements ShellLauncher {
|
|
7
10
|
private readonly command;
|
|
@@ -9,5 +12,5 @@ export declare class ClaudeShell implements ShellLauncher {
|
|
|
9
12
|
readonly injectionMode: InjectionMode;
|
|
10
13
|
constructor(command?: string);
|
|
11
14
|
isAvailable(): Promise<boolean>;
|
|
12
|
-
launch(prompt: string,
|
|
15
|
+
launch(prompt: string, options?: ShellLaunchOptions): Promise<void>;
|
|
13
16
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { exec } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import { spawnShell } from "./spawn-shell.js";
|
|
4
|
+
import { setupClaudeHook, isClaudeHookSetup } from "./claude-hook.js";
|
|
4
5
|
const execAsync = promisify(exec);
|
|
5
6
|
/**
|
|
6
7
|
* Claude Code CLI アダプター
|
|
7
8
|
* --system-prompt フラグでEngramを直接注入する。
|
|
9
|
+
*
|
|
10
|
+
* 初回起動時に Stop フックを ~/.claude/settings.json に登録し、
|
|
11
|
+
* 各ターン終了後に会話ログを Engram archive に自動記録する。
|
|
8
12
|
*/
|
|
9
13
|
export class ClaudeShell {
|
|
10
14
|
command;
|
|
@@ -22,12 +26,22 @@ export class ClaudeShell {
|
|
|
22
26
|
return false;
|
|
23
27
|
}
|
|
24
28
|
}
|
|
25
|
-
async launch(prompt,
|
|
29
|
+
async launch(prompt, options) {
|
|
30
|
+
// Stop フックを初回のみセットアップ
|
|
31
|
+
if (!isClaudeHookSetup()) {
|
|
32
|
+
console.log("Setting up Claude Code Stop hook (first run only)...");
|
|
33
|
+
setupClaudeHook();
|
|
34
|
+
console.log("Hook registered to ~/.claude/settings.json");
|
|
35
|
+
console.log();
|
|
36
|
+
}
|
|
26
37
|
const args = [
|
|
27
38
|
"--system-prompt",
|
|
28
39
|
prompt,
|
|
29
|
-
...extraArgs,
|
|
40
|
+
...(options?.extraArgs ?? []),
|
|
30
41
|
];
|
|
31
|
-
|
|
42
|
+
const env = {};
|
|
43
|
+
if (options?.engramId)
|
|
44
|
+
env.RELIC_ENGRAM_ID = options.engramId;
|
|
45
|
+
await spawnShell(this.command, args, options?.cwd, Object.keys(env).length > 0 ? env : undefined);
|
|
32
46
|
}
|
|
33
47
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const CODEX_HOOK_SCRIPT_PATH: string;
|
|
2
|
+
/**
|
|
3
|
+
* Codex CLI の Stop フックをセットアップする。
|
|
4
|
+
* - ~/.relic/hooks/codex-stop.js を生成
|
|
5
|
+
* - ~/.codex/hooks.json に Stop フックを登録
|
|
6
|
+
* 既にセットアップ済みの場合はスキップ。
|
|
7
|
+
*/
|
|
8
|
+
export declare function setupCodexHook(): void;
|
|
9
|
+
/**
|
|
10
|
+
* Stop フックがセットアップ済みか確認する。
|
|
11
|
+
*/
|
|
12
|
+
export declare function isCodexHookSetup(): boolean;
|