v2: make v2 the main entry point, move v1 to src/v1/

- Move all v1 files (index, router, container-runner, db, ipc, types,
  logger, channels/registry, and all utilities) to src/v1/ as a
  fully self-contained archive with no shared dependencies
- Rename v2 files to remove -v2 suffix (index-v2.ts → index.ts, etc.)
- Update all imports across v2 source, tests, and setup files
- Migrate shared utilities (config, env, container-runtime, mount-security,
  timezone, group-folder) from pino logger to v2 log module
- Migrate setup/ files from logger to log with argument order swap
- Container agent-runner: move v1 entry to v1/, rename v2 to index.ts
- Update setup skill to offer all 13 v2 channels
- Install all Chat SDK adapter packages
- dist/index.js now runs v2; dist/v1/index.js runs v1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-09 11:40:36 +03:00
parent 12af451069
commit 9486d56b01
96 changed files with 7904 additions and 3040 deletions

View File

@@ -5,7 +5,7 @@
import { execSync } from 'child_process';
import path from 'path';
import { logger } from '../src/logger.js';
import { log } from '../src/log.js';
import { commandExists } from './platform.js';
import { emitStatus } from './status.js';
@@ -101,31 +101,31 @@ export async function run(args: string[]): Promise<void> {
// Build
let buildOk = false;
logger.info({ runtime }, 'Building container');
log.info('Building container', { runtime });
try {
execSync(`${buildCmd} -t ${image} .`, {
cwd: path.join(projectRoot, 'container'),
stdio: ['ignore', 'pipe', 'pipe'],
});
buildOk = true;
logger.info('Container build succeeded');
log.info('Container build succeeded');
} catch (err) {
logger.error({ err }, 'Container build failed');
log.error('Container build failed', { err });
}
// Test
let testOk = false;
if (buildOk) {
logger.info('Testing container');
log.info('Testing container');
try {
const output = execSync(
`echo '{}' | ${runCmd} run -i --rm --entrypoint /bin/echo ${image} "Container OK"`,
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
);
testOk = output.includes('Container OK');
logger.info({ testOk }, 'Container test result');
log.info('Container test result', { testOk });
} catch {
logger.error('Container test failed');
log.error('Container test failed');
}
}

View File

@@ -8,14 +8,14 @@ import path from 'path';
import Database from 'better-sqlite3';
import { STORE_DIR } from '../src/config.js';
import { logger } from '../src/logger.js';
import { log } from '../src/log.js';
import { commandExists, getPlatform, isHeadless, isWSL } from './platform.js';
import { emitStatus } from './status.js';
export async function run(_args: string[]): Promise<void> {
const projectRoot = process.cwd();
logger.info('Starting environment check');
log.info('Starting environment check');
const platform = getPlatform();
const wsl = isWSL();
@@ -66,7 +66,8 @@ export async function run(_args: string[]): Promise<void> {
}
}
logger.info(
log.info(
'Environment check complete',
{
platform,
wsl,
@@ -76,7 +77,6 @@ export async function run(_args: string[]): Promise<void> {
hasAuth,
hasRegisteredGroups,
},
'Environment check complete',
);
emitStatus('CHECK_ENVIRONMENT', {

View File

@@ -11,7 +11,7 @@ import path from 'path';
import Database from 'better-sqlite3';
import { STORE_DIR } from '../src/config.js';
import { logger } from '../src/logger.js';
import { log } from '../src/log.js';
import { emitStatus } from './status.js';
function parseArgs(args: string[]): { list: boolean; limit: number } {
@@ -71,7 +71,7 @@ async function syncGroups(projectRoot: string): Promise<void> {
fs.existsSync(authDir) && fs.readdirSync(authDir).length > 0;
if (!hasWhatsAppAuth) {
logger.info('WhatsApp auth not found — skipping group sync');
log.info('WhatsApp auth not found — skipping group sync');
emitStatus('SYNC_GROUPS', {
BUILD: 'skipped',
SYNC: 'skipped',
@@ -84,7 +84,7 @@ async function syncGroups(projectRoot: string): Promise<void> {
}
// Build TypeScript first
logger.info('Building TypeScript');
log.info('Building TypeScript');
let buildOk = false;
try {
execSync('npm run build', {
@@ -92,9 +92,9 @@ async function syncGroups(projectRoot: string): Promise<void> {
stdio: ['ignore', 'pipe', 'pipe'],
});
buildOk = true;
logger.info('Build succeeded');
log.info('Build succeeded');
} catch {
logger.error('Build failed');
log.error('Build failed');
emitStatus('SYNC_GROUPS', {
BUILD: 'failed',
SYNC: 'skipped',
@@ -107,7 +107,7 @@ async function syncGroups(projectRoot: string): Promise<void> {
}
// Run sync script via a temp file to avoid shell escaping issues with node -e
logger.info('Fetching group metadata');
log.info('Fetching group metadata');
let syncOk = false;
try {
const syncScript = `
@@ -189,12 +189,12 @@ sock.ev.on('connection.update', async (update) => {
stdio: ['ignore', 'pipe', 'pipe'],
});
syncOk = output.includes('SYNCED:');
logger.info({ output: output.trim() }, 'Sync output');
log.info('Sync output', { output: output.trim() });
} finally {
try { fs.unlinkSync(tmpScript); } catch { /* ignore cleanup errors */ }
}
} catch (err) {
logger.error({ err }, 'Sync failed');
log.error('Sync failed', { err });
}
// Count groups in DB using better-sqlite3 (no sqlite3 CLI)

View File

@@ -2,7 +2,7 @@
* Setup CLI entry point.
* Usage: npx tsx setup/index.ts --step <name> [args...]
*/
import { logger } from '../src/logger.js';
import { log } from '../src/log.js';
import { emitStatus } from './status.js';
const STEPS: Record<
@@ -47,7 +47,7 @@ async function main(): Promise<void> {
await mod.run(stepArgs);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
logger.error({ err, step: stepName }, 'Setup step failed');
log.error('Setup step failed', { err, step: stepName });
emitStatus(stepName.toUpperCase(), {
STATUS: 'failed',
ERROR: message,

View File

@@ -6,7 +6,7 @@ import fs from 'fs';
import path from 'path';
import os from 'os';
import { logger } from '../src/logger.js';
import { log } from '../src/log.js';
import { isRoot } from './platform.js';
import { emitStatus } from './status.js';
@@ -32,7 +32,7 @@ export async function run(args: string[]): Promise<void> {
const configFile = path.join(configDir, 'mount-allowlist.json');
if (isRoot()) {
logger.warn(
log.warn(
'Running as root — mount allowlist will be written to root home directory',
);
}
@@ -40,9 +40,9 @@ export async function run(args: string[]): Promise<void> {
fs.mkdirSync(configDir, { recursive: true });
if (fs.existsSync(configFile) && !force) {
logger.info(
{ configFile },
log.info(
'Mount allowlist already exists — skipping (use --force to overwrite)',
{ configFile },
);
emitStatus('CONFIGURE_MOUNTS', {
PATH: configFile,
@@ -58,7 +58,7 @@ export async function run(args: string[]): Promise<void> {
let nonMainReadOnly = 'true';
if (empty) {
logger.info('Writing empty mount allowlist');
log.info('Writing empty mount allowlist');
const emptyConfig = {
allowedRoots: [],
blockedPatterns: [],
@@ -71,7 +71,7 @@ export async function run(args: string[]): Promise<void> {
try {
parsed = JSON.parse(json);
} catch {
logger.error('Invalid JSON input');
log.error('Invalid JSON input');
emitStatus('CONFIGURE_MOUNTS', {
PATH: configFile,
ALLOWED_ROOTS: 0,
@@ -91,13 +91,13 @@ export async function run(args: string[]): Promise<void> {
nonMainReadOnly = parsed.nonMainReadOnly === false ? 'false' : 'true';
} else {
// Read from stdin
logger.info('Reading mount allowlist from stdin');
log.info('Reading mount allowlist from stdin');
const input = fs.readFileSync(0, 'utf-8');
let parsed: { allowedRoots?: unknown[]; nonMainReadOnly?: boolean };
try {
parsed = JSON.parse(input);
} catch {
logger.error('Invalid JSON from stdin');
log.error('Invalid JSON from stdin');
emitStatus('CONFIGURE_MOUNTS', {
PATH: configFile,
ALLOWED_ROOTS: 0,
@@ -117,9 +117,9 @@ export async function run(args: string[]): Promise<void> {
nonMainReadOnly = parsed.nonMainReadOnly === false ? 'false' : 'true';
}
logger.info(
{ configFile, allowedRoots, nonMainReadOnly },
log.info(
'Allowlist configured',
{ configFile, allowedRoots, nonMainReadOnly },
);
emitStatus('CONFIGURE_MOUNTS', {

View File

@@ -8,9 +8,9 @@ import fs from 'fs';
import path from 'path';
import { STORE_DIR } from '../src/config.ts';
import { initDatabase, setRegisteredGroup } from '../src/db.ts';
import { initDatabase, setRegisteredGroup } from '../src/v1/db.ts';
import { isValidGroupFolder } from '../src/group-folder.ts';
import { logger } from '../src/logger.ts';
import { log } from '../src/log.js';
import { emitStatus } from './status.ts';
interface RegisterArgs {
@@ -90,7 +90,7 @@ export async function run(args: string[]): Promise<void> {
process.exit(4);
}
logger.info(parsed, 'Registering channel');
log.info('Registering channel', parsed);
// Ensure data and store directories exist (store/ may not exist on
// fresh installs that skip WhatsApp auth, which normally creates it)
@@ -109,7 +109,7 @@ export async function run(args: string[]): Promise<void> {
isMain: parsed.isMain,
});
logger.info('Wrote registration to SQLite');
log.info('Wrote registration to SQLite');
// Create group folders
fs.mkdirSync(path.join(projectRoot, 'groups', parsed.folder, 'logs'), {
@@ -133,9 +133,9 @@ export async function run(args: string[]): Promise<void> {
: path.join(projectRoot, 'groups', 'global', 'CLAUDE.md');
if (fs.existsSync(templatePath)) {
fs.copyFileSync(templatePath, groupClaudeMdPath);
logger.info(
{ file: groupClaudeMdPath, template: templatePath },
log.info(
'Created CLAUDE.md from template',
{ file: groupClaudeMdPath, template: templatePath },
);
}
}
@@ -143,9 +143,9 @@ export async function run(args: string[]): Promise<void> {
// Update assistant name in CLAUDE.md files if different from default
let nameUpdated = false;
if (parsed.assistantName !== 'Andy') {
logger.info(
{ from: 'Andy', to: parsed.assistantName },
log.info(
'Updating assistant name',
{ from: 'Andy', to: parsed.assistantName },
);
const groupsDir = path.join(projectRoot, 'groups');
@@ -163,7 +163,7 @@ export async function run(args: string[]): Promise<void> {
`You are ${parsed.assistantName}`,
);
fs.writeFileSync(mdFile, content);
logger.info({ file: mdFile }, 'Updated CLAUDE.md');
log.info('Updated CLAUDE.md', { file: mdFile });
}
}
@@ -183,7 +183,7 @@ export async function run(args: string[]): Promise<void> {
} else {
fs.writeFileSync(envFile, `ASSISTANT_NAME="${parsed.assistantName}"\n`);
}
logger.info('Set ASSISTANT_NAME in .env');
log.info('Set ASSISTANT_NAME in .env');
nameUpdated = true;
}

View File

@@ -9,7 +9,7 @@ import fs from 'fs';
import os from 'os';
import path from 'path';
import { logger } from '../src/logger.js';
import { log } from '../src/log.js';
import {
getPlatform,
getNodePath,
@@ -26,18 +26,18 @@ export async function run(_args: string[]): Promise<void> {
const nodePath = getNodePath();
const homeDir = os.homedir();
logger.info({ platform, nodePath, projectRoot }, 'Setting up service');
log.info('Setting up service', { platform, nodePath, projectRoot });
// Build first
logger.info('Building TypeScript');
log.info('Building TypeScript');
try {
execSync('npm run build', {
cwd: projectRoot,
stdio: ['ignore', 'pipe', 'pipe'],
});
logger.info('Build succeeded');
log.info('Build succeeded');
} catch {
logger.error('Build failed');
log.error('Build failed');
emitStatus('SETUP_SERVICE', {
SERVICE_TYPE: 'unknown',
NODE_PATH: nodePath,
@@ -113,15 +113,15 @@ function setupLaunchd(
</plist>`;
fs.writeFileSync(plistPath, plist);
logger.info({ plistPath }, 'Wrote launchd plist');
log.info('Wrote launchd plist', { plistPath });
try {
execSync(`launchctl load ${JSON.stringify(plistPath)}`, {
stdio: 'ignore',
});
logger.info('launchctl load succeeded');
log.info('launchctl load succeeded');
} catch {
logger.warn('launchctl load failed (may already be loaded)');
log.warn('launchctl load failed (may already be loaded)');
}
// Verify
@@ -168,7 +168,7 @@ function killOrphanedProcesses(projectRoot: string): void {
execSync(`pkill -f '${projectRoot}/dist/index\\.js' || true`, {
stdio: 'ignore',
});
logger.info('Stopped any orphaned nanoclaw processes');
log.info('Stopped any orphaned nanoclaw processes');
} catch {
// pkill not available or no orphans
}
@@ -215,13 +215,13 @@ function setupSystemd(
if (runningAsRoot) {
unitPath = '/etc/systemd/system/nanoclaw.service';
systemctlPrefix = 'systemctl';
logger.info('Running as root — installing system-level systemd unit');
log.info('Running as root — installing system-level systemd unit');
} else {
// Check if user-level systemd session is available
try {
execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
} catch {
logger.warn(
log.warn(
'systemd user session not available — falling back to nohup wrapper',
);
setupNohupFallback(projectRoot, nodePath, homeDir);
@@ -253,12 +253,12 @@ StandardError=append:${projectRoot}/logs/nanoclaw.error.log
WantedBy=${runningAsRoot ? 'multi-user.target' : 'default.target'}`;
fs.writeFileSync(unitPath, unit);
logger.info({ unitPath }, 'Wrote systemd unit');
log.info('Wrote systemd unit', { unitPath });
// Detect stale docker group before starting (user systemd only)
const dockerGroupStale = !runningAsRoot && checkDockerGroupStale();
if (dockerGroupStale) {
logger.warn(
log.warn(
'Docker group not active in systemd session — user was likely added to docker group mid-session',
);
}
@@ -271,11 +271,11 @@ WantedBy=${runningAsRoot ? 'multi-user.target' : 'default.target'}`;
if (!runningAsRoot) {
try {
execSync('loginctl enable-linger', { stdio: 'ignore' });
logger.info('Enabled loginctl linger for current user');
log.info('Enabled loginctl linger for current user');
} catch (err) {
logger.warn(
{ err },
log.warn(
'loginctl enable-linger failed — service may stop on SSH logout',
{ err },
);
}
}
@@ -284,19 +284,19 @@ WantedBy=${runningAsRoot ? 'multi-user.target' : 'default.target'}`;
try {
execSync(`${systemctlPrefix} daemon-reload`, { stdio: 'ignore' });
} catch (err) {
logger.error({ err }, 'systemctl daemon-reload failed');
log.error('systemctl daemon-reload failed', { err });
}
try {
execSync(`${systemctlPrefix} enable nanoclaw`, { stdio: 'ignore' });
} catch (err) {
logger.error({ err }, 'systemctl enable failed');
log.error('systemctl enable failed', { err });
}
try {
execSync(`${systemctlPrefix} start nanoclaw`, { stdio: 'ignore' });
} catch (err) {
logger.error({ err }, 'systemctl start failed');
log.error('systemctl start failed', { err });
}
// Verify
@@ -326,7 +326,7 @@ function setupNohupFallback(
nodePath: string,
homeDir: string,
): void {
logger.warn('No systemd detected — generating nohup wrapper script');
log.warn('No systemd detected — generating nohup wrapper script');
const wrapperPath = path.join(projectRoot, 'start-nanoclaw.sh');
const pidFile = path.join(projectRoot, 'nanoclaw.pid');
@@ -362,7 +362,7 @@ function setupNohupFallback(
const wrapper = lines.join('\n') + '\n';
fs.writeFileSync(wrapperPath, wrapper, { mode: 0o755 });
logger.info({ wrapperPath }, 'Wrote nohup wrapper script');
log.info('Wrote nohup wrapper script', { wrapperPath });
emitStatus('SETUP_SERVICE', {
SERVICE_TYPE: 'nohup',

View File

@@ -7,7 +7,7 @@ import fs from 'fs';
import path from 'path';
import { isValidTimezone } from '../src/timezone.js';
import { logger } from '../src/logger.js';
import { log } from '../src/log.js';
import { emitStatus } from './status.js';
export async function run(args: string[]): Promise<void> {
@@ -53,7 +53,7 @@ export async function run(args: string[]): Promise<void> {
} else {
fs.writeFileSync(envFile, `TZ=${resolvedTz}\n`);
}
logger.info({ timezone: resolvedTz }, 'Set TZ in .env');
log.info('Set TZ in .env', { timezone: resolvedTz });
}
emitStatus('TIMEZONE', {

View File

@@ -13,7 +13,7 @@ import Database from 'better-sqlite3';
import { STORE_DIR } from '../src/config.js';
import { readEnvFile } from '../src/env.js';
import { logger } from '../src/logger.js';
import { log } from '../src/log.js';
import {
getPlatform,
getServiceManager,
@@ -27,7 +27,7 @@ export async function run(_args: string[]): Promise<void> {
const platform = getPlatform();
const homeDir = os.homedir();
logger.info('Starting verification');
log.info('Starting verification');
// 1. Check service status
let service = 'not_found';
@@ -80,7 +80,7 @@ export async function run(_args: string[]): Promise<void> {
}
}
}
logger.info({ service }, 'Service status');
log.info('Service status', { service });
// 2. Check container runtime
let containerRuntime = 'none';
@@ -174,7 +174,7 @@ export async function run(_args: string[]): Promise<void> {
? 'success'
: 'failed';
logger.info({ status, channelAuth }, 'Verification complete');
log.info('Verification complete', { status, channelAuth });
emitStatus('VERIFY', {
SERVICE: service,