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 +12 -0
- package/dist/scripts/codex-setup.js +9 -1
- package/dist/scripts/iranti-cli.js +524 -86
- package/dist/scripts/iranti-mcp.js +1 -1
- package/dist/scripts/seed.js +10 -10
- package/dist/src/api/server.js +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1312
|
-
|
|
1313
|
-
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
1675
|
-
const agentId = sanitizeIdentifier(await promptNonEmpty(prompt, '
|
|
1676
|
-
const memoryEntity = await promptNonEmpty(prompt, '
|
|
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}
|
|
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
|
|
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
|
|
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.
|
|
1984
|
-
python: latestPython ? compareVersions(latestPython, context.
|
|
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 =
|
|
1987
|
-
let execution =
|
|
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 (
|
|
1991
|
-
throw new Error('No executable upgrade path was detected. Use --target npm-global, --target npm-repo,
|
|
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
|
|
1997
|
-
|
|
2253
|
+
else {
|
|
2254
|
+
execution = await executeUpgradeTargets(selectedTargets, context);
|
|
1998
2255
|
}
|
|
1999
|
-
|
|
2000
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2026
|
-
|
|
2288
|
+
requestedTargets,
|
|
2289
|
+
selectedTargets,
|
|
2027
2290
|
availableTargets: context.availableTargets,
|
|
2291
|
+
targets: statuses,
|
|
2028
2292
|
updateAvailable,
|
|
2029
2293
|
commands,
|
|
2030
|
-
plan
|
|
2031
|
-
action:
|
|
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 (
|
|
2048
|
-
console.log(` selected_target
|
|
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
|
|
2315
|
+
console.log(` - ${step}`);
|
|
2052
2316
|
}
|
|
2053
2317
|
}
|
|
2054
2318
|
else {
|
|
2055
|
-
console.log(`
|
|
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
|
-
|
|
2070
|
-
|
|
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('
|
|
2270
|
-
dbUrl = await prompt.line('DATABASE_URL', dbUrl ?? env.DATABASE_URL);
|
|
2271
|
-
providerInput = await prompt.line('
|
|
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
|
-
|
|
2274
|
-
|
|
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('
|
|
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('
|
|
2347
|
-
explicitUrl = await prompt.line('
|
|
2348
|
-
explicitApiKey = await prompt.secret('
|
|
2349
|
-
explicitAgentId = await prompt.line('
|
|
2350
|
-
explicitMemoryEntity = await prompt.line('
|
|
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.
|
|
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.
|
package/dist/scripts/seed.js
CHANGED
|
@@ -15,7 +15,7 @@ const STAFF_ENTRIES = [
|
|
|
15
15
|
entityId: 'librarian',
|
|
16
16
|
key: 'operating_rules',
|
|
17
17
|
valueRaw: {
|
|
18
|
-
version: '0.2.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
240
|
+
version: '0.2.6',
|
|
241
241
|
events: [
|
|
242
242
|
{
|
|
243
243
|
at: new Date().toISOString(),
|
package/dist/src/api/server.js
CHANGED