iranti 0.2.2 → 0.2.3

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.
Files changed (51) hide show
  1. package/README.md +38 -11
  2. package/dist/scripts/iranti-cli.js +513 -38
  3. package/dist/scripts/iranti-mcp.js +1 -1
  4. package/dist/scripts/seed.js +10 -10
  5. package/dist/src/api/server.js +1 -1
  6. package/dist/src/chat/index.d.ts +8 -0
  7. package/dist/src/chat/index.d.ts.map +1 -0
  8. package/dist/src/chat/index.js +565 -0
  9. package/dist/src/chat/index.js.map +1 -0
  10. package/dist/src/lib/llm.d.ts +1 -0
  11. package/dist/src/lib/llm.d.ts.map +1 -1
  12. package/dist/src/lib/llm.js +4 -0
  13. package/dist/src/lib/llm.js.map +1 -1
  14. package/dist/src/lib/router.d.ts.map +1 -1
  15. package/dist/src/lib/router.js +46 -42
  16. package/dist/src/lib/router.js.map +1 -1
  17. package/dist/src/librarian/contextual-conflicts.d.ts +9 -0
  18. package/dist/src/librarian/contextual-conflicts.d.ts.map +1 -0
  19. package/dist/src/librarian/contextual-conflicts.js +243 -0
  20. package/dist/src/librarian/contextual-conflicts.js.map +1 -0
  21. package/dist/src/librarian/index.d.ts.map +1 -1
  22. package/dist/src/librarian/index.js +50 -0
  23. package/dist/src/librarian/index.js.map +1 -1
  24. package/dist/src/library/backends/chromaBackend.d.ts +27 -0
  25. package/dist/src/library/backends/chromaBackend.d.ts.map +1 -0
  26. package/dist/src/library/backends/chromaBackend.js +99 -0
  27. package/dist/src/library/backends/chromaBackend.js.map +1 -0
  28. package/dist/src/library/backends/index.d.ts +15 -0
  29. package/dist/src/library/backends/index.d.ts.map +1 -0
  30. package/dist/src/library/backends/index.js +39 -0
  31. package/dist/src/library/backends/index.js.map +1 -0
  32. package/dist/src/library/backends/pgvectorBackend.d.ts +8 -0
  33. package/dist/src/library/backends/pgvectorBackend.d.ts.map +1 -0
  34. package/dist/src/library/backends/pgvectorBackend.js +128 -0
  35. package/dist/src/library/backends/pgvectorBackend.js.map +1 -0
  36. package/dist/src/library/backends/qdrantBackend.d.ts +21 -0
  37. package/dist/src/library/backends/qdrantBackend.d.ts.map +1 -0
  38. package/dist/src/library/backends/qdrantBackend.js +107 -0
  39. package/dist/src/library/backends/qdrantBackend.js.map +1 -0
  40. package/dist/src/library/queries.d.ts.map +1 -1
  41. package/dist/src/library/queries.js +105 -123
  42. package/dist/src/library/queries.js.map +1 -1
  43. package/dist/src/library/vectorBackend.d.ts +19 -0
  44. package/dist/src/library/vectorBackend.d.ts.map +1 -0
  45. package/dist/src/library/vectorBackend.js +3 -0
  46. package/dist/src/library/vectorBackend.js.map +1 -0
  47. package/dist/src/resolutionist/index.d.ts +8 -0
  48. package/dist/src/resolutionist/index.d.ts.map +1 -0
  49. package/dist/src/resolutionist/index.js +265 -0
  50. package/dist/src/resolutionist/index.js.map +1 -0
  51. package/package.json +2 -1
@@ -6,6 +6,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const promises_1 = __importDefault(require("fs/promises"));
9
+ const https_1 = __importDefault(require("https"));
9
10
  const os_1 = __importDefault(require("os"));
10
11
  const path_1 = __importDefault(require("path"));
11
12
  const child_process_1 = require("child_process");
@@ -14,6 +15,10 @@ const stream_1 = require("stream");
14
15
  const net_1 = __importDefault(require("net"));
15
16
  const client_1 = require("../src/library/client");
16
17
  const apiKeys_1 = require("../src/security/apiKeys");
18
+ const escalationPaths_1 = require("../src/lib/escalationPaths");
19
+ const resolutionist_1 = require("../src/resolutionist");
20
+ const chat_1 = require("../src/chat");
21
+ const backends_1 = require("../src/library/backends");
17
22
  const PROVIDER_ENV_KEYS = {
18
23
  mock: null,
19
24
  ollama: null,
@@ -314,6 +319,13 @@ function formatEnvValue(value) {
314
319
  ? `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
315
320
  : value;
316
321
  }
322
+ function vectorBackendUrl(name, env) {
323
+ if (name === 'qdrant')
324
+ return env.IRANTI_QDRANT_URL ?? null;
325
+ if (name === 'chroma')
326
+ return env.IRANTI_CHROMA_URL ?? 'http://localhost:8000';
327
+ return null;
328
+ }
317
329
  async function upsertEnvFile(filePath, updates) {
318
330
  const existingRaw = fs_1.default.existsSync(filePath) ? await promises_1.default.readFile(filePath, 'utf-8') : '';
319
331
  const lines = existingRaw.length > 0 ? existingRaw.split(/\r?\n/) : [];
@@ -1003,6 +1015,337 @@ function summarizeStatus(checks) {
1003
1015
  return 'warn';
1004
1016
  return 'pass';
1005
1017
  }
1018
+ function resolveDoctorEnvTarget(args) {
1019
+ const scope = normalizeScope(getFlag(args, 'scope'));
1020
+ const instanceName = getFlag(args, 'instance');
1021
+ const explicitEnv = getFlag(args, 'env');
1022
+ const cwd = process.cwd();
1023
+ if (explicitEnv) {
1024
+ return {
1025
+ envFile: path_1.default.resolve(explicitEnv),
1026
+ envSource: 'explicit-env',
1027
+ };
1028
+ }
1029
+ if (instanceName) {
1030
+ const root = resolveInstallRoot(args, scope);
1031
+ return {
1032
+ envFile: path_1.default.join(root, 'instances', instanceName, '.env'),
1033
+ envSource: `instance:${instanceName}`,
1034
+ };
1035
+ }
1036
+ const repoEnv = path_1.default.join(cwd, '.env');
1037
+ const projectEnv = path_1.default.join(cwd, '.env.iranti');
1038
+ if (fs_1.default.existsSync(repoEnv)) {
1039
+ return { envFile: repoEnv, envSource: 'repo' };
1040
+ }
1041
+ if (fs_1.default.existsSync(projectEnv)) {
1042
+ return { envFile: projectEnv, envSource: 'project-binding' };
1043
+ }
1044
+ return { envFile: null, envSource: 'repo' };
1045
+ }
1046
+ function resolveUpgradeTarget(raw) {
1047
+ if (!raw)
1048
+ return 'auto';
1049
+ const normalized = raw.trim().toLowerCase();
1050
+ if (normalized === 'auto' || normalized === 'npm-global' || normalized === 'npm-repo' || normalized === 'python') {
1051
+ return normalized;
1052
+ }
1053
+ throw new Error(`Invalid --target '${raw}'. Use auto, npm-global, npm-repo, or python.`);
1054
+ }
1055
+ function parseVersion(value) {
1056
+ if (!value)
1057
+ return [0];
1058
+ const match = value.trim().match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
1059
+ if (!match)
1060
+ return [0];
1061
+ return [
1062
+ Number.parseInt(match[1] ?? '0', 10),
1063
+ Number.parseInt(match[2] ?? '0', 10),
1064
+ Number.parseInt(match[3] ?? '0', 10),
1065
+ ];
1066
+ }
1067
+ function compareVersions(left, right) {
1068
+ const a = parseVersion(left);
1069
+ const b = parseVersion(right);
1070
+ const limit = Math.max(a.length, b.length, 3);
1071
+ for (let i = 0; i < limit; i++) {
1072
+ const av = a[i] ?? 0;
1073
+ const bv = b[i] ?? 0;
1074
+ if (av > bv)
1075
+ return 1;
1076
+ if (av < bv)
1077
+ return -1;
1078
+ }
1079
+ return 0;
1080
+ }
1081
+ function normalizePathForCompare(value) {
1082
+ return path_1.default.resolve(value).replace(/\\/g, '/').toLowerCase();
1083
+ }
1084
+ function isPathInside(parentDir, childDir) {
1085
+ const parent = normalizePathForCompare(parentDir);
1086
+ const child = normalizePathForCompare(childDir);
1087
+ return child === parent || child.startsWith(`${parent}/`);
1088
+ }
1089
+ function resolveSpawnExecutable(executable) {
1090
+ if (process.platform !== 'win32')
1091
+ return executable;
1092
+ if (executable === 'npm')
1093
+ return 'npm.cmd';
1094
+ if (executable === 'npx')
1095
+ return 'npx.cmd';
1096
+ return executable;
1097
+ }
1098
+ function runCommandCapture(executable, args, cwd) {
1099
+ const proc = (0, child_process_1.spawnSync)(resolveSpawnExecutable(executable), args, {
1100
+ cwd,
1101
+ encoding: 'utf8',
1102
+ stdio: ['ignore', 'pipe', 'pipe'],
1103
+ });
1104
+ return {
1105
+ status: proc.status,
1106
+ stdout: proc.stdout ?? '',
1107
+ stderr: proc.stderr ?? '',
1108
+ };
1109
+ }
1110
+ function runCommandInteractive(step) {
1111
+ const proc = (0, child_process_1.spawnSync)(resolveSpawnExecutable(step.executable), step.args, {
1112
+ cwd: step.cwd,
1113
+ stdio: 'inherit',
1114
+ });
1115
+ return proc.status;
1116
+ }
1117
+ function detectPythonLauncher() {
1118
+ const candidates = process.platform === 'win32'
1119
+ ? [
1120
+ { label: 'python', display: 'python -m pip install --upgrade iranti', executable: 'python', args: ['-m', 'pip', 'install', '--upgrade', 'iranti'] },
1121
+ { label: 'py', display: 'py -3 -m pip install --upgrade iranti', executable: 'py', args: ['-3', '-m', 'pip', 'install', '--upgrade', 'iranti'] },
1122
+ ]
1123
+ : [
1124
+ { label: 'python3', display: 'python3 -m pip install --upgrade iranti', executable: 'python3', args: ['-m', 'pip', 'install', '--upgrade', 'iranti'] },
1125
+ { label: 'python', display: 'python -m pip install --upgrade iranti', executable: 'python', args: ['-m', 'pip', 'install', '--upgrade', 'iranti'] },
1126
+ ];
1127
+ for (const candidate of candidates) {
1128
+ const probeArgs = candidate.args[0] === '-3' ? ['-3', '--version'] : ['--version'];
1129
+ const probe = runCommandCapture(candidate.executable, probeArgs);
1130
+ if (probe.status === 0)
1131
+ return candidate;
1132
+ }
1133
+ return null;
1134
+ }
1135
+ function detectGlobalNpmRoot() {
1136
+ const proc = runCommandCapture('npm', ['root', '-g']);
1137
+ if (proc.status !== 0)
1138
+ return null;
1139
+ const value = proc.stdout.trim();
1140
+ return value ? path_1.default.resolve(value) : null;
1141
+ }
1142
+ function readJsonFile(filePath) {
1143
+ if (!fs_1.default.existsSync(filePath))
1144
+ return null;
1145
+ try {
1146
+ return JSON.parse(fs_1.default.readFileSync(filePath, 'utf8'));
1147
+ }
1148
+ catch {
1149
+ return null;
1150
+ }
1151
+ }
1152
+ function httpsJson(url, headers = {}) {
1153
+ return new Promise((resolve, reject) => {
1154
+ const request = https_1.default.get(url, { headers }, (response) => {
1155
+ const statusCode = response.statusCode ?? 0;
1156
+ if (statusCode >= 300 && statusCode < 400 && response.headers.location) {
1157
+ response.resume();
1158
+ const redirect = new URL(response.headers.location, url).toString();
1159
+ httpsJson(redirect, headers).then(resolve).catch(reject);
1160
+ return;
1161
+ }
1162
+ if (statusCode < 200 || statusCode >= 300) {
1163
+ response.resume();
1164
+ reject(new Error(`HTTP ${statusCode} from ${url}`));
1165
+ return;
1166
+ }
1167
+ let raw = '';
1168
+ response.setEncoding('utf8');
1169
+ response.on('data', (chunk) => {
1170
+ raw += chunk;
1171
+ });
1172
+ response.on('end', () => {
1173
+ try {
1174
+ resolve(JSON.parse(raw));
1175
+ }
1176
+ catch (error) {
1177
+ reject(error);
1178
+ }
1179
+ });
1180
+ });
1181
+ request.setTimeout(5000, () => {
1182
+ request.destroy(new Error(`Timed out fetching ${url}`));
1183
+ });
1184
+ request.on('error', reject);
1185
+ });
1186
+ }
1187
+ async function fetchLatestNpmVersion() {
1188
+ try {
1189
+ const payload = await httpsJson('https://registry.npmjs.org/iranti/latest');
1190
+ return typeof payload?.version === 'string' ? payload.version : null;
1191
+ }
1192
+ catch {
1193
+ return null;
1194
+ }
1195
+ }
1196
+ async function fetchLatestPypiVersion() {
1197
+ try {
1198
+ const payload = await httpsJson('https://pypi.org/pypi/iranti/json');
1199
+ return typeof payload?.info?.version === 'string' ? payload.info.version : null;
1200
+ }
1201
+ catch {
1202
+ return null;
1203
+ }
1204
+ }
1205
+ function repoUpgradeCommands(root) {
1206
+ return [
1207
+ { label: 'git pull', display: 'git pull --ff-only', executable: 'git', args: ['pull', '--ff-only'], cwd: root },
1208
+ { label: 'npm install', display: 'npm install', executable: 'npm', args: ['install'], cwd: root },
1209
+ { label: 'npm build', display: 'npm run build', executable: 'npm', args: ['run', 'build'], cwd: root },
1210
+ ];
1211
+ }
1212
+ function repoIsDirty(root) {
1213
+ const proc = runCommandCapture('git', ['status', '--porcelain'], root);
1214
+ return proc.status === 0 && proc.stdout.trim().length > 0;
1215
+ }
1216
+ function detectUpgradeContext(args) {
1217
+ const scope = normalizeScope(getFlag(args, 'scope'));
1218
+ const packageRootPath = packageRoot();
1219
+ const runtimeRoot = resolveInstallRoot(args, scope);
1220
+ const runtimeInstalled = fs_1.default.existsSync(path_1.default.join(runtimeRoot, 'install.json'));
1221
+ const repoCheckout = fs_1.default.existsSync(path_1.default.join(packageRootPath, '.git'));
1222
+ const globalNpmRoot = detectGlobalNpmRoot();
1223
+ const globalNpmInstall = globalNpmRoot !== null && isPathInside(globalNpmRoot, packageRootPath);
1224
+ const python = detectPythonLauncher();
1225
+ const availableTargets = [];
1226
+ if (globalNpmInstall)
1227
+ availableTargets.push('npm-global');
1228
+ if (repoCheckout)
1229
+ availableTargets.push('npm-repo');
1230
+ if (python)
1231
+ availableTargets.push('python');
1232
+ return {
1233
+ packageRootPath,
1234
+ currentVersion: getPackageVersion(),
1235
+ runtimeRoot,
1236
+ runtimeInstalled,
1237
+ repoCheckout,
1238
+ globalNpmInstall,
1239
+ globalNpmRoot,
1240
+ python,
1241
+ availableTargets,
1242
+ };
1243
+ }
1244
+ function chooseUpgradeTarget(requested, context) {
1245
+ if (requested !== 'auto') {
1246
+ if (!context.availableTargets.includes(requested)) {
1247
+ throw new Error(`Requested target '${requested}' is not available in this environment.`);
1248
+ }
1249
+ return requested;
1250
+ }
1251
+ if (context.repoCheckout)
1252
+ return 'npm-repo';
1253
+ if (context.globalNpmInstall)
1254
+ return 'npm-global';
1255
+ if (context.python)
1256
+ return 'python';
1257
+ return null;
1258
+ }
1259
+ function commandListForTarget(target, context) {
1260
+ if (target === 'npm-repo') {
1261
+ return repoUpgradeCommands(context.packageRootPath);
1262
+ }
1263
+ if (target === 'npm-global') {
1264
+ return [{
1265
+ label: 'npm global',
1266
+ display: 'npm install -g iranti@latest',
1267
+ executable: 'npm',
1268
+ args: ['install', '-g', 'iranti@latest'],
1269
+ cwd: context.packageRootPath,
1270
+ }];
1271
+ }
1272
+ if (!context.python) {
1273
+ throw new Error('Python launcher not found for python upgrade target.');
1274
+ }
1275
+ return [context.python];
1276
+ }
1277
+ async function refreshInstallMetaVersion(runtimeRoot, version) {
1278
+ const installMetaPath = path_1.default.join(runtimeRoot, 'install.json');
1279
+ const meta = readJsonFile(installMetaPath);
1280
+ if (!meta)
1281
+ return;
1282
+ await writeJson(installMetaPath, {
1283
+ ...meta,
1284
+ version,
1285
+ upgradedAt: new Date().toISOString(),
1286
+ });
1287
+ }
1288
+ function verifyGlobalNpmInstall() {
1289
+ const proc = runCommandCapture('npm', ['list', '-g', 'iranti', '--depth=0', '--json']);
1290
+ if (proc.status !== 0) {
1291
+ return {
1292
+ status: 'warn',
1293
+ detail: 'npm global upgrade finished, but `npm list -g iranti` did not return cleanly.',
1294
+ };
1295
+ }
1296
+ try {
1297
+ const payload = JSON.parse(proc.stdout);
1298
+ const version = payload?.dependencies?.iranti?.version;
1299
+ return typeof version === 'string'
1300
+ ? { status: 'pass', detail: `npm global install reports iranti@${version}.` }
1301
+ : { status: 'warn', detail: 'npm global upgrade finished, but installed version could not be confirmed.' };
1302
+ }
1303
+ catch {
1304
+ return {
1305
+ status: 'warn',
1306
+ detail: 'npm global upgrade finished, but version verification output was unreadable.',
1307
+ };
1308
+ }
1309
+ }
1310
+ 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()}.` }
1322
+ : { status: 'warn', detail: 'Python upgrade finished, but installed version could not be confirmed.' };
1323
+ }
1324
+ async function executeUpgradeTarget(target, context) {
1325
+ if (target === 'npm-repo' && repoIsDirty(context.packageRootPath)) {
1326
+ throw new Error('Repository worktree is dirty. Commit or stash changes before running `iranti upgrade --target npm-repo --yes`.');
1327
+ }
1328
+ const commands = commandListForTarget(target, context);
1329
+ const steps = [];
1330
+ for (const command of commands) {
1331
+ console.log(`${infoLabel()} ${command.display}`);
1332
+ const status = runCommandInteractive(command);
1333
+ steps.push({ label: command.label, command: command.display });
1334
+ if (status !== 0) {
1335
+ throw new Error(`Upgrade step failed: ${command.display}`);
1336
+ }
1337
+ }
1338
+ const verification = target === 'npm-global'
1339
+ ? verifyGlobalNpmInstall()
1340
+ : target === 'python'
1341
+ ? verifyPythonInstall(commands[0])
1342
+ : { status: 'pass', detail: 'Repository refresh completed and build succeeded.' };
1343
+ if (context.runtimeInstalled && verification.status !== 'fail') {
1344
+ const nextVersion = target === 'python' ? context.currentVersion : (await fetchLatestNpmVersion()) ?? context.currentVersion;
1345
+ await refreshInstallMetaVersion(context.runtimeRoot, nextVersion);
1346
+ }
1347
+ return { target, steps, verification };
1348
+ }
1006
1349
  async function listProviderKeysCommand(args) {
1007
1350
  const target = await resolveProviderKeyTarget(args);
1008
1351
  const currentProvider = normalizeProvider(target.env.LLM_PROVIDER ?? 'mock');
@@ -1385,34 +1728,8 @@ async function setupCommand(args) {
1385
1728
  console.log(` 2. iranti doctor --instance ${finalResult.instanceName} --root "${finalResult.root}"`);
1386
1729
  }
1387
1730
  async function doctorCommand(args) {
1388
- const scope = normalizeScope(getFlag(args, 'scope'));
1389
- const instanceName = getFlag(args, 'instance');
1390
- const explicitEnv = getFlag(args, 'env');
1391
1731
  const json = hasFlag(args, 'json');
1392
- const cwd = process.cwd();
1393
- let envFile = null;
1394
- let envSource = 'repo';
1395
- if (explicitEnv) {
1396
- envFile = path_1.default.resolve(explicitEnv);
1397
- envSource = 'explicit-env';
1398
- }
1399
- else if (instanceName) {
1400
- const root = resolveInstallRoot(args, scope);
1401
- envFile = path_1.default.join(root, 'instances', instanceName, '.env');
1402
- envSource = `instance:${instanceName}`;
1403
- }
1404
- else {
1405
- const repoEnv = path_1.default.join(cwd, '.env');
1406
- const projectEnv = path_1.default.join(cwd, '.env.iranti');
1407
- if (fs_1.default.existsSync(repoEnv)) {
1408
- envFile = repoEnv;
1409
- envSource = 'repo';
1410
- }
1411
- else if (fs_1.default.existsSync(projectEnv)) {
1412
- envFile = projectEnv;
1413
- envSource = 'project-binding';
1414
- }
1415
- }
1732
+ const { envFile, envSource } = resolveDoctorEnvTarget(args);
1416
1733
  const checks = [];
1417
1734
  const version = getPackageVersion();
1418
1735
  checks.push({
@@ -1507,6 +1824,46 @@ async function doctorCommand(args) {
1507
1824
  detail: `LLM_PROVIDER=${provider}`,
1508
1825
  });
1509
1826
  checks.push(detectProviderKey(provider, env));
1827
+ try {
1828
+ const backendName = (0, backends_1.resolveVectorBackendName)({
1829
+ vectorBackend: env.IRANTI_VECTOR_BACKEND,
1830
+ qdrantUrl: env.IRANTI_QDRANT_URL,
1831
+ qdrantApiKey: env.IRANTI_QDRANT_API_KEY,
1832
+ qdrantCollection: env.IRANTI_QDRANT_COLLECTION,
1833
+ chromaUrl: env.IRANTI_CHROMA_URL,
1834
+ chromaCollection: env.IRANTI_CHROMA_COLLECTION,
1835
+ chromaTenant: env.IRANTI_CHROMA_TENANT,
1836
+ chromaDatabase: env.IRANTI_CHROMA_DATABASE,
1837
+ chromaToken: env.IRANTI_CHROMA_TOKEN,
1838
+ });
1839
+ const backend = (0, backends_1.createVectorBackend)({
1840
+ vectorBackend: backendName,
1841
+ qdrantUrl: env.IRANTI_QDRANT_URL,
1842
+ qdrantApiKey: env.IRANTI_QDRANT_API_KEY,
1843
+ qdrantCollection: env.IRANTI_QDRANT_COLLECTION,
1844
+ chromaUrl: env.IRANTI_CHROMA_URL,
1845
+ chromaCollection: env.IRANTI_CHROMA_COLLECTION,
1846
+ chromaTenant: env.IRANTI_CHROMA_TENANT,
1847
+ chromaDatabase: env.IRANTI_CHROMA_DATABASE,
1848
+ chromaToken: env.IRANTI_CHROMA_TOKEN,
1849
+ });
1850
+ const reachable = await backend.ping();
1851
+ const url = vectorBackendUrl(backendName, env);
1852
+ checks.push({
1853
+ name: 'vector backend',
1854
+ status: reachable ? 'pass' : 'warn',
1855
+ detail: url
1856
+ ? `${backendName} (${url}) is ${reachable ? 'reachable' : 'unreachable'}`
1857
+ : `${backendName} is ${reachable ? 'reachable' : 'unreachable'}`,
1858
+ });
1859
+ }
1860
+ catch (error) {
1861
+ checks.push({
1862
+ name: 'vector backend',
1863
+ status: 'fail',
1864
+ detail: error instanceof Error ? error.message : String(error),
1865
+ });
1866
+ }
1510
1867
  }
1511
1868
  const result = {
1512
1869
  version,
@@ -1608,28 +1965,119 @@ async function statusCommand(args) {
1608
1965
  }
1609
1966
  }
1610
1967
  async function upgradeCommand(args) {
1968
+ const checkOnly = hasFlag(args, 'check');
1969
+ const dryRun = hasFlag(args, 'dry-run');
1970
+ const execute = hasFlag(args, 'yes');
1611
1971
  const json = hasFlag(args, 'json');
1612
- const version = getPackageVersion();
1972
+ const requestedTarget = resolveUpgradeTarget(getFlag(args, 'target'));
1973
+ const context = detectUpgradeContext(args);
1974
+ const latestNpm = await fetchLatestNpmVersion();
1975
+ const latestPython = await fetchLatestPypiVersion();
1976
+ const chosenTarget = chooseUpgradeTarget(requestedTarget, context);
1613
1977
  const commands = {
1614
1978
  npmGlobal: 'npm install -g iranti@latest',
1615
- npmRepo: 'git pull && npm install && npm run build',
1616
- python: 'pip install --upgrade iranti',
1979
+ npmRepo: 'git pull --ff-only && npm install && npm run build',
1980
+ python: context.python?.display ?? 'python -m pip install --upgrade iranti',
1617
1981
  };
1982
+ const updateAvailable = {
1983
+ npm: latestNpm ? compareVersions(latestNpm, context.currentVersion) > 0 : null,
1984
+ python: latestPython ? compareVersions(latestPython, context.currentVersion) > 0 : null,
1985
+ };
1986
+ const plan = chosenTarget ? commandListForTarget(chosenTarget, context) : [];
1987
+ let execution = null;
1988
+ let note = null;
1989
+ 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.');
1992
+ }
1993
+ if (dryRun || checkOnly) {
1994
+ note = 'Execution skipped because --dry-run or --check was provided.';
1995
+ }
1996
+ else if (chosenTarget === 'npm-global' && updateAvailable.npm === false) {
1997
+ note = 'npm global install is already at the latest published version.';
1998
+ }
1999
+ else if (chosenTarget === 'python' && updateAvailable.python === false) {
2000
+ note = 'Python client is already at the latest published version.';
2001
+ }
2002
+ else {
2003
+ execution = await executeUpgradeTarget(chosenTarget, context);
2004
+ }
2005
+ }
2006
+ 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.';
2008
+ }
1618
2009
  if (json) {
1619
2010
  console.log(JSON.stringify({
1620
- version,
1621
- note: 'iranti upgrade currently prints recommended upgrade commands. It does not self-update in place yet.',
2011
+ currentVersion: context.currentVersion,
2012
+ latest: {
2013
+ npm: latestNpm,
2014
+ python: latestPython,
2015
+ },
2016
+ install: {
2017
+ packageRoot: context.packageRootPath,
2018
+ runtimeRoot: context.runtimeRoot,
2019
+ runtimeInstalled: context.runtimeInstalled,
2020
+ repoCheckout: context.repoCheckout,
2021
+ globalNpmInstall: context.globalNpmInstall,
2022
+ globalNpmRoot: context.globalNpmRoot,
2023
+ pythonLauncher: context.python?.executable ?? null,
2024
+ },
2025
+ requestedTarget,
2026
+ selectedTarget: chosenTarget,
2027
+ availableTargets: context.availableTargets,
2028
+ updateAvailable,
1622
2029
  commands,
2030
+ plan: plan.map((step) => step.display),
2031
+ action: execute && !dryRun && !checkOnly ? 'upgrade' : checkOnly ? 'check' : dryRun ? 'dry-run' : 'inspect',
2032
+ execution,
2033
+ note,
1623
2034
  }, null, 2));
1624
2035
  return;
1625
2036
  }
1626
2037
  console.log(bold('Iranti upgrade'));
1627
- console.log(` current_version ${version}`);
1628
- console.log(' note This command does not self-update yet. Use the appropriate package-manager path below.');
2038
+ console.log(` current_version ${context.currentVersion}`);
2039
+ console.log(` latest_npm ${latestNpm ?? '(unavailable)'}`);
2040
+ console.log(` latest_python ${latestPython ?? '(unavailable)'}`);
2041
+ console.log(` package_root ${context.packageRootPath}`);
2042
+ 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')}`);
2046
+ console.log('');
2047
+ if (chosenTarget) {
2048
+ console.log(` selected_target ${paint(chosenTarget, 'cyan')}${requestedTarget === 'auto' ? paint(' (auto)', 'gray') : ''}`);
2049
+ console.log(' plan');
2050
+ for (const step of plan) {
2051
+ console.log(` - ${step.display}`);
2052
+ }
2053
+ }
2054
+ else {
2055
+ console.log(` selected_target ${paint('none', 'yellow')}`);
2056
+ console.log(' plan No executable upgrade path detected automatically.');
2057
+ }
1629
2058
  console.log('');
1630
2059
  console.log(` npm global ${commands.npmGlobal}`);
1631
2060
  console.log(` npm repo ${commands.npmRepo}`);
1632
2061
  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');
2068
+ console.log('');
2069
+ console.log(`${okLabel()} Upgrade completed for ${execution.target}.`);
2070
+ console.log(`${marker} ${execution.verification.detail}`);
2071
+ const { envFile } = resolveDoctorEnvTarget(args);
2072
+ if (envFile) {
2073
+ console.log(`${infoLabel()} Run \`iranti doctor\` to verify the active environment after the package upgrade.`);
2074
+ }
2075
+ return;
2076
+ }
2077
+ if (note) {
2078
+ console.log('');
2079
+ console.log(`${infoLabel()} ${note}`);
2080
+ }
1633
2081
  }
1634
2082
  async function installCommand(args) {
1635
2083
  const scope = normalizeScope(getFlag(args, 'scope'));
@@ -2066,6 +2514,23 @@ async function authRevokeKeyCommand(args) {
2066
2514
  console.log(`${okLabel()} Revoked API key '${keyId}' for instance '${instanceName}'.`);
2067
2515
  process.exit(0);
2068
2516
  }
2517
+ async function resolveCommand(args) {
2518
+ const explicitDir = getFlag(args, 'dir');
2519
+ const escalationDir = explicitDir ? path_1.default.resolve(explicitDir) : (0, escalationPaths_1.getEscalationPaths)().root;
2520
+ await (0, resolutionist_1.resolveInteractive)(escalationDir);
2521
+ }
2522
+ async function chatCommand(args) {
2523
+ const provider = normalizeProvider(getFlag(args, 'provider'));
2524
+ if (provider && !isSupportedProvider(provider)) {
2525
+ throw new Error(`Unsupported provider '${provider}'.`);
2526
+ }
2527
+ await (0, chat_1.startChatSession)({
2528
+ agentId: getFlag(args, 'agent') ?? 'iranti_chat',
2529
+ provider,
2530
+ model: getFlag(args, 'model'),
2531
+ cwd: process.cwd(),
2532
+ });
2533
+ }
2069
2534
  function printHelp() {
2070
2535
  console.log(`Iranti CLI
2071
2536
 
@@ -2097,10 +2562,12 @@ Configuration:
2097
2562
  Project-level:
2098
2563
  iranti project init [path] --instance <name> [--api-key <token>] [--agent-id <id>] [--force]
2099
2564
 
2100
- Diagnostics:
2101
- iranti doctor [--instance <name>] [--scope user|system] [--env <file>] [--json]
2102
- iranti status [--scope user|system] [--json]
2103
- iranti upgrade [--json]
2565
+ Diagnostics:
2566
+ iranti doctor [--instance <name>] [--scope user|system] [--env <file>] [--json]
2567
+ iranti status [--scope user|system] [--json]
2568
+ iranti upgrade [--check] [--dry-run] [--yes] [--target auto|npm-global|npm-repo|python] [--json]
2569
+ iranti chat [--agent <agent-id>] [--provider <provider>] [--model <model>]
2570
+ iranti resolve [--dir <escalation-dir>]
2104
2571
 
2105
2572
  Integrations:
2106
2573
  iranti mcp [--help]
@@ -2199,6 +2666,14 @@ async function main() {
2199
2666
  await upgradeCommand(args);
2200
2667
  return;
2201
2668
  }
2669
+ if (args.command === 'chat') {
2670
+ await chatCommand(args);
2671
+ return;
2672
+ }
2673
+ if (args.command === 'resolve') {
2674
+ await resolveCommand(args);
2675
+ return;
2676
+ }
2202
2677
  if (args.command === 'mcp') {
2203
2678
  await handoffToScript('iranti-mcp', process.argv.slice(3));
2204
2679
  return;
@@ -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.2',
147
+ version: '0.2.3',
148
148
  });
149
149
  server.registerTool('iranti_handshake', {
150
150
  description: `Initialize or refresh an agent's working-memory brief for the current task.