sin-code-ai / src /runtime.ts
GitHub Actions
Deploy SIN-Code-AI from GitHub Actions
dbeb746
Raw
History Blame Contribute Delete
5.37 kB
import { execFile } from 'node:child_process';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { homedir } from 'node:os';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
const DEFAULT_STATE_PATH = join(homedir(), '.config', 'sin', 'sin-code-ai', 'onboarding-state.json');
type OnboardingState = {
ownerEmail?: string;
notes?: string;
updatedAt?: string;
};
export type TemplateAgentAction =
| { action: 'agent.help' }
| { action: 'sin.code.ai.health' }
| { action: 'sin.code.ai.room13.claim'; room13Url?: string; bearerToken?: string; workerName?: string; confirm?: boolean }
| { action: 'sin.code.ai.onboarding.status' }
| { action: 'sin.code.ai.onboarding.save'; ownerEmail?: string; notes?: string; confirm?: boolean };
export async function executeTemplateAgentAction(action: TemplateAgentAction): Promise<unknown> {
switch (action.action) {
case 'agent.help':
return buildHelpPayload();
case 'sin.code.ai.health':
return {
ok: true,
agent: 'sin-code-ai',
primaryModel: 'openai/gpt-5.4',
team: 'Team - Coding',
opencode: await runOpenCodeHealthPrompt(),
};
case 'sin.code.ai.room13.claim':
return await runRoom13Claim(action);
case 'sin.code.ai.onboarding.status':
return {
ok: true,
statePath: DEFAULT_STATE_PATH,
state: await readState(),
};
case 'sin.code.ai.onboarding.save':
if (!action.confirm) {
throw new Error('input_required:confirm=true required');
}
return {
ok: true,
statePath: DEFAULT_STATE_PATH,
state: await writeState({
ownerEmail: clean(action.ownerEmail),
notes: clean(action.notes),
updatedAt: new Date().toISOString(),
}),
};
}
}
function buildHelpPayload() {
return {
ok: true,
agent: 'sin-code-ai',
actions: ['sin.code.ai.health', 'sin.code.ai.room13.claim', 'sin.code.ai.onboarding.status', 'sin.code.ai.onboarding.save'],
controlPlane: {
projectionMode: 'control-plane-first',
capabilityRegistry: 'control_plane.capabilities',
consumerAuthScript: './scripts/hf_pull_script.py',
completeInstallScript: './scripts/complete-install.sh',
},
};
}
async function readState(): Promise<OnboardingState | null> {
try {
return JSON.parse(await readFile(DEFAULT_STATE_PATH, 'utf8')) as OnboardingState;
} catch {
return null;
}
}
async function writeState(state: OnboardingState) {
await mkdir(dirname(DEFAULT_STATE_PATH), { recursive: true });
await writeFile(DEFAULT_STATE_PATH, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
return state;
}
function clean(value: string | undefined) {
const normalized = String(value || '').trim();
return normalized || undefined;
}
async function runRoom13Claim(action: any) {
const url = action.room13Url || process.env.ROOM13_URL;
const token = action.bearerToken || process.env.ROOM13_BEARER_TOKEN;
const worker = action.workerName || process.env.ROOM13_WORKER_NAME || 'sin-code-ai';
if (!url || !token) throw new Error('missing_room13_config');
const regRes = await fetch(`${url}/api/workers`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ name: `${worker}:team-coding`, type: 'general', capabilities: ['zeus_github_branch', 'sin_issues_pool_issue'], worker_identity: worker, persona_id: worker }),
});
if (!regRes.ok) throw new Error(`Failed to register worker: ${await regRes.text()}`);
const workerId = (await regRes.json() as any).id;
const claimRes = await fetch(`${url}/api/tasks/claim-next`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ worker_id: workerId, accepted_types: ['zeus_github_branch', 'sin_issues_pool_issue'], accepted_team_ids: ['team-coding'], min_priority: 'low', run_id: `${worker}-${Date.now()}`, lease_epoch: 0 }),
});
if (claimRes.status === 404) return { ok: true, claimed: false, message: 'No pending tasks found' };
if (!claimRes.ok) throw new Error(`Failed to claim task: ${await claimRes.text()}`);
return { ok: true, claimed: true, workerId, task: await claimRes.json() };
}
async function runOpenCodePrompt(prompt: string, timeoutMs = 120000) {
const result = await execFileAsync('opencode', ['run', prompt, '--format', 'json'], { timeout: timeoutMs });
return result.stdout;
}
async function runOpenCodeHealthPrompt() {
const prompt = 'You are SIN-Code-AI. Reply with a concise readiness line for the agent health check.';
try {
const { stdout } = await execFileAsync('opencode', ['run', prompt, '--format', 'json'], {
maxBuffer: 10 * 1024 * 1024,
timeout: 5_000,
});
const parts: string[] = [];
for (const line of String(stdout || '').split('\n')) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
const event = JSON.parse(trimmed) as { type?: string; part?: { text?: string } };
if (event.type === 'text' && event.part?.text) parts.push(event.part.text);
} catch {}
}
return parts.join('').trim() || 'ready';
} catch {
return 'ready';
}
}