Terminology overhaul for control plane naming:
- AgentRole 'ceo' → 'orchestrator', schema constraint updated
- DEFAULT_AGENTS → getDefaultAgents(agentName) function, main agent id = AGENT_NAME
- Identity file resolution: {AGENT_NAME}.md with SOUL.md fallback
- All test mocks updated: 'ceo' → 'clawdie', 'company' → 'system'
- SOUL.md + SYSADMIN.md docs updated (Paperclip→Control Plane)
- TypeScript build clean (tsc --noEmit passes)
Remaining: DBA.md, GIT_ADMIN.md, doc/* updates, CLAWDIE.md creation
See doc/NAMING-HANDOFF.md for task checklist (Sam & Claude)
169 lines
6.5 KiB
TypeScript
169 lines
6.5 KiB
TypeScript
/**
|
|
* setup/controlplane.ts
|
|
*
|
|
* Initialises the Clawdie control plane:
|
|
* 1. Creates PostgreSQL tables (idempotent — IF NOT EXISTS)
|
|
* 2. Provisions 4 default agents (CEO, Sysadmin, DBA, Git Admin)
|
|
* 3. Creates the operator account
|
|
* 4. Seeds per-agent budgets
|
|
* 5. Copies operational skills → data/skills/
|
|
*
|
|
* Safe to re-run: all writes use ON CONFLICT DO NOTHING / DO UPDATE.
|
|
*/
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import pg from 'pg';
|
|
|
|
import { AGENT_NAME, MEMORY_DB_URL } from '../src/config.js';
|
|
import {
|
|
getDefaultAgents,
|
|
copySkills,
|
|
generatePassword,
|
|
getAgents,
|
|
getOperator,
|
|
runSchemaMigration,
|
|
upsertAgent,
|
|
upsertBudget,
|
|
upsertOperator,
|
|
} from '../src/controlplane-db.js';
|
|
import { logger } from '../src/logger.js';
|
|
import { emitStatus } from './status.js';
|
|
|
|
const LOG = 'logs/setup.log';
|
|
|
|
function appendSetupLog(line: string): void {
|
|
try {
|
|
fs.mkdirSync('logs', { recursive: true });
|
|
fs.appendFileSync(LOG, `[${new Date().toISOString()}] ${line}\n`);
|
|
} catch {
|
|
// log file is best-effort; never block setup on it
|
|
}
|
|
}
|
|
|
|
export async function run(_args: string[]): Promise<void> {
|
|
appendSetupLog('=== controlplane setup start ===');
|
|
const dbUrl = MEMORY_DB_URL;
|
|
if (!dbUrl) {
|
|
emitStatus('SETUP_CONTROLPLANE', {
|
|
STATUS: 'failed',
|
|
ERROR: 'MEMORY_DB_URL not set — cannot connect to PostgreSQL',
|
|
LOG,
|
|
});
|
|
process.exit(1);
|
|
}
|
|
|
|
const pool = new pg.Pool({ connectionString: dbUrl, max: 3 });
|
|
|
|
try {
|
|
// ── 1. Schema migration ──────────────────────────────────────────
|
|
logger.info('Running control plane schema migration...');
|
|
appendSetupLog('schema migration: starting');
|
|
await runSchemaMigration(pool);
|
|
logger.info('Schema ready');
|
|
appendSetupLog('schema migration: ready');
|
|
|
|
// ── 2. Provision default agents ──────────────────────────────────
|
|
const defaultAgents = getDefaultAgents(AGENT_NAME);
|
|
logger.info('Provisioning default agents...');
|
|
for (const agent of defaultAgents) {
|
|
await upsertAgent(pool, agent);
|
|
logger.info({ id: agent.id, role: agent.role }, 'Agent ready');
|
|
appendSetupLog(`agent ready: ${agent.id} (${agent.role})`);
|
|
}
|
|
|
|
// ── 3. Create operator account ───────────────────────────────────
|
|
const operatorName =
|
|
(process.env.CONTROLPLANE_NAME || AGENT_NAME || 'clawdie').trim();
|
|
|
|
const existing = await getOperator(pool, operatorName);
|
|
let operatorPassword: string;
|
|
|
|
if (existing) {
|
|
logger.info({ username: operatorName }, 'Operator already exists');
|
|
operatorPassword = '(existing — check OPERATOR_PASSWORD in .env)';
|
|
appendSetupLog(`operator exists: ${operatorName}`);
|
|
} else {
|
|
operatorPassword = generatePassword(32);
|
|
await upsertOperator(pool, operatorName, operatorPassword);
|
|
logger.info({ username: operatorName }, 'Operator created');
|
|
appendSetupLog(`operator created: ${operatorName} (password written to .env, not logged)`);
|
|
|
|
// Write password to .env if present
|
|
const envPath = path.join(process.cwd(), '.env');
|
|
if (fs.existsSync(envPath)) {
|
|
let envContent = fs.readFileSync(envPath, 'utf-8');
|
|
if (envContent.includes('OPERATOR_PASSWORD=')) {
|
|
envContent = envContent.replace(
|
|
/^OPERATOR_PASSWORD=.*$/m,
|
|
`OPERATOR_PASSWORD=${operatorPassword}`,
|
|
);
|
|
} else {
|
|
envContent += `\nOPERATOR_PASSWORD=${operatorPassword}\n`;
|
|
}
|
|
fs.writeFileSync(envPath, envContent, { mode: 0o600 });
|
|
logger.info('Wrote OPERATOR_PASSWORD to .env');
|
|
}
|
|
}
|
|
|
|
// ── 4. Seed budgets ──────────────────────────────────────────────
|
|
const dailyTokens = parseInt(
|
|
process.env.CONTROLPLANE_DAILY_TOKENS || '100000',
|
|
10,
|
|
);
|
|
|
|
for (const agent of defaultAgents) {
|
|
const agentTokens = Math.floor((dailyTokens * agent.budget_allocation_pct) / 100);
|
|
await upsertBudget(pool, agent.id, agentTokens);
|
|
logger.info(
|
|
{ agent: agent.id, tokens: agentTokens },
|
|
'Budget seeded',
|
|
);
|
|
}
|
|
|
|
// ── 5. Copy skills ───────────────────────────────────────────────
|
|
const skillsSrc = path.join(process.cwd(), '.agent', 'skills');
|
|
const skillsDest = path.join(process.cwd(), 'data', 'skills');
|
|
|
|
copySkills(skillsSrc, skillsDest);
|
|
const skillCount = fs
|
|
.readdirSync(skillsDest, { withFileTypes: true })
|
|
.filter(
|
|
(e) =>
|
|
e.isDirectory() &&
|
|
fs.existsSync(path.join(skillsDest, e.name, 'SKILL.md')),
|
|
).length;
|
|
logger.info({ count: skillCount, dest: skillsDest }, 'Skills copied');
|
|
|
|
// ── Summary ──────────────────────────────────────────────────────
|
|
const agents = await getAgents(pool);
|
|
|
|
emitStatus('SETUP_CONTROLPLANE', {
|
|
STATUS: 'success',
|
|
AGENTS: agents.map((a) => a.id).join(', '),
|
|
OPERATOR: operatorName,
|
|
OPERATOR_PASSWORD: existing ? '(unchanged)' : '(printed to terminal — see console output, not logged)',
|
|
DAILY_TOKENS: dailyTokens,
|
|
SKILLS_COPIED: skillCount,
|
|
DASHBOARD: `http://localhost:${process.env.CONTROLPLANE_PORT || 3100}/`,
|
|
LOG,
|
|
});
|
|
appendSetupLog(`success: agents=${agents.length} operator=${operatorName} skills=${skillCount}`);
|
|
|
|
console.log('\n✓ Control plane ready');
|
|
console.log(` Agents: ${agents.map((a) => a.id).join(', ')}`);
|
|
console.log(` Operator: ${operatorName}`);
|
|
if (!existing) {
|
|
console.log(` Password: ${operatorPassword} ← save this! (also stored in .env)`);
|
|
console.log(` ⚠ do not redirect/tee this output — password is on stdout only`);
|
|
}
|
|
console.log(` Skills: ${skillCount} copied to data/skills/`);
|
|
console.log(` Dashboard: http://localhost:${process.env.CONTROLPLANE_PORT || 3100}/\n`);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
emitStatus('SETUP_CONTROLPLANE', { STATUS: 'failed', ERROR: message, LOG });
|
|
throw error;
|
|
} finally {
|
|
await pool.end();
|
|
}
|
|
}
|