iranti 0.2.3 → 0.2.5

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.
@@ -722,7 +722,7 @@ async function chooseAvailablePort(session, promptText, preferredPort, allowOccu
722
722
  let suggested = preferredPort;
723
723
  if (!allowOccupiedCurrent && !(await isPortAvailable(preferredPort))) {
724
724
  suggested = await findNextAvailablePort(preferredPort + 1);
725
- console.log(`${warnLabel()} Port ${preferredPort} is already in use. Suggested port: ${suggested}`);
725
+ console.log(`${warnLabel()} Port ${preferredPort} is already in use. A good next option is ${suggested}.`);
726
726
  }
727
727
  while (true) {
728
728
  const raw = await promptNonEmpty(session, promptText, String(suggested));
@@ -1015,6 +1015,53 @@ function summarizeStatus(checks) {
1015
1015
  return 'warn';
1016
1016
  return 'pass';
1017
1017
  }
1018
+ function collectDoctorRemediations(checks, envSource, envFile) {
1019
+ const hints = [];
1020
+ const add = (hint) => {
1021
+ if (!hints.includes(hint))
1022
+ hints.push(hint);
1023
+ };
1024
+ for (const check of checks) {
1025
+ if (check.name === 'node version' && check.status === 'fail') {
1026
+ add('Upgrade Node.js to version 18 or newer, then rerun `iranti doctor`.');
1027
+ }
1028
+ if (check.name === 'cli build artifact' && check.status !== 'pass') {
1029
+ 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`.');
1030
+ }
1031
+ if (check.name === 'environment file' && check.status === 'fail') {
1032
+ if (envFile) {
1033
+ add(`Fix or recreate the target env file at ${envFile}, or rerun \`iranti setup\`.`);
1034
+ }
1035
+ else {
1036
+ add('Run `iranti setup`, or rerun `iranti doctor` with `--instance <name>` or `--env <file>`.');
1037
+ }
1038
+ }
1039
+ if (check.name === 'database configuration' && check.status === 'fail') {
1040
+ add(`Set a real DATABASE_URL in ${envFile ?? 'the target env file'}, or rerun \`iranti setup\` to configure the database again.`);
1041
+ }
1042
+ if (check.name === 'project binding url' && check.status === 'fail') {
1043
+ add('Run `iranti configure project` to refresh the project binding, or set IRANTI_URL in `.env.iranti`.');
1044
+ }
1045
+ if (check.name === 'project api key' && check.status === 'fail') {
1046
+ add('Run `iranti configure project` or set IRANTI_API_KEY in `.env.iranti`.');
1047
+ }
1048
+ if (check.name === 'api key' && check.status !== 'pass') {
1049
+ add(envSource === 'project-binding'
1050
+ ? 'Set IRANTI_API_KEY in the project binding, or rerun `iranti configure project`.'
1051
+ : 'Create or rotate an Iranti key with `iranti auth create-key`, then store it in the target env.');
1052
+ }
1053
+ if (check.name === 'provider credentials' && check.status === 'fail') {
1054
+ add('Store or refresh the upstream provider key with `iranti add api-key` or `iranti update api-key`.');
1055
+ }
1056
+ if (check.name === 'vector backend' && check.status === 'fail') {
1057
+ add('Check the vector backend env vars, or switch back to `IRANTI_VECTOR_BACKEND=pgvector` if the external backend is not ready.');
1058
+ }
1059
+ }
1060
+ if (checks.some((check) => check.status !== 'pass')) {
1061
+ add('Use `iranti upgrade --all --dry-run` to see whether this machine has stale CLI or Python installs.');
1062
+ }
1063
+ return hints;
1064
+ }
1018
1065
  function resolveDoctorEnvTarget(args) {
1019
1066
  const scope = normalizeScope(getFlag(args, 'scope'));
1020
1067
  const instanceName = getFlag(args, 'instance');
@@ -1139,6 +1186,32 @@ function detectGlobalNpmRoot() {
1139
1186
  const value = proc.stdout.trim();
1140
1187
  return value ? path_1.default.resolve(value) : null;
1141
1188
  }
1189
+ function detectGlobalNpmInstalledVersion() {
1190
+ const proc = runCommandCapture('npm', ['list', '-g', 'iranti', '--depth=0', '--json']);
1191
+ if (proc.status !== 0)
1192
+ return null;
1193
+ try {
1194
+ const payload = JSON.parse(proc.stdout);
1195
+ return typeof payload?.dependencies?.iranti?.version === 'string'
1196
+ ? payload.dependencies.iranti.version
1197
+ : null;
1198
+ }
1199
+ catch {
1200
+ return null;
1201
+ }
1202
+ }
1203
+ function detectPythonInstalledVersion(command) {
1204
+ if (!command)
1205
+ return null;
1206
+ const args = command.executable === 'py' ? ['-3', '-m', 'pip', 'show', 'iranti'] : ['-m', 'pip', 'show', 'iranti'];
1207
+ const proc = runCommandCapture(command.executable, args);
1208
+ if (proc.status !== 0)
1209
+ return null;
1210
+ const versionLine = proc.stdout.split(/\r?\n/).find((line) => line.toLowerCase().startsWith('version:'));
1211
+ if (!versionLine)
1212
+ return null;
1213
+ return versionLine.split(':').slice(1).join(':').trim() || null;
1214
+ }
1142
1215
  function readJsonFile(filePath) {
1143
1216
  if (!fs_1.default.existsSync(filePath))
1144
1217
  return null;
@@ -1219,9 +1292,12 @@ function detectUpgradeContext(args) {
1219
1292
  const runtimeRoot = resolveInstallRoot(args, scope);
1220
1293
  const runtimeInstalled = fs_1.default.existsSync(path_1.default.join(runtimeRoot, 'install.json'));
1221
1294
  const repoCheckout = fs_1.default.existsSync(path_1.default.join(packageRootPath, '.git'));
1295
+ const repoDirty = repoCheckout ? repoIsDirty(packageRootPath) : false;
1222
1296
  const globalNpmRoot = detectGlobalNpmRoot();
1223
1297
  const globalNpmInstall = globalNpmRoot !== null && isPathInside(globalNpmRoot, packageRootPath);
1298
+ const globalNpmVersion = globalNpmInstall ? detectGlobalNpmInstalledVersion() : null;
1224
1299
  const python = detectPythonLauncher();
1300
+ const pythonVersion = detectPythonInstalledVersion(python);
1225
1301
  const availableTargets = [];
1226
1302
  if (globalNpmInstall)
1227
1303
  availableTargets.push('npm-global');
@@ -1235,9 +1311,12 @@ function detectUpgradeContext(args) {
1235
1311
  runtimeRoot,
1236
1312
  runtimeInstalled,
1237
1313
  repoCheckout,
1314
+ repoDirty,
1238
1315
  globalNpmInstall,
1239
1316
  globalNpmRoot,
1317
+ globalNpmVersion,
1240
1318
  python,
1319
+ pythonVersion,
1241
1320
  availableTargets,
1242
1321
  };
1243
1322
  }
@@ -1256,6 +1335,101 @@ function chooseUpgradeTarget(requested, context) {
1256
1335
  return 'python';
1257
1336
  return null;
1258
1337
  }
1338
+ function resolveRequestedUpgradeTargets(raw, all) {
1339
+ if (all) {
1340
+ return ['npm-global', 'npm-repo', 'python'];
1341
+ }
1342
+ if (!raw) {
1343
+ return ['auto'];
1344
+ }
1345
+ return raw
1346
+ .split(',')
1347
+ .map((value) => resolveUpgradeTarget(value))
1348
+ .filter((value, index, array) => array.indexOf(value) === index);
1349
+ }
1350
+ function buildUpgradeTargetStatuses(context, latestNpm, latestPython) {
1351
+ return [
1352
+ {
1353
+ target: 'npm-global',
1354
+ available: context.globalNpmInstall,
1355
+ currentVersion: context.globalNpmVersion,
1356
+ latestVersion: latestNpm,
1357
+ upToDate: context.globalNpmVersion && latestNpm ? compareVersions(context.globalNpmVersion, latestNpm) >= 0 : null,
1358
+ blockedReason: context.globalNpmInstall ? undefined : 'No global npm install detected on PATH.',
1359
+ },
1360
+ {
1361
+ target: 'npm-repo',
1362
+ available: context.repoCheckout,
1363
+ currentVersion: context.currentVersion,
1364
+ latestVersion: latestNpm,
1365
+ upToDate: null,
1366
+ blockedReason: !context.repoCheckout
1367
+ ? 'Current package root is not a git checkout.'
1368
+ : context.repoDirty
1369
+ ? 'Repository worktree is dirty.'
1370
+ : undefined,
1371
+ },
1372
+ {
1373
+ target: 'python',
1374
+ available: context.python !== null,
1375
+ currentVersion: context.pythonVersion,
1376
+ latestVersion: latestPython,
1377
+ upToDate: context.pythonVersion && latestPython ? compareVersions(context.pythonVersion, latestPython) >= 0 : null,
1378
+ blockedReason: context.python ? undefined : 'Python launcher not found.',
1379
+ },
1380
+ ];
1381
+ }
1382
+ function describeUpgradeTarget(target) {
1383
+ const current = target.currentVersion ?? 'not installed';
1384
+ const latest = target.latestVersion ?? 'unknown';
1385
+ if (target.target === 'npm-repo') {
1386
+ return target.blockedReason
1387
+ ? `repo checkout (${current}) — ${target.blockedReason}`
1388
+ : `repo checkout (${current}) — refresh local checkout and rebuild`;
1389
+ }
1390
+ if (target.upToDate === true) {
1391
+ return `${target.target} (${current}) — already at latest ${latest}`;
1392
+ }
1393
+ if (target.blockedReason) {
1394
+ return `${target.target} (${current}) — ${target.blockedReason}`;
1395
+ }
1396
+ return `${target.target} (${current}) — latest ${latest}`;
1397
+ }
1398
+ async function chooseInteractiveUpgradeTargets(statuses) {
1399
+ const selected = [];
1400
+ await withPromptSession(async (prompt) => {
1401
+ for (const status of statuses) {
1402
+ if (!status.available) {
1403
+ console.log(`${warnLabel()} ${describeUpgradeTarget(status)}`);
1404
+ continue;
1405
+ }
1406
+ if (status.target === 'npm-repo' && status.blockedReason) {
1407
+ console.log(`${warnLabel()} ${describeUpgradeTarget(status)}`);
1408
+ continue;
1409
+ }
1410
+ const defaultChoice = status.target === 'npm-repo'
1411
+ ? false
1412
+ : status.upToDate === false;
1413
+ const question = status.target === 'npm-global'
1414
+ ? `Upgrade global npm install now? (${describeUpgradeTarget(status)})`
1415
+ : status.target === 'python'
1416
+ ? `Upgrade Python client now? (${describeUpgradeTarget(status)})`
1417
+ : `Refresh local repo checkout now? (${describeUpgradeTarget(status)})`;
1418
+ if (await promptYesNo(prompt, question, defaultChoice)) {
1419
+ selected.push(status.target);
1420
+ }
1421
+ }
1422
+ });
1423
+ return selected;
1424
+ }
1425
+ async function executeUpgradeTargets(targets, context) {
1426
+ const results = [];
1427
+ for (const target of targets) {
1428
+ const result = await executeUpgradeTarget(target, context);
1429
+ results.push(result);
1430
+ }
1431
+ return results;
1432
+ }
1259
1433
  function commandListForTarget(target, context) {
1260
1434
  if (target === 'npm-repo') {
1261
1435
  return repoUpgradeCommands(context.packageRootPath);
@@ -1308,17 +1482,9 @@ function verifyGlobalNpmInstall() {
1308
1482
  }
1309
1483
  }
1310
1484
  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()}.` }
1485
+ const version = detectPythonInstalledVersion(command);
1486
+ return version
1487
+ ? { status: 'pass', detail: `Python client Version: ${version}.` }
1322
1488
  : { status: 'warn', detail: 'Python upgrade finished, but installed version could not be confirmed.' };
1323
1489
  }
1324
1490
  async function executeUpgradeTarget(target, context) {
@@ -1522,13 +1688,13 @@ async function setupCommand(args) {
1522
1688
  const explicitScope = getFlag(args, 'scope');
1523
1689
  const explicitRoot = getFlag(args, 'root');
1524
1690
  console.log(bold('Iranti setup'));
1525
- console.log('This wizard will install a runtime, create or update an instance, configure provider keys, create a usable Iranti API key, and optionally bind one or more project folders.');
1691
+ console.log('This wizard will get Iranti set up: install a runtime, create or update an instance, connect provider keys, create a usable Iranti API key, and optionally bind one or more project folders.');
1526
1692
  console.log('');
1527
1693
  let result = null;
1528
1694
  await withPromptSession(async (prompt) => {
1529
1695
  let setupMode = 'shared';
1530
1696
  while (true) {
1531
- const chosen = (await prompt.line('Setup mode: shared runtime or isolated runtime folder', 'shared') ?? 'shared').trim().toLowerCase();
1697
+ const chosen = (await prompt.line('How should Iranti install the runtime: shared or isolated folder', 'shared') ?? 'shared').trim().toLowerCase();
1532
1698
  if (chosen === 'shared' || chosen === 'isolated') {
1533
1699
  setupMode = chosen;
1534
1700
  break;
@@ -1538,43 +1704,43 @@ async function setupCommand(args) {
1538
1704
  let finalScope = 'user';
1539
1705
  let finalRoot = '';
1540
1706
  if (setupMode === 'isolated') {
1541
- finalRoot = path_1.default.resolve(await promptNonEmpty(prompt, 'Runtime root folder', explicitRoot ?? path_1.default.join(process.cwd(), '.iranti-runtime')));
1707
+ finalRoot = path_1.default.resolve(await promptNonEmpty(prompt, 'Where should the isolated runtime live', explicitRoot ?? path_1.default.join(process.cwd(), '.iranti-runtime')));
1542
1708
  finalScope = 'user';
1543
1709
  }
1544
1710
  else {
1545
1711
  while (true) {
1546
- const chosenScope = (await prompt.line('Install scope', explicitScope ?? 'user') ?? 'user').trim().toLowerCase();
1712
+ const chosenScope = (await prompt.line('Install scope: user or system', explicitScope ?? 'user') ?? 'user').trim().toLowerCase();
1547
1713
  if (chosenScope === 'user' || chosenScope === 'system') {
1548
1714
  finalScope = chosenScope;
1549
1715
  break;
1550
1716
  }
1551
- console.log(`${warnLabel()} Install scope must be user or system.`);
1717
+ console.log(`${warnLabel()} Please choose either user or system.`);
1552
1718
  }
1553
1719
  finalRoot = explicitRoot ? path_1.default.resolve(explicitRoot) : resolveInstallRoot(args, finalScope);
1554
1720
  }
1555
1721
  await ensureRuntimeInstalled(finalRoot, finalScope);
1556
1722
  console.log(`${okLabel()} Runtime ready at ${finalRoot}`);
1557
- const instanceName = sanitizeIdentifier(await promptNonEmpty(prompt, 'Instance name', setupMode === 'isolated' ? sanitizeIdentifier(path_1.default.basename(process.cwd()), 'local') : 'local'), 'local');
1723
+ const instanceName = sanitizeIdentifier(await promptNonEmpty(prompt, 'What should this instance be called', setupMode === 'isolated' ? sanitizeIdentifier(path_1.default.basename(process.cwd()), 'local') : 'local'), 'local');
1558
1724
  const existingInstance = fs_1.default.existsSync(instancePaths(finalRoot, instanceName).envFile)
1559
1725
  ? await loadInstanceEnv(finalRoot, instanceName)
1560
1726
  : null;
1561
1727
  if (existingInstance) {
1562
- console.log(`${infoLabel()} Updating existing instance '${instanceName}'.`);
1728
+ console.log(`${infoLabel()} Found existing instance '${instanceName}'. Updating it.`);
1563
1729
  }
1564
1730
  else {
1565
1731
  console.log(`${infoLabel()} Creating new instance '${instanceName}'.`);
1566
1732
  }
1567
1733
  const existingPort = Number.parseInt(existingInstance?.env.IRANTI_PORT ?? '3001', 10);
1568
- const port = await chooseAvailablePort(prompt, 'Iranti API port', existingPort, Boolean(existingInstance));
1734
+ const port = await chooseAvailablePort(prompt, 'Which port should the Iranti API use', existingPort, Boolean(existingInstance));
1569
1735
  const dockerAvailable = hasDockerInstalled();
1570
1736
  let dbUrl = '';
1571
1737
  let bootstrapDatabase = false;
1572
1738
  while (true) {
1573
1739
  const defaultMode = dockerAvailable ? 'docker' : 'existing';
1574
- const dbMode = (await prompt.line('Database setup mode: existing, managed, or docker', defaultMode) ?? defaultMode).trim().toLowerCase();
1740
+ const dbMode = (await prompt.line('How should we set up the database: existing, managed, or docker', defaultMode) ?? defaultMode).trim().toLowerCase();
1575
1741
  if (dbMode === 'existing' || dbMode === 'managed') {
1576
1742
  while (true) {
1577
- dbUrl = await promptNonEmpty(prompt, 'DATABASE_URL', existingInstance?.env.DATABASE_URL ?? `postgresql://postgres:yourpassword@localhost:5432/iranti_${instanceName}`);
1743
+ dbUrl = await promptNonEmpty(prompt, 'Database connection string (DATABASE_URL)', existingInstance?.env.DATABASE_URL ?? `postgresql://postgres:yourpassword@localhost:5432/iranti_${instanceName}`);
1578
1744
  if (!detectPlaceholder(dbUrl))
1579
1745
  break;
1580
1746
  console.log(`${warnLabel()} DATABASE_URL still looks like a placeholder. Enter a real connection string before finishing setup.`);
@@ -1587,9 +1753,9 @@ async function setupCommand(args) {
1587
1753
  console.log(`${warnLabel()} Docker is not installed or not on PATH. Choose existing or managed instead.`);
1588
1754
  continue;
1589
1755
  }
1590
- const dbHostPort = await chooseAvailablePort(prompt, 'Docker PostgreSQL host port', 5432, false);
1591
- const dbName = sanitizeIdentifier(await promptNonEmpty(prompt, 'Docker PostgreSQL database name', `iranti_${instanceName}`), `iranti_${instanceName}`);
1592
- const dbPassword = await promptRequiredSecret(prompt, 'Docker PostgreSQL password');
1756
+ const dbHostPort = await chooseAvailablePort(prompt, 'Which host port should Docker PostgreSQL use', 5432, false);
1757
+ const dbName = sanitizeIdentifier(await promptNonEmpty(prompt, 'What should the Docker PostgreSQL database be called', `iranti_${instanceName}`), `iranti_${instanceName}`);
1758
+ const dbPassword = await promptRequiredSecret(prompt, 'Set a password for Docker PostgreSQL');
1593
1759
  const containerName = sanitizeIdentifier(await promptNonEmpty(prompt, 'Docker container name', `iranti_${instanceName}_db`), `iranti_${instanceName}_db`);
1594
1760
  dbUrl = `postgresql://postgres:${dbPassword}@localhost:${dbHostPort}/${dbName}`;
1595
1761
  console.log(`${infoLabel()} Docker will be used only for PostgreSQL. Iranti itself does not require Docker once a PostgreSQL database is available.`);
@@ -1613,7 +1779,7 @@ async function setupCommand(args) {
1613
1779
  let provider = normalizeProvider(existingInstance?.env.LLM_PROVIDER ?? 'openai') ?? 'openai';
1614
1780
  while (true) {
1615
1781
  listProviderChoices(provider, existingInstance?.env ?? {});
1616
- const chosen = normalizeProvider(await promptNonEmpty(prompt, 'Default LLM provider', provider));
1782
+ const chosen = normalizeProvider(await promptNonEmpty(prompt, 'Which LLM provider should Iranti use by default', provider));
1617
1783
  if (chosen && isSupportedProvider(chosen)) {
1618
1784
  provider = chosen;
1619
1785
  break;
@@ -1636,7 +1802,7 @@ async function setupCommand(args) {
1636
1802
  let extraProvider = provider;
1637
1803
  while (true) {
1638
1804
  listProviderChoices(provider, { ...seedEnv, ...providerKeys });
1639
- const chosen = normalizeProvider(await promptNonEmpty(prompt, 'Provider to add', 'claude'));
1805
+ const chosen = normalizeProvider(await promptNonEmpty(prompt, 'Which extra provider would you like to add', 'claude'));
1640
1806
  if (!chosen) {
1641
1807
  console.log(`${warnLabel()} Provider is required.`);
1642
1808
  continue;
@@ -1671,10 +1837,10 @@ async function setupCommand(args) {
1671
1837
  const defaultProjectPath = process.cwd();
1672
1838
  let shouldBindProject = await promptYesNo(prompt, 'Bind a project folder to this instance now?', true);
1673
1839
  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);
1840
+ const projectPath = path_1.default.resolve(await promptNonEmpty(prompt, 'Which project folder should we bind', projects.length === 0 ? defaultProjectPath : process.cwd()));
1841
+ const agentId = sanitizeIdentifier(await promptNonEmpty(prompt, 'What agent id should this project use', projectAgentDefault(projectPath)), 'project_main');
1842
+ const memoryEntity = await promptNonEmpty(prompt, 'What memory entity should this project use', 'user/main');
1843
+ const claudeCode = await promptYesNo(prompt, 'Create Claude Code project files here now?', true);
1678
1844
  projects.push({
1679
1845
  path: projectPath,
1680
1846
  agentId,
@@ -1726,6 +1892,10 @@ async function setupCommand(args) {
1726
1892
  console.log(`${infoLabel()} Next steps:`);
1727
1893
  console.log(` 1. iranti run --instance ${finalResult.instanceName} --root "${finalResult.root}"`);
1728
1894
  console.log(` 2. iranti doctor --instance ${finalResult.instanceName} --root "${finalResult.root}"`);
1895
+ if (finalResult.bindings.length > 0) {
1896
+ console.log(` 3. cd "${finalResult.bindings[0].projectPath}"`);
1897
+ console.log(' 4. iranti chat');
1898
+ }
1729
1899
  }
1730
1900
  async function doctorCommand(args) {
1731
1901
  const json = hasFlag(args, 'json');
@@ -1871,6 +2041,7 @@ async function doctorCommand(args) {
1871
2041
  envFile,
1872
2042
  status: summarizeStatus(checks),
1873
2043
  checks,
2044
+ remediations: collectDoctorRemediations(checks, envSource, envFile),
1874
2045
  };
1875
2046
  if (json) {
1876
2047
  console.log(JSON.stringify(result, null, 2));
@@ -1894,6 +2065,13 @@ async function doctorCommand(args) {
1894
2065
  : failLabel('FAIL');
1895
2066
  console.log(`${marker} ${check.name} — ${check.detail}`);
1896
2067
  }
2068
+ if (result.remediations.length > 0) {
2069
+ console.log('');
2070
+ console.log('Suggested fixes:');
2071
+ for (const remediation of result.remediations) {
2072
+ console.log(` - ${remediation}`);
2073
+ }
2074
+ }
1897
2075
  if (result.status !== 'pass') {
1898
2076
  process.exitCode = 1;
1899
2077
  }
@@ -1965,46 +2143,72 @@ async function statusCommand(args) {
1965
2143
  }
1966
2144
  }
1967
2145
  async function upgradeCommand(args) {
2146
+ const runAll = hasFlag(args, 'all');
1968
2147
  const checkOnly = hasFlag(args, 'check');
1969
2148
  const dryRun = hasFlag(args, 'dry-run');
1970
2149
  const execute = hasFlag(args, 'yes');
1971
2150
  const json = hasFlag(args, 'json');
1972
- const requestedTarget = resolveUpgradeTarget(getFlag(args, 'target'));
2151
+ const requestedTargets = resolveRequestedUpgradeTargets(getFlag(args, 'target'), runAll);
1973
2152
  const context = detectUpgradeContext(args);
1974
2153
  const latestNpm = await fetchLatestNpmVersion();
1975
2154
  const latestPython = await fetchLatestPypiVersion();
1976
- const chosenTarget = chooseUpgradeTarget(requestedTarget, context);
2155
+ const statuses = buildUpgradeTargetStatuses(context, latestNpm, latestPython);
2156
+ const statusByTarget = new Map(statuses.map((status) => [status.target, status]));
2157
+ const autoSelected = requestedTargets.includes('auto')
2158
+ ? chooseUpgradeTarget('auto', context)
2159
+ : null;
2160
+ const explicitTargets = requestedTargets
2161
+ .filter((target) => target !== 'auto');
2162
+ for (const target of explicitTargets) {
2163
+ const status = statusByTarget.get(target);
2164
+ if (!runAll && !status?.available) {
2165
+ throw new Error(`Requested target '${target}' is not available in this environment.`);
2166
+ }
2167
+ }
2168
+ const selectedTargets = requestedTargets.includes('auto')
2169
+ ? (autoSelected ? [autoSelected] : [])
2170
+ : explicitTargets.filter((target) => {
2171
+ const status = statusByTarget.get(target);
2172
+ if (!status?.available)
2173
+ return false;
2174
+ if (runAll && status.blockedReason)
2175
+ return false;
2176
+ return true;
2177
+ });
1977
2178
  const commands = {
1978
2179
  npmGlobal: 'npm install -g iranti@latest',
1979
2180
  npmRepo: 'git pull --ff-only && npm install && npm run build',
1980
2181
  python: context.python?.display ?? 'python -m pip install --upgrade iranti',
1981
2182
  };
1982
2183
  const updateAvailable = {
1983
- npm: latestNpm ? compareVersions(latestNpm, context.currentVersion) > 0 : null,
1984
- python: latestPython ? compareVersions(latestPython, context.currentVersion) > 0 : null,
2184
+ npm: context.globalNpmVersion && latestNpm ? compareVersions(latestNpm, context.globalNpmVersion) > 0 : null,
2185
+ python: context.pythonVersion && latestPython ? compareVersions(latestPython, context.pythonVersion) > 0 : null,
1985
2186
  };
1986
- const plan = chosenTarget ? commandListForTarget(chosenTarget, context) : [];
1987
- let execution = null;
2187
+ const plan = selectedTargets.flatMap((target) => commandListForTarget(target, context).map((step) => step.display));
2188
+ let execution = [];
1988
2189
  let note = null;
1989
2190
  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.');
2191
+ if (selectedTargets.length === 0) {
2192
+ throw new Error('No executable upgrade path was detected. Use --target npm-global, --target npm-repo, --target python, or --all.');
1992
2193
  }
1993
2194
  if (dryRun || checkOnly) {
1994
2195
  note = 'Execution skipped because --dry-run or --check was provided.';
1995
2196
  }
1996
- else if (chosenTarget === 'npm-global' && updateAvailable.npm === false) {
1997
- note = 'npm global install is already at the latest published version.';
2197
+ else {
2198
+ execution = await executeUpgradeTargets(selectedTargets, context);
1998
2199
  }
1999
- else if (chosenTarget === 'python' && updateAvailable.python === false) {
2000
- note = 'Python client is already at the latest published version.';
2200
+ }
2201
+ else if (!checkOnly && !dryRun && !json && process.stdin.isTTY && process.stdout.isTTY) {
2202
+ const interactiveTargets = await chooseInteractiveUpgradeTargets(statuses);
2203
+ if (interactiveTargets.length === 0) {
2204
+ note = 'No upgrade targets selected.';
2001
2205
  }
2002
2206
  else {
2003
- execution = await executeUpgradeTarget(chosenTarget, context);
2207
+ execution = await executeUpgradeTargets(interactiveTargets, context);
2004
2208
  }
2005
2209
  }
2006
2210
  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.';
2211
+ note = 'Run with --yes to execute the selected upgrade path, or run plain `iranti upgrade` in a TTY to choose interactively.';
2008
2212
  }
2009
2213
  if (json) {
2010
2214
  console.log(JSON.stringify({
@@ -2018,17 +2222,21 @@ async function upgradeCommand(args) {
2018
2222
  runtimeRoot: context.runtimeRoot,
2019
2223
  runtimeInstalled: context.runtimeInstalled,
2020
2224
  repoCheckout: context.repoCheckout,
2225
+ repoDirty: context.repoDirty,
2021
2226
  globalNpmInstall: context.globalNpmInstall,
2022
2227
  globalNpmRoot: context.globalNpmRoot,
2228
+ globalNpmVersion: context.globalNpmVersion,
2023
2229
  pythonLauncher: context.python?.executable ?? null,
2230
+ pythonVersion: context.pythonVersion,
2024
2231
  },
2025
- requestedTarget,
2026
- selectedTarget: chosenTarget,
2232
+ requestedTargets,
2233
+ selectedTargets,
2027
2234
  availableTargets: context.availableTargets,
2235
+ targets: statuses,
2028
2236
  updateAvailable,
2029
2237
  commands,
2030
- plan: plan.map((step) => step.display),
2031
- action: execute && !dryRun && !checkOnly ? 'upgrade' : checkOnly ? 'check' : dryRun ? 'dry-run' : 'inspect',
2238
+ plan,
2239
+ action: execution.length > 0 ? 'upgrade' : checkOnly ? 'check' : dryRun ? 'dry-run' : 'inspect',
2032
2240
  execution,
2033
2241
  note,
2034
2242
  }, null, 2));
@@ -2040,38 +2248,43 @@ async function upgradeCommand(args) {
2040
2248
  console.log(` latest_python ${latestPython ?? '(unavailable)'}`);
2041
2249
  console.log(` package_root ${context.packageRootPath}`);
2042
2250
  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')}`);
2251
+ console.log(` repo_checkout ${context.repoCheckout ? paint('yes', 'green') : paint('no', 'gray')}${context.repoDirty ? paint(' (dirty)', 'yellow') : ''}`);
2252
+ console.log(` npm_global ${context.globalNpmInstall ? paint('yes', 'green') : paint('no', 'gray')}${context.globalNpmVersion ? ` (${context.globalNpmVersion})` : ''}`);
2253
+ console.log(` python ${context.python?.executable ?? paint('not found', 'yellow')}${context.pythonVersion ? ` (${context.pythonVersion})` : ''}`);
2046
2254
  console.log('');
2047
- if (chosenTarget) {
2048
- console.log(` selected_target ${paint(chosenTarget, 'cyan')}${requestedTarget === 'auto' ? paint(' (auto)', 'gray') : ''}`);
2255
+ if (selectedTargets.length > 0) {
2256
+ console.log(` selected_target${selectedTargets.length > 1 ? 's' : ''} ${paint(selectedTargets.join(', '), 'cyan')}${requestedTargets.includes('auto') ? paint(' (auto)', 'gray') : ''}`);
2049
2257
  console.log(' plan');
2050
2258
  for (const step of plan) {
2051
- console.log(` - ${step.display}`);
2259
+ console.log(` - ${step}`);
2052
2260
  }
2053
2261
  }
2054
2262
  else {
2055
- console.log(` selected_target ${paint('none', 'yellow')}`);
2263
+ console.log(` selected_targets ${paint('none', 'yellow')}`);
2056
2264
  console.log(' plan No executable upgrade path detected automatically.');
2057
2265
  }
2058
2266
  console.log('');
2059
2267
  console.log(` npm global ${commands.npmGlobal}`);
2060
2268
  console.log(` npm repo ${commands.npmRepo}`);
2061
2269
  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');
2270
+ if (execution.length > 0) {
2068
2271
  console.log('');
2069
- console.log(`${okLabel()} Upgrade completed for ${execution.target}.`);
2070
- console.log(`${marker} ${execution.verification.detail}`);
2272
+ for (const result of execution) {
2273
+ const marker = result.verification.status === 'pass'
2274
+ ? okLabel('PASS')
2275
+ : result.verification.status === 'warn'
2276
+ ? warnLabel('WARN')
2277
+ : failLabel('FAIL');
2278
+ console.log(`${okLabel()} Upgrade completed for ${result.target}.`);
2279
+ console.log(`${marker} ${result.verification.detail}`);
2280
+ }
2071
2281
  const { envFile } = resolveDoctorEnvTarget(args);
2072
2282
  if (envFile) {
2073
2283
  console.log(`${infoLabel()} Run \`iranti doctor\` to verify the active environment after the package upgrade.`);
2074
2284
  }
2285
+ if (execution.some((result) => result.target === 'npm-global')) {
2286
+ 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.`);
2287
+ }
2075
2288
  return;
2076
2289
  }
2077
2290
  if (note) {
@@ -2266,14 +2479,15 @@ async function configureInstanceCommand(args) {
2266
2479
  let clearProviderKey = hasFlag(args, 'clear-provider-key');
2267
2480
  if (hasFlag(args, 'interactive')) {
2268
2481
  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');
2482
+ portRaw = await prompt.line('Which API port should this instance use', portRaw ?? env.IRANTI_PORT);
2483
+ dbUrl = await prompt.line('Database connection string (DATABASE_URL)', dbUrl ?? env.DATABASE_URL);
2484
+ providerInput = await prompt.line('Which LLM provider should this instance use', providerInput ?? env.LLM_PROVIDER ?? 'mock');
2272
2485
  const interactiveProvider = normalizeProvider(providerInput ?? env.LLM_PROVIDER ?? 'mock');
2273
- if (providerKeyEnv(interactiveProvider)) {
2274
- providerKey = await prompt.secret(`${providerKeyEnv(interactiveProvider)}`, providerKey ?? env[providerKeyEnv(interactiveProvider)]);
2486
+ const interactiveProviderEnvKey = providerKeyEnv(interactiveProvider);
2487
+ if (interactiveProvider && interactiveProviderEnvKey) {
2488
+ providerKey = await prompt.secret(`Enter the ${providerDisplayName(interactiveProvider)} API key`, providerKey ?? env[interactiveProviderEnvKey]);
2275
2489
  }
2276
- apiKey = await prompt.secret('IRANTI_API_KEY', apiKey ?? env.IRANTI_API_KEY);
2490
+ apiKey = await prompt.secret('Iranti API key', apiKey ?? env.IRANTI_API_KEY);
2277
2491
  });
2278
2492
  clearProviderKey = false;
2279
2493
  }
@@ -2330,6 +2544,7 @@ async function configureInstanceCommand(args) {
2330
2544
  if (providerKey) {
2331
2545
  console.log(` provider ${result.provider}`);
2332
2546
  }
2547
+ console.log(`${infoLabel()} Next: iranti doctor --instance ${name}${scope ? ` --scope ${scope}` : ''}`);
2333
2548
  }
2334
2549
  async function configureProjectCommand(args) {
2335
2550
  const projectPath = path_1.default.resolve(args.positionals[0] ?? process.cwd());
@@ -2343,11 +2558,11 @@ async function configureProjectCommand(args) {
2343
2558
  let explicitMemoryEntity = getFlag(args, 'memory-entity');
2344
2559
  if (hasFlag(args, 'interactive')) {
2345
2560
  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');
2561
+ instanceName = await prompt.line('Which instance should this project use', instanceName);
2562
+ explicitUrl = await prompt.line('What Iranti URL should this project talk to', explicitUrl ?? existing.IRANTI_URL);
2563
+ explicitApiKey = await prompt.secret('What API key should this project use', explicitApiKey ?? existing.IRANTI_API_KEY);
2564
+ explicitAgentId = await prompt.line('What agent id should this project use', explicitAgentId ?? existing.IRANTI_AGENT_ID ?? 'my_agent');
2565
+ explicitMemoryEntity = await prompt.line('What memory entity should this project use', explicitMemoryEntity ?? existing.IRANTI_MEMORY_ENTITY ?? 'user/main');
2351
2566
  });
2352
2567
  }
2353
2568
  let instanceEnvFile = existing.IRANTI_INSTANCE_ENV;
@@ -2395,6 +2610,7 @@ async function configureProjectCommand(args) {
2395
2610
  if (updates.IRANTI_INSTANCE) {
2396
2611
  console.log(` instance ${updates.IRANTI_INSTANCE}`);
2397
2612
  }
2613
+ console.log(`${infoLabel()} Next: iranti doctor${updates.IRANTI_INSTANCE ? ` --instance ${updates.IRANTI_INSTANCE}` : ''}`);
2398
2614
  }
2399
2615
  async function authCreateKeyCommand(args) {
2400
2616
  const instanceName = getFlag(args, 'instance');
@@ -2462,6 +2678,7 @@ async function authCreateKeyCommand(args) {
2462
2678
  if (projectPath) {
2463
2679
  console.log(` project ${path_1.default.resolve(projectPath)}`);
2464
2680
  }
2681
+ console.log(`${infoLabel()} Next: iranti doctor --instance ${instanceName}`);
2465
2682
  process.exit(0);
2466
2683
  }
2467
2684
  async function authListKeysCommand(args) {
@@ -2565,7 +2782,7 @@ Project-level:
2565
2782
  Diagnostics:
2566
2783
  iranti doctor [--instance <name>] [--scope user|system] [--env <file>] [--json]
2567
2784
  iranti status [--scope user|system] [--json]
2568
- iranti upgrade [--check] [--dry-run] [--yes] [--target auto|npm-global|npm-repo|python] [--json]
2785
+ iranti upgrade [--check] [--dry-run] [--yes] [--all] [--target auto|npm-global|npm-repo|python[,python]] [--json]
2569
2786
  iranti chat [--agent <agent-id>] [--provider <provider>] [--model <model>]
2570
2787
  iranti resolve [--dir <escalation-dir>]
2571
2788
 
@@ -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.3',
147
+ version: '0.2.5',
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.3',
18
+ version: '0.2.5',
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.3',
42
+ version: '0.2.5',
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.3',
64
+ version: '0.2.5',
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.3' },
85
+ valueRaw: { version: '0.2.5' },
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.3',
98
+ seedVersion: '0.2.5',
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.3',
111
+ version: '0.2.5',
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.3',
159
+ version: '0.2.5',
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.3',
190
+ version: '0.2.5',
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.3',
204
+ version: '0.2.5',
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.3',
240
+ version: '0.2.5',
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.3',
72
+ version: '0.2.5',
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.3",
3
+ "version": "0.2.5",
4
4
  "description": "Memory infrastructure for multi-agent AI systems",
5
5
  "main": "dist/src/sdk/index.js",
6
6
  "files": [