clawdie-ai/setup/skills-memory.ts
Mevy Assistant c65c289f08 refactor(multitenant): make tenant and platform identity explicit
Replace ambiguous AGENT_NAME usage across runtime, setup, and helper scripts with explicit TENANT_ID or platform runtime identity where appropriate. Keep AGENT_NAME as a compatibility boundary instead of the primary source for shared runtime naming.

---
Build: pass | Tests: pass — 138 passed (10 files)
2026-04-23 21:41:42 +02:00

237 lines
6.5 KiB
TypeScript

/**
* Step: skills-memory — bootstrap built-in knowledge into the skills DB.
*
* Goal: reduce install/onboarding LLM calls by importing a checked-in artifact
* instead of generating knowledge live during first setup.
*/
import { execFileSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import pg from 'pg';
import {
DB_RUNTIME,
SKILLS_DB_NAME,
SKILLS_DB_URL,
SKILLS_DB_USER,
TENANT_ID,
} from '../src/config.js';
import {
BUILTIN_KNOWLEDGE_ARTIFACT_SQL as ARTIFACT_SQL,
BUILTIN_KNOWLEDGE_BOOTSTRAP_DIR as BOOTSTRAP_DIR,
BUILTIN_KNOWLEDGE_METADATA_JSON as METADATA_JSON,
readBuiltinKnowledgeMetadata as readMetadata,
} from '../src/builtin-knowledge-artifact.js';
import { logger } from '../src/logger.js';
import { readEnvFile } from '../src/env.js';
import { commandExists, getPlatform } from './platform.js';
import { emitStatus } from './status.js';
import { jailExists, jailRoot } from './bastille-helpers.js';
function getSkillsDbUrl(): string {
return SKILLS_DB_URL;
}
function statusReport(
status: string,
extra: Record<string, string | number | boolean> = {},
): void {
const metadata = readMetadata();
emitStatus('SETUP_SKILLS_MEMORY', {
STATUS: status,
PSQL: commandExists('psql'),
ARTIFACT_SQL: fs.existsSync(ARTIFACT_SQL),
METADATA_JSON: fs.existsSync(METADATA_JSON),
BOOTSTRAP_DIR: BOOTSTRAP_DIR,
EMBEDDING_MODEL: String(metadata?.embedding_model ?? 'unknown'),
ARTIFACT_VERSION: String(metadata?.artifact_version ?? 'unknown'),
CHUNKING_VERSION: String(
metadata?.chunker_version ??
(metadata as Record<string, unknown> | undefined)?.[
'chunking_version'
] ??
'unknown',
),
...extra,
LOG: 'logs/setup.log',
});
}
function jailRootLocal(jailName: string): string {
return `/usr/local/bastille/jails/${jailName}/root`;
}
function detectDbJailName(): string {
const envOverrides = readEnvFile(['DB_JAIL_NAME']);
const explicit = (
process.env.DB_JAIL_NAME ||
envOverrides.DB_JAIL_NAME ||
''
).trim();
if (explicit) return explicit;
const safeAgentName = TENANT_ID.replace(/[-_]/g, '');
const preferred = `${safeAgentName}db`;
const legacyHyphen = `${TENANT_ID}-db`;
const legacy = 'db';
if (jailExists(preferred)) return preferred;
if (jailExists(legacyHyphen)) return legacyHyphen;
if (jailExists(legacy)) return legacy;
return preferred;
}
function importArtifactViaBastille(jailName: string, dbName: string): void {
const projectRoot = process.cwd();
const tmpDir = path.join(projectRoot, 'tmp');
fs.mkdirSync(tmpDir, { recursive: true });
const stamp = Date.now();
const hostTmp = path.join(tmpDir, `skills-artifact-${stamp}.sql`);
fs.copyFileSync(ARTIFACT_SQL, hostTmp);
const jailTmpRel = path.join('var', 'tmp', `skills-artifact-${stamp}.sql`);
const jailTmpHostPath = path.join(jailRootLocal(jailName), jailTmpRel);
fs.mkdirSync(path.dirname(jailTmpHostPath), { recursive: true });
fs.copyFileSync(hostTmp, jailTmpHostPath);
try {
logger.info(
{ artifact: ARTIFACT_SQL, jailName },
'Importing built-in knowledge artifact',
);
execFileSync(
'bastille',
[
'cmd',
jailName,
'su',
'-m',
'postgres',
'-c',
`psql -v ON_ERROR_STOP=1 -d ${dbName} -f /${jailTmpRel}`,
],
{ stdio: 'inherit' },
);
} finally {
fs.rmSync(hostTmp, { force: true });
fs.rmSync(jailTmpHostPath, { force: true });
}
}
function grantSkillsReadAccess(
jailName: string,
dbName: string,
dbUser: string,
): void {
const grantSql = [
`GRANT USAGE ON SCHEMA public TO ${dbUser};`,
`GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${dbUser};`,
`GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO ${dbUser};`,
`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${dbUser};`,
`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON SEQUENCES TO ${dbUser};`,
].join(' ');
execFileSync(
'bastille',
[
'cmd',
jailName,
'su',
'-m',
'postgres',
'-c',
`psql -v ON_ERROR_STOP=1 -d ${dbName} -c ${JSON.stringify(grantSql)}`,
],
{ stdio: 'inherit' },
);
}
export function importArtifact(): void {
if (!fs.existsSync(ARTIFACT_SQL)) {
throw new Error('missing_skills_memory_artifact');
}
if (
DB_RUNTIME !== 'host' &&
getPlatform() === 'freebsd' &&
commandExists('bastille')
) {
const jailName = detectDbJailName();
importArtifactViaBastille(jailName, SKILLS_DB_NAME);
grantSkillsReadAccess(jailName, SKILLS_DB_NAME, SKILLS_DB_USER);
return;
}
if (!commandExists('psql')) {
throw new Error('missing_psql');
}
const dbUrl = getSkillsDbUrl();
logger.info(
{ artifact: ARTIFACT_SQL },
'Importing built-in knowledge artifact',
);
execFileSync('psql', ['-v', 'ON_ERROR_STOP=1', dbUrl, '-f', ARTIFACT_SQL], {
stdio: 'inherit',
});
}
async function verifyImport(): Promise<Record<string, number>> {
const { Client } = pg;
const client = new Client({
connectionString: getSkillsDbUrl(),
connectionTimeoutMillis: 5000,
});
await client.connect();
try {
const [artifacts, documents, chunks] = await Promise.all([
client.query<{ count: string }>(
'SELECT count(*) FROM builtin_knowledge_artifacts',
),
client.query<{ count: string }>(
'SELECT count(*) FROM builtin_knowledge_documents',
),
client.query<{ count: string }>(
'SELECT count(*) FROM builtin_knowledge_chunks',
),
]);
return {
artifacts: Number(artifacts.rows[0]?.count ?? 0),
documents: Number(documents.rows[0]?.count ?? 0),
chunks: Number(chunks.rows[0]?.count ?? 0),
};
} finally {
await client.end();
}
}
export async function run(args: string[]): Promise<void> {
const doImport = args.includes('--import');
const doStatus = args.includes('--status') || !doImport;
if (doStatus && !doImport) {
let counts: Record<string, number> = {};
try {
counts = await verifyImport();
} catch {
counts = {};
}
statusReport('ready', {
ARTIFACT_ROWS: counts.artifacts ?? 0,
DOCUMENT_ROWS: counts.documents ?? 0,
CHUNK_ROWS: counts.chunks ?? 0,
});
return;
}
importArtifact();
const counts = await verifyImport();
statusReport('imported', {
DB_URL_TARGET: getSkillsDbUrl(),
ARTIFACT_ROWS: counts.artifacts,
DOCUMENT_ROWS: counts.documents,
CHUNK_ROWS: counts.chunks,
});
}