Move prefix handling from register.ts to router.ts. Users register with raw platform IDs (what they naturally have), adapters send prefixed IDs (their internal format). Router now tries stripping the channel type prefix when the exact lookup fails, matching either format. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
251 lines
8.0 KiB
TypeScript
251 lines
8.0 KiB
TypeScript
/**
|
|
* Step: register — Create v2 entities (agent group, messaging group, wiring).
|
|
*
|
|
* Writes to the v2 central DB (data/v2.db) — NOT the v1 store/messages.db.
|
|
* Creates: agent_group, messaging_group, messaging_group_agents.
|
|
*/
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import { DATA_DIR } from '../src/config.js';
|
|
import { initDb } from '../src/db/connection.js';
|
|
import { runMigrations } from '../src/db/migrations/index.js';
|
|
import { createAgentGroup, getAgentGroupByFolder } from '../src/db/agent-groups.js';
|
|
import {
|
|
createMessagingGroup,
|
|
createMessagingGroupAgent,
|
|
getMessagingGroupByPlatform,
|
|
getMessagingGroupAgentByPair,
|
|
} from '../src/db/messaging-groups.js';
|
|
import { isValidGroupFolder } from '../src/group-folder.js';
|
|
import { log } from '../src/log.js';
|
|
import { emitStatus } from './status.js';
|
|
|
|
interface RegisterArgs {
|
|
/** Platform-specific channel/group ID (Discord channel ID, Slack channel, etc.) */
|
|
platformId: string;
|
|
/** Human-readable name for the messaging group */
|
|
name: string;
|
|
/** Trigger pattern (regex or keyword) */
|
|
trigger: string;
|
|
/** Agent group folder name */
|
|
folder: string;
|
|
/** Channel type (discord, slack, telegram, etc.) */
|
|
channel: string;
|
|
/** Whether messages require the trigger pattern to activate */
|
|
requiresTrigger: boolean;
|
|
/** Whether this is the admin/main agent group */
|
|
isMain: boolean;
|
|
/** Display name for the assistant */
|
|
assistantName: string;
|
|
/** Session mode: 'shared' (one session per channel) or 'per-thread' */
|
|
sessionMode: string;
|
|
}
|
|
|
|
function parseArgs(args: string[]): RegisterArgs {
|
|
const result: RegisterArgs = {
|
|
platformId: '',
|
|
name: '',
|
|
trigger: '',
|
|
folder: '',
|
|
channel: 'discord',
|
|
requiresTrigger: true,
|
|
isMain: false,
|
|
assistantName: 'Andy',
|
|
sessionMode: 'shared',
|
|
};
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
switch (args[i]) {
|
|
// Accept both --jid (v1 compat) and --platform-id (v2)
|
|
case '--jid':
|
|
case '--platform-id':
|
|
result.platformId = args[++i] || '';
|
|
break;
|
|
case '--name':
|
|
result.name = args[++i] || '';
|
|
break;
|
|
case '--trigger':
|
|
result.trigger = args[++i] || '';
|
|
break;
|
|
case '--folder':
|
|
result.folder = args[++i] || '';
|
|
break;
|
|
case '--channel':
|
|
result.channel = (args[++i] || '').toLowerCase();
|
|
break;
|
|
case '--no-trigger-required':
|
|
result.requiresTrigger = false;
|
|
break;
|
|
case '--is-main':
|
|
result.isMain = true;
|
|
break;
|
|
case '--assistant-name':
|
|
result.assistantName = args[++i] || 'Andy';
|
|
break;
|
|
case '--session-mode':
|
|
result.sessionMode = args[++i] || 'shared';
|
|
break;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function generateId(prefix: string): string {
|
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
}
|
|
|
|
export async function run(args: string[]): Promise<void> {
|
|
const projectRoot = process.cwd();
|
|
const parsed = parseArgs(args);
|
|
|
|
if (!parsed.platformId || !parsed.name || !parsed.folder) {
|
|
emitStatus('REGISTER_CHANNEL', {
|
|
STATUS: 'failed',
|
|
ERROR: 'missing_required_args',
|
|
LOG: 'logs/setup.log',
|
|
});
|
|
process.exit(4);
|
|
}
|
|
|
|
if (!isValidGroupFolder(parsed.folder)) {
|
|
emitStatus('REGISTER_CHANNEL', {
|
|
STATUS: 'failed',
|
|
ERROR: 'invalid_folder',
|
|
LOG: 'logs/setup.log',
|
|
});
|
|
process.exit(4);
|
|
}
|
|
|
|
log.info('Registering channel', parsed);
|
|
|
|
// Init v2 central DB
|
|
fs.mkdirSync(path.join(projectRoot, 'data'), { recursive: true });
|
|
const dbPath = path.join(DATA_DIR, 'v2.db');
|
|
const db = initDb(dbPath);
|
|
runMigrations(db);
|
|
|
|
// 1. Create or find agent group
|
|
let agentGroup = getAgentGroupByFolder(parsed.folder);
|
|
if (!agentGroup) {
|
|
const agId = generateId('ag');
|
|
createAgentGroup({
|
|
id: agId,
|
|
name: parsed.assistantName,
|
|
folder: parsed.folder,
|
|
is_admin: parsed.isMain ? 1 : 0,
|
|
agent_provider: null,
|
|
container_config: null,
|
|
created_at: new Date().toISOString(),
|
|
});
|
|
agentGroup = getAgentGroupByFolder(parsed.folder)!;
|
|
log.info('Created agent group', { id: agId, folder: parsed.folder });
|
|
}
|
|
|
|
// 2. Create or find messaging group
|
|
let messagingGroup = getMessagingGroupByPlatform(parsed.channel, parsed.platformId);
|
|
if (!messagingGroup) {
|
|
const mgId = generateId('mg');
|
|
createMessagingGroup({
|
|
id: mgId,
|
|
channel_type: parsed.channel,
|
|
platform_id: parsed.platformId,
|
|
name: parsed.name,
|
|
is_group: 1,
|
|
admin_user_id: null,
|
|
created_at: new Date().toISOString(),
|
|
});
|
|
messagingGroup = getMessagingGroupByPlatform(parsed.channel, parsed.platformId)!;
|
|
log.info('Created messaging group', { id: mgId, channel: parsed.channel, platformId: parsed.platformId });
|
|
}
|
|
|
|
// 3. Wire agent to messaging group
|
|
const existing = getMessagingGroupAgentByPair(messagingGroup.id, agentGroup.id);
|
|
if (!existing) {
|
|
const mgaId = generateId('mga');
|
|
const triggerRules = parsed.trigger
|
|
? JSON.stringify({
|
|
pattern: parsed.trigger,
|
|
requiresTrigger: parsed.requiresTrigger,
|
|
})
|
|
: null;
|
|
createMessagingGroupAgent({
|
|
id: mgaId,
|
|
messaging_group_id: messagingGroup.id,
|
|
agent_group_id: agentGroup.id,
|
|
trigger_rules: triggerRules,
|
|
response_scope: 'all',
|
|
session_mode: parsed.sessionMode,
|
|
priority: parsed.isMain ? 10 : 0,
|
|
created_at: new Date().toISOString(),
|
|
});
|
|
log.info('Wired agent to messaging group', { mgaId, agentGroup: agentGroup.id, messagingGroup: messagingGroup.id });
|
|
}
|
|
|
|
// 4. Create group folders
|
|
fs.mkdirSync(path.join(projectRoot, 'groups', parsed.folder, 'logs'), { recursive: true });
|
|
|
|
// Create CLAUDE.md from template if it doesn't exist
|
|
const groupClaudeMdPath = path.join(projectRoot, 'groups', parsed.folder, 'CLAUDE.md');
|
|
if (!fs.existsSync(groupClaudeMdPath)) {
|
|
const templatePath = parsed.isMain
|
|
? path.join(projectRoot, 'groups', 'main', 'CLAUDE.md')
|
|
: path.join(projectRoot, 'groups', 'global', 'CLAUDE.md');
|
|
if (fs.existsSync(templatePath)) {
|
|
fs.copyFileSync(templatePath, groupClaudeMdPath);
|
|
log.info('Created CLAUDE.md from template', { file: groupClaudeMdPath, template: templatePath });
|
|
}
|
|
}
|
|
|
|
// 5. Update assistant name in CLAUDE.md files if different from default
|
|
let nameUpdated = false;
|
|
if (parsed.assistantName !== 'Andy') {
|
|
log.info('Updating assistant name', { from: 'Andy', to: parsed.assistantName });
|
|
|
|
const groupsDir = path.join(projectRoot, 'groups');
|
|
const mdFiles = fs
|
|
.readdirSync(groupsDir)
|
|
.map((d) => path.join(groupsDir, d, 'CLAUDE.md'))
|
|
.filter((f) => fs.existsSync(f));
|
|
|
|
for (const mdFile of mdFiles) {
|
|
let content = fs.readFileSync(mdFile, 'utf-8');
|
|
content = content.replace(/^# Andy$/m, `# ${parsed.assistantName}`);
|
|
content = content.replace(/You are Andy/g, `You are ${parsed.assistantName}`);
|
|
fs.writeFileSync(mdFile, content);
|
|
log.info('Updated CLAUDE.md', { file: mdFile });
|
|
}
|
|
|
|
// Update .env
|
|
const envFile = path.join(projectRoot, '.env');
|
|
if (fs.existsSync(envFile)) {
|
|
let envContent = fs.readFileSync(envFile, 'utf-8');
|
|
if (envContent.includes('ASSISTANT_NAME=')) {
|
|
envContent = envContent.replace(/^ASSISTANT_NAME=.*$/m, `ASSISTANT_NAME="${parsed.assistantName}"`);
|
|
} else {
|
|
envContent += `\nASSISTANT_NAME="${parsed.assistantName}"`;
|
|
}
|
|
fs.writeFileSync(envFile, envContent);
|
|
} else {
|
|
fs.writeFileSync(envFile, `ASSISTANT_NAME="${parsed.assistantName}"\n`);
|
|
}
|
|
log.info('Set ASSISTANT_NAME in .env');
|
|
nameUpdated = true;
|
|
}
|
|
|
|
emitStatus('REGISTER_CHANNEL', {
|
|
PLATFORM_ID: parsed.platformId,
|
|
NAME: parsed.name,
|
|
FOLDER: parsed.folder,
|
|
CHANNEL: parsed.channel,
|
|
TRIGGER: parsed.trigger,
|
|
REQUIRES_TRIGGER: parsed.requiresTrigger,
|
|
ASSISTANT_NAME: parsed.assistantName,
|
|
SESSION_MODE: parsed.sessionMode,
|
|
NAME_UPDATED: nameUpdated,
|
|
STATUS: 'success',
|
|
LOG: 'logs/setup.log',
|
|
});
|
|
}
|