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.
- package/dist/scripts/codex-setup.js +74 -7
- package/dist/scripts/iranti-cli.js +244 -20
- package/dist/scripts/iranti-mcp.js +28 -1
- package/dist/src/api/server.js +1 -1
- package/dist/src/attendant/AttendantInstance.d.ts +1 -0
- package/dist/src/attendant/AttendantInstance.d.ts.map +1 -1
- package/dist/src/attendant/AttendantInstance.js +20 -3
- package/dist/src/attendant/AttendantInstance.js.map +1 -1
- package/dist/src/lib/autoRemember.d.ts +19 -0
- package/dist/src/lib/autoRemember.d.ts.map +1 -1
- package/dist/src/lib/autoRemember.js +132 -48
- package/dist/src/lib/autoRemember.js.map +1 -1
- package/dist/src/lib/cliHelpCatalog.js +8 -8
- package/dist/src/lib/cliHelpCatalog.js.map +1 -1
- package/dist/src/lib/runtimeDependencies.d.ts +23 -0
- package/dist/src/lib/runtimeDependencies.d.ts.map +1 -0
- package/dist/src/lib/runtimeDependencies.js +212 -0
- package/dist/src/lib/runtimeDependencies.js.map +1 -0
- package/dist/src/librarian/index.d.ts.map +1 -1
- package/dist/src/librarian/index.js +55 -0
- package/dist/src/librarian/index.js.map +1 -1
- package/package.json +2 -1
|
@@ -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
|
|
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
|
|
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
|
|
271
|
-
?
|
|
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 (
|
|
300
|
-
console.log(`Workspace .mcp.json: ${
|
|
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
|
|
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
|
|
1796
|
-
|
|
1797
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|