@remnic/plugin-claude-code 1.0.0 → 9.3.515
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/.claude-plugin/plugin.json +1 -1
- package/README.md +90 -0
- package/hooks/bin/session-start.sh +117 -13
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remnic",
|
|
3
3
|
"description": "Universal memory for AI agents — automatic recall, observation, and cross-agent knowledge sharing",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.1",
|
|
5
5
|
"author": "Joshua Warren",
|
|
6
6
|
"homepage": "https://github.com/joshuaswarren/remnic",
|
|
7
7
|
"repository": "https://github.com/joshuaswarren/remnic"
|
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @remnic/plugin-claude-code
|
|
2
|
+
|
|
3
|
+
Native [Claude Code](https://docs.claude.com/en/docs/claude-code) plugin for [Remnic](https://github.com/joshuaswarren/remnic) memory. Wires Claude Code's session hooks, MCP server, skills, and the `memory-review` agent into a running Remnic daemon so every Claude Code session gets persistent long-term memory automatically.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Three discrete steps. None is automated for you end-to-end today; each writes to a different place.
|
|
8
|
+
|
|
9
|
+
1. **Mint a Remnic-side bearer token and record the connector.**
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
remnic connectors install claude-code
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This writes `~/.remnic/connectors/claude-code.json` (Remnic's connector-state file) and stores a bearer token in Remnic's token store. It does NOT touch any Claude Code configuration — the Claude Code memory-extension publisher in `@remnic/core` is a stub (`isHostAvailable()` → false, `publish()` → no-op), so no hook/skill/agent files are written and no Claude MCP config is edited.
|
|
16
|
+
|
|
17
|
+
2. **Add Remnic as an MCP server in your Claude Code config.** Paste the `.mcp.json` block from the "MCP setup" section below into Claude Code's MCP config (commonly `~/.mcp.json` or `~/.claude.json`, per your Claude install), replacing `{{REMNIC_TOKEN}}` with the token minted in step 1. Without this step Claude Code has no way to talk to the Remnic daemon.
|
|
18
|
+
|
|
19
|
+
3. **Install this package and load it through Claude Code's plugin system** so the hook scripts, skills, and `memory-review` agent are actually active:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g @remnic/plugin-claude-code
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Consult Claude Code's plugin docs for the exact load mechanism your install supports (plugin marketplace / symlink / etc.). Until this step runs, auto-recall and auto-observe don't fire even if step 2 is correct.
|
|
26
|
+
|
|
27
|
+
## What ships
|
|
28
|
+
|
|
29
|
+
The package is **data-only** (no JavaScript runtime) — it's a bundle of Claude Code configuration:
|
|
30
|
+
|
|
31
|
+
| File / dir | Purpose |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `.claude-plugin/plugin.json` | Plugin manifest |
|
|
34
|
+
| `hooks/hooks.json` | `SessionStart`, `PostToolUse` (Write/Edit/MultiEdit), and `UserPromptSubmit` hook wiring |
|
|
35
|
+
| `hooks/bin/*.sh` | Small shell scripts that call the local Remnic daemon |
|
|
36
|
+
| `skills/` | `remnic-recall`, `remnic-remember`, `remnic-search`, `remnic-status`, `remnic-entities`, `remnic-memory-workflow` |
|
|
37
|
+
| `agents/memory-review.md` | Subagent that audits + summarizes memory for long-running sessions |
|
|
38
|
+
| `.mcp.json` | MCP server config pointing Claude Code at `http://localhost:4318/mcp` |
|
|
39
|
+
| `settings.json` | Default Claude Code settings for the plugin |
|
|
40
|
+
|
|
41
|
+
## What you get at runtime
|
|
42
|
+
|
|
43
|
+
Once installed and a Remnic daemon is running (`remnic daemon start`):
|
|
44
|
+
|
|
45
|
+
- **Auto-recall** on `SessionStart` and on every `UserPromptSubmit` — relevant memories are injected into the session prompt before Claude Code's first turn and before each subsequent user turn.
|
|
46
|
+
- **Auto-observe** on `PostToolUse` for `Write`, `Edit`, and `MultiEdit` tools — new facts, decisions, and entities touched by file edits are buffered for extraction without the user lifting a finger.
|
|
47
|
+
- **Memory skills** — invoke `/remnic-recall`, `/remnic-search`, `/remnic-remember`, `/remnic-entities`, `/remnic-status` directly in Claude Code chats.
|
|
48
|
+
- **Cross-agent sharing** — the same memory store is shared with every other Remnic-connected agent (Codex, OpenClaw, Replit, Hermes, etc.), so what one agent learns is available to all.
|
|
49
|
+
|
|
50
|
+
## MCP setup
|
|
51
|
+
|
|
52
|
+
The plugin expects a Remnic daemon reachable at `http://localhost:4318/mcp` with a bearer token. `remnic connectors install claude-code` does NOT write this for you — the Claude Code publisher in `@remnic/core` is a stub, so no Claude MCP config is touched. You must paste the following `.mcp.json` block into Claude Code's MCP config by hand (step 2 of the Install flow above):
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"mcpServers": {
|
|
57
|
+
"remnic": {
|
|
58
|
+
"type": "http",
|
|
59
|
+
"url": "http://localhost:4318/mcp",
|
|
60
|
+
"headers": {
|
|
61
|
+
"Authorization": "Bearer {{REMNIC_TOKEN}}",
|
|
62
|
+
"X-Engram-Client-Id": "claude-code"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Replace `{{REMNIC_TOKEN}}` with a token minted via `remnic token generate <connector-id>`.
|
|
70
|
+
|
|
71
|
+
## Agent note
|
|
72
|
+
|
|
73
|
+
If you're an AI agent scaffolding a Claude Code integration: **do not** hand-edit hook scripts in a user's `~/.claude/` tree. The full setup has two components:
|
|
74
|
+
|
|
75
|
+
1. `remnic connectors install claude-code` mints the MCP token and writes Remnic-side connector config. This does NOT deploy hooks/skills/agents — Claude Code doesn't yet expose a file-based extension directory, so the corresponding publisher in `@remnic/core` is a stub.
|
|
76
|
+
2. Install this npm package and load it through Claude Code's plugin system so the hook/skill/agent tree is picked up. Until both steps run, auto-recall and auto-observe will not fire even though `remnic connectors doctor claude-code` reports green.
|
|
77
|
+
|
|
78
|
+
The plugin is intentionally data-only so Claude Code's plugin loader can manage upgrades atomically.
|
|
79
|
+
|
|
80
|
+
## Related
|
|
81
|
+
|
|
82
|
+
- [`@remnic/cli`](https://www.npmjs.com/package/@remnic/cli) — daemon lifecycle + installer
|
|
83
|
+
- [`@remnic/plugin-codex`](https://www.npmjs.com/package/@remnic/plugin-codex) — same idea, for OpenAI Codex CLI
|
|
84
|
+
- [`@remnic/plugin-openclaw`](https://www.npmjs.com/package/@remnic/plugin-openclaw) — OpenClaw memory-slot plugin
|
|
85
|
+
- Connector guide: [docs/integration/connector-setup.md](https://github.com/joshuaswarren/remnic/blob/main/docs/integration/connector-setup.md) in the repo
|
|
86
|
+
- Source + issues: <https://github.com/joshuaswarren/remnic>
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT. See the root [LICENSE](https://github.com/joshuaswarren/remnic/blob/main/LICENSE) file.
|
|
@@ -56,7 +56,87 @@ SESSION_ID="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write
|
|
|
56
56
|
CWD="$(node -e "const d=JSON.parse(process.argv[1]); process.stdout.write(d.cwd||'')" "$INPUT" 2>/dev/null || echo "")"
|
|
57
57
|
PROJECT_NAME="$(basename "$CWD" 2>/dev/null || echo "unknown")"
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
# Resolve git context for the session's cwd (issue #569 PR 5). Produces
|
|
60
|
+
# either a JSON object for the `codingContext` field, or an empty string
|
|
61
|
+
# when the cwd is not inside a git repo. All git calls are wrapped in &&
|
|
62
|
+
# so any failure silently drops back to no-context.
|
|
63
|
+
CODING_CONTEXT_JSON=""
|
|
64
|
+
if [ -n "$CWD" ] && [ -d "$CWD" ] && command -v git >/dev/null 2>&1; then
|
|
65
|
+
# `git` calls are short-timeout and local. Any failure → empty.
|
|
66
|
+
REMNIC_GIT_TOP="$(git -C "$CWD" rev-parse --show-toplevel 2>/dev/null || echo "")"
|
|
67
|
+
if [ -n "$REMNIC_GIT_TOP" ]; then
|
|
68
|
+
REMNIC_GIT_BRANCH="$(git -C "$REMNIC_GIT_TOP" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "HEAD")"
|
|
69
|
+
[ "$REMNIC_GIT_BRANCH" = "HEAD" ] && REMNIC_GIT_BRANCH=""
|
|
70
|
+
REMNIC_GIT_ORIGIN="$(git -C "$REMNIC_GIT_TOP" remote get-url origin 2>/dev/null || echo "")"
|
|
71
|
+
REMNIC_GIT_DEFAULT_BRANCH="$(git -C "$REMNIC_GIT_TOP" symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null | sed 's|^refs/remotes/origin/||' || echo "")"
|
|
72
|
+
CODING_CONTEXT_JSON="$(REMNIC_GIT_TOP="$REMNIC_GIT_TOP" REMNIC_GIT_BRANCH="$REMNIC_GIT_BRANCH" REMNIC_GIT_ORIGIN="$REMNIC_GIT_ORIGIN" REMNIC_GIT_DEFAULT_BRANCH="$REMNIC_GIT_DEFAULT_BRANCH" node -e "
|
|
73
|
+
// Mirror the pure logic from @remnic/core's resolveGitContext so the
|
|
74
|
+
// hook produces the same projectId without calling into the daemon
|
|
75
|
+
// first. FNV-1a 32-bit stable hash.
|
|
76
|
+
const rootPath = process.env.REMNIC_GIT_TOP || '';
|
|
77
|
+
const branch = process.env.REMNIC_GIT_BRANCH || null;
|
|
78
|
+
const origin = process.env.REMNIC_GIT_ORIGIN || '';
|
|
79
|
+
const defaultBranch = process.env.REMNIC_GIT_DEFAULT_BRANCH || null;
|
|
80
|
+
function stableHash(input) {
|
|
81
|
+
let hash = 0x811c9dc5;
|
|
82
|
+
for (let i = 0; i < input.length; i++) {
|
|
83
|
+
hash ^= input.charCodeAt(i);
|
|
84
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
85
|
+
}
|
|
86
|
+
return hash.toString(16).padStart(8, '0');
|
|
87
|
+
}
|
|
88
|
+
// Mirrors packages/remnic-core/src/coding/git-context.ts
|
|
89
|
+
// normalizeOriginUrl. Keep the two in sync so the hook-computed
|
|
90
|
+
// projectId matches what the daemon computes on the same origin.
|
|
91
|
+
function normalizeOriginUrl(raw) {
|
|
92
|
+
let u = (raw || '').trim();
|
|
93
|
+
if (!u) return '';
|
|
94
|
+
// Case-insensitive .git strip — matches the TS canonical form.
|
|
95
|
+
if (/\\.git\$/i.test(u)) u = u.slice(0, -4);
|
|
96
|
+
// Windows drive-letter: short-circuit scp parsing.
|
|
97
|
+
if (/^[A-Za-z]:[\\\\/]/.test(u)) return u.toLowerCase();
|
|
98
|
+
// Protocol form: handles ssh://, https://, file:///, bracketed
|
|
99
|
+
// IPv6 hosts, optional user, optional port, and empty host
|
|
100
|
+
// (file:///path).
|
|
101
|
+
const proto = /^[a-z][a-z0-9+.-]*:\\/\\/(?:[^@/]+@)?(\\[[^\\]]+\\]|[^/:]*)(?::(\\d+))?(\\/.*)?\$/i.exec(u);
|
|
102
|
+
if (proto) {
|
|
103
|
+
let host = proto[1] || '';
|
|
104
|
+
const wasBracketed = host.startsWith('[') && host.endsWith(']');
|
|
105
|
+
if (wasBracketed) host = host.slice(1, -1);
|
|
106
|
+
const port = proto[2];
|
|
107
|
+
const p = (proto[3] || '').replace(/^\\/+/, '');
|
|
108
|
+
const hostPort = port
|
|
109
|
+
? (wasBracketed ? '[' + host + ']:' + port : host + ':' + port)
|
|
110
|
+
: host;
|
|
111
|
+
const prefix = hostPort.length > 0 ? hostPort : 'localhost';
|
|
112
|
+
return (prefix + '/' + p).toLowerCase();
|
|
113
|
+
}
|
|
114
|
+
// scp form: [user@]host:path — user@ optional, bracketed IPv6 host
|
|
115
|
+
// supported. A matched path starting with // is a protocol-URL
|
|
116
|
+
// leftover and is rejected.
|
|
117
|
+
const scp = /^(?:([^@\\s\\/]+)@)?(\\[[^\\]]+\\]|[^:@\\s\\/]+):(.+)\$/.exec(u);
|
|
118
|
+
if (scp) {
|
|
119
|
+
let host = scp[2] || '';
|
|
120
|
+
if (host.startsWith('[') && host.endsWith(']')) host = host.slice(1, -1);
|
|
121
|
+
const p = scp[3] || '';
|
|
122
|
+
if (p.startsWith('//')) return u.toLowerCase();
|
|
123
|
+
return (host + '/' + p.replace(/^\\/+/, '')).toLowerCase();
|
|
124
|
+
}
|
|
125
|
+
return u.toLowerCase();
|
|
126
|
+
}
|
|
127
|
+
const normalized = normalizeOriginUrl(origin);
|
|
128
|
+
const projectId = normalized ? 'origin:' + stableHash(normalized) : 'root:' + stableHash(rootPath);
|
|
129
|
+
process.stdout.write(JSON.stringify({
|
|
130
|
+
projectId,
|
|
131
|
+
branch: branch || null,
|
|
132
|
+
rootPath,
|
|
133
|
+
defaultBranch: defaultBranch || null,
|
|
134
|
+
}));
|
|
135
|
+
" 2>/dev/null || echo "")"
|
|
136
|
+
fi
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
log "session=$SESSION_ID project=$PROJECT_NAME coding-context=${CODING_CONTEXT_JSON:+yes}"
|
|
60
140
|
|
|
61
141
|
# Health check — start daemon if not running
|
|
62
142
|
if ! curl -sf --max-time 2 "$REMNIC_HEALTH_URL" >/dev/null 2>&1; then
|
|
@@ -82,12 +162,29 @@ fi
|
|
|
82
162
|
|
|
83
163
|
QUERY="Starting a new coding session in project: ${PROJECT_NAME}. Recall relevant memories, preferences, decisions, patterns, and context about this project and the user."
|
|
84
164
|
|
|
85
|
-
REQUEST_BODY="$(node -e "
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
165
|
+
REQUEST_BODY="$(REMNIC_CODING_CONTEXT_JSON="$CODING_CONTEXT_JSON" node -e "
|
|
166
|
+
const body = {
|
|
167
|
+
query: process.argv[1],
|
|
168
|
+
sessionKey: process.argv[2],
|
|
169
|
+
topK: 12,
|
|
170
|
+
mode: 'auto',
|
|
171
|
+
};
|
|
172
|
+
const raw = process.env.REMNIC_CODING_CONTEXT_JSON || '';
|
|
173
|
+
if (raw) {
|
|
174
|
+
try { body.codingContext = JSON.parse(raw); } catch (_) {
|
|
175
|
+
// Context envelope was provided but failed to parse. Explicitly
|
|
176
|
+
// clear any previously-attached context for this session so a
|
|
177
|
+
// malformed envelope does not silently keep stale state.
|
|
178
|
+
body.codingContext = null;
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
// No git context resolvable for this cwd. Explicitly clear any
|
|
182
|
+
// previously-attached context so a session that moves out of a repo
|
|
183
|
+
// does not keep routing to the old project namespace.
|
|
184
|
+
body.codingContext = null;
|
|
185
|
+
}
|
|
186
|
+
process.stdout.write(JSON.stringify(body));
|
|
187
|
+
" "$QUERY" "$SESSION_ID" 2>/dev/null)"
|
|
91
188
|
|
|
92
189
|
[ -z "$REQUEST_BODY" ] && echo '{"continue":true}' && exit 0
|
|
93
190
|
|
|
@@ -104,12 +201,19 @@ RESPONSE="$(echo "$RAW" | sed '$d')"
|
|
|
104
201
|
|
|
105
202
|
if [ $CURL_EXIT -ne 0 ] || ! [[ "$HTTP_STATUS" =~ ^2 ]] || [ -z "$RESPONSE" ]; then
|
|
106
203
|
log "full recall failed (curl=$CURL_EXIT http=$HTTP_STATUS) — falling back to minimal"
|
|
107
|
-
MINIMAL_BODY="$(node -e "
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
204
|
+
MINIMAL_BODY="$(REMNIC_CODING_CONTEXT_JSON="$CODING_CONTEXT_JSON" node -e "
|
|
205
|
+
const body = {
|
|
206
|
+
query: process.argv[1],
|
|
207
|
+
sessionKey: process.argv[2],
|
|
208
|
+
topK: 8,
|
|
209
|
+
mode: 'minimal',
|
|
210
|
+
};
|
|
211
|
+
const raw = process.env.REMNIC_CODING_CONTEXT_JSON || '';
|
|
212
|
+
if (raw) {
|
|
213
|
+
try { body.codingContext = JSON.parse(raw); } catch (_) { /* ignore */ }
|
|
214
|
+
}
|
|
215
|
+
process.stdout.write(JSON.stringify(body));
|
|
216
|
+
" "$QUERY" "$SESSION_ID" 2>/dev/null)"
|
|
113
217
|
RAW="$(curl -s -w "\n%{http_code}" --max-time 20 \
|
|
114
218
|
-X POST "$REMNIC_URL" \
|
|
115
219
|
-H "Authorization: Bearer ${REMNIC_TOKEN}" \
|