iranti 0.2.44 → 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.
- package/dist/scripts/iranti-cli.js +344 -9
- package/dist/scripts/iranti-mcp.js +14 -3
- package/dist/scripts/seed.js +13 -8
- package/dist/src/api/server.js +3 -1
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/attendant/AttendantInstance.d.ts +17 -0
- package/dist/src/attendant/AttendantInstance.d.ts.map +1 -1
- package/dist/src/attendant/AttendantInstance.js +182 -6
- package/dist/src/attendant/AttendantInstance.js.map +1 -1
- package/dist/src/attendant/index.d.ts +1 -1
- package/dist/src/attendant/index.d.ts.map +1 -1
- package/dist/src/attendant/index.js.map +1 -1
- package/dist/src/lib/autoRemember.d.ts +18 -0
- package/dist/src/lib/autoRemember.d.ts.map +1 -1
- package/dist/src/lib/autoRemember.js +91 -0
- package/dist/src/lib/autoRemember.js.map +1 -1
- package/dist/src/lib/cliHelpCatalog.d.ts.map +1 -1
- package/dist/src/lib/cliHelpCatalog.js +9 -8
- package/dist/src/lib/cliHelpCatalog.js.map +1 -1
- package/dist/src/lib/dbStaffEventEmitter.d.ts +7 -0
- package/dist/src/lib/dbStaffEventEmitter.d.ts.map +1 -0
- package/dist/src/lib/dbStaffEventEmitter.js +64 -0
- package/dist/src/lib/dbStaffEventEmitter.js.map +1 -0
- package/dist/src/lib/runtimeLifecycle.d.ts.map +1 -1
- package/dist/src/lib/runtimeLifecycle.js +25 -4
- package/dist/src/lib/runtimeLifecycle.js.map +1 -1
- package/dist/src/sdk/index.d.ts.map +1 -1
- package/dist/src/sdk/index.js +5 -1
- package/dist/src/sdk/index.js.map +1 -1
- package/package.json +2 -1
|
@@ -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 =
|
|
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(
|
|
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,
|
|
@@ -2658,12 +2877,28 @@ async function spawnDetachedCli(args, cwd) {
|
|
|
2658
2877
|
child.unref();
|
|
2659
2878
|
return pid;
|
|
2660
2879
|
}
|
|
2661
|
-
|
|
2880
|
+
function isFreshRestartState(inspection, previousPid, previousStartedAt) {
|
|
2881
|
+
if (!inspection.running || !inspection.state)
|
|
2882
|
+
return false;
|
|
2883
|
+
if (inspection.state.pid && inspection.state.pid !== previousPid) {
|
|
2884
|
+
return true;
|
|
2885
|
+
}
|
|
2886
|
+
if (!previousStartedAt) {
|
|
2887
|
+
return false;
|
|
2888
|
+
}
|
|
2889
|
+
const previousStartedMs = Date.parse(previousStartedAt);
|
|
2890
|
+
const currentStartedMs = Date.parse(inspection.state.startedAt);
|
|
2891
|
+
if (!Number.isFinite(previousStartedMs) || !Number.isFinite(currentStartedMs)) {
|
|
2892
|
+
return false;
|
|
2893
|
+
}
|
|
2894
|
+
return currentStartedMs > previousStartedMs;
|
|
2895
|
+
}
|
|
2896
|
+
async function waitForRestartedRuntime(runtimeFile, previousPid, previousStartedAt, timeoutMs) {
|
|
2662
2897
|
const startedAt = Date.now();
|
|
2663
2898
|
let lastInspection = await (0, runtimeLifecycle_1.inspectRuntimeState)(runtimeFile);
|
|
2664
2899
|
while (Date.now() - startedAt < timeoutMs) {
|
|
2665
2900
|
lastInspection = await (0, runtimeLifecycle_1.inspectRuntimeState)(runtimeFile);
|
|
2666
|
-
if (lastInspection
|
|
2901
|
+
if (isFreshRestartState(lastInspection, previousPid, previousStartedAt)) {
|
|
2667
2902
|
return lastInspection;
|
|
2668
2903
|
}
|
|
2669
2904
|
if (lastInspection.classification === 'invalid') {
|
|
@@ -2689,6 +2924,7 @@ async function restartInstanceRuntime(args, instanceName, scope, root) {
|
|
|
2689
2924
|
}
|
|
2690
2925
|
}
|
|
2691
2926
|
const runtimeFile = instancePaths(root, instanceName).runtimeFile;
|
|
2927
|
+
const startupTimeoutMs = Math.max(timeoutMs, 30000);
|
|
2692
2928
|
const newPid = await spawnDetachedCli([
|
|
2693
2929
|
'run',
|
|
2694
2930
|
'--instance',
|
|
@@ -2698,7 +2934,7 @@ async function restartInstanceRuntime(args, instanceName, scope, root) {
|
|
|
2698
2934
|
'--root',
|
|
2699
2935
|
root,
|
|
2700
2936
|
], root);
|
|
2701
|
-
await waitForRestartedRuntime(runtimeFile, previousPid,
|
|
2937
|
+
await waitForRestartedRuntime(runtimeFile, previousPid, runtimeBefore.state?.startedAt ?? null, startupTimeoutMs);
|
|
2702
2938
|
return {
|
|
2703
2939
|
previousPid,
|
|
2704
2940
|
newPid,
|
|
@@ -4120,6 +4356,7 @@ async function setupCommand(args) {
|
|
|
4120
4356
|
console.log(` instance url http://localhost:${result.port}`);
|
|
4121
4357
|
console.log(` memory mode ${result.mode}`);
|
|
4122
4358
|
console.log(` database mode ${result.databaseMode}`);
|
|
4359
|
+
console.log(` db strategy ${describeInstanceDatabaseIntent(result.databaseIntent)}`);
|
|
4123
4360
|
if (result.bindings.length === 0) {
|
|
4124
4361
|
console.log(` projects ${paint('none bound yet', 'yellow')}`);
|
|
4125
4362
|
}
|
|
@@ -4207,6 +4444,9 @@ async function setupCommand(args) {
|
|
|
4207
4444
|
const existingInstance = fs_1.default.existsSync(instancePaths(finalRoot, instanceName).envFile)
|
|
4208
4445
|
? await loadInstanceEnv(finalRoot, instanceName)
|
|
4209
4446
|
: null;
|
|
4447
|
+
const existingInstanceMeta = fs_1.default.existsSync(instancePaths(finalRoot, instanceName).metaFile)
|
|
4448
|
+
? await readInstanceMetaFile(instancePaths(finalRoot, instanceName).metaFile)
|
|
4449
|
+
: null;
|
|
4210
4450
|
if (existingInstance) {
|
|
4211
4451
|
console.log(`${infoLabel()} Found existing instance '${instanceName}'. Updating it.`);
|
|
4212
4452
|
}
|
|
@@ -4227,6 +4467,7 @@ async function setupCommand(args) {
|
|
|
4227
4467
|
let databaseProvisioned = false;
|
|
4228
4468
|
let dockerContainerName;
|
|
4229
4469
|
let databaseMode = recommendedDatabaseMode;
|
|
4470
|
+
let databaseIntentStrategy = null;
|
|
4230
4471
|
printChoiceGuide('Database Mode Choices', [
|
|
4231
4472
|
{
|
|
4232
4473
|
choice: 'local',
|
|
@@ -4266,6 +4507,13 @@ async function setupCommand(args) {
|
|
|
4266
4507
|
console.log(`${infoLabel()} Iranti will create the local database automatically if it does not already exist.`);
|
|
4267
4508
|
}
|
|
4268
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');
|
|
4269
4517
|
bootstrapDatabase = await promptYesNo(prompt, 'Run migrations and seed the database now?', true);
|
|
4270
4518
|
break;
|
|
4271
4519
|
}
|
|
@@ -4281,6 +4529,15 @@ async function setupCommand(args) {
|
|
|
4281
4529
|
const containerName = sanitizeIdentifier(await promptNonEmpty(prompt, 'Docker container name', `iranti_${instanceName}_db`), `iranti_${instanceName}_db`);
|
|
4282
4530
|
dockerContainerName = containerName;
|
|
4283
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');
|
|
4284
4541
|
console.log(`${infoLabel()} Docker will be used only for PostgreSQL. Iranti itself does not require Docker once a PostgreSQL database is available.`);
|
|
4285
4542
|
if (await promptYesNo(prompt, `Start or reuse Docker container '${containerName}' now?`, true)) {
|
|
4286
4543
|
await runDockerPostgresContainer({
|
|
@@ -4425,6 +4682,13 @@ async function setupCommand(args) {
|
|
|
4425
4682
|
port,
|
|
4426
4683
|
databaseUrl: dbUrl,
|
|
4427
4684
|
databaseMode,
|
|
4685
|
+
databaseIntent: buildInstanceDatabaseIntent({
|
|
4686
|
+
instanceName,
|
|
4687
|
+
databaseMode,
|
|
4688
|
+
databaseUrl: dbUrl,
|
|
4689
|
+
strategy: databaseIntentStrategy,
|
|
4690
|
+
dockerContainerName,
|
|
4691
|
+
}),
|
|
4428
4692
|
provider,
|
|
4429
4693
|
providerKeys,
|
|
4430
4694
|
apiKey: defaultApiKey,
|
|
@@ -4455,6 +4719,7 @@ async function setupCommand(args) {
|
|
|
4455
4719
|
console.log(` instance url http://localhost:${finalResult.port}`);
|
|
4456
4720
|
console.log(` memory mode ${finalResult.mode}`);
|
|
4457
4721
|
console.log(` database mode ${finalResult.databaseMode}`);
|
|
4722
|
+
console.log(` db strategy ${describeInstanceDatabaseIntent(finalResult.databaseIntent)}`);
|
|
4458
4723
|
if (finalResult.bindings.length === 0) {
|
|
4459
4724
|
console.log(` projects ${paint('none bound yet', 'yellow')}`);
|
|
4460
4725
|
}
|
|
@@ -5219,6 +5484,12 @@ async function showInstanceCommand(args) {
|
|
|
5219
5484
|
: {};
|
|
5220
5485
|
const meta = await readInstanceMetaFile(instancePaths(root, name).metaFile);
|
|
5221
5486
|
const dependencies = (0, runtimeDependencies_1.parseInstanceDependencies)(meta?.dependencies).dependencies;
|
|
5487
|
+
const databaseIntent = resolveInstanceDatabaseIntent({
|
|
5488
|
+
instanceName: name,
|
|
5489
|
+
env,
|
|
5490
|
+
meta,
|
|
5491
|
+
dependencies,
|
|
5492
|
+
});
|
|
5222
5493
|
const runtime = await readInstanceRuntimeSummary(root, name);
|
|
5223
5494
|
console.log(bold(`Instance: ${name}`));
|
|
5224
5495
|
console.log(` dir : ${instanceDir}`);
|
|
@@ -5226,6 +5497,9 @@ async function showInstanceCommand(args) {
|
|
|
5226
5497
|
console.log(` config: ${describeInstanceConfig(config)}`);
|
|
5227
5498
|
console.log(` port: ${env.IRANTI_PORT ?? '3001'}`);
|
|
5228
5499
|
console.log(` db : ${env.DATABASE_URL ?? '(missing)'}`);
|
|
5500
|
+
if (databaseIntent.intent) {
|
|
5501
|
+
console.log(` db strategy: ${describeInstanceDatabaseIntent(databaseIntent.intent, databaseIntent.source)}`);
|
|
5502
|
+
}
|
|
5229
5503
|
console.log(` esc : ${env.IRANTI_ESCALATION_DIR ?? '(missing)'}`);
|
|
5230
5504
|
if (dependencies.length > 0) {
|
|
5231
5505
|
console.log(` deps: ${dependencies.map((dependency) => (0, runtimeDependencies_1.describeInstanceDependency)(dependency)).join(', ')}`);
|
|
@@ -5354,6 +5628,12 @@ async function configureInstanceCommand(args) {
|
|
|
5354
5628
|
const { instanceDir, envFile, env, config } = await loadInstanceEnv(root, name, { allowRepair: true });
|
|
5355
5629
|
const currentMeta = await readInstanceMetaFile(instancePaths(root, name).metaFile);
|
|
5356
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
|
+
});
|
|
5357
5637
|
const updates = {};
|
|
5358
5638
|
let portRaw = getFlag(args, 'port');
|
|
5359
5639
|
let dbUrl = getFlag(args, 'db-url');
|
|
@@ -5363,6 +5643,7 @@ async function configureInstanceCommand(args) {
|
|
|
5363
5643
|
let clearProviderKey = hasFlag(args, 'clear-provider-key');
|
|
5364
5644
|
let dockerContainerName = getFlag(args, 'docker-container');
|
|
5365
5645
|
let dockerHealthPortRaw = getFlag(args, 'docker-health-port');
|
|
5646
|
+
let dbIntentRaw = getFlag(args, 'db-intent');
|
|
5366
5647
|
const clearDockerContainer = hasFlag(args, 'clear-docker-container');
|
|
5367
5648
|
if (hasFlag(args, 'interactive')) {
|
|
5368
5649
|
await withPromptSession(async (prompt) => {
|
|
@@ -5370,12 +5651,14 @@ async function configureInstanceCommand(args) {
|
|
|
5370
5651
|
'This updates one existing instance in place.',
|
|
5371
5652
|
'API port controls where the Iranti API listens.',
|
|
5372
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.',
|
|
5373
5655
|
'LLM provider and provider key control which model backend Iranti uses.',
|
|
5374
5656
|
'Iranti API key is the client credential other tools and project bindings use to authenticate.',
|
|
5375
5657
|
'Optional Docker dependency settings let `iranti run --instance` start a recorded backing container before the API boots.',
|
|
5376
5658
|
]);
|
|
5377
5659
|
portRaw = await prompt.line('API port', portRaw ?? env.IRANTI_PORT);
|
|
5378
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));
|
|
5379
5662
|
providerInput = await prompt.line('LLM provider', providerInput ?? env.LLM_PROVIDER ?? 'mock');
|
|
5380
5663
|
const interactiveProvider = normalizeProvider(providerInput ?? env.LLM_PROVIDER ?? 'mock');
|
|
5381
5664
|
const interactiveProviderEnvKey = providerKeyEnv(interactiveProvider);
|
|
@@ -5398,6 +5681,7 @@ async function configureInstanceCommand(args) {
|
|
|
5398
5681
|
}
|
|
5399
5682
|
if (dbUrl)
|
|
5400
5683
|
updates.DATABASE_URL = dbUrl;
|
|
5684
|
+
const requestedDatabaseIntent = parseDatabaseIntentStrategyFlag(dbIntentRaw, '--db-intent');
|
|
5401
5685
|
if (apiKey)
|
|
5402
5686
|
updates.IRANTI_API_KEY = apiKey;
|
|
5403
5687
|
const provider = normalizeProvider(providerInput ?? env.LLM_PROVIDER ?? 'mock');
|
|
@@ -5450,8 +5734,9 @@ async function configureInstanceCommand(args) {
|
|
|
5450
5734
|
}
|
|
5451
5735
|
if (Object.keys(updates).length === 0) {
|
|
5452
5736
|
const dependenciesUnchanged = JSON.stringify(nextDependencies) === JSON.stringify(currentDependencies);
|
|
5453
|
-
|
|
5454
|
-
|
|
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.');
|
|
5455
5740
|
}
|
|
5456
5741
|
}
|
|
5457
5742
|
const nextEnv = { ...env };
|
|
@@ -5471,9 +5756,19 @@ async function configureInstanceCommand(args) {
|
|
|
5471
5756
|
if (!nextEnv.DATABASE_URL?.trim()) {
|
|
5472
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 });
|
|
5473
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
|
+
});
|
|
5474
5766
|
await ensureDir(instanceDir);
|
|
5475
5767
|
await upsertEnvFile(envFile, updates);
|
|
5476
|
-
await syncInstanceMeta(root, name, nextPort, {
|
|
5768
|
+
await syncInstanceMeta(root, name, nextPort, {
|
|
5769
|
+
dependencies: nextDependencies,
|
|
5770
|
+
databaseIntent: nextDatabaseIntent,
|
|
5771
|
+
});
|
|
5477
5772
|
const json = hasFlag(args, 'json');
|
|
5478
5773
|
const result = {
|
|
5479
5774
|
instance: name,
|
|
@@ -5482,6 +5777,7 @@ async function configureInstanceCommand(args) {
|
|
|
5482
5777
|
provider: updates.LLM_PROVIDER ?? env.LLM_PROVIDER ?? 'mock',
|
|
5483
5778
|
apiKeyChanged: Boolean(apiKey),
|
|
5484
5779
|
providerKeyChanged: Boolean(providerKey) || hasFlag(args, 'clear-provider-key'),
|
|
5780
|
+
databaseIntent: nextDatabaseIntent.strategy,
|
|
5485
5781
|
dependencies: nextDependencies.map((dependency) => (0, runtimeDependencies_1.describeInstanceDependency)(dependency)),
|
|
5486
5782
|
};
|
|
5487
5783
|
if (json) {
|
|
@@ -5492,6 +5788,7 @@ async function configureInstanceCommand(args) {
|
|
|
5492
5788
|
console.log(` status ${okLabel()}`);
|
|
5493
5789
|
console.log(` env ${envFile}`);
|
|
5494
5790
|
console.log(` keys ${result.updatedKeys.join(', ')}`);
|
|
5791
|
+
console.log(` db ${describeInstanceDatabaseIntent(nextDatabaseIntent)}`);
|
|
5495
5792
|
if (result.dependencies.length > 0) {
|
|
5496
5793
|
console.log(` deps ${result.dependencies.join(', ')}`);
|
|
5497
5794
|
}
|
|
@@ -5731,7 +6028,24 @@ async function handshakeCommand(args) {
|
|
|
5731
6028
|
const json = hasFlag(args, 'json');
|
|
5732
6029
|
const target = await resolveAttendantCliTarget(args);
|
|
5733
6030
|
const task = getFlag(args, 'task')?.trim() || 'CLI handshake';
|
|
5734
|
-
|
|
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
|
+
}
|
|
5735
6049
|
const result = await target.iranti.handshake({
|
|
5736
6050
|
agent: target.agentId,
|
|
5737
6051
|
task,
|
|
@@ -5743,12 +6057,33 @@ async function handshakeCommand(args) {
|
|
|
5743
6057
|
envSource: target.envSource,
|
|
5744
6058
|
envFile: target.envFile,
|
|
5745
6059
|
task,
|
|
6060
|
+
backfillFile,
|
|
6061
|
+
backfillResult,
|
|
5746
6062
|
recentMessages,
|
|
5747
6063
|
result,
|
|
5748
6064
|
}, null, 2));
|
|
5749
6065
|
return;
|
|
5750
6066
|
}
|
|
5751
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
|
+
}
|
|
5752
6087
|
console.log('');
|
|
5753
6088
|
console.log(`${infoLabel()} This is a manual Attendant inspection tool. Claude Code should still use hooks + MCP in normal operation.`);
|
|
5754
6089
|
}
|