iranti 0.1.0 → 0.1.2

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 (74) hide show
  1. package/.env.example +24 -10
  2. package/README.md +106 -15
  3. package/bin/iranti.js +1 -1
  4. package/dist/scripts/api-key-create.js +1 -7
  5. package/dist/scripts/api-key-list.js +1 -7
  6. package/dist/scripts/api-key-revoke.js +1 -7
  7. package/dist/scripts/claude-code-memory-hook.js +328 -0
  8. package/dist/scripts/codex-setup.js +124 -0
  9. package/dist/scripts/iranti-cli.js +867 -33
  10. package/dist/scripts/iranti-mcp.js +314 -0
  11. package/dist/scripts/seed.js +154 -5
  12. package/dist/scripts/setup.js +11 -1
  13. package/dist/src/api/middleware/validation.d.ts +45 -0
  14. package/dist/src/api/middleware/validation.d.ts.map +1 -1
  15. package/dist/src/api/middleware/validation.js +9 -0
  16. package/dist/src/api/middleware/validation.js.map +1 -1
  17. package/dist/src/api/routes/knowledge.d.ts.map +1 -1
  18. package/dist/src/api/routes/knowledge.js +45 -0
  19. package/dist/src/api/routes/knowledge.js.map +1 -1
  20. package/dist/src/api/server.js +18 -51
  21. package/dist/src/api/server.js.map +1 -1
  22. package/dist/src/archivist/index.js +3 -0
  23. package/dist/src/archivist/index.js.map +1 -1
  24. package/dist/src/generated/prisma/internal/class.js +1 -1
  25. package/dist/src/generated/prisma/internal/class.js.map +1 -1
  26. package/dist/src/lib/providers/claude.d.ts +4 -1
  27. package/dist/src/lib/providers/claude.d.ts.map +1 -1
  28. package/dist/src/lib/providers/claude.js +38 -2
  29. package/dist/src/lib/providers/claude.js.map +1 -1
  30. package/dist/src/lib/providers/gemini.js +1 -1
  31. package/dist/src/lib/providers/groq.js +1 -1
  32. package/dist/src/lib/providers/groq.js.map +1 -1
  33. package/dist/src/lib/providers/openai.d.ts +4 -0
  34. package/dist/src/lib/providers/openai.d.ts.map +1 -1
  35. package/dist/src/lib/providers/openai.js +58 -10
  36. package/dist/src/lib/providers/openai.js.map +1 -1
  37. package/dist/src/lib/router.d.ts.map +1 -1
  38. package/dist/src/lib/router.js +66 -18
  39. package/dist/src/lib/router.js.map +1 -1
  40. package/dist/src/lib/runtimeEnv.d.ts +15 -0
  41. package/dist/src/lib/runtimeEnv.d.ts.map +1 -0
  42. package/dist/src/lib/runtimeEnv.js +91 -0
  43. package/dist/src/lib/runtimeEnv.js.map +1 -0
  44. package/dist/src/librarian/getPolicy.d.ts.map +1 -1
  45. package/dist/src/librarian/getPolicy.js +27 -4
  46. package/dist/src/librarian/getPolicy.js.map +1 -1
  47. package/dist/src/librarian/guards.d.ts.map +1 -1
  48. package/dist/src/librarian/guards.js +6 -0
  49. package/dist/src/librarian/guards.js.map +1 -1
  50. package/dist/src/librarian/index.d.ts +3 -1
  51. package/dist/src/librarian/index.d.ts.map +1 -1
  52. package/dist/src/librarian/index.js +57 -7
  53. package/dist/src/librarian/index.js.map +1 -1
  54. package/dist/src/library/client.d.ts +1 -0
  55. package/dist/src/library/client.d.ts.map +1 -1
  56. package/dist/src/library/client.js +14 -0
  57. package/dist/src/library/client.js.map +1 -1
  58. package/dist/src/library/embeddings.d.ts +5 -0
  59. package/dist/src/library/embeddings.d.ts.map +1 -0
  60. package/dist/src/library/embeddings.js +71 -0
  61. package/dist/src/library/embeddings.js.map +1 -0
  62. package/dist/src/library/queries.d.ts +2 -1
  63. package/dist/src/library/queries.d.ts.map +1 -1
  64. package/dist/src/library/queries.js +246 -8
  65. package/dist/src/library/queries.js.map +1 -1
  66. package/dist/src/sdk/index.d.ts +24 -0
  67. package/dist/src/sdk/index.d.ts.map +1 -1
  68. package/dist/src/sdk/index.js +28 -0
  69. package/dist/src/sdk/index.js.map +1 -1
  70. package/dist/src/types.d.ts +23 -0
  71. package/dist/src/types.d.ts.map +1 -1
  72. package/package.json +13 -2
  73. package/prisma/migrations/20260305000100_add_hybrid_search/migration.sql +13 -0
  74. package/prisma/schema.prisma +1 -0
package/.env.example CHANGED
@@ -11,27 +11,31 @@ LLM_PROVIDER_FALLBACK=openai,groq,mistral,mock
11
11
 
12
12
  # Gemini (when LLM_PROVIDER=gemini)
13
13
  GEMINI_API_KEY=
14
- GEMINI_MODEL=gemini-2.0-flash-001
14
+ GEMINI_MODEL=gemini-2.5-flash
15
15
 
16
- # Model routing overrides (optional)
17
- CLASSIFICATION_MODEL=gemini-2.0-flash-001
18
- RELEVANCE_MODEL=gemini-2.0-flash-001
19
- CONFLICT_MODEL=gemini-2.5-pro
20
- SUMMARIZATION_MODEL=gemini-2.0-flash-001
21
- TASK_INFERENCE_MODEL=gemini-2.0-flash-001
22
- EXTRACTION_MODEL=gemini-2.0-flash-001
16
+ # Model routing overrides (optional).
17
+ # Leave these blank unless you intentionally pin per-task models
18
+ # for your selected provider.
19
+ CLASSIFICATION_MODEL=
20
+ RELEVANCE_MODEL=
21
+ CONFLICT_MODEL=
22
+ SUMMARIZATION_MODEL=
23
+ TASK_INFERENCE_MODEL=
24
+ EXTRACTION_MODEL=
23
25
 
24
26
  # Anthropic (when LLM_PROVIDER=claude)
25
27
  ANTHROPIC_API_KEY=
28
+ ANTHROPIC_MODEL=claude-sonnet-4
29
+ ANTHROPIC_BASE_URL=
26
30
 
27
31
  # OpenAI (when LLM_PROVIDER=openai)
28
32
  OPENAI_API_KEY=
29
- OPENAI_MODEL=gpt-4o-mini
33
+ OPENAI_MODEL=gpt-5-mini
30
34
  OPENAI_BASE_URL=https://api.openai.com/v1
31
35
 
32
36
  # Groq (when LLM_PROVIDER=groq)
33
37
  GROQ_API_KEY=
34
- GROQ_MODEL=llama-3.3-70b-versatile
38
+ GROQ_MODEL=meta-llama/llama-4-scout-17b-16e-instruct
35
39
 
36
40
  # Mistral (when LLM_PROVIDER=mistral)
37
41
  MISTRAL_API_KEY=
@@ -59,5 +63,15 @@ IRANTI_ARCHIVIST_DEBOUNCE_MS=60000
59
63
  # Periodic maintenance interval; set >0 to enable (e.g. 21600000 = 6h)
60
64
  IRANTI_ARCHIVIST_INTERVAL_MS=0
61
65
 
66
+ # Hybrid search embedding dimensions (default 256)
67
+ IRANTI_EMBEDDING_DIM=256
68
+
69
+ # Optional Claude Code / MCP integration defaults
70
+ IRANTI_MCP_DEFAULT_AGENT=claude_code
71
+ IRANTI_MCP_DEFAULT_SOURCE=ClaudeCode
72
+ IRANTI_CLAUDE_AGENT_ID=
73
+ IRANTI_CLAUDE_ENTITY_HINTS=
74
+ IRANTI_CLAUDE_MAX_FACTS=6
75
+
62
76
  # Node environment
63
77
  NODE_ENV=development
package/README.md CHANGED
@@ -7,13 +7,13 @@
7
7
 
8
8
  **Memory infrastructure for multi-agent AI systems.**
9
9
 
10
- Iranti gives agents persistent, identity-based memory. Facts written by one agent are retrievable by any other agent through exact entity+key lookup, not similarity search. Memory persists across sessions and survives context window limits.
10
+ Iranti gives agents persistent, identity-based memory. Facts written by one agent are retrievable by any other agent through exact entity+key lookup. Iranti also supports hybrid search (lexical + vector) when exact keys are unknown. Memory persists across sessions and survives context window limits.
11
11
 
12
12
  ---
13
13
 
14
14
  ## What is Iranti?
15
15
 
16
- Iranti is a knowledge base for multi-agent systems. Unlike vector databases that retrieve by semantic similarity, Iranti retrieves by identity — this specific entity (`project/nexus_prime`), this specific key (`deadline`), with confidence attached. When Agent A writes a fact, Agent B can retrieve it by exact lookup without being told it exists. Facts persist in PostgreSQL and survive context window boundaries through the `observe()` API.
16
+ Iranti is a knowledge base for multi-agent systems. The primary read path is identity retrieval — this specific entity (`project/nexus_prime`), this specific key (`deadline`), with confidence attached. When Agent A writes a fact, Agent B can retrieve it by exact lookup without being told it exists. Facts persist in PostgreSQL and survive context window boundaries through the `observe()` API. For discovery workflows, Iranti supports hybrid search (full-text + vector similarity).
17
17
 
18
18
  ---
19
19
 
@@ -33,14 +33,14 @@ Iranti is a knowledge base for multi-agent systems. Unlike vector databases that
33
33
 
34
34
  | Feature | Vector DB | Iranti |
35
35
  |---|---|---|
36
- | **Retrieval** | Similarity (nearest neighbor) | Identity (entity+key) |
36
+ | **Retrieval** | Similarity (nearest neighbor) | Identity-first + optional hybrid search |
37
37
  | **Storage** | Embeddings in vector space | Structured facts with keys |
38
38
  | **Persistence** | Stateless between calls | Persistent across sessions |
39
39
  | **Confidence** | No confidence tracking | Per-fact confidence scores |
40
40
  | **Conflicts** | No conflict resolution | Automatic resolution + escalation |
41
41
  | **Context** | No context awareness | `observe()` injects missing facts |
42
42
 
43
- Vector databases answer "what's similar to X?" Iranti answers "what do we know about X?"
43
+ Vector databases answer "what's similar to X?" Iranti answers "what do we know about X?" and can run hybrid search when exact keys are unknown.
44
44
 
45
45
  ---
46
46
 
@@ -161,6 +161,42 @@ npm run api-key:revoke -- --key-id chatbot_alice
161
161
  Use the printed token (`keyId.secret`) as `X-Iranti-Key`.
162
162
  Scopes use `resource:action` format (for example `kb:read`, `memory:write`, `metrics:read`, `proxy:chat`).
163
163
 
164
+ ### Security Baseline
165
+
166
+ - Use one scoped key per app/service identity.
167
+ - Rotate any key that is exposed in logs, screenshots, or chat.
168
+ - Keep escalation/log paths outside the repo working tree.
169
+ - Use TLS/reverse proxy for non-local deployments.
170
+
171
+ Security quickstart: [`docs/guides/security-quickstart.md`](docs/guides/security-quickstart.md)
172
+ Claude Code guide: [`docs/guides/claude-code.md`](docs/guides/claude-code.md)
173
+ Codex guide: [`docs/guides/codex.md`](docs/guides/codex.md)
174
+ Release guide: [`docs/guides/releasing.md`](docs/guides/releasing.md)
175
+
176
+ ### Claude Code via MCP
177
+
178
+ Iranti ships a local stdio MCP server for Claude Code and other MCP clients:
179
+
180
+ ```bash
181
+ iranti mcp
182
+ ```
183
+
184
+ Use it with a project-local `.mcp.json`, and optionally add `iranti claude-hook` for `SessionStart` and `UserPromptSubmit`.
185
+
186
+ Guide: [`docs/guides/claude-code.md`](docs/guides/claude-code.md)
187
+
188
+ ### Codex via MCP
189
+
190
+ Codex uses a global MCP registry rather than a project-local `.mcp.json`. Register Iranti once, then launch Codex in this repo so `AGENTS.md` applies:
191
+
192
+ ```bash
193
+ npm run build
194
+ npm run codex:setup
195
+ npm run codex:run
196
+ ```
197
+
198
+ Guide: [`docs/guides/codex.md`](docs/guides/codex.md)
199
+
164
200
  ---
165
201
 
166
202
  ## Install Strategy (Double Layer)
@@ -195,13 +231,22 @@ Defaults:
195
231
  ### 3) Create a named instance
196
232
 
197
233
  ```bash
198
- iranti instance create local --port 3001 --db-url "postgresql://postgres:yourpassword@localhost:5432/iranti_local"
234
+ iranti instance create local --port 3001 --db-url "postgresql://postgres:yourpassword@localhost:5432/iranti_local" --provider mock
199
235
  iranti instance show local
200
236
  ```
201
237
 
202
- Then edit the printed instance `.env` file and set:
203
- - `DATABASE_URL` (real value)
204
- - `IRANTI_API_KEY` (real token)
238
+ Finish onboarding or change settings later with:
239
+
240
+ ```bash
241
+ # Provider/db updates
242
+ iranti configure instance local --provider openai --provider-key sk-... --db-url "postgresql://postgres:realpassword@localhost:5432/iranti_local"
243
+ iranti configure instance local --interactive
244
+
245
+ # Create a registry-backed API key and sync it into the instance env
246
+ iranti auth create-key --instance local --key-id local_admin --owner "Local Admin" --scopes "kb:read,kb:write,memory:read,memory:write,agents:read,agents:write" --write-instance
247
+ ```
248
+
249
+ You can rerun `iranti configure instance ...` later to rotate provider keys or replace the instance API token.
205
250
 
206
251
  ### 4) Run Iranti from that instance
207
252
 
@@ -218,8 +263,33 @@ iranti project init . --instance local --agent-id chatbot_main
218
263
 
219
264
  This writes `.env.iranti` in the project with the correct `IRANTI_URL`, `IRANTI_API_KEY`, and default agent identity.
220
265
 
266
+ Later changes use the same surface:
267
+
268
+ ```bash
269
+ iranti configure project . --instance local --agent-id chatbot_worker
270
+ iranti configure project . --interactive
271
+ iranti auth create-key --instance local --key-id chatbot_worker --owner "Chatbot Worker" --scopes "kb:read,memory:read,memory:write" --project .
272
+ ```
273
+
221
274
  For multi-agent systems, bind once per project and set unique agent IDs per worker (for example `planner_agent`, `research_agent`, `critic_agent`).
222
275
 
276
+ ### Installation Diagnostics
277
+
278
+ Use the CLI doctor command before first run or before a release check:
279
+
280
+ ```bash
281
+ iranti doctor
282
+ iranti doctor --instance local
283
+ iranti status
284
+ iranti upgrade
285
+ ```
286
+
287
+ This validates the active env file, database URL, API key presence, provider selection, and provider-specific credentials.
288
+ `iranti status` shows the current runtime root, known instances, and local binding files.
289
+ `iranti upgrade` prints the supported upgrade commands for npm and Python installs.
290
+ `iranti configure ...` updates instance/project credentials without manual env editing.
291
+ `iranti auth ...` manages registry-backed API keys and can sync them into instance or project bindings.
292
+
223
293
  ---
224
294
 
225
295
  ## Core API
@@ -267,6 +337,21 @@ for fact in facts:
267
337
  print(f"[{fact['key']}] {fact['summary']} (confidence: {fact['confidence']})")
268
338
  ```
269
339
 
340
+ ### Hybrid Search
341
+
342
+ ```python
343
+ matches = client.search(
344
+ query="current blocker launch readiness",
345
+ entity_type="project",
346
+ limit=5,
347
+ lexical_weight=0.45,
348
+ vector_weight=0.55,
349
+ )
350
+
351
+ for item in matches:
352
+ print(item["entity"], item["key"], item["score"])
353
+ ```
354
+
270
355
  ### Context Persistence (attend)
271
356
 
272
357
  ```python
@@ -404,7 +489,7 @@ Iranti has four internal components:
404
489
 
405
490
  | Component | Role |
406
491
  |---|---|
407
- | **Library** | PostgreSQL knowledge base. Active truth (soft-deleted entries marked as archived). Full provenance in Archive table. |
492
+ | **Library** | PostgreSQL knowledge base. Active truth in `knowledge_base` with full provenance in `archive`; archived rows are retained and marked `[ARCHIVED]` in active storage. |
408
493
  | **Librarian** | Manages all writes. Detects conflicts, reasons about resolution, escalates when uncertain. |
409
494
  | **Attendant** | Per-agent working memory manager. Implements `attend()`, `observe()`, and `handshake()` APIs. |
410
495
  | **Archivist** | Periodic cleanup. Archives expired and low-confidence entries. Processes human-resolved conflicts. |
@@ -417,6 +502,7 @@ Express server on port 3001 with endpoints:
417
502
  - `POST /kb/ingest` - Ingest raw text, auto-chunk into facts
418
503
  - `GET /kb/query/:entityType/:entityId/:key` - Query specific fact
419
504
  - `GET /kb/query/:entityType/:entityId` - Query all facts for entity
505
+ - `GET /kb/search` - Hybrid search across facts
420
506
  - `POST /memory/attend` - Decide whether to inject memory for this turn
421
507
  - `POST /memory/observe` - Context persistence (inject missing facts)
422
508
  - `POST /memory/handshake` - Working memory brief for agent session
@@ -430,17 +516,20 @@ All endpoints require `X-Iranti-Key` header for authentication.
430
516
 
431
517
  ## Schema
432
518
 
433
- Three PostgreSQL tables:
519
+ Six PostgreSQL tables:
434
520
 
435
521
  ```
436
- knowledge_base active truth (archived entries soft-deleted with confidence=0)
437
- archive full provenance history, never deleted, includes supersededBy links
438
- entity_relationships directional graph: MEMBER_OF, PART_OF, AUTHORED, etc.
522
+ knowledge_base - active truth (archived rows retained with confidence=0)
523
+ archive - full provenance history, never deleted
524
+ entity_relationships - directional graph: MEMBER_OF, PART_OF, AUTHORED, etc.
525
+ entities - canonical entity identity registry
526
+ entity_aliases - normalized aliases mapped to canonical entities
527
+ write_receipts - idempotency receipts for requestId replay safety
439
528
  ```
440
529
 
441
- Every table has a `properties` JSON column for caller-defined metadata. New entity types, relationship types, and fact keys never require migrations they are just strings you define.
530
+ New entity types, relationship types, and fact keys do not require migrations; they are caller-defined strings.
442
531
 
443
- **Archive semantics**: When an entry is archived, it remains in knowledge_base with confidence set to 0 and summary marked as `[ARCHIVED]`. A full copy is written to the archive table with supersededBy linking for traceability. Nothing is ever truly deleted.
532
+ **Archive semantics**: When an entry is archived, it remains in knowledge_base with confidence set to 0 and summary marked as `[ARCHIVED]`. A full copy is written to the archive table for traceability. Nothing is ever truly deleted.
444
533
 
445
534
  ---
446
535
 
@@ -514,7 +603,9 @@ docs/
514
603
  - **Issues**: [GitHub Issues](https://github.com/nfemmanuel/iranti/issues)
515
604
  - **Discussions**: [GitHub Discussions](https://github.com/nfemmanuel/iranti/discussions)
516
605
  - **Email**: oluwaniifemi.emmanuel@uni.minerva.edu
606
+ - **Changelog**: [`CHANGELOG.md`](CHANGELOG.md)
517
607
 
518
608
  ---
519
609
 
520
610
  **Built with ❤️ for the multi-agent AI community.**
611
+
package/bin/iranti.js CHANGED
@@ -21,4 +21,4 @@ try {
21
21
  console.error('Iranti CLI is not built. Run "npm run build" first.');
22
22
  console.error(message);
23
23
  process.exit(1);
24
- }
24
+ }
@@ -42,16 +42,10 @@ async function main() {
42
42
  console.log('\nCopy this token now (it will not be shown again):');
43
43
  console.log(created.token);
44
44
  console.log('\nUse it as: X-Iranti-Key: <token>\n');
45
- await (0, client_1.getDb)().$disconnect();
45
+ process.exit(0);
46
46
  }
47
47
  main().catch(async (err) => {
48
48
  console.error('Failed to create API key:', err instanceof Error ? err.message : String(err));
49
- try {
50
- await (0, client_1.getDb)().$disconnect();
51
- }
52
- catch {
53
- // ignore
54
- }
55
49
  process.exit(1);
56
50
  });
57
51
  //# sourceMappingURL=api-key-create.js.map
@@ -27,16 +27,10 @@ async function main() {
27
27
  }
28
28
  }
29
29
  }
30
- await (0, client_1.getDb)().$disconnect();
30
+ process.exit(0);
31
31
  }
32
32
  main().catch(async (err) => {
33
33
  console.error('Failed to list API keys:', err instanceof Error ? err.message : String(err));
34
- try {
35
- await (0, client_1.getDb)().$disconnect();
36
- }
37
- catch {
38
- // ignore
39
- }
40
34
  process.exit(1);
41
35
  });
42
36
  //# sourceMappingURL=api-key-list.js.map
@@ -27,16 +27,10 @@ async function main() {
27
27
  process.exit(1);
28
28
  }
29
29
  console.log(`Revoked API key: ${keyId}`);
30
- await (0, client_1.getDb)().$disconnect();
30
+ process.exit(0);
31
31
  }
32
32
  main().catch(async (err) => {
33
33
  console.error('Failed to revoke API key:', err instanceof Error ? err.message : String(err));
34
- try {
35
- await (0, client_1.getDb)().$disconnect();
36
- }
37
- catch {
38
- // ignore
39
- }
40
34
  process.exit(1);
41
35
  });
42
36
  //# sourceMappingURL=api-key-revoke.js.map
@@ -0,0 +1,328 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ require("dotenv/config");
7
+ const path_1 = __importDefault(require("path"));
8
+ const sdk_1 = require("../src/sdk");
9
+ const runtimeEnv_1 = require("../src/lib/runtimeEnv");
10
+ const MEMORY_NEED_POSITIVE_PATTERNS = [
11
+ /\bwhat(?:'s| is| was)?\s+my\b/i,
12
+ /\bdo you remember\b/i,
13
+ /\bremind me\b/i,
14
+ /\bmy\s+(?:favorite|favourite|name|email|phone|address|city|country|movie|snack|color|colour)\b/i,
15
+ /\bwe decided\b/i,
16
+ /\bearlier\b/i,
17
+ /\bprevious(?:ly)?\b/i,
18
+ /\bagain\b/i,
19
+ ];
20
+ const MEMORY_NEED_NEGATIVE_PATTERNS = [
21
+ /^\s*(hi|hello|hey|yo|sup|good (?:morning|afternoon|evening))\b[!.?\s]*$/i,
22
+ /^\s*(thanks|thank you|cool|great|nice)\b[!.?\s]*$/i,
23
+ ];
24
+ function printHelp() {
25
+ console.log([
26
+ 'Claude Code -> Iranti hook helper',
27
+ '',
28
+ 'Usage:',
29
+ ' ts-node scripts/claude-code-memory-hook.ts --event SessionStart',
30
+ ' ts-node scripts/claude-code-memory-hook.ts --event UserPromptSubmit',
31
+ '',
32
+ 'Optional flags:',
33
+ ' --project-env <path> Explicit .env.iranti path',
34
+ ' --instance-env <path> Explicit instance env path',
35
+ ' --env-file <path> Explicit base .env path',
36
+ '',
37
+ 'Reads Claude Code hook JSON from stdin and returns hookSpecificOutput.additionalContext on stdout.',
38
+ ].join('\n'));
39
+ }
40
+ function parseArgs(argv) {
41
+ const out = {};
42
+ for (let i = 0; i < argv.length; i += 1) {
43
+ const token = argv[i];
44
+ if (!token.startsWith('--'))
45
+ continue;
46
+ const key = token.slice(2);
47
+ const next = argv[i + 1];
48
+ if (!next || next.startsWith('--')) {
49
+ out[key] = 'true';
50
+ continue;
51
+ }
52
+ out[key] = next;
53
+ i += 1;
54
+ }
55
+ return out;
56
+ }
57
+ async function readStdin() {
58
+ return new Promise((resolve) => {
59
+ let out = '';
60
+ process.stdin.setEncoding('utf8');
61
+ process.stdin.on('data', (chunk) => { out += chunk; });
62
+ process.stdin.on('end', () => resolve(out));
63
+ process.stdin.resume();
64
+ });
65
+ }
66
+ function parsePayload(raw) {
67
+ const trimmed = raw.trim();
68
+ if (!trimmed)
69
+ return {};
70
+ try {
71
+ return JSON.parse(trimmed);
72
+ }
73
+ catch {
74
+ return {};
75
+ }
76
+ }
77
+ function requireConnectionString() {
78
+ const connectionString = process.env.DATABASE_URL?.trim();
79
+ if (!connectionString) {
80
+ throw new Error('DATABASE_URL is required for claude-code-memory-hook.');
81
+ }
82
+ return connectionString;
83
+ }
84
+ function slugify(value) {
85
+ return value
86
+ .toLowerCase()
87
+ .replace(/[^a-z0-9]+/g, '_')
88
+ .replace(/^_+|_+$/g, '');
89
+ }
90
+ function getCwd(payload) {
91
+ const fromPayload = typeof payload.cwd === 'string' && payload.cwd.trim().length > 0
92
+ ? payload.cwd.trim()
93
+ : null;
94
+ return fromPayload || process.cwd();
95
+ }
96
+ function getDefaultAgentId(payload) {
97
+ const explicit = process.env.IRANTI_CLAUDE_AGENT_ID?.trim();
98
+ if (explicit)
99
+ return explicit;
100
+ const projectBindingAgent = process.env.IRANTI_AGENT_ID?.trim();
101
+ if (projectBindingAgent)
102
+ return projectBindingAgent;
103
+ const base = path_1.default.basename(getCwd(payload));
104
+ return `claude_code_${slugify(base || 'project')}`;
105
+ }
106
+ function getEntityHints(payload) {
107
+ const out = new Set();
108
+ const cwd = getCwd(payload);
109
+ const projectHint = `project/${slugify(path_1.default.basename(cwd) || 'project')}`;
110
+ out.add(projectHint);
111
+ const memoryEntity = process.env.IRANTI_MEMORY_ENTITY?.trim();
112
+ if (memoryEntity) {
113
+ out.add(memoryEntity);
114
+ }
115
+ const envHints = (process.env.IRANTI_CLAUDE_ENTITY_HINTS ?? '')
116
+ .split(',')
117
+ .map((value) => value.trim())
118
+ .filter(Boolean);
119
+ for (const hint of envHints)
120
+ out.add(hint);
121
+ return Array.from(out);
122
+ }
123
+ async function ensureHookAgent(iranti, payload) {
124
+ const agentId = getDefaultAgentId(payload);
125
+ await iranti.registerAgent({
126
+ agentId,
127
+ name: process.env.IRANTI_CLAUDE_AGENT_NAME?.trim() || 'Claude Code Hook',
128
+ description: process.env.IRANTI_CLAUDE_AGENT_DESCRIPTION?.trim() || 'Claude Code automatic memory hook',
129
+ capabilities: ['working_memory', 'memory_injection'],
130
+ model: process.env.ANTHROPIC_MODEL || 'claude-code',
131
+ });
132
+ return agentId;
133
+ }
134
+ function getPrompt(payload) {
135
+ const candidates = [
136
+ payload.prompt,
137
+ payload.message,
138
+ payload.text,
139
+ ];
140
+ for (const candidate of candidates) {
141
+ if (typeof candidate === 'string' && candidate.trim()) {
142
+ return candidate.trim();
143
+ }
144
+ }
145
+ return '';
146
+ }
147
+ function getMaxFacts() {
148
+ const raw = Number(process.env.IRANTI_CLAUDE_MAX_FACTS ?? 6);
149
+ if (!Number.isFinite(raw) || raw < 1)
150
+ return 6;
151
+ return Math.min(12, Math.trunc(raw));
152
+ }
153
+ function formatSessionContext(facts, cwd) {
154
+ const limited = facts.slice(0, getMaxFacts());
155
+ const lines = [
156
+ '[Iranti Session Memory]',
157
+ `Project: ${path_1.default.basename(cwd)}`,
158
+ ];
159
+ if (limited.length > 0) {
160
+ lines.push('Relevant memory:');
161
+ for (const fact of limited) {
162
+ lines.push(`- ${fact.entity}/${fact.key}: ${fact.summary}`);
163
+ }
164
+ }
165
+ return lines.join('\n');
166
+ }
167
+ function formatPromptContext(facts) {
168
+ if (facts.length === 0)
169
+ return '';
170
+ const lines = ['[Iranti Retrieved Memory]'];
171
+ for (const fact of facts) {
172
+ lines.push(`- ${fact.entity}/${fact.key}: ${fact.summary}`);
173
+ }
174
+ return lines.join('\n');
175
+ }
176
+ function emitHookContext(event, additionalContext) {
177
+ const payload = {
178
+ hookSpecificOutput: {
179
+ hookEventName: event,
180
+ additionalContext,
181
+ },
182
+ };
183
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
184
+ }
185
+ function shouldFetchMemory(prompt) {
186
+ const normalized = prompt.trim();
187
+ if (!normalized)
188
+ return false;
189
+ if (MEMORY_NEED_NEGATIVE_PATTERNS.some((pattern) => pattern.test(normalized))) {
190
+ return false;
191
+ }
192
+ if (MEMORY_NEED_POSITIVE_PATTERNS.some((pattern) => pattern.test(normalized))) {
193
+ return true;
194
+ }
195
+ if (/\b(my|our|we)\b/i.test(normalized)) {
196
+ return true;
197
+ }
198
+ return normalized.includes('/');
199
+ }
200
+ function dedupeFacts(facts) {
201
+ const byKey = new Map();
202
+ for (const fact of facts) {
203
+ const identity = `${fact.entity}/${fact.key}`;
204
+ const existing = byKey.get(identity);
205
+ if (!existing || fact.confidence > existing.confidence) {
206
+ byKey.set(identity, fact);
207
+ }
208
+ }
209
+ return Array.from(byKey.values())
210
+ .sort((a, b) => b.confidence - a.confidence || a.entity.localeCompare(b.entity) || a.key.localeCompare(b.key))
211
+ .slice(0, getMaxFacts());
212
+ }
213
+ async function loadAttendantStateFacts(iranti, agent) {
214
+ const state = await iranti.query(`agent/${agent}`, 'attendant_state');
215
+ if (!state.found || !state.value || typeof state.value !== 'object') {
216
+ return [];
217
+ }
218
+ const workingMemory = Array.isArray(state.value.workingMemory)
219
+ ? state.value.workingMemory
220
+ : [];
221
+ return workingMemory.flatMap((entry) => {
222
+ const entityKey = typeof entry.entityKey === 'string' ? entry.entityKey.trim() : '';
223
+ const summary = typeof entry.summary === 'string' ? entry.summary.trim() : '';
224
+ if (!entityKey || !summary)
225
+ return [];
226
+ const segments = entityKey.split('/');
227
+ if (segments.length < 3)
228
+ return [];
229
+ return [{
230
+ entity: `${segments[0]}/${segments[1]}`,
231
+ key: segments.slice(2).join('/'),
232
+ summary,
233
+ confidence: typeof entry.confidence === 'number' ? entry.confidence : 0,
234
+ source: typeof entry.source === 'string' ? entry.source : 'attendant',
235
+ }];
236
+ });
237
+ }
238
+ async function loadEntityFacts(iranti, entities) {
239
+ const out = [];
240
+ for (const entity of entities) {
241
+ const trimmed = entity.trim();
242
+ if (!trimmed)
243
+ continue;
244
+ const entries = await iranti.queryAll(trimmed).catch(() => []);
245
+ for (const entry of entries) {
246
+ out.push({
247
+ entity: trimmed,
248
+ key: entry.key,
249
+ summary: entry.summary,
250
+ confidence: entry.confidence,
251
+ source: entry.source,
252
+ });
253
+ }
254
+ }
255
+ return out;
256
+ }
257
+ async function searchPromptFacts(iranti, prompt, entityHints) {
258
+ if (!prompt.trim())
259
+ return [];
260
+ const results = await iranti.search({
261
+ query: prompt,
262
+ limit: getMaxFacts(),
263
+ minScore: Number(process.env.IRANTI_CLAUDE_MIN_SEARCH_SCORE ?? 0.05),
264
+ }).catch(() => []);
265
+ const searched = results.map((result) => ({
266
+ entity: result.entity,
267
+ key: result.key,
268
+ summary: result.summary,
269
+ confidence: result.confidence,
270
+ source: result.source,
271
+ }));
272
+ if (searched.length > 0) {
273
+ return searched;
274
+ }
275
+ return loadEntityFacts(iranti, entityHints);
276
+ }
277
+ async function main() {
278
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
279
+ printHelp();
280
+ return;
281
+ }
282
+ const args = parseArgs(process.argv.slice(2));
283
+ const event = args.event;
284
+ if (event !== 'SessionStart' && event !== 'UserPromptSubmit') {
285
+ throw new Error('--event must be SessionStart or UserPromptSubmit');
286
+ }
287
+ const payload = parsePayload(await readStdin());
288
+ (0, runtimeEnv_1.loadRuntimeEnv)({
289
+ payloadCwd: getCwd(payload),
290
+ projectEnvFile: args['project-env'],
291
+ instanceEnvFile: args['instance-env'],
292
+ explicitEnvFile: args['env-file'],
293
+ });
294
+ const cwd = getCwd(payload);
295
+ const iranti = new sdk_1.Iranti({
296
+ connectionString: requireConnectionString(),
297
+ llmProvider: process.env.LLM_PROVIDER,
298
+ });
299
+ const agent = await ensureHookAgent(iranti, payload);
300
+ const entityHints = getEntityHints(payload);
301
+ if (event === 'SessionStart') {
302
+ const persistedFacts = await loadAttendantStateFacts(iranti, agent);
303
+ const directFacts = persistedFacts.length > 0
304
+ ? persistedFacts
305
+ : await loadEntityFacts(iranti, entityHints);
306
+ emitHookContext(event, formatSessionContext(dedupeFacts(directFacts), cwd));
307
+ process.exit(0);
308
+ }
309
+ const prompt = getPrompt(payload);
310
+ if (!prompt) {
311
+ process.exit(0);
312
+ }
313
+ if (!shouldFetchMemory(prompt)) {
314
+ process.exit(0);
315
+ }
316
+ const facts = await searchPromptFacts(iranti, prompt, entityHints);
317
+ const context = formatPromptContext(dedupeFacts(facts));
318
+ if (!context) {
319
+ process.exit(0);
320
+ }
321
+ emitHookContext(event, context);
322
+ process.exit(0);
323
+ }
324
+ main().catch((error) => {
325
+ console.error('[claude-code-memory-hook] fatal:', error instanceof Error ? error.message : String(error));
326
+ process.exit(1);
327
+ });
328
+ //# sourceMappingURL=claude-code-memory-hook.js.map