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.
- package/.env.example +24 -10
- package/README.md +106 -15
- package/bin/iranti.js +1 -1
- package/dist/scripts/api-key-create.js +1 -7
- package/dist/scripts/api-key-list.js +1 -7
- package/dist/scripts/api-key-revoke.js +1 -7
- package/dist/scripts/claude-code-memory-hook.js +328 -0
- package/dist/scripts/codex-setup.js +124 -0
- package/dist/scripts/iranti-cli.js +867 -33
- package/dist/scripts/iranti-mcp.js +314 -0
- package/dist/scripts/seed.js +154 -5
- package/dist/scripts/setup.js +11 -1
- package/dist/src/api/middleware/validation.d.ts +45 -0
- package/dist/src/api/middleware/validation.d.ts.map +1 -1
- package/dist/src/api/middleware/validation.js +9 -0
- package/dist/src/api/middleware/validation.js.map +1 -1
- package/dist/src/api/routes/knowledge.d.ts.map +1 -1
- package/dist/src/api/routes/knowledge.js +45 -0
- package/dist/src/api/routes/knowledge.js.map +1 -1
- package/dist/src/api/server.js +18 -51
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/archivist/index.js +3 -0
- package/dist/src/archivist/index.js.map +1 -1
- package/dist/src/generated/prisma/internal/class.js +1 -1
- package/dist/src/generated/prisma/internal/class.js.map +1 -1
- package/dist/src/lib/providers/claude.d.ts +4 -1
- package/dist/src/lib/providers/claude.d.ts.map +1 -1
- package/dist/src/lib/providers/claude.js +38 -2
- package/dist/src/lib/providers/claude.js.map +1 -1
- package/dist/src/lib/providers/gemini.js +1 -1
- package/dist/src/lib/providers/groq.js +1 -1
- package/dist/src/lib/providers/groq.js.map +1 -1
- package/dist/src/lib/providers/openai.d.ts +4 -0
- package/dist/src/lib/providers/openai.d.ts.map +1 -1
- package/dist/src/lib/providers/openai.js +58 -10
- package/dist/src/lib/providers/openai.js.map +1 -1
- package/dist/src/lib/router.d.ts.map +1 -1
- package/dist/src/lib/router.js +66 -18
- package/dist/src/lib/router.js.map +1 -1
- package/dist/src/lib/runtimeEnv.d.ts +15 -0
- package/dist/src/lib/runtimeEnv.d.ts.map +1 -0
- package/dist/src/lib/runtimeEnv.js +91 -0
- package/dist/src/lib/runtimeEnv.js.map +1 -0
- package/dist/src/librarian/getPolicy.d.ts.map +1 -1
- package/dist/src/librarian/getPolicy.js +27 -4
- package/dist/src/librarian/getPolicy.js.map +1 -1
- package/dist/src/librarian/guards.d.ts.map +1 -1
- package/dist/src/librarian/guards.js +6 -0
- package/dist/src/librarian/guards.js.map +1 -1
- package/dist/src/librarian/index.d.ts +3 -1
- package/dist/src/librarian/index.d.ts.map +1 -1
- package/dist/src/librarian/index.js +57 -7
- package/dist/src/librarian/index.js.map +1 -1
- package/dist/src/library/client.d.ts +1 -0
- package/dist/src/library/client.d.ts.map +1 -1
- package/dist/src/library/client.js +14 -0
- package/dist/src/library/client.js.map +1 -1
- package/dist/src/library/embeddings.d.ts +5 -0
- package/dist/src/library/embeddings.d.ts.map +1 -0
- package/dist/src/library/embeddings.js +71 -0
- package/dist/src/library/embeddings.js.map +1 -0
- package/dist/src/library/queries.d.ts +2 -1
- package/dist/src/library/queries.d.ts.map +1 -1
- package/dist/src/library/queries.js +246 -8
- package/dist/src/library/queries.js.map +1 -1
- package/dist/src/sdk/index.d.ts +24 -0
- package/dist/src/sdk/index.d.ts.map +1 -1
- package/dist/src/sdk/index.js +28 -0
- package/dist/src/sdk/index.js.map +1 -1
- package/dist/src/types.d.ts +23 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +13 -2
- package/prisma/migrations/20260305000100_add_hybrid_search/migration.sql +13 -0
- 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.
|
|
14
|
+
GEMINI_MODEL=gemini-2.5-flash
|
|
15
15
|
|
|
16
|
-
# Model routing overrides (optional)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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-
|
|
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-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
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
|
-
|
|
519
|
+
Six PostgreSQL tables:
|
|
434
520
|
|
|
435
521
|
```
|
|
436
|
-
knowledge_base
|
|
437
|
-
archive
|
|
438
|
-
entity_relationships
|
|
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
|
-
|
|
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
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|