clawdie-ai/setup/skills-memory.ts
Clawdie AI 8456fcc526 test: expand coverage + fix setup/ TypeScript errors
New test files (83 tests):
- src/agent-identity.test.ts — resolveAgentIdentity across locales/genders
- src/env.test.ts — readEnvFile parsing, quoting, edge cases
- src/jail-registry.test.ts — getJailIp with/without env override
- src/local-hosts.test.ts — block markers, entries, render, upsert
- src/mount-security.test.ts — validateMount allowlist enforcement
- src/transcription.test.ts — initTranscription + transcribeAudio with mocked OpenAI

setup/ TypeScript audit (tsconfig.setup.json):
- agent-jails: JAILS value serialised to JSON string for emitStatus
- environment.test: use import type for pg.Pool type cast
- onboarding: wrap showProfileMenu in normalizePiTuiProfile
- preflight.test: fix process.exit mock type + typed call array casts
- sanoid: execSync → spawnSync for multi-arg zfs invocation
- skills-memory: bracket access for legacy chunking_version field
- upstream: pass process.cwd() to isGitRepo()
- verify: import StripeKeyMode type, annotate stripeKeyMode variable

Full suite: 69 files, 1162 tests passing; tsc --noEmit clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---
Build: pass | Tests: pass — Tests  1162 passed (1162)

---
Build: pass | Tests: pass — Tests  1162 passed (1162)
2026-04-14 09:40:28 +00:00

286 lines
7.7 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 {
AGENT_NAME,
SKILLS_DB_NAME,
SKILLS_DB_URL,
SKILLS_DB_USER,
} 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 = AGENT_NAME.replace(/[-_]/g, '');
const preferred = `${safeAgentName}db`;
const legacyHyphen = `${AGENT_NAME}-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' },
);
}
function applyPermissionsSql(
jailName: string,
dbName: string,
dbUser: string,
): void {
const projectRoot = process.cwd();
const permissionsSql = path.join(
projectRoot,
'docs',
'internal',
'sql',
'permissions.sql',
);
if (!fs.existsSync(permissionsSql)) {
logger.warn(
{ permissions: permissionsSql },
'permissions.sql not found, skipping',
);
return;
}
const tmpDir = path.join(projectRoot, 'tmp');
fs.mkdirSync(tmpDir, { recursive: true });
const stamp = Date.now();
const hostTmp = path.join(tmpDir, `permissions-${stamp}.sql`);
fs.copyFileSync(permissionsSql, hostTmp);
const jailTmpRel = path.join('var', 'tmp', `permissions-${stamp}.sql`);
const jailTmpHostPath = path.join(jailRootLocal(jailName), jailTmpRel);
fs.mkdirSync(path.dirname(jailTmpHostPath), { recursive: true });
fs.copyFileSync(hostTmp, jailTmpHostPath);
try {
logger.info(
{ permissions: permissionsSql, jailName, dbUser },
'Applying permissions.sql',
);
execFileSync(
'bastille',
[
'cmd',
jailName,
'su',
'-m',
'postgres',
'-c',
`psql -v db_user=${dbUser} -d ${dbName} -f /${jailTmpRel}`,
],
{ stdio: 'inherit' },
);
} finally {
fs.rmSync(hostTmp, { force: true });
fs.rmSync(jailTmpHostPath, { force: true });
}
}
export function importArtifact(): void {
if (!fs.existsSync(ARTIFACT_SQL)) {
throw new Error('missing_skills_memory_artifact');
}
if (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,
});
}