iranti 0.2.5 → 0.2.6

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
@@ -305,6 +305,12 @@ iranti mcp
305
305
 
306
306
  Use it with a project-local `.mcp.json`, and optionally add `iranti claude-hook` for `SessionStart` and `UserPromptSubmit`.
307
307
 
308
+ Fast path:
309
+
310
+ ```bash
311
+ iranti claude-setup
312
+ ```
313
+
308
314
  Guide: [`docs/guides/claude-code.md`](docs/guides/claude-code.md)
309
315
 
310
316
  ### Codex via MCP
@@ -318,6 +324,12 @@ codex -C /path/to/your/project
318
324
 
319
325
  When `iranti codex-setup` is run from a project directory, it automatically captures that project's `.env.iranti` as `IRANTI_PROJECT_ENV` so Codex resolves the correct Iranti instance consistently.
320
326
 
327
+ Alias:
328
+
329
+ ```bash
330
+ iranti integrate codex
331
+ ```
332
+
321
333
  Guide: [`docs/guides/codex.md`](docs/guides/codex.md)
322
334
 
323
335
  ### Resolve Pending Escalations
@@ -153,11 +153,19 @@ function canUseInstalledIranti(repoRoot) {
153
153
  return false;
154
154
  }
155
155
  }
156
+ function ensureCodexInstalled(repoRoot) {
157
+ try {
158
+ run('codex', ['--version'], repoRoot);
159
+ }
160
+ catch {
161
+ throw new Error('Codex CLI is not installed or not on PATH. Install Codex first, confirm `codex --version` works, then rerun `iranti codex-setup`.');
162
+ }
163
+ }
156
164
  function main() {
157
165
  const options = parseArgs(process.argv.slice(2));
158
166
  const repoRoot = findPackageRoot(__dirname);
159
167
  const mcpScript = node_path_1.default.join(repoRoot, 'dist', 'scripts', 'iranti-mcp.js');
160
- run('codex', ['--version'], repoRoot);
168
+ ensureCodexInstalled(repoRoot);
161
169
  const useInstalled = !options.useLocalScript && canUseInstalledIranti(repoRoot);
162
170
  if (!useInstalled && !node_fs_1.default.existsSync(mcpScript)) {
163
171
  throw new Error(`Missing build artifact: ${mcpScript}. Run "npm run build" first, or install iranti globally and rerun without --local-script.`);
@@ -644,39 +644,95 @@ async function ensureInstanceConfigured(root, name, config) {
644
644
  });
645
645
  return { envFile, instanceDir, created };
646
646
  }
647
- async function writeClaudeCodeProjectFiles(projectPath) {
647
+ function makeIrantiMcpServerConfig() {
648
+ return {
649
+ command: 'iranti',
650
+ args: ['mcp'],
651
+ };
652
+ }
653
+ function makeClaudeHookSettings(projectEnvPath) {
654
+ const hookArgs = (event) => {
655
+ const args = ['claude-hook', '--event', event];
656
+ if (projectEnvPath) {
657
+ args.push('--project-env', projectEnvPath);
658
+ }
659
+ return args;
660
+ };
661
+ return {
662
+ hooks: {
663
+ SessionStart: [
664
+ {
665
+ command: 'iranti',
666
+ args: hookArgs('SessionStart'),
667
+ },
668
+ ],
669
+ UserPromptSubmit: [
670
+ {
671
+ command: 'iranti',
672
+ args: hookArgs('UserPromptSubmit'),
673
+ },
674
+ ],
675
+ },
676
+ };
677
+ }
678
+ async function writeClaudeCodeProjectFiles(projectPath, projectEnvPath, force = false) {
648
679
  const mcpFile = path_1.default.join(projectPath, '.mcp.json');
680
+ let mcpStatus = 'unchanged';
681
+ const irantiMcpServer = makeIrantiMcpServerConfig();
649
682
  if (!fs_1.default.existsSync(mcpFile)) {
650
683
  await writeText(mcpFile, `${JSON.stringify({
651
684
  mcpServers: {
652
- iranti: {
653
- command: 'iranti',
654
- args: ['mcp'],
655
- },
685
+ iranti: irantiMcpServer,
656
686
  },
657
687
  }, null, 2)}\n`);
688
+ mcpStatus = 'created';
689
+ }
690
+ else {
691
+ const existing = readJsonFile(mcpFile);
692
+ if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
693
+ if (!force) {
694
+ throw new Error(`Existing .mcp.json is not valid JSON. Re-run with --force to overwrite it: ${mcpFile}`);
695
+ }
696
+ await writeText(mcpFile, `${JSON.stringify({
697
+ mcpServers: {
698
+ iranti: irantiMcpServer,
699
+ },
700
+ }, null, 2)}\n`);
701
+ mcpStatus = 'updated';
702
+ }
703
+ else {
704
+ const existingServers = existing.mcpServers && typeof existing.mcpServers === 'object' && !Array.isArray(existing.mcpServers)
705
+ ? existing.mcpServers
706
+ : {};
707
+ const hasIranti = Object.prototype.hasOwnProperty.call(existingServers, 'iranti');
708
+ if (!hasIranti || force) {
709
+ await writeText(mcpFile, `${JSON.stringify({
710
+ ...existing,
711
+ mcpServers: {
712
+ ...existingServers,
713
+ iranti: irantiMcpServer,
714
+ },
715
+ }, null, 2)}\n`);
716
+ mcpStatus = 'updated';
717
+ }
718
+ }
658
719
  }
659
720
  const claudeDir = path_1.default.join(projectPath, '.claude');
660
721
  await ensureDir(claudeDir);
661
722
  const settingsFile = path_1.default.join(claudeDir, 'settings.local.json');
723
+ let settingsStatus = 'unchanged';
662
724
  if (!fs_1.default.existsSync(settingsFile)) {
663
- await writeText(settingsFile, `${JSON.stringify({
664
- hooks: {
665
- SessionStart: [
666
- {
667
- command: 'iranti',
668
- args: ['claude-hook', '--event', 'SessionStart'],
669
- },
670
- ],
671
- UserPromptSubmit: [
672
- {
673
- command: 'iranti',
674
- args: ['claude-hook', '--event', 'UserPromptSubmit'],
675
- },
676
- ],
677
- },
678
- }, null, 2)}\n`);
725
+ await writeText(settingsFile, `${JSON.stringify(makeClaudeHookSettings(projectEnvPath), null, 2)}\n`);
726
+ settingsStatus = 'created';
679
727
  }
728
+ else if (force) {
729
+ await writeText(settingsFile, `${JSON.stringify(makeClaudeHookSettings(projectEnvPath), null, 2)}\n`);
730
+ settingsStatus = 'updated';
731
+ }
732
+ return {
733
+ mcp: mcpStatus,
734
+ settings: settingsStatus,
735
+ };
680
736
  }
681
737
  function hasCodexInstalled() {
682
738
  try {
@@ -1384,16 +1440,16 @@ function describeUpgradeTarget(target) {
1384
1440
  const latest = target.latestVersion ?? 'unknown';
1385
1441
  if (target.target === 'npm-repo') {
1386
1442
  return target.blockedReason
1387
- ? `repo checkout (${current}) ${target.blockedReason}`
1388
- : `repo checkout (${current}) refresh local checkout and rebuild`;
1443
+ ? `repo checkout (${current}) — ${target.blockedReason}`
1444
+ : `repo checkout (${current}) — refresh local checkout and rebuild`;
1389
1445
  }
1390
1446
  if (target.upToDate === true) {
1391
- return `${target.target} (${current}) already at latest ${latest}`;
1447
+ return `${target.target} (${current}) — already at latest ${latest}`;
1392
1448
  }
1393
1449
  if (target.blockedReason) {
1394
- return `${target.target} (${current}) ${target.blockedReason}`;
1450
+ return `${target.target} (${current}) — ${target.blockedReason}`;
1395
1451
  }
1396
- return `${target.target} (${current}) latest ${latest}`;
1452
+ return `${target.target} (${current}) — latest ${latest}`;
1397
1453
  }
1398
1454
  async function chooseInteractiveUpgradeTargets(statuses) {
1399
1455
  const selected = [];
@@ -2063,7 +2119,7 @@ async function doctorCommand(args) {
2063
2119
  : check.status === 'warn'
2064
2120
  ? warnLabel('WARN')
2065
2121
  : failLabel('FAIL');
2066
- console.log(`${marker} ${check.name} ${check.detail}`);
2122
+ console.log(`${marker} ${check.name} — ${check.detail}`);
2067
2123
  }
2068
2124
  if (result.remediations.length > 0) {
2069
2125
  console.log('');
@@ -2736,6 +2792,151 @@ async function resolveCommand(args) {
2736
2792
  const escalationDir = explicitDir ? path_1.default.resolve(explicitDir) : (0, escalationPaths_1.getEscalationPaths)().root;
2737
2793
  await (0, resolutionist_1.resolveInteractive)(escalationDir);
2738
2794
  }
2795
+ function printClaudeSetupHelp() {
2796
+ console.log([
2797
+ 'Scaffold Claude Code MCP and hook files for the current project.',
2798
+ '',
2799
+ 'Usage:',
2800
+ ' iranti claude-setup [path] [--project-env <path>] [--force]',
2801
+ ' iranti claude-setup --scan <dir> [--recursive] [--force]',
2802
+ ' iranti integrate claude [path] [--project-env <path>] [--force]',
2803
+ ' iranti integrate claude --scan <dir> [--recursive] [--force]',
2804
+ '',
2805
+ 'Notes:',
2806
+ ' - Expects a project binding at .env.iranti unless --project-env is supplied.',
2807
+ ' - Writes .mcp.json and .claude/settings.local.json.',
2808
+ ' - Adds the Iranti MCP server to existing .mcp.json files without removing other servers.',
2809
+ ' - Leaves existing Claude hook files untouched unless --force is supplied.',
2810
+ '',
2811
+ 'Scan mode (--scan):',
2812
+ ' - Scans immediate subdirectories of the given dir by default.',
2813
+ ' - Add --recursive to scan nested project trees too.',
2814
+ ' - Only scaffolds projects that already have a .claude subfolder.',
2815
+ ' - No .env.iranti required - skips the per-project binding check.',
2816
+ ' - Scan mode adds or merges .mcp.json and only creates hook settings when missing.',
2817
+ ].join('\n'));
2818
+ }
2819
+ function shouldSkipRecursiveClaudeScanDir(name) {
2820
+ if (name.startsWith('.'))
2821
+ return true;
2822
+ return [
2823
+ 'node_modules',
2824
+ 'dist',
2825
+ 'build',
2826
+ 'out',
2827
+ 'coverage',
2828
+ '.next',
2829
+ '.turbo',
2830
+ '.cache',
2831
+ ].includes(name);
2832
+ }
2833
+ function findClaudeProjects(scanDir, recursive) {
2834
+ if (!recursive) {
2835
+ return fs_1.default.readdirSync(scanDir, { withFileTypes: true })
2836
+ .filter((entry) => entry.isDirectory())
2837
+ .map((entry) => path_1.default.join(scanDir, entry.name))
2838
+ .filter((candidate) => fs_1.default.existsSync(path_1.default.join(candidate, '.claude')));
2839
+ }
2840
+ const found = new Set();
2841
+ const queue = [scanDir];
2842
+ while (queue.length > 0) {
2843
+ const current = queue.shift();
2844
+ let entries = [];
2845
+ try {
2846
+ entries = fs_1.default.readdirSync(current, { withFileTypes: true });
2847
+ }
2848
+ catch {
2849
+ continue;
2850
+ }
2851
+ if (fs_1.default.existsSync(path_1.default.join(current, '.claude'))) {
2852
+ found.add(current);
2853
+ }
2854
+ for (const entry of entries) {
2855
+ if (!entry.isDirectory())
2856
+ continue;
2857
+ if (shouldSkipRecursiveClaudeScanDir(entry.name))
2858
+ continue;
2859
+ queue.push(path_1.default.join(current, entry.name));
2860
+ }
2861
+ }
2862
+ found.delete(scanDir);
2863
+ return Array.from(found).sort((a, b) => a.localeCompare(b));
2864
+ }
2865
+ async function claudeSetupCommand(args) {
2866
+ if (hasFlag(args, 'help')) {
2867
+ printClaudeSetupHelp();
2868
+ return;
2869
+ }
2870
+ const force = hasFlag(args, 'force');
2871
+ if (args.flags.has('scan')) {
2872
+ const recursive = hasFlag(args, 'recursive');
2873
+ const dirArg = getFlag(args, 'scan')
2874
+ ?? args.positionals[0]
2875
+ ?? (args.command === 'claude-setup' ? args.subcommand ?? undefined : undefined);
2876
+ const scanDir = path_1.default.resolve(dirArg ?? process.cwd());
2877
+ if (!fs_1.default.existsSync(scanDir)) {
2878
+ throw new Error(`Scan directory not found: ${scanDir}`);
2879
+ }
2880
+ const candidates = findClaudeProjects(scanDir, recursive);
2881
+ if (candidates.length === 0) {
2882
+ console.log(`${infoLabel()} No ${recursive ? 'nested project directories' : 'subdirectories'} with a .claude folder found in ${scanDir}`);
2883
+ return;
2884
+ }
2885
+ console.log(`${okLabel()} Scanning ${scanDir} - found ${candidates.length} project(s) with .claude${recursive ? ' (recursive)' : ''}`);
2886
+ let createdMcp = 0;
2887
+ let updatedMcp = 0;
2888
+ let createdSettings = 0;
2889
+ let updatedSettings = 0;
2890
+ let unchanged = 0;
2891
+ for (const projectPath of candidates) {
2892
+ const result = await writeClaudeCodeProjectFiles(projectPath, undefined, force);
2893
+ if (result.mcp === 'created')
2894
+ createdMcp += 1;
2895
+ if (result.mcp === 'updated')
2896
+ updatedMcp += 1;
2897
+ if (result.settings === 'created')
2898
+ createdSettings += 1;
2899
+ if (result.settings === 'updated')
2900
+ updatedSettings += 1;
2901
+ if (result.mcp === 'unchanged' && result.settings === 'unchanged')
2902
+ unchanged += 1;
2903
+ console.log(` ${projectPath}`);
2904
+ console.log(` mcp ${result.mcp}`);
2905
+ console.log(` settings ${result.settings}`);
2906
+ }
2907
+ console.log('');
2908
+ console.log('Summary:');
2909
+ console.log(` projects ${candidates.length}`);
2910
+ console.log(` mcp created ${createdMcp}`);
2911
+ console.log(` mcp updated ${updatedMcp}`);
2912
+ console.log(` settings created ${createdSettings}`);
2913
+ console.log(` settings updated ${updatedSettings}`);
2914
+ console.log(` unchanged ${unchanged}`);
2915
+ console.log(`${infoLabel()} Done. Open each project in Claude Code to verify Iranti tools are available.`);
2916
+ return;
2917
+ }
2918
+ const projectArg = args.positionals[0] ?? (args.command === 'claude-setup' ? args.subcommand ?? undefined : undefined);
2919
+ const projectPath = path_1.default.resolve(projectArg ?? process.cwd());
2920
+ const explicitProjectEnv = getFlag(args, 'project-env');
2921
+ const projectEnvPath = explicitProjectEnv
2922
+ ? path_1.default.resolve(explicitProjectEnv)
2923
+ : path_1.default.join(projectPath, '.env.iranti');
2924
+ if (!fs_1.default.existsSync(projectPath)) {
2925
+ throw new Error(`Project path not found: ${projectPath}`);
2926
+ }
2927
+ if (!fs_1.default.existsSync(projectEnvPath)) {
2928
+ throw new Error(`Project binding not found at ${projectEnvPath}. Run \`iranti project init\` or \`iranti configure project\` first.`);
2929
+ }
2930
+ const result = await writeClaudeCodeProjectFiles(projectPath, projectEnvPath, force);
2931
+ console.log(`${okLabel()} Claude Code integration scaffolded`);
2932
+ console.log(` project ${projectPath}`);
2933
+ console.log(` binding ${projectEnvPath}`);
2934
+ console.log(` mcp ${path_1.default.join(projectPath, '.mcp.json')}`);
2935
+ console.log(` settings ${path_1.default.join(projectPath, '.claude', 'settings.local.json')}`);
2936
+ console.log(` mcp status ${result.mcp}`);
2937
+ console.log(` settings status ${result.settings}`);
2938
+ console.log(`${infoLabel()} Next: open Claude Code in this project and verify Iranti tools are available.`);
2939
+ }
2739
2940
  async function chatCommand(args) {
2740
2941
  const provider = normalizeProvider(getFlag(args, 'provider'));
2741
2942
  if (provider && !isSupportedProvider(provider)) {
@@ -2788,8 +2989,13 @@ Project-level:
2788
2989
 
2789
2990
  Integrations:
2790
2991
  iranti mcp [--help]
2992
+ iranti claude-setup [path] [--project-env <path>] [--force]
2993
+ iranti claude-setup --scan <dir> [--force]
2791
2994
  iranti claude-hook --event SessionStart|UserPromptSubmit [--project-env <path>] [--instance-env <path>] [--env-file <path>]
2792
2995
  iranti codex-setup [--name iranti] [--agent codex_code] [--source Codex] [--provider openai] [--project-env <path>] [--local-script]
2996
+ iranti integrate claude [path] [--project-env <path>] [--force]
2997
+ iranti integrate claude --scan <dir> [--force]
2998
+ iranti integrate codex [--name iranti] [--agent codex_code] [--source Codex] [--provider openai] [--project-env <path>] [--local-script]
2793
2999
  `);
2794
3000
  }
2795
3001
  async function main() {
@@ -2895,6 +3101,10 @@ async function main() {
2895
3101
  await handoffToScript('iranti-mcp', process.argv.slice(3));
2896
3102
  return;
2897
3103
  }
3104
+ if (args.command === 'claude-setup') {
3105
+ await claudeSetupCommand(args);
3106
+ return;
3107
+ }
2898
3108
  if (args.command === 'claude-hook') {
2899
3109
  await handoffToScript('claude-code-memory-hook', process.argv.slice(3));
2900
3110
  return;
@@ -2903,6 +3113,17 @@ async function main() {
2903
3113
  await handoffToScript('codex-setup', process.argv.slice(3));
2904
3114
  return;
2905
3115
  }
3116
+ if (args.command === 'integrate') {
3117
+ if (args.subcommand === 'claude') {
3118
+ await claudeSetupCommand(args);
3119
+ return;
3120
+ }
3121
+ if (args.subcommand === 'codex') {
3122
+ await handoffToScript('codex-setup', process.argv.slice(4));
3123
+ return;
3124
+ }
3125
+ throw new Error(`Unknown integrate target '${args.subcommand ?? ''}'. Use 'claude' or 'codex'.`);
3126
+ }
2906
3127
  throw new Error(`Unknown command '${args.command}'. Run: iranti help`);
2907
3128
  }
2908
3129
  main().catch((err) => {
@@ -144,7 +144,7 @@ async function main() {
144
144
  await ensureDefaultAgent(iranti);
145
145
  const server = new mcp_js_1.McpServer({
146
146
  name: 'iranti-mcp',
147
- version: '0.2.5',
147
+ version: '0.2.6',
148
148
  });
149
149
  server.registerTool('iranti_handshake', {
150
150
  description: `Initialize or refresh an agent's working-memory brief for the current task.
@@ -15,7 +15,7 @@ const STAFF_ENTRIES = [
15
15
  entityId: 'librarian',
16
16
  key: 'operating_rules',
17
17
  valueRaw: {
18
- version: '0.2.5',
18
+ version: '0.2.6',
19
19
  rules: [
20
20
  'All writes from external agents go through the Librarian — never directly to the database',
21
21
  'Check for existing entries before every write',
@@ -39,7 +39,7 @@ const STAFF_ENTRIES = [
39
39
  entityId: 'attendant',
40
40
  key: 'operating_rules',
41
41
  valueRaw: {
42
- version: '0.2.5',
42
+ version: '0.2.6',
43
43
  rules: [
44
44
  'Assigned one-per-external-agent — serve the agent, not the user',
45
45
  'On handshake: read AGENTS.md and MCP config, query Librarian for relevant rules and task context',
@@ -61,7 +61,7 @@ const STAFF_ENTRIES = [
61
61
  entityId: 'archivist',
62
62
  key: 'operating_rules',
63
63
  valueRaw: {
64
- version: '0.2.5',
64
+ version: '0.2.6',
65
65
  rules: [
66
66
  'Run on schedule or when conflict flags exceed threshold — not on every write',
67
67
  'Scan for expired, low-confidence, flagged, and duplicate entries',
@@ -82,7 +82,7 @@ const STAFF_ENTRIES = [
82
82
  entityType: 'system',
83
83
  entityId: 'library',
84
84
  key: 'schema_version',
85
- valueRaw: { version: '0.2.5' },
85
+ valueRaw: { version: '0.2.6' },
86
86
  valueSummary: 'Current Library schema version.',
87
87
  confidence: 100,
88
88
  source: 'seed',
@@ -95,7 +95,7 @@ const STAFF_ENTRIES = [
95
95
  key: 'initialization_log',
96
96
  valueRaw: {
97
97
  initializedAt: new Date().toISOString(),
98
- seedVersion: '0.2.5',
98
+ seedVersion: '0.2.6',
99
99
  },
100
100
  valueSummary: 'Record of when and how this Library was initialized.',
101
101
  confidence: 100,
@@ -108,7 +108,7 @@ const STAFF_ENTRIES = [
108
108
  entityId: 'ontology',
109
109
  key: 'core_schema',
110
110
  valueRaw: {
111
- version: '0.2.5',
111
+ version: '0.2.6',
112
112
  states: ['candidate', 'provisional', 'canonical'],
113
113
  coreEntityTypes: [
114
114
  'person',
@@ -156,7 +156,7 @@ const STAFF_ENTRIES = [
156
156
  entityId: 'ontology',
157
157
  key: 'extension_registry',
158
158
  valueRaw: {
159
- version: '0.2.5',
159
+ version: '0.2.6',
160
160
  namespaces: {
161
161
  education: {
162
162
  status: 'provisional',
@@ -187,7 +187,7 @@ const STAFF_ENTRIES = [
187
187
  entityId: 'ontology',
188
188
  key: 'candidate_terms',
189
189
  valueRaw: {
190
- version: '0.2.5',
190
+ version: '0.2.6',
191
191
  terms: [],
192
192
  },
193
193
  valueSummary: 'Staging area for ontology terms detected repeatedly but not yet promoted.',
@@ -201,7 +201,7 @@ const STAFF_ENTRIES = [
201
201
  entityId: 'ontology',
202
202
  key: 'promotion_policy',
203
203
  valueRaw: {
204
- version: '0.2.5',
204
+ version: '0.2.6',
205
205
  candidateToProvisional: {
206
206
  minSeenCount: 3,
207
207
  minDistinctAgents: 2,
@@ -237,7 +237,7 @@ const STAFF_ENTRIES = [
237
237
  entityId: 'ontology',
238
238
  key: 'change_log',
239
239
  valueRaw: {
240
- version: '0.2.5',
240
+ version: '0.2.6',
241
241
  events: [
242
242
  {
243
243
  at: new Date().toISOString(),
@@ -69,7 +69,7 @@ app.use(express_1.default.json({ limit: process.env.IRANTI_MAX_BODY_BYTES ?? '25
69
69
  app.get(ROUTES.health, (_req, res) => {
70
70
  res.json({
71
71
  status: 'ok',
72
- version: '0.2.5',
72
+ version: '0.2.6',
73
73
  provider: process.env.LLM_PROVIDER ?? 'mock',
74
74
  });
75
75
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iranti",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Memory infrastructure for multi-agent AI systems",
5
5
  "main": "dist/src/sdk/index.js",
6
6
  "files": [