iranti 0.2.41 → 0.2.44

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.
@@ -75,11 +75,11 @@ function printHelp() {
75
75
  'Notes:',
76
76
  ' - Registers a global Codex MCP entry using `codex mcp add`.',
77
77
  ' - Prefers the installed CLI path: `iranti mcp`.',
78
- ' - When a project binding is available, also writes or merges a project-local `.mcp.json` entry pinned to that binding.',
78
+ ' - When a project binding is available, also writes or merges project-local `.mcp.json` and `.vscode/mcp.json` entries pinned to that binding.',
79
79
  ' - By default does not pin IRANTI_PROJECT_ENV, so Codex can resolve .env.iranti from the active project/workspace at runtime.',
80
80
  ' - Use --project-env only when you deliberately want to pin Codex globally to one project binding.',
81
81
  ' - Use --local-script only if you need to point Codex at this repo build directly.',
82
- ' - Use --no-workspace-file only if you explicitly want global registration without a project-local `.mcp.json` update.',
82
+ ' - Use --no-workspace-file only if you explicitly want global registration without project-local MCP file updates.',
83
83
  ' - Does not store DATABASE_URL in Codex config; iranti-mcp loads project/instance env at runtime.',
84
84
  ' - Replaces any existing MCP entry with the same name.',
85
85
  ].join('\n'));
@@ -173,6 +173,29 @@ function makeWorkspaceMcpServer(options, projectEnv) {
173
173
  env,
174
174
  };
175
175
  }
176
+ function makeVsCodeWorkspaceMcpServer(options, projectEnv) {
177
+ const projectPath = node_path_1.default.dirname(projectEnv);
178
+ const env = {
179
+ IRANTI_MCP_DEFAULT_AGENT: options.agent,
180
+ IRANTI_MCP_DEFAULT_SOURCE: options.source,
181
+ };
182
+ if (options.provider) {
183
+ env.LLM_PROVIDER = options.provider;
184
+ }
185
+ const localBinding = node_path_1.default.join(projectPath, '.env.iranti');
186
+ if (node_path_1.default.resolve(projectEnv) !== node_path_1.default.resolve(localBinding)) {
187
+ env.IRANTI_PROJECT_ENV = projectEnv;
188
+ }
189
+ return {
190
+ type: 'stdio',
191
+ command: 'iranti',
192
+ args: ['mcp'],
193
+ ...(node_path_1.default.resolve(projectEnv) === node_path_1.default.resolve(localBinding)
194
+ ? { envFile: '${workspaceFolder}/.env.iranti' }
195
+ : {}),
196
+ env,
197
+ };
198
+ }
176
199
  function writeWorkspaceMcpFile(projectEnv, options) {
177
200
  const projectPath = node_path_1.default.dirname(projectEnv);
178
201
  const mcpFile = node_path_1.default.join(projectPath, '.mcp.json');
@@ -211,6 +234,46 @@ function writeWorkspaceMcpFile(projectEnv, options) {
211
234
  }, null, 2)}\n`, 'utf8');
212
235
  return { filePath: mcpFile, status: 'updated' };
213
236
  }
237
+ function writeWorkspaceVsCodeMcpFile(projectEnv, options) {
238
+ const projectPath = node_path_1.default.dirname(projectEnv);
239
+ const vscodeDir = node_path_1.default.join(projectPath, '.vscode');
240
+ const mcpFile = node_path_1.default.join(vscodeDir, 'mcp.json');
241
+ const nextServer = makeVsCodeWorkspaceMcpServer(options, projectEnv);
242
+ node_fs_1.default.mkdirSync(vscodeDir, { recursive: true });
243
+ if (!node_fs_1.default.existsSync(mcpFile)) {
244
+ node_fs_1.default.writeFileSync(mcpFile, `${JSON.stringify({
245
+ servers: {
246
+ iranti: nextServer,
247
+ },
248
+ }, null, 2)}\n`, 'utf8');
249
+ return { filePath: mcpFile, status: 'created' };
250
+ }
251
+ let existing;
252
+ try {
253
+ existing = JSON.parse(node_fs_1.default.readFileSync(mcpFile, 'utf8'));
254
+ }
255
+ catch {
256
+ throw new Error(`Existing .vscode/mcp.json is not valid JSON: ${mcpFile}`);
257
+ }
258
+ if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
259
+ throw new Error(`Existing .vscode/mcp.json must contain a JSON object: ${mcpFile}`);
260
+ }
261
+ const existingServers = existing.servers && typeof existing.servers === 'object' && !Array.isArray(existing.servers)
262
+ ? existing.servers
263
+ : {};
264
+ const currentIranti = existingServers.iranti;
265
+ if (JSON.stringify(currentIranti) === JSON.stringify(nextServer)) {
266
+ return { filePath: mcpFile, status: 'unchanged' };
267
+ }
268
+ node_fs_1.default.writeFileSync(mcpFile, `${JSON.stringify({
269
+ ...existing,
270
+ servers: {
271
+ ...existingServers,
272
+ iranti: nextServer,
273
+ },
274
+ }, null, 2)}\n`, 'utf8');
275
+ return { filePath: mcpFile, status: 'updated' };
276
+ }
214
277
  function canUseInstalledIranti(repoRoot) {
215
278
  try {
216
279
  run('iranti', ['mcp', '--help'], repoRoot);
@@ -267,8 +330,11 @@ function main() {
267
330
  const workspaceProjectEnv = options.writeWorkspaceFile
268
331
  ? resolveWorkspaceProjectEnv(options)
269
332
  : undefined;
270
- const workspaceMcpResult = workspaceProjectEnv
271
- ? writeWorkspaceMcpFile(workspaceProjectEnv, options)
333
+ const workspaceFilesResult = workspaceProjectEnv
334
+ ? {
335
+ mcp: writeWorkspaceMcpFile(workspaceProjectEnv, options),
336
+ vscode: writeWorkspaceVsCodeMcpFile(workspaceProjectEnv, options),
337
+ }
272
338
  : null;
273
339
  const registered = run('codex', ['mcp', 'get', options.name], repoRoot);
274
340
  console.log(registered);
@@ -296,11 +362,12 @@ function main() {
296
362
  console.log(`Launch with: codex -C "${repoRoot}"`);
297
363
  }
298
364
  if (options.writeWorkspaceFile) {
299
- if (workspaceMcpResult) {
300
- console.log(`Workspace .mcp.json: ${workspaceMcpResult.status} (${workspaceMcpResult.filePath})`);
365
+ if (workspaceFilesResult) {
366
+ console.log(`Workspace .mcp.json: ${workspaceFilesResult.mcp.status} (${workspaceFilesResult.mcp.filePath})`);
367
+ console.log(`Workspace .vscode/mcp.json: ${workspaceFilesResult.vscode.status} (${workspaceFilesResult.vscode.filePath})`);
301
368
  }
302
369
  else {
303
- console.log('Workspace .mcp.json: unchanged (no project binding found from the current working directory)');
370
+ console.log('Workspace MCP files: unchanged (no project binding found from the current working directory)');
304
371
  }
305
372
  }
306
373
  }
@@ -22,6 +22,7 @@ const commandErrors_1 = require("../src/lib/commandErrors");
22
22
  const commandInvocation_1 = require("../src/lib/commandInvocation");
23
23
  const runtimeEnv_1 = require("../src/lib/runtimeEnv");
24
24
  const fileMutation_1 = require("../src/lib/fileMutation");
25
+ const runtimeDependencies_1 = require("../src/lib/runtimeDependencies");
25
26
  const resolutionist_1 = require("../src/resolutionist");
26
27
  const chat_1 = require("../src/chat");
27
28
  const backends_1 = require("../src/library/backends");
@@ -606,6 +607,17 @@ async function readEnvFile(filePath) {
606
607
  }
607
608
  return out;
608
609
  }
610
+ async function readInstanceMetaFile(metaFile) {
611
+ if (!fs_1.default.existsSync(metaFile))
612
+ return null;
613
+ try {
614
+ const raw = await promises_1.default.readFile(metaFile, 'utf8');
615
+ return JSON.parse(raw);
616
+ }
617
+ catch {
618
+ return null;
619
+ }
620
+ }
609
621
  function makeInstanceEnv(name, port, dbUrl, apiKey, instanceDir) {
610
622
  const lines = [
611
623
  '# Iranti instance env',
@@ -769,6 +781,10 @@ async function inspectInstanceConfig(root, name) {
769
781
  if (typeof parsed.envFile === 'string' && path_1.default.resolve(parsed.envFile) !== path_1.default.resolve(envFile)) {
770
782
  ownershipIssues.push(`instance.json envFile points to ${parsed.envFile}`);
771
783
  }
784
+ const dependencyValidation = (0, runtimeDependencies_1.parseInstanceDependencies)(parsed.dependencies);
785
+ if (dependencyValidation.errors.length > 0) {
786
+ ownershipIssues.push(`instance.json dependencies invalid: ${dependencyValidation.errors.join(', ')}`);
787
+ }
772
788
  }
773
789
  }
774
790
  catch {
@@ -1133,6 +1149,10 @@ function sanitizeIdentifier(input, fallback) {
1133
1149
  }
1134
1150
  return value || fallback;
1135
1151
  }
1152
+ function normalizeDockerContainerName(input, fallback) {
1153
+ const trimmed = input?.trim() ?? '';
1154
+ return trimmed || fallback;
1155
+ }
1136
1156
  function projectAgentDefault(projectPath) {
1137
1157
  return `${sanitizeIdentifier(path_1.default.basename(projectPath), 'project')}_main`;
1138
1158
  }
@@ -1266,6 +1286,7 @@ async function ensureInstanceConfigured(root, name, config) {
1266
1286
  port: config.port,
1267
1287
  envFile,
1268
1288
  instanceDir,
1289
+ ...(config.dependencies && config.dependencies.length > 0 ? { dependencies: config.dependencies } : {}),
1269
1290
  };
1270
1291
  await writeJson(metaFile, meta);
1271
1292
  }
@@ -1276,7 +1297,9 @@ async function ensureInstanceConfigured(root, name, config) {
1276
1297
  LLM_PROVIDER: config.provider,
1277
1298
  ...config.providerKeys,
1278
1299
  });
1279
- await syncInstanceMeta(root, name, config.port);
1300
+ await syncInstanceMeta(root, name, config.port, {
1301
+ ...(config.dependencies !== undefined ? { dependencies: config.dependencies } : {}),
1302
+ });
1280
1303
  return { envFile, instanceDir, created };
1281
1304
  }
1282
1305
  function makeIrantiMcpServerConfig(projectEnvPath) {
@@ -1292,6 +1315,23 @@ function makeIrantiMcpServerConfig(projectEnvPath) {
1292
1315
  : {}),
1293
1316
  };
1294
1317
  }
1318
+ function makeVsCodeIrantiMcpServerConfig(projectPath, projectEnvPath) {
1319
+ const resolvedProjectEnvPath = projectEnvPath ? path_1.default.resolve(projectEnvPath) : undefined;
1320
+ const localProjectEnvPath = path_1.default.join(projectPath, '.env.iranti');
1321
+ const env = {};
1322
+ if (resolvedProjectEnvPath && path_1.default.resolve(localProjectEnvPath) !== resolvedProjectEnvPath) {
1323
+ env.IRANTI_PROJECT_ENV = resolvedProjectEnvPath;
1324
+ }
1325
+ return {
1326
+ type: 'stdio',
1327
+ command: 'iranti',
1328
+ args: ['mcp'],
1329
+ ...(resolvedProjectEnvPath && path_1.default.resolve(localProjectEnvPath) === resolvedProjectEnvPath
1330
+ ? { envFile: '${workspaceFolder}/.env.iranti' }
1331
+ : {}),
1332
+ ...(Object.keys(env).length > 0 ? { env } : {}),
1333
+ };
1334
+ }
1295
1335
  function applyEnvMap(vars) {
1296
1336
  for (const [key, value] of Object.entries(vars)) {
1297
1337
  process.env[key] = value;
@@ -1638,6 +1678,49 @@ async function writeClaudeCodeProjectFiles(projectPath, projectEnvPath, force =
1638
1678
  }
1639
1679
  }
1640
1680
  }
1681
+ const vscodeDir = path_1.default.join(projectPath, '.vscode');
1682
+ const vscodeMcpFile = path_1.default.join(vscodeDir, 'mcp.json');
1683
+ let vscodeMcpStatus = 'unchanged';
1684
+ const vscodeMcpServer = makeVsCodeIrantiMcpServerConfig(projectPath, resolvedProjectEnvPath);
1685
+ await ensureDir(vscodeDir);
1686
+ if (!fs_1.default.existsSync(vscodeMcpFile)) {
1687
+ await writeText(vscodeMcpFile, `${JSON.stringify({
1688
+ servers: {
1689
+ iranti: vscodeMcpServer,
1690
+ },
1691
+ }, null, 2)}\n`);
1692
+ vscodeMcpStatus = 'created';
1693
+ }
1694
+ else {
1695
+ const existing = readJsonFile(vscodeMcpFile);
1696
+ if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
1697
+ if (!force) {
1698
+ throw new Error(`Existing .vscode/mcp.json is not valid JSON. Re-run with --force to overwrite it: ${vscodeMcpFile}`);
1699
+ }
1700
+ await writeText(vscodeMcpFile, `${JSON.stringify({
1701
+ servers: {
1702
+ iranti: vscodeMcpServer,
1703
+ },
1704
+ }, null, 2)}\n`);
1705
+ vscodeMcpStatus = 'updated';
1706
+ }
1707
+ else {
1708
+ const existingServers = existing.servers && typeof existing.servers === 'object' && !Array.isArray(existing.servers)
1709
+ ? existing.servers
1710
+ : {};
1711
+ const hasIranti = Object.prototype.hasOwnProperty.call(existingServers, 'iranti');
1712
+ if (!hasIranti || force) {
1713
+ await writeText(vscodeMcpFile, `${JSON.stringify({
1714
+ ...existing,
1715
+ servers: {
1716
+ ...existingServers,
1717
+ iranti: vscodeMcpServer,
1718
+ },
1719
+ }, null, 2)}\n`);
1720
+ vscodeMcpStatus = 'updated';
1721
+ }
1722
+ }
1723
+ }
1641
1724
  const claudeDir = path_1.default.join(projectPath, '.claude');
1642
1725
  await ensureDir(claudeDir);
1643
1726
  const settingsFile = path_1.default.join(claudeDir, 'settings.local.json');
@@ -1661,6 +1744,7 @@ async function writeClaudeCodeProjectFiles(projectPath, projectEnvPath, force =
1661
1744
  }
1662
1745
  return {
1663
1746
  mcp: mcpStatus,
1747
+ vscodeMcp: vscodeMcpStatus,
1664
1748
  settings: settingsStatus,
1665
1749
  };
1666
1750
  }
@@ -1790,26 +1874,22 @@ async function chooseAvailablePort(session, promptText, preferredPort, allowOccu
1790
1874
  suggested = next;
1791
1875
  }
1792
1876
  }
1793
- async function syncInstanceMeta(root, name, port) {
1877
+ async function syncInstanceMeta(root, name, port, options = {}) {
1794
1878
  const { instanceDir, envFile, metaFile } = instancePaths(root, name);
1795
- const existingCreatedAt = fs_1.default.existsSync(metaFile)
1796
- ? await promises_1.default.readFile(metaFile, 'utf-8')
1797
- .then((raw) => {
1798
- try {
1799
- const parsed = JSON.parse(raw);
1800
- return typeof parsed.createdAt === 'string' ? parsed.createdAt : undefined;
1801
- }
1802
- catch {
1803
- return undefined;
1804
- }
1805
- })
1806
- : undefined;
1879
+ const existing = await readInstanceMetaFile(metaFile);
1880
+ const existingCreatedAt = typeof existing?.createdAt === 'string' ? existing.createdAt : undefined;
1881
+ const existingDependencies = (0, runtimeDependencies_1.parseInstanceDependencies)(existing?.dependencies).dependencies;
1807
1882
  const meta = {
1808
1883
  name,
1809
1884
  createdAt: existingCreatedAt ?? new Date().toISOString(),
1810
1885
  port,
1811
1886
  envFile,
1812
1887
  instanceDir,
1888
+ ...(options.dependencies !== undefined
1889
+ ? { dependencies: options.dependencies }
1890
+ : existingDependencies.length > 0
1891
+ ? { dependencies: existingDependencies }
1892
+ : {}),
1813
1893
  };
1814
1894
  await writeJson(metaFile, meta);
1815
1895
  }
@@ -1915,6 +1995,7 @@ async function executeSetupPlan(plan) {
1915
1995
  provider: plan.provider,
1916
1996
  providerKeys: plan.providerKeys,
1917
1997
  apiKey: plan.apiKey,
1998
+ dependencies: plan.dependencies,
1918
1999
  });
1919
2000
  if (plan.bootstrapDatabase) {
1920
2001
  try {
@@ -2060,6 +2141,12 @@ function parseSetupConfig(filePath) {
2060
2141
  codex: Boolean(raw?.codex),
2061
2142
  codexAgent: raw?.codexAgent ? sanitizeIdentifier(String(raw.codexAgent), 'codex_code') : undefined,
2062
2143
  bootstrapDatabase: Boolean(raw?.bootstrapDatabase),
2144
+ dependencies: inferSetupDependencies({
2145
+ databaseMode,
2146
+ databaseUrl,
2147
+ dockerContainerName: typeof raw?.dockerContainerName === 'string' ? raw.dockerContainerName : undefined,
2148
+ instanceName,
2149
+ }),
2063
2150
  };
2064
2151
  }
2065
2152
  function defaultsSetupPlan(args) {
@@ -2135,6 +2222,12 @@ function defaultsSetupPlan(args) {
2135
2222
  codex: hasFlag(args, 'codex'),
2136
2223
  codexAgent: sanitizeIdentifier(getFlag(args, 'codex-agent') ?? 'codex_code', 'codex_code'),
2137
2224
  bootstrapDatabase: hasFlag(args, 'bootstrap-db'),
2225
+ dependencies: inferSetupDependencies({
2226
+ databaseMode,
2227
+ databaseUrl,
2228
+ dockerContainerName: getFlag(args, 'docker-container-name'),
2229
+ instanceName,
2230
+ }),
2138
2231
  };
2139
2232
  }
2140
2233
  function detectProviderKey(provider, env) {
@@ -2231,6 +2324,39 @@ function summarizeStatus(checks) {
2231
2324
  return 'warn';
2232
2325
  return 'pass';
2233
2326
  }
2327
+ function detectVsCodeMcpWorkspaceCheck(projectEnvPath) {
2328
+ const vscodeMcpPath = path_1.default.join(path_1.default.dirname(projectEnvPath), '.vscode', 'mcp.json');
2329
+ if (!fs_1.default.existsSync(vscodeMcpPath)) {
2330
+ return {
2331
+ name: 'vscode mcp workspace',
2332
+ status: 'warn',
2333
+ detail: `VS Code MCP workspace file is missing: ${vscodeMcpPath}. Codex VS Code sessions may not expose Iranti tools even when Codex CLI works.`,
2334
+ };
2335
+ }
2336
+ const parsed = readJsonFile(vscodeMcpPath);
2337
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
2338
+ return {
2339
+ name: 'vscode mcp workspace',
2340
+ status: 'warn',
2341
+ detail: `.vscode/mcp.json is not valid JSON: ${vscodeMcpPath}`,
2342
+ };
2343
+ }
2344
+ const servers = parsed.servers && typeof parsed.servers === 'object' && !Array.isArray(parsed.servers)
2345
+ ? parsed.servers
2346
+ : {};
2347
+ if (!Object.prototype.hasOwnProperty.call(servers, 'iranti')) {
2348
+ return {
2349
+ name: 'vscode mcp workspace',
2350
+ status: 'warn',
2351
+ detail: `.vscode/mcp.json exists but does not expose an \`iranti\` server: ${vscodeMcpPath}`,
2352
+ };
2353
+ }
2354
+ return {
2355
+ name: 'vscode mcp workspace',
2356
+ status: 'pass',
2357
+ detail: `VS Code MCP workspace file is present and exposes \`iranti\`: ${vscodeMcpPath}`,
2358
+ };
2359
+ }
2234
2360
  function collectDoctorRemediations(checks, envSource, envFile) {
2235
2361
  const hints = [];
2236
2362
  const add = (hint) => {
@@ -2269,6 +2395,9 @@ function collectDoctorRemediations(checks, envSource, envFile) {
2269
2395
  if (check.name === 'bound instance env' && check.status !== 'pass') {
2270
2396
  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.');
2271
2397
  }
2398
+ if (check.name === 'vscode mcp workspace' && check.status !== 'pass') {
2399
+ add('Run `iranti codex-setup` from the project root to scaffold `.vscode/mcp.json`, or add an `iranti` server entry there manually for VS Code MCP clients.');
2400
+ }
2272
2401
  if (check.name === 'api key' && check.status !== 'pass') {
2273
2402
  add(envSource === 'project-binding'
2274
2403
  ? 'Set IRANTI_API_KEY in the project binding, or rerun `iranti configure project`.'
@@ -2590,6 +2719,19 @@ function deriveDatabaseUrlForMode(mode, instanceName, explicitDatabaseUrl) {
2590
2719
  }
2591
2720
  return `postgresql://${user}:${password}@localhost:5432/iranti_${instanceName}`;
2592
2721
  }
2722
+ function inferSetupDependencies(plan) {
2723
+ if (plan.databaseMode !== 'docker') {
2724
+ return [];
2725
+ }
2726
+ const parsed = parsePostgresConnectionString(plan.databaseUrl);
2727
+ const hostPort = Number.parseInt(parsed.port || '5432', 10);
2728
+ const containerName = normalizeDockerContainerName(plan.dockerContainerName, `iranti_${plan.instanceName}_db`);
2729
+ return [{
2730
+ kind: 'docker-container',
2731
+ name: containerName,
2732
+ ...(Number.isFinite(hostPort) && hostPort > 0 ? { healthTcpPort: hostPort } : {}),
2733
+ }];
2734
+ }
2593
2735
  async function ensurePostgresDatabaseExists(databaseUrl) {
2594
2736
  const parsed = parsePostgresConnectionString(databaseUrl);
2595
2737
  if (!isLocalPostgresHost(parsed.hostname)) {
@@ -4292,6 +4434,12 @@ async function setupCommand(args) {
4292
4434
  bootstrapDatabase,
4293
4435
  dockerContainerName,
4294
4436
  databaseProvisioned,
4437
+ dependencies: inferSetupDependencies({
4438
+ databaseMode,
4439
+ databaseUrl: dbUrl,
4440
+ dockerContainerName,
4441
+ instanceName,
4442
+ }),
4295
4443
  });
4296
4444
  });
4297
4445
  if (!result) {
@@ -4469,6 +4617,7 @@ async function doctorCommand(args) {
4469
4617
  status: 'pass',
4470
4618
  detail: `IRANTI_URL=${env.IRANTI_URL}`,
4471
4619
  });
4620
+ checks.push(detectVsCodeMcpWorkspaceCheck(envFile));
4472
4621
  }
4473
4622
  if (treatAsProjectBinding) {
4474
4623
  checks.push(detectPlaceholder(env.IRANTI_API_KEY)
@@ -5068,6 +5217,8 @@ async function showInstanceCommand(args) {
5068
5217
  const env = config.state.envPresent && config.state.envReadable
5069
5218
  ? await readEnvFile(envFile)
5070
5219
  : {};
5220
+ const meta = await readInstanceMetaFile(instancePaths(root, name).metaFile);
5221
+ const dependencies = (0, runtimeDependencies_1.parseInstanceDependencies)(meta?.dependencies).dependencies;
5071
5222
  const runtime = await readInstanceRuntimeSummary(root, name);
5072
5223
  console.log(bold(`Instance: ${name}`));
5073
5224
  console.log(` dir : ${instanceDir}`);
@@ -5076,6 +5227,9 @@ async function showInstanceCommand(args) {
5076
5227
  console.log(` port: ${env.IRANTI_PORT ?? '3001'}`);
5077
5228
  console.log(` db : ${env.DATABASE_URL ?? '(missing)'}`);
5078
5229
  console.log(` esc : ${env.IRANTI_ESCALATION_DIR ?? '(missing)'}`);
5230
+ if (dependencies.length > 0) {
5231
+ console.log(` deps: ${dependencies.map((dependency) => (0, runtimeDependencies_1.describeInstanceDependency)(dependency)).join(', ')}`);
5232
+ }
5079
5233
  console.log(` runtime: ${describeInstanceRuntime(runtime)}`);
5080
5234
  if (runtime.state?.healthUrl) {
5081
5235
  console.log(` health: ${runtime.state.healthUrl}`);
@@ -5090,6 +5244,8 @@ async function runInstanceCommand(args) {
5090
5244
  const scope = normalizeScope(getFlag(args, 'scope'));
5091
5245
  const root = resolveInstallRoot(args, scope);
5092
5246
  const { instanceDir, envFile, runtimeFile, env } = await loadInstanceEnv(root, name);
5247
+ const meta = await readInstanceMetaFile(instancePaths(root, name).metaFile);
5248
+ const dependencies = (0, runtimeDependencies_1.parseInstanceDependencies)(meta?.dependencies).dependencies;
5093
5249
  const runtime = await readInstanceRuntimeSummary(root, name);
5094
5250
  if (runtime.running) {
5095
5251
  throw cliError('IRANTI_INSTANCE_ALREADY_RUNNING', `Instance '${name}' is already running on pid ${runtime.state?.pid ?? '(unknown)'}.`, [`Run \`iranti instance restart ${name}\` to restart the live process, or stop the existing process first.`], { instance: name, pid: runtime.state?.pid ?? null, runtimeFile });
@@ -5114,6 +5270,16 @@ async function runInstanceCommand(args) {
5114
5270
  if (!(await isPortUsable(port, '0.0.0.0', listPublishedDockerHostPorts()))) {
5115
5271
  throw cliError('IRANTI_INSTANCE_PORT_IN_USE', `Cannot start instance '${name}' because port ${port} is already in use.`, ['Run `iranti configure instance <name> --port <n>` or free the port before retrying.'], { instance: name, envFile, port });
5116
5272
  }
5273
+ if (dependencies.length > 0) {
5274
+ console.log(`${infoLabel()} Ensuring instance dependencies are ready...`);
5275
+ const ensured = await (0, runtimeDependencies_1.ensureInstanceDependenciesHealthy)(dependencies, { cwd: instanceDir });
5276
+ if (ensured.started.length > 0) {
5277
+ console.log(`${okLabel()} Started dependency containers: ${ensured.started.join(', ')}`);
5278
+ }
5279
+ if (ensured.alreadyRunning.length > 0) {
5280
+ console.log(`${infoLabel()} Dependency containers already running: ${ensured.alreadyRunning.join(', ')}`);
5281
+ }
5282
+ }
5117
5283
  await startInstanceRuntime(name, instanceDir, envFile, runtimeFile);
5118
5284
  }
5119
5285
  async function restartInstanceCommand(args) {
@@ -5186,6 +5352,8 @@ async function configureInstanceCommand(args) {
5186
5352
  const scope = normalizeScope(getFlag(args, 'scope'));
5187
5353
  const root = resolveInstallRoot(args, scope);
5188
5354
  const { instanceDir, envFile, env, config } = await loadInstanceEnv(root, name, { allowRepair: true });
5355
+ const currentMeta = await readInstanceMetaFile(instancePaths(root, name).metaFile);
5356
+ const currentDependencies = (0, runtimeDependencies_1.parseInstanceDependencies)(currentMeta?.dependencies).dependencies;
5189
5357
  const updates = {};
5190
5358
  let portRaw = getFlag(args, 'port');
5191
5359
  let dbUrl = getFlag(args, 'db-url');
@@ -5193,6 +5361,9 @@ async function configureInstanceCommand(args) {
5193
5361
  let providerInput = getFlag(args, 'provider');
5194
5362
  let providerKey = getFlag(args, 'provider-key');
5195
5363
  let clearProviderKey = hasFlag(args, 'clear-provider-key');
5364
+ let dockerContainerName = getFlag(args, 'docker-container');
5365
+ let dockerHealthPortRaw = getFlag(args, 'docker-health-port');
5366
+ const clearDockerContainer = hasFlag(args, 'clear-docker-container');
5196
5367
  if (hasFlag(args, 'interactive')) {
5197
5368
  await withPromptSession(async (prompt) => {
5198
5369
  printWizardNotes('Interactive Instance Configuration', [
@@ -5201,6 +5372,7 @@ async function configureInstanceCommand(args) {
5201
5372
  'DATABASE_URL points at the PostgreSQL database for this instance.',
5202
5373
  'LLM provider and provider key control which model backend Iranti uses.',
5203
5374
  'Iranti API key is the client credential other tools and project bindings use to authenticate.',
5375
+ 'Optional Docker dependency settings let `iranti run --instance` start a recorded backing container before the API boots.',
5204
5376
  ]);
5205
5377
  portRaw = await prompt.line('API port', portRaw ?? env.IRANTI_PORT);
5206
5378
  dbUrl = await prompt.line('DATABASE_URL', dbUrl ?? env.DATABASE_URL);
@@ -5211,6 +5383,9 @@ async function configureInstanceCommand(args) {
5211
5383
  providerKey = await prompt.secret(`${providerDisplayName(interactiveProvider)} API key`, providerKey ?? env[interactiveProviderEnvKey]);
5212
5384
  }
5213
5385
  apiKey = await prompt.secret('Iranti API key', apiKey ?? env.IRANTI_API_KEY);
5386
+ const currentDockerDependency = currentDependencies.find((dependency) => dependency.kind === 'docker-container');
5387
+ dockerContainerName = await prompt.line('Docker dependency container (optional)', dockerContainerName ?? currentDockerDependency?.name);
5388
+ dockerHealthPortRaw = await prompt.line('Docker dependency health TCP port (optional)', dockerHealthPortRaw ?? (currentDockerDependency?.healthTcpPort ? String(currentDockerDependency.healthTcpPort) : ''));
5214
5389
  });
5215
5390
  clearProviderKey = false;
5216
5391
  }
@@ -5242,8 +5417,42 @@ async function configureInstanceCommand(args) {
5242
5417
  }
5243
5418
  updates[envKey] = undefined;
5244
5419
  }
5420
+ let nextDependencies = currentDependencies;
5421
+ if (clearDockerContainer) {
5422
+ nextDependencies = currentDependencies.filter((dependency) => dependency.kind !== 'docker-container');
5423
+ }
5424
+ if (dockerHealthPortRaw && !dockerContainerName) {
5425
+ throw new Error('--docker-health-port requires --docker-container <name>.');
5426
+ }
5427
+ if (dockerContainerName) {
5428
+ const normalizedName = normalizeDockerContainerName(dockerContainerName, 'iranti_db');
5429
+ const dockerHealthPortCandidate = dockerHealthPortRaw
5430
+ ? Number.parseInt(dockerHealthPortRaw, 10)
5431
+ : undefined;
5432
+ if (dockerHealthPortRaw
5433
+ && (dockerHealthPortCandidate === undefined
5434
+ || !Number.isFinite(dockerHealthPortCandidate)
5435
+ || dockerHealthPortCandidate <= 0)) {
5436
+ throw new Error(`Invalid --docker-health-port '${dockerHealthPortRaw}'.`);
5437
+ }
5438
+ const dockerHealthPort = typeof dockerHealthPortCandidate === 'number' && dockerHealthPortCandidate > 0
5439
+ ? dockerHealthPortCandidate
5440
+ : undefined;
5441
+ const withoutDocker = currentDependencies.filter((dependency) => dependency.kind !== 'docker-container');
5442
+ nextDependencies = [
5443
+ ...withoutDocker,
5444
+ {
5445
+ kind: 'docker-container',
5446
+ name: normalizedName,
5447
+ ...(dockerHealthPort ? { healthTcpPort: dockerHealthPort } : {}),
5448
+ },
5449
+ ];
5450
+ }
5245
5451
  if (Object.keys(updates).length === 0) {
5246
- throw new Error('No changes provided. Use flags like --provider, --provider-key, --api-key, --db-url, or --port.');
5452
+ const dependenciesUnchanged = JSON.stringify(nextDependencies) === JSON.stringify(currentDependencies);
5453
+ if (dependenciesUnchanged) {
5454
+ throw new Error('No changes provided. Use flags like --provider, --provider-key, --api-key, --db-url, --port, or the Docker dependency flags.');
5455
+ }
5247
5456
  }
5248
5457
  const nextEnv = { ...env };
5249
5458
  for (const [key, value] of Object.entries(updates)) {
@@ -5264,7 +5473,7 @@ async function configureInstanceCommand(args) {
5264
5473
  }
5265
5474
  await ensureDir(instanceDir);
5266
5475
  await upsertEnvFile(envFile, updates);
5267
- await syncInstanceMeta(root, name, nextPort);
5476
+ await syncInstanceMeta(root, name, nextPort, { dependencies: nextDependencies });
5268
5477
  const json = hasFlag(args, 'json');
5269
5478
  const result = {
5270
5479
  instance: name,
@@ -5273,6 +5482,7 @@ async function configureInstanceCommand(args) {
5273
5482
  provider: updates.LLM_PROVIDER ?? env.LLM_PROVIDER ?? 'mock',
5274
5483
  apiKeyChanged: Boolean(apiKey),
5275
5484
  providerKeyChanged: Boolean(providerKey) || hasFlag(args, 'clear-provider-key'),
5485
+ dependencies: nextDependencies.map((dependency) => (0, runtimeDependencies_1.describeInstanceDependency)(dependency)),
5276
5486
  };
5277
5487
  if (json) {
5278
5488
  console.log(JSON.stringify(result, null, 2));
@@ -5282,6 +5492,9 @@ async function configureInstanceCommand(args) {
5282
5492
  console.log(` status ${okLabel()}`);
5283
5493
  console.log(` env ${envFile}`);
5284
5494
  console.log(` keys ${result.updatedKeys.join(', ')}`);
5495
+ if (result.dependencies.length > 0) {
5496
+ console.log(` deps ${result.dependencies.join(', ')}`);
5497
+ }
5285
5498
  if (apiKey) {
5286
5499
  console.log(` api key ${redactSecret(apiKey)}`);
5287
5500
  }
@@ -5685,7 +5898,7 @@ async function handoffCommand(args) {
5685
5898
  function printClaudeSetupHelp() {
5686
5899
  console.log([
5687
5900
  'Scaffold Claude Code MCP and hook files for the current project.',
5688
- 'Use this when a bound repo should be ready for Claude Code without hand-editing `.mcp.json` or `.claude/settings.local.json`.',
5901
+ 'Use this when a bound repo should be ready for Claude Code without hand-editing `.mcp.json`, `.vscode/mcp.json`, or `.claude/settings.local.json`.',
5689
5902
  '',
5690
5903
  'Usage:',
5691
5904
  ' iranti claude-setup [path] [--project-env <path>] [--force]',
@@ -5699,8 +5912,8 @@ function printClaudeSetupHelp() {
5699
5912
  '',
5700
5913
  'Notes:',
5701
5914
  ' - Expects a project binding at .env.iranti unless --project-env is supplied.',
5702
- ' - Writes .mcp.json and .claude/settings.local.json.',
5703
- ' - Adds the Iranti MCP server to existing .mcp.json files without removing other servers.',
5915
+ ' - Writes .mcp.json, .vscode/mcp.json, and .claude/settings.local.json.',
5916
+ ' - Adds the Iranti MCP server to existing .mcp.json / .vscode/mcp.json files without removing other servers.',
5704
5917
  ' - Leaves existing Claude hook files untouched unless --force is supplied.',
5705
5918
  '',
5706
5919
  'Scan mode (--scan):',
@@ -5870,6 +6083,8 @@ async function claudeSetupCommand(args) {
5870
6083
  console.log(`${okLabel()} Scanning ${scanDir} - found ${candidates.length} project(s) with .claude${recursive ? ' (recursive)' : ''}`);
5871
6084
  let createdMcp = 0;
5872
6085
  let updatedMcp = 0;
6086
+ let createdVsCodeMcp = 0;
6087
+ let updatedVsCodeMcp = 0;
5873
6088
  let createdSettings = 0;
5874
6089
  let updatedSettings = 0;
5875
6090
  let unchanged = 0;
@@ -5879,14 +6094,19 @@ async function claudeSetupCommand(args) {
5879
6094
  createdMcp += 1;
5880
6095
  if (result.mcp === 'updated')
5881
6096
  updatedMcp += 1;
6097
+ if (result.vscodeMcp === 'created')
6098
+ createdVsCodeMcp += 1;
6099
+ if (result.vscodeMcp === 'updated')
6100
+ updatedVsCodeMcp += 1;
5882
6101
  if (result.settings === 'created')
5883
6102
  createdSettings += 1;
5884
6103
  if (result.settings === 'updated')
5885
6104
  updatedSettings += 1;
5886
- if (result.mcp === 'unchanged' && result.settings === 'unchanged')
6105
+ if (result.mcp === 'unchanged' && result.vscodeMcp === 'unchanged' && result.settings === 'unchanged')
5887
6106
  unchanged += 1;
5888
6107
  console.log(` ${projectPath}`);
5889
6108
  console.log(` mcp ${result.mcp}`);
6109
+ console.log(` vscode ${result.vscodeMcp}`);
5890
6110
  console.log(` settings ${result.settings}`);
5891
6111
  }
5892
6112
  console.log('');
@@ -5894,6 +6114,8 @@ async function claudeSetupCommand(args) {
5894
6114
  console.log(` projects ${candidates.length}`);
5895
6115
  console.log(` mcp created ${createdMcp}`);
5896
6116
  console.log(` mcp updated ${updatedMcp}`);
6117
+ console.log(` vscode created ${createdVsCodeMcp}`);
6118
+ console.log(` vscode updated ${updatedVsCodeMcp}`);
5897
6119
  console.log(` settings created ${createdSettings}`);
5898
6120
  console.log(` settings updated ${updatedSettings}`);
5899
6121
  console.log(` unchanged ${unchanged}`);
@@ -5917,8 +6139,10 @@ async function claudeSetupCommand(args) {
5917
6139
  console.log(` project ${projectPath}`);
5918
6140
  console.log(` binding ${projectEnvPath}`);
5919
6141
  console.log(` mcp ${path_1.default.join(projectPath, '.mcp.json')}`);
6142
+ console.log(` vscode ${path_1.default.join(projectPath, '.vscode', 'mcp.json')}`);
5920
6143
  console.log(` settings ${path_1.default.join(projectPath, '.claude', 'settings.local.json')}`);
5921
6144
  console.log(` mcp status ${result.mcp}`);
6145
+ console.log(` vscode status ${result.vscodeMcp}`);
5922
6146
  console.log(` settings status ${result.settings}`);
5923
6147
  console.log(`${infoLabel()} Next: open Claude Code in this project and verify Iranti tools are available.`);
5924
6148
  }