iranti 0.2.10 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -245,6 +245,14 @@ iranti run --instance local
245
245
 
246
246
  If local PostgreSQL is available, setup can bootstrap a localhost database for you. If local PostgreSQL is not available, setup recommends Docker when Docker is installed, and otherwise steers you to managed PostgreSQL with concrete install guidance.
247
247
 
248
+ If something still fails and you need more detail, use:
249
+
250
+ ```bash
251
+ iranti doctor --debug
252
+ iranti run --instance local --debug
253
+ iranti upgrade --verbose
254
+ ```
255
+
248
256
  Advanced/manual path:
249
257
 
250
258
  ```bash
@@ -21,6 +21,15 @@ const resolutionist_1 = require("../src/resolutionist");
21
21
  const chat_1 = require("../src/chat");
22
22
  const backends_1 = require("../src/library/backends");
23
23
  const sdk_1 = require("../src/sdk");
24
+ class CliError extends Error {
25
+ constructor(code, message, hints = [], details) {
26
+ super(message);
27
+ this.name = 'CliError';
28
+ this.code = code;
29
+ this.hints = hints;
30
+ this.details = details;
31
+ }
32
+ }
24
33
  const PROVIDER_ENV_KEYS = {
25
34
  mock: null,
26
35
  ollama: null,
@@ -43,6 +52,8 @@ const ANSI = {
43
52
  cyan: '\x1b[36m',
44
53
  gray: '\x1b[90m',
45
54
  };
55
+ let CLI_DEBUG = process.argv.includes('--debug') || process.env.IRANTI_DEBUG === '1';
56
+ let CLI_VERBOSE = CLI_DEBUG || process.argv.includes('--verbose') || process.env.IRANTI_VERBOSE === '1';
46
57
  function useColor() {
47
58
  return Boolean(process.stdout.isTTY && !process.env.NO_COLOR);
48
59
  }
@@ -81,6 +92,25 @@ function printNextSteps(steps) {
81
92
  console.log(` ${index + 1}. ${step}`);
82
93
  }
83
94
  }
95
+ function setCliDebugFlags(args) {
96
+ CLI_DEBUG = CLI_DEBUG || hasFlag(args, 'debug');
97
+ CLI_VERBOSE = CLI_VERBOSE || CLI_DEBUG || hasFlag(args, 'verbose');
98
+ }
99
+ function debugLog(message, details) {
100
+ if (!CLI_DEBUG)
101
+ return;
102
+ const suffix = details && Object.keys(details).length > 0 ? ` ${JSON.stringify(details)}` : '';
103
+ console.log(`${paint('[DEBUG]', 'gray')} ${message}${suffix}`);
104
+ }
105
+ function verboseLog(message, details) {
106
+ if (!CLI_VERBOSE)
107
+ return;
108
+ const suffix = details && Object.keys(details).length > 0 ? ` ${JSON.stringify(details)}` : '';
109
+ console.log(`${paint('[TRACE]', 'gray')} ${message}${suffix}`);
110
+ }
111
+ function cliError(code, message, hints = [], details) {
112
+ return new CliError(code, message, hints, details);
113
+ }
84
114
  function parseArgs(argv) {
85
115
  const flags = new Map();
86
116
  const positionals = [];
@@ -197,6 +227,7 @@ function formatSetupBootstrapFailure(error) {
197
227
  }
198
228
  async function handoffToScript(scriptName, rawArgs) {
199
229
  const builtPath = builtScriptPath(scriptName);
230
+ debugLog('Handing off to companion script.', { scriptName, builtPath, rawArgs: rawArgs.join(' ') });
200
231
  if (fs_1.default.existsSync(builtPath)) {
201
232
  await new Promise((resolve, reject) => {
202
233
  const child = (0, child_process_1.spawn)(process.execPath, [builtPath, ...rawArgs], {
@@ -219,7 +250,7 @@ async function handoffToScript(scriptName, rawArgs) {
219
250
  }
220
251
  const sourcePath = path_1.default.resolve(process.cwd(), 'scripts', `${scriptName}.ts`);
221
252
  if (!fs_1.default.existsSync(sourcePath)) {
222
- throw new Error(`Unable to locate ${scriptName} implementation.`);
253
+ throw cliError('IRANTI_SCRIPT_NOT_FOUND', `Unable to locate ${scriptName} implementation.`, ['Run from an installed Iranti package or from the Iranti repo root where scripts/ exists.'], { scriptName, sourcePath, builtPath });
223
254
  }
224
255
  await new Promise((resolve, reject) => {
225
256
  const child = (0, child_process_1.spawn)('npx', ['ts-node', sourcePath, ...rawArgs], {
@@ -428,8 +459,12 @@ function instancePaths(root, name) {
428
459
  async function loadInstanceEnv(root, name) {
429
460
  const paths = instancePaths(root, name);
430
461
  if (!fs_1.default.existsSync(paths.envFile)) {
431
- throw new Error(`Instance '${name}' not found at ${paths.instanceDir}`);
462
+ throw cliError('IRANTI_INSTANCE_NOT_FOUND', `Instance '${name}' not found at ${paths.instanceDir}`, [
463
+ 'Run `iranti instance list` to see known instances.',
464
+ `Run \`iranti setup\` or \`iranti instance create ${name}\` if this instance does not exist yet.`,
465
+ ], { instance: name, root, instanceDir: paths.instanceDir });
432
466
  }
467
+ debugLog('Loaded instance env target.', { instance: name, envFile: paths.envFile });
433
468
  return {
434
469
  ...paths,
435
470
  env: await readEnvFile(paths.envFile),
@@ -451,15 +486,15 @@ async function resolveProviderKeyTarget(args) {
451
486
  const projectPath = path_1.default.resolve(getFlag(args, 'project') ?? process.cwd());
452
487
  const bindingFile = path_1.default.join(projectPath, '.env.iranti');
453
488
  if (!fs_1.default.existsSync(bindingFile)) {
454
- throw new Error('No --instance provided and no .env.iranti found in the current project. Run from a bound project or pass --instance <name>.');
489
+ throw cliError('IRANTI_PROJECT_BINDING_MISSING', 'No --instance provided and no .env.iranti found in the current project.', ['Run `iranti project init . --instance <name>` or pass `--instance <name>`.'], { projectPath, bindingFile });
455
490
  }
456
491
  const binding = await readEnvFile(bindingFile);
457
492
  const envFile = binding.IRANTI_INSTANCE_ENV?.trim();
458
493
  if (!envFile) {
459
- throw new Error(`Project binding is missing IRANTI_INSTANCE_ENV: ${bindingFile}`);
494
+ throw cliError('IRANTI_BINDING_INSTANCE_ENV_MISSING', `Project binding is missing IRANTI_INSTANCE_ENV: ${bindingFile}`, ['Run `iranti configure project` to refresh the binding.'], { bindingFile });
460
495
  }
461
496
  if (!fs_1.default.existsSync(envFile)) {
462
- throw new Error(`Instance env referenced by project binding was not found: ${envFile}`);
497
+ throw cliError('IRANTI_BINDING_INSTANCE_ENV_NOT_FOUND', `Instance env referenced by project binding was not found: ${envFile}`, ['Run `iranti configure project` to refresh the binding or recreate the target instance.'], { bindingFile, envFile });
463
498
  }
464
499
  return {
465
500
  instanceName: binding.IRANTI_INSTANCE?.trim() || undefined,
@@ -707,7 +742,7 @@ function isSupportedProvider(provider) {
707
742
  async function promptYesNo(session, prompt, defaultValue) {
708
743
  const defaultToken = defaultValue ? 'Y/n' : 'y/N';
709
744
  while (true) {
710
- const answer = (await session.line(`${prompt} (${defaultToken})`, '') ?? '').trim().toLowerCase();
745
+ const answer = (await session.line(`${prompt} (${defaultToken})`) ?? '').trim().toLowerCase();
711
746
  if (!answer)
712
747
  return defaultValue;
713
748
  if (['y', 'yes'].includes(answer))
@@ -730,7 +765,7 @@ async function promptRequiredSecret(session, prompt, currentValue) {
730
765
  const value = (await session.secretRequired(prompt, currentValue) ?? '').trim();
731
766
  if (value.length > 0 && !detectPlaceholder(value))
732
767
  return value;
733
- console.log(`${warnLabel()} A real secret is required.`);
768
+ console.log(`${warnLabel()} ${prompt} is required.`);
734
769
  }
735
770
  }
736
771
  function makeLegacyInstanceApiKey(instanceName) {
@@ -1466,8 +1501,13 @@ function collectDoctorRemediations(checks, envSource, envFile) {
1466
1501
  add('Run `iranti setup`, or rerun `iranti doctor` with `--instance <name>` or `--env <file>`.');
1467
1502
  }
1468
1503
  }
1469
- if (check.name === 'database configuration' && check.status === 'fail') {
1470
- add(`Set a real DATABASE_URL in ${envFile ?? 'the target env file'}, or rerun \`iranti setup\` to configure the database again.`);
1504
+ if ((check.name === 'database configuration' || check.name === 'bound instance database configuration') && check.status === 'fail') {
1505
+ if (check.name === 'bound instance database configuration') {
1506
+ add('Fix the linked instance env, or rerun `iranti setup` / `iranti configure instance` for the bound instance.');
1507
+ }
1508
+ else {
1509
+ add(`Set a real DATABASE_URL in ${envFile ?? 'the target env file'}, or rerun \`iranti setup\` to configure the database again.`);
1510
+ }
1471
1511
  }
1472
1512
  if (check.name === 'project binding url' && check.status === 'fail') {
1473
1513
  add('Run `iranti configure project` to refresh the project binding, or set IRANTI_URL in `.env.iranti`.');
@@ -1475,15 +1515,18 @@ function collectDoctorRemediations(checks, envSource, envFile) {
1475
1515
  if (check.name === 'project api key' && check.status === 'fail') {
1476
1516
  add('Run `iranti configure project` or set IRANTI_API_KEY in `.env.iranti`.');
1477
1517
  }
1518
+ if (check.name === 'bound instance env' && check.status !== 'pass') {
1519
+ add('Run `iranti configure project` to refresh the project binding, or set IRANTI_INSTANCE_ENV in `.env.iranti` so doctor can inspect the bound local instance.');
1520
+ }
1478
1521
  if (check.name === 'api key' && check.status !== 'pass') {
1479
1522
  add(envSource === 'project-binding'
1480
1523
  ? 'Set IRANTI_API_KEY in the project binding, or rerun `iranti configure project`.'
1481
1524
  : 'Create or rotate an Iranti key with `iranti auth create-key`, then store it in the target env.');
1482
1525
  }
1483
- if (check.name === 'provider credentials' && check.status === 'fail') {
1526
+ if ((check.name === 'provider credentials' || check.name === 'bound instance provider credentials') && check.status === 'fail') {
1484
1527
  add('Store or refresh the upstream provider key with `iranti add api-key` or `iranti update api-key`.');
1485
1528
  }
1486
- if (check.name === 'vector backend' && check.status === 'fail') {
1529
+ if ((check.name === 'vector backend' || check.name === 'bound instance vector backend') && check.status === 'fail') {
1487
1530
  add('Check the vector backend env vars, or switch back to `IRANTI_VECTOR_BACKEND=pgvector` if the external backend is not ready.');
1488
1531
  }
1489
1532
  }
@@ -1498,6 +1541,7 @@ function resolveDoctorEnvTarget(args) {
1498
1541
  const explicitEnv = getFlag(args, 'env');
1499
1542
  const cwd = process.cwd();
1500
1543
  if (explicitEnv) {
1544
+ debugLog('Doctor target resolved from explicit env.', { envFile: path_1.default.resolve(explicitEnv) });
1501
1545
  return {
1502
1546
  envFile: path_1.default.resolve(explicitEnv),
1503
1547
  envSource: 'explicit-env',
@@ -1505,6 +1549,7 @@ function resolveDoctorEnvTarget(args) {
1505
1549
  }
1506
1550
  if (instanceName) {
1507
1551
  const root = resolveInstallRoot(args, scope);
1552
+ debugLog('Doctor target resolved from instance.', { instance: instanceName, root });
1508
1553
  return {
1509
1554
  envFile: path_1.default.join(root, 'instances', instanceName, '.env'),
1510
1555
  envSource: `instance:${instanceName}`,
@@ -1513,11 +1558,14 @@ function resolveDoctorEnvTarget(args) {
1513
1558
  const repoEnv = path_1.default.join(cwd, '.env');
1514
1559
  const projectEnv = path_1.default.join(cwd, '.env.iranti');
1515
1560
  if (fs_1.default.existsSync(repoEnv)) {
1561
+ debugLog('Doctor target resolved from repo env.', { envFile: repoEnv });
1516
1562
  return { envFile: repoEnv, envSource: 'repo' };
1517
1563
  }
1518
1564
  if (fs_1.default.existsSync(projectEnv)) {
1565
+ debugLog('Doctor target resolved from project binding.', { envFile: projectEnv });
1519
1566
  return { envFile: projectEnv, envSource: 'project-binding' };
1520
1567
  }
1568
+ debugLog('Doctor target resolution found no env file.', { cwd });
1521
1569
  return { envFile: null, envSource: 'repo' };
1522
1570
  }
1523
1571
  function resolveUpgradeTarget(raw) {
@@ -1638,6 +1686,11 @@ function quoteForCmd(arg) {
1638
1686
  return `"${arg.replace(/"/g, '\\"')}"`;
1639
1687
  }
1640
1688
  function runCommandCapture(executable, args, cwd, extraEnv) {
1689
+ verboseLog('Running subprocess (capture).', {
1690
+ executable,
1691
+ args: args.join(' '),
1692
+ cwd: cwd ?? process.cwd(),
1693
+ });
1641
1694
  const proc = process.platform === 'win32'
1642
1695
  ? (0, child_process_1.spawnSync)(process.env.ComSpec ?? 'cmd.exe', [
1643
1696
  '/d',
@@ -1661,13 +1714,24 @@ function runCommandCapture(executable, args, cwd, extraEnv) {
1661
1714
  ...extraEnv,
1662
1715
  },
1663
1716
  });
1664
- return {
1717
+ const result = {
1665
1718
  status: proc.status,
1666
1719
  stdout: proc.stdout ?? '',
1667
1720
  stderr: proc.stderr ?? '',
1668
1721
  };
1722
+ verboseLog('Subprocess finished (capture).', {
1723
+ executable,
1724
+ status: result.status ?? -1,
1725
+ stderr: result.stderr.trim() || null,
1726
+ });
1727
+ return result;
1669
1728
  }
1670
1729
  function runCommandInteractive(step) {
1730
+ verboseLog('Running subprocess (interactive).', {
1731
+ label: step.label,
1732
+ command: step.display,
1733
+ cwd: step.cwd ?? process.cwd(),
1734
+ });
1671
1735
  const proc = process.platform === 'win32'
1672
1736
  ? (0, child_process_1.spawnSync)(process.env.ComSpec ?? 'cmd.exe', [
1673
1737
  '/d',
@@ -1681,6 +1745,10 @@ function runCommandInteractive(step) {
1681
1745
  cwd: step.cwd,
1682
1746
  stdio: 'inherit',
1683
1747
  });
1748
+ verboseLog('Subprocess finished (interactive).', {
1749
+ label: step.label,
1750
+ status: proc.status ?? -1,
1751
+ });
1684
1752
  return proc.status;
1685
1753
  }
1686
1754
  function deriveDatabaseUrlForMode(mode, instanceName, explicitDatabaseUrl) {
@@ -2341,7 +2409,7 @@ async function setupCommand(args) {
2341
2409
  await withPromptSession(async (prompt) => {
2342
2410
  let setupMode = 'isolated';
2343
2411
  while (true) {
2344
- const chosen = (await prompt.line('How should Iranti install the runtime: isolated per-project or shared machine runtime', 'isolated') ?? 'isolated').trim().toLowerCase();
2412
+ const chosen = (await prompt.line('Runtime mode (isolated or shared)', 'isolated') ?? 'isolated').trim().toLowerCase();
2345
2413
  if (chosen === 'shared' || chosen === 'isolated') {
2346
2414
  setupMode = chosen;
2347
2415
  break;
@@ -2351,12 +2419,12 @@ async function setupCommand(args) {
2351
2419
  let finalScope = 'user';
2352
2420
  let finalRoot = '';
2353
2421
  if (setupMode === 'isolated') {
2354
- finalRoot = path_1.default.resolve(await promptNonEmpty(prompt, 'Where should the isolated runtime live', explicitRoot ?? path_1.default.join(process.cwd(), '.iranti-runtime')));
2422
+ finalRoot = path_1.default.resolve(await promptNonEmpty(prompt, 'Isolated runtime path', explicitRoot ?? path_1.default.join(process.cwd(), '.iranti-runtime')));
2355
2423
  finalScope = 'user';
2356
2424
  }
2357
2425
  else {
2358
2426
  while (true) {
2359
- const chosenScope = (await prompt.line('Install scope: user or system', explicitScope ?? 'user') ?? 'user').trim().toLowerCase();
2427
+ const chosenScope = (await prompt.line('Install scope (user or system)', explicitScope ?? 'user') ?? 'user').trim().toLowerCase();
2360
2428
  if (chosenScope === 'user' || chosenScope === 'system') {
2361
2429
  finalScope = chosenScope;
2362
2430
  break;
@@ -2367,7 +2435,7 @@ async function setupCommand(args) {
2367
2435
  }
2368
2436
  await ensureRuntimeInstalled(finalRoot, finalScope);
2369
2437
  console.log(`${okLabel()} Runtime ready at ${finalRoot}`);
2370
- const instanceName = sanitizeIdentifier(await promptNonEmpty(prompt, 'What should this instance be called', setupMode === 'isolated' ? sanitizeIdentifier(path_1.default.basename(process.cwd()), 'local') : 'local'), 'local');
2438
+ const instanceName = sanitizeIdentifier(await promptNonEmpty(prompt, 'Instance name', setupMode === 'isolated' ? sanitizeIdentifier(path_1.default.basename(process.cwd()), 'local') : 'local'), 'local');
2371
2439
  const existingInstance = fs_1.default.existsSync(instancePaths(finalRoot, instanceName).envFile)
2372
2440
  ? await loadInstanceEnv(finalRoot, instanceName)
2373
2441
  : null;
@@ -2378,7 +2446,7 @@ async function setupCommand(args) {
2378
2446
  console.log(`${infoLabel()} Creating new instance '${instanceName}'.`);
2379
2447
  }
2380
2448
  const existingPort = Number.parseInt(existingInstance?.env.IRANTI_PORT ?? '3001', 10);
2381
- const port = await chooseAvailablePort(prompt, 'Which port should the Iranti API use', existingPort, Boolean(existingInstance));
2449
+ const port = await chooseAvailablePort(prompt, 'API port', existingPort, Boolean(existingInstance));
2382
2450
  const dockerAvailable = hasDockerInstalled();
2383
2451
  const psqlAvailable = hasCommandInstalled('psql');
2384
2452
  let dbUrl = '';
@@ -2386,14 +2454,14 @@ async function setupCommand(args) {
2386
2454
  let databaseMode = recommendedDatabaseMode;
2387
2455
  while (true) {
2388
2456
  const defaultMode = recommendedDatabaseMode;
2389
- const dbMode = (await prompt.line('How should we set up the database: local, managed, or docker', defaultMode) ?? defaultMode).trim().toLowerCase();
2457
+ const dbMode = (await prompt.line('Database mode (local, managed, or docker)', defaultMode) ?? defaultMode).trim().toLowerCase();
2390
2458
  if (dbMode === 'existing' || dbMode === 'local' || dbMode === 'managed') {
2391
2459
  databaseMode = dbMode === 'existing' ? 'local' : dbMode;
2392
2460
  const defaultDatabaseUrl = databaseMode === 'local'
2393
2461
  ? existingInstance?.env.DATABASE_URL ?? deriveDatabaseUrlForMode('local', instanceName)
2394
2462
  : existingInstance?.env.DATABASE_URL ?? '';
2395
2463
  while (true) {
2396
- dbUrl = await promptNonEmpty(prompt, 'Database connection string (DATABASE_URL)', defaultDatabaseUrl);
2464
+ dbUrl = await promptNonEmpty(prompt, 'DATABASE_URL', defaultDatabaseUrl);
2397
2465
  if (!detectPlaceholder(dbUrl))
2398
2466
  break;
2399
2467
  console.log(`${warnLabel()} DATABASE_URL still looks like a placeholder. Enter a real connection string before finishing setup.`);
@@ -2415,9 +2483,9 @@ async function setupCommand(args) {
2415
2483
  console.log(`${warnLabel()} Docker is not installed or not on PATH. Choose local or managed instead.`);
2416
2484
  continue;
2417
2485
  }
2418
- const dbHostPort = await chooseAvailablePort(prompt, 'Which host port should Docker PostgreSQL use', 5432, false);
2419
- const dbName = sanitizeIdentifier(await promptNonEmpty(prompt, 'What should the Docker PostgreSQL database be called', `iranti_${instanceName}`), `iranti_${instanceName}`);
2420
- const dbPassword = await promptRequiredSecret(prompt, 'Set a password for Docker PostgreSQL');
2486
+ const dbHostPort = await chooseAvailablePort(prompt, 'Docker PostgreSQL host port', 5432, false);
2487
+ const dbName = sanitizeIdentifier(await promptNonEmpty(prompt, 'Docker PostgreSQL database name', `iranti_${instanceName}`), `iranti_${instanceName}`);
2488
+ const dbPassword = await promptRequiredSecret(prompt, 'Docker PostgreSQL password');
2421
2489
  const containerName = sanitizeIdentifier(await promptNonEmpty(prompt, 'Docker container name', `iranti_${instanceName}_db`), `iranti_${instanceName}_db`);
2422
2490
  dbUrl = `postgresql://postgres:${dbPassword}@localhost:${dbHostPort}/${dbName}`;
2423
2491
  console.log(`${infoLabel()} Docker will be used only for PostgreSQL. Iranti itself does not require Docker once a PostgreSQL database is available.`);
@@ -2441,7 +2509,7 @@ async function setupCommand(args) {
2441
2509
  let provider = normalizeProvider(existingInstance?.env.LLM_PROVIDER ?? 'openai') ?? 'openai';
2442
2510
  while (true) {
2443
2511
  listProviderChoices(provider, existingInstance?.env ?? {});
2444
- const chosen = normalizeProvider(await promptNonEmpty(prompt, 'Which LLM provider should Iranti use by default', provider));
2512
+ const chosen = normalizeProvider(await promptNonEmpty(prompt, 'Default LLM provider', provider));
2445
2513
  if (chosen && isSupportedProvider(chosen)) {
2446
2514
  provider = chosen;
2447
2515
  break;
@@ -2464,7 +2532,7 @@ async function setupCommand(args) {
2464
2532
  let extraProvider = provider;
2465
2533
  while (true) {
2466
2534
  listProviderChoices(provider, { ...seedEnv, ...providerKeys });
2467
- const chosen = normalizeProvider(await promptNonEmpty(prompt, 'Which extra provider would you like to add', 'claude'));
2535
+ const chosen = normalizeProvider(await promptNonEmpty(prompt, 'Additional provider', 'claude'));
2468
2536
  if (!chosen) {
2469
2537
  console.log(`${warnLabel()} Provider is required.`);
2470
2538
  continue;
@@ -2499,9 +2567,9 @@ async function setupCommand(args) {
2499
2567
  const defaultProjectPath = process.cwd();
2500
2568
  let shouldBindProject = await promptYesNo(prompt, 'Bind a project folder to this instance now?', true);
2501
2569
  while (shouldBindProject) {
2502
- const projectPath = path_1.default.resolve(await promptNonEmpty(prompt, 'Which project folder should we bind', projects.length === 0 ? defaultProjectPath : process.cwd()));
2503
- const agentId = sanitizeIdentifier(await promptNonEmpty(prompt, 'What agent id should this project use', projectAgentDefault(projectPath)), 'project_main');
2504
- const memoryEntity = await promptNonEmpty(prompt, 'What memory entity should this project use', 'user/main');
2570
+ const projectPath = path_1.default.resolve(await promptNonEmpty(prompt, 'Project path to bind', projects.length === 0 ? defaultProjectPath : process.cwd()));
2571
+ const agentId = sanitizeIdentifier(await promptNonEmpty(prompt, 'Project agent ID', projectAgentDefault(projectPath)), 'project_main');
2572
+ const memoryEntity = await promptNonEmpty(prompt, 'Project memory entity', 'user/main');
2505
2573
  const claudeCode = await promptYesNo(prompt, 'Create Claude Code project files here now?', true);
2506
2574
  projects.push({
2507
2575
  path: projectPath,
@@ -2574,6 +2642,71 @@ async function doctorCommand(args) {
2574
2642
  const { envFile, envSource } = resolveDoctorEnvTarget(args);
2575
2643
  const checks = [];
2576
2644
  const version = getPackageVersion();
2645
+ const pushEnvironmentChecks = async (env, prefix = '') => {
2646
+ const databaseUrl = env.DATABASE_URL;
2647
+ checks.push(detectPlaceholder(databaseUrl)
2648
+ ? {
2649
+ name: `${prefix}database configuration`,
2650
+ status: 'fail',
2651
+ detail: 'DATABASE_URL is missing or still uses a placeholder value.',
2652
+ }
2653
+ : {
2654
+ name: `${prefix}database configuration`,
2655
+ status: 'pass',
2656
+ detail: 'DATABASE_URL is present and non-placeholder.',
2657
+ });
2658
+ const provider = env.LLM_PROVIDER ?? 'mock';
2659
+ checks.push({
2660
+ name: `${prefix}llm provider`,
2661
+ status: 'pass',
2662
+ detail: `LLM_PROVIDER=${provider}`,
2663
+ });
2664
+ const providerKeyCheck = detectProviderKey(provider, env);
2665
+ checks.push({
2666
+ ...providerKeyCheck,
2667
+ name: `${prefix}${providerKeyCheck.name}`,
2668
+ });
2669
+ try {
2670
+ const backendName = (0, backends_1.resolveVectorBackendName)({
2671
+ vectorBackend: env.IRANTI_VECTOR_BACKEND,
2672
+ qdrantUrl: env.IRANTI_QDRANT_URL,
2673
+ qdrantApiKey: env.IRANTI_QDRANT_API_KEY,
2674
+ qdrantCollection: env.IRANTI_QDRANT_COLLECTION,
2675
+ chromaUrl: env.IRANTI_CHROMA_URL,
2676
+ chromaCollection: env.IRANTI_CHROMA_COLLECTION,
2677
+ chromaTenant: env.IRANTI_CHROMA_TENANT,
2678
+ chromaDatabase: env.IRANTI_CHROMA_DATABASE,
2679
+ chromaToken: env.IRANTI_CHROMA_TOKEN,
2680
+ });
2681
+ const backend = (0, backends_1.createVectorBackend)({
2682
+ vectorBackend: backendName,
2683
+ qdrantUrl: env.IRANTI_QDRANT_URL,
2684
+ qdrantApiKey: env.IRANTI_QDRANT_API_KEY,
2685
+ qdrantCollection: env.IRANTI_QDRANT_COLLECTION,
2686
+ chromaUrl: env.IRANTI_CHROMA_URL,
2687
+ chromaCollection: env.IRANTI_CHROMA_COLLECTION,
2688
+ chromaTenant: env.IRANTI_CHROMA_TENANT,
2689
+ chromaDatabase: env.IRANTI_CHROMA_DATABASE,
2690
+ chromaToken: env.IRANTI_CHROMA_TOKEN,
2691
+ });
2692
+ const reachable = await backend.ping();
2693
+ const url = vectorBackendUrl(backendName, env);
2694
+ checks.push({
2695
+ name: `${prefix}vector backend`,
2696
+ status: reachable ? 'pass' : 'warn',
2697
+ detail: url
2698
+ ? `${backendName} (${url}) is ${reachable ? 'reachable' : 'unreachable'}`
2699
+ : `${backendName} is ${reachable ? 'reachable' : 'unreachable'}`,
2700
+ });
2701
+ }
2702
+ catch (error) {
2703
+ checks.push({
2704
+ name: `${prefix}vector backend`,
2705
+ status: 'fail',
2706
+ detail: error instanceof Error ? error.message : String(error),
2707
+ });
2708
+ }
2709
+ };
2577
2710
  checks.push({
2578
2711
  name: 'node version',
2579
2712
  status: Number.parseInt(process.versions.node.split('.')[0] ?? '0', 10) >= 18 ? 'pass' : 'fail',
@@ -2603,24 +2736,15 @@ async function doctorCommand(args) {
2603
2736
  }
2604
2737
  else {
2605
2738
  const env = await readEnvFile(envFile);
2739
+ const treatAsProjectBinding = envSource === 'project-binding'
2740
+ || path_1.default.basename(envFile).toLowerCase() === '.env.iranti'
2741
+ || (Boolean(env.IRANTI_URL?.trim()) && detectPlaceholder(env.DATABASE_URL));
2606
2742
  checks.push({
2607
2743
  name: 'environment file',
2608
2744
  status: 'pass',
2609
2745
  detail: `${envSource} env loaded from ${envFile}`,
2610
2746
  });
2611
- const databaseUrl = env.DATABASE_URL;
2612
- checks.push(detectPlaceholder(databaseUrl)
2613
- ? {
2614
- name: 'database configuration',
2615
- status: 'fail',
2616
- detail: 'DATABASE_URL is missing or still uses a placeholder value.',
2617
- }
2618
- : {
2619
- name: 'database configuration',
2620
- status: 'pass',
2621
- detail: 'DATABASE_URL is present and non-placeholder.',
2622
- });
2623
- if (envSource === 'project-binding') {
2747
+ if (treatAsProjectBinding) {
2624
2748
  checks.push(detectPlaceholder(env.IRANTI_URL)
2625
2749
  ? {
2626
2750
  name: 'project binding url',
@@ -2633,7 +2757,7 @@ async function doctorCommand(args) {
2633
2757
  detail: `IRANTI_URL=${env.IRANTI_URL}`,
2634
2758
  });
2635
2759
  }
2636
- if (envSource === 'project-binding') {
2760
+ if (treatAsProjectBinding) {
2637
2761
  checks.push(detectPlaceholder(env.IRANTI_API_KEY)
2638
2762
  ? {
2639
2763
  name: 'project api key',
@@ -2645,8 +2769,33 @@ async function doctorCommand(args) {
2645
2769
  status: 'pass',
2646
2770
  detail: 'IRANTI_API_KEY is present in .env.iranti.',
2647
2771
  });
2772
+ const linkedInstanceEnv = env.IRANTI_INSTANCE_ENV?.trim();
2773
+ if (!linkedInstanceEnv) {
2774
+ checks.push({
2775
+ name: 'bound instance env',
2776
+ status: 'warn',
2777
+ detail: 'IRANTI_INSTANCE_ENV is not set in .env.iranti. Skipping database and provider checks for the bound instance.',
2778
+ });
2779
+ }
2780
+ else if (!fs_1.default.existsSync(linkedInstanceEnv)) {
2781
+ checks.push({
2782
+ name: 'bound instance env',
2783
+ status: 'warn',
2784
+ detail: `Linked instance env not found: ${linkedInstanceEnv}. Skipping database and provider checks for the bound instance.`,
2785
+ });
2786
+ }
2787
+ else {
2788
+ checks.push({
2789
+ name: 'bound instance env',
2790
+ status: 'pass',
2791
+ detail: `Using ${linkedInstanceEnv} for bound instance diagnostics.`,
2792
+ });
2793
+ const linkedEnv = await readEnvFile(linkedInstanceEnv);
2794
+ await pushEnvironmentChecks(linkedEnv, 'bound instance ');
2795
+ }
2648
2796
  }
2649
2797
  else {
2798
+ await pushEnvironmentChecks(env);
2650
2799
  checks.push(detectPlaceholder(env.IRANTI_API_KEY)
2651
2800
  ? {
2652
2801
  name: 'api key',
@@ -2659,53 +2808,6 @@ async function doctorCommand(args) {
2659
2808
  detail: 'IRANTI_API_KEY is present.',
2660
2809
  });
2661
2810
  }
2662
- const provider = env.LLM_PROVIDER ?? 'mock';
2663
- checks.push({
2664
- name: 'llm provider',
2665
- status: 'pass',
2666
- detail: `LLM_PROVIDER=${provider}`,
2667
- });
2668
- checks.push(detectProviderKey(provider, env));
2669
- try {
2670
- const backendName = (0, backends_1.resolveVectorBackendName)({
2671
- vectorBackend: env.IRANTI_VECTOR_BACKEND,
2672
- qdrantUrl: env.IRANTI_QDRANT_URL,
2673
- qdrantApiKey: env.IRANTI_QDRANT_API_KEY,
2674
- qdrantCollection: env.IRANTI_QDRANT_COLLECTION,
2675
- chromaUrl: env.IRANTI_CHROMA_URL,
2676
- chromaCollection: env.IRANTI_CHROMA_COLLECTION,
2677
- chromaTenant: env.IRANTI_CHROMA_TENANT,
2678
- chromaDatabase: env.IRANTI_CHROMA_DATABASE,
2679
- chromaToken: env.IRANTI_CHROMA_TOKEN,
2680
- });
2681
- const backend = (0, backends_1.createVectorBackend)({
2682
- vectorBackend: backendName,
2683
- qdrantUrl: env.IRANTI_QDRANT_URL,
2684
- qdrantApiKey: env.IRANTI_QDRANT_API_KEY,
2685
- qdrantCollection: env.IRANTI_QDRANT_COLLECTION,
2686
- chromaUrl: env.IRANTI_CHROMA_URL,
2687
- chromaCollection: env.IRANTI_CHROMA_COLLECTION,
2688
- chromaTenant: env.IRANTI_CHROMA_TENANT,
2689
- chromaDatabase: env.IRANTI_CHROMA_DATABASE,
2690
- chromaToken: env.IRANTI_CHROMA_TOKEN,
2691
- });
2692
- const reachable = await backend.ping();
2693
- const url = vectorBackendUrl(backendName, env);
2694
- checks.push({
2695
- name: 'vector backend',
2696
- status: reachable ? 'pass' : 'warn',
2697
- detail: url
2698
- ? `${backendName} (${url}) is ${reachable ? 'reachable' : 'unreachable'}`
2699
- : `${backendName} is ${reachable ? 'reachable' : 'unreachable'}`,
2700
- });
2701
- }
2702
- catch (error) {
2703
- checks.push({
2704
- name: 'vector backend',
2705
- status: 'fail',
2706
- detail: error instanceof Error ? error.message : String(error),
2707
- });
2708
- }
2709
2811
  }
2710
2812
  const result = {
2711
2813
  version,
@@ -3102,19 +3204,21 @@ async function showInstanceCommand(args) {
3102
3204
  }
3103
3205
  async function runInstanceCommand(args) {
3104
3206
  const name = getFlag(args, 'instance') ?? args.positionals[0] ?? args.subcommand;
3105
- if (!name)
3106
- throw new Error('Missing instance name. Usage: iranti run --instance <name>');
3207
+ if (!name) {
3208
+ throw cliError('IRANTI_INSTANCE_NAME_REQUIRED', 'Missing instance name. Usage: iranti run --instance <name>', ['Run `iranti instance list` to see configured instances.']);
3209
+ }
3107
3210
  const scope = normalizeScope(getFlag(args, 'scope'));
3108
3211
  const root = resolveInstallRoot(args, scope);
3109
3212
  const envFile = path_1.default.join(root, 'instances', name, '.env');
3110
- if (!fs_1.default.existsSync(envFile))
3111
- throw new Error(`Instance '${name}' not found. Create it first.`);
3213
+ if (!fs_1.default.existsSync(envFile)) {
3214
+ throw cliError('IRANTI_INSTANCE_NOT_FOUND', `Instance '${name}' not found. Create it first.`, [`Run \`iranti setup\` or \`iranti instance create ${name}\` first.`], { instance: name, envFile });
3215
+ }
3112
3216
  const env = await readEnvFile(envFile);
3113
3217
  for (const [k, v] of Object.entries(env)) {
3114
3218
  process.env[k] = v;
3115
3219
  }
3116
3220
  if (!process.env.DATABASE_URL || process.env.DATABASE_URL.includes('yourpassword')) {
3117
- throw new Error(`Instance '${name}' has placeholder DATABASE_URL. Edit ${envFile} first.`);
3221
+ throw cliError('IRANTI_INSTANCE_DATABASE_PLACEHOLDER', `Instance '${name}' has placeholder DATABASE_URL. Edit ${envFile} first.`, ['Run `iranti configure instance <name> --interactive` or rerun `iranti setup`.'], { instance: name, envFile });
3118
3222
  }
3119
3223
  console.log(`${infoLabel()} Starting Iranti instance '${name}' on port ${process.env.IRANTI_PORT ?? '3001'}...`);
3120
3224
  const serverEntry = path_1.default.resolve(__dirname, '..', 'src', 'api', 'server');
@@ -3125,7 +3229,7 @@ async function projectInitCommand(args) {
3125
3229
  const projectPath = path_1.default.resolve(args.positionals[0] ?? process.cwd());
3126
3230
  const instanceName = getFlag(args, 'instance');
3127
3231
  if (!instanceName) {
3128
- throw new Error('Missing --instance <name>. Usage: iranti project init [path] --instance <name>');
3232
+ throw cliError('IRANTI_INSTANCE_NAME_REQUIRED', 'Missing --instance <name>. Usage: iranti project init [path] --instance <name>', ['Run `iranti instance list` to see available instances.']);
3129
3233
  }
3130
3234
  const scope = normalizeScope(getFlag(args, 'scope'));
3131
3235
  const root = resolveInstallRoot(args, scope);
@@ -3136,7 +3240,7 @@ async function projectInitCommand(args) {
3136
3240
  const projectMode = normalizeProjectMode(getFlag(args, 'mode'), 'isolated');
3137
3241
  const outFile = path_1.default.join(projectPath, '.env.iranti');
3138
3242
  if (fs_1.default.existsSync(outFile) && !hasFlag(args, 'force')) {
3139
- throw new Error(`${outFile} already exists. Use --force to overwrite.`);
3243
+ throw cliError('IRANTI_PROJECT_BINDING_EXISTS', `${outFile} already exists. Use --force to overwrite.`, ['Use `iranti configure project` if you want to refresh the existing binding instead.'], { outFile });
3140
3244
  }
3141
3245
  await writeProjectBinding(projectPath, {
3142
3246
  IRANTI_URL: `http://localhost:${port}`,
@@ -3173,13 +3277,13 @@ async function configureInstanceCommand(args) {
3173
3277
  let clearProviderKey = hasFlag(args, 'clear-provider-key');
3174
3278
  if (hasFlag(args, 'interactive')) {
3175
3279
  await withPromptSession(async (prompt) => {
3176
- portRaw = await prompt.line('Which API port should this instance use', portRaw ?? env.IRANTI_PORT);
3177
- dbUrl = await prompt.line('Database connection string (DATABASE_URL)', dbUrl ?? env.DATABASE_URL);
3178
- providerInput = await prompt.line('Which LLM provider should this instance use', providerInput ?? env.LLM_PROVIDER ?? 'mock');
3280
+ portRaw = await prompt.line('API port', portRaw ?? env.IRANTI_PORT);
3281
+ dbUrl = await prompt.line('DATABASE_URL', dbUrl ?? env.DATABASE_URL);
3282
+ providerInput = await prompt.line('LLM provider', providerInput ?? env.LLM_PROVIDER ?? 'mock');
3179
3283
  const interactiveProvider = normalizeProvider(providerInput ?? env.LLM_PROVIDER ?? 'mock');
3180
3284
  const interactiveProviderEnvKey = providerKeyEnv(interactiveProvider);
3181
3285
  if (interactiveProvider && interactiveProviderEnvKey) {
3182
- providerKey = await prompt.secret(`Enter the ${providerDisplayName(interactiveProvider)} API key`, providerKey ?? env[interactiveProviderEnvKey]);
3286
+ providerKey = await prompt.secret(`${providerDisplayName(interactiveProvider)} API key`, providerKey ?? env[interactiveProviderEnvKey]);
3183
3287
  }
3184
3288
  apiKey = await prompt.secret('Iranti API key', apiKey ?? env.IRANTI_API_KEY);
3185
3289
  });
@@ -3256,12 +3360,12 @@ async function configureProjectCommand(args) {
3256
3360
  let explicitProjectMode = getFlag(args, 'mode');
3257
3361
  if (hasFlag(args, 'interactive')) {
3258
3362
  await withPromptSession(async (prompt) => {
3259
- instanceName = await prompt.line('Which instance should this project use', instanceName);
3260
- explicitUrl = await prompt.line('What Iranti URL should this project talk to', explicitUrl ?? existing.IRANTI_URL);
3261
- explicitApiKey = await prompt.secret('What API key should this project use', explicitApiKey ?? existing.IRANTI_API_KEY);
3262
- explicitAgentId = await prompt.line('What agent id should this project use', explicitAgentId ?? existing.IRANTI_AGENT_ID ?? projectAgentDefault(projectPath));
3263
- explicitMemoryEntity = await prompt.line('What memory entity should this project use', explicitMemoryEntity ?? existing.IRANTI_MEMORY_ENTITY ?? 'user/main');
3264
- explicitProjectMode = await prompt.line('Should this project be isolated or shared', explicitProjectMode ?? existing.IRANTI_PROJECT_MODE ?? inferProjectMode(projectPath, existing.IRANTI_INSTANCE_ENV));
3363
+ instanceName = await prompt.line('Instance name', instanceName);
3364
+ explicitUrl = await prompt.line('Iranti URL', explicitUrl ?? existing.IRANTI_URL);
3365
+ explicitApiKey = await prompt.secret('Project API key', explicitApiKey ?? existing.IRANTI_API_KEY);
3366
+ explicitAgentId = await prompt.line('Project agent ID', explicitAgentId ?? existing.IRANTI_AGENT_ID ?? projectAgentDefault(projectPath));
3367
+ explicitMemoryEntity = await prompt.line('Project memory entity', explicitMemoryEntity ?? existing.IRANTI_MEMORY_ENTITY ?? 'user/main');
3368
+ explicitProjectMode = await prompt.line('Project mode (isolated or shared)', explicitProjectMode ?? existing.IRANTI_PROJECT_MODE ?? inferProjectMode(projectPath, existing.IRANTI_INSTANCE_ENV));
3265
3369
  });
3266
3370
  }
3267
3371
  let instanceEnvFile = existing.IRANTI_INSTANCE_ENV;
@@ -3591,7 +3695,7 @@ async function claudeSetupCommand(args) {
3591
3695
  ?? (args.command === 'claude-setup' ? args.subcommand ?? undefined : undefined);
3592
3696
  const scanDir = path_1.default.resolve(dirArg ?? process.cwd());
3593
3697
  if (!fs_1.default.existsSync(scanDir)) {
3594
- throw new Error(`Scan directory not found: ${scanDir}`);
3698
+ throw cliError('IRANTI_SCAN_PATH_NOT_FOUND', `Scan directory not found: ${scanDir}`);
3595
3699
  }
3596
3700
  const candidates = findClaudeProjects(scanDir, recursive);
3597
3701
  if (candidates.length === 0) {
@@ -3638,10 +3742,10 @@ async function claudeSetupCommand(args) {
3638
3742
  ? path_1.default.resolve(explicitProjectEnv)
3639
3743
  : path_1.default.join(projectPath, '.env.iranti');
3640
3744
  if (!fs_1.default.existsSync(projectPath)) {
3641
- throw new Error(`Project path not found: ${projectPath}`);
3745
+ throw cliError('IRANTI_PROJECT_PATH_NOT_FOUND', `Project path not found: ${projectPath}`);
3642
3746
  }
3643
3747
  if (!fs_1.default.existsSync(projectEnvPath)) {
3644
- throw new Error(`Project binding not found at ${projectEnvPath}. Run \`iranti project init\` or \`iranti configure project\` first.`);
3748
+ throw cliError('IRANTI_PROJECT_BINDING_MISSING', `Project binding not found at ${projectEnvPath}. Run \`iranti project init\` or \`iranti configure project\` first.`, ['Run `iranti project init . --instance <name>` before `iranti claude-setup`.'], { projectEnvPath });
3645
3749
  }
3646
3750
  const result = await writeClaudeCodeProjectFiles(projectPath, projectEnvPath, force);
3647
3751
  console.log(`${okLabel()} Claude Code integration scaffolded`);
@@ -3675,24 +3779,27 @@ function printHelp() {
3675
3779
  const printRows = (title, entries) => {
3676
3780
  console.log(sectionTitle(title));
3677
3781
  for (const [command, description] of entries) {
3678
- console.log(` ${commandText(command.padEnd(72))} ${description}`);
3782
+ console.log(` ${commandText(command)}`);
3783
+ console.log(` ${description}`);
3679
3784
  }
3680
3785
  console.log('');
3681
3786
  };
3682
3787
  console.log(sectionTitle('Iranti CLI'));
3683
3788
  console.log('Memory infrastructure for multi-agent systems.');
3789
+ console.log('Most instance-aware commands also accept --root <path> in addition to --scope.');
3790
+ console.log('Global debugging flags: --debug for extra diagnostics, --verbose for subprocess trace output.');
3684
3791
  console.log('');
3685
3792
  printRows('Start Here', rows);
3686
3793
  printRows('Setup And Runtime', [
3687
3794
  ['iranti install [--scope user|system] [--root <path>]', 'Initialize the machine-level runtime folders.'],
3688
- ['iranti setup [--scope user|system] [--root <path>] [--mode isolated|shared] [--config <file> | --defaults] [--db-mode local|managed|docker] [--db-url <url>] [--bootstrap-db]', 'Guided setup for runtime, database, instance, keys, and project binding.'],
3795
+ ['iranti setup [--scope user|system] [--root <path>] [--mode isolated|shared] [--instance <name>] [--port <n>] [--config <file> | --defaults] [--db-mode local|managed|docker] [--db-url <url>] [--provider <name>] [--api-key <token>] [--projects <path1,path2>] [--claude-code] [--bootstrap-db]', 'Guided setup for runtime, database, instance, keys, and project binding. Run iranti setup --help for the non-interactive flow.'],
3689
3796
  ['iranti instance create <name> [--port 3001] [--db-url <url>] [--api-key <token>] [--provider <name>] [--provider-key <token>] [--scope user|system]', 'Create an instance directly if you want low-level control.'],
3690
3797
  ['iranti instance list [--scope user|system]', 'List configured instances.'],
3691
3798
  ['iranti instance show <name> [--scope user|system]', 'Show one instance env, port, and database target.'],
3692
3799
  ['iranti run --instance <name> [--scope user|system]', 'Start an instance.'],
3693
3800
  ]);
3694
3801
  printRows('Configuration', [
3695
- ['iranti configure instance <name> [--interactive] [--db-url <url>] [--port <n>] [--api-key <token>] [--provider <name>] [--provider-key <token>] [--clear-provider-key]', 'Update an instance without editing env files manually.'],
3802
+ ['iranti configure instance <name> [--interactive] [--db-url <url>] [--port <n>] [--api-key <token>] [--provider <name>] [--provider-key <token>] [--clear-provider-key] [--json]', 'Update an instance without editing env files manually.'],
3696
3803
  ['iranti project init [path] --instance <name> [--api-key <token>] [--agent-id <id>] [--mode isolated|shared] [--force]', 'Create a new .env.iranti binding for one project.'],
3697
3804
  ['iranti configure project [path] [--interactive] [--instance <name>] [--url <http://host:port>] [--api-key <token>] [--agent-id <id>] [--memory-entity <entity>] [--mode isolated|shared] [--json]', 'Refresh or retarget an existing project binding.'],
3698
3805
  ]);
@@ -3706,7 +3813,7 @@ function printHelp() {
3706
3813
  ['iranti remove api-key [provider] [--instance <name>] [--project <path>] [--json]', 'Remove a stored provider key.'],
3707
3814
  ]);
3708
3815
  printRows('Diagnostics And Operator Tools', [
3709
- ['iranti doctor [--instance <name>] [--scope user|system] [--env <file>] [--json]', 'Run environment and runtime diagnostics.'],
3816
+ ['iranti doctor [--instance <name>] [--scope user|system] [--env <file>] [--json] [--debug]', 'Run environment and runtime diagnostics.'],
3710
3817
  ['iranti status [--scope user|system] [--json]', 'Show runtime roots, bindings, and known instances.'],
3711
3818
  ['iranti upgrade [--check] [--dry-run] [--yes] [--all] [--target auto|npm-global|npm-repo|python[,python]] [--json]', 'Check or run CLI/runtime/package upgrades.'],
3712
3819
  ['iranti handshake [--instance <name> | --project-env <file>] [--agent <id>] [--task <text>] [--recent <msg1||msg2>] [--recent-file <path>] [--json]', 'Manually inspect Attendant handshake output.'],
@@ -3738,22 +3845,31 @@ function printHelp() {
3738
3845
  console.log(` ${commandText('iranti auth create-key --instance local --key-id app_main --owner \"App Main\" --scopes \"kb:read,kb:write,memory:read,memory:write\"')}`);
3739
3846
  console.log(` ${commandText('iranti add api-key openai --instance local --set-default')}`);
3740
3847
  }
3848
+ function printSetupHelp() {
3849
+ console.log(sectionTitle('Setup Command'));
3850
+ console.log(` ${commandText('iranti setup [--scope user|system] [--root <path>] [--mode isolated|shared] [--instance <name>] [--port <n>] [--config <file> | --defaults] [--db-mode local|managed|docker] [--db-url <url>] [--provider <name>] [--api-key <token>] [--projects <path1,path2>] [--claude-code] [--bootstrap-db]')}`);
3851
+ console.log('');
3852
+ console.log(' Interactive mode walks through runtime, database, provider keys, API keys, and project binding.');
3853
+ console.log(' Use `--defaults` to build a plan from flags and environment variables without prompts.');
3854
+ console.log(' Use `--config <file>` to execute a saved setup plan.');
3855
+ console.log(' `--projects` and `--claude-code` apply to the non-interactive defaults flow.');
3856
+ }
3741
3857
  function printInstanceHelp() {
3742
3858
  console.log(sectionTitle('Instance Commands'));
3743
- console.log(` ${commandText('iranti instance create <name> [--port 3001] [--db-url <url>] [--api-key <token>] [--provider <name>] [--provider-key <token>] [--scope user|system]')}`);
3744
- console.log(` ${commandText('iranti instance list [--scope user|system]')}`);
3745
- console.log(` ${commandText('iranti instance show <name> [--scope user|system]')}`);
3859
+ console.log(` ${commandText('iranti instance create <name> [--port 3001] [--db-url <url>] [--api-key <token>] [--provider <name>] [--provider-key <token>] [--scope user|system] [--root <path>]')}`);
3860
+ console.log(` ${commandText('iranti instance list [--scope user|system] [--root <path>]')}`);
3861
+ console.log(` ${commandText('iranti instance show <name> [--scope user|system] [--root <path>]')}`);
3746
3862
  }
3747
3863
  function printConfigureHelp() {
3748
3864
  console.log(sectionTitle('Configure Commands'));
3749
- console.log(` ${commandText('iranti configure instance <name> [--interactive] [--db-url <url>] [--port <n>] [--api-key <token>] [--provider <name>] [--provider-key <token>] [--clear-provider-key]')}`);
3750
- console.log(` ${commandText('iranti configure project [path] [--interactive] [--instance <name>] [--url <http://host:port>] [--api-key <token>] [--agent-id <id>] [--memory-entity <entity>] [--mode isolated|shared] [--json]')}`);
3865
+ console.log(` ${commandText('iranti configure instance <name> [--interactive] [--db-url <url>] [--port <n>] [--api-key <token>] [--provider <name>] [--provider-key <token>] [--clear-provider-key] [--scope user|system] [--root <path>] [--json]')}`);
3866
+ console.log(` ${commandText('iranti configure project [path] [--interactive] [--instance <name>] [--url <http://host:port>] [--api-key <token>] [--agent-id <id>] [--memory-entity <entity>] [--mode isolated|shared] [--scope user|system] [--root <path>] [--json]')}`);
3751
3867
  }
3752
3868
  function printAuthHelp() {
3753
3869
  console.log(sectionTitle('Auth Commands'));
3754
- console.log(` ${commandText('iranti auth create-key --instance <name> --key-id <id> --owner <owner> [--scopes ...] [--description <text>] [--write-instance] [--project <path>] [--agent-id <id>] [--json]')}`);
3755
- console.log(` ${commandText('iranti auth list-keys --instance <name> [--json]')}`);
3756
- console.log(` ${commandText('iranti auth revoke-key --instance <name> --key-id <id> [--json]')}`);
3870
+ console.log(` ${commandText('iranti auth create-key --instance <name> --key-id <id> --owner <owner> [--scopes ...] [--description <text>] [--write-instance] [--project <path>] [--agent-id <id>] [--scope user|system] [--root <path>] [--json]')}`);
3871
+ console.log(` ${commandText('iranti auth list-keys --instance <name> [--scope user|system] [--root <path>] [--json]')}`);
3872
+ console.log(` ${commandText('iranti auth revoke-key --instance <name> --key-id <id> [--scope user|system] [--root <path>] [--json]')}`);
3757
3873
  }
3758
3874
  function printIntegrateHelp() {
3759
3875
  console.log(sectionTitle('Integrations'));
@@ -3761,8 +3877,23 @@ function printIntegrateHelp() {
3761
3877
  console.log(` ${commandText('iranti integrate claude --scan <dir> [--recursive] [--force]')}`);
3762
3878
  console.log(` ${commandText('iranti integrate codex [--name iranti] [--agent codex_code] [--source Codex] [--provider openai] [--project-env <path>] [--local-script]')}`);
3763
3879
  }
3880
+ function printProviderKeyHelp() {
3881
+ console.log(sectionTitle('Provider Key Commands'));
3882
+ console.log(` ${commandText('iranti list api-keys [--instance <name>] [--project <path>] [--json]')}`);
3883
+ console.log(` ${commandText('iranti add api-key [provider] [--instance <name>] [--project <path>] [--key <token>] [--set-default] [--json]')}`);
3884
+ console.log(` ${commandText('iranti update api-key [provider] [--instance <name>] [--project <path>] [--key <token>] [--set-default] [--json]')}`);
3885
+ console.log(` ${commandText('iranti remove api-key [provider] [--instance <name>] [--project <path>] [--json]')}`);
3886
+ console.log('');
3887
+ console.log(' Target either an instance env or a project binding. If neither is supplied, the CLI will try the current project first.');
3888
+ }
3764
3889
  async function main() {
3765
3890
  const args = parseArgs(process.argv.slice(2));
3891
+ setCliDebugFlags(args);
3892
+ debugLog('CLI invocation started.', {
3893
+ command: args.command,
3894
+ subcommand: args.subcommand,
3895
+ cwd: process.cwd(),
3896
+ });
3766
3897
  if (!args.command || args.command === 'help' || args.command === '--help') {
3767
3898
  printHelp();
3768
3899
  return;
@@ -3772,6 +3903,10 @@ async function main() {
3772
3903
  return;
3773
3904
  }
3774
3905
  if (args.command === 'setup') {
3906
+ if (hasFlag(args, 'help')) {
3907
+ printSetupHelp();
3908
+ return;
3909
+ }
3775
3910
  await setupCommand(args);
3776
3911
  return;
3777
3912
  }
@@ -3833,18 +3968,34 @@ async function main() {
3833
3968
  throw new Error(`Unknown auth subcommand '${args.subcommand ?? ''}'.`);
3834
3969
  }
3835
3970
  if (args.command === 'list' && args.subcommand === 'api-keys') {
3971
+ if (hasFlag(args, 'help')) {
3972
+ printProviderKeyHelp();
3973
+ return;
3974
+ }
3836
3975
  await listProviderKeysCommand(args);
3837
3976
  return;
3838
3977
  }
3839
3978
  if (args.command === 'add' && args.subcommand === 'api-key') {
3979
+ if (hasFlag(args, 'help')) {
3980
+ printProviderKeyHelp();
3981
+ return;
3982
+ }
3840
3983
  await upsertProviderKeyCommand(args, 'add');
3841
3984
  return;
3842
3985
  }
3843
3986
  if (args.command === 'update' && args.subcommand === 'api-key') {
3987
+ if (hasFlag(args, 'help')) {
3988
+ printProviderKeyHelp();
3989
+ return;
3990
+ }
3844
3991
  await upsertProviderKeyCommand(args, 'update');
3845
3992
  return;
3846
3993
  }
3847
3994
  if (args.command === 'remove' && args.subcommand === 'api-key') {
3995
+ if (hasFlag(args, 'help')) {
3996
+ printProviderKeyHelp();
3997
+ return;
3998
+ }
3848
3999
  await removeProviderKeyCommand(args);
3849
4000
  return;
3850
4001
  }
@@ -3911,11 +4062,27 @@ async function main() {
3911
4062
  }
3912
4063
  throw new Error(`Unknown integrate target '${args.subcommand ?? ''}'. Use 'claude' or 'codex'.`);
3913
4064
  }
3914
- throw new Error(`Unknown command '${args.command}'. Run: iranti help`);
4065
+ throw cliError('IRANTI_UNKNOWN_COMMAND', `Unknown command '${args.command}'. Run: iranti help`, ['Use `iranti help` to see the current command surface.'], { command: args.command });
3915
4066
  }
3916
4067
  main().catch((err) => {
3917
4068
  const message = err instanceof Error ? err.message : String(err);
3918
- console.error(`${failLabel('ERROR')} ${message}`);
4069
+ const code = err instanceof CliError ? err.code : null;
4070
+ console.error(`${failLabel('ERROR')}${code ? ` [${code}]` : ''} ${message}`);
4071
+ if (err instanceof CliError && err.hints.length > 0) {
4072
+ console.error('');
4073
+ console.error('Possible fixes:');
4074
+ for (const hint of err.hints) {
4075
+ console.error(` - ${hint}`);
4076
+ }
4077
+ }
4078
+ if (CLI_DEBUG && err instanceof CliError && err.details && Object.keys(err.details).length > 0) {
4079
+ console.error('');
4080
+ console.error(`${paint('[DEBUG]', 'gray')} ${JSON.stringify(err.details, null, 2)}`);
4081
+ }
4082
+ if (CLI_DEBUG && err instanceof Error && err.stack) {
4083
+ console.error('');
4084
+ console.error(err.stack);
4085
+ }
3919
4086
  process.exit(1);
3920
4087
  });
3921
4088
  //# sourceMappingURL=iranti-cli.js.map
@@ -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.10',
147
+ version: '0.2.12',
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.10',
18
+ version: '0.2.12',
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.10',
42
+ version: '0.2.12',
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.10',
64
+ version: '0.2.12',
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.10' },
85
+ valueRaw: { version: '0.2.12' },
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.10',
98
+ seedVersion: '0.2.12',
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.10',
111
+ version: '0.2.12',
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.10',
159
+ version: '0.2.12',
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.10',
190
+ version: '0.2.12',
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.10',
204
+ version: '0.2.12',
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.10',
240
+ version: '0.2.12',
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.10',
72
+ version: '0.2.12',
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.10",
3
+ "version": "0.2.12",
4
4
  "description": "Memory infrastructure for multi-agent AI systems",
5
5
  "main": "dist/src/sdk/index.js",
6
6
  "files": [