@anmol-srv/sigil 0.10.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.
Files changed (49) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +417 -0
  3. package/dist/cli.js +1019 -0
  4. package/dist/hooks/post-tool-use.js +70 -0
  5. package/dist/hooks/session-end.js +222 -0
  6. package/dist/hooks/stop.js +259 -0
  7. package/dist/hooks/user-prompt-submit.js +279 -0
  8. package/dist/server.js +573 -0
  9. package/integrations/hermes/README.md +41 -0
  10. package/integrations/hermes/plugin/README.md +72 -0
  11. package/integrations/hermes/plugin/__init__.py +353 -0
  12. package/integrations/hermes/plugin/plugin.yaml +10 -0
  13. package/knexfile.js +15 -0
  14. package/package.json +100 -0
  15. package/prompts/audm-decision.md +31 -0
  16. package/prompts/chunk-context.md +23 -0
  17. package/prompts/default-extraction.md +35 -0
  18. package/prompts/entity-extraction.md +37 -0
  19. package/prompts/input-classifier.md +23 -0
  20. package/prompts/query-router.md +18 -0
  21. package/src/db/migrations/20260310120000_create-cortex-document-table.cjs +21 -0
  22. package/src/db/migrations/20260310120001_create-cortex-chunk-table.cjs +37 -0
  23. package/src/db/migrations/20260310120002_create-cortex-fact-table.cjs +37 -0
  24. package/src/db/migrations/20260310120003_create-cortex-entity-table.cjs +26 -0
  25. package/src/db/migrations/20260310120004_create-cortex-relation-table.cjs +27 -0
  26. package/src/db/migrations/20260310120005_create-cortex-history-table.cjs +16 -0
  27. package/src/db/migrations/20260311120000_add-entity-namespace-and-relation-indexes.cjs +32 -0
  28. package/src/db/migrations/20260312120000_add-fact-entity-linking.cjs +22 -0
  29. package/src/db/migrations/20260313093130_create-api-key-table.cjs +15 -0
  30. package/src/db/migrations/20260313120000_add-entity-dedup-support.cjs +13 -0
  31. package/src/db/migrations/20260313150000_create-connector-tables.cjs +46 -0
  32. package/src/db/migrations/20260318120000_add-contextual-chunk-prefix.cjs +11 -0
  33. package/src/db/migrations/20260318120001_add-fact-temporal-validity.cjs +15 -0
  34. package/src/db/migrations/20260318120002_add-fact-importance.cjs +11 -0
  35. package/src/db/migrations/20260318120003_add-fact-access-tracking.cjs +13 -0
  36. package/src/db/migrations/20260405120000_add-unique-constraints.cjs +58 -0
  37. package/src/db/migrations/20260405140000_create-llm-log-table.cjs +21 -0
  38. package/src/db/migrations/20260424120000_split-fact-lifecycle.cjs +86 -0
  39. package/src/db/migrations/20260424120002_create-embedding-cache.cjs +26 -0
  40. package/src/db/migrations/20260429120000_halfvec-index-compression.cjs +34 -0
  41. package/src/db/migrations/20260429120100_create-hebbian-edge-table.cjs +37 -0
  42. package/src/db/migrations/20260429120200_upgrade-embedding-dim-1024.cjs +68 -0
  43. package/src/db/migrations/20260504120000_scope-document-source-path-uniqueness.cjs +45 -0
  44. package/src/db/migrations/20260508001733_add-entity-aliases.cjs +42 -0
  45. package/src/db/migrations/20260512120000_create-entity-hebbian-edge.cjs +42 -0
  46. package/src/db/migrations/20260512120000_create-pod-tables.cjs +71 -0
  47. package/src/db/migrations/20260512120100_create-pod-membership.cjs +50 -0
  48. package/src/db/migrations/20260512120200_add-document-source-metadata.cjs +32 -0
  49. package/src/db/migrations/20260514023428_rewrite-session-pods-and-add-fact-attribution-columns.cjs +86 -0
@@ -0,0 +1,41 @@
1
+ # Hermes integration
2
+
3
+ Sigil ships as a [Hermes Agent](https://hermes.chat) memory provider plugin. The plugin source lives at [`plugin/`](./plugin) — copy that directory into Hermes' plugin tree on whichever machine runs Hermes.
4
+
5
+ ## Quick deploy (manual)
6
+
7
+ ```bash
8
+ # 1. From the Sigil repo root on your laptop:
9
+ scp -r integrations/hermes/plugin/ \
10
+ claude@neutron:.hermes/hermes-agent/plugins/memory/sigil/
11
+
12
+ # 2. On the server:
13
+ ssh claude@neutron
14
+ sigil --help # confirm sigil CLI is on PATH
15
+ sigil init # configure DB + embedder + LLM (once)
16
+ hermes config set memory.provider sigil
17
+ ```
18
+
19
+ Restart Hermes. Verify with `hermes memory status` (or whatever Hermes' status command surfaces).
20
+
21
+ ## What the plugin does
22
+
23
+ | Hermes hook | Sigil call | Why |
24
+ |---|---|---|
25
+ | `is_available()` | `which sigil` | Avoid network calls; just check the binary exists. |
26
+ | `initialize(session_id, platform, ...)` | sets namespace = `hermes-<platform>` | Per-platform classification — see plugin/README.md. |
27
+ | `prefetch(query)` | `sigil search <q> --namespace=hermes-<platform>,default --limit=5 --no-graph` | Fast cross-namespace recall = the shared brain. |
28
+ | `sync_turn(user, assistant)` | `sigil remember --bg "<user>"` in a daemon thread | Non-blocking. Sigil's classifier decides what's worth keeping. |
29
+ | `get_tool_schemas()` | `sigil_search`, `sigil_remember` | Lets the model explicitly drill down or save mid-turn. |
30
+
31
+ The contract Hermes expects is documented at `~/.hermes/hermes-agent/website/docs/developer-guide/memory-provider-plugin.md` on any Hermes install.
32
+
33
+ ## Future: one-shot install via `sigil init`
34
+
35
+ A `src/lib/clients/hermes.js` module (5th client alongside Claude Code / Cursor / Codex / Kiro) would let `sigil init` copy this plugin into `~/.hermes/hermes-agent/plugins/memory/sigil/` and flip `memory.provider: sigil` in `config.yaml` automatically. That lands when we're confident the manual deploy works end-to-end.
36
+
37
+ ## Caveats
38
+
39
+ - **Sigil CLI must be on `PATH`** on whichever machine runs Hermes. If `which sigil` returns nothing, `is_available()` returns false and Hermes silently falls back to its built-in memory.
40
+ - **`~/.sigil/.env` must be configured** — run `sigil init` on the Hermes host before activating the plugin.
41
+ - **The plugin shells out for every prefetch.** Latency is `sigil search` latency. The plugin keeps this path retrieval-only; if Hermes' per-turn budget is tighter, we could move to in-process via a Python<>Node bridge — out of scope for v0.1.
@@ -0,0 +1,72 @@
1
+ # Sigil Memory Provider
2
+
3
+ Persistent memory for Hermes Agent, backed by [Sigil](https://github.com/anmolsrv/sigil) — a local-first knowledge engine with atomic facts, entity graph, and hybrid retrieval. Same memory store used by Claude Code, Cursor, Codex CLI, and Kiro.
4
+
5
+ ## Why this exists
6
+
7
+ You're running Hermes on a server (e.g. via iMessage / Telegram / Discord gateway) and you also use Claude Code / Cursor / etc. on your laptop. You want **one brain** that all of them share — without copying memories around or rebuilding them per tool.
8
+
9
+ This plugin makes that real: every Hermes turn lands in a Sigil namespace, every laptop turn lands in `default`, and cross-namespace search means anyone can recall anything.
10
+
11
+ ## Requirements
12
+
13
+ - Sigil CLI on `PATH` — `npm install -g @anmol-srv/sigil`
14
+ - `sigil init` completed once (configures DB, embedder, LLM provider)
15
+ - Postgres reachable from this machine (local install or shared via Tailscale / cloud)
16
+
17
+ ## Setup
18
+
19
+ ```bash
20
+ hermes config set memory.provider sigil
21
+ ```
22
+
23
+ No additional env vars or config files — Sigil reads its own `~/.sigil/.env`.
24
+
25
+ ## How it classifies sources
26
+
27
+ Each Hermes platform writes to its own Sigil namespace:
28
+
29
+ | Hermes platform | Sigil namespace |
30
+ |---|---|
31
+ | `cli` | `hermes-cli` |
32
+ | `imessage` | `hermes-imessage` |
33
+ | `telegram` | `hermes-telegram` |
34
+ | `discord` | `hermes-discord` |
35
+ | `cron` | `hermes-cron` |
36
+
37
+ Recall reads across **two namespaces**: the active platform's own (`hermes-imessage`) AND `default` (where the user's laptop tools write). Result: a fact captured in iMessage is reachable when you're back at your laptop in Claude Code, and vice versa.
38
+
39
+ To see what's in each namespace:
40
+
41
+ ```bash
42
+ sigil facts --namespace=hermes-imessage
43
+ sigil facts --namespace=default
44
+ sigil namespace list
45
+ ```
46
+
47
+ ## Tools exposed to the model
48
+
49
+ | Tool | Purpose |
50
+ |---|---|
51
+ | `sigil_search` | Drill-down search across this platform + `default`. The model is told to use this only when the auto-injected context didn't surface what it needed. |
52
+ | `sigil_remember` | Explicit save. The model is told to use this only when the user asks ("remember that...") or a critical fact arrives mid-turn. |
53
+
54
+ Routine fact capture happens automatically via `sync_turn` — no model action required.
55
+
56
+ ## What lives where
57
+
58
+ | Layer | Where | Owns |
59
+ |---|---|---|
60
+ | This plugin | `~/.hermes/hermes-agent/plugins/memory/sigil/` | The Hermes ABC contract — initialize, prefetch, sync_turn, tool dispatch. Thin subprocess wrapper. |
61
+ | Sigil CLI | `which sigil` | Hybrid search, fact extraction, AUDM dedup, pod-aware retrieval, embedder calls. |
62
+ | Sigil config | `~/.sigil/.env` | DB connection, embedder choice, LLM provider. Run `sigil init` to reconfigure. |
63
+ | Sigil data | Postgres (`SIGIL_DB_HOST` in `~/.sigil/.env`) | All facts, entities, pods, relations. Shared across machines when they point at the same Postgres. |
64
+
65
+ ## Shared brain across machines
66
+
67
+ Point `SIGIL_DB_HOST` in every machine's `~/.sigil/.env` at the *same* Postgres. Two common topologies:
68
+
69
+ 1. **Server-hosted Postgres** — Postgres on this server; laptop connects over Tailscale.
70
+ 2. **Cloud Postgres** — Supabase / Neon / RDS; both machines connect to it.
71
+
72
+ Either way: one DB, many writers, every namespace visible from everywhere.
@@ -0,0 +1,353 @@
1
+ """Sigil memory provider for Hermes Agent.
2
+
3
+ Bridges Hermes' memory system to a local Sigil install via the `sigil` CLI.
4
+ No new network surface — the plugin shells out to the same subprocess
5
+ commands Claude Code uses through its hooks. This means Hermes inherits
6
+ all of Sigil's behavior for free: AUDM dedup, Hebbian retrieval, hot-context
7
+ budgets, pod-aware blending, the lot.
8
+
9
+ Architecture
10
+ ------------
11
+ prefetch(query) → `sigil search <q> --namespace=<ns>,default`
12
+ sync_turn(u, a) → `sigil remember --bg "<user_content>"` (daemon thread)
13
+ is_available() → shell test: `sigil --help` returns 0
14
+ handle_tool_call() → explicit search / remember invocations from the model
15
+
16
+ Shared brain via namespaces
17
+ ---------------------------
18
+ Each Hermes platform writes to its own Sigil namespace:
19
+
20
+ cli → hermes-cli
21
+ telegram → hermes-telegram
22
+ imessage → hermes-imessage
23
+ discord → hermes-discord
24
+ cron → hermes-cron
25
+
26
+ Search reads across the platform's own namespace AND `default` — the
27
+ namespace Claude Code's hooks write to from the user's laptop. Result:
28
+ facts captured anywhere are reachable from anywhere, with natural source
29
+ classification (a Hermes-iMessage fact lives in `hermes-imessage`, a
30
+ laptop-Claude-Code fact lives in `default`, but both surface in any
31
+ search).
32
+
33
+ Requires
34
+ --------
35
+ sigil CLI on PATH (the local install — `npm install -g @anmolsrv/sigil`
36
+ or wherever the binary is installed)
37
+ ~/.sigil/.env configured (run `sigil init` once before activating
38
+ this plugin)
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import json
44
+ import logging
45
+ import os
46
+ import shutil
47
+ import subprocess
48
+ import threading
49
+ from typing import Any, Dict, List, Optional
50
+
51
+ from agent.memory_provider import MemoryProvider
52
+
53
+ logger = logging.getLogger(__name__)
54
+
55
+ # Subprocess timeouts. Search is on the prompt-critical path → tight budget.
56
+ # Remember is fire-and-forget via --bg, but we still cap the spawn-and-detach.
57
+ _SEARCH_TIMEOUT_S = 5
58
+ _REMEMBER_TIMEOUT_S = 10
59
+ _PREFETCH_LIMIT = 5
60
+
61
+ # Cap the prefetched context block — Hermes already has a memory_char_limit
62
+ # in config.yaml, but we trim early to avoid wasting characters on results
63
+ # the agent will never use.
64
+ _PREFETCH_CHAR_LIMIT = 2000
65
+
66
+
67
+ def _clean_text(value: Any) -> str:
68
+ """Strip subprocess noise that can break Hermes' tool/result framing."""
69
+ if value is None:
70
+ return ""
71
+ return str(value).replace("\x00", "").strip()
72
+
73
+
74
+ def _ok(payload: Dict[str, Any]) -> str:
75
+ return json.dumps(payload, ensure_ascii=False)
76
+
77
+
78
+ def _err(message: str) -> str:
79
+ return json.dumps({"error": message}, ensure_ascii=False)
80
+
81
+
82
+ def _sigil_search_args(query: str, namespaces: str, limit: int) -> List[str]:
83
+ return [
84
+ "sigil", "search", query,
85
+ f"--namespace={namespaces}",
86
+ f"--limit={limit}",
87
+ "--no-graph",
88
+ "--no-route",
89
+ "--no-synthesize",
90
+ ]
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Provider
95
+ # ---------------------------------------------------------------------------
96
+
97
+ class SigilProvider(MemoryProvider):
98
+ """Hermes memory provider backed by a local Sigil install."""
99
+
100
+ def __init__(self) -> None:
101
+ self._session_id: str = ""
102
+ self._platform: str = "cli"
103
+ self._namespace: str = "hermes-cli"
104
+ self._search_namespaces: str = "hermes-cli,default"
105
+ self._hermes_home: str = ""
106
+ self._sync_thread: Optional[threading.Thread] = None
107
+
108
+ @property
109
+ def name(self) -> str:
110
+ return "sigil"
111
+
112
+ # -- Lifecycle -----------------------------------------------------------
113
+
114
+ def is_available(self) -> bool:
115
+ """Check the sigil CLI is on PATH. No network calls."""
116
+ return shutil.which("sigil") is not None
117
+
118
+ def initialize(self, session_id: str, **kwargs: Any) -> None:
119
+ self._session_id = session_id
120
+ self._platform = kwargs.get("platform", "cli")
121
+ self._namespace = f"hermes-{self._platform}"
122
+ # Cross-namespace search: this platform's facts PLUS the default
123
+ # namespace where Claude Code writes from the user's other machines.
124
+ self._search_namespaces = f"{self._namespace},default"
125
+ self._hermes_home = kwargs.get("hermes_home", "")
126
+ logger.info(
127
+ "Sigil provider initialised: namespace=%s session=%s platform=%s",
128
+ self._namespace, session_id, self._platform,
129
+ )
130
+
131
+ def shutdown(self) -> None:
132
+ if self._sync_thread and self._sync_thread.is_alive():
133
+ self._sync_thread.join(timeout=5.0)
134
+
135
+ # -- Recall (per-turn) ---------------------------------------------------
136
+
137
+ def system_prompt_block(self) -> str:
138
+ return (
139
+ "## Memory (Sigil)\n"
140
+ "Persistent memory across all your sessions and the user's other AI tools "
141
+ "(Claude Code, Cursor, Codex CLI, Kiro). Recent relevant facts are "
142
+ f"auto-injected at the top of each turn from namespaces `{self._search_namespaces}`. "
143
+ "Trust the injection — answer from it first.\n\n"
144
+ "Call `sigil_search` ONLY for drill-down questions when the injection "
145
+ "clearly missed something specific. Call `sigil_remember` ONLY when the "
146
+ "user explicitly asks (\"remember that...\", \"save this...\") or when "
147
+ "they share a critical fact mid-turn that the Stop-equivalent flush will "
148
+ "miss."
149
+ )
150
+
151
+ def prefetch(self, query: str, *, session_id: str = "") -> str:
152
+ """Synchronous recall before the next API call.
153
+
154
+ Calls `sigil search` against this platform's namespace plus `default`
155
+ (the cross-machine shared brain). Returns the raw CLI output as
156
+ context text; Sigil's hybrid search already formats one fact per line
157
+ which is exactly what the system prompt wants.
158
+ """
159
+ if not query or not query.strip():
160
+ return ""
161
+
162
+ try:
163
+ result = subprocess.run(
164
+ _sigil_search_args(query, self._search_namespaces, _PREFETCH_LIMIT),
165
+ timeout=_SEARCH_TIMEOUT_S,
166
+ capture_output=True,
167
+ text=True,
168
+ check=False,
169
+ )
170
+ except subprocess.TimeoutExpired:
171
+ logger.warning("sigil search timed out after %ss", _SEARCH_TIMEOUT_S)
172
+ return ""
173
+ except Exception as exc: # noqa: BLE001 — never break the agent's turn
174
+ logger.warning("sigil search failed: %s", exc)
175
+ return ""
176
+
177
+ if result.returncode != 0:
178
+ logger.warning("sigil search exit %s: %s", result.returncode, _clean_text(result.stderr))
179
+ return ""
180
+
181
+ out = _clean_text(result.stdout)
182
+ if not out or out == "No results found.":
183
+ return ""
184
+
185
+ # Trim early — Hermes also enforces memory_char_limit but truncating
186
+ # here avoids feeding the model results it can't use.
187
+ return out[:_PREFETCH_CHAR_LIMIT]
188
+
189
+ # -- Write (per-turn) ----------------------------------------------------
190
+
191
+ def sync_turn(self, user_content: str, assistant_content: str, *,
192
+ session_id: str = "") -> None:
193
+ """Persist memorable content from the just-completed turn.
194
+
195
+ Sigil's `remember` command runs its own classifier + AUDM dedup, so
196
+ we don't try to be clever about what's "memorable" — just hand
197
+ the user message over and let Sigil decide.
198
+
199
+ Background thread is belt-and-braces: `sigil remember --bg` already
200
+ spawns a detached subprocess, but wrapping it in a daemon thread
201
+ means the .run() call itself can't block sync_turn.
202
+ """
203
+ text = (user_content or "").strip()
204
+ if not text:
205
+ return
206
+
207
+ # Sigil's CLI takes facts as positional args. We send the raw user
208
+ # message — its ingestion pipeline classifies, extracts, dedupes.
209
+ # Trimming to a sensible upper bound avoids enormous argv on long
210
+ # pasted content.
211
+ snippet = text[:4000]
212
+
213
+ def _save() -> None:
214
+ try:
215
+ subprocess.run(
216
+ ["sigil", "remember", "--bg", snippet],
217
+ env={**os.environ, "DEFAULT_NAMESPACE": self._namespace},
218
+ timeout=_REMEMBER_TIMEOUT_S,
219
+ capture_output=True,
220
+ )
221
+ except Exception as exc: # noqa: BLE001
222
+ logger.warning("sigil remember failed: %s", exc)
223
+
224
+ # If the previous turn's sync is still running, let it finish first
225
+ # so we don't pile up zombie threads on chatty sessions.
226
+ if self._sync_thread and self._sync_thread.is_alive():
227
+ self._sync_thread.join(timeout=5.0)
228
+ self._sync_thread = threading.Thread(target=_save, daemon=True)
229
+ self._sync_thread.start()
230
+
231
+ # -- Tools (explicit invocation by the model) ----------------------------
232
+
233
+ def get_tool_schemas(self) -> List[Dict[str, Any]]:
234
+ return [
235
+ {
236
+ "name": "sigil_search",
237
+ "description": (
238
+ "Search persistent memory across all of the user's AI sessions "
239
+ "(this Hermes platform + their laptop's Claude Code / Cursor / "
240
+ "Codex / Kiro). Use for drill-down questions when the "
241
+ "auto-injected context block didn't surface what you need."
242
+ ),
243
+ "parameters": {
244
+ "type": "object",
245
+ "properties": {
246
+ "query": {
247
+ "type": "string",
248
+ "description": "Natural-language search query."
249
+ },
250
+ "limit": {
251
+ "type": "integer",
252
+ "description": "Max results (default 5).",
253
+ "default": _PREFETCH_LIMIT,
254
+ },
255
+ },
256
+ "required": ["query"],
257
+ },
258
+ },
259
+ {
260
+ "name": "sigil_remember",
261
+ "description": (
262
+ "Save a single self-contained fact to persistent memory. Use "
263
+ "ONLY when the user explicitly asks to remember something, or "
264
+ "when they share a critical mid-turn fact. Routine facts are "
265
+ "captured automatically — don't double-save."
266
+ ),
267
+ "parameters": {
268
+ "type": "object",
269
+ "properties": {
270
+ "fact": {
271
+ "type": "string",
272
+ "description": (
273
+ "A short, self-contained statement that makes sense "
274
+ "out of context. Not a conversation summary."
275
+ )
276
+ },
277
+ },
278
+ "required": ["fact"],
279
+ },
280
+ },
281
+ ]
282
+
283
+ def handle_tool_call(self, tool_name: str, args: Dict[str, Any]) -> Any:
284
+ if tool_name == "sigil_search":
285
+ return self._tool_search(args)
286
+ if tool_name == "sigil_remember":
287
+ return self._tool_remember(args)
288
+ return _err(f"unknown tool: {tool_name}")
289
+
290
+ def _tool_search(self, args: Dict[str, Any]) -> str:
291
+ query = (args.get("query") or "").strip()
292
+ if not query:
293
+ return _err("query is required")
294
+ limit = int(args.get("limit", _PREFETCH_LIMIT))
295
+
296
+ try:
297
+ result = subprocess.run(
298
+ _sigil_search_args(query, self._search_namespaces, limit),
299
+ timeout=_SEARCH_TIMEOUT_S,
300
+ capture_output=True,
301
+ text=True,
302
+ check=False,
303
+ )
304
+ except Exception as exc: # noqa: BLE001
305
+ return _err(_clean_text(f"sigil search failed: {exc}"))
306
+
307
+ if result.returncode != 0:
308
+ return _err(_clean_text(result.stderr or "search exited non-zero"))
309
+ return _ok({"results": _clean_text(result.stdout)})
310
+
311
+ def _tool_remember(self, args: Dict[str, Any]) -> str:
312
+ fact = (args.get("fact") or "").strip()
313
+ if not fact:
314
+ return _err("fact is required")
315
+
316
+ try:
317
+ result = subprocess.run(
318
+ ["sigil", "remember", "--bg", fact],
319
+ env={**os.environ, "DEFAULT_NAMESPACE": self._namespace},
320
+ timeout=_REMEMBER_TIMEOUT_S,
321
+ capture_output=True,
322
+ text=True,
323
+ check=False,
324
+ )
325
+ except Exception as exc: # noqa: BLE001
326
+ return _err(_clean_text(f"sigil remember failed: {exc}"))
327
+
328
+ if result.returncode != 0:
329
+ return _err(_clean_text(result.stderr or "remember exited non-zero"))
330
+ return _ok({"ok": True, "namespace": self._namespace})
331
+
332
+ # -- Config --------------------------------------------------------------
333
+ #
334
+ # Sigil reads its own ~/.sigil/.env (DB connection, embedder, LLM provider).
335
+ # Hermes doesn't need to know any of that — we return an empty schema so
336
+ # `hermes memory setup` doesn't ask redundant questions.
337
+
338
+ def get_config_schema(self) -> List[Dict[str, Any]]:
339
+ return []
340
+
341
+ def save_config(self, values: Dict[str, Any], hermes_home: str) -> None:
342
+ # No-op — Sigil owns its own config at ~/.sigil/.env. Run `sigil init`
343
+ # to (re)configure it.
344
+ return None
345
+
346
+
347
+ # ---------------------------------------------------------------------------
348
+ # Plugin entry point
349
+ # ---------------------------------------------------------------------------
350
+
351
+ def register(ctx: Any) -> None:
352
+ """Called by Hermes' memory plugin discovery system."""
353
+ ctx.register_memory_provider(SigilProvider())
@@ -0,0 +1,10 @@
1
+ name: sigil
2
+ version: 0.1.0
3
+ description: >
4
+ Persistent shared-brain memory via a local Sigil install. Hermes turns are
5
+ saved to a per-platform namespace; recall reads across that namespace plus
6
+ `default` (where the user's laptop tools — Claude Code, Cursor, Codex, Kiro —
7
+ write). One Postgres backs every machine; sources/pods classify the source.
8
+ hooks:
9
+ - prefetch
10
+ - sync_turn
package/knexfile.js ADDED
@@ -0,0 +1,15 @@
1
+ import 'dotenv/config';
2
+
3
+ const env = (key, fallback) => process.env[key] ?? fallback;
4
+
5
+ export default {
6
+ client: 'pg',
7
+ connection: {
8
+ host: env('SIGIL_DB_HOST', 'localhost'),
9
+ port: Number(env('SIGIL_DB_PORT', 5432)),
10
+ database: env('SIGIL_DB_NAME', 'sigil'),
11
+ user: env('SIGIL_DB_USER', 'sigil_app'),
12
+ password: env('SIGIL_DB_PASSWORD', ''),
13
+ },
14
+ migrations: { directory: './src/db/migrations' },
15
+ };
package/package.json ADDED
@@ -0,0 +1,100 @@
1
+ {
2
+ "name": "@anmol-srv/sigil",
3
+ "version": "0.10.3",
4
+ "type": "module",
5
+ "description": "Local-first memory infrastructure for AI coding agents. One brain shared across Claude Code, Codex CLI, Cursor, Kiro, Continue, Cline, Windsurf — any MCP client. Organized in pluggable pods, stored in your own Postgres. No cloud, no telemetry. Auto-captured from Claude Code via hooks; surfaced everywhere else as a 9-tool MCP server.",
6
+ "bin": {
7
+ "sigil": "dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "node --watch src/server.js",
11
+ "start": "node src/server.js",
12
+ "test": "vitest",
13
+ "lint": "eslint src/",
14
+ "lint:fix": "eslint src/ --fix",
15
+ "build": "node build.js",
16
+ "prepublishOnly": "npm run build",
17
+ "migrate": "knex migrate:latest",
18
+ "migrate:rollback": "knex migrate:rollback",
19
+ "migrate:make": "knex migrate:make"
20
+ },
21
+ "engines": {
22
+ "node": ">=20.0.0"
23
+ },
24
+ "files": [
25
+ "dist/",
26
+ "prompts/",
27
+ "src/db/migrations/",
28
+ "integrations/",
29
+ "knexfile.js",
30
+ "LICENSE",
31
+ "README.md"
32
+ ],
33
+ "keywords": [
34
+ "sigil",
35
+ "memory",
36
+ "ai-memory",
37
+ "persistent-memory",
38
+ "agent-memory",
39
+ "long-term-memory",
40
+ "shared-memory",
41
+ "ai-agent",
42
+ "coding-agent",
43
+ "ai-infrastructure",
44
+ "mcp",
45
+ "mcp-server",
46
+ "model-context-protocol",
47
+ "claude",
48
+ "claude-code",
49
+ "codex",
50
+ "codex-cli",
51
+ "cursor",
52
+ "kiro",
53
+ "windsurf",
54
+ "continue-dev",
55
+ "cline",
56
+ "chatgpt",
57
+ "ollama",
58
+ "rag",
59
+ "knowledge-graph",
60
+ "knowledge-base",
61
+ "vector-search",
62
+ "hybrid-search",
63
+ "llm",
64
+ "context",
65
+ "pgvector",
66
+ "postgres",
67
+ "local-first"
68
+ ],
69
+ "author": "Anmol Srivastava",
70
+ "license": "ISC",
71
+ "repository": {
72
+ "type": "git",
73
+ "url": "https://github.com/Anmol-Srv/sigil.git"
74
+ },
75
+ "homepage": "https://github.com/Anmol-Srv/sigil#readme",
76
+ "bugs": {
77
+ "url": "https://github.com/Anmol-Srv/sigil/issues"
78
+ },
79
+ "dependencies": {
80
+ "@iarna/toml": "^2.2.5",
81
+ "@modelcontextprotocol/sdk": "^1.27.1",
82
+ "dotenv": "^17.3.1",
83
+ "knex": "^3.1.0",
84
+ "pg": "^8.20.0"
85
+ },
86
+ "optionalDependencies": {
87
+ "@anthropic-ai/sdk": "^0.30.0"
88
+ },
89
+ "devDependencies": {
90
+ "@clack/prompts": "^1.2.0",
91
+ "@electric-sql/pglite": "^0.4.4",
92
+ "dayjs": "^1.11.19",
93
+ "esbuild": "^0.28.0",
94
+ "eslint": "^10.0.3",
95
+ "lodash-es": "^4.17.23",
96
+ "nanoid": "^5.1.7",
97
+ "vitest": "^4.0.18",
98
+ "zod": "^3.24.0"
99
+ }
100
+ }
@@ -0,0 +1,31 @@
1
+ You are comparing two facts from an organizational knowledge base. Decide if the NEW fact should be added as a separate entry or if it updates/replaces the EXISTING fact.
2
+
3
+ ## Decisions
4
+
5
+ - **UPDATE** — The new fact covers the same topic/event/observation as the existing fact, even if worded differently. They describe the same underlying thing. Replace the existing fact with the new version. **Err on the side of UPDATE** — a knowledge base with fewer, better facts is more useful than one with many overlapping facts.
6
+ - **ADD** — The new fact describes a genuinely different piece of information. Different event, different metric, different insight. Not just a rephrase.
7
+ - **CONTRADICT** — The new fact directly contradicts the existing fact (e.g., different numbers for the same metric, opposite conclusions).
8
+
9
+ ## Examples
10
+
11
+ EXISTING: "Database Design session covered normalization from 1NF through 3NF"
12
+ NEW: "Database Design Fundamentals covered database normalization (1NF through 3NF), live schema design, and denormalization"
13
+ → **UPDATE** (same core topic, new version adds more detail)
14
+
15
+ EXISTING: "Rahul Sharma recommended starting with PostgreSQL"
16
+ NEW: "In Q&A, a student asked about Redis vs PostgreSQL. Rahul recommended starting with PostgreSQL, moving to Redis for server-side revocation."
17
+ → **UPDATE** (same recommendation, new version adds the question context)
18
+
19
+ EXISTING: "Session had 78% attendance with 32 of 41 enrolled"
20
+ NEW: "Session had 8 attendees with an average rating of 4.4/5"
21
+ → **ADD** (different metrics — one is attendance %, other is count + rating)
22
+
23
+ EXISTING: "Students said 3NF was covered too quickly"
24
+ NEW: "Students requested more practice exercises for 3NF"
25
+ → **UPDATE** (same underlying feedback about 3NF pacing)
26
+
27
+ EXISTING: "Session started 2 minutes late"
28
+ NEW: "Session started on time"
29
+ → **CONTRADICT**
30
+
31
+ Respond with exactly one of: UPDATE, ADD, or CONTRADICT.
@@ -0,0 +1,23 @@
1
+ You are enriching chunks of a document with contextual prefixes. Each chunk is a section of a larger document, and your job is to write a brief context sentence (1-2 sentences, 50-100 tokens) that situates the chunk within the full document.
2
+
3
+ The prefix should help a search engine understand what the chunk is about even when read in isolation. Include the document title, section topic, and any relevant framing from the surrounding document.
4
+
5
+ Do NOT repeat the chunk content. Just provide the context that would be lost if the chunk were read alone.
6
+
7
+ ## Input
8
+
9
+ You will receive:
10
+ 1. The full document text
11
+ 2. A list of chunk excerpts (first 200 characters of each)
12
+
13
+ ## Output
14
+
15
+ Respond with ONLY a JSON array of strings — one context prefix per chunk, in the same order as the input chunks.
16
+
17
+ Example:
18
+ ```json
19
+ [
20
+ "This chunk from the API Design Guide covers authentication requirements for the REST API.",
21
+ "This section of the API Design Guide describes rate limiting policies and retry behavior."
22
+ ]
23
+ ```