iranti 0.3.10 → 0.3.12

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 CHANGED
@@ -1,19 +1,21 @@
1
1
  # Iranti
2
2
 
3
3
  [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](https://www.gnu.org/licenses/agpl-3.0.en.html)
4
+ [![MCP Server](https://img.shields.io/badge/MCP-server-purple.svg)](https://modelcontextprotocol.io)
5
+ [![npm](https://img.shields.io/badge/npm-iranti-red.svg)](https://www.npmjs.com/package/iranti)
4
6
  [![Python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
5
7
  [![TypeScript](https://img.shields.io/badge/typescript-5.0+-blue.svg)](https://www.typescriptlang.org/)
6
8
  [![CrewAI Compatible](https://img.shields.io/badge/CrewAI-compatible-green.svg)](https://www.crewai.com/)
7
9
 
8
- **Memory infrastructure for multi-agent AI systems.**
10
+ **Memory infrastructure for multi-agent AI systems — with a built-in MCP server.**
9
11
 
10
12
  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
13
 
12
- **Repo version:** `0.3.4`
14
+ **Repo version:** `0.3.11`
13
15
  Published packages:
14
- - npm `iranti@0.3.4`
15
- - npm `@iranti/sdk@0.3.4`
16
- - PyPI `iranti==0.3.4`
16
+ - npm `iranti@0.3.11`
17
+ - npm `@iranti/sdk@0.3.11`
18
+ - PyPI `iranti==0.3.11`
17
19
 
18
20
  ---
19
21
 
@@ -23,6 +25,32 @@ Iranti is a knowledge base for multi-agent systems. The primary read path is ide
23
25
 
24
26
  ---
25
27
 
28
+ ## MCP Server
29
+
30
+ Iranti ships a stdio MCP server compatible with Claude Code, GitHub Copilot, Codex, and any MCP-compliant client:
31
+
32
+ ```bash
33
+ # Fast path for Claude Code
34
+ iranti claude-setup
35
+
36
+ # Fast path for Codex
37
+ iranti codex-setup
38
+
39
+ # Fast path for GitHub Copilot
40
+ iranti copilot-setup
41
+
42
+ # Manual / other MCP clients
43
+ iranti mcp
44
+ ```
45
+
46
+ MCP tools exposed: `iranti_handshake`, `iranti_attend`, `iranti_write`, `iranti_query`, `iranti_search`, `iranti_checkpoint`, `iranti_ingest`, `iranti_relate`, `iranti_related`, `iranti_related_deep`, `iranti_history`, `iranti_who_knows`, `iranti_observe`, and more.
47
+
48
+ Full setup guides:
49
+ - [Claude Code](docs/guides/claude-code.md)
50
+ - [Codex](docs/guides/codex.md)
51
+
52
+ ---
53
+
26
54
  ## Runtime Roles
27
55
 
28
56
  - **User**: Person who interacts with an app or chatbot built on Iranti.
@@ -315,7 +315,9 @@ function emitHookContext(event, additionalContext) {
315
315
  additionalContext,
316
316
  },
317
317
  };
318
- process.stdout.write(`${JSON.stringify(payload)}\n`);
318
+ // Use synchronous write to fd 1 to avoid libuv UV_HANDLE_CLOSING assertion
319
+ // on Windows when Node exits while an async stdout write is still pending.
320
+ require('fs').writeSync(1, `${JSON.stringify(payload)}\n`);
319
321
  }
320
322
  function shouldFetchMemory(prompt) {
321
323
  const normalized = prompt.trim();
@@ -364,6 +364,147 @@ function writeWorkspaceVsCodeMcpFile(projectEnv, options) {
364
364
  }, null, 2)}\n`, 'utf8');
365
365
  return { filePath: mcpFile, status: 'updated' };
366
366
  }
367
+ /**
368
+ * Protocol reminder hook script content for Codex UserPromptSubmit.
369
+ * Same content as the Claude Code version — fires before every user prompt.
370
+ */
371
+ function buildProtocolReminderHookScript() {
372
+ return [
373
+ '#!/usr/bin/env node',
374
+ "'use strict';",
375
+ '// Iranti protocol reminder hook — fires on UserPromptSubmit for any Iranti project.',
376
+ '// Cross-platform: runs on Windows, macOS, Linux via Node.js.',
377
+ '// Exits cleanly with no output for non-Iranti projects.',
378
+ "const fs = require('fs');",
379
+ "const path = require('path');",
380
+ '',
381
+ "const envFile = path.join(process.cwd(), '.env.iranti');",
382
+ 'if (!fs.existsSync(envFile)) process.exit(0);',
383
+ '',
384
+ 'const content = [',
385
+ " 'IRANTI PROTOCOL (required this turn):',",
386
+ " '1. iranti_attend(phase=pre-response) BEFORE replying',",
387
+ " '2. iranti_attend BEFORE each Read / Grep / Glob / Bash / WebSearch / WebFetch',",
388
+ " '3. iranti_write AFTER each Edit or Write:',",
389
+ " ' entity: project/[id]/file/[filename] -- not the broad project entity',",
390
+ " ' value must include: absolutePath, lines, before, after, verify, why',",
391
+ " '4. iranti_write AFTER each Bash that reveals system state (build, errors, ports, env)',",
392
+ " '5. iranti_write AFTER each WebSearch/WebFetch -- write findings AND dead ends / 404s',",
393
+ " '6. iranti_attend(phase=post-response) AFTER every response without exception',",
394
+ "].join('\\n') + '\\n';",
395
+ "require('fs').writeSync(1, content);",
396
+ '',
397
+ ].join('\n');
398
+ }
399
+ /**
400
+ * Write the protocol-reminder hook script into the project's .codex/ directory
401
+ * and return a status result matching the workspace file pattern.
402
+ */
403
+ function writeProtocolReminderHook(projectEnv) {
404
+ const projectPath = node_path_1.default.dirname(projectEnv);
405
+ const codexDir = node_path_1.default.join(projectPath, '.codex');
406
+ const hookFile = node_path_1.default.join(codexDir, 'iranti-protocol-hook.js');
407
+ const hookContent = buildProtocolReminderHookScript();
408
+ node_fs_1.default.mkdirSync(codexDir, { recursive: true });
409
+ if (!node_fs_1.default.existsSync(hookFile)) {
410
+ node_fs_1.default.writeFileSync(hookFile, hookContent, 'utf8');
411
+ return { filePath: hookFile, status: 'created' };
412
+ }
413
+ const existing = node_fs_1.default.readFileSync(hookFile, 'utf8');
414
+ if (existing !== hookContent) {
415
+ node_fs_1.default.writeFileSync(hookFile, hookContent, 'utf8');
416
+ return { filePath: hookFile, status: 'updated' };
417
+ }
418
+ return { filePath: hookFile, status: 'unchanged' };
419
+ }
420
+ /**
421
+ * Write a .codex/hooks.json referencing the protocol-reminder hook.
422
+ * This fires on UserPromptSubmit when the codex_hooks feature is enabled.
423
+ */
424
+ function writeCodexHooksConfig(projectEnv) {
425
+ const projectPath = node_path_1.default.dirname(projectEnv);
426
+ const codexDir = node_path_1.default.join(projectPath, '.codex');
427
+ const hooksConfigFile = node_path_1.default.join(codexDir, 'hooks.json');
428
+ const hookScriptPath = node_path_1.default.join(codexDir, 'iranti-protocol-hook.js');
429
+ const hooksConfig = {
430
+ hooks: {
431
+ UserPromptSubmit: [
432
+ {
433
+ matcher: '',
434
+ hooks: [
435
+ {
436
+ type: 'command',
437
+ command: `node ${hookScriptPath.replace(/\\/g, '/')}`,
438
+ },
439
+ ],
440
+ },
441
+ ],
442
+ },
443
+ };
444
+ node_fs_1.default.mkdirSync(codexDir, { recursive: true });
445
+ const nextContent = `${JSON.stringify(hooksConfig, null, 2)}\n`;
446
+ if (!node_fs_1.default.existsSync(hooksConfigFile)) {
447
+ node_fs_1.default.writeFileSync(hooksConfigFile, nextContent, 'utf8');
448
+ return { filePath: hooksConfigFile, status: 'created' };
449
+ }
450
+ const existing = node_fs_1.default.readFileSync(hooksConfigFile, 'utf8');
451
+ if (existing === nextContent) {
452
+ return { filePath: hooksConfigFile, status: 'unchanged' };
453
+ }
454
+ // Merge: keep existing hooks, add/replace UserPromptSubmit from iranti.
455
+ try {
456
+ const parsed = JSON.parse(existing);
457
+ const existingHooks = parsed.hooks && typeof parsed.hooks === 'object' && !Array.isArray(parsed.hooks)
458
+ ? parsed.hooks
459
+ : {};
460
+ const merged = {
461
+ ...parsed,
462
+ hooks: {
463
+ ...existingHooks,
464
+ UserPromptSubmit: hooksConfig.hooks.UserPromptSubmit,
465
+ },
466
+ };
467
+ const mergedContent = `${JSON.stringify(merged, null, 2)}\n`;
468
+ if (mergedContent === existing) {
469
+ return { filePath: hooksConfigFile, status: 'unchanged' };
470
+ }
471
+ node_fs_1.default.writeFileSync(hooksConfigFile, mergedContent, 'utf8');
472
+ return { filePath: hooksConfigFile, status: 'updated' };
473
+ }
474
+ catch {
475
+ // Can't parse existing — overwrite
476
+ node_fs_1.default.writeFileSync(hooksConfigFile, nextContent, 'utf8');
477
+ return { filePath: hooksConfigFile, status: 'updated' };
478
+ }
479
+ }
480
+ /**
481
+ * Check if the codex_hooks feature is enabled globally.
482
+ */
483
+ function isCodexHooksFeatureEnabled(repoRoot) {
484
+ try {
485
+ const output = run('codex', ['features', 'list'], repoRoot);
486
+ const match = output.match(/codex_hooks\s+\S+\s+(true|false)/);
487
+ return match?.[1] === 'true';
488
+ }
489
+ catch {
490
+ return false;
491
+ }
492
+ }
493
+ /**
494
+ * Enable the codex_hooks feature flag if not already enabled.
495
+ */
496
+ function ensureCodexHooksFeature(repoRoot) {
497
+ if (isCodexHooksFeatureEnabled(repoRoot)) {
498
+ return true;
499
+ }
500
+ try {
501
+ run('codex', ['features', 'enable', 'codex_hooks'], repoRoot);
502
+ return true;
503
+ }
504
+ catch {
505
+ return false;
506
+ }
507
+ }
367
508
  function canUseInstalledIranti(repoRoot) {
368
509
  try {
369
510
  run('iranti', ['mcp', '--help'], repoRoot);
@@ -428,8 +569,12 @@ async function main() {
428
569
  vscode: writeWorkspaceVsCodeMcpFile(workspaceProjectEnv, options),
429
570
  agents: writeWorkspaceAgentsFile(workspaceProjectEnv),
430
571
  irantiMd: writeWorkspaceIrantiMdFile(workspaceProjectEnv),
572
+ protocolHook: writeProtocolReminderHook(workspaceProjectEnv),
573
+ hooksConfig: writeCodexHooksConfig(workspaceProjectEnv),
431
574
  }
432
575
  : null;
576
+ // Enable the codex_hooks feature flag so the UserPromptSubmit hook fires.
577
+ const hooksFeatureEnabled = ensureCodexHooksFeature(repoRoot);
433
578
  const registered = run('codex', ['mcp', 'get', options.name], repoRoot);
434
579
  console.log(registered);
435
580
  console.log('');
@@ -468,6 +613,9 @@ async function main() {
468
613
  console.log(`Workspace .vscode/mcp.json: ${workspaceFilesResult.vscode.status} (${workspaceFilesResult.vscode.filePath})`);
469
614
  console.log(`Workspace AGENTS.md: ${workspaceFilesResult.agents.status} (${workspaceFilesResult.agents.filePath})`);
470
615
  console.log(`Workspace IRANTI.md: ${workspaceFilesResult.irantiMd.status} (${workspaceFilesResult.irantiMd.filePath})`);
616
+ console.log(`Protocol hook: ${workspaceFilesResult.protocolHook.status} (${workspaceFilesResult.protocolHook.filePath})`);
617
+ console.log(`Hooks config: ${workspaceFilesResult.hooksConfig.status} (${workspaceFilesResult.hooksConfig.filePath})`);
618
+ console.log(`codex_hooks feature: ${hooksFeatureEnabled ? 'enabled' : 'not enabled (UserPromptSubmit hook requires codex_hooks feature)'}`);
471
619
  const closeout = await (0, scaffoldCloseout_1.writeProjectScaffoldCloseout)({
472
620
  tool: 'codex',
473
621
  projectPath: node_path_1.default.dirname(boundProjectEnv),
@@ -477,6 +625,8 @@ async function main() {
477
625
  { path: workspaceFilesResult.vscode.filePath, status: workspaceFilesResult.vscode.status },
478
626
  { path: workspaceFilesResult.agents.filePath, status: workspaceFilesResult.agents.status },
479
627
  { path: workspaceFilesResult.irantiMd.filePath, status: workspaceFilesResult.irantiMd.status },
628
+ { path: workspaceFilesResult.protocolHook.filePath, status: workspaceFilesResult.protocolHook.status },
629
+ { path: workspaceFilesResult.hooksConfig.filePath, status: workspaceFilesResult.hooksConfig.status },
480
630
  ],
481
631
  agentId: options.agent || 'codex_code',
482
632
  });