Files
nanoclaw/container/agent-runner/src/index-v2.ts
T
gavrielc afbc20a6c4 v2 phase 4+5: Discord via Chat SDK, expanded MCP tools, message seq IDs
- Chat SDK bridge + Discord adapter (gateway listener, message routing)
- MCP tools refactored into modular structure: core (send_message, send_file,
  edit_message, add_reaction), scheduling (schedule/list/cancel/pause/resume
  tasks), interactive (ask_user_question, send_card), agents (send_to_agent)
- Message seq IDs: shared integer sequence across messages_in/out so agents
  see small numeric IDs instead of platform snowflakes
- busy_timeout=5000 for session DB (poll loop + MCP server concurrent access)
- Always copy agent-runner source to fix stale cache when non-index files change
- Seed script for Discord testing, e2e test script

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 02:53:39 +03:00

97 lines
2.9 KiB
TypeScript

/**
* NanoClaw Agent Runner v2
*
* Runs inside a container. All IO goes through the session DB.
* No stdin, no stdout markers, no IPC files.
*
* Config:
* - SESSION_DB_PATH: path to session SQLite DB (default: /workspace/session.db)
* - AGENT_PROVIDER: 'claude' | 'mock' (default: claude)
* - NANOCLAW_ASSISTANT_NAME: assistant name for transcript archiving
* - NANOCLAW_ADMIN_USER_ID: admin user ID for permission checks
*
* Mount structure:
* /workspace/
* session.db ← session SQLite DB
* outbox/ ← outbound files
* agent/ ← agent group folder (CLAUDE.md, skills, working files)
* .claude/ ← Claude SDK session data
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { createProvider, type ProviderName } from './providers/factory.js';
import { runPollLoop } from './poll-loop.js';
function log(msg: string): void {
console.error(`[agent-runner] ${msg}`);
}
const CWD = '/workspace/agent';
const GLOBAL_CLAUDE_MD = '/workspace/global/CLAUDE.md';
async function main(): Promise<void> {
const providerName = (process.env.AGENT_PROVIDER || 'claude') as ProviderName;
const assistantName = process.env.NANOCLAW_ASSISTANT_NAME;
log(`Starting v2 agent-runner (provider: ${providerName})`);
const provider = createProvider(providerName, { assistantName });
// Load global CLAUDE.md as additional system context
let systemPrompt: string | undefined;
if (fs.existsSync(GLOBAL_CLAUDE_MD)) {
systemPrompt = fs.readFileSync(GLOBAL_CLAUDE_MD, 'utf-8');
log('Loaded global CLAUDE.md');
}
// Discover additional directories mounted at /workspace/extra/*
const additionalDirectories: string[] = [];
const extraBase = '/workspace/extra';
if (fs.existsSync(extraBase)) {
for (const entry of fs.readdirSync(extraBase)) {
const fullPath = path.join(extraBase, entry);
if (fs.statSync(fullPath).isDirectory()) {
additionalDirectories.push(fullPath);
}
}
if (additionalDirectories.length > 0) {
log(`Additional directories: ${additionalDirectories.join(', ')}`);
}
}
// MCP server path
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const mcpServerPath = path.join(__dirname, 'mcp-tools', 'index.js');
// SDK env
const env: Record<string, string | undefined> = {
...process.env,
CLAUDE_CODE_AUTO_COMPACT_WINDOW: '165000',
};
await runPollLoop({
provider,
cwd: CWD,
mcpServers: {
nanoclaw: {
command: 'node',
args: [mcpServerPath],
env: {
SESSION_DB_PATH: process.env.SESSION_DB_PATH || '/workspace/session.db',
},
},
},
systemPrompt,
env,
additionalDirectories: additionalDirectories.length > 0 ? additionalDirectories : undefined,
});
}
main().catch((err) => {
log(`Fatal error: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
});