iranti 0.2.45 → 0.2.46

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.
@@ -29,6 +29,7 @@ const backends_1 = require("../src/library/backends");
29
29
  const runtimeLifecycle_1 = require("../src/lib/runtimeLifecycle");
30
30
  const sdk_1 = require("../src/sdk");
31
31
  const queries_1 = require("../src/library/queries");
32
+ const autoRemember_1 = require("../src/lib/autoRemember");
32
33
  class CliError extends Error {
33
34
  constructor(code, message, hints = [], details) {
34
35
  super(message);
@@ -766,12 +767,28 @@ async function inspectInstanceConfig(root, name) {
766
767
  let metaReadable = false;
767
768
  let envReadable = false;
768
769
  const ownershipIssues = [];
770
+ let rewrittenMeta = false;
769
771
  if (metaPresent) {
770
772
  try {
771
773
  const raw = await promises_1.default.readFile(metaFile, 'utf8');
772
774
  const parsed = JSON.parse(raw);
773
775
  metaReadable = typeof parsed.name === 'string' && parsed.name.trim().length > 0;
774
776
  if (metaReadable) {
777
+ const shouldRewriteOwnership = parsed.name?.trim() === name
778
+ && ((typeof parsed.instanceDir === 'string' && path_1.default.resolve(parsed.instanceDir) !== path_1.default.resolve(instanceDir))
779
+ || (typeof parsed.envFile === 'string' && path_1.default.resolve(parsed.envFile) !== path_1.default.resolve(envFile)));
780
+ if (shouldRewriteOwnership) {
781
+ const nextMeta = {
782
+ ...parsed,
783
+ name,
784
+ instanceDir,
785
+ envFile,
786
+ };
787
+ await writeText(metaFile, `${JSON.stringify(nextMeta, null, 2)}\n`);
788
+ rewrittenMeta = true;
789
+ parsed.instanceDir = instanceDir;
790
+ parsed.envFile = envFile;
791
+ }
775
792
  if (parsed.name?.trim() !== name) {
776
793
  ownershipIssues.push(`instance.json name is ${parsed.name}`);
777
794
  }
@@ -781,6 +798,10 @@ async function inspectInstanceConfig(root, name) {
781
798
  if (typeof parsed.envFile === 'string' && path_1.default.resolve(parsed.envFile) !== path_1.default.resolve(envFile)) {
782
799
  ownershipIssues.push(`instance.json envFile points to ${parsed.envFile}`);
783
800
  }
801
+ const databaseIntentValidation = parseInstanceDatabaseIntent(parsed.databaseIntent);
802
+ if (databaseIntentValidation.errors.length > 0) {
803
+ ownershipIssues.push(`instance.json databaseIntent invalid: ${databaseIntentValidation.errors.join(', ')}`);
804
+ }
784
805
  const dependencyValidation = (0, runtimeDependencies_1.parseInstanceDependencies)(parsed.dependencies);
785
806
  if (dependencyValidation.errors.length > 0) {
786
807
  ownershipIssues.push(`instance.json dependencies invalid: ${dependencyValidation.errors.join(', ')}`);
@@ -808,7 +829,9 @@ async function inspectInstanceConfig(root, name) {
808
829
  }
809
830
  else if (metaPresent && envPresent && metaReadable && envReadable) {
810
831
  classification = 'complete';
811
- detail = 'instance metadata and env are present';
832
+ detail = rewrittenMeta
833
+ ? 'instance metadata was repaired and env is present'
834
+ : 'instance metadata and env are present';
812
835
  }
813
836
  else if ((metaPresent && !metaReadable) || (envPresent && !envReadable)) {
814
837
  classification = 'invalid';
@@ -1142,6 +1165,160 @@ function isLocalPostgresHost(hostname) {
1142
1165
  const normalized = hostname.trim().toLowerCase();
1143
1166
  return normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1';
1144
1167
  }
1168
+ function parseDatabaseIntentStrategy(raw) {
1169
+ const normalized = raw?.trim().toLowerCase() ?? '';
1170
+ if (!normalized)
1171
+ return null;
1172
+ if (normalized === 'dedicated' || normalized === 'dedicated-local')
1173
+ return 'dedicated-local';
1174
+ if (normalized === 'shared' || normalized === 'shared-local')
1175
+ return 'shared-local';
1176
+ if (normalized === 'external' || normalized === 'managed' || normalized === 'external-existing')
1177
+ return 'external-existing';
1178
+ return null;
1179
+ }
1180
+ function parseDatabaseIntentStrategyFlag(raw, label) {
1181
+ const trimmed = raw?.trim() ?? '';
1182
+ if (!trimmed)
1183
+ return null;
1184
+ const parsed = parseDatabaseIntentStrategy(trimmed);
1185
+ if (!parsed) {
1186
+ throw new Error(`Invalid ${label} '${raw}'. Use dedicated, shared, or external.`);
1187
+ }
1188
+ return parsed;
1189
+ }
1190
+ function databaseIntentPromptValue(strategy) {
1191
+ if (strategy === 'dedicated-local')
1192
+ return 'dedicated';
1193
+ if (strategy === 'shared-local')
1194
+ return 'shared';
1195
+ return 'external';
1196
+ }
1197
+ function describeDatabaseIntentStrategy(strategy) {
1198
+ switch (strategy) {
1199
+ case 'dedicated-local':
1200
+ return 'dedicated local database';
1201
+ case 'shared-local':
1202
+ return 'shared local database';
1203
+ case 'external-existing':
1204
+ default:
1205
+ return 'external existing database';
1206
+ }
1207
+ }
1208
+ function inferDatabaseProvisioning(databaseUrl, dependencies = []) {
1209
+ if (dependencies.some((dependency) => dependency.kind === 'docker-container')) {
1210
+ return 'docker';
1211
+ }
1212
+ const parsed = parsePostgresConnectionString(databaseUrl);
1213
+ return isLocalPostgresHost(parsed.hostname) ? 'local' : 'managed';
1214
+ }
1215
+ function inferDatabaseIntentStrategy(options) {
1216
+ const databaseName = postgresDatabaseName(options.databaseUrl);
1217
+ if (options.databaseMode === 'managed') {
1218
+ return 'external-existing';
1219
+ }
1220
+ return databaseName === `iranti_${options.instanceName}`
1221
+ ? 'dedicated-local'
1222
+ : 'shared-local';
1223
+ }
1224
+ function buildInstanceDatabaseIntent(options) {
1225
+ const parsed = parsePostgresConnectionString(options.databaseUrl);
1226
+ const strategy = options.strategy ?? inferDatabaseIntentStrategy({
1227
+ instanceName: options.instanceName,
1228
+ databaseMode: options.databaseMode,
1229
+ databaseUrl: options.databaseUrl,
1230
+ });
1231
+ const port = Number.parseInt(parsed.port || '5432', 10);
1232
+ return {
1233
+ strategy,
1234
+ provisioning: options.databaseMode,
1235
+ host: parsed.hostname,
1236
+ ...(Number.isFinite(port) && port > 0 ? { port } : {}),
1237
+ database: postgresDatabaseName(options.databaseUrl),
1238
+ ...(options.dockerContainerName ? { dockerContainerName: options.dockerContainerName } : {}),
1239
+ };
1240
+ }
1241
+ function parseInstanceDatabaseIntent(raw) {
1242
+ if (raw === undefined || raw === null) {
1243
+ return { intent: null, errors: [] };
1244
+ }
1245
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
1246
+ return { intent: null, errors: ['databaseIntent must be an object'] };
1247
+ }
1248
+ const record = raw;
1249
+ const strategy = parseDatabaseIntentStrategy(typeof record.strategy === 'string' ? record.strategy : undefined);
1250
+ const provisioningRaw = typeof record.provisioning === 'string' ? record.provisioning.trim().toLowerCase() : '';
1251
+ const provisioning = provisioningRaw === 'local' || provisioningRaw === 'managed' || provisioningRaw === 'docker'
1252
+ ? provisioningRaw
1253
+ : null;
1254
+ const host = typeof record.host === 'string' ? record.host.trim() : '';
1255
+ const database = typeof record.database === 'string' ? record.database.trim() : '';
1256
+ const portRaw = typeof record.port === 'number'
1257
+ ? record.port
1258
+ : typeof record.port === 'string'
1259
+ ? Number.parseInt(record.port, 10)
1260
+ : undefined;
1261
+ const dockerContainerName = typeof record.dockerContainerName === 'string'
1262
+ ? record.dockerContainerName.trim()
1263
+ : undefined;
1264
+ const errors = [];
1265
+ if (!strategy) {
1266
+ errors.push('databaseIntent.strategy must be dedicated-local, shared-local, or external-existing');
1267
+ }
1268
+ if (!provisioning) {
1269
+ errors.push('databaseIntent.provisioning must be local, managed, or docker');
1270
+ }
1271
+ if (!host) {
1272
+ errors.push('databaseIntent.host is required');
1273
+ }
1274
+ if (!database) {
1275
+ errors.push('databaseIntent.database is required');
1276
+ }
1277
+ if (portRaw !== undefined && (!Number.isFinite(portRaw) || portRaw <= 0)) {
1278
+ errors.push('databaseIntent.port must be a positive integer');
1279
+ }
1280
+ if (errors.length > 0 || !strategy || !provisioning || !host || !database) {
1281
+ return { intent: null, errors };
1282
+ }
1283
+ return {
1284
+ intent: {
1285
+ strategy,
1286
+ provisioning,
1287
+ host,
1288
+ ...(portRaw !== undefined ? { port: portRaw } : {}),
1289
+ database,
1290
+ ...(dockerContainerName ? { dockerContainerName } : {}),
1291
+ },
1292
+ errors: [],
1293
+ };
1294
+ }
1295
+ function resolveInstanceDatabaseIntent(options) {
1296
+ const parsedMeta = parseInstanceDatabaseIntent(options.meta?.databaseIntent);
1297
+ if (parsedMeta.intent) {
1298
+ return { intent: parsedMeta.intent, source: 'meta', errors: parsedMeta.errors };
1299
+ }
1300
+ const dbUrl = options.env.DATABASE_URL?.trim();
1301
+ if (!dbUrl || detectPlaceholder(dbUrl)) {
1302
+ return { intent: null, source: 'none', errors: parsedMeta.errors };
1303
+ }
1304
+ const dockerDependency = options.dependencies.find((dependency) => dependency.kind === 'docker-container');
1305
+ return {
1306
+ intent: buildInstanceDatabaseIntent({
1307
+ instanceName: options.instanceName,
1308
+ databaseMode: inferDatabaseProvisioning(dbUrl, options.dependencies),
1309
+ databaseUrl: dbUrl,
1310
+ dockerContainerName: dockerDependency?.name,
1311
+ }),
1312
+ source: 'inferred',
1313
+ errors: parsedMeta.errors,
1314
+ };
1315
+ }
1316
+ function describeInstanceDatabaseIntent(intent, source = 'meta') {
1317
+ const target = `${intent.host}${intent.port ? `:${intent.port}` : ''}/${intent.database}`;
1318
+ const sourceSuffix = source === 'inferred' ? ' (inferred)' : '';
1319
+ const dockerSuffix = intent.dockerContainerName ? `, container ${intent.dockerContainerName}` : '';
1320
+ return `${describeDatabaseIntentStrategy(intent.strategy)} via ${intent.provisioning} -> ${target}${dockerSuffix}${sourceSuffix}`;
1321
+ }
1145
1322
  function sanitizeIdentifier(input, fallback) {
1146
1323
  const value = input.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '_').replace(/^_+|_+$/g, '');
1147
1324
  if (!value && input.trim()) {
@@ -1286,6 +1463,7 @@ async function ensureInstanceConfigured(root, name, config) {
1286
1463
  port: config.port,
1287
1464
  envFile,
1288
1465
  instanceDir,
1466
+ databaseIntent: config.databaseIntent,
1289
1467
  ...(config.dependencies && config.dependencies.length > 0 ? { dependencies: config.dependencies } : {}),
1290
1468
  };
1291
1469
  await writeJson(metaFile, meta);
@@ -1298,6 +1476,7 @@ async function ensureInstanceConfigured(root, name, config) {
1298
1476
  ...config.providerKeys,
1299
1477
  });
1300
1478
  await syncInstanceMeta(root, name, config.port, {
1479
+ databaseIntent: config.databaseIntent,
1301
1480
  ...(config.dependencies !== undefined ? { dependencies: config.dependencies } : {}),
1302
1481
  });
1303
1482
  return { envFile, instanceDir, created };
@@ -1420,6 +1599,10 @@ function resolveRecentMessages(args) {
1420
1599
  }
1421
1600
  return [];
1422
1601
  }
1602
+ function resolveBackfillFile(args) {
1603
+ const backfill = getFlag(args, 'backfill')?.trim();
1604
+ return backfill ? path_1.default.resolve(backfill) : null;
1605
+ }
1423
1606
  function parsePositiveInteger(raw, label) {
1424
1607
  if (!raw)
1425
1608
  return undefined;
@@ -1529,7 +1712,10 @@ function printHandshakeResult(target, task, result) {
1529
1712
  console.log(` memory facts ${result.workingMemory.length}`);
1530
1713
  console.log(` generated ${result.briefGeneratedAt}`);
1531
1714
  console.log('');
1532
- console.log(`Rules: ${truncateText(result.operatingRules, 160)}`);
1715
+ console.log('Rules:');
1716
+ for (const line of result.operatingRules.split(/\r?\n/)) {
1717
+ console.log(line.trim().length > 0 ? ` ${line}` : '');
1718
+ }
1533
1719
  if (result.workingMemory.length === 0) {
1534
1720
  console.log('');
1535
1721
  console.log('No working memory entries loaded.');
@@ -1549,6 +1735,9 @@ function printAttendResult(target, latestMessage, result) {
1549
1735
  console.log(` message ${truncateText(latestMessage, 120)}`);
1550
1736
  console.log(` inject ${result.shouldInject ? 'yes' : 'no'}`);
1551
1737
  console.log(` reason ${result.reason}`);
1738
+ if (result.bootstrap?.handshakePerformed) {
1739
+ console.log(` bootstrap handshake auto-ran (${result.bootstrap.task})`);
1740
+ }
1552
1741
  console.log(` method ${result.decision.method}`);
1553
1742
  console.log(` confidence ${result.decision.confidence}`);
1554
1743
  console.log(` explanation ${result.decision.explanation}`);
@@ -1879,12 +2068,24 @@ async function syncInstanceMeta(root, name, port, options = {}) {
1879
2068
  const existing = await readInstanceMetaFile(metaFile);
1880
2069
  const existingCreatedAt = typeof existing?.createdAt === 'string' ? existing.createdAt : undefined;
1881
2070
  const existingDependencies = (0, runtimeDependencies_1.parseInstanceDependencies)(existing?.dependencies).dependencies;
2071
+ const currentDependencies = options.dependencies ?? existingDependencies;
2072
+ const env = fs_1.default.existsSync(envFile)
2073
+ ? await readEnvFile(envFile).catch(() => ({}))
2074
+ : {};
2075
+ const resolvedDatabaseIntent = options.databaseIntent
2076
+ ?? resolveInstanceDatabaseIntent({
2077
+ instanceName: name,
2078
+ env,
2079
+ meta: existing,
2080
+ dependencies: currentDependencies,
2081
+ }).intent;
1882
2082
  const meta = {
1883
2083
  name,
1884
2084
  createdAt: existingCreatedAt ?? new Date().toISOString(),
1885
2085
  port,
1886
2086
  envFile,
1887
2087
  instanceDir,
2088
+ ...(resolvedDatabaseIntent ? { databaseIntent: resolvedDatabaseIntent } : {}),
1888
2089
  ...(options.dependencies !== undefined
1889
2090
  ? { dependencies: options.dependencies }
1890
2091
  : existingDependencies.length > 0
@@ -1992,6 +2193,7 @@ async function executeSetupPlan(plan) {
1992
2193
  const configured = await ensureInstanceConfigured(plan.root, plan.instanceName, {
1993
2194
  port: plan.port,
1994
2195
  dbUrl: plan.databaseUrl,
2196
+ databaseIntent: plan.databaseIntent,
1995
2197
  provider: plan.provider,
1996
2198
  providerKeys: plan.providerKeys,
1997
2199
  apiKey: plan.apiKey,
@@ -2070,6 +2272,7 @@ async function executeSetupPlan(plan) {
2070
2272
  port: plan.port,
2071
2273
  mode: plan.mode,
2072
2274
  databaseMode: plan.databaseMode,
2275
+ databaseIntent: plan.databaseIntent,
2073
2276
  bindings,
2074
2277
  };
2075
2278
  }
@@ -2096,6 +2299,7 @@ function parseSetupConfig(filePath) {
2096
2299
  ? 'local'
2097
2300
  : (() => { throw new Error(`Unsupported databaseMode in setup config: ${databaseModeRaw}`); })();
2098
2301
  const databaseUrl = deriveDatabaseUrlForMode(databaseMode, instanceName, String(raw?.databaseUrl ?? raw?.dbUrl ?? '').trim());
2302
+ const databaseIntentStrategy = parseDatabaseIntentStrategyFlag(String(raw?.databaseIntent ?? raw?.dbIntent ?? '').trim(), 'databaseIntent');
2099
2303
  const provider = normalizeProvider(String(raw?.provider ?? 'mock')) ?? 'mock';
2100
2304
  if (!isSupportedProvider(provider)) {
2101
2305
  throw new Error(`Unsupported provider in setup config: ${provider}`);
@@ -2134,6 +2338,13 @@ function parseSetupConfig(filePath) {
2134
2338
  port,
2135
2339
  databaseUrl,
2136
2340
  databaseMode,
2341
+ databaseIntent: buildInstanceDatabaseIntent({
2342
+ instanceName,
2343
+ databaseMode,
2344
+ databaseUrl,
2345
+ strategy: databaseIntentStrategy,
2346
+ dockerContainerName: typeof raw?.dockerContainerName === 'string' ? raw.dockerContainerName : undefined,
2347
+ }),
2137
2348
  provider,
2138
2349
  providerKeys,
2139
2350
  apiKey,
@@ -2174,6 +2385,7 @@ function defaultsSetupPlan(args) {
2174
2385
  throw new Error(`Invalid --db-mode '${explicit}'. Use local, managed, or docker.`);
2175
2386
  })();
2176
2387
  const databaseUrl = deriveDatabaseUrlForMode(databaseMode, instanceName, (getFlag(args, 'db-url') ?? process.env.DATABASE_URL ?? '').trim());
2388
+ const databaseIntentStrategy = parseDatabaseIntentStrategyFlag(getFlag(args, 'db-intent'), '--db-intent');
2177
2389
  const provider = normalizeProvider(getFlag(args, 'provider') ?? process.env.LLM_PROVIDER ?? 'mock') ?? 'mock';
2178
2390
  if (!isSupportedProvider(provider)) {
2179
2391
  throw new Error(`Unsupported provider '${provider}' for --defaults.`);
@@ -2215,6 +2427,13 @@ function defaultsSetupPlan(args) {
2215
2427
  port,
2216
2428
  databaseUrl,
2217
2429
  databaseMode,
2430
+ databaseIntent: buildInstanceDatabaseIntent({
2431
+ instanceName,
2432
+ databaseMode,
2433
+ databaseUrl,
2434
+ strategy: databaseIntentStrategy,
2435
+ dockerContainerName: getFlag(args, 'docker-container-name'),
2436
+ }),
2218
2437
  provider,
2219
2438
  providerKeys,
2220
2439
  apiKey,
@@ -4137,6 +4356,7 @@ async function setupCommand(args) {
4137
4356
  console.log(` instance url http://localhost:${result.port}`);
4138
4357
  console.log(` memory mode ${result.mode}`);
4139
4358
  console.log(` database mode ${result.databaseMode}`);
4359
+ console.log(` db strategy ${describeInstanceDatabaseIntent(result.databaseIntent)}`);
4140
4360
  if (result.bindings.length === 0) {
4141
4361
  console.log(` projects ${paint('none bound yet', 'yellow')}`);
4142
4362
  }
@@ -4224,6 +4444,9 @@ async function setupCommand(args) {
4224
4444
  const existingInstance = fs_1.default.existsSync(instancePaths(finalRoot, instanceName).envFile)
4225
4445
  ? await loadInstanceEnv(finalRoot, instanceName)
4226
4446
  : null;
4447
+ const existingInstanceMeta = fs_1.default.existsSync(instancePaths(finalRoot, instanceName).metaFile)
4448
+ ? await readInstanceMetaFile(instancePaths(finalRoot, instanceName).metaFile)
4449
+ : null;
4227
4450
  if (existingInstance) {
4228
4451
  console.log(`${infoLabel()} Found existing instance '${instanceName}'. Updating it.`);
4229
4452
  }
@@ -4244,6 +4467,7 @@ async function setupCommand(args) {
4244
4467
  let databaseProvisioned = false;
4245
4468
  let dockerContainerName;
4246
4469
  let databaseMode = recommendedDatabaseMode;
4470
+ let databaseIntentStrategy = null;
4247
4471
  printChoiceGuide('Database Mode Choices', [
4248
4472
  {
4249
4473
  choice: 'local',
@@ -4283,6 +4507,13 @@ async function setupCommand(args) {
4283
4507
  console.log(`${infoLabel()} Iranti will create the local database automatically if it does not already exist.`);
4284
4508
  }
4285
4509
  }
4510
+ databaseIntentStrategy = parseDatabaseIntentStrategyFlag(await prompt.line('Database intent (dedicated, shared, external)', databaseIntentPromptValue(resolveInstanceDatabaseIntent({
4511
+ instanceName,
4512
+ env: existingInstance?.env ?? { DATABASE_URL: dbUrl },
4513
+ meta: existingInstanceMeta,
4514
+ dependencies: (0, runtimeDependencies_1.parseInstanceDependencies)(existingInstanceMeta?.dependencies).dependencies,
4515
+ }).intent?.strategy
4516
+ ?? inferDatabaseIntentStrategy({ instanceName, databaseMode, databaseUrl: dbUrl }))), 'database intent');
4286
4517
  bootstrapDatabase = await promptYesNo(prompt, 'Run migrations and seed the database now?', true);
4287
4518
  break;
4288
4519
  }
@@ -4298,6 +4529,15 @@ async function setupCommand(args) {
4298
4529
  const containerName = sanitizeIdentifier(await promptNonEmpty(prompt, 'Docker container name', `iranti_${instanceName}_db`), `iranti_${instanceName}_db`);
4299
4530
  dockerContainerName = containerName;
4300
4531
  dbUrl = `postgresql://postgres:${dbPassword}@localhost:${dbHostPort}/${dbName}`;
4532
+ databaseIntentStrategy = parseDatabaseIntentStrategyFlag(await prompt.line('Database intent (dedicated, shared, external)', databaseIntentPromptValue(resolveInstanceDatabaseIntent({
4533
+ instanceName,
4534
+ env: existingInstance?.env ?? { DATABASE_URL: dbUrl },
4535
+ meta: existingInstanceMeta,
4536
+ dependencies: dockerContainerName
4537
+ ? [{ kind: 'docker-container', name: dockerContainerName, ...(dbHostPort > 0 ? { healthTcpPort: dbHostPort } : {}) }]
4538
+ : (0, runtimeDependencies_1.parseInstanceDependencies)(existingInstanceMeta?.dependencies).dependencies,
4539
+ }).intent?.strategy
4540
+ ?? inferDatabaseIntentStrategy({ instanceName, databaseMode, databaseUrl: dbUrl }))), 'database intent');
4301
4541
  console.log(`${infoLabel()} Docker will be used only for PostgreSQL. Iranti itself does not require Docker once a PostgreSQL database is available.`);
4302
4542
  if (await promptYesNo(prompt, `Start or reuse Docker container '${containerName}' now?`, true)) {
4303
4543
  await runDockerPostgresContainer({
@@ -4442,6 +4682,13 @@ async function setupCommand(args) {
4442
4682
  port,
4443
4683
  databaseUrl: dbUrl,
4444
4684
  databaseMode,
4685
+ databaseIntent: buildInstanceDatabaseIntent({
4686
+ instanceName,
4687
+ databaseMode,
4688
+ databaseUrl: dbUrl,
4689
+ strategy: databaseIntentStrategy,
4690
+ dockerContainerName,
4691
+ }),
4445
4692
  provider,
4446
4693
  providerKeys,
4447
4694
  apiKey: defaultApiKey,
@@ -4472,6 +4719,7 @@ async function setupCommand(args) {
4472
4719
  console.log(` instance url http://localhost:${finalResult.port}`);
4473
4720
  console.log(` memory mode ${finalResult.mode}`);
4474
4721
  console.log(` database mode ${finalResult.databaseMode}`);
4722
+ console.log(` db strategy ${describeInstanceDatabaseIntent(finalResult.databaseIntent)}`);
4475
4723
  if (finalResult.bindings.length === 0) {
4476
4724
  console.log(` projects ${paint('none bound yet', 'yellow')}`);
4477
4725
  }
@@ -5236,6 +5484,12 @@ async function showInstanceCommand(args) {
5236
5484
  : {};
5237
5485
  const meta = await readInstanceMetaFile(instancePaths(root, name).metaFile);
5238
5486
  const dependencies = (0, runtimeDependencies_1.parseInstanceDependencies)(meta?.dependencies).dependencies;
5487
+ const databaseIntent = resolveInstanceDatabaseIntent({
5488
+ instanceName: name,
5489
+ env,
5490
+ meta,
5491
+ dependencies,
5492
+ });
5239
5493
  const runtime = await readInstanceRuntimeSummary(root, name);
5240
5494
  console.log(bold(`Instance: ${name}`));
5241
5495
  console.log(` dir : ${instanceDir}`);
@@ -5243,6 +5497,9 @@ async function showInstanceCommand(args) {
5243
5497
  console.log(` config: ${describeInstanceConfig(config)}`);
5244
5498
  console.log(` port: ${env.IRANTI_PORT ?? '3001'}`);
5245
5499
  console.log(` db : ${env.DATABASE_URL ?? '(missing)'}`);
5500
+ if (databaseIntent.intent) {
5501
+ console.log(` db strategy: ${describeInstanceDatabaseIntent(databaseIntent.intent, databaseIntent.source)}`);
5502
+ }
5246
5503
  console.log(` esc : ${env.IRANTI_ESCALATION_DIR ?? '(missing)'}`);
5247
5504
  if (dependencies.length > 0) {
5248
5505
  console.log(` deps: ${dependencies.map((dependency) => (0, runtimeDependencies_1.describeInstanceDependency)(dependency)).join(', ')}`);
@@ -5371,6 +5628,12 @@ async function configureInstanceCommand(args) {
5371
5628
  const { instanceDir, envFile, env, config } = await loadInstanceEnv(root, name, { allowRepair: true });
5372
5629
  const currentMeta = await readInstanceMetaFile(instancePaths(root, name).metaFile);
5373
5630
  const currentDependencies = (0, runtimeDependencies_1.parseInstanceDependencies)(currentMeta?.dependencies).dependencies;
5631
+ const currentDatabaseIntent = resolveInstanceDatabaseIntent({
5632
+ instanceName: name,
5633
+ env,
5634
+ meta: currentMeta,
5635
+ dependencies: currentDependencies,
5636
+ });
5374
5637
  const updates = {};
5375
5638
  let portRaw = getFlag(args, 'port');
5376
5639
  let dbUrl = getFlag(args, 'db-url');
@@ -5380,6 +5643,7 @@ async function configureInstanceCommand(args) {
5380
5643
  let clearProviderKey = hasFlag(args, 'clear-provider-key');
5381
5644
  let dockerContainerName = getFlag(args, 'docker-container');
5382
5645
  let dockerHealthPortRaw = getFlag(args, 'docker-health-port');
5646
+ let dbIntentRaw = getFlag(args, 'db-intent');
5383
5647
  const clearDockerContainer = hasFlag(args, 'clear-docker-container');
5384
5648
  if (hasFlag(args, 'interactive')) {
5385
5649
  await withPromptSession(async (prompt) => {
@@ -5387,12 +5651,14 @@ async function configureInstanceCommand(args) {
5387
5651
  'This updates one existing instance in place.',
5388
5652
  'API port controls where the Iranti API listens.',
5389
5653
  'DATABASE_URL points at the PostgreSQL database for this instance.',
5654
+ 'Database intent tells Iranti whether this should be treated as a dedicated local DB, a shared local DB, or an external existing DB.',
5390
5655
  'LLM provider and provider key control which model backend Iranti uses.',
5391
5656
  'Iranti API key is the client credential other tools and project bindings use to authenticate.',
5392
5657
  'Optional Docker dependency settings let `iranti run --instance` start a recorded backing container before the API boots.',
5393
5658
  ]);
5394
5659
  portRaw = await prompt.line('API port', portRaw ?? env.IRANTI_PORT);
5395
5660
  dbUrl = await prompt.line('DATABASE_URL', dbUrl ?? env.DATABASE_URL);
5661
+ dbIntentRaw = await prompt.line('Database intent (dedicated, shared, external)', dbIntentRaw ?? databaseIntentPromptValue(currentDatabaseIntent.intent?.strategy));
5396
5662
  providerInput = await prompt.line('LLM provider', providerInput ?? env.LLM_PROVIDER ?? 'mock');
5397
5663
  const interactiveProvider = normalizeProvider(providerInput ?? env.LLM_PROVIDER ?? 'mock');
5398
5664
  const interactiveProviderEnvKey = providerKeyEnv(interactiveProvider);
@@ -5415,6 +5681,7 @@ async function configureInstanceCommand(args) {
5415
5681
  }
5416
5682
  if (dbUrl)
5417
5683
  updates.DATABASE_URL = dbUrl;
5684
+ const requestedDatabaseIntent = parseDatabaseIntentStrategyFlag(dbIntentRaw, '--db-intent');
5418
5685
  if (apiKey)
5419
5686
  updates.IRANTI_API_KEY = apiKey;
5420
5687
  const provider = normalizeProvider(providerInput ?? env.LLM_PROVIDER ?? 'mock');
@@ -5467,8 +5734,9 @@ async function configureInstanceCommand(args) {
5467
5734
  }
5468
5735
  if (Object.keys(updates).length === 0) {
5469
5736
  const dependenciesUnchanged = JSON.stringify(nextDependencies) === JSON.stringify(currentDependencies);
5470
- if (dependenciesUnchanged) {
5471
- throw new Error('No changes provided. Use flags like --provider, --provider-key, --api-key, --db-url, --port, or the Docker dependency flags.');
5737
+ const databaseIntentUnchanged = requestedDatabaseIntent === null;
5738
+ if (dependenciesUnchanged && databaseIntentUnchanged) {
5739
+ throw new Error('No changes provided. Use flags like --provider, --provider-key, --api-key, --db-url, --db-intent, --port, or the Docker dependency flags.');
5472
5740
  }
5473
5741
  }
5474
5742
  const nextEnv = { ...env };
@@ -5488,9 +5756,19 @@ async function configureInstanceCommand(args) {
5488
5756
  if (!nextEnv.DATABASE_URL?.trim()) {
5489
5757
  throw cliError('IRANTI_INSTANCE_DATABASE_REQUIRED', `Instance '${name}' still needs DATABASE_URL before it can be considered repaired.`, ['Pass `--db-url <postgresql://...>` or rerun `iranti configure instance <name> --interactive`.'], { instance: name, envFile, config: config.classification });
5490
5758
  }
5759
+ const nextDatabaseIntent = buildInstanceDatabaseIntent({
5760
+ instanceName: name,
5761
+ databaseMode: inferDatabaseProvisioning(nextEnv.DATABASE_URL, nextDependencies),
5762
+ databaseUrl: nextEnv.DATABASE_URL,
5763
+ strategy: requestedDatabaseIntent ?? currentDatabaseIntent.intent?.strategy,
5764
+ dockerContainerName: nextDependencies.find((dependency) => dependency.kind === 'docker-container')?.name,
5765
+ });
5491
5766
  await ensureDir(instanceDir);
5492
5767
  await upsertEnvFile(envFile, updates);
5493
- await syncInstanceMeta(root, name, nextPort, { dependencies: nextDependencies });
5768
+ await syncInstanceMeta(root, name, nextPort, {
5769
+ dependencies: nextDependencies,
5770
+ databaseIntent: nextDatabaseIntent,
5771
+ });
5494
5772
  const json = hasFlag(args, 'json');
5495
5773
  const result = {
5496
5774
  instance: name,
@@ -5499,6 +5777,7 @@ async function configureInstanceCommand(args) {
5499
5777
  provider: updates.LLM_PROVIDER ?? env.LLM_PROVIDER ?? 'mock',
5500
5778
  apiKeyChanged: Boolean(apiKey),
5501
5779
  providerKeyChanged: Boolean(providerKey) || hasFlag(args, 'clear-provider-key'),
5780
+ databaseIntent: nextDatabaseIntent.strategy,
5502
5781
  dependencies: nextDependencies.map((dependency) => (0, runtimeDependencies_1.describeInstanceDependency)(dependency)),
5503
5782
  };
5504
5783
  if (json) {
@@ -5509,6 +5788,7 @@ async function configureInstanceCommand(args) {
5509
5788
  console.log(` status ${okLabel()}`);
5510
5789
  console.log(` env ${envFile}`);
5511
5790
  console.log(` keys ${result.updatedKeys.join(', ')}`);
5791
+ console.log(` db ${describeInstanceDatabaseIntent(nextDatabaseIntent)}`);
5512
5792
  if (result.dependencies.length > 0) {
5513
5793
  console.log(` deps ${result.dependencies.join(', ')}`);
5514
5794
  }
@@ -5748,7 +6028,24 @@ async function handshakeCommand(args) {
5748
6028
  const json = hasFlag(args, 'json');
5749
6029
  const target = await resolveAttendantCliTarget(args);
5750
6030
  const task = getFlag(args, 'task')?.trim() || 'CLI handshake';
5751
- const recentMessages = resolveRecentMessages(args);
6031
+ let recentMessages = resolveRecentMessages(args);
6032
+ const backfillFile = resolveBackfillFile(args);
6033
+ let backfillResult = null;
6034
+ if (backfillFile) {
6035
+ const content = fs_1.default.readFileSync(backfillFile, 'utf-8');
6036
+ backfillResult = await (0, autoRemember_1.backfillChatHistory)({
6037
+ iranti: target.iranti,
6038
+ content,
6039
+ agent: target.agentId,
6040
+ source: 'CLIBackfill',
6041
+ });
6042
+ if (recentMessages.length === 0) {
6043
+ recentMessages = (0, autoRemember_1.parseBackfillChatTranscript)(content)
6044
+ .map((message) => message.text.trim())
6045
+ .filter(Boolean)
6046
+ .slice(-12);
6047
+ }
6048
+ }
5752
6049
  const result = await target.iranti.handshake({
5753
6050
  agent: target.agentId,
5754
6051
  task,
@@ -5760,12 +6057,33 @@ async function handshakeCommand(args) {
5760
6057
  envSource: target.envSource,
5761
6058
  envFile: target.envFile,
5762
6059
  task,
6060
+ backfillFile,
6061
+ backfillResult,
5763
6062
  recentMessages,
5764
6063
  result,
5765
6064
  }, null, 2));
5766
6065
  return;
5767
6066
  }
5768
6067
  printHandshakeResult(target, task, result);
6068
+ if (backfillResult) {
6069
+ console.log('');
6070
+ console.log('Backfill:');
6071
+ console.log(` file ${backfillFile}`);
6072
+ console.log(` messages ${backfillResult.messagesParsed}`);
6073
+ console.log(` extracted ${backfillResult.extracted}`);
6074
+ console.log(` written ${backfillResult.written}`);
6075
+ if (backfillResult.skipped.length > 0) {
6076
+ console.log(` skipped ${backfillResult.skipped.length}`);
6077
+ }
6078
+ }
6079
+ if (result.backfillSuggestion?.suggested && !backfillFile) {
6080
+ console.log('');
6081
+ console.log('Backfill suggestion:');
6082
+ console.log(` reason ${result.backfillSuggestion.reason}`);
6083
+ console.log(` candidateFacts ${result.backfillSuggestion.candidateFacts}`);
6084
+ console.log(` sample keys ${result.backfillSuggestion.sampleKeys.join(', ')}`);
6085
+ console.log(` command ${result.backfillSuggestion.suggestedCommand}`);
6086
+ }
5769
6087
  console.log('');
5770
6088
  console.log(`${infoLabel()} This is a manual Attendant inspection tool. Claude Code should still use hooks + MCP in normal operation.`);
5771
6089
  }
@@ -62,6 +62,7 @@ function printHelp() {
62
62
  ' IRANTI_AUTO_REMEMBER Opt-in explicit prompt auto-save before attend()',
63
63
  '',
64
64
  'This server is intended for Claude Code and other MCP clients over stdio.',
65
+ 'If you run `iranti mcp` directly in a terminal, it will stay running and wait for an MCP client.',
65
66
  ].join('\n'));
66
67
  }
67
68
  function requireConnectionString() {
@@ -135,6 +136,9 @@ function normalizeRecentMessages(messages) {
135
136
  .filter(Boolean)
136
137
  .slice(-12);
137
138
  }
139
+ function isInteractiveTerminalLaunch() {
140
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
141
+ }
138
142
  async function main() {
139
143
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
140
144
  printHelp();
@@ -147,13 +151,15 @@ async function main() {
147
151
  await ensureDefaultAgent(iranti);
148
152
  const server = new mcp_js_1.McpServer({
149
153
  name: 'iranti-mcp',
150
- version: '0.2.45',
154
+ version: '0.2.46',
151
155
  });
152
156
  server.registerTool('iranti_handshake', {
153
157
  description: `Initialize or refresh an agent's working-memory brief for the current task.
154
158
  Call this at session start or when a new task begins, passing the task and
155
159
  recent messages. Returns operating rules plus prioritized relevant memory
156
- for that task. Do not use this as a per-turn retrieval tool; use iranti_attend.`,
160
+ for that task. If the recent messages appear to contain durable facts that
161
+ are not yet in shared memory, the result may include a backfill suggestion.
162
+ Do not use this as a per-turn retrieval tool; use iranti_attend.`,
157
163
  inputSchema: {
158
164
  task: z.string().min(1).describe('The current task or objective.'),
159
165
  recentMessages: z.array(z.string()).optional().describe('Recent conversation messages.'),
@@ -174,7 +180,9 @@ visible context window. If the user is asking you to recall a remembered
174
180
  fact (for example a preference, decision, blocker, next step, or prior
175
181
  project detail), use this before answering instead of guessing or saying
176
182
  you do not know. Returns an injection decision plus any facts that should
177
- be added to context if relevant memory is missing.
183
+ be added to context if relevant memory is missing. If no handshake has been
184
+ performed yet for this agent in the current process, attend will auto-bootstrap
185
+ the session first and report that in the result metadata.
178
186
  Omitting currentContext falls back to latestMessage only; pass the
179
187
  full visible context when available.`,
180
188
  inputSchema: {
@@ -387,6 +395,9 @@ this for arbitrary prose or every turn.`,
387
395
  const result = await iranti.whoKnows(entity);
388
396
  return textResult(result);
389
397
  });
398
+ if (isInteractiveTerminalLaunch()) {
399
+ console.error('[iranti-mcp] stdio server running; waiting for an MCP client. Press Ctrl+C to exit.');
400
+ }
390
401
  const transport = new stdio_js_1.StdioServerTransport();
391
402
  await server.connect(transport);
392
403
  }