iranti 0.2.19 → 0.2.21

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
@@ -9,10 +9,10 @@
9
9
 
10
10
  Iranti gives agents persistent, identity-based memory. Facts written by one agent are retrievable by any other agent through exact entity+key lookup. Iranti also supports hybrid search (lexical + vector) when exact keys are unknown. Memory persists across sessions and survives context window limits.
11
11
 
12
- **Latest release:** [`v0.2.19`](https://github.com/nfemmanuel/iranti/releases/tag/v0.2.19)
12
+ **Latest release:** [`v0.2.20`](https://github.com/nfemmanuel/iranti/releases/tag/v0.2.20)
13
13
  Published packages:
14
- - `iranti@0.2.19`
15
- - `@iranti/sdk@0.2.19`
14
+ - `iranti@0.2.20`
15
+ - `@iranti/sdk@0.2.20`
16
16
 
17
17
  ---
18
18
 
@@ -173,7 +173,7 @@ The next leverage is still product simplicity: setup, operations, and day-to-day
173
173
 
174
174
  ## Quickstart
175
175
 
176
- **Requirements**: Node.js 18+, PostgreSQL, Python 3.8+
176
+ **Requirements**: Node.js 18+, PostgreSQL with pgvector support, Python 3.8+
177
177
 
178
178
  Docker is optional. It is one local way to run PostgreSQL if you do not already have a database. Iranti still requires PostgreSQL; the setup improvement is smarter bootstrap and clearer guidance, not a second storage backend.
179
179
 
@@ -190,7 +190,7 @@ iranti run --instance local
190
190
 
191
191
  `iranti setup` now defaults to an isolated per-project runtime. Shared machine-level instances are still supported, but they are now an explicit choice rather than the default.
192
192
 
193
- 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.
193
+ If local PostgreSQL is available and pgvector-capable, setup can bootstrap a localhost database for you. If Docker is available, setup now prefers the Docker path over a plain local listener because it guarantees pgvector. If local PostgreSQL is reachable but does not provide pgvector, setup now fails early with a direct action path instead of a late Prisma migration error.
194
194
 
195
195
  Long-running agents can now checkpoint and recover interrupted work. Programmatic session lifecycle methods are available through the SDK and REST API:
196
196
  - `checkpoint()`
@@ -221,6 +221,17 @@ function builtScriptPath(scriptName) {
221
221
  }
222
222
  function formatSetupBootstrapFailure(error) {
223
223
  const reason = error instanceof Error ? error.message : String(error);
224
+ if (/Could not find Prisma Schema/i.test(reason)) {
225
+ return new Error(`Database bootstrap failed because Prisma could not locate prisma/schema.prisma from the active package. ` +
226
+ `This usually means the installed CLI bundle or working-directory handoff is wrong. ` +
227
+ `Underlying error: ${reason}`);
228
+ }
229
+ if (/extension "vector" is not available|pgvector extension is not installed|does not have the pgvector extension installed/i.test(reason)) {
230
+ return new Error(`Database bootstrap failed because the target PostgreSQL server does not provide pgvector. ` +
231
+ `Iranti currently requires pgvector-capable PostgreSQL for schema bootstrap. ` +
232
+ `Install pgvector on that server, or rerun setup with --db-mode docker or a managed pgvector-capable database. ` +
233
+ `Underlying error: ${reason}`);
234
+ }
224
235
  return new Error(`Database bootstrap failed after instance configuration. ` +
225
236
  `Common causes are a non-empty database that Prisma has not baselined yet, or a PostgreSQL server without the pgvector extension installed. ` +
226
237
  `Re-run setup without --bootstrap-db, or point Iranti at a fresh pgvector-capable database. ` +
@@ -761,29 +772,31 @@ function inferProjectMode(projectPath, instanceEnvFile) {
761
772
  }
762
773
  function recommendDatabaseMode(checks) {
763
774
  const reachableLocal = checks.find((check) => check.name === 'localhost:5432')?.status === 'pass';
764
- const docker = checks.find((check) => check.name === 'docker')?.status === 'pass';
765
- if (reachableLocal)
775
+ const docker = checks.find((check) => check.name === 'docker daemon')?.status === 'pass';
776
+ if (reachableLocal && !docker)
766
777
  return 'local';
767
778
  if (docker)
768
779
  return 'docker';
780
+ if (reachableLocal)
781
+ return 'local';
769
782
  return 'managed';
770
783
  }
771
784
  function quickInstallGuidanceLines() {
772
785
  if (process.platform === 'win32') {
773
786
  return [
774
787
  'Docker: winget install Docker.DockerDesktop',
775
- 'PostgreSQL: winget install PostgreSQL.PostgreSQL.17',
788
+ 'PostgreSQL: install PostgreSQL 17 and a pgvector-capable build or extension package.',
776
789
  ];
777
790
  }
778
791
  if (process.platform === 'darwin') {
779
792
  return [
780
793
  'Docker: brew install --cask docker',
781
- 'PostgreSQL: brew install postgresql@17',
794
+ 'PostgreSQL: brew install postgresql@17 plus pgvector for that server.',
782
795
  ];
783
796
  }
784
797
  return [
785
798
  'Docker: install Docker Engine using your distro package manager or the official Docker instructions.',
786
- 'PostgreSQL: install PostgreSQL 16+ using your distro package manager.',
799
+ 'PostgreSQL: install PostgreSQL 16+ and the pgvector extension using your distro package manager.',
787
800
  ];
788
801
  }
789
802
  function printQuickInstallGuidance() {
@@ -1181,6 +1194,32 @@ function hasDockerInstalled() {
1181
1194
  return false;
1182
1195
  }
1183
1196
  }
1197
+ function inspectDockerAvailability() {
1198
+ if (!hasDockerInstalled()) {
1199
+ return {
1200
+ installed: false,
1201
+ daemonReachable: false,
1202
+ detail: 'Docker is not installed or not on PATH.',
1203
+ };
1204
+ }
1205
+ const proc = runCommandCapture('docker', ['info', '--format', '{{.ServerVersion}}']);
1206
+ if (proc.status === 0) {
1207
+ const version = proc.stdout.trim();
1208
+ return {
1209
+ installed: true,
1210
+ daemonReachable: true,
1211
+ detail: version
1212
+ ? `Docker daemon is reachable (server ${version}).`
1213
+ : 'Docker daemon is reachable.',
1214
+ };
1215
+ }
1216
+ const reason = (proc.stderr || proc.stdout).trim() || 'Docker daemon did not respond.';
1217
+ return {
1218
+ installed: true,
1219
+ daemonReachable: false,
1220
+ detail: `Docker CLI is installed, but the daemon is not reachable. ${reason}`,
1221
+ };
1222
+ }
1184
1223
  async function isPortAvailable(port, host = '127.0.0.1') {
1185
1224
  return await new Promise((resolve) => {
1186
1225
  const server = net_1.default.createServer();
@@ -1244,9 +1283,19 @@ async function waitForTcpPort(host, port, timeoutMs) {
1244
1283
  throw new Error(`Timed out waiting for ${host}:${port} to accept TCP connections.`);
1245
1284
  }
1246
1285
  async function runDockerPostgresContainer(options) {
1286
+ const docker = inspectDockerAvailability();
1287
+ if (!docker.installed) {
1288
+ throw new Error('Docker CLI is not installed or not on PATH. Install Docker Desktop or Docker Engine before using --db-mode docker.');
1289
+ }
1290
+ if (!docker.daemonReachable) {
1291
+ throw new Error(`Docker daemon is not reachable. Start Docker Desktop or Docker Engine, then retry. ${docker.detail}`);
1292
+ }
1247
1293
  const inspect = process.platform === 'win32'
1248
1294
  ? (0, child_process_1.spawnSync)(process.env.ComSpec ?? 'cmd.exe', ['/d', '/c', `docker ps -a --format "{{.Names}}"`], { encoding: 'utf8' })
1249
1295
  : (0, child_process_1.spawnSync)('docker', ['ps', '-a', '--format', '{{.Names}}'], { encoding: 'utf8' });
1296
+ if (inspect.status !== 0) {
1297
+ throw new Error(`Failed to inspect Docker containers. ${(inspect.stderr ?? inspect.stdout ?? '').trim() || 'docker ps returned a non-zero exit code.'}`);
1298
+ }
1250
1299
  const names = (inspect.stdout ?? '').split(/\r?\n/).map((value) => value.trim()).filter(Boolean);
1251
1300
  if (names.includes(options.containerName)) {
1252
1301
  const start = process.platform === 'win32'
@@ -1295,10 +1344,12 @@ async function executeSetupPlan(plan) {
1295
1344
  });
1296
1345
  if (plan.bootstrapDatabase) {
1297
1346
  try {
1298
- if (plan.databaseMode === 'docker') {
1347
+ if (plan.databaseMode === 'docker' && !plan.databaseProvisioned) {
1299
1348
  const parsed = parsePostgresConnectionString(plan.databaseUrl);
1300
1349
  await runDockerPostgresContainer({
1301
- containerName: sanitizeIdentifier(`iranti_${plan.instanceName}_db`, `iranti_${plan.instanceName}_db`),
1350
+ containerName: plan.dockerContainerName
1351
+ ? sanitizeIdentifier(plan.dockerContainerName, `iranti_${plan.instanceName}_db`)
1352
+ : sanitizeIdentifier(`iranti_${plan.instanceName}_db`, `iranti_${plan.instanceName}_db`),
1302
1353
  hostPort: Number.parseInt(parsed.port || '5432', 10),
1303
1354
  password: decodeURIComponent(parsed.password || 'postgres'),
1304
1355
  database: postgresDatabaseName(plan.databaseUrl),
@@ -1306,6 +1357,7 @@ async function executeSetupPlan(plan) {
1306
1357
  }
1307
1358
  if (plan.databaseMode === 'local') {
1308
1359
  await ensurePostgresDatabaseExists(plan.databaseUrl);
1360
+ await ensureLocalPostgresPgvectorAvailable(plan.databaseUrl);
1309
1361
  }
1310
1362
  await runBundledScript('setup', [], {
1311
1363
  DATABASE_URL: plan.databaseUrl,
@@ -1313,6 +1365,9 @@ async function executeSetupPlan(plan) {
1313
1365
  });
1314
1366
  }
1315
1367
  catch (error) {
1368
+ if (plan.databaseMode === 'docker' && error instanceof Error && /Docker daemon is not reachable|Docker CLI is not installed/i.test(error.message)) {
1369
+ throw error;
1370
+ }
1316
1371
  throw formatSetupBootstrapFailure(error);
1317
1372
  }
1318
1373
  }
@@ -1437,7 +1492,7 @@ function defaultsSetupPlan(args) {
1437
1492
  if (!explicit) {
1438
1493
  if (hasCommandInstalled('psql'))
1439
1494
  return 'local';
1440
- if (hasDockerInstalled())
1495
+ if (inspectDockerAvailability().daemonReachable)
1441
1496
  return 'docker';
1442
1497
  return 'managed';
1443
1498
  }
@@ -1699,15 +1754,20 @@ async function canConnectTcp(port, host = '127.0.0.1', timeoutMs = 800) {
1699
1754
  });
1700
1755
  }
1701
1756
  async function collectDependencyChecks() {
1702
- const docker = hasDockerInstalled();
1757
+ const docker = inspectDockerAvailability();
1703
1758
  const psql = hasCommandInstalled('psql');
1704
1759
  const pgIsReady = hasCommandInstalled('pg_isready');
1705
1760
  const postgresPort = await canConnectTcp(5432);
1706
1761
  const checks = [
1707
1762
  {
1708
1763
  name: 'docker',
1709
- status: docker ? 'pass' : 'warn',
1710
- detail: docker ? 'Docker is installed.' : 'Docker is not installed or not on PATH.',
1764
+ status: docker.installed ? 'pass' : 'warn',
1765
+ detail: docker.installed ? 'Docker CLI is installed.' : docker.detail,
1766
+ },
1767
+ {
1768
+ name: 'docker daemon',
1769
+ status: docker.daemonReachable ? 'pass' : 'warn',
1770
+ detail: docker.detail,
1711
1771
  },
1712
1772
  {
1713
1773
  name: 'psql',
@@ -1942,6 +2002,46 @@ async function ensurePostgresDatabaseExists(databaseUrl) {
1942
2002
  throw new Error(`Failed to create local PostgreSQL database '${databaseName}'. ${create.stderr?.trim() || create.stdout?.trim() || 'psql returned a non-zero exit code.'}`);
1943
2003
  }
1944
2004
  }
2005
+ async function ensureLocalPostgresPgvectorAvailable(databaseUrl) {
2006
+ const parsed = parsePostgresConnectionString(databaseUrl);
2007
+ if (!isLocalPostgresHost(parsed.hostname)) {
2008
+ return;
2009
+ }
2010
+ if (!hasCommandInstalled('psql')) {
2011
+ return;
2012
+ }
2013
+ const adminDatabase = parsed.searchParams.get('admin_db')?.trim() || 'postgres';
2014
+ const host = parsed.hostname;
2015
+ const port = parsed.port || '5432';
2016
+ const user = decodeURIComponent(parsed.username || 'postgres');
2017
+ const password = decodeURIComponent(parsed.password || '');
2018
+ const extraEnv = password ? { PGPASSWORD: password } : undefined;
2019
+ const probe = (0, child_process_1.spawnSync)('psql', [
2020
+ '-h',
2021
+ host,
2022
+ '-p',
2023
+ port,
2024
+ '-U',
2025
+ user,
2026
+ '-d',
2027
+ adminDatabase,
2028
+ '-tAc',
2029
+ "SELECT 1 FROM pg_available_extensions WHERE name = 'vector';",
2030
+ ], {
2031
+ encoding: 'utf8',
2032
+ stdio: ['ignore', 'pipe', 'pipe'],
2033
+ env: {
2034
+ ...process.env,
2035
+ ...extraEnv,
2036
+ },
2037
+ });
2038
+ if (probe.status !== 0) {
2039
+ throw new Error(`Failed to inspect local PostgreSQL for pgvector availability. ${probe.stderr?.trim() || probe.stdout?.trim() || 'psql returned a non-zero exit code.'}`);
2040
+ }
2041
+ if ((probe.stdout ?? '').trim() !== '1') {
2042
+ throw new Error('Local PostgreSQL server does not have the pgvector extension installed.');
2043
+ }
2044
+ }
1945
2045
  function detectPythonLauncher() {
1946
2046
  const candidates = process.platform === 'win32'
1947
2047
  ? [
@@ -2270,6 +2370,32 @@ function canScheduleWindowsGlobalNpmSelfUpgrade(context) {
2270
2370
  function escapeForSingleQuotedPowerShell(value) {
2271
2371
  return value.replace(/'/g, "''");
2272
2372
  }
2373
+ function resolveWindowsDetachedExecutable(executable) {
2374
+ if (path_1.default.isAbsolute(executable)) {
2375
+ return executable;
2376
+ }
2377
+ const candidates = executable.toLowerCase().endsWith('.cmd') || executable.toLowerCase().endsWith('.exe')
2378
+ ? [executable]
2379
+ : [`${executable}.cmd`, `${executable}.exe`, executable];
2380
+ for (const candidate of candidates) {
2381
+ const probe = (0, child_process_1.spawnSync)('where', [candidate], { encoding: 'utf8' });
2382
+ if (probe.status === 0) {
2383
+ const resolved = (probe.stdout ?? '').split(/\r?\n/).map((line) => line.trim()).find(Boolean);
2384
+ if (resolved) {
2385
+ if (!path_1.default.extname(resolved)) {
2386
+ const cmdVariant = `${resolved}.cmd`;
2387
+ const exeVariant = `${resolved}.exe`;
2388
+ if (fs_1.default.existsSync(cmdVariant))
2389
+ return cmdVariant;
2390
+ if (fs_1.default.existsSync(exeVariant))
2391
+ return exeVariant;
2392
+ }
2393
+ return resolved;
2394
+ }
2395
+ }
2396
+ }
2397
+ return executable;
2398
+ }
2273
2399
  function resolveDetachedUpgradeCwd(command) {
2274
2400
  const desired = command.cwd?.trim();
2275
2401
  if (!desired) {
@@ -2283,12 +2409,30 @@ function resolveDetachedUpgradeCwd(command) {
2283
2409
  }
2284
2410
  return normalized;
2285
2411
  }
2412
+ function launchDetachedWindowsPowerShellFile(scriptPath, cwd) {
2413
+ const command = [
2414
+ 'Start-Process',
2415
+ '-WindowStyle Hidden',
2416
+ `-WorkingDirectory '${escapeForSingleQuotedPowerShell(cwd)}'`,
2417
+ "-FilePath 'powershell.exe'",
2418
+ `-ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','${escapeForSingleQuotedPowerShell(scriptPath)}')`,
2419
+ ].join(' ');
2420
+ const proc = (0, child_process_1.spawnSync)('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', command], {
2421
+ encoding: 'utf8',
2422
+ cwd,
2423
+ env: process.env,
2424
+ windowsHide: true,
2425
+ });
2426
+ if (proc.status !== 0) {
2427
+ throw new Error(`Failed to schedule detached PowerShell handoff. ${(proc.stderr || proc.stdout).trim() || 'powershell returned a non-zero exit code.'}`);
2428
+ }
2429
+ }
2286
2430
  function scheduleDetachedWindowsGlobalNpmUpgrade(command, postCommand) {
2287
2431
  const neutralCwd = resolveDetachedUpgradeCwd(command);
2288
2432
  const parentPid = process.pid;
2289
- const powershell = 'powershell.exe';
2290
2433
  const escapedCwd = escapeForSingleQuotedPowerShell(neutralCwd);
2291
- const escapedExecutable = escapeForSingleQuotedPowerShell(command.executable);
2434
+ const detachedExecutable = resolveWindowsDetachedExecutable(command.executable);
2435
+ const escapedExecutable = escapeForSingleQuotedPowerShell(detachedExecutable);
2292
2436
  const escapedArgs = command.args.map((arg) => `'${escapeForSingleQuotedPowerShell(arg)}'`).join(', ');
2293
2437
  const script = [
2294
2438
  `$parentPid = ${parentPid}`,
@@ -2297,15 +2441,9 @@ function scheduleDetachedWindowsGlobalNpmUpgrade(command, postCommand) {
2297
2441
  `& '${escapedExecutable}' @(${escapedArgs})`,
2298
2442
  ...(postCommand ? [`if ($LASTEXITCODE -eq 0) { ${postCommand} }`] : []),
2299
2443
  'exit $LASTEXITCODE',
2300
- ].join('; ');
2301
- const child = (0, child_process_1.spawn)(powershell, ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script], {
2302
- detached: true,
2303
- stdio: 'ignore',
2304
- windowsHide: true,
2305
- cwd: neutralCwd,
2306
- env: process.env,
2307
- });
2308
- child.unref();
2444
+ ].join(';\r\n');
2445
+ const scriptPath = writeDetachedWindowsScript('iranti-upgrade', `${script};\r\n`);
2446
+ launchDetachedWindowsPowerShellFile(scriptPath, neutralCwd);
2309
2447
  }
2310
2448
  function verifyPythonInstall(command) {
2311
2449
  const version = detectPythonInstalledVersion(command);
@@ -2684,6 +2822,7 @@ async function stopUninstallProcesses(processes) {
2684
2822
  }
2685
2823
  function buildDetachedWindowsUninstallScript(options) {
2686
2824
  const lines = [
2825
+ "$ErrorActionPreference = 'SilentlyContinue'",
2687
2826
  `$parentPid = ${options.parentPid}`,
2688
2827
  'while (Get-Process -Id $parentPid -ErrorAction SilentlyContinue) { Start-Sleep -Milliseconds 500 }',
2689
2828
  ];
@@ -2691,15 +2830,20 @@ function buildDetachedWindowsUninstallScript(options) {
2691
2830
  lines.push(`taskkill /PID ${pid} /T /F > $null 2>&1`);
2692
2831
  }
2693
2832
  if (options.removeCodex) {
2694
- lines.push("$codexGet = Get-Command codex -ErrorAction SilentlyContinue");
2695
- lines.push("if ($codexGet) { codex mcp get iranti --json > $null 2>&1; if ($LASTEXITCODE -eq 0) { codex mcp remove iranti > $null 2>&1 } }");
2833
+ const codexExecutable = escapeForSingleQuotedPowerShell(options.codexExecutable ?? resolveWindowsDetachedExecutable('codex'));
2834
+ lines.push(`$codexExe = '${codexExecutable}'`);
2835
+ lines.push("if (Test-Path -LiteralPath $codexExe) { & $codexExe mcp get iranti --json > $null 2>&1; if ($LASTEXITCODE -eq 0) { & $codexExe mcp remove iranti > $null 2>&1 } }");
2696
2836
  }
2697
2837
  if (options.removeGlobalNpm) {
2698
- lines.push('& npm uninstall -g iranti');
2838
+ const npmExecutable = escapeForSingleQuotedPowerShell(options.npmExecutable ?? resolveWindowsDetachedExecutable('npm'));
2839
+ lines.push(`$npmExe = '${npmExecutable}'`);
2840
+ lines.push("if (Test-Path -LiteralPath $npmExe) { & $npmExe uninstall -g iranti > $null 2>&1 }");
2699
2841
  }
2700
2842
  if (options.python) {
2701
2843
  const args = options.python.args.map((arg) => `'${escapeForSingleQuotedPowerShell(arg)}'`).join(', ');
2702
- lines.push(`& '${escapeForSingleQuotedPowerShell(options.python.executable)}' @(${args})`);
2844
+ const pythonExecutable = escapeForSingleQuotedPowerShell(resolveWindowsDetachedExecutable(options.python.executable));
2845
+ lines.push(`$pythonExe = '${pythonExecutable}'`);
2846
+ lines.push(`if (Test-Path -LiteralPath $pythonExe) { & $pythonExe @(${args}) > $null 2>&1 }`);
2703
2847
  }
2704
2848
  for (const filePath of options.artifactFiles) {
2705
2849
  lines.push(`if (Test-Path -LiteralPath '${escapeForSingleQuotedPowerShell(filePath)}') { Remove-Item -LiteralPath '${escapeForSingleQuotedPowerShell(filePath)}' -Force }`);
@@ -2708,7 +2852,13 @@ function buildDetachedWindowsUninstallScript(options) {
2708
2852
  lines.push(`if (Test-Path -LiteralPath '${escapeForSingleQuotedPowerShell(dirPath)}') { Remove-Item -LiteralPath '${escapeForSingleQuotedPowerShell(dirPath)}' -Recurse -Force }`);
2709
2853
  }
2710
2854
  lines.push('exit 0');
2711
- return lines.join('; ');
2855
+ return `${lines.join(';\r\n')};\r\n`;
2856
+ }
2857
+ function writeDetachedWindowsScript(prefix, scriptContents) {
2858
+ const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), `${prefix}-`));
2859
+ const scriptPath = path_1.default.join(tempDir, `${prefix}.ps1`);
2860
+ fs_1.default.writeFileSync(scriptPath, scriptContents, 'utf8');
2861
+ return scriptPath;
2712
2862
  }
2713
2863
  async function uninstallCommand(args) {
2714
2864
  const scope = normalizeScope(getFlag(args, 'scope'));
@@ -2786,17 +2936,13 @@ async function uninstallCommand(args) {
2786
2936
  removeCodex: actions.removeCodexRegistration,
2787
2937
  python: actions.removePython ? pythonCommand : null,
2788
2938
  removeGlobalNpm: actions.removeGlobalNpm,
2939
+ npmExecutable: resolveWindowsDetachedExecutable('npm'),
2940
+ codexExecutable: resolveWindowsDetachedExecutable('codex'),
2789
2941
  runtimeRoots: actions.removeRuntimeRoots ? runtimeRoots.map((entry) => entry.path) : [],
2790
2942
  artifactFiles,
2791
2943
  });
2792
- const child = (0, child_process_1.spawn)('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script], {
2793
- detached: true,
2794
- stdio: 'ignore',
2795
- windowsHide: true,
2796
- cwd: os_1.default.homedir(),
2797
- env: process.env,
2798
- });
2799
- child.unref();
2944
+ const scriptPath = writeDetachedWindowsScript('iranti-uninstall', script);
2945
+ launchDetachedWindowsPowerShellFile(scriptPath, os_1.default.homedir());
2800
2946
  execution.push({
2801
2947
  label: 'detached-uninstall',
2802
2948
  status: 'warn',
@@ -3130,7 +3276,7 @@ async function setupCommand(args) {
3130
3276
  const dependencyChecks = await collectDependencyChecks();
3131
3277
  printDependencyChecks(dependencyChecks);
3132
3278
  const recommendedDatabaseMode = recommendDatabaseMode(dependencyChecks);
3133
- console.log(`${infoLabel()} Recommended database path: ${recommendedDatabaseMode === 'local' ? 'local PostgreSQL' : recommendedDatabaseMode === 'docker' ? 'Docker-hosted PostgreSQL' : 'managed PostgreSQL'}`);
3279
+ console.log(`${infoLabel()} Recommended database path: ${recommendedDatabaseMode === 'local' ? 'local PostgreSQL (pgvector required)' : recommendedDatabaseMode === 'docker' ? 'Docker-hosted PostgreSQL' : 'managed PostgreSQL'}`);
3134
3280
  if (recommendedDatabaseMode === 'managed' && dependencyChecks.every((check) => check.status === 'warn')) {
3135
3281
  printQuickInstallGuidance();
3136
3282
  }
@@ -3177,10 +3323,13 @@ async function setupCommand(args) {
3177
3323
  }
3178
3324
  const existingPort = Number.parseInt(existingInstance?.env.IRANTI_PORT ?? '3001', 10);
3179
3325
  const port = await chooseAvailablePort(prompt, 'API port', existingPort, Boolean(existingInstance));
3180
- const dockerAvailable = hasDockerInstalled();
3326
+ const dockerStatus = inspectDockerAvailability();
3327
+ const dockerAvailable = dockerStatus.daemonReachable;
3181
3328
  const psqlAvailable = hasCommandInstalled('psql');
3182
3329
  let dbUrl = '';
3183
3330
  let bootstrapDatabase = false;
3331
+ let databaseProvisioned = false;
3332
+ let dockerContainerName;
3184
3333
  let databaseMode = recommendedDatabaseMode;
3185
3334
  while (true) {
3186
3335
  const defaultMode = recommendedDatabaseMode;
@@ -3210,13 +3359,14 @@ async function setupCommand(args) {
3210
3359
  if (dbMode === 'docker') {
3211
3360
  databaseMode = 'docker';
3212
3361
  if (!dockerAvailable) {
3213
- console.log(`${warnLabel()} Docker is not installed or not on PATH. Choose local or managed instead.`);
3362
+ console.log(`${warnLabel()} ${dockerStatus.detail}`);
3214
3363
  continue;
3215
3364
  }
3216
3365
  const dbHostPort = await chooseAvailablePort(prompt, 'Docker PostgreSQL host port', 5432, false);
3217
3366
  const dbName = sanitizeIdentifier(await promptNonEmpty(prompt, 'Docker PostgreSQL database name', `iranti_${instanceName}`), `iranti_${instanceName}`);
3218
3367
  const dbPassword = await promptRequiredSecret(prompt, 'Docker PostgreSQL password');
3219
3368
  const containerName = sanitizeIdentifier(await promptNonEmpty(prompt, 'Docker container name', `iranti_${instanceName}_db`), `iranti_${instanceName}_db`);
3369
+ dockerContainerName = containerName;
3220
3370
  dbUrl = `postgresql://postgres:${dbPassword}@localhost:${dbHostPort}/${dbName}`;
3221
3371
  console.log(`${infoLabel()} Docker will be used only for PostgreSQL. Iranti itself does not require Docker once a PostgreSQL database is available.`);
3222
3372
  if (await promptYesNo(prompt, `Start or reuse Docker container '${containerName}' now?`, true)) {
@@ -3227,11 +3377,9 @@ async function setupCommand(args) {
3227
3377
  database: dbName,
3228
3378
  });
3229
3379
  console.log(`${okLabel()} Docker PostgreSQL ready at localhost:${dbHostPort}`);
3230
- bootstrapDatabase = true;
3231
- }
3232
- else {
3233
- bootstrapDatabase = await promptYesNo(prompt, 'Will you start PostgreSQL separately before first run?', false);
3380
+ databaseProvisioned = true;
3234
3381
  }
3382
+ bootstrapDatabase = await promptYesNo(prompt, 'Run migrations and seed the database now?', true);
3235
3383
  break;
3236
3384
  }
3237
3385
  console.log(`${warnLabel()} Choose one of: local, managed, docker.`);
@@ -3333,6 +3481,8 @@ async function setupCommand(args) {
3333
3481
  codex,
3334
3482
  codexAgent: projects[0]?.agentId,
3335
3483
  bootstrapDatabase,
3484
+ dockerContainerName,
3485
+ databaseProvisioned,
3336
3486
  });
3337
3487
  });
3338
3488
  if (!result) {
@@ -3374,6 +3524,7 @@ async function doctorCommand(args) {
3374
3524
  const version = getPackageVersion();
3375
3525
  const pushEnvironmentChecks = async (env, prefix = '') => {
3376
3526
  const databaseUrl = env.DATABASE_URL;
3527
+ let databaseInitializedForDoctor = false;
3377
3528
  checks.push(detectPlaceholder(databaseUrl)
3378
3529
  ? {
3379
3530
  name: `${prefix}database configuration`,
@@ -3397,6 +3548,10 @@ async function doctorCommand(args) {
3397
3548
  name: `${prefix}${providerKeyCheck.name}`,
3398
3549
  });
3399
3550
  try {
3551
+ if (!detectPlaceholder(databaseUrl)) {
3552
+ (0, client_1.initDb)(databaseUrl);
3553
+ databaseInitializedForDoctor = true;
3554
+ }
3400
3555
  const backendName = (0, backends_1.resolveVectorBackendName)({
3401
3556
  vectorBackend: env.IRANTI_VECTOR_BACKEND,
3402
3557
  qdrantUrl: env.IRANTI_QDRANT_URL,
@@ -3436,6 +3591,11 @@ async function doctorCommand(args) {
3436
3591
  detail: error instanceof Error ? error.message : String(error),
3437
3592
  });
3438
3593
  }
3594
+ finally {
3595
+ if (databaseInitializedForDoctor) {
3596
+ await (0, client_1.disconnectDb)();
3597
+ }
3598
+ }
3439
3599
  };
3440
3600
  checks.push({
3441
3601
  name: 'node version',
@@ -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.19',
147
+ version: '0.2.21',
148
148
  });
149
149
  server.registerTool('iranti_handshake', {
150
150
  description: `Initialize or refresh an agent's working-memory brief for the current task.
@@ -9,15 +9,35 @@ const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const client_1 = require("../src/library/client");
11
11
  const escalationPaths_1 = require("../src/lib/escalationPaths");
12
- const PACKAGE_ROOT = path_1.default.resolve(__dirname, '..');
12
+ function resolvePackageRoot() {
13
+ let dir = __dirname;
14
+ for (let i = 0; i < 8; i += 1) {
15
+ const pkgPath = path_1.default.join(dir, 'package.json');
16
+ const prismaSchema = path_1.default.join(dir, 'prisma', 'schema.prisma');
17
+ if (fs_1.default.existsSync(pkgPath) && fs_1.default.existsSync(prismaSchema)) {
18
+ return dir;
19
+ }
20
+ const parent = path_1.default.dirname(dir);
21
+ if (parent === dir)
22
+ break;
23
+ dir = parent;
24
+ }
25
+ throw new Error(`Could not resolve Iranti package root from ${__dirname}`);
26
+ }
27
+ const PACKAGE_ROOT = resolvePackageRoot();
28
+ const PRISMA_SCHEMA = path_1.default.join(PACKAGE_ROOT, 'prisma', 'schema.prisma');
29
+ const BASE_ENV = { ...process.env };
13
30
  // ─── Helpers ─────────────────────────────────────────────────────────────────
14
- function run(command, label) {
31
+ function run(command, label, extraEnv) {
15
32
  console.log(` Running: ${label}...`);
16
33
  try {
17
34
  (0, child_process_1.execSync)(command, {
18
35
  stdio: 'inherit',
19
36
  cwd: PACKAGE_ROOT,
20
- env: process.env,
37
+ env: {
38
+ ...BASE_ENV,
39
+ ...extraEnv,
40
+ },
21
41
  });
22
42
  }
23
43
  catch {
@@ -26,7 +46,7 @@ function run(command, label) {
26
46
  }
27
47
  }
28
48
  function scriptCommand(distScriptName, sourceScriptPath) {
29
- const distScriptPath = path_1.default.resolve(__dirname, '..', 'dist', 'scripts', distScriptName);
49
+ const distScriptPath = path_1.default.join(PACKAGE_ROOT, 'dist', 'scripts', distScriptName);
30
50
  if (fs_1.default.existsSync(distScriptPath)) {
31
51
  return `node ${JSON.stringify(distScriptPath)}`;
32
52
  }
@@ -57,13 +77,18 @@ async function setup() {
57
77
  process.env.DATABASE_URL = dbUrl;
58
78
  (0, client_1.initDb)(dbUrl);
59
79
  }
80
+ const runtimeEnv = dbUrl
81
+ ? {
82
+ DATABASE_URL: dbUrl,
83
+ }
84
+ : undefined;
60
85
  // 1. Run migrations
61
86
  console.log('Step 1 — Running database migrations...');
62
- run('npx prisma migrate deploy', 'prisma migrate deploy');
87
+ run(`npx prisma migrate deploy --schema ${JSON.stringify(PRISMA_SCHEMA)}`, 'prisma migrate deploy', runtimeEnv);
63
88
  console.log(' ✓ Migrations complete\n');
64
89
  // 2. Generate Prisma client
65
90
  console.log('Step 2 — Generating Prisma client...');
66
- run('npx prisma generate', 'prisma generate');
91
+ run(`npx prisma generate --schema ${JSON.stringify(PRISMA_SCHEMA)}`, 'prisma generate', runtimeEnv);
67
92
  console.log(' ✓ Client generated\n');
68
93
  // 3. Seed Staff Namespace if not already seeded
69
94
  console.log('Step 3 — Seeding Staff Namespace...');
@@ -39,7 +39,7 @@ const INSTANCE_DIR = process.env.IRANTI_INSTANCE_DIR?.trim()
39
39
  const INSTANCE_RUNTIME_FILE = process.env.IRANTI_INSTANCE_RUNTIME_FILE?.trim()
40
40
  || (INSTANCE_DIR ? (0, runtimeLifecycle_1.runtimeFileForInstance)(INSTANCE_DIR) : null);
41
41
  const INSTANCE_NAME = process.env.IRANTI_INSTANCE_NAME?.trim() || (INSTANCE_DIR ? path_1.default.basename(INSTANCE_DIR) : 'adhoc');
42
- const VERSION = '0.2.19';
42
+ const VERSION = '0.2.21';
43
43
  try {
44
44
  fs_1.default.mkdirSync(path_1.default.dirname(REQUEST_LOG_FILE), { recursive: true });
45
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iranti",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
4
4
  "description": "Memory infrastructure for multi-agent AI systems",
5
5
  "main": "dist/src/sdk/index.js",
6
6
  "files": [
@@ -16,6 +16,7 @@
16
16
  "dist/scripts/api-key-list.js",
17
17
  "dist/scripts/api-key-revoke.js",
18
18
  "bin/iranti.js",
19
+ "prisma.config.ts",
19
20
  "prisma/schema.prisma",
20
21
  "prisma/migrations/**/*",
21
22
  ".env.example",
@@ -0,0 +1,14 @@
1
+ // This file was generated by Prisma, and assumes you have installed the following:
2
+ // npm install --save-dev prisma dotenv
3
+ import "dotenv/config";
4
+ import { defineConfig } from "prisma/config";
5
+
6
+ export default defineConfig({
7
+ schema: "prisma/schema.prisma",
8
+ migrations: {
9
+ path: "prisma/migrations",
10
+ },
11
+ datasource: {
12
+ url: process.env["DATABASE_URL"],
13
+ },
14
+ });