iranti 0.2.4 → 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 {
@@ -1015,6 +1071,53 @@ function summarizeStatus(checks) {
1015
1071
  return 'warn';
1016
1072
  return 'pass';
1017
1073
  }
1074
+ function collectDoctorRemediations(checks, envSource, envFile) {
1075
+ const hints = [];
1076
+ const add = (hint) => {
1077
+ if (!hints.includes(hint))
1078
+ hints.push(hint);
1079
+ };
1080
+ for (const check of checks) {
1081
+ if (check.name === 'node version' && check.status === 'fail') {
1082
+ add('Upgrade Node.js to version 18 or newer, then rerun `iranti doctor`.');
1083
+ }
1084
+ if (check.name === 'cli build artifact' && check.status !== 'pass') {
1085
+ add('If this is a repo checkout, run `npm run build`. If this is an installed CLI, reinstall it with `npm install -g iranti@latest`.');
1086
+ }
1087
+ if (check.name === 'environment file' && check.status === 'fail') {
1088
+ if (envFile) {
1089
+ add(`Fix or recreate the target env file at ${envFile}, or rerun \`iranti setup\`.`);
1090
+ }
1091
+ else {
1092
+ add('Run `iranti setup`, or rerun `iranti doctor` with `--instance <name>` or `--env <file>`.');
1093
+ }
1094
+ }
1095
+ if (check.name === 'database configuration' && check.status === 'fail') {
1096
+ add(`Set a real DATABASE_URL in ${envFile ?? 'the target env file'}, or rerun \`iranti setup\` to configure the database again.`);
1097
+ }
1098
+ if (check.name === 'project binding url' && check.status === 'fail') {
1099
+ add('Run `iranti configure project` to refresh the project binding, or set IRANTI_URL in `.env.iranti`.');
1100
+ }
1101
+ if (check.name === 'project api key' && check.status === 'fail') {
1102
+ add('Run `iranti configure project` or set IRANTI_API_KEY in `.env.iranti`.');
1103
+ }
1104
+ if (check.name === 'api key' && check.status !== 'pass') {
1105
+ add(envSource === 'project-binding'
1106
+ ? 'Set IRANTI_API_KEY in the project binding, or rerun `iranti configure project`.'
1107
+ : 'Create or rotate an Iranti key with `iranti auth create-key`, then store it in the target env.');
1108
+ }
1109
+ if (check.name === 'provider credentials' && check.status === 'fail') {
1110
+ add('Store or refresh the upstream provider key with `iranti add api-key` or `iranti update api-key`.');
1111
+ }
1112
+ if (check.name === 'vector backend' && check.status === 'fail') {
1113
+ add('Check the vector backend env vars, or switch back to `IRANTI_VECTOR_BACKEND=pgvector` if the external backend is not ready.');
1114
+ }
1115
+ }
1116
+ if (checks.some((check) => check.status !== 'pass')) {
1117
+ add('Use `iranti upgrade --all --dry-run` to see whether this machine has stale CLI or Python installs.');
1118
+ }
1119
+ return hints;
1120
+ }
1018
1121
  function resolveDoctorEnvTarget(args) {
1019
1122
  const scope = normalizeScope(getFlag(args, 'scope'));
1020
1123
  const instanceName = getFlag(args, 'instance');
@@ -1139,6 +1242,32 @@ function detectGlobalNpmRoot() {
1139
1242
  const value = proc.stdout.trim();
1140
1243
  return value ? path_1.default.resolve(value) : null;
1141
1244
  }
1245
+ function detectGlobalNpmInstalledVersion() {
1246
+ const proc = runCommandCapture('npm', ['list', '-g', 'iranti', '--depth=0', '--json']);
1247
+ if (proc.status !== 0)
1248
+ return null;
1249
+ try {
1250
+ const payload = JSON.parse(proc.stdout);
1251
+ return typeof payload?.dependencies?.iranti?.version === 'string'
1252
+ ? payload.dependencies.iranti.version
1253
+ : null;
1254
+ }
1255
+ catch {
1256
+ return null;
1257
+ }
1258
+ }
1259
+ function detectPythonInstalledVersion(command) {
1260
+ if (!command)
1261
+ return null;
1262
+ const args = command.executable === 'py' ? ['-3', '-m', 'pip', 'show', 'iranti'] : ['-m', 'pip', 'show', 'iranti'];
1263
+ const proc = runCommandCapture(command.executable, args);
1264
+ if (proc.status !== 0)
1265
+ return null;
1266
+ const versionLine = proc.stdout.split(/\r?\n/).find((line) => line.toLowerCase().startsWith('version:'));
1267
+ if (!versionLine)
1268
+ return null;
1269
+ return versionLine.split(':').slice(1).join(':').trim() || null;
1270
+ }
1142
1271
  function readJsonFile(filePath) {
1143
1272
  if (!fs_1.default.existsSync(filePath))
1144
1273
  return null;
@@ -1219,9 +1348,12 @@ function detectUpgradeContext(args) {
1219
1348
  const runtimeRoot = resolveInstallRoot(args, scope);
1220
1349
  const runtimeInstalled = fs_1.default.existsSync(path_1.default.join(runtimeRoot, 'install.json'));
1221
1350
  const repoCheckout = fs_1.default.existsSync(path_1.default.join(packageRootPath, '.git'));
1351
+ const repoDirty = repoCheckout ? repoIsDirty(packageRootPath) : false;
1222
1352
  const globalNpmRoot = detectGlobalNpmRoot();
1223
1353
  const globalNpmInstall = globalNpmRoot !== null && isPathInside(globalNpmRoot, packageRootPath);
1354
+ const globalNpmVersion = globalNpmInstall ? detectGlobalNpmInstalledVersion() : null;
1224
1355
  const python = detectPythonLauncher();
1356
+ const pythonVersion = detectPythonInstalledVersion(python);
1225
1357
  const availableTargets = [];
1226
1358
  if (globalNpmInstall)
1227
1359
  availableTargets.push('npm-global');
@@ -1235,9 +1367,12 @@ function detectUpgradeContext(args) {
1235
1367
  runtimeRoot,
1236
1368
  runtimeInstalled,
1237
1369
  repoCheckout,
1370
+ repoDirty,
1238
1371
  globalNpmInstall,
1239
1372
  globalNpmRoot,
1373
+ globalNpmVersion,
1240
1374
  python,
1375
+ pythonVersion,
1241
1376
  availableTargets,
1242
1377
  };
1243
1378
  }
@@ -1256,6 +1391,101 @@ function chooseUpgradeTarget(requested, context) {
1256
1391
  return 'python';
1257
1392
  return null;
1258
1393
  }
1394
+ function resolveRequestedUpgradeTargets(raw, all) {
1395
+ if (all) {
1396
+ return ['npm-global', 'npm-repo', 'python'];
1397
+ }
1398
+ if (!raw) {
1399
+ return ['auto'];
1400
+ }
1401
+ return raw
1402
+ .split(',')
1403
+ .map((value) => resolveUpgradeTarget(value))
1404
+ .filter((value, index, array) => array.indexOf(value) === index);
1405
+ }
1406
+ function buildUpgradeTargetStatuses(context, latestNpm, latestPython) {
1407
+ return [
1408
+ {
1409
+ target: 'npm-global',
1410
+ available: context.globalNpmInstall,
1411
+ currentVersion: context.globalNpmVersion,
1412
+ latestVersion: latestNpm,
1413
+ upToDate: context.globalNpmVersion && latestNpm ? compareVersions(context.globalNpmVersion, latestNpm) >= 0 : null,
1414
+ blockedReason: context.globalNpmInstall ? undefined : 'No global npm install detected on PATH.',
1415
+ },
1416
+ {
1417
+ target: 'npm-repo',
1418
+ available: context.repoCheckout,
1419
+ currentVersion: context.currentVersion,
1420
+ latestVersion: latestNpm,
1421
+ upToDate: null,
1422
+ blockedReason: !context.repoCheckout
1423
+ ? 'Current package root is not a git checkout.'
1424
+ : context.repoDirty
1425
+ ? 'Repository worktree is dirty.'
1426
+ : undefined,
1427
+ },
1428
+ {
1429
+ target: 'python',
1430
+ available: context.python !== null,
1431
+ currentVersion: context.pythonVersion,
1432
+ latestVersion: latestPython,
1433
+ upToDate: context.pythonVersion && latestPython ? compareVersions(context.pythonVersion, latestPython) >= 0 : null,
1434
+ blockedReason: context.python ? undefined : 'Python launcher not found.',
1435
+ },
1436
+ ];
1437
+ }
1438
+ function describeUpgradeTarget(target) {
1439
+ const current = target.currentVersion ?? 'not installed';
1440
+ const latest = target.latestVersion ?? 'unknown';
1441
+ if (target.target === 'npm-repo') {
1442
+ return target.blockedReason
1443
+ ? `repo checkout (${current}) — ${target.blockedReason}`
1444
+ : `repo checkout (${current}) — refresh local checkout and rebuild`;
1445
+ }
1446
+ if (target.upToDate === true) {
1447
+ return `${target.target} (${current}) — already at latest ${latest}`;
1448
+ }
1449
+ if (target.blockedReason) {
1450
+ return `${target.target} (${current}) — ${target.blockedReason}`;
1451
+ }
1452
+ return `${target.target} (${current}) — latest ${latest}`;
1453
+ }
1454
+ async function chooseInteractiveUpgradeTargets(statuses) {
1455
+ const selected = [];
1456
+ await withPromptSession(async (prompt) => {
1457
+ for (const status of statuses) {
1458
+ if (!status.available) {
1459
+ console.log(`${warnLabel()} ${describeUpgradeTarget(status)}`);
1460
+ continue;
1461
+ }
1462
+ if (status.target === 'npm-repo' && status.blockedReason) {
1463
+ console.log(`${warnLabel()} ${describeUpgradeTarget(status)}`);
1464
+ continue;
1465
+ }
1466
+ const defaultChoice = status.target === 'npm-repo'
1467
+ ? false
1468
+ : status.upToDate === false;
1469
+ const question = status.target === 'npm-global'
1470
+ ? `Upgrade global npm install now? (${describeUpgradeTarget(status)})`
1471
+ : status.target === 'python'
1472
+ ? `Upgrade Python client now? (${describeUpgradeTarget(status)})`
1473
+ : `Refresh local repo checkout now? (${describeUpgradeTarget(status)})`;
1474
+ if (await promptYesNo(prompt, question, defaultChoice)) {
1475
+ selected.push(status.target);
1476
+ }
1477
+ }
1478
+ });
1479
+ return selected;
1480
+ }
1481
+ async function executeUpgradeTargets(targets, context) {
1482
+ const results = [];
1483
+ for (const target of targets) {
1484
+ const result = await executeUpgradeTarget(target, context);
1485
+ results.push(result);
1486
+ }
1487
+ return results;
1488
+ }
1259
1489
  function commandListForTarget(target, context) {
1260
1490
  if (target === 'npm-repo') {
1261
1491
  return repoUpgradeCommands(context.packageRootPath);
@@ -1308,17 +1538,9 @@ function verifyGlobalNpmInstall() {
1308
1538
  }
1309
1539
  }
1310
1540
  function verifyPythonInstall(command) {
1311
- const args = command.executable === 'py' ? ['-3', '-m', 'pip', 'show', 'iranti'] : ['-m', 'pip', 'show', 'iranti'];
1312
- const proc = runCommandCapture(command.executable, args);
1313
- if (proc.status !== 0) {
1314
- return {
1315
- status: 'warn',
1316
- detail: 'Python upgrade finished, but `pip show iranti` did not confirm the installed version.',
1317
- };
1318
- }
1319
- const versionLine = proc.stdout.split(/\r?\n/).find((line) => line.toLowerCase().startsWith('version:'));
1320
- return versionLine
1321
- ? { status: 'pass', detail: `Python client ${versionLine.trim()}.` }
1541
+ const version = detectPythonInstalledVersion(command);
1542
+ return version
1543
+ ? { status: 'pass', detail: `Python client Version: ${version}.` }
1322
1544
  : { status: 'warn', detail: 'Python upgrade finished, but installed version could not be confirmed.' };
1323
1545
  }
1324
1546
  async function executeUpgradeTarget(target, context) {
@@ -1538,7 +1760,7 @@ async function setupCommand(args) {
1538
1760
  let finalScope = 'user';
1539
1761
  let finalRoot = '';
1540
1762
  if (setupMode === 'isolated') {
1541
- finalRoot = path_1.default.resolve(await promptNonEmpty(prompt, 'Runtime root folder', explicitRoot ?? path_1.default.join(process.cwd(), '.iranti-runtime')));
1763
+ finalRoot = path_1.default.resolve(await promptNonEmpty(prompt, 'Where should the isolated runtime live', explicitRoot ?? path_1.default.join(process.cwd(), '.iranti-runtime')));
1542
1764
  finalScope = 'user';
1543
1765
  }
1544
1766
  else {
@@ -1574,7 +1796,7 @@ async function setupCommand(args) {
1574
1796
  const dbMode = (await prompt.line('How should we set up the database: existing, managed, or docker', defaultMode) ?? defaultMode).trim().toLowerCase();
1575
1797
  if (dbMode === 'existing' || dbMode === 'managed') {
1576
1798
  while (true) {
1577
- dbUrl = await promptNonEmpty(prompt, 'DATABASE_URL', existingInstance?.env.DATABASE_URL ?? `postgresql://postgres:yourpassword@localhost:5432/iranti_${instanceName}`);
1799
+ dbUrl = await promptNonEmpty(prompt, 'Database connection string (DATABASE_URL)', existingInstance?.env.DATABASE_URL ?? `postgresql://postgres:yourpassword@localhost:5432/iranti_${instanceName}`);
1578
1800
  if (!detectPlaceholder(dbUrl))
1579
1801
  break;
1580
1802
  console.log(`${warnLabel()} DATABASE_URL still looks like a placeholder. Enter a real connection string before finishing setup.`);
@@ -1613,7 +1835,7 @@ async function setupCommand(args) {
1613
1835
  let provider = normalizeProvider(existingInstance?.env.LLM_PROVIDER ?? 'openai') ?? 'openai';
1614
1836
  while (true) {
1615
1837
  listProviderChoices(provider, existingInstance?.env ?? {});
1616
- const chosen = normalizeProvider(await promptNonEmpty(prompt, 'Default LLM provider', provider));
1838
+ const chosen = normalizeProvider(await promptNonEmpty(prompt, 'Which LLM provider should Iranti use by default', provider));
1617
1839
  if (chosen && isSupportedProvider(chosen)) {
1618
1840
  provider = chosen;
1619
1841
  break;
@@ -1636,7 +1858,7 @@ async function setupCommand(args) {
1636
1858
  let extraProvider = provider;
1637
1859
  while (true) {
1638
1860
  listProviderChoices(provider, { ...seedEnv, ...providerKeys });
1639
- const chosen = normalizeProvider(await promptNonEmpty(prompt, 'Provider to add', 'claude'));
1861
+ const chosen = normalizeProvider(await promptNonEmpty(prompt, 'Which extra provider would you like to add', 'claude'));
1640
1862
  if (!chosen) {
1641
1863
  console.log(`${warnLabel()} Provider is required.`);
1642
1864
  continue;
@@ -1671,10 +1893,10 @@ async function setupCommand(args) {
1671
1893
  const defaultProjectPath = process.cwd();
1672
1894
  let shouldBindProject = await promptYesNo(prompt, 'Bind a project folder to this instance now?', true);
1673
1895
  while (shouldBindProject) {
1674
- const projectPath = path_1.default.resolve(await promptNonEmpty(prompt, 'Project path', projects.length === 0 ? defaultProjectPath : process.cwd()));
1675
- const agentId = sanitizeIdentifier(await promptNonEmpty(prompt, 'Agent id for this project', projectAgentDefault(projectPath)), 'project_main');
1676
- const memoryEntity = await promptNonEmpty(prompt, 'Memory entity for this project', 'user/main');
1677
- const claudeCode = await promptYesNo(prompt, 'Create Claude Code project files here?', true);
1896
+ const projectPath = path_1.default.resolve(await promptNonEmpty(prompt, 'Which project folder should we bind', projects.length === 0 ? defaultProjectPath : process.cwd()));
1897
+ const agentId = sanitizeIdentifier(await promptNonEmpty(prompt, 'What agent id should this project use', projectAgentDefault(projectPath)), 'project_main');
1898
+ const memoryEntity = await promptNonEmpty(prompt, 'What memory entity should this project use', 'user/main');
1899
+ const claudeCode = await promptYesNo(prompt, 'Create Claude Code project files here now?', true);
1678
1900
  projects.push({
1679
1901
  path: projectPath,
1680
1902
  agentId,
@@ -1726,6 +1948,10 @@ async function setupCommand(args) {
1726
1948
  console.log(`${infoLabel()} Next steps:`);
1727
1949
  console.log(` 1. iranti run --instance ${finalResult.instanceName} --root "${finalResult.root}"`);
1728
1950
  console.log(` 2. iranti doctor --instance ${finalResult.instanceName} --root "${finalResult.root}"`);
1951
+ if (finalResult.bindings.length > 0) {
1952
+ console.log(` 3. cd "${finalResult.bindings[0].projectPath}"`);
1953
+ console.log(' 4. iranti chat');
1954
+ }
1729
1955
  }
1730
1956
  async function doctorCommand(args) {
1731
1957
  const json = hasFlag(args, 'json');
@@ -1871,6 +2097,7 @@ async function doctorCommand(args) {
1871
2097
  envFile,
1872
2098
  status: summarizeStatus(checks),
1873
2099
  checks,
2100
+ remediations: collectDoctorRemediations(checks, envSource, envFile),
1874
2101
  };
1875
2102
  if (json) {
1876
2103
  console.log(JSON.stringify(result, null, 2));
@@ -1892,7 +2119,14 @@ async function doctorCommand(args) {
1892
2119
  : check.status === 'warn'
1893
2120
  ? warnLabel('WARN')
1894
2121
  : failLabel('FAIL');
1895
- console.log(`${marker} ${check.name} ${check.detail}`);
2122
+ console.log(`${marker} ${check.name} — ${check.detail}`);
2123
+ }
2124
+ if (result.remediations.length > 0) {
2125
+ console.log('');
2126
+ console.log('Suggested fixes:');
2127
+ for (const remediation of result.remediations) {
2128
+ console.log(` - ${remediation}`);
2129
+ }
1896
2130
  }
1897
2131
  if (result.status !== 'pass') {
1898
2132
  process.exitCode = 1;
@@ -1965,46 +2199,72 @@ async function statusCommand(args) {
1965
2199
  }
1966
2200
  }
1967
2201
  async function upgradeCommand(args) {
2202
+ const runAll = hasFlag(args, 'all');
1968
2203
  const checkOnly = hasFlag(args, 'check');
1969
2204
  const dryRun = hasFlag(args, 'dry-run');
1970
2205
  const execute = hasFlag(args, 'yes');
1971
2206
  const json = hasFlag(args, 'json');
1972
- const requestedTarget = resolveUpgradeTarget(getFlag(args, 'target'));
2207
+ const requestedTargets = resolveRequestedUpgradeTargets(getFlag(args, 'target'), runAll);
1973
2208
  const context = detectUpgradeContext(args);
1974
2209
  const latestNpm = await fetchLatestNpmVersion();
1975
2210
  const latestPython = await fetchLatestPypiVersion();
1976
- const chosenTarget = chooseUpgradeTarget(requestedTarget, context);
2211
+ const statuses = buildUpgradeTargetStatuses(context, latestNpm, latestPython);
2212
+ const statusByTarget = new Map(statuses.map((status) => [status.target, status]));
2213
+ const autoSelected = requestedTargets.includes('auto')
2214
+ ? chooseUpgradeTarget('auto', context)
2215
+ : null;
2216
+ const explicitTargets = requestedTargets
2217
+ .filter((target) => target !== 'auto');
2218
+ for (const target of explicitTargets) {
2219
+ const status = statusByTarget.get(target);
2220
+ if (!runAll && !status?.available) {
2221
+ throw new Error(`Requested target '${target}' is not available in this environment.`);
2222
+ }
2223
+ }
2224
+ const selectedTargets = requestedTargets.includes('auto')
2225
+ ? (autoSelected ? [autoSelected] : [])
2226
+ : explicitTargets.filter((target) => {
2227
+ const status = statusByTarget.get(target);
2228
+ if (!status?.available)
2229
+ return false;
2230
+ if (runAll && status.blockedReason)
2231
+ return false;
2232
+ return true;
2233
+ });
1977
2234
  const commands = {
1978
2235
  npmGlobal: 'npm install -g iranti@latest',
1979
2236
  npmRepo: 'git pull --ff-only && npm install && npm run build',
1980
2237
  python: context.python?.display ?? 'python -m pip install --upgrade iranti',
1981
2238
  };
1982
2239
  const updateAvailable = {
1983
- npm: latestNpm ? compareVersions(latestNpm, context.currentVersion) > 0 : null,
1984
- python: latestPython ? compareVersions(latestPython, context.currentVersion) > 0 : null,
2240
+ npm: context.globalNpmVersion && latestNpm ? compareVersions(latestNpm, context.globalNpmVersion) > 0 : null,
2241
+ python: context.pythonVersion && latestPython ? compareVersions(latestPython, context.pythonVersion) > 0 : null,
1985
2242
  };
1986
- const plan = chosenTarget ? commandListForTarget(chosenTarget, context) : [];
1987
- let execution = null;
2243
+ const plan = selectedTargets.flatMap((target) => commandListForTarget(target, context).map((step) => step.display));
2244
+ let execution = [];
1988
2245
  let note = null;
1989
2246
  if (execute) {
1990
- if (!chosenTarget) {
1991
- throw new Error('No executable upgrade path was detected. Use --target npm-global, --target npm-repo, or --target python.');
2247
+ if (selectedTargets.length === 0) {
2248
+ throw new Error('No executable upgrade path was detected. Use --target npm-global, --target npm-repo, --target python, or --all.');
1992
2249
  }
1993
2250
  if (dryRun || checkOnly) {
1994
2251
  note = 'Execution skipped because --dry-run or --check was provided.';
1995
2252
  }
1996
- else if (chosenTarget === 'npm-global' && updateAvailable.npm === false) {
1997
- note = 'npm global install is already at the latest published version.';
2253
+ else {
2254
+ execution = await executeUpgradeTargets(selectedTargets, context);
1998
2255
  }
1999
- else if (chosenTarget === 'python' && updateAvailable.python === false) {
2000
- note = 'Python client is already at the latest published version.';
2256
+ }
2257
+ else if (!checkOnly && !dryRun && !json && process.stdin.isTTY && process.stdout.isTTY) {
2258
+ const interactiveTargets = await chooseInteractiveUpgradeTargets(statuses);
2259
+ if (interactiveTargets.length === 0) {
2260
+ note = 'No upgrade targets selected.';
2001
2261
  }
2002
2262
  else {
2003
- execution = await executeUpgradeTarget(chosenTarget, context);
2263
+ execution = await executeUpgradeTargets(interactiveTargets, context);
2004
2264
  }
2005
2265
  }
2006
2266
  else if (!checkOnly && !dryRun) {
2007
- note = 'Run with --yes to execute the selected upgrade path. Use --check to inspect and --dry-run to print exact commands.';
2267
+ note = 'Run with --yes to execute the selected upgrade path, or run plain `iranti upgrade` in a TTY to choose interactively.';
2008
2268
  }
2009
2269
  if (json) {
2010
2270
  console.log(JSON.stringify({
@@ -2018,17 +2278,21 @@ async function upgradeCommand(args) {
2018
2278
  runtimeRoot: context.runtimeRoot,
2019
2279
  runtimeInstalled: context.runtimeInstalled,
2020
2280
  repoCheckout: context.repoCheckout,
2281
+ repoDirty: context.repoDirty,
2021
2282
  globalNpmInstall: context.globalNpmInstall,
2022
2283
  globalNpmRoot: context.globalNpmRoot,
2284
+ globalNpmVersion: context.globalNpmVersion,
2023
2285
  pythonLauncher: context.python?.executable ?? null,
2286
+ pythonVersion: context.pythonVersion,
2024
2287
  },
2025
- requestedTarget,
2026
- selectedTarget: chosenTarget,
2288
+ requestedTargets,
2289
+ selectedTargets,
2027
2290
  availableTargets: context.availableTargets,
2291
+ targets: statuses,
2028
2292
  updateAvailable,
2029
2293
  commands,
2030
- plan: plan.map((step) => step.display),
2031
- action: execute && !dryRun && !checkOnly ? 'upgrade' : checkOnly ? 'check' : dryRun ? 'dry-run' : 'inspect',
2294
+ plan,
2295
+ action: execution.length > 0 ? 'upgrade' : checkOnly ? 'check' : dryRun ? 'dry-run' : 'inspect',
2032
2296
  execution,
2033
2297
  note,
2034
2298
  }, null, 2));
@@ -2040,38 +2304,43 @@ async function upgradeCommand(args) {
2040
2304
  console.log(` latest_python ${latestPython ?? '(unavailable)'}`);
2041
2305
  console.log(` package_root ${context.packageRootPath}`);
2042
2306
  console.log(` runtime_root ${context.runtimeRoot}`);
2043
- console.log(` repo_checkout ${context.repoCheckout ? paint('yes', 'green') : paint('no', 'gray')}`);
2044
- console.log(` npm_global ${context.globalNpmInstall ? paint('yes', 'green') : paint('no', 'gray')}`);
2045
- console.log(` python ${context.python?.executable ?? paint('not found', 'yellow')}`);
2307
+ console.log(` repo_checkout ${context.repoCheckout ? paint('yes', 'green') : paint('no', 'gray')}${context.repoDirty ? paint(' (dirty)', 'yellow') : ''}`);
2308
+ console.log(` npm_global ${context.globalNpmInstall ? paint('yes', 'green') : paint('no', 'gray')}${context.globalNpmVersion ? ` (${context.globalNpmVersion})` : ''}`);
2309
+ console.log(` python ${context.python?.executable ?? paint('not found', 'yellow')}${context.pythonVersion ? ` (${context.pythonVersion})` : ''}`);
2046
2310
  console.log('');
2047
- if (chosenTarget) {
2048
- console.log(` selected_target ${paint(chosenTarget, 'cyan')}${requestedTarget === 'auto' ? paint(' (auto)', 'gray') : ''}`);
2311
+ if (selectedTargets.length > 0) {
2312
+ console.log(` selected_target${selectedTargets.length > 1 ? 's' : ''} ${paint(selectedTargets.join(', '), 'cyan')}${requestedTargets.includes('auto') ? paint(' (auto)', 'gray') : ''}`);
2049
2313
  console.log(' plan');
2050
2314
  for (const step of plan) {
2051
- console.log(` - ${step.display}`);
2315
+ console.log(` - ${step}`);
2052
2316
  }
2053
2317
  }
2054
2318
  else {
2055
- console.log(` selected_target ${paint('none', 'yellow')}`);
2319
+ console.log(` selected_targets ${paint('none', 'yellow')}`);
2056
2320
  console.log(' plan No executable upgrade path detected automatically.');
2057
2321
  }
2058
2322
  console.log('');
2059
2323
  console.log(` npm global ${commands.npmGlobal}`);
2060
2324
  console.log(` npm repo ${commands.npmRepo}`);
2061
2325
  console.log(` python client ${commands.python}`);
2062
- if (execution) {
2063
- const marker = execution.verification.status === 'pass'
2064
- ? okLabel('PASS')
2065
- : execution.verification.status === 'warn'
2066
- ? warnLabel('WARN')
2067
- : failLabel('FAIL');
2326
+ if (execution.length > 0) {
2068
2327
  console.log('');
2069
- console.log(`${okLabel()} Upgrade completed for ${execution.target}.`);
2070
- console.log(`${marker} ${execution.verification.detail}`);
2328
+ for (const result of execution) {
2329
+ const marker = result.verification.status === 'pass'
2330
+ ? okLabel('PASS')
2331
+ : result.verification.status === 'warn'
2332
+ ? warnLabel('WARN')
2333
+ : failLabel('FAIL');
2334
+ console.log(`${okLabel()} Upgrade completed for ${result.target}.`);
2335
+ console.log(`${marker} ${result.verification.detail}`);
2336
+ }
2071
2337
  const { envFile } = resolveDoctorEnvTarget(args);
2072
2338
  if (envFile) {
2073
2339
  console.log(`${infoLabel()} Run \`iranti doctor\` to verify the active environment after the package upgrade.`);
2074
2340
  }
2341
+ if (execution.some((result) => result.target === 'npm-global')) {
2342
+ console.log(`${infoLabel()} If this shell started on an older global CLI, open a new terminal or rerun \`iranti upgrade --check\` to confirm the new binary is active.`);
2343
+ }
2075
2344
  return;
2076
2345
  }
2077
2346
  if (note) {
@@ -2266,14 +2535,15 @@ async function configureInstanceCommand(args) {
2266
2535
  let clearProviderKey = hasFlag(args, 'clear-provider-key');
2267
2536
  if (hasFlag(args, 'interactive')) {
2268
2537
  await withPromptSession(async (prompt) => {
2269
- portRaw = await prompt.line('IRANTI_PORT', portRaw ?? env.IRANTI_PORT);
2270
- dbUrl = await prompt.line('DATABASE_URL', dbUrl ?? env.DATABASE_URL);
2271
- providerInput = await prompt.line('LLM_PROVIDER', providerInput ?? env.LLM_PROVIDER ?? 'mock');
2538
+ portRaw = await prompt.line('Which API port should this instance use', portRaw ?? env.IRANTI_PORT);
2539
+ dbUrl = await prompt.line('Database connection string (DATABASE_URL)', dbUrl ?? env.DATABASE_URL);
2540
+ providerInput = await prompt.line('Which LLM provider should this instance use', providerInput ?? env.LLM_PROVIDER ?? 'mock');
2272
2541
  const interactiveProvider = normalizeProvider(providerInput ?? env.LLM_PROVIDER ?? 'mock');
2273
- if (providerKeyEnv(interactiveProvider)) {
2274
- providerKey = await prompt.secret(`${providerKeyEnv(interactiveProvider)}`, providerKey ?? env[providerKeyEnv(interactiveProvider)]);
2542
+ const interactiveProviderEnvKey = providerKeyEnv(interactiveProvider);
2543
+ if (interactiveProvider && interactiveProviderEnvKey) {
2544
+ providerKey = await prompt.secret(`Enter the ${providerDisplayName(interactiveProvider)} API key`, providerKey ?? env[interactiveProviderEnvKey]);
2275
2545
  }
2276
- apiKey = await prompt.secret('IRANTI_API_KEY', apiKey ?? env.IRANTI_API_KEY);
2546
+ apiKey = await prompt.secret('Iranti API key', apiKey ?? env.IRANTI_API_KEY);
2277
2547
  });
2278
2548
  clearProviderKey = false;
2279
2549
  }
@@ -2330,6 +2600,7 @@ async function configureInstanceCommand(args) {
2330
2600
  if (providerKey) {
2331
2601
  console.log(` provider ${result.provider}`);
2332
2602
  }
2603
+ console.log(`${infoLabel()} Next: iranti doctor --instance ${name}${scope ? ` --scope ${scope}` : ''}`);
2333
2604
  }
2334
2605
  async function configureProjectCommand(args) {
2335
2606
  const projectPath = path_1.default.resolve(args.positionals[0] ?? process.cwd());
@@ -2343,11 +2614,11 @@ async function configureProjectCommand(args) {
2343
2614
  let explicitMemoryEntity = getFlag(args, 'memory-entity');
2344
2615
  if (hasFlag(args, 'interactive')) {
2345
2616
  await withPromptSession(async (prompt) => {
2346
- instanceName = await prompt.line('IRANTI_INSTANCE', instanceName);
2347
- explicitUrl = await prompt.line('IRANTI_URL', explicitUrl ?? existing.IRANTI_URL);
2348
- explicitApiKey = await prompt.secret('IRANTI_API_KEY', explicitApiKey ?? existing.IRANTI_API_KEY);
2349
- explicitAgentId = await prompt.line('IRANTI_AGENT_ID', explicitAgentId ?? existing.IRANTI_AGENT_ID ?? 'my_agent');
2350
- explicitMemoryEntity = await prompt.line('IRANTI_MEMORY_ENTITY', explicitMemoryEntity ?? existing.IRANTI_MEMORY_ENTITY ?? 'user/main');
2617
+ instanceName = await prompt.line('Which instance should this project use', instanceName);
2618
+ explicitUrl = await prompt.line('What Iranti URL should this project talk to', explicitUrl ?? existing.IRANTI_URL);
2619
+ explicitApiKey = await prompt.secret('What API key should this project use', explicitApiKey ?? existing.IRANTI_API_KEY);
2620
+ explicitAgentId = await prompt.line('What agent id should this project use', explicitAgentId ?? existing.IRANTI_AGENT_ID ?? 'my_agent');
2621
+ explicitMemoryEntity = await prompt.line('What memory entity should this project use', explicitMemoryEntity ?? existing.IRANTI_MEMORY_ENTITY ?? 'user/main');
2351
2622
  });
2352
2623
  }
2353
2624
  let instanceEnvFile = existing.IRANTI_INSTANCE_ENV;
@@ -2395,6 +2666,7 @@ async function configureProjectCommand(args) {
2395
2666
  if (updates.IRANTI_INSTANCE) {
2396
2667
  console.log(` instance ${updates.IRANTI_INSTANCE}`);
2397
2668
  }
2669
+ console.log(`${infoLabel()} Next: iranti doctor${updates.IRANTI_INSTANCE ? ` --instance ${updates.IRANTI_INSTANCE}` : ''}`);
2398
2670
  }
2399
2671
  async function authCreateKeyCommand(args) {
2400
2672
  const instanceName = getFlag(args, 'instance');
@@ -2462,6 +2734,7 @@ async function authCreateKeyCommand(args) {
2462
2734
  if (projectPath) {
2463
2735
  console.log(` project ${path_1.default.resolve(projectPath)}`);
2464
2736
  }
2737
+ console.log(`${infoLabel()} Next: iranti doctor --instance ${instanceName}`);
2465
2738
  process.exit(0);
2466
2739
  }
2467
2740
  async function authListKeysCommand(args) {
@@ -2519,6 +2792,151 @@ async function resolveCommand(args) {
2519
2792
  const escalationDir = explicitDir ? path_1.default.resolve(explicitDir) : (0, escalationPaths_1.getEscalationPaths)().root;
2520
2793
  await (0, resolutionist_1.resolveInteractive)(escalationDir);
2521
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
+ }
2522
2940
  async function chatCommand(args) {
2523
2941
  const provider = normalizeProvider(getFlag(args, 'provider'));
2524
2942
  if (provider && !isSupportedProvider(provider)) {
@@ -2565,14 +2983,19 @@ Project-level:
2565
2983
  Diagnostics:
2566
2984
  iranti doctor [--instance <name>] [--scope user|system] [--env <file>] [--json]
2567
2985
  iranti status [--scope user|system] [--json]
2568
- iranti upgrade [--check] [--dry-run] [--yes] [--target auto|npm-global|npm-repo|python] [--json]
2986
+ iranti upgrade [--check] [--dry-run] [--yes] [--all] [--target auto|npm-global|npm-repo|python[,python]] [--json]
2569
2987
  iranti chat [--agent <agent-id>] [--provider <provider>] [--model <model>]
2570
2988
  iranti resolve [--dir <escalation-dir>]
2571
2989
 
2572
2990
  Integrations:
2573
2991
  iranti mcp [--help]
2992
+ iranti claude-setup [path] [--project-env <path>] [--force]
2993
+ iranti claude-setup --scan <dir> [--force]
2574
2994
  iranti claude-hook --event SessionStart|UserPromptSubmit [--project-env <path>] [--instance-env <path>] [--env-file <path>]
2575
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]
2576
2999
  `);
2577
3000
  }
2578
3001
  async function main() {
@@ -2678,6 +3101,10 @@ async function main() {
2678
3101
  await handoffToScript('iranti-mcp', process.argv.slice(3));
2679
3102
  return;
2680
3103
  }
3104
+ if (args.command === 'claude-setup') {
3105
+ await claudeSetupCommand(args);
3106
+ return;
3107
+ }
2681
3108
  if (args.command === 'claude-hook') {
2682
3109
  await handoffToScript('claude-code-memory-hook', process.argv.slice(3));
2683
3110
  return;
@@ -2686,6 +3113,17 @@ async function main() {
2686
3113
  await handoffToScript('codex-setup', process.argv.slice(3));
2687
3114
  return;
2688
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
+ }
2689
3127
  throw new Error(`Unknown command '${args.command}'. Run: iranti help`);
2690
3128
  }
2691
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.4',
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.4',
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.4',
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.4',
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.4' },
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.4',
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.4',
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.4',
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.4',
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.4',
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.4',
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.4',
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.4",
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": [