chore(skills): rebase core skills (telegram, discord, voice) to latest main and fix db schema gaps
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { readEnvFile } from './env.js';
|
import { readEnvFile } from './env.js';
|
||||||
@@ -21,7 +22,7 @@ export const SCHEDULER_POLL_INTERVAL = 60000;
|
|||||||
|
|
||||||
// Absolute paths needed for container mounts
|
// Absolute paths needed for container mounts
|
||||||
const PROJECT_ROOT = process.cwd();
|
const PROJECT_ROOT = process.cwd();
|
||||||
const HOME_DIR = process.env.HOME || '/Users/user';
|
const HOME_DIR = process.env.HOME || os.homedir();
|
||||||
|
|
||||||
// Mount security: allowlist stored OUTSIDE project root, never mounted into containers
|
// Mount security: allowlist stored OUTSIDE project root, never mounted into containers
|
||||||
export const MOUNT_ALLOWLIST_PATH = path.join(
|
export const MOUNT_ALLOWLIST_PATH = path.join(
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { execSync } from 'child_process';
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ASSISTANT_NAME,
|
ASSISTANT_NAME,
|
||||||
DATA_DIR,
|
|
||||||
DISCORD_BOT_TOKEN,
|
DISCORD_BOT_TOKEN,
|
||||||
DISCORD_ONLY,
|
DISCORD_ONLY,
|
||||||
IDLE_TIMEOUT,
|
IDLE_TIMEOUT,
|
||||||
@@ -20,6 +18,7 @@ import {
|
|||||||
writeGroupsSnapshot,
|
writeGroupsSnapshot,
|
||||||
writeTasksSnapshot,
|
writeTasksSnapshot,
|
||||||
} from './container-runner.js';
|
} from './container-runner.js';
|
||||||
|
import { cleanupOrphans, ensureContainerRuntimeRunning } from './container-runtime.js';
|
||||||
import {
|
import {
|
||||||
getAllChats,
|
getAllChats,
|
||||||
getAllRegisteredGroups,
|
getAllRegisteredGroups,
|
||||||
@@ -36,6 +35,7 @@ import {
|
|||||||
storeMessage,
|
storeMessage,
|
||||||
} from './db.js';
|
} from './db.js';
|
||||||
import { GroupQueue } from './group-queue.js';
|
import { GroupQueue } from './group-queue.js';
|
||||||
|
import { resolveGroupFolderPath } from './group-folder.js';
|
||||||
import { startIpcWatcher } from './ipc.js';
|
import { startIpcWatcher } from './ipc.js';
|
||||||
import { findChannel, formatMessages, formatOutbound } from './router.js';
|
import { findChannel, formatMessages, formatOutbound } from './router.js';
|
||||||
import { startSchedulerLoop } from './task-scheduler.js';
|
import { startSchedulerLoop } from './task-scheduler.js';
|
||||||
@@ -81,11 +81,21 @@ function saveState(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function registerGroup(jid: string, group: RegisteredGroup): void {
|
function registerGroup(jid: string, group: RegisteredGroup): void {
|
||||||
|
let groupDir: string;
|
||||||
|
try {
|
||||||
|
groupDir = resolveGroupFolderPath(group.folder);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
{ jid, folder: group.folder, err },
|
||||||
|
'Rejecting group registration with invalid folder',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
registeredGroups[jid] = group;
|
registeredGroups[jid] = group;
|
||||||
setRegisteredGroup(jid, group);
|
setRegisteredGroup(jid, group);
|
||||||
|
|
||||||
// Create group folder
|
// Create group folder
|
||||||
const groupDir = path.join(DATA_DIR, '..', 'groups', group.folder);
|
|
||||||
fs.mkdirSync(path.join(groupDir, 'logs'), { recursive: true });
|
fs.mkdirSync(path.join(groupDir, 'logs'), { recursive: true });
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -126,7 +136,10 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
|
|||||||
if (!group) return true;
|
if (!group) return true;
|
||||||
|
|
||||||
const channel = findChannel(channels, chatJid);
|
const channel = findChannel(channels, chatJid);
|
||||||
if (!channel) return true;
|
if (!channel) {
|
||||||
|
console.log(`Warning: no channel owns JID ${chatJid}, skipping messages`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const isMainGroup = group.folder === MAIN_GROUP_FOLDER;
|
const isMainGroup = group.folder === MAIN_GROUP_FOLDER;
|
||||||
|
|
||||||
@@ -187,6 +200,10 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
|
|||||||
resetIdleTimer();
|
resetIdleTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.status === 'success') {
|
||||||
|
queue.notifyIdle(chatJid);
|
||||||
|
}
|
||||||
|
|
||||||
if (result.status === 'error') {
|
if (result.status === 'error') {
|
||||||
hadError = true;
|
hadError = true;
|
||||||
}
|
}
|
||||||
@@ -266,6 +283,7 @@ async function runAgent(
|
|||||||
groupFolder: group.folder,
|
groupFolder: group.folder,
|
||||||
chatJid,
|
chatJid,
|
||||||
isMain,
|
isMain,
|
||||||
|
assistantName: ASSISTANT_NAME,
|
||||||
},
|
},
|
||||||
(proc, containerName) => queue.registerProcess(chatJid, proc, containerName, group.folder),
|
(proc, containerName) => queue.registerProcess(chatJid, proc, containerName, group.folder),
|
||||||
wrappedOnOutput,
|
wrappedOnOutput,
|
||||||
@@ -328,7 +346,10 @@ async function startMessageLoop(): Promise<void> {
|
|||||||
if (!group) continue;
|
if (!group) continue;
|
||||||
|
|
||||||
const channel = findChannel(channels, chatJid);
|
const channel = findChannel(channels, chatJid);
|
||||||
if (!channel) continue;
|
if (!channel) {
|
||||||
|
console.log(`Warning: no channel owns JID ${chatJid}, skipping messages`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const isMainGroup = group.folder === MAIN_GROUP_FOLDER;
|
const isMainGroup = group.folder === MAIN_GROUP_FOLDER;
|
||||||
const needsTrigger = !isMainGroup && group.requiresTrigger !== false;
|
const needsTrigger = !isMainGroup && group.requiresTrigger !== false;
|
||||||
@@ -363,7 +384,9 @@ async function startMessageLoop(): Promise<void> {
|
|||||||
messagesToSend[messagesToSend.length - 1].timestamp;
|
messagesToSend[messagesToSend.length - 1].timestamp;
|
||||||
saveState();
|
saveState();
|
||||||
// Show typing indicator while the container processes the piped message
|
// Show typing indicator while the container processes the piped message
|
||||||
channel.setTyping?.(chatJid, true);
|
channel.setTyping?.(chatJid, true)?.catch((err) =>
|
||||||
|
logger.warn({ chatJid, err }, 'Failed to set typing indicator'),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// No active container — enqueue for a new one
|
// No active container — enqueue for a new one
|
||||||
queue.enqueueMessageCheck(chatJid);
|
queue.enqueueMessageCheck(chatJid);
|
||||||
@@ -396,65 +419,8 @@ function recoverPendingMessages(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureContainerSystemRunning(): void {
|
function ensureContainerSystemRunning(): void {
|
||||||
try {
|
ensureContainerRuntimeRunning();
|
||||||
execSync('container system status', { stdio: 'pipe' });
|
cleanupOrphans();
|
||||||
logger.debug('Apple Container system already running');
|
|
||||||
} catch {
|
|
||||||
logger.info('Starting Apple Container system...');
|
|
||||||
try {
|
|
||||||
execSync('container system start', { stdio: 'pipe', timeout: 30000 });
|
|
||||||
logger.info('Apple Container system started');
|
|
||||||
} catch (err) {
|
|
||||||
logger.error({ err }, 'Failed to start Apple Container system');
|
|
||||||
console.error(
|
|
||||||
'\n╔════════════════════════════════════════════════════════════════╗',
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
'║ FATAL: Apple Container system failed to start ║',
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
'║ ║',
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
'║ Agents cannot run without Apple Container. To fix: ║',
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
'║ 1. Install from: https://github.com/apple/container/releases ║',
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
'║ 2. Run: container system start ║',
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
'║ 3. Restart NanoClaw ║',
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
'╚════════════════════════════════════════════════════════════════╝\n',
|
|
||||||
);
|
|
||||||
throw new Error('Apple Container system is required but failed to start');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kill and clean up orphaned NanoClaw containers from previous runs
|
|
||||||
try {
|
|
||||||
const output = execSync('container ls --format json', {
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
encoding: 'utf-8',
|
|
||||||
});
|
|
||||||
const containers: { status: string; configuration: { id: string } }[] = JSON.parse(output || '[]');
|
|
||||||
const orphans = containers
|
|
||||||
.filter((c) => c.status === 'running' && c.configuration.id.startsWith('nanoclaw-'))
|
|
||||||
.map((c) => c.configuration.id);
|
|
||||||
for (const name of orphans) {
|
|
||||||
try {
|
|
||||||
execSync(`container stop ${name}`, { stdio: 'pipe' });
|
|
||||||
} catch { /* already stopped */ }
|
|
||||||
}
|
|
||||||
if (orphans.length > 0) {
|
|
||||||
logger.info({ count: orphans.length, names: orphans }, 'Stopped orphaned containers');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn({ err }, 'Failed to clean up orphaned containers');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
@@ -502,7 +468,10 @@ async function main(): Promise<void> {
|
|||||||
onProcess: (groupJid, proc, containerName, groupFolder) => queue.registerProcess(groupJid, proc, containerName, groupFolder),
|
onProcess: (groupJid, proc, containerName, groupFolder) => queue.registerProcess(groupJid, proc, containerName, groupFolder),
|
||||||
sendMessage: async (jid, rawText) => {
|
sendMessage: async (jid, rawText) => {
|
||||||
const channel = findChannel(channels, jid);
|
const channel = findChannel(channels, jid);
|
||||||
if (!channel) return;
|
if (!channel) {
|
||||||
|
console.log(`Warning: no channel owns JID ${jid}, cannot send message`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const text = formatOutbound(rawText);
|
const text = formatOutbound(rawText);
|
||||||
if (text) await channel.sendMessage(jid, text);
|
if (text) await channel.sendMessage(jid, text);
|
||||||
},
|
},
|
||||||
@@ -521,7 +490,10 @@ async function main(): Promise<void> {
|
|||||||
});
|
});
|
||||||
queue.setProcessMessagesFn(processGroupMessages);
|
queue.setProcessMessagesFn(processGroupMessages);
|
||||||
recoverPendingMessages();
|
recoverPendingMessages();
|
||||||
startMessageLoop();
|
startMessageLoop().catch((err) => {
|
||||||
|
logger.fatal({ err }, 'Message loop crashed unexpectedly');
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guard: only run when executed directly, not when imported by tests
|
// Guard: only run when executed directly, not when imported by tests
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ export class TelegramChannel implements Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store chat metadata for discovery
|
// Store chat metadata for discovery
|
||||||
this.opts.onChatMetadata(chatJid, timestamp, chatName);
|
const isGroup = ctx.chat.type === 'group' || ctx.chat.type === 'supergroup';
|
||||||
|
this.opts.onChatMetadata(chatJid, timestamp, chatName, 'telegram', isGroup);
|
||||||
|
|
||||||
// Only deliver full message for registered groups
|
// Only deliver full message for registered groups
|
||||||
const group = this.opts.registeredGroups()[chatJid];
|
const group = this.opts.registeredGroups()[chatJid];
|
||||||
@@ -135,7 +136,8 @@ export class TelegramChannel implements Channel {
|
|||||||
'Unknown';
|
'Unknown';
|
||||||
const caption = ctx.message.caption ? ` ${ctx.message.caption}` : '';
|
const caption = ctx.message.caption ? ` ${ctx.message.caption}` : '';
|
||||||
|
|
||||||
this.opts.onChatMetadata(chatJid, timestamp);
|
const isGroup = ctx.chat.type === 'group' || ctx.chat.type === 'supergroup';
|
||||||
|
this.opts.onChatMetadata(chatJid, timestamp, undefined, 'telegram', isGroup);
|
||||||
this.opts.onMessage(chatJid, {
|
this.opts.onMessage(chatJid, {
|
||||||
id: ctx.message.message_id.toString(),
|
id: ctx.message.message_id.toString(),
|
||||||
chat_jid: chatJid,
|
chat_jid: chatJid,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { readEnvFile } from './env.js';
|
import { readEnvFile } from './env.js';
|
||||||
@@ -21,7 +22,7 @@ export const SCHEDULER_POLL_INTERVAL = 60000;
|
|||||||
|
|
||||||
// Absolute paths needed for container mounts
|
// Absolute paths needed for container mounts
|
||||||
const PROJECT_ROOT = process.cwd();
|
const PROJECT_ROOT = process.cwd();
|
||||||
const HOME_DIR = process.env.HOME || '/Users/user';
|
const HOME_DIR = process.env.HOME || os.homedir();
|
||||||
|
|
||||||
// Mount security: allowlist stored OUTSIDE project root, never mounted into containers
|
// Mount security: allowlist stored OUTSIDE project root, never mounted into containers
|
||||||
export const MOUNT_ALLOWLIST_PATH = path.join(
|
export const MOUNT_ALLOWLIST_PATH = path.join(
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { execSync } from 'child_process';
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ASSISTANT_NAME,
|
ASSISTANT_NAME,
|
||||||
DATA_DIR,
|
|
||||||
IDLE_TIMEOUT,
|
IDLE_TIMEOUT,
|
||||||
MAIN_GROUP_FOLDER,
|
MAIN_GROUP_FOLDER,
|
||||||
POLL_INTERVAL,
|
POLL_INTERVAL,
|
||||||
@@ -12,14 +10,15 @@ import {
|
|||||||
TELEGRAM_ONLY,
|
TELEGRAM_ONLY,
|
||||||
TRIGGER_PATTERN,
|
TRIGGER_PATTERN,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
import { WhatsAppChannel } from './channels/whatsapp.js';
|
|
||||||
import { TelegramChannel } from './channels/telegram.js';
|
import { TelegramChannel } from './channels/telegram.js';
|
||||||
|
import { WhatsAppChannel } from './channels/whatsapp.js';
|
||||||
import {
|
import {
|
||||||
ContainerOutput,
|
ContainerOutput,
|
||||||
runContainerAgent,
|
runContainerAgent,
|
||||||
writeGroupsSnapshot,
|
writeGroupsSnapshot,
|
||||||
writeTasksSnapshot,
|
writeTasksSnapshot,
|
||||||
} from './container-runner.js';
|
} from './container-runner.js';
|
||||||
|
import { cleanupOrphans, ensureContainerRuntimeRunning } from './container-runtime.js';
|
||||||
import {
|
import {
|
||||||
getAllChats,
|
getAllChats,
|
||||||
getAllRegisteredGroups,
|
getAllRegisteredGroups,
|
||||||
@@ -36,6 +35,7 @@ import {
|
|||||||
storeMessage,
|
storeMessage,
|
||||||
} from './db.js';
|
} from './db.js';
|
||||||
import { GroupQueue } from './group-queue.js';
|
import { GroupQueue } from './group-queue.js';
|
||||||
|
import { resolveGroupFolderPath } from './group-folder.js';
|
||||||
import { startIpcWatcher } from './ipc.js';
|
import { startIpcWatcher } from './ipc.js';
|
||||||
import { findChannel, formatMessages, formatOutbound } from './router.js';
|
import { findChannel, formatMessages, formatOutbound } from './router.js';
|
||||||
import { startSchedulerLoop } from './task-scheduler.js';
|
import { startSchedulerLoop } from './task-scheduler.js';
|
||||||
@@ -81,11 +81,21 @@ function saveState(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function registerGroup(jid: string, group: RegisteredGroup): void {
|
function registerGroup(jid: string, group: RegisteredGroup): void {
|
||||||
|
let groupDir: string;
|
||||||
|
try {
|
||||||
|
groupDir = resolveGroupFolderPath(group.folder);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
{ jid, folder: group.folder, err },
|
||||||
|
'Rejecting group registration with invalid folder',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
registeredGroups[jid] = group;
|
registeredGroups[jid] = group;
|
||||||
setRegisteredGroup(jid, group);
|
setRegisteredGroup(jid, group);
|
||||||
|
|
||||||
// Create group folder
|
// Create group folder
|
||||||
const groupDir = path.join(DATA_DIR, '..', 'groups', group.folder);
|
|
||||||
fs.mkdirSync(path.join(groupDir, 'logs'), { recursive: true });
|
fs.mkdirSync(path.join(groupDir, 'logs'), { recursive: true });
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -126,7 +136,10 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
|
|||||||
if (!group) return true;
|
if (!group) return true;
|
||||||
|
|
||||||
const channel = findChannel(channels, chatJid);
|
const channel = findChannel(channels, chatJid);
|
||||||
if (!channel) return true;
|
if (!channel) {
|
||||||
|
console.log(`Warning: no channel owns JID ${chatJid}, skipping messages`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const isMainGroup = group.folder === MAIN_GROUP_FOLDER;
|
const isMainGroup = group.folder === MAIN_GROUP_FOLDER;
|
||||||
|
|
||||||
@@ -187,6 +200,10 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
|
|||||||
resetIdleTimer();
|
resetIdleTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.status === 'success') {
|
||||||
|
queue.notifyIdle(chatJid);
|
||||||
|
}
|
||||||
|
|
||||||
if (result.status === 'error') {
|
if (result.status === 'error') {
|
||||||
hadError = true;
|
hadError = true;
|
||||||
}
|
}
|
||||||
@@ -266,6 +283,7 @@ async function runAgent(
|
|||||||
groupFolder: group.folder,
|
groupFolder: group.folder,
|
||||||
chatJid,
|
chatJid,
|
||||||
isMain,
|
isMain,
|
||||||
|
assistantName: ASSISTANT_NAME,
|
||||||
},
|
},
|
||||||
(proc, containerName) => queue.registerProcess(chatJid, proc, containerName, group.folder),
|
(proc, containerName) => queue.registerProcess(chatJid, proc, containerName, group.folder),
|
||||||
wrappedOnOutput,
|
wrappedOnOutput,
|
||||||
@@ -328,7 +346,10 @@ async function startMessageLoop(): Promise<void> {
|
|||||||
if (!group) continue;
|
if (!group) continue;
|
||||||
|
|
||||||
const channel = findChannel(channels, chatJid);
|
const channel = findChannel(channels, chatJid);
|
||||||
if (!channel) continue;
|
if (!channel) {
|
||||||
|
console.log(`Warning: no channel owns JID ${chatJid}, skipping messages`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const isMainGroup = group.folder === MAIN_GROUP_FOLDER;
|
const isMainGroup = group.folder === MAIN_GROUP_FOLDER;
|
||||||
const needsTrigger = !isMainGroup && group.requiresTrigger !== false;
|
const needsTrigger = !isMainGroup && group.requiresTrigger !== false;
|
||||||
@@ -363,7 +384,9 @@ async function startMessageLoop(): Promise<void> {
|
|||||||
messagesToSend[messagesToSend.length - 1].timestamp;
|
messagesToSend[messagesToSend.length - 1].timestamp;
|
||||||
saveState();
|
saveState();
|
||||||
// Show typing indicator while the container processes the piped message
|
// Show typing indicator while the container processes the piped message
|
||||||
channel.setTyping?.(chatJid, true);
|
channel.setTyping?.(chatJid, true)?.catch((err) =>
|
||||||
|
logger.warn({ chatJid, err }, 'Failed to set typing indicator'),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// No active container — enqueue for a new one
|
// No active container — enqueue for a new one
|
||||||
queue.enqueueMessageCheck(chatJid);
|
queue.enqueueMessageCheck(chatJid);
|
||||||
@@ -396,65 +419,8 @@ function recoverPendingMessages(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureContainerSystemRunning(): void {
|
function ensureContainerSystemRunning(): void {
|
||||||
try {
|
ensureContainerRuntimeRunning();
|
||||||
execSync('container system status', { stdio: 'pipe' });
|
cleanupOrphans();
|
||||||
logger.debug('Apple Container system already running');
|
|
||||||
} catch {
|
|
||||||
logger.info('Starting Apple Container system...');
|
|
||||||
try {
|
|
||||||
execSync('container system start', { stdio: 'pipe', timeout: 30000 });
|
|
||||||
logger.info('Apple Container system started');
|
|
||||||
} catch (err) {
|
|
||||||
logger.error({ err }, 'Failed to start Apple Container system');
|
|
||||||
console.error(
|
|
||||||
'\n╔════════════════════════════════════════════════════════════════╗',
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
'║ FATAL: Apple Container system failed to start ║',
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
'║ ║',
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
'║ Agents cannot run without Apple Container. To fix: ║',
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
'║ 1. Install from: https://github.com/apple/container/releases ║',
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
'║ 2. Run: container system start ║',
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
'║ 3. Restart NanoClaw ║',
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
'╚════════════════════════════════════════════════════════════════╝\n',
|
|
||||||
);
|
|
||||||
throw new Error('Apple Container system is required but failed to start');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kill and clean up orphaned NanoClaw containers from previous runs
|
|
||||||
try {
|
|
||||||
const output = execSync('container ls --format json', {
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
encoding: 'utf-8',
|
|
||||||
});
|
|
||||||
const containers: { status: string; configuration: { id: string } }[] = JSON.parse(output || '[]');
|
|
||||||
const orphans = containers
|
|
||||||
.filter((c) => c.status === 'running' && c.configuration.id.startsWith('nanoclaw-'))
|
|
||||||
.map((c) => c.configuration.id);
|
|
||||||
for (const name of orphans) {
|
|
||||||
try {
|
|
||||||
execSync(`container stop ${name}`, { stdio: 'pipe' });
|
|
||||||
} catch { /* already stopped */ }
|
|
||||||
}
|
|
||||||
if (orphans.length > 0) {
|
|
||||||
logger.info({ count: orphans.length, names: orphans }, 'Stopped orphaned containers');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn({ err }, 'Failed to clean up orphaned containers');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
@@ -482,18 +448,18 @@ async function main(): Promise<void> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create and connect channels
|
// Create and connect channels
|
||||||
if (!TELEGRAM_ONLY) {
|
|
||||||
whatsapp = new WhatsAppChannel(channelOpts);
|
|
||||||
channels.push(whatsapp);
|
|
||||||
await whatsapp.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TELEGRAM_BOT_TOKEN) {
|
if (TELEGRAM_BOT_TOKEN) {
|
||||||
const telegram = new TelegramChannel(TELEGRAM_BOT_TOKEN, channelOpts);
|
const telegram = new TelegramChannel(TELEGRAM_BOT_TOKEN, channelOpts);
|
||||||
channels.push(telegram);
|
channels.push(telegram);
|
||||||
await telegram.connect();
|
await telegram.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!TELEGRAM_ONLY) {
|
||||||
|
whatsapp = new WhatsAppChannel(channelOpts);
|
||||||
|
channels.push(whatsapp);
|
||||||
|
await whatsapp.connect();
|
||||||
|
}
|
||||||
|
|
||||||
// Start subsystems (independently of connection handler)
|
// Start subsystems (independently of connection handler)
|
||||||
startSchedulerLoop({
|
startSchedulerLoop({
|
||||||
registeredGroups: () => registeredGroups,
|
registeredGroups: () => registeredGroups,
|
||||||
@@ -502,7 +468,10 @@ async function main(): Promise<void> {
|
|||||||
onProcess: (groupJid, proc, containerName, groupFolder) => queue.registerProcess(groupJid, proc, containerName, groupFolder),
|
onProcess: (groupJid, proc, containerName, groupFolder) => queue.registerProcess(groupJid, proc, containerName, groupFolder),
|
||||||
sendMessage: async (jid, rawText) => {
|
sendMessage: async (jid, rawText) => {
|
||||||
const channel = findChannel(channels, jid);
|
const channel = findChannel(channels, jid);
|
||||||
if (!channel) return;
|
if (!channel) {
|
||||||
|
console.log(`Warning: no channel owns JID ${jid}, cannot send message`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const text = formatOutbound(rawText);
|
const text = formatOutbound(rawText);
|
||||||
if (text) await channel.sendMessage(jid, text);
|
if (text) await channel.sendMessage(jid, text);
|
||||||
},
|
},
|
||||||
@@ -521,7 +490,10 @@ async function main(): Promise<void> {
|
|||||||
});
|
});
|
||||||
queue.setProcessMessagesFn(processGroupMessages);
|
queue.setProcessMessagesFn(processGroupMessages);
|
||||||
recoverPendingMessages();
|
recoverPendingMessages();
|
||||||
startMessageLoop();
|
startMessageLoop().catch((err) => {
|
||||||
|
logger.fatal({ err }, 'Message loop crashed unexpectedly');
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guard: only run when executed directly, not when imported by tests
|
// Guard: only run when executed directly, not when imported by tests
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import makeWASocket, {
|
|||||||
Browsers,
|
Browsers,
|
||||||
DisconnectReason,
|
DisconnectReason,
|
||||||
WASocket,
|
WASocket,
|
||||||
|
fetchLatestWaWebVersion,
|
||||||
makeCacheableSignalKeyStore,
|
makeCacheableSignalKeyStore,
|
||||||
useMultiFileAuthState,
|
useMultiFileAuthState,
|
||||||
} from '@whiskeysockets/baileys';
|
} from '@whiskeysockets/baileys';
|
||||||
@@ -56,7 +57,12 @@ export class WhatsAppChannel implements Channel {
|
|||||||
|
|
||||||
const { state, saveCreds } = await useMultiFileAuthState(authDir);
|
const { state, saveCreds } = await useMultiFileAuthState(authDir);
|
||||||
|
|
||||||
|
const { version } = await fetchLatestWaWebVersion({}).catch((err) => {
|
||||||
|
logger.warn({ err }, 'Failed to fetch latest WA Web version, using default');
|
||||||
|
return { version: undefined };
|
||||||
|
});
|
||||||
this.sock = makeWASocket({
|
this.sock = makeWASocket({
|
||||||
|
version,
|
||||||
auth: {
|
auth: {
|
||||||
creds: state.creds,
|
creds: state.creds,
|
||||||
keys: makeCacheableSignalKeyStore(state.keys, logger),
|
keys: makeCacheableSignalKeyStore(state.keys, logger),
|
||||||
@@ -81,7 +87,7 @@ export class WhatsAppChannel implements Channel {
|
|||||||
|
|
||||||
if (connection === 'close') {
|
if (connection === 'close') {
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
const reason = (lastDisconnect?.error as any)?.output?.statusCode;
|
const reason = (lastDisconnect?.error as { output?: { statusCode?: number } })?.output?.statusCode;
|
||||||
const shouldReconnect = reason !== DisconnectReason.loggedOut;
|
const shouldReconnect = reason !== DisconnectReason.loggedOut;
|
||||||
logger.info({ reason, shouldReconnect, queuedMessages: this.outgoingQueue.length }, 'Connection closed');
|
logger.info({ reason, shouldReconnect, queuedMessages: this.outgoingQueue.length }, 'Connection closed');
|
||||||
|
|
||||||
@@ -104,7 +110,9 @@ export class WhatsAppChannel implements Channel {
|
|||||||
logger.info('Connected to WhatsApp');
|
logger.info('Connected to WhatsApp');
|
||||||
|
|
||||||
// Announce availability so WhatsApp relays subsequent presence updates (typing indicators)
|
// Announce availability so WhatsApp relays subsequent presence updates (typing indicators)
|
||||||
this.sock.sendPresenceUpdate('available').catch(() => {});
|
this.sock.sendPresenceUpdate('available').catch((err) => {
|
||||||
|
logger.warn({ err }, 'Failed to send presence update');
|
||||||
|
});
|
||||||
|
|
||||||
// Build LID to phone mapping from auth state for self-chat translation
|
// Build LID to phone mapping from auth state for self-chat translation
|
||||||
if (this.sock.user) {
|
if (this.sock.user) {
|
||||||
@@ -172,6 +180,10 @@ export class WhatsAppChannel implements Channel {
|
|||||||
msg.message?.videoMessage?.caption ||
|
msg.message?.videoMessage?.caption ||
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
// Skip protocol messages with no text content (encryption keys, read receipts, etc.)
|
||||||
|
// but allow voice messages through for transcription
|
||||||
|
if (!content && !isVoiceMessage(msg)) continue;
|
||||||
|
|
||||||
const sender = msg.key.participant || msg.key.remoteJid || '';
|
const sender = msg.key.participant || msg.key.remoteJid || '';
|
||||||
const senderName = msg.pushName || sender.split('@')[0];
|
const senderName = msg.pushName || sender.split('@')[0];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user