iranti 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -7
- package/dist/scripts/iranti-cli.js +220 -23
- package/dist/scripts/iranti-mcp.js +30 -11
- package/dist/scripts/seed.js +10 -10
- package/dist/src/api/middleware/validation.d.ts +1 -1
- package/dist/src/api/middleware/validation.js +1 -1
- 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 +29 -1
- package/dist/src/api/routes/knowledge.js.map +1 -1
- package/dist/src/api/server.js +1 -1
- package/dist/src/archivist/index.d.ts.map +1 -1
- package/dist/src/archivist/index.js +99 -54
- package/dist/src/archivist/index.js.map +1 -1
- package/dist/src/attendant/AttendantInstance.d.ts.map +1 -1
- package/dist/src/attendant/AttendantInstance.js +17 -6
- package/dist/src/attendant/AttendantInstance.js.map +1 -1
- package/dist/src/generated/prisma/commonInputTypes.d.ts +181 -50
- package/dist/src/generated/prisma/commonInputTypes.d.ts.map +1 -1
- package/dist/src/generated/prisma/enums.d.ts +21 -1
- package/dist/src/generated/prisma/enums.d.ts.map +1 -1
- package/dist/src/generated/prisma/enums.js +19 -0
- package/dist/src/generated/prisma/enums.js.map +1 -1
- package/dist/src/generated/prisma/internal/class.js +4 -4
- package/dist/src/generated/prisma/internal/class.js.map +1 -1
- package/dist/src/generated/prisma/internal/prismaNamespace.d.ts +34 -4
- package/dist/src/generated/prisma/internal/prismaNamespace.d.ts.map +1 -1
- package/dist/src/generated/prisma/internal/prismaNamespace.js +6 -0
- package/dist/src/generated/prisma/internal/prismaNamespace.js.map +1 -1
- package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.d.ts +6 -0
- package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.d.ts.map +1 -1
- package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.js +6 -0
- package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.js.map +1 -1
- package/dist/src/generated/prisma/models/Archive.d.ts +110 -16
- package/dist/src/generated/prisma/models/Archive.d.ts.map +1 -1
- package/dist/src/generated/prisma/models/KnowledgeEntry.d.ts +100 -3
- package/dist/src/generated/prisma/models/KnowledgeEntry.d.ts.map +1 -1
- package/dist/src/lib/decay.d.ts +13 -0
- package/dist/src/lib/decay.d.ts.map +1 -0
- package/dist/src/lib/decay.js +54 -0
- package/dist/src/lib/decay.js.map +1 -0
- package/dist/src/lib/providers/mock.d.ts.map +1 -1
- package/dist/src/lib/providers/mock.js +25 -4
- package/dist/src/lib/providers/mock.js.map +1 -1
- package/dist/src/librarian/chunker.d.ts +1 -0
- package/dist/src/librarian/chunker.d.ts.map +1 -1
- package/dist/src/librarian/chunker.js +50 -10
- package/dist/src/librarian/chunker.js.map +1 -1
- package/dist/src/librarian/index.d.ts +3 -0
- package/dist/src/librarian/index.d.ts.map +1 -1
- package/dist/src/librarian/index.js +168 -198
- package/dist/src/librarian/index.js.map +1 -1
- package/dist/src/library/queries.d.ts +36 -7
- package/dist/src/library/queries.d.ts.map +1 -1
- package/dist/src/library/queries.js +160 -31
- package/dist/src/library/queries.js.map +1 -1
- package/dist/src/sdk/index.d.ts +38 -3
- package/dist/src/sdk/index.d.ts.map +1 -1
- package/dist/src/sdk/index.js +131 -61
- package/dist/src/sdk/index.js.map +1 -1
- package/dist/src/types.d.ts +6 -1
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +7 -1
- package/prisma/migrations/20260314120000_add_temporal_versioning_mvp/migration.sql +61 -0
- package/prisma/migrations/20260314164000_add_memory_decay_fields/migration.sql +23 -0
- package/prisma/schema.prisma +45 -16
package/README.md
CHANGED
|
@@ -70,6 +70,34 @@ Validated with multiple agent frameworks:
|
|
|
70
70
|
|
|
71
71
|
Full validation report: [`docs/internal/validation_results.md`](docs/internal/validation_results.md) | Multi-framework details: [`docs/internal/MULTI_FRAMEWORK_VALIDATION.md`](docs/internal/MULTI_FRAMEWORK_VALIDATION.md)
|
|
72
72
|
|
|
73
|
+
### Conflict Benchmark Baseline
|
|
74
|
+
|
|
75
|
+
Iranti now also has an adversarial conflict benchmark that measures contradiction handling rather than basic retrieval.
|
|
76
|
+
|
|
77
|
+
| Suite | Score | Notes |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| **Direct contradiction** | `4/4` | Same entity+key conflicts are explicitly resolved or escalated |
|
|
80
|
+
| **Temporal conflict** | `3/4` | One known-failing edge remains |
|
|
81
|
+
| **Cascading conflict** | `0/4` | Cross-key contradiction detection not implemented yet |
|
|
82
|
+
| **Multi-hop conflict** | `0/4` | Graph-aware conflict reasoning not implemented yet |
|
|
83
|
+
| **Total** | `7/16 (44%)` | Honest baseline for the current Librarian |
|
|
84
|
+
|
|
85
|
+
Conflict benchmark methodology: [`docs/internal/conflict_benchmark.md`](docs/internal/conflict_benchmark.md)
|
|
86
|
+
|
|
87
|
+
### Consistency Validation
|
|
88
|
+
|
|
89
|
+
Iranti also now documents and validates its consistency model empirically:
|
|
90
|
+
|
|
91
|
+
| Check | Result |
|
|
92
|
+
|---|---|
|
|
93
|
+
| Concurrent write serialization | `PASS` |
|
|
94
|
+
| Read-after-write visibility | `PASS` |
|
|
95
|
+
| Escalation state integrity | `PASS` |
|
|
96
|
+
| Observe isolation from uncommitted writes | `PASS` |
|
|
97
|
+
| **Total** | `4/4` |
|
|
98
|
+
|
|
99
|
+
Consistency model and validation: [`docs/internal/consistency_model.md`](docs/internal/consistency_model.md)
|
|
100
|
+
|
|
73
101
|
### Goal 1: Easy Integration
|
|
74
102
|
|
|
75
103
|
- **Entity**: `project/quantum_bridge`
|
|
@@ -108,9 +136,93 @@ Full validation report: [`docs/internal/validation_results.md`](docs/internal/va
|
|
|
108
136
|
|
|
109
137
|
Full validation report: [`docs/internal/validation_results.md`](docs/internal/validation_results.md)
|
|
110
138
|
|
|
139
|
+
## Gap Analysis
|
|
140
|
+
|
|
141
|
+
Iranti targets a specific gap in the agent infrastructure stack: most competing systems give you semantic retrieval, framework-specific memory, or raw vector storage, but not the same combination of structured fact storage, cross-agent sharing, identity-based lookup, explicit confidence, and developer-visible conflict handling in one self-hostable package.
|
|
142
|
+
|
|
143
|
+
The current competitive case for Iranti is strongest when a team needs memory that behaves more like shared infrastructure than a chat transcript: facts are attached to entities, retrieved deterministically by `entityType/entityId + key`, versioned over time, and made available across agents without framework lock-in.
|
|
144
|
+
|
|
145
|
+
### Where Iranti Is Differentiated
|
|
146
|
+
|
|
147
|
+
- Identity-first fact retrieval through `entityType/entityId + key`
|
|
148
|
+
- Cross-agent fact sharing as a first-class model
|
|
149
|
+
- Conflict-aware writes through the Librarian
|
|
150
|
+
- Explicit per-fact confidence scores
|
|
151
|
+
- Per-agent memory injection through the Attendant
|
|
152
|
+
- Temporal exact lookup with `asOf` and ordered `history()`
|
|
153
|
+
- Relationship primitives through `relate()`, `getRelated()`, and `getRelatedDeep()`
|
|
154
|
+
- Hybrid retrieval when exact keys are unknown
|
|
155
|
+
- Local install + project binding flow for Claude Code and Codex
|
|
156
|
+
- Published npm / PyPI surfaces with machine-level CLI setup
|
|
157
|
+
|
|
158
|
+
### Why That Gap Exists
|
|
159
|
+
|
|
160
|
+
The current landscape splits into three buckets:
|
|
161
|
+
|
|
162
|
+
1. **Memory libraries**
|
|
163
|
+
- Systems like Mem0, Zep, Letta, and framework-native memory layers solve parts of the problem.
|
|
164
|
+
- They usually optimize for semantic retrieval, agent-local memory, or framework integration.
|
|
165
|
+
- They rarely expose deterministic `entity + key` lookup, explicit confidence surfaces, and developer-controlled conflict handling together.
|
|
166
|
+
|
|
167
|
+
2. **Vector databases**
|
|
168
|
+
- Pinecone, Weaviate, Qdrant, Chroma, Milvus, LanceDB, and `pgvector` solve storage and retrieval infrastructure.
|
|
169
|
+
- They do not, by themselves, solve memory semantics such as conflict resolution, context injection, fact lifecycle, or shared agent-facing state.
|
|
170
|
+
|
|
171
|
+
3. **Multi-agent frameworks**
|
|
172
|
+
- CrewAI, LangGraph, AutoGen, CAMEL, MetaGPT, and similar frameworks often include some memory support.
|
|
173
|
+
- In practice, that memory is usually framework-coupled, shallow on conflict semantics, and difficult to reuse outside the framework that created it.
|
|
174
|
+
|
|
175
|
+
### Main Gaps
|
|
176
|
+
|
|
177
|
+
1. **Operational maturity**
|
|
178
|
+
- Local PostgreSQL setup is still a real source of friction.
|
|
179
|
+
- The product needs stronger diagnostics, connection recovery, and less dependence on users debugging local database state by hand.
|
|
180
|
+
|
|
181
|
+
2. **Onboarding still has sharp edges**
|
|
182
|
+
- `iranti setup` is materially better than before, but first-run still assumes too much infrastructure literacy.
|
|
183
|
+
- Managed Postgres paths, cleaner bootstrap verification, and fewer environment-level surprises are still needed.
|
|
184
|
+
|
|
185
|
+
3. **No operator UI yet**
|
|
186
|
+
- Iranti is still CLI-first.
|
|
187
|
+
- There is no control plane yet for provider keys, project bindings, integrations, memory inspection, and escalation review.
|
|
188
|
+
|
|
189
|
+
4. **Adoption proof is still early**
|
|
190
|
+
- The repo has validation experiments and real local end-to-end usage, but broad production adoption is still limited.
|
|
191
|
+
- The next product truth has to come from external users and real workloads, not more speculative architecture alone.
|
|
192
|
+
|
|
193
|
+
5. **Hosted product is not built**
|
|
194
|
+
- Open-source/local infrastructure is the active surface today.
|
|
195
|
+
- Hosted deployment, multi-tenant operations, billing, and cloud onboarding remain future work.
|
|
196
|
+
|
|
197
|
+
6. **Graph-native reasoning is still limited**
|
|
198
|
+
- Iranti supports explicit entity relationships today.
|
|
199
|
+
- It does not yet compete with graph-first systems on temporal graph traversal or graph-native reasoning depth.
|
|
200
|
+
|
|
201
|
+
7. **Memory extraction is not the main model**
|
|
202
|
+
- Iranti supports structured writes and ingest/chunking, but it is not primarily a "dump arbitrary conversations in and auto-magically derive perfect memory" system.
|
|
203
|
+
- That is a deliberate tradeoff in favor of explicit, inspectable facts, but it increases integration work.
|
|
204
|
+
|
|
205
|
+
### Current Position
|
|
206
|
+
|
|
207
|
+
Iranti is strongest today as infrastructure for developers building multi-agent systems who need shared, structured, queryable memory rather than pure semantic recall. The current evidence base is now more concrete than a positioning claim alone:
|
|
208
|
+
|
|
209
|
+
- `16/16` fictional-fact transfer in retrieval validation
|
|
210
|
+
- `7/16 (44%)` on an adversarial conflict benchmark
|
|
211
|
+
- `4/4` on empirical consistency validation for serialized writes and read visibility
|
|
212
|
+
|
|
213
|
+
That is not a claim that multi-agent memory is solved. It is a claim that Iranti now has reproducible evidence for three things at once:
|
|
214
|
+
|
|
215
|
+
- exact cross-agent fact transfer works
|
|
216
|
+
- same-key conflicting writes are serialized and observable
|
|
217
|
+
- conflict handling quality is measurable, including clearly documented failure modes
|
|
218
|
+
|
|
219
|
+
The next leverage is still product simplicity: setup, operations, and day-to-day inspection need to be simple enough that real users keep Iranti in the loop.
|
|
220
|
+
|
|
111
221
|
## Quickstart
|
|
112
222
|
|
|
113
|
-
**Requirements**: Node.js 18+,
|
|
223
|
+
**Requirements**: Node.js 18+, PostgreSQL, Python 3.8+
|
|
224
|
+
|
|
225
|
+
Docker is optional. It is one local way to run PostgreSQL if you do not already have a database.
|
|
114
226
|
|
|
115
227
|
```bash
|
|
116
228
|
# 1. Clone and configure
|
|
@@ -136,6 +248,9 @@ npm run api # Runs on port 3001
|
|
|
136
248
|
|
|
137
249
|
# 5. Install Python client
|
|
138
250
|
pip install iranti
|
|
251
|
+
|
|
252
|
+
# Optional: install the TypeScript client
|
|
253
|
+
npm install @iranti/sdk
|
|
139
254
|
```
|
|
140
255
|
|
|
141
256
|
### Archivist Scheduling Knobs
|
|
@@ -233,7 +348,11 @@ iranti install --scope user
|
|
|
233
348
|
`iranti setup` is the recommended first-run path. It walks through:
|
|
234
349
|
- shared vs isolated runtime setup
|
|
235
350
|
- instance creation or update
|
|
236
|
-
-
|
|
351
|
+
- API port selection with conflict detection and next-free suggestions
|
|
352
|
+
- database onboarding:
|
|
353
|
+
- existing Postgres
|
|
354
|
+
- managed Postgres
|
|
355
|
+
- optional Docker-hosted Postgres for local development
|
|
237
356
|
- provider API keys
|
|
238
357
|
- Iranti client API key generation
|
|
239
358
|
- one or more project bindings
|
|
@@ -242,8 +361,11 @@ iranti install --scope user
|
|
|
242
361
|
For automation:
|
|
243
362
|
- `iranti setup --defaults` uses sensible defaults plus environment/flag input, but still requires a real `DATABASE_URL`.
|
|
244
363
|
- `iranti setup --config <file>` reads a JSON setup plan for repeatable bootstrap.
|
|
364
|
+
- `--bootstrap-db` runs migrations and seeding during automated setup when the database is reachable.
|
|
245
365
|
- Example config: [docs/guides/iranti.setup.example.json](docs/guides/iranti.setup.example.json)
|
|
246
366
|
|
|
367
|
+
Default API port remains `3001`. The setup wizard now warns when that port is already in use and suggests the next free port instead of forcing users to debug the collision manually.
|
|
368
|
+
|
|
247
369
|
Defaults:
|
|
248
370
|
- Windows user scope: `%USERPROFILE%\\.iranti`
|
|
249
371
|
- Windows system scope: `%ProgramData%\\Iranti`
|
|
@@ -367,6 +489,51 @@ for fact in facts:
|
|
|
367
489
|
print(f"[{fact['key']}] {fact['summary']} (confidence: {fact['confidence']})")
|
|
368
490
|
```
|
|
369
491
|
|
|
492
|
+
### Graph Traversal
|
|
493
|
+
|
|
494
|
+
```python
|
|
495
|
+
from clients.python.iranti import IrantiClient
|
|
496
|
+
|
|
497
|
+
client = IrantiClient(base_url="http://localhost:3001", api_key="your_api_key_here")
|
|
498
|
+
|
|
499
|
+
# Agent 1 writes facts and links them into a graph.
|
|
500
|
+
client.write("researcher/jane_smith", "affiliation", {"lab": "CSAIL"}, "Jane Smith is affiliated with CSAIL", 90, "OpenAlex", "research_agent")
|
|
501
|
+
client.write("project/quantum_bridge", "status", {"phase": "active"}, "Quantum Bridge is active", 88, "project_brief", "research_agent")
|
|
502
|
+
|
|
503
|
+
client.relate("researcher/jane_smith", "MEMBER_OF", "lab/csail", created_by="research_agent")
|
|
504
|
+
client.relate("lab/csail", "LEADS", "project/quantum_bridge", created_by="research_agent")
|
|
505
|
+
|
|
506
|
+
# Agent 2 starts cold and traverses outward from Jane Smith.
|
|
507
|
+
one_hop = client.related("researcher/jane_smith")
|
|
508
|
+
labs = [f"{r['toType']}/{r['toId']}" for r in one_hop if r["relationshipType"] == "MEMBER_OF"]
|
|
509
|
+
|
|
510
|
+
projects = []
|
|
511
|
+
for lab in labs:
|
|
512
|
+
for rel in client.related(lab):
|
|
513
|
+
if rel["relationshipType"] == "LEADS":
|
|
514
|
+
project = f"{rel['toType']}/{rel['toId']}"
|
|
515
|
+
status = client.query(project, "status")
|
|
516
|
+
projects.append((project, status.value["phase"]))
|
|
517
|
+
|
|
518
|
+
print(projects)
|
|
519
|
+
# Agent 2 learned which project Jane Smith is connected to without being told the project directly.
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Relationship Types
|
|
523
|
+
|
|
524
|
+
Relationship types are caller-defined strings. Common conventions:
|
|
525
|
+
|
|
526
|
+
| Relationship Type | Meaning |
|
|
527
|
+
|---|---|
|
|
528
|
+
| `MEMBER_OF` | Entity belongs to a team, lab, org, or group |
|
|
529
|
+
| `PART_OF` | Entity is a component or sub-unit of another entity |
|
|
530
|
+
| `AUTHORED` | Person or agent created a document, paper, or artifact |
|
|
531
|
+
| `LEADS` | Person, team, or org leads a project or effort |
|
|
532
|
+
| `DEPENDS_ON` | Project, service, or task depends on another entity |
|
|
533
|
+
| `REPORTS_TO` | Directed reporting relationship between people or agents |
|
|
534
|
+
|
|
535
|
+
Use uppercase snake case for consistency. Iranti does not enforce a fixed ontology here; the calling application owns the relationship vocabulary.
|
|
536
|
+
|
|
370
537
|
### Hybrid Search
|
|
371
538
|
|
|
372
539
|
```python
|
|
@@ -519,7 +686,7 @@ Iranti has four internal components:
|
|
|
519
686
|
|
|
520
687
|
| Component | Role |
|
|
521
688
|
|---|---|
|
|
522
|
-
| **Library** | PostgreSQL knowledge base.
|
|
689
|
+
| **Library** | PostgreSQL knowledge base. Current truth lives in `knowledge_base`; closed and contested intervals live in `archive`. |
|
|
523
690
|
| **Librarian** | Manages all writes. Detects conflicts, reasons about resolution, escalates when uncertain. |
|
|
524
691
|
| **Attendant** | Per-agent working memory manager. Implements `attend()`, `observe()`, and `handshake()` APIs. |
|
|
525
692
|
| **Archivist** | Periodic cleanup. Archives expired and low-confidence entries. Processes human-resolved conflicts. |
|
|
@@ -529,7 +696,7 @@ Iranti has four internal components:
|
|
|
529
696
|
Express server on port 3001 with endpoints:
|
|
530
697
|
|
|
531
698
|
- `POST /kb/write` - Write atomic fact
|
|
532
|
-
- `POST /kb/ingest` - Ingest raw text, auto-chunk into facts
|
|
699
|
+
- `POST /kb/ingest` - Ingest raw text for one entity, auto-chunk into facts with per-fact confidence and per-fact write outcomes
|
|
533
700
|
- `GET /kb/query/:entityType/:entityId/:key` - Query specific fact
|
|
534
701
|
- `GET /kb/query/:entityType/:entityId` - Query all facts for entity
|
|
535
702
|
- `GET /kb/search` - Hybrid search across facts
|
|
@@ -549,8 +716,8 @@ All endpoints require `X-Iranti-Key` header for authentication.
|
|
|
549
716
|
Six PostgreSQL tables:
|
|
550
717
|
|
|
551
718
|
```
|
|
552
|
-
knowledge_base -
|
|
553
|
-
archive -
|
|
719
|
+
knowledge_base - current truth (one live row per entity/key)
|
|
720
|
+
archive - temporal and provenance history for superseded, contradicted, escalated, and expired rows
|
|
554
721
|
entity_relationships - directional graph: MEMBER_OF, PART_OF, AUTHORED, etc.
|
|
555
722
|
entities - canonical entity identity registry
|
|
556
723
|
entity_aliases - normalized aliases mapped to canonical entities
|
|
@@ -559,7 +726,7 @@ write_receipts - idempotency receipts for requestId replay safety
|
|
|
559
726
|
|
|
560
727
|
New entity types, relationship types, and fact keys do not require migrations; they are caller-defined strings.
|
|
561
728
|
|
|
562
|
-
**Archive semantics**: When
|
|
729
|
+
**Archive semantics**: When a current fact is superseded or contested, the current row is removed from `knowledge_base` and a closed historical interval is written to `archive`. Temporal queries use `validFrom` / `validUntil` plus archive metadata to answer point-in-time reads.
|
|
563
730
|
|
|
564
731
|
---
|
|
565
732
|
|
|
@@ -11,6 +11,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
11
11
|
const child_process_1 = require("child_process");
|
|
12
12
|
const promises_2 = __importDefault(require("readline/promises"));
|
|
13
13
|
const stream_1 = require("stream");
|
|
14
|
+
const net_1 = __importDefault(require("net"));
|
|
14
15
|
const client_1 = require("../src/library/client");
|
|
15
16
|
const apiKeys_1 = require("../src/security/apiKeys");
|
|
16
17
|
const PROVIDER_ENV_KEYS = {
|
|
@@ -135,29 +136,43 @@ function resolveInstallRoot(args, scope) {
|
|
|
135
136
|
return userRoot;
|
|
136
137
|
}
|
|
137
138
|
function getPackageVersion() {
|
|
139
|
+
const pkgPath = path_1.default.join(packageRoot(), 'package.json');
|
|
140
|
+
if (fs_1.default.existsSync(pkgPath)) {
|
|
141
|
+
try {
|
|
142
|
+
const raw = fs_1.default.readFileSync(pkgPath, 'utf-8');
|
|
143
|
+
const pkg = JSON.parse(raw);
|
|
144
|
+
return String(pkg.version ?? '0.0.0');
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return '0.0.0';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return '0.0.0';
|
|
151
|
+
}
|
|
152
|
+
function packageRoot() {
|
|
138
153
|
let dir = __dirname;
|
|
139
|
-
for (let i = 0; i <
|
|
154
|
+
for (let i = 0; i < 6; i++) {
|
|
140
155
|
const pkgPath = path_1.default.join(dir, 'package.json');
|
|
141
156
|
if (fs_1.default.existsSync(pkgPath)) {
|
|
142
|
-
|
|
143
|
-
const raw = fs_1.default.readFileSync(pkgPath, 'utf-8');
|
|
144
|
-
const pkg = JSON.parse(raw);
|
|
145
|
-
return String(pkg.version ?? '0.0.0');
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
return '0.0.0';
|
|
149
|
-
}
|
|
157
|
+
return dir;
|
|
150
158
|
}
|
|
151
159
|
const parent = path_1.default.dirname(dir);
|
|
152
160
|
if (parent === dir)
|
|
153
161
|
break;
|
|
154
162
|
dir = parent;
|
|
155
163
|
}
|
|
156
|
-
return
|
|
164
|
+
return process.cwd();
|
|
157
165
|
}
|
|
158
166
|
function builtScriptPath(scriptName) {
|
|
159
167
|
return path_1.default.resolve(__dirname, `${scriptName}.js`);
|
|
160
168
|
}
|
|
169
|
+
function formatSetupBootstrapFailure(error) {
|
|
170
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
171
|
+
return new Error(`Database bootstrap failed after instance configuration. ` +
|
|
172
|
+
`Common causes are a non-empty database that Prisma has not baselined yet, or a PostgreSQL server without the pgvector extension installed. ` +
|
|
173
|
+
`Re-run setup without --bootstrap-db, or point Iranti at a fresh pgvector-capable database. ` +
|
|
174
|
+
`Underlying error: ${reason}`);
|
|
175
|
+
}
|
|
161
176
|
async function handoffToScript(scriptName, rawArgs) {
|
|
162
177
|
const builtPath = builtScriptPath(scriptName);
|
|
163
178
|
if (fs_1.default.existsSync(builtPath)) {
|
|
@@ -203,6 +218,34 @@ async function handoffToScript(scriptName, rawArgs) {
|
|
|
203
218
|
});
|
|
204
219
|
});
|
|
205
220
|
}
|
|
221
|
+
async function runBundledScript(scriptName, rawArgs, extraEnv) {
|
|
222
|
+
const builtPath = builtScriptPath(scriptName);
|
|
223
|
+
if (!fs_1.default.existsSync(builtPath)) {
|
|
224
|
+
throw new Error(`Unable to locate bundled script: ${scriptName}`);
|
|
225
|
+
}
|
|
226
|
+
await new Promise((resolve, reject) => {
|
|
227
|
+
const child = (0, child_process_1.spawn)(process.execPath, [builtPath, ...rawArgs], {
|
|
228
|
+
stdio: 'inherit',
|
|
229
|
+
env: {
|
|
230
|
+
...process.env,
|
|
231
|
+
...extraEnv,
|
|
232
|
+
},
|
|
233
|
+
cwd: packageRoot(),
|
|
234
|
+
});
|
|
235
|
+
child.on('error', reject);
|
|
236
|
+
child.on('exit', (code, signal) => {
|
|
237
|
+
if (signal) {
|
|
238
|
+
reject(new Error(`${scriptName} terminated with signal ${signal}`));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if ((code ?? 0) !== 0) {
|
|
242
|
+
reject(new Error(`${scriptName} exited with code ${code ?? 1}`));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
resolve();
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
206
249
|
async function ensureDir(dir) {
|
|
207
250
|
await promises_1.default.mkdir(dir, { recursive: true });
|
|
208
251
|
}
|
|
@@ -634,6 +677,117 @@ function hasCodexInstalled() {
|
|
|
634
677
|
return false;
|
|
635
678
|
}
|
|
636
679
|
}
|
|
680
|
+
function hasDockerInstalled() {
|
|
681
|
+
try {
|
|
682
|
+
const proc = process.platform === 'win32'
|
|
683
|
+
? (0, child_process_1.spawnSync)(process.env.ComSpec ?? 'cmd.exe', ['/d', '/c', 'docker --version'], { stdio: 'ignore' })
|
|
684
|
+
: (0, child_process_1.spawnSync)('docker', ['--version'], { stdio: 'ignore' });
|
|
685
|
+
return proc.status === 0;
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
async function isPortAvailable(port, host = '127.0.0.1') {
|
|
692
|
+
return await new Promise((resolve) => {
|
|
693
|
+
const server = net_1.default.createServer();
|
|
694
|
+
server.unref();
|
|
695
|
+
server.on('error', () => resolve(false));
|
|
696
|
+
server.listen(port, host, () => {
|
|
697
|
+
server.close(() => resolve(true));
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
async function findNextAvailablePort(start, host = '127.0.0.1', maxSteps = 50) {
|
|
702
|
+
for (let port = start; port < start + maxSteps; port += 1) {
|
|
703
|
+
if (await isPortAvailable(port, host)) {
|
|
704
|
+
return port;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
throw new Error(`No available port found in range ${start}-${start + maxSteps - 1}.`);
|
|
708
|
+
}
|
|
709
|
+
async function chooseAvailablePort(session, promptText, preferredPort, allowOccupiedCurrent = false) {
|
|
710
|
+
let suggested = preferredPort;
|
|
711
|
+
if (!allowOccupiedCurrent && !(await isPortAvailable(preferredPort))) {
|
|
712
|
+
suggested = await findNextAvailablePort(preferredPort + 1);
|
|
713
|
+
console.log(`${warnLabel()} Port ${preferredPort} is already in use. Suggested port: ${suggested}`);
|
|
714
|
+
}
|
|
715
|
+
while (true) {
|
|
716
|
+
const raw = await promptNonEmpty(session, promptText, String(suggested));
|
|
717
|
+
const parsed = Number.parseInt(raw, 10);
|
|
718
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
719
|
+
console.log(`${warnLabel()} Port must be a positive integer.`);
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
if (allowOccupiedCurrent && parsed === preferredPort) {
|
|
723
|
+
return parsed;
|
|
724
|
+
}
|
|
725
|
+
if (await isPortAvailable(parsed)) {
|
|
726
|
+
return parsed;
|
|
727
|
+
}
|
|
728
|
+
const next = await findNextAvailablePort(parsed + 1);
|
|
729
|
+
console.log(`${warnLabel()} Port ${parsed} is already in use. Try ${next} instead.`);
|
|
730
|
+
suggested = next;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
async function waitForTcpPort(host, port, timeoutMs) {
|
|
734
|
+
const deadline = Date.now() + timeoutMs;
|
|
735
|
+
while (Date.now() < deadline) {
|
|
736
|
+
const ready = await new Promise((resolve) => {
|
|
737
|
+
const socket = net_1.default.connect({ host, port });
|
|
738
|
+
socket.once('connect', () => {
|
|
739
|
+
socket.destroy();
|
|
740
|
+
resolve(true);
|
|
741
|
+
});
|
|
742
|
+
socket.once('error', () => {
|
|
743
|
+
socket.destroy();
|
|
744
|
+
resolve(false);
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
if (ready)
|
|
748
|
+
return;
|
|
749
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
750
|
+
}
|
|
751
|
+
throw new Error(`Timed out waiting for ${host}:${port} to accept TCP connections.`);
|
|
752
|
+
}
|
|
753
|
+
async function runDockerPostgresContainer(options) {
|
|
754
|
+
const inspect = process.platform === 'win32'
|
|
755
|
+
? (0, child_process_1.spawnSync)(process.env.ComSpec ?? 'cmd.exe', ['/d', '/c', `docker ps -a --format "{{.Names}}"`], { encoding: 'utf8' })
|
|
756
|
+
: (0, child_process_1.spawnSync)('docker', ['ps', '-a', '--format', '{{.Names}}'], { encoding: 'utf8' });
|
|
757
|
+
const names = (inspect.stdout ?? '').split(/\r?\n/).map((value) => value.trim()).filter(Boolean);
|
|
758
|
+
if (names.includes(options.containerName)) {
|
|
759
|
+
const start = process.platform === 'win32'
|
|
760
|
+
? (0, child_process_1.spawnSync)(process.env.ComSpec ?? 'cmd.exe', ['/d', '/c', `docker start ${options.containerName}`], { stdio: 'inherit' })
|
|
761
|
+
: (0, child_process_1.spawnSync)('docker', ['start', options.containerName], { stdio: 'inherit' });
|
|
762
|
+
if (start.status !== 0) {
|
|
763
|
+
throw new Error(`Failed to start existing Docker container '${options.containerName}'.`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
const args = [
|
|
768
|
+
'run',
|
|
769
|
+
'-d',
|
|
770
|
+
'--name',
|
|
771
|
+
options.containerName,
|
|
772
|
+
'-e',
|
|
773
|
+
`POSTGRES_USER=postgres`,
|
|
774
|
+
'-e',
|
|
775
|
+
`POSTGRES_PASSWORD=${options.password}`,
|
|
776
|
+
'-e',
|
|
777
|
+
`POSTGRES_DB=${options.database}`,
|
|
778
|
+
'-p',
|
|
779
|
+
`${options.hostPort}:5432`,
|
|
780
|
+
'pgvector/pgvector:pg16',
|
|
781
|
+
];
|
|
782
|
+
const result = process.platform === 'win32'
|
|
783
|
+
? (0, child_process_1.spawnSync)(process.env.ComSpec ?? 'cmd.exe', ['/d', '/c', ['docker', ...args].join(' ')], { stdio: 'inherit' })
|
|
784
|
+
: (0, child_process_1.spawnSync)('docker', args, { stdio: 'inherit' });
|
|
785
|
+
if (result.status !== 0) {
|
|
786
|
+
throw new Error(`Failed to start Docker PostgreSQL container '${options.containerName}'.`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
await waitForTcpPort('127.0.0.1', options.hostPort, 30000);
|
|
790
|
+
}
|
|
637
791
|
async function executeSetupPlan(plan) {
|
|
638
792
|
await ensureRuntimeInstalled(plan.root, plan.scope);
|
|
639
793
|
const configured = await ensureInstanceConfigured(plan.root, plan.instanceName, {
|
|
@@ -643,6 +797,17 @@ async function executeSetupPlan(plan) {
|
|
|
643
797
|
providerKeys: plan.providerKeys,
|
|
644
798
|
apiKey: plan.apiKey,
|
|
645
799
|
});
|
|
800
|
+
if (plan.bootstrapDatabase) {
|
|
801
|
+
try {
|
|
802
|
+
await runBundledScript('setup', [], {
|
|
803
|
+
DATABASE_URL: plan.databaseUrl,
|
|
804
|
+
IRANTI_ESCALATION_DIR: path_1.default.join(configured.instanceDir, 'escalation'),
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
throw formatSetupBootstrapFailure(error);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
646
811
|
const bindings = [];
|
|
647
812
|
for (const project of plan.projects) {
|
|
648
813
|
const projectPath = path_1.default.resolve(project.path);
|
|
@@ -737,6 +902,7 @@ function parseSetupConfig(filePath) {
|
|
|
737
902
|
projects,
|
|
738
903
|
codex: Boolean(raw?.codex),
|
|
739
904
|
codexAgent: raw?.codexAgent ? sanitizeIdentifier(String(raw.codexAgent), 'codex_code') : undefined,
|
|
905
|
+
bootstrapDatabase: Boolean(raw?.bootstrapDatabase),
|
|
740
906
|
};
|
|
741
907
|
}
|
|
742
908
|
function defaultsSetupPlan(args) {
|
|
@@ -791,6 +957,7 @@ function defaultsSetupPlan(args) {
|
|
|
791
957
|
projects,
|
|
792
958
|
codex: hasFlag(args, 'codex'),
|
|
793
959
|
codexAgent: sanitizeIdentifier(getFlag(args, 'codex-agent') ?? 'codex_code', 'codex_code'),
|
|
960
|
+
bootstrapDatabase: hasFlag(args, 'bootstrap-db'),
|
|
794
961
|
};
|
|
795
962
|
}
|
|
796
963
|
function detectProviderKey(provider, env) {
|
|
@@ -1054,22 +1221,51 @@ async function setupCommand(args) {
|
|
|
1054
1221
|
else {
|
|
1055
1222
|
console.log(`${infoLabel()} Creating new instance '${instanceName}'.`);
|
|
1056
1223
|
}
|
|
1057
|
-
|
|
1224
|
+
const existingPort = Number.parseInt(existingInstance?.env.IRANTI_PORT ?? '3001', 10);
|
|
1225
|
+
const port = await chooseAvailablePort(prompt, 'Iranti API port', existingPort, Boolean(existingInstance));
|
|
1226
|
+
const dockerAvailable = hasDockerInstalled();
|
|
1227
|
+
let dbUrl = '';
|
|
1228
|
+
let bootstrapDatabase = false;
|
|
1058
1229
|
while (true) {
|
|
1059
|
-
const
|
|
1060
|
-
const
|
|
1061
|
-
if (
|
|
1062
|
-
|
|
1230
|
+
const defaultMode = dockerAvailable ? 'docker' : 'existing';
|
|
1231
|
+
const dbMode = (await prompt.line('Database setup mode: existing, managed, or docker', defaultMode) ?? defaultMode).trim().toLowerCase();
|
|
1232
|
+
if (dbMode === 'existing' || dbMode === 'managed') {
|
|
1233
|
+
while (true) {
|
|
1234
|
+
dbUrl = await promptNonEmpty(prompt, 'DATABASE_URL', existingInstance?.env.DATABASE_URL ?? `postgresql://postgres:yourpassword@localhost:5432/iranti_${instanceName}`);
|
|
1235
|
+
if (!detectPlaceholder(dbUrl))
|
|
1236
|
+
break;
|
|
1237
|
+
console.log(`${warnLabel()} DATABASE_URL still looks like a placeholder. Enter a real connection string before finishing setup.`);
|
|
1238
|
+
}
|
|
1239
|
+
bootstrapDatabase = await promptYesNo(prompt, 'Run migrations and seed the database now?', true);
|
|
1063
1240
|
break;
|
|
1064
1241
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1242
|
+
if (dbMode === 'docker') {
|
|
1243
|
+
if (!dockerAvailable) {
|
|
1244
|
+
console.log(`${warnLabel()} Docker is not installed or not on PATH. Choose existing or managed instead.`);
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
const dbHostPort = await chooseAvailablePort(prompt, 'Docker PostgreSQL host port', 5432, false);
|
|
1248
|
+
const dbName = sanitizeIdentifier(await promptNonEmpty(prompt, 'Docker PostgreSQL database name', `iranti_${instanceName}`), `iranti_${instanceName}`);
|
|
1249
|
+
const dbPassword = await promptRequiredSecret(prompt, 'Docker PostgreSQL password');
|
|
1250
|
+
const containerName = sanitizeIdentifier(await promptNonEmpty(prompt, 'Docker container name', `iranti_${instanceName}_db`), `iranti_${instanceName}_db`);
|
|
1251
|
+
dbUrl = `postgresql://postgres:${dbPassword}@localhost:${dbHostPort}/${dbName}`;
|
|
1252
|
+
console.log(`${infoLabel()} Docker will be used only for PostgreSQL. Iranti itself does not require Docker once a PostgreSQL database is available.`);
|
|
1253
|
+
if (await promptYesNo(prompt, `Start or reuse Docker container '${containerName}' now?`, true)) {
|
|
1254
|
+
await runDockerPostgresContainer({
|
|
1255
|
+
containerName,
|
|
1256
|
+
hostPort: dbHostPort,
|
|
1257
|
+
password: dbPassword,
|
|
1258
|
+
database: dbName,
|
|
1259
|
+
});
|
|
1260
|
+
console.log(`${okLabel()} Docker PostgreSQL ready at localhost:${dbHostPort}`);
|
|
1261
|
+
bootstrapDatabase = true;
|
|
1262
|
+
}
|
|
1263
|
+
else {
|
|
1264
|
+
bootstrapDatabase = await promptYesNo(prompt, 'Will you start PostgreSQL separately before first run?', false);
|
|
1265
|
+
}
|
|
1071
1266
|
break;
|
|
1072
|
-
|
|
1267
|
+
}
|
|
1268
|
+
console.log(`${warnLabel()} Choose one of: existing, managed, docker.`);
|
|
1073
1269
|
}
|
|
1074
1270
|
let provider = normalizeProvider(existingInstance?.env.LLM_PROVIDER ?? 'openai') ?? 'openai';
|
|
1075
1271
|
while (true) {
|
|
@@ -1160,6 +1356,7 @@ async function setupCommand(args) {
|
|
|
1160
1356
|
projects,
|
|
1161
1357
|
codex,
|
|
1162
1358
|
codexAgent: projects[0]?.agentId,
|
|
1359
|
+
bootstrapDatabase,
|
|
1163
1360
|
});
|
|
1164
1361
|
});
|
|
1165
1362
|
if (!result) {
|
|
@@ -1874,7 +2071,7 @@ function printHelp() {
|
|
|
1874
2071
|
|
|
1875
2072
|
Machine-level:
|
|
1876
2073
|
iranti install [--scope user|system] [--root <path>]
|
|
1877
|
-
iranti setup [--scope user|system] [--root <path>] [--config <file> | --defaults]
|
|
2074
|
+
iranti setup [--scope user|system] [--root <path>] [--config <file> | --defaults] [--db-url <url>] [--bootstrap-db]
|
|
1878
2075
|
|
|
1879
2076
|
Instance-level:
|
|
1880
2077
|
iranti instance create <name> [--port 3001] [--db-url <url>] [--api-key <token>] [--provider <name>] [--provider-key <token>] [--scope user|system]
|